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 (
25 nmlib, STATUS, NotmuchError, Enum, _str, Python3StringMixIn,
26 NotmuchTagsP, NotmuchMessagesP, NotmuchMessageP, NotmuchFilenamesP)
27 from notmuch.tag import Tags
28 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 init'ed
135 :meth:`collect_tags` will iterate over the messages and therefore
136 will not allow further iterations.
139 raise NotmuchError(STATUS.NOT_INITIALIZED)
141 # collect all tags (returns NULL on error)
142 tags_p = Messages._collect_tags(self._msgs)
143 #reset _msgs as we iterated over it and can do so only once
147 raise NotmuchError(STATUS.NULL_POINTER)
148 return Tags(tags_p, self)
151 """ Make Messages an iterator """
154 _valid = nmlib.notmuch_messages_valid
155 _valid.argtypes = [NotmuchMessagesP]
156 _valid.restype = bool
158 _move_to_next = nmlib.notmuch_messages_move_to_next
159 _move_to_next.argtypes = [NotmuchMessagesP]
160 _move_to_next.restype = None
164 raise NotmuchError(STATUS.NOT_INITIALIZED)
166 if not self._valid(self._msgs):
170 msg = Message(Messages._get(self._msgs), self)
171 self._move_to_next(self._msgs)
173 next = __next__ # python2.x iterator protocol compatibility
175 def __nonzero__(self):
177 :return: True if there is at least one more thread in the
178 Iterator, False if not."""
179 return self._msgs is not None and \
180 self._valid(self._msgs) > 0
182 _destroy = nmlib.notmuch_messages_destroy
183 _destroy.argtypes = [NotmuchMessagesP]
184 _destroy.restype = None
187 """Close and free the notmuch Messages"""
188 if self._msgs is not None:
189 self._destroy(self._msgs)
191 def format_messages(self, format, indent=0, entire_thread=False):
192 """Formats messages as needed for 'notmuch show'.
194 :param format: A string of either 'text' or 'json'.
195 :param indent: A number indicating the reply depth of these messages.
196 :param entire_thread: A bool, indicating whether we want to output
197 whole threads or only the matching messages.
198 :return: a list of lines
202 if format.lower() == "text":
206 elif format.lower() == "json":
211 raise TypeError("format must be either 'text' or 'json'")
215 result.append(set_start)
217 # iterate through all toplevel messages in this thread
222 result.append(set_sep)
225 result.append(set_start)
226 match = msg.is_match()
229 if (match or entire_thread):
230 if format.lower() == "text":
231 result.append(msg.format_message_as_text(indent))
233 result.append(msg.format_message_as_json(indent))
234 next_indent = indent + 1
236 # get replies and print them also out (if there are any)
237 replies = msg.get_replies().format_messages(format, next_indent, entire_thread)
239 result.append(set_sep)
240 result.extend(replies)
242 result.append(set_end)
243 result.append(set_end)
247 def print_messages(self, format, indent=0, entire_thread=False, handle=sys.stdout):
248 """Outputs messages as needed for 'notmuch show' to a file like object.
250 :param format: A string of either 'text' or 'json'.
251 :param handle: A file like object to print to (default is sys.stdout).
252 :param indent: A number indicating the reply depth of these messages.
253 :param entire_thread: A bool, indicating whether we want to output
254 whole threads or only the matching messages.
256 handle.write(''.join(self.format_messages(format, indent, entire_thread)))
259 class EmptyMessagesResult(Messages):
260 def __init__(self, parent):
262 self._parent = parent
265 raise StopIteration()
269 class Message(Python3StringMixIn):
270 """Represents a single Email message
272 Technically, this wraps the underlying *notmuch_message_t*
273 structure. A user will usually not create these objects themselves
274 but get them as search results.
276 As it implements :meth:`__cmp__`, it is possible to compare two
277 :class:`Message`\s using `if msg1 == msg2: ...`.
280 """notmuch_message_get_filename (notmuch_message_t *message)"""
281 _get_filename = nmlib.notmuch_message_get_filename
282 _get_filename.argtypes = [NotmuchMessageP]
283 _get_filename.restype = c_char_p
285 """return all filenames for a message"""
286 _get_filenames = nmlib.notmuch_message_get_filenames
287 _get_filenames.argtypes = [NotmuchMessageP]
288 _get_filenames.restype = NotmuchFilenamesP
290 """notmuch_message_get_flag"""
291 _get_flag = nmlib.notmuch_message_get_flag
292 _get_flag.argtypes = [NotmuchMessageP, c_uint]
293 _get_flag.restype = bool
295 """notmuch_message_set_flag"""
296 _set_flag = nmlib.notmuch_message_set_flag
297 _set_flag.argtypes = [NotmuchMessageP, c_uint, c_int]
298 _set_flag.restype = None
300 """notmuch_message_get_message_id (notmuch_message_t *message)"""
301 _get_message_id = nmlib.notmuch_message_get_message_id
302 _get_message_id.argtypes = [NotmuchMessageP]
303 _get_message_id.restype = c_char_p
305 """notmuch_message_get_thread_id"""
306 _get_thread_id = nmlib.notmuch_message_get_thread_id
307 _get_thread_id.argtypes = [NotmuchMessageP]
308 _get_thread_id.restype = c_char_p
310 """notmuch_message_get_replies"""
311 _get_replies = nmlib.notmuch_message_get_replies
312 _get_replies.argtypes = [NotmuchMessageP]
313 _get_replies.restype = NotmuchMessagesP
315 """notmuch_message_get_tags (notmuch_message_t *message)"""
316 _get_tags = nmlib.notmuch_message_get_tags
317 _get_tags.argtypes = [NotmuchMessageP]
318 _get_tags.restype = NotmuchTagsP
320 _get_date = nmlib.notmuch_message_get_date
321 _get_date.argtypes = [NotmuchMessageP]
322 _get_date.restype = c_long
324 _get_header = nmlib.notmuch_message_get_header
325 _get_header.argtypes = [NotmuchMessageP, c_char_p]
326 _get_header.restype = c_char_p
328 """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
329 _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
330 _tags_to_maildir_flags.argtypes = [NotmuchMessageP]
331 _tags_to_maildir_flags.restype = c_int
333 """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
334 _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
335 _maildir_flags_to_tags.argtypes = [NotmuchMessageP]
336 _maildir_flags_to_tags.restype = c_int
338 #Constants: Flags that can be set/get with set_flag
339 FLAG = Enum(['MATCH'])
341 def __init__(self, msg_p, parent=None):
343 :param msg_p: A pointer to an internal notmuch_message_t
344 Structure. If it is `None`, we will raise an :exc:`NotmuchError`
347 :param parent: A 'parent' object is passed which this message is
348 derived from. We save a reference to it, so we can
349 automatically delete the parent object once all derived
353 raise NotmuchError(STATUS.NULL_POINTER)
355 #keep reference to parent, so we keep it alive
356 self._parent = parent
358 def get_message_id(self):
359 """Returns the message ID
361 :returns: String with a message ID
362 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
366 raise NotmuchError(STATUS.NOT_INITIALIZED)
367 return Message._get_message_id(self._msg).decode('utf-8', 'ignore')
369 def get_thread_id(self):
370 """Returns the thread ID
372 The returned string belongs to 'message' will only be valid for as
373 long as the message is valid.
375 This function will not return `None` since Notmuch ensures that every
376 message belongs to a single thread.
378 :returns: String with a thread ID
379 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
383 raise NotmuchError(STATUS.NOT_INITIALIZED)
385 return Message._get_thread_id(self._msg).decode('utf-8', 'ignore')
387 def get_replies(self):
388 """Gets all direct replies to this message as :class:`Messages`
393 This call only makes sense if 'message' was ultimately obtained from
394 a :class:`Thread` object, (such as by coming directly from the
395 result of calling :meth:`Thread.get_toplevel_messages` or by any
396 number of subsequent calls to :meth:`get_replies`). If this message
397 was obtained through some non-thread means, (such as by a call to
398 :meth:`Query.search_messages`), then this function will return
399 an empty Messages iterator.
401 :returns: :class:`Messages`.
402 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
406 raise NotmuchError(STATUS.NOT_INITIALIZED)
408 msgs_p = Message._get_replies(self._msg)
411 return EmptyMessagesResult(self)
413 return Messages(msgs_p, self)
416 """Returns time_t of the message date
418 For the original textual representation of the Date header from the
419 message call notmuch_message_get_header() with a header value of
422 :returns: A time_t timestamp.
424 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
428 raise NotmuchError(STATUS.NOT_INITIALIZED)
429 return Message._get_date(self._msg)
431 def get_header(self, header):
432 """Get the value of the specified header.
434 The value will be read from the actual message file, not from
435 the notmuch database. The header name is case insensitive.
437 Returns an empty string ("") if the message does not contain a
438 header line matching 'header'.
440 :param header: The name of the header to be retrieved.
441 It is not case-sensitive.
443 :returns: The header value as string
444 :exception: :exc:`NotmuchError`
446 * STATUS.NOT_INITIALIZED if the message
448 * STATUS.NULL_POINTER if any error occured.
451 raise NotmuchError(STATUS.NOT_INITIALIZED)
453 #Returns NULL if any error occurs.
454 header = Message._get_header(self._msg, _str(header))
456 raise NotmuchError(STATUS.NULL_POINTER)
457 return header.decode('UTF-8', 'ignore')
459 def get_filename(self):
460 """Returns the file path of the message file
462 :returns: Absolute file path & name of the message file
463 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
467 raise NotmuchError(STATUS.NOT_INITIALIZED)
468 return Message._get_filename(self._msg).decode('utf-8', 'ignore')
470 def get_filenames(self):
471 """Get all filenames for the email corresponding to 'message'
473 Returns a Filenames() generator with all absolute filepaths for
474 messages recorded to have the same Message-ID. These files must
475 not necessarily have identical content."""
477 raise NotmuchError(STATUS.NOT_INITIALIZED)
479 files_p = Message._get_filenames(self._msg)
481 return Filenames(files_p, self).as_generator()
483 def get_flag(self, flag):
484 """Checks whether a specific flag is set for this message
486 The method :meth:`Query.search_threads` sets
487 *Message.FLAG.MATCH* for those messages that match the
488 query. This method allows us to get the value of this flag.
490 :param flag: One of the :attr:`Message.FLAG` values (currently only
492 :returns: An unsigned int (0/1), indicating whether the flag is set.
493 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
497 raise NotmuchError(STATUS.NOT_INITIALIZED)
498 return Message._get_flag(self._msg, flag)
500 def set_flag(self, flag, value):
501 """Sets/Unsets a specific flag for this message
503 :param flag: One of the :attr:`Message.FLAG` values (currently only
505 :param value: A bool indicating whether to set or unset the flag.
508 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
512 raise NotmuchError(STATUS.NOT_INITIALIZED)
513 self._set_flag(self._msg, flag, value)
516 """Returns the message tags
518 :returns: A :class:`Tags` iterator.
519 :exception: :exc:`NotmuchError`
521 * STATUS.NOT_INITIALIZED if the message
523 * STATUS.NULL_POINTER, on error
526 raise NotmuchError(STATUS.NOT_INITIALIZED)
528 tags_p = Message._get_tags(self._msg)
530 raise NotmuchError(STATUS.NULL_POINTER)
531 return Tags(tags_p, self)
533 _add_tag = nmlib.notmuch_message_add_tag
534 _add_tag.argtypes = [NotmuchMessageP, c_char_p]
535 _add_tag.restype = c_uint
537 def add_tag(self, tag, sync_maildir_flags=False):
538 """Adds a tag to the given message
540 Adds a tag to the current message. The maximal tag length is defined in
541 the notmuch library and is currently 200 bytes.
543 :param tag: String with a 'tag' to be added.
545 :param sync_maildir_flags: If notmuch configuration is set to do
546 this, add maildir flags corresponding to notmuch tags. See
547 underlying method :meth:`tags_to_maildir_flags`. Use False
548 if you want to add/remove many tags on a message without
549 having to physically rename the file every time. Do note,
550 that this will do nothing when a message is frozen, as tag
551 changes will not be committed to the database yet.
553 :returns: STATUS.SUCCESS if the tag was successfully added.
554 Raises an exception otherwise.
555 :exception: :exc:`NotmuchError`. They have the following meaning:
558 The 'tag' argument is NULL
560 The length of 'tag' is too long
561 (exceeds Message.NOTMUCH_TAG_MAX)
562 STATUS.READ_ONLY_DATABASE
563 Database was opened in read-only mode so message cannot be
565 STATUS.NOT_INITIALIZED
566 The message has not been initialized.
569 raise NotmuchError(STATUS.NOT_INITIALIZED)
571 status = self._add_tag(self._msg, _str(tag))
573 # bail out on failure
574 if status != STATUS.SUCCESS:
575 raise NotmuchError(status)
577 if sync_maildir_flags:
578 self.tags_to_maildir_flags()
579 return STATUS.SUCCESS
581 _remove_tag = nmlib.notmuch_message_remove_tag
582 _remove_tag.argtypes = [NotmuchMessageP, c_char_p]
583 _remove_tag.restype = c_uint
585 def remove_tag(self, tag, sync_maildir_flags=False):
586 """Removes a tag from the given message
588 If the message has no such tag, this is a non-operation and
589 will report success anyway.
591 :param tag: String with a 'tag' to be removed.
592 :param sync_maildir_flags: If notmuch configuration is set to do
593 this, add maildir flags corresponding to notmuch tags. See
594 underlying method :meth:`tags_to_maildir_flags`. Use False
595 if you want to add/remove many tags on a message without
596 having to physically rename the file every time. Do note,
597 that this will do nothing when a message is frozen, as tag
598 changes will not be committed to the database yet.
600 :returns: STATUS.SUCCESS if the tag was successfully removed or if
601 the message had no such tag.
602 Raises an exception otherwise.
603 :exception: :exc:`NotmuchError`. They have the following meaning:
606 The 'tag' argument is NULL
608 The length of 'tag' is too long
609 (exceeds NOTMUCH_TAG_MAX)
610 STATUS.READ_ONLY_DATABASE
611 Database was opened in read-only mode so message cannot
613 STATUS.NOT_INITIALIZED
614 The message has not been initialized.
617 raise NotmuchError(STATUS.NOT_INITIALIZED)
619 status = self._remove_tag(self._msg, _str(tag))
621 if status != STATUS.SUCCESS:
622 raise NotmuchError(status)
624 if sync_maildir_flags:
625 self.tags_to_maildir_flags()
626 return STATUS.SUCCESS
628 _remove_all_tags = nmlib.notmuch_message_remove_all_tags
629 _remove_all_tags.argtypes = [NotmuchMessageP]
630 _remove_all_tags.restype = c_uint
632 def remove_all_tags(self, sync_maildir_flags=False):
633 """Removes all tags from the given message.
635 See :meth:`freeze` for an example showing how to safely
639 :param sync_maildir_flags: If notmuch configuration is set to do
640 this, add maildir flags corresponding to notmuch tags. See
641 :meth:`tags_to_maildir_flags`. Use False if you want to
642 add/remove many tags on a message without having to
643 physically rename the file every time. Do note, that this
644 will do nothing when a message is frozen, as tag changes
645 will not be committed to the database yet.
647 :returns: STATUS.SUCCESS if the tags were successfully removed.
648 Raises an exception otherwise.
649 :exception: :exc:`NotmuchError`. They have the following meaning:
651 STATUS.READ_ONLY_DATABASE
652 Database was opened in read-only mode so message cannot
654 STATUS.NOT_INITIALIZED
655 The message has not been initialized.
658 raise NotmuchError(STATUS.NOT_INITIALIZED)
660 status = self._remove_all_tags(self._msg)
663 if status != STATUS.SUCCESS:
664 raise NotmuchError(status)
666 if sync_maildir_flags:
667 self.tags_to_maildir_flags()
668 return STATUS.SUCCESS
670 _freeze = nmlib.notmuch_message_freeze
671 _freeze.argtypes = [NotmuchMessageP]
672 _freeze.restype = c_uint
675 """Freezes the current state of 'message' within the database
677 This means that changes to the message state, (via :meth:`add_tag`,
678 :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
679 committed to the database until the message is :meth:`thaw` ed.
681 Multiple calls to freeze/thaw are valid and these calls will
682 "stack". That is there must be as many calls to thaw as to freeze
683 before a message is actually thawed.
685 The ability to do freeze/thaw allows for safe transactions to
686 change tag values. For example, explicitly setting a message to
687 have a given set of tags might look like this::
690 msg.remove_all_tags(False)
692 msg.add_tag(tag, False)
694 msg.tags_to_maildir_flags()
696 With freeze/thaw used like this, the message in the database is
697 guaranteed to have either the full set of original tag values, or
698 the full set of new tag values, but nothing in between.
700 Imagine the example above without freeze/thaw and the operation
701 somehow getting interrupted. This could result in the message being
702 left with no tags if the interruption happened after
703 :meth:`remove_all_tags` but before :meth:`add_tag`.
705 :returns: STATUS.SUCCESS if the message was successfully frozen.
706 Raises an exception otherwise.
707 :exception: :exc:`NotmuchError`. They have the following meaning:
709 STATUS.READ_ONLY_DATABASE
710 Database was opened in read-only mode so message cannot
712 STATUS.NOT_INITIALIZED
713 The message has not been initialized.
716 raise NotmuchError(STATUS.NOT_INITIALIZED)
718 status = self._freeze(self._msg)
720 if STATUS.SUCCESS == status:
724 raise NotmuchError(status)
726 _thaw = nmlib.notmuch_message_thaw
727 _thaw.argtypes = [NotmuchMessageP]
728 _thaw.restype = c_uint
731 """Thaws the current 'message'
733 Thaw the current 'message', synchronizing any changes that may have
734 occurred while 'message' was frozen into the notmuch database.
736 See :meth:`freeze` for an example of how to use this
737 function to safely provide tag changes.
739 Multiple calls to freeze/thaw are valid and these calls with
740 "stack". That is there must be as many calls to thaw as to freeze
741 before a message is actually thawed.
743 :returns: STATUS.SUCCESS if the message was successfully frozen.
744 Raises an exception otherwise.
745 :exception: :exc:`NotmuchError`. They have the following meaning:
747 STATUS.UNBALANCED_FREEZE_THAW
748 An attempt was made to thaw an unfrozen message.
749 That is, there have been an unbalanced number of calls
750 to :meth:`freeze` and :meth:`thaw`.
751 STATUS.NOT_INITIALIZED
752 The message has not been initialized.
755 raise NotmuchError(STATUS.NOT_INITIALIZED)
757 status = self._thaw(self._msg)
759 if STATUS.SUCCESS == status:
763 raise NotmuchError(status)
766 """(Not implemented)"""
767 return self.get_flag(Message.FLAG.MATCH)
769 def tags_to_maildir_flags(self):
770 """Synchronize notmuch tags to file Maildir flags
772 'D' if the message has the "draft" tag
773 'F' if the message has the "flagged" tag
774 'P' if the message has the "passed" tag
775 'R' if the message has the "replied" tag
776 'S' if the message does not have the "unread" tag
778 Any existing flags unmentioned in the list above will be
779 preserved in the renaming.
781 Also, if this filename is in a directory named "new", rename it
782 to be within the neighboring directory named "cur".
784 Do note that calling this method while a message is frozen might
785 not work yet, as the modified tags have not been committed yet
788 :returns: a :class:`STATUS` value. In short, you want to see
789 notmuch.STATUS.SUCCESS here. See there for details."""
791 raise NotmuchError(STATUS.NOT_INITIALIZED)
792 return Message._tags_to_maildir_flags(self._msg)
794 def maildir_flags_to_tags(self):
795 """Synchronize file Maildir flags to notmuch tags
797 Flag Action if present
798 ---- -----------------
799 'D' Adds the "draft" tag to the message
800 'F' Adds the "flagged" tag to the message
801 'P' Adds the "passed" tag to the message
802 'R' Adds the "replied" tag to the message
803 'S' Removes the "unread" tag from the message
805 For each flag that is not present, the opposite action
806 (add/remove) is performed for the corresponding tags. If there
807 are multiple filenames associated with this message, the flag is
808 considered present if it appears in one or more filenames. (That
809 is, the flags from the multiple filenames are combined with the
810 logical OR operator.)
812 As a convenience, you can set the sync_maildir_flags parameter in
813 :meth:`Database.add_message` to implicitly call this.
815 :returns: a :class:`STATUS`. In short, you want to see
816 notmuch.STATUS.SUCCESS here. See there for details."""
818 raise NotmuchError(STATUS.NOT_INITIALIZED)
819 return Message._tags_to_maildir_flags(self._msg)
822 """Represent a Message() object by str()"""
823 return self.__str__()
825 def __unicode__(self):
826 format = "%s (%s) (%s)"
827 return format % (self.get_header('from'),
829 date.fromtimestamp(self.get_date()),
832 def get_message_parts(self):
833 """Output like notmuch show"""
834 fp = open(self.get_filename())
835 email_msg = email.message_from_file(fp)
839 for msg in email_msg.walk():
840 if not msg.is_multipart():
844 def get_part(self, num):
845 """Returns the nth message body part"""
846 parts = self.get_message_parts()
847 if (num <= 0 or num > len(parts)):
850 out_part = parts[(num - 1)]
851 return out_part.get_payload(decode=True)
853 def format_message_internal(self):
854 """Create an internal representation of the message parts,
855 which can easily be output to json, text, or another output
856 format. The argument match tells whether this matched a
859 output["id"] = self.get_message_id()
860 output["match"] = self.is_match()
861 output["filename"] = self.get_filename()
862 output["tags"] = list(self.get_tags())
865 for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
866 headers[h] = self.get_header(h)
867 output["headers"] = headers
870 parts = self.get_message_parts()
871 for i in xrange(len(parts)):
874 part_dict["id"] = i + 1
875 # We'll be using this is a lot, so let's just get it once.
876 cont_type = msg.get_content_type()
877 part_dict["content-type"] = cont_type
879 # Now we emulate the current behaviour, where it ignores
880 # the html if there's a text representation.
882 # This is being worked on, but it will be easier to fix
883 # here in the future than to end up with another
884 # incompatible solution.
885 disposition = msg["Content-Disposition"]
886 if disposition and disposition.lower().startswith("attachment"):
887 part_dict["filename"] = msg.get_filename()
889 if cont_type.lower() == "text/plain":
890 part_dict["content"] = msg.get_payload()
891 elif (cont_type.lower() == "text/html" and
893 part_dict["content"] = msg.get_payload()
894 body.append(part_dict)
896 output["body"] = body
900 def format_message_as_json(self, indent=0):
901 """Outputs the message as json. This is essentially the same
902 as python's dict format, but we run it through, just so we
903 don't have to worry about the details."""
904 return json.dumps(self.format_message_internal())
906 def format_message_as_text(self, indent=0):
907 """Outputs it in the old-fashioned notmuch text form. Will be
908 easy to change to a new format when the format changes."""
910 format = self.format_message_internal()
911 output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
912 % (format['id'], indent, format['match'], format['filename'])
913 output += "\n\fheader{"
915 #Todo: this date is supposed to be prettified, as in the index.
916 output += "\n%s (%s) (" % (format["headers"]["From"],
917 format["headers"]["Date"])
918 output += ", ".join(format["tags"])
921 output += "\nSubject: %s" % format["headers"]["Subject"]
922 output += "\nFrom: %s" % format["headers"]["From"]
923 output += "\nTo: %s" % format["headers"]["To"]
924 if format["headers"]["Cc"]:
925 output += "\nCc: %s" % format["headers"]["Cc"]
926 if format["headers"]["Bcc"]:
927 output += "\nBcc: %s" % format["headers"]["Bcc"]
928 output += "\nDate: %s" % format["headers"]["Date"]
929 output += "\n\fheader}"
931 output += "\n\fbody{"
933 parts = format["body"]
934 parts.sort(key=lambda x: x['id'])
936 if not "filename" in p:
937 output += "\n\fpart{ "
938 output += "ID: %d, Content-type: %s\n" % (p["id"],
941 output += "\n%s\n" % p["content"]
943 output += "Non-text part: %s\n" % p["content-type"]
944 output += "\n\fpart}"
946 output += "\n\fattachment{ "
947 output += "ID: %d, Content-type:%s\n" % (p["id"],
949 output += "Attachment: %s\n" % p["filename"]
950 output += "\n\fattachment}\n"
952 output += "\n\fbody}\n"
953 output += "\n\fmessage}"
958 """Implement hash(), so we can use Message() sets"""
959 file = self.get_filename()
964 def __cmp__(self, other):
965 """Implement cmp(), so we can compare Message()s
967 2 messages are considered equal if they point to the same
968 Message-Id and if they point to the same file names. If 2
969 Messages derive from different queries where some files have
970 been added or removed, the same messages would not be considered
971 equal (as they do not point to the same set of files
973 res = cmp(self.get_message_id(), other.get_message_id())
975 res = cmp(list(self.get_filenames()), list(other.get_filenames()))
978 _destroy = nmlib.notmuch_message_destroy
979 _destroy.argtypes = [NotmuchMessageP]
980 _destroy.restype = None
983 """Close and free the notmuch Message"""
984 if self._msg is not None:
985 self._destroy(self._msg)