Development of an internal social media platform with personalised dashboards for students
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

message.py 17KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454
  1. import mimetypes
  2. from email import (
  3. charset as Charset, encoders as Encoders, generator, message_from_string,
  4. )
  5. from email.errors import InvalidHeaderDefect, NonASCIILocalPartDefect
  6. from email.header import Header
  7. from email.headerregistry import Address
  8. from email.message import Message
  9. from email.mime.base import MIMEBase
  10. from email.mime.message import MIMEMessage
  11. from email.mime.multipart import MIMEMultipart
  12. from email.mime.text import MIMEText
  13. from email.utils import formatdate, getaddresses, make_msgid, parseaddr
  14. from io import BytesIO, StringIO
  15. from pathlib import Path
  16. from django.conf import settings
  17. from django.core.mail.utils import DNS_NAME
  18. from django.utils.encoding import force_text
  19. # Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from
  20. # some spam filters.
  21. utf8_charset = Charset.Charset('utf-8')
  22. utf8_charset.body_encoding = None # Python defaults to BASE64
  23. utf8_charset_qp = Charset.Charset('utf-8')
  24. utf8_charset_qp.body_encoding = Charset.QP
  25. # Default MIME type to use on attachments (if it is not explicitly given
  26. # and cannot be guessed).
  27. DEFAULT_ATTACHMENT_MIME_TYPE = 'application/octet-stream'
  28. RFC5322_EMAIL_LINE_LENGTH_LIMIT = 998
  29. class BadHeaderError(ValueError):
  30. pass
  31. # Header names that contain structured address data (RFC #5322)
  32. ADDRESS_HEADERS = {
  33. 'from',
  34. 'sender',
  35. 'reply-to',
  36. 'to',
  37. 'cc',
  38. 'bcc',
  39. 'resent-from',
  40. 'resent-sender',
  41. 'resent-to',
  42. 'resent-cc',
  43. 'resent-bcc',
  44. }
  45. def forbid_multi_line_headers(name, val, encoding):
  46. """Forbid multi-line headers to prevent header injection."""
  47. encoding = encoding or settings.DEFAULT_CHARSET
  48. val = str(val) # val may be lazy
  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.encode('ascii')
  53. except UnicodeEncodeError:
  54. if name.lower() in ADDRESS_HEADERS:
  55. val = ', '.join(sanitize_address(addr, encoding) for addr in getaddresses((val,)))
  56. else:
  57. val = Header(val, encoding).encode()
  58. else:
  59. if name.lower() == 'subject':
  60. val = Header(val).encode()
  61. return name, val
  62. def split_addr(addr, encoding):
  63. """
  64. Split the address into local part and domain and encode them.
  65. When non-ascii characters are present in the local part, it must be
  66. MIME-word encoded. The domain name must be idna-encoded if it contains
  67. non-ascii characters.
  68. """
  69. if '@' in addr:
  70. localpart, domain = addr.split('@', 1)
  71. # Try to get the simplest encoding - ascii if possible so that
  72. # to@example.com doesn't become =?utf-8?q?to?=@example.com. This
  73. # makes unit testing a bit easier and more readable.
  74. try:
  75. localpart.encode('ascii')
  76. except UnicodeEncodeError:
  77. localpart = Header(localpart, encoding).encode()
  78. domain = domain.encode('idna').decode('ascii')
  79. else:
  80. localpart = Header(addr, encoding).encode()
  81. domain = ''
  82. return (localpart, domain)
  83. def sanitize_address(addr, encoding):
  84. """
  85. Format a pair of (name, address) or an email address string.
  86. """
  87. if not isinstance(addr, tuple):
  88. addr = parseaddr(addr)
  89. nm, addr = addr
  90. localpart, domain = None, None
  91. nm = Header(nm, encoding).encode()
  92. try:
  93. addr.encode('ascii')
  94. except UnicodeEncodeError: # IDN or non-ascii in the local part
  95. localpart, domain = split_addr(addr, encoding)
  96. # An `email.headerregistry.Address` object is used since
  97. # email.utils.formataddr() naively encodes the name as ascii (see #25986).
  98. if localpart and domain:
  99. address = Address(nm, username=localpart, domain=domain)
  100. return str(address)
  101. try:
  102. address = Address(nm, addr_spec=addr)
  103. except (InvalidHeaderDefect, NonASCIILocalPartDefect):
  104. localpart, domain = split_addr(addr, encoding)
  105. address = Address(nm, username=localpart, domain=domain)
  106. return str(address)
  107. class MIMEMixin:
  108. def as_string(self, unixfrom=False, linesep='\n'):
  109. """Return the entire formatted message as a string.
  110. Optional `unixfrom' when True, means include the Unix From_ envelope
  111. header.
  112. This overrides the default as_string() implementation to not mangle
  113. lines that begin with 'From '. See bug #13433 for details.
  114. """
  115. fp = StringIO()
  116. g = generator.Generator(fp, mangle_from_=False)
  117. g.flatten(self, unixfrom=unixfrom, linesep=linesep)
  118. return fp.getvalue()
  119. def as_bytes(self, unixfrom=False, linesep='\n'):
  120. """Return the entire formatted message as bytes.
  121. Optional `unixfrom' when True, means include the Unix From_ envelope
  122. header.
  123. This overrides the default as_bytes() implementation to not mangle
  124. lines that begin with 'From '. See bug #13433 for details.
  125. """
  126. fp = BytesIO()
  127. g = generator.BytesGenerator(fp, mangle_from_=False)
  128. g.flatten(self, unixfrom=unixfrom, linesep=linesep)
  129. return fp.getvalue()
  130. class SafeMIMEMessage(MIMEMixin, MIMEMessage):
  131. def __setitem__(self, name, val):
  132. # message/rfc822 attachments must be ASCII
  133. name, val = forbid_multi_line_headers(name, val, 'ascii')
  134. MIMEMessage.__setitem__(self, name, val)
  135. class SafeMIMEText(MIMEMixin, MIMEText):
  136. def __init__(self, _text, _subtype='plain', _charset=None):
  137. self.encoding = _charset
  138. MIMEText.__init__(self, _text, _subtype=_subtype, _charset=_charset)
  139. def __setitem__(self, name, val):
  140. name, val = forbid_multi_line_headers(name, val, self.encoding)
  141. MIMEText.__setitem__(self, name, val)
  142. def set_payload(self, payload, charset=None):
  143. if charset == 'utf-8':
  144. has_long_lines = any(
  145. len(l.encode()) > RFC5322_EMAIL_LINE_LENGTH_LIMIT
  146. for l in payload.splitlines()
  147. )
  148. # Quoted-Printable encoding has the side effect of shortening long
  149. # lines, if any (#22561).
  150. charset = utf8_charset_qp if has_long_lines else utf8_charset
  151. MIMEText.set_payload(self, payload, charset=charset)
  152. class SafeMIMEMultipart(MIMEMixin, MIMEMultipart):
  153. def __init__(self, _subtype='mixed', boundary=None, _subparts=None, encoding=None, **_params):
  154. self.encoding = encoding
  155. MIMEMultipart.__init__(self, _subtype, boundary, _subparts, **_params)
  156. def __setitem__(self, name, val):
  157. name, val = forbid_multi_line_headers(name, val, self.encoding)
  158. MIMEMultipart.__setitem__(self, name, val)
  159. class EmailMessage:
  160. """A container for email information."""
  161. content_subtype = 'plain'
  162. mixed_subtype = 'mixed'
  163. encoding = None # None => use settings default
  164. def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
  165. connection=None, attachments=None, headers=None, cc=None,
  166. reply_to=None):
  167. """
  168. Initialize a single email message (which can be sent to multiple
  169. recipients).
  170. """
  171. if to:
  172. if isinstance(to, str):
  173. raise TypeError('"to" argument must be a list or tuple')
  174. self.to = list(to)
  175. else:
  176. self.to = []
  177. if cc:
  178. if isinstance(cc, str):
  179. raise TypeError('"cc" argument must be a list or tuple')
  180. self.cc = list(cc)
  181. else:
  182. self.cc = []
  183. if bcc:
  184. if isinstance(bcc, str):
  185. raise TypeError('"bcc" argument must be a list or tuple')
  186. self.bcc = list(bcc)
  187. else:
  188. self.bcc = []
  189. if reply_to:
  190. if isinstance(reply_to, str):
  191. raise TypeError('"reply_to" argument must be a list or tuple')
  192. self.reply_to = list(reply_to)
  193. else:
  194. self.reply_to = []
  195. self.from_email = from_email or settings.DEFAULT_FROM_EMAIL
  196. self.subject = subject
  197. self.body = body or ''
  198. self.attachments = []
  199. if attachments:
  200. for attachment in attachments:
  201. if isinstance(attachment, MIMEBase):
  202. self.attach(attachment)
  203. else:
  204. self.attach(*attachment)
  205. self.extra_headers = headers or {}
  206. self.connection = connection
  207. def get_connection(self, fail_silently=False):
  208. from django.core.mail import get_connection
  209. if not self.connection:
  210. self.connection = get_connection(fail_silently=fail_silently)
  211. return self.connection
  212. def message(self):
  213. encoding = self.encoding or settings.DEFAULT_CHARSET
  214. msg = SafeMIMEText(self.body, self.content_subtype, encoding)
  215. msg = self._create_message(msg)
  216. msg['Subject'] = self.subject
  217. msg['From'] = self.extra_headers.get('From', self.from_email)
  218. self._set_list_header_if_not_empty(msg, 'To', self.to)
  219. self._set_list_header_if_not_empty(msg, 'Cc', self.cc)
  220. self._set_list_header_if_not_empty(msg, 'Reply-To', self.reply_to)
  221. # Email header names are case-insensitive (RFC 2045), so we have to
  222. # accommodate that when doing comparisons.
  223. header_names = [key.lower() for key in self.extra_headers]
  224. if 'date' not in header_names:
  225. # formatdate() uses stdlib methods to format the date, which use
  226. # the stdlib/OS concept of a timezone, however, Django sets the
  227. # TZ environment variable based on the TIME_ZONE setting which
  228. # will get picked up by formatdate().
  229. msg['Date'] = formatdate(localtime=settings.EMAIL_USE_LOCALTIME)
  230. if 'message-id' not in header_names:
  231. # Use cached DNS_NAME for performance
  232. msg['Message-ID'] = make_msgid(domain=DNS_NAME)
  233. for name, value in self.extra_headers.items():
  234. if name.lower() != 'from': # From is already handled
  235. msg[name] = value
  236. return msg
  237. def recipients(self):
  238. """
  239. Return a list of all recipients of the email (includes direct
  240. addressees as well as Cc and Bcc entries).
  241. """
  242. return [email for email in (self.to + self.cc + self.bcc) if email]
  243. def send(self, fail_silently=False):
  244. """Send the email message."""
  245. if not self.recipients():
  246. # Don't bother creating the network connection if there's nobody to
  247. # send to.
  248. return 0
  249. return self.get_connection(fail_silently).send_messages([self])
  250. def attach(self, filename=None, content=None, mimetype=None):
  251. """
  252. Attach a file with the given filename and content. The filename can
  253. be omitted and the mimetype is guessed, if not provided.
  254. If the first parameter is a MIMEBase subclass, insert it directly
  255. into the resulting message attachments.
  256. For a text/* mimetype (guessed or specified), when a bytes object is
  257. specified as content, decode it as UTF-8. If that fails, set the
  258. mimetype to DEFAULT_ATTACHMENT_MIME_TYPE and don't decode the content.
  259. """
  260. if isinstance(filename, MIMEBase):
  261. assert content is None
  262. assert mimetype is None
  263. self.attachments.append(filename)
  264. else:
  265. assert content is not None
  266. mimetype = mimetype or mimetypes.guess_type(filename)[0] or DEFAULT_ATTACHMENT_MIME_TYPE
  267. basetype, subtype = mimetype.split('/', 1)
  268. if basetype == 'text':
  269. if isinstance(content, bytes):
  270. try:
  271. content = content.decode()
  272. except UnicodeDecodeError:
  273. # If mimetype suggests the file is text but it's
  274. # actually binary, read() raises a UnicodeDecodeError.
  275. mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
  276. self.attachments.append((filename, content, mimetype))
  277. def attach_file(self, path, mimetype=None):
  278. """
  279. Attach a file from the filesystem.
  280. Set the mimetype to DEFAULT_ATTACHMENT_MIME_TYPE if it isn't specified
  281. and cannot be guessed.
  282. For a text/* mimetype (guessed or specified), decode the file's content
  283. as UTF-8. If that fails, set the mimetype to
  284. DEFAULT_ATTACHMENT_MIME_TYPE and don't decode the content.
  285. """
  286. path = Path(path)
  287. with path.open('rb') as file:
  288. content = file.read()
  289. self.attach(path.name, content, mimetype)
  290. def _create_message(self, msg):
  291. return self._create_attachments(msg)
  292. def _create_attachments(self, msg):
  293. if self.attachments:
  294. encoding = self.encoding or settings.DEFAULT_CHARSET
  295. body_msg = msg
  296. msg = SafeMIMEMultipart(_subtype=self.mixed_subtype, encoding=encoding)
  297. if self.body or body_msg.is_multipart():
  298. msg.attach(body_msg)
  299. for attachment in self.attachments:
  300. if isinstance(attachment, MIMEBase):
  301. msg.attach(attachment)
  302. else:
  303. msg.attach(self._create_attachment(*attachment))
  304. return msg
  305. def _create_mime_attachment(self, content, mimetype):
  306. """
  307. Convert the content, mimetype pair into a MIME attachment object.
  308. If the mimetype is message/rfc822, content may be an
  309. email.Message or EmailMessage object, as well as a str.
  310. """
  311. basetype, subtype = mimetype.split('/', 1)
  312. if basetype == 'text':
  313. encoding = self.encoding or settings.DEFAULT_CHARSET
  314. attachment = SafeMIMEText(content, subtype, encoding)
  315. elif basetype == 'message' and subtype == 'rfc822':
  316. # Bug #18967: per RFC2046 s5.2.1, message/rfc822 attachments
  317. # must not be base64 encoded.
  318. if isinstance(content, EmailMessage):
  319. # convert content into an email.Message first
  320. content = content.message()
  321. elif not isinstance(content, Message):
  322. # For compatibility with existing code, parse the message
  323. # into an email.Message object if it is not one already.
  324. content = message_from_string(force_text(content))
  325. attachment = SafeMIMEMessage(content, subtype)
  326. else:
  327. # Encode non-text attachments with base64.
  328. attachment = MIMEBase(basetype, subtype)
  329. attachment.set_payload(content)
  330. Encoders.encode_base64(attachment)
  331. return attachment
  332. def _create_attachment(self, filename, content, mimetype=None):
  333. """
  334. Convert the filename, content, mimetype triple into a MIME attachment
  335. object.
  336. """
  337. attachment = self._create_mime_attachment(content, mimetype)
  338. if filename:
  339. try:
  340. filename.encode('ascii')
  341. except UnicodeEncodeError:
  342. filename = ('utf-8', '', filename)
  343. attachment.add_header('Content-Disposition', 'attachment', filename=filename)
  344. return attachment
  345. def _set_list_header_if_not_empty(self, msg, header, values):
  346. """
  347. Set msg's header, either from self.extra_headers, if present, or from
  348. the values argument.
  349. """
  350. if values:
  351. try:
  352. value = self.extra_headers[header]
  353. except KeyError:
  354. value = ', '.join(str(v) for v in values)
  355. msg[header] = value
  356. class EmailMultiAlternatives(EmailMessage):
  357. """
  358. A version of EmailMessage that makes it easy to send multipart/alternative
  359. messages. For example, including text and HTML versions of the text is
  360. made easier.
  361. """
  362. alternative_subtype = 'alternative'
  363. def __init__(self, subject='', body='', from_email=None, to=None, bcc=None,
  364. connection=None, attachments=None, headers=None, alternatives=None,
  365. cc=None, reply_to=None):
  366. """
  367. Initialize a single email message (which can be sent to multiple
  368. recipients).
  369. """
  370. super().__init__(
  371. subject, body, from_email, to, bcc, connection, attachments,
  372. headers, cc, reply_to,
  373. )
  374. self.alternatives = alternatives or []
  375. def attach_alternative(self, content, mimetype):
  376. """Attach an alternative content representation."""
  377. assert content is not None
  378. assert mimetype is not None
  379. self.alternatives.append((content, mimetype))
  380. def _create_message(self, msg):
  381. return self._create_attachments(self._create_alternatives(msg))
  382. def _create_alternatives(self, msg):
  383. encoding = self.encoding or settings.DEFAULT_CHARSET
  384. if self.alternatives:
  385. body_msg = msg
  386. msg = SafeMIMEMultipart(_subtype=self.alternative_subtype, encoding=encoding)
  387. if self.body:
  388. msg.attach(body_msg)
  389. for alternative in self.alternatives:
  390. msg.attach(self._create_mime_attachment(*alternative))
  391. return msg