UI/EmailDialog.py

Sat, 02 Mar 2019 11:12:25 +0100

author
Detlev Offenbach <detlev@die-offenbachs.de>
date
Sat, 02 Mar 2019 11:12:25 +0100
changeset 6825
e659bb96cdfa
parent 6780
7f7453c8e5fc
child 6828
bb6667ea9ae7
permissions
-rw-r--r--

Fixed a UI info inconsistency related to installation of Google mail related packages.

# -*- 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 base64 import b64encode as _bencode

from email import encoders
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.             ##
############################################################


def _encode_base64(msg):
    """
    Function to encode the message's payload in Base64.

    Note: It adds an appropriate Content-Transfer-Encoding header.
    
    @param msg reference to the message object (email.Message)
    """
    orig = msg.get_payload()
    encdata = str(_bencode(orig), "ASCII")
    msg.set_payload(encdata)
    msg['Content-Transfer-Encoding'] = 'base64'

encoders.encode_base64 = _encode_base64
# WORK AROUND: implement our corrected encoder


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
    
    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:
                helpStr = self.tr(
                    "<p>The Google Mail Client API is not installed."
                    " Use <code>pip install --upgrade google-api-python-client"
                    " google-auth-oauthlib</code> to install it.</p>")
            
            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"):
            ok = self.__sendmailGoogle(msg)
        else:
            ok = self.__sendmail(msg.as_string())
        
        if ok:
            for f in self.__deleteFiles:
                try:
                    os.remove(f)
                except OSError:
                    pass
            self.accept()
        
    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
        @return flag indicating success
        @rtype bool
        """
        from E5Network.E5GoogleMail import GoogleMailSendMessage
        QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
        QApplication.processEvents()
        ok, result = GoogleMailSendMessage(msg)
        QApplication.restoreOverrideCursor()
        if not ok:
            # we got an error
            E5MessageBox.critical(
                self,
                self.tr("Send Message"),
                self.tr(
                    """<p>Message could not be sent.<br>Reason: {0}</p>""")
                .format(result)
            )
        
        return ok
    
    @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