[PATCH v3] emacs: postpone/resume support
[notmuch-archives.git] / 73 / b3322d06a86912fe3a75f3ac4b21bd527cd0d8
diff --git a/73/b3322d06a86912fe3a75f3ac4b21bd527cd0d8 b/73/b3322d06a86912fe3a75f3ac4b21bd527cd0d8
new file mode 100644 (file)
index 0000000..100309c
--- /dev/null
@@ -0,0 +1,413 @@
+Return-Path: <markwalters1009@gmail.com>\r
+X-Original-To: notmuch@notmuchmail.org\r
+Delivered-To: notmuch@notmuchmail.org\r
+Received: from localhost (localhost [127.0.0.1])\r
+ by arlo.cworth.org (Postfix) with ESMTP id 8E3B36DE0243\r
+ for <notmuch@notmuchmail.org>; Fri,  3 Jun 2016 17:44:00 -0700 (PDT)\r
+X-Virus-Scanned: Debian amavisd-new at cworth.org\r
+X-Spam-Flag: NO\r
+X-Spam-Score: 0.171\r
+X-Spam-Level: \r
+X-Spam-Status: No, score=0.171 tagged_above=-999 required=5 tests=[AWL=-0.259,\r
+  DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1,\r
+ FREEMAIL_ENVFROM_END_DIGIT=0.25, FREEMAIL_FROM=0.001, FREEMAIL_REPLY=1,\r
+ RCVD_IN_DNSWL_LOW=-0.7, RCVD_IN_MSPIKE_H3=-0.01, RCVD_IN_MSPIKE_WL=-0.01,\r
+ SPF_PASS=-0.001] autolearn=disabled\r
+Received: from arlo.cworth.org ([127.0.0.1])\r
+ by localhost (arlo.cworth.org [127.0.0.1]) (amavisd-new, port 10024)\r
+ with ESMTP id WBziaDPHjiNj for <notmuch@notmuchmail.org>;\r
+ Fri,  3 Jun 2016 17:43:52 -0700 (PDT)\r
+Received: from mail-wm0-f66.google.com (mail-wm0-f66.google.com\r
+ [74.125.82.66]) by arlo.cworth.org (Postfix) with ESMTPS id 0509D6DE0159 for\r
+ <notmuch@notmuchmail.org>; Fri,  3 Jun 2016 17:43:52 -0700 (PDT)\r
+Received: by mail-wm0-f66.google.com with SMTP id e3so2725829wme.2\r
+ for <notmuch@notmuchmail.org>; Fri, 03 Jun 2016 17:43:51 -0700 (PDT)\r
+DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20120113;\r
+ h=from:to:cc:subject:date:message-id;\r
+ bh=fOHO2wjwDJ9yaRpIdpA4cNWj9fDAyYMhINLHTSMJwEg=;\r
+ b=SCRGHj4lcmaRtupkl9iBTvM0IsVms5RyuoA/fnY9Bp4rQWqgNdUpFT7vevFNk9mrQn\r
+ zxTiB+b+bhIwZrCwvKr+6wHLprlWcBorN7dIobwYyuXhC0ccPWEXSSX8egfhg0Rnc9dx\r
+ yVPNW3AZSOGJs1+1CWvjoNkkktl89UpLUQyjHRISTUR0WTOv9OdWGIk98XTGgnZikKWO\r
+ sQAqrvIGDGpBFAbyPqizTznE48lmGIk4DmqKxabbqgQ7dLH0wRKgRAr5qaifpOwiRK+l\r
+ DrSY8i0bHt5nVoJQ6uuRkrEgA2/C8PFcRjum4j1hymycnYBbrCDJPKXCzy+D/W+6aC6v\r
+ J4+A==\r
+X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;\r
+ d=1e100.net; s=20130820;\r
+ h=x-gm-message-state:from:to:cc:subject:date:message-id;\r
+ bh=fOHO2wjwDJ9yaRpIdpA4cNWj9fDAyYMhINLHTSMJwEg=;\r
+ b=Gb/jJlXV7Ez9RkFTse1jWP149Oy0e0XB18VfK796TYj+hbTTfVrTnF3OQpuLxNo2xc\r
+ P6Kg+UBJ4MMc91HcmxWYlyY3wCpvFmuni9z/wjbpUDrAO0Dgjuy7kw48pOU6IBZMDRdG\r
+ FgrAOBbhnNjcLhCz10OqyA1BxuI48xF6UO9OHdVYmDK2cxUpdLsnUYjIOXM/shcS+d7p\r
+ sNOJ7SFF9hdSX9o0vghEqkgNVClX8j10LvcpKwNfUccNITf/WibusyoP9e5HfKu6khia\r
+ 3Antyx2Il2ZHOkF5B7SpByeYS12nUBp593IRzU7xrWa0/NUCL233UoHXaJrXYP/0w/RX\r
+ w4bw==\r
+X-Gm-Message-State:\r
+ ALyK8tIs1ZWPEs3vI3wrB+Qerobi+ng3KAQIs9CPqHAe35+ZqcKWPJd3D5KRdQ9LvSCiYQ==\r
+X-Received: by 10.194.184.169 with SMTP id ev9mr5528316wjc.27.1465001030025;\r
+ Fri, 03 Jun 2016 17:43:50 -0700 (PDT)\r
+Received: from localhost (5751dfa2.skybroadband.com. [87.81.223.162])\r
+ by smtp.gmail.com with ESMTPSA id db6sm8105538wjb.2.2016.06.03.17.43.48\r
+ (version=TLS1_2 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128/128);\r
+ Fri, 03 Jun 2016 17:43:49 -0700 (PDT)\r
+From: Mark Walters <markwalters1009@gmail.com>\r
+To: notmuch@notmuchmail.org\r
+Subject: [PATCH v3] emacs:  postpone/resume support\r
+Date: Sat,  4 Jun 2016 01:43:46 +0100\r
+Message-Id: <1465001026-29392-1-git-send-email-markwalters1009@gmail.com>\r
+X-Mailer: git-send-email 2.1.4\r
+X-BeenThere: notmuch@notmuchmail.org\r
+X-Mailman-Version: 2.1.20\r
+Precedence: list\r
+List-Id: "Use and development of the notmuch mail system."\r
+ <notmuch.notmuchmail.org>\r
+List-Unsubscribe: <https://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: <https://notmuchmail.org/mailman/listinfo/notmuch>,\r
+ <mailto:notmuch-request@notmuchmail.org?subject=subscribe>\r
+X-List-Received-Date: Sat, 04 Jun 2016 00:44:00 -0000\r
+\r
+This provides preliminary support for postponing and resuming in the\r
+emacs frontend. On postponing it uses notmuch insert to put the\r
+message in the notmuch database; resume gets the raw file from notmuch\r
+and using the emacs function mime-to-mml reconstructs the message\r
+(including attachments).\r
+\r
+Current bindings are C-x C-s to save a draft, C-c C-p to postpone a\r
+draft (save and exit compose buffer), and e to resume a draft from\r
+show or tree mode.\r
+\r
+Previous drafts get tagged deleted on subsequent saves, or on the\r
+message being sent.\r
+\r
+Each draft gets its own message-id, and we use the namespace\r
+draft-.... for draft message ids (so, at least for most people, drafts\r
+are easily distinguisable).\r
+---\r
+\r
+Sorry to be rather spamming the list. This is another version of the\r
+postpone/resume series. This replaces the third patch in the series at\r
+id:1464976195-23134-1-git-send-email-markwalters1009@gmail.com (so\r
+should be applied on top of the first two).\r
+\r
+There are three main changes --\r
+\r
+1) It seems that editing an already sent message does work -- as it is\r
+not heavily tested we warn before doing it. But now when you send the\r
+new version it does not tag the old version as deleted (we only tag\r
+drafts deleted).\r
+\r
+2) We quote secure mml tags before saving. This avoids problems with\r
+signing the wrong message, stale signatures, and using the wrong keys\r
+for encryption. Note the draft message will be stored in the mail\r
+store unencrypted.\r
+\r
+3) You can choose to quote more mml tags than just secure; there is a\r
+custom variable notmuch-message-quoted-tags under notmuch-send which\r
+should be a list of tags to quote. If you set it to '("secure" "part")\r
+then attachments won't be saved with the draft. This may be desired in\r
+some cases (but may break things like postponing rfc822 forwarded\r
+messages).  Anyway the option is there for anyone who wants to test!\r
+\r
+Best wishes\r
+\r
+Mark\r
+\r
+\r
+\r
+\r
+emacs/notmuch-message.el | 190 +++++++++++++++++++++++++++++++++++++++++++++++\r
+ emacs/notmuch-mua.el     |   4 +\r
+ emacs/notmuch-show.el    |   9 +++\r
+ emacs/notmuch-tree.el    |   1 +\r
+ 4 files changed, 204 insertions(+)\r
+\r
+diff --git a/emacs/notmuch-message.el b/emacs/notmuch-message.el\r
+index d437b85..6a137b5 100644\r
+--- a/emacs/notmuch-message.el\r
++++ b/emacs/notmuch-message.el\r
+@@ -25,6 +25,8 @@\r
+ (require 'notmuch-tag)\r
+ (require 'notmuch-mua)\r
\r
++(declare-function notmuch-show-get-message-id "notmuch-show" (&optional bare))\r
++\r
+ (defcustom notmuch-message-replied-tags '("+replied")\r
+   "List of tag changes to apply to a message when it has been replied to.\r
\r
+@@ -38,6 +40,49 @@ the \"inbox\" and \"todo\" tags, you would set:\r
+   :type '(repeat string)\r
+   :group 'notmuch-send)\r
\r
++(defcustom notmuch-message-draft-tags '("+draft")\r
++  "List of tags changes to apply to a draft message when it is saved in the database.\r
++\r
++Tags starting with \"+\" (or not starting with either \"+\" or\r
++\"-\") in the list will be added, and tags starting with \"-\"\r
++will be removed from the message being stored.\r
++\r
++For example, if you wanted to give the message a \"draft\" tag\r
++but not the (normally added by default) \"inbox\" tag, you would\r
++set:\r
++    (\"+draft\" \"-inbox\")"\r
++  :type '(repeat string)\r
++  :group 'notmuch-send)\r
++\r
++(defcustom notmuch-message-draft-folder "drafts"\r
++  "Folder to save draft messages in.\r
++\r
++This should be specified relative to the root of the notmuch\r
++database. It will be created if necessary."\r
++  :type 'string\r
++  :group 'notmuch-send)\r
++\r
++(defcustom notmuch-message-quoted-tags '("secure")\r
++  "Mml tags to quote.\r
++\r
++This should be a list of mml tags to quote before saving. It is\r
++recommended that the list includes \"secure\".\r
++\r
++If you include \"part\" then attachments will not be saved with\r
++the draft -- if not then they will be saved with the draft. The\r
++former means the attachments may not still exist when you resume\r
++the message, the latter means that the attachments as they were\r
++when you postponed will be sent with the resumed message.\r
++\r
++Note you may get strange results if you change this between\r
++postponing and resuming a message."\r
++  :type '(repeat string)\r
++  :group 'notmuch-send)\r
++\r
++(defvar notmuch-message-draft-id nil\r
++  "Message-id of the most recent saved draft of this message")\r
++(make-variable-buffer-local 'notmuch-message-draft-id)\r
++\r
+ (defun notmuch-message-mark-replied ()\r
+   ;; get the in-reply-to header and parse it for the message id.\r
+   (let ((rep (mail-header-parse-addresses (message-field-value "In-Reply-To"))))\r
+@@ -45,7 +90,152 @@ the \"inbox\" and \"todo\" tags, you would set:\r
+       (notmuch-tag (notmuch-id-to-query (car (car rep)))\r
+              (notmuch-tag-change-list notmuch-message-replied-tags)))))\r
\r
++(defun notmuch-message-mark-draft-deleted ()\r
++  "Tag the last saved draft deleted.\r
++\r
++Used when a new version is saved, or the message is sent."\r
++  (when notmuch-message-draft-id\r
++    (notmuch-tag notmuch-message-draft-id '("+deleted"))))\r
++\r
++(defun notmuch-message-quote-some-mml ()\r
++  "Quote the mml tags in `notmuch-message-quoted-tags`."\r
++  ;; This is copied from mml-quote-region but only quotes the\r
++  ;; specified tags.\r
++  (when notmuch-message-quoted-tags\r
++    (save-excursion\r
++      (let ((re (concat "<#!*/?\\("\r
++                      (mapconcat 'identity notmuch-message-quoted-tags "\\|")\r
++                      "\\)")))\r
++      (message-goto-body)\r
++      (while (re-search-forward re nil t)\r
++        ;; Insert ! after the #.\r
++        (goto-char (+ (match-beginning 0) 2))\r
++        (insert "!"))))))\r
++\r
++(defun notmuch-message-unquote-some-mml ()\r
++  "Unquote the mml tags in `notmuch-message-quoted-tags`."\r
++  (when notmuch-message-quoted-tags\r
++    (save-excursion\r
++      (let ((re (concat "<#!+/?\\("\r
++                      (mapconcat 'identity notmuch-message-quoted-tags "\\|")\r
++                      "\\)")))\r
++      (message-goto-body)\r
++      (while (re-search-forward re nil t)\r
++        ;; Remove one ! from after the #.\r
++        (goto-char (+ (match-beginning 0) 2))\r
++        (delete-char 1))))))\r
++\r
++(defun notmuch-message-save-draft ()\r
++  "Save the current draft message in the notmuch database.\r
++\r
++This saves the current message in the database with tags\r
++`notmuch-message-draft-tags` (in addition to any default tags\r
++applied to newly inserted messages)."\r
++  (interactive)\r
++\r
++  ;; This is based on message-do-fcc but modified for our needs.\r
++  (let ((case-fold-search t)\r
++      (buf (current-buffer))\r
++      (mml-externalize-attachments nil)\r
++      ;; We generate a message id now as we will need it later. Note\r
++      ;; message-make-message-id gives the id inside a "<" ">" pair,\r
++      ;; but notmuch doesn't want that form, so remove them.\r
++      (id (concat "draft-" (substring (message-make-message-id) 1 -1))))\r
++    (with-current-buffer (get-buffer-create " *message temp*")\r
++      (erase-buffer)\r
++      (insert-buffer-substring buf)\r
++      ;; We insert a Date header and a Message-ID header, the former\r
++      ;; so that it is easier to search for the message, and the\r
++      ;; latter so we have a way of accessing the saved message (for\r
++      ;; example to delete it at a later time). We check that the\r
++      ;; user has these in `message-deletable-headers` (the default)\r
++      ;; as otherwise they are doing something strange and we\r
++      ;; shouldn't interfere. Note, since we are doing this in a new\r
++      ;; buffer we don't change the version in the compose buffer.\r
++      (if (member 'Message-ID message-deletable-headers)\r
++        (progn\r
++          (message-remove-header "Message-ID")\r
++          (message-add-header (concat "Message-ID: <" id ">")))\r
++      (message "You have customized emacs so Message-ID is not a deletable header, so not changing it")\r
++      (setq id nil))\r
++      (if (member 'Date message-deletable-headers)\r
++        (progn\r
++          (message-remove-header "Date")\r
++          (message-add-header (concat "Date: " (message-make-date))))\r
++      (message "You have customized emacs so Date is not a deletable header, so not changing it"))\r
++      (notmuch-message-quote-some-mml)\r
++      ;; Back to following message-do-fcc\r
++      (message-encode-message-body)\r
++      (save-restriction\r
++      (message-narrow-to-headers)\r
++      (let ((mail-parse-charset message-default-charset)\r
++            (rfc2047-header-encoding-alist\r
++             (cons '("Newsgroups" . default)\r
++                   rfc2047-header-encoding-alist)))\r
++        (mail-encode-encoded-word-buffer)))\r
++      (goto-char (point-min))\r
++      (when (re-search-forward\r
++           (concat "^" (regexp-quote mail-header-separator) "$")\r
++           nil t)\r
++      (replace-match "" t t ))\r
++\r
++      (apply 'notmuch-call-notmuch-process :stdin-string (buffer-string)\r
++           "insert" "--create-folder"\r
++           (concat "--folder=" notmuch-message-draft-folder)\r
++           notmuch-message-draft-tags))\r
++    ;; We are now back in the original compose buffer. Note the\r
++    ;; function notmuch-call-notmuch-process signals an error on\r
++    ;; failure, so to get to this point it must have succeeded. Note\r
++    ;; notmuch-message-draft-id is still the id of the previous draft,\r
++    ;; so it is safe to mark it deleted.\r
++    (notmuch-message-mark-draft-deleted)\r
++    (setq notmuch-message-draft-id (concat "id:" id))\r
++    (set-buffer-modified-p nil)))\r
++\r
++(defun notmuch-message-postpone ()\r
++  "Save the draft message in the notmuch database and exit buffer."\r
++  (interactive)\r
++  (notmuch-message-save-draft)\r
++  (kill-buffer))\r
++\r
++(defun notmuch-message-resume (id)\r
++  "Resume editing of message with id ID."\r
++  (let* ((tags (process-lines notmuch-command "search" "--output=tags"\r
++                            "--exclude=false" id))\r
++       (draft (equal tags (notmuch-update-tags tags notmuch-message-draft-tags))))\r
++    (when (or draft\r
++            (yes-or-no-p "Message does not appear to be a draft: really resume? "))\r
++      (switch-to-buffer (get-buffer-create (concat "*notmuch-draft-" id "*")))\r
++      (setq buffer-read-only nil)\r
++      (erase-buffer)\r
++      (let ((coding-system-for-read 'no-conversion))\r
++      (call-process notmuch-command nil t nil "show" "--format=raw" id))\r
++      (mime-to-mml)\r
++      (goto-char (point-min))\r
++      (when (re-search-forward "^$" nil t)\r
++      (replace-match mail-header-separator t t))\r
++      ;; Remove our added Date and Message-ID headers (unless the user has\r
++      ;; explicitly customized emacs to tell us not to).\r
++      (save-restriction\r
++      (message-narrow-to-headers)\r
++      (when (member 'Message-ID message-deletable-headers)\r
++        (message-remove-header "Message-ID"))\r
++      (when (member 'Date message-deletable-headers)\r
++        (message-remove-header "Date")))\r
++      ;; If the message does not appear to be a draft, the postpone\r
++      ;; code probably didn't write it, so it should not be unquoted.\r
++      (when draft\r
++      (notmuch-message-unquote-some-mml))\r
++      (notmuch-message-mode)\r
++      (set-buffer-modified-p nil)\r
++      ;; If the resumed message was a draft then set the draft\r
++      ;; message-id so that we can delete the current saved draft if the\r
++      ;; message is resaved or sent.\r
++      (setq notmuch-message-draft-id (when draft id)))))\r
++\r
++\r
+ (add-hook 'message-send-hook 'notmuch-message-mark-replied)\r
++(add-hook 'message-send-hook 'notmuch-message-mark-draft-deleted)\r
\r
+ (provide 'notmuch-message)\r
\r
+diff --git a/emacs/notmuch-mua.el b/emacs/notmuch-mua.el\r
+index 399e138..3118e5d 100644\r
+--- a/emacs/notmuch-mua.el\r
++++ b/emacs/notmuch-mua.el\r
+@@ -33,6 +33,8 @@\r
+ (declare-function notmuch-show-insert-body "notmuch-show" (msg body depth))\r
+ (declare-function notmuch-fcc-header-setup "notmuch-maildir-fcc" ())\r
+ (declare-function notmuch-fcc-handler "notmuch-maildir-fcc" (destdir))\r
++(declare-function notmuch-message-postpone "notmuch-message" ())\r
++(declare-function notmuch-message-save-draft "notmuch-message" ())\r
\r
+ ;;\r
\r
+@@ -283,6 +285,8 @@ mutiple parts get a header."\r
\r
+ (define-key notmuch-message-mode-map (kbd "C-c C-c") #'notmuch-mua-send-and-exit)\r
+ (define-key notmuch-message-mode-map (kbd "C-c C-s") #'notmuch-mua-send)\r
++(define-key notmuch-message-mode-map (kbd "C-c C-p") #'notmuch-message-postpone)\r
++(define-key notmuch-message-mode-map (kbd "C-x C-s") #'notmuch-message-save-draft)\r
\r
+ (defun notmuch-mua-pop-to-buffer (name switch-function)\r
+   "Pop to buffer NAME, and warn if it already exists and is\r
+diff --git a/emacs/notmuch-show.el b/emacs/notmuch-show.el\r
+index f33096c..12b21c9 100644\r
+--- a/emacs/notmuch-show.el\r
++++ b/emacs/notmuch-show.el\r
+@@ -38,6 +38,7 @@\r
+ (require 'notmuch-mua)\r
+ (require 'notmuch-crypto)\r
+ (require 'notmuch-print)\r
++(require 'notmuch-message)\r
\r
+ (declare-function notmuch-call-notmuch-process "notmuch" (&rest args))\r
+ (declare-function notmuch-search-next-thread "notmuch" nil)\r
+@@ -1425,6 +1426,7 @@ reset based on the original query."\r
+     (define-key map "|" 'notmuch-show-pipe-message)\r
+     (define-key map "w" 'notmuch-show-save-attachments)\r
+     (define-key map "V" 'notmuch-show-view-raw-message)\r
++    (define-key map "e" 'notmuch-show-resume-message)\r
+     (define-key map "c" 'notmuch-show-stash-map)\r
+     (define-key map "h" 'notmuch-show-toggle-visibility-headers)\r
+     (define-key map "*" 'notmuch-show-tag-all)\r
+@@ -1955,6 +1957,13 @@ to show, nil otherwise."\r
+     (setq buffer-read-only t)\r
+     (view-buffer buf 'kill-buffer-if-not-modified)))\r
\r
++(defun notmuch-show-resume-message ()\r
++  "Resume EDITING the current draft message."\r
++  (interactive)\r
++  (let ((id (notmuch-show-get-message-id)))\r
++    (when id\r
++      (notmuch-message-resume id))))\r
++\r
+ (put 'notmuch-show-pipe-message 'notmuch-doc\r
+      "Pipe the contents of the current message to a command.")\r
+ (put 'notmuch-show-pipe-message 'notmuch-prefix-doc\r
+diff --git a/emacs/notmuch-tree.el b/emacs/notmuch-tree.el\r
+index 6c35543..c759290 100644\r
+--- a/emacs/notmuch-tree.el\r
++++ b/emacs/notmuch-tree.el\r
+@@ -261,6 +261,7 @@ FUNC."\r
+     (define-key map "r" (notmuch-tree-close-message-pane-and #'notmuch-show-reply-sender))\r
+     (define-key map "R" (notmuch-tree-close-message-pane-and #'notmuch-show-reply))\r
+     (define-key map "V" (notmuch-tree-close-message-pane-and #'notmuch-show-view-raw-message))\r
++    (define-key map "e" (notmuch-tree-close-message-pane-and #'notmuch-show-resume-message))\r
\r
+     ;; The main tree view bindings\r
+     (define-key map (kbd "RET") 'notmuch-tree-show-message)\r
+-- \r
+2.1.4\r
+\r