mirror of
https://github.com/python/cpython.git
synced 2024-12-01 11:15:56 +01:00
e8a0a5c92a
message may consist of several MIME parts in which case each part is converted independent of the others. Only converts when necessary.
416 lines
11 KiB
Python
Executable File
416 lines
11 KiB
Python
Executable File
#!/usr/local/bin/python
|
|
|
|
'''Mimification and unmimification of mail messages.
|
|
|
|
decode quoted-printable parts of a mail message or encode using
|
|
quoted-printable.
|
|
|
|
Usage:
|
|
mimify(input, output)
|
|
unmimify(input, output)
|
|
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
|
|
same.
|
|
|
|
Interactive usage:
|
|
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.
|
|
'''
|
|
|
|
# Configure
|
|
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
|
|
# End configure
|
|
|
|
import regex, regsub, string
|
|
|
|
qp = regex.compile('^content-transfer-encoding:[\000-\377]*quoted-printable',
|
|
regex.casefold)
|
|
mp = regex.compile('^content-type:[\000-\377]*multipart/[\000-\377]*boundary="?\\([^;"\n]*\\)',
|
|
regex.casefold)
|
|
chrset = regex.compile('^\\(content-type:.*charset="\\)\\(us-ascii\\|iso-8859-[0-9]+\\)\\("[\000-\377]*\\)',
|
|
regex.casefold)
|
|
he = regex.compile('^-*$')
|
|
mime_code = regex.compile('=\\([0-9a-f][0-9a-f]\\)', regex.casefold)
|
|
mime_head = regex.compile('=\\?iso-8859-1\\?q\\?\\([^?]+\\)\\?=',
|
|
regex.casefold)
|
|
repl = regex.compile('^subject:[ \t]+re: ', regex.casefold)
|
|
|
|
class File:
|
|
'''A simple fake file object that knows about limited
|
|
read-ahead and boundaries.
|
|
The only supported method is readline().'''
|
|
|
|
def __init__(self, file, boundary):
|
|
self.file = file
|
|
self.boundary = boundary
|
|
self.peek = None
|
|
|
|
def readline(self):
|
|
if self.peek is not None:
|
|
return ''
|
|
line = self.file.readline()
|
|
if not line:
|
|
return line
|
|
if self.boundary:
|
|
if line == self.boundary + '\n':
|
|
self.peek = line
|
|
return ''
|
|
if line == self.boundary + '--\n':
|
|
self.peek = line
|
|
return ''
|
|
return line
|
|
|
|
class HeaderFile:
|
|
def __init__(self, file):
|
|
self.file = file
|
|
self.peek = None
|
|
|
|
def readline(self):
|
|
if self.peek is not None:
|
|
line = self.peek
|
|
self.peek = None
|
|
else:
|
|
line = self.file.readline()
|
|
if not line:
|
|
return line
|
|
if he.match(line) >= 0:
|
|
return line
|
|
while 1:
|
|
self.peek = self.file.readline()
|
|
if len(self.peek) == 0 or \
|
|
(self.peek[0] != ' ' and self.peek[0] != '\t'):
|
|
return line
|
|
line = line + self.peek
|
|
self.peek = None
|
|
|
|
def mime_decode(line):
|
|
'''Decode a single line of quoted-printable text to 8bit.'''
|
|
newline = ''
|
|
while 1:
|
|
i = mime_code.search(line)
|
|
if i < 0:
|
|
break
|
|
newline = newline + line[:i] + \
|
|
chr(string.atoi(mime_code.group(1), 16))
|
|
line = line[i+3:]
|
|
return newline + line
|
|
|
|
def mime_decode_header(line):
|
|
'''Decode a header line to 8bit.'''
|
|
newline = ''
|
|
while 1:
|
|
i = mime_head.search(line)
|
|
if i < 0:
|
|
break
|
|
match = mime_head.group(0, 1)
|
|
newline = newline + line[:i] + mime_decode(match[1])
|
|
line = line[i + len(match[0]):]
|
|
return newline + line
|
|
|
|
def unmimify_part(ifile, ofile):
|
|
'''Convert a quoted-printable part of a MIME mail message to 8bit.'''
|
|
multipart = None
|
|
quoted_printable = 0
|
|
is_repl = 0
|
|
if ifile.boundary and ifile.boundary[:2] == QUOTE:
|
|
prefix = QUOTE
|
|
else:
|
|
prefix = ''
|
|
|
|
# read header
|
|
hfile = HeaderFile(ifile)
|
|
while 1:
|
|
line = hfile.readline()
|
|
if not line:
|
|
return
|
|
if prefix and line[:len(prefix)] == prefix:
|
|
line = line[len(prefix):]
|
|
pref = prefix
|
|
else:
|
|
pref = ''
|
|
line = mime_decode_header(line)
|
|
if qp.match(line) >= 0:
|
|
quoted_printable = 1
|
|
continue # skip this header
|
|
ofile.write(pref + line)
|
|
if not prefix and repl.match(line) >= 0:
|
|
# we're dealing with a reply message
|
|
is_repl = 1
|
|
if mp.match(line) >= 0:
|
|
multipart = '--' + mp.group(1)
|
|
if he.match(line) >= 0:
|
|
break
|
|
if is_repl and (quoted_printable or multipart):
|
|
is_repl = 0
|
|
|
|
# read body
|
|
while 1:
|
|
line = ifile.readline()
|
|
if not line:
|
|
return
|
|
line = regsub.gsub(mime_head, '\\1', line)
|
|
if prefix and line[:len(prefix)] == prefix:
|
|
line = line[len(prefix):]
|
|
pref = prefix
|
|
else:
|
|
pref = ''
|
|
## if is_repl and len(line) >= 4 and line[:4] == QUOTE+'--' and line[-3:] != '--\n':
|
|
## multipart = line[:-1]
|
|
while multipart:
|
|
if line == multipart + '--\n':
|
|
ofile.write(pref + line)
|
|
multipart = None
|
|
line = None
|
|
break
|
|
if line == multipart + '\n':
|
|
ofile.write(pref + line)
|
|
nifile = File(ifile, multipart)
|
|
unmimify_part(nifile, ofile)
|
|
line = nifile.peek
|
|
continue
|
|
# not a boundary between parts
|
|
break
|
|
if line and quoted_printable:
|
|
while line[-2:] == '=\n':
|
|
line = line[:-2]
|
|
newline = ifile.readline()
|
|
if newline[:len(QUOTE)] == QUOTE:
|
|
newline = newline[len(QUOTE):]
|
|
line = line + newline
|
|
line = mime_decode(line)
|
|
if line:
|
|
ofile.write(pref + line)
|
|
|
|
def unmimify(infile, outfile):
|
|
'''Convert quoted-printable parts of a MIME mail message to 8bit.'''
|
|
if type(infile) == type(''):
|
|
ifile = open(infile)
|
|
if type(outfile) == type('') and infile == outfile:
|
|
import os
|
|
d, f = os.path.split(infile)
|
|
os.rename(infile, os.path.join(d, ',' + f))
|
|
else:
|
|
ifile = infile
|
|
if type(outfile) == type(''):
|
|
ofile = open(outfile, 'w')
|
|
else:
|
|
ofile = outfile
|
|
nifile = File(ifile, None)
|
|
unmimify_part(nifile, ofile)
|
|
ofile.flush()
|
|
|
|
mime_char = regex.compile('[=\240-\377]') # quote these chars in body
|
|
mime_header_char = regex.compile('[=?\240-\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 header:
|
|
reg = mime_header_char
|
|
else:
|
|
reg = mime_char
|
|
newline = ''
|
|
if len(line) >= 5 and line[:5] == 'From ':
|
|
# quote 'From ' at the start of a line for stupid mailers
|
|
newline = string.upper('=%02x' % ord('F'))
|
|
line = line[1:]
|
|
while 1:
|
|
i = reg.search(line)
|
|
if i < 0:
|
|
break
|
|
newline = newline + line[:i] + \
|
|
string.upper('=%02x' % ord(line[i]))
|
|
line = line[i+1:]
|
|
line = newline + line
|
|
|
|
newline = ''
|
|
while len(line) >= 75:
|
|
i = 73
|
|
while line[i] == '=' or line[i-1] == '=':
|
|
i = i - 1
|
|
i = i + 1
|
|
newline = newline + line[:i] + '=\n'
|
|
line = line[i:]
|
|
return newline + line
|
|
|
|
mime_header = regex.compile('\\([ \t(]\\)\\([-a-zA-Z0-9_+]*[\240-\377][-a-zA-Z0-9_+\240-\377]*\\)\\([ \t)]\\|$\\)')
|
|
|
|
def mime_encode_header(line):
|
|
'''Code a single header line as quoted-printable.'''
|
|
newline = ''
|
|
while 1:
|
|
i = mime_header.search(line)
|
|
if i < 0:
|
|
break
|
|
newline = newline + line[:i] + mime_header.group(1) + \
|
|
'=?' + CHARSET + '?Q?' + \
|
|
mime_encode(mime_header.group(2), 1) + \
|
|
'?=' + mime_header.group(3)
|
|
line = line[i+len(mime_header.group(0)):]
|
|
return newline + line
|
|
|
|
mv = regex.compile('^mime-version:', regex.casefold)
|
|
cte = regex.compile('^content-transfer-encoding:', regex.casefold)
|
|
iso_char = regex.compile('[\240-\377]')
|
|
|
|
def mimify_part(ifile, ofile, is_mime):
|
|
'''Convert an 8bit part of a MIME mail message to quoted-printable.'''
|
|
has_cte = is_qp = 0
|
|
multipart = None
|
|
must_quote_body = must_quote_header = has_iso_chars = 0
|
|
|
|
header = []
|
|
header_end = ''
|
|
message = []
|
|
message_end = ''
|
|
# read header
|
|
hfile = HeaderFile(ifile)
|
|
while 1:
|
|
line = hfile.readline()
|
|
if not line:
|
|
break
|
|
if not must_quote_header and iso_char.search(line) >= 0:
|
|
must_quote_header = 1
|
|
if mv.match(line) >= 0:
|
|
is_mime = 1
|
|
if cte.match(line) >= 0:
|
|
has_cte = 1
|
|
if qp.match(line) >= 0:
|
|
is_qp = 1
|
|
if mp.match(line) >= 0:
|
|
multipart = '--' + mp.group(1)
|
|
if he.match(line) >= 0:
|
|
header_end = line
|
|
break
|
|
header.append(line)
|
|
|
|
# read body
|
|
while 1:
|
|
line = ifile.readline()
|
|
if not line:
|
|
break
|
|
if multipart:
|
|
if line == multipart + '--\n':
|
|
message_end = line
|
|
break
|
|
if line == multipart + '\n':
|
|
message_end = line
|
|
break
|
|
if is_qp:
|
|
while line[-2:] == '=\n':
|
|
line = line[:-2]
|
|
newline = ifile.readline()
|
|
if newline[:len(QUOTE)] == QUOTE:
|
|
newline = newline[len(QUOTE):]
|
|
line = line + newline
|
|
line = mime_decode(line)
|
|
message.append(line)
|
|
if not has_iso_chars:
|
|
if iso_char.search(line) >= 0:
|
|
has_iso_chars = must_quote_body = 1
|
|
if not must_quote_body:
|
|
if len(line) > MAXLEN:
|
|
must_quote_body = 1
|
|
|
|
# convert and output header and body
|
|
for line in header:
|
|
if must_quote_header:
|
|
line = mime_encode_header(line)
|
|
if chrset.match(line) >= 0:
|
|
if has_iso_chars:
|
|
# change us-ascii into iso-8859-1
|
|
if string.lower(chrset.group(2)) == 'us-ascii':
|
|
line = chrset.group(1) + \
|
|
CHARSET + chrset.group(3)
|
|
else:
|
|
# change iso-8859-* into us-ascii
|
|
line = chrset.group(1) + 'us-ascii' + chrset.group(3)
|
|
if has_cte and cte.match(line) >= 0:
|
|
line = 'Content-Transfer-Encoding: '
|
|
if must_quote_body:
|
|
line = line + 'quoted-printable\n'
|
|
else:
|
|
line = line + '7bit\n'
|
|
ofile.write(line)
|
|
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; ')
|
|
if has_iso_chars:
|
|
ofile.write('charset="%s"\n' % CHARSET)
|
|
else:
|
|
ofile.write('charset="us-ascii"\n')
|
|
if must_quote_body and not has_cte:
|
|
ofile.write('Content-Transfer-Encoding: quoted-printable\n')
|
|
ofile.write(header_end)
|
|
|
|
for line in message:
|
|
if must_quote_body:
|
|
line = mime_encode(line, 0)
|
|
ofile.write(line)
|
|
ofile.write(message_end)
|
|
|
|
line = message_end
|
|
while multipart:
|
|
if line == multipart + '--\n':
|
|
return
|
|
if line == multipart + '\n':
|
|
nifile = File(ifile, multipart)
|
|
mimify_part(nifile, ofile, 1)
|
|
line = nifile.peek
|
|
ofile.write(line)
|
|
continue
|
|
|
|
def mimify(infile, outfile):
|
|
'''Convert 8bit parts of a MIME mail message to quoted-printable.'''
|
|
if type(infile) == type(''):
|
|
ifile = open(infile)
|
|
if type(outfile) == type('') and infile == outfile:
|
|
import os
|
|
d, f = os.path.split(infile)
|
|
os.rename(infile, os.path.join(d, ',' + f))
|
|
else:
|
|
ifile = infile
|
|
if type(outfile) == type(''):
|
|
ofile = open(outfile, 'w')
|
|
else:
|
|
ofile = outfile
|
|
nifile = File(ifile, None)
|
|
mimify_part(nifile, ofile, 0)
|
|
ofile.flush()
|
|
|
|
import sys
|
|
if __name__ == '__main__' or (len(sys.argv) > 0 and sys.argv[0] == 'mimify'):
|
|
import getopt
|
|
usage = 'Usage: mimify [-l len] -[ed] [infile [outfile]]'
|
|
|
|
opts, args = getopt.getopt(sys.argv[1:], 'l:ed')
|
|
if len(args) not in (0, 1, 2):
|
|
print usage
|
|
sys.exit(1)
|
|
if (('-e', '') in opts) == (('-d', '') in opts):
|
|
print usage
|
|
sys.exit(1)
|
|
for o, a in opts:
|
|
if o == '-e':
|
|
encode = mimify
|
|
elif o == '-d':
|
|
encode = unmimify
|
|
elif o == '-l':
|
|
try:
|
|
MAXLEN = string.atoi(a)
|
|
except:
|
|
print usage
|
|
sys.exit(1)
|
|
if len(args) == 0:
|
|
encode(sys.stdin, sys.stdout)
|
|
elif len(args) == 1:
|
|
encode(args[0], sys.stdout)
|
|
else:
|
|
encode(args[0], args[1])
|