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 :exception: :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 :exception: :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 :exception: :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 :exception: :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 :exception: :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 :exception: :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.
517 :exception: :exc:`NotInitializedError` if the message
521 raise NotInitializedError()
522 self._set_flag(self._msg, flag, value)
525 """Returns the message tags
527 :returns: A :class:`Tags` iterator.
528 :raises: :exc:`NotInitializedError` if the message is not
530 :raises: :exc:`NullPointerError` if any error occured
533 raise NotInitializedError()
535 tags_p = Message._get_tags(self._msg)
537 raise NullPointerError()
538 return Tags(tags_p, self)
540 _add_tag = nmlib.notmuch_message_add_tag
541 _add_tag.argtypes = [NotmuchMessageP, c_char_p]
542 _add_tag.restype = c_uint
544 def add_tag(self, tag, sync_maildir_flags=False):
545 """Adds a tag to the given message
547 Adds a tag to the current message. The maximal tag length is defined in
548 the notmuch library and is currently 200 bytes.
550 :param tag: String with a 'tag' to be added.
552 :param sync_maildir_flags: If notmuch configuration is set to do
553 this, add maildir flags corresponding to notmuch tags. See
554 underlying method :meth:`tags_to_maildir_flags`. Use False
555 if you want to add/remove many tags on a message without
556 having to physically rename the file every time. Do note,
557 that this will do nothing when a message is frozen, as tag
558 changes will not be committed to the database yet.
560 :returns: STATUS.SUCCESS if the tag was successfully added.
561 Raises an exception otherwise.
562 :raises: :exc:`NullPointerError` if the `tag` argument is NULL
563 :raises: :exc:`TagTooLongError` if the length of `tag` exceeds
564 Message.NOTMUCH_TAG_MAX)
565 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
566 in read-only mode so message cannot be modified
567 :raises: :exc:`NotInitializedError` if message has not been
571 raise NotInitializedError()
573 status = self._add_tag(self._msg, _str(tag))
575 # bail out on failure
576 if status != STATUS.SUCCESS:
577 raise NotmuchError(status)
579 if sync_maildir_flags:
580 self.tags_to_maildir_flags()
581 return STATUS.SUCCESS
583 _remove_tag = nmlib.notmuch_message_remove_tag
584 _remove_tag.argtypes = [NotmuchMessageP, c_char_p]
585 _remove_tag.restype = c_uint
587 def remove_tag(self, tag, sync_maildir_flags=False):
588 """Removes a tag from the given message
590 If the message has no such tag, this is a non-operation and
591 will report success anyway.
593 :param tag: String with a 'tag' to be removed.
594 :param sync_maildir_flags: If notmuch configuration is set to do
595 this, add maildir flags corresponding to notmuch tags. See
596 underlying method :meth:`tags_to_maildir_flags`. Use False
597 if you want to add/remove many tags on a message without
598 having to physically rename the file every time. Do note,
599 that this will do nothing when a message is frozen, as tag
600 changes will not be committed to the database yet.
602 :returns: STATUS.SUCCESS if the tag was successfully removed or if
603 the message had no such tag.
604 Raises an exception otherwise.
605 :raises: :exc:`NullPointerError` if the `tag` argument is NULL
606 :raises: :exc:`TagTooLongError` if the length of `tag` exceeds
607 Message.NOTMUCH_TAG_MAX)
608 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
609 in read-only mode so message cannot be modified
610 :raises: :exc:`NotInitializedError` if message has not been
614 raise NotInitializedError()
616 status = self._remove_tag(self._msg, _str(tag))
618 if status != STATUS.SUCCESS:
619 raise NotmuchError(status)
621 if sync_maildir_flags:
622 self.tags_to_maildir_flags()
623 return STATUS.SUCCESS
625 _remove_all_tags = nmlib.notmuch_message_remove_all_tags
626 _remove_all_tags.argtypes = [NotmuchMessageP]
627 _remove_all_tags.restype = c_uint
629 def remove_all_tags(self, sync_maildir_flags=False):
630 """Removes all tags from the given message.
632 See :meth:`freeze` for an example showing how to safely
636 :param sync_maildir_flags: If notmuch configuration is set to do
637 this, add maildir flags corresponding to notmuch tags. See
638 :meth:`tags_to_maildir_flags`. Use False if you want to
639 add/remove many tags on a message without having to
640 physically rename the file every time. Do note, that this
641 will do nothing when a message is frozen, as tag changes
642 will not be committed to the database yet.
644 :returns: STATUS.SUCCESS if the tags were successfully removed.
645 Raises an exception otherwise.
646 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
647 in read-only mode so message cannot be modified
648 :raises: :exc:`NotInitializedError` if message has not been
652 raise NotInitializedError()
654 status = self._remove_all_tags(self._msg)
657 if status != STATUS.SUCCESS:
658 raise NotmuchError(status)
660 if sync_maildir_flags:
661 self.tags_to_maildir_flags()
662 return STATUS.SUCCESS
664 _freeze = nmlib.notmuch_message_freeze
665 _freeze.argtypes = [NotmuchMessageP]
666 _freeze.restype = c_uint
669 """Freezes the current state of 'message' within the database
671 This means that changes to the message state, (via :meth:`add_tag`,
672 :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
673 committed to the database until the message is :meth:`thaw` ed.
675 Multiple calls to freeze/thaw are valid and these calls will
676 "stack". That is there must be as many calls to thaw as to freeze
677 before a message is actually thawed.
679 The ability to do freeze/thaw allows for safe transactions to
680 change tag values. For example, explicitly setting a message to
681 have a given set of tags might look like this::
684 msg.remove_all_tags(False)
686 msg.add_tag(tag, False)
688 msg.tags_to_maildir_flags()
690 With freeze/thaw used like this, the message in the database is
691 guaranteed to have either the full set of original tag values, or
692 the full set of new tag values, but nothing in between.
694 Imagine the example above without freeze/thaw and the operation
695 somehow getting interrupted. This could result in the message being
696 left with no tags if the interruption happened after
697 :meth:`remove_all_tags` but before :meth:`add_tag`.
699 :returns: STATUS.SUCCESS if the message was successfully frozen.
700 Raises an exception otherwise.
701 :raises: :exc:`ReadOnlyDatabaseError` if the database was opened
702 in read-only mode so message cannot be modified
703 :raises: :exc:`NotInitializedError` if message has not been
707 raise NotInitializedError()
709 status = self._freeze(self._msg)
711 if STATUS.SUCCESS == status:
715 raise NotmuchError(status)
717 _thaw = nmlib.notmuch_message_thaw
718 _thaw.argtypes = [NotmuchMessageP]
719 _thaw.restype = c_uint
722 """Thaws the current 'message'
724 Thaw the current 'message', synchronizing any changes that may have
725 occurred while 'message' was frozen into the notmuch database.
727 See :meth:`freeze` for an example of how to use this
728 function to safely provide tag changes.
730 Multiple calls to freeze/thaw are valid and these calls with
731 "stack". That is there must be as many calls to thaw as to freeze
732 before a message is actually thawed.
734 :returns: STATUS.SUCCESS if the message was successfully frozen.
735 Raises an exception otherwise.
736 :raises: :exc:`UnbalancedFreezeThawError` if an attempt was made
737 to thaw an unfrozen message. That is, there have been
738 an unbalanced number of calls to :meth:`freeze` and
740 :raises: :exc:`NotInitializedError` if message has not been
744 raise NotInitializedError()
746 status = self._thaw(self._msg)
748 if STATUS.SUCCESS == status:
752 raise NotmuchError(status)
755 """(Not implemented)"""
756 return self.get_flag(Message.FLAG.MATCH)
758 def tags_to_maildir_flags(self):
759 """Synchronize notmuch tags to file Maildir flags
761 'D' if the message has the "draft" tag
762 'F' if the message has the "flagged" tag
763 'P' if the message has the "passed" tag
764 'R' if the message has the "replied" tag
765 'S' if the message does not have the "unread" tag
767 Any existing flags unmentioned in the list above will be
768 preserved in the renaming.
770 Also, if this filename is in a directory named "new", rename it
771 to be within the neighboring directory named "cur".
773 Do note that calling this method while a message is frozen might
774 not work yet, as the modified tags have not been committed yet
777 :returns: a :class:`STATUS` value. In short, you want to see
778 notmuch.STATUS.SUCCESS here. See there for details."""
780 raise NotInitializedError()
781 return Message._tags_to_maildir_flags(self._msg)
783 def maildir_flags_to_tags(self):
784 """Synchronize file Maildir flags to notmuch tags
786 Flag Action if present
787 ---- -----------------
788 'D' Adds the "draft" tag to the message
789 'F' Adds the "flagged" tag to the message
790 'P' Adds the "passed" tag to the message
791 'R' Adds the "replied" tag to the message
792 'S' Removes the "unread" tag from the message
794 For each flag that is not present, the opposite action
795 (add/remove) is performed for the corresponding tags. If there
796 are multiple filenames associated with this message, the flag is
797 considered present if it appears in one or more filenames. (That
798 is, the flags from the multiple filenames are combined with the
799 logical OR operator.)
801 As a convenience, you can set the sync_maildir_flags parameter in
802 :meth:`Database.add_message` to implicitly call this.
804 :returns: a :class:`STATUS`. In short, you want to see
805 notmuch.STATUS.SUCCESS here. See there for details."""
807 raise NotInitializedError()
808 return Message._tags_to_maildir_flags(self._msg)
811 """Represent a Message() object by str()"""
812 return self.__str__()
814 def __unicode__(self):
815 format = "%s (%s) (%s)"
816 return format % (self.get_header('from'),
818 date.fromtimestamp(self.get_date()),
821 def get_message_parts(self):
822 """Output like notmuch show"""
823 fp = open(self.get_filename())
824 email_msg = email.message_from_file(fp)
828 for msg in email_msg.walk():
829 if not msg.is_multipart():
833 def get_part(self, num):
834 """Returns the nth message body part"""
835 parts = self.get_message_parts()
836 if (num <= 0 or num > len(parts)):
839 out_part = parts[(num - 1)]
840 return out_part.get_payload(decode=True)
842 def format_message_internal(self):
843 """Create an internal representation of the message parts,
844 which can easily be output to json, text, or another output
845 format. The argument match tells whether this matched a
848 output["id"] = self.get_message_id()
849 output["match"] = self.is_match()
850 output["filename"] = self.get_filename()
851 output["tags"] = list(self.get_tags())
854 for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
855 headers[h] = self.get_header(h)
856 output["headers"] = headers
859 parts = self.get_message_parts()
860 for i in xrange(len(parts)):
863 part_dict["id"] = i + 1
864 # We'll be using this is a lot, so let's just get it once.
865 cont_type = msg.get_content_type()
866 part_dict["content-type"] = cont_type
868 # Now we emulate the current behaviour, where it ignores
869 # the html if there's a text representation.
871 # This is being worked on, but it will be easier to fix
872 # here in the future than to end up with another
873 # incompatible solution.
874 disposition = msg["Content-Disposition"]
875 if disposition and disposition.lower().startswith("attachment"):
876 part_dict["filename"] = msg.get_filename()
878 if cont_type.lower() == "text/plain":
879 part_dict["content"] = msg.get_payload()
880 elif (cont_type.lower() == "text/html" and
882 part_dict["content"] = msg.get_payload()
883 body.append(part_dict)
885 output["body"] = body
889 def format_message_as_json(self, indent=0):
890 """Outputs the message as json. This is essentially the same
891 as python's dict format, but we run it through, just so we
892 don't have to worry about the details."""
893 return json.dumps(self.format_message_internal())
895 def format_message_as_text(self, indent=0):
896 """Outputs it in the old-fashioned notmuch text form. Will be
897 easy to change to a new format when the format changes."""
899 format = self.format_message_internal()
900 output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
901 % (format['id'], indent, format['match'], format['filename'])
902 output += "\n\fheader{"
904 #Todo: this date is supposed to be prettified, as in the index.
905 output += "\n%s (%s) (" % (format["headers"]["From"],
906 format["headers"]["Date"])
907 output += ", ".join(format["tags"])
910 output += "\nSubject: %s" % format["headers"]["Subject"]
911 output += "\nFrom: %s" % format["headers"]["From"]
912 output += "\nTo: %s" % format["headers"]["To"]
913 if format["headers"]["Cc"]:
914 output += "\nCc: %s" % format["headers"]["Cc"]
915 if format["headers"]["Bcc"]:
916 output += "\nBcc: %s" % format["headers"]["Bcc"]
917 output += "\nDate: %s" % format["headers"]["Date"]
918 output += "\n\fheader}"
920 output += "\n\fbody{"
922 parts = format["body"]
923 parts.sort(key=lambda x: x['id'])
925 if not "filename" in p:
926 output += "\n\fpart{ "
927 output += "ID: %d, Content-type: %s\n" % (p["id"],
930 output += "\n%s\n" % p["content"]
932 output += "Non-text part: %s\n" % p["content-type"]
933 output += "\n\fpart}"
935 output += "\n\fattachment{ "
936 output += "ID: %d, Content-type:%s\n" % (p["id"],
938 output += "Attachment: %s\n" % p["filename"]
939 output += "\n\fattachment}\n"
941 output += "\n\fbody}\n"
942 output += "\n\fmessage}"
947 """Implement hash(), so we can use Message() sets"""
948 file = self.get_filename()
953 def __cmp__(self, other):
954 """Implement cmp(), so we can compare Message()s
956 2 messages are considered equal if they point to the same
957 Message-Id and if they point to the same file names. If 2
958 Messages derive from different queries where some files have
959 been added or removed, the same messages would not be considered
960 equal (as they do not point to the same set of files
962 res = cmp(self.get_message_id(), other.get_message_id())
964 res = cmp(list(self.get_filenames()), list(other.get_filenames()))
967 _destroy = nmlib.notmuch_message_destroy
968 _destroy.argtypes = [NotmuchMessageP]
969 _destroy.restype = None
972 """Close and free the notmuch Message"""
973 if self._msg is not None:
974 self._destroy(self._msg)