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 qp
= re
.compile('^content-transfer-encoding:\\s*quoted-printable', re
.I
)
33 base64_re
= re
.compile('^content-transfer-encoding:\\s*base64', re
.I
)
34 mp
= re
.compile('^content-type:.*multipart/.*boundary="?([^;"\n]*)', re
.I|re
.S
)
35 chrset
= re
.compile('^(content-type:.*charset=")(us-ascii|iso-8859-[0-9]+)(".*)', re
.I|re
.S
)
36 he
= re
.compile('^-*\n')
37 mime_code
= re
.compile('=([0-9a-f][0-9a-f])', re
.I
)
38 mime_head
= re
.compile('=\\?iso-8859-1\\?q\\?([^? \t\n]+)\\?=', re
.I
)
39 repl
= re
.compile('^subject:\\s+re: ', re
.I
)
42 """A simple fake file object that knows about limited read-ahead and
43 boundaries. The only supported method is readline()."""
45 def __init__(self
, file, boundary
):
47 self
.boundary
= boundary
51 if self
.peek
is not None:
53 line
= self
.file.readline()
57 if line
== self
.boundary
+ '\n':
60 if line
== self
.boundary
+ '--\n':
66 def __init__(self
, file):
71 if self
.peek
is not None:
75 line
= self
.file.readline()
81 self
.peek
= self
.file.readline()
82 if len(self
.peek
) == 0 or \
83 (self
.peek
[0] != ' ' and self
.peek
[0] != '\t'):
85 line
= line
+ self
.peek
88 def mime_decode(line
):
89 """Decode a single line of quoted-printable text to 8bit."""
93 res
= mime_code
.search(line
, pos
)
96 newline
= newline
+ line
[pos
:res
.start(0)] + \
97 chr(string
.atoi(res
.group(1), 16))
99 return newline
+ line
[pos
:]
101 def mime_decode_header(line
):
102 """Decode a header line to 8bit."""
106 res
= mime_head
.search(line
, pos
)
110 # convert underscores to spaces (before =XX conversion!)
111 match
= string
.join(string
.split(match
, '_'), ' ')
112 newline
= newline
+ line
[pos
:res
.start(0)] + mime_decode(match
)
114 return newline
+ line
[pos
:]
116 def unmimify_part(ifile
, ofile
, decode_base64
= 0):
117 """Convert a quoted-printable part of a MIME mail message to 8bit."""
122 if ifile
.boundary
and ifile
.boundary
[:2] == QUOTE
:
128 hfile
= HeaderFile(ifile
)
130 line
= hfile
.readline()
133 if prefix
and line
[:len(prefix
)] == prefix
:
134 line
= line
[len(prefix
):]
138 line
= mime_decode_header(line
)
141 continue # skip this header
142 if decode_base64
and base64_re
.match(line
):
145 ofile
.write(pref
+ line
)
146 if not prefix
and repl
.match(line
):
147 # we're dealing with a reply message
149 mp_res
= mp
.match(line
)
151 multipart
= '--' + mp_res
.group(1)
154 if is_repl
and (quoted_printable
or multipart
):
159 line
= ifile
.readline()
162 line
= re
.sub(mime_head
, '\\1', line
)
163 if prefix
and line
[:len(prefix
)] == prefix
:
164 line
= line
[len(prefix
):]
168 ## if is_repl and len(line) >= 4 and line[:4] == QUOTE+'--' and line[-3:] != '--\n':
169 ## multipart = line[:-1]
171 if line
== multipart
+ '--\n':
172 ofile
.write(pref
+ line
)
176 if line
== multipart
+ '\n':
177 ofile
.write(pref
+ line
)
178 nifile
= File(ifile
, multipart
)
179 unmimify_part(nifile
, ofile
, decode_base64
)
182 # premature end of file
185 # not a boundary between parts
187 if line
and quoted_printable
:
188 while line
[-2:] == '=\n':
190 newline
= ifile
.readline()
191 if newline
[:len(QUOTE
)] == QUOTE
:
192 newline
= newline
[len(QUOTE
):]
193 line
= line
+ newline
194 line
= mime_decode(line
)
195 if line
and is_base64
and not pref
:
197 line
= base64
.decodestring(line
)
199 ofile
.write(pref
+ line
)
201 def unmimify(infile
, outfile
, decode_base64
= 0):
202 """Convert quoted-printable parts of a MIME mail message to 8bit."""
203 if type(infile
) == type(''):
205 if type(outfile
) == type('') and infile
== outfile
:
207 d
, f
= os
.path
.split(infile
)
208 os
.rename(infile
, os
.path
.join(d
, ',' + f
))
211 if type(outfile
) == type(''):
212 ofile
= open(outfile
, 'w')
215 nifile
= File(ifile
, None)
216 unmimify_part(nifile
, ofile
, decode_base64
)
219 mime_char
= re
.compile('[=\177-\377]') # quote these chars in body
220 mime_header_char
= re
.compile('[=?\177-\377]') # quote these in header
222 def mime_encode(line
, header
):
223 """Code a single line as quoted-printable.
224 If header is set, quote some extra characters."""
226 reg
= mime_header_char
231 if len(line
) >= 5 and line
[:5] == 'From ':
232 # quote 'From ' at the start of a line for stupid mailers
233 newline
= string
.upper('=%02x' % ord('F'))
236 res
= reg
.search(line
, pos
)
239 newline
= newline
+ line
[pos
:res
.start(0)] + \
240 string
.upper('=%02x' % ord(res
.group(0)))
242 line
= newline
+ line
[pos
:]
245 while len(line
) >= 75:
247 while line
[i
] == '=' or line
[i
-1] == '=':
250 newline
= newline
+ line
[:i
] + '=\n'
252 return newline
+ line
254 mime_header
= re
.compile('([ \t(]|^)([-a-zA-Z0-9_+]*[\177-\377][-a-zA-Z0-9_+\177-\377]*)([ \t)]|\n)')
256 def mime_encode_header(line
):
257 """Code a single header line as quoted-printable."""
261 res
= mime_header
.search(line
, pos
)
264 newline
= '%s%s%s=?%s?Q?%s?=%s' % \
265 (newline
, line
[pos
:res
.start(0)], res
.group(1),
266 CHARSET
, mime_encode(res
.group(2), 1), res
.group(3))
268 return newline
+ line
[pos
:]
270 mv
= re
.compile('^mime-version:', re
.I
)
271 cte
= re
.compile('^content-transfer-encoding:', re
.I
)
272 iso_char
= re
.compile('[\177-\377]')
274 def mimify_part(ifile
, ofile
, is_mime
):
275 """Convert an 8bit part of a MIME mail message to quoted-printable."""
276 has_cte
= is_qp
= is_base64
= 0
278 must_quote_body
= must_quote_header
= has_iso_chars
= 0
285 hfile
= HeaderFile(ifile
)
287 line
= hfile
.readline()
290 if not must_quote_header
and iso_char
.search(line
):
291 must_quote_header
= 1
298 elif base64_re
.match(line
):
300 mp_res
= mp
.match(line
)
302 multipart
= '--' + mp_res
.group(1)
310 line
= ifile
.readline()
314 if line
== multipart
+ '--\n':
317 if line
== multipart
+ '\n':
324 while line
[-2:] == '=\n':
326 newline
= ifile
.readline()
327 if newline
[:len(QUOTE
)] == QUOTE
:
328 newline
= newline
[len(QUOTE
):]
329 line
= line
+ newline
330 line
= mime_decode(line
)
332 if not has_iso_chars
:
333 if iso_char
.search(line
):
334 has_iso_chars
= must_quote_body
= 1
335 if not must_quote_body
:
336 if len(line
) > MAXLEN
:
339 # convert and output header and body
341 if must_quote_header
:
342 line
= mime_encode_header(line
)
343 chrset_res
= chrset
.match(line
)
346 # change us-ascii into iso-8859-1
347 if string
.lower(chrset_res
.group(2)) == 'us-ascii':
348 line
= '%s%s%s' % (chrset_res
.group(1),
352 # change iso-8859-* into us-ascii
353 line
= '%sus-ascii%s' % chrset_res
.group(1, 3)
354 if has_cte
and cte
.match(line
):
355 line
= 'Content-Transfer-Encoding: '
357 line
= line
+ 'base64\n'
358 elif must_quote_body
:
359 line
= line
+ 'quoted-printable\n'
361 line
= line
+ '7bit\n'
363 if (must_quote_header
or must_quote_body
) and not is_mime
:
364 ofile
.write('Mime-Version: 1.0\n')
365 ofile
.write('Content-Type: text/plain; ')
367 ofile
.write('charset="%s"\n' % CHARSET
)
369 ofile
.write('charset="us-ascii"\n')
370 if must_quote_body
and not has_cte
:
371 ofile
.write('Content-Transfer-Encoding: quoted-printable\n')
372 ofile
.write(header_end
)
376 line
= mime_encode(line
, 0)
378 ofile
.write(message_end
)
382 if line
== multipart
+ '--\n':
383 # read bit after the end of the last part
385 line
= ifile
.readline()
389 line
= mime_encode(line
, 0)
391 if line
== multipart
+ '\n':
392 nifile
= File(ifile
, multipart
)
393 mimify_part(nifile
, ofile
, 1)
396 # premature end of file
400 # unexpectedly no multipart separator--copy rest of file
402 line
= ifile
.readline()
406 line
= mime_encode(line
, 0)
409 def mimify(infile
, outfile
):
410 """Convert 8bit parts of a MIME mail message to quoted-printable."""
411 if type(infile
) == type(''):
413 if type(outfile
) == type('') and infile
== outfile
:
415 d
, f
= os
.path
.split(infile
)
416 os
.rename(infile
, os
.path
.join(d
, ',' + f
))
419 if type(outfile
) == type(''):
420 ofile
= open(outfile
, 'w')
423 nifile
= File(ifile
, None)
424 mimify_part(nifile
, ofile
, 0)
428 if __name__
== '__main__' or (len(sys
.argv
) > 0 and sys
.argv
[0] == 'mimify'):
430 usage
= 'Usage: mimify [-l len] -[ed] [infile [outfile]]'
433 opts
, args
= getopt
.getopt(sys
.argv
[1:], 'l:edb')
434 if len(args
) not in (0, 1, 2):
437 if (('-e', '') in opts
) == (('-d', '') in opts
) or \
438 ((('-b', '') in opts
) and (('-d', '') not in opts
)):
448 MAXLEN
= string
.atoi(a
)
455 encode_args
= (sys
.stdin
, sys
.stdout
)
457 encode_args
= (args
[0], sys
.stdout
)
459 encode_args
= (args
[0], args
[1])
461 encode_args
= encode_args
+ (decode_base64
,)
462 apply(encode
, encode_args
)