emlx2maildir.py (6336B)
1 #!/usr/bin/env python 2 3 # Copyright (C) 2009-2013 Mike Laiosa <mike@laiosa.org> and others. 4 # 5 # This program is free software; you can redistribute it and/or 6 # modify it under the terms of the GNU General Public License 7 # as published by the Free Software Foundation; either version 2 8 # of the License, or (at your option) any later version. 9 # 10 # This program is distributed in the hope that it will be useful, 11 # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 # GNU General Public License for more details. 14 # 15 # You should have received a copy of the GNU General Public License 16 # along with this program; if not, write to the Free Software 17 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 19 import optparse 20 import xml.sax, xml.sax.handler 21 import re, os, os.path, socket, time 22 23 class PlistHandler(xml.sax.handler.ContentHandler): 24 def __init__(self): 25 xml.sax.handler.ContentHandler.__init__(self) 26 self.key = None 27 def set_top(k,v): 28 self.top = v 29 self.stack = [set_top] 30 def generate(self, value): 31 self.stack[-1](self.key, value) 32 def startElement(self, name, attrs): 33 self.elem = name 34 self.value = "" 35 if name == "array": 36 ar = [] 37 self.stack.append(ar) 38 self.stack.append(lambda k,v: ar.append(v)) 39 elif name == "dict": 40 d = {} 41 def add(k, v): 42 d[k] = v 43 self.stack.append(d) 44 self.stack.append(add) 45 def endElement(self, name): 46 if name == "string": 47 self.generate(self.value) 48 elif name == "integer": 49 self.generate(long(self.value)) 50 elif name == "real": 51 self.generate(float(self.value)) 52 elif name == "key": 53 self.key = self.value 54 elif name in ("dict", "array"): 55 x = self.stack[-2] 56 self.stack = self.stack[:-2] 57 self.generate(x) 58 elif name in ("plist", "data"): 59 pass 60 else: 61 print "Unknown tag: %s" % name 62 def characters(self, chars): 63 self.value += chars 64 65 def parse_plist(plist_xml): 66 # Remove any DOCTYPE declarations, to stop the XML parser from trying to 67 # download the DTD. 68 plist_xml = re.sub(r'<!DOCTYPE.*?"\s*>', "", plist_xml) 69 p = PlistHandler() 70 xml.sax.parseString(plist_xml, p) 71 return p.top 72 73 FL_READ = (1<<0) 74 FL_DELETED = (1<<1) 75 FL_ANSWERED = (1<<2) 76 FL_ENCRYPTED = (1<<3) 77 FL_FLAGGED = (1<<4) 78 FL_RECENT = (1<<5) 79 FL_DRAFT = (1<<6) 80 FL_INITIAL = (1<<7) 81 FL_FORWARDED = (1<<8) 82 FL_REDIRECTED = (1<<9) 83 FL_SIGNED = (1<<23) 84 FL_IS_JUNK = (1<<24) 85 FL_IS_NOT_JUNK = (1<<25) 86 FL_JUNK_LEVEL_RECORDED = (1<<29) 87 FL_HIGHLIGHT_IN_TOC = (1<<30) 88 89 flag_mapping = [ 90 (FL_DRAFT, "D"), 91 (FL_FLAGGED, "F"), 92 ((FL_FORWARDED | FL_REDIRECTED), "P"), 93 (FL_ANSWERED, "R"), 94 (FL_READ, "S"), 95 (FL_DELETED, "T"), 96 ] 97 98 hostname = socket.gethostname() 99 pid = os.getpid() 100 gSeq = 0 101 102 def md_filename(date, flags): 103 global gSeq 104 gSeq += 1 105 return "%d.M%dP%dQ%d.%s:2,%s" % (date, time.time(), pid, gSeq, hostname, flags) 106 107 def convert_one(emlx_file, maildir): 108 contents = open(emlx_file, "rb").read() 109 boundry = contents.find("\x0a") 110 length = long(contents[:boundry]) 111 body = contents[boundry+1:boundry+1+length] 112 metadata = parse_plist(contents[boundry+1+length:]) 113 114 flags = "" 115 if "flags" in metadata: 116 for fl, let in flag_mapping: 117 if metadata['flags'] & fl: 118 flags += let 119 120 date = long(metadata.get('date-sent', time.time())) 121 filename = md_filename(date, flags) 122 tmp_name = os.path.join(maildir, "tmp", filename) 123 cur_name = os.path.join(maildir, "cur", filename) 124 open(tmp_name, "wb").write(body) 125 os.rename(tmp_name, cur_name) 126 os.utime(cur_name, (date,date)) 127 128 def emlx_message_dir(emlx_dir): 129 msg_dir = os.path.join(emlx_dir + ".mbox", "Messages") 130 if not os.path.isdir(msg_dir): 131 msg_dir = os.path.join(emlx_dir + ".imapmbox", "Messages") 132 if not os.path.isdir(msg_dir): 133 return None 134 return msg_dir 135 136 def emlx_message_dirs(emlx_dir): 137 for x in os.listdir(emlx_dir): 138 suffixes = [".sbd", ".mbox", ".imapmbox"] 139 search = True 140 for s in suffixes: 141 if x.endswith(s): 142 search = False 143 if x == "Messages": 144 msg_dir = os.path.join(emlx_dir,"Messages") 145 yield msg_dir 146 elif search: 147 subfolder = os.path.join(emlx_dir, x) 148 if os.path.isdir(subfolder): 149 print "Recursing into %r" % (subfolder) 150 for tmp in emlx_message_dirs(subfolder): 151 yield tmp 152 153 def emlx_messages(emlx_dir): 154 if os.path.isdir(emlx_dir + ".mbox"): 155 for msg_dir in emlx_message_dirs(emlx_dir + ".mbox"): 156 for x in os.listdir(msg_dir): 157 if x.endswith(".emlx") and x[0] != '.': 158 yield os.path.join(msg_dir, x) 159 160 def emlx_subfolders(emlx_dir): 161 if not os.path.isdir(emlx_dir): 162 if os.path.isdir(emlx_dir + ".sbd"): 163 emlx_dir = ".sbd" 164 else: 165 return 166 for x in os.listdir(emlx_dir): 167 suffixes = [".sbd", ".mbox", ".imapmbox"] 168 for s in suffixes: 169 if x.endswith(s): 170 subfolder = os.path.join(emlx_dir, x[:-len(s)]) 171 for tmp in emlx_subfolders(os.path.join(emlx_dir, x)): 172 yield tmp 173 yield subfolder 174 175 def maildirmake(dir): 176 for s in ["cur", "new", "tmp"]: 177 if not os.path.exists(os.path.join(dir, s)): 178 os.makedirs(os.path.join(dir, s)) 179 180 def remove_slash(s): 181 if len(s) and s[-1] == '/': 182 return s[:-1] 183 else: 184 return s 185 186 def main(): 187 parser = optparse.OptionParser(usage="Usage: %prog [options] emlx_folder maildir") 188 parser.add_option("-r", "--recursive", action="store_true", help="Recurse into subfolders") 189 parser.add_option("-q", "--quiet", action="store_true", help="Only print error output") 190 parser.add_option("--dry-run", action="store_true", help="Don't do anything") 191 parser.add_option("--verbose", action="store_true", help="Displays lots of stuff") 192 opts, args = parser.parse_args() 193 194 def P(s): 195 if not opts.quiet: 196 print s 197 def V(s): 198 if opts.dry_run or opts.verbose: 199 P(s) 200 201 def dry(s, act, *args, **kwargs): 202 V(s) 203 if not opts.dry_run: 204 return act(*args, **kwargs) 205 206 if len(args) != 2: 207 parser.error("Not enough arguments") 208 209 tasks = [(remove_slash(args[0]), args[1] + '/')] 210 while len(tasks): 211 emlx_folder, maildir = tasks[-1] 212 213 P("Converting %r -> %r" % (emlx_folder, maildir)) 214 tasks = tasks[:-1] 215 dry("Making maildir %r" % maildir, maildirmake, maildir) 216 for msg in emlx_messages(emlx_folder): 217 dry("Converting message %r" % msg, convert_one, msg, maildir) 218 if opts.recursive: 219 for f in emlx_subfolders(emlx_folder): 220 tasks.append((f, maildir + "." + os.path.basename(f))) 221 222 if __name__ == "__main__": 223 main()