[Python-checkins] python/dist/src/Lib/email _parseaddr.py,NONE,1.5.2.1 Charset.py,1.7.2.3,1.7.2.4 Generator.py,1.6.10.3,1.6.10.4 Header.py,1.13.2.2,1.13.2.3 MIMEText.py,1.3.10.1,1.3.10.2 Message.py,1.9.6.2,1.9.6.3 Parser.py,1.5.10.3,1.5.10.4 Utils.py,1.9.6.1,1.9.6.2 __init__.py,1.4.10.4,1.4.10.5 _compat21.py,1.4.2.1,1.4.2.2 _compat22.py,1.4.2.1,1.4.2.2 base64MIME.py,1.5.2.1,1.5.2.2 quopriMIME.py,1.4.2.1,1.4.2.2

bwarsaw@users.sourceforge.net bwarsaw@users.sourceforge.net
Fri, 21 Mar 2003 13:09:34 -0800


Update of /cvsroot/python/python/dist/src/Lib/email
In directory sc8-pr-cvs1:/tmp/cvs-serv27730/Lib/email

Modified Files:
      Tag: release22-maint
	Charset.py Generator.py Header.py MIMEText.py Message.py 
	Parser.py Utils.py __init__.py _compat21.py _compat22.py 
	base64MIME.py quopriMIME.py 
Added Files:
      Tag: release22-maint
	_parseaddr.py 
Log Message:
Backporting email 2.5 to Python 2.2 maint branch.


--- NEW FILE: _parseaddr.py ---
# Copyright (C) 2002 Python Software Foundation

"""Email address parsing code.

Lifted directly from rfc822.py.  This should eventually be rewritten.
"""

import time
from types import TupleType

try:
    True, False
except NameError:
    True = 1
    False = 0

SPACE = ' '
EMPTYSTRING = ''
COMMASPACE = ', '

# Parse a date field
_monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun', 'jul',
               'aug', 'sep', 'oct', 'nov', 'dec',
               'january', 'february', 'march', 'april', 'may', 'june', 'july',
               'august', 'september', 'october', 'november', 'december']

_daynames = ['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']

# The timezone table does not include the military time zones defined
# in RFC822, other than Z.  According to RFC1123, the description in
# RFC822 gets the signs wrong, so we can't rely on any such time
# zones.  RFC1123 recommends that numeric timezone indicators be used
# instead of timezone names.

_timezones = {'UT':0, 'UTC':0, 'GMT':0, 'Z':0,
              'AST': -400, 'ADT': -300,  # Atlantic (used in Canada)
              'EST': -500, 'EDT': -400,  # Eastern
              'CST': -600, 'CDT': -500,  # Central
              'MST': -700, 'MDT': -600,  # Mountain
              'PST': -800, 'PDT': -700   # Pacific
              }


def parsedate_tz(data):
    """Convert a date string to a time tuple.

    Accounts for military timezones.
    """
    data = data.split()
    # The FWS after the comma after the day-of-week is optional, so search and
    # adjust for this.
    if data[0].endswith(',') or data[0].lower() in _daynames:
        # There's a dayname here. Skip it
        del data[0]
    else:
        i = data[0].rfind(',')
        if i < 0:
            return None
        data[0] = data[0][i+1:]
    if len(data) == 3: # RFC 850 date, deprecated
        stuff = data[0].split('-')
        if len(stuff) == 3:
            data = stuff + data[1:]
    if len(data) == 4:
        s = data[3]
        i = s.find('+')
        if i > 0:
            data[3:] = [s[:i], s[i+1:]]
        else:
            data.append('') # Dummy tz
    if len(data) < 5:
        return None
    data = data[:5]
    [dd, mm, yy, tm, tz] = data
    mm = mm.lower()
    if mm not in _monthnames:
        dd, mm = mm, dd.lower()
        if mm not in _monthnames:
            return None
    mm = _monthnames.index(mm) + 1
    if mm > 12:
        mm -= 12
    if dd[-1] == ',':
        dd = dd[:-1]
    i = yy.find(':')
    if i > 0:
        yy, tm = tm, yy
    if yy[-1] == ',':
        yy = yy[:-1]
    if not yy[0].isdigit():
        yy, tz = tz, yy
    if tm[-1] == ',':
        tm = tm[:-1]
    tm = tm.split(':')
    if len(tm) == 2:
        [thh, tmm] = tm
        tss = '0'
    elif len(tm) == 3:
        [thh, tmm, tss] = tm
    else:
        return None
    try:
        yy = int(yy)
        dd = int(dd)
        thh = int(thh)
        tmm = int(tmm)
        tss = int(tss)
    except ValueError:
        return None
    tzoffset = None
    tz = tz.upper()
    if _timezones.has_key(tz):
        tzoffset = _timezones[tz]
    else:
        try:
            tzoffset = int(tz)
        except ValueError:
            pass
    # Convert a timezone offset into seconds ; -0500 -> -18000
    if tzoffset:
        if tzoffset < 0:
            tzsign = -1
            tzoffset = -tzoffset
        else:
            tzsign = 1
        tzoffset = tzsign * ( (tzoffset/100)*3600 + (tzoffset % 100)*60)
    tuple = (yy, mm, dd, thh, tmm, tss, 0, 0, 0, tzoffset)
    return tuple


def parsedate(data):
    """Convert a time string to a time tuple."""
    t = parsedate_tz(data)
    if isinstance(t, TupleType):
        return t[:9]
    else:
        return t


def mktime_tz(data):
    """Turn a 10-tuple as returned by parsedate_tz() into a UTC timestamp."""
    if data[9] is None:
        # No zone info, so localtime is better assumption than GMT
        return time.mktime(data[:8] + (-1,))
    else:
        t = time.mktime(data[:8] + (0,))
        return t - data[9] - time.timezone


def quote(str):
    """Add quotes around a string."""
    return str.replace('\\', '\\\\').replace('"', '\\"')


