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