Remove debug statements from KCal backends.
[beagle.git] / Util / Thunderbird.cs
blob0cf4dc0c3149d0cc542d3889f7feeb146b977b7d
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 class AccountReader {
531 private string profile_dir;
532 private ArrayList accounts;
533 private Hashtable content;
534 private bool success = false;
536 public AccountReader (string profile_dir)
538 this.profile_dir = profile_dir;
539 this.accounts = new ArrayList ();
540 this.content = new Hashtable ();
542 Read ();
544 // In case the address book file exists, add it as well
545 if (File.Exists (Path.Combine (profile_dir, "abook.mab"))) {
546 accounts.Add (new Account (
547 "abook.mab",
548 Path.GetFullPath (Path.Combine (profile_dir, "abook.mab")),
550 AccountType.AddressBook,
551 ' '));
555 public void Read ()
557 StreamReader reader = new StreamReader (Path.Combine (profile_dir, "prefs.js"));
558 Regex reg = new Regex (@"user_pref\(""mail\.(?<key>.*)""\s*,\s*(""(?<value>.*)"" | (?<value>.*))\);",
559 RegexOptions.Compiled | RegexOptions.IgnorePatternWhitespace);
561 foreach (Match m in reg.Matches (reader.ReadToEnd ()))
562 content [m.Result ("${key}")] = m.Result ("${value}");
564 foreach (string key in content.Keys) {
565 Match m = Regex.Match (key, @"account.account(?<id>\d).server");
567 if (!m.Success)
568 continue;
570 try {
571 AddAccount (m.Result ("${id}"));
572 } catch (Exception e) {
573 Console.WriteLine ("Failed to add: {0}", e);
578 private void AddAccount (string id)
580 char delimiter;
581 AccountType type = ParseAccountType (GetValue (id, "type"));
583 if (type == AccountType.Invalid)
584 return;
586 delimiter = GetDelimiter (
587 GetValue(id, "namespace.personal"),
588 GetValue (id, "namespace.public"),
589 GetValue (id, "namespace.other_users"));
591 accounts.Add (new Account (
592 String.Format ("{0}@{1}", GetValue (id, "userName"), GetValue (id, "hostname")),
593 GetValue (id, "directory"),
594 Convert.ToInt32 (GetValue (id, "port")),
595 type,
596 delimiter));
599 private string GetValue (string id, string key)
601 return (string) content [String.Format ("server.server{0}.{1}", id, key)];
604 public IEnumerator GetEnumerator ()
606 return accounts.GetEnumerator ();
610 /////////////////////////////////////////////////////////////////////////////////////
612 public static string ExecutableName {
613 get {
614 bool is_mt = false;
615 string exec_name = "thunderbird";
617 foreach (string s in PathFinder.Paths) {
618 if (File.Exists (Path.Combine (s, "mozilla-thunderbird"))) {
619 exec_name = "mozilla-thunderbird";
620 break;
624 return exec_name;
628 /////////////////////////////////////////////////////////////////////////////////////
630 public static string HexDateToString (string hex)
632 DateTime time = new DateTime (1970,1,1,0,0,0);
634 try {
635 time = time.AddSeconds (
636 Int32.Parse (hex, NumberStyles.HexNumber));
637 } catch {}
639 return time.ToString ();
642 public static int Hex2Dec (string hex)
644 int dec = -1;
646 try {
647 dec = Convert.ToInt32 (hex, 16);
648 } catch { }
650 return dec;
653 public static int ParsePort (AccountType type)
655 int port = 0;
657 switch (type) {
658 case AccountType.Pop3:
659 port = 110;
660 break;
661 case AccountType.Imap:
662 port = 143;
663 break;
666 return port;
669 public static AccountType ParseAccountType (string type_str)
671 AccountType type;
673 try {
674 type = (AccountType) Enum.Parse (typeof (AccountType), type_str, true);
675 } catch {
676 if (type_str.ToLower ().Equals ("none"))
677 type = AccountType.Pop3;
678 else
679 type = AccountType.Invalid;
682 return type;
685 // A hack to extract a potential delimiter from a namespace-string
686 public static char GetDelimiter (params string[] namespace_str)
688 MatchCollection matches = null;
689 Regex reg = new Regex (@"\\\""(.*)(?<delimiter>[^,])\\\""", RegexOptions.Compiled);
691 if (namespace_str == null)
692 return char.MinValue;
694 foreach (string str in namespace_str) {
695 try {
696 matches = reg.Matches (str);
697 } catch {
698 continue;
701 foreach (Match m in matches) {
702 char delim = Convert.ToChar (m.Result ("${delimiter}"));
703 if (delim != ' ')
704 return delim;
708 return char.MinValue;
711 public static Uri NewUri (Account account, string mailbox, string id)
713 Uri uri = null;
715 switch (account.Type) {
716 case AccountType.Pop3:
717 case AccountType.MoveMail:
718 case AccountType.Rss: // rss, movemail and pop3 share the same uri scheme
719 uri = new Uri (String.Format ("mailbox://{0}/{1}?number={2}",
720 account.Path, mailbox, Convert.ToInt32 (id, 16)));
721 break;
722 case AccountType.Imap:
723 uri = new Uri (String.Format ("imap://{0}:{1}/fetch%3EUID%3E{2}%3E{3}",
724 account.Server, account.Port, mailbox, Convert.ToInt32 (id, 16)));
725 break;
726 case AccountType.AddressBook:
727 uri = new Uri (String.Format ("abook://{0}?id={1}", mailbox, id));
728 break;
729 case AccountType.Nntp:
730 uri = new Uri (String.Format ("news://{0}:{1}/{2}?number={3}" ,
731 account.Server, account.Port.ToString(), mailbox, id));
732 break;
733 case AccountType.Invalid:
734 break;
737 return uri;
740 public static long GetFileSize (string filename)
742 long filesize = -1;
744 try {
745 FileInfo file = new FileInfo (filename);
746 filesize = file.Length;
747 } catch { }
749 return filesize;
752 public static string GetFullyIndexableFile (string mork_file)
754 string mailbox_file = Path.Combine (
755 Path.GetDirectoryName (mork_file),
756 Path.GetFileNameWithoutExtension (mork_file));
758 return mailbox_file;
761 // a generic way to determine where thunderbird is storing it's files
762 public static string GetRootPath ()
764 foreach (string dir in Directory.GetDirectories (PathFinder.HomeDir, ".*thunderbird*")) {
765 if (File.Exists (Path.Combine (dir, "profiles.ini")))
766 return dir;
769 return null;
772 public static string[] GetProfilePaths (string root)
774 string line;
775 StreamReader reader;
776 ArrayList profiles = new ArrayList ();
778 try {
779 reader = new StreamReader (Path.Combine (root, "profiles.ini"));
780 } catch {
781 return (string[]) profiles.ToArray ();
784 // Read the profile path
785 while ((line = reader.ReadLine ()) != null) {
786 if (line.StartsWith ("Path=")) {
787 profiles.Add (String.Format ("{0}/{1}", root, line.Substring (5)));
788 continue;
792 return (string[]) profiles.ToArray (typeof (string));
795 public static string GetRelativePath (string mork_file)
797 string path = null;
798 AccountReader reader = null;
800 foreach (string root in Thunderbird.GetProfilePaths (Thunderbird.GetRootPath ())) {
801 try {
802 reader = new AccountReader (root);
804 foreach (Account account in reader) {
805 if (!mork_file.StartsWith (account.Path))
806 continue;
808 path = String.Format ("{0}/{1}",
809 account.Server, mork_file.Substring (account.Path.Length+1));
810 break;
812 } catch {
813 continue;
817 return path;
820 public static bool IsMorkFile (string path, string filename)
822 string full_path = Path.Combine (path, filename);
824 if (Path.GetExtension (filename) == ".msf" && File.Exists (full_path))
825 return true;
827 return false;
830 public static bool IsFullyIndexable (string mork_file)
832 try {
833 FileInfo file_info = new FileInfo (GetFullyIndexableFile (mork_file));
834 if (file_info.Length > 0)
835 return true;
836 } catch {}
838 return false;
841 public static string ConstructMailboxString (string mork_file, Account account)
843 string mailbox = null;
845 switch (account.Type) {
846 case AccountType.Pop3:
847 case AccountType.Rss:
848 case AccountType.MoveMail:
849 mailbox = GetFullyIndexableFile (mork_file.Substring (account.Path.Length+1));
850 break;
851 case AccountType.Imap:
852 mailbox = String.Format ("{0}{1}",
853 account.Delimiter,
854 GetFullyIndexableFile (mork_file.Substring (account.Path.Length+1).Replace (".sbd/", Convert.ToString (account.Delimiter))));
855 break;
856 case AccountType.AddressBook:
857 mailbox = mork_file;
858 break;
859 case AccountType.Nntp:
860 // Doesn't really matter what this is as long as it's unique (at least until I've figure the uri schemes)
861 mailbox = account.Server;
862 break;
863 case AccountType.Invalid:
864 mailbox = String.Format ("InvalidMailbox-{0}", mork_file);
865 break;
868 return mailbox;