2006-09-10 Francisco Javier F. Serrador <serrador@openshine.com>
[beagle.git] / Filters / FilterMail.cs
blobf03b44abcaec0b4182c970ef86b04d6636899976
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 [PropertyKeywordMapping (Keyword="mailfrom", PropertyName="fixme:from_name", IsKeyword=false)]
41 [PropertyKeywordMapping (Keyword="mailfromaddr", PropertyName="fixme:from_address", IsKeyword=false)]
42 [PropertyKeywordMapping (Keyword="mailto", PropertyName="fixme:to_name", IsKeyword=false)]
43 [PropertyKeywordMapping (Keyword="mailtoaddr", PropertyName="fixme:to_address", IsKeyword=false)]
44 [PropertyKeywordMapping (Keyword="mailinglist", PropertyName="fixme:mlist", IsKeyword=true, Description="Mailing list id")]
45 public class FilterMail : Beagle.Daemon.Filter, IDisposable {
47 private static bool gmime_initialized = false;
49 private GMime.Message message;
50 private PartHandler handler;
52 public FilterMail ()
54 // 1: Make email addresses non-keyword, add sanitized version
55 // for eaching for parts of an email address.
56 SetVersion (1);
58 AddSupportedFlavor (FilterFlavor.NewFromMimeType ("message/rfc822"));
61 protected override void DoOpen (FileInfo info)
63 if (!gmime_initialized) {
64 try {
65 GMime.Global.Init ();
66 gmime_initialized = true;
67 } catch {
68 Error ();
69 return;
73 int mail_fd = Mono.Unix.Native.Syscall.open (info.FullName, Mono.Unix.Native.OpenFlags.O_RDONLY);
75 if (mail_fd == -1)
76 throw new IOException (String.Format ("Unable to read {0} for parsing mail", info.FullName));
78 GMime.StreamFs stream = new GMime.StreamFs (mail_fd);
79 GMime.Parser parser = new GMime.Parser (stream);
80 this.message = parser.ConstructMessage ();
81 stream.Dispose ();
82 parser.Dispose ();
84 if (this.message == null)
85 Error ();
88 private bool HasAttachments (GMime.Object mime_part)
90 if (mime_part is GMime.MessagePart)
91 return true;
93 // Messages that are multipart/alternative shouldn't be considered as having
94 // attachments. Unless of course they do.
95 if (mime_part is GMime.Multipart && mime_part.ContentType.Subtype.ToLower () != "alternative")
96 return true;
98 return false;
101 protected override void DoPullProperties ()
103 string subject = GMime.Utils.HeaderDecodePhrase (this.message.Subject);
104 AddProperty (Property.New ("dc:title", subject));
106 AddProperty (Property.NewDate ("fixme:date", message.Date.ToUniversalTime ()));
108 GMime.InternetAddressList addrs;
109 addrs = this.message.GetRecipients (GMime.Message.RecipientType.To);
110 foreach (GMime.InternetAddress ia in addrs) {
111 AddProperty (Property.NewUnsearched ("fixme:to", ia.ToString (false)));
112 if (ia.AddressType != GMime.InternetAddressType.Group) {
113 AddProperty (Property.New ("fixme:to_address", ia.Addr));
114 AddProperty (Property.NewUnstored ("fixme:to_sanitized", StringFu.SanitizeEmail (ia.Addr)));
116 AddProperty (Property.New ("fixme:to_name", ia.Name));
118 addrs.Dispose ();
120 addrs = this.message.GetRecipients (GMime.Message.RecipientType.Cc);
121 foreach (GMime.InternetAddress ia in addrs) {
122 AddProperty (Property.NewUnsearched ("fixme:cc", ia.ToString (false)));
123 if (ia.AddressType != GMime.InternetAddressType.Group) {
124 AddProperty (Property.New ("fixme:cc_address", ia.Addr));
125 AddProperty (Property.NewUnstored ("fixme:cc_sanitized", StringFu.SanitizeEmail (ia.Addr)));
127 AddProperty (Property.New ("fixme:cc_name", ia.Name));
129 addrs.Dispose ();
131 addrs = GMime.InternetAddressList.ParseString (GMime.Utils.HeaderDecodePhrase (this.message.Sender));
132 foreach (GMime.InternetAddress ia in addrs) {
133 AddProperty (Property.NewUnsearched ("fixme:from", ia.ToString (false)));
134 if (ia.AddressType != GMime.InternetAddressType.Group) {
135 AddProperty (Property.New ("fixme:from_address", ia.Addr));
136 AddProperty (Property.NewUnstored ("fixme:from_sanitized", StringFu.SanitizeEmail (ia.Addr)));
138 AddProperty (Property.New ("fixme:from_name", ia.Name));
140 addrs.Dispose ();
142 if (HasAttachments (this.message.MimePart))
143 AddProperty (Property.NewFlag ("fixme:hasAttachments"));
145 // Store the message ID and references are unsearched
146 // properties. They will be used to generate
147 // conversations in the frontend.
148 string msgid = this.message.GetHeader ("Message-Id");
149 if (msgid != null)
150 AddProperty (Property.NewUnsearched ("fixme:msgid", GMime.Utils.DecodeMessageId (msgid)));
152 foreach (GMime.References refs in this.message.References)
153 AddProperty (Property.NewUnsearched ("fixme:reference", refs.Msgid));
155 string list_id = this.message.GetHeader ("List-Id");
156 if (list_id != null) {
157 // FIXME: Might need some additional parsing.
158 AddProperty (Property.NewKeyword ("fixme:mlist", GMime.Utils.HeaderDecodePhrase (list_id)));
161 // KMail can store replies in the same folder
162 // Use issent flag to distinguish between incoming
163 // and outgoing message
164 string kmail_msg_sent = this.message.GetHeader ("X-KMail-Link-Type");
165 bool issent_is_set = false;
166 foreach (Property property in IndexableProperties) {
167 if (property.Key == "fixme:isSent") {
168 issent_is_set = true;
169 break;
172 if (!issent_is_set && kmail_msg_sent != null && kmail_msg_sent == "reply")
173 AddProperty (Property.NewFlag ("fixme:isSent"));
176 protected override void DoPullSetup ()
178 this.handler = new PartHandler (this);
179 using (GMime.Object mime_part = this.message.MimePart)
180 this.handler.OnEachPart (mime_part);
182 AddChildIndexables (this.handler.ChildIndexables);
185 protected override void DoPull ()
187 if (handler.Reader == null) {
188 Finished ();
189 return;
192 string l = handler.Reader.ReadLine ();
194 if (l == null)
195 Finished ();
196 else if (l.Length > 0) {
197 AppendText (l);
198 AppendStructuralBreak ();
202 protected override void DoClose ()
204 Dispose ();
207 public void Dispose ()
209 if (this.handler != null && this.handler.Reader != null)
210 this.handler.Reader.Close ();
211 this.handler = null;
213 if (this.message != null) {
214 this.message.Dispose ();
215 this.message = null;
219 private class PartHandler {
220 private Beagle.Daemon.Filter filter;
221 private int count = 0; // parts handled so far
222 private int depth = 0; // part recursion depth
223 private ArrayList child_indexables = new ArrayList ();
224 private TextReader reader;
226 public PartHandler (Beagle.Daemon.Filter filter)
228 this.filter = filter;
231 private bool IsMimeTypeHandled (string mime_type)
233 foreach (FilterFlavor flavor in FilterFlavor.Flavors) {
234 if (flavor.IsMatch (null, null, mime_type.ToLower ()))
235 return true;
238 return false;
241 public void OnEachPart (GMime.Object mime_part)
243 GMime.Object part = null;
244 bool part_needs_dispose = false;
246 //for (int i = 0; i < this.depth; i++)
247 // Console.Write (" ");
248 //Console.WriteLine ("Content-Type: {0}", mime_part.ContentType);
250 ++depth;
252 if (mime_part is GMime.MessagePart) {
253 GMime.MessagePart msg_part = (GMime.MessagePart) mime_part;
255 using (GMime.Message message = msg_part.Message) {
256 using (GMime.Object subpart = message.MimePart)
257 this.OnEachPart (subpart);
259 } else if (mime_part is GMime.Multipart) {
260 GMime.Multipart multipart = (GMime.Multipart) mime_part;
262 int num_parts = multipart.Number;
264 // If the mimetype is multipart/alternative, we only want to index
265 // one part -- the richest one we can filter.
266 if (mime_part.ContentType.Subtype.ToLower () == "alternative") {
267 // The richest formats are at the end, so work from there
268 // backward.
269 for (int i = num_parts - 1; i >= 0; i--) {
270 GMime.Object subpart = multipart.GetPart (i);
272 if (IsMimeTypeHandled (subpart.ContentType.ToString ())) {
273 part = subpart;
274 part_needs_dispose = true;
275 break;
276 } else {
277 subpart.Dispose ();
282 // If it's not alternative, or we don't know how to filter any of
283 // the parts, treat them like a bunch of attachments.
284 if (part == null) {
285 for (int i = 0; i < num_parts; i++) {
286 using (GMime.Object subpart = multipart.GetPart (i))
287 this.OnEachPart (subpart);
290 } else if (mime_part is GMime.Part)
291 part = mime_part;
292 else
293 throw new Exception (String.Format ("Unknown part type: {0}", part.GetType ()));
295 if (part != null) {
296 System.IO.Stream stream = null;
298 using (GMime.DataWrapper content_obj = ((GMime.Part) part).ContentObject)
299 stream = content_obj.Stream;
301 // If this is the only part and it's plain text, we
302 // want to just attach it to our filter instead of
303 // creating a child indexable for it.
304 bool no_child_needed = false;
306 if (this.depth == 1 && this.count == 0) {
307 if (part.ContentType.ToString ().ToLower () == "text/plain") {
308 no_child_needed = true;
310 this.reader = new StreamReader (stream);
314 if (!no_child_needed) {
315 string sub_uri = this.filter.Uri.ToString () + "#" + this.count;
316 Indexable child = new Indexable (new Uri (sub_uri));
318 child.HitType = "MailMessage";
319 child.MimeType = part.ContentType.ToString ();
320 child.CacheContent = false;
322 child.AddProperty (Property.NewKeyword ("fixme:attachment_title", ((GMime.Part)part).Filename));
324 if (part.ContentType.Type.ToLower () == "text")
325 child.SetTextReader (new StreamReader (stream));
326 else
327 child.SetBinaryStream (stream);
329 this.child_indexables.Add (child);
332 this.count++;
335 if (part_needs_dispose)
336 part.Dispose ();
338 --depth;
341 public ICollection ChildIndexables {
342 get { return this.child_indexables; }
345 public TextReader Reader {
346 get { return this.reader; }