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