Mailing List Archive

python/dist/src/Lib/email Charset.py,NONE,1.7.2.1 Header.py,NONE,1.13.2.1 MIMEMultipart.py,NONE,1.3.2.1 MIMENonMultipart.py,NONE,1.2.2.1 _compat21.py,NONE,1.4.2.1 _compat22.py,NONE,1.4.2.1 base64MIME.py,NONE,1.5.2.1 quopriMIME.py,NONE,1.4.2.1 Encoders.py,
Update of /cvsroot/python/python/dist/src/Lib/email
In directory usw-pr-cvs1:/tmp/cvs-serv11766/Lib/email

Modified Files:
Tag: release22-maint
Encoders.py Errors.py Generator.py Iterators.py MIMEAudio.py
MIMEBase.py MIMEImage.py MIMEMessage.py MIMEText.py Message.py
Parser.py Utils.py __init__.py
Added Files:
Tag: release22-maint
Charset.py Header.py MIMEMultipart.py MIMENonMultipart.py
_compat21.py _compat22.py base64MIME.py quopriMIME.py
Log Message:
Backporting of email 2.4 from Python 2.3. Many newly added modules,
some updated modules, updated documentation, and updated tests. Note
that Lib/test/regrtest.py added test_email_codecs to the expected
skips for all platforms. Also note that test_email_codecs.py differs
slightly from its Python 2.3 counterpart due to the difference in
package location for TestSkipped.


--- NEW FILE: Charset.py ---
# Copyright (C) 2001,2002 Python Software Foundation
# Author: che@debian.org (Ben Gertzfield), barry@zope.com (Barry Warsaw)

from types import UnicodeType
from email.Encoders import encode_7or8bit
import email.base64MIME
import email.quopriMIME

def _isunicode(s):
return isinstance(s, UnicodeType)

# Python 2.2.1 and beyond has these symbols
try:
True, False
except NameError:
True = 1
False = 0



# Flags for types of header encodings
QP = 1 # Quoted-Printable
BASE64 = 2 # Base64
SHORTEST = 3 # the shorter of QP and base64, but only for headers

# In "=?charset?q?hello_world?=", the =?, ?q?, and ?= add up to 7
MISC_LEN = 7

DEFAULT_CHARSET = 'us-ascii'



# Defaults
CHARSETS = {
# input header enc body enc output conv
'iso-8859-1': (QP, QP, None),
'iso-8859-2': (QP, QP, None),
'us-ascii': (None, None, None),
'big5': (BASE64, BASE64, None),
'gb2312': (BASE64, BASE64, None),
'euc-jp': (BASE64, None, 'iso-2022-jp'),
'shift_jis': (BASE64, None, 'iso-2022-jp'),
'iso-2022-jp': (BASE64, None, None),
'koi8-r': (BASE64, BASE64, None),
'utf-8': (SHORTEST, BASE64, 'utf-8'),
}

# Aliases for other commonly-used names for character sets. Map
# them to the real ones used in email.
ALIASES = {
'latin_1': 'iso-8859-1',
'latin-1': 'iso-8859-1',
'ascii': 'us-ascii',
}

# Map charsets to their Unicode codec strings. Note that the Japanese
# examples included below do not (yet) come with Python! They are available
# from http://pseudo.grad.sccs.chukyo-u.ac.jp/~kajiyama/python/

# The Chinese and Korean codecs are available from SourceForge:
#
# http://sourceforge.net/projects/python-codecs/
#
# although you'll need to check them out of cvs since they haven't been file
# released yet. You might also try to use
#
# http://www.freshports.org/port-description.php3?port=6702
#
# if you can get logged in. AFAICT, both the Chinese and Korean codecs are
# fairly experimental at this point.
CODEC_MAP = {
'euc-jp': 'japanese.euc-jp',
'iso-2022-jp': 'japanese.iso-2022-jp',
'shift_jis': 'japanese.shift_jis',
'gb2132': 'eucgb2312_cn',
'big5': 'big5_tw',
'utf-8': 'utf-8',
# Hack: We don't want *any* conversion for stuff marked us-ascii, as all
# sorts of garbage might be sent to us in the guise of 7-bit us-ascii.
# Let that stuff pass through without conversion to/from Unicode.
'us-ascii': None,
}



# Convenience functions for extending the above mappings
def add_charset(charset, header_enc=None, body_enc=None, output_charset=None):
"""Add character set properties to the global registry.

charset is the input character set, and must be the canonical name of a
character set.

Optional header_enc and body_enc is either Charset.QP for
quoted-printable, Charset.BASE64 for base64 encoding, Charset.SHORTEST for
the shortest of qp or base64 encoding, or None for no encoding. SHORTEST
is only valid for header_enc. It describes how message headers and
message bodies in the input charset are to be encoded. Default is no
encoding.

Optional output_charset is the character set that the output should be
in. Conversions will proceed from input charset, to Unicode, to the
output charset when the method Charset.convert() is called. The default
is to output in the same character set as the input.

Both input_charset and output_charset must have Unicode codec entries in
the module's charset-to-codec mapping; use add_codec(charset, codecname)
to add codecs the module does not know about. See the codecs module's
documentation for more information.
"""
if body_enc == SHORTEST:
raise ValueError, 'SHORTEST not allowed for body_enc'
CHARSETS[charset] = (header_enc, body_enc, output_charset)


def add_alias(alias, canonical):
"""Add a character set alias.

alias is the alias name, e.g. latin-1
canonical is the character set's canonical name, e.g. iso-8859-1
"""
ALIASES[alias] = canonical


def add_codec(charset, codecname):
"""Add a codec that map characters in the given charset to/from Unicode.

charset is the canonical name of a character set. codecname is the name
of a Python codec, as appropriate for the second argument to the unicode()
built-in, or to the encode() method of a Unicode string.
"""
CODEC_MAP[charset] = codecname



class Charset:
"""Map character sets to their email properties.

This class provides information about the requirements imposed on email
for a specific character set. It also provides convenience routines for
converting between character sets, given the availability of the
applicable codecs. Given a character set, it will do its best to provide
information on how to use that character set in an email in an
RFC-compliant way.

Certain character sets must be encoded with quoted-printable or base64
when used in email headers or bodies. Certain character sets must be
converted outright, and are not allowed in email. Instances of this
module expose the following information about a character set:

input_charset: The initial character set specified. Common aliases
are converted to their `official' email names (e.g. latin_1
is converted to iso-8859-1). Defaults to 7-bit us-ascii.

header_encoding: If the character set must be encoded before it can be
used in an email header, this attribute will be set to
Charset.QP (for quoted-printable), Charset.BASE64 (for
base64 encoding), or Charset.SHORTEST for the shortest of
QP or BASE64 encoding. Otherwise, it will be None.

body_encoding: Same as header_encoding, but describes the encoding for the
mail message's body, which indeed may be different than the
header encoding. Charset.SHORTEST is not allowed for
body_encoding.

output_charset: Some character sets must be converted before the can be
used in email headers or bodies. If the input_charset is
one of them, this attribute will contain the name of the
charset output will be converted to. Otherwise, it will
be None.

input_codec: The name of the Python codec used to convert the
input_charset to Unicode. If no conversion codec is
necessary, this attribute will be None.

output_codec: The name of the Python codec used to convert Unicode
to the output_charset. If no conversion codec is necessary,
this attribute will have the same value as the input_codec.
"""
def __init__(self, input_charset=DEFAULT_CHARSET):
# Set the input charset after filtering through the aliases
self.input_charset = ALIASES.get(input_charset, input_charset)
# We can try to guess which encoding and conversion to use by the
# charset_map dictionary. Try that first, but let the user override
# it.
henc, benc, conv = CHARSETS.get(self.input_charset,
(SHORTEST, SHORTEST, None))
# Set the attributes, allowing the arguments to override the default.
self.header_encoding = henc
self.body_encoding = benc
self.output_charset = ALIASES.get(conv, conv)
# Now set the codecs. If one isn't defined for input_charset,
# guess and try a Unicode codec with the same name as input_codec.
self.input_codec = CODEC_MAP.get(self.input_charset,
self.input_charset)
self.output_codec = CODEC_MAP.get(self.output_charset,
self.input_codec)

def __str__(self):
return self.input_charset.lower()

def __eq__(self, other):
return str(self) == str(other).lower()

def __ne__(self, other):
return not self.__eq__(other)

def get_body_encoding(self):
"""Return the content-transfer-encoding used for body encoding.

This is either the string `quoted-printable' or `base64' depending on
the encoding used, or it is a function in which case you should call
the function with a single argument, the Message object being
encoded. The function should then set the Content-Transfer-Encoding
header itself to whatever is appropriate.

Returns "quoted-printable" if self.body_encoding is QP.
Returns "base64" if self.body_encoding is BASE64.
Returns "7bit" otherwise.
"""
assert self.body_encoding <> SHORTEST
if self.body_encoding == QP:
return 'quoted-printable'
elif self.body_encoding == BASE64:
return 'base64'
else:
return encode_7or8bit

def convert(self, s):
"""Convert a string from the input_codec to the output_codec."""
if self.input_codec <> self.output_codec:
return unicode(s, self.input_codec).encode(self.output_codec)
else:
return s

def to_splittable(self, s):
"""Convert a possibly multibyte string to a safely splittable format.

Uses the input_codec to try and convert the string to Unicode, so it
can be safely split on character boundaries (even for multibyte
characters).

Returns the string as-is if it isn't known how to convert it to
Unicode with the input_charset.

Characters that could not be converted to Unicode will be replaced
with the Unicode replacement character U+FFFD.
"""
if _isunicode(s) or self.input_codec is None:
return s
try:
return unicode(s, self.input_codec, 'replace')
except LookupError:
# Input codec not installed on system, so return the original
# string unchanged.
return s

def from_splittable(self, ustr, to_output=True):
"""Convert a splittable string back into an encoded string.

Uses the proper codec to try and convert the string from Unicode back
into an encoded format. Return the string as-is if it is not Unicode,
or if it could not be converted from Unicode.

Characters that could not be converted from Unicode will be replaced
with an appropriate character (usually '?').

If to_output is True (the default), uses output_codec to convert to an
encoded format. If to_output is False, uses input_codec.
"""
if to_output:
codec = self.output_codec
else:
codec = self.input_codec
if not _isunicode(ustr) or codec is None:
return ustr
try:
return ustr.encode(codec, 'replace')
except LookupError:
# Output codec not installed
return ustr

def get_output_charset(self):
"""Return the output character set.

This is self.output_charset if that is not None, otherwise it is
self.input_charset.
"""
return self.output_charset or self.input_charset

def encoded_header_len(self, s):
"""Return the length of the encoded header string."""
cset = self.get_output_charset()
# The len(s) of a 7bit encoding is len(s)
if self.header_encoding == BASE64:
return email.base64MIME.base64_len(s) + len(cset) + MISC_LEN
elif self.header_encoding == QP:
return email.quopriMIME.header_quopri_len(s) + len(cset) + MISC_LEN
elif self.header_encoding == SHORTEST:
lenb64 = email.base64MIME.base64_len(s)
lenqp = email.quopriMIME.header_quopri_len(s)
return min(lenb64, lenqp) + len(cset) + MISC_LEN
else:
return len(s)

def header_encode(self, s, convert=False):
"""Header-encode a string, optionally converting it to output_charset.

If convert is True, the string will be converted from the input
charset to the output charset automatically. This is not useful for
multibyte character sets, which have line length issues (multibyte
characters must be split on a character, not a byte boundary); use the
high-level Header class to deal with these issues. convert defaults
to False.

The type of encoding (base64 or quoted-printable) will be based on
self.header_encoding.
"""
cset = self.get_output_charset()
if convert:
s = self.convert(s)
# 7bit/8bit encodings return the string unchanged (modulo conversions)
if self.header_encoding == BASE64:
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)
lenqp = email.quopriMIME.header_quopri_len(s)
if lenb64 < lenqp:
return email.base64MIME.header_encode(s, cset)
else:
return email.quopriMIME.header_encode(s, cset)
else:
return s

def body_encode(self, s, convert=True):
"""Body-encode a string and convert it to output_charset.

If convert is True (the default), the string will be converted from
the input charset to output charset automatically. Unlike
header_encode(), there are no issues with byte boundaries and
multibyte charsets in email bodies, so this is usually pretty safe.

The type of encoding (base64 or quoted-printable) will be based on
self.body_encoding.
"""
if convert:
s = self.convert(s)
# 7bit/8bit encodings return the string unchanged (module conversions)
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:
return s

--- NEW FILE: Header.py ---
# Copyright (C) 2002 Python Software Foundation
# Author: che@debian.org (Ben Gertzfield), barry@zope.com (Barry Warsaw)

"""Header encoding and decoding functionality."""

import re
from types import StringType, UnicodeType

import email.quopriMIME
import email.base64MIME
from email.Charset import Charset

try:
from email._compat22 import _floordiv
except SyntaxError:
# Python 2.1 spells integer division differently
from email._compat21 import _floordiv

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

CRLFSPACE = '\r\n '
CRLF = '\r\n'
NL = '\n'
SPACE8 = ' ' * 8
EMPTYSTRING = ''

MAXLINELEN = 76

ENCODE = 1
DECODE = 2

USASCII = Charset('us-ascii')
UTF8 = Charset('utf-8')

# Match encoded-word strings in the form =?charset?q?Hello_World?=
ecre = re.compile(r'''
=\? # literal =?
(?P<charset>[^?]*?) # non-greedy up to the next ? is the charset
\? # literal ?
(?P<encoding>[qb]) # either a "q" or a "b", case insensitive
\? # literal ?
(?P<encoded>.*?) # non-greedy up to the next ?= is the encoded string
\?= # literal ?=
''', re.VERBOSE | re.IGNORECASE)



# Helpers
_max_append = email.quopriMIME._max_append



def decode_header(header):
"""Decode a message header value without converting charset.

Returns a list of (decoded_string, charset) pairs containing each of the
decoded parts of the header. Charset is None for non-encoded parts of the
header, otherwise a lower-case string containing the name of the character
set specified in the encoded string.
"""
# If no encoding, just return the header
header = str(header)
if not ecre.search(header):
return [(header, None)]
decoded = []
dec = ''
for line in header.splitlines():
# This line might not have an encoding in it
if not ecre.search(line):
decoded.append((line, None))
continue
parts = ecre.split(line)
while parts:
unenc = parts.pop(0).strip()
if unenc:
# 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))
if parts:
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

if decoded and decoded[-1][1] == charset:
decoded[-1] = (decoded[-1][0] + dec, decoded[-1][1])
else:
decoded.append((dec, charset))
del parts[0:3]
return decoded



