update to python fastpi
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
aiosmtplib
|
||||
==========
|
||||
|
||||
An asyncio SMTP client.
|
||||
|
||||
Originally based on smtplib from the Python 3 standard library by:
|
||||
The Dragon De Monsyne <dragondm@integral.org>
|
||||
|
||||
Author: Cole Maclean <hi@colemaclean.dev>
|
||||
"""
|
||||
from .api import send
|
||||
from .errors import (
|
||||
SMTPAuthenticationError,
|
||||
SMTPConnectError,
|
||||
SMTPConnectTimeoutError,
|
||||
SMTPDataError,
|
||||
SMTPException,
|
||||
SMTPHeloError,
|
||||
SMTPNotSupported,
|
||||
SMTPReadTimeoutError,
|
||||
SMTPRecipientRefused,
|
||||
SMTPRecipientsRefused,
|
||||
SMTPResponseException,
|
||||
SMTPSenderRefused,
|
||||
SMTPServerDisconnected,
|
||||
SMTPTimeoutError,
|
||||
SMTPConnectResponseError,
|
||||
)
|
||||
from .response import SMTPResponse
|
||||
from .smtp import SMTP
|
||||
from .typing import SMTPStatus
|
||||
|
||||
|
||||
__title__ = "aiosmtplib"
|
||||
__version__ = "3.0.1"
|
||||
__author__ = "Cole Maclean"
|
||||
__license__ = "MIT"
|
||||
__copyright__ = "Copyright 2022 Cole Maclean"
|
||||
__all__ = (
|
||||
"send",
|
||||
"SMTP",
|
||||
"SMTPResponse",
|
||||
"SMTPStatus",
|
||||
"SMTPAuthenticationError",
|
||||
"SMTPConnectError",
|
||||
"SMTPDataError",
|
||||
"SMTPException",
|
||||
"SMTPHeloError",
|
||||
"SMTPNotSupported",
|
||||
"SMTPRecipientRefused",
|
||||
"SMTPRecipientsRefused",
|
||||
"SMTPResponseException",
|
||||
"SMTPSenderRefused",
|
||||
"SMTPServerDisconnected",
|
||||
"SMTPTimeoutError",
|
||||
"SMTPConnectTimeoutError",
|
||||
"SMTPReadTimeoutError",
|
||||
"SMTPConnectResponseError",
|
||||
)
|
||||
@@ -0,0 +1,30 @@
|
||||
from aiosmtplib.smtp import SMTP, SMTP_PORT
|
||||
|
||||
|
||||
raw_hostname = input("SMTP server hostname [localhost]: ") # nosec
|
||||
raw_port = input(f"SMTP server port [{SMTP_PORT}]: ") # nosec
|
||||
raw_sender = input("From: ") # nosec
|
||||
raw_recipients = input("To: ") # nosec
|
||||
|
||||
hostname = raw_hostname or "localhost"
|
||||
port = int(raw_port) if raw_port else SMTP_PORT
|
||||
recipients = raw_recipients.split(",")
|
||||
lines = []
|
||||
|
||||
print("Enter message, end with ^D:")
|
||||
while True:
|
||||
try:
|
||||
lines.append(input()) # nosec
|
||||
except EOFError:
|
||||
break
|
||||
|
||||
message = "\n".join(lines)
|
||||
message_len = len(message.encode("utf-8"))
|
||||
print(f"Message length (bytes): {message_len}")
|
||||
|
||||
smtp_client = SMTP(hostname=hostname or "localhost", port=port, start_tls=False)
|
||||
sendmail_errors, sendmail_response = smtp_client.sendmail_sync(
|
||||
raw_sender, recipients, message
|
||||
)
|
||||
|
||||
print(f"Server response: {sendmail_response}")
|
||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
139
Backend/venv/lib/python3.12/site-packages/aiosmtplib/api.py
Normal file
139
Backend/venv/lib/python3.12/site-packages/aiosmtplib/api.py
Normal file
@@ -0,0 +1,139 @@
|
||||
"""
|
||||
Main public API.
|
||||
"""
|
||||
import email.message
|
||||
import socket
|
||||
import ssl
|
||||
from typing import Dict, Optional, Sequence, Tuple, Union, cast
|
||||
|
||||
from .response import SMTPResponse
|
||||
from .smtp import DEFAULT_TIMEOUT, SMTP
|
||||
from .typing import SocketPathType
|
||||
|
||||
|
||||
__all__ = ("send",)
|
||||
|
||||
|
||||
async def send(
|
||||
message: Union[email.message.EmailMessage, email.message.Message, str, bytes],
|
||||
/,
|
||||
*,
|
||||
sender: Optional[str] = None,
|
||||
recipients: Optional[Union[str, Sequence[str]]] = None,
|
||||
mail_options: Optional[Sequence[str]] = None,
|
||||
rcpt_options: Optional[Sequence[str]] = None,
|
||||
hostname: Optional[str] = "localhost",
|
||||
port: Optional[int] = None,
|
||||
username: Optional[Union[str, bytes]] = None,
|
||||
password: Optional[Union[str, bytes]] = None,
|
||||
local_hostname: Optional[str] = None,
|
||||
source_address: Optional[Tuple[str, int]] = None,
|
||||
timeout: Optional[float] = DEFAULT_TIMEOUT,
|
||||
use_tls: bool = False,
|
||||
start_tls: Optional[bool] = None,
|
||||
validate_certs: bool = True,
|
||||
client_cert: Optional[str] = None,
|
||||
client_key: Optional[str] = None,
|
||||
tls_context: Optional[ssl.SSLContext] = None,
|
||||
cert_bundle: Optional[str] = None,
|
||||
socket_path: Optional[SocketPathType] = None,
|
||||
sock: Optional[socket.socket] = None,
|
||||
) -> Tuple[Dict[str, SMTPResponse], str]:
|
||||
"""
|
||||
Send an email message. On await, connects to the SMTP server using the details
|
||||
provided, sends the message, then disconnects.
|
||||
|
||||
:param message: Message text. Either an :py:class:`email.message.EmailMessage`
|
||||
object, ``str`` or ``bytes``. If an :py:class:`email.message.EmailMessage`
|
||||
object is provided, sender and recipients set in the message headers will be
|
||||
used, unless overridden by the respective keyword arguments.
|
||||
:keyword sender: From email address. Not required if an
|
||||
:py:class:`email.message.EmailMessage` object is provided for the `message`
|
||||
argument.
|
||||
:keyword recipients: Recipient email addresses. Not required if an
|
||||
:py:class:`email.message.EmailMessage` object is provided for the `message`
|
||||
argument.
|
||||
:keyword hostname: Server name (or IP) to connect to. Defaults to "localhost".
|
||||
:keyword port: Server port. Defaults ``465`` if ``use_tls`` is ``True``,
|
||||
``587`` if ``start_tls`` is ``True``, or ``25`` otherwise.
|
||||
:keyword username: Username to login as after connect.
|
||||
:keyword password: Password for login after connect.
|
||||
:keyword local_hostname: The hostname of the client. If specified, used as the
|
||||
FQDN of the local host in the HELO/EHLO command. Otherwise, the result of
|
||||
:func:`socket.getfqdn`. **Note that getfqdn will block the event loop.**
|
||||
:keyword source_address: Takes a 2-tuple (host, port) for the socket to bind to
|
||||
as its source address before connecting. If the host is '' and port is 0,
|
||||
the OS default behavior will be used.
|
||||
:keyword timeout: Default timeout value for the connection, in seconds.
|
||||
Defaults to 60.
|
||||
:keyword use_tls: If True, make the initial connection to the server
|
||||
over TLS/SSL. Mutually exclusive with ``start_tls``; if the server uses
|
||||
STARTTLS, ``use_tls`` should be ``False``.
|
||||
:keyword start_tls: Flag to initiate a STARTTLS upgrade on connect.
|
||||
If ``None`` (the default), upgrade will be initiated if supported by the
|
||||
server.
|
||||
If ``True``, and upgrade will be initiated regardless of server support.
|
||||
If ``False``, no upgrade will occur.
|
||||
Mutually exclusive with ``use_tls``.
|
||||
:keyword validate_certs: Determines if server certificates are
|
||||
validated. Defaults to ``True``.
|
||||
:keyword client_cert: Path to client side certificate, for TLS.
|
||||
:keyword client_key: Path to client side key, for TLS.
|
||||
:keyword tls_context: An existing :py:class:`ssl.SSLContext`, for TLS.
|
||||
Mutually exclusive with ``client_cert``/``client_key``.
|
||||
:keyword cert_bundle: Path to certificate bundle, for TLS verification.
|
||||
:keyword socket_path: Path to a Unix domain socket. Not compatible with
|
||||
hostname or port. Accepts str, bytes, or a pathlike object.
|
||||
:keyword sock: An existing, connected socket object. If given, none of
|
||||
hostname, port, or socket_path should be provided.
|
||||
|
||||
:raises ValueError: required arguments missing or mutually exclusive options
|
||||
provided
|
||||
"""
|
||||
if not isinstance(message, (email.message.EmailMessage, email.message.Message)):
|
||||
if not recipients:
|
||||
raise ValueError("Recipients must be provided with raw messages.")
|
||||
if not sender:
|
||||
raise ValueError("Sender must be provided with raw messages.")
|
||||
|
||||
sender = cast(str, sender)
|
||||
recipients = cast(Union[str, Sequence[str]], recipients)
|
||||
|
||||
client = SMTP(
|
||||
hostname=hostname,
|
||||
port=port,
|
||||
local_hostname=local_hostname,
|
||||
source_address=source_address,
|
||||
timeout=timeout,
|
||||
use_tls=use_tls,
|
||||
start_tls=start_tls,
|
||||
validate_certs=validate_certs,
|
||||
client_cert=client_cert,
|
||||
client_key=client_key,
|
||||
tls_context=tls_context,
|
||||
cert_bundle=cert_bundle,
|
||||
socket_path=socket_path,
|
||||
sock=sock,
|
||||
username=username,
|
||||
password=password,
|
||||
)
|
||||
|
||||
async with client:
|
||||
if isinstance(message, (email.message.EmailMessage, email.message.Message)):
|
||||
result = await client.send_message(
|
||||
message,
|
||||
sender=sender,
|
||||
recipients=recipients,
|
||||
mail_options=mail_options,
|
||||
rcpt_options=rcpt_options,
|
||||
)
|
||||
else:
|
||||
result = await client.sendmail(
|
||||
sender,
|
||||
recipients,
|
||||
message,
|
||||
mail_options=mail_options,
|
||||
rcpt_options=rcpt_options,
|
||||
)
|
||||
|
||||
return result
|
||||
72
Backend/venv/lib/python3.12/site-packages/aiosmtplib/auth.py
Normal file
72
Backend/venv/lib/python3.12/site-packages/aiosmtplib/auth.py
Normal file
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
Authentication related methods.
|
||||
"""
|
||||
import base64
|
||||
import hmac
|
||||
from typing import Tuple, Union
|
||||
|
||||
|
||||
__all__ = ("auth_crammd5_verify", "auth_plain_encode", "auth_login_encode")
|
||||
|
||||
|
||||
def _ensure_bytes(value: Union[str, bytes]) -> bytes:
|
||||
if isinstance(value, bytes):
|
||||
return value
|
||||
|
||||
return value.encode("utf-8")
|
||||
|
||||
|
||||
def auth_crammd5_verify(
|
||||
username: Union[str, bytes],
|
||||
password: Union[str, bytes],
|
||||
challenge: Union[str, bytes],
|
||||
/,
|
||||
) -> bytes:
|
||||
"""
|
||||
CRAM-MD5 auth uses the password as a shared secret to MD5 the server's
|
||||
response, and sends the username combined with that (base64 encoded).
|
||||
"""
|
||||
username_bytes = _ensure_bytes(username)
|
||||
password_bytes = _ensure_bytes(password)
|
||||
decoded_challenge = base64.b64decode(challenge)
|
||||
|
||||
md5_digest = hmac.new(password_bytes, msg=decoded_challenge, digestmod="md5")
|
||||
verification = username_bytes + b" " + md5_digest.hexdigest().encode("ascii")
|
||||
encoded_verification = base64.b64encode(verification)
|
||||
|
||||
return encoded_verification
|
||||
|
||||
|
||||
def auth_plain_encode(
|
||||
username: Union[str, bytes],
|
||||
password: Union[str, bytes],
|
||||
/,
|
||||
) -> bytes:
|
||||
"""
|
||||
PLAIN auth base64 encodes the username and password together.
|
||||
"""
|
||||
username_bytes = _ensure_bytes(username)
|
||||
password_bytes = _ensure_bytes(password)
|
||||
|
||||
username_and_password = b"\0" + username_bytes + b"\0" + password_bytes
|
||||
encoded = base64.b64encode(username_and_password)
|
||||
|
||||
return encoded
|
||||
|
||||
|
||||
def auth_login_encode(
|
||||
username: Union[str, bytes],
|
||||
password: Union[str, bytes],
|
||||
/,
|
||||
) -> Tuple[bytes, bytes]:
|
||||
"""
|
||||
LOGIN auth base64 encodes the username and password and sends them
|
||||
in sequence.
|
||||
"""
|
||||
username_bytes = _ensure_bytes(username)
|
||||
password_bytes = _ensure_bytes(password)
|
||||
|
||||
encoded_username = base64.b64encode(username_bytes)
|
||||
encoded_password = base64.b64encode(password_bytes)
|
||||
|
||||
return encoded_username, encoded_password
|
||||
186
Backend/venv/lib/python3.12/site-packages/aiosmtplib/email.py
Normal file
186
Backend/venv/lib/python3.12/site-packages/aiosmtplib/email.py
Normal file
@@ -0,0 +1,186 @@
|
||||
"""
|
||||
Email message and address formatting/parsing functions.
|
||||
"""
|
||||
import copy
|
||||
import email.charset
|
||||
import email.generator
|
||||
import email.header
|
||||
import email.headerregistry
|
||||
import email.message
|
||||
import email.policy
|
||||
import email.utils
|
||||
import io
|
||||
import re
|
||||
from typing import List, Optional, Tuple, Union
|
||||
|
||||
|
||||
__all__ = (
|
||||
"extract_recipients",
|
||||
"extract_sender",
|
||||
"flatten_message",
|
||||
"parse_address",
|
||||
"quote_address",
|
||||
)
|
||||
|
||||
|
||||
LINE_SEP = "\r\n"
|
||||
SPECIALS_REGEX = re.compile(r'[][\\()<>@,:;".]')
|
||||
ESCAPES_REGEX = re.compile(r'[\\"]')
|
||||
UTF8_CHARSET = email.charset.Charset("utf-8")
|
||||
|
||||
|
||||
def parse_address(address: str) -> str:
|
||||
"""
|
||||
Parse an email address, falling back to the raw string given.
|
||||
"""
|
||||
display_name, parsed_address = email.utils.parseaddr(address)
|
||||
|
||||
return parsed_address or address.strip()
|
||||
|
||||
|
||||
def quote_address(address: str) -> str:
|
||||
"""
|
||||
Quote a subset of the email addresses defined by RFC 821.
|
||||
"""
|
||||
parsed_address = parse_address(address)
|
||||
return f"<{parsed_address}>"
|
||||
|
||||
|
||||
def formataddr(pair: Tuple[str, str]) -> str:
|
||||
"""
|
||||
Copied from the standard library, and modified to handle international (UTF-8)
|
||||
email addresses.
|
||||
|
||||
The inverse of parseaddr(), this takes a 2-tuple of the form
|
||||
(realname, email_address) and returns the string value suitable
|
||||
for an RFC 2822 From, To or Cc header.
|
||||
If the first element of pair is false, then the second element is
|
||||
returned unmodified.
|
||||
"""
|
||||
name, address = pair
|
||||
if name:
|
||||
encoded_name = UTF8_CHARSET.header_encode(name)
|
||||
return f"{encoded_name} <{address}>"
|
||||
else:
|
||||
quotes = ""
|
||||
if SPECIALS_REGEX.search(name):
|
||||
quotes = '"'
|
||||
name = ESCAPES_REGEX.sub(r"\\\g<0>", name)
|
||||
return f"{quotes}{name}{quotes} <{address}>"
|
||||
|
||||
return address
|
||||
|
||||
|
||||
def flatten_message(
|
||||
message: Union[email.message.EmailMessage, email.message.Message],
|
||||
/,
|
||||
*,
|
||||
utf8: bool = False,
|
||||
cte_type: str = "8bit",
|
||||
) -> bytes:
|
||||
# Make a local copy so we can delete the bcc headers.
|
||||
message_copy = copy.copy(message)
|
||||
del message_copy["Bcc"]
|
||||
del message_copy["Resent-Bcc"]
|
||||
|
||||
if isinstance(message, email.message.EmailMessage):
|
||||
# New message class, default policy
|
||||
policy = email.policy.default.clone(
|
||||
linesep=LINE_SEP,
|
||||
utf8=utf8,
|
||||
cte_type=cte_type,
|
||||
)
|
||||
else:
|
||||
# Old message class, Compat32 policy.
|
||||
# Compat32 cannot use UTF8
|
||||
policy = email.policy.compat32.clone(linesep=LINE_SEP, cte_type=cte_type)
|
||||
|
||||
with io.BytesIO() as messageio:
|
||||
generator = email.generator.BytesGenerator(messageio, policy=policy)
|
||||
generator.flatten(message_copy)
|
||||
flat_message = messageio.getvalue()
|
||||
|
||||
return flat_message
|
||||
|
||||
|
||||
def extract_addresses(
|
||||
header: Union[str, email.headerregistry.AddressHeader, email.header.Header],
|
||||
/,
|
||||
) -> List[str]:
|
||||
"""
|
||||
Convert address headers into raw email addresses, suitable for use in
|
||||
low level SMTP commands.
|
||||
"""
|
||||
addresses = []
|
||||
if isinstance(header, email.headerregistry.AddressHeader):
|
||||
for address in header.addresses:
|
||||
# If the object has been assigned an iterable, it's possible to get
|
||||
# a string here
|
||||
if isinstance(address, email.headerregistry.Address):
|
||||
addresses.append(address.addr_spec)
|
||||
else:
|
||||
addresses.append(parse_address(address))
|
||||
elif isinstance(header, email.header.Header):
|
||||
for address_bytes, charset in email.header.decode_header(header):
|
||||
if charset is None:
|
||||
charset = "ascii"
|
||||
addresses.append(parse_address(str(address_bytes, encoding=charset)))
|
||||
else:
|
||||
addresses.extend(addr for _, addr in email.utils.getaddresses([header]))
|
||||
|
||||
return addresses
|
||||
|
||||
|
||||
def extract_sender(
|
||||
message: Union[email.message.EmailMessage, email.message.Message],
|
||||
/,
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Extract the sender from the message object given.
|
||||
"""
|
||||
resent_dates = message.get_all("Resent-Date")
|
||||
|
||||
if resent_dates is not None and len(resent_dates) > 1:
|
||||
raise ValueError("Message has more than one 'Resent-' header block")
|
||||
elif resent_dates:
|
||||
sender_header_name = "Resent-Sender"
|
||||
from_header_name = "Resent-From"
|
||||
else:
|
||||
sender_header_name = "Sender"
|
||||
from_header_name = "From"
|
||||
|
||||
# Prefer the sender field per RFC 2822:3.6.2.
|
||||
if sender_header_name in message:
|
||||
sender_header = message[sender_header_name]
|
||||
else:
|
||||
sender_header = message[from_header_name]
|
||||
|
||||
if sender_header is None:
|
||||
return None
|
||||
|
||||
return extract_addresses(sender_header)[0]
|
||||
|
||||
|
||||
def extract_recipients(
|
||||
message: Union[email.message.EmailMessage, email.message.Message],
|
||||
/,
|
||||
) -> List[str]:
|
||||
"""
|
||||
Extract the recipients from the message object given.
|
||||
"""
|
||||
recipients: List[str] = []
|
||||
|
||||
resent_dates = message.get_all("Resent-Date")
|
||||
|
||||
if resent_dates is not None and len(resent_dates) > 1:
|
||||
raise ValueError("Message has more than one 'Resent-' header block")
|
||||
elif resent_dates:
|
||||
recipient_headers = ("Resent-To", "Resent-Cc", "Resent-Bcc")
|
||||
else:
|
||||
recipient_headers = ("To", "Cc", "Bcc")
|
||||
|
||||
for header in recipient_headers:
|
||||
for recipient in message.get_all(header, failobj=[]):
|
||||
recipients.extend(extract_addresses(recipient))
|
||||
|
||||
return recipients
|
||||
137
Backend/venv/lib/python3.12/site-packages/aiosmtplib/errors.py
Normal file
137
Backend/venv/lib/python3.12/site-packages/aiosmtplib/errors.py
Normal file
@@ -0,0 +1,137 @@
|
||||
from asyncio import TimeoutError
|
||||
from typing import List
|
||||
|
||||
|
||||
__all__ = (
|
||||
"SMTPAuthenticationError",
|
||||
"SMTPConnectError",
|
||||
"SMTPDataError",
|
||||
"SMTPException",
|
||||
"SMTPHeloError",
|
||||
"SMTPNotSupported",
|
||||
"SMTPRecipientRefused",
|
||||
"SMTPRecipientsRefused",
|
||||
"SMTPResponseException",
|
||||
"SMTPSenderRefused",
|
||||
"SMTPServerDisconnected",
|
||||
"SMTPTimeoutError",
|
||||
"SMTPConnectTimeoutError",
|
||||
"SMTPReadTimeoutError",
|
||||
"SMTPConnectResponseError",
|
||||
)
|
||||
|
||||
|
||||
class SMTPException(Exception):
|
||||
"""
|
||||
Base class for all SMTP exceptions.
|
||||
"""
|
||||
|
||||
def __init__(self, message: str, /) -> None:
|
||||
self.message = message
|
||||
self.args = (message,)
|
||||
|
||||
|
||||
class SMTPServerDisconnected(SMTPException, ConnectionError):
|
||||
"""
|
||||
The connection was lost unexpectedly, or a command was run that requires
|
||||
a connection.
|
||||
"""
|
||||
|
||||
|
||||
class SMTPConnectError(SMTPException, ConnectionError):
|
||||
"""
|
||||
An error occurred while connecting to the SMTP server.
|
||||
"""
|
||||
|
||||
|
||||
class SMTPTimeoutError(SMTPException, TimeoutError):
|
||||
"""
|
||||
A timeout occurred while performing a network operation.
|
||||
"""
|
||||
|
||||
|
||||
class SMTPConnectTimeoutError(SMTPTimeoutError, SMTPConnectError):
|
||||
"""
|
||||
A timeout occurred while connecting to the SMTP server.
|
||||
"""
|
||||
|
||||
|
||||
class SMTPReadTimeoutError(SMTPTimeoutError):
|
||||
"""
|
||||
A timeout occurred while waiting for a response from the SMTP server.
|
||||
"""
|
||||
|
||||
|
||||
class SMTPNotSupported(SMTPException):
|
||||
"""
|
||||
A command or argument sent to the SMTP server is not supported.
|
||||
"""
|
||||
|
||||
|
||||
class SMTPResponseException(SMTPException):
|
||||
"""
|
||||
Base class for all server responses with error codes.
|
||||
"""
|
||||
|
||||
def __init__(self, code: int, message: str, /) -> None:
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.args = (code, message)
|
||||
|
||||
|
||||
class SMTPConnectResponseError(SMTPResponseException, SMTPConnectError):
|
||||
"""
|
||||
The SMTP server returned an invalid response code after connecting.
|
||||
"""
|
||||
|
||||
|
||||
class SMTPHeloError(SMTPResponseException):
|
||||
"""
|
||||
Server refused HELO or EHLO.
|
||||
"""
|
||||
|
||||
|
||||
class SMTPDataError(SMTPResponseException):
|
||||
"""
|
||||
Server refused DATA content.
|
||||
"""
|
||||
|
||||
|
||||
class SMTPAuthenticationError(SMTPResponseException):
|
||||
"""
|
||||
Server refused our AUTH request; may be caused by invalid credentials.
|
||||
"""
|
||||
|
||||
|
||||
class SMTPSenderRefused(SMTPResponseException):
|
||||
"""
|
||||
SMTP server refused the message sender.
|
||||
"""
|
||||
|
||||
def __init__(self, code: int, message: str, sender: str, /) -> None:
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.sender = sender
|
||||
self.args = (code, message, sender)
|
||||
|
||||
|
||||
class SMTPRecipientRefused(SMTPResponseException):
|
||||
"""
|
||||
SMTP server refused a message recipient.
|
||||
"""
|
||||
|
||||
def __init__(self, code: int, message: str, recipient: str, /) -> None:
|
||||
self.code = code
|
||||
self.message = message
|
||||
self.recipient = recipient
|
||||
self.args = (code, message, recipient)
|
||||
|
||||
|
||||
class SMTPRecipientsRefused(SMTPException):
|
||||
"""
|
||||
SMTP server refused multiple recipients.
|
||||
"""
|
||||
|
||||
def __init__(self, recipients: List[SMTPRecipientRefused], /) -> None:
|
||||
self.recipients = recipients
|
||||
self.args = (recipients,)
|
||||
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
ESMTP utils
|
||||
"""
|
||||
import re
|
||||
from typing import Dict, List, Tuple
|
||||
|
||||
|
||||
__all__ = ("parse_esmtp_extensions",)
|
||||
|
||||
|
||||
OLDSTYLE_AUTH_REGEX = re.compile(r"auth=(?P<auth>.*)", flags=re.I)
|
||||
EXTENSIONS_REGEX = re.compile(r"(?P<ext>[A-Za-z0-9][A-Za-z0-9\-]*) ?")
|
||||
|
||||
|
||||
def parse_esmtp_extensions(message: str) -> Tuple[Dict[str, str], List[str]]:
|
||||
"""
|
||||
Parse an EHLO response from the server into a dict of {extension: params}
|
||||
and a list of auth method names.
|
||||
|
||||
It might look something like:
|
||||
|
||||
220 size.does.matter.af.MIL (More ESMTP than Crappysoft!)
|
||||
EHLO heaven.af.mil
|
||||
250-size.does.matter.af.MIL offers FIFTEEN extensions:
|
||||
250-8BITMIME
|
||||
250-PIPELINING
|
||||
250-DSN
|
||||
250-ENHANCEDSTATUSCODES
|
||||
250-EXPN
|
||||
250-HELP
|
||||
250-SAML
|
||||
250-SEND
|
||||
250-SOML
|
||||
250-TURN
|
||||
250-XADR
|
||||
250-XSTA
|
||||
250-ETRN
|
||||
250-XGEN
|
||||
250 SIZE 51200000
|
||||
"""
|
||||
esmtp_extensions = {}
|
||||
auth_types: List[str] = []
|
||||
|
||||
response_lines = message.split("\n")
|
||||
|
||||
# ignore the first line
|
||||
for line in response_lines[1:]:
|
||||
# To be able to communicate with as many SMTP servers as possible,
|
||||
# we have to take the old-style auth advertisement into account,
|
||||
# because:
|
||||
# 1) Else our SMTP feature parser gets confused.
|
||||
# 2) There are some servers that only advertise the auth methods we
|
||||
# support using the old style.
|
||||
auth_match = OLDSTYLE_AUTH_REGEX.match(line)
|
||||
if auth_match is not None:
|
||||
auth_type = auth_match.group("auth")
|
||||
auth_types.append(auth_type.lower().strip())
|
||||
|
||||
# RFC 1869 requires a space between ehlo keyword and parameters.
|
||||
# It's actually stricter, in that only spaces are allowed between
|
||||
# parameters, but were not going to check for that here. Note
|
||||
# that the space isn't present if there are no parameters.
|
||||
extensions = EXTENSIONS_REGEX.match(line)
|
||||
if extensions is not None:
|
||||
extension = extensions.group("ext").lower()
|
||||
params = extensions.string[extensions.end("ext") :].strip()
|
||||
esmtp_extensions[extension] = params
|
||||
|
||||
if extension == "auth":
|
||||
auth_types.extend([param.strip().lower() for param in params.split()])
|
||||
|
||||
return esmtp_extensions, auth_types
|
||||
373
Backend/venv/lib/python3.12/site-packages/aiosmtplib/protocol.py
Normal file
373
Backend/venv/lib/python3.12/site-packages/aiosmtplib/protocol.py
Normal file
@@ -0,0 +1,373 @@
|
||||
"""
|
||||
An ``asyncio.Protocol`` subclass for lower level IO handling.
|
||||
"""
|
||||
import asyncio
|
||||
import collections
|
||||
import re
|
||||
import ssl
|
||||
from typing import Deque, Optional, cast
|
||||
|
||||
from .errors import (
|
||||
SMTPDataError,
|
||||
SMTPReadTimeoutError,
|
||||
SMTPResponseException,
|
||||
SMTPServerDisconnected,
|
||||
SMTPTimeoutError,
|
||||
)
|
||||
from .response import SMTPResponse
|
||||
from .typing import SMTPStatus
|
||||
|
||||
|
||||
__all__ = ("SMTPProtocol",)
|
||||
|
||||
|
||||
MAX_LINE_LENGTH = 8192
|
||||
LINE_ENDINGS_REGEX = re.compile(rb"(?:\r\n|\n|\r(?!\n))")
|
||||
PERIOD_REGEX = re.compile(rb"(?m)^\.")
|
||||
|
||||
|
||||
class FlowControlMixin(asyncio.Protocol):
|
||||
"""
|
||||
Reusable flow control logic for StreamWriter.drain().
|
||||
This implements the protocol methods pause_writing(),
|
||||
resume_writing() and connection_lost(). If the subclass overrides
|
||||
these it must call the super methods.
|
||||
StreamWriter.drain() must wait for _drain_helper() coroutine.
|
||||
|
||||
Copied from stdlib as per recommendation: https://bugs.python.org/msg343685.
|
||||
Logging and asserts removed, type annotations added.
|
||||
"""
|
||||
|
||||
def __init__(self, loop: Optional[asyncio.AbstractEventLoop] = None):
|
||||
if loop is None:
|
||||
self._loop = asyncio.get_event_loop()
|
||||
else:
|
||||
self._loop = loop
|
||||
|
||||
self._paused = False
|
||||
self._drain_waiters: Deque[asyncio.Future[None]] = collections.deque()
|
||||
self._connection_lost = False
|
||||
|
||||
def pause_writing(self) -> None:
|
||||
self._paused = True
|
||||
|
||||
def resume_writing(self) -> None:
|
||||
self._paused = False
|
||||
|
||||
for waiter in self._drain_waiters:
|
||||
if not waiter.done():
|
||||
waiter.set_result(None)
|
||||
|
||||
def connection_lost(self, exc: Optional[Exception]) -> None:
|
||||
self._connection_lost = True
|
||||
# Wake up the writer(s) if currently paused.
|
||||
if not self._paused:
|
||||
return
|
||||
|
||||
for waiter in self._drain_waiters:
|
||||
if not waiter.done():
|
||||
if exc is None:
|
||||
waiter.set_result(None)
|
||||
else:
|
||||
waiter.set_exception(exc)
|
||||
|
||||
async def _drain_helper(self) -> None:
|
||||
if self._connection_lost:
|
||||
raise ConnectionResetError("Connection lost")
|
||||
if not self._paused:
|
||||
return
|
||||
waiter = self._loop.create_future()
|
||||
self._drain_waiters.append(waiter)
|
||||
try:
|
||||
await waiter
|
||||
finally:
|
||||
self._drain_waiters.remove(waiter)
|
||||
|
||||
def _get_close_waiter(self, stream: asyncio.StreamWriter) -> "asyncio.Future[None]":
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class SMTPProtocol(FlowControlMixin, asyncio.BaseProtocol):
|
||||
def __init__(
|
||||
self,
|
||||
loop: Optional[asyncio.AbstractEventLoop] = None,
|
||||
) -> None:
|
||||
super().__init__(loop=loop)
|
||||
self._over_ssl = False
|
||||
self._buffer = bytearray()
|
||||
self._response_waiter: Optional[asyncio.Future[SMTPResponse]] = None
|
||||
|
||||
self.transport: Optional[asyncio.BaseTransport] = None
|
||||
self._command_lock: Optional[asyncio.Lock] = None
|
||||
self._closed: "asyncio.Future[None]" = self._loop.create_future()
|
||||
self._quit_sent = False
|
||||
|
||||
def _get_close_waiter(self, stream: asyncio.StreamWriter) -> "asyncio.Future[None]":
|
||||
return self._closed
|
||||
|
||||
def __del__(self) -> None:
|
||||
# Avoid 'Future exception was never retrieved' warnings
|
||||
# Some unknown race conditions can sometimes trigger these :(
|
||||
self._retrieve_response_exception()
|
||||
|
||||
@property
|
||||
def is_connected(self) -> bool:
|
||||
"""
|
||||
Check if our transport is still connected.
|
||||
"""
|
||||
return bool(self.transport is not None and not self.transport.is_closing())
|
||||
|
||||
def connection_made(self, transport: asyncio.BaseTransport) -> None:
|
||||
self.transport = cast(asyncio.Transport, transport)
|
||||
self._over_ssl = transport.get_extra_info("sslcontext") is not None
|
||||
self._response_waiter = self._loop.create_future()
|
||||
self._command_lock = asyncio.Lock()
|
||||
self._quit_sent = False
|
||||
|
||||
def connection_lost(self, exc: Optional[Exception]) -> None:
|
||||
super().connection_lost(exc)
|
||||
|
||||
if not self._quit_sent:
|
||||
smtp_exc = SMTPServerDisconnected("Connection lost")
|
||||
if exc:
|
||||
smtp_exc.__cause__ = exc
|
||||
|
||||
if self._response_waiter and not self._response_waiter.done():
|
||||
self._response_waiter.set_exception(smtp_exc)
|
||||
|
||||
self.transport = None
|
||||
self._command_lock = None
|
||||
|
||||
def data_received(self, data: bytes) -> None:
|
||||
if self._response_waiter is None:
|
||||
raise RuntimeError(
|
||||
f"data_received called without a response waiter set: {data!r}"
|
||||
)
|
||||
elif self._response_waiter.done():
|
||||
# We got a response without issuing a command; ignore it.
|
||||
return
|
||||
|
||||
self._buffer.extend(data)
|
||||
|
||||
# If we got an obvious partial message, don't try to parse the buffer
|
||||
last_linebreak = data.rfind(b"\n")
|
||||
if (
|
||||
last_linebreak == -1
|
||||
or data[last_linebreak + 3 : last_linebreak + 4] == b"-"
|
||||
):
|
||||
return
|
||||
|
||||
try:
|
||||
response = self._read_response_from_buffer()
|
||||
except Exception as exc:
|
||||
self._response_waiter.set_exception(exc)
|
||||
else:
|
||||
if response is not None:
|
||||
self._response_waiter.set_result(response)
|
||||
|
||||
def eof_received(self) -> bool:
|
||||
exc = SMTPServerDisconnected("Unexpected EOF received")
|
||||
if self._response_waiter and not self._response_waiter.done():
|
||||
self._response_waiter.set_exception(exc)
|
||||
|
||||
# Returning false closes the transport
|
||||
return False
|
||||
|
||||
def _retrieve_response_exception(self) -> Optional[BaseException]:
|
||||
"""
|
||||
Return any exception that has been set on the response waiter.
|
||||
|
||||
Used to avoid 'Future exception was never retrieved' warnings
|
||||
"""
|
||||
if (
|
||||
self._response_waiter
|
||||
and self._response_waiter.done()
|
||||
and not self._response_waiter.cancelled()
|
||||
):
|
||||
return self._response_waiter.exception()
|
||||
|
||||
return None
|
||||
|
||||
def _read_response_from_buffer(self) -> Optional[SMTPResponse]:
|
||||
"""Parse the actual response (if any) from the data buffer"""
|
||||
code = -1
|
||||
message = bytearray()
|
||||
offset = 0
|
||||
message_complete = False
|
||||
|
||||
while True:
|
||||
line_end_index = self._buffer.find(b"\n", offset)
|
||||
if line_end_index == -1:
|
||||
break
|
||||
|
||||
line = bytes(self._buffer[offset : line_end_index + 1])
|
||||
|
||||
if len(line) > MAX_LINE_LENGTH:
|
||||
raise SMTPResponseException(
|
||||
SMTPStatus.unrecognized_command, "Response too long"
|
||||
)
|
||||
|
||||
try:
|
||||
code = int(line[:3])
|
||||
except ValueError:
|
||||
raise SMTPResponseException(
|
||||
SMTPStatus.invalid_response.value,
|
||||
f"Malformed SMTP response line: {line!r}",
|
||||
) from None
|
||||
|
||||
offset += len(line)
|
||||
if len(message):
|
||||
message.extend(b"\n")
|
||||
message.extend(line[4:].strip(b" \t\r\n"))
|
||||
if line[3:4] != b"-":
|
||||
message_complete = True
|
||||
break
|
||||
|
||||
if message_complete:
|
||||
response = SMTPResponse(
|
||||
code, bytes(message).decode("utf-8", "surrogateescape")
|
||||
)
|
||||
del self._buffer[:offset]
|
||||
return response
|
||||
else:
|
||||
return None
|
||||
|
||||
async def read_response(self, timeout: Optional[float] = None) -> SMTPResponse:
|
||||
"""
|
||||
Get a status response from the server.
|
||||
|
||||
This method must be awaited once per command sent; if multiple commands
|
||||
are written to the transport without awaiting, response data will be lost.
|
||||
|
||||
Returns an :class:`.response.SMTPResponse` namedtuple consisting of:
|
||||
- server response code (e.g. 250, or such, if all goes well)
|
||||
- server response string (multiline responses are converted to a
|
||||
single, multiline string).
|
||||
"""
|
||||
if self._response_waiter is None:
|
||||
raise SMTPServerDisconnected("Connection lost")
|
||||
|
||||
try:
|
||||
result = await asyncio.wait_for(self._response_waiter, timeout)
|
||||
except (TimeoutError, asyncio.TimeoutError) as exc:
|
||||
raise SMTPReadTimeoutError("Timed out waiting for server response") from exc
|
||||
finally:
|
||||
# If we were disconnected, don't create a new waiter
|
||||
if self.transport is None:
|
||||
self._response_waiter = None
|
||||
else:
|
||||
self._response_waiter = self._loop.create_future()
|
||||
|
||||
return result
|
||||
|
||||
def write(self, data: bytes) -> None:
|
||||
if self.transport is None or self.transport.is_closing():
|
||||
raise SMTPServerDisconnected("Connection lost")
|
||||
if not hasattr(self.transport, "write"):
|
||||
raise RuntimeError(
|
||||
f"Transport {self.transport!r} does not support writing."
|
||||
)
|
||||
|
||||
self.transport.write(data) # type: ignore
|
||||
|
||||
async def execute_command(
|
||||
self, *args: bytes, timeout: Optional[float] = None
|
||||
) -> SMTPResponse:
|
||||
"""
|
||||
Sends an SMTP command along with any args to the server, and returns
|
||||
a response.
|
||||
"""
|
||||
if self._command_lock is None:
|
||||
raise SMTPServerDisconnected("Server not connected")
|
||||
command = b" ".join(args) + b"\r\n"
|
||||
|
||||
async with self._command_lock:
|
||||
self.write(command)
|
||||
|
||||
if command == b"QUIT\r\n":
|
||||
self._quit_sent = True
|
||||
|
||||
response = await self.read_response(timeout=timeout)
|
||||
|
||||
return response
|
||||
|
||||
async def execute_data_command(
|
||||
self, message: bytes, timeout: Optional[float] = None
|
||||
) -> SMTPResponse:
|
||||
"""
|
||||
Sends an SMTP DATA command to the server, followed by encoded message content.
|
||||
|
||||
Automatically quotes lines beginning with a period per RFC821.
|
||||
Lone \\\\r and \\\\n characters are converted to \\\\r\\\\n
|
||||
characters.
|
||||
"""
|
||||
if self._command_lock is None:
|
||||
raise SMTPServerDisconnected("Server not connected")
|
||||
|
||||
message = LINE_ENDINGS_REGEX.sub(b"\r\n", message)
|
||||
message = PERIOD_REGEX.sub(b"..", message)
|
||||
if not message.endswith(b"\r\n"):
|
||||
message += b"\r\n"
|
||||
message += b".\r\n"
|
||||
|
||||
async with self._command_lock:
|
||||
self.write(b"DATA\r\n")
|
||||
start_response = await self.read_response(timeout=timeout)
|
||||
if start_response.code != SMTPStatus.start_input:
|
||||
raise SMTPDataError(start_response.code, start_response.message)
|
||||
|
||||
self.write(message)
|
||||
response = await self.read_response(timeout=timeout)
|
||||
if response.code != SMTPStatus.completed:
|
||||
raise SMTPDataError(response.code, response.message)
|
||||
|
||||
return response
|
||||
|
||||
async def start_tls(
|
||||
self,
|
||||
tls_context: ssl.SSLContext,
|
||||
server_hostname: Optional[str] = None,
|
||||
timeout: Optional[float] = None,
|
||||
) -> SMTPResponse:
|
||||
"""
|
||||
Puts the connection to the SMTP server into TLS mode.
|
||||
"""
|
||||
if self._over_ssl:
|
||||
raise RuntimeError("Already using TLS.")
|
||||
if self._command_lock is None:
|
||||
raise SMTPServerDisconnected("Server not connected")
|
||||
|
||||
async with self._command_lock:
|
||||
self.write(b"STARTTLS\r\n")
|
||||
response = await self.read_response(timeout=timeout)
|
||||
if response.code != SMTPStatus.ready:
|
||||
raise SMTPResponseException(response.code, response.message)
|
||||
|
||||
# Check for disconnect after response
|
||||
if self.transport is None or self.transport.is_closing():
|
||||
raise SMTPServerDisconnected("Connection lost")
|
||||
|
||||
try:
|
||||
tls_transport = await self._loop.start_tls(
|
||||
self.transport,
|
||||
self,
|
||||
tls_context,
|
||||
server_side=False,
|
||||
server_hostname=server_hostname,
|
||||
ssl_handshake_timeout=timeout,
|
||||
)
|
||||
except (TimeoutError, asyncio.TimeoutError) as exc:
|
||||
raise SMTPTimeoutError("Timed out while upgrading transport") from exc
|
||||
# SSLProtocol only raises ConnectionAbortedError on timeout
|
||||
except ConnectionAbortedError as exc:
|
||||
raise SMTPTimeoutError(exc.args[0]) from exc
|
||||
except ConnectionResetError as exc:
|
||||
if exc.args:
|
||||
message = exc.args[0]
|
||||
else:
|
||||
message = "Connection was reset while upgrading transport"
|
||||
raise SMTPServerDisconnected(message) from exc
|
||||
|
||||
self.transport = tls_transport
|
||||
|
||||
return response
|
||||
@@ -0,0 +1 @@
|
||||
This file exists to help mypy (and other tools) find inline type hints. See PR #141 and PEP 561.
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
SMTPResponse class, a simple namedtuple of (code, message).
|
||||
"""
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
__all__ = ("SMTPResponse",)
|
||||
|
||||
|
||||
class SMTPResponse(NamedTuple):
|
||||
"""
|
||||
NamedTuple of server response code and server response message.
|
||||
|
||||
``code`` and ``message`` can be accessed via attributes or indexes:
|
||||
|
||||
>>> response = SMTPResponse(200, "OK")
|
||||
>>> response.message
|
||||
'OK'
|
||||
>>> response[0]
|
||||
200
|
||||
>>> response.code
|
||||
200
|
||||
|
||||
"""
|
||||
|
||||
code: int
|
||||
message: str
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"({self.code}, {self.message})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.code} {self.message}"
|
||||
1459
Backend/venv/lib/python3.12/site-packages/aiosmtplib/smtp.py
Normal file
1459
Backend/venv/lib/python3.12/site-packages/aiosmtplib/smtp.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
from .typing import SMTPStatus
|
||||
|
||||
|
||||
# alias SMTPStatus for backwards compatibility
|
||||
__all__ = ("SMTPStatus",)
|
||||
@@ -0,0 +1,60 @@
|
||||
import enum
|
||||
import os
|
||||
from typing import Union
|
||||
|
||||
|
||||
__all__ = ("Default", "SMTPStatus", "SocketPathType", "_default")
|
||||
|
||||
|
||||
SocketPathType = Union[str, bytes, os.PathLike]
|
||||
|
||||
|
||||
class Default(enum.Enum):
|
||||
"""
|
||||
Used for type hinting kwarg defaults.
|
||||
"""
|
||||
|
||||
token = 0
|
||||
|
||||
|
||||
_default = Default.token
|
||||
|
||||
|
||||
@enum.unique
|
||||
class SMTPStatus(enum.IntEnum):
|
||||
"""
|
||||
Defines SMTP statuses for code readability.
|
||||
|
||||
See also: http://www.greenend.org.uk/rjk/tech/smtpreplies.html
|
||||
"""
|
||||
|
||||
invalid_response = -1
|
||||
system_status_ok = 211
|
||||
help_message = 214
|
||||
ready = 220
|
||||
closing = 221
|
||||
auth_successful = 235
|
||||
completed = 250
|
||||
will_forward = 251
|
||||
cannot_vrfy = 252
|
||||
auth_continue = 334
|
||||
start_input = 354
|
||||
domain_unavailable = 421
|
||||
mailbox_unavailable = 450
|
||||
error_processing = 451
|
||||
insufficient_storage = 452
|
||||
tls_not_available = 454
|
||||
unrecognized_command = 500
|
||||
unrecognized_parameters = 501
|
||||
command_not_implemented = 502
|
||||
bad_command_sequence = 503
|
||||
parameter_not_implemented = 504
|
||||
domain_does_not_accept_mail = 521
|
||||
access_denied = 530 # Sendmail specific
|
||||
auth_failed = 535
|
||||
mailbox_does_not_exist = 550
|
||||
user_not_local = 551
|
||||
storage_exceeded = 552
|
||||
mailbox_name_invalid = 553
|
||||
transaction_failed = 554
|
||||
syntax_error = 555
|
||||
Reference in New Issue
Block a user