message.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. from __future__ import unicode_literals
  2. import mimetypes
  3. import os
  4. import random
  5. import sys
  6. import time
  7. from email import (charset as Charset, encoders as Encoders,
  8. message_from_string, generator)
  9. from email.message import Message
  10. from email.mime.text import MIMEText
  11. from email.mime.multipart import MIMEMultipart
  12. from email.mime.base import MIMEBase
  13. from email.mime.message import MIMEMessage
  14. from email.header import Header
  15. from email.utils import formatdate, getaddresses, formataddr, parseaddr
  16. from django.conf import settings
  17. from django.core.mail.utils import DNS_NAME
  18. from django.utils.encoding import force_text
  19. from django.utils import six
  20. # Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from
  21. # some spam filters.
  22. utf8_charset = Charset.Charset('utf-8')
  23. utf8_charset.body_encoding = None # Python defaults to BASE64
  24. # Default MIME type to use on attachments (if it is not explicitly given
  25. # and cannot be guessed).
  26. DEFAULT_ATTACHMENT_MIME_TYPE = 'application/octet-stream'
  27. class BadHeaderError(ValueError):
  28. pass
  29. # Copied from Python standard library, with the following modifications:
  30. # * Used cached hostname for performance.
  31. # * Added try/except to support lack of getpid() in Jython (#5496).
  32. def make_msgid(idstring=None):
  33. """Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
  34. <20020201195627.33539.96671@nightshade.la.mastaler.com>
  35. Optional idstring if given is a string used to strengthen the
  36. uniqueness of the message id.
  37. """
  38. timeval = time.time()
  39. utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval))
  40. try:
  41. pid = os.getpid()
  42. except AttributeError:
  43. # No getpid() in Jython, for example.
  44. pid = 1
  45. randint = random.randrange(100000)
  46. if idstring is None:
  47. idstring = ''
  48. else:
  49. idstring = '.' + idstring
  50. idhost = DNS_NAME
  51. msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost)
  52. return msgid
  53. # Header names that contain structured address data (RFC #5322)
  54. ADDRESS_HEADERS = set([
  55. 'from',
  56. 'sender',
  57. 'reply-to',
  58. 'to',
  59. 'cc',
  60. 'bcc',
  61. 'resent-from',
  62. 'resent-sender',
  63. 'resent-to',
  64. 'resent-cc',
  65. 'resent-bcc',
  66. ])
  67. def forbid_multi_line_headers(name, val, encoding):
  68. """Forbids multi-line headers, to prevent header injection."""
  69. encoding = encoding or settings.DEFAULT_CHARSET
  70. val = force_text(val)
  71. if '\n' in val or '\r' in val:
  72. raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (val, name))
  73. try:
  74. val.encode('ascii')
  75. except UnicodeEncodeError:
  76. if name.lower() in ADDRESS_HEADERS:
  77. val = ', '.join(sanitize_address(addr, encoding)
  78. for addr in getaddresses((val,)))
  79. else:
  80. val = Header(val, encoding).encode()
  81. else:
  82. if name.lower() == 'subject':
  83. val = Header(val).encode()
  84. return str(name), val
  85. def sanitize_address(addr, encoding):
  86. if isinstance(addr, six.string_types):
  87. addr = parseaddr(force_text(addr))
  88. nm, addr = addr
  89. # This try-except clause is needed on Python 3 < 3.2.4
  90. # http://bugs.python.org/issue14291
  91. try:
  92. nm = Header(nm, encoding).encode()
  93. except UnicodeEncodeError:
  94. nm = Header(nm, 'utf-8').encode()
  95. try:
  96. addr.encode('ascii')
  97. except UnicodeEncodeError: # IDN
  98. if '@' in addr:
  99. localpart, domain = addr.split('@', 1)
  100. localpart = str(Header(localpart, encoding))
  101. domain = domain.encode('idna').decode('ascii')
  102. addr = '@'.join([localpart, domain])
  103. else:
  104. addr = Header(addr, encoding).encode()
  105. return formataddr((nm, addr))
  106. class MIMEMixin():
  107. def as_string(self, unixfrom=False):
  108. """Return the entire formatted message as a string.
  109. Optional `unixfrom' when True, means include the Unix From_ envelope
  110. header.
  111. This overrides the default as_string() implementation to not mangle
  112. lines that begin with 'From '. See bug #13433 for details.
  113. """
  114. fp = six.StringIO()
  115. g = generator.Generator(fp, mangle_from_=False)
  116. g.flatten(self, unixfrom=unixfrom)
  117. return fp.getvalue()
  118. if six.PY2:
  119. as_bytes = as_string
  120. else:
  121. def as_bytes(self, unixfrom=False):
  122. """Return the entire formatted message as bytes.
  123. Optional `unixfrom' when True, means include the Unix From_ envelope
  124. header.
  125. This overrides the default as_bytes() implementation to not mangle
  126. lines that begin with 'From '. See bug #13433 for details.
  127. """
  128. fp = six.BytesIO()
  129. g = generator.BytesGenerator(fp, mangle_from_=False)
  130. g.flatten(self, unixfrom=unixfrom)
  131. return fp.getvalue()
  132. class SafeMIMEMessage(MIMEMixin, MIMEMessage):
  133. def __setitem__(self, name, val):
  134. # message/rfc822 attachments must be ASCII
  135. name, val = forbid_multi_line_headers(name, val, 'ascii')
  136. MIMEMessage.__setitem__(self, name, val)
  137. class SafeMIMEText(MIMEMixin, MIMEText):
  138. def __init__(self, text, subtype, charset):
  139. self.encoding = charset
  140. if charset == 'utf-8':
  141. # Unfortunately, Python doesn't support setting a Charset instance
  142. # as MIMEText init parameter (http://bugs.python.org/issue16324).
  143. # We do it manually and trigger re-encoding of the payload.
  144. MIMEText.__init__(self, text, subtype, None)
  145. del self['Content-Transfer-Encoding']
  146. # Workaround for versions without http://bugs.python.org/issue19063
  147. if (3, 2) < sys.version_info < (3, 3, 4):
  148. payload = text.encode(utf8_charset.output_charset)
  149. self._payload = payload.decode('ascii', 'surrogateescape')
  150. self.set_charset(utf8_charset)
  151. else:
  152. self.set_payload(text, utf8_charset)
  153. self.replace_header('Content-Type', 'text/%s; charset="%s"' % (subtype, charset))
  154. else:
  155. MIMEText.__init__(self, text, subtype, charset)
  156. def __setitem__(self, name, val):
  157. name, val = forbid_multi_line_headers(name, val, self.encoding)
  158. MIMEText.__setitem__(self, name, val)
  159. class SafeMIMEMultipart(MIMEMixin, MIMEMultipart):
  160. def __init__(self, _subtype='mixed', boundary=None, _subparts=None, encoding=None, **_params):
  161. self.encoding = encoding
  162. MIMEMultipart.__init__(self, _subtype, boundary, _subparts, **_params)
  163. def __setitem__(self, name, val):
  164. name, val = forbid_multi_line_headers(name, val, self.encoding)
  165. MIMEMultipart.__setitem__(self, name, val)
  166. class EmailMessage(object):
  167. """
  168. A container for email information.
  169. """
  170. content_subtype = 'plain'
  171. mixed_subtype = 'mixed'
  172. encoding = None # None => use settings default
  173. def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
  174. connection=None, attachments=None, headers=None, cc=None):
  175. """
  176. Initialize a single email message (which can be sent to multiple
  177. recipients).
  178. All strings used to create the message can be unicode strings
  179. (or UTF-8 bytestrings). The SafeMIMEText class will handle any
  180. necessary encoding conversions.
  181. """
  182. if to:
  183. assert not isinstance(to, six.string_types), '"to" argument must be a list or tuple'
  184. self.to = list(to)
  185. else:
  186. self.to = []
  187. if cc:
  188. assert not isinstance(cc, six.string_types), '"cc" argument must be a list or tuple'
  189. self.cc = list(cc)
  190. else:
  191. self.cc = []
  192. if bcc:
  193. assert not isinstance(bcc, six.string_types), '"bcc" argument must be a list or tuple'
  194. self.bcc = list(bcc)
  195. else:
  196. self.bcc = []
  197. self.from_email = from_email or settings.DEFAULT_FROM_EMAIL
  198. self.subject = subject
  199. self.body = body
  200. self.attachments = attachments or []
  201. self.extra_headers = headers or {}
  202. self.connection = connection
  203. def get_connection(self, fail_silently=False):
  204. from django.core.mail import get_connection
  205. if not self.connection:
  206. self.connection = get_connection(fail_silently=fail_silently)
  207. return self.connection
  208. def message(self):
  209. encoding = self.encoding or settings.DEFAULT_CHARSET
  210. msg = SafeMIMEText(self.body, self.content_subtype, encoding)
  211. msg = self._create_message(msg)
  212. msg['Subject'] = self.subject
  213. msg['From'] = self.extra_headers.get('From', self.from_email)
  214. msg['To'] = self.extra_headers.get('To', ', '.join(self.to))
  215. if self.cc:
  216. msg['Cc'] = ', '.join(self.cc)
  217. # Email header names are case-insensitive (RFC 2045), so we have to
  218. # accommodate that when doing comparisons.
  219. header_names = [key.lower() for key in self.extra_headers]
  220. if 'date' not in header_names:
  221. msg['Date'] = formatdate()
  222. if 'message-id' not in header_names:
  223. msg['Message-ID'] = make_msgid()
  224. for name, value in self.extra_headers.items():
  225. if name.lower() in ('from', 'to'): # From and To are already handled
  226. continue
  227. msg[name] = value
  228. return msg
  229. def recipients(self):
  230. """
  231. Returns a list of all recipients of the email (includes direct
  232. addressees as well as Cc and Bcc entries).
  233. """
  234. return self.to + self.cc + self.bcc
  235. def send(self, fail_silently=False):
  236. """Sends the email message."""
  237. if not self.recipients():
  238. # Don't bother creating the network connection if there's nobody to
  239. # send to.
  240. return 0
  241. return self.get_connection(fail_silently).send_messages([self])
  242. def attach(self, filename=None, content=None, mimetype=None):
  243. """
  244. Attaches a file with the given filename and content. The filename can
  245. be omitted and the mimetype is guessed, if not provided.
  246. If the first parameter is a MIMEBase subclass it is inserted directly
  247. into the resulting message attachments.
  248. """
  249. if isinstance(filename, MIMEBase):
  250. assert content is None
  251. assert mimetype is None
  252. self.attachments.append(filename)
  253. else:
  254. assert content is not None
  255. self.attachments.append((filename, content, mimetype))
  256. def attach_file(self, path, mimetype=None):
  257. """Attaches a file from the filesystem."""
  258. filename = os.path.basename(path)
  259. with open(path, 'rb') as f:
  260. content = f.read()
  261. self.attach(filename, content, mimetype)
  262. def _create_message(self, msg):
  263. return self._create_attachments(msg)
  264. def _create_attachments(self, msg):
  265. if self.attachments:
  266. encoding = self.encoding or settings.DEFAULT_CHARSET
  267. body_msg = msg
  268. msg = SafeMIMEMultipart(_subtype=self.mixed_subtype, encoding=encoding)
  269. if self.body:
  270. msg.attach(body_msg)
  271. for attachment in self.attachments:
  272. if isinstance(attachment, MIMEBase):
  273. msg.attach(attachment)
  274. else:
  275. msg.attach(self._create_attachment(*attachment))
  276. return msg
  277. def _create_mime_attachment(self, content, mimetype):
  278. """
  279. Converts the content, mimetype pair into a MIME attachment object.
  280. If the mimetype is message/rfc822, content may be an
  281. email.Message or EmailMessage object, as well as a str.
  282. """
  283. basetype, subtype = mimetype.split('/', 1)
  284. if basetype == 'text':
  285. encoding = self.encoding or settings.DEFAULT_CHARSET
  286. attachment = SafeMIMEText(content, subtype, encoding)
  287. elif basetype == 'message' and subtype == 'rfc822':
  288. # Bug #18967: per RFC2046 s5.2.1, message/rfc822 attachments
  289. # must not be base64 encoded.
  290. if isinstance(content, EmailMessage):
  291. # convert content into an email.Message first
  292. content = content.message()
  293. elif not isinstance(content, Message):
  294. # For compatibility with existing code, parse the message
  295. # into an email.Message object if it is not one already.
  296. content = message_from_string(content)
  297. attachment = SafeMIMEMessage(content, subtype)
  298. else:
  299. # Encode non-text attachments with base64.
  300. attachment = MIMEBase(basetype, subtype)
  301. attachment.set_payload(content)
  302. Encoders.encode_base64(attachment)
  303. return attachment
  304. def _create_attachment(self, filename, content, mimetype=None):
  305. """
  306. Converts the filename, content, mimetype triple into a MIME attachment
  307. object.
  308. """
  309. if mimetype is None:
  310. mimetype, _ = mimetypes.guess_type(filename)
  311. if mimetype is None:
  312. mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
  313. attachment = self._create_mime_attachment(content, mimetype)
  314. if filename:
  315. try:
  316. filename.encode('ascii')
  317. except UnicodeEncodeError:
  318. if six.PY2:
  319. filename = filename.encode('utf-8')
  320. filename = ('utf-8', '', filename)
  321. attachment.add_header('Content-Disposition', 'attachment',
  322. filename=filename)
  323. return attachment
  324. class EmailMultiAlternatives(EmailMessage):
  325. """
  326. A version of EmailMessage that makes it easy to send multipart/alternative
  327. messages. For example, including text and HTML versions of the text is
  328. made easier.
  329. """
  330. alternative_subtype = 'alternative'
  331. def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
  332. connection=None, attachments=None, headers=None, alternatives=None,
  333. cc=None):
  334. """
  335. Initialize a single email message (which can be sent to multiple
  336. recipients).
  337. All strings used to create the message can be unicode strings (or UTF-8
  338. bytestrings). The SafeMIMEText class will handle any necessary encoding
  339. conversions.
  340. """
  341. super(EmailMultiAlternatives, self).__init__(subject, body, from_email, to, bcc, connection, attachments, headers, cc)
  342. self.alternatives = alternatives or []
  343. def attach_alternative(self, content, mimetype):
  344. """Attach an alternative content representation."""
  345. assert content is not None
  346. assert mimetype is not None
  347. self.alternatives.append((content, mimetype))
  348. def _create_message(self, msg):
  349. return self._create_attachments(self._create_alternatives(msg))
  350. def _create_alternatives(self, msg):
  351. encoding = self.encoding or settings.DEFAULT_CHARSET
  352. if self.alternatives:
  353. body_msg = msg
  354. msg = SafeMIMEMultipart(_subtype=self.alternative_subtype, encoding=encoding)
  355. if self.body:
  356. msg.attach(body_msg)
  357. for alternative in self.alternatives:
  358. msg.attach(self._create_mime_attachment(*alternative))
  359. return msg