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