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
33 warnings
.warn("the mimify module is deprecated; use the email package instead",
34 DeprecationWarning, 2)
36 __all__
= ["mimify","unmimify","mime_encode_header","mime_decode_header"]
38 qp
= re
.compile('^content-transfer-encoding:\\s*quoted-printable', re
.I
)
39 base64_re
= re
.compile('^content-transfer-encoding:\\s*base64', re
.I
)
40 mp
= re
.compile('^content-type:.*multipart/.*boundary="?([^;"\n]*)', re
.I|re
.S
)
41 chrset
= re
.compile('^(content-type:.*charset=")(us-ascii|iso-8859-[0-9]+)(".*)', re
.I|re
.S
)
42 he
= re
.compile('^-*\n')
43 mime_code
= re
.compile('=([0-9a-f][0-9a-f])', re
.I
)
44 mime_head
= re
.compile('=\\?iso-8859-1\\?q\\?([^? \t\n]+)\\?=', re
.I
)
45 repl
= re
.compile('^subject:\\s+re: ', re
.I
)
48 """A simple fake file object that knows about limited read-ahead and
49 boundaries. The only supported method is readline()."""
51 def __init__(self
, file, boundary
):
53 self
.boundary
= boundary
57 if self
.peek
is not None:
59 line
= self
.file.readline()
63 if line
== self
.boundary
+ '\n':
66 if line
== self
.boundary
+ '--\n':
72 def __init__(self
, file):
77 if self
.peek
is not None:
81 line
= self
.file.readline()
87 self
.peek
= self
.file.readline()
88 if len(self
.peek
) == 0 or \
89 (self
.peek
[0] != ' ' and self
.peek
[0] != '\t'):
91 line
= line
+ self
.peek
94 def mime_decode(line
):
95 """Decode a single line of quoted-printable text to 8bit."""
99 res
= mime_code
.search(line
, pos
)
102 newline
= newline
+ line
[pos
:res
.start(0)] + \
103 chr(int(res
.group(1), 16))
105 return newline
+ line
[pos
:]
107 def mime_decode_header(line
):
108 """Decode a header line to 8bit."""
112 res
= mime_head
.search(line
, pos
)
116 # convert underscores to spaces (before =XX conversion!)
117 match
= ' '.join(match
.split('_'))
118 newline
= newline
+ line
[pos
:res
.start(0)] + mime_decode(match
)
120 return newline
+ line
[pos
:]
122 def unmimify_part(ifile
, ofile
, decode_base64
= 0):
123 """Convert a quoted-printable part of a MIME mail message to 8bit."""
128 if ifile
.boundary
and ifile
.boundary
[:2] == QUOTE
:
134 hfile
= HeaderFile(ifile
)
136 line
= hfile
.readline()
139 if prefix
and line
[:len(prefix
)] == prefix
:
140 line
= line
[len(prefix
):]
144 line
= mime_decode_header(line
)
147 continue # skip this header
148 if decode_base64
and base64_re
.match(line
):
151 ofile
.write(pref
+ line
)
152 if not prefix
and repl
.match(line
):
153 # we're dealing with a reply message
155 mp_res
= mp
.match(line
)
157 multipart
= '--' + mp_res
.group(1)
160 if is_repl
and (quoted_printable
or multipart
):
165 line
= ifile
.readline()
168 line
= re
.sub(mime_head
, '\\1', line
)
169 if prefix
and line
[:len(prefix
)] == prefix
:
170 line
= line
[len(prefix
):]
174 ## if is_repl and len(line) >= 4 and line[:4] == QUOTE+'--' and line[-3:] != '--\n':
175 ## multipart = line[:-1]
177 if line
== multipart
+ '--\n':
178 ofile
.write(pref
+ line
)
182 if line
== multipart
+ '\n':
183 ofile
.write(pref
+ line
)
184 nifile
= File(ifile
, multipart
)
185 unmimify_part(nifile
, ofile
, decode_base64
)
188 # premature end of file
191 # not a boundary between parts
193 if line
and quoted_printable
:
194 while line
[-2:] == '=\n':
196 newline
= ifile
.readline()
197 if newline
[:len(QUOTE
)] == QUOTE
:
198 newline
= newline
[len(QUOTE
):]
199 line
= line
+ newline
200 line
= mime_decode(line
)
201 if line
and is_base64
and not pref
:
203 line
= base64
.decodestring(line
)
205 ofile
.write(pref
+ line
)
207 def unmimify(infile
, outfile
, decode_base64
= 0):
208 """Convert quoted-printable parts of a MIME mail message to 8bit."""
209 if type(infile
) == type(''):
211 if type(outfile
) == type('') and infile
== outfile
:
213 d
, f
= os
.path
.split(infile
)
214 os
.rename(infile
, os
.path
.join(d
, ',' + f
))
217 if type(outfile
) == type(''):
218 ofile
= open(outfile
, 'w')
221 nifile
= File(ifile
, None)
222 unmimify_part(nifile
, ofile
, decode_base64
)
225 mime_char
= re
.compile('[=\177-\377]') # quote these chars in body
226 mime_header_char
= re
.compile('[=?\177-\377]') # quote these in header
228 def mime_encode(line
, header
):
229 """Code a single line as quoted-printable.
230 If header is set, quote some extra characters."""
232 reg
= mime_header_char
237 if len(line
) >= 5 and line
[:5] == 'From ':
238 # quote 'From ' at the start of a line for stupid mailers
239 newline
= ('=%02x' % ord('F')).upper()
242 res
= reg
.search(line
, pos
)
245 newline
= newline
+ line
[pos
:res
.start(0)] + \
246 ('=%02x' % ord(res
.group(0))).upper()
248 line
= newline
+ line
[pos
:]
251 while len(line
) >= 75:
253 while line
[i
] == '=' or line
[i
-1] == '=':
256 newline
= newline
+ line
[:i
] + '=\n'
258 return newline
+ line
260 mime_header
= re
.compile('([ \t(]|^)([-a-zA-Z0-9_+]*[\177-\377][-a-zA-Z0-9_+\177-\377]*)(?=[ \t)]|\n)')
262 def mime_encode_header(line
):
263 """Code a single header line as quoted-printable."""
267 res
= mime_header
.search(line
, pos
)
270 newline
= '%s%s%s=?%s?Q?%s?=' % \
271 (newline
, line
[pos
:res
.start(0)], res
.group(1),
272 CHARSET
, mime_encode(res
.group(2), 1))
274 return newline
+ line
[pos
:]
276 mv
= re
.compile('^mime-version:', re
.I
)
277 cte
= re
.compile('^content-transfer-encoding:', re
.I
)
278 iso_char
= re
.compile('[\177-\377]')
280 def mimify_part(ifile
, ofile
, is_mime
):
281 """Convert an 8bit part of a MIME mail message to quoted-printable."""
282 has_cte
= is_qp
= is_base64
= 0
284 must_quote_body
= must_quote_header
= has_iso_chars
= 0
291 hfile
= HeaderFile(ifile
)
293 line
= hfile
.readline()
296 if not must_quote_header
and iso_char
.search(line
):
297 must_quote_header
= 1
304 elif base64_re
.match(line
):
306 mp_res
= mp
.match(line
)
308 multipart
= '--' + mp_res
.group(1)
316 line
= ifile
.readline()
320 if line
== multipart
+ '--\n':
323 if line
== multipart
+ '\n':
330 while line
[-2:] == '=\n':
332 newline
= ifile
.readline()
333 if newline
[:len(QUOTE
)] == QUOTE
:
334 newline
= newline
[len(QUOTE
):]
335 line
= line
+ newline
336 line
= mime_decode(line
)
338 if not has_iso_chars
:
339 if iso_char
.search(line
):
340 has_iso_chars
= must_quote_body
= 1
341 if not must_quote_body
:
342 if len(line
) > MAXLEN
:
345 # convert and output header and body
347 if must_quote_header
:
348 line
= mime_encode_header(line
)
349 chrset_res
= chrset
.match(line
)
352 # change us-ascii into iso-8859-1
353 if chrset_res
.group(2).lower() == 'us-ascii':
354 line
= '%s%s%s' % (chrset_res
.group(1),
358 # change iso-8859-* into us-ascii
359 line
= '%sus-ascii%s' % chrset_res
.group(1, 3)
360 if has_cte
and cte
.match(line
):
361 line
= 'Content-Transfer-Encoding: '
363 line
= line
+ 'base64\n'
364 elif must_quote_body
:
365 line
= line
+ 'quoted-printable\n'
367 line
= line
+ '7bit\n'
369 if (must_quote_header
or must_quote_body
) and not is_mime
:
370 ofile
.write('Mime-Version: 1.0\n')
371 ofile
.write('Content-Type: text/plain; ')
373 ofile
.write('charset="%s"\n' % CHARSET
)
375 ofile
.write('charset="us-ascii"\n')
376 if must_quote_body
and not has_cte
:
377 ofile
.write('Content-Transfer-Encoding: quoted-printable\n')
378 ofile
.write(header_end
)
382 line
= mime_encode(line
, 0)
384 ofile
.write(message_end
)
388 if line
== multipart
+ '--\n':
389 # read bit after the end of the last part
391 line
= ifile
.readline()
395 line
= mime_encode(line
, 0)
397 if line
== multipart
+ '\n':
398 nifile
= File(ifile
, multipart
)
399 mimify_part(nifile
, ofile
, 1)
402 # premature end of file
406 # unexpectedly no multipart separator--copy rest of file
408 line
= ifile
.readline()
412 line
= mime_encode(line
, 0)
415 def mimify(infile
, outfile
):
416 """Convert 8bit parts of a MIME mail message to quoted-printable."""
417 if type(infile
) == type(''):
419 if type(outfile
) == type('') and infile
== outfile
:
421 d
, f
= os
.path
.split(infile
)
422 os
.rename(infile
, os
.path
.join(d
, ',' + f
))
425 if type(outfile
) == type(''):
426 ofile
= open(outfile
, 'w')
429 nifile
= File(ifile
, None)
430 mimify_part(nifile
, ofile
, 0)
434 if __name__
== '__main__' or (len(sys
.argv
) > 0 and sys
.argv
[0] == 'mimify'):
436 usage
= 'Usage: mimify [-l len] -[ed] [infile [outfile]]'
439 opts
, args
= getopt
.getopt(sys
.argv
[1:], 'l:edb')
440 if len(args
) not in (0, 1, 2):
443 if (('-e', '') in opts
) == (('-d', '') in opts
) or \
444 ((('-b', '') in opts
) and (('-d', '') not in opts
)):
455 except (ValueError, OverflowError):
461 encode_args
= (sys
.stdin
, sys
.stdout
)
463 encode_args
= (args
[0], sys
.stdout
)
465 encode_args
= (args
[0], args
[1])
467 encode_args
= encode_args
+ (decode_base64
,)