]> git.notmuchmail.org Git - notmuch/blob - bindings/python/notmuch/message.py
bf0c4da488a0d240066dfcde2a5ce799a21042eb
[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 notmuch.globals import (nmlib, STATUS, NotmuchError, Enum, _str,
25     NotmuchTagsP, NotmuchMessagesP, NotmuchMessageP, NotmuchFilenamesP)
26 from notmuch.tag import Tags
27 from notmuch.filename import Filenames
28 import sys
29 import email
30 try:
31     import simplejson as json
32 except ImportError:
33     import json
34
35
36 class Messages(object):
37     """Represents a list of notmuch messages
38
39     This object provides an iterator over a list of notmuch messages
40     (Technically, it provides a wrapper for the underlying
41     *notmuch_messages_t* structure). Do note that the underlying library
42     only provides a one-time iterator (it cannot reset the iterator to
43     the start). Thus iterating over the function will "exhaust" the list
44     of messages, and a subsequent iteration attempt will raise a
45     :exc:`NotmuchError` STATUS.NOT_INITIALIZED. If you need to
46     re-iterate over a list of messages you will need to retrieve a new
47     :class:`Messages` object or cache your :class:`Message`\s in a list
48     via::
49
50        msglist = list(msgs)
51
52     You can store and reuse the single :class:`Message` objects as often
53     as you want as long as you keep the parent :class:`Messages` object
54     around. (Due to hierarchical memory allocation, all derived
55     :class:`Message` objects will be invalid when we delete the parent
56     :class:`Messages` object, even if it was already exhausted.) So
57     this works::
58
59       db   = Database()
60       msgs = Query(db,'').search_messages() #get a Messages() object
61       msglist = list(msgs)
62
63       # msgs is "exhausted" now and msgs.next() will raise an exception.
64       # However it will be kept alive until all retrieved Message()
65       # objects are also deleted. If you do e.g. an explicit del(msgs)
66       # here, the following lines would fail.
67
68       # You can reiterate over *msglist* however as often as you want.
69       # It is simply a list with :class:`Message`s.
70
71       print (msglist[0].get_filename())
72       print (msglist[1].get_filename())
73       print (msglist[0].get_message_id())
74
75
76     As :class:`Message` implements both __hash__() and __cmp__(), it is
77     possible to make sets out of :class:`Messages` and use set
78     arithmetic (this happens in python and will of course be *much*
79     slower than redoing a proper query with the appropriate filters::
80
81         s1, s2 = set(msgs1), set(msgs2)
82         s.union(s2)
83         s1 -= s2
84         ...
85
86     Be careful when using set arithmetic between message sets derived
87     from different Databases (ie the same database reopened after
88     messages have changed). If messages have added or removed associated
89     files in the meantime, it is possible that the same message would be
90     considered as a different object (as it points to a different file).
91     """
92
93     #notmuch_messages_get
94     _get = nmlib.notmuch_messages_get
95     _get.argtypes = [NotmuchMessagesP]
96     _get.restype = NotmuchMessageP
97
98     _collect_tags = nmlib.notmuch_messages_collect_tags
99     _collect_tags.argtypes = [NotmuchMessagesP]
100     _collect_tags.restype = NotmuchTagsP
101
102     def __init__(self, msgs_p, parent=None):
103         """
104         :param msgs_p:  A pointer to an underlying *notmuch_messages_t*
105              structure. These are not publically exposed, so a user
106              will almost never instantiate a :class:`Messages` object
107              herself. They are usually handed back as a result,
108              e.g. in :meth:`Query.search_messages`.  *msgs_p* must be
109              valid, we will raise an :exc:`NotmuchError`
110              (STATUS.NULL_POINTER) if it is `None`.
111         :type msgs_p: :class:`ctypes.c_void_p`
112         :param parent: The parent object
113              (ie :class:`Query`) these tags are derived from. It saves
114              a reference to it, so we can automatically delete the db
115              object once all derived objects are dead.
116         :TODO: Make the iterator work more than once and cache the tags in
117                the Python object.(?)
118         """
119         if msgs_p is None:
120             raise NotmuchError(STATUS.NULL_POINTER)
121
122         self._msgs = msgs_p
123         #store parent, so we keep them alive as long as self  is alive
124         self._parent = parent
125
126     def collect_tags(self):
127         """Return the unique :class:`Tags` in the contained messages
128
129         :returns: :class:`Tags`
130         :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not init'ed
131
132         .. note::
133
134             :meth:`collect_tags` will iterate over the messages and therefore
135             will not allow further iterations.
136         """
137         if self._msgs is None:
138             raise NotmuchError(STATUS.NOT_INITIALIZED)
139
140         # collect all tags (returns NULL on error)
141         tags_p = Messages._collect_tags(self._msgs)
142         #reset _msgs as we iterated over it and can do so only once
143         self._msgs = None
144
145         if tags_p == None:
146             raise NotmuchError(STATUS.NULL_POINTER)
147         return Tags(tags_p, self)
148
149     def __iter__(self):
150         """ Make Messages an iterator """
151         return self
152
153     _valid = nmlib.notmuch_messages_valid
154     _valid.argtypes = [NotmuchMessagesP]
155     _valid.restype = bool
156
157     _move_to_next = nmlib.notmuch_messages_move_to_next
158     _move_to_next.argtypes = [NotmuchMessagesP]
159     _move_to_next.restype = None
160
161     def __next__(self):
162         if self._msgs is None:
163             raise NotmuchError(STATUS.NOT_INITIALIZED)
164
165         if not self._valid(self._msgs):
166             self._msgs = None
167             raise StopIteration
168
169         msg = Message(Messages._get(self._msgs), self)
170         self._move_to_next(self._msgs)
171         return msg
172     next = __next__ # python2.x iterator protocol compatibility
173
174     def __nonzero__(self):
175         """
176         :return: True if there is at least one more thread in the
177             Iterator, False if not."""
178         return self._msgs is not None and \
179             self._valid(self._msgs) > 0
180
181     _destroy = nmlib.notmuch_messages_destroy
182     _destroy.argtypes = [NotmuchMessagesP]
183     _destroy.restype = None
184
185     def __del__(self):
186         """Close and free the notmuch Messages"""
187         if self._msgs is not None:
188             self._destroy(self._msgs)
189
190     def print_messages(self, format, indent=0, entire_thread=False):
191         """Outputs messages as needed for 'notmuch show' to sys.stdout
192
193         :param format: A string of either 'text' or 'json'.
194         :param indent: A number indicating the reply depth of these messages.
195         :param entire_thread: A bool, indicating whether we want to output
196                        whole threads or only the matching messages.
197         """
198         if format.lower() == "text":
199             set_start = ""
200             set_end = ""
201             set_sep = ""
202         elif format.lower() == "json":
203             set_start = "["
204             set_end = "]"
205             set_sep = ", "
206         else:
207             raise TypeError("format must be either 'text' or 'json'")
208
209         first_set = True
210
211         sys.stdout.write(set_start)
212
213         # iterate through all toplevel messages in this thread
214         for msg in self:
215             # if not msg:
216             #     break
217             if not first_set:
218                 sys.stdout.write(set_sep)
219             first_set = False
220
221             sys.stdout.write(set_start)
222             match = msg.is_match()
223             next_indent = indent
224
225             if (match or entire_thread):
226                 if format.lower() == "text":
227                     sys.stdout.write(msg.format_message_as_text(indent))
228                 else:
229                     sys.stdout.write(msg.format_message_as_json(indent))
230                 next_indent = indent + 1
231
232             # get replies and print them also out (if there are any)
233             replies = msg.get_replies()
234             if not replies is None:
235                 sys.stdout.write(set_sep)
236                 replies.print_messages(format, next_indent, entire_thread)
237
238             sys.stdout.write(set_end)
239         sys.stdout.write(set_end)
240
241
242 class Message(object):
243     """Represents a single Email message
244
245     Technically, this wraps the underlying *notmuch_message_t*
246     structure. A user will usually not create these objects themselves
247     but get them as search results.
248
249     As it implements :meth:`__cmp__`, it is possible to compare two
250     :class:`Message`\s using `if msg1 == msg2: ...`.
251     """
252
253     """notmuch_message_get_filename (notmuch_message_t *message)"""
254     _get_filename = nmlib.notmuch_message_get_filename
255     _get_filename.argtypes = [NotmuchMessageP]
256     _get_filename.restype = c_char_p
257
258     """return all filenames for a message"""
259     _get_filenames = nmlib.notmuch_message_get_filenames
260     _get_filenames.argtypes = [NotmuchMessageP]
261     _get_filenames.restype = NotmuchFilenamesP
262
263     """notmuch_message_get_flag"""
264     _get_flag = nmlib.notmuch_message_get_flag
265     _get_flag.argtypes = [NotmuchMessageP, c_uint]
266     _get_flag.restype = bool
267
268     """notmuch_message_set_flag"""
269     _set_flag = nmlib.notmuch_message_set_flag
270     _set_flag.argtypes = [NotmuchMessageP, c_uint, c_int]
271     _set_flag.restype = None
272
273     """notmuch_message_get_message_id (notmuch_message_t *message)"""
274     _get_message_id = nmlib.notmuch_message_get_message_id
275     _get_message_id.argtypes = [NotmuchMessageP]
276     _get_message_id.restype = c_char_p
277
278     """notmuch_message_get_thread_id"""
279     _get_thread_id = nmlib.notmuch_message_get_thread_id
280     _get_thread_id.argtypes = [NotmuchMessageP]
281     _get_thread_id.restype = c_char_p
282
283     """notmuch_message_get_replies"""
284     _get_replies = nmlib.notmuch_message_get_replies
285     _get_replies.argtypes = [NotmuchMessageP]
286     _get_replies.restype = NotmuchMessagesP
287
288     """notmuch_message_get_tags (notmuch_message_t *message)"""
289     _get_tags = nmlib.notmuch_message_get_tags
290     _get_tags.argtypes = [NotmuchMessageP]
291     _get_tags.restype = NotmuchTagsP
292
293     _get_date = nmlib.notmuch_message_get_date
294     _get_date.argtypes = [NotmuchMessageP]
295     _get_date.restype = c_long
296
297     _get_header = nmlib.notmuch_message_get_header
298     _get_header.argtypes = [NotmuchMessageP, c_char_p]
299     _get_header.restype = c_char_p
300
301     """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
302     _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
303     _tags_to_maildir_flags.argtypes = [NotmuchMessageP]
304     _tags_to_maildir_flags.restype = c_int
305
306     """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
307     _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
308     _maildir_flags_to_tags.argtypes = [NotmuchMessageP]
309     _maildir_flags_to_tags.restype = c_int
310
311     #Constants: Flags that can be set/get with set_flag
312     FLAG = Enum(['MATCH'])
313
314     def __init__(self, msg_p, parent=None):
315         """
316         :param msg_p: A pointer to an internal notmuch_message_t
317             Structure.  If it is `None`, we will raise an :exc:`NotmuchError`
318             STATUS.NULL_POINTER.
319
320         :param parent: A 'parent' object is passed which this message is
321               derived from. We save a reference to it, so we can
322               automatically delete the parent object once all derived
323               objects are dead.
324         """
325         if msg_p is None:
326             raise NotmuchError(STATUS.NULL_POINTER)
327         self._msg = msg_p
328         #keep reference to parent, so we keep it alive
329         self._parent = parent
330
331     def get_message_id(self):
332         """Returns the message ID
333
334         :returns: String with a message ID
335         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
336                     is not initialized.
337         """
338         if self._msg is None:
339             raise NotmuchError(STATUS.NOT_INITIALIZED)
340         return Message._get_message_id(self._msg)
341
342     def get_thread_id(self):
343         """Returns the thread ID
344
345         The returned string belongs to 'message' will only be valid for as
346         long as the message is valid.
347
348         This function will not return `None` since Notmuch ensures that every
349         message belongs to a single thread.
350
351         :returns: String with a thread ID
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
358         return Message._get_thread_id(self._msg)
359
360     def get_replies(self):
361         """Gets all direct replies to this message as :class:`Messages`
362         iterator
363
364         .. note::
365
366             This call only makes sense if 'message' was ultimately obtained from
367             a :class:`Thread` object, (such as by coming directly from the
368             result of calling :meth:`Thread.get_toplevel_messages` or by any
369             number of subsequent calls to :meth:`get_replies`). If this message
370             was obtained through some non-thread means, (such as by a call to
371             :meth:`Query.search_messages`), then this function will return
372             `None`.
373
374         :returns: :class:`Messages` or `None` if there are no replies to
375             this message.
376         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
377                     is not initialized.
378         """
379         if self._msg is None:
380             raise NotmuchError(STATUS.NOT_INITIALIZED)
381
382         msgs_p = Message._get_replies(self._msg)
383
384         if msgs_p is None:
385             return None
386
387         return Messages(msgs_p, self)
388
389     def get_date(self):
390         """Returns time_t of the message date
391
392         For the original textual representation of the Date header from the
393         message call notmuch_message_get_header() with a header value of
394         "date".
395
396         :returns: A time_t timestamp.
397         :rtype: c_unit64
398         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
399                     is not initialized.
400         """
401         if self._msg is None:
402             raise NotmuchError(STATUS.NOT_INITIALIZED)
403         return Message._get_date(self._msg)
404
405     def get_header(self, header):
406         """Get the value of the specified header.
407
408         The value will be read from the actual message file, not from
409         the notmuch database. The header name is case insensitive.
410
411         Returns an empty string ("") if the message does not contain a
412         header line matching 'header'.
413
414         :param header: The name of the header to be retrieved.
415                        It is not case-sensitive.
416         :type header: str
417         :returns: The header value as string
418         :exception: :exc:`NotmuchError`
419
420                     * STATUS.NOT_INITIALIZED if the message
421                       is not initialized.
422                     * STATUS.NULL_POINTER if any error occured.
423         """
424         if self._msg is None:
425             raise NotmuchError(STATUS.NOT_INITIALIZED)
426
427         #Returns NULL if any error occurs.
428         header = Message._get_header(self._msg, header)
429         if header == None:
430             raise NotmuchError(STATUS.NULL_POINTER)
431         return header.decode('UTF-8', errors='ignore')
432
433     def get_filename(self):
434         """Returns the file path of the message file
435
436         :returns: Absolute file path & name of the message file
437         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
438               is not initialized.
439         """
440         if self._msg is None:
441             raise NotmuchError(STATUS.NOT_INITIALIZED)
442         return Message._get_filename(self._msg)
443
444     def get_filenames(self):
445         """Get all filenames for the email corresponding to 'message'
446
447         Returns a Filenames() generator with all absolute filepaths for
448         messages recorded to have the same Message-ID. These files must
449         not necessarily have identical content."""
450         if self._msg is None:
451             raise NotmuchError(STATUS.NOT_INITIALIZED)
452
453         files_p = Message._get_filenames(self._msg)
454
455         return Filenames(files_p, self).as_generator()
456
457     def get_flag(self, flag):
458         """Checks whether a specific flag is set for this message
459
460         The method :meth:`Query.search_threads` sets
461         *Message.FLAG.MATCH* for those messages that match the
462         query. This method allows us to get the value of this flag.
463
464         :param flag: One of the :attr:`Message.FLAG` values (currently only
465                      *Message.FLAG.MATCH*
466         :returns: An unsigned int (0/1), indicating whether the flag is set.
467         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
468               is not initialized.
469         """
470         if self._msg is None:
471             raise NotmuchError(STATUS.NOT_INITIALIZED)
472         return Message._get_flag(self._msg, flag)
473
474     def set_flag(self, flag, value):
475         """Sets/Unsets a specific flag for this message
476
477         :param flag: One of the :attr:`Message.FLAG` values (currently only
478                      *Message.FLAG.MATCH*
479         :param value: A bool indicating whether to set or unset the flag.
480
481         :returns: Nothing
482         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
483               is not initialized.
484         """
485         if self._msg is None:
486             raise NotmuchError(STATUS.NOT_INITIALIZED)
487         self._set_flag(self._msg, flag, value)
488
489     def get_tags(self):
490         """Returns the message tags
491
492         :returns: A :class:`Tags` iterator.
493         :exception: :exc:`NotmuchError`
494
495                       * STATUS.NOT_INITIALIZED if the message
496                         is not initialized.
497                       * STATUS.NULL_POINTER, on error
498         """
499         if self._msg is None:
500             raise NotmuchError(STATUS.NOT_INITIALIZED)
501
502         tags_p = Message._get_tags(self._msg)
503         if tags_p == None:
504             raise NotmuchError(STATUS.NULL_POINTER)
505         return Tags(tags_p, self)
506
507     _add_tag = nmlib.notmuch_message_add_tag
508     _add_tag.argtypes = [NotmuchMessageP, c_char_p]
509     _add_tag.restype = c_uint
510
511     def add_tag(self, tag, sync_maildir_flags=False):
512         """Adds a tag to the given message
513
514         Adds a tag to the current message. The maximal tag length is defined in
515         the notmuch library and is currently 200 bytes.
516
517         :param tag: String with a 'tag' to be added.
518
519         :param sync_maildir_flags: If notmuch configuration is set to do
520             this, add maildir flags corresponding to notmuch tags. See
521             underlying method :meth:`tags_to_maildir_flags`. Use False
522             if you want to add/remove many tags on a message without
523             having to physically rename the file every time. Do note,
524             that this will do nothing when a message is frozen, as tag
525             changes will not be committed to the database yet.
526
527         :returns: STATUS.SUCCESS if the tag was successfully added.
528                   Raises an exception otherwise.
529         :exception: :exc:`NotmuchError`. They have the following meaning:
530
531                   STATUS.NULL_POINTER
532                     The 'tag' argument is NULL
533                   STATUS.TAG_TOO_LONG
534                     The length of 'tag' is too long
535                     (exceeds Message.NOTMUCH_TAG_MAX)
536                   STATUS.READ_ONLY_DATABASE
537                     Database was opened in read-only mode so message cannot be
538                     modified.
539                   STATUS.NOT_INITIALIZED
540                      The message has not been initialized.
541        """
542         if self._msg is None:
543             raise NotmuchError(STATUS.NOT_INITIALIZED)
544
545         status = self._add_tag(self._msg, _str(tag))
546
547         # bail out on failure
548         if status != STATUS.SUCCESS:
549             raise NotmuchError(status)
550
551         if sync_maildir_flags:
552             self.tags_to_maildir_flags()
553         return STATUS.SUCCESS
554
555     _remove_tag = nmlib.notmuch_message_remove_tag
556     _remove_tag.argtypes = [NotmuchMessageP, c_char_p]
557     _remove_tag.restype = c_uint
558
559     def remove_tag(self, tag, sync_maildir_flags=False):
560         """Removes a tag from the given message
561
562         If the message has no such tag, this is a non-operation and
563         will report success anyway.
564
565         :param tag: String with a 'tag' to be removed.
566         :param sync_maildir_flags: If notmuch configuration is set to do
567             this, add maildir flags corresponding to notmuch tags. See
568             underlying method :meth:`tags_to_maildir_flags`. Use False
569             if you want to add/remove many tags on a message without
570             having to physically rename the file every time. Do note,
571             that this will do nothing when a message is frozen, as tag
572             changes will not be committed to the database yet.
573
574         :returns: STATUS.SUCCESS if the tag was successfully removed or if
575                   the message had no such tag.
576                   Raises an exception otherwise.
577         :exception: :exc:`NotmuchError`. They have the following meaning:
578
579                    STATUS.NULL_POINTER
580                      The 'tag' argument is NULL
581                    STATUS.TAG_TOO_LONG
582                      The length of 'tag' is too long
583                      (exceeds NOTMUCH_TAG_MAX)
584                    STATUS.READ_ONLY_DATABASE
585                      Database was opened in read-only mode so message cannot
586                      be modified.
587                    STATUS.NOT_INITIALIZED
588                      The message has not been initialized.
589         """
590         if self._msg is None:
591             raise NotmuchError(STATUS.NOT_INITIALIZED)
592
593         status = self._remove_tag(self._msg, _str(tag))
594         # bail out on error
595         if status != STATUS.SUCCESS:
596             raise NotmuchError(status)
597
598         if sync_maildir_flags:
599             self.tags_to_maildir_flags()
600         return STATUS.SUCCESS
601
602     _remove_all_tags = nmlib.notmuch_message_remove_all_tags
603     _remove_all_tags.argtypes = [NotmuchMessageP]
604     _remove_all_tags.restype = c_uint
605
606     def remove_all_tags(self, sync_maildir_flags=False):
607         """Removes all tags from the given message.
608
609         See :meth:`freeze` for an example showing how to safely
610         replace tag values.
611
612
613         :param sync_maildir_flags: If notmuch configuration is set to do
614             this, add maildir flags corresponding to notmuch tags. See
615             :meth:`tags_to_maildir_flags`. Use False if you want to
616             add/remove many tags on a message without having to
617             physically rename the file every time. Do note, that this
618             will do nothing when a message is frozen, as tag changes
619             will not be committed to the database yet.
620
621         :returns: STATUS.SUCCESS if the tags were successfully removed.
622                   Raises an exception otherwise.
623         :exception: :exc:`NotmuchError`. They have the following meaning:
624
625                    STATUS.READ_ONLY_DATABASE
626                      Database was opened in read-only mode so message cannot
627                      be modified.
628                    STATUS.NOT_INITIALIZED
629                      The message has not been initialized.
630         """
631         if self._msg is None:
632             raise NotmuchError(STATUS.NOT_INITIALIZED)
633
634         status = self._remove_all_tags(self._msg)
635
636         # bail out on error
637         if status != STATUS.SUCCESS:
638             raise NotmuchError(status)
639
640         if sync_maildir_flags:
641             self.tags_to_maildir_flags()
642         return STATUS.SUCCESS
643
644     _freeze = nmlib.notmuch_message_freeze
645     _freeze.argtypes = [NotmuchMessageP]
646     _freeze.restype = c_uint
647
648     def freeze(self):
649         """Freezes the current state of 'message' within the database
650
651         This means that changes to the message state, (via :meth:`add_tag`,
652         :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
653         committed to the database until the message is :meth:`thaw` ed.
654
655         Multiple calls to freeze/thaw are valid and these calls will
656         "stack". That is there must be as many calls to thaw as to freeze
657         before a message is actually thawed.
658
659         The ability to do freeze/thaw allows for safe transactions to
660         change tag values. For example, explicitly setting a message to
661         have a given set of tags might look like this::
662
663           msg.freeze()
664           msg.remove_all_tags(False)
665           for tag in new_tags:
666               msg.add_tag(tag, False)
667           msg.thaw()
668           msg.tags_to_maildir_flags()
669
670         With freeze/thaw used like this, the message in the database is
671         guaranteed to have either the full set of original tag values, or
672         the full set of new tag values, but nothing in between.
673
674         Imagine the example above without freeze/thaw and the operation
675         somehow getting interrupted. This could result in the message being
676         left with no tags if the interruption happened after
677         :meth:`remove_all_tags` but before :meth:`add_tag`.
678
679         :returns: STATUS.SUCCESS if the message was successfully frozen.
680                   Raises an exception otherwise.
681         :exception: :exc:`NotmuchError`. They have the following meaning:
682
683                    STATUS.READ_ONLY_DATABASE
684                      Database was opened in read-only mode so message cannot
685                      be modified.
686                    STATUS.NOT_INITIALIZED
687                      The message has not been initialized.
688         """
689         if self._msg is None:
690             raise NotmuchError(STATUS.NOT_INITIALIZED)
691
692         status = self._freeze(self._msg)
693
694         if STATUS.SUCCESS == status:
695             # return on success
696             return status
697
698         raise NotmuchError(status)
699
700     _thaw = nmlib.notmuch_message_thaw
701     _thaw.argtypes = [NotmuchMessageP]
702     _thaw.restype = c_uint
703
704     def thaw(self):
705         """Thaws the current 'message'
706
707         Thaw the current 'message', synchronizing any changes that may have
708         occurred while 'message' was frozen into the notmuch database.
709
710         See :meth:`freeze` for an example of how to use this
711         function to safely provide tag changes.
712
713         Multiple calls to freeze/thaw are valid and these calls with
714         "stack". That is there must be as many calls to thaw as to freeze
715         before a message is actually thawed.
716
717         :returns: STATUS.SUCCESS if the message was successfully frozen.
718                   Raises an exception otherwise.
719         :exception: :exc:`NotmuchError`. They have the following meaning:
720
721                    STATUS.UNBALANCED_FREEZE_THAW
722                      An attempt was made to thaw an unfrozen message.
723                      That is, there have been an unbalanced number of calls
724                      to :meth:`freeze` and :meth:`thaw`.
725                    STATUS.NOT_INITIALIZED
726                      The message has not been initialized.
727         """
728         if self._msg is None:
729             raise NotmuchError(STATUS.NOT_INITIALIZED)
730
731         status = self._thaw(self._msg)
732
733         if STATUS.SUCCESS == status:
734             # return on success
735             return status
736
737         raise NotmuchError(status)
738
739     def is_match(self):
740         """(Not implemented)"""
741         return self.get_flag(Message.FLAG.MATCH)
742
743     def tags_to_maildir_flags(self):
744         """Synchronize notmuch tags to file Maildir flags
745
746               'D' if the message has the "draft" tag
747               'F' if the message has the "flagged" tag
748               'P' if the message has the "passed" tag
749               'R' if the message has the "replied" tag
750               'S' if the message does not have the "unread" tag
751
752         Any existing flags unmentioned in the list above will be
753         preserved in the renaming.
754
755         Also, if this filename is in a directory named "new", rename it
756         to be within the neighboring directory named "cur".
757
758         Do note that calling this method while a message is frozen might
759         not work yet, as the modified tags have not been committed yet
760         to the database.
761
762         :returns: a :class:`STATUS` value. In short, you want to see
763             notmuch.STATUS.SUCCESS here. See there for details."""
764         if self._msg is None:
765             raise NotmuchError(STATUS.NOT_INITIALIZED)
766         return Message._tags_to_maildir_flags(self._msg)
767
768     def maildir_flags_to_tags(self):
769         """Synchronize file Maildir flags to notmuch tags
770
771             Flag    Action if present
772             ----    -----------------
773             'D'     Adds the "draft" tag to the message
774             'F'     Adds the "flagged" tag to the message
775             'P'     Adds the "passed" tag to the message
776             'R'     Adds the "replied" tag to the message
777             'S'     Removes the "unread" tag from the message
778
779         For each flag that is not present, the opposite action
780         (add/remove) is performed for the corresponding tags.  If there
781         are multiple filenames associated with this message, the flag is
782         considered present if it appears in one or more filenames. (That
783         is, the flags from the multiple filenames are combined with the
784         logical OR operator.)
785
786         As a convenience, you can set the sync_maildir_flags parameter in
787         :meth:`Database.add_message` to implicitly call this.
788
789         :returns: a :class:`STATUS`. In short, you want to see
790             notmuch.STATUS.SUCCESS here. See there for details."""
791         if self._msg is None:
792             raise NotmuchError(STATUS.NOT_INITIALIZED)
793         return Message._tags_to_maildir_flags(self._msg)
794
795     def __repr__(self):
796         """Represent a Message() object by str()"""
797         return self.__str__()
798
799     def __str__(self):
800         return unicode(self).encode('utf-8')
801
802     def __unicode__(self):
803         format = "%s (%s) (%s)"
804         return format % (self.get_header('from'),
805                          self.get_tags(),
806                          date.fromtimestamp(self.get_date()),
807                         )
808
809     def get_message_parts(self):
810         """Output like notmuch show"""
811         fp = open(self.get_filename())
812         email_msg = email.message_from_file(fp)
813         fp.close()
814
815         out = []
816         for msg in email_msg.walk():
817             if not msg.is_multipart():
818                 out.append(msg)
819         return out
820
821     def get_part(self, num):
822         """Returns the nth message body part"""
823         parts = self.get_message_parts()
824         if (num <= 0 or num > len(parts)):
825             return ""
826         else:
827             out_part = parts[(num - 1)]
828             return out_part.get_payload(decode=True)
829
830     def format_message_internal(self):
831         """Create an internal representation of the message parts,
832         which can easily be output to json, text, or another output
833         format. The argument match tells whether this matched a
834         query."""
835         output = {}
836         output["id"] = self.get_message_id()
837         output["match"] = self.is_match()
838         output["filename"] = self.get_filename()
839         output["tags"] = list(self.get_tags())
840
841         headers = {}
842         for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
843             headers[h] = self.get_header(h)
844         output["headers"] = headers
845
846         body = []
847         parts = self.get_message_parts()
848         for i in xrange(len(parts)):
849             msg = parts[i]
850             part_dict = {}
851             part_dict["id"] = i + 1
852             # We'll be using this is a lot, so let's just get it once.
853             cont_type = msg.get_content_type()
854             part_dict["content-type"] = cont_type
855             # NOTE:
856             # Now we emulate the current behaviour, where it ignores
857             # the html if there's a text representation.
858             #
859             # This is being worked on, but it will be easier to fix
860             # here in the future than to end up with another
861             # incompatible solution.
862             disposition = msg["Content-Disposition"]
863             if disposition and disposition.lower().startswith("attachment"):
864                 part_dict["filename"] = msg.get_filename()
865             else:
866                 if cont_type.lower() == "text/plain":
867                     part_dict["content"] = msg.get_payload()
868                 elif (cont_type.lower() == "text/html" and
869                       i == 0):
870                     part_dict["content"] = msg.get_payload()
871             body.append(part_dict)
872
873         output["body"] = body
874
875         return output
876
877     def format_message_as_json(self, indent=0):
878         """Outputs the message as json. This is essentially the same
879         as python's dict format, but we run it through, just so we
880         don't have to worry about the details."""
881         return json.dumps(self.format_message_internal())
882
883     def format_message_as_text(self, indent=0):
884         """Outputs it in the old-fashioned notmuch text form. Will be
885         easy to change to a new format when the format changes."""
886
887         format = self.format_message_internal()
888         output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
889                  % (format['id'], indent, format['match'], format['filename'])
890         output += "\n\fheader{"
891
892         #Todo: this date is supposed to be prettified, as in the index.
893         output += "\n%s (%s) (" % (format["headers"]["From"],
894                                    format["headers"]["Date"])
895         output += ", ".join(format["tags"])
896         output += ")"
897
898         output += "\nSubject: %s" % format["headers"]["Subject"]
899         output += "\nFrom: %s" % format["headers"]["From"]
900         output += "\nTo: %s" % format["headers"]["To"]
901         if format["headers"]["Cc"]:
902             output += "\nCc: %s" % format["headers"]["Cc"]
903         if format["headers"]["Bcc"]:
904             output += "\nBcc: %s" % format["headers"]["Bcc"]
905         output += "\nDate: %s" % format["headers"]["Date"]
906         output += "\n\fheader}"
907
908         output += "\n\fbody{"
909
910         parts = format["body"]
911         parts.sort(key=lambda x: x['id'])
912         for p in parts:
913             if not "filename" in p:
914                 output += "\n\fpart{ "
915                 output += "ID: %d, Content-type: %s\n" % (p["id"],
916                                                           p["content-type"])
917                 if "content" in p:
918                     output += "\n%s\n" % p["content"]
919                 else:
920                     output += "Non-text part: %s\n" % p["content-type"]
921                     output += "\n\fpart}"
922             else:
923                 output += "\n\fattachment{ "
924                 output += "ID: %d, Content-type:%s\n" % (p["id"],
925                                                          p["content-type"])
926                 output += "Attachment: %s\n" % p["filename"]
927                 output += "\n\fattachment}\n"
928
929         output += "\n\fbody}\n"
930         output += "\n\fmessage}"
931
932         return output
933
934     def __hash__(self):
935         """Implement hash(), so we can use Message() sets"""
936         file = self.get_filename()
937         if file is None:
938             return None
939         return hash(file)
940
941     def __cmp__(self, other):
942         """Implement cmp(), so we can compare Message()s
943
944         2 messages are considered equal if they point to the same
945         Message-Id and if they point to the same file names. If 2
946         Messages derive from different queries where some files have
947         been added or removed, the same messages would not be considered
948         equal (as they do not point to the same set of files
949         any more)."""
950         res = cmp(self.get_message_id(), other.get_message_id())
951         if res:
952             res = cmp(list(self.get_filenames()), list(other.get_filenames()))
953         return res
954
955     _destroy = nmlib.notmuch_message_destroy
956     _destroy.argtypes = [NotmuchMessageP]
957     _destroy.restype = None
958
959     def __del__(self):
960         """Close and free the notmuch Message"""
961         if self._msg is not None:
962             self._destroy(self._msg)