X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=bindings%2Fpython%2Fnotmuch%2Fmessage.py;h=e71dbe3e9933011d334d3ed18990d6fa2844693d;hp=763d2c6827b6f6e3243c98a9af4ca25ee9aaf254;hb=HEAD;hpb=e59eaa5ddd2c23742c95e2acd34673b58ea34d2d diff --git a/bindings/python/notmuch/message.py b/bindings/python/notmuch/message.py index 763d2c68..e71dbe3e 100644 --- a/bindings/python/notmuch/message.py +++ b/bindings/python/notmuch/message.py @@ -12,219 +12,41 @@ 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 notmuch. If not, see . +along with notmuch. If not, see . -Copyright 2010 Sebastian Spaeth ' +Copyright 2010 Sebastian Spaeth Jesse Rosenthal """ - -from ctypes import c_char_p, c_void_p, c_long, c_uint, c_int -from datetime import date -from notmuch.globals import nmlib, STATUS, NotmuchError, Enum -from notmuch.tag import Tags -from notmuch.filename import Filenames -import sys -import email -import types -try: - import simplejson as json -except ImportError: - import json -#------------------------------------------------------------------------------ -class Messages(object): - """Represents a list of notmuch messages - - This object provides an iterator over a list of notmuch messages - (Technically, it provides a wrapper for the underlying - *notmuch_messages_t* structure). Do note that the underlying library - only provides a one-time iterator (it cannot reset the iterator to - the start). Thus iterating over the function will "exhaust" the list - of messages, and a subsequent iteration attempt will raise a - :exc:`NotmuchError` STATUS.NOT_INITIALIZED. If you need to - re-iterate over a list of messages you will need to retrieve a new - :class:`Messages` object or cache your :class:`Message`\s in a list - via:: - - msglist = list(msgs) - - You can store and reuse the single :class:`Message` objects as often - as you want as long as you keep the parent :class:`Messages` object - around. (Due to hierarchical memory allocation, all derived - :class:`Message` objects will be invalid when we delete the parent - :class:`Messages` object, even if it was already exhausted.) So - this works:: - - db = Database() - msgs = Query(db,'').search_messages() #get a Messages() object - msglist = list(msgs) - - # msgs is "exhausted" now and msgs.next() will raise an exception. - # However it will be kept alive until all retrieved Message() - # objects are also deleted. If you do e.g. an explicit del(msgs) - # here, the following lines would fail. - - # You can reiterate over *msglist* however as often as you want. - # It is simply a list with :class:`Message`s. - - print (msglist[0].get_filename()) - print (msglist[1].get_filename()) - print (msglist[0].get_message_id()) - - - As :class:`Message` implements both __hash__() and __cmp__(), it is - possible to make sets out of :class:`Messages` and use set - arithmetic (this happens in python and will of course be *much* - slower than redoing a proper query with the appropriate filters:: - - s1, s2 = set(msgs1), set(msgs2) - s.union(s2) - s1 -= s2 - ... - - Be careful when using set arithmetic between message sets derived - from different Databases (ie the same database reopened after - messages have changed). If messages have added or removed associated - files in the meantime, it is possible that the same message would be - considered as a different object (as it points to a different file). - """ - - #notmuch_messages_get - _get = nmlib.notmuch_messages_get - _get.restype = c_void_p - - _collect_tags = nmlib.notmuch_messages_collect_tags - _collect_tags.restype = c_void_p - - def __init__(self, msgs_p, parent=None): - """ - :param msgs_p: A pointer to an underlying *notmuch_messages_t* - structure. These are not publically exposed, so a user - will almost never instantiate a :class:`Messages` object - herself. They are usually handed back as a result, - e.g. in :meth:`Query.search_messages`. *msgs_p* must be - valid, we will raise an :exc:`NotmuchError` - (STATUS.NULL_POINTER) if it is `None`. - :type msgs_p: :class:`ctypes.c_void_p` - :param parent: The parent object - (ie :class:`Query`) these tags are derived from. It saves - a reference to it, so we can automatically delete the db - object once all derived objects are dead. - :TODO: Make the iterator work more than once and cache the tags in - the Python object.(?) - """ - if msgs_p is None: - NotmuchError(STATUS.NULL_POINTER) - - self._msgs = msgs_p - #store parent, so we keep them alive as long as self is alive - self._parent = parent - - def collect_tags(self): - """Return the unique :class:`Tags` in the contained messages - - :returns: :class:`Tags` - :exceptions: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if not inited - - .. note:: :meth:`collect_tags` will iterate over the messages and - therefore will not allow further iterations. - """ - if self._msgs is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - - # collect all tags (returns NULL on error) - tags_p = Messages._collect_tags (self._msgs) - #reset _msgs as we iterated over it and can do so only once - self._msgs = None - - if tags_p == None: - raise NotmuchError(STATUS.NULL_POINTER) - return Tags(tags_p, self) - - def __iter__(self): - """ Make Messages an iterator """ - return self - - def next(self): - if self._msgs is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - if not nmlib.notmuch_messages_valid(self._msgs): - self._msgs = None - raise StopIteration - - msg = Message(Messages._get (self._msgs), self) - nmlib.notmuch_messages_move_to_next(self._msgs) - return msg - - def __nonzero__(self): - """ - :return: True if there is at least one more thread in the - Iterator, False if not.""" - return self._msgs is not None and \ - nmlib.notmuch_messages_valid(self._msgs) > 0 +from ctypes import c_char_p, c_long, c_uint, c_int, POINTER, byref +from datetime import date +from .globals import ( + nmlib, + Enum, + _str, + Python3StringMixIn, + NotmuchTagsP, + NotmuchMessageP, + NotmuchMessagesP, + NotmuchMessagePropertiesP, + NotmuchFilenamesP, +) +from .errors import ( + STATUS, + NotmuchError, + NullPointerError, + NotInitializedError, +) +from .tag import Tags +from .filenames import Filenames - def __del__(self): - """Close and free the notmuch Messages""" - if self._msgs is not None: - nmlib.notmuch_messages_destroy (self._msgs) +import email +import sys - 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): - """Represents a single Email message +class Message(Python3StringMixIn): + r"""Represents a single Email message Technically, this wraps the underlying *notmuch_message_t* structure. A user will usually not create these objects themselves @@ -236,122 +58,170 @@ class Message(object): """notmuch_message_get_filename (notmuch_message_t *message)""" _get_filename = nmlib.notmuch_message_get_filename - _get_filename.restype = c_char_p + _get_filename.argtypes = [NotmuchMessageP] + _get_filename.restype = c_char_p """return all filenames for a message""" _get_filenames = nmlib.notmuch_message_get_filenames - _get_filenames.restype = c_void_p + _get_filenames.argtypes = [NotmuchMessageP] + _get_filenames.restype = NotmuchFilenamesP """notmuch_message_get_flag""" _get_flag = nmlib.notmuch_message_get_flag - _get_flag.restype = c_uint + _get_flag.argtypes = [NotmuchMessageP, c_uint] + _get_flag.restype = bool + + """notmuch_message_set_flag""" + _set_flag = nmlib.notmuch_message_set_flag + _set_flag.argtypes = [NotmuchMessageP, c_uint, c_int] + _set_flag.restype = None """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 + _get_message_id.argtypes = [NotmuchMessageP] + _get_message_id.restype = c_char_p """notmuch_message_get_thread_id""" _get_thread_id = nmlib.notmuch_message_get_thread_id + _get_thread_id.argtypes = [NotmuchMessageP] _get_thread_id.restype = c_char_p """notmuch_message_get_replies""" _get_replies = nmlib.notmuch_message_get_replies - _get_replies.restype = c_void_p + _get_replies.argtypes = [NotmuchMessageP] + _get_replies.restype = NotmuchMessagesP """notmuch_message_get_tags (notmuch_message_t *message)""" _get_tags = nmlib.notmuch_message_get_tags - _get_tags.restype = c_void_p + _get_tags.argtypes = [NotmuchMessageP] + _get_tags.restype = NotmuchTagsP _get_date = nmlib.notmuch_message_get_date + _get_date.argtypes = [NotmuchMessageP] _get_date.restype = c_long _get_header = nmlib.notmuch_message_get_header + _get_header.argtypes = [NotmuchMessageP, c_char_p] _get_header.restype = c_char_p """notmuch_status_t ..._maildir_flags_to_tags (notmuch_message_t *)""" _tags_to_maildir_flags = nmlib.notmuch_message_tags_to_maildir_flags + _tags_to_maildir_flags.argtypes = [NotmuchMessageP] _tags_to_maildir_flags.restype = c_int """notmuch_status_t ..._tags_to_maildir_flags (notmuch_message_t *)""" _maildir_flags_to_tags = nmlib.notmuch_message_maildir_flags_to_tags + _maildir_flags_to_tags.argtypes = [NotmuchMessageP] _maildir_flags_to_tags.restype = c_int + """notmuch_message_get_property""" + _get_property = nmlib.notmuch_message_get_property + _get_property.argtypes = [NotmuchMessageP, c_char_p, POINTER(c_char_p)] + _get_property.restype = c_int + + """notmuch_message_get_properties""" + _get_properties = nmlib.notmuch_message_get_properties + _get_properties.argtypes = [NotmuchMessageP, c_char_p, c_int] + _get_properties.restype = NotmuchMessagePropertiesP + + """notmuch_message_properties_valid""" + _properties_valid = nmlib.notmuch_message_properties_valid + _properties_valid.argtypes = [NotmuchMessagePropertiesP] + _properties_valid.restype = bool + + """notmuch_message_properties_value""" + _properties_value = nmlib.notmuch_message_properties_value + _properties_value.argtypes = [NotmuchMessagePropertiesP] + _properties_value.restype = c_char_p + + """notmuch_message_properties_key""" + _properties_key = nmlib.notmuch_message_properties_key + _properties_key.argtypes = [NotmuchMessagePropertiesP] + _properties_key.restype = c_char_p + + """notmuch_message_properties_move_to_next""" + _properties_move_to_next = nmlib.notmuch_message_properties_move_to_next + _properties_move_to_next.argtypes = [NotmuchMessagePropertiesP] + _properties_move_to_next.restype = None + #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 - Structure. If it is `None`, we will raise an :exc:`NotmuchError` - STATUS.NULL_POINTER. + Structure. If it is `None`, we will raise an + :exc:`NullPointerError`. :param parent: A 'parent' object is passed which this message is derived from. We save a reference to it, so we can automatically delete the parent object once all derived objects are dead. """ - if msg_p is None: - NotmuchError(STATUS.NULL_POINTER) + if not msg_p: + raise NullPointerError() self._msg = msg_p #keep reference to parent, so we keep it alive self._parent = parent - def get_message_id(self): """Returns the message ID - + :returns: String with a message ID - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + :raises: :exc:`NotInitializedError` if the message is not initialized. """ - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - return Message._get_message_id(self._msg) + if not self._msg: + raise NotInitializedError() + return Message._get_message_id(self._msg).decode('utf-8', 'ignore') def get_thread_id(self): """Returns the thread ID - The returned string belongs to 'message' will only be valid for as + The returned string belongs to 'message' will only be valid for as long as the message is valid. This function will not return `None` since Notmuch ensures that every message belongs to a single thread. :returns: String with a thread ID - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + :raises: :exc:`NotInitializedError` if the message is not initialized. """ - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + if not self._msg: + raise NotInitializedError() - return Message._get_thread_id (self._msg); + return Message._get_thread_id(self._msg).decode('utf-8', 'ignore') def get_replies(self): - """Gets all direct replies to this message as :class:`Messages` iterator - - .. note:: This call only makes sense if 'message' was - ultimately obtained from a :class:`Thread` object, (such as - by coming directly from the result of calling - :meth:`Thread.get_toplevel_messages` or by any number of - subsequent calls to :meth:`get_replies`). If this message was - obtained through some non-thread means, (such as by a call - to :meth:`Query.search_messages`), then this function will - return `None`. - - :returns: :class:`Messages` or `None` if there are no replies to - this message. - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + """Gets all direct replies to this message as :class:`Messages` + iterator + + .. note:: + + This call only makes sense if 'message' was ultimately obtained from + a :class:`Thread` object, (such as by coming directly from the + result of calling :meth:`Thread.get_toplevel_messages` or by any + number of subsequent calls to :meth:`get_replies`). If this message + was obtained through some non-thread means, (such as by a call to + :meth:`Query.search_messages`), then this function will return + an empty Messages iterator. + + :returns: :class:`Messages`. + :raises: :exc:`NotInitializedError` if the message is not initialized. """ - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + if not self._msg: + raise NotInitializedError() - msgs_p = Message._get_replies(self._msg); + msgs_p = Message._get_replies(self._msg) - if msgs_p is None: - return None + from .messages import Messages, EmptyMessagesResult - return Messages(msgs_p,self) + if not msgs_p: + return EmptyMessagesResult(self) + + return Messages(msgs_p, self) def get_date(self): """Returns time_t of the message date @@ -362,50 +232,49 @@ class Message(object): :returns: A time_t timestamp. :rtype: c_unit64 - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + :raises: :exc:`NotInitializedError` if the message is not initialized. """ - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + if not self._msg: + raise NotInitializedError() return Message._get_date(self._msg) def get_header(self, header): - """Returns a message header - - This returns any message header that is stored in the notmuch database. - This is only a selected subset of headers, which is currently: + """Get the value of the specified header. + + The value will be read from the actual message file, not from + the notmuch database. The header name is case insensitive. - TODO: add stored headers + Returns an empty string ("") if the message does not contain a + header line matching 'header'. :param header: The name of the header to be retrieved. - It is not case-sensitive (TODO: confirm). + It is not case-sensitive. :type header: str :returns: The header value as string - :exception: :exc:`NotmuchError` - - * STATUS.NOT_INITIALIZED if the message - is not initialized. - * STATUS.NULL_POINTER, if no header was found + :raises: :exc:`NotInitializedError` if the message is not + initialized + :raises: :exc:`NullPointerError` if any error occurred """ - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + if not self._msg: + raise NotInitializedError() #Returns NULL if any error occurs. - header = Message._get_header (self._msg, header) + header = Message._get_header(self._msg, _str(header)) if header == None: - raise NotmuchError(STATUS.NULL_POINTER) - return header + raise NullPointerError() + return header.decode('UTF-8', 'ignore') def get_filename(self): """Returns the file path of the message file :returns: Absolute file path & name of the message file - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + :raises: :exc:`NotInitializedError` if the message is not initialized. """ - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - return Message._get_filename(self._msg) + if not self._msg: + raise NotInitializedError() + return Message._get_filename(self._msg).decode('utf-8', 'ignore') def get_filenames(self): """Get all filenames for the email corresponding to 'message' @@ -413,12 +282,12 @@ class Message(object): Returns a Filenames() generator with all absolute filepaths for messages recorded to have the same Message-ID. These files must not necessarily have identical content.""" - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - + if not self._msg: + raise NotInitializedError() + files_p = Message._get_filenames(self._msg) - return Filenames(files_p, self).as_generator() + return Filenames(files_p, self) def get_flag(self, flag): """Checks whether a specific flag is set for this message @@ -427,49 +296,50 @@ class Message(object): *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 + :param flag: One of the :attr:`Message.FLAG` values (currently only *Message.FLAG.MATCH* :returns: An unsigned int (0/1), indicating whether the flag is set. - :exception: :exc:`NotmuchError` STATUS.NOT_INITIALIZED if the message + :raises: :exc:`NotInitializedError` if the message is not initialized. """ - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + if not self._msg: + raise NotInitializedError() 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 + :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 + :raises: :exc:`NotInitializedError` 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) + if not self._msg: + raise NotInitializedError() + self._set_flag(self._msg, flag, value) def get_tags(self): """Returns the message tags :returns: A :class:`Tags` iterator. - :exception: :exc:`NotmuchError` - - * STATUS.NOT_INITIALIZED if the message - is not initialized. - * STATUS.NULL_POINTER, on error + :raises: :exc:`NotInitializedError` if the message is not + initialized + :raises: :exc:`NullPointerError` if any error occurred """ - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + if not self._msg: + raise NotInitializedError() tags_p = Message._get_tags(self._msg) - if tags_p == None: - raise NotmuchError(STATUS.NULL_POINTER) + if not tags_p: + raise NullPointerError() return Tags(tags_p, self) + _add_tag = nmlib.notmuch_message_add_tag + _add_tag.argtypes = [NotmuchMessageP, c_char_p] + _add_tag.restype = c_uint + def add_tag(self, tag, sync_maildir_flags=False): """Adds a tag to the given message @@ -488,23 +358,18 @@ class Message(object): :returns: STATUS.SUCCESS if the tag was successfully added. Raises an exception otherwise. - :exception: :exc:`NotmuchError`. They have the following meaning: - - STATUS.NULL_POINTER - The 'tag' argument is NULL - STATUS.TAG_TOO_LONG - The length of 'tag' is too long - (exceeds Message.NOTMUCH_TAG_MAX) - STATUS.READ_ONLY_DATABASE - Database was opened in read-only mode so message cannot be - modified. - STATUS.NOT_INITIALIZED - The message has not been initialized. - """ - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - - status = nmlib.notmuch_message_add_tag (self._msg, tag) + :raises: :exc:`NullPointerError` if the `tag` argument is NULL + :raises: :exc:`TagTooLongError` if the length of `tag` exceeds + Message.NOTMUCH_TAG_MAX) + :raises: :exc:`ReadOnlyDatabaseError` if the database was opened + in read-only mode so message cannot be modified + :raises: :exc:`NotInitializedError` if message has not been + initialized + """ + if not self._msg: + raise NotInitializedError() + + status = self._add_tag(self._msg, _str(tag)) # bail out on failure if status != STATUS.SUCCESS: @@ -514,6 +379,10 @@ class Message(object): self.tags_to_maildir_flags() return STATUS.SUCCESS + _remove_tag = nmlib.notmuch_message_remove_tag + _remove_tag.argtypes = [NotmuchMessageP, c_char_p] + _remove_tag.restype = c_uint + def remove_tag(self, tag, sync_maildir_flags=False): """Removes a tag from the given message @@ -529,26 +398,21 @@ class Message(object): that this will do nothing when a message is frozen, as tag changes will not be committed to the database yet. - :returns: STATUS.SUCCESS if the tag was successfully removed or if + :returns: STATUS.SUCCESS if the tag was successfully removed or if the message had no such tag. Raises an exception otherwise. - :exception: :exc:`NotmuchError`. They have the following meaning: - - STATUS.NULL_POINTER - The 'tag' argument is NULL - STATUS.TAG_TOO_LONG - The length of 'tag' is too long - (exceeds NOTMUCH_TAG_MAX) - STATUS.READ_ONLY_DATABASE - Database was opened in read-only mode so message cannot - be modified. - STATUS.NOT_INITIALIZED - The message has not been initialized. + :raises: :exc:`NullPointerError` if the `tag` argument is NULL + :raises: :exc:`TagTooLongError` if the length of `tag` exceeds + Message.NOTMUCH_TAG_MAX) + :raises: :exc:`ReadOnlyDatabaseError` if the database was opened + in read-only mode so message cannot be modified + :raises: :exc:`NotInitializedError` if message has not been + initialized """ - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) + if not self._msg: + raise NotInitializedError() - status = nmlib.notmuch_message_remove_tag(self._msg, tag) + status = self._remove_tag(self._msg, _str(tag)) # bail out on error if status != STATUS.SUCCESS: raise NotmuchError(status) @@ -557,7 +421,9 @@ class Message(object): self.tags_to_maildir_flags() return STATUS.SUCCESS - + _remove_all_tags = nmlib.notmuch_message_remove_all_tags + _remove_all_tags.argtypes = [NotmuchMessageP] + _remove_all_tags.restype = c_uint def remove_all_tags(self, sync_maildir_flags=False): """Removes all tags from the given message. @@ -576,18 +442,15 @@ class Message(object): :returns: STATUS.SUCCESS if the tags were successfully removed. Raises an exception otherwise. - :exception: :exc:`NotmuchError`. They have the following meaning: - - STATUS.READ_ONLY_DATABASE - Database was opened in read-only mode so message cannot - be modified. - STATUS.NOT_INITIALIZED - The message has not been initialized. + :raises: :exc:`ReadOnlyDatabaseError` if the database was opened + in read-only mode so message cannot be modified + :raises: :exc:`NotInitializedError` if message has not been + initialized """ - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - - status = nmlib.notmuch_message_remove_all_tags(self._msg) + if not self._msg: + raise NotInitializedError() + + status = self._remove_all_tags(self._msg) # bail out on error if status != STATUS.SUCCESS: @@ -597,12 +460,65 @@ class Message(object): self.tags_to_maildir_flags() return STATUS.SUCCESS + _freeze = nmlib.notmuch_message_freeze + _freeze.argtypes = [NotmuchMessageP] + _freeze.restype = c_uint + + def get_property(self, prop): + """ Retrieve the value for a single property key + + :param prop: The name of the property to get. + :returns: String with the property value or None if there is no such + key. In the case of multiple values for the given key, the + first one is retrieved. + :raises: :exc:`NotInitializedError` if message has not been + initialized + """ + if not self._msg: + raise NotInitializedError() + + value = c_char_p() + status = Message._get_property(self._msg, _str(prop), byref(value)) + if status != 0: + raise NotmuchError(status) + + if value is None or value.value is None: + return None + return value.value.decode('utf-8') + + def get_properties(self, prop="", exact=False): + """ Get the properties of the message, returning a generator of + name, value pairs. + + The generator will yield once per value. There might be more than one + value on each name, so the generator might yield the same name several + times. + + :param prop: The name of the property to get. Otherwise it will return + the full list of properties of the message. + :param exact: if True, require exact match with key. Otherwise + treat as prefix. + :yields: Each property values as a pair of `name, value` + :ytype: pairs of str + :raises: :exc:`NotInitializedError` if message has not been + initialized + """ + if not self._msg: + raise NotInitializedError() + + properties = Message._get_properties(self._msg, _str(prop), exact) + while Message._properties_valid(properties): + key = Message._properties_key(properties) + value = Message._properties_value(properties) + yield key.decode("utf-8"), value.decode("utf-8") + Message._properties_move_to_next(properties) + def freeze(self): """Freezes the current state of 'message' within the database - This means that changes to the message state, (via :meth:`add_tag`, - :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be - committed to the database until the message is :meth:`thaw`ed. + This means that changes to the message state, (via :meth:`add_tag`, + :meth:`remove_tag`, and :meth:`remove_all_tags`), will not be + committed to the database until the message is :meth:`thaw` ed. Multiple calls to freeze/thaw are valid and these calls will "stack". That is there must be as many calls to thaw as to freeze @@ -630,18 +546,15 @@ class Message(object): :returns: STATUS.SUCCESS if the message was successfully frozen. Raises an exception otherwise. - :exception: :exc:`NotmuchError`. They have the following meaning: - - STATUS.READ_ONLY_DATABASE - Database was opened in read-only mode so message cannot - be modified. - STATUS.NOT_INITIALIZED - The message has not been initialized. + :raises: :exc:`ReadOnlyDatabaseError` if the database was opened + in read-only mode so message cannot be modified + :raises: :exc:`NotInitializedError` if message has not been + initialized """ - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - - status = nmlib.notmuch_message_freeze(self._msg) + if not self._msg: + raise NotInitializedError() + + status = self._freeze(self._msg) if STATUS.SUCCESS == status: # return on success @@ -649,10 +562,14 @@ class Message(object): raise NotmuchError(status) + _thaw = nmlib.notmuch_message_thaw + _thaw.argtypes = [NotmuchMessageP] + _thaw.restype = c_uint + def thaw(self): """Thaws the current 'message' - Thaw the current 'message', synchronizing any changes that may have + Thaw the current 'message', synchronizing any changes that may have occurred while 'message' was frozen into the notmuch database. See :meth:`freeze` for an example of how to use this @@ -664,19 +581,17 @@ class Message(object): :returns: STATUS.SUCCESS if the message was successfully frozen. Raises an exception otherwise. - :exception: :exc:`NotmuchError`. They have the following meaning: - - STATUS.UNBALANCED_FREEZE_THAW - An attempt was made to thaw an unfrozen message. - That is, there have been an unbalanced number of calls - to :meth:`freeze` and :meth:`thaw`. - STATUS.NOT_INITIALIZED - The message has not been initialized. + :raises: :exc:`UnbalancedFreezeThawError` if an attempt was made + to thaw an unfrozen message. That is, there have been + an unbalanced number of calls to :meth:`freeze` and + :meth:`thaw`. + :raises: :exc:`NotInitializedError` if message has not been + initialized """ - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - - status = nmlib.notmuch_message_thaw(self._msg) + if not self._msg: + raise NotInitializedError() + + status = self._thaw(self._msg) if STATUS.SUCCESS == status: # return on success @@ -684,7 +599,6 @@ class Message(object): raise NotmuchError(status) - def is_match(self): """(Not implemented)""" return self.get_flag(Message.FLAG.MATCH) @@ -697,10 +611,10 @@ class Message(object): 'P' if the message has the "passed" tag 'R' if the message has the "replied" tag 'S' if the message does not have the "unread" tag - + Any existing flags unmentioned in the list above will be preserved in the renaming. - + Also, if this filename is in a directory named "new", rename it to be within the neighboring directory named "cur". @@ -708,11 +622,11 @@ class Message(object): not work yet, as the modified tags have not been committed yet to the database. - :returns: a :class:`STATUS`. In short, you want to see + :returns: a :class:`STATUS` value. In short, you want to see notmuch.STATUS.SUCCESS here. See there for details.""" - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - status = Message._tags_to_maildir_flags(self._msg) + if not self._msg: + raise NotInitializedError() + return Message._tags_to_maildir_flags(self._msg) def maildir_flags_to_tags(self): """Synchronize file Maildir flags to notmuch tags @@ -733,31 +647,32 @@ class Message(object): logical OR operator.) As a convenience, you can set the sync_maildir_flags parameter in - :meth:`Database.add_message` to implicitly call this. + :meth:`Database.index_file` to implicitly call this. :returns: a :class:`STATUS`. In short, you want to see notmuch.STATUS.SUCCESS here. See there for details.""" - if self._msg is None: - raise NotmuchError(STATUS.NOT_INITIALIZED) - status = Message._tags_to_maildir_flags(self._msg) + if not self._msg: + raise NotInitializedError() + return Message._maildir_flags_to_tags(self._msg) def __repr__(self): """Represent a Message() object by str()""" return self.__str__() - def __str__(self): - """A message() is represented by a 1-line summary""" - msg = {} - msg['from'] = self.get_header('from') - msg['tags'] = str(self.get_tags()) - msg['date'] = date.fromtimestamp(self.get_date()) - return "%(from)s (%(date)s) (%(tags)s)" % (msg) - + def __unicode__(self): + format = "%s (%s) (%s)" + return format % (self.get_header('from'), + self.get_tags(), + date.fromtimestamp(self.get_date()), + ) def get_message_parts(self): """Output like notmuch show""" - fp = open(self.get_filename()) - email_msg = email.message_from_file(fp) + fp = open(self.get_filename(), 'rb') + if sys.version_info[0] < 3: + email_msg = email.message_from_file(fp) + else: + email_msg = email.message_from_binary_file(fp) fp.close() out = [] @@ -775,114 +690,10 @@ class Message(object): 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 x: x['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 __hash__(self): """Implement hash(), so we can use Message() sets""" file = self.get_filename() - if file is None: + if not file: return None return hash(file) @@ -895,12 +706,16 @@ class Message(object): been added or removed, the same messages would not be considered equal (as they do not point to the same set of files any more).""" - res = cmp(self.get_message_id(), other.get_message_id()) + res = cmp(self.get_message_id(), other.get_message_id()) if res: res = cmp(list(self.get_filenames()), list(other.get_filenames())) return res + _destroy = nmlib.notmuch_message_destroy + _destroy.argtypes = [NotmuchMessageP] + _destroy.restype = None + def __del__(self): """Close and free the notmuch Message""" - if self._msg is not None: - nmlib.notmuch_message_destroy (self._msg) + if self._msg: + self._destroy(self._msg)