5 // Copyright (C) 2004-2005 Novell, Inc.
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.
29 using System
.Collections
;
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
;
54 // 1: Make email addresses non-keyword, add sanitized version
55 // for eaching for parts of an email address.
58 AddSupportedFlavor (FilterFlavor
.NewFromMimeType ("message/rfc822"));
61 protected override void DoOpen (FileInfo info
)
63 if (!gmime_initialized
) {
66 gmime_initialized
= true;
73 int mail_fd
= Mono
.Unix
.Native
.Syscall
.open (info
.FullName
, Mono
.Unix
.Native
.OpenFlags
.O_RDONLY
);
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 ();
84 if (this.message
== null)
88 private bool HasAttachments (GMime
.Object mime_part
)
90 if (mime_part
is GMime
.MessagePart
)
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")
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
));
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
));
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
));
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");
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;
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) {
192 string l
= handler
.Reader
.ReadLine ();
200 protected override void DoClose ()
205 public void Dispose ()
207 if (this.handler
!= null && this.handler
.Reader
!= null)
208 this.handler
.Reader
.Close ();
211 if (this.message
!= null) {
212 this.message
.Dispose ();
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 ()))
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);
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
267 for (int i
= num_parts
- 1; i
>= 0; i
--) {
268 GMime
.Object subpart
= multipart
.GetPart (i
);
270 if (IsMimeTypeHandled (subpart
.ContentType
.ToString ())) {
272 part_needs_dispose
= true;
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.
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
)
291 throw new Exception (String
.Format ("Unknown part type: {0}", part
.GetType ()));
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
));
325 child
.SetBinaryStream (stream
);
327 this.child_indexables
.Add (child
);
333 if (part_needs_dispose
)
339 public ICollection ChildIndexables
{
340 get { return this.child_indexables; }
343 public TextReader Reader
{
344 get { return this.reader; }