def _to_stream_when_changed(self, out): ctype = self.content_type if ctype.is_singlepart(): if self._container.body_changed(): charset, encoding, body = encode_body(self) if charset: self.charset = charset self.content_encoding = WithParams(encoding) else: body = self._container.read_body() # RFC allows subparts without headers if self.headers: self.headers.to_stream(out) elif self.is_root(): raise EncodingError("Root message should have headers") out.write(CRLF) out.write(body) else: self.headers.to_stream(out) out.write(CRLF) if ctype.is_multipart(): boundary = ctype.get_boundary_line() for index, part in enumerate(self.parts): out.write((CRLF if index != 0 else "") + boundary + CRLF) part.to_stream(out) out.write(CRLF + ctype.get_boundary_line(final=True) + CRLF) elif ctype.is_message_container(): self.enclosed.to_stream(out)
def attachment(content_type, body, filename=None, disposition=None, charset=None): """Smarter method to build attachments that detects the proper content type and form of the message based on content type string, body and filename of the attachment """ # fix and sanitize content type string and get main and sub parts: main, sub = fix_content_type( content_type, default=('application', 'octet-stream')) # adjust content type based on body or filename if it's not too accurate content_type = adjust_content_type( ContentType(main, sub), body, filename) if content_type.main == 'message': try: message = message_container(from_string(body)) message.headers['Content-Disposition'] = WithParams(disposition) return message except DecodingError: content_type = ContentType('application', 'octet-stream') return binary( content_type.main, content_type.sub, body, filename, disposition, charset)
def __init__( self, content_type, body, charset=None, disposition=None, filename=None): self.headers = headers.MimeHeaders() self.body = body self.disposition = disposition or ('attachment' if filename else None) self.filename = filename self.size = len(body) if self.filename: self.filename = path.basename(self.filename) content_type = adjust_content_type(content_type, body, filename) if content_type.main == 'text': # the text should have a charset if not charset: charset = "utf-8" # it should be stored as unicode. period self.body = charsets.convert_to_unicode(charset, body) # let's be simple when possible if charset != 'ascii' and is_pure_ascii(body): charset = 'ascii' self.headers['MIME-Version'] = '1.0' self.headers['Content-Type'] = content_type if charset: content_type.params['charset'] = charset if self.disposition: self.headers['Content-Disposition'] = WithParams(disposition) if self.filename: self.headers['Content-Disposition'].params['filename'] = self.filename self.headers['Content-Type'].params['name'] = self.filename
def create_email( from_name, from_email, reply_to, nylas_uid, to_addr, cc_addr, bcc_addr, subject, html, in_reply_to, references, attachments, ): """ Creates a MIME email message (both body and sets the needed headers). Parameters ---------- from_name: string The name aka phrase of the sender. from_email: string The sender's email address. to_addr, cc_addr, bcc_addr: list of pairs (name, email_address), or None Message recipients. reply_to: tuple or None Indicates the mailbox in (name, email_address) format to which the author of the message suggests that replies be sent. subject : string a utf-8 encoded string html : string a utf-8 encoded string in_reply_to: string or None If this message is a reply, the Message-Id of the message being replied to. references: list or None If this message is a reply, the Message-Ids of prior messages in the thread. attachments: list of dicts, optional a list of dicts(filename, data, content_type, content_disposition) """ html = html if html else "" plaintext = html2text(html) # Create a multipart/alternative message msg = mime.create.multipart("alternative") html_part = mime.create.text("html", html) html_part.content_encoding = WithParams("base64") msg.append(mime.create.text("plain", plaintext), html_part) # Create an outer multipart/mixed message if attachments: text_msg = msg msg = mime.create.multipart("mixed") # The first part is the multipart/alternative text part msg.append(text_msg) # The subsequent parts are the attachment parts for a in attachments: # Disposition should be inline if we add Content-ID attachment = mime.create.attachment( a["content_type"], a["data"], filename=a["filename"], disposition=a["content_disposition"], ) if a["content_disposition"] == "inline": attachment.headers["Content-Id"] = "<{}>".format(a["block_id"]) msg.append(attachment) msg.headers["Subject"] = subject if subject else "" # Gmail sets the From: header to the default sending account. We can # however set our own custom phrase i.e. the name that appears next to the # email address (useful if the user has multiple aliases and wants to # specify which to send as), see: http://lee-phillips.org/gmailRewriting/ # For other providers, we simply use name = '' from_addr = address.EmailAddress(from_name, from_email) msg.headers["From"] = from_addr.full_spec() # Need to set these headers so recipients know we sent the email to them # TODO(emfree): should these really be unicode? if to_addr: full_to_specs = [ _get_full_spec_without_validation(name, spec) for name, spec in to_addr ] msg.headers["To"] = u", ".join(full_to_specs) if cc_addr: full_cc_specs = [ _get_full_spec_without_validation(name, spec) for name, spec in cc_addr ] msg.headers["Cc"] = u", ".join(full_cc_specs) if bcc_addr: full_bcc_specs = [ _get_full_spec_without_validation(name, spec) for name, spec in bcc_addr ] msg.headers["Bcc"] = u", ".join(full_bcc_specs) if reply_to: # reply_to is only ever a list with one element msg.headers["Reply-To"] = _get_full_spec_without_validation( reply_to[0][0], reply_to[0][1]) add_nylas_headers(msg, nylas_uid) if in_reply_to: msg.headers["In-Reply-To"] = in_reply_to if references: msg.headers["References"] = "\t".join(references) # Most ISPs set date automatically, but we need to set it here for those # which do not. The Date header is required and omitting it causes issues # with scoring in many spam systems like SpamAssassin # Set dates in UTC since we don't know the timezone of the sending user # +0000 means UTC, whereas -0000 means unsure of timezone utc_datetime = datetime.utcnow() day = utc_datetime.strftime("%a") date = utc_datetime.strftime("%d %b %Y %X") date_header = "{day}, {date} +0000\r\n".format(day=day, date=date) msg.headers["Date"] = date_header rfcmsg = _rfc_transform(msg) return rfcmsg
def content_encoding(self): return self.headers.get( 'Content-Transfer-Encoding', WithParams('7bit'))
def content_disposition(self): """ returns tuple (value, params) """ return self.headers.get('Content-Disposition', WithParams(None))
from os import path from email.mime import audio from flanker import metrics from flanker.mime import bounce from flanker.mime.message import headers, charsets from flanker.mime.message.headers import (WithParams, ContentType, MessageId, Subject) from flanker.mime.message.headers.parametrized import fix_content_type from flanker.mime.message.errors import EncodingError, DecodingError from flanker.utils import is_pure_ascii log = logging.getLogger(__name__) CTE = WithParams('7bit', {}) class Stream(object): def __init__(self, content_type, start, end, string, stream): self.content_type = content_type self.start = start self.end = end self.string = string self.stream = stream self._headers = None self._body_start = None self._body = None self._body_changed = False self.size = len(self.string)