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_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
31 import simplejson as json
36 class Messages(object):
37 """Represents a list of notmuch messages
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
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
60 msgs = Query(db,'').search_messages() #get a Messages() object
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.
68 # You can reiterate over *msglist* however as often as you want.
69 # It is simply a list with :class:`Message`s.
71 print (msglist[0].get_filename())
72 print (msglist[1].get_filename())
73 print (msglist[0].get_message_id())
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::
81 s1, s2 = set(msgs1), set(msgs2)
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).
94 _get = nmlib.notmuch_messages_get
95 _get.argtypes = [NotmuchMessagesP]
96 _get.restype = NotmuchMessageP
98 _collect_tags = nmlib.notmuch_messages_collect_tags
99 _collect_tags.argtypes = [NotmuchMessagesP]
100 _collect_tags.restype = NotmuchTagsP
102 def __init__(self, msgs_p, parent=None):
104 :param msgs_p: A pointer to an underlying *notmuch_messages_t*
105 structure. These are not publically exposed, so a user
106 will almost never instantiate a :class:`Messages` object
107 herself. They are usually handed back as a result,
108 e.g. in :meth:`Query.search_messages`. *msgs_p* must be
109 valid, we will raise an :exc:`NotmuchError`
110 (STATUS.NULL_POINTER) if it is `None`.
111 :type msgs_p: :class:`ctypes.c_void_p`
112 :param parent: The parent object
113 (ie :class:`Query`) these tags are derived from. It saves
114 a reference to it, so we can automatically delete the db
115 object once all derived objects are dead.
116 :TODO: Make the iterator work more than once and cache the tags in
117 the Python object.(?)
120 raise NotmuchError(STATUS.NULL_POINTER)
123 #store parent, so we keep them alive as long as self is alive
124 self._parent = parent
126 def collect_tags(self):
127 """Return the unique :class:`Tags` in the contained messages
129 :returns: :class:`Tags`
130 :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not init'ed
134 :meth:`collect_tags` will iterate over the messages and therefore
135 will not allow further iterations.
137 if self._msgs is None:
138 raise NotmuchError(STATUS.NOT_INITIALIZED)
140 # collect all tags (returns NULL on error)
141 tags_p = Messages._collect_tags(self._msgs)
142 #reset _msgs as we iterated over it and can do so only once
146 raise NotmuchError(STATUS.NULL_POINTER)
147 return Tags(tags_p, self)
150 """ Make Messages an iterator """
153 _valid = nmlib.notmuch_messages_valid
154 _valid.argtypes = [NotmuchMessagesP]
155 _valid.restype = bool
157 _move_to_next = nmlib.notmuch_messages_move_to_next
158 _move_to_next.argtypes = [NotmuchMessagesP]
159 _move_to_next.restype = None
162 if self._msgs is None:
163 raise NotmuchError(STATUS.NOT_INITIALIZED)
165 if not self._valid(self._msgs):
169 msg = Message(Messages._get(self._msgs), self)
170 self._move_to_next(self._msgs)
172 next = __next__ # python2.x iterator protocol compatibility
174 def __nonzero__(self):
176 :return: True if there is at least one more thread in the
177 Iterator, False if not."""
178 return self._msgs is not None and \
179 self._valid(self._msgs) > 0
181 _destroy = nmlib.notmuch_messages_destroy
182 _destroy.argtypes = [NotmuchMessagesP]
183 _destroy.restype = None
186 """Close and free the notmuch Messages"""
187 if self._msgs is not None:
188 self._destroy(self._msgs)
190 def print_messages(self, format, indent=0, entire_thread=False):
191 """Outputs messages as needed for 'notmuch show' to sys.stdout
193 :param format: A string of either 'text' or 'json'.
194 :param indent: A number indicating the reply depth of these messages.
195 :param entire_thread: A bool, indicating whether we want to output
196 whole threads or only the matching messages.
198 if format.lower() == "text":
202 elif format.lower() == "json":
207 raise TypeError("format must be either 'text' or 'json'")
211 sys.stdout.write(set_start)
213 # iterate through all toplevel messages in this thread
218 sys.stdout.write(set_sep)
221 sys.stdout.write(set_start)
222 match = msg.is_match()
225 if (match or entire_thread):
226 if format.lower() == "text":
227 sys.stdout.write(msg.format_message_as_text(indent))
229 sys.stdout.write(msg.format_message_as_json(indent))
230 next_indent = indent + 1
232 # get replies and print them also out (if there are any)
233 replies = msg.get_replies()
234 if not replies is None:
235 sys.stdout.write(set_sep)
236 replies.print_messages(format, next_indent, entire_thread)
238 sys.stdout.write(set_end)
239 sys.stdout.write(set_end)
242 class Message(object):
243 """Represents a single Email message
245 Technically, this wraps the underlying *notmuch_message_t*
246 structure. A user will usually not create these objects themselves
247 but get them as search results.
249 As it implements :meth:`__cmp__`, it is possible to compare two
250 :class:`Message`\s using `if msg1 == msg2: ...`.
253 """notmuch_message_get_filename (notmuch_message_t *message)"""
254 _get_filename = nmlib.notmuch_message_get_filename
255 _get_filename.argtypes = [NotmuchMessageP]
256 _get_filename.restype = c_char_p
258 """return all filenames for a message"""
259 _get_filenames = nmlib.notmuch_message_get_filenames
260 _get_filenames.argtypes = [NotmuchMessageP]
261 _get_filenames.restype = NotmuchFilenamesP
263 """notmuch_message_get_flag"""
264 _get_flag = nmlib.notmuch_message_get_flag
265 _get_flag.argtypes = [NotmuchMessageP, c_uint]
266 _get_flag.restype = bool
268 """notmuch_message_set_flag"""
269 _set_flag = nmlib.notmuch_message_set_flag
270 _set_flag.argtypes = [NotmuchMessageP, c_uint, c_int]
271 _set_flag.restype = None
273 """notmuch_message_get_message_id (notmuch_message_t *message)"""
274 _get_message_id = nmlib.notmuch_message_get_message_id
275 _get_message_id.argtypes = [NotmuchMessageP]
276 _get_message_id.restype = c_char_p
278 """notmuch_message_get_thread_id"""
279 _get_thread_id = nmlib.notmuch_message_get_thread_id
280 _get_thread_id.argtypes = [NotmuchMessageP]
281 _get_thread_id.restype = c_char_p
283 """notmuch_message_get_replies"""
284 _get_replies = nmlib.notmuch_message_get_replies
285 _get_replies.argtypes = [NotmuchMessageP]
286 _get_replies.restype = NotmuchMessagesP
288 """notmuch_message_get_tags (notmuch_message_t *message)"""
289 _get_tags = nmlib.notmuch_message_get_tags
290 _get_tags.argtypes = [NotmuchMessageP]
291 _get_tags.restype = NotmuchTagsP
293 _get_date = nmlib.notmuch_message_get_date
294 _get_date.argtypes = [NotmuchMessageP]
295 _get_date.restype = c_long
297 _get_header = nmlib.notmuch_message_get_header
298 _get_header.argtypes = [NotmuchMessageP, c_char_p]
299 _get_header.restype = c_char_p
301 """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
302 _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
303 _tags_to_maildir_flags.argtypes = [NotmuchMessageP]
304 _tags_to_maildir_flags.restype = c_int
306 """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
307 _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
308 _maildir_flags_to_tags.argtypes = [NotmuchMessageP]
309 _maildir_flags_to_tags.restype = c_int
311 #Constants: Flags that can be set/get with set_flag
312 FLAG = Enum(['MATCH'])
314 def __init__(self, msg_p, parent=None):
316 :param msg_p: A pointer to an internal notmuch_message_t
317 Structure. If it is `None`, we will raise an :exc:`NotmuchError`
320 :param parent: A 'parent' object is passed which this message is
321 derived from. We save a reference to it, so we can
322 automatically delete the parent object once all derived
326 raise NotmuchError(STATUS.NULL_POINTER)
328 #keep reference to parent, so we keep it alive
329 self._parent = parent
331 def get_message_id(self):
332 """Returns the message ID
334 :returns: String with a message ID
335 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
338 if self._msg is None:
339 raise NotmuchError(STATUS.NOT_INITIALIZED)
340 return Message._get_message_id(self._msg)
342 def get_thread_id(self):
343 """Returns the thread ID
345 The returned string belongs to 'message' will only be valid for as
346 long as the message is valid.
348 This function will not return `None` since Notmuch ensures that every
349 message belongs to a single thread.
351 :returns: String with a thread ID
352 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
355 if self._msg is None:
356 raise NotmuchError(STATUS.NOT_INITIALIZED)
358 return Message._get_thread_id(self._msg)
360 def get_replies(self):
361 """Gets all direct replies to this message as :class:`Messages`
366 This call only makes sense if 'message' was ultimately obtained from
367 a :class:`Thread` object, (such as by coming directly from the
368 result of calling :meth:`Thread.get_toplevel_messages` or by any
369 number of subsequent calls to :meth:`get_replies`). If this message
370 was obtained through some non-thread means, (such as by a call to
371 :meth:`Query.search_messages`), then this function will return
374 :returns: :class:`Messages` or `None` if there are no replies to
376 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
379 if self._msg is None:
380 raise NotmuchError(STATUS.NOT_INITIALIZED)
382 msgs_p = Message._get_replies(self._msg)
387 return Messages(msgs_p, self)
390 """Returns time_t of the message date
392 For the original textual representation of the Date header from the
393 message call notmuch_message_get_header() with a header value of
396 :returns: A time_t timestamp.
398 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
401 if self._msg is None:
402 raise NotmuchError(STATUS.NOT_INITIALIZED)
403 return Message._get_date(self._msg)
405 def get_header(self, header):
406 """Get the value of the specified header.
408 The value will be read from the actual message file, not from
409 the notmuch database. The header name is case insensitive.
411 Returns an empty string ("") if the message does not contain a
412 header line matching 'header'.
414 :param header: The name of the header to be retrieved.
415 It is not case-sensitive.
417 :returns: The header value as string
418 :exception: :exc:`NotmuchError`
420 * STATUS.NOT_INITIALIZED if the message
422 * STATUS.NULL_POINTER if any error occured.
424 if self._msg is None:
425 raise NotmuchError(STATUS.NOT_INITIALIZED)
427 #Returns NULL if any error occurs.
428 header = Message._get_header(self._msg, header)
430 raise NotmuchError(STATUS.NULL_POINTER)
431 return header.decode('UTF-8', errors='ignore')
433 def get_filename(self):
434 """Returns the file path of the message file
436 :returns: Absolute file path & name of the message file
437 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
440 if self._msg is None:
441 raise NotmuchError(STATUS.NOT_INITIALIZED)
442 return Message._get_filename(self._msg)
444 def get_filenames(self):
445 """Get all filenames for the email corresponding to 'message'
447 Returns a Filenames() generator with all absolute filepaths for
448 messages recorded to have the same Message-ID. These files must
449 not necessarily have identical content."""
450 if self._msg is None:
451 raise NotmuchError(STATUS.NOT_INITIALIZED)
453 files_p = Message._get_filenames(self._msg)
455 return Filenames(files_p, self).as_generator()
457 def get_flag(self, flag):
458 """Checks whether a specific flag is set for this message
460 The method :meth:`Query.search_threads` sets
461 *Message.FLAG.MATCH* for those messages that match the
462 query. This method allows us to get the value of this flag.
464 :param flag: One of the :attr:`Message.FLAG` values (currently only
466 :returns: An unsigned int (0/1), indicating whether the flag is set.
467 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
470 if self._msg is None:
471 raise NotmuchError(STATUS.NOT_INITIALIZED)
472 return Message._get_flag(self._msg, flag)
474 def set_flag(self, flag, value):
475 """Sets/Unsets a specific flag for this message
477 :param flag: One of the :attr:`Message.FLAG` values (currently only
479 :param value: A bool indicating whether to set or unset the flag.
482 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
485 if self._msg is None:
486 raise NotmuchError(STATUS.NOT_INITIALIZED)
487 self._set_flag(self._msg, flag, value)
490 """Returns the message tags
492 :returns: A :class:`Tags` iterator.
493 :exception: :exc:`NotmuchError`
495 * STATUS.NOT_INITIALIZED if the message
497 * STATUS.NULL_POINTER, on error
499 if self._msg is None:
500 raise NotmuchError(STATUS.NOT_INITIALIZED)
502 tags_p = Message._get_tags(self._msg)
504 raise NotmuchError(STATUS.NULL_POINTER)
505 return Tags(tags_p, self)
507 _add_tag = nmlib.notmuch_message_add_tag
508 _add_tag.argtypes = [NotmuchMessageP, c_char_p]
509 _add_tag.restype = c_uint
511 def add_tag(self, tag, sync_maildir_flags=False):
512 """Adds a tag to the given message
514 Adds a tag to the current message. The maximal tag length is defined in
515 the notmuch library and is currently 200 bytes.
517 :param tag: String with a 'tag' to be added.
519 :param sync_maildir_flags: If notmuch configuration is set to do
520 this, add maildir flags corresponding to notmuch tags. See
521 underlying method :meth:`tags_to_maildir_flags`. Use False
522 if you want to add/remove many tags on a message without
523 having to physically rename the file every time. Do note,
524 that this will do nothing when a message is frozen, as tag
525 changes will not be committed to the database yet.
527 :returns: STATUS.SUCCESS if the tag was successfully added.
528 Raises an exception otherwise.
529 :exception: :exc:`NotmuchError`. They have the following meaning:
532 The 'tag' argument is NULL
534 The length of 'tag' is too long
535 (exceeds Message.NOTMUCH_TAG_MAX)
536 STATUS.READ_ONLY_DATABASE
537 Database was opened in read-only mode so message cannot be
539 STATUS.NOT_INITIALIZED
540 The message has not been initialized.
542 if self._msg is None:
543 raise NotmuchError(STATUS.NOT_INITIALIZED)
545 status = self._add_tag(self._msg, _str(tag))
547 # bail out on failure
548 if status != STATUS.SUCCESS:
549 raise NotmuchError(status)
551 if sync_maildir_flags:
552 self.tags_to_maildir_flags()
553 return STATUS.SUCCESS
555 _remove_tag = nmlib.notmuch_message_remove_tag
556 _remove_tag.argtypes = [NotmuchMessageP, c_char_p]
557 _remove_tag.restype = c_uint
559 def remove_tag(self, tag, sync_maildir_flags=False):
560 """Removes a tag from the given message
562 If the message has no such tag, this is a non-operation and
563 will report success anyway.
565 :param tag: String with a 'tag' to be removed.
566 :param sync_maildir_flags: If notmuch configuration is set to do
567 this, add maildir flags corresponding to notmuch tags. See
568 underlying method :meth:`tags_to_maildir_flags`. Use False
569 if you want to add/remove many tags on a message without
570 having to physically rename the file every time. Do note,
571 that this will do nothing when a message is frozen, as tag
572 changes will not be committed to the database yet.
574 :returns: STATUS.SUCCESS if the tag was successfully removed or if
575 the message had no such tag.
576 Raises an exception otherwise.
577 :exception: :exc:`NotmuchError`. They have the following meaning:
580 The 'tag' argument is NULL
582 The length of 'tag' is too long
583 (exceeds NOTMUCH_TAG_MAX)
584 STATUS.READ_ONLY_DATABASE
585 Database was opened in read-only mode so message cannot
587 STATUS.NOT_INITIALIZED
588 The message has not been initialized.
590 if self._msg is None:
591 raise NotmuchError(STATUS.NOT_INITIALIZED)
593 status = self._remove_tag(self._msg, _str(tag))
595 if status != STATUS.SUCCESS:
596 raise NotmuchError(status)
598 if sync_maildir_flags:
599 self.tags_to_maildir_flags()
600 return STATUS.SUCCESS
602 _remove_all_tags = nmlib.notmuch_message_remove_all_tags
603 _remove_all_tags.argtypes = [NotmuchMessageP]
604 _remove_all_tags.restype = c_uint
606 def remove_all_tags(self, sync_maildir_flags=False):
607 """Removes all tags from the given message.
609 See :meth:`freeze` for an example showing how to safely
613 :param sync_maildir_flags: If notmuch configuration is set to do
614 this, add maildir flags corresponding to notmuch tags. See
615 :meth:`tags_to_maildir_flags`. Use False if you want to
616 add/remove many tags on a message without having to
617 physically rename the file every time. Do note, that this
618 will do nothing when a message is frozen, as tag changes
619 will not be committed to the database yet.
621 :returns: STATUS.SUCCESS if the tags were successfully removed.
622 Raises an exception otherwise.
623 :exception: :exc:`NotmuchError`. They have the following meaning:
625 STATUS.READ_ONLY_DATABASE
626 Database was opened in read-only mode so message cannot
628 STATUS.NOT_INITIALIZED
629 The message has not been initialized.
631 if self._msg is None:
632 raise NotmuchError(STATUS.NOT_INITIALIZED)
634 status = self._remove_all_tags(self._msg)
637 if status != STATUS.SUCCESS:
638 raise NotmuchError(status)
640 if sync_maildir_flags:
641 self.tags_to_maildir_flags()
642 return STATUS.SUCCESS
644 _freeze = nmlib.notmuch_message_freeze
645 _freeze.argtypes = [NotmuchMessageP]
646 _freeze.restype = c_uint
649 """Freezes the current state of 'message' within the database
651 This means that changes to the message state, (via :meth:`add_tag`,
652 :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
653 committed to the database until the message is :meth:`thaw` ed.
655 Multiple calls to freeze/thaw are valid and these calls will
656 "stack". That is there must be as many calls to thaw as to freeze
657 before a message is actually thawed.
659 The ability to do freeze/thaw allows for safe transactions to
660 change tag values. For example, explicitly setting a message to
661 have a given set of tags might look like this::
664 msg.remove_all_tags(False)
666 msg.add_tag(tag, False)
668 msg.tags_to_maildir_flags()
670 With freeze/thaw used like this, the message in the database is
671 guaranteed to have either the full set of original tag values, or
672 the full set of new tag values, but nothing in between.
674 Imagine the example above without freeze/thaw and the operation
675 somehow getting interrupted. This could result in the message being
676 left with no tags if the interruption happened after
677 :meth:`remove_all_tags` but before :meth:`add_tag`.
679 :returns: STATUS.SUCCESS if the message was successfully frozen.
680 Raises an exception otherwise.
681 :exception: :exc:`NotmuchError`. They have the following meaning:
683 STATUS.READ_ONLY_DATABASE
684 Database was opened in read-only mode so message cannot
686 STATUS.NOT_INITIALIZED
687 The message has not been initialized.
689 if self._msg is None:
690 raise NotmuchError(STATUS.NOT_INITIALIZED)
692 status = self._freeze(self._msg)
694 if STATUS.SUCCESS == status:
698 raise NotmuchError(status)
700 _thaw = nmlib.notmuch_message_thaw
701 _thaw.argtypes = [NotmuchMessageP]
702 _thaw.restype = c_uint
705 """Thaws the current 'message'
707 Thaw the current 'message', synchronizing any changes that may have
708 occurred while 'message' was frozen into the notmuch database.
710 See :meth:`freeze` for an example of how to use this
711 function to safely provide tag changes.
713 Multiple calls to freeze/thaw are valid and these calls with
714 "stack". That is there must be as many calls to thaw as to freeze
715 before a message is actually thawed.
717 :returns: STATUS.SUCCESS if the message was successfully frozen.
718 Raises an exception otherwise.
719 :exception: :exc:`NotmuchError`. They have the following meaning:
721 STATUS.UNBALANCED_FREEZE_THAW
722 An attempt was made to thaw an unfrozen message.
723 That is, there have been an unbalanced number of calls
724 to :meth:`freeze` and :meth:`thaw`.
725 STATUS.NOT_INITIALIZED
726 The message has not been initialized.
728 if self._msg is None:
729 raise NotmuchError(STATUS.NOT_INITIALIZED)
731 status = self._thaw(self._msg)
733 if STATUS.SUCCESS == status:
737 raise NotmuchError(status)
740 """(Not implemented)"""
741 return self.get_flag(Message.FLAG.MATCH)
743 def tags_to_maildir_flags(self):
744 """Synchronize notmuch tags to file Maildir flags
746 'D' if the message has the "draft" tag
747 'F' if the message has the "flagged" tag
748 'P' if the message has the "passed" tag
749 'R' if the message has the "replied" tag
750 'S' if the message does not have the "unread" tag
752 Any existing flags unmentioned in the list above will be
753 preserved in the renaming.
755 Also, if this filename is in a directory named "new", rename it
756 to be within the neighboring directory named "cur".
758 Do note that calling this method while a message is frozen might
759 not work yet, as the modified tags have not been committed yet
762 :returns: a :class:`STATUS` value. In short, you want to see
763 notmuch.STATUS.SUCCESS here. See there for details."""
764 if self._msg is None:
765 raise NotmuchError(STATUS.NOT_INITIALIZED)
766 return Message._tags_to_maildir_flags(self._msg)
768 def maildir_flags_to_tags(self):
769 """Synchronize file Maildir flags to notmuch tags
771 Flag Action if present
772 ---- -----------------
773 'D' Adds the "draft" tag to the message
774 'F' Adds the "flagged" tag to the message
775 'P' Adds the "passed" tag to the message
776 'R' Adds the "replied" tag to the message
777 'S' Removes the "unread" tag from the message
779 For each flag that is not present, the opposite action
780 (add/remove) is performed for the corresponding tags. If there
781 are multiple filenames associated with this message, the flag is
782 considered present if it appears in one or more filenames. (That
783 is, the flags from the multiple filenames are combined with the
784 logical OR operator.)
786 As a convenience, you can set the sync_maildir_flags parameter in
787 :meth:`Database.add_message` to implicitly call this.
789 :returns: a :class:`STATUS`. In short, you want to see
790 notmuch.STATUS.SUCCESS here. See there for details."""
791 if self._msg is None:
792 raise NotmuchError(STATUS.NOT_INITIALIZED)
793 return Message._tags_to_maildir_flags(self._msg)
796 """Represent a Message() object by str()"""
797 return self.__str__()
800 return unicode(self).encode('utf-8')
802 def __unicode__(self):
803 format = "%s (%s) (%s)"
804 return format % (self.get_header('from'),
806 date.fromtimestamp(self.get_date()),
809 def get_message_parts(self):
810 """Output like notmuch show"""
811 fp = open(self.get_filename())
812 email_msg = email.message_from_file(fp)
816 for msg in email_msg.walk():
817 if not msg.is_multipart():
821 def get_part(self, num):
822 """Returns the nth message body part"""
823 parts = self.get_message_parts()
824 if (num <= 0 or num > len(parts)):
827 out_part = parts[(num - 1)]
828 return out_part.get_payload(decode=True)
830 def format_message_internal(self):
831 """Create an internal representation of the message parts,
832 which can easily be output to json, text, or another output
833 format. The argument match tells whether this matched a
836 output["id"] = self.get_message_id()
837 output["match"] = self.is_match()
838 output["filename"] = self.get_filename()
839 output["tags"] = list(self.get_tags())
842 for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
843 headers[h] = self.get_header(h)
844 output["headers"] = headers
847 parts = self.get_message_parts()
848 for i in xrange(len(parts)):
851 part_dict["id"] = i + 1
852 # We'll be using this is a lot, so let's just get it once.
853 cont_type = msg.get_content_type()
854 part_dict["content-type"] = cont_type
856 # Now we emulate the current behaviour, where it ignores
857 # the html if there's a text representation.
859 # This is being worked on, but it will be easier to fix
860 # here in the future than to end up with another
861 # incompatible solution.
862 disposition = msg["Content-Disposition"]
863 if disposition and disposition.lower().startswith("attachment"):
864 part_dict["filename"] = msg.get_filename()
866 if cont_type.lower() == "text/plain":
867 part_dict["content"] = msg.get_payload()
868 elif (cont_type.lower() == "text/html" and
870 part_dict["content"] = msg.get_payload()
871 body.append(part_dict)
873 output["body"] = body
877 def format_message_as_json(self, indent=0):
878 """Outputs the message as json. This is essentially the same
879 as python's dict format, but we run it through, just so we
880 don't have to worry about the details."""
881 return json.dumps(self.format_message_internal())
883 def format_message_as_text(self, indent=0):
884 """Outputs it in the old-fashioned notmuch text form. Will be
885 easy to change to a new format when the format changes."""
887 format = self.format_message_internal()
888 output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
889 % (format['id'], indent, format['match'], format['filename'])
890 output += "\n\fheader{"
892 #Todo: this date is supposed to be prettified, as in the index.
893 output += "\n%s (%s) (" % (format["headers"]["From"],
894 format["headers"]["Date"])
895 output += ", ".join(format["tags"])
898 output += "\nSubject: %s" % format["headers"]["Subject"]
899 output += "\nFrom: %s" % format["headers"]["From"]
900 output += "\nTo: %s" % format["headers"]["To"]
901 if format["headers"]["Cc"]:
902 output += "\nCc: %s" % format["headers"]["Cc"]
903 if format["headers"]["Bcc"]:
904 output += "\nBcc: %s" % format["headers"]["Bcc"]
905 output += "\nDate: %s" % format["headers"]["Date"]
906 output += "\n\fheader}"
908 output += "\n\fbody{"
910 parts = format["body"]
911 parts.sort(key=lambda x: x['id'])
913 if not "filename" in p:
914 output += "\n\fpart{ "
915 output += "ID: %d, Content-type: %s\n" % (p["id"],
918 output += "\n%s\n" % p["content"]
920 output += "Non-text part: %s\n" % p["content-type"]
921 output += "\n\fpart}"
923 output += "\n\fattachment{ "
924 output += "ID: %d, Content-type:%s\n" % (p["id"],
926 output += "Attachment: %s\n" % p["filename"]
927 output += "\n\fattachment}\n"
929 output += "\n\fbody}\n"
930 output += "\n\fmessage}"
935 """Implement hash(), so we can use Message() sets"""
936 file = self.get_filename()
941 def __cmp__(self, other):
942 """Implement cmp(), so we can compare Message()s
944 2 messages are considered equal if they point to the same
945 Message-Id and if they point to the same file names. If 2
946 Messages derive from different queries where some files have
947 been added or removed, the same messages would not be considered
948 equal (as they do not point to the same set of files
950 res = cmp(self.get_message_id(), other.get_message_id())
952 res = cmp(list(self.get_filenames()), list(other.get_filenames()))
955 _destroy = nmlib.notmuch_message_destroy
956 _destroy.argtypes = [NotmuchMessageP]
957 _destroy.restype = None
960 """Close and free the notmuch Message"""
961 if self._msg is not None:
962 self._destroy(self._msg)