2 This file is part of notmuch.
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.
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
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/>.
17 Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
18 Jesse Rosenthal <jrosenthal@jhu.edu>
22 from ctypes import c_char_p, c_void_p, c_long, c_uint, c_int
23 from datetime import date
24 from notmuch.globals import (nmlib, STATUS, NotmuchError, Enum, _str,
25 NotmuchTagsP, NotmuchMessagesP, NotmuchMessageP, NotmuchFilenamesP)
26 from notmuch.tag import Tags
27 from notmuch.filename import Filenames
32 import simplejson as json
37 class Messages(object):
38 """Represents a list of notmuch messages
40 This object provides an iterator over a list of notmuch messages
41 (Technically, it provides a wrapper for the underlying
42 *notmuch_messages_t* structure). Do note that the underlying library
43 only provides a one-time iterator (it cannot reset the iterator to
44 the start). Thus iterating over the function will "exhaust" the list
45 of messages, and a subsequent iteration attempt will raise a
46 :exc:`NotmuchError` STATUS.NOT_INITIALIZED. If you need to
47 re-iterate over a list of messages you will need to retrieve a new
48 :class:`Messages` object or cache your :class:`Message`\s in a list
53 You can store and reuse the single :class:`Message` objects as often
54 as you want as long as you keep the parent :class:`Messages` object
55 around. (Due to hierarchical memory allocation, all derived
56 :class:`Message` objects will be invalid when we delete the parent
57 :class:`Messages` object, even if it was already exhausted.) So
61 msgs = Query(db,'').search_messages() #get a Messages() object
64 # msgs is "exhausted" now and msgs.next() will raise an exception.
65 # However it will be kept alive until all retrieved Message()
66 # objects are also deleted. If you do e.g. an explicit del(msgs)
67 # here, the following lines would fail.
69 # You can reiterate over *msglist* however as often as you want.
70 # It is simply a list with :class:`Message`s.
72 print (msglist[0].get_filename())
73 print (msglist[1].get_filename())
74 print (msglist[0].get_message_id())
77 As :class:`Message` implements both __hash__() and __cmp__(), it is
78 possible to make sets out of :class:`Messages` and use set
79 arithmetic (this happens in python and will of course be *much*
80 slower than redoing a proper query with the appropriate filters::
82 s1, s2 = set(msgs1), set(msgs2)
87 Be careful when using set arithmetic between message sets derived
88 from different Databases (ie the same database reopened after
89 messages have changed). If messages have added or removed associated
90 files in the meantime, it is possible that the same message would be
91 considered as a different object (as it points to a different file).
95 _get = nmlib.notmuch_messages_get
96 _get.argtypes = [NotmuchMessagesP]
97 _get.restype = NotmuchMessageP
99 _collect_tags = nmlib.notmuch_messages_collect_tags
100 _collect_tags.argtypes = [NotmuchMessagesP]
101 _collect_tags.restype = NotmuchTagsP
103 def __init__(self, msgs_p, parent=None):
105 :param msgs_p: A pointer to an underlying *notmuch_messages_t*
106 structure. These are not publically exposed, so a user
107 will almost never instantiate a :class:`Messages` object
108 herself. They are usually handed back as a result,
109 e.g. in :meth:`Query.search_messages`. *msgs_p* must be
110 valid, we will raise an :exc:`NotmuchError`
111 (STATUS.NULL_POINTER) if it is `None`.
112 :type msgs_p: :class:`ctypes.c_void_p`
113 :param parent: The parent object
114 (ie :class:`Query`) these tags are derived from. It saves
115 a reference to it, so we can automatically delete the db
116 object once all derived objects are dead.
117 :TODO: Make the iterator work more than once and cache the tags in
118 the Python object.(?)
121 raise NotmuchError(STATUS.NULL_POINTER)
124 #store parent, so we keep them alive as long as self is alive
125 self._parent = parent
127 def collect_tags(self):
128 """Return the unique :class:`Tags` in the contained messages
130 :returns: :class:`Tags`
131 :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not inited
133 .. note:: :meth:`collect_tags` will iterate over the messages and
134 therefore will not allow further iterations.
136 if self._msgs is None:
137 raise NotmuchError(STATUS.NOT_INITIALIZED)
139 # collect all tags (returns NULL on error)
140 tags_p = Messages._collect_tags(self._msgs)
141 #reset _msgs as we iterated over it and can do so only once
145 raise NotmuchError(STATUS.NULL_POINTER)
146 return Tags(tags_p, self)
149 """ Make Messages an iterator """
152 _valid = nmlib.notmuch_messages_valid
153 _valid.argtypes = [NotmuchMessagesP]
154 _valid.restype = bool
156 _move_to_next = nmlib.notmuch_messages_move_to_next
157 _move_to_next.argtypes = [NotmuchMessagesP]
158 _move_to_next.restype = None
161 if self._msgs is None:
162 raise NotmuchError(STATUS.NOT_INITIALIZED)
164 if not self._valid(self._msgs):
168 msg = Message(Messages._get(self._msgs), self)
169 self._move_to_next(self._msgs)
172 def __nonzero__(self):
174 :return: True if there is at least one more thread in the
175 Iterator, False if not."""
176 return self._msgs is not None and \
177 self._valid(self._msgs) > 0
179 _destroy = nmlib.notmuch_messages_destroy
180 _destroy.argtypes = [NotmuchMessagesP]
181 _destroy.restype = None
184 """Close and free the notmuch Messages"""
185 if self._msgs is not None:
186 self._destroy(self._msgs)
188 def print_messages(self, format, indent=0, entire_thread=False):
189 """Outputs messages as needed for 'notmuch show' to sys.stdout
191 :param format: A string of either 'text' or 'json'.
192 :param indent: A number indicating the reply depth of these messages.
193 :param entire_thread: A bool, indicating whether we want to output
194 whole threads or only the matching messages.
196 if format.lower() == "text":
200 elif format.lower() == "json":
205 raise TypeError("format must be either 'text' or 'json'")
209 sys.stdout.write(set_start)
211 # iterate through all toplevel messages in this thread
216 sys.stdout.write(set_sep)
219 sys.stdout.write(set_start)
220 match = msg.is_match()
223 if (match or entire_thread):
224 if format.lower() == "text":
225 sys.stdout.write(msg.format_message_as_text(indent))
227 sys.stdout.write(msg.format_message_as_json(indent))
228 next_indent = indent + 1
230 # get replies and print them also out (if there are any)
231 replies = msg.get_replies()
232 if not replies is None:
233 sys.stdout.write(set_sep)
234 replies.print_messages(format, next_indent, entire_thread)
236 sys.stdout.write(set_end)
237 sys.stdout.write(set_end)
240 class Message(object):
241 """Represents a single Email message
243 Technically, this wraps the underlying *notmuch_message_t*
244 structure. A user will usually not create these objects themselves
245 but get them as search results.
247 As it implements :meth:`__cmp__`, it is possible to compare two
248 :class:`Message`\s using `if msg1 == msg2: ...`.
251 """notmuch_message_get_filename (notmuch_message_t *message)"""
252 _get_filename = nmlib.notmuch_message_get_filename
253 _get_filename.argtypes = [NotmuchMessageP]
254 _get_filename.restype = c_char_p
256 """return all filenames for a message"""
257 _get_filenames = nmlib.notmuch_message_get_filenames
258 _get_filenames.argtypes = [NotmuchMessageP]
259 _get_filenames.restype = NotmuchFilenamesP
261 """notmuch_message_get_flag"""
262 _get_flag = nmlib.notmuch_message_get_flag
263 _get_flag.argtypes = [NotmuchMessageP, c_uint]
264 _get_flag.restype = bool
266 """notmuch_message_set_flag"""
267 _set_flag = nmlib.notmuch_message_set_flag
268 _set_flag.argtypes = [NotmuchMessageP, c_uint, c_int]
269 _set_flag.restype = None
271 """notmuch_message_get_message_id (notmuch_message_t *message)"""
272 _get_message_id = nmlib.notmuch_message_get_message_id
273 _get_message_id.argtypes = [NotmuchMessageP]
274 _get_message_id.restype = c_char_p
276 """notmuch_message_get_thread_id"""
277 _get_thread_id = nmlib.notmuch_message_get_thread_id
278 _get_thread_id.argtypes = [NotmuchMessageP]
279 _get_thread_id.restype = c_char_p
281 """notmuch_message_get_replies"""
282 _get_replies = nmlib.notmuch_message_get_replies
283 _get_replies.argtypes = [NotmuchMessageP]
284 _get_replies.restype = NotmuchMessagesP
286 """notmuch_message_get_tags (notmuch_message_t *message)"""
287 _get_tags = nmlib.notmuch_message_get_tags
288 _get_tags.argtypes = [NotmuchMessageP]
289 _get_tags.restype = NotmuchTagsP
291 _get_date = nmlib.notmuch_message_get_date
292 _get_date.argtypes = [NotmuchMessageP]
293 _get_date.restype = c_long
295 _get_header = nmlib.notmuch_message_get_header
296 _get_header.argtypes = [NotmuchMessageP, c_char_p]
297 _get_header.restype = c_char_p
299 """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
300 _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
301 _tags_to_maildir_flags.argtypes = [NotmuchMessageP]
302 _tags_to_maildir_flags.restype = c_int
304 """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
305 _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
306 _maildir_flags_to_tags.argtypes = [NotmuchMessageP]
307 _maildir_flags_to_tags.restype = c_int
309 #Constants: Flags that can be set/get with set_flag
310 FLAG = Enum(['MATCH'])
312 def __init__(self, msg_p, parent=None):
314 :param msg_p: A pointer to an internal notmuch_message_t
315 Structure. If it is `None`, we will raise an :exc:`NotmuchError`
318 :param parent: A 'parent' object is passed which this message is
319 derived from. We save a reference to it, so we can
320 automatically delete the parent object once all derived
324 raise NotmuchError(STATUS.NULL_POINTER)
326 #keep reference to parent, so we keep it alive
327 self._parent = parent
329 def get_message_id(self):
330 """Returns the message ID
332 :returns: String with a message ID
333 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
336 if self._msg is None:
337 raise NotmuchError(STATUS.NOT_INITIALIZED)
338 return Message._get_message_id(self._msg)
340 def get_thread_id(self):
341 """Returns the thread ID
343 The returned string belongs to 'message' will only be valid for as
344 long as the message is valid.
346 This function will not return `None` since Notmuch ensures that every
347 message belongs to a single thread.
349 :returns: String with a thread ID
350 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
353 if self._msg is None:
354 raise NotmuchError(STATUS.NOT_INITIALIZED)
356 return Message._get_thread_id(self._msg)
358 def get_replies(self):
359 """Gets all direct replies to this message as :class:`Messages`
362 .. note:: This call only makes sense if 'message' was
363 ultimately obtained from a :class:`Thread` object, (such as
364 by coming directly from the result of calling
365 :meth:`Thread.get_toplevel_messages` or by any number of
366 subsequent calls to :meth:`get_replies`). If this message was
367 obtained through some non-thread means, (such as by a call
368 to :meth:`Query.search_messages`), then this function will
371 :returns: :class:`Messages` or `None` if there are no replies to
373 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
376 if self._msg is None:
377 raise NotmuchError(STATUS.NOT_INITIALIZED)
379 msgs_p = Message._get_replies(self._msg)
384 return Messages(msgs_p, self)
387 """Returns time_t of the message date
389 For the original textual representation of the Date header from the
390 message call notmuch_message_get_header() with a header value of
393 :returns: A time_t timestamp.
395 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
398 if self._msg is None:
399 raise NotmuchError(STATUS.NOT_INITIALIZED)
400 return Message._get_date(self._msg)
402 def get_header(self, header):
403 """Get the value of the specified header.
405 The value will be read from the actual message file, not from
406 the notmuch database. The header name is case insensitive.
408 Returns an empty string ("") if the message does not contain a
409 header line matching 'header'.
411 :param header: The name of the header to be retrieved.
412 It is not case-sensitive.
414 :returns: The header value as string
415 :exception: :exc:`NotmuchError`
417 * STATUS.NOT_INITIALIZED if the message
419 * STATUS.NULL_POINTER if any error occured.
421 if self._msg is None:
422 raise NotmuchError(STATUS.NOT_INITIALIZED)
424 #Returns NULL if any error occurs.
425 header = Message._get_header(self._msg, header)
427 raise NotmuchError(STATUS.NULL_POINTER)
428 return header.decode('UTF-8')
430 def get_filename(self):
431 """Returns the file path of the message file
433 :returns: Absolute file path & name of the message file
434 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
437 if self._msg is None:
438 raise NotmuchError(STATUS.NOT_INITIALIZED)
439 return Message._get_filename(self._msg)
441 def get_filenames(self):
442 """Get all filenames for the email corresponding to 'message'
444 Returns a Filenames() generator with all absolute filepaths for
445 messages recorded to have the same Message-ID. These files must
446 not necessarily have identical content."""
447 if self._msg is None:
448 raise NotmuchError(STATUS.NOT_INITIALIZED)
450 files_p = Message._get_filenames(self._msg)
452 return Filenames(files_p, self).as_generator()
454 def get_flag(self, flag):
455 """Checks whether a specific flag is set for this message
457 The method :meth:`Query.search_threads` sets
458 *Message.FLAG.MATCH* for those messages that match the
459 query. This method allows us to get the value of this flag.
461 :param flag: One of the :attr:`Message.FLAG` values (currently only
463 :returns: An unsigned int (0/1), indicating whether the flag is set.
464 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
467 if self._msg is None:
468 raise NotmuchError(STATUS.NOT_INITIALIZED)
469 return Message._get_flag(self._msg, flag)
471 def set_flag(self, flag, value):
472 """Sets/Unsets a specific flag for this message
474 :param flag: One of the :attr:`Message.FLAG` values (currently only
476 :param value: A bool indicating whether to set or unset the flag.
479 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
482 if self._msg is None:
483 raise NotmuchError(STATUS.NOT_INITIALIZED)
484 self._set_flag(self._msg, flag, value)
487 """Returns the message tags
489 :returns: A :class:`Tags` iterator.
490 :exception: :exc:`NotmuchError`
492 * STATUS.NOT_INITIALIZED if the message
494 * STATUS.NULL_POINTER, on error
496 if self._msg is None:
497 raise NotmuchError(STATUS.NOT_INITIALIZED)
499 tags_p = Message._get_tags(self._msg)
501 raise NotmuchError(STATUS.NULL_POINTER)
502 return Tags(tags_p, self)
504 _add_tag = nmlib.notmuch_message_add_tag
505 _add_tag.argtypes = [NotmuchMessageP, c_char_p]
506 _add_tag.restype = c_uint
508 def add_tag(self, tag, sync_maildir_flags=False):
509 """Adds a tag to the given message
511 Adds a tag to the current message. The maximal tag length is defined in
512 the notmuch library and is currently 200 bytes.
514 :param tag: String with a 'tag' to be added.
516 :param sync_maildir_flags: If notmuch configuration is set to do
517 this, add maildir flags corresponding to notmuch tags. See
518 underlying method :meth:`tags_to_maildir_flags`. Use False
519 if you want to add/remove many tags on a message without
520 having to physically rename the file every time. Do note,
521 that this will do nothing when a message is frozen, as tag
522 changes will not be committed to the database yet.
524 :returns: STATUS.SUCCESS if the tag was successfully added.
525 Raises an exception otherwise.
526 :exception: :exc:`NotmuchError`. They have the following meaning:
529 The 'tag' argument is NULL
531 The length of 'tag' is too long
532 (exceeds Message.NOTMUCH_TAG_MAX)
533 STATUS.READ_ONLY_DATABASE
534 Database was opened in read-only mode so message cannot be
536 STATUS.NOT_INITIALIZED
537 The message has not been initialized.
539 if self._msg is None:
540 raise NotmuchError(STATUS.NOT_INITIALIZED)
542 status = self._add_tag(self._msg, _str(tag))
544 # bail out on failure
545 if status != STATUS.SUCCESS:
546 raise NotmuchError(status)
548 if sync_maildir_flags:
549 self.tags_to_maildir_flags()
550 return STATUS.SUCCESS
552 _remove_tag = nmlib.notmuch_message_remove_tag
553 _remove_tag.argtypes = [NotmuchMessageP, c_char_p]
554 _remove_tag.restype = c_uint
556 def remove_tag(self, tag, sync_maildir_flags=False):
557 """Removes a tag from the given message
559 If the message has no such tag, this is a non-operation and
560 will report success anyway.
562 :param tag: String with a 'tag' to be removed.
563 :param sync_maildir_flags: If notmuch configuration is set to do
564 this, add maildir flags corresponding to notmuch tags. See
565 underlying method :meth:`tags_to_maildir_flags`. Use False
566 if you want to add/remove many tags on a message without
567 having to physically rename the file every time. Do note,
568 that this will do nothing when a message is frozen, as tag
569 changes will not be committed to the database yet.
571 :returns: STATUS.SUCCESS if the tag was successfully removed or if
572 the message had no such tag.
573 Raises an exception otherwise.
574 :exception: :exc:`NotmuchError`. They have the following meaning:
577 The 'tag' argument is NULL
579 The length of 'tag' is too long
580 (exceeds NOTMUCH_TAG_MAX)
581 STATUS.READ_ONLY_DATABASE
582 Database was opened in read-only mode so message cannot
584 STATUS.NOT_INITIALIZED
585 The message has not been initialized.
587 if self._msg is None:
588 raise NotmuchError(STATUS.NOT_INITIALIZED)
590 status = self._remove_tag(self._msg, _str(tag))
592 if status != STATUS.SUCCESS:
593 raise NotmuchError(status)
595 if sync_maildir_flags:
596 self.tags_to_maildir_flags()
597 return STATUS.SUCCESS
599 _remove_all_tags = nmlib.notmuch_message_remove_all_tags
600 _remove_all_tags.argtypes = [NotmuchMessageP]
601 _remove_all_tags.restype = c_uint
603 def remove_all_tags(self, sync_maildir_flags=False):
604 """Removes all tags from the given message.
606 See :meth:`freeze` for an example showing how to safely
610 :param sync_maildir_flags: If notmuch configuration is set to do
611 this, add maildir flags corresponding to notmuch tags. See
612 :meth:`tags_to_maildir_flags`. Use False if you want to
613 add/remove many tags on a message without having to
614 physically rename the file every time. Do note, that this
615 will do nothing when a message is frozen, as tag changes
616 will not be committed to the database yet.
618 :returns: STATUS.SUCCESS if the tags were successfully removed.
619 Raises an exception otherwise.
620 :exception: :exc:`NotmuchError`. They have the following meaning:
622 STATUS.READ_ONLY_DATABASE
623 Database was opened in read-only mode so message cannot
625 STATUS.NOT_INITIALIZED
626 The message has not been initialized.
628 if self._msg is None:
629 raise NotmuchError(STATUS.NOT_INITIALIZED)
631 status = self._remove_all_tags(self._msg)
634 if status != STATUS.SUCCESS:
635 raise NotmuchError(status)
637 if sync_maildir_flags:
638 self.tags_to_maildir_flags()
639 return STATUS.SUCCESS
641 _freeze = nmlib.notmuch_message_freeze
642 _freeze.argtypes = [NotmuchMessageP]
643 _freeze.restype = c_uint
646 """Freezes the current state of 'message' within the database
648 This means that changes to the message state, (via :meth:`add_tag`,
649 :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
650 committed to the database until the message is :meth:`thaw`ed.
652 Multiple calls to freeze/thaw are valid and these calls will
653 "stack". That is there must be as many calls to thaw as to freeze
654 before a message is actually thawed.
656 The ability to do freeze/thaw allows for safe transactions to
657 change tag values. For example, explicitly setting a message to
658 have a given set of tags might look like this::
661 msg.remove_all_tags(False)
663 msg.add_tag(tag, False)
665 msg.tags_to_maildir_flags()
667 With freeze/thaw used like this, the message in the database is
668 guaranteed to have either the full set of original tag values, or
669 the full set of new tag values, but nothing in between.
671 Imagine the example above without freeze/thaw and the operation
672 somehow getting interrupted. This could result in the message being
673 left with no tags if the interruption happened after
674 :meth:`remove_all_tags` but before :meth:`add_tag`.
676 :returns: STATUS.SUCCESS if the message was successfully frozen.
677 Raises an exception otherwise.
678 :exception: :exc:`NotmuchError`. They have the following meaning:
680 STATUS.READ_ONLY_DATABASE
681 Database was opened in read-only mode so message cannot
683 STATUS.NOT_INITIALIZED
684 The message has not been initialized.
686 if self._msg is None:
687 raise NotmuchError(STATUS.NOT_INITIALIZED)
689 status = self._freeze(self._msg)
691 if STATUS.SUCCESS == status:
695 raise NotmuchError(status)
697 _thaw = nmlib.notmuch_message_thaw
698 _thaw.argtypes = [NotmuchMessageP]
699 _thaw.restype = c_uint
702 """Thaws the current 'message'
704 Thaw the current 'message', synchronizing any changes that may have
705 occurred while 'message' was frozen into the notmuch database.
707 See :meth:`freeze` for an example of how to use this
708 function to safely provide tag changes.
710 Multiple calls to freeze/thaw are valid and these calls with
711 "stack". That is there must be as many calls to thaw as to freeze
712 before a message is actually thawed.
714 :returns: STATUS.SUCCESS if the message was successfully frozen.
715 Raises an exception otherwise.
716 :exception: :exc:`NotmuchError`. They have the following meaning:
718 STATUS.UNBALANCED_FREEZE_THAW
719 An attempt was made to thaw an unfrozen message.
720 That is, there have been an unbalanced number of calls
721 to :meth:`freeze` and :meth:`thaw`.
722 STATUS.NOT_INITIALIZED
723 The message has not been initialized.
725 if self._msg is None:
726 raise NotmuchError(STATUS.NOT_INITIALIZED)
728 status = self._thaw(self._msg)
730 if STATUS.SUCCESS == status:
734 raise NotmuchError(status)
737 """(Not implemented)"""
738 return self.get_flag(Message.FLAG.MATCH)
740 def tags_to_maildir_flags(self):
741 """Synchronize notmuch tags to file Maildir flags
743 'D' if the message has the "draft" tag
744 'F' if the message has the "flagged" tag
745 'P' if the message has the "passed" tag
746 'R' if the message has the "replied" tag
747 'S' if the message does not have the "unread" tag
749 Any existing flags unmentioned in the list above will be
750 preserved in the renaming.
752 Also, if this filename is in a directory named "new", rename it
753 to be within the neighboring directory named "cur".
755 Do note that calling this method while a message is frozen might
756 not work yet, as the modified tags have not been committed yet
759 :returns: a :class:`STATUS`. In short, you want to see
760 notmuch.STATUS.SUCCESS here. See there for details."""
761 if self._msg is None:
762 raise NotmuchError(STATUS.NOT_INITIALIZED)
763 status = Message._tags_to_maildir_flags(self._msg)
765 def maildir_flags_to_tags(self):
766 """Synchronize file Maildir flags to notmuch tags
768 Flag Action if present
769 ---- -----------------
770 'D' Adds the "draft" tag to the message
771 'F' Adds the "flagged" tag to the message
772 'P' Adds the "passed" tag to the message
773 'R' Adds the "replied" tag to the message
774 'S' Removes the "unread" tag from the message
776 For each flag that is not present, the opposite action
777 (add/remove) is performed for the corresponding tags. If there
778 are multiple filenames associated with this message, the flag is
779 considered present if it appears in one or more filenames. (That
780 is, the flags from the multiple filenames are combined with the
781 logical OR operator.)
783 As a convenience, you can set the sync_maildir_flags parameter in
784 :meth:`Database.add_message` to implicitly call this.
786 :returns: a :class:`STATUS`. In short, you want to see
787 notmuch.STATUS.SUCCESS here. See there for details."""
788 if self._msg is None:
789 raise NotmuchError(STATUS.NOT_INITIALIZED)
790 status = Message._tags_to_maildir_flags(self._msg)
793 """Represent a Message() object by str()"""
794 return self.__str__()
797 """A message() is represented by a 1-line summary"""
799 msg['from'] = self.get_header('from')
800 msg['tags'] = self.get_tags()
801 msg['date'] = date.fromtimestamp(self.get_date())
802 return "%(from)s (%(date)s) (%(tags)s)" % (msg)
804 def get_message_parts(self):
805 """Output like notmuch show"""
806 fp = open(self.get_filename())
807 email_msg = email.message_from_file(fp)
811 for msg in email_msg.walk():
812 if not msg.is_multipart():
816 def get_part(self, num):
817 """Returns the nth message body part"""
818 parts = self.get_message_parts()
819 if (num <= 0 or num > len(parts)):
822 out_part = parts[(num - 1)]
823 return out_part.get_payload(decode=True)
825 def format_message_internal(self):
826 """Create an internal representation of the message parts,
827 which can easily be output to json, text, or another output
828 format. The argument match tells whether this matched a
831 output["id"] = self.get_message_id()
832 output["match"] = self.is_match()
833 output["filename"] = self.get_filename()
834 output["tags"] = list(self.get_tags())
837 for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
838 headers[h] = self.get_header(h)
839 output["headers"] = headers
842 parts = self.get_message_parts()
843 for i in xrange(len(parts)):
846 part_dict["id"] = i + 1
847 # We'll be using this is a lot, so let's just get it once.
848 cont_type = msg.get_content_type()
849 part_dict["content-type"] = cont_type
851 # Now we emulate the current behaviour, where it ignores
852 # the html if there's a text representation.
854 # This is being worked on, but it will be easier to fix
855 # here in the future than to end up with another
856 # incompatible solution.
857 disposition = msg["Content-Disposition"]
858 if disposition and disposition.lower().startswith("attachment"):
859 part_dict["filename"] = msg.get_filename()
861 if cont_type.lower() == "text/plain":
862 part_dict["content"] = msg.get_payload()
863 elif (cont_type.lower() == "text/html" and
865 part_dict["content"] = msg.get_payload()
866 body.append(part_dict)
868 output["body"] = body
872 def format_message_as_json(self, indent=0):
873 """Outputs the message as json. This is essentially the same
874 as python's dict format, but we run it through, just so we
875 don't have to worry about the details."""
876 return json.dumps(self.format_message_internal())
878 def format_message_as_text(self, indent=0):
879 """Outputs it in the old-fashioned notmuch text form. Will be
880 easy to change to a new format when the format changes."""
882 format = self.format_message_internal()
883 output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
884 % (format['id'], indent, format['match'], format['filename'])
885 output += "\n\fheader{"
887 #Todo: this date is supposed to be prettified, as in the index.
888 output += "\n%s (%s) (" % (format["headers"]["From"],
889 format["headers"]["Date"])
890 output += ", ".join(format["tags"])
893 output += "\nSubject: %s" % format["headers"]["Subject"]
894 output += "\nFrom: %s" % format["headers"]["From"]
895 output += "\nTo: %s" % format["headers"]["To"]
896 if format["headers"]["Cc"]:
897 output += "\nCc: %s" % format["headers"]["Cc"]
898 if format["headers"]["Bcc"]:
899 output += "\nBcc: %s" % format["headers"]["Bcc"]
900 output += "\nDate: %s" % format["headers"]["Date"]
901 output += "\n\fheader}"
903 output += "\n\fbody{"
905 parts = format["body"]
906 parts.sort(key=lambda x: x['id'])
908 if not "filename" in p:
909 output += "\n\fpart{ "
910 output += "ID: %d, Content-type: %s\n" % (p["id"],
913 output += "\n%s\n" % p["content"]
915 output += "Non-text part: %s\n" % p["content-type"]
916 output += "\n\fpart}"
918 output += "\n\fattachment{ "
919 output += "ID: %d, Content-type:%s\n" % (p["id"],
921 output += "Attachment: %s\n" % p["filename"]
922 output += "\n\fattachment}\n"
924 output += "\n\fbody}\n"
925 output += "\n\fmessage}"
930 """Implement hash(), so we can use Message() sets"""
931 file = self.get_filename()
936 def __cmp__(self, other):
937 """Implement cmp(), so we can compare Message()s
939 2 messages are considered equal if they point to the same
940 Message-Id and if they point to the same file names. If 2
941 Messages derive from different queries where some files have
942 been added or removed, the same messages would not be considered
943 equal (as they do not point to the same set of files
945 res = cmp(self.get_message_id(), other.get_message_id())
947 res = cmp(list(self.get_filenames()), list(other.get_filenames()))
950 _destroy = nmlib.notmuch_message_destroy
951 _destroy.argtypes = [NotmuchMessageP]
952 _destroy.restype = None
955 """Close and free the notmuch Message"""
956 if self._msg is not None:
957 self._destroy(self._msg)