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)
173 def __nonzero__(self):
175 :return: True if there is at least one more thread in the
176 Iterator, False if not."""
177 return self._msgs is not None and \
178 self._valid(self._msgs) > 0
180 _destroy = nmlib.notmuch_messages_destroy
181 _destroy.argtypes = [NotmuchMessagesP]
182 _destroy.restype = None
185 """Close and free the notmuch Messages"""
186 if self._msgs is not None:
187 self._destroy(self._msgs)
189 def print_messages(self, format, indent=0, entire_thread=False):
190 """Outputs messages as needed for 'notmuch show' to sys.stdout
192 :param format: A string of either 'text' or 'json'.
193 :param indent: A number indicating the reply depth of these messages.
194 :param entire_thread: A bool, indicating whether we want to output
195 whole threads or only the matching messages.
197 if format.lower() == "text":
201 elif format.lower() == "json":
206 raise TypeError("format must be either 'text' or 'json'")
210 sys.stdout.write(set_start)
212 # iterate through all toplevel messages in this thread
217 sys.stdout.write(set_sep)
220 sys.stdout.write(set_start)
221 match = msg.is_match()
224 if (match or entire_thread):
225 if format.lower() == "text":
226 sys.stdout.write(msg.format_message_as_text(indent))
228 sys.stdout.write(msg.format_message_as_json(indent))
229 next_indent = indent + 1
231 # get replies and print them also out (if there are any)
232 replies = msg.get_replies()
233 if not replies is None:
234 sys.stdout.write(set_sep)
235 replies.print_messages(format, next_indent, entire_thread)
237 sys.stdout.write(set_end)
238 sys.stdout.write(set_end)
241 class Message(object):
242 """Represents a single Email message
244 Technically, this wraps the underlying *notmuch_message_t*
245 structure. A user will usually not create these objects themselves
246 but get them as search results.
248 As it implements :meth:`__cmp__`, it is possible to compare two
249 :class:`Message`\s using `if msg1 == msg2: ...`.
252 """notmuch_message_get_filename (notmuch_message_t *message)"""
253 _get_filename = nmlib.notmuch_message_get_filename
254 _get_filename.argtypes = [NotmuchMessageP]
255 _get_filename.restype = c_char_p
257 """return all filenames for a message"""
258 _get_filenames = nmlib.notmuch_message_get_filenames
259 _get_filenames.argtypes = [NotmuchMessageP]
260 _get_filenames.restype = NotmuchFilenamesP
262 """notmuch_message_get_flag"""
263 _get_flag = nmlib.notmuch_message_get_flag
264 _get_flag.argtypes = [NotmuchMessageP, c_uint]
265 _get_flag.restype = bool
267 """notmuch_message_set_flag"""
268 _set_flag = nmlib.notmuch_message_set_flag
269 _set_flag.argtypes = [NotmuchMessageP, c_uint, c_int]
270 _set_flag.restype = None
272 """notmuch_message_get_message_id (notmuch_message_t *message)"""
273 _get_message_id = nmlib.notmuch_message_get_message_id
274 _get_message_id.argtypes = [NotmuchMessageP]
275 _get_message_id.restype = c_char_p
277 """notmuch_message_get_thread_id"""
278 _get_thread_id = nmlib.notmuch_message_get_thread_id
279 _get_thread_id.argtypes = [NotmuchMessageP]
280 _get_thread_id.restype = c_char_p
282 """notmuch_message_get_replies"""
283 _get_replies = nmlib.notmuch_message_get_replies
284 _get_replies.argtypes = [NotmuchMessageP]
285 _get_replies.restype = NotmuchMessagesP
287 """notmuch_message_get_tags (notmuch_message_t *message)"""
288 _get_tags = nmlib.notmuch_message_get_tags
289 _get_tags.argtypes = [NotmuchMessageP]
290 _get_tags.restype = NotmuchTagsP
292 _get_date = nmlib.notmuch_message_get_date
293 _get_date.argtypes = [NotmuchMessageP]
294 _get_date.restype = c_long
296 _get_header = nmlib.notmuch_message_get_header
297 _get_header.argtypes = [NotmuchMessageP, c_char_p]
298 _get_header.restype = c_char_p
300 """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
301 _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
302 _tags_to_maildir_flags.argtypes = [NotmuchMessageP]
303 _tags_to_maildir_flags.restype = c_int
305 """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
306 _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
307 _maildir_flags_to_tags.argtypes = [NotmuchMessageP]
308 _maildir_flags_to_tags.restype = c_int
310 #Constants: Flags that can be set/get with set_flag
311 FLAG = Enum(['MATCH'])
313 def __init__(self, msg_p, parent=None):
315 :param msg_p: A pointer to an internal notmuch_message_t
316 Structure. If it is `None`, we will raise an :exc:`NotmuchError`
319 :param parent: A 'parent' object is passed which this message is
320 derived from. We save a reference to it, so we can
321 automatically delete the parent object once all derived
325 raise NotmuchError(STATUS.NULL_POINTER)
327 #keep reference to parent, so we keep it alive
328 self._parent = parent
330 def get_message_id(self):
331 """Returns the message ID
333 :returns: String with a message ID
334 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
337 if self._msg is None:
338 raise NotmuchError(STATUS.NOT_INITIALIZED)
339 return Message._get_message_id(self._msg)
341 def get_thread_id(self):
342 """Returns the thread ID
344 The returned string belongs to 'message' will only be valid for as
345 long as the message is valid.
347 This function will not return `None` since Notmuch ensures that every
348 message belongs to a single thread.
350 :returns: String with a thread ID
351 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
354 if self._msg is None:
355 raise NotmuchError(STATUS.NOT_INITIALIZED)
357 return Message._get_thread_id(self._msg)
359 def get_replies(self):
360 """Gets all direct replies to this message as :class:`Messages`
365 This call only makes sense if 'message' was ultimately obtained from
366 a :class:`Thread` object, (such as by coming directly from the
367 result of calling :meth:`Thread.get_toplevel_messages` or by any
368 number of subsequent calls to :meth:`get_replies`). If this message
369 was obtained through some non-thread means, (such as by a call to
370 :meth:`Query.search_messages`), then this function will return
373 :returns: :class:`Messages` or `None` if there are no replies to
375 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
378 if self._msg is None:
379 raise NotmuchError(STATUS.NOT_INITIALIZED)
381 msgs_p = Message._get_replies(self._msg)
386 return Messages(msgs_p, self)
389 """Returns time_t of the message date
391 For the original textual representation of the Date header from the
392 message call notmuch_message_get_header() with a header value of
395 :returns: A time_t timestamp.
397 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
400 if self._msg is None:
401 raise NotmuchError(STATUS.NOT_INITIALIZED)
402 return Message._get_date(self._msg)
404 def get_header(self, header):
405 """Get the value of the specified header.
407 The value will be read from the actual message file, not from
408 the notmuch database. The header name is case insensitive.
410 Returns an empty string ("") if the message does not contain a
411 header line matching 'header'.
413 :param header: The name of the header to be retrieved.
414 It is not case-sensitive.
416 :returns: The header value as string
417 :exception: :exc:`NotmuchError`
419 * STATUS.NOT_INITIALIZED if the message
421 * STATUS.NULL_POINTER if any error occured.
423 if self._msg is None:
424 raise NotmuchError(STATUS.NOT_INITIALIZED)
426 #Returns NULL if any error occurs.
427 header = Message._get_header(self._msg, header)
429 raise NotmuchError(STATUS.NULL_POINTER)
430 return header.decode('UTF-8')
432 def get_filename(self):
433 """Returns the file path of the message file
435 :returns: Absolute file path & name of the message file
436 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
439 if self._msg is None:
440 raise NotmuchError(STATUS.NOT_INITIALIZED)
441 return Message._get_filename(self._msg)
443 def get_filenames(self):
444 """Get all filenames for the email corresponding to 'message'
446 Returns a Filenames() generator with all absolute filepaths for
447 messages recorded to have the same Message-ID. These files must
448 not necessarily have identical content."""
449 if self._msg is None:
450 raise NotmuchError(STATUS.NOT_INITIALIZED)
452 files_p = Message._get_filenames(self._msg)
454 return Filenames(files_p, self).as_generator()
456 def get_flag(self, flag):
457 """Checks whether a specific flag is set for this message
459 The method :meth:`Query.search_threads` sets
460 *Message.FLAG.MATCH* for those messages that match the
461 query. This method allows us to get the value of this flag.
463 :param flag: One of the :attr:`Message.FLAG` values (currently only
465 :returns: An unsigned int (0/1), indicating whether the flag is set.
466 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
469 if self._msg is None:
470 raise NotmuchError(STATUS.NOT_INITIALIZED)
471 return Message._get_flag(self._msg, flag)
473 def set_flag(self, flag, value):
474 """Sets/Unsets a specific flag for this message
476 :param flag: One of the :attr:`Message.FLAG` values (currently only
478 :param value: A bool indicating whether to set or unset the flag.
481 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
484 if self._msg is None:
485 raise NotmuchError(STATUS.NOT_INITIALIZED)
486 self._set_flag(self._msg, flag, value)
489 """Returns the message tags
491 :returns: A :class:`Tags` iterator.
492 :exception: :exc:`NotmuchError`
494 * STATUS.NOT_INITIALIZED if the message
496 * STATUS.NULL_POINTER, on error
498 if self._msg is None:
499 raise NotmuchError(STATUS.NOT_INITIALIZED)
501 tags_p = Message._get_tags(self._msg)
503 raise NotmuchError(STATUS.NULL_POINTER)
504 return Tags(tags_p, self)
506 _add_tag = nmlib.notmuch_message_add_tag
507 _add_tag.argtypes = [NotmuchMessageP, c_char_p]
508 _add_tag.restype = c_uint
510 def add_tag(self, tag, sync_maildir_flags=False):
511 """Adds a tag to the given message
513 Adds a tag to the current message. The maximal tag length is defined in
514 the notmuch library and is currently 200 bytes.
516 :param tag: String with a 'tag' to be added.
518 :param sync_maildir_flags: If notmuch configuration is set to do
519 this, add maildir flags corresponding to notmuch tags. See
520 underlying method :meth:`tags_to_maildir_flags`. Use False
521 if you want to add/remove many tags on a message without
522 having to physically rename the file every time. Do note,
523 that this will do nothing when a message is frozen, as tag
524 changes will not be committed to the database yet.
526 :returns: STATUS.SUCCESS if the tag was successfully added.
527 Raises an exception otherwise.
528 :exception: :exc:`NotmuchError`. They have the following meaning:
531 The 'tag' argument is NULL
533 The length of 'tag' is too long
534 (exceeds Message.NOTMUCH_TAG_MAX)
535 STATUS.READ_ONLY_DATABASE
536 Database was opened in read-only mode so message cannot be
538 STATUS.NOT_INITIALIZED
539 The message has not been initialized.
541 if self._msg is None:
542 raise NotmuchError(STATUS.NOT_INITIALIZED)
544 status = self._add_tag(self._msg, _str(tag))
546 # bail out on failure
547 if status != STATUS.SUCCESS:
548 raise NotmuchError(status)
550 if sync_maildir_flags:
551 self.tags_to_maildir_flags()
552 return STATUS.SUCCESS
554 _remove_tag = nmlib.notmuch_message_remove_tag
555 _remove_tag.argtypes = [NotmuchMessageP, c_char_p]
556 _remove_tag.restype = c_uint
558 def remove_tag(self, tag, sync_maildir_flags=False):
559 """Removes a tag from the given message
561 If the message has no such tag, this is a non-operation and
562 will report success anyway.
564 :param tag: String with a 'tag' to be removed.
565 :param sync_maildir_flags: If notmuch configuration is set to do
566 this, add maildir flags corresponding to notmuch tags. See
567 underlying method :meth:`tags_to_maildir_flags`. Use False
568 if you want to add/remove many tags on a message without
569 having to physically rename the file every time. Do note,
570 that this will do nothing when a message is frozen, as tag
571 changes will not be committed to the database yet.
573 :returns: STATUS.SUCCESS if the tag was successfully removed or if
574 the message had no such tag.
575 Raises an exception otherwise.
576 :exception: :exc:`NotmuchError`. They have the following meaning:
579 The 'tag' argument is NULL
581 The length of 'tag' is too long
582 (exceeds NOTMUCH_TAG_MAX)
583 STATUS.READ_ONLY_DATABASE
584 Database was opened in read-only mode so message cannot
586 STATUS.NOT_INITIALIZED
587 The message has not been initialized.
589 if self._msg is None:
590 raise NotmuchError(STATUS.NOT_INITIALIZED)
592 status = self._remove_tag(self._msg, _str(tag))
594 if status != STATUS.SUCCESS:
595 raise NotmuchError(status)
597 if sync_maildir_flags:
598 self.tags_to_maildir_flags()
599 return STATUS.SUCCESS
601 _remove_all_tags = nmlib.notmuch_message_remove_all_tags
602 _remove_all_tags.argtypes = [NotmuchMessageP]
603 _remove_all_tags.restype = c_uint
605 def remove_all_tags(self, sync_maildir_flags=False):
606 """Removes all tags from the given message.
608 See :meth:`freeze` for an example showing how to safely
612 :param sync_maildir_flags: If notmuch configuration is set to do
613 this, add maildir flags corresponding to notmuch tags. See
614 :meth:`tags_to_maildir_flags`. Use False if you want to
615 add/remove many tags on a message without having to
616 physically rename the file every time. Do note, that this
617 will do nothing when a message is frozen, as tag changes
618 will not be committed to the database yet.
620 :returns: STATUS.SUCCESS if the tags were successfully removed.
621 Raises an exception otherwise.
622 :exception: :exc:`NotmuchError`. They have the following meaning:
624 STATUS.READ_ONLY_DATABASE
625 Database was opened in read-only mode so message cannot
627 STATUS.NOT_INITIALIZED
628 The message has not been initialized.
630 if self._msg is None:
631 raise NotmuchError(STATUS.NOT_INITIALIZED)
633 status = self._remove_all_tags(self._msg)
636 if status != STATUS.SUCCESS:
637 raise NotmuchError(status)
639 if sync_maildir_flags:
640 self.tags_to_maildir_flags()
641 return STATUS.SUCCESS
643 _freeze = nmlib.notmuch_message_freeze
644 _freeze.argtypes = [NotmuchMessageP]
645 _freeze.restype = c_uint
648 """Freezes the current state of 'message' within the database
650 This means that changes to the message state, (via :meth:`add_tag`,
651 :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
652 committed to the database until the message is :meth:`thaw` ed.
654 Multiple calls to freeze/thaw are valid and these calls will
655 "stack". That is there must be as many calls to thaw as to freeze
656 before a message is actually thawed.
658 The ability to do freeze/thaw allows for safe transactions to
659 change tag values. For example, explicitly setting a message to
660 have a given set of tags might look like this::
663 msg.remove_all_tags(False)
665 msg.add_tag(tag, False)
667 msg.tags_to_maildir_flags()
669 With freeze/thaw used like this, the message in the database is
670 guaranteed to have either the full set of original tag values, or
671 the full set of new tag values, but nothing in between.
673 Imagine the example above without freeze/thaw and the operation
674 somehow getting interrupted. This could result in the message being
675 left with no tags if the interruption happened after
676 :meth:`remove_all_tags` but before :meth:`add_tag`.
678 :returns: STATUS.SUCCESS if the message was successfully frozen.
679 Raises an exception otherwise.
680 :exception: :exc:`NotmuchError`. They have the following meaning:
682 STATUS.READ_ONLY_DATABASE
683 Database was opened in read-only mode so message cannot
685 STATUS.NOT_INITIALIZED
686 The message has not been initialized.
688 if self._msg is None:
689 raise NotmuchError(STATUS.NOT_INITIALIZED)
691 status = self._freeze(self._msg)
693 if STATUS.SUCCESS == status:
697 raise NotmuchError(status)
699 _thaw = nmlib.notmuch_message_thaw
700 _thaw.argtypes = [NotmuchMessageP]
701 _thaw.restype = c_uint
704 """Thaws the current 'message'
706 Thaw the current 'message', synchronizing any changes that may have
707 occurred while 'message' was frozen into the notmuch database.
709 See :meth:`freeze` for an example of how to use this
710 function to safely provide tag changes.
712 Multiple calls to freeze/thaw are valid and these calls with
713 "stack". That is there must be as many calls to thaw as to freeze
714 before a message is actually thawed.
716 :returns: STATUS.SUCCESS if the message was successfully frozen.
717 Raises an exception otherwise.
718 :exception: :exc:`NotmuchError`. They have the following meaning:
720 STATUS.UNBALANCED_FREEZE_THAW
721 An attempt was made to thaw an unfrozen message.
722 That is, there have been an unbalanced number of calls
723 to :meth:`freeze` and :meth:`thaw`.
724 STATUS.NOT_INITIALIZED
725 The message has not been initialized.
727 if self._msg is None:
728 raise NotmuchError(STATUS.NOT_INITIALIZED)
730 status = self._thaw(self._msg)
732 if STATUS.SUCCESS == status:
736 raise NotmuchError(status)
739 """(Not implemented)"""
740 return self.get_flag(Message.FLAG.MATCH)
742 def tags_to_maildir_flags(self):
743 """Synchronize notmuch tags to file Maildir flags
745 'D' if the message has the "draft" tag
746 'F' if the message has the "flagged" tag
747 'P' if the message has the "passed" tag
748 'R' if the message has the "replied" tag
749 'S' if the message does not have the "unread" tag
751 Any existing flags unmentioned in the list above will be
752 preserved in the renaming.
754 Also, if this filename is in a directory named "new", rename it
755 to be within the neighboring directory named "cur".
757 Do note that calling this method while a message is frozen might
758 not work yet, as the modified tags have not been committed yet
761 :returns: a :class:`STATUS`. In short, you want to see
762 notmuch.STATUS.SUCCESS here. See there for details."""
763 if self._msg is None:
764 raise NotmuchError(STATUS.NOT_INITIALIZED)
765 status = Message._tags_to_maildir_flags(self._msg)
767 def maildir_flags_to_tags(self):
768 """Synchronize file Maildir flags to notmuch tags
770 Flag Action if present
771 ---- -----------------
772 'D' Adds the "draft" tag to the message
773 'F' Adds the "flagged" tag to the message
774 'P' Adds the "passed" tag to the message
775 'R' Adds the "replied" tag to the message
776 'S' Removes the "unread" tag from the message
778 For each flag that is not present, the opposite action
779 (add/remove) is performed for the corresponding tags. If there
780 are multiple filenames associated with this message, the flag is
781 considered present if it appears in one or more filenames. (That
782 is, the flags from the multiple filenames are combined with the
783 logical OR operator.)
785 As a convenience, you can set the sync_maildir_flags parameter in
786 :meth:`Database.add_message` to implicitly call this.
788 :returns: a :class:`STATUS`. In short, you want to see
789 notmuch.STATUS.SUCCESS here. See there for details."""
790 if self._msg is None:
791 raise NotmuchError(STATUS.NOT_INITIALIZED)
792 status = Message._tags_to_maildir_flags(self._msg)
795 """Represent a Message() object by str()"""
796 return self.__str__()
799 """A message() is represented by a 1-line summary"""
801 msg['from'] = self.get_header('from')
802 msg['tags'] = self.get_tags()
803 msg['date'] = date.fromtimestamp(self.get_date())
804 return "%(from)s (%(date)s) (%(tags)s)" % (msg)
806 def get_message_parts(self):
807 """Output like notmuch show"""
808 fp = open(self.get_filename())
809 email_msg = email.message_from_file(fp)
813 for msg in email_msg.walk():
814 if not msg.is_multipart():
818 def get_part(self, num):
819 """Returns the nth message body part"""
820 parts = self.get_message_parts()
821 if (num <= 0 or num > len(parts)):
824 out_part = parts[(num - 1)]
825 return out_part.get_payload(decode=True)
827 def format_message_internal(self):
828 """Create an internal representation of the message parts,
829 which can easily be output to json, text, or another output
830 format. The argument match tells whether this matched a
833 output["id"] = self.get_message_id()
834 output["match"] = self.is_match()
835 output["filename"] = self.get_filename()
836 output["tags"] = list(self.get_tags())
839 for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
840 headers[h] = self.get_header(h)
841 output["headers"] = headers
844 parts = self.get_message_parts()
845 for i in xrange(len(parts)):
848 part_dict["id"] = i + 1
849 # We'll be using this is a lot, so let's just get it once.
850 cont_type = msg.get_content_type()
851 part_dict["content-type"] = cont_type
853 # Now we emulate the current behaviour, where it ignores
854 # the html if there's a text representation.
856 # This is being worked on, but it will be easier to fix
857 # here in the future than to end up with another
858 # incompatible solution.
859 disposition = msg["Content-Disposition"]
860 if disposition and disposition.lower().startswith("attachment"):
861 part_dict["filename"] = msg.get_filename()
863 if cont_type.lower() == "text/plain":
864 part_dict["content"] = msg.get_payload()
865 elif (cont_type.lower() == "text/html" and
867 part_dict["content"] = msg.get_payload()
868 body.append(part_dict)
870 output["body"] = body
874 def format_message_as_json(self, indent=0):
875 """Outputs the message as json. This is essentially the same
876 as python's dict format, but we run it through, just so we
877 don't have to worry about the details."""
878 return json.dumps(self.format_message_internal())
880 def format_message_as_text(self, indent=0):
881 """Outputs it in the old-fashioned notmuch text form. Will be
882 easy to change to a new format when the format changes."""
884 format = self.format_message_internal()
885 output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
886 % (format['id'], indent, format['match'], format['filename'])
887 output += "\n\fheader{"
889 #Todo: this date is supposed to be prettified, as in the index.
890 output += "\n%s (%s) (" % (format["headers"]["From"],
891 format["headers"]["Date"])
892 output += ", ".join(format["tags"])
895 output += "\nSubject: %s" % format["headers"]["Subject"]
896 output += "\nFrom: %s" % format["headers"]["From"]
897 output += "\nTo: %s" % format["headers"]["To"]
898 if format["headers"]["Cc"]:
899 output += "\nCc: %s" % format["headers"]["Cc"]
900 if format["headers"]["Bcc"]:
901 output += "\nBcc: %s" % format["headers"]["Bcc"]
902 output += "\nDate: %s" % format["headers"]["Date"]
903 output += "\n\fheader}"
905 output += "\n\fbody{"
907 parts = format["body"]
908 parts.sort(key=lambda x: x['id'])
910 if not "filename" in p:
911 output += "\n\fpart{ "
912 output += "ID: %d, Content-type: %s\n" % (p["id"],
915 output += "\n%s\n" % p["content"]
917 output += "Non-text part: %s\n" % p["content-type"]
918 output += "\n\fpart}"
920 output += "\n\fattachment{ "
921 output += "ID: %d, Content-type:%s\n" % (p["id"],
923 output += "Attachment: %s\n" % p["filename"]
924 output += "\n\fattachment}\n"
926 output += "\n\fbody}\n"
927 output += "\n\fmessage}"
932 """Implement hash(), so we can use Message() sets"""
933 file = self.get_filename()
938 def __cmp__(self, other):
939 """Implement cmp(), so we can compare Message()s
941 2 messages are considered equal if they point to the same
942 Message-Id and if they point to the same file names. If 2
943 Messages derive from different queries where some files have
944 been added or removed, the same messages would not be considered
945 equal (as they do not point to the same set of files
947 res = cmp(self.get_message_id(), other.get_message_id())
949 res = cmp(list(self.get_filenames()), list(other.get_filenames()))
952 _destroy = nmlib.notmuch_message_destroy
953 _destroy.argtypes = [NotmuchMessageP]
954 _destroy.restype = None
957 """Close and free the notmuch Message"""
958 if self._msg is not None:
959 self._destroy(self._msg)