def encode_header_param(param_text): """Returns an appropriate RFC2047 encoded representation of the given header parameter value, suitable for direct assignation as the param value (e.g. via Message.set_param() or Message.add_header()) RFC2822 assumes that headers contain only 7-bit characters, so we ensure it is the case, using RFC2047 encoding when needed. :param param_text: unicode or utf-8 encoded string with header value :rtype: string :return: if ``param_text`` represents a plain ASCII string, return the same 7-bit string, otherwise returns an ASCII string containing the RFC2047 encoded text. """ # For details see the encode_header() method that uses the same logic if not param_text: return "" param_text_utf8 = tools.ustr(param_text).encode('utf-8') param_text_ascii = try_coerce_ascii(param_text_utf8) return param_text_ascii if param_text_ascii\ else Charset('utf8').header_encode(param_text_utf8)
def write_patch_file(filename, commit_info, diff): """Write patch file""" if not diff: gbp.log.debug("I won't generate empty diff %s" % filename) return None try: with open(filename, 'w') as patch: msg = Message() charset = Charset('utf-8') charset.body_encoding = None charset.header_encoding = QP # Write headers name = commit_info['author']['name'] email = commit_info['author']['email'] # Git compat: put name in quotes if special characters found if re.search("[,.@()\[\]\\\:;]", name): name = '"%s"' % name from_header = Header(unicode(name, 'utf-8'), charset, 77, 'from') from_header.append(unicode('<%s>' % email)) msg['From'] = from_header date = commit_info['author'].datetime datestr = date.strftime('%a, %-d %b %Y %H:%M:%S %z') msg['Date'] = Header(unicode(datestr, 'utf-8'), charset, 77, 'date') msg['Subject'] = Header(unicode(commit_info['subject'], 'utf-8'), charset, 77, 'subject') # Write message body if commit_info['body']: # Strip extra linefeeds body = commit_info['body'].rstrip() + '\n' try: msg.set_payload(body.encode('ascii')) except UnicodeDecodeError: msg.set_payload(body, charset) patch.write(msg.as_string(unixfrom=False)) # Write diff patch.write('---\n') patch.write(diff) except IOError as err: raise GbpError('Unable to create patch file: %s' % err) return filename
def append(self, s, charset=None, errors='strict'): """Append a string to the MIME header. Optional charset, if given, should be a Charset instance or the name of a character set (which will be converted to a Charset instance). A value of None (the default) means that the charset given in the constructor is used. s may be a byte string or a Unicode string. If it is a byte string (i.e. isinstance(s, str) is true), then charset is the encoding of that byte string, and a UnicodeError will be raised if the string cannot be decoded with that charset. If s is a Unicode string, then charset is a hint specifying the character set of the characters in the string. In this case, when producing an RFC 2822 compliant header using RFC 2047 rules, the Unicode string will be encoded using the following charsets in order: us-ascii, the charset hint, utf-8. The first character set not to provoke a UnicodeError is used. Optional `errors' is passed as the third argument to any unicode() or ustr.encode() call. """ if charset is None: charset = self._charset elif not isinstance(charset, Charset): charset = Charset(charset) if charset != '8bit': if isinstance(s, str): incodec = charset.input_codec or 'us-ascii' ustr = unicode(s, incodec, errors) outcodec = charset.output_codec or 'us-ascii' ustr.encode(outcodec, errors) elif isinstance(s, unicode): for charset in (USASCII, charset, UTF8): try: outcodec = charset.output_codec or 'us-ascii' s = s.encode(outcodec, errors) break except UnicodeError: pass self._chunks.append((s, charset)) return
def __init__(self, recip, sender, subject=None, text=None, lang=None): Message.__init__(self) charset = None if lang is not None: charset = Charset(Utils.GetCharSet(lang)) if text is not None: self.set_payload(text, charset) if subject is None: subject = '(no subject)' self['Subject'] = Header(subject, charset, header_name='Subject', errors='replace') self['From'] = sender if isinstance(recip, list): self['To'] = COMMASPACE.join(recip) self.recips = recip else: self['To'] = recip self.recips = [recip]
def encode_header_param(param_text): """Returns an appropriate RFC2047 encoded representation of the given header parameter value, suitable for direct assignation as the param value (e.g. via Message.set_param() or Message.add_header()) RFC2822 assumes that headers contain only 7-bit characters, so we ensure it is the case, using RFC2047 encoding when needed. :param param_text: unicode or utf-8 encoded string with header value :rtype: string :return: if ``param_text`` represents a plain ASCII string, return the same 7-bit string, otherwise returns an ASCII string containing the RFC2047 encoded text. """ # For details see the encode_header() method that uses the same logic if not param_text: return "" param_text = ustr(param_text) # FIXME: require unicode higher up? if is_ascii(param_text): return pycompat.to_native(param_text) # TODO: is that actually necessary? return Charset("utf-8").header_encode(param_text)
def make_header(decoded_seq, maxlinelen=None, header_name=None, continuation_ws=' '): """Create a Header from a sequence of pairs as returned by decode_header() decode_header() takes a header value string and returns a sequence of pairs of the format (decoded_string, charset) where charset is the string name of the character set. This function takes one of those sequence of pairs and returns a Header instance. Optional maxlinelen, header_name, and continuation_ws are as in the Header constructor. """ h = Header(maxlinelen=maxlinelen, header_name=header_name, continuation_ws=continuation_ws) for s, charset in decoded_seq: # None means us-ascii but we can simply pass it on to h.append() if charset is not None and not isinstance(charset, Charset): charset = Charset(charset) h.append(s, charset) return h
def test_send_utf8(self): subject = u'sübjèçt' body = u'bödÿ-àéïöñß' mailsender = MailSender(debug=True) mailsender.send(to=['*****@*****.**'], subject=subject, body=body, charset='utf-8', _callback=self._catch_mail_sent) assert self.catched_msg self.assertEqual(self.catched_msg['subject'], subject) self.assertEqual(self.catched_msg['body'], body) msg = self.catched_msg['msg'] self.assertEqual(msg['subject'], subject) self.assertEqual(msg.get_payload(), body) self.assertEqual(msg.get_charset(), Charset('utf-8')) self.assertEqual(msg.get('Content-Type'), 'text/plain; charset="utf-8"')
def append(self, s, charset=None, errors='strict'): if charset is None: charset = self._charset elif not isinstance(charset, Charset): charset = Charset(charset) if charset != '8bit': if isinstance(s, str): incodec = charset.input_codec or 'us-ascii' ustr = unicode(s, incodec, errors) outcodec = charset.output_codec or 'us-ascii' ustr.encode(outcodec, errors) elif isinstance(s, unicode): for charset in (USASCII, charset, UTF8): try: outcodec = charset.output_codec or 'us-ascii' s = s.encode(outcodec, errors) break except UnicodeError: pass self._chunks.append((s, charset))
def init_payload(self): super().init_payload() self.all_recipients = [] self.mime_message = self.message.message() # Work around an Amazon SES bug where, if all of: # - the message body (text or html) contains non-ASCII characters # - the body is sent with `Content-Transfer-Encoding: 8bit` # (which is Django email's default for most non-ASCII bodies) # - you are using an SES ConfigurationSet with open or click tracking enabled # then SES replaces the non-ASCII characters with question marks as it rewrites # the message to add tracking. Forcing `CTE: quoted-printable` avoids the problem. # (https://forums.aws.amazon.com/thread.jspa?threadID=287048) for part in self.mime_message.walk(): if part.get_content_maintype() == "text" and part["Content-Transfer-Encoding"] == "8bit": content = part.get_payload() del part["Content-Transfer-Encoding"] qp_charset = Charset(part.get_content_charset("us-ascii")) qp_charset.body_encoding = QP # (can't use part.set_payload, because SafeMIMEText can undo this workaround) MIMEText.set_payload(part, content, charset=qp_charset)
def test_send_utf8(self): subject = "sübjèçt" body = "bödÿ-àéïöñß" mailsender = MailSender(debug=True) mailsender.send( to=["*****@*****.**"], subject=subject, body=body, charset="utf-8", _callback=self._catch_mail_sent, ) assert self.catched_msg self.assertEqual(self.catched_msg["subject"], subject) self.assertEqual(self.catched_msg["body"], body) msg = self.catched_msg["msg"] self.assertEqual(msg["subject"], subject) self.assertEqual(msg.get_payload(), body) self.assertEqual(msg.get_charset(), Charset("utf-8")) self.assertEqual(msg.get("Content-Type"), 'text/plain; charset="utf-8"')
def send_mail(self, mail_template, in_reply_to = None, **kwargs): msgid = make_msgid() subject = mail_template['subject'].format(**kwargs) message = mail_template['message'].format(**kwargs) msg = MIMEMultipart('alternative') msg['Subject'] = str(Header(subject, 'utf-8')) msg['From'] = str(Header(SMTP_FROM, 'utf-8')) msg['To'] = str(Header(self.email, 'utf-8')) msg['Message-ID'] = msgid msg['Reply-To'] = SMTP_REPLY_TO_EMAIL msg['Date'] = datetime.datetime.now(pytz.utc).strftime("%a, %e %b %Y %T %z") if in_reply_to: msg['In-Reply-To'] = in_reply_to msg['References'] = in_reply_to # add message charset = Charset('utf-8') # QP = quoted printable; this is better readable instead of base64, when # the mail is read in plaintext! charset.body_encoding = QP message_part = MIMEText(message.encode('utf-8'), 'plain', charset) msg.attach(message_part) if DEBUG: with open("/tmp/keepitup_mails.log", "a") as f: f.write(msg.as_string() + "\n") else: with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server: server.ehlo() if SMTP_USE_STARTTLS: context = ssl.create_default_context() server.starttls(context=context) server.sendmail(SMTP_FROM, self.email, msg.as_string()) return msgid
def append(self, s, charset=None, errors='strict'): """Append a string to the MIME header. Optional charset, if given, should be a Charset instance or the name of a character set (which will be converted to a Charset instance). A value of None (the default) means that the charset given in the constructor is used. s may be a byte string or a Unicode string. If it is a byte string (i.e. isinstance(s, str) is true), then charset is the encoding of that byte string, and a UnicodeError will be raised if the string cannot be decoded with that charset. If s is a Unicode string, then charset is a hint specifying the character set of the characters in the string. In this case, when producing an RFC 2822 compliant header using RFC 2047 rules, the Unicode string will be encoded using the following charsets in order: us-ascii, the charset hint, utf-8. The first character set not to provoke a UnicodeError is used. Optional `errors' is passed as the third argument to any unicode() or ustr.encode() call. """ if charset is None: charset = self._charset elif not isinstance(charset, Charset): charset = Charset(charset) if isinstance(s, str): # Convert the string from the input character set to the output # character set and store the resulting bytes and the charset for # composition later. input_charset = charset.input_codec or 'us-ascii' input_bytes = s.encode(input_charset, errors) else: # We already have the bytes we will store internally. input_bytes = s # Ensure that the bytes we're storing can be decoded to the output # character set, otherwise an early error is thrown. output_charset = charset.output_codec or 'us-ascii' output_string = input_bytes.decode(output_charset, errors) self._chunks.append((output_string, charset))
def build_message( subject, from_email, html, to_recipients=[], bcc_recipients=[], attachments=[], headers=[], ): """Build raw email for sending.""" message = MIMEMultipart("alternative") cs = Charset("utf-8") cs.body_encoding = QP message["Subject"] = subject message["From"] = from_email if to_recipients: message["To"] = ",".join(to_recipients) if bcc_recipients: message["Bcc"] = ",".join(bcc_recipients) text = get_text_from_html(html) plain_text = MIMEText(text, "plain", cs) message.attach(plain_text) html_text = MIMEText(html, "html", cs) message.attach(html_text) for header in headers: message[header["key"]] = header["value"] for filename in attachments: with open(filename, "rb") as attachment: part = MIMEApplication(attachment.read()) part.add_header("Content-Disposition", "attachment", filename="report.pdf") message.attach(part) return message.as_string()
def __init__(self, encoding="utf-8", templates=None, from_addr="*****@*****.**", from_name="System", server_factory=smtplib.SMTP): """initialize the mail API. :param server: an smtplib.SMTP server or a component implementing ``connect()``, ``sendmail()`` and ``quit()`` :param encoding: the encoding to use for emails :param templates: The jinja2 template environment to use :param from_addr: the full name of the sender """ self.templates = templates self.from_addr = from_addr self.from_name = from_name self.charset = Charset("utf-8") self.charset.header_encoding = QP self.charset.body_encoding = QP self.server_factory = server_factory
def __setstate__(self, d): # The base class attributes have changed over time. Which could # affect Mailman if messages are sitting in the queue at the time of # upgrading the email package. We shouldn't burden email with this, # so we handle schema updates here. self.__dict__ = d # We know that email 2.4.3 is up-to-date version = d.get('__version__', (0, 0, 0)) d['__version__'] = VERSION if version >= VERSION: return # Messages grew a _charset attribute between email version 0.97 and 1.1 if '_charset' not in d: self._charset = None # Messages grew a _default_type attribute between v2.1 and v2.2 if '_default_type' not in d: # We really have no idea whether this message object is contained # inside a multipart/digest or not, so I think this is the best we # can do. self._default_type = 'text/plain' # Header instances used to allow both strings and Charsets in their # _chunks, but by email 2.4.3 now it's just Charsets. headers = [] hchanged = 0 for k, v in self._headers: if isinstance(v, Header): chunks = [] cchanged = 0 for s, charset in v._chunks: if isinstance(charset, str): charset = Charset(charset) cchanged = 1 chunks.append((s, charset)) if cchanged: v._chunks = chunks hchanged = 1 headers.append((k, v)) if hchanged: self._headers = headers
def set_charset(self, charset): """Set the charset of the payload to a given character set. charset can be a Charset instance, a string naming a character set, or None. If it is a string it will be converted to a Charset instance. If charset is None, the charset parameter will be removed from the Content-Type field. Anything else will generate a TypeError. The message will be assumed to be of type text/* encoded with charset.input_charset. It will be converted to charset.output_charset and encoded properly, if needed, when generating the plain text representation of the message. MIME headers (MIME-Version, Content-Type, Content-Transfer-Encoding) will be added as needed. """ if charset is None: self.del_param('charset') self._charset = None return if not isinstance(charset, Charset): charset = Charset(charset) self._charset = charset if 'MIME-Version' not in self: self.add_header('MIME-Version', '1.0') if 'Content-Type' not in self: self.add_header('Content-Type', 'text/plain', charset=charset.get_output_charset()) else: self.set_param('charset', charset.get_output_charset()) if charset != charset.get_output_charset(): self._payload = charset.body_encode(self._payload) if 'Content-Transfer-Encoding' not in self: cte = charset.get_body_encoding() try: cte(self) except TypeError: self._payload = charset.body_encode(self._payload) self.add_header('Content-Transfer-Encoding', cte)
def __init__(self, s=None, charset=None, maxlinelen=None, header_name=None, continuation_ws=' ', errors='strict'): if charset is None: charset = USASCII if not isinstance(charset, Charset): charset = Charset(charset) self._charset = charset self._continuation_ws = continuation_ws cws_expanded_len = len(continuation_ws.replace('\t', SPACE8)) self._chunks = [] if s is not None: self.append(s, charset, errors) if maxlinelen is None: maxlinelen = MAXLINELEN if header_name is None: self._firstlinelen = maxlinelen else: self._firstlinelen = maxlinelen - len(header_name) - 2 self._maxlinelen = maxlinelen - cws_expanded_len
def _init_pref_encoding(self): self._charset = Charset() self._charset.input_charset = 'utf-8' pref = self.mime_encoding.lower() if pref == 'base64': self._charset.header_encoding = BASE64 self._charset.body_encoding = BASE64 self._charset.output_charset = 'utf-8' self._charset.input_codec = 'utf-8' self._charset.output_codec = 'utf-8' elif pref in ['qp', 'quoted-printable']: self._charset.header_encoding = QP self._charset.body_encoding = QP self._charset.output_charset = 'utf-8' self._charset.input_codec = 'utf-8' self._charset.output_codec = 'utf-8' elif pref == 'none': self._charset.header_encoding = None self._charset.body_encoding = None self._charset.input_codec = None self._charset.output_charset = 'ascii' else: raise TracError(_("Invalid email encoding setting: %(pref)s", pref=pref))
def sendmail(subject, text, to=None, cc=None, bcc=None, mail_from=None, html=None): """ Create and send a text/plain message Return a tuple of success or error indicator and message. :param subject: subject of email :type subject: unicode :param text: email body text :type text: unicode :param to: recipients :type to: list :param cc: recipients (CC) :type cc: list :param bcc: recipients (BCC) :type bcc: list :param mail_from: override default mail_from :type mail_from: unicode :param html: html email body text :type html: unicode :rtype: tuple :returns: (is_ok, Description of error or OK message) """ import smtplib import socket from email.message import Message from email.mime.multipart import MIMEMultipart from email.mime.text import MIMEText from email.charset import Charset, QP from email.utils import formatdate, make_msgid cfg = app.cfg if not cfg.mail_enabled: return ( 0, _("Contact administrator: cannot send password recovery e-mail " "because mail configuration is incomplete.")) mail_from = mail_from or cfg.mail_from logging.debug("send mail, from: {0!r}, subj: {1!r}".format( mail_from, subject)) logging.debug("send mail, to: {0!r}".format(to)) if not to and not cc and not bcc: return 1, _("No recipients, nothing to do") subject = subject.encode(CHARSET) # Create a text/plain body using CRLF (see RFC2822) text = text.replace(u'\n', u'\r\n') text = text.encode(CHARSET) # Create a message using CHARSET and quoted printable # encoding, which should be supported better by mail clients. # TODO: check if its really works better for major mail clients text_msg = Message() charset = Charset(CHARSET) charset.header_encoding = QP charset.body_encoding = QP text_msg.set_charset(charset) # work around a bug in python 2.4.3 and above: text_msg.set_payload('=') if text_msg.as_string().endswith('='): text = charset.body_encode(text) text_msg.set_payload(text) if html: msg = MIMEMultipart('alternative') msg.attach(text_msg) html = html.encode(CHARSET) html_msg = MIMEText(html, 'html') html_msg.set_charset(charset) msg.attach(html_msg) else: msg = text_msg address = encodeAddress(mail_from, charset) msg['From'] = address if to: msg['To'] = ','.join(to) if cc: msg['CC'] = ','.join(cc) msg['Date'] = formatdate() msg['Message-ID'] = make_msgid() msg['Subject'] = Header(subject, charset) # See RFC 3834 section 5: msg['Auto-Submitted'] = 'auto-generated' if cfg.mail_sendmail: if bcc: # Set the BCC. This will be stripped later by sendmail. msg['BCC'] = ','.join(bcc) # Set Return-Path so that it isn't set (generally incorrectly) for us. msg['Return-Path'] = address # Send the message if not cfg.mail_sendmail: try: logging.debug( "trying to send mail (smtp) via smtp server '{0}'".format( cfg.mail_smarthost)) host, port = (cfg.mail_smarthost + ':25').split(':')[:2] server = smtplib.SMTP(host, int(port)) try: # server.set_debuglevel(1) if cfg.mail_username is not None and cfg.mail_password is not None: try: # try to do TLS server.ehlo() if server.has_extn('starttls'): server.starttls() server.ehlo() logging.debug( "tls connection to smtp server established") except Exception: logging.debug( "could not establish a tls connection to smtp server, continuing without tls" ) logging.debug( "trying to log in to smtp server using account '{0}'". format(cfg.mail_username)) server.login(cfg.mail_username, cfg.mail_password) server.sendmail(mail_from, (to or []) + (cc or []) + (bcc or []), msg.as_string()) finally: try: server.quit() except AttributeError: # in case the connection failed, SMTP has no "sock" attribute pass except smtplib.SMTPException as e: logging.exception("smtp mail failed with an exception.") return 0, str(e) except (os.error, socket.error) as e: logging.exception("smtp mail failed with an exception.") return ( 0, _("Connection to mailserver '%(server)s' failed: %(reason)s", server=cfg.mail_smarthost, reason=str(e))) else: try: logging.debug("trying to send mail (sendmail)") sendmailp = os.popen(cfg.mail_sendmail, "w") # msg contains everything we need, so this is a simple write sendmailp.write(msg.as_string()) sendmail_status = sendmailp.close() if sendmail_status: logging.error("sendmail failed with status: {0!s}".format( sendmail_status)) return 0, str(sendmail_status) except Exception: logging.exception("sendmail failed with an exception.") return 0, _("Mail not sent") logging.debug("Mail sent successfully") return 1, _("Mail sent successfully")
def process(mlist, msg, msgdata=None): sanitize = mm_cfg.ARCHIVE_HTML_SANITIZER outer = True if msgdata is None: msgdata = {} if msgdata: # msgdata is available if it is in GLOBAL_PIPELINE # ie. not in digest or archiver # check if the list owner want to scrub regular delivery if not mlist.scrub_nondigest: return dir = calculate_attachments_dir(mlist, msg, msgdata) charset = None lcset = Utils.GetCharSet(mlist.preferred_language) lcset_out = Charset(lcset).output_charset or lcset # Now walk over all subparts of this message and scrub out various types format = delsp = None for part in msg.walk(): ctype = part.get_content_type() # If the part is text/plain, we leave it alone if ctype == 'text/plain': # We need to choose a charset for the scrubbed message, so we'll # arbitrarily pick the charset of the first text/plain part in the # message. # MAS: Also get the RFC 3676 stuff from this part. This seems to # work OK for scrub_nondigest. It will also work as far as # scrubbing messages for the archive is concerned, but pipermail # doesn't pay any attention to the RFC 3676 parameters. The plain # format digest is going to be a disaster in any case as some of # messages will be format="flowed" and some not. ToDigest creates # its own Content-Type: header for the plain digest which won't # have RFC 3676 parameters. If the message Content-Type: headers # are retained for display in the digest, the parameters will be # there for information, but not for the MUA. This is the best we # can do without having get_payload() process the parameters. if charset is None: charset = part.get_content_charset(lcset) format = part.get_param('format') delsp = part.get_param('delsp') # TK: if part is attached then check charset and scrub if none if part.get('content-disposition') and \ not part.get_content_charset(): omask = os.umask(0o02) try: url = save_attachment(mlist, part, dir) finally: os.umask(omask) filename = part.get_filename(_('not available')) filename = Utils.oneline(filename, lcset) replace_payload_by_text( part, _("""\ An embedded and charset-unspecified text was scrubbed... Name: %(filename)s URL: %(url)s """), lcset) elif ctype == 'text/html' and isinstance(sanitize, int): if sanitize == 0: if outer: raise DiscardMessage replace_payload_by_text( part, _('HTML attachment scrubbed and removed'), # Adding charset arg and removing content-type # sets content-type to text/plain lcset) elif sanitize == 2: # By leaving it alone, Pipermail will automatically escape it pass elif sanitize == 3: # Pull it out as an attachment but leave it unescaped. This # is dangerous, but perhaps useful for heavily moderated # lists. omask = os.umask(0o02) try: url = save_attachment(mlist, part, dir, filter_html=False) finally: os.umask(omask) replace_payload_by_text( part, _("""\ An HTML attachment was scrubbed... URL: %(url)s """), lcset) else: # HTML-escape it and store it as an attachment, but make it # look a /little/ bit prettier. :( payload = Utils.websafe(part.get_payload(decode=True)) # For whitespace in the margin, change spaces into # non-breaking spaces, and tabs into 8 of those. Then use a # mono-space font. Still looks hideous to me, but then I'd # just as soon discard them. def doreplace(s): return s.expandtabs(8).replace(' ', ' ') lines = [doreplace(s) for s in payload.split('\n')] payload = '<tt>\n' + BR.join(lines) + '\n</tt>\n' part.set_payload(payload) # We're replacing the payload with the decoded payload so this # will just get in the way. del part['content-transfer-encoding'] omask = os.umask(0o02) try: url = save_attachment(mlist, part, dir, filter_html=False) finally: os.umask(omask) replace_payload_by_text( part, _("""\ An HTML attachment was scrubbed... URL: %(url)s """), lcset) elif ctype == 'message/rfc822': # This part contains a submessage, so it too needs scrubbing submsg = part.get_payload(0) omask = os.umask(0o02) try: url = save_attachment(mlist, part, dir) finally: os.umask(omask) subject = submsg.get('subject', _('no subject')) subject = Utils.oneline(subject, lcset) date = submsg.get('date', _('no date')) who = submsg.get('from', _('unknown sender')) size = len(str(submsg)) replace_payload_by_text( part, _("""\ An embedded message was scrubbed... From: %(who)s Subject: %(subject)s Date: %(date)s Size: %(size)s URL: %(url)s """), lcset) # If the message isn't a multipart, then we'll strip it out as an # attachment that would have to be separately downloaded. Pipermail # will transform the url into a hyperlink. elif part.get_payload() and not part.is_multipart(): payload = part.get_payload(decode=True) ctype = part.get_content_type() # XXX Under email 2.5, it is possible that payload will be None. # This can happen when you have a Content-Type: multipart/* with # only one part and that part has two blank lines between the # first boundary and the end boundary. In email 3.0 you end up # with a string in the payload. I think in this case it's safe to # ignore the part. if payload is None: continue size = len(payload) omask = os.umask(0o02) try: url = save_attachment(mlist, part, dir) finally: os.umask(omask) desc = part.get('content-description', _('not available')) desc = Utils.oneline(desc, lcset) filename = part.get_filename(_('not available')) filename = Utils.oneline(filename, lcset) replace_payload_by_text( part, _("""\ A non-text attachment was scrubbed... Name: %(filename)s Type: %(ctype)s Size: %(size)d bytes Desc: %(desc)s URL: %(url)s """), lcset) outer = False # We still have to sanitize multipart messages to flat text because # Pipermail can't handle messages with list payloads. This is a kludge; # def (n) clever hack ;). if msg.is_multipart(): # By default we take the charset of the first text/plain part in the # message, but if there was none, we'll use the list's preferred # language's charset. if not charset or charset == 'us-ascii': charset = lcset_out else: # normalize to the output charset if input/output are different charset = Charset(charset).output_charset or charset # We now want to concatenate all the parts which have been scrubbed to # text/plain, into a single text/plain payload. We need to make sure # all the characters in the concatenated string are in the same # encoding, so we'll use the 'replace' key in the coercion call. # BAW: Martin's original patch suggested we might want to try # generalizing to utf-8, and that's probably a good idea (eventually). text = [] for part in msg.walk(): # TK: bug-id 1099138 and multipart # MAS test payload - if part may fail if there are no headers. if not part.get_payload() or part.is_multipart(): continue # All parts should be scrubbed to text/plain by now, except # if sanitize == 2, there could be text/html parts so keep them # but skip any other parts. partctype = part.get_content_type() if partctype != 'text/plain' and (partctype != 'text/html' or sanitize != 2): text.append(_('Skipped content of type %(partctype)s\n')) continue try: t = part.get_payload(decode=True) or '' # MAS: TypeError exception can occur if payload is None. This # was observed with a message that contained an attached # message/delivery-status part. Because of the special parsing # of this type, this resulted in a text/plain sub-part with a # null body. See bug 1430236. except (binascii.Error, TypeError): t = part.get_payload() or '' # TK: get_content_charset() returns 'iso-2022-jp' for internally # crafted (scrubbed) 'euc-jp' text part. So, first try # get_charset(), then get_content_charset() for the parts # which are already embeded in the incoming message. partcharset = part.get_charset() if partcharset: partcharset = str(partcharset) else: partcharset = part.get_content_charset() if partcharset and partcharset != charset: try: t = str(t, partcharset, 'replace') except (UnicodeError, LookupError, ValueError, AssertionError): # We can get here if partcharset is bogus in come way. # Replace funny characters. We use errors='replace' t = str(t, 'ascii', 'replace') try: # Should use HTML-Escape, or try generalizing to UTF-8 t = t.encode(charset, 'replace') except (UnicodeError, LookupError, ValueError, AssertionError): # if the message charset is bogus, use the list's. t = t.encode(lcset, 'replace') # Separation is useful if isinstance(t, str): if not t.endswith('\n'): t += '\n' text.append(t) # Now join the text and set the payload sep = _('-------------- next part --------------\n') # The i18n separator is in the list's charset. Coerce it to the # message charset. try: s = str(sep, lcset, 'replace') sep = s.encode(charset, 'replace') except (UnicodeError, LookupError, ValueError, AssertionError): pass replace_payload_by_text(msg, sep.join(text), charset) if format: msg.set_param('Format', format) if delsp: msg.set_param('DelSp', delsp) return msg
def sendMailMessage(self, xMailMessage): COMMASPACE = ', ' if dbg: print("PyMailSMTPService sendMailMessage", file=dbgout) recipients = xMailMessage.getRecipients() sendermail = xMailMessage.SenderAddress sendername = xMailMessage.SenderName subject = xMailMessage.Subject ccrecipients = xMailMessage.getCcRecipients() bccrecipients = xMailMessage.getBccRecipients() if dbg: print("PyMailSMTPService subject: " + subject, file=dbgout) print("PyMailSMTPService from: " + sendername, file=dbgout) print("PyMailSMTPService from: " + sendermail, file=dbgout) print("PyMailSMTPService send to: %s" % (recipients, ), file=dbgout) attachments = xMailMessage.getAttachments() textmsg = Message() content = xMailMessage.Body flavors = content.getTransferDataFlavors() if dbg: print("PyMailSMTPService flavors len: %d" % (len(flavors), ), file=dbgout) #Use first flavor that's sane for an email body for flavor in flavors: if flavor.MimeType.find('text/html') != -1 or flavor.MimeType.find( 'text/plain') != -1: if dbg: print("PyMailSMTPService mimetype is: " + flavor.MimeType, file=dbgout) textbody = content.getTransferData(flavor) if len(textbody): mimeEncoding = re.sub("charset=.*", "charset=UTF-8", flavor.MimeType) if mimeEncoding.find('charset=UTF-8') == -1: mimeEncoding = mimeEncoding + "; charset=UTF-8" textmsg['Content-Type'] = mimeEncoding textmsg['MIME-Version'] = '1.0' try: #it's a string, get it as utf-8 bytes textbody = textbody.encode('utf-8') except: #it's a bytesequence, get raw bytes textbody = textbody.value textbody = textbody.decode('utf-8') c = Charset('utf-8') c.body_encoding = QP textmsg.set_payload(textbody, c) break if (len(attachments)): msg = MIMEMultipart() msg.epilogue = '' msg.attach(textmsg) else: msg = textmsg hdr = Header(sendername, 'utf-8') hdr.append('<' + sendermail + '>', 'us-ascii') msg['Subject'] = subject msg['From'] = hdr msg['To'] = COMMASPACE.join(recipients) if len(ccrecipients): msg['Cc'] = COMMASPACE.join(ccrecipients) if xMailMessage.ReplyToAddress != '': msg['Reply-To'] = xMailMessage.ReplyToAddress mailerstring = "LibreOffice via Caolan's mailmerge component" try: ctx = uno.getComponentContext() aConfigProvider = ctx.ServiceManager.createInstance( "com.sun.star.configuration.ConfigurationProvider") prop = uno.createUnoStruct('com.sun.star.beans.PropertyValue') prop.Name = "nodepath" prop.Value = "/org.openoffice.Setup/Product" aSettings = aConfigProvider.createInstanceWithArguments( "com.sun.star.configuration.ConfigurationAccess", (prop, )) mailerstring = aSettings.getByName("ooName") + " " + \ aSettings.getByName("ooSetupVersion") + " via Caolan's mailmerge component" except: pass msg['X-Mailer'] = mailerstring msg['Date'] = formatdate(localtime=True) for attachment in attachments: content = attachment.Data flavors = content.getTransferDataFlavors() flavor = flavors[0] ctype = flavor.MimeType maintype, subtype = ctype.split('/', 1) msgattachment = MIMEBase(maintype, subtype) data = content.getTransferData(flavor) msgattachment.set_payload(data.value) encode_base64(msgattachment) fname = attachment.ReadableName try: msgattachment.add_header('Content-Disposition', 'attachment', \ filename=fname) except: msgattachment.add_header('Content-Disposition', 'attachment', \ filename=('utf-8','',fname)) if dbg: print(("PyMailSMTPService attachmentheader: ", str(msgattachment)), file=dbgout) msg.attach(msgattachment) uniquer = {} for key in recipients: uniquer[key] = True if len(ccrecipients): for key in ccrecipients: uniquer[key] = True if len(bccrecipients): for key in bccrecipients: uniquer[key] = True truerecipients = uniquer.keys() if dbg: print(("PyMailSMTPService recipients are: ", truerecipients), file=dbgout) self.server.sendmail(sendermail, truerecipients, msg.as_string())
def fix_text_required(encodingname): from email.charset import Charset, BASE64, QP charset = Charset(encodingname) bodyenc = charset.body_encoding return bodyenc in (None, QP)
def send_email(smtp, recipients, cc, subject, content, csv_reports): """Sends an email with attachment. Refer to https://gist.github.com/BietteMaxime/f75ae41f7b4557274a9f Args: smtp: A dictionary containing smtp info: - smtp_url - smtp_auth_username # optional - smtp_auth_password # optional - smtp_from recipients: To whom to send the email. cc: To whom to cc the email. subject: Email subject. content: Email body content csv_reports: List of dictionaries containing "filename", "data" to construct CSV attachments. Returns: None """ if not isinstance(smtp, dict): logger.warning("smtp is not a dictionary. Skip.") return sender = smtp.get("smtp_from", None) smtp_url = smtp.get("smtp_url", None) smtp_auth_username = smtp.get("smtp_auth_username", None) smtp_auth_password = smtp.get("smtp_auth_password", None) if sender is None or smtp_url is None: logger.warning("Some fields in smtp %s is None. Skip.", smtp) return # Create message container - the correct MIME type is multipart/mixed # to allow attachment. full_email = MIMEMultipart("mixed") full_email["Subject"] = subject full_email["From"] = sender full_email["To"] = ", ".join(recipients) full_email["CC"] = ", ".join(cc) # Create the body of the message (a plain-text version). content = content.encode(ENCODING) content = MIMEText(content, "plain", _charset=ENCODING) full_email.attach(content) # Create the attachment of the message in text/csv. for report in csv_reports: attachment = MIMENonMultipart("text", "csv", charset=ENCODING) attachment.add_header("Content-Disposition", "attachment", filename=report["filename"]) cs = Charset(ENCODING) cs.body_encoding = BASE64 attachment.set_payload(report["data"].encode(ENCODING), charset=cs) full_email.attach(attachment) try: with smtplib.SMTP(smtp_url) as server: if smtp_auth_username is not None and smtp_auth_password is not None: server.starttls() server.login(smtp_auth_username, smtp_auth_password) receivers = recipients + cc server.sendmail(sender, receivers, full_email.as_string()) logger.info("Successfully sent email to %s and cc %s", ", ".join(recipients), ", ".join(cc)) except smtplib.SMTPAuthenticationError: logger.warning("The server didn\'t accept the user\\password " "combination.") except smtplib.SMTPServerDisconnected: logger.warning("Server unexpectedly disconnected") except smtplib.SMTPException as e: logger.exception("SMTP error occurred: %s", e)
def sendMailMessage(self, xMailMessage): COMMASPACE = ', ' if dbg: print("PyMailSMTPService sendMailMessage", file=dbgout) recipients = xMailMessage.getRecipients() sendermail = xMailMessage.SenderAddress sendername = xMailMessage.SenderName subject = xMailMessage.Subject ccrecipients = xMailMessage.getCcRecipients() bccrecipients = xMailMessage.getBccRecipients() if dbg: print("PyMailSMTPService subject: " + subject, file=dbgout) print("PyMailSMTPService from: " + sendername, file=dbgout) print("PyMailSMTPService from: " + sendermail, file=dbgout) print("PyMailSMTPService send to: %s" % (recipients, ), file=dbgout) attachments = xMailMessage.getAttachments() textmsg = Message() content = xMailMessage.Body flavors = content.getTransferDataFlavors() if dbg: print("PyMailSMTPService flavors len: %d" % (len(flavors), ), file=dbgout) #Use first flavor that's sane for an email body for flavor in flavors: if flavor.MimeType.find('text/html') != -1 or flavor.MimeType.find( 'text/plain') != -1: if dbg: print("PyMailSMTPService mimetype is: " + flavor.MimeType, file=dbgout) textbody = content.getTransferData(flavor) if len(textbody): mimeEncoding = re.sub("charset=.*", "charset=UTF-8", flavor.MimeType) if mimeEncoding.find('charset=UTF-8') == -1: mimeEncoding = mimeEncoding + "; charset=UTF-8" textmsg['Content-Type'] = mimeEncoding textmsg['MIME-Version'] = '1.0' try: #it's a string, get it as utf-8 bytes textbody = textbody.encode('utf-8') except: #it's a bytesequence, get raw bytes textbody = textbody.value if sys.version >= '3': if sys.version_info.minor < 3 or ( sys.version_info.minor == 3 and sys.version_info.micro <= 1): #http://stackoverflow.com/questions/9403265/how-do-i-use-python-3-2-email-module-to-send-unicode-messages-encoded-in-utf-8-w #see http://bugs.python.org/16564, etc. basically it now *seems* to be all ok #in python 3.3.2 onwards, but a little busted in 3.3.0 textbody = textbody.decode('iso8859-1') else: textbody = textbody.decode('utf-8') c = Charset('utf-8') c.body_encoding = QP textmsg.set_payload(textbody, c) else: textmsg.set_payload(textbody) break if (len(attachments)): msg = MIMEMultipart() msg.epilogue = '' msg.attach(textmsg) else: msg = textmsg hdr = Header(sendername, 'utf-8') hdr.append('<' + sendermail + '>', 'us-ascii') msg['Subject'] = subject msg['From'] = hdr msg['To'] = COMMASPACE.join(recipients) if len(ccrecipients): msg['Cc'] = COMMASPACE.join(ccrecipients) if xMailMessage.ReplyToAddress != '': msg['Reply-To'] = xMailMessage.ReplyToAddress mailerstring = "LibreOffice via Caolan's mailmerge component" try: configuration = self._getConfiguration( "/org.openoffice.Setup/Product") mailerstring = "%s %s via Caolan's mailmerge component" % ( configuration.getByName("ooName"), configuration.getByName("ooSetupVersion")) except: pass msg['X-Mailer'] = mailerstring msg['Date'] = formatdate(localtime=True) for attachment in attachments: content = attachment.Data flavors = content.getTransferDataFlavors() flavor = flavors[0] ctype = flavor.MimeType maintype, subtype = ctype.split('/', 1) msgattachment = MIMEBase(maintype, subtype) data = content.getTransferData(flavor) msgattachment.set_payload(data.value) encode_base64(msgattachment) fname = attachment.ReadableName try: msgattachment.add_header('Content-Disposition', 'attachment', \ filename=fname) except: msgattachment.add_header('Content-Disposition', 'attachment', \ filename=('utf-8','',fname)) if dbg: print(("PyMailSMTPService attachmentheader: ", str(msgattachment)), file=dbgout) msg.attach(msgattachment) uniquer = {} for key in recipients: uniquer[key] = True if len(ccrecipients): for key in ccrecipients: uniquer[key] = True if len(bccrecipients): for key in bccrecipients: uniquer[key] = True truerecipients = uniquer.keys() if dbg: print(("PyMailSMTPService recipients are: ", truerecipients), file=dbgout) self.server.sendmail(sendermail, truerecipients, msg.as_string())
def as_message(self, escape_addresses=True): # http://wordeology.com/computer/how-to-send-good-unicode-email-with-python.html # http://stackoverflow.com/questions/31714221/how-to-send-an-email-with-quoted # http://stackoverflow.com/questions/9403265/how-do-i-use-python/9509718#9509718 charset = Charset('utf-8') charset.header_encoding = QP charset.body_encoding = QP msg = MIMEMultipart() # Headers unixfrom = "From %s %s" % (self.sender.address, self.archived_date.strftime("%c")) header_from = self.sender.address if self.sender_name and self.sender_name != self.sender.address: header_from = "%s <%s>" % (self.sender_name, header_from) header_to = self.mailinglist.name if escape_addresses: header_from = header_from.replace("@", " at ") header_to = header_to.replace("@", " at ") unixfrom = unixfrom.replace("@", " at ") msg.set_unixfrom(unixfrom) headers = ( ("From", header_from), ("To", header_to), ("Subject", self.subject), ) for header_name, header_value in headers: if not header_value: continue try: msg[header_name] = header_value.encode('ascii') except UnicodeEncodeError: msg[header_name] = Header(header_value.encode('utf-8'), charset).encode() tz = get_fixed_timezone(self.timezone) header_date = self.date.astimezone(tz).replace(microsecond=0) # Date format: http://tools.ietf.org/html/rfc5322#section-3.3 msg["Date"] = header_date.strftime("%a, %d %b %Y %H:%M:%S %z") msg["Message-ID"] = "<%s>" % self.message_id if self.in_reply_to: msg["In-Reply-To"] = self.in_reply_to # Body content = self.ADDRESS_REPLACE_RE.sub(r"\1(a)\2", self.content) # Don't use MIMEText, it won't encode to quoted-printable textpart = MIMENonMultipart("text", "plain", charset='utf-8') textpart.set_payload(content, charset=charset) msg.attach(textpart) # Attachments for attachment in self.attachments.order_by("counter"): mimetype = attachment.content_type.split('/', 1) part = MIMEBase(mimetype[0], mimetype[1]) part.set_payload(attachment.content) encode_base64(part) part.add_header('Content-Disposition', 'attachment', filename=attachment.name) msg.attach(part) return msg
# Functions for sending email # # Copyright (c) 2009 UK Citizens Online Democracy. All rights reserved. # Email: [email protected]; WWW: http://www.mysociety.org/ # # $Id: sendemail.py,v 1.5 2009/12/17 17:31:04 francis dead $ # import re, smtplib from minimock import mock, Mock from email.message import Message from email.header import Header from email.utils import formataddr, make_msgid, formatdate from email.charset import Charset, QP charset = Charset('utf-8') charset.body_encoding = QP def send_email(sender, to, message, headers={}): """Sends MESSAGE from SENDER to TO, with HEADERS Returns True if successful, False if not >>> mock('smtplib.SMTP', returns=Mock('smtp_connection')) >>> send_email("[email protected]", "[email protected]", "Hello, this is a message!", { ... 'Subject': 'Mapumental message', ... 'From': ("[email protected]", "Ms. A"), ... 'To': "[email protected]" ... }) # doctest:+ELLIPSIS Called smtplib.SMTP('localhost') Called smtp_connection.sendmail(
def _contact(self, address): if isinstance(address, tuple): return formataddr((Charset('utf-8').header_encode(address[0]), address[1])) else: return address
def verpdeliver(mlist, msg, msgdata, envsender, failures, conn): for recip in msgdata['recips']: # We now need to stitch together the message with its header and # footer. If we're VERPIng, we have to calculate the envelope sender # for each recipient. Note that the list of recipients must be of # length 1. # # BAW: ezmlm includes the message number in the envelope, used when # sending a notification to the user telling her how many messages # they missed due to bouncing. Neat idea. msgdata['recips'] = [recip] # Make a copy of the message and decorate + delivery that msgcopy = copy.deepcopy(msg) Decorate.process(mlist, msgcopy, msgdata) # Calculate the envelope sender, which we may be VERPing if msgdata.get('verp'): bmailbox, bdomain = Utils.ParseEmail(envsender) rmailbox, rdomain = Utils.ParseEmail(recip) if rdomain is None: # The recipient address is not fully-qualified. We can't # deliver it to this person, nor can we craft a valid verp # header. I don't think there's much we can do except ignore # this recipient. syslog('smtp', 'Skipping VERP delivery to unqual recip: %s', recip) continue d = {'bounces': bmailbox, 'mailbox': rmailbox, 'host' : DOT.join(rdomain), } envsender = '%s@%s' % ((mm_cfg.VERP_FORMAT % d), DOT.join(bdomain)) if mlist.personalize == 2: # When fully personalizing, we want the To address to point to the # recipient, not to the mailing list del msgcopy['to'] name = None if mlist.isMember(recip): name = mlist.getMemberName(recip) if name: # Convert the name to an email-safe representation. If the # name is a byte string, convert it first to Unicode, given # the character set of the member's language, replacing bad # characters for which we can do nothing about. Once we have # the name as Unicode, we can create a Header instance for it # so that it's properly encoded for email transport. charset = Utils.GetCharSet(mlist.getMemberLanguage(recip)) if charset == 'us-ascii': # Since Header already tries both us-ascii and utf-8, # let's add something a bit more useful. charset = 'iso-8859-1' charset = Charset(charset) codec = charset.input_codec or 'ascii' if not isinstance(name, str): name = str(name, codec, 'replace') name = Header(name, charset).encode() msgcopy['To'] = formataddr((name, recip)) else: msgcopy['To'] = recip # We can flag the mail as a duplicate for each member, if they've # already received this message, as calculated by Message-ID. See # AvoidDuplicates.py for details. del msgcopy['x-mailman-copy'] if recip in msgdata.get('add-dup-header', {}): msgcopy['X-Mailman-Copy'] = 'yes' # If desired, add the RCPT_BASE64_HEADER_NAME header if len(mm_cfg.RCPT_BASE64_HEADER_NAME) > 0: del msgcopy[mm_cfg.RCPT_BASE64_HEADER_NAME] msgcopy[mm_cfg.RCPT_BASE64_HEADER_NAME] = b64encode(recip) # For the final delivery stage, we can just bulk deliver to a party of # one. ;) bulkdeliver(mlist, msgcopy, msgdata, envsender, failures, conn)
#Embedded file name: email\header.py """Header encoding and decoding functionality.""" __all__ = ['Header', 'decode_header', 'make_header'] import re import binascii import email.quoprimime import email.base64mime from email.errors import HeaderParseError from email.charset import Charset NL = '\n' SPACE = ' ' USPACE = u' ' SPACE8 = ' ' * 8 UEMPTYSTRING = u'' MAXLINELEN = 76 USASCII = Charset('us-ascii') UTF8 = Charset('utf-8') ecre = re.compile('\n =\\? # literal =?\n (?P<charset>[^?]*?) # non-greedy up to the next ? is the charset\n \\? # literal ?\n (?P<encoding>[qb]) # either a "q" or a "b", case insensitive\n \\? # literal ?\n (?P<encoded>.*?) # non-greedy up to the next ?= is the encoded string\n \\?= # literal ?=\n (?=[ \\t]|$) # whitespace or the end of the string\n ', re.VERBOSE | re.IGNORECASE | re.MULTILINE) fcre = re.compile('[\\041-\\176]+:$') _max_append = email.quoprimime._max_append def decode_header(header): """Decode a message header value without converting charset. Returns a list of (decoded_string, charset) pairs containing each of the decoded parts of the header. Charset is None for non-encoded parts of the header, otherwise a lower-case string containing the name of the character set specified in the encoded string. An email.errors.HeaderParseError may be raised when certain decoding error occurs (e.g. a base64 decoding exception).
def send(self, emails): if isinstance(emails, Email): emails = [emails] if len([e for e in emails if e.__class__ != Email]): raise TypeError('emails must be Email or list of Email instances') smtpclass = SMTP_SSL if self.ssl else SMTP if self.server == 'localhost': smtp = smtpclass(self.server) else: smtp = smtpclass(self.server, self.port) if self.login and self.password: smtp.login(self.login, self.password) for email in emails: c = Charset(email.charset) c.header_encoding = QP c.body_encoding = 0 r = Charset(email.charset) r.header_encoding = 0 r.body_encoding = 0 mime1, mime2 = email.mimetype.split('/') mainpart = MIMEBase(mime1, mime2) if not email.force_7bit: mainpart.set_param('charset', email.charset) if len(email.attachments): message = MIMEMultipart('mixed') message.attach(mainpart) del mainpart['mime-version'] else: message = mainpart message['Date'] = datetime.datetime.now().strftime( '%a, %d %b %Y %H:%M:%S') + (" +%04d" % (time.timezone / -36, )) h = Header() fromname = self.fromname.encode(email.charset, 'xmlcharrefreplace') h.append(fromname, r if is7bit(fromname) else c) h.append('<%s>' % self.email, r) message['From'] = h message['To'] = email.get_emails_header('rcpt') if len(email.cc): message['CC'] = email.get_emails_header('cc') if len(email.bcc): message['BCC'] = email.get_emails_header('bcc') subject = email.subject.encode(email.charset, 'xmlcharrefreplace') message['Subject'] = Header(subject, r if is7bit(subject) else c) if email.force_7bit: body = email.body.encode('ascii', 'xmlcharrefreplace') else: body = email.body.encode(email.charset, 'xmlcharrefreplace') mainpart.set_payload(body) for hn, hv in email.headers.items(): if hn == 'X-Mailgun-Campaign-Tag': hn = 'X-Mailgun-Tag' message[hn] = hv if is7bit(body): mainpart['Content-Transfer-Encoding'] = '7bit' else: encode_quopri(mainpart) for attachment in email.attachments: if attachment.__class__ != Attachment: raise TypeError("invalid attachment") mimetype = attachment.mimetype if not mimetype: mimetype, encoding = guess_type(attachment.filename) if not mimetype: mimetype = 'application/octet-stream' mime1, mime2 = mimetype.split('/') part = MIMEBase(mime1, mime2) # using newer rfc2231 (not supported by Outlook): # part.set_param('name', attachment.filename.encode('utf-8'), charset = 'utf-8') # hack: using deprecated rfc2047 - supported by Outlook: part.set_param('name', str(Header(attachment.filename))) del part['mime-version'] if attachment.id: part['Content-Disposition'] = 'inline' else: part['Content-Disposition'] = 'attachment' # using newer rfc2231 (not supported by Outlook): # part.set_param('filename', # attachment.filename.encode('utf-8'), # 'Content-Disposition', # charset = 'utf-8') # hack: using deprecated rfc2047 - supported by Outlook: part.set_param('filename', str(Header(attachment.filename)), 'Content-Disposition') if attachment.id: part['Content-ID'] = '<%s>' % attachment.id part.set_payload(attachment.content) encode_base64(part) message.attach(part) emails = email.rcpt + email.cc + email.bcc smtp.sendmail(self.email, [email for _, email in emails], message.as_string()) smtp.quit()