class AddrlistClass:
    """Address parser class by Ben Escoto.

    To understand what this class does, it helps to have a copy of RFC 2822 in
    front of you.

    Note: this class interface is deprecated and may be removed in the future.
    Use rfc822.AddressList instead.
    """

    def __init__(self, field):
        """Initialize a new instance.

        `field' is an unparsed address header field, containing
        one or more addresses.
        """
        self.specials = '()<>@,:;.\"[]'
        self.pos = 0
        self.LWS = ' \t'
        self.CR = '\r\n'
        self.atomends = self.specials + self.LWS + self.CR
        # Note that RFC 2822 now specifies `.' as obs-phrase, meaning that it
        # is obsolete syntax.  RFC 2822 requires that we recognize obsolete
        # syntax, so allow dots in phrases.
        self.phraseends = self.atomends.replace('.', '')
        self.field = field
        self.commentlist = []

    def gotonext(self):
        """Parse up to the start of the next address."""
        while self.pos < len(self.field):
            if self.field[self.pos] in self.LWS + '\n\r':
                self.pos += 1
            elif self.field[self.pos] == '(':
                self.commentlist.append(self.getcomment())
            else:
                break

    def getaddrlist(self):
        """Parse all addresses.

        Returns a list containing all of the addresses.
        """
        result = []
        while self.pos < len(self.field):
            ad = self.getaddress()
            if ad:
                result += ad
            else:
                result.append(('', ''))
        return result

    def getaddress(self):
        """Parse the next address."""
        self.commentlist = []
        self.gotonext()

        oldpos = self.pos
        oldcl = self.commentlist
        plist = self.getphraselist()

        self.gotonext()
        returnlist = []

        if self.pos >= len(self.field):
            # Bad email address technically, no domain.
            if plist:
                returnlist = [(SPACE.join(self.commentlist), plist[0])]

        elif self.field[self.pos] in '.@':
            # email address is just an addrspec
            # this isn't very efficient since we start over
            self.pos = oldpos
            self.commentlist = oldcl
            addrspec = self.getaddrspec()
            returnlist = [(SPACE.join(self.commentlist), addrspec)]

        elif self.field[self.pos] == ':':
            # address is a group
            returnlist = []

            fieldlen = len(self.field)
            self.pos += 1
            while self.pos < len(self.field):
                self.gotonext()
                if self.pos < fieldlen and self.field[self.pos] == ';':
                    self.pos += 1
                    break
                returnlist = returnlist + self.getaddress()

        elif self.field[self.pos] == '<':
            # Address is a phrase then a route addr
            routeaddr = self.getrouteaddr()

            if self.commentlist:
                returnlist = [(SPACE.join(plist) + ' (' +
                               ' '.join(self.commentlist) + ')', routeaddr)]
            else:
                returnlist = [(SPACE.join(plist), routeaddr)]

        else:
            if plist:
                returnlist = [(SPACE.join(self.commentlist), plist[0])]
            elif self.field[self.pos] in self.specials:
                self.pos += 1

        self.gotonext()
        if self.pos < len(self.field) and self.field[self.pos] == ',':
            self.pos += 1
        return returnlist

    def getrouteaddr(self):
        """Parse a route address (Return-path value).

        This method just skips all the route stuff and returns the addrspec.
        """
        if self.field[self.pos] != '<':
            return

        expectroute = False
        self.pos += 1
        self.gotonext()
        adlist = ''
        while self.pos < len(self.field):
            if expectroute:
                self.getdomain()
                expectroute = False
            elif self.field[self.pos] == '>':
                self.pos += 1
                break
            elif self.field[self.pos] == '@':
                self.pos += 1
                expectroute = True
            elif self.field[self.pos] == ':':
                self.pos += 1
            else:
                adlist = self.getaddrspec()
                self.pos += 1
                break
            self.gotonext()

        return adlist

    def getaddrspec(self):
        """Parse an RFC 2822 addr-spec."""
        aslist = []

        self.gotonext()
        while self.pos < len(self.field):
            if self.field[self.pos] == '.':
                aslist.append('.')
                self.pos += 1
            elif self.field[self.pos] == '"':
                aslist.append('"%s"' % self.getquote())
            elif self.field[self.pos] in self.atomends:
                break
            else:
                aslist.append(self.getatom())
            self.gotonext()

        if self.pos >= len(self.field) or self.field[self.pos] != '@':
            return EMPTYSTRING.join(aslist)

        aslist.append('@')
        self.pos += 1
        self.gotonext()
        return EMPTYSTRING.join(aslist) + self.getdomain()

    def getdomain(self):
        """Get the complete domain name from an address."""
        sdlist = []
        while self.pos < len(self.field):
            if self.field[self.pos] in self.LWS:
                self.pos += 1
            elif self.field[self.pos] == '(':
                self.commentlist.append(self.getcomment())
            elif self.field[self.pos] == '[':
                sdlist.append(self.getdomainliteral())
            elif self.field[self.pos] == '.':
                self.pos += 1
                sdlist.append('.')
            elif self.field[self.pos] in self.atomends:
                break
            else:
                sdlist.append(self.getatom())
        return EMPTYSTRING.join(sdlist)

    def getdelimited(self, beginchar, endchars, allowcomments=True):
        """Parse a header fragment delimited by special characters.

        `beginchar' is the start character for the fragment.
        If self is not looking at an instance of `beginchar' then
        getdelimited returns the empty string.

        `endchars' is a sequence of allowable end-delimiting characters.
        Parsing stops when one of these is encountered.

        If `allowcomments' is non-zero, embedded RFC 2822 comments are allowed
        within the parsed fragment.
        """
        if self.field[self.pos] != beginchar:
            return ''

        slist = ['']
        quote = False
        self.pos += 1
        while self.pos < len(self.field):
            if quote:
                slist.append(self.field[self.pos])
                quote = False
            elif self.field[self.pos] in endchars:
                self.pos += 1
                break
            elif allowcomments and self.field[self.pos] == '(':
                slist.append(self.getcomment())
            elif self.field[self.pos] == '\\':
                quote = True
            else:
                slist.append(self.field[self.pos])
            self.pos += 1

        return EMPTYSTRING.join(slist)

    def getquote(self):
        """Get a quote-delimited fragment from self's field."""
        return self.getdelimited('"', '"\r', False)

    def getcomment(self):
        """Get a parenthesis-delimited fragment from self's field."""
        return self.getdelimited('(', ')\r', True)

    def getdomainliteral(self):
        """Parse an RFC 2822 domain-literal."""
        return '[%s]' % self.getdelimited('[', ']\r', False)

    def getatom(self, atomends=None):
        """Parse an RFC 2822 atom.

        Optional atomends specifies a different set of end token delimiters
        (the default is to use self.atomends).  This is used e.g. in
        getphraselist() since phrase endings must not include the `.' (which
        is legal in phrases)."""
        atomlist = ['']
        if atomends is None:
            atomends = self.atomends

        while self.pos < len(self.field):
            if self.field[self.pos] in atomends:
                break
            else:
                atomlist.append(self.field[self.pos])
            self.pos += 1

        return EMPTYSTRING.join(atomlist)

    def getphraselist(self):
        """Parse a sequence of RFC 2822 phrases.

        A phrase is a sequence of words, which are in turn either RFC 2822
        atoms or quoted-strings.  Phrases are canonicalized by squeezing all
        runs of continuous whitespace into one space.
        """
        plist = []

        while self.pos < len(self.field):
            if self.field[self.pos] in self.LWS:
                self.pos += 1
            elif self.field[self.pos] == '"':
                plist.append(self.getquote())
            elif self.field[self.pos] == '(':
                self.commentlist.append(self.getcomment())
            elif self.field[self.pos] in self.phraseends:
                break
            else:
                plist.append(self.getatom(self.phraseends))

        return plist

class AddressList(AddrlistClass):
    """An AddressList encapsulates a list of parsed RFC 2822 addresses."""
    def __init__(self, field):
        AddrlistClass.__init__(self, field)
        if field:
            self.addresslist = self.getaddrlist()
        else:
            self.addresslist = []

    def __len__(self):
        return len(self.addresslist)

    def __str__(self):
        return COMMASPACE.join(map(dump_address_pair, self.addresslist))

    def __add__(self, other):
        # Set union
        newaddr = AddressList(None)
        newaddr.addresslist = self.addresslist[:]
        for x in other.addresslist:
            if not x in self.addresslist:
                newaddr.addresslist.append(x)
        return newaddr

    def __iadd__(self, other):
        # Set union, in-place
        for x in other.addresslist:
            if not x in self.addresslist:
                self.addresslist.append(x)
        return self

    def __sub__(self, other):
        # Set difference
        newaddr = AddressList(None)
        for x in self.addresslist:
            if not x in other.addresslist:
                newaddr.addresslist.append(x)
        return newaddr

    def __isub__(self, other):
        # Set difference, in-place
        for x in other.addresslist:
            if x in self.addresslist:
                self.addresslist.remove(x)
        return self

    def __getitem__(self, index):
        # Make indexing, slices, and 'in' work
        return self.addresslist[index]

Index: Charset.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/Charset.py,v
retrieving revision 1.7.2.3
retrieving revision 1.7.2.4
diff -C2 -d -r1.7.2.3 -r1.7.2.4
*** Charset.py	14 Oct 2002 17:26:00 -0000	1.7.2.3
--- Charset.py	21 Mar 2003 21:09:31 -0000	1.7.2.4
***************
*** 36,39 ****
--- 36,53 ----
      'iso-8859-1':  (QP,        QP,      None),
      'iso-8859-2':  (QP,        QP,      None),
+     'iso-8859-3':  (QP,        QP,      None),
+     'iso-8859-4':  (QP,        QP,      None),
+     # iso-8859-5 is Cyrillic, and not especially used
+     # iso-8859-6 is Arabic, also not particularly used
+     # iso-8859-7 is Greek, QP will not make it readable
+     # iso-8859-8 is Hebrew, QP will not make it readable
+     'iso-8859-9':  (QP,        QP,      None),
+     'iso-8859-10': (QP,        QP,      None),
+     # iso-8859-11 is Thai, QP will not make it readable
+     'iso-8859-13': (QP,        QP,      None),
+     'iso-8859-14': (QP,        QP,      None),
+     'iso-8859-15': (QP,        QP,      None),
+     'windows-1252':(QP,        QP,      None),
+     'viscii':      (QP,        QP,      None),
      'us-ascii':    (None,      None,    None),
      'big5':        (BASE64,    BASE64,  None),
***************
*** 53,56 ****
--- 67,89 ----
      'latin_1': 'iso-8859-1',
      'latin-1': 'iso-8859-1',
+     'latin_2': 'iso-8859-2',
+     'latin-2': 'iso-8859-2',
+     'latin_3': 'iso-8859-3',
+     'latin-3': 'iso-8859-3',
+     'latin_4': 'iso-8859-4',
+     'latin-4': 'iso-8859-4',
+     'latin_5': 'iso-8859-9',
+     'latin-5': 'iso-8859-9',
+     'latin_6': 'iso-8859-10',
+     'latin-6': 'iso-8859-10',
+     'latin_7': 'iso-8859-13',
+     'latin-7': 'iso-8859-13',
+     'latin_8': 'iso-8859-14',
+     'latin-8': 'iso-8859-14',
+     'latin_9': 'iso-8859-15',
+     'latin-9': 'iso-8859-15',
+     'cp949':   'ks_c_5601-1987',
+     'euc_jp':  'euc-jp',
+     'euc_kr':  'euc-kr',
      'ascii':   'us-ascii',
      }
***************
*** 70,73 ****
--- 103,110 ----
      'iso-2022-jp': 'japanese.iso-2022-jp',
      'shift_jis':   'japanese.shift_jis',
+     'euc-kr':      'korean.euc-kr',
+     'ks_c_5601-1987': 'korean.cp949',
+     'iso-2022-kr': 'korean.iso-2022-kr',
+     'johab':       'korean.johab',
      'gb2132':      'eucgb2312_cn',
      'big5':        'big5_tw',
***************
*** 198,201 ****
--- 235,240 ----
          return self.input_charset.lower()
  
+     __repr__ = __str__
+ 
      def __eq__(self, other):
          return str(self) == str(other).lower()
***************
*** 322,326 ****
              return email.base64MIME.header_encode(s, cset)
          elif self.header_encoding == QP:
!             return email.quopriMIME.header_encode(s, cset)
          elif self.header_encoding == SHORTEST:
              lenb64 = email.base64MIME.base64_len(s)
--- 361,365 ----
              return email.base64MIME.header_encode(s, cset)
          elif self.header_encoding == QP:
!             return email.quopriMIME.header_encode(s, cset, maxlinelen=None)
          elif self.header_encoding == SHORTEST:
              lenb64 = email.base64MIME.base64_len(s)
***************
*** 329,333 ****
                  return email.base64MIME.header_encode(s, cset)
              else:
!                 return email.quopriMIME.header_encode(s, cset)
          else:
              return s
--- 368,372 ----
                  return email.base64MIME.header_encode(s, cset)
              else:
!                 return email.quopriMIME.header_encode(s, cset, maxlinelen=None)
          else:
              return s
***************
*** 349,353 ****
          if self.body_encoding is BASE64:
              return email.base64MIME.body_encode(s)
!         elif self.header_encoding is QP:
              return email.quopriMIME.body_encode(s)
          else:
--- 388,392 ----
          if self.body_encoding is BASE64:
              return email.base64MIME.body_encode(s)
!         elif self.body_encoding is QP:
              return email.quopriMIME.body_encode(s)
          else:

Index: Generator.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/Generator.py,v
retrieving revision 1.6.10.3
retrieving revision 1.6.10.4
diff -C2 -d -r1.6.10.3 -r1.6.10.4
*** Generator.py	14 Oct 2002 17:26:01 -0000	1.6.10.3
--- Generator.py	21 Mar 2003 21:09:31 -0000	1.6.10.4
***************
*** 5,10 ****
  """
  
- import time
  import re
  import random
  
--- 5,11 ----
  """
  
  import re
+ import time
+ import locale
  import random
  
***************
*** 13,16 ****
--- 14,18 ----
  
  from email.Header import Header
+ from email.Parser import NLCRE
  
  try:
***************
*** 160,201 ****
      def _write_headers(self, msg):
          for h, v in msg.items():
!             # RFC 2822 says that lines SHOULD be no more than maxheaderlen
!             # characters wide, so we're well within our rights to split long
!             # headers.
!             text = '%s: %s' % (h, v)
!             if self.__maxheaderlen > 0 and len(text) > self.__maxheaderlen:
!                 text = self._split_header(text)
!             print >> self._fp, text
          # A blank line always separates headers from body
          print >> self._fp
  
-     def _split_header(self, text):
-         maxheaderlen = self.__maxheaderlen
-         # Find out whether any lines in the header are really longer than
-         # maxheaderlen characters wide.  There could be continuation lines
-         # that actually shorten it.  Also, replace hard tabs with 8 spaces.
-         lines = [s.replace('\t', SPACE8) for s in text.splitlines()]
-         for line in lines:
-             if len(line) > maxheaderlen:
-                 break
-         else:
-             # No line was actually longer than maxheaderlen characters, so
-             # just return the original unchanged.
-             return text
-         # If we have raw 8bit data in a byte string, we have no idea what the
-         # encoding is.  I think there is no safe way to split this string.  If
-         # it's ascii-subset, then we could do a normal ascii split, but if
-         # it's multibyte then we could break the string.  There's no way to
-         # know so the least harm seems to be to not split the string and risk
-         # it being too long.
-         if _is8bitstring(text):
-             return text
-         # The `text' argument already has the field name prepended, so don't
-         # provide it here or the first line will get folded too short.
-         h = Header(text, maxlinelen=maxheaderlen,
-                    # For backwards compatibility, we use a hard tab here
-                    continuation_ws='\t')
-         return h.encode()
- 
      #
      # Handlers for writing types and subtypes
--- 162,188 ----
      def _write_headers(self, msg):
          for h, v in msg.items():
!             print >> self._fp, '%s:' % h,
!             if self.__maxheaderlen == 0:
!                 # Explicit no-wrapping
!                 print >> self._fp, v
!             elif isinstance(v, Header):
!                 # Header instances know what to do
!                 print >> self._fp, v.encode()
!             elif _is8bitstring(v):
!                 # If we have raw 8bit data in a byte string, we have no idea
!                 # what the encoding is.  There is no safe way to split this
!                 # string.  If it's ascii-subset, then we could do a normal
!                 # ascii split, but if it's multibyte then we could break the
!                 # string.  There's no way to know so the least harm seems to
!                 # be to not split the string and risk it being too long.
!                 print >> self._fp, v
!             else:
!                 # Header's got lots of smarts, so use it.
!                 print >> self._fp, Header(
!                     v, maxlinelen=self.__maxheaderlen,
!                     header_name=h, continuation_ws='\t').encode()
          # A blank line always separates headers from body
          print >> self._fp
  
      #
      # Handlers for writing types and subtypes
***************
*** 259,262 ****
--- 246,257 ----
          if msg.preamble is not None:
              self._fp.write(msg.preamble)
+             # If preamble is the empty string, the length of the split will be
+             # 1, but the last element will be the empty string.  If it's
+             # anything else but does not end in a line separator, the length
+             # will be > 1 and not end in an empty string.  We need to
+             # guarantee a newline after the preamble, but don't add too many.
+             plines = NLCRE.split(msg.preamble)
+             if plines <> [''] and plines[-1] <> '':
+                 self._fp.write('\n')
          # First boundary is a bit different; it doesn't have a leading extra
          # newline.
