[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