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.
138 if self._msgs is None:
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
163 if self._msgs is 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 print_messages(self, format, indent=0, entire_thread=False):
192 """Outputs messages as needed for 'notmuch show' to sys.stdout
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.
199 if format.lower() == "text":
203 elif format.lower() == "json":
208 raise TypeError("format must be either 'text' or 'json'")
212 sys.stdout.write(set_start)
214 # iterate through all toplevel messages in this thread
219 sys.stdout.write(set_sep)
222 sys.stdout.write(set_start)
223 match = msg.is_match()
226 if (match or entire_thread):
227 if format.lower() == "text":
228 sys.stdout.write(msg.format_message_as_text(indent))
230 sys.stdout.write(msg.format_message_as_json(indent))
231 next_indent = indent + 1
233 # get replies and print them also out (if there are any)
234 replies = msg.get_replies()
235 if not replies is None:
236 sys.stdout.write(set_sep)
237 replies.print_messages(format, next_indent, entire_thread)
239 sys.stdout.write(set_end)
240 sys.stdout.write(set_end)
243 class Message(Python3StringMixIn):
244 """Represents a single Email message
246 Technically, this wraps the underlying *notmuch_message_t*
247 structure. A user will usually not create these objects themselves
248 but get them as search results.
250 As it implements :meth:`__cmp__`, it is possible to compare two
251 :class:`Message`\s using `if msg1 == msg2: ...`.
254 """notmuch_message_get_filename (notmuch_message_t *message)"""
255 _get_filename = nmlib.notmuch_message_get_filename
256 _get_filename.argtypes = [NotmuchMessageP]
257 _get_filename.restype = c_char_p
259 """return all filenames for a message"""
260 _get_filenames = nmlib.notmuch_message_get_filenames
261 _get_filenames.argtypes = [NotmuchMessageP]
262 _get_filenames.restype = NotmuchFilenamesP
264 """notmuch_message_get_flag"""
265 _get_flag = nmlib.notmuch_message_get_flag
266 _get_flag.argtypes = [NotmuchMessageP, c_uint]
267 _get_flag.restype = bool
269 """notmuch_message_set_flag"""
270 _set_flag = nmlib.notmuch_message_set_flag
271 _set_flag.argtypes = [NotmuchMessageP, c_uint, c_int]
272 _set_flag.restype = None
274 """notmuch_message_get_message_id (notmuch_message_t *message)"""
275 _get_message_id = nmlib.notmuch_message_get_message_id
276 _get_message_id.argtypes = [NotmuchMessageP]
277 _get_message_id.restype = c_char_p
279 """notmuch_message_get_thread_id"""
280 _get_thread_id = nmlib.notmuch_message_get_thread_id
281 _get_thread_id.argtypes = [NotmuchMessageP]
282 _get_thread_id.restype = c_char_p
284 """notmuch_message_get_replies"""
285 _get_replies = nmlib.notmuch_message_get_replies
286 _get_replies.argtypes = [NotmuchMessageP]
287 _get_replies.restype = NotmuchMessagesP
289 """notmuch_message_get_tags (notmuch_message_t *message)"""
290 _get_tags = nmlib.notmuch_message_get_tags
291 _get_tags.argtypes = [NotmuchMessageP]
292 _get_tags.restype = NotmuchTagsP
294 _get_date = nmlib.notmuch_message_get_date
295 _get_date.argtypes = [NotmuchMessageP]
296 _get_date.restype = c_long
298 _get_header = nmlib.notmuch_message_get_header
299 _get_header.argtypes = [NotmuchMessageP, c_char_p]
300 _get_header.restype = c_char_p
302 """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
303 _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
304 _tags_to_maildir_flags.argtypes = [NotmuchMessageP]
305 _tags_to_maildir_flags.restype = c_int
307 """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
308 _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
309 _maildir_flags_to_tags.argtypes = [NotmuchMessageP]
310 _maildir_flags_to_tags.restype = c_int
312 #Constants: Flags that can be set/get with set_flag
313 FLAG = Enum(['MATCH'])
315 def __init__(self, msg_p, parent=None):
317 :param msg_p: A pointer to an internal notmuch_message_t
318 Structure. If it is `None`, we will raise an :exc:`NotmuchError`
321 :param parent: A 'parent' object is passed which this message is
322 derived from. We save a reference to it, so we can
323 automatically delete the parent object once all derived
327 raise NotmuchError(STATUS.NULL_POINTER)
329 #keep reference to parent, so we keep it alive
330 self._parent = parent
332 def get_message_id(self):
333 """Returns the message ID
335 :returns: String with a message ID
336 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
339 if self._msg is None:
340 raise NotmuchError(STATUS.NOT_INITIALIZED)
341 return Message._get_message_id(self._msg)
343 def get_thread_id(self):
344 """Returns the thread ID
346 The returned string belongs to 'message' will only be valid for as
347 long as the message is valid.
349 This function will not return `None` since Notmuch ensures that every
350 message belongs to a single thread.
352 :returns: String with a thread ID
353 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
356 if self._msg is None:
357 raise NotmuchError(STATUS.NOT_INITIALIZED)
359 return Message._get_thread_id(self._msg)
361 def get_replies(self):
362 """Gets all direct replies to this message as :class:`Messages`
367 This call only makes sense if 'message' was ultimately obtained from
368 a :class:`Thread` object, (such as by coming directly from the
369 result of calling :meth:`Thread.get_toplevel_messages` or by any
370 number of subsequent calls to :meth:`get_replies`). If this message
371 was obtained through some non-thread means, (such as by a call to
372 :meth:`Query.search_messages`), then this function will return
375 :returns: :class:`Messages` or `None` if there are no replies to
377 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
380 if self._msg is None:
381 raise NotmuchError(STATUS.NOT_INITIALIZED)
383 msgs_p = Message._get_replies(self._msg)
388 return Messages(msgs_p, self)
391 """Returns time_t of the message date
393 For the original textual representation of the Date header from the
394 message call notmuch_message_get_header() with a header value of
397 :returns: A time_t timestamp.
399 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
402 if self._msg is None:
403 raise NotmuchError(STATUS.NOT_INITIALIZED)
404 return Message._get_date(self._msg)
406 def get_header(self, header):
407 """Get the value of the specified header.
409 The value will be read from the actual message file, not from
410 the notmuch database. The header name is case insensitive.
412 Returns an empty string ("") if the message does not contain a
413 header line matching 'header'.
415 :param header: The name of the header to be retrieved.
416 It is not case-sensitive.
418 :returns: The header value as string
419 :exception: :exc:`NotmuchError`
421 * STATUS.NOT_INITIALIZED if the message
423 * STATUS.NULL_POINTER if any error occured.
425 if self._msg is None:
426 raise NotmuchError(STATUS.NOT_INITIALIZED)
428 #Returns NULL if any error occurs.
429 header = Message._get_header(self._msg, header)
431 raise NotmuchError(STATUS.NULL_POINTER)
432 return header.decode('UTF-8', errors='ignore')
434 def get_filename(self):
435 """Returns the file path of the message file
437 :returns: Absolute file path & name of the message file
438 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
441 if self._msg is None:
442 raise NotmuchError(STATUS.NOT_INITIALIZED)
443 return Message._get_filename(self._msg)
445 def get_filenames(self):
446 """Get all filenames for the email corresponding to 'message'
448 Returns a Filenames() generator with all absolute filepaths for
449 messages recorded to have the same Message-ID. These files must
450 not necessarily have identical content."""
451 if self._msg is None:
452 raise NotmuchError(STATUS.NOT_INITIALIZED)
454 files_p = Message._get_filenames(self._msg)
456 return Filenames(files_p, self).as_generator()
458 def get_flag(self, flag):
459 """Checks whether a specific flag is set for this message
461 The method :meth:`Query.search_threads` sets
462 *Message.FLAG.MATCH* for those messages that match the
463 query. This method allows us to get the value of this flag.
465 :param flag: One of the :attr:`Message.FLAG` values (currently only
467 :returns: An unsigned int (0/1), indicating whether the flag is set.
468 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
471 if self._msg is None:
472 raise NotmuchError(STATUS.NOT_INITIALIZED)
473 return Message._get_flag(self._msg, flag)
475 def set_flag(self, flag, value):
476 """Sets/Unsets a specific flag for this message
478 :param flag: One of the :attr:`Message.FLAG` values (currently only
480 :param value: A bool indicating whether to set or unset the flag.
483 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
486 if self._msg is None:
487 raise NotmuchError(STATUS.NOT_INITIALIZED)
488 self._set_flag(self._msg, flag, value)
491 """Returns the message tags
493 :returns: A :class:`Tags` iterator.
494 :exception: :exc:`NotmuchError`
496 * STATUS.NOT_INITIALIZED if the message
498 * STATUS.NULL_POINTER, on error
500 if self._msg is None:
501 raise NotmuchError(STATUS.NOT_INITIALIZED)
503 tags_p = Message._get_tags(self._msg)
505 raise NotmuchError(STATUS.NULL_POINTER)
506 return Tags(tags_p, self)
508 _add_tag = nmlib.notmuch_message_add_tag
509 _add_tag.argtypes = [NotmuchMessageP, c_char_p]
510 _add_tag.restype = c_uint
512 def add_tag(self, tag, sync_maildir_flags=False):
513 """Adds a tag to the given message
515 Adds a tag to the current message. The maximal tag length is defined in
516 the notmuch library and is currently 200 bytes.
518 :param tag: String with a 'tag' to be added.
520 :param sync_maildir_flags: If notmuch configuration is set to do
521 this, add maildir flags corresponding to notmuch tags. See
522 underlying method :meth:`tags_to_maildir_flags`. Use False
523 if you want to add/remove many tags on a message without
524 having to physically rename the file every time. Do note,
525 that this will do nothing when a message is frozen, as tag
526 changes will not be committed to the database yet.
528 :returns: STATUS.SUCCESS if the tag was successfully added.
529 Raises an exception otherwise.
530 :exception: :exc:`NotmuchError`. They have the following meaning:
533 The 'tag' argument is NULL
535 The length of 'tag' is too long
536 (exceeds Message.NOTMUCH_TAG_MAX)
537 STATUS.READ_ONLY_DATABASE
538 Database was opened in read-only mode so message cannot be
540 STATUS.NOT_INITIALIZED
541 The message has not been initialized.
543 if self._msg is None:
544 raise NotmuchError(STATUS.NOT_INITIALIZED)
546 status = self._add_tag(self._msg, _str(tag))
548 # bail out on failure
549 if status != STATUS.SUCCESS:
550 raise NotmuchError(status)
552 if sync_maildir_flags:
553 self.tags_to_maildir_flags()
554 return STATUS.SUCCESS
556 _remove_tag = nmlib.notmuch_message_remove_tag
557 _remove_tag.argtypes = [NotmuchMessageP, c_char_p]
558 _remove_tag.restype = c_uint
560 def remove_tag(self, tag, sync_maildir_flags=False):
561 """Removes a tag from the given message
563 If the message has no such tag, this is a non-operation and
564 will report success anyway.
566 :param tag: String with a 'tag' to be removed.
567 :param sync_maildir_flags: If notmuch configuration is set to do
568 this, add maildir flags corresponding to notmuch tags. See
569 underlying method :meth:`tags_to_maildir_flags`. Use False
570 if you want to add/remove many tags on a message without
571 having to physically rename the file every time. Do note,
572 that this will do nothing when a message is frozen, as tag
573 changes will not be committed to the database yet.
575 :returns: STATUS.SUCCESS if the tag was successfully removed or if
576 the message had no such tag.
577 Raises an exception otherwise.
578 :exception: :exc:`NotmuchError`. They have the following meaning:
581 The 'tag' argument is NULL
583 The length of 'tag' is too long
584 (exceeds NOTMUCH_TAG_MAX)
585 STATUS.READ_ONLY_DATABASE
586 Database was opened in read-only mode so message cannot
588 STATUS.NOT_INITIALIZED
589 The message has not been initialized.
591 if self._msg is None:
592 raise NotmuchError(STATUS.NOT_INITIALIZED)
594 status = self._remove_tag(self._msg, _str(tag))
596 if status != STATUS.SUCCESS:
597 raise NotmuchError(status)
599 if sync_maildir_flags:
600 self.tags_to_maildir_flags()
601 return STATUS.SUCCESS
603 _remove_all_tags = nmlib.notmuch_message_remove_all_tags
604 _remove_all_tags.argtypes = [NotmuchMessageP]
605 _remove_all_tags.restype = c_uint
607 def remove_all_tags(self, sync_maildir_flags=False):
608 """Removes all tags from the given message.
610 See :meth:`freeze` for an example showing how to safely
614 :param sync_maildir_flags: If notmuch configuration is set to do
615 this, add maildir flags corresponding to notmuch tags. See
616 :meth:`tags_to_maildir_flags`. Use False if you want to
617 add/remove many tags on a message without having to
618 physically rename the file every time. Do note, that this
619 will do nothing when a message is frozen, as tag changes
620 will not be committed to the database yet.
622 :returns: STATUS.SUCCESS if the tags were successfully removed.
623 Raises an exception otherwise.
624 :exception: :exc:`NotmuchError`. They have the following meaning:
626 STATUS.READ_ONLY_DATABASE
627 Database was opened in read-only mode so message cannot
629 STATUS.NOT_INITIALIZED
630 The message has not been initialized.
632 if self._msg is None:
633 raise NotmuchError(STATUS.NOT_INITIALIZED)
635 status = self._remove_all_tags(self._msg)
638 if status != STATUS.SUCCESS:
639 raise NotmuchError(status)
641 if sync_maildir_flags:
642 self.tags_to_maildir_flags()
643 return STATUS.SUCCESS
645 _freeze = nmlib.notmuch_message_freeze
646 _freeze.argtypes = [NotmuchMessageP]
647 _freeze.restype = c_uint
650 """Freezes the current state of 'message' within the database
652 This means that changes to the message state, (via :meth:`add_tag`,
653 :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
654 committed to the database until the message is :meth:`thaw` ed.
656 Multiple calls to freeze/thaw are valid and these calls will
657 "stack". That is there must be as many calls to thaw as to freeze
658 before a message is actually thawed.
660 The ability to do freeze/thaw allows for safe transactions to
661 change tag values. For example, explicitly setting a message to
662 have a given set of tags might look like this::
665 msg.remove_all_tags(False)
667 msg.add_tag(tag, False)
669 msg.tags_to_maildir_flags()
671 With freeze/thaw used like this, the message in the database is
672 guaranteed to have either the full set of original tag values, or
673 the full set of new tag values, but nothing in between.
675 Imagine the example above without freeze/thaw and the operation
676 somehow getting interrupted. This could result in the message being
677 left with no tags if the interruption happened after
678 :meth:`remove_all_tags` but before :meth:`add_tag`.
680 :returns: STATUS.SUCCESS if the message was successfully frozen.
681 Raises an exception otherwise.
682 :exception: :exc:`NotmuchError`. They have the following meaning:
684 STATUS.READ_ONLY_DATABASE
685 Database was opened in read-only mode so message cannot
687 STATUS.NOT_INITIALIZED
688 The message has not been initialized.
690 if self._msg is None:
691 raise NotmuchError(STATUS.NOT_INITIALIZED)
693 status = self._freeze(self._msg)
695 if STATUS.SUCCESS == status:
699 raise NotmuchError(status)
701 _thaw = nmlib.notmuch_message_thaw
702 _thaw.argtypes = [NotmuchMessageP]
703 _thaw.restype = c_uint
706 """Thaws the current 'message'
708 Thaw the current 'message', synchronizing any changes that may have
709 occurred while 'message' was frozen into the notmuch database.
711 See :meth:`freeze` for an example of how to use this
712 function to safely provide tag changes.
714 Multiple calls to freeze/thaw are valid and these calls with
715 "stack". That is there must be as many calls to thaw as to freeze
716 before a message is actually thawed.
718 :returns: STATUS.SUCCESS if the message was successfully frozen.
719 Raises an exception otherwise.
720 :exception: :exc:`NotmuchError`. They have the following meaning:
722 STATUS.UNBALANCED_FREEZE_THAW
723 An attempt was made to thaw an unfrozen message.
724 That is, there have been an unbalanced number of calls
725 to :meth:`freeze` and :meth:`thaw`.
726 STATUS.NOT_INITIALIZED
727 The message has not been initialized.
729 if self._msg is None:
730 raise NotmuchError(STATUS.NOT_INITIALIZED)
732 status = self._thaw(self._msg)
734 if STATUS.SUCCESS == status:
738 raise NotmuchError(status)
741 """(Not implemented)"""
742 return self.get_flag(Message.FLAG.MATCH)
744 def tags_to_maildir_flags(self):
745 """Synchronize notmuch tags to file Maildir flags
747 'D' if the message has the "draft" tag
748 'F' if the message has the "flagged" tag
749 'P' if the message has the "passed" tag
750 'R' if the message has the "replied" tag
751 'S' if the message does not have the "unread" tag
753 Any existing flags unmentioned in the list above will be
754 preserved in the renaming.
756 Also, if this filename is in a directory named "new", rename it
757 to be within the neighboring directory named "cur".
759 Do note that calling this method while a message is frozen might
760 not work yet, as the modified tags have not been committed yet
763 :returns: a :class:`STATUS` value. In short, you want to see
764 notmuch.STATUS.SUCCESS here. See there for details."""
765 if self._msg is None:
766 raise NotmuchError(STATUS.NOT_INITIALIZED)
767 return Message._tags_to_maildir_flags(self._msg)
769 def maildir_flags_to_tags(self):
770 """Synchronize file Maildir flags to notmuch tags
772 Flag Action if present
773 ---- -----------------
774 'D' Adds the "draft" tag to the message
775 'F' Adds the "flagged" tag to the message
776 'P' Adds the "passed" tag to the message
777 'R' Adds the "replied" tag to the message
778 'S' Removes the "unread" tag from the message
780 For each flag that is not present, the opposite action
781 (add/remove) is performed for the corresponding tags. If there
782 are multiple filenames associated with this message, the flag is
783 considered present if it appears in one or more filenames. (That
784 is, the flags from the multiple filenames are combined with the
785 logical OR operator.)
787 As a convenience, you can set the sync_maildir_flags parameter in
788 :meth:`Database.add_message` to implicitly call this.
790 :returns: a :class:`STATUS`. In short, you want to see
791 notmuch.STATUS.SUCCESS here. See there for details."""
792 if self._msg is None:
793 raise NotmuchError(STATUS.NOT_INITIALIZED)
794 return Message._tags_to_maildir_flags(self._msg)
797 """Represent a Message() object by str()"""
798 return self.__str__()
800 def __unicode__(self):
801 format = "%s (%s) (%s)"
802 return format % (self.get_header('from'),
804 date.fromtimestamp(self.get_date()),
807 def get_message_parts(self):
808 """Output like notmuch show"""
809 fp = open(self.get_filename())
810 email_msg = email.message_from_file(fp)
814 for msg in email_msg.walk():
815 if not msg.is_multipart():
819 def get_part(self, num):
820 """Returns the nth message body part"""
821 parts = self.get_message_parts()
822 if (num <= 0 or num > len(parts)):
825 out_part = parts[(num - 1)]
826 return out_part.get_payload(decode=True)
828 def format_message_internal(self):
829 """Create an internal representation of the message parts,
830 which can easily be output to json, text, or another output
831 format. The argument match tells whether this matched a
834 output["id"] = self.get_message_id()
835 output["match"] = self.is_match()
836 output["filename"] = self.get_filename()
837 output["tags"] = list(self.get_tags())
840 for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
841 headers[h] = self.get_header(h)
842 output["headers"] = headers
845 parts = self.get_message_parts()
846 for i in xrange(len(parts)):
849 part_dict["id"] = i + 1
850 # We'll be using this is a lot, so let's just get it once.
851 cont_type = msg.get_content_type()
852 part_dict["content-type"] = cont_type
854 # Now we emulate the current behaviour, where it ignores
855 # the html if there's a text representation.
857 # This is being worked on, but it will be easier to fix
858 # here in the future than to end up with another
859 # incompatible solution.
860 disposition = msg["Content-Disposition"]
861 if disposition and disposition.lower().startswith("attachment"):
862 part_dict["filename"] = msg.get_filename()
864 if cont_type.lower() == "text/plain":
865 part_dict["content"] = msg.get_payload()
866 elif (cont_type.lower() == "text/html" and
868 part_dict["content"] = msg.get_payload()
869 body.append(part_dict)
871 output["body"] = body
875 def format_message_as_json(self, indent=0):
876 """Outputs the message as json. This is essentially the same
877 as python's dict format, but we run it through, just so we
878 don't have to worry about the details."""
879 return json.dumps(self.format_message_internal())
881 def format_message_as_text(self, indent=0):
882 """Outputs it in the old-fashioned notmuch text form. Will be
883 easy to change to a new format when the format changes."""
885 format = self.format_message_internal()
886 output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
887 % (format['id'], indent, format['match'], format['filename'])
888 output += "\n\fheader{"
890 #Todo: this date is supposed to be prettified, as in the index.
891 output += "\n%s (%s) (" % (format["headers"]["From"],
892 format["headers"]["Date"])
893 output += ", ".join(format["tags"])
896 output += "\nSubject: %s" % format["headers"]["Subject"]
897 output += "\nFrom: %s" % format["headers"]["From"]
898 output += "\nTo: %s" % format["headers"]["To"]
899 if format["headers"]["Cc"]:
900 output += "\nCc: %s" % format["headers"]["Cc"]
901 if format["headers"]["Bcc"]:
902 output += "\nBcc: %s" % format["headers"]["Bcc"]
903 output += "\nDate: %s" % format["headers"]["Date"]
904 output += "\n\fheader}"
906 output += "\n\fbody{"
908 parts = format["body"]
909 parts.sort(key=lambda x: x['id'])
911 if not "filename" in p:
912 output += "\n\fpart{ "
913 output += "ID: %d, Content-type: %s\n" % (p["id"],
916 output += "\n%s\n" % p["content"]
918 output += "Non-text part: %s\n" % p["content-type"]
919 output += "\n\fpart}"
921 output += "\n\fattachment{ "
922 output += "ID: %d, Content-type:%s\n" % (p["id"],
924 output += "Attachment: %s\n" % p["filename"]
925 output += "\n\fattachment}\n"
927 output += "\n\fbody}\n"
928 output += "\n\fmessage}"
933 """Implement hash(), so we can use Message() sets"""
934 file = self.get_filename()
939 def __cmp__(self, other):
940 """Implement cmp(), so we can compare Message()s
942 2 messages are considered equal if they point to the same
943 Message-Id and if they point to the same file names. If 2
944 Messages derive from different queries where some files have
945 been added or removed, the same messages would not be considered
946 equal (as they do not point to the same set of files
948 res = cmp(self.get_message_id(), other.get_message_id())
950 res = cmp(list(self.get_filenames()), list(other.get_filenames()))
953 _destroy = nmlib.notmuch_message_destroy
954 _destroy.argtypes = [NotmuchMessageP]
955 _destroy.restype = None
958 """Close and free the notmuch Message"""
959 if self._msg is not None:
960 self._destroy(self._msg)