]> git.phdru.name Git - mimedecode.git/blob - mimedecode.py
Fixed version.
[mimedecode.git] / mimedecode.py
1 #! /usr/bin/env python
2 """Decode MIME message"""
3
4
5 __version__ = "2.1.1"
6 __author__ = "Oleg Broytman <phd@phdru.name>"
7 __copyright__ = "Copyright (C) 2001-2012 PhiloSoft Design"
8 __license__ = "GNU GPL"
9
10
11 import sys, os
12 import email
13
14 try:
15    from cStringIO import StringIO
16 except ImportError:
17    from StringIO import StringIO
18
19
20 import socket
21 host_name = socket.gethostname()
22
23 me = os.path.basename(sys.argv[0])
24
25
26 def version(exit=1):
27    sys.stdout.write("""\
28 Broytman mimedecode.py version %s, %s
29 """ % (_version, __copyright__))
30    if exit: sys.exit(0)
31
32
33 def usage(code=0):
34    version(0)
35    sys.stdout.write("""\
36 Usage: %s [-h|--help] [-V|--version] [-cCDP] [-f charset] [-d header] [-p header:param] [-beit mask] [filename]
37 """ % me)
38    sys.exit(code)
39
40
41 def output(s, outfile = sys.stdout):
42    outfile.write(s)
43
44 def output_headers(msg, outfile = sys.stdout):
45    unix_from = msg.get_unixfrom()
46    if unix_from:
47       output(unix_from + os.linesep)
48    for key, value in msg.items():
49       output("%s: %s\n" % (key, value), outfile)
50    output("\n", outfile) # End of headers
51
52
53 def recode(s, charset):
54    return unicode(s, charset, "replace").encode(GlobalOptions.default_encoding, "replace")
55
56
57 def recode2(s, charset):
58    if charset and charset.lower() <> GlobalOptions.default_encoding:
59       s = recode(s, charset)
60    return s
61
62
63 def _decode_header(s):
64    """Return a decoded string according to RFC 2047.
65    NOTE: This is almost the same as email.Utils.decode.
66    """
67    from types import ListType
68    import email.Header
69
70    L = email.Header.decode_header(s)
71    if not isinstance(L, ListType):
72       # s wasn't decoded
73       return s
74
75    rtn = []
76    for atom, charset in L:
77       if charset is None:
78          rtn.append(atom)
79       else:
80          rtn.append(recode2(atom, charset))
81       rtn.append(' ')
82    del rtn[-1] # remove the last space
83
84    # Now that we've decoded everything, we just need to join all the parts
85    # together into the final string.
86    return ''.join(rtn)
87
88
89 def decode_header(msg, header):
90    "Decode mail header (if exists) and put it back, if it was encoded"
91
92    if msg.has_key(header):
93       value = msg[header]
94       new_value = _decode_header(value)
95       if new_value <> value: # do not bother to touch msg if not changed
96          set_header(msg, header, new_value)
97
98
99 def _decode_header_param(s):
100    return recode2(s[2], s[0])
101
102
103 def decode_header_param(msg, header, param):
104    "Decode mail header's parameter (if exists) and put it back, if it was encoded"
105
106    if msg.has_key(header):
107       value = msg.get_param(param, header=header)
108       if value:
109          from types import TupleType
110          if isinstance(value, TupleType):
111             new_value = _decode_header_param(value)
112          else:
113             new_value = _decode_header(value)
114          if new_value <> value: # do not bother to touch msg if not changed
115             msg.set_param(param, new_value, header)
116
117
118 def decode_headers(msg):
119    "Decode message headers according to global options"
120
121    for header in GlobalOptions.decode_headers:
122       decode_header(msg, header)
123
124    for header, param in GlobalOptions.decode_header_params:
125       decode_header_param(msg, header, param)
126
127
128 def set_header(msg, header, value):
129    "Replace header"
130
131    if msg.has_key(header):
132       msg.replace_header(header, value)
133    else:
134       msg[header] = value
135
136
137 def set_content_type(msg, newtype, charset=None):
138    msg.set_type(newtype)
139
140    if charset:
141       msg.set_param("charset", charset, "Content-Type")
142
143
144
145 caps = None # Globally stored mailcap database; initialized only if needed
146
147 def decode_body(msg, s):
148    "Decode body to plain text using first copiousoutput filter from mailcap"
149
150    import mailcap, tempfile
151
152    global caps
153    if caps is None:
154       caps = mailcap.getcaps()
155
156    content_type = msg.get_content_type()
157    filename = tempfile.mktemp()
158    command = None
159
160    entries = mailcap.lookup(caps, content_type, "view")
161    for entry in entries:
162       if entry.has_key('copiousoutput'):
163          if entry.has_key('test'):
164             test = mailcap.subst(entry['test'], content_type, filename)
165             if test and os.system(test) != 0:
166                continue
167          command = mailcap.subst(entry["view"], content_type, filename)
168          break
169
170    if not command:
171       return s
172
173    file = open(filename, 'w')
174    file.write(s)
175    file.close()
176
177    pipe = os.popen(command, 'r')
178    s = pipe.read()
179    pipe.close()
180    os.remove(filename)
181
182    set_content_type(msg, "text/plain")
183    msg["X-MIME-Autoconverted"] = "from %s to text/plain by %s id %s" % (content_type, host_name, command.split()[0])
184
185    return s
186
187
188 def recode_charset(msg, s):
189    "Recode charset of the message to the default charset"
190
191    save_charset = charset = msg.get_content_charset()
192    if charset and charset.lower() <> GlobalOptions.default_encoding:
193       s = recode2(s, charset)
194       content_type = msg.get_content_type()
195       set_content_type(msg, content_type, GlobalOptions.default_encoding)
196       msg["X-MIME-Autoconverted"] = "from %s to %s by %s id %s" % (save_charset, GlobalOptions.default_encoding, host_name, me)
197    return s
198
199
200 def totext(msg, instring):
201    "Convert instring content to text"
202
203    if msg.is_multipart(): # Recursively decode all parts of the multipart message
204       newfile = StringIO(str(msg))
205       newfile.seek(0)
206       decode_file(newfile)
207       return
208
209    # Decode body and recode charset
210    s = decode_body(msg, instring)
211    if GlobalOptions.recode_charset:
212       s = recode_charset(msg, s)
213
214    output_headers(msg)
215    output(s)
216
217
218 def decode_part(msg):
219    "Decode one part of the message"
220
221    decode_headers(msg)
222    encoding = msg["Content-Transfer-Encoding"]
223
224    if encoding in (None, '', '7bit', '8bit', 'binary'):
225       outstring = str(msg.get_payload())
226    else: # Decode from transfer ecoding to text or binary form
227       outstring = str(msg.get_payload(decode=1))
228       set_header(msg, "Content-Transfer-Encoding", "8bit")
229       msg["X-MIME-Autoconverted"] = "from %s to 8bit by %s id %s" % (encoding, host_name, me)
230
231    # Test all mask lists and find what to do with this content type
232    masks = []
233    ctype = msg.get_content_type()
234    if ctype:
235       masks.append(ctype)
236    mtype = msg.get_content_maintype()
237    if mtype:
238       masks.append(mtype + '/*')
239    masks.append('*/*')
240
241    for content_type in masks:
242       if content_type in GlobalOptions.totext_mask:
243          totext(msg, outstring)
244          return
245       elif content_type in GlobalOptions.binary_mask:
246          output_headers(msg)
247          output(outstring)
248          return
249       elif content_type in GlobalOptions.ignore_mask:
250          output_headers(msg)
251          output("\nMessage body of type `%s' skipped.\n" % content_type)
252          return
253       elif content_type in GlobalOptions.error_mask:
254          raise ValueError, "content type `%s' prohibited" % content_type
255
256    # Neither content type nor masks were listed - decode by default
257    totext(msg, outstring)
258
259
260 def decode_file(infile):
261    "Decode the entire message"
262
263    msg = email.message_from_file(infile)
264    boundary = msg.get_boundary()
265
266    if msg.is_multipart():
267       decode_headers(msg)
268       output_headers(msg)
269
270       if msg.preamble: # Preserve the first part, it is probably not a RFC822-message
271          output(msg.preamble) # Usually it is just a few lines of text (MIME warning)
272
273       for subpart in msg.get_payload():
274          output("\n--%s\n" % boundary)
275          decode_part(subpart)
276
277       output("\n--%s--\n" % boundary)
278
279       if msg.epilogue:
280          output(msg.epilogue)
281
282    else:
283       if msg.has_key("Content-Type"): # Simple one-part message - decode it
284          decode_part(msg)
285
286       else: # Not a message, just text - copy it literally
287          output(str(msg))
288
289
290 class GlobalOptions:
291    from m_lib.defenc import default_encoding
292    recode_charset = 1 # recode charset of message body
293
294    decode_headers = ["Subject", "From"] # A list of headers to decode
295    decode_header_params = [("Content-Type", "name"),
296       ("Content-Disposition", "filename")
297    ] # A list of headers' parameters to decode
298
299    totext_mask = [] # A list of content-types to decode
300    binary_mask = [] # A list to pass through
301    ignore_mask = [] # Ignore (skip, do not decode and do not include into output)
302    error_mask = []  # Raise error if encounter one of these
303
304
305 def init():
306    from getopt import getopt, GetoptError
307
308    try:
309       options, arguments = getopt(sys.argv[1:], 'hVcCDPf:d:p:b:e:i:t:',
310          ['help', 'version'])
311    except GetoptError:
312       usage(1)
313
314    for option, value in options:
315       if option == '-h':
316          usage()
317       elif option == '--help':
318          usage()
319       elif option == '-V':
320          version()
321       elif option == '--version':
322          version()
323       elif option == '-c':
324          GlobalOptions.recode_charset = 1
325       elif option == '-C':
326          GlobalOptions.recode_charset = 0
327       elif option == '-f':
328          GlobalOptions.default_encoding = value
329       elif option == '-d':
330          GlobalOptions.decode_headers.append(value)
331       elif option == '-D':
332          GlobalOptions.decode_headers = []
333       elif option == '-p':
334          GlobalOptions.decode_header_params.append(value.split(':', 1))
335       elif option == '-P':
336          GlobalOptions.decode_header_params = []
337       elif option == '-t':
338          GlobalOptions.totext_mask.append(value)
339       elif option == '-b':
340          GlobalOptions.binary_mask.append(value)
341       elif option == '-i':
342          GlobalOptions.ignore_mask.append(value)
343       elif option == '-e':
344          GlobalOptions.error_mask.append(value)
345       else:
346          usage(1)
347
348    return arguments
349
350
351 if __name__ == "__main__":
352    arguments = init()
353
354    la = len(arguments)
355    if la == 0:
356       infile = sys.stdin
357    elif la <> 1:
358       usage(1)
359    elif arguments[0] == '-':
360       infile = sys.stdin
361    else:
362       infile = open(arguments[0], 'r')
363
364    decode_file(infile)