]> git.notmuchmail.org Git - notmuch/blobdiff - bindings/python/notmuch/message.py
Implement Message.tags_to_maildir_flags
[notmuch] / bindings / python / notmuch / message.py
index 613cc4abc2be7f89c1501cbabaf008288e50204e..8944af42b33323be7142a251589834989d354824 100644 (file)
@@ -19,10 +19,11 @@ Copyright 2010 Sebastian Spaeth <Sebastian@SSpaeth.de>'
 """
 
         
 """
 
         
-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
 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
 import sys
 import email
 import types
@@ -36,50 +37,59 @@ class Messages(object):
 
     This object provides an iterator over a list of notmuch messages
     (Technically, it provides a wrapper for the underlying
 
     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. Also
-    note, that any function that uses iteration will also
-    exhaust the messages. So both::
-
-      for msg in msgs: print msg 
-
-    as well as::
-
-       number_of_msgs = len(msgs)
-
-    will "exhaust" the Messages. If you need to re-iterate over a list of
-    messages you will need to retrieve a new :class:`Messages` object.
-
-    Things are not as bad as it seems though, 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::
+    *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
 
       db   = Database()
       msgs = Query(db,'').search_messages() #get a Messages() object
-      msglist = []
-      for m in msgs:
-         msglist.append(m)
-
-      # 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.
+      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. 
       
       # 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())
 
       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_tags_get
+    #notmuch_messages_get
     _get = nmlib.notmuch_messages_get
     _get.restype = c_void_p
 
     _get = nmlib.notmuch_messages_get
     _get.restype = c_void_p
 
@@ -147,33 +157,12 @@ class Messages(object):
         nmlib.notmuch_messages_move_to_next(self._msgs)
         return msg
 
         nmlib.notmuch_messages_move_to_next(self._msgs)
         return msg
 
-    def __len__(self):
-        """len(:class:`Messages`) returns the number of contained messages
-
-        .. note:: As this iterates over the messages, we will not be able to 
-               iterate over them again! So this will fail::
-
-                 #THIS FAILS
-                 msgs = Database().create_query('').search_message()
-                 if len(msgs) > 0:              #this 'exhausts' msgs
-                     # next line raises NotmuchError(STATUS.NOT_INITIALIZED)!!!
-                     for msg in msgs: print msg
-
-               Most of the time, using the
-               :meth:`Query.count_messages` is therefore more
-               appropriate (and much faster). While not guaranteeing
-               that it will return the exact same number than len(),
-               in my tests it effectively always did so.
+    def __nonzero__(self):
         """
         """
-        if self._msgs is None:
-            raise NotmuchError(STATUS.NOT_INITIALIZED)
-
-        i=0
-        while nmlib.notmuch_messages_valid(self._msgs):
-            nmlib.notmuch_messages_move_to_next(self._msgs)
-            i += 1
-        self._msgs = None
-        return i
+        :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
 
     def __del__(self):
         """Close and free the notmuch Messages"""
 
     def __del__(self):
         """Close and free the notmuch Messages"""
@@ -238,12 +227,21 @@ class Message(object):
     """Represents a single Email message
 
     Technically, this wraps the underlying *notmuch_message_t* structure.
     """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)"""
     _get_filename = nmlib.notmuch_message_get_filename
     _get_filename.restype = c_char_p 
 
     """
 
     """notmuch_message_get_filename (notmuch_message_t *message)"""
     _get_filename = nmlib.notmuch_message_get_filename
     _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
+
     """notmuch_message_get_flag"""
     _get_flag = nmlib.notmuch_message_get_flag
     _get_flag.restype = c_uint
     """notmuch_message_get_flag"""
     _get_flag = nmlib.notmuch_message_get_flag
     _get_flag.restype = c_uint
@@ -270,6 +268,14 @@ class Message(object):
     _get_header = nmlib.notmuch_message_get_header
     _get_header.restype = c_char_p
 
     _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'])
 
     #Constants: Flags that can be set/get with set_flag
     FLAG = Enum(['MATCH'])
 
@@ -400,6 +406,19 @@ class Message(object):
             raise NotmuchError(STATUS.NOT_INITIALIZED)
         return Message._get_filename(self._msg)
 
             raise NotmuchError(STATUS.NOT_INITIALIZED)
         return Message._get_filename(self._msg)
 
+    def get_filenames(self):
+        """Get all filenames for the email corresponding to 'message'
+
+        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)
+        
+        files_p = Message._get_filenames(self._msg)
+
+        return Filenames(files_p, self).as_generator()
+
     def get_flag(self, flag):
         """Checks whether a specific flag is set for this message
 
     def get_flag(self, flag):
         """Checks whether a specific flag is set for this message
 
@@ -450,13 +469,22 @@ class Message(object):
             raise NotmuchError(STATUS.NULL_POINTER)
         return Tags(tags_p, self)
 
             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.
         """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:
         :returns: STATUS.SUCCESS if the tag was successfully added.
                   Raises an exception otherwise.
         :exception: :exc:`NotmuchError`. They have the following meaning:
@@ -477,19 +505,29 @@ class Message(object):
 
         status = nmlib.notmuch_message_add_tag (self._msg, tag)
 
 
         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.
         """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.
         :returns: STATUS.SUCCESS if the tag was successfully removed or if 
                   the message had no such tag.
                   Raises an exception otherwise.
@@ -510,19 +548,30 @@ class Message(object):
             raise NotmuchError(STATUS.NOT_INITIALIZED)
 
         status = nmlib.notmuch_message_remove_tag(self._msg, tag)
             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 sync_maildir_flags:
+            self.tags_to_maildir_flags()
+        return STATUS.SUCCESS
 
 
-        if STATUS.SUCCESS == status:
-            # return on success
-            return status
 
 
-        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.
 
         """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:
         :returns: STATUS.SUCCESS if the tags were successfully removed.
                   Raises an exception otherwise.
         :exception: :exc:`NotmuchError`. They have the following meaning:
@@ -538,11 +587,13 @@ class Message(object):
  
         status = nmlib.notmuch_message_remove_all_tags(self._msg)
 
  
         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
 
     def freeze(self):
         """Freezes the current state of 'message' within the database
@@ -560,10 +611,11 @@ class Message(object):
         have a given set of tags might look like this::
 
           msg.freeze()
         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:
           for tag in new_tags:
-              msg.add_tag(tag)
+              msg.add_tag(tag, False)
           msg.thaw()
           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
 
         With freeze/thaw used like this, the message in the database is
         guaranteed to have either the full set of original tag values, or
@@ -635,6 +687,61 @@ class Message(object):
         """(Not implemented)"""
         return self.get_flag(Message.FLAG.MATCH)
 
         """(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 = {}
     def __str__(self):
         """A message() is represented by a 1-line summary"""
         msg = {}
@@ -642,8 +749,8 @@ class Message(object):
         msg['tags'] = str(self.get_tags())
         msg['date'] = date.fromtimestamp(self.get_date())
         replies = self.get_replies()
         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):
 
 
     def get_message_parts(self):
@@ -748,7 +855,7 @@ class Message(object):
         output += "\n\fbody{"
 
         parts = format["body"]
         output += "\n\fbody{"
 
         parts = format["body"]
-        parts.sort(key=lambda(p): p["id"])
+        parts.sort(key=lambda x: x['id'])
         for p in parts:
             if not p.has_key("filename"):
                 output += "\n\fpart{ "
         for p in parts:
             if not p.has_key("filename"):
                 output += "\n\fpart{ "
@@ -771,6 +878,27 @@ class Message(object):
 
         return output
 
 
         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:
     def __del__(self):
         """Close and free the notmuch Message"""
         if self._msg is not None: