2 # Copyright (C) 2009-2010 W. Trevor King <wking@drexel.edu>
4 # This program is free software; you can redistribute it and/or modify
5 # it under the terms of the GNU General Public License as published by
6 # the Free Software Foundation; either version 2 of the License, or
7 # (at your option) any later version.
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
14 # You should have received a copy of the GNU General Public License along
15 # with this program; if not, write to the Free Software Foundation, Inc.,
16 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18 Convert an mbox into xml suitable for input into be.
19 $ be-mbox-to-xml file.mbox | be import-xml -c <ID> -
20 mbox is a flat-file format, consisting of a series of messages.
21 Messages begin with a a From_ line, followed by RFC 822 email,
22 followed by a blank line.
28 from libbe.util.encoding import get_output_encoding
29 from libbe.util.utility import time_to_str
30 import mailbox # the mailbox people really want an on-disk copy
33 from time import asctime, gmtime, mktime
35 from xml.sax.saxutils import escape
37 BREAK = u'--' # signature separator
38 DEFAULT_ENCODING = get_output_encoding()
39 sys.stdout = codecs.getwriter(DEFAULT_ENCODING)(sys.stdout)
43 def normalize_email_address(address):
45 Standardize whitespace, etc.
47 addr = email.utils.formataddr(email.utils.parseaddr(address))
52 def normalize_RFC_2822_date(date):
54 Some email clients write non-RFC 2822-compliant date tags like:
55 Fri, 18 Sep 2009 08:49:02 -0400 (EDT)
56 with the non-standard (EDT) timezone name. This funtion attempts
57 to deal with such inconsistencies.
59 time_tuple = email.utils.parsedate(date)
60 assert time_tuple != None, \
61 'unparsable date: "%s"' % date
62 return time_to_str(mktime(time_tuple))
64 def strip_footer(body):
65 body_lines = body.splitlines()
66 for i,line in enumerate(body_lines):
67 if line.startswith(BREAK):
69 i += 1 # increment past the current valid line.
70 return u'\n'.join(body_lines[:i]).strip()
72 def comment_message_to_xml(message, fields=None):
76 new_fields[u'alt-id'] = message[u'message-id']
77 new_fields[u'in-reply-to'] = message[u'in-reply-to']
78 new_fields[u'author'] = normalize_email_address(message[u'from'])
79 new_fields[u'date'] = message[u'date']
80 if new_fields[u'date'] != None:
81 new_fields[u'date'] = normalize_RFC_2822_date(new_fields[u'date'])
82 new_fields[u'content-type'] = message.get_content_type()
83 for k,v in new_fields.items():
84 if v != None and type(v) != types.UnicodeType:
85 fields[k] = unicode(v, encoding=DEFAULT_ENCODING)
86 elif v == None and k in fields:
87 new_fields[k] = fields[k]
88 for k,v in fields.items():
89 if k not in new_fields:
90 new_fields.k = fields[k]
93 if fields[u'in-reply-to'] == None:
94 if message[u'references'] != None:
95 refs = message[u'references'].split()
96 for ref in refs: # search for a known reference id.
98 fields[u'in-reply-to'] = ref
100 if fields[u'in-reply-to'] == None and len(refs) > 0:
101 fields[u'in-reply-to'] = refs[0] # default to the first
102 else: # check for mutliple in-reply-to references.
103 refs = fields[u'in-reply-to'].split()
105 for ref in refs: # search for a known reference id.
107 fields[u'in-reply-to'] = ref
110 if found_ref == False and len(refs) > 0:
111 fields[u'in-reply-to'] = refs[0] # default to the first
113 if fields[u'alt-id'] != None:
114 KNOWN_IDS.append(fields[u'alt-id'])
116 if message.is_multipart():
118 alt_id = fields[u'alt-id']
119 from_str = fields[u'author']
120 date = fields[u'date']
121 for m in message.walk():
124 fields[u'author'] = from_str
125 fields[u'date'] = date
126 if len(ret) > 0: # we've added one part already
127 fields.pop(u'alt-id') # don't pass alt-id to other parts
128 fields[u'in-reply-to'] = alt_id # others respond to first
129 ret.append(comment_message_to_xml(m, fields))
130 return u'\n'.join(ret)
132 charset = message.get_content_charset(DEFAULT_ENCODING).lower()
133 #assert charset == DEFAULT_ENCODING.lower(), \
134 # u"Unknown charset: %s" % charset
136 if message[u'content-transfer-encoding'] == None:
137 encoding = DEFAULT_ENCODING
139 encoding = message[u'content-transfer-encoding'].lower()
140 body = message.get_payload(decode=True) # attempt to decode
141 assert body != None, "Unable to decode?"
142 if fields[u'content-type'].startswith(u"text/"):
143 body = strip_footer(unicode(body, encoding=charset))
145 body = base64.encode(body)
146 fields[u'body'] = body
147 lines = [u"<comment>"]
148 for tag,body in fields.items():
151 lines.append(u" <%s>%s</%s>" % (tag, ebody, tag))
152 lines.append(u"</comment>")
153 return u'\n'.join(lines)
156 parser = optparse.OptionParser(usage='%prog [options] mailbox')
157 formats = ['mbox', 'Maildir', 'MH', 'Babyl', 'MMDF']
158 parser.add_option('-f', '--format', type='choice', dest='format',
159 help="Select the mailbox format from %s. See the mailbox module's documention for descriptions of these formats." \
160 % ', '.join(formats),
161 default='mbox', choices=formats)
162 options,args = parser.parse_args(argv)
163 mailbox_file = args[1]
164 reader = getattr(mailbox, options.format)
165 mb = reader(mailbox_file, factory=None)
166 print u'<?xml version="1.0" encoding="%s" ?>' % DEFAULT_ENCODING
169 print comment_message_to_xml(message)
173 if __name__ == "__main__":