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