"""Mimification and unmimification of mail messages.
Decode quoted-printable parts of a mail message or encode using
unmimify(input, output, decode_base64 = 0)
to encode and decode respectively. Input and output may be the name
of a file or an open file object. Only a readline() method is used
on the input file, only a write() method is used on the output file.
When using file names, the input and output file names may be the
mimify.py -e [infile [outfile]]
mimify.py -d [infile [outfile]]
to encode and decode respectively. Infile defaults to standard
input and outfile to standard output.
MAXLEN
= 200 # if lines longer than this, encode as quoted-printable
CHARSET
= 'ISO-8859-1' # default charset for non-US-ASCII mail
QUOTE
= '> ' # string replies are quoted with
__all__
= ["mimify","unmimify","mime_encode_header","mime_decode_header"]
qp
= re
.compile('^content-transfer-encoding:\\s*quoted-printable', re
.I
)
base64_re
= re
.compile('^content-transfer-encoding:\\s*base64', re
.I
)
mp
= re
.compile('^content-type:.*multipart/.*boundary="?([^;"\n]*)', re
.I|re
.S
)
chrset
= re
.compile('^(content-type:.*charset=")(us-ascii|iso-8859-[0-9]+)(".*)', re
.I|re
.S
)
mime_code
= re
.compile('=([0-9a-f][0-9a-f])', re
.I
)
mime_head
= re
.compile('=\\?iso-8859-1\\?q\\?([^? \t\n]+)\\?=', re
.I
)
repl
= re
.compile('^subject:\\s+re: ', re
.I
)
"""A simple fake file object that knows about limited read-ahead and
boundaries. The only supported method is readline()."""
def __init__(self
, file, boundary
):
if self
.peek
is not None:
line
= self
.file.readline()
if line
== self
.boundary
+ '\n':
if line
== self
.boundary
+ '--\n':
def __init__(self
, file):
if self
.peek
is not None:
line
= self
.file.readline()
self
.peek
= self
.file.readline()
if len(self
.peek
) == 0 or \
(self
.peek
[0] != ' ' and self
.peek
[0] != '\t'):
"""Decode a single line of quoted-printable text to 8bit."""
res
= mime_code
.search(line
, pos
)
newline
= newline
+ line
[pos
:res
.start(0)] + \
chr(int(res
.group(1), 16))
return newline
+ line
[pos
:]
def mime_decode_header(line
):
"""Decode a header line to 8bit."""
res
= mime_head
.search(line
, pos
)
# convert underscores to spaces (before =XX conversion!)
match
= ' '.join(match
.split('_'))
newline
= newline
+ line
[pos
:res
.start(0)] + mime_decode(match
)
return newline
+ line
[pos
:]
def unmimify_part(ifile
, ofile
, decode_base64
= 0):
"""Convert a quoted-printable part of a MIME mail message to 8bit."""
if ifile
.boundary
and ifile
.boundary
[:2] == QUOTE
:
hfile
= HeaderFile(ifile
)
if prefix
and line
[:len(prefix
)] == prefix
:
line
= line
[len(prefix
):]
line
= mime_decode_header(line
)
continue # skip this header
if decode_base64
and base64_re
.match(line
):
if not prefix
and repl
.match(line
):
# we're dealing with a reply message
multipart
= '--' + mp_res
.group(1)
if is_repl
and (quoted_printable
or multipart
):
line
= re
.sub(mime_head
, '\\1', line
)
if prefix
and line
[:len(prefix
)] == prefix
:
line
= line
[len(prefix
):]
## if is_repl and len(line) >= 4 and line[:4] == QUOTE+'--' and line[-3:] != '--\n':
if line
== multipart
+ '--\n':
if line
== multipart
+ '\n':
nifile
= File(ifile
, multipart
)
unmimify_part(nifile
, ofile
, decode_base64
)
# not a boundary between parts
if line
and quoted_printable
:
while line
[-2:] == '=\n':
newline
= ifile
.readline()
if newline
[:len(QUOTE
)] == QUOTE
:
newline
= newline
[len(QUOTE
):]
if line
and is_base64
and not pref
:
line
= base64
.decodestring(line
)
def unmimify(infile
, outfile
, decode_base64
= 0):
"""Convert quoted-printable parts of a MIME mail message to 8bit."""
if type(infile
) == type(''):
if type(outfile
) == type('') and infile
== outfile
:
d
, f
= os
.path
.split(infile
)
os
.rename(infile
, os
.path
.join(d
, ',' + f
))
if type(outfile
) == type(''):
ofile
= open(outfile
, 'w')
nifile
= File(ifile
, None)
unmimify_part(nifile
, ofile
, decode_base64
)
mime_char
= re
.compile('[=\177-\377]') # quote these chars in body
mime_header_char
= re
.compile('[=?\177-\377]') # quote these in header
def mime_encode(line
, header
):
"""Code a single line as quoted-printable.
If header is set, quote some extra characters."""
if len(line
) >= 5 and line
[:5] == 'From ':
# quote 'From ' at the start of a line for stupid mailers
newline
= ('=%02x' % ord('F')).upper()
res
= reg
.search(line
, pos
)
newline
= newline
+ line
[pos
:res
.start(0)] + \
('=%02x' % ord(res
.group(0))).upper()
line
= newline
+ line
[pos
:]
while line
[i
] == '=' or line
[i
-1] == '=':
newline
= newline
+ line
[:i
] + '=\n'
mime_header
= re
.compile('([ \t(]|^)([-a-zA-Z0-9_+]*[\177-\377][-a-zA-Z0-9_+\177-\377]*)(?=[ \t)]|\n)')
def mime_encode_header(line
):
"""Code a single header line as quoted-printable."""
res
= mime_header
.search(line
, pos
)
newline
= '%s%s%s=?%s?Q?%s?=' % \
(newline
, line
[pos
:res
.start(0)], res
.group(1),
CHARSET
, mime_encode(res
.group(2), 1))
return newline
+ line
[pos
:]
mv
= re
.compile('^mime-version:', re
.I
)
cte
= re
.compile('^content-transfer-encoding:', re
.I
)
iso_char
= re
.compile('[\177-\377]')
def mimify_part(ifile
, ofile
, is_mime
):
"""Convert an 8bit part of a MIME mail message to quoted-printable."""
has_cte
= is_qp
= is_base64
= 0
must_quote_body
= must_quote_header
= has_iso_chars
= 0
hfile
= HeaderFile(ifile
)
if not must_quote_header
and iso_char
.search(line
):
elif base64_re
.match(line
):
multipart
= '--' + mp_res
.group(1)
if line
== multipart
+ '--\n':
if line
== multipart
+ '\n':
while line
[-2:] == '=\n':
newline
= ifile
.readline()
if newline
[:len(QUOTE
)] == QUOTE
:
newline
= newline
[len(QUOTE
):]
if iso_char
.search(line
):
has_iso_chars
= must_quote_body
= 1
# convert and output header and body
line
= mime_encode_header(line
)
chrset_res
= chrset
.match(line
)
# change us-ascii into iso-8859-1
if chrset_res
.group(2).lower() == 'us-ascii':
line
= '%s%s%s' % (chrset_res
.group(1),
# change iso-8859-* into us-ascii
line
= '%sus-ascii%s' % chrset_res
.group(1, 3)
if has_cte
and cte
.match(line
):
line
= 'Content-Transfer-Encoding: '
line
= line
+ 'quoted-printable\n'
if (must_quote_header
or must_quote_body
) and not is_mime
:
ofile
.write('Mime-Version: 1.0\n')
ofile
.write('Content-Type: text/plain; ')
ofile
.write('charset="%s"\n' % CHARSET
)
ofile
.write('charset="us-ascii"\n')
if must_quote_body
and not has_cte
:
ofile
.write('Content-Transfer-Encoding: quoted-printable\n')
line
= mime_encode(line
, 0)
if line
== multipart
+ '--\n':
# read bit after the end of the last part
line
= mime_encode(line
, 0)
if line
== multipart
+ '\n':
nifile
= File(ifile
, multipart
)
mimify_part(nifile
, ofile
, 1)
# unexpectedly no multipart separator--copy rest of file
line
= mime_encode(line
, 0)
def mimify(infile
, outfile
):
"""Convert 8bit parts of a MIME mail message to quoted-printable."""
if type(infile
) == type(''):
if type(outfile
) == type('') and infile
== outfile
:
d
, f
= os
.path
.split(infile
)
os
.rename(infile
, os
.path
.join(d
, ',' + f
))
if type(outfile
) == type(''):
ofile
= open(outfile
, 'w')
nifile
= File(ifile
, None)
mimify_part(nifile
, ofile
, 0)
if __name__
== '__main__' or (len(sys
.argv
) > 0 and sys
.argv
[0] == 'mimify'):
usage
= 'Usage: mimify [-l len] -[ed] [infile [outfile]]'
opts
, args
= getopt
.getopt(sys
.argv
[1:], 'l:edb')
if len(args
) not in (0, 1, 2):
if (('-e', '') in opts
) == (('-d', '') in opts
) or \
((('-b', '') in opts
) and (('-d', '') not in opts
)):
except (ValueError, OverflowError):
encode_args
= (sys
.stdin
, sys
.stdout
)
encode_args
= (args
[0], sys
.stdout
)
encode_args
= (args
[0], args
[1])
encode_args
= encode_args
+ (decode_base64
,)