RAHHH
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Twisted Mail: Servers and clients for POP3, ESMTP, and IMAP.
|
||||
"""
|
||||
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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
105
backend/lib/python3.12/site-packages/twisted/mail/_cred.py
Normal file
105
backend/lib/python3.12/site-packages/twisted/mail/_cred.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Credential managers for L{twisted.mail}.
|
||||
"""
|
||||
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.cred import credentials
|
||||
from twisted.mail._except import IllegalClientResponse
|
||||
from twisted.mail.interfaces import IChallengeResponse, IClientAuthentication
|
||||
from twisted.python.compat import nativeString
|
||||
|
||||
|
||||
@implementer(IClientAuthentication)
|
||||
class CramMD5ClientAuthenticator:
|
||||
def __init__(self, user):
|
||||
self.user = user
|
||||
|
||||
def getName(self):
|
||||
return b"CRAM-MD5"
|
||||
|
||||
def challengeResponse(self, secret, chal):
|
||||
response = hmac.HMAC(secret, chal, digestmod=hashlib.md5).hexdigest()
|
||||
return self.user + b" " + response.encode("ascii")
|
||||
|
||||
|
||||
@implementer(IClientAuthentication)
|
||||
class LOGINAuthenticator:
|
||||
def __init__(self, user):
|
||||
self.user = user
|
||||
self.challengeResponse = self.challengeUsername
|
||||
|
||||
def getName(self):
|
||||
return b"LOGIN"
|
||||
|
||||
def challengeUsername(self, secret, chal):
|
||||
# Respond to something like "Username:"
|
||||
self.challengeResponse = self.challengeSecret
|
||||
return self.user
|
||||
|
||||
def challengeSecret(self, secret, chal):
|
||||
# Respond to something like "Password:"
|
||||
return secret
|
||||
|
||||
|
||||
@implementer(IClientAuthentication)
|
||||
class PLAINAuthenticator:
|
||||
def __init__(self, user):
|
||||
self.user = user
|
||||
|
||||
def getName(self):
|
||||
return b"PLAIN"
|
||||
|
||||
def challengeResponse(self, secret, chal):
|
||||
return b"\0" + self.user + b"\0" + secret
|
||||
|
||||
|
||||
@implementer(IChallengeResponse)
|
||||
class LOGINCredentials(credentials.UsernamePassword):
|
||||
def __init__(self):
|
||||
self.challenges = [b"Password\0", b"User Name\0"]
|
||||
self.responses = [b"password", b"username"]
|
||||
credentials.UsernamePassword.__init__(self, None, None)
|
||||
|
||||
def getChallenge(self):
|
||||
return self.challenges.pop()
|
||||
|
||||
def setResponse(self, response):
|
||||
setattr(self, nativeString(self.responses.pop()), response)
|
||||
|
||||
def moreChallenges(self):
|
||||
return bool(self.challenges)
|
||||
|
||||
|
||||
@implementer(IChallengeResponse)
|
||||
class PLAINCredentials(credentials.UsernamePassword):
|
||||
def __init__(self):
|
||||
credentials.UsernamePassword.__init__(self, None, None)
|
||||
|
||||
def getChallenge(self):
|
||||
return b""
|
||||
|
||||
def setResponse(self, response):
|
||||
parts = response.split(b"\0")
|
||||
if len(parts) != 3:
|
||||
raise IllegalClientResponse("Malformed Response - wrong number of parts")
|
||||
useless, self.username, self.password = parts
|
||||
|
||||
def moreChallenges(self):
|
||||
return False
|
||||
|
||||
|
||||
__all__ = [
|
||||
"CramMD5ClientAuthenticator",
|
||||
"LOGINCredentials",
|
||||
"LOGINAuthenticator",
|
||||
"PLAINCredentials",
|
||||
"PLAINAuthenticator",
|
||||
]
|
||||
350
backend/lib/python3.12/site-packages/twisted/mail/_except.py
Normal file
350
backend/lib/python3.12/site-packages/twisted/mail/_except.py
Normal file
@@ -0,0 +1,350 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Exceptions in L{twisted.mail}.
|
||||
"""
|
||||
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class IMAP4Exception(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class IllegalClientResponse(IMAP4Exception):
|
||||
pass
|
||||
|
||||
|
||||
class IllegalOperation(IMAP4Exception):
|
||||
pass
|
||||
|
||||
|
||||
class IllegalMailboxEncoding(IMAP4Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MailboxException(IMAP4Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MailboxCollision(MailboxException):
|
||||
def __str__(self) -> str:
|
||||
return "Mailbox named %s already exists" % self.args
|
||||
|
||||
|
||||
class NoSuchMailbox(MailboxException):
|
||||
def __str__(self) -> str:
|
||||
return "No mailbox named %s exists" % self.args
|
||||
|
||||
|
||||
class ReadOnlyMailbox(MailboxException):
|
||||
def __str__(self) -> str:
|
||||
return "Mailbox open in read-only state"
|
||||
|
||||
|
||||
class UnhandledResponse(IMAP4Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NegativeResponse(IMAP4Exception):
|
||||
pass
|
||||
|
||||
|
||||
class NoSupportedAuthentication(IMAP4Exception):
|
||||
def __init__(self, serverSupports, clientSupports):
|
||||
IMAP4Exception.__init__(self, "No supported authentication schemes available")
|
||||
self.serverSupports = serverSupports
|
||||
self.clientSupports = clientSupports
|
||||
|
||||
def __str__(self) -> str:
|
||||
return IMAP4Exception.__str__(
|
||||
self
|
||||
) + ": Server supports {!r}, client supports {!r}".format(
|
||||
self.serverSupports,
|
||||
self.clientSupports,
|
||||
)
|
||||
|
||||
|
||||
class IllegalServerResponse(IMAP4Exception):
|
||||
pass
|
||||
|
||||
|
||||
class IllegalIdentifierError(IMAP4Exception):
|
||||
pass
|
||||
|
||||
|
||||
class IllegalQueryError(IMAP4Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MismatchedNesting(IMAP4Exception):
|
||||
pass
|
||||
|
||||
|
||||
class MismatchedQuoting(IMAP4Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SMTPError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class SMTPClientError(SMTPError):
|
||||
"""
|
||||
Base class for SMTP client errors.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
code: int,
|
||||
resp: bytes,
|
||||
log: Optional[bytes] = None,
|
||||
addresses: Optional[object] = None,
|
||||
isFatal: bool = False,
|
||||
retry: bool = False,
|
||||
):
|
||||
"""
|
||||
@param code: The SMTP response code associated with this error.
|
||||
@param resp: The string response associated with this error.
|
||||
@param log: A string log of the exchange leading up to and including
|
||||
the error.
|
||||
@param isFatal: A boolean indicating whether this connection can
|
||||
proceed or not. If True, the connection will be dropped.
|
||||
@param retry: A boolean indicating whether the delivery should be
|
||||
retried. If True and the factory indicates further retries are
|
||||
desirable, they will be attempted, otherwise the delivery will be
|
||||
failed.
|
||||
"""
|
||||
if isinstance(resp, str): # type: ignore[unreachable]
|
||||
resp = resp.encode("utf-8") # type: ignore[unreachable]
|
||||
|
||||
if isinstance(log, str):
|
||||
log = log.encode("utf-8") # type: ignore[unreachable]
|
||||
|
||||
self.code = code
|
||||
self.resp = resp
|
||||
self.log = log
|
||||
self.addresses = addresses
|
||||
self.isFatal = isFatal
|
||||
self.retry = retry
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.__bytes__().decode("utf-8")
|
||||
|
||||
def __bytes__(self) -> bytes:
|
||||
if self.code > 0:
|
||||
res = [f"{self.code:03d} ".encode() + self.resp]
|
||||
else:
|
||||
res = [self.resp]
|
||||
if self.log:
|
||||
res.append(self.log)
|
||||
res.append(b"")
|
||||
return b"\n".join(res)
|
||||
|
||||
|
||||
class ESMTPClientError(SMTPClientError):
|
||||
"""
|
||||
Base class for ESMTP client errors.
|
||||
"""
|
||||
|
||||
|
||||
class EHLORequiredError(ESMTPClientError):
|
||||
"""
|
||||
The server does not support EHLO.
|
||||
|
||||
This is considered a non-fatal error (the connection will not be dropped).
|
||||
"""
|
||||
|
||||
|
||||
class AUTHRequiredError(ESMTPClientError):
|
||||
"""
|
||||
Authentication was required but the server does not support it.
|
||||
|
||||
This is considered a non-fatal error (the connection will not be dropped).
|
||||
"""
|
||||
|
||||
|
||||
class TLSRequiredError(ESMTPClientError):
|
||||
"""
|
||||
Transport security was required but the server does not support it.
|
||||
|
||||
This is considered a non-fatal error (the connection will not be dropped).
|
||||
"""
|
||||
|
||||
|
||||
class AUTHDeclinedError(ESMTPClientError):
|
||||
"""
|
||||
The server rejected our credentials.
|
||||
|
||||
Either the username, password, or challenge response
|
||||
given to the server was rejected.
|
||||
|
||||
This is considered a non-fatal error (the connection will not be
|
||||
dropped).
|
||||
"""
|
||||
|
||||
|
||||
class AuthenticationError(ESMTPClientError):
|
||||
"""
|
||||
An error occurred while authenticating.
|
||||
|
||||
Either the server rejected our request for authentication or the
|
||||
challenge received was malformed.
|
||||
|
||||
This is considered a non-fatal error (the connection will not be
|
||||
dropped).
|
||||
"""
|
||||
|
||||
|
||||
class SMTPTLSError(ESMTPClientError):
|
||||
"""
|
||||
An error occurred while negiotiating for transport security.
|
||||
|
||||
This is considered a non-fatal error (the connection will not be dropped).
|
||||
"""
|
||||
|
||||
|
||||
class SMTPConnectError(SMTPClientError):
|
||||
"""
|
||||
Failed to connect to the mail exchange host.
|
||||
|
||||
This is considered a fatal error. A retry will be made.
|
||||
"""
|
||||
|
||||
def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=True):
|
||||
SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry)
|
||||
|
||||
|
||||
class SMTPTimeoutError(SMTPClientError):
|
||||
"""
|
||||
Failed to receive a response from the server in the expected time period.
|
||||
|
||||
This is considered a fatal error. A retry will be made.
|
||||
"""
|
||||
|
||||
def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=True):
|
||||
SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry)
|
||||
|
||||
|
||||
class SMTPProtocolError(SMTPClientError):
|
||||
"""
|
||||
The server sent a mangled response.
|
||||
|
||||
This is considered a fatal error. A retry will not be made.
|
||||
"""
|
||||
|
||||
def __init__(self, code, resp, log=None, addresses=None, isFatal=True, retry=False):
|
||||
SMTPClientError.__init__(self, code, resp, log, addresses, isFatal, retry)
|
||||
|
||||
|
||||
class SMTPDeliveryError(SMTPClientError):
|
||||
"""
|
||||
Indicates that a delivery attempt has had an error.
|
||||
"""
|
||||
|
||||
|
||||
class SMTPServerError(SMTPError):
|
||||
def __init__(self, code, resp):
|
||||
self.code = code
|
||||
self.resp = resp
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "%.3d %s" % (self.code, self.resp)
|
||||
|
||||
|
||||
class SMTPAddressError(SMTPServerError):
|
||||
def __init__(self, addr, code, resp):
|
||||
from twisted.mail.smtp import Address
|
||||
|
||||
SMTPServerError.__init__(self, code, resp)
|
||||
self.addr = Address(addr)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return "%.3d <%s>... %s" % (self.code, self.addr, self.resp)
|
||||
|
||||
|
||||
class SMTPBadRcpt(SMTPAddressError):
|
||||
def __init__(self, addr, code=550, resp="Cannot receive for specified address"):
|
||||
SMTPAddressError.__init__(self, addr, code, resp)
|
||||
|
||||
|
||||
class SMTPBadSender(SMTPAddressError):
|
||||
def __init__(self, addr, code=550, resp="Sender not acceptable"):
|
||||
SMTPAddressError.__init__(self, addr, code, resp)
|
||||
|
||||
|
||||
class AddressError(SMTPError):
|
||||
"""
|
||||
Parse error in address
|
||||
"""
|
||||
|
||||
|
||||
class POP3Error(Exception):
|
||||
"""
|
||||
The base class for POP3 errors.
|
||||
"""
|
||||
|
||||
pass
|
||||
|
||||
|
||||
class _POP3MessageDeleted(Exception):
|
||||
"""
|
||||
An internal control-flow error which indicates that a deleted message was
|
||||
requested.
|
||||
"""
|
||||
|
||||
|
||||
class POP3ClientError(Exception):
|
||||
"""
|
||||
The base class for all exceptions raised by POP3Client.
|
||||
"""
|
||||
|
||||
|
||||
class InsecureAuthenticationDisallowed(POP3ClientError):
|
||||
"""
|
||||
An error indicating secure authentication was required but no mechanism
|
||||
could be found.
|
||||
"""
|
||||
|
||||
|
||||
class TLSError(POP3ClientError):
|
||||
"""
|
||||
An error indicating secure authentication was required but either the
|
||||
transport does not support TLS or no TLS context factory was supplied.
|
||||
"""
|
||||
|
||||
|
||||
class TLSNotSupportedError(POP3ClientError):
|
||||
"""
|
||||
An error indicating secure authentication was required but the server does
|
||||
not support TLS.
|
||||
"""
|
||||
|
||||
|
||||
class ServerErrorResponse(POP3ClientError):
|
||||
"""
|
||||
An error indicating that the server returned an error response to a
|
||||
request.
|
||||
|
||||
@ivar consumer: See L{__init__}
|
||||
"""
|
||||
|
||||
def __init__(self, reason, consumer=None):
|
||||
"""
|
||||
@type reason: L{bytes}
|
||||
@param reason: The server response minus the status indicator.
|
||||
|
||||
@type consumer: callable that takes L{object}
|
||||
@param consumer: The function meant to handle the values for a
|
||||
multi-line response.
|
||||
"""
|
||||
POP3ClientError.__init__(self, reason)
|
||||
self.consumer = consumer
|
||||
|
||||
|
||||
class LineTooLong(POP3ClientError):
|
||||
"""
|
||||
An error indicating that the server sent a line which exceeded the
|
||||
maximum line length (L{LineOnlyReceiver.MAX_LENGTH}).
|
||||
"""
|
||||
1235
backend/lib/python3.12/site-packages/twisted/mail/_pop3client.py
Normal file
1235
backend/lib/python3.12/site-packages/twisted/mail/_pop3client.py
Normal file
File diff suppressed because it is too large
Load Diff
765
backend/lib/python3.12/site-packages/twisted/mail/alias.py
Normal file
765
backend/lib/python3.12/site-packages/twisted/mail/alias.py
Normal file
@@ -0,0 +1,765 @@
|
||||
# -*- test-case-name: twisted.mail.test.test_mail -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""
|
||||
Support for aliases(5) configuration files.
|
||||
|
||||
@author: Jp Calderone
|
||||
"""
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.internet import defer, protocol, reactor
|
||||
from twisted.mail import smtp
|
||||
from twisted.mail.interfaces import IAlias
|
||||
from twisted.python import failure, log
|
||||
|
||||
|
||||
def handle(result, line, filename, lineNo):
|
||||
"""
|
||||
Parse a line from an aliases file.
|
||||
|
||||
@type result: L{dict} mapping L{bytes} to L{list} of L{bytes}
|
||||
@param result: A dictionary mapping username to aliases to which
|
||||
the results of parsing the line are added.
|
||||
|
||||
@type line: L{bytes}
|
||||
@param line: A line from an aliases file.
|
||||
|
||||
@type filename: L{bytes}
|
||||
@param filename: The full or relative path to the aliases file.
|
||||
|
||||
@type lineNo: L{int}
|
||||
@param lineNo: The position of the line within the aliases file.
|
||||
"""
|
||||
parts = [p.strip() for p in line.split(":", 1)]
|
||||
if len(parts) != 2:
|
||||
fmt = "Invalid format on line %d of alias file %s."
|
||||
arg = (lineNo, filename)
|
||||
log.err(fmt % arg)
|
||||
else:
|
||||
user, alias = parts
|
||||
result.setdefault(user.strip(), []).extend(map(str.strip, alias.split(",")))
|
||||
|
||||
|
||||
def loadAliasFile(domains, filename=None, fp=None):
|
||||
"""
|
||||
Load a file containing email aliases.
|
||||
|
||||
Lines in the file should be formatted like so::
|
||||
|
||||
username: alias1, alias2, ..., aliasN
|
||||
|
||||
Aliases beginning with a C{|} will be treated as programs, will be run, and
|
||||
the message will be written to their stdin.
|
||||
|
||||
Aliases beginning with a C{:} will be treated as a file containing
|
||||
additional aliases for the username.
|
||||
|
||||
Aliases beginning with a C{/} will be treated as the full pathname to a file
|
||||
to which the message will be appended.
|
||||
|
||||
Aliases without a host part will be assumed to be addresses on localhost.
|
||||
|
||||
If a username is specified multiple times, the aliases for each are joined
|
||||
together as if they had all been on one line.
|
||||
|
||||
Lines beginning with a space or a tab are continuations of the previous
|
||||
line.
|
||||
|
||||
Lines beginning with a C{#} are comments.
|
||||
|
||||
@type domains: L{dict} mapping L{bytes} to L{IDomain} provider
|
||||
@param domains: A mapping of domain name to domain object.
|
||||
|
||||
@type filename: L{bytes} or L{None}
|
||||
@param filename: The full or relative path to a file from which to load
|
||||
aliases. If omitted, the C{fp} parameter must be specified.
|
||||
|
||||
@type fp: file-like object or L{None}
|
||||
@param fp: The file from which to load aliases. If specified,
|
||||
the C{filename} parameter is ignored.
|
||||
|
||||
@rtype: L{dict} mapping L{bytes} to L{AliasGroup}
|
||||
@return: A mapping from username to group of aliases.
|
||||
"""
|
||||
result = {}
|
||||
close = False
|
||||
if fp is None:
|
||||
fp = open(filename)
|
||||
close = True
|
||||
else:
|
||||
filename = getattr(fp, "name", "<unknown>")
|
||||
i = 0
|
||||
prev = ""
|
||||
try:
|
||||
for line in fp:
|
||||
i += 1
|
||||
line = line.rstrip()
|
||||
if line.lstrip().startswith("#"):
|
||||
continue
|
||||
elif line.startswith(" ") or line.startswith("\t"):
|
||||
prev = prev + line
|
||||
else:
|
||||
if prev:
|
||||
handle(result, prev, filename, i)
|
||||
prev = line
|
||||
finally:
|
||||
if close:
|
||||
fp.close()
|
||||
if prev:
|
||||
handle(result, prev, filename, i)
|
||||
for u, a in result.items():
|
||||
result[u] = AliasGroup(a, domains, u)
|
||||
return result
|
||||
|
||||
|
||||
class AliasBase:
|
||||
"""
|
||||
The default base class for aliases.
|
||||
|
||||
@ivar domains: See L{__init__}.
|
||||
|
||||
@type original: L{Address}
|
||||
@ivar original: The original address being aliased.
|
||||
"""
|
||||
|
||||
def __init__(self, domains, original):
|
||||
"""
|
||||
@type domains: L{dict} mapping L{bytes} to L{IDomain} provider
|
||||
@param domains: A mapping of domain name to domain object.
|
||||
|
||||
@type original: L{bytes}
|
||||
@param original: The original address being aliased.
|
||||
"""
|
||||
self.domains = domains
|
||||
self.original = smtp.Address(original)
|
||||
|
||||
def domain(self):
|
||||
"""
|
||||
Return the domain associated with original address.
|
||||
|
||||
@rtype: L{IDomain} provider
|
||||
@return: The domain for the original address.
|
||||
"""
|
||||
return self.domains[self.original.domain]
|
||||
|
||||
def resolve(self, aliasmap, memo=None):
|
||||
"""
|
||||
Map this alias to its ultimate destination.
|
||||
|
||||
@type aliasmap: L{dict} mapping L{bytes} to L{AliasBase}
|
||||
@param aliasmap: A mapping of username to alias or group of aliases.
|
||||
|
||||
@type memo: L{None} or L{dict} of L{AliasBase}
|
||||
@param memo: A record of the aliases already considered in the
|
||||
resolution process. If provided, C{memo} is modified to include
|
||||
this alias.
|
||||
|
||||
@rtype: L{IMessage <smtp.IMessage>} or L{None}
|
||||
@return: A message receiver for the ultimate destination or None for
|
||||
an invalid destination.
|
||||
"""
|
||||
if memo is None:
|
||||
memo = {}
|
||||
if str(self) in memo:
|
||||
return None
|
||||
memo[str(self)] = None
|
||||
return self.createMessageReceiver()
|
||||
|
||||
|
||||
@implementer(IAlias)
|
||||
class AddressAlias(AliasBase):
|
||||
"""
|
||||
An alias which translates one email address into another.
|
||||
|
||||
@type alias : L{Address}
|
||||
@ivar alias: The destination address.
|
||||
"""
|
||||
|
||||
def __init__(self, alias, *args):
|
||||
"""
|
||||
@type alias: L{Address}, L{User}, L{bytes} or object which can be
|
||||
converted into L{bytes}
|
||||
@param alias: The destination address.
|
||||
|
||||
@type args: 2-L{tuple} of (0) L{dict} mapping L{bytes} to L{IDomain}
|
||||
provider, (1) L{bytes}
|
||||
@param args: Arguments for L{AliasBase.__init__}.
|
||||
"""
|
||||
AliasBase.__init__(self, *args)
|
||||
self.alias = smtp.Address(alias)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Build a string representation of this L{AddressAlias} instance.
|
||||
|
||||
@rtype: L{bytes}
|
||||
@return: A string containing the destination address.
|
||||
"""
|
||||
return f"<Address {self.alias}>"
|
||||
|
||||
def createMessageReceiver(self):
|
||||
"""
|
||||
Create a message receiver which delivers a message to
|
||||
the destination address.
|
||||
|
||||
@rtype: L{IMessage <smtp.IMessage>} provider
|
||||
@return: A message receiver.
|
||||
"""
|
||||
return self.domain().exists(str(self.alias))
|
||||
|
||||
def resolve(self, aliasmap, memo=None):
|
||||
"""
|
||||
Map this alias to its ultimate destination.
|
||||
|
||||
@type aliasmap: L{dict} mapping L{bytes} to L{AliasBase}
|
||||
@param aliasmap: A mapping of username to alias or group of aliases.
|
||||
|
||||
@type memo: L{None} or L{dict} of L{AliasBase}
|
||||
@param memo: A record of the aliases already considered in the
|
||||
resolution process. If provided, C{memo} is modified to include
|
||||
this alias.
|
||||
|
||||
@rtype: L{IMessage <smtp.IMessage>} or L{None}
|
||||
@return: A message receiver for the ultimate destination or None for
|
||||
an invalid destination.
|
||||
"""
|
||||
if memo is None:
|
||||
memo = {}
|
||||
if str(self) in memo:
|
||||
return None
|
||||
memo[str(self)] = None
|
||||
try:
|
||||
return self.domain().exists(smtp.User(self.alias, None, None, None), memo)()
|
||||
except smtp.SMTPBadRcpt:
|
||||
pass
|
||||
if self.alias.local in aliasmap:
|
||||
return aliasmap[self.alias.local].resolve(aliasmap, memo)
|
||||
return None
|
||||
|
||||
|
||||
@implementer(smtp.IMessage)
|
||||
class FileWrapper:
|
||||
"""
|
||||
A message receiver which delivers a message to a file.
|
||||
|
||||
@type fp: file-like object
|
||||
@ivar fp: A file used for temporary storage of the message.
|
||||
|
||||
@type finalname: L{bytes}
|
||||
@ivar finalname: The name of the file in which the message should be
|
||||
stored.
|
||||
"""
|
||||
|
||||
def __init__(self, filename):
|
||||
"""
|
||||
@type filename: L{bytes}
|
||||
@param filename: The name of the file in which the message should be
|
||||
stored.
|
||||
"""
|
||||
self.fp = tempfile.TemporaryFile()
|
||||
self.finalname = filename
|
||||
|
||||
def lineReceived(self, line):
|
||||
"""
|
||||
Write a received line to the temporary file.
|
||||
|
||||
@type line: L{bytes}
|
||||
@param line: A received line of the message.
|
||||
"""
|
||||
self.fp.write(line + "\n")
|
||||
|
||||
def eomReceived(self):
|
||||
"""
|
||||
Handle end of message by writing the message to the file.
|
||||
|
||||
@rtype: L{Deferred <defer.Deferred>} which successfully results in
|
||||
L{bytes}
|
||||
@return: A deferred which succeeds with the name of the file to which
|
||||
the message has been stored or fails if the message cannot be
|
||||
saved to the file.
|
||||
"""
|
||||
self.fp.seek(0, 0)
|
||||
try:
|
||||
f = open(self.finalname, "a")
|
||||
except BaseException:
|
||||
return defer.fail(failure.Failure())
|
||||
|
||||
with f:
|
||||
f.write(self.fp.read())
|
||||
self.fp.close()
|
||||
|
||||
return defer.succeed(self.finalname)
|
||||
|
||||
def connectionLost(self):
|
||||
"""
|
||||
Close the temporary file when the connection is lost.
|
||||
"""
|
||||
self.fp.close()
|
||||
self.fp = None
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Build a string representation of this L{FileWrapper} instance.
|
||||
|
||||
@rtype: L{bytes}
|
||||
@return: A string containing the file name of the message.
|
||||
"""
|
||||
return f"<FileWrapper {self.finalname}>"
|
||||
|
||||
|
||||
@implementer(IAlias)
|
||||
class FileAlias(AliasBase):
|
||||
"""
|
||||
An alias which translates an address to a file.
|
||||
|
||||
@ivar filename: See L{__init__}.
|
||||
"""
|
||||
|
||||
def __init__(self, filename, *args):
|
||||
"""
|
||||
@type filename: L{bytes}
|
||||
@param filename: The name of the file in which to store the message.
|
||||
|
||||
@type args: 2-L{tuple} of (0) L{dict} mapping L{bytes} to L{IDomain}
|
||||
provider, (1) L{bytes}
|
||||
@param args: Arguments for L{AliasBase.__init__}.
|
||||
"""
|
||||
AliasBase.__init__(self, *args)
|
||||
self.filename = filename
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Build a string representation of this L{FileAlias} instance.
|
||||
|
||||
@rtype: L{bytes}
|
||||
@return: A string containing the name of the file.
|
||||
"""
|
||||
return f"<File {self.filename}>"
|
||||
|
||||
def createMessageReceiver(self):
|
||||
"""
|
||||
Create a message receiver which delivers a message to the file.
|
||||
|
||||
@rtype: L{FileWrapper}
|
||||
@return: A message receiver which writes a message to the file.
|
||||
"""
|
||||
return FileWrapper(self.filename)
|
||||
|
||||
|
||||
class ProcessAliasTimeout(Exception):
|
||||
"""
|
||||
An error indicating that a timeout occurred while waiting for a process
|
||||
to complete.
|
||||
"""
|
||||
|
||||
|
||||
@implementer(smtp.IMessage)
|
||||
class MessageWrapper:
|
||||
"""
|
||||
A message receiver which delivers a message to a child process.
|
||||
|
||||
@type completionTimeout: L{int} or L{float}
|
||||
@ivar completionTimeout: The number of seconds to wait for the child
|
||||
process to exit before reporting the delivery as a failure.
|
||||
|
||||
@type _timeoutCallID: L{None} or
|
||||
L{IDelayedCall <twisted.internet.interfaces.IDelayedCall>} provider
|
||||
@ivar _timeoutCallID: The call used to time out delivery, started when the
|
||||
connection to the child process is closed.
|
||||
|
||||
@type done: L{bool}
|
||||
@ivar done: A flag indicating whether the child process has exited
|
||||
(C{True}) or not (C{False}).
|
||||
|
||||
@type reactor: L{IReactorTime <twisted.internet.interfaces.IReactorTime>}
|
||||
provider
|
||||
@ivar reactor: A reactor which will be used to schedule timeouts.
|
||||
|
||||
@ivar protocol: See L{__init__}.
|
||||
|
||||
@type processName: L{bytes} or L{None}
|
||||
@ivar processName: The process name.
|
||||
|
||||
@type completion: L{Deferred <defer.Deferred>}
|
||||
@ivar completion: The deferred which will be triggered by the protocol
|
||||
when the child process exits.
|
||||
"""
|
||||
|
||||
done = False
|
||||
|
||||
completionTimeout = 60
|
||||
_timeoutCallID = None
|
||||
|
||||
reactor = reactor
|
||||
|
||||
def __init__(self, protocol, process=None, reactor=None):
|
||||
"""
|
||||
@type protocol: L{ProcessAliasProtocol}
|
||||
@param protocol: The protocol associated with the child process.
|
||||
|
||||
@type process: L{bytes} or L{None}
|
||||
@param process: The process name.
|
||||
|
||||
@type reactor: L{None} or L{IReactorTime
|
||||
<twisted.internet.interfaces.IReactorTime>} provider
|
||||
@param reactor: A reactor which will be used to schedule timeouts.
|
||||
"""
|
||||
self.processName = process
|
||||
self.protocol = protocol
|
||||
self.completion = defer.Deferred()
|
||||
self.protocol.onEnd = self.completion
|
||||
self.completion.addBoth(self._processEnded)
|
||||
|
||||
if reactor is not None:
|
||||
self.reactor = reactor
|
||||
|
||||
def _processEnded(self, result):
|
||||
"""
|
||||
Record process termination and cancel the timeout call if it is active.
|
||||
|
||||
@type result: L{Failure <failure.Failure>}
|
||||
@param result: The reason the child process terminated.
|
||||
|
||||
@rtype: L{None} or L{Failure <failure.Failure>}
|
||||
@return: None, if the process end is expected, or the reason the child
|
||||
process terminated, if the process end is unexpected.
|
||||
"""
|
||||
self.done = True
|
||||
if self._timeoutCallID is not None:
|
||||
# eomReceived was called, we're actually waiting for the process to
|
||||
# exit.
|
||||
self._timeoutCallID.cancel()
|
||||
self._timeoutCallID = None
|
||||
else:
|
||||
# eomReceived was not called, this is unexpected, propagate the
|
||||
# error.
|
||||
return result
|
||||
|
||||
def lineReceived(self, line):
|
||||
"""
|
||||
Write a received line to the child process.
|
||||
|
||||
@type line: L{bytes}
|
||||
@param line: A received line of the message.
|
||||
"""
|
||||
if self.done:
|
||||
return
|
||||
self.protocol.transport.write(line + "\n")
|
||||
|
||||
def eomReceived(self):
|
||||
"""
|
||||
Disconnect from the child process and set up a timeout to wait for it
|
||||
to exit.
|
||||
|
||||
@rtype: L{Deferred <defer.Deferred>}
|
||||
@return: A deferred which will be called back when the child process
|
||||
exits.
|
||||
"""
|
||||
if not self.done:
|
||||
self.protocol.transport.loseConnection()
|
||||
self._timeoutCallID = self.reactor.callLater(
|
||||
self.completionTimeout, self._completionCancel
|
||||
)
|
||||
return self.completion
|
||||
|
||||
def _completionCancel(self):
|
||||
"""
|
||||
Handle the expiration of the timeout for the child process to exit by
|
||||
terminating the child process forcefully and issuing a failure to the
|
||||
L{completion} deferred.
|
||||
"""
|
||||
self._timeoutCallID = None
|
||||
self.protocol.transport.signalProcess("KILL")
|
||||
exc = ProcessAliasTimeout(f"No answer after {self.completionTimeout} seconds")
|
||||
self.protocol.onEnd = None
|
||||
self.completion.errback(failure.Failure(exc))
|
||||
|
||||
def connectionLost(self):
|
||||
"""
|
||||
Ignore notification of lost connection.
|
||||
"""
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Build a string representation of this L{MessageWrapper} instance.
|
||||
|
||||
@rtype: L{bytes}
|
||||
@return: A string containing the name of the process.
|
||||
"""
|
||||
return f"<ProcessWrapper {self.processName}>"
|
||||
|
||||
|
||||
class ProcessAliasProtocol(protocol.ProcessProtocol):
|
||||
"""
|
||||
A process protocol which errbacks a deferred when the associated
|
||||
process ends.
|
||||
|
||||
@type onEnd: L{None} or L{Deferred <defer.Deferred>}
|
||||
@ivar onEnd: If set, a deferred on which to errback when the process ends.
|
||||
"""
|
||||
|
||||
onEnd = None
|
||||
|
||||
def processEnded(self, reason):
|
||||
"""
|
||||
Call an errback.
|
||||
|
||||
@type reason: L{Failure <failure.Failure>}
|
||||
@param reason: The reason the child process terminated.
|
||||
"""
|
||||
if self.onEnd is not None:
|
||||
self.onEnd.errback(reason)
|
||||
|
||||
|
||||
@implementer(IAlias)
|
||||
class ProcessAlias(AliasBase):
|
||||
"""
|
||||
An alias which is handled by the execution of a program.
|
||||
|
||||
@type path: L{list} of L{bytes}
|
||||
@ivar path: The arguments to pass to the process. The first string is
|
||||
the executable's name.
|
||||
|
||||
@type program: L{bytes}
|
||||
@ivar program: The path of the program to be executed.
|
||||
|
||||
@type reactor: L{IReactorTime <twisted.internet.interfaces.IReactorTime>}
|
||||
and L{IReactorProcess <twisted.internet.interfaces.IReactorProcess>}
|
||||
provider
|
||||
@ivar reactor: A reactor which will be used to create and timeout the
|
||||
child process.
|
||||
"""
|
||||
|
||||
reactor = reactor
|
||||
|
||||
def __init__(self, path, *args):
|
||||
"""
|
||||
@type path: L{bytes}
|
||||
@param path: The command to invoke the program consisting of the path
|
||||
to the executable followed by any arguments.
|
||||
|
||||
@type args: 2-L{tuple} of (0) L{dict} mapping L{bytes} to L{IDomain}
|
||||
provider, (1) L{bytes}
|
||||
@param args: Arguments for L{AliasBase.__init__}.
|
||||
"""
|
||||
|
||||
AliasBase.__init__(self, *args)
|
||||
self.path = path.split()
|
||||
self.program = self.path[0]
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Build a string representation of this L{ProcessAlias} instance.
|
||||
|
||||
@rtype: L{bytes}
|
||||
@return: A string containing the command used to invoke the process.
|
||||
"""
|
||||
return f"<Process {self.path}>"
|
||||
|
||||
def spawnProcess(self, proto, program, path):
|
||||
"""
|
||||
Spawn a process.
|
||||
|
||||
This wraps the L{spawnProcess
|
||||
<twisted.internet.interfaces.IReactorProcess.spawnProcess>} method on
|
||||
L{reactor} so that it can be customized for test purposes.
|
||||
|
||||
@type proto: L{IProcessProtocol
|
||||
<twisted.internet.interfaces.IProcessProtocol>} provider
|
||||
@param proto: An object which will be notified of all events related to
|
||||
the created process.
|
||||
|
||||
@type program: L{bytes}
|
||||
@param program: The full path name of the file to execute.
|
||||
|
||||
@type path: L{list} of L{bytes}
|
||||
@param path: The arguments to pass to the process. The first string
|
||||
should be the executable's name.
|
||||
|
||||
@rtype: L{IProcessTransport
|
||||
<twisted.internet.interfaces.IProcessTransport>} provider
|
||||
@return: A process transport.
|
||||
"""
|
||||
return self.reactor.spawnProcess(proto, program, path)
|
||||
|
||||
def createMessageReceiver(self):
|
||||
"""
|
||||
Launch a process and create a message receiver to pass a message
|
||||
to the process.
|
||||
|
||||
@rtype: L{MessageWrapper}
|
||||
@return: A message receiver which delivers a message to the process.
|
||||
"""
|
||||
p = ProcessAliasProtocol()
|
||||
m = MessageWrapper(p, self.program, self.reactor)
|
||||
self.spawnProcess(p, self.program, self.path)
|
||||
return m
|
||||
|
||||
|
||||
@implementer(smtp.IMessage)
|
||||
class MultiWrapper:
|
||||
"""
|
||||
A message receiver which delivers a single message to multiple other
|
||||
message receivers.
|
||||
|
||||
@ivar objs: See L{__init__}.
|
||||
"""
|
||||
|
||||
def __init__(self, objs):
|
||||
"""
|
||||
@type objs: L{list} of L{IMessage <smtp.IMessage>} provider
|
||||
@param objs: Message receivers to which the incoming message should be
|
||||
directed.
|
||||
"""
|
||||
self.objs = objs
|
||||
|
||||
def lineReceived(self, line):
|
||||
"""
|
||||
Pass a received line to the message receivers.
|
||||
|
||||
@type line: L{bytes}
|
||||
@param line: A line of the message.
|
||||
"""
|
||||
for o in self.objs:
|
||||
o.lineReceived(line)
|
||||
|
||||
def eomReceived(self):
|
||||
"""
|
||||
Pass the end of message along to the message receivers.
|
||||
|
||||
@rtype: L{DeferredList <defer.DeferredList>} whose successful results
|
||||
are L{bytes} or L{None}
|
||||
@return: A deferred list which triggers when all of the message
|
||||
receivers have finished handling their end of message.
|
||||
"""
|
||||
return defer.DeferredList([o.eomReceived() for o in self.objs])
|
||||
|
||||
def connectionLost(self):
|
||||
"""
|
||||
Inform the message receivers that the connection has been lost.
|
||||
"""
|
||||
for o in self.objs:
|
||||
o.connectionLost()
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Build a string representation of this L{MultiWrapper} instance.
|
||||
|
||||
@rtype: L{bytes}
|
||||
@return: A string containing a list of the message receivers.
|
||||
"""
|
||||
return f"<GroupWrapper {map(str, self.objs)!r}>"
|
||||
|
||||
|
||||
@implementer(IAlias)
|
||||
class AliasGroup(AliasBase):
|
||||
"""
|
||||
An alias which points to multiple destination aliases.
|
||||
|
||||
@type processAliasFactory: no-argument callable which returns
|
||||
L{ProcessAlias}
|
||||
@ivar processAliasFactory: A factory for process aliases.
|
||||
|
||||
@type aliases: L{list} of L{AliasBase} which implements L{IAlias}
|
||||
@ivar aliases: The destination aliases.
|
||||
"""
|
||||
|
||||
processAliasFactory = ProcessAlias
|
||||
|
||||
def __init__(self, items, *args):
|
||||
"""
|
||||
Create a group of aliases.
|
||||
|
||||
Parse a list of alias strings and, for each, create an appropriate
|
||||
alias object.
|
||||
|
||||
@type items: L{list} of L{bytes}
|
||||
@param items: Aliases.
|
||||
|
||||
@type args: n-L{tuple} of (0) L{dict} mapping L{bytes} to L{IDomain}
|
||||
provider, (1) L{bytes}
|
||||
@param args: Arguments for L{AliasBase.__init__}.
|
||||
"""
|
||||
|
||||
AliasBase.__init__(self, *args)
|
||||
self.aliases = []
|
||||
while items:
|
||||
addr = items.pop().strip()
|
||||
if addr.startswith(":"):
|
||||
try:
|
||||
f = open(addr[1:])
|
||||
except BaseException:
|
||||
log.err(f"Invalid filename in alias file {addr[1:]!r}")
|
||||
else:
|
||||
with f:
|
||||
addr = " ".join([l.strip() for l in f])
|
||||
items.extend(addr.split(","))
|
||||
elif addr.startswith("|"):
|
||||
self.aliases.append(self.processAliasFactory(addr[1:], *args))
|
||||
elif addr.startswith("/"):
|
||||
if os.path.isdir(addr):
|
||||
log.err("Directory delivery not supported")
|
||||
else:
|
||||
self.aliases.append(FileAlias(addr, *args))
|
||||
else:
|
||||
self.aliases.append(AddressAlias(addr, *args))
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Return the number of aliases in the group.
|
||||
|
||||
@rtype: L{int}
|
||||
@return: The number of aliases in the group.
|
||||
"""
|
||||
return len(self.aliases)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Build a string representation of this L{AliasGroup} instance.
|
||||
|
||||
@rtype: L{bytes}
|
||||
@return: A string containing the aliases in the group.
|
||||
"""
|
||||
return "<AliasGroup [%s]>" % (", ".join(map(str, self.aliases)))
|
||||
|
||||
def createMessageReceiver(self):
|
||||
"""
|
||||
Create a message receiver for each alias and return a message receiver
|
||||
which will pass on a message to each of those.
|
||||
|
||||
@rtype: L{MultiWrapper}
|
||||
@return: A message receiver which passes a message on to message
|
||||
receivers for each alias in the group.
|
||||
"""
|
||||
return MultiWrapper([a.createMessageReceiver() for a in self.aliases])
|
||||
|
||||
def resolve(self, aliasmap, memo=None):
|
||||
"""
|
||||
Map each of the aliases in the group to its ultimate destination.
|
||||
|
||||
@type aliasmap: L{dict} mapping L{bytes} to L{AliasBase}
|
||||
@param aliasmap: A mapping of username to alias or group of aliases.
|
||||
|
||||
@type memo: L{None} or L{dict} of L{AliasBase}
|
||||
@param memo: A record of the aliases already considered in the
|
||||
resolution process. If provided, C{memo} is modified to include
|
||||
this alias.
|
||||
|
||||
@rtype: L{MultiWrapper}
|
||||
@return: A message receiver which passes the message on to message
|
||||
receivers for the ultimate destination of each alias in the group.
|
||||
"""
|
||||
if memo is None:
|
||||
memo = {}
|
||||
r = []
|
||||
for a in self.aliases:
|
||||
r.append(a.resolve(aliasmap, memo))
|
||||
return MultiWrapper(filter(None, r))
|
||||
107
backend/lib/python3.12/site-packages/twisted/mail/bounce.py
Normal file
107
backend/lib/python3.12/site-packages/twisted/mail/bounce.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# -*- test-case-name: twisted.mail.test.test_bounce -*-
|
||||
#
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""
|
||||
Support for bounce message generation.
|
||||
"""
|
||||
import email.utils
|
||||
import os
|
||||
import time
|
||||
from io import SEEK_END, SEEK_SET, StringIO
|
||||
|
||||
from twisted.mail import smtp
|
||||
|
||||
BOUNCE_FORMAT = """\
|
||||
From: postmaster@{failedDomain}
|
||||
To: {failedFrom}
|
||||
Subject: Returned Mail: see transcript for details
|
||||
Message-ID: {messageID}
|
||||
Content-Type: multipart/report; report-type=delivery-status;
|
||||
boundary="{boundary}"
|
||||
|
||||
--{boundary}
|
||||
|
||||
{transcript}
|
||||
|
||||
--{boundary}
|
||||
Content-Type: message/delivery-status
|
||||
Arrival-Date: {ctime}
|
||||
Final-Recipient: RFC822; {failedTo}
|
||||
"""
|
||||
|
||||
|
||||
def generateBounce(message, failedFrom, failedTo, transcript="", encoding="utf-8"):
|
||||
"""
|
||||
Generate a bounce message for an undeliverable email message.
|
||||
|
||||
@type message: a file-like object
|
||||
@param message: The undeliverable message.
|
||||
|
||||
@type failedFrom: L{bytes} or L{unicode}
|
||||
@param failedFrom: The originator of the undeliverable message.
|
||||
|
||||
@type failedTo: L{bytes} or L{unicode}
|
||||
@param failedTo: The destination of the undeliverable message.
|
||||
|
||||
@type transcript: L{bytes} or L{unicode}
|
||||
@param transcript: An error message to include in the bounce message.
|
||||
|
||||
@type encoding: L{str} or L{unicode}
|
||||
@param encoding: Encoding to use, default: utf-8
|
||||
|
||||
@rtype: 3-L{tuple} of (E{1}) L{bytes}, (E{2}) L{bytes}, (E{3}) L{bytes}
|
||||
@return: The originator, the destination and the contents of the bounce
|
||||
message. The destination of the bounce message is the originator of
|
||||
the undeliverable message.
|
||||
"""
|
||||
|
||||
if isinstance(failedFrom, bytes):
|
||||
failedFrom = failedFrom.decode(encoding)
|
||||
|
||||
if isinstance(failedTo, bytes):
|
||||
failedTo = failedTo.decode(encoding)
|
||||
|
||||
if not transcript:
|
||||
transcript = """\
|
||||
I'm sorry, the following address has permanent errors: {failedTo}.
|
||||
I've given up, and I will not retry the message again.
|
||||
""".format(
|
||||
failedTo=failedTo
|
||||
)
|
||||
|
||||
failedAddress = email.utils.parseaddr(failedTo)[1]
|
||||
data = {
|
||||
"boundary": "{}_{}_{}".format(time.time(), os.getpid(), "XXXXX"),
|
||||
"ctime": time.ctime(time.time()),
|
||||
"failedAddress": failedAddress,
|
||||
"failedDomain": failedAddress.split("@", 1)[1],
|
||||
"failedFrom": failedFrom,
|
||||
"failedTo": failedTo,
|
||||
"messageID": smtp.messageid(uniq="bounce"),
|
||||
"message": message,
|
||||
"transcript": transcript,
|
||||
}
|
||||
|
||||
fp = StringIO()
|
||||
fp.write(BOUNCE_FORMAT.format(**data))
|
||||
orig = message.tell()
|
||||
message.seek(0, SEEK_END)
|
||||
sz = message.tell()
|
||||
message.seek(orig, SEEK_SET)
|
||||
if sz > 10000:
|
||||
while 1:
|
||||
line = message.readline()
|
||||
if isinstance(line, bytes):
|
||||
line = line.decode(encoding)
|
||||
if len(line) <= 0:
|
||||
break
|
||||
fp.write(line)
|
||||
else:
|
||||
messageContent = message.read()
|
||||
if isinstance(messageContent, bytes):
|
||||
messageContent = messageContent.decode(encoding)
|
||||
fp.write(messageContent)
|
||||
return b"", failedFrom.encode(encoding), fp.getvalue().encode(encoding)
|
||||
6248
backend/lib/python3.12/site-packages/twisted/mail/imap4.py
Normal file
6248
backend/lib/python3.12/site-packages/twisted/mail/imap4.py
Normal file
File diff suppressed because it is too large
Load Diff
1050
backend/lib/python3.12/site-packages/twisted/mail/interfaces.py
Normal file
1050
backend/lib/python3.12/site-packages/twisted/mail/interfaces.py
Normal file
File diff suppressed because it is too large
Load Diff
706
backend/lib/python3.12/site-packages/twisted/mail/mail.py
Normal file
706
backend/lib/python3.12/site-packages/twisted/mail/mail.py
Normal file
@@ -0,0 +1,706 @@
|
||||
# -*- test-case-name: twisted.mail.test.test_mail -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Mail service support.
|
||||
"""
|
||||
|
||||
# System imports
|
||||
import os
|
||||
import warnings
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.application import internet, service
|
||||
from twisted.cred.portal import Portal
|
||||
|
||||
# Twisted imports
|
||||
from twisted.internet import defer
|
||||
|
||||
# Sibling imports
|
||||
from twisted.mail import protocols, smtp
|
||||
from twisted.mail.interfaces import IAliasableDomain, IDomain
|
||||
from twisted.python import log, util
|
||||
|
||||
|
||||
class DomainWithDefaultDict:
|
||||
"""
|
||||
A simulated dictionary for mapping domain names to domain objects with
|
||||
a default value for non-existing keys.
|
||||
|
||||
@ivar domains: See L{__init__}
|
||||
@ivar default: See L{__init__}
|
||||
"""
|
||||
|
||||
def __init__(self, domains, default):
|
||||
"""
|
||||
@type domains: L{dict} of L{bytes} -> L{IDomain} provider
|
||||
@param domains: A mapping of domain name to domain object.
|
||||
|
||||
@type default: L{IDomain} provider
|
||||
@param default: The default domain.
|
||||
"""
|
||||
self.domains = domains
|
||||
self.default = default
|
||||
|
||||
def setDefaultDomain(self, domain):
|
||||
"""
|
||||
Set the default domain.
|
||||
|
||||
@type domain: L{IDomain} provider
|
||||
@param domain: The default domain.
|
||||
"""
|
||||
self.default = domain
|
||||
|
||||
def has_key(self, name):
|
||||
"""
|
||||
Test for the presence of a domain name in this dictionary.
|
||||
|
||||
This always returns C{True} because a default value will be returned
|
||||
if the name doesn't exist in this dictionary.
|
||||
|
||||
@type name: L{bytes}
|
||||
@param name: A domain name.
|
||||
|
||||
@rtype: L{bool}
|
||||
@return: C{True} to indicate that the domain name is in this
|
||||
dictionary.
|
||||
"""
|
||||
warnings.warn(
|
||||
"twisted.mail.mail.DomainWithDefaultDict.has_key was deprecated "
|
||||
"in Twisted 16.3.0. "
|
||||
"Use the `in` keyword instead.",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
return 1
|
||||
|
||||
@classmethod
|
||||
def fromkeys(klass, keys, value=None):
|
||||
"""
|
||||
Create a new L{DomainWithDefaultDict} with the specified keys.
|
||||
|
||||
@type keys: iterable of L{bytes}
|
||||
@param keys: Domain names to serve as keys in the new dictionary.
|
||||
|
||||
@type value: L{None} or L{IDomain} provider
|
||||
@param value: A domain object to serve as the value for all new keys
|
||||
in the dictionary.
|
||||
|
||||
@rtype: L{DomainWithDefaultDict}
|
||||
@return: A new dictionary.
|
||||
"""
|
||||
d = klass()
|
||||
for k in keys:
|
||||
d[k] = value
|
||||
return d
|
||||
|
||||
def __contains__(self, name):
|
||||
"""
|
||||
Test for the presence of a domain name in this dictionary.
|
||||
|
||||
This always returns C{True} because a default value will be returned
|
||||
if the name doesn't exist in this dictionary.
|
||||
|
||||
@type name: L{bytes}
|
||||
@param name: A domain name.
|
||||
|
||||
@rtype: L{bool}
|
||||
@return: C{True} to indicate that the domain name is in this
|
||||
dictionary.
|
||||
"""
|
||||
return 1
|
||||
|
||||
def __getitem__(self, name):
|
||||
"""
|
||||
Look up a domain name and, if it is present, return the domain object
|
||||
associated with it. Otherwise return the default domain.
|
||||
|
||||
@type name: L{bytes}
|
||||
@param name: A domain name.
|
||||
|
||||
@rtype: L{IDomain} provider or L{None}
|
||||
@return: A domain object.
|
||||
"""
|
||||
return self.domains.get(name, self.default)
|
||||
|
||||
def __setitem__(self, name, value):
|
||||
"""
|
||||
Associate a domain object with a domain name in this dictionary.
|
||||
|
||||
@type name: L{bytes}
|
||||
@param name: A domain name.
|
||||
|
||||
@type value: L{IDomain} provider
|
||||
@param value: A domain object.
|
||||
"""
|
||||
self.domains[name] = value
|
||||
|
||||
def __delitem__(self, name):
|
||||
"""
|
||||
Delete the entry for a domain name in this dictionary.
|
||||
|
||||
@type name: L{bytes}
|
||||
@param name: A domain name.
|
||||
"""
|
||||
del self.domains[name]
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
Return an iterator over the domain names in this dictionary.
|
||||
|
||||
@rtype: iterator over L{bytes}
|
||||
@return: An iterator over the domain names.
|
||||
"""
|
||||
return iter(self.domains)
|
||||
|
||||
def __len__(self):
|
||||
"""
|
||||
Return the number of domains in this dictionary.
|
||||
|
||||
@rtype: L{int}
|
||||
@return: The number of domains in this dictionary.
|
||||
"""
|
||||
return len(self.domains)
|
||||
|
||||
def __str__(self) -> str:
|
||||
"""
|
||||
Build an informal string representation of this dictionary.
|
||||
|
||||
@rtype: L{bytes}
|
||||
@return: A string containing the mapping of domain names to domain
|
||||
objects.
|
||||
"""
|
||||
return f"<DomainWithDefaultDict {self.domains}>"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Build an "official" string representation of this dictionary.
|
||||
|
||||
@rtype: L{bytes}
|
||||
@return: A pseudo-executable string describing the underlying domain
|
||||
mapping of this object.
|
||||
"""
|
||||
return f"DomainWithDefaultDict({self.domains})"
|
||||
|
||||
def get(self, key, default=None):
|
||||
"""
|
||||
Look up a domain name in this dictionary.
|
||||
|
||||
@type key: L{bytes}
|
||||
@param key: A domain name.
|
||||
|
||||
@type default: L{IDomain} provider or L{None}
|
||||
@param default: A domain object to be returned if the domain name is
|
||||
not in this dictionary.
|
||||
|
||||
@rtype: L{IDomain} provider or L{None}
|
||||
@return: The domain object associated with the domain name if it is in
|
||||
this dictionary. Otherwise, the default value.
|
||||
"""
|
||||
return self.domains.get(key, default)
|
||||
|
||||
def copy(self):
|
||||
"""
|
||||
Make a copy of this dictionary.
|
||||
|
||||
@rtype: L{DomainWithDefaultDict}
|
||||
@return: A copy of this dictionary.
|
||||
"""
|
||||
return DomainWithDefaultDict(self.domains.copy(), self.default)
|
||||
|
||||
def iteritems(self):
|
||||
"""
|
||||
Return an iterator over the domain name/domain object pairs in the
|
||||
dictionary.
|
||||
|
||||
Using the returned iterator while adding or deleting entries from the
|
||||
dictionary may result in a L{RuntimeError} or failing to iterate over
|
||||
all the domain name/domain object pairs.
|
||||
|
||||
@rtype: iterator over 2-L{tuple} of (E{1}) L{bytes},
|
||||
(E{2}) L{IDomain} provider or L{None}
|
||||
@return: An iterator over the domain name/domain object pairs.
|
||||
"""
|
||||
return self.domains.iteritems()
|
||||
|
||||
def iterkeys(self):
|
||||
"""
|
||||
Return an iterator over the domain names in this dictionary.
|
||||
|
||||
Using the returned iterator while adding or deleting entries from the
|
||||
dictionary may result in a L{RuntimeError} or failing to iterate over
|
||||
all the domain names.
|
||||
|
||||
@rtype: iterator over L{bytes}
|
||||
@return: An iterator over the domain names.
|
||||
"""
|
||||
return self.domains.iterkeys()
|
||||
|
||||
def itervalues(self):
|
||||
"""
|
||||
Return an iterator over the domain objects in this dictionary.
|
||||
|
||||
Using the returned iterator while adding or deleting entries from the
|
||||
dictionary may result in a L{RuntimeError} or failing to iterate over
|
||||
all the domain objects.
|
||||
|
||||
@rtype: iterator over L{IDomain} provider or
|
||||
L{None}
|
||||
@return: An iterator over the domain objects.
|
||||
"""
|
||||
return self.domains.itervalues()
|
||||
|
||||
def keys(self):
|
||||
"""
|
||||
Return a list of all domain names in this dictionary.
|
||||
|
||||
@rtype: L{list} of L{bytes}
|
||||
@return: The domain names in this dictionary.
|
||||
|
||||
"""
|
||||
return self.domains.keys()
|
||||
|
||||
def values(self):
|
||||
"""
|
||||
Return a list of all domain objects in this dictionary.
|
||||
|
||||
@rtype: L{list} of L{IDomain} provider or L{None}
|
||||
@return: The domain objects in this dictionary.
|
||||
"""
|
||||
return self.domains.values()
|
||||
|
||||
def items(self):
|
||||
"""
|
||||
Return a list of all domain name/domain object pairs in this
|
||||
dictionary.
|
||||
|
||||
@rtype: L{list} of 2-L{tuple} of (E{1}) L{bytes}, (E{2}) L{IDomain}
|
||||
provider or L{None}
|
||||
@return: Domain name/domain object pairs in this dictionary.
|
||||
"""
|
||||
return self.domains.items()
|
||||
|
||||
def popitem(self):
|
||||
"""
|
||||
Remove a random domain name/domain object pair from this dictionary and
|
||||
return it as a tuple.
|
||||
|
||||
@rtype: 2-L{tuple} of (E{1}) L{bytes}, (E{2}) L{IDomain} provider or
|
||||
L{None}
|
||||
@return: A domain name/domain object pair.
|
||||
|
||||
@raise KeyError: When this dictionary is empty.
|
||||
"""
|
||||
return self.domains.popitem()
|
||||
|
||||
def update(self, other):
|
||||
"""
|
||||
Update this dictionary with domain name/domain object pairs from
|
||||
another dictionary.
|
||||
|
||||
When this dictionary contains a domain name which is in the other
|
||||
dictionary, its value will be overwritten.
|
||||
|
||||
@type other: L{dict} of L{bytes} -> L{IDomain} provider and/or
|
||||
L{bytes} -> L{None}
|
||||
@param other: Another dictionary of domain name/domain object pairs.
|
||||
|
||||
@rtype: L{None}
|
||||
@return: None.
|
||||
"""
|
||||
return self.domains.update(other)
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Remove all items from this dictionary.
|
||||
|
||||
@rtype: L{None}
|
||||
@return: None.
|
||||
"""
|
||||
return self.domains.clear()
|
||||
|
||||
def setdefault(self, key, default):
|
||||
"""
|
||||
Return the domain object associated with the domain name if it is
|
||||
present in this dictionary. Otherwise, set the value for the
|
||||
domain name to the default and return that value.
|
||||
|
||||
@type key: L{bytes}
|
||||
@param key: A domain name.
|
||||
|
||||
@type default: L{IDomain} provider
|
||||
@param default: A domain object.
|
||||
|
||||
@rtype: L{IDomain} provider or L{None}
|
||||
@return: The domain object associated with the domain name.
|
||||
"""
|
||||
return self.domains.setdefault(key, default)
|
||||
|
||||
|
||||
@implementer(IDomain)
|
||||
class BounceDomain:
|
||||
"""
|
||||
A domain with no users.
|
||||
|
||||
This can be used to block off a domain.
|
||||
"""
|
||||
|
||||
def exists(self, user):
|
||||
"""
|
||||
Raise an exception to indicate that the user does not exist in this
|
||||
domain.
|
||||
|
||||
@type user: L{User}
|
||||
@param user: A user.
|
||||
|
||||
@raise SMTPBadRcpt: When the given user does not exist in this domain.
|
||||
"""
|
||||
raise smtp.SMTPBadRcpt(user)
|
||||
|
||||
def willRelay(self, user, protocol):
|
||||
"""
|
||||
Indicate that this domain will not relay.
|
||||
|
||||
@type user: L{Address}
|
||||
@param user: The destination address.
|
||||
|
||||
@type protocol: L{Protocol <twisted.internet.protocol.Protocol>}
|
||||
@param protocol: The protocol over which the message to be relayed is
|
||||
being received.
|
||||
|
||||
@rtype: L{bool}
|
||||
@return: C{False}.
|
||||
"""
|
||||
return False
|
||||
|
||||
def addUser(self, user, password):
|
||||
"""
|
||||
Ignore attempts to add a user to this domain.
|
||||
|
||||
@type user: L{bytes}
|
||||
@param user: A username.
|
||||
|
||||
@type password: L{bytes}
|
||||
@param password: A password.
|
||||
"""
|
||||
pass
|
||||
|
||||
def getCredentialsCheckers(self):
|
||||
"""
|
||||
Return no credentials checkers for this domain.
|
||||
|
||||
@rtype: L{list}
|
||||
@return: The empty list.
|
||||
"""
|
||||
return []
|
||||
|
||||
|
||||
@implementer(smtp.IMessage)
|
||||
class FileMessage:
|
||||
"""
|
||||
A message receiver which delivers a message to a file.
|
||||
|
||||
@ivar fp: See L{__init__}.
|
||||
@ivar name: See L{__init__}.
|
||||
@ivar finalName: See L{__init__}.
|
||||
"""
|
||||
|
||||
def __init__(self, fp, name, finalName):
|
||||
"""
|
||||
@type fp: file-like object
|
||||
@param fp: The file in which to store the message while it is being
|
||||
received.
|
||||
|
||||
@type name: L{bytes}
|
||||
@param name: The full path name of the temporary file.
|
||||
|
||||
@type finalName: L{bytes}
|
||||
@param finalName: The full path name that should be given to the file
|
||||
holding the message after it has been fully received.
|
||||
"""
|
||||
self.fp = fp
|
||||
self.name = name
|
||||
self.finalName = finalName
|
||||
|
||||
def lineReceived(self, line):
|
||||
"""
|
||||
Write a received line to the file.
|
||||
|
||||
@type line: L{bytes}
|
||||
@param line: A received line.
|
||||
"""
|
||||
self.fp.write(line + b"\n")
|
||||
|
||||
def eomReceived(self):
|
||||
"""
|
||||
At the end of message, rename the file holding the message to its
|
||||
final name.
|
||||
|
||||
@rtype: L{Deferred} which successfully results in L{bytes}
|
||||
@return: A deferred which returns the final name of the file.
|
||||
"""
|
||||
self.fp.close()
|
||||
os.rename(self.name, self.finalName)
|
||||
return defer.succeed(self.finalName)
|
||||
|
||||
def connectionLost(self):
|
||||
"""
|
||||
Delete the file holding the partially received message.
|
||||
"""
|
||||
self.fp.close()
|
||||
os.remove(self.name)
|
||||
|
||||
|
||||
class MailService(service.MultiService):
|
||||
"""
|
||||
An email service.
|
||||
|
||||
@type queue: L{Queue} or L{None}
|
||||
@ivar queue: A queue for outgoing messages.
|
||||
|
||||
@type domains: L{dict} of L{bytes} -> L{IDomain} provider
|
||||
@ivar domains: A mapping of supported domain name to domain object.
|
||||
|
||||
@type portals: L{dict} of L{bytes} -> L{Portal}
|
||||
@ivar portals: A mapping of domain name to authentication portal.
|
||||
|
||||
@type aliases: L{None} or L{dict} of
|
||||
L{bytes} -> L{IAlias} provider
|
||||
@ivar aliases: A mapping of domain name to alias.
|
||||
|
||||
@type smtpPortal: L{Portal}
|
||||
@ivar smtpPortal: A portal for authentication for the SMTP server.
|
||||
|
||||
@type monitor: L{FileMonitoringService}
|
||||
@ivar monitor: A service to monitor changes to files.
|
||||
"""
|
||||
|
||||
queue = None
|
||||
domains = None
|
||||
portals = None
|
||||
aliases = None
|
||||
smtpPortal = None
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize the mail service.
|
||||
"""
|
||||
service.MultiService.__init__(self)
|
||||
# Domains and portals for "client" protocols - POP3, IMAP4, etc
|
||||
self.domains = DomainWithDefaultDict({}, BounceDomain())
|
||||
self.portals = {}
|
||||
|
||||
self.monitor = FileMonitoringService()
|
||||
self.monitor.setServiceParent(self)
|
||||
self.smtpPortal = Portal(self)
|
||||
|
||||
def getPOP3Factory(self):
|
||||
"""
|
||||
Create a POP3 protocol factory.
|
||||
|
||||
@rtype: L{POP3Factory}
|
||||
@return: A POP3 protocol factory.
|
||||
"""
|
||||
return protocols.POP3Factory(self)
|
||||
|
||||
def getSMTPFactory(self):
|
||||
"""
|
||||
Create an SMTP protocol factory.
|
||||
|
||||
@rtype: L{SMTPFactory <protocols.SMTPFactory>}
|
||||
@return: An SMTP protocol factory.
|
||||
"""
|
||||
return protocols.SMTPFactory(self, self.smtpPortal)
|
||||
|
||||
def getESMTPFactory(self):
|
||||
"""
|
||||
Create an ESMTP protocol factory.
|
||||
|
||||
@rtype: L{ESMTPFactory <protocols.ESMTPFactory>}
|
||||
@return: An ESMTP protocol factory.
|
||||
"""
|
||||
return protocols.ESMTPFactory(self, self.smtpPortal)
|
||||
|
||||
def addDomain(self, name, domain):
|
||||
"""
|
||||
Add a domain for which the service will accept email.
|
||||
|
||||
@type name: L{bytes}
|
||||
@param name: A domain name.
|
||||
|
||||
@type domain: L{IDomain} provider
|
||||
@param domain: A domain object.
|
||||
"""
|
||||
portal = Portal(domain)
|
||||
map(portal.registerChecker, domain.getCredentialsCheckers())
|
||||
self.domains[name] = domain
|
||||
self.portals[name] = portal
|
||||
if self.aliases and IAliasableDomain.providedBy(domain):
|
||||
domain.setAliasGroup(self.aliases)
|
||||
|
||||
def setQueue(self, queue):
|
||||
"""
|
||||
Set the queue for outgoing emails.
|
||||
|
||||
@type queue: L{Queue}
|
||||
@param queue: A queue for outgoing messages.
|
||||
"""
|
||||
self.queue = queue
|
||||
|
||||
def requestAvatar(self, avatarId, mind, *interfaces):
|
||||
"""
|
||||
Return a message delivery for an authenticated SMTP user.
|
||||
|
||||
@type avatarId: L{bytes}
|
||||
@param avatarId: A string which identifies an authenticated user.
|
||||
|
||||
@type mind: L{None}
|
||||
@param mind: Unused.
|
||||
|
||||
@type interfaces: n-L{tuple} of C{zope.interface.Interface}
|
||||
@param interfaces: A group of interfaces one of which the avatar must
|
||||
support.
|
||||
|
||||
@rtype: 3-L{tuple} of (E{1}) L{IMessageDelivery},
|
||||
(E{2}) L{ESMTPDomainDelivery}, (E{3}) no-argument callable
|
||||
@return: A tuple of the supported interface, a message delivery, and
|
||||
a logout function.
|
||||
|
||||
@raise NotImplementedError: When the given interfaces do not include
|
||||
L{IMessageDelivery}.
|
||||
"""
|
||||
if smtp.IMessageDelivery in interfaces:
|
||||
a = protocols.ESMTPDomainDelivery(self, avatarId)
|
||||
return smtp.IMessageDelivery, a, lambda: None
|
||||
raise NotImplementedError()
|
||||
|
||||
def lookupPortal(self, name):
|
||||
"""
|
||||
Find the portal for a domain.
|
||||
|
||||
@type name: L{bytes}
|
||||
@param name: A domain name.
|
||||
|
||||
@rtype: L{Portal}
|
||||
@return: A portal.
|
||||
"""
|
||||
return self.portals[name]
|
||||
|
||||
def defaultPortal(self):
|
||||
"""
|
||||
Return the portal for the default domain.
|
||||
|
||||
The default domain is named ''.
|
||||
|
||||
@rtype: L{Portal}
|
||||
@return: The portal for the default domain.
|
||||
"""
|
||||
return self.portals[""]
|
||||
|
||||
|
||||
class FileMonitoringService(internet.TimerService):
|
||||
"""
|
||||
A service for monitoring changes to files.
|
||||
|
||||
@type files: L{list} of L{list} of (E{1}) L{float}, (E{2}) L{bytes},
|
||||
(E{3}) callable which takes a L{bytes} argument, (E{4}) L{float}
|
||||
@ivar files: Information about files to be monitored. Each list entry
|
||||
provides the following information for a file: interval in seconds
|
||||
between checks, filename, callback function, time of last modification
|
||||
to the file.
|
||||
|
||||
@type intervals: L{_IntervalDifferentialIterator
|
||||
<twisted.python.util._IntervalDifferentialIterator>}
|
||||
@ivar intervals: Intervals between successive file checks.
|
||||
|
||||
@type _call: L{IDelayedCall <twisted.internet.interfaces.IDelayedCall>}
|
||||
provider
|
||||
@ivar _call: The next scheduled call to check a file.
|
||||
|
||||
@type index: L{int}
|
||||
@ivar index: The index of the next file to be checked.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Initialize the file monitoring service.
|
||||
"""
|
||||
self.files = []
|
||||
self.intervals = iter(util.IntervalDifferential([], 60))
|
||||
|
||||
def startService(self):
|
||||
"""
|
||||
Start the file monitoring service.
|
||||
"""
|
||||
service.Service.startService(self)
|
||||
self._setupMonitor()
|
||||
|
||||
def _setupMonitor(self):
|
||||
"""
|
||||
Schedule the next monitoring call.
|
||||
"""
|
||||
from twisted.internet import reactor
|
||||
|
||||
t, self.index = self.intervals.next()
|
||||
self._call = reactor.callLater(t, self._monitor)
|
||||
|
||||
def stopService(self):
|
||||
"""
|
||||
Stop the file monitoring service.
|
||||
"""
|
||||
service.Service.stopService(self)
|
||||
if self._call:
|
||||
self._call.cancel()
|
||||
self._call = None
|
||||
|
||||
def monitorFile(self, name, callback, interval=10):
|
||||
"""
|
||||
Start monitoring a file for changes.
|
||||
|
||||
@type name: L{bytes}
|
||||
@param name: The name of a file to monitor.
|
||||
|
||||
@type callback: callable which takes a L{bytes} argument
|
||||
@param callback: The function to call when the file has changed.
|
||||
|
||||
@type interval: L{float}
|
||||
@param interval: The interval in seconds between checks.
|
||||
"""
|
||||
try:
|
||||
mtime = os.path.getmtime(name)
|
||||
except BaseException:
|
||||
mtime = 0
|
||||
self.files.append([interval, name, callback, mtime])
|
||||
self.intervals.addInterval(interval)
|
||||
|
||||
def unmonitorFile(self, name):
|
||||
"""
|
||||
Stop monitoring a file.
|
||||
|
||||
@type name: L{bytes}
|
||||
@param name: A file name.
|
||||
"""
|
||||
for i in range(len(self.files)):
|
||||
if name == self.files[i][1]:
|
||||
self.intervals.removeInterval(self.files[i][0])
|
||||
del self.files[i]
|
||||
break
|
||||
|
||||
def _monitor(self):
|
||||
"""
|
||||
Monitor a file and make a callback if it has changed.
|
||||
"""
|
||||
self._call = None
|
||||
if self.index is not None:
|
||||
name, callback, mtime = self.files[self.index][1:]
|
||||
try:
|
||||
now = os.path.getmtime(name)
|
||||
except BaseException:
|
||||
now = 0
|
||||
if now > mtime:
|
||||
log.msg(f"{name} changed, notifying listener")
|
||||
self.files[self.index][3] = now
|
||||
callback(name)
|
||||
self._setupMonitor()
|
||||
910
backend/lib/python3.12/site-packages/twisted/mail/maildir.py
Normal file
910
backend/lib/python3.12/site-packages/twisted/mail/maildir.py
Normal file
@@ -0,0 +1,910 @@
|
||||
# -*- test-case-name: twisted.mail.test.test_mail -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""
|
||||
Maildir-style mailbox support.
|
||||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import socket
|
||||
import stat
|
||||
from hashlib import md5
|
||||
from typing import IO
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.cred import checkers, credentials, portal
|
||||
from twisted.cred.error import UnauthorizedLogin
|
||||
from twisted.internet import defer, interfaces, reactor
|
||||
from twisted.mail import mail, pop3, smtp
|
||||
from twisted.persisted import dirdbm
|
||||
from twisted.protocols import basic
|
||||
from twisted.python import failure, log
|
||||
|
||||
INTERNAL_ERROR = """\
|
||||
From: Twisted.mail Internals
|
||||
Subject: An Error Occurred
|
||||
|
||||
An internal server error has occurred. Please contact the
|
||||
server administrator.
|
||||
"""
|
||||
|
||||
|
||||
class _MaildirNameGenerator:
|
||||
"""
|
||||
A utility class to generate a unique maildir name.
|
||||
|
||||
@type n: L{int}
|
||||
@ivar n: A counter used to generate unique integers.
|
||||
|
||||
@type p: L{int}
|
||||
@ivar p: The ID of the current process.
|
||||
|
||||
@type s: L{bytes}
|
||||
@ivar s: A representation of the hostname.
|
||||
|
||||
@ivar _clock: See C{clock} parameter of L{__init__}.
|
||||
"""
|
||||
|
||||
n = 0
|
||||
p = os.getpid()
|
||||
s = socket.gethostname().replace("/", r"\057").replace(":", r"\072")
|
||||
|
||||
def __init__(self, clock):
|
||||
"""
|
||||
@type clock: L{IReactorTime <interfaces.IReactorTime>} provider
|
||||
@param clock: A reactor which will be used to learn the current time.
|
||||
"""
|
||||
self._clock = clock
|
||||
|
||||
def generate(self):
|
||||
"""
|
||||
Generate a string which is intended to be unique across all calls to
|
||||
this function (across all processes, reboots, etc).
|
||||
|
||||
Strings returned by earlier calls to this method will compare less
|
||||
than strings returned by later calls as long as the clock provided
|
||||
doesn't go backwards.
|
||||
|
||||
@rtype: L{bytes}
|
||||
@return: A unique string.
|
||||
"""
|
||||
self.n = self.n + 1
|
||||
t = self._clock.seconds()
|
||||
seconds = str(int(t))
|
||||
microseconds = "%07d" % (int((t - int(t)) * 10e6),)
|
||||
return f"{seconds}.M{microseconds}P{self.p}Q{self.n}.{self.s}"
|
||||
|
||||
|
||||
_generateMaildirName = _MaildirNameGenerator(reactor).generate
|
||||
|
||||
|
||||
def initializeMaildir(dir):
|
||||
"""
|
||||
Create a maildir user directory if it doesn't already exist.
|
||||
|
||||
@type dir: L{bytes}
|
||||
@param dir: The path name for a user directory.
|
||||
"""
|
||||
dir = os.fsdecode(dir)
|
||||
if not os.path.isdir(dir):
|
||||
os.mkdir(dir, 0o700)
|
||||
for subdir in ["new", "cur", "tmp", ".Trash"]:
|
||||
os.mkdir(os.path.join(dir, subdir), 0o700)
|
||||
for subdir in ["new", "cur", "tmp"]:
|
||||
os.mkdir(os.path.join(dir, ".Trash", subdir), 0o700)
|
||||
# touch
|
||||
open(os.path.join(dir, ".Trash", "maildirfolder"), "w").close()
|
||||
|
||||
|
||||
class MaildirMessage(mail.FileMessage):
|
||||
"""
|
||||
A message receiver which adds a header and delivers a message to a file
|
||||
whose name includes the size of the message.
|
||||
|
||||
@type size: L{int}
|
||||
@ivar size: The number of octets in the message.
|
||||
"""
|
||||
|
||||
size = None
|
||||
|
||||
def __init__(self, address, fp, *a, **kw):
|
||||
"""
|
||||
@type address: L{bytes}
|
||||
@param address: The address of the message recipient.
|
||||
|
||||
@type fp: file-like object
|
||||
@param fp: The file in which to store the message while it is being
|
||||
received.
|
||||
|
||||
@type a: 2-L{tuple} of (0) L{bytes}, (1) L{bytes}
|
||||
@param a: Positional arguments for L{FileMessage.__init__}.
|
||||
|
||||
@type kw: L{dict}
|
||||
@param kw: Keyword arguments for L{FileMessage.__init__}.
|
||||
"""
|
||||
header = b"Delivered-To: %s\n" % address
|
||||
fp.write(header)
|
||||
self.size = len(header)
|
||||
mail.FileMessage.__init__(self, fp, *a, **kw)
|
||||
|
||||
def lineReceived(self, line):
|
||||
"""
|
||||
Write a line to the file.
|
||||
|
||||
@type line: L{bytes}
|
||||
@param line: A received line.
|
||||
"""
|
||||
mail.FileMessage.lineReceived(self, line)
|
||||
self.size += len(line) + 1
|
||||
|
||||
def eomReceived(self):
|
||||
"""
|
||||
At the end of message, rename the file holding the message to its final
|
||||
name concatenated with the size of the file.
|
||||
|
||||
@rtype: L{Deferred <defer.Deferred>} which successfully results in
|
||||
L{bytes}
|
||||
@return: A deferred which returns the name of the file holding the
|
||||
message.
|
||||
"""
|
||||
self.finalName = self.finalName + ",S=%d" % self.size
|
||||
return mail.FileMessage.eomReceived(self)
|
||||
|
||||
|
||||
@implementer(mail.IAliasableDomain)
|
||||
class AbstractMaildirDomain:
|
||||
"""
|
||||
An abstract maildir-backed domain.
|
||||
|
||||
@type alias: L{None} or L{dict} mapping
|
||||
L{bytes} to L{AliasBase}
|
||||
@ivar alias: A mapping of username to alias.
|
||||
|
||||
@ivar root: See L{__init__}.
|
||||
"""
|
||||
|
||||
alias = None
|
||||
root = None
|
||||
|
||||
def __init__(self, service, root):
|
||||
"""
|
||||
@type service: L{MailService}
|
||||
@param service: An email service.
|
||||
|
||||
@type root: L{bytes}
|
||||
@param root: The maildir root directory.
|
||||
"""
|
||||
self.root = root
|
||||
|
||||
def userDirectory(self, user):
|
||||
"""
|
||||
Return the maildir directory for a user.
|
||||
|
||||
@type user: L{bytes}
|
||||
@param user: A username.
|
||||
|
||||
@rtype: L{bytes} or L{None}
|
||||
@return: The user's mail directory for a valid user. Otherwise,
|
||||
L{None}.
|
||||
"""
|
||||
return None
|
||||
|
||||
def setAliasGroup(self, alias):
|
||||
"""
|
||||
Set the group of defined aliases for this domain.
|
||||
|
||||
@type alias: L{dict} mapping L{bytes} to L{IAlias} provider.
|
||||
@param alias: A mapping of domain name to alias.
|
||||
"""
|
||||
self.alias = alias
|
||||
|
||||
def exists(self, user, memo=None):
|
||||
"""
|
||||
Check whether a user exists in this domain or an alias of it.
|
||||
|
||||
@type user: L{User}
|
||||
@param user: A user.
|
||||
|
||||
@type memo: L{None} or L{dict} of L{AliasBase}
|
||||
@param memo: A record of the addresses already considered while
|
||||
resolving aliases. The default value should be used by all
|
||||
external code.
|
||||
|
||||
@rtype: no-argument callable which returns L{IMessage <smtp.IMessage>}
|
||||
provider.
|
||||
@return: A function which takes no arguments and returns a message
|
||||
receiver for the user.
|
||||
|
||||
@raises SMTPBadRcpt: When the given user does not exist in this domain
|
||||
or an alias of it.
|
||||
"""
|
||||
if self.userDirectory(user.dest.local) is not None:
|
||||
return lambda: self.startMessage(user)
|
||||
try:
|
||||
a = self.alias[user.dest.local]
|
||||
except BaseException:
|
||||
raise smtp.SMTPBadRcpt(user)
|
||||
else:
|
||||
aliases = a.resolve(self.alias, memo)
|
||||
if aliases:
|
||||
return lambda: aliases
|
||||
log.err("Bad alias configuration: " + str(user))
|
||||
raise smtp.SMTPBadRcpt(user)
|
||||
|
||||
def startMessage(self, user):
|
||||
"""
|
||||
Create a maildir message for a user.
|
||||
|
||||
@type user: L{bytes}
|
||||
@param user: A username.
|
||||
|
||||
@rtype: L{MaildirMessage}
|
||||
@return: A message receiver for this user.
|
||||
"""
|
||||
if isinstance(user, str):
|
||||
name, domain = user.split("@", 1)
|
||||
else:
|
||||
name, domain = user.dest.local, user.dest.domain
|
||||
dir = self.userDirectory(name)
|
||||
fname = _generateMaildirName()
|
||||
filename = os.path.join(dir, "tmp", fname)
|
||||
fp = open(filename, "w")
|
||||
return MaildirMessage(
|
||||
f"{name}@{domain}", fp, filename, os.path.join(dir, "new", fname)
|
||||
)
|
||||
|
||||
def willRelay(self, user, protocol):
|
||||
"""
|
||||
Check whether this domain will relay.
|
||||
|
||||
@type user: L{Address}
|
||||
@param user: The destination address.
|
||||
|
||||
@type protocol: L{SMTP}
|
||||
@param protocol: The protocol over which the message to be relayed is
|
||||
being received.
|
||||
|
||||
@rtype: L{bool}
|
||||
@return: An indication of whether this domain will relay the message to
|
||||
the destination.
|
||||
"""
|
||||
return False
|
||||
|
||||
def addUser(self, user, password):
|
||||
"""
|
||||
Add a user to this domain.
|
||||
|
||||
Subclasses should override this method.
|
||||
|
||||
@type user: L{bytes}
|
||||
@param user: A username.
|
||||
|
||||
@type password: L{bytes}
|
||||
@param password: A password.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def getCredentialsCheckers(self):
|
||||
"""
|
||||
Return credentials checkers for this domain.
|
||||
|
||||
Subclasses should override this method.
|
||||
|
||||
@rtype: L{list} of L{ICredentialsChecker
|
||||
<checkers.ICredentialsChecker>} provider
|
||||
@return: Credentials checkers for this domain.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
@implementer(interfaces.IConsumer)
|
||||
class _MaildirMailboxAppendMessageTask:
|
||||
"""
|
||||
A task which adds a message to a maildir mailbox.
|
||||
|
||||
@ivar mbox: See L{__init__}.
|
||||
|
||||
@type defer: L{Deferred <defer.Deferred>} which successfully returns
|
||||
L{None}
|
||||
@ivar defer: A deferred which fires when the task has completed.
|
||||
|
||||
@type opencall: L{IDelayedCall <interfaces.IDelayedCall>} provider or
|
||||
L{None}
|
||||
@ivar opencall: A scheduled call to L{prodProducer}.
|
||||
|
||||
@type msg: file-like object
|
||||
@ivar msg: The message to add.
|
||||
|
||||
@type tmpname: L{bytes}
|
||||
@ivar tmpname: The pathname of the temporary file holding the message while
|
||||
it is being transferred.
|
||||
|
||||
@type fh: file
|
||||
@ivar fh: The new maildir file.
|
||||
|
||||
@type filesender: L{FileSender <basic.FileSender>}
|
||||
@ivar filesender: A file sender which sends the message.
|
||||
|
||||
@type myproducer: L{IProducer <interfaces.IProducer>}
|
||||
@ivar myproducer: The registered producer.
|
||||
|
||||
@type streaming: L{bool}
|
||||
@ivar streaming: Indicates whether the registered producer provides a
|
||||
streaming interface.
|
||||
"""
|
||||
|
||||
osopen = staticmethod(os.open)
|
||||
oswrite = staticmethod(os.write)
|
||||
osclose = staticmethod(os.close)
|
||||
osrename = staticmethod(os.rename)
|
||||
|
||||
def __init__(self, mbox, msg):
|
||||
"""
|
||||
@type mbox: L{MaildirMailbox}
|
||||
@param mbox: A maildir mailbox.
|
||||
|
||||
@type msg: L{bytes} or file-like object
|
||||
@param msg: The message to add.
|
||||
"""
|
||||
self.mbox = mbox
|
||||
self.defer = defer.Deferred()
|
||||
self.openCall = None
|
||||
if not hasattr(msg, "read"):
|
||||
msg = io.BytesIO(msg)
|
||||
self.msg = msg
|
||||
|
||||
def startUp(self):
|
||||
"""
|
||||
Start transferring the message to the mailbox.
|
||||
"""
|
||||
self.createTempFile()
|
||||
if self.fh != -1:
|
||||
self.filesender = basic.FileSender()
|
||||
self.filesender.beginFileTransfer(self.msg, self)
|
||||
|
||||
def registerProducer(self, producer, streaming):
|
||||
"""
|
||||
Register a producer and start asking it for data if it is
|
||||
non-streaming.
|
||||
|
||||
@type producer: L{IProducer <interfaces.IProducer>}
|
||||
@param producer: A producer.
|
||||
|
||||
@type streaming: L{bool}
|
||||
@param streaming: A flag indicating whether the producer provides a
|
||||
streaming interface.
|
||||
"""
|
||||
self.myproducer = producer
|
||||
self.streaming = streaming
|
||||
if not streaming:
|
||||
self.prodProducer()
|
||||
|
||||
def prodProducer(self):
|
||||
"""
|
||||
Repeatedly prod a non-streaming producer to produce data.
|
||||
"""
|
||||
self.openCall = None
|
||||
if self.myproducer is not None:
|
||||
self.openCall = reactor.callLater(0, self.prodProducer)
|
||||
self.myproducer.resumeProducing()
|
||||
|
||||
def unregisterProducer(self):
|
||||
"""
|
||||
Finish transferring the message to the mailbox.
|
||||
"""
|
||||
self.myproducer = None
|
||||
self.streaming = None
|
||||
self.osclose(self.fh)
|
||||
self.moveFileToNew()
|
||||
|
||||
def write(self, data):
|
||||
"""
|
||||
Write data to the maildir file.
|
||||
|
||||
@type data: L{bytes}
|
||||
@param data: Data to be written to the file.
|
||||
"""
|
||||
try:
|
||||
self.oswrite(self.fh, data)
|
||||
except BaseException:
|
||||
self.fail()
|
||||
|
||||
def fail(self, err=None):
|
||||
"""
|
||||
Fire the deferred to indicate the task completed with a failure.
|
||||
|
||||
@type err: L{Failure <failure.Failure>}
|
||||
@param err: The error that occurred.
|
||||
"""
|
||||
if err is None:
|
||||
err = failure.Failure()
|
||||
if self.openCall is not None:
|
||||
self.openCall.cancel()
|
||||
self.defer.errback(err)
|
||||
self.defer = None
|
||||
|
||||
def moveFileToNew(self):
|
||||
"""
|
||||
Place the message in the I{new/} directory, add it to the mailbox and
|
||||
fire the deferred to indicate that the task has completed
|
||||
successfully.
|
||||
"""
|
||||
while True:
|
||||
newname = os.path.join(self.mbox.path, "new", _generateMaildirName())
|
||||
try:
|
||||
self.osrename(self.tmpname, newname)
|
||||
break
|
||||
except OSError as e:
|
||||
(err, estr) = e.args
|
||||
import errno
|
||||
|
||||
# if the newname exists, retry with a new newname.
|
||||
if err != errno.EEXIST:
|
||||
self.fail()
|
||||
newname = None
|
||||
break
|
||||
if newname is not None:
|
||||
self.mbox.list.append(newname)
|
||||
self.defer.callback(None)
|
||||
self.defer = None
|
||||
|
||||
def createTempFile(self):
|
||||
"""
|
||||
Create a temporary file to hold the message as it is being transferred.
|
||||
"""
|
||||
attr = (
|
||||
os.O_RDWR
|
||||
| os.O_CREAT
|
||||
| os.O_EXCL
|
||||
| getattr(os, "O_NOINHERIT", 0)
|
||||
| getattr(os, "O_NOFOLLOW", 0)
|
||||
)
|
||||
tries = 0
|
||||
self.fh = -1
|
||||
while True:
|
||||
self.tmpname = os.path.join(self.mbox.path, "tmp", _generateMaildirName())
|
||||
try:
|
||||
self.fh = self.osopen(self.tmpname, attr, 0o600)
|
||||
return None
|
||||
except OSError:
|
||||
tries += 1
|
||||
if tries > 500:
|
||||
self.defer.errback(
|
||||
RuntimeError(
|
||||
"Could not create tmp file for %s" % self.mbox.path
|
||||
)
|
||||
)
|
||||
self.defer = None
|
||||
return None
|
||||
|
||||
|
||||
class MaildirMailbox(pop3.Mailbox):
|
||||
"""
|
||||
A maildir-backed mailbox.
|
||||
|
||||
@ivar path: See L{__init__}.
|
||||
|
||||
@type list: L{list} of L{int} or 2-L{tuple} of (0) file-like object,
|
||||
(1) L{bytes}
|
||||
@ivar list: Information about the messages in the mailbox. For undeleted
|
||||
messages, the file containing the message and the
|
||||
full path name of the file are stored. Deleted messages are indicated
|
||||
by 0.
|
||||
|
||||
@type deleted: L{dict} mapping 2-L{tuple} of (0) file-like object,
|
||||
(1) L{bytes} to L{bytes}
|
||||
@type deleted: A mapping of the information about a file before it was
|
||||
deleted to the full path name of the deleted file in the I{.Trash/}
|
||||
subfolder.
|
||||
"""
|
||||
|
||||
AppendFactory = _MaildirMailboxAppendMessageTask
|
||||
|
||||
def __init__(self, path):
|
||||
"""
|
||||
@type path: L{bytes}
|
||||
@param path: The directory name for a maildir mailbox.
|
||||
"""
|
||||
self.path = path
|
||||
self.list = []
|
||||
self.deleted = {}
|
||||
initializeMaildir(path)
|
||||
for name in ("cur", "new"):
|
||||
for file in os.listdir(os.path.join(path, name)):
|
||||
self.list.append((file, os.path.join(path, name, file)))
|
||||
self.list.sort()
|
||||
self.list = [e[1] for e in self.list]
|
||||
|
||||
def listMessages(self, i=None):
|
||||
"""
|
||||
Retrieve the size of a message, or, if none is specified, the size of
|
||||
each message in the mailbox.
|
||||
|
||||
@type i: L{int} or L{None}
|
||||
@param i: The 0-based index of a message.
|
||||
|
||||
@rtype: L{int} or L{list} of L{int}
|
||||
@return: The number of octets in the specified message, or, if an index
|
||||
is not specified, a list of the number of octets for all messages
|
||||
in the mailbox. Any value which corresponds to a deleted message
|
||||
is set to 0.
|
||||
|
||||
@raise IndexError: When the index does not correspond to a message in
|
||||
the mailbox.
|
||||
"""
|
||||
if i is None:
|
||||
ret = []
|
||||
for mess in self.list:
|
||||
if mess:
|
||||
ret.append(os.stat(mess)[stat.ST_SIZE])
|
||||
else:
|
||||
ret.append(0)
|
||||
return ret
|
||||
return self.list[i] and os.stat(self.list[i])[stat.ST_SIZE] or 0
|
||||
|
||||
def getMessage(self, i):
|
||||
"""
|
||||
Retrieve a file-like object with the contents of a message.
|
||||
|
||||
@type i: L{int}
|
||||
@param i: The 0-based index of a message.
|
||||
|
||||
@rtype: file-like object
|
||||
@return: A file containing the message.
|
||||
|
||||
@raise IndexError: When the index does not correspond to a message in
|
||||
the mailbox.
|
||||
"""
|
||||
return open(self.list[i])
|
||||
|
||||
def getUidl(self, i):
|
||||
"""
|
||||
Get a unique identifier for a message.
|
||||
|
||||
@type i: L{int}
|
||||
@param i: The 0-based index of a message.
|
||||
|
||||
@rtype: L{bytes}
|
||||
@return: A string of printable characters uniquely identifying the
|
||||
message for all time.
|
||||
|
||||
@raise IndexError: When the index does not correspond to a message in
|
||||
the mailbox.
|
||||
"""
|
||||
# Returning the actual filename is a mistake. Hash it.
|
||||
base = os.path.basename(self.list[i])
|
||||
return md5(base).hexdigest()
|
||||
|
||||
def deleteMessage(self, i):
|
||||
"""
|
||||
Mark a message for deletion.
|
||||
|
||||
Move the message to the I{.Trash/} subfolder so it can be undeleted
|
||||
by an administrator.
|
||||
|
||||
@type i: L{int}
|
||||
@param i: The 0-based index of a message.
|
||||
|
||||
@raise IndexError: When the index does not correspond to a message in
|
||||
the mailbox.
|
||||
"""
|
||||
trashFile = os.path.join(
|
||||
self.path, ".Trash", "cur", os.path.basename(self.list[i])
|
||||
)
|
||||
os.rename(self.list[i], trashFile)
|
||||
self.deleted[self.list[i]] = trashFile
|
||||
self.list[i] = 0
|
||||
|
||||
def undeleteMessages(self):
|
||||
"""
|
||||
Undelete all messages marked for deletion.
|
||||
|
||||
Move each message marked for deletion from the I{.Trash/} subfolder back
|
||||
to its original position.
|
||||
"""
|
||||
for real, trash in self.deleted.items():
|
||||
try:
|
||||
os.rename(trash, real)
|
||||
except OSError as e:
|
||||
(err, estr) = e.args
|
||||
import errno
|
||||
|
||||
# If the file has been deleted from disk, oh well!
|
||||
if err != errno.ENOENT:
|
||||
raise
|
||||
# This is a pass
|
||||
else:
|
||||
try:
|
||||
self.list[self.list.index(0)] = real
|
||||
except ValueError:
|
||||
self.list.append(real)
|
||||
self.deleted.clear()
|
||||
|
||||
def appendMessage(self, txt):
|
||||
"""
|
||||
Add a message to the mailbox.
|
||||
|
||||
@type txt: L{bytes} or file-like object
|
||||
@param txt: A message to add.
|
||||
|
||||
@rtype: L{Deferred <defer.Deferred>}
|
||||
@return: A deferred which fires when the message has been added to
|
||||
the mailbox.
|
||||
"""
|
||||
task = self.AppendFactory(self, txt)
|
||||
result = task.defer
|
||||
task.startUp()
|
||||
return result
|
||||
|
||||
|
||||
@implementer(pop3.IMailbox)
|
||||
class StringListMailbox:
|
||||
"""
|
||||
An in-memory mailbox.
|
||||
|
||||
@ivar msgs: See L{__init__}.
|
||||
|
||||
@type _delete: L{set} of L{int}
|
||||
@ivar _delete: The indices of messages which have been marked for deletion.
|
||||
"""
|
||||
|
||||
def __init__(self, msgs):
|
||||
"""
|
||||
@type msgs: L{list} of L{bytes}
|
||||
@param msgs: The contents of each message in the mailbox.
|
||||
"""
|
||||
self.msgs = msgs
|
||||
self._delete = set()
|
||||
|
||||
def listMessages(self, i=None):
|
||||
"""
|
||||
Retrieve the size of a message, or, if none is specified, the size of
|
||||
each message in the mailbox.
|
||||
|
||||
@type i: L{int} or L{None}
|
||||
@param i: The 0-based index of a message.
|
||||
|
||||
@rtype: L{int} or L{list} of L{int}
|
||||
@return: The number of octets in the specified message, or, if an index
|
||||
is not specified, a list of the number of octets in each message in
|
||||
the mailbox. Any value which corresponds to a deleted message is
|
||||
set to 0.
|
||||
|
||||
@raise IndexError: When the index does not correspond to a message in
|
||||
the mailbox.
|
||||
"""
|
||||
if i is None:
|
||||
return [self.listMessages(msg) for msg in range(len(self.msgs))]
|
||||
if i in self._delete:
|
||||
return 0
|
||||
return len(self.msgs[i])
|
||||
|
||||
def getMessage(self, i: int) -> IO[bytes]:
|
||||
"""
|
||||
Return an in-memory file-like object with the contents of a message.
|
||||
|
||||
@param i: The 0-based index of a message.
|
||||
|
||||
@return: An in-memory file-like object containing the message.
|
||||
|
||||
@raise IndexError: When the index does not correspond to a message in
|
||||
the mailbox.
|
||||
"""
|
||||
return io.BytesIO(self.msgs[i])
|
||||
|
||||
def getUidl(self, i):
|
||||
"""
|
||||
Get a unique identifier for a message.
|
||||
|
||||
@type i: L{int}
|
||||
@param i: The 0-based index of a message.
|
||||
|
||||
@rtype: L{bytes}
|
||||
@return: A hash of the contents of the message at the given index.
|
||||
|
||||
@raise IndexError: When the index does not correspond to a message in
|
||||
the mailbox.
|
||||
"""
|
||||
return md5(self.msgs[i]).hexdigest()
|
||||
|
||||
def deleteMessage(self, i):
|
||||
"""
|
||||
Mark a message for deletion.
|
||||
|
||||
@type i: L{int}
|
||||
@param i: The 0-based index of a message to delete.
|
||||
|
||||
@raise IndexError: When the index does not correspond to a message in
|
||||
the mailbox.
|
||||
"""
|
||||
self._delete.add(i)
|
||||
|
||||
def undeleteMessages(self):
|
||||
"""
|
||||
Undelete any messages which have been marked for deletion.
|
||||
"""
|
||||
self._delete = set()
|
||||
|
||||
def sync(self):
|
||||
"""
|
||||
Discard the contents of any messages marked for deletion.
|
||||
"""
|
||||
for index in self._delete:
|
||||
self.msgs[index] = ""
|
||||
self._delete = set()
|
||||
|
||||
|
||||
@implementer(portal.IRealm)
|
||||
class MaildirDirdbmDomain(AbstractMaildirDomain):
|
||||
"""
|
||||
A maildir-backed domain where membership is checked with a
|
||||
L{DirDBM <dirdbm.DirDBM>} database.
|
||||
|
||||
The directory structure of a MaildirDirdbmDomain is:
|
||||
|
||||
/passwd <-- a DirDBM directory
|
||||
|
||||
/USER/{cur, new, del} <-- each user has these three directories
|
||||
|
||||
@ivar postmaster: See L{__init__}.
|
||||
|
||||
@type dbm: L{DirDBM <dirdbm.DirDBM>}
|
||||
@ivar dbm: The authentication database for the domain.
|
||||
"""
|
||||
|
||||
portal = None
|
||||
_credcheckers = None
|
||||
|
||||
def __init__(self, service, root, postmaster=0):
|
||||
"""
|
||||
@type service: L{MailService}
|
||||
@param service: An email service.
|
||||
|
||||
@type root: L{bytes}
|
||||
@param root: The maildir root directory.
|
||||
|
||||
@type postmaster: L{bool}
|
||||
@param postmaster: A flag indicating whether non-existent addresses
|
||||
should be forwarded to the postmaster (C{True}) or
|
||||
bounced (C{False}).
|
||||
"""
|
||||
root = os.fsencode(root)
|
||||
AbstractMaildirDomain.__init__(self, service, root)
|
||||
dbm = os.path.join(root, b"passwd")
|
||||
if not os.path.exists(dbm):
|
||||
os.makedirs(dbm)
|
||||
self.dbm = dirdbm.open(dbm)
|
||||
self.postmaster = postmaster
|
||||
|
||||
def userDirectory(self, name):
|
||||
"""
|
||||
Return the path to a user's mail directory.
|
||||
|
||||
@type name: L{bytes}
|
||||
@param name: A username.
|
||||
|
||||
@rtype: L{bytes} or L{None}
|
||||
@return: The path to the user's mail directory for a valid user. For
|
||||
an invalid user, the path to the postmaster's mailbox if bounces
|
||||
are redirected there. Otherwise, L{None}.
|
||||
"""
|
||||
if name not in self.dbm:
|
||||
if not self.postmaster:
|
||||
return None
|
||||
name = "postmaster"
|
||||
dir = os.path.join(self.root, name)
|
||||
if not os.path.exists(dir):
|
||||
initializeMaildir(dir)
|
||||
return dir
|
||||
|
||||
def addUser(self, user, password):
|
||||
"""
|
||||
Add a user to this domain by adding an entry in the authentication
|
||||
database and initializing the user's mail directory.
|
||||
|
||||
@type user: L{bytes}
|
||||
@param user: A username.
|
||||
|
||||
@type password: L{bytes}
|
||||
@param password: A password.
|
||||
"""
|
||||
self.dbm[user] = password
|
||||
# Ensure it is initialized
|
||||
self.userDirectory(user)
|
||||
|
||||
def getCredentialsCheckers(self):
|
||||
"""
|
||||
Return credentials checkers for this domain.
|
||||
|
||||
@rtype: L{list} of L{ICredentialsChecker
|
||||
<checkers.ICredentialsChecker>} provider
|
||||
@return: Credentials checkers for this domain.
|
||||
"""
|
||||
if self._credcheckers is None:
|
||||
self._credcheckers = [DirdbmDatabase(self.dbm)]
|
||||
return self._credcheckers
|
||||
|
||||
def requestAvatar(self, avatarId, mind, *interfaces):
|
||||
"""
|
||||
Get the mailbox for an authenticated user.
|
||||
|
||||
The mailbox for the authenticated user will be returned only if the
|
||||
given interfaces include L{IMailbox <pop3.IMailbox>}. Requests for
|
||||
anonymous access will be met with a mailbox containing a message
|
||||
indicating that an internal error has occurred.
|
||||
|
||||
@type avatarId: L{bytes} or C{twisted.cred.checkers.ANONYMOUS}
|
||||
@param avatarId: A string which identifies a user or an object which
|
||||
signals a request for anonymous access.
|
||||
|
||||
@type mind: L{None}
|
||||
@param mind: Unused.
|
||||
|
||||
@type interfaces: n-L{tuple} of C{zope.interface.Interface}
|
||||
@param interfaces: A group of interfaces, one of which the avatar
|
||||
must support.
|
||||
|
||||
@rtype: 3-L{tuple} of (0) L{IMailbox <pop3.IMailbox>},
|
||||
(1) L{IMailbox <pop3.IMailbox>} provider, (2) no-argument
|
||||
callable
|
||||
@return: A tuple of the supported interface, a mailbox, and a
|
||||
logout function.
|
||||
|
||||
@raise NotImplementedError: When the given interfaces do not include
|
||||
L{IMailbox <pop3.IMailbox>}.
|
||||
"""
|
||||
if pop3.IMailbox not in interfaces:
|
||||
raise NotImplementedError("No interface")
|
||||
if avatarId == checkers.ANONYMOUS:
|
||||
mbox = StringListMailbox([INTERNAL_ERROR])
|
||||
else:
|
||||
mbox = MaildirMailbox(os.path.join(self.root, avatarId))
|
||||
|
||||
return (pop3.IMailbox, mbox, lambda: None)
|
||||
|
||||
|
||||
@implementer(checkers.ICredentialsChecker)
|
||||
class DirdbmDatabase:
|
||||
"""
|
||||
A credentials checker which authenticates users out of a
|
||||
L{DirDBM <dirdbm.DirDBM>} database.
|
||||
|
||||
@type dirdbm: L{DirDBM <dirdbm.DirDBM>}
|
||||
@ivar dirdbm: An authentication database.
|
||||
"""
|
||||
|
||||
# credentialInterfaces is not used by the class
|
||||
credentialInterfaces = (
|
||||
credentials.IUsernamePassword,
|
||||
credentials.IUsernameHashedPassword,
|
||||
)
|
||||
|
||||
def __init__(self, dbm):
|
||||
"""
|
||||
@type dbm: L{DirDBM <dirdbm.DirDBM>}
|
||||
@param dbm: An authentication database.
|
||||
"""
|
||||
self.dirdbm = dbm
|
||||
|
||||
def requestAvatarId(self, c):
|
||||
"""
|
||||
Authenticate a user and, if successful, return their username.
|
||||
|
||||
@type c: L{IUsernamePassword <credentials.IUsernamePassword>} or
|
||||
L{IUsernameHashedPassword <credentials.IUsernameHashedPassword>}
|
||||
provider.
|
||||
@param c: Credentials.
|
||||
|
||||
@rtype: L{bytes}
|
||||
@return: A string which identifies an user.
|
||||
|
||||
@raise UnauthorizedLogin: When the credentials check fails.
|
||||
"""
|
||||
if c.username in self.dirdbm:
|
||||
if c.checkPassword(self.dirdbm[c.username]):
|
||||
return c.username
|
||||
raise UnauthorizedLogin()
|
||||
1
backend/lib/python3.12/site-packages/twisted/mail/newsfragments/.gitignore
vendored
Normal file
1
backend/lib/python3.12/site-packages/twisted/mail/newsfragments/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
!.gitignore
|
||||
117
backend/lib/python3.12/site-packages/twisted/mail/pb.py
Normal file
117
backend/lib/python3.12/site-packages/twisted/mail/pb.py
Normal file
@@ -0,0 +1,117 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
import os
|
||||
|
||||
from twisted.spread import pb
|
||||
|
||||
|
||||
class Maildir(pb.Referenceable):
|
||||
def __init__(self, directory, rootDirectory):
|
||||
self.virtualDirectory = directory
|
||||
self.rootDirectory = rootDirectory
|
||||
self.directory = os.path.join(rootDirectory, directory)
|
||||
|
||||
def getFolderMessage(self, folder, name):
|
||||
if "/" in name:
|
||||
raise OSError("can only open files in '%s' directory'" % folder)
|
||||
with open(os.path.join(self.directory, "new", name)) as fp:
|
||||
return fp.read()
|
||||
|
||||
def deleteFolderMessage(self, folder, name):
|
||||
if "/" in name:
|
||||
raise OSError("can only delete files in '%s' directory'" % folder)
|
||||
os.rename(
|
||||
os.path.join(self.directory, folder, name),
|
||||
os.path.join(self.rootDirectory, ".Trash", folder, name),
|
||||
)
|
||||
|
||||
def deleteNewMessage(self, name):
|
||||
return self.deleteFolderMessage("new", name)
|
||||
|
||||
remote_deleteNewMessage = deleteNewMessage
|
||||
|
||||
def deleteCurMessage(self, name):
|
||||
return self.deleteFolderMessage("cur", name)
|
||||
|
||||
remote_deleteCurMessage = deleteCurMessage
|
||||
|
||||
def getNewMessages(self):
|
||||
return os.listdir(os.path.join(self.directory, "new"))
|
||||
|
||||
remote_getNewMessages = getNewMessages
|
||||
|
||||
def getCurMessages(self):
|
||||
return os.listdir(os.path.join(self.directory, "cur"))
|
||||
|
||||
remote_getCurMessages = getCurMessages
|
||||
|
||||
def getNewMessage(self, name):
|
||||
return self.getFolderMessage("new", name)
|
||||
|
||||
remote_getNewMessage = getNewMessage
|
||||
|
||||
def getCurMessage(self, name):
|
||||
return self.getFolderMessage("cur", name)
|
||||
|
||||
remote_getCurMessage = getCurMessage
|
||||
|
||||
def getSubFolder(self, name):
|
||||
if name[0] == ".":
|
||||
raise OSError("subfolder name cannot begin with a '.'")
|
||||
name = name.replace("/", ":")
|
||||
if self.virtualDirectoy == ".":
|
||||
name = "." + name
|
||||
else:
|
||||
name = self.virtualDirectory + ":" + name
|
||||
if not self._isSubFolder(name):
|
||||
raise OSError("not a subfolder")
|
||||
return Maildir(name, self.rootDirectory)
|
||||
|
||||
remote_getSubFolder = getSubFolder
|
||||
|
||||
def _isSubFolder(self, name):
|
||||
return not os.path.isdir(
|
||||
os.path.join(self.rootDirectory, name)
|
||||
) or not os.path.isfile(os.path.join(self.rootDirectory, name, "maildirfolder"))
|
||||
|
||||
|
||||
class MaildirCollection(pb.Referenceable):
|
||||
def __init__(self, root):
|
||||
self.root = root
|
||||
|
||||
def getSubFolders(self):
|
||||
return os.listdir(self.getRoot())
|
||||
|
||||
remote_getSubFolders = getSubFolders
|
||||
|
||||
def getSubFolder(self, name):
|
||||
if "/" in name or name[0] == ".":
|
||||
raise OSError("invalid name")
|
||||
return Maildir(".", os.path.join(self.getRoot(), name))
|
||||
|
||||
remote_getSubFolder = getSubFolder
|
||||
|
||||
|
||||
class MaildirBroker(pb.Broker):
|
||||
def proto_getCollection(self, requestID, name, domain, password):
|
||||
collection = self._getCollection()
|
||||
if collection is None:
|
||||
self.sendError(requestID, "permission denied")
|
||||
else:
|
||||
self.sendAnswer(requestID, collection)
|
||||
|
||||
def getCollection(self, name, domain, password):
|
||||
if domain not in self.domains:
|
||||
return
|
||||
domain = self.domains[domain]
|
||||
if name in domain.dbm and domain.dbm[name] == password:
|
||||
return MaildirCollection(domain.userDirectory(name))
|
||||
|
||||
|
||||
class MaildirClient(pb.Broker):
|
||||
def getCollection(self, name, domain, password, callback, errback):
|
||||
requestID = self.newRequestID()
|
||||
self.waitingForAnswers[requestID] = callback, errback
|
||||
self.sendCall("getCollection", requestID, name, domain, password)
|
||||
1704
backend/lib/python3.12/site-packages/twisted/mail/pop3.py
Normal file
1704
backend/lib/python3.12/site-packages/twisted/mail/pop3.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Deprecated POP3 client protocol implementation.
|
||||
|
||||
Don't use this module directly. Use twisted.mail.pop3 instead.
|
||||
"""
|
||||
import warnings
|
||||
from typing import List
|
||||
|
||||
from twisted.mail._pop3client import ERR, OK, POP3Client
|
||||
|
||||
warnings.warn(
|
||||
"twisted.mail.pop3client was deprecated in Twisted 21.2.0. Use twisted.mail.pop3 instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
|
||||
# Fake usage to please pyflakes as we don't to add them to __all__.
|
||||
OK
|
||||
ERR
|
||||
POP3Client
|
||||
|
||||
__all__: List[str] = []
|
||||
385
backend/lib/python3.12/site-packages/twisted/mail/protocols.py
Normal file
385
backend/lib/python3.12/site-packages/twisted/mail/protocols.py
Normal file
@@ -0,0 +1,385 @@
|
||||
# -*- test-case-name: twisted.mail.test.test_mail -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Mail protocol support.
|
||||
"""
|
||||
|
||||
|
||||
from zope.interface import implementer
|
||||
|
||||
from twisted.copyright import longversion
|
||||
from twisted.cred.credentials import CramMD5Credentials, UsernamePassword
|
||||
from twisted.cred.error import UnauthorizedLogin
|
||||
from twisted.internet import defer, protocol
|
||||
from twisted.mail import pop3, relay, smtp
|
||||
from twisted.python import log
|
||||
|
||||
|
||||
@implementer(smtp.IMessageDelivery)
|
||||
class DomainDeliveryBase:
|
||||
"""
|
||||
A base class for message delivery using the domains of a mail service.
|
||||
|
||||
@ivar service: See L{__init__}
|
||||
@ivar user: See L{__init__}
|
||||
@ivar host: See L{__init__}
|
||||
|
||||
@type protocolName: L{bytes}
|
||||
@ivar protocolName: The protocol being used to deliver the mail.
|
||||
Sub-classes should set this appropriately.
|
||||
"""
|
||||
|
||||
service = None
|
||||
protocolName: bytes = b"not-implemented-protocol"
|
||||
|
||||
def __init__(self, service, user, host=smtp.DNSNAME):
|
||||
"""
|
||||
@type service: L{MailService}
|
||||
@param service: A mail service.
|
||||
|
||||
@type user: L{bytes} or L{None}
|
||||
@param user: The authenticated SMTP user.
|
||||
|
||||
@type host: L{bytes}
|
||||
@param host: The hostname.
|
||||
"""
|
||||
self.service = service
|
||||
self.user = user
|
||||
self.host = host
|
||||
|
||||
def receivedHeader(self, helo, origin, recipients):
|
||||
"""
|
||||
Generate a received header string for a message.
|
||||
|
||||
@type helo: 2-L{tuple} of (L{bytes}, L{bytes})
|
||||
@param helo: The client's identity as sent in the HELO command and its
|
||||
IP address.
|
||||
|
||||
@type origin: L{Address}
|
||||
@param origin: The origination address of the message.
|
||||
|
||||
@type recipients: L{list} of L{User}
|
||||
@param recipients: The destination addresses for the message.
|
||||
|
||||
@rtype: L{bytes}
|
||||
@return: A received header string.
|
||||
"""
|
||||
authStr = heloStr = b""
|
||||
if self.user:
|
||||
authStr = b" auth=" + self.user.encode("xtext")
|
||||
if helo[0]:
|
||||
heloStr = b" helo=" + helo[0]
|
||||
fromUser = b"from " + helo[0] + b" ([" + helo[1] + b"]" + heloStr + authStr
|
||||
by = (
|
||||
b"by "
|
||||
+ self.host
|
||||
+ b" with "
|
||||
+ self.protocolName
|
||||
+ b" ("
|
||||
+ longversion.encode("ascii")
|
||||
+ b")"
|
||||
)
|
||||
forUser = (
|
||||
b"for <" + b" ".join(map(bytes, recipients)) + b"> " + smtp.rfc822date()
|
||||
)
|
||||
return b"Received: " + fromUser + b"\n\t" + by + b"\n\t" + forUser
|
||||
|
||||
def validateTo(self, user):
|
||||
"""
|
||||
Validate the address for which a message is destined.
|
||||
|
||||
@type user: L{User}
|
||||
@param user: The destination address.
|
||||
|
||||
@rtype: L{Deferred <defer.Deferred>} which successfully fires with
|
||||
no-argument callable which returns L{IMessage <smtp.IMessage>}
|
||||
provider.
|
||||
@return: A deferred which successfully fires with a no-argument
|
||||
callable which returns a message receiver for the destination.
|
||||
|
||||
@raise SMTPBadRcpt: When messages cannot be accepted for the
|
||||
destination address.
|
||||
"""
|
||||
# XXX - Yick. This needs cleaning up.
|
||||
if self.user and self.service.queue:
|
||||
d = self.service.domains.get(user.dest.domain, None)
|
||||
if d is None:
|
||||
d = relay.DomainQueuer(self.service, True)
|
||||
else:
|
||||
d = self.service.domains[user.dest.domain]
|
||||
return defer.maybeDeferred(d.exists, user)
|
||||
|
||||
def validateFrom(self, helo, origin):
|
||||
"""
|
||||
Validate the address from which a message originates.
|
||||
|
||||
@type helo: 2-L{tuple} of (L{bytes}, L{bytes})
|
||||
@param helo: The client's identity as sent in the HELO command and its
|
||||
IP address.
|
||||
|
||||
@type origin: L{Address}
|
||||
@param origin: The origination address of the message.
|
||||
|
||||
@rtype: L{Address}
|
||||
@return: The origination address.
|
||||
|
||||
@raise SMTPBadSender: When messages cannot be accepted from the
|
||||
origination address.
|
||||
"""
|
||||
if not helo:
|
||||
raise smtp.SMTPBadSender(origin, 503, "Who are you? Say HELO first.")
|
||||
if origin.local != b"" and origin.domain == b"":
|
||||
raise smtp.SMTPBadSender(origin, 501, "Sender address must contain domain.")
|
||||
return origin
|
||||
|
||||
|
||||
class SMTPDomainDelivery(DomainDeliveryBase):
|
||||
"""
|
||||
A domain delivery base class for use in an SMTP server.
|
||||
"""
|
||||
|
||||
protocolName = b"smtp"
|
||||
|
||||
|
||||
class ESMTPDomainDelivery(DomainDeliveryBase):
|
||||
"""
|
||||
A domain delivery base class for use in an ESMTP server.
|
||||
"""
|
||||
|
||||
protocolName = b"esmtp"
|
||||
|
||||
|
||||
class SMTPFactory(smtp.SMTPFactory):
|
||||
"""
|
||||
An SMTP server protocol factory.
|
||||
|
||||
@ivar service: See L{__init__}
|
||||
@ivar portal: See L{__init__}
|
||||
|
||||
@type protocol: no-argument callable which returns a L{Protocol
|
||||
<protocol.Protocol>} subclass
|
||||
@ivar protocol: A callable which creates a protocol. The default value is
|
||||
L{SMTP}.
|
||||
"""
|
||||
|
||||
protocol = smtp.SMTP
|
||||
portal = None
|
||||
|
||||
def __init__(self, service, portal=None):
|
||||
"""
|
||||
@type service: L{MailService}
|
||||
@param service: An email service.
|
||||
|
||||
@type portal: L{Portal <twisted.cred.portal.Portal>} or
|
||||
L{None}
|
||||
@param portal: A portal to use for authentication.
|
||||
"""
|
||||
smtp.SMTPFactory.__init__(self)
|
||||
self.service = service
|
||||
self.portal = portal
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
"""
|
||||
Create an instance of an SMTP server protocol.
|
||||
|
||||
@type addr: L{IAddress <twisted.internet.interfaces.IAddress>} provider
|
||||
@param addr: The address of the SMTP client.
|
||||
|
||||
@rtype: L{SMTP}
|
||||
@return: An SMTP protocol.
|
||||
"""
|
||||
log.msg(f"Connection from {addr}")
|
||||
p = smtp.SMTPFactory.buildProtocol(self, addr)
|
||||
p.service = self.service
|
||||
p.portal = self.portal
|
||||
return p
|
||||
|
||||
|
||||
class ESMTPFactory(SMTPFactory):
|
||||
"""
|
||||
An ESMTP server protocol factory.
|
||||
|
||||
@type protocol: no-argument callable which returns a L{Protocol
|
||||
<protocol.Protocol>} subclass
|
||||
@ivar protocol: A callable which creates a protocol. The default value is
|
||||
L{ESMTP}.
|
||||
|
||||
@type context: L{IOpenSSLContextFactory
|
||||
<twisted.internet.interfaces.IOpenSSLContextFactory>} or L{None}
|
||||
@ivar context: A factory to generate contexts to be used in negotiating
|
||||
encrypted communication.
|
||||
|
||||
@type challengers: L{dict} mapping L{bytes} to no-argument callable which
|
||||
returns L{ICredentials <twisted.cred.credentials.ICredentials>}
|
||||
subclass provider.
|
||||
@ivar challengers: A mapping of acceptable authorization mechanism to
|
||||
callable which creates credentials to use for authentication.
|
||||
"""
|
||||
|
||||
protocol = smtp.ESMTP
|
||||
context = None
|
||||
|
||||
def __init__(self, *args):
|
||||
"""
|
||||
@param args: Arguments for L{SMTPFactory.__init__}
|
||||
|
||||
@see: L{SMTPFactory.__init__}
|
||||
"""
|
||||
SMTPFactory.__init__(self, *args)
|
||||
self.challengers = {b"CRAM-MD5": CramMD5Credentials}
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
"""
|
||||
Create an instance of an ESMTP server protocol.
|
||||
|
||||
@type addr: L{IAddress <twisted.internet.interfaces.IAddress>} provider
|
||||
@param addr: The address of the ESMTP client.
|
||||
|
||||
@rtype: L{ESMTP}
|
||||
@return: An ESMTP protocol.
|
||||
"""
|
||||
p = SMTPFactory.buildProtocol(self, addr)
|
||||
p.challengers = self.challengers
|
||||
p.ctx = self.context
|
||||
return p
|
||||
|
||||
|
||||
class VirtualPOP3(pop3.POP3):
|
||||
"""
|
||||
A virtual hosting POP3 server.
|
||||
|
||||
@type service: L{MailService}
|
||||
@ivar service: The email service that created this server. This must be
|
||||
set by the service.
|
||||
|
||||
@type domainSpecifier: L{bytes}
|
||||
@ivar domainSpecifier: The character to use to split an email address into
|
||||
local-part and domain. The default is '@'.
|
||||
"""
|
||||
|
||||
service = None
|
||||
|
||||
domainSpecifier = b"@" # Gaagh! I hate POP3. No standardized way
|
||||
# to indicate user@host. '@' doesn't work
|
||||
# with NS, e.g.
|
||||
|
||||
def authenticateUserAPOP(self, user, digest):
|
||||
"""
|
||||
Perform APOP authentication.
|
||||
|
||||
Override the default lookup scheme to allow virtual domains.
|
||||
|
||||
@type user: L{bytes}
|
||||
@param user: The name of the user attempting to log in.
|
||||
|
||||
@type digest: L{bytes}
|
||||
@param digest: The challenge response.
|
||||
|
||||
@rtype: L{Deferred} which successfully results in 3-L{tuple} of
|
||||
(L{IMailbox <pop3.IMailbox>}, L{IMailbox <pop3.IMailbox>}
|
||||
provider, no-argument callable)
|
||||
@return: A deferred which fires when authentication is complete.
|
||||
If successful, it returns an L{IMailbox <pop3.IMailbox>} interface,
|
||||
a mailbox and a logout function. If authentication fails, the
|
||||
deferred fails with an L{UnauthorizedLogin
|
||||
<twisted.cred.error.UnauthorizedLogin>} error.
|
||||
"""
|
||||
user, domain = self.lookupDomain(user)
|
||||
try:
|
||||
portal = self.service.lookupPortal(domain)
|
||||
except KeyError:
|
||||
return defer.fail(UnauthorizedLogin())
|
||||
else:
|
||||
return portal.login(
|
||||
pop3.APOPCredentials(self.magic, user, digest), None, pop3.IMailbox
|
||||
)
|
||||
|
||||
def authenticateUserPASS(self, user, password):
|
||||
"""
|
||||
Perform authentication for a username/password login.
|
||||
|
||||
Override the default lookup scheme to allow virtual domains.
|
||||
|
||||
@type user: L{bytes}
|
||||
@param user: The name of the user attempting to log in.
|
||||
|
||||
@type password: L{bytes}
|
||||
@param password: The password to authenticate with.
|
||||
|
||||
@rtype: L{Deferred} which successfully results in 3-L{tuple} of
|
||||
(L{IMailbox <pop3.IMailbox>}, L{IMailbox <pop3.IMailbox>}
|
||||
provider, no-argument callable)
|
||||
@return: A deferred which fires when authentication is complete.
|
||||
If successful, it returns an L{IMailbox <pop3.IMailbox>} interface,
|
||||
a mailbox and a logout function. If authentication fails, the
|
||||
deferred fails with an L{UnauthorizedLogin
|
||||
<twisted.cred.error.UnauthorizedLogin>} error.
|
||||
"""
|
||||
user, domain = self.lookupDomain(user)
|
||||
try:
|
||||
portal = self.service.lookupPortal(domain)
|
||||
except KeyError:
|
||||
return defer.fail(UnauthorizedLogin())
|
||||
else:
|
||||
return portal.login(UsernamePassword(user, password), None, pop3.IMailbox)
|
||||
|
||||
def lookupDomain(self, user):
|
||||
"""
|
||||
Check whether a domain is among the virtual domains supported by the
|
||||
mail service.
|
||||
|
||||
@type user: L{bytes}
|
||||
@param user: An email address.
|
||||
|
||||
@rtype: 2-L{tuple} of (L{bytes}, L{bytes})
|
||||
@return: The local part and the domain part of the email address if the
|
||||
domain is supported.
|
||||
|
||||
@raise POP3Error: When the domain is not supported by the mail service.
|
||||
"""
|
||||
try:
|
||||
user, domain = user.split(self.domainSpecifier, 1)
|
||||
except ValueError:
|
||||
domain = b""
|
||||
if domain not in self.service.domains:
|
||||
raise pop3.POP3Error("no such domain {}".format(domain.decode("utf-8")))
|
||||
return user, domain
|
||||
|
||||
|
||||
class POP3Factory(protocol.ServerFactory):
|
||||
"""
|
||||
A POP3 server protocol factory.
|
||||
|
||||
@ivar service: See L{__init__}
|
||||
|
||||
@type protocol: no-argument callable which returns a L{Protocol
|
||||
<protocol.Protocol>} subclass
|
||||
@ivar protocol: A callable which creates a protocol. The default value is
|
||||
L{VirtualPOP3}.
|
||||
"""
|
||||
|
||||
protocol = VirtualPOP3
|
||||
service = None
|
||||
|
||||
def __init__(self, service):
|
||||
"""
|
||||
@type service: L{MailService}
|
||||
@param service: An email service.
|
||||
"""
|
||||
self.service = service
|
||||
|
||||
def buildProtocol(self, addr):
|
||||
"""
|
||||
Create an instance of a POP3 server protocol.
|
||||
|
||||
@type addr: L{IAddress <twisted.internet.interfaces.IAddress>} provider
|
||||
@param addr: The address of the POP3 client.
|
||||
|
||||
@rtype: L{POP3}
|
||||
@return: A POP3 protocol.
|
||||
"""
|
||||
p = protocol.ServerFactory.buildProtocol(self, addr)
|
||||
p.service = self.service
|
||||
return p
|
||||
164
backend/lib/python3.12/site-packages/twisted/mail/relay.py
Normal file
164
backend/lib/python3.12/site-packages/twisted/mail/relay.py
Normal file
@@ -0,0 +1,164 @@
|
||||
# -*- test-case-name: twisted.mail.test.test_mail -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Support for relaying mail.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pickle
|
||||
|
||||
from twisted.internet.address import UNIXAddress
|
||||
from twisted.mail import smtp
|
||||
from twisted.python import log
|
||||
|
||||
|
||||
class DomainQueuer:
|
||||
"""
|
||||
An SMTP domain which add messages to a queue intended for relaying.
|
||||
"""
|
||||
|
||||
def __init__(self, service, authenticated=False):
|
||||
self.service = service
|
||||
self.authed = authenticated
|
||||
|
||||
def exists(self, user):
|
||||
"""
|
||||
Check whether mail can be relayed to a user.
|
||||
|
||||
@type user: L{User}
|
||||
@param user: A user.
|
||||
|
||||
@rtype: no-argument callable which returns L{IMessage <smtp.IMessage>}
|
||||
provider
|
||||
@return: A function which takes no arguments and returns a message
|
||||
receiver for the user.
|
||||
|
||||
@raise SMTPBadRcpt: When mail cannot be relayed to the user.
|
||||
"""
|
||||
if self.willRelay(user.dest, user.protocol):
|
||||
# The most cursor form of verification of the addresses
|
||||
orig = filter(None, str(user.orig).split("@", 1))
|
||||
dest = filter(None, str(user.dest).split("@", 1))
|
||||
if len(orig) == 2 and len(dest) == 2:
|
||||
return lambda: self.startMessage(user)
|
||||
raise smtp.SMTPBadRcpt(user)
|
||||
|
||||
def willRelay(self, address, protocol):
|
||||
"""
|
||||
Check whether we agree to relay.
|
||||
|
||||
The default is to relay for all connections over UNIX
|
||||
sockets and all connections from localhost.
|
||||
"""
|
||||
peer = protocol.transport.getPeer()
|
||||
return self.authed or isinstance(peer, UNIXAddress) or peer.host == "127.0.0.1"
|
||||
|
||||
def startMessage(self, user):
|
||||
"""
|
||||
Create an envelope and a message receiver for the relay queue.
|
||||
|
||||
@type user: L{User}
|
||||
@param user: A user.
|
||||
|
||||
@rtype: L{IMessage <smtp.IMessage>}
|
||||
@return: A message receiver.
|
||||
"""
|
||||
queue = self.service.queue
|
||||
envelopeFile, smtpMessage = queue.createNewMessage()
|
||||
with envelopeFile:
|
||||
log.msg(f"Queueing mail {str(user.orig)!r} -> {str(user.dest)!r}")
|
||||
pickle.dump([str(user.orig), str(user.dest)], envelopeFile)
|
||||
return smtpMessage
|
||||
|
||||
|
||||
class RelayerMixin:
|
||||
# XXX - This is -totally- bogus
|
||||
# It opens about a -hundred- -billion- files
|
||||
# and -leaves- them open!
|
||||
|
||||
def loadMessages(self, messagePaths):
|
||||
self.messages = []
|
||||
self.names = []
|
||||
for message in messagePaths:
|
||||
with open(message + "-H", "rb") as fp:
|
||||
messageContents = pickle.load(fp)
|
||||
fp = open(message + "-D")
|
||||
messageContents.append(fp)
|
||||
self.messages.append(messageContents)
|
||||
self.names.append(message)
|
||||
|
||||
def getMailFrom(self):
|
||||
if not self.messages:
|
||||
return None
|
||||
return self.messages[0][0]
|
||||
|
||||
def getMailTo(self):
|
||||
if not self.messages:
|
||||
return None
|
||||
return [self.messages[0][1]]
|
||||
|
||||
def getMailData(self):
|
||||
if not self.messages:
|
||||
return None
|
||||
return self.messages[0][2]
|
||||
|
||||
def sentMail(self, code, resp, numOk, addresses, log):
|
||||
"""Since we only use one recipient per envelope, this
|
||||
will be called with 0 or 1 addresses. We probably want
|
||||
to do something with the error message if we failed.
|
||||
"""
|
||||
if code in smtp.SUCCESS:
|
||||
# At least one, i.e. all, recipients successfully delivered
|
||||
os.remove(self.names[0] + "-D")
|
||||
os.remove(self.names[0] + "-H")
|
||||
del self.messages[0]
|
||||
del self.names[0]
|
||||
|
||||
|
||||
class SMTPRelayer(RelayerMixin, smtp.SMTPClient):
|
||||
"""
|
||||
A base class for SMTP relayers.
|
||||
"""
|
||||
|
||||
def __init__(self, messagePaths, *args, **kw):
|
||||
"""
|
||||
@type messagePaths: L{list} of L{bytes}
|
||||
@param messagePaths: The base filename for each message to be relayed.
|
||||
|
||||
@type args: 1-L{tuple} of (0) L{bytes} or 2-L{tuple} of
|
||||
(0) L{bytes}, (1) L{int}
|
||||
@param args: Positional arguments for L{SMTPClient.__init__}
|
||||
|
||||
@type kw: L{dict}
|
||||
@param kw: Keyword arguments for L{SMTPClient.__init__}
|
||||
"""
|
||||
smtp.SMTPClient.__init__(self, *args, **kw)
|
||||
self.loadMessages(messagePaths)
|
||||
|
||||
|
||||
class ESMTPRelayer(RelayerMixin, smtp.ESMTPClient):
|
||||
"""
|
||||
A base class for ESMTP relayers.
|
||||
"""
|
||||
|
||||
def __init__(self, messagePaths, *args, **kw):
|
||||
"""
|
||||
@type messagePaths: L{list} of L{bytes}
|
||||
@param messagePaths: The base filename for each message to be relayed.
|
||||
|
||||
@type args: 3-L{tuple} of (0) L{bytes}, (1) L{None} or
|
||||
L{ClientContextFactory
|
||||
<twisted.internet.ssl.ClientContextFactory>},
|
||||
(2) L{bytes} or 4-L{tuple} of (0) L{bytes}, (1) L{None}
|
||||
or L{ClientContextFactory
|
||||
<twisted.internet.ssl.ClientContextFactory>}, (2) L{bytes},
|
||||
(3) L{int}
|
||||
@param args: Positional arguments for L{ESMTPClient.__init__}
|
||||
|
||||
@type kw: L{dict}
|
||||
@param kw: Keyword arguments for L{ESMTPClient.__init__}
|
||||
"""
|
||||
smtp.ESMTPClient.__init__(self, *args, **kw)
|
||||
self.loadMessages(messagePaths)
|
||||
1135
backend/lib/python3.12/site-packages/twisted/mail/relaymanager.py
Normal file
1135
backend/lib/python3.12/site-packages/twisted/mail/relaymanager.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1 @@
|
||||
"mail scripts"
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,386 @@
|
||||
# -*- test-case-name: twisted.mail.test.test_mailmail -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Implementation module for the I{mailmail} command.
|
||||
"""
|
||||
|
||||
|
||||
import email.utils
|
||||
import getpass
|
||||
import os
|
||||
import sys
|
||||
from configparser import ConfigParser
|
||||
from io import StringIO
|
||||
|
||||
from twisted.copyright import version
|
||||
from twisted.internet import reactor
|
||||
from twisted.logger import Logger, textFileLogObserver
|
||||
from twisted.mail import smtp
|
||||
|
||||
GLOBAL_CFG = "/etc/mailmail"
|
||||
LOCAL_CFG = os.path.expanduser("~/.twisted/mailmail")
|
||||
SMARTHOST = "127.0.0.1"
|
||||
|
||||
ERROR_FMT = """\
|
||||
Subject: Failed Message Delivery
|
||||
|
||||
Message delivery failed. The following occurred:
|
||||
|
||||
%s
|
||||
--
|
||||
The Twisted sendmail application.
|
||||
"""
|
||||
|
||||
_logObserver = textFileLogObserver(sys.stderr)
|
||||
_log = Logger(observer=_logObserver)
|
||||
|
||||
|
||||
class Options:
|
||||
"""
|
||||
Store the values of the parsed command-line options to the I{mailmail}
|
||||
script.
|
||||
|
||||
@type to: L{list} of L{str}
|
||||
@ivar to: The addresses to which to deliver this message.
|
||||
|
||||
@type sender: L{str}
|
||||
@ivar sender: The address from which this message is being sent.
|
||||
|
||||
@type body: C{file}
|
||||
@ivar body: The object from which the message is to be read.
|
||||
"""
|
||||
|
||||
|
||||
def getlogin():
|
||||
try:
|
||||
return os.getlogin()
|
||||
except BaseException:
|
||||
return getpass.getuser()
|
||||
|
||||
|
||||
_unsupportedOption = SystemExit("Unsupported option.")
|
||||
|
||||
|
||||
def parseOptions(argv):
|
||||
o = Options()
|
||||
o.to = [e for e in argv if not e.startswith("-")]
|
||||
o.sender = getlogin()
|
||||
|
||||
# Just be very stupid
|
||||
|
||||
# Skip -bm -- it is the default
|
||||
|
||||
# Add a non-standard option for querying the version of this tool.
|
||||
if "--version" in argv:
|
||||
print("mailmail version:", version)
|
||||
raise SystemExit()
|
||||
|
||||
# -bp lists queue information. Screw that.
|
||||
if "-bp" in argv:
|
||||
raise _unsupportedOption
|
||||
|
||||
# -bs makes sendmail use stdin/stdout as its transport. Screw that.
|
||||
if "-bs" in argv:
|
||||
raise _unsupportedOption
|
||||
|
||||
# -F sets who the mail is from, but is overridable by the From header
|
||||
if "-F" in argv:
|
||||
o.sender = argv[argv.index("-F") + 1]
|
||||
o.to.remove(o.sender)
|
||||
|
||||
# -i and -oi makes us ignore lone "."
|
||||
if ("-i" in argv) or ("-oi" in argv):
|
||||
raise _unsupportedOption
|
||||
|
||||
# -odb is background delivery
|
||||
if "-odb" in argv:
|
||||
o.background = True
|
||||
else:
|
||||
o.background = False
|
||||
|
||||
# -odf is foreground delivery
|
||||
if "-odf" in argv:
|
||||
o.background = False
|
||||
else:
|
||||
o.background = True
|
||||
|
||||
# -oem and -em cause errors to be mailed back to the sender.
|
||||
# It is also the default.
|
||||
|
||||
# -oep and -ep cause errors to be printed to stderr
|
||||
if ("-oep" in argv) or ("-ep" in argv):
|
||||
o.printErrors = True
|
||||
else:
|
||||
o.printErrors = False
|
||||
|
||||
# -om causes a copy of the message to be sent to the sender if the sender
|
||||
# appears in an alias expansion. We do not support aliases.
|
||||
if "-om" in argv:
|
||||
raise _unsupportedOption
|
||||
|
||||
# -t causes us to pick the recipients of the message from
|
||||
# the To, Cc, and Bcc headers, and to remove the Bcc header
|
||||
# if present.
|
||||
if "-t" in argv:
|
||||
o.recipientsFromHeaders = True
|
||||
o.excludeAddresses = o.to
|
||||
o.to = []
|
||||
else:
|
||||
o.recipientsFromHeaders = False
|
||||
o.exludeAddresses = []
|
||||
|
||||
requiredHeaders = {
|
||||
"from": [],
|
||||
"to": [],
|
||||
"cc": [],
|
||||
"bcc": [],
|
||||
"date": [],
|
||||
}
|
||||
|
||||
buffer = StringIO()
|
||||
while 1:
|
||||
write = 1
|
||||
line = sys.stdin.readline()
|
||||
if not line.strip():
|
||||
break
|
||||
|
||||
hdrs = line.split(": ", 1)
|
||||
|
||||
hdr = hdrs[0].lower()
|
||||
if o.recipientsFromHeaders and hdr in ("to", "cc", "bcc"):
|
||||
o.to.extend([email.utils.parseaddr(hdrs[1])[1]])
|
||||
if hdr == "bcc":
|
||||
write = 0
|
||||
elif hdr == "from":
|
||||
o.sender = email.utils.parseaddr(hdrs[1])[1]
|
||||
|
||||
if hdr in requiredHeaders:
|
||||
requiredHeaders[hdr].append(hdrs[1])
|
||||
|
||||
if write:
|
||||
buffer.write(line)
|
||||
|
||||
if not requiredHeaders["from"]:
|
||||
buffer.write(f"From: {o.sender}\r\n")
|
||||
if not requiredHeaders["to"]:
|
||||
if not o.to:
|
||||
raise SystemExit("No recipients specified.")
|
||||
buffer.write("To: {}\r\n".format(", ".join(o.to)))
|
||||
if not requiredHeaders["date"]:
|
||||
buffer.write(f"Date: {smtp.rfc822date()}\r\n")
|
||||
|
||||
buffer.write(line)
|
||||
|
||||
if o.recipientsFromHeaders:
|
||||
for a in o.excludeAddresses:
|
||||
try:
|
||||
o.to.remove(a)
|
||||
except BaseException:
|
||||
pass
|
||||
|
||||
buffer.seek(0, 0)
|
||||
o.body = StringIO(buffer.getvalue() + sys.stdin.read())
|
||||
return o
|
||||
|
||||
|
||||
class Configuration:
|
||||
"""
|
||||
|
||||
@ivar allowUIDs: A list of UIDs which are allowed to send mail.
|
||||
@ivar allowGIDs: A list of GIDs which are allowed to send mail.
|
||||
@ivar denyUIDs: A list of UIDs which are not allowed to send mail.
|
||||
@ivar denyGIDs: A list of GIDs which are not allowed to send mail.
|
||||
|
||||
@type defaultAccess: L{bool}
|
||||
@ivar defaultAccess: L{True} if access will be allowed when no other access
|
||||
control rule matches or L{False} if it will be denied in that case.
|
||||
|
||||
@ivar useraccess: Either C{'allow'} to check C{allowUID} first
|
||||
or C{'deny'} to check C{denyUID} first.
|
||||
|
||||
@ivar groupaccess: Either C{'allow'} to check C{allowGID} first or
|
||||
C{'deny'} to check C{denyGID} first.
|
||||
|
||||
@ivar identities: A L{dict} mapping hostnames to credentials to use when
|
||||
sending mail to that host.
|
||||
|
||||
@ivar smarthost: L{None} or a hostname through which all outgoing mail will
|
||||
be sent.
|
||||
|
||||
@ivar domain: L{None} or the hostname with which to identify ourselves when
|
||||
connecting to an MTA.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.allowUIDs = []
|
||||
self.denyUIDs = []
|
||||
self.allowGIDs = []
|
||||
self.denyGIDs = []
|
||||
self.useraccess = "deny"
|
||||
self.groupaccess = "deny"
|
||||
|
||||
self.identities = {}
|
||||
self.smarthost = None
|
||||
self.domain = None
|
||||
|
||||
self.defaultAccess = True
|
||||
|
||||
|
||||
def loadConfig(path):
|
||||
# [useraccess]
|
||||
# allow=uid1,uid2,...
|
||||
# deny=uid1,uid2,...
|
||||
# order=allow,deny
|
||||
# [groupaccess]
|
||||
# allow=gid1,gid2,...
|
||||
# deny=gid1,gid2,...
|
||||
# order=deny,allow
|
||||
# [identity]
|
||||
# host1=username:password
|
||||
# host2=username:password
|
||||
# [addresses]
|
||||
# smarthost=a.b.c.d
|
||||
# default_domain=x.y.z
|
||||
|
||||
c = Configuration()
|
||||
|
||||
if not os.access(path, os.R_OK):
|
||||
return c
|
||||
|
||||
p = ConfigParser()
|
||||
p.read(path)
|
||||
|
||||
au = c.allowUIDs
|
||||
du = c.denyUIDs
|
||||
ag = c.allowGIDs
|
||||
dg = c.denyGIDs
|
||||
for section, a, d in (("useraccess", au, du), ("groupaccess", ag, dg)):
|
||||
if p.has_section(section):
|
||||
for mode, L in (("allow", a), ("deny", d)):
|
||||
if p.has_option(section, mode) and p.get(section, mode):
|
||||
for sectionID in p.get(section, mode).split(","):
|
||||
try:
|
||||
sectionID = int(sectionID)
|
||||
except ValueError:
|
||||
_log.error(
|
||||
"Illegal {prefix}ID in "
|
||||
"[{section}] section: {sectionID}",
|
||||
prefix=section[0].upper(),
|
||||
section=section,
|
||||
sectionID=sectionID,
|
||||
)
|
||||
else:
|
||||
L.append(sectionID)
|
||||
order = p.get(section, "order")
|
||||
order = [s.split() for s in [s.lower() for s in order.split(",")]]
|
||||
if order[0] == "allow":
|
||||
setattr(c, section, "allow")
|
||||
else:
|
||||
setattr(c, section, "deny")
|
||||
|
||||
if p.has_section("identity"):
|
||||
for host, up in p.items("identity"):
|
||||
parts = up.split(":", 1)
|
||||
if len(parts) != 2:
|
||||
_log.error("Illegal entry in [identity] section: {section}", section=up)
|
||||
continue
|
||||
c.identities[host] = parts
|
||||
|
||||
if p.has_section("addresses"):
|
||||
if p.has_option("addresses", "smarthost"):
|
||||
c.smarthost = p.get("addresses", "smarthost")
|
||||
if p.has_option("addresses", "default_domain"):
|
||||
c.domain = p.get("addresses", "default_domain")
|
||||
|
||||
return c
|
||||
|
||||
|
||||
def success(result):
|
||||
reactor.stop()
|
||||
|
||||
|
||||
failed = None
|
||||
|
||||
|
||||
def failure(f):
|
||||
global failed
|
||||
reactor.stop()
|
||||
failed = f
|
||||
|
||||
|
||||
def sendmail(host, options, ident):
|
||||
d = smtp.sendmail(host, options.sender, options.to, options.body)
|
||||
d.addCallbacks(success, failure)
|
||||
reactor.run()
|
||||
|
||||
|
||||
def senderror(failure, options):
|
||||
recipient = [options.sender]
|
||||
sender = '"Internally Generated Message ({})"<postmaster@{}>'.format(
|
||||
sys.argv[0], smtp.DNSNAME.decode("ascii")
|
||||
)
|
||||
error = StringIO()
|
||||
failure.printTraceback(file=error)
|
||||
body = StringIO(ERROR_FMT % error.getvalue())
|
||||
d = smtp.sendmail("localhost", sender, recipient, body)
|
||||
d.addBoth(lambda _: reactor.stop())
|
||||
|
||||
|
||||
def deny(conf):
|
||||
uid = os.getuid()
|
||||
gid = os.getgid()
|
||||
|
||||
if conf.useraccess == "deny":
|
||||
if uid in conf.denyUIDs:
|
||||
return True
|
||||
if uid in conf.allowUIDs:
|
||||
return False
|
||||
else:
|
||||
if uid in conf.allowUIDs:
|
||||
return False
|
||||
if uid in conf.denyUIDs:
|
||||
return True
|
||||
|
||||
if conf.groupaccess == "deny":
|
||||
if gid in conf.denyGIDs:
|
||||
return True
|
||||
if gid in conf.allowGIDs:
|
||||
return False
|
||||
else:
|
||||
if gid in conf.allowGIDs:
|
||||
return False
|
||||
if gid in conf.denyGIDs:
|
||||
return True
|
||||
|
||||
return not conf.defaultAccess
|
||||
|
||||
|
||||
def run():
|
||||
o = parseOptions(sys.argv[1:])
|
||||
gConf = loadConfig(GLOBAL_CFG)
|
||||
lConf = loadConfig(LOCAL_CFG)
|
||||
|
||||
if deny(gConf) or deny(lConf):
|
||||
_log.error("Permission denied")
|
||||
return
|
||||
|
||||
host = lConf.smarthost or gConf.smarthost or SMARTHOST
|
||||
|
||||
ident = gConf.identities.copy()
|
||||
ident.update(lConf.identities)
|
||||
|
||||
if lConf.domain:
|
||||
smtp.DNSNAME = lConf.domain
|
||||
elif gConf.domain:
|
||||
smtp.DNSNAME = gConf.domain
|
||||
|
||||
sendmail(host, o, ident)
|
||||
|
||||
if failed:
|
||||
if o.printErrors:
|
||||
failed.printTraceback(file=sys.stderr)
|
||||
raise SystemExit(1)
|
||||
else:
|
||||
senderror(failed, o)
|
||||
2270
backend/lib/python3.12/site-packages/twisted/mail/smtp.py
Normal file
2270
backend/lib/python3.12/site-packages/twisted/mail/smtp.py
Normal file
File diff suppressed because it is too large
Load Diff
384
backend/lib/python3.12/site-packages/twisted/mail/tap.py
Normal file
384
backend/lib/python3.12/site-packages/twisted/mail/tap.py
Normal file
@@ -0,0 +1,384 @@
|
||||
# -*- test-case-name: twisted.mail.test.test_options -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
"""
|
||||
Support for creating mail servers with twistd.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from twisted.application import internet
|
||||
from twisted.cred import checkers, strcred
|
||||
from twisted.internet import endpoints
|
||||
from twisted.mail import alias, mail, maildir, relay, relaymanager
|
||||
from twisted.python import usage
|
||||
|
||||
|
||||
class Options(usage.Options, strcred.AuthOptionMixin):
|
||||
"""
|
||||
An options list parser for twistd mail.
|
||||
|
||||
@type synopsis: L{bytes}
|
||||
@ivar synopsis: A description of options for use in the usage message.
|
||||
|
||||
@type optParameters: L{list} of L{list} of (0) L{bytes}, (1) L{bytes},
|
||||
(2) L{object}, (3) L{bytes}, (4) L{None} or
|
||||
callable which takes L{bytes} and returns L{object}
|
||||
@ivar optParameters: Information about supported parameters. See
|
||||
L{Options <twisted.python.usage.Options>} for details.
|
||||
|
||||
@type optFlags: L{list} of L{list} of (0) L{bytes}, (1) L{bytes} or
|
||||
L{None}, (2) L{bytes}
|
||||
@ivar optFlags: Information about supported flags. See
|
||||
L{Options <twisted.python.usage.Options>} for details.
|
||||
|
||||
@type _protoDefaults: L{dict} mapping L{bytes} to L{int}
|
||||
@ivar _protoDefaults: A mapping of default service to port.
|
||||
|
||||
@type compData: L{Completions <usage.Completions>}
|
||||
@ivar compData: Metadata for the shell tab completion system.
|
||||
|
||||
@type longdesc: L{bytes}
|
||||
@ivar longdesc: A long description of the plugin for use in the usage
|
||||
message.
|
||||
|
||||
@type service: L{MailService}
|
||||
@ivar service: The email service.
|
||||
|
||||
@type last_domain: L{IDomain} provider or L{None}
|
||||
@ivar last_domain: The most recently specified domain.
|
||||
"""
|
||||
|
||||
synopsis = "[options]"
|
||||
|
||||
optParameters = [
|
||||
[
|
||||
"relay",
|
||||
"R",
|
||||
None,
|
||||
"Relay messages according to their envelope 'To', using "
|
||||
"the given path as a queue directory.",
|
||||
],
|
||||
["hostname", "H", None, "The hostname by which to identify this server."],
|
||||
]
|
||||
|
||||
optFlags = [
|
||||
["esmtp", "E", "Use RFC 1425/1869 SMTP extensions"],
|
||||
["disable-anonymous", None, "Disallow non-authenticated SMTP connections"],
|
||||
["no-pop3", None, "Disable the default POP3 server."],
|
||||
["no-smtp", None, "Disable the default SMTP server."],
|
||||
]
|
||||
|
||||
_protoDefaults = {
|
||||
"pop3": 8110,
|
||||
"smtp": 8025,
|
||||
}
|
||||
|
||||
compData = usage.Completions(optActions={"hostname": usage.CompleteHostnames()})
|
||||
|
||||
longdesc = """
|
||||
An SMTP / POP3 email server plugin for twistd.
|
||||
|
||||
Examples:
|
||||
|
||||
1. SMTP and POP server
|
||||
|
||||
twistd mail --maildirdbmdomain=example.com=/tmp/example.com
|
||||
--user=joe=password
|
||||
|
||||
Starts an SMTP server that only accepts emails to joe@example.com and saves
|
||||
them to /tmp/example.com.
|
||||
|
||||
Also starts a POP mail server which will allow a client to log in using
|
||||
username: joe@example.com and password: password and collect any email that
|
||||
has been saved in /tmp/example.com.
|
||||
|
||||
2. SMTP relay
|
||||
|
||||
twistd mail --relay=/tmp/mail_queue
|
||||
|
||||
Starts an SMTP server that accepts emails to any email address and relays
|
||||
them to an appropriate remote SMTP server. Queued emails will be
|
||||
temporarily stored in /tmp/mail_queue.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Parse options and create a mail service.
|
||||
"""
|
||||
usage.Options.__init__(self)
|
||||
self.service = mail.MailService()
|
||||
self.last_domain = None
|
||||
for service in self._protoDefaults:
|
||||
self[service] = []
|
||||
|
||||
def addEndpoint(self, service, description):
|
||||
"""
|
||||
Add an endpoint to a service.
|
||||
|
||||
@type service: L{bytes}
|
||||
@param service: A service, either C{b'smtp'} or C{b'pop3'}.
|
||||
|
||||
@type description: L{bytes}
|
||||
@param description: An endpoint description string or a TCP port
|
||||
number.
|
||||
"""
|
||||
from twisted.internet import reactor
|
||||
|
||||
self[service].append(endpoints.serverFromString(reactor, description))
|
||||
|
||||
def opt_pop3(self, description):
|
||||
"""
|
||||
Add a POP3 port listener on the specified endpoint.
|
||||
|
||||
You can listen on multiple ports by specifying multiple --pop3 options.
|
||||
"""
|
||||
self.addEndpoint("pop3", description)
|
||||
|
||||
opt_p = opt_pop3
|
||||
|
||||
def opt_smtp(self, description):
|
||||
"""
|
||||
Add an SMTP port listener on the specified endpoint.
|
||||
|
||||
You can listen on multiple ports by specifying multiple --smtp options.
|
||||
"""
|
||||
self.addEndpoint("smtp", description)
|
||||
|
||||
opt_s = opt_smtp
|
||||
|
||||
def opt_default(self):
|
||||
"""
|
||||
Make the most recently specified domain the default domain.
|
||||
"""
|
||||
if self.last_domain:
|
||||
self.service.addDomain("", self.last_domain)
|
||||
else:
|
||||
raise usage.UsageError("Specify a domain before specifying using --default")
|
||||
|
||||
opt_D = opt_default
|
||||
|
||||
def opt_maildirdbmdomain(self, domain):
|
||||
"""
|
||||
Generate an SMTP/POP3 virtual domain.
|
||||
|
||||
This option requires an argument of the form 'NAME=PATH' where NAME is
|
||||
the DNS domain name for which email will be accepted and where PATH is
|
||||
a the filesystem path to a Maildir folder.
|
||||
[Example: 'example.com=/tmp/example.com']
|
||||
"""
|
||||
try:
|
||||
name, path = domain.split("=")
|
||||
except ValueError:
|
||||
raise usage.UsageError(
|
||||
"Argument to --maildirdbmdomain must be of the form 'name=path'"
|
||||
)
|
||||
|
||||
self.last_domain = maildir.MaildirDirdbmDomain(
|
||||
self.service, os.path.abspath(path)
|
||||
)
|
||||
self.service.addDomain(name, self.last_domain)
|
||||
|
||||
opt_d = opt_maildirdbmdomain
|
||||
|
||||
def opt_user(self, user_pass):
|
||||
"""
|
||||
Add a user and password to the last specified domain.
|
||||
"""
|
||||
try:
|
||||
user, password = user_pass.split("=", 1)
|
||||
except ValueError:
|
||||
raise usage.UsageError(
|
||||
"Argument to --user must be of the form 'user=password'"
|
||||
)
|
||||
if self.last_domain:
|
||||
self.last_domain.addUser(user, password)
|
||||
else:
|
||||
raise usage.UsageError("Specify a domain before specifying users")
|
||||
|
||||
opt_u = opt_user
|
||||
|
||||
def opt_bounce_to_postmaster(self):
|
||||
"""
|
||||
Send undeliverable messages to the postmaster.
|
||||
"""
|
||||
self.last_domain.postmaster = 1
|
||||
|
||||
opt_b = opt_bounce_to_postmaster
|
||||
|
||||
def opt_aliases(self, filename):
|
||||
"""
|
||||
Specify an aliases(5) file to use for the last specified domain.
|
||||
"""
|
||||
if self.last_domain is not None:
|
||||
if mail.IAliasableDomain.providedBy(self.last_domain):
|
||||
aliases = alias.loadAliasFile(self.service.domains, filename)
|
||||
self.last_domain.setAliasGroup(aliases)
|
||||
self.service.monitor.monitorFile(
|
||||
filename, AliasUpdater(self.service.domains, self.last_domain)
|
||||
)
|
||||
else:
|
||||
raise usage.UsageError(
|
||||
"%s does not support alias files"
|
||||
% (self.last_domain.__class__.__name__,)
|
||||
)
|
||||
else:
|
||||
raise usage.UsageError("Specify a domain before specifying aliases")
|
||||
|
||||
opt_A = opt_aliases
|
||||
|
||||
def _getEndpoints(self, reactor, service):
|
||||
"""
|
||||
Return a list of endpoints for the specified service, constructing
|
||||
defaults if necessary.
|
||||
|
||||
If no endpoints were configured for the service and the protocol
|
||||
was not explicitly disabled with a I{--no-*} option, a default
|
||||
endpoint for the service is created.
|
||||
|
||||
@type reactor: L{IReactorTCP <twisted.internet.interfaces.IReactorTCP>}
|
||||
provider
|
||||
@param reactor: If any endpoints are created, the reactor with
|
||||
which they are created.
|
||||
|
||||
@type service: L{bytes}
|
||||
@param service: The type of service for which to retrieve endpoints,
|
||||
either C{b'pop3'} or C{b'smtp'}.
|
||||
|
||||
@rtype: L{list} of L{IStreamServerEndpoint
|
||||
<twisted.internet.interfaces.IStreamServerEndpoint>} provider
|
||||
@return: The endpoints for the specified service as configured by the
|
||||
command line parameters.
|
||||
"""
|
||||
if self[service]:
|
||||
# If there are any services set up, just return those.
|
||||
return self[service]
|
||||
elif self["no-" + service]:
|
||||
# If there are no services, but the service was explicitly disabled,
|
||||
# return nothing.
|
||||
return []
|
||||
else:
|
||||
# Otherwise, return the old default service.
|
||||
return [endpoints.TCP4ServerEndpoint(reactor, self._protoDefaults[service])]
|
||||
|
||||
def postOptions(self):
|
||||
"""
|
||||
Check the validity of the specified set of options and
|
||||
configure authentication.
|
||||
|
||||
@raise UsageError: When the set of options is invalid.
|
||||
"""
|
||||
from twisted.internet import reactor
|
||||
|
||||
if self["esmtp"] and self["hostname"] is None:
|
||||
raise usage.UsageError("--esmtp requires --hostname")
|
||||
|
||||
# If the --auth option was passed, this will be present -- otherwise,
|
||||
# it won't be, which is also a perfectly valid state.
|
||||
if "credCheckers" in self:
|
||||
for ch in self["credCheckers"]:
|
||||
self.service.smtpPortal.registerChecker(ch)
|
||||
|
||||
if not self["disable-anonymous"]:
|
||||
self.service.smtpPortal.registerChecker(checkers.AllowAnonymousAccess())
|
||||
|
||||
anything = False
|
||||
for service in self._protoDefaults:
|
||||
self[service] = self._getEndpoints(reactor, service)
|
||||
if self[service]:
|
||||
anything = True
|
||||
|
||||
if not anything:
|
||||
raise usage.UsageError("You cannot disable all protocols")
|
||||
|
||||
|
||||
class AliasUpdater:
|
||||
"""
|
||||
A callable object which updates the aliases for a domain from an aliases(5)
|
||||
file.
|
||||
|
||||
@ivar domains: See L{__init__}.
|
||||
@ivar domain: See L{__init__}.
|
||||
"""
|
||||
|
||||
def __init__(self, domains, domain):
|
||||
"""
|
||||
@type domains: L{dict} mapping L{bytes} to L{IDomain} provider
|
||||
@param domains: A mapping of domain name to domain object
|
||||
|
||||
@type domain: L{IAliasableDomain} provider
|
||||
@param domain: The domain to update.
|
||||
"""
|
||||
self.domains = domains
|
||||
self.domain = domain
|
||||
|
||||
def __call__(self, new):
|
||||
"""
|
||||
Update the aliases for a domain from an aliases(5) file.
|
||||
|
||||
@type new: L{bytes}
|
||||
@param new: The name of an aliases(5) file.
|
||||
"""
|
||||
self.domain.setAliasGroup(alias.loadAliasFile(self.domains, new))
|
||||
|
||||
|
||||
def makeService(config):
|
||||
"""
|
||||
Configure a service for operating a mail server.
|
||||
|
||||
The returned service may include POP3 servers, SMTP servers, or both,
|
||||
depending on the configuration passed in. If there are multiple servers,
|
||||
they will share all of their non-network state (i.e. the same user accounts
|
||||
are available on all of them).
|
||||
|
||||
@type config: L{Options <usage.Options>}
|
||||
@param config: Configuration options specifying which servers to include in
|
||||
the returned service and where they should keep mail data.
|
||||
|
||||
@rtype: L{IService <twisted.application.service.IService>} provider
|
||||
@return: A service which contains the requested mail servers.
|
||||
"""
|
||||
if config["esmtp"]:
|
||||
rmType = relaymanager.SmartHostESMTPRelayingManager
|
||||
smtpFactory = config.service.getESMTPFactory
|
||||
else:
|
||||
rmType = relaymanager.SmartHostSMTPRelayingManager
|
||||
smtpFactory = config.service.getSMTPFactory
|
||||
|
||||
if config["relay"]:
|
||||
dir = config["relay"]
|
||||
if not os.path.isdir(dir):
|
||||
os.mkdir(dir)
|
||||
|
||||
config.service.setQueue(relaymanager.Queue(dir))
|
||||
default = relay.DomainQueuer(config.service)
|
||||
|
||||
manager = rmType(config.service.queue)
|
||||
if config["esmtp"]:
|
||||
manager.fArgs += (None, None)
|
||||
manager.fArgs += (config["hostname"],)
|
||||
|
||||
helper = relaymanager.RelayStateHelper(manager, 1)
|
||||
helper.setServiceParent(config.service)
|
||||
config.service.domains.setDefaultDomain(default)
|
||||
|
||||
if config["pop3"]:
|
||||
f = config.service.getPOP3Factory()
|
||||
for endpoint in config["pop3"]:
|
||||
svc = internet.StreamServerEndpointService(endpoint, f)
|
||||
svc.setServiceParent(config.service)
|
||||
|
||||
if config["smtp"]:
|
||||
f = smtpFactory()
|
||||
if config["hostname"]:
|
||||
f.domain = config["hostname"]
|
||||
f.fArgs = (f.domain,)
|
||||
if config["esmtp"]:
|
||||
f.fArgs = (None, None) + f.fArgs
|
||||
for endpoint in config["smtp"]:
|
||||
svc = internet.StreamServerEndpointService(endpoint, f)
|
||||
svc.setServiceParent(config.service)
|
||||
|
||||
return config.service
|
||||
@@ -0,0 +1 @@
|
||||
"Tests for twistd.mail"
|
||||
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.
@@ -0,0 +1,299 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- test-case-name: twisted.mail.test.test_pop3client -*-
|
||||
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
|
||||
import sys
|
||||
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.protocol import Factory
|
||||
from twisted.protocols import basic
|
||||
|
||||
USER = "test"
|
||||
PASS = "twisted"
|
||||
|
||||
PORT = 1100
|
||||
|
||||
SSL_SUPPORT = True
|
||||
UIDL_SUPPORT = True
|
||||
INVALID_SERVER_RESPONSE = False
|
||||
INVALID_CAPABILITY_RESPONSE = False
|
||||
INVALID_LOGIN_RESPONSE = False
|
||||
DENY_CONNECTION = False
|
||||
DROP_CONNECTION = False
|
||||
BAD_TLS_RESPONSE = False
|
||||
TIMEOUT_RESPONSE = False
|
||||
TIMEOUT_DEFERRED = False
|
||||
SLOW_GREETING = False
|
||||
|
||||
"""Commands"""
|
||||
CONNECTION_MADE = b"+OK POP3 localhost v2003.83 server ready"
|
||||
|
||||
CAPABILITIES = [b"TOP", b"LOGIN-DELAY 180", b"USER", b"SASL LOGIN"]
|
||||
|
||||
CAPABILITIES_SSL = b"STLS"
|
||||
CAPABILITIES_UIDL = b"UIDL"
|
||||
|
||||
|
||||
INVALID_RESPONSE = b"-ERR Unknown request"
|
||||
VALID_RESPONSE = b"+OK Command Completed"
|
||||
AUTH_DECLINED = b"-ERR LOGIN failed"
|
||||
AUTH_ACCEPTED = b"+OK Mailbox open, 0 messages"
|
||||
TLS_ERROR = b"-ERR server side error start TLS handshake"
|
||||
LOGOUT_COMPLETE = b"+OK quit completed"
|
||||
NOT_LOGGED_IN = b"-ERR Unknown AUHORIZATION state command"
|
||||
STAT = b"+OK 0 0"
|
||||
UIDL = b"+OK Unique-ID listing follows\r\n."
|
||||
LIST = b"+OK Mailbox scan listing follows\r\n."
|
||||
CAP_START = b"+OK Capability list follows:"
|
||||
|
||||
|
||||
class POP3TestServer(basic.LineReceiver):
|
||||
def __init__(self, contextFactory=None):
|
||||
self.loggedIn = False
|
||||
self.caps = None
|
||||
self.tmpUser = None
|
||||
self.ctx = contextFactory
|
||||
|
||||
def sendSTATResp(self, req):
|
||||
self.sendLine(STAT)
|
||||
|
||||
def sendUIDLResp(self, req):
|
||||
self.sendLine(UIDL)
|
||||
|
||||
def sendLISTResp(self, req):
|
||||
self.sendLine(LIST)
|
||||
|
||||
def sendCapabilities(self):
|
||||
if self.caps is None:
|
||||
self.caps = [CAP_START]
|
||||
|
||||
if UIDL_SUPPORT:
|
||||
self.caps.append(CAPABILITIES_UIDL)
|
||||
|
||||
if SSL_SUPPORT:
|
||||
self.caps.append(CAPABILITIES_SSL)
|
||||
|
||||
for cap in CAPABILITIES:
|
||||
self.caps.append(cap)
|
||||
resp = b"\r\n".join(self.caps)
|
||||
resp += b"\r\n."
|
||||
|
||||
self.sendLine(resp)
|
||||
|
||||
def connectionMade(self):
|
||||
if DENY_CONNECTION:
|
||||
self.disconnect()
|
||||
return
|
||||
|
||||
if SLOW_GREETING:
|
||||
reactor.callLater(20, self.sendGreeting)
|
||||
|
||||
else:
|
||||
self.sendGreeting()
|
||||
|
||||
def sendGreeting(self):
|
||||
self.sendLine(CONNECTION_MADE)
|
||||
|
||||
def lineReceived(self, line):
|
||||
"""Error Conditions"""
|
||||
|
||||
uline = line.upper()
|
||||
find = lambda s: uline.find(s) != -1
|
||||
|
||||
if TIMEOUT_RESPONSE:
|
||||
# Do not respond to clients request
|
||||
return
|
||||
|
||||
if DROP_CONNECTION:
|
||||
self.disconnect()
|
||||
return
|
||||
|
||||
elif find(b"CAPA"):
|
||||
if INVALID_CAPABILITY_RESPONSE:
|
||||
self.sendLine(INVALID_RESPONSE)
|
||||
else:
|
||||
self.sendCapabilities()
|
||||
|
||||
elif find(b"STLS") and SSL_SUPPORT:
|
||||
self.startTLS()
|
||||
|
||||
elif find(b"USER"):
|
||||
if INVALID_LOGIN_RESPONSE:
|
||||
self.sendLine(INVALID_RESPONSE)
|
||||
return
|
||||
|
||||
resp = None
|
||||
try:
|
||||
self.tmpUser = line.split(" ")[1]
|
||||
resp = VALID_RESPONSE
|
||||
except BaseException:
|
||||
resp = AUTH_DECLINED
|
||||
|
||||
self.sendLine(resp)
|
||||
|
||||
elif find(b"PASS"):
|
||||
resp = None
|
||||
try:
|
||||
pwd = line.split(" ")[1]
|
||||
|
||||
if self.tmpUser is None or pwd is None:
|
||||
resp = AUTH_DECLINED
|
||||
elif self.tmpUser == USER and pwd == PASS:
|
||||
resp = AUTH_ACCEPTED
|
||||
self.loggedIn = True
|
||||
else:
|
||||
resp = AUTH_DECLINED
|
||||
except BaseException:
|
||||
resp = AUTH_DECLINED
|
||||
|
||||
self.sendLine(resp)
|
||||
|
||||
elif find(b"QUIT"):
|
||||
self.loggedIn = False
|
||||
self.sendLine(LOGOUT_COMPLETE)
|
||||
self.disconnect()
|
||||
|
||||
elif INVALID_SERVER_RESPONSE:
|
||||
self.sendLine(INVALID_RESPONSE)
|
||||
|
||||
elif not self.loggedIn:
|
||||
self.sendLine(NOT_LOGGED_IN)
|
||||
|
||||
elif find(b"NOOP"):
|
||||
self.sendLine(VALID_RESPONSE)
|
||||
|
||||
elif find(b"STAT"):
|
||||
if TIMEOUT_DEFERRED:
|
||||
return
|
||||
self.sendLine(STAT)
|
||||
|
||||
elif find(b"LIST"):
|
||||
if TIMEOUT_DEFERRED:
|
||||
return
|
||||
self.sendLine(LIST)
|
||||
|
||||
elif find(b"UIDL"):
|
||||
if TIMEOUT_DEFERRED:
|
||||
return
|
||||
elif not UIDL_SUPPORT:
|
||||
self.sendLine(INVALID_RESPONSE)
|
||||
return
|
||||
|
||||
self.sendLine(UIDL)
|
||||
|
||||
def startTLS(self):
|
||||
if SSL_SUPPORT and self.ctx is not None:
|
||||
self.sendLine(b"+OK Begin TLS negotiation now")
|
||||
self.transport.startTLS(self.ctx)
|
||||
else:
|
||||
self.sendLine(b"-ERR TLS not available")
|
||||
|
||||
def disconnect(self):
|
||||
self.transport.loseConnection()
|
||||
|
||||
|
||||
usage = """popServer.py [arg] (default is Standard POP Server with no messages)
|
||||
no_ssl - Start with no SSL support
|
||||
no_uidl - Start with no UIDL support
|
||||
bad_resp - Send a non-RFC compliant response to the Client
|
||||
bad_cap_resp - send a non-RFC compliant response when the Client sends a 'CAPABILITY' request
|
||||
bad_login_resp - send a non-RFC compliant response when the Client sends a 'LOGIN' request
|
||||
deny - Deny the connection
|
||||
drop - Drop the connection after sending the greeting
|
||||
bad_tls - Send a bad response to a STARTTLS
|
||||
timeout - Do not return a response to a Client request
|
||||
to_deferred - Do not return a response on a 'Select' request. This
|
||||
will test Deferred callback handling
|
||||
slow - Wait 20 seconds after the connection is made to return a Server Greeting
|
||||
"""
|
||||
|
||||
|
||||
def printMessage(msg):
|
||||
print("Server Starting in %s mode" % msg)
|
||||
|
||||
|
||||
def processArg(arg):
|
||||
if arg.lower() == "no_ssl":
|
||||
global SSL_SUPPORT
|
||||
SSL_SUPPORT = False
|
||||
printMessage("NON-SSL")
|
||||
|
||||
elif arg.lower() == "no_uidl":
|
||||
global UIDL_SUPPORT
|
||||
UIDL_SUPPORT = False
|
||||
printMessage("NON-UIDL")
|
||||
|
||||
elif arg.lower() == "bad_resp":
|
||||
global INVALID_SERVER_RESPONSE
|
||||
INVALID_SERVER_RESPONSE = True
|
||||
printMessage("Invalid Server Response")
|
||||
|
||||
elif arg.lower() == "bad_cap_resp":
|
||||
global INVALID_CAPABILITY_RESPONSE
|
||||
INVALID_CAPABILITY_RESPONSE = True
|
||||
printMessage("Invalid Capability Response")
|
||||
|
||||
elif arg.lower() == "bad_login_resp":
|
||||
global INVALID_LOGIN_RESPONSE
|
||||
INVALID_LOGIN_RESPONSE = True
|
||||
printMessage("Invalid Capability Response")
|
||||
|
||||
elif arg.lower() == "deny":
|
||||
global DENY_CONNECTION
|
||||
DENY_CONNECTION = True
|
||||
printMessage("Deny Connection")
|
||||
|
||||
elif arg.lower() == "drop":
|
||||
global DROP_CONNECTION
|
||||
DROP_CONNECTION = True
|
||||
printMessage("Drop Connection")
|
||||
|
||||
elif arg.lower() == "bad_tls":
|
||||
global BAD_TLS_RESPONSE
|
||||
BAD_TLS_RESPONSE = True
|
||||
printMessage("Bad TLS Response")
|
||||
|
||||
elif arg.lower() == "timeout":
|
||||
global TIMEOUT_RESPONSE
|
||||
TIMEOUT_RESPONSE = True
|
||||
printMessage("Timeout Response")
|
||||
|
||||
elif arg.lower() == "to_deferred":
|
||||
global TIMEOUT_DEFERRED
|
||||
TIMEOUT_DEFERRED = True
|
||||
printMessage("Timeout Deferred Response")
|
||||
|
||||
elif arg.lower() == "slow":
|
||||
global SLOW_GREETING
|
||||
SLOW_GREETING = True
|
||||
printMessage("Slow Greeting")
|
||||
|
||||
elif arg.lower() == "--help":
|
||||
print(usage)
|
||||
sys.exit()
|
||||
|
||||
else:
|
||||
print(usage)
|
||||
sys.exit()
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
printMessage("POP3 with no messages")
|
||||
else:
|
||||
args = sys.argv[1:]
|
||||
|
||||
for arg in args:
|
||||
processArg(arg)
|
||||
|
||||
f = Factory()
|
||||
f.protocol = POP3TestServer
|
||||
reactor.listenTCP(PORT, f)
|
||||
reactor.run()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,86 @@
|
||||
Return-Path: <twisted-commits-admin@twistedmatrix.com>
|
||||
Delivered-To: exarkun@meson.dyndns.org
|
||||
Received: from localhost [127.0.0.1]
|
||||
by localhost with POP3 (fetchmail-6.2.1)
|
||||
for exarkun@localhost (single-drop); Thu, 20 Mar 2003 14:50:20 -0500 (EST)
|
||||
Received: from pyramid.twistedmatrix.com (adsl-64-123-27-105.dsl.austtx.swbell.net [64.123.27.105])
|
||||
by intarweb.us (Postfix) with ESMTP id 4A4A513EA4
|
||||
for <exarkun@meson.dyndns.org>; Thu, 20 Mar 2003 14:49:27 -0500 (EST)
|
||||
Received: from localhost ([127.0.0.1] helo=pyramid.twistedmatrix.com)
|
||||
by pyramid.twistedmatrix.com with esmtp (Exim 3.35 #1 (Debian))
|
||||
id 18w648-0007Vl-00; Thu, 20 Mar 2003 13:51:04 -0600
|
||||
Received: from acapnotic by pyramid.twistedmatrix.com with local (Exim 3.35 #1 (Debian))
|
||||
id 18w63j-0007VK-00
|
||||
for <twisted-commits@twistedmatrix.com>; Thu, 20 Mar 2003 13:50:39 -0600
|
||||
To: twisted-commits@twistedmatrix.com
|
||||
From: etrepum CVS <etrepum@twistedmatrix.com>
|
||||
Reply-To: twisted-python@twistedmatrix.com
|
||||
X-Mailer: CVSToys
|
||||
Message-Id: <E18w63j-0007VK-00@pyramid.twistedmatrix.com>
|
||||
Subject: [Twisted-commits] rebuild now works on python versions from 2.2.0 and up.
|
||||
Sender: twisted-commits-admin@twistedmatrix.com
|
||||
Errors-To: twisted-commits-admin@twistedmatrix.com
|
||||
X-BeenThere: twisted-commits@twistedmatrix.com
|
||||
X-Mailman-Version: 2.0.11
|
||||
Precedence: bulk
|
||||
List-Help: <mailto:twisted-commits-request@twistedmatrix.com?subject=help>
|
||||
List-Post: <mailto:twisted-commits@twistedmatrix.com>
|
||||
List-Subscribe: <http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits>,
|
||||
<mailto:twisted-commits-request@twistedmatrix.com?subject=subscribe>
|
||||
List-Id: <twisted-commits.twistedmatrix.com>
|
||||
List-Unsubscribe: <http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits>,
|
||||
<mailto:twisted-commits-request@twistedmatrix.com?subject=unsubscribe>
|
||||
List-Archive: <http://twistedmatrix.com/pipermail/twisted-commits/>
|
||||
Date: Thu, 20 Mar 2003 13:50:39 -0600
|
||||
|
||||
Modified files:
|
||||
Twisted/twisted/python/rebuild.py 1.19 1.20
|
||||
|
||||
Log message:
|
||||
rebuild now works on python versions from 2.2.0 and up.
|
||||
|
||||
|
||||
ViewCVS links:
|
||||
http://twistedmatrix.com/users/jh.twistd/viewcvs/cgi/viewcvs.cgi/twisted/python/rebuild.py.diff?r1=text&tr1=1.19&r2=text&tr2=1.20&cvsroot=Twisted
|
||||
|
||||
Index: Twisted/twisted/python/rebuild.py
|
||||
diff -u Twisted/twisted/python/rebuild.py:1.19 Twisted/twisted/python/rebuild.py:1.20
|
||||
--- Twisted/twisted/python/rebuild.py:1.19 Fri Jan 17 13:50:49 2003
|
||||
+++ Twisted/twisted/python/rebuild.py Thu Mar 20 11:50:08 2003
|
||||
@@ -206,15 +206,27 @@
|
||||
clazz.__dict__.clear()
|
||||
clazz.__getattr__ = __getattr__
|
||||
clazz.__module__ = module.__name__
|
||||
+ if newclasses:
|
||||
+ import gc
|
||||
+ if (2, 2, 0) <= sys.version_info[:3] < (2, 2, 2):
|
||||
+ hasBrokenRebuild = 1
|
||||
+ gc_objects = gc.get_objects()
|
||||
+ else:
|
||||
+ hasBrokenRebuild = 0
|
||||
for nclass in newclasses:
|
||||
ga = getattr(module, nclass.__name__)
|
||||
if ga is nclass:
|
||||
log.msg("WARNING: new-class %s not replaced by reload!" % reflect.qual(nclass))
|
||||
else:
|
||||
- import gc
|
||||
- for r in gc.get_referrers(nclass):
|
||||
- if isinstance(r, nclass):
|
||||
+ if hasBrokenRebuild:
|
||||
+ for r in gc_objects:
|
||||
+ if not getattr(r, '__class__', None) is nclass:
|
||||
+ continue
|
||||
r.__class__ = ga
|
||||
+ else:
|
||||
+ for r in gc.get_referrers(nclass):
|
||||
+ if getattr(r, '__class__', None) is nclass:
|
||||
+ r.__class__ = ga
|
||||
if doLog:
|
||||
log.msg('')
|
||||
log.msg(' (fixing %s): ' % str(module.__name__))
|
||||
|
||||
|
||||
_______________________________________________
|
||||
Twisted-commits mailing list
|
||||
Twisted-commits@twistedmatrix.com
|
||||
http://twistedmatrix.com/cgi-bin/mailman/listinfo/twisted-commits
|
||||
@@ -0,0 +1,135 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
from __future__ import annotations
|
||||
|
||||
import email.message
|
||||
import email.parser
|
||||
from email.message import Message
|
||||
from io import BytesIO, StringIO
|
||||
from typing import IO, AnyStr, Callable
|
||||
|
||||
from twisted.mail import bounce
|
||||
from twisted.trial import unittest
|
||||
|
||||
|
||||
class BounceTests(unittest.TestCase):
|
||||
"""
|
||||
Bounce message generation
|
||||
"""
|
||||
|
||||
def test_bounceMessageUnicode(self) -> None:
|
||||
"""
|
||||
L{twisted.mail.bounce.generateBounce} can accept L{unicode}.
|
||||
"""
|
||||
fromAddress, to, s = bounce.generateBounce(
|
||||
StringIO(
|
||||
"""\
|
||||
From: Moshe Zadka <moshez@example.com>
|
||||
To: nonexistent@example.org
|
||||
Subject: test
|
||||
|
||||
"""
|
||||
),
|
||||
"moshez@example.com",
|
||||
"nonexistent@example.org",
|
||||
)
|
||||
self.assertEqual(fromAddress, b"")
|
||||
self.assertEqual(to, b"moshez@example.com")
|
||||
emailParser = email.parser.Parser()
|
||||
mess = emailParser.parse(StringIO(s.decode("utf-8")))
|
||||
self.assertEqual(mess["To"], "moshez@example.com")
|
||||
self.assertEqual(mess["From"], "postmaster@example.org")
|
||||
self.assertEqual(mess["subject"], "Returned Mail: see transcript for details")
|
||||
|
||||
def test_bounceMessageBytes(self) -> None:
|
||||
"""
|
||||
L{twisted.mail.bounce.generateBounce} can accept L{bytes}.
|
||||
"""
|
||||
fromAddress, to, s = bounce.generateBounce(
|
||||
BytesIO(
|
||||
b"""\
|
||||
From: Moshe Zadka <moshez@example.com>
|
||||
To: nonexistent@example.org
|
||||
Subject: test
|
||||
|
||||
"""
|
||||
),
|
||||
b"moshez@example.com",
|
||||
b"nonexistent@example.org",
|
||||
)
|
||||
self.assertEqual(fromAddress, b"")
|
||||
self.assertEqual(to, b"moshez@example.com")
|
||||
emailParser = email.parser.Parser()
|
||||
mess = emailParser.parse(StringIO(s.decode("utf-8")))
|
||||
self.assertEqual(mess["To"], "moshez@example.com")
|
||||
self.assertEqual(mess["From"], "postmaster@example.org")
|
||||
self.assertEqual(mess["subject"], "Returned Mail: see transcript for details")
|
||||
|
||||
def test_bounceMessageCustomTranscript(self) -> None:
|
||||
"""
|
||||
Pass a custom transcript message to L{twisted.mail.bounce.generateBounce}.
|
||||
"""
|
||||
fromAddress, to, s = bounce.generateBounce(
|
||||
BytesIO(
|
||||
b"""\
|
||||
From: Moshe Zadka <moshez@example.com>
|
||||
To: nonexistent@example.org
|
||||
Subject: test
|
||||
|
||||
"""
|
||||
),
|
||||
b"moshez@example.com",
|
||||
b"nonexistent@example.org",
|
||||
"Custom transcript",
|
||||
)
|
||||
self.assertEqual(fromAddress, b"")
|
||||
self.assertEqual(to, b"moshez@example.com")
|
||||
emailParser = email.parser.Parser()
|
||||
mess = emailParser.parse(StringIO(s.decode("utf-8")))
|
||||
self.assertEqual(mess["To"], "moshez@example.com")
|
||||
self.assertEqual(mess["From"], "postmaster@example.org")
|
||||
self.assertEqual(mess["subject"], "Returned Mail: see transcript for details")
|
||||
self.assertTrue(mess.is_multipart())
|
||||
parts: list[Message] = mess.get_payload() # type:ignore[assignment]
|
||||
self.assertEqual(
|
||||
parts[0].get_payload(),
|
||||
"Custom transcript\n",
|
||||
)
|
||||
|
||||
def _bounceBigMessage(
|
||||
self, header: AnyStr, message: AnyStr, ioType: Callable[[AnyStr], IO[AnyStr]]
|
||||
) -> None:
|
||||
"""
|
||||
Pass a really big message to L{twisted.mail.bounce.generateBounce}.
|
||||
"""
|
||||
fromAddress, to, s = bounce.generateBounce(
|
||||
ioType(header + message), "moshez@example.com", "nonexistent@example.org"
|
||||
)
|
||||
emailParser = email.parser.Parser()
|
||||
mess = emailParser.parse(StringIO(s.decode("utf-8")))
|
||||
self.assertEqual(mess["To"], "moshez@example.com")
|
||||
self.assertEqual(mess["From"], "postmaster@example.org")
|
||||
self.assertEqual(mess["subject"], "Returned Mail: see transcript for details")
|
||||
self.assertTrue(mess.is_multipart())
|
||||
parts: list[Message] = mess.get_payload() # type:ignore[assignment]
|
||||
innerMessage: list[Message] = parts[1].get_payload() # type:ignore[assignment]
|
||||
if isinstance(message, bytes):
|
||||
messageText = message.decode("utf-8")
|
||||
else:
|
||||
messageText = message
|
||||
pl: str = innerMessage[0].get_payload() # type:ignore[assignment]
|
||||
self.assertEqual(pl + "\n", messageText)
|
||||
|
||||
def test_bounceBigMessage(self) -> None:
|
||||
"""
|
||||
L{twisted.mail.bounce.generateBounce} with big L{unicode} and
|
||||
L{bytes} messages.
|
||||
"""
|
||||
header = b"""\
|
||||
From: Moshe Zadka <moshez@example.com>
|
||||
To: nonexistent@example.org
|
||||
Subject: test
|
||||
|
||||
"""
|
||||
self._bounceBigMessage(header, b"Test test\n" * 10000, BytesIO)
|
||||
self._bounceBigMessage(header.decode("utf-8"), "More test\n" * 10000, StringIO)
|
||||
7974
backend/lib/python3.12/site-packages/twisted/mail/test/test_imap.py
Normal file
7974
backend/lib/python3.12/site-packages/twisted/mail/test/test_imap.py
Normal file
File diff suppressed because it is too large
Load Diff
2643
backend/lib/python3.12/site-packages/twisted/mail/test/test_mail.py
Normal file
2643
backend/lib/python3.12/site-packages/twisted/mail/test/test_mail.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,391 @@
|
||||
# -*- test-case-name: twisted.mail.test.test_mailmail -*-
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.mail.scripts.mailmail}, the implementation of the
|
||||
command line program I{mailmail}.
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from io import StringIO
|
||||
from unittest import skipIf
|
||||
|
||||
from twisted.copyright import version
|
||||
from twisted.internet.defer import Deferred
|
||||
from twisted.internet.testing import MemoryReactor
|
||||
from twisted.mail import smtp
|
||||
from twisted.mail.scripts import mailmail
|
||||
from twisted.mail.scripts.mailmail import parseOptions
|
||||
from twisted.python.failure import Failure
|
||||
from twisted.python.runtime import platformType
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class OptionsTests(TestCase):
|
||||
"""
|
||||
Tests for L{parseOptions} which parses command line arguments and reads
|
||||
message text from stdin to produce an L{Options} instance which can be
|
||||
used to send a message.
|
||||
"""
|
||||
|
||||
memoryReactor = MemoryReactor()
|
||||
|
||||
def setUp(self):
|
||||
"""
|
||||
Override some things in mailmail, so that we capture C{stdout},
|
||||
and do not call L{reactor.stop}.
|
||||
"""
|
||||
self.out = StringIO()
|
||||
# Override the mailmail logger, so we capture stderr output
|
||||
from twisted.logger import Logger, textFileLogObserver
|
||||
|
||||
logObserver = textFileLogObserver(self.out)
|
||||
self.patch(mailmail, "_log", Logger(observer=logObserver))
|
||||
self.host = None
|
||||
self.options = None
|
||||
self.ident = None
|
||||
|
||||
# Override mailmail.sendmail, so we don't call reactor.stop()
|
||||
def sendmail(host, options, ident):
|
||||
self.host = host
|
||||
self.options = options
|
||||
self.ident = ident
|
||||
return smtp.sendmail(
|
||||
host,
|
||||
options.sender,
|
||||
options.to,
|
||||
options.body,
|
||||
reactor=self.memoryReactor,
|
||||
)
|
||||
|
||||
self.patch(mailmail, "sendmail", sendmail)
|
||||
|
||||
def test_unspecifiedRecipients(self):
|
||||
"""
|
||||
If no recipients are given in the argument list and there is no
|
||||
recipient header in the message text, L{parseOptions} raises
|
||||
L{SystemExit} with a string describing the problem.
|
||||
"""
|
||||
self.patch(sys, "stdin", StringIO("Subject: foo\n" "\n" "Hello, goodbye.\n"))
|
||||
exc = self.assertRaises(SystemExit, parseOptions, [])
|
||||
self.assertEqual(exc.args, ("No recipients specified.",))
|
||||
|
||||
def test_listQueueInformation(self):
|
||||
"""
|
||||
The I{-bp} option for listing queue information is unsupported and
|
||||
if it is passed to L{parseOptions}, L{SystemExit} is raised.
|
||||
"""
|
||||
exc = self.assertRaises(SystemExit, parseOptions, ["-bp"])
|
||||
self.assertEqual(exc.args, ("Unsupported option.",))
|
||||
|
||||
def test_stdioTransport(self):
|
||||
"""
|
||||
The I{-bs} option for using stdin and stdout as the SMTP transport
|
||||
is unsupported and if it is passed to L{parseOptions}, L{SystemExit}
|
||||
is raised.
|
||||
"""
|
||||
exc = self.assertRaises(SystemExit, parseOptions, ["-bs"])
|
||||
self.assertEqual(exc.args, ("Unsupported option.",))
|
||||
|
||||
def test_ignoreFullStop(self):
|
||||
"""
|
||||
The I{-i} and I{-oi} options for ignoring C{"."} by itself on a line
|
||||
are unsupported and if either is passed to L{parseOptions},
|
||||
L{SystemExit} is raised.
|
||||
"""
|
||||
exc = self.assertRaises(SystemExit, parseOptions, ["-i"])
|
||||
self.assertEqual(exc.args, ("Unsupported option.",))
|
||||
exc = self.assertRaises(SystemExit, parseOptions, ["-oi"])
|
||||
self.assertEqual(exc.args, ("Unsupported option.",))
|
||||
|
||||
def test_copyAliasedSender(self):
|
||||
"""
|
||||
The I{-om} option for copying the sender if they appear in an alias
|
||||
expansion is unsupported and if it is passed to L{parseOptions},
|
||||
L{SystemExit} is raised.
|
||||
"""
|
||||
exc = self.assertRaises(SystemExit, parseOptions, ["-om"])
|
||||
self.assertEqual(exc.args, ("Unsupported option.",))
|
||||
|
||||
def test_version(self):
|
||||
"""
|
||||
The I{--version} option displays the version and raises
|
||||
L{SystemExit} with L{None} as the exit code.
|
||||
"""
|
||||
out = StringIO()
|
||||
self.patch(sys, "stdout", out)
|
||||
systemExitCode = self.assertRaises(SystemExit, parseOptions, "--version")
|
||||
# SystemExit.code is None on success
|
||||
self.assertEqual(systemExitCode.code, None)
|
||||
data = out.getvalue()
|
||||
self.assertEqual(data, f"mailmail version: {version}\n")
|
||||
|
||||
def test_backgroundDelivery(self):
|
||||
"""
|
||||
The I{-odb} flag specifies background delivery.
|
||||
"""
|
||||
stdin = StringIO("\n")
|
||||
self.patch(sys, "stdin", stdin)
|
||||
o = parseOptions("-odb")
|
||||
self.assertTrue(o.background)
|
||||
|
||||
def test_foregroundDelivery(self):
|
||||
"""
|
||||
The I{-odf} flags specifies foreground delivery.
|
||||
"""
|
||||
stdin = StringIO("\n")
|
||||
self.patch(sys, "stdin", stdin)
|
||||
o = parseOptions("-odf")
|
||||
self.assertFalse(o.background)
|
||||
|
||||
def test_recipientsFromHeaders(self):
|
||||
"""
|
||||
The I{-t} flags specifies that recipients should be obtained
|
||||
from headers.
|
||||
"""
|
||||
stdin = StringIO(
|
||||
"To: Curly <invaliduser2@example.com>\n"
|
||||
"Cc: Larry <invaliduser1@example.com>\n"
|
||||
"Bcc: Moe <invaliduser3@example.com>\n"
|
||||
"\n"
|
||||
"Oh, a wise guy?\n"
|
||||
)
|
||||
self.patch(sys, "stdin", stdin)
|
||||
o = parseOptions("-t")
|
||||
self.assertEqual(len(o.to), 3)
|
||||
|
||||
def test_setFrom(self):
|
||||
"""
|
||||
When a message has no I{From:} header, a I{From:} value can be
|
||||
specified with the I{-F} flag.
|
||||
"""
|
||||
stdin = StringIO("To: invaliduser2@example.com\n" "Subject: A wise guy?\n\n")
|
||||
self.patch(sys, "stdin", stdin)
|
||||
o = parseOptions(["-F", "Larry <invaliduser1@example.com>", "-t"])
|
||||
self.assertEqual(o.sender, "Larry <invaliduser1@example.com>")
|
||||
|
||||
def test_overrideFromFlagByFromHeader(self):
|
||||
"""
|
||||
The I{-F} flag specifies the From: value. However, I{-F} flag is
|
||||
overriden by the value of From: in the e-mail header.
|
||||
"""
|
||||
stdin = StringIO(
|
||||
"To: Curly <invaliduser4@example.com>\n"
|
||||
"From: Shemp <invaliduser4@example.com>\n"
|
||||
)
|
||||
self.patch(sys, "stdin", stdin)
|
||||
o = parseOptions(["-F", "Groucho <invaliduser5@example.com>", "-t"])
|
||||
self.assertEqual(o.sender, "invaliduser4@example.com")
|
||||
|
||||
@skipIf(
|
||||
platformType == "win32",
|
||||
"mailmail.run() does not work on win32 due to lack of support for" " getuid()",
|
||||
)
|
||||
def test_runErrorsToStderr(self):
|
||||
"""
|
||||
Call L{mailmail.run}, and specify I{-oep} to print errors
|
||||
to stderr. The sender, to, and printErrors options should be
|
||||
set and there should be no failure.
|
||||
"""
|
||||
argv = ("test_mailmail.py", "invaliduser2@example.com", "-oep")
|
||||
stdin = StringIO("\n")
|
||||
self.patch(sys, "argv", argv)
|
||||
self.patch(sys, "stdin", stdin)
|
||||
mailmail.run()
|
||||
self.assertEqual(self.options.sender, mailmail.getlogin())
|
||||
self.assertEqual(self.options.to, ["invaliduser2@example.com"])
|
||||
# We should have printErrors set because we specified "-oep"
|
||||
self.assertTrue(self.options.printErrors)
|
||||
# We should not have any failures.
|
||||
self.assertIsNone(mailmail.failed)
|
||||
|
||||
@skipIf(
|
||||
platformType == "win32",
|
||||
"mailmail.run() does not work on win32 due to lack of support for" " getuid()",
|
||||
)
|
||||
def test_readInvalidConfig(self):
|
||||
"""
|
||||
Error messages for illegal UID value, illegal GID value, and illegal
|
||||
identity entry will be sent to stderr.
|
||||
"""
|
||||
stdin = StringIO("\n")
|
||||
self.patch(sys, "stdin", stdin)
|
||||
|
||||
filename = self.mktemp()
|
||||
myUid = os.getuid()
|
||||
myGid = os.getgid()
|
||||
|
||||
with open(filename, "w") as f:
|
||||
# Create a config file with some invalid values
|
||||
f.write(
|
||||
"[useraccess]\n"
|
||||
"allow=invaliduser2,invaliduser1\n"
|
||||
"deny=invaliduser3,invaliduser4,{}\n"
|
||||
"order=allow,deny\n"
|
||||
"[groupaccess]\n"
|
||||
"allow=invalidgid1,invalidgid2\n"
|
||||
"deny=invalidgid1,invalidgid2,{}\n"
|
||||
"order=deny,allow\n"
|
||||
"[identity]\n"
|
||||
"localhost=funny\n"
|
||||
"[addresses]\n"
|
||||
"smarthost=localhost\n"
|
||||
"default_domain=example.com\n".format(myUid, myGid)
|
||||
)
|
||||
|
||||
# The mailmail script looks in
|
||||
# the twisted.mail.scripts.GLOBAL_CFG variable
|
||||
# and then the twisted.mail.scripts.LOCAL_CFG
|
||||
# variable for the path to it's config file.
|
||||
#
|
||||
# Override twisted.mail.scripts.LOCAL_CFG with the file we just
|
||||
# created.
|
||||
self.patch(mailmail, "LOCAL_CFG", filename)
|
||||
|
||||
argv = ("test_mailmail.py", "invaliduser2@example.com", "-oep")
|
||||
self.patch(sys, "argv", argv)
|
||||
mailmail.run()
|
||||
self.assertRegex(
|
||||
self.out.getvalue(),
|
||||
"Illegal UID in \\[useraccess\\] section: " "invaliduser1",
|
||||
)
|
||||
self.assertRegex(
|
||||
self.out.getvalue(),
|
||||
"Illegal GID in \\[groupaccess\\] section: " "invalidgid1",
|
||||
)
|
||||
self.assertRegex(
|
||||
self.out.getvalue(), "Illegal entry in \\[identity\\] section: funny"
|
||||
)
|
||||
|
||||
def getConfigFromFile(self, config):
|
||||
"""
|
||||
Read a mailmail configuration file.
|
||||
|
||||
The mailmail script checks the twisted.mail.scripts.mailmail.GLOBAL_CFG
|
||||
variable and then the twisted.mail.scripts.mailmail.LOCAL_CFG
|
||||
variable for the path to its config file.
|
||||
|
||||
@param config: path to config file
|
||||
@type config: L{str}
|
||||
|
||||
@return: A parsed config.
|
||||
@rtype: L{twisted.mail.scripts.mailmail.Configuration}
|
||||
"""
|
||||
|
||||
from twisted.mail.scripts.mailmail import loadConfig
|
||||
|
||||
filename = self.mktemp()
|
||||
|
||||
with open(filename, "w") as f:
|
||||
f.write(config)
|
||||
|
||||
return loadConfig(filename)
|
||||
|
||||
def test_loadConfig(self):
|
||||
"""
|
||||
L{twisted.mail.scripts.mailmail.loadConfig}
|
||||
parses the config file for mailmail.
|
||||
"""
|
||||
config = self.getConfigFromFile(
|
||||
"""
|
||||
[addresses]
|
||||
smarthost=localhost"""
|
||||
)
|
||||
self.assertEqual(config.smarthost, "localhost")
|
||||
|
||||
config = self.getConfigFromFile(
|
||||
"""
|
||||
[addresses]
|
||||
default_domain=example.com"""
|
||||
)
|
||||
self.assertEqual(config.domain, "example.com")
|
||||
|
||||
config = self.getConfigFromFile(
|
||||
"""
|
||||
[addresses]
|
||||
smarthost=localhost
|
||||
default_domain=example.com"""
|
||||
)
|
||||
self.assertEqual(config.smarthost, "localhost")
|
||||
self.assertEqual(config.domain, "example.com")
|
||||
|
||||
config = self.getConfigFromFile(
|
||||
"""
|
||||
[identity]
|
||||
host1=invalid
|
||||
host2=username:password"""
|
||||
)
|
||||
self.assertNotIn("host1", config.identities)
|
||||
self.assertEqual(config.identities["host2"], ["username", "password"])
|
||||
|
||||
config = self.getConfigFromFile(
|
||||
"""
|
||||
[useraccess]
|
||||
allow=invalid1,35
|
||||
order=allow"""
|
||||
)
|
||||
self.assertEqual(config.allowUIDs, [35])
|
||||
|
||||
config = self.getConfigFromFile(
|
||||
"""
|
||||
[useraccess]
|
||||
deny=35,36
|
||||
order=deny"""
|
||||
)
|
||||
self.assertEqual(config.denyUIDs, [35, 36])
|
||||
|
||||
config = self.getConfigFromFile(
|
||||
"""
|
||||
[useraccess]
|
||||
allow=35,36
|
||||
deny=37,38
|
||||
order=deny"""
|
||||
)
|
||||
self.assertEqual(config.allowUIDs, [35, 36])
|
||||
self.assertEqual(config.denyUIDs, [37, 38])
|
||||
|
||||
config = self.getConfigFromFile(
|
||||
"""
|
||||
[groupaccess]
|
||||
allow=gid1,41
|
||||
order=allow"""
|
||||
)
|
||||
self.assertEqual(config.allowGIDs, [41])
|
||||
|
||||
config = self.getConfigFromFile(
|
||||
"""
|
||||
[groupaccess]
|
||||
deny=41
|
||||
order=deny"""
|
||||
)
|
||||
self.assertEqual(config.denyGIDs, [41])
|
||||
|
||||
config = self.getConfigFromFile(
|
||||
"""
|
||||
[groupaccess]
|
||||
allow=41,42
|
||||
deny=43,44
|
||||
order=allow,deny"""
|
||||
)
|
||||
self.assertEqual(config.allowGIDs, [41, 42])
|
||||
self.assertEqual(config.denyGIDs, [43, 44])
|
||||
|
||||
def test_senderror(self):
|
||||
"""
|
||||
L{twisted.mail.scripts.mailmail.senderror} sends mail back to the
|
||||
sender if an error occurs while sending mail to the recipient.
|
||||
"""
|
||||
|
||||
def sendmail(host, sender, recipient, body):
|
||||
self.assertRegex(sender, "postmaster@")
|
||||
self.assertEqual(recipient, ["testsender"])
|
||||
self.assertRegex(body.getvalue(), "ValueError")
|
||||
return Deferred()
|
||||
|
||||
self.patch(smtp, "sendmail", sendmail)
|
||||
opts = mailmail.Options()
|
||||
opts.sender = "testsender"
|
||||
fail = Failure(ValueError())
|
||||
mailmail.senderror(fail, opts)
|
||||
@@ -0,0 +1,183 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for L{twisted.mail.tap}.
|
||||
"""
|
||||
|
||||
from twisted.internet import defer, endpoints
|
||||
from twisted.mail import protocols
|
||||
from twisted.mail.tap import Options, makeService
|
||||
from twisted.python.usage import UsageError
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class OptionsTests(TestCase):
|
||||
"""
|
||||
Tests for the command line option parser used for I{twistd mail}.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
self.aliasFilename = self.mktemp()
|
||||
with open(self.aliasFilename, "w") as aliasFile:
|
||||
aliasFile.write("someuser:\tdifferentuser\n")
|
||||
|
||||
def testAliasesWithoutDomain(self):
|
||||
"""
|
||||
Test that adding an aliases(5) file before adding a domain raises a
|
||||
UsageError.
|
||||
"""
|
||||
self.assertRaises(
|
||||
UsageError, Options().parseOptions, ["--aliases", self.aliasFilename]
|
||||
)
|
||||
|
||||
def testAliases(self):
|
||||
"""
|
||||
Test that adding an aliases(5) file to an IAliasableDomain at least
|
||||
doesn't raise an unhandled exception.
|
||||
"""
|
||||
Options().parseOptions(
|
||||
[
|
||||
"--maildirdbmdomain",
|
||||
"example.com=example.com",
|
||||
"--aliases",
|
||||
self.aliasFilename,
|
||||
]
|
||||
)
|
||||
|
||||
def _endpointTest(self, service):
|
||||
"""
|
||||
Use L{Options} to parse a single service configuration parameter and
|
||||
verify that an endpoint of the correct type is added to the list for
|
||||
that service.
|
||||
"""
|
||||
options = Options()
|
||||
options.parseOptions(["--" + service, "tcp:1234"])
|
||||
self.assertEqual(len(options[service]), 1)
|
||||
self.assertIsInstance(options[service][0], endpoints.TCP4ServerEndpoint)
|
||||
|
||||
def test_endpointSMTP(self):
|
||||
"""
|
||||
When I{--smtp} is given a TCP endpoint description as an argument, a
|
||||
TCPServerEndpoint is added to the list of SMTP endpoints.
|
||||
"""
|
||||
self._endpointTest("smtp")
|
||||
|
||||
def test_endpointPOP3(self):
|
||||
"""
|
||||
When I{--pop3} is given a TCP endpoint description as an argument, a
|
||||
TCPServerEndpoint is added to the list of POP3 endpoints.
|
||||
"""
|
||||
self._endpointTest("pop3")
|
||||
|
||||
def test_protoDefaults(self):
|
||||
"""
|
||||
POP3 and SMTP each listen on a TCP4ServerEndpoint by default.
|
||||
"""
|
||||
options = Options()
|
||||
options.parseOptions([])
|
||||
|
||||
self.assertEqual(len(options["pop3"]), 1)
|
||||
self.assertIsInstance(options["pop3"][0], endpoints.TCP4ServerEndpoint)
|
||||
|
||||
self.assertEqual(len(options["smtp"]), 1)
|
||||
self.assertIsInstance(options["smtp"][0], endpoints.TCP4ServerEndpoint)
|
||||
|
||||
def test_protoDisable(self):
|
||||
"""
|
||||
The I{--no-pop3} and I{--no-smtp} options disable POP3 and SMTP
|
||||
respectively.
|
||||
"""
|
||||
options = Options()
|
||||
options.parseOptions(["--no-pop3"])
|
||||
self.assertEqual(options._getEndpoints(None, "pop3"), [])
|
||||
self.assertNotEqual(options._getEndpoints(None, "smtp"), [])
|
||||
|
||||
options = Options()
|
||||
options.parseOptions(["--no-smtp"])
|
||||
self.assertNotEqual(options._getEndpoints(None, "pop3"), [])
|
||||
self.assertEqual(options._getEndpoints(None, "smtp"), [])
|
||||
|
||||
def test_allProtosDisabledError(self):
|
||||
"""
|
||||
If all protocols are disabled, L{UsageError} is raised.
|
||||
"""
|
||||
options = Options()
|
||||
self.assertRaises(
|
||||
UsageError, options.parseOptions, (["--no-pop3", "--no-smtp"])
|
||||
)
|
||||
|
||||
def test_esmtpWithoutHostname(self):
|
||||
"""
|
||||
If I{--esmtp} is given without I{--hostname}, L{Options.parseOptions}
|
||||
raises L{UsageError}.
|
||||
"""
|
||||
options = Options()
|
||||
exc = self.assertRaises(UsageError, options.parseOptions, ["--esmtp"])
|
||||
self.assertEqual("--esmtp requires --hostname", str(exc))
|
||||
|
||||
def test_auth(self):
|
||||
"""
|
||||
Tests that the --auth option registers a checker.
|
||||
"""
|
||||
options = Options()
|
||||
options.parseOptions(["--auth", "memory:admin:admin:bob:password"])
|
||||
self.assertEqual(len(options["credCheckers"]), 1)
|
||||
checker = options["credCheckers"][0]
|
||||
interfaces = checker.credentialInterfaces
|
||||
registered_checkers = options.service.smtpPortal.checkers
|
||||
for iface in interfaces:
|
||||
self.assertEqual(checker, registered_checkers[iface])
|
||||
|
||||
|
||||
class SpyEndpoint:
|
||||
"""
|
||||
SpyEndpoint remembers what factory it is told to listen with.
|
||||
"""
|
||||
|
||||
listeningWith = None
|
||||
|
||||
def listen(self, factory):
|
||||
self.listeningWith = factory
|
||||
return defer.succeed(None)
|
||||
|
||||
|
||||
class MakeServiceTests(TestCase):
|
||||
"""
|
||||
Tests for L{twisted.mail.tap.makeService}
|
||||
"""
|
||||
|
||||
def _endpointServerTest(self, key, factoryClass):
|
||||
"""
|
||||
Configure a service with two endpoints for the protocol associated with
|
||||
C{key} and verify that when the service is started a factory of type
|
||||
C{factoryClass} is used to listen on each of them.
|
||||
"""
|
||||
cleartext = SpyEndpoint()
|
||||
secure = SpyEndpoint()
|
||||
config = Options()
|
||||
config[key] = [cleartext, secure]
|
||||
service = makeService(config)
|
||||
service.privilegedStartService()
|
||||
service.startService()
|
||||
self.addCleanup(service.stopService)
|
||||
self.assertIsInstance(cleartext.listeningWith, factoryClass)
|
||||
self.assertIsInstance(secure.listeningWith, factoryClass)
|
||||
|
||||
def test_pop3(self):
|
||||
"""
|
||||
If one or more endpoints is included in the configuration passed to
|
||||
L{makeService} for the C{"pop3"} key, a service for starting a POP3
|
||||
server is constructed for each of them and attached to the returned
|
||||
service.
|
||||
"""
|
||||
self._endpointServerTest("pop3", protocols.POP3Factory)
|
||||
|
||||
def test_smtp(self):
|
||||
"""
|
||||
If one or more endpoints is included in the configuration passed to
|
||||
L{makeService} for the C{"smtp"} key, a service for starting an SMTP
|
||||
server is constructed for each of them and attached to the returned
|
||||
service.
|
||||
"""
|
||||
self._endpointServerTest("smtp", protocols.SMTPFactory)
|
||||
1613
backend/lib/python3.12/site-packages/twisted/mail/test/test_pop3.py
Normal file
1613
backend/lib/python3.12/site-packages/twisted/mail/test/test_pop3.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,639 @@
|
||||
# -*- test-case-name: twisted.mail.test.test_pop3client -*-
|
||||
# Copyright (c) 2001-2004 Divmod Inc.
|
||||
# See LICENSE for details.
|
||||
|
||||
import inspect
|
||||
import sys
|
||||
from typing import List
|
||||
from unittest import skipIf
|
||||
|
||||
from zope.interface import directlyProvides
|
||||
|
||||
import twisted.mail._pop3client
|
||||
from twisted.internet import defer, error, interfaces, protocol, reactor
|
||||
from twisted.internet.testing import StringTransport
|
||||
from twisted.mail.pop3 import (
|
||||
AdvancedPOP3Client as POP3Client,
|
||||
InsecureAuthenticationDisallowed,
|
||||
ServerErrorResponse,
|
||||
)
|
||||
from twisted.mail.test import pop3testserver
|
||||
from twisted.protocols import basic, loopback
|
||||
from twisted.python import log
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
try:
|
||||
from twisted.test.ssl_helpers import ClientTLSContext, ServerTLSContext
|
||||
except ImportError:
|
||||
ClientTLSContext = None # type: ignore[assignment,misc]
|
||||
ServerTLSContext = None # type: ignore[assignment,misc]
|
||||
|
||||
|
||||
class StringTransportWithConnectionLosing(StringTransport):
|
||||
def loseConnection(self):
|
||||
self.protocol.connectionLost(error.ConnectionDone())
|
||||
|
||||
|
||||
capCache = {
|
||||
b"TOP": None,
|
||||
b"LOGIN-DELAY": b"180",
|
||||
b"UIDL": None,
|
||||
b"STLS": None,
|
||||
b"USER": None,
|
||||
b"SASL": b"LOGIN",
|
||||
}
|
||||
|
||||
|
||||
def setUp(greet=True):
|
||||
p = POP3Client()
|
||||
|
||||
# Skip the CAPA login will issue if it doesn't already have a
|
||||
# capability cache
|
||||
p._capCache = capCache
|
||||
|
||||
t = StringTransportWithConnectionLosing()
|
||||
t.protocol = p
|
||||
p.makeConnection(t)
|
||||
|
||||
if greet:
|
||||
p.dataReceived(b"+OK Hello!\r\n")
|
||||
|
||||
return p, t
|
||||
|
||||
|
||||
def strip(f):
|
||||
return lambda result, f=f: f()
|
||||
|
||||
|
||||
class POP3ClientLoginTests(TestCase):
|
||||
def testNegativeGreeting(self):
|
||||
p, t = setUp(greet=False)
|
||||
p.allowInsecureLogin = True
|
||||
d = p.login(b"username", b"password")
|
||||
p.dataReceived(b"-ERR Offline for maintenance\r\n")
|
||||
return self.assertFailure(d, ServerErrorResponse).addCallback(
|
||||
lambda exc: self.assertEqual(exc.args[0], b"Offline for maintenance")
|
||||
)
|
||||
|
||||
def testOkUser(self):
|
||||
p, t = setUp()
|
||||
d = p.user(b"username")
|
||||
self.assertEqual(t.value(), b"USER username\r\n")
|
||||
p.dataReceived(b"+OK send password\r\n")
|
||||
return d.addCallback(self.assertEqual, b"send password")
|
||||
|
||||
def testBadUser(self):
|
||||
p, t = setUp()
|
||||
d = p.user(b"username")
|
||||
self.assertEqual(t.value(), b"USER username\r\n")
|
||||
p.dataReceived(b"-ERR account suspended\r\n")
|
||||
return self.assertFailure(d, ServerErrorResponse).addCallback(
|
||||
lambda exc: self.assertEqual(exc.args[0], b"account suspended")
|
||||
)
|
||||
|
||||
def testOkPass(self):
|
||||
p, t = setUp()
|
||||
d = p.password(b"password")
|
||||
self.assertEqual(t.value(), b"PASS password\r\n")
|
||||
p.dataReceived(b"+OK you're in!\r\n")
|
||||
return d.addCallback(self.assertEqual, b"you're in!")
|
||||
|
||||
def testBadPass(self):
|
||||
p, t = setUp()
|
||||
d = p.password(b"password")
|
||||
self.assertEqual(t.value(), b"PASS password\r\n")
|
||||
p.dataReceived(b"-ERR go away\r\n")
|
||||
return self.assertFailure(d, ServerErrorResponse).addCallback(
|
||||
lambda exc: self.assertEqual(exc.args[0], b"go away")
|
||||
)
|
||||
|
||||
def testOkLogin(self):
|
||||
p, t = setUp()
|
||||
p.allowInsecureLogin = True
|
||||
d = p.login(b"username", b"password")
|
||||
self.assertEqual(t.value(), b"USER username\r\n")
|
||||
p.dataReceived(b"+OK go ahead\r\n")
|
||||
self.assertEqual(t.value(), b"USER username\r\nPASS password\r\n")
|
||||
p.dataReceived(b"+OK password accepted\r\n")
|
||||
return d.addCallback(self.assertEqual, b"password accepted")
|
||||
|
||||
def testBadPasswordLogin(self):
|
||||
p, t = setUp()
|
||||
p.allowInsecureLogin = True
|
||||
d = p.login(b"username", b"password")
|
||||
self.assertEqual(t.value(), b"USER username\r\n")
|
||||
p.dataReceived(b"+OK waiting on you\r\n")
|
||||
self.assertEqual(t.value(), b"USER username\r\nPASS password\r\n")
|
||||
p.dataReceived(b"-ERR bogus login\r\n")
|
||||
return self.assertFailure(d, ServerErrorResponse).addCallback(
|
||||
lambda exc: self.assertEqual(exc.args[0], b"bogus login")
|
||||
)
|
||||
|
||||
def testBadUsernameLogin(self):
|
||||
p, t = setUp()
|
||||
p.allowInsecureLogin = True
|
||||
d = p.login(b"username", b"password")
|
||||
self.assertEqual(t.value(), b"USER username\r\n")
|
||||
p.dataReceived(b"-ERR bogus login\r\n")
|
||||
return self.assertFailure(d, ServerErrorResponse).addCallback(
|
||||
lambda exc: self.assertEqual(exc.args[0], b"bogus login")
|
||||
)
|
||||
|
||||
def testServerGreeting(self):
|
||||
p, t = setUp(greet=False)
|
||||
p.dataReceived(b"+OK lalala this has no challenge\r\n")
|
||||
self.assertEqual(p.serverChallenge, None)
|
||||
|
||||
def testServerGreetingWithChallenge(self):
|
||||
p, t = setUp(greet=False)
|
||||
p.dataReceived(b"+OK <here is the challenge>\r\n")
|
||||
self.assertEqual(p.serverChallenge, b"<here is the challenge>")
|
||||
|
||||
def testAPOP(self):
|
||||
p, t = setUp(greet=False)
|
||||
p.dataReceived(b"+OK <challenge string goes here>\r\n")
|
||||
d = p.login(b"username", b"password")
|
||||
self.assertEqual(
|
||||
t.value(), b"APOP username f34f1e464d0d7927607753129cabe39a\r\n"
|
||||
)
|
||||
p.dataReceived(b"+OK Welcome!\r\n")
|
||||
return d.addCallback(self.assertEqual, b"Welcome!")
|
||||
|
||||
def testInsecureLoginRaisesException(self):
|
||||
p, t = setUp(greet=False)
|
||||
p.dataReceived(b"+OK Howdy\r\n")
|
||||
d = p.login(b"username", b"password")
|
||||
self.assertFalse(t.value())
|
||||
return self.assertFailure(d, InsecureAuthenticationDisallowed)
|
||||
|
||||
def testSSLTransportConsideredSecure(self):
|
||||
"""
|
||||
If a server doesn't offer APOP but the transport is secured using
|
||||
SSL or TLS, a plaintext login should be allowed, not rejected with
|
||||
an InsecureAuthenticationDisallowed exception.
|
||||
"""
|
||||
p, t = setUp(greet=False)
|
||||
directlyProvides(t, interfaces.ISSLTransport)
|
||||
p.dataReceived(b"+OK Howdy\r\n")
|
||||
d = p.login(b"username", b"password")
|
||||
self.assertEqual(t.value(), b"USER username\r\n")
|
||||
t.clear()
|
||||
p.dataReceived(b"+OK\r\n")
|
||||
self.assertEqual(t.value(), b"PASS password\r\n")
|
||||
p.dataReceived(b"+OK\r\n")
|
||||
return d
|
||||
|
||||
|
||||
class ListConsumer:
|
||||
def __init__(self):
|
||||
self.data = {}
|
||||
|
||||
def consume(self, result):
|
||||
(item, value) = result
|
||||
self.data.setdefault(item, []).append(value)
|
||||
|
||||
|
||||
class MessageConsumer:
|
||||
def __init__(self):
|
||||
self.data = []
|
||||
|
||||
def consume(self, line):
|
||||
self.data.append(line)
|
||||
|
||||
|
||||
class POP3ClientListTests(TestCase):
|
||||
def testListSize(self):
|
||||
p, t = setUp()
|
||||
d = p.listSize()
|
||||
self.assertEqual(t.value(), b"LIST\r\n")
|
||||
p.dataReceived(b"+OK Here it comes\r\n")
|
||||
p.dataReceived(b"1 3\r\n2 2\r\n3 1\r\n.\r\n")
|
||||
return d.addCallback(self.assertEqual, [3, 2, 1])
|
||||
|
||||
def testListSizeWithConsumer(self):
|
||||
p, t = setUp()
|
||||
c = ListConsumer()
|
||||
f = c.consume
|
||||
d = p.listSize(f)
|
||||
self.assertEqual(t.value(), b"LIST\r\n")
|
||||
p.dataReceived(b"+OK Here it comes\r\n")
|
||||
p.dataReceived(b"1 3\r\n2 2\r\n3 1\r\n")
|
||||
self.assertEqual(c.data, {0: [3], 1: [2], 2: [1]})
|
||||
p.dataReceived(b"5 3\r\n6 2\r\n7 1\r\n")
|
||||
self.assertEqual(c.data, {0: [3], 1: [2], 2: [1], 4: [3], 5: [2], 6: [1]})
|
||||
p.dataReceived(b".\r\n")
|
||||
return d.addCallback(self.assertIdentical, f)
|
||||
|
||||
def testFailedListSize(self):
|
||||
p, t = setUp()
|
||||
d = p.listSize()
|
||||
self.assertEqual(t.value(), b"LIST\r\n")
|
||||
p.dataReceived(b"-ERR Fatal doom server exploded\r\n")
|
||||
return self.assertFailure(d, ServerErrorResponse).addCallback(
|
||||
lambda exc: self.assertEqual(exc.args[0], b"Fatal doom server exploded")
|
||||
)
|
||||
|
||||
def testListUID(self):
|
||||
p, t = setUp()
|
||||
d = p.listUID()
|
||||
self.assertEqual(t.value(), b"UIDL\r\n")
|
||||
p.dataReceived(b"+OK Here it comes\r\n")
|
||||
p.dataReceived(b"1 abc\r\n2 def\r\n3 ghi\r\n.\r\n")
|
||||
return d.addCallback(self.assertEqual, [b"abc", b"def", b"ghi"])
|
||||
|
||||
def testListUIDWithConsumer(self):
|
||||
p, t = setUp()
|
||||
c = ListConsumer()
|
||||
f = c.consume
|
||||
d = p.listUID(f)
|
||||
self.assertEqual(t.value(), b"UIDL\r\n")
|
||||
p.dataReceived(b"+OK Here it comes\r\n")
|
||||
p.dataReceived(b"1 xyz\r\n2 abc\r\n5 mno\r\n")
|
||||
self.assertEqual(c.data, {0: [b"xyz"], 1: [b"abc"], 4: [b"mno"]})
|
||||
p.dataReceived(b".\r\n")
|
||||
return d.addCallback(self.assertIdentical, f)
|
||||
|
||||
def testFailedListUID(self):
|
||||
p, t = setUp()
|
||||
d = p.listUID()
|
||||
self.assertEqual(t.value(), b"UIDL\r\n")
|
||||
p.dataReceived(b"-ERR Fatal doom server exploded\r\n")
|
||||
return self.assertFailure(d, ServerErrorResponse).addCallback(
|
||||
lambda exc: self.assertEqual(exc.args[0], b"Fatal doom server exploded")
|
||||
)
|
||||
|
||||
|
||||
class POP3ClientMessageTests(TestCase):
|
||||
def testRetrieve(self):
|
||||
p, t = setUp()
|
||||
d = p.retrieve(7)
|
||||
self.assertEqual(t.value(), b"RETR 8\r\n")
|
||||
p.dataReceived(b"+OK Message incoming\r\n")
|
||||
p.dataReceived(b"La la la here is message text\r\n")
|
||||
p.dataReceived(b"..Further message text tra la la\r\n")
|
||||
p.dataReceived(b".\r\n")
|
||||
return d.addCallback(
|
||||
self.assertEqual,
|
||||
[b"La la la here is message text", b".Further message text tra la la"],
|
||||
)
|
||||
|
||||
def testRetrieveWithConsumer(self):
|
||||
p, t = setUp()
|
||||
c = MessageConsumer()
|
||||
f = c.consume
|
||||
d = p.retrieve(7, f)
|
||||
self.assertEqual(t.value(), b"RETR 8\r\n")
|
||||
p.dataReceived(b"+OK Message incoming\r\n")
|
||||
p.dataReceived(b"La la la here is message text\r\n")
|
||||
p.dataReceived(b"..Further message text\r\n.\r\n")
|
||||
return d.addCallback(self._cbTestRetrieveWithConsumer, f, c)
|
||||
|
||||
def _cbTestRetrieveWithConsumer(self, result, f, c):
|
||||
self.assertIdentical(result, f)
|
||||
self.assertEqual(
|
||||
c.data, [b"La la la here is message text", b".Further message text"]
|
||||
)
|
||||
|
||||
def testPartialRetrieve(self):
|
||||
p, t = setUp()
|
||||
d = p.retrieve(7, lines=2)
|
||||
self.assertEqual(t.value(), b"TOP 8 2\r\n")
|
||||
p.dataReceived(b"+OK 2 lines on the way\r\n")
|
||||
p.dataReceived(b"Line the first! Woop\r\n")
|
||||
p.dataReceived(b"Line the last! Bye\r\n")
|
||||
p.dataReceived(b".\r\n")
|
||||
return d.addCallback(
|
||||
self.assertEqual, [b"Line the first! Woop", b"Line the last! Bye"]
|
||||
)
|
||||
|
||||
def testPartialRetrieveWithConsumer(self):
|
||||
p, t = setUp()
|
||||
c = MessageConsumer()
|
||||
f = c.consume
|
||||
d = p.retrieve(7, f, lines=2)
|
||||
self.assertEqual(t.value(), b"TOP 8 2\r\n")
|
||||
p.dataReceived(b"+OK 2 lines on the way\r\n")
|
||||
p.dataReceived(b"Line the first! Woop\r\n")
|
||||
p.dataReceived(b"Line the last! Bye\r\n")
|
||||
p.dataReceived(b".\r\n")
|
||||
return d.addCallback(self._cbTestPartialRetrieveWithConsumer, f, c)
|
||||
|
||||
def _cbTestPartialRetrieveWithConsumer(self, result, f, c):
|
||||
self.assertIdentical(result, f)
|
||||
self.assertEqual(c.data, [b"Line the first! Woop", b"Line the last! Bye"])
|
||||
|
||||
def testFailedRetrieve(self):
|
||||
p, t = setUp()
|
||||
d = p.retrieve(0)
|
||||
self.assertEqual(t.value(), b"RETR 1\r\n")
|
||||
p.dataReceived(b"-ERR Fatal doom server exploded\r\n")
|
||||
return self.assertFailure(d, ServerErrorResponse).addCallback(
|
||||
lambda exc: self.assertEqual(exc.args[0], b"Fatal doom server exploded")
|
||||
)
|
||||
|
||||
def test_concurrentRetrieves(self):
|
||||
"""
|
||||
Issue three retrieve calls immediately without waiting for any to
|
||||
succeed and make sure they all do succeed eventually.
|
||||
"""
|
||||
p, t = setUp()
|
||||
messages = [
|
||||
p.retrieve(i).addCallback(
|
||||
self.assertEqual,
|
||||
[b"First line of %d." % (i + 1,), b"Second line of %d." % (i + 1,)],
|
||||
)
|
||||
for i in range(3)
|
||||
]
|
||||
|
||||
for i in range(1, 4):
|
||||
self.assertEqual(t.value(), b"RETR %d\r\n" % (i,))
|
||||
t.clear()
|
||||
p.dataReceived(b"+OK 2 lines on the way\r\n")
|
||||
p.dataReceived(b"First line of %d.\r\n" % (i,))
|
||||
p.dataReceived(b"Second line of %d.\r\n" % (i,))
|
||||
self.assertEqual(t.value(), b"")
|
||||
p.dataReceived(b".\r\n")
|
||||
|
||||
return defer.DeferredList(messages, fireOnOneErrback=True)
|
||||
|
||||
|
||||
class POP3ClientMiscTests(TestCase):
|
||||
def testCapability(self):
|
||||
p, t = setUp()
|
||||
d = p.capabilities(useCache=0)
|
||||
self.assertEqual(t.value(), b"CAPA\r\n")
|
||||
p.dataReceived(b"+OK Capabilities on the way\r\n")
|
||||
p.dataReceived(b"X\r\nY\r\nZ\r\nA 1 2 3\r\nB 1 2\r\nC 1\r\n.\r\n")
|
||||
return d.addCallback(
|
||||
self.assertEqual,
|
||||
{
|
||||
b"X": None,
|
||||
b"Y": None,
|
||||
b"Z": None,
|
||||
b"A": [b"1", b"2", b"3"],
|
||||
b"B": [b"1", b"2"],
|
||||
b"C": [b"1"],
|
||||
},
|
||||
)
|
||||
|
||||
def testCapabilityError(self):
|
||||
p, t = setUp()
|
||||
d = p.capabilities(useCache=0)
|
||||
self.assertEqual(t.value(), b"CAPA\r\n")
|
||||
p.dataReceived(b"-ERR This server is lame!\r\n")
|
||||
return d.addCallback(self.assertEqual, {})
|
||||
|
||||
def testStat(self):
|
||||
p, t = setUp()
|
||||
d = p.stat()
|
||||
self.assertEqual(t.value(), b"STAT\r\n")
|
||||
p.dataReceived(b"+OK 1 1212\r\n")
|
||||
return d.addCallback(self.assertEqual, (1, 1212))
|
||||
|
||||
def testStatError(self):
|
||||
p, t = setUp()
|
||||
d = p.stat()
|
||||
self.assertEqual(t.value(), b"STAT\r\n")
|
||||
p.dataReceived(b"-ERR This server is lame!\r\n")
|
||||
return self.assertFailure(d, ServerErrorResponse).addCallback(
|
||||
lambda exc: self.assertEqual(exc.args[0], b"This server is lame!")
|
||||
)
|
||||
|
||||
def testNoop(self):
|
||||
p, t = setUp()
|
||||
d = p.noop()
|
||||
self.assertEqual(t.value(), b"NOOP\r\n")
|
||||
p.dataReceived(b"+OK No-op to you too!\r\n")
|
||||
return d.addCallback(self.assertEqual, b"No-op to you too!")
|
||||
|
||||
def testNoopError(self):
|
||||
p, t = setUp()
|
||||
d = p.noop()
|
||||
self.assertEqual(t.value(), b"NOOP\r\n")
|
||||
p.dataReceived(b"-ERR This server is lame!\r\n")
|
||||
return self.assertFailure(d, ServerErrorResponse).addCallback(
|
||||
lambda exc: self.assertEqual(exc.args[0], b"This server is lame!")
|
||||
)
|
||||
|
||||
def testRset(self):
|
||||
p, t = setUp()
|
||||
d = p.reset()
|
||||
self.assertEqual(t.value(), b"RSET\r\n")
|
||||
p.dataReceived(b"+OK Reset state\r\n")
|
||||
return d.addCallback(self.assertEqual, b"Reset state")
|
||||
|
||||
def testRsetError(self):
|
||||
p, t = setUp()
|
||||
d = p.reset()
|
||||
self.assertEqual(t.value(), b"RSET\r\n")
|
||||
p.dataReceived(b"-ERR This server is lame!\r\n")
|
||||
return self.assertFailure(d, ServerErrorResponse).addCallback(
|
||||
lambda exc: self.assertEqual(exc.args[0], b"This server is lame!")
|
||||
)
|
||||
|
||||
def testDelete(self):
|
||||
p, t = setUp()
|
||||
d = p.delete(3)
|
||||
self.assertEqual(t.value(), b"DELE 4\r\n")
|
||||
p.dataReceived(b"+OK Hasta la vista\r\n")
|
||||
return d.addCallback(self.assertEqual, b"Hasta la vista")
|
||||
|
||||
def testDeleteError(self):
|
||||
p, t = setUp()
|
||||
d = p.delete(3)
|
||||
self.assertEqual(t.value(), b"DELE 4\r\n")
|
||||
p.dataReceived(b"-ERR Winner is not you.\r\n")
|
||||
return self.assertFailure(d, ServerErrorResponse).addCallback(
|
||||
lambda exc: self.assertEqual(exc.args[0], b"Winner is not you.")
|
||||
)
|
||||
|
||||
|
||||
class SimpleClient(POP3Client):
|
||||
def __init__(self, deferred, contextFactory=None):
|
||||
self.deferred = deferred
|
||||
self.allowInsecureLogin = True
|
||||
|
||||
def serverGreeting(self, challenge):
|
||||
self.deferred.callback(None)
|
||||
|
||||
|
||||
class POP3HelperMixin:
|
||||
serverCTX = None
|
||||
clientCTX = None
|
||||
|
||||
def setUp(self):
|
||||
d = defer.Deferred()
|
||||
self.server = pop3testserver.POP3TestServer(contextFactory=self.serverCTX)
|
||||
self.client = SimpleClient(d, contextFactory=self.clientCTX)
|
||||
self.client.timeout = 30
|
||||
self.connected = d
|
||||
|
||||
def tearDown(self):
|
||||
del self.server
|
||||
del self.client
|
||||
del self.connected
|
||||
|
||||
def _cbStopClient(self, ignore):
|
||||
self.client.transport.loseConnection()
|
||||
|
||||
def _ebGeneral(self, failure):
|
||||
self.client.transport.loseConnection()
|
||||
self.server.transport.loseConnection()
|
||||
return failure
|
||||
|
||||
def loopback(self):
|
||||
return loopback.loopbackTCP(self.server, self.client, noisy=False)
|
||||
|
||||
|
||||
class TLSServerFactory(protocol.ServerFactory):
|
||||
class protocol(basic.LineReceiver):
|
||||
context = None
|
||||
output: List[bytes] = []
|
||||
|
||||
def connectionMade(self):
|
||||
self.factory.input = []
|
||||
self.output = self.output[:]
|
||||
for line in self.output.pop(0):
|
||||
self.sendLine(line)
|
||||
|
||||
def lineReceived(self, line):
|
||||
self.factory.input.append(line)
|
||||
[self.sendLine(l) for l in self.output.pop(0)]
|
||||
if line == b"STLS":
|
||||
self.transport.startTLS(self.context)
|
||||
|
||||
|
||||
@skipIf(not ClientTLSContext, "OpenSSL not present")
|
||||
@skipIf(not interfaces.IReactorSSL(reactor, None), "OpenSSL not present")
|
||||
class POP3TLSTests(TestCase):
|
||||
"""
|
||||
Tests for POP3Client's support for TLS connections.
|
||||
"""
|
||||
|
||||
def test_startTLS(self):
|
||||
"""
|
||||
POP3Client.startTLS starts a TLS session over its existing TCP
|
||||
connection.
|
||||
"""
|
||||
sf = TLSServerFactory()
|
||||
sf.protocol.output = [
|
||||
[b"+OK"], # Server greeting
|
||||
[b"+OK", b"STLS", b"."], # CAPA response
|
||||
[b"+OK"], # STLS response
|
||||
[b"+OK", b"."], # Second CAPA response
|
||||
[b"+OK"], # QUIT response
|
||||
]
|
||||
sf.protocol.context = ServerTLSContext()
|
||||
port = reactor.listenTCP(0, sf, interface="127.0.0.1")
|
||||
self.addCleanup(port.stopListening)
|
||||
H = port.getHost().host
|
||||
P = port.getHost().port
|
||||
|
||||
connLostDeferred = defer.Deferred()
|
||||
cp = SimpleClient(defer.Deferred(), ClientTLSContext())
|
||||
|
||||
def connectionLost(reason):
|
||||
SimpleClient.connectionLost(cp, reason)
|
||||
connLostDeferred.callback(None)
|
||||
|
||||
cp.connectionLost = connectionLost
|
||||
cf = protocol.ClientFactory()
|
||||
cf.protocol = lambda: cp
|
||||
|
||||
conn = reactor.connectTCP(H, P, cf)
|
||||
|
||||
def cbConnected(ignored):
|
||||
log.msg("Connected to server; starting TLS")
|
||||
return cp.startTLS()
|
||||
|
||||
def cbStartedTLS(ignored):
|
||||
log.msg("Started TLS; disconnecting")
|
||||
return cp.quit()
|
||||
|
||||
def cbDisconnected(ign):
|
||||
log.msg("Disconnected; asserting correct input received")
|
||||
self.assertEqual(sf.input, [b"CAPA", b"STLS", b"CAPA", b"QUIT"])
|
||||
|
||||
def cleanup(result):
|
||||
log.msg(
|
||||
"Asserted correct input; disconnecting "
|
||||
"client and shutting down server"
|
||||
)
|
||||
conn.disconnect()
|
||||
return connLostDeferred
|
||||
|
||||
cp.deferred.addCallback(cbConnected)
|
||||
cp.deferred.addCallback(cbStartedTLS)
|
||||
cp.deferred.addCallback(cbDisconnected)
|
||||
cp.deferred.addBoth(cleanup)
|
||||
|
||||
return cp.deferred
|
||||
|
||||
|
||||
class POP3TimeoutTests(POP3HelperMixin, TestCase):
|
||||
def testTimeout(self):
|
||||
def login():
|
||||
d = self.client.login("test", "twisted")
|
||||
d.addCallback(loggedIn)
|
||||
d.addErrback(timedOut)
|
||||
return d
|
||||
|
||||
def loggedIn(result):
|
||||
self.fail("Successfully logged in!? Impossible!")
|
||||
|
||||
def timedOut(failure):
|
||||
failure.trap(error.TimeoutError)
|
||||
self._cbStopClient(None)
|
||||
|
||||
def quit():
|
||||
return self.client.quit()
|
||||
|
||||
self.client.timeout = 0.01
|
||||
|
||||
# Tell the server to not return a response to client. This
|
||||
# will trigger a timeout.
|
||||
pop3testserver.TIMEOUT_RESPONSE = True
|
||||
|
||||
methods = [login, quit]
|
||||
map(self.connected.addCallback, map(strip, methods))
|
||||
self.connected.addCallback(self._cbStopClient)
|
||||
self.connected.addErrback(self._ebGeneral)
|
||||
return self.loopback()
|
||||
|
||||
|
||||
class POP3ClientModuleStructureTests(TestCase):
|
||||
"""
|
||||
Miscellaneous tests more to do with module/package structure than
|
||||
anything to do with the POP3 client.
|
||||
"""
|
||||
|
||||
def test_all(self):
|
||||
"""
|
||||
twisted.mail._pop3client.__all__ should be empty because all classes
|
||||
should be imported through twisted.mail.pop3.
|
||||
"""
|
||||
self.assertEqual(twisted.mail._pop3client.__all__, [])
|
||||
|
||||
def test_import(self):
|
||||
"""
|
||||
Every public class in twisted.mail._pop3client should be available as
|
||||
a member of twisted.mail.pop3 with the exception of
|
||||
twisted.mail._pop3client.POP3Client which should be available as
|
||||
twisted.mail.pop3.AdvancedClient.
|
||||
"""
|
||||
publicClasses = [
|
||||
c[0]
|
||||
for c in inspect.getmembers(
|
||||
sys.modules["twisted.mail._pop3client"], inspect.isclass
|
||||
)
|
||||
if not c[0][0] == "_"
|
||||
]
|
||||
|
||||
for pc in publicClasses:
|
||||
if not pc == "POP3Client":
|
||||
self.assertTrue(
|
||||
hasattr(twisted.mail.pop3, pc),
|
||||
f"{pc} not in {twisted.mail.pop3}",
|
||||
)
|
||||
else:
|
||||
self.assertTrue(hasattr(twisted.mail.pop3, "AdvancedPOP3Client"))
|
||||
@@ -0,0 +1,18 @@
|
||||
# Copyright (c) Twisted Matrix Laboratories.
|
||||
# See LICENSE for details.
|
||||
|
||||
"""
|
||||
Tests for the command-line mailer tool provided by Twisted Mail.
|
||||
"""
|
||||
|
||||
from twisted.scripts.test.test_scripts import ScriptTestsMixin
|
||||
from twisted.trial.unittest import TestCase
|
||||
|
||||
|
||||
class ScriptTests(TestCase, ScriptTestsMixin):
|
||||
"""
|
||||
Tests for all one of mail's scripts.
|
||||
"""
|
||||
|
||||
def test_mailmail(self) -> None:
|
||||
self.scriptTest("mail/mailmail")
|
||||
1908
backend/lib/python3.12/site-packages/twisted/mail/test/test_smtp.py
Normal file
1908
backend/lib/python3.12/site-packages/twisted/mail/test/test_smtp.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user