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
25 from notmuch.tag import Tags
26 from notmuch.filename import Filenames
31 import simplejson as json
34 #------------------------------------------------------------------------------
35 class Messages(object):
36 """Represents a list of notmuch messages
38 This object provides an iterator over a list of notmuch messages
39 (Technically, it provides a wrapper for the underlying
40 *notmuch_messages_t* structure). Do note that the underlying library
41 only provides a one-time iterator (it cannot reset the iterator to
42 the start). Thus iterating over the function will "exhaust" the list
43 of messages, and a subsequent iteration attempt will raise a
44 :exc:`NotmuchError` STATUS.NOT_INITIALIZED. If you need to
45 re-iterate over a list of messages you will need to retrieve a new
46 :class:`Messages` object or cache your :class:`Message`\s in a list
51 You can store and reuse the single :class:`Message` objects as often
52 as you want as long as you keep the parent :class:`Messages` object
53 around. (Due to hierarchical memory allocation, all derived
54 :class:`Message` objects will be invalid when we delete the parent
55 :class:`Messages` object, even if it was already exhausted.) So
59 msgs = Query(db,'').search_messages() #get a Messages() object
62 # msgs is "exhausted" now and msgs.next() will raise an exception.
63 # However it will be kept alive until all retrieved Message()
64 # objects are also deleted. If you do e.g. an explicit del(msgs)
65 # here, the following lines would fail.
67 # You can reiterate over *msglist* however as often as you want.
68 # It is simply a list with :class:`Message`s.
70 print (msglist[0].get_filename())
71 print (msglist[1].get_filename())
72 print (msglist[0].get_message_id())
75 As :class:`Message` implements both __hash__() and __cmp__(), it is
76 possible to make sets out of :class:`Messages` and use set
77 arithmetic (this happens in python and will of course be *much*
78 slower than redoing a proper query with the appropriate filters::
80 s1, s2 = set(msgs1), set(msgs2)
85 Be careful when using set arithmetic between message sets derived
86 from different Databases (ie the same database reopened after
87 messages have changed). If messages have added or removed associated
88 files in the meantime, it is possible that the same message would be
89 considered as a different object (as it points to a different file).
93 _get = nmlib.notmuch_messages_get
94 _get.restype = c_void_p
96 _collect_tags = nmlib.notmuch_messages_collect_tags
97 _collect_tags.restype = c_void_p
99 def __init__(self, msgs_p, parent=None):
101 :param msgs_p: A pointer to an underlying *notmuch_messages_t*
102 structure. These are not publically exposed, so a user
103 will almost never instantiate a :class:`Messages` object
104 herself. They are usually handed back as a result,
105 e.g. in :meth:`Query.search_messages`. *msgs_p* must be
106 valid, we will raise an :exc:`NotmuchError`
107 (STATUS.NULL_POINTER) if it is `None`.
108 :type msgs_p: :class:`ctypes.c_void_p`
109 :param parent: The parent object
110 (ie :class:`Query`) these tags are derived from. It saves
111 a reference to it, so we can automatically delete the db
112 object once all derived objects are dead.
113 :TODO: Make the iterator work more than once and cache the tags in
114 the Python object.(?)
117 NotmuchError(STATUS.NULL_POINTER)
120 #store parent, so we keep them alive as long as self is alive
121 self._parent = parent
123 def collect_tags(self):
124 """Return the unique :class:`Tags` in the contained messages
126 :returns: :class:`Tags`
127 :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not inited
129 .. note:: :meth:`collect_tags` will iterate over the messages and
130 therefore will not allow further iterations.
132 if self._msgs is None:
133 raise NotmuchError(STATUS.NOT_INITIALIZED)
135 # collect all tags (returns NULL on error)
136 tags_p = Messages._collect_tags (self._msgs)
137 #reset _msgs as we iterated over it and can do so only once
141 raise NotmuchError(STATUS.NULL_POINTER)
142 return Tags(tags_p, self)
145 """ Make Messages an iterator """
149 if self._msgs is None:
150 raise NotmuchError(STATUS.NOT_INITIALIZED)
152 if not nmlib.notmuch_messages_valid(self._msgs):
156 msg = Message(Messages._get (self._msgs), self)
157 nmlib.notmuch_messages_move_to_next(self._msgs)
160 def __nonzero__(self):
162 :return: True if there is at least one more thread in the
163 Iterator, False if not."""
164 return self._msgs is not None and \
165 nmlib.notmuch_messages_valid(self._msgs) > 0
168 """Close and free the notmuch Messages"""
169 if self._msgs is not None:
170 nmlib.notmuch_messages_destroy (self._msgs)
172 def print_messages(self, format, indent=0, entire_thread=False):
173 """Outputs messages as needed for 'notmuch show' to sys.stdout
175 :param format: A string of either 'text' or 'json'.
176 :param indent: A number indicating the reply depth of these messages.
177 :param entire_thread: A bool, indicating whether we want to output
178 whole threads or only the matching messages.
180 if format.lower() == "text":
184 elif format.lower() == "json":
193 sys.stdout.write(set_start)
195 # iterate through all toplevel messages in this thread
200 sys.stdout.write(set_sep)
203 sys.stdout.write(set_start)
204 match = msg.is_match()
207 if (match or entire_thread):
208 if format.lower() == "text":
209 sys.stdout.write(msg.format_message_as_text(indent))
210 elif format.lower() == "json":
211 sys.stdout.write(msg.format_message_as_json(indent))
214 next_indent = indent + 1
216 # get replies and print them also out (if there are any)
217 replies = msg.get_replies()
218 if not replies is None:
219 sys.stdout.write(set_sep)
220 replies.print_messages(format, next_indent, entire_thread)
222 sys.stdout.write(set_end)
223 sys.stdout.write(set_end)
225 #------------------------------------------------------------------------------
226 class Message(object):
227 """Represents a single Email message
229 Technically, this wraps the underlying *notmuch_message_t*
230 structure. A user will usually not create these objects themselves
231 but get them as search results.
233 As it implements :meth:`__cmp__`, it is possible to compare two
234 :class:`Message`\s using `if msg1 == msg2: ...`.
237 """notmuch_message_get_filename (notmuch_message_t *message)"""
238 _get_filename = nmlib.notmuch_message_get_filename
239 _get_filename.restype = c_char_p
241 """return all filenames for a message"""
242 _get_filenames = nmlib.notmuch_message_get_filenames
243 _get_filenames.restype = c_void_p
245 """notmuch_message_get_flag"""
246 _get_flag = nmlib.notmuch_message_get_flag
247 _get_flag.restype = c_uint
249 """notmuch_message_get_message_id (notmuch_message_t *message)"""
250 _get_message_id = nmlib.notmuch_message_get_message_id
251 _get_message_id.restype = c_char_p
253 """notmuch_message_get_thread_id"""
254 _get_thread_id = nmlib.notmuch_message_get_thread_id
255 _get_thread_id.restype = c_char_p
257 """notmuch_message_get_replies"""
258 _get_replies = nmlib.notmuch_message_get_replies
259 _get_replies.restype = c_void_p
261 """notmuch_message_get_tags (notmuch_message_t *message)"""
262 _get_tags = nmlib.notmuch_message_get_tags
263 _get_tags.restype = c_void_p
265 _get_date = nmlib.notmuch_message_get_date
266 _get_date.restype = c_long
268 _get_header = nmlib.notmuch_message_get_header
269 _get_header.restype = c_char_p
271 """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)"""
272 _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags
273 _tags_to_maildir_flags.restype = c_int
275 """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)"""
276 _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags
277 _maildir_flags_to_tags.restype = c_int
279 #Constants: Flags that can be set/get with set_flag
280 FLAG = Enum(['MATCH'])
282 def __init__(self, msg_p, parent=None):
284 :param msg_p: A pointer to an internal notmuch_message_t
285 Structure. If it is `None`, we will raise an :exc:`NotmuchError`
288 :param parent: A 'parent' object is passed which this message is
289 derived from. We save a reference to it, so we can
290 automatically delete the parent object once all derived
294 NotmuchError(STATUS.NULL_POINTER)
296 #keep reference to parent, so we keep it alive
297 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` iterator
332 .. note:: This call only makes sense if 'message' was
333 ultimately obtained from a :class:`Thread` object, (such as
334 by coming directly from the result of calling
335 :meth:`Thread.get_toplevel_messages` or by any number of
336 subsequent calls to :meth:`get_replies`). If this message was
337 obtained through some non-thread means, (such as by a call
338 to :meth:`Query.search_messages`), then this function will
341 :returns: :class:`Messages` or `None` if there are no replies to
343 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
346 if self._msg is None:
347 raise NotmuchError(STATUS.NOT_INITIALIZED)
349 msgs_p = Message._get_replies(self._msg);
354 return Messages(msgs_p,self)
357 """Returns time_t of the message date
359 For the original textual representation of the Date header from the
360 message call notmuch_message_get_header() with a header value of
363 :returns: A time_t timestamp.
365 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
368 if self._msg is None:
369 raise NotmuchError(STATUS.NOT_INITIALIZED)
370 return Message._get_date(self._msg)
372 def get_header(self, header):
373 """Returns a message header
375 This returns any message header that is stored in the notmuch database.
376 This is only a selected subset of headers, which is currently:
378 TODO: add stored headers
380 :param header: The name of the header to be retrieved.
381 It is not case-sensitive (TODO: confirm).
383 :returns: The header value as string
384 :exception: :exc:`NotmuchError`
386 * STATUS.NOT_INITIALIZED if the message
388 * STATUS.NULL_POINTER, if no header was found
390 if self._msg is None:
391 raise NotmuchError(STATUS.NOT_INITIALIZED)
393 #Returns NULL if any error occurs.
394 header = Message._get_header (self._msg, header)
396 raise NotmuchError(STATUS.NULL_POINTER)
399 def get_filename(self):
400 """Returns the file path of the message file
402 :returns: Absolute file path & name of the message file
403 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
406 if self._msg is None:
407 raise NotmuchError(STATUS.NOT_INITIALIZED)
408 return Message._get_filename(self._msg)
410 def get_filenames(self):
411 """Get all filenames for the email corresponding to 'message'
413 Returns a Filenames() generator with all absolute filepaths for
414 messages recorded to have the same Message-ID. These files must
415 not necessarily have identical content."""
416 if self._msg is None:
417 raise NotmuchError(STATUS.NOT_INITIALIZED)
419 files_p = Message._get_filenames(self._msg)
421 return Filenames(files_p, self).as_generator()
423 def get_flag(self, flag):
424 """Checks whether a specific flag is set for this message
426 The method :meth:`Query.search_threads` sets
427 *Message.FLAG.MATCH* for those messages that match the
428 query. This method allows us to get the value of this flag.
430 :param flag: One of the :attr:`Message.FLAG` values (currently only
432 :returns: An unsigned int (0/1), indicating whether the flag is set.
433 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
436 if self._msg is None:
437 raise NotmuchError(STATUS.NOT_INITIALIZED)
438 return Message._get_flag(self._msg, flag)
440 def set_flag(self, flag, value):
441 """Sets/Unsets a specific flag for this message
443 :param flag: One of the :attr:`Message.FLAG` values (currently only
445 :param value: A bool indicating whether to set or unset the flag.
448 :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message
451 if self._msg is None:
452 raise NotmuchError(STATUS.NOT_INITIALIZED)
453 nmlib.notmuch_message_set_flag(self._msg, flag, value)
456 """Returns the message tags
458 :returns: A :class:`Tags` iterator.
459 :exception: :exc:`NotmuchError`
461 * STATUS.NOT_INITIALIZED if the message
463 * STATUS.NULL_POINTER, on error
465 if self._msg is None:
466 raise NotmuchError(STATUS.NOT_INITIALIZED)
468 tags_p = Message._get_tags(self._msg)
470 raise NotmuchError(STATUS.NULL_POINTER)
471 return Tags(tags_p, self)
473 def add_tag(self, tag, sync_maildir_flags=False):
474 """Adds a tag to the given message
476 Adds a tag to the current message. The maximal tag length is defined in
477 the notmuch library and is currently 200 bytes.
479 :param tag: String with a 'tag' to be added.
481 :param sync_maildir_flags: If notmuch configuration is set to do
482 this, add maildir flags corresponding to notmuch tags. See
483 underlying method :meth:`tags_to_maildir_flags`. Use False
484 if you want to add/remove many tags on a message without
485 having to physically rename the file every time. Do note,
486 that this will do nothing when a message is frozen, as tag
487 changes will not be committed to the database yet.
489 :returns: STATUS.SUCCESS if the tag was successfully added.
490 Raises an exception otherwise.
491 :exception: :exc:`NotmuchError`. They have the following meaning:
494 The 'tag' argument is NULL
496 The length of 'tag' is too long
497 (exceeds Message.NOTMUCH_TAG_MAX)
498 STATUS.READ_ONLY_DATABASE
499 Database was opened in read-only mode so message cannot be
501 STATUS.NOT_INITIALIZED
502 The message has not been initialized.
504 if self._msg is None:
505 raise NotmuchError(STATUS.NOT_INITIALIZED)
507 status = nmlib.notmuch_message_add_tag (self._msg, tag)
509 # bail out on failure
510 if status != STATUS.SUCCESS:
511 raise NotmuchError(status)
513 if sync_maildir_flags:
514 self.tags_to_maildir_flags()
515 return STATUS.SUCCESS
517 def remove_tag(self, tag, sync_maildir_flags=False):
518 """Removes a tag from the given message
520 If the message has no such tag, this is a non-operation and
521 will report success anyway.
523 :param tag: String with a 'tag' to be removed.
524 :param sync_maildir_flags: If notmuch configuration is set to do
525 this, add maildir flags corresponding to notmuch tags. See
526 underlying method :meth:`tags_to_maildir_flags`. Use False
527 if you want to add/remove many tags on a message without
528 having to physically rename the file every time. Do note,
529 that this will do nothing when a message is frozen, as tag
530 changes will not be committed to the database yet.
532 :returns: STATUS.SUCCESS if the tag was successfully removed or if
533 the message had no such tag.
534 Raises an exception otherwise.
535 :exception: :exc:`NotmuchError`. They have the following meaning:
538 The 'tag' argument is NULL
540 The length of 'tag' is too long
541 (exceeds NOTMUCH_TAG_MAX)
542 STATUS.READ_ONLY_DATABASE
543 Database was opened in read-only mode so message cannot
545 STATUS.NOT_INITIALIZED
546 The message has not been initialized.
548 if self._msg is None:
549 raise NotmuchError(STATUS.NOT_INITIALIZED)
551 status = nmlib.notmuch_message_remove_tag(self._msg, tag)
553 if status != STATUS.SUCCESS:
554 raise NotmuchError(status)
556 if sync_maildir_flags:
557 self.tags_to_maildir_flags()
558 return STATUS.SUCCESS
562 def remove_all_tags(self, sync_maildir_flags=False):
563 """Removes all tags from the given message.
565 See :meth:`freeze` for an example showing how to safely
569 :param sync_maildir_flags: If notmuch configuration is set to do
570 this, add maildir flags corresponding to notmuch tags. See
571 :meth:`tags_to_maildir_flags`. Use False if you want to
572 add/remove many tags on a message without having to
573 physically rename the file every time. Do note, that this
574 will do nothing when a message is frozen, as tag changes
575 will not be committed to the database yet.
577 :returns: STATUS.SUCCESS if the tags were successfully removed.
578 Raises an exception otherwise.
579 :exception: :exc:`NotmuchError`. They have the following meaning:
581 STATUS.READ_ONLY_DATABASE
582 Database was opened in read-only mode so message cannot
584 STATUS.NOT_INITIALIZED
585 The message has not been initialized.
587 if self._msg is None:
588 raise NotmuchError(STATUS.NOT_INITIALIZED)
590 status = nmlib.notmuch_message_remove_all_tags(self._msg)
593 if status != STATUS.SUCCESS:
594 raise NotmuchError(status)
596 if sync_maildir_flags:
597 self.tags_to_maildir_flags()
598 return STATUS.SUCCESS
601 """Freezes the current state of 'message' within the database
603 This means that changes to the message state, (via :meth:`add_tag`,
604 :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be
605 committed to the database until the message is :meth:`thaw`ed.
607 Multiple calls to freeze/thaw are valid and these calls will
608 "stack". That is there must be as many calls to thaw as to freeze
609 before a message is actually thawed.
611 The ability to do freeze/thaw allows for safe transactions to
612 change tag values. For example, explicitly setting a message to
613 have a given set of tags might look like this::
616 msg.remove_all_tags(False)
618 msg.add_tag(tag, False)
620 msg.tags_to_maildir_flags()
622 With freeze/thaw used like this, the message in the database is
623 guaranteed to have either the full set of original tag values, or
624 the full set of new tag values, but nothing in between.
626 Imagine the example above without freeze/thaw and the operation
627 somehow getting interrupted. This could result in the message being
628 left with no tags if the interruption happened after
629 :meth:`remove_all_tags` but before :meth:`add_tag`.
631 :returns: STATUS.SUCCESS if the message was successfully frozen.
632 Raises an exception otherwise.
633 :exception: :exc:`NotmuchError`. They have the following meaning:
635 STATUS.READ_ONLY_DATABASE
636 Database was opened in read-only mode so message cannot
638 STATUS.NOT_INITIALIZED
639 The message has not been initialized.
641 if self._msg is None:
642 raise NotmuchError(STATUS.NOT_INITIALIZED)
644 status = nmlib.notmuch_message_freeze(self._msg)
646 if STATUS.SUCCESS == status:
650 raise NotmuchError(status)
653 """Thaws the current 'message'
655 Thaw the current 'message', synchronizing any changes that may have
656 occurred while 'message' was frozen into the notmuch database.
658 See :meth:`freeze` for an example of how to use this
659 function to safely provide tag changes.
661 Multiple calls to freeze/thaw are valid and these calls with
662 "stack". That is there must be as many calls to thaw as to freeze
663 before a message is actually thawed.
665 :returns: STATUS.SUCCESS if the message was successfully frozen.
666 Raises an exception otherwise.
667 :exception: :exc:`NotmuchError`. They have the following meaning:
669 STATUS.UNBALANCED_FREEZE_THAW
670 An attempt was made to thaw an unfrozen message.
671 That is, there have been an unbalanced number of calls
672 to :meth:`freeze` and :meth:`thaw`.
673 STATUS.NOT_INITIALIZED
674 The message has not been initialized.
676 if self._msg is None:
677 raise NotmuchError(STATUS.NOT_INITIALIZED)
679 status = nmlib.notmuch_message_thaw(self._msg)
681 if STATUS.SUCCESS == status:
685 raise NotmuchError(status)
689 """(Not implemented)"""
690 return self.get_flag(Message.FLAG.MATCH)
692 def tags_to_maildir_flags(self):
693 """Synchronize notmuch tags to file Maildir flags
695 'D' if the message has the "draft" tag
696 'F' if the message has the "flagged" tag
697 'P' if the message has the "passed" tag
698 'R' if the message has the "replied" tag
699 'S' if the message does not have the "unread" tag
701 Any existing flags unmentioned in the list above will be
702 preserved in the renaming.
704 Also, if this filename is in a directory named "new", rename it
705 to be within the neighboring directory named "cur".
707 Do note that calling this method while a message is frozen might
708 not work yet, as the modified tags have not been committed yet
711 :returns: a :class:`STATUS`. In short, you want to see
712 notmuch.STATUS.SUCCESS here. See there for details."""
713 if self._msg is None:
714 raise NotmuchError(STATUS.NOT_INITIALIZED)
715 status = Message._tags_to_maildir_flags(self._msg)
717 def maildir_flags_to_tags(self):
718 """Synchronize file Maildir flags to notmuch tags
720 Flag Action if present
721 ---- -----------------
722 'D' Adds the "draft" tag to the message
723 'F' Adds the "flagged" tag to the message
724 'P' Adds the "passed" tag to the message
725 'R' Adds the "replied" tag to the message
726 'S' Removes the "unread" tag from the message
728 For each flag that is not present, the opposite action
729 (add/remove) is performed for the corresponding tags. If there
730 are multiple filenames associated with this message, the flag is
731 considered present if it appears in one or more filenames. (That
732 is, the flags from the multiple filenames are combined with the
733 logical OR operator.)
735 As a convenience, you can set the sync_maildir_flags parameter in
736 :meth:`Database.add_message` to implicitly call this.
738 :returns: a :class:`STATUS`. In short, you want to see
739 notmuch.STATUS.SUCCESS here. See there for details."""
740 if self._msg is None:
741 raise NotmuchError(STATUS.NOT_INITIALIZED)
742 status = Message._tags_to_maildir_flags(self._msg)
745 """Represent a Message() object by str()"""
746 return self.__str__()
749 """A message() is represented by a 1-line summary"""
751 msg['from'] = self.get_header('from')
752 msg['tags'] = str(self.get_tags())
753 msg['date'] = date.fromtimestamp(self.get_date())
754 return "%(from)s (%(date)s) (%(tags)s)" % (msg)
757 def get_message_parts(self):
758 """Output like notmuch show"""
759 fp = open(self.get_filename())
760 email_msg = email.message_from_file(fp)
764 for msg in email_msg.walk():
765 if not msg.is_multipart():
769 def get_part(self, num):
770 """Returns the nth message body part"""
771 parts = self.get_message_parts()
772 if (num <= 0 or num > len(parts)):
775 out_part = parts[(num - 1)]
776 return out_part.get_payload(decode=True)
778 def format_message_internal(self):
779 """Create an internal representation of the message parts,
780 which can easily be output to json, text, or another output
781 format. The argument match tells whether this matched a
784 output["id"] = self.get_message_id()
785 output["match"] = self.is_match()
786 output["filename"] = self.get_filename()
787 output["tags"] = list(self.get_tags())
790 for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]:
791 headers[h] = self.get_header(h)
792 output["headers"] = headers
795 parts = self.get_message_parts()
796 for i in xrange(len(parts)):
799 part_dict["id"] = i + 1
800 # We'll be using this is a lot, so let's just get it once.
801 cont_type = msg.get_content_type()
802 part_dict["content-type"] = cont_type
804 # Now we emulate the current behaviour, where it ignores
805 # the html if there's a text representation.
807 # This is being worked on, but it will be easier to fix
808 # here in the future than to end up with another
809 # incompatible solution.
810 disposition = msg["Content-Disposition"]
811 if disposition and disposition.lower().startswith("attachment"):
812 part_dict["filename"] = msg.get_filename()
814 if cont_type.lower() == "text/plain":
815 part_dict["content"] = msg.get_payload()
816 elif (cont_type.lower() == "text/html" and
818 part_dict["content"] = msg.get_payload()
819 body.append(part_dict)
821 output["body"] = body
825 def format_message_as_json(self, indent=0):
826 """Outputs the message as json. This is essentially the same
827 as python's dict format, but we run it through, just so we
828 don't have to worry about the details."""
829 return json.dumps(self.format_message_internal())
831 def format_message_as_text(self, indent=0):
832 """Outputs it in the old-fashioned notmuch text form. Will be
833 easy to change to a new format when the format changes."""
835 format = self.format_message_internal()
836 output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \
837 % (format['id'], indent, format['match'], format['filename'])
838 output += "\n\fheader{"
840 #Todo: this date is supposed to be prettified, as in the index.
841 output += "\n%s (%s) (" % (format["headers"]["From"],
842 format["headers"]["Date"])
843 output += ", ".join(format["tags"])
846 output += "\nSubject: %s" % format["headers"]["Subject"]
847 output += "\nFrom: %s" % format["headers"]["From"]
848 output += "\nTo: %s" % format["headers"]["To"]
849 if format["headers"]["Cc"]:
850 output += "\nCc: %s" % format["headers"]["Cc"]
851 if format["headers"]["Bcc"]:
852 output += "\nBcc: %s" % format["headers"]["Bcc"]
853 output += "\nDate: %s" % format["headers"]["Date"]
854 output += "\n\fheader}"
856 output += "\n\fbody{"
858 parts = format["body"]
859 parts.sort(key=lambda x: x['id'])
861 if not p.has_key("filename"):
862 output += "\n\fpart{ "
863 output += "ID: %d, Content-type: %s\n" % (p["id"],
865 if p.has_key("content"):
866 output += "\n%s\n" % p["content"]
868 output += "Non-text part: %s\n" % p["content-type"]
869 output += "\n\fpart}"
871 output += "\n\fattachment{ "
872 output += "ID: %d, Content-type:%s\n" % (p["id"],
874 output += "Attachment: %s\n" % p["filename"]
875 output += "\n\fattachment}\n"
877 output += "\n\fbody}\n"
878 output += "\n\fmessage}"
883 """Implement hash(), so we can use Message() sets"""
884 file = self.get_filename()
889 def __cmp__(self, other):
890 """Implement cmp(), so we can compare Message()s
892 2 messages are considered equal if they point to the same
893 Message-Id and if they point to the same file names. If 2
894 Messages derive from different queries where some files have
895 been added or removed, the same messages would not be considered
896 equal (as they do not point to the same set of files
898 res = cmp(self.get_message_id(), other.get_message_id())
900 res = cmp(list(self.get_filenames()), list(other.get_filenames()))
904 """Close and free the notmuch Message"""
905 if self._msg is not None:
906 nmlib.notmuch_message_destroy (self._msg)