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_void_p, c_long, c_uint, c_int
23 from datetime import date
24 from notmuch.globals import nmlib, STATUS, NotmuchError, Enum, _str
25 from notmuch.tag import Tags
26 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.restype = c_void_p
97 _collect_tags = nmlib.notmuch_messages_collect_tags
98 _collect_tags.restype = c_void_p
100 def __init__(self, msgs_p, parent=None):
102 :param msgs_p: A pointer to an underlying *notmuch_messages_t*
103 structure. These are not publically exposed, so a user
104 will almost never instantiate a :class:`Messages` object
105 herself. They are usually handed back as a result,
106 e.g. in :meth:`Query.search_messages`. *msgs_p* must be
107 valid, we will raise an :exc:`NotmuchError`
108 (STATUS.NULL_POINTER) if it is `None`.
109 :type msgs_p: :class:`ctypes.c_void_p`
110 :param parent: The parent object
111 (ie :class:`Query`) these tags are derived from. It saves
112 a reference to it, so we can automatically delete the db
113 object once all derived objects are dead.
114 :TODO: Make the iterator work more than once and cache the tags in
115 the Python object.(?)
118 NotmuchError(STATUS.NULL_POINTER)
121 #store parent, so we keep them alive as long as self is alive
122 self._parent = parent
124 def collect_tags(self):
125 """Return the unique :class:`Tags` in the contained messages
127 :returns: :class:`Tags`
128 :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not inited
130 .. note:: :meth:`collect_tags` will iterate over the messages and
131 therefore will not allow further iterations.
133 if self._msgs is None:
134 raise NotmuchError(STATUS.NOT_INITIALIZED)
136 # collect all tags (returns NULL on error)
137 tags_p = Messages._collect_tags(self._msgs)
138 #reset _msgs as we iterated over it and can do so only once
142 raise NotmuchError(STATUS.NULL_POINTER)
143 return Tags(tags_p, self)
146 """ Make Messages an iterator """
150 if self._msgs is None:
151 raise NotmuchError(STATUS.NOT_INITIALIZED)
153 if not nmlib.notmuch_messages_valid(self._msgs):
157 msg = Message(Messages._get(self._msgs), self)
158 nmlib.notmuch_messages_move_to_next(self._msgs)
161 def __nonzero__(self):
163 :return: True if there is at least one more thread in the
164 Iterator, False if not."""
165 return self._msgs is not None and \
166 nmlib.notmuch_messages_valid(self._msgs) > 0
169 """Close and free the notmuch Messages"""
170 if self._msgs is not None:
171 nmlib.notmuch_messages_destroy(self._msgs)
173 def print_messages(self, format, indent=0, entire_thread=False):
174 """Outputs messages as needed for 'notmuch show' to sys.stdout
176 :param format: A string of either 'text' or 'json'.
177 :param indent: A number indicating the reply depth of these messages.
178 :param entire_thread: A bool, indicating whether we want to output
179 whole threads or only the matching messages.
181 if format.lower() == "text":
185 elif format.lower() == "json":
194 sys.stdout.write(set_start)
196 # iterate through all toplevel messages in this thread
201 sys.stdout.write(set_sep)
204 sys.stdout.write(set_start)
205 match = msg.is_match()
208 if (match or entire_thread):
209 if format.lower() == "text":
210 sys.stdout.write(msg.format_message_as_text(indent))
211 elif format.lower() == "json":
212 sys.stdout.write(msg.format_message_as_json(indent))
215 next_indent = indent + 1
217 # get replies and print them also out (if there are any)
218 replies = msg.get_replies()
219 if not replies is None:
220 sys.stdout.write(set_sep)
221 replies.print_messages(format, next_indent, entire_thread)
223 sys.stdout.write(set_end)
224 sys.stdout.write(set_end)
227 class Message(object):
228 """Represents a single Email message
230 Technically, this wraps the underlying *notmuch_message_t*
231 structure. A user will usually not create these objects themselves
232 but get them as search results.
234 As it implements :meth:`__cmp__`, it is possible to compare two
235 :class:`Message`\s using `if msg1 == msg2: ...`.
238 """notmuch_message_get_filename (notmuch_message_t *message)"""
239 _get_filename = nmlib.notmuch_message_get_filename
240 _get_filename.restype = c_char_p
242 """return all filenames for a message"""
243 _get_filenames = nmlib.notmuch_message_get_filenames
244 _get_filenames.restype = c_void_p
246 """notmuch_message_get_flag"""
247 _get_flag = nmlib.notmuch_message_get_flag
248 _get_flag.restype = c_uint
250 """notmuch_message_get_message_id (notmuch_message_t *message)"""
251 _get_message_id = nmlib.notmuch_message_get_message_id
252 _get_message_id.restype = c_char_p
254 """notmuch_message_get_thread_id"""
255 _get_thread_id = nmlib.notmuch_message_get_thread_id
256 _get_thread_id.restype = c_char_p
258 """notmuch_message_get_replies"""
259 _get_replies = nmlib.notmuch_message_get_replies
260 _get_replies.restype = c_void_p
262 """notmuch_message_get_tags (notmuch_message_t *message)"""
263 _get_tags = nmlib.notmuch_message_get_tags
264 _get_tags.restype = c_void_p
266 _get_date = nmlib.notmuch_message_get_date
267 _get_date.restype = c_long
269 _get_header = nmlib.notmuch_message_get_header
270 _get_header.restype = c_char_p
272 """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
273 _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
274 _tags_to_maildir_flags.restype = c_int
276 """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
277 _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
278 _maildir_flags_to_tags.restype = c_int
280 #Constants: Flags that can be set/get with set_flag
281 FLAG = Enum(['MATCH'])
283 def __init__(self, msg_p, parent=None):
285 :param msg_p: A pointer to an internal notmuch_message_t
286 Structure. If it is `None`, we will raise an :exc:`NotmuchError`
289 :param parent: A 'parent' object is passed which this message is
290 derived from. We save a reference to it, so we can
291 automatically delete the parent object once all derived
295 NotmuchError(STATUS.NULL_POINTER)
297 #keep reference to parent, so we keep it alive
298 self._parent = parent
300 def get_message_id(self):
301 """Returns the message ID
303 :returns: String with a message ID
304 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
307 if self._msg is None:
308 raise NotmuchError(STATUS.NOT_INITIALIZED)
309 return Message._get_message_id(self._msg)
311 def get_thread_id(self):
312 """Returns the thread ID
314 The returned string belongs to 'message' will only be valid for as
315 long as the message is valid.
317 This function will not return `None` since Notmuch ensures that every
318 message belongs to a single thread.
320 :returns: String with a thread ID
321 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
324 if self._msg is None:
325 raise NotmuchError(STATUS.NOT_INITIALIZED)
327 return Message._get_thread_id(self._msg)
329 def get_replies(self):
330 """Gets all direct replies to this message as :class:`Messages`
333 .. note:: This call only makes sense if 'message' was
334 ultimately obtained from a :class:`Thread` object, (such as
335 by coming directly from the result of calling
336 :meth:`Thread.get_toplevel_messages` or by any number of
337 subsequent calls to :meth:`get_replies`). If this message was
338 obtained through some non-thread means, (such as by a call
339 to :meth:`Query.search_messages`), then this function will
342 :returns: :class:`Messages` or `None` if there are no replies to
344 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
347 if self._msg is None:
348 raise NotmuchError(STATUS.NOT_INITIALIZED)
350 msgs_p = Message._get_replies(self._msg)
355 return Messages(msgs_p, self)
358 """Returns time_t of the message date
360 For the original textual representation of the Date header from the
361 message call notmuch_message_get_header() with a header value of
364 :returns: A time_t timestamp.
366 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
369 if self._msg is None:
370 raise NotmuchError(STATUS.NOT_INITIALIZED)
371 return Message._get_date(self._msg)
373 def get_header(self, header):
374 """Returns a message header
376 This returns any message header that is stored in the notmuch database.
377 This is only a selected subset of headers, which is currently:
379 TODO: add stored headers
381 :param header: The name of the header to be retrieved.
382 It is not case-sensitive (TODO: confirm).
384 :returns: The header value as string
385 :exception: :exc:`NotmuchError`
387 * STATUS.NOT_INITIALIZED if the message
389 * STATUS.NULL_POINTER, if no header was found
391 if self._msg is None:
392 raise NotmuchError(STATUS.NOT_INITIALIZED)
394 #Returns NULL if any error occurs.
395 header = Message._get_header(self._msg, header)
397 raise NotmuchError(STATUS.NULL_POINTER)
398 return header.decode('UTF-8')
400 def get_filename(self):
401 """Returns the file path of the message file
403 :returns: Absolute file path & name of the message file
404 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
407 if self._msg is None:
408 raise NotmuchError(STATUS.NOT_INITIALIZED)
409 return Message._get_filename(self._msg)
411 def get_filenames(self):
412 """Get all filenames for the email corresponding to 'message'
414 Returns a Filenames() generator with all absolute filepaths for
415 messages recorded to have the same Message-ID. These files must
416 not necessarily have identical content."""
417 if self._msg is None:
418 raise NotmuchError(STATUS.NOT_INITIALIZED)
420 files_p = Message._get_filenames(self._msg)
422 return Filenames(files_p, self).as_generator()
424 def get_flag(self, flag):
425 """Checks whether a specific flag is set for this message
427 The method :meth:`Query.search_threads` sets
428 *Message.FLAG.MATCH* for those messages that match the
429 query. This method allows us to get the value of this flag.
431 :param flag: One of the :attr:`Message.FLAG` values (currently only
433 :returns: An unsigned int (0/1), indicating whether the flag is set.
434 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
437 if self._msg is None:
438 raise NotmuchError(STATUS.NOT_INITIALIZED)
439 return Message._get_flag(self._msg, flag)
441 def set_flag(self, flag, value):
442 """Sets/Unsets a specific flag for this message
444 :param flag: One of the :attr:`Message.FLAG` values (currently only
446 :param value: A bool indicating whether to set or unset the flag.
449 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
452 if self._msg is None:
453 raise NotmuchError(STATUS.NOT_INITIALIZED)
454 nmlib.notmuch_message_set_flag(self._msg, flag, value)
457 """Returns the message tags
459 :returns: A :class:`Tags` iterator.
460 :exception: :exc:`NotmuchError`
462 * STATUS.NOT_INITIALIZED if the message
464 * STATUS.NULL_POINTER, on error
466 if self._msg is None:
467 raise NotmuchError(STATUS.NOT_INITIALIZED)
469 tags_p = Message._get_tags(self._msg)
471 raise NotmuchError(STATUS.NULL_POINTER)
472 return Tags(tags_p, self)
474 def add_tag(self, tag, sync_maildir_flags=False):
475 """Adds a tag to the given message
477 Adds a tag to the current message. The maximal tag length is defined in
478 the notmuch library and is currently 200 bytes.
480 :param tag: String with a 'tag' to be added.
482 :param sync_maildir_flags: If notmuch configuration is set to do
483 this, add maildir flags corresponding to notmuch tags. See
484 underlying method :meth:`tags_to_maildir_flags`. Use False
485 if you want to add/remove many tags on a message without
486 having to physically rename the file every time. Do note,
487 that this will do nothing when a message is frozen, as tag
488 changes will not be committed to the database yet.
490 :returns: STATUS.SUCCESS if the tag was successfully added.
491 Raises an exception otherwise.
492 :exception: :exc:`NotmuchError`. They have the following meaning:
495 The 'tag' argument is NULL
497 The length of 'tag' is too long
498 (exceeds Message.NOTMUCH_TAG_MAX)
499 STATUS.READ_ONLY_DATABASE
500 Database was opened in read-only mode so message cannot be
502 STATUS.NOT_INITIALIZED
503 The message has not been initialized.
505 if self._msg is None:
506 raise NotmuchError(STATUS.NOT_INITIALIZED)
508 status = nmlib.notmuch_message_add_tag(self._msg, _str(tag))
510 # bail out on failure
511 if status != STATUS.SUCCESS:
512 raise NotmuchError(status)
514 if sync_maildir_flags:
515 self.tags_to_maildir_flags()
516 return STATUS.SUCCESS
518 def remove_tag(self, tag, sync_maildir_flags=False):
519 """Removes a tag from the given message
521 If the message has no such tag, this is a non-operation and
522 will report success anyway.
524 :param tag: String with a 'tag' to be removed.
525 :param sync_maildir_flags: If notmuch configuration is set to do
526 this, add maildir flags corresponding to notmuch tags. See
527 underlying method :meth:`tags_to_maildir_flags`. Use False
528 if you want to add/remove many tags on a message without
529 having to physically rename the file every time. Do note,
530 that this will do nothing when a message is frozen, as tag
531 changes will not be committed to the database yet.
533 :returns: STATUS.SUCCESS if the tag was successfully removed or if
534 the message had no such tag.
535 Raises an exception otherwise.
536 :exception: :exc:`NotmuchError`. They have the following meaning:
539 The 'tag' argument is NULL
541 The length of 'tag' is too long
542 (exceeds NOTMUCH_TAG_MAX)
543 STATUS.READ_ONLY_DATABASE
544 Database was opened in read-only mode so message cannot
546 STATUS.NOT_INITIALIZED
547 The message has not been initialized.
549 if self._msg is None:
550 raise NotmuchError(STATUS.NOT_INITIALIZED)
552 status = nmlib.notmuch_message_remove_tag(self._msg, _str(tag))
554 if status != STATUS.SUCCESS:
555 raise NotmuchError(status)
557 if sync_maildir_flags:
558 self.tags_to_maildir_flags()
559 return STATUS.SUCCESS
561 def remove_all_tags(self, sync_maildir_flags=False):
562 """Removes all tags from the given message.
564 See :meth:`freeze` for an example showing how to safely
568 :param sync_maildir_flags: If notmuch configuration is set to do
569 this, add maildir flags corresponding to notmuch tags. See
570 :meth:`tags_to_maildir_flags`. Use False if you want to
571 add/remove many tags on a message without having to
572 physically rename the file every time. Do note, that this
573 will do nothing when a message is frozen, as tag changes
574 will not be committed to the database yet.
576 :returns: STATUS.SUCCESS if the tags were successfully removed.
577 Raises an exception otherwise.
578 :exception: :exc:`NotmuchError`. They have the following meaning:
580 STATUS.READ_ONLY_DATABASE
581 Database was opened in read-only mode so message cannot
583 STATUS.NOT_INITIALIZED
584 The message has not been initialized.
586 if self._msg is None:
587 raise NotmuchError(STATUS.NOT_INITIALIZED)
589 status = nmlib.notmuch_message_remove_all_tags(self._msg)
592 if status != STATUS.SUCCESS:
593 raise NotmuchError(status)
595 if sync_maildir_flags:
596 self.tags_to_maildir_flags()
597 return STATUS.SUCCESS
600 """Freezes the current state of 'message' within the database
602 This means that changes to the message state, (via :meth:`add_tag`,
603 :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
604 committed to the database until the message is :meth:`thaw`ed.
606 Multiple calls to freeze/thaw are valid and these calls will
607 "stack". That is there must be as many calls to thaw as to freeze
608 before a message is actually thawed.
610 The ability to do freeze/thaw allows for safe transactions to
611 change tag values. For example, explicitly setting a message to
612 have a given set of tags might look like this::
615 msg.remove_all_tags(False)
617 msg.add_tag(tag, False)
619 msg.tags_to_maildir_flags()
621 With freeze/thaw used like this, the message in the database is
622 guaranteed to have either the full set of original tag values, or
623 the full set of new tag values, but nothing in between.
625 Imagine the example above without freeze/thaw and the operation
626 somehow getting interrupted. This could result in the message being
627 left with no tags if the interruption happened after
628 :meth:`remove_all_tags` but before :meth:`add_tag`.
630 :returns: STATUS.SUCCESS if the message was successfully frozen.
631 Raises an exception otherwise.
632 :exception: :exc:`NotmuchError`. They have the following meaning:
634 STATUS.READ_ONLY_DATABASE
635 Database was opened in read-only mode so message cannot
637 STATUS.NOT_INITIALIZED
638 The message has not been initialized.
640 if self._msg is None:
641 raise NotmuchError(STATUS.NOT_INITIALIZED)
643 status = nmlib.notmuch_message_freeze(self._msg)
645 if STATUS.SUCCESS == status:
649 raise NotmuchError(status)
652 """Thaws the current 'message'
654 Thaw the current 'message', synchronizing any changes that may have
655 occurred while 'message' was frozen into the notmuch database.
657 See :meth:`freeze` for an example of how to use this
658 function to safely provide tag changes.
660 Multiple calls to freeze/thaw are valid and these calls with
661 "stack". That is there must be as many calls to thaw as to freeze
662 before a message is actually thawed.
664 :returns: STATUS.SUCCESS if the message was successfully frozen.
665 Raises an exception otherwise.
666 :exception: :exc:`NotmuchError`. They have the following meaning:
668 STATUS.UNBALANCED_FREEZE_THAW
669 An attempt was made to thaw an unfrozen message.
670 That is, there have been an unbalanced number of calls
671 to :meth:`freeze` and :meth:`thaw`.
672 STATUS.NOT_INITIALIZED
673 The message has not been initialized.
675 if self._msg is None:
676 raise NotmuchError(STATUS.NOT_INITIALIZED)
678 status = nmlib.notmuch_message_thaw(self._msg)
680 if STATUS.SUCCESS == status:
684 raise NotmuchError(status)
687 """(Not implemented)"""
688 return self.get_flag(Message.FLAG.MATCH)
690 def tags_to_maildir_flags(self):
691 """Synchronize notmuch tags to file Maildir flags
693 'D' if the message has the "draft" tag
694 'F' if the message has the "flagged" tag
695 'P' if the message has the "passed" tag
696 'R' if the message has the "replied" tag
697 'S' if the message does not have the "unread" tag
699 Any existing flags unmentioned in the list above will be
700 preserved in the renaming.
702 Also, if this filename is in a directory named "new", rename it
703 to be within the neighboring directory named "cur".
705 Do note that calling this method while a message is frozen might
706 not work yet, as the modified tags have not been committed yet
709 :returns: a :class:`STATUS`. In short, you want to see
710 notmuch.STATUS.SUCCESS here. See there for details."""
711 if self._msg is None:
712 raise NotmuchError(STATUS.NOT_INITIALIZED)
713 status = Message._tags_to_maildir_flags(self._msg)
715 def maildir_flags_to_tags(self):
716 """Synchronize file Maildir flags to notmuch tags
718 Flag Action if present
719 ---- -----------------
720 'D' Adds the "draft" tag to the message
721 'F' Adds the "flagged" tag to the message
722 'P' Adds the "passed" tag to the message
723 'R' Adds the "replied" tag to the message
724 'S' Removes the "unread" tag from the message
726 For each flag that is not present, the opposite action
727 (add/remove) is performed for the corresponding tags. If there
728 are multiple filenames associated with this message, the flag is
729 considered present if it appears in one or more filenames. (That
730 is, the flags from the multiple filenames are combined with the
731 logical OR operator.)
733 As a convenience, you can set the sync_maildir_flags parameter in
734 :meth:`Database.add_message` to implicitly call this.
736 :returns: a :class:`STATUS`. In short, you want to see
737 notmuch.STATUS.SUCCESS here. See there for details."""
738 if self._msg is None:
739 raise NotmuchError(STATUS.NOT_INITIALIZED)
740 status = Message._tags_to_maildir_flags(self._msg)
743 """Represent a Message() object by str()"""
744 return self.__str__()
747 """A message() is represented by a 1-line summary"""
749 msg['from'] = self.get_header('from')
750 msg['tags'] = self.get_tags()
751 msg['date'] = date.fromtimestamp(self.get_date())
752 return "%(from)s (%(date)s) (%(tags)s)" % (msg)
754 def get_message_parts(self):
755 """Output like notmuch show"""
756 fp = open(self.get_filename())
757 email_msg = email.message_from_file(fp)
761 for msg in email_msg.walk():
762 if not msg.is_multipart():
766 def get_part(self, num):
767 """Returns the nth message body part"""
768 parts = self.get_message_parts()
769 if (num <= 0 or num > len(parts)):
772 out_part = parts[(num - 1)]
773 return out_part.get_payload(decode=True)
775 def format_message_internal(self):
776 """Create an internal representation of the message parts,
777 which can easily be output to json, text, or another output
778 format. The argument match tells whether this matched a
781 output["id"] = self.get_message_id()
782 output["match"] = self.is_match()
783 output["filename"] = self.get_filename()
784 output["tags"] = list(self.get_tags())
787 for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
788 headers[h] = self.get_header(h)
789 output["headers"] = headers
792 parts = self.get_message_parts()
793 for i in xrange(len(parts)):
796 part_dict["id"] = i + 1
797 # We'll be using this is a lot, so let's just get it once.
798 cont_type = msg.get_content_type()
799 part_dict["content-type"] = cont_type
801 # Now we emulate the current behaviour, where it ignores
802 # the html if there's a text representation.
804 # This is being worked on, but it will be easier to fix
805 # here in the future than to end up with another
806 # incompatible solution.
807 disposition = msg["Content-Disposition"]
808 if disposition and disposition.lower().startswith("attachment"):
809 part_dict["filename"] = msg.get_filename()
811 if cont_type.lower() == "text/plain":
812 part_dict["content"] = msg.get_payload()
813 elif (cont_type.lower() == "text/html" and
815 part_dict["content"] = msg.get_payload()
816 body.append(part_dict)
818 output["body"] = body
822 def format_message_as_json(self, indent=0):
823 """Outputs the message as json. This is essentially the same
824 as python's dict format, but we run it through, just so we
825 don't have to worry about the details."""
826 return json.dumps(self.format_message_internal())
828 def format_message_as_text(self, indent=0):
829 """Outputs it in the old-fashioned notmuch text form. Will be
830 easy to change to a new format when the format changes."""
832 format = self.format_message_internal()
833 output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
834 % (format['id'], indent, format['match'], format['filename'])
835 output += "\n\fheader{"
837 #Todo: this date is supposed to be prettified, as in the index.
838 output += "\n%s (%s) (" % (format["headers"]["From"],
839 format["headers"]["Date"])
840 output += ", ".join(format["tags"])
843 output += "\nSubject: %s" % format["headers"]["Subject"]
844 output += "\nFrom: %s" % format["headers"]["From"]
845 output += "\nTo: %s" % format["headers"]["To"]
846 if format["headers"]["Cc"]:
847 output += "\nCc: %s" % format["headers"]["Cc"]
848 if format["headers"]["Bcc"]:
849 output += "\nBcc: %s" % format["headers"]["Bcc"]
850 output += "\nDate: %s" % format["headers"]["Date"]
851 output += "\n\fheader}"
853 output += "\n\fbody{"
855 parts = format["body"]
856 parts.sort(key=lambda x: x['id'])
858 if not "filename" in p:
859 output += "\n\fpart{ "
860 output += "ID: %d, Content-type: %s\n" % (p["id"],
863 output += "\n%s\n" % p["content"]
865 output += "Non-text part: %s\n" % p["content-type"]
866 output += "\n\fpart}"
868 output += "\n\fattachment{ "
869 output += "ID: %d, Content-type:%s\n" % (p["id"],
871 output += "Attachment: %s\n" % p["filename"]
872 output += "\n\fattachment}\n"
874 output += "\n\fbody}\n"
875 output += "\n\fmessage}"
880 """Implement hash(), so we can use Message() sets"""
881 file = self.get_filename()
886 def __cmp__(self, other):
887 """Implement cmp(), so we can compare Message()s
889 2 messages are considered equal if they point to the same
890 Message-Id and if they point to the same file names. If 2
891 Messages derive from different queries where some files have
892 been added or removed, the same messages would not be considered
893 equal (as they do not point to the same set of files
895 res = cmp(self.get_message_id(), other.get_message_id())
897 res = cmp(list(self.get_filenames()), list(other.get_filenames()))
901 """Close and free the notmuch Message"""
902 if self._msg is not None:
903 nmlib.notmuch_message_destroy(self._msg)