Added FilterDeb from Kevin Kubasik.
[beagle.git] / Filters / FilterMail.cs
blob10073c67c90e263798730033e3c66b036b13c762
2 //
3 // FilterMail.cs
4 //
5 // Copyright (C) 2004-2005 Novell, Inc.
6 //
7 //
8 //
9 // Permission is hereby granted, free of charge, to any person obtaining a
10 // copy of this software and associated documentation files (the "Software"),
11 // to deal in the Software without restriction, including without limitation
12 // the rights to use, copy, modify, merge, publish, distribute, sublicense,
13 // and/or sell copies of the Software, and to permit persons to whom the
14 // Software is furnished to do so, subject to the following conditions:
16 // The above copyright notice and this permission notice shall be included in
17 // all copies or substantial portions of the Software.
19 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
25 // DEALINGS IN THE SOFTWARE.
28 using System;
29 using System.Collections;
30 using System.IO;
32 using GMime;
34 using Beagle;
35 using Beagle.Daemon;
36 using Beagle.Util;
38 namespace Beagle.Filters {
40 public class FilterMail : Beagle.Daemon.Filter, IDisposable {
42 private static bool gmime_initialized = false;
44 private GMime.Message message;
45 private PartHandler handler;
47 public FilterMail ()
49 // 1: Make email addresses non-keyword, add sanitized version
50 // for eaching for parts of an email address.
51 SetVersion (1);
53 AddSupportedFlavor (FilterFlavor.NewFromMimeType ("message/rfc822"));
56 protected override void DoOpen (FileInfo info)
58 if (!gmime_initialized) {
59 try {
60 GMime.Global.Init ();
61 gmime_initialized = true;
62 } catch {
63 Error ();
64 return;
68 int mail_fd = Mono.Unix.Native.Syscall.open (info.FullName, Mono.Unix.Native.OpenFlags.O_RDONLY);
70 if (mail_fd == -1)
71 throw new IOException (String.Format ("Unable to read {0} for parsing mail", info.FullName));
73 GMime.StreamFs stream = new GMime.StreamFs (mail_fd);
74 GMime.Parser parser = new GMime.Parser (stream);
75 this.message = parser.ConstructMessage ();
76 stream.Dispose ();
77 parser.Dispose ();
79 if (this.message == null)
80 Error ();
83 private bool HasAttachments (GMime.Object mime_part)
85 if (mime_part is GMime.MessagePart)
86 return true;
88 // Messages that are multipart/alternative shouldn't be considered as having
89 // attachments. Unless of course they do.
90 if (mime_part is GMime.Multipart && mime_part.ContentType.Subtype.ToLower () != "alternative")
91 return true;
93 return false;
96 protected override void DoPullProperties ()
98 string subject = GMime.Utils.HeaderDecodePhrase (this.message.Subject);
99 AddProperty (Property.New ("dc:title", subject));
101 AddProperty (Property.NewDate ("fixme:date", message.Date.ToUniversalTime ()));
103 GMime.InternetAddressList addrs;
104 addrs = this.message.GetRecipients (GMime.Message.RecipientType.To);
105 foreach (GMime.InternetAddress ia in addrs) {
106 AddProperty (Property.NewUnsearched ("fixme:to", ia.ToString (false)));
107 if (ia.AddressType != GMime.InternetAddressType.Group) {
108 AddProperty (Property.New ("fixme:to_address", ia.Addr));
109 AddProperty (Property.NewUnstored ("fixme:to_sanitized", StringFu.SanitizeEmail (ia.Addr)));
111 AddProperty (Property.New ("fixme:to_name", ia.Name));
113 addrs.Dispose ();
115 addrs = this.message.GetRecipients (GMime.Message.RecipientType.Cc);
116 foreach (GMime.InternetAddress ia in addrs) {
117 AddProperty (Property.NewUnsearched ("fixme:cc", ia.ToString (false)));
118 if (ia.AddressType != GMime.InternetAddressType.Group) {
119 AddProperty (Property.New ("fixme:cc_address", ia.Addr));
120 AddProperty (Property.NewUnstored ("fixme:cc_sanitized", StringFu.SanitizeEmail (ia.Addr)));
122 AddProperty (Property.New ("fixme:cc_name", ia.Name));
124 addrs.Dispose ();
126 addrs = GMime.InternetAddressList.ParseString (GMime.Utils.HeaderDecodePhrase (this.message.Sender));
127 foreach (GMime.InternetAddress ia in addrs) {
128 AddProperty (Property.NewUnsearched ("fixme:from", ia.ToString (false)));
129 if (ia.AddressType != GMime.InternetAddressType.Group) {
130 AddProperty (Property.New ("fixme:from_address", ia.Addr));
131 AddProperty (Property.NewUnstored ("fixme:from_sanitized", StringFu.SanitizeEmail (ia.Addr)));
133 AddProperty (Property.New ("fixme:from_name", ia.Name));
135 addrs.Dispose ();
137 if (HasAttachments (this.message.MimePart))
138 AddProperty (Property.NewFlag ("fixme:hasAttachments"));
140 // Store the message ID and references are unsearched
141 // properties. They will be used to generate
142 // conversations in the frontend.
143 string msgid = this.message.GetHeader ("Message-Id");
144 if (msgid != null)
145 AddProperty (Property.NewUnsearched ("fixme:msgid", GMime.Utils.DecodeMessageId (msgid)));
147 foreach (GMime.References refs in this.message.References)
148 AddProperty (Property.NewUnsearched ("fixme:reference", refs.Msgid));
150 string list_id = this.message.GetHeader ("List-Id");
151 if (list_id != null) {
152 // FIXME: Might need some additional parsing.
153 AddProperty (Property.NewKeyword ("fixme:mlist", GMime.Utils.HeaderDecodePhrase (list_id)));
156 // KMail can store replies in the same folder
157 // Use issent flag to distinguish between incoming
158 // and outgoing message
159 string kmail_msg_sent = this.message.GetHeader ("X-KMail-Link-Type");
160 bool issent_is_set = false;
161 foreach (Property property in IndexableProperties) {
162 if (property.Key == "fixme:isSent") {
163 issent_is_set = true;
164 break;
167 if (!issent_is_set && kmail_msg_sent != null && kmail_msg_sent == "reply")
168 AddProperty (Property.NewFlag ("fixme:isSent"));
171 protected override void DoPullSetup ()
173 this.handler = new PartHandler (this);
174 using (GMime.Object mime_part = this.message.MimePart)
175 this.handler.OnEachPart (mime_part);
177 AddChildIndexables (this.handler.ChildIndexables);
180 protected override void DoPull ()
182 if (handler.Reader == null) {
183 Finished ();
184 return;
187 string l = handler.Reader.ReadLine ();
189 if (l != null)
190 AppendText (l);
191 else
192 Finished ();
195 protected override void DoClose ()
197 Dispose ();
200 public void Dispose ()
202 if (this.handler != null && this.handler.Reader != null)
203 this.handler.Reader.Close ();
204 this.handler = null;
206 if (this.message != null) {
207 this.message.Dispose ();
208 this.message = null;
212 private class PartHandler {
213 private Beagle.Daemon.Filter filter;
214 private int count = 0; // parts handled so far
215 private int depth = 0; // part recursion depth
216 private ArrayList child_indexables = new ArrayList ();
217 private TextReader reader;
219 public PartHandler (Beagle.Daemon.Filter filter)
221 this.filter = filter;
224 private bool IsMimeTypeHandled (string mime_type)
226 foreach (FilterFlavor flavor in FilterFlavor.Flavors) {
227 if (flavor.IsMatch (null, null, mime_type.ToLower ()))
228 return true;
231 return false;
234 public void OnEachPart (GMime.Object mime_part)
236 GMime.Object part = null;
237 bool part_needs_dispose = false;
239 //for (int i = 0; i < this.depth; i++)
240 // Console.Write (" ");
241 //Console.WriteLine ("Content-Type: {0}", mime_part.ContentType);
243 ++depth;
245 if (mime_part is GMime.MessagePart) {
246 GMime.MessagePart msg_part = (GMime.MessagePart) mime_part;
248 using (GMime.Message message = msg_part.Message) {
249 using (GMime.Object subpart = message.MimePart)
250 this.OnEachPart (subpart);
252 } else if (mime_part is GMime.Multipart) {
253 GMime.Multipart multipart = (GMime.Multipart) mime_part;
255 int num_parts = multipart.Number;
257 // If the mimetype is multipart/alternative, we only want to index
258 // one part -- the richest one we can filter.
259 if (mime_part.ContentType.Subtype.ToLower () == "alternative") {
260 // The richest formats are at the end, so work from there
261 // backward.
262 for (int i = num_parts - 1; i >= 0; i--) {
263 GMime.Object subpart = multipart.GetPart (i);
265 if (IsMimeTypeHandled (subpart.ContentType.ToString ())) {
266 part = subpart;
267 part_needs_dispose = true;
268 break;
269 } else {
270 subpart.Dispose ();
275 // If it's not alternative, or we don't know how to filter any of
276 // the parts, treat them like a bunch of attachments.
277 if (part == null) {
278 for (int i = 0; i < num_parts; i++) {
279 using (GMime.Object subpart = multipart.GetPart (i))
280 this.OnEachPart (subpart);
283 } else if (mime_part is GMime.Part)
284 part = mime_part;
285 else
286 throw new Exception (String.Format ("Unknown part type: {0}", part.GetType ()));
288 if (part != null) {
289 System.IO.Stream stream = null;
291 using (GMime.DataWrapper content_obj = ((GMime.Part) part).ContentObject)
292 stream = content_obj.Stream;
294 // If this is the only part and it's plain text, we
295 // want to just attach it to our filter instead of
296 // creating a child indexable for it.
297 bool no_child_needed = false;
299 if (this.depth == 1 && this.count == 0) {
300 if (part.ContentType.ToString ().ToLower () == "text/plain") {
301 no_child_needed = true;
303 this.reader = new StreamReader (stream);
307 if (!no_child_needed) {
308 string sub_uri = this.filter.Uri.ToString () + "#" + this.count;
309 Indexable child = new Indexable (new Uri (sub_uri));
311 child.HitType = "MailMessage";
312 child.MimeType = part.ContentType.ToString ();
313 child.CacheContent = false;
315 child.AddProperty (Property.NewKeyword ("fixme:attachment_title", ((GMime.Part)part).Filename));
317 if (part.ContentType.Type.ToLower () == "text")
318 child.SetTextReader (new StreamReader (stream));
319 else
320 child.SetBinaryStream (stream);
322 this.child_indexables.Add (child);
325 this.count++;
328 if (part_needs_dispose)
329 part.Dispose ();
331 --depth;
334 public ICollection ChildIndexables {
335 get { return this.child_indexables; }
338 public TextReader Reader {
339 get { return this.reader; }