aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/Twisted/py3/twisted/mail
diff options
context:
space:
mode:
authorshmel1k <shmel1k@ydb.tech>2023-11-26 18:16:14 +0300
committershmel1k <shmel1k@ydb.tech>2023-11-26 18:43:30 +0300
commitb8cf9e88f4c5c64d9406af533d8948deb050d695 (patch)
tree218eb61fb3c3b96ec08b4d8cdfef383104a87d63 /contrib/python/Twisted/py3/twisted/mail
parent523f645a83a0ec97a0332dbc3863bb354c92a328 (diff)
downloadydb-b8cf9e88f4c5c64d9406af533d8948deb050d695.tar.gz
add kikimr_configure
Diffstat (limited to 'contrib/python/Twisted/py3/twisted/mail')
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/__init__.py6
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/_cred.py105
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/_except.py350
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/_pop3client.py1235
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/alias.py765
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/bounce.py107
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/imap4.py6233
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/interfaces.py1050
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/mail.py706
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/maildir.py910
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/newsfragments/.gitignore1
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/pb.py117
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/pop3.py1704
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/pop3client.py22
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/protocols.py385
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/relay.py164
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/relaymanager.py1135
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/scripts/__init__.py1
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/scripts/mailmail.py386
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/smtp.py2270
-rw-r--r--contrib/python/Twisted/py3/twisted/mail/tap.py384
21 files changed, 18036 insertions, 0 deletions
diff --git a/contrib/python/Twisted/py3/twisted/mail/__init__.py b/contrib/python/Twisted/py3/twisted/mail/__init__.py
new file mode 100644
index 0000000000..0f1604e8a5
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/__init__.py
@@ -0,0 +1,6 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Twisted Mail: Servers and clients for POP3, ESMTP, and IMAP.
+"""
diff --git a/contrib/python/Twisted/py3/twisted/mail/_cred.py b/contrib/python/Twisted/py3/twisted/mail/_cred.py
new file mode 100644
index 0000000000..0a9442627d
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/_cred.py
@@ -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",
+]
diff --git a/contrib/python/Twisted/py3/twisted/mail/_except.py b/contrib/python/Twisted/py3/twisted/mail/_except.py
new file mode 100644
index 0000000000..ce01aedeb5
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/_except.py
@@ -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}).
+ """
diff --git a/contrib/python/Twisted/py3/twisted/mail/_pop3client.py b/contrib/python/Twisted/py3/twisted/mail/_pop3client.py
new file mode 100644
index 0000000000..08efe1ec54
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/_pop3client.py
@@ -0,0 +1,1235 @@
+# -*- test-case-name: twisted.mail.test.test_pop3client -*-
+# Copyright (c) 2001-2004 Divmod Inc.
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+A POP3 client protocol implementation.
+
+Don't use this module directly. Use twisted.mail.pop3 instead.
+
+@author: Jp Calderone
+"""
+
+import re
+from hashlib import md5
+from typing import List
+
+from twisted.internet import defer, error, interfaces
+from twisted.mail._except import (
+ InsecureAuthenticationDisallowed,
+ LineTooLong,
+ ServerErrorResponse,
+ TLSError,
+ TLSNotSupportedError,
+)
+from twisted.protocols import basic, policies
+from twisted.python import log
+
+OK = b"+OK"
+ERR = b"-ERR"
+
+
+class _ListSetter:
+ """
+ A utility class to construct a list from a multi-line response accounting
+ for deleted messages.
+
+ POP3 responses sometimes occur in the form of a list of lines containing
+ two pieces of data, a message index and a value of some sort. When a
+ message is deleted, it is omitted from these responses. The L{setitem}
+ method of this class is meant to be called with these two values. In the
+ cases where indices are skipped, it takes care of padding out the missing
+ values with L{None}.
+
+ @ivar L: See L{__init__}
+ """
+
+ def __init__(self, L):
+ """
+ @type L: L{list} of L{object}
+ @param L: The list being constructed. An empty list should be
+ passed in.
+ """
+ self.L = L
+
+ def setitem(self, itemAndValue):
+ """
+ Add the value at the specified position, padding out missing entries.
+
+ @type itemAndValue: C{tuple}
+ @param itemAndValue: A tuple of (item, value). The I{item} is the 0-based
+ index in the list at which the value should be placed. The value is
+ is an L{object} to put in the list.
+ """
+ (item, value) = itemAndValue
+ diff = item - len(self.L) + 1
+ if diff > 0:
+ self.L.extend([None] * diff)
+ self.L[item] = value
+
+
+def _statXform(line):
+ """
+ Parse the response to a STAT command.
+
+ @type line: L{bytes}
+ @param line: The response from the server to a STAT command minus the
+ status indicator.
+
+ @rtype: 2-L{tuple} of (0) L{int}, (1) L{int}
+ @return: The number of messages in the mailbox and the size of the mailbox.
+ """
+ numMsgs, totalSize = line.split(None, 1)
+ return int(numMsgs), int(totalSize)
+
+
+def _listXform(line):
+ """
+ Parse a line of the response to a LIST command.
+
+ The line from the LIST response consists of a 1-based message number
+ followed by a size.
+
+ @type line: L{bytes}
+ @param line: A non-initial line from the multi-line response to a LIST
+ command.
+
+ @rtype: 2-L{tuple} of (0) L{int}, (1) L{int}
+ @return: The 0-based index of the message and the size of the message.
+ """
+ index, size = line.split(None, 1)
+ return int(index) - 1, int(size)
+
+
+def _uidXform(line):
+ """
+ Parse a line of the response to a UIDL command.
+
+ The line from the UIDL response consists of a 1-based message number
+ followed by a unique id.
+
+ @type line: L{bytes}
+ @param line: A non-initial line from the multi-line response to a UIDL
+ command.
+
+ @rtype: 2-L{tuple} of (0) L{int}, (1) L{bytes}
+ @return: The 0-based index of the message and the unique identifier
+ for the message.
+ """
+ index, uid = line.split(None, 1)
+ return int(index) - 1, uid
+
+
+def _codeStatusSplit(line):
+ """
+ Parse the first line of a multi-line server response.
+
+ @type line: L{bytes}
+ @param line: The first line of a multi-line server response.
+
+ @rtype: 2-tuple of (0) L{bytes}, (1) L{bytes}
+ @return: The status indicator and the rest of the server response.
+ """
+ parts = line.split(b" ", 1)
+ if len(parts) == 1:
+ return parts[0], b""
+ return parts
+
+
+def _dotUnquoter(line):
+ """
+ Remove a byte-stuffed termination character at the beginning of a line if
+ present.
+
+ When the termination character (C{'.'}) appears at the beginning of a line,
+ the server byte-stuffs it by adding another termination character to
+ avoid confusion with the terminating sequence (C{'.\\r\\n'}).
+
+ @type line: L{bytes}
+ @param line: A received line.
+
+ @rtype: L{bytes}
+ @return: The line without the byte-stuffed termination character at the
+ beginning if it was present. Otherwise, the line unchanged.
+ """
+ if line.startswith(b".."):
+ return line[1:]
+ return line
+
+
+class POP3Client(basic.LineOnlyReceiver, policies.TimeoutMixin):
+ """
+ A POP3 client protocol.
+
+ Instances of this class provide a convenient, efficient API for
+ retrieving and deleting messages from a POP3 server.
+
+ This API provides a pipelining interface but POP3 pipelining
+ on the network is not yet supported.
+
+ @type startedTLS: L{bool}
+ @ivar startedTLS: An indication of whether TLS has been negotiated
+ successfully.
+
+ @type allowInsecureLogin: L{bool}
+ @ivar allowInsecureLogin: An indication of whether plaintext login should
+ be allowed when the server offers no authentication challenge and the
+ transport does not offer any protection via encryption.
+
+ @type serverChallenge: L{bytes} or L{None}
+ @ivar serverChallenge: The challenge received in the server greeting.
+
+ @type timeout: L{int}
+ @ivar timeout: The number of seconds to wait on a response from the server
+ before timing out a connection. If the number is <= 0, no timeout
+ checking will be performed.
+
+ @type _capCache: L{None} or L{dict} mapping L{bytes}
+ to L{list} of L{bytes} and/or L{bytes} to L{None}
+ @ivar _capCache: The cached server capabilities. Capabilities are not
+ allowed to change during the session (except when TLS is negotiated),
+ so the first response to a capabilities command can be used for
+ later lookups.
+
+ @type _challengeMagicRe: L{Pattern <re.Pattern.search>}
+ @ivar _challengeMagicRe: A regular expression which matches the
+ challenge in the server greeting.
+
+ @type _blockedQueue: L{None} or L{list} of 3-L{tuple}
+ of (0) L{Deferred <defer.Deferred>}, (1) callable which results
+ in a L{Deferred <defer.Deferred>}, (2) L{tuple}
+ @ivar _blockedQueue: A list of blocked commands. While a command is
+ awaiting a response from the server, other commands are blocked. When
+ no command is outstanding, C{_blockedQueue} is set to L{None}.
+ Otherwise, it contains a list of information about blocked commands.
+ Each list entry provides the following information about a blocked
+ command: the deferred that should be called when the response to the
+ command is received, the function that sends the command, and the
+ arguments to the function.
+
+ @type _waiting: L{Deferred <defer.Deferred>} or
+ L{None}
+ @ivar _waiting: A deferred which fires when the response to the
+ outstanding command is received from the server.
+
+ @type _timedOut: L{bool}
+ @ivar _timedOut: An indication of whether the connection was dropped
+ because of a timeout.
+
+ @type _greetingError: L{bytes} or L{None}
+ @ivar _greetingError: The server greeting minus the status indicator, when
+ the connection was dropped because of an error in the server greeting.
+ Otherwise, L{None}.
+
+ @type state: L{bytes}
+ @ivar state: The state which indicates what type of response is expected
+ from the server. Valid states are: 'WELCOME', 'WAITING', 'SHORT',
+ 'LONG_INITIAL', 'LONG'.
+
+ @type _xform: L{None} or callable that takes L{bytes}
+ and returns L{object}
+ @ivar _xform: The transform function which is used to convert each
+ line of a multi-line response into usable values for use by the
+ consumer function. If L{None}, each line of the multi-line response
+ is sent directly to the consumer function.
+
+ @type _consumer: callable that takes L{object}
+ @ivar _consumer: The consumer function which is used to store the
+ values derived by the transform function from each line of a
+ multi-line response into a list.
+ """
+
+ startedTLS = False
+ allowInsecureLogin = False
+ timeout = 0
+ serverChallenge = None
+
+ _capCache = None
+ _challengeMagicRe = re.compile(b"(<[^>]+>)")
+ _blockedQueue = None
+ _waiting = None
+ _timedOut = False
+ _greetingError = None
+
+ def _blocked(self, f, *a):
+ """
+ Block a command, if necessary.
+
+ If commands are being blocked, append information about the function
+ which sends the command to a list and return a deferred that will be
+ chained with the return value of the function when it eventually runs.
+ Otherwise, set up for subsequent commands to be blocked and return
+ L{None}.
+
+ @type f: callable
+ @param f: A function which sends a command.
+
+ @type a: L{tuple}
+ @param a: Arguments to the function.
+
+ @rtype: L{None} or L{Deferred <defer.Deferred>}
+ @return: L{None} if the command can run immediately. Otherwise,
+ a deferred that will eventually trigger with the return value of
+ the function.
+ """
+ if self._blockedQueue is not None:
+ d = defer.Deferred()
+ self._blockedQueue.append((d, f, a))
+ return d
+ self._blockedQueue = []
+ return None
+
+ def _unblock(self):
+ """
+ Send the next blocked command.
+
+ If there are no more commands in the blocked queue, set up for the next
+ command to be sent immediately.
+ """
+ if self._blockedQueue == []:
+ self._blockedQueue = None
+ elif self._blockedQueue is not None:
+ _blockedQueue = self._blockedQueue
+ self._blockedQueue = None
+
+ d, f, a = _blockedQueue.pop(0)
+ d2 = f(*a)
+ d2.chainDeferred(d)
+ # f is a function which uses _blocked (otherwise it wouldn't
+ # have gotten into the blocked queue), which means it will have
+ # re-set _blockedQueue to an empty list, so we can put the rest
+ # of the blocked queue back into it now.
+ self._blockedQueue.extend(_blockedQueue)
+
+ def sendShort(self, cmd, args):
+ """
+ Send a POP3 command to which a short response is expected.
+
+ Block all further commands from being sent until the response is
+ received. Transition the state to SHORT.
+
+ @type cmd: L{bytes}
+ @param cmd: A POP3 command.
+
+ @type args: L{bytes}
+ @param args: The command arguments.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully fires with
+ L{bytes} or fails with L{ServerErrorResponse}
+ @return: A deferred which fires when the entire response is received.
+ On an OK response, it returns the response from the server minus
+ the status indicator. On an ERR response, it issues a server
+ error response failure with the response from the server minus the
+ status indicator.
+ """
+ d = self._blocked(self.sendShort, cmd, args)
+ if d is not None:
+ return d
+
+ if args:
+ self.sendLine(cmd + b" " + args)
+ else:
+ self.sendLine(cmd)
+ self.state = "SHORT"
+ self._waiting = defer.Deferred()
+ return self._waiting
+
+ def sendLong(self, cmd, args, consumer, xform):
+ """
+ Send a POP3 command to which a multi-line response is expected.
+
+ Block all further commands from being sent until the entire response is
+ received. Transition the state to LONG_INITIAL.
+
+ @type cmd: L{bytes}
+ @param cmd: A POP3 command.
+
+ @type args: L{bytes}
+ @param args: The command arguments.
+
+ @type consumer: callable that takes L{object}
+ @param consumer: A consumer function which should be used to put
+ the values derived by a transform function from each line of the
+ multi-line response into a list.
+
+ @type xform: L{None} or callable that takes
+ L{bytes} and returns L{object}
+ @param xform: A transform function which should be used to transform
+ each line of the multi-line response into usable values for use by
+ a consumer function. If L{None}, each line of the multi-line
+ response should be sent directly to the consumer function.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully fires with
+ callable that takes L{object} and fails with L{ServerErrorResponse}
+ @return: A deferred which fires when the entire response is received.
+ On an OK response, it returns the consumer function. On an ERR
+ response, it issues a server error response failure with the
+ response from the server minus the status indicator and the
+ consumer function.
+ """
+ d = self._blocked(self.sendLong, cmd, args, consumer, xform)
+ if d is not None:
+ return d
+
+ if args:
+ self.sendLine(cmd + b" " + args)
+ else:
+ self.sendLine(cmd)
+ self.state = "LONG_INITIAL"
+ self._xform = xform
+ self._consumer = consumer
+ self._waiting = defer.Deferred()
+ return self._waiting
+
+ # Twisted protocol callback
+ def connectionMade(self):
+ """
+ Wait for a greeting from the server after the connection has been made.
+
+ Start the connection in the WELCOME state.
+ """
+ if self.timeout > 0:
+ self.setTimeout(self.timeout)
+
+ self.state = "WELCOME"
+ self._blockedQueue = []
+
+ def timeoutConnection(self):
+ """
+ Drop the connection when the server does not respond in time.
+ """
+ self._timedOut = True
+ self.transport.loseConnection()
+
+ def connectionLost(self, reason):
+ """
+ Clean up when the connection has been lost.
+
+ When the loss of connection was initiated by the client due to a
+ timeout, the L{_timedOut} flag will be set. When it was initiated by
+ the client due to an error in the server greeting, L{_greetingError}
+ will be set to the server response minus the status indicator.
+
+ @type reason: L{Failure <twisted.python.failure.Failure>}
+ @param reason: The reason the connection was terminated.
+ """
+ if self.timeout > 0:
+ self.setTimeout(None)
+
+ if self._timedOut:
+ reason = error.TimeoutError()
+ elif self._greetingError:
+ reason = ServerErrorResponse(self._greetingError)
+
+ d = []
+ if self._waiting is not None:
+ d.append(self._waiting)
+ self._waiting = None
+ if self._blockedQueue is not None:
+ d.extend([deferred for (deferred, f, a) in self._blockedQueue])
+ self._blockedQueue = None
+ for w in d:
+ w.errback(reason)
+
+ def lineReceived(self, line):
+ """
+ Pass a received line to a state machine function and
+ transition to the next state.
+
+ @type line: L{bytes}
+ @param line: A received line.
+ """
+ if self.timeout > 0:
+ self.resetTimeout()
+
+ state = self.state
+ self.state = None
+ state = getattr(self, "state_" + state)(line) or state
+ if self.state is None:
+ self.state = state
+
+ def lineLengthExceeded(self, buffer):
+ """
+ Drop the connection when a server response exceeds the maximum line
+ length (L{LineOnlyReceiver.MAX_LENGTH}).
+
+ @type buffer: L{bytes}
+ @param buffer: A received line which exceeds the maximum line length.
+ """
+ # XXX - We need to be smarter about this
+ if self._waiting is not None:
+ waiting, self._waiting = self._waiting, None
+ waiting.errback(LineTooLong())
+ self.transport.loseConnection()
+
+ # POP3 Client state logic - don't touch this.
+ def state_WELCOME(self, line):
+ """
+ Handle server responses for the WELCOME state in which the server
+ greeting is expected.
+
+ WELCOME is the first state. The server should send one line of text
+ with a greeting and possibly an APOP challenge. Transition the state
+ to WAITING.
+
+ @type line: L{bytes}
+ @param line: A line received from the server.
+
+ @rtype: L{bytes}
+ @return: The next state.
+ """
+ code, status = _codeStatusSplit(line)
+ if code != OK:
+ self._greetingError = status
+ self.transport.loseConnection()
+ else:
+ m = self._challengeMagicRe.search(status)
+
+ if m is not None:
+ self.serverChallenge = m.group(1)
+
+ self.serverGreeting(status)
+
+ self._unblock()
+ return "WAITING"
+
+ def state_WAITING(self, line):
+ """
+ Log an error for server responses received in the WAITING state during
+ which the server is not expected to send anything.
+
+ @type line: L{bytes}
+ @param line: A line received from the server.
+ """
+ log.msg("Illegal line from server: " + repr(line))
+
+ def state_SHORT(self, line):
+ """
+ Handle server responses for the SHORT state in which the server is
+ expected to send a single line response.
+
+ Parse the response and fire the deferred which is waiting on receipt of
+ a complete response. Transition the state back to WAITING.
+
+ @type line: L{bytes}
+ @param line: A line received from the server.
+
+ @rtype: L{bytes}
+ @return: The next state.
+ """
+ deferred, self._waiting = self._waiting, None
+ self._unblock()
+ code, status = _codeStatusSplit(line)
+ if code == OK:
+ deferred.callback(status)
+ else:
+ deferred.errback(ServerErrorResponse(status))
+ return "WAITING"
+
+ def state_LONG_INITIAL(self, line):
+ """
+ Handle server responses for the LONG_INITIAL state in which the server
+ is expected to send the first line of a multi-line response.
+
+ Parse the response. On an OK response, transition the state to
+ LONG. On an ERR response, cleanup and transition the state to
+ WAITING.
+
+ @type line: L{bytes}
+ @param line: A line received from the server.
+
+ @rtype: L{bytes}
+ @return: The next state.
+ """
+ code, status = _codeStatusSplit(line)
+ if code == OK:
+ return "LONG"
+ consumer = self._consumer
+ deferred = self._waiting
+ self._consumer = self._waiting = self._xform = None
+ self._unblock()
+ deferred.errback(ServerErrorResponse(status, consumer))
+ return "WAITING"
+
+ def state_LONG(self, line):
+ """
+ Handle server responses for the LONG state in which the server is
+ expected to send a non-initial line of a multi-line response.
+
+ On receipt of the last line of the response, clean up, fire the
+ deferred which is waiting on receipt of a complete response, and
+ transition the state to WAITING. Otherwise, pass the line to the
+ transform function, if provided, and then the consumer function.
+
+ @type line: L{bytes}
+ @param line: A line received from the server.
+
+ @rtype: L{bytes}
+ @return: The next state.
+ """
+ # This is the state for each line of a long response.
+ if line == b".":
+ consumer = self._consumer
+ deferred = self._waiting
+ self._consumer = self._waiting = self._xform = None
+ self._unblock()
+ deferred.callback(consumer)
+ return "WAITING"
+ else:
+ if self._xform is not None:
+ self._consumer(self._xform(line))
+ else:
+ self._consumer(line)
+ return "LONG"
+
+ # Callbacks - override these
+ def serverGreeting(self, greeting):
+ """
+ Handle the server greeting.
+
+ @type greeting: L{bytes}
+ @param greeting: The server greeting minus the status indicator.
+ For servers implementing APOP authentication, this will contain a
+ challenge string.
+ """
+
+ # External API - call these (most of 'em anyway)
+ def startTLS(self, contextFactory=None):
+ """
+ Switch to encrypted communication using TLS.
+
+ The first step of switching to encrypted communication is obtaining
+ the server's capabilities. When that is complete, the L{_startTLS}
+ callback function continues the switching process.
+
+ @type contextFactory: L{None} or
+ L{ClientContextFactory <twisted.internet.ssl.ClientContextFactory>}
+ @param contextFactory: The context factory with which to negotiate TLS.
+ If not provided, try to create a new one.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully results in
+ L{dict} mapping L{bytes} to L{list} of L{bytes} and/or L{bytes} to
+ L{None} or fails with L{TLSError}
+ @return: A deferred which fires when the transport has been
+ secured according to the given context factory with the server
+ capabilities, or which fails with a TLS error if the transport
+ cannot be secured.
+ """
+ tls = interfaces.ITLSTransport(self.transport, None)
+ if tls is None:
+ return defer.fail(
+ TLSError(
+ "POP3Client transport does not implement "
+ "interfaces.ITLSTransport"
+ )
+ )
+
+ if contextFactory is None:
+ contextFactory = self._getContextFactory()
+
+ if contextFactory is None:
+ return defer.fail(
+ TLSError(
+ "POP3Client requires a TLS context to "
+ "initiate the STLS handshake"
+ )
+ )
+
+ d = self.capabilities()
+ d.addCallback(self._startTLS, contextFactory, tls)
+ return d
+
+ def _startTLS(self, caps, contextFactory, tls):
+ """
+ Continue the process of switching to encrypted communication.
+
+ This callback function runs after the server capabilities are received.
+
+ The next step is sending the server an STLS command to request a
+ switch to encrypted communication. When an OK response is received,
+ the L{_startedTLS} callback function completes the switch to encrypted
+ communication. Then, the new server capabilities are requested.
+
+ @type caps: L{dict} mapping L{bytes} to L{list} of L{bytes} and/or
+ L{bytes} to L{None}
+ @param caps: The server capabilities.
+
+ @type contextFactory: L{ClientContextFactory
+ <twisted.internet.ssl.ClientContextFactory>}
+ @param contextFactory: A context factory with which to negotiate TLS.
+
+ @type tls: L{ITLSTransport <interfaces.ITLSTransport>}
+ @param tls: A TCP transport that supports switching to TLS midstream.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully triggers with
+ L{dict} mapping L{bytes} to L{list} of L{bytes} and/or L{bytes} to
+ L{None} or fails with L{TLSNotSupportedError}
+ @return: A deferred which successfully fires when the response from
+ the server to the request to start TLS has been received and the
+ new server capabilities have been received or fails when the server
+ does not support TLS.
+ """
+ assert (
+ not self.startedTLS
+ ), "Client and Server are currently communicating via TLS"
+
+ if b"STLS" not in caps:
+ return defer.fail(
+ TLSNotSupportedError(
+ "Server does not support secure communication " "via TLS / SSL"
+ )
+ )
+
+ d = self.sendShort(b"STLS", None)
+ d.addCallback(self._startedTLS, contextFactory, tls)
+ d.addCallback(lambda _: self.capabilities())
+ return d
+
+ def _startedTLS(self, result, context, tls):
+ """
+ Complete the process of switching to encrypted communication.
+
+ This callback function runs after the response to the STLS command has
+ been received.
+
+ The final steps are discarding the cached capabilities and initiating
+ TLS negotiation on the transport.
+
+ @type result: L{dict} mapping L{bytes} to L{list} of L{bytes} and/or
+ L{bytes} to L{None}
+ @param result: The server capabilities.
+
+ @type context: L{ClientContextFactory
+ <twisted.internet.ssl.ClientContextFactory>}
+ @param context: A context factory with which to negotiate TLS.
+
+ @type tls: L{ITLSTransport <interfaces.ITLSTransport>}
+ @param tls: A TCP transport that supports switching to TLS midstream.
+
+ @rtype: L{dict} mapping L{bytes} to L{list} of L{bytes} and/or L{bytes}
+ to L{None}
+ @return: The server capabilities.
+ """
+ self.transport = tls
+ self.transport.startTLS(context)
+ self._capCache = None
+ self.startedTLS = True
+ return result
+
+ def _getContextFactory(self):
+ """
+ Get a context factory with which to negotiate TLS.
+
+ @rtype: L{None} or
+ L{ClientContextFactory <twisted.internet.ssl.ClientContextFactory>}
+ @return: A context factory or L{None} if TLS is not supported on the
+ client.
+ """
+ try:
+ from twisted.internet import ssl
+ except ImportError:
+ return None
+ else:
+ context = ssl.ClientContextFactory()
+ context.method = ssl.SSL.TLSv1_2_METHOD
+ return context
+
+ def login(self, username, password):
+ """
+ Log in to the server.
+
+ If APOP is available it will be used. Otherwise, if TLS is
+ available, an encrypted session will be started and plaintext
+ login will proceed. Otherwise, if L{allowInsecureLogin} is set,
+ insecure plaintext login will proceed. Otherwise,
+ L{InsecureAuthenticationDisallowed} will be raised.
+
+ The first step of logging into the server is obtaining the server's
+ capabilities. When that is complete, the L{_login} callback function
+ continues the login process.
+
+ @type username: L{bytes}
+ @param username: The username with which to log in.
+
+ @type password: L{bytes}
+ @param password: The password with which to log in.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully fires with
+ L{bytes}
+ @return: A deferred which fires when the login process is complete.
+ On a successful login, it returns the server's response minus the
+ status indicator.
+ """
+ d = self.capabilities()
+ d.addCallback(self._login, username, password)
+ return d
+
+ def _login(self, caps, username, password):
+ """
+ Continue the process of logging in to the server.
+
+ This callback function runs after the server capabilities are received.
+
+ If the server provided a challenge in the greeting, proceed with an
+ APOP login. Otherwise, if the server and the transport support
+ encrypted communication, try to switch to TLS and then complete
+ the login process with the L{_loginTLS} callback function. Otherwise,
+ if insecure authentication is allowed, do a plaintext login.
+ Otherwise, fail with an L{InsecureAuthenticationDisallowed} error.
+
+ @type caps: L{dict} mapping L{bytes} to L{list} of L{bytes} and/or
+ L{bytes} to L{None}
+ @param caps: The server capabilities.
+
+ @type username: L{bytes}
+ @param username: The username with which to log in.
+
+ @type password: L{bytes}
+ @param password: The password with which to log in.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully fires with
+ L{bytes}
+ @return: A deferred which fires when the login process is complete.
+ On a successful login, it returns the server's response minus the
+ status indicator.
+ """
+ if self.serverChallenge is not None:
+ return self._apop(username, password, self.serverChallenge)
+
+ tryTLS = b"STLS" in caps
+
+ # If our transport supports switching to TLS, we might want to
+ # try to switch to TLS.
+ tlsableTransport = interfaces.ITLSTransport(self.transport, None) is not None
+
+ # If our transport is not already using TLS, we might want to
+ # try to switch to TLS.
+ nontlsTransport = interfaces.ISSLTransport(self.transport, None) is None
+
+ if not self.startedTLS and tryTLS and tlsableTransport and nontlsTransport:
+ d = self.startTLS()
+
+ d.addCallback(self._loginTLS, username, password)
+ return d
+
+ elif self.startedTLS or not nontlsTransport or self.allowInsecureLogin:
+ return self._plaintext(username, password)
+ else:
+ return defer.fail(InsecureAuthenticationDisallowed())
+
+ def _loginTLS(self, res, username, password):
+ """
+ Do a plaintext login over an encrypted transport.
+
+ This callback function runs after the transport switches to encrypted
+ communication.
+
+ @type res: L{dict} mapping L{bytes} to L{list} of L{bytes} and/or
+ L{bytes} to L{None}
+ @param res: The server capabilities.
+
+ @type username: L{bytes}
+ @param username: The username with which to log in.
+
+ @type password: L{bytes}
+ @param password: The password with which to log in.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully fires with
+ L{bytes} or fails with L{ServerErrorResponse}
+ @return: A deferred which fires when the server accepts the username
+ and password or fails when the server rejects either. On a
+ successful login, it returns the server's response minus the
+ status indicator.
+ """
+ return self._plaintext(username, password)
+
+ def _plaintext(self, username, password):
+ """
+ Perform a plaintext login.
+
+ @type username: L{bytes}
+ @param username: The username with which to log in.
+
+ @type password: L{bytes}
+ @param password: The password with which to log in.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully fires with
+ L{bytes} or fails with L{ServerErrorResponse}
+ @return: A deferred which fires when the server accepts the username
+ and password or fails when the server rejects either. On a
+ successful login, it returns the server's response minus the
+ status indicator.
+ """
+ return self.user(username).addCallback(lambda r: self.password(password))
+
+ def _apop(self, username, password, challenge):
+ """
+ Perform an APOP login.
+
+ @type username: L{bytes}
+ @param username: The username with which to log in.
+
+ @type password: L{bytes}
+ @param password: The password with which to log in.
+
+ @type challenge: L{bytes}
+ @param challenge: A challenge string.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully fires with
+ L{bytes} or fails with L{ServerErrorResponse}
+ @return: A deferred which fires when the server response is received.
+ On a successful login, it returns the server response minus
+ the status indicator.
+ """
+ digest = md5(challenge + password).hexdigest().encode("ascii")
+ return self.apop(username, digest)
+
+ def apop(self, username, digest):
+ """
+ Send an APOP command to perform authenticated login.
+
+ This should be used in special circumstances only, when it is
+ known that the server supports APOP authentication, and APOP
+ authentication is absolutely required. For the common case,
+ use L{login} instead.
+
+ @type username: L{bytes}
+ @param username: The username with which to log in.
+
+ @type digest: L{bytes}
+ @param digest: The challenge response to authenticate with.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully fires with
+ L{bytes} or fails with L{ServerErrorResponse}
+ @return: A deferred which fires when the server response is received.
+ On an OK response, the deferred succeeds with the server
+ response minus the status indicator. On an ERR response, the
+ deferred fails with a server error response failure.
+ """
+ return self.sendShort(b"APOP", username + b" " + digest)
+
+ def user(self, username):
+ """
+ Send a USER command to perform the first half of plaintext login.
+
+ Unless this is absolutely required, use the L{login} method instead.
+
+ @type username: L{bytes}
+ @param username: The username with which to log in.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully fires with
+ L{bytes} or fails with L{ServerErrorResponse}
+ @return: A deferred which fires when the server response is received.
+ On an OK response, the deferred succeeds with the server
+ response minus the status indicator. On an ERR response, the
+ deferred fails with a server error response failure.
+ """
+ return self.sendShort(b"USER", username)
+
+ def password(self, password):
+ """
+ Send a PASS command to perform the second half of plaintext login.
+
+ Unless this is absolutely required, use the L{login} method instead.
+
+ @type password: L{bytes}
+ @param password: The plaintext password with which to authenticate.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully fires with
+ L{bytes} or fails with L{ServerErrorResponse}
+ @return: A deferred which fires when the server response is received.
+ On an OK response, the deferred succeeds with the server
+ response minus the status indicator. On an ERR response, the
+ deferred fails with a server error response failure.
+ """
+ return self.sendShort(b"PASS", password)
+
+ def delete(self, index):
+ """
+ Send a DELE command to delete a message from the server.
+
+ @type index: L{int}
+ @param index: The 0-based index of the message to delete.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully fires with
+ L{bytes} or fails with L{ServerErrorResponse}
+ @return: A deferred which fires when the server response is received.
+ On an OK response, the deferred succeeds with the server
+ response minus the status indicator. On an ERR response, the
+ deferred fails with a server error response failure.
+ """
+ return self.sendShort(b"DELE", b"%d" % (index + 1,))
+
+ def _consumeOrSetItem(self, cmd, args, consumer, xform):
+ """
+ Send a command to which a long response is expected and process the
+ multi-line response into a list accounting for deleted messages.
+
+ @type cmd: L{bytes}
+ @param cmd: A POP3 command to which a long response is expected.
+
+ @type args: L{bytes}
+ @param args: The command arguments.
+
+ @type consumer: L{None} or callable that takes
+ L{object}
+ @param consumer: L{None} or a function that consumes the output from
+ the transform function.
+
+ @type xform: L{None}, callable that takes
+ L{bytes} and returns 2-L{tuple} of (0) L{int}, (1) L{object},
+ or callable that takes L{bytes} and returns L{object}
+ @param xform: A function that parses a line from a multi-line response
+ and transforms the values into usable form for input to the
+ consumer function. If no consumer function is specified, the
+ output must be a message index and corresponding value. If no
+ transform function is specified, the line is used as is.
+
+ @rtype: L{Deferred <defer.Deferred>} which fires with L{list} of
+ L{object} or callable that takes L{list} of L{object}
+ @return: A deferred which fires when the entire response has been
+ received. When a consumer is not provided, the return value is a
+ list of the value for each message or L{None} for deleted messages.
+ Otherwise, it returns the consumer itself.
+ """
+ if consumer is None:
+ L = []
+ consumer = _ListSetter(L).setitem
+ return self.sendLong(cmd, args, consumer, xform).addCallback(lambda r: L)
+ return self.sendLong(cmd, args, consumer, xform)
+
+ def _consumeOrAppend(self, cmd, args, consumer, xform):
+ """
+ Send a command to which a long response is expected and process the
+ multi-line response into a list.
+
+ @type cmd: L{bytes}
+ @param cmd: A POP3 command which expects a long response.
+
+ @type args: L{bytes}
+ @param args: The command arguments.
+
+ @type consumer: L{None} or callable that takes
+ L{object}
+ @param consumer: L{None} or a function that consumes the output from the
+ transform function.
+
+ @type xform: L{None} or callable that takes
+ L{bytes} and returns L{object}
+ @param xform: A function that transforms a line from a multi-line
+ response into usable form for input to the consumer function. If
+ no transform function is specified, the line is used as is.
+
+ @rtype: L{Deferred <defer.Deferred>} which fires with L{list} of
+ 2-L{tuple} of (0) L{int}, (1) L{object} or callable that
+ takes 2-L{tuple} of (0) L{int}, (1) L{object}
+ @return: A deferred which fires when the entire response has been
+ received. When a consumer is not provided, the return value is a
+ list of the transformed lines. Otherwise, it returns the consumer
+ itself.
+ """
+ if consumer is None:
+ L = []
+ consumer = L.append
+ return self.sendLong(cmd, args, consumer, xform).addCallback(lambda r: L)
+ return self.sendLong(cmd, args, consumer, xform)
+
+ def capabilities(self, useCache=True):
+ """
+ Send a CAPA command to retrieve the capabilities supported by
+ the server.
+
+ Not all servers support this command. If the server does not
+ support this, it is treated as though it returned a successful
+ response listing no capabilities. At some future time, this may be
+ changed to instead seek out information about a server's
+ capabilities in some other fashion (only if it proves useful to do
+ so, and only if there are servers still in use which do not support
+ CAPA but which do support POP3 extensions that are useful).
+
+ @type useCache: L{bool}
+ @param useCache: A flag that determines whether previously retrieved
+ results should be used if available.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully results in
+ L{dict} mapping L{bytes} to L{list} of L{bytes} and/or L{bytes} to
+ L{None}
+ @return: A deferred which fires with a mapping of capability name to
+ parameters. For example::
+
+ C: CAPA
+ S: +OK Capability list follows
+ S: TOP
+ S: USER
+ S: SASL CRAM-MD5 KERBEROS_V4
+ S: RESP-CODES
+ S: LOGIN-DELAY 900
+ S: PIPELINING
+ S: EXPIRE 60
+ S: UIDL
+ S: IMPLEMENTATION Shlemazle-Plotz-v302
+ S: .
+
+ will be lead to a result of::
+
+ | {'TOP': None,
+ | 'USER': None,
+ | 'SASL': ['CRAM-MD5', 'KERBEROS_V4'],
+ | 'RESP-CODES': None,
+ | 'LOGIN-DELAY': ['900'],
+ | 'PIPELINING': None,
+ | 'EXPIRE': ['60'],
+ | 'UIDL': None,
+ | 'IMPLEMENTATION': ['Shlemazle-Plotz-v302']}
+ """
+ if useCache and self._capCache is not None:
+ return defer.succeed(self._capCache)
+
+ cache = {}
+
+ def consume(line):
+ tmp = line.split()
+ if len(tmp) == 1:
+ cache[tmp[0]] = None
+ elif len(tmp) > 1:
+ cache[tmp[0]] = tmp[1:]
+
+ def capaNotSupported(err):
+ err.trap(ServerErrorResponse)
+ return None
+
+ def gotCapabilities(result):
+ self._capCache = cache
+ return cache
+
+ d = self._consumeOrAppend(b"CAPA", None, consume, None)
+ d.addErrback(capaNotSupported).addCallback(gotCapabilities)
+ return d
+
+ def noop(self):
+ """
+ Send a NOOP command asking the server to do nothing but respond.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully fires with
+ L{bytes} or fails with L{ServerErrorResponse}
+ @return: A deferred which fires when the server response is received.
+ On an OK response, the deferred succeeds with the server
+ response minus the status indicator. On an ERR response, the
+ deferred fails with a server error response failure.
+ """
+ return self.sendShort(b"NOOP", None)
+
+ def reset(self):
+ """
+ Send a RSET command to unmark any messages that have been flagged
+ for deletion on the server.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully fires with
+ L{bytes} or fails with L{ServerErrorResponse}
+ @return: A deferred which fires when the server response is received.
+ On an OK response, the deferred succeeds with the server
+ response minus the status indicator. On an ERR response, the
+ deferred fails with a server error response failure.
+ """
+ return self.sendShort(b"RSET", None)
+
+ def retrieve(self, index, consumer=None, lines=None):
+ """
+ Send a RETR or TOP command to retrieve all or part of a message from
+ the server.
+
+ @type index: L{int}
+ @param index: A 0-based message index.
+
+ @type consumer: L{None} or callable that takes
+ L{bytes}
+ @param consumer: A function which consumes each transformed line from a
+ multi-line response as it is received.
+
+ @type lines: L{None} or L{int}
+ @param lines: If specified, the number of lines of the message to be
+ retrieved. Otherwise, the entire message is retrieved.
+
+ @rtype: L{Deferred <defer.Deferred>} which fires with L{list} of
+ L{bytes}, or callable that takes 2-L{tuple} of (0) L{int},
+ (1) L{object}
+ @return: A deferred which fires when the entire response has been
+ received. When a consumer is not provided, the return value is a
+ list of the transformed lines. Otherwise, it returns the consumer
+ itself.
+ """
+ idx = b"%d" % (index + 1,)
+ if lines is None:
+ return self._consumeOrAppend(b"RETR", idx, consumer, _dotUnquoter)
+
+ return self._consumeOrAppend(
+ b"TOP", b"%b %d" % (idx, lines), consumer, _dotUnquoter
+ )
+
+ def stat(self):
+ """
+ Send a STAT command to get information about the size of the mailbox.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully fires with
+ a 2-tuple of (0) L{int}, (1) L{int} or fails with
+ L{ServerErrorResponse}
+ @return: A deferred which fires when the server response is received.
+ On an OK response, the deferred succeeds with the number of
+ messages in the mailbox and the size of the mailbox in octets.
+ On an ERR response, the deferred fails with a server error
+ response failure.
+ """
+ return self.sendShort(b"STAT", None).addCallback(_statXform)
+
+ def listSize(self, consumer=None):
+ """
+ Send a LIST command to retrieve the sizes of all messages on the
+ server.
+
+ @type consumer: L{None} or callable that takes
+ 2-L{tuple} of (0) L{int}, (1) L{int}
+ @param consumer: A function which consumes the 0-based message index
+ and message size derived from the server response.
+
+ @rtype: L{Deferred <defer.Deferred>} which fires L{list} of L{int} or
+ callable that takes 2-L{tuple} of (0) L{int}, (1) L{int}
+ @return: A deferred which fires when the entire response has been
+ received. When a consumer is not provided, the return value is a
+ list of message sizes. Otherwise, it returns the consumer itself.
+ """
+ return self._consumeOrSetItem(b"LIST", None, consumer, _listXform)
+
+ def listUID(self, consumer=None):
+ """
+ Send a UIDL command to retrieve the UIDs of all messages on the server.
+
+ @type consumer: L{None} or callable that takes
+ 2-L{tuple} of (0) L{int}, (1) L{bytes}
+ @param consumer: A function which consumes the 0-based message index
+ and UID derived from the server response.
+
+ @rtype: L{Deferred <defer.Deferred>} which fires with L{list} of
+ L{object} or callable that takes 2-L{tuple} of (0) L{int},
+ (1) L{bytes}
+ @return: A deferred which fires when the entire response has been
+ received. When a consumer is not provided, the return value is a
+ list of message sizes. Otherwise, it returns the consumer itself.
+ """
+ return self._consumeOrSetItem(b"UIDL", None, consumer, _uidXform)
+
+ def quit(self):
+ """
+ Send a QUIT command to disconnect from the server.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully fires with
+ L{bytes} or fails with L{ServerErrorResponse}
+ @return: A deferred which fires when the server response is received.
+ On an OK response, the deferred succeeds with the server
+ response minus the status indicator. On an ERR response, the
+ deferred fails with a server error response failure.
+ """
+ return self.sendShort(b"QUIT", None)
+
+
+__all__: List[str] = []
diff --git a/contrib/python/Twisted/py3/twisted/mail/alias.py b/contrib/python/Twisted/py3/twisted/mail/alias.py
new file mode 100644
index 0000000000..4713ad3821
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/alias.py
@@ -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))
diff --git a/contrib/python/Twisted/py3/twisted/mail/bounce.py b/contrib/python/Twisted/py3/twisted/mail/bounce.py
new file mode 100644
index 0000000000..adf9d313af
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/bounce.py
@@ -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)
diff --git a/contrib/python/Twisted/py3/twisted/mail/imap4.py b/contrib/python/Twisted/py3/twisted/mail/imap4.py
new file mode 100644
index 0000000000..032624e3db
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/imap4.py
@@ -0,0 +1,6233 @@
+# -*- test-case-name: twisted.mail.test.test_imap.IMAP4HelperTests -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+An IMAP4 protocol implementation
+
+@author: Jp Calderone
+
+To do::
+ Suspend idle timeout while server is processing
+ Use an async message parser instead of buffering in memory
+ Figure out a way to not queue multi-message client requests (Flow? A simple callback?)
+ Clarify some API docs (Query, etc)
+ Make APPEND recognize (again) non-existent mailboxes before accepting the literal
+"""
+
+import binascii
+import codecs
+import copy
+import email.utils
+import functools
+import re
+import string
+import tempfile
+import time
+import uuid
+from base64 import decodebytes, encodebytes
+from io import BytesIO
+from itertools import chain
+from typing import Any, List, cast
+
+from zope.interface import implementer
+
+from twisted.cred import credentials
+from twisted.cred.error import UnauthorizedLogin, UnhandledCredentials
+from twisted.internet import defer, error, interfaces
+from twisted.internet.defer import maybeDeferred
+from twisted.mail._cred import (
+ CramMD5ClientAuthenticator,
+ LOGINAuthenticator,
+ LOGINCredentials,
+ PLAINAuthenticator,
+ PLAINCredentials,
+)
+from twisted.mail._except import (
+ IllegalClientResponse,
+ IllegalIdentifierError,
+ IllegalMailboxEncoding,
+ IllegalOperation,
+ IllegalQueryError,
+ IllegalServerResponse,
+ IMAP4Exception,
+ MailboxCollision,
+ MailboxException,
+ MismatchedNesting,
+ MismatchedQuoting,
+ NegativeResponse,
+ NoSuchMailbox,
+ NoSupportedAuthentication,
+ ReadOnlyMailbox,
+ UnhandledResponse,
+)
+
+# Re-exported for compatibility reasons
+from twisted.mail.interfaces import (
+ IAccountIMAP as IAccount,
+ IClientAuthentication,
+ ICloseableMailboxIMAP as ICloseableMailbox,
+ IMailboxIMAP as IMailbox,
+ IMailboxIMAPInfo as IMailboxInfo,
+ IMailboxIMAPListener as IMailboxListener,
+ IMessageIMAP as IMessage,
+ IMessageIMAPCopier as IMessageCopier,
+ IMessageIMAPFile as IMessageFile,
+ IMessageIMAPPart as IMessagePart,
+ INamespacePresenter,
+ ISearchableIMAPMailbox as ISearchableMailbox,
+)
+from twisted.protocols import basic, policies
+from twisted.python import log, text
+from twisted.python.compat import (
+ _get_async_param,
+ _matchingString,
+ iterbytes,
+ nativeString,
+ networkString,
+)
+
+# locale-independent month names to use instead of strftime's
+_MONTH_NAMES = dict(
+ zip(range(1, 13), "Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec".split())
+)
+
+
+def _swap(this, that, ifIs):
+ """
+ Swap C{this} with C{that} if C{this} is C{ifIs}.
+
+ @param this: The object that may be replaced.
+
+ @param that: The object that may replace C{this}.
+
+ @param ifIs: An object whose identity will be compared to
+ C{this}.
+ """
+ return that if this is ifIs else this
+
+
+def _swapAllPairs(of, that, ifIs):
+ """
+ Swap each element in each pair in C{of} with C{that} it is
+ C{ifIs}.
+
+ @param of: A list of 2-L{tuple}s, whose members may be the object
+ C{that}
+ @type of: L{list} of 2-L{tuple}s
+
+ @param ifIs: An object whose identity will be compared to members
+ of each pair in C{of}
+
+ @return: A L{list} of 2-L{tuple}s with all occurences of C{ifIs}
+ replaced with C{that}
+ """
+ return [
+ (_swap(first, that, ifIs), _swap(second, that, ifIs)) for first, second in of
+ ]
+
+
+class MessageSet:
+ """
+ A set of message identifiers usable by both L{IMAP4Client} and
+ L{IMAP4Server} via L{IMailboxIMAP.store} and
+ L{IMailboxIMAP.fetch}.
+
+ These identifiers can be either message sequence numbers or unique
+ identifiers. See Section 2.3.1, "Message Numbers", RFC 3501.
+
+ This represents the C{sequence-set} described in Section 9,
+ "Formal Syntax" of RFC 3501:
+
+ - A L{MessageSet} can describe a single identifier, e.g.
+ C{MessageSet(1)}
+
+ - A L{MessageSet} can describe C{*} via L{None}, e.g.
+ C{MessageSet(None)}
+
+ - A L{MessageSet} can describe a range of identifiers, e.g.
+ C{MessageSet(1, 2)}. The range is inclusive and unordered
+ (see C{seq-range} in RFC 3501, Section 9), so that
+ C{Message(2, 1)} is equivalent to C{MessageSet(1, 2)}, and
+ both describe messages 1 and 2. Ranges can include C{*} by
+ specifying L{None}, e.g. C{MessageSet(None, 1)}. In all
+ cases ranges are normalized so that the smallest identifier
+ comes first, and L{None} always comes last; C{Message(2, 1)}
+ becomes C{MessageSet(1, 2)} and C{MessageSet(None, 1)}
+ becomes C{MessageSet(1, None)}
+
+ - A L{MessageSet} can describe a sequence of single
+ identifiers and ranges, constructed by addition.
+ C{MessageSet(1) + MessageSet(5, 10)} refers the message
+ identified by C{1} and the messages identified by C{5}
+ through C{10}.
+
+ B{NB: The meaning of * varies, but it always represents the
+ largest number in use}.
+
+ B{For servers}: Your L{IMailboxIMAP} provider must set
+ L{MessageSet.last} to the highest-valued identifier (unique or
+ message sequence) before iterating over it.
+
+ B{For clients}: C{*} consumes ranges smaller than it, e.g.
+ C{MessageSet(1, 100) + MessageSet(50, None)} is equivalent to
+ C{1:*}.
+
+ @type getnext: Function taking L{int} returning L{int}
+ @ivar getnext: A function that returns the next message number,
+ used when iterating through the L{MessageSet}. By default, a
+ function returning the next integer is supplied, but as this
+ can be rather inefficient for sparse UID iterations, it is
+ recommended to supply one when messages are requested by UID.
+ The argument is provided as a hint to the implementation and
+ may be ignored if it makes sense to do so (eg, if an iterator
+ is being used that maintains its own state, it is guaranteed
+ that it will not be called out-of-order).
+ """
+
+ _empty: List[Any] = []
+ _infinity = float("inf")
+
+ def __init__(self, start=_empty, end=_empty):
+ """
+ Create a new MessageSet()
+
+ @type start: Optional L{int}
+ @param start: Start of range, or only message number
+
+ @type end: Optional L{int}
+ @param end: End of range.
+ """
+ self._last = self._empty # Last message/UID in use
+ self.ranges = [] # List of ranges included
+ self.getnext = lambda x: x + 1 # A function which will return the next
+ # message id. Handy for UID requests.
+
+ if start is self._empty:
+ return
+
+ if isinstance(start, list):
+ self.ranges = start[:]
+ self.clean()
+ else:
+ self.add(start, end)
+
+ @property
+ def last(self):
+ """
+ The largest number in use.
+ This is undefined until it has been set by assigning to this property.
+ """
+ return self._last
+
+ @last.setter
+ def last(self, value):
+ """
+ Replaces all occurrences of "*". This should be the
+ largest number in use. Must be set before attempting to
+ use the MessageSet as a container.
+
+ @raises ValueError: if a largest value has already been set.
+ """
+ if self._last is not self._empty:
+ raise ValueError("last already set")
+
+ self._last = value
+ for i, (low, high) in enumerate(self.ranges):
+ if low is None:
+ low = value
+ if high is None:
+ high = value
+ if low > high:
+ low, high = high, low
+ self.ranges[i] = (low, high)
+ self.clean()
+
+ def add(self, start, end=_empty):
+ """
+ Add another range
+
+ @type start: L{int}
+ @param start: Start of range, or only message number
+
+ @type end: Optional L{int}
+ @param end: End of range.
+ """
+ if end is self._empty:
+ end = start
+
+ if self._last is not self._empty:
+ if start is None:
+ start = self.last
+ if end is None:
+ end = self.last
+
+ start, end = sorted(
+ [start, end], key=functools.partial(_swap, that=self._infinity, ifIs=None)
+ )
+ self.ranges.append((start, end))
+ self.clean()
+
+ def __add__(self, other):
+ if isinstance(other, MessageSet):
+ ranges = self.ranges + other.ranges
+ return MessageSet(ranges)
+ else:
+ res = MessageSet(self.ranges)
+ if self.last is not self._empty:
+ res.last = self.last
+ try:
+ res.add(*other)
+ except TypeError:
+ res.add(other)
+ return res
+
+ def extend(self, other):
+ """
+ Extend our messages with another message or set of messages.
+
+ @param other: The messages to include.
+ @type other: L{MessageSet}, L{tuple} of two L{int}s, or a
+ single L{int}
+ """
+ if isinstance(other, MessageSet):
+ self.ranges.extend(other.ranges)
+ self.clean()
+ else:
+ try:
+ self.add(*other)
+ except TypeError:
+ self.add(other)
+
+ return self
+
+ def clean(self):
+ """
+ Clean ranges list, combining adjacent ranges
+ """
+
+ ranges = sorted(_swapAllPairs(self.ranges, that=self._infinity, ifIs=None))
+
+ mergedRanges = [(float("-inf"), float("-inf"))]
+
+ for low, high in ranges:
+ previousLow, previousHigh = mergedRanges[-1]
+
+ if previousHigh < low - 1:
+ mergedRanges.append((low, high))
+ continue
+
+ mergedRanges[-1] = (min(previousLow, low), max(previousHigh, high))
+
+ self.ranges = _swapAllPairs(mergedRanges[1:], that=None, ifIs=self._infinity)
+
+ def _noneInRanges(self):
+ """
+ Is there a L{None} in our ranges?
+
+ L{MessageSet.clean} merges overlapping or consecutive ranges.
+ None is represents a value larger than any number. There are
+ thus two cases:
+
+ 1. C{(x, *) + (y, z)} such that C{x} is smaller than C{y}
+
+ 2. C{(z, *) + (x, y)} such that C{z} is larger than C{y}
+
+ (Other cases, such as C{y < x < z}, can be split into these
+ two cases; for example C{(y - 1, y)} + C{(x, x) + (z, z + 1)})
+
+ In case 1, C{* > y} and C{* > z}, so C{(x, *) + (y, z) = (x,
+ *)}
+
+ In case 2, C{z > x and z > y}, so the intervals do not merge,
+ and the ranges are sorted as C{[(x, y), (z, *)]}. C{*} is
+ represented as C{(*, *)}, so this is the same as 2. but with
+ a C{z} that is greater than everything.
+
+ The result is that there is a maximum of two L{None}s, and one
+ of them has to be the high element in the last tuple in
+ C{self.ranges}. That means checking if C{self.ranges[-1][-1]}
+ is L{None} suffices to check if I{any} element is L{None}.
+
+ @return: L{True} if L{None} is in some range in ranges and
+ L{False} if otherwise.
+ """
+ return self.ranges[-1][-1] is None
+
+ def __contains__(self, value):
+ """
+ May raise TypeError if we encounter an open-ended range
+
+ @param value: Is this in our ranges?
+ @type value: L{int}
+ """
+
+ if self._noneInRanges():
+ raise TypeError("Can't determine membership; last value not set")
+
+ for low, high in self.ranges:
+ if low <= value <= high:
+ return True
+
+ return False
+
+ def _iterator(self):
+ for l, h in self.ranges:
+ l = self.getnext(l - 1)
+ while l <= h:
+ yield l
+ l = self.getnext(l)
+
+ def __iter__(self):
+ if self._noneInRanges():
+ raise TypeError("Can't iterate; last value not set")
+
+ return self._iterator()
+
+ def __len__(self):
+ res = 0
+ for l, h in self.ranges:
+ if l is None:
+ res += 1
+ elif h is None:
+ raise TypeError("Can't size object; last value not set")
+ else:
+ res += (h - l) + 1
+
+ return res
+
+ def __str__(self) -> str:
+ p = []
+ for low, high in self.ranges:
+ if low == high:
+ if low is None:
+ p.append("*")
+ else:
+ p.append(str(low))
+ elif high is None:
+ p.append("%d:*" % (low,))
+ else:
+ p.append("%d:%d" % (low, high))
+ return ",".join(p)
+
+ def __repr__(self) -> str:
+ return f"<MessageSet {str(self)}>"
+
+ def __eq__(self, other: object) -> bool:
+ if isinstance(other, MessageSet):
+ return cast(bool, self.ranges == other.ranges)
+ return NotImplemented
+
+
+class LiteralString:
+ def __init__(self, size, defered):
+ self.size = size
+ self.data = []
+ self.defer = defered
+
+ def write(self, data):
+ self.size -= len(data)
+ passon = None
+ if self.size > 0:
+ self.data.append(data)
+ else:
+ if self.size:
+ data, passon = data[: self.size], data[self.size :]
+ else:
+ passon = b""
+ if data:
+ self.data.append(data)
+
+ return passon
+
+ def callback(self, line):
+ """
+ Call deferred with data and rest of line
+ """
+ self.defer.callback((b"".join(self.data), line))
+
+
+class LiteralFile:
+ _memoryFileLimit = 1024 * 1024 * 10
+
+ def __init__(self, size, defered):
+ self.size = size
+ self.defer = defered
+ if size > self._memoryFileLimit:
+ self.data = tempfile.TemporaryFile()
+ else:
+ self.data = BytesIO()
+
+ def write(self, data):
+ self.size -= len(data)
+ passon = None
+ if self.size > 0:
+ self.data.write(data)
+ else:
+ if self.size:
+ data, passon = data[: self.size], data[self.size :]
+ else:
+ passon = b""
+ if data:
+ self.data.write(data)
+ return passon
+
+ def callback(self, line):
+ """
+ Call deferred with data and rest of line
+ """
+ self.data.seek(0, 0)
+ self.defer.callback((self.data, line))
+
+
+class WriteBuffer:
+ """
+ Buffer up a bunch of writes before sending them all to a transport at once.
+ """
+
+ def __init__(self, transport, size=8192):
+ self.bufferSize = size
+ self.transport = transport
+ self._length = 0
+ self._writes = []
+
+ def write(self, s):
+ self._length += len(s)
+ self._writes.append(s)
+ if self._length > self.bufferSize:
+ self.flush()
+
+ def flush(self):
+ if self._writes:
+ self.transport.writeSequence(self._writes)
+ self._writes = []
+ self._length = 0
+
+
+class Command:
+ _1_RESPONSES = (
+ b"CAPABILITY",
+ b"FLAGS",
+ b"LIST",
+ b"LSUB",
+ b"STATUS",
+ b"SEARCH",
+ b"NAMESPACE",
+ )
+ _2_RESPONSES = (b"EXISTS", b"EXPUNGE", b"FETCH", b"RECENT")
+ _OK_RESPONSES = (
+ b"UIDVALIDITY",
+ b"UNSEEN",
+ b"READ-WRITE",
+ b"READ-ONLY",
+ b"UIDNEXT",
+ b"PERMANENTFLAGS",
+ )
+ defer = None
+
+ def __init__(
+ self,
+ command,
+ args=None,
+ wantResponse=(),
+ continuation=None,
+ *contArgs,
+ **contKw,
+ ):
+ self.command = command
+ self.args = args
+ self.wantResponse = wantResponse
+ self.continuation = lambda x: continuation(x, *contArgs, **contKw)
+ self.lines = []
+
+ def __repr__(self) -> str:
+ return "<imap4.Command {!r} {!r} {!r} {!r} {!r}>".format(
+ self.command, self.args, self.wantResponse, self.continuation, self.lines
+ )
+
+ def format(self, tag):
+ if self.args is None:
+ return b" ".join((tag, self.command))
+ return b" ".join((tag, self.command, self.args))
+
+ def finish(self, lastLine, unusedCallback):
+ send = []
+ unuse = []
+ for L in self.lines:
+ names = parseNestedParens(L)
+ N = len(names)
+ if (
+ N >= 1
+ and names[0] in self._1_RESPONSES
+ or N >= 2
+ and names[1] in self._2_RESPONSES
+ or N >= 2
+ and names[0] == b"OK"
+ and isinstance(names[1], list)
+ and names[1][0] in self._OK_RESPONSES
+ ):
+ send.append(names)
+ else:
+ unuse.append(names)
+ d, self.defer = self.defer, None
+ d.callback((send, lastLine))
+ if unuse:
+ unusedCallback(unuse)
+
+
+# Some constants to help define what an atom is and is not - see the grammar
+# section of the IMAP4 RFC - <https://tools.ietf.org/html/rfc3501#section-9>.
+# Some definitions (SP, CTL, DQUOTE) are also from the ABNF RFC -
+# <https://tools.ietf.org/html/rfc2234>.
+_SP = b" "
+_CTL = bytes(chain(range(0x21), range(0x80, 0x100)))
+
+# It is easier to define ATOM-CHAR in terms of what it does not match than in
+# terms of what it does match.
+_nonAtomChars = b']\\\\(){%*"' + _SP + _CTL
+
+# _nonAtomRE is only used in Query, so it uses native strings.
+_nativeNonAtomChars = _nonAtomChars.decode("charmap")
+_nonAtomRE = re.compile("[" + _nativeNonAtomChars + "]")
+
+# This is all the bytes that match the ATOM-CHAR from the grammar in the RFC.
+_atomChars = bytes(ch for ch in range(0x100) if ch not in _nonAtomChars)
+
+
+@implementer(IMailboxListener)
+class IMAP4Server(basic.LineReceiver, policies.TimeoutMixin):
+ """
+ Protocol implementation for an IMAP4rev1 server.
+
+ The server can be in any of four states:
+ - Non-authenticated
+ - Authenticated
+ - Selected
+ - Logout
+ """
+
+ # Identifier for this server software
+ IDENT = b"Twisted IMAP4rev1 Ready"
+
+ # Number of seconds before idle timeout
+ # Initially 1 minute. Raised to 30 minutes after login.
+ timeOut = 60
+
+ POSTAUTH_TIMEOUT = 60 * 30
+
+ # Whether STARTTLS has been issued successfully yet or not.
+ startedTLS = False
+
+ # Whether our transport supports TLS
+ canStartTLS = False
+
+ # Mapping of tags to commands we have received
+ tags = None
+
+ # The object which will handle logins for us
+ portal = None
+
+ # The account object for this connection
+ account = None
+
+ # Logout callback
+ _onLogout = None
+
+ # The currently selected mailbox
+ mbox = None
+
+ # Command data to be processed when literal data is received
+ _pendingLiteral = None
+
+ # Maximum length to accept for a "short" string literal
+ _literalStringLimit = 4096
+
+ # IChallengeResponse factories for AUTHENTICATE command
+ challengers = None
+
+ # Search terms the implementation of which needs to be passed both the last
+ # message identifier (UID) and the last sequence id.
+ _requiresLastMessageInfo = {b"OR", b"NOT", b"UID"}
+
+ state = "unauth"
+
+ parseState = "command"
+
+ def __init__(self, chal=None, contextFactory=None, scheduler=None):
+ if chal is None:
+ chal = {}
+ self.challengers = chal
+ self.ctx = contextFactory
+ if scheduler is None:
+ scheduler = iterateInReactor
+ self._scheduler = scheduler
+ self._queuedAsync = []
+
+ def capabilities(self):
+ cap = {b"AUTH": list(self.challengers.keys())}
+ if self.ctx and self.canStartTLS:
+ if (
+ not self.startedTLS
+ and interfaces.ISSLTransport(self.transport, None) is None
+ ):
+ cap[b"LOGINDISABLED"] = None
+ cap[b"STARTTLS"] = None
+ cap[b"NAMESPACE"] = None
+ cap[b"IDLE"] = None
+ return cap
+
+ def connectionMade(self):
+ self.tags = {}
+ self.canStartTLS = interfaces.ITLSTransport(self.transport, None) is not None
+ self.setTimeout(self.timeOut)
+ self.sendServerGreeting()
+
+ def connectionLost(self, reason):
+ self.setTimeout(None)
+ if self._onLogout:
+ self._onLogout()
+ self._onLogout = None
+
+ def timeoutConnection(self):
+ self.sendLine(b"* BYE Autologout; connection idle too long")
+ self.transport.loseConnection()
+ if self.mbox:
+ self.mbox.removeListener(self)
+ cmbx = ICloseableMailbox(self.mbox, None)
+ if cmbx is not None:
+ maybeDeferred(cmbx.close).addErrback(log.err)
+ self.mbox = None
+ self.state = "timeout"
+
+ def rawDataReceived(self, data):
+ self.resetTimeout()
+ passon = self._pendingLiteral.write(data)
+ if passon is not None:
+ self.setLineMode(passon)
+
+ # Avoid processing commands while buffers are being dumped to
+ # our transport
+ blocked = None
+
+ def _unblock(self):
+ commands = self.blocked
+ self.blocked = None
+ while commands and self.blocked is None:
+ self.lineReceived(commands.pop(0))
+ if self.blocked is not None:
+ self.blocked.extend(commands)
+
+ def lineReceived(self, line):
+ if self.blocked is not None:
+ self.blocked.append(line)
+ return
+
+ self.resetTimeout()
+ f = getattr(self, "parse_" + self.parseState)
+ try:
+ f(line)
+ except Exception as e:
+ self.sendUntaggedResponse(b"BAD Server error: " + networkString(str(e)))
+ log.err()
+
+ def parse_command(self, line):
+ args = line.split(None, 2)
+ rest = None
+ if len(args) == 3:
+ tag, cmd, rest = args
+ elif len(args) == 2:
+ tag, cmd = args
+ elif len(args) == 1:
+ tag = args[0]
+ self.sendBadResponse(tag, b"Missing command")
+ return None
+ else:
+ self.sendBadResponse(None, b"Null command")
+ return None
+
+ cmd = cmd.upper()
+ try:
+ return self.dispatchCommand(tag, cmd, rest)
+ except IllegalClientResponse as e:
+ self.sendBadResponse(tag, b"Illegal syntax: " + networkString(str(e)))
+ except IllegalOperation as e:
+ self.sendNegativeResponse(
+ tag, b"Illegal operation: " + networkString(str(e))
+ )
+ except IllegalMailboxEncoding as e:
+ self.sendNegativeResponse(
+ tag, b"Illegal mailbox name: " + networkString(str(e))
+ )
+
+ def parse_pending(self, line):
+ d = self._pendingLiteral
+ self._pendingLiteral = None
+ self.parseState = "command"
+ d.callback(line)
+
+ def dispatchCommand(self, tag, cmd, rest, uid=None):
+ f = self.lookupCommand(cmd)
+ if f:
+ fn = f[0]
+ parseargs = f[1:]
+ self.__doCommand(tag, fn, [self, tag], parseargs, rest, uid)
+ else:
+ self.sendBadResponse(tag, b"Unsupported command")
+
+ def lookupCommand(self, cmd):
+ return getattr(self, "_".join((self.state, nativeString(cmd.upper()))), None)
+
+ def __doCommand(self, tag, handler, args, parseargs, line, uid):
+ for i, arg in enumerate(parseargs):
+ if callable(arg):
+ parseargs = parseargs[i + 1 :]
+ maybeDeferred(arg, self, line).addCallback(
+ self.__cbDispatch, tag, handler, args, parseargs, uid
+ ).addErrback(self.__ebDispatch, tag)
+ return
+ else:
+ args.append(arg)
+
+ if line:
+ # Too many arguments
+ raise IllegalClientResponse("Too many arguments for command: " + repr(line))
+
+ if uid is not None:
+ handler(uid=uid, *args)
+ else:
+ handler(*args)
+
+ def __cbDispatch(self, result, tag, fn, args, parseargs, uid):
+ (arg, rest) = result
+ args.append(arg)
+ self.__doCommand(tag, fn, args, parseargs, rest, uid)
+
+ def __ebDispatch(self, failure, tag):
+ if failure.check(IllegalClientResponse):
+ self.sendBadResponse(
+ tag, b"Illegal syntax: " + networkString(str(failure.value))
+ )
+ elif failure.check(IllegalOperation):
+ self.sendNegativeResponse(
+ tag, b"Illegal operation: " + networkString(str(failure.value))
+ )
+ elif failure.check(IllegalMailboxEncoding):
+ self.sendNegativeResponse(
+ tag, b"Illegal mailbox name: " + networkString(str(failure.value))
+ )
+ else:
+ self.sendBadResponse(
+ tag, b"Server error: " + networkString(str(failure.value))
+ )
+ log.err(failure)
+
+ def _stringLiteral(self, size):
+ if size > self._literalStringLimit:
+ raise IllegalClientResponse(
+ "Literal too long! I accept at most %d octets"
+ % (self._literalStringLimit,)
+ )
+ d = defer.Deferred()
+ self.parseState = "pending"
+ self._pendingLiteral = LiteralString(size, d)
+ self.sendContinuationRequest(
+ networkString("Ready for %d octets of text" % size)
+ )
+ self.setRawMode()
+ return d
+
+ def _fileLiteral(self, size):
+ d = defer.Deferred()
+ self.parseState = "pending"
+ self._pendingLiteral = LiteralFile(size, d)
+ self.sendContinuationRequest(
+ networkString("Ready for %d octets of data" % size)
+ )
+ self.setRawMode()
+ return d
+
+ def arg_finalastring(self, line):
+ """
+ Parse an astring from line that represents a command's final
+ argument. This special case exists to enable parsing empty
+ string literals.
+
+ @param line: A line that contains a string literal.
+ @type line: L{bytes}
+
+ @return: A 2-tuple containing the parsed argument and any
+ trailing data, or a L{Deferred} that fires with that
+ 2-tuple
+ @rtype: L{tuple} of (L{bytes}, L{bytes}) or a L{Deferred}
+
+ @see: https://twistedmatrix.com/trac/ticket/9207
+ """
+ return self.arg_astring(line, final=True)
+
+ def arg_astring(self, line, final=False):
+ """
+ Parse an astring from the line, return (arg, rest), possibly
+ via a deferred (to handle literals)
+
+ @param line: A line that contains a string literal.
+ @type line: L{bytes}
+
+ @param final: Is this the final argument?
+ @type final L{bool}
+
+ @return: A 2-tuple containing the parsed argument and any
+ trailing data, or a L{Deferred} that fires with that
+ 2-tuple
+ @rtype: L{tuple} of (L{bytes}, L{bytes}) or a L{Deferred}
+
+ """
+ line = line.strip()
+ if not line:
+ raise IllegalClientResponse("Missing argument")
+ d = None
+ arg, rest = None, None
+ if line[0:1] == b'"':
+ try:
+ spam, arg, rest = line.split(b'"', 2)
+ rest = rest[1:] # Strip space
+ except ValueError:
+ raise IllegalClientResponse("Unmatched quotes")
+ elif line[0:1] == b"{":
+ # literal
+ if line[-1:] != b"}":
+ raise IllegalClientResponse("Malformed literal")
+ try:
+ size = int(line[1:-1])
+ except ValueError:
+ raise IllegalClientResponse("Bad literal size: " + repr(line[1:-1]))
+ if final and not size:
+ return (b"", b"")
+ d = self._stringLiteral(size)
+ else:
+ arg = line.split(b" ", 1)
+ if len(arg) == 1:
+ arg.append(b"")
+ arg, rest = arg
+ return d or (arg, rest)
+
+ # ATOM: Any CHAR except ( ) { % * " \ ] CTL SP (CHAR is 7bit)
+ atomre = re.compile(
+ b"(?P<atom>[" + re.escape(_atomChars) + b"]+)( (?P<rest>.*$)|$)"
+ )
+
+ def arg_atom(self, line):
+ """
+ Parse an atom from the line
+ """
+ if not line:
+ raise IllegalClientResponse("Missing argument")
+ m = self.atomre.match(line)
+ if m:
+ return m.group("atom"), m.group("rest")
+ else:
+ raise IllegalClientResponse("Malformed ATOM")
+
+ def arg_plist(self, line):
+ """
+ Parse a (non-nested) parenthesised list from the line
+ """
+ if not line:
+ raise IllegalClientResponse("Missing argument")
+
+ if line[:1] != b"(":
+ raise IllegalClientResponse("Missing parenthesis")
+
+ i = line.find(b")")
+
+ if i == -1:
+ raise IllegalClientResponse("Mismatched parenthesis")
+
+ return (parseNestedParens(line[1:i], 0), line[i + 2 :])
+
+ def arg_literal(self, line):
+ """
+ Parse a literal from the line
+ """
+ if not line:
+ raise IllegalClientResponse("Missing argument")
+
+ if line[:1] != b"{":
+ raise IllegalClientResponse("Missing literal")
+
+ if line[-1:] != b"}":
+ raise IllegalClientResponse("Malformed literal")
+
+ try:
+ size = int(line[1:-1])
+ except ValueError:
+ raise IllegalClientResponse(f"Bad literal size: {line[1:-1]!r}")
+
+ return self._fileLiteral(size)
+
+ def arg_searchkeys(self, line):
+ """
+ searchkeys
+ """
+ query = parseNestedParens(line)
+ # XXX Should really use list of search terms and parse into
+ # a proper tree
+ return (query, b"")
+
+ def arg_seqset(self, line):
+ """
+ sequence-set
+ """
+ rest = b""
+ arg = line.split(b" ", 1)
+ if len(arg) == 2:
+ rest = arg[1]
+ arg = arg[0]
+
+ try:
+ return (parseIdList(arg), rest)
+ except IllegalIdentifierError as e:
+ raise IllegalClientResponse("Bad message number " + str(e))
+
+ def arg_fetchatt(self, line):
+ """
+ fetch-att
+ """
+ p = _FetchParser()
+ p.parseString(line)
+ return (p.result, b"")
+
+ def arg_flaglist(self, line):
+ """
+ Flag part of store-att-flag
+ """
+ flags = []
+ if line[0:1] == b"(":
+ if line[-1:] != b")":
+ raise IllegalClientResponse("Mismatched parenthesis")
+ line = line[1:-1]
+
+ while line:
+ m = self.atomre.search(line)
+ if not m:
+ raise IllegalClientResponse("Malformed flag")
+ if line[0:1] == b"\\" and m.start() == 1:
+ flags.append(b"\\" + m.group("atom"))
+ elif m.start() == 0:
+ flags.append(m.group("atom"))
+ else:
+ raise IllegalClientResponse("Malformed flag")
+ line = m.group("rest")
+
+ return (flags, b"")
+
+ def arg_line(self, line):
+ """
+ Command line of UID command
+ """
+ return (line, b"")
+
+ def opt_plist(self, line):
+ """
+ Optional parenthesised list
+ """
+ if line.startswith(b"("):
+ return self.arg_plist(line)
+ else:
+ return (None, line)
+
+ def opt_datetime(self, line):
+ """
+ Optional date-time string
+ """
+ if line.startswith(b'"'):
+ try:
+ spam, date, rest = line.split(b'"', 2)
+ except ValueError:
+ raise IllegalClientResponse("Malformed date-time")
+ return (date, rest[1:])
+ else:
+ return (None, line)
+
+ def opt_charset(self, line):
+ """
+ Optional charset of SEARCH command
+ """
+ if line[:7].upper() == b"CHARSET":
+ arg = line.split(b" ", 2)
+ if len(arg) == 1:
+ raise IllegalClientResponse("Missing charset identifier")
+ if len(arg) == 2:
+ arg.append(b"")
+ spam, arg, rest = arg
+ return (arg, rest)
+ else:
+ return (None, line)
+
+ def sendServerGreeting(self):
+ msg = b"[CAPABILITY " + b" ".join(self.listCapabilities()) + b"] " + self.IDENT
+ self.sendPositiveResponse(message=msg)
+
+ def sendBadResponse(self, tag=None, message=b""):
+ self._respond(b"BAD", tag, message)
+
+ def sendPositiveResponse(self, tag=None, message=b""):
+ self._respond(b"OK", tag, message)
+
+ def sendNegativeResponse(self, tag=None, message=b""):
+ self._respond(b"NO", tag, message)
+
+ def sendUntaggedResponse(self, message, isAsync=None, **kwargs):
+ isAsync = _get_async_param(isAsync, **kwargs)
+ if not isAsync or (self.blocked is None):
+ self._respond(message, None, None)
+ else:
+ self._queuedAsync.append(message)
+
+ def sendContinuationRequest(self, msg=b"Ready for additional command text"):
+ if msg:
+ self.sendLine(b"+ " + msg)
+ else:
+ self.sendLine(b"+")
+
+ def _respond(self, state, tag, message):
+ if state in (b"OK", b"NO", b"BAD") and self._queuedAsync:
+ lines = self._queuedAsync
+ self._queuedAsync = []
+ for msg in lines:
+ self._respond(msg, None, None)
+ if not tag:
+ tag = b"*"
+ if message:
+ self.sendLine(b" ".join((tag, state, message)))
+ else:
+ self.sendLine(b" ".join((tag, state)))
+
+ def listCapabilities(self):
+ caps = [b"IMAP4rev1"]
+ for c, v in self.capabilities().items():
+ if v is None:
+ caps.append(c)
+ elif len(v):
+ caps.extend([(c + b"=" + cap) for cap in v])
+ return caps
+
+ def do_CAPABILITY(self, tag):
+ self.sendUntaggedResponse(b"CAPABILITY " + b" ".join(self.listCapabilities()))
+ self.sendPositiveResponse(tag, b"CAPABILITY completed")
+
+ unauth_CAPABILITY = (do_CAPABILITY,)
+ auth_CAPABILITY = unauth_CAPABILITY
+ select_CAPABILITY = unauth_CAPABILITY
+ logout_CAPABILITY = unauth_CAPABILITY
+
+ def do_LOGOUT(self, tag):
+ self.sendUntaggedResponse(b"BYE Nice talking to you")
+ self.sendPositiveResponse(tag, b"LOGOUT successful")
+ self.transport.loseConnection()
+
+ unauth_LOGOUT = (do_LOGOUT,)
+ auth_LOGOUT = unauth_LOGOUT
+ select_LOGOUT = unauth_LOGOUT
+ logout_LOGOUT = unauth_LOGOUT
+
+ def do_NOOP(self, tag):
+ self.sendPositiveResponse(tag, b"NOOP No operation performed")
+
+ unauth_NOOP = (do_NOOP,)
+ auth_NOOP = unauth_NOOP
+ select_NOOP = unauth_NOOP
+ logout_NOOP = unauth_NOOP
+
+ def do_AUTHENTICATE(self, tag, args):
+ args = args.upper().strip()
+ if args not in self.challengers:
+ self.sendNegativeResponse(tag, b"AUTHENTICATE method unsupported")
+ else:
+ self.authenticate(self.challengers[args](), tag)
+
+ unauth_AUTHENTICATE = (do_AUTHENTICATE, arg_atom)
+
+ def authenticate(self, chal, tag):
+ if self.portal is None:
+ self.sendNegativeResponse(tag, b"Temporary authentication failure")
+ return
+
+ self._setupChallenge(chal, tag)
+
+ def _setupChallenge(self, chal, tag):
+ try:
+ challenge = chal.getChallenge()
+ except Exception as e:
+ self.sendBadResponse(tag, b"Server error: " + networkString(str(e)))
+ else:
+ coded = encodebytes(challenge)[:-1]
+ self.parseState = "pending"
+ self._pendingLiteral = defer.Deferred()
+ self.sendContinuationRequest(coded)
+ self._pendingLiteral.addCallback(self.__cbAuthChunk, chal, tag)
+ self._pendingLiteral.addErrback(self.__ebAuthChunk, tag)
+
+ def __cbAuthChunk(self, result, chal, tag):
+ try:
+ uncoded = decodebytes(result)
+ except binascii.Error:
+ raise IllegalClientResponse("Malformed Response - not base64")
+
+ chal.setResponse(uncoded)
+ if chal.moreChallenges():
+ self._setupChallenge(chal, tag)
+ else:
+ self.portal.login(chal, None, IAccount).addCallbacks(
+ self.__cbAuthResp, self.__ebAuthResp, (tag,), None, (tag,), None
+ )
+
+ def __cbAuthResp(self, result, tag):
+ (iface, avatar, logout) = result
+ assert iface is IAccount, "IAccount is the only supported interface"
+ self.account = avatar
+ self.state = "auth"
+ self._onLogout = logout
+ self.sendPositiveResponse(tag, b"Authentication successful")
+ self.setTimeout(self.POSTAUTH_TIMEOUT)
+
+ def __ebAuthResp(self, failure, tag):
+ if failure.check(UnauthorizedLogin):
+ self.sendNegativeResponse(tag, b"Authentication failed: unauthorized")
+ elif failure.check(UnhandledCredentials):
+ self.sendNegativeResponse(
+ tag, b"Authentication failed: server misconfigured"
+ )
+ else:
+ self.sendBadResponse(tag, b"Server error: login failed unexpectedly")
+ log.err(failure)
+
+ def __ebAuthChunk(self, failure, tag):
+ self.sendNegativeResponse(
+ tag, b"Authentication failed: " + networkString(str(failure.value))
+ )
+
+ def do_STARTTLS(self, tag):
+ if self.startedTLS:
+ self.sendNegativeResponse(tag, b"TLS already negotiated")
+ elif self.ctx and self.canStartTLS:
+ self.sendPositiveResponse(tag, b"Begin TLS negotiation now")
+ self.transport.startTLS(self.ctx)
+ self.startedTLS = True
+ self.challengers = self.challengers.copy()
+ if b"LOGIN" not in self.challengers:
+ self.challengers[b"LOGIN"] = LOGINCredentials
+ if b"PLAIN" not in self.challengers:
+ self.challengers[b"PLAIN"] = PLAINCredentials
+ else:
+ self.sendNegativeResponse(tag, b"TLS not available")
+
+ unauth_STARTTLS = (do_STARTTLS,)
+
+ def do_LOGIN(self, tag, user, passwd):
+ if b"LOGINDISABLED" in self.capabilities():
+ self.sendBadResponse(tag, b"LOGIN is disabled before STARTTLS")
+ return
+
+ maybeDeferred(self.authenticateLogin, user, passwd).addCallback(
+ self.__cbLogin, tag
+ ).addErrback(self.__ebLogin, tag)
+
+ unauth_LOGIN = (do_LOGIN, arg_astring, arg_finalastring)
+
+ def authenticateLogin(self, user, passwd):
+ """
+ Lookup the account associated with the given parameters
+
+ Override this method to define the desired authentication behavior.
+
+ The default behavior is to defer authentication to C{self.portal}
+ if it is not None, or to deny the login otherwise.
+
+ @type user: L{str}
+ @param user: The username to lookup
+
+ @type passwd: L{str}
+ @param passwd: The password to login with
+ """
+ if self.portal:
+ return self.portal.login(
+ credentials.UsernamePassword(user, passwd), None, IAccount
+ )
+ raise UnauthorizedLogin()
+
+ def __cbLogin(self, result, tag):
+ (iface, avatar, logout) = result
+ if iface is not IAccount:
+ self.sendBadResponse(tag, b"Server error: login returned unexpected value")
+ log.err(f"__cbLogin called with {iface!r}, IAccount expected")
+ else:
+ self.account = avatar
+ self._onLogout = logout
+ self.sendPositiveResponse(tag, b"LOGIN succeeded")
+ self.state = "auth"
+ self.setTimeout(self.POSTAUTH_TIMEOUT)
+
+ def __ebLogin(self, failure, tag):
+ if failure.check(UnauthorizedLogin):
+ self.sendNegativeResponse(tag, b"LOGIN failed")
+ else:
+ self.sendBadResponse(
+ tag, b"Server error: " + networkString(str(failure.value))
+ )
+ log.err(failure)
+
+ def do_NAMESPACE(self, tag):
+ personal = public = shared = None
+ np = INamespacePresenter(self.account, None)
+ if np is not None:
+ personal = np.getPersonalNamespaces()
+ public = np.getSharedNamespaces()
+ shared = np.getSharedNamespaces()
+ self.sendUntaggedResponse(
+ b"NAMESPACE " + collapseNestedLists([personal, public, shared])
+ )
+ self.sendPositiveResponse(tag, b"NAMESPACE command completed")
+
+ auth_NAMESPACE = (do_NAMESPACE,)
+ select_NAMESPACE = auth_NAMESPACE
+
+ def _selectWork(self, tag, name, rw, cmdName):
+ if self.mbox:
+ self.mbox.removeListener(self)
+ cmbx = ICloseableMailbox(self.mbox, None)
+ if cmbx is not None:
+ maybeDeferred(cmbx.close).addErrback(log.err)
+ self.mbox = None
+ self.state = "auth"
+
+ name = _parseMbox(name)
+ maybeDeferred(self.account.select, _parseMbox(name), rw).addCallback(
+ self._cbSelectWork, cmdName, tag
+ ).addErrback(self._ebSelectWork, cmdName, tag)
+
+ def _ebSelectWork(self, failure, cmdName, tag):
+ self.sendBadResponse(tag, cmdName + b" failed: Server error")
+ log.err(failure)
+
+ def _cbSelectWork(self, mbox, cmdName, tag):
+ if mbox is None:
+ self.sendNegativeResponse(tag, b"No such mailbox")
+ return
+ if "\\noselect" in [s.lower() for s in mbox.getFlags()]:
+ self.sendNegativeResponse(tag, "Mailbox cannot be selected")
+ return
+
+ flags = [networkString(flag) for flag in mbox.getFlags()]
+ self.sendUntaggedResponse(b"%d EXISTS" % (mbox.getMessageCount(),))
+ self.sendUntaggedResponse(b"%d RECENT" % (mbox.getRecentCount(),))
+ self.sendUntaggedResponse(b"FLAGS (" + b" ".join(flags) + b")")
+ self.sendPositiveResponse(None, b"[UIDVALIDITY %d]" % (mbox.getUIDValidity(),))
+
+ s = mbox.isWriteable() and b"READ-WRITE" or b"READ-ONLY"
+ mbox.addListener(self)
+ self.sendPositiveResponse(tag, b"[" + s + b"] " + cmdName + b" successful")
+ self.state = "select"
+ self.mbox = mbox
+
+ auth_SELECT = (_selectWork, arg_astring, 1, b"SELECT")
+ select_SELECT = auth_SELECT
+
+ auth_EXAMINE = (_selectWork, arg_astring, 0, b"EXAMINE")
+ select_EXAMINE = auth_EXAMINE
+
+ def do_IDLE(self, tag):
+ self.sendContinuationRequest(None)
+ self.parseTag = tag
+ self.lastState = self.parseState
+ self.parseState = "idle"
+
+ def parse_idle(self, *args):
+ self.parseState = self.lastState
+ del self.lastState
+ self.sendPositiveResponse(self.parseTag, b"IDLE terminated")
+ del self.parseTag
+
+ select_IDLE = (do_IDLE,)
+ auth_IDLE = select_IDLE
+
+ def do_CREATE(self, tag, name):
+ name = _parseMbox(name)
+ try:
+ result = self.account.create(name)
+ except MailboxException as c:
+ self.sendNegativeResponse(tag, networkString(str(c)))
+ except BaseException:
+ self.sendBadResponse(
+ tag, b"Server error encountered while creating mailbox"
+ )
+ log.err()
+ else:
+ if result:
+ self.sendPositiveResponse(tag, b"Mailbox created")
+ else:
+ self.sendNegativeResponse(tag, b"Mailbox not created")
+
+ auth_CREATE = (do_CREATE, arg_finalastring)
+ select_CREATE = auth_CREATE
+
+ def do_DELETE(self, tag, name):
+ name = _parseMbox(name)
+ if name.lower() == "inbox":
+ self.sendNegativeResponse(tag, b"You cannot delete the inbox")
+ return
+ try:
+ self.account.delete(name)
+ except MailboxException as m:
+ self.sendNegativeResponse(tag, str(m).encode("imap4-utf-7"))
+ except BaseException:
+ self.sendBadResponse(
+ tag, b"Server error encountered while deleting mailbox"
+ )
+ log.err()
+ else:
+ self.sendPositiveResponse(tag, b"Mailbox deleted")
+
+ auth_DELETE = (do_DELETE, arg_finalastring)
+ select_DELETE = auth_DELETE
+
+ def do_RENAME(self, tag, oldname, newname):
+ oldname, newname = (_parseMbox(n) for n in (oldname, newname))
+ if oldname.lower() == "inbox" or newname.lower() == "inbox":
+ self.sendNegativeResponse(
+ tag, b"You cannot rename the inbox, or rename another mailbox to inbox."
+ )
+ return
+ try:
+ self.account.rename(oldname, newname)
+ except TypeError:
+ self.sendBadResponse(tag, b"Invalid command syntax")
+ except MailboxException as m:
+ self.sendNegativeResponse(tag, networkString(str(m)))
+ except BaseException:
+ self.sendBadResponse(
+ tag, b"Server error encountered while renaming mailbox"
+ )
+ log.err()
+ else:
+ self.sendPositiveResponse(tag, b"Mailbox renamed")
+
+ auth_RENAME = (do_RENAME, arg_astring, arg_finalastring)
+ select_RENAME = auth_RENAME
+
+ def do_SUBSCRIBE(self, tag, name):
+ name = _parseMbox(name)
+ try:
+ self.account.subscribe(name)
+ except MailboxException as m:
+ self.sendNegativeResponse(tag, networkString(str(m)))
+ except BaseException:
+ self.sendBadResponse(
+ tag, b"Server error encountered while subscribing to mailbox"
+ )
+ log.err()
+ else:
+ self.sendPositiveResponse(tag, b"Subscribed")
+
+ auth_SUBSCRIBE = (do_SUBSCRIBE, arg_finalastring)
+ select_SUBSCRIBE = auth_SUBSCRIBE
+
+ def do_UNSUBSCRIBE(self, tag, name):
+ name = _parseMbox(name)
+ try:
+ self.account.unsubscribe(name)
+ except MailboxException as m:
+ self.sendNegativeResponse(tag, networkString(str(m)))
+ except BaseException:
+ self.sendBadResponse(
+ tag, b"Server error encountered while unsubscribing from mailbox"
+ )
+ log.err()
+ else:
+ self.sendPositiveResponse(tag, b"Unsubscribed")
+
+ auth_UNSUBSCRIBE = (do_UNSUBSCRIBE, arg_finalastring)
+ select_UNSUBSCRIBE = auth_UNSUBSCRIBE
+
+ def _listWork(self, tag, ref, mbox, sub, cmdName):
+ mbox = _parseMbox(mbox)
+ ref = _parseMbox(ref)
+ maybeDeferred(self.account.listMailboxes, ref, mbox).addCallback(
+ self._cbListWork, tag, sub, cmdName
+ ).addErrback(self._ebListWork, tag)
+
+ def _cbListWork(self, mailboxes, tag, sub, cmdName):
+ for name, box in mailboxes:
+ if not sub or self.account.isSubscribed(name):
+ flags = [networkString(flag) for flag in box.getFlags()]
+ delim = box.getHierarchicalDelimiter().encode("imap4-utf-7")
+ resp = (
+ DontQuoteMe(cmdName),
+ map(DontQuoteMe, flags),
+ delim,
+ name.encode("imap4-utf-7"),
+ )
+ self.sendUntaggedResponse(collapseNestedLists(resp))
+ self.sendPositiveResponse(tag, cmdName + b" completed")
+
+ def _ebListWork(self, failure, tag):
+ self.sendBadResponse(tag, b"Server error encountered while listing mailboxes.")
+ log.err(failure)
+
+ auth_LIST = (_listWork, arg_astring, arg_astring, 0, b"LIST")
+ select_LIST = auth_LIST
+
+ auth_LSUB = (_listWork, arg_astring, arg_astring, 1, b"LSUB")
+ select_LSUB = auth_LSUB
+
+ def do_STATUS(self, tag, mailbox, names):
+ nativeNames = []
+ for name in names:
+ nativeNames.append(nativeString(name))
+
+ mailbox = _parseMbox(mailbox)
+
+ maybeDeferred(self.account.select, mailbox, 0).addCallback(
+ self._cbStatusGotMailbox, tag, mailbox, nativeNames
+ ).addErrback(self._ebStatusGotMailbox, tag)
+
+ def _cbStatusGotMailbox(self, mbox, tag, mailbox, names):
+ if mbox:
+ maybeDeferred(mbox.requestStatus, names).addCallbacks(
+ self.__cbStatus,
+ self.__ebStatus,
+ (tag, mailbox),
+ None,
+ (tag, mailbox),
+ None,
+ )
+ else:
+ self.sendNegativeResponse(tag, b"Could not open mailbox")
+
+ def _ebStatusGotMailbox(self, failure, tag):
+ self.sendBadResponse(tag, b"Server error encountered while opening mailbox.")
+ log.err(failure)
+
+ auth_STATUS = (do_STATUS, arg_astring, arg_plist)
+ select_STATUS = auth_STATUS
+
+ def __cbStatus(self, status, tag, box):
+ # STATUS names should only be ASCII
+ line = networkString(" ".join(["%s %s" % x for x in status.items()]))
+ self.sendUntaggedResponse(
+ b"STATUS " + box.encode("imap4-utf-7") + b" (" + line + b")"
+ )
+ self.sendPositiveResponse(tag, b"STATUS complete")
+
+ def __ebStatus(self, failure, tag, box):
+ self.sendBadResponse(
+ tag, b"STATUS " + box + b" failed: " + networkString(str(failure.value))
+ )
+
+ def do_APPEND(self, tag, mailbox, flags, date, message):
+ mailbox = _parseMbox(mailbox)
+ maybeDeferred(self.account.select, mailbox).addCallback(
+ self._cbAppendGotMailbox, tag, flags, date, message
+ ).addErrback(self._ebAppendGotMailbox, tag)
+
+ def _cbAppendGotMailbox(self, mbox, tag, flags, date, message):
+ if not mbox:
+ self.sendNegativeResponse(tag, "[TRYCREATE] No such mailbox")
+ return
+
+ decodedFlags = [nativeString(flag) for flag in flags]
+ d = mbox.addMessage(message, decodedFlags, date)
+ d.addCallback(self.__cbAppend, tag, mbox)
+ d.addErrback(self.__ebAppend, tag)
+
+ def _ebAppendGotMailbox(self, failure, tag):
+ self.sendBadResponse(tag, b"Server error encountered while opening mailbox.")
+ log.err(failure)
+
+ auth_APPEND = (do_APPEND, arg_astring, opt_plist, opt_datetime, arg_literal)
+ select_APPEND = auth_APPEND
+
+ def __cbAppend(self, result, tag, mbox):
+ self.sendUntaggedResponse(b"%d EXISTS" % (mbox.getMessageCount(),))
+ self.sendPositiveResponse(tag, b"APPEND complete")
+
+ def __ebAppend(self, failure, tag):
+ self.sendBadResponse(
+ tag, b"APPEND failed: " + networkString(str(failure.value))
+ )
+
+ def do_CHECK(self, tag):
+ d = self.checkpoint()
+ if d is None:
+ self.__cbCheck(None, tag)
+ else:
+ d.addCallbacks(
+ self.__cbCheck, self.__ebCheck, callbackArgs=(tag,), errbackArgs=(tag,)
+ )
+
+ select_CHECK = (do_CHECK,)
+
+ def __cbCheck(self, result, tag):
+ self.sendPositiveResponse(tag, b"CHECK completed")
+
+ def __ebCheck(self, failure, tag):
+ self.sendBadResponse(tag, b"CHECK failed: " + networkString(str(failure.value)))
+
+ def checkpoint(self):
+ """
+ Called when the client issues a CHECK command.
+
+ This should perform any checkpoint operations required by the server.
+ It may be a long running operation, but may not block. If it returns
+ a deferred, the client will only be informed of success (or failure)
+ when the deferred's callback (or errback) is invoked.
+ """
+ return None
+
+ def do_CLOSE(self, tag):
+ d = None
+ if self.mbox.isWriteable():
+ d = maybeDeferred(self.mbox.expunge)
+ cmbx = ICloseableMailbox(self.mbox, None)
+ if cmbx is not None:
+ if d is not None:
+ d.addCallback(lambda result: cmbx.close())
+ else:
+ d = maybeDeferred(cmbx.close)
+ if d is not None:
+ d.addCallbacks(self.__cbClose, self.__ebClose, (tag,), None, (tag,), None)
+ else:
+ self.__cbClose(None, tag)
+
+ select_CLOSE = (do_CLOSE,)
+
+ def __cbClose(self, result, tag):
+ self.sendPositiveResponse(tag, b"CLOSE completed")
+ self.mbox.removeListener(self)
+ self.mbox = None
+ self.state = "auth"
+
+ def __ebClose(self, failure, tag):
+ self.sendBadResponse(tag, b"CLOSE failed: " + networkString(str(failure.value)))
+
+ def do_EXPUNGE(self, tag):
+ if self.mbox.isWriteable():
+ maybeDeferred(self.mbox.expunge).addCallbacks(
+ self.__cbExpunge, self.__ebExpunge, (tag,), None, (tag,), None
+ )
+ else:
+ self.sendNegativeResponse(tag, b"EXPUNGE ignored on read-only mailbox")
+
+ select_EXPUNGE = (do_EXPUNGE,)
+
+ def __cbExpunge(self, result, tag):
+ for e in result:
+ self.sendUntaggedResponse(b"%d EXPUNGE" % (e,))
+ self.sendPositiveResponse(tag, b"EXPUNGE completed")
+
+ def __ebExpunge(self, failure, tag):
+ self.sendBadResponse(
+ tag, b"EXPUNGE failed: " + networkString(str(failure.value))
+ )
+ log.err(failure)
+
+ def do_SEARCH(self, tag, charset, query, uid=0):
+ sm = ISearchableMailbox(self.mbox, None)
+ if sm is not None:
+ maybeDeferred(sm.search, query, uid=uid).addCallback(
+ self.__cbSearch, tag, self.mbox, uid
+ ).addErrback(self.__ebSearch, tag)
+ else:
+ # that's not the ideal way to get all messages, there should be a
+ # method on mailboxes that gives you all of them
+ s = parseIdList(b"1:*")
+ maybeDeferred(self.mbox.fetch, s, uid=uid).addCallback(
+ self.__cbManualSearch, tag, self.mbox, query, uid
+ ).addErrback(self.__ebSearch, tag)
+
+ select_SEARCH = (do_SEARCH, opt_charset, arg_searchkeys)
+
+ def __cbSearch(self, result, tag, mbox, uid):
+ if uid:
+ result = map(mbox.getUID, result)
+ ids = networkString(" ".join([str(i) for i in result]))
+ self.sendUntaggedResponse(b"SEARCH " + ids)
+ self.sendPositiveResponse(tag, b"SEARCH completed")
+
+ def __cbManualSearch(self, result, tag, mbox, query, uid, searchResults=None):
+ """
+ Apply the search filter to a set of messages. Send the response to the
+ client.
+
+ @type result: L{list} of L{tuple} of (L{int}, provider of
+ L{imap4.IMessage})
+ @param result: A list two tuples of messages with their sequence ids,
+ sorted by the ids in descending order.
+
+ @type tag: L{str}
+ @param tag: A command tag.
+
+ @type mbox: Provider of L{imap4.IMailbox}
+ @param mbox: The searched mailbox.
+
+ @type query: L{list}
+ @param query: A list representing the parsed form of the search query.
+
+ @param uid: A flag indicating whether the search is over message
+ sequence numbers or UIDs.
+
+ @type searchResults: L{list}
+ @param searchResults: The search results so far or L{None} if no
+ results yet.
+ """
+ if searchResults is None:
+ searchResults = []
+ i = 0
+
+ # result is a list of tuples (sequenceId, Message)
+ lastSequenceId = result and result[-1][0]
+ lastMessageId = result and result[-1][1].getUID()
+ for i, (msgId, msg) in list(zip(range(5), result)):
+ # searchFilter and singleSearchStep will mutate the query. Dang.
+ # Copy it here or else things will go poorly for subsequent
+ # messages.
+ if self._searchFilter(
+ copy.deepcopy(query), msgId, msg, lastSequenceId, lastMessageId
+ ):
+ searchResults.append(b"%d" % (msg.getUID() if uid else msgId,))
+
+ if i == 4:
+ from twisted.internet import reactor
+
+ reactor.callLater(
+ 0,
+ self.__cbManualSearch,
+ list(result[5:]),
+ tag,
+ mbox,
+ query,
+ uid,
+ searchResults,
+ )
+ else:
+ if searchResults:
+ self.sendUntaggedResponse(b"SEARCH " + b" ".join(searchResults))
+ self.sendPositiveResponse(tag, b"SEARCH completed")
+
+ def _searchFilter(self, query, id, msg, lastSequenceId, lastMessageId):
+ """
+ Pop search terms from the beginning of C{query} until there are none
+ left and apply them to the given message.
+
+ @param query: A list representing the parsed form of the search query.
+
+ @param id: The sequence number of the message being checked.
+
+ @param msg: The message being checked.
+
+ @type lastSequenceId: L{int}
+ @param lastSequenceId: The highest sequence number of any message in
+ the mailbox being searched.
+
+ @type lastMessageId: L{int}
+ @param lastMessageId: The highest UID of any message in the mailbox
+ being searched.
+
+ @return: Boolean indicating whether all of the query terms match the
+ message.
+ """
+ while query:
+ if not self._singleSearchStep(
+ query, id, msg, lastSequenceId, lastMessageId
+ ):
+ return False
+ return True
+
+ def _singleSearchStep(self, query, msgId, msg, lastSequenceId, lastMessageId):
+ """
+ Pop one search term from the beginning of C{query} (possibly more than
+ one element) and return whether it matches the given message.
+
+ @param query: A list representing the parsed form of the search query.
+
+ @param msgId: The sequence number of the message being checked.
+
+ @param msg: The message being checked.
+
+ @param lastSequenceId: The highest sequence number of any message in
+ the mailbox being searched.
+
+ @param lastMessageId: The highest UID of any message in the mailbox
+ being searched.
+
+ @return: Boolean indicating whether the query term matched the message.
+ """
+
+ q = query.pop(0)
+ if isinstance(q, list):
+ if not self._searchFilter(q, msgId, msg, lastSequenceId, lastMessageId):
+ return False
+ else:
+ c = q.upper()
+ if not c[:1].isalpha():
+ # A search term may be a word like ALL, ANSWERED, BCC, etc (see
+ # below) or it may be a message sequence set. Here we
+ # recognize a message sequence set "N:M".
+ messageSet = parseIdList(c, lastSequenceId)
+ return msgId in messageSet
+ else:
+ f = getattr(self, "search_" + nativeString(c), None)
+ if f is None:
+ raise IllegalQueryError(
+ "Invalid search command %s" % nativeString(c)
+ )
+
+ if c in self._requiresLastMessageInfo:
+ result = f(query, msgId, msg, (lastSequenceId, lastMessageId))
+ else:
+ result = f(query, msgId, msg)
+
+ if not result:
+ return False
+ return True
+
+ def search_ALL(self, query, id, msg):
+ """
+ Returns C{True} if the message matches the ALL search key (always).
+
+ @type query: A L{list} of L{str}
+ @param query: A list representing the parsed query string.
+
+ @type id: L{int}
+ @param id: The sequence number of the message being checked.
+
+ @type msg: Provider of L{imap4.IMessage}
+ """
+ return True
+
+ def search_ANSWERED(self, query, id, msg):
+ """
+ Returns C{True} if the message has been answered.
+
+ @type query: A L{list} of L{str}
+ @param query: A list representing the parsed query string.
+
+ @type id: L{int}
+ @param id: The sequence number of the message being checked.
+
+ @type msg: Provider of L{imap4.IMessage}
+ """
+ return "\\Answered" in msg.getFlags()
+
+ def search_BCC(self, query, id, msg):
+ """
+ Returns C{True} if the message has a BCC address matching the query.
+
+ @type query: A L{list} of L{str}
+ @param query: A list whose first element is a BCC L{str}
+
+ @type id: L{int}
+ @param id: The sequence number of the message being checked.
+
+ @type msg: Provider of L{imap4.IMessage}
+ """
+ bcc = msg.getHeaders(False, "bcc").get("bcc", "")
+ return bcc.lower().find(query.pop(0).lower()) != -1
+
+ def search_BEFORE(self, query, id, msg):
+ date = parseTime(query.pop(0))
+ return email.utils.parsedate(nativeString(msg.getInternalDate())) < date
+
+ def search_BODY(self, query, id, msg):
+ body = query.pop(0).lower()
+ return text.strFile(body, msg.getBodyFile(), False)
+
+ def search_CC(self, query, id, msg):
+ cc = msg.getHeaders(False, "cc").get("cc", "")
+ return cc.lower().find(query.pop(0).lower()) != -1
+
+ def search_DELETED(self, query, id, msg):
+ return "\\Deleted" in msg.getFlags()
+
+ def search_DRAFT(self, query, id, msg):
+ return "\\Draft" in msg.getFlags()
+
+ def search_FLAGGED(self, query, id, msg):
+ return "\\Flagged" in msg.getFlags()
+
+ def search_FROM(self, query, id, msg):
+ fm = msg.getHeaders(False, "from").get("from", "")
+ return fm.lower().find(query.pop(0).lower()) != -1
+
+ def search_HEADER(self, query, id, msg):
+ hdr = query.pop(0).lower()
+ hdr = msg.getHeaders(False, hdr).get(hdr, "")
+ return hdr.lower().find(query.pop(0).lower()) != -1
+
+ def search_KEYWORD(self, query, id, msg):
+ query.pop(0)
+ return False
+
+ def search_LARGER(self, query, id, msg):
+ return int(query.pop(0)) < msg.getSize()
+
+ def search_NEW(self, query, id, msg):
+ return "\\Recent" in msg.getFlags() and "\\Seen" not in msg.getFlags()
+
+ def search_NOT(self, query, id, msg, lastIDs):
+ """
+ Returns C{True} if the message does not match the query.
+
+ @type query: A L{list} of L{str}
+ @param query: A list representing the parsed form of the search query.
+
+ @type id: L{int}
+ @param id: The sequence number of the message being checked.
+
+ @type msg: Provider of L{imap4.IMessage}
+ @param msg: The message being checked.
+
+ @type lastIDs: L{tuple}
+ @param lastIDs: A tuple of (last sequence id, last message id).
+ The I{last sequence id} is an L{int} containing the highest sequence
+ number of a message in the mailbox. The I{last message id} is an
+ L{int} containing the highest UID of a message in the mailbox.
+ """
+ (lastSequenceId, lastMessageId) = lastIDs
+ return not self._singleSearchStep(query, id, msg, lastSequenceId, lastMessageId)
+
+ def search_OLD(self, query, id, msg):
+ return "\\Recent" not in msg.getFlags()
+
+ def search_ON(self, query, id, msg):
+ date = parseTime(query.pop(0))
+ return email.utils.parsedate(msg.getInternalDate()) == date
+
+ def search_OR(self, query, id, msg, lastIDs):
+ """
+ Returns C{True} if the message matches any of the first two query
+ items.
+
+ @type query: A L{list} of L{str}
+ @param query: A list representing the parsed form of the search query.
+
+ @type id: L{int}
+ @param id: The sequence number of the message being checked.
+
+ @type msg: Provider of L{imap4.IMessage}
+ @param msg: The message being checked.
+
+ @type lastIDs: L{tuple}
+ @param lastIDs: A tuple of (last sequence id, last message id).
+ The I{last sequence id} is an L{int} containing the highest sequence
+ number of a message in the mailbox. The I{last message id} is an
+ L{int} containing the highest UID of a message in the mailbox.
+ """
+ (lastSequenceId, lastMessageId) = lastIDs
+ a = self._singleSearchStep(query, id, msg, lastSequenceId, lastMessageId)
+ b = self._singleSearchStep(query, id, msg, lastSequenceId, lastMessageId)
+ return a or b
+
+ def search_RECENT(self, query, id, msg):
+ return "\\Recent" in msg.getFlags()
+
+ def search_SEEN(self, query, id, msg):
+ return "\\Seen" in msg.getFlags()
+
+ def search_SENTBEFORE(self, query, id, msg):
+ """
+ Returns C{True} if the message date is earlier than the query date.
+
+ @type query: A L{list} of L{str}
+ @param query: A list whose first element starts with a stringified date
+ that is a fragment of an L{imap4.Query()}. The date must be in the
+ format 'DD-Mon-YYYY', for example '03-March-2003' or '03-Mar-2003'.
+
+ @type id: L{int}
+ @param id: The sequence number of the message being checked.
+
+ @type msg: Provider of L{imap4.IMessage}
+ """
+ date = msg.getHeaders(False, "date").get("date", "")
+ date = email.utils.parsedate(date)
+ return date < parseTime(query.pop(0))
+
+ def search_SENTON(self, query, id, msg):
+ """
+ Returns C{True} if the message date is the same as the query date.
+
+ @type query: A L{list} of L{str}
+ @param query: A list whose first element starts with a stringified date
+ that is a fragment of an L{imap4.Query()}. The date must be in the
+ format 'DD-Mon-YYYY', for example '03-March-2003' or '03-Mar-2003'.
+
+ @type msg: Provider of L{imap4.IMessage}
+ """
+ date = msg.getHeaders(False, "date").get("date", "")
+ date = email.utils.parsedate(date)
+ return date[:3] == parseTime(query.pop(0))[:3]
+
+ def search_SENTSINCE(self, query, id, msg):
+ """
+ Returns C{True} if the message date is later than the query date.
+
+ @type query: A L{list} of L{str}
+ @param query: A list whose first element starts with a stringified date
+ that is a fragment of an L{imap4.Query()}. The date must be in the
+ format 'DD-Mon-YYYY', for example '03-March-2003' or '03-Mar-2003'.
+
+ @type msg: Provider of L{imap4.IMessage}
+ """
+ date = msg.getHeaders(False, "date").get("date", "")
+ date = email.utils.parsedate(date)
+ return date > parseTime(query.pop(0))
+
+ def search_SINCE(self, query, id, msg):
+ date = parseTime(query.pop(0))
+ return email.utils.parsedate(msg.getInternalDate()) > date
+
+ def search_SMALLER(self, query, id, msg):
+ return int(query.pop(0)) > msg.getSize()
+
+ def search_SUBJECT(self, query, id, msg):
+ subj = msg.getHeaders(False, "subject").get("subject", "")
+ return subj.lower().find(query.pop(0).lower()) != -1
+
+ def search_TEXT(self, query, id, msg):
+ # XXX - This must search headers too
+ body = query.pop(0).lower()
+ return text.strFile(body, msg.getBodyFile(), False)
+
+ def search_TO(self, query, id, msg):
+ to = msg.getHeaders(False, "to").get("to", "")
+ return to.lower().find(query.pop(0).lower()) != -1
+
+ def search_UID(self, query, id, msg, lastIDs):
+ """
+ Returns C{True} if the message UID is in the range defined by the
+ search query.
+
+ @type query: A L{list} of L{bytes}
+ @param query: A list representing the parsed form of the search
+ query. Its first element should be a L{str} that can be interpreted
+ as a sequence range, for example '2:4,5:*'.
+
+ @type id: L{int}
+ @param id: The sequence number of the message being checked.
+
+ @type msg: Provider of L{imap4.IMessage}
+ @param msg: The message being checked.
+
+ @type lastIDs: L{tuple}
+ @param lastIDs: A tuple of (last sequence id, last message id).
+ The I{last sequence id} is an L{int} containing the highest sequence
+ number of a message in the mailbox. The I{last message id} is an
+ L{int} containing the highest UID of a message in the mailbox.
+ """
+ (lastSequenceId, lastMessageId) = lastIDs
+ c = query.pop(0)
+ m = parseIdList(c, lastMessageId)
+ return msg.getUID() in m
+
+ def search_UNANSWERED(self, query, id, msg):
+ return "\\Answered" not in msg.getFlags()
+
+ def search_UNDELETED(self, query, id, msg):
+ return "\\Deleted" not in msg.getFlags()
+
+ def search_UNDRAFT(self, query, id, msg):
+ return "\\Draft" not in msg.getFlags()
+
+ def search_UNFLAGGED(self, query, id, msg):
+ return "\\Flagged" not in msg.getFlags()
+
+ def search_UNKEYWORD(self, query, id, msg):
+ query.pop(0)
+ return False
+
+ def search_UNSEEN(self, query, id, msg):
+ return "\\Seen" not in msg.getFlags()
+
+ def __ebSearch(self, failure, tag):
+ self.sendBadResponse(
+ tag, b"SEARCH failed: " + networkString(str(failure.value))
+ )
+ log.err(failure)
+
+ def do_FETCH(self, tag, messages, query, uid=0):
+ if query:
+ self._oldTimeout = self.setTimeout(None)
+ maybeDeferred(self.mbox.fetch, messages, uid=uid).addCallback(
+ iter
+ ).addCallback(self.__cbFetch, tag, query, uid).addErrback(
+ self.__ebFetch, tag
+ )
+ else:
+ self.sendPositiveResponse(tag, b"FETCH complete")
+
+ select_FETCH = (do_FETCH, arg_seqset, arg_fetchatt)
+
+ def __cbFetch(self, results, tag, query, uid):
+ if self.blocked is None:
+ self.blocked = []
+ try:
+ id, msg = next(results)
+ except StopIteration:
+ # The idle timeout was suspended while we delivered results,
+ # restore it now.
+ self.setTimeout(self._oldTimeout)
+ del self._oldTimeout
+
+ # All results have been processed, deliver completion notification.
+
+ # It's important to run this *after* resetting the timeout to "rig
+ # a race" in some test code. writing to the transport will
+ # synchronously call test code, which synchronously loses the
+ # connection, calling our connectionLost method, which cancels the
+ # timeout. We want to make sure that timeout is cancelled *after*
+ # we reset it above, so that the final state is no timed
+ # calls. This avoids reactor uncleanliness errors in the test
+ # suite.
+ # XXX: Perhaps loopback should be fixed to not call the user code
+ # synchronously in transport.write?
+ self.sendPositiveResponse(tag, b"FETCH completed")
+
+ # Instance state is now consistent again (ie, it is as though
+ # the fetch command never ran), so allow any pending blocked
+ # commands to execute.
+ self._unblock()
+ else:
+ self.spewMessage(id, msg, query, uid).addCallback(
+ lambda _: self.__cbFetch(results, tag, query, uid)
+ ).addErrback(self.__ebSpewMessage)
+
+ def __ebSpewMessage(self, failure):
+ # This indicates a programming error.
+ # There's no reliable way to indicate anything to the client, since we
+ # may have already written an arbitrary amount of data in response to
+ # the command.
+ log.err(failure)
+ self.transport.loseConnection()
+
+ def spew_envelope(self, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.write
+ _w(b"ENVELOPE " + collapseNestedLists([getEnvelope(msg)]))
+
+ def spew_flags(self, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.writen
+ encodedFlags = [networkString(flag) for flag in msg.getFlags()]
+ _w(b"FLAGS " + b"(" + b" ".join(encodedFlags) + b")")
+
+ def spew_internaldate(self, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.write
+ idate = msg.getInternalDate()
+ ttup = email.utils.parsedate_tz(nativeString(idate))
+ if ttup is None:
+ log.msg("%d:%r: unpareseable internaldate: %r" % (id, msg, idate))
+ raise IMAP4Exception("Internal failure generating INTERNALDATE")
+
+ # need to specify the month manually, as strftime depends on locale
+ strdate = time.strftime("%d-%%s-%Y %H:%M:%S ", ttup[:9])
+ odate = networkString(strdate % (_MONTH_NAMES[ttup[1]],))
+ if ttup[9] is None:
+ odate = odate + b"+0000"
+ else:
+ if ttup[9] >= 0:
+ sign = b"+"
+ else:
+ sign = b"-"
+ odate = (
+ odate
+ + sign
+ + b"%04d"
+ % ((abs(ttup[9]) // 3600) * 100 + (abs(ttup[9]) % 3600) // 60,)
+ )
+ _w(b"INTERNALDATE " + _quote(odate))
+
+ def spew_rfc822header(self, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.write
+ hdrs = _formatHeaders(msg.getHeaders(True))
+ _w(b"RFC822.HEADER " + _literal(hdrs))
+
+ def spew_rfc822text(self, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.write
+ _w(b"RFC822.TEXT ")
+ _f()
+ return FileProducer(msg.getBodyFile()).beginProducing(self.transport)
+
+ def spew_rfc822size(self, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.write
+ _w(b"RFC822.SIZE %d" % (msg.getSize(),))
+
+ def spew_rfc822(self, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.write
+ _w(b"RFC822 ")
+ _f()
+ mf = IMessageFile(msg, None)
+ if mf is not None:
+ return FileProducer(mf.open()).beginProducing(self.transport)
+ return MessageProducer(msg, None, self._scheduler).beginProducing(
+ self.transport
+ )
+
+ def spew_uid(self, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.write
+ _w(b"UID %d" % (msg.getUID(),))
+
+ def spew_bodystructure(self, id, msg, _w=None, _f=None):
+ _w(b"BODYSTRUCTURE " + collapseNestedLists([getBodyStructure(msg, True)]))
+
+ def spew_body(self, part, id, msg, _w=None, _f=None):
+ if _w is None:
+ _w = self.transport.write
+ for p in part.part:
+ if msg.isMultipart():
+ msg = msg.getSubPart(p)
+ elif p > 0:
+ # Non-multipart messages have an implicit first part but no
+ # other parts - reject any request for any other part.
+ raise TypeError("Requested subpart of non-multipart message")
+
+ if part.header:
+ hdrs = msg.getHeaders(part.header.negate, *part.header.fields)
+ hdrs = _formatHeaders(hdrs)
+ _w(part.__bytes__() + b" " + _literal(hdrs))
+ elif part.text:
+ _w(part.__bytes__() + b" ")
+ _f()
+ return FileProducer(msg.getBodyFile()).beginProducing(self.transport)
+ elif part.mime:
+ hdrs = _formatHeaders(msg.getHeaders(True))
+ _w(part.__bytes__() + b" " + _literal(hdrs))
+ elif part.empty:
+ _w(part.__bytes__() + b" ")
+ _f()
+ if part.part:
+ return FileProducer(msg.getBodyFile()).beginProducing(self.transport)
+ else:
+ mf = IMessageFile(msg, None)
+ if mf is not None:
+ return FileProducer(mf.open()).beginProducing(self.transport)
+ return MessageProducer(msg, None, self._scheduler).beginProducing(
+ self.transport
+ )
+
+ else:
+ _w(b"BODY " + collapseNestedLists([getBodyStructure(msg)]))
+
+ def spewMessage(self, id, msg, query, uid):
+ wbuf = WriteBuffer(self.transport)
+ write = wbuf.write
+ flush = wbuf.flush
+
+ def start():
+ write(b"* %d FETCH (" % (id,))
+
+ def finish():
+ write(b")\r\n")
+
+ def space():
+ write(b" ")
+
+ def spew():
+ seenUID = False
+ start()
+ for part in query:
+ if part.type == "uid":
+ seenUID = True
+ if part.type == "body":
+ yield self.spew_body(part, id, msg, write, flush)
+ else:
+ f = getattr(self, "spew_" + part.type)
+ yield f(id, msg, write, flush)
+ if part is not query[-1]:
+ space()
+ if uid and not seenUID:
+ space()
+ yield self.spew_uid(id, msg, write, flush)
+ finish()
+ flush()
+
+ return self._scheduler(spew())
+
+ def __ebFetch(self, failure, tag):
+ self.setTimeout(self._oldTimeout)
+ del self._oldTimeout
+ log.err(failure)
+ self.sendBadResponse(tag, b"FETCH failed: " + networkString(str(failure.value)))
+
+ def do_STORE(self, tag, messages, mode, flags, uid=0):
+ mode = mode.upper()
+ silent = mode.endswith(b"SILENT")
+ if mode.startswith(b"+"):
+ mode = 1
+ elif mode.startswith(b"-"):
+ mode = -1
+ else:
+ mode = 0
+
+ flags = [nativeString(flag) for flag in flags]
+ maybeDeferred(self.mbox.store, messages, flags, mode, uid=uid).addCallbacks(
+ self.__cbStore,
+ self.__ebStore,
+ (tag, self.mbox, uid, silent),
+ None,
+ (tag,),
+ None,
+ )
+
+ select_STORE = (do_STORE, arg_seqset, arg_atom, arg_flaglist)
+
+ def __cbStore(self, result, tag, mbox, uid, silent):
+ if result and not silent:
+ for k, v in result.items():
+ if uid:
+ uidstr = b" UID %d" % (mbox.getUID(k),)
+ else:
+ uidstr = b""
+
+ flags = [networkString(flag) for flag in v]
+ self.sendUntaggedResponse(
+ b"%d FETCH (FLAGS (%b)%b)" % (k, b" ".join(flags), uidstr)
+ )
+ self.sendPositiveResponse(tag, b"STORE completed")
+
+ def __ebStore(self, failure, tag):
+ self.sendBadResponse(tag, b"Server error: " + networkString(str(failure.value)))
+
+ def do_COPY(self, tag, messages, mailbox, uid=0):
+ mailbox = _parseMbox(mailbox)
+ maybeDeferred(self.account.select, mailbox).addCallback(
+ self._cbCopySelectedMailbox, tag, messages, mailbox, uid
+ ).addErrback(self._ebCopySelectedMailbox, tag)
+
+ select_COPY = (do_COPY, arg_seqset, arg_finalastring)
+
+ def _cbCopySelectedMailbox(self, mbox, tag, messages, mailbox, uid):
+ if not mbox:
+ self.sendNegativeResponse(tag, "No such mailbox: " + mailbox)
+ else:
+ maybeDeferred(self.mbox.fetch, messages, uid).addCallback(
+ self.__cbCopy, tag, mbox
+ ).addCallback(self.__cbCopied, tag, mbox).addErrback(self.__ebCopy, tag)
+
+ def _ebCopySelectedMailbox(self, failure, tag):
+ self.sendBadResponse(tag, b"Server error: " + networkString(str(failure.value)))
+
+ def __cbCopy(self, messages, tag, mbox):
+ # XXX - This should handle failures with a rollback or something
+ addedDeferreds = []
+
+ fastCopyMbox = IMessageCopier(mbox, None)
+ for id, msg in messages:
+ if fastCopyMbox is not None:
+ d = maybeDeferred(fastCopyMbox.copy, msg)
+ addedDeferreds.append(d)
+ continue
+
+ # XXX - The following should be an implementation of IMessageCopier.copy
+ # on an IMailbox->IMessageCopier adapter.
+
+ flags = msg.getFlags()
+ date = msg.getInternalDate()
+
+ body = IMessageFile(msg, None)
+ if body is not None:
+ bodyFile = body.open()
+ d = maybeDeferred(mbox.addMessage, bodyFile, flags, date)
+ else:
+
+ def rewind(f):
+ f.seek(0)
+ return f
+
+ buffer = tempfile.TemporaryFile()
+ d = (
+ MessageProducer(msg, buffer, self._scheduler)
+ .beginProducing(None)
+ .addCallback(
+ lambda _, b=buffer, f=flags, d=date: mbox.addMessage(
+ rewind(b), f, d
+ )
+ )
+ )
+ addedDeferreds.append(d)
+ return defer.DeferredList(addedDeferreds)
+
+ def __cbCopied(self, deferredIds, tag, mbox):
+ ids = []
+ failures = []
+ for status, result in deferredIds:
+ if status:
+ ids.append(result)
+ else:
+ failures.append(result.value)
+ if failures:
+ self.sendNegativeResponse(tag, "[ALERT] Some messages were not copied")
+ else:
+ self.sendPositiveResponse(tag, b"COPY completed")
+
+ def __ebCopy(self, failure, tag):
+ self.sendBadResponse(tag, b"COPY failed:" + networkString(str(failure.value)))
+ log.err(failure)
+
+ def do_UID(self, tag, command, line):
+ command = command.upper()
+
+ if command not in (b"COPY", b"FETCH", b"STORE", b"SEARCH"):
+ raise IllegalClientResponse(command)
+
+ self.dispatchCommand(tag, command, line, uid=1)
+
+ select_UID = (do_UID, arg_atom, arg_line)
+
+ #
+ # IMailboxListener implementation
+ #
+ def modeChanged(self, writeable):
+ if writeable:
+ self.sendUntaggedResponse(message=b"[READ-WRITE]", isAsync=True)
+ else:
+ self.sendUntaggedResponse(message=b"[READ-ONLY]", isAsync=True)
+
+ def flagsChanged(self, newFlags):
+ for mId, flags in newFlags.items():
+ encodedFlags = [networkString(flag) for flag in flags]
+ msg = b"%d FETCH (FLAGS (%b))" % (mId, b" ".join(encodedFlags))
+ self.sendUntaggedResponse(msg, isAsync=True)
+
+ def newMessages(self, exists, recent):
+ if exists is not None:
+ self.sendUntaggedResponse(b"%d EXISTS" % (exists,), isAsync=True)
+ if recent is not None:
+ self.sendUntaggedResponse(b"%d RECENT" % (recent,), isAsync=True)
+
+
+TIMEOUT_ERROR = error.TimeoutError()
+
+
+@implementer(IMailboxListener)
+class IMAP4Client(basic.LineReceiver, policies.TimeoutMixin):
+ """IMAP4 client protocol implementation
+
+ @ivar state: A string representing the state the connection is currently
+ in.
+ """
+
+ tags = None
+ waiting = None
+ queued = None
+ tagID = 1
+ state = None
+
+ startedTLS = False
+
+ # Number of seconds to wait before timing out a connection.
+ # If the number is <= 0 no timeout checking will be performed.
+ timeout = 0
+
+ # Capabilities are not allowed to change during the session
+ # So cache the first response and use that for all later
+ # lookups
+ _capCache = None
+
+ _memoryFileLimit = 1024 * 1024 * 10
+
+ # Authentication is pluggable. This maps names to IClientAuthentication
+ # objects.
+ authenticators = None
+
+ STATUS_CODES = ("OK", "NO", "BAD", "PREAUTH", "BYE")
+
+ STATUS_TRANSFORMATIONS = {"MESSAGES": int, "RECENT": int, "UNSEEN": int}
+
+ context = None
+
+ def __init__(self, contextFactory=None):
+ self.tags = {}
+ self.queued = []
+ self.authenticators = {}
+ self.context = contextFactory
+
+ self._tag = None
+ self._parts = None
+ self._lastCmd = None
+
+ def registerAuthenticator(self, auth):
+ """
+ Register a new form of authentication
+
+ When invoking the authenticate() method of IMAP4Client, the first
+ matching authentication scheme found will be used. The ordering is
+ that in which the server lists support authentication schemes.
+
+ @type auth: Implementor of C{IClientAuthentication}
+ @param auth: The object to use to perform the client
+ side of this authentication scheme.
+ """
+ self.authenticators[auth.getName().upper()] = auth
+
+ def rawDataReceived(self, data):
+ if self.timeout > 0:
+ self.resetTimeout()
+
+ self._pendingSize -= len(data)
+ if self._pendingSize > 0:
+ self._pendingBuffer.write(data)
+ else:
+ passon = b""
+ if self._pendingSize < 0:
+ data, passon = data[: self._pendingSize], data[self._pendingSize :]
+ self._pendingBuffer.write(data)
+ rest = self._pendingBuffer
+ self._pendingBuffer = None
+ self._pendingSize = None
+ rest.seek(0, 0)
+ self._parts.append(rest.read())
+ self.setLineMode(passon.lstrip(b"\r\n"))
+
+ # def sendLine(self, line):
+ # print 'S:', repr(line)
+ # return basic.LineReceiver.sendLine(self, line)
+
+ def _setupForLiteral(self, rest, octets):
+ self._pendingBuffer = self.messageFile(octets)
+ self._pendingSize = octets
+ if self._parts is None:
+ self._parts = [rest, b"\r\n"]
+ else:
+ self._parts.extend([rest, b"\r\n"])
+ self.setRawMode()
+
+ def connectionMade(self):
+ if self.timeout > 0:
+ self.setTimeout(self.timeout)
+
+ def connectionLost(self, reason):
+ """
+ We are no longer connected
+ """
+ if self.timeout > 0:
+ self.setTimeout(None)
+ if self.queued is not None:
+ queued = self.queued
+ self.queued = None
+ for cmd in queued:
+ cmd.defer.errback(reason)
+ if self.tags is not None:
+ tags = self.tags
+ self.tags = None
+ for cmd in tags.values():
+ if cmd is not None and cmd.defer is not None:
+ cmd.defer.errback(reason)
+
+ def lineReceived(self, line):
+ """
+ Attempt to parse a single line from the server.
+
+ @type line: L{bytes}
+ @param line: The line from the server, without the line delimiter.
+
+ @raise IllegalServerResponse: If the line or some part of the line
+ does not represent an allowed message from the server at this time.
+ """
+ # print('C: ' + repr(line))
+ if self.timeout > 0:
+ self.resetTimeout()
+
+ lastPart = line.rfind(b"{")
+ if lastPart != -1:
+ lastPart = line[lastPart + 1 :]
+ if lastPart.endswith(b"}"):
+ # It's a literal a-comin' in
+ try:
+ octets = int(lastPart[:-1])
+ except ValueError:
+ raise IllegalServerResponse(line)
+ if self._parts is None:
+ self._tag, parts = line.split(None, 1)
+ else:
+ parts = line
+ self._setupForLiteral(parts, octets)
+ return
+
+ if self._parts is None:
+ # It isn't a literal at all
+ self._regularDispatch(line)
+ else:
+ # If an expression is in progress, no tag is required here
+ # Since we didn't find a literal indicator, this expression
+ # is done.
+ self._parts.append(line)
+ tag, rest = self._tag, b"".join(self._parts)
+ self._tag = self._parts = None
+ self.dispatchCommand(tag, rest)
+
+ def timeoutConnection(self):
+ if self._lastCmd and self._lastCmd.defer is not None:
+ d, self._lastCmd.defer = self._lastCmd.defer, None
+ d.errback(TIMEOUT_ERROR)
+
+ if self.queued:
+ for cmd in self.queued:
+ if cmd.defer is not None:
+ d, cmd.defer = cmd.defer, d
+ d.errback(TIMEOUT_ERROR)
+
+ self.transport.loseConnection()
+
+ def _regularDispatch(self, line):
+ parts = line.split(None, 1)
+ if len(parts) != 2:
+ parts.append(b"")
+ tag, rest = parts
+ self.dispatchCommand(tag, rest)
+
+ def messageFile(self, octets):
+ """
+ Create a file to which an incoming message may be written.
+
+ @type octets: L{int}
+ @param octets: The number of octets which will be written to the file
+
+ @rtype: Any object which implements C{write(string)} and
+ C{seek(int, int)}
+ @return: A file-like object
+ """
+ if octets > self._memoryFileLimit:
+ return tempfile.TemporaryFile()
+ else:
+ return BytesIO()
+
+ def makeTag(self):
+ tag = ("%0.4X" % self.tagID).encode("ascii")
+ self.tagID += 1
+ return tag
+
+ def dispatchCommand(self, tag, rest):
+ if self.state is None:
+ f = self.response_UNAUTH
+ else:
+ f = getattr(self, "response_" + self.state.upper(), None)
+ if f:
+ try:
+ f(tag, rest)
+ except BaseException:
+ log.err()
+ self.transport.loseConnection()
+ else:
+ log.err(f"Cannot dispatch: {self.state}, {tag!r}, {rest!r}")
+ self.transport.loseConnection()
+
+ def response_UNAUTH(self, tag, rest):
+ if self.state is None:
+ # Server greeting, this is
+ status, rest = rest.split(None, 1)
+ if status.upper() == b"OK":
+ self.state = "unauth"
+ elif status.upper() == b"PREAUTH":
+ self.state = "auth"
+ else:
+ # XXX - This is rude.
+ self.transport.loseConnection()
+ raise IllegalServerResponse(tag + b" " + rest)
+
+ b, e = rest.find(b"["), rest.find(b"]")
+ if b != -1 and e != -1:
+ self.serverGreeting(
+ self.__cbCapabilities(([parseNestedParens(rest[b + 1 : e])], None))
+ )
+ else:
+ self.serverGreeting(None)
+ else:
+ self._defaultHandler(tag, rest)
+
+ def response_AUTH(self, tag, rest):
+ self._defaultHandler(tag, rest)
+
+ def _defaultHandler(self, tag, rest):
+ if tag == b"*" or tag == b"+":
+ if not self.waiting:
+ self._extraInfo([parseNestedParens(rest)])
+ else:
+ cmd = self.tags[self.waiting]
+ if tag == b"+":
+ cmd.continuation(rest)
+ else:
+ cmd.lines.append(rest)
+ else:
+ try:
+ cmd = self.tags[tag]
+ except KeyError:
+ # XXX - This is rude.
+ self.transport.loseConnection()
+ raise IllegalServerResponse(tag + b" " + rest)
+ else:
+ status, line = rest.split(None, 1)
+ if status == b"OK":
+ # Give them this last line, too
+ cmd.finish(rest, self._extraInfo)
+ else:
+ cmd.defer.errback(IMAP4Exception(line))
+ del self.tags[tag]
+ self.waiting = None
+ self._flushQueue()
+
+ def _flushQueue(self):
+ if self.queued:
+ cmd = self.queued.pop(0)
+ t = self.makeTag()
+ self.tags[t] = cmd
+ self.sendLine(cmd.format(t))
+ self.waiting = t
+
+ def _extraInfo(self, lines):
+ # XXX - This is terrible.
+ # XXX - Also, this should collapse temporally proximate calls into single
+ # invocations of IMailboxListener methods, where possible.
+ flags = {}
+ recent = exists = None
+ for response in lines:
+ elements = len(response)
+ if elements == 1 and response[0] == [b"READ-ONLY"]:
+ self.modeChanged(False)
+ elif elements == 1 and response[0] == [b"READ-WRITE"]:
+ self.modeChanged(True)
+ elif elements == 2 and response[1] == b"EXISTS":
+ exists = int(response[0])
+ elif elements == 2 and response[1] == b"RECENT":
+ recent = int(response[0])
+ elif elements == 3 and response[1] == b"FETCH":
+ mId = int(response[0])
+ values, _ = self._parseFetchPairs(response[2])
+ flags.setdefault(mId, []).extend(values.get("FLAGS", ()))
+ else:
+ log.msg(f"Unhandled unsolicited response: {response}")
+
+ if flags:
+ self.flagsChanged(flags)
+ if recent is not None or exists is not None:
+ self.newMessages(exists, recent)
+
+ def sendCommand(self, cmd):
+ cmd.defer = defer.Deferred()
+ if self.waiting:
+ self.queued.append(cmd)
+ return cmd.defer
+ t = self.makeTag()
+ self.tags[t] = cmd
+ self.sendLine(cmd.format(t))
+ self.waiting = t
+ self._lastCmd = cmd
+ return cmd.defer
+
+ def getCapabilities(self, useCache=1):
+ """
+ Request the capabilities available on this server.
+
+ This command is allowed in any state of connection.
+
+ @type useCache: C{bool}
+ @param useCache: Specify whether to use the capability-cache or to
+ re-retrieve the capabilities from the server. Server capabilities
+ should never change, so for normal use, this flag should never be
+ false.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback will be invoked with a
+ dictionary mapping capability types to lists of supported
+ mechanisms, or to None if a support list is not applicable.
+ """
+ if useCache and self._capCache is not None:
+ return defer.succeed(self._capCache)
+ cmd = b"CAPABILITY"
+ resp = (b"CAPABILITY",)
+ d = self.sendCommand(Command(cmd, wantResponse=resp))
+ d.addCallback(self.__cbCapabilities)
+ return d
+
+ def __cbCapabilities(self, result):
+ (lines, tagline) = result
+ caps = {}
+ for rest in lines:
+ for cap in rest[1:]:
+ parts = cap.split(b"=", 1)
+ if len(parts) == 1:
+ category, value = parts[0], None
+ else:
+ category, value = parts
+ caps.setdefault(category, []).append(value)
+
+ # Preserve a non-ideal API for backwards compatibility. It would
+ # probably be entirely sensible to have an object with a wider API than
+ # dict here so this could be presented less insanely.
+ for category in caps:
+ if caps[category] == [None]:
+ caps[category] = None
+ self._capCache = caps
+ return caps
+
+ def logout(self):
+ """
+ Inform the server that we are done with the connection.
+
+ This command is allowed in any state of connection.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback will be invoked with None
+ when the proper server acknowledgement has been received.
+ """
+ d = self.sendCommand(Command(b"LOGOUT", wantResponse=(b"BYE",)))
+ d.addCallback(self.__cbLogout)
+ return d
+
+ def __cbLogout(self, result):
+ (lines, tagline) = result
+ self.transport.loseConnection()
+ # We don't particularly care what the server said
+ return None
+
+ def noop(self):
+ """
+ Perform no operation.
+
+ This command is allowed in any state of connection.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback will be invoked with a list
+ of untagged status updates the server responds with.
+ """
+ d = self.sendCommand(Command(b"NOOP"))
+ d.addCallback(self.__cbNoop)
+ return d
+
+ def __cbNoop(self, result):
+ # Conceivable, this is elidable.
+ # It is, afterall, a no-op.
+ (lines, tagline) = result
+ return lines
+
+ def startTLS(self, contextFactory=None):
+ """
+ Initiates a 'STARTTLS' request and negotiates the TLS / SSL
+ Handshake.
+
+ @param contextFactory: The TLS / SSL Context Factory to
+ leverage. If the contextFactory is None the IMAP4Client will
+ either use the current TLS / SSL Context Factory or attempt to
+ create a new one.
+
+ @type contextFactory: C{ssl.ClientContextFactory}
+
+ @return: A Deferred which fires when the transport has been
+ secured according to the given contextFactory, or which fails
+ if the transport cannot be secured.
+ """
+ assert (
+ not self.startedTLS
+ ), "Client and Server are currently communicating via TLS"
+ if contextFactory is None:
+ contextFactory = self._getContextFactory()
+
+ if contextFactory is None:
+ return defer.fail(
+ IMAP4Exception(
+ "IMAP4Client requires a TLS context to "
+ "initiate the STARTTLS handshake"
+ )
+ )
+
+ if b"STARTTLS" not in self._capCache:
+ return defer.fail(
+ IMAP4Exception(
+ "Server does not support secure communication " "via TLS / SSL"
+ )
+ )
+
+ tls = interfaces.ITLSTransport(self.transport, None)
+ if tls is None:
+ return defer.fail(
+ IMAP4Exception(
+ "IMAP4Client transport does not implement "
+ "interfaces.ITLSTransport"
+ )
+ )
+
+ d = self.sendCommand(Command(b"STARTTLS"))
+ d.addCallback(self._startedTLS, contextFactory)
+ d.addCallback(lambda _: self.getCapabilities())
+ return d
+
+ def authenticate(self, secret):
+ """
+ Attempt to enter the authenticated state with the server
+
+ This command is allowed in the Non-Authenticated state.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked if the authentication
+ succeeds and whose errback will be invoked otherwise.
+ """
+ if self._capCache is None:
+ d = self.getCapabilities()
+ else:
+ d = defer.succeed(self._capCache)
+ d.addCallback(self.__cbAuthenticate, secret)
+ return d
+
+ def __cbAuthenticate(self, caps, secret):
+ auths = caps.get(b"AUTH", ())
+ for scheme in auths:
+ if scheme.upper() in self.authenticators:
+ cmd = Command(
+ b"AUTHENTICATE", scheme, (), self.__cbContinueAuth, scheme, secret
+ )
+ return self.sendCommand(cmd)
+
+ if self.startedTLS:
+ return defer.fail(
+ NoSupportedAuthentication(auths, self.authenticators.keys())
+ )
+ else:
+
+ def ebStartTLS(err):
+ err.trap(IMAP4Exception)
+ # We couldn't negotiate TLS for some reason
+ return defer.fail(
+ NoSupportedAuthentication(auths, self.authenticators.keys())
+ )
+
+ d = self.startTLS()
+ d.addErrback(ebStartTLS)
+ d.addCallback(lambda _: self.getCapabilities())
+ d.addCallback(self.__cbAuthTLS, secret)
+ return d
+
+ def __cbContinueAuth(self, rest, scheme, secret):
+ try:
+ chal = decodebytes(rest + b"\n")
+ except binascii.Error:
+ self.sendLine(b"*")
+ raise IllegalServerResponse(rest)
+ else:
+ auth = self.authenticators[scheme]
+ chal = auth.challengeResponse(secret, chal)
+ self.sendLine(encodebytes(chal).strip())
+
+ def __cbAuthTLS(self, caps, secret):
+ auths = caps.get(b"AUTH", ())
+ for scheme in auths:
+ if scheme.upper() in self.authenticators:
+ cmd = Command(
+ b"AUTHENTICATE", scheme, (), self.__cbContinueAuth, scheme, secret
+ )
+ return self.sendCommand(cmd)
+ raise NoSupportedAuthentication(auths, self.authenticators.keys())
+
+ def login(self, username, password):
+ """
+ Authenticate with the server using a username and password
+
+ This command is allowed in the Non-Authenticated state. If the
+ server supports the STARTTLS capability and our transport supports
+ TLS, TLS is negotiated before the login command is issued.
+
+ A more secure way to log in is to use C{startTLS} or
+ C{authenticate} or both.
+
+ @type username: L{str}
+ @param username: The username to log in with
+
+ @type password: L{str}
+ @param password: The password to log in with
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked if login is successful
+ and whose errback is invoked otherwise.
+ """
+ d = maybeDeferred(self.getCapabilities)
+ d.addCallback(self.__cbLoginCaps, username, password)
+ return d
+
+ def serverGreeting(self, caps):
+ """
+ Called when the server has sent us a greeting.
+
+ @type caps: C{dict}
+ @param caps: Capabilities the server advertised in its greeting.
+ """
+
+ def _getContextFactory(self):
+ if self.context is not None:
+ return self.context
+ try:
+ from twisted.internet import ssl
+ except ImportError:
+ return None
+ else:
+ return ssl.ClientContextFactory()
+
+ def __cbLoginCaps(self, capabilities, username, password):
+ # If the server advertises STARTTLS, we might want to try to switch to TLS
+ tryTLS = b"STARTTLS" in capabilities
+
+ # If our transport supports switching to TLS, we might want to try to switch to TLS.
+ tlsableTransport = interfaces.ITLSTransport(self.transport, None) is not None
+
+ # If our transport is not already using TLS, we might want to try to switch to TLS.
+ nontlsTransport = interfaces.ISSLTransport(self.transport, None) is None
+
+ if not self.startedTLS and tryTLS and tlsableTransport and nontlsTransport:
+ d = self.startTLS()
+
+ d.addCallbacks(
+ self.__cbLoginTLS,
+ self.__ebLoginTLS,
+ callbackArgs=(username, password),
+ )
+ return d
+ else:
+ if nontlsTransport:
+ log.msg("Server has no TLS support. logging in over cleartext!")
+ args = b" ".join((_quote(username), _quote(password)))
+ return self.sendCommand(Command(b"LOGIN", args))
+
+ def _startedTLS(self, result, context):
+ self.transport.startTLS(context)
+ self._capCache = None
+ self.startedTLS = True
+ return result
+
+ def __cbLoginTLS(self, result, username, password):
+ args = b" ".join((_quote(username), _quote(password)))
+ return self.sendCommand(Command(b"LOGIN", args))
+
+ def __ebLoginTLS(self, failure):
+ log.err(failure)
+ return failure
+
+ def namespace(self):
+ """
+ Retrieve information about the namespaces available to this account
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with namespace
+ information. An example of this information is::
+
+ [[['', '/']], [], []]
+
+ which indicates a single personal namespace called '' with '/'
+ as its hierarchical delimiter, and no shared or user namespaces.
+ """
+ cmd = b"NAMESPACE"
+ resp = (b"NAMESPACE",)
+ d = self.sendCommand(Command(cmd, wantResponse=resp))
+ d.addCallback(self.__cbNamespace)
+ return d
+
+ def __cbNamespace(self, result):
+ (lines, last) = result
+
+ # Namespaces and their delimiters qualify and delimit
+ # mailboxes, so they should be native strings
+ #
+ # On Python 2, no decoding is necessary to maintain
+ # the API contract.
+ #
+ # On Python 3, users specify mailboxes with native strings, so
+ # they should receive namespaces and delimiters as native
+ # strings. Both cases are possible because of the imap4-utf-7
+ # encoding.
+ def _prepareNamespaceOrDelimiter(namespaceList):
+ return [element.decode("imap4-utf-7") for element in namespaceList]
+
+ for parts in lines:
+ if len(parts) == 4 and parts[0] == b"NAMESPACE":
+ return [
+ []
+ if pairOrNone is None
+ else [_prepareNamespaceOrDelimiter(value) for value in pairOrNone]
+ for pairOrNone in parts[1:]
+ ]
+ log.err("No NAMESPACE response to NAMESPACE command")
+ return [[], [], []]
+
+ def select(self, mailbox):
+ """
+ Select a mailbox
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type mailbox: L{str}
+ @param mailbox: The name of the mailbox to select
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with mailbox
+ information if the select is successful and whose errback is
+ invoked otherwise. Mailbox information consists of a dictionary
+ with the following L{str} keys and values::
+
+ FLAGS: A list of strings containing the flags settable on
+ messages in this mailbox.
+
+ EXISTS: An integer indicating the number of messages in this
+ mailbox.
+
+ RECENT: An integer indicating the number of "recent"
+ messages in this mailbox.
+
+ UNSEEN: The message sequence number (an integer) of the
+ first unseen message in the mailbox.
+
+ PERMANENTFLAGS: A list of strings containing the flags that
+ can be permanently set on messages in this mailbox.
+
+ UIDVALIDITY: An integer uniquely identifying this mailbox.
+ """
+ cmd = b"SELECT"
+ args = _prepareMailboxName(mailbox)
+ # This appears not to be used, so we can use native strings to
+ # indicate that the return type is native strings.
+ resp = ("FLAGS", "EXISTS", "RECENT", "UNSEEN", "PERMANENTFLAGS", "UIDVALIDITY")
+ d = self.sendCommand(Command(cmd, args, wantResponse=resp))
+ d.addCallback(self.__cbSelect, 1)
+ return d
+
+ def examine(self, mailbox):
+ """
+ Select a mailbox in read-only mode
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type mailbox: L{str}
+ @param mailbox: The name of the mailbox to examine
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with mailbox
+ information if the examine is successful and whose errback
+ is invoked otherwise. Mailbox information consists of a dictionary
+ with the following keys and values::
+
+ 'FLAGS': A list of strings containing the flags settable on
+ messages in this mailbox.
+
+ 'EXISTS': An integer indicating the number of messages in this
+ mailbox.
+
+ 'RECENT': An integer indicating the number of \"recent\"
+ messages in this mailbox.
+
+ 'UNSEEN': An integer indicating the number of messages not
+ flagged \\Seen in this mailbox.
+
+ 'PERMANENTFLAGS': A list of strings containing the flags that
+ can be permanently set on messages in this mailbox.
+
+ 'UIDVALIDITY': An integer uniquely identifying this mailbox.
+ """
+ cmd = b"EXAMINE"
+ args = _prepareMailboxName(mailbox)
+ resp = (
+ b"FLAGS",
+ b"EXISTS",
+ b"RECENT",
+ b"UNSEEN",
+ b"PERMANENTFLAGS",
+ b"UIDVALIDITY",
+ )
+ d = self.sendCommand(Command(cmd, args, wantResponse=resp))
+ d.addCallback(self.__cbSelect, 0)
+ return d
+
+ def _intOrRaise(self, value, phrase):
+ """
+ Parse C{value} as an integer and return the result or raise
+ L{IllegalServerResponse} with C{phrase} as an argument if C{value}
+ cannot be parsed as an integer.
+ """
+ try:
+ return int(value)
+ except ValueError:
+ raise IllegalServerResponse(phrase)
+
+ def __cbSelect(self, result, rw):
+ """
+ Handle lines received in response to a SELECT or EXAMINE command.
+
+ See RFC 3501, section 6.3.1.
+ """
+ (lines, tagline) = result
+ # In the absence of specification, we are free to assume:
+ # READ-WRITE access
+ datum = {"READ-WRITE": rw}
+ lines.append(parseNestedParens(tagline))
+ for split in lines:
+ if len(split) > 0 and split[0].upper() == b"OK":
+ # Handle all the kinds of OK response.
+ content = split[1]
+ if isinstance(content, list):
+ key = content[0]
+ else:
+ # not multi-valued, like OK LOGIN
+ key = content
+ key = key.upper()
+ if key == b"READ-ONLY":
+ datum["READ-WRITE"] = False
+ elif key == b"READ-WRITE":
+ datum["READ-WRITE"] = True
+ elif key == b"UIDVALIDITY":
+ datum["UIDVALIDITY"] = self._intOrRaise(content[1], split)
+ elif key == b"UNSEEN":
+ datum["UNSEEN"] = self._intOrRaise(content[1], split)
+ elif key == b"UIDNEXT":
+ datum["UIDNEXT"] = self._intOrRaise(content[1], split)
+ elif key == b"PERMANENTFLAGS":
+ datum["PERMANENTFLAGS"] = tuple(
+ nativeString(flag) for flag in content[1]
+ )
+ else:
+ log.err(f"Unhandled SELECT response (2): {split}")
+ elif len(split) == 2:
+ # Handle FLAGS, EXISTS, and RECENT
+ if split[0].upper() == b"FLAGS":
+ datum["FLAGS"] = tuple(nativeString(flag) for flag in split[1])
+ elif isinstance(split[1], bytes):
+ # Must make sure things are strings before treating them as
+ # strings since some other forms of response have nesting in
+ # places which results in lists instead.
+ if split[1].upper() == b"EXISTS":
+ datum["EXISTS"] = self._intOrRaise(split[0], split)
+ elif split[1].upper() == b"RECENT":
+ datum["RECENT"] = self._intOrRaise(split[0], split)
+ else:
+ log.err(f"Unhandled SELECT response (0): {split}")
+ else:
+ log.err(f"Unhandled SELECT response (1): {split}")
+ else:
+ log.err(f"Unhandled SELECT response (4): {split}")
+ return datum
+
+ def create(self, name):
+ """
+ Create a new mailbox on the server
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type name: L{str}
+ @param name: The name of the mailbox to create.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked if the mailbox creation
+ is successful and whose errback is invoked otherwise.
+ """
+ return self.sendCommand(Command(b"CREATE", _prepareMailboxName(name)))
+
+ def delete(self, name):
+ """
+ Delete a mailbox
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type name: L{str}
+ @param name: The name of the mailbox to delete.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose calblack is invoked if the mailbox is
+ deleted successfully and whose errback is invoked otherwise.
+ """
+ return self.sendCommand(Command(b"DELETE", _prepareMailboxName(name)))
+
+ def rename(self, oldname, newname):
+ """
+ Rename a mailbox
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type oldname: L{str}
+ @param oldname: The current name of the mailbox to rename.
+
+ @type newname: L{str}
+ @param newname: The new name to give the mailbox.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked if the rename is
+ successful and whose errback is invoked otherwise.
+ """
+ oldname = _prepareMailboxName(oldname)
+ newname = _prepareMailboxName(newname)
+ return self.sendCommand(Command(b"RENAME", b" ".join((oldname, newname))))
+
+ def subscribe(self, name):
+ """
+ Add a mailbox to the subscription list
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type name: L{str}
+ @param name: The mailbox to mark as 'active' or 'subscribed'
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked if the subscription
+ is successful and whose errback is invoked otherwise.
+ """
+ return self.sendCommand(Command(b"SUBSCRIBE", _prepareMailboxName(name)))
+
+ def unsubscribe(self, name):
+ """
+ Remove a mailbox from the subscription list
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type name: L{str}
+ @param name: The mailbox to unsubscribe
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked if the unsubscription
+ is successful and whose errback is invoked otherwise.
+ """
+ return self.sendCommand(Command(b"UNSUBSCRIBE", _prepareMailboxName(name)))
+
+ def list(self, reference, wildcard):
+ """
+ List a subset of the available mailboxes
+
+ This command is allowed in the Authenticated and Selected
+ states.
+
+ @type reference: L{str}
+ @param reference: The context in which to interpret
+ C{wildcard}
+
+ @type wildcard: L{str}
+ @param wildcard: The pattern of mailbox names to match,
+ optionally including either or both of the '*' and '%'
+ wildcards. '*' will match zero or more characters and
+ cross hierarchical boundaries. '%' will also match zero
+ or more characters, but is limited to a single
+ hierarchical level.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a list of
+ L{tuple}s, the first element of which is a L{tuple} of
+ mailbox flags, the second element of which is the
+ hierarchy delimiter for this mailbox, and the third of
+ which is the mailbox name; if the command is unsuccessful,
+ the deferred's errback is invoked instead. B{NB}: the
+ delimiter and the mailbox name are L{str}s.
+ """
+ cmd = b"LIST"
+ args = (f'"{reference}" "{wildcard}"').encode("imap4-utf-7")
+ resp = (b"LIST",)
+ d = self.sendCommand(Command(cmd, args, wantResponse=resp))
+ d.addCallback(self.__cbList, b"LIST")
+ return d
+
+ def lsub(self, reference, wildcard):
+ """
+ List a subset of the subscribed available mailboxes
+
+ This command is allowed in the Authenticated and Selected states.
+
+ The parameters and returned object are the same as for the L{list}
+ method, with one slight difference: Only mailboxes which have been
+ subscribed can be included in the resulting list.
+ """
+ cmd = b"LSUB"
+
+ encodedReference = reference.encode("ascii")
+ encodedWildcard = wildcard.encode("imap4-utf-7")
+ args = b"".join(
+ [
+ b'"',
+ encodedReference,
+ b'"' b' "',
+ encodedWildcard,
+ b'"',
+ ]
+ )
+ resp = (b"LSUB",)
+ d = self.sendCommand(Command(cmd, args, wantResponse=resp))
+ d.addCallback(self.__cbList, b"LSUB")
+ return d
+
+ def __cbList(self, result, command):
+ (lines, last) = result
+ results = []
+
+ for parts in lines:
+ if len(parts) == 4 and parts[0] == command:
+ # flags
+ parts[1] = tuple(nativeString(flag) for flag in parts[1])
+
+ # The mailbox should be a native string.
+ # On Python 2, this maintains the API's contract.
+ #
+ # On Python 3, users specify mailboxes with native
+ # strings, so they should receive mailboxes as native
+ # strings. Both cases are possible because of the
+ # imap4-utf-7 encoding.
+ #
+ # Mailbox names contain the hierarchical delimiter, so
+ # it too should be a native string.
+ # delimiter
+ parts[2] = parts[2].decode("imap4-utf-7")
+ # mailbox
+ parts[3] = parts[3].decode("imap4-utf-7")
+
+ results.append(tuple(parts[1:]))
+ return results
+
+ _statusNames = {
+ name: name.encode("ascii")
+ for name in (
+ "MESSAGES",
+ "RECENT",
+ "UIDNEXT",
+ "UIDVALIDITY",
+ "UNSEEN",
+ )
+ }
+
+ def status(self, mailbox, *names):
+ """
+ Retrieve the status of the given mailbox
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type mailbox: L{str}
+ @param mailbox: The name of the mailbox to query
+
+ @type names: L{bytes}
+ @param names: The status names to query. These may be any number of:
+ C{'MESSAGES'}, C{'RECENT'}, C{'UIDNEXT'}, C{'UIDVALIDITY'}, and
+ C{'UNSEEN'}.
+
+ @rtype: L{Deferred}
+ @return: A deferred which fires with the status information if the
+ command is successful and whose errback is invoked otherwise. The
+ status information is in the form of a C{dict}. Each element of
+ C{names} is a key in the dictionary. The value for each key is the
+ corresponding response from the server.
+ """
+ cmd = b"STATUS"
+
+ preparedMailbox = _prepareMailboxName(mailbox)
+ try:
+ names = b" ".join(self._statusNames[name] for name in names)
+ except KeyError:
+ raise ValueError(f"Unknown names: {set(names) - set(self._statusNames)!r}")
+
+ args = b"".join([preparedMailbox, b" (", names, b")"])
+ resp = (b"STATUS",)
+ d = self.sendCommand(Command(cmd, args, wantResponse=resp))
+ d.addCallback(self.__cbStatus)
+ return d
+
+ def __cbStatus(self, result):
+ (lines, last) = result
+ status = {}
+ for parts in lines:
+ if parts[0] == b"STATUS":
+ items = parts[2]
+ items = [items[i : i + 2] for i in range(0, len(items), 2)]
+ for k, v in items:
+ try:
+ status[nativeString(k)] = v
+ except UnicodeDecodeError:
+ raise IllegalServerResponse(repr(items))
+ for k in status.keys():
+ t = self.STATUS_TRANSFORMATIONS.get(k)
+ if t:
+ try:
+ status[k] = t(status[k])
+ except Exception as e:
+ raise IllegalServerResponse(
+ "(" + k + " " + status[k] + "): " + str(e)
+ )
+ return status
+
+ def append(self, mailbox, message, flags=(), date=None):
+ """
+ Add the given message to the given mailbox.
+
+ This command is allowed in the Authenticated and Selected states.
+
+ @type mailbox: L{str}
+ @param mailbox: The mailbox to which to add this message.
+
+ @type message: Any file-like object opened in B{binary mode}.
+ @param message: The message to add, in RFC822 format. Newlines
+ in this file should be \\r\\n-style.
+
+ @type flags: Any iterable of L{str}
+ @param flags: The flags to associated with this message.
+
+ @type date: L{str}
+ @param date: The date to associate with this message. This should
+ be of the format DD-MM-YYYY HH:MM:SS +/-HHMM. For example, in
+ Eastern Standard Time, on July 1st 2004 at half past 1 PM,
+ \"01-07-2004 13:30:00 -0500\".
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked when this command
+ succeeds or whose errback is invoked if it fails.
+ """
+ message.seek(0, 2)
+ L = message.tell()
+ message.seek(0, 0)
+ if date:
+ date = networkString(' "%s"' % nativeString(date))
+ else:
+ date = b""
+
+ encodedFlags = [networkString(flag) for flag in flags]
+
+ cmd = b"%b (%b)%b {%d}" % (
+ _prepareMailboxName(mailbox),
+ b" ".join(encodedFlags),
+ date,
+ L,
+ )
+
+ d = self.sendCommand(
+ Command(b"APPEND", cmd, (), self.__cbContinueAppend, message)
+ )
+ return d
+
+ def __cbContinueAppend(self, lines, message):
+ s = basic.FileSender()
+ return s.beginFileTransfer(message, self.transport, None).addCallback(
+ self.__cbFinishAppend
+ )
+
+ def __cbFinishAppend(self, foo):
+ self.sendLine(b"")
+
+ def check(self):
+ """
+ Tell the server to perform a checkpoint
+
+ This command is allowed in the Selected state.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked when this command
+ succeeds or whose errback is invoked if it fails.
+ """
+ return self.sendCommand(Command(b"CHECK"))
+
+ def close(self):
+ """
+ Return the connection to the Authenticated state.
+
+ This command is allowed in the Selected state.
+
+ Issuing this command will also remove all messages flagged \\Deleted
+ from the selected mailbox if it is opened in read-write mode,
+ otherwise it indicates success by no messages are removed.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked when the command
+ completes successfully or whose errback is invoked if it fails.
+ """
+ return self.sendCommand(Command(b"CLOSE"))
+
+ def expunge(self):
+ """
+ Return the connection to the Authenticate state.
+
+ This command is allowed in the Selected state.
+
+ Issuing this command will perform the same actions as issuing the
+ close command, but will also generate an 'expunge' response for
+ every message deleted.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a list of the
+ 'expunge' responses when this command is successful or whose errback
+ is invoked otherwise.
+ """
+ cmd = b"EXPUNGE"
+ resp = (b"EXPUNGE",)
+ d = self.sendCommand(Command(cmd, wantResponse=resp))
+ d.addCallback(self.__cbExpunge)
+ return d
+
+ def __cbExpunge(self, result):
+ (lines, last) = result
+ ids = []
+ for parts in lines:
+ if len(parts) == 2 and parts[1] == b"EXPUNGE":
+ ids.append(self._intOrRaise(parts[0], parts))
+ return ids
+
+ def search(self, *queries, uid=False):
+ """
+ Search messages in the currently selected mailbox
+
+ This command is allowed in the Selected state.
+
+ Any non-zero number of queries are accepted by this method, as returned
+ by the C{Query}, C{Or}, and C{Not} functions.
+
+ @param uid: if true, the server is asked to return message UIDs instead
+ of message sequence numbers.
+ @type uid: L{bool}
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback will be invoked with a list of all
+ the message sequence numbers return by the search, or whose errback
+ will be invoked if there is an error.
+ """
+ # Queries should be encoded as ASCII unless a charset
+ # identifier is provided. See #9201.
+ queries = [query.encode("charmap") for query in queries]
+
+ cmd = b"UID SEARCH" if uid else b"SEARCH"
+ args = b" ".join(queries)
+ d = self.sendCommand(Command(cmd, args, wantResponse=(cmd,)))
+ d.addCallback(self.__cbSearch)
+ return d
+
+ def __cbSearch(self, result):
+ (lines, end) = result
+ ids = []
+ for parts in lines:
+ if len(parts) > 0 and parts[0] == b"SEARCH":
+ ids.extend([self._intOrRaise(p, parts) for p in parts[1:]])
+ return ids
+
+ def fetchUID(self, messages, uid=0):
+ """
+ Retrieve the unique identifier for one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: L{MessageSet} or L{str}
+ @param messages: A message sequence set
+
+ @type uid: L{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message sequence numbers to unique message identifiers, or whose
+ errback is invoked if there is an error.
+ """
+ return self._fetch(messages, useUID=uid, uid=1)
+
+ def fetchFlags(self, messages, uid=0):
+ """
+ Retrieve the flags for one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: L{MessageSet} or L{str}
+ @param messages: The messages for which to retrieve flags.
+
+ @type uid: L{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to lists of flags, or whose errback is invoked if
+ there is an error.
+ """
+ return self._fetch(messages, useUID=uid, flags=1)
+
+ def fetchInternalDate(self, messages, uid=0):
+ """
+ Retrieve the internal date associated with one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: L{MessageSet} or L{str}
+ @param messages: The messages for which to retrieve the internal date.
+
+ @type uid: L{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to date strings, or whose errback is invoked
+ if there is an error. Date strings take the format of
+ \"day-month-year time timezone\".
+ """
+ return self._fetch(messages, useUID=uid, internaldate=1)
+
+ def fetchEnvelope(self, messages, uid=0):
+ """
+ Retrieve the envelope data for one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: L{MessageSet} or L{str}
+ @param messages: The messages for which to retrieve envelope
+ data.
+
+ @type uid: L{bool}
+ @param uid: Indicates whether the message sequence set is of
+ message numbers or of unique message IDs.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a dict
+ mapping message numbers to envelope data, or whose errback
+ is invoked if there is an error. Envelope data consists
+ of a sequence of the date, subject, from, sender,
+ reply-to, to, cc, bcc, in-reply-to, and message-id header
+ fields. The date, subject, in-reply-to, and message-id
+ fields are L{str}, while the from, sender, reply-to, to,
+ cc, and bcc fields contain address data as L{str}s.
+ Address data consists of a sequence of name, source route,
+ mailbox name, and hostname. Fields which are not present
+ for a particular address may be L{None}.
+ """
+ return self._fetch(messages, useUID=uid, envelope=1)
+
+ def fetchBodyStructure(self, messages, uid=0):
+ """
+ Retrieve the structure of the body of one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: L{MessageSet} or L{str}
+ @param messages: The messages for which to retrieve body structure
+ data.
+
+ @type uid: L{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to body structure data, or whose errback is invoked
+ if there is an error. Body structure data describes the MIME-IMB
+ format of a message and consists of a sequence of mime type, mime
+ subtype, parameters, content id, description, encoding, and size.
+ The fields following the size field are variable: if the mime
+ type/subtype is message/rfc822, the contained message's envelope
+ information, body structure data, and number of lines of text; if
+ the mime type is text, the number of lines of text. Extension fields
+ may also be included; if present, they are: the MD5 hash of the body,
+ body disposition, body language.
+ """
+ return self._fetch(messages, useUID=uid, bodystructure=1)
+
+ def fetchSimplifiedBody(self, messages, uid=0):
+ """
+ Retrieve the simplified body structure of one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: L{MessageSet} or L{str}
+ @param messages: A message sequence set
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to body data, or whose errback is invoked
+ if there is an error. The simplified body structure is the same
+ as the body structure, except that extension fields will never be
+ present.
+ """
+ return self._fetch(messages, useUID=uid, body=1)
+
+ def fetchMessage(self, messages, uid=0):
+ """
+ Retrieve one or more entire messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: L{MessageSet} or L{str}
+ @param messages: A message sequence set
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: L{Deferred}
+
+ @return: A L{Deferred} which will fire with a C{dict} mapping message
+ sequence numbers to C{dict}s giving message data for the
+ corresponding message. If C{uid} is true, the inner dictionaries
+ have a C{'UID'} key mapped to a L{str} giving the UID for the
+ message. The text of the message is a L{str} associated with the
+ C{'RFC822'} key in each dictionary.
+ """
+ return self._fetch(messages, useUID=uid, rfc822=1)
+
+ def fetchHeaders(self, messages, uid=0):
+ """
+ Retrieve headers of one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: L{MessageSet} or L{str}
+ @param messages: A message sequence set
+
+ @type uid: L{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to dicts of message headers, or whose errback is
+ invoked if there is an error.
+ """
+ return self._fetch(messages, useUID=uid, rfc822header=1)
+
+ def fetchBody(self, messages, uid=0):
+ """
+ Retrieve body text of one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: L{MessageSet} or L{str}
+ @param messages: A message sequence set
+
+ @type uid: L{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to file-like objects containing body text, or whose
+ errback is invoked if there is an error.
+ """
+ return self._fetch(messages, useUID=uid, rfc822text=1)
+
+ def fetchSize(self, messages, uid=0):
+ """
+ Retrieve the size, in octets, of one or more messages
+
+ This command is allowed in the Selected state.
+
+ @type messages: L{MessageSet} or L{str}
+ @param messages: A message sequence set
+
+ @type uid: L{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to sizes, or whose errback is invoked if there is
+ an error.
+ """
+ return self._fetch(messages, useUID=uid, rfc822size=1)
+
+ def fetchFull(self, messages, uid=0):
+ """
+ Retrieve several different fields of one or more messages
+
+ This command is allowed in the Selected state. This is equivalent
+ to issuing all of the C{fetchFlags}, C{fetchInternalDate},
+ C{fetchSize}, C{fetchEnvelope}, and C{fetchSimplifiedBody}
+ functions.
+
+ @type messages: L{MessageSet} or L{str}
+ @param messages: A message sequence set
+
+ @type uid: L{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to dict of the retrieved data values, or whose
+ errback is invoked if there is an error. They dictionary keys
+ are "flags", "date", "size", "envelope", and "body".
+ """
+ return self._fetch(
+ messages,
+ useUID=uid,
+ flags=1,
+ internaldate=1,
+ rfc822size=1,
+ envelope=1,
+ body=1,
+ )
+
+ def fetchAll(self, messages, uid=0):
+ """
+ Retrieve several different fields of one or more messages
+
+ This command is allowed in the Selected state. This is equivalent
+ to issuing all of the C{fetchFlags}, C{fetchInternalDate},
+ C{fetchSize}, and C{fetchEnvelope} functions.
+
+ @type messages: L{MessageSet} or L{str}
+ @param messages: A message sequence set
+
+ @type uid: L{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to dict of the retrieved data values, or whose
+ errback is invoked if there is an error. They dictionary keys
+ are "flags", "date", "size", and "envelope".
+ """
+ return self._fetch(
+ messages, useUID=uid, flags=1, internaldate=1, rfc822size=1, envelope=1
+ )
+
+ def fetchFast(self, messages, uid=0):
+ """
+ Retrieve several different fields of one or more messages
+
+ This command is allowed in the Selected state. This is equivalent
+ to issuing all of the C{fetchFlags}, C{fetchInternalDate}, and
+ C{fetchSize} functions.
+
+ @type messages: L{MessageSet} or L{str}
+ @param messages: A message sequence set
+
+ @type uid: L{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a dict mapping
+ message numbers to dict of the retrieved data values, or whose
+ errback is invoked if there is an error. They dictionary keys are
+ "flags", "date", and "size".
+ """
+ return self._fetch(messages, useUID=uid, flags=1, internaldate=1, rfc822size=1)
+
+ def _parseFetchPairs(self, fetchResponseList):
+ """
+ Given the result of parsing a single I{FETCH} response, construct a
+ L{dict} mapping response keys to response values.
+
+ @param fetchResponseList: The result of parsing a I{FETCH} response
+ with L{parseNestedParens} and extracting just the response data
+ (that is, just the part that comes after C{"FETCH"}). The form
+ of this input (and therefore the output of this method) is very
+ disagreeable. A valuable improvement would be to enumerate the
+ possible keys (representing them as structured objects of some
+ sort) rather than using strings and tuples of tuples of strings
+ and so forth. This would allow the keys to be documented more
+ easily and would allow for a much simpler application-facing API
+ (one not based on looking up somewhat hard to predict keys in a
+ dict). Since C{fetchResponseList} notionally represents a
+ flattened sequence of pairs (identifying keys followed by their
+ associated values), collapsing such complex elements of this
+ list as C{["BODY", ["HEADER.FIELDS", ["SUBJECT"]]]} into a
+ single object would also greatly simplify the implementation of
+ this method.
+
+ @return: A C{dict} of the response data represented by C{pairs}. Keys
+ in this dictionary are things like C{"RFC822.TEXT"}, C{"FLAGS"}, or
+ C{("BODY", ("HEADER.FIELDS", ("SUBJECT",)))}. Values are entirely
+ dependent on the key with which they are associated, but retain the
+ same structured as produced by L{parseNestedParens}.
+ """
+
+ # TODO: RFC 3501 Section 7.4.2, "FETCH Response", says for
+ # BODY responses that "8-bit textual data is permitted if a
+ # charset identifier is part of the body parameter
+ # parenthesized list". Every other component is 7-bit. This
+ # should parse out the charset identifier and use it to decode
+ # 8-bit bodies. Until then, on Python 2 it should continue to
+ # return native (byte) strings, while on Python 3 it should
+ # decode bytes to native strings via charmap, ensuring data
+ # fidelity at the cost of mojibake.
+ def nativeStringResponse(thing):
+ if isinstance(thing, bytes):
+ return thing.decode("charmap")
+ elif isinstance(thing, list):
+ return [nativeStringResponse(subthing) for subthing in thing]
+
+ values = {}
+ unstructured = []
+
+ responseParts = iter(fetchResponseList)
+ while True:
+ try:
+ key = next(responseParts)
+ except StopIteration:
+ break
+
+ try:
+ value = next(responseParts)
+ except StopIteration:
+ raise IllegalServerResponse(b"Not enough arguments", fetchResponseList)
+
+ # The parsed forms of responses like:
+ #
+ # BODY[] VALUE
+ # BODY[TEXT] VALUE
+ # BODY[HEADER.FIELDS (SUBJECT)] VALUE
+ # BODY[HEADER.FIELDS (SUBJECT)]<N.M> VALUE
+ #
+ # are:
+ #
+ # ["BODY", [], VALUE]
+ # ["BODY", ["TEXT"], VALUE]
+ # ["BODY", ["HEADER.FIELDS", ["SUBJECT"]], VALUE]
+ # ["BODY", ["HEADER.FIELDS", ["SUBJECT"]], "<N.M>", VALUE]
+ #
+ # Additionally, BODY responses for multipart messages are
+ # represented as:
+ #
+ # ["BODY", VALUE]
+ #
+ # with list as the type of VALUE and the type of VALUE[0].
+ #
+ # See #6281 for ideas on how this might be improved.
+
+ if key not in (b"BODY", b"BODY.PEEK"):
+ # Only BODY (and by extension, BODY.PEEK) responses can have
+ # body sections.
+ hasSection = False
+ elif not isinstance(value, list):
+ # A BODY section is always represented as a list. Any non-list
+ # is not a BODY section.
+ hasSection = False
+ elif len(value) > 2:
+ # The list representing a BODY section has at most two elements.
+ hasSection = False
+ elif value and isinstance(value[0], list):
+ # A list containing a list represents the body structure of a
+ # multipart message, instead.
+ hasSection = False
+ else:
+ # Otherwise it must have a BODY section to examine.
+ hasSection = True
+
+ # If it has a BODY section, grab some extra elements and shuffle
+ # around the shape of the key a little bit.
+
+ key = nativeString(key)
+ unstructured.append(key)
+
+ if hasSection:
+ if len(value) < 2:
+ value = [nativeString(v) for v in value]
+ unstructured.append(value)
+
+ key = (key, tuple(value))
+ else:
+ valueHead = nativeString(value[0])
+ valueTail = [nativeString(v) for v in value[1]]
+ unstructured.append([valueHead, valueTail])
+
+ key = (key, (valueHead, tuple(valueTail)))
+ try:
+ value = next(responseParts)
+ except StopIteration:
+ raise IllegalServerResponse(
+ b"Not enough arguments", fetchResponseList
+ )
+
+ # Handle partial ranges
+ if value.startswith(b"<") and value.endswith(b">"):
+ try:
+ int(value[1:-1])
+ except ValueError:
+ # This isn't really a range, it's some content.
+ pass
+ else:
+ value = nativeString(value)
+ unstructured.append(value)
+ key = key + (value,)
+ try:
+ value = next(responseParts)
+ except StopIteration:
+ raise IllegalServerResponse(
+ b"Not enough arguments", fetchResponseList
+ )
+
+ value = nativeStringResponse(value)
+ unstructured.append(value)
+ values[key] = value
+
+ return values, unstructured
+
+ def _cbFetch(self, result, requestedParts, structured):
+ (lines, last) = result
+ info = {}
+ for parts in lines:
+ if len(parts) == 3 and parts[1] == b"FETCH":
+ id = self._intOrRaise(parts[0], parts)
+ if id not in info:
+ info[id] = [parts[2]]
+ else:
+ info[id][0].extend(parts[2])
+
+ results = {}
+ decodedInfo = {}
+ for messageId, values in info.items():
+ structuredMap, unstructuredList = self._parseFetchPairs(values[0])
+ decodedInfo.setdefault(messageId, [[]])[0].extend(unstructuredList)
+ results.setdefault(messageId, {}).update(structuredMap)
+ info = decodedInfo
+
+ flagChanges = {}
+ for messageId in list(results.keys()):
+ values = results[messageId]
+ for part in list(values.keys()):
+ if part not in requestedParts and part == "FLAGS":
+ flagChanges[messageId] = values["FLAGS"]
+ # Find flags in the result and get rid of them.
+ for i in range(len(info[messageId][0])):
+ if info[messageId][0][i] == "FLAGS":
+ del info[messageId][0][i : i + 2]
+ break
+ del values["FLAGS"]
+ if not values:
+ del results[messageId]
+
+ if flagChanges:
+ self.flagsChanged(flagChanges)
+
+ if structured:
+ return results
+ else:
+ return info
+
+ def fetchSpecific(
+ self,
+ messages,
+ uid=0,
+ headerType=None,
+ headerNumber=None,
+ headerArgs=None,
+ peek=None,
+ offset=None,
+ length=None,
+ ):
+ """
+ Retrieve a specific section of one or more messages
+
+ @type messages: L{MessageSet} or L{str}
+ @param messages: A message sequence set
+
+ @type uid: L{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @type headerType: L{str}
+ @param headerType: If specified, must be one of HEADER, HEADER.FIELDS,
+ HEADER.FIELDS.NOT, MIME, or TEXT, and will determine which part of
+ the message is retrieved. For HEADER.FIELDS and HEADER.FIELDS.NOT,
+ C{headerArgs} must be a sequence of header names. For MIME,
+ C{headerNumber} must be specified.
+
+ @type headerNumber: L{int} or L{int} sequence
+ @param headerNumber: The nested rfc822 index specifying the entity to
+ retrieve. For example, C{1} retrieves the first entity of the
+ message, and C{(2, 1, 3}) retrieves the 3rd entity inside the first
+ entity inside the second entity of the message.
+
+ @type headerArgs: A sequence of L{str}
+ @param headerArgs: If C{headerType} is HEADER.FIELDS, these are the
+ headers to retrieve. If it is HEADER.FIELDS.NOT, these are the
+ headers to exclude from retrieval.
+
+ @type peek: C{bool}
+ @param peek: If true, cause the server to not set the \\Seen flag on
+ this message as a result of this command.
+
+ @type offset: L{int}
+ @param offset: The number of octets at the beginning of the result to
+ skip.
+
+ @type length: L{int}
+ @param length: The number of octets to retrieve.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a mapping of message
+ numbers to retrieved data, or whose errback is invoked if there is
+ an error.
+ """
+ fmt = "%s BODY%s[%s%s%s]%s"
+ if headerNumber is None:
+ number = ""
+ elif isinstance(headerNumber, int):
+ number = str(headerNumber)
+ else:
+ number = ".".join(map(str, headerNumber))
+ if headerType is None:
+ header = ""
+ elif number:
+ header = "." + headerType
+ else:
+ header = headerType
+ if header and headerType in ("HEADER.FIELDS", "HEADER.FIELDS.NOT"):
+ if headerArgs is not None:
+ payload = " (%s)" % " ".join(headerArgs)
+ else:
+ payload = " ()"
+ else:
+ payload = ""
+ if offset is None:
+ extra = ""
+ else:
+ extra = "<%d.%d>" % (offset, length)
+ fetch = uid and b"UID FETCH" or b"FETCH"
+ cmd = fmt % (messages, peek and ".PEEK" or "", number, header, payload, extra)
+
+ # APPEND components should be encoded as ASCII unless a
+ # charset identifier is provided. See #9201.
+ cmd = cmd.encode("charmap")
+
+ d = self.sendCommand(Command(fetch, cmd, wantResponse=(b"FETCH",)))
+ d.addCallback(self._cbFetch, (), False)
+ return d
+
+ def _fetch(self, messages, useUID=0, **terms):
+ messages = str(messages).encode("ascii")
+ fetch = useUID and b"UID FETCH" or b"FETCH"
+
+ if "rfc822text" in terms:
+ del terms["rfc822text"]
+ terms["rfc822.text"] = True
+ if "rfc822size" in terms:
+ del terms["rfc822size"]
+ terms["rfc822.size"] = True
+ if "rfc822header" in terms:
+ del terms["rfc822header"]
+ terms["rfc822.header"] = True
+
+ # The terms in 6.4.5 are all ASCII congruent, so wing it.
+ # Note that this isn't a public API, so terms in responses
+ # should not be decoded to native strings.
+ encodedTerms = [networkString(s) for s in terms]
+ cmd = messages + b" (" + b" ".join([s.upper() for s in encodedTerms]) + b")"
+
+ d = self.sendCommand(Command(fetch, cmd, wantResponse=(b"FETCH",)))
+ d.addCallback(self._cbFetch, [t.upper() for t in terms.keys()], True)
+ return d
+
+ def setFlags(self, messages, flags, silent=1, uid=0):
+ """
+ Set the flags for one or more messages.
+
+ This command is allowed in the Selected state.
+
+ @type messages: L{MessageSet} or L{str}
+ @param messages: A message sequence set
+
+ @type flags: Any iterable of L{str}
+ @param flags: The flags to set
+
+ @type silent: L{bool}
+ @param silent: If true, cause the server to suppress its verbose
+ response.
+
+ @type uid: L{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a list of the
+ server's responses (C{[]} if C{silent} is true) or whose
+ errback is invoked if there is an error.
+ """
+ return self._store(messages, b"FLAGS", silent, flags, uid)
+
+ def addFlags(self, messages, flags, silent=1, uid=0):
+ """
+ Add to the set flags for one or more messages.
+
+ This command is allowed in the Selected state.
+
+ @type messages: C{MessageSet} or L{str}
+ @param messages: A message sequence set
+
+ @type flags: Any iterable of L{str}
+ @param flags: The flags to set
+
+ @type silent: C{bool}
+ @param silent: If true, cause the server to suppress its verbose
+ response.
+
+ @type uid: C{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a list of the
+ server's responses (C{[]} if C{silent} is true) or whose
+ errback is invoked if there is an error.
+ """
+ return self._store(messages, b"+FLAGS", silent, flags, uid)
+
+ def removeFlags(self, messages, flags, silent=1, uid=0):
+ """
+ Remove from the set flags for one or more messages.
+
+ This command is allowed in the Selected state.
+
+ @type messages: L{MessageSet} or L{str}
+ @param messages: A message sequence set
+
+ @type flags: Any iterable of L{str}
+ @param flags: The flags to set
+
+ @type silent: L{bool}
+ @param silent: If true, cause the server to suppress its verbose
+ response.
+
+ @type uid: L{bool}
+ @param uid: Indicates whether the message sequence set is of message
+ numbers or of unique message IDs.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a list of the
+ server's responses (C{[]} if C{silent} is true) or whose
+ errback is invoked if there is an error.
+ """
+ return self._store(messages, b"-FLAGS", silent, flags, uid)
+
+ def _store(self, messages, cmd, silent, flags, uid):
+ messages = str(messages).encode("ascii")
+ encodedFlags = [networkString(flag) for flag in flags]
+ if silent:
+ cmd = cmd + b".SILENT"
+ store = uid and b"UID STORE" or b"STORE"
+ args = b" ".join((messages, cmd, b"(" + b" ".join(encodedFlags) + b")"))
+ d = self.sendCommand(Command(store, args, wantResponse=(b"FETCH",)))
+ expected = ()
+ if not silent:
+ expected = ("FLAGS",)
+ d.addCallback(self._cbFetch, expected, True)
+ return d
+
+ def copy(self, messages, mailbox, uid):
+ """
+ Copy the specified messages to the specified mailbox.
+
+ This command is allowed in the Selected state.
+
+ @type messages: L{MessageSet} or L{str}
+ @param messages: A message sequence set
+
+ @type mailbox: L{str}
+ @param mailbox: The mailbox to which to copy the messages
+
+ @type uid: C{bool}
+ @param uid: If true, the C{messages} refers to message UIDs, rather
+ than message sequence numbers.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with a true value
+ when the copy is successful, or whose errback is invoked if there
+ is an error.
+ """
+ messages = str(messages).encode("ascii")
+ if uid:
+ cmd = b"UID COPY"
+ else:
+ cmd = b"COPY"
+ args = b" ".join([messages, _prepareMailboxName(mailbox)])
+ return self.sendCommand(Command(cmd, args))
+
+ #
+ # IMailboxListener methods
+ #
+ def modeChanged(self, writeable):
+ """Override me"""
+
+ def flagsChanged(self, newFlags):
+ """Override me"""
+
+ def newMessages(self, exists, recent):
+ """Override me"""
+
+
+def parseIdList(s, lastMessageId=None):
+ """
+ Parse a message set search key into a C{MessageSet}.
+
+ @type s: L{bytes}
+ @param s: A string description of an id list, for example "1:3, 4:*"
+
+ @type lastMessageId: L{int}
+ @param lastMessageId: The last message sequence id or UID, depending on
+ whether we are parsing the list in UID or sequence id context. The
+ caller should pass in the correct value.
+
+ @rtype: C{MessageSet}
+ @return: A C{MessageSet} that contains the ids defined in the list
+ """
+ res = MessageSet()
+ parts = s.split(b",")
+ for p in parts:
+ if b":" in p:
+ low, high = p.split(b":", 1)
+ try:
+ if low == b"*":
+ low = None
+ else:
+ low = int(low)
+ if high == b"*":
+ high = None
+ else:
+ high = int(high)
+ if low is high is None:
+ # *:* does not make sense
+ raise IllegalIdentifierError(p)
+ # non-positive values are illegal according to RFC 3501
+ if (low is not None and low <= 0) or (high is not None and high <= 0):
+ raise IllegalIdentifierError(p)
+ # star means "highest value of an id in the mailbox"
+ high = high or lastMessageId
+ low = low or lastMessageId
+
+ res.add(low, high)
+ except ValueError:
+ raise IllegalIdentifierError(p)
+ else:
+ try:
+ if p == b"*":
+ p = None
+ else:
+ p = int(p)
+ if p is not None and p <= 0:
+ raise IllegalIdentifierError(p)
+ except ValueError:
+ raise IllegalIdentifierError(p)
+ else:
+ res.extend(p or lastMessageId)
+ return res
+
+
+_SIMPLE_BOOL = (
+ "ALL",
+ "ANSWERED",
+ "DELETED",
+ "DRAFT",
+ "FLAGGED",
+ "NEW",
+ "OLD",
+ "RECENT",
+ "SEEN",
+ "UNANSWERED",
+ "UNDELETED",
+ "UNDRAFT",
+ "UNFLAGGED",
+ "UNSEEN",
+)
+
+_NO_QUOTES = ("LARGER", "SMALLER", "UID")
+
+_sorted = sorted
+
+
+def Query(sorted=0, **kwarg):
+ """
+ Create a query string
+
+ Among the accepted keywords are::
+
+ all : If set to a true value, search all messages in the
+ current mailbox
+
+ answered : If set to a true value, search messages flagged with
+ \\Answered
+
+ bcc : A substring to search the BCC header field for
+
+ before : Search messages with an internal date before this
+ value. The given date should be a string in the format
+ of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
+
+ body : A substring to search the body of the messages for
+
+ cc : A substring to search the CC header field for
+
+ deleted : If set to a true value, search messages flagged with
+ \\Deleted
+
+ draft : If set to a true value, search messages flagged with
+ \\Draft
+
+ flagged : If set to a true value, search messages flagged with
+ \\Flagged
+
+ from : A substring to search the From header field for
+
+ header : A two-tuple of a header name and substring to search
+ for in that header
+
+ keyword : Search for messages with the given keyword set
+
+ larger : Search for messages larger than this number of octets
+
+ messages : Search only the given message sequence set.
+
+ new : If set to a true value, search messages flagged with
+ \\Recent but not \\Seen
+
+ old : If set to a true value, search messages not flagged with
+ \\Recent
+
+ on : Search messages with an internal date which is on this
+ date. The given date should be a string in the format
+ of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
+
+ recent : If set to a true value, search for messages flagged with
+ \\Recent
+
+ seen : If set to a true value, search for messages flagged with
+ \\Seen
+
+ sentbefore : Search for messages with an RFC822 'Date' header before
+ this date. The given date should be a string in the format
+ of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
+
+ senton : Search for messages with an RFC822 'Date' header which is
+ on this date The given date should be a string in the format
+ of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
+
+ sentsince : Search for messages with an RFC822 'Date' header which is
+ after this date. The given date should be a string in the format
+ of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
+
+ since : Search for messages with an internal date that is after
+ this date.. The given date should be a string in the format
+ of 'DD-Mon-YYYY'. For example, '03-Mar-2003'.
+
+ smaller : Search for messages smaller than this number of octets
+
+ subject : A substring to search the 'subject' header for
+
+ text : A substring to search the entire message for
+
+ to : A substring to search the 'to' header for
+
+ uid : Search only the messages in the given message set
+
+ unanswered : If set to a true value, search for messages not
+ flagged with \\Answered
+
+ undeleted : If set to a true value, search for messages not
+ flagged with \\Deleted
+
+ undraft : If set to a true value, search for messages not
+ flagged with \\Draft
+
+ unflagged : If set to a true value, search for messages not
+ flagged with \\Flagged
+
+ unkeyword : Search for messages without the given keyword set
+
+ unseen : If set to a true value, search for messages not
+ flagged with \\Seen
+
+ @type sorted: C{bool}
+ @param sorted: If true, the output will be sorted, alphabetically.
+ The standard does not require it, but it makes testing this function
+ easier. The default is zero, and this should be acceptable for any
+ application.
+
+ @rtype: L{str}
+ @return: The formatted query string
+ """
+ cmd = []
+ keys = kwarg.keys()
+ if sorted:
+ keys = _sorted(keys)
+ for k in keys:
+ v = kwarg[k]
+ k = k.upper()
+ if k in _SIMPLE_BOOL and v:
+ cmd.append(k)
+ elif k == "HEADER":
+ cmd.extend([k, str(v[0]), str(v[1])])
+ elif k == "KEYWORD" or k == "UNKEYWORD":
+ # Discard anything that does not fit into an "atom". Perhaps turn
+ # the case where this actually removes bytes from the value into a
+ # warning and then an error, eventually. See #6277.
+ v = _nonAtomRE.sub("", v)
+ cmd.extend([k, v])
+ elif k not in _NO_QUOTES:
+ if isinstance(v, MessageSet):
+ fmt = '"%s"'
+ elif isinstance(v, str):
+ fmt = '"%s"'
+ else:
+ fmt = '"%d"'
+ cmd.extend([k, fmt % (v,)])
+ elif isinstance(v, int):
+ cmd.extend([k, "%d" % (v,)])
+ else:
+ cmd.extend([k, f"{v}"])
+ if len(cmd) > 1:
+ return "(" + " ".join(cmd) + ")"
+ else:
+ return " ".join(cmd)
+
+
+def Or(*args):
+ """
+ The disjunction of two or more queries
+ """
+ if len(args) < 2:
+ raise IllegalQueryError(args)
+ elif len(args) == 2:
+ return "(OR %s %s)" % args
+ else:
+ return f"(OR {args[0]} {Or(*args[1:])})"
+
+
+def Not(query):
+ """The negation of a query"""
+ return f"(NOT {query})"
+
+
+def wildcardToRegexp(wildcard, delim=None):
+ wildcard = wildcard.replace("*", "(?:.*?)")
+ if delim is None:
+ wildcard = wildcard.replace("%", "(?:.*?)")
+ else:
+ wildcard = wildcard.replace("%", "(?:(?:[^%s])*?)" % re.escape(delim))
+ return re.compile(wildcard, re.I)
+
+
+def splitQuoted(s):
+ """
+ Split a string into whitespace delimited tokens
+
+ Tokens that would otherwise be separated but are surrounded by \"
+ remain as a single token. Any token that is not quoted and is
+ equal to \"NIL\" is tokenized as L{None}.
+
+ @type s: L{bytes}
+ @param s: The string to be split
+
+ @rtype: L{list} of L{bytes}
+ @return: A list of the resulting tokens
+
+ @raise MismatchedQuoting: Raised if an odd number of quotes are present
+ """
+ s = s.strip()
+ result = []
+ word = []
+ inQuote = inWord = False
+ qu = _matchingString('"', s)
+ esc = _matchingString("\x5c", s)
+ empty = _matchingString("", s)
+ nil = _matchingString("NIL", s)
+ for i, c in enumerate(iterbytes(s)):
+ if c == qu:
+ if i and s[i - 1 : i] == esc:
+ word.pop()
+ word.append(qu)
+ elif not inQuote:
+ inQuote = True
+ else:
+ inQuote = False
+ result.append(empty.join(word))
+ word = []
+ elif (
+ not inWord
+ and not inQuote
+ and c not in (qu + (string.whitespace.encode("ascii")))
+ ):
+ inWord = True
+ word.append(c)
+ elif inWord and not inQuote and c in string.whitespace.encode("ascii"):
+ w = empty.join(word)
+ if w == nil:
+ result.append(None)
+ else:
+ result.append(w)
+ word = []
+ inWord = False
+ elif inWord or inQuote:
+ word.append(c)
+
+ if inQuote:
+ raise MismatchedQuoting(s)
+ if inWord:
+ w = empty.join(word)
+ if w == nil:
+ result.append(None)
+ else:
+ result.append(w)
+
+ return result
+
+
+def splitOn(sequence, predicate, transformers):
+ result = []
+ mode = predicate(sequence[0])
+ tmp = [sequence[0]]
+ for e in sequence[1:]:
+ p = predicate(e)
+ if p != mode:
+ result.extend(transformers[mode](tmp))
+ tmp = [e]
+ mode = p
+ else:
+ tmp.append(e)
+ result.extend(transformers[mode](tmp))
+ return result
+
+
+def collapseStrings(results):
+ """
+ Turns a list of length-one strings and lists into a list of longer
+ strings and lists. For example,
+
+ ['a', 'b', ['c', 'd']] is returned as ['ab', ['cd']]
+
+ @type results: L{list} of L{bytes} and L{list}
+ @param results: The list to be collapsed
+
+ @rtype: L{list} of L{bytes} and L{list}
+ @return: A new list which is the collapsed form of C{results}
+ """
+ copy = []
+ begun = None
+
+ pred = lambda e: isinstance(e, tuple)
+ tran = {
+ 0: lambda e: splitQuoted(b"".join(e)),
+ 1: lambda e: [b"".join([i[0] for i in e])],
+ }
+ for i, c in enumerate(results):
+ if isinstance(c, list):
+ if begun is not None:
+ copy.extend(splitOn(results[begun:i], pred, tran))
+ begun = None
+ copy.append(collapseStrings(c))
+ elif begun is None:
+ begun = i
+ if begun is not None:
+ copy.extend(splitOn(results[begun:], pred, tran))
+ return copy
+
+
+def parseNestedParens(s, handleLiteral=1):
+ """
+ Parse an s-exp-like string into a more useful data structure.
+
+ @type s: L{bytes}
+ @param s: The s-exp-like string to parse
+
+ @rtype: L{list} of L{bytes} and L{list}
+ @return: A list containing the tokens present in the input.
+
+ @raise MismatchedNesting: Raised if the number or placement
+ of opening or closing parenthesis is invalid.
+ """
+ s = s.strip()
+ inQuote = 0
+ contentStack = [[]]
+ try:
+ i = 0
+ L = len(s)
+ while i < L:
+ c = s[i : i + 1]
+ if inQuote:
+ if c == b"\\":
+ contentStack[-1].append(s[i : i + 2])
+ i += 2
+ continue
+ elif c == b'"':
+ inQuote = not inQuote
+ contentStack[-1].append(c)
+ i += 1
+ else:
+ if c == b'"':
+ contentStack[-1].append(c)
+ inQuote = not inQuote
+ i += 1
+ elif handleLiteral and c == b"{":
+ end = s.find(b"}", i)
+ if end == -1:
+ raise ValueError("Malformed literal")
+ literalSize = int(s[i + 1 : end])
+ contentStack[-1].append((s[end + 3 : end + 3 + literalSize],))
+ i = end + 3 + literalSize
+ elif c == b"(" or c == b"[":
+ contentStack.append([])
+ i += 1
+ elif c == b")" or c == b"]":
+ contentStack[-2].append(contentStack.pop())
+ i += 1
+ else:
+ contentStack[-1].append(c)
+ i += 1
+ except IndexError:
+ raise MismatchedNesting(s)
+ if len(contentStack) != 1:
+ raise MismatchedNesting(s)
+ return collapseStrings(contentStack[0])
+
+
+def _quote(s):
+ qu = _matchingString('"', s)
+ esc = _matchingString("\x5c", s)
+ return qu + s.replace(esc, esc + esc).replace(qu, esc + qu) + qu
+
+
+def _literal(s: bytes) -> bytes:
+ return b"{%d}\r\n%b" % (len(s), s)
+
+
+class DontQuoteMe:
+ def __init__(self, value):
+ self.value = value
+
+ def __str__(self) -> str:
+ return str(self.value)
+
+
+_ATOM_SPECIALS = b'(){ %*"'
+
+
+def _needsQuote(s):
+ if s == b"":
+ return 1
+ for c in iterbytes(s):
+ if c < b"\x20" or c > b"\x7f":
+ return 1
+ if c in _ATOM_SPECIALS:
+ return 1
+ return 0
+
+
+def _parseMbox(name):
+ if isinstance(name, str):
+ return name
+ try:
+ return name.decode("imap4-utf-7")
+ except BaseException:
+ log.err()
+ raise IllegalMailboxEncoding(name)
+
+
+def _prepareMailboxName(name):
+ if not isinstance(name, str):
+ name = name.decode("charmap")
+ name = name.encode("imap4-utf-7")
+ if _needsQuote(name):
+ return _quote(name)
+ return name
+
+
+def _needsLiteral(s):
+ # change this to "return 1" to wig out stupid clients
+ cr = _matchingString("\n", s)
+ lf = _matchingString("\r", s)
+ return cr in s or lf in s or len(s) > 1000
+
+
+def collapseNestedLists(items):
+ """
+ Turn a nested list structure into an s-exp-like string.
+
+ Strings in C{items} will be sent as literals if they contain CR or LF,
+ otherwise they will be quoted. References to None in C{items} will be
+ translated to the atom NIL. Objects with a 'read' attribute will have
+ it called on them with no arguments and the returned string will be
+ inserted into the output as a literal. Integers will be converted to
+ strings and inserted into the output unquoted. Instances of
+ C{DontQuoteMe} will be converted to strings and inserted into the output
+ unquoted.
+
+ This function used to be much nicer, and only quote things that really
+ needed to be quoted (and C{DontQuoteMe} did not exist), however, many
+ broken IMAP4 clients were unable to deal with this level of sophistication,
+ forcing the current behavior to be adopted for practical reasons.
+
+ @type items: Any iterable
+
+ @rtype: L{str}
+ """
+ pieces = []
+ for i in items:
+ if isinstance(i, str):
+ # anything besides ASCII will have to wait for an RFC 5738
+ # implementation. See
+ # https://twistedmatrix.com/trac/ticket/9258
+ i = i.encode("ascii")
+ if i is None:
+ pieces.extend([b" ", b"NIL"])
+ elif isinstance(i, int):
+ pieces.extend([b" ", networkString(str(i))])
+ elif isinstance(i, DontQuoteMe):
+ pieces.extend([b" ", i.value])
+ elif isinstance(i, bytes):
+ # XXX warning
+ if _needsLiteral(i):
+ pieces.extend([b" ", b"{%d}" % (len(i),), IMAP4Server.delimiter, i])
+ else:
+ pieces.extend([b" ", _quote(i)])
+ elif hasattr(i, "read"):
+ d = i.read()
+ pieces.extend([b" ", b"{%d}" % (len(d),), IMAP4Server.delimiter, d])
+ else:
+ pieces.extend([b" ", b"(" + collapseNestedLists(i) + b")"])
+ return b"".join(pieces[1:])
+
+
+@implementer(IAccount)
+class MemoryAccountWithoutNamespaces:
+ mailboxes = None
+ subscriptions = None
+ top_id = 0
+
+ def __init__(self, name):
+ self.name = name
+ self.mailboxes = {}
+ self.subscriptions = []
+
+ def allocateID(self):
+ id = self.top_id
+ self.top_id += 1
+ return id
+
+ ##
+ ## IAccount
+ ##
+ def addMailbox(self, name, mbox=None):
+ name = _parseMbox(name.upper())
+ if name in self.mailboxes:
+ raise MailboxCollision(name)
+ if mbox is None:
+ mbox = self._emptyMailbox(name, self.allocateID())
+ self.mailboxes[name] = mbox
+ return 1
+
+ def create(self, pathspec):
+ paths = [path for path in pathspec.split("/") if path]
+ for accum in range(1, len(paths)):
+ try:
+ self.addMailbox("/".join(paths[:accum]))
+ except MailboxCollision:
+ pass
+ try:
+ self.addMailbox("/".join(paths))
+ except MailboxCollision:
+ if not pathspec.endswith("/"):
+ return False
+ return True
+
+ def _emptyMailbox(self, name, id):
+ raise NotImplementedError
+
+ def select(self, name, readwrite=1):
+ return self.mailboxes.get(_parseMbox(name.upper()))
+
+ def delete(self, name):
+ name = _parseMbox(name.upper())
+ # See if this mailbox exists at all
+ mbox = self.mailboxes.get(name)
+ if not mbox:
+ raise MailboxException("No such mailbox")
+ # See if this box is flagged \Noselect
+ if r"\Noselect" in mbox.getFlags():
+ # Check for hierarchically inferior mailboxes with this one
+ # as part of their root.
+ for others in self.mailboxes.keys():
+ if others != name and others.startswith(name):
+ raise MailboxException(
+ "Hierarchically inferior mailboxes exist and \\Noselect is set"
+ )
+ mbox.destroy()
+
+ # iff there are no hierarchically inferior names, we will
+ # delete it from our ken.
+ if len(self._inferiorNames(name)) > 1:
+ raise MailboxException(f'Name "{name}" has inferior hierarchical names')
+ del self.mailboxes[name]
+
+ def rename(self, oldname, newname):
+ oldname = _parseMbox(oldname.upper())
+ newname = _parseMbox(newname.upper())
+ if oldname not in self.mailboxes:
+ raise NoSuchMailbox(oldname)
+
+ inferiors = self._inferiorNames(oldname)
+ inferiors = [(o, o.replace(oldname, newname, 1)) for o in inferiors]
+
+ for old, new in inferiors:
+ if new in self.mailboxes:
+ raise MailboxCollision(new)
+
+ for old, new in inferiors:
+ self.mailboxes[new] = self.mailboxes[old]
+ del self.mailboxes[old]
+
+ def _inferiorNames(self, name):
+ inferiors = []
+ for infname in self.mailboxes.keys():
+ if infname.startswith(name):
+ inferiors.append(infname)
+ return inferiors
+
+ def isSubscribed(self, name):
+ return _parseMbox(name.upper()) in self.subscriptions
+
+ def subscribe(self, name):
+ name = _parseMbox(name.upper())
+ if name not in self.subscriptions:
+ self.subscriptions.append(name)
+
+ def unsubscribe(self, name):
+ name = _parseMbox(name.upper())
+ if name not in self.subscriptions:
+ raise MailboxException(f"Not currently subscribed to {name}")
+ self.subscriptions.remove(name)
+
+ def listMailboxes(self, ref, wildcard):
+ ref = self._inferiorNames(_parseMbox(ref.upper()))
+ wildcard = wildcardToRegexp(wildcard, "/")
+ return [(i, self.mailboxes[i]) for i in ref if wildcard.match(i)]
+
+
+@implementer(INamespacePresenter)
+class MemoryAccount(MemoryAccountWithoutNamespaces):
+ ##
+ ## INamespacePresenter
+ ##
+ def getPersonalNamespaces(self):
+ return [[b"", b"/"]]
+
+ def getSharedNamespaces(self):
+ return None
+
+ def getOtherNamespaces(self):
+ return None
+
+ def getUserNamespaces(self):
+ # INamespacePresenter.getUserNamespaces
+ return None
+
+
+_statusRequestDict = {
+ "MESSAGES": "getMessageCount",
+ "RECENT": "getRecentCount",
+ "UIDNEXT": "getUIDNext",
+ "UIDVALIDITY": "getUIDValidity",
+ "UNSEEN": "getUnseenCount",
+}
+
+
+def statusRequestHelper(mbox, names):
+ r = {}
+ for n in names:
+ r[n] = getattr(mbox, _statusRequestDict[n.upper()])()
+ return r
+
+
+def parseAddr(addr):
+ if addr is None:
+ return [
+ (None, None, None),
+ ]
+ addr = email.utils.getaddresses([addr])
+ return [[fn or None, None] + address.split("@") for fn, address in addr]
+
+
+def getEnvelope(msg):
+ headers = msg.getHeaders(True)
+ date = headers.get("date")
+ subject = headers.get("subject")
+ from_ = headers.get("from")
+ sender = headers.get("sender", from_)
+ reply_to = headers.get("reply-to", from_)
+ to = headers.get("to")
+ cc = headers.get("cc")
+ bcc = headers.get("bcc")
+ in_reply_to = headers.get("in-reply-to")
+ mid = headers.get("message-id")
+ return (
+ date,
+ subject,
+ parseAddr(from_),
+ parseAddr(sender),
+ reply_to and parseAddr(reply_to),
+ to and parseAddr(to),
+ cc and parseAddr(cc),
+ bcc and parseAddr(bcc),
+ in_reply_to,
+ mid,
+ )
+
+
+def getLineCount(msg):
+ # XXX - Super expensive, CACHE THIS VALUE FOR LATER RE-USE
+ # XXX - This must be the number of lines in the ENCODED version
+ lines = 0
+ for _ in msg.getBodyFile():
+ lines += 1
+ return lines
+
+
+def unquote(s):
+ if s[0] == s[-1] == '"':
+ return s[1:-1]
+ return s
+
+
+def _getContentType(msg):
+ """
+ Return a two-tuple of the main and subtype of the given message.
+ """
+ attrs = None
+ mm = msg.getHeaders(False, "content-type").get("content-type", "")
+ mm = "".join(mm.splitlines())
+ if mm:
+ mimetype = mm.split(";")
+ type = mimetype[0].split("/", 1)
+ if len(type) == 1:
+ major = type[0]
+ minor = None
+ else:
+ # length must be 2, because of split('/', 1)
+ major, minor = type
+ attrs = dict(x.strip().lower().split("=", 1) for x in mimetype[1:])
+ else:
+ major = minor = None
+ return major, minor, attrs
+
+
+def _getMessageStructure(message):
+ """
+ Construct an appropriate type of message structure object for the given
+ message object.
+
+ @param message: A L{IMessagePart} provider
+
+ @return: A L{_MessageStructure} instance of the most specific type available
+ for the given message, determined by inspecting the MIME type of the
+ message.
+ """
+ main, subtype, attrs = _getContentType(message)
+ if main is not None:
+ main = main.lower()
+ if subtype is not None:
+ subtype = subtype.lower()
+ if main == "multipart":
+ return _MultipartMessageStructure(message, subtype, attrs)
+ elif (main, subtype) == ("message", "rfc822"):
+ return _RFC822MessageStructure(message, main, subtype, attrs)
+ elif main == "text":
+ return _TextMessageStructure(message, main, subtype, attrs)
+ else:
+ return _SinglepartMessageStructure(message, main, subtype, attrs)
+
+
+class _MessageStructure:
+ """
+ L{_MessageStructure} is a helper base class for message structure classes
+ representing the structure of particular kinds of messages, as defined by
+ their MIME type.
+ """
+
+ def __init__(self, message, attrs):
+ """
+ @param message: An L{IMessagePart} provider which this structure object
+ reports on.
+
+ @param attrs: A C{dict} giving the parameters of the I{Content-Type}
+ header of the message.
+ """
+ self.message = message
+ self.attrs = attrs
+
+ def _disposition(self, disp):
+ """
+ Parse a I{Content-Disposition} header into a two-sequence of the
+ disposition and a flattened list of its parameters.
+
+ @return: L{None} if there is no disposition header value, a L{list} with
+ two elements otherwise.
+ """
+ if disp:
+ disp = disp.split("; ")
+ if len(disp) == 1:
+ disp = (disp[0].lower(), None)
+ elif len(disp) > 1:
+ # XXX Poorly tested parser
+ params = [x for param in disp[1:] for x in param.split("=", 1)]
+ disp = [disp[0].lower(), params]
+ return disp
+ else:
+ return None
+
+ def _unquotedAttrs(self):
+ """
+ @return: The I{Content-Type} parameters, unquoted, as a flat list with
+ each Nth element giving a parameter name and N+1th element giving
+ the corresponding parameter value.
+ """
+ if self.attrs:
+ unquoted = [(k, unquote(v)) for (k, v) in self.attrs.items()]
+ return [y for x in sorted(unquoted) for y in x]
+ return None
+
+
+class _SinglepartMessageStructure(_MessageStructure):
+ """
+ L{_SinglepartMessageStructure} represents the message structure of a
+ non-I{multipart/*} message.
+ """
+
+ _HEADERS = ["content-id", "content-description", "content-transfer-encoding"]
+
+ def __init__(self, message, main, subtype, attrs):
+ """
+ @param message: An L{IMessagePart} provider which this structure object
+ reports on.
+
+ @param main: A L{str} giving the main MIME type of the message (for
+ example, C{"text"}).
+
+ @param subtype: A L{str} giving the MIME subtype of the message (for
+ example, C{"plain"}).
+
+ @param attrs: A C{dict} giving the parameters of the I{Content-Type}
+ header of the message.
+ """
+ _MessageStructure.__init__(self, message, attrs)
+ self.main = main
+ self.subtype = subtype
+ self.attrs = attrs
+
+ def _basicFields(self):
+ """
+ Return a list of the basic fields for a single-part message.
+ """
+ headers = self.message.getHeaders(False, *self._HEADERS)
+
+ # Number of octets total
+ size = self.message.getSize()
+
+ major, minor = self.main, self.subtype
+
+ # content-type parameter list
+ unquotedAttrs = self._unquotedAttrs()
+
+ return [
+ major,
+ minor,
+ unquotedAttrs,
+ headers.get("content-id"),
+ headers.get("content-description"),
+ headers.get("content-transfer-encoding"),
+ size,
+ ]
+
+ def encode(self, extended):
+ """
+ Construct and return a list of the basic and extended fields for a
+ single-part message. The list suitable to be encoded into a BODY or
+ BODYSTRUCTURE response.
+ """
+ result = self._basicFields()
+ if extended:
+ result.extend(self._extended())
+ return result
+
+ def _extended(self):
+ """
+ The extension data of a non-multipart body part are in the
+ following order:
+
+ 1. body MD5
+
+ A string giving the body MD5 value as defined in [MD5].
+
+ 2. body disposition
+
+ A parenthesized list with the same content and function as
+ the body disposition for a multipart body part.
+
+ 3. body language
+
+ A string or parenthesized list giving the body language
+ value as defined in [LANGUAGE-TAGS].
+
+ 4. body location
+
+ A string list giving the body content URI as defined in
+ [LOCATION].
+
+ """
+ result = []
+ headers = self.message.getHeaders(
+ False,
+ "content-md5",
+ "content-disposition",
+ "content-language",
+ "content-language",
+ )
+
+ result.append(headers.get("content-md5"))
+ result.append(self._disposition(headers.get("content-disposition")))
+ result.append(headers.get("content-language"))
+ result.append(headers.get("content-location"))
+
+ return result
+
+
+class _TextMessageStructure(_SinglepartMessageStructure):
+ """
+ L{_TextMessageStructure} represents the message structure of a I{text/*}
+ message.
+ """
+
+ def encode(self, extended):
+ """
+ A body type of type TEXT contains, immediately after the basic
+ fields, the size of the body in text lines. Note that this
+ size is the size in its content transfer encoding and not the
+ resulting size after any decoding.
+ """
+ result = _SinglepartMessageStructure._basicFields(self)
+ result.append(getLineCount(self.message))
+ if extended:
+ result.extend(self._extended())
+ return result
+
+
+class _RFC822MessageStructure(_SinglepartMessageStructure):
+ """
+ L{_RFC822MessageStructure} represents the message structure of a
+ I{message/rfc822} message.
+ """
+
+ def encode(self, extended):
+ """
+ A body type of type MESSAGE and subtype RFC822 contains,
+ immediately after the basic fields, the envelope structure,
+ body structure, and size in text lines of the encapsulated
+ message.
+ """
+ result = _SinglepartMessageStructure.encode(self, extended)
+ contained = self.message.getSubPart(0)
+ result.append(getEnvelope(contained))
+ result.append(getBodyStructure(contained, False))
+ result.append(getLineCount(contained))
+ return result
+
+
+class _MultipartMessageStructure(_MessageStructure):
+ """
+ L{_MultipartMessageStructure} represents the message structure of a
+ I{multipart/*} message.
+ """
+
+ def __init__(self, message, subtype, attrs):
+ """
+ @param message: An L{IMessagePart} provider which this structure object
+ reports on.
+
+ @param subtype: A L{str} giving the MIME subtype of the message (for
+ example, C{"plain"}).
+
+ @param attrs: A C{dict} giving the parameters of the I{Content-Type}
+ header of the message.
+ """
+ _MessageStructure.__init__(self, message, attrs)
+ self.subtype = subtype
+
+ def _getParts(self):
+ """
+ Return an iterator over all of the sub-messages of this message.
+ """
+ i = 0
+ while True:
+ try:
+ part = self.message.getSubPart(i)
+ except IndexError:
+ break
+ else:
+ yield part
+ i += 1
+
+ def encode(self, extended):
+ """
+ Encode each sub-message and added the additional I{multipart} fields.
+ """
+ result = [_getMessageStructure(p).encode(extended) for p in self._getParts()]
+ result.append(self.subtype)
+ if extended:
+ result.extend(self._extended())
+ return result
+
+ def _extended(self):
+ """
+ The extension data of a multipart body part are in the following order:
+
+ 1. body parameter parenthesized list
+ A parenthesized list of attribute/value pairs [e.g., ("foo"
+ "bar" "baz" "rag") where "bar" is the value of "foo", and
+ "rag" is the value of "baz"] as defined in [MIME-IMB].
+
+ 2. body disposition
+ A parenthesized list, consisting of a disposition type
+ string, followed by a parenthesized list of disposition
+ attribute/value pairs as defined in [DISPOSITION].
+
+ 3. body language
+ A string or parenthesized list giving the body language
+ value as defined in [LANGUAGE-TAGS].
+
+ 4. body location
+ A string list giving the body content URI as defined in
+ [LOCATION].
+ """
+ result = []
+ headers = self.message.getHeaders(
+ False, "content-language", "content-location", "content-disposition"
+ )
+
+ result.append(self._unquotedAttrs())
+ result.append(self._disposition(headers.get("content-disposition")))
+ result.append(headers.get("content-language", None))
+ result.append(headers.get("content-location", None))
+
+ return result
+
+
+def getBodyStructure(msg, extended=False):
+ """
+ RFC 3501, 7.4.2, BODYSTRUCTURE::
+
+ A parenthesized list that describes the [MIME-IMB] body structure of a
+ message. This is computed by the server by parsing the [MIME-IMB] header
+ fields, defaulting various fields as necessary.
+
+ For example, a simple text message of 48 lines and 2279 octets can have
+ a body structure of: ("TEXT" "PLAIN" ("CHARSET" "US-ASCII") NIL NIL
+ "7BIT" 2279 48)
+
+ This is represented as::
+
+ ["TEXT", "PLAIN", ["CHARSET", "US-ASCII"], None, None, "7BIT", 2279, 48]
+
+ These basic fields are documented in the RFC as:
+
+ 1. body type
+
+ A string giving the content media type name as defined in
+ [MIME-IMB].
+
+ 2. body subtype
+
+ A string giving the content subtype name as defined in
+ [MIME-IMB].
+
+ 3. body parameter parenthesized list
+
+ A parenthesized list of attribute/value pairs [e.g., ("foo"
+ "bar" "baz" "rag") where "bar" is the value of "foo" and
+ "rag" is the value of "baz"] as defined in [MIME-IMB].
+
+ 4. body id
+
+ A string giving the content id as defined in [MIME-IMB].
+
+ 5. body description
+
+ A string giving the content description as defined in
+ [MIME-IMB].
+
+ 6. body encoding
+
+ A string giving the content transfer encoding as defined in
+ [MIME-IMB].
+
+ 7. body size
+
+ A number giving the size of the body in octets. Note that this size is
+ the size in its transfer encoding and not the resulting size after any
+ decoding.
+
+ Put another way, the body structure is a list of seven elements. The
+ semantics of the elements of this list are:
+
+ 1. Byte string giving the major MIME type
+ 2. Byte string giving the minor MIME type
+ 3. A list giving the Content-Type parameters of the message
+ 4. A byte string giving the content identifier for the message part, or
+ None if it has no content identifier.
+ 5. A byte string giving the content description for the message part, or
+ None if it has no content description.
+ 6. A byte string giving the Content-Encoding of the message body
+ 7. An integer giving the number of octets in the message body
+
+ The RFC goes on::
+
+ Multiple parts are indicated by parenthesis nesting. Instead of a body
+ type as the first element of the parenthesized list, there is a sequence
+ of one or more nested body structures. The second element of the
+ parenthesized list is the multipart subtype (mixed, digest, parallel,
+ alternative, etc.).
+
+ For example, a two part message consisting of a text and a
+ BASE64-encoded text attachment can have a body structure of: (("TEXT"
+ "PLAIN" ("CHARSET" "US-ASCII") NIL NIL "7BIT" 1152 23)("TEXT" "PLAIN"
+ ("CHARSET" "US-ASCII" "NAME" "cc.diff")
+ "<960723163407.20117h@cac.washington.edu>" "Compiler diff" "BASE64" 4554
+ 73) "MIXED")
+
+ This is represented as::
+
+ [["TEXT", "PLAIN", ["CHARSET", "US-ASCII"], None, None, "7BIT", 1152,
+ 23],
+ ["TEXT", "PLAIN", ["CHARSET", "US-ASCII", "NAME", "cc.diff"],
+ "<960723163407.20117h@cac.washington.edu>", "Compiler diff",
+ "BASE64", 4554, 73],
+ "MIXED"]
+
+ In other words, a list of N + 1 elements, where N is the number of parts in
+ the message. The first N elements are structures as defined by the previous
+ section. The last element is the minor MIME subtype of the multipart
+ message.
+
+ Additionally, the RFC describes extension data::
+
+ Extension data follows the multipart subtype. Extension data is never
+ returned with the BODY fetch, but can be returned with a BODYSTRUCTURE
+ fetch. Extension data, if present, MUST be in the defined order.
+
+ The C{extended} flag controls whether extension data might be returned with
+ the normal data.
+ """
+ return _getMessageStructure(msg).encode(extended)
+
+
+def _formatHeaders(headers):
+ # TODO: This should use email.header.Header, which handles encoding
+ hdrs = [
+ ": ".join((k.title(), "\r\n".join(v.splitlines())))
+ for (k, v) in headers.items()
+ ]
+ hdrs = "\r\n".join(hdrs) + "\r\n"
+ return networkString(hdrs)
+
+
+def subparts(m):
+ i = 0
+ try:
+ while True:
+ yield m.getSubPart(i)
+ i += 1
+ except IndexError:
+ pass
+
+
+def iterateInReactor(i):
+ """
+ Consume an interator at most a single iteration per reactor iteration.
+
+ If the iterator produces a Deferred, the next iteration will not occur
+ until the Deferred fires, otherwise the next iteration will be taken
+ in the next reactor iteration.
+
+ @rtype: C{Deferred}
+ @return: A deferred which fires (with None) when the iterator is
+ exhausted or whose errback is called if there is an exception.
+ """
+ from twisted.internet import reactor
+
+ d = defer.Deferred()
+
+ def go(last):
+ try:
+ r = next(i)
+ except StopIteration:
+ d.callback(last)
+ except BaseException:
+ d.errback()
+ else:
+ if isinstance(r, defer.Deferred):
+ r.addCallback(go)
+ else:
+ reactor.callLater(0, go, r)
+
+ go(None)
+ return d
+
+
+class MessageProducer:
+ CHUNK_SIZE = 2**2**2**2
+ _uuid4 = staticmethod(uuid.uuid4)
+
+ def __init__(self, msg, buffer=None, scheduler=None):
+ """
+ Produce this message.
+
+ @param msg: The message I am to produce.
+ @type msg: L{IMessage}
+
+ @param buffer: A buffer to hold the message in. If None, I will
+ use a L{tempfile.TemporaryFile}.
+ @type buffer: file-like
+ """
+ self.msg = msg
+ if buffer is None:
+ buffer = tempfile.TemporaryFile()
+ self.buffer = buffer
+ if scheduler is None:
+ scheduler = iterateInReactor
+ self.scheduler = scheduler
+ self.write = self.buffer.write
+
+ def beginProducing(self, consumer):
+ self.consumer = consumer
+ return self.scheduler(self._produce())
+
+ def _produce(self):
+ headers = self.msg.getHeaders(True)
+ boundary = None
+ if self.msg.isMultipart():
+ content = headers.get("content-type")
+ parts = [x.split("=", 1) for x in content.split(";")[1:]]
+ parts = {k.lower().strip(): v for (k, v) in parts}
+ boundary = parts.get("boundary")
+ if boundary is None:
+ # Bastards
+ boundary = f"----={self._uuid4().hex}"
+ headers["content-type"] += f'; boundary="{boundary}"'
+ else:
+ if boundary.startswith('"') and boundary.endswith('"'):
+ boundary = boundary[1:-1]
+ boundary = networkString(boundary)
+
+ self.write(_formatHeaders(headers))
+ self.write(b"\r\n")
+ if self.msg.isMultipart():
+ for p in subparts(self.msg):
+ self.write(b"\r\n--" + boundary + b"\r\n")
+ yield MessageProducer(p, self.buffer, self.scheduler).beginProducing(
+ None
+ )
+ self.write(b"\r\n--" + boundary + b"--\r\n")
+ else:
+ f = self.msg.getBodyFile()
+ while True:
+ b = f.read(self.CHUNK_SIZE)
+ if b:
+ self.buffer.write(b)
+ yield None
+ else:
+ break
+ if self.consumer:
+ self.buffer.seek(0, 0)
+ yield FileProducer(self.buffer).beginProducing(self.consumer).addCallback(
+ lambda _: self
+ )
+
+
+class _FetchParser:
+ class Envelope:
+ # Response should be a list of fields from the message:
+ # date, subject, from, sender, reply-to, to, cc, bcc, in-reply-to,
+ # and message-id.
+ #
+ # from, sender, reply-to, to, cc, and bcc are themselves lists of
+ # address information:
+ # personal name, source route, mailbox name, host name
+ #
+ # reply-to and sender must not be None. If not present in a message
+ # they should be defaulted to the value of the from field.
+ type = "envelope"
+ __str__ = lambda self: "envelope"
+
+ class Flags:
+ type = "flags"
+ __str__ = lambda self: "flags"
+
+ class InternalDate:
+ type = "internaldate"
+ __str__ = lambda self: "internaldate"
+
+ class RFC822Header:
+ type = "rfc822header"
+ __str__ = lambda self: "rfc822.header"
+
+ class RFC822Text:
+ type = "rfc822text"
+ __str__ = lambda self: "rfc822.text"
+
+ class RFC822Size:
+ type = "rfc822size"
+ __str__ = lambda self: "rfc822.size"
+
+ class RFC822:
+ type = "rfc822"
+ __str__ = lambda self: "rfc822"
+
+ class UID:
+ type = "uid"
+ __str__ = lambda self: "uid"
+
+ class Body:
+ type = "body"
+ peek = False
+ header = None
+ mime = None
+ text = None
+ part = ()
+ empty = False
+ partialBegin = None
+ partialLength = None
+
+ def __str__(self) -> str:
+ return self.__bytes__().decode("ascii")
+
+ def __bytes__(self) -> bytes:
+ base = b"BODY"
+ part = b""
+ separator = b""
+ if self.part:
+ part = b".".join([str(x + 1).encode("ascii") for x in self.part]) # type: ignore[unreachable]
+ separator = b"."
+ # if self.peek:
+ # base += '.PEEK'
+ if self.header:
+ base += ( # type: ignore[unreachable]
+ b"[" + part + separator + str(self.header).encode("ascii") + b"]"
+ )
+ elif self.text:
+ base += b"[" + part + separator + b"TEXT]" # type: ignore[unreachable]
+ elif self.mime:
+ base += b"[" + part + separator + b"MIME]" # type: ignore[unreachable]
+ elif self.empty:
+ base += b"[" + part + b"]"
+ if self.partialBegin is not None:
+ base += b"<%d.%d>" % (self.partialBegin, self.partialLength) # type: ignore[unreachable]
+ return base
+
+ class BodyStructure:
+ type = "bodystructure"
+ __str__ = lambda self: "bodystructure"
+
+ # These three aren't top-level, they don't need type indicators
+ class Header:
+ negate = False
+ fields = None
+ part = None
+
+ def __str__(self) -> str:
+ return self.__bytes__().decode("ascii")
+
+ def __bytes__(self) -> bytes:
+ base = b"HEADER"
+ if self.fields:
+ base += b".FIELDS" # type: ignore[unreachable]
+ if self.negate:
+ base += b".NOT"
+ fields = []
+ for f in self.fields:
+ f = f.title()
+ if _needsQuote(f):
+ f = _quote(f)
+ fields.append(f)
+ base += b" (" + b" ".join(fields) + b")"
+ if self.part:
+ # TODO: _FetchParser never assigns Header.part - dead
+ # code?
+ base = b".".join([(x + 1).__bytes__() for x in self.part]) + b"." + base # type: ignore[unreachable]
+ return base
+
+ class Text:
+ pass
+
+ class MIME:
+ pass
+
+ parts = None
+
+ _simple_fetch_att = [
+ (b"envelope", Envelope),
+ (b"flags", Flags),
+ (b"internaldate", InternalDate),
+ (b"rfc822.header", RFC822Header),
+ (b"rfc822.text", RFC822Text),
+ (b"rfc822.size", RFC822Size),
+ (b"rfc822", RFC822),
+ (b"uid", UID),
+ (b"bodystructure", BodyStructure),
+ ]
+
+ def __init__(self):
+ self.state = ["initial"]
+ self.result = []
+ self.remaining = b""
+
+ def parseString(self, s):
+ s = self.remaining + s
+ try:
+ while s or self.state:
+ if not self.state:
+ raise IllegalClientResponse("Invalid Argument")
+ # print 'Entering state_' + self.state[-1] + ' with', repr(s)
+ state = self.state.pop()
+ try:
+ used = getattr(self, "state_" + state)(s)
+ except BaseException:
+ self.state.append(state)
+ raise
+ else:
+ # print state, 'consumed', repr(s[:used])
+ s = s[used:]
+ finally:
+ self.remaining = s
+
+ def state_initial(self, s):
+ # In the initial state, the literals "ALL", "FULL", and "FAST"
+ # are accepted, as is a ( indicating the beginning of a fetch_att
+ # token, as is the beginning of a fetch_att token.
+ if s == b"":
+ return 0
+
+ l = s.lower()
+ if l.startswith(b"all"):
+ self.result.extend(
+ (self.Flags(), self.InternalDate(), self.RFC822Size(), self.Envelope())
+ )
+ return 3
+ if l.startswith(b"full"):
+ self.result.extend(
+ (
+ self.Flags(),
+ self.InternalDate(),
+ self.RFC822Size(),
+ self.Envelope(),
+ self.Body(),
+ )
+ )
+ return 4
+ if l.startswith(b"fast"):
+ self.result.extend(
+ (
+ self.Flags(),
+ self.InternalDate(),
+ self.RFC822Size(),
+ )
+ )
+ return 4
+
+ if l.startswith(b"("):
+ self.state.extend(("close_paren", "maybe_fetch_att", "fetch_att"))
+ return 1
+
+ self.state.append("fetch_att")
+ return 0
+
+ def state_close_paren(self, s):
+ if s.startswith(b")"):
+ return 1
+ # TODO: does maybe_fetch_att's startswith(b')') make this dead
+ # code?
+ raise Exception("Missing )")
+
+ def state_whitespace(self, s):
+ # Eat up all the leading whitespace
+ if not s or not s[0:1].isspace():
+ raise Exception("Whitespace expected, none found")
+ i = 0
+ for i in range(len(s)):
+ if not s[i : i + 1].isspace():
+ break
+ return i
+
+ def state_maybe_fetch_att(self, s):
+ if not s.startswith(b")"):
+ self.state.extend(("maybe_fetch_att", "fetch_att", "whitespace"))
+ return 0
+
+ def state_fetch_att(self, s):
+ # Allowed fetch_att tokens are "ENVELOPE", "FLAGS", "INTERNALDATE",
+ # "RFC822", "RFC822.HEADER", "RFC822.SIZE", "RFC822.TEXT", "BODY",
+ # "BODYSTRUCTURE", "UID",
+ # "BODY [".PEEK"] [<section>] ["<" <number> "." <nz_number> ">"]
+
+ l = s.lower()
+ for name, cls in self._simple_fetch_att:
+ if l.startswith(name):
+ self.result.append(cls())
+ return len(name)
+
+ b = self.Body()
+ if l.startswith(b"body.peek"):
+ b.peek = True
+ used = 9
+ elif l.startswith(b"body"):
+ used = 4
+ else:
+ raise Exception(f"Nothing recognized in fetch_att: {l}")
+
+ self.pending_body = b
+ self.state.extend(("got_body", "maybe_partial", "maybe_section"))
+ return used
+
+ def state_got_body(self, s):
+ self.result.append(self.pending_body)
+ del self.pending_body
+ return 0
+
+ def state_maybe_section(self, s):
+ if not s.startswith(b"["):
+ return 0
+
+ self.state.extend(("section", "part_number"))
+ return 1
+
+ _partExpr = re.compile(rb"(\d+(?:\.\d+)*)\.?")
+
+ def state_part_number(self, s):
+ m = self._partExpr.match(s)
+ if m is not None:
+ self.parts = [int(p) - 1 for p in m.groups()[0].split(b".")]
+ return m.end()
+ else:
+ self.parts = []
+ return 0
+
+ def state_section(self, s):
+ # Grab "HEADER]" or "HEADER.FIELDS (Header list)]" or
+ # "HEADER.FIELDS.NOT (Header list)]" or "TEXT]" or "MIME]" or
+ # just "]".
+
+ l = s.lower()
+ used = 0
+ if l.startswith(b"]"):
+ self.pending_body.empty = True
+ used += 1
+ elif l.startswith(b"header]"):
+ h = self.pending_body.header = self.Header()
+ h.negate = True
+ h.fields = ()
+ used += 7
+ elif l.startswith(b"text]"):
+ self.pending_body.text = self.Text()
+ used += 5
+ elif l.startswith(b"mime]"):
+ self.pending_body.mime = self.MIME()
+ used += 5
+ else:
+ h = self.Header()
+ if l.startswith(b"header.fields.not"):
+ h.negate = True
+ used += 17
+ elif l.startswith(b"header.fields"):
+ used += 13
+ else:
+ raise Exception(f"Unhandled section contents: {l!r}")
+
+ self.pending_body.header = h
+ self.state.extend(("finish_section", "header_list", "whitespace"))
+ self.pending_body.part = tuple(self.parts)
+ self.parts = None
+ return used
+
+ def state_finish_section(self, s):
+ if not s.startswith(b"]"):
+ raise Exception("section must end with ]")
+ return 1
+
+ def state_header_list(self, s):
+ if not s.startswith(b"("):
+ raise Exception("Header list must begin with (")
+ end = s.find(b")")
+ if end == -1:
+ raise Exception("Header list must end with )")
+
+ headers = s[1:end].split()
+ self.pending_body.header.fields = [h.upper() for h in headers]
+ return end + 1
+
+ def state_maybe_partial(self, s):
+ # Grab <number.number> or nothing at all
+ if not s.startswith(b"<"):
+ return 0
+ end = s.find(b">")
+ if end == -1:
+ raise Exception("Found < but not >")
+
+ partial = s[1:end]
+ parts = partial.split(b".", 1)
+ if len(parts) != 2:
+ raise Exception(
+ "Partial specification did not include two .-delimited integers"
+ )
+ begin, length = map(int, parts)
+ self.pending_body.partialBegin = begin
+ self.pending_body.partialLength = length
+
+ return end + 1
+
+
+class FileProducer:
+ CHUNK_SIZE = 2**2**2**2
+
+ firstWrite = True
+
+ def __init__(self, f):
+ self.f = f
+
+ def beginProducing(self, consumer):
+ self.consumer = consumer
+ self.produce = consumer.write
+ d = self._onDone = defer.Deferred()
+ self.consumer.registerProducer(self, False)
+ return d
+
+ def resumeProducing(self):
+ b = b""
+ if self.firstWrite:
+ b = b"{%d}\r\n" % (self._size(),)
+ self.firstWrite = False
+ if not self.f:
+ return
+ b = b + self.f.read(self.CHUNK_SIZE)
+ if not b:
+ self.consumer.unregisterProducer()
+ self._onDone.callback(self)
+ self._onDone = self.f = self.consumer = None
+ else:
+ self.produce(b)
+
+ def pauseProducing(self):
+ """
+ Pause the producer. This does nothing.
+ """
+
+ def stopProducing(self):
+ """
+ Stop the producer. This does nothing.
+ """
+
+ def _size(self):
+ b = self.f.tell()
+ self.f.seek(0, 2)
+ e = self.f.tell()
+ self.f.seek(b, 0)
+ return e - b
+
+
+def parseTime(s):
+ # XXX - This may require localization :(
+ months = [
+ "jan",
+ "feb",
+ "mar",
+ "apr",
+ "may",
+ "jun",
+ "jul",
+ "aug",
+ "sep",
+ "oct",
+ "nov",
+ "dec",
+ "january",
+ "february",
+ "march",
+ "april",
+ "may",
+ "june",
+ "july",
+ "august",
+ "september",
+ "october",
+ "november",
+ "december",
+ ]
+ expr = {
+ "day": r"(?P<day>3[0-1]|[1-2]\d|0[1-9]|[1-9]| [1-9])",
+ "mon": r"(?P<mon>\w+)",
+ "year": r"(?P<year>\d\d\d\d)",
+ }
+ m = re.match("%(day)s-%(mon)s-%(year)s" % expr, s)
+ if not m:
+ raise ValueError(f"Cannot parse time string {s!r}")
+ d = m.groupdict()
+ try:
+ d["mon"] = 1 + (months.index(d["mon"].lower()) % 12)
+ d["year"] = int(d["year"])
+ d["day"] = int(d["day"])
+ except ValueError:
+ raise ValueError(f"Cannot parse time string {s!r}")
+ else:
+ return time.struct_time((d["year"], d["mon"], d["day"], 0, 0, 0, -1, -1, -1))
+
+
+# we need to cast Python >=3.3 memoryview to chars (from unsigned bytes), but
+# cast is absent in previous versions: thus, the lambda returns the
+# memoryview instance while ignoring the format
+memory_cast = getattr(memoryview, "cast", lambda *x: x[0])
+
+
+def modified_base64(s):
+ s_utf7 = s.encode("utf-7")
+ return s_utf7[1:-1].replace(b"/", b",")
+
+
+def modified_unbase64(s):
+ s_utf7 = b"+" + s.replace(b",", b"/") + b"-"
+ return s_utf7.decode("utf-7")
+
+
+def encoder(s, errors=None):
+ """
+ Encode the given C{unicode} string using the IMAP4 specific variation of
+ UTF-7.
+
+ @type s: C{unicode}
+ @param s: The text to encode.
+
+ @param errors: Policy for handling encoding errors. Currently ignored.
+
+ @return: L{tuple} of a L{str} giving the encoded bytes and an L{int}
+ giving the number of code units consumed from the input.
+ """
+ r = bytearray()
+ _in = []
+ valid_chars = set(map(chr, range(0x20, 0x7F))) - {"&"}
+ for c in s:
+ if c in valid_chars:
+ if _in:
+ r += b"&" + modified_base64("".join(_in)) + b"-"
+ del _in[:]
+ r.append(ord(c))
+ elif c == "&":
+ if _in:
+ r += b"&" + modified_base64("".join(_in)) + b"-"
+ del _in[:]
+ r += b"&-"
+ else:
+ _in.append(c)
+ if _in:
+ r.extend(b"&" + modified_base64("".join(_in)) + b"-")
+ return (bytes(r), len(s))
+
+
+def decoder(s, errors=None):
+ """
+ Decode the given L{str} using the IMAP4 specific variation of UTF-7.
+
+ @type s: L{str}
+ @param s: The bytes to decode.
+
+ @param errors: Policy for handling decoding errors. Currently ignored.
+
+ @return: a L{tuple} of a C{unicode} string giving the text which was
+ decoded and an L{int} giving the number of bytes consumed from the
+ input.
+ """
+ r = []
+ decode = []
+ s = memory_cast(memoryview(s), "c")
+ for c in s:
+ if c == b"&" and not decode:
+ decode.append(b"&")
+ elif c == b"-" and decode:
+ if len(decode) == 1:
+ r.append("&")
+ else:
+ r.append(modified_unbase64(b"".join(decode[1:])))
+ decode = []
+ elif decode:
+ decode.append(c)
+ else:
+ r.append(c.decode())
+ if decode:
+ r.append(modified_unbase64(b"".join(decode[1:])))
+ return ("".join(r), len(s))
+
+
+class StreamReader(codecs.StreamReader):
+ def decode(self, s, errors="strict"):
+ return decoder(s)
+
+
+class StreamWriter(codecs.StreamWriter):
+ def encode(self, s, errors="strict"):
+ return encoder(s)
+
+
+_codecInfo = codecs.CodecInfo(encoder, decoder, StreamReader, StreamWriter)
+
+
+def imap4_utf_7(name):
+ # In Python 3.9, codecs.lookup() was changed to normalize the codec name
+ # in the same way as encodings.normalize_encoding(). The docstring
+ # for encodings.normalize_encoding() describes how the codec name is
+ # normalized. We need to replace '-' with '_' to be compatible with
+ # older Python versions.
+ # See: https://bugs.python.org/issue37751
+ # https://github.com/python/cpython/pull/17997
+ if name.replace("-", "_") == "imap4_utf_7":
+ return _codecInfo
+
+
+codecs.register(imap4_utf_7)
+
+__all__ = [
+ # Protocol classes
+ "IMAP4Server",
+ "IMAP4Client",
+ # Interfaces
+ "IMailboxListener",
+ "IClientAuthentication",
+ "IAccount",
+ "IMailbox",
+ "INamespacePresenter",
+ "ICloseableMailbox",
+ "IMailboxInfo",
+ "IMessage",
+ "IMessageCopier",
+ "IMessageFile",
+ "ISearchableMailbox",
+ "IMessagePart",
+ # Exceptions
+ "IMAP4Exception",
+ "IllegalClientResponse",
+ "IllegalOperation",
+ "IllegalMailboxEncoding",
+ "UnhandledResponse",
+ "NegativeResponse",
+ "NoSupportedAuthentication",
+ "IllegalServerResponse",
+ "IllegalIdentifierError",
+ "IllegalQueryError",
+ "MismatchedNesting",
+ "MismatchedQuoting",
+ "MailboxException",
+ "MailboxCollision",
+ "NoSuchMailbox",
+ "ReadOnlyMailbox",
+ # Auth objects
+ "CramMD5ClientAuthenticator",
+ "PLAINAuthenticator",
+ "LOGINAuthenticator",
+ "PLAINCredentials",
+ "LOGINCredentials",
+ # Simple query interface
+ "Query",
+ "Not",
+ "Or",
+ # Miscellaneous
+ "MemoryAccount",
+ "statusRequestHelper",
+]
diff --git a/contrib/python/Twisted/py3/twisted/mail/interfaces.py b/contrib/python/Twisted/py3/twisted/mail/interfaces.py
new file mode 100644
index 0000000000..dd87d35a63
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/interfaces.py
@@ -0,0 +1,1050 @@
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Interfaces for L{twisted.mail}.
+
+@since: 16.5
+"""
+
+
+from zope.interface import Interface
+
+
+class IChallengeResponse(Interface):
+ """
+ An C{IMAPrev4} authorization challenge mechanism.
+ """
+
+ def getChallenge():
+ """
+ Return a client challenge.
+
+ @return: A challenge.
+ @rtype: L{bytes}
+ """
+
+ def setResponse(response):
+ """
+ Extract a username and possibly a password from a response and
+ assign them to C{username} and C{password} instance variables.
+
+ @param response: A decoded response.
+ @type response: L{bytes}
+
+ @see: L{credentials.IUsernamePassword} or
+ L{credentials.IUsernameHashedPassword}
+ """
+
+ def moreChallenges():
+ """
+ Are there more challenges than just the first? If so, callers
+ should challenge clients with the result of L{getChallenge},
+ and check their response with L{setResponse} in a loop until
+ this returns L{False}
+
+ @return: Are there more challenges?
+ @rtype: L{bool}
+ """
+
+
+class IClientAuthentication(Interface):
+ def getName():
+ """
+ Return an identifier associated with this authentication scheme.
+
+ @rtype: L{bytes}
+ """
+
+ def challengeResponse(secret, challenge):
+ """
+ Generate a challenge response string.
+ """
+
+
+class IServerFactoryPOP3(Interface):
+ """
+ An interface for querying capabilities of a POP3 server.
+
+ Any cap_* method may raise L{NotImplementedError} if the particular
+ capability is not supported. If L{cap_EXPIRE()} does not raise
+ L{NotImplementedError}, L{perUserExpiration()} must be implemented,
+ otherwise they are optional. If L{cap_LOGIN_DELAY()} is implemented,
+ L{perUserLoginDelay()} must be implemented, otherwise they are optional.
+
+ @type challengers: L{dict} of L{bytes} -> L{IUsernameHashedPassword
+ <cred.credentials.IUsernameHashedPassword>}
+ @ivar challengers: A mapping of challenger names to
+ L{IUsernameHashedPassword <cred.credentials.IUsernameHashedPassword>}
+ provider.
+ """
+
+ def cap_IMPLEMENTATION():
+ """
+ Return a string describing the POP3 server implementation.
+
+ @rtype: L{bytes}
+ @return: Server implementation information.
+ """
+
+ def cap_EXPIRE():
+ """
+ Return the minimum number of days messages are retained.
+
+ @rtype: L{int} or L{None}
+ @return: The minimum number of days messages are retained or none, if
+ the server never deletes messages.
+ """
+
+ def perUserExpiration():
+ """
+ Indicate whether the message expiration policy differs per user.
+
+ @rtype: L{bool}
+ @return: C{True} when the message expiration policy differs per user,
+ C{False} otherwise.
+ """
+
+ def cap_LOGIN_DELAY():
+ """
+ Return the minimum number of seconds between client logins.
+
+ @rtype: L{int}
+ @return: The minimum number of seconds between client logins.
+ """
+
+ def perUserLoginDelay():
+ """
+ Indicate whether the login delay period differs per user.
+
+ @rtype: L{bool}
+ @return: C{True} when the login delay differs per user, C{False}
+ otherwise.
+ """
+
+
+class IMailboxPOP3(Interface):
+ """
+ An interface for mailbox access.
+
+ Message indices are 0-based.
+
+ @type loginDelay: L{int}
+ @ivar loginDelay: The number of seconds between allowed logins for the
+ user associated with this mailbox.
+
+ @type messageExpiration: L{int}
+ @ivar messageExpiration: The number of days messages in this mailbox will
+ remain on the server before being deleted.
+ """
+
+ def listMessages(index=None):
+ """
+ Retrieve the size of a message, or, if none is specified, the size of
+ each message in the mailbox.
+
+ @type index: L{int} or L{None}
+ @param index: The 0-based index of the message.
+
+ @rtype: L{int}, sequence of L{int}, or L{Deferred <defer.Deferred>}
+ @return: The number of octets in the specified message, or, if an
+ index is not specified, a sequence of the number of octets for
+ all messages in the mailbox or a deferred which fires with
+ one of those. Any value which corresponds to a deleted message
+ is set to 0.
+
+ @raise ValueError or IndexError: When the index does not correspond to
+ a message in the mailbox. The use of ValueError is preferred.
+ """
+
+ def getMessage(index):
+ """
+ Retrieve a file containing the contents of a message.
+
+ @type index: L{int}
+ @param index: The 0-based index of a message.
+
+ @rtype: file-like object
+ @return: A file containing the message.
+
+ @raise ValueError or IndexError: When the index does not correspond to
+ a message in the mailbox. The use of ValueError is preferred.
+ """
+
+ def getUidl(index):
+ """
+ Get a unique identifier for a message.
+
+ @type index: L{int}
+ @param index: The 0-based index of a message.
+
+ @rtype: L{bytes}
+ @return: A string of printable characters uniquely identifying the
+ message for all time.
+
+ @raise ValueError or IndexError: When the index does not correspond to
+ a message in the mailbox. The use of ValueError is preferred.
+ """
+
+ def deleteMessage(index):
+ """
+ Mark a message for deletion.
+
+ This must not change the number of messages in this mailbox. Further
+ requests for the size of the deleted message should return 0. Further
+ requests for the message itself may raise an exception.
+
+ @type index: L{int}
+ @param index: The 0-based index of a message.
+
+ @raise ValueError or IndexError: When the index does not correspond to
+ a message in the mailbox. The use of ValueError is preferred.
+ """
+
+ def undeleteMessages():
+ """
+ Undelete all messages marked for deletion.
+
+ Any message which can be undeleted should be returned to its original
+ position in the message sequence and retain its original UID.
+ """
+
+ def sync():
+ """
+ Discard the contents of any message marked for deletion.
+ """
+
+
+class IDomain(Interface):
+ """
+ An interface for email domains.
+ """
+
+ def exists(user):
+ """
+ Check whether a user exists in this domain.
+
+ @type user: L{User}
+ @param user: A user.
+
+ @rtype: no-argument callable which returns L{IMessageSMTP} provider
+ @return: A function which takes no arguments and returns a message
+ receiver for the user.
+
+ @raise SMTPBadRcpt: When the given user does not exist in this domain.
+ """
+
+ def addUser(user, password):
+ """
+ Add a user to this domain.
+
+ @type user: L{bytes}
+ @param user: A username.
+
+ @type password: L{bytes}
+ @param password: A password.
+ """
+
+ def getCredentialsCheckers():
+ """
+ Return credentials checkers for this domain.
+
+ @rtype: L{list} of L{ICredentialsChecker
+ <twisted.cred.checkers.ICredentialsChecker>} provider
+ @return: Credentials checkers for this domain.
+ """
+
+
+class IAlias(Interface):
+ """
+ An interface for aliases.
+ """
+
+ def createMessageReceiver():
+ """
+ Create a message receiver.
+
+ @rtype: L{IMessageSMTP} provider
+ @return: A message receiver.
+ """
+
+
+class IAliasableDomain(IDomain):
+ """
+ An interface for email domains which can be aliased to other domains.
+ """
+
+ def setAliasGroup(aliases):
+ """
+ Set the group of defined aliases for this domain.
+
+ @type aliases: L{dict} of L{bytes} -> L{IAlias} provider
+ @param aliases: A mapping of domain name to alias.
+ """
+
+ def exists(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 <twisted.mail.alias.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{IMessageSMTP} provider
+ @return: A function which takes no arguments and returns a message
+ receiver for the user.
+
+ @raise SMTPBadRcpt: When the given user does not exist in this domain
+ or an alias of it.
+ """
+
+
+class IMessageDelivery(Interface):
+ def receivedHeader(helo, origin, recipients):
+ """
+ Generate the Received header for a message.
+
+ @type helo: 2-L{tuple} of L{bytes} and L{bytes}.
+ @param helo: The argument to the HELO command and the client's IP
+ address.
+
+ @type origin: L{Address}
+ @param origin: The address the message is from
+
+ @type recipients: L{list} of L{User}
+ @param recipients: A list of the addresses for which this message
+ is bound.
+
+ @rtype: L{bytes}
+ @return: The full C{"Received"} header string.
+ """
+
+ def validateTo(user):
+ """
+ Validate the address for which the message is destined.
+
+ @type user: L{User}
+ @param user: The address to validate.
+
+ @rtype: no-argument callable
+ @return: A L{Deferred} which becomes, or a callable which takes no
+ arguments and returns an object implementing L{IMessageSMTP}. This
+ will be called and the returned object used to deliver the message
+ when it arrives.
+
+ @raise SMTPBadRcpt: Raised if messages to the address are not to be
+ accepted.
+ """
+
+ def validateFrom(helo, origin):
+ """
+ Validate the address from which the message originates.
+
+ @type helo: 2-L{tuple} of L{bytes} and L{bytes}.
+ @param helo: The argument to the HELO command and the client's IP
+ address.
+
+ @type origin: L{Address}
+ @param origin: The address the message is from
+
+ @rtype: L{Deferred} or L{Address}
+ @return: C{origin} or a L{Deferred} whose callback will be
+ passed C{origin}.
+
+ @raise SMTPBadSender: Raised of messages from this address are
+ not to be accepted.
+ """
+
+
+class IMessageDeliveryFactory(Interface):
+ """
+ An alternate interface to implement for handling message delivery.
+
+ It is useful to implement this interface instead of L{IMessageDelivery}
+ directly because it allows the implementor to distinguish between different
+ messages delivery over the same connection. This can be used to optimize
+ delivery of a single message to multiple recipients, something which cannot
+ be done by L{IMessageDelivery} implementors due to their lack of
+ information.
+ """
+
+ def getMessageDelivery():
+ """
+ Return an L{IMessageDelivery} object.
+
+ This will be called once per message.
+ """
+
+
+class IMessageSMTP(Interface):
+ """
+ Interface definition for messages that can be sent via SMTP.
+ """
+
+ def lineReceived(line):
+ """
+ Handle another line.
+ """
+
+ def eomReceived():
+ """
+ Handle end of message.
+
+ return a deferred. The deferred should be called with either:
+ callback(string) or errback(error)
+
+ @rtype: L{Deferred}
+ """
+
+ def connectionLost():
+ """
+ Handle message truncated.
+
+ semantics should be to discard the message
+ """
+
+
+class IMessageIMAPPart(Interface):
+ def getHeaders(negate, *names):
+ """
+ Retrieve a group of message headers.
+
+ @type names: L{tuple} of L{str}
+ @param names: The names of the headers to retrieve or omit.
+
+ @type negate: L{bool}
+ @param negate: If True, indicates that the headers listed in C{names}
+ should be omitted from the return value, rather than included.
+
+ @rtype: L{dict}
+ @return: A mapping of header field names to header field values
+ """
+
+ def getBodyFile():
+ """
+ Retrieve a file object containing only the body of this message.
+ """
+
+ def getSize():
+ """
+ Retrieve the total size, in octets, of this message.
+
+ @rtype: L{int}
+ """
+
+ def isMultipart():
+ """
+ Indicate whether this message has subparts.
+
+ @rtype: L{bool}
+ """
+
+ def getSubPart(part):
+ """
+ Retrieve a MIME sub-message
+
+ @type part: L{int}
+ @param part: The number of the part to retrieve, indexed from 0.
+
+ @raise IndexError: Raised if the specified part does not exist.
+ @raise TypeError: Raised if this message is not multipart.
+
+ @rtype: Any object implementing L{IMessageIMAPPart}.
+ @return: The specified sub-part.
+ """
+
+
+class IMessageIMAP(IMessageIMAPPart):
+ def getUID():
+ """
+ Retrieve the unique identifier associated with this message.
+ """
+
+ def getFlags():
+ """
+ Retrieve the flags associated with this message.
+
+ @rtype: C{iterable}
+ @return: The flags, represented as strings.
+ """
+
+ def getInternalDate():
+ """
+ Retrieve the date internally associated with this message.
+
+ @rtype: L{bytes}
+ @return: An RFC822-formatted date string.
+ """
+
+
+class IMessageIMAPFile(Interface):
+ """
+ Optional message interface for representing messages as files.
+
+ If provided by message objects, this interface will be used instead the
+ more complex MIME-based interface.
+ """
+
+ def open():
+ """
+ Return a file-like object opened for reading.
+
+ Reading from the returned file will return all the bytes of which this
+ message consists.
+ """
+
+
+class ISearchableIMAPMailbox(Interface):
+ def search(query, uid):
+ """
+ Search for messages that meet the given query criteria.
+
+ If this interface is not implemented by the mailbox,
+ L{IMailboxIMAP.fetch} and various methods of L{IMessageIMAP} will be
+ used instead.
+
+ Implementations which wish to offer better performance than the default
+ implementation should implement this interface.
+
+ @type query: L{list}
+ @param query: The search criteria
+
+ @type uid: L{bool}
+ @param uid: If true, the IDs specified in the query are UIDs; otherwise
+ they are message sequence IDs.
+
+ @rtype: L{list} or L{Deferred}
+ @return: A list of message sequence numbers or message UIDs which match
+ the search criteria or a L{Deferred} whose callback will be invoked
+ with such a list.
+
+ @raise IllegalQueryError: Raised when query is not valid.
+ """
+
+
+class IMailboxIMAPListener(Interface):
+ """
+ Interface for objects interested in mailbox events
+ """
+
+ def modeChanged(writeable):
+ """
+ Indicates that the write status of a mailbox has changed.
+
+ @type writeable: L{bool}
+ @param writeable: A true value if write is now allowed, false
+ otherwise.
+ """
+
+ def flagsChanged(newFlags):
+ """
+ Indicates that the flags of one or more messages have changed.
+
+ @type newFlags: L{dict}
+ @param newFlags: A mapping of message identifiers to tuples of flags
+ now set on that message.
+ """
+
+ def newMessages(exists, recent):
+ """
+ Indicates that the number of messages in a mailbox has changed.
+
+ @type exists: L{int} or L{None}
+ @param exists: The total number of messages now in this mailbox. If the
+ total number of messages has not changed, this should be L{None}.
+
+ @type recent: L{int}
+ @param recent: The number of messages now flagged C{\\Recent}. If the
+ number of recent messages has not changed, this should be L{None}.
+ """
+
+
+class IMessageIMAPCopier(Interface):
+ def copy(messageObject):
+ """
+ Copy the given message object into this mailbox.
+
+ The message object will be one which was previously returned by
+ L{IMailboxIMAP.fetch}.
+
+ Implementations which wish to offer better performance than the default
+ implementation should implement this interface.
+
+ If this interface is not implemented by the mailbox,
+ L{IMailboxIMAP.addMessage} will be used instead.
+
+ @rtype: L{Deferred} or L{int}
+ @return: Either the UID of the message or a Deferred which fires with
+ the UID when the copy finishes.
+ """
+
+
+class IMailboxIMAPInfo(Interface):
+ """
+ Interface specifying only the methods required for C{listMailboxes}.
+
+ Implementations can return objects implementing only these methods for
+ return to C{listMailboxes} if it can allow them to operate more
+ efficiently.
+ """
+
+ def getFlags():
+ """
+ Return the flags defined in this mailbox
+
+ Flags with the \\ prefix are reserved for use as system flags.
+
+ @rtype: L{list} of L{str}
+ @return: A list of the flags that can be set on messages in this
+ mailbox.
+ """
+
+ def getHierarchicalDelimiter():
+ """
+ Get the character which delimits namespaces for in this mailbox.
+
+ @rtype: L{bytes}
+ """
+
+
+class IMailboxIMAP(IMailboxIMAPInfo):
+ def getUIDValidity():
+ """
+ Return the unique validity identifier for this mailbox.
+
+ @rtype: L{int}
+ """
+
+ def getUIDNext():
+ """
+ Return the likely UID for the next message added to this mailbox.
+
+ @rtype: L{int}
+ """
+
+ def getUID(message):
+ """
+ Return the UID of a message in the mailbox
+
+ @type message: L{int}
+ @param message: The message sequence number
+
+ @rtype: L{int}
+ @return: The UID of the message.
+ """
+
+ def getMessageCount():
+ """
+ Return the number of messages in this mailbox.
+
+ @rtype: L{int}
+ """
+
+ def getRecentCount():
+ """
+ Return the number of messages with the 'Recent' flag.
+
+ @rtype: L{int}
+ """
+
+ def getUnseenCount():
+ """
+ Return the number of messages with the 'Unseen' flag.
+
+ @rtype: L{int}
+ """
+
+ def isWriteable():
+ """
+ Get the read/write status of the mailbox.
+
+ @rtype: L{int}
+ @return: A true value if write permission is allowed, a false value
+ otherwise.
+ """
+
+ def destroy():
+ """
+ Called before this mailbox is deleted, permanently.
+
+ If necessary, all resources held by this mailbox should be cleaned up
+ here. This function _must_ set the \\Noselect flag on this mailbox.
+ """
+
+ def requestStatus(names):
+ """
+ Return status information about this mailbox.
+
+ Mailboxes which do not intend to do any special processing to generate
+ the return value, C{statusRequestHelper} can be used to build the
+ dictionary by calling the other interface methods which return the data
+ for each name.
+
+ @type names: Any iterable
+ @param names: The status names to return information regarding. The
+ possible values for each name are: MESSAGES, RECENT, UIDNEXT,
+ UIDVALIDITY, UNSEEN.
+
+ @rtype: L{dict} or L{Deferred}
+ @return: A dictionary containing status information about the requested
+ names is returned. If the process of looking this information up
+ would be costly, a deferred whose callback will eventually be
+ passed this dictionary is returned instead.
+ """
+
+ def addListener(listener):
+ """
+ Add a mailbox change listener
+
+ @type listener: Any object which implements C{IMailboxIMAPListener}
+ @param listener: An object to add to the set of those which will be
+ notified when the contents of this mailbox change.
+ """
+
+ def removeListener(listener):
+ """
+ Remove a mailbox change listener
+
+ @type listener: Any object previously added to and not removed from
+ this mailbox as a listener.
+ @param listener: The object to remove from the set of listeners.
+
+ @raise ValueError: Raised when the given object is not a listener for
+ this mailbox.
+ """
+
+ def addMessage(message, flags, date):
+ """
+ Add the given message to this mailbox.
+
+ @type message: A file-like object
+ @param message: The RFC822 formatted message
+
+ @type flags: Any iterable of L{bytes}
+ @param flags: The flags to associate with this message
+
+ @type date: L{bytes}
+ @param date: If specified, the date to associate with this message.
+
+ @rtype: L{Deferred}
+ @return: A deferred whose callback is invoked with the message id if
+ the message is added successfully and whose errback is invoked
+ otherwise.
+
+ @raise ReadOnlyMailbox: Raised if this Mailbox is not open for
+ read-write.
+ """
+
+ def expunge():
+ """
+ Remove all messages flagged \\Deleted.
+
+ @rtype: L{list} or L{Deferred}
+ @return: The list of message sequence numbers which were deleted, or a
+ L{Deferred} whose callback will be invoked with such a list.
+
+ @raise ReadOnlyMailbox: Raised if this Mailbox is not open for
+ read-write.
+ """
+
+ def fetch(messages, uid):
+ """
+ Retrieve one or more messages.
+
+ @type messages: C{MessageSet}
+ @param messages: The identifiers of messages to retrieve information
+ about
+
+ @type uid: L{bool}
+ @param uid: If true, the IDs specified in the query are UIDs; otherwise
+ they are message sequence IDs.
+
+ @rtype: Any iterable of two-tuples of message sequence numbers and
+ implementors of C{IMessageIMAP}.
+ """
+
+ def store(messages, flags, mode, uid):
+ """
+ Set the flags of one or more messages.
+
+ @type messages: A MessageSet object with the list of messages requested
+ @param messages: The identifiers of the messages to set the flags of.
+
+ @type flags: sequence of L{str}
+ @param flags: The flags to set, unset, or add.
+
+ @type mode: -1, 0, or 1
+ @param mode: If mode is -1, these flags should be removed from the
+ specified messages. If mode is 1, these flags should be added to
+ the specified messages. If mode is 0, all existing flags should be
+ cleared and these flags should be added.
+
+ @type uid: L{bool}
+ @param uid: If true, the IDs specified in the query are UIDs; otherwise
+ they are message sequence IDs.
+
+ @rtype: L{dict} or L{Deferred}
+ @return: A L{dict} mapping message sequence numbers to sequences of
+ L{str} representing the flags set on the message after this
+ operation has been performed, or a L{Deferred} whose callback will
+ be invoked with such a L{dict}.
+
+ @raise ReadOnlyMailbox: Raised if this mailbox is not open for
+ read-write.
+ """
+
+
+class ICloseableMailboxIMAP(Interface):
+ """
+ A supplementary interface for mailboxes which require cleanup on close.
+
+ Implementing this interface is optional. If it is implemented, the protocol
+ code will call the close method defined whenever a mailbox is closed.
+ """
+
+ def close():
+ """
+ Close this mailbox.
+
+ @return: A L{Deferred} which fires when this mailbox has been closed,
+ or None if the mailbox can be closed immediately.
+ """
+
+
+class IAccountIMAP(Interface):
+ """
+ Interface for Account classes
+
+ Implementors of this interface should consider implementing
+ C{INamespacePresenter}.
+ """
+
+ def addMailbox(name, mbox=None):
+ """
+ Add a new mailbox to this account
+
+ @type name: L{bytes}
+ @param name: The name associated with this mailbox. It may not contain
+ multiple hierarchical parts.
+
+ @type mbox: An object implementing C{IMailboxIMAP}
+ @param mbox: The mailbox to associate with this name. If L{None}, a
+ suitable default is created and used.
+
+ @rtype: L{Deferred} or L{bool}
+ @return: A true value if the creation succeeds, or a deferred whose
+ callback will be invoked when the creation succeeds.
+
+ @raise MailboxException: Raised if this mailbox cannot be added for
+ some reason. This may also be raised asynchronously, if a
+ L{Deferred} is returned.
+ """
+
+ def create(pathspec):
+ """
+ Create a new mailbox from the given hierarchical name.
+
+ @type pathspec: L{bytes}
+ @param pathspec: The full hierarchical name of a new mailbox to create.
+ If any of the inferior hierarchical names to this one do not exist,
+ they are created as well.
+
+ @rtype: L{Deferred} or L{bool}
+ @return: A true value if the creation succeeds, or a deferred whose
+ callback will be invoked when the creation succeeds.
+
+ @raise MailboxException: Raised if this mailbox cannot be added. This
+ may also be raised asynchronously, if a L{Deferred} is returned.
+ """
+
+ def select(name, rw=True):
+ """
+ Acquire a mailbox, given its name.
+
+ @type name: L{bytes}
+ @param name: The mailbox to acquire
+
+ @type rw: L{bool}
+ @param rw: If a true value, request a read-write version of this
+ mailbox. If a false value, request a read-only version.
+
+ @rtype: Any object implementing C{IMailboxIMAP} or L{Deferred}
+ @return: The mailbox object, or a L{Deferred} whose callback will be
+ invoked with the mailbox object. None may be returned if the
+ specified mailbox may not be selected for any reason.
+ """
+
+ def delete(name):
+ """
+ Delete the mailbox with the specified name.
+
+ @type name: L{bytes}
+ @param name: The mailbox to delete.
+
+ @rtype: L{Deferred} or L{bool}
+ @return: A true value if the mailbox is successfully deleted, or a
+ L{Deferred} whose callback will be invoked when the deletion
+ completes.
+
+ @raise MailboxException: Raised if this mailbox cannot be deleted. This
+ may also be raised asynchronously, if a L{Deferred} is returned.
+ """
+
+ def rename(oldname, newname):
+ """
+ Rename a mailbox
+
+ @type oldname: L{bytes}
+ @param oldname: The current name of the mailbox to rename.
+
+ @type newname: L{bytes}
+ @param newname: The new name to associate with the mailbox.
+
+ @rtype: L{Deferred} or L{bool}
+ @return: A true value if the mailbox is successfully renamed, or a
+ L{Deferred} whose callback will be invoked when the rename
+ operation is completed.
+
+ @raise MailboxException: Raised if this mailbox cannot be renamed. This
+ may also be raised asynchronously, if a L{Deferred} is returned.
+ """
+
+ def isSubscribed(name):
+ """
+ Check the subscription status of a mailbox
+
+ @type name: L{bytes}
+ @param name: The name of the mailbox to check
+
+ @rtype: L{Deferred} or L{bool}
+ @return: A true value if the given mailbox is currently subscribed to,
+ a false value otherwise. A L{Deferred} may also be returned whose
+ callback will be invoked with one of these values.
+ """
+
+ def subscribe(name):
+ """
+ Subscribe to a mailbox
+
+ @type name: L{bytes}
+ @param name: The name of the mailbox to subscribe to
+
+ @rtype: L{Deferred} or L{bool}
+ @return: A true value if the mailbox is subscribed to successfully, or
+ a Deferred whose callback will be invoked with this value when the
+ subscription is successful.
+
+ @raise MailboxException: Raised if this mailbox cannot be subscribed
+ to. This may also be raised asynchronously, if a L{Deferred} is
+ returned.
+ """
+
+ def unsubscribe(name):
+ """
+ Unsubscribe from a mailbox
+
+ @type name: L{bytes}
+ @param name: The name of the mailbox to unsubscribe from
+
+ @rtype: L{Deferred} or L{bool}
+ @return: A true value if the mailbox is unsubscribed from successfully,
+ or a Deferred whose callback will be invoked with this value when
+ the unsubscription is successful.
+
+ @raise MailboxException: Raised if this mailbox cannot be unsubscribed
+ from. This may also be raised asynchronously, if a L{Deferred} is
+ returned.
+ """
+
+ def listMailboxes(ref, wildcard):
+ """
+ List all the mailboxes that meet a certain criteria
+
+ @type ref: L{bytes}
+ @param ref: The context in which to apply the wildcard
+
+ @type wildcard: L{bytes}
+ @param wildcard: An expression against which to match mailbox names.
+ '*' matches any number of characters in a mailbox name, and '%'
+ matches similarly, but will not match across hierarchical
+ boundaries.
+
+ @rtype: L{list} of L{tuple}
+ @return: A list of C{(mailboxName, mailboxObject)} which meet the given
+ criteria. C{mailboxObject} should implement either
+ C{IMailboxIMAPInfo} or C{IMailboxIMAP}. A Deferred may also be
+ returned.
+ """
+
+
+class INamespacePresenter(Interface):
+ def getPersonalNamespaces():
+ """
+ Report the available personal namespaces.
+
+ Typically there should be only one personal namespace. A common name
+ for it is C{\"\"}, and its hierarchical delimiter is usually C{\"/\"}.
+
+ @rtype: iterable of two-tuples of strings
+ @return: The personal namespaces and their hierarchical delimiters. If
+ no namespaces of this type exist, None should be returned.
+ """
+
+ def getSharedNamespaces():
+ """
+ Report the available shared namespaces.
+
+ Shared namespaces do not belong to any individual user but are usually
+ to one or more of them. Examples of shared namespaces might be
+ C{\"#news\"} for a usenet gateway.
+
+ @rtype: iterable of two-tuples of strings
+ @return: The shared namespaces and their hierarchical delimiters. If no
+ namespaces of this type exist, None should be returned.
+ """
+
+ def getUserNamespaces():
+ """
+ Report the available user namespaces.
+
+ These are namespaces that contain folders belonging to other users
+ access to which this account has been granted.
+
+ @rtype: iterable of two-tuples of strings
+ @return: The user namespaces and their hierarchical delimiters. If no
+ namespaces of this type exist, None should be returned.
+ """
+
+
+__all__ = [
+ # IMAP
+ "IAccountIMAP",
+ "ICloseableMailboxIMAP",
+ "IMailboxIMAP",
+ "IMailboxIMAPInfo",
+ "IMailboxIMAPListener",
+ "IMessageIMAP",
+ "IMessageIMAPCopier",
+ "IMessageIMAPFile",
+ "IMessageIMAPPart",
+ "ISearchableIMAPMailbox",
+ "INamespacePresenter",
+ # SMTP
+ "IMessageDelivery",
+ "IMessageDeliveryFactory",
+ "IMessageSMTP",
+ # Domains and aliases
+ "IDomain",
+ "IAlias",
+ "IAliasableDomain",
+ # POP3
+ "IMailboxPOP3",
+ "IServerFactoryPOP3",
+ # Authentication
+ "IClientAuthentication",
+]
diff --git a/contrib/python/Twisted/py3/twisted/mail/mail.py b/contrib/python/Twisted/py3/twisted/mail/mail.py
new file mode 100644
index 0000000000..2dc405344b
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/mail.py
@@ -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()
diff --git a/contrib/python/Twisted/py3/twisted/mail/maildir.py b/contrib/python/Twisted/py3/twisted/mail/maildir.py
new file mode 100644
index 0000000000..c58bf31a94
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/maildir.py
@@ -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()
diff --git a/contrib/python/Twisted/py3/twisted/mail/newsfragments/.gitignore b/contrib/python/Twisted/py3/twisted/mail/newsfragments/.gitignore
new file mode 100644
index 0000000000..f935021a8f
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/newsfragments/.gitignore
@@ -0,0 +1 @@
+!.gitignore
diff --git a/contrib/python/Twisted/py3/twisted/mail/pb.py b/contrib/python/Twisted/py3/twisted/mail/pb.py
new file mode 100644
index 0000000000..1b57e26c95
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/pb.py
@@ -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)
diff --git a/contrib/python/Twisted/py3/twisted/mail/pop3.py b/contrib/python/Twisted/py3/twisted/mail/pop3.py
new file mode 100644
index 0000000000..7b230d2059
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/pop3.py
@@ -0,0 +1,1704 @@
+# -*- test-case-name: twisted.mail.test.test_pop3 -*-
+#
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+
+"""
+Post-office Protocol version 3.
+
+@author: Glyph Lefkowitz
+@author: Jp Calderone
+"""
+
+import base64
+import binascii
+import warnings
+from hashlib import md5
+from typing import Optional
+
+from zope.interface import implementer
+
+from twisted import cred
+from twisted.internet import defer, interfaces, task
+from twisted.mail import smtp
+from twisted.mail._except import POP3ClientError, POP3Error, _POP3MessageDeleted
+from twisted.mail.interfaces import (
+ IMailboxPOP3 as IMailbox,
+ IServerFactoryPOP3 as IServerFactory,
+)
+from twisted.protocols import basic, policies
+from twisted.python import log
+
+
+# Authentication
+@implementer(cred.credentials.IUsernameHashedPassword)
+class APOPCredentials:
+ """
+ Credentials for use in APOP authentication.
+
+ @ivar magic: See L{__init__}
+ @ivar username: See L{__init__}
+ @ivar digest: See L{__init__}
+ """
+
+ def __init__(self, magic, username, digest):
+ """
+ @type magic: L{bytes}
+ @param magic: The challenge string used to encrypt the password.
+
+ @type username: L{bytes}
+ @param username: The username associated with these credentials.
+
+ @type digest: L{bytes}
+ @param digest: An encrypted version of the user's password. Should be
+ generated as an MD5 hash of the challenge string concatenated with
+ the plaintext password.
+ """
+ self.magic = magic
+ self.username = username
+ self.digest = digest
+
+ def checkPassword(self, password):
+ """
+ Validate a plaintext password against the credentials.
+
+ @type password: L{bytes}
+ @param password: A plaintext password.
+
+ @rtype: L{bool}
+ @return: C{True} if the credentials represented by this object match
+ the given password, C{False} if they do not.
+ """
+ seed = self.magic + password
+ myDigest = md5(seed).hexdigest()
+ return myDigest == self.digest
+
+
+class _HeadersPlusNLines:
+ """
+ A utility class to retrieve the header and some lines of the body of a mail
+ message.
+
+ @ivar _file: See L{__init__}
+ @ivar _extraLines: See L{__init__}
+
+ @type linecount: L{int}
+ @ivar linecount: The number of full lines of the message body scanned.
+
+ @type headers: L{bool}
+ @ivar headers: An indication of which part of the message is being scanned.
+ C{True} for the header and C{False} for the body.
+
+ @type done: L{bool}
+ @ivar done: A flag indicating when the desired part of the message has been
+ scanned.
+
+ @type buf: L{bytes}
+ @ivar buf: The portion of the message body that has been scanned, up to
+ C{n} lines.
+ """
+
+ def __init__(self, file, extraLines):
+ """
+ @type file: file-like object
+ @param file: A file containing a mail message.
+
+ @type extraLines: L{int}
+ @param extraLines: The number of lines of the message body to retrieve.
+ """
+ self._file = file
+ self._extraLines = extraLines
+ self.linecount = 0
+ self.headers = 1
+ self.done = 0
+ self.buf = b""
+
+ def read(self, bytes):
+ """
+ Scan bytes from the file.
+
+ @type bytes: L{int}
+ @param bytes: The number of bytes to read from the file.
+
+ @rtype: L{bytes}
+ @return: Each portion of the header as it is scanned. Then, full lines
+ of the message body as they are scanned. When more than one line
+ of the header and/or body has been scanned, the result is the
+ concatenation of the lines. When the scan results in no full
+ lines, the empty string is returned.
+ """
+ if self.done:
+ return b""
+ data = self._file.read(bytes)
+ if not data:
+ return data
+ if self.headers:
+ df, sz = data.find(b"\r\n\r\n"), 4
+ if df == -1:
+ df, sz = data.find(b"\n\n"), 2
+ if df != -1:
+ df += sz
+ val = data[:df]
+ data = data[df:]
+ self.linecount = 1
+ self.headers = 0
+ else:
+ val = b""
+ if self.linecount > 0:
+ dsplit = (self.buf + data).split(b"\n")
+ self.buf = dsplit[-1]
+ for ln in dsplit[:-1]:
+ if self.linecount > self._extraLines:
+ self.done = 1
+ return val
+ val += ln + b"\n"
+ self.linecount += 1
+ return val
+ else:
+ return data
+
+
+class _IteratorBuffer:
+ """
+ An iterator which buffers the elements of a container and periodically
+ passes them as input to a writer.
+
+ @ivar write: See L{__init__}.
+ @ivar memoryBufferSize: See L{__init__}.
+
+ @type bufSize: L{int}
+ @ivar bufSize: The number of bytes currently in the buffer.
+
+ @type lines: L{list} of L{bytes}
+ @ivar lines: The buffer, which is a list of strings.
+
+ @type iterator: iterator which yields L{bytes}
+ @ivar iterator: An iterator over a container of strings.
+ """
+
+ bufSize = 0
+
+ def __init__(self, write, iterable, memoryBufferSize=None):
+ """
+ @type write: callable that takes L{list} of L{bytes}
+ @param write: A writer which is a callable that takes a list of
+ strings.
+
+ @type iterable: iterable which yields L{bytes}
+ @param iterable: An iterable container of strings.
+
+ @type memoryBufferSize: L{int} or L{None}
+ @param memoryBufferSize: The number of bytes to buffer before flushing
+ the buffer to the writer.
+ """
+ self.lines = []
+ self.write = write
+ self.iterator = iter(iterable)
+ if memoryBufferSize is None:
+ memoryBufferSize = 2**16
+ self.memoryBufferSize = memoryBufferSize
+
+ def __iter__(self):
+ """
+ Return an iterator.
+
+ @rtype: iterator which yields L{bytes}
+ @return: An iterator over strings.
+ """
+ return self
+
+ def __next__(self):
+ """
+ Get the next string from the container, buffer it, and possibly send
+ the buffer to the writer.
+
+ The contents of the buffer are written when it is full or when no
+ further values are available from the container.
+
+ @raise StopIteration: When no further values are available from the
+ container.
+ """
+ try:
+ v = next(self.iterator)
+ except StopIteration:
+ if self.lines:
+ self.write(self.lines)
+ # Drop some references, in case they're edges in a cycle.
+ del self.iterator, self.lines, self.write
+ raise
+ else:
+ if v is not None:
+ self.lines.append(v)
+ self.bufSize += len(v)
+ if self.bufSize > self.memoryBufferSize:
+ self.write(self.lines)
+ self.lines = []
+ self.bufSize = 0
+
+ next = __next__
+
+
+def iterateLineGenerator(proto, gen):
+ """
+ Direct the output of an iterator to the transport of a protocol and arrange
+ for iteration to take place.
+
+ @type proto: L{POP3}
+ @param proto: A POP3 server protocol.
+
+ @type gen: iterator which yields L{bytes}
+ @param gen: An iterator over strings.
+
+ @rtype: L{Deferred <defer.Deferred>}
+ @return: A deferred which fires when the iterator finishes.
+ """
+ coll = _IteratorBuffer(proto.transport.writeSequence, gen)
+ return proto.schedule(coll)
+
+
+def successResponse(response):
+ """
+ Format an object as a positive response.
+
+ @type response: stringifyable L{object}
+ @param response: An object with a string representation.
+
+ @rtype: L{bytes}
+ @return: A positive POP3 response string.
+ """
+ if not isinstance(response, bytes):
+ response = str(response).encode("utf-8")
+ return b"+OK " + response + b"\r\n"
+
+
+def formatStatResponse(msgs):
+ """
+ Format a list of message sizes into a STAT response.
+
+ This generator function is intended to be used with
+ L{Cooperator <twisted.internet.task.Cooperator>}.
+
+ @type msgs: L{list} of L{int}
+ @param msgs: A list of message sizes.
+
+ @rtype: L{None} or L{bytes}
+ @return: Yields none until a result is available, then a string that is
+ suitable for use in a STAT response. The string consists of the number
+ of messages and the total size of the messages in octets.
+ """
+ i = 0
+ bytes = 0
+ for size in msgs:
+ i += 1
+ bytes += size
+ yield None
+ yield successResponse(b"%d %d" % (i, bytes))
+
+
+def formatListLines(msgs):
+ """
+ Format a list of message sizes for use in a LIST response.
+
+ @type msgs: L{list} of L{int}
+ @param msgs: A list of message sizes.
+
+ @rtype: L{bytes}
+ @return: Yields a series of strings that are suitable for use as scan
+ listings in a LIST response. Each string consists of a message number
+ and its size in octets.
+ """
+ i = 0
+ for size in msgs:
+ i += 1
+ yield b"%d %d\r\n" % (i, size)
+
+
+def formatListResponse(msgs):
+ """
+ Format a list of message sizes into a complete LIST response.
+
+ This generator function is intended to be used with
+ L{Cooperator <twisted.internet.task.Cooperator>}.
+
+ @type msgs: L{list} of L{int}
+ @param msgs: A list of message sizes.
+
+ @rtype: L{bytes}
+ @return: Yields a series of strings which make up a complete LIST response.
+ """
+ yield successResponse(b"%d" % (len(msgs),))
+ yield from formatListLines(msgs)
+ yield b".\r\n"
+
+
+def formatUIDListLines(msgs, getUidl):
+ """
+ Format a list of message sizes for use in a UIDL response.
+
+ @param msgs: See L{formatUIDListResponse}
+ @param getUidl: See L{formatUIDListResponse}
+
+ @rtype: L{bytes}
+ @return: Yields a series of strings that are suitable for use as unique-id
+ listings in a UIDL response. Each string consists of a message number
+ and its unique id.
+ """
+ for i, m in enumerate(msgs):
+ if m is not None:
+ uid = getUidl(i)
+ if not isinstance(uid, bytes):
+ uid = str(uid).encode("utf-8")
+ yield b"%d %b\r\n" % (i + 1, uid)
+
+
+def formatUIDListResponse(msgs, getUidl):
+ """
+ Format a list of message sizes into a complete UIDL response.
+
+ This generator function is intended to be used with
+ L{Cooperator <twisted.internet.task.Cooperator>}.
+
+ @type msgs: L{list} of L{int}
+ @param msgs: A list of message sizes.
+
+ @type getUidl: one-argument callable returning bytes
+ @param getUidl: A callable which takes a message index number and returns
+ the UID of the corresponding message in the mailbox.
+
+ @rtype: L{bytes}
+ @return: Yields a series of strings which make up a complete UIDL response.
+ """
+ yield successResponse("")
+ yield from formatUIDListLines(msgs, getUidl)
+ yield b".\r\n"
+
+
+@implementer(interfaces.IProducer)
+class POP3(basic.LineOnlyReceiver, policies.TimeoutMixin):
+ """
+ A POP3 server protocol.
+
+ @type portal: L{Portal}
+ @ivar portal: A portal for authentication.
+
+ @type factory: L{IServerFactory} provider
+ @ivar factory: A server factory which provides an interface for querying
+ capabilities of the server.
+
+ @type timeOut: L{int}
+ @ivar timeOut: The number of seconds to wait for a command from the client
+ before disconnecting.
+
+ @type schedule: callable that takes interator and returns
+ L{Deferred <defer.Deferred>}
+ @ivar schedule: A callable that arranges for an iterator to be
+ cooperatively iterated over along with all other iterators which have
+ been passed to it such that runtime is divided between all of them. It
+ returns a deferred which fires when the iterator finishes.
+
+ @type magic: L{bytes} or L{None}
+ @ivar magic: An APOP challenge. If not set, an APOP challenge string
+ will be generated when a connection is made.
+
+ @type _userIs: L{bytes} or L{None}
+ @ivar _userIs: The username sent with the USER command.
+
+ @type _onLogout: no-argument callable or L{None}
+ @ivar _onLogout: The function to be executed when the connection is
+ lost.
+
+ @type mbox: L{IMailbox} provider
+ @ivar mbox: The mailbox for the authenticated user.
+
+ @type state: L{bytes}
+ @ivar state: The state which indicates what type of messages are expected
+ from the client. Valid states are 'COMMAND' and 'AUTH'
+
+ @type blocked: L{None} or L{list} of 2-L{tuple} of
+ (E{1}) L{bytes} (E{2}) L{tuple} of L{bytes}
+ @ivar blocked: A list of blocked commands. While a response to a command
+ is being generated by the server, other commands are blocked. When
+ no command is outstanding, C{blocked} is set to none. Otherwise, it
+ contains a list of information about blocked commands. Each list
+ entry consists of the command and the arguments to the command.
+
+ @type _highest: L{int}
+ @ivar _highest: The 1-based index of the highest message retrieved.
+
+ @type _auth: L{IUsernameHashedPassword
+ <cred.credentials.IUsernameHashedPassword>} provider
+ @ivar _auth: Authorization credentials.
+ """
+
+ magic: Optional[bytes] = None
+ _userIs = None
+ _onLogout = None
+
+ AUTH_CMDS = [b"CAPA", b"USER", b"PASS", b"APOP", b"AUTH", b"RPOP", b"QUIT"]
+
+ portal = None
+ factory = None
+
+ # The mailbox we're serving
+ mbox = None
+
+ # Set this pretty low -- POP3 clients are expected to log in, download
+ # everything, and log out.
+ timeOut = 300
+
+ state = "COMMAND"
+
+ # PIPELINE
+ blocked = None
+
+ # Cooperate and suchlike.
+ schedule = staticmethod(task.coiterate)
+
+ _highest = 0
+
+ def connectionMade(self):
+ """
+ Send a greeting to the client after the connection has been made.
+ """
+ if self.magic is None:
+ self.magic = self.generateMagic()
+ self.successResponse(self.magic)
+ self.setTimeout(self.timeOut)
+ if getattr(self.factory, "noisy", True):
+ log.msg("New connection from " + str(self.transport.getPeer()))
+
+ def connectionLost(self, reason):
+ """
+ Clean up when the connection has been lost.
+
+ @type reason: L{Failure}
+ @param reason: The reason the connection was terminated.
+ """
+ if self._onLogout is not None:
+ self._onLogout()
+ self._onLogout = None
+ self.setTimeout(None)
+
+ def generateMagic(self):
+ """
+ Generate an APOP challenge.
+
+ @rtype: L{bytes}
+ @return: An RFC 822 message id format string.
+ """
+ return smtp.messageid()
+
+ def successResponse(self, message=""):
+ """
+ Send a response indicating success.
+
+ @type message: stringifyable L{object}
+ @param message: An object whose string representation should be
+ included in the response.
+ """
+ self.transport.write(successResponse(message))
+
+ def failResponse(self, message=b""):
+ """
+ Send a response indicating failure.
+
+ @type message: stringifyable L{object}
+ @param message: An object whose string representation should be
+ included in the response.
+ """
+ if not isinstance(message, bytes):
+ message = str(message).encode("utf-8")
+ self.sendLine(b"-ERR " + message)
+
+ def lineReceived(self, line):
+ """
+ Pass a received line to a state machine function.
+
+ @type line: L{bytes}
+ @param line: A received line.
+ """
+ self.resetTimeout()
+ getattr(self, "state_" + self.state)(line)
+
+ def _unblock(self, _):
+ """
+ Process as many blocked commands as possible.
+
+ If there are no more blocked commands, set up for the next command to
+ be sent immediately.
+
+ @type _: L{object}
+ @param _: Ignored.
+ """
+ commands = self.blocked
+ self.blocked = None
+ while commands and self.blocked is None:
+ cmd, args = commands.pop(0)
+ self.processCommand(cmd, *args)
+ if self.blocked is not None:
+ self.blocked.extend(commands)
+
+ def state_COMMAND(self, line):
+ """
+ Handle received lines for the COMMAND state in which commands from the
+ client are expected.
+
+ @type line: L{bytes}
+ @param line: A received command.
+ """
+ try:
+ return self.processCommand(*line.split(b" "))
+ except (ValueError, AttributeError, POP3Error, TypeError) as e:
+ log.err()
+ self.failResponse(
+ b": ".join(
+ [
+ b"bad protocol or server",
+ e.__class__.__name__.encode("utf-8"),
+ b"".join(e.args),
+ ]
+ )
+ )
+
+ def processCommand(self, command, *args):
+ """
+ Dispatch a command from the client for handling.
+
+ @type command: L{bytes}
+ @param command: A POP3 command.
+
+ @type args: L{tuple} of L{bytes}
+ @param args: Arguments to the command.
+
+ @raise POP3Error: When the command is invalid or the command requires
+ prior authentication which hasn't been performed.
+ """
+ if self.blocked is not None:
+ self.blocked.append((command, args))
+ return
+
+ command = command.upper()
+ authCmd = command in self.AUTH_CMDS
+ if not self.mbox and not authCmd:
+ raise POP3Error(b"not authenticated yet: cannot do " + command)
+ f = getattr(self, "do_" + command.decode("utf-8"), None)
+ if f:
+ return f(*args)
+ raise POP3Error(b"Unknown protocol command: " + command)
+
+ def listCapabilities(self):
+ """
+ Return a list of server capabilities suitable for use in a CAPA
+ response.
+
+ @rtype: L{list} of L{bytes}
+ @return: A list of server capabilities.
+ """
+ baseCaps = [
+ b"TOP",
+ b"USER",
+ b"UIDL",
+ b"PIPELINE",
+ b"CELERITY",
+ b"AUSPEX",
+ b"POTENCE",
+ ]
+
+ if IServerFactory.providedBy(self.factory):
+ # Oh my god. We can't just loop over a list of these because
+ # each has spectacularly different return value semantics!
+ try:
+ v = self.factory.cap_IMPLEMENTATION()
+ if v and not isinstance(v, bytes):
+ v = str(v).encode("utf-8")
+ except NotImplementedError:
+ pass
+ except BaseException:
+ log.err()
+ else:
+ baseCaps.append(b"IMPLEMENTATION " + v)
+
+ try:
+ v = self.factory.cap_EXPIRE()
+ if v and not isinstance(v, bytes):
+ v = str(v).encode("utf-8")
+ except NotImplementedError:
+ pass
+ except BaseException:
+ log.err()
+ else:
+ if v is None:
+ v = b"NEVER"
+ if self.factory.perUserExpiration():
+ if self.mbox:
+ v = str(self.mbox.messageExpiration).encode("utf-8")
+ else:
+ v = v + b" USER"
+ baseCaps.append(b"EXPIRE " + v)
+
+ try:
+ v = self.factory.cap_LOGIN_DELAY()
+ if v and not isinstance(v, bytes):
+ v = str(v).encode("utf-8")
+ except NotImplementedError:
+ pass
+ except BaseException:
+ log.err()
+ else:
+ if self.factory.perUserLoginDelay():
+ if self.mbox:
+ v = str(self.mbox.loginDelay).encode("utf-8")
+ else:
+ v = v + b" USER"
+ baseCaps.append(b"LOGIN-DELAY " + v)
+
+ try:
+ v = self.factory.challengers
+ except AttributeError:
+ pass
+ except BaseException:
+ log.err()
+ else:
+ baseCaps.append(b"SASL " + b" ".join(v.keys()))
+ return baseCaps
+
+ def do_CAPA(self):
+ """
+ Handle a CAPA command.
+
+ Respond with the server capabilities.
+ """
+ self.successResponse(b"I can do the following:")
+ for cap in self.listCapabilities():
+ self.sendLine(cap)
+ self.sendLine(b".")
+
+ def do_AUTH(self, args=None):
+ """
+ Handle an AUTH command.
+
+ If the AUTH extension is not supported, send an error response. If an
+ authentication mechanism was not specified in the command, send a list
+ of all supported authentication methods. Otherwise, send an
+ authentication challenge to the client and transition to the
+ AUTH state.
+
+ @type args: L{bytes} or L{None}
+ @param args: The name of an authentication mechanism.
+ """
+ if not getattr(self.factory, "challengers", None):
+ self.failResponse(b"AUTH extension unsupported")
+ return
+
+ if args is None:
+ self.successResponse("Supported authentication methods:")
+ for a in self.factory.challengers:
+ self.sendLine(a.upper())
+ self.sendLine(b".")
+ return
+
+ auth = self.factory.challengers.get(args.strip().upper())
+ if not self.portal or not auth:
+ self.failResponse(b"Unsupported SASL selected")
+ return
+
+ self._auth = auth()
+ chal = self._auth.getChallenge()
+
+ self.sendLine(b"+ " + base64.b64encode(chal))
+ self.state = "AUTH"
+
+ def state_AUTH(self, line):
+ """
+ Handle received lines for the AUTH state in which an authentication
+ challenge response from the client is expected.
+
+ Transition back to the COMMAND state. Check the credentials and
+ complete the authorization process with the L{_cbMailbox}
+ callback function on success or the L{_ebMailbox} and L{_ebUnexpected}
+ errback functions on failure.
+
+ @type line: L{bytes}
+ @param line: The challenge response.
+ """
+ self.state = "COMMAND"
+ try:
+ parts = base64.b64decode(line).split(None, 1)
+ except binascii.Error:
+ self.failResponse(b"Invalid BASE64 encoding")
+ else:
+ if len(parts) != 2:
+ self.failResponse(b"Invalid AUTH response")
+ return
+ self._auth.username = parts[0]
+ self._auth.response = parts[1]
+ d = self.portal.login(self._auth, None, IMailbox)
+ d.addCallback(self._cbMailbox, parts[0])
+ d.addErrback(self._ebMailbox)
+ d.addErrback(self._ebUnexpected)
+
+ def do_APOP(self, user, digest):
+ """
+ Handle an APOP command.
+
+ Perform APOP authentication and complete the authorization process with
+ the L{_cbMailbox} callback function on success or the L{_ebMailbox}
+ and L{_ebUnexpected} errback functions on failure.
+
+ @type user: L{bytes}
+ @param user: A username.
+
+ @type digest: L{bytes}
+ @param digest: An MD5 digest string.
+ """
+ d = defer.maybeDeferred(self.authenticateUserAPOP, user, digest)
+ d.addCallbacks(
+ self._cbMailbox, self._ebMailbox, callbackArgs=(user,)
+ ).addErrback(self._ebUnexpected)
+
+ def _cbMailbox(self, result, user):
+ """
+ Complete successful authentication.
+
+ Save the mailbox and logout function for the authenticated user and
+ send a successful response to the client.
+
+ @type result: C{tuple}
+ @param result: The first item of the tuple is a
+ C{zope.interface.Interface} which is the interface
+ supported by the avatar. The second item of the tuple is a
+ L{IMailbox} provider which is the mailbox for the
+ authenticated user. The third item of the tuple is a no-argument
+ callable which is a function to be invoked when the session is
+ terminated.
+
+ @type user: L{bytes}
+ @param user: The user being authenticated.
+ """
+ (interface, avatar, logout) = result
+ if interface is not IMailbox:
+ self.failResponse(b"Authentication failed")
+ log.err("_cbMailbox() called with an interface other than IMailbox")
+ return
+
+ self.mbox = avatar
+ self._onLogout = logout
+ self.successResponse("Authentication succeeded")
+ if getattr(self.factory, "noisy", True):
+ log.msg(b"Authenticated login for " + user)
+
+ def _ebMailbox(self, failure):
+ """
+ Handle an expected authentication failure.
+
+ Send an appropriate error response for a L{LoginDenied} or
+ L{LoginFailed} authentication failure.
+
+ @type failure: L{Failure}
+ @param failure: The authentication error.
+ """
+ failure = failure.trap(cred.error.LoginDenied, cred.error.LoginFailed)
+ if issubclass(failure, cred.error.LoginDenied):
+ self.failResponse("Access denied: " + str(failure))
+ elif issubclass(failure, cred.error.LoginFailed):
+ self.failResponse(b"Authentication failed")
+ if getattr(self.factory, "noisy", True):
+ log.msg("Denied login attempt from " + str(self.transport.getPeer()))
+
+ def _ebUnexpected(self, failure):
+ """
+ Handle an unexpected authentication failure.
+
+ Send an error response for an unexpected authentication failure.
+
+ @type failure: L{Failure}
+ @param failure: The authentication error.
+ """
+ self.failResponse("Server error: " + failure.getErrorMessage())
+ log.err(failure)
+
+ def do_USER(self, user):
+ """
+ Handle a USER command.
+
+ Save the username and send a successful response prompting the client
+ for the password.
+
+ @type user: L{bytes}
+ @param user: A username.
+ """
+ self._userIs = user
+ self.successResponse(b"USER accepted, send PASS")
+
+ def do_PASS(self, password, *words):
+ """
+ Handle a PASS command.
+
+ If a USER command was previously received, authenticate the user and
+ complete the authorization process with the L{_cbMailbox} callback
+ function on success or the L{_ebMailbox} and L{_ebUnexpected} errback
+ functions on failure. If a USER command was not previously received,
+ send an error response.
+
+ @type password: L{bytes}
+ @param password: A password.
+
+ @type words: L{tuple} of L{bytes}
+ @param words: Other parts of the password split by spaces.
+ """
+ if self._userIs is None:
+ self.failResponse(b"USER required before PASS")
+ return
+ user = self._userIs
+ self._userIs = None
+ password = b" ".join((password,) + words)
+ d = defer.maybeDeferred(self.authenticateUserPASS, user, password)
+ d.addCallbacks(
+ self._cbMailbox, self._ebMailbox, callbackArgs=(user,)
+ ).addErrback(self._ebUnexpected)
+
+ def _longOperation(self, d):
+ """
+ Stop timeouts and block further command processing while a long
+ operation completes.
+
+ @type d: L{Deferred <defer.Deferred>}
+ @param d: A deferred which triggers at the completion of a long
+ operation.
+
+ @rtype: L{Deferred <defer.Deferred>}
+ @return: A deferred which triggers after command processing resumes and
+ timeouts restart after the completion of a long operation.
+ """
+ timeOut = self.timeOut
+ self.setTimeout(None)
+ self.blocked = []
+ d.addCallback(self._unblock)
+ d.addCallback(lambda ign: self.setTimeout(timeOut))
+ return d
+
+ def _coiterate(self, gen):
+ """
+ Direct the output of an iterator to the transport and arrange for
+ iteration to take place.
+
+ @type gen: iterable which yields L{bytes}
+ @param gen: An iterator over strings.
+
+ @rtype: L{Deferred <defer.Deferred>}
+ @return: A deferred which fires when the iterator finishes.
+ """
+ return self.schedule(_IteratorBuffer(self.transport.writeSequence, gen))
+
+ def do_STAT(self):
+ """
+ Handle a STAT command.
+
+ @rtype: L{Deferred <defer.Deferred>}
+ @return: A deferred which triggers after the response to the STAT
+ command has been issued.
+ """
+ d = defer.maybeDeferred(self.mbox.listMessages)
+
+ def cbMessages(msgs):
+ return self._coiterate(formatStatResponse(msgs))
+
+ def ebMessages(err):
+ self.failResponse(err.getErrorMessage())
+ log.msg("Unexpected do_STAT failure:")
+ log.err(err)
+
+ return self._longOperation(d.addCallbacks(cbMessages, ebMessages))
+
+ def do_LIST(self, i=None):
+ """
+ Handle a LIST command.
+
+ @type i: L{bytes} or L{None}
+ @param i: A 1-based message index.
+
+ @rtype: L{Deferred <defer.Deferred>}
+ @return: A deferred which triggers after the response to the LIST
+ command has been issued.
+ """
+ if i is None:
+ d = defer.maybeDeferred(self.mbox.listMessages)
+
+ def cbMessages(msgs):
+ return self._coiterate(formatListResponse(msgs))
+
+ def ebMessages(err):
+ self.failResponse(err.getErrorMessage())
+ log.msg("Unexpected do_LIST failure:")
+ log.err(err)
+
+ return self._longOperation(d.addCallbacks(cbMessages, ebMessages))
+ else:
+ try:
+ i = int(i)
+ if i < 1:
+ raise ValueError()
+ except ValueError:
+ if not isinstance(i, bytes):
+ i = str(i).encode("utf-8")
+ self.failResponse(b"Invalid message-number: " + i)
+ else:
+ d = defer.maybeDeferred(self.mbox.listMessages, i - 1)
+
+ def cbMessage(msg):
+ self.successResponse(b"%d %d" % (i, msg))
+
+ def ebMessage(err):
+ errcls = err.check(ValueError, IndexError)
+ if errcls is not None:
+ if errcls is IndexError:
+ # IndexError was supported for a while, but really
+ # shouldn't be. One error condition, one exception
+ # type. See ticket #6669.
+ warnings.warn(
+ "twisted.mail.pop3.IMailbox.listMessages may "
+ "not raise IndexError for out-of-bounds "
+ "message numbers: raise ValueError instead.",
+ PendingDeprecationWarning,
+ )
+ invalidNum = i
+ if invalidNum and not isinstance(invalidNum, bytes):
+ invalidNum = str(invalidNum).encode("utf-8")
+ self.failResponse(b"Invalid message-number: " + invalidNum)
+ else:
+ self.failResponse(err.getErrorMessage())
+ log.msg("Unexpected do_LIST failure:")
+ log.err(err)
+
+ d.addCallbacks(cbMessage, ebMessage)
+ return self._longOperation(d)
+
+ def do_UIDL(self, i=None):
+ """
+ Handle a UIDL command.
+
+ @type i: L{bytes} or L{None}
+ @param i: A 1-based message index.
+
+ @rtype: L{Deferred <defer.Deferred>}
+ @return: A deferred which triggers after the response to the UIDL
+ command has been issued.
+ """
+ if i is None:
+ d = defer.maybeDeferred(self.mbox.listMessages)
+
+ def cbMessages(msgs):
+ return self._coiterate(
+ formatUIDListResponse(msgs, self.mbox.getUidl),
+ )
+
+ def ebMessages(err):
+ self.failResponse(err.getErrorMessage())
+ log.msg("Unexpected do_UIDL failure:")
+ log.err(err)
+
+ return self._longOperation(d.addCallbacks(cbMessages, ebMessages))
+ else:
+ try:
+ i = int(i)
+ if i < 1:
+ raise ValueError()
+ except ValueError:
+ self.failResponse("Bad message number argument")
+ else:
+ try:
+ msg = self.mbox.getUidl(i - 1)
+ except IndexError:
+ # XXX TODO See above comment regarding IndexError.
+ warnings.warn(
+ "twisted.mail.pop3.IMailbox.getUidl may not "
+ "raise IndexError for out-of-bounds message numbers: "
+ "raise ValueError instead.",
+ PendingDeprecationWarning,
+ )
+ self.failResponse("Bad message number argument")
+ except ValueError:
+ self.failResponse("Bad message number argument")
+ else:
+ if not isinstance(msg, bytes):
+ msg = str(msg).encode("utf-8")
+ self.successResponse(msg)
+
+ def _getMessageFile(self, i):
+ """
+ Retrieve the size and contents of a message.
+
+ @type i: L{bytes}
+ @param i: A 1-based message index.
+
+ @rtype: L{Deferred <defer.Deferred>} which successfully fires with
+ 2-L{tuple} of (E{1}) L{int}, (E{2}) file-like object
+ @return: A deferred which successfully fires with the size of the
+ message and a file containing the contents of the message.
+ """
+ try:
+ msg = int(i) - 1
+ if msg < 0:
+ raise ValueError()
+ except ValueError:
+ self.failResponse("Bad message number argument")
+ return defer.succeed(None)
+
+ sizeDeferred = defer.maybeDeferred(self.mbox.listMessages, msg)
+
+ def cbMessageSize(size):
+ if not size:
+ return defer.fail(_POP3MessageDeleted())
+ fileDeferred = defer.maybeDeferred(self.mbox.getMessage, msg)
+ fileDeferred.addCallback(lambda fObj: (size, fObj))
+ return fileDeferred
+
+ def ebMessageSomething(err):
+ errcls = err.check(_POP3MessageDeleted, ValueError, IndexError)
+ if errcls is _POP3MessageDeleted:
+ self.failResponse("message deleted")
+ elif errcls in (ValueError, IndexError):
+ if errcls is IndexError:
+ # XXX TODO See above comment regarding IndexError.
+ warnings.warn(
+ "twisted.mail.pop3.IMailbox.listMessages may not "
+ "raise IndexError for out-of-bounds message numbers: "
+ "raise ValueError instead.",
+ PendingDeprecationWarning,
+ )
+ self.failResponse("Bad message number argument")
+ else:
+ log.msg("Unexpected _getMessageFile failure:")
+ log.err(err)
+ return None
+
+ sizeDeferred.addCallback(cbMessageSize)
+ sizeDeferred.addErrback(ebMessageSomething)
+ return sizeDeferred
+
+ def _sendMessageContent(self, i, fpWrapper, successResponse):
+ """
+ Send the contents of a message.
+
+ @type i: L{bytes}
+ @param i: A 1-based message index.
+
+ @type fpWrapper: callable that takes a file-like object and returns
+ a file-like object
+ @param fpWrapper:
+
+ @type successResponse: callable that takes L{int} and returns
+ L{bytes}
+ @param successResponse:
+
+ @rtype: L{Deferred}
+ @return: A deferred which triggers after the message has been sent.
+ """
+ d = self._getMessageFile(i)
+
+ def cbMessageFile(info):
+ if info is None:
+ # Some error occurred - a failure response has been sent
+ # already, just give up.
+ return
+
+ self._highest = max(self._highest, int(i))
+ resp, fp = info
+ fp = fpWrapper(fp)
+ self.successResponse(successResponse(resp))
+ s = basic.FileSender()
+ d = s.beginFileTransfer(fp, self.transport, self.transformChunk)
+
+ def cbFileTransfer(lastsent):
+ if lastsent != b"\n":
+ line = b"\r\n."
+ else:
+ line = b"."
+ self.sendLine(line)
+
+ def ebFileTransfer(err):
+ self.transport.loseConnection()
+ log.msg("Unexpected error in _sendMessageContent:")
+ log.err(err)
+
+ d.addCallback(cbFileTransfer)
+ d.addErrback(ebFileTransfer)
+ return d
+
+ return self._longOperation(d.addCallback(cbMessageFile))
+
+ def do_TOP(self, i, size):
+ """
+ Handle a TOP command.
+
+ @type i: L{bytes}
+ @param i: A 1-based message index.
+
+ @type size: L{bytes}
+ @param size: The number of lines of the message to retrieve.
+
+ @rtype: L{Deferred}
+ @return: A deferred which triggers after the response to the TOP
+ command has been issued.
+ """
+ try:
+ size = int(size)
+ if size < 0:
+ raise ValueError
+ except ValueError:
+ self.failResponse("Bad line count argument")
+ else:
+ return self._sendMessageContent(
+ i,
+ lambda fp: _HeadersPlusNLines(fp, size),
+ lambda size: "Top of message follows",
+ )
+
+ def do_RETR(self, i):
+ """
+ Handle a RETR command.
+
+ @type i: L{bytes}
+ @param i: A 1-based message index.
+
+ @rtype: L{Deferred}
+ @return: A deferred which triggers after the response to the RETR
+ command has been issued.
+ """
+ return self._sendMessageContent(i, lambda fp: fp, lambda size: "%d" % (size,))
+
+ def transformChunk(self, chunk):
+ """
+ Transform a chunk of a message to POP3 message format.
+
+ Make sure each line ends with C{'\\r\\n'} and byte-stuff the
+ termination character (C{'.'}) by adding an extra one when one appears
+ at the beginning of a line.
+
+ @type chunk: L{bytes}
+ @param chunk: A string to transform.
+
+ @rtype: L{bytes}
+ @return: The transformed string.
+ """
+ return chunk.replace(b"\n", b"\r\n").replace(b"\r\n.", b"\r\n..")
+
+ def finishedFileTransfer(self, lastsent):
+ """
+ Send the termination sequence.
+
+ @type lastsent: L{bytes}
+ @param lastsent: The last character of the file.
+ """
+ if lastsent != b"\n":
+ line = b"\r\n."
+ else:
+ line = b"."
+ self.sendLine(line)
+
+ def do_DELE(self, i):
+ """
+ Handle a DELE command.
+
+ Mark a message for deletion and issue a successful response.
+
+ @type i: L{int}
+ @param i: A 1-based message index.
+ """
+ i = int(i) - 1
+ self.mbox.deleteMessage(i)
+ self.successResponse()
+
+ def do_NOOP(self):
+ """
+ Handle a NOOP command.
+
+ Do nothing but issue a successful response.
+ """
+ self.successResponse()
+
+ def do_RSET(self):
+ """
+ Handle a RSET command.
+
+ Unmark any messages that have been flagged for deletion.
+ """
+ try:
+ self.mbox.undeleteMessages()
+ except BaseException:
+ log.err()
+ self.failResponse()
+ else:
+ self._highest = 0
+ self.successResponse()
+
+ def do_LAST(self):
+ """
+ Handle a LAST command.
+
+ Respond with the 1-based index of the highest retrieved message.
+ """
+ self.successResponse(self._highest)
+
+ def do_RPOP(self, user):
+ """
+ Handle an RPOP command.
+
+ RPOP is not supported. Send an error response.
+
+ @type user: L{bytes}
+ @param user: A username.
+
+ """
+ self.failResponse("permission denied, sucker")
+
+ def do_QUIT(self):
+ """
+ Handle a QUIT command.
+
+ Remove any messages marked for deletion, issue a successful response,
+ and drop the connection.
+ """
+ if self.mbox:
+ self.mbox.sync()
+ self.successResponse()
+ self.transport.loseConnection()
+
+ def authenticateUserAPOP(self, user, digest):
+ """
+ Perform APOP authentication.
+
+ @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 <defer.Deferred>} which successfully results in
+ 3-L{tuple} of (E{1}) L{IMailbox <pop3.IMailbox>}, (E{2})
+ L{IMailbox <pop3.IMailbox>} provider, (E{3}) 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 function to be invoked with the session is
+ terminated. If authentication fails, the deferred fails with an
+ L{UnathorizedLogin <cred.error.UnauthorizedLogin>} error.
+
+ @raise cred.error.UnauthorizedLogin: When authentication fails.
+ """
+ if self.portal is not None:
+ return self.portal.login(
+ APOPCredentials(self.magic, user, digest), None, IMailbox
+ )
+ raise cred.error.UnauthorizedLogin()
+
+ def authenticateUserPASS(self, user, password):
+ """
+ Perform authentication for a username/password login.
+
+ @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 <defer.Deferred>} which successfully results in
+ 3-L{tuple} of (E{1}) L{IMailbox <pop3.IMailbox>}, (E{2}) L{IMailbox
+ <pop3.IMailbox>} provider, (E{3}) no-argument callable
+ @return: A deferred which fires when authentication is complete. If
+ successful, it returns a L{pop3.IMailbox} interface, a mailbox,
+ and a function to be invoked with the session is terminated.
+ If authentication fails, the deferred fails with an
+ L{UnathorizedLogin <cred.error.UnauthorizedLogin>} error.
+
+ @raise cred.error.UnauthorizedLogin: When authentication fails.
+ """
+ if self.portal is not None:
+ return self.portal.login(
+ cred.credentials.UsernamePassword(user, password), None, IMailbox
+ )
+ raise cred.error.UnauthorizedLogin()
+
+ def stopProducing(self):
+ # IProducer.stopProducing
+ raise NotImplementedError()
+
+
+@implementer(IMailbox)
+class Mailbox:
+ """
+ A base class for mailboxes.
+ """
+
+ 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 the message.
+
+ @rtype: L{int}, sequence of L{int}, or L{Deferred <defer.Deferred>}
+ @return: The number of octets in the specified message, or, if an
+ index is not specified, a sequence of the number of octets for
+ all messages in the mailbox or a deferred which fires with
+ one of those. Any value which corresponds to a deleted message
+ is set to 0.
+
+ @raise ValueError or IndexError: When the index does not correspond to
+ a message in the mailbox. The use of ValueError is preferred.
+ """
+ return []
+
+ def getMessage(self, i):
+ """
+ Retrieve a file containing 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 ValueError or IndexError: When the index does not correspond to
+ a message in the mailbox. The use of ValueError is preferred.
+ """
+ raise ValueError
+
+ 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 ValueError or IndexError: When the index does not correspond to
+ a message in the mailbox. The use of ValueError is preferred.
+ """
+ raise ValueError
+
+ def deleteMessage(self, i):
+ """
+ Mark a message for deletion.
+
+ This must not change the number of messages in this mailbox. Further
+ requests for the size of the deleted message should return 0. Further
+ requests for the message itself may raise an exception.
+
+ @type i: L{int}
+ @param i: The 0-based index of a message.
+
+ @raise ValueError or IndexError: When the index does not correspond to
+ a message in the mailbox. The use of ValueError is preferred.
+ """
+ raise ValueError
+
+ def undeleteMessages(self):
+ """
+ Undelete all messages marked for deletion.
+
+ Any message which can be undeleted should be returned to its original
+ position in the message sequence and retain its original UID.
+ """
+ pass
+
+ def sync(self):
+ """
+ Discard the contents of any message marked for deletion.
+ """
+ pass
+
+
+NONE, SHORT, FIRST_LONG, LONG = range(4)
+
+NEXT = {}
+NEXT[NONE] = NONE
+NEXT[SHORT] = NONE
+NEXT[FIRST_LONG] = LONG
+NEXT[LONG] = NONE
+
+
+class POP3Client(basic.LineOnlyReceiver):
+ """
+ A POP3 client protocol.
+
+ @type mode: L{int}
+ @ivar mode: The type of response expected from the server. Choices include
+ none (0), a one line response (1), the first line of a multi-line
+ response (2), and subsequent lines of a multi-line response (3).
+
+ @type command: L{bytes}
+ @ivar command: The command most recently sent to the server.
+
+ @type welcomeRe: L{Pattern <re.Pattern.search>}
+ @ivar welcomeRe: A regular expression which matches the APOP challenge in
+ the server greeting.
+
+ @type welcomeCode: L{bytes}
+ @ivar welcomeCode: The APOP challenge passed in the server greeting.
+ """
+
+ mode = SHORT
+ command = b"WELCOME"
+ import re
+
+ welcomeRe = re.compile(b"<(.*)>")
+
+ def __init__(self):
+ """
+ Issue deprecation warning.
+ """
+ import warnings
+
+ warnings.warn(
+ "twisted.mail.pop3.POP3Client is deprecated, "
+ "please use twisted.mail.pop3.AdvancedPOP3Client "
+ "instead.",
+ DeprecationWarning,
+ stacklevel=3,
+ )
+
+ def sendShort(self, command, params=None):
+ """
+ Send a POP3 command to which a short response is expected.
+
+ @type command: L{bytes}
+ @param command: A POP3 command.
+
+ @type params: stringifyable L{object} or L{None}
+ @param params: Command arguments.
+ """
+ if params is not None:
+ if not isinstance(params, bytes):
+ params = str(params).encode("utf-8")
+ self.sendLine(command + b" " + params)
+ else:
+ self.sendLine(command)
+ self.command = command
+ self.mode = SHORT
+
+ def sendLong(self, command, params):
+ """
+ Send a POP3 command to which a long response is expected.
+
+ @type command: L{bytes}
+ @param command: A POP3 command.
+
+ @type params: stringifyable L{object}
+ @param params: Command arguments.
+ """
+ if params:
+ if not isinstance(params, bytes):
+ params = str(params).encode("utf-8")
+ self.sendLine(command + b" " + params)
+ else:
+ self.sendLine(command)
+ self.command = command
+ self.mode = FIRST_LONG
+
+ def handle_default(self, line):
+ """
+ Handle responses from the server for which no other handler exists.
+
+ @type line: L{bytes}
+ @param line: A received line.
+ """
+ if line[:-4] == b"-ERR":
+ self.mode = NONE
+
+ def handle_WELCOME(self, line):
+ """
+ Handle a server response which is expected to be a server greeting.
+
+ @type line: L{bytes}
+ @param line: A received line.
+ """
+ code, data = line.split(b" ", 1)
+ if code != b"+OK":
+ self.transport.loseConnection()
+ else:
+ m = self.welcomeRe.match(line)
+ if m:
+ self.welcomeCode = m.group(1)
+
+ def _dispatch(self, command, default, *args):
+ """
+ Dispatch a response from the server for handling.
+
+ Command X is dispatched to handle_X() if it exists. If not, it is
+ dispatched to the default handler.
+
+ @type command: L{bytes}
+ @param command: The command.
+
+ @type default: callable that takes L{bytes} or
+ L{None}
+ @param default: The default handler.
+
+ @type args: L{tuple} or L{None}
+ @param args: Arguments to the handler function.
+ """
+ try:
+ method = getattr(self, "handle_" + command.decode("utf-8"), default)
+ if method is not None:
+ method(*args)
+ except BaseException:
+ log.err()
+
+ def lineReceived(self, line):
+ """
+ Dispatch a received line for processing.
+
+ The choice of function to handle the received line is based on the
+ type of response expected to the command sent to the server and how
+ much of that response has been received.
+
+ An expected one line response to command X is handled by handle_X().
+ The first line of a multi-line response to command X is also handled by
+ handle_X(). Subsequent lines of the multi-line response are handled by
+ handle_X_continue() except for the last line which is handled by
+ handle_X_end().
+
+ @type line: L{bytes}
+ @param line: A received line.
+ """
+ if self.mode == SHORT or self.mode == FIRST_LONG:
+ self.mode = NEXT[self.mode]
+ self._dispatch(self.command, self.handle_default, line)
+ elif self.mode == LONG:
+ if line == b".":
+ self.mode = NEXT[self.mode]
+ self._dispatch(self.command + b"_end", None)
+ return
+ if line[:1] == b".":
+ line = line[1:]
+ self._dispatch(self.command + b"_continue", None, line)
+
+ def apopAuthenticate(self, user, password, magic):
+ """
+ Perform an authenticated login.
+
+ @type user: L{bytes}
+ @param user: The username with which to log in.
+
+ @type password: L{bytes}
+ @param password: The password with which to log in.
+
+ @type magic: L{bytes}
+ @param magic: The challenge provided by the server.
+ """
+ digest = md5(magic + password).hexdigest().encode("ascii")
+ self.apop(user, digest)
+
+ def apop(self, user, digest):
+ """
+ Send an APOP command to perform authenticated login.
+
+ @type user: L{bytes}
+ @param user: The username with which to log in.
+
+ @type digest: L{bytes}
+ @param digest: The challenge response with which to authenticate.
+ """
+ self.sendLong(b"APOP", b" ".join((user, digest)))
+
+ def retr(self, i):
+ """
+ Send a RETR command to retrieve a message from the server.
+
+ @type i: L{int} or L{bytes}
+ @param i: A 0-based message index.
+ """
+ self.sendLong(b"RETR", i)
+
+ def dele(self, i):
+ """
+ Send a DELE command to delete a message from the server.
+
+ @type i: L{int} or L{bytes}
+ @param i: A 0-based message index.
+ """
+ self.sendShort(b"DELE", i)
+
+ def list(self, i=""):
+ """
+ Send a LIST command to retrieve the size of a message or, if no message
+ is specified, the sizes of all messages.
+
+ @type i: L{int} or L{bytes}
+ @param i: A 0-based message index or the empty string to specify all
+ messages.
+ """
+ self.sendLong(b"LIST", i)
+
+ def uidl(self, i=""):
+ """
+ Send a UIDL command to retrieve the unique identifier of a message or,
+ if no message is specified, the unique identifiers of all messages.
+
+ @type i: L{int} or L{bytes}
+ @param i: A 0-based message index or the empty string to specify all
+ messages.
+ """
+ self.sendLong(b"UIDL", i)
+
+ def user(self, name):
+ """
+ Send a USER command to perform the first half of a plaintext login.
+
+ @type name: L{bytes}
+ @param name: The username with which to log in.
+ """
+ self.sendShort(b"USER", name)
+
+ def password(self, password):
+ """
+ Perform the second half of a plaintext login.
+
+ @type password: L{bytes}
+ @param password: The plaintext password with which to authenticate.
+ """
+ self.sendShort(b"PASS", password)
+
+ pass_ = password
+
+ def quit(self):
+ """
+ Send a QUIT command to disconnect from the server.
+ """
+ self.sendShort(b"QUIT")
+
+
+from twisted.mail._except import (
+ InsecureAuthenticationDisallowed,
+ LineTooLong,
+ ServerErrorResponse,
+ TLSError,
+ TLSNotSupportedError,
+)
+from twisted.mail._pop3client import POP3Client as AdvancedPOP3Client
+
+__all__ = [
+ # Interfaces
+ "IMailbox",
+ "IServerFactory",
+ # Exceptions
+ "POP3Error",
+ "POP3ClientError",
+ "InsecureAuthenticationDisallowed",
+ "ServerErrorResponse",
+ "LineTooLong",
+ "TLSError",
+ "TLSNotSupportedError",
+ # Protocol classes
+ "POP3",
+ "POP3Client",
+ "AdvancedPOP3Client",
+ # Misc
+ "APOPCredentials",
+ "Mailbox",
+]
diff --git a/contrib/python/Twisted/py3/twisted/mail/pop3client.py b/contrib/python/Twisted/py3/twisted/mail/pop3client.py
new file mode 100644
index 0000000000..72676a69a8
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/pop3client.py
@@ -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] = []
diff --git a/contrib/python/Twisted/py3/twisted/mail/protocols.py b/contrib/python/Twisted/py3/twisted/mail/protocols.py
new file mode 100644
index 0000000000..7bd1eebbee
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/protocols.py
@@ -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
diff --git a/contrib/python/Twisted/py3/twisted/mail/relay.py b/contrib/python/Twisted/py3/twisted/mail/relay.py
new file mode 100644
index 0000000000..4ba50ea378
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/relay.py
@@ -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)
diff --git a/contrib/python/Twisted/py3/twisted/mail/relaymanager.py b/contrib/python/Twisted/py3/twisted/mail/relaymanager.py
new file mode 100644
index 0000000000..18cc287833
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/relaymanager.py
@@ -0,0 +1,1135 @@
+# -*- test-case-name: twisted.mail.test.test_mail -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+
+"""
+Infrastructure for relaying mail through a smart host.
+
+Traditional peer-to-peer email has been increasingly replaced by smart host
+configurations. Instead of sending mail directly to the recipient, a sender
+sends mail to a smart host. The smart host finds the mail exchange server for
+the recipient and sends on the message.
+"""
+
+import email.utils
+import os
+import pickle
+import time
+from typing import Type
+
+from twisted.application import internet
+from twisted.internet import protocol
+from twisted.internet.defer import Deferred, DeferredList
+from twisted.internet.error import DNSLookupError
+from twisted.internet.protocol import connectionDone
+from twisted.mail import bounce, relay, smtp
+from twisted.python import log
+from twisted.python.failure import Failure
+
+
+class ManagedRelayerMixin:
+ """
+ SMTP Relayer which notifies a manager
+
+ Notify the manager about successful mail, failed mail
+ and broken connections
+ """
+
+ def __init__(self, manager):
+ self.manager = manager
+
+ @property
+ def factory(self):
+ return self._factory
+
+ @factory.setter
+ def factory(self, value):
+ self._factory = value
+
+ def sentMail(self, code, resp, numOk, addresses, log):
+ """
+ called when e-mail has been sent
+
+ we will always get 0 or 1 addresses.
+ """
+ message = self.names[0]
+ if code in smtp.SUCCESS:
+ self.manager.notifySuccess(self.factory, message)
+ else:
+ self.manager.notifyFailure(self.factory, message)
+ del self.messages[0]
+ del self.names[0]
+
+ def connectionLost(self, reason: Failure = connectionDone) -> None:
+ """
+ called when connection is broken
+
+ notify manager we will try to send no more e-mail
+ """
+ self.manager.notifyDone(self.factory)
+
+
+class SMTPManagedRelayer(ManagedRelayerMixin, relay.SMTPRelayer): # type: ignore[misc]
+ """
+ An SMTP managed relayer.
+
+ This managed relayer is an SMTP client which is responsible for sending a
+ set of messages and keeping an attempt manager informed about its progress.
+
+ @type factory: L{SMTPManagedRelayerFactory}
+ @ivar factory: The factory that created this relayer. This must be set by
+ the factory.
+ """
+
+ def __init__(self, messages, manager, *args, **kw):
+ """
+ @type messages: L{list} of L{bytes}
+ @param messages: The base filenames of messages to be relayed.
+
+ @type manager: L{_AttemptManager}
+ @param manager: An attempt manager.
+
+ @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__}
+ """
+ ManagedRelayerMixin.__init__(self, manager)
+ relay.SMTPRelayer.__init__(self, messages, *args, **kw)
+
+
+class ESMTPManagedRelayer(ManagedRelayerMixin, relay.ESMTPRelayer): # type: ignore[misc]
+ """
+ An ESMTP managed relayer.
+
+ This managed relayer is an ESMTP client which is responsible for sending a
+ set of messages and keeping an attempt manager informed about its progress.
+ """
+
+ def __init__(self, messages, manager, *args, **kw):
+ """
+ @type messages: L{list} of L{bytes}
+ @param messages: The base filenames of messages to be relayed.
+
+ @type manager: L{_AttemptManager}
+ @param manager: An attempt manager.
+
+ @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__}
+ """
+ ManagedRelayerMixin.__init__(self, manager)
+ relay.ESMTPRelayer.__init__(self, messages, *args, **kw)
+
+
+class SMTPManagedRelayerFactory(protocol.ClientFactory):
+ """
+ A factory to create an L{SMTPManagedRelayer}.
+
+ This factory creates a managed relayer which relays a set of messages over
+ SMTP and informs an attempt manager of its progress.
+
+ @ivar messages: See L{__init__}
+ @ivar manager: See L{__init__}
+
+ @type protocol: callable which returns L{SMTPManagedRelayer}
+ @ivar protocol: A callable which returns a managed relayer for SMTP. See
+ L{SMTPManagedRelayer.__init__} for parameters to the callable.
+
+ @type pArgs: 1-L{tuple} of (0) L{bytes} or 2-L{tuple} of
+ (0) L{bytes}, (1), L{int}
+ @ivar pArgs: Positional arguments for L{SMTPClient.__init__}
+
+ @type pKwArgs: L{dict}
+ @ivar pKwArgs: Keyword arguments for L{SMTPClient.__init__}
+ """
+
+ protocol: "Type[protocol.Protocol]" = SMTPManagedRelayer
+
+ def __init__(self, messages, manager, *args, **kw):
+ """
+ @type messages: L{list} of L{bytes}
+ @param messages: The base filenames of messages to be relayed.
+
+ @type manager: L{_AttemptManager}
+ @param manager: An attempt manager.
+
+ @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__}
+ """
+ self.messages = messages
+ self.manager = manager
+ self.pArgs = args
+ self.pKwArgs = kw
+
+ def buildProtocol(self, addr):
+ """
+ Create an L{SMTPManagedRelayer}.
+
+ @type addr: L{IAddress <twisted.internet.interfaces.IAddress>} provider
+ @param addr: The address of the SMTP server.
+
+ @rtype: L{SMTPManagedRelayer}
+ @return: A managed relayer for SMTP.
+ """
+ protocol = self.protocol(
+ self.messages, self.manager, *self.pArgs, **self.pKwArgs
+ )
+ protocol.factory = self
+ return protocol
+
+ def clientConnectionFailed(self, connector, reason):
+ """
+ Notify the attempt manager that a connection could not be established.
+
+ @type connector: L{IConnector <twisted.internet.interfaces.IConnector>}
+ provider
+ @param connector: A connector.
+
+ @type reason: L{Failure}
+ @param reason: The reason the connection attempt failed.
+ """
+ self.manager.notifyNoConnection(self)
+ self.manager.notifyDone(self)
+
+
+class ESMTPManagedRelayerFactory(SMTPManagedRelayerFactory):
+ """
+ A factory to create an L{ESMTPManagedRelayer}.
+
+ This factory creates a managed relayer which relays a set of messages over
+ ESMTP and informs an attempt manager of its progress.
+
+ @type protocol: callable which returns L{ESMTPManagedRelayer}
+ @ivar protocol: A callable which returns a managed relayer for ESMTP. See
+ L{ESMTPManagedRelayer.__init__} for parameters to the callable.
+
+ @ivar secret: See L{__init__}
+ @ivar contextFactory: See L{__init__}
+ """
+
+ protocol = ESMTPManagedRelayer
+
+ def __init__(self, messages, manager, secret, contextFactory, *args, **kw):
+ """
+ @type messages: L{list} of L{bytes}
+ @param messages: The base filenames of messages to be relayed.
+
+ @type manager: L{_AttemptManager}
+ @param manager: An attempt manager.
+
+ @type secret: L{bytes}
+ @param secret: A string for the authentication challenge response.
+
+ @type contextFactory: L{None} or
+ L{ClientContextFactory <twisted.internet.ssl.ClientContextFactory>}
+ @param contextFactory: An SSL context factory.
+
+ @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__}
+
+ @param kw: Keyword arguments for L{SMTPClient.__init__}
+ """
+ self.secret = secret
+ self.contextFactory = contextFactory
+ SMTPManagedRelayerFactory.__init__(self, messages, manager, *args, **kw)
+
+ def buildProtocol(self, addr):
+ """
+ Create an L{ESMTPManagedRelayer}.
+
+ @type addr: L{IAddress <twisted.internet.interfaces.IAddress>} provider
+ @param addr: The address of the ESMTP server.
+
+ @rtype: L{ESMTPManagedRelayer}
+ @return: A managed relayer for ESMTP.
+ """
+ s = self.secret and self.secret(addr)
+ protocol = self.protocol(
+ self.messages,
+ self.manager,
+ s,
+ self.contextFactory,
+ *self.pArgs,
+ **self.pKwArgs,
+ )
+ protocol.factory = self
+ return protocol
+
+
+class Queue:
+ """
+ A queue for messages to be relayed.
+
+ @ivar directory: See L{__init__}
+
+ @type n: L{int}
+ @ivar n: A number used to form unique filenames.
+
+ @type waiting: L{dict} of L{bytes}
+ @ivar waiting: The base filenames of messages waiting to be relayed.
+
+ @type relayed: L{dict} of L{bytes}
+ @ivar relayed: The base filenames of messages in the process of being
+ relayed.
+
+ @type noisy: L{bool}
+ @ivar noisy: A flag which determines whether informational log messages
+ will be generated (C{True}) or not (C{False}).
+ """
+
+ noisy = True
+
+ def __init__(self, directory):
+ """
+ Initialize non-volatile state.
+
+ @type directory: L{bytes}
+ @param directory: The pathname of the directory holding messages in the
+ queue.
+ """
+ self.directory = directory
+ self._init()
+
+ def _init(self):
+ """
+ Initialize volatile state.
+ """
+ self.n = 0
+ self.waiting = {}
+ self.relayed = {}
+ self.readDirectory()
+
+ def __getstate__(self):
+ """
+ Create a representation of the non-volatile state of the queue.
+
+ @rtype: L{dict} mapping L{bytes} to L{object}
+ @return: The non-volatile state of the queue.
+ """
+ return {"directory": self.directory}
+
+ def __setstate__(self, state):
+ """
+ Restore the non-volatile state of the queue and recreate the volatile
+ state.
+
+ @type state: L{dict} mapping L{bytes} to L{object}
+ @param state: The non-volatile state of the queue.
+ """
+ self.__dict__.update(state)
+ self._init()
+
+ def readDirectory(self):
+ """
+ Scan the message directory for new messages.
+ """
+ for message in os.listdir(self.directory):
+ # Skip non data files
+ if message[-2:] != "-D":
+ continue
+ self.addMessage(message[:-2])
+
+ def getWaiting(self):
+ """
+ Return the base filenames of messages waiting to be relayed.
+
+ @rtype: L{list} of L{bytes}
+ @return: The base filenames of messages waiting to be relayed.
+ """
+ return self.waiting.keys()
+
+ def hasWaiting(self):
+ """
+ Return an indication of whether the queue has messages waiting to be
+ relayed.
+
+ @rtype: L{bool}
+ @return: C{True} if messages are waiting to be relayed. C{False}
+ otherwise.
+ """
+ return len(self.waiting) > 0
+
+ def getRelayed(self):
+ """
+ Return the base filenames of messages in the process of being relayed.
+
+ @rtype: L{list} of L{bytes}
+ @return: The base filenames of messages in the process of being
+ relayed.
+ """
+ return self.relayed.keys()
+
+ def setRelaying(self, message):
+ """
+ Mark a message as being relayed.
+
+ @type message: L{bytes}
+ @param message: The base filename of a message.
+ """
+ del self.waiting[message]
+ self.relayed[message] = 1
+
+ def setWaiting(self, message):
+ """
+ Mark a message as waiting to be relayed.
+
+ @type message: L{bytes}
+ @param message: The base filename of a message.
+ """
+ del self.relayed[message]
+ self.waiting[message] = 1
+
+ def addMessage(self, message):
+ """
+ Mark a message as waiting to be relayed unless it is in the process of
+ being relayed.
+
+ @type message: L{bytes}
+ @param message: The base filename of a message.
+ """
+ if message not in self.relayed:
+ self.waiting[message] = 1
+ if self.noisy:
+ log.msg("Set " + message + " waiting")
+
+ def done(self, message):
+ """
+ Remove a message from the queue.
+
+ @type message: L{bytes}
+ @param message: The base filename of a message.
+ """
+ message = os.path.basename(message)
+ os.remove(self.getPath(message) + "-D")
+ os.remove(self.getPath(message) + "-H")
+ del self.relayed[message]
+
+ def getPath(self, message):
+ """
+ Return the full base pathname of a message in the queue.
+
+ @type message: L{bytes}
+ @param message: The base filename of a message.
+
+ @rtype: L{bytes}
+ @return: The full base pathname of the message.
+ """
+ return os.path.join(self.directory, message)
+
+ def getEnvelope(self, message):
+ """
+ Get the envelope for a message.
+
+ @type message: L{bytes}
+ @param message: The base filename of a message.
+
+ @rtype: L{list} of two L{bytes}
+ @return: A list containing the origination and destination addresses
+ for the message.
+ """
+ with self.getEnvelopeFile(message) as f:
+ return pickle.load(f)
+
+ def getEnvelopeFile(self, message):
+ """
+ Return the envelope file for a message in the queue.
+
+ @type message: L{bytes}
+ @param message: The base filename of a message.
+
+ @rtype: file
+ @return: The envelope file for the message.
+ """
+ return open(os.path.join(self.directory, message + "-H"), "rb")
+
+ def createNewMessage(self):
+ """
+ Create a new message in the queue.
+
+ @rtype: 2-L{tuple} of (0) file, (1) L{FileMessage}
+ @return: The envelope file and a message receiver for a new message in
+ the queue.
+ """
+ fname = f"{os.getpid()}_{time.time()}_{self.n}_{id(self)}"
+ self.n = self.n + 1
+ headerFile = open(os.path.join(self.directory, fname + "-H"), "wb")
+ tempFilename = os.path.join(self.directory, fname + "-C")
+ finalFilename = os.path.join(self.directory, fname + "-D")
+ messageFile = open(tempFilename, "wb")
+
+ from twisted.mail.mail import FileMessage
+
+ return headerFile, FileMessage(messageFile, tempFilename, finalFilename)
+
+
+class _AttemptManager:
+ """
+ A manager for an attempt to relay a set of messages to a mail exchange
+ server.
+
+ @ivar manager: See L{__init__}
+
+ @type _completionDeferreds: L{list} of L{Deferred}
+ @ivar _completionDeferreds: Deferreds which are to be notified when the
+ attempt to relay is finished.
+ """
+
+ def __init__(self, manager, noisy=True, reactor=None):
+ """
+ @type manager: L{SmartHostSMTPRelayingManager}
+ @param manager: A smart host.
+
+ @type noisy: L{bool}
+ @param noisy: A flag which determines whether informational log
+ messages will be generated (L{True}) or not (L{False}).
+
+ @type reactor: L{IReactorTime
+ <twisted.internet.interfaces.IReactorTime>} provider
+ @param reactor: A reactor which will be used to schedule delayed calls.
+ """
+ self.manager = manager
+ self._completionDeferreds = []
+ self.noisy = noisy
+
+ if not reactor:
+ from twisted.internet import reactor
+ self.reactor = reactor
+
+ def getCompletionDeferred(self):
+ """
+ Return a deferred which will fire when the attempt to relay is
+ finished.
+
+ @rtype: L{Deferred}
+ @return: A deferred which will fire when the attempt to relay is
+ finished.
+ """
+ self._completionDeferreds.append(Deferred())
+ return self._completionDeferreds[-1]
+
+ def _finish(self, relay, message):
+ """
+ Remove a message from the relay queue and from the smart host's list of
+ messages being relayed.
+
+ @type relay: L{SMTPManagedRelayerFactory}
+ @param relay: The factory for the relayer which sent the message.
+
+ @type message: L{bytes}
+ @param message: The path of the file holding the message.
+ """
+ self.manager.managed[relay].remove(os.path.basename(message))
+ self.manager.queue.done(message)
+
+ def notifySuccess(self, relay, message):
+ """
+ Remove a message from the relay queue after it has been successfully
+ sent.
+
+ @type relay: L{SMTPManagedRelayerFactory}
+ @param relay: The factory for the relayer which sent the message.
+
+ @type message: L{bytes}
+ @param message: The path of the file holding the message.
+ """
+ if self.noisy:
+ log.msg("success sending %s, removing from queue" % message)
+ self._finish(relay, message)
+
+ def notifyFailure(self, relay, message):
+ """
+ Generate a bounce message for a message which cannot be relayed.
+
+ @type relay: L{SMTPManagedRelayerFactory}
+ @param relay: The factory for the relayer responsible for the message.
+
+ @type message: L{bytes}
+ @param message: The path of the file holding the message.
+ """
+ if self.noisy:
+ log.msg("could not relay " + message)
+ # Moshe - Bounce E-mail here
+ # Be careful: if it's a bounced bounce, silently
+ # discard it
+ message = os.path.basename(message)
+ with self.manager.queue.getEnvelopeFile(message) as fp:
+ from_, to = pickle.load(fp)
+ from_, to, bounceMessage = bounce.generateBounce(
+ open(self.manager.queue.getPath(message) + "-D"), from_, to
+ )
+ fp, outgoingMessage = self.manager.queue.createNewMessage()
+ with fp:
+ pickle.dump([from_, to], fp)
+ for line in bounceMessage.splitlines():
+ outgoingMessage.lineReceived(line)
+ outgoingMessage.eomReceived()
+ self._finish(relay, self.manager.queue.getPath(message))
+
+ def notifyDone(self, relay):
+ """
+ When the connection is lost or cannot be established, prepare to
+ resend unsent messages and fire all deferred which are waiting for
+ the completion of the attempt to relay.
+
+ @type relay: L{SMTPManagedRelayerFactory}
+ @param relay: The factory for the relayer for the connection.
+ """
+ for message in self.manager.managed.get(relay, ()):
+ if self.noisy:
+ log.msg("Setting " + message + " waiting")
+ self.manager.queue.setWaiting(message)
+ try:
+ del self.manager.managed[relay]
+ except KeyError:
+ pass
+ notifications = self._completionDeferreds
+ self._completionDeferreds = None
+ for d in notifications:
+ d.callback(None)
+
+ def notifyNoConnection(self, relay):
+ """
+ When a connection to the mail exchange server cannot be established,
+ prepare to resend messages later.
+
+ @type relay: L{SMTPManagedRelayerFactory}
+ @param relay: The factory for the relayer meant to use the connection.
+ """
+ # Back off a bit
+ try:
+ msgs = self.manager.managed[relay]
+ except KeyError:
+ log.msg("notifyNoConnection passed unknown relay!")
+ return
+
+ if self.noisy:
+ log.msg("Backing off on delivery of " + str(msgs))
+
+ def setWaiting(queue, messages):
+ map(queue.setWaiting, messages)
+
+ self.reactor.callLater(30, setWaiting, self.manager.queue, msgs)
+ del self.manager.managed[relay]
+
+
+class SmartHostSMTPRelayingManager:
+ """
+ A smart host which uses SMTP managed relayers to send messages from the
+ relay queue.
+
+ L{checkState} must be called periodically at which time the state of the
+ relay queue is checked and new relayers are created as needed.
+
+ In order to relay a set of messages to a mail exchange server, a smart host
+ creates an attempt manager and a managed relayer factory for that set of
+ messages. When a connection is made with the mail exchange server, the
+ managed relayer factory creates a managed relayer to send the messages.
+ The managed relayer reports on its progress to the attempt manager which,
+ in turn, updates the smart host's relay queue and information about its
+ managed relayers.
+
+ @ivar queue: See L{__init__}.
+ @ivar maxConnections: See L{__init__}.
+ @ivar maxMessagesPerConnection: See L{__init__}.
+
+ @type fArgs: 3-L{tuple} of (0) L{list} of L{bytes},
+ (1) L{_AttemptManager}, (2) L{bytes} or 4-L{tuple} of (0) L{list}
+ of L{bytes}, (1) L{_AttemptManager}, (2) L{bytes}, (3) L{int}
+ @ivar fArgs: Positional arguments for
+ L{SMTPManagedRelayerFactory.__init__}.
+
+ @type fKwArgs: L{dict}
+ @ivar fKwArgs: Keyword arguments for L{SMTPManagedRelayerFactory.__init__}.
+
+ @type factory: callable which returns L{SMTPManagedRelayerFactory}
+ @ivar factory: A callable which creates a factory for creating a managed
+ relayer. See L{SMTPManagedRelayerFactory.__init__} for parameters to
+ the callable.
+
+ @type PORT: L{int}
+ @ivar PORT: The port over which to connect to the SMTP server.
+
+ @type mxcalc: L{None} or L{MXCalculator}
+ @ivar mxcalc: A resource for mail exchange host lookups.
+
+ @type managed: L{dict} mapping L{SMTPManagedRelayerFactory} to L{list} of
+ L{bytes}
+ @ivar managed: A mapping of factory for a managed relayer to
+ filenames of messages the managed relayer is responsible for.
+ """
+
+ factory: Type[protocol.ClientFactory] = SMTPManagedRelayerFactory
+
+ PORT = 25
+
+ mxcalc = None
+
+ def __init__(self, queue, maxConnections=2, maxMessagesPerConnection=10):
+ """
+ Initialize a smart host.
+
+ The default values specify connection limits appropriate for a
+ low-volume smart host.
+
+ @type queue: L{Queue}
+ @param queue: A relay queue.
+
+ @type maxConnections: L{int}
+ @param maxConnections: The maximum number of concurrent connections to
+ SMTP servers.
+
+ @type maxMessagesPerConnection: L{int}
+ @param maxMessagesPerConnection: The maximum number of messages for
+ which a relayer will be given responsibility.
+ """
+ self.maxConnections = maxConnections
+ self.maxMessagesPerConnection = maxMessagesPerConnection
+ self.managed = {} # SMTP clients we're managing
+ self.queue = queue
+ self.fArgs = ()
+ self.fKwArgs = {}
+
+ def __getstate__(self):
+ """
+ Create a representation of the non-volatile state of this object.
+
+ @rtype: L{dict} mapping L{bytes} to L{object}
+ @return: The non-volatile state of the queue.
+ """
+ dct = self.__dict__.copy()
+ del dct["managed"]
+ return dct
+
+ def __setstate__(self, state):
+ """
+ Restore the non-volatile state of this object and recreate the volatile
+ state.
+
+ @type state: L{dict} mapping L{bytes} to L{object}
+ @param state: The non-volatile state of the queue.
+ """
+ self.__dict__.update(state)
+ self.managed = {}
+
+ def checkState(self):
+ """
+ Check the state of the relay queue and, if possible, launch relayers to
+ handle waiting messages.
+
+ @rtype: L{None} or L{Deferred}
+ @return: No return value if no further messages can be relayed or a
+ deferred which fires when all of the SMTP connections initiated by
+ this call have disconnected.
+ """
+ self.queue.readDirectory()
+ if len(self.managed) >= self.maxConnections:
+ return
+ if not self.queue.hasWaiting():
+ return
+
+ return self._checkStateMX()
+
+ def _checkStateMX(self):
+ nextMessages = self.queue.getWaiting()
+ nextMessages.reverse()
+
+ exchanges = {}
+ for msg in nextMessages:
+ from_, to = self.queue.getEnvelope(msg)
+ name, addr = email.utils.parseaddr(to)
+ parts = addr.split("@", 1)
+ if len(parts) != 2:
+ log.err("Illegal message destination: " + to)
+ continue
+ domain = parts[1]
+
+ self.queue.setRelaying(msg)
+ exchanges.setdefault(domain, []).append(self.queue.getPath(msg))
+ if len(exchanges) >= (self.maxConnections - len(self.managed)):
+ break
+
+ if self.mxcalc is None:
+ self.mxcalc = MXCalculator()
+
+ relays = []
+ for domain, msgs in exchanges.iteritems():
+ manager = _AttemptManager(self, self.queue.noisy)
+ factory = self.factory(msgs, manager, *self.fArgs, **self.fKwArgs)
+ self.managed[factory] = map(os.path.basename, msgs)
+ relayAttemptDeferred = manager.getCompletionDeferred()
+ connectSetupDeferred = self.mxcalc.getMX(domain)
+ connectSetupDeferred.addCallback(lambda mx: str(mx.name))
+ connectSetupDeferred.addCallback(self._cbExchange, self.PORT, factory)
+ connectSetupDeferred.addErrback(
+ lambda err: (relayAttemptDeferred.errback(err), err)[1]
+ )
+ connectSetupDeferred.addErrback(self._ebExchange, factory, domain)
+ relays.append(relayAttemptDeferred)
+ return DeferredList(relays)
+
+ def _cbExchange(self, address, port, factory):
+ """
+ Initiate a connection with a mail exchange server.
+
+ This callback function runs after mail exchange server for the domain
+ has been looked up.
+
+ @type address: L{bytes}
+ @param address: The hostname of a mail exchange server.
+
+ @type port: L{int}
+ @param port: A port number.
+
+ @type factory: L{SMTPManagedRelayerFactory}
+ @param factory: A factory which can create a relayer for the mail
+ exchange server.
+ """
+ from twisted.internet import reactor
+
+ reactor.connectTCP(address, port, factory)
+
+ def _ebExchange(self, failure, factory, domain):
+ """
+ Prepare to resend messages later.
+
+ This errback function runs when no mail exchange server for the domain
+ can be found.
+
+ @type failure: L{Failure}
+ @param failure: The reason the mail exchange lookup failed.
+
+ @type factory: L{SMTPManagedRelayerFactory}
+ @param factory: A factory which can create a relayer for the mail
+ exchange server.
+
+ @type domain: L{bytes}
+ @param domain: A domain.
+ """
+ log.err("Error setting up managed relay factory for " + domain)
+ log.err(failure)
+
+ def setWaiting(queue, messages):
+ map(queue.setWaiting, messages)
+
+ from twisted.internet import reactor
+
+ reactor.callLater(30, setWaiting, self.queue, self.managed[factory])
+ del self.managed[factory]
+
+
+class SmartHostESMTPRelayingManager(SmartHostSMTPRelayingManager):
+ """
+ A smart host which uses ESMTP managed relayers to send messages from the
+ relay queue.
+
+ @type factory: callable which returns L{ESMTPManagedRelayerFactory}
+ @ivar factory: A callable which creates a factory for creating a managed
+ relayer. See L{ESMTPManagedRelayerFactory.__init__} for parameters to
+ the callable.
+ """
+
+ factory = ESMTPManagedRelayerFactory
+
+
+def _checkState(manager):
+ """
+ Prompt a relaying manager to check state.
+
+ @type manager: L{SmartHostSMTPRelayingManager}
+ @param manager: A relaying manager.
+ """
+ manager.checkState()
+
+
+def RelayStateHelper(manager, delay):
+ """
+ Set up a periodic call to prompt a relaying manager to check state.
+
+ @type manager: L{SmartHostSMTPRelayingManager}
+ @param manager: A relaying manager.
+
+ @type delay: L{float}
+ @param delay: The number of seconds between calls.
+
+ @rtype: L{TimerService <internet.TimerService>}
+ @return: A service which periodically reminds a relaying manager to check
+ state.
+ """
+ return internet.TimerService(delay, _checkState, manager)
+
+
+class CanonicalNameLoop(Exception):
+ """
+ An error indicating that when trying to look up a mail exchange host, a set
+ of canonical name records was found which form a cycle and resolution was
+ abandoned.
+ """
+
+
+class CanonicalNameChainTooLong(Exception):
+ """
+ An error indicating that when trying to look up a mail exchange host, too
+ many canonical name records which point to other canonical name records
+ were encountered and resolution was abandoned.
+ """
+
+
+class MXCalculator:
+ """
+ A utility for looking up mail exchange hosts and tracking whether they are
+ working or not.
+
+ @type clock: L{IReactorTime <twisted.internet.interfaces.IReactorTime>}
+ provider
+ @ivar clock: A reactor which will be used to schedule timeouts.
+
+ @type resolver: L{IResolver <twisted.internet.interfaces.IResolver>}
+ @ivar resolver: A resolver.
+
+ @type badMXs: L{dict} mapping L{bytes} to L{float}
+ @ivar badMXs: A mapping of non-functioning mail exchange hostname to time
+ at which another attempt at contacting it may be made.
+
+ @type timeOutBadMX: L{int}
+ @ivar timeOutBadMX: Period in seconds between attempts to contact a
+ non-functioning mail exchange host.
+
+ @type fallbackToDomain: L{bool}
+ @ivar fallbackToDomain: A flag indicating whether to attempt to use the
+ hostname directly when no mail exchange can be found (C{True}) or
+ not (C{False}).
+ """
+
+ timeOutBadMX = 60 * 60 # One hour
+ fallbackToDomain = True
+
+ def __init__(self, resolver=None, clock=None):
+ """
+ @type resolver: L{IResolver <twisted.internet.interfaces.IResolver>}
+ provider or L{None}
+ @param resolver: A resolver.
+
+ @type clock: L{IReactorTime <twisted.internet.interfaces.IReactorTime>}
+ provider or L{None}
+ @param clock: A reactor which will be used to schedule timeouts.
+ """
+ self.badMXs = {}
+ if resolver is None:
+ from twisted.names.client import createResolver
+
+ resolver = createResolver()
+ self.resolver = resolver
+ if clock is None:
+ from twisted.internet import reactor as clock
+ self.clock = clock
+
+ def markBad(self, mx):
+ """
+ Record that a mail exchange host is not currently functioning.
+
+ @type mx: L{bytes}
+ @param mx: The hostname of a mail exchange host.
+ """
+ self.badMXs[str(mx)] = self.clock.seconds() + self.timeOutBadMX
+
+ def markGood(self, mx):
+ """
+ Record that a mail exchange host is functioning.
+
+ @type mx: L{bytes}
+ @param mx: The hostname of a mail exchange host.
+ """
+ try:
+ del self.badMXs[mx]
+ except KeyError:
+ pass
+
+ def getMX(self, domain, maximumCanonicalChainLength=3):
+ """
+ Find the name of a host that acts as a mail exchange server
+ for a domain.
+
+ @type domain: L{bytes}
+ @param domain: A domain name.
+
+ @type maximumCanonicalChainLength: L{int}
+ @param maximumCanonicalChainLength: The maximum number of unique
+ canonical name records to follow while looking up the mail exchange
+ host.
+
+ @rtype: L{Deferred} which successfully fires with L{Record_MX}
+ @return: A deferred which succeeds with the MX record for the mail
+ exchange server for the domain or fails if none can be found.
+ """
+ mailExchangeDeferred = self.resolver.lookupMailExchange(domain)
+ mailExchangeDeferred.addCallback(self._filterRecords)
+ mailExchangeDeferred.addCallback(
+ self._cbMX, domain, maximumCanonicalChainLength
+ )
+ mailExchangeDeferred.addErrback(self._ebMX, domain)
+ return mailExchangeDeferred
+
+ def _filterRecords(self, records):
+ """
+ Organize the records of a DNS response by record name.
+
+ @type records: 3-L{tuple} of (0) L{list} of L{RRHeader
+ <twisted.names.dns.RRHeader>}, (1) L{list} of L{RRHeader
+ <twisted.names.dns.RRHeader>}, (2) L{list} of L{RRHeader
+ <twisted.names.dns.RRHeader>}
+ @param records: Answer resource records, authority resource records and
+ additional resource records.
+
+ @rtype: L{dict} mapping L{bytes} to L{list} of L{IRecord
+ <twisted.names.dns.IRecord>} provider
+ @return: A mapping of record name to record payload.
+ """
+ recordBag = {}
+ for answer in records[0]:
+ recordBag.setdefault(str(answer.name), []).append(answer.payload)
+ return recordBag
+
+ def _cbMX(self, answers, domain, cnamesLeft):
+ """
+ Try to find the mail exchange host for a domain from the given DNS
+ records.
+
+ This will attempt to resolve canonical name record results. It can
+ recognize loops and will give up on non-cyclic chains after a specified
+ number of lookups.
+
+ @type answers: L{dict} mapping L{bytes} to L{list} of L{IRecord
+ <twisted.names.dns.IRecord>} provider
+ @param answers: A mapping of record name to record payload.
+
+ @type domain: L{bytes}
+ @param domain: A domain name.
+
+ @type cnamesLeft: L{int}
+ @param cnamesLeft: The number of unique canonical name records
+ left to follow while looking up the mail exchange host.
+
+ @rtype: L{Record_MX <twisted.names.dns.Record_MX>} or L{Failure}
+ @return: An MX record for the mail exchange host or a failure if one
+ cannot be found.
+ """
+ # Do this import here so that relaymanager.py doesn't depend on
+ # twisted.names, only MXCalculator will.
+ from twisted.names import dns, error
+
+ seenAliases = set()
+ exchanges = []
+ # Examine the answers for the domain we asked about
+ pertinentRecords = answers.get(domain, [])
+ while pertinentRecords:
+ record = pertinentRecords.pop()
+
+ # If it's a CNAME, we'll need to do some more processing
+ if record.TYPE == dns.CNAME:
+ # Remember that this name was an alias.
+ seenAliases.add(domain)
+
+ canonicalName = str(record.name)
+ # See if we have some local records which might be relevant.
+ if canonicalName in answers:
+ # Make sure it isn't a loop contained entirely within the
+ # results we have here.
+ if canonicalName in seenAliases:
+ return Failure(CanonicalNameLoop(record))
+
+ pertinentRecords = answers[canonicalName]
+ exchanges = []
+ else:
+ if cnamesLeft:
+ # Request more information from the server.
+ return self.getMX(canonicalName, cnamesLeft - 1)
+ else:
+ # Give up.
+ return Failure(CanonicalNameChainTooLong(record))
+
+ # If it's an MX, collect it.
+ if record.TYPE == dns.MX:
+ exchanges.append((record.preference, record))
+
+ if exchanges:
+ exchanges.sort()
+ for preference, record in exchanges:
+ host = str(record.name)
+ if host not in self.badMXs:
+ return record
+ t = self.clock.seconds() - self.badMXs[host]
+ if t >= 0:
+ del self.badMXs[host]
+ return record
+ return exchanges[0][1]
+ else:
+ # Treat no answers the same as an error - jump to the errback to
+ # try to look up an A record. This provides behavior described as
+ # a special case in RFC 974 in the section headed I{Interpreting
+ # the List of MX RRs}.
+ return Failure(error.DNSNameError(f"No MX records for {domain!r}"))
+
+ def _ebMX(self, failure, domain):
+ """
+ Attempt to use the name of the domain directly when mail exchange
+ lookup fails.
+
+ @type failure: L{Failure}
+ @param failure: The reason for the lookup failure.
+
+ @type domain: L{bytes}
+ @param domain: The domain name.
+
+ @rtype: L{Record_MX <twisted.names.dns.Record_MX>} or L{Failure}
+ @return: An MX record for the domain or a failure if the fallback to
+ domain option is not in effect and an error, other than not
+ finding an MX record, occurred during lookup.
+
+ @raise IOError: When no MX record could be found and the fallback to
+ domain option is not in effect.
+
+ @raise DNSLookupError: When no MX record could be found and the
+ fallback to domain option is in effect but no address for the
+ domain could be found.
+ """
+ from twisted.names import dns, error
+
+ if self.fallbackToDomain:
+ failure.trap(error.DNSNameError)
+ log.msg(
+ "MX lookup failed; attempting to use hostname ({}) directly".format(
+ domain
+ )
+ )
+
+ # Alright, I admit, this is a bit icky.
+ d = self.resolver.getHostByName(domain)
+
+ def cbResolved(addr):
+ return dns.Record_MX(name=addr)
+
+ def ebResolved(err):
+ err.trap(error.DNSNameError)
+ raise DNSLookupError()
+
+ d.addCallbacks(cbResolved, ebResolved)
+ return d
+ elif failure.check(error.DNSNameError):
+ raise OSError(f"No MX found for {domain!r}")
+ return failure
diff --git a/contrib/python/Twisted/py3/twisted/mail/scripts/__init__.py b/contrib/python/Twisted/py3/twisted/mail/scripts/__init__.py
new file mode 100644
index 0000000000..f653cc71ed
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/scripts/__init__.py
@@ -0,0 +1 @@
+"mail scripts"
diff --git a/contrib/python/Twisted/py3/twisted/mail/scripts/mailmail.py b/contrib/python/Twisted/py3/twisted/mail/scripts/mailmail.py
new file mode 100644
index 0000000000..cf1e58f5b6
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/scripts/mailmail.py
@@ -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)
diff --git a/contrib/python/Twisted/py3/twisted/mail/smtp.py b/contrib/python/Twisted/py3/twisted/mail/smtp.py
new file mode 100644
index 0000000000..55511647e6
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/smtp.py
@@ -0,0 +1,2270 @@
+# -*- test-case-name: twisted.mail.test.test_smtp -*-
+# Copyright (c) Twisted Matrix Laboratories.
+# See LICENSE for details.
+#
+# pylint: disable=I0011,C0103,C9302
+
+"""
+Simple Mail Transfer Protocol implementation.
+"""
+
+
+import base64
+import binascii
+import os
+import random
+import re
+import socket
+import time
+import warnings
+from email.utils import parseaddr
+from io import BytesIO
+from typing import Type
+
+from zope.interface import implementer
+
+from twisted import cred
+from twisted.copyright import longversion
+from twisted.internet import defer, error, protocol, reactor
+from twisted.internet._idna import _idnaText
+from twisted.internet.interfaces import ISSLTransport, ITLSTransport
+from twisted.mail._cred import (
+ CramMD5ClientAuthenticator,
+ LOGINAuthenticator,
+ LOGINCredentials as _lcredentials,
+)
+from twisted.mail._except import (
+ AddressError,
+ AUTHDeclinedError,
+ AuthenticationError,
+ AUTHRequiredError,
+ EHLORequiredError,
+ ESMTPClientError,
+ SMTPAddressError,
+ SMTPBadRcpt,
+ SMTPBadSender,
+ SMTPClientError,
+ SMTPConnectError,
+ SMTPDeliveryError,
+ SMTPError,
+ SMTPProtocolError,
+ SMTPServerError,
+ SMTPTimeoutError,
+ SMTPTLSError as TLSError,
+ TLSRequiredError,
+)
+from twisted.mail.interfaces import (
+ IClientAuthentication,
+ IMessageDelivery,
+ IMessageDeliveryFactory,
+ IMessageSMTP as IMessage,
+)
+from twisted.protocols import basic, policies
+from twisted.python import log, util
+from twisted.python.compat import iterbytes, nativeString, networkString
+from twisted.python.runtime import platform
+
+__all__ = [
+ "AUTHDeclinedError",
+ "AUTHRequiredError",
+ "AddressError",
+ "AuthenticationError",
+ "EHLORequiredError",
+ "ESMTPClientError",
+ "SMTPAddressError",
+ "SMTPBadRcpt",
+ "SMTPBadSender",
+ "SMTPClientError",
+ "SMTPConnectError",
+ "SMTPDeliveryError",
+ "SMTPError",
+ "SMTPServerError",
+ "SMTPTimeoutError",
+ "TLSError",
+ "TLSRequiredError",
+ "SMTPProtocolError",
+ "IClientAuthentication",
+ "IMessage",
+ "IMessageDelivery",
+ "IMessageDeliveryFactory",
+ "CramMD5ClientAuthenticator",
+ "LOGINAuthenticator",
+ "LOGINCredentials",
+ "PLAINAuthenticator",
+ "Address",
+ "User",
+ "sendmail",
+ "SenderMixin",
+ "ESMTP",
+ "ESMTPClient",
+ "ESMTPSender",
+ "ESMTPSenderFactory",
+ "SMTP",
+ "SMTPClient",
+ "SMTPFactory",
+ "SMTPSender",
+ "SMTPSenderFactory",
+ "idGenerator",
+ "messageid",
+ "quoteaddr",
+ "rfc822date",
+ "xtextStreamReader",
+ "xtextStreamWriter",
+ "xtext_codec",
+ "xtext_decode",
+ "xtext_encode",
+]
+
+
+# Cache the hostname (XXX Yes - this is broken)
+# Encode the DNS name into something we can send over the wire
+if platform.isMacOSX():
+ # On macOS, getfqdn() is ridiculously slow - use the
+ # probably-identical-but-sometimes-not gethostname() there.
+ DNSNAME = socket.gethostname().encode("ascii")
+else:
+ DNSNAME = socket.getfqdn().encode("ascii")
+
+# Used for fast success code lookup
+SUCCESS = dict.fromkeys(range(200, 300))
+
+
+def rfc822date(timeinfo=None, local=1):
+ """
+ Format an RFC-2822 compliant date string.
+
+ @param timeinfo: (optional) A sequence as returned by C{time.localtime()}
+ or C{time.gmtime()}. Default is now.
+ @param local: (optional) Indicates if the supplied time is local or
+ universal time, or if no time is given, whether now should be local or
+ universal time. Default is local, as suggested (SHOULD) by rfc-2822.
+
+ @returns: A L{bytes} representing the time and date in RFC-2822 format.
+ """
+ if not timeinfo:
+ if local:
+ timeinfo = time.localtime()
+ else:
+ timeinfo = time.gmtime()
+ if local:
+ if timeinfo[8]:
+ # DST
+ tz = -time.altzone
+ else:
+ tz = -time.timezone
+
+ (tzhr, tzmin) = divmod(abs(tz), 3600)
+ if tz:
+ tzhr *= int(abs(tz) // tz)
+ (tzmin, tzsec) = divmod(tzmin, 60)
+ else:
+ (tzhr, tzmin) = (0, 0)
+
+ return networkString(
+ "%s, %02d %s %04d %02d:%02d:%02d %+03d%02d"
+ % (
+ ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"][timeinfo[6]],
+ timeinfo[2],
+ [
+ "Jan",
+ "Feb",
+ "Mar",
+ "Apr",
+ "May",
+ "Jun",
+ "Jul",
+ "Aug",
+ "Sep",
+ "Oct",
+ "Nov",
+ "Dec",
+ ][timeinfo[1] - 1],
+ timeinfo[0],
+ timeinfo[3],
+ timeinfo[4],
+ timeinfo[5],
+ tzhr,
+ tzmin,
+ )
+ )
+
+
+def idGenerator():
+ i = 0
+ while True:
+ yield i
+ i += 1
+
+
+_gen = idGenerator()
+
+
+def messageid(uniq=None, N=lambda: next(_gen)):
+ """
+ Return a globally unique random string in RFC 2822 Message-ID format
+
+ <datetime.pid.random@host.dom.ain>
+
+ Optional uniq string will be added to strengthen uniqueness if given.
+ """
+ datetime = time.strftime("%Y%m%d%H%M%S", time.gmtime())
+ pid = os.getpid()
+ rand = random.randrange(2**31 - 1)
+ if uniq is None:
+ uniq = ""
+ else:
+ uniq = "." + uniq
+
+ return "<{}.{}.{}{}.{}@{}>".format(
+ datetime, pid, rand, uniq, N(), DNSNAME.decode()
+ ).encode()
+
+
+def quoteaddr(addr):
+ """
+ Turn an email address, possibly with realname part etc, into
+ a form suitable for and SMTP envelope.
+ """
+
+ if isinstance(addr, Address):
+ return b"<" + bytes(addr) + b">"
+
+ if isinstance(addr, bytes):
+ addr = addr.decode("ascii")
+
+ res = parseaddr(addr)
+
+ if res == (None, None):
+ # It didn't parse, use it as-is
+ return b"<" + bytes(addr) + b">"
+ else:
+ return b"<" + res[1].encode("ascii") + b">"
+
+
+COMMAND, DATA, AUTH = "COMMAND", "DATA", "AUTH"
+
+
+# Character classes for parsing addresses
+atom = rb"[-A-Za-z0-9!\#$%&'*+/=?^_`{|}~]"
+
+
+class Address:
+ """Parse and hold an RFC 2821 address.
+
+ Source routes are stipped and ignored, UUCP-style bang-paths
+ and %-style routing are not parsed.
+
+ @type domain: C{bytes}
+ @ivar domain: The domain within which this address resides.
+
+ @type local: C{bytes}
+ @ivar local: The local (\"user\") portion of this address.
+ """
+
+ tstring = re.compile(
+ rb"""( # A string of
+ (?:"[^"]*" # quoted string
+ |\\. # backslash-escaped characted
+ |"""
+ + atom
+ + rb""" # atom character
+ )+|.) # or any single character""",
+ re.X,
+ )
+ atomre = re.compile(atom) # match any one atom character
+
+ def __init__(self, addr, defaultDomain=None):
+ if isinstance(addr, User):
+ addr = addr.dest
+ if isinstance(addr, Address):
+ self.__dict__ = addr.__dict__.copy()
+ return
+ elif not isinstance(addr, bytes):
+ addr = str(addr).encode("ascii")
+
+ self.addrstr = addr
+
+ # Tokenize
+ atl = list(filter(None, self.tstring.split(addr)))
+ local = []
+ domain = []
+
+ while atl:
+ if atl[0] == b"<":
+ if atl[-1] != b">":
+ raise AddressError("Unbalanced <>")
+ atl = atl[1:-1]
+ elif atl[0] == b"@":
+ atl = atl[1:]
+ if not local:
+ # Source route
+ while atl and atl[0] != b":":
+ # remove it
+ atl = atl[1:]
+ if not atl:
+ raise AddressError("Malformed source route")
+ atl = atl[1:] # remove :
+ elif domain:
+ raise AddressError("Too many @")
+ else:
+ # Now in domain
+ domain = [b""]
+ elif len(atl[0]) == 1 and not self.atomre.match(atl[0]) and atl[0] != b".":
+ raise AddressError(f"Parse error at {atl[0]!r} of {(addr, atl)!r}")
+ else:
+ if not domain:
+ local.append(atl[0])
+ else:
+ domain.append(atl[0])
+ atl = atl[1:]
+
+ self.local = b"".join(local)
+ self.domain = b"".join(domain)
+ if self.local != b"" and self.domain == b"":
+ if defaultDomain is None:
+ defaultDomain = DNSNAME
+ self.domain = defaultDomain
+
+ dequotebs = re.compile(rb"\\(.)")
+
+ def dequote(self, addr):
+ """
+ Remove RFC-2821 quotes from address.
+ """
+ res = []
+
+ if not isinstance(addr, bytes):
+ addr = str(addr).encode("ascii")
+
+ atl = filter(None, self.tstring.split(addr))
+
+ for t in atl:
+ if t[0] == b'"' and t[-1] == b'"':
+ res.append(t[1:-1])
+ elif "\\" in t:
+ res.append(self.dequotebs.sub(rb"\1", t))
+ else:
+ res.append(t)
+
+ return b"".join(res)
+
+ def __str__(self) -> str:
+ return self.__bytes__().decode("ascii")
+
+ def __bytes__(self) -> bytes:
+ if self.local or self.domain:
+ return b"@".join((self.local, self.domain))
+ else:
+ return b""
+
+ def __repr__(self) -> str:
+ return "{}.{}({})".format(
+ self.__module__, self.__class__.__name__, repr(str(self))
+ )
+
+
+class User:
+ """
+ Hold information about and SMTP message recipient,
+ including information on where the message came from
+ """
+
+ def __init__(self, destination, helo, protocol, orig):
+ try:
+ host = protocol.host
+ except AttributeError:
+ host = None
+ self.dest = Address(destination, host)
+ self.helo = helo
+ self.protocol = protocol
+ if isinstance(orig, Address):
+ self.orig = orig
+ else:
+ self.orig = Address(orig, host)
+
+ def __getstate__(self):
+ """
+ Helper for pickle.
+
+ protocol isn't picklabe, but we want User to be, so skip it in
+ the pickle.
+ """
+ return {
+ "dest": self.dest,
+ "helo": self.helo,
+ "protocol": None,
+ "orig": self.orig,
+ }
+
+ def __str__(self) -> str:
+ return self.__bytes__().decode("ascii")
+
+ def __bytes__(self) -> bytes:
+ return bytes(self.dest)
+
+
+class SMTP(basic.LineOnlyReceiver, policies.TimeoutMixin):
+ """
+ SMTP server-side protocol.
+
+ @ivar host: The hostname of this mail server.
+ @type host: L{bytes}
+ """
+
+ timeout = 600
+ portal = None
+
+ # Control whether we log SMTP events
+ noisy = True
+
+ # A factory for IMessageDelivery objects. If an
+ # avatar implementing IMessageDeliveryFactory can
+ # be acquired from the portal, it will be used to
+ # create a new IMessageDelivery object for each
+ # message which is received.
+ deliveryFactory = None
+
+ # An IMessageDelivery object. A new instance is
+ # used for each message received if we can get an
+ # IMessageDeliveryFactory from the portal. Otherwise,
+ # a single instance is used throughout the lifetime
+ # of the connection.
+ delivery = None
+
+ # Cred cleanup function.
+ _onLogout = None
+
+ def __init__(self, delivery=None, deliveryFactory=None):
+ self.mode = COMMAND
+ self._from = None
+ self._helo = None
+ self._to = []
+ self.delivery = delivery
+ self.deliveryFactory = deliveryFactory
+ self.host = DNSNAME
+
+ @property
+ def host(self):
+ return self._host
+
+ @host.setter
+ def host(self, toSet):
+ if not isinstance(toSet, bytes):
+ toSet = str(toSet).encode("ascii")
+ self._host = toSet
+
+ def timeoutConnection(self):
+ msg = self.host + b" Timeout. Try talking faster next time!"
+ self.sendCode(421, msg)
+ self.transport.loseConnection()
+
+ def greeting(self):
+ return self.host + b" NO UCE NO UBE NO RELAY PROBES"
+
+ def connectionMade(self):
+ # Ensure user-code always gets something sane for _helo
+ peer = self.transport.getPeer()
+ try:
+ host = peer.host
+ except AttributeError: # not an IPv4Address
+ host = str(peer)
+ self._helo = (None, host)
+ self.sendCode(220, self.greeting())
+ self.setTimeout(self.timeout)
+
+ def sendCode(self, code, message=b""):
+ """
+ Send an SMTP code with a message.
+ """
+ lines = message.splitlines()
+ lastline = lines[-1:]
+ for line in lines[:-1]:
+ self.sendLine(networkString("%3.3d-" % (code,)) + line)
+ self.sendLine(
+ networkString("%3.3d " % (code,)) + (lastline and lastline[0] or b"")
+ )
+
+ def lineReceived(self, line):
+ self.resetTimeout()
+ return getattr(self, "state_" + self.mode)(line)
+
+ def state_COMMAND(self, line):
+ # Ignore leading and trailing whitespace, as well as an arbitrary
+ # amount of whitespace between the command and its argument, though
+ # it is not required by the protocol, for it is a nice thing to do.
+ line = line.strip()
+
+ parts = line.split(None, 1)
+ if parts:
+ method = self.lookupMethod(parts[0]) or self.do_UNKNOWN
+ if len(parts) == 2:
+ method(parts[1])
+ else:
+ method(b"")
+ else:
+ self.sendSyntaxError()
+
+ def sendSyntaxError(self):
+ self.sendCode(500, b"Error: bad syntax")
+
+ def lookupMethod(self, command):
+ """
+
+ @param command: The command to get from this class.
+ @type command: L{str}
+ @return: The function which executes this command.
+ """
+ if not isinstance(command, str):
+ command = nativeString(command)
+
+ return getattr(self, "do_" + command.upper(), None)
+
+ def lineLengthExceeded(self, line):
+ if self.mode is DATA:
+ for message in self.__messages:
+ message.connectionLost()
+ self.mode = COMMAND
+ del self.__messages
+ self.sendCode(500, b"Line too long")
+
+ def do_UNKNOWN(self, rest):
+ self.sendCode(500, b"Command not implemented")
+
+ def do_HELO(self, rest):
+ peer = self.transport.getPeer()
+ try:
+ host = peer.host
+ except AttributeError:
+ host = str(peer)
+
+ if not isinstance(host, bytes):
+ host = host.encode("idna")
+
+ self._helo = (rest, host)
+ self._from = None
+ self._to = []
+ self.sendCode(250, self.host + b" Hello " + host + b", nice to meet you")
+
+ def do_QUIT(self, rest):
+ self.sendCode(221, b"See you later")
+ self.transport.loseConnection()
+
+ # A string of quoted strings, backslash-escaped character or
+ # atom characters + '@.,:'
+ qstring = rb'("[^"]*"|\\.|' + atom + rb"|[@.,:])+"
+
+ mail_re = re.compile(
+ rb"""\s*FROM:\s*(?P<path><> # Empty <>
+ |<"""
+ + qstring
+ + rb"""> # <addr>
+ |"""
+ + qstring
+ + rb""" # addr
+ )\s*(\s(?P<opts>.*))? # Optional WS + ESMTP options
+ $""",
+ re.I | re.X,
+ )
+ rcpt_re = re.compile(
+ rb"\s*TO:\s*(?P<path><"
+ + qstring
+ + rb"""> # <addr>
+ |"""
+ + qstring
+ + rb""" # addr
+ )\s*(\s(?P<opts>.*))? # Optional WS + ESMTP options
+ $""",
+ re.I | re.X,
+ )
+
+ def do_MAIL(self, rest):
+ if self._from:
+ self.sendCode(503, b"Only one sender per message, please")
+ return
+ # Clear old recipient list
+ self._to = []
+ m = self.mail_re.match(rest)
+ if not m:
+ self.sendCode(501, b"Syntax error")
+ return
+
+ try:
+ addr = Address(m.group("path"), self.host)
+ except AddressError as e:
+ self.sendCode(553, networkString(str(e)))
+ return
+
+ validated = defer.maybeDeferred(self.validateFrom, self._helo, addr)
+ validated.addCallbacks(self._cbFromValidate, self._ebFromValidate)
+
+ def _cbFromValidate(self, fromEmail, code=250, msg=b"Sender address accepted"):
+ self._from = fromEmail
+ self.sendCode(code, msg)
+
+ def _ebFromValidate(self, failure):
+ if failure.check(SMTPBadSender):
+ self.sendCode(
+ failure.value.code,
+ (
+ b"Cannot receive from specified address "
+ + quoteaddr(failure.value.addr)
+ + b": "
+ + networkString(failure.value.resp)
+ ),
+ )
+ elif failure.check(SMTPServerError):
+ self.sendCode(failure.value.code, networkString(failure.value.resp))
+ else:
+ log.err(failure, "SMTP sender validation failure")
+ self.sendCode(451, b"Requested action aborted: local error in processing")
+
+ def do_RCPT(self, rest):
+ if not self._from:
+ self.sendCode(503, b"Must have sender before recipient")
+ return
+ m = self.rcpt_re.match(rest)
+ if not m:
+ self.sendCode(501, b"Syntax error")
+ return
+
+ try:
+ user = User(m.group("path"), self._helo, self, self._from)
+ except AddressError as e:
+ self.sendCode(553, networkString(str(e)))
+ return
+
+ d = defer.maybeDeferred(self.validateTo, user)
+ d.addCallbacks(self._cbToValidate, self._ebToValidate, callbackArgs=(user,))
+
+ def _cbToValidate(self, to, user=None, code=250, msg=b"Recipient address accepted"):
+ if user is None:
+ user = to
+ self._to.append((user, to))
+ self.sendCode(code, msg)
+
+ def _ebToValidate(self, failure):
+ if failure.check(SMTPBadRcpt, SMTPServerError):
+ self.sendCode(failure.value.code, networkString(failure.value.resp))
+ else:
+ log.err(failure)
+ self.sendCode(451, b"Requested action aborted: local error in processing")
+
+ def _disconnect(self, msgs):
+ for msg in msgs:
+ try:
+ msg.connectionLost()
+ except BaseException:
+ log.msg("msg raised exception from connectionLost")
+ log.err()
+
+ def do_DATA(self, rest):
+ if self._from is None or (not self._to):
+ self.sendCode(503, b"Must have valid receiver and originator")
+ return
+ self.mode = DATA
+ helo, origin = self._helo, self._from
+ recipients = self._to
+
+ self._from = None
+ self._to = []
+ self.datafailed = None
+
+ msgs = []
+ for user, msgFunc in recipients:
+ try:
+ msg = msgFunc()
+ rcvdhdr = self.receivedHeader(helo, origin, [user])
+ if rcvdhdr:
+ msg.lineReceived(rcvdhdr)
+ msgs.append(msg)
+ except SMTPServerError as e:
+ self.sendCode(e.code, e.resp)
+ self.mode = COMMAND
+ self._disconnect(msgs)
+ return
+ except BaseException:
+ log.err()
+ self.sendCode(550, b"Internal server error")
+ self.mode = COMMAND
+ self._disconnect(msgs)
+ return
+ self.__messages = msgs
+
+ self.__inheader = self.__inbody = 0
+ self.sendCode(354, b"Continue")
+
+ if self.noisy:
+ fmt = "Receiving message for delivery: from=%s to=%s"
+ log.msg(fmt % (origin, [str(u) for (u, f) in recipients]))
+
+ def connectionLost(self, reason):
+ # self.sendCode(421, 'Dropping connection.') # This does nothing...
+ # Ideally, if we (rather than the other side) lose the connection,
+ # we should be able to tell the other side that we are going away.
+ # RFC-2821 requires that we try.
+ if self.mode is DATA:
+ try:
+ for message in self.__messages:
+ try:
+ message.connectionLost()
+ except BaseException:
+ log.err()
+ del self.__messages
+ except AttributeError:
+ pass
+ if self._onLogout:
+ self._onLogout()
+ self._onLogout = None
+ self.setTimeout(None)
+
+ def do_RSET(self, rest):
+ self._from = None
+ self._to = []
+ self.sendCode(250, b"I remember nothing.")
+
+ def dataLineReceived(self, line):
+ if line[:1] == b".":
+ if line == b".":
+ self.mode = COMMAND
+ if self.datafailed:
+ self.sendCode(self.datafailed.code, self.datafailed.resp)
+ return
+ if not self.__messages:
+ self._messageHandled("thrown away")
+ return
+ defer.DeferredList(
+ [m.eomReceived() for m in self.__messages], consumeErrors=True
+ ).addCallback(self._messageHandled)
+ del self.__messages
+ return
+ line = line[1:]
+
+ if self.datafailed:
+ return
+
+ try:
+ # Add a blank line between the generated Received:-header
+ # and the message body if the message comes in without any
+ # headers
+ if not self.__inheader and not self.__inbody:
+ if b":" in line:
+ self.__inheader = 1
+ elif line:
+ for message in self.__messages:
+ message.lineReceived(b"")
+ self.__inbody = 1
+
+ if not line:
+ self.__inbody = 1
+
+ for message in self.__messages:
+ message.lineReceived(line)
+ except SMTPServerError as e:
+ self.datafailed = e
+ for message in self.__messages:
+ message.connectionLost()
+
+ state_DATA = dataLineReceived
+
+ def _messageHandled(self, resultList):
+ failures = 0
+ for success, result in resultList:
+ if not success:
+ failures += 1
+ log.err(result)
+ if failures:
+ msg = "Could not send e-mail"
+ resultLen = len(resultList)
+ if resultLen > 1:
+ msg += f" ({failures} failures out of {resultLen} recipients)"
+ self.sendCode(550, networkString(msg))
+ else:
+ self.sendCode(250, b"Delivery in progress")
+
+ def _cbAnonymousAuthentication(self, result):
+ """
+ Save the state resulting from a successful anonymous cred login.
+ """
+ (iface, avatar, logout) = result
+ if issubclass(iface, IMessageDeliveryFactory):
+ self.deliveryFactory = avatar
+ self.delivery = None
+ elif issubclass(iface, IMessageDelivery):
+ self.deliveryFactory = None
+ self.delivery = avatar
+ else:
+ raise RuntimeError(f"{iface.__name__} is not a supported interface")
+ self._onLogout = logout
+ self.challenger = None
+
+ # overridable methods:
+ def validateFrom(self, helo, origin):
+ """
+ Validate the address from which the message originates.
+
+ @type helo: C{(bytes, bytes)}
+ @param helo: The argument to the HELO command and the client's IP
+ address.
+
+ @type origin: C{Address}
+ @param origin: The address the message is from
+
+ @rtype: C{Deferred} or C{Address}
+ @return: C{origin} or a C{Deferred} whose callback will be
+ passed C{origin}.
+
+ @raise SMTPBadSender: Raised of messages from this address are
+ not to be accepted.
+ """
+ if self.deliveryFactory is not None:
+ self.delivery = self.deliveryFactory.getMessageDelivery()
+
+ if self.delivery is not None:
+ return defer.maybeDeferred(self.delivery.validateFrom, helo, origin)
+
+ # No login has been performed, no default delivery object has been
+ # provided: try to perform an anonymous login and then invoke this
+ # method again.
+ if self.portal:
+ result = self.portal.login(
+ cred.credentials.Anonymous(),
+ None,
+ IMessageDeliveryFactory,
+ IMessageDelivery,
+ )
+
+ def ebAuthentication(err):
+ """
+ Translate cred exceptions into SMTP exceptions so that the
+ protocol code which invokes C{validateFrom} can properly report
+ the failure.
+ """
+ if err.check(cred.error.UnauthorizedLogin):
+ exc = SMTPBadSender(origin)
+ elif err.check(cred.error.UnhandledCredentials):
+ exc = SMTPBadSender(
+ origin, resp="Unauthenticated senders not allowed"
+ )
+ else:
+ return err
+ return defer.fail(exc)
+
+ result.addCallbacks(self._cbAnonymousAuthentication, ebAuthentication)
+
+ def continueValidation(ignored):
+ """
+ Re-attempt from address validation.
+ """
+ return self.validateFrom(helo, origin)
+
+ result.addCallback(continueValidation)
+ return result
+
+ raise SMTPBadSender(origin)
+
+ def validateTo(self, user):
+ """
+ Validate the address for which the message is destined.
+
+ @type user: L{User}
+ @param user: The address to validate.
+
+ @rtype: no-argument callable
+ @return: A C{Deferred} which becomes, or a callable which
+ takes no arguments and returns an object implementing C{IMessage}.
+ This will be called and the returned object used to deliver the
+ message when it arrives.
+
+ @raise SMTPBadRcpt: Raised if messages to the address are
+ not to be accepted.
+ """
+ if self.delivery is not None:
+ return self.delivery.validateTo(user)
+ raise SMTPBadRcpt(user)
+
+ def receivedHeader(self, helo, origin, recipients):
+ if self.delivery is not None:
+ return self.delivery.receivedHeader(helo, origin, recipients)
+
+ heloStr = b""
+ if helo[0]:
+ heloStr = b" helo=" + helo[0]
+ domain = networkString(self.transport.getHost().host)
+
+ from_ = b"from " + helo[0] + b" ([" + helo[1] + b"]" + heloStr + b")"
+ by = b"by %s with %s (%s)" % (domain, self.__class__.__name__, longversion)
+ for_ = b"for %s; %s" % (" ".join(map(str, recipients)), rfc822date())
+ return b"Received: " + from_ + b"\n\t" + by + b"\n\t" + for_
+
+
+class SMTPFactory(protocol.ServerFactory):
+ """
+ Factory for SMTP.
+ """
+
+ # override in instances or subclasses
+ domain = DNSNAME
+ timeout = 600
+ protocol = SMTP
+
+ portal = None
+
+ def __init__(self, portal=None):
+ self.portal = portal
+
+ def buildProtocol(self, addr):
+ p = protocol.ServerFactory.buildProtocol(self, addr)
+ p.portal = self.portal
+ p.host = self.domain
+ return p
+
+
+class SMTPClient(basic.LineReceiver, policies.TimeoutMixin):
+ """
+ SMTP client for sending emails.
+
+ After the client has connected to the SMTP server, it repeatedly calls
+ L{SMTPClient.getMailFrom}, L{SMTPClient.getMailTo} and
+ L{SMTPClient.getMailData} and uses this information to send an email.
+ It then calls L{SMTPClient.getMailFrom} again; if it returns L{None}, the
+ client will disconnect, otherwise it will continue as normal i.e. call
+ L{SMTPClient.getMailTo} and L{SMTPClient.getMailData} and send a new email.
+ """
+
+ # If enabled then log SMTP client server communication
+ debug = True
+
+ # Number of seconds to wait before timing out a connection. If
+ # None, perform no timeout checking.
+ timeout = None
+
+ def __init__(self, identity, logsize=10):
+ if isinstance(identity, str):
+ identity = identity.encode("ascii")
+
+ self.identity = identity or b""
+ self.toAddressesResult = []
+ self.successAddresses = []
+ self._from = None
+ self.resp = []
+ self.code = -1
+ self.log = util.LineLog(logsize)
+
+ def sendLine(self, line):
+ # Log sendLine only if you are in debug mode for performance
+ if self.debug:
+ self.log.append(b">>> " + line)
+
+ basic.LineReceiver.sendLine(self, line)
+
+ def connectionMade(self):
+ self.setTimeout(self.timeout)
+
+ self._expected = [220]
+ self._okresponse = self.smtpState_helo
+ self._failresponse = self.smtpConnectionFailed
+
+ def connectionLost(self, reason=protocol.connectionDone):
+ """
+ We are no longer connected
+ """
+ self.setTimeout(None)
+ self.mailFile = None
+
+ def timeoutConnection(self):
+ self.sendError(
+ SMTPTimeoutError(
+ -1, b"Timeout waiting for SMTP server response", self.log.str()
+ )
+ )
+
+ def lineReceived(self, line):
+ self.resetTimeout()
+
+ # Log lineReceived only if you are in debug mode for performance
+ if self.debug:
+ self.log.append(b"<<< " + line)
+
+ why = None
+
+ try:
+ self.code = int(line[:3])
+ except ValueError:
+ # This is a fatal error and will disconnect the transport
+ # lineReceived will not be called again.
+ self.sendError(
+ SMTPProtocolError(
+ -1,
+ f"Invalid response from SMTP server: {line}",
+ self.log.str(),
+ )
+ )
+ return
+
+ if line[0:1] == b"0":
+ # Verbose informational message, ignore it
+ return
+
+ self.resp.append(line[4:])
+
+ if line[3:4] == b"-":
+ # Continuation
+ return
+
+ if self.code in self._expected:
+ why = self._okresponse(self.code, b"\n".join(self.resp))
+ else:
+ why = self._failresponse(self.code, b"\n".join(self.resp))
+
+ self.code = -1
+ self.resp = []
+ return why
+
+ def smtpConnectionFailed(self, code, resp):
+ self.sendError(SMTPConnectError(code, resp, self.log.str()))
+
+ def smtpTransferFailed(self, code, resp):
+ if code < 0:
+ self.sendError(SMTPProtocolError(code, resp, self.log.str()))
+ else:
+ self.smtpState_msgSent(code, resp)
+
+ def smtpState_helo(self, code, resp):
+ self.sendLine(b"HELO " + self.identity)
+ self._expected = SUCCESS
+ self._okresponse = self.smtpState_from
+
+ def smtpState_from(self, code, resp):
+ self._from = self.getMailFrom()
+ self._failresponse = self.smtpTransferFailed
+ if self._from is not None:
+ self.sendLine(b"MAIL FROM:" + quoteaddr(self._from))
+ self._expected = [250]
+ self._okresponse = self.smtpState_to
+ else:
+ # All messages have been sent, disconnect
+ self._disconnectFromServer()
+
+ def smtpState_disconnect(self, code, resp):
+ self.transport.loseConnection()
+
+ def smtpState_to(self, code, resp):
+ self.toAddresses = iter(self.getMailTo())
+ self.toAddressesResult = []
+ self.successAddresses = []
+ self._okresponse = self.smtpState_toOrData
+ self._expected = range(0, 1000)
+ self.lastAddress = None
+ return self.smtpState_toOrData(0, b"")
+
+ def smtpState_toOrData(self, code, resp):
+ if self.lastAddress is not None:
+ self.toAddressesResult.append((self.lastAddress, code, resp))
+ if code in SUCCESS:
+ self.successAddresses.append(self.lastAddress)
+ try:
+ self.lastAddress = next(self.toAddresses)
+ except StopIteration:
+ if self.successAddresses:
+ self.sendLine(b"DATA")
+ self._expected = [354]
+ self._okresponse = self.smtpState_data
+ else:
+ return self.smtpState_msgSent(code, "No recipients accepted")
+ else:
+ self.sendLine(b"RCPT TO:" + quoteaddr(self.lastAddress))
+
+ def smtpState_data(self, code, resp):
+ s = basic.FileSender()
+ d = s.beginFileTransfer(self.getMailData(), self.transport, self.transformChunk)
+
+ def ebTransfer(err):
+ self.sendError(err.value)
+
+ d.addCallbacks(self.finishedFileTransfer, ebTransfer)
+ self._expected = SUCCESS
+ self._okresponse = self.smtpState_msgSent
+
+ def smtpState_msgSent(self, code, resp):
+ if self._from is not None:
+ self.sentMail(
+ code, resp, len(self.successAddresses), self.toAddressesResult, self.log
+ )
+
+ self.toAddressesResult = []
+ self._from = None
+ self.sendLine(b"RSET")
+ self._expected = SUCCESS
+ self._okresponse = self.smtpState_from
+
+ ##
+ ## Helpers for FileSender
+ ##
+ def transformChunk(self, chunk):
+ """
+ Perform the necessary local to network newline conversion and escape
+ leading periods.
+
+ This method also resets the idle timeout so that as long as process is
+ being made sending the message body, the client will not time out.
+ """
+ self.resetTimeout()
+ return chunk.replace(b"\n", b"\r\n").replace(b"\r\n.", b"\r\n..")
+
+ def finishedFileTransfer(self, lastsent):
+ if lastsent != b"\n":
+ line = b"\r\n."
+ else:
+ line = b"."
+ self.sendLine(line)
+
+ ##
+ # these methods should be overridden in subclasses
+ def getMailFrom(self):
+ """
+ Return the email address the mail is from.
+ """
+ raise NotImplementedError
+
+ def getMailTo(self):
+ """
+ Return a list of emails to send to.
+ """
+ raise NotImplementedError
+
+ def getMailData(self):
+ """
+ Return file-like object containing data of message to be sent.
+
+ Lines in the file should be delimited by '\\n'.
+ """
+ raise NotImplementedError
+
+ def sendError(self, exc):
+ """
+ If an error occurs before a mail message is sent sendError will be
+ called. This base class method sends a QUIT if the error is
+ non-fatal and disconnects the connection.
+
+ @param exc: The SMTPClientError (or child class) raised
+ @type exc: C{SMTPClientError}
+ """
+ if isinstance(exc, SMTPClientError) and not exc.isFatal:
+ self._disconnectFromServer()
+ else:
+ # If the error was fatal then the communication channel with the
+ # SMTP Server is broken so just close the transport connection
+ self.smtpState_disconnect(-1, None)
+
+ def sentMail(self, code, resp, numOk, addresses, log):
+ """
+ Called when an attempt to send an email is completed.
+
+ If some addresses were accepted, code and resp are the response
+ to the DATA command. If no addresses were accepted, code is -1
+ and resp is an informative message.
+
+ @param code: the code returned by the SMTP Server
+ @param resp: The string response returned from the SMTP Server
+ @param numOk: the number of addresses accepted by the remote host.
+ @param addresses: is a list of tuples (address, code, resp) listing
+ the response to each RCPT command.
+ @param log: is the SMTP session log
+ """
+ raise NotImplementedError
+
+ def _disconnectFromServer(self):
+ self._expected = range(0, 1000)
+ self._okresponse = self.smtpState_disconnect
+ self.sendLine(b"QUIT")
+
+
+class ESMTPClient(SMTPClient):
+ """
+ A client for sending emails over ESMTP.
+
+ @ivar heloFallback: Whether or not to fall back to plain SMTP if the C{EHLO}
+ command is not recognised by the server. If L{requireAuthentication} is
+ C{True}, or L{requireTransportSecurity} is C{True} and the connection is
+ not over TLS, this fallback flag will not be honored.
+ @type heloFallback: L{bool}
+
+ @ivar requireAuthentication: If C{True}, refuse to proceed if authentication
+ cannot be performed. Overrides L{heloFallback}.
+ @type requireAuthentication: L{bool}
+
+ @ivar requireTransportSecurity: If C{True}, refuse to proceed if the
+ transport cannot be secured. If the transport layer is not already
+ secured via TLS, this will override L{heloFallback}.
+ @type requireAuthentication: L{bool}
+
+ @ivar context: The context factory to use for STARTTLS, if desired.
+ @type context: L{IOpenSSLClientConnectionCreator}
+
+ @ivar _tlsMode: Whether or not the connection is over TLS.
+ @type _tlsMode: L{bool}
+ """
+
+ heloFallback = True
+ requireAuthentication = False
+ requireTransportSecurity = False
+ context = None
+ _tlsMode = False
+
+ def __init__(self, secret, contextFactory=None, *args, **kw):
+ SMTPClient.__init__(self, *args, **kw)
+ self.authenticators = []
+ self.secret = secret
+ self.context = contextFactory
+
+ def __getattr__(self, name):
+ if name == "tlsMode":
+ warnings.warn(
+ "tlsMode attribute of twisted.mail.smtp.ESMTPClient "
+ "is deprecated since Twisted 13.0",
+ category=DeprecationWarning,
+ stacklevel=2,
+ )
+ return self._tlsMode
+ else:
+ raise AttributeError(
+ "%s instance has no attribute %r"
+ % (
+ self.__class__.__name__,
+ name,
+ )
+ )
+
+ def __setattr__(self, name, value):
+ if name == "tlsMode":
+ warnings.warn(
+ "tlsMode attribute of twisted.mail.smtp.ESMTPClient "
+ "is deprecated since Twisted 13.0",
+ category=DeprecationWarning,
+ stacklevel=2,
+ )
+ self._tlsMode = value
+ else:
+ self.__dict__[name] = value
+
+ def esmtpEHLORequired(self, code=-1, resp=None):
+ """
+ Fail because authentication is required, but the server does not support
+ ESMTP, which is required for authentication.
+
+ @param code: The server status code from the most recently received
+ server message.
+ @type code: L{int}
+
+ @param resp: The server status response from the most recently received
+ server message.
+ @type resp: L{bytes}
+ """
+ self.sendError(
+ EHLORequiredError(
+ 502, b"Server does not support ESMTP " b"Authentication", self.log.str()
+ )
+ )
+
+ def esmtpAUTHRequired(self, code=-1, resp=None):
+ """
+ Fail because authentication is required, but the server does not support
+ any schemes we support.
+
+ @param code: The server status code from the most recently received
+ server message.
+ @type code: L{int}
+
+ @param resp: The server status response from the most recently received
+ server message.
+ @type resp: L{bytes}
+ """
+ tmp = []
+
+ for a in self.authenticators:
+ tmp.append(a.getName().upper())
+
+ auth = b"[%s]" % b", ".join(tmp)
+
+ self.sendError(
+ AUTHRequiredError(
+ 502,
+ b"Server does not support Client " b"Authentication schemes %s" % auth,
+ self.log.str(),
+ )
+ )
+
+ def esmtpTLSRequired(self, code=-1, resp=None):
+ """
+ Fail because TLS is required and the server does not support it.
+
+ @param code: The server status code from the most recently received
+ server message.
+ @type code: L{int}
+
+ @param resp: The server status response from the most recently received
+ server message.
+ @type resp: L{bytes}
+ """
+ self.sendError(
+ TLSRequiredError(
+ 502,
+ b"Server does not support secure " b"communication via TLS / SSL",
+ self.log.str(),
+ )
+ )
+
+ def esmtpTLSFailed(self, code=-1, resp=None):
+ """
+ Fail because the TLS handshake wasn't able to be completed.
+
+ @param code: The server status code from the most recently received
+ server message.
+ @type code: L{int}
+
+ @param resp: The server status response from the most recently received
+ server message.
+ @type resp: L{bytes}
+ """
+ self.sendError(
+ TLSError(
+ code, b"Could not complete the SSL/TLS " b"handshake", self.log.str()
+ )
+ )
+
+ def esmtpAUTHDeclined(self, code=-1, resp=None):
+ """
+ Fail because the authentication was rejected.
+
+ @param code: The server status code from the most recently received
+ server message.
+ @type code: L{int}
+
+ @param resp: The server status response from the most recently received
+ server message.
+ @type resp: L{bytes}
+ """
+ self.sendError(AUTHDeclinedError(code, resp, self.log.str()))
+
+ def esmtpAUTHMalformedChallenge(self, code=-1, resp=None):
+ """
+ Fail because the server sent a malformed authentication challenge.
+
+ @param code: The server status code from the most recently received
+ server message.
+ @type code: L{int}
+
+ @param resp: The server status response from the most recently received
+ server message.
+ @type resp: L{bytes}
+ """
+ self.sendError(
+ AuthenticationError(
+ 501,
+ b"Login failed because the "
+ b"SMTP Server returned a malformed Authentication Challenge",
+ self.log.str(),
+ )
+ )
+
+ def esmtpAUTHServerError(self, code=-1, resp=None):
+ """
+ Fail because of some other authentication error.
+
+ @param code: The server status code from the most recently received
+ server message.
+ @type code: L{int}
+
+ @param resp: The server status response from the most recently received
+ server message.
+ @type resp: L{bytes}
+ """
+ self.sendError(AuthenticationError(code, resp, self.log.str()))
+
+ def registerAuthenticator(self, auth):
+ """
+ Registers an Authenticator with the ESMTPClient. The ESMTPClient will
+ attempt to login to the SMTP Server in the order the Authenticators are
+ registered. The most secure Authentication mechanism should be
+ registered first.
+
+ @param auth: The Authentication mechanism to register
+ @type auth: L{IClientAuthentication} implementor
+
+ @return: L{None}
+ """
+ self.authenticators.append(auth)
+
+ def connectionMade(self):
+ """
+ Called when a connection has been made, and triggers sending an C{EHLO}
+ to the server.
+ """
+ self._tlsMode = ISSLTransport.providedBy(self.transport)
+ SMTPClient.connectionMade(self)
+ self._okresponse = self.esmtpState_ehlo
+
+ def esmtpState_ehlo(self, code, resp):
+ """
+ Send an C{EHLO} to the server.
+
+ If L{heloFallback} is C{True}, and there is no requirement for TLS or
+ authentication, the client will fall back to basic SMTP.
+
+ @param code: The server status code from the most recently received
+ server message.
+ @type code: L{int}
+
+ @param resp: The server status response from the most recently received
+ server message.
+ @type resp: L{bytes}
+
+ @return: L{None}
+ """
+ self._expected = SUCCESS
+
+ self._okresponse = self.esmtpState_serverConfig
+ self._failresponse = self.esmtpEHLORequired
+
+ if self._tlsMode:
+ needTLS = False
+ else:
+ needTLS = self.requireTransportSecurity
+
+ if self.heloFallback and not self.requireAuthentication and not needTLS:
+ self._failresponse = self.smtpState_helo
+
+ self.sendLine(b"EHLO " + self.identity)
+
+ def esmtpState_serverConfig(self, code, resp):
+ """
+ Handle a positive response to the I{EHLO} command by parsing the
+ capabilities in the server's response and then taking the most
+ appropriate next step towards entering a mail transaction.
+ """
+ items = {}
+ for line in resp.splitlines():
+ e = line.split(None, 1)
+ if len(e) > 1:
+ items[e[0]] = e[1]
+ else:
+ items[e[0]] = None
+
+ self.tryTLS(code, resp, items)
+
+ def tryTLS(self, code, resp, items):
+ """
+ Take a necessary step towards being able to begin a mail transaction.
+
+ The step may be to ask the server to being a TLS session. If TLS is
+ already in use or not necessary and not available then the step may be
+ to authenticate with the server. If TLS is necessary and not available,
+ fail the mail transmission attempt.
+
+ This is an internal helper method.
+
+ @param code: The server status code from the most recently received
+ server message.
+ @type code: L{int}
+
+ @param resp: The server status response from the most recently received
+ server message.
+ @type resp: L{bytes}
+
+ @param items: A mapping of ESMTP extensions offered by the server. Keys
+ are extension identifiers and values are the associated values.
+ @type items: L{dict} mapping L{bytes} to L{bytes}
+
+ @return: L{None}
+ """
+
+ # has tls can tls must tls result
+ # t t t authenticate
+ # t t f authenticate
+ # t f t authenticate
+ # t f f authenticate
+
+ # f t t STARTTLS
+ # f t f STARTTLS
+ # f f t esmtpTLSRequired
+ # f f f authenticate
+
+ hasTLS = self._tlsMode
+ canTLS = self.context and b"STARTTLS" in items
+ mustTLS = self.requireTransportSecurity
+
+ if hasTLS or not (canTLS or mustTLS):
+ self.authenticate(code, resp, items)
+ elif canTLS:
+ self._expected = [220]
+ self._okresponse = self.esmtpState_starttls
+ self._failresponse = self.esmtpTLSFailed
+ self.sendLine(b"STARTTLS")
+ else:
+ self.esmtpTLSRequired()
+
+ def esmtpState_starttls(self, code, resp):
+ """
+ Handle a positive response to the I{STARTTLS} command by starting a new
+ TLS session on C{self.transport}.
+
+ Upon success, re-handshake with the server to discover what capabilities
+ it has when TLS is in use.
+ """
+ try:
+ self.transport.startTLS(self.context)
+ self._tlsMode = True
+ except BaseException:
+ log.err()
+ self.esmtpTLSFailed(451)
+
+ # Send another EHLO once TLS has been started to
+ # get the TLS / AUTH schemes. Some servers only allow AUTH in TLS mode.
+ self.esmtpState_ehlo(code, resp)
+
+ def authenticate(self, code, resp, items):
+ if self.secret and items.get(b"AUTH"):
+ schemes = items[b"AUTH"].split()
+ tmpSchemes = {}
+
+ # XXX: May want to come up with a more efficient way to do this
+ for s in schemes:
+ tmpSchemes[s.upper()] = 1
+
+ for a in self.authenticators:
+ auth = a.getName().upper()
+
+ if auth in tmpSchemes:
+ self._authinfo = a
+
+ # Special condition handled
+ if auth == b"PLAIN":
+ self._okresponse = self.smtpState_from
+ self._failresponse = self._esmtpState_plainAuth
+ self._expected = [235]
+ challenge = base64.b64encode(
+ self._authinfo.challengeResponse(self.secret, 1)
+ )
+ self.sendLine(b"AUTH %s %s" % (auth, challenge))
+ else:
+ self._expected = [334]
+ self._okresponse = self.esmtpState_challenge
+ # If some error occurs here, the server declined the
+ # AUTH before the user / password phase. This would be
+ # a very rare case
+ self._failresponse = self.esmtpAUTHServerError
+ self.sendLine(b"AUTH " + auth)
+ return
+
+ if self.requireAuthentication:
+ self.esmtpAUTHRequired()
+ else:
+ self.smtpState_from(code, resp)
+
+ def _esmtpState_plainAuth(self, code, resp):
+ self._okresponse = self.smtpState_from
+ self._failresponse = self.esmtpAUTHDeclined
+ self._expected = [235]
+ challenge = base64.b64encode(self._authinfo.challengeResponse(self.secret, 2))
+ self.sendLine(b"AUTH PLAIN " + challenge)
+
+ def esmtpState_challenge(self, code, resp):
+ self._authResponse(self._authinfo, resp)
+
+ def _authResponse(self, auth, challenge):
+ self._failresponse = self.esmtpAUTHDeclined
+ try:
+ challenge = base64.b64decode(challenge)
+ except binascii.Error:
+ # Illegal challenge, give up, then quit
+ self.sendLine(b"*")
+ self._okresponse = self.esmtpAUTHMalformedChallenge
+ self._failresponse = self.esmtpAUTHMalformedChallenge
+ else:
+ resp = auth.challengeResponse(self.secret, challenge)
+ self._expected = [235, 334]
+ self._okresponse = self.smtpState_maybeAuthenticated
+ self.sendLine(base64.b64encode(resp))
+
+ def smtpState_maybeAuthenticated(self, code, resp):
+ """
+ Called to handle the next message from the server after sending a
+ response to a SASL challenge. The server response might be another
+ challenge or it might indicate authentication has succeeded.
+ """
+ if code == 235:
+ # Yes, authenticated!
+ del self._authinfo
+ self.smtpState_from(code, resp)
+ else:
+ # No, not authenticated yet. Keep trying.
+ self._authResponse(self._authinfo, resp)
+
+
+class ESMTP(SMTP):
+ ctx = None
+ canStartTLS = False
+ startedTLS = False
+
+ authenticated = False
+
+ def __init__(self, chal=None, contextFactory=None):
+ SMTP.__init__(self)
+ if chal is None:
+ chal = {}
+ self.challengers = chal
+ self.authenticated = False
+ self.ctx = contextFactory
+
+ def connectionMade(self):
+ SMTP.connectionMade(self)
+ self.canStartTLS = ITLSTransport.providedBy(self.transport)
+ self.canStartTLS = self.canStartTLS and (self.ctx is not None)
+
+ def greeting(self):
+ return SMTP.greeting(self) + b" ESMTP"
+
+ def extensions(self):
+ """
+ SMTP service extensions
+
+ @return: the SMTP service extensions that are supported.
+ @rtype: L{dict} with L{bytes} keys and a value of either L{None} or a
+ L{list} of L{bytes}.
+ """
+ ext = {b"AUTH": list(self.challengers.keys())}
+ if self.canStartTLS and not self.startedTLS:
+ ext[b"STARTTLS"] = None
+ return ext
+
+ def lookupMethod(self, command):
+ command = nativeString(command)
+
+ m = SMTP.lookupMethod(self, command)
+ if m is None:
+ m = getattr(self, "ext_" + command.upper(), None)
+ return m
+
+ def listExtensions(self):
+ r = []
+ for c, v in self.extensions().items():
+ if v is not None:
+ if v:
+ # Intentionally omit extensions with empty argument lists
+ r.append(c + b" " + b" ".join(v))
+ else:
+ r.append(c)
+
+ return b"\n".join(r)
+
+ def do_EHLO(self, rest):
+ peer = self.transport.getPeer().host
+
+ if not isinstance(peer, bytes):
+ peer = peer.encode("idna")
+
+ self._helo = (rest, peer)
+ self._from = None
+ self._to = []
+ self.sendCode(
+ 250,
+ (
+ self.host
+ + b" Hello "
+ + peer
+ + b", nice to meet you\n"
+ + self.listExtensions()
+ ),
+ )
+
+ def ext_STARTTLS(self, rest):
+ if self.startedTLS:
+ self.sendCode(503, b"TLS already negotiated")
+ elif self.ctx and self.canStartTLS:
+ self.sendCode(220, b"Begin TLS negotiation now")
+ self.transport.startTLS(self.ctx)
+ self.startedTLS = True
+ else:
+ self.sendCode(454, b"TLS not available")
+
+ def ext_AUTH(self, rest):
+ if self.authenticated:
+ self.sendCode(503, b"Already authenticated")
+ return
+ parts = rest.split(None, 1)
+ chal = self.challengers.get(parts[0].upper(), lambda: None)()
+ if not chal:
+ self.sendCode(504, b"Unrecognized authentication type")
+ return
+
+ self.mode = AUTH
+ self.challenger = chal
+
+ if len(parts) > 1:
+ chal.getChallenge() # Discard it, apparently the client does not
+ # care about it.
+ rest = parts[1]
+ else:
+ rest = None
+ self.state_AUTH(rest)
+
+ def _cbAuthenticated(self, loginInfo):
+ """
+ Save the state resulting from a successful cred login and mark this
+ connection as authenticated.
+ """
+ result = SMTP._cbAnonymousAuthentication(self, loginInfo)
+ self.authenticated = True
+ return result
+
+ def _ebAuthenticated(self, reason):
+ """
+ Handle cred login errors by translating them to the SMTP authenticate
+ failed. Translate all other errors into a generic SMTP error code and
+ log the failure for inspection. Stop all errors from propagating.
+
+ @param reason: Reason for failure.
+ """
+ self.challenge = None
+ if reason.check(cred.error.UnauthorizedLogin):
+ self.sendCode(535, b"Authentication failed")
+ else:
+ log.err(reason, "SMTP authentication failure")
+ self.sendCode(451, b"Requested action aborted: local error in processing")
+
+ def state_AUTH(self, response):
+ """
+ Handle one step of challenge/response authentication.
+
+ @param response: The text of a response. If None, this
+ function has been called as a result of an AUTH command with
+ no initial response. A response of '*' aborts authentication,
+ as per RFC 2554.
+ """
+ if self.portal is None:
+ self.sendCode(454, b"Temporary authentication failure")
+ self.mode = COMMAND
+ return
+
+ if response is None:
+ challenge = self.challenger.getChallenge()
+ encoded = base64.b64encode(challenge)
+ self.sendCode(334, encoded)
+ return
+
+ if response == b"*":
+ self.sendCode(501, b"Authentication aborted")
+ self.challenger = None
+ self.mode = COMMAND
+ return
+
+ try:
+ uncoded = base64.b64decode(response)
+ except (TypeError, binascii.Error):
+ self.sendCode(501, b"Syntax error in parameters or arguments")
+ self.challenger = None
+ self.mode = COMMAND
+ return
+
+ self.challenger.setResponse(uncoded)
+ if self.challenger.moreChallenges():
+ challenge = self.challenger.getChallenge()
+ coded = base64.b64encode(challenge)
+ self.sendCode(334, coded)
+ return
+
+ self.mode = COMMAND
+ result = self.portal.login(
+ self.challenger, None, IMessageDeliveryFactory, IMessageDelivery
+ )
+ result.addCallback(self._cbAuthenticated)
+ result.addCallback(
+ lambda ign: self.sendCode(235, b"Authentication successful.")
+ )
+ result.addErrback(self._ebAuthenticated)
+
+
+class SenderMixin:
+ """
+ Utility class for sending emails easily.
+
+ Use with SMTPSenderFactory or ESMTPSenderFactory.
+ """
+
+ done = 0
+
+ def getMailFrom(self):
+ if not self.done:
+ self.done = 1
+ return str(self.factory.fromEmail)
+ else:
+ return None
+
+ def getMailTo(self):
+ return self.factory.toEmail
+
+ def getMailData(self):
+ return self.factory.file
+
+ def sendError(self, exc):
+ # Call the base class to close the connection with the SMTP server
+ SMTPClient.sendError(self, exc)
+
+ # Do not retry to connect to SMTP Server if:
+ # 1. No more retries left (This allows the correct error to be returned to the errorback)
+ # 2. retry is false
+ # 3. The error code is not in the 4xx range (Communication Errors)
+
+ if self.factory.retries >= 0 or (
+ not exc.retry and not (exc.code >= 400 and exc.code < 500)
+ ):
+ self.factory.sendFinished = True
+ self.factory.result.errback(exc)
+
+ def sentMail(self, code, resp, numOk, addresses, log):
+ # Do not retry, the SMTP server acknowledged the request
+ self.factory.sendFinished = True
+ if code not in SUCCESS:
+ errlog = []
+ for addr, acode, aresp in addresses:
+ if acode not in SUCCESS:
+ errlog.append(
+ addr + b": " + networkString("%03d" % (acode,)) + b" " + aresp
+ )
+
+ errlog.append(log.str())
+
+ exc = SMTPDeliveryError(code, resp, b"\n".join(errlog), addresses)
+ self.factory.result.errback(exc)
+ else:
+ self.factory.result.callback((numOk, addresses))
+
+
+class SMTPSender(SenderMixin, SMTPClient):
+ """
+ SMTP protocol that sends a single email based on information it
+ gets from its factory, a L{SMTPSenderFactory}.
+ """
+
+
+class SMTPSenderFactory(protocol.ClientFactory):
+ """
+ Utility factory for sending emails easily.
+
+ @type currentProtocol: L{SMTPSender}
+ @ivar currentProtocol: The current running protocol returned by
+ L{buildProtocol}.
+
+ @type sendFinished: C{bool}
+ @ivar sendFinished: When the value is set to True, it means the message has
+ been sent or there has been an unrecoverable error or the sending has
+ been cancelled. The default value is False.
+ """
+
+ domain = DNSNAME
+ protocol: Type[SMTPClient] = SMTPSender
+
+ def __init__(self, fromEmail, toEmail, file, deferred, retries=5, timeout=None):
+ """
+ @param fromEmail: The RFC 2821 address from which to send this
+ message.
+
+ @param toEmail: A sequence of RFC 2821 addresses to which to
+ send this message.
+
+ @param file: A file-like object containing the message to send.
+
+ @param deferred: A Deferred to callback or errback when sending
+ of this message completes.
+ @type deferred: L{defer.Deferred}
+
+ @param retries: The number of times to retry delivery of this
+ message.
+
+ @param timeout: Period, in seconds, for which to wait for
+ server responses, or None to wait forever.
+ """
+ assert isinstance(retries, int)
+
+ if isinstance(toEmail, str):
+ toEmail = [toEmail.encode("ascii")]
+ elif isinstance(toEmail, bytes):
+ toEmail = [toEmail]
+ else:
+ toEmailFinal = []
+ for _email in toEmail:
+ if not isinstance(_email, bytes):
+ _email = _email.encode("ascii")
+
+ toEmailFinal.append(_email)
+ toEmail = toEmailFinal
+
+ self.fromEmail = Address(fromEmail)
+ self.nEmails = len(toEmail)
+ self.toEmail = toEmail
+ self.file = file
+ self.result = deferred
+ self.result.addBoth(self._removeDeferred)
+ self.sendFinished = False
+ self.currentProtocol = None
+
+ self.retries = -retries
+ self.timeout = timeout
+
+ def _removeDeferred(self, result):
+ del self.result
+ return result
+
+ def clientConnectionFailed(self, connector, err):
+ self._processConnectionError(connector, err)
+
+ def clientConnectionLost(self, connector, err):
+ self._processConnectionError(connector, err)
+
+ def _processConnectionError(self, connector, err):
+ self.currentProtocol = None
+ if (self.retries < 0) and (not self.sendFinished):
+ log.msg("SMTP Client retrying server. Retry: %s" % -self.retries)
+
+ # Rewind the file in case part of it was read while attempting to
+ # send the message.
+ self.file.seek(0, 0)
+ connector.connect()
+ self.retries += 1
+ elif not self.sendFinished:
+ # If we were unable to communicate with the SMTP server a ConnectionDone will be
+ # returned. We want a more clear error message for debugging
+ if err.check(error.ConnectionDone):
+ err.value = SMTPConnectError(-1, "Unable to connect to server.")
+ self.result.errback(err.value)
+
+ def buildProtocol(self, addr):
+ p = self.protocol(self.domain, self.nEmails * 2 + 2)
+ p.factory = self
+ p.timeout = self.timeout
+ self.currentProtocol = p
+ self.result.addBoth(self._removeProtocol)
+ return p
+
+ def _removeProtocol(self, result):
+ """
+ Remove the protocol created in C{buildProtocol}.
+
+ @param result: The result/error passed to the callback/errback of
+ L{defer.Deferred}.
+
+ @return: The C{result} untouched.
+ """
+ if self.currentProtocol:
+ self.currentProtocol = None
+ return result
+
+
+class LOGINCredentials(_lcredentials):
+ """
+ L{LOGINCredentials} generates challenges for I{LOGIN} authentication.
+
+ For interoperability with Outlook, the challenge generated does not exactly
+ match the one defined in the
+ U{draft specification<http://sepp.oetiker.ch/sasl-2.1.19-ds/draft-murchison-sasl-login-00.txt>}.
+ """
+
+ def __init__(self):
+ _lcredentials.__init__(self)
+ self.challenges = [b"Password:", b"Username:"]
+
+
+@implementer(IClientAuthentication)
+class PLAINAuthenticator:
+ def __init__(self, user):
+ self.user = user
+
+ def getName(self):
+ return b"PLAIN"
+
+ def challengeResponse(self, secret, chal=1):
+ if chal == 1:
+ return self.user + b"\0" + self.user + b"\0" + secret
+ else:
+ return b"\0" + self.user + b"\0" + secret
+
+
+class ESMTPSender(SenderMixin, ESMTPClient):
+ requireAuthentication = True
+ requireTransportSecurity = True
+
+ def __init__(self, username, secret, contextFactory=None, *args, **kw):
+ self.heloFallback = 0
+ self.username = username
+
+ self._hostname = kw.pop("hostname", None)
+
+ if contextFactory is None:
+ contextFactory = self._getContextFactory()
+
+ ESMTPClient.__init__(self, secret, contextFactory, *args, **kw)
+
+ self._registerAuthenticators()
+
+ def _registerAuthenticators(self):
+ # Register Authenticator in order from most secure to least secure
+ self.registerAuthenticator(CramMD5ClientAuthenticator(self.username))
+ self.registerAuthenticator(LOGINAuthenticator(self.username))
+ self.registerAuthenticator(PLAINAuthenticator(self.username))
+
+ def _getContextFactory(self):
+ if self.context is not None:
+ return self.context
+ if self._hostname is None:
+ return None
+ try:
+ from twisted.internet.ssl import optionsForClientTLS
+ except ImportError:
+ return None
+ else:
+ context = optionsForClientTLS(self._hostname)
+ return context
+
+
+class ESMTPSenderFactory(SMTPSenderFactory):
+ """
+ Utility factory for sending emails easily.
+
+ @type currentProtocol: L{ESMTPSender}
+ @ivar currentProtocol: The current running protocol as made by
+ L{buildProtocol}.
+ """
+
+ protocol = ESMTPSender
+
+ def __init__(
+ self,
+ username,
+ password,
+ fromEmail,
+ toEmail,
+ file,
+ deferred,
+ retries=5,
+ timeout=None,
+ contextFactory=None,
+ heloFallback=False,
+ requireAuthentication=True,
+ requireTransportSecurity=True,
+ hostname=None,
+ ):
+ SMTPSenderFactory.__init__(
+ self, fromEmail, toEmail, file, deferred, retries, timeout
+ )
+ self.username = username
+ self.password = password
+ self._contextFactory = contextFactory
+ self._heloFallback = heloFallback
+ self._requireAuthentication = requireAuthentication
+ self._requireTransportSecurity = requireTransportSecurity
+ self._hostname = hostname
+
+ def buildProtocol(self, addr):
+ """
+ Build an L{ESMTPSender} protocol configured with C{heloFallback},
+ C{requireAuthentication}, and C{requireTransportSecurity} as specified
+ in L{__init__}.
+
+ This sets L{currentProtocol} on the factory, as well as returning it.
+
+ @rtype: L{ESMTPSender}
+ """
+ p = self.protocol(
+ self.username,
+ self.password,
+ self._contextFactory,
+ self.domain,
+ self.nEmails * 2 + 2,
+ hostname=self._hostname,
+ )
+ p.heloFallback = self._heloFallback
+ p.requireAuthentication = self._requireAuthentication
+ p.requireTransportSecurity = self._requireTransportSecurity
+ p.factory = self
+ p.timeout = self.timeout
+ self.currentProtocol = p
+ self.result.addBoth(self._removeProtocol)
+ return p
+
+
+def sendmail(
+ smtphost,
+ from_addr,
+ to_addrs,
+ msg,
+ senderDomainName=None,
+ port=25,
+ reactor=reactor,
+ username=None,
+ password=None,
+ requireAuthentication=False,
+ requireTransportSecurity=False,
+):
+ """
+ Send an email.
+
+ This interface is intended to be a replacement for L{smtplib.SMTP.sendmail}
+ and related methods. To maintain backwards compatibility, it will fall back
+ to plain SMTP, if ESMTP support is not available. If ESMTP support is
+ available, it will attempt to provide encryption via STARTTLS and
+ authentication if a secret is provided.
+
+ @param smtphost: The host the message should be sent to.
+ @type smtphost: L{bytes}
+
+ @param from_addr: The (envelope) address sending this mail.
+ @type from_addr: L{bytes}
+
+ @param to_addrs: A list of addresses to send this mail to. A string will
+ be treated as a list of one address.
+ @type to_addrs: L{list} of L{bytes} or L{bytes}
+
+ @param msg: The message, including headers, either as a file or a string.
+ File-like objects need to support read() and close(). Lines must be
+ delimited by '\\n'. If you pass something that doesn't look like a file,
+ we try to convert it to a string (so you should be able to pass an
+ L{email.message} directly, but doing the conversion with
+ L{email.generator} manually will give you more control over the process).
+
+ @param senderDomainName: Name by which to identify. If None, try to pick
+ something sane (but this depends on external configuration and may not
+ succeed).
+ @type senderDomainName: L{bytes}
+
+ @param port: Remote port to which to connect.
+ @type port: L{int}
+
+ @param username: The username to use, if wanting to authenticate.
+ @type username: L{bytes} or L{unicode}
+
+ @param password: The secret to use, if wanting to authenticate. If you do
+ not specify this, SMTP authentication will not occur.
+ @type password: L{bytes} or L{unicode}
+
+ @param requireTransportSecurity: Whether or not STARTTLS is required.
+ @type requireTransportSecurity: L{bool}
+
+ @param requireAuthentication: Whether or not authentication is required.
+ @type requireAuthentication: L{bool}
+
+ @param reactor: The L{reactor} used to make the TCP connection.
+
+ @rtype: L{Deferred}
+ @returns: A cancellable L{Deferred}, its callback will be called if a
+ message is sent to ANY address, the errback if no message is sent. When
+ the C{cancel} method is called, it will stop retrying and disconnect
+ the connection immediately.
+
+ The callback will be called with a tuple (numOk, addresses) where numOk
+ is the number of successful recipient addresses and addresses is a list
+ of tuples (address, code, resp) giving the response to the RCPT command
+ for each address.
+ """
+ if not hasattr(msg, "read"):
+ # It's not a file
+ msg = BytesIO(bytes(msg))
+
+ def cancel(d):
+ """
+ Cancel the L{twisted.mail.smtp.sendmail} call, tell the factory not to
+ retry and disconnect the connection.
+
+ @param d: The L{defer.Deferred} to be cancelled.
+ """
+ factory.sendFinished = True
+ if factory.currentProtocol:
+ factory.currentProtocol.transport.abortConnection()
+ else:
+ # Connection hasn't been made yet
+ connector.disconnect()
+
+ d = defer.Deferred(cancel)
+
+ if isinstance(username, str):
+ username = username.encode("utf-8")
+ if isinstance(password, str):
+ password = password.encode("utf-8")
+
+ tlsHostname = smtphost
+ if not isinstance(tlsHostname, str):
+ tlsHostname = _idnaText(tlsHostname)
+
+ factory = ESMTPSenderFactory(
+ username,
+ password,
+ from_addr,
+ to_addrs,
+ msg,
+ d,
+ heloFallback=True,
+ requireAuthentication=requireAuthentication,
+ requireTransportSecurity=requireTransportSecurity,
+ hostname=tlsHostname,
+ )
+
+ if senderDomainName is not None:
+ factory.domain = networkString(senderDomainName)
+
+ connector = reactor.connectTCP(smtphost, port, factory)
+
+ return d
+
+
+import codecs
+
+
+def xtext_encode(s, errors=None):
+ r = []
+ for ch in iterbytes(s):
+ o = ord(ch)
+ if ch == "+" or ch == "=" or o < 33 or o > 126:
+ r.append(networkString(f"+{o:02X}"))
+ else:
+ r.append(bytes((o,)))
+ return (b"".join(r), len(s))
+
+
+def xtext_decode(s, errors=None):
+ """
+ Decode the xtext-encoded string C{s}.
+
+ @param s: String to decode.
+ @param errors: codec error handling scheme.
+ @return: The decoded string.
+ """
+ r = []
+ i = 0
+ while i < len(s):
+ if s[i : i + 1] == b"+":
+ try:
+ r.append(chr(int(bytes(s[i + 1 : i + 3]), 16)))
+ except ValueError:
+ r.append(ord(s[i : i + 3]))
+ i += 3
+ else:
+ r.append(bytes(s[i : i + 1]).decode("ascii"))
+ i += 1
+ return ("".join(r), len(s))
+
+
+class xtextStreamReader(codecs.StreamReader):
+ def decode(self, s, errors="strict"):
+ return xtext_decode(s)
+
+
+class xtextStreamWriter(codecs.StreamWriter):
+ def decode(self, s, errors="strict"):
+ return xtext_encode(s)
+
+
+def xtext_codec(name):
+ if name == "xtext":
+ return (xtext_encode, xtext_decode, xtextStreamReader, xtextStreamWriter)
+
+
+codecs.register(xtext_codec)
diff --git a/contrib/python/Twisted/py3/twisted/mail/tap.py b/contrib/python/Twisted/py3/twisted/mail/tap.py
new file mode 100644
index 0000000000..1217808e3a
--- /dev/null
+++ b/contrib/python/Twisted/py3/twisted/mail/tap.py
@@ -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