X-Git-Url: https://git.notmuchmail.org/git?p=notmuch;a=blobdiff_plain;f=bindings%2Fpython%2Fnotmuch%2Fmessage.py;h=8944af42b33323be7142a251589834989d354824;hp=8fd9eacef0088c84da98e1b15eb34f71dba50a91;hb=d8c0e0c72dd19b50d51f07c70b880f21d69c2590;hpb=8866a89e3cff46dbd791a1385ca5c800a5c9091e diff --git a/bindings/python/notmuch/message.py b/bindings/python/notmuch/message.py index 8fd9eace..8944af42 100644 --- a/bindings/python/notmuch/message.py +++ b/bindings/python/notmuch/message.py @@ -19,7 +19,7 @@ Copyright 2010 Sebastian Spaeth ' """ -from ctypes import c_char_p, c_void_p, c_long, c_uint +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 @@ -41,35 +41,52 @@ class Messages(object): 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. Also note, that any - function that uses iteration will also exhaust the messages.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:: + :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 Message objects as often as you - want as long as you keep the parent Messages object around. (Recall - that due to hierarchical memory allocation, all derived Message - objects will be invalid when we delete the parent Messages() object, - even if it was already "exhausted".) So this works:: + 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 even len(msgs) will raise an exception. - # However it will be kept around until all retrieved Message() objects are - # also deleted. If you did e.g. an explicit del(msgs) here, the - # following lines would fail. + # 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 Message objects. + # 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 @@ -210,6 +227,11 @@ class Message(object): """Represents a single Email message Technically, this wraps the underlying *notmuch_message_t* structure. + + As this implements __cmp__() it is possible to compare 2 + :class:`Message`s with:: + + if msg1 == msg2: """ """notmuch_message_get_filename (notmuch_message_t *message)""" @@ -246,6 +268,14 @@ class Message(object): _get_header = nmlib.notmuch_message_get_header _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.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.restype = c_int + #Constants: Flags that can be set/get with set_flag FLAG = Enum(['MATCH']) @@ -439,13 +469,22 @@ class Message(object): raise NotmuchError(STATUS.NULL_POINTER) return Tags(tags_p, self) - def add_tag(self, tag): + def add_tag(self, tag, sync_maildir_flags=True): """Adds a tag to the given message Adds a tag to the current message. The maximal tag length is defined in the notmuch library and is currently 200 bytes. :param tag: String with a 'tag' to be added. + + :param sync_maildir_flags: If notmuch configuration is set to do + this, add maildir flags corresponding to notmuch tags. See + :meth:`tags_to_maildir_flags`. Use False if you want to + add/remove many tags on a message without having to + physically rename the file every time. Do note, 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 added. Raises an exception otherwise. :exception: :exc:`NotmuchError`. They have the following meaning: @@ -466,19 +505,29 @@ class Message(object): status = nmlib.notmuch_message_add_tag (self._msg, tag) - if STATUS.SUCCESS == status: - # return on success - return status + # bail out on failure + if status != STATUS.SUCCESS: + raise NotmuchError(status) - raise NotmuchError(status) + if sync_maildir_flags: + self.tags_to_maildir_flags() + return STATUS.SUCCESS - def remove_tag(self, tag): + def remove_tag(self, tag, sync_maildir_flags=True): """Removes a tag from the given message If the message has no such tag, this is a non-operation and will report success anyway. :param tag: String with a 'tag' to be removed. + :param sync_maildir_flags: If notmuch configuration is set to do + this, add maildir flags corresponding to notmuch tags. See + :meth:`tags_to_maildir_flags`. Use False if you want to + add/remove many tags on a message without having to + physically rename the file every time. Do note, 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 the message had no such tag. Raises an exception otherwise. @@ -499,19 +548,30 @@ class Message(object): raise NotmuchError(STATUS.NOT_INITIALIZED) status = nmlib.notmuch_message_remove_tag(self._msg, tag) + # bail out on error + if status != STATUS.SUCCESS: + raise NotmuchError(status) - if STATUS.SUCCESS == status: - # return on success - return status + if sync_maildir_flags: + self.tags_to_maildir_flags() + return STATUS.SUCCESS - raise NotmuchError(status) - def remove_all_tags(self): + + def remove_all_tags(self, sync_maildir_flags=True): """Removes all tags from the given message. See :meth:`freeze` for an example showing how to safely replace tag values. + :param sync_maildir_flags: If notmuch configuration is set to do + this, add maildir flags corresponding to notmuch tags. See + :meth:`tags_to_maildir_flags`. Use False if you want to + add/remove many tags on a message without having to + physically rename the file every time. Do note, 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 tags were successfully removed. Raises an exception otherwise. :exception: :exc:`NotmuchError`. They have the following meaning: @@ -527,11 +587,13 @@ class Message(object): status = nmlib.notmuch_message_remove_all_tags(self._msg) - if STATUS.SUCCESS == status: - # return on success - return status + # bail out on error + if status != STATUS.SUCCESS: + raise NotmuchError(status) - raise NotmuchError(status) + if sync_maildir_flags: + self.tags_to_maildir_flags() + return STATUS.SUCCESS def freeze(self): """Freezes the current state of 'message' within the database @@ -549,10 +611,11 @@ class Message(object): have a given set of tags might look like this:: msg.freeze() - msg.remove_all_tags() + msg.remove_all_tags(False) for tag in new_tags: - msg.add_tag(tag) + msg.add_tag(tag, False) msg.thaw() + msg.tags_to_maildir_flags() With freeze/thaw used like this, the message in the database is guaranteed to have either the full set of original tag values, or @@ -624,6 +687,61 @@ class Message(object): """(Not implemented)""" return self.get_flag(Message.FLAG.MATCH) + def tags_to_maildir_flags(self): + """Synchronize notmuch tags to file Maildir flags + + 'D' if the message has the "draft" tag + 'F' if the message has the "flagged" tag + '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". + + Usually, you do not need to call this manually as + tag changing methods should be implicitly calling it. + + :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) + + def maildir_flags_to_tags(self): + """Synchronize file Maildir flags to notmuch tags + + Flag Action if present + ---- ----------------- + 'D' Adds the "draft" tag to the message + 'F' Adds the "flagged" tag to the message + 'P' Adds the "passed" tag to the message + 'R' Adds the "replied" tag to the message + 'S' Removes the "unread" tag from the message + + For each flag that is not present, the opposite action + (add/remove) is performed for the corresponding tags. If there + are multiple filenames associated with this message, the flag is + considered present if it appears in one or more filenames. (That + is, the flags from the multiple filenames are combined with the + logical OR operator.) + + Usually, you do not need to call this manually as + :meth:`Database.add_message` implicitly calls it. + + :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) + + 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 = {} @@ -631,8 +749,8 @@ class Message(object): msg['tags'] = str(self.get_tags()) msg['date'] = date.fromtimestamp(self.get_date()) replies = self.get_replies() - msg['replies'] = len(replies) if replies is not None else -1 - return "%(from)s (%(date)s) (%(tags)s) (%(replies)d) replies" % (msg) + msg['replies'] = len(replies) if replies is not None else 0 + return "%(from)s (%(date)s) (%(tags)s) %(replies)d replies" % (msg) def get_message_parts(self): @@ -760,6 +878,27 @@ class Message(object): return output + def __hash__(self): + """Implement hash(), so we can use Message() sets""" + file = self.get_filename() + if file is None: + return None + return hash(file) + + def __cmp__(self, other): + """Implement cmp(), so we can compare Message()s + + 2 messages are considered equal if they point to the same + Message-Id and if they point to the same file names. If 2 + Messages derive from different queries where some files have + 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()) + if res: + res = cmp(list(self.get_filenames()), list(other.get_filenames())) + return res + def __del__(self): """Close and free the notmuch Message""" if self._msg is not None: