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