src/eric7/UI/EmailDialog.py

branch
eric7
changeset 9209
b99e7fd55fd3
parent 8881
54e42bc2437a
child 9221
bf71ee032bb4
equal deleted inserted replaced
9208:3fc8dfeb6ebe 9209:b99e7fd55fd3
1 # -*- coding: utf-8 -*-
2
3 # Copyright (c) 2003 - 2022 Detlev Offenbach <detlev@die-offenbachs.de>
4 #
5
6 """
7 Module implementing a dialog to send bug reports or feature requests.
8 """
9
10 import os
11 import mimetypes
12 import smtplib
13 import contextlib
14
15 from PyQt6.QtCore import Qt, pyqtSlot
16 from PyQt6.QtGui import QTextOption
17 from PyQt6.QtWidgets import (
18 QHeaderView, QLineEdit, QDialog, QInputDialog, QDialogButtonBox,
19 QTreeWidgetItem
20 )
21
22 from EricWidgets import EricMessageBox, EricFileDialog
23 from EricGui.EricOverrideCursor import EricOverrideCursor
24
25 from .Ui_EmailDialog import Ui_EmailDialog
26
27 from .Info import BugAddress, FeatureAddress
28 import Preferences
29 import Utilities
30
31 from email.mime.text import MIMEText
32 from email.mime.image import MIMEImage
33 from email.mime.audio import MIMEAudio
34 from email.mime.application import MIMEApplication
35 from email.mime.multipart import MIMEMultipart
36 from email.header import Header
37
38
39 ############################################################
40 ## This code is to work around a bug in the Python email ##
41 ## package for Image and Audio mime messages. ##
42 ############################################################
43
44
45 class EmailDialog(QDialog, Ui_EmailDialog):
46 """
47 Class implementing a dialog to send bug reports or feature requests.
48 """
49 def __init__(self, mode="bug", parent=None):
50 """
51 Constructor
52
53 @param mode mode of this dialog (string, "bug" or "feature")
54 @param parent parent widget of this dialog (QWidget)
55 """
56 super().__init__(parent)
57 self.setupUi(self)
58 self.setWindowFlags(Qt.WindowType.Window)
59
60 self.message.setWordWrapMode(QTextOption.WrapMode.WordWrap)
61
62 self.__mode = mode
63 if self.__mode == "feature":
64 self.setWindowTitle(self.tr("Send feature request"))
65 self.msgLabel.setText(self.tr(
66 "Enter your &feature request below."
67 " Version information is added automatically."))
68 self.__toAddress = FeatureAddress
69 else:
70 # default is bug
71 self.msgLabel.setText(self.tr(
72 "Enter your &bug description below."
73 " Version information is added automatically."))
74 self.__toAddress = BugAddress
75
76 self.sendButton = self.buttonBox.addButton(
77 self.tr("Send"), QDialogButtonBox.ButtonRole.ActionRole)
78 self.sendButton.setEnabled(False)
79 self.sendButton.setDefault(True)
80
81 self.googleHelpButton = self.buttonBox.addButton(
82 self.tr("Google Mail API Help"),
83 QDialogButtonBox.ButtonRole.HelpRole)
84
85 height = self.height()
86 self.mainSplitter.setSizes([int(0.7 * height), int(0.3 * height)])
87
88 self.attachments.headerItem().setText(
89 self.attachments.columnCount(), "")
90 self.attachments.header().setSectionResizeMode(
91 QHeaderView.ResizeMode.Interactive)
92
93 sig = Preferences.getUser("Signature")
94 if sig:
95 self.message.setPlainText(sig)
96 cursor = self.message.textCursor()
97 cursor.setPosition(0)
98 self.message.setTextCursor(cursor)
99 self.message.ensureCursorVisible()
100
101 self.__deleteFiles = []
102
103 self.__helpDialog = None
104 self.__googleMail = None
105
106 def keyPressEvent(self, ev):
107 """
108 Protected method to handle the user pressing the escape key.
109
110 @param ev key event (QKeyEvent)
111 """
112 if ev.key() == Qt.Key.Key_Escape:
113 res = EricMessageBox.yesNo(
114 self,
115 self.tr("Close dialog"),
116 self.tr("""Do you really want to close the dialog?"""))
117 if res:
118 self.reject()
119
120 def on_buttonBox_clicked(self, button):
121 """
122 Private slot called by a button of the button box clicked.
123
124 @param button button that was clicked (QAbstractButton)
125 """
126 if button == self.sendButton:
127 self.on_sendButton_clicked()
128 elif button == self.googleHelpButton:
129 self.on_googleHelpButton_clicked()
130
131 def on_buttonBox_rejected(self):
132 """
133 Private slot to handle the rejected signal of the button box.
134 """
135 res = EricMessageBox.yesNo(
136 self,
137 self.tr("Close dialog"),
138 self.tr("""Do you really want to close the dialog?"""))
139 if res:
140 self.reject()
141
142 @pyqtSlot()
143 def on_googleHelpButton_clicked(self):
144 """
145 Private slot to show some help text "how to turn on the Gmail API".
146 """
147 if self.__helpDialog is None:
148 try:
149 from EricNetwork.EricGoogleMail import GoogleMailHelp
150 helpStr = GoogleMailHelp()
151 except ImportError:
152 from EricNetwork.EricGoogleMailHelpers import getInstallCommand
153 helpStr = self.tr(
154 "<p>The Google Mail Client API is not installed."
155 " Use <code>{0}</code> to install it.</p>"
156 ).format(getInstallCommand())
157
158 from EricWidgets.EricSimpleHelpDialog import EricSimpleHelpDialog
159 self.__helpDialog = EricSimpleHelpDialog(
160 title=self.tr("Gmail API Help"),
161 helpStr=helpStr, parent=self)
162
163 self.__helpDialog.show()
164
165 @pyqtSlot()
166 def on_sendButton_clicked(self):
167 """
168 Private slot to send the email message.
169 """
170 msg = (
171 self.__createMultipartMail()
172 if self.attachments.topLevelItemCount() else
173 self.__createSimpleMail()
174 )
175
176 if Preferences.getUser("UseGoogleMailOAuth2"):
177 self.__sendmailGoogle(msg)
178 else:
179 ok = self.__sendmail(msg.as_string())
180 if ok:
181 self.__deleteAttachedFiles()
182 self.accept()
183
184 def __deleteAttachedFiles(self):
185 """
186 Private method to delete attached files.
187 """
188 for f in self.__deleteFiles:
189 with contextlib.suppress(OSError):
190 os.remove(f)
191
192 def __encodedText(self, txt):
193 """
194 Private method to create a MIMEText message with correct encoding.
195
196 @param txt text to be put into the MIMEText object (string)
197 @return MIMEText object
198 """
199 try:
200 txt.encode("us-ascii")
201 return MIMEText(txt)
202 except UnicodeEncodeError:
203 coding = Preferences.getSystem("StringEncoding")
204 return MIMEText(txt.encode(coding), _charset=coding)
205
206 def __encodedHeader(self, txt):
207 """
208 Private method to create a correctly encoded mail header.
209
210 @param txt header text to encode (string)
211 @return encoded header (email.header.Header)
212 """
213 try:
214 txt.encode("us-ascii")
215 return Header(txt)
216 except UnicodeEncodeError:
217 coding = Preferences.getSystem("StringEncoding")
218 return Header(txt, coding)
219
220 def __createSimpleMail(self):
221 """
222 Private method to create a simple mail message.
223
224 @return prepared mail message
225 @rtype email.mime.text.MIMEText
226 """
227 msgtext = "{0}\r\n----\r\n{1}\r\n----\r\n{2}\r\n----\r\n{3}".format(
228 self.message.toPlainText(),
229 Utilities.generateVersionInfo("\r\n"),
230 Utilities.generatePluginsVersionInfo("\r\n"),
231 Utilities.generateDistroInfo("\r\n"))
232
233 msg = self.__encodedText(msgtext)
234 msg['From'] = Preferences.getUser("Email")
235 msg['To'] = self.__toAddress
236 subject = '[eric7] {0}'.format(self.subject.text())
237 msg['Subject'] = self.__encodedHeader(subject)
238
239 return msg
240
241 def __createMultipartMail(self):
242 """
243 Private method to create a multipart mail message.
244
245 @return prepared mail message
246 @rtype email.mime.text.MIMEMultipart
247 """
248 mpPreamble = ("This is a MIME-encoded message with attachments. "
249 "If you see this message, your mail client is not "
250 "capable of displaying the attachments.")
251
252 msgtext = "{0}\r\n----\r\n{1}\r\n----\r\n{2}\r\n----\r\n{3}".format(
253 self.message.toPlainText(),
254 Utilities.generateVersionInfo("\r\n"),
255 Utilities.generatePluginsVersionInfo("\r\n"),
256 Utilities.generateDistroInfo("\r\n"))
257
258 # first part of multipart mail explains format
259 msg = MIMEMultipart()
260 msg['From'] = Preferences.getUser("Email")
261 msg['To'] = self.__toAddress
262 subject = '[eric7] {0}'.format(self.subject.text())
263 msg['Subject'] = self.__encodedHeader(subject)
264 msg.preamble = mpPreamble
265 msg.epilogue = ''
266
267 # second part is intended to be read
268 att = self.__encodedText(msgtext)
269 msg.attach(att)
270
271 # next parts contain the attachments
272 for index in range(self.attachments.topLevelItemCount()):
273 itm = self.attachments.topLevelItem(index)
274 maintype, subtype = itm.text(1).split('/', 1)
275 fname = itm.text(0)
276 name = os.path.basename(fname)
277
278 if maintype == 'text':
279 with open(fname, 'r', encoding="utf-8") as f:
280 txt = f.read()
281 try:
282 txt.encode("us-ascii")
283 att = MIMEText(txt, _subtype=subtype)
284 except UnicodeEncodeError:
285 att = MIMEText(
286 txt.encode("utf-8"), _subtype=subtype,
287 _charset="utf-8")
288 elif maintype == 'image':
289 with open(fname, 'rb') as f:
290 att = MIMEImage(f.read(), _subtype=subtype)
291 elif maintype == 'audio':
292 with open(fname, 'rb') as f:
293 att = MIMEAudio(f.read(), _subtype=subtype)
294 else:
295 with open(fname, 'rb') as f:
296 att = MIMEApplication(f.read())
297 att.add_header('Content-Disposition', 'attachment', filename=name)
298 msg.attach(att)
299
300 return msg
301
302 def __sendmail(self, msg):
303 """
304 Private method to actually send the message.
305
306 @param msg the message to be sent (string)
307 @return flag indicating success (boolean)
308 """
309 try:
310 encryption = Preferences.getUser("MailServerEncryption")
311 if encryption == "SSL":
312 server = smtplib.SMTP_SSL(
313 Preferences.getUser("MailServer"),
314 Preferences.getUser("MailServerPort"))
315 else:
316 server = smtplib.SMTP(
317 Preferences.getUser("MailServer"),
318 Preferences.getUser("MailServerPort"))
319 if encryption == "TLS":
320 server.starttls()
321 if Preferences.getUser("MailServerAuthentication"):
322 # mail server needs authentication
323 password = Preferences.getUser("MailServerPassword")
324 if not password:
325 password, ok = QInputDialog.getText(
326 self,
327 self.tr("Mail Server Password"),
328 self.tr("Enter your mail server password"),
329 QLineEdit.EchoMode.Password)
330 if not ok:
331 # abort
332 return False
333 try:
334 server.login(Preferences.getUser("MailServerUser"),
335 password)
336 except (smtplib.SMTPException, OSError) as e:
337 if isinstance(e, smtplib.SMTPResponseException):
338 errorStr = e.smtp_error.decode()
339 elif isinstance(e, OSError):
340 errorStr = e.strerror
341 elif isinstance(e, OSError):
342 errorStr = e[1]
343 else:
344 errorStr = str(e)
345 res = EricMessageBox.retryAbort(
346 self,
347 self.tr("Send Message"),
348 self.tr(
349 """<p>Authentication failed.<br>Reason: {0}</p>""")
350 .format(errorStr),
351 EricMessageBox.Critical)
352 if res:
353 return self.__sendmail(msg)
354 else:
355 return False
356
357 with EricOverrideCursor():
358 server.sendmail(Preferences.getUser("Email"), self.__toAddress,
359 msg)
360 server.quit()
361 except (smtplib.SMTPException, OSError) as e:
362 if isinstance(e, smtplib.SMTPResponseException):
363 errorStr = e.smtp_error.decode()
364 elif isinstance(e, smtplib.SMTPException):
365 errorStr = str(e)
366 elif isinstance(e, OSError):
367 errorStr = e.strerror
368 else:
369 errorStr = str(e)
370 res = EricMessageBox.retryAbort(
371 self,
372 self.tr("Send Message"),
373 self.tr(
374 """<p>Message could not be sent.<br>Reason: {0}</p>""")
375 .format(errorStr),
376 EricMessageBox.Critical)
377 if res:
378 return self.__sendmail(msg)
379 else:
380 return False
381 return True
382
383 def __sendmailGoogle(self, msg):
384 """
385 Private method to actually send the message via Google Mail.
386
387 @param msg email message to be sent
388 @type email.mime.text.MIMEBase
389 """
390 from EricNetwork.EricGoogleMail import EricGoogleMail
391
392 if self.__googleMail is None:
393 self.__googleMail = EricGoogleMail(self)
394 self.__googleMail.sendResult.connect(self.__gmailSendResult)
395
396 self.__googleMail.sendMessage(msg)
397
398 @pyqtSlot(bool, str)
399 def __gmailSendResult(self, ok, message):
400 """
401 Private slot handling the send result reported by the Google Mail
402 interface.
403
404 @param ok flag indicating success
405 @type bool
406 @param message message from the interface
407 @type str
408 """
409 if ok:
410 self.__deleteAttachedFiles()
411 self.accept()
412 else:
413 # we got an error
414 EricMessageBox.critical(
415 self,
416 self.tr("Send Message via Gmail"),
417 self.tr(
418 """<p>Message could not be sent.<br>Reason: {0}</p>""")
419 .format(message)
420 )
421
422 @pyqtSlot()
423 def on_addButton_clicked(self):
424 """
425 Private slot to handle the Add... button.
426 """
427 fname = EricFileDialog.getOpenFileName(
428 self,
429 self.tr("Attach file"))
430 if fname:
431 self.attachFile(fname, False)
432
433 def attachFile(self, fname, deleteFile):
434 """
435 Public method to add an attachment.
436
437 @param fname name of the file to be attached (string)
438 @param deleteFile flag indicating to delete the file after it has
439 been sent (boolean)
440 """
441 mimeType = mimetypes.guess_type(fname)[0]
442 if not mimeType:
443 mimeType = "application/octet-stream"
444 QTreeWidgetItem(self.attachments, [fname, mimeType])
445 self.attachments.header().resizeSections(
446 QHeaderView.ResizeMode.ResizeToContents)
447 self.attachments.header().setStretchLastSection(True)
448
449 if deleteFile:
450 self.__deleteFiles.append(fname)
451
452 @pyqtSlot()
453 def on_deleteButton_clicked(self):
454 """
455 Private slot to handle the Delete button.
456 """
457 itm = self.attachments.currentItem()
458 if itm is not None:
459 itm = self.attachments.takeTopLevelItem(
460 self.attachments.indexOfTopLevelItem(itm))
461 del itm
462
463 def on_subject_textChanged(self, txt):
464 """
465 Private slot to handle the textChanged signal of the subject edit.
466
467 @param txt changed text (string)
468 """
469 self.sendButton.setEnabled(
470 self.subject.text() != "" and
471 self.message.toPlainText() != "")
472
473 def on_message_textChanged(self):
474 """
475 Private slot to handle the textChanged signal of the message edit.
476 """
477 self.sendButton.setEnabled(
478 self.subject.text() != "" and
479 self.message.toPlainText() != "")

eric ide

mercurial