2006-09-10 Francisco Javier F. Serrador <serrador@openshine.com>
[beagle.git] / Util / Thunderbird.cs
blobc5cb28f0b0f436d39a32a5cd39d9a583cd5bb085
1 //
2 // Thunderbird.cs: A utility class with methods and classes that might be needed to parse Thunderbird data
3 //
4 // Copyright (C) 2006 Pierre Östlund
5 //
7 //
8 // Permission is hereby granted, free of charge, to any person obtaining a copy
9 // of this software and associated documentation files (the "Software"), to deal
10 // in the Software without restriction, including without limitation the rights
11 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 // copies of the Software, and to permit persons to whom the Software is
13 // furnished to do so, subject to the following conditions:
15 // The above copyright notice and this permission notice shall be included in all
16 // copies or substantial portions of the Software.
18 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 // SOFTWARE.
27 using System;
28 using System.IO;
29 using System.Text;
30 using System.Collections;
31 using System.Globalization;
32 using System.Text.RegularExpressions;
34 using Beagle;
35 using Beagle.Util;
37 using GMime;
39 namespace Beagle.Util {
41 public class Thunderbird {
43 public static bool Debug = false;
45 /////////////////////////////////////////////////////////////////////////////////////
47 public enum AccountType {
48 Pop3,
49 Imap,
50 Rss,
51 Nntp,
52 AddressBook,
53 MoveMail,
54 Invalid
57 /////////////////////////////////////////////////////////////////////////////////////
59 public class Account {
60 private string server_string = null;
61 private string path = null;
62 private int server_port = -1;
63 private AccountType account_type;
64 private char delimiter;
66 public Account (string server, string path, int port, AccountType type, char delim)
68 this.server_string = server;
69 this.path = path;
70 this.server_port = port;
71 this.account_type = type;
72 this.delimiter = delim;
75 public string Server {
76 get { return server_string; }
79 public string Path {
80 get { return path; }
83 public int Port {
84 get { return (server_port > 0 ? server_port : Thunderbird.ParsePort (Type)); }
87 public AccountType Type {
88 get { return account_type; }
91 public char Delimiter {
92 get { return (delimiter == char.MinValue ? '/' : delimiter); }
96 /////////////////////////////////////////////////////////////////////////////////////
98 public abstract class StorageBase {
99 protected Hashtable data;
100 protected System.Uri uri;
101 protected Account account;
103 public StorageBase ()
105 data = new Hashtable ();
108 public string GetString (string key)
110 return Convert.ToString (data [key]);
113 public int GetInt (string key)
115 try {
116 if (!data.ContainsKey (key))
117 return -1;
119 return Convert.ToInt32 (data [key]);
120 } catch {
121 return -1;
125 public bool GetBool (string key)
127 try {
128 return Convert.ToBoolean (data [key]);
129 } catch {
130 return false;
134 public object GetObject (string key)
136 return data [key];
139 public void SetObject (string key, object value)
141 if (key != null)
142 data [key] = value;
145 public System.Uri Uri {
146 get { return uri; }
147 set { uri = value; }
150 public Account Account {
151 get { return account; }
155 /////////////////////////////////////////////////////////////////////////////////////
157 // String types:
158 // id, sender, subject, recipients, date, mailbox
159 // Integer types:
160 // size, msgOffset, offlineMsgSize
161 // Bool types:
162 // FullIndex
163 public class Mail : StorageBase {
164 private string workfile;
166 public Mail (Account account, Hashtable data, string workfile)
168 foreach (string key in data.Keys) {
169 if (key == "id")
170 SetObject (key, data [key]);
171 else if (key == "sender")
172 SetObject (key, Utils.HeaderDecodePhrase ((string) data [key]));
173 else if (key == "subject")
174 SetObject (key, Utils.HeaderDecodeText ((string) data [key]));
175 else if (key == "recipients")
176 SetObject (key, Utils.HeaderDecodePhrase ((string) data [key]));
177 else if (key == "date")
178 SetObject (key, Thunderbird.HexDateToString ((string) data [key]));
179 else if (key == "size")
180 SetObject (key, Thunderbird.Hex2Dec ((string) data [key]));
181 else if (key == "msgOffset")
182 SetObject (key, Thunderbird.Hex2Dec ((string) data[key]));
183 else if (key == "offlineMsgSize")
184 SetObject (key, Thunderbird.Hex2Dec ((string) data [key]));
185 else if (key == "message-id")
186 SetObject (key, (string) data [key]);
187 else if (key == "references")
188 SetObject (key, (data [key] as string).Replace ("\\", ""));
191 this.account = account;
192 this.workfile = workfile;
193 SetObject ("mailbox", Thunderbird.ConstructMailboxString (workfile, account));
194 this.uri = Thunderbird.NewUri (Account, GetString ("mailbox"), GetString ("id"));
197 private GMime.Message ConstructMessage ()
199 GMime.Message message = null;
201 // Try to fully index this mail by loading the entire mail into memory
202 if (GetBool ("FullIndex"))
203 message = FullMessage ();
205 // Make sure we have the correct status set on this message, in case something went wrong
206 if (message == null || (message != null && message.Stream.Length <= 1)) {
207 SetObject ("FullIndex", (object) false);
208 return PartialMessage ();
209 } else
210 return message;
213 private GMime.Message PartialMessage ()
215 string date = GetString ("date");
216 GMime.Message message = new GMime.Message (true);
218 message.Subject = GetString ("subject");
219 message.Sender = GetString ("sender");
220 message.MessageId = GetString ("message-id");
221 message.SetDate ((date != string.Empty ? DateTime.Parse (date) : new DateTime (1970, 1, 1, 0, 0, 0)), 0);
223 // Add references
224 if (data.ContainsKey ("references")) {
225 foreach (Match m in Regex.Matches ((data ["references"] as string), @"\<(?>[^\<\>]+)\>"))
226 message.AddHeader ("References", m.Value);
229 return message;
232 private GMime.Message FullMessage ()
234 int fd;
235 string file = Thunderbird.GetFullyIndexableFile (workfile);
236 GMime.Message message = null;
238 // gmime will go nuts and make the daemon "segmentation fault" in case the file doesn't exist!
239 if (!File.Exists (file))
240 return message;
242 try {
243 fd = Mono.Unix.Native.Syscall.open (file, Mono.Unix.Native.OpenFlags.O_RDONLY);
244 StreamFs stream = new StreamFs (fd, Offset, Offset + Size);
245 Parser parser = new Parser (stream);
246 message = parser.ConstructMessage ();
248 stream.Dispose ();
249 parser.Dispose ();
250 } catch {}
252 return message;
255 public int Offset {
256 get {
257 int msg_offset = GetInt ("msgOffset");
258 return (msg_offset >= 0 ? msg_offset : Thunderbird.Hex2Dec (GetString ("id")));
262 public int Size {
263 get {
264 int msg_offline_size = GetInt ("offlineMsgSize");
265 return (msg_offline_size >= 0 ? msg_offline_size : GetInt ("size"));
269 public GMime.Message Message {
270 get { return ConstructMessage (); }
274 /////////////////////////////////////////////////////////////////////////////////////
276 // String types:
277 // id, FirstName, LastName, DisplayName, NickName, PrimaryEmail, SecondEmail,
278 // WorkPhone, FaxNumber, HomePhone, PagerNumber, CellularNumber, HomeAddress,
279 // HomeAddress2, HomeCity, HomeState, HomeZipCode, HomeCountry, WorkAddress,
280 // WorkAddress2, WorkCity, WorkState, WorkZipCode, WorkCountry, JobTitle, Department,
281 // Company, _AimScreenName, FamilyName, WebPage1, WebPage2, BirthYear, BirthMonth
282 // , BirthDay, Custom1, Custom2, Custom3, Custom4, Notes, PreferMailFormat
283 // Integer types:
284 // None
285 public class Contact : StorageBase {
286 private string workfile;
288 public Contact (Account account, Hashtable data, string workfile)
290 this.account = account;
291 this.data = data;
292 this.workfile = workfile;
293 this.uri = NewUri (account, Thunderbird.ConstructMailboxString (workfile, account), GetString ("id"));
296 public string Workfile {
297 get { return workfile; }
302 /////////////////////////////////////////////////////////////////////////////////////
304 // String types:
305 // id, subject, sender, date, message-id
306 // Integer types:
307 // size
308 public class RssFeed : StorageBase {
309 private string workfile;
311 public RssFeed (Account account, Hashtable data, string workfile)
313 foreach (string key in data.Keys) {
314 if (key == "id")
315 SetObject (key, data [key]);
316 else if (key == "subject") // title
317 SetObject (key, Utils.HeaderDecodePhrase ((string) data [key]));
318 else if (key == "sender") // publisher
319 SetObject (key, Utils.HeaderDecodePhrase ((string) data [key]));
320 else if (key == "date") // date
321 SetObject (key, HexDateToString ((string) data [key]));
322 else if (key == "size") // size
323 SetObject (key, Hex2Dec ((string) data [key]));
324 else if (key == "message-id") { // links
325 string tmp = (string) data [key];
326 SetObject (key, Utils.HeaderDecodePhrase (tmp.Substring (0, tmp.LastIndexOf ("@"))));
330 this.account = account;
331 this.workfile = workfile;
332 this.uri = NewUri (account, ConstructMailboxString (workfile, account), GetString ("id"));
335 // FIXME: Make this a lot faster!
336 private StringReader ConstructContent ()
338 string content = null;
339 string file = GetFullyIndexableFile (workfile);
341 if (!File.Exists (file))
342 return null;
344 try {
345 StreamReader reader = new StreamReader (file);
347 char[] tmp = new char [GetInt ("size")];
348 reader.BaseStream.Seek (Hex2Dec (GetString ("id")), SeekOrigin.Begin);
349 reader.Read (tmp, 0, tmp.Length);
351 // We don't want to index all HTTP headers, so we cut 'em off
352 content = new string (tmp);
353 content = content.Substring (content.IndexOf ("<html>"));
355 reader.Close ();
356 } catch { }
358 return (content != null ? new StringReader (content) : null);
361 public string Workfile {
362 get { return workfile; }
365 public StringReader Content {
366 get { return ConstructContent (); }
370 /////////////////////////////////////////////////////////////////////////////////////
372 // String types:
373 // id, subject, sender, date
374 // Integer types:
375 // size
376 // An NNTP message resambles a mail so very much...
377 public class NntpMessage : Mail {
379 public NntpMessage (Account account, Hashtable data, string workfile)
380 : base (account, data, workfile)
382 foreach (string key in data.Keys) {
383 if (key == "id")
384 SetObject (key, data [key]);
385 else if (key == "subject")
386 SetObject (key, Utils.HeaderDecodeText ((string) data [key]));
387 else if (key == "sender")
388 SetObject (key, Utils.HeaderDecodePhrase ((string) data [key]));
389 else if (key == "date")
390 SetObject (key, Thunderbird.HexDateToString ((string) data [key]));
391 else if (key == "size")
392 SetObject (key, Thunderbird.Hex2Dec ((string) data [key]));
395 Uri = NewUri (account, ConstructMailboxString (workfile, account), GetString ("id"));
400 /////////////////////////////////////////////////////////////////////////////////////
402 // Still just a stub, will be fixed later on
403 public class MoveMail : StorageBase {
405 public MoveMail (Account account, Hashtable data, string workfile)
407 this.account = account;
408 this.data = data;
409 //this.workfile = workfile;
410 this.uri = NewUri (account, GetString ("tmp"), GetString ("id"));
415 /////////////////////////////////////////////////////////////////////////////////////
417 public class Database : IEnumerable {
418 private static MorkDatabase db;
419 private Account account;
420 private string file;
422 private IEnumerator current = null;
424 public Database (Account account, string file)
426 this.account = account;
427 this.file = file;
430 public void Load ()
432 db = new MorkDatabase (file);
433 db.Read();
435 switch (account.Type) {
436 case AccountType.Pop3:
437 case AccountType.Imap:
438 case AccountType.Rss:
439 case AccountType.Nntp:
440 case AccountType.MoveMail:
441 db.EnumNamespace = "ns:msg:db:row:scope:msgs:all";
442 break;
443 case AccountType.AddressBook:
444 db.EnumNamespace = "ns:addrbk:db:row:scope:card:all";
445 break;
448 current = db.GetEnumerator ();
451 public Account Account {
452 get { return account; }
455 public int Count {
456 get {
457 if (db == null)
458 return 0;
460 return (account.Type == AccountType.AddressBook ?
461 db.GetRowCount ("ns:addrbk:db:row:scope:card:all", "BF") :
462 db.GetRowCount ("ns:msg:db:row:scope:msgs:all"));
466 public string Filename {
467 get { return (db != null ? db.Filename : string.Empty); }
470 public MorkDatabase Db {
471 get { return db; }
474 public IEnumerator GetEnumerator ()
476 return new DatabaseEnumerator (db, account, current);
479 public class DatabaseEnumerator : IEnumerator {
480 private MorkDatabase db;
481 private Account account;
482 private IEnumerator enumerator;
484 public DatabaseEnumerator (MorkDatabase db, Account account, IEnumerator enumerator)
486 this.db = db;
487 this.enumerator = enumerator;
488 this.account = account;
491 public bool MoveNext ()
493 return (enumerator != null ? enumerator.MoveNext () : false);
496 public void Reset ()
498 enumerator.Reset ();
501 public object Current {
502 get {
503 switch (account.Type) {
504 case AccountType.Pop3:
505 case AccountType.Imap:
506 return new Mail (account, db.Compile ((string) enumerator.Current,
507 "ns:msg:db:row:scope:msgs:all"), db.Filename);
508 case AccountType.AddressBook:
509 return new Contact (account, db.Compile ((string) enumerator.Current,
510 "ns:addrbk:db:row:scope:card:all"), db.Filename);
511 case AccountType.Rss:
512 return new RssFeed (account, db.Compile ((string) enumerator.Current,
513 "ns:msg:db:row:scope:msgs:all"), db.Filename);
514 case AccountType.Nntp:
515 return new NntpMessage (account, db.Compile ((string) enumerator.Current,
516 "ns:msg:db:row:scope:msgs:all"), db.Filename);
517 case AccountType.MoveMail:
518 return new MoveMail (account, db.Compile ((string) enumerator.Current,
519 "ns:msg:db:row:scope:msgs:all"), db.Filename);
522 return null;
528 /////////////////////////////////////////////////////////////////////////////////////
530 public static string ExecutableName {
531 get {
532 bool is_mt = false;
533 string exec_name = "thunderbird";
535 foreach (string s in PathFinder.Paths) {
536 if (File.Exists (Path.Combine (s, "mozilla-thunderbird"))) {
537 exec_name = "mozilla-thunderbird";
538 break;
542 return exec_name;
546 /////////////////////////////////////////////////////////////////////////////////////
548 public static string HexDateToString (string hex)
550 DateTime time = new DateTime (1970,1,1,0,0,0);
552 try {
553 time = time.AddSeconds (
554 Int32.Parse (hex, NumberStyles.HexNumber));
555 } catch {}
557 return time.ToString ();
560 public static int Hex2Dec (string hex)
562 int dec = -1;
564 try {
565 dec = Convert.ToInt32 (hex, 16);
566 } catch { }
568 return dec;
571 public static int ParsePort (AccountType type)
573 int port = 0;
575 switch (type) {
576 case AccountType.Pop3:
577 port = 110;
578 break;
579 case AccountType.Imap:
580 port = 143;
581 break;
584 return port;
587 public static AccountType ParseAccountType (string type_str)
589 AccountType type;
591 try {
592 type = (AccountType) Enum.Parse (typeof (AccountType), type_str, true);
593 } catch {
594 type = AccountType.Invalid;
597 return type;
600 // A hack to extract a potential delimiter from a namespace-string
601 public static char GetDelimiter (params string[] namespace_str)
603 MatchCollection matches = null;
604 Regex reg = new Regex (@"\\\""(.*)(?<delimiter>[^,])\\\""", RegexOptions.Compiled);
606 if (namespace_str == null)
607 return char.MinValue;
609 foreach (string str in namespace_str) {
610 try {
611 matches = reg.Matches (str);
612 } catch {
613 continue;
616 foreach (Match m in matches) {
617 char delim = Convert.ToChar (m.Result ("${delimiter}"));
618 if (delim != ' ')
619 return delim;
623 return char.MinValue;
626 public static Uri NewUri (Account account, string mailbox, string id)
628 Uri uri = null;
630 switch (account.Type) {
631 case AccountType.Pop3:
632 case AccountType.MoveMail:
633 case AccountType.Rss: // rss, movemail and pop3 share the same uri scheme
634 uri = new Uri (String.Format ("mailbox://{0}/{1}?number={2}",
635 account.Path, mailbox, Convert.ToInt32 (id, 16)));
636 break;
637 case AccountType.Imap:
638 uri = new Uri (String.Format ("imap://{0}:{1}/fetch%3EUID%3E{2}%3E{3}",
639 account.Server, account.Port, mailbox, Convert.ToInt32 (id, 16)));
640 break;
641 case AccountType.AddressBook:
642 uri = new Uri (String.Format ("abook://{0}?id={1}", mailbox, id));
643 break;
644 case AccountType.Nntp:
645 uri = new Uri (String.Format ("news://{0}:{1}/{2}?number={3}" ,
646 account.Server, account.Port.ToString(), mailbox, id));
647 break;
648 case AccountType.Invalid:
649 break;
652 return uri;
655 public static long GetFileSize (string filename)
657 long filesize = -1;
659 try {
660 FileInfo file = new FileInfo (filename);
661 filesize = file.Length;
662 } catch { }
664 return filesize;
667 public static string GetFullyIndexableFile (string mork_file)
669 string mailbox_file = Path.Combine (
670 Path.GetDirectoryName (mork_file),
671 Path.GetFileNameWithoutExtension (mork_file));
673 return mailbox_file;
676 // a generic way to determine where thunderbird is storing it's files
677 public static string GetRootPath ()
679 foreach (string dir in Directory.GetDirectories (PathFinder.HomeDir, ".*thunderbird*")) {
680 if (File.Exists (Path.Combine (dir, "profiles.ini")))
681 return dir;
684 return null;
687 public static string[] GetProfilePaths (string root)
689 string line;
690 StreamReader reader;
691 ArrayList profiles = new ArrayList ();
693 try {
694 reader = new StreamReader (Path.Combine (root, "profiles.ini"));
695 } catch {
696 return (string[]) profiles.ToArray ();
699 // Read the profile path
700 while ((line = reader.ReadLine ()) != null) {
701 if (line.StartsWith ("Path=")) {
702 profiles.Add (String.Format ("{0}/{1}", root, line.Substring (5)));
703 continue;
707 return (string[]) profiles.ToArray (typeof (string));
710 public static string GetRelativePath (string mork_file)
712 string path = null;
713 foreach (string root in Thunderbird.GetProfilePaths (Thunderbird.GetRootPath ())) {
714 if (!mork_file.StartsWith (root))
715 continue;
717 path = mork_file.Substring (root.Length+1);
718 break;
721 return path;
724 public static ArrayList ReadAccounts (string profile_dir)
726 string line = null;
727 Queue accounts = new Queue();
728 Hashtable tbl = new Hashtable ();
729 ArrayList account_list = new ArrayList ();
730 StreamReader reader;
731 Regex id_reg = new Regex (@"account.account(?<id>\d).server");
732 Regex reg = new Regex (@"user_pref\(""mail\.(?<key>.*)""\s*,\s*(""(?<value>.*)"" | (?<value>.*))\);",
733 RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
735 try {
736 reader = new StreamReader (Path.Combine (profile_dir, "prefs.js"));
737 } catch (Exception e) {
738 if (Debug)
739 Logger.Log.Debug (e, "Failed to open file {0}:", Path.Combine (profile_dir , "prefs.js"));
741 return account_list;
744 while ((line = reader.ReadLine()) != null) {
745 if (!line.StartsWith ("user_pref(\"mail."))
746 continue;
748 try {
749 string key = reg.Match (line).Result ("${key}");
751 if (key.StartsWith ("account.account")) {
752 if (Debug)
753 Logger.Log.Debug ("account.account: {0}", id_reg.Match (key).Result ("${id}"));
755 accounts.Enqueue (id_reg.Match (key).Result ("${id}"));
758 tbl [key] = reg.Match (line).Result ("${value}");
759 } catch (Exception e) {
760 if (Debug)
761 Logger.Log.Debug (e, "ReadAccounts 1:");
765 if (Debug)
766 Logger.Log.Info ("ReadAccounts: {0} accounts", accounts.Count);
768 while (accounts.Count > 0) {
769 string id = "server.server" + (accounts.Dequeue() as string);
770 AccountType type = ParseAccountType ((string) tbl [id + ".type"]);
771 char delimiter = GetDelimiter ((string) tbl [id + ".namespace.personal"],
772 (string) tbl [id + ".namespace.public"], (string) tbl [id + ".namespace.other_users"]);
774 if (type == AccountType.Invalid)
775 continue;
777 if (Debug)
778 Logger.Log.Debug ("ReadAccounts 2: {0}", id);
780 try {
781 account_list.Add (new Account (
782 String.Format ("{0}@{1}", (string) tbl [id + ".userName"], (string) tbl [id + ".hostname"]),
783 (string) tbl [id + ".directory"], Convert.ToInt32 ((string) tbl [id + ".port"]), type, delimiter));
784 } catch (Exception e) {
785 if (Debug)
786 Logger.Log.Debug (e, "ReadAccounts 3:");
787 continue;
791 // In case the address book file exists, add it as well
792 if (File.Exists (Path.Combine (profile_dir, "abook.mab"))) {
793 account_list.Add (new Account (Path.GetFileName (profile_dir),
794 Path.Combine (profile_dir, "abook.mab"), 0, AccountType.AddressBook, ' '));
797 return account_list;
800 public static bool IsMorkFile (string path, string filename)
802 string full_path = Path.Combine (path, filename);
804 if (Path.GetExtension (filename) == ".msf" && File.Exists (full_path))
805 return true;
807 return false;
810 public static bool IsFullyIndexable (string mork_file)
812 try {
813 FileInfo file_info = new FileInfo (GetFullyIndexableFile (mork_file));
814 if (file_info.Length > 0)
815 return true;
816 } catch {}
818 return false;
821 public static string ConstructMailboxString (string mork_file, Account account)
823 string mailbox = null;
825 switch (account.Type) {
826 case AccountType.Pop3:
827 case AccountType.Rss:
828 case AccountType.MoveMail:
829 mailbox = GetFullyIndexableFile (mork_file.Substring (account.Path.Length+1));
830 break;
831 case AccountType.Imap:
832 mailbox = String.Format ("{0}{1}",
833 account.Delimiter,
834 GetFullyIndexableFile (mork_file.Substring (account.Path.Length+1).Replace (".sbd/", Convert.ToString (account.Delimiter))));
835 break;
836 case AccountType.AddressBook:
837 mailbox = mork_file;
838 break;
839 case AccountType.Nntp:
840 // Doesn't really matter what this is as long as it's unique (at least until I've figure the uri schemes)
841 mailbox = account.Server;
842 break;
843 case AccountType.Invalid:
844 mailbox = String.Format ("InvalidMailbox-{0}", mork_file);
845 break;
848 return mailbox;