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