X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=cnotmuch%2Fmessage.py;h=ba93d8fea773fdb1807f9dddc1417973b48caf69;hp=3ff3e7f158fab4c66951d1ac42c718747df026b5;hb=bac66abdd2a02c0af677c3e503ea3bae705287c6;hpb=81a041d4cbc4aefae5d5e8c6f753fd68af62c27c diff --git a/cnotmuch/message.py b/cnotmuch/message.py index 3ff3e7f1..ba93d8fe 100644 --- a/cnotmuch/message.py +++ b/cnotmuch/message.py @@ -1,7 +1,32 @@ -from ctypes import c_char_p, c_void_p, c_long +# This file is part of cnotmuch. +# +# cnotmuch is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# cnotmuch is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with cnotmuch. If not, see . +# +# (C) Copyright 2010 Sebastian Spaeth +# Jesse Rosenthal + +from ctypes import c_char_p, c_void_p, c_long, c_bool from datetime import date -from cnotmuch.globals import nmlib, STATUS, NotmuchError +from cnotmuch.globals import nmlib, STATUS, NotmuchError, Enum from cnotmuch.tag import Tags +import sys +import email +import types +try: + import simplejson as json +except ImportError: + import json #------------------------------------------------------------------------------ class Messages(object): """Represents a list of notmuch messages @@ -147,13 +172,63 @@ class Messages(object): self._msgs = None return i - - def __del__(self): """Close and free the notmuch Messages""" if self._msgs is not None: nmlib.notmuch_messages_destroy (self._msgs) + def print_messages(self, format, indent=0, entire_thread=False): + """Outputs messages as needed for 'notmuch show' to sys.stdout + + :param format: A string of either 'text' or 'json'. + :param indent: A number indicating the reply depth of these messages. + :param entire_thread: A bool, indicating whether we want to output + whole threads or only the matching messages. + """ + if format.lower() == "text": + set_start = "" + set_end = "" + set_sep = "" + elif format.lower() == "json": + set_start = "[" + set_end = "]" + set_sep = ", " + else: + raise Exception + + first_set = True + + sys.stdout.write(set_start) + + # iterate through all toplevel messages in this thread + for msg in self: + # if not msg: + # break + if not first_set: + sys.stdout.write(set_sep) + first_set = False + + sys.stdout.write(set_start) + match = msg.is_match() + next_indent = indent + + if (match or entire_thread): + if format.lower() == "text": + sys.stdout.write(msg.format_message_as_text(indent)) + elif format.lower() == "json": + sys.stdout.write(msg.format_message_as_json(indent)) + else: + raise NotmuchError + next_indent = indent + 1 + + # get replies and print them also out (if there are any) + replies = msg.get_replies() + if not replies is None: + sys.stdout.write(set_sep) + replies.print_messages(format, next_indent, entire_thread) + + sys.stdout.write(set_end) + sys.stdout.write(set_end) #------------------------------------------------------------------------------ class Message(object): @@ -166,6 +241,10 @@ class Message(object): _get_filename = nmlib.notmuch_message_get_filename _get_filename.restype = c_char_p + """notmuch_message_get_flag""" + _get_flag = nmlib.notmuch_message_get_flag + _get_flag.restype = c_bool + """notmuch_message_get_message_id (notmuch_message_t *message)""" _get_message_id = nmlib.notmuch_message_get_message_id _get_message_id.restype = c_char_p @@ -188,6 +267,9 @@ class Message(object): _get_header = nmlib.notmuch_message_get_header _get_header.restype = c_char_p + #Constants: Flags that can be set/get with set_flag + FLAG = Enum(['MATCH']) + def __init__(self, msg_p, parent=None): """ :param msg_p: A pointer to an internal notmuch_message_t @@ -315,6 +397,38 @@ class Message(object): raise NotmuchError(STATUS.NOT_INITIALIZED) return Message._get_filename(self._msg) + def get_flag(self, flag): + """Checks whether a specific flag is set for this message + + The method :meth:`Query.search_threads` sets + *Message.FLAG.MATCH* for those messages that match the + query. This method allows us to get the value of this flag. + + :param flag: One of the :attr:`Message.FLAG` values (currently only + *Message.FLAG.MATCH* + :returns: A bool, indicating whether the flag is set. + :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + is not initialized. + """ + if self._msg is None: + raise NotmuchError(STATUS.NOT_INITIALIZED) + return Message._get_flag(self._msg, flag) + + def set_flag(self, flag, value): + """Sets/Unsets a specific flag for this message + + :param flag: One of the :attr:`Message.FLAG` values (currently only + *Message.FLAG.MATCH* + :param value: A bool indicating whether to set or unset the flag. + + :returns: Nothing + :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + is not initialized. + """ + if self._msg is None: + raise NotmuchError(STATUS.NOT_INITIALIZED) + nmlib.notmuch_message_set_flag(self._msg, flag, value) + def get_tags(self): """Returns the message tags @@ -513,7 +627,11 @@ class Message(object): raise NotmuchError(status) - + + def is_match(self): + """(Not implemented)""" + return self.get_flag(Message.FLAG.MATCH) + def __str__(self): """A message() is represented by a 1-line summary""" msg = {} @@ -524,9 +642,131 @@ class Message(object): msg['replies'] = len(replies) if replies is not None else -1 return "%(from)s (%(date)s) (%(tags)s) (%(replies)d) replies" % (msg) - def format_as_text(self): - """Output like notmuch show (Not implemented)""" - return str(self) + + def get_message_parts(self): + """Output like notmuch show""" + fp = open(self.get_filename()) + email_msg = email.message_from_file(fp) + fp.close() + + out = [] + for msg in email_msg.walk(): + if not msg.is_multipart(): + out.append(msg) + return out + + def get_part(self, num): + """Returns the nth message body part""" + parts = self.get_message_parts() + if (num <= 0 or num > len(parts)): + return "" + else: + out_part = parts[(num - 1)] + return out_part.get_payload(decode=True) + + def format_message_internal(self): + """Create an internal representation of the message parts, + which can easily be output to json, text, or another output + format. The argument match tells whether this matched a + query.""" + output = {} + output["id"] = self.get_message_id() + output["match"] = self.is_match() + output["filename"] = self.get_filename() + output["tags"] = list(self.get_tags()) + + headers = {} + for h in ["Subject", "From", "To", "Cc", "Bcc", "Date"]: + headers[h] = self.get_header(h) + output["headers"] = headers + + body = [] + parts = self.get_message_parts() + for i in xrange(len(parts)): + msg = parts[i] + part_dict = {} + part_dict["id"] = i + 1 + # We'll be using this is a lot, so let's just get it once. + cont_type = msg.get_content_type() + part_dict["content-type"] = cont_type + # NOTE: + # Now we emulate the current behaviour, where it ignores + # the html if there's a text representation. + # + # This is being worked on, but it will be easier to fix + # here in the future than to end up with another + # incompatible solution. + disposition = msg["Content-Disposition"] + if disposition and disposition.lower().startswith("attachment"): + part_dict["filename"] = msg.get_filename() + else: + if cont_type.lower() == "text/plain": + part_dict["content"] = msg.get_payload() + elif (cont_type.lower() == "text/html" and + i == 0): + part_dict["content"] = msg.get_payload() + body.append(part_dict) + + output["body"] = body + + return output + + def format_message_as_json(self, indent=0): + """Outputs the message as json. This is essentially the same + as python's dict format, but we run it through, just so we + don't have to worry about the details.""" + return json.dumps(self.format_message_internal()) + + def format_message_as_text(self, indent=0): + """Outputs it in the old-fashioned notmuch text form. Will be + easy to change to a new format when the format changes.""" + + format = self.format_message_internal() + output = "\fmessage{ id:%s depth:%d match:%d filename:%s" \ + % (format['id'], indent, format['match'], format['filename']) + output += "\n\fheader{" + + #Todo: this date is supposed to be prettified, as in the index. + output += "\n%s (%s) (" % (format["headers"]["from"], + format["headers"]["date"]) + output += ", ".join(format["tags"]) + output += ")" + + output += "\nSubject: %s" % format["headers"]["subject"] + output += "\nFrom: %s" % format["headers"]["from"] + output += "\nTo: %s" % format["headers"]["to"] + if format["headers"]["cc"]: + output += "\nCc: %s" % format["headers"]["cc"] + if format["headers"]["bcc"]: + output += "\nBcc: %s" % format["headers"]["bcc"] + output += "\nDate: %s" % format["headers"]["date"] + output += "\n\fheader}" + + output += "\n\fbody{" + + parts = format["body"] + parts.sort(key=lambda(p): p["id"]) + for p in parts: + if not p.has_key("filename"): + output += "\n\fpart{ " + output += "ID: %d, Content-type: %s\n" % (p["id"], + p["content_type"]) + if p.has_key("content"): + output += "\n%s\n" % p["content"] + else: + output += "Non-text part: %s\n" % p["content_type"] + output += "\n\fpart}" + else: + output += "\n\fattachment{ " + output += "ID: %d, Content-type:%s\n" % (p["id"], + p["content_type"]) + output += "Attachment: %s\n" % p["filename"] + output += "\n\fattachment}\n" + + output += "\n\fbody}\n" + output += "\n\fmessage}" + + return output def __del__(self): """Close and free the notmuch Message"""