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
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
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
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
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
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
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
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
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()
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