Example #1
0
 def message(self):
     attachments = self.attachments or []
     if len(attachments) == 0 and not self.alts:
         # No html content and zero attachments means plain text
         msg = self._mimetext(self.body)
     elif len(attachments) > 0 and not self.alts:
         # No html and at least one attachment means multipart
         msg = MIMEMultipart()
         msg.attach(self._mimetext(self.body))
     else:
         # Anything else
         msg = MIMEMultipart()
         alternative = MIMEMultipart('alternative')
         alternative.attach(self._mimetext(self.body, 'plain'))
         for mimetype, content in self.alts.items():
             alternative.attach(self._mimetext(content, mimetype))
         msg.attach(alternative)
     if self.subject:
         msg['Subject'] = sanitize_subject(
             to_unicode(self.subject), self.charset)
     msg['From'] = sanitize_address(self.sender, self.charset)
     msg['To'] = ', '.join(
         list(set(sanitize_addresses(self.recipients, self.charset))))
     msg['Date'] = formatdate(self.date, localtime=True)
     msg['Message-ID'] = self.msgId
     if self.cc:
         msg['Cc'] = ', '.join(
             list(set(sanitize_addresses(self.cc, self.charset))))
     if self.reply_to:
         msg['Reply-To'] = sanitize_address(self.reply_to, self.charset)
     if self.extra_headers:
         for k, v in self.extra_headers.items():
             msg[k] = v
     for attachment in attachments:
         f = MIMEBase(*attachment.content_type.split('/'))
         f.set_payload(attachment.data)
         encode_base64(f)
         filename = attachment.filename
         try:
             filename and filename.encode('ascii')
         except UnicodeEncodeError:
             if PY2:
                 filename = filename.encode('utf8')
             filename = ('UTF8', '', filename)
         f.add_header(
             'Content-Disposition', attachment.disposition,
             filename=filename)
         for key, value in attachment.headers.items():
             f.add_header(key, value)
         msg.attach(f)
     if message_policy:
         msg.policy = message_policy
     return msg
Example #2
0
File: mailer.py Project: yws/weppy
 def message(self):
     attachments = self.attachments or []
     if len(attachments) == 0 and not self.alts:
         # No html content and zero attachments means plain text
         msg = self._mimetext(self.body)
     elif len(attachments) > 0 and not self.alts:
         # No html and at least one attachment means multipart
         msg = MIMEMultipart()
         msg.attach(self._mimetext(self.body))
     else:
         # Anything else
         msg = MIMEMultipart()
         alternative = MIMEMultipart('alternative')
         alternative.attach(self._mimetext(self.body, 'plain'))
         for mimetype, content in self.alts.items():
             alternative.attach(self._mimetext(content, mimetype))
         msg.attach(alternative)
     if self.subject:
         msg['Subject'] = sanitize_subject(to_unicode(self.subject),
                                           self.charset)
     msg['From'] = sanitize_address(self.sender, self.charset)
     msg['To'] = ', '.join(
         list(set(sanitize_addresses(self.recipients, self.charset))))
     msg['Date'] = formatdate(self.date, localtime=True)
     msg['Message-ID'] = self.msgId
     if self.cc:
         msg['Cc'] = ', '.join(
             list(set(sanitize_addresses(self.cc, self.charset))))
     if self.reply_to:
         msg['Reply-To'] = sanitize_address(self.reply_to, self.charset)
     if self.extra_headers:
         for k, v in self.extra_headers.items():
             msg[k] = v
     for attachment in attachments:
         f = MIMEBase(*attachment.content_type.split('/'))
         f.set_payload(attachment.data)
         encode_base64(f)
         filename = attachment.filename
         try:
             filename and filename.encode('ascii')
         except UnicodeEncodeError:
             if PY2:
                 filename = filename.encode('utf8')
             filename = ('UTF8', '', filename)
         f.add_header('Content-Disposition',
                      attachment.disposition,
                      filename=filename)
         for key, value in attachment.headers.items():
             f.add_header(key, value)
         msg.attach(f)
     if message_policy:
         msg.policy = message_policy
     return msg
Example #3
0
def _build_mail_to_encrypt(message: str, files: list) -> MIMEMultipart:
    """
    Create the MIMEMultipart mail containing the text message and the potentials attachments.

    :param message: The text message of the encrypted email.
    :param files: The files to attach and encrypt.
    :return: A MIMEMultipart mail object.
    """
    mail_to_encrypt = MIMEMultipart()
    mail_to_encrypt.policy = policy.SMTPUTF8
    if message == '--':
        message = sys.stdin.read()

    message_mail = MIMEBase('text', 'plain', charset='UTF-8')
    message_mail.policy = policy.SMTPUTF8
    message_mail.set_payload(message.encode('UTF-8'))
    encoders.encode_quopri(message_mail)
    mail_to_encrypt.attach(message_mail)

    if files:
        for file in files:
            path = Path(file)

            guessed_type = mimetypes.guess_type(path.absolute().as_uri())[0]

            if not guessed_type:
                print('Could not guess file %s mime-type, using application/octet-stream.' % file, file=sys.stderr)
                guessed_type = 'application/octet-stream'

            mimetype = guessed_type.split('/')

            mail_attachment = MIMEBase(mimetype[0], mimetype[1])
            mail_attachment.policy = policy.SMTPUTF8
            mail_attachment.set_payload(open(str(path.absolute()), 'rb').read())
            encoders.encode_base64(mail_attachment)
            mail_attachment.add_header('Content-Disposition', "attachment", filename=path.name)

            mail_to_encrypt.attach(mail_attachment)

    return mail_to_encrypt
Example #4
0
    def _message(self):
        """Creates the email"""
        ascii_attachments = current_app.extensions['mail'].ascii_attachments
        encoding = self.charset or 'utf-8'

        attachments = self.attachments or []

        if len(attachments) == 0 and not self.alts:
            # No html content and zero attachments means plain text
            msg = self._mimetext(self.body)
        elif len(attachments) > 0 and not self.alts:
            # No html and at least one attachment means multipart
            msg = MIMEMultipart()
            msg.attach(self._mimetext(self.body))
        else:
            # Anything else
            msg = MIMEMultipart()
            alternative = MIMEMultipart('alternative')
            alternative.attach(self._mimetext(self.body, 'plain'))
            for mimetype, content in self.alts.items():
                alternative.attach(self._mimetext(content, mimetype))
            msg.attach(alternative)

        if self.subject:
            msg['Subject'] = sanitize_subject(force_text(self.subject), encoding)

        msg['From'] = sanitize_address(self.sender, encoding)
        msg['To'] = ', '.join(list(set(sanitize_addresses(self.recipients, encoding))))

        msg['Date'] = formatdate(self.date, localtime=True)
        # see RFC 5322 section 3.6.4.
        msg['Message-ID'] = self.msgId

        if self.cc:
            msg['Cc'] = ', '.join(list(set(sanitize_addresses(self.cc, encoding))))

        if self.reply_to:
            msg['Reply-To'] = sanitize_address(self.reply_to, encoding)

        if self.extra_headers:
            for k, v in self.extra_headers.items():
                msg[k] = v

        SPACES = re.compile(r'[\s]+', re.UNICODE)
        for attachment in attachments:
            f = MIMEBase(*attachment.content_type.split('/'))
            f.set_payload(attachment.data)
            encode_base64(f)

            filename = attachment.filename
            if filename and ascii_attachments:
                # force filename to ascii
                filename = unicodedata.normalize('NFKD', filename)
                filename = filename.encode('ascii', 'ignore').decode('ascii')
                filename = SPACES.sub(u' ', filename).strip()

            try:
                filename and filename.encode('ascii')
            except UnicodeEncodeError:
                if not PY3:
                    filename = filename.encode('utf8')
                filename = ('UTF8', '', filename)

            f.add_header('Content-Disposition',
                         attachment.disposition,
                         filename=filename)

            for key, value in attachment.headers.items():
                f.add_header(key, value)

            msg.attach(f)
        if message_policy:
            msg.policy = message_policy

        return msg
