[Python-checkins] cpython (merge default -> default): Merged upstream changes.
vinay.sajip
python-checkins at python.org
Sat May 26 21:40:26 CEST 2012
http://hg.python.org/cpython/rev/1b5bdef96af0
changeset: 77168:1b5bdef96af0
parent: 77167:564caa2c61ae
parent: 77166:ec7456b3f4fe
user: Vinay Sajip <vinay_sajip at yahoo.co.uk>
date: Sat May 26 20:39:27 2012 +0100
summary:
Merged upstream changes.
files:
Doc/howto/index.rst | 1 +
Doc/howto/ipaddress.rst | 291 ++++++++++
Doc/library/smtpd.rst | 20 +-
Lib/email/_header_value_parser.py | 12 +-
Lib/importlib/_bootstrap.py | 2 -
Lib/ipaddress.py | 136 +---
Lib/smtpd.py | 245 ++++++-
Lib/test/test_email/test__header_value_parser.py | 15 +-
Lib/test/test_ipaddress.py | 38 +-
Lib/test/test_logging.py | 3 +
Lib/test/test_smtpd.py | 280 ++++++++-
Lib/test/test_smtplib.py | 19 +-
Misc/ACKS | 3 +
Misc/NEWS | 3 +
Python/importlib.h | Bin
15 files changed, 844 insertions(+), 224 deletions(-)
diff --git a/Doc/howto/index.rst b/Doc/howto/index.rst
--- a/Doc/howto/index.rst
+++ b/Doc/howto/index.rst
@@ -28,4 +28,5 @@
urllib2.rst
webservers.rst
argparse.rst
+ ipaddress.rst
diff --git a/Doc/howto/ipaddress.rst b/Doc/howto/ipaddress.rst
new file mode 100644
--- /dev/null
+++ b/Doc/howto/ipaddress.rst
@@ -0,0 +1,291 @@
+.. _ipaddress-howto:
+
+***************
+Ipaddress Howto
+***************
+
+:author: Peter Moody
+
+.. topic:: Abstract
+
+ This document is a gentle introduction to :mod:`ipaddress` module.
+
+
+Creating Address/Network/Interface objects
+==========================================
+
+Since :mod:`ipaddress` is a module for inspecting and manipulating IP address,
+the first thing you'll want to do is create some objects. You can use
+:mod:`ipaddress` to create objects from strings and integers.
+
+
+A Note on IP Versions
+---------------------
+
+For readers that aren't particularly familiar with IP addressing, it's
+important to know that the Internet Protocol is currently in the process
+of moving from version 4 of the protocol to version 6. This transition is
+occurring largely because version 4 of the protocol doesn't provide enough
+addresses to handle the needs of the whole world, especially given the
+increasing number of devices with direct connections to the internet.
+
+Explaining the details of the differences between the two versions of the
+protocol is beyond the scope of this introduction, but readers need to at
+least be aware that these two versions exist, and it will sometimes be
+necessary to force the use of one version or the other.
+
+
+IP Host Addresses
+-----------------
+
+Addresses, often referred to as "host addresses" are the most basic unit
+when working with IP addressing. The simplest way to create addresses is
+to use the ``ip_address`` factory function, which automatically determines
+whether to create an IPv4 or IPv6 address based on the passed in value::
+
+ >>> ipaddress.ip_address('192.0.2.1')
+ IPv4Address('192.0.2.1')
+ >>> ipaddress.ip_address('2001:DB8::1')
+ IPv6Address('2001:db8::1')
+
+Addresses can also be created directly from integers. Values that will
+fit within 32 bits are assumed to be IPv4 addresses::
+
+ >>> ipaddress.ip_address(3221225985)
+ IPv4Address('192.0.2.1')
+ >>> ipaddress.ip_address(42540766411282592856903984951653826561)
+ IPv6Address('2001:db8::1')
+
+To force the use of IPv4 or IPv6 addresses, the relevant classes can be
+invoked directly. This is particularly useful to force creation of IPv6
+addresses for small integers::
+
+ >>> ipaddress.ip_address(1)
+ IPv4Address('0.0.0.1')
+ >>> ipaddress.IPv4Address(1)
+ IPv4Address('0.0.0.1')
+ >>> ipaddress.IPv6Address(1)
+ IPv6Address('::1')
+
+
+Defining Networks
+-----------------
+
+Host addresses are usually grouped together into IP networks, so
+:mod:`ipaddress` provides a way to create, inspect and manipulate network
+definitions. IP network objects are constructed from strings that define the
+range of host addresses that are part of that network. The simplest form
+for that information is a "network address/network prefix" pair, where the
+prefix defines the number of leading bits that are compared to determine
+whether or not an address is part of the network and the network address
+defines the expected value of those bits.
+
+As for addresses, a factory function is provided that determines the correct
+IP version automatically::
+
+ >>> ipaddress.ip_network('192.0.2.0/24')
+ IPv4Network('192.0.2.0/24')
+ >>> ipaddress.ip_network('2001:db8::0/96')
+ IPv6Network('2001:db8::/96')
+
+Network objects cannot have any host bits set. The practical effect of this
+is that ``192.0.2.1/24`` does not describe a network. Such definitions are
+referred to as interface objects since the ip-on-a-network notation is
+commonly used to describe network interfaces of a computer on a given network
+and are described further in the next section.
+
+By default, attempting to create a network object with host bits set will
+result in :exc:`ValueError` being raised. To request that the
+additional bits instead be coerced to zero, the flag ``strict=False`` can
+be passed to the constructor::
+
+ >>> ipaddress.ip_network('192.0.2.1/24')
+ Traceback (most recent call last):
+ ...
+ ValueError: 192.0.2.1/24 has host bits set
+ >>> ipaddress.ip_network('192.0.2.1/24', strict=False)
+ IPv4Network('192.0.2.0/24')
+
+While the string form offers significantly more flexibility, networks can
+also be defined with integers, just like host addresses. In this case, the
+network is considered to contain only the single address identified by the
+integer, so the network prefix includes the entire network address::
+
+ >>> ipaddress.ip_network(3221225984)
+ IPv4Network('192.0.2.0/32')
+ >>> ipaddress.ip_network(42540766411282592856903984951653826560L)
+ IPv6Network('2001:db8::/128')
+
+Creation of a particular kind of network can be forced by calling the
+class constructor directly instead of using the factory function.
+
+
+Host Interfaces
+---------------
+
+As mentioned just above, if you need to describe an address on a particular
+network, neither the address nor the network classes are sufficient.
+Notation like ``192.0.2.1/24`` is commonly used network engineers and the
+people who write tools for firewalls and routers as shorthand for "the host
+``192.0.2.1`` on the network ``192.0.2.0/24``", Accordingly, :mod:`ipaddress`
+provides a set of hybrid classes that associate an address with a particular
+network. The interface for creation is identical to that for defining network
+objects, except that the address portion isn't constrained to being a network
+address.
+
+ >>> ipaddress.ip_interface('192.0.2.1/24')
+ IPv4Interface('192.0.2.1/24')
+ >>> ipaddress.ip_network('2001:db8::1/96')
+ IPv6Interface('2001:db8::1/96')
+
+Integer inputs are accepted (as with networks), and use of a particular IP
+version can be forced by calling the relevant constructor directly.
+
+
+Inspecting Address/Network/Interface Objects
+============================================
+
+You've gone to the trouble of creating an IPv(4|6)(Address|Network|Interface)
+object, so you probably want to get information about it. :mod:`ipaddress`
+tries to make doing this easy and intuitive.
+
+Extracting the IP version::
+
+ >>> addr4 = ipaddress.ip_address('192.0.2.1')
+ >>> addr6 = ipaddress.ip_address('2001:db8::1')
+ >>> addr6.version
+ 6
+ >>> addr4.version
+ 4
+
+Obtaining the network from an interface::
+
+ >>> host4 = ipaddress.ip_interface('192.0.2.1/24')
+ >>> host4.network
+ IPv4Network('192.0.2.0/24')
+ >>> host6 = ipaddress.ip_interface('2001:db8::1/96')
+ >>> host6.network
+ IPv6Network('2001:db8::/96')
+
+Finding out how many individual addresses are in a network::
+
+ >>> net4 = ipaddress.ip_network('192.0.2.0/24')
+ >>> net4.numhosts
+ 256
+ >>> net6 = ipaddress.ip_network('2001:db8::0/96')
+ >>> net6.numhosts
+ 4294967296
+
+Iterating through the 'usable' addresses on a network::
+
+ >>> net4 = ipaddress.ip_network('192.0.2.0/24')
+ >>> for x in net4.iterhosts():
+ print(x)
+ 192.0.2.1
+ 192.0.2.2
+ 192.0.2.3
+ 192.0.2.4
+ <snip>
+ 192.0.2.252
+ 192.0.2.253
+ 192.0.2.254
+
+
+Obtaining the netmask (i.e. set bits corresponding to the network prefix) or
+the hostmask (any bits that are not part of the netmask):
+
+ >>> net4 = ipaddress.ip_network('192.0.2.0/24')
+ >>> net4.netmask
+ IPv4Address('255.255.255.0')
+ >>> net4.hostmask
+ IPv4Address('0.0.0.255')
+ >>> net6 = ipaddress.ip_network('2001:db8::0/96')
+ >>> net6.netmask
+ IPv6Address('ffff:ffff:ffff:ffff:ffff:ffff::')
+ >>> net6.hostmask
+ IPv6Address('::ffff:ffff')
+
+
+Exploding or compressing the address::
+
+ >>> net6.exploded
+ '2001:0000:0000:0000:0000:0000:0000:0000/96'
+ >>> addr6.exploded
+ '2001:0000:0000:0000:0000:0000:0000:0001'
+
+
+Networks as lists of Addresses
+==============================
+
+It's sometimes useful to treat networks as lists. This means it is possible
+to index them like this::
+
+ >>> net4[1]
+ IPv4Address('192.0.2.1')
+ >>> net4[-1]
+ IPv4Address('192.0.2.255')
+ >>> net6[1]
+ IPv6Address('2001::1')
+ >>> net6[-1]
+ IPv6Address('2001::ffff:ffff')
+
+
+It also means that network objects lend themselves to using the list
+membership test syntax like this::
+
+ if address in network:
+ # do something
+
+Containment testing is done efficiently based on the network prefix::
+
+ >>> addr4 = ipaddress.ip_address('192.0.2.1')
+ >>> addr4 in ipaddress.ip_network('192.0.2.0/24')
+ True
+ >>> addr4 in ipaddress.ip_network('192.0.3.0/24')
+ False
+
+
+Comparisons
+===========
+
+:mod:`ipaddress` provides some simple, hopefully intuitive ways to compare
+objects, where it makes sense::
+
+ >>> ipaddress.ip_address('192.0.2.1') < ipaddress.ip_address('192.0.2.2')
+ True
+
+A :exc:`TypeError` exception is raised if you try to compare objects of
+different versions or different types.
+
+
+Using IP Addresses with other modules
+=====================================
+
+Other modules that use IP addresses (such as :mod:`socket`) usually won't
+accept objects from this module directly. Instead, they must be coerced to
+an integer or string that the other module will accept::
+
+ >>> addr4 = ipaddress.ip_address('192.0.2.1')
+ >>> str(addr4)
+ '192.0.2.1'
+ >>> int(addr4)
+ 3221225985
+
+
+Exceptions raised by :mod:`ipaddress`
+=====================================
+
+If you try to create an address/network/interface object with an invalid value
+for either the address or netmask, :mod:`ipaddress` will raise an
+:exc:`AddressValueError` or :exc:`NetmaskValueError` respectively. However,
+this applies only when calling the class constructors directly. The factory
+functions and other module level functions will just raise :exc:`ValueError`.
+
+Both of the module specific exceptions have :exc:`ValueError` as their
+parent class, so if you're not concerned with the particular type of error,
+you can still do the following::
+
+ try:
+ ipaddress.IPv4Address(address)
+ except ValueError:
+ print 'address/netmask is invalid: %s' % address
diff --git a/Doc/library/smtpd.rst b/Doc/library/smtpd.rst
--- a/Doc/library/smtpd.rst
+++ b/Doc/library/smtpd.rst
@@ -20,17 +20,24 @@
Additionally the SMTPChannel may be extended to implement very specific
interaction behaviour with SMTP clients.
+The code supports :RFC:`5321`, plus the :rfc:`1870` SIZE extension.
+
+
SMTPServer Objects
------------------
-.. class:: SMTPServer(localaddr, remoteaddr)
+.. class:: SMTPServer(localaddr, remoteaddr, data_size_limit=33554432)
Create a new :class:`SMTPServer` object, which binds to local address
*localaddr*. It will treat *remoteaddr* as an upstream SMTP relayer. It
inherits from :class:`asyncore.dispatcher`, and so will insert itself into
:mod:`asyncore`'s event loop on instantiation.
+ *data_size_limit* specifies the maximum number of bytes that will be
+ accepted in a ``DATA`` command. A value of ``None`` or ``0`` means no
+ limit.
+
.. method:: process_message(peer, mailfrom, rcpttos, data)
Raise :exc:`NotImplementedError` exception. Override this in subclasses to
@@ -155,11 +162,15 @@
Command Action taken
======== ===================================================================
HELO Accepts the greeting from the client and stores it in
- :attr:`seen_greeting`.
+ :attr:`seen_greeting`. Sets server to base command mode.
+ EHLO Accepts the greeting from the client and stores it in
+ :attr:`seen_greeting`. Sets server to extended command mode.
NOOP Takes no action.
QUIT Closes the connection cleanly.
MAIL Accepts the "MAIL FROM:" syntax and stores the supplied address as
- :attr:`mailfrom`.
+ :attr:`mailfrom`. In extended command mode, accepts the
+ :rfc:`1870` SIZE attribute and responds appropriately based on the
+ value of ``data_size_limit``.
RCPT Accepts the "RCPT TO:" syntax and stores the supplied addresses in
the :attr:`rcpttos` list.
RSET Resets the :attr:`mailfrom`, :attr:`rcpttos`, and
@@ -167,4 +178,7 @@
DATA Sets the internal state to :attr:`DATA` and stores remaining lines
from the client in :attr:`received_data` until the terminator
"\r\n.\r\n" is received.
+ HELP Returns minimal information on command syntax
+ VRFY Returns code 252 (the server doesn't know if the address is valid)
+ EXPN Reports that the command is not implemented.
======== ===================================================================
diff --git a/Lib/email/_header_value_parser.py b/Lib/email/_header_value_parser.py
--- a/Lib/email/_header_value_parser.py
+++ b/Lib/email/_header_value_parser.py
@@ -791,6 +791,8 @@
for x in self:
if x.token_type == 'addr-spec':
return x.addr_spec
+ else:
+ return '<>'
class ObsRoute(TokenList):
@@ -1829,6 +1831,14 @@
"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 @@
"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
--- a/Lib/importlib/_bootstrap.py
+++ b/Lib/importlib/_bootstrap.py
@@ -949,8 +949,6 @@
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
--- 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 @@
"""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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 _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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
'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 @@
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 @@
'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 @@
.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 @@
"""
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 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
--- 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 @@
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 @@
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 @@
#
# - support mailbox delivery
# - alias files
-# - ESMTP
+# - Handle more ESMTP extensions
# - handle error codes from the backend smtpd
import sys
@@ -77,12 +81,14 @@
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 @@
NEWLINE = '\n'
EMPTYSTRING = ''
COMMASPACE = ', '
+DATA_SIZE_DEFAULT = 33554432
-
def usage(code, msg=''):
print(__doc__ % globals(), file=sys.stderr)
if msg:
@@ -104,19 +110,23 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 @@
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 at 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('501 Syntax: MAIL FROM:<address>')
+ self.push(syntaxerr)
+ return
+ if not self.extended_smtp and params:
+ self.push(syntaxerr)
+ return
+ if not 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 @@
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 @@
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 @@
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 @@
raise NotImplementedError
-
class DebuggingServer(SMTPServer):
# Do something with the gathered message
def process_message(self, peer, mailfrom, rcpttos, data):
@@ -503,7 +640,6 @@
print('------------ END MESSAGE ------------')
-
class PureProxy(SMTPServer):
def process_message(self, peer, mailfrom, rcpttos, data):
lines = data.split('\n')
@@ -544,7 +680,6 @@
return refused
-
class MailmanProxy(PureProxy):
def process_message(self, peer, mailfrom, rcpttos, data):
from io import StringIO
@@ -623,19 +758,18 @@
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 @@
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 @@
return options
-
if __name__ == '__main__':
options = parseargs()
# Become nobody
@@ -699,7 +839,8 @@
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
--- a/Lib/test/test_email/test__header_value_parser.py
+++ b/Lib/test/test_email/test__header_value_parser.py
@@ -1429,6 +1429,19 @@
self.assertIsNone(angle_addr.route)
self.assertEqual(angle_addr.addr_spec, 'dinsdale at 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 at example.com>(bar)',
@@ -2007,7 +2020,7 @@
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
--- 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 @@
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 @@
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 @@
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 @@
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
--- a/Lib/test/test_logging.py
+++ b/Lib/test/test_logging.py
@@ -663,6 +663,7 @@
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 @@
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 @@
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
--- 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 @@
raise DummyDispatcherBroken()
-class SMTPDServerTest(TestCase):
+class SMTPDServerTest(unittest.TestCase):
def setUp(self):
smtpd.socket = asyncore.socket = mock_socket
@@ -39,7 +39,7 @@
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 at example')
write_line(b'RCPT To:spam at example')
write_line(b'DATA')
@@ -50,7 +50,7 @@
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 @@
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'502 Error: command "EHLO" not implemented\r\n')
+ 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'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 at 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 at 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 at 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 @@
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 at example')
self.write_line(b'RCPT To:spam at example')
@@ -154,28 +226,93 @@
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 at 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 at 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 at 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 at example>')
+ self.write_line(b'RCPT TO:<eggs at 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 at 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 at 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 at 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 at 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 at 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 @@
self.assertEqual(self.channel.socket.last,
b'503 Error: nested MAIL command\r\n')
+ def test_VRFY(self):
+ self.write_line(b'VRFY eggs at 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 at example.com>')
self.assertEqual(self.channel.socket.last,
@@ -196,13 +349,26 @@
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 at example')
+ self.write_line(b'MAIL From: eggs at example')
self.write_line(b'RCPT to eggs at 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 at example')
+ self.write_line(b'RCPT to eggs at 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 at example')
+ self.write_line(b'RCPT to: <eggs at 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 at example')
self.assertEqual(self.channel.socket.last,
@@ -211,15 +377,15 @@
def test_data_dialog(self):
self.write_line(b'HELO example')
self.write_line(b'MAIL From:eggs at 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 at 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 at example', ['spam at example'], 'data\nmore')])
@@ -267,7 +433,7 @@
self.write_line(b'MAIL From:eggs at example')
self.write_line(b'RCPT To:spam at 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 at example')
self.write_line(b'RCPT To:eggs at example')
self.write_line(b'DATA')
@@ -278,12 +444,18 @@
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 @@
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 at example')
+ self.assertEqual(self.channel.socket.last, b'250 OK\r\n')
+ self.write_line(b'RCPT To:spam at 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 at example', ['spam at 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 at example')
+ self.assertEqual(self.channel.socket.last, b'250 OK\r\n')
+ self.write_line(b'RCPT To:spam at 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
--- a/Lib/test/test_smtplib.py
+++ b/Lib/test/test_smtplib.py
@@ -229,13 +229,13 @@
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 @@
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 at nowhere.com'), expected)
self.assertEqual(smtp.verify('nobody at nowhere.com'), expected)
smtp.quit()
@@ -265,7 +273,8 @@
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):
diff --git a/Misc/ACKS b/Misc/ACKS
--- a/Misc/ACKS
+++ b/Misc/ACKS
@@ -112,6 +112,7 @@
Matias Bordese
Jurjen Bos
Peter Bosch
+Dan Boswell
Eric Bouck
Thierry Bousch
Sebastian Boving
@@ -494,6 +495,7 @@
Jack Jansen
Bill Janssen
Thomas Jarosch
+Juhana Jauhiainen
Zbigniew Jędrzejewski-Szmek
Julien Jehannet
Drew Jenkins
@@ -1039,6 +1041,7 @@
Richard Townsend
David Townshend
Laurence Tratt
+Alberto Trevino
Matthias Troffaes
John Tromp
Jason Trowbridge
diff --git a/Misc/NEWS b/Misc/NEWS
--- a/Misc/NEWS
+++ b/Misc/NEWS
@@ -46,6 +46,9 @@
Library
-------
+- Issue #8739: Updated smtpd to support RFC 5321, and added support for the
+ RFC 1870 SIZE extension.
+
- Issue #665194: Added a localtime function to email.utils to provide an
aware local datetime for use in setting Date headers.
diff --git a/Python/importlib.h b/Python/importlib.h
index bdb3644db479da7d7f6b949be135c6b1c3d1fc33..cd8fbebb9ff7c4a671320695174aa8953db75845
GIT binary patch
[stripped]
--
Repository URL: http://hg.python.org/cpython
More information about the Python-checkins
mailing list