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