Example #5
0
def encrypt_mail(recipient: str, subject=_MAIL_DEFAULT_SUBJECT, message=_MAIL_DEFAULT_MESSAGE,
                 files=_MAIL_DEFAULT_ATTACHMENTS, gpgenv=_DEFAULT_GPG_ENV,
                 trust=_DEFAULT_GPG_TRUST, signer=None, sign_password=None) -> MIMEMultipart:
    """
    Build and encrypt an email using the given parameters.

    :param recipient: Recipient the mail will be encrypted for. Can use key fingerprint or id.
    :param subject: The email subject.
    :param message: The email message. If "--" is used, read the standard input.
    :param files: A list of str containing the names of mail attachments.
    :param gpgenv: The path to the GPG environment.
    :param trust: Whether to always trust or not the recipient key.
    :param signer: The key ID used to sign the email.
    :param sign_password: The password of the signing key.
    :return: The MIMEMultipart corresponding to the encrypted email.
    """
    gpg = gnupg.GPG(gnupghome=gpgenv)

    mail_to_encrypt = _build_mail_to_encrypt(message, files)

    if signer:
        signature = gpg.sign(str(mail_to_encrypt), keyid=signer, passphrase=sign_password, detach=True)

        # Values defined from gnupg/common/openpgpdefs.h and  gnupg/tests/openpgp/mds.scm
        hash_mapping = {'1': 'md5',
                        '2': 'sha1',
                        '3': 'ripemd160',
                        '8': 'sha256',
                        '9': 'sha384',
                        '10': 'sha512',
                        '11': 'sha224'
                        }

        signed_mail = MIMEMultipart('signed',
                                    micalg='pgp-%s' % hash_mapping[signature.hash_algo],
                                    protocol='application/pgp-signature')
        signed_mail.policy = policy.SMTPUTF8
        signed_mail.attach(mail_to_encrypt)

        signature_part = MIMEApplication(str(signature), 'pgp-signature', encoders.encode_noop, name='signature.asc')
        signature_part.policy = policy.SMTPUTF8

        signed_mail.attach(signature_part)
        mail_to_encrypt = signed_mail

    encrypted_mail = gpg.encrypt(str(mail_to_encrypt), recipient, always_trust=trust)

    if not encrypted_mail.ok:
        print(encrypted_mail.status, file=sys.stderr)
        sys.exit(2)

    mail_to_send = MIMEMultipart('encrypted', protocol='application/pgp-encrypted')
    mail_to_send.policy = policy.SMTPUTF8
    mail_to_send.add_header('Subject', subject)

    version_part = MIMEApplication("Version: 1", 'pgp-encrypted', encoders.encode_7or8bit)
    version_part.policy = policy.SMTPUTF8
    mail_to_send.attach(version_part)

    content_part = MIMEApplication(str(encrypted_mail),
                                   'octet-stream',
                                   encoders.encode_7or8bit,
                                   name='encrypted.asc')
    content_part.policy = policy.SMTPUTF8
    mail_to_send.attach(content_part)

    return mail_to_send
Example #6
0
def to_message(base):
    """
    Given a MailBase, this will construct a MIME part that is canonicalized for
    use with the Python email API.
    """
    ctype, ctparams = base.get_content_type()

    if not ctype:
        if base.parts:
            ctype = 'multipart/mixed'
        else:
            ctype = 'text/plain'

    maintype, subtype = ctype.split('/')
    is_text = maintype == 'text'
    is_multipart = maintype == 'multipart'

    if base.parts and not is_multipart:
        raise RuntimeError('Content type should be multipart, not %r' % ctype)

    body = base.get_body()
    ctenc = base.get_transfer_encoding()
    charset = ctparams.get('charset')

    if is_multipart:
        out = MIMEMultipart(subtype, **ctparams)
    else:
        out = MIMENonMultipart(maintype, subtype, **ctparams)
        if ctenc:
            out['Content-Transfer-Encoding'] = ctenc
        if isinstance(body, str):
            if not charset:
                if is_text:
                    charset, _ = best_charset(body)
                else:
                    charset = 'utf-8'
            body = body.encode(charset, 'surrogateescape')
        if body is not None:
            if ctenc:
                body = transfer_encode(ctenc, body)

            body = body.decode(charset or 'ascii', 'replace')
        out.set_payload(body, charset)

    for k in base.keys():  # returned sorted
        value = base[k]
        if not value:
            continue
        out[k] = value

    cdisp, cdisp_params = base.get_content_disposition()

    if cdisp:
        out.add_header('Content-Disposition', cdisp, **cdisp_params)

    # go through the children
    for part in base.parts:
        sub = to_message(part)
        out.attach(sub)

    # Message.policy tells how we format out raw ASCII email
    # Lone \n is not allowed in email message, but
    # Python generates this by default.
    # Sparkpost SMTP would reject us as
    # smtplib.SMTPDataError:
    # (550, b'5.6.0 Lone CR or LF in headers (see RFC2822 section 2.2)')
    policy = Compat32(linesep="\r\n")
    out.policy = policy

    return out
