Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/core/mail/message.py: 21%
257 statements
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
« prev ^ index » next coverage.py v6.4.4, created at 2023-07-17 14:22 -0600
1import mimetypes
2from email import charset as Charset
3from email import encoders as Encoders
4from email import generator, message_from_string
5from email.errors import HeaderParseError
6from email.header import Header
7from email.headerregistry import Address, parser
8from email.message import Message
9from email.mime.base import MIMEBase
10from email.mime.message import MIMEMessage
11from email.mime.multipart import MIMEMultipart
12from email.mime.text import MIMEText
13from email.utils import formataddr, formatdate, getaddresses, make_msgid
14from io import BytesIO, StringIO
15from pathlib import Path
17from django.conf import settings
18from django.core.mail.utils import DNS_NAME
19from django.utils.encoding import force_str, punycode
21# Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from
22# some spam filters.
23utf8_charset = Charset.Charset("utf-8")
24utf8_charset.body_encoding = None # Python defaults to BASE64
25utf8_charset_qp = Charset.Charset("utf-8")
26utf8_charset_qp.body_encoding = Charset.QP
28# Default MIME type to use on attachments (if it is not explicitly given
29# and cannot be guessed).
30DEFAULT_ATTACHMENT_MIME_TYPE = "application/octet-stream"
32RFC5322_EMAIL_LINE_LENGTH_LIMIT = 998
35class BadHeaderError(ValueError):
36 pass
39# Header names that contain structured address data (RFC #5322)
40ADDRESS_HEADERS = {
41 "from",
42 "sender",
43 "reply-to",
44 "to",
45 "cc",
46 "bcc",
47 "resent-from",
48 "resent-sender",
49 "resent-to",
50 "resent-cc",
51 "resent-bcc",
52}
55def forbid_multi_line_headers(name, val, encoding):
56 """Forbid multi-line headers to prevent header injection."""
57 encoding = encoding or settings.DEFAULT_CHARSET
58 val = str(val) # val may be lazy
59 if "\n" in val or "\r" in val:
60 raise BadHeaderError(
61 "Header values can't contain newlines (got %r for header %r)" % (val, name)
62 )
63 try:
64 val.encode("ascii")
65 except UnicodeEncodeError:
66 if name.lower() in ADDRESS_HEADERS:
67 val = ", ".join(
68 sanitize_address(addr, encoding) for addr in getaddresses((val,))
69 )
70 else:
71 val = Header(val, encoding).encode()
72 else:
73 if name.lower() == "subject":
74 val = Header(val).encode()
75 return name, val
78def sanitize_address(addr, encoding):
79 """
80 Format a pair of (name, address) or an email address string.
81 """
82 address = None
83 if not isinstance(addr, tuple):
84 addr = force_str(addr)
85 try:
86 token, rest = parser.get_mailbox(addr)
87 except (HeaderParseError, ValueError, IndexError):
88 raise ValueError('Invalid address "%s"' % addr)
89 else:
90 if rest:
91 # The entire email address must be parsed.
92 raise ValueError(
93 'Invalid address; only %s could be parsed from "%s"' % (token, addr)
94 )
95 nm = token.display_name or ""
96 localpart = token.local_part
97 domain = token.domain or ""
98 else:
99 nm, address = addr
100 localpart, domain = address.rsplit("@", 1)
102 address_parts = nm + localpart + domain
103 if "\n" in address_parts or "\r" in address_parts:
104 raise ValueError("Invalid address; address parts cannot contain newlines.")
106 # Avoid UTF-8 encode, if it's possible.
107 try:
108 nm.encode("ascii")
109 nm = Header(nm).encode()
110 except UnicodeEncodeError:
111 nm = Header(nm, encoding).encode()
112 try:
113 localpart.encode("ascii")
114 except UnicodeEncodeError:
115 localpart = Header(localpart, encoding).encode()
116 domain = punycode(domain)
118 parsed_address = Address(username=localpart, domain=domain)
119 return formataddr((nm, parsed_address.addr_spec))
122class MIMEMixin:
123 def as_string(self, unixfrom=False, linesep="\n"):
124 """Return the entire formatted message as a string.
125 Optional `unixfrom' when True, means include the Unix From_ envelope
126 header.
128 This overrides the default as_string() implementation to not mangle
129 lines that begin with 'From '. See bug #13433 for details.
130 """
131 fp = StringIO()
132 g = generator.Generator(fp, mangle_from_=False)
133 g.flatten(self, unixfrom=unixfrom, linesep=linesep)
134 return fp.getvalue()
136 def as_bytes(self, unixfrom=False, linesep="\n"):
137 """Return the entire formatted message as bytes.
138 Optional `unixfrom' when True, means include the Unix From_ envelope
139 header.
141 This overrides the default as_bytes() implementation to not mangle
142 lines that begin with 'From '. See bug #13433 for details.
143 """
144 fp = BytesIO()
145 g = generator.BytesGenerator(fp, mangle_from_=False)
146 g.flatten(self, unixfrom=unixfrom, linesep=linesep)
147 return fp.getvalue()
150class SafeMIMEMessage(MIMEMixin, MIMEMessage):
151 def __setitem__(self, name, val):
152 # message/rfc822 attachments must be ASCII
153 name, val = forbid_multi_line_headers(name, val, "ascii")
154 MIMEMessage.__setitem__(self, name, val)
157class SafeMIMEText(MIMEMixin, MIMEText):
158 def __init__(self, _text, _subtype="plain", _charset=None):
159 self.encoding = _charset
160 MIMEText.__init__(self, _text, _subtype=_subtype, _charset=_charset)
162 def __setitem__(self, name, val):
163 name, val = forbid_multi_line_headers(name, val, self.encoding)
164 MIMEText.__setitem__(self, name, val)
166 def set_payload(self, payload, charset=None):
167 if charset == "utf-8" and not isinstance(charset, Charset.Charset):
168 has_long_lines = any(
169 len(line.encode()) > RFC5322_EMAIL_LINE_LENGTH_LIMIT
170 for line in payload.splitlines()
171 )
172 # Quoted-Printable encoding has the side effect of shortening long
173 # lines, if any (#22561).
174 charset = utf8_charset_qp if has_long_lines else utf8_charset
175 MIMEText.set_payload(self, payload, charset=charset)
178class SafeMIMEMultipart(MIMEMixin, MIMEMultipart):
179 def __init__(
180 self, _subtype="mixed", boundary=None, _subparts=None, encoding=None, **_params
181 ):
182 self.encoding = encoding
183 MIMEMultipart.__init__(self, _subtype, boundary, _subparts, **_params)
185 def __setitem__(self, name, val):
186 name, val = forbid_multi_line_headers(name, val, self.encoding)
187 MIMEMultipart.__setitem__(self, name, val)
190class EmailMessage:
191 """A container for email information."""
193 content_subtype = "plain"
194 mixed_subtype = "mixed"
195 encoding = None # None => use settings default
197 def __init__(
198 self,
199 subject="",
200 body="",
201 from_email=None,
202 to=None,
203 bcc=None,
204 connection=None,
205 attachments=None,
206 headers=None,
207 cc=None,
208 reply_to=None,
209 ):
210 """
211 Initialize a single email message (which can be sent to multiple
212 recipients).
213 """
214 if to:
215 if isinstance(to, str):
216 raise TypeError('"to" argument must be a list or tuple')
217 self.to = list(to)
218 else:
219 self.to = []
220 if cc:
221 if isinstance(cc, str):
222 raise TypeError('"cc" argument must be a list or tuple')
223 self.cc = list(cc)
224 else:
225 self.cc = []
226 if bcc:
227 if isinstance(bcc, str):
228 raise TypeError('"bcc" argument must be a list or tuple')
229 self.bcc = list(bcc)
230 else:
231 self.bcc = []
232 if reply_to:
233 if isinstance(reply_to, str):
234 raise TypeError('"reply_to" argument must be a list or tuple')
235 self.reply_to = list(reply_to)
236 else:
237 self.reply_to = []
238 self.from_email = from_email or settings.DEFAULT_FROM_EMAIL
239 self.subject = subject
240 self.body = body or ""
241 self.attachments = []
242 if attachments:
243 for attachment in attachments:
244 if isinstance(attachment, MIMEBase):
245 self.attach(attachment)
246 else:
247 self.attach(*attachment)
248 self.extra_headers = headers or {}
249 self.connection = connection
251 def get_connection(self, fail_silently=False):
252 from django.core.mail import get_connection
254 if not self.connection:
255 self.connection = get_connection(fail_silently=fail_silently)
256 return self.connection
258 def message(self):
259 encoding = self.encoding or settings.DEFAULT_CHARSET
260 msg = SafeMIMEText(self.body, self.content_subtype, encoding)
261 msg = self._create_message(msg)
262 msg["Subject"] = self.subject
263 msg["From"] = self.extra_headers.get("From", self.from_email)
264 self._set_list_header_if_not_empty(msg, "To", self.to)
265 self._set_list_header_if_not_empty(msg, "Cc", self.cc)
266 self._set_list_header_if_not_empty(msg, "Reply-To", self.reply_to)
268 # Email header names are case-insensitive (RFC 2045), so we have to
269 # accommodate that when doing comparisons.
270 header_names = [key.lower() for key in self.extra_headers]
271 if "date" not in header_names:
272 # formatdate() uses stdlib methods to format the date, which use
273 # the stdlib/OS concept of a timezone, however, Django sets the
274 # TZ environment variable based on the TIME_ZONE setting which
275 # will get picked up by formatdate().
276 msg["Date"] = formatdate(localtime=settings.EMAIL_USE_LOCALTIME)
277 if "message-id" not in header_names:
278 # Use cached DNS_NAME for performance
279 msg["Message-ID"] = make_msgid(domain=DNS_NAME)
280 for name, value in self.extra_headers.items():
281 if name.lower() != "from": # From is already handled
282 msg[name] = value
283 return msg
285 def recipients(self):
286 """
287 Return a list of all recipients of the email (includes direct
288 addressees as well as Cc and Bcc entries).
289 """
290 return [email for email in (self.to + self.cc + self.bcc) if email]
292 def send(self, fail_silently=False):
293 """Send the email message."""
294 if not self.recipients():
295 # Don't bother creating the network connection if there's nobody to
296 # send to.
297 return 0
298 return self.get_connection(fail_silently).send_messages([self])
300 def attach(self, filename=None, content=None, mimetype=None):
301 """
302 Attach a file with the given filename and content. The filename can
303 be omitted and the mimetype is guessed, if not provided.
305 If the first parameter is a MIMEBase subclass, insert it directly
306 into the resulting message attachments.
308 For a text/* mimetype (guessed or specified), when a bytes object is
309 specified as content, decode it as UTF-8. If that fails, set the
310 mimetype to DEFAULT_ATTACHMENT_MIME_TYPE and don't decode the content.
311 """
312 if isinstance(filename, MIMEBase):
313 if content is not None or mimetype is not None:
314 raise ValueError(
315 "content and mimetype must not be given when a MIMEBase "
316 "instance is provided."
317 )
318 self.attachments.append(filename)
319 elif content is None:
320 raise ValueError("content must be provided.")
321 else:
322 mimetype = (
323 mimetype
324 or mimetypes.guess_type(filename)[0]
325 or DEFAULT_ATTACHMENT_MIME_TYPE
326 )
327 basetype, subtype = mimetype.split("/", 1)
329 if basetype == "text":
330 if isinstance(content, bytes):
331 try:
332 content = content.decode()
333 except UnicodeDecodeError:
334 # If mimetype suggests the file is text but it's
335 # actually binary, read() raises a UnicodeDecodeError.
336 mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
338 self.attachments.append((filename, content, mimetype))
340 def attach_file(self, path, mimetype=None):
341 """
342 Attach a file from the filesystem.
344 Set the mimetype to DEFAULT_ATTACHMENT_MIME_TYPE if it isn't specified
345 and cannot be guessed.
347 For a text/* mimetype (guessed or specified), decode the file's content
348 as UTF-8. If that fails, set the mimetype to
349 DEFAULT_ATTACHMENT_MIME_TYPE and don't decode the content.
350 """
351 path = Path(path)
352 with path.open("rb") as file:
353 content = file.read()
354 self.attach(path.name, content, mimetype)
356 def _create_message(self, msg):
357 return self._create_attachments(msg)
359 def _create_attachments(self, msg):
360 if self.attachments:
361 encoding = self.encoding or settings.DEFAULT_CHARSET
362 body_msg = msg
363 msg = SafeMIMEMultipart(_subtype=self.mixed_subtype, encoding=encoding)
364 if self.body or body_msg.is_multipart():
365 msg.attach(body_msg)
366 for attachment in self.attachments:
367 if isinstance(attachment, MIMEBase):
368 msg.attach(attachment)
369 else:
370 msg.attach(self._create_attachment(*attachment))
371 return msg
373 def _create_mime_attachment(self, content, mimetype):
374 """
375 Convert the content, mimetype pair into a MIME attachment object.
377 If the mimetype is message/rfc822, content may be an
378 email.Message or EmailMessage object, as well as a str.
379 """
380 basetype, subtype = mimetype.split("/", 1)
381 if basetype == "text":
382 encoding = self.encoding or settings.DEFAULT_CHARSET
383 attachment = SafeMIMEText(content, subtype, encoding)
384 elif basetype == "message" and subtype == "rfc822":
385 # Bug #18967: per RFC2046 s5.2.1, message/rfc822 attachments
386 # must not be base64 encoded.
387 if isinstance(content, EmailMessage):
388 # convert content into an email.Message first
389 content = content.message()
390 elif not isinstance(content, Message):
391 # For compatibility with existing code, parse the message
392 # into an email.Message object if it is not one already.
393 content = message_from_string(force_str(content))
395 attachment = SafeMIMEMessage(content, subtype)
396 else:
397 # Encode non-text attachments with base64.
398 attachment = MIMEBase(basetype, subtype)
399 attachment.set_payload(content)
400 Encoders.encode_base64(attachment)
401 return attachment
403 def _create_attachment(self, filename, content, mimetype=None):
404 """
405 Convert the filename, content, mimetype triple into a MIME attachment
406 object.
407 """
408 attachment = self._create_mime_attachment(content, mimetype)
409 if filename:
410 try:
411 filename.encode("ascii")
412 except UnicodeEncodeError:
413 filename = ("utf-8", "", filename)
414 attachment.add_header(
415 "Content-Disposition", "attachment", filename=filename
416 )
417 return attachment
419 def _set_list_header_if_not_empty(self, msg, header, values):
420 """
421 Set msg's header, either from self.extra_headers, if present, or from
422 the values argument.
423 """
424 if values:
425 try:
426 value = self.extra_headers[header]
427 except KeyError:
428 value = ", ".join(str(v) for v in values)
429 msg[header] = value
432class EmailMultiAlternatives(EmailMessage):
433 """
434 A version of EmailMessage that makes it easy to send multipart/alternative
435 messages. For example, including text and HTML versions of the text is
436 made easier.
437 """
439 alternative_subtype = "alternative"
441 def __init__(
442 self,
443 subject="",
444 body="",
445 from_email=None,
446 to=None,
447 bcc=None,
448 connection=None,
449 attachments=None,
450 headers=None,
451 alternatives=None,
452 cc=None,
453 reply_to=None,
454 ):
455 """
456 Initialize a single email message (which can be sent to multiple
457 recipients).
458 """
459 super().__init__(
460 subject,
461 body,
462 from_email,
463 to,
464 bcc,
465 connection,
466 attachments,
467 headers,
468 cc,
469 reply_to,
470 )
471 self.alternatives = alternatives or []
473 def attach_alternative(self, content, mimetype):
474 """Attach an alternative content representation."""
475 if content is None or mimetype is None:
476 raise ValueError("Both content and mimetype must be provided.")
477 self.alternatives.append((content, mimetype))
479 def _create_message(self, msg):
480 return self._create_attachments(self._create_alternatives(msg))
482 def _create_alternatives(self, msg):
483 encoding = self.encoding or settings.DEFAULT_CHARSET
484 if self.alternatives:
485 body_msg = msg
486 msg = SafeMIMEMultipart(
487 _subtype=self.alternative_subtype, encoding=encoding
488 )
489 if self.body:
490 msg.attach(body_msg)
491 for alternative in self.alternatives:
492 msg.attach(self._create_mime_attachment(*alternative))
493 return msg