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