Initial commit
This commit is contained in:
159
.venv/Lib/site-packages/django/core/mail/__init__.py
Normal file
159
.venv/Lib/site-packages/django/core/mail/__init__.py
Normal file
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
Tools for sending email.
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
# Imported for backwards compatibility and for the sake
|
||||
# of a cleaner namespace. These symbols used to be in
|
||||
# django/core/mail.py before the introduction of email
|
||||
# backends and the subsequent reorganization (See #10355)
|
||||
from django.core.mail.message import (
|
||||
DEFAULT_ATTACHMENT_MIME_TYPE,
|
||||
BadHeaderError,
|
||||
EmailAlternative,
|
||||
EmailAttachment,
|
||||
EmailMessage,
|
||||
EmailMultiAlternatives,
|
||||
SafeMIMEMultipart,
|
||||
SafeMIMEText,
|
||||
forbid_multi_line_headers,
|
||||
make_msgid,
|
||||
)
|
||||
from django.core.mail.utils import DNS_NAME, CachedDnsName
|
||||
from django.utils.module_loading import import_string
|
||||
|
||||
__all__ = [
|
||||
"CachedDnsName",
|
||||
"DNS_NAME",
|
||||
"EmailMessage",
|
||||
"EmailMultiAlternatives",
|
||||
"SafeMIMEText",
|
||||
"SafeMIMEMultipart",
|
||||
"DEFAULT_ATTACHMENT_MIME_TYPE",
|
||||
"make_msgid",
|
||||
"BadHeaderError",
|
||||
"forbid_multi_line_headers",
|
||||
"get_connection",
|
||||
"send_mail",
|
||||
"send_mass_mail",
|
||||
"mail_admins",
|
||||
"mail_managers",
|
||||
"EmailAlternative",
|
||||
"EmailAttachment",
|
||||
]
|
||||
|
||||
|
||||
def get_connection(backend=None, fail_silently=False, **kwds):
|
||||
"""Load an email backend and return an instance of it.
|
||||
|
||||
If backend is None (default), use settings.EMAIL_BACKEND.
|
||||
|
||||
Both fail_silently and other keyword arguments are used in the
|
||||
constructor of the backend.
|
||||
"""
|
||||
klass = import_string(backend or settings.EMAIL_BACKEND)
|
||||
return klass(fail_silently=fail_silently, **kwds)
|
||||
|
||||
|
||||
def send_mail(
|
||||
subject,
|
||||
message,
|
||||
from_email,
|
||||
recipient_list,
|
||||
fail_silently=False,
|
||||
auth_user=None,
|
||||
auth_password=None,
|
||||
connection=None,
|
||||
html_message=None,
|
||||
):
|
||||
"""
|
||||
Easy wrapper for sending a single message to a recipient list. All members
|
||||
of the recipient list will see the other recipients in the 'To' field.
|
||||
|
||||
If from_email is None, use the DEFAULT_FROM_EMAIL setting.
|
||||
If auth_user is None, use the EMAIL_HOST_USER setting.
|
||||
If auth_password is None, use the EMAIL_HOST_PASSWORD setting.
|
||||
|
||||
Note: The API for this method is frozen. New code wanting to extend the
|
||||
functionality should use the EmailMessage class directly.
|
||||
"""
|
||||
connection = connection or get_connection(
|
||||
username=auth_user,
|
||||
password=auth_password,
|
||||
fail_silently=fail_silently,
|
||||
)
|
||||
mail = EmailMultiAlternatives(
|
||||
subject, message, from_email, recipient_list, connection=connection
|
||||
)
|
||||
if html_message:
|
||||
mail.attach_alternative(html_message, "text/html")
|
||||
|
||||
return mail.send()
|
||||
|
||||
|
||||
def send_mass_mail(
|
||||
datatuple, fail_silently=False, auth_user=None, auth_password=None, connection=None
|
||||
):
|
||||
"""
|
||||
Given a datatuple of (subject, message, from_email, recipient_list), send
|
||||
each message to each recipient list. Return the number of emails sent.
|
||||
|
||||
If from_email is None, use the DEFAULT_FROM_EMAIL setting.
|
||||
If auth_user and auth_password are set, use them to log in.
|
||||
If auth_user is None, use the EMAIL_HOST_USER setting.
|
||||
If auth_password is None, use the EMAIL_HOST_PASSWORD setting.
|
||||
|
||||
Note: The API for this method is frozen. New code wanting to extend the
|
||||
functionality should use the EmailMessage class directly.
|
||||
"""
|
||||
connection = connection or get_connection(
|
||||
username=auth_user,
|
||||
password=auth_password,
|
||||
fail_silently=fail_silently,
|
||||
)
|
||||
messages = [
|
||||
EmailMessage(subject, message, sender, recipient, connection=connection)
|
||||
for subject, message, sender, recipient in datatuple
|
||||
]
|
||||
return connection.send_messages(messages)
|
||||
|
||||
|
||||
def mail_admins(
|
||||
subject, message, fail_silently=False, connection=None, html_message=None
|
||||
):
|
||||
"""Send a message to the admins, as defined by the ADMINS setting."""
|
||||
if not settings.ADMINS:
|
||||
return
|
||||
if not all(isinstance(a, (list, tuple)) and len(a) == 2 for a in settings.ADMINS):
|
||||
raise ValueError("The ADMINS setting must be a list of 2-tuples.")
|
||||
mail = EmailMultiAlternatives(
|
||||
"%s%s" % (settings.EMAIL_SUBJECT_PREFIX, subject),
|
||||
message,
|
||||
settings.SERVER_EMAIL,
|
||||
[a[1] for a in settings.ADMINS],
|
||||
connection=connection,
|
||||
)
|
||||
if html_message:
|
||||
mail.attach_alternative(html_message, "text/html")
|
||||
mail.send(fail_silently=fail_silently)
|
||||
|
||||
|
||||
def mail_managers(
|
||||
subject, message, fail_silently=False, connection=None, html_message=None
|
||||
):
|
||||
"""Send a message to the managers, as defined by the MANAGERS setting."""
|
||||
if not settings.MANAGERS:
|
||||
return
|
||||
if not all(isinstance(a, (list, tuple)) and len(a) == 2 for a in settings.MANAGERS):
|
||||
raise ValueError("The MANAGERS setting must be a list of 2-tuples.")
|
||||
mail = EmailMultiAlternatives(
|
||||
"%s%s" % (settings.EMAIL_SUBJECT_PREFIX, subject),
|
||||
message,
|
||||
settings.SERVER_EMAIL,
|
||||
[a[1] for a in settings.MANAGERS],
|
||||
connection=connection,
|
||||
)
|
||||
if html_message:
|
||||
mail.attach_alternative(html_message, "text/html")
|
||||
mail.send(fail_silently=fail_silently)
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1 @@
|
||||
# Mail backends shipped with Django.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
62
.venv/Lib/site-packages/django/core/mail/backends/base.py
Normal file
62
.venv/Lib/site-packages/django/core/mail/backends/base.py
Normal file
@@ -0,0 +1,62 @@
|
||||
"""Base email backend class."""
|
||||
|
||||
|
||||
class BaseEmailBackend:
|
||||
"""
|
||||
Base class for email backend implementations.
|
||||
|
||||
Subclasses must at least overwrite send_messages().
|
||||
|
||||
open() and close() can be called indirectly by using a backend object as a
|
||||
context manager:
|
||||
|
||||
with backend as connection:
|
||||
# do something with connection
|
||||
pass
|
||||
"""
|
||||
|
||||
def __init__(self, fail_silently=False, **kwargs):
|
||||
self.fail_silently = fail_silently
|
||||
|
||||
def open(self):
|
||||
"""
|
||||
Open a network connection.
|
||||
|
||||
This method can be overwritten by backend implementations to
|
||||
open a network connection.
|
||||
|
||||
It's up to the backend implementation to track the status of
|
||||
a network connection if it's needed by the backend.
|
||||
|
||||
This method can be called by applications to force a single
|
||||
network connection to be used when sending mails. See the
|
||||
send_messages() method of the SMTP backend for a reference
|
||||
implementation.
|
||||
|
||||
The default implementation does nothing.
|
||||
"""
|
||||
pass
|
||||
|
||||
def close(self):
|
||||
"""Close a network connection."""
|
||||
pass
|
||||
|
||||
def __enter__(self):
|
||||
try:
|
||||
self.open()
|
||||
except Exception:
|
||||
self.close()
|
||||
raise
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
|
||||
def send_messages(self, email_messages):
|
||||
"""
|
||||
Send one or more EmailMessage objects and return the number of email
|
||||
messages sent.
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
"subclasses of BaseEmailBackend must override send_messages() method"
|
||||
)
|
||||
45
.venv/Lib/site-packages/django/core/mail/backends/console.py
Normal file
45
.venv/Lib/site-packages/django/core/mail/backends/console.py
Normal file
@@ -0,0 +1,45 @@
|
||||
"""
|
||||
Email backend that writes messages to console instead of sending them.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import threading
|
||||
|
||||
from django.core.mail.backends.base import BaseEmailBackend
|
||||
|
||||
|
||||
class EmailBackend(BaseEmailBackend):
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.stream = kwargs.pop("stream", sys.stdout)
|
||||
self._lock = threading.RLock()
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def write_message(self, message):
|
||||
msg = message.message()
|
||||
msg_data = msg.as_bytes()
|
||||
charset = (
|
||||
msg.get_charset().get_output_charset() if msg.get_charset() else "utf-8"
|
||||
)
|
||||
msg_data = msg_data.decode(charset)
|
||||
self.stream.write("%s\n" % msg_data)
|
||||
self.stream.write("-" * 79)
|
||||
self.stream.write("\n")
|
||||
|
||||
def send_messages(self, email_messages):
|
||||
"""Write all messages to the stream in a thread-safe way."""
|
||||
if not email_messages:
|
||||
return
|
||||
msg_count = 0
|
||||
with self._lock:
|
||||
try:
|
||||
stream_created = self.open()
|
||||
for message in email_messages:
|
||||
self.write_message(message)
|
||||
self.stream.flush() # flush after each message
|
||||
msg_count += 1
|
||||
if stream_created:
|
||||
self.close()
|
||||
except Exception:
|
||||
if not self.fail_silently:
|
||||
raise
|
||||
return msg_count
|
||||
10
.venv/Lib/site-packages/django/core/mail/backends/dummy.py
Normal file
10
.venv/Lib/site-packages/django/core/mail/backends/dummy.py
Normal file
@@ -0,0 +1,10 @@
|
||||
"""
|
||||
Dummy email backend that does nothing.
|
||||
"""
|
||||
|
||||
from django.core.mail.backends.base import BaseEmailBackend
|
||||
|
||||
|
||||
class EmailBackend(BaseEmailBackend):
|
||||
def send_messages(self, email_messages):
|
||||
return len(list(email_messages))
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Email backend that writes messages to a file."""
|
||||
|
||||
import datetime
|
||||
import os
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.exceptions import ImproperlyConfigured
|
||||
from django.core.mail.backends.console import EmailBackend as ConsoleEmailBackend
|
||||
|
||||
|
||||
class EmailBackend(ConsoleEmailBackend):
|
||||
def __init__(self, *args, file_path=None, **kwargs):
|
||||
self._fname = None
|
||||
if file_path is not None:
|
||||
self.file_path = file_path
|
||||
else:
|
||||
self.file_path = getattr(settings, "EMAIL_FILE_PATH", None)
|
||||
self.file_path = os.path.abspath(self.file_path)
|
||||
try:
|
||||
os.makedirs(self.file_path, exist_ok=True)
|
||||
except FileExistsError:
|
||||
raise ImproperlyConfigured(
|
||||
"Path for saving email messages exists, but is not a directory: %s"
|
||||
% self.file_path
|
||||
)
|
||||
except OSError as err:
|
||||
raise ImproperlyConfigured(
|
||||
"Could not create directory for saving email messages: %s (%s)"
|
||||
% (self.file_path, err)
|
||||
)
|
||||
# Make sure that self.file_path is writable.
|
||||
if not os.access(self.file_path, os.W_OK):
|
||||
raise ImproperlyConfigured(
|
||||
"Could not write to directory: %s" % self.file_path
|
||||
)
|
||||
# Finally, call super().
|
||||
# Since we're using the console-based backend as a base,
|
||||
# force the stream to be None, so we don't default to stdout
|
||||
kwargs["stream"] = None
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def write_message(self, message):
|
||||
self.stream.write(message.message().as_bytes() + b"\n")
|
||||
self.stream.write(b"-" * 79)
|
||||
self.stream.write(b"\n")
|
||||
|
||||
def _get_filename(self):
|
||||
"""Return a unique file name."""
|
||||
if self._fname is None:
|
||||
timestamp = datetime.datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
fname = "%s-%s.log" % (timestamp, abs(id(self)))
|
||||
self._fname = os.path.join(self.file_path, fname)
|
||||
return self._fname
|
||||
|
||||
def open(self):
|
||||
if self.stream is None:
|
||||
self.stream = open(self._get_filename(), "ab")
|
||||
return True
|
||||
return False
|
||||
|
||||
def close(self):
|
||||
try:
|
||||
if self.stream is not None:
|
||||
self.stream.close()
|
||||
finally:
|
||||
self.stream = None
|
||||
33
.venv/Lib/site-packages/django/core/mail/backends/locmem.py
Normal file
33
.venv/Lib/site-packages/django/core/mail/backends/locmem.py
Normal file
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
Backend for test environment.
|
||||
"""
|
||||
|
||||
import copy
|
||||
|
||||
from django.core import mail
|
||||
from django.core.mail.backends.base import BaseEmailBackend
|
||||
|
||||
|
||||
class EmailBackend(BaseEmailBackend):
|
||||
"""
|
||||
An email backend for use during test sessions.
|
||||
|
||||
The test connection stores email messages in a dummy outbox,
|
||||
rather than sending them out on the wire.
|
||||
|
||||
The dummy outbox is accessible through the outbox instance attribute.
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
if not hasattr(mail, "outbox"):
|
||||
mail.outbox = []
|
||||
|
||||
def send_messages(self, messages):
|
||||
"""Redirect messages to the dummy outbox"""
|
||||
msg_count = 0
|
||||
for message in messages: # .message() triggers header validation
|
||||
message.message()
|
||||
mail.outbox.append(copy.deepcopy(message))
|
||||
msg_count += 1
|
||||
return msg_count
|
||||
162
.venv/Lib/site-packages/django/core/mail/backends/smtp.py
Normal file
162
.venv/Lib/site-packages/django/core/mail/backends/smtp.py
Normal file
@@ -0,0 +1,162 @@
|
||||
"""SMTP email backend class."""
|
||||
|
||||
import smtplib
|
||||
import ssl
|
||||
import threading
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail.backends.base import BaseEmailBackend
|
||||
from django.core.mail.message import sanitize_address
|
||||
from django.core.mail.utils import DNS_NAME
|
||||
from django.utils.functional import cached_property
|
||||
|
||||
|
||||
class EmailBackend(BaseEmailBackend):
|
||||
"""
|
||||
A wrapper that manages the SMTP network connection.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
host=None,
|
||||
port=None,
|
||||
username=None,
|
||||
password=None,
|
||||
use_tls=None,
|
||||
fail_silently=False,
|
||||
use_ssl=None,
|
||||
timeout=None,
|
||||
ssl_keyfile=None,
|
||||
ssl_certfile=None,
|
||||
**kwargs,
|
||||
):
|
||||
super().__init__(fail_silently=fail_silently)
|
||||
self.host = host or settings.EMAIL_HOST
|
||||
self.port = port or settings.EMAIL_PORT
|
||||
self.username = settings.EMAIL_HOST_USER if username is None else username
|
||||
self.password = settings.EMAIL_HOST_PASSWORD if password is None else password
|
||||
self.use_tls = settings.EMAIL_USE_TLS if use_tls is None else use_tls
|
||||
self.use_ssl = settings.EMAIL_USE_SSL if use_ssl is None else use_ssl
|
||||
self.timeout = settings.EMAIL_TIMEOUT if timeout is None else timeout
|
||||
self.ssl_keyfile = (
|
||||
settings.EMAIL_SSL_KEYFILE if ssl_keyfile is None else ssl_keyfile
|
||||
)
|
||||
self.ssl_certfile = (
|
||||
settings.EMAIL_SSL_CERTFILE if ssl_certfile is None else ssl_certfile
|
||||
)
|
||||
if self.use_ssl and self.use_tls:
|
||||
raise ValueError(
|
||||
"EMAIL_USE_TLS/EMAIL_USE_SSL are mutually exclusive, so only set "
|
||||
"one of those settings to True."
|
||||
)
|
||||
self.connection = None
|
||||
self._lock = threading.RLock()
|
||||
|
||||
@property
|
||||
def connection_class(self):
|
||||
return smtplib.SMTP_SSL if self.use_ssl else smtplib.SMTP
|
||||
|
||||
@cached_property
|
||||
def ssl_context(self):
|
||||
if self.ssl_certfile or self.ssl_keyfile:
|
||||
ssl_context = ssl.SSLContext(protocol=ssl.PROTOCOL_TLS_CLIENT)
|
||||
ssl_context.load_cert_chain(self.ssl_certfile, self.ssl_keyfile)
|
||||
return ssl_context
|
||||
else:
|
||||
return ssl.create_default_context()
|
||||
|
||||
def open(self):
|
||||
"""
|
||||
Ensure an open connection to the email server. Return whether or not a
|
||||
new connection was required (True or False) or None if an exception
|
||||
passed silently.
|
||||
"""
|
||||
if self.connection:
|
||||
# Nothing to do if the connection is already open.
|
||||
return False
|
||||
|
||||
# If local_hostname is not specified, socket.getfqdn() gets used.
|
||||
# For performance, we use the cached FQDN for local_hostname.
|
||||
connection_params = {"local_hostname": DNS_NAME.get_fqdn()}
|
||||
if self.timeout is not None:
|
||||
connection_params["timeout"] = self.timeout
|
||||
if self.use_ssl:
|
||||
connection_params["context"] = self.ssl_context
|
||||
try:
|
||||
self.connection = self.connection_class(
|
||||
self.host, self.port, **connection_params
|
||||
)
|
||||
|
||||
# TLS/SSL are mutually exclusive, so only attempt TLS over
|
||||
# non-secure connections.
|
||||
if not self.use_ssl and self.use_tls:
|
||||
self.connection.starttls(context=self.ssl_context)
|
||||
if self.username and self.password:
|
||||
self.connection.login(self.username, self.password)
|
||||
return True
|
||||
except OSError:
|
||||
if not self.fail_silently:
|
||||
raise
|
||||
|
||||
def close(self):
|
||||
"""Close the connection to the email server."""
|
||||
if self.connection is None:
|
||||
return
|
||||
try:
|
||||
try:
|
||||
self.connection.quit()
|
||||
except (ssl.SSLError, smtplib.SMTPServerDisconnected):
|
||||
# This happens when calling quit() on a TLS connection
|
||||
# sometimes, or when the connection was already disconnected
|
||||
# by the server.
|
||||
self.connection.close()
|
||||
except smtplib.SMTPException:
|
||||
if self.fail_silently:
|
||||
return
|
||||
raise
|
||||
finally:
|
||||
self.connection = None
|
||||
|
||||
def send_messages(self, email_messages):
|
||||
"""
|
||||
Send one or more EmailMessage objects and return the number of email
|
||||
messages sent.
|
||||
"""
|
||||
if not email_messages:
|
||||
return 0
|
||||
with self._lock:
|
||||
new_conn_created = self.open()
|
||||
if not self.connection or new_conn_created is None:
|
||||
# We failed silently on open().
|
||||
# Trying to send would be pointless.
|
||||
return 0
|
||||
num_sent = 0
|
||||
try:
|
||||
for message in email_messages:
|
||||
sent = self._send(message)
|
||||
if sent:
|
||||
num_sent += 1
|
||||
finally:
|
||||
if new_conn_created:
|
||||
self.close()
|
||||
return num_sent
|
||||
|
||||
def _send(self, email_message):
|
||||
"""A helper method that does the actual sending."""
|
||||
if not email_message.recipients():
|
||||
return False
|
||||
encoding = email_message.encoding or settings.DEFAULT_CHARSET
|
||||
from_email = sanitize_address(email_message.from_email, encoding)
|
||||
recipients = [
|
||||
sanitize_address(addr, encoding) for addr in email_message.recipients()
|
||||
]
|
||||
message = email_message.message()
|
||||
try:
|
||||
self.connection.sendmail(
|
||||
from_email, recipients, message.as_bytes(linesep="\r\n")
|
||||
)
|
||||
except smtplib.SMTPException:
|
||||
if not self.fail_silently:
|
||||
raise
|
||||
return False
|
||||
return True
|
||||
520
.venv/Lib/site-packages/django/core/mail/message.py
Normal file
520
.venv/Lib/site-packages/django/core/mail/message.py
Normal file
@@ -0,0 +1,520 @@
|
||||
import mimetypes
|
||||
from collections import namedtuple
|
||||
from email import charset as Charset
|
||||
from email import encoders as Encoders
|
||||
from email import generator, message_from_bytes
|
||||
from email.errors import HeaderParseError
|
||||
from email.header import Header
|
||||
from email.headerregistry import Address, parser
|
||||
from email.message import Message
|
||||
from email.mime.base import MIMEBase
|
||||
from email.mime.message import MIMEMessage
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
from email.utils import formataddr, formatdate, getaddresses, make_msgid
|
||||
from io import BytesIO, StringIO
|
||||
from pathlib import Path
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.mail.utils import DNS_NAME
|
||||
from django.utils.encoding import force_bytes, force_str, punycode
|
||||
|
||||
# Don't BASE64-encode UTF-8 messages so that we avoid unwanted attention from
|
||||
# some spam filters.
|
||||
utf8_charset = Charset.Charset("utf-8")
|
||||
utf8_charset.body_encoding = None # Python defaults to BASE64
|
||||
utf8_charset_qp = Charset.Charset("utf-8")
|
||||
utf8_charset_qp.body_encoding = Charset.QP
|
||||
|
||||
# Default MIME type to use on attachments (if it is not explicitly given
|
||||
# and cannot be guessed).
|
||||
DEFAULT_ATTACHMENT_MIME_TYPE = "application/octet-stream"
|
||||
|
||||
RFC5322_EMAIL_LINE_LENGTH_LIMIT = 998
|
||||
|
||||
|
||||
class BadHeaderError(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
# Header names that contain structured address data (RFC 5322).
|
||||
ADDRESS_HEADERS = {
|
||||
"from",
|
||||
"sender",
|
||||
"reply-to",
|
||||
"to",
|
||||
"cc",
|
||||
"bcc",
|
||||
"resent-from",
|
||||
"resent-sender",
|
||||
"resent-to",
|
||||
"resent-cc",
|
||||
"resent-bcc",
|
||||
}
|
||||
|
||||
|
||||
def forbid_multi_line_headers(name, val, encoding):
|
||||
"""Forbid multi-line headers to prevent header injection."""
|
||||
encoding = encoding or settings.DEFAULT_CHARSET
|
||||
val = str(val) # val may be lazy
|
||||
if "\n" in val or "\r" in val:
|
||||
raise BadHeaderError(
|
||||
"Header values can't contain newlines (got %r for header %r)" % (val, name)
|
||||
)
|
||||
try:
|
||||
val.encode("ascii")
|
||||
except UnicodeEncodeError:
|
||||
if name.lower() in ADDRESS_HEADERS:
|
||||
val = ", ".join(
|
||||
sanitize_address(addr, encoding) for addr in getaddresses((val,))
|
||||
)
|
||||
else:
|
||||
val = Header(val, encoding).encode()
|
||||
else:
|
||||
if name.lower() == "subject":
|
||||
val = Header(val).encode()
|
||||
return name, val
|
||||
|
||||
|
||||
def sanitize_address(addr, encoding):
|
||||
"""
|
||||
Format a pair of (name, address) or an email address string.
|
||||
"""
|
||||
address = None
|
||||
if not isinstance(addr, tuple):
|
||||
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(
|
||||
'Invalid address; only %s could be parsed from "%s"' % (token, addr)
|
||||
)
|
||||
nm = token.display_name or ""
|
||||
localpart = token.local_part
|
||||
domain = token.domain or ""
|
||||
else:
|
||||
nm, address = addr
|
||||
if "@" not in address:
|
||||
raise ValueError(f'Invalid address "{address}"')
|
||||
localpart, domain = address.rsplit("@", 1)
|
||||
|
||||
address_parts = nm + localpart + domain
|
||||
if "\n" in address_parts or "\r" in address_parts:
|
||||
raise ValueError("Invalid address; address parts cannot contain newlines.")
|
||||
|
||||
# Avoid UTF-8 encode, if it's possible.
|
||||
try:
|
||||
nm.encode("ascii")
|
||||
nm = Header(nm).encode()
|
||||
except UnicodeEncodeError:
|
||||
nm = Header(nm, encoding).encode()
|
||||
try:
|
||||
localpart.encode("ascii")
|
||||
except UnicodeEncodeError:
|
||||
localpart = Header(localpart, encoding).encode()
|
||||
domain = punycode(domain)
|
||||
|
||||
parsed_address = Address(username=localpart, domain=domain)
|
||||
return formataddr((nm, parsed_address.addr_spec))
|
||||
|
||||
|
||||
class MIMEMixin:
|
||||
def as_string(self, unixfrom=False, linesep="\n"):
|
||||
"""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.
|
||||
"""
|
||||
fp = StringIO()
|
||||
g = generator.Generator(fp, mangle_from_=False)
|
||||
g.flatten(self, unixfrom=unixfrom, linesep=linesep)
|
||||
return fp.getvalue()
|
||||
|
||||
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.
|
||||
|
||||
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()
|
||||
|
||||
|
||||
class SafeMIMEMessage(MIMEMixin, MIMEMessage):
|
||||
def __setitem__(self, name, val):
|
||||
# Per RFC 2046 Section 5.2.1, message/rfc822 attachment headers must be ASCII.
|
||||
name, val = forbid_multi_line_headers(name, val, "ascii")
|
||||
MIMEMessage.__setitem__(self, name, val)
|
||||
|
||||
|
||||
class SafeMIMEText(MIMEMixin, MIMEText):
|
||||
def __init__(self, _text, _subtype="plain", _charset=None):
|
||||
self.encoding = _charset
|
||||
MIMEText.__init__(self, _text, _subtype=_subtype, _charset=_charset)
|
||||
|
||||
def __setitem__(self, name, val):
|
||||
name, val = forbid_multi_line_headers(name, val, self.encoding)
|
||||
MIMEText.__setitem__(self, name, val)
|
||||
|
||||
def set_payload(self, payload, charset=None):
|
||||
if charset == "utf-8" and not isinstance(charset, Charset.Charset):
|
||||
has_long_lines = any(
|
||||
len(line.encode(errors="surrogateescape"))
|
||||
> RFC5322_EMAIL_LINE_LENGTH_LIMIT
|
||||
for line in payload.splitlines()
|
||||
)
|
||||
# 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)
|
||||
|
||||
|
||||
class SafeMIMEMultipart(MIMEMixin, MIMEMultipart):
|
||||
def __init__(
|
||||
self, _subtype="mixed", boundary=None, _subparts=None, encoding=None, **_params
|
||||
):
|
||||
self.encoding = encoding
|
||||
MIMEMultipart.__init__(self, _subtype, boundary, _subparts, **_params)
|
||||
|
||||
def __setitem__(self, name, val):
|
||||
name, val = forbid_multi_line_headers(name, val, self.encoding)
|
||||
MIMEMultipart.__setitem__(self, name, val)
|
||||
|
||||
|
||||
EmailAlternative = namedtuple("EmailAlternative", ["content", "mimetype"])
|
||||
EmailAttachment = namedtuple("EmailAttachment", ["filename", "content", "mimetype"])
|
||||
|
||||
|
||||
class EmailMessage:
|
||||
"""A container for email information."""
|
||||
|
||||
content_subtype = "plain"
|
||||
mixed_subtype = "mixed"
|
||||
encoding = None # None => use settings default
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
subject="",
|
||||
body="",
|
||||
from_email=None,
|
||||
to=None,
|
||||
bcc=None,
|
||||
connection=None,
|
||||
attachments=None,
|
||||
headers=None,
|
||||
cc=None,
|
||||
reply_to=None,
|
||||
):
|
||||
"""
|
||||
Initialize a single email message (which can be sent to multiple
|
||||
recipients).
|
||||
"""
|
||||
if to:
|
||||
if isinstance(to, str):
|
||||
raise TypeError('"to" argument must be a list or tuple')
|
||||
self.to = list(to)
|
||||
else:
|
||||
self.to = []
|
||||
if cc:
|
||||
if isinstance(cc, str):
|
||||
raise TypeError('"cc" argument must be a list or tuple')
|
||||
self.cc = list(cc)
|
||||
else:
|
||||
self.cc = []
|
||||
if bcc:
|
||||
if isinstance(bcc, str):
|
||||
raise TypeError('"bcc" argument must be a list or tuple')
|
||||
self.bcc = list(bcc)
|
||||
else:
|
||||
self.bcc = []
|
||||
if reply_to:
|
||||
if isinstance(reply_to, str):
|
||||
raise TypeError('"reply_to" argument must be a list or tuple')
|
||||
self.reply_to = list(reply_to)
|
||||
else:
|
||||
self.reply_to = []
|
||||
self.from_email = from_email or settings.DEFAULT_FROM_EMAIL
|
||||
self.subject = subject
|
||||
self.body = body or ""
|
||||
self.attachments = []
|
||||
if attachments:
|
||||
for attachment in attachments:
|
||||
if isinstance(attachment, MIMEBase):
|
||||
self.attach(attachment)
|
||||
else:
|
||||
self.attach(*attachment)
|
||||
self.extra_headers = headers or {}
|
||||
self.connection = connection
|
||||
|
||||
def get_connection(self, fail_silently=False):
|
||||
from django.core.mail import get_connection
|
||||
|
||||
if not self.connection:
|
||||
self.connection = get_connection(fail_silently=fail_silently)
|
||||
return self.connection
|
||||
|
||||
def message(self):
|
||||
encoding = self.encoding or settings.DEFAULT_CHARSET
|
||||
msg = SafeMIMEText(self.body, self.content_subtype, encoding)
|
||||
msg = self._create_message(msg)
|
||||
msg["Subject"] = self.subject
|
||||
msg["From"] = self.extra_headers.get("From", self.from_email)
|
||||
self._set_list_header_if_not_empty(msg, "To", self.to)
|
||||
self._set_list_header_if_not_empty(msg, "Cc", self.cc)
|
||||
self._set_list_header_if_not_empty(msg, "Reply-To", self.reply_to)
|
||||
|
||||
# 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:
|
||||
# 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)
|
||||
if "message-id" not in header_names:
|
||||
# Use cached DNS_NAME for performance
|
||||
msg["Message-ID"] = make_msgid(domain=DNS_NAME)
|
||||
for name, value in self.extra_headers.items():
|
||||
# Avoid headers handled above.
|
||||
if name.lower() not in {"from", "to", "cc", "reply-to"}:
|
||||
msg[name] = value
|
||||
return msg
|
||||
|
||||
def recipients(self):
|
||||
"""
|
||||
Return a list of all recipients of the email (includes direct
|
||||
addressees as well as Cc and Bcc entries).
|
||||
"""
|
||||
return [email for email in (self.to + self.cc + self.bcc) if email]
|
||||
|
||||
def send(self, fail_silently=False):
|
||||
"""Send the email message."""
|
||||
if not self.recipients():
|
||||
# Don't bother creating the network connection if there's nobody to
|
||||
# send to.
|
||||
return 0
|
||||
return self.get_connection(fail_silently).send_messages([self])
|
||||
|
||||
def attach(self, filename=None, content=None, mimetype=None):
|
||||
"""
|
||||
Attach a file with the given filename and content. The filename can
|
||||
be omitted and the mimetype is guessed, if not provided.
|
||||
|
||||
If the first parameter is a MIMEBase subclass, insert it directly
|
||||
into the resulting message attachments.
|
||||
|
||||
For a text/* mimetype (guessed or specified), when a bytes object is
|
||||
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.
|
||||
"""
|
||||
if isinstance(filename, MIMEBase):
|
||||
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."
|
||||
)
|
||||
self.attachments.append(filename)
|
||||
elif content is None:
|
||||
raise ValueError("content must be provided.")
|
||||
else:
|
||||
mimetype = (
|
||||
mimetype
|
||||
or mimetypes.guess_type(filename)[0]
|
||||
or DEFAULT_ATTACHMENT_MIME_TYPE
|
||||
)
|
||||
basetype, subtype = mimetype.split("/", 1)
|
||||
|
||||
if basetype == "text":
|
||||
if isinstance(content, bytes):
|
||||
try:
|
||||
content = content.decode()
|
||||
except UnicodeDecodeError:
|
||||
# If mimetype suggests the file is text but it's
|
||||
# actually binary, read() raises a UnicodeDecodeError.
|
||||
mimetype = DEFAULT_ATTACHMENT_MIME_TYPE
|
||||
|
||||
self.attachments.append(EmailAttachment(filename, content, mimetype))
|
||||
|
||||
def attach_file(self, path, mimetype=None):
|
||||
"""
|
||||
Attach a file from the filesystem.
|
||||
|
||||
Set the mimetype to DEFAULT_ATTACHMENT_MIME_TYPE if it isn't specified
|
||||
and cannot be guessed.
|
||||
|
||||
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.
|
||||
"""
|
||||
path = Path(path)
|
||||
with path.open("rb") as file:
|
||||
content = file.read()
|
||||
self.attach(path.name, content, mimetype)
|
||||
|
||||
def _create_message(self, msg):
|
||||
return self._create_attachments(msg)
|
||||
|
||||
def _create_attachments(self, msg):
|
||||
if self.attachments:
|
||||
encoding = self.encoding or settings.DEFAULT_CHARSET
|
||||
body_msg = msg
|
||||
msg = SafeMIMEMultipart(_subtype=self.mixed_subtype, encoding=encoding)
|
||||
if self.body or body_msg.is_multipart():
|
||||
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):
|
||||
"""
|
||||
Convert the content, mimetype pair into a MIME attachment object.
|
||||
|
||||
If the mimetype is message/rfc822, content may be an
|
||||
email.Message or EmailMessage object, as well as a str.
|
||||
"""
|
||||
basetype, subtype = mimetype.split("/", 1)
|
||||
if basetype == "text":
|
||||
encoding = self.encoding or settings.DEFAULT_CHARSET
|
||||
attachment = SafeMIMEText(content, subtype, encoding)
|
||||
elif basetype == "message" and subtype == "rfc822":
|
||||
# Bug #18967: Per RFC 2046 Section 5.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
|
||||
# into an email.Message object if it is not one already.
|
||||
content = message_from_bytes(force_bytes(content))
|
||||
|
||||
attachment = SafeMIMEMessage(content, subtype)
|
||||
else:
|
||||
# Encode non-text attachments with base64.
|
||||
attachment = MIMEBase(basetype, subtype)
|
||||
attachment.set_payload(content)
|
||||
Encoders.encode_base64(attachment)
|
||||
return attachment
|
||||
|
||||
def _create_attachment(self, filename, content, mimetype=None):
|
||||
"""
|
||||
Convert the filename, content, mimetype triple into a MIME attachment
|
||||
object.
|
||||
"""
|
||||
attachment = self._create_mime_attachment(content, mimetype)
|
||||
if filename:
|
||||
try:
|
||||
filename.encode("ascii")
|
||||
except UnicodeEncodeError:
|
||||
filename = ("utf-8", "", filename)
|
||||
attachment.add_header(
|
||||
"Content-Disposition", "attachment", filename=filename
|
||||
)
|
||||
return attachment
|
||||
|
||||
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 not empty.
|
||||
"""
|
||||
try:
|
||||
msg[header] = self.extra_headers[header]
|
||||
except KeyError:
|
||||
if values:
|
||||
msg[header] = ", ".join(str(v) for v in values)
|
||||
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
alternative_subtype = "alternative"
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
subject="",
|
||||
body="",
|
||||
from_email=None,
|
||||
to=None,
|
||||
bcc=None,
|
||||
connection=None,
|
||||
attachments=None,
|
||||
headers=None,
|
||||
alternatives=None,
|
||||
cc=None,
|
||||
reply_to=None,
|
||||
):
|
||||
"""
|
||||
Initialize a single email message (which can be sent to multiple
|
||||
recipients).
|
||||
"""
|
||||
super().__init__(
|
||||
subject,
|
||||
body,
|
||||
from_email,
|
||||
to,
|
||||
bcc,
|
||||
connection,
|
||||
attachments,
|
||||
headers,
|
||||
cc,
|
||||
reply_to,
|
||||
)
|
||||
self.alternatives = [
|
||||
EmailAlternative(*alternative) for alternative in (alternatives or [])
|
||||
]
|
||||
|
||||
def attach_alternative(self, content, mimetype):
|
||||
"""Attach an alternative content representation."""
|
||||
if content is None or mimetype is None:
|
||||
raise ValueError("Both content and mimetype must be provided.")
|
||||
self.alternatives.append(EmailAlternative(content, mimetype))
|
||||
|
||||
def _create_message(self, msg):
|
||||
return self._create_attachments(self._create_alternatives(msg))
|
||||
|
||||
def _create_alternatives(self, msg):
|
||||
encoding = self.encoding or settings.DEFAULT_CHARSET
|
||||
if self.alternatives:
|
||||
body_msg = msg
|
||||
msg = SafeMIMEMultipart(
|
||||
_subtype=self.alternative_subtype, encoding=encoding
|
||||
)
|
||||
if self.body:
|
||||
msg.attach(body_msg)
|
||||
for alternative in self.alternatives:
|
||||
msg.attach(
|
||||
self._create_mime_attachment(
|
||||
alternative.content, alternative.mimetype
|
||||
)
|
||||
)
|
||||
return msg
|
||||
|
||||
def body_contains(self, text):
|
||||
"""
|
||||
Checks that ``text`` occurs in the email body and in all attached MIME
|
||||
type text/* alternatives.
|
||||
"""
|
||||
if text not in self.body:
|
||||
return False
|
||||
|
||||
for content, mimetype in self.alternatives:
|
||||
if mimetype.startswith("text/") and text not in content:
|
||||
return False
|
||||
return True
|
||||
22
.venv/Lib/site-packages/django/core/mail/utils.py
Normal file
22
.venv/Lib/site-packages/django/core/mail/utils.py
Normal file
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Email message and email sending related helper functions.
|
||||
"""
|
||||
|
||||
import socket
|
||||
|
||||
from django.utils.encoding import punycode
|
||||
|
||||
|
||||
# Cache the hostname, but do it lazily: socket.getfqdn() can take a couple of
|
||||
# seconds, which slows down the restart of the server.
|
||||
class CachedDnsName:
|
||||
def __str__(self):
|
||||
return self.get_fqdn()
|
||||
|
||||
def get_fqdn(self):
|
||||
if not hasattr(self, "_fqdn"):
|
||||
self._fqdn = punycode(socket.getfqdn())
|
||||
return self._fqdn
|
||||
|
||||
|
||||
DNS_NAME = CachedDnsName()
|
||||
Reference in New Issue
Block a user