[PATCH 2/4] Maildir synchronization
authorMichal Sojka <sojkam1@fel.cvut.cz>
Tue, 11 May 2010 12:14:19 +0000 (14:14 +0200)
committerW. Trevor King <wking@tremily.us>
Fri, 7 Nov 2014 17:37:02 +0000 (09:37 -0800)
26/1533d2ab35aa861c2207aa2e9ee3de80ffc78e [new file with mode: 0644]

diff --git a/26/1533d2ab35aa861c2207aa2e9ee3de80ffc78e b/26/1533d2ab35aa861c2207aa2e9ee3de80ffc78e
new file mode 100644 (file)
index 0000000..d4e651d
--- /dev/null
@@ -0,0 +1,442 @@
+Return-Path: <sojkam1@fel.cvut.cz>\r
+X-Original-To: notmuch@notmuchmail.org\r
+Delivered-To: notmuch@notmuchmail.org\r
+Received: from localhost (localhost [127.0.0.1])\r
+       by olra.theworths.org (Postfix) with ESMTP id D0B4B40CAE9\r
+       for <notmuch@notmuchmail.org>; Tue, 11 May 2010 05:15:01 -0700 (PDT)\r
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org\r
+X-Spam-Flag: NO\r
+X-Spam-Score: -1.9\r
+X-Spam-Level: \r
+X-Spam-Status: No, score=-1.9 tagged_above=-999 required=5\r
+       tests=[BAYES_00=-1.9] autolearn=ham\r
+Received: from olra.theworths.org ([127.0.0.1])\r
+       by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)\r
+       with ESMTP id 6RKDn6TjOyXo for <notmuch@notmuchmail.org>;\r
+       Tue, 11 May 2010 05:14:47 -0700 (PDT)\r
+Received: from max.feld.cvut.cz (max.feld.cvut.cz [147.32.192.36])\r
+       by olra.theworths.org (Postfix) with ESMTP id 87D2B4196F4\r
+       for <notmuch@notmuchmail.org>; Tue, 11 May 2010 05:14:34 -0700 (PDT)\r
+Received: from localhost (unknown [192.168.200.4])\r
+       by max.feld.cvut.cz (Postfix) with ESMTP id 8465819F33F4;\r
+       Tue, 11 May 2010 14:14:33 +0200 (CEST)\r
+X-Virus-Scanned: IMAP AMAVIS\r
+Received: from max.feld.cvut.cz ([192.168.200.1])\r
+       by localhost (styx.feld.cvut.cz [192.168.200.4]) (amavisd-new,\r
+       port 10044)\r
+       with ESMTP id mIligpFZ4x-E; Tue, 11 May 2010 14:14:31 +0200 (CEST)\r
+Received: from imap.feld.cvut.cz (imap.feld.cvut.cz [147.32.192.34])\r
+       by max.feld.cvut.cz (Postfix) with ESMTP id 84A8119F33F8;\r
+       Tue, 11 May 2010 14:14:31 +0200 (CEST)\r
+Received: from steelpick.2x.cz (k335-30.felk.cvut.cz [147.32.86.30])\r
+       (Authenticated sender: sojkam1)\r
+       by imap.feld.cvut.cz (Postfix) with ESMTPSA id 7522BFA003;\r
+       Tue, 11 May 2010 14:14:31 +0200 (CEST)\r
+Received: from wsh by steelpick.2x.cz with local (Exim 4.71)\r
+       (envelope-from <sojkam1@fel.cvut.cz>)\r
+       id 1OBoM3-0005st-6u; Tue, 11 May 2010 14:14:31 +0200\r
+From: Michal Sojka <sojkam1@fel.cvut.cz>\r
+To: notmuch@notmuchmail.org\r
+Subject: [PATCH 2/4] Maildir synchronization\r
+Date: Tue, 11 May 2010 14:14:19 +0200\r
+Message-Id: <1273580061-22580-3-git-send-email-sojkam1@fel.cvut.cz>\r
+X-Mailer: git-send-email 1.7.1\r
+In-Reply-To: <1273580061-22580-1-git-send-email-sojkam1@fel.cvut.cz>\r
+References: <1273580061-22580-1-git-send-email-sojkam1@fel.cvut.cz>\r
+X-BeenThere: notmuch@notmuchmail.org\r
+X-Mailman-Version: 2.1.13\r
+Precedence: list\r
+List-Id: "Use and development of the notmuch mail system."\r
+       <notmuch.notmuchmail.org>\r
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,\r
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>\r
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>\r
+List-Post: <mailto:notmuch@notmuchmail.org>\r
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>\r
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,\r
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>\r
+X-List-Received-Date: Tue, 11 May 2010 12:15:02 -0000\r
+\r
+This patch allows bi-directional synchronization between maildir\r
+flags and certain tags. The flag-to-tag mapping is defined by flag2tag\r
+array.\r
+\r
+The synchronization works this way:\r
+\r
+1) Whenever notmuch new is executed, the following happens:\r
+   o New messages are tagged with configured new_tags.\r
+   o For new or renamed messages with maildir info present in the file\r
+     name, the tags defined in flag2tag are either added or removed\r
+     depending on the flags from the file name.\r
+\r
+2) Whenever notmuch tag (or notmuch restore) is executed, a new set of\r
+   flags based on the tags is constructed for every message and a new\r
+   file name is prepared based on the old file name but with the new\r
+   flags. If the flags differs and the old message was in 'new'\r
+   directory then this is replaced with 'cur' in the new file name. If\r
+   the new and old file names differ, the file is renamed and notmuch\r
+   database is updated accordingly.\r
+\r
+   The rename happens before the database is updated. In case of crash\r
+   between rename and database update, the next run of notmuch new\r
+   brings the database in sync with the mail store again.\r
+\r
+There is currenlty one known issue: Viewing/storing of attachments of\r
+unread messages doesn't work. The reason is that when you view the\r
+message its unread tag is removed which leads to rename of the file,\r
+but Emacs still uses the original name to access the attachment.\r
+\r
+Workaround: close the message and open it again.\r
+---\r
+ lib/database.cc       |    7 ++\r
+ lib/message.cc        |  226 +++++++++++++++++++++++++++++++++++++++++++++++++\r
+ lib/notmuch-private.h |    4 +\r
+ lib/notmuch.h         |    7 ++\r
+ notmuch-new.c         |    3 +-\r
+ 5 files changed, 246 insertions(+), 1 deletions(-)\r
+\r
+diff --git a/lib/database.cc b/lib/database.cc\r
+index 694b7ec..908bbaa 100644\r
+--- a/lib/database.cc\r
++++ b/lib/database.cc\r
+@@ -1585,6 +1585,13 @@ notmuch_database_add_message (notmuch_database_t *notmuch,\r
\r
+       _notmuch_message_add_filename (message, filename);\r
\r
++      /* This is a new message or it has a new filename and as such,\r
++       * its tags in database either do not exists or might be out\r
++       * of date. We assign the tags later in notmuch new, but until\r
++       * then we should not synchronize the tags back to the maildir\r
++       * flags (if notmuch is configured to do so). */\r
++      notmuch_message_set_flag(message, NOTMUCH_MESSAGE_FLAG_TAGS_INVALID, TRUE);\r
++\r
+       /* Is this a newly created message object? */\r
+       if (private_status == NOTMUCH_PRIVATE_STATUS_NO_DOCUMENT_FOUND) {\r
+           _notmuch_message_add_term (message, "type", "mail");\r
+diff --git a/lib/message.cc b/lib/message.cc\r
+index 4b2f98f..0de3f01 100644\r
+--- a/lib/message.cc\r
++++ b/lib/message.cc\r
+@@ -43,6 +43,24 @@ struct _notmuch_message {\r
+     Xapian::Document doc;\r
+ };\r
\r
++#define ARRAY_SIZE(arr) (sizeof (arr) / sizeof (arr[0]))\r
++\r
++struct maildir_flag_tag {\r
++    char flag;\r
++    const char *tag;\r
++    bool inverse;\r
++};\r
++\r
++/* ASCII ordered table of Maildir flags and associated tags */\r
++struct maildir_flag_tag flag2tag[] = {\r
++    { 'D', "draft",   false},\r
++    { 'F', "flagged", false},\r
++    { 'P', "passed",  false},\r
++    { 'R', "replied", false},\r
++    { 'S', "unread",  true },\r
++    { 'T', "delete",  false},\r
++};\r
++\r
+ /* We end up having to call the destructor explicitly because we had\r
+  * to use "placement new" in order to initialize C++ objects within a\r
+  * block that we allocated with talloc. So C++ is making talloc\r
+@@ -568,15 +586,29 @@ _notmuch_message_set_date (notmuch_message_t *message,\r
+                           Xapian::sortable_serialise (time_value));\r
+ }\r
\r
++static notmuch_private_status_t\r
++_notmuch_message_tags_to_maildir (notmuch_message_t *message);\r
++\r
+ /* Synchronize changes made to message->doc out into the database. */\r
+ void\r
+ _notmuch_message_sync (notmuch_message_t *message)\r
+ {\r
+     Xapian::WritableDatabase *db;\r
++    notmuch_private_status_t status;\r
\r
+     if (message->notmuch->mode == NOTMUCH_DATABASE_MODE_READ_ONLY)\r
+       return;\r
\r
++    if (// todo_sync_enabled &&\r
++      !notmuch_message_get_flag(message, NOTMUCH_MESSAGE_FLAG_TAGS_INVALID)) {\r
++      status = _notmuch_message_tags_to_maildir (message);\r
++      if (status != NOTMUCH_PRIVATE_STATUS_SUCCESS) {\r
++          fprintf (stderr, "Error: Cannot sync tags to maildir (%s)\n",\r
++                   notmuch_status_to_string ((notmuch_status_t)status));\r
++          /* Exit to avoid unsynchronized mailstore. */\r
++          exit(1);\r
++      }\r
++    }\r
+     db = static_cast <Xapian::WritableDatabase *> (message->notmuch->xapian_db);\r
+     db->replace_document (message->doc_id, message->doc);\r
+ }\r
+@@ -688,6 +720,44 @@ _notmuch_message_remove_term (notmuch_message_t *message,\r
+     return NOTMUCH_PRIVATE_STATUS_SUCCESS;\r
+ }\r
\r
++/* Change the message filename stored in the database.\r
++ *\r
++ * This change will not be reflected in the database until the next\r
++ * call to _notmuch_message_sync.\r
++ */\r
++notmuch_private_status_t\r
++_notmuch_message_rename (notmuch_message_t *message,\r
++                       const char *new_filename)\r
++{\r
++    void *local = talloc_new (message);\r
++    char *direntry;\r
++    Xapian::PostingIterator i, end;\r
++    Xapian::Document document;\r
++    notmuch_private_status_t pstatus;\r
++    notmuch_status_t status;\r
++    const char *old_filename;\r
++\r
++    old_filename = notmuch_message_get_filename(message);\r
++    old_filename = talloc_reference(local, old_filename);\r
++    if (unlikely(!old_filename))\r
++      return NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;\r
++\r
++    status = _notmuch_message_add_filename (message, new_filename);\r
++    if (status)\r
++      return (notmuch_private_status_t)status;\r
++\r
++    status = _notmuch_database_filename_to_direntry (local, message->notmuch,\r
++                                                   old_filename, &direntry);\r
++    if (status)\r
++      return (notmuch_private_status_t)status;\r
++\r
++    pstatus = _notmuch_message_remove_term (message, "file-direntry", direntry);\r
++\r
++    talloc_free (local);\r
++\r
++    return pstatus;\r
++}\r
++\r
+ notmuch_status_t\r
+ notmuch_message_add_tag (notmuch_message_t *message, const char *tag)\r
+ {\r
+@@ -745,6 +815,162 @@ notmuch_message_remove_tag (notmuch_message_t *message, const char *tag)\r
+ }\r
\r
+ notmuch_status_t\r
++notmuch_message_maildir_to_tags (notmuch_message_t *message, const char *filename)\r
++{\r
++    const char *flags, *p;\r
++    char f;\r
++    bool valid, unread;\r
++    unsigned i;\r
++    notmuch_status_t status;\r
++\r
++    flags = strstr (filename, ":2,");\r
++    if (!flags)\r
++      return NOTMUCH_STATUS_FILE_NOT_EMAIL;\r
++    flags += 3;\r
++\r
++    /*  Check that the letters are valid Maildir flags */\r
++    f = 0;\r
++    valid = true;\r
++    for (p=flags; valid && *p; p++) {\r
++      switch (*p) {\r
++      case 'P':\r
++      case 'R':\r
++      case 'S':\r
++      case 'T':\r
++      case 'D':\r
++      case 'F':\r
++          if (*p > f) f=*p;\r
++          else valid = false;\r
++      break;\r
++      default:\r
++          valid = false;\r
++      }\r
++    }\r
++    if (!valid) {\r
++      fprintf (stderr, "Warning: Invalid maildir flags in filename %s\n", filename);\r
++      return NOTMUCH_STATUS_FILE_NOT_EMAIL;\r
++    }\r
++\r
++    status = notmuch_message_freeze (message);\r
++    if (status)\r
++      return status;\r
++    unread = true;\r
++    for (i = 0; i < ARRAY_SIZE(flag2tag); i++) {\r
++      if ((strchr (flags, flag2tag[i].flag) != NULL) ^ flag2tag[i].inverse) {\r
++          status = notmuch_message_add_tag (message, flag2tag[i].tag);\r
++      } else {\r
++          status = notmuch_message_remove_tag (message, flag2tag[i].tag);\r
++      }\r
++      if (status)\r
++          return status;\r
++    }\r
++    status = notmuch_message_thaw (message);\r
++\r
++    /* From now on, we can synchronize the tags from the database to\r
++     * the mailstore. */\r
++    notmuch_message_set_flag (message, NOTMUCH_MESSAGE_FLAG_TAGS_INVALID, FALSE);\r
++    return status;\r
++}\r
++\r
++static void\r
++maildir_get_new_flags(notmuch_message_t *message, char *flags)\r
++{\r
++    notmuch_tags_t *tags;\r
++    const char *tag;\r
++    unsigned i;\r
++    char *p;\r
++\r
++    for (i = 0; i < ARRAY_SIZE(flag2tag); i++)\r
++      flags[i] = flag2tag[i].inverse ? flag2tag[i].flag : '\0';\r
++\r
++    for (tags = notmuch_message_get_tags (message);\r
++       notmuch_tags_valid (tags);\r
++       notmuch_tags_move_to_next (tags))\r
++    {\r
++      tag = notmuch_tags_get (tags);\r
++      for (i = 0; i < ARRAY_SIZE(flag2tag); i++) {\r
++          if (strcmp(tag, flag2tag[i].tag) == 0)\r
++              flags[i] = flag2tag[i].inverse ? '\0' : flag2tag[i].flag;\r
++      }\r
++    }\r
++\r
++    p = flags;\r
++    for (i = 0; i < ARRAY_SIZE(flag2tag); i++) {\r
++      if (flags[i])\r
++          *p++ = flags[i];\r
++    }\r
++    *p = '\0';\r
++}\r
++\r
++static char *\r
++maildir_get_subdir (char *filename)\r
++{\r
++    char *p, *subdir = NULL;\r
++\r
++    p = filename + strlen (filename) - 1;\r
++    while (p > filename + 3 && *p != '/')\r
++      p--;\r
++    if (*p == '/') {\r
++      subdir = p - 3;\r
++      if (subdir > filename && *(subdir - 1) != '/')\r
++          subdir = NULL;\r
++    }\r
++    return subdir;\r
++}\r
++\r
++/* Rename the message file so that maildir flags corresponds to the\r
++ * tags and, if aplicable, move the message from new/ to cur/. */\r
++static notmuch_private_status_t\r
++_notmuch_message_tags_to_maildir (notmuch_message_t *message)\r
++{\r
++    char flags[ARRAY_SIZE(flag2tag)+1];\r
++    const char *filename, *p;\r
++    char *filename_new, *subdir = NULL;\r
++    int ret;\r
++\r
++    maildir_get_new_flags (message, flags);\r
++\r
++    filename = notmuch_message_get_filename (message);\r
++    /* TODO: Iterate over all file names. */\r
++    p = strstr(filename, ":2,");\r
++    if ((p && strcmp (p+3, flags) == 0) ||\r
++      (!p && flags[0] == '\0')) {\r
++      // Return if flags are not to be changed - this suppresses\r
++      // moving the message from new/ to cur/ during initial\r
++      // tagging.\r
++      return NOTMUCH_PRIVATE_STATUS_SUCCESS;\r
++    }\r
++    if (!p)\r
++      p = filename + strlen(filename);\r
++\r
++    filename_new = (char*)talloc_size(message, (p-filename) + 3 + sizeof(flags));\r
++    if (unlikely (filename_new == NULL))\r
++      return NOTMUCH_PRIVATE_STATUS_OUT_OF_MEMORY;\r
++    memcpy(filename_new, filename, p-filename);\r
++    filename_new[p-filename] = '\0';\r
++\r
++    /* If message is in new/ move it under cur/. */\r
++    subdir = maildir_get_subdir (filename_new);\r
++    if (subdir && memcmp (subdir, "new/", 4) == 0)\r
++      memcpy (subdir, "cur/", 4);\r
++\r
++    strcpy (filename_new+(p-filename), ":2,");\r
++    strcpy (filename_new+(p-filename)+3, flags);\r
++\r
++    if (strcmp (filename, filename_new) != 0) {\r
++      ret = rename (filename, filename_new);\r
++      if (ret == -1) {\r
++          perror (talloc_asprintf (message, "rename of %s to %s failed",\r
++                                   filename, filename_new));\r
++          exit (1);\r
++      }\r
++      return _notmuch_message_rename (message, filename_new);\r
++      /* _notmuch_message_sync is our caller. Do not call it here. */\r
++    }\r
++    return NOTMUCH_PRIVATE_STATUS_SUCCESS;\r
++}\r
++\r
++notmuch_status_t\r
+ notmuch_message_remove_all_tags (notmuch_message_t *message)\r
+ {\r
+     notmuch_private_status_t private_status;\r
+diff --git a/lib/notmuch-private.h b/lib/notmuch-private.h\r
+index 3768d6f..ba11498 100644\r
+--- a/lib/notmuch-private.h\r
++++ b/lib/notmuch-private.h\r
+@@ -246,6 +246,10 @@ notmuch_status_t\r
+ _notmuch_message_add_filename (notmuch_message_t *message,\r
+                              const char *filename);\r
\r
++notmuch_private_status_t\r
++_notmuch_message_rename (notmuch_message_t *message,\r
++                       const char *new_filename);\r
++\r
+ void\r
+ _notmuch_message_ensure_thread_id (notmuch_message_t *message);\r
\r
+diff --git a/lib/notmuch.h b/lib/notmuch.h\r
+index 0ba1416..408d633 100644\r
+--- a/lib/notmuch.h\r
++++ b/lib/notmuch.h\r
+@@ -770,6 +770,7 @@ notmuch_message_get_filename (notmuch_message_t *message);\r
+ /* Message flags */\r
+ typedef enum _notmuch_message_flag {\r
+     NOTMUCH_MESSAGE_FLAG_MATCH,\r
++    NOTMUCH_MESSAGE_FLAG_TAGS_INVALID,\r
+ } notmuch_message_flag_t;\r
\r
+ /* Get a value of a flag for the email corresponding to 'message'. */\r
+@@ -886,6 +887,12 @@ notmuch_message_remove_tag (notmuch_message_t *message, const char *tag);\r
+ notmuch_status_t\r
+ notmuch_message_remove_all_tags (notmuch_message_t *message);\r
\r
++/* Add or remove tags based on the maildir flags in the file name.\r
++ */\r
++notmuch_status_t\r
++notmuch_message_maildir_to_tags (notmuch_message_t *message,\r
++                               const char *filename);\r
++\r
+ /* Freeze the current state of 'message' within the database.\r
+  *\r
+  * This means that changes to the message state, (via\r
+diff --git a/notmuch-new.c b/notmuch-new.c\r
+index 8818728..ed3f944 100644\r
+--- a/notmuch-new.c\r
++++ b/notmuch-new.c\r
+@@ -410,10 +410,11 @@ add_files_recursive (notmuch_database_t *notmuch,\r
+           state->added_messages++;\r
+           for (tag=state->new_tags; *tag != NULL; tag++)\r
+               notmuch_message_add_tag (message, *tag);\r
++          notmuch_message_maildir_to_tags (message, next);\r
+           break;\r
+       /* Non-fatal issues (go on to next file) */\r
+       case NOTMUCH_STATUS_DUPLICATE_MESSAGE_ID:\r
+-          /* Stay silent on this one. */\r
++          notmuch_message_maildir_to_tags (message, next);\r
+           break;\r
+       case NOTMUCH_STATUS_FILE_NOT_EMAIL:\r
+           fprintf (stderr, "Note: Ignoring non-mail file: %s\n",\r
+-- \r
+1.7.1\r
+\r