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 (
38 from notmuch.tag import Tags
39 from notmuch.filename import Filenames
43 import simplejson as json
48 class Messages(object):
49 """Represents a list of notmuch messages
51 This object provides an iterator over a list of notmuch messages
52 (Technically, it provides a wrapper for the underlying
53 *notmuch_messages_t* structure). Do note that the underlying library
54 only provides a one-time iterator (it cannot reset the iterator to
55 the start). Thus iterating over the function will "exhaust" the list
56 of messages, and a subsequent iteration attempt will raise a
57 :exc:`NotInitializedError`. If you need to
58 re-iterate over a list of messages you will need to retrieve a new
59 :class:`Messages` object or cache your :class:`Message`\s in a list
64 You can store and reuse the single :class:`Message` objects as often
65 as you want as long as you keep the parent :class:`Messages` object
66 around. (Due to hierarchical memory allocation, all derived
67 :class:`Message` objects will be invalid when we delete the parent
68 :class:`Messages` object, even if it was already exhausted.) So
72 msgs = Query(db,'').search_messages() #get a Messages() object
75 # msgs is "exhausted" now and msgs.next() will raise an exception.
76 # However it will be kept alive until all retrieved Message()
77 # objects are also deleted. If you do e.g. an explicit del(msgs)
78 # here, the following lines would fail.
80 # You can reiterate over *msglist* however as often as you want.
81 # It is simply a list with :class:`Message`s.
83 print (msglist[0].get_filename())
84 print (msglist[1].get_filename())
85 print (msglist[0].get_message_id())
88 As :class:`Message` implements both __hash__() and __cmp__(), it is
89 possible to make sets out of :class:`Messages` and use set
90 arithmetic (this happens in python and will of course be *much*
91 slower than redoing a proper query with the appropriate filters::
93 s1, s2 = set(msgs1), set(msgs2)
98 Be careful when using set arithmetic between message sets derived
99 from different Databases (ie the same database reopened after
100 messages have changed). If messages have added or removed associated
101 files in the meantime, it is possible that the same message would be
102 considered as a different object (as it points to a different file).
105 #notmuch_messages_get
106 _get = nmlib.notmuch_messages_get
107 _get.argtypes = [NotmuchMessagesP]
108 _get.restype = NotmuchMessageP
110 _collect_tags = nmlib.notmuch_messages_collect_tags
111 _collect_tags.argtypes = [NotmuchMessagesP]
112 _collect_tags.restype = NotmuchTagsP
114 def __init__(self, msgs_p, parent=None):
116 :param msgs_p: A pointer to an underlying *notmuch_messages_t*
117 structure. These are not publically exposed, so a user
118 will almost never instantiate a :class:`Messages` object
119 herself. They are usually handed back as a result,
120 e.g. in :meth:`Query.search_messages`. *msgs_p* must be
121 valid, we will raise an :exc:`NullPointerError` if it is
123 :type msgs_p: :class:`ctypes.c_void_p`
124 :param parent: The parent object
125 (ie :class:`Query`) these tags are derived from. It saves
126 a reference to it, so we can automatically delete the db
127 object once all derived objects are dead.
128 :TODO: Make the iterator work more than once and cache the tags in
129 the Python object.(?)
132 raise NullPointerError()
135 #store parent, so we keep them alive as long as self is alive
136 self._parent = parent
138 def collect_tags(self):
139 """Return the unique :class:`Tags` in the contained messages
141 :returns: :class:`Tags`
142 :exceptions: :exc:`NotInitializedError` if not init'ed
146 :meth:`collect_tags` will iterate over the messages and therefore
147 will not allow further iterations.
150 raise NotInitializedError()
152 # collect all tags (returns NULL on error)
153 tags_p = Messages._collect_tags(self._msgs)
154 #reset _msgs as we iterated over it and can do so only once
158 raise NullPointerError()
159 return Tags(tags_p, self)
162 """ Make Messages an iterator """
165 _valid = nmlib.notmuch_messages_valid
166 _valid.argtypes = [NotmuchMessagesP]
167 _valid.restype = bool
169 _move_to_next = nmlib.notmuch_messages_move_to_next
170 _move_to_next.argtypes = [NotmuchMessagesP]
171 _move_to_next.restype = None
175 raise NotInitializedError()
177 if not self._valid(self._msgs):
181 msg = Message(Messages._get(self._msgs), self)
182 self._move_to_next(self._msgs)
184 next = __next__ # python2.x iterator protocol compatibility
186 def __nonzero__(self):
188 :return: True if there is at least one more thread in the
189 Iterator, False if not."""
190 return self._msgs is not None and \
191 self._valid(self._msgs) > 0
193 _destroy = nmlib.notmuch_messages_destroy
194 _destroy.argtypes = [NotmuchMessagesP]
195 _destroy.restype = None
198 """Close and free the notmuch Messages"""
199 if self._msgs is not None:
200 self._destroy(self._msgs)
202 def format_messages(self, format, indent=0, entire_thread=False):
203 """Formats messages as needed for 'notmuch show'.
205 :param format: A string of either 'text' or 'json'.
206 :param indent: A number indicating the reply depth of these messages.
207 :param entire_thread: A bool, indicating whether we want to output
208 whole threads or only the matching messages.
209 :return: a list of lines
213 if format.lower() == "text":
217 elif format.lower() == "json":
222 raise TypeError("format must be either 'text' or 'json'")
226 result.append(set_start)
228 # iterate through all toplevel messages in this thread
233 result.append(set_sep)
236 result.append(set_start)
237 match = msg.is_match()
240 if (match or entire_thread):
241 if format.lower() == "text":
242 result.append(msg.format_message_as_text(indent))
244 result.append(msg.format_message_as_json(indent))
245 next_indent = indent + 1
247 # get replies and print them also out (if there are any)
248 replies = msg.get_replies().format_messages(format, next_indent, entire_thread)
250 result.append(set_sep)
251 result.extend(replies)
253 result.append(set_end)
254 result.append(set_end)
258 def print_messages(self, format, indent=0, entire_thread=False, handle=sys.stdout):
259 """Outputs messages as needed for 'notmuch show' to a file like object.
261 :param format: A string of either 'text' or 'json'.
262 :param handle: A file like object to print to (default is sys.stdout).
263 :param indent: A number indicating the reply depth of these messages.
264 :param entire_thread: A bool, indicating whether we want to output
265 whole threads or only the matching messages.
267 handle.write(''.join(self.format_messages(format, indent, entire_thread)))
270 class EmptyMessagesResult(Messages):
271 def __init__(self, parent):
273 self._parent = parent
276 raise StopIteration()
280 class Message(Python3StringMixIn):
281 """Represents a single Email message
283 Technically, this wraps the underlying *notmuch_message_t*
284 structure. A user will usually not create these objects themselves
285 but get them as search results.
287 As it implements :meth:`__cmp__`, it is possible to compare two
288 :class:`Message`\s using `if msg1 == msg2: ...`.
291 """notmuch_message_get_filename (notmuch_message_t *message)"""
292 _get_filename = nmlib.notmuch_message_get_filename
293 _get_filename.argtypes = [NotmuchMessageP]
294 _get_filename.restype = c_char_p
296 """return all filenames for a message"""
297 _get_filenames = nmlib.notmuch_message_get_filenames
298 _get_filenames.argtypes = [NotmuchMessageP]
299 _get_filenames.restype = NotmuchFilenamesP
301 """notmuch_message_get_flag"""
302 _get_flag = nmlib.notmuch_message_get_flag
303 _get_flag.argtypes = [NotmuchMessageP, c_uint]
304 _get_flag.restype = bool
306 """notmuch_message_set_flag"""
307 _set_flag = nmlib.notmuch_message_set_flag
308 _set_flag.argtypes = [NotmuchMessageP, c_uint, c_int]
309 _set_flag.restype = None
311 """notmuch_message_get_message_id (notmuch_message_t *message)"""
312 _get_message_id = nmlib.notmuch_message_get_message_id
313 _get_message_id.argtypes = [NotmuchMessageP]
314 _get_message_id.restype = c_char_p
316 """notmuch_message_get_thread_id"""
317 _get_thread_id = nmlib.notmuch_message_get_thread_id
318 _get_thread_id.argtypes = [NotmuchMessageP]
319 _get_thread_id.restype = c_char_p
321 """notmuch_message_get_replies"""
322 _get_replies = nmlib.notmuch_message_get_replies
323 _get_replies.argtypes = [NotmuchMessageP]
324 _get_replies.restype = NotmuchMessagesP
326 """notmuch_message_get_tags (notmuch_message_t *message)"""
327 _get_tags = nmlib.notmuch_message_get_tags
328 _get_tags.argtypes = [NotmuchMessageP]
329 _get_tags.restype = NotmuchTagsP
331 _get_date = nmlib.notmuch_message_get_date
332 _get_date.argtypes = [NotmuchMessageP]
333 _get_date.restype = c_long
335 _get_header = nmlib.notmuch_message_get_header
336 _get_header.argtypes = [NotmuchMessageP, c_char_p]
337 _get_header.restype = c_char_p
339 """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
340 _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
341 _tags_to_maildir_flags.argtypes = [NotmuchMessageP]
342 _tags_to_maildir_flags.restype = c_int
344 """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
345 _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
346 _maildir_flags_to_tags.argtypes = [NotmuchMessageP]
347 _maildir_flags_to_tags.restype = c_int
349 #Constants: Flags that can be set/get with set_flag
350 FLAG = Enum(['MATCH'])
352 def __init__(self, msg_p, parent=None):
354 :param msg_p: A pointer to an internal notmuch_message_t
355 Structure. If it is `None`, we will raise an
356 :exc:`NullPointerError`.
358 :param parent: A 'parent' object is passed which this message is
359 derived from. We save a reference to it, so we can
360 automatically delete the parent object once all derived
364 raise NullPointerError()
366 #keep reference to parent, so we keep it alive
367 self._parent = parent
369 def get_message_id(self):
370 """Returns the message ID
372 :returns: String with a message ID
373 :raises: :exc:`NotInitializedError` if the message
377 raise NotInitializedError()
378 return Message._get_message_id(self._msg).decode('utf-8', 'ignore')
380 def get_thread_id(self):
381 """Returns the thread ID
383 The returned string belongs to 'message' will only be valid for as
384 long as the message is valid.
386 This function will not return `None` since Notmuch ensures that every
387 message belongs to a single thread.
389 :returns: String with a thread ID
390 :raises: :exc:`NotInitializedError` if the message
394 raise NotInitializedError()
396 return Message._get_thread_id(self._msg).decode('utf-8', 'ignore')
398 def get_replies(self):
399 """Gets all direct replies to this message as :class:`Messages`
404 This call only makes sense if 'message' was ultimately obtained from
405 a :class:`Thread` object, (such as by coming directly from the
406 result of calling :meth:`Thread.get_toplevel_messages` or by any
407 number of subsequent calls to :meth:`get_replies`). If this message
408 was obtained through some non-thread means, (such as by a call to
409 :meth:`Query.search_messages`), then this function will return
410 an empty Messages iterator.
412 :returns: :class:`Messages`.
413 :raises: :exc:`NotInitializedError` if the message
417 raise NotInitializedError()
419 msgs_p = Message._get_replies(self._msg)
422 return EmptyMessagesResult(self)
424 return Messages(msgs_p, self)
427 """Returns time_t of the message date
429 For the original textual representation of the Date header from the
430 message call notmuch_message_get_header() with a header value of
433 :returns: A time_t timestamp.
435 :raises: :exc:`NotInitializedError` if the message
439 raise NotInitializedError()
440 return Message._get_date(self._msg)
442 def get_header(self, header):
443 """Get the value of the specified header.
445 The value will be read from the actual message file, not from
446 the notmuch database. The header name is case insensitive.
448 Returns an empty string ("") if the message does not contain a
449 header line matching 'header'.
451 :param header: The name of the header to be retrieved.
452 It is not case-sensitive.
454 :returns: The header value as string
455 :raises: :exc:`NotInitializedError` if the message is not
457 :raises: :exc:`NullPointerError` if any error occured
460 raise NotInitializedError()
462 #Returns NULL if any error occurs.
463 header = Message._get_header(self._msg, _str(header))
465 raise NullPointerError()
466 return header.decode('UTF-8', 'ignore')
468 def get_filename(self):
469 """Returns the file path of the message file
471 :returns: Absolute file path & name of the message file
472 :raises: :exc:`NotInitializedError` if the message
476 raise NotInitializedError()
477 return Message._get_filename(self._msg).decode('utf-8', 'ignore')
479 def get_filenames(self):
480 """Get all filenames for the email corresponding to 'message'
482 Returns a Filenames() generator with all absolute filepaths for
483 messages recorded to have the same Message-ID. These files must
484 not necessarily have identical content."""
486 raise NotInitializedError()
488 files_p = Message._get_filenames(self._msg)
490 return Filenames(files_p, self).as_generator()
492 def get_flag(self, flag):
493 """Checks whether a specific flag is set for this message
495 The method :meth:`Query.search_threads` sets
496 *Message.FLAG.MATCH* for those messages that match the
497 query. This method allows us to get the value of this flag.
499 :param flag: One of the :attr:`Message.FLAG` values (currently only
501 :returns: An unsigned int (0/1), indicating whether the flag is set.
502 :raises: :exc:`NotInitializedError` if the message
506 raise NotInitializedError()
507 return Message._get_flag(self._msg, flag)
509 def set_flag(self, flag, value):
510 """Sets/Unsets a specific flag for this message
512 :param flag: One of the :attr:`Message.FLAG` values (currently only
514 :param value: A bool indicating whether to set or unset the flag.
516 :raises: :exc:`NotInitializedError` if the message
520 raise NotInitializedError()
521 self._set_flag(self._msg, flag, value)
524 """Returns the message tags
526 :returns: A :class:`Tags` iterator.
527 :raises: :exc:`NotInitializedError` if the message is not
529 :raises: :exc:`NullPointerError` if any error occured
532 raise NotInitializedError()
534 tags_p = Message._get_tags(self._msg)
536 raise NullPointerError()
537 return Tags(tags_p, self)
539 _add_tag = nmlib.notmuch_message_add_tag
540 _add_tag.argtypes = [NotmuchMessageP, c_char_p]
541 _add_tag.restype = c_uint
543 def add_tag(self, tag, sync_maildir_flags=False):
544 """Adds a tag to the given message
546 Adds a tag to the current message. The maximal tag length is defined in
547 the notmuch library and is currently 200 bytes.
549 :param tag: String with a 'tag' to be added.
551 :param sync_maildir_flags: If notmuch configuration is set to do
552 this, add maildir flags corresponding to notmuch tags. See
553 underlying method :meth:`tags_to_maildir_flags`. Use False
554 if you want to add/remove many tags on a message without
555 having to physically rename the file every time. Do note,
556 that this will do nothing when a message is frozen, as tag
557 changes will not be committed to the database yet.
559 :returns: STATUS.SUCCESS if the tag was successfully added.
560 Raises an exception otherwise.
561 :raises: :exc:`NullPointerError` if the `tag` argument is NULL
562 :raises: :exc:`TagTooLongError` if the length of `tag` exceeds
563 Message.NOTMUCH_TAG_MAX)
564 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
565 in read-only mode so message cannot be modified
566 :raises: :exc:`NotInitializedError` if message has not been
570 raise NotInitializedError()
572 status = self._add_tag(self._msg, _str(tag))
574 # bail out on failure
575 if status != STATUS.SUCCESS:
576 raise NotmuchError(status)
578 if sync_maildir_flags:
579 self.tags_to_maildir_flags()
580 return STATUS.SUCCESS
582 _remove_tag = nmlib.notmuch_message_remove_tag
583 _remove_tag.argtypes = [NotmuchMessageP, c_char_p]
584 _remove_tag.restype = c_uint
586 def remove_tag(self, tag, sync_maildir_flags=False):
587 """Removes a tag from the given message
589 If the message has no such tag, this is a non-operation and
590 will report success anyway.
592 :param tag: String with a 'tag' to be removed.
593 :param sync_maildir_flags: If notmuch configuration is set to do
594 this, add maildir flags corresponding to notmuch tags. See
595 underlying method :meth:`tags_to_maildir_flags`. Use False
596 if you want to add/remove many tags on a message without
597 having to physically rename the file every time. Do note,
598 that this will do nothing when a message is frozen, as tag
599 changes will not be committed to the database yet.
601 :returns: STATUS.SUCCESS if the tag was successfully removed or if
602 the message had no such tag.
603 Raises an exception otherwise.
604 :raises: :exc:`NullPointerError` if the `tag` argument is NULL
605 :raises: :exc:`TagTooLongError` if the length of `tag` exceeds
606 Message.NOTMUCH_TAG_MAX)
607 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
608 in read-only mode so message cannot be modified
609 :raises: :exc:`NotInitializedError` if message has not been
613 raise NotInitializedError()
615 status = self._remove_tag(self._msg, _str(tag))
617 if status != STATUS.SUCCESS:
618 raise NotmuchError(status)
620 if sync_maildir_flags:
621 self.tags_to_maildir_flags()
622 return STATUS.SUCCESS
624 _remove_all_tags = nmlib.notmuch_message_remove_all_tags
625 _remove_all_tags.argtypes = [NotmuchMessageP]
626 _remove_all_tags.restype = c_uint
628 def remove_all_tags(self, sync_maildir_flags=False):
629 """Removes all tags from the given message.
631 See :meth:`freeze` for an example showing how to safely
635 :param sync_maildir_flags: If notmuch configuration is set to do
636 this, add maildir flags corresponding to notmuch tags. See
637 :meth:`tags_to_maildir_flags`. Use False if you want to
638 add/remove many tags on a message without having to
639 physically rename the file every time. Do note, that this
640 will do nothing when a message is frozen, as tag changes
641 will not be committed to the database yet.
643 :returns: STATUS.SUCCESS if the tags were successfully removed.
644 Raises an exception otherwise.
645 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
646 in read-only mode so message cannot be modified
647 :raises: :exc:`NotInitializedError` if message has not been
651 raise NotInitializedError()
653 status = self._remove_all_tags(self._msg)
656 if status != STATUS.SUCCESS:
657 raise NotmuchError(status)
659 if sync_maildir_flags:
660 self.tags_to_maildir_flags()
661 return STATUS.SUCCESS
663 _freeze = nmlib.notmuch_message_freeze
664 _freeze.argtypes = [NotmuchMessageP]
665 _freeze.restype = c_uint
668 """Freezes the current state of 'message' within the database
670 This means that changes to the message state, (via :meth:`add_tag`,
671 :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
672 committed to the database until the message is :meth:`thaw` ed.
674 Multiple calls to freeze/thaw are valid and these calls will
675 "stack". That is there must be as many calls to thaw as to freeze
676 before a message is actually thawed.
678 The ability to do freeze/thaw allows for safe transactions to
679 change tag values. For example, explicitly setting a message to
680 have a given set of tags might look like this::
683 msg.remove_all_tags(False)
685 msg.add_tag(tag, False)
687 msg.tags_to_maildir_flags()
689 With freeze/thaw used like this, the message in the database is
690 guaranteed to have either the full set of original tag values, or
691 the full set of new tag values, but nothing in between.
693 Imagine the example above without freeze/thaw and the operation
694 somehow getting interrupted. This could result in the message being
695 left with no tags if the interruption happened after
696 :meth:`remove_all_tags` but before :meth:`add_tag`.
698 :returns: STATUS.SUCCESS if the message was successfully frozen.
699 Raises an exception otherwise.
700 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
701 in read-only mode so message cannot be modified
702 :raises: :exc:`NotInitializedError` if message has not been
706 raise NotInitializedError()
708 status = self._freeze(self._msg)
710 if STATUS.SUCCESS == status:
714 raise NotmuchError(status)
716 _thaw = nmlib.notmuch_message_thaw
717 _thaw.argtypes = [NotmuchMessageP]
718 _thaw.restype = c_uint
721 """Thaws the current 'message'
723 Thaw the current 'message', synchronizing any changes that may have
724 occurred while 'message' was frozen into the notmuch database.
726 See :meth:`freeze` for an example of how to use this
727 function to safely provide tag changes.
729 Multiple calls to freeze/thaw are valid and these calls with
730 "stack". That is there must be as many calls to thaw as to freeze
731 before a message is actually thawed.
733 :returns: STATUS.SUCCESS if the message was successfully frozen.
734 Raises an exception otherwise.
735 :raises: :exc:`UnbalancedFreezeThawError` if an attempt was made
736 to thaw an unfrozen message. That is, there have been
737 an unbalanced number of calls to :meth:`freeze` and
739 :raises: :exc:`NotInitializedError` if message has not been
743 raise NotInitializedError()
745 status = self._thaw(self._msg)
747 if STATUS.SUCCESS == status:
751 raise NotmuchError(status)
754 """(Not implemented)"""
755 return self.get_flag(Message.FLAG.MATCH)
757 def tags_to_maildir_flags(self):
758 """Synchronize notmuch tags to file Maildir flags
760 'D' if the message has the "draft" tag
761 'F' if the message has the "flagged" tag
762 'P' if the message has the "passed" tag
763 'R' if the message has the "replied" tag
764 'S' if the message does not have the "unread" tag
766 Any existing flags unmentioned in the list above will be
767 preserved in the renaming.
769 Also, if this filename is in a directory named "new", rename it
770 to be within the neighboring directory named "cur".
772 Do note that calling this method while a message is frozen might
773 not work yet, as the modified tags have not been committed yet
776 :returns: a :class:`STATUS` value. In short, you want to see
777 notmuch.STATUS.SUCCESS here. See there for details."""
779 raise NotInitializedError()
780 return Message._tags_to_maildir_flags(self._msg)
782 def maildir_flags_to_tags(self):
783 """Synchronize file Maildir flags to notmuch tags
785 Flag Action if present
786 ---- -----------------
787 'D' Adds the "draft" tag to the message
788 'F' Adds the "flagged" tag to the message
789 'P' Adds the "passed" tag to the message
790 'R' Adds the "replied" tag to the message
791 'S' Removes the "unread" tag from the message
793 For each flag that is not present, the opposite action
794 (add/remove) is performed for the corresponding tags. If there
795 are multiple filenames associated with this message, the flag is
796 considered present if it appears in one or more filenames. (That
797 is, the flags from the multiple filenames are combined with the
798 logical OR operator.)
800 As a convenience, you can set the sync_maildir_flags parameter in
801 :meth:`Database.add_message` to implicitly call this.
803 :returns: a :class:`STATUS`. In short, you want to see
804 notmuch.STATUS.SUCCESS here. See there for details."""
806 raise NotInitializedError()
807 return Message._tags_to_maildir_flags(self._msg)
810 """Represent a Message() object by str()"""
811 return self.__str__()
813 def __unicode__(self):
814 format = "%s (%s) (%s)"
815 return format % (self.get_header('from'),
817 date.fromtimestamp(self.get_date()),
820 def get_message_parts(self):
821 """Output like notmuch show"""
822 fp = open(self.get_filename())
823 email_msg = email.message_from_file(fp)
827 for msg in email_msg.walk():
828 if not msg.is_multipart():
832 def get_part(self, num):
833 """Returns the nth message body part"""
834 parts = self.get_message_parts()
835 if (num <= 0 or num > len(parts)):
838 out_part = parts[(num - 1)]
839 return out_part.get_payload(decode=True)
841 def format_message_internal(self):
842 """Create an internal representation of the message parts,
843 which can easily be output to json, text, or another output
844 format. The argument match tells whether this matched a
847 output["id"] = self.get_message_id()
848 output["match"] = self.is_match()
849 output["filename"] = self.get_filename()
850 output["tags"] = list(self.get_tags())
853 for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
854 headers[h] = self.get_header(h)
855 output["headers"] = headers
858 parts = self.get_message_parts()
859 for i in xrange(len(parts)):
862 part_dict["id"] = i + 1
863 # We'll be using this is a lot, so let's just get it once.
864 cont_type = msg.get_content_type()
865 part_dict["content-type"] = cont_type
867 # Now we emulate the current behaviour, where it ignores
868 # the html if there's a text representation.
870 # This is being worked on, but it will be easier to fix
871 # here in the future than to end up with another
872 # incompatible solution.
873 disposition = msg["Content-Disposition"]
874 if disposition and disposition.lower().startswith("attachment"):
875 part_dict["filename"] = msg.get_filename()
877 if cont_type.lower() == "text/plain":
878 part_dict["content"] = msg.get_payload()
879 elif (cont_type.lower() == "text/html" and
881 part_dict["content"] = msg.get_payload()
882 body.append(part_dict)
884 output["body"] = body
888 def format_message_as_json(self, indent=0):
889 """Outputs the message as json. This is essentially the same
890 as python's dict format, but we run it through, just so we
891 don't have to worry about the details."""
892 return json.dumps(self.format_message_internal())
894 def format_message_as_text(self, indent=0):
895 """Outputs it in the old-fashioned notmuch text form. Will be
896 easy to change to a new format when the format changes."""
898 format = self.format_message_internal()
899 output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
900 % (format['id'], indent, format['match'], format['filename'])
901 output += "\n\fheader{"
903 #Todo: this date is supposed to be prettified, as in the index.
904 output += "\n%s (%s) (" % (format["headers"]["From"],
905 format["headers"]["Date"])
906 output += ", ".join(format["tags"])
909 output += "\nSubject: %s" % format["headers"]["Subject"]
910 output += "\nFrom: %s" % format["headers"]["From"]
911 output += "\nTo: %s" % format["headers"]["To"]
912 if format["headers"]["Cc"]:
913 output += "\nCc: %s" % format["headers"]["Cc"]
914 if format["headers"]["Bcc"]:
915 output += "\nBcc: %s" % format["headers"]["Bcc"]
916 output += "\nDate: %s" % format["headers"]["Date"]
917 output += "\n\fheader}"
919 output += "\n\fbody{"
921 parts = format["body"]
922 parts.sort(key=lambda x: x['id'])
924 if not "filename" in p:
925 output += "\n\fpart{ "
926 output += "ID: %d, Content-type: %s\n" % (p["id"],
929 output += "\n%s\n" % p["content"]
931 output += "Non-text part: %s\n" % p["content-type"]
932 output += "\n\fpart}"
934 output += "\n\fattachment{ "
935 output += "ID: %d, Content-type:%s\n" % (p["id"],
937 output += "Attachment: %s\n" % p["filename"]
938 output += "\n\fattachment}\n"
940 output += "\n\fbody}\n"
941 output += "\n\fmessage}"
946 """Implement hash(), so we can use Message() sets"""
947 file = self.get_filename()
952 def __cmp__(self, other):
953 """Implement cmp(), so we can compare Message()s
955 2 messages are considered equal if they point to the same
956 Message-Id and if they point to the same file names. If 2
957 Messages derive from different queries where some files have
958 been added or removed, the same messages would not be considered
959 equal (as they do not point to the same set of files
961 res = cmp(self.get_message_id(), other.get_message_id())
963 res = cmp(list(self.get_filenames()), list(other.get_filenames()))
966 _destroy = nmlib.notmuch_message_destroy
967 _destroy.argtypes = [NotmuchMessageP]
968 _destroy.restype = None
971 """Close and free the notmuch Message"""
972 if self._msg is not None:
973 self._destroy(self._msg)