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