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 public class FilterMail
: Beagle
.Daemon
.Filter
, IDisposable
{
42 private static bool gmime_initialized
= false;
44 private GMime
.Message message
;
45 private PartHandler handler
;
49 // 1: Make email addresses non-keyword, add sanitized version
50 // for eaching for parts of an email address.
53 AddSupportedFlavor (FilterFlavor
.NewFromMimeType ("message/rfc822"));
56 protected override void DoOpen (FileInfo info
)
58 if (!gmime_initialized
) {
61 gmime_initialized
= true;
68 int mail_fd
= Mono
.Unix
.Native
.Syscall
.open (info
.FullName
, Mono
.Unix
.Native
.OpenFlags
.O_RDONLY
);
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 ();
79 if (this.message
== null)
83 private bool HasAttachments (GMime
.Object mime_part
)
85 if (mime_part
is GMime
.MessagePart
)
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")
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
));
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
));
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
));
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");
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;
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) {
187 string l
= handler
.Reader
.ReadLine ();
195 protected override void DoClose ()
200 public void Dispose ()
202 if (this.handler
!= null && this.handler
.Reader
!= null)
203 this.handler
.Reader
.Close ();
206 if (this.message
!= null) {
207 this.message
.Dispose ();
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 ()))
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);
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
262 for (int i
= num_parts
- 1; i
>= 0; i
--) {
263 GMime
.Object subpart
= multipart
.GetPart (i
);
265 if (IsMimeTypeHandled (subpart
.ContentType
.ToString ())) {
267 part_needs_dispose
= true;
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.
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
)
286 throw new Exception (String
.Format ("Unknown part type: {0}", part
.GetType ()));
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
));
320 child
.SetBinaryStream (stream
);
322 this.child_indexables
.Add (child
);
328 if (part_needs_dispose
)
334 public ICollection ChildIndexables
{
335 get { return this.child_indexables; }
338 public TextReader Reader
{
339 get { return this.reader; }