eric6/UI/EmailDialog.py

changeset 6942
2602857055c5
parent 6834
ae1e7530c854
child 7198
684261ef2165
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/eric6/UI/EmailDialog.py	Sun Apr 14 15:09:21 2019 +0200
@@ -0,0 +1,481 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (c) 2003 - 2019 Detlev Offenbach <detlev@die-offenbachs.de>
+#
+
+"""
+Module implementing a dialog to send bug reports or feature requests.
+"""
+
+from __future__ import unicode_literals
+
+import os
+import mimetypes
+import smtplib
+import socket
+
+from PyQt5.QtCore import Qt, pyqtSlot
+from PyQt5.QtGui import QCursor, QTextOption
+from PyQt5.QtWidgets import QHeaderView, QLineEdit, QDialog, QInputDialog, \
+    QApplication, QDialogButtonBox, QTreeWidgetItem
+
+from E5Gui import E5MessageBox, E5FileDialog
+
+from .Ui_EmailDialog import Ui_EmailDialog
+
+from .Info import BugAddress, FeatureAddress
+import Preferences
+import Utilities
+
+from email.mime.text import MIMEText
+from email.mime.image import MIMEImage
+from email.mime.audio import MIMEAudio
+from email.mime.application import MIMEApplication
+from email.mime.multipart import MIMEMultipart
+from email.header import Header
+
+from Globals import qVersionTuple
+
+
+############################################################
+## This code is to work around a bug in the Python email  ##
+## package for Image and Audio mime messages.             ##
+############################################################
+
+
+class EmailDialog(QDialog, Ui_EmailDialog):
+    """
+    Class implementing a dialog to send bug reports or feature requests.
+    """
+    def __init__(self, mode="bug", parent=None):
+        """
+        Constructor
+        
+        @param mode mode of this dialog (string, "bug" or "feature")
+        @param parent parent widget of this dialog (QWidget)
+        """
+        super(EmailDialog, self).__init__(parent)
+        self.setupUi(self)
+        self.setWindowFlags(Qt.Window)
+        
+        self.message.setWordWrapMode(QTextOption.WordWrap)
+        
+        self.__mode = mode
+        if self.__mode == "feature":
+            self.setWindowTitle(self.tr("Send feature request"))
+            self.msgLabel.setText(self.tr(
+                "Enter your &feature request below."
+                " Version information is added automatically."))
+            self.__toAddress = FeatureAddress
+        else:
+            # default is bug
+            self.msgLabel.setText(self.tr(
+                "Enter your &bug description below."
+                " Version information is added automatically."))
+            self.__toAddress = BugAddress
+        
+        self.sendButton = self.buttonBox.addButton(
+            self.tr("Send"), QDialogButtonBox.ActionRole)
+        self.sendButton.setEnabled(False)
+        self.sendButton.setDefault(True)
+        
+        self.googleHelpButton = self.buttonBox.addButton(
+            self.tr("Google Mail API Help"), QDialogButtonBox.HelpRole)
+        
+        height = self.height()
+        self.mainSplitter.setSizes([int(0.7 * height), int(0.3 * height)])
+        
+        self.attachments.headerItem().setText(
+            self.attachments.columnCount(), "")
+        if qVersionTuple() >= (5, 0, 0):
+            self.attachments.header().setSectionResizeMode(
+                QHeaderView.Interactive)
+        else:
+            self.attachments.header().setResizeMode(QHeaderView.Interactive)
+        
+        sig = Preferences.getUser("Signature")
+        if sig:
+            self.message.setPlainText(sig)
+            cursor = self.message.textCursor()
+            cursor.setPosition(0)
+            self.message.setTextCursor(cursor)
+            self.message.ensureCursorVisible()
+        
+        self.__deleteFiles = []
+        
+        self.__helpDialog = None
+        self.__googleMail = None
+    
+    def keyPressEvent(self, ev):
+        """
+        Protected method to handle the user pressing the escape key.
+        
+        @param ev key event (QKeyEvent)
+        """
+        if ev.key() == Qt.Key_Escape:
+            res = E5MessageBox.yesNo(
+                self,
+                self.tr("Close dialog"),
+                self.tr("""Do you really want to close the dialog?"""))
+            if res:
+                self.reject()
+        
+    def on_buttonBox_clicked(self, button):
+        """
+        Private slot called by a button of the button box clicked.
+        
+        @param button button that was clicked (QAbstractButton)
+        """
+        if button == self.sendButton:
+            self.on_sendButton_clicked()
+        elif button == self.googleHelpButton:
+            self.on_googleHelpButton_clicked()
+        
+    def on_buttonBox_rejected(self):
+        """
+        Private slot to handle the rejected signal of the button box.
+        """
+        res = E5MessageBox.yesNo(
+            self,
+            self.tr("Close dialog"),
+            self.tr("""Do you really want to close the dialog?"""))
+        if res:
+            self.reject()
+    
+    @pyqtSlot()
+    def on_googleHelpButton_clicked(self):
+        """
+        Private slot to show some help text "how to turn on the Gmail API".
+        """
+        if self.__helpDialog is None:
+            try:
+                from E5Network.E5GoogleMail import GoogleMailHelp
+                helpStr = GoogleMailHelp()
+            except ImportError:
+                from E5Network.E5GoogleMailHelpers import getInstallCommand
+                helpStr = self.tr(
+                    "<p>The Google Mail Client API is not installed."
+                    " Use <code>{0}</code> to install it.</p>"
+                ).format(getInstallCommand())
+            
+            from E5Gui.E5SimpleHelpDialog import E5SimpleHelpDialog
+            self.__helpDialog = E5SimpleHelpDialog(
+                title=self.tr("Gmail API Help"),
+                helpStr=helpStr, parent=self)
+        
+        self.__helpDialog.show()
+    
+    @pyqtSlot()
+    def on_sendButton_clicked(self):
+        """
+        Private slot to send the email message.
+        """
+        if self.attachments.topLevelItemCount():
+            msg = self.__createMultipartMail()
+        else:
+            msg = self.__createSimpleMail()
+        
+        if Preferences.getUser("UseGoogleMailOAuth2"):
+            self.__sendmailGoogle(msg)
+        else:
+            ok = self.__sendmail(msg.as_string())
+            if ok:
+                self.__deleteAttachedFiles()
+                self.accept()
+    
+    def __deleteAttachedFiles(self):
+        """
+        Private method to delete attached files.
+        """
+        for f in self.__deleteFiles:
+            try:
+                os.remove(f)
+            except OSError:
+                pass
+    
+    def __encodedText(self, txt):
+        """
+        Private method to create a MIMEText message with correct encoding.
+        
+        @param txt text to be put into the MIMEText object (string)
+        @return MIMEText object
+        """
+        try:
+            txt.encode("us-ascii")
+            return MIMEText(txt)
+        except UnicodeEncodeError:
+            coding = Preferences.getSystem("StringEncoding")
+            return MIMEText(txt.encode(coding), _charset=coding)
+        
+    def __encodedHeader(self, txt):
+        """
+        Private method to create a correctly encoded mail header.
+        
+        @param txt header text to encode (string)
+        @return encoded header (email.header.Header)
+        """
+        try:
+            txt.encode("us-ascii")
+            return Header(txt)
+        except UnicodeEncodeError:
+            coding = Preferences.getSystem("StringEncoding")
+            return Header(txt, coding)
+        
+    def __createSimpleMail(self):
+        """
+        Private method to create a simple mail message.
+        
+        @return prepared mail message
+        @rtype email.mime.text.MIMEText
+        """
+        msgtext = "{0}\r\n----\r\n{1}----\r\n{2}----\r\n{3}".format(
+            self.message.toPlainText(),
+            Utilities.generateVersionInfo("\r\n"),
+            Utilities.generatePluginsVersionInfo("\r\n"),
+            Utilities.generateDistroInfo("\r\n"))
+        
+        msg = self.__encodedText(msgtext)
+        msg['From'] = Preferences.getUser("Email")
+        msg['To'] = self.__toAddress
+        subject = '[eric6] {0}'.format(self.subject.text())
+        msg['Subject'] = self.__encodedHeader(subject)
+        
+        return msg
+        
+    def __createMultipartMail(self):
+        """
+        Private method to create a multipart mail message.
+        
+        @return prepared mail message
+        @rtype email.mime.text.MIMEMultipart
+        """
+        mpPreamble = ("This is a MIME-encoded message with attachments. "
+                      "If you see this message, your mail client is not "
+                      "capable of displaying the attachments.")
+        
+        msgtext = "{0}\r\n----\r\n{1}----\r\n{2}----\r\n{3}".format(
+            self.message.toPlainText(),
+            Utilities.generateVersionInfo("\r\n"),
+            Utilities.generatePluginsVersionInfo("\r\n"),
+            Utilities.generateDistroInfo("\r\n"))
+        
+        # first part of multipart mail explains format
+        msg = MIMEMultipart()
+        msg['From'] = Preferences.getUser("Email")
+        msg['To'] = self.__toAddress
+        subject = '[eric6] {0}'.format(self.subject.text())
+        msg['Subject'] = self.__encodedHeader(subject)
+        msg.preamble = mpPreamble
+        msg.epilogue = ''
+        
+        # second part is intended to be read
+        att = self.__encodedText(msgtext)
+        msg.attach(att)
+        
+        # next parts contain the attachments
+        for index in range(self.attachments.topLevelItemCount()):
+            itm = self.attachments.topLevelItem(index)
+            maintype, subtype = itm.text(1).split('/', 1)
+            fname = itm.text(0)
+            name = os.path.basename(fname)
+            
+            if maintype == 'text':
+                txt = open(fname, 'r', encoding="utf-8").read()
+                try:
+                    txt.encode("us-ascii")
+                    att = MIMEText(txt, _subtype=subtype)
+                except UnicodeEncodeError:
+                    att = MIMEText(
+                        txt.encode("utf-8"), _subtype=subtype,
+                        _charset="utf-8")
+            elif maintype == 'image':
+                att = MIMEImage(open(fname, 'rb').read(), _subtype=subtype)
+            elif maintype == 'audio':
+                att = MIMEAudio(open(fname, 'rb').read(), _subtype=subtype)
+            else:
+                att = MIMEApplication(open(fname, 'rb').read())
+            att.add_header('Content-Disposition', 'attachment', filename=name)
+            msg.attach(att)
+            
+        return msg
+
+    def __sendmail(self, msg):
+        """
+        Private method to actually send the message.
+        
+        @param msg the message to be sent (string)
+        @return flag indicating success (boolean)
+        """
+        try:
+            encryption = Preferences.getUser("MailServerEncryption")
+            if encryption == "SSL":
+                server = smtplib.SMTP_SSL(
+                    Preferences.getUser("MailServer"),
+                    Preferences.getUser("MailServerPort"))
+            else:
+                server = smtplib.SMTP(
+                    Preferences.getUser("MailServer"),
+                    Preferences.getUser("MailServerPort"))
+                if encryption == "TLS":
+                    server.starttls()
+            if Preferences.getUser("MailServerAuthentication"):
+                # mail server needs authentication
+                password = Preferences.getUser("MailServerPassword")
+                if not password:
+                    password, ok = QInputDialog.getText(
+                        self,
+                        self.tr("Mail Server Password"),
+                        self.tr("Enter your mail server password"),
+                        QLineEdit.Password)
+                    if not ok:
+                        # abort
+                        return False
+                try:
+                    server.login(Preferences.getUser("MailServerUser"),
+                                 password)
+                except (smtplib.SMTPException, socket.error) as e:
+                    if isinstance(e, smtplib.SMTPResponseException):
+                        errorStr = e.smtp_error.decode()
+                    elif isinstance(e, OSError):
+                        errorStr = e.strerror
+                    elif isinstance(e, socket.error):
+                        errorStr = e[1]
+                    else:
+                        errorStr = str(e)
+                    res = E5MessageBox.retryAbort(
+                        self,
+                        self.tr("Send Message"),
+                        self.tr(
+                            """<p>Authentication failed.<br>Reason: {0}</p>""")
+                        .format(errorStr),
+                        E5MessageBox.Critical)
+                    if res:
+                        return self.__sendmail(msg)
+                    else:
+                        return False
+
+            QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
+            QApplication.processEvents()
+            server.sendmail(Preferences.getUser("Email"), self.__toAddress,
+                            msg)
+            server.quit()
+            QApplication.restoreOverrideCursor()
+        except (smtplib.SMTPException, socket.error) as e:
+            QApplication.restoreOverrideCursor()
+            if isinstance(e, smtplib.SMTPResponseException):
+                errorStr = e.smtp_error.decode()
+            elif isinstance(e, smtplib.SMTPException):
+                errorStr = str(e)
+            elif isinstance(e, socket.error):
+                errorStr = e.strerror
+            else:
+                errorStr = str(e)
+            res = E5MessageBox.retryAbort(
+                self,
+                self.tr("Send Message"),
+                self.tr(
+                    """<p>Message could not be sent.<br>Reason: {0}</p>""")
+                .format(errorStr),
+                E5MessageBox.Critical)
+            if res:
+                return self.__sendmail(msg)
+            else:
+                return False
+        return True
+    
+    def __sendmailGoogle(self, msg):
+        """
+        Private method to actually send the message via Google Mail.
+        
+        @param msg email message to be sent
+        @type email.mime.text.MIMEBase
+        """
+        from E5Network.E5GoogleMail import E5GoogleMail
+        
+        if self.__googleMail is None:
+            self.__googleMail = E5GoogleMail(self)
+            self.__googleMail.sendResult.connect(self.__gmailSendResult)
+        
+        self.__googleMail.sendMessage(msg)
+    
+    @pyqtSlot(bool, str)
+    def __gmailSendResult(self, ok, message):
+        """
+        Private slot handling the send result reported by the Google Mail
+        interface.
+        
+        @param ok flag indicating success
+        @type bool
+        @param message message from the interface
+        @type str
+        """
+        if ok:
+            self.__deleteAttachedFiles()
+            self.accept()
+        else:
+            # we got an error
+            E5MessageBox.critical(
+                self,
+                self.tr("Send Message via Gmail"),
+                self.tr(
+                    """<p>Message could not be sent.<br>Reason: {0}</p>""")
+                .format(message)
+            )
+    
+    @pyqtSlot()
+    def on_addButton_clicked(self):
+        """
+        Private slot to handle the Add... button.
+        """
+        fname = E5FileDialog.getOpenFileName(
+            self,
+            self.tr("Attach file"))
+        if fname:
+            self.attachFile(fname, False)
+        
+    def attachFile(self, fname, deleteFile):
+        """
+        Public method to add an attachment.
+        
+        @param fname name of the file to be attached (string)
+        @param deleteFile flag indicating to delete the file after it has
+            been sent (boolean)
+        """
+        mimeType = mimetypes.guess_type(fname)[0]
+        if not mimeType:
+            mimeType = "application/octet-stream"
+        QTreeWidgetItem(self.attachments, [fname, mimeType])
+        self.attachments.header().resizeSections(QHeaderView.ResizeToContents)
+        self.attachments.header().setStretchLastSection(True)
+        
+        if deleteFile:
+            self.__deleteFiles.append(fname)
+        
+    @pyqtSlot()
+    def on_deleteButton_clicked(self):
+        """
+        Private slot to handle the Delete button.
+        """
+        itm = self.attachments.currentItem()
+        if itm is not None:
+            itm = self.attachments.takeTopLevelItem(
+                self.attachments.indexOfTopLevelItem(itm))
+            del itm
+        
+    def on_subject_textChanged(self, txt):
+        """
+        Private slot to handle the textChanged signal of the subject edit.
+        
+        @param txt changed text (string)
+        """
+        self.sendButton.setEnabled(
+            self.subject.text() != "" and
+            self.message.toPlainText() != "")
+        
+    def on_message_textChanged(self):
+        """
+        Private slot to handle the textChanged signal of the message edit.
+        """
+        self.sendButton.setEnabled(
+            self.subject.text() != "" and
+            self.message.toPlainText() != "")

eric ide

mercurial