2 // Thunderbird.cs: A utility class with methods and classes that might be needed to parse Thunderbird data
4 // Copyright (C) 2006 Pierre Östlund
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
30 using System
.Collections
;
31 using System
.Globalization
;
32 using System
.Text
.RegularExpressions
;
39 namespace Beagle
.Util
{
41 public class Thunderbird
{
43 public static bool Debug
= false;
45 /////////////////////////////////////////////////////////////////////////////////////
47 public enum AccountType
{
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
;
70 this.server_port
= port
;
71 this.account_type
= type
;
72 this.delimiter
= delim
;
75 public string Server
{
76 get { return server_string; }
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
)
116 if (!data
.ContainsKey (key
))
119 return Convert
.ToInt32 (data
[key
]);
125 public bool GetBool (string key
)
128 return Convert
.ToBoolean (data
[key
]);
134 public object GetObject (string key
)
139 public void SetObject (string key
, object value)
145 public System
.Uri Uri
{
150 public Account Account
{
151 get { return account; }
155 /////////////////////////////////////////////////////////////////////////////////////
158 // id, sender, subject, recipients, date, mailbox
160 // size, msgOffset, offlineMsgSize
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
) {
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 ();
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);
224 if (data
.ContainsKey ("references")) {
225 foreach (Match m
in Regex
.Matches ((data
["references"] as string), @"\<(?>[^\<\>]+)\>"))
226 message
.AddHeader ("References", m
.Value
);
232 private GMime
.Message
FullMessage ()
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
))
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 ();
257 int msg_offset
= GetInt ("msgOffset");
258 return (msg_offset
>= 0 ? msg_offset
: Thunderbird
.Hex2Dec (GetString ("id")));
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 /////////////////////////////////////////////////////////////////////////////////////
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
285 public class Contact
: StorageBase
{
286 private string workfile
;
288 public Contact (Account account
, Hashtable data
, string workfile
)
290 this.account
= account
;
292 this.workfile
= workfile
;
293 this.uri
= NewUri (account
, Thunderbird
.ConstructMailboxString (workfile
, account
), GetString ("id"));
296 public string Workfile
{
297 get { return workfile; }
302 /////////////////////////////////////////////////////////////////////////////////////
305 // id, subject, sender, date, message-id
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
) {
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
))
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>"));
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 /////////////////////////////////////////////////////////////////////////////////////
373 // id, subject, sender, date
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
) {
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
;
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
;
422 private IEnumerator current
= null;
424 public Database (Account account
, string file
)
426 this.account
= account
;
432 db
= new MorkDatabase (file
);
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";
443 case AccountType
.AddressBook
:
444 db
.EnumNamespace
= "ns:addrbk:db:row:scope:card:all";
448 current
= db
.GetEnumerator ();
451 public Account Account
{
452 get { return account; }
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
{
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
)
487 this.enumerator
= enumerator
;
488 this.account
= account
;
491 public bool MoveNext ()
493 return (enumerator
!= null ? enumerator
.MoveNext () : false);
501 public object Current
{
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
);
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 ();
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 (
548 Path
.GetFullPath (Path
.Combine (profile_dir
, "abook.mab")),
550 AccountType
.AddressBook
,
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");
571 AddAccount (m
.Result ("${id}"));
572 } catch (Exception e
) {
573 Console
.WriteLine ("Failed to add: {0}", e
);
578 private void AddAccount (string id
)
581 AccountType type
= ParseAccountType (GetValue (id
, "type"));
583 if (type
== AccountType
.Invalid
)
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")),
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
{
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";
628 /////////////////////////////////////////////////////////////////////////////////////
630 public static string HexDateToString (string hex
)
632 DateTime time
= new DateTime (1970,1,1,0,0,0);
635 time
= time
.AddSeconds (
636 Int32
.Parse (hex
, NumberStyles
.HexNumber
));
639 return time
.ToString ();
642 public static int Hex2Dec (string hex
)
647 dec
= Convert
.ToInt32 (hex
, 16);
653 public static int ParsePort (AccountType type
)
658 case AccountType
.Pop3
:
661 case AccountType
.Imap
:
669 public static AccountType
ParseAccountType (string type_str
)
674 type
= (AccountType
) Enum
.Parse (typeof (AccountType
), type_str
, true);
676 if (type_str
.ToLower ().Equals ("none"))
677 type
= AccountType
.Pop3
;
679 type
= AccountType
.Invalid
;
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) {
696 matches = reg.Matches (str);
701 foreach (Match m in matches) {
702 char delim = Convert.ToChar (m.Result ("${delimiter}
"));
708 return char.MinValue;
711 public static Uri NewUri (Account account, string mailbox, string id)
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)));
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)));
726 case AccountType
.AddressBook
:
727 uri
= new Uri (String
.Format ("abook://{0}?id={1}", mailbox
, id
));
729 case AccountType
.Nntp
:
730 uri
= new Uri (String
.Format ("news://{0}:{1}/{2}?number={3}" ,
731 account
.Server
, account
.Port
.ToString(), mailbox
, id
));
733 case AccountType
.Invalid
:
740 public static long GetFileSize (string filename
)
745 FileInfo file
= new FileInfo (filename
);
746 filesize
= file
.Length
;
752 public static string GetFullyIndexableFile (string mork_file
)
754 string mailbox_file
= Path
.Combine (
755 Path
.GetDirectoryName (mork_file
),
756 Path
.GetFileNameWithoutExtension (mork_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")))
772 public static string[] GetProfilePaths (string root
)
776 ArrayList profiles
= new ArrayList ();
779 reader
= new StreamReader (Path
.Combine (root
, "profiles.ini"));
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)));
792 return (string[]) profiles
.ToArray (typeof (string));
795 public static string GetRelativePath (string mork_file
)
798 AccountReader reader
= null;
800 foreach (string root
in Thunderbird
.GetProfilePaths (Thunderbird
.GetRootPath ())) {
802 reader
= new AccountReader (root
);
804 foreach (Account account
in reader
) {
805 if (!mork_file
.StartsWith (account
.Path
))
808 path
= String
.Format ("{0}/{1}",
809 account
.Server
, mork_file
.Substring (account
.Path
.Length
+1));
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
))
830 public static bool IsFullyIndexable (string mork_file
)
833 FileInfo file_info
= new FileInfo (GetFullyIndexableFile (mork_file
));
834 if (file_info
.Length
> 0)
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));
851 case AccountType
.Imap
:
852 mailbox
= String
.Format ("{0}{1}",
854 GetFullyIndexableFile (mork_file
.Substring (account
.Path
.Length
+1).Replace (".sbd/", Convert
.ToString (account
.Delimiter
))));
856 case AccountType
.AddressBook
:
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
;
863 case AccountType
.Invalid
:
864 mailbox
= String
.Format ("InvalidMailbox-{0}", mork_file
);