***************
*** 365,369 ****
      # Craft a random boundary.  If text is given, ensure that the chosen
      # boundary doesn't appear in the text.
!     boundary = ('=' * 15) + repr(random.random()).split('.')[1] + '=='
      if text is None:
          return boundary
--- 360,365 ----
      # Craft a random boundary.  If text is given, ensure that the chosen
      # boundary doesn't appear in the text.
!     dp = locale.localeconv().get('decimal_point', '.')
!     boundary = ('=' * 15) + repr(random.random()).split(dp)[1] + '=='
      if text is None:
          return boundary

Index: Header.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/Header.py,v
retrieving revision 1.13.2.2
retrieving revision 1.13.2.3
diff -C2 -d -r1.13.2.2 -r1.13.2.3
*** Header.py	14 Oct 2002 17:26:02 -0000	1.13.2.2
--- Header.py	21 Mar 2003 21:09:31 -0000	1.13.2.3
***************
*** 5,12 ****
--- 5,14 ----
  
  import re
+ import binascii
  from types import StringType, UnicodeType
  
  import email.quopriMIME
  import email.base64MIME
+ from email.Errors import HeaderParseError
  from email.Charset import Charset
  
***************
*** 26,31 ****
--- 28,36 ----
  CRLF = '\r\n'
  NL = '\n'
+ SPACE = ' '
+ USPACE = u' '
  SPACE8 = ' ' * 8
  EMPTYSTRING = ''
+ UEMPTYSTRING = u''
  
  MAXLINELEN = 76
