message.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274
  1. import mimetypes
  2. import os
  3. import random
  4. import time
  5. from email import Charset, Encoders
  6. from email.MIMEText import MIMEText
  7. from email.MIMEMultipart import MIMEMultipart
  8. from email.MIMEBase import MIMEBase
  9. from email.Header import Header
  10. from email.Utils import formatdate, parseaddr, formataddr
  11. from django.conf import settings
  12. from django.core.mail.utils import DNS_NAME
  13. from django.utils.encoding import smart_str, force_unicode
  14. # Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from
  15. # some spam filters.
  16. Charset.add_charset('utf-8', Charset.SHORTEST, Charset.QP, 'utf-8')
  17. # Default MIME type to use on attachments (if it is not explicitly given
  18. # and cannot be guessed).
  19. DEFAULT_ATTACHMENT_MIME_TYPE = 'application/octet-stream'
  20. class BadHeaderError(ValueError):
  21. pass
  22. # Copied from Python standard library, with the following modifications:
  23. # * Used cached hostname for performance.
  24. # * Added try/except to support lack of getpid() in Jython (#5496).
  25. def make_msgid(idstring=None):
  26. """Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
  27. <20020201195627.33539.96671@nightshade.la.mastaler.com>
  28. Optional idstring if given is a string used to strengthen the
  29. uniqueness of the message id.
  30. """
  31. timeval = time.time()
  32. utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval))
  33. try:
  34. pid = os.getpid()
  35. except AttributeError:
  36. # No getpid() in Jython, for example.
  37. pid = 1
  38. randint = random.randrange(100000)
  39. if idstring is None:
  40. idstring = ''
  41. else:
  42. idstring = '.' + idstring
  43. idhost = DNS_NAME
  44. msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost)
  45. return msgid
  46. def forbid_multi_line_headers(name, val):
  47. """Forbids multi-line headers, to prevent header injection."""
  48. val = force_unicode(val)
  49. if '\n' in val or '\r' in val:
  50. raise BadHeaderError("Header values can't contain newlines (got %r for header %r)" % (val, name))
  51. try:
  52. val = val.encode('ascii')
  53. except UnicodeEncodeError:
  54. if name.lower() in ('to', 'from', 'cc'):
  55. result = []
  56. for item in val.split(', '):
  57. nm, addr = parseaddr(item)
  58. nm = str(Header(nm, settings.DEFAULT_CHARSET))
  59. result.append(formataddr((nm, str(addr))))
  60. val = ', '.join(result)
  61. else:
  62. val = Header(val, settings.DEFAULT_CHARSET)
  63. else:
  64. if name.lower() == 'subject':
  65. val = Header(val)
  66. return name, val
  67. class SafeMIMEText(MIMEText):
  68. def __setitem__(self, name, val):
  69. name, val = forbid_multi_line_headers(name, val)
  70. MIMEText.__setitem__(self, name, val)
  71. class SafeMIMEMultipart(MIMEMultipart):
  72. def __setitem__(self, name, val):
  73. name, val = forbid_multi_line_headers(name, val)
  74. MIMEMultipart.__setitem__(self, name, val)
  75. class EmailMessage(object):
  76. """
  77. A container for email information.
  78. """
  79. content_subtype = 'plain'
  80. mixed_subtype = 'mixed'
  81. encoding = None # None => use settings default
  82. def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
  83. connection=None, attachments=None, headers=None):
  84. """
  85. Initialize a single email message (which can be sent to multiple
  86. recipients).
  87. All strings used to create the message can be unicode strings
  88. (or UTF-8 bytestrings). The SafeMIMEText class will handle any
  89. necessary encoding conversions.
  90. """
  91. if to:
  92. assert not isinstance(to, basestring), '"to" argument must be a list or tuple'
  93. self.to = list(to)
  94. else:
  95. self.to = []
  96. if bcc:
  97. assert not isinstance(bcc, basestring), '"bcc" argument must be a list or tuple'
  98. self.bcc = list(bcc)
  99. else:
  100. self.bcc = []
  101. self.from_email = from_email or settings.DEFAULT_FROM_EMAIL
  102. self.subject = subject
  103. self.body = body
  104. self.attachments = attachments or []
  105. self.extra_headers = headers or {}
  106. self.connection = connection
  107. def get_connection(self, fail_silently=False):
  108. from django.core.mail import get_connection
  109. if not self.connection:
  110. self.connection = get_connection(fail_silently=fail_silently)
  111. return self.connection
  112. def message(self):
  113. encoding = self.encoding or settings.DEFAULT_CHARSET
  114. msg = SafeMIMEText(smart_str(self.body, settings.DEFAULT_CHARSET),
  115. self.content_subtype, encoding)
  116. msg = self._create_message(msg)
  117. msg['Subject'] = self.subject
  118. msg['From'] = self.extra_headers.pop('From', self.from_email)
  119. msg['To'] = ', '.join(self.to)
  120. # Email header names are case-insensitive (RFC 2045), so we have to
  121. # accommodate that when doing comparisons.
  122. header_names = [key.lower() for key in self.extra_headers]
  123. if 'date' not in header_names:
  124. msg['Date'] = formatdate()
  125. if 'message-id' not in header_names:
  126. msg['Message-ID'] = make_msgid()
  127. for name, value in self.extra_headers.items():
  128. msg[name] = value
  129. return msg
  130. def recipients(self):
  131. """
  132. Returns a list of all recipients of the email (includes direct
  133. addressees as well as Bcc entries).
  134. """
  135. return self.to + self.bcc
  136. def send(self, fail_silently=False):
  137. """Sends the email message."""
  138. if not self.recipients():
  139. # Don't bother creating the network connection if there's nobody to
  140. # send to.
  141. return 0
  142. return self.get_connection(fail_silently).send_messages([self])
  143. def attach(self, filename=None, content=None, mimetype=None):
  144. """
  145. Attaches a file with the given filename and content. The filename can
  146. be omitted and the mimetype is guessed, if not provided.
  147. If the first parameter is a MIMEBase subclass it is inserted directly
  148. into the resulting message attachments.
  149. """
  150. if isinstance(filename, MIMEBase):
  151. assert content == mimetype == None
  152. self.attachments.append(filename)
  153. else:
  154. assert content is not None
  155. self.attachments.append((filename, content, mimetype))
  156. def attach_file(self, path, mimetype=None):
  157. """Attaches a file from the filesystem."""
  158. filename = os.path.basename(path)
  159. content = open(path, 'rb').read()
  160. self.attach(filename, content, mimetype)
  161. def _create_message(self, msg):
  162. return self._create_attachments(msg)
  163. def _create_attachments(self, msg):
  164. if self.attachments:
  165. body_msg = msg
  166. msg = SafeMIMEMultipart(_subtype=self.mixed_subtype)
  167. if self.body:
  168. msg.attach(body_msg)
  169. for attachment in self.attachments:
  170. if isinstance(attachment, MIMEBase):
  171. msg.attach(attachment)
  172. else:
  173. msg.attach(self._create_attachment(*attachment))
  174. return msg
  175. def _create_mime_attachment(self, content, mimetype):
  176. """
  177. Converts the content, mimetype pair into a MIME attachment object.
  178. """
  179. basetype, subtype = mimetype.split('/', 1)
  180. if basetype == 'text':
  181. attachment = SafeMIMEText(smart_str(content,
  182. settings.DEFAULT_CHARSET), subtype, settings.DEFAULT_CHARSET)
  183. else:
  184. # Encode non-text attachments with base64.
  185. attachment = MIMEBase(basetype, subtype)
  186. attachment.set_payload(content)
  187. Encoders.encode_base64(attachment)
  188. return attachment
  189. def _create_attachment(self, filename, content, mimetype=None):
  190. """
  191. Converts the filename, content, mimetype triple into a MIME attachment
  192. object.
  193. """
  194. if mimetype is None:
  195. mimetype, _ = mimetypes.guess_type(filename)
  196. if mimetype is None:
  197. mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
  198. attachment = self._create_mime_attachment(content, mimetype)
  199. if filename:
  200. attachment.add_header('Content-Disposition', 'attachment',
  201. filename=filename)
  202. return attachment
  203. class EmailMultiAlternatives(EmailMessage):
  204. """
  205. A version of EmailMessage that makes it easy to send multipart/alternative
  206. messages. For example, including text and HTML versions of the text is
  207. made easier.
  208. """
  209. alternative_subtype = 'alternative'
  210. def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
  211. connection=None, attachments=None, headers=None, alternatives=None):
  212. """
  213. Initialize a single email message (which can be sent to multiple
  214. recipients).
  215. All strings used to create the message can be unicode strings (or UTF-8
  216. bytestrings). The SafeMIMEText class will handle any necessary encoding
  217. conversions.
  218. """
  219. super(EmailMultiAlternatives, self).__init__(subject, body, from_email, to, bcc, connection, attachments, headers)
  220. self.alternatives=alternatives or []
  221. def attach_alternative(self, content, mimetype):
  222. """Attach an alternative content representation."""
  223. assert content is not None
  224. assert mimetype is not None
  225. self.alternatives.append((content, mimetype))
  226. def _create_message(self, msg):
  227. return self._create_attachments(self._create_alternatives(msg))
  228. def _create_alternatives(self, msg):
  229. if self.alternatives:
  230. body_msg = msg
  231. msg = SafeMIMEMultipart(_subtype=self.alternative_subtype)
  232. if self.body:
  233. msg.attach(body_msg)
  234. for alternative in self.alternatives:
  235. msg.attach(self._create_mime_attachment(*alternative))
  236. return msg