]> git.notmuchmail.org Git - notmuch/blob - bindings/python/notmuch/message.py
python: deprecate code formatting messages as text and json
[notmuch] / bindings / python / notmuch / message.py
1 """
2 This file is part of notmuch.
3
4 Notmuch is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License as published by the
6 Free Software Foundation, either version 3 of the License, or (at your
7 option) any later version.
8
9 Notmuch is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with notmuch.  If not, see <http://www.gnu.org/licenses/>.
16
17 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>
18                Jesse Rosenthal <jrosenthal@jhu.edu>
19 """
20
21
22 from ctypes import c_char_p, c_long, c_uint, c_int
23 from datetime import date
24 from .globals import (
25     nmlib,
26     Enum,
27     _str,
28     Python3StringMixIn,
29     NotmuchTagsP,
30     NotmuchMessageP,
31     NotmuchMessagesP,
32     NotmuchFilenamesP,
33 )
34 from .errors import (
35     STATUS,
36     NotmuchError,
37     NullPointerError,
38     NotInitializedError,
39 )
40 from .tag import Tags
41 from .filenames import Filenames
42
43 import email
44 try:
45     import simplejson as json
46 except ImportError:
47     import json
48
49
50 class Message(Python3StringMixIn):
51     """Represents a single Email message
52
53     Technically, this wraps the underlying *notmuch_message_t*
54     structure. A user will usually not create these objects themselves
55     but get them as search results.
56
57     As it implements :meth:`__cmp__`, it is possible to compare two
58     :class:`Message`\s using `if msg1 == msg2: ...`.
59     """
60
61     """notmuch_message_get_filename (notmuch_message_t *message)"""
62     _get_filename = nmlib.notmuch_message_get_filename
63     _get_filename.argtypes = [NotmuchMessageP]
64     _get_filename.restype = c_char_p
65
66     """return all filenames for a message"""
67     _get_filenames = nmlib.notmuch_message_get_filenames
68     _get_filenames.argtypes = [NotmuchMessageP]
69     _get_filenames.restype = NotmuchFilenamesP
70
71     """notmuch_message_get_flag"""
72     _get_flag = nmlib.notmuch_message_get_flag
73     _get_flag.argtypes = [NotmuchMessageP, c_uint]
74     _get_flag.restype = bool
75
76     """notmuch_message_set_flag"""
77     _set_flag = nmlib.notmuch_message_set_flag
78     _set_flag.argtypes = [NotmuchMessageP, c_uint, c_int]
79     _set_flag.restype = None
80
81     """notmuch_message_get_message_id (notmuch_message_t *message)"""
82     _get_message_id = nmlib.notmuch_message_get_message_id
83     _get_message_id.argtypes = [NotmuchMessageP]
84     _get_message_id.restype = c_char_p
85
86     """notmuch_message_get_thread_id"""
87     _get_thread_id = nmlib.notmuch_message_get_thread_id
88     _get_thread_id.argtypes = [NotmuchMessageP]
89     _get_thread_id.restype = c_char_p
90
91     """notmuch_message_get_replies"""
92     _get_replies = nmlib.notmuch_message_get_replies
93     _get_replies.argtypes = [NotmuchMessageP]
94     _get_replies.restype = NotmuchMessagesP
95
96     """notmuch_message_get_tags (notmuch_message_t *message)"""
97     _get_tags = nmlib.notmuch_message_get_tags
98     _get_tags.argtypes = [NotmuchMessageP]
99     _get_tags.restype = NotmuchTagsP
100
101     _get_date = nmlib.notmuch_message_get_date
102     _get_date.argtypes = [NotmuchMessageP]
103     _get_date.restype = c_long
104
105     _get_header = nmlib.notmuch_message_get_header
106     _get_header.argtypes = [NotmuchMessageP, c_char_p]
107     _get_header.restype = c_char_p
108
109     """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
110     _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
111     _tags_to_maildir_flags.argtypes = [NotmuchMessageP]
112     _tags_to_maildir_flags.restype = c_int
113
114     """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
115     _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
116     _maildir_flags_to_tags.argtypes = [NotmuchMessageP]
117     _maildir_flags_to_tags.restype = c_int
118
119     #Constants: Flags that can be set/get with set_flag
120     FLAG = Enum(['MATCH'])
121
122     def __init__(self, msg_p, parent=None):
123         """
124         :param msg_p: A pointer to an internal notmuch_message_t
125             Structure.  If it is `None`, we will raise an
126             :exc:`NullPointerError`.
127
128         :param parent: A 'parent' object is passed which this message is
129               derived from. We save a reference to it, so we can
130               automatically delete the parent object once all derived
131               objects are dead.
132         """
133         if not msg_p:
134             raise NullPointerError()
135         self._msg = msg_p
136         #keep reference to parent, so we keep it alive
137         self._parent = parent
138
139     def get_message_id(self):
140         """Returns the message ID
141
142         :returns: String with a message ID
143         :raises: :exc:`NotInitializedError` if the message
144                     is not initialized.
145         """
146         if not self._msg:
147             raise NotInitializedError()
148         return Message._get_message_id(self._msg).decode('utf-8', 'ignore')
149
150     def get_thread_id(self):
151         """Returns the thread ID
152
153         The returned string belongs to 'message' will only be valid for as
154         long as the message is valid.
155
156         This function will not return `None` since Notmuch ensures that every
157         message belongs to a single thread.
158
159         :returns: String with a thread ID
160         :raises: :exc:`NotInitializedError` if the message
161                     is not initialized.
162         """
163         if not self._msg:
164             raise NotInitializedError()
165
166         return Message._get_thread_id(self._msg).decode('utf-8', 'ignore')
167
168     def get_replies(self):
169         """Gets all direct replies to this message as :class:`Messages`
170         iterator
171
172         .. note::
173
174             This call only makes sense if 'message' was ultimately obtained from
175             a :class:`Thread` object, (such as by coming directly from the
176             result of calling :meth:`Thread.get_toplevel_messages` or by any
177             number of subsequent calls to :meth:`get_replies`). If this message
178             was obtained through some non-thread means, (such as by a call to
179             :meth:`Query.search_messages`), then this function will return
180             an empty Messages iterator.
181
182         :returns: :class:`Messages`.
183         :raises: :exc:`NotInitializedError` if the message
184                     is not initialized.
185         """
186         if not self._msg:
187             raise NotInitializedError()
188
189         msgs_p = Message._get_replies(self._msg)
190
191         from .messages import Messages, EmptyMessagesResult
192
193         if not msgs_p:
194             return EmptyMessagesResult(self)
195
196         return Messages(msgs_p, self)
197
198     def get_date(self):
199         """Returns time_t of the message date
200
201         For the original textual representation of the Date header from the
202         message call notmuch_message_get_header() with a header value of
203         "date".
204
205         :returns: A time_t timestamp.
206         :rtype: c_unit64
207         :raises: :exc:`NotInitializedError` if the message
208                     is not initialized.
209         """
210         if not self._msg:
211             raise NotInitializedError()
212         return Message._get_date(self._msg)
213
214     def get_header(self, header):
215         """Get the value of the specified header.
216
217         The value will be read from the actual message file, not from
218         the notmuch database. The header name is case insensitive.
219
220         Returns an empty string ("") if the message does not contain a
221         header line matching 'header'.
222
223         :param header: The name of the header to be retrieved.
224                        It is not case-sensitive.
225         :type header: str
226         :returns: The header value as string
227         :raises: :exc:`NotInitializedError` if the message is not
228                  initialized
229         :raises: :exc:`NullPointerError` if any error occured
230         """
231         if not self._msg:
232             raise NotInitializedError()
233
234         #Returns NULL if any error occurs.
235         header = Message._get_header(self._msg, _str(header))
236         if header == None:
237             raise NullPointerError()
238         return header.decode('UTF-8', 'ignore')
239
240     def get_filename(self):
241         """Returns the file path of the message file
242
243         :returns: Absolute file path & name of the message file
244         :raises: :exc:`NotInitializedError` if the message
245               is not initialized.
246         """
247         if not self._msg:
248             raise NotInitializedError()
249         return Message._get_filename(self._msg).decode('utf-8', 'ignore')
250
251     def get_filenames(self):
252         """Get all filenames for the email corresponding to 'message'
253
254         Returns a Filenames() generator with all absolute filepaths for
255         messages recorded to have the same Message-ID. These files must
256         not necessarily have identical content."""
257         if not self._msg:
258             raise NotInitializedError()
259
260         files_p = Message._get_filenames(self._msg)
261
262         return Filenames(files_p, self)
263
264     def get_flag(self, flag):
265         """Checks whether a specific flag is set for this message
266
267         The method :meth:`Query.search_threads` sets
268         *Message.FLAG.MATCH* for those messages that match the
269         query. This method allows us to get the value of this flag.
270
271         :param flag: One of the :attr:`Message.FLAG` values (currently only
272                      *Message.FLAG.MATCH*
273         :returns: An unsigned int (0/1), indicating whether the flag is set.
274         :raises: :exc:`NotInitializedError` if the message
275               is not initialized.
276         """
277         if not self._msg:
278             raise NotInitializedError()
279         return Message._get_flag(self._msg, flag)
280
281     def set_flag(self, flag, value):
282         """Sets/Unsets a specific flag for this message
283
284         :param flag: One of the :attr:`Message.FLAG` values (currently only
285                      *Message.FLAG.MATCH*
286         :param value: A bool indicating whether to set or unset the flag.
287
288         :raises: :exc:`NotInitializedError` if the message
289               is not initialized.
290         """
291         if not self._msg:
292             raise NotInitializedError()
293         self._set_flag(self._msg, flag, value)
294
295     def get_tags(self):
296         """Returns the message tags
297
298         :returns: A :class:`Tags` iterator.
299         :raises: :exc:`NotInitializedError` if the message is not
300                  initialized
301         :raises: :exc:`NullPointerError` if any error occured
302         """
303         if not self._msg:
304             raise NotInitializedError()
305
306         tags_p = Message._get_tags(self._msg)
307         if tags_p == None:
308             raise NullPointerError()
309         return Tags(tags_p, self)
310
311     _add_tag = nmlib.notmuch_message_add_tag
312     _add_tag.argtypes = [NotmuchMessageP, c_char_p]
313     _add_tag.restype = c_uint
314
315     def add_tag(self, tag, sync_maildir_flags=False):
316         """Adds a tag to the given message
317
318         Adds a tag to the current message. The maximal tag length is defined in
319         the notmuch library and is currently 200 bytes.
320
321         :param tag: String with a 'tag' to be added.
322
323         :param sync_maildir_flags: If notmuch configuration is set to do
324             this, add maildir flags corresponding to notmuch tags. See
325             underlying method :meth:`tags_to_maildir_flags`. Use False
326             if you want to add/remove many tags on a message without
327             having to physically rename the file every time. Do note,
328             that this will do nothing when a message is frozen, as tag
329             changes will not be committed to the database yet.
330
331         :returns: STATUS.SUCCESS if the tag was successfully added.
332                   Raises an exception otherwise.
333         :raises: :exc:`NullPointerError` if the `tag` argument is NULL
334         :raises: :exc:`TagTooLongError` if the length of `tag` exceeds
335                  Message.NOTMUCH_TAG_MAX)
336         :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
337                  in read-only mode so message cannot be modified
338         :raises: :exc:`NotInitializedError` if message has not been
339                  initialized
340         """
341         if not self._msg:
342             raise NotInitializedError()
343
344         status = self._add_tag(self._msg, _str(tag))
345
346         # bail out on failure
347         if status != STATUS.SUCCESS:
348             raise NotmuchError(status)
349
350         if sync_maildir_flags:
351             self.tags_to_maildir_flags()
352         return STATUS.SUCCESS
353
354     _remove_tag = nmlib.notmuch_message_remove_tag
355     _remove_tag.argtypes = [NotmuchMessageP, c_char_p]
356     _remove_tag.restype = c_uint
357
358     def remove_tag(self, tag, sync_maildir_flags=False):
359         """Removes a tag from the given message
360
361         If the message has no such tag, this is a non-operation and
362         will report success anyway.
363
364         :param tag: String with a 'tag' to be removed.
365         :param sync_maildir_flags: If notmuch configuration is set to do
366             this, add maildir flags corresponding to notmuch tags. See
367             underlying method :meth:`tags_to_maildir_flags`. Use False
368             if you want to add/remove many tags on a message without
369             having to physically rename the file every time. Do note,
370             that this will do nothing when a message is frozen, as tag
371             changes will not be committed to the database yet.
372
373         :returns: STATUS.SUCCESS if the tag was successfully removed or if
374                   the message had no such tag.
375                   Raises an exception otherwise.
376         :raises: :exc:`NullPointerError` if the `tag` argument is NULL
377         :raises: :exc:`TagTooLongError` if the length of `tag` exceeds
378                  Message.NOTMUCH_TAG_MAX)
379         :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
380                  in read-only mode so message cannot be modified
381         :raises: :exc:`NotInitializedError` if message has not been
382                  initialized
383         """
384         if not self._msg:
385             raise NotInitializedError()
386
387         status = self._remove_tag(self._msg, _str(tag))
388         # bail out on error
389         if status != STATUS.SUCCESS:
390             raise NotmuchError(status)
391
392         if sync_maildir_flags:
393             self.tags_to_maildir_flags()
394         return STATUS.SUCCESS
395
396     _remove_all_tags = nmlib.notmuch_message_remove_all_tags
397     _remove_all_tags.argtypes = [NotmuchMessageP]
398     _remove_all_tags.restype = c_uint
399
400     def remove_all_tags(self, sync_maildir_flags=False):
401         """Removes all tags from the given message.
402
403         See :meth:`freeze` for an example showing how to safely
404         replace tag values.
405
406
407         :param sync_maildir_flags: If notmuch configuration is set to do
408             this, add maildir flags corresponding to notmuch tags. See
409             :meth:`tags_to_maildir_flags`. Use False if you want to
410             add/remove many tags on a message without having to
411             physically rename the file every time. Do note, that this
412             will do nothing when a message is frozen, as tag changes
413             will not be committed to the database yet.
414
415         :returns: STATUS.SUCCESS if the tags were successfully removed.
416                   Raises an exception otherwise.
417         :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
418                  in read-only mode so message cannot be modified
419         :raises: :exc:`NotInitializedError` if message has not been
420                  initialized
421         """
422         if not self._msg:
423             raise NotInitializedError()
424
425         status = self._remove_all_tags(self._msg)
426
427         # bail out on error
428         if status != STATUS.SUCCESS:
429             raise NotmuchError(status)
430
431         if sync_maildir_flags:
432             self.tags_to_maildir_flags()
433         return STATUS.SUCCESS
434
435     _freeze = nmlib.notmuch_message_freeze
436     _freeze.argtypes = [NotmuchMessageP]
437     _freeze.restype = c_uint
438
439     def freeze(self):
440         """Freezes the current state of 'message' within the database
441
442         This means that changes to the message state, (via :meth:`add_tag`,
443         :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
444         committed to the database until the message is :meth:`thaw` ed.
445
446         Multiple calls to freeze/thaw are valid and these calls will
447         "stack". That is there must be as many calls to thaw as to freeze
448         before a message is actually thawed.
449
450         The ability to do freeze/thaw allows for safe transactions to
451         change tag values. For example, explicitly setting a message to
452         have a given set of tags might look like this::
453
454           msg.freeze()
455           msg.remove_all_tags(False)
456           for tag in new_tags:
457               msg.add_tag(tag, False)
458           msg.thaw()
459           msg.tags_to_maildir_flags()
460
461         With freeze/thaw used like this, the message in the database is
462         guaranteed to have either the full set of original tag values, or
463         the full set of new tag values, but nothing in between.
464
465         Imagine the example above without freeze/thaw and the operation
466         somehow getting interrupted. This could result in the message being
467         left with no tags if the interruption happened after
468         :meth:`remove_all_tags` but before :meth:`add_tag`.
469
470         :returns: STATUS.SUCCESS if the message was successfully frozen.
471                   Raises an exception otherwise.
472         :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
473                  in read-only mode so message cannot be modified
474         :raises: :exc:`NotInitializedError` if message has not been
475                  initialized
476         """
477         if not self._msg:
478             raise NotInitializedError()
479
480         status = self._freeze(self._msg)
481
482         if STATUS.SUCCESS == status:
483             # return on success
484             return status
485
486         raise NotmuchError(status)
487
488     _thaw = nmlib.notmuch_message_thaw
489     _thaw.argtypes = [NotmuchMessageP]
490     _thaw.restype = c_uint
491
492     def thaw(self):
493         """Thaws the current 'message'
494
495         Thaw the current 'message', synchronizing any changes that may have
496         occurred while 'message' was frozen into the notmuch database.
497
498         See :meth:`freeze` for an example of how to use this
499         function to safely provide tag changes.
500
501         Multiple calls to freeze/thaw are valid and these calls with
502         "stack". That is there must be as many calls to thaw as to freeze
503         before a message is actually thawed.
504
505         :returns: STATUS.SUCCESS if the message was successfully frozen.
506                   Raises an exception otherwise.
507         :raises: :exc:`UnbalancedFreezeThawError` if an attempt was made
508                  to thaw an unfrozen message. That is, there have been
509                  an unbalanced number of calls to :meth:`freeze` and
510                  :meth:`thaw`.
511         :raises: :exc:`NotInitializedError` if message has not been
512                  initialized
513         """
514         if not self._msg:
515             raise NotInitializedError()
516
517         status = self._thaw(self._msg)
518
519         if STATUS.SUCCESS == status:
520             # return on success
521             return status
522
523         raise NotmuchError(status)
524
525     def is_match(self):
526         """(Not implemented)"""
527         return self.get_flag(Message.FLAG.MATCH)
528
529     def tags_to_maildir_flags(self):
530         """Synchronize notmuch tags to file Maildir flags
531
532               'D' if the message has the "draft" tag
533               'F' if the message has the "flagged" tag
534               'P' if the message has the "passed" tag
535               'R' if the message has the "replied" tag
536               'S' if the message does not have the "unread" tag
537
538         Any existing flags unmentioned in the list above will be
539         preserved in the renaming.
540
541         Also, if this filename is in a directory named "new", rename it
542         to be within the neighboring directory named "cur".
543
544         Do note that calling this method while a message is frozen might
545         not work yet, as the modified tags have not been committed yet
546         to the database.
547
548         :returns: a :class:`STATUS` value. In short, you want to see
549             notmuch.STATUS.SUCCESS here. See there for details."""
550         if not self._msg:
551             raise NotInitializedError()
552         return Message._tags_to_maildir_flags(self._msg)
553
554     def maildir_flags_to_tags(self):
555         """Synchronize file Maildir flags to notmuch tags
556
557             Flag    Action if present
558             ----    -----------------
559             'D'     Adds the "draft" tag to the message
560             'F'     Adds the "flagged" tag to the message
561             'P'     Adds the "passed" tag to the message
562             'R'     Adds the "replied" tag to the message
563             'S'     Removes the "unread" tag from the message
564
565         For each flag that is not present, the opposite action
566         (add/remove) is performed for the corresponding tags.  If there
567         are multiple filenames associated with this message, the flag is
568         considered present if it appears in one or more filenames. (That
569         is, the flags from the multiple filenames are combined with the
570         logical OR operator.)
571
572         As a convenience, you can set the sync_maildir_flags parameter in
573         :meth:`Database.add_message` to implicitly call this.
574
575         :returns: a :class:`STATUS`. In short, you want to see
576             notmuch.STATUS.SUCCESS here. See there for details."""
577         if not self._msg:
578             raise NotInitializedError()
579         return Message._tags_to_maildir_flags(self._msg)
580
581     def __repr__(self):
582         """Represent a Message() object by str()"""
583         return self.__str__()
584
585     def __unicode__(self):
586         format = "%s (%s) (%s)"
587         return format % (self.get_header('from'),
588                          self.get_tags(),
589                          date.fromtimestamp(self.get_date()),
590                         )
591
592     def get_message_parts(self):
593         """Output like notmuch show"""
594         fp = open(self.get_filename())
595         email_msg = email.message_from_file(fp)
596         fp.close()
597
598         out = []
599         for msg in email_msg.walk():
600             if not msg.is_multipart():
601                 out.append(msg)
602         return out
603
604     def get_part(self, num):
605         """Returns the nth message body part"""
606         parts = self.get_message_parts()
607         if (num <= 0 or num > len(parts)):
608             return ""
609         else:
610             out_part = parts[(num - 1)]
611             return out_part.get_payload(decode=True)
612
613     def format_message_internal(self):
614         """Create an internal representation of the message parts,
615         which can easily be output to json, text, or another output
616         format. The argument match tells whether this matched a
617         query.
618
619         .. deprecated:: 0.13
620                         This code adds functionality at the python
621                         level that is unlikely to be useful for
622                         anyone. Furthermore the python bindings strive
623                         to be a thin wrapper around libnotmuch, so
624                         this code will be removed in notmuch 0.14.
625         """
626         output = {}
627         output["id"] = self.get_message_id()
628         output["match"] = self.is_match()
629         output["filename"] = self.get_filename()
630         output["tags"] = list(self.get_tags())
631
632         headers = {}
633         for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
634             headers[h] = self.get_header(h)
635         output["headers"] = headers
636
637         body = []
638         parts = self.get_message_parts()
639         for i in xrange(len(parts)):
640             msg = parts[i]
641             part_dict = {}
642             part_dict["id"] = i + 1
643             # We'll be using this is a lot, so let's just get it once.
644             cont_type = msg.get_content_type()
645             part_dict["content-type"] = cont_type
646             # NOTE:
647             # Now we emulate the current behaviour, where it ignores
648             # the html if there's a text representation.
649             #
650             # This is being worked on, but it will be easier to fix
651             # here in the future than to end up with another
652             # incompatible solution.
653             disposition = msg["Content-Disposition"]
654             if disposition and disposition.lower().startswith("attachment"):
655                 part_dict["filename"] = msg.get_filename()
656             else:
657                 if cont_type.lower() == "text/plain":
658                     part_dict["content"] = msg.get_payload()
659                 elif (cont_type.lower() == "text/html" and
660                       i == 0):
661                     part_dict["content"] = msg.get_payload()
662             body.append(part_dict)
663
664         output["body"] = body
665
666         return output
667
668     def format_message_as_json(self, indent=0):
669         """Outputs the message as json. This is essentially the same
670         as python's dict format, but we run it through, just so we
671         don't have to worry about the details.
672
673         .. deprecated:: 0.13
674                         This code adds functionality at the python
675                         level that is unlikely to be useful for
676                         anyone. Furthermore the python bindings strive
677                         to be a thin wrapper around libnotmuch, so
678                         this code will be removed in notmuch 0.14.
679         """
680         return json.dumps(self.format_message_internal())
681
682     def format_message_as_text(self, indent=0):
683         """Outputs it in the old-fashioned notmuch text form. Will be
684         easy to change to a new format when the format changes.
685
686         .. deprecated:: 0.13
687                         This code adds functionality at the python
688                         level that is unlikely to be useful for
689                         anyone. Furthermore the python bindings strive
690                         to be a thin wrapper around libnotmuch, so
691                         this code will be removed in notmuch 0.14.
692         """
693
694
695         format = self.format_message_internal()
696         output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
697                  % (format['id'], indent, format['match'], format['filename'])
698         output += "\n\fheader{"
699
700         #Todo: this date is supposed to be prettified, as in the index.
701         output += "\n%s (%s) (" % (format["headers"]["From"],
702                                    format["headers"]["Date"])
703         output += ", ".join(format["tags"])
704         output += ")"
705
706         output += "\nSubject: %s" % format["headers"]["Subject"]
707         output += "\nFrom: %s" % format["headers"]["From"]
708         output += "\nTo: %s" % format["headers"]["To"]
709         if format["headers"]["Cc"]:
710             output += "\nCc: %s" % format["headers"]["Cc"]
711         if format["headers"]["Bcc"]:
712             output += "\nBcc: %s" % format["headers"]["Bcc"]
713         output += "\nDate: %s" % format["headers"]["Date"]
714         output += "\n\fheader}"
715
716         output += "\n\fbody{"
717
718         parts = format["body"]
719         parts.sort(key=lambda x: x['id'])
720         for p in parts:
721             if not "filename" in p:
722                 output += "\n\fpart{ "
723                 output += "ID: %d, Content-type: %s\n" % (p["id"],
724                                                           p["content-type"])
725                 if "content" in p:
726                     output += "\n%s\n" % p["content"]
727                 else:
728                     output += "Non-text part: %s\n" % p["content-type"]
729                     output += "\n\fpart}"
730             else:
731                 output += "\n\fattachment{ "
732                 output += "ID: %d, Content-type:%s\n" % (p["id"],
733                                                          p["content-type"])
734                 output += "Attachment: %s\n" % p["filename"]
735                 output += "\n\fattachment}\n"
736
737         output += "\n\fbody}\n"
738         output += "\n\fmessage}"
739
740         return output
741
742     def __hash__(self):
743         """Implement hash(), so we can use Message() sets"""
744         file = self.get_filename()
745         if not file:
746             return None
747         return hash(file)
748
749     def __cmp__(self, other):
750         """Implement cmp(), so we can compare Message()s
751
752         2 messages are considered equal if they point to the same
753         Message-Id and if they point to the same file names. If 2
754         Messages derive from different queries where some files have
755         been added or removed, the same messages would not be considered
756         equal (as they do not point to the same set of files
757         any more)."""
758         res = cmp(self.get_message_id(), other.get_message_id())
759         if res:
760             res = cmp(list(self.get_filenames()), list(other.get_filenames()))
761         return res
762
763     _destroy = nmlib.notmuch_message_destroy
764     _destroy.argtypes = [NotmuchMessageP]
765     _destroy.restype = None
766
767     def __del__(self):
768         """Close and free the notmuch Message"""
769         if self._msg:
770             self._destroy(self._msg)