def make_header(decoded_seq, maxlinelen=None, header_name=None,
continuation_ws=' '):
"""Create a Header from a sequence of pairs as returned by decode_header()

decode_header() takes a header value string and returns a sequence of
pairs of the format (decoded_string, charset) where charset is the string
name of the character set.

This function takes one of those sequence of pairs and returns a Header
instance. Optional maxlinelen, header_name, and continuation_ws are as in
the Header constructor.
"""
h = Header(maxlinelen=maxlinelen, header_name=header_name,
continuation_ws=continuation_ws)
for s, charset in decoded_seq:
# None means us-ascii but we can simply pass it on to h.append()
if charset is not None and not isinstance(charset, Charset):
charset = Charset(charset)
h.append(s, charset)
return h



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.

Optional s is the initial header value. If None, the initial header
value is not set. You can later append to the header with .append()
method calls. s may be a byte string or a Unicode string, but see the
.append() documentation for semantics.

Optional charset serves two purposes: it has the same meaning as the
charset argument to the .append() method. It also sets the default
character set for all subsequent .append() calls that omit the charset
argument. If charset is not provided in the constructor, the us-ascii
charset is used both as s's initial charset and as the default for
subsequent .append() calls.

The maximum line length can be specified explicit via maxlinelen. For
splitting the first line to a shorter value (to account for the field
header which isn't included in s, e.g. `Subject') pass in the name of
the field in header_name. The default maxlinelen is 76.

continuation_ws must be RFC 2822 compliant folding whitespace (usually
either a space or a hard tab) which will be prepended to continuation
lines.
"""
if charset is None:
charset = USASCII
self._charset = charset
self._continuation_ws = continuation_ws
cws_expanded_len = len(continuation_ws.replace('\t', SPACE8))
# BAW: I believe `chunks' and `maxlinelen' should be non-public.
self._chunks = []
if s is not None:
self.append(s, charset)
if maxlinelen is None:
maxlinelen = MAXLINELEN
if header_name is None:
# We don't know anything about the field header so the first line
# is the same length as subsequent lines.
self._firstlinelen = maxlinelen
else:
# The first line should be shorter to take into account the field
# header. Also subtract off 2 extra for the colon and space.
self._firstlinelen = maxlinelen - len(header_name) - 2
# Second and subsequent lines should subtract off the length in
# columns of the continuation whitespace prefix.
self._maxlinelen = maxlinelen - cws_expanded_len

def __str__(self):
"""A synonym for self.encode()."""
return self.encode()

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
# have or explicitly disable <, <=, >, >= operators?
def __eq__(self, other):
# other may be a Header or a string. Both are fine so coerce
# ourselves to a string, swap the args and do another comparison.
return other == self.encode()

def __ne__(self, other):
return not self == other

def append(self, s, charset=None):
"""Append a string to the MIME header.

Optional charset, if given, should be a Charset instance or the name
of a character set (which will be converted to a Charset instance). A
value of None (the default) means that the charset given in the
constructor is used.

s may be a byte string or a Unicode string. If it is a byte string
(i.e. isinstance(s, StringType) is true), then charset is the encoding
of that byte string, and a UnicodeError will be raised if the string
cannot be decoded with that charset. If s is a Unicode string, then
charset is a hint specifying the character set of the characters in
the string. In this case, when producing an RFC 2822 compliant header
using RFC 2047 rules, the Unicode string will be encoded using the
following charsets in order: us-ascii, the charset hint, utf-8. The
first character set not to provoke a UnicodeError is used.
"""
if charset is None:
charset = self._charset
elif not isinstance(charset, Charset):
charset = Charset(charset)
# Normalize and check the string
if isinstance(s, StringType):
# Possibly raise UnicodeError if it can't e encoded
unicode(s, charset.get_output_charset())
elif isinstance(s, UnicodeType):
# Convert Unicode to byte string for later concatenation
for charset in USASCII, charset, UTF8:
try:
s = s.encode(charset.get_output_charset())
break
except UnicodeError:
pass
else:
assert False, 'Could not encode to utf-8'
self._chunks.append((s, charset))

def _split(self, s, charset, firstline=False):
# Split up a header safely for use with encode_chunks. BAW: this
# appears to be a private convenience method.
splittable = charset.to_splittable(s)
encoded = charset.from_splittable(splittable)
elen = charset.encoded_header_len(encoded)

if elen <= self._maxlinelen:
return [(encoded, charset)]
# BAW: I'm not sure what the right test here is. What we're trying to
# do is be faithful to RFC 2822's recommendation that ($2.2.3):
#
# "Note: Though structured field bodies are defined in such a way that
# folding can take place between many of the lexical tokens (and even
# within some of the lexical tokens), folding SHOULD be limited to
# placing the CRLF at higher-level syntactic breaks."
#
# For now, I can only imagine doing this when the charset is us-ascii,
# 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):
"""MIME-encode a header with many different charsets and/or encodings.

Given a list of pairs (string, charset), return a MIME-encoded string
suitable for use in a header field. Each pair may have different
charsets and/or encodings, and the resulting header will accurately
reflect each setting.

Each encoding can be email.Utils.QP (quoted-printable, for ASCII-like
character sets like iso-8859-1), email.Utils.BASE64 (Base64, for
non-ASCII like character sets like KOI8-R and iso-2022-jp), or None
(no encoding).

Each pair will be represented on a separate line; the resulting string
will be in the format:

