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