Dont throw EncodingFoundException unless asked to. Should remove the occassional...
[beagle.git] / Filters / FilterMail.cs
blob009c12846d66ba9969ba36966867724e6eb7b2e3
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 AppendText (l);
196 else
197 Finished ();
200 protected override void DoClose ()
202 Dispose ();
205 public void Dispose ()
207 if (this.handler != null && this.handler.Reader != null)
208 this.handler.Reader.Close ();
209 this.handler = null;
211 if (this.message != null) {
212 this.message.Dispose ();
213 this.message = null;
217 private class PartHandler {
218 private Beagle.Daemon.Filter filter;
219 private int count = 0; // parts handled so far
220 private int depth = 0; // part recursion depth
221 private ArrayList child_indexables = new ArrayList ();
222 private TextReader reader;
224 public PartHandler (Beagle.Daemon.Filter filter)
226 this.filter = filter;
229 private bool IsMimeTypeHandled (string mime_type)
231 foreach (FilterFlavor flavor in FilterFlavor.Flavors) {
232 if (flavor.IsMatch (null, null, mime_type.ToLower ()))
233 return true;
236 return false;
239 public void OnEachPart (GMime.Object mime_part)
241 GMime.Object part = null;
242 bool part_needs_dispose = false;
244 //for (int i = 0; i < this.depth; i++)
245 // Console.Write (" ");
246 //Console.WriteLine ("Content-Type: {0}", mime_part.ContentType);
248 ++depth;
250 if (mime_part is GMime.MessagePart) {
251 GMime.MessagePart msg_part = (GMime.MessagePart) mime_part;
253 using (GMime.Message message = msg_part.Message) {
254 using (GMime.Object subpart = message.MimePart)
255 this.OnEachPart (subpart);
257 } else if (mime_part is GMime.Multipart) {
258 GMime.Multipart multipart = (GMime.Multipart) mime_part;
260 int num_parts = multipart.Number;
262 // If the mimetype is multipart/alternative, we only want to index
263 // one part -- the richest one we can filter.
264 if (mime_part.ContentType.Subtype.ToLower () == "alternative") {
265 // The richest formats are at the end, so work from there
266 // backward.
267 for (int i = num_parts - 1; i >= 0; i--) {
268 GMime.Object subpart = multipart.GetPart (i);
270 if (IsMimeTypeHandled (subpart.ContentType.ToString ())) {
271 part = subpart;
272 part_needs_dispose = true;
273 break;
274 } else {
275 subpart.Dispose ();
280 // If it's not alternative, or we don't know how to filter any of
281 // the parts, treat them like a bunch of attachments.
282 if (part == null) {
283 for (int i = 0; i < num_parts; i++) {
284 using (GMime.Object subpart = multipart.GetPart (i))
285 this.OnEachPart (subpart);
288 } else if (mime_part is GMime.Part)
289 part = mime_part;
290 else
291 throw new Exception (String.Format ("Unknown part type: {0}", part.GetType ()));
293 if (part != null) {
294 System.IO.Stream stream = null;
296 using (GMime.DataWrapper content_obj = ((GMime.Part) part).ContentObject)
297 stream = content_obj.Stream;
299 // If this is the only part and it's plain text, we
300 // want to just attach it to our filter instead of
301 // creating a child indexable for it.
302 bool no_child_needed = false;
304 if (this.depth == 1 && this.count == 0) {
305 if (part.ContentType.ToString ().ToLower () == "text/plain") {
306 no_child_needed = true;
308 this.reader = new StreamReader (stream);
312 if (!no_child_needed) {
313 string sub_uri = this.filter.Uri.ToString () + "#" + this.count;
314 Indexable child = new Indexable (new Uri (sub_uri));
316 child.HitType = "MailMessage";
317 child.MimeType = part.ContentType.ToString ();
318 child.CacheContent = false;
320 child.AddProperty (Property.NewKeyword ("fixme:attachment_title", ((GMime.Part)part).Filename));
322 if (part.ContentType.Type.ToLower () == "text")
323 child.SetTextReader (new StreamReader (stream));
324 else
325 child.SetBinaryStream (stream);
327 this.child_indexables.Add (child);
330 this.count++;
333 if (part_needs_dispose)
334 part.Dispose ();
336 --depth;
339 public ICollection ChildIndexables {
340 get { return this.child_indexables; }
343 public TextReader Reader {
344 get { return this.reader; }