3 """Mimification and unmimification of mail messages.
5 Decode quoted-printable parts of a mail message or encode using
10 unmimify(input, output, decode_base64 = 0)
11 to encode and decode respectively. Input and output may be the name
12 of a file or an open file object. Only a readline() method is used
13 on the input file, only a write() method is used on the output file.
14 When using file names, the input and output file names may be the
18 mimify.py -e [infile [outfile]]
19 mimify.py -d [infile [outfile]]
20 to encode and decode respectively. Infile defaults to standard
21 input and outfile to standard output.
25 MAXLEN
= 200 # if lines longer than this, encode as quoted-printable
26 CHARSET
= 'ISO-8859-1' # default charset for non-US-ASCII mail
27 QUOTE
= '> ' # string replies are quoted with
32 __all__
= ["mimify","unmimify","mime_encode_header","mime_decode_header"]
34 qp
= re
.compile('^content-transfer-encoding:\\s*quoted-printable', re
.I
)
35 base64_re
= re
.compile('^content-transfer-encoding:\\s*base64', re
.I
)
36 mp
= re
.compile('^content-type:.*multipart/.*boundary="?([^;"\n]*)', re
.I|re
.S
)
37 chrset
= re
.compile('^(content-type:.*charset=")(us-ascii|iso-8859-[0-9]+)(".*)', re
.I|re
.S
)
38 he
= re
.compile('^-*\n')
39 mime_code
= re
.compile('=([0-9a-f][0-9a-f])', re
.I
)
40 mime_head
= re
.compile('=\\?iso-8859-1\\?q\\?([^? \t\n]+)\\?=', re
.I
)
41 repl
= re
.compile('^subject:\\s+re: ', re
.I
)
44 """A simple fake file object that knows about limited read-ahead and
45 boundaries. The only supported method is readline()."""
47 def __init__(self
, file, boundary
):
49 self
.boundary
= boundary
53 if self
.peek
is not None:
55 line
= self
.file.readline()
59 if line
== self
.boundary
+ '\n':
62 if line
== self
.boundary
+ '--\n':
68 def __init__(self
, file):
73 if self
.peek
is not None:
77 line
= self
.file.readline()
83 self
.peek
= self
.file.readline()
84 if len(self
.peek
) == 0 or \
85 (self
.peek
[0] != ' ' and self
.peek
[0] != '\t'):
87 line
= line
+ self
.peek
90 def mime_decode(line
):
91 """Decode a single line of quoted-printable text to 8bit."""
95 res
= mime_code
.search(line
, pos
)
98 newline
= newline
+ line
[pos
:res
.start(0)] + \
99 chr(int(res
.group(1), 16))
101 return newline
+ line
[pos
:]
103 def mime_decode_header(line
):
104 """Decode a header line to 8bit."""
108 res
= mime_head
.search(line
, pos
)
112 # convert underscores to spaces (before =XX conversion!)
113 match
= ' '.join(match
.split('_'))
114 newline
= newline
+ line
[pos
:res
.start(0)] + mime_decode(match
)
116 return newline
+ line
[pos
:]
118 def unmimify_part(ifile
, ofile
, decode_base64
= 0):
119 """Convert a quoted-printable part of a MIME mail message to 8bit."""
124 if ifile
.boundary
and ifile
.boundary
[:2] == QUOTE
:
130 hfile
= HeaderFile(ifile
)
132 line
= hfile
.readline()
135 if prefix
and line
[:len(prefix
)] == prefix
:
136 line
= line
[len(prefix
):]
140 line
= mime_decode_header(line
)
143 continue # skip this header
144 if decode_base64
and base64_re
.match(line
):
147 ofile
.write(pref
+ line
)
148 if not prefix
and repl
.match(line
):
149 # we're dealing with a reply message
151 mp_res
= mp
.match(line
)
153 multipart
= '--' + mp_res
.group(1)
156 if is_repl
and (quoted_printable
or multipart
):
161 line
= ifile
.readline()
164 line
= re
.sub(mime_head
, '\\1', line
)
165 if prefix
and line
[:len(prefix
)] == prefix
:
166 line
= line
[len(prefix
):]
170 ## if is_repl and len(line) >= 4 and line[:4] == QUOTE+'--' and line[-3:] != '--\n':
171 ## multipart = line[:-1]
173 if line
== multipart
+ '--\n':
174 ofile
.write(pref
+ line
)
178 if line
== multipart
+ '\n':
179 ofile
.write(pref
+ line
)
180 nifile
= File(ifile
, multipart
)
181 unmimify_part(nifile
, ofile
, decode_base64
)
184 # premature end of file
187 # not a boundary between parts
189 if line
and quoted_printable
:
190 while line
[-2:] == '=\n':
192 newline
= ifile
.readline()
193 if newline
[:len(QUOTE
)] == QUOTE
:
194 newline
= newline
[len(QUOTE
):]
195 line
= line
+ newline
196 line
= mime_decode(line
)
197 if line
and is_base64
and not pref
:
199 line
= base64
.decodestring(line
)
201 ofile
.write(pref
+ line
)
203 def unmimify(infile
, outfile
, decode_base64
= 0):
204 """Convert quoted-printable parts of a MIME mail message to 8bit."""
205 if type(infile
) == type(''):
207 if type(outfile
) == type('') and infile
== outfile
:
209 d
, f
= os
.path
.split(infile
)
210 os
.rename(infile
, os
.path
.join(d
, ',' + f
))
213 if type(outfile
) == type(''):
214 ofile
= open(outfile
, 'w')
217 nifile
= File(ifile
, None)
218 unmimify_part(nifile
, ofile
, decode_base64
)
221 mime_char
= re
.compile('[=\177-\377]') # quote these chars in body
222 mime_header_char
= re
.compile('[=?\177-\377]') # quote these in header
224 def mime_encode(line
, header
):
225 """Code a single line as quoted-printable.
226 If header is set, quote some extra characters."""
228 reg
= mime_header_char
233 if len(line
) >= 5 and line
[:5] == 'From ':
234 # quote 'From ' at the start of a line for stupid mailers
235 newline
= ('=%02x' % ord('F')).upper()
238 res
= reg
.search(line
, pos
)
241 newline
= newline
+ line
[pos
:res
.start(0)] + \
242 ('=%02x' % ord(res
.group(0))).upper()
244 line
= newline
+ line
[pos
:]
247 while len(line
) >= 75:
249 while line
[i
] == '=' or line
[i
-1] == '=':
252 newline
= newline
+ line
[:i
] + '=\n'
254 return newline
+ line
256 mime_header
= re
.compile('([ \t(]|^)([-a-zA-Z0-9_+]*[\177-\377][-a-zA-Z0-9_+\177-\377]*)(?=[ \t)]|\n)')
258 def mime_encode_header(line
):
259 """Code a single header line as quoted-printable."""
263 res
= mime_header
.search(line
, pos
)
266 newline
= '%s%s%s=?%s?Q?%s?=' % \
267 (newline
, line
[pos
:res
.start(0)], res
.group(1),
268 CHARSET
, mime_encode(res
.group(2), 1))
270 return newline
+ line
[pos
:]
272 mv
= re
.compile('^mime-version:', re
.I
)
273 cte
= re
.compile('^content-transfer-encoding:', re
.I
)
274 iso_char
= re
.compile('[\177-\377]')
276 def mimify_part(ifile
, ofile
, is_mime
):
277 """Convert an 8bit part of a MIME mail message to quoted-printable."""
278 has_cte
= is_qp
= is_base64
= 0
280 must_quote_body
= must_quote_header
= has_iso_chars
= 0
287 hfile
= HeaderFile(ifile
)
289 line
= hfile
.readline()
292 if not must_quote_header
and iso_char
.search(line
):
293 must_quote_header
= 1
300 elif base64_re
.match(line
):
302 mp_res
= mp
.match(line
)
304 multipart
= '--' + mp_res
.group(1)
312 line
= ifile
.readline()
316 if line
== multipart
+ '--\n':
319 if line
== multipart
+ '\n':
326 while line
[-2:] == '=\n':
328 newline
= ifile
.readline()
329 if newline
[:len(QUOTE
)] == QUOTE
:
330 newline
= newline
[len(QUOTE
):]
331 line
= line
+ newline
332 line
= mime_decode(line
)
334 if not has_iso_chars
:
335 if iso_char
.search(line
):
336 has_iso_chars
= must_quote_body
= 1
337 if not must_quote_body
:
338 if len(line
) > MAXLEN
:
341 # convert and output header and body
343 if must_quote_header
:
344 line
= mime_encode_header(line
)
345 chrset_res
= chrset
.match(line
)
348 # change us-ascii into iso-8859-1
349 if chrset_res
.group(2).lower() == 'us-ascii':
350 line
= '%s%s%s' % (chrset_res
.group(1),
354 # change iso-8859-* into us-ascii
355 line
= '%sus-ascii%s' % chrset_res
.group(1, 3)
356 if has_cte
and cte
.match(line
):
357 line
= 'Content-Transfer-Encoding: '
359 line
= line
+ 'base64\n'
360 elif must_quote_body
:
361 line
= line
+ 'quoted-printable\n'
363 line
= line
+ '7bit\n'
365 if (must_quote_header
or must_quote_body
) and not is_mime
:
366 ofile
.write('Mime-Version: 1.0\n')
367 ofile
.write('Content-Type: text/plain; ')
369 ofile
.write('charset="%s"\n' % CHARSET
)
371 ofile
.write('charset="us-ascii"\n')
372 if must_quote_body
and not has_cte
:
373 ofile
.write('Content-Transfer-Encoding: quoted-printable\n')
374 ofile
.write(header_end
)
378 line
= mime_encode(line
, 0)
380 ofile
.write(message_end
)
384 if line
== multipart
+ '--\n':
385 # read bit after the end of the last part
387 line
= ifile
.readline()
391 line
= mime_encode(line
, 0)
393 if line
== multipart
+ '\n':
394 nifile
= File(ifile
, multipart
)
395 mimify_part(nifile
, ofile
, 1)
398 # premature end of file
402 # unexpectedly no multipart separator--copy rest of file
404 line
= ifile
.readline()
408 line
= mime_encode(line
, 0)
411 def mimify(infile
, outfile
):
412 """Convert 8bit parts of a MIME mail message to quoted-printable."""
413 if type(infile
) == type(''):
415 if type(outfile
) == type('') and infile
== outfile
:
417 d
, f
= os
.path
.split(infile
)
418 os
.rename(infile
, os
.path
.join(d
, ',' + f
))
421 if type(outfile
) == type(''):
422 ofile
= open(outfile
, 'w')
425 nifile
= File(ifile
, None)
426 mimify_part(nifile
, ofile
, 0)
430 if __name__
== '__main__' or (len(sys
.argv
) > 0 and sys
.argv
[0] == 'mimify'):
432 usage
= 'Usage: mimify [-l len] -[ed] [infile [outfile]]'
435 opts
, args
= getopt
.getopt(sys
.argv
[1:], 'l:edb')
436 if len(args
) not in (0, 1, 2):
439 if (('-e', '') in opts
) == (('-d', '') in opts
) or \
440 ((('-b', '') in opts
) and (('-d', '') not in opts
)):
451 except (ValueError, OverflowError):
457 encode_args
= (sys
.stdin
, sys
.stdout
)
459 encode_args
= (args
[0], sys
.stdout
)
461 encode_args
= (args
[0], args
[1])
463 encode_args
= encode_args
+ (decode_base64
,)
464 apply(encode
, encode_args
)