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