Example #7
0
    def _message(self):
        """Creates the email"""
        encoding = self.charset or "utf-8"

        attachments = self.attachments or []

        if len(attachments) == 0 and not self.alts:
            # No html content and zero attachments means plain text
            msg = self._mimetext(self.body)
        elif len(attachments) > 0 and not self.alts:
            # No html and at least one attachment means multipart
            msg = MIMEMultipart()
            msg.attach(self._mimetext(self.body))
        else:
            # Anything else
            msg = MIMEMultipart()
            alternative = MIMEMultipart("alternative")
            alternative.attach(self._mimetext(self.body, "plain"))
            for mimetype, content in self.alts.items():
                alternative.attach(self._mimetext(content, mimetype))
            msg.attach(alternative)

        if self.subject:
            msg["Subject"] = sanitize_subject(force_text(self.subject),
                                              encoding)

        msg["From"] = sanitize_address(self.sender, encoding)
        msg["To"] = ", ".join(
            list(set(sanitize_addresses(self.recipients, encoding))))

        msg["Date"] = formatdate(self.date, localtime=True)
        # see RFC 5322 section 3.6.4.
        msg["Message-ID"] = self.msgId

        if self.cc:
            msg["Cc"] = ", ".join(
                list(set(sanitize_addresses(self.cc, encoding))))

        if self.reply_to:
            msg["Reply-To"] = sanitize_address(self.reply_to, encoding)

        if self.extra_headers:
            for k, v in self.extra_headers.items():
                msg[k] = v

        SPACES = re.compile(r"[\s]+", re.UNICODE)
        for attachment in attachments:
            f = MIMEBase(*attachment.content_type.split("/"))
            f.set_payload(attachment.data)
            encode_base64(f)

            filename = attachment.filename
            if filename and self.ascii_attachments:
                # force filename to ascii
                filename = unicodedata.normalize("NFKD", filename)
                filename = filename.encode("ascii", "ignore").decode("ascii")
                filename = SPACES.sub(u" ", filename).strip()

            try:
                filename and filename.encode("ascii")

            except UnicodeEncodeError:
                filename = ("UTF8", "", filename)

            f.add_header("Content-Disposition",
                         attachment.disposition,
                         filename=filename)

            for key, value in attachment.headers.items():
                f.add_header(key, value)

            msg.attach(f)

        msg.policy = policy.SMTP

        return msg
