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