"=?charset1?q?Mar=EDa_Gonz=E1lez_Alonso?=\n
=?charset2?b?SvxyZ2VuIEL2aW5n?="
"""
chunks = []
for header, charset in self._chunks:
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.

There are many issues involved in converting a given string for use in
an email header. Only certain character sets are readable in most
email clients, and as header strings can only contain a subset of
7-bit ASCII, care must be taken to properly convert and encode (with
Base64 or quoted-printable) header strings. In addition, there is a
75-character length limit on any given encoded header field, so
line-wrapping must be performed, even with double-byte character sets.

This method will do its best to convert the string to the correct
character set used in email, and encode and line wrap it safely with
the appropriate scheme for that character set.

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)
self._chunks = newchunks
return self._encode_chunks()

--- NEW FILE: MIMEMultipart.py ---
# Copyright (C) 2002 Python Software Foundation
# Author: barry@zope.com (Barry Warsaw)

"""Base class for MIME multipart/* type messages.
"""

from email import MIMEBase



class MIMEMultipart(MIMEBase.MIMEBase):
"""Base class for MIME multipart/* type messages."""

def __init__(self, _subtype='mixed', boundary=None, *_subparts, **_params):
"""Creates a multipart/* type message.

By default, creates a multipart/mixed message, with proper
Content-Type and MIME-Version headers.

_subtype is the subtype of the multipart content type, defaulting to
`mixed'.

boundary is the multipart boundary string. By default it is
calculated as needed.

_subparts is a sequence of initial subparts for the payload. It
must be possible to convert this sequence to a list. You can always
attach new subparts to the message by using the attach() method.

Additional parameters for the Content-Type header are taken from the
keyword arguments (or passed into the _params argument).
"""
MIMEBase.MIMEBase.__init__(self, 'multipart', _subtype, **_params)
if _subparts:
self.attach(*list(_subparts))
if boundary:
self.set_boundary(boundary)

--- NEW FILE: MIMENonMultipart.py ---
# Copyright (C) 2002 Python Software Foundation
# Author: barry@zope.com (Barry Warsaw)

"""Base class for MIME type messages that are not multipart.
"""

from email import Errors
from email import MIMEBase



class MIMENonMultipart(MIMEBase.MIMEBase):
"""Base class for MIME multipart/* type messages."""

__pychecker__ = 'unusednames=payload'

def attach(self, payload):
# The public API prohibits attaching multiple subparts to MIMEBase
# derived subtypes since none of them are, by definition, of content
# type multipart/*
raise Errors.MultipartConversionError(
'Cannot attach additional subparts to non-multipart/*')

del __pychecker__

--- NEW FILE: _compat21.py ---
# Copyright (C) 2002 Python Software Foundation
# Author: barry@zope.com

"""Module containing compatibility functions for Python 2.1.
"""

from cStringIO import StringIO
from types import StringType, UnicodeType



# This function will become a method of the Message class
def walk(self):
"""Walk over the message tree, yielding each subpart.

The walk is performed in depth-first order. This method is a
generator.
"""
parts = []
parts.append(self)
if self.is_multipart():
for subpart in self.get_payload():
parts.extend(subpart.walk())
return parts


# Python 2.2 spells floor division //
def _floordiv(i, j):
"""Do a floor division, i/j."""
return i / j


def _isstring(obj):
return isinstance(obj, StringType) or isinstance(obj, UnicodeType)



# 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():
lines.append(line)
return lines


def typed_subpart_iterator(msg, maintype='text', subtype=None):
"""Iterate over the subparts with a given MIME type.

Use `maintype' as the main MIME type to match against; this defaults to
"text". Optional `subtype' is the MIME subtype to match against; if
omitted, only the main type is matched.
"""
parts = []
for subpart in msg.walk():
if subpart.get_main_type('text') == maintype:
if subtype is None or subpart.get_subtype('plain') == subtype:
parts.append(subpart)
return parts

--- NEW FILE: _compat22.py ---
# Copyright (C) 2002 Python Software Foundation
# Author: barry@zope.com

"""Module containing compatibility functions for Python 2.1.
"""

from __future__ import generators
from __future__ import division
from cStringIO import StringIO
from types import StringTypes



# This function will become a method of the Message class
def walk(self):
"""Walk over the message tree, yielding each subpart.

The walk is performed in depth-first order. This method is a
generator.
"""
yield self
if self.is_multipart():
for subpart in self.get_payload():
for subsubpart in subpart.walk():
yield subsubpart


# Python 2.2 spells floor division //
def _floordiv(i, j):
"""Do a floor division, i/j."""
return i // j


def _isstring(obj):
return isinstance(obj, StringTypes)



# 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):
yield line


def typed_subpart_iterator(msg, maintype='text', subtype=None):
"""Iterate over the subparts with a given MIME type.

Use `maintype' as the main MIME type to match against; this defaults to
"text". Optional `subtype' is the MIME subtype to match against; if
omitted, only the main type is matched.
"""
for subpart in msg.walk():
if subpart.get_main_type('text') == maintype:
if subtype is None or subpart.get_subtype('plain') == subtype:
yield subpart

--- NEW FILE: base64MIME.py ---
# Copyright (C) 2002 Python Software Foundation
# Author: che@debian.org (Ben Gertzfield)

"""Base64 content transfer encoding per RFCs 2045-2047.

This module handles the content transfer encoding method defined in RFC 2045
to encode arbitrary 8-bit data using the three 8-bit bytes in four 7-bit
characters encoding known as Base64.

It is used in the MIME standards for email to attach images, audio, and text
using some 8-bit character sets to messages.

This module provides an interface to encode and decode both headers and bodies
with Base64 encoding.

RFC 2045 defines a method for including character set information in an
`encoded-word' in a header. This method is commonly used for 8-bit real names
in To:, From:, Cc:, etc. fields, as well as Subject: lines.

This module does not do the line wrapping or end-of-line character conversion
necessary for proper internationalized headers; it only does dumb encoding and
decoding. To deal with the various line wrapping issues, use the email.Header
module.
"""

import re
from binascii import b2a_base64, a2b_base64
from email.Utils import fix_eols

try:
from email._compat22 import _floordiv
except SyntaxError:
# Python 2.1 spells integer division differently
from email._compat21 import _floordiv


CRLF = '\r\n'
NL = '\n'
EMPTYSTRING = ''

# See also Charset.py
MISC_LEN = 7

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



# Helpers
def base64_len(s):
"""Return the length of s when it is encoded with base64."""
groups_of_3, leftover = divmod(len(s), 3)
# 4 bytes out for each 3 bytes (or nonzero fraction thereof) in.
# Thanks, Tim!
n = groups_of_3 * 4
if leftover:
n += 4
return n



def header_encode(header, charset='iso-8859-1', keep_eols=False,
maxlinelen=76, eol=NL):
"""Encode a single header line with Base64 encoding in a given charset.

Defined in RFC 2045, this Base64 encoding is identical to normal Base64
encoding, except that each line must be intelligently wrapped (respecting
the Base64 encoding), and subsequent lines must start with a space.

charset names the character set to use to encode the header. It defaults
to iso-8859-1.

End-of-line characters (\\r, \\n, \\r\\n) will be automatically converted
to the canonical email line separator \\r\\n unless the keep_eols
parameter is True (the default is False).

Each line of the header will be terminated in the value of eol, which
defaults to "\\n". Set this to "\\r\\n" if you are using the result of
this function directly in email.

The resulting string will be in the form:

"=?charset?b?WW/5ciBtYXp66XLrIHf8eiBhIGhhbXBzdGHuciBBIFlv+XIgbWF6euly?=\\n
=?charset?b?6yB3/HogYSBoYW1wc3Rh7nIgQkMgWW/5ciBtYXp66XLrIHf8eiBhIGhh?="

with each line wrapped at, at most, maxlinelen characters (defaults to 76
characters).
"""
# Return empty headers unchanged
if not header:
return header

if not keep_eols:
header = fix_eols(header)

# Base64 encode each line, in encoded chunks no greater than maxlinelen in
# length, after the RFC chrome is added in.
base64ed = []
max_encoded = maxlinelen - len(charset) - MISC_LEN
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]))

# Now add the RFC chrome to each encoded chunk
lines = []
for line in base64ed:
# Ignore the last character of each line if it is a newline
if line.endswith(NL):
line = line[:-1]
# Add the chrome
lines.append('=?%s?b?%s?=' % (charset, line))
# Glue the lines together and return it. BAW: should we be able to
# specify the leading whitespace in the joiner?
joiner = eol + ' '
return joiner.join(lines)



def encode(s, binary=True, maxlinelen=76, eol=NL):
"""Encode a string with base64.

Each line will be wrapped at, at most, maxlinelen characters (defaults to
76 characters).

If binary is False, end-of-line characters will be converted to the
canonical email end-of-line sequence \\r\\n. Otherwise they will be left
verbatim (this is the default).

Each line of encoded text will end with eol, which defaults to "\\n". Set
this to "\r\n" if you will be using the result of this function directly
in an email.
"""
if not s:
return s

if not binary:
s = fix_eols(s)

encvec = []
max_unencoded = _floordiv(maxlinelen * 3, 4)
for i in range(0, len(s), max_unencoded):
# BAW: should encode() inherit b2a_base64()'s dubious behavior in
# adding a newline to the encoded string?
enc = b2a_base64(s[i:i + max_unencoded])
if enc.endswith(NL) and eol <> NL:
enc = enc[:-1] + eol
encvec.append(enc)
return EMPTYSTRING.join(encvec)


# For convenience and backwards compatibility w/ standard base64 module
body_encode = encode
encodestring = encode



def decode(s, convert_eols=None):
"""Decode a raw base64 string.

If convert_eols is set to a string value, all canonical email linefeeds,
e.g. "\\r\\n", in the decoded text will be converted to the value of
convert_eols. os.linesep is a good choice for convert_eols if you are
decoding a text attachment.

This function does not parse a full MIME header value encoded with
base64 (like =?iso-8895-1?b?bmloISBuaWgh?=) -- please use the high
level email.Header class for that functionality.
"""
if not s:
return s

dec = a2b_base64(s)
if convert_eols:
return dec.replace(CRLF, convert_eols)
return dec


# For convenience and backwards compatibility w/ standard base64 module
body_decode = decode
decodestring = decode

--- NEW FILE: quopriMIME.py ---
# Copyright (C) 2001,2002 Python Software Foundation
# Author: che@debian.org (Ben Gertzfield)

"""Quoted-printable content transfer encoding per RFCs 2045-2047.

This module handles the content transfer encoding method defined in RFC 2045
to encode US ASCII-like 8-bit data called `quoted-printable'. It is used to
safely encode text that is in a character set similar to the 7-bit US ASCII
character set, but that includes some 8-bit characters that are normally not
allowed in email bodies or headers.

Quoted-printable is very space-inefficient for encoding binary files; use the
email.base64MIME module for that instead.

This module provides an interface to encode and decode both headers and bodies
with quoted-printable encoding.

RFC 2045 defines a method for including character set information in an
`encoded-word' in a header. This method is commonly used for 8-bit real names
in To:/From:/Cc: etc. fields, as well as Subject: lines.

This module does not do the line wrapping or end-of-line character
conversion necessary for proper internationalized headers; it only
does dumb encoding and decoding. To deal with the various line
wrapping issues, use the email.Header module.
"""

import re
from string import hexdigits
from email.Utils import fix_eols

CRLF = '\r\n'
NL = '\n'

# See also Charset.py
MISC_LEN = 7

hqre = re.compile(r'[^-a-zA-Z0-9!*+/ ]')
bqre = re.compile(r'[^ !-<>-~\t]')

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



# Helpers
def header_quopri_check(c):
"""Return True if the character should be escaped with header quopri."""
return hqre.match(c) and True


def body_quopri_check(c):
"""Return True if the character should be escaped with body quopri."""
return bqre.match(c) and True


def header_quopri_len(s):
"""Return the length of str when it is encoded with header quopri."""
count = 0
for c in s:
if hqre.match(c):
count += 3
else:
count += 1
return count


def body_quopri_len(str):
"""Return the length of str when it is encoded with body quopri."""
count = 0
for c in str:
if bqre.match(c):
count += 3
else:
count += 1
return count


def _max_append(L, s, maxlen, extra=''):
if not L:
L.append(s.lstrip())
elif len(L[-1]) + len(s) < maxlen:
L[-1] += extra + s
else:
L.append(s.lstrip())


def unquote(s):
"""Turn a string in the form =AB to the ASCII character with value 0xab"""
return chr(int(s[1:3], 16))


def quote(c):
return "=%02X" % ord(c)



def header_encode(header, charset="iso-8859-1", keep_eols=False,
maxlinelen=76, eol=NL):
"""Encode a single header line with quoted-printable (like) encoding.

Defined in RFC 2045, this `Q' encoding is similar to quoted-printable, but
used specifically for email header fields to allow charsets with mostly 7
bit characters (and some 8 bit) to remain more or less readable in non-RFC
2045 aware mail clients.

charset names the character set to use to encode the header. It defaults
to iso-8859-1.

The resulting string will be in the form:

"=?charset?q?I_f=E2rt_in_your_g=E8n=E8ral_dire=E7tion?\\n
=?charset?q?Silly_=C8nglish_Kn=EEghts?="

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
to the canonical email line separator \\r\\n unless the keep_eols
parameter is True (the default is False).

Each line of the header will be terminated in the value of eol, which
defaults to "\\n". Set this to "\\r\\n" if you are using the result of
this function directly in email.
"""
# Return empty headers unchanged
if not header:
return header

if not keep_eols:
header = fix_eols(header)

# 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:
# Space may be represented as _ instead of =20 for readability
if c == ' ':
_max_append(quoted, '_', max_encoded)
# These characters can be included verbatim
elif not hqre.match(c):
_max_append(quoted, c, max_encoded)
# Otherwise, replace with hex value like =E2
else:
_max_append(quoted, "=%02X" % ord(c), max_encoded)

# Now add the RFC chrome to each encoded chunk and glue the chunks
# together. BAW: should we be able to specify the leading whitespace in
# the joiner?
joiner = eol + ' '
return joiner.join(['=?%s?q?%s?=' % (charset, line) for line in quoted])



def encode(body, binary=False, maxlinelen=76, eol=NL):
"""Encode with quoted-printable, wrapping at maxlinelen characters.

If binary is False (the default), end-of-line characters will be converted
to the canonical email end-of-line sequence \\r\\n. Otherwise they will
be left verbatim.

Each line of encoded text will end with eol, which defaults to "\\n". Set
this to "\\r\\n" if you will be using the result of this function directly
in an email.

Each line will be wrapped at, at most, maxlinelen characters (defaults to
76 characters). Long lines will have the `soft linefeed' quoted-printable
character "=" appended to them, so the decoded text will be identical to
the original text.
"""
if not body:
return body

if not binary:
body = fix_eols(body)

# BAW: We're accumulating the body text by string concatenation. That
# can't be very efficient, but I don't have time now to rewrite it. It
# just feels like this algorithm could be more efficient.
encoded_body = ''
lineno = -1
# Preserve line endings here so we can check later to see an eol needs to
# be added to the output later.
lines = body.splitlines(1)
for line in lines:
# But strip off line-endings for processing this line.
if line.endswith(CRLF):
line = line[:-2]
elif line[-1] in CRLF:
line = line[:-1]

lineno += 1
encoded_line = ''
prev = None
linelen = len(line)
# Now we need to examine every character to see if it needs to be
# quopri encoded. BAW: again, string concatenation is inefficient.
for j in range(linelen):
c = line[j]
prev = c
if bqre.match(c):
c = quote(c)
elif j+1 == linelen:
# Check for whitespace at end of line; special case
if c not in ' \t':
encoded_line += c
prev = c
continue
# Check to see to see if the line has reached its maximum length
if len(encoded_line) + len(c) >= maxlinelen:
encoded_body += encoded_line + '=' + eol
encoded_line = ''
encoded_line += c
# Now at end of line..
if prev and prev in ' \t':
# Special case for whitespace at end of file
if lineno + 1 == len(lines):
prev = quote(prev)
if len(encoded_line) + len(prev) > maxlinelen:
encoded_body += encoded_line + '=' + eol + prev
else:
encoded_body += encoded_line + prev
# Just normal whitespace at end of line
else:
encoded_body += encoded_line + prev + '=' + eol
encoded_line = ''
# Now look at the line we just finished and it has a line ending, we
# need to add eol to the end of the line.
if lines[lineno].endswith(CRLF) or lines[lineno][-1] in CRLF:
encoded_body += encoded_line + eol
else:
encoded_body += encoded_line
encoded_line = ''
return encoded_body


# For convenience and backwards compatibility w/ standard base64 module
body_encode = encode
encodestring = encode



# BAW: I'm not sure if the intent was for the signature of this function to be
# the same as base64MIME.decode() or not...
def decode(encoded, eol=NL):
"""Decode a quoted-printable string.

Lines are separated with eol, which defaults to \\n.
"""
if not encoded:
return encoded
# BAW: see comment in encode() above. Again, we're building up the
# decoded string with string concatenation, which could be done much more
# efficiently.
decoded = ''

for line in encoded.splitlines():
line = line.rstrip()
if not line:
decoded += eol
continue

i = 0
n = len(line)
while i < n:
c = line[i]
if c <> '=':
decoded += c
i += 1
# Otherwise, c == "=". Are we at the end of the line? If so, add
# a soft line break.
elif i+1 == n:
i += 1
continue
# Decode if in form =AB
elif i+2 < n and line[i+1] in hexdigits and line[i+2] in hexdigits:
decoded += unquote(line[i:i+3])
i += 3
# Otherwise, not in form =AB, pass literally
else:
decoded += c
i += 1

if i == n:
decoded += eol
# Special case if original string did not end with eol
if not encoded.endswith(eol) and decoded.endswith(eol):
decoded = decoded[:-1]
return decoded


# For convenience and backwards compatibility w/ standard base64 module
body_decode = decode
decodestring = decode



def _unquote_match(match):
"""Turn a match in the form =AB to the ASCII character with value 0xab"""
s = match.group(0)
return unquote(s)


# Header decoding is done a bit differently
def header_decode(s):
"""Decode a string encoded with RFC 2045 MIME header `Q' encoding.

This function does not parse a full MIME header value encoded with
quoted-printable (like =?iso-8895-1?q?Hello_World?=) -- please use
the high level email.Header class for that functionality.
"""
s = s.replace('_', ' ')
return re.sub(r'=\w{2}', _unquote_match, s)

Index: Encoders.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/Encoders.py,v
retrieving revision 1.4
retrieving revision 1.4.10.1
diff -C2 -d -r1.4 -r1.4.10.1
*** Encoders.py 4 Oct 2001 17:05:11 -0000 1.4
--- Encoders.py 4 Oct 2002 17:24:23 -0000 1.4.10.1
***************
*** 1,3 ****
# Author: barry@zope.com (Barry Warsaw)

--- 1,3 ----
# Author: barry@zope.com (Barry Warsaw)

***************
*** 6,16 ****

import base64
- from quopri import encodestring as _encodestring



# Helpers
! def _qencode(s):
! return _encodestring(s, quotetabs=1)


--- 6,37 ----

import base64



# Helpers
! try:
! from quopri import encodestring as _encodestring
!
! def _qencode(s):
! enc = _encodestring(s, quotetabs=1)
! # Must encode spaces, which quopri.encodestring() doesn't do
! return enc.replace(' ', '=20')
! except ImportError:
! # Python 2.1 doesn't have quopri.encodestring()
! from cStringIO import StringIO
! import quopri as _quopri
!
! def _qencode(s):
! if not s:
! return s
! hasnewline = (s[-1] == '\n')
! infp = StringIO(s)
! outfp = StringIO()
! _quopri.encode(infp, outfp, quotetabs=1)
! # Python 2.x's encode() doesn't encode spaces even when quotetabs==1
! value = outfp.getvalue().replace(' ', '=20')
! if not hasnewline and value[-1] == '\n':
! return value[:-1]
! return value


***************
*** 31,35 ****
"""Encode the message's payload in Base64.

! Also, add an appropriate Content-Transfer-Encoding: header.
"""
orig = msg.get_payload()
--- 52,56 ----
"""Encode the message's payload in Base64.

! Also, add an appropriate Content-Transfer-Encoding header.
"""
orig = msg.get_payload()
***************
*** 41,47 ****

def encode_quopri(msg):
! """Encode the message's payload in Quoted-Printable.

! Also, add an appropriate Content-Transfer-Encoding: header.
"""
orig = msg.get_payload()
--- 62,68 ----

def encode_quopri(msg):
! """Encode the message's payload in quoted-printable.

! Also, add an appropriate Content-Transfer-Encoding header.
"""
orig = msg.get_payload()
***************
*** 53,58 ****

def encode_7or8bit(msg):
! """Set the Content-Transfer-Encoding: header to 7bit or 8bit."""
orig = msg.get_payload()
# We play a trick to make this go fast. If encoding to ASCII succeeds, we
# know the data must be 7bit, otherwise treat it as 8bit.
--- 74,83 ----

def encode_7or8bit(msg):
! """Set the Content-Transfer-Encoding header to 7bit or 8bit."""
orig = msg.get_payload()
+ if orig is None:
+ # There's no payload. For backwards compatibility we use 7bit
+ msg['Content-Transfer-Encoding'] = '7bit'
+ return
# We play a trick to make this go fast. If encoding to ASCII succeeds, we
# know the data must be 7bit, otherwise treat it as 8bit.

Index: Errors.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/Errors.py,v
retrieving revision 1.3
retrieving revision 1.3.10.1
diff -C2 -d -r1.3 -r1.3.10.1
*** Errors.py 4 Oct 2001 17:05:11 -0000 1.3
--- Errors.py 4 Oct 2002 17:24:23 -0000 1.3.10.1
***************
*** 1,3 ****
# Author: barry@zope.com (Barry Warsaw)

--- 1,3 ----
# Author: barry@zope.com (Barry Warsaw)

***************
*** 8,12 ****

class MessageError(Exception):
! """Base class for errors in this module."""


--- 8,12 ----

class MessageError(Exception):
! """Base class for errors in the email package."""



Index: Generator.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/Generator.py,v
retrieving revision 1.6.10.1
retrieving revision 1.6.10.2
diff -C2 -d -r1.6.10.1 -r1.6.10.2
*** Generator.py 22 Mar 2002 16:21:56 -0000 1.6.10.1
--- Generator.py 4 Oct 2002 17:24:23 -0000 1.6.10.2
***************
*** 1,3 ****
# Author: barry@zope.com (Barry Warsaw)

--- 1,3 ----
# Author: barry@zope.com (Barry Warsaw)

***************
*** 9,18 ****
import random

! from types import ListType, StringType
from cStringIO import StringIO

! # Intrapackage imports
! import Message
! import Errors

EMPTYSTRING = ''
--- 9,27 ----
import random

! from types import ListType
from cStringIO import StringIO

! from email.Header import Header
!
! try:
! from email._compat22 import _isstring
! except SyntaxError:
! from email._compat21 import _isstring
!
! try:
! True, False
! except NameError:
! True = 1
! False = 0

EMPTYSTRING = ''
***************
*** 39,43 ****
#

! def __init__(self, outfp, mangle_from_=1, maxheaderlen=78):
"""Create the generator for message flattening.

--- 48,52 ----
#

! def __init__(self, outfp, mangle_from_=True, maxheaderlen=78):
"""Create the generator for message flattening.

***************
*** 45,50 ****
must have a write() method.

! Optional mangle_from_ is a flag that, when true, escapes From_ lines
! in the body of the message by putting a `>' in front of them.

Optional maxheaderlen specifies the longest length for a non-continued
--- 54,60 ----
must have a write() method.

! Optional mangle_from_ is a flag that, when True (the default), escapes
! From_ lines in the body of the message by putting a `>' in front of
! them.

Optional maxheaderlen specifies the longest length for a non-continued
***************
*** 58,62 ****
self._fp = outfp
self._mangle_from_ = mangle_from_
- self.__first = 1
self.__maxheaderlen = maxheaderlen

--- 68,71 ----
***************
*** 65,69 ****
self._fp.write(s)

! def __call__(self, msg, unixfrom=0):
"""Print the message object tree rooted at msg to the output file
specified when the Generator instance was created.
--- 74,78 ----
self._fp.write(s)

! def flatten(self, msg, unixfrom=False):
"""Print the message object tree rooted at msg to the output file
specified when the Generator instance was created.
***************
*** 72,76 ****
before the first object in the message tree. If the original message
has no From_ delimiter, a `standard' one is crafted. By default, this
! is 0 to inhibit the printing of any From_ delimiter.

Note that for subobjects, no From_ line is printed.
--- 81,85 ----
before the first object in the message tree. If the original message
has no From_ delimiter, a `standard' one is crafted. By default, this
! is False to inhibit the printing of any From_ delimiter.

Note that for subobjects, no From_ line is printed.
***************
*** 83,86 ****
--- 92,102 ----
self._write(msg)

+ # For backwards compatibility, but this is slower
+ __call__ = flatten
+
+ def clone(self, fp):
+ """Clone this generator with the exact same options."""
+ return self.__class__(fp, self._mangle_from_, self.__maxheaderlen)
+
#
# Protected interface - undocumented ;/
***************
*** 116,136 ****
def _dispatch(self, msg):
# Get the Content-Type: for the message, then try to dispatch to
! # self._handle_maintype_subtype(). If there's no handler for the full
! # MIME type, then dispatch to self._handle_maintype(). If that's
! # missing too, then dispatch to self._writeBody().
! ctype = msg.get_type()
! if ctype is None:
! # No Content-Type: header so try the default handler
! self._writeBody(msg)
! else:
! # We do have a Content-Type: header.
! specific = UNDERSCORE.join(ctype.split('/')).replace('-', '_')
! meth = getattr(self, '_handle_' + specific, None)
if meth is None:
! generic = msg.get_main_type().replace('-', '_')
! meth = getattr(self, '_handle_' + generic, None)
! if meth is None:
! meth = self._writeBody
! meth(msg)

#
--- 132,148 ----
def _dispatch(self, msg):
# Get the Content-Type: for the message, then try to dispatch to
! # self._handle_<maintype>_<subtype>(). If there's no handler for the
! # full MIME type, then dispatch to self._handle_<maintype>(). If
! # that's missing too, then dispatch to self._writeBody().
! main = msg.get_content_maintype()
! sub = msg.get_content_subtype()
! specific = UNDERSCORE.join((main, sub)).replace('-', '_')
! meth = getattr(self, '_handle_' + specific, None)
! if meth is None:
! generic = main.replace('-', '_')
! meth = getattr(self, '_handle_' + generic, None)
if meth is None:
! meth = self._writeBody
! meth(msg)

#
***************
*** 140,149 ****
def _write_headers(self, msg):
for h, v in msg.items():
- # We only write the MIME-Version: header for the outermost
- # container message. Unfortunately, we can't use same technique
- # as for the Unix-From above because we don't know when
- # MIME-Version: will occur.
- if h.lower() == 'mime-version' and not self.__first:
- continue
# RFC 2822 says that lines SHOULD be no more than maxheaderlen
# characters wide, so we're well within our rights to split long
--- 152,155 ----
***************
*** 161,165 ****
# 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.split('\n')]
for line in lines:
if len(line) > maxheaderlen:
--- 167,171 ----
# 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:
***************
*** 169,218 ****
# just return the original unchanged.
return text
! rtn = []
! for line in text.split('\n'):
! # Short lines can remain unchanged
! if len(line.replace('\t', SPACE8)) <= maxheaderlen:
! rtn.append(line)
! SEMINLTAB.join(rtn)
! else:
! oldlen = len(text)
! # Try to break the line on semicolons, but if that doesn't
! # work, try to split on folding whitespace.
! while len(text) > maxheaderlen:
! i = text.rfind(';', 0, maxheaderlen)
! if i < 0:
! break
! rtn.append(text[:i])
! text = text[i+1:].lstrip()
! if len(text) <> oldlen:
! # Splitting on semis worked
! rtn.append(text)
! return SEMINLTAB.join(rtn)
! # Splitting on semis didn't help, so try to split on
! # whitespace.
! parts = re.split(r'(\s+)', text)
! # Watch out though for "Header: longnonsplittableline"
! if parts[0].endswith(':') and len(parts) == 3:
! return text
! first = parts.pop(0)
! sublines = [first]
! acc = len(first)
! while parts:
! len0 = len(parts[0])
! len1 = len(parts[1])
! if acc + len0 + len1 < maxheaderlen:
! 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
! rtn.append(EMPTYSTRING.join(sublines))
! del parts[0]
! first = parts.pop(0)
! sublines = [first]
! acc = len(first)
! rtn.append(EMPTYSTRING.join(sublines))
! return NLTAB.join(rtn)

#
--- 175,184 ----
# just return the original unchanged.
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()

#
***************
*** 224,228 ****
if payload is None:
return
! if not isinstance(payload, StringType):
raise TypeError, 'string payload expected: %s' % type(payload)
if self._mangle_from_:
--- 190,197 ----
if payload is None:
return
! cset = msg.get_charset()
! if cset is not None:
! payload = cset.body_encode(payload)
! if not _isstring(payload):
raise TypeError, 'string payload expected: %s' % type(payload)
if self._mangle_from_:
***************
*** 233,246 ****
_writeBody = _handle_text

! def _handle_multipart(self, msg, isdigest=0):
# The trick here is to write out each part separately, merge them all
# together, and then make sure that the boundary we've chosen isn't
# present in the payload.
msgtexts = []
- # BAW: kludge for broken add_payload() semantics; watch out for
- # multipart/* MIME types with None or scalar payloads.
subparts = msg.get_payload()
if subparts is None:
! # Nothing has every been attached
boundary = msg.get_boundary(failobj=_make_boundary())
print >> self._fp, '--' + boundary
--- 202,213 ----
_writeBody = _handle_text

! def _handle_multipart(self, msg):
# The trick here is to write out each part separately, merge them all
# together, and then make sure that the boundary we've chosen isn't
# present in the payload.
msgtexts = []
subparts = msg.get_payload()
if subparts is None:
boundary = msg.get_boundary(failobj=_make_boundary())
print >> self._fp, '--' + boundary
***************
*** 248,251 ****
--- 215,222 ----
print >> self._fp, '--' + boundary + '--'
return
+ elif _isstring(subparts):
+ # e.g. a non-strict parse of a message with no starting boundary.
+ self._fp.write(subparts)
+ return
elif not isinstance(subparts, ListType):
# Scalar payload
***************
*** 253,258 ****
for part in subparts:
s = StringIO()
! g = self.__class__(s, self._mangle_from_, self.__maxheaderlen)
! g(part, unixfrom=0)
msgtexts.append(s.getvalue())
# Now make sure the boundary we've selected doesn't appear in any of
--- 224,229 ----
for part in subparts:
s = StringIO()
! g = self.clone(s)
! g.flatten(part, unixfrom=False)
msgtexts.append(s.getvalue())
# Now make sure the boundary we've selected doesn't appear in any of
***************
*** 275,286 ****
# newline.
print >> self._fp, '--' + boundary
- if isdigest:
- print >> self._fp
# Join and write the individual parts
joiner = '\n--' + boundary + '\n'
- if isdigest:
- # multipart/digest types effectively add an extra newline between
- # the boundary and the body part.
- joiner += '\n'
self._fp.write(joiner.join(msgtexts))
print >> self._fp, '\n--' + boundary + '--',
--- 246,251 ----
***************
*** 291,297 ****
self._fp.write(msg.epilogue)

- def _handle_multipart_digest(self, msg):
- self._handle_multipart(msg, isdigest=1)
-
def _handle_message_delivery_status(self, msg):
# We can't just write the headers directly to self's file object
--- 256,259 ----
***************
*** 301,306 ****
for part in msg.get_payload():
s = StringIO()
! g = self.__class__(s, self._mangle_from_, self.__maxheaderlen)
! g(part, unixfrom=0)
text = s.getvalue()
lines = text.split('\n')
--- 263,268 ----
for part in msg.get_payload():
s = StringIO()
! g = self.clone(s)
! g.flatten(part, unixfrom=False)
text = s.getvalue()
lines = text.split('\n')
***************
*** 317,325 ****
def _handle_message(self, msg):
s = StringIO()
! g = self.__class__(s, self._mangle_from_, self.__maxheaderlen)
! # A message/rfc822 should contain a scalar payload which is another
! # Message object. Extract that object, stringify it, and write that
! # out.
! g(msg.get_payload(), unixfrom=0)
self._fp.write(s.getvalue())

--- 279,288 ----
def _handle_message(self, msg):
s = StringIO()
! g = self.clone(s)
! # The payload of a message/rfc822 part should be a multipart sequence
! # of length 1. The zeroth element of the list should be the Message
! # object for the subpart. Extract that object, stringify it, and
! # write it out.
! g.flatten(msg.get_payload(0), unixfrom=False)
self._fp.write(s.getvalue())

***************
*** 332,336 ****
with a format string representing the part.
"""
! def __init__(self, outfp, mangle_from_=1, maxheaderlen=78, fmt=None):
"""Like Generator.__init__() except that an additional optional
argument is allowed.
--- 295,299 ----
with a format string representing the part.
"""
! def __init__(self, outfp, mangle_from_=True, maxheaderlen=78, fmt=None):
"""Like Generator.__init__() except that an additional optional
argument is allowed.
***************
*** 364,368 ****
maintype = part.get_main_type('text')
if maintype == 'text':
! print >> self, part.get_payload(decode=1)
elif maintype == 'multipart':
# Just skip this
--- 327,331 ----
maintype = part.get_main_type('text')
if maintype == 'text':
! print >> self, part.get_payload(decode=True)
elif maintype == 'multipart':
# Just skip this
***************
*** 391,395 ****
b = boundary
counter = 0
! while 1:
cre = re.compile('^--' + re.escape(b) + '(--)?$', re.MULTILINE)
if not cre.search(text):
--- 354,358 ----
b = boundary
counter = 0
! while True:
cre = re.compile('^--' + re.escape(b) + '(--)?$', re.MULTILINE)
if not cre.search(text):

Index: Iterators.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/Iterators.py,v
retrieving revision 1.5
retrieving revision 1.5.10.1
diff -C2 -d -r1.5 -r1.5.10.1
*** Iterators.py 15 Oct 2001 04:38:22 -0000 1.5
--- Iterators.py 4 Oct 2002 17:24:24 -0000 1.5.10.1
***************
*** 1,3 ****
# Author: barry@zope.com (Barry Warsaw)

--- 1,3 ----
# Author: barry@zope.com (Barry Warsaw)

***************
*** 5,33 ****
"""

! from __future__ import generators
! from cStringIO import StringIO
! from types import StringType
!

!
! 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 type(payload) is StringType:
! for line in StringIO(payload):
! yield line



! def typed_subpart_iterator(msg, maintype='text', subtype=None):
! """Iterate over the subparts with a given MIME type.
!
! Use `maintype' as the main MIME type to match against; this defaults to
! "text". Optional `subtype' is the MIME subtype to match against; if
! omitted, only the main type is matched.
! """
! for subpart in msg.walk():
! if subpart.get_main_type('text') == maintype:
! if subtype is None or subpart.get_subtype('plain') == subtype:
! yield subpart
--- 5,25 ----
"""

! import sys

! try:
! from email._compat22 import body_line_iterator, typed_subpart_iterator
! except SyntaxError:
! # Python 2.1 doesn't have generators
! from email._compat21 import body_line_iterator, typed_subpart_iterator



! def _structure(msg, fp=None, level=0):
! """A handy debugging aid"""
! if fp is None:
! fp = sys.stdout
! tab = ' ' * (level * 4)
! print >> fp, tab + msg.get_content_type()
! if msg.is_multipart():
! for subpart in msg.get_payload():
! _structure(subpart, fp, level+1)

Index: MIMEAudio.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/MIMEAudio.py,v
retrieving revision 1.2
retrieving revision 1.2.6.1
diff -C2 -d -r1.2 -r1.2.6.1
*** MIMEAudio.py 24 Nov 2001 15:49:53 -0000 1.2
--- MIMEAudio.py 4 Oct 2002 17:24:24 -0000 1.2.6.1
***************
*** 7,13 ****
from cStringIO import StringIO

! import MIMEBase
! import Errors
! import Encoders


--- 7,13 ----
from cStringIO import StringIO

! from email import Errors
! from email import Encoders
! from email.MIMENonMultipart import MIMENonMultipart


***************
*** 38,42 ****


! class MIMEAudio(MIMEBase.MIMEBase):
"""Class for generating audio/* MIME documents."""

--- 38,42 ----


! class MIMEAudio(MIMENonMultipart):
"""Class for generating audio/* MIME documents."""

***************
*** 47,51 ****
_audiodata is a string containing the raw audio data. If this data
can be decoded by the standard Python `sndhdr' module, then the
! subtype will be automatically included in the Content-Type: header.
Otherwise, you can specify the specific audio subtype via the
_subtype parameter. If _subtype is not given, and no subtype can be
--- 47,51 ----
_audiodata is a string containing the raw audio data. If this data
can be decoded by the standard Python `sndhdr' module, then the
! subtype will be automatically included in the Content-Type header.
Otherwise, you can specify the specific audio subtype via the
_subtype parameter. If _subtype is not given, and no subtype can be
***************
*** 56,64 ****
Image instance. It should use get_payload() and set_payload() to
change the payload to the encoded form. It should also add any
! Content-Transfer-Encoding: or other headers to the message as
necessary. The default encoding is Base64.

Any additional keyword arguments are passed to the base class
! constructor, which turns them into parameters on the Content-Type:
header.
"""
--- 56,64 ----
Image instance. It should use get_payload() and set_payload() to
change the payload to the encoded form. It should also add any
! Content-Transfer-Encoding or other headers to the message as
necessary. The default encoding is Base64.

Any additional keyword arguments are passed to the base class
! constructor, which turns them into parameters on the Content-Type
header.
"""
***************
*** 67,71 ****
if _subtype is None:
raise TypeError, 'Could not find audio MIME subtype'
! MIMEBase.MIMEBase.__init__(self, 'audio', _subtype, **_params)
self.set_payload(_audiodata)
_encoder(self)
--- 67,71 ----
if _subtype is None:
raise TypeError, 'Could not find audio MIME subtype'
! MIMENonMultipart.__init__(self, 'audio', _subtype, **_params)
self.set_payload(_audiodata)
_encoder(self)

Index: MIMEBase.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/MIMEBase.py,v
retrieving revision 1.4
retrieving revision 1.4.10.1
diff -C2 -d -r1.4 -r1.4.10.1
*** MIMEBase.py 4 Oct 2001 17:05:11 -0000 1.4
--- MIMEBase.py 4 Oct 2002 17:24:24 -0000 1.4.10.1
***************
*** 1,3 ****
# Author: barry@zope.com (Barry Warsaw)

--- 1,3 ----
# Author: barry@zope.com (Barry Warsaw)

***************
*** 5,9 ****
"""

! import Message


--- 5,9 ----
"""

! from email import Message



Index: MIMEImage.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/MIMEImage.py,v
retrieving revision 1.3
retrieving revision 1.3.10.1
diff -C2 -d -r1.3 -r1.3.10.1
*** MIMEImage.py 4 Oct 2001 17:05:11 -0000 1.3
--- MIMEImage.py 4 Oct 2002 17:24:24 -0000 1.3.10.1
***************
*** 1,3 ****
# Author: barry@zope.com (Barry Warsaw)

--- 1,3 ----
# Author: barry@zope.com (Barry Warsaw)

***************
*** 7,18 ****
import imghdr

! # Intrapackage imports
! import MIMEBase
! import Errors
! import Encoders



! class MIMEImage(MIMEBase.MIMEBase):
"""Class for generating image/* type MIME documents."""

--- 7,17 ----
import imghdr

! from email import Errors
! from email import Encoders
! from email.MIMENonMultipart import MIMENonMultipart



! class MIMEImage(MIMENonMultipart):
"""Class for generating image/* type MIME documents."""

***************
*** 23,27 ****
_imagedata is a string containing the raw image data. If this data
can be decoded by the standard Python `imghdr' module, then the
! subtype will be automatically included in the Content-Type: header.
Otherwise, you can specify the specific image subtype via the _subtype
parameter.
--- 22,26 ----
_imagedata is a string containing the raw image data. If this data
can be decoded by the standard Python `imghdr' module, then the
! subtype will be automatically included in the Content-Type header.
Otherwise, you can specify the specific image subtype via the _subtype
parameter.
***************
*** 31,39 ****
Image instance. It should use get_payload() and set_payload() to
change the payload to the encoded form. It should also add any
! Content-Transfer-Encoding: or other headers to the message as
necessary. The default encoding is Base64.

Any additional keyword arguments are passed to the base class
! constructor, which turns them into parameters on the Content-Type:
header.
"""
--- 30,38 ----
Image instance. It should use get_payload() and set_payload() to
change the payload to the encoded form. It should also add any
! Content-Transfer-Encoding or other headers to the message as
necessary. The default encoding is Base64.

Any additional keyword arguments are passed to the base class
! constructor, which turns them into parameters on the Content-Type
header.
"""
***************
*** 42,46 ****
if _subtype is None:
raise TypeError, 'Could not guess image MIME subtype'
! MIMEBase.MIMEBase.__init__(self, 'image', _subtype, **_params)
self.set_payload(_imagedata)
_encoder(self)
--- 41,45 ----
if _subtype is None:
raise TypeError, 'Could not guess image MIME subtype'
! MIMENonMultipart.__init__(self, 'image', _subtype, **_params)
self.set_payload(_imagedata)
_encoder(self)

Index: MIMEMessage.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/MIMEMessage.py,v
retrieving revision 1.3
retrieving revision 1.3.10.1
diff -C2 -d -r1.3 -r1.3.10.1
*** MIMEMessage.py 4 Oct 2001 17:05:11 -0000 1.3
--- MIMEMessage.py 4 Oct 2002 17:24:24 -0000 1.3.10.1
***************
*** 1,3 ****
# Author: barry@zope.com (Barry Warsaw)

--- 1,3 ----
# Author: barry@zope.com (Barry Warsaw)

***************
*** 5,14 ****
"""

! import Message
! import MIMEBase



! class MIMEMessage(MIMEBase.MIMEBase):
"""Class representing message/* MIME documents."""

--- 5,14 ----
"""

! from email import Message
! from email.MIMENonMultipart import MIMENonMultipart



! class MIMEMessage(MIMENonMultipart):
"""Class representing message/* MIME documents."""

***************
*** 23,28 ****
the term "rfc822" is technically outdated by RFC 2822).
"""
! MIMEBase.MIMEBase.__init__(self, 'message', _subtype)
if not isinstance(_msg, Message.Message):
raise TypeError, 'Argument is not an instance of Message'
! self.set_payload(_msg)
--- 23,32 ----
the term "rfc822" is technically outdated by RFC 2822).
"""
! MIMENonMultipart.__init__(self, 'message', _subtype)
if not isinstance(_msg, Message.Message):
raise TypeError, 'Argument is not an instance of Message'
! # It's convenient to use this base class method. We need to do it
! # this way or we'll get an exception
! Message.Message.attach(self, _msg)
! # And be sure our default type is set correctly
! self.set_default_type('message/rfc822')

Index: MIMEText.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/MIMEText.py,v
retrieving revision 1.3
retrieving revision 1.3.10.1
diff -C2 -d -r1.3 -r1.3.10.1
*** MIMEText.py 4 Oct 2001 17:05:11 -0000 1.3
--- MIMEText.py 4 Oct 2002 17:24:24 -0000 1.3.10.1
***************
*** 1,3 ****
# Author: barry@zope.com (Barry Warsaw)

--- 1,3 ----
# Author: barry@zope.com (Barry Warsaw)

***************
*** 5,18 ****
"""

! import MIMEBase
! from Encoders import encode_7or8bit



! class MIMEText(MIMEBase.MIMEBase):
"""Class for generating text/* type MIME documents."""

def __init__(self, _text, _subtype='plain', _charset='us-ascii',
! _encoder=encode_7or8bit):
"""Create a text/* type MIME document.

--- 5,19 ----
"""

! import warnings
! from email.MIMENonMultipart import MIMENonMultipart
! from email.Encoders import encode_7or8bit



! class MIMEText(MIMENonMultipart):
"""Class for generating text/* type MIME documents."""

def __init__(self, _text, _subtype='plain', _charset='us-ascii',
! _encoder=None):
"""Create a text/* type MIME document.

***************
*** 22,41 ****
_subtype is the MIME sub content type, defaulting to "plain".

! _charset is the character set parameter added to the Content-Type:
! header. This defaults to "us-ascii".

! _encoder is a function which will perform the actual encoding for
! transport of the text data. It takes one argument, which is this
! Text instance. It should use get_payload() and set_payload() to
! change the payload to the encoded form. It should also add any
! Content-Transfer-Encoding: or other headers to the message as
! necessary. The default encoding doesn't actually modify the payload,
! but it does set Content-Transfer-Encoding: to either `7bit' or `8bit'
! as appropriate.
"""
! MIMEBase.MIMEBase.__init__(self, 'text', _subtype,
! **{'charset': _charset})
! if _text and _text[-1] <> '\n':
_text += '\n'
! self.set_payload(_text)
! _encoder(self)
--- 23,48 ----
_subtype is the MIME sub content type, defaulting to "plain".

! _charset is the character set parameter added to the Content-Type
! header. This defaults to "us-ascii". Note that as a side-effect, the
! Content-Transfer-Encoding header will also be set.

! The use of the _encoder is deprecated. The encoding of the payload,
! and the setting of the character set parameter now happens implicitly
! based on the _charset argument. If _encoder is supplied, then a
! DeprecationWarning is used, and the _encoder functionality may
! override any header settings indicated by _charset. This is probably
! not what you want.
"""
! 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:
! warnings.warn('_encoder argument is obsolete.',
! DeprecationWarning, 2)
! # Because set_payload() with a _charset will set its own
! # Content-Transfer-Encoding header, we need to delete the
! # existing one or will end up with two of them. :(
! del self['content-transfer-encoding']
! _encoder(self)

Index: Message.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/Message.py,v
retrieving revision 1.9
retrieving revision 1.9.6.1
diff -C2 -d -r1.9 -r1.9.6.1
*** Message.py 24 Nov 2001 16:56:56 -0000 1.9
--- Message.py 4 Oct 2002 17:24:24 -0000 1.9.6.1
***************
*** 1,3 ****
# Author: barry@zope.com (Barry Warsaw)

--- 1,3 ----
# Author: barry@zope.com (Barry Warsaw)

***************
*** 5,38 ****
"""

- from __future__ import generators
-
import re
! import base64
! import quopri
from cStringIO import StringIO
! from types import ListType

# Intrapackage imports
! import Errors
! import Utils

SEMISPACE = '; '
paramre = re.compile(r'\s*;\s*')



class Message:
! """Basic message object for use inside the object tree.

A message object is defined as something that has a bunch of RFC 2822
! headers and a payload. If the body of the message is a multipart, then
! the payload is a list of Messages, otherwise it is a string.

! These objects implement part of the `mapping' interface, which assumes
there is exactly one occurrance of the header per message. Some headers
! do in fact appear multiple times (e.g. Received:) and for those headers,
you must use the explicit API to set or get all the headers. Not all of
the mapping methods are implemented.
-
"""
def __init__(self):
--- 5,83 ----
"""

import re
! import warnings
from cStringIO import StringIO
! from types import ListType, TupleType, StringType

# Intrapackage imports
! from email import Errors
! from email import Utils
! from email import Charset

SEMISPACE = '; '
+
+ try:
+ True, False
+ except NameError:
+ True = 1
+ False = 0
+
+ # Regular expression used to split header parameters. BAW: this may be too
+ # simple. It isn't strictly RFC 2045 (section 5.1) compliant, but it catches
+ # most headers found in the wild. We may eventually need a full fledged
+ # parser eventually.
paramre = re.compile(r'\s*;\s*')
+ # Regular expression that matches `special' characters in parameters, the
+ # existance of which force quoting of the parameter value.
+ tspecials = re.compile(r'[ \(\)<>@,;:\\"/\[\]\?=]')
+
+
+
+ # Helper functions
+ def _formatparam(param, value=None, quote=True):
+ """Convenience function to format and return a key=value pair.
+
+ This will quote the value if needed or if quote is true.
+ """
+ if value is not None and len(value) > 0:
+ # TupleType is used for RFC 2231 encoded parameter values where items
+ # are (charset, language, value). charset is a string, not a Charset
+ # instance.
+ if isinstance(value, TupleType):
+ # Encode as per RFC 2231
+ param += '*'
+ value = Utils.encode_rfc2231(value[2], value[0], value[1])
+ # BAW: Please check this. I think that if quote is set it should
+ # force quoting even if not necessary.
+ if quote or tspecials.search(value):
+ return '%s="%s"' % (param, Utils.quote(value))
+ else:
+ return '%s=%s' % (param, value)
+ else:
+ return param
+
+
+ def _unquotevalue(value):
+ if isinstance(value, TupleType):
+ return value[0], value[1], Utils.unquote(value[2])
+ else:
+ return Utils.unquote(value)



class Message:
! """Basic message object.

A message object is defined as something that has a bunch of RFC 2822
! headers and a payload. It may optionally have an envelope header
! (a.k.a. Unix-From or From_ header). If the message is a container (i.e. a
! multipart or a message/rfc822), then the payload is a list of Message
! objects, otherwise it is a string.

! Message objects implement part of the `mapping' interface, which assumes
there is exactly one occurrance of the header per message. Some headers
! do in fact appear multiple times (e.g. Received) and for those headers,
you must use the explicit API to set or get all the headers. Not all of
the mapping methods are implemented.
"""
def __init__(self):
***************
*** 40,68 ****
self._unixfrom = None
self._payload = None
# Defaults for multipart messages
self.preamble = self.epilogue = None

def __str__(self):
"""Return the entire formatted message as a string.
! This includes the headers, body, and `unixfrom' line.
"""
! return self.as_string(unixfrom=1)

! def as_string(self, unixfrom=0):
"""Return the entire formatted message as a string.
! Optional `unixfrom' when true, means include the Unix From_ envelope
header.
"""
! from Generator import Generator
fp = StringIO()
g = Generator(fp)
! g(self, unixfrom=unixfrom)
return fp.getvalue()

def is_multipart(self):
! """Return true if the message consists of multiple parts."""
! if type(self._payload) is ListType:
! return 1
! return 0

#
--- 85,116 ----
self._unixfrom = None
self._payload = None
+ self._charset = None
# Defaults for multipart messages
self.preamble = self.epilogue = None
+ # Default content type
+ self._default_type = 'text/plain'

def __str__(self):
"""Return the entire formatted message as a string.
! This includes the headers, body, and envelope header.
"""
! return self.as_string(unixfrom=True)

! def as_string(self, unixfrom=False):
"""Return the entire formatted message as a string.
! Optional `unixfrom' when True, means include the Unix From_ envelope
header.
"""
! from email.Generator import Generator
fp = StringIO()
g = Generator(fp)
! g.flatten(self, unixfrom=unixfrom)
return fp.getvalue()

def is_multipart(self):
! """Return True if the message consists of multiple parts."""
! if isinstance(self._payload, ListType):
! return True
! return False

#
***************
*** 83,116 ****
If the current payload is empty, then the current payload will be made
a scalar, set to the given value.
"""
if self._payload is None:
self._payload = payload
! elif type(self._payload) is ListType:
self._payload.append(payload)
elif self.get_main_type() not in (None, 'multipart'):
raise Errors.MultipartConversionError(
! 'Message main Content-Type: must be "multipart" or missing')
else:
self._payload = [self._payload, payload]

! # A useful synonym
! attach = add_payload

! def get_payload(self, i=None, decode=0):
! """Return the current payload exactly as is.

! Optional 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.
! 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:
payload = self._payload
! elif type(self._payload) is not ListType:
raise TypeError, i
else:
--- 131,180 ----
If the current payload is empty, then the current payload will be made
a scalar, set to the given value.
+
+ Note: This method is deprecated. Use .attach() instead.
"""
+ warnings.warn('add_payload() is deprecated, use attach() instead.',
+ DeprecationWarning, 2)
if self._payload is None:
self._payload = payload
! elif isinstance(self._payload, ListType):
self._payload.append(payload)
elif self.get_main_type() not in (None, 'multipart'):
raise Errors.MultipartConversionError(
! 'Message main content type must be "multipart" or missing')
else:
self._payload = [self._payload, payload]

! def attach(self, payload):
! """Add the given payload to the current payload.

! The current payload will always be a list of objects after this method
! is called. If you want to set the payload to a scalar object, use
! set_payload() instead.
! """
! if self._payload is None:
! self._payload = [payload]
! else:
! self._payload.append(payload)

! def get_payload(self, i=None, decode=False):
! """Return a reference to the payload.

! The payload will either be a list object or a string. If you mutate
! the list object, you modify the message's payload in place. Optional
! 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:
payload = self._payload
! elif not isinstance(self._payload, ListType):
raise TypeError, i
else:
***************
*** 128,135 ****
return payload


! def set_payload(self, payload):
! """Set the payload to the given value."""
self._payload = payload

#
--- 192,249 ----
return payload

+ def set_payload(self, payload, charset=None):
+ """Set the payload to the given value.

! Optional charset sets the message's default character set. See
! set_charset() for details.
! """
self._payload = payload
+ if charset is not None:
+ self.set_charset(charset)
+
+ def set_charset(self, charset):
+ """Set the charset of the payload to a given character set.
+
+ charset can be a Charset instance, a string naming a character set, or
+ None. If it is a string it will be converted to a Charset instance.
+ If charset is None, the charset parameter will be removed from the
+ Content-Type field. Anything else will generate a TypeError.
+
+ The message will be assumed to be of type text/* encoded with
+ charset.input_charset. It will be converted to charset.output_charset
+ and encoded properly, if needed, when generating the plain text
+ representation of the message. MIME headers (MIME-Version,
+ Content-Type, Content-Transfer-Encoding) will be added as needed.
+
+ """
+ if charset is None:
+ self.del_param('charset')
+ self._charset = None
+ return
+ if isinstance(charset, StringType):
+ charset = Charset.Charset(charset)
+ if not isinstance(charset, Charset.Charset):
+ raise TypeError, charset
+ # BAW: should we accept strings that can serve as arguments to the
+ # Charset constructor?
+ self._charset = charset
+ if not self.has_key('MIME-Version'):
+ self.add_header('MIME-Version', '1.0')
+ if not self.has_key('Content-Type'):
+ self.add_header('Content-Type', 'text/plain',
+ charset=charset.get_output_charset())
+ else:
+ self.set_param('charset', charset.get_output_charset())
+ if not self.has_key('Content-Transfer-Encoding'):
+ cte = charset.get_body_encoding()
+ if callable(cte):
+ cte(self)
+ else:
+ self.add_header('Content-Transfer-Encoding', cte)
+
+ def get_charset(self):
+ """Return the Charset instance associated with the message's payload.
+ """
+ return self._charset

#
***************
*** 171,176 ****
self._headers = newheaders

! def __contains__(self, key):
! return key.lower() in [k.lower() for k, v in self._headers]

def has_key(self, name):
--- 285,290 ----
self._headers = newheaders

! def __contains__(self, name):
! return name.lower() in [k.lower() for k, v in self._headers]

def has_key(self, name):
***************
*** 183,188 ****

These will be sorted in the order they appeared in the original
! message, and may contain duplicates. Any fields deleted and
! re-inserted are always appended to the header list.
"""
return [k for k, v in self._headers]
--- 297,303 ----

These will be sorted in the order they appeared in the original
! message, or were added to the message, and may contain duplicates.
! Any fields deleted and re-inserted are always appended to the header
! list.
"""
return [k for k, v in self._headers]
***************
*** 192,197 ****

These will be sorted in the order they appeared in the original
! message, and may contain duplicates. Any fields deleted and
! re-inserted are always appended to the header list.
"""
return [v for k, v in self._headers]
--- 307,313 ----

These will be sorted in the order they appeared in the original
! message, or were added to the message, and may contain duplicates.
! Any fields deleted and re-inserted are always appended to the header
! list.
"""
return [v for k, v in self._headers]
***************
*** 201,206 ****

These will be sorted in the order they appeared in the original
! message, and may contain duplicates. Any fields deleted and
! re-inserted are always appended to the header list.
"""
return self._headers[:]
--- 317,323 ----

These will be sorted in the order they appeared in the original
! message, or were added to the message, and may contain duplicates.
! Any fields deleted and re-inserted are always appended to the header
! list.
"""
return self._headers[:]
***************
*** 251,255 ****

msg.add_header('content-disposition', 'attachment', filename='bud.gif')
-
"""
parts = []
--- 368,371 ----
***************
*** 258,271 ****
parts.append(k.replace('_', '-'))
else:
! parts.append('%s="%s"' % (k.replace('_', '-'), v))
if _value is not None:
parts.insert(0, _value)
self._headers.append((_name, SEMISPACE.join(parts)))

def get_type(self, failobj=None):
"""Returns the message's content type.

The returned string is coerced to lowercase and returned as a single
! string of the form `maintype/subtype'. If there was no Content-Type:
header in the message, failobj is returned (defaults to None).
"""
--- 374,407 ----
parts.append(k.replace('_', '-'))
else:
! parts.append(_formatparam(k.replace('_', '-'), v))
if _value is not None:
parts.insert(0, _value)
self._headers.append((_name, SEMISPACE.join(parts)))

+ def replace_header(self, _name, _value):
+ """Replace a header.
+
+ Replace the first matching header found in the message, retaining
+ header order and case. If no matching header was found, a KeyError is
+ raised.
+ """
+ _name = _name.lower()
+ for i, (k, v) in zip(range(len(self._headers)), self._headers):
+ if k.lower() == _name:
+ self._headers[i] = (k, _value)
+ break
+ else:
+ raise KeyError, _name
+
+ #
+ # These methods are silently deprecated in favor of get_content_type() and
+ # friends (see below). They will be noisily deprecated in email 3.0.
+ #
+
def get_type(self, failobj=None):
"""Returns the message's content type.

The returned string is coerced to lowercase and returned as a single
! string of the form `maintype/subtype'. If there was no Content-Type
header in the message, failobj is returned (defaults to None).
"""
***************
*** 274,278 ****
if value is missing:
return failobj
! return paramre.split(value)[0].lower()

def get_main_type(self, failobj=None):
--- 410,414 ----
if value is missing:
return failobj
! return paramre.split(value)[0].lower().strip()

def get_main_type(self, failobj=None):
***************
*** 282,289 ****
if ctype is missing:
return failobj
! parts = ctype.split('/')
! if len(parts) > 0:
! return ctype.split('/')[0]
! return failobj

def get_subtype(self, failobj=None):
--- 418,424 ----
if ctype is missing:
return failobj
! if ctype.count('/') <> 1:
! return failobj
! return ctype.split('/')[0]

def get_subtype(self, failobj=None):
***************
*** 293,300 ****
if ctype is missing:
return failobj
! parts = ctype.split('/')
! if len(parts) > 1:
! return ctype.split('/')[1]
! return failobj

def _get_params_preserve(self, failobj, header):
--- 428,498 ----
if ctype is missing:
return failobj
! if ctype.count('/') <> 1:
! return failobj
! return ctype.split('/')[1]
!
! #
! # Use these three methods instead of the three above.
! #
!
! def get_content_type(self):
! """Return the message's content type.
!
! The returned string is coerced to lower case of the form
! `maintype/subtype'. If there was no Content-Type header in the
! message, the default type as given by get_default_type() will be
! returned. Since according to RFC 2045, messages always have a default
! type this will always return a value.
!
! RFC 2045 defines a message's default type to be text/plain unless it
! appears inside a multipart/digest container, in which case it would be
! message/rfc822.
! """
! missing = []
! value = self.get('content-type', missing)
! if value is missing:
! # This should have no parameters
! return self.get_default_type()
! ctype = paramre.split(value)[0].lower().strip()
! # RFC 2045, section 5.2 says if its invalid, use text/plain
! if ctype.count('/') <> 1:
! return 'text/plain'
! return ctype
!
! def get_content_maintype(self):
! """Return the message's main content type.
!
! This is the `maintype' part of the string returned by
! get_content_type().
! """
! ctype = self.get_content_type()
! return ctype.split('/')[0]
!
! def get_content_subtype(self):
! """Returns the message's sub-content type.
!
! This is the `subtype' part of the string returned by
! get_content_type().
! """
! ctype = self.get_content_type()
! return ctype.split('/')[1]
!
! def get_default_type(self):
! """Return the `default' content type.
!
! Most messages have a default content type of text/plain, except for
! messages that are subparts of multipart/digest containers. Such
! subparts have a default content type of message/rfc822.
! """
! return self._default_type
!
! def set_default_type(self, ctype):
! """Set the `default' content type.
!
! ctype should be either "text/plain" or "message/rfc822", although this
! is not enforced. The default content type is not stored in the
! Content-Type header.
! """
! self._default_type = ctype

def _get_params_preserve(self, failobj, header):
***************
*** 309,331 ****
try:
name, val = p.split('=', 1)
except ValueError:
# Must have been a bare attribute
! name = p
val = ''
params.append((name, val))
return params

! def get_params(self, failobj=None, header='content-type'):
! """Return the message's Content-Type: parameters, as a list.

The elements of the returned list are 2-tuples of key/value pairs, as
split on the `=' sign. The left hand side of the `=' is the key,
while the right hand side is the value. If there is no `=' sign in
! the parameter the value is the empty string. The value is always
! unquoted.

! Optional failobj is the object to return if there is no Content-Type:
header. Optional header is the header to search instead of
! Content-Type:
"""
missing = []
--- 507,532 ----
try:
name, val = p.split('=', 1)
+ name = name.strip()
+ val = val.strip()
except ValueError:
# Must have been a bare attribute
! name = p.strip()
val = ''
params.append((name, val))
+ params = Utils.decode_params(params)
return params

! def get_params(self, failobj=None, header='content-type', unquote=True):
! """Return the message's Content-Type parameters, as a list.

The elements of the returned list are 2-tuples of key/value pairs, as
split on the `=' sign. The left hand side of the `=' is the key,
while the right hand side is the value. If there is no `=' sign in
! the parameter the value is the empty string. The value is as
! described in the get_param() method.

! Optional failobj is the object to return if there is no Content-Type
header. Optional header is the header to search instead of
! Content-Type. If unquote is True, the value is unquoted.
"""
missing = []
***************
*** 333,347 ****
if params is missing:
return failobj
! return [(k, Utils.unquote(v)) for k, v in params]

! def get_param(self, param, failobj=None, header='content-type'):
! """Return the parameter value if found in the Content-Type: header.

! Optional failobj is the object to return if there is no Content-Type:
! header. Optional header is the header to search instead of
! Content-Type:

! Parameter keys are always compared case insensitively. Values are
! always unquoted.
"""
if not self.has_key(header):
--- 534,564 ----
if params is missing:
return failobj
! if unquote:
! return [(k, _unquotevalue(v)) for k, v in params]
! else:
! return params

! def get_param(self, param, failobj=None, header='content-type',
! unquote=True):
! """Return the parameter value if found in the Content-Type header.

! Optional failobj is the object to return if there is no Content-Type
! header, or the Content-Type header has no such parameter. Optional
! header is the header to search instead of Content-Type.

! Parameter keys are always compared case insensitively. The return
! value can either be a string, or a 3-tuple if the parameter was RFC
! 2231 encoded. When it's a 3-tuple, the elements of the value are of
! the form (CHARSET, LANGUAGE, VALUE), where LANGUAGE may be the empty
! string. Your application should be prepared to deal with these, and
! can convert the parameter to a Unicode string like so:
!
! param = msg.get_param('foo')
! if isinstance(param, tuple):
! param = unicode(param[2], param[0])
!
! In any case, the parameter value (either the returned string, or the
! VALUE item in the 3-tuple) is always unquoted, unless unquote is set
! to False.
"""
if not self.has_key(header):
***************
*** 349,359 ****
for k, v in self._get_params_preserve(failobj, header):
if k.lower() == param.lower():
! return Utils.unquote(v)
return failobj

def get_filename(self, failobj=None):
"""Return the filename associated with the payload if present.

! The filename is extracted from the Content-Disposition: header's
`filename' parameter, and it is unquoted.
"""
--- 566,681 ----
for k, v in self._get_params_preserve(failobj, header):
if k.lower() == param.lower():
! if unquote:
! return _unquotevalue(v)
! else:
! return v
return failobj

+ def set_param(self, param, value, header='Content-Type', requote=True,
+ charset=None, language=''):
+ """Set a parameter in the Content-Type header.
+
+ If the parameter already exists in the header, its value will be
+ replaced with the new value.
+
+ If header is Content-Type and has not yet been defined for this
+ message, it will be set to "text/plain" and the new parameter and
+ value will be appended as per RFC 2045.
+
+ An alternate header can specified in the header argument, and all
+ parameters will be quoted as necessary unless requote is False.
+
+ If charset is specified, the parameter will be encoded according to RFC
+ 2231. Optional language specifies the RFC 2231 language, defaulting
+ to the empty string. Both charset and language should be strings.
+ """
+ if not isinstance(value, TupleType) and charset:
+ value = (charset, language, value)
+
+ if not self.has_key(header) and header.lower() == 'content-type':
+ ctype = 'text/plain'
+ else:
+ ctype = self.get(header)
+ if not self.get_param(param, header=header):
+ if not ctype:
+ ctype = _formatparam(param, value, requote)
+ else:
+ ctype = SEMISPACE.join(
+ [ctype, _formatparam(param, value, requote)])
+ else:
+ ctype = ''
+ for old_param, old_value in self.get_params(header=header,
+ unquote=requote):
+ append_param = ''
+ if old_param.lower() == param.lower():
+ append_param = _formatparam(param, value, requote)
+ else:
+ append_param = _formatparam(old_param, old_value, requote)
+ if not ctype:
+ ctype = append_param
+ else:
+ ctype = SEMISPACE.join([ctype, append_param])
+ if ctype <> self.get(header):
+ del self[header]
+ self[header] = ctype
+
+ def del_param(self, param, header='content-type', requote=True):
+ """Remove the given parameter completely from the Content-Type header.
+
+ The header will be re-written in place without the parameter or its
+ value. All values will be quoted as necessary unless requote is
+ False. Optional header specifies an alternative to the Content-Type
+ header.
+ """
+ if not self.has_key(header):
+ return
+ new_ctype = ''
+ for p, v in self.get_params(header, unquote=requote):
+ if p.lower() <> param.lower():
+ if not new_ctype:
+ new_ctype = _formatparam(p, v, requote)
+ else:
+ new_ctype = SEMISPACE.join([new_ctype,
+ _formatparam(p, v, requote)])
+ if new_ctype <> self.get(header):
+ del self[header]
+ self[header] = new_ctype
+
+ def set_type(self, type, header='Content-Type', requote=True):
+ """Set the main type and subtype for the Content-Type header.
+
+ type must be a string in the form "maintype/subtype", otherwise a
+ ValueError is raised.
+
+ This method replaces the Content-Type header, keeping all the
+ parameters in place. If requote is False, this leaves the existing
+ header's quoting as is. Otherwise, the parameters will be quoted (the
+ default).
+
+ An alternative header can be specified in the header argument. When
+ the Content-Type header is set, we'll always also add a MIME-Version
+ header.
+ """
+ # BAW: should we be strict?
+ if not type.count('/') == 1:
+ raise ValueError
+ # Set the Content-Type, you get a MIME-Version
+ if header.lower() == 'content-type':
+ del self['mime-version']
+ self['MIME-Version'] = '1.0'
+ if not self.has_key(header):
+ self[header] = type
+ return
+ params = self.get_params(header, unquote=requote)
+ del self[header]
+ self[header] = type
+ # Skip the first param; it's the old type.
+ for p, v in params[1:]:
+ self.set_param(p, v, header, requote)
+
def get_filename(self, failobj=None):
"""Return the filename associated with the payload if present.

! The filename is extracted from the Content-Disposition header's
`filename' parameter, and it is unquoted.
"""
***************
*** 362,371 ****
if filename is missing:
return failobj
! return Utils.unquote(filename.strip())

def get_boundary(self, failobj=None):
"""Return the boundary associated with the payload if present.

! The boundary is extracted from the Content-Type: header's `boundary'
parameter, and it is unquoted.
"""
--- 684,699 ----
if filename is missing:
return failobj
! if isinstance(filename, TupleType):
! # It's an RFC 2231 encoded parameter
! newvalue = _unquotevalue(filename)
! return unicode(newvalue[2], newvalue[0])
! else:
! newvalue = _unquotevalue(filename.strip())
! return newvalue

def get_boundary(self, failobj=None):
"""Return the boundary associated with the payload if present.

! The boundary is extracted from the Content-Type header's `boundary'
parameter, and it is unquoted.
"""
***************
*** 374,409 ****
if boundary is missing:
return failobj
! return Utils.unquote(boundary.strip())

def set_boundary(self, boundary):
! """Set the boundary parameter in Content-Type: to 'boundary'.

! This is subtly different than deleting the Content-Type: header and
adding a new one with a new boundary parameter via add_header(). The
main difference is that using the set_boundary() method preserves the
! order of the Content-Type: header in the original message.

! HeaderParseError is raised if the message has no Content-Type: header.
"""
missing = []
params = self._get_params_preserve(missing, 'content-type')
if params is missing:
# to set it to, so raise an exception.
! raise Errors.HeaderParseError, 'No Content-Type: header found'
newparams = []
! foundp = 0
for pk, pv in params:
if pk.lower() == 'boundary':
newparams.append(('boundary', '"%s"' % boundary))
! foundp = 1
else:
newparams.append((pk, pv))
if not foundp:
# Tack one one the end. BAW: should we raise an exception
# instead???
newparams.append(('boundary', '"%s"' % boundary))
! # Replace the existing Content-Type: header with the new value
newheaders = []
for h, v in self._headers:
--- 702,740 ----
if boundary is missing:
return failobj
! if isinstance(boundary, TupleType):
! # RFC 2231 encoded, so decode. It better end up as ascii
! return unicode(boundary[2], boundary[0]).encode('us-ascii')
! return _unquotevalue(boundary.strip())

def set_boundary(self, boundary):
! """Set the boundary parameter in Content-Type to 'boundary'.

! This is subtly different than deleting the Content-Type header and
adding a new one with a new boundary parameter via add_header(). The
main difference is that using the set_boundary() method preserves the
! order of the Content-Type header in the original message.

! HeaderParseError is raised if the message has no Content-Type header.
"""
missing = []
params = self._get_params_preserve(missing, 'content-type')
if params is missing:
# to set it to, so raise an exception.
! raise Errors.HeaderParseError, 'No Content-Type header found'
newparams = []
! foundp = False
for pk, pv in params:
if pk.lower() == 'boundary':
newparams.append(('boundary', '"%s"' % boundary))
! foundp = True
else:
newparams.append((pk, pv))
if not foundp:
# Tack one one the end. BAW: should we raise an exception
# instead???
newparams.append(('boundary', '"%s"' % boundary))
! # Replace the existing Content-Type header with the new value
newheaders = []
for h, v in self._headers:
***************
*** 421,445 ****
self._headers = newheaders

! def walk(self):
! """Walk over the message tree, yielding each subpart.

! The walk is performed in depth-first order. This method is a
! generator.
"""
! yield self
! if self.is_multipart():
! for subpart in self.get_payload():
! for subsubpart in subpart.walk():
! yield subsubpart

def get_charsets(self, failobj=None):
"""Return a list containing the charset(s) used in this message.

! The returned list of items describes the Content-Type: headers'
charset parameter for this message and all the subparts in its
payload.

Each item will either be a string (the value of the charset parameter
! in the Content-Type: header of that part) or the value of the
'failobj' parameter (defaults to None), if the part does not have a
main MIME type of "text", or the charset is not defined.
--- 752,785 ----
self._headers = newheaders

! try:
! from email._compat22 import walk
! except SyntaxError:
! # Must be using Python 2.1
! from email._compat21 import walk

! def get_content_charset(self, failobj=None):
! """Return the charset parameter of the Content-Type header.
!
! If there is no Content-Type header, or if that header has no charset
! parameter, failobj is returned.
"""
! missing = []
! charset = self.get_param('charset', missing)
! if charset is missing:
! return failobj
! if isinstance(charset, TupleType):
! # RFC 2231 encoded, so decode it, and it better end up as ascii.
! return unicode(charset[2], charset[0]).encode('us-ascii')
! return charset

def get_charsets(self, failobj=None):
"""Return a list containing the charset(s) used in this message.

! The returned list of items describes the Content-Type headers'
charset parameter for this message and all the subparts in its
payload.

Each item will either be a string (the value of the charset parameter
! in the Content-Type header of that part) or the value of the
'failobj' parameter (defaults to None), if the part does not have a
main MIME type of "text", or the charset is not defined.
***************
*** 449,451 ****
message will still return a list of length 1.
"""
! return [part.get_param('charset', failobj) for part in self.walk()]
--- 789,791 ----
message will still return a list of length 1.
"""
! return [part.get_content_charset(failobj) for part in self.walk()]

Index: Parser.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/Parser.py,v
retrieving revision 1.5.10.1
retrieving revision 1.5.10.2
diff -C2 -d -r1.5.10.1 -r1.5.10.2
*** Parser.py 28 Jan 2002 15:27:20 -0000 1.5.10.1
--- Parser.py 4 Oct 2002 17:24:24 -0000 1.5.10.2
***************
*** 5,22 ****
"""

from cStringIO import StringIO
from types import ListType

! # Intrapackage imports
! import Errors
! import Message

EMPTYSTRING = ''
NL = '\n'



class Parser:
! def __init__(self, _class=Message.Message):
"""Parser of RFC 2822 and MIME email messages.

--- 5,28 ----
"""

+ import re
from cStringIO import StringIO
from types import ListType

! from email import Errors
! from email import Message

EMPTYSTRING = ''
NL = '\n'

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


class Parser:
! def __init__(self, _class=Message.Message, strict=False):
"""Parser of RFC 2822 and MIME email messages.

***************
*** 33,47 ****
must be created. This class must have a constructor that can take
zero arguments. Default is Message.Message.
"""
self._class = _class

! def parse(self, fp):
root = self._class()
self._parseheaders(root, fp)
! self._parsebody(root, fp)
return root

! def parsestr(self, text):
! return self.parse(StringIO(text))

def _parseheaders(self, container, fp):
--- 39,75 ----
must be created. This class must have a constructor that can take
zero arguments. Default is Message.Message.
+
+ Optional strict tells the parser to be strictly RFC compliant or to be
+ more forgiving in parsing of ill-formatted MIME documents. When
+ non-strict mode is used, the parser will try to make up for missing or
+ erroneous boundaries and other peculiarities seen in the wild.
+ Default is non-strict parsing.
"""
self._class = _class
+ self._strict = strict

! def parse(self, fp, headersonly=False):
! """Create a message structure from the data in a file.
!
! Reads all the data from the file and returns the root of the message
! structure. Optional headersonly is a flag specifying whether to stop
! parsing after reading the headers or not. The default is False,
! meaning it parses the entire contents of the file.
! """
root = self._class()
self._parseheaders(root, fp)
! if not headersonly:
! self._parsebody(root, fp)
return root

! def parsestr(self, text, headersonly=False):
! """Create a message structure from a string.
!
! Returns the root of the message structure. Optional headersonly is a
! flag specifying whether to stop parsing after reading the headers or
! not. The default is False, meaning it parses the entire contents of
! the file.
! """
! return self.parse(StringIO(text), headersonly=headersonly)

def _parseheaders(self, container, fp):
***************
*** 51,58 ****
lastvalue = []
lineno = 0
! while 1:
! line = fp.readline()[:-1]
! if not line or not line.strip():
break
lineno += 1
# Check for initial Unix From_ line
--- 79,93 ----
lastvalue = []
lineno = 0
! while True:
! # Don't strip the line before we test for the end condition,
! # because whitespace-only header lines are RFC compliant
! # continuation lines.
! line = fp.readline()
! if not line:
break
+ line = line.splitlines()[0]
+ if not line:
+ break
+ # Ignore the trailing newline
lineno += 1
# Check for initial Unix From_ line
***************
*** 61,68 ****
container.set_unixfrom(line)
continue
! else:
raise Errors.HeaderParseError(
'Unix-from in headers after first rfc822 header')
# Header continuation line
if line[0] in ' \t':
--- 96,106 ----
container.set_unixfrom(line)
continue
! elif self._strict:
raise Errors.HeaderParseError(
'Unix-from in headers after first rfc822 header')
! else:
! # ignore the wierdly placed From_ line
! # XXX: maybe set unixfrom anyway? or only if not already?
! continue
# Header continuation line
if line[0] in ' \t':
***************
*** 79,84 ****
i = line.find(':')
if i < 0:
! raise Errors.HeaderParseError(
! 'Not a header, not a continuation')
if lastheader:
container[lastheader] = NL.join(lastvalue)
--- 117,129 ----
i = line.find(':')
if i < 0:
! 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)
***************
*** 101,143 ****
preamble = epilogue = None
# Split into subparts. The first boundary we're looking for won't
! # have the leading newline since we're at the start of the body
! # text.
separator = '--' + boundary
payload = fp.read()
! start = payload.find(separator)
! if start < 0:
! raise Errors.BoundaryError(
! "Couldn't find starting boundary: %s" % boundary)
if start > 0:
# there's some pre-MIME boundary preamble
preamble = payload[0:start]
! start += len(separator) + 1 + isdigest
! terminator = payload.find('\n' + separator + '--', start)
! if terminator < 0:
raise Errors.BoundaryError(
! "Couldn't find terminating boundary: %s" % boundary)
! if terminator+len(separator)+3 < len(payload):
! # there's some post-MIME boundary epilogue
! epilogue = payload[terminator+len(separator)+3:]
# We split the textual payload on the boundary separator, which
# multipart/digest then the subparts are by default message/rfc822
! # instead of text/plain. In that case, they'll have an extra
! # newline before the headers to distinguish the message's headers
! # from the subpart headers.
! if isdigest:
! separator += '\n\n'
! else:
! separator += '\n'
! parts = payload[start:terminator].split('\n' + separator)
for part in parts:
! msgobj = self.parsestr(part)
container.preamble = preamble
container.epilogue = epilogue
! # Ensure that the container's payload is a list
! if not isinstance(container.get_payload(), ListType):
! container.set_payload([msgobj])
! else:
! container.add_payload(msgobj)
elif container.get_type() == 'message/delivery-status':
# This special kind of type contains blocks of headers separated
--- 146,236 ----
preamble = epilogue = None
# Split into subparts. The first boundary we're looking for won't
! # always have a leading newline since we're at the start of the
! # body text, and there's not always a preamble before the first
! # boundary.
separator = '--' + boundary
payload = fp.read()
! # We use an RE here because boundaries can have trailing
! # whitespace.
! mo = re.search(
! r'(?P<sep>' + re.escape(separator) + r')(?P<ws>[ \t]*)',
! payload)
! if not mo:
! if self._strict:
! raise Errors.BoundaryError(
! "Couldn't find starting boundary: %s" % boundary)
! container.set_payload(payload)
! return
! start = mo.start()
if start > 0:
# there's some pre-MIME boundary preamble
preamble = payload[0:start]
! # Find out what kind of line endings we're using
! start += len(mo.group('sep')) + len(mo.group('ws'))
! cre = re.compile('\r\n|\r|\n')
! mo = cre.search(payload, start)
! if mo:
! start += len(mo.group(0))
! # We create a compiled regexp first because we need to be able to
! # specify the start position, and the module function doesn't
! # support this signature. :(
! cre = re.compile('(?P<sep>\r\n|\r|\n)' +
! re.escape(separator) + '--')
! mo = cre.search(payload, start)
! if mo:
! terminator = mo.start()
! linesep = mo.group('sep')
! if mo.end() < len(payload):
! # There's some post-MIME boundary epilogue
! epilogue = payload[mo.end():]
! elif self._strict:
raise Errors.BoundaryError(
! "Couldn't find terminating boundary: %s" % boundary)
! else:
! # Handle the case of no trailing boundary. Check that it ends
! # in a blank line. Some cases (spamspamspam) don't even have
! # that!
! mo = re.search('(?P<sep>\r\n|\r|\n){2}$', payload)
! if not mo:
! mo = re.search('(?P<sep>\r\n|\r|\n)$', payload)
! if not mo:
! raise Errors.BoundaryError(
! 'No terminating boundary and no trailing empty line')
! linesep = mo.group('sep')
! terminator = len(payload)
# We split the textual payload on the boundary separator, which
# multipart/digest then the subparts are by default message/rfc822
! # instead of text/plain. In that case, they'll have a optional
! # block of MIME headers, then an empty line followed by the
! # message headers.
! parts = re.split(
! linesep + re.escape(separator) + r'[ \t]*' + linesep,
! payload[start:terminator])
for part in parts:
! if isdigest:
! if part[0] == linesep:
! # There's no header block so create an empty message
! # object as the container, and lop off the newline so
! # we can parse the sub-subobject
! msgobj = self._class()
! part = part[1:]
! else:
! parthdrs, part = part.split(linesep+linesep, 1)
! # msgobj in this case is the "message/rfc822" container
! 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)
container.preamble = preamble
container.epilogue = epilogue
! container.attach(msgobj)
! elif container.get_main_type() == 'multipart':
! # Very bad. A message is a multipart with no boundary!
! raise Errors.BoundaryError(
! 'multipart message with no defined boundary')
elif container.get_type() == 'message/delivery-status':
# This special kind of type contains blocks of headers separated
***************
*** 145,149 ****
# separate Message object
blocks = []
! while 1:
blockmsg = self._class()
self._parseheaders(blockmsg, fp)
--- 238,242 ----
# separate Message object
blocks = []
! while True:
blockmsg = self._class()
self._parseheaders(blockmsg, fp)
***************
*** 161,167 ****
msg = self._class()
self._parsebody(msg, fp)
! container.add_payload(msg)
else:
! container.add_payload(fp.read())


--- 254,260 ----
msg = self._class()
self._parsebody(msg, fp)
! container.attach(msg)
else:
! container.set_payload(fp.read())



Index: Utils.py
===================================================================
RCS file: /cvsroot/python/python/dist/src/Lib/email/Utils.py,v
retrieving revision 1.9
retrieving revision 1.9.6.1
diff -C2 -d -r1.9 -r1.9.6.1
*** Utils.py 3 Dec 2001 19:26:40 -0000 1.9
--- Utils.py 4 Oct 2002 17:24:24 -0000 1.9.6.1
***************
*** 1,3 ****
# Author: barry@zope.com (Barry Warsaw)

--- 1,3 ----
# Author: barry@zope.com (Barry Warsaw)

***************
*** 6,24 ****

import time
import re

! from rfc822 import unquote, quote, parseaddr
! from rfc822 import dump_address_pair
! from rfc822 import AddrlistClass as _AddrlistClass
! from rfc822 import parsedate_tz, parsedate, mktime_tz

- from quopri import decodestring as _qdecode
import base64

# Intrapackage imports
! from Encoders import _bencode, _qencode

COMMASPACE = ', '
UEMPTYSTRING = u''


--- 6,60 ----

import time
+ import socket
import re
+ import random
+ import os
+ import warnings
+ from cStringIO import StringIO
+ 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:
! True, False
! except NameError:
! True = 1
! False = 0
!
! try:
! from quopri import decodestring as _qdecode
! except ImportError:
! # Python 2.1 doesn't have quopri.decodestring()
! def _qdecode(s):
! import quopri as _quopri
!
! if not s:
! return s
! infp = StringIO(s)
! outfp = StringIO()
! _quopri.decode(infp, outfp)
! value = outfp.getvalue()
! if not s.endswith('\n') and value.endswith('\n'):
! return value[:-1]
! return value

import base64

# Intrapackage imports
! from email.Encoders import _bencode, _qencode

COMMASPACE = ', '
+ EMPTYSTRING = ''
UEMPTYSTRING = u''
+ CRLF = '\r\n'
+
+ specialsre = re.compile(r'[][\()<>@,:;".]')
+ escapesre = re.compile(r'[][\()"]')


***************
*** 37,43 ****
if not s:
return s
- hasnewline = (s[-1] == '\n')
value = base64.decodestring(s)
! if not hasnewline and value[-1] == '\n':
return value[:-1]
return value
--- 73,78 ----
if not s:
return s
value = base64.decodestring(s)
! if not s.endswith('\n') and value.endswith('\n'):
return value[:-1]
return value
***************
*** 45,53 ****


def getaddresses(fieldvalues):
"""Return a list of (REALNAME, EMAIL) for each fieldvalue."""
all = COMMASPACE.join(fieldvalues)
! a = _AddrlistClass(all)
! return a.getaddrlist()


--- 80,123 ----


+ def fix_eols(s):
+ """Replace all line-ending characters with \r\n."""
+ # Fix newlines with no preceding carriage return
+ s = re.sub(r'(?<!\r)\n', CRLF, s)
+ # Fix carriage returns with no following newline
+ s = re.sub(r'\r(?!\n)', CRLF, s)
+ return s
+
+
+
+ def formataddr(pair):
+ """The inverse of parseaddr(), this takes a 2-tuple of the form
+ (realname, email_address) and returns the string value suitable
+ for an RFC 2822 From, To or Cc header.
+
+ If the first element of pair is false, then the second element is
+ returned unmodified.
+ """
+ name, address = pair
+ if name:
+ quotes = ''
+ if specialsre.search(name):
+ quotes = '"'
+ name = escapesre.sub(r'\\\g<0>', name)
+ return '%s%s%s <%s>' % (quotes, name, quotes, address)
+ return address
+
+ # For backwards compatibility
+ def dump_address_pair(pair):
+ warnings.warn('Use email.Utils.formataddr() instead',
+ DeprecationWarning, 2)
+ return formataddr(pair)
+
+
+
def getaddresses(fieldvalues):
"""Return a list of (REALNAME, EMAIL) for each fieldvalue."""
all = COMMASPACE.join(fieldvalues)
! a = _AddressList(all)
! return a.addresslist


***************
*** 65,92 ****

def decode(s):
! """Return a decoded string according to RFC 2047, as a unicode string."""
rtn = []
! parts = ecre.split(s, 1)
! while parts:
! # If there are less than 4 parts, it can't be encoded and we're done
! if len(parts) < 5:
! rtn.extend(parts)
! break
! # The first element is any non-encoded leading text
! rtn.append(parts[0])
! charset = parts[1]
! encoding = parts[2].lower()
! atom = parts[3]
! # The next chunk to decode should be in parts[4]
! parts = ecre.split(parts[4])
! # The encoding must be either `q' or `b', case-insensitive
! if encoding == 'q':
! func = _qdecode
! elif encoding == 'b':
! func = _bdecode
else:
! func = _identity
! # Decode and get the unicode in the charset
! rtn.append(unicode(func(atom), charset))
# Now that we've decoded everything, we just need to join all the parts
# together into the final string.
--- 135,158 ----

def decode(s):
! """Return a decoded string according to RFC 2047, as a unicode string.
!
! NOTE: This function is deprecated. Use Header.decode_header() instead.
! """
! warnings.warn('Use Header.decode_header() instead.', DeprecationWarning, 2)
! # Intra-package import here to avoid circular import problems.
! from email.Header import decode_header
! L = decode_header(s)
! if not isinstance(L, ListType):
! # s wasn't decoded
! return s
!
rtn = []
! for atom, charset in L:
! if charset is None:
! rtn.append(atom)
else:
! # Convert the string to Unicode using the given encoding. Leave
! # Unicode conversion errors to strict.
! rtn.append(unicode(atom, charset))
# Now that we've decoded everything, we just need to join all the parts
# together into the final string.
***************
*** 97,100 ****
--- 163,167 ----
def encode(s, charset='iso-8859-1', encoding='q'):
"""Encode a string according to RFC 2047."""
+ warnings.warn('Use Header.Header.encode() instead.', DeprecationWarning, 2)
encoding = encoding.lower()
if encoding == 'q':
***************
*** 108,112 ****


! def formatdate(timeval=None, localtime=0):
"""Returns a date string as specified by RFC 2822, e.g.:

--- 175,179 ----


! def formatdate(timeval=None, localtime=False):
"""Returns a date string as specified by RFC 2822, e.g.:

***************
*** 116,120 ****
gmtime() and localtime(), otherwise the current time is used.

! Optional localtime is a flag that when true, interprets timeval, and
returns a date relative to the local timezone instead of UTC, properly
taking daylight savings time into account.
--- 183,187 ----
gmtime() and localtime(), otherwise the current time is used.

! Optional localtime is a flag that when True, interprets timeval, and
returns a date relative to the local timezone instead of UTC, properly
taking daylight savings time into account.
***************
*** 151,152 ****
--- 218,340 ----
now[0], now[3], now[4], now[5],
zone)
+
+
+
+ def make_msgid(idstring=None):
+ """Returns a string suitable for RFC 2822 compliant Message-ID, e.g:
+
+ <20020201195627.33539.96671@nightshade.la.mastaler.com>
+
+ Optional idstring if given is a string used to strengthen the
+ uniqueness of the message id.
+ """
+ timeval = time.time()
+ utcdate = time.strftime('%Y%m%d%H%M%S', time.gmtime(timeval))
+ pid = os.getpid()
+ randint = random.randrange(100000)
+ if idstring is None:
+ idstring = ''
+ else:
+ idstring = '.' + idstring
+ idhost = socket.getfqdn()
+ msgid = '<%s.%s.%s%s@%s>' % (utcdate, pid, randint, idstring, idhost)
+ return msgid
+
+
+
+ # These functions are in the standalone mimelib version only because they've
+ # subsequently been fixed in the latest Python versions. We use this to worm
+ # around broken older Pythons.
+ def parsedate(data):
+ if not data:
+ return None
+ return _parsedate(data)
+
+
+ def parsedate_tz(data):
+ if not data:
+ return None
+ return _parsedate_tz(data)
+
+
+ def parseaddr(addr):
+ addrs = _AddressList(addr).addresslist
+ if not addrs:
+ return '', ''
+ return addrs[0]
+
+
+ # rfc822.unquote() doesn't properly de-backslash-ify in Python pre-2.3.
+ def unquote(str):
+ """Remove quotes from a string."""
+ if len(str) > 1:
+ if str.startswith('"') and str.endswith('"'):
+ return str[1:-1].replace('\\\\', '\\').replace('\\"', '"')
+ if str.startswith('<') and str.endswith('>'):
+ return str[1:-1]
+ return str
+
+
+
+ # RFC2231-related functions - parameter encoding and decoding
+ def decode_rfc2231(s):
+ """Decode string according to RFC 2231"""
+ import urllib
+ charset, language, s = s.split("'", 2)
+ s = urllib.unquote(s)
+ return charset, language, s
+
+
+ def encode_rfc2231(s, charset=None, language=None):
+ """Encode string according to RFC 2231.
+
+ If neither charset nor language is given, then s is returned as-is. If
+ charset is given but not language, the string is encoded using the empty
+ string for language.
+ """
+ import urllib
+ s = urllib.quote(s, safe='')
+ if charset is None and language is None:
+ return s
+ if language is None:
+ language = ''
+ return "%s'%s'%s" % (charset, language, s)
+
+
+ rfc2231_continuation = re.compile(r'^(?P<name>\w+)\*((?P<num>[0-9]+)\*?)?$')
+
+ def decode_params(params):
+ """Decode parameters list according to RFC 2231.
+
+ params is a sequence of 2-tuples containing (content type, string value).
+ """
+ new_params = []
+ # maps parameter's name to a list of continuations
+ rfc2231_params = {}
+ # params is a sequence of 2-tuples containing (content_type, string value)
+ name, value = params[0]
+ new_params.append((name, value))
+ # Cycle through each of the rest of the parameters.
+ for name, value in params[1:]:
+ value = unquote(value)
+ mo = rfc2231_continuation.match(name)
+ if mo:
+ name, num = mo.group('name', 'num')
+ if num is not None:
+ num = int(num)
+ rfc2231_param1 = rfc2231_params.setdefault(name, [])
+ rfc2231_param1.append((num, value))
+ else:
+ new_params.append((name, '"%s"' % quote(value)))
+ if rfc2231_params:
+ for name, continuations in rfc2231_params.items():
+ value = []
+ # Sort by number
+ continuations.sort()
+ # And now append all values in num order
+ for num, continuation in continuations:
+ 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
retrieving revision 1.4.10.1
diff -C2 -d -r1.4 -r1.4.10.1
*** __init__.py 9 Oct 2001 19:14:59 -0000 1.4
--- __init__.py 4 Oct 2002 17:24:24 -0000 1.4.10.1
***************
*** 1,3 ****
# Author: barry@zope.com (Barry Warsaw)

--- 1,3 ----
# Author: barry@zope.com (Barry Warsaw)

***************
*** 5,35 ****
"""

! __version__ = '1.0'

! __all__ = [.'Encoders',
! 'Errors',
! 'Generator',
! 'Iterators',
! 'MIMEAudio',
! 'MIMEBase',
! 'MIMEImage',
! 'MIMEMessage',
! 'MIMEText',
! 'Message',
! 'Parser',
! 'Utils',
! 'message_from_string',
! 'message_from_file',
! ]



! # Some convenience routines
! from Parser import Parser as _Parser
! from Message import Message as _Message

! def message_from_string(s, _class=_Message):
! return _Parser(_class).parsestr(s)

! def message_from_file(fp, _class=_Message):
! return _Parser(_class).parse(fp)
--- 5,72 ----
"""

! __version__ = '2.4'

! __all__ = [.
! 'base64MIME',
! 'Charset',
! 'Encoders',
! 'Errors',
! 'Generator',
! 'Header',
! 'Iterators',
! 'Message',
! 'MIMEAudio',
! 'MIMEBase',
! 'MIMEImage',
! 'MIMEMessage',
! 'MIMEMultipart',
! 'MIMENonMultipart',
! 'MIMEText',
! 'Parser',
! 'quopriMIME',
! 'Utils',
! 'message_from_string',
! 'message_from_file',
! ]
!
! try:
! True, False
! except NameError:
! True = 1
! False = 0



! # Some convenience routines. Don't import Parser and Message as side-effects
! # of importing email since those cascadingly import most of the rest of the
! # email package.
! def message_from_string(s, _class=None, strict=False):
! """Parse a string into a Message object model.

! Optional _class and strict are passed to the Parser constructor.
! """
! from email.Parser import Parser
! if _class is None:
! from email.Message import Message
! _class = Message
! return Parser(_class, strict=strict).parsestr(s)

! def message_from_file(fp, _class=None, strict=False):
! """Read a file and parse its contents into a Message object model.
!
! Optional _class and strict are passed to the Parser constructor.
! """
! from email.Parser import Parser
! if _class is None:
! from email.Message import Message
! _class = Message
! return Parser(_class, strict=strict).parse(fp)
!
!
!
! # Patch encodings.aliases to recognize 'ansi_x3.4_1968' which isn't a standard
! # alias in Python 2.1.3, but is used by the email package test suite.
! from encodings.aliases import aliases # The aliases dictionary
! if not aliases.has_key('ansi_x3.4_1968'):
! aliases['ansi_x3.4_1968'] = 'ascii'
! del aliases # Not needed any more