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 raise 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":
190 raise TypeError("format must be either 'text' or '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))
212 sys.stdout.write(msg.format_message_as_json(indent))
213 next_indent = indent + 1
215 # get replies and print them also out (if there are any)
216 replies = msg.get_replies()
217 if not replies is None:
218 sys.stdout.write(set_sep)
219 replies.print_messages(format, next_indent, entire_thread)
221 sys.stdout.write(set_end)
222 sys.stdout.write(set_end)
225 class Message(object):
226 """Represents a single Email message
228 Technically, this wraps the underlying *notmuch_message_t*
229 structure. A user will usually not create these objects themselves
230 but get them as search results.
232 As it implements :meth:`__cmp__`, it is possible to compare two
233 :class:`Message`\s using `if msg1 == msg2: ...`.
236 """notmuch_message_get_filename (notmuch_message_t *message)"""
237 _get_filename = nmlib.notmuch_message_get_filename
238 _get_filename.restype = c_char_p
240 """return all filenames for a message"""
241 _get_filenames = nmlib.notmuch_message_get_filenames
242 _get_filenames.restype = c_void_p
244 """notmuch_message_get_flag"""
245 _get_flag = nmlib.notmuch_message_get_flag
246 _get_flag.restype = c_uint
248 """notmuch_message_get_message_id (notmuch_message_t *message)"""
249 _get_message_id = nmlib.notmuch_message_get_message_id
250 _get_message_id.restype = c_char_p
252 """notmuch_message_get_thread_id"""
253 _get_thread_id = nmlib.notmuch_message_get_thread_id
254 _get_thread_id.restype = c_char_p
256 """notmuch_message_get_replies"""
257 _get_replies = nmlib.notmuch_message_get_replies
258 _get_replies.restype = c_void_p
260 """notmuch_message_get_tags (notmuch_message_t *message)"""
261 _get_tags = nmlib.notmuch_message_get_tags
262 _get_tags.restype = c_void_p
264 _get_date = nmlib.notmuch_message_get_date
265 _get_date.restype = c_long
267 _get_header = nmlib.notmuch_message_get_header
268 _get_header.restype = c_char_p
270 """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
271 _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
272 _tags_to_maildir_flags.restype = c_int
274 """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
275 _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
276 _maildir_flags_to_tags.restype = c_int
278 #Constants: Flags that can be set/get with set_flag
279 FLAG = Enum(['MATCH'])
281 def __init__(self, msg_p, parent=None):
283 :param msg_p: A pointer to an internal notmuch_message_t
284 Structure. If it is `None`, we will raise an :exc:`NotmuchError`
287 :param parent: A 'parent' object is passed which this message is
288 derived from. We save a reference to it, so we can
289 automatically delete the parent object once all derived
293 raise NotmuchError(STATUS.NULL_POINTER)
295 #keep reference to parent, so we keep it alive
296 self._parent = parent
298 def get_message_id(self):
299 """Returns the message ID
301 :returns: String with a message ID
302 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
305 if self._msg is None:
306 raise NotmuchError(STATUS.NOT_INITIALIZED)
307 return Message._get_message_id(self._msg)
309 def get_thread_id(self):
310 """Returns the thread ID
312 The returned string belongs to 'message' will only be valid for as
313 long as the message is valid.
315 This function will not return `None` since Notmuch ensures that every
316 message belongs to a single thread.
318 :returns: String with a thread ID
319 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
322 if self._msg is None:
323 raise NotmuchError(STATUS.NOT_INITIALIZED)
325 return Message._get_thread_id(self._msg)
327 def get_replies(self):
328 """Gets all direct replies to this message as :class:`Messages`
331 .. note:: This call only makes sense if 'message' was
332 ultimately obtained from a :class:`Thread` object, (such as
333 by coming directly from the result of calling
334 :meth:`Thread.get_toplevel_messages` or by any number of
335 subsequent calls to :meth:`get_replies`). If this message was
336 obtained through some non-thread means, (such as by a call
337 to :meth:`Query.search_messages`), then this function will
340 :returns: :class:`Messages` or `None` if there are no replies to
342 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
345 if self._msg is None:
346 raise NotmuchError(STATUS.NOT_INITIALIZED)
348 msgs_p = Message._get_replies(self._msg)
353 return Messages(msgs_p, self)
356 """Returns time_t of the message date
358 For the original textual representation of the Date header from the
359 message call notmuch_message_get_header() with a header value of
362 :returns: A time_t timestamp.
364 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
367 if self._msg is None:
368 raise NotmuchError(STATUS.NOT_INITIALIZED)
369 return Message._get_date(self._msg)
371 def get_header(self, header):
372 """Returns a message header
374 This returns any message header that is stored in the notmuch database.
375 This is only a selected subset of headers, which is currently:
377 TODO: add stored headers
379 :param header: The name of the header to be retrieved.
380 It is not case-sensitive (TODO: confirm).
382 :returns: The header value as string
383 :exception: :exc:`NotmuchError`
385 * STATUS.NOT_INITIALIZED if the message
387 * STATUS.NULL_POINTER, if no header was found
389 if self._msg is None:
390 raise NotmuchError(STATUS.NOT_INITIALIZED)
392 #Returns NULL if any error occurs.
393 header = Message._get_header(self._msg, header)
395 raise NotmuchError(STATUS.NULL_POINTER)
396 return header.decode('UTF-8')
398 def get_filename(self):
399 """Returns the file path of the message file
401 :returns: Absolute file path & name of the message file
402 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
405 if self._msg is None:
406 raise NotmuchError(STATUS.NOT_INITIALIZED)
407 return Message._get_filename(self._msg)
409 def get_filenames(self):
410 """Get all filenames for the email corresponding to 'message'
412 Returns a Filenames() generator with all absolute filepaths for
413 messages recorded to have the same Message-ID. These files must
414 not necessarily have identical content."""
415 if self._msg is None:
416 raise NotmuchError(STATUS.NOT_INITIALIZED)
418 files_p = Message._get_filenames(self._msg)
420 return Filenames(files_p, self).as_generator()
422 def get_flag(self, flag):
423 """Checks whether a specific flag is set for this message
425 The method :meth:`Query.search_threads` sets
426 *Message.FLAG.MATCH* for those messages that match the
427 query. This method allows us to get the value of this flag.
429 :param flag: One of the :attr:`Message.FLAG` values (currently only
431 :returns: An unsigned int (0/1), indicating whether the flag is set.
432 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
435 if self._msg is None:
436 raise NotmuchError(STATUS.NOT_INITIALIZED)
437 return Message._get_flag(self._msg, flag)
439 def set_flag(self, flag, value):
440 """Sets/Unsets a specific flag for this message
442 :param flag: One of the :attr:`Message.FLAG` values (currently only
444 :param value: A bool indicating whether to set or unset the flag.
447 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
450 if self._msg is None:
451 raise NotmuchError(STATUS.NOT_INITIALIZED)
452 nmlib.notmuch_message_set_flag(self._msg, flag, value)
455 """Returns the message tags
457 :returns: A :class:`Tags` iterator.
458 :exception: :exc:`NotmuchError`
460 * STATUS.NOT_INITIALIZED if the message
462 * STATUS.NULL_POINTER, on error
464 if self._msg is None:
465 raise NotmuchError(STATUS.NOT_INITIALIZED)
467 tags_p = Message._get_tags(self._msg)
469 raise NotmuchError(STATUS.NULL_POINTER)
470 return Tags(tags_p, self)
472 def add_tag(self, tag, sync_maildir_flags=False):
473 """Adds a tag to the given message
475 Adds a tag to the current message. The maximal tag length is defined in
476 the notmuch library and is currently 200 bytes.
478 :param tag: String with a 'tag' to be added.
480 :param sync_maildir_flags: If notmuch configuration is set to do
481 this, add maildir flags corresponding to notmuch tags. See
482 underlying method :meth:`tags_to_maildir_flags`. Use False
483 if you want to add/remove many tags on a message without
484 having to physically rename the file every time. Do note,
485 that this will do nothing when a message is frozen, as tag
486 changes will not be committed to the database yet.
488 :returns: STATUS.SUCCESS if the tag was successfully added.
489 Raises an exception otherwise.
490 :exception: :exc:`NotmuchError`. They have the following meaning:
493 The 'tag' argument is NULL
495 The length of 'tag' is too long
496 (exceeds Message.NOTMUCH_TAG_MAX)
497 STATUS.READ_ONLY_DATABASE
498 Database was opened in read-only mode so message cannot be
500 STATUS.NOT_INITIALIZED
501 The message has not been initialized.
503 if self._msg is None:
504 raise NotmuchError(STATUS.NOT_INITIALIZED)
506 status = nmlib.notmuch_message_add_tag(self._msg, _str(tag))
508 # bail out on failure
509 if status != STATUS.SUCCESS:
510 raise NotmuchError(status)
512 if sync_maildir_flags:
513 self.tags_to_maildir_flags()
514 return STATUS.SUCCESS
516 def remove_tag(self, tag, sync_maildir_flags=False):
517 """Removes a tag from the given message
519 If the message has no such tag, this is a non-operation and
520 will report success anyway.
522 :param tag: String with a 'tag' to be removed.
523 :param sync_maildir_flags: If notmuch configuration is set to do
524 this, add maildir flags corresponding to notmuch tags. See
525 underlying method :meth:`tags_to_maildir_flags`. Use False
526 if you want to add/remove many tags on a message without
527 having to physically rename the file every time. Do note,
528 that this will do nothing when a message is frozen, as tag
529 changes will not be committed to the database yet.
531 :returns: STATUS.SUCCESS if the tag was successfully removed or if
532 the message had no such tag.
533 Raises an exception otherwise.
534 :exception: :exc:`NotmuchError`. They have the following meaning:
537 The 'tag' argument is NULL
539 The length of 'tag' is too long
540 (exceeds NOTMUCH_TAG_MAX)
541 STATUS.READ_ONLY_DATABASE
542 Database was opened in read-only mode so message cannot
544 STATUS.NOT_INITIALIZED
545 The message has not been initialized.
547 if self._msg is None:
548 raise NotmuchError(STATUS.NOT_INITIALIZED)
550 status = nmlib.notmuch_message_remove_tag(self._msg, _str(tag))
552 if status != STATUS.SUCCESS:
553 raise NotmuchError(status)
555 if sync_maildir_flags:
556 self.tags_to_maildir_flags()
557 return STATUS.SUCCESS
559 def remove_all_tags(self, sync_maildir_flags=False):
560 """Removes all tags from the given message.
562 See :meth:`freeze` for an example showing how to safely
566 :param sync_maildir_flags: If notmuch configuration is set to do
567 this, add maildir flags corresponding to notmuch tags. See
568 :meth:`tags_to_maildir_flags`. Use False if you want to
569 add/remove many tags on a message without having to
570 physically rename the file every time. Do note, that this
571 will do nothing when a message is frozen, as tag changes
572 will not be committed to the database yet.
574 :returns: STATUS.SUCCESS if the tags were successfully removed.
575 Raises an exception otherwise.
576 :exception: :exc:`NotmuchError`. They have the following meaning:
578 STATUS.READ_ONLY_DATABASE
579 Database was opened in read-only mode so message cannot
581 STATUS.NOT_INITIALIZED
582 The message has not been initialized.
584 if self._msg is None:
585 raise NotmuchError(STATUS.NOT_INITIALIZED)
587 status = nmlib.notmuch_message_remove_all_tags(self._msg)
590 if status != STATUS.SUCCESS:
591 raise NotmuchError(status)
593 if sync_maildir_flags:
594 self.tags_to_maildir_flags()
595 return STATUS.SUCCESS
598 """Freezes the current state of 'message' within the database
600 This means that changes to the message state, (via :meth:`add_tag`,
601 :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
602 committed to the database until the message is :meth:`thaw`ed.
604 Multiple calls to freeze/thaw are valid and these calls will
605 "stack". That is there must be as many calls to thaw as to freeze
606 before a message is actually thawed.
608 The ability to do freeze/thaw allows for safe transactions to
609 change tag values. For example, explicitly setting a message to
610 have a given set of tags might look like this::
613 msg.remove_all_tags(False)
615 msg.add_tag(tag, False)
617 msg.tags_to_maildir_flags()
619 With freeze/thaw used like this, the message in the database is
620 guaranteed to have either the full set of original tag values, or
621 the full set of new tag values, but nothing in between.
623 Imagine the example above without freeze/thaw and the operation
624 somehow getting interrupted. This could result in the message being
625 left with no tags if the interruption happened after
626 :meth:`remove_all_tags` but before :meth:`add_tag`.
628 :returns: STATUS.SUCCESS if the message was successfully frozen.
629 Raises an exception otherwise.
630 :exception: :exc:`NotmuchError`. They have the following meaning:
632 STATUS.READ_ONLY_DATABASE
633 Database was opened in read-only mode so message cannot
635 STATUS.NOT_INITIALIZED
636 The message has not been initialized.
638 if self._msg is None:
639 raise NotmuchError(STATUS.NOT_INITIALIZED)
641 status = nmlib.notmuch_message_freeze(self._msg)
643 if STATUS.SUCCESS == status:
647 raise NotmuchError(status)
650 """Thaws the current 'message'
652 Thaw the current 'message', synchronizing any changes that may have
653 occurred while 'message' was frozen into the notmuch database.
655 See :meth:`freeze` for an example of how to use this
656 function to safely provide tag changes.
658 Multiple calls to freeze/thaw are valid and these calls with
659 "stack". That is there must be as many calls to thaw as to freeze
660 before a message is actually thawed.
662 :returns: STATUS.SUCCESS if the message was successfully frozen.
663 Raises an exception otherwise.
664 :exception: :exc:`NotmuchError`. They have the following meaning:
666 STATUS.UNBALANCED_FREEZE_THAW
667 An attempt was made to thaw an unfrozen message.
668 That is, there have been an unbalanced number of calls
669 to :meth:`freeze` and :meth:`thaw`.
670 STATUS.NOT_INITIALIZED
671 The message has not been initialized.
673 if self._msg is None:
674 raise NotmuchError(STATUS.NOT_INITIALIZED)
676 status = nmlib.notmuch_message_thaw(self._msg)
678 if STATUS.SUCCESS == status:
682 raise NotmuchError(status)
685 """(Not implemented)"""
686 return self.get_flag(Message.FLAG.MATCH)
688 def tags_to_maildir_flags(self):
689 """Synchronize notmuch tags to file Maildir flags
691 'D' if the message has the "draft" tag
692 'F' if the message has the "flagged" tag
693 'P' if the message has the "passed" tag
694 'R' if the message has the "replied" tag
695 'S' if the message does not have the "unread" tag
697 Any existing flags unmentioned in the list above will be
698 preserved in the renaming.
700 Also, if this filename is in a directory named "new", rename it
701 to be within the neighboring directory named "cur".
703 Do note that calling this method while a message is frozen might
704 not work yet, as the modified tags have not been committed yet
707 :returns: a :class:`STATUS`. In short, you want to see
708 notmuch.STATUS.SUCCESS here. See there for details."""
709 if self._msg is None:
710 raise NotmuchError(STATUS.NOT_INITIALIZED)
711 status = Message._tags_to_maildir_flags(self._msg)
713 def maildir_flags_to_tags(self):
714 """Synchronize file Maildir flags to notmuch tags
716 Flag Action if present
717 ---- -----------------
718 'D' Adds the "draft" tag to the message
719 'F' Adds the "flagged" tag to the message
720 'P' Adds the "passed" tag to the message
721 'R' Adds the "replied" tag to the message
722 'S' Removes the "unread" tag from the message
724 For each flag that is not present, the opposite action
725 (add/remove) is performed for the corresponding tags. If there
726 are multiple filenames associated with this message, the flag is
727 considered present if it appears in one or more filenames. (That
728 is, the flags from the multiple filenames are combined with the
729 logical OR operator.)
731 As a convenience, you can set the sync_maildir_flags parameter in
732 :meth:`Database.add_message` to implicitly call this.
734 :returns: a :class:`STATUS`. In short, you want to see
735 notmuch.STATUS.SUCCESS here. See there for details."""
736 if self._msg is None:
737 raise NotmuchError(STATUS.NOT_INITIALIZED)
738 status = Message._tags_to_maildir_flags(self._msg)
741 """Represent a Message() object by str()"""
742 return self.__str__()
745 """A message() is represented by a 1-line summary"""
747 msg['from'] = self.get_header('from')
748 msg['tags'] = self.get_tags()
749 msg['date'] = date.fromtimestamp(self.get_date())
750 return "%(from)s (%(date)s) (%(tags)s)" % (msg)
752 def get_message_parts(self):
753 """Output like notmuch show"""
754 fp = open(self.get_filename())
755 email_msg = email.message_from_file(fp)
759 for msg in email_msg.walk():
760 if not msg.is_multipart():
764 def get_part(self, num):
765 """Returns the nth message body part"""
766 parts = self.get_message_parts()
767 if (num <= 0 or num > len(parts)):
770 out_part = parts[(num - 1)]
771 return out_part.get_payload(decode=True)
773 def format_message_internal(self):
774 """Create an internal representation of the message parts,
775 which can easily be output to json, text, or another output
776 format. The argument match tells whether this matched a
779 output["id"] = self.get_message_id()
780 output["match"] = self.is_match()
781 output["filename"] = self.get_filename()
782 output["tags"] = list(self.get_tags())
785 for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
786 headers[h] = self.get_header(h)
787 output["headers"] = headers
790 parts = self.get_message_parts()
791 for i in xrange(len(parts)):
794 part_dict["id"] = i + 1
795 # We'll be using this is a lot, so let's just get it once.
796 cont_type = msg.get_content_type()
797 part_dict["content-type"] = cont_type
799 # Now we emulate the current behaviour, where it ignores
800 # the html if there's a text representation.
802 # This is being worked on, but it will be easier to fix
803 # here in the future than to end up with another
804 # incompatible solution.
805 disposition = msg["Content-Disposition"]
806 if disposition and disposition.lower().startswith("attachment"):
807 part_dict["filename"] = msg.get_filename()
809 if cont_type.lower() == "text/plain":
810 part_dict["content"] = msg.get_payload()
811 elif (cont_type.lower() == "text/html" and
813 part_dict["content"] = msg.get_payload()
814 body.append(part_dict)
816 output["body"] = body
820 def format_message_as_json(self, indent=0):
821 """Outputs the message as json. This is essentially the same
822 as python's dict format, but we run it through, just so we
823 don't have to worry about the details."""
824 return json.dumps(self.format_message_internal())
826 def format_message_as_text(self, indent=0):
827 """Outputs it in the old-fashioned notmuch text form. Will be
828 easy to change to a new format when the format changes."""
830 format = self.format_message_internal()
831 output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
832 % (format['id'], indent, format['match'], format['filename'])
833 output += "\n\fheader{"
835 #Todo: this date is supposed to be prettified, as in the index.
836 output += "\n%s (%s) (" % (format["headers"]["From"],
837 format["headers"]["Date"])
838 output += ", ".join(format["tags"])
841 output += "\nSubject: %s" % format["headers"]["Subject"]
842 output += "\nFrom: %s" % format["headers"]["From"]
843 output += "\nTo: %s" % format["headers"]["To"]
844 if format["headers"]["Cc"]:
845 output += "\nCc: %s" % format["headers"]["Cc"]
846 if format["headers"]["Bcc"]:
847 output += "\nBcc: %s" % format["headers"]["Bcc"]
848 output += "\nDate: %s" % format["headers"]["Date"]
849 output += "\n\fheader}"
851 output += "\n\fbody{"
853 parts = format["body"]
854 parts.sort(key=lambda x: x['id'])
856 if not "filename" in p:
857 output += "\n\fpart{ "
858 output += "ID: %d, Content-type: %s\n" % (p["id"],
861 output += "\n%s\n" % p["content"]
863 output += "Non-text part: %s\n" % p["content-type"]
864 output += "\n\fpart}"
866 output += "\n\fattachment{ "
867 output += "ID: %d, Content-type:%s\n" % (p["id"],
869 output += "Attachment: %s\n" % p["filename"]
870 output += "\n\fattachment}\n"
872 output += "\n\fbody}\n"
873 output += "\n\fmessage}"
878 """Implement hash(), so we can use Message() sets"""
879 file = self.get_filename()
884 def __cmp__(self, other):
885 """Implement cmp(), so we can compare Message()s
887 2 messages are considered equal if they point to the same
888 Message-Id and if they point to the same file names. If 2
889 Messages derive from different queries where some files have
890 been added or removed, the same messages would not be considered
891 equal (as they do not point to the same set of files
893 res = cmp(self.get_message_id(), other.get_message_id())
895 res = cmp(list(self.get_filenames()), list(other.get_filenames()))
899 """Close and free the notmuch Message"""
900 if self._msg is not None:
901 nmlib.notmuch_message_destroy(self._msg)