--- /dev/null
+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