]> git.notmuchmail.org Git - notmuch/blob - bindings/python/notmuch/message.py
remove unused imports
[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')
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`. 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         status = 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         status = 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         """A message() is represented by a 1-line summary"""
800         msg = {}
801         msg['from'] = self.get_header('from')
802         msg['tags'] = self.get_tags()
803         msg['date'] = date.fromtimestamp(self.get_date())
804         return "%(from)s (%(date)s) (%(tags)s)" % (msg)
805
806     def get_message_parts(self):
807         """Output like notmuch show"""
808         fp = open(self.get_filename())
809         email_msg = email.message_from_file(fp)
810         fp.close()
811
812         out = []
813         for msg in email_msg.walk():
814             if not msg.is_multipart():
815                 out.append(msg)
816         return out
817
818     def get_part(self, num):
819         """Returns the nth message body part"""
820         parts = self.get_message_parts()
821         if (num <= 0 or num > len(parts)):
822             return ""
823         else:
824             out_part = parts[(num - 1)]
825             return out_part.get_payload(decode=True)
826
827     def format_message_internal(self):
828         """Create an internal representation of the message parts,
829         which can easily be output to json, text, or another output
830         format. The argument match tells whether this matched a
831         query."""
832         output = {}
833         output["id"] = self.get_message_id()
834         output["match"] = self.is_match()
835         output["filename"] = self.get_filename()
836         output["tags"] = list(self.get_tags())
837
838         headers = {}
839         for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
840             headers[h] = self.get_header(h)
841         output["headers"] = headers
842
843         body = []
844         parts = self.get_message_parts()
845         for i in xrange(len(parts)):
846             msg = parts[i]
847             part_dict = {}
848             part_dict["id"] = i + 1
849             # We'll be using this is a lot, so let's just get it once.
850             cont_type = msg.get_content_type()
851             part_dict["content-type"] = cont_type
852             # NOTE:
853             # Now we emulate the current behaviour, where it ignores
854             # the html if there's a text representation.
855             #
856             # This is being worked on, but it will be easier to fix
857             # here in the future than to end up with another
858             # incompatible solution.
859             disposition = msg["Content-Disposition"]
860             if disposition and disposition.lower().startswith("attachment"):
861                 part_dict["filename"] = msg.get_filename()
862             else:
863                 if cont_type.lower() == "text/plain":
864                     part_dict["content"] = msg.get_payload()
865                 elif (cont_type.lower() == "text/html" and
866                       i == 0):
867                     part_dict["content"] = msg.get_payload()
868             body.append(part_dict)
869
870         output["body"] = body
871
872         return output
873
874     def format_message_as_json(self, indent=0):
875         """Outputs the message as json. This is essentially the same
876         as python's dict format, but we run it through, just so we
877         don't have to worry about the details."""
878         return json.dumps(self.format_message_internal())
879
880     def format_message_as_text(self, indent=0):
881         """Outputs it in the old-fashioned notmuch text form. Will be
882         easy to change to a new format when the format changes."""
883
884         format = self.format_message_internal()
885         output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
886                  % (format['id'], indent, format['match'], format['filename'])
887         output += "\n\fheader{"
888
889         #Todo: this date is supposed to be prettified, as in the index.
890         output += "\n%s (%s) (" % (format["headers"]["From"],
891                                    format["headers"]["Date"])
892         output += ", ".join(format["tags"])
893         output += ")"
894
895         output += "\nSubject: %s" % format["headers"]["Subject"]
896         output += "\nFrom: %s" % format["headers"]["From"]
897         output += "\nTo: %s" % format["headers"]["To"]
898         if format["headers"]["Cc"]:
899             output += "\nCc: %s" % format["headers"]["Cc"]
900         if format["headers"]["Bcc"]:
901             output += "\nBcc: %s" % format["headers"]["Bcc"]
902         output += "\nDate: %s" % format["headers"]["Date"]
903         output += "\n\fheader}"
904
905         output += "\n\fbody{"
906
907         parts = format["body"]
908         parts.sort(key=lambda x: x['id'])
909         for p in parts:
910             if not "filename" in p:
911                 output += "\n\fpart{ "
912                 output += "ID: %d, Content-type: %s\n" % (p["id"],
913                                                           p["content-type"])
914                 if "content" in p:
915                     output += "\n%s\n" % p["content"]
916                 else:
917                     output += "Non-text part: %s\n" % p["content-type"]
918                     output += "\n\fpart}"
919             else:
920                 output += "\n\fattachment{ "
921                 output += "ID: %d, Content-type:%s\n" % (p["id"],
922                                                          p["content-type"])
923                 output += "Attachment: %s\n" % p["filename"]
924                 output += "\n\fattachment}\n"
925
926         output += "\n\fbody}\n"
927         output += "\n\fmessage}"
928
929         return output
930
931     def __hash__(self):
932         """Implement hash(), so we can use Message() sets"""
933         file = self.get_filename()
934         if file is None:
935             return None
936         return hash(file)
937
938     def __cmp__(self, other):
939         """Implement cmp(), so we can compare Message()s
940
941         2 messages are considered equal if they point to the same
942         Message-Id and if they point to the same file names. If 2
943         Messages derive from different queries where some files have
944         been added or removed, the same messages would not be considered
945         equal (as they do not point to the same set of files
946         any more)."""
947         res = cmp(self.get_message_id(), other.get_message_id())
948         if res:
949             res = cmp(list(self.get_filenames()), list(other.get_filenames()))
950         return res
951
952     _destroy = nmlib.notmuch_message_destroy
953     _destroy.argtypes = [NotmuchMessageP]
954     _destroy.restype = None
955
956     def __del__(self):
957         """Close and free the notmuch Message"""
958         if self._msg is not None:
959             self._destroy(self._msg)