diff options
author | Vinay Sajip <vinay_sajip@yahoo.co.uk> | 2012-05-26 19:39:27 (GMT) |
---|---|---|
committer | Vinay Sajip <vinay_sajip@yahoo.co.uk> | 2012-05-26 19:39:27 (GMT) |
commit | ee2bbed9252fe286bc14765992877e1502b70328 (patch) | |
tree | 6d562b8297c5d7a21c52b4c3cfad6266d79ba630 /Lib | |
parent | 42211426eb9771424b9e5153d12f8c2c0c538d34 (diff) | |
parent | d1a30c939cc6378423dd3cc22382a9abe2a7d882 (diff) | |
download | cpython-ee2bbed9252fe286bc14765992877e1502b70328.zip cpython-ee2bbed9252fe286bc14765992877e1502b70328.tar.gz cpython-ee2bbed9252fe286bc14765992877e1502b70328.tar.bz2 |
Merged upstream changes.
Diffstat (limited to 'Lib')
-rw-r--r-- | Lib/email/_header_value_parser.py | 12 | ||||
-rw-r--r-- | Lib/importlib/_bootstrap.py | 2 | ||||
-rw-r--r-- | Lib/ipaddress.py | 136 | ||||
-rwxr-xr-x | Lib/smtpd.py | 245 | ||||
-rw-r--r-- | Lib/test/test_email/test__header_value_parser.py | 15 | ||||
-rw-r--r-- | Lib/test/test_ipaddress.py | 40 | ||||
-rw-r--r-- | Lib/test/test_logging.py | 3 | ||||
-rw-r--r-- | Lib/test/test_smtpd.py | 280 | ||||
-rw-r--r-- | Lib/test/test_smtplib.py | 19 |
9 files changed, 530 insertions, 222 deletions
diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py index 87d8f68..f4a01f1 100644 --- a/Lib/email/_header_value_parser.py +++ b/Lib/email/_header_value_parser.py @@ -791,6 +791,8 @@ class AngleAddr(TokenList): for x in self: if x.token_type == 'addr-spec': return x.addr_spec + else: + return '<>' class ObsRoute(TokenList): @@ -1829,6 +1831,14 @@ def get_angle_addr(value): "expected angle-addr but found '{}'".format(value)) angle_addr.append(ValueTerminal('<', 'angle-addr-start')) value = value[1:] + # Although it is not legal per RFC5322, SMTP uses '<>' in certain + # circumstances. + if value[0] == '>': + angle_addr.append(ValueTerminal('>', 'angle-addr-end')) + angle_addr.defects.append(errors.InvalidHeaderDefect( + "null addr-spec in angle-addr")) + value = value[1:] + return angle_addr, value try: token, value = get_addr_spec(value) except errors.HeaderParseError: @@ -1838,7 +1848,7 @@ def get_angle_addr(value): "obsolete route specification in angle-addr")) except errors.HeaderParseError: raise errors.HeaderParseError( - "expected addr-spec or but found '{}'".format(value)) + "expected addr-spec or obs-route but found '{}'".format(value)) angle_addr.append(token) token, value = get_addr_spec(value) angle_addr.append(token) diff --git a/Lib/importlib/_bootstrap.py b/Lib/importlib/_bootstrap.py index 3dcd05a..deaded9 100644 --- a/Lib/importlib/_bootstrap.py +++ b/Lib/importlib/_bootstrap.py @@ -949,8 +949,6 @@ class NamespaceLoader: def module_repr(cls, module): return "<module '{}' (namespace)>".format(module.__name__) - @set_package - @set_loader @module_for_loader def load_module(self, module): """Load a namespace module.""" diff --git a/Lib/ipaddress.py b/Lib/ipaddress.py index cc6760b..cb35685 100644 --- a/Lib/ipaddress.py +++ b/Lib/ipaddress.py @@ -1,17 +1,5 @@ # Copyright 2007 Google Inc. # Licensed to PSF under a Contributor Agreement. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. See the License for the specific language governing -# permissions and limitations under the License. """A fast, lightweight IPv4/IPv6 manipulation library in Python. @@ -36,34 +24,22 @@ class NetmaskValueError(ValueError): """A Value Error related to the netmask.""" -def ip_address(address, version=None): +def ip_address(address): """Take an IP string/int and return an object of the correct type. Args: address: A string or integer, the IP address. Either IPv4 or IPv6 addresses may be supplied; integers less than 2**32 will be considered to be IPv4 by default. - version: An integer, 4 or 6. If set, don't try to automatically - determine what the IP address type is. Important for things - like ip_address(1), which could be IPv4, '192.0.2.1', or IPv6, - '2001:db8::1'. Returns: An IPv4Address or IPv6Address object. Raises: ValueError: if the *address* passed isn't either a v4 or a v6 - address, or if the version is not None, 4, or 6. + address """ - if version is not None: - if version == 4: - return IPv4Address(address) - elif version == 6: - return IPv6Address(address) - else: - raise ValueError() - try: return IPv4Address(address) except (AddressValueError, NetmaskValueError): @@ -78,35 +54,22 @@ def ip_address(address, version=None): address) -def ip_network(address, version=None, strict=True): +def ip_network(address, strict=True): """Take an IP string/int and return an object of the correct type. Args: address: A string or integer, the IP network. Either IPv4 or IPv6 networks may be supplied; integers less than 2**32 will be considered to be IPv4 by default. - version: An integer, 4 or 6. If set, don't try to automatically - determine what the IP address type is. Important for things - like ip_network(1), which could be IPv4, '192.0.2.1/32', or IPv6, - '2001:db8::1/128'. Returns: An IPv4Network or IPv6Network object. Raises: ValueError: if the string passed isn't either a v4 or a v6 - address. Or if the network has host bits set. Or if the version - is not None, 4, or 6. + address. Or if the network has host bits set. """ - if version is not None: - if version == 4: - return IPv4Network(address, strict) - elif version == 6: - return IPv6Network(address, strict) - else: - raise ValueError() - try: return IPv4Network(address, strict) except (AddressValueError, NetmaskValueError): @@ -121,24 +84,20 @@ def ip_network(address, version=None, strict=True): address) -def ip_interface(address, version=None): +def ip_interface(address): """Take an IP string/int and return an object of the correct type. Args: address: A string or integer, the IP address. Either IPv4 or IPv6 addresses may be supplied; integers less than 2**32 will be considered to be IPv4 by default. - version: An integer, 4 or 6. If set, don't try to automatically - determine what the IP address type is. Important for things - like ip_interface(1), which could be IPv4, '192.0.2.1/32', or IPv6, - '2001:db8::1/128'. Returns: An IPv4Interface or IPv6Interface object. Raises: ValueError: if the string passed isn't either a v4 or a v6 - address. Or if the version is not None, 4, or 6. + address. Notes: The IPv?Interface classes describe an Address on a particular @@ -146,14 +105,6 @@ def ip_interface(address, version=None): and Network classes. """ - if version is not None: - if version == 4: - return IPv4Interface(address) - elif version == 6: - return IPv6Interface(address) - else: - raise ValueError() - try: return IPv4Interface(address) except (AddressValueError, NetmaskValueError): @@ -281,7 +232,7 @@ def summarize_address_range(first, last): If the first and last objects are not the same version. ValueError: If the last object is not greater than the first. - If the version is not 4 or 6. + If the version of the first address is not 4 or 6. """ if (not (isinstance(first, _BaseAddress) and @@ -318,7 +269,7 @@ def summarize_address_range(first, last): if current == ip._ALL_ONES: break first_int = current + 1 - first = ip_address(first_int, version=first._version) + first = first.__class__(first_int) def _collapse_addresses_recursive(addresses): @@ -586,12 +537,12 @@ class _BaseAddress(_IPAddressBase): def __add__(self, other): if not isinstance(other, int): return NotImplemented - return ip_address(int(self) + other, version=self._version) + return self.__class__(int(self) + other) def __sub__(self, other): if not isinstance(other, int): return NotImplemented - return ip_address(int(self) - other, version=self._version) + return self.__class__(int(self) - other) def __repr__(self): return '%s(%r)' % (self.__class__.__name__, str(self)) @@ -612,13 +563,12 @@ class _BaseAddress(_IPAddressBase): class _BaseNetwork(_IPAddressBase): - """A generic IP object. + """A generic IP network object. This IP class contains the version independent methods which are used by networks. """ - def __init__(self, address): self._cache = {} @@ -642,14 +592,14 @@ class _BaseNetwork(_IPAddressBase): bcast = int(self.broadcast_address) - 1 while cur <= bcast: cur += 1 - yield ip_address(cur - 1, version=self._version) + yield self._address_class(cur - 1) def __iter__(self): cur = int(self.network_address) bcast = int(self.broadcast_address) while cur <= bcast: cur += 1 - yield ip_address(cur - 1, version=self._version) + yield self._address_class(cur - 1) def __getitem__(self, n): network = int(self.network_address) @@ -657,12 +607,12 @@ class _BaseNetwork(_IPAddressBase): if n >= 0: if network + n > broadcast: raise IndexError - return ip_address(network + n, version=self._version) + return self._address_class(network + n) else: n += 1 if broadcast + n < network: raise IndexError - return ip_address(broadcast + n, version=self._version) + return self._address_class(broadcast + n) def __lt__(self, other): if self._version != other._version: @@ -746,8 +696,8 @@ class _BaseNetwork(_IPAddressBase): def broadcast_address(self): x = self._cache.get('broadcast_address') if x is None: - x = ip_address(int(self.network_address) | int(self.hostmask), - version=self._version) + x = self._address_class(int(self.network_address) | + int(self.hostmask)) self._cache['broadcast_address'] = x return x @@ -755,17 +705,11 @@ class _BaseNetwork(_IPAddressBase): def hostmask(self): x = self._cache.get('hostmask') if x is None: - x = ip_address(int(self.netmask) ^ self._ALL_ONES, - version=self._version) + x = self._address_class(int(self.netmask) ^ self._ALL_ONES) self._cache['hostmask'] = x return x @property - def network(self): - return ip_network('%s/%d' % (str(self.network_address), - self.prefixlen)) - - @property def with_prefixlen(self): return '%s/%d' % (str(self.ip), self._prefixlen) @@ -787,6 +731,10 @@ class _BaseNetwork(_IPAddressBase): raise NotImplementedError('BaseNet has no version') @property + def _address_class(self): + raise NotImplementedError('BaseNet has no associated address class') + + @property def prefixlen(self): return self._prefixlen @@ -840,9 +788,8 @@ class _BaseNetwork(_IPAddressBase): raise StopIteration # Make sure we're comparing the network of other. - other = ip_network('%s/%s' % (str(other.network_address), - str(other.prefixlen)), - version=other._version) + other = other.__class__('%s/%s' % (str(other.network_address), + str(other.prefixlen))) s1, s2 = self.subnets() while s1 != other and s2 != other: @@ -973,9 +920,9 @@ class _BaseNetwork(_IPAddressBase): 'prefix length diff %d is invalid for netblock %s' % ( new_prefixlen, str(self))) - first = ip_network('%s/%s' % (str(self.network_address), - str(self._prefixlen + prefixlen_diff)), - version=self._version) + first = self.__class__('%s/%s' % + (str(self.network_address), + str(self._prefixlen + prefixlen_diff))) yield first current = first @@ -983,17 +930,12 @@ class _BaseNetwork(_IPAddressBase): broadcast = current.broadcast_address if broadcast == self.broadcast_address: return - new_addr = ip_address(int(broadcast) + 1, version=self._version) - current = ip_network('%s/%s' % (str(new_addr), str(new_prefixlen)), - version=self._version) + new_addr = self._address_class(int(broadcast) + 1) + current = self.__class__('%s/%s' % (str(new_addr), + str(new_prefixlen))) yield current - def masked(self): - """Return the network object with the host bits masked out.""" - return ip_network('%s/%d' % (self.network_address, self._prefixlen), - version=self._version) - def supernet(self, prefixlen_diff=1, new_prefix=None): """The supernet containing the current network. @@ -1030,11 +972,10 @@ class _BaseNetwork(_IPAddressBase): 'current prefixlen is %d, cannot have a prefixlen_diff of %d' % (self.prefixlen, prefixlen_diff)) # TODO (pmoody): optimize this. - t = ip_network('%s/%d' % (str(self.network_address), - self.prefixlen - prefixlen_diff), - version=self._version, strict=False) - return ip_network('%s/%d' % (str(t.network_address), t.prefixlen), - version=t._version) + t = self.__class__('%s/%d' % (str(self.network_address), + self.prefixlen - prefixlen_diff), + strict=False) + return t.__class__('%s/%d' % (str(t.network_address), t.prefixlen)) class _BaseV4(object): @@ -1391,6 +1332,9 @@ class IPv4Network(_BaseV4, _BaseNetwork): .prefixlen: 27 """ + # Class to use when creating address objects + # TODO (ncoghlan): Investigate using IPv4Interface instead + _address_class = IPv4Address # the valid octets for host and netmasks. only useful for IPv4. _valid_mask_octets = set((255, 254, 252, 248, 240, 224, 192, 128, 0)) @@ -1952,7 +1896,7 @@ class _BaseV6(object): """ if isinstance(self, IPv6Network): - return int(self.network) == 1 and getattr( + return int(self) == 1 and getattr( self, '_prefixlen', 128) == 128 elif isinstance(self, IPv6Interface): return int(self.network.network_address) == 1 and getattr( @@ -2071,6 +2015,10 @@ class IPv6Network(_BaseV6, _BaseNetwork): """ + # Class to use when creating address objects + # TODO (ncoghlan): Investigate using IPv6Interface instead + _address_class = IPv6Address + def __init__(self, address, strict=True): """Instantiate a new IPv6 Network object. diff --git a/Lib/smtpd.py b/Lib/smtpd.py index 748fcae..778d6d6 100755 --- a/Lib/smtpd.py +++ b/Lib/smtpd.py @@ -1,5 +1,5 @@ #! /usr/bin/env python3 -"""An RFC 2821 smtp proxy. +"""An RFC 5321 smtp proxy. Usage: %(program)s [options] [localhost:localport [remotehost:remoteport]] @@ -20,6 +20,11 @@ Options: Use `classname' as the concrete SMTP proxy class. Uses `PureProxy' by default. + --size limit + -s limit + Restrict the total size of the incoming message to "limit" number of + bytes via the RFC 1870 SIZE extension. Defaults to 33554432 bytes. + --debug -d Turn on debugging prints. @@ -35,10 +40,9 @@ given then 8025 is used. If remotehost is not given then `localhost' is used, and if remoteport is not given, then 25 is used. """ - # Overview: # -# This file implements the minimal SMTP protocol as defined in RFC 821. It +# This file implements the minimal SMTP protocol as defined in RFC 5321. It # has a hierarchy of classes which implement the backend functionality for the # smtpd. A number of classes are provided: # @@ -66,7 +70,7 @@ and if remoteport is not given, then 25 is used. # # - support mailbox delivery # - alias files -# - ESMTP +# - Handle more ESMTP extensions # - handle error codes from the backend smtpd import sys @@ -77,12 +81,14 @@ import time import socket import asyncore import asynchat +import collections from warnings import warn +from email._header_value_parser import get_addr_spec, get_angle_addr __all__ = ["SMTPServer","DebuggingServer","PureProxy","MailmanProxy"] program = sys.argv[0] -__version__ = 'Python SMTP proxy version 0.2' +__version__ = 'Python SMTP proxy version 0.3' class Devnull: @@ -94,9 +100,9 @@ DEBUGSTREAM = Devnull() NEWLINE = '\n' EMPTYSTRING = '' COMMASPACE = ', ' +DATA_SIZE_DEFAULT = 33554432 - def usage(code, msg=''): print(__doc__ % globals(), file=sys.stderr) if msg: @@ -104,19 +110,23 @@ def usage(code, msg=''): sys.exit(code) - class SMTPChannel(asynchat.async_chat): COMMAND = 0 DATA = 1 - data_size_limit = 33554432 command_size_limit = 512 + command_size_limits = collections.defaultdict(lambda x=command_size_limit: x) + command_size_limits.update({ + 'MAIL': command_size_limit + 26, + }) + max_command_size_limit = max(command_size_limits.values()) - def __init__(self, server, conn, addr): + def __init__(self, server, conn, addr, data_size_limit=DATA_SIZE_DEFAULT): asynchat.async_chat.__init__(self, conn) self.smtp_server = server self.conn = conn self.addr = addr + self.data_size_limit = data_size_limit self.received_lines = [] self.smtp_state = self.COMMAND self.seen_greeting = '' @@ -137,6 +147,7 @@ class SMTPChannel(asynchat.async_chat): print('Peer:', repr(self.peer), file=DEBUGSTREAM) self.push('220 %s %s' % (self.fqdn, __version__)) self.set_terminator(b'\r\n') + self.extended_smtp = False # properties for backwards-compatibility @property @@ -268,7 +279,7 @@ class SMTPChannel(asynchat.async_chat): def collect_incoming_data(self, data): limit = None if self.smtp_state == self.COMMAND: - limit = self.command_size_limit + limit = self.max_command_size_limit elif self.smtp_state == self.DATA: limit = self.data_size_limit if limit and self.num_bytes > limit: @@ -283,11 +294,7 @@ class SMTPChannel(asynchat.async_chat): print('Data:', repr(line), file=DEBUGSTREAM) self.received_lines = [] if self.smtp_state == self.COMMAND: - if self.num_bytes > self.command_size_limit: - self.push('500 Error: line too long') - self.num_bytes = 0 - return - self.num_bytes = 0 + sz, self.num_bytes = self.num_bytes, 0 if not line: self.push('500 Error: bad syntax') return @@ -299,9 +306,14 @@ class SMTPChannel(asynchat.async_chat): else: command = line[:i].upper() arg = line[i+1:].strip() + max_sz = (self.command_size_limits[command] + if self.extended_smtp else self.command_size_limit) + if sz > max_sz: + self.push('500 Error: line too long') + return method = getattr(self, 'smtp_' + command, None) if not method: - self.push('502 Error: command "%s" not implemented' % command) + self.push('500 Error: command "%s" not recognized' % command) return method(arg) return @@ -310,12 +322,12 @@ class SMTPChannel(asynchat.async_chat): self.push('451 Internal confusion') self.num_bytes = 0 return - if self.num_bytes > self.data_size_limit: + if self.data_size_limit and self.num_bytes > self.data_size_limit: self.push('552 Error: Too much mail data') self.num_bytes = 0 return # Remove extraneous carriage returns and de-transparency according - # to RFC 821, Section 4.5.2. + # to RFC 5321, Section 4.5.2. data = [] for text in line.split('\r\n'): if text and text[0] == '.': @@ -333,7 +345,7 @@ class SMTPChannel(asynchat.async_chat): self.num_bytes = 0 self.set_terminator(b'\r\n') if not status: - self.push('250 Ok') + self.push('250 OK') else: self.push(status) @@ -346,66 +358,188 @@ class SMTPChannel(asynchat.async_chat): self.push('503 Duplicate HELO/EHLO') else: self.seen_greeting = arg + self.extended_smtp = False self.push('250 %s' % self.fqdn) + def smtp_EHLO(self, arg): + if not arg: + self.push('501 Syntax: EHLO hostname') + return + if self.seen_greeting: + self.push('503 Duplicate HELO/EHLO') + else: + self.seen_greeting = arg + self.extended_smtp = True + self.push('250-%s' % self.fqdn) + if self.data_size_limit: + self.push('250-SIZE %s' % self.data_size_limit) + self.push('250 HELP') + def smtp_NOOP(self, arg): if arg: self.push('501 Syntax: NOOP') else: - self.push('250 Ok') + self.push('250 OK') def smtp_QUIT(self, arg): # args is ignored self.push('221 Bye') self.close_when_done() - # factored - def __getaddr(self, keyword, arg): - address = None + def _strip_command_keyword(self, keyword, arg): keylen = len(keyword) if arg[:keylen].upper() == keyword: - address = arg[keylen:].strip() - if not address: - pass - elif address[0] == '<' and address[-1] == '>' and address != '<>': - # Addresses can be in the form <person@dom.com> but watch out - # for null address, e.g. <> - address = address[1:-1] - return address + return arg[keylen:].strip() + return '' + + def _getaddr(self, arg): + if not arg: + return '', '' + if arg.lstrip().startswith('<'): + address, rest = get_angle_addr(arg) + else: + address, rest = get_addr_spec(arg) + if not address: + return address, rest + return address.addr_spec, rest + + def _getparams(self, params): + # Return any parameters that appear to be syntactically valid according + # to RFC 1869, ignore all others. (Postel rule: accept what we can.) + params = [param.split('=', 1) for param in params.split() + if '=' in param] + return {k: v for k, v in params if k.isalnum()} + + def smtp_HELP(self, arg): + if arg: + extended = ' [SP <mail parameters]' + lc_arg = arg.upper() + if lc_arg == 'EHLO': + self.push('250 Syntax: EHLO hostname') + elif lc_arg == 'HELO': + self.push('250 Syntax: HELO hostname') + elif lc_arg == 'MAIL': + msg = '250 Syntax: MAIL FROM: <address>' + if self.extended_smtp: + msg += extended + self.push(msg) + elif lc_arg == 'RCPT': + msg = '250 Syntax: RCPT TO: <address>' + if self.extended_smtp: + msg += extended + self.push(msg) + elif lc_arg == 'DATA': + self.push('250 Syntax: DATA') + elif lc_arg == 'RSET': + self.push('250 Syntax: RSET') + elif lc_arg == 'NOOP': + self.push('250 Syntax: NOOP') + elif lc_arg == 'QUIT': + self.push('250 Syntax: QUIT') + elif lc_arg == 'VRFY': + self.push('250 Syntax: VRFY <address>') + else: + self.push('501 Supported commands: EHLO HELO MAIL RCPT ' + 'DATA RSET NOOP QUIT VRFY') + else: + self.push('250 Supported commands: EHLO HELO MAIL RCPT DATA ' + 'RSET NOOP QUIT VRFY') + + def smtp_VRFY(self, arg): + if arg: + address, params = self._getaddr(arg) + if address: + self.push('252 Cannot VRFY user, but will accept message ' + 'and attempt delivery') + else: + self.push('502 Could not VRFY %s' % arg) + else: + self.push('501 Syntax: VRFY <address>') def smtp_MAIL(self, arg): if not self.seen_greeting: self.push('503 Error: send HELO first'); return - print('===> MAIL', arg, file=DEBUGSTREAM) - address = self.__getaddr('FROM:', arg) if arg else None + syntaxerr = '501 Syntax: MAIL FROM: <address>' + if self.extended_smtp: + syntaxerr += ' [SP <mail-parameters>]' + if arg is None: + self.push(syntaxerr) + return + arg = self._strip_command_keyword('FROM:', arg) + address, params = self._getaddr(arg) + if not address: + self.push(syntaxerr) + return + if not self.extended_smtp and params: + self.push(syntaxerr) + return if not address: - self.push('501 Syntax: MAIL FROM:<address>') + self.push(syntaxerr) return if self.mailfrom: self.push('503 Error: nested MAIL command') return + params = self._getparams(params.upper()) + if params is None: + self.push(syntaxerr) + return + size = params.pop('SIZE', None) + if size: + if not size.isdigit(): + self.push(syntaxerr) + return + elif self.data_size_limit and int(size) > self.data_size_limit: + self.push('552 Error: message size exceeds fixed maximum message size') + return + if len(params.keys()) > 0: + self.push('555 MAIL FROM parameters not recognized or not implemented') + return self.mailfrom = address print('sender:', self.mailfrom, file=DEBUGSTREAM) - self.push('250 Ok') + self.push('250 OK') def smtp_RCPT(self, arg): if not self.seen_greeting: self.push('503 Error: send HELO first'); return - print('===> RCPT', arg, file=DEBUGSTREAM) if not self.mailfrom: self.push('503 Error: need MAIL command') return - address = self.__getaddr('TO:', arg) if arg else None + syntaxerr = '501 Syntax: RCPT TO: <address>' + if self.extended_smtp: + syntaxerr += ' [SP <mail-parameters>]' + if arg is None: + self.push(syntaxerr) + return + arg = self._strip_command_keyword('TO:', arg) + address, params = self._getaddr(arg) + if not address: + self.push(syntaxerr) + return + if params: + if self.extended_smtp: + params = self._getparams(params.upper()) + if params is None: + self.push(syntaxerr) + return + else: + self.push(syntaxerr) + return + if not address: + self.push(syntaxerr) + return + if params and len(params.keys()) > 0: + self.push('555 RCPT TO parameters not recognized or not implemented') + return if not address: self.push('501 Syntax: RCPT TO: <address>') return self.rcpttos.append(address) print('recips:', self.rcpttos, file=DEBUGSTREAM) - self.push('250 Ok') + self.push('250 OK') def smtp_RSET(self, arg): if arg: @@ -416,13 +550,12 @@ class SMTPChannel(asynchat.async_chat): self.rcpttos = [] self.received_data = '' self.smtp_state = self.COMMAND - self.push('250 Ok') + self.push('250 OK') def smtp_DATA(self, arg): if not self.seen_greeting: self.push('503 Error: send HELO first'); return - if not self.rcpttos: self.push('503 Error: need RCPT command') return @@ -433,15 +566,20 @@ class SMTPChannel(asynchat.async_chat): self.set_terminator(b'\r\n.\r\n') self.push('354 End data with <CR><LF>.<CR><LF>') + # Commands that have not been implemented + def smtp_EXPN(self, arg): + self.push('502 EXPN not implemented') + - class SMTPServer(asyncore.dispatcher): # SMTPChannel class to use for managing client connections channel_class = SMTPChannel - def __init__(self, localaddr, remoteaddr): + def __init__(self, localaddr, remoteaddr, + data_size_limit=DATA_SIZE_DEFAULT): self._localaddr = localaddr self._remoteaddr = remoteaddr + self.data_size_limit = data_size_limit asyncore.dispatcher.__init__(self) try: self.create_socket(socket.AF_INET, socket.SOCK_STREAM) @@ -459,7 +597,7 @@ class SMTPServer(asyncore.dispatcher): def handle_accepted(self, conn, addr): print('Incoming connection from %s' % repr(addr), file=DEBUGSTREAM) - channel = self.channel_class(self, conn, addr) + channel = self.channel_class(self, conn, addr, self.data_size_limit) # API for "doing something useful with the message" def process_message(self, peer, mailfrom, rcpttos, data): @@ -487,7 +625,6 @@ class SMTPServer(asyncore.dispatcher): raise NotImplementedError - class DebuggingServer(SMTPServer): # Do something with the gathered message def process_message(self, peer, mailfrom, rcpttos, data): @@ -503,7 +640,6 @@ class DebuggingServer(SMTPServer): print('------------ END MESSAGE ------------') - class PureProxy(SMTPServer): def process_message(self, peer, mailfrom, rcpttos, data): lines = data.split('\n') @@ -544,7 +680,6 @@ class PureProxy(SMTPServer): return refused - class MailmanProxy(PureProxy): def process_message(self, peer, mailfrom, rcpttos, data): from io import StringIO @@ -623,19 +758,18 @@ class MailmanProxy(PureProxy): msg.Enqueue(mlist, torequest=1) - class Options: setuid = 1 classname = 'PureProxy' + size_limit = None - def parseargs(): global DEBUGSTREAM try: opts, args = getopt.getopt( - sys.argv[1:], 'nVhc:d', - ['class=', 'nosetuid', 'version', 'help', 'debug']) + sys.argv[1:], 'nVhc:s:d', + ['class=', 'nosetuid', 'version', 'help', 'size=', 'debug']) except getopt.error as e: usage(1, e) @@ -652,6 +786,13 @@ def parseargs(): options.classname = arg elif opt in ('-d', '--debug'): DEBUGSTREAM = sys.stderr + elif opt in ('-s', '--size'): + try: + int_size = int(arg) + options.size_limit = int_size + except: + print('Invalid size: ' + arg, file=sys.stderr) + sys.exit(1) # parse the rest of the arguments if len(args) < 1: @@ -686,7 +827,6 @@ def parseargs(): return options - if __name__ == '__main__': options = parseargs() # Become nobody @@ -699,7 +839,8 @@ if __name__ == '__main__': import __main__ as mod class_ = getattr(mod, classname) proxy = class_((options.localhost, options.localport), - (options.remotehost, options.remoteport)) + (options.remotehost, options.remoteport), + options.size_limit) if options.setuid: try: import pwd diff --git a/Lib/test/test_email/test__header_value_parser.py b/Lib/test/test_email/test__header_value_parser.py index 75fe299..2161af1 100644 --- a/Lib/test/test_email/test__header_value_parser.py +++ b/Lib/test/test_email/test__header_value_parser.py @@ -1429,6 +1429,19 @@ class TestParser(TestEmailBase): self.assertIsNone(angle_addr.route) self.assertEqual(angle_addr.addr_spec, 'dinsdale@example.com') + def test_get_angle_addr_empty(self): + angle_addr = self._test_get_x(parser.get_angle_addr, + '<>', + '<>', + '<>', + [errors.InvalidHeaderDefect], + '') + self.assertEqual(angle_addr.token_type, 'angle-addr') + self.assertIsNone(angle_addr.local_part) + self.assertIsNone(angle_addr.domain) + self.assertIsNone(angle_addr.route) + self.assertEqual(angle_addr.addr_spec, '<>') + def test_get_angle_addr_with_cfws(self): angle_addr = self._test_get_x(parser.get_angle_addr, ' (foo) <dinsdale@example.com>(bar)', @@ -2007,7 +2020,7 @@ class TestParser(TestEmailBase): self.assertEqual(group.mailboxes, group.all_mailboxes) - def test_get_troup_null_addr_spec(self): + def test_get_group_null_addr_spec(self): group = self._test_get_x(parser.get_group, 'foo: <>;', 'foo: <>;', diff --git a/Lib/test/test_ipaddress.py b/Lib/test/test_ipaddress.py index 6bf5174..fd6c38c 100644 --- a/Lib/test/test_ipaddress.py +++ b/Lib/test/test_ipaddress.py @@ -1,21 +1,7 @@ -#!/usr/bin/python3 -# # Copyright 2007 Google Inc. # Licensed to PSF under a Contributor Agreement. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Unittest for ipaddressmodule.""" + +"""Unittest for ipaddress module.""" import unittest @@ -404,7 +390,7 @@ class IpaddrUnitTest(unittest.TestCase): self.assertRaises(ValueError, list, self.ipv4_interface.network.subnets(-1)) self.assertRaises(ValueError, list, - self.ipv4_network.network.subnets(-1)) + self.ipv4_network.subnets(-1)) self.assertRaises(ValueError, list, self.ipv6_interface.network.subnets(-1)) self.assertRaises(ValueError, list, @@ -780,12 +766,6 @@ class IpaddrUnitTest(unittest.TestCase): self.assertEqual(self.ipv4_address.version, 4) self.assertEqual(self.ipv6_address.version, 6) - with self.assertRaises(ValueError): - ipaddress.ip_address('1', version=[]) - - with self.assertRaises(ValueError): - ipaddress.ip_address('1', version=5) - def testMaxPrefixLength(self): self.assertEqual(self.ipv4_interface.max_prefixlen, 32) self.assertEqual(self.ipv6_interface.max_prefixlen, 128) @@ -1052,12 +1032,7 @@ class IpaddrUnitTest(unittest.TestCase): def testForceVersion(self): self.assertEqual(ipaddress.ip_network(1).version, 4) - self.assertEqual(ipaddress.ip_network(1, version=6).version, 6) - - with self.assertRaises(ValueError): - ipaddress.ip_network(1, version='l') - with self.assertRaises(ValueError): - ipaddress.ip_network(1, version=3) + self.assertEqual(ipaddress.IPv6Network(1).version, 6) def testWithStar(self): self.assertEqual(str(self.ipv4_interface.with_prefixlen), "1.2.3.4/24") @@ -1148,13 +1123,6 @@ class IpaddrUnitTest(unittest.TestCase): sixtofouraddr.sixtofour) self.assertFalse(bad_addr.sixtofour) - def testIpInterfaceVersion(self): - with self.assertRaises(ValueError): - ipaddress.ip_interface(1, version=123) - - with self.assertRaises(ValueError): - ipaddress.ip_interface(1, version='') - if __name__ == '__main__': unittest.main() diff --git a/Lib/test/test_logging.py b/Lib/test/test_logging.py index 3adeaec..26baf11 100644 --- a/Lib/test/test_logging.py +++ b/Lib/test/test_logging.py @@ -663,6 +663,7 @@ if threading: self.smtp_server = server self.conn = conn self.addr = addr + self.data_size_limit = None self.received_lines = [] self.smtp_state = self.COMMAND self.seen_greeting = '' @@ -682,6 +683,7 @@ if threading: return self.push('220 %s %s' % (self.fqdn, smtpd.__version__)) self.set_terminator(b'\r\n') + self.extended_smtp = False class TestSMTPServer(smtpd.SMTPServer): @@ -709,6 +711,7 @@ if threading: def __init__(self, addr, handler, poll_interval, sockmap): self._localaddr = addr self._remoteaddr = None + self.data_size_limit = None self.sockmap = sockmap asyncore.dispatcher.__init__(self, map=sockmap) try: diff --git a/Lib/test/test_smtpd.py b/Lib/test/test_smtpd.py index a7dc5f6..dda1941 100644 --- a/Lib/test/test_smtpd.py +++ b/Lib/test/test_smtpd.py @@ -1,4 +1,4 @@ -from unittest import TestCase +import unittest from test import support, mock_socket import socket import io @@ -26,7 +26,7 @@ class BrokenDummyServer(DummyServer): raise DummyDispatcherBroken() -class SMTPDServerTest(TestCase): +class SMTPDServerTest(unittest.TestCase): def setUp(self): smtpd.socket = asyncore.socket = mock_socket @@ -39,7 +39,7 @@ class SMTPDServerTest(TestCase): channel.socket.queue_recv(line) channel.handle_read() - write_line(b'HELO test.example') + write_line(b'HELO example') write_line(b'MAIL From:eggs@example') write_line(b'RCPT To:spam@example') write_line(b'DATA') @@ -50,7 +50,7 @@ class SMTPDServerTest(TestCase): asyncore.socket = smtpd.socket = socket -class SMTPDChannelTest(TestCase): +class SMTPDChannelTest(unittest.TestCase): def setUp(self): smtpd.socket = asyncore.socket = mock_socket self.old_debugstream = smtpd.DEBUGSTREAM @@ -79,36 +79,94 @@ class SMTPDChannelTest(TestCase): self.assertEqual(self.channel.socket.last, b'500 Error: bad syntax\r\n') - def test_EHLO_not_implemented(self): - self.write_line(b'EHLO test.example') + def test_EHLO(self): + self.write_line(b'EHLO example') + self.assertEqual(self.channel.socket.last, b'250 HELP\r\n') + + def test_EHLO_bad_syntax(self): + self.write_line(b'EHLO') + self.assertEqual(self.channel.socket.last, + b'501 Syntax: EHLO hostname\r\n') + + def test_EHLO_duplicate(self): + self.write_line(b'EHLO example') + self.write_line(b'EHLO example') + self.assertEqual(self.channel.socket.last, + b'503 Duplicate HELO/EHLO\r\n') + + def test_EHLO_HELO_duplicate(self): + self.write_line(b'EHLO example') + self.write_line(b'HELO example') self.assertEqual(self.channel.socket.last, - b'502 Error: command "EHLO" not implemented\r\n') + b'503 Duplicate HELO/EHLO\r\n') def test_HELO(self): name = smtpd.socket.getfqdn() - self.write_line(b'HELO test.example') + self.write_line(b'HELO example') self.assertEqual(self.channel.socket.last, '250 {}\r\n'.format(name).encode('ascii')) + def test_HELO_EHLO_duplicate(self): + self.write_line(b'HELO example') + self.write_line(b'EHLO example') + self.assertEqual(self.channel.socket.last, + b'503 Duplicate HELO/EHLO\r\n') + + def test_HELP(self): + self.write_line(b'HELP') + self.assertEqual(self.channel.socket.last, + b'250 Supported commands: EHLO HELO MAIL RCPT ' + \ + b'DATA RSET NOOP QUIT VRFY\r\n') + + def test_HELP_command(self): + self.write_line(b'HELP MAIL') + self.assertEqual(self.channel.socket.last, + b'250 Syntax: MAIL FROM: <address>\r\n') + + def test_HELP_command_unknown(self): + self.write_line(b'HELP SPAM') + self.assertEqual(self.channel.socket.last, + b'501 Supported commands: EHLO HELO MAIL RCPT ' + \ + b'DATA RSET NOOP QUIT VRFY\r\n') + def test_HELO_bad_syntax(self): self.write_line(b'HELO') self.assertEqual(self.channel.socket.last, b'501 Syntax: HELO hostname\r\n') def test_HELO_duplicate(self): - self.write_line(b'HELO test.example') - self.write_line(b'HELO test.example') + self.write_line(b'HELO example') + self.write_line(b'HELO example') self.assertEqual(self.channel.socket.last, b'503 Duplicate HELO/EHLO\r\n') + def test_HELO_parameter_rejected_when_extensions_not_enabled(self): + self.extended_smtp = False + self.write_line(b'HELO example') + self.write_line(b'MAIL from:<foo@example.com> SIZE=1234') + self.assertEqual(self.channel.socket.last, + b'501 Syntax: MAIL FROM: <address>\r\n') + + def test_MAIL_allows_space_after_colon(self): + self.write_line(b'HELO example') + self.write_line(b'MAIL from: <foo@example.com>') + self.assertEqual(self.channel.socket.last, + b'250 OK\r\n') + + def test_extended_MAIL_allows_space_after_colon(self): + self.write_line(b'EHLO example') + self.write_line(b'MAIL from: <foo@example.com> size=20') + self.assertEqual(self.channel.socket.last, + b'250 OK\r\n') + def test_NOOP(self): self.write_line(b'NOOP') - self.assertEqual(self.channel.socket.last, b'250 Ok\r\n') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') def test_HELO_NOOP(self): self.write_line(b'HELO example') self.write_line(b'NOOP') - self.assertEqual(self.channel.socket.last, b'250 Ok\r\n') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') def test_NOOP_bad_syntax(self): self.write_line(b'NOOP hi') @@ -136,15 +194,29 @@ class SMTPDChannelTest(TestCase): def test_command_too_long(self): self.write_line(b'HELO example') - self.write_line(b'MAIL from ' + + self.write_line(b'MAIL from: ' + b'a' * self.channel.command_size_limit + b'@example') self.assertEqual(self.channel.socket.last, b'500 Error: line too long\r\n') - def test_data_too_long(self): - # Small hack. Setting limit to 2K octets here will save us some time. - self.channel.data_size_limit = 2048 + def test_MAIL_command_limit_extended_with_SIZE(self): + self.write_line(b'EHLO example') + fill_len = self.channel.command_size_limit - len('MAIL from:<@example>') + self.write_line(b'MAIL from:<' + + b'a' * fill_len + + b'@example> SIZE=1234') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') + + self.write_line(b'MAIL from:<' + + b'a' * (fill_len + 26) + + b'@example> SIZE=1234') + self.assertEqual(self.channel.socket.last, + b'500 Error: line too long\r\n') + + def test_data_longer_than_default_data_size_limit(self): + # Hack the default so we don't have to generate so much data. + self.channel.data_size_limit = 1048 self.write_line(b'HELO example') self.write_line(b'MAIL From:eggs@example') self.write_line(b'RCPT To:spam@example') @@ -154,28 +226,93 @@ class SMTPDChannelTest(TestCase): self.assertEqual(self.channel.socket.last, b'552 Error: Too much mail data\r\n') + def test_MAIL_size_parameter(self): + self.write_line(b'EHLO example') + self.write_line(b'MAIL FROM:<eggs@example> SIZE=512') + self.assertEqual(self.channel.socket.last, + b'250 OK\r\n') + + def test_MAIL_invalid_size_parameter(self): + self.write_line(b'EHLO example') + self.write_line(b'MAIL FROM:<eggs@example> SIZE=invalid') + self.assertEqual(self.channel.socket.last, + b'501 Syntax: MAIL FROM: <address> [SP <mail-parameters>]\r\n') + + def test_MAIL_RCPT_unknown_parameters(self): + self.write_line(b'EHLO example') + self.write_line(b'MAIL FROM:<eggs@example> ham=green') + self.assertEqual(self.channel.socket.last, + b'555 MAIL FROM parameters not recognized or not implemented\r\n') + + self.write_line(b'MAIL FROM:<eggs@example>') + self.write_line(b'RCPT TO:<eggs@example> ham=green') + self.assertEqual(self.channel.socket.last, + b'555 RCPT TO parameters not recognized or not implemented\r\n') + + def test_MAIL_size_parameter_larger_than_default_data_size_limit(self): + self.channel.data_size_limit = 1048 + self.write_line(b'EHLO example') + self.write_line(b'MAIL FROM:<eggs@example> SIZE=2096') + self.assertEqual(self.channel.socket.last, + b'552 Error: message size exceeds fixed maximum message size\r\n') + def test_need_MAIL(self): self.write_line(b'HELO example') self.write_line(b'RCPT to:spam@example') self.assertEqual(self.channel.socket.last, b'503 Error: need MAIL command\r\n') - def test_MAIL_syntax(self): + def test_MAIL_syntax_HELO(self): self.write_line(b'HELO example') self.write_line(b'MAIL from eggs@example') self.assertEqual(self.channel.socket.last, - b'501 Syntax: MAIL FROM:<address>\r\n') + b'501 Syntax: MAIL FROM: <address>\r\n') - def test_MAIL_missing_from(self): + def test_MAIL_syntax_EHLO(self): + self.write_line(b'EHLO example') + self.write_line(b'MAIL from eggs@example') + self.assertEqual(self.channel.socket.last, + b'501 Syntax: MAIL FROM: <address> [SP <mail-parameters>]\r\n') + + def test_MAIL_missing_address(self): self.write_line(b'HELO example') self.write_line(b'MAIL from:') self.assertEqual(self.channel.socket.last, - b'501 Syntax: MAIL FROM:<address>\r\n') + b'501 Syntax: MAIL FROM: <address>\r\n') def test_MAIL_chevrons(self): self.write_line(b'HELO example') self.write_line(b'MAIL from:<eggs@example>') - self.assertEqual(self.channel.socket.last, b'250 Ok\r\n') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') + + def test_MAIL_empty_chevrons(self): + self.write_line(b'EHLO example') + self.write_line(b'MAIL from:<>') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') + + def test_MAIL_quoted_localpart(self): + self.write_line(b'EHLO example') + self.write_line(b'MAIL from: <"Fred Blogs"@example.com>') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') + self.assertEqual(self.channel.mailfrom, '"Fred Blogs"@example.com') + + def test_MAIL_quoted_localpart_no_angles(self): + self.write_line(b'EHLO example') + self.write_line(b'MAIL from: "Fred Blogs"@example.com') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') + self.assertEqual(self.channel.mailfrom, '"Fred Blogs"@example.com') + + def test_MAIL_quoted_localpart_with_size(self): + self.write_line(b'EHLO example') + self.write_line(b'MAIL from: <"Fred Blogs"@example.com> SIZE=1000') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') + self.assertEqual(self.channel.mailfrom, '"Fred Blogs"@example.com') + + def test_MAIL_quoted_localpart_with_size_no_angles(self): + self.write_line(b'EHLO example') + self.write_line(b'MAIL from: "Fred Blogs"@example.com SIZE=1000') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') + self.assertEqual(self.channel.mailfrom, '"Fred Blogs"@example.com') def test_nested_MAIL(self): self.write_line(b'HELO example') @@ -184,6 +321,22 @@ class SMTPDChannelTest(TestCase): self.assertEqual(self.channel.socket.last, b'503 Error: nested MAIL command\r\n') + def test_VRFY(self): + self.write_line(b'VRFY eggs@example') + self.assertEqual(self.channel.socket.last, + b'252 Cannot VRFY user, but will accept message and attempt ' + \ + b'delivery\r\n') + + def test_VRFY_syntax(self): + self.write_line(b'VRFY') + self.assertEqual(self.channel.socket.last, + b'501 Syntax: VRFY <address>\r\n') + + def test_EXPN_not_implemented(self): + self.write_line(b'EXPN') + self.assertEqual(self.channel.socket.last, + b'502 EXPN not implemented\r\n') + def test_no_HELO_MAIL(self): self.write_line(b'MAIL from:<foo@example.com>') self.assertEqual(self.channel.socket.last, @@ -196,13 +349,26 @@ class SMTPDChannelTest(TestCase): self.assertEqual(self.channel.socket.last, b'503 Error: need RCPT command\r\n') - def test_RCPT_syntax(self): + def test_RCPT_syntax_HELO(self): self.write_line(b'HELO example') - self.write_line(b'MAIL From:eggs@example') + self.write_line(b'MAIL From: eggs@example') self.write_line(b'RCPT to eggs@example') self.assertEqual(self.channel.socket.last, b'501 Syntax: RCPT TO: <address>\r\n') + def test_RCPT_syntax_EHLO(self): + self.write_line(b'EHLO example') + self.write_line(b'MAIL From: eggs@example') + self.write_line(b'RCPT to eggs@example') + self.assertEqual(self.channel.socket.last, + b'501 Syntax: RCPT TO: <address> [SP <mail-parameters>]\r\n') + + def test_RCPT_lowercase_to_OK(self): + self.write_line(b'HELO example') + self.write_line(b'MAIL From: eggs@example') + self.write_line(b'RCPT to: <eggs@example>') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') + def test_no_HELO_RCPT(self): self.write_line(b'RCPT to eggs@example') self.assertEqual(self.channel.socket.last, @@ -211,15 +377,15 @@ class SMTPDChannelTest(TestCase): def test_data_dialog(self): self.write_line(b'HELO example') self.write_line(b'MAIL From:eggs@example') - self.assertEqual(self.channel.socket.last, b'250 Ok\r\n') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') self.write_line(b'RCPT To:spam@example') - self.assertEqual(self.channel.socket.last, b'250 Ok\r\n') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') self.write_line(b'DATA') self.assertEqual(self.channel.socket.last, b'354 End data with <CR><LF>.<CR><LF>\r\n') self.write_line(b'data\r\nmore\r\n.') - self.assertEqual(self.channel.socket.last, b'250 Ok\r\n') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') self.assertEqual(self.server.messages, [('peer', 'eggs@example', ['spam@example'], 'data\nmore')]) @@ -267,7 +433,7 @@ class SMTPDChannelTest(TestCase): self.write_line(b'MAIL From:eggs@example') self.write_line(b'RCPT To:spam@example') self.write_line(b'RSET') - self.assertEqual(self.channel.socket.last, b'250 Ok\r\n') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') self.write_line(b'MAIL From:foo@example') self.write_line(b'RCPT To:eggs@example') self.write_line(b'DATA') @@ -278,12 +444,18 @@ class SMTPDChannelTest(TestCase): def test_HELO_RSET(self): self.write_line(b'HELO example') self.write_line(b'RSET') - self.assertEqual(self.channel.socket.last, b'250 Ok\r\n') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') def test_RSET_syntax(self): self.write_line(b'RSET hi') self.assertEqual(self.channel.socket.last, b'501 Syntax: RSET\r\n') + def test_unknown_command(self): + self.write_line(b'UNKNOWN_CMD') + self.assertEqual(self.channel.socket.last, + b'500 Error: command "UNKNOWN_CMD" not ' + \ + b'recognized\r\n') + def test_attribute_deprecations(self): with support.check_warnings(('', DeprecationWarning)): spam = self.channel._SMTPChannel__server @@ -330,8 +502,54 @@ class SMTPDChannelTest(TestCase): with support.check_warnings(('', DeprecationWarning)): self.channel._SMTPChannel__addr = 'spam' -def test_main(): - support.run_unittest(SMTPDServerTest, SMTPDChannelTest) + +class SMTPDChannelWithDataSizeLimitTest(unittest.TestCase): + + def setUp(self): + smtpd.socket = asyncore.socket = mock_socket + self.debug = smtpd.DEBUGSTREAM = io.StringIO() + self.server = DummyServer('a', 'b') + conn, addr = self.server.accept() + # Set DATA size limit to 32 bytes for easy testing + self.channel = smtpd.SMTPChannel(self.server, conn, addr, 32) + + def tearDown(self): + asyncore.close_all() + asyncore.socket = smtpd.socket = socket + + def write_line(self, line): + self.channel.socket.queue_recv(line) + self.channel.handle_read() + + def test_data_limit_dialog(self): + self.write_line(b'HELO example') + self.write_line(b'MAIL From:eggs@example') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') + self.write_line(b'RCPT To:spam@example') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') + + self.write_line(b'DATA') + self.assertEqual(self.channel.socket.last, + b'354 End data with <CR><LF>.<CR><LF>\r\n') + self.write_line(b'data\r\nmore\r\n.') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') + self.assertEqual(self.server.messages, + [('peer', 'eggs@example', ['spam@example'], 'data\nmore')]) + + def test_data_limit_dialog_too_much_data(self): + self.write_line(b'HELO example') + self.write_line(b'MAIL From:eggs@example') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') + self.write_line(b'RCPT To:spam@example') + self.assertEqual(self.channel.socket.last, b'250 OK\r\n') + + self.write_line(b'DATA') + self.assertEqual(self.channel.socket.last, + b'354 End data with <CR><LF>.<CR><LF>\r\n') + self.write_line(b'This message is longer than 32 bytes\r\n.') + self.assertEqual(self.channel.socket.last, + b'552 Error: Too much mail data\r\n') + if __name__ == "__main__": - test_main() + unittest.main() diff --git a/Lib/test/test_smtplib.py b/Lib/test/test_smtplib.py index 18dde2f..befc49e 100644 --- a/Lib/test/test_smtplib.py +++ b/Lib/test/test_smtplib.py @@ -229,13 +229,13 @@ class DebuggingServerTests(unittest.TestCase): def testNOOP(self): smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3) - expected = (250, b'Ok') + expected = (250, b'OK') self.assertEqual(smtp.noop(), expected) smtp.quit() def testRSET(self): smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3) - expected = (250, b'Ok') + expected = (250, b'OK') self.assertEqual(smtp.rset(), expected) smtp.quit() @@ -246,10 +246,18 @@ class DebuggingServerTests(unittest.TestCase): self.assertEqual(smtp.ehlo(), expected) smtp.quit() + def testNotImplemented(self): + # EXPN isn't implemented in DebuggingServer + smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3) + expected = (502, b'EXPN not implemented') + smtp.putcmd('EXPN') + self.assertEqual(smtp.getreply(), expected) + smtp.quit() + def testVRFY(self): - # VRFY isn't implemented in DebuggingServer smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3) - expected = (502, b'Error: command "VRFY" not implemented') + expected = (252, b'Cannot VRFY user, but will accept message ' + \ + b'and attempt delivery') self.assertEqual(smtp.vrfy('nobody@nowhere.com'), expected) self.assertEqual(smtp.verify('nobody@nowhere.com'), expected) smtp.quit() @@ -265,7 +273,8 @@ class DebuggingServerTests(unittest.TestCase): def testHELP(self): smtp = smtplib.SMTP(HOST, self.port, local_hostname='localhost', timeout=3) - self.assertEqual(smtp.help(), b'Error: command "HELP" not implemented') + self.assertEqual(smtp.help(), b'Supported commands: EHLO HELO MAIL ' + \ + b'RCPT DATA RSET NOOP QUIT VRFY') smtp.quit() def testSend(self): |