]> git.notmuchmail.org Git - notmuch/blob - bindings/python/notmuch/message.py
950d632a5de6d9c4b764b2d9bc03da7f8bded153
[notmuch] / bindings / python / notmuch / message.py
1 """
2 This file is part of notmuch.
3
4 Notmuch is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License as published by the
6 Free Software Foundation, either version 3 of the License, or (at your
7 option) any later version.
8
9 Notmuch is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with notmuch.  If not, see <http://www.gnu.org/licenses/>.
16
17 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
18                Jesse Rosenthal <jrosenthal@jhu.edu>
19 """
20
21         
22 from ctypes import c_char_p, c_void_p, c_long, c_uint
23 from datetime import date
24 from notmuch.globals import nmlib, STATUS, NotmuchError, Enum
25 from notmuch.tag import Tags
26 from notmuch.filename import Filenames
27 import sys
28 import email
29 import types
30 try:
31     import simplejson as json
32 except ImportError:
33     import json
34 #------------------------------------------------------------------------------
35 class Messages(object):
36     """Represents a list of notmuch messages
37
38     This object provides an iterator over a list of notmuch messages
39     (Technically, it provides a wrapper for the underlying
40     *notmuch_messages_t* structure). Do note that the underlying library
41     only provides a one-time iterator (it cannot reset the iterator to
42     the start). Thus iterating over the function will "exhaust" the list
43     of messages, and a subsequent iteration attempt will raise a
44     :exc:`NotmuchError` STATUS.NOT_INITIALIZED. If you need to
45     re-iterate over a list of messages you will need to retrieve a new
46     :class:`Messages` object or cache your :class:`Message`s in a list
47     via::
48
49        msglist = list(msgs)
50
51     You can store and reuse the single :class:`Message` objects as often
52     as you want as long as you keep the parent :class:`Messages` object
53     around. (Due to hierarchical memory allocation, all derived
54     :class:`Message` objects will be invalid when we delete the parent
55     :class:`Messages` object, even if it was already exhausted.) So
56     this works::
57
58       db   = Database()
59       msgs = Query(db,'').search_messages() #get a Messages() object
60       msglist = list(msgs)
61
62       # msgs is "exhausted" now and msgs.next() will raise an exception.
63       # However it will be kept alive until all retrieved Message()
64       # objects are also deleted. If you do e.g. an explicit del(msgs)
65       # here, the following lines would fail.
66       
67       # You can reiterate over *msglist* however as often as you want. 
68       # It is simply a list with :class:`Message`s.
69
70       print (msglist[0].get_filename())
71       print (msglist[1].get_filename())
72       print (msglist[0].get_message_id())
73
74
75     As :class:`Message` implements both __hash__() and __cmp__(), it is
76     possible to make sets out of :class:`Messages` and use set
77     arithmetic (this happens in python and will of course be *much*
78     slower than redoing a proper query with the appropriate filters::
79
80         s1, s2 = set(msgs1), set(msgs2)
81         s.union(s2)
82         s1 -= s2
83         ...
84
85     Be careful when using set arithmetic between message sets derived
86     from different Databases (ie the same database reopened after
87     messages have changed). If messages have added or removed associated
88     files in the meantime, it is possible that the same message would be
89     considered as a different object (as it points to a different file).
90     """
91
92     #notmuch_messages_get
93     _get = nmlib.notmuch_messages_get
94     _get.restype = c_void_p
95
96     _collect_tags = nmlib.notmuch_messages_collect_tags
97     _collect_tags.restype = c_void_p
98
99     def __init__(self, msgs_p, parent=None):
100         """
101         :param msgs_p:  A pointer to an underlying *notmuch_messages_t*
102              structure. These are not publically exposed, so a user
103              will almost never instantiate a :class:`Messages` object
104              herself. They are usually handed back as a result,
105              e.g. in :meth:`Query.search_messages`.  *msgs_p* must be
106              valid, we will raise an :exc:`NotmuchError`
107              (STATUS.NULL_POINTER) if it is `None`.
108         :type msgs_p: :class:`ctypes.c_void_p`
109         :param parent: The parent object
110              (ie :class:`Query`) these tags are derived from. It saves
111              a reference to it, so we can automatically delete the db
112              object once all derived objects are dead.
113         :TODO: Make the iterator work more than once and cache the tags in 
114                the Python object.(?)
115         """
116         if msgs_p is None:
117             NotmuchError(STATUS.NULL_POINTER)
118
119         self._msgs = msgs_p
120         #store parent, so we keep them alive as long as self  is alive
121         self._parent = parent
122
123     def collect_tags(self):
124         """Return the unique :class:`Tags` in the contained messages
125
126         :returns: :class:`Tags`
127         :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not inited
128
129         .. note:: :meth:`collect_tags` will iterate over the messages and
130           therefore will not allow further iterations.
131         """
132         if self._msgs is None:
133             raise NotmuchError(STATUS.NOT_INITIALIZED)
134
135         # collect all tags (returns NULL on error)
136         tags_p = Messages._collect_tags (self._msgs)
137         #reset _msgs as we iterated over it and can do so only once
138         self._msgs = None
139
140         if tags_p == None:
141             raise NotmuchError(STATUS.NULL_POINTER)
142         return Tags(tags_p, self)
143
144     def __iter__(self):
145         """ Make Messages an iterator """
146         return self
147
148     def next(self):
149         if self._msgs is None:
150             raise NotmuchError(STATUS.NOT_INITIALIZED)
151
152         if not nmlib.notmuch_messages_valid(self._msgs):
153             self._msgs = None
154             raise StopIteration
155
156         msg = Message(Messages._get (self._msgs), self)
157         nmlib.notmuch_messages_move_to_next(self._msgs)
158         return msg
159
160     def __nonzero__(self):
161         """
162         :return: True if there is at least one more thread in the
163             Iterator, False if not."""
164         return self._msgs is not None and \
165             nmlib.notmuch_messages_valid(self._msgs) > 0
166
167     def __del__(self):
168         """Close and free the notmuch Messages"""
169         if self._msgs is not None:
170             nmlib.notmuch_messages_destroy (self._msgs)
171
172     def print_messages(self, format, indent=0, entire_thread=False):
173         """Outputs messages as needed for 'notmuch show' to sys.stdout
174
175         :param format: A string of either 'text' or 'json'.
176         :param indent: A number indicating the reply depth of these messages.
177         :param entire_thread: A bool, indicating whether we want to output 
178                        whole threads or only the matching messages.
179         """
180         if format.lower() == "text":
181             set_start = ""
182             set_end = ""
183             set_sep = ""
184         elif format.lower() == "json":
185             set_start = "["
186             set_end = "]"
187             set_sep = ", "
188         else:
189             raise Exception
190
191         first_set = True
192
193         sys.stdout.write(set_start)
194
195         # iterate through all toplevel messages in this thread
196         for msg in self:
197             # if not msg:
198             #     break 
199             if not first_set:
200                 sys.stdout.write(set_sep)
201             first_set = False
202
203             sys.stdout.write(set_start)
204             match = msg.is_match()
205             next_indent = indent
206
207             if (match or entire_thread):
208                 if format.lower() == "text":
209                     sys.stdout.write(msg.format_message_as_text(indent))
210                 elif format.lower() == "json":
211                     sys.stdout.write(msg.format_message_as_json(indent))
212                 else:
213                     raise NotmuchError
214                 next_indent = indent + 1
215
216             # get replies and print them also out (if there are any)
217             replies = msg.get_replies()
218             if not replies is None:
219                 sys.stdout.write(set_sep)
220                 replies.print_messages(format, next_indent, entire_thread)
221
222             sys.stdout.write(set_end)
223         sys.stdout.write(set_end)
224
225 #------------------------------------------------------------------------------
226 class Message(object):
227     """Represents a single Email message
228
229     Technically, this wraps the underlying *notmuch_message_t* structure.
230
231     As this implements __cmp__() it is possible to compare 2
232     :class:`Message`s with::
233
234         if msg1 == msg2:
235     """
236
237     """notmuch_message_get_filename (notmuch_message_t *message)"""
238     _get_filename = nmlib.notmuch_message_get_filename
239     _get_filename.restype = c_char_p 
240
241     """return all filenames for a message"""
242     _get_filenames = nmlib.notmuch_message_get_filenames
243     _get_filenames.restype = c_void_p
244
245     """notmuch_message_get_flag"""
246     _get_flag = nmlib.notmuch_message_get_flag
247     _get_flag.restype = c_uint
248
249     """notmuch_message_get_message_id (notmuch_message_t *message)"""
250     _get_message_id = nmlib.notmuch_message_get_message_id
251     _get_message_id.restype = c_char_p 
252
253     """notmuch_message_get_thread_id"""
254     _get_thread_id = nmlib.notmuch_message_get_thread_id
255     _get_thread_id.restype = c_char_p
256
257     """notmuch_message_get_replies"""
258     _get_replies = nmlib.notmuch_message_get_replies
259     _get_replies.restype = c_void_p
260
261     """notmuch_message_get_tags (notmuch_message_t *message)"""
262     _get_tags = nmlib.notmuch_message_get_tags
263     _get_tags.restype = c_void_p
264
265     _get_date = nmlib.notmuch_message_get_date
266     _get_date.restype = c_long
267
268     _get_header = nmlib.notmuch_message_get_header
269     _get_header.restype = c_char_p
270
271     #Constants: Flags that can be set/get with set_flag
272     FLAG = Enum(['MATCH'])
273
274     def __init__(self, msg_p, parent=None):
275         """
276         :param msg_p: A pointer to an internal notmuch_message_t
277             Structure.  If it is `None`, we will raise an :exc:`NotmuchError`
278             STATUS.NULL_POINTER.
279         :param parent: A 'parent' object is passed which this message is
280               derived from. We save a reference to it, so we can
281               automatically delete the parent object once all derived
282               objects are dead.
283         """
284         if msg_p is None:
285             NotmuchError(STATUS.NULL_POINTER)
286         self._msg = msg_p
287         #keep reference to parent, so we keep it alive
288         self._parent = parent
289
290
291     def get_message_id(self):
292         """Returns the message ID
293         
294         :returns: String with a message ID
295         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
296                     is not initialized.
297         """
298         if self._msg is None:
299             raise NotmuchError(STATUS.NOT_INITIALIZED)
300         return Message._get_message_id(self._msg)
301
302     def get_thread_id(self):
303         """Returns the thread ID
304
305         The returned string belongs to 'message' will only be valid for as 
306         long as the message is valid.
307
308         This function will not return None since Notmuch ensures that every
309         message belongs to a single thread.
310
311         :returns: String with a thread ID
312         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
313                     is not initialized.
314         """
315         if self._msg is None:
316             raise NotmuchError(STATUS.NOT_INITIALIZED)
317
318         return Message._get_thread_id (self._msg);
319
320     def get_replies(self):
321         """Gets all direct replies to this message as :class:`Messages` iterator
322
323         .. note:: This call only makes sense if 'message' was
324           ultimately obtained from a :class:`Thread` object, (such as
325           by coming directly from the result of calling
326           :meth:`Thread.get_toplevel_messages` or by any number of
327           subsequent calls to :meth:`get_replies`). If this message was
328           obtained through some non-thread means, (such as by a call
329           to :meth:`Query.search_messages`), then this function will
330           return `None`.
331
332         :returns: :class:`Messages` or `None` if there are no replies to 
333             this message.
334         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
335                     is not initialized.
336         """
337         if self._msg is None:
338             raise NotmuchError(STATUS.NOT_INITIALIZED)
339
340         msgs_p = Message._get_replies(self._msg);
341
342         if msgs_p is None:
343             return None
344
345         return Messages(msgs_p,self)
346
347     def get_date(self):
348         """Returns time_t of the message date
349
350         For the original textual representation of the Date header from the
351         message call notmuch_message_get_header() with a header value of
352         "date".
353
354         :returns: A time_t timestamp.
355         :rtype: c_unit64
356         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
357                     is not initialized.
358         """
359         if self._msg is None:
360             raise NotmuchError(STATUS.NOT_INITIALIZED)
361         return Message._get_date(self._msg)
362
363     def get_header(self, header):
364         """Returns a message header
365         
366         This returns any message header that is stored in the notmuch database.
367         This is only a selected subset of headers, which is currently:
368
369           TODO: add stored headers
370
371         :param header: The name of the header to be retrieved.
372                        It is not case-sensitive (TODO: confirm).
373         :type header: str
374         :returns: The header value as string
375         :exception: :exc:`NotmuchError`
376
377                     * STATUS.NOT_INITIALIZED if the message 
378                       is not initialized.
379                     * STATUS.NULL_POINTER, if no header was found
380         """
381         if self._msg is None:
382             raise NotmuchError(STATUS.NOT_INITIALIZED)
383
384         #Returns NULL if any error occurs.
385         header = Message._get_header (self._msg, header)
386         if header == None:
387             raise NotmuchError(STATUS.NULL_POINTER)
388         return header
389
390     def get_filename(self):
391         """Returns the file path of the message file
392
393         :returns: Absolute file path & name of the message file
394         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
395               is not initialized.
396         """
397         if self._msg is None:
398             raise NotmuchError(STATUS.NOT_INITIALIZED)
399         return Message._get_filename(self._msg)
400
401     def get_filenames(self):
402         """Get all filenames for the email corresponding to 'message'
403
404         Returns a Filenames() generator with all absolute filepaths for
405         messages recorded to have the same Message-ID. These files must
406         not necessarily have identical content."""
407         if self._msg is None:
408             raise NotmuchError(STATUS.NOT_INITIALIZED)
409         
410         files_p = Message._get_filenames(self._msg)
411
412         return Filenames(files_p, self).as_generator()
413
414     def get_flag(self, flag):
415         """Checks whether a specific flag is set for this message
416
417         The method :meth:`Query.search_threads` sets
418         *Message.FLAG.MATCH* for those messages that match the
419         query. This method allows us to get the value of this flag.
420
421         :param flag: One of the :attr:`Message.FLAG` values (currently only 
422                      *Message.FLAG.MATCH*
423         :returns: An unsigned int (0/1), indicating whether the flag is set.
424         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
425               is not initialized.
426         """
427         if self._msg is None:
428             raise NotmuchError(STATUS.NOT_INITIALIZED)
429         return Message._get_flag(self._msg, flag)
430
431     def set_flag(self, flag, value):
432         """Sets/Unsets a specific flag for this message
433
434         :param flag: One of the :attr:`Message.FLAG` values (currently only 
435                      *Message.FLAG.MATCH*
436         :param value: A bool indicating whether to set or unset the flag.
437
438         :returns: Nothing
439         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
440               is not initialized.
441         """
442         if self._msg is None:
443             raise NotmuchError(STATUS.NOT_INITIALIZED)
444         nmlib.notmuch_message_set_flag(self._msg, flag, value)
445
446     def get_tags(self):
447         """Returns the message tags
448
449         :returns: A :class:`Tags` iterator.
450         :exception: :exc:`NotmuchError`
451
452                       * STATUS.NOT_INITIALIZED if the message 
453                         is not initialized.
454                       * STATUS.NULL_POINTER, on error
455         """
456         if self._msg is None:
457             raise NotmuchError(STATUS.NOT_INITIALIZED)
458
459         tags_p = Message._get_tags(self._msg)
460         if tags_p == None:
461             raise NotmuchError(STATUS.NULL_POINTER)
462         return Tags(tags_p, self)
463
464     def add_tag(self, tag):
465         """Adds a tag to the given message
466
467         Adds a tag to the current message. The maximal tag length is defined in
468         the notmuch library and is currently 200 bytes.
469
470         :param tag: String with a 'tag' to be added.
471         :returns: STATUS.SUCCESS if the tag was successfully added.
472                   Raises an exception otherwise.
473         :exception: :exc:`NotmuchError`. They have the following meaning:
474
475                   STATUS.NULL_POINTER
476                     The 'tag' argument is NULL
477                   STATUS.TAG_TOO_LONG
478                     The length of 'tag' is too long 
479                     (exceeds Message.NOTMUCH_TAG_MAX)
480                   STATUS.READ_ONLY_DATABASE
481                     Database was opened in read-only mode so message cannot be 
482                     modified.
483                   STATUS.NOT_INITIALIZED
484                      The message has not been initialized.
485        """
486         if self._msg is None:
487             raise NotmuchError(STATUS.NOT_INITIALIZED)
488
489         status = nmlib.notmuch_message_add_tag (self._msg, tag)
490
491         if STATUS.SUCCESS == status:
492             # return on success
493             return status
494
495         raise NotmuchError(status)
496
497     def remove_tag(self, tag):
498         """Removes a tag from the given message
499
500         If the message has no such tag, this is a non-operation and
501         will report success anyway.
502
503         :param tag: String with a 'tag' to be removed.
504         :returns: STATUS.SUCCESS if the tag was successfully removed or if 
505                   the message had no such tag.
506                   Raises an exception otherwise.
507         :exception: :exc:`NotmuchError`. They have the following meaning:
508
509                    STATUS.NULL_POINTER
510                      The 'tag' argument is NULL
511                    STATUS.TAG_TOO_LONG
512                      The length of 'tag' is too long
513                      (exceeds NOTMUCH_TAG_MAX)
514                    STATUS.READ_ONLY_DATABASE
515                      Database was opened in read-only mode so message cannot 
516                      be modified.
517                    STATUS.NOT_INITIALIZED
518                      The message has not been initialized.
519         """
520         if self._msg is None:
521             raise NotmuchError(STATUS.NOT_INITIALIZED)
522
523         status = nmlib.notmuch_message_remove_tag(self._msg, tag)
524
525         if STATUS.SUCCESS == status:
526             # return on success
527             return status
528
529         raise NotmuchError(status)
530
531     def remove_all_tags(self):
532         """Removes all tags from the given message.
533
534         See :meth:`freeze` for an example showing how to safely
535         replace tag values.
536
537         :returns: STATUS.SUCCESS if the tags were successfully removed.
538                   Raises an exception otherwise.
539         :exception: :exc:`NotmuchError`. They have the following meaning:
540
541                    STATUS.READ_ONLY_DATABASE
542                      Database was opened in read-only mode so message cannot 
543                      be modified.
544                    STATUS.NOT_INITIALIZED
545                      The message has not been initialized.
546         """
547         if self._msg is None:
548             raise NotmuchError(STATUS.NOT_INITIALIZED)
549  
550         status = nmlib.notmuch_message_remove_all_tags(self._msg)
551
552         if STATUS.SUCCESS == status:
553             # return on success
554             return status
555
556         raise NotmuchError(status)
557
558     def freeze(self):
559         """Freezes the current state of 'message' within the database
560
561         This means that changes to the message state, (via :meth:`add_tag`, 
562         :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be 
563         committed to the database until the message is :meth:`thaw`ed.
564
565         Multiple calls to freeze/thaw are valid and these calls will
566         "stack". That is there must be as many calls to thaw as to freeze
567         before a message is actually thawed.
568
569         The ability to do freeze/thaw allows for safe transactions to
570         change tag values. For example, explicitly setting a message to
571         have a given set of tags might look like this::
572
573           msg.freeze()
574           msg.remove_all_tags()
575           for tag in new_tags:
576               msg.add_tag(tag)
577           msg.thaw()
578
579         With freeze/thaw used like this, the message in the database is
580         guaranteed to have either the full set of original tag values, or
581         the full set of new tag values, but nothing in between.
582
583         Imagine the example above without freeze/thaw and the operation
584         somehow getting interrupted. This could result in the message being
585         left with no tags if the interruption happened after
586         :meth:`remove_all_tags` but before :meth:`add_tag`.
587
588         :returns: STATUS.SUCCESS if the message was successfully frozen.
589                   Raises an exception otherwise.
590         :exception: :exc:`NotmuchError`. They have the following meaning:
591
592                    STATUS.READ_ONLY_DATABASE
593                      Database was opened in read-only mode so message cannot 
594                      be modified.
595                    STATUS.NOT_INITIALIZED
596                      The message has not been initialized.
597         """
598         if self._msg is None:
599             raise NotmuchError(STATUS.NOT_INITIALIZED)
600  
601         status = nmlib.notmuch_message_freeze(self._msg)
602
603         if STATUS.SUCCESS == status:
604             # return on success
605             return status
606
607         raise NotmuchError(status)
608
609     def thaw(self):
610         """Thaws the current 'message'
611
612         Thaw the current 'message', synchronizing any changes that may have 
613         occurred while 'message' was frozen into the notmuch database.
614
615         See :meth:`freeze` for an example of how to use this
616         function to safely provide tag changes.
617
618         Multiple calls to freeze/thaw are valid and these calls with
619         "stack". That is there must be as many calls to thaw as to freeze
620         before a message is actually thawed.
621
622         :returns: STATUS.SUCCESS if the message was successfully frozen.
623                   Raises an exception otherwise.
624         :exception: :exc:`NotmuchError`. They have the following meaning:
625
626                    STATUS.UNBALANCED_FREEZE_THAW
627                      An attempt was made to thaw an unfrozen message. 
628                      That is, there have been an unbalanced number of calls 
629                      to :meth:`freeze` and :meth:`thaw`.
630                    STATUS.NOT_INITIALIZED
631                      The message has not been initialized.
632         """
633         if self._msg is None:
634             raise NotmuchError(STATUS.NOT_INITIALIZED)
635  
636         status = nmlib.notmuch_message_thaw(self._msg)
637
638         if STATUS.SUCCESS == status:
639             # return on success
640             return status
641
642         raise NotmuchError(status)
643
644
645     def is_match(self):
646         """(Not implemented)"""
647         return self.get_flag(Message.FLAG.MATCH)
648
649     def __str__(self):
650         """A message() is represented by a 1-line summary"""
651         msg = {}
652         msg['from'] = self.get_header('from')
653         msg['tags'] = str(self.get_tags())
654         msg['date'] = date.fromtimestamp(self.get_date())
655         replies = self.get_replies()
656         msg['replies'] = len(replies) if replies is not None else -1
657         return "%(from)s (%(date)s) (%(tags)s) (%(replies)d) replies" % (msg)
658
659
660     def get_message_parts(self):
661         """Output like notmuch show"""
662         fp = open(self.get_filename())
663         email_msg = email.message_from_file(fp)
664         fp.close()
665
666         out = []
667         for msg in email_msg.walk():
668             if not msg.is_multipart():
669                 out.append(msg)
670         return out
671
672     def get_part(self, num):
673         """Returns the nth message body part"""
674         parts = self.get_message_parts()
675         if (num <= 0 or num > len(parts)):
676             return ""
677         else:
678             out_part = parts[(num - 1)]
679             return out_part.get_payload(decode=True)
680
681     def format_message_internal(self):
682         """Create an internal representation of the message parts,
683         which can easily be output to json, text, or another output
684         format. The argument match tells whether this matched a
685         query."""
686         output = {}
687         output["id"] = self.get_message_id()
688         output["match"] = self.is_match()
689         output["filename"] = self.get_filename()
690         output["tags"] = list(self.get_tags())
691
692         headers = {}
693         for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
694             headers[h] = self.get_header(h)
695         output["headers"] = headers
696
697         body = []
698         parts = self.get_message_parts()
699         for i in xrange(len(parts)):
700             msg = parts[i]
701             part_dict = {}
702             part_dict["id"] = i + 1
703             # We'll be using this is a lot, so let's just get it once.
704             cont_type = msg.get_content_type()
705             part_dict["content-type"] = cont_type
706             # NOTE:
707             # Now we emulate the current behaviour, where it ignores
708             # the html if there's a text representation. 
709             #
710             # This is being worked on, but it will be easier to fix
711             # here in the future than to end up with another
712             # incompatible solution.
713             disposition = msg["Content-Disposition"]
714             if disposition and disposition.lower().startswith("attachment"):
715                 part_dict["filename"] = msg.get_filename()
716             else:
717                 if cont_type.lower() == "text/plain":
718                     part_dict["content"] = msg.get_payload()
719                 elif (cont_type.lower() == "text/html" and 
720                       i == 0):
721                     part_dict["content"] = msg.get_payload()
722             body.append(part_dict)
723
724         output["body"] = body
725
726         return output
727
728     def format_message_as_json(self, indent=0):
729         """Outputs the message as json. This is essentially the same
730         as python's dict format, but we run it through, just so we
731         don't have to worry about the details."""
732         return json.dumps(self.format_message_internal())
733
734     def format_message_as_text(self, indent=0):
735         """Outputs it in the old-fashioned notmuch text form. Will be
736         easy to change to a new format when the format changes."""
737
738         format = self.format_message_internal()
739         output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
740                  % (format['id'], indent, format['match'], format['filename'])
741         output += "\n\fheader{"
742
743         #Todo: this date is supposed to be prettified, as in the index.
744         output += "\n%s (%s) (" % (format["headers"]["From"],
745                                    format["headers"]["Date"])
746         output += ", ".join(format["tags"])
747         output += ")"
748
749         output += "\nSubject: %s" % format["headers"]["Subject"]
750         output += "\nFrom: %s" % format["headers"]["From"]
751         output += "\nTo: %s" % format["headers"]["To"]
752         if format["headers"]["Cc"]:
753             output += "\nCc: %s" % format["headers"]["Cc"]
754         if format["headers"]["Bcc"]:
755             output += "\nBcc: %s" % format["headers"]["Bcc"]
756         output += "\nDate: %s" % format["headers"]["Date"]
757         output += "\n\fheader}"
758
759         output += "\n\fbody{"
760
761         parts = format["body"]
762         parts.sort(key=lambda x: x['id'])
763         for p in parts:
764             if not p.has_key("filename"):
765                 output += "\n\fpart{ "
766                 output += "ID: %d, Content-type: %s\n" % (p["id"], 
767                                                          p["content-type"])
768                 if p.has_key("content"):
769                     output += "\n%s\n" % p["content"]
770                 else:
771                     output += "Non-text part: %s\n" % p["content-type"]
772                     output += "\n\fpart}"                    
773             else:
774                 output += "\n\fattachment{ "
775                 output += "ID: %d, Content-type:%s\n" % (p["id"], 
776                                                          p["content-type"])
777                 output += "Attachment: %s\n" % p["filename"]
778                 output += "\n\fattachment}\n"
779
780         output += "\n\fbody}\n"
781         output += "\n\fmessage}"
782
783         return output
784
785     def __hash__(self):
786         """Implement hash(), so we can use Message() sets"""
787         file = self.get_filename()
788         if file is None:
789             return None
790         return hash(file)
791
792     def __cmp__(self, other):
793         """Implement cmp(), so we can compare Message()s
794
795         2 messages are considered equal if they point to the same
796         Message-Id and if they point to the same file names. If 2
797         Messages derive from different queries where some files have
798         been added or removed, the same messages would not be considered
799         equal (as they do not point to the same set of files
800         any more)."""
801         res =  cmp(self.get_message_id(), other.get_message_id())
802         if res:
803             res = cmp(list(self.get_filenames()), list(other.get_filenames()))
804         return res
805
806     def __del__(self):
807         """Close and free the notmuch Message"""
808         if self._msg is not None:
809             nmlib.notmuch_message_destroy (self._msg)