summaryrefslogtreecommitdiffstats
path: root/Lib
diff options
context:
space:
mode:
authorVinay Sajip <vinay_sajip@yahoo.co.uk>2012-05-26 19:39:27 (GMT)
committerVinay Sajip <vinay_sajip@yahoo.co.uk>2012-05-26 19:39:27 (GMT)
commitee2bbed9252fe286bc14765992877e1502b70328 (patch)
tree6d562b8297c5d7a21c52b4c3cfad6266d79ba630 /Lib
parent42211426eb9771424b9e5153d12f8c2c0c538d34 (diff)
parentd1a30c939cc6378423dd3cc22382a9abe2a7d882 (diff)
downloadcpython-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.py12
-rw-r--r--Lib/importlib/_bootstrap.py2
-rw-r--r--Lib/ipaddress.py136
-rwxr-xr-xLib/smtpd.py245
-rw-r--r--Lib/test/test_email/test__header_value_parser.py15
-rw-r--r--Lib/test/test_ipaddress.py40
-rw-r--r--Lib/test/test_logging.py3
-rw-r--r--Lib/test/test_smtpd.py280
-rw-r--r--Lib/test/test_smtplib.py19
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):