Yet another. Init the gobject type system.
[beagle.git] / Util / Thunderbird.cs
blob3e6221c21ca545020fde13eaff1eb3480ee0b130
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 HexDateToString (string hex)
532 DateTime time = new DateTime (1970,1,1,0,0,0);
534 try {
535 time = time.AddSeconds (
536 Int32.Parse (hex, NumberStyles.HexNumber));
537 } catch {}
539 return time.ToString ();
542 public static int Hex2Dec (string hex)
544 int dec = -1;
546 try {
547 dec = Convert.ToInt32 (hex, 16);
548 } catch { }
550 return dec;
553 public static int ParsePort (AccountType type)
555 int port = 0;
557 switch (type) {
558 case AccountType.Pop3:
559 port = 110;
560 break;
561 case AccountType.Imap:
562 port = 143;
563 break;
566 return port;
569 public static AccountType ParseAccountType (string type_str)
571 AccountType type;
573 try {
574 type = (AccountType) Enum.Parse (typeof (AccountType), type_str, true);
575 } catch {
576 type = AccountType.Invalid;
579 return type;
582 // A hack to extract a potential delimiter from a namespace-string
583 public static char GetDelimiter (params string[] namespace_str)
585 MatchCollection matches = null;
586 Regex reg = new Regex (@"\\\""(.*)(?<delimiter>[^,])\\\""", RegexOptions.Compiled);
588 if (namespace_str == null)
589 return char.MinValue;
591 foreach (string str in namespace_str) {
592 try {
593 matches = reg.Matches (str);
594 } catch {
595 continue;
598 foreach (Match m in matches) {
599 char delim = Convert.ToChar (m.Result ("${delimiter}"));
600 if (delim != ' ')
601 return delim;
605 return char.MinValue;
608 public static Uri NewUri (Account account, string mailbox, string id)
610 Uri uri = null;
612 switch (account.Type) {
613 case AccountType.Pop3:
614 case AccountType.MoveMail:
615 case AccountType.Rss: // rss, movemail and pop3 share the same uri scheme
616 uri = new Uri (String.Format ("mailbox://{0}/{1}?number={2}",
617 account.Path, mailbox, Convert.ToInt32 (id, 16)));
618 break;
619 case AccountType.Imap:
620 uri = new Uri (String.Format ("imap://{0}:{1}/fetch%3EUID%3E{2}%3E{3}",
621 account.Server, account.Port, mailbox, Convert.ToInt32 (id, 16)));
622 break;
623 case AccountType.AddressBook:
624 uri = new Uri (String.Format ("abook://{0}?id={1}", mailbox, id));
625 break;
626 case AccountType.Nntp:
627 uri = new Uri (String.Format ("news://{0}:{1}/{2}?number={3}" ,
628 account.Server, account.Port.ToString(), mailbox, id));
629 break;
630 case AccountType.Invalid:
631 break;
634 return uri;
637 public static long GetFileSize (string filename)
639 long filesize = -1;
641 try {
642 FileInfo file = new FileInfo (filename);
643 filesize = file.Length;
644 } catch { }
646 return filesize;
649 public static string GetFullyIndexableFile (string mork_file)
651 string mailbox_file = Path.Combine (
652 Path.GetDirectoryName (mork_file),
653 Path.GetFileNameWithoutExtension (mork_file));
655 return mailbox_file;
658 // a generic way to determine where thunderbird is storing it's files
659 public static string GetRootPath ()
661 foreach (string dir in Directory.GetDirectories (PathFinder.HomeDir, ".*thunderbird*")) {
662 if (File.Exists (Path.Combine (dir, "profiles.ini")))
663 return dir;
666 return null;
669 public static string[] GetProfilePaths (string root)
671 string line;
672 StreamReader reader;
673 ArrayList profiles = new ArrayList ();
675 try {
676 reader = new StreamReader (Path.Combine (root, "profiles.ini"));
677 } catch {
678 return (string[]) profiles.ToArray ();
681 // Read the profile path
682 while ((line = reader.ReadLine ()) != null) {
683 if (line.StartsWith ("Path=")) {
684 profiles.Add (String.Format ("{0}/{1}", root, line.Substring (5)));
685 continue;
689 return (string[]) profiles.ToArray (typeof (string));
692 public static string GetRelativePath (string mork_file)
694 string path = null;
695 foreach (string root in Thunderbird.GetProfilePaths (Thunderbird.GetRootPath ())) {
696 if (!mork_file.StartsWith (root))
697 continue;
699 path = mork_file.Substring (root.Length+1);
700 break;
703 return path;
706 public static ArrayList ReadAccounts (string profile_dir)
708 string line = null;
709 Queue accounts = new Queue();
710 Hashtable tbl = new Hashtable ();
711 ArrayList account_list = new ArrayList ();
712 StreamReader reader;
713 Regex id_reg = new Regex (@"account.account(?<id>\d).server");
714 Regex reg = new Regex (@"user_pref\(""mail\.(?<key>.*)""\s*,\s*(""(?<value>.*)"" | (?<value>.*))\);",
715 RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
717 try {
718 reader = new StreamReader (Path.Combine (profile_dir, "prefs.js"));
719 } catch (Exception e) {
720 if (Debug)
721 Logger.Log.Debug ("Failed to open file {0}: {1}", Path.Combine (profile_dir , "prefs.js"), e.Message);
723 return account_list;
726 while ((line = reader.ReadLine()) != null) {
727 if (!line.StartsWith ("user_pref(\"mail."))
728 continue;
730 try {
731 string key = reg.Match (line).Result ("${key}");
733 if (key.StartsWith ("account.account")) {
734 if (Debug)
735 Logger.Log.Debug ("account.account: {0}", id_reg.Match (key).Result ("${id}"));
737 accounts.Enqueue (id_reg.Match (key).Result ("${id}"));
740 tbl [key] = reg.Match (line).Result ("${value}");
741 } catch (Exception e) {
742 if (Debug)
743 Logger.Log.Debug ("ReadAccounts 1: {0}", e.Message);
747 if (Debug)
748 Logger.Log.Info ("ReadAccounts: {0} accounts", accounts.Count);
750 while (accounts.Count > 0) {
751 string id = "server.server" + (accounts.Dequeue() as string);
752 AccountType type = ParseAccountType ((string) tbl [id + ".type"]);
753 char delimiter = GetDelimiter ((string) tbl [id + ".namespace.personal"],
754 (string) tbl [id + ".namespace.public"], (string) tbl [id + ".namespace.other_users"]);
756 if (type == AccountType.Invalid)
757 continue;
759 if (Debug)
760 Logger.Log.Debug ("ReadAccounts 2: {0}", id);
762 try {
763 account_list.Add (new Account (
764 String.Format ("{0}@{1}", (string) tbl [id + ".userName"], (string) tbl [id + ".hostname"]),
765 (string) tbl [id + ".directory"], Convert.ToInt32 ((string) tbl [id + ".port"]), type, delimiter));
766 } catch (Exception e) {
767 if (Debug)
768 Logger.Log.Debug ("ReadAccounts 3: {0}", e.Message);
769 continue;
773 // In case the address book file exists, add it as well
774 if (File.Exists (Path.Combine (profile_dir, "abook.mab"))) {
775 account_list.Add (new Account (Path.GetFileName (profile_dir),
776 Path.Combine (profile_dir, "abook.mab"), 0, AccountType.AddressBook, ' '));
779 return account_list;
782 public static bool IsMorkFile (string path, string filename)
784 string full_path = Path.Combine (path, filename);
786 if (Path.GetExtension (filename) == ".msf" && File.Exists (full_path))
787 return true;
789 return false;
792 public static bool IsFullyIndexable (string mork_file)
794 try {
795 FileInfo file_info = new FileInfo (GetFullyIndexableFile (mork_file));
796 if (file_info.Length > 0)
797 return true;
798 } catch {}
800 return false;
803 public static string ConstructMailboxString (string mork_file, Account account)
805 string mailbox = null;
807 switch (account.Type) {
808 case AccountType.Pop3:
809 case AccountType.Rss:
810 case AccountType.MoveMail:
811 mailbox = GetFullyIndexableFile (mork_file.Substring (account.Path.Length+1));
812 break;
813 case AccountType.Imap:
814 mailbox = String.Format ("{0}{1}",
815 account.Delimiter,
816 GetFullyIndexableFile (mork_file.Substring (account.Path.Length+1).Replace (".sbd/", Convert.ToString (account.Delimiter))));
817 break;
818 case AccountType.AddressBook:
819 mailbox = mork_file;
820 break;
821 case AccountType.Nntp:
822 // Doesn't really matter what this is as long as it's unique (at least until I've figure the uri schemes)
823 mailbox = account.Server;
824 break;
825 case AccountType.Invalid:
826 mailbox = String.Format ("InvalidMailbox-{0}", mork_file);
827 break;
830 return mailbox;