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