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