|
1 # -*- coding: utf-8 -*- |
|
2 |
|
3 # Copyright (c) 2003 - 2009 Detlev Offenbach <detlev@die-offenbachs.de> |
|
4 # |
|
5 |
|
6 """ |
|
7 Module implementing a dialog to send bug reports. |
|
8 """ |
|
9 |
|
10 import sys |
|
11 import os |
|
12 import mimetypes |
|
13 import smtplib |
|
14 import socket |
|
15 |
|
16 from PyQt4.QtCore import * |
|
17 from PyQt4.QtGui import * |
|
18 |
|
19 from Ui_EmailDialog import Ui_EmailDialog |
|
20 |
|
21 from Info import Program, Version, BugAddress, FeatureAddress |
|
22 import Preferences |
|
23 import Utilities |
|
24 |
|
25 from email import Encoders |
|
26 from email.MIMEBase import MIMEBase |
|
27 from email.MIMEText import MIMEText |
|
28 from email.MIMEImage import MIMEImage |
|
29 from email.MIMEAudio import MIMEAudio |
|
30 from email.MIMEMultipart import MIMEMultipart |
|
31 |
|
32 class EmailDialog(QDialog, Ui_EmailDialog): |
|
33 """ |
|
34 Class implementing a dialog to send bug reports. |
|
35 """ |
|
36 def __init__(self, mode = "bug", parent = None): |
|
37 """ |
|
38 Constructor |
|
39 |
|
40 @param mode mode of this dialog (string, "bug" or "feature") |
|
41 @param parent parent widget of this dialog (QWidget) |
|
42 """ |
|
43 QDialog.__init__(self, parent) |
|
44 self.setupUi(self) |
|
45 |
|
46 self.__mode = mode |
|
47 if self.__mode == "feature": |
|
48 self.setWindowTitle(self.trUtf8("Send feature request")) |
|
49 self.msgLabel.setText(self.trUtf8( |
|
50 "Enter your &feature request below." |
|
51 " Version information is added automatically.")) |
|
52 self.__toAddress = FeatureAddress |
|
53 else: |
|
54 # default is bug |
|
55 self.msgLabel.setText(self.trUtf8( |
|
56 "Enter your &bug description below." |
|
57 " Version information is added automatically.")) |
|
58 self.__toAddress = BugAddress |
|
59 |
|
60 self.sendButton = \ |
|
61 self.buttonBox.addButton(self.trUtf8("Send"), QDialogButtonBox.ActionRole) |
|
62 self.sendButton.setEnabled(False) |
|
63 self.sendButton.setDefault(True) |
|
64 |
|
65 height = self.height() |
|
66 self.mainSplitter.setSizes([int(0.7 * height), int(0.3 * height)]) |
|
67 |
|
68 self.attachments.headerItem().setText(self.attachments.columnCount(), "") |
|
69 self.attachments.header().setResizeMode(QHeaderView.Interactive) |
|
70 |
|
71 sig = Preferences.getUser("Signature") |
|
72 if sig: |
|
73 self.message.setPlainText(sig) |
|
74 cursor = self.message.textCursor() |
|
75 cursor.setPosition(0) |
|
76 self.message.setTextCursor(cursor) |
|
77 self.message.ensureCursorVisible() |
|
78 |
|
79 self.__deleteFiles = [] |
|
80 |
|
81 def keyPressEvent(self, ev): |
|
82 """ |
|
83 Re-implemented to handle the user pressing the escape key. |
|
84 |
|
85 @param ev key event (QKeyEvent) |
|
86 """ |
|
87 if ev.key() == Qt.Key_Escape: |
|
88 res = QMessageBox.question(self, |
|
89 self.trUtf8("Close dialog"), |
|
90 self.trUtf8("""Do you really want to close the dialog?"""), |
|
91 QMessageBox.StandardButtons(\ |
|
92 QMessageBox.No | \ |
|
93 QMessageBox.Yes), |
|
94 QMessageBox.No) |
|
95 if res == QMessageBox.Yes: |
|
96 self.reject() |
|
97 |
|
98 def on_buttonBox_clicked(self, button): |
|
99 """ |
|
100 Private slot called by a button of the button box clicked. |
|
101 |
|
102 @param button button that was clicked (QAbstractButton) |
|
103 """ |
|
104 if button == self.sendButton: |
|
105 self.on_sendButton_clicked() |
|
106 |
|
107 def on_buttonBox_rejected(self): |
|
108 """ |
|
109 Private slot to handle the rejected signal of the button box. |
|
110 """ |
|
111 res = QMessageBox.question(self, |
|
112 self.trUtf8("Close dialog"), |
|
113 self.trUtf8("""Do you really want to close the dialog?"""), |
|
114 QMessageBox.StandardButtons(\ |
|
115 QMessageBox.No | \ |
|
116 QMessageBox.Yes), |
|
117 QMessageBox.No) |
|
118 if res == QMessageBox.Yes: |
|
119 self.reject() |
|
120 |
|
121 @pyqtSlot() |
|
122 def on_sendButton_clicked(self): |
|
123 """ |
|
124 Private slot to send the email message. |
|
125 """ |
|
126 if self.attachments.topLevelItemCount(): |
|
127 msg = self.__createMultipartMail() |
|
128 else: |
|
129 msg = self.__createSimpleMail() |
|
130 |
|
131 ok = self.__sendmail(msg) |
|
132 |
|
133 if ok: |
|
134 for f in self.__deleteFiles: |
|
135 try: |
|
136 os.remove(f) |
|
137 except OSError: |
|
138 pass |
|
139 self.accept() |
|
140 |
|
141 def __createSimpleMail(self): |
|
142 """ |
|
143 Private method to create a simple mail message. |
|
144 |
|
145 @return string containing the mail message |
|
146 """ |
|
147 try: |
|
148 import sipconfig |
|
149 sip_version_str = sipconfig.Configuration().sip_version_str |
|
150 except ImportError: |
|
151 sip_version_str = "sip version not available" |
|
152 |
|
153 msgtext = "%s\r\n----\r\n%s----\r\n%s----\r\n%s" % \ |
|
154 (self.message.toPlainText(), |
|
155 Utilities.generateVersionInfo("\r\n"), |
|
156 Utilities.generatePluginsVersionInfo("\r\n"), |
|
157 Utilities.generateDistroInfo("\r\n")) |
|
158 |
|
159 msg = MIMEText(msgtext, |
|
160 _charset = Preferences.getSystem("StringEncoding")) |
|
161 msg['From'] = Preferences.getUser("Email") |
|
162 msg['To'] = self.__toAddress |
|
163 msg['Subject'] = '[eric5] %s' % self.subject.text() |
|
164 |
|
165 return msg.as_string() |
|
166 |
|
167 def __createMultipartMail(self): |
|
168 """ |
|
169 Private method to create a multipart mail message. |
|
170 |
|
171 @return string containing the mail message |
|
172 """ |
|
173 try: |
|
174 import sipconfig |
|
175 sip_version_str = sipconfig.Configuration().sip_version_str |
|
176 except ImportError: |
|
177 sip_version_str = "sip version not available" |
|
178 |
|
179 mpPreamble = ("This is a MIME-encoded message with attachments. " |
|
180 "If you see this message, your mail client is not " |
|
181 "capable of displaying the attachments.") |
|
182 |
|
183 msgtext = "%s\r\n----\r\n%s----\r\n%s----\r\n%s" % \ |
|
184 (self.message.toPlainText(), |
|
185 Utilities.generateVersionInfo("\r\n"), |
|
186 Utilities.generatePluginsVersionInfo("\r\n"), |
|
187 Utilities.generateDistroInfo("\r\n")) |
|
188 |
|
189 # first part of multipart mail explains format |
|
190 msg = MIMEMultipart() |
|
191 msg['From'] = Preferences.getUser("Email") |
|
192 msg['To'] = self.__toAddress |
|
193 msg['Subject'] = '[eric5] %s' % self.subject.text() |
|
194 msg.preamble = mpPreamble |
|
195 msg.epilogue = '' |
|
196 |
|
197 # second part is intended to be read |
|
198 att = MIMEText(msgtext, |
|
199 _charset = Preferences.getSystem("StringEncoding")) |
|
200 msg.attach(att) |
|
201 |
|
202 # next parts contain the attachments |
|
203 for index in range(self.attachments.topLevelItemCount()): |
|
204 itm = self.attachments.topLevelItem(index) |
|
205 maintype, subtype = str(itm.text(1)).split('/', 1) |
|
206 fname = itm.text(0) |
|
207 name = os.path.basename(fname) |
|
208 |
|
209 if maintype == 'text': |
|
210 att = MIMEText(open(fname, 'rb').read(), _subtype = subtype) |
|
211 elif maintype == 'image': |
|
212 att = MIMEImage(open(fname, 'rb').read(), _subtype = subtype) |
|
213 elif maintype == 'audio': |
|
214 att = MIMEAudio(open(fname, 'rb').read(), _subtype = subtype) |
|
215 else: |
|
216 att = MIMEBase(maintype, subtype) |
|
217 att.set_payload(open(fname, 'rb').read()) |
|
218 Encoders.encode_base64(att) |
|
219 att.add_header('Content-Disposition', 'attachment', filename = fname) |
|
220 msg.attach(att) |
|
221 |
|
222 return msg.as_string() |
|
223 |
|
224 def __sendmail(self, msg): |
|
225 """ |
|
226 Private method to actually send the message. |
|
227 |
|
228 @param msg the message to be sent (string) |
|
229 @return flag indicating success (boolean) |
|
230 """ |
|
231 try: |
|
232 server = smtplib.SMTP(str(Preferences.getUser("MailServer")), |
|
233 Preferences.getUser("MailServerPort")) |
|
234 if Preferences.getUser("MailServerUseTLS"): |
|
235 server.starttls() |
|
236 if Preferences.getUser("MailServerAuthentication"): |
|
237 # mail server needs authentication |
|
238 password = Preferences.getUser("MailServerPassword") |
|
239 if not password: |
|
240 password, ok = QInputDialog.getText(\ |
|
241 self, |
|
242 self.trUtf8("Mail Server Password"), |
|
243 self.trUtf8("Enter your mail server password"), |
|
244 QLineEdit.Password) |
|
245 if not ok: |
|
246 # abort |
|
247 return False |
|
248 try: |
|
249 server.login(Preferences.getUser("MailServerUser"), |
|
250 password) |
|
251 except (smtplib.SMTPException, socket.error), e: |
|
252 res = QMessageBox.critical(self, |
|
253 self.trUtf8("Send bug report"), |
|
254 self.trUtf8("""<p>Authentication failed.<br>Reason: {0}</p>""") |
|
255 .format(unicode(e)), |
|
256 QMessageBox.StandardButtons(\ |
|
257 QMessageBox.Abort | \ |
|
258 QMessageBox.Retry), |
|
259 QMessageBox.Retry) |
|
260 if res == QMessageBox.Retry: |
|
261 return self.__sendmail(msg) |
|
262 else: |
|
263 return False |
|
264 |
|
265 QApplication.setOverrideCursor(QCursor(Qt.WaitCursor)) |
|
266 QApplication.processEvents() |
|
267 result = server.sendmail(Preferences.getUser("Email"), |
|
268 self.__toAddress, msg) |
|
269 server.quit() |
|
270 QApplication.restoreOverrideCursor() |
|
271 except (smtplib.SMTPException, socket.error), e: |
|
272 QApplication.restoreOverrideCursor() |
|
273 res = QMessageBox.critical(self, |
|
274 self.trUtf8("Send bug report"), |
|
275 self.trUtf8("""<p>Message could not be sent.<br>Reason: {0}</p>""") |
|
276 .format(unicode(e)), |
|
277 QMessageBox.StandardButtons(\ |
|
278 QMessageBox.Abort | \ |
|
279 QMessageBox.Retry), |
|
280 QMessageBox.Retry) |
|
281 if res == QMessageBox.Retry: |
|
282 return self.__sendmail(msg) |
|
283 else: |
|
284 return False |
|
285 return True |
|
286 |
|
287 @pyqtSlot() |
|
288 def on_addButton_clicked(self): |
|
289 """ |
|
290 Private slot to handle the Add... button. |
|
291 """ |
|
292 fname = QFileDialog.getOpenFileName(\ |
|
293 self, |
|
294 self.trUtf8("Attach file")) |
|
295 if fname: |
|
296 self.attachFile(fname, False) |
|
297 |
|
298 def attachFile(self, fname, deleteFile): |
|
299 """ |
|
300 Public method to add an attachment. |
|
301 |
|
302 @param fname name of the file to be attached (string) |
|
303 @param deleteFile flag indicating to delete the file after it has |
|
304 been sent (boolean) |
|
305 """ |
|
306 type = mimetypes.guess_type(fname)[0] |
|
307 if not type: |
|
308 type = "application/octet-stream" |
|
309 itm = QTreeWidgetItem(self.attachments, [fname, type]) |
|
310 self.attachments.header().resizeSections(QHeaderView.ResizeToContents) |
|
311 self.attachments.header().setStretchLastSection(True) |
|
312 |
|
313 if deleteFile: |
|
314 self.__deleteFiles.append(fname) |
|
315 |
|
316 @pyqtSlot() |
|
317 def on_deleteButton_clicked(self): |
|
318 """ |
|
319 Private slot to handle the Delete button. |
|
320 """ |
|
321 itm = self.attachments.currentItem() |
|
322 if itm is not None: |
|
323 itm = self.attachments.takeTopLevelItem(\ |
|
324 self.attachments.indexOfTopLevelItem(itm)) |
|
325 del itm |
|
326 |
|
327 def on_subject_textChanged(self, txt): |
|
328 """ |
|
329 Private slot to handle the textChanged signal of the subject edit. |
|
330 |
|
331 @param txt changed text (string) |
|
332 """ |
|
333 self.sendButton.setEnabled(\ |
|
334 self.subject.text() != "" and \ |
|
335 self.message.toPlainText() != "") |
|
336 |
|
337 def on_message_textChanged(self): |
|
338 """ |
|
339 Private slot to handle the textChanged signal of the message edit. |
|
340 |
|
341 @param txt changed text (string) |
|
342 """ |
|
343 self.sendButton.setEnabled(\ |
|
344 self.subject.text() != "" and \ |
|
345 self.message.toPlainText() != "") |