The email package version 1.0, prototyped as mimelib
<http://sf.net/projects/mimelib>. There /are/ API differences between mimelib and email, but most of the implementations are shared (except where cool Py2.2 stuff like generators are used).
This commit is contained in:
parent
d61d0d3f6d
commit
ba92580f01
|
@ -0,0 +1,68 @@
|
|||
# Copyright (C) 2001 Python Software Foundation
|
||||
# Author: barry@zope.com (Barry Warsaw)
|
||||
|
||||
"""Module containing encoding functions for Image.Image and Text.Text.
|
||||
"""
|
||||
|
||||
import base64
|
||||
from quopri import encodestring as _encodestring
|
||||
|
||||
|
||||
|
||||
# Helpers
|
||||
def _qencode(s):
|
||||
return _encodestring(s, quotetabs=1)
|
||||
|
||||
def _bencode(s):
|
||||
# We can't quite use base64.encodestring() since it tacks on a "courtesy
|
||||
# newline". Blech!
|
||||
if not s:
|
||||
return s
|
||||
hasnewline = (s[-1] == '\n')
|
||||
value = base64.encodestring(s)
|
||||
if not hasnewline and value[-1] == '\n':
|
||||
return value[:-1]
|
||||
return value
|
||||
|
||||
|
||||
|
||||
def encode_base64(msg):
|
||||
"""Encode the message's payload in Base64.
|
||||
|
||||
Also, add an appropriate Content-Transfer-Encoding: header.
|
||||
"""
|
||||
orig = msg.get_payload()
|
||||
encdata = _bencode(orig)
|
||||
msg.set_payload(encdata)
|
||||
msg['Content-Transfer-Encoding'] = 'base64'
|
||||
|
||||
|
||||
|
||||
def encode_quopri(msg):
|
||||
"""Encode the message's payload in Quoted-Printable.
|
||||
|
||||
Also, add an appropriate Content-Transfer-Encoding: header.
|
||||
"""
|
||||
orig = msg.get_payload()
|
||||
encdata = _qencode(orig)
|
||||
msg.set_payload(encdata)
|
||||
msg['Content-Transfer-Encoding'] = 'quoted-printable'
|
||||
|
||||
|
||||
|
||||
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.
|
||||
try:
|
||||
orig.encode('ascii')
|
||||
except UnicodeError:
|
||||
msg['Content-Transfer-Encoding'] = '8bit'
|
||||
else:
|
||||
msg['Content-Transfer-Encoding'] = '7bit'
|
||||
|
||||
|
||||
|
||||
def encode_noop(msg):
|
||||
"""Do nothing."""
|
|
@ -0,0 +1,26 @@
|
|||
# Copyright (C) 2001 Python Software Foundation
|
||||
# Author: barry@zope.com (Barry Warsaw)
|
||||
|
||||
"""email package exception classes.
|
||||
"""
|
||||
|
||||
|
||||
|
||||
class MessageError(Exception):
|
||||
"""Base class for errors in this module."""
|
||||
|
||||
|
||||
class MessageParseError(MessageError):
|
||||
"""Base class for message parsing errors."""
|
||||
|
||||
|
||||
class HeaderParseError(MessageParseError):
|
||||
"""Error while parsing headers."""
|
||||
|
||||
|
||||
class BoundaryError(MessageParseError):
|
||||
"""Couldn't find terminating boundary."""
|
||||
|
||||
|
||||
class MultipartConversionError(MessageError, TypeError):
|
||||
"""Conversion to a multipart is prohibited."""
|
|
@ -0,0 +1,326 @@
|
|||
# Copyright (C) 2001 Python Software Foundation
|
||||
# Author: barry@zope.com (Barry Warsaw)
|
||||
|
||||
"""Classes to generate plain text from a message object tree.
|
||||
"""
|
||||
|
||||
import time
|
||||
import re
|
||||
import random
|
||||
|
||||
from types import ListType, StringType
|
||||
from cStringIO import StringIO
|
||||
|
||||
# Intrapackage imports
|
||||
import Message
|
||||
import Errors
|
||||
|
||||
SEMISPACE = '; '
|
||||
BAR = '|'
|
||||
UNDERSCORE = '_'
|
||||
NL = '\n'
|
||||
SEMINLTAB = ';\n\t'
|
||||
SPACE8 = ' ' * 8
|
||||
|
||||
fcre = re.compile(r'^From ', re.MULTILINE)
|
||||
|
||||
|
||||
|
||||
class Generator:
|
||||
"""Generates output from a Message object tree.
|
||||
|
||||
This basic generator writes the message to the given file object as plain
|
||||
text.
|
||||
"""
|
||||
#
|
||||
# Public interface
|
||||
#
|
||||
|
||||
def __init__(self, outfp, mangle_from_=1, maxheaderlen=78):
|
||||
"""Create the generator for message flattening.
|
||||
|
||||
outfp is the output file-like object for writing the message to. It
|
||||
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
|
||||
header. When a header line is longer (in characters, with tabs
|
||||
expanded to 8 spaces), than maxheaderlen, the header will be broken on
|
||||
semicolons and continued as per RFC 2822. If no semicolon is found,
|
||||
then the header is left alone. Set to zero to disable wrapping
|
||||
headers. Default is 78, as recommended (but not required by RFC
|
||||
2822.
|
||||
"""
|
||||
self._fp = outfp
|
||||
self._mangle_from_ = mangle_from_
|
||||
self.__first = 1
|
||||
self.__maxheaderlen = maxheaderlen
|
||||
|
||||
def write(self, s):
|
||||
# Just delegate to the file object
|
||||
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.
|
||||
|
||||
unixfrom is a flag that forces the printing of a Unix From_ delimiter
|
||||
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.
|
||||
"""
|
||||
if unixfrom:
|
||||
ufrom = msg.get_unixfrom()
|
||||
if not ufrom:
|
||||
ufrom = 'From nobody ' + time.ctime(time.time())
|
||||
print >> self._fp, ufrom
|
||||
self._write(msg)
|
||||
|
||||
#
|
||||
# Protected interface - undocumented ;/
|
||||
#
|
||||
|
||||
def _write(self, msg):
|
||||
# We can't write the headers yet because of the following scenario:
|
||||
# say a multipart message includes the boundary string somewhere in
|
||||
# its body. We'd have to calculate the new boundary /before/ we write
|
||||
# the headers so that we can write the correct Content-Type:
|
||||
# parameter.
|
||||
#
|
||||
# The way we do this, so as to make the _handle_*() methods simpler,
|
||||
# is to cache any subpart writes into a StringIO. The we write the
|
||||
# headers and the StringIO contents. That way, subpart handlers can
|
||||
# Do The Right Thing, and can still modify the Content-Type: header if
|
||||
# necessary.
|
||||
oldfp = self._fp
|
||||
try:
|
||||
self._fp = sfp = StringIO()
|
||||
self._dispatch(msg)
|
||||
finally:
|
||||
self._fp = oldfp
|
||||
# Write the headers. First we see if the message object wants to
|
||||
# handle that itself. If not, we'll do it generically.
|
||||
meth = getattr(msg, '_write_headers', None)
|
||||
if meth is None:
|
||||
self._write_headers(msg)
|
||||
else:
|
||||
meth(self)
|
||||
self._fp.write(sfp.getvalue())
|
||||
|
||||
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)
|
||||
|
||||
#
|
||||
# Default handlers
|
||||
#
|
||||
|
||||
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
|
||||
# headers.
|
||||
text = '%s: %s' % (h, v)
|
||||
if self.__maxheaderlen > 0 and len(text) > self.__maxheaderlen:
|
||||
text = self._split_header(text)
|
||||
print >> self._fp, text
|
||||
# A blank line always separates headers from body
|
||||
print >> self._fp
|
||||
|
||||
def _split_header(self, text):
|
||||
maxheaderlen = self.__maxheaderlen
|
||||
# Find out whether any lines in the header are really longer than
|
||||
# maxheaderlen characters wide. There could be continuation lines
|
||||
# that actually shorten it. Also, replace hard tabs with 8 spaces.
|
||||
lines = [s.replace('\t', SPACE8) for s in text.split('\n')]
|
||||
for line in lines:
|
||||
if len(line) > maxheaderlen:
|
||||
break
|
||||
else:
|
||||
# No line was actually longer than maxheaderlen characters, so
|
||||
# just return the original unchanged.
|
||||
return text
|
||||
rtn = []
|
||||
for line in text.split('\n'):
|
||||
# Short lines can remain unchanged
|
||||
if len(line.replace('\t', SPACE8)) <= maxheaderlen:
|
||||
rtn.append(line)
|
||||
else:
|
||||
# Try to break the line on semicolons, but if that doesn't
|
||||
# work, then just leave it alone.
|
||||
while len(text) > maxheaderlen:
|
||||
i = text.rfind(';', 0, maxheaderlen)
|
||||
if i < 0:
|
||||
rtn.append(text)
|
||||
break
|
||||
rtn.append(text[:i])
|
||||
text = text[i+1:].lstrip()
|
||||
rtn.append(text)
|
||||
return SEMINLTAB.join(rtn)
|
||||
|
||||
#
|
||||
# Handlers for writing types and subtypes
|
||||
#
|
||||
|
||||
def _handle_text(self, msg):
|
||||
payload = msg.get_payload()
|
||||
if not isinstance(payload, StringType):
|
||||
raise TypeError, 'string payload expected'
|
||||
if self._mangle_from_:
|
||||
payload = fcre.sub('>From ', payload)
|
||||
self._fp.write(payload)
|
||||
|
||||
# Default body handler
|
||||
_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 = []
|
||||
for part in msg.get_payload():
|
||||
s = StringIO()
|
||||
g = self.__class__(s)
|
||||
g(part, unixfrom=0)
|
||||
msgtexts.append(s.getvalue())
|
||||
# Now make sure the boundary we've selected doesn't appear in any of
|
||||
# the message texts.
|
||||
alltext = NL.join(msgtexts)
|
||||
# BAW: What about boundaries that are wrapped in double-quotes?
|
||||
boundary = msg.get_boundary(failobj=_make_boundary(alltext))
|
||||
# If we had to calculate a new boundary because the body text
|
||||
# contained that string, set the new boundary. We don't do it
|
||||
# unconditionally because, while set_boundary() preserves order, it
|
||||
# doesn't preserve newlines/continuations in headers. This is no big
|
||||
# deal in practice, but turns out to be inconvenient for the unittest
|
||||
# suite.
|
||||
if msg.get_boundary() <> boundary:
|
||||
msg.set_boundary(boundary)
|
||||
# Write out any preamble
|
||||
if msg.preamble is not None:
|
||||
self._fp.write(msg.preamble)
|
||||
# First boundary is a bit different; it doesn't have a leading extra
|
||||
# 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 + '--',
|
||||
# Write out any epilogue
|
||||
if msg.epilogue is not None:
|
||||
self._fp.write(msg.epilogue)
|
||||
|
||||
def _handle_multipart_digest(self, msg):
|
||||
self._handle_multipart(msg, isdigest=1)
|
||||
|
||||
def _handle_message_rfc822(self, msg):
|
||||
s = StringIO()
|
||||
g = self.__class__(s)
|
||||
# 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())
|
||||
|
||||
|
||||
|
||||
class DecodedGenerator(Generator):
|
||||
"""Generator a text representation of a message.
|
||||
|
||||
Like the Generator base class, except that non-text parts are substituted
|
||||
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.
|
||||
|
||||
Walks through all subparts of a message. If the subpart is of main
|
||||
type `text', then it prints the decoded payload of the subpart.
|
||||
|
||||
Otherwise, fmt is a format string that is used instead of the message
|
||||
payload. fmt is expanded with the following keywords (in
|
||||
%(keyword)s format):
|
||||
|
||||
type : Full MIME type of the non-text part
|
||||
maintype : Main MIME type of the non-text part
|
||||
subtype : Sub-MIME type of the non-text part
|
||||
filename : Filename of the non-text part
|
||||
description: Description associated with the non-text part
|
||||
encoding : Content transfer encoding of the non-text part
|
||||
|
||||
The default value for fmt is None, meaning
|
||||
|
||||
[Non-text (%(type)s) part of message omitted, filename %(filename)s]
|
||||
"""
|
||||
Generator.__init__(self, outfp, mangle_from_, maxheaderlen)
|
||||
if fmt is None:
|
||||
fmt = ('[Non-text (%(type)s) part of message omitted, '
|
||||
'filename %(filename)s]')
|
||||
self._fmt = fmt
|
||||
|
||||
def _dispatch(self, msg):
|
||||
for part in msg.walk():
|
||||
if part.get_main_type('text') == 'text':
|
||||
print >> self, part.get_payload(decode=1)
|
||||
else:
|
||||
print >> self, self._fmt % {
|
||||
'type' : part.get_type('[no MIME type]'),
|
||||
'maintype' : part.get_main_type('[no main MIME type]'),
|
||||
'subtype' : part.get_subtype('[no sub-MIME type]'),
|
||||
'filename' : part.get_filename('[no filename]'),
|
||||
'description': part.get('Content-Description',
|
||||
'[no description]'),
|
||||
'encoding' : part.get('Content-Transfer-Encoding',
|
||||
'[no encoding]'),
|
||||
}
|
||||
|
||||
|
||||
|
||||
# Helper
|
||||
def _make_boundary(self, text=None):
|
||||
# Craft a random boundary. If text is given, ensure that the chosen
|
||||
# boundary doesn't appear in the text.
|
||||
boundary = ('=' * 15) + repr(random.random()).split('.')[1] + '=='
|
||||
if text is None:
|
||||
return boundary
|
||||
b = boundary
|
||||
counter = 0
|
||||
while 1:
|
||||
cre = re.compile('^--' + re.escape(b) + '(--)?$', re.MULTILINE)
|
||||
if not cre.search(text):
|
||||
break
|
||||
b = boundary + '.' + str(counter)
|
||||
counter += 1
|
||||
return b
|
|
@ -0,0 +1,46 @@
|
|||
# Copyright (C) 2001 Python Software Foundation
|
||||
# Author: barry@zope.com (Barry Warsaw)
|
||||
|
||||
"""Class representing image/* type MIME documents.
|
||||
"""
|
||||
|
||||
import imghdr
|
||||
|
||||
# Intrapackage imports
|
||||
import MIMEBase
|
||||
import Errors
|
||||
import Encoders
|
||||
|
||||
|
||||
|
||||
class Image(MIMEBase.MIMEBase):
|
||||
"""Class for generating image/* type MIME documents."""
|
||||
|
||||
def __init__(self, _imagedata, _minor=None,
|
||||
_encoder=Encoders.encode_base64, **_params):
|
||||
"""Create an image/* type MIME document.
|
||||
|
||||
_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 _minor
|
||||
parameter.
|
||||
|
||||
_encoder is a function which will perform the actual encoding for
|
||||
transport of the image data. It takes one argument, which is this
|
||||
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.
|
||||
"""
|
||||
if _minor is None:
|
||||
_minor = imghdr.what(None, _imagedata)
|
||||
if _minor is None:
|
||||
raise TypeError, 'Could not guess image _minor type'
|
||||
MIMEBase.MIMEBase.__init__(self, 'image', _minor, **_params)
|
||||
self.set_payload(_imagedata)
|
||||
_encoder(self)
|
|
@ -0,0 +1,33 @@
|
|||
# Copyright (C) 2001 Python Software Foundation
|
||||
# Author: barry@zope.com (Barry Warsaw)
|
||||
|
||||
"""Various types of useful iterators and generators.
|
||||
"""
|
||||
|
||||
from __future__ import generators
|
||||
from cStringIO import StringIO
|
||||
from types import StringType
|
||||
|
||||
|
||||
|
||||
def body_line_iterator(msg):
|
||||
"""Iterator over the parts, returning the lines in a string payload."""
|
||||
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, major='text', minor=None):
|
||||
"""Iterator over the subparts with a given MIME type.
|
||||
|
||||
Use `major' as the main MIME type to match against; this defaults to
|
||||
"text". Optional `minor' 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() == major:
|
||||
if minor is None or subpart.get_subtype() == minor:
|
||||
yield subpart
|
|
@ -0,0 +1,24 @@
|
|||
# Copyright (C) 2001 Python Software Foundation
|
||||
# Author: barry@zope.com (Barry Warsaw)
|
||||
|
||||
"""Base class for MIME specializations.
|
||||
"""
|
||||
|
||||
import Message
|
||||
|
||||
|
||||
|
||||
class MIMEBase(Message.Message):
|
||||
"""Base class for MIME specializations."""
|
||||
|
||||
def __init__(self, _major, _minor, **_params):
|
||||
"""This constructor adds a Content-Type: and a MIME-Version: header.
|
||||
|
||||
The Content-Type: header is taken from the _major and _minor
|
||||
arguments. Additional parameters for this header are taken from the
|
||||
keyword arguments.
|
||||
"""
|
||||
Message.Message.__init__(self)
|
||||
ctype = '%s/%s' % (_major, _minor)
|
||||
self.add_header('Content-Type', ctype, **_params)
|
||||
self['MIME-Version'] = '1.0'
|
|
@ -0,0 +1,422 @@
|
|||
# Copyright (C) 2001 Python Software Foundation
|
||||
# Author: barry@zope.com (Barry Warsaw)
|
||||
|
||||
"""Basic message object for the email package object model.
|
||||
"""
|
||||
|
||||
from __future__ import generators
|
||||
|
||||
import re
|
||||
import base64
|
||||
import quopri
|
||||
from cStringIO import StringIO
|
||||
from types import ListType
|
||||
|
||||
SEMISPACE = '; '
|
||||
|
||||
# Intrapackage imports
|
||||
import Errors
|
||||
import Utils
|
||||
|
||||
|
||||
|
||||
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):
|
||||
self._headers = []
|
||||
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
|
||||
|
||||
#
|
||||
# Unix From_ line
|
||||
#
|
||||
def set_unixfrom(self, unixfrom):
|
||||
self._unixfrom = unixfrom
|
||||
|
||||
def get_unixfrom(self):
|
||||
return self._unixfrom
|
||||
|
||||
#
|
||||
# Payload manipulation.
|
||||
#
|
||||
def add_payload(self, payload):
|
||||
"""Add the given payload to the current payload.
|
||||
|
||||
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:
|
||||
payload = self._payload[i]
|
||||
if decode:
|
||||
if self.is_multipart():
|
||||
return None
|
||||
cte = self.get('content-transfer-encoding', '')
|
||||
if cte.lower() == 'quoted-printable':
|
||||
return Utils._qdecode(payload)
|
||||
elif cte.lower() == 'base64':
|
||||
return Utils._bdecode(payload)
|
||||
# Everything else, including encodings with 8bit or 7bit are returned
|
||||
# unchanged.
|
||||
return payload
|
||||
|
||||
|
||||
def set_payload(self, payload):
|
||||
"""Set the payload to the given value."""
|
||||
self._payload = payload
|
||||
|
||||
#
|
||||
# MAPPING INTERFACE (partial)
|
||||
#
|
||||
def __len__(self):
|
||||
"""Get the total number of headers, including duplicates."""
|
||||
return len(self._headers)
|
||||
|
||||
def __getitem__(self, name):
|
||||
"""Get a header value.
|
||||
|
||||
Return None if the header is missing instead of raising an exception.
|
||||
|
||||
Note that if the header appeared multiple times, exactly which
|
||||
occurrance gets returned is undefined. Use getall() to get all
|
||||
the values matching a header field name.
|
||||
"""
|
||||
return self.get(name)
|
||||
|
||||
def __setitem__(self, name, val):
|
||||
"""Set the value of a header.
|
||||
|
||||
Note: this does not overwrite an existing header with the same field
|
||||
name. Use __delitem__() first to delete any existing headers.
|
||||
"""
|
||||
self._headers.append((name, val))
|
||||
|
||||
def __delitem__(self, name):
|
||||
"""Delete all occurrences of a header, if present.
|
||||
|
||||
Does not raise an exception if the header is missing.
|
||||
"""
|
||||
name = name.lower()
|
||||
newheaders = []
|
||||
for k, v in self._headers:
|
||||
if k.lower() <> name:
|
||||
newheaders.append((k, v))
|
||||
self._headers = newheaders
|
||||
|
||||
def __contains__(self, key):
|
||||
return key.lower() in [k.lower() for k, v in self._headers]
|
||||
|
||||
def has_key(self, name):
|
||||
"""Return true if the message contains the header."""
|
||||
return self[name] <> None
|
||||
|
||||
def keys(self):
|
||||
"""Return a list of all the message's header field names.
|
||||
|
||||
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]
|
||||
|
||||
def values(self):
|
||||
"""Return a list of all the message's header values.
|
||||
|
||||
These will be sorted in the order they appeared in the original
|
||||
message, and may contain duplicates. Any fields deleted and
|
||||
re-inserted are alwyas appended to the header list.
|
||||
"""
|
||||
return [v for k, v in self._headers]
|
||||
|
||||
def items(self):
|
||||
"""Get all the message's header fields and values.
|
||||
|
||||
These will be sorted in the order they appeared in the original
|
||||
message, and may contain duplicates. Any fields deleted and
|
||||
re-inserted are alwyas appended to the header list.
|
||||
"""
|
||||
return self._headers[:]
|
||||
|
||||
def get(self, name, failobj=None):
|
||||
"""Get a header value.
|
||||
|
||||
Like __getitem__() but return failobj instead of None when the field
|
||||
is missing.
|
||||
"""
|
||||
name = name.lower()
|
||||
for k, v in self._headers:
|
||||
if k.lower() == name:
|
||||
return v
|
||||
return failobj
|
||||
|
||||
#
|
||||
# Additional useful stuff
|
||||
#
|
||||
|
||||
def get_all(self, name, failobj=None):
|
||||
"""Return a list of all the values for the named field.
|
||||
|
||||
These will be sorted in the order they appeared in the original
|
||||
message, and may contain duplicates. Any fields deleted and
|
||||
re-inserted are alwyas appended to the header list.
|
||||
"""
|
||||
values = []
|
||||
name = name.lower()
|
||||
for k, v in self._headers:
|
||||
if k.lower() == name:
|
||||
values.append(v)
|
||||
return values
|
||||
|
||||
def add_header(self, _name, _value, **_params):
|
||||
"""Extended header setting.
|
||||
|
||||
name is the header field to add. keyword arguments can be used to set
|
||||
additional parameters for the header field, with underscores converted
|
||||
to dashes. Normally the parameter will be added as key="value" unless
|
||||
value is None, in which case only the key will be added.
|
||||
|
||||
Example:
|
||||
|
||||
msg.add_header('content-disposition', 'attachment', filename='bud.gif')
|
||||
|
||||
"""
|
||||
parts = []
|
||||
for k, v in _params.items():
|
||||
if v is None:
|
||||
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).
|
||||
"""
|
||||
missing = []
|
||||
value = self.get('content-type', missing)
|
||||
if value is missing:
|
||||
return failobj
|
||||
return re.split(r';\s+', value)[0].lower()
|
||||
|
||||
def get_main_type(self, failobj=None):
|
||||
"""Return the message's main content type if present."""
|
||||
missing = []
|
||||
ctype = self.get_type(missing)
|
||||
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):
|
||||
"""Return the message's content subtype if present."""
|
||||
missing = []
|
||||
ctype = self.get_type(missing)
|
||||
if ctype is missing:
|
||||
return failobj
|
||||
parts = ctype.split('/')
|
||||
if len(parts) > 1:
|
||||
return ctype.split('/')[1]
|
||||
return failobj
|
||||
|
||||
def get_params(self, failobj=None, header='content-type'):
|
||||
"""Return the message's Content-Type: parameters, as a list.
|
||||
|
||||
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 = []
|
||||
value = self.get(header, missing)
|
||||
if value is missing:
|
||||
return failobj
|
||||
return re.split(r';\s+', value)[1:]
|
||||
|
||||
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:
|
||||
"""
|
||||
param = param.lower()
|
||||
missing = []
|
||||
params = self.get_params(missing, header=header)
|
||||
if params is missing:
|
||||
return failobj
|
||||
for p in params:
|
||||
try:
|
||||
name, val = p.split('=', 1)
|
||||
except ValueError:
|
||||
# Must have been a bare attribute
|
||||
name = p
|
||||
val = ''
|
||||
if name.lower() == param:
|
||||
return Utils.unquote(val)
|
||||
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.
|
||||
"""
|
||||
missing = []
|
||||
filename = self.get_param('filename', missing, 'content-disposition')
|
||||
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.
|
||||
"""
|
||||
missing = []
|
||||
boundary = self.get_param('boundary', missing)
|
||||
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.
|
||||
"""
|
||||
params = self.get_params()
|
||||
if not params:
|
||||
# There was no Content-Type: header, and we don't know what type
|
||||
# to set it to, so raise an exception.
|
||||
raise Errors.HeaderParseError, 'No Content-Type: header found'
|
||||
newparams = []
|
||||
foundp = 0
|
||||
for p in params:
|
||||
if p.lower().startswith('boundary='):
|
||||
newparams.append('boundary="%s"' % boundary)
|
||||
foundp = 1
|
||||
else:
|
||||
newparams.append(p)
|
||||
if not foundp:
|
||||
# The original Content-Type: header had no boundary attribute.
|
||||
# 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:
|
||||
if h.lower() == 'content-type':
|
||||
value = v.split(';', 1)[0]
|
||||
newparams.insert(0, value)
|
||||
newheaders.append((h, SEMISPACE.join(newparams)))
|
||||
else:
|
||||
newheaders.append((h, v))
|
||||
self._headers = newheaders
|
||||
|
||||
def walk(self):
|
||||
"""Walk over the message tree, yielding each subpart.
|
||||
|
||||
The walk is performed in breadth-first order. This method is a
|
||||
generator.
|
||||
"""
|
||||
if self.is_multipart():
|
||||
for subpart in self.get_payload():
|
||||
for subsubpart in subpart.walk():
|
||||
yield subsubpart
|
||||
else:
|
||||
yield self
|
||||
|
||||
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.
|
||||
|
||||
The list will contain one string for each part of the message, plus
|
||||
one for the container message (i.e. self), so that a non-multipart
|
||||
message will still return a list of length 1.
|
||||
"""
|
||||
return [part.get_param('charset', failobj) for part in self.walk()]
|
|
@ -0,0 +1,24 @@
|
|||
# Copyright (C) 2001 Python Software Foundation
|
||||
# Author: barry@zope.com (Barry Warsaw)
|
||||
|
||||
"""Class for generating message/rfc822 MIME documents.
|
||||
"""
|
||||
|
||||
import Message
|
||||
import MIMEBase
|
||||
|
||||
|
||||
|
||||
class MessageRFC822(MIMEBase.MIMEBase):
|
||||
"""Class for generating message/rfc822 MIME documents."""
|
||||
|
||||
def __init__(self, _msg):
|
||||
"""Create a message/rfc822 type MIME document.
|
||||
|
||||
_msg is a message object and must be an instance of Message, or a
|
||||
derived class of Message, otherwise a TypeError is raised.
|
||||
"""
|
||||
MIMEBase.MIMEBase.__init__(self, 'message', 'rfc822')
|
||||
if not isinstance(_msg, Message.Message):
|
||||
raise TypeError, 'Argument is not an instance of Message'
|
||||
self.set_payload(_msg)
|
|
@ -0,0 +1,154 @@
|
|||
# Copyright (C) 2001 Python Software Foundation
|
||||
# Author: barry@zope.com (Barry Warsaw)
|
||||
|
||||
"""A parser of RFC 2822 and MIME email messages.
|
||||
"""
|
||||
|
||||
import re
|
||||
from cStringIO import StringIO
|
||||
|
||||
# Intrapackage imports
|
||||
import Errors
|
||||
import Message
|
||||
|
||||
bcre = re.compile('boundary="?([^"]+)"?', re.IGNORECASE)
|
||||
EMPTYSTRING = ''
|
||||
NL = '\n'
|
||||
|
||||
|
||||
|
||||
class Parser:
|
||||
def __init__(self, _class=Message.Message):
|
||||
"""Parser of RFC 2822 and MIME email messages.
|
||||
|
||||
Creates an in-memory object tree representing the email message, which
|
||||
can then be manipulated and turned over to a Generator to return the
|
||||
textual representation of the message.
|
||||
|
||||
The string must be formatted as a block of RFC 2822 headers and header
|
||||
continuation lines, optionally preceeded by a `Unix-from' header. The
|
||||
header block is terminated either by the end of the string or by a
|
||||
blank line.
|
||||
|
||||
_class is the class to instantiate for new message objects when they
|
||||
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):
|
||||
# Parse the headers, returning a list of header/value pairs. None as
|
||||
# the header means the Unix-From header.
|
||||
lastheader = ''
|
||||
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
|
||||
if line.startswith('From '):
|
||||
if lineno == 1:
|
||||
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':
|
||||
if not lastheader:
|
||||
raise Errors.HeaderParseError(
|
||||
'Continuation line seen before first header')
|
||||
lastvalue.append(line)
|
||||
continue
|
||||
# Normal, non-continuation header. BAW: this should check to make
|
||||
# sure it's a legal header, e.g. doesn't contain spaces. Also, we
|
||||
# should expose the header matching algorithm in the API, and
|
||||
# allow for a non-strict parsing mode (that ignores the line
|
||||
# instead of raising the exception).
|
||||
i = line.find(':')
|
||||
if i < 0:
|
||||
raise Errors.HeaderParseError(
|
||||
'Not a header, not a continuation')
|
||||
if lastheader:
|
||||
container[lastheader] = NL.join(lastvalue)
|
||||
lastheader = line[:i]
|
||||
lastvalue = [line[i+1:].lstrip()]
|
||||
# Make sure we retain the last header
|
||||
if lastheader:
|
||||
container[lastheader] = NL.join(lastvalue)
|
||||
|
||||
def _parsebody(self, container, fp):
|
||||
# Parse the body, but first split the payload on the content-type
|
||||
# boundary if present.
|
||||
boundary = isdigest = None
|
||||
ctype = container['content-type']
|
||||
if ctype:
|
||||
mo = bcre.search(ctype)
|
||||
if mo:
|
||||
boundary = mo.group(1)
|
||||
isdigest = container.get_type() == 'multipart/digest'
|
||||
# If there's a boundary, split the payload text into its constituent
|
||||
# parts and parse each separately. Otherwise, just parse the rest of
|
||||
# the body as a single message. Note: any exceptions raised in the
|
||||
# recursive parse need to have their line numbers coerced.
|
||||
if boundary:
|
||||
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
|
||||
# includes the trailing newline. If the container is a
|
||||
# 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
|
||||
container.add_payload(msgobj)
|
||||
elif ctype == 'message/rfc822':
|
||||
# Create a container for the payload, but watch out for there not
|
||||
# being any headers left
|
||||
try:
|
||||
msg = self.parse(fp)
|
||||
except Errors.HeaderParseError:
|
||||
msg = self._class()
|
||||
self._parsebody(msg, fp)
|
||||
container.add_payload(msg)
|
||||
else:
|
||||
container.add_payload(fp.read())
|
|
@ -0,0 +1,41 @@
|
|||
# Copyright (C) 2001 Python Software Foundation
|
||||
# Author: barry@zope.com (Barry Warsaw)
|
||||
|
||||
"""Class representing text/* type MIME documents.
|
||||
"""
|
||||
|
||||
import MIMEBase
|
||||
from Encoders import encode_7or8bit
|
||||
|
||||
|
||||
|
||||
class Text(MIMEBase.MIMEBase):
|
||||
"""Class for generating text/* type MIME documents."""
|
||||
|
||||
def __init__(self, _text, _minor='plain', _charset='us-ascii',
|
||||
_encoder=encode_7or8bit):
|
||||
"""Create a text/* type MIME document.
|
||||
|
||||
_text is the string for this message object. If the text does not end
|
||||
in a newline, one is added.
|
||||
|
||||
_minor is the minor 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', _minor,
|
||||
**{'charset': _charset})
|
||||
if _text and _text[-1] <> '\n':
|
||||
_text += '\n'
|
||||
self.set_payload(_text)
|
||||
_encoder(self)
|
|
@ -0,0 +1,104 @@
|
|||
# Copyright (C) 2001 Python Software Foundation
|
||||
# Author: barry@zope.com (Barry Warsaw)
|
||||
|
||||
"""Miscellaneous utilities.
|
||||
"""
|
||||
|
||||
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, formatdate
|
||||
|
||||
from quopri import decodestring as _qdecode
|
||||
import base64
|
||||
|
||||
# Intrapackage imports
|
||||
from Encoders import _bencode, _qencode
|
||||
|
||||
COMMASPACE = ', '
|
||||
UEMPTYSTRING = u''
|
||||
|
||||
|
||||
|
||||
# Helpers
|
||||
|
||||
def _identity(s):
|
||||
return s
|
||||
|
||||
|
||||
def _bdecode(s):
|
||||
if not s:
|
||||
return s
|
||||
# We can't quite use base64.encodestring() since it tacks on a "courtesy
|
||||
# newline". Blech!
|
||||
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
|
||||
|
||||
|
||||
|
||||
def getaddresses(fieldvalues):
|
||||
"""Return a list of (REALNAME, EMAIL) for each fieldvalue."""
|
||||
all = COMMASPACE.join(fieldvalues)
|
||||
a = _AddrlistClass(all)
|
||||
return a.getaddrlist()
|
||||
|
||||
|
||||
|
||||
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<atom>.*?) # non-greedy up to the next ?= is the atom
|
||||
\?= # literal ?=
|
||||
''', re.VERBOSE | re.IGNORECASE)
|
||||
|
||||
|
||||
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]
|
||||
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.lower() == 'q':
|
||||
func = _qdecode
|
||||
elif encoding.lower() == '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.
|
||||
return UEMPTYSTRING.join(rtn)
|
||||
|
||||
|
||||
|
||||
def encode(s, charset='iso-8859-1', encoding='q'):
|
||||
"""Encode a string according to RFC 2047."""
|
||||
if encoding.lower() == 'q':
|
||||
estr = _qencode(s)
|
||||
elif encoding.lower() == 'b':
|
||||
estr = _bencode(s)
|
||||
else:
|
||||
raise ValueError, 'Illegal encoding code: ' + encoding
|
||||
return '=?%s?%s?%s?=' % (charset.lower(), encoding.lower(), estr)
|
|
@ -0,0 +1,34 @@
|
|||
# Copyright (C) 2001 Python Software Foundation
|
||||
# Author: barry@zope.com (Barry Warsaw)
|
||||
|
||||
"""A package for parsing, handling, and generating email messages.
|
||||
"""
|
||||
|
||||
__version__ = '1.0'
|
||||
|
||||
__all__ = ['Encoders',
|
||||
'Errors',
|
||||
'Generator',
|
||||
'Image',
|
||||
'Iterators',
|
||||
'MIMEBase',
|
||||
'Message',
|
||||
'MessageRFC822',
|
||||
'Parser',
|
||||
'Text',
|
||||
'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)
|
Loading…
Reference in New Issue