2007-06-27 17:44:55 +08:00
|
|
|
import mimetypes
|
2015-01-28 20:35:27 +08:00
|
|
|
from email import charset as Charset
|
|
|
|
from email import encoders as Encoders
|
|
|
|
from email import generator, message_from_string
|
2019-04-30 00:48:20 +08:00
|
|
|
from email.errors import HeaderParseError
|
2015-01-28 20:35:27 +08:00
|
|
|
from email.header import Header
|
2019-04-30 00:48:20 +08:00
|
|
|
from email.headerregistry import Address, parser
|
2013-08-21 09:17:26 +08:00
|
|
|
from email.message import Message
|
2011-09-10 00:18:38 +08:00
|
|
|
from email.mime.base import MIMEBase
|
2013-08-21 09:17:26 +08:00
|
|
|
from email.mime.message import MIMEMessage
|
2015-01-28 20:35:27 +08:00
|
|
|
from email.mime.multipart import MIMEMultipart
|
|
|
|
from email.mime.text import MIMEText
|
2020-07-15 13:30:15 +08:00
|
|
|
from email.utils import formataddr, formatdate, getaddresses, make_msgid
|
2017-01-07 19:11:46 +08:00
|
|
|
from io import BytesIO, StringIO
|
2017-07-22 04:33:26 +08:00
|
|
|
from pathlib import Path
|
2008-03-22 05:52:34 +08:00
|
|
|
|
|
|
|
from django.conf import settings
|
2009-11-03 20:53:26 +08:00
|
|
|
from django.core.mail.utils import DNS_NAME
|
2019-07-03 01:32:17 +08:00
|
|
|
from django.utils.encoding import force_str, punycode
|
2005-07-13 09:25:57 +08:00
|
|
|
|
2007-05-03 20:08:31 +08:00
|
|
|
# Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from
|
|
|
|
# some spam filters.
|
2013-08-20 07:04:50 +08:00
|
|
|
utf8_charset = Charset.Charset("utf-8")
|
|
|
|
utf8_charset.body_encoding = None # Python defaults to BASE64
|
2016-04-18 03:03:15 +08:00
|
|
|
utf8_charset_qp = Charset.Charset("utf-8")
|
|
|
|
utf8_charset_qp.body_encoding = Charset.QP
|
2007-05-03 20:08:31 +08:00
|
|
|
|
2007-06-27 17:44:55 +08:00
|
|
|
# Default MIME type to use on attachments (if it is not explicitly given
|
|
|
|
# and cannot be guessed).
|
|
|
|
DEFAULT_ATTACHMENT_MIME_TYPE = "application/octet-stream"
|
|
|
|
|
2016-04-18 03:03:15 +08:00
|
|
|
RFC5322_EMAIL_LINE_LENGTH_LIMIT = 998
|
|
|
|
|
2007-02-17 14:01:17 +08:00
|
|
|
|
2009-11-03 20:53:26 +08:00
|
|
|
class BadHeaderError(ValueError):
|
|
|
|
pass
|
2007-02-17 14:01:17 +08:00
|
|
|
|
2006-11-10 11:28:58 +08:00
|
|
|
|
2011-01-15 13:55:24 +08:00
|
|
|
# Header names that contain structured address data (RFC #5322)
|
2014-09-26 20:31:50 +08:00
|
|
|
ADDRESS_HEADERS = {
|
2011-01-15 13:55:24 +08:00
|
|
|
"from",
|
|
|
|
"sender",
|
|
|
|
"reply-to",
|
|
|
|
"to",
|
|
|
|
"cc",
|
|
|
|
"bcc",
|
|
|
|
"resent-from",
|
|
|
|
"resent-sender",
|
|
|
|
"resent-to",
|
|
|
|
"resent-cc",
|
|
|
|
"resent-bcc",
|
2014-09-26 20:31:50 +08:00
|
|
|
}
|
2011-01-15 13:55:24 +08:00
|
|
|
|
|
|
|
|
2010-03-06 05:27:58 +08:00
|
|
|
def forbid_multi_line_headers(name, val, encoding):
|
2017-01-26 03:02:33 +08:00
|
|
|
"""Forbid multi-line headers to prevent header injection."""
|
2010-03-06 23:50:12 +08:00
|
|
|
encoding = encoding or settings.DEFAULT_CHARSET
|
2017-04-22 01:52:26 +08:00
|
|
|
val = str(val) # val may be lazy
|
2008-01-02 16:39:03 +08:00
|
|
|
if "\n" in val or "\r" in val:
|
|
|
|
raise BadHeaderError(
|
|
|
|
"Header values can't contain newlines (got %r for header %r)" % (val, name)
|
2022-02-04 03:24:19 +08:00
|
|
|
)
|
2008-01-02 16:39:03 +08:00
|
|
|
try:
|
2012-08-09 18:12:22 +08:00
|
|
|
val.encode("ascii")
|
2008-01-02 16:39:03 +08:00
|
|
|
except UnicodeEncodeError:
|
2011-01-15 13:55:24 +08:00
|
|
|
if name.lower() in ADDRESS_HEADERS:
|
2016-03-29 06:33:29 +08:00
|
|
|
val = ", ".join(
|
|
|
|
sanitize_address(addr, encoding) for addr in getaddresses((val,))
|
2022-02-04 03:24:19 +08:00
|
|
|
)
|
2008-01-02 16:39:03 +08:00
|
|
|
else:
|
2012-08-09 18:12:22 +08:00
|
|
|
val = Header(val, encoding).encode()
|
2008-08-23 21:31:28 +08:00
|
|
|
else:
|
|
|
|
if name.lower() == "subject":
|
2012-08-09 18:12:22 +08:00
|
|
|
val = Header(val).encode()
|
2017-01-20 17:20:53 +08:00
|
|
|
return name, val
|
2008-01-02 13:29:10 +08:00
|
|
|
|
2011-01-15 13:55:24 +08:00
|
|
|
|
|
|
|
def sanitize_address(addr, encoding):
|
2016-04-02 17:41:47 +08:00
|
|
|
"""
|
|
|
|
Format a pair of (name, address) or an email address string.
|
|
|
|
"""
|
2019-04-30 00:48:20 +08:00
|
|
|
address = None
|
2015-02-26 05:17:15 +08:00
|
|
|
if not isinstance(addr, tuple):
|
2019-04-30 00:48:20 +08:00
|
|
|
addr = force_str(addr)
|
|
|
|
try:
|
|
|
|
token, rest = parser.get_mailbox(addr)
|
|
|
|
except (HeaderParseError, ValueError, IndexError):
|
|
|
|
raise ValueError('Invalid address "%s"' % addr)
|
|
|
|
else:
|
|
|
|
if rest:
|
|
|
|
# The entire email address must be parsed.
|
|
|
|
raise ValueError(
|
2019-08-05 23:47:50 +08:00
|
|
|
'Invalid address; only %s could be parsed from "%s"' % (token, addr)
|
2019-04-30 00:48:20 +08:00
|
|
|
)
|
|
|
|
nm = token.display_name or ""
|
|
|
|
localpart = token.local_part
|
|
|
|
domain = token.domain or ""
|
|
|
|
else:
|
|
|
|
nm, address = addr
|
|
|
|
localpart, domain = address.rsplit("@", 1)
|
|
|
|
|
2020-07-15 13:30:15 +08:00
|
|
|
address_parts = nm + localpart + domain
|
|
|
|
if "\n" in address_parts or "\r" in address_parts:
|
|
|
|
raise ValueError("Invalid address; address parts cannot contain newlines.")
|
|
|
|
|
2019-04-30 00:48:20 +08:00
|
|
|
# Avoid UTF-8 encode, if it's possible.
|
2020-07-15 13:30:15 +08:00
|
|
|
try:
|
|
|
|
nm.encode("ascii")
|
|
|
|
nm = Header(nm).encode()
|
|
|
|
except UnicodeEncodeError:
|
|
|
|
nm = Header(nm, encoding).encode()
|
2011-01-15 13:55:24 +08:00
|
|
|
try:
|
2019-04-30 00:48:20 +08:00
|
|
|
localpart.encode("ascii")
|
|
|
|
except UnicodeEncodeError:
|
|
|
|
localpart = Header(localpart, encoding).encode()
|
2019-07-03 01:32:17 +08:00
|
|
|
domain = punycode(domain)
|
2019-04-30 00:48:20 +08:00
|
|
|
|
2020-07-15 13:30:15 +08:00
|
|
|
parsed_address = Address(username=localpart, domain=domain)
|
|
|
|
return formataddr((nm, parsed_address.addr_spec))
|
2011-01-15 13:55:24 +08:00
|
|
|
|
|
|
|
|
2017-01-30 01:06:26 +08:00
|
|
|
class MIMEMixin:
|
2014-10-09 20:18:31 +08:00
|
|
|
def as_string(self, unixfrom=False, linesep="\n"):
|
2013-08-21 09:17:26 +08:00
|
|
|
"""Return the entire formatted message as a string.
|
|
|
|
Optional `unixfrom' when True, means include the Unix From_ envelope
|
|
|
|
header.
|
|
|
|
|
|
|
|
This overrides the default as_string() implementation to not mangle
|
|
|
|
lines that begin with 'From '. See bug #13433 for details.
|
|
|
|
"""
|
2017-01-07 19:11:46 +08:00
|
|
|
fp = StringIO()
|
2013-12-29 01:35:17 +08:00
|
|
|
g = generator.Generator(fp, mangle_from_=False)
|
2016-12-01 18:38:01 +08:00
|
|
|
g.flatten(self, unixfrom=unixfrom, linesep=linesep)
|
2013-12-29 01:35:17 +08:00
|
|
|
return fp.getvalue()
|
|
|
|
|
2016-12-01 18:38:01 +08:00
|
|
|
def as_bytes(self, unixfrom=False, linesep="\n"):
|
|
|
|
"""Return the entire formatted message as bytes.
|
|
|
|
Optional `unixfrom' when True, means include the Unix From_ envelope
|
|
|
|
header.
|
2013-12-29 01:35:17 +08:00
|
|
|
|
2016-12-01 18:38:01 +08:00
|
|
|
This overrides the default as_bytes() implementation to not mangle
|
|
|
|
lines that begin with 'From '. See bug #13433 for details.
|
|
|
|
"""
|
|
|
|
fp = BytesIO()
|
|
|
|
g = generator.BytesGenerator(fp, mangle_from_=False)
|
|
|
|
g.flatten(self, unixfrom=unixfrom, linesep=linesep)
|
|
|
|
return fp.getvalue()
|
2013-12-28 19:40:10 +08:00
|
|
|
|
2013-08-21 09:17:26 +08:00
|
|
|
|
2013-12-28 19:40:10 +08:00
|
|
|
class SafeMIMEMessage(MIMEMixin, MIMEMessage):
|
|
|
|
def __setitem__(self, name, val):
|
|
|
|
# message/rfc822 attachments must be ASCII
|
|
|
|
name, val = forbid_multi_line_headers(name, val, "ascii")
|
|
|
|
MIMEMessage.__setitem__(self, name, val)
|
|
|
|
|
|
|
|
|
|
|
|
class SafeMIMEText(MIMEMixin, MIMEText):
|
2014-11-23 09:12:24 +08:00
|
|
|
def __init__(self, _text, _subtype="plain", _charset=None):
|
|
|
|
self.encoding = _charset
|
2016-10-12 02:53:26 +08:00
|
|
|
MIMEText.__init__(self, _text, _subtype=_subtype, _charset=_charset)
|
2011-01-15 13:55:24 +08:00
|
|
|
|
|
|
|
def __setitem__(self, name, val):
|
2010-03-06 05:27:58 +08:00
|
|
|
name, val = forbid_multi_line_headers(name, val, self.encoding)
|
2007-07-01 14:32:34 +08:00
|
|
|
MIMEText.__setitem__(self, name, val)
|
2007-06-27 17:44:55 +08:00
|
|
|
|
2016-10-12 02:53:26 +08:00
|
|
|
def set_payload(self, payload, charset=None):
|
2018-10-23 03:21:33 +08:00
|
|
|
if charset == "utf-8" and not isinstance(charset, Charset.Charset):
|
2017-01-06 17:33:53 +08:00
|
|
|
has_long_lines = any(
|
2020-05-12 14:52:23 +08:00
|
|
|
len(line.encode()) > RFC5322_EMAIL_LINE_LENGTH_LIMIT
|
|
|
|
for line in payload.splitlines()
|
2017-01-06 17:33:53 +08:00
|
|
|
)
|
2016-10-12 02:53:26 +08:00
|
|
|
# Quoted-Printable encoding has the side effect of shortening long
|
|
|
|
# lines, if any (#22561).
|
|
|
|
charset = utf8_charset_qp if has_long_lines else utf8_charset
|
|
|
|
MIMEText.set_payload(self, payload, charset=charset)
|
|
|
|
|
2011-01-15 13:55:24 +08:00
|
|
|
|
2013-12-28 19:40:10 +08:00
|
|
|
class SafeMIMEMultipart(MIMEMixin, MIMEMultipart):
|
2010-03-06 05:27:58 +08:00
|
|
|
def __init__(
|
|
|
|
self, _subtype="mixed", boundary=None, _subparts=None, encoding=None, **_params
|
|
|
|
):
|
|
|
|
self.encoding = encoding
|
|
|
|
MIMEMultipart.__init__(self, _subtype, boundary, _subparts, **_params)
|
2011-01-15 13:55:24 +08:00
|
|
|
|
2007-07-01 14:32:34 +08:00
|
|
|
def __setitem__(self, name, val):
|
2010-03-06 05:27:58 +08:00
|
|
|
name, val = forbid_multi_line_headers(name, val, self.encoding)
|
2007-07-01 14:32:34 +08:00
|
|
|
MIMEMultipart.__setitem__(self, name, val)
|
2005-12-30 04:33:56 +08:00
|
|
|
|
2011-01-15 13:55:24 +08:00
|
|
|
|
2017-01-19 15:39:46 +08:00
|
|
|
class EmailMessage:
|
2017-01-26 03:02:33 +08:00
|
|
|
"""A container for email information."""
|
2007-06-27 20:18:05 +08:00
|
|
|
|
2007-06-27 17:44:55 +08:00
|
|
|
content_subtype = "plain"
|
2009-06-12 21:56:40 +08:00
|
|
|
mixed_subtype = "mixed"
|
2007-06-27 17:44:55 +08:00
|
|
|
encoding = None # None => use settings default
|
2022-02-04 03:24:19 +08:00
|
|
|
|
2007-06-27 17:44:55 +08:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
subject="",
|
|
|
|
body="",
|
|
|
|
from_email=None,
|
2014-11-26 07:05:44 +08:00
|
|
|
to=None,
|
|
|
|
bcc=None,
|
|
|
|
connection=None,
|
|
|
|
attachments=None,
|
|
|
|
headers=None,
|
|
|
|
cc=None,
|
|
|
|
reply_to=None,
|
|
|
|
):
|
Merged Unicode branch into trunk (r4952:5608). This should be fully
backwards compatible for all practical purposes.
Fixed #2391, #2489, #2996, #3322, #3344, #3370, #3406, #3432, #3454, #3492, #3582, #3690, #3878, #3891, #3937, #4039, #4141, #4227, #4286, #4291, #4300, #4452, #4702
git-svn-id: http://code.djangoproject.com/svn/django/trunk@5609 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-07-04 20:11:04 +08:00
|
|
|
"""
|
2008-03-22 05:52:34 +08:00
|
|
|
Initialize a single email message (which can be sent to multiple
|
Merged Unicode branch into trunk (r4952:5608). This should be fully
backwards compatible for all practical purposes.
Fixed #2391, #2489, #2996, #3322, #3344, #3370, #3406, #3432, #3454, #3492, #3582, #3690, #3878, #3891, #3937, #4039, #4141, #4227, #4286, #4291, #4300, #4452, #4702
git-svn-id: http://code.djangoproject.com/svn/django/trunk@5609 bcc190cf-cafb-0310-a4f2-bffc1f526a37
2007-07-04 20:11:04 +08:00
|
|
|
recipients).
|
|
|
|
"""
|
2007-12-19 12:51:35 +08:00
|
|
|
if to:
|
2016-12-29 23:27:49 +08:00
|
|
|
if isinstance(to, str):
|
2014-11-27 10:16:31 +08:00
|
|
|
raise TypeError('"to" argument must be a list or tuple')
|
2007-12-19 12:51:35 +08:00
|
|
|
self.to = list(to)
|
|
|
|
else:
|
|
|
|
self.to = []
|
2010-10-08 07:36:02 +08:00
|
|
|
if cc:
|
2016-12-29 23:27:49 +08:00
|
|
|
if isinstance(cc, str):
|
2014-11-27 10:16:31 +08:00
|
|
|
raise TypeError('"cc" argument must be a list or tuple')
|
2010-10-08 07:36:02 +08:00
|
|
|
self.cc = list(cc)
|
|
|
|
else:
|
|
|
|
self.cc = []
|
2007-12-19 12:51:35 +08:00
|
|
|
if bcc:
|
2016-12-29 23:27:49 +08:00
|
|
|
if isinstance(bcc, str):
|
2014-11-27 10:16:31 +08:00
|
|
|
raise TypeError('"bcc" argument must be a list or tuple')
|
2007-12-19 12:51:35 +08:00
|
|
|
self.bcc = list(bcc)
|
|
|
|
else:
|
|
|
|
self.bcc = []
|
2014-11-26 07:05:44 +08:00
|
|
|
if reply_to:
|
2016-12-29 23:27:49 +08:00
|
|
|
if isinstance(reply_to, str):
|
2014-11-26 07:05:44 +08:00
|
|
|
raise TypeError('"reply_to" argument must be a list or tuple')
|
|
|
|
self.reply_to = list(reply_to)
|
|
|
|
else:
|
|
|
|
self.reply_to = []
|
2007-05-03 22:38:45 +08:00
|
|
|
self.from_email = from_email or settings.DEFAULT_FROM_EMAIL
|
2007-05-03 19:35:11 +08:00
|
|
|
self.subject = subject
|
2018-02-19 21:39:53 +08:00
|
|
|
self.body = body or ""
|
2017-04-07 18:23:25 +08:00
|
|
|
self.attachments = []
|
|
|
|
if attachments:
|
|
|
|
for attachment in attachments:
|
|
|
|
if isinstance(attachment, MIMEBase):
|
|
|
|
self.attach(attachment)
|
|
|
|
else:
|
|
|
|
self.attach(*attachment)
|
2007-06-27 20:41:37 +08:00
|
|
|
self.extra_headers = headers or {}
|
2007-05-03 19:35:11 +08:00
|
|
|
self.connection = connection
|
|
|
|
|
|
|
|
def get_connection(self, fail_silently=False):
|
2009-11-03 20:53:26 +08:00
|
|
|
from django.core.mail import get_connection
|
2022-02-04 03:24:19 +08:00
|
|
|
|
2007-05-03 19:35:11 +08:00
|
|
|
if not self.connection:
|
2009-11-03 20:53:26 +08:00
|
|
|
self.connection = get_connection(fail_silently=fail_silently)
|
2007-05-03 19:35:11 +08:00
|
|
|
return self.connection
|
|
|
|
|
|
|
|
def message(self):
|
2007-06-27 20:54:15 +08:00
|
|
|
encoding = self.encoding or settings.DEFAULT_CHARSET
|
2012-08-09 18:12:22 +08:00
|
|
|
msg = SafeMIMEText(self.body, self.content_subtype, encoding)
|
2009-06-12 21:56:40 +08:00
|
|
|
msg = self._create_message(msg)
|
2007-05-03 19:35:11 +08:00
|
|
|
msg["Subject"] = self.subject
|
2010-04-01 23:16:26 +08:00
|
|
|
msg["From"] = self.extra_headers.get("From", self.from_email)
|
2017-12-09 11:28:34 +08:00
|
|
|
self._set_list_header_if_not_empty(msg, "To", self.to)
|
2017-12-09 22:42:46 +08:00
|
|
|
self._set_list_header_if_not_empty(msg, "Cc", self.cc)
|
|
|
|
self._set_list_header_if_not_empty(msg, "Reply-To", self.reply_to)
|
2008-10-07 20:20:01 +08:00
|
|
|
|
|
|
|
# Email header names are case-insensitive (RFC 2045), so we have to
|
|
|
|
# accommodate that when doing comparisons.
|
|
|
|
header_names = [key.lower() for key in self.extra_headers]
|
|
|
|
if "date" not in header_names:
|
2016-06-03 07:41:13 +08:00
|
|
|
# formatdate() uses stdlib methods to format the date, which use
|
|
|
|
# the stdlib/OS concept of a timezone, however, Django sets the
|
|
|
|
# TZ environment variable based on the TIME_ZONE setting which
|
|
|
|
# will get picked up by formatdate().
|
|
|
|
msg["Date"] = formatdate(localtime=settings.EMAIL_USE_LOCALTIME)
|
2008-10-07 20:20:01 +08:00
|
|
|
if "message-id" not in header_names:
|
2014-11-24 22:51:41 +08:00
|
|
|
# Use cached DNS_NAME for performance
|
|
|
|
msg["Message-ID"] = make_msgid(domain=DNS_NAME)
|
2007-06-27 20:41:37 +08:00
|
|
|
for name, value in self.extra_headers.items():
|
2018-01-12 22:05:16 +08:00
|
|
|
if name.lower() != "from": # From is already handled
|
|
|
|
msg[name] = value
|
2007-05-03 19:50:43 +08:00
|
|
|
return msg
|
2007-05-03 19:35:11 +08:00
|
|
|
|
2007-05-03 22:38:45 +08:00
|
|
|
def recipients(self):
|
|
|
|
"""
|
2017-01-26 03:02:33 +08:00
|
|
|
Return a list of all recipients of the email (includes direct
|
2010-10-08 07:36:02 +08:00
|
|
|
addressees as well as Cc and Bcc entries).
|
2007-05-03 22:38:45 +08:00
|
|
|
"""
|
2016-11-10 21:07:19 +08:00
|
|
|
return [email for email in (self.to + self.cc + self.bcc) if email]
|
2007-05-03 22:38:45 +08:00
|
|
|
|
2007-05-03 19:35:11 +08:00
|
|
|
def send(self, fail_silently=False):
|
2017-01-26 03:02:33 +08:00
|
|
|
"""Send the email message."""
|
2008-10-24 12:38:43 +08:00
|
|
|
if not self.recipients():
|
|
|
|
# Don't bother creating the network connection if there's nobody to
|
|
|
|
# send to.
|
|
|
|
return 0
|
2007-05-03 19:35:11 +08:00
|
|
|
return self.get_connection(fail_silently).send_messages([self])
|
|
|
|
|
2007-06-27 20:18:05 +08:00
|
|
|
def attach(self, filename=None, content=None, mimetype=None):
|
2007-06-27 17:44:55 +08:00
|
|
|
"""
|
2017-01-26 03:02:33 +08:00
|
|
|
Attach a file with the given filename and content. The filename can
|
2009-06-12 21:56:40 +08:00
|
|
|
be omitted and the mimetype is guessed, if not provided.
|
2007-06-27 17:44:55 +08:00
|
|
|
|
2017-01-26 03:02:33 +08:00
|
|
|
If the first parameter is a MIMEBase subclass, insert it directly
|
2007-06-27 20:18:05 +08:00
|
|
|
into the resulting message attachments.
|
2016-08-03 21:53:06 +08:00
|
|
|
|
|
|
|
For a text/* mimetype (guessed or specified), when a bytes object is
|
2017-01-26 03:02:33 +08:00
|
|
|
specified as content, decode it as UTF-8. If that fails, set the
|
|
|
|
mimetype to DEFAULT_ATTACHMENT_MIME_TYPE and don't decode the content.
|
2007-06-27 17:44:55 +08:00
|
|
|
"""
|
|
|
|
if isinstance(filename, MIMEBase):
|
2021-03-16 23:41:27 +08:00
|
|
|
if content is not None or mimetype is not None:
|
|
|
|
raise ValueError(
|
|
|
|
"content and mimetype must not be given when a MIMEBase "
|
|
|
|
"instance is provided."
|
|
|
|
)
|
2007-07-06 14:53:27 +08:00
|
|
|
self.attachments.append(filename)
|
2021-03-16 23:41:27 +08:00
|
|
|
elif content is None:
|
|
|
|
raise ValueError("content must be provided.")
|
2007-06-27 17:44:55 +08:00
|
|
|
else:
|
2018-01-04 07:52:12 +08:00
|
|
|
mimetype = (
|
|
|
|
mimetype
|
|
|
|
or mimetypes.guess_type(filename)[0]
|
|
|
|
or DEFAULT_ATTACHMENT_MIME_TYPE
|
2022-02-04 03:24:19 +08:00
|
|
|
)
|
2016-08-03 21:53:06 +08:00
|
|
|
basetype, subtype = mimetype.split("/", 1)
|
|
|
|
|
|
|
|
if basetype == "text":
|
2016-12-29 23:27:49 +08:00
|
|
|
if isinstance(content, bytes):
|
2016-08-03 21:53:06 +08:00
|
|
|
try:
|
2017-02-08 01:05:47 +08:00
|
|
|
content = content.decode()
|
2016-08-03 21:53:06 +08:00
|
|
|
except UnicodeDecodeError:
|
2017-01-26 02:59:25 +08:00
|
|
|
# If mimetype suggests the file is text but it's
|
|
|
|
# actually binary, read() raises a UnicodeDecodeError.
|
2016-08-03 21:53:06 +08:00
|
|
|
mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
|
|
|
|
|
2007-06-27 17:44:55 +08:00
|
|
|
self.attachments.append((filename, content, mimetype))
|
|
|
|
|
|
|
|
def attach_file(self, path, mimetype=None):
|
2015-07-22 23:47:32 +08:00
|
|
|
"""
|
2017-01-26 03:02:33 +08:00
|
|
|
Attach a file from the filesystem.
|
2015-07-22 23:47:32 +08:00
|
|
|
|
2017-01-26 03:02:33 +08:00
|
|
|
Set the mimetype to DEFAULT_ATTACHMENT_MIME_TYPE if it isn't specified
|
|
|
|
and cannot be guessed.
|
2016-08-03 21:53:06 +08:00
|
|
|
|
2017-01-26 03:02:33 +08:00
|
|
|
For a text/* mimetype (guessed or specified), decode the file's content
|
|
|
|
as UTF-8. If that fails, set the mimetype to
|
|
|
|
DEFAULT_ATTACHMENT_MIME_TYPE and don't decode the content.
|
2015-07-22 23:47:32 +08:00
|
|
|
"""
|
2017-07-22 04:33:26 +08:00
|
|
|
path = Path(path)
|
|
|
|
with path.open("rb") as file:
|
2016-08-03 21:53:06 +08:00
|
|
|
content = file.read()
|
2017-07-22 04:33:26 +08:00
|
|
|
self.attach(path.name, content, mimetype)
|
2007-06-27 17:44:55 +08:00
|
|
|
|
2009-06-12 21:56:40 +08:00
|
|
|
def _create_message(self, msg):
|
|
|
|
return self._create_attachments(msg)
|
|
|
|
|
|
|
|
def _create_attachments(self, msg):
|
|
|
|
if self.attachments:
|
2010-03-06 05:27:58 +08:00
|
|
|
encoding = self.encoding or settings.DEFAULT_CHARSET
|
2009-06-12 21:56:40 +08:00
|
|
|
body_msg = msg
|
2010-03-06 05:27:58 +08:00
|
|
|
msg = SafeMIMEMultipart(_subtype=self.mixed_subtype, encoding=encoding)
|
2017-08-28 12:28:18 +08:00
|
|
|
if self.body or body_msg.is_multipart():
|
2009-06-12 21:56:40 +08:00
|
|
|
msg.attach(body_msg)
|
|
|
|
for attachment in self.attachments:
|
|
|
|
if isinstance(attachment, MIMEBase):
|
|
|
|
msg.attach(attachment)
|
|
|
|
else:
|
|
|
|
msg.attach(self._create_attachment(*attachment))
|
|
|
|
return msg
|
|
|
|
|
|
|
|
def _create_mime_attachment(self, content, mimetype):
|
2007-06-27 17:44:55 +08:00
|
|
|
"""
|
2017-01-26 03:02:33 +08:00
|
|
|
Convert the content, mimetype pair into a MIME attachment object.
|
2013-08-21 09:17:26 +08:00
|
|
|
|
|
|
|
If the mimetype is message/rfc822, content may be an
|
|
|
|
email.Message or EmailMessage object, as well as a str.
|
2007-06-27 17:44:55 +08:00
|
|
|
"""
|
|
|
|
basetype, subtype = mimetype.split("/", 1)
|
|
|
|
if basetype == "text":
|
2010-03-06 05:27:58 +08:00
|
|
|
encoding = self.encoding or settings.DEFAULT_CHARSET
|
2012-08-09 18:12:22 +08:00
|
|
|
attachment = SafeMIMEText(content, subtype, encoding)
|
2013-08-21 09:17:26 +08:00
|
|
|
elif basetype == "message" and subtype == "rfc822":
|
|
|
|
# Bug #18967: per RFC2046 s5.2.1, message/rfc822 attachments
|
|
|
|
# must not be base64 encoded.
|
|
|
|
if isinstance(content, EmailMessage):
|
|
|
|
# convert content into an email.Message first
|
|
|
|
content = content.message()
|
|
|
|
elif not isinstance(content, Message):
|
|
|
|
# For compatibility with existing code, parse the message
|
2014-05-29 08:39:14 +08:00
|
|
|
# into an email.Message object if it is not one already.
|
2017-01-26 17:08:08 +08:00
|
|
|
content = message_from_string(force_str(content))
|
2013-08-21 09:17:26 +08:00
|
|
|
|
|
|
|
attachment = SafeMIMEMessage(content, subtype)
|
2007-06-27 17:44:55 +08:00
|
|
|
else:
|
|
|
|
# Encode non-text attachments with base64.
|
|
|
|
attachment = MIMEBase(basetype, subtype)
|
|
|
|
attachment.set_payload(content)
|
|
|
|
Encoders.encode_base64(attachment)
|
2009-06-12 21:56:40 +08:00
|
|
|
return attachment
|
|
|
|
|
|
|
|
def _create_attachment(self, filename, content, mimetype=None):
|
|
|
|
"""
|
2017-01-26 03:02:33 +08:00
|
|
|
Convert the filename, content, mimetype triple into a MIME attachment
|
2009-06-12 21:56:40 +08:00
|
|
|
object.
|
|
|
|
"""
|
|
|
|
attachment = self._create_mime_attachment(content, mimetype)
|
2007-06-27 20:18:05 +08:00
|
|
|
if filename:
|
2012-01-15 10:33:31 +08:00
|
|
|
try:
|
2012-08-09 18:12:22 +08:00
|
|
|
filename.encode("ascii")
|
2012-01-15 10:33:31 +08:00
|
|
|
except UnicodeEncodeError:
|
2012-08-09 18:12:22 +08:00
|
|
|
filename = ("utf-8", "", filename)
|
2018-03-16 17:54:34 +08:00
|
|
|
attachment.add_header(
|
|
|
|
"Content-Disposition", "attachment", filename=filename
|
|
|
|
)
|
2007-06-27 17:44:55 +08:00
|
|
|
return attachment
|
|
|
|
|
2017-12-09 22:42:46 +08:00
|
|
|
def _set_list_header_if_not_empty(self, msg, header, values):
|
|
|
|
"""
|
|
|
|
Set msg's header, either from self.extra_headers, if present, or from
|
|
|
|
the values argument.
|
|
|
|
"""
|
|
|
|
if values:
|
|
|
|
try:
|
|
|
|
value = self.extra_headers[header]
|
|
|
|
except KeyError:
|
|
|
|
value = ", ".join(str(v) for v in values)
|
|
|
|
msg[header] = value
|
|
|
|
|
2009-11-03 20:53:26 +08:00
|
|
|
|
2007-06-27 20:18:05 +08:00
|
|
|
class EmailMultiAlternatives(EmailMessage):
|
|
|
|
"""
|
|
|
|
A version of EmailMessage that makes it easy to send multipart/alternative
|
|
|
|
messages. For example, including text and HTML versions of the text is
|
|
|
|
made easier.
|
|
|
|
"""
|
|
|
|
|
2009-06-12 21:56:40 +08:00
|
|
|
alternative_subtype = "alternative"
|
2022-02-04 03:24:19 +08:00
|
|
|
|
2009-06-12 21:56:40 +08:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
subject="",
|
|
|
|
body="",
|
|
|
|
from_email=None,
|
|
|
|
to=None,
|
|
|
|
bcc=None,
|
2016-03-29 06:33:29 +08:00
|
|
|
connection=None,
|
|
|
|
attachments=None,
|
|
|
|
headers=None,
|
|
|
|
alternatives=None,
|
|
|
|
cc=None,
|
|
|
|
reply_to=None,
|
|
|
|
):
|
2009-06-12 21:56:40 +08:00
|
|
|
"""
|
|
|
|
Initialize a single email message (which can be sent to multiple
|
|
|
|
recipients).
|
|
|
|
"""
|
2017-01-21 21:13:44 +08:00
|
|
|
super().__init__(
|
2014-11-26 07:05:44 +08:00
|
|
|
subject,
|
|
|
|
body,
|
|
|
|
from_email,
|
|
|
|
to,
|
|
|
|
bcc,
|
|
|
|
connection,
|
|
|
|
attachments,
|
|
|
|
headers,
|
|
|
|
cc,
|
|
|
|
reply_to,
|
2014-09-04 20:15:09 +08:00
|
|
|
)
|
2011-01-15 13:55:24 +08:00
|
|
|
self.alternatives = alternatives or []
|
2009-06-12 21:56:40 +08:00
|
|
|
|
|
|
|
def attach_alternative(self, content, mimetype):
|
2007-06-27 20:18:05 +08:00
|
|
|
"""Attach an alternative content representation."""
|
2021-03-16 23:41:27 +08:00
|
|
|
if content is None or mimetype is None:
|
|
|
|
raise ValueError("Both content and mimetype must be provided.")
|
2009-06-12 21:56:40 +08:00
|
|
|
self.alternatives.append((content, mimetype))
|
|
|
|
|
|
|
|
def _create_message(self, msg):
|
|
|
|
return self._create_attachments(self._create_alternatives(msg))
|
|
|
|
|
|
|
|
def _create_alternatives(self, msg):
|
2010-03-06 05:27:58 +08:00
|
|
|
encoding = self.encoding or settings.DEFAULT_CHARSET
|
2009-06-12 21:56:40 +08:00
|
|
|
if self.alternatives:
|
|
|
|
body_msg = msg
|
2010-03-06 05:27:58 +08:00
|
|
|
msg = SafeMIMEMultipart(
|
|
|
|
_subtype=self.alternative_subtype, encoding=encoding
|
|
|
|
)
|
2009-06-12 21:56:40 +08:00
|
|
|
if self.body:
|
|
|
|
msg.attach(body_msg)
|
|
|
|
for alternative in self.alternatives:
|
|
|
|
msg.attach(self._create_mime_attachment(*alternative))
|
|
|
|
return msg
|