***************
*** 48,51 ****
--- 53,63 ----
    ''', re.VERBOSE | re.IGNORECASE)
  
+ pcre = re.compile('([,;])')
+ 
+ # Field name regexp, including trailing colon, but not separating whitespace,
+ # according to RFC 2822.  Character range is from tilde to exclamation mark.
+ # For use with .match()
+ fcre = re.compile(r'[\041-\176]+:$')
+ 
  
  
***************
*** 62,65 ****
--- 74,80 ----
      header, otherwise a lower-case string containing the name of the character
      set specified in the encoded string.
+ 
+     An email.Errors.HeaderParseError may be raised when certain decoding error
+     occurs (e.g. a base64 decoding exception).
      """
      # If no encoding, just return the header
***************
*** 80,84 ****
                  # Should we continue a long line?
                  if decoded and decoded[-1][1] is None:
!                     decoded[-1] = (decoded[-1][0] + dec, None)
                  else:
                      decoded.append((unenc, None))
--- 95,99 ----
                  # Should we continue a long line?
                  if decoded and decoded[-1][1] is None:
!                     decoded[-1] = (decoded[-1][0] + SPACE + unenc, None)
                  else:
                      decoded.append((unenc, None))
***************
*** 86,95 ****
                  charset, encoding = [s.lower() for s in parts[0:2]]
                  encoded = parts[2]
!                 dec = ''
                  if encoding == 'q':
                      dec = email.quopriMIME.header_decode(encoded)
                  elif encoding == 'b':
!                     dec = email.base64MIME.decode(encoded)
!                 else:
                      dec = encoded
  
--- 101,116 ----
                  charset, encoding = [s.lower() for s in parts[0:2]]
                  encoded = parts[2]
!                 dec = None
                  if encoding == 'q':
                      dec = email.quopriMIME.header_decode(encoded)
                  elif encoding == 'b':
!                     try:
!                         dec = email.base64MIME.decode(encoded)
!                     except binascii.Error:
!                         # Turn this into a higher level exception.  BAW: Right
!                         # now we throw the lower level exception away but
!                         # when/if we get exception chaining, we'll preserve it.
!                         raise HeaderParseError
!                 if dec is None:
                      dec = encoded
  
***************
*** 127,132 ****
  
  class Header:
!     def __init__(self, s=None, charset=None, maxlinelen=None, header_name=None,
!                  continuation_ws=' '):
          """Create a MIME-compliant header that can contain many character sets.
  
--- 148,154 ----
  
  class Header:
!     def __init__(self, s=None, charset=None,
!                  maxlinelen=None, header_name=None,
!                  continuation_ws=' ', errors='strict'):
          """Create a MIME-compliant header that can contain many character sets.
  
***************
*** 151,154 ****
--- 173,178 ----
          either a space or a hard tab) which will be prepended to continuation
          lines.
+ 
+         errors is passed through to the .append() call.
          """
          if charset is None:
***************
*** 162,166 ****
          self._chunks = []
          if s is not None:
!             self.append(s, charset)
          if maxlinelen is None:
              maxlinelen = MAXLINELEN
--- 186,190 ----
          self._chunks = []
          if s is not None:
!             self.append(s, charset, errors)
          if maxlinelen is None:
              maxlinelen = MAXLINELEN
***************
*** 183,189 ****
      def __unicode__(self):
          """Helper for the built-in unicode function."""
!         # charset item is a Charset instance so we need to stringify it.
!         uchunks = [unicode(s, str(charset)) for s, charset in self._chunks]
!         return u''.join(uchunks)
  
      # Rich comparison operators for equality only.  BAW: does it make sense to
--- 207,228 ----
      def __unicode__(self):
          """Helper for the built-in unicode function."""
!         uchunks = []
!         lastcs = None
!         for s, charset in self._chunks:
!             # We must preserve spaces between encoded and non-encoded word
!             # boundaries, which means for us we need to add a space when we go
!             # from a charset to None/us-ascii, or from None/us-ascii to a
!             # charset.  Only do this for the second and subsequent chunks.
!             nextcs = charset
!             if uchunks:
!                 if lastcs is not None:
!                     if nextcs is None or nextcs == 'us-ascii':
!                         uchunks.append(USPACE)
!                         nextcs = None
!                 elif nextcs is not None and nextcs <> 'us-ascii':
!                     uchunks.append(USPACE)
!             lastcs = nextcs
!             uchunks.append(unicode(s, str(charset)))
!         return UEMPTYSTRING.join(uchunks)
  
      # Rich comparison operators for equality only.  BAW: does it make sense to
***************
*** 197,201 ****
          return not self == other
  
!     def append(self, s, charset=None):
          """Append a string to the MIME header.
  
--- 236,240 ----
          return not self == other
  
!     def append(self, s, charset=None, errors='strict'):
          """Append a string to the MIME header.
  
***************
*** 214,217 ****
--- 253,259 ----
          following charsets in order: us-ascii, the charset hint, utf-8.  The
          first character set not to provoke a UnicodeError is used.
+ 
+         Optional `errors' is passed as the third argument to any unicode() or
+         ustr.encode() call.
          """
          if charset is None:
***************
*** 228,237 ****
                  # converted to a unicode with the input codec of the charset.
                  incodec = charset.input_codec or 'us-ascii'
!                 ustr = unicode(s, incodec)
                  # Now make sure that the unicode could be converted back to a
                  # byte string with the output codec, which may be different
                  # than the iput coded.  Still, use the original byte string.
                  outcodec = charset.output_codec or 'us-ascii'
!                 ustr.encode(outcodec)
              elif isinstance(s, UnicodeType):
                  # Now we have to be sure the unicode string can be converted
--- 270,279 ----
                  # converted to a unicode with the input codec of the charset.
                  incodec = charset.input_codec or 'us-ascii'
!                 ustr = unicode(s, incodec, errors)
                  # Now make sure that the unicode could be converted back to a
                  # byte string with the output codec, which may be different
                  # than the iput coded.  Still, use the original byte string.
                  outcodec = charset.output_codec or 'us-ascii'
!                 ustr.encode(outcodec, errors)
              elif isinstance(s, UnicodeType):
                  # Now we have to be sure the unicode string can be converted
***************
*** 241,245 ****
                      try:
                          outcodec = charset.output_codec or 'us-ascii'
!                         s = s.encode(outcodec)
                          break
                      except UnicodeError:
--- 283,287 ----
                      try:
                          outcodec = charset.output_codec or 'us-ascii'
!                         s = s.encode(outcodec, errors)
                          break
                      except UnicodeError:
***************
*** 249,259 ****
          self._chunks.append((s, charset))
  
!     def _split(self, s, charset, firstline=False):
          # Split up a header safely for use with encode_chunks.
          splittable = charset.to_splittable(s)
!         encoded = charset.from_splittable(splittable)
          elen = charset.encoded_header_len(encoded)
! 
!         if elen <= self._maxlinelen:
              return [(encoded, charset)]
          # If we have undetermined raw 8bit characters sitting in a byte
--- 291,301 ----
          self._chunks.append((s, charset))
  
!     def _split(self, s, charset, maxlinelen, splitchars):
          # Split up a header safely for use with encode_chunks.
          splittable = charset.to_splittable(s)
!         encoded = charset.from_splittable(splittable, True)
          elen = charset.encoded_header_len(encoded)
!         # If the line's encoded length first, just return it
!         if elen <= maxlinelen:
              return [(encoded, charset)]
          # If we have undetermined raw 8bit characters sitting in a byte
***************
*** 263,267 ****
          # be to not split the header at all, but that means they could go out
          # longer than maxlinelen.
!         elif charset == '8bit':
              return [(s, charset)]
          # BAW: I'm not sure what the right test here is.  What we're trying to
--- 305,309 ----
          # be to not split the header at all, but that means they could go out
          # longer than maxlinelen.
!         if charset == '8bit':
              return [(s, charset)]
          # BAW: I'm not sure what the right test here is.  What we're trying to
***************
*** 276,374 ****
          # although it's possible that other charsets may also benefit from the
          # higher-level syntactic breaks.
-         #
          elif charset == 'us-ascii':
!             return self._ascii_split(s, charset, firstline)
          # BAW: should we use encoded?
          elif elen == len(s):
              # We can split on _maxlinelen boundaries because we know that the
              # encoding won't change the size of the string
!             splitpnt = self._maxlinelen
              first = charset.from_splittable(splittable[:splitpnt], False)
              last = charset.from_splittable(splittable[splitpnt:], False)
          else:
!             # Divide and conquer.
!             halfway = _floordiv(len(splittable), 2)
!             first = charset.from_splittable(splittable[:halfway], False)
!             last = charset.from_splittable(splittable[halfway:], False)
!         # Do the split
!         return self._split(first, charset, firstline) + \
!                self._split(last, charset)
  
!     def _ascii_split(self, s, charset, firstline):
!         # Attempt to split the line at the highest-level syntactic break
!         # possible.  Note that we don't have a lot of smarts about field
!         # syntax; we just try to break on semi-colons, then whitespace.
!         rtn = []
!         lines = s.splitlines()
!         while lines:
!             line = lines.pop(0)
!             if firstline:
!                 maxlinelen = self._firstlinelen
!                 firstline = False
!             else:
!                 #line = line.lstrip()
!                 maxlinelen = self._maxlinelen
!             # Short lines can remain unchanged
!             if len(line.replace('\t', SPACE8)) <= maxlinelen:
!                 rtn.append(line)
!             else:
!                 oldlen = len(line)
!                 # Try to break the line on semicolons, but if that doesn't
!                 # work, try to split on folding whitespace.
!                 while len(line) > maxlinelen:
!                     i = line.rfind(';', 0, maxlinelen)
!                     if i < 0:
!                         break
!                     rtn.append(line[:i] + ';')
!                     line = line[i+1:]
!                 # Is the remaining stuff still longer than maxlinelen?
!                 if len(line) <= maxlinelen:
!                     # Splitting on semis worked
!                     rtn.append(line)
!                     continue
!                 # Splitting on semis didn't finish the job.  If it did any
!                 # work at all, stick the remaining junk on the front of the
!                 # `lines' sequence and let the next pass do its thing.
!                 if len(line) <> oldlen:
!                     lines.insert(0, line)
!                     continue
!                 # Otherwise, splitting on semis didn't help at all.
!                 parts = re.split(r'(\s+)', line)
!                 if len(parts) == 1 or (len(parts) == 3 and
!                                        parts[0].endswith(':')):
!                     # This line can't be split on whitespace.  There's now
!                     # little we can do to get this into maxlinelen.  BAW:
!                     # We're still potentially breaking the RFC by possibly
!                     # allowing lines longer than the absolute maximum of 998
!                     # characters.  For now, let it slide.
!                     #
!                     # len(parts) will be 1 if this line has no `Field: '
!                     # prefix, otherwise it will be len(3).
!                     rtn.append(line)
!                     continue
!                 # There is whitespace we can split on.
!                 first = parts.pop(0)
!                 sublines = [first]
!                 acc = len(first)
!                 while parts:
!                     len0 = len(parts[0])
!                     len1 = len(parts[1])
!                     if acc + len0 + len1 <= maxlinelen:
!                         sublines.append(parts.pop(0))
!                         sublines.append(parts.pop(0))
!                         acc += len0 + len1
!                     else:
!                         # Split it here, but don't forget to ignore the
!                         # next whitespace-only part
!                         if first <> '':
!                             rtn.append(EMPTYSTRING.join(sublines))
!                         del parts[0]
!                         first = parts.pop(0)
!                         sublines = [first]
!                         acc = len(first)
!                 rtn.append(EMPTYSTRING.join(sublines))
!         return [(chunk, charset) for chunk in rtn]
  
!     def _encode_chunks(self, newchunks):
          # MIME-encode a header with many different charsets and/or encodings.
          #
--- 318,346 ----
          # although it's possible that other charsets may also benefit from the
          # higher-level syntactic breaks.
          elif charset == 'us-ascii':
!             return self._split_ascii(s, charset, maxlinelen, splitchars)
          # BAW: should we use encoded?
          elif elen == len(s):
              # We can split on _maxlinelen boundaries because we know that the
              # encoding won't change the size of the string
!             splitpnt = maxlinelen
              first = charset.from_splittable(splittable[:splitpnt], False)
              last = charset.from_splittable(splittable[splitpnt:], False)
          else:
!             # Binary search for split point
!             first, last = _binsplit(splittable, charset, maxlinelen)
!         # first is of the proper length so just wrap it in the appropriate
!         # chrome.  last must be recursively split.
!         fsplittable = charset.to_splittable(first)
!         fencoded = charset.from_splittable(fsplittable, True)
!         chunk = [(fencoded, charset)]
!         return chunk + self._split(last, charset, self._maxlinelen, splitchars)
  
!     def _split_ascii(self, s, charset, firstlen, splitchars):
!         chunks = _split_ascii(s, firstlen, self._maxlinelen,
!                               self._continuation_ws, splitchars)
!         return zip(chunks, [charset]*len(chunks))
  
!     def _encode_chunks(self, newchunks, maxlinelen):
          # MIME-encode a header with many different charsets and/or encodings.
          #
***************
*** 388,404 ****
          # =?charset1?q?Mar=EDa_Gonz=E1lez_Alonso?=\n
          #  =?charset2?b?SvxyZ2VuIEL2aW5n?="
-         #
          chunks = []
          for header, charset in newchunks:
              if charset is None or charset.header_encoding is None:
!                 # There's no encoding for this chunk's charsets
!                 _max_append(chunks, header, self._maxlinelen)
              else:
!                 _max_append(chunks, charset.header_encode(header),
!                             self._maxlinelen, ' ')
          joiner = NL + self._continuation_ws
          return joiner.join(chunks)
  
!     def encode(self):
          """Encode a message header into an RFC-compliant format.
  
--- 360,381 ----
          # =?charset1?q?Mar=EDa_Gonz=E1lez_Alonso?=\n
          #  =?charset2?b?SvxyZ2VuIEL2aW5n?="
          chunks = []
          for header, charset in newchunks:
+             if not header:
+                 continue
              if charset is None or charset.header_encoding is None:
!                 s = header
              else:
!                 s = charset.header_encode(header)
!             # Don't add more folding whitespace than necessary
!             if chunks and chunks[-1].endswith(' '):
!                 extra = ''
!             else:
!                 extra = ' '
!             _max_append(chunks, s, maxlinelen, extra)
          joiner = NL + self._continuation_ws
          return joiner.join(chunks)
  
!     def encode(self, splitchars=';, '):
          """Encode a message header into an RFC-compliant format.
  
***************
*** 417,423 ****
          If the given charset is not known or an error occurs during
          conversion, this function will return the header untouched.
          """
          newchunks = []
          for s, charset in self._chunks:
!             newchunks += self._split(s, charset, True)
!         return self._encode_chunks(newchunks)
--- 394,515 ----
          If the given charset is not known or an error occurs during
          conversion, this function will return the header untouched.
+ 
+         Optional splitchars is a string containing characters to split long
+         ASCII lines on, in rough support of RFC 2822's `highest level
+         syntactic breaks'.  This doesn't affect RFC 2047 encoded lines.
          """
          newchunks = []
+         maxlinelen = self._firstlinelen
+         lastlen = 0
          for s, charset in self._chunks:
!             # The first bit of the next chunk should be just long enough to
!             # fill the next line.  Don't forget the space separating the
!             # encoded words.
!             targetlen = maxlinelen - lastlen - 1
!             if targetlen < charset.encoded_header_len(''):
!                 # Stick it on the next line
!                 targetlen = maxlinelen
!             newchunks += self._split(s, charset, targetlen, splitchars)
!             lastchunk, lastcharset = newchunks[-1]
!             lastlen = lastcharset.encoded_header_len(lastchunk)
!         return self._encode_chunks(newchunks, maxlinelen)
! 
! 
! 
! def _split_ascii(s, firstlen, restlen, continuation_ws, splitchars):
!     lines = []
!     maxlen = firstlen
!     for line in s.splitlines():
!         # Ignore any leading whitespace (i.e. continuation whitespace) already
!         # on the line, since we'll be adding our own.
!         line = line.lstrip()
!         if len(line) < maxlen:
!             lines.append(line)
!             maxlen = restlen
!             continue
!         # Attempt to split the line at the highest-level syntactic break
!         # possible.  Note that we don't have a lot of smarts about field
!         # syntax; we just try to break on semi-colons, then commas, then
!         # whitespace.
!         for ch in splitchars:
!             if line.find(ch) >= 0:
!                 break
!         else:
!             # There's nothing useful to split the line on, not even spaces, so
!             # just append this line unchanged
!             lines.append(line)
!             maxlen = restlen
!             continue
!         # Now split the line on the character plus trailing whitespace
!         cre = re.compile(r'%s\s*' % ch)
!         if ch in ';,':
!             eol = ch
!         else:
!             eol = ''
!         joiner = eol + ' '
!         joinlen = len(joiner)
!         wslen = len(continuation_ws.replace('\t', SPACE8))
!         this = []
!         linelen = 0
!         for part in cre.split(line):
!             curlen = linelen + max(0, len(this)-1) * joinlen
!             partlen = len(part)
!             onfirstline = not lines
!             # We don't want to split after the field name, if we're on the
!             # first line and the field name is present in the header string.
!             if ch == ' ' and onfirstline and \
!                    len(this) == 1 and fcre.match(this[0]):
!                 this.append(part)
!                 linelen += partlen
!             elif curlen + partlen > maxlen:
!                 if this:
!                     lines.append(joiner.join(this) + eol)
!                 # If this part is longer than maxlen and we aren't already
!                 # splitting on whitespace, try to recursively split this line
!                 # on whitespace.
!                 if partlen > maxlen and ch <> ' ':
!                     subl = _split_ascii(part, maxlen, restlen,
!                                         continuation_ws, ' ')
!                     lines.extend(subl[:-1])
!                     this = [subl[-1]]
!                 else:
!                     this = [part]
!                 linelen = wslen + len(this[-1])
!                 maxlen = restlen
!             else:
!                 this.append(part)
!                 linelen += partlen
!         # Put any left over parts on a line by themselves
!         if this:
!             lines.append(joiner.join(this))
!     return lines
! 
! 
! 
! def _binsplit(splittable, charset, maxlinelen):
!     i = 0
!     j = len(splittable)
!     while i < j:
!         # Invariants:
!         # 1. splittable[:k] fits for all k <= i (note that we *assume*,
!         #    at the start, that splittable[:0] fits).
!         # 2. splittable[:k] does not fit for any k > j (at the start,
!         #    this means we shouldn't look at any k > len(splittable)).
!         # 3. We don't know about splittable[:k] for k in i+1..j.
!         # 4. We want to set i to the largest k that fits, with i <= k <= j.
!         #
!         m = (i+j+1) >> 1  # ceiling((i+j)/2); i < m <= j
!         chunk = charset.from_splittable(splittable[:m], True)
!         chunklen = charset.encoded_header_len(chunk)
!         if chunklen <= maxlinelen:
!             # m is acceptable, so is a new lower bound.
!             i = m
!         else:
!             # m is not acceptable, so final i must be < m.
!             j = m - 1
!     # i == j.  Invariant #1 implies that splittable[:i] fits, and
!     # invariant #2 implies that splittable[:i+1] does not fit, so i
!     # is what we're looking for.
!     first = charset.from_splittable(splittable[:i], False)
!     last  = charset.from_splittable(splittable[i:], False)
!     return first, last

Index: MIMEText.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/MIMEText.py,v
retrieving revision 1.3.10.1
retrieving revision 1.3.10.2
diff -C2 -d -r1.3.10.1 -r1.3.10.2
*** MIMEText.py	4 Oct 2002 17:24:24 -0000	1.3.10.1
--- MIMEText.py	21 Mar 2003 21:09:31 -0000	1.3.10.2
***************
*** 18,23 ****
          """Create a text/* type MIME document.
  
!         _text is the string for this message object.  If the text does not end
!         in a newline, one is added.
  
          _subtype is the MIME sub content type, defaulting to "plain".
--- 18,22 ----
          """Create a text/* type MIME document.
  
!         _text is the string for this message object.
  
          _subtype is the MIME sub content type, defaulting to "plain".
***************
*** 36,41 ****
          MIMENonMultipart.__init__(self, 'text', _subtype,
                                    **{'charset': _charset})
-         if _text and not _text.endswith('\n'):
-             _text += '\n'
          self.set_payload(_text, _charset)
          if _encoder is not None:
--- 35,38 ----

Index: Message.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/Message.py,v
retrieving revision 1.9.6.2
retrieving revision 1.9.6.3
diff -C2 -d -r1.9.6.2 -r1.9.6.3
*** Message.py	10 Oct 2002 19:09:24 -0000	1.9.6.2
--- Message.py	21 Mar 2003 21:09:31 -0000	1.9.6.3
***************
*** 6,9 ****
--- 6,11 ----
  
  import re
+ import uu
+ import binascii
  import warnings
  from cStringIO import StringIO
***************
*** 11,16 ****
  
  # Intrapackage imports
- from email import Errors
  from email import Utils
  from email import Charset
  
--- 13,18 ----
  
  # Intrapackage imports
  from email import Utils
+ from email import Errors
  from email import Charset
  
***************
*** 165,176 ****
          i returns that index into the payload.
  
!         Optional decode is a flag (defaulting to False) indicating whether the
!         payload should be decoded or not, according to the
!         Content-Transfer-Encoding header.  When True and the message is not a
!         multipart, the payload will be decoded if this header's value is
!         `quoted-printable' or `base64'.  If some other encoding is used, or
!         the header is missing, the payload is returned as-is (undecoded).  If
!         the message is a multipart and the decode flag is True, then None is
!         returned.
          """
          if i is None:
--- 167,182 ----
          i returns that index into the payload.
  
!         Optional decode is a flag indicating whether the payload should be
!         decoded or not, according to the Content-Transfer-Encoding header
!         (default is False).
! 
!         When True and the message is not a multipart, the payload will be
!         decoded if this header's value is `quoted-printable' or `base64'.  If
!         some other encoding is used, or the header is missing, or if the
!         payload has bogus data (i.e. bogus base64 or uuencoded data), the
!         payload is returned as-is.
! 
!         If the message is a multipart and the decode flag is True, then None
!         is returned.
          """
          if i is None:
***************
*** 183,191 ****
              if self.is_multipart():
                  return None
!             cte = self.get('content-transfer-encoding', '')
!             if cte.lower() == 'quoted-printable':
                  return Utils._qdecode(payload)
!             elif cte.lower() == 'base64':
!                 return Utils._bdecode(payload)
          # Everything else, including encodings with 8bit or 7bit are returned
          # unchanged.
--- 189,209 ----
              if self.is_multipart():
                  return None
!             cte = self.get('content-transfer-encoding', '').lower()
!             if cte == 'quoted-printable':
                  return Utils._qdecode(payload)
!             elif cte == 'base64':
!                 try:
!                     return Utils._bdecode(payload)
!                 except binascii.Error:
!                     # Incorrect padding
!                     return payload
!             elif cte in ('x-uuencode', 'uuencode', 'uue', 'x-uue'):
!                 sfp = StringIO()
!                 try:
!                     uu.decode(StringIO(payload+'\n'), sfp)
!                     payload = sfp.getvalue()
!                 except uu.Error:
!                     # Some decoding problem
!                     return payload
          # Everything else, including encodings with 8bit or 7bit are returned
          # unchanged.

Index: Parser.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/Parser.py,v
retrieving revision 1.5.10.3
retrieving revision 1.5.10.4
diff -C2 -d -r1.5.10.3 -r1.5.10.4
*** Parser.py	7 Oct 2002 17:02:40 -0000	1.5.10.3
--- Parser.py	21 Mar 2003 21:09:31 -0000	1.5.10.4
***************
*** 21,25 ****
      False = 0
  
! nlcre = re.compile('\r\n|\r|\n')
  
  
--- 21,25 ----
      False = 0
  
! NLCRE = re.compile('\r\n|\r|\n')
  
  
***************
*** 60,66 ****
          """
          root = self._class()
!         self._parseheaders(root, fp)
          if not headersonly:
!             self._parsebody(root, fp)
          return root
  
--- 60,66 ----
          """
          root = self._class()
!         firstbodyline = self._parseheaders(root, fp)
          if not headersonly:
!             self._parsebody(root, fp, firstbodyline)
          return root
  
***************
*** 81,84 ****
--- 81,85 ----
          lastvalue = []
          lineno = 0
+         firstbodyline = None
          while True:
              # Don't strip the line before we test for the end condition,
***************
*** 121,131 ****
                  if self._strict:
                      raise Errors.HeaderParseError(
!                         "Not a header, not a continuation: ``%s''"%line)
                  elif lineno == 1 and line.startswith('--'):
                      # allow through duplicate boundary tags.
                      continue
                  else:
!                     raise Errors.HeaderParseError(
!                         "Not a header, not a continuation: ``%s''"%line)
              if lastheader:
                  container[lastheader] = NL.join(lastvalue)
--- 122,135 ----
                  if self._strict:
                      raise Errors.HeaderParseError(
!                         "Not a header, not a continuation: ``%s''" % line)
                  elif lineno == 1 and line.startswith('--'):
                      # allow through duplicate boundary tags.
                      continue
                  else:
!                     # There was no separating blank line as mandated by RFC
!                     # 2822, but we're in non-strict mode.  So just offer up
!                     # this current line as the first body line.
!                     firstbodyline = line
!                     break
              if lastheader:
                  container[lastheader] = NL.join(lastvalue)
***************
*** 135,140 ****
          if lastheader:
              container[lastheader] = NL.join(lastvalue)
  
!     def _parsebody(self, container, fp):
          # Parse the body, but first split the payload on the content-type
          # boundary if present.
--- 139,145 ----
          if lastheader:
              container[lastheader] = NL.join(lastvalue)
+         return firstbodyline
  
!     def _parsebody(self, container, fp, firstbodyline=None):
          # Parse the body, but first split the payload on the content-type
          # boundary if present.
***************
*** 153,156 ****
--- 158,163 ----
              separator = '--' + boundary
              payload = fp.read()
+             if firstbodyline is not None:
+                 payload = firstbodyline + '\n' + payload
              # We use an RE here because boundaries can have trailing
              # whitespace.
***************
*** 170,174 ****
              # Find out what kind of line endings we're using
              start += len(mo.group('sep')) + len(mo.group('ws'))
!             mo = nlcre.search(payload, start)
              if mo:
                  start += len(mo.group(0))
--- 177,181 ----
              # Find out what kind of line endings we're using
              start += len(mo.group('sep')) + len(mo.group('ws'))
!             mo = NLCRE.search(payload, start)
              if mo:
                  start += len(mo.group(0))
***************
*** 222,228 ****
                          msgobj = self.parsestr(parthdrs, headersonly=1)
                      # while submsgobj is the message itself
-                     submsgobj = self.parsestr(part)
-                     msgobj.attach(submsgobj)
                      msgobj.set_default_type('message/rfc822')
                  else:
                      msgobj = self.parsestr(part)
--- 229,239 ----
                          msgobj = self.parsestr(parthdrs, headersonly=1)
                      # while submsgobj is the message itself
                      msgobj.set_default_type('message/rfc822')
+                     maintype = msgobj.get_content_maintype()
+                     if maintype in ('message', 'multipart'):
+                         submsgobj = self.parsestr(part)
+                         msgobj.attach(submsgobj)
+                     else:
+                         msgobj.set_payload(part)
                  else:
                      msgobj = self.parsestr(part)
***************
*** 257,261 ****
              container.attach(msg)
          else:
!             container.set_payload(fp.read())
  
  
--- 268,275 ----
              container.attach(msg)
          else:
!             text = fp.read()
!             if firstbodyline is not None:
!                 text = firstbodyline + '\n' + text
!             container.set_payload(text)
  
  
***************
*** 271,275 ****
      interested in is the message headers.
      """
!     def _parsebody(self, container, fp):
          # Consume but do not parse, the body
!         container.set_payload(fp.read())
--- 285,292 ----
      interested in is the message headers.
      """
!     def _parsebody(self, container, fp, firstbodyline=None):
          # Consume but do not parse, the body
!         text = fp.read()
!         if firstbodyline is not None:
!             text = firstbodyline + '\n' + text
!         container.set_payload(text)

Index: Utils.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/Utils.py,v
retrieving revision 1.9.6.1
retrieving revision 1.9.6.2
diff -C2 -d -r1.9.6.1 -r1.9.6.2
*** Utils.py	4 Oct 2002 17:24:24 -0000	1.9.6.1
--- Utils.py	21 Mar 2003 21:09:31 -0000	1.9.6.2
***************
*** 14,24 ****
  from types import ListType
  
! from rfc822 import quote
! from rfc822 import AddressList as _AddressList
! from rfc822 import mktime_tz
  
  # We need wormarounds for bugs in these methods in older Pythons (see below)
! from rfc822 import parsedate as _parsedate
! from rfc822 import parsedate_tz as _parsedate_tz
  
  try:
--- 14,24 ----
  from types import ListType
  
! from email._parseaddr import quote
! from email._parseaddr import AddressList as _AddressList
! from email._parseaddr import mktime_tz
  
  # We need wormarounds for bugs in these methods in older Pythons (see below)
! from email._parseaddr import parsedate as _parsedate
! from email._parseaddr import parsedate_tz as _parsedate_tz
  
  try:
***************
*** 55,60 ****
  CRLF = '\r\n'
  
! specialsre = re.compile(r'[][\()<>@,:;".]')
! escapesre = re.compile(r'[][\()"]')
  
  
--- 55,60 ----
  CRLF = '\r\n'
  
! specialsre = re.compile(r'[][\\()<>@,:;".]')
! escapesre = re.compile(r'[][\\()"]')
  
  
***************
*** 67,72 ****
  
  def _bdecode(s):
-     if not s:
-         return s
      # We can't quite use base64.encodestring() since it tacks on a "courtesy
      # newline".  Blech!
--- 67,70 ----
***************
*** 281,287 ****
      """Decode string according to RFC 2231"""
      import urllib
!     charset, language, s = s.split("'", 2)
!     s = urllib.unquote(s)
!     return charset, language, s
  
  
--- 279,287 ----
      """Decode string according to RFC 2231"""
      import urllib
!     parts = s.split("'", 2)
!     if len(parts) == 1:
!         return None, None, s
!     charset, language, s = parts
!     return charset, language, urllib.unquote(s)
  
  
***************
*** 336,340 ****
                  value.append(continuation)
              charset, language, value = decode_rfc2231(EMPTYSTRING.join(value))
!             new_params.append((name,
!                                (charset, language, '"%s"' % quote(value))))
      return new_params
--- 336,340 ----
                  value.append(continuation)
              charset, language, value = decode_rfc2231(EMPTYSTRING.join(value))
!             new_params.append(
!                 (name, (charset, language, '"%s"' % quote(value))))
      return new_params

Index: __init__.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/__init__.py,v
retrieving revision 1.4.10.4
retrieving revision 1.4.10.5
diff -C2 -d -r1.4.10.4 -r1.4.10.5
*** __init__.py	14 Oct 2002 17:26:02 -0000	1.4.10.4
--- __init__.py	21 Mar 2003 21:09:31 -0000	1.4.10.5
***************
*** 5,9 ****
  """
  
! __version__ = '2.4.3'
  
  __all__ = [
--- 5,9 ----
  """
  
! __version__ = '2.5'
  
  __all__ = [

Index: _compat21.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/_compat21.py,v
retrieving revision 1.4.2.1
retrieving revision 1.4.2.2
diff -C2 -d -r1.4.2.1 -r1.4.2.2
*** _compat21.py	4 Oct 2002 17:24:24 -0000	1.4.2.1
--- _compat21.py	21 Mar 2003 21:09:31 -0000	1.4.2.2
***************
*** 8,11 ****
--- 8,14 ----
  from types import StringType, UnicodeType
  
+ False = 0
+ True = 1
+ 
  
  
***************
*** 32,36 ****
  
  def _isstring(obj):
!     return isinstance(obj, StringType) or isinstance(obj, UnicodeType)    
  
  
--- 35,39 ----
  
  def _isstring(obj):
!     return isinstance(obj, StringType) or isinstance(obj, UnicodeType)
  
  
***************
*** 38,46 ****
  # These two functions are imported into the Iterators.py interface module.
  # The Python 2.2 version uses generators for efficiency.
! def body_line_iterator(msg):
!     """Iterate over the parts, returning string payloads line-by-line."""
      lines = []
      for subpart in msg.walk():
!         payload = subpart.get_payload()
          if _isstring(payload):
              for line in StringIO(payload).readlines():
--- 41,52 ----
  # These two functions are imported into the Iterators.py interface module.
  # The Python 2.2 version uses generators for efficiency.
! def body_line_iterator(msg, decode=False):
!     """Iterate over the parts, returning string payloads line-by-line.
! 
!     Optional decode (default False) is passed through to .get_payload().
!     """
      lines = []
      for subpart in msg.walk():
!         payload = subpart.get_payload(decode=decode)
          if _isstring(payload):
              for line in StringIO(payload).readlines():

Index: _compat22.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/_compat22.py,v
retrieving revision 1.4.2.1
retrieving revision 1.4.2.2
diff -C2 -d -r1.4.2.1 -r1.4.2.2
*** _compat22.py	4 Oct 2002 17:24:24 -0000	1.4.2.1
--- _compat22.py	21 Mar 2003 21:09:31 -0000	1.4.2.2
***************
*** 39,46 ****
  # These two functions are imported into the Iterators.py interface module.
  # The Python 2.2 version uses generators for efficiency.
! def body_line_iterator(msg):
!     """Iterate over the parts, returning string payloads line-by-line."""
      for subpart in msg.walk():
!         payload = subpart.get_payload()
          if _isstring(payload):
              for line in StringIO(payload):
--- 39,49 ----
  # These two functions are imported into the Iterators.py interface module.
  # The Python 2.2 version uses generators for efficiency.
! def body_line_iterator(msg, decode=False):
!     """Iterate over the parts, returning string payloads line-by-line.
! 
!     Optional decode (default False) is passed through to .get_payload().
!     """
      for subpart in msg.walk():
!         payload = subpart.get_payload(decode=decode)
          if _isstring(payload):
              for line in StringIO(payload):

Index: base64MIME.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/base64MIME.py,v
retrieving revision 1.5.2.1
retrieving revision 1.5.2.2
diff -C2 -d -r1.5.2.1 -r1.5.2.2
*** base64MIME.py	4 Oct 2002 17:24:24 -0000	1.5.2.1
--- base64MIME.py	21 Mar 2003 21:09:31 -0000	1.5.2.2
***************
*** 103,109 ****
      max_unencoded = _floordiv(max_encoded * 3, 4)
  
-     # BAW: Ben's original code used a step of max_unencoded, but I think it
-     # ought to be max_encoded.  Otherwise, where's max_encoded used?  I'm
-     # still not sure what the
      for i in range(0, len(header), max_unencoded):
          base64ed.append(b2a_base64(header[i:i+max_unencoded]))
--- 103,106 ----

Index: quopriMIME.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/quopriMIME.py,v
retrieving revision 1.4.2.1
retrieving revision 1.4.2.2
diff -C2 -d -r1.4.2.1 -r1.4.2.2
*** quopriMIME.py	4 Oct 2002 17:24:24 -0000	1.4.2.1
--- quopriMIME.py	21 Mar 2003 21:09:31 -0000	1.4.2.2
***************
*** 83,87 ****
      if not L:
          L.append(s.lstrip())
!     elif len(L[-1]) + len(s) < maxlen:
          L[-1] += extra + s
      else:
--- 83,87 ----
      if not L:
          L.append(s.lstrip())
!     elif len(L[-1]) + len(s) <= maxlen:
          L[-1] += extra + s
      else:
***************
*** 117,121 ****
  
      with each line wrapped safely at, at most, maxlinelen characters (defaults
!     to 76 characters).
  
      End-of-line characters (\\r, \\n, \\r\\n) will be automatically converted
--- 117,122 ----
  
      with each line wrapped safely at, at most, maxlinelen characters (defaults
!     to 76 characters).  If maxlinelen is None, the entire string is encoded in
!     one chunk with no splitting.
  
      End-of-line characters (\\r, \\n, \\r\\n) will be automatically converted
***************
*** 135,141 ****
  
      # Quopri encode each line, in encoded chunks no greater than maxlinelen in
!     # lenght, after the RFC chrome is added in.
      quoted = []
!     max_encoded = maxlinelen - len(charset) - MISC_LEN
  
      for c in header:
--- 136,146 ----
  
      # Quopri encode each line, in encoded chunks no greater than maxlinelen in
!     # length, after the RFC chrome is added in.
      quoted = []
!     if maxlinelen is None:
!         # An obnoxiously large number that's good enough
!         max_encoded = 100000
!     else:
!         max_encoded = maxlinelen - len(charset) - MISC_LEN - 1
  
      for c in header: