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