ba93d8fea773fdb1807f9dddc1417973b48caf69
[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             # get replies and print them also out (if there are any)
225             replies = msg.get_replies()
226             if not replies is None:
227                 sys.stdout.write(set_sep)
228                 replies.print_messages(format, next_indent, entire_thread)
229
230             sys.stdout.write(set_end)
231         sys.stdout.write(set_end)
232
233 #------------------------------------------------------------------------------
234 class Message(object):
235     """Represents a single Email message
236
237     Technically, this wraps the underlying *notmuch_message_t* structure.
238     """
239
240     """notmuch_message_get_filename (notmuch_message_t *message)"""
241     _get_filename = nmlib.notmuch_message_get_filename
242     _get_filename.restype = c_char_p 
243
244     """notmuch_message_get_flag"""
245     _get_flag = nmlib.notmuch_message_get_flag
246     _get_flag.restype = c_bool
247
248     """notmuch_message_get_message_id (notmuch_message_t *message)"""
249     _get_message_id = nmlib.notmuch_message_get_message_id
250     _get_message_id.restype = c_char_p 
251
252     """notmuch_message_get_thread_id"""
253     _get_thread_id = nmlib.notmuch_message_get_thread_id
254     _get_thread_id.restype = c_char_p
255
256     """notmuch_message_get_replies"""
257     _get_replies = nmlib.notmuch_message_get_replies
258     _get_replies.restype = c_void_p
259
260     """notmuch_message_get_tags (notmuch_message_t *message)"""
261     _get_tags = nmlib.notmuch_message_get_tags
262     _get_tags.restype = c_void_p
263
264     _get_date = nmlib.notmuch_message_get_date
265     _get_date.restype = c_long
266
267     _get_header = nmlib.notmuch_message_get_header
268     _get_header.restype = c_char_p
269
270     #Constants: Flags that can be set/get with set_flag
271     FLAG = Enum(['MATCH'])
272
273     def __init__(self, msg_p, parent=None):
274         """
275         :param msg_p: A pointer to an internal notmuch_message_t
276             Structure.  If it is `None`, we will raise an :exc:`NotmuchError`
277             STATUS.NULL_POINTER.
278         :param parent: A 'parent' object is passed which this message is
279               derived from. We save a reference to it, so we can
280               automatically delete the parent object once all derived
281               objects are dead.
282         """
283         if msg_p is None:
284             NotmuchError(STATUS.NULL_POINTER)
285         self._msg = msg_p
286         #keep reference to parent, so we keep it alive
287         self._parent = parent
288
289
290     def get_message_id(self):
291         """Returns the message ID
292         
293         :returns: String with a message ID
294         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
295                     is not initialized.
296         """
297         if self._msg is None:
298             raise NotmuchError(STATUS.NOT_INITIALIZED)
299         return Message._get_message_id(self._msg)
300
301     def get_thread_id(self):
302         """Returns the thread ID
303
304         The returned string belongs to 'message' will only be valid for as 
305         long as the message is valid.
306
307         This function will not return None since Notmuch ensures that every
308         message belongs to a single thread.
309
310         :returns: String with a thread ID
311         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
312                     is not initialized.
313         """
314         if self._msg is None:
315             raise NotmuchError(STATUS.NOT_INITIALIZED)
316
317         return Message._get_thread_id (self._msg);
318
319     def get_replies(self):
320         """Gets all direct replies to this message as :class:`Messages` iterator
321
322         .. note:: This call only makes sense if 'message' was
323           ultimately obtained from a :class:`Thread` object, (such as
324           by coming directly from the result of calling
325           :meth:`Thread.get_toplevel_messages` or by any number of
326           subsequent calls to :meth:`get_replies`). If this message was
327           obtained through some non-thread means, (such as by a call
328           to :meth:`Query.search_messages`), then this function will
329           return `None`.
330
331         :returns: :class:`Messages` or `None` if there are no replies to 
332             this message.
333         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
334                     is not initialized.
335         """
336         if self._msg is None:
337             raise NotmuchError(STATUS.NOT_INITIALIZED)
338
339         msgs_p = Message._get_replies(self._msg);
340
341         if msgs_p is None:
342             return None
343
344         return Messages(msgs_p,self)
345
346     def get_date(self):
347         """Returns time_t of the message date
348
349         For the original textual representation of the Date header from the
350         message call notmuch_message_get_header() with a header value of
351         "date".
352
353         :returns: A time_t timestamp.
354         :rtype: c_unit64
355         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
356                     is not initialized.
357         """
358         if self._msg is None:
359             raise NotmuchError(STATUS.NOT_INITIALIZED)
360         return Message._get_date(self._msg)
361
362     def get_header(self, header):
363         """Returns a message header
364         
365         This returns any message header that is stored in the notmuch database.
366         This is only a selected subset of headers, which is currently:
367
368           TODO: add stored headers
369
370         :param header: The name of the header to be retrieved.
371                        It is not case-sensitive (TODO: confirm).
372         :type header: str
373         :returns: The header value as string
374         :exception: :exc:`NotmuchError`
375
376                     * STATUS.NOT_INITIALIZED if the message 
377                       is not initialized.
378                     * STATUS.NULL_POINTER, if no header was found
379         """
380         if self._msg is None:
381             raise NotmuchError(STATUS.NOT_INITIALIZED)
382
383         #Returns NULL if any error occurs.
384         header = Message._get_header (self._msg, header)
385         if header == None:
386             raise NotmuchError(STATUS.NULL_POINTER)
387         return header
388
389     def get_filename(self):
390         """Returns the file path of the message file
391
392         :returns: Absolute file path & name of the message file
393         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
394               is not initialized.
395         """
396         if self._msg is None:
397             raise NotmuchError(STATUS.NOT_INITIALIZED)
398         return Message._get_filename(self._msg)
399
400     def get_flag(self, flag):
401         """Checks whether a specific flag is set for this message
402
403         The method :meth:`Query.search_threads` sets
404         *Message.FLAG.MATCH* for those messages that match the
405         query. This method allows us to get the value of this flag.
406
407         :param flag: One of the :attr:`Message.FLAG` values (currently only 
408                      *Message.FLAG.MATCH*
409         :returns: A bool, indicating whether the flag is set.
410         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
411               is not initialized.
412         """
413         if self._msg is None:
414             raise NotmuchError(STATUS.NOT_INITIALIZED)
415         return Message._get_flag(self._msg, flag)
416
417     def set_flag(self, flag, value):
418         """Sets/Unsets a specific flag for this message
419
420         :param flag: One of the :attr:`Message.FLAG` values (currently only 
421                      *Message.FLAG.MATCH*
422         :param value: A bool indicating whether to set or unset the flag.
423
424         :returns: Nothing
425         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message 
426               is not initialized.
427         """
428         if self._msg is None:
429             raise NotmuchError(STATUS.NOT_INITIALIZED)
430         nmlib.notmuch_message_set_flag(self._msg, flag, value)
431
432     def get_tags(self):
433         """Returns the message tags
434
435         :returns: A :class:`Tags` iterator.
436         :exception: :exc:`NotmuchError`
437
438                       * STATUS.NOT_INITIALIZED if the message 
439                         is not initialized.
440                       * STATUS.NULL_POINTER, on error
441         """
442         if self._msg is None:
443             raise NotmuchError(STATUS.NOT_INITIALIZED)
444
445         tags_p = Message._get_tags(self._msg)
446         if tags_p == None:
447             raise NotmuchError(STATUS.NULL_POINTER)
448         return Tags(tags_p, self)
449
450     def add_tag(self, tag):
451         """Adds a tag to the given message
452
453         Adds a tag to the current message. The maximal tag length is defined in
454         the notmuch library and is currently 200 bytes.
455
456         :param tag: String with a 'tag' to be added.
457         :returns: STATUS.SUCCESS if the tag was successfully added.
458                   Raises an exception otherwise.
459         :exception: :exc:`NotmuchError`. They have the following meaning:
460
461                   STATUS.NULL_POINTER
462                     The 'tag' argument is NULL
463                   STATUS.TAG_TOO_LONG
464                     The length of 'tag' is too long 
465                     (exceeds Message.NOTMUCH_TAG_MAX)
466                   STATUS.READ_ONLY_DATABASE
467                     Database was opened in read-only mode so message cannot be 
468                     modified.
469                   STATUS.NOT_INITIALIZED
470                      The message has not been initialized.
471        """
472         if self._msg is None:
473             raise NotmuchError(STATUS.NOT_INITIALIZED)
474
475         status = nmlib.notmuch_message_add_tag (self._msg, tag)
476
477         if STATUS.SUCCESS == status:
478             # return on success
479             return status
480
481         raise NotmuchError(status)
482
483     def remove_tag(self, tag):
484         """Removes a tag from the given message
485
486         If the message has no such tag, this is a non-operation and
487         will report success anyway.
488
489         :param tag: String with a 'tag' to be removed.
490         :returns: STATUS.SUCCESS if the tag was successfully removed or if 
491                   the message had no such tag.
492                   Raises an exception otherwise.
493         :exception: :exc:`NotmuchError`. They have the following meaning:
494
495                    STATUS.NULL_POINTER
496                      The 'tag' argument is NULL
497                    STATUS.TAG_TOO_LONG
498                      The length of 'tag' is too long
499                      (exceeds NOTMUCH_TAG_MAX)
500                    STATUS.READ_ONLY_DATABASE
501                      Database was opened in read-only mode so message cannot 
502                      be modified.
503                    STATUS.NOT_INITIALIZED
504                      The message has not been initialized.
505         """
506         if self._msg is None:
507             raise NotmuchError(STATUS.NOT_INITIALIZED)
508
509         status = nmlib.notmuch_message_remove_tag(self._msg, tag)
510
511         if STATUS.SUCCESS == status:
512             # return on success
513             return status
514
515         raise NotmuchError(status)
516
517     def remove_all_tags(self):
518         """Removes all tags from the given message.
519
520         See :meth:`freeze` for an example showing how to safely
521         replace tag values.
522
523         :returns: STATUS.SUCCESS if the tags were successfully removed.
524                   Raises an exception otherwise.
525         :exception: :exc:`NotmuchError`. They have the following meaning:
526
527                    STATUS.READ_ONLY_DATABASE
528                      Database was opened in read-only mode so message cannot 
529                      be modified.
530                    STATUS.NOT_INITIALIZED
531                      The message has not been initialized.
532         """
533         if self._msg is None:
534             raise NotmuchError(STATUS.NOT_INITIALIZED)
535  
536         status = nmlib.notmuch_message_remove_all_tags(self._msg)
537
538         if STATUS.SUCCESS == status:
539             # return on success
540             return status
541
542         raise NotmuchError(status)
543
544     def freeze(self):
545         """Freezes the current state of 'message' within the database
546
547         This means that changes to the message state, (via :meth:`add_tag`, 
548         :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be 
549         committed to the database until the message is :meth:`thaw`ed.
550
551         Multiple calls to freeze/thaw are valid and these calls will
552         "stack". That is there must be as many calls to thaw as to freeze
553         before a message is actually thawed.
554
555         The ability to do freeze/thaw allows for safe transactions to
556         change tag values. For example, explicitly setting a message to
557         have a given set of tags might look like this::
558
559           msg.freeze()
560           msg.remove_all_tags()
561           for tag in new_tags:
562               msg.add_tag(tag)
563           msg.thaw()
564
565         With freeze/thaw used like this, the message in the database is
566         guaranteed to have either the full set of original tag values, or
567         the full set of new tag values, but nothing in between.
568
569         Imagine the example above without freeze/thaw and the operation
570         somehow getting interrupted. This could result in the message being
571         left with no tags if the interruption happened after
572         :meth:`remove_all_tags` but before :meth:`add_tag`.
573
574         :returns: STATUS.SUCCESS if the message was successfully frozen.
575                   Raises an exception otherwise.
576         :exception: :exc:`NotmuchError`. They have the following meaning:
577
578                    STATUS.READ_ONLY_DATABASE
579                      Database was opened in read-only mode so message cannot 
580                      be modified.
581                    STATUS.NOT_INITIALIZED
582                      The message has not been initialized.
583         """
584         if self._msg is None:
585             raise NotmuchError(STATUS.NOT_INITIALIZED)
586  
587         status = nmlib.notmuch_message_freeze(self._msg)
588
589         if STATUS.SUCCESS == status:
590             # return on success
591             return status
592
593         raise NotmuchError(status)
594
595     def thaw(self):
596         """Thaws the current 'message'
597
598         Thaw the current 'message', synchronizing any changes that may have 
599         occurred while 'message' was frozen into the notmuch database.
600
601         See :meth:`freeze` for an example of how to use this
602         function to safely provide tag changes.
603
604         Multiple calls to freeze/thaw are valid and these calls with
605         "stack". That is there must be as many calls to thaw as to freeze
606         before a message is actually thawed.
607
608         :returns: STATUS.SUCCESS if the message was successfully frozen.
609                   Raises an exception otherwise.
610         :exception: :exc:`NotmuchError`. They have the following meaning:
611
612                    STATUS.UNBALANCED_FREEZE_THAW
613                      An attempt was made to thaw an unfrozen message. 
614                      That is, there have been an unbalanced number of calls 
615                      to :meth:`freeze` and :meth:`thaw`.
616                    STATUS.NOT_INITIALIZED
617                      The message has not been initialized.
618         """
619         if self._msg is None:
620             raise NotmuchError(STATUS.NOT_INITIALIZED)
621  
622         status = nmlib.notmuch_message_thaw(self._msg)
623
624         if STATUS.SUCCESS == status:
625             # return on success
626             return status
627
628         raise NotmuchError(status)
629
630
631     def is_match(self):
632         """(Not implemented)"""
633         return self.get_flag(Message.FLAG.MATCH)
634
635     def __str__(self):
636         """A message() is represented by a 1-line summary"""
637         msg = {}
638         msg['from'] = self.get_header('from')
639         msg['tags'] = str(self.get_tags())
640         msg['date'] = date.fromtimestamp(self.get_date())
641         replies = self.get_replies()
642         msg['replies'] = len(replies) if replies is not None else -1
643         return "%(from)s (%(date)s) (%(tags)s) (%(replies)d) replies" % (msg)
644
645
646     def get_message_parts(self):
647         """Output like notmuch show"""
648         fp = open(self.get_filename())
649         email_msg = email.message_from_file(fp)
650         fp.close()
651
652         out = []
653         for msg in email_msg.walk():
654             if not msg.is_multipart():
655                 out.append(msg)
656         return out
657
658     def get_part(self, num):
659         """Returns the nth message body part"""
660         parts = self.get_message_parts()
661         if (num <= 0 or num > len(parts)):
662             return ""
663         else:
664             out_part = parts[(num - 1)]
665             return out_part.get_payload(decode=True)
666
667     def format_message_internal(self):
668         """Create an internal representation of the message parts,
669         which can easily be output to json, text, or another output
670         format. The argument match tells whether this matched a
671         query."""
672         output = {}
673         output["id"] = self.get_message_id()
674         output["match"] = self.is_match()
675         output["filename"] = self.get_filename()
676         output["tags"] = list(self.get_tags())
677
678         headers = {}
679         for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
680             headers[h] = self.get_header(h)
681         output["headers"] = headers
682
683         body = []
684         parts = self.get_message_parts()
685         for i in xrange(len(parts)):
686             msg = parts[i]
687             part_dict = {}
688             part_dict["id"] = i + 1
689             # We'll be using this is a lot, so let's just get it once.
690             cont_type = msg.get_content_type()
691             part_dict["content-type"] = cont_type
692             # NOTE:
693             # Now we emulate the current behaviour, where it ignores
694             # the html if there's a text representation. 
695             #
696             # This is being worked on, but it will be easier to fix
697             # here in the future than to end up with another
698             # incompatible solution.
699             disposition = msg["Content-Disposition"]
700             if disposition and disposition.lower().startswith("attachment"):
701                 part_dict["filename"] = msg.get_filename()
702             else:
703                 if cont_type.lower() == "text/plain":
704                     part_dict["content"] = msg.get_payload()
705                 elif (cont_type.lower() == "text/html" and 
706                       i == 0):
707                     part_dict["content"] = msg.get_payload()
708         body.append(part_dict)
709
710         output["body"] = body
711
712         return output
713
714     def format_message_as_json(self, indent=0):
715         """Outputs the message as json. This is essentially the same
716         as python's dict format, but we run it through, just so we
717         don't have to worry about the details."""
718         return json.dumps(self.format_message_internal())
719
720     def format_message_as_text(self, indent=0):
721         """Outputs it in the old-fashioned notmuch text form. Will be
722         easy to change to a new format when the format changes."""
723
724         format = self.format_message_internal()
725         output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
726                  % (format['id'], indent, format['match'], format['filename'])
727         output += "\n\fheader{"
728
729         #Todo: this date is supposed to be prettified, as in the index.
730         output += "\n%s (%s) (" % (format["headers"]["from"],
731                                    format["headers"]["date"])
732         output += ", ".join(format["tags"])
733         output += ")"
734
735         output += "\nSubject: %s" % format["headers"]["subject"]
736         output += "\nFrom: %s" % format["headers"]["from"]
737         output += "\nTo: %s" % format["headers"]["to"]
738         if format["headers"]["cc"]:
739             output += "\nCc: %s" % format["headers"]["cc"]
740         if format["headers"]["bcc"]:
741             output += "\nBcc: %s" % format["headers"]["bcc"]
742         output += "\nDate: %s" % format["headers"]["date"]
743         output += "\n\fheader}"
744
745         output += "\n\fbody{"
746
747         parts = format["body"]
748         parts.sort(key=lambda(p): p["id"])
749         for p in parts:
750             if not p.has_key("filename"):
751                 output += "\n\fpart{ "
752                 output += "ID: %d, Content-type: %s\n" % (p["id"], 
753                                                          p["content_type"])
754                 if p.has_key("content"):
755                     output += "\n%s\n" % p["content"]
756                 else:
757                     output += "Non-text part: %s\n" % p["content_type"]
758                     output += "\n\fpart}"                    
759             else:
760                 output += "\n\fattachment{ "
761                 output += "ID: %d, Content-type:%s\n" % (p["id"], 
762                                                          p["content_type"])
763                 output += "Attachment: %s\n" % p["filename"]
764                 output += "\n\fattachment}\n"
765
766         output += "\n\fbody}\n"
767         output += "\n\fmessage}"
768
769         return output
770
771     def __del__(self):
772         """Close and free the notmuch Message"""
773         if self._msg is not None:
774             nmlib.notmuch_message_destroy (self._msg)