]> git.notmuchmail.org Git - notmuch/blob - bindings/python/notmuch/message.py
4bf90c22f9bb62fbfe3c4bef1c69b7a2cb72da36
[notmuch] / bindings / python / notmuch / message.py
1 """
2 This file is part of notmuch.
3
4 Notmuch is free software: you can redistribute it and/or modify it
5 under the terms of the GNU General Public License as published by the
6 Free Software Foundation, either version 3 of the License, or (at your
7 option) any later version.
8
9 Notmuch is distributed in the hope that it will be useful, but WITHOUT
10 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 for more details.
13
14 You should have received a copy of the GNU General Public License
15 along with notmuch.  If not, see <http://www.gnu.org/licenses/>.
16
17 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
18                Jesse Rosenthal <jrosenthal@jhu.edu>
19 """
20
21
22 from ctypes import c_char_p, c_void_p, c_long, c_uint, c_int
23 from datetime import date
24 from notmuch.globals import nmlib, STATUS, NotmuchError, Enum, _str
25 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             raise 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 TypeError("format must be either 'text' or 'json'")
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                 else:
212                     sys.stdout.write(msg.format_message_as_json(indent))
213                 next_indent = indent + 1
214
215             # get replies and print them also out (if there are any)
216             replies = msg.get_replies()
217             if not replies is None:
218                 sys.stdout.write(set_sep)
219                 replies.print_messages(format, next_indent, entire_thread)
220
221             sys.stdout.write(set_end)
222         sys.stdout.write(set_end)
223
224
225 class Message(object):
226     """Represents a single Email message
227
228     Technically, this wraps the underlying *notmuch_message_t*
229     structure. A user will usually not create these objects themselves
230     but get them as search results.
231
232     As it implements :meth:`__cmp__`, it is possible to compare two
233     :class:`Message`\s using `if msg1 == msg2: ...`.
234     """
235
236     """notmuch_message_get_filename (notmuch_message_t *message)"""
237     _get_filename = nmlib.notmuch_message_get_filename
238     _get_filename.restype = c_char_p
239
240     """return all filenames for a message"""
241     _get_filenames = nmlib.notmuch_message_get_filenames
242     _get_filenames.restype = c_void_p
243
244     """notmuch_message_get_flag"""
245     _get_flag = nmlib.notmuch_message_get_flag
246     _get_flag.restype = c_uint
247
248     """notmuch_message_get_message_id (notmuch_message_t *message)"""
249     _get_message_id = nmlib.notmuch_message_get_message_id
250     _get_message_id.restype = c_char_p
251
252     """notmuch_message_get_thread_id"""
253     _get_thread_id = nmlib.notmuch_message_get_thread_id
254     _get_thread_id.restype = c_char_p
255
256     """notmuch_message_get_replies"""
257     _get_replies = nmlib.notmuch_message_get_replies
258     _get_replies.restype = c_void_p
259
260     """notmuch_message_get_tags (notmuch_message_t *message)"""
261     _get_tags = nmlib.notmuch_message_get_tags
262     _get_tags.restype = c_void_p
263
264     _get_date = nmlib.notmuch_message_get_date
265     _get_date.restype = c_long
266
267     _get_header = nmlib.notmuch_message_get_header
268     _get_header.restype = c_char_p
269
270     """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
271     _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
272     _tags_to_maildir_flags.restype = c_int
273
274     """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
275     _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
276     _maildir_flags_to_tags.restype = c_int
277
278     #Constants: Flags that can be set/get with set_flag
279     FLAG = Enum(['MATCH'])
280
281     def __init__(self, msg_p, parent=None):
282         """
283         :param msg_p: A pointer to an internal notmuch_message_t
284             Structure.  If it is `None`, we will raise an :exc:`NotmuchError`
285             STATUS.NULL_POINTER.
286
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             raise 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     def get_message_id(self):
299         """Returns the message ID
300
301         :returns: String with a message ID
302         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
303                     is not initialized.
304         """
305         if self._msg is None:
306             raise NotmuchError(STATUS.NOT_INITIALIZED)
307         return Message._get_message_id(self._msg)
308
309     def get_thread_id(self):
310         """Returns the thread ID
311
312         The returned string belongs to 'message' will only be valid for as
313         long as the message is valid.
314
315         This function will not return `None` since Notmuch ensures that every
316         message belongs to a single thread.
317
318         :returns: String with a thread ID
319         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
320                     is not initialized.
321         """
322         if self._msg is None:
323             raise NotmuchError(STATUS.NOT_INITIALIZED)
324
325         return Message._get_thread_id(self._msg)
326
327     def get_replies(self):
328         """Gets all direct replies to this message as :class:`Messages`
329         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         """Get the value of the specified header.
373
374         The value will be read from the actual message file, not from
375         the notmuch database. The header name is case insensitive.
376
377         Returns an empty string ("") if the message does not contain a
378         header line matching 'header'.
379
380         :param header: The name of the header to be retrieved.
381                        It is not case-sensitive.
382         :type header: str
383         :returns: The header value as string
384         :exception: :exc:`NotmuchError`
385
386                     * STATUS.NOT_INITIALIZED if the message
387                       is not initialized.
388                     * STATUS.NULL_POINTER if any error occured.
389         """
390         if self._msg is None:
391             raise NotmuchError(STATUS.NOT_INITIALIZED)
392
393         #Returns NULL if any error occurs.
394         header = Message._get_header(self._msg, header)
395         if header == None:
396             raise NotmuchError(STATUS.NULL_POINTER)
397         return header.decode('UTF-8')
398
399     def get_filename(self):
400         """Returns the file path of the message file
401
402         :returns: Absolute file path & name of the message file
403         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
404               is not initialized.
405         """
406         if self._msg is None:
407             raise NotmuchError(STATUS.NOT_INITIALIZED)
408         return Message._get_filename(self._msg)
409
410     def get_filenames(self):
411         """Get all filenames for the email corresponding to 'message'
412
413         Returns a Filenames() generator with all absolute filepaths for
414         messages recorded to have the same Message-ID. These files must
415         not necessarily have identical content."""
416         if self._msg is None:
417             raise NotmuchError(STATUS.NOT_INITIALIZED)
418
419         files_p = Message._get_filenames(self._msg)
420
421         return Filenames(files_p, self).as_generator()
422
423     def get_flag(self, flag):
424         """Checks whether a specific flag is set for this message
425
426         The method :meth:`Query.search_threads` sets
427         *Message.FLAG.MATCH* for those messages that match the
428         query. This method allows us to get the value of this flag.
429
430         :param flag: One of the :attr:`Message.FLAG` values (currently only
431                      *Message.FLAG.MATCH*
432         :returns: An unsigned int (0/1), indicating whether the flag is set.
433         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
434               is not initialized.
435         """
436         if self._msg is None:
437             raise NotmuchError(STATUS.NOT_INITIALIZED)
438         return Message._get_flag(self._msg, flag)
439
440     def set_flag(self, flag, value):
441         """Sets/Unsets a specific flag for this message
442
443         :param flag: One of the :attr:`Message.FLAG` values (currently only
444                      *Message.FLAG.MATCH*
445         :param value: A bool indicating whether to set or unset the flag.
446
447         :returns: Nothing
448         :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
449               is not initialized.
450         """
451         if self._msg is None:
452             raise NotmuchError(STATUS.NOT_INITIALIZED)
453         nmlib.notmuch_message_set_flag(self._msg, flag, value)
454
455     def get_tags(self):
456         """Returns the message tags
457
458         :returns: A :class:`Tags` iterator.
459         :exception: :exc:`NotmuchError`
460
461                       * STATUS.NOT_INITIALIZED if the message
462                         is not initialized.
463                       * STATUS.NULL_POINTER, on error
464         """
465         if self._msg is None:
466             raise NotmuchError(STATUS.NOT_INITIALIZED)
467
468         tags_p = Message._get_tags(self._msg)
469         if tags_p == None:
470             raise NotmuchError(STATUS.NULL_POINTER)
471         return Tags(tags_p, self)
472
473     def add_tag(self, tag, sync_maildir_flags=False):
474         """Adds a tag to the given message
475
476         Adds a tag to the current message. The maximal tag length is defined in
477         the notmuch library and is currently 200 bytes.
478
479         :param tag: String with a 'tag' to be added.
480
481         :param sync_maildir_flags: If notmuch configuration is set to do
482             this, add maildir flags corresponding to notmuch tags. See
483             underlying method :meth:`tags_to_maildir_flags`. Use False
484             if you want to add/remove many tags on a message without
485             having to physically rename the file every time. Do note,
486             that this will do nothing when a message is frozen, as tag
487             changes will not be committed to the database yet.
488
489         :returns: STATUS.SUCCESS if the tag was successfully added.
490                   Raises an exception otherwise.
491         :exception: :exc:`NotmuchError`. They have the following meaning:
492
493                   STATUS.NULL_POINTER
494                     The 'tag' argument is NULL
495                   STATUS.TAG_TOO_LONG
496                     The length of 'tag' is too long
497                     (exceeds Message.NOTMUCH_TAG_MAX)
498                   STATUS.READ_ONLY_DATABASE
499                     Database was opened in read-only mode so message cannot be
500                     modified.
501                   STATUS.NOT_INITIALIZED
502                      The message has not been initialized.
503        """
504         if self._msg is None:
505             raise NotmuchError(STATUS.NOT_INITIALIZED)
506
507         status = nmlib.notmuch_message_add_tag(self._msg, _str(tag))
508
509         # bail out on failure
510         if status != STATUS.SUCCESS:
511             raise NotmuchError(status)
512
513         if sync_maildir_flags:
514             self.tags_to_maildir_flags()
515         return STATUS.SUCCESS
516
517     def remove_tag(self, tag, sync_maildir_flags=False):
518         """Removes a tag from the given message
519
520         If the message has no such tag, this is a non-operation and
521         will report success anyway.
522
523         :param tag: String with a 'tag' to be removed.
524         :param sync_maildir_flags: If notmuch configuration is set to do
525             this, add maildir flags corresponding to notmuch tags. See
526             underlying method :meth:`tags_to_maildir_flags`. Use False
527             if you want to add/remove many tags on a message without
528             having to physically rename the file every time. Do note,
529             that this will do nothing when a message is frozen, as tag
530             changes will not be committed to the database yet.
531
532         :returns: STATUS.SUCCESS if the tag was successfully removed or if
533                   the message had no such tag.
534                   Raises an exception otherwise.
535         :exception: :exc:`NotmuchError`. They have the following meaning:
536
537                    STATUS.NULL_POINTER
538                      The 'tag' argument is NULL
539                    STATUS.TAG_TOO_LONG
540                      The length of 'tag' is too long
541                      (exceeds NOTMUCH_TAG_MAX)
542                    STATUS.READ_ONLY_DATABASE
543                      Database was opened in read-only mode so message cannot
544                      be modified.
545                    STATUS.NOT_INITIALIZED
546                      The message has not been initialized.
547         """
548         if self._msg is None:
549             raise NotmuchError(STATUS.NOT_INITIALIZED)
550
551         status = nmlib.notmuch_message_remove_tag(self._msg, _str(tag))
552         # bail out on error
553         if status != STATUS.SUCCESS:
554             raise NotmuchError(status)
555
556         if sync_maildir_flags:
557             self.tags_to_maildir_flags()
558         return STATUS.SUCCESS
559
560     def remove_all_tags(self, sync_maildir_flags=False):
561         """Removes all tags from the given message.
562
563         See :meth:`freeze` for an example showing how to safely
564         replace tag values.
565
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     def is_match(self):
686         """(Not implemented)"""
687         return self.get_flag(Message.FLAG.MATCH)
688
689     def tags_to_maildir_flags(self):
690         """Synchronize notmuch tags to file Maildir flags
691
692               'D' if the message has the "draft" tag
693               'F' if the message has the "flagged" tag
694               'P' if the message has the "passed" tag
695               'R' if the message has the "replied" tag
696               'S' if the message does not have the "unread" tag
697
698         Any existing flags unmentioned in the list above will be
699         preserved in the renaming.
700
701         Also, if this filename is in a directory named "new", rename it
702         to be within the neighboring directory named "cur".
703
704         Do note that calling this method while a message is frozen might
705         not work yet, as the modified tags have not been committed yet
706         to the database.
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         As a convenience, you can set the sync_maildir_flags parameter in
733         :meth:`Database.add_message` to implicitly call this.
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'] = self.get_tags()
750         msg['date'] = date.fromtimestamp(self.get_date())
751         return "%(from)s (%(date)s) (%(tags)s)" % (msg)
752
753     def get_message_parts(self):
754         """Output like notmuch show"""
755         fp = open(self.get_filename())
756         email_msg = email.message_from_file(fp)
757         fp.close()
758
759         out = []
760         for msg in email_msg.walk():
761             if not msg.is_multipart():
762                 out.append(msg)
763         return out
764
765     def get_part(self, num):
766         """Returns the nth message body part"""
767         parts = self.get_message_parts()
768         if (num <= 0 or num > len(parts)):
769             return ""
770         else:
771             out_part = parts[(num - 1)]
772             return out_part.get_payload(decode=True)
773
774     def format_message_internal(self):
775         """Create an internal representation of the message parts,
776         which can easily be output to json, text, or another output
777         format. The argument match tells whether this matched a
778         query."""
779         output = {}
780         output["id"] = self.get_message_id()
781         output["match"] = self.is_match()
782         output["filename"] = self.get_filename()
783         output["tags"] = list(self.get_tags())
784
785         headers = {}
786         for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
787             headers[h] = self.get_header(h)
788         output["headers"] = headers
789
790         body = []
791         parts = self.get_message_parts()
792         for i in xrange(len(parts)):
793             msg = parts[i]
794             part_dict = {}
795             part_dict["id"] = i + 1
796             # We'll be using this is a lot, so let's just get it once.
797             cont_type = msg.get_content_type()
798             part_dict["content-type"] = cont_type
799             # NOTE:
800             # Now we emulate the current behaviour, where it ignores
801             # the html if there's a text representation.
802             #
803             # This is being worked on, but it will be easier to fix
804             # here in the future than to end up with another
805             # incompatible solution.
806             disposition = msg["Content-Disposition"]
807             if disposition and disposition.lower().startswith("attachment"):
808                 part_dict["filename"] = msg.get_filename()
809             else:
810                 if cont_type.lower() == "text/plain":
811                     part_dict["content"] = msg.get_payload()
812                 elif (cont_type.lower() == "text/html" and
813                       i == 0):
814                     part_dict["content"] = msg.get_payload()
815             body.append(part_dict)
816
817         output["body"] = body
818
819         return output
820
821     def format_message_as_json(self, indent=0):
822         """Outputs the message as json. This is essentially the same
823         as python's dict format, but we run it through, just so we
824         don't have to worry about the details."""
825         return json.dumps(self.format_message_internal())
826
827     def format_message_as_text(self, indent=0):
828         """Outputs it in the old-fashioned notmuch text form. Will be
829         easy to change to a new format when the format changes."""
830
831         format = self.format_message_internal()
832         output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
833                  % (format['id'], indent, format['match'], format['filename'])
834         output += "\n\fheader{"
835
836         #Todo: this date is supposed to be prettified, as in the index.
837         output += "\n%s (%s) (" % (format["headers"]["From"],
838                                    format["headers"]["Date"])
839         output += ", ".join(format["tags"])
840         output += ")"
841
842         output += "\nSubject: %s" % format["headers"]["Subject"]
843         output += "\nFrom: %s" % format["headers"]["From"]
844         output += "\nTo: %s" % format["headers"]["To"]
845         if format["headers"]["Cc"]:
846             output += "\nCc: %s" % format["headers"]["Cc"]
847         if format["headers"]["Bcc"]:
848             output += "\nBcc: %s" % format["headers"]["Bcc"]
849         output += "\nDate: %s" % format["headers"]["Date"]
850         output += "\n\fheader}"
851
852         output += "\n\fbody{"
853
854         parts = format["body"]
855         parts.sort(key=lambda x: x['id'])
856         for p in parts:
857             if not "filename" in p:
858                 output += "\n\fpart{ "
859                 output += "ID: %d, Content-type: %s\n" % (p["id"],
860                                                           p["content-type"])
861                 if "content" in p:
862                     output += "\n%s\n" % p["content"]
863                 else:
864                     output += "Non-text part: %s\n" % p["content-type"]
865                     output += "\n\fpart}"
866             else:
867                 output += "\n\fattachment{ "
868                 output += "ID: %d, Content-type:%s\n" % (p["id"],
869                                                          p["content-type"])
870                 output += "Attachment: %s\n" % p["filename"]
871                 output += "\n\fattachment}\n"
872
873         output += "\n\fbody}\n"
874         output += "\n\fmessage}"
875
876         return output
877
878     def __hash__(self):
879         """Implement hash(), so we can use Message() sets"""
880         file = self.get_filename()
881         if file is None:
882             return None
883         return hash(file)
884
885     def __cmp__(self, other):
886         """Implement cmp(), so we can compare Message()s
887
888         2 messages are considered equal if they point to the same
889         Message-Id and if they point to the same file names. If 2
890         Messages derive from different queries where some files have
891         been added or removed, the same messages would not be considered
892         equal (as they do not point to the same set of files
893         any more)."""
894         res = cmp(self.get_message_id(), other.get_message_id())
895         if res:
896             res = cmp(list(self.get_filenames()), list(other.get_filenames()))
897         return res
898
899     def __del__(self):
900         """Close and free the notmuch Message"""
901         if self._msg is not None:
902             nmlib.notmuch_message_destroy(self._msg)