Example #8
0
    def construct_mail(self):
        """
        Compiles the information contained in this envelope into a
        :class:`email.Message`.
        """
        # Build body text part. To properly sign/encrypt messages later on, we
        # convert the text to its canonical format (as per RFC 2015).
        canonical_format = self.body_txt.encode('utf-8')
        textpart = MIMEText(canonical_format, 'plain', 'utf-8')
        inner_msg = textpart

        if self.body_html:
            htmlpart = MIMEText(self.body_html, 'html', 'utf-8')
            inner_msg = MIMEMultipart('alternative')
            inner_msg.attach(textpart)
            inner_msg.attach(htmlpart)

        # wrap everything in a multipart container if there are attachments
        if self.attachments:
            msg = MIMEMultipart('mixed')
            msg.attach(inner_msg)
            # add attachments
            for a in self.attachments:
                msg.attach(a.get_mime_representation())
            inner_msg = msg

        if self.sign:
            plaintext = inner_msg.as_bytes(policy=email.policy.SMTP)
            logging.debug('signing plaintext: %s', plaintext)

            try:
                signatures, signature_str = crypto.detached_signature_for(
                    plaintext, [self.sign_key])
                if len(signatures) != 1:
                    raise GPGProblem("Could not sign message (GPGME "
                                     "did not return a signature)",
                                     code=GPGCode.KEY_CANNOT_SIGN)
            except gpg.errors.GPGMEError as e:
                if e.getcode() == gpg.errors.BAD_PASSPHRASE:
                    # If GPG_AGENT_INFO is unset or empty, the user just does
                    # not have gpg-agent running (properly).
                    if os.environ.get('GPG_AGENT_INFO', '').strip() == '':
                        msg = "Got invalid passphrase and GPG_AGENT_INFO\
                                not set. Please set up gpg-agent."
                        raise GPGProblem(msg, code=GPGCode.BAD_PASSPHRASE)
                    else:
                        raise GPGProblem("Bad passphrase. Is gpg-agent "
                                         "running?",
                                         code=GPGCode.BAD_PASSPHRASE)
                raise GPGProblem(str(e), code=GPGCode.KEY_CANNOT_SIGN)

            micalg = crypto.RFC3156_micalg_from_algo(signatures[0].hash_algo)
            unencrypted_msg = MIMEMultipart(
                'signed', micalg=micalg, protocol='application/pgp-signature')

            # wrap signature in MIMEcontainter
            stype = 'pgp-signature; name="signature.asc"'
            signature_mime = MIMEApplication(
                _data=signature_str.decode('ascii'),
                _subtype=stype,
                _encoder=encode_7or8bit)
            signature_mime['Content-Description'] = 'signature'
            signature_mime.set_charset('us-ascii')

            # add signed message and signature to outer message
            unencrypted_msg.attach(inner_msg)
            unencrypted_msg.attach(signature_mime)
            unencrypted_msg['Content-Disposition'] = 'inline'
        else:
            unencrypted_msg = inner_msg

        if self.encrypt:
            plaintext = unencrypted_msg.as_bytes(policy=email.policy.SMTP)
            logging.debug('encrypting plaintext: %s', plaintext)

            try:
                encrypted_str = crypto.encrypt(
                    plaintext, list(self.encrypt_keys.values()))
            except gpg.errors.GPGMEError as e:
                raise GPGProblem(str(e), code=GPGCode.KEY_CANNOT_ENCRYPT)

            outer_msg = MIMEMultipart('encrypted',
                                      protocol='application/pgp-encrypted')

            version_str = 'Version: 1'
            encryption_mime = MIMEApplication(_data=version_str,
                                              _subtype='pgp-encrypted',
                                              _encoder=encode_7or8bit)
            encryption_mime.set_charset('us-ascii')

            encrypted_mime = MIMEApplication(
                _data=encrypted_str.decode('ascii'),
                _subtype='octet-stream',
                _encoder=encode_7or8bit)
            encrypted_mime.set_charset('us-ascii')
            outer_msg.attach(encryption_mime)
            outer_msg.attach(encrypted_mime)

        else:
            outer_msg = unencrypted_msg

        headers = self.headers.copy()

        # add Date header
        if 'Date' not in headers:
            headers['Date'] = [email.utils.formatdate(localtime=True)]

        # add Message-ID
        if 'Message-ID' not in headers:
            domain = self.account.message_id_domain
            headers['Message-ID'] = [email.utils.make_msgid(domain=domain)]

        if 'User-Agent' in headers:
            uastring_format = headers['User-Agent'][0]
        else:
            uastring_format = settings.get('user_agent').strip()
        uastring = uastring_format.format(version=__version__)
        if uastring:
            headers['User-Agent'] = [uastring]

        # set policy on outer_msg to ease encoding headers
        outer_msg.policy = email.policy.default
        # copy headers from envelope to mail
        for k, vlist in headers.items():
            for v in vlist:
                outer_msg.add_header(k, v)

        return outer_msg
Example #9
0
    def as_string(self):
        """Creates the email"""

        encoding = self.charset or 'utf-8'

        attachments = self.attachments or []

        if len(attachments) == 0 and not self.html:
            # No html content and zero attachments means plain text
            msg = self._mimetext(self.body)
        elif len(attachments) > 0 and not self.html:
            # No html and at least one attachment means multipart
            msg = MIMEMultipart()
            msg.attach(self._mimetext(self.body))
        else:
            # Anything else
            msg = MIMEMultipart()
            alternative = MIMEMultipart('alternative')
            alternative.attach(self._mimetext(self.body, 'plain'))
            alternative.attach(self._mimetext(self.html, 'html'))
            msg.attach(alternative)

        if self.subject:
            msg['Subject'] = sanitize_subject(force_text(self.subject), encoding)

        msg['From'] = sanitize_address(self.sender, encoding)
        msg['To'] = ', '.join(list(set(sanitize_addresses(self.recipients, encoding))))

        msg['Date'] = formatdate(self.date, localtime=True)
        # see RFC 5322 section 3.6.4.
        msg['Message-ID'] = self.msgId

        if self.cc:
            msg['Cc'] = ', '.join(list(set(sanitize_addresses(self.cc, encoding))))

        if self.reply_to:
            msg['Reply-To'] = sanitize_address(self.reply_to, encoding)

        if self.extra_headers:
            for k, v in self.extra_headers.items():
                msg[k] = v

        for attachment in attachments:
            f = MIMEBase(*attachment.content_type.split('/'))
            f.set_payload(attachment.data)
            encode_base64(f)

            try:
                attachment.filename and attachment.filename.encode('ascii')
            except UnicodeEncodeError:
                filename = attachment.filename
                if not PY3:
                    filename = filename.encode('utf8')
                f.add_header('Content-Disposition', attachment.disposition,
                            filename=('UTF8', '', filename))
            else:
                f.add_header('Content-Disposition', '%s;filename=%s' %
                             (attachment.disposition, attachment.filename))

            for key, value in attachment.headers:
                f.add_header(key, value)

            msg.attach(f)
        if message_policy:
            msg.policy = message_policy

        return msg.as_string()
Example #10
0
def to_message(base):
    """
    Given a MailBase, this will construct a MIME part that is canonicalized for
    use with the Python email API.
    """
    ctype, ctparams = base.get_content_type()

    if not ctype:
        if base.parts:
            ctype = 'multipart/mixed'
        else:
            ctype = 'text/plain'

    maintype, subtype = ctype.split('/')
    is_text = maintype == 'text'
    is_multipart = maintype == 'multipart'

    if base.parts and not is_multipart:
        raise RuntimeError('Content type should be multipart, not %r' % ctype)

    body = base.get_body()
    ctenc = base.get_transfer_encoding()
    charset = ctparams.get('charset')

    if is_multipart:
        out = MIMEMultipart(subtype, **ctparams)
    else:
        out = MIMENonMultipart(maintype, subtype, **ctparams)
        if ctenc:
            out['Content-Transfer-Encoding'] = ctenc
        if isinstance(body, text_type):
            if not charset:
                if is_text:
                    charset, _ = best_charset(body)
                else:
                    charset = 'utf-8'
            if PY2:
                body = body.encode(charset)
            else:
                body = body.encode(charset, 'surrogateescape')
        if body is not None:
            if ctenc:
                body = transfer_encode(ctenc, body)
            if not PY2:
                body = body.decode(charset or 'ascii', 'replace')
        out.set_payload(body, charset)

    for k in base.keys():  # returned sorted
        value = base[k]
        if not value:
            continue
        out[k] = value

    cdisp, cdisp_params = base.get_content_disposition()

    if cdisp:
        out.add_header('Content-Disposition', cdisp, **cdisp_params)

    # go through the children
    for part in base.parts:
        sub = to_message(part)
        out.attach(sub)

    if not PY2:
        out.policy = Compat32(linesep="\r\n")

    return out