From: Eric Abrahamsen Date: Thu, 9 Oct 2014 01:47:46 +0000 (+0800) Subject: Squashed 'packages/gnorb/' content from commit de3a512 X-Git-Url: https://code.delx.au/gnu-emacs-elpa/commitdiff_plain/0b9eb2b647a49ffa3dc4e3e61cb8bd94c7fe3634 Squashed 'packages/gnorb/' content from commit de3a512 git-subtree-dir: packages/gnorb git-subtree-split: de3a512fc8b33b6403e1a5b6391de0dac3f80c84 --- 0b9eb2b647a49ffa3dc4e3e61cb8bd94c7fe3634 diff --git a/.elpaignore b/.elpaignore new file mode 100644 index 000000000..f614ef766 --- /dev/null +++ b/.elpaignore @@ -0,0 +1 @@ +gnorb.org \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..60fa2cac2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +*.elc +notes.org +gnorb-pkg.el +gnorb-autoloads.el \ No newline at end of file diff --git a/NEWS b/NEWS new file mode 100644 index 000000000..728f21950 --- /dev/null +++ b/NEWS @@ -0,0 +1,11 @@ +GNU Emacs Gnorb NEWS -- history of user-visible changes. -*- org -*- + +* Version 1 [2014-10-07 Tue] +** First Elpa Version +** Email Tracking +The mechanism for email tracking has changed since Gnorb was made +available on Elpa. See the manual for set-up instructions. +** Directory Structure +The directory structure has changed since Gnorb was made available on +Elpa. There is no longer a lisp/ directory -- all *.el files are now +at the top level. diff --git a/README.org b/README.org new file mode 100644 index 000000000..cbb13afe9 --- /dev/null +++ b/README.org @@ -0,0 +1,31 @@ +* Gnorb + +Glue code between the Gnus, Org, and BBDB packages for Emacs. + +This package connects Emacs-based email, project management, and +contact management a little more closely together. The goal is to +reduce friction when manipulating TODOs, contacts, messages, and +files. + +Probably the most interesting thing Gnorb does is tracking +correspondences between Gnus email messages and Org headings. Rather +than "turning your inbox into a TODO list", as some software puts it, +Gnorb (kind of) does the opposite: turning your TODO headings into +mini mailboxes. + +*Note for previous users*: If you were using Gnorb from Github before +it shifted to the Elpa repository, the email tracking mechanism has +changed, please see the manual for details. + +** Installation + +It's easiest to install Gnorb from Elpa: run `list-packages' and look +for it there. + +Or clone the Git repo at https://github.com/girzel/gnorb, and add the +top-level directory to your load path. + +If you want to use Gnorb for tracking emails with TODOs, you'll need +to add a nngnorb server to your `gnus-secondary-select-methods' +variable, then call `gnorb-tracking-initialize' in your init files. +Again, see the manual for details. diff --git a/dir b/dir new file mode 100644 index 000000000..b0d0ad6e6 --- /dev/null +++ b/dir @@ -0,0 +1,18 @@ +This is the file .../info/dir, which contains the +topmost node of the Info hierarchy, called (dir)Top. +The first time you invoke Info you start off looking at this node. + +File: dir, Node: Top This is the top of the INFO tree + + This (the Directory node) gives a menu of major topics. + Typing "q" exits, "?" lists all Info commands, "d" returns here, + "h" gives a primer for first-timers, + "mEmacs" visits the Emacs manual, etc. + + In Emacs, you can click mouse button 2 on a menu item or cross reference + to select it. + +* Menu: + +Emacs +* Gnorb: (gnorb). Glue code for Gnus, Org, and BBDB. diff --git a/gnorb-bbdb.el b/gnorb-bbdb.el new file mode 100644 index 000000000..b30298f92 --- /dev/null +++ b/gnorb-bbdb.el @@ -0,0 +1,565 @@ +;;; gnorb-bbdb.el --- The BBDB-centric functions of gnorb + +;; Copyright (C) 2014 Eric Abrahamsen + +;; Author: Eric Abrahamsen +;; Keywords: + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; + +;;; Code: + +(require 'gnorb-utils) + +(defgroup gnorb-bbdb nil + "The BBDB bits of gnorb." + :tag "Gnorb BBDB" + :group 'gnorb) + +(defcustom gnorb-bbdb-org-tag-field 'org-tags + "The name (as a symbol) of the field to use for org tags." + :group 'gnorb-bbdb + :type 'symbol) + +(unless (assoc gnorb-bbdb-org-tag-field bbdb-separator-alist) + (push `(,gnorb-bbdb-org-tag-field ":" ":") bbdb-separator-alist)) + +(defcustom gnorb-bbdb-messages-field 'messages + "The name (as a symbol) of the field where links to recent gnus +messages from this record are stored. + +\\Records that do not have this field defined +will not collect links to messages: you have to call +\"\\[gnorb-bbdb-open-link]\" on the record once -- after that, +message links will be collected and updated automatically." + :group 'gnorb-bbdb + :type 'symbol) + +(defcustom gnorb-bbdb-collect-N-messages 5 + "For records with a `gnorb-bbdb-messages-field' defined, +collect links to a maximum of this many messages." + :group 'gnorb-bbdb + :type 'integer) + +(defcustom gnorb-bbdb-define-recent 'seen + "For records with a `gnorb-bbdb-message-tag-field' defined, +this variable controls how gnorb defines a \"recent\" message. +Setting it to the symbol seen will collect the messages most +recently opened and viewed. The symbol received means gnorb will +collect the most recent messages by Date header. + +In other words, if this variable is set to 'received, and a +record's messages field is already full of recently-received +messages, opening a five-year-old message (for instance) from +this record will not push a link to the message into the field." + :group 'gnorb-bbdb + :type '(choice (const :tag "Most recently seen" 'seen) + (const :tag "Most recently received" 'received))) + +(defcustom gnorb-bbdb-message-link-format-multi "%:count. %D: %:subject" + "How a single message is formatted in the list of recent messages. +This format string is used in multi-line record display. + +Available information for each message includes the subject, the +date, and the message's count in the list, as an integer. You can +access subject and count using the %:subject and %:count escapes. +The message date can be formatted using any of the escapes +mentioned in the docstring of `format-time-string', which see." + :group 'gnorb-bbdb + :type 'string) + +(defcustom gnorb-bbdb-message-link-format-one "%:count" + "How a single message is formatted in the list of recent messages. +This format string is used in single-line display -- note that by +default, no user-created xfields are displayed in the 'one-line +layout found in `bbdb-layout-alist'. If you want this field to +appear there, put its name in the \"order\" list of the 'one-line +layout. + +Available information for each message includes the subject, the +date, and the message's count in the list, as an integer. You can +access subject and count using the %:subject and %:count escapes. +The message date can be formatted using any of the escapes +mentioned in the docstring of `format-time-string', which see." + :group 'gnorb-bbdb + :type 'string) + +(defface gnorb-bbdb-link (org-compatible-face 'org-link nil) + "Custom face for displaying message links in the *BBDB* buffer. + Defaults to org-link." + :group 'gnorb-bbdb) + +(defstruct gnorb-bbdb-link + subject date group id) + +(defcustom gnorb-bbdb-posting-styles nil + "An alist of styles to use when composing messages to the BBDB +record(s) under point. This is entirely analogous to +`gnus-posting-styles', it simply works by examining record fields +rather than group names. + +When composing a message to multiple contacts (using the \"*\" +prefix), the records will be scanned in order, with the record +initially under point (if any) set aside for last. That means +that, in the case of conflicting styles, the record under point +will override the others. + +In order not to be too intrusive, this option has no effect on +the usual `bbdb-mail' command. Instead, the wrapper command +`gnorb-bbdb-mail' is provided, which consults this option and +then hands off to `bbdb-compose-mail'. If you'd always like to +use `gnorb-bbdb-mail', you can simply bind it to \"m\" in the +`bbdb-mode-map'. + +The value of the option should be a list of sexps, each one +matching a single field. The first element should match a field +name: one of the built-in fields like lastname, or an xfield. +Field names should be given as symbols. + +The second element is a regexp used to match against the value of +the field (non-string field values will be cast to strings, if +possible). It can also be a cons of two strings, the first of +which matches the field label, the second the field value. + +Alternately, the first element can be the name of a custom +function that is called with the record as its only argument, and +returns either t or nil. In this case, the second element of the +list is disregarded. + +All following elements should be field setters for the message to +be composed, just as in `gnus-posting-styles'. + +An example value might look like:" + :group 'gnorb-bbdb) + +;;;###autoload +(defun gnorb-bbdb-mail (records &optional subject n verbose) + "\\Acts just like `bbdb-mail', except runs +RECORDS through `gnorb-bbdb-posting-styles', allowing +customization of message styles for certain records. From the +`bbdb-mail' docstring: + +Compose a mail message to RECORDS (optional: using SUBJECT). +Interactively, use BBDB prefix \\[bbdb-do-all-records], see +`bbdb-do-all-records'. By default, the first mail addresses of +RECORDS are used. If prefix N is a number, use Nth mail address +of RECORDS (starting from 1). If prefix N is C-u (t +noninteractively) use all mail addresses of RECORDS. If VERBOSE +is non-nil (as in interactive calls) be verbose." + ;; see the function `gnus-configure-posting-styles' for tips on how + ;; to actually do this. + (interactive (list (bbdb-do-records) nil + (or (consp current-prefix-arg) + current-prefix-arg) + t)) + (setq records (bbdb-record-list records)) + (if (not records) + (user-error "No records displayed") + (let ((head (bbdb-current-record)) + (to (bbdb-mail-address records n nil verbose)) + (message-mode-hook (copy-sequence message-mode-hook))) + (setq records (remove head records)) + (when gnorb-bbdb-posting-styles + (add-hook 'message-mode-hook + `(lambda () + (gnorb-bbdb-configure-posting-styles (quote ,records)) + (gnorb-bbdb-configure-posting-styles (list ,head))))) + (bbdb-compose-mail to subject)))) + +(defun gnorb-bbdb-configure-posting-styles (recs) + ;; My most magnificent work of copy pasta! + (dolist (r recs) + (let (field val label rec-val element filep + element v value results name address) + (dolist (style gnorb-bbdb-posting-styles) + (setq field (pop style) + val (pop style)) + (when (consp val) ;; (label value) + (setq label (pop val) + val (pop val))) + (unless (fboundp field) + ;; what's the record's existing value for this field? + (setq rec-val (bbdb-record-field r field))) + (when (cond + ((eq field 'address) + (dolist (a rec-val) + (unless (and label + (not (string-match label (car a)))) + (string-match val (bbdb-format-address-default a))))) + ((eq field 'phone) + (dolist (p rec-val) + (unless (and label + (not (string-match label (car p)))) + (string-match val (bbdb-phone-string p))))) + ((consp rec-val) + (dolist (f rec-val) + (string-match val f))) + ((fboundp field) + (funcall field r)) + ((stringp rec-val) + (string-match val rec-val))) + ;; there are matches, run through the field setters in last + ;; element of the sexp + (dolist (attribute style) + (setq element (pop attribute) + filep nil) + (setq value + (cond + ((eq (car attribute) :file) + (setq filep t) + (cadr attribute)) + ((eq (car attribute) :value) + (cadr attribute)) + (t + (car attribute)))) + ;; We get the value. + (setq v + (cond + ((stringp value) + value) + ((or (symbolp value) + (functionp value)) + (cond ((functionp value) + (funcall value)) + ((boundp value) + (symbol-value value)))) + ((listp value) + (eval value)))) + ;; Post-processing for the signature posting-style: + (and (eq element 'signature) filep + message-signature-directory + ;; don't actually use the signature directory + ;; if message-signature-file contains a path. + (not (file-name-directory v)) + (setq v (nnheader-concat message-signature-directory v))) + ;; Get the contents of file elems. + (when (and filep v) + (setq v (with-temp-buffer + (insert-file-contents v) + (buffer-substring + (point-min) + (progn + (goto-char (point-max)) + (if (zerop (skip-chars-backward "\n")) + (point) + (1+ (point)))))))) + (setq results (delq (assoc element results) results)) + (push (cons element v) results)))) + (setq name (assq 'name results) + address (assq 'address results)) + (setq results (delq name (delq address results))) + (gnus-make-local-hook 'message-setup-hook) + (setq results (sort results (lambda (x y) + (string-lessp (car x) (car y))))) + (dolist (result results) + (add-hook 'message-setup-hook + (cond + ((eq 'eval (car result)) + 'ignore) + ((eq 'body (car result)) + `(lambda () + (save-excursion + (message-goto-body) + (insert ,(cdr result))))) + ((eq 'signature (car result)) + (set (make-local-variable 'message-signature) nil) + (set (make-local-variable 'message-signature-file) nil) + (if (not (cdr result)) + 'ignore + `(lambda () + (save-excursion + (let ((message-signature ,(cdr result))) + (when message-signature + (message-insert-signature))))))) + (t + (let ((header + (if (symbolp (car result)) + (capitalize (symbol-name (car result))) + (car result)))) + `(lambda () + (save-excursion + (message-remove-header ,header) + (let ((value ,(cdr result))) + (when value + (message-goto-eoh) + (insert ,header ": " value) + (unless (bolp) + (insert "\n"))))))))) + t 'local)) + (when (or name address) + (add-hook 'message-setup-hook + `(lambda () + (set (make-local-variable 'user-mail-address) + ,(or (cdr address) user-mail-address)) + (let ((user-full-name ,(or (cdr name) (user-full-name))) + (user-mail-address + ,(or (cdr address) user-mail-address))) + (save-excursion + (message-remove-header "From") + (message-goto-eoh) + (insert "From: " (message-make-from) "\n")))) + t 'local))))) + +;;;###autoload +(defun gnorb-bbdb-tag-agenda (records) + "Open an Org agenda tags view from the BBDB buffer, using the +value of the record's org-tags field. This shows only TODOs by +default; a prefix argument shows all tagged headings; a \"*\" +prefix operates on all currently visible records. If you want +both, use \"C-u\" before the \"*\"." + (interactive (list (bbdb-do-records))) + (require 'org-agenda) + (unless (and (eq major-mode 'bbdb-mode) + (equal (buffer-name) bbdb-buffer-name)) + (error "Only works in the BBDB buffer")) + (setq records (bbdb-record-list records)) + (let ((tag-string + (mapconcat + 'identity + (delete-dups + (mapcan (lambda (r) + (bbdb-record-xfield-split r gnorb-bbdb-org-tag-field)) + records)) + "|"))) + (if tag-string + ;; C-u = all headings, not just todos + (if (equal current-prefix-arg '(4)) + (org-tags-view nil tag-string) + (org-tags-view t tag-string)) + (error "No org-tags field present")))) + +;;;###autoload +(defun gnorb-bbdb-mail-search (records) + "Initiate a mail search from the BBDB buffer. + +Use the prefix arg to edit the search string first, and the \"*\" +prefix to search mails from all visible contacts. When using both +a prefix arg and \"*\", the prefix arg must come first." + (interactive (list (bbdb-do-records))) + (unless (and (eq major-mode 'bbdb-mode) + (equal (buffer-name) bbdb-buffer-name)) + (error "Only works in the BBDB buffer")) + (setq records (bbdb-record-list records)) + (require 'gnorb-gnus) + (let* ((backend (or (assoc gnorb-gnus-mail-search-backend + gnorb-gnus-mail-search-backends) + (error "No search backend specified"))) + (search-string + (funcall (second backend) + (cl-mapcan 'bbdb-record-mail records)))) + (when (equal current-prefix-arg '(4)) + (setq search-string + (read-from-minibuffer + (format "%s search string: " (first backend)) search-string))) + (funcall (third backend) search-string) + (delete-other-windows))) + +;;;###autoload +(defun gnorb-bbdb-cite-contact (rec) + (interactive (list (gnorb-prompt-for-bbdb-record))) + (let ((mail-string (bbdb-dwim-mail rec))) + (if (called-interactively-p 'any) + (insert mail-string) + mail-string))) + +;;; Field containing links to recent messages + +(add-to-list 'bbdb-xfield-label-list gnorb-bbdb-messages-field nil 'eq) + +(defun gnorb-bbdb-display-messages (record format) + "Show links to the messages collected in the +`gnorb-bbdb-messages-field' field of a BBDB record. Each link +will be formatted using the format string in +`gnorb-bbdb-message-link-format-multi' or +`gnorb-bbdb-message-link-format-one', depending on the current +layout type." + (let ((full-field (assq gnorb-bbdb-messages-field + (bbdb-record-xfields record))) + (val (bbdb-record-xfield record gnorb-bbdb-messages-field)) + (map (make-sparse-keymap)) + (count 1)) ; one-indexed to fit with prefix arg to `gnorb-bbdb-open-link' + (define-key map [mouse-1] 'gnorb-bbdb-mouse-open-link) + (define-key map (kbd "") 'gnorb-bbdb-RET-open-link) + (when val + ;; indent and fmt are dynamically bound + (when (eq format 'multi) + (bbdb-display-text (format fmt gnorb-bbdb-messages-field) + `(xfields ,full-field field-label) + 'bbdb-field-name)) + (insert (cond ((and (stringp val) + (eq format 'multi)) + (bbdb-indent-string (concat val "\n") indent)) + ((listp val) + (concat + (bbdb-indent-string + (mapconcat + (lambda (m) + (prog1 + (org-propertize + (concat + (format-time-string + (replace-regexp-in-string + "%:subject" (gnorb-bbdb-link-subject m) + (replace-regexp-in-string + "%:count" (number-to-string count) + (if (eq format 'multi) + gnorb-bbdb-message-link-format-multi + gnorb-bbdb-message-link-format-one))) + (gnorb-bbdb-link-date m))) + 'face 'gnorb-bbdb-link + 'mouse-face 'highlight + 'gnorb-bbdb-link-count count + 'keymap map) + (incf count))) + val (if (eq format 'multi) + "\n" ", ")) + indent) + (if (eq format 'multi) "\n" ""))) + (t + "")))))) + +(fset (intern (format "bbdb-display-%s-multi-line" + gnorb-bbdb-messages-field)) + (lambda (record) + (gnorb-bbdb-display-messages record 'multi))) + +(fset (intern (format "bbdb-display-%s-one-line" + gnorb-bbdb-messages-field)) + (lambda (record) + (gnorb-bbdb-display-messages record 'one))) + +;; Don't allow direct editing of this field + +(fset (intern (format "bbdb-read-xfield-%s" + gnorb-bbdb-messages-field)) + (lambda (&optional init) + (user-error "This field shouldn't be edited manually"))) + +;; Open links from the *BBDB* buffer. + +;;;###autoload +(defun gnorb-bbdb-open-link (record arg) + "\\Call this on a BBDB record to open one of the +links in the message field. By default, the first link will be +opened. Use a prefix arg to open different links. For instance, +M-3 \\[gnorb-bbdb-open-link] will open the third link in the +list. If the %:count escape is present in the message formatting +string (see `gnorb-bbdb-message-link-format-multi' and +`gnorb-bbdb-message-link-format-one'), that's the number to use. + +This function also needs to be called on a contact once before +that contact will start collecting links to messages." + (interactive (list + (or (bbdb-current-record) + (user-error "No record under point")) + current-prefix-arg)) + (unless (fboundp 'bbdb-record-xfield-string) + (user-error "This function only works with the git version of BBDB")) + (let* ((record (bbdb-current-record)) + msg-list target-msg) + (if (not (memq gnorb-bbdb-messages-field + (mapcar 'car (bbdb-record-xfields record)))) + (when (y-or-n-p + (format "Start collecting message links for %s?" + (bbdb-record-name record))) + (bbdb-record-set-xfield record gnorb-bbdb-messages-field "no links yet") + (message "Opening messages from %s will add links to the %s field" + (bbdb-record-name record) + gnorb-bbdb-messages-field) + (bbdb-change-record record)) + (setq msg-list + (bbdb-record-xfield record gnorb-bbdb-messages-field)) + (setq target-msg + (or (and arg + (nth (1- arg) msg-list)) + (car msg-list))) + (when target-msg + (org-gnus-follow-link (gnorb-bbdb-link-group target-msg) + (gnorb-bbdb-link-id target-msg)))))) + +(defun gnorb-bbdb-mouse-open-link (event) + (interactive "e") + (mouse-set-point event) + (let ((rec (bbdb-current-record)) + (num (get-text-property (point) 'gnorb-bbdb-link-count))) + (if (not num) + (user-error "No link under point") + (gnorb-bbdb-open-link rec num)))) + +(defun gnorb-bbdb-RET-open-link () + (interactive) + (let ((rec (bbdb-current-record)) + (num (get-text-property (point) 'gnorb-bbdb-link-count))) + (if (not num) + (user-error "No link under point") + (gnorb-bbdb-open-link rec num)))) + +(defun gnorb-bbdb-store-message-link (record) + "Used in the `bbdb-notice-record-hook' to possibly save a link +to a message into the record's `gnorb-bbdb-messages-field'." + + (when (not (fboundp 'bbdb-record-xfield-string)) + (user-error "This function only works with the git version of BBDB")) + (unless (or (not (and (memq gnorb-bbdb-messages-field + (mapcar 'car (bbdb-record-xfields record))) + (memq major-mode '(gnus-summary-mode gnus-article-mode)))) + (with-current-buffer gnus-article-buffer + (not ; only store messages if the record is the sender + (member (nth 1 (car (bbdb-get-address-components 'sender))) + (bbdb-record-mail record))))) + (with-current-buffer gnus-summary-buffer + (let* ((val (bbdb-record-xfield record gnorb-bbdb-messages-field)) + (art-no (gnus-summary-article-number)) + (heads (gnus-summary-article-header art-no)) + (date (apply 'encode-time + (parse-time-string (mail-header-date heads)))) + (subject (mail-header-subject heads)) + (id (mail-header-id heads)) + (group gnus-newsgroup-name) + link) + ;; check for both nnvirtual and nnir, and link to the real + ;; group in those cases + (when (eq (car (gnus-find-method-for-group group)) + 'nnvirtual) + (setq group (car (nnvirtual-map-article art-no)))) + (when (eq (car (gnus-find-method-for-group group)) + 'nnir) + (setq group (nnir-article-group art-no))) + (if (not (and date subject id group)) + (message "Could not save a link to this message") + (setq link (make-gnorb-bbdb-link :subject subject :date date + :group group :id id)) + (when (stringp val) + (setq val nil)) + (setq val (cons link (delete link val))) + (when (eq gnorb-bbdb-define-recent 'received) + (setq val (sort val + (lambda (a b) + (time-less-p + (gnorb-bbdb-link-date b) + (gnorb-bbdb-link-date a)))))) + (setq val (subseq val 0 gnorb-bbdb-collect-N-messages)) + (bbdb-record-set-xfield record + gnorb-bbdb-messages-field + (delq nil val)) + (bbdb-change-record record)))))) + +(add-hook 'bbdb-notice-record-hook 'gnorb-bbdb-store-message-link) + +(provide 'gnorb-bbdb) +;;; gnorb-bbdb.el ends here diff --git a/gnorb-gnus.el b/gnorb-gnus.el new file mode 100644 index 000000000..ba72107f9 --- /dev/null +++ b/gnorb-gnus.el @@ -0,0 +1,671 @@ +;;; gnorb-gnus.el --- The gnus-centric fuctions of gnorb + +;; Copyright (C) 2014 Eric Abrahamsen + +;; Author: Eric Abrahamsen +;; Keywords: + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; + +;;; Code: + +(require 'gnorb-utils) + +(declare-function org-gnus-article-link "org-gnus" + (group newsgroups message-id x-no-archive)) +(declare-function org-gnus-follow-link "org-gnus" + (group article)) + +(defgroup gnorb-gnus nil + "The Gnus bits of Gnorb." + :tag "Gnorb Gnus" + :group 'gnorb) + + +(defcustom gnorb-gnus-mail-search-backends + '((notmuch (lambda (terms) + (mapconcat + (lambda (m) + (replace-regexp-in-string "\\." "\\\\." m)) + terms " OR ")) + notmuch-search) + (mairix (lambda (terms) + (mapconcat 'identity + terms ",")) + mairix-search) + (namazu (lambda (terms) + (mapconcat 'identity + terms " or ")) + namazu-search)) + "Various backends for mail search. + +An alist of backends, where each element consists of three parts: +the symbol name of the backend, a lambda form which receives a +list of email addresses and returns a properly-formatted search +string, and the symbol name of the function used to initiate the +search." + :group 'gnorb-gnus + :type 'list) + +(defcustom gnorb-gnus-mail-search-backend nil + "Mail search backend currently in use. One of the three symbols +notmuch, namazu, or mairix." + :group 'gnorb-gnus + :type 'symbol) + +(defcustom gnorb-gnus-capture-always-attach nil + "Always prompt about attaching attachments when capturing from + a Gnus message, even if the template being used hasn't + specified the :gnus-attachments key. + +Basically behave as if all attachments have \":gnus-attachments t\"." + :group 'gnorb-gnus + :type 'boolean) + +(defcustom gnorb-gnus-new-todo-capture-key nil + "Key for the capture template to use when creating a new TODO + from an outgoing message." + :group 'gnorb-gnus + :type 'string) + +(defcustom gnorb-gnus-hint-relevant-article t + "When opening a gnus message, should gnorb let you know if the + message is relevant to an existing TODO?" + :group 'gnorb-gnus + :type 'boolean) + +(defcustom gnorb-gnus-summary-mark-format-letter "g" + "Format letter to be used as part of your + `gnus-summary-line-format', to indicate in the *Summary* buffer + which articles might be relevant to TODOs. Since this is a user + format code, it should be prefixed with %u, eg %ug. It will + result in the insertion of the value of + `gnorb-gnus-summary-mark', for relevant messages, or + else a space." + :group 'gnorb-gnus + :type 'string) + +(defcustom gnorb-gnus-summary-mark "¡" + "Default mark to insert in the summary format line of articles + that are likely relevant to existing TODO headings." + :group 'gnorb-gnus + :type 'string) + +(defcustom gnorb-gnus-trigger-refile-targets + '((org-agenda-files :maxlevel . 4)) + "A value to use as an equivalent of `org-refile-targets' (which + see) when offering trigger targets for + `gnorb-gnus-incoming-do-todo'." + :group 'gnorb-gnus + :type 'list) + +(defcustom gnorb-gnus-sent-groups nil + "A list of strings indicating sent mail groups. + +In some cases, Gnorb can't detect where your sent messages are +stored (ie if you're using IMAP sent mail folders instead of +local archiving. If you want Gnorb to be able to find sent +messages, this option can help it do that. It should be set to a +list of strings, which are assumed to be fully qualified +server+group combinations, ie \"nnimap+Server:[Gmail]/Sent +Mail\", or something similar. This only has to be done once for +each message." + :group 'gnorb-gnus + :type 'list) + +(defvar gnorb-gnus-capture-attachments nil + "Holding place for attachment names during the capture + process.") + +;;; What follows is a very careful copy-pasta of bits and pieces from +;;; mm-decode.el and gnus-art.el. Voodoo was involved. + +;;;###autoload +(defun gnorb-gnus-article-org-attach (n) + "Save MIME part N, which is the numerical prefix, of the + article under point as an attachment to the specified org + heading." + (interactive "P") + (gnus-article-part-wrapper n 'gnorb-gnus-attach-part)) + +;;;###autoload +(defun gnorb-gnus-mime-org-attach () + "Save the MIME part under point as an attachment to the + specified org heading." + (interactive) + (gnus-article-check-buffer) + (let ((data (get-text-property (point) 'gnus-data))) + (when data + (gnorb-gnus-attach-part data)))) + +(defun gnorb-gnus-attach-part (handle &optional org-heading) + "Attach HANDLE to an existing org heading." + (let* ((filename (gnorb-gnus-save-part handle)) + (org-refile-targets gnorb-gnus-trigger-refile-targets) + (ref-msg-ids + (concat (gnus-fetch-original-field "references") " " + (gnus-fetch-original-field "in-reply-to"))) + (rel-heading + (when gnorb-tracking-enabled + (car (gnorb-find-visit-candidates + ref-msg-ids)))) + (org-heading + (if (and rel-heading + (y-or-n-p (message + "Attach part to %s" + (gnorb-pretty-outline rel-heading)))) + rel-heading + (org-refile-get-location "Attach part to" nil t)))) + (require 'org-attach) + (save-window-excursion + (if (stringp org-heading) + (org-id-goto org-heading) + (progn + (find-file (nth 1 org-heading)) + (goto-char (nth 3 org-heading)))) + (org-attach-attach filename nil 'mv)))) + +(defun gnorb-gnus-save-part (handle) + (let ((filename (or (mail-content-type-get + (mm-handle-disposition handle) 'filename) + (mail-content-type-get + (mm-handle-type handle) 'name)))) + (setq filename + (gnus-map-function mm-file-name-rewrite-functions + (file-name-nondirectory filename))) + (setq filename (expand-file-name filename gnorb-tmp-dir)) + (mm-save-part-to-file handle filename) + filename)) + +(defun gnorb-gnus-collect-all-attachments (&optional capture-p store) + "Collect all the attachments from the message under point, and +save them into `gnorb-tmp-dir'." + (save-window-excursion + (when capture-p + (set-buffer (org-capture-get :original-buffer))) + (unless (memq major-mode '(gnus-summary-mode gnus-article-mode)) + (error "Only works in Gnus summary or article buffers")) + (let ((article (gnus-summary-article-number)) + mime-handles) + (when (or (null gnus-current-article) + (null gnus-article-current) + (/= article (cdr gnus-article-current)) + (not (equal (car gnus-article-current) gnus-newsgroup-name))) + (gnus-summary-display-article article)) + (gnus-eval-in-buffer-window gnus-article-buffer + (setq mime-handles (cl-remove-if-not + (lambda (h) + (let ((disp (mm-handle-disposition (cdr h)))) + (and (member (car disp) + '("inline" "attachment")) + (mail-content-type-get disp 'filename)))) + gnus-article-mime-handle-alist))) + (when mime-handles + (dolist (h mime-handles) + (let ((filename + (gnorb-gnus-save-part (cdr h)))) + (when (or capture-p store) + (push filename gnorb-gnus-capture-attachments)))))))) + +;;; Make the above work in the capture process + +(defun gnorb-gnus-capture-attach () + (when (and (or gnorb-gnus-capture-always-attach + (org-capture-get :gnus-attachments)) + (with-current-buffer + (org-capture-get :original-buffer) + (memq major-mode '(gnus-summary-mode gnus-article-mode)))) + (require 'org-attach) + (setq gnorb-gnus-capture-attachments nil) + (gnorb-gnus-collect-all-attachments t) + (map-y-or-n-p + (lambda (a) + (format "Attach %s to capture heading? " + (file-name-nondirectory a))) + (lambda (a) (org-attach-attach a nil 'mv)) + gnorb-gnus-capture-attachments + '("file" "files" "attach")) + (setq gnorb-gnus-capture-attachments nil))) + +(add-hook 'org-capture-mode-hook 'gnorb-gnus-capture-attach) + +(defun gnorb-gnus-capture-abort-cleanup () + (when (and org-note-abort + (org-capture-get :gnus-attachments)) + (condition-case error + (progn (org-attach-delete-all) + (setq abort-note 'clean) + ;; remove any gnorb-mail-header values here + ) + (error + (setq abort-note 'dirty))))) + +(add-hook 'org-capture-prepare-finalize-hook + 'gnorb-gnus-capture-abort-cleanup) + +;;; Storing, removing, and acting on Org headers in messages. + +(defvar gnorb-gnus-message-info nil + "Place to store the To, Subject, Date, and Message-ID headers + of the currently-sending or last-sent message.") + +(defun gnorb-gnus-check-outgoing-headers () + "Save the value of the `gnorb-mail-header' for the current +message; multiple header values returned as a string. Also save +information about the outgoing message into +`gnorb-gnus-message-info'." + (save-restriction + (message-narrow-to-headers) + (setq gnorb-gnus-message-info nil) + (let* ((org-ids (mail-fetch-field gnorb-mail-header nil nil t)) + (msg-id (mail-fetch-field "Message-ID")) + (refs (mail-fetch-field "References")) + (in-reply-to (mail-fetch-field "In-Reply-To")) + (to (if (message-news-p) + (mail-fetch-field "Newsgroups") + (mail-fetch-field "To"))) + (from (mail-fetch-field "From")) + (subject (mail-fetch-field "Subject")) + (date (mail-fetch-field "Date")) + ;; If we can get a link, that's awesome. + (gcc (mail-fetch-field "Gcc")) + (link (or (and gcc + (org-store-link nil)) + nil)) + (group (ignore-errors (car (split-string link "#"))))) + ;; If we can't make a real link, then save some information so + ;; we can fake it. + (when in-reply-to + (setq refs (concat refs " " in-reply-to))) + (when refs + (setq refs (gnus-extract-references refs))) + (setq gnorb-gnus-message-info + `(:subject ,subject :msg-id ,msg-id + :to ,to :from ,from + :link ,link :date ,date :refs ,refs + :group ,group)) + (if org-ids + (progn + (require 'gnorb-org) + (setq gnorb-message-org-ids org-ids) + ;; `gnorb-org-setup-message' may have put this here, but + ;; if we're working from a draft, or triggering this from + ;; a reply, it might not be there yet. + (add-to-list 'message-exit-actions + 'gnorb-org-restore-after-send t)) + (setq gnorb-message-org-ids nil))))) + +(add-hook 'message-header-hook 'gnorb-gnus-check-outgoing-headers) + +;;;###autoload +(defun gnorb-gnus-outgoing-do-todo (&optional arg) + "Call this function to use the message currently being composed +as an email todo action. If it's a new message, or a reply to a +message that isn't referenced by any TODOs, a new TODO will be +created. If it references an existing TODO, you'll be prompted to +trigger a state-change or a note on that TODO. + +Otherwise, you can call it with a prefix arg to associate the +sending/sent message with an existing Org subtree, and trigger an +action on that subtree. + +If a new todo is made, it needs a capture template: set +`gnorb-gnus-new-todo-capture-key' to the string key for the +appropriate capture template. If you're using a gnus-based +archive method (ie you have `gnus-message-archive-group' set to +something, and your outgoing messages have a \"Fcc\" header), +then a real link will be made to the outgoing message, and all +the gnus-type escapes will be available (see the Info +manual (org) Template expansion section). If you don't, then the +%:subject, %:to, %:toname, %:toaddress, and %:date escapes for +the outgoing message will still be available -- nothing else will +work." + (interactive "P") + (let ((org-refile-targets gnorb-gnus-trigger-refile-targets) + (compose-marker (make-marker)) + header-ids ref-ids rel-headings gnorb-window-conf + reply-id reply-group in-reply-to) + (when arg + (setq rel-headings + (org-refile-get-location "Trigger action on" nil t)) + (setq rel-headings + (list (list (save-window-excursion + (find-file (nth 1 rel-headings)) + (goto-char (nth 3 rel-headings)) + (org-id-get-create)))))) + (if (not (eq major-mode 'message-mode)) + ;; The message is already sent, so we're relying on whatever was + ;; stored into `gnorb-gnus-message-info'. + (if arg + (progn + (push (car rel-headings) gnorb-message-org-ids) + (gnorb-org-restore-after-send)) + (setq ref-ids (plist-get gnorb-gnus-message-info :refs)) + (if ref-ids + ;; the message might be relevant to some TODO + ;; heading(s). But if there had been org-id + ;; headers, they would already have been + ;; handled when the message was sent. + (progn + (setq rel-headings (gnorb-find-visit-candidates ref-ids)) + (if (not rel-headings) + (gnorb-gnus-outgoing-make-todo-1) + (dolist (h rel-headings) + (push h gnorb-message-org-ids)) + (gnorb-org-restore-after-send))) + ;; not relevant, just make a new TODO + (gnorb-gnus-outgoing-make-todo-1))) + ;; We are still in the message composition buffer, so let's see + ;; what we've got. + + ;; What we want is a link to the original message we're replying + ;; to, if this is actually a reply. + (when message-reply-headers + (setq reply-id (aref message-reply-headers 4))) + ;; Save-excursion won't work, because point will move if we + ;; insert headings. + (move-marker compose-marker (point)) + (save-restriction + (widen) + (message-narrow-to-headers-or-head) + (setq header-ids (mail-fetch-field gnorb-mail-header nil nil t)) + ;; With a prefix arg we do not check references, because the + ;; whole point is to add new references. We still want to know + ;; what org id headers are present, though, so we don't add + ;; duplicates. + (setq ref-ids (unless arg (mail-fetch-field "References" t))) + (setq in-reply-to (unless arg (mail-fetch-field "In-Reply-to" t))) + (when in-reply-to + (setq ref-ids (concat ref-ids " " in-reply-to))) + (setq reply-group (when (mail-fetch-field "X-Draft-From" t) + (car-safe (read (mail-fetch-field "X-Draft-From" t))))) + ;; when it's a reply, store a link to the reply just in case. + ;; This is pretty embarrassing -- we follow a link just to + ;; create a link. But I'm not going to recreate all of + ;; `org-store-link' by hand. + (when (and reply-group reply-id) + (save-window-excursion + (org-gnus-follow-link reply-group reply-id) + (call-interactively 'org-store-link))) + (when ref-ids + ;; if the References header points to any message ids that are + ;; tracked by TODO headings... + (setq rel-headings (gnorb-find-visit-candidates ref-ids))) + (when rel-headings + (goto-char (point-min)) + (dolist (h (delete-dups rel-headings)) + ;; then get the org-ids of those headings, and insert + ;; them into this message as headers. If the id was + ;; already present in a header, don't add it again. + (unless (member h header-ids) + (goto-char (point-at-bol)) + (open-line 1) + (message-insert-header + (intern gnorb-mail-header) + h) + ;; tell the rest of the function that this is a relevant + ;; message + (push h header-ids))))) + (goto-char compose-marker) + (add-to-list + 'message-exit-actions + (if header-ids + 'gnorb-org-restore-after-send + 'gnorb-gnus-outgoing-make-todo-1) + t) + (message + (if header-ids + "Message will trigger TODO state-changes after sending" + "A TODO will be made from this message after it's sent"))))) + +(defun gnorb-gnus-outgoing-make-todo-1 () + (unless gnorb-gnus-new-todo-capture-key + (error "No capture template key set, customize gnorb-gnus-new-todo-capture-key")) + (let* ((link (plist-get gnorb-gnus-message-info :link)) + (group (plist-get gnorb-gnus-message-info :group)) + (date (plist-get gnorb-gnus-message-info :date)) + (date-ts (and date + (ignore-errors + (format-time-string + (org-time-stamp-format t) + (date-to-time date))))) + (date-ts-ia (and date + (ignore-errors + (format-time-string + (org-time-stamp-format t t) + (date-to-time date))))) + (msg-id (plist-get gnorb-gnus-message-info :msg-id)) + (sender (plist-get gnorb-gnus-message-info :from)) + (subject (plist-get gnorb-gnus-message-info :subject)) + ;; Convince Org we already have a link stored, even if we + ;; don't. + (org-capture-link-is-already-stored t)) + (if link + ;; Even if you make a link to not-yet-sent messages, even if + ;; you've saved the draft and it has a Date header, that + ;; header isn't saved into the link plist. So fake that, too. + (org-add-link-props + :date date + :date-timestamp date-ts + :date-timestamp-inactive date-ts-ia + :annotation link) + (org-store-link-props + :subject (plist-get gnorb-gnus-message-info :subject) + :to (plist-get gnorb-gnus-message-info :to) + :date date + :date-timestamp date-ts + :date-timestamp-inactive date-ts-ia + :message-id msg-id + :annotation link)) + (org-capture nil gnorb-gnus-new-todo-capture-key) + (when msg-id + (org-entry-put (point) gnorb-org-msg-id-key msg-id) + (gnorb-registry-make-entry msg-id sender subject (org-id-get-create) group)))) + +;;; If an incoming message should trigger state-change for a Org todo, +;;; call this function on it. + +;;;###autoload +(defun gnorb-gnus-incoming-do-todo (arg headers &optional id) + "Call this function from a received gnus message to store a +link to the message, prompt for a related Org heading, visit the +heading, and either add a note or trigger a TODO state change. +Set `gnorb-trigger-todo-default' to 'note or 'todo (you can +get the non-default behavior by calling this function with a +prefix argument), or to 'prompt to always be prompted. + +In some cases, Gnorb can guess for you which Org heading you +probably want to trigger, which can save some time. It does this +by looking in the References header, and seeing if any of the IDs +there match the value of the `gnorb-org-msg-id-key' property for +any headings. In order for this to work, you will have to have +loaded org-id, and have the variable `org-id-track-globally' set +to t (it is, by default)." + (interactive (gnus-interactive "P\nH")) + (when (not (memq major-mode '(gnus-summary-mode gnus-article-mode))) + (user-error "Only works in gnus summary or article mode")) + ;; We should only store a link if it's not already at the head of + ;; `org-stored-links'. There's some duplicate storage, at + ;; present. Take a look at calling it non-interactively. + (setq gnorb-window-conf (current-window-configuration)) + (move-marker gnorb-return-marker (point)) + (setq gnorb-gnus-message-info nil) + (let* ((msg-id (mail-header-id headers)) + (from (mail-header-from headers)) + (subject (mail-header-subject headers)) + (date (mail-header-date headers)) + (to (cdr (assoc 'To (mail-header-extra headers)))) + (group gnus-newsgroup-name) + (link (call-interactively 'org-store-link)) + (org-refile-targets gnorb-gnus-trigger-refile-targets) + (ref-msg-ids (mail-header-references headers)) + (offer-heading + (when (and (not id) ref-msg-ids gnorb-tracking-enabled) + (if org-id-track-globally + ;; for now we're basically ignoring the fact that + ;; multiple candidates could exist; just do the first + ;; one. + (car (gnorb-find-visit-candidates + ref-msg-ids)) + (message "Gnorb can't check for relevant headings unless `org-id-track-globally' is t") + (sit-for 1)))) + targ) + (setq gnorb-gnus-message-info + `(:subject ,subject :msg-id ,msg-id + :to ,to :from ,from + :link ,link :date ,date :refs ,ref-msg-ids + :group ,group)) + (gnorb-gnus-collect-all-attachments nil t) + ;; Delete other windows, users can restore with + ;; `gnorb-restore-layout'. + (delete-other-windows) + (if id + (gnorb-trigger-todo-action arg id) + (if (and offer-heading + (y-or-n-p (format "Trigger action on %s" + (gnorb-pretty-outline offer-heading)))) + (gnorb-trigger-todo-action arg offer-heading) + (setq targ (org-refile-get-location + "Trigger heading" nil t)) + (find-file (nth 1 targ)) + (goto-char (nth 3 targ)) + (gnorb-trigger-todo-action arg))))) + +;;;###autoload +(defun gnorb-gnus-search-messages (str &optional ret) + "Initiate a search for gnus message links in an org subtree. +The arg STR can be one of two things: an Org heading id value +\(IDs should be prefixed with \"id+\"\), in which case links will +be collected from that heading, or a string corresponding to an +Org tags search, in which case links will be collected from all +matching headings. + +In either case, once a collection of links have been made, they +will all be displayed in an ephemeral group on the \"nngnorb\" +server. There must be an active \"nngnorb\" server for this to +work." + (interactive) + (let ((nnir-address + (or (gnus-method-to-server '(nngnorb)) + (user-error + "Please add a \"nngnorb\" backend to your gnus installation.")))) + (when (version= "5.13" gnus-version-number) + (setq nnir-current-query nil + nnir-current-server nil + nnir-current-group-marked nil + nnir-artlist nil)) + (gnus-group-read-ephemeral-group + ;; in 24.4, the group name is mostly decorative. in 24.3, the + ;; query itself is read from there. It should look like (concat + ;; "nnir:" (prin1-to-string '((query str)))) + (if (version= "5.13" gnus-version-number) + (concat "nnir:" (prin1-to-string `((query ,str)))) + (concat "gnorb-" str)) + (if (version= "5.13" gnus-version-number) + (list 'nnir nnir-address) + (list 'nnir "nnir")) + nil + ret ;; it's possible you can't just put an arbitrary form in + ;; here, which sucks. + nil nil + ;; the following seems to simply be ignored under gnus 5.13 + (list (cons 'nnir-specs (list (cons 'nnir-query-spec `((query . ,str))) + (cons 'nnir-group-spec `((,nnir-address nil))))) + (cons 'nnir-artlist nil))) + (gnorb-summary-minor-mode))) + +;;; Automatic noticing of relevant messages + +;; likely hooks for the summary buffer include: +;; `gnus-parse-headers-hook' + +;; BBDB puts its notice stuff in the `gnus-article-prepare-hook', +;; which seems as good a spot as any. + +(defun gnorb-gnus-hint-relevant-message () + "When opening an article buffer, check the message to see if it +is relevant to any existing TODO headings. If so, flash a message +to that effect. This function is added to the +`gnus-article-prepare-hook'. It will only do anything if the +option `gnorb-gnus-hint-relevant-article' is non-nil." + (when (and gnorb-tracking-enabled + gnorb-gnus-hint-relevant-article + (not (memq (car (gnus-find-method-for-group + gnus-newsgroup-name)) + '(nnvirtual nnir)))) + (let* ((ref-ids (concat + (gnus-fetch-original-field "references") " " + (gnus-fetch-original-field "in-reply-to"))) + (msg-id (gnus-fetch-original-field "message-id")) + (assoc-heading + (gnus-registry-get-id-key msg-id 'gnorb-ids)) + (key + (where-is-internal 'gnorb-gnus-incoming-do-todo + nil t)) + rel-headings) + (cond (assoc-heading + (message "Message is associated with %s" + (gnorb-pretty-outline (car assoc-heading) t))) + (ref-ids + (when (setq rel-headings + (gnorb-find-visit-candidates ref-ids)) + (message "Possible relevant todo %s, trigger with %s" + (gnorb-pretty-outline (car rel-headings) t) + (if key + (key-description key) + "M-x gnorb-gnus-incoming-do-todo")))))))) + +(add-hook 'gnus-article-prepare-hook 'gnorb-gnus-hint-relevant-message) + +(defun gnorb-gnus-insert-format-letter-maybe (header) + (if (and gnorb-tracking-enabled + (not (memq (car (gnus-find-method-for-group + gnus-newsgroup-name)) + '(nnvirtual nnir)))) + (let ((ref-ids (mail-header-references header)) + (msg-id (mail-header-message-id header))) + (if (or (gnus-registry-get-id-key msg-id 'gnorb-ids) + (and ref-ids + (gnorb-find-visit-candidates ref-ids))) + gnorb-gnus-summary-mark + " ")) + " ")) + +(fset (intern (concat "gnus-user-format-function-" + gnorb-gnus-summary-mark-format-letter)) + (lambda (header) + (gnorb-gnus-insert-format-letter-maybe header))) + +;;;###autoload +(defun gnorb-gnus-view () + "Display the first relevant TODO heading for the message under point" + ;; this is pretty barebones, need to make sure we have a valid + ;; article buffer to access, and think about what to do for + ;; window-configuration! + + ;; boy is this broken now. + (interactive) + (let ((refs (gnus-fetch-original-field "references")) + rel-headings) + (when refs + (setq rel-headings (gnorb-find-visit-candidates refs)) + (delete-other-windows) + (org-id-goto (car rel-headings))))) + +(provide 'gnorb-gnus) +;;; gnorb-gnus.el ends here diff --git a/gnorb-org.el b/gnorb-org.el new file mode 100644 index 000000000..bc46eda7a --- /dev/null +++ b/gnorb-org.el @@ -0,0 +1,678 @@ +;;; gnorb-org.el --- The Org-centric functions of gnorb + +;; Copyright (C) 2014 Eric Abrahamsen + +;; Author: Eric Abrahamsen +;; Keywords: + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; + +;;; Code: + +(require 'gnorb-utils) + +(defgroup gnorb-org nil + "The Org bits of Gnorb." + :tag "Gnorb Org" + :group 'gnorb) + +(defcustom gnorb-org-after-message-setup-hook nil + "Hook run in a message buffer after setting up the message from + `gnorb-org-handle-mail' or `gnorb-org-email-subtree'." + :group 'gnorb-org + :type 'hook) + +(defcustom gnorb-org-trigger-actions + '(("todo state" . todo) + ("take note" . note) + ("don't associate" . no-associate) + ("only associate" . associate) +; ("capture to child" . cap-child) +; ("capture to sibling" . cap-sib) +) + "List of potential actions that can be taken on headings. + +When triggering an Org heading after receiving or sending a +message, this option lists the possible actions to take. Built-in +actions include: + +todo state: Associate the message, and change TODO state. +take note: Associate the message, and take a note. +don't associate: Do nothing at all, don't connect the message and TODO. +only associate: Associate the message with this heading, do nothing else. +capture to child: [not yet implemented] Associate this message with a new child heading. +capture to sibling: [not yet implemented] Associate this message with a new sibling heading. + +You can reorder this list or remove items as suits your workflow. +The two \"capture\" options will use the value of +`gnorb-gnus-new-todo-capture-key' to find the appropriate +template. + +You can also add custom actions to the list. Actions should be a +cons of a string tag and a symbol indicating a custom function. +This function will be called on the heading in question, and +passed a plist containing information about the message from +which we're triggering." + :group 'gnorb-org + :type 'list) + +(defcustom gnorb-org-msg-id-key "GNORB_MSG_ID" + "The name of the org property used to store the Message-IDs + from relevant messages. This is no longer used, and will be + removed soon." + :group 'gnorb-org + :type 'string) + +(defcustom gnorb-org-mail-scan-scope 2 + "Number of paragraphs to scan for mail-related links. + +When handling a TODO heading with `gnorb-org-handle-mail', Gnorb +will typically reply to the most recent message associated with +this heading. If there are no such messages, or message tracking +is disabled entirely, or `gnorb-org-handle-mail' has been called +with a prefix arg, the heading and body text of the subtree under +point will instead be scanned for gnus:, mailto:, and bbdb: +links. This option controls how many paragraphs of body text to +scan. Set to 0 to only look in the heading.") + +(make-obsolete-variable + 'gnorb-org-mail-scan-strategies + "This variable has been superseded by `gnorb-org-trigger-actions'" + "September 12, 2014" 'set) + +(make-obsolete-variable + 'gnorb-org-mail-scan-state-changes + "This variable has been superseded by `gnorb-org-trigger-actions'" + "September 12, 2014" 'set) + +(make-obsolete-variable + 'gnorb-org-mail-scan-function + "This variable has been superseded by `gnorb-org-trigger-actions'" + "September 12, 2014" 'set) + +(defcustom gnorb-org-find-candidates-match nil + "When scanning all org files for heading related to an incoming +message, this option will limit which headings will be offered as +target candidates. Specifically it will be used as the second +argument to `org-map-entries', and syntax is the same as that +used in an agenda tags view." + :group 'gnorb-org + :type 'symbol) + +;;;###autoload +(defun gnorb-org-contact-link (rec) + "Prompt for a BBDB record and insert a link to that record at +point. + +There's really no reason to use this instead of regular old +`org-insert-link' with BBDB completion. But there might be in the +future!" + ;; this needs to handle an active region. + (interactive (list (gnorb-prompt-for-bbdb-record))) + (let* ((name (bbdb-record-name rec)) + (link (concat "bbdb:" (org-link-escape name)))) + (org-store-link-props :type "bbdb" :name name + :link link :description name) + (if (called-interactively-p 'any) + (insert (format "[[%s][%s]]" link name)) + link))) + +(defun gnorb-org-restore-after-send () + "After an email is sent, clean up the gnus summary buffer, put +us back where we came from, and go through all the org ids that +might have been in the outgoing message's headers and call +`gnorb-trigger-todo-action' on each one." + (delete-other-windows) + (dolist (id gnorb-message-org-ids) + (org-id-goto id) + (org-reveal) + (gnorb-trigger-todo-action nil id)) + ;; this is a little unnecessary, but it may save grief + (setq gnorb-gnus-message-info nil) + (setq gnorb-message-org-ids nil)) + +(defun gnorb-org-extract-links (&optional arg region) + "See if there are viable links in the subtree under point." + ;; We're not currently using the arg. What could we do with it? + (let (strings) + ;; If the region was active, only use the region + (if region + (push (buffer-substring (car region) (cdr region)) + strings) + ;; Otherwise collect the heading text, and all the paragraph + ;; text. + (save-restriction + (org-narrow-to-subtree) + (let ((head (org-element-at-point)) + (tree (org-element-parse-buffer))) + (push (org-element-property + :raw-value + head) + strings) + (org-element-map tree 'paragraph + (lambda (p) + (push (org-element-interpret-data p) + strings)) + nil nil 'drawer)))) + (when strings + ;; Limit number of paragraphs based on + ;; `gnorb-org-mail-scan-scope' + (setq strings + (cond ((eq gnorb-org-mail-scan-scope 'all) + strings) + ((numberp gnorb-org-mail-scan-scope) + (delq nil + (subseq + strings 0 (1+ gnorb-org-mail-scan-scope)))) + ;; We could provide more options here. 'tree vs + ;; 'subtree, for instance. + (t + strings))) + (with-temp-buffer + (dolist (s strings) + (insert s) + (insert "\n")) + (goto-char (point-min)) + (gnorb-scan-links (point-max) 'gnus 'mail 'bbdb))))) + +(defun gnorb-org-extract-mail-stuff (&optional arg region) + "Decide how to hande the Org heading under point as an email task. + +See the docstring of `gnorb-org-handle-mail' for details." + (if (or (not gnorb-tracking-enabled) + region) + (gnorb-org-extract-links arg region) + ;; Get all the messages associated with the IDS in this subtree. + (let ((assoc-msg-ids + (delete-dups + (cl-mapcan + (lambda (id) + (gnorb-registry-org-id-search id)) + (gnorb-collect-ids))))) + (gnorb-org-extract-mail-tracking assoc-msg-ids arg region)))) + +(defun gnorb-org-extract-mail-tracking (assoc-msg-ids &optional arg region) + + (let* ((all-links (gnorb-org-extract-links nil region)) + ;; The latest (by the creation-time registry key) of all the + ;; tracked messages that were not sent by our user. + (latest-msg-id + (when assoc-msg-ids + (car + (sort + (remove-if + (lambda (m) + (let ((from (car (gnus-registry-get-id-key m 'sender)))) + (or (null from) + (string-match-p + user-mail-address from) + (string-match-p + message-alternative-emails from)))) + assoc-msg-ids) + (lambda (r l) + (time-less-p + (car (gnus-registry-get-id-key l 'creation-time)) + (car (gnus-registry-get-id-key r 'creation-time))))))))) + (cond + ;; If there are no tracked messages, or the user has specifically + ;; requested we ignore them with the prefix arg, just return the + ;; found links in the subtree. + ((or arg + (null latest-msg-id)) + all-links) + ;; Otherwise ignore the other links in the subtree, and return + ;; the latest message. + (latest-msg-id + `(:gnus ,(list (gnorb-msg-id-to-link latest-msg-id))))))) + +(defun gnorb-org-setup-message + (&optional messages mails from cc bcc attachments text ids) + "Common message setup routine for other gnorb-org commands. +MESSAGES is a list of gnus links pointing to messages -- we +currently only use the first of the list. MAILS is a list of +email address strings suitable for inserting in the To header. +ATTACHMENTS is a list of filenames to attach. TEXT is a string or +buffer, which is inserted in the message body. IDS is one or more +Org heading ids, associating the outgoing message with those +headings." + (require 'gnorb-gnus) + (if (not messages) + ;; Either compose new message... + (compose-mail (mapconcat 'identity mails ", ")) + ;; ...or follow link and start reply. + (condition-case err + (let ((ret-val (org-gnus-open (org-link-unescape (car messages))))) + ;; We failed to open the link (probably), ret-val would be + ;; t otherwise + (when (stringp ret-val) + (error ret-val)) + (call-interactively + 'gnus-summary-wide-reply-with-original) + ;; Add MAILS to message To header. + (when mails + (message-goto-to) + (insert ", ") + (insert (mapconcat 'identity mails ", ")))) + (error (when (and (window-configuration-p gnorb-window-conf) + gnorb-return-marker) + (set-window-configuration gnorb-window-conf) + (goto-char gnorb-return-marker)) + (signal (car err) (cdr err))))) + ;; Return us after message is sent. + (add-to-list 'message-exit-actions + 'gnorb-org-restore-after-send t) + ;; Set headers from MAIL_* properties (from, cc, and bcc). + (cl-flet ((sh (h) + (when (cdr h) + (funcall (intern (format "message-goto-%s" (car h)))) + (let ((message-beginning-of-line t) + (show-trailing-whitespace t)) + (message-beginning-of-line) + (unless (bolp) + (kill-line)) + (insert (cdr h)))))) + (dolist (h `((from . ,from) (cc . ,cc) (bcc . ,bcc))) + (sh h))) + ;; attach ATTACHMENTS + (map-y-or-n-p + (lambda (a) (format "Attach %s to outgoing message? " + (file-name-nondirectory a))) + (lambda (a) + (mml-attach-file a (mm-default-file-encoding a) + nil "attachment")) + attachments + '("file" "files" "attach")) + ;; insert text, if any + (when text + (message-goto-body) + (insert"\n") + (if (bufferp text) + (insert-buffer-substring text) + (insert text))) + ;; insert org ids, if any + (when ids + (unless (listp ids) + (setq ids (list ids))) + (save-excursion + (save-restriction + (message-narrow-to-headers) + (dolist (i ids) + (goto-char (point-at-bol)) + (open-line 1) + ;; this function hardly does anything + (message-insert-header + (intern gnorb-mail-header) i))))) + ;; put point somewhere reasonable + (if (or mails messages) + (if (not messages) + (message-goto-subject) + (message-goto-body)) + (message-goto-to)) + (run-hooks 'gnorb-org-after-message-setup-hook)) + +(defun gnorb-org-attachment-list (&optional id) + "Get a list of files (absolute filenames) attached to the +current heading, or the heading indicated by optional argument ID." + (when (featurep 'org-attach) + (let* ((attach-dir (save-excursion + (when id + (org-id-goto id)) + (org-attach-dir t))) + (files + (mapcar + (lambda (f) + (expand-file-name f attach-dir)) + (org-attach-file-list attach-dir)))) + files))) + +;;;###autoload +(defun gnorb-org-handle-mail (&optional arg text file) + "Handle current headline as a mail TODO. + +How this function behaves depends on whether you're using Gnorb +for email tracking, also on the prefix arg, and on the active +region. + +If tracking is enabled and there is no prefix arg, Gnorb will +begin a reply to the newest associated message that wasn't sent +by the user -- ie, the Sender header doesn't match +`user-mail-address' or `message-alternative-emails'. + +If tracking is enabled and there is a prefix arg, ignore the +tracked messages and instead scan the subtree for mail-related +links. This means links prefixed with gnus:, mailto:, or bbdb:. +See `gnorb-org-mail-scan-scope' to limit the scope of this scan. +Do something appropriate with the resulting links. + +With a double prefix arg, ignore all tracked messages and all +links, and compose a blank new message. + +If tracking is enabled and you want to reply to a +specific (earlier) message in the tracking history, use +`gnorb-org-view' to open an nnir *Summary* buffer containing all +the messages, and reply to the one you want. Your reply will be +automatically tracked, as well. + +If tracking is not enabled and you want to use a specific link in +the subtree as a basis for the email action, then put the region +around that link before you call this message." + (interactive "P") + (setq gnorb-window-conf (current-window-configuration)) + (move-marker gnorb-return-marker (point)) + (when (eq major-mode 'org-agenda-mode) + ;; If this is all the different types, we could skip the check. + (org-agenda-check-type t 'agenda 'timeline 'todo 'tags 'search) + (org-agenda-check-no-diary) + (let* ((marker (or (org-get-at-bol 'org-hd-marker) + (org-agenda-error))) + (buffer (marker-buffer marker)) + (pos (marker-position marker))) + (switch-to-buffer buffer) + (widen) + (goto-char pos))) + (let ((region + (when (use-region-p) + (cons (region-beginning) (region-end))))) + (deactivate-mark) + (save-excursion + (unless (org-back-to-heading t) + (error "Not in an org item")) + (cl-flet ((mp (p) (org-entry-get (point) p t))) + ;; Double prefix means ignore everything and compose a blank + ;; mail. + (let* ((links (unless (equal arg '(16)) + (gnorb-org-extract-mail-stuff arg region))) + (attachments (gnorb-org-attachment-list)) + (from (mp "MAIL_FROM")) + (cc (mp "MAIL_CC")) + (bcc (mp "MAIL_BCC")) + (org-id (org-id-get-create)) + (recs (plist-get links :bbdb)) + (message-mode-hook (copy-sequence message-mode-hook)) + mails) + (when file + (cons file attachments)) + (when recs + (setq recs + (delq nil + (mapcar + (lambda (r) + (car (bbdb-message-search + (org-link-unescape r) + nil))) + recs)))) + (when recs + (dolist (r recs) + (push (bbdb-mail-address r) mails))) + (when (and recs + gnorb-bbdb-posting-styles) + (add-hook 'message-mode-hook + (lambda () + (gnorb-bbdb-configure-posting-styles (cdr recs)) + (gnorb-bbdb-configure-posting-styles (list (car recs)))))) + (gnorb-org-setup-message + (plist-get links :gnus) + (append mails (plist-get links :mail)) + from cc bcc + attachments text org-id)))))) + +;;; Email subtree + +(defcustom gnorb-org-email-subtree-text-parameters nil + "A plist of export parameters corresponding to the EXT-PLIST + argument to the export functions, for use when exporting to + text." + :group 'gnorb-org + :type 'boolean) + +(defcustom gnorb-org-email-subtree-file-parameters nil + "A plist of export parameters corresponding to the EXT-PLIST + argument to the export functions, for use when exporting to a + file." + :group 'gnorb-org + :type 'boolean) + +(defcustom gnorb-org-email-subtree-text-options '(nil t nil t) + "A list of ts and nils corresponding to Org's export options, +to be used when exporting to text. The options, in order, are +async, subtreep, visible-only, and body-only." + :group 'gnorb-org + :type 'list) + +(defcustom gnorb-org-email-subtree-file-options '(nil t nil nil) + "A list of ts and nils corresponding to Org's export options, +to be used when exporting to a file. The options, in order, are +async, subtreep, visible-only, and body-only." + :group 'gnorb-org + :type 'list) + +(defcustom gnorb-org-export-extensions + '((latex ".tex") + (ascii ".txt") + (html ".html") + (org ".org") + (icalendar ".ics") + (man ".man") + (md ".md") + (odt ".odt") ; not really, though + (texinfo ".texi") + (beamer ".tex")) + "Correspondence between export backends and their +respective (usual) file extensions. Ugly way to do it, but what +the hey..." + :group 'gnorb-org) + +;;;###autoload +(defun gnorb-org-email-subtree (&optional arg) + "Call on a subtree to export it either to a text string or a file, +then compose a mail message either with the exported text +inserted into the message body, or the exported file attached to +the message. + +Export options default to the following: When exporting to a +buffer: async = nil, subtreep = t, visible-only = nil, body-only += t. Options are the same for files, except body-only is set to +nil. Customize `gnorb-org-email-subtree-text-options' and +`gnorb-org-email-subtree-file-options', respectively. + +Customize `gnorb-org-email-subtree-parameters' to your preferred +default set of parameters." + ;; I sure would have liked to use the built-in dispatch ui, but it's + ;; got too much hard-coded stuff. + (interactive "P") + (org-back-to-heading t) + (let* ((backend-string + (org-completing-read + "Export backend: " + (mapcar (lambda (b) + (symbol-name (org-export-backend-name b))) + org-export--registered-backends) nil t)) + (backend-symbol (intern backend-string)) + (f-or-t (org-completing-read "Export as file or text? " + '("file" "text") nil t)) + (org-export-show-temporary-export-buffer nil) + (opts (if (equal f-or-t "text") + gnorb-org-email-subtree-text-options + gnorb-org-email-subtree-file-options)) + (result + (if (equal f-or-t "text") + (apply 'org-export-to-buffer + `(,backend-symbol + "*Gnorb Export*" + ,@opts + ,gnorb-org-email-subtree-text-parameters)) + (apply 'org-export-to-file + `(,backend-symbol + ,(org-export-output-file-name + (second (assoc backend-symbol gnorb-org-export-extensions)) + t gnorb-tmp-dir) + ,@opts + ,gnorb-org-email-subtree-file-parameters)))) + text file) + (setq gnorb-window-conf (current-window-configuration)) + (move-marker gnorb-return-marker (point)) + (if (bufferp result) + (setq text result) + (setq file result)) + (gnorb-org-handle-mail arg text file))) + +(defcustom gnorb-org-capture-collect-link-p t + "Should the capture process store a link to the gnus message or + BBDB record under point, even if it's not part of the template? + You'll probably end up needing it, anyway." + :group 'gnorb-org) + +(defun gnorb-org-capture-collect-link () + (when gnorb-org-capture-collect-link-p + (let ((buf (org-capture-get :original-buffer))) + (when buf + (with-current-buffer buf + (when (memq major-mode '(gnus-summary-mode + gnus-article-mode + bbdb-mode)) + (call-interactively 'org-store-link))))))) + +(add-hook 'org-capture-mode-hook 'gnorb-org-capture-collect-link) + +;;; Agenda/BBDB popup stuff + +(defcustom gnorb-org-agenda-popup-bbdb nil + "Should Agenda tags search pop up a BBDB buffer with matching + records? + +Records are considered matching if they have an `org-tags' field +matching the current Agenda search. The name of that field can be +customized with `gnorb-bbdb-org-tag-field'." + :group 'gnorb-org) + +(defcustom gnorb-org-bbdb-popup-layout 'pop-up-multi-line + "Default BBDB buffer layout for automatic Org Agenda display." + :group 'gnorb-org + :type '(choice (const one-line) + (const multi-line) + (const full-multi-line) + (symbol))) + +;;;###autoload +(defun gnorb-org-popup-bbdb (&optional str) + "In an `org-tags-view' Agenda buffer, pop up a BBDB buffer +showing records whose `org-tags' field matches the current tags +search." + ;; I was hoping to use `org-make-tags-matcher' directly, then snag + ;; the tagmatcher from the resulting value, but there doesn't seem + ;; to be a reliable way of only getting the tag-related returns. But + ;; I'd still like to use that function. So an ugly hack to first + ;; remove non-tag contents from the query string, and then make a + ;; new call to `org-make-tags-matcher'. + (interactive) + (require 'gnorb-bbdb) + (let (recs) + (cond ((and + (and (eq major-mode 'org-agenda-mode) + (eq org-agenda-type 'tags)) + (or (called-interactively-p 'any) + gnorb-org-agenda-popup-bbdb)) + (let ((todo-only nil) + (str (or str org-agenda-query-string)) + (re "^&?\\([-+:]\\)?\\({[^}]+}\\|LEVEL\\([<=>]\\{1,2\\}\\)\\([0-9]+\\)\\|\\(\\(?:[[:alnum:]_]+\\(?:\\\\-\\)*\\)+\\)\\([<>=]\\{1,2\\}\\)\\({[^}]+}\\|\"[^\"]*\"\\|-?[.0-9]+\\(?:[eE][-+]?[0-9]+\\)?\\)\\|[[:alnum:]_@#%]+\\)") + or-terms term rest out-or acc tag-clause) + (setq or-terms (org-split-string str "|")) + (while (setq term (pop or-terms)) + (setq acc nil) + (while (string-match re term) + (setq rest (substring term (match-end 0))) + (let ((sub-term (match-string 0 term))) + (unless (save-match-data ; this isn't a tag, don't want it + (string-match "\\([<>=]\\)" sub-term)) + (push sub-term acc)) + (setq term rest))) + (push (mapconcat 'identity (nreverse acc) "") out-or)) + (setq str (mapconcat 'identity (nreverse out-or) "|")) + (setq tag-clause (cdr (org-make-tags-matcher str))) + (unless (equal str "") + (setq recs + (remove-if-not + (lambda (r) + (let ((rec-tags (bbdb-record-xfield + r gnorb-bbdb-org-tag-field))) + (and rec-tags + (let ((tags-list (org-split-string rec-tags ":")) + (case-fold-search t) + (org-trust-scanner-tags t)) + (eval tag-clause))))) + (bbdb-records)))))) + ((eq major-mode 'org-mode) + (save-excursion + (org-back-to-heading) + (let ((bound (org-element-property + :end (org-element-at-point))) + desc rec) + (while (re-search-forward + org-bracket-link-analytic-regexp bound t) + (when (string-match-p "bbdb" (match-string 2)) + (setq desc (match-string 5) + rec (bbdb-search (bbdb-records) desc desc desc) + recs (append recs rec)))))))) + (if recs + (bbdb-display-records + recs gnorb-org-bbdb-popup-layout) + (when (get-buffer-window bbdb-buffer-name) + (quit-window nil + (get-buffer-window bbdb-buffer-name))) + (when (called-interactively-p 'any) + (message "No relevant BBDB records"))))) + +(if (featurep 'gnorb-bbdb) + (add-hook 'org-agenda-finalize-hook 'gnorb-org-popup-bbdb)) + +;;; Groups from the gnorb gnus server backend + +;;;###autoload +(defun gnorb-org-view () + "Search the subtree at point for links to gnus messages, and +then show them in an ephemeral group, in gnus. + +This won't work unless you've added a \"nngnorb\" server to +your gnus select methods." + ;; this should also work on the active region, if there is one. + (interactive) + (setq gnorb-window-conf (current-window-configuration)) + (move-marker gnorb-return-marker (point)) + (when (eq major-mode 'org-agenda-mode) + (org-agenda-check-type t 'agenda 'timeline 'todo 'tags) + (org-agenda-check-no-diary) + (let* ((marker (or (org-get-at-bol 'org-hd-marker) + (org-agenda-error))) + (buffer (marker-buffer marker)) + (pos (marker-position marker))) + (switch-to-buffer buffer) + (goto-char pos) + (org-reveal))) + (let (id) + (save-excursion + (org-back-to-heading) + (setq id (concat "id+" (org-id-get-create)))) + (gnorb-gnus-search-messages + id + `(when (and (window-configuration-p gnorb-window-conf) + gnorb-return-marker) + (set-window-configuration gnorb-window-conf) + (goto-char gnorb-return-marker))))) + +(provide 'gnorb-org) +;;; gnorb-org.el ends here diff --git a/gnorb-registry.el b/gnorb-registry.el new file mode 100644 index 000000000..0eee32ccd --- /dev/null +++ b/gnorb-registry.el @@ -0,0 +1,194 @@ +;;; gnorb-registry.el --- Registry implementation for Gnorb + +;; This file is in the public domain. + +;; Author: Eric Abrahamsen + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: + +;; Early on, Gnorb's message/todo tracking was done by relying on the +;; user to insert links to received messages into an Org heading, and +;; by automatically storing the Message-Ids of sent messages in a +;; property (`gnorb-org-msg-id-key', defaulting to GNORB_MSG_ID) on +;; the same heading. The heading could find all relevant messages by +;; combining the links (incoming) and the IDs of the Gnorb-specific +;; property (outgoing). +;; +;; In the end, this proved to be fragile and messy. Enter the +;; registry. The Gnus registry is a specialization of a general +;; "registry" library -- it's possible to roll your own. If you want +;; to track connections between messages and Org headings, it's an +;; obvious choice: Each relevant message is stored in the registry, +;; keyed on its Message-ID, and the org-ids of all relevant headings +;; are stored in a custom property, in our case gnorb-ids. This allows +;; us to keep all Gnorb-specific data in one place, without polluting +;; Org files or Gnus messages, persistent on disk, and with the added +;; bonus of providing a place to keep arbitrary additional metadata. +;; +;; The drawback is that the connections are no longer readily visible +;; to the user (they need to query the registry to see them), and it +;; becomes perhaps a bit more difficult (but only a bit) to keep +;; registry data in sync with the current state of the user's Gnus and +;; Org files. But a clear win, in the end. + +;;; Code: + +(require 'gnus-registry) + +(defgroup gnorb-registry nil + "Gnorb's use of the Gnus registry." + :tag "Gnorb Registry" + :group 'gnorb) + +(defun gnorb-registry-make-entry (msg-id sender subject org-id group) + "Create a Gnus registry entry for a message, either received or +sent. Save the relevant Org ids in the 'gnorb-ids key." + ;; This set-id-key stuff is actually horribly + ;; inefficient. + (when gnorb-tracking-enabled + (gnus-registry-get-or-make-entry msg-id) + (when sender + (gnus-registry-set-id-key msg-id 'sender (list sender))) + (when subject + (gnus-registry-set-id-key msg-id 'subject (list subject))) + (when org-id + (let ((ids (gnus-registry-get-id-key msg-id 'gnorb-ids))) + (unless (member org-id ids) + (gnus-registry-set-id-key msg-id 'gnorb-ids (if (stringp org-id) + (cons org-id ids) + (append org-id ids)))))) + (when group + (gnus-registry-set-id-key msg-id 'group (list group))) + (gnus-registry-get-or-make-entry msg-id))) + +(defun gnorb-registry-capture () + "When capturing from a Gnus message, add our new Org heading id +to the message's registry entry, under the 'gnorb-ids key." + (when (and (with-current-buffer + (org-capture-get :original-buffer) + (memq major-mode '(gnus-summary-mode gnus-article-mode))) + (not org-note-abort)) + (let* ((msg-id + (format "<%s>" (plist-get org-store-link-plist :message-id))) + (entry (gnus-registry-get-or-make-entry msg-id)) + (org-ids + (gnus-registry-get-id-key msg-id 'gnorb-ids)) + (new-org-id (org-id-get-create))) + (plist-put org-capture-plist :gnorb-id new-org-id) + (setq org-ids (cons new-org-id org-ids)) + (setq org-ids (delete-dups org-ids)) + (gnus-registry-set-id-key msg-id 'gnorb-ids org-ids)))) + + +(defun gnorb-registry-capture-abort-cleanup () + (when (and (org-capture-get :gnorb-id) + org-note-abort) + (condition-case error + (let* ((msg-id (format "<%s>" (plist-get org-store-link-plist :message-id))) + (existing-org-ids (gnus-registry-get-id-key msg-id 'gnorb-ids)) + (org-id (org-capture-get :gnorb-id))) + (when (member org-id existing-org-ids) + (gnus-registry-set-id-key msg-id 'gnorb-ids + (remove org-id existing-org-ids))) + (setq abort-note 'clean)) + (error + (setq abort-note 'dirty))))) + +(defun gnorb-find-visit-candidates (ids) + "For all message-ids in IDS (which should be a list of +Message-ID strings, with angle brackets, or a single string of +Message-IDs), produce a list of Org ids for headings that are +relevant to that message." + (let (ret-val sub-val) + (when (stringp ids) + (setq ids (gnus-extract-references ids))) + (when gnorb-tracking-enabled + (setq ids (delete-dups ids)) + (progn + (dolist (id ids) + (when + (setq sub-val + (gnus-registry-get-id-key id 'gnorb-ids)) + (setq ret-val (append sub-val ret-val)))))) + (delete-dups ret-val))) + +(defun gnorb-registry-org-id-search (id) + "Find all messages that have the org ID in their 'gnorb-ids +key." + (registry-search gnus-registry-db :member `((gnorb-ids ,id)))) + +(defun gnorb-registry-transition-from-props (arg) + "Helper function for transitioning the old tracking system to the new. + +The old system relied on storing sent message ids on relevant Org +headings, in the `gnorb-org-msg-id-key' property. The new system +uses the gnus registry to track relations between messages and +Org headings. This function will go through your agenda files, +find headings that have the `gnorb-org-msg-id-key' property set, +and create new registry entries that reflect that connection. + +Call with a prefix arg to additionally delete the +`gnorb-org-msg-id-key' altogether from your Org headings. As this +function will not create duplicate registry entries, it's safe to +run it once with no prefix arg, to keep the properties in place, +and then once you're sure everything's working okay, run it again +with a prefix arg, to clean the Gnorb-specific properties from +your Org files." + (interactive "P") + (let ((count 0)) + (message "Collecting all relevant Org headings, this could take a while...") + (org-map-entries + (lambda () + (let ((id (org-id-get)) + (props (org-entry-get-multivalued-property + (point) gnorb-org-msg-id-key)) + links group id) + (when props + ;; If the property is set, we should probably assume that any + ;; Gnus links in the subtree are relevant, and should also be + ;; collected and associated. + (setq links (gnorb-scan-links + (org-element-property :end (org-element-at-point)) + 'gnus)) + (dolist (l (plist-get links :gnus)) + (gnorb-registry-make-entry + (second (split-string l "#")) nil nil + id (first (split-string l "#")))) + (dolist (p props) + (setq id ) + (gnorb-registry-make-entry p nil nil id nil) + ;; This function will try to find the group for the message + ;; and set that value on the registry entry if it can find + ;; it. + (unless (gnus-registry-get-id-key p 'group) + (gnorb-msg-id-to-group p)) + (incf count))))) + gnorb-org-find-candidates-match + 'agenda 'archive 'comment) + (message "Collecting all relevant Org headings, this could take a while... done") + ;; Delete the properties if the user has asked us to do so. + (if (equal arg '(4)) + (progn + (dolist (f (org-agenda-files)) + (with-current-buffer (get-file-buffer f) + (org-delete-property-globally gnorb-org-msg-id-key))) + (message "%d entries created; all Gnorb-specific properties deleted." + count)) + (message "%d entries created." count)))) + +(provide 'gnorb-registry) diff --git a/gnorb-utils.el b/gnorb-utils.el new file mode 100644 index 000000000..68fe6b670 --- /dev/null +++ b/gnorb-utils.el @@ -0,0 +1,311 @@ +;;; gnorb-utils.el --- Common utilities for all gnorb stuff. + +;; Copyright (C) 2014 Eric Abrahamsen + +;; Author: Eric Abrahamsen +;; Keywords: + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; + +;;; Code: + +(require 'cl) +(require 'mailcap) +(require 'gnus) +;(require 'message) +(require 'bbdb) +(require 'org) +(require 'org-bbdb) +(require 'org-gnus) + +(mailcap-parse-mimetypes) + +(defgroup gnorb nil + "Glue code between Gnus, Org, and BBDB." + :tag "Gnorb") + +(make-obsolete-variable + 'gnorb-trigger-todo-default + "This variable has been superseded by +`gnorb-org-trigger-actions'" + "September 8, 2014" 'set) + +(defun gnorb-prompt-for-bbdb-record () + "Prompt the user for a BBDB record." + (let ((recs (bbdb-records)) + name) + (while (> (length recs) 1) + (setq name + (completing-read + (format "Filter records by regexp (%d remaining): " + (length recs)) + (mapcar 'bbdb-record-name recs))) + (setq recs (bbdb-search recs name name name nil nil))) + (if recs + (car recs) + (error "No matching records")))) + +(defvar gnorb-tmp-dir (make-temp-file "emacs-gnorb" t) + "Temporary directory where attachments etc are saved.") + +(defvar gnorb-message-org-ids nil + "List of Org heading IDs from the outgoing Gnus message, used + to mark mail TODOs as done once the message is sent." + ;; The send hook either populates this, or sets it to nil, depending + ;; on whether the message in question has an Org id header. Then + ;; `gnorb-org-restore-after-send' checks for it and acts + ;; appropriately, then sets it to nil. + ) + +(defvar gnorb-window-conf nil + "Save window configurations here, for restoration after mails +are sent, or Org headings triggered.") + +(defvar gnorb-return-marker (make-marker) + "Return point here after various actions, to be used together +with `gnorb-window-conf'.") + +(defcustom gnorb-mail-header "X-Org-ID" + "Name of the mail header used to store the ID of a related Org + heading. Only used locally: always stripped when the mail is + sent." + :group 'gnorb + :type 'string) + +;;; this is just ghastly, but the value of this var is single regexp +;;; group containing various header names, and we want our value +;;; inside that group. +(eval-after-load 'message + `(let ((ign-headers-list + (split-string message-ignored-mail-headers + "|")) + (our-val (concat gnorb-mail-header "\\"))) + (unless (member our-val ign-headers-list) + (setq ign-headers-list + `(,@(butlast ign-headers-list 1) ,our-val + ,@(last ign-headers-list 1))) + (setq message-ignored-mail-headers + (mapconcat + 'identity ign-headers-list "|"))))) + +(defun gnorb-restore-layout () + "Restore window layout and value of point after a Gnorb command. + +Some Gnorb commands change the window layout (ie `gnorb-org-view' +or incoming email triggering). This command restores the layout +to what it was. Bind it to a global key, or to local keys in Org +and Gnus and BBDB maps." + (interactive) + (when (window-configuration-p gnorb-window-conf) + (set-window-configuration gnorb-window-conf) + (when (buffer-live-p (marker-buffer gnorb-return-marker)) + (goto-char gnorb-return-marker)))) + +(defun gnorb-trigger-todo-action (arg &optional id) + "Do the actual restore action. Two main things here. First: if +we were in the agenda when this was called, then keep us in the +agenda. Then let the user choose an action from the value of +`gnorb-org-trigger-actions'." + (let ((agenda-p (eq major-mode 'org-agenda-mode)) + (action (cdr (assoc + (org-completing-read + "Action to take: " + gnorb-org-trigger-actions nil t) + gnorb-org-trigger-actions))) + (root-marker (make-marker))) + ;; Place the marker for the relevant TODO heading. + (cond (agenda-p + (setq root-marker + (copy-marker + (org-get-at-bol 'org-hd-marker)))) + ((derived-mode-p 'org-mode) + (move-marker root-marker (point-at-bol))) + (id + (save-excursion + (org-id-goto id) + (move-marker root-marker (point-at-bol))))) + ;; Query about attaching email attachments. + (org-with-point-at root-marker + (map-y-or-n-p + (lambda (a) + (format "Attach %s to heading? " + (file-name-nondirectory a))) + (lambda (a) (org-attach-attach a nil 'mv)) + gnorb-gnus-capture-attachments + '("file" "files" "attach"))) + (setq gnorb-gnus-capture-attachments nil) + (cl-labels + ((make-entry + (id) + (gnorb-registry-make-entry + (plist-get gnorb-gnus-message-info :msg-id) + (plist-get gnorb-gnus-message-info :from) + (plist-get gnorb-gnus-message-info :subject) + id + (plist-get gnorb-gnus-message-info :group)))) + ;; Handle our action. + (cond ((eq action 'note) + (org-with-point-at root-marker + (make-entry (org-id-get-create)) + (call-interactively 'org-add-note))) + ((eq action 'todo) + (if agenda-p + (progn + (org-with-point-at root-marker + (make-entry (org-id-get-create))) + (call-interactively 'org-agenda-todo)) + (org-with-point-at root-marker + (make-entry (org-id-get-create)) + (call-interactively 'org-todo)))) + ((eq action 'no-associate) + nil) + ((eq action 'associate) + (org-with-point-at root-marker + (make-entry (org-id-get-create)))) + ((fboundp action) + (org-with-point-at root-marker + (make-entry (org-id-get-create)) + (funcall action gnorb-gnus-message-info))))))) + +(defun gnorb-pretty-outline (id &optional kw) + "Return pretty outline path of the Org heading indicated by ID. + +If the KW argument is true, add the TODO keyword into the path." + (org-with-point-at (org-id-find id t) + (let ((el (org-element-at-point))) + (concat + (if kw + (format "(%s): " + (org-element-property :todo-keyword el)) + "") + (org-format-outline-path + (append + (list + (file-name-nondirectory + (buffer-file-name + (org-base-buffer (current-buffer))))) + (org-get-outline-path) + (list + (replace-regexp-in-string + org-bracket-link-regexp + "\\3" (org-element-property :raw-value el))))))))) + +(defun gnorb-scan-links (bound &rest types) + "Scan from point to BOUND looking for links of type in TYPES. + +TYPES is a list of symbols, possible values include 'bbdb, 'mail, +and 'gnus." + ;; this function could be refactored somewhat -- lots of code + ;; repetition. It also should be a little faster for when we're + ;; scanning for gnus links only, that's a little slow. We should + ;; probably use a different regexp based on the value of TYPES. + ;; + ;; This function should also *not* be responsible for unescaping + ;; links -- we don't know what they're going to be used for, and + ;; unescaped is safer. + (unless (= (point) bound) + (let (addr gnus mail bbdb) + (while (re-search-forward org-any-link-re bound t) + (setq addr (or (match-string-no-properties 2) + (match-string-no-properties 0))) + (cond + ((and (memq 'gnus types) + (string-match "^ + +;; Author: Eric Abrahamsen +;; Keywords: mail org gnus bbdb todo task + +;; URL: https://github.com/girzel/gnorb + +;; This program is free software; you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; This program is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with this program. If not, see . + +;;; Commentary: + +;; Load this file to load everything. + +;;; Code: + +(require 'gnorb-utils) +(require 'nngnorb) +(require 'gnorb-gnus) +(require 'gnorb-bbdb) +(require 'gnorb-org) +(require 'gnorb-registry) + +(provide 'gnorb) +;;; gnorb.el ends here diff --git a/gnorb.info b/gnorb.info new file mode 100644 index 000000000..64304bdce --- /dev/null +++ b/gnorb.info @@ -0,0 +1,701 @@ +This is gnorb.info, produced by makeinfo version 5.2 from gnorb.texi. + +INFO-DIR-SECTION Emacs +START-INFO-DIR-ENTRY +* Gnorb: (gnorb). Glue code for Gnus, Org, and BBDB. +END-INFO-DIR-ENTRY + + +File: gnorb.info, Node: Top, Next: Introduction, Up: (dir) + +Gnorb Manual +************ + +* Menu: + +* Introduction:: +* Installation:: +* Setup:: +* Email Tracking:: +* Restoring Window Layout:: +* Recent Mails From BBDB Contacts:: +* BBDB posting styles:: +* BBDB Org tagging:: +* Misc BBDB:: +* Misc Org:: +* Misc Gnus:: +* Suggested Keybindings:: +* Wishlist/TODO:: +* Index:: + +— The Detailed Node Listing — + +Email Tracking + +* Email-Related Commands:: +* Trigger Actions:: +* Viewing Tracked Messages in *Summary* Buffers:: +* Hinting in Gnus:: +* Message Attachments:: + +Misc BBDB + +* Searching for messages from BBDB contacts:: +* Citing BBDB contacts:: +* User Options:: + +Misc Org + +* Inserting BBDB links:: +* User Options: User Optionsx. + +Misc Gnus + +* Viewing Org headlines relevant to a message:: +* User Options: User Optionsxx. + + +File: gnorb.info, Node: Introduction, Next: Installation, Prev: Top, Up: Top + +1 Introduction +************** + +Gnorb provides glue code between the Gnus, Org, and BBDB packages. It’s +aimed at supporting email-based project management, and generally making +it easier to keep track of email communication. + + Much of the code consists of single-use convenience functions, but +tracking email conversations with Org requires is more complicated, and +requires a bit of setup. + + Gnorb can be used in a modular fashion, by selectively loading the +files “gnorb-org”, “gnorb-gnus” or “gnorb-bbdb” instead of plain old +“gnorb”. The package as a whole is rather Org-centric, though, and it +won’t do much of interest without “gnorb-org”. + + This means that Gnorb doesn’t have hard requirements to any of the +three base libraries. For the libraries you are using, however, you’ll +get best results from using the most recent stable version (yes, that +means BBDB 3). Some of the features in Gnorb only work with development +versions of these libraries (those cases are noted below). + + +File: gnorb.info, Node: Installation, Next: Setup, Prev: Introduction, Up: Top + +2 Installation +************** + +Gnorb is best installed via the Elpa package manager – look for it in +‘list-packages’. + + You can also clone the source code from +, and put the “gnorb” directory on your +load-path. The Github site is also a good place to report bugs and +other issues. + + +File: gnorb.info, Node: Setup, Next: Email Tracking, Prev: Installation, Up: Top + +3 Setup +******* + +Loading “gnorb” will make the basic functions available. Using Gnorb +for email tracking takes a bit more setup, however: + + 1. Email tracking is done via the Gnus registry, so that must be + activated with ‘gnus-registry-initialize’. + 2. It also requires the org-id package to be loaded, and + ‘org-id-track-globally’ set to t (that’s the default value, so + simply loading the package should be enough). + 3. Add a nngnorb entry to your ‘gnus-secondary-select-methods’ + variable. It will look like (nngnorb “Server name”). This does + nothing but provide a place to hang nnir searches. + 4. Then put a call to ‘gnorb-tracking-initialize’ in your init files, + at some point after the Gnus registry is initialized. + 5. If you’re not using a local archive method for saving your sent + messages (ie you’re using IMAP), you’ll also need to tell Gnorb + where to find your sent messages. Set the variable + ‘gnorb-gnus-sent-groups’ to a list of strings; each string should + indicate a fully-qualified group name, eg “nnimap+SERVER:GROUP”. + + Lastly, Gnorb doesn’t bind any keys by default; see the *note +Suggested Keybindings: Suggested Keybindings. section below for +possibilities. + + +File: gnorb.info, Node: Email Tracking, Next: Restoring Window Layout, Prev: Setup, Up: Top + +4 Email Tracking +**************** + +The most interesting thing Gnorb does is using Org headings to track +email conversations. This can mean anything from reminding yourself to +write to your mother, to conducting delicate business negotiations over +email, to running an email-based bug tracker. + + Gnorb assists in this process by using the Gnus registry to track +correspondences between emails and Org headings – specifically, message +IDs are associated with Org heading ids. As a conversation develops, +messages are collected on a heading (and/or its children). You can +compose new messages directly from the Org heading, and Gnorb will +automatically associate your sent message with the conversation. You +can open temporary Gnus *Summary* buffers holding all the messages +associated with an Org subtree, and reply from there. When you receive +new messages relevant to a conversation, Gnorb will notice them and +prompt you to associate them with the appropriate Org heading. +Attachments on incoming messages can be automatically saved as +attachments on Org headings, using org-attach. + + In general, the goal is to keep track of whole conversations, reduce +friction when moving between Gnus and Org, and keep you in the Org +agenda rather than in Gnus. +* Menu: + +* Email-Related Commands:: +* Trigger Actions:: +* Viewing Tracked Messages in *Summary* Buffers:: +* Hinting in Gnus:: +* Message Attachments:: + + +File: gnorb.info, Node: Email-Related Commands, Next: Trigger Actions, Up: Email Tracking + +4.1 Email-Related Commands +========================== + +Email tracking starts in one of three ways: + + 1. With an Org heading that represents an email TODO. Call + ‘gnorb-org-handle-mail’ (see below) on the heading to compose a new + message, and start the tracking process. + 2. By calling org-capture on a received message. Any heading captured + from a message will automatically be associated with that message. + 3. By calling ‘gnorb-gnus-outgoing-do-todo’ in a message composition + buffer – see below. + + There are three main email-related commands: + + 1. ‘gnorb-org-handle-mail’ is called on an Org heading to compose a + new message. By default, this will begin a reply to the most + recent message in the conversation. If there are no associated + messages to reply to (or you call the function with a double prefix + arg), Gnorb will look for mailto: or bbdb: links in the heading, + and compose a new message to them. + + The sent message will be associated with the Org heading, and + you’ll be brought back to the heading and asked to trigger an + action on it. + + ‘gnorb-email-subtree’ is an alternative entry-point to + ‘gnorb-org-handle-mail’. It does the same thing as the latter, but + first exports the body of the subtree as either text or a file, + then inserts the text into the message body, or attaches the file + to the message, depending on what you’ve chosen. + 2. ‘gnorb-gnus-incoming-do-todo’ is called on a message in a Gnus + *Summary* buffer. You’ll be prompted for an Org heading, taken to + that heading, and asked to trigger an action on it. + 3. ‘gnorb-gnus-outgoing-do-todo’ is called in message mode, while + composing a new message. + + If called without a prefix arg, a new Org heading will be created + after the message is sent, and the sent message associated with it. + The new heading will be created as a capture heading, using the + template specified by the ‘gnorb-gnus-new-todo-capture-key’ option. + + If you call this function with a prefix arg, you’ll be prompted to + choose an existing Org heading instead. After the the message is + sent, you’ll be taken to that heading and prompted to trigger an + action on it. + + It’s also possible to call this function *after* a message is sent, + in case you forgot. Gnorb saves information about the most + recently sent message for this purpose. + + Because these three commands all express a similar intent, but are +called in different modes, it can make sense to give each of them the +same keybinding in the keymaps for Org mode, Gnus summary mode, and +Message mode, respectively. + + +File: gnorb.info, Node: Trigger Actions, Next: Viewing Tracked Messages in *Summary* Buffers, Prev: Email-Related Commands, Up: Email Tracking + +4.2 Trigger Actions +=================== + +After calling ‘gnorb-gnus-incoming-do-todo’ on a message, or after +sending a message associated with an Org heading, you’ll be taken to the +heading and asked to “trigger an action” on it. At the moment there are +four different possibilities: triggering a TODO state-change on the +heading, taking a note on the heading (both these options will associate +the message with the heading), associating the message but doing nothing +else, and lastly, doing nothing at all. + + More actions will be added in the future; it’s also possible to add +your own action: see the docstring of ‘gnorb-org-trigger-actions’. + + +File: gnorb.info, Node: Viewing Tracked Messages in *Summary* Buffers, Next: Hinting in Gnus, Prev: Trigger Actions, Up: Email Tracking + +4.3 Viewing Tracked Messages in *Summary* Buffers +================================================= + +Call ‘gnorb-org-view’ on an Org heading to open an nnir *Summary* buffer +showing all the messages associated with that heading (this requires +that you’ve added an nngnorb server to your Gnus backends). A minor +mode will be in effect, ensuring that any replies you send to messages +in this buffer will automatically be associated with the original Org +heading. You can also invoke ‘gnorb-summary-disassociate-message’ (“C-c +d”) to disassociate the message with the Org heading. + + As a bonus, it’s possible to go into Gnus’ *Server* buffer, find the +line specifying your nngnorb server, and hit “G” (aka +‘gnus-group-make-nnir-group’). At the query prompt, enter an Org-style +tags-todo Agenda query string (eg “+work-computer”, or what have you). +Gnorb will find all headings matching this query, scan their subtrees +for gnus links, and then give you a Summary buffer containing all the +linked messages. This is dog-slow at the moment; it will get faster. + + +File: gnorb.info, Node: Hinting in Gnus, Next: Message Attachments, Prev: Viewing Tracked Messages in *Summary* Buffers, Up: Email Tracking + +4.4 Hinting in Gnus +=================== + +When you receive new mails that might be relevant to existing Org TODOs, +Gnorb can alert you to that fact. When +‘gnorb-gnus-hint-relevant-article’ is t (the default), Gnorb will +display a message in the minibuffer when opening potentially relevant +messages. You can then use ‘gnorb-gnus-incoming-to-todo’ to trigger an +action on the relevant TODO. + + This hinting can happen in the Gnus summary buffer as well. If you +use the escape indicated by ‘gnorb-gnus-summary-mark-format-letter” as +part of your ‘gnus-summary-line-format’, articles that are relevant to +TODOs will be marked with a special character in the Summary buffer, as +determined by ‘gnorb-gnus-summary-mark’. By default, the format letter +is “g” (meaning it is used as “%ug” in the format line), and the mark is +“¡”. + + +File: gnorb.info, Node: Message Attachments, Prev: Hinting in Gnus, Up: Email Tracking + +4.5 Message Attachments +======================= + +Gnorb simplifies the handling of attachments that you receive in emails. +When you call ‘gnorb-gnus-incoming-do-todo’ on a message, you’ll be +prompted to re-attach the email’s attachments onto the Org heading, +using the org-attach library. + + You can also do this as part of the capture process. Set the new +:gnus-attachments key to “t” in a capture template that you use on mail +messages, and you’ll be queried to re-attach the message’s attachments +onto the newly-captured heading. Or set +‘gnorb-gnus-capture-always-attach’ to “t” to have Gnorb do this for all +capture templates. + + You can also do this using the regular system of MIME commands, +without invoking the email tracking process. See *note Suggested +Keybindings: Suggested Keybindings, below. + + The same process works in reverse: when you send a message from an +Org heading using ‘gnorb-org-handle-mail’, Gnorb will ask if you want to +attach the files in the heading’s org-attach directory to the outgoing +message. + + +File: gnorb.info, Node: Restoring Window Layout, Next: Recent Mails From BBDB Contacts, Prev: Email Tracking, Up: Top + +5 Restoring Window Layout +************************* + +Many Gnorb functions alter the window layout and value of point. In +most of these cases, you can restore the previous layout using the +interactive function ‘gnorb-restore-layout’. + + +File: gnorb.info, Node: Recent Mails From BBDB Contacts, Next: BBDB posting styles, Prev: Restoring Window Layout, Up: Top + +6 Recent Mails From BBDB Contacts +********************************* + +If you’re using a recent git version of BBDB (circa mid-May 2014 or +later), you can give your BBDB contacts a special field which will +collect links to recent emails from that contact. The default name of +the field is “messages”, but you can customize that name using the +‘gnorb-bbdb-messages-field’ option. + + Gnorb will not collect links by default: you need to call +‘gnorb-bbdb-open-link’ on a contact once to start the process. +Thereafter, opening mails from that contact will store a link to the +message. + + Once some links are stored, ‘gnorb-bbdb-open-link’ will open them: +Use a prefix arg to the function call to select particular messages to +open. There are several options controlling how all this works; see the +gnorb-bbdb user options section below for details. + + +File: gnorb.info, Node: BBDB posting styles, Next: BBDB Org tagging, Prev: Recent Mails From BBDB Contacts, Up: Top + +7 BBDB posting styles +********************* + +Gnorb comes with a BBDB posting-style system, inspired by (copied from) +gnus-posting-styles. You can specify how messages are composed to +specific contacts, by matching on contact field values (the same way +gnus-posting-styles matches on group names). See the docstring of +‘gnorb-bbdb-posting-styles’ for details. + + In order not to be too intrusive, Gnorb doesn’t alter the behavior of +‘bbdb-mail’, the usual mail-composition function. Instead it provides +an alternate ‘gnorb-bbdb-mail’, which does exactly the same thing, but +first processes the new mail according to ‘gnorb-bbdb-posting-styles’. +If you want to use this feature regularly, you can remap ‘bbdb-mail’ to +‘gnorb-bbdb-mail’ in the ‘bbdb-mode-map’. + + +File: gnorb.info, Node: BBDB Org tagging, Next: Misc BBDB, Prev: BBDB posting styles, Up: Top + +8 BBDB Org tagging +****************** + +BBDB contacts can be tagged with the same tags you use in your Org +files. This allows you to pop up a *BBDB* buffer alongside your Org +Agenda when searching for certain tags. This can happen automatically +for all Org tags-todo searches, if you set the option +‘gnorb-org-agenda-popup-bbdb’ to t. Or you can do it manually, by +calling the command of the same name. This command only shows TODOs by +default: use a prefix argument to show all tagged headings. + + Tags are stored in an xfield named org-tags, by default. You can +customize the name of this field using ‘gnorb-bbdb-org-tag-field’. + + +File: gnorb.info, Node: Misc BBDB, Next: Misc Org, Prev: BBDB Org tagging, Up: Top + +9 Misc BBDB +*********** + +* Menu: + +* Searching for messages from BBDB contacts:: +* Citing BBDB contacts:: +* User Options:: + + +File: gnorb.info, Node: Searching for messages from BBDB contacts, Next: Citing BBDB contacts, Up: Misc BBDB + +9.1 Searching for messages from BBDB contacts +============================================= + +Call ‘gnorb-bbdb-mail-search’ to search for all mail messages from the +record(s) displayed. Currently supports the notmuch, mairix, and namazu +search backends; set ‘gnorb-gnus-mail-search-backend’ to one of those +symbol values. + + +File: gnorb.info, Node: Citing BBDB contacts, Next: User Options, Prev: Searching for messages from BBDB contacts, Up: Misc BBDB + +9.2 Citing BBDB contacts +======================== + +Calling ‘gnorb-bbdb-cite-contact’ will prompt for a BBDB record and +insert a string of the type “Bob Smith ”. + + +File: gnorb.info, Node: User Options, Prev: Citing BBDB contacts, Up: Misc BBDB + +9.3 User Options +================ + +‘`gnorb-bbdb-org-tag-field’ + The name of the BBDB xfield, as a symbol, that holds Org-related + tags. Specified as a string with the “:” separator between tags, + same as for Org headings. Defaults to org-tag. +‘`gnorb-bbdb-messages-field'’ + The name of the BBDB xfield that holds links to recently-received + messages from this contact. Defaults to ‘messages. +‘`gnorb-bbdb-collect-N-messages'’ + Collect at most this many links to messages from this contact. + Defaults to 5. +‘`gnorb-bbdb-define-recent'’ + What does “recently-received” mean? Possible values are the + symbols seen and received. When set to seen, the most + recently-opened messages are collected. When set to received, the + most recently-received (by Date header) messages are collected. + Defaults to seen. +‘`gnorb-bbdb-message-link-format-multi'’ + How is a single message’s link formatted in the multi-line BBDB + layout format? Defaults to “%:count. %D: %:subject” (see the + docstring for details). +‘` gnorb-bbdb-message-link-format-one'’ + How is a single message’s link formatted in the one-line BBDB + layout format? Defaults to nil (see the docstring for details). +‘`gnorb-bbdb-posting-styles'’ + Styles to use for influencing the format of mails composed to the + BBDB record(s) under point (see the docstring for details). + + +File: gnorb.info, Node: Misc Org, Next: Misc Gnus, Prev: Misc BBDB, Up: Top + +10 Misc Org +*********** + +* Menu: + +* Inserting BBDB links:: +* User Options: User Optionsx. + + +File: gnorb.info, Node: Inserting BBDB links, Next: User Optionsx, Up: Misc Org + +10.1 Inserting BBDB links +========================= + +Calling ‘gnorb-org-contact-link’ will prompt for a BBDB record and +insert an Org link to that record at point. + + +File: gnorb.info, Node: User Optionsx, Prev: Inserting BBDB links, Up: Misc Org + +10.2 User Options +================= + +‘`gnorb-org-after-message-setup-hook'’ + Hook run in a message buffer after setting up the message, from + ‘gnorb-org-handle-mail’ or ‘gnorb-org-email-subtree’. +‘`gnorb-org-trigger-actions'’ + List of potential actions that can be taken on headings after a + message is sent. See docstring for details. +‘`gnorb-org-mail-scan-scope'’ + The number of paragraphs to scan for mail-related links. This + comes into play when calling ‘gnorb-org-handle-mail’ on a heading + with no associated messages, or when ‘gnorb-org-handle-mail’ is + called with a prefix arg. +‘`gnorb-org-find-candidates-match'’ + When searching all Org files for headings to collect messages from, + this option can limit which headings are searched. It is used as + the second argument to a call to ‘org-map-entries’, and has the + same syntax as that used in an agenda tags view. +‘`gnorb-org-email-subtree-text-parameters'’ + A plist of export parameters corresponding to the EXT-PLIST + argument to the export functions, for use when exporting to text. +‘`gnorb-org-email-subtree-file-parameters'’ + A plist of export parameters corresponding to the EXT-PLIST + argument to the export functions, for use when exporting to a file. +‘`gnorb-org-email-subtree-text-options'’ + A list of ts and nils corresponding to Org’s export options, to be + used when exporting to text. The options, in order, are async, + subtreep, visible-only, and body-only. +‘`gnorb-org-email-subtree-file-options'’ + A list of ts and nils corresponding to Org’s export options, to be + used when exporting to a file. The options, in order, are async, + subtreep, visible-only, and body-only. +‘`gnorb-org-export-extensions'’ + Correspondence between export backends and their respective (usual) + file extensions. +‘`gnorb-org-capture-collect-link-p'’ + When this is set to t, the capture process will always store a link + to the Gnus message or BBDB record under point, even when the link + isn’t part of the capture template. It can then be added to the + captured heading with org-insert-link, as usual. +‘`gnorb-org-agenda-popup-bbdb'’ + Set to “t” to automatically pop up the BBDB buffer displaying + records corresponding to the Org Agenda tags search underway. If + this is nil you can always do it manually with the command of the + same name. +‘`gnorb-org-bbdb-popup-layout'’ + Controls the layout of the Agenda-related BBDB popup, takes the + same values as bbdb-pop-up-layout. + + +File: gnorb.info, Node: Misc Gnus, Next: Suggested Keybindings, Prev: Misc Org, Up: Top + +11 Misc Gnus +************ + +* Menu: + +* Viewing Org headlines relevant to a message:: +* User Options: User Optionsxx. + + +File: gnorb.info, Node: Viewing Org headlines relevant to a message, Next: User Optionsxx, Up: Misc Gnus + +11.1 Viewing Org headlines relevant to a message +================================================ + +Call ‘gnorb-gnus-view’ on a message that is associated with an Org +heading to jump to that heading. + + +File: gnorb.info, Node: User Optionsxx, Prev: Viewing Org headlines relevant to a message, Up: Misc Gnus + +11.2 User Options +================= + +‘`gnorb-gnus-mail-search-backend'’ + Specifies the search backend that you use for searching mails. + Currently supports notmuch, mairix, and namazu: set this option to + one of those symbols. +‘`gnorb-gnus-capture-always-attach'’ + Treat all capture templates as if they had the :gnus-attachments + key set to “t”. This only has any effect if you’re capturing from + a Gnus summary or article buffer. +‘`gnorb-trigger-todo-default'’ + Set to either ‘note or ‘todo to tell ‘gnorb-gnus-incoming-do-todo’ + what to do by default. You can reach the non-default behavior by + calling that function with a prefix argument. Alternately, set to + ‘prompt to always prompt for the appropriate action. +‘`gnorb-gnus-trigger-refile-targets'’ + If you use ‘gnorb-gnus-incoming-do-todo’ on an incoming message, + Gnorb will try to locate a TODO heading that’s relevant to that + message. If it can’t, it will prompt you for one, using the refile + interface. This option will be used as the value of + ‘org-refile-targets’ during that process: see the docstring of + ‘org-refile-targets’ for the appropriate syntax. +‘`gnorb-gnus-new-todo-capture-key'’ + Set this to a single-character string pointing at an Org capture + template to use when creating TODOs from outgoing messages. The + template is a regular capture template, with a few exceptions. If + Gnus helps you archive outgoing messages (ie you have + ‘gnus-message-archive-group’ set to something, and your outgoing + messages have a “Fcc” header), a link to that message will be made, + and you’ll be able to use all the escapes related to gnus messages. + If you don’t archive outgoing messages, you’ll still be able to use + the %:subject, %:to, %:toname, %:toaddress, and %:date escapes in + the capture template. +‘`gnorb-gnus-hint-relevant-article'’ + Set to “t” (the default) to have Gnorb give you a hint in the + minibuffer when opening messages that might be relevant to existing + Org TODOs. +‘`gnorb-gnus-summary-mark-format-letter'’ + The formatting letter to use as part of your + ‘gnus-summary-line-format’, to indicate messages which might be + relevant to Org TODOs. Defaults to “g”, meaning it should be used + as “%ug” in the format line. +‘`gnorb-gnus-summary-mark'’ + The mark used to indicate relevant messages in the Summary buffer, + when ‘gnorb-gnus-summary-mark-format-letter’ is present in the + format line. Defaults to “¡”. + + +File: gnorb.info, Node: Suggested Keybindings, Next: Wishlist/TODO, Prev: Misc Gnus, Up: Top + +12 Suggested Keybindings +************************ + + (eval-after-load "gnorb-bbdb" + '(progn + (define-key bbdb-mode-map (kbd "O") 'gnorb-bbdb-tag-agenda) + (define-key bbdb-mode-map (kbd "S") 'gnorb-bbdb-mail-search) + (define-key bbdb-mode-map [remap bbdb-mail] 'gnorb-bbdb-mail) + (define-key bbdb-mode-map (kbd "l") 'gnorb-bbdb-open-link) + (global-set-key (kbd "C-c C") 'gnorb-bbdb-cite-contact))) + + (eval-after-load "gnorb-org" + '(progn + (org-defkey org-mode-map (kbd "C-c C") 'gnorb-org-contact-link) + (org-defkey org-mode-map (kbd "C-c t") 'gnorb-org-handle-mail) + (org-defkey org-mode-map (kbd "C-c e") 'gnorb-org-view) + (org-defkey org-mode-map (kbd "C-c E") 'gnorb-org-email-subtree) + (org-defkey org-mode-map (kbd "C-c V") 'gnorb-org-popup-bbdb) + (setq gnorb-org-agenda-popup-bbdb t) + (eval-after-load "org-agenda" + '(progn (org-defkey org-agenda-mode-map (kbd "H") 'gnorb-org-handle-mail) + (org-defkey org-agenda-mode-map (kbd "V") 'gnorb-org-popup-bbdb))))) + + (eval-after-load "gnorb-gnus" + '(progn + (define-key gnus-summary-mime-map "a" 'gnorb-gnus-article-org-attach) + (define-key gnus-summary-mode-map (kbd "C-c t") 'gnorb-gnus-incoming-do-todo) + (push '("attach to org heading" . gnorb-gnus-mime-org-attach) + gnus-mime-action-alist) + ;; The only way to add mime button command keys is by redefining + ;; gnus-mime-button-map, possibly not ideal. Ideal would be a + ;; setter function in gnus itself. + (push '(gnorb-gnus-mime-org-attach "a" "Attach to Org heading") + gnus-mime-button-commands) + (setq gnus-mime-button-map + (let ((map (make-sparse-keymap))) + (define-key map gnus-mouse-2 'gnus-article-push-button) + (define-key map gnus-down-mouse-3 'gnus-mime-button-menu) + (dolist (c gnus-mime-button-commands) + (define-key map (cadr c) (car c))) + map)))) + + (eval-after-load "message" + '(progn + (define-key message-mode-map (kbd "C-c t") 'gnorb-gnus-outgoing-do-todo))) + + +File: gnorb.info, Node: Wishlist/TODO, Next: Index, Prev: Suggested Keybindings, Up: Top + +13 Wishlist/TODO +**************** + + • Provide a command that, when in the Org Agenda, does an email + search for messages received in the visible date span, or day under + point, etc. Make it work in the calendar, as well? + • Add trigger actions that create new sibling or child headings on + the original Org heading. + • Allow tagging of Gnus messages, by giving the message’s registry + entry an ‘org-tags key. + • Provide persistent nngnorb search groups. + • Allow automatic org-tagging of BBDB contacts: when messages from a + contact are associated with an Org heading, make it possible for + the contact to inherit that heading’s tags automatically. + • Provide completion when setting Org tags on a BBDB contact. + • Provide a ‘gnorb-bbdb-view’ command that opens a *Summary* buffer + containing all the tracked messages from the contact(s) under + point. + • Provide a ‘gnorb-view’ command that takes a tags-todo search phrase + (or a single Org heading ID), finds all relevant messages, Org + headings, and BBDB records, and sets up a four-pane view: Org + Agenda, **Article* SummaryBBDB* buffer, Gnus *buffer, and an * + buffer. + + +File: gnorb.info, Node: Index, Prev: Wishlist/TODO, Up: Top + +14 Index +******** + + + +Tag Table: +Node: Top194 +Node: Introduction1017 +Node: Installation2126 +Node: Setup2540 +Node: Email Tracking3907 +Node: Email-Related Commands5418 +Node: Trigger Actions8239 +Node: Viewing Tracked Messages in *Summary* Buffers9053 +Node: Hinting in Gnus10287 +Node: Message Attachments11295 +Node: Restoring Window Layout12453 +Node: Recent Mails From BBDB Contacts12817 +Node: BBDB posting styles13813 +Node: BBDB Org tagging14729 +Node: Misc BBDB15475 +Node: Searching for messages from BBDB contacts15688 +Node: Citing BBDB contacts16134 +Node: User Options16455 +Node: Misc Org17994 +Node: Inserting BBDB links18169 +Node: User Optionsx18424 +Node: Misc Gnus21161 +Node: Viewing Org headlines relevant to a message21374 +Node: User Optionsxx21689 +Node: Suggested Keybindings24453 +Node: Wishlist/TODO26824 +Node: Index28139 + +End Tag Table + + +Local Variables: +coding: utf-8 +End: diff --git a/gnorb.org b/gnorb.org new file mode 100644 index 000000000..b03098b20 --- /dev/null +++ b/gnorb.org @@ -0,0 +1,507 @@ +#+TEXINFO_CLASS: info +#+TEXINFO_HEADER: @syncodeindex pg cp +#+TITLE: Gnorb Manual +#+SUBTITLE: for version 1, updated 3 October, 2014 +#+TEXINFO_DIR_CATEGORY: Emacs +#+TEXINFO_DIR_TITLE: Gnorb: (gnorb) +#+TEXINFO_DIR_DESC: Glue code for Gnus, Org, and BBDB +#+OPTIONS: *:nil num:t toc:nil +* Introduction + +Gnorb provides glue code between the Gnus, Org, and BBDB packages. +It's aimed at supporting email-based project management, and generally +making it easier to keep track of email communication. + +Much of the code consists of single-use convenience functions, but +tracking email conversations with Org requires is more complicated, +and requires a bit of setup. + +Gnorb can be used in a modular fashion, by selectively loading the +files "gnorb-org", "gnorb-gnus" or "gnorb-bbdb" instead of plain old +"gnorb". The package as a whole is rather Org-centric, though, and it +won't do much of interest without "gnorb-org". + +This means that Gnorb doesn't have hard requirements to any of the +three base libraries. For the libraries you are using, however, you'll +get best results from using the most recent stable version (yes, that +means BBDB 3). Some of the features in Gnorb only work with +development versions of these libraries (those cases are noted below). +* Installation +Gnorb is best installed via the Elpa package manager -- look for it in +`list-packages'. + +You can also clone the source code from +https://github.com/girzel/gnorb, and put the "gnorb" directory on your +load-path. The Github site is also a good place to report bugs and +other issues. +* Setup +Loading "gnorb" will make the basic functions available. Using Gnorb +for email tracking takes a bit more setup, however: + +1. Email tracking is done via the Gnus registry, so that must be + activated with 'gnus-registry-initialize'. +2. It also requires the org-id package to be loaded, and + `org-id-track-globally' set to t (that's the default value, so + simply loading the package should be enough). +3. Add a nngnorb entry to your `gnus-secondary-select-methods' + variable. It will look like (nngnorb "Server name"). This does + nothing but provide a place to hang nnir searches. +4. Then put a call to `gnorb-tracking-initialize' in your init files, + at some point after the Gnus registry is initialized. +5. If you're not using a local archive method for saving your sent + messages (ie you're using IMAP), you'll also need to tell Gnorb + where to find your sent messages. Set the variable + `gnorb-gnus-sent-groups' to a list of strings; each string should + indicate a fully-qualified group name, eg "nnimap+SERVER:GROUP". + +Lastly, Gnorb doesn't bind any keys by default; see the [[id:de1b2579-86c2-4bb1-b77e-3467a3d2b3c7][Suggested +Keybindings]] section below for possibilities. +* Email Tracking +The most interesting thing Gnorb does is using Org headings to track +email conversations. This can mean anything from reminding yourself to +write to your mother, to conducting delicate business negotiations +over email, to running an email-based bug tracker. + +Gnorb assists in this process by using the Gnus registry to track +correspondences between emails and Org headings -- specifically, +message IDs are associated with Org heading ids. As a conversation +develops, messages are collected on a heading (and/or its children). +You can compose new messages directly from the Org heading, and Gnorb +will automatically associate your sent message with the conversation. +You can open temporary Gnus *Summary* buffers holding all the messages +associated with an Org subtree, and reply from there. When you receive +new messages relevant to a conversation, Gnorb will notice them and +prompt you to associate them with the appropriate Org heading. +Attachments on incoming messages can be automatically saved as +attachments on Org headings, using org-attach. + +In general, the goal is to keep track of whole conversations, reduce +friction when moving between Gnus and Org, and keep you in the Org +agenda rather than in Gnus. +** Email-Related Commands +Email tracking starts in one of three ways: + +1. With an Org heading that represents an email TODO. Call + `gnorb-org-handle-mail' (see below) on the heading to compose a new + message, and start the tracking process. +2. By calling org-capture on a received message. Any heading captured + from a message will automatically be associated with that message. +3. By calling `gnorb-gnus-outgoing-do-todo' in a message composition + buffer -- see below. + +There are three main email-related commands: + +1. `gnorb-org-handle-mail' is called on an Org heading to compose a + new message. By default, this will begin a reply to the most recent + message in the conversation. If there are no associated messages to + reply to (or you call the function with a double prefix arg), Gnorb + will look for mailto: or bbdb: links in the heading, and compose a + new message to them. + + The sent message will be associated with the Org heading, and + you'll be brought back to the heading and asked to trigger an + action on it. + + `gnorb-email-subtree' is an alternative entry-point to + `gnorb-org-handle-mail'. It does the same thing as the latter, but + first exports the body of the subtree as either text or a file, + then inserts the text into the message body, or attaches the file + to the message, depending on what you've chosen. +2. `gnorb-gnus-incoming-do-todo' is called on a message in a Gnus + *Summary* buffer. You'll be prompted for an Org heading, taken to + that heading, and asked to trigger an action on it. +3. `gnorb-gnus-outgoing-do-todo' is called in message mode, while + composing a new message. + + If called without a prefix arg, a new Org heading will be created + after the message is sent, and the sent message associated with it. + The new heading will be created as a capture heading, using the + template specified by the `gnorb-gnus-new-todo-capture-key' option. + + If you call this function with a prefix arg, you'll be prompted to + choose an existing Org heading instead. After the the message is + sent, you'll be taken to that heading and prompted to trigger an + action on it. + + It's also possible to call this function *after* a message is sent, + in case you forgot. Gnorb saves information about the most recently + sent message for this purpose. + +Because these three commands all express a similar intent, but are +called in different modes, it can make sense to give each of them the +same keybinding in the keymaps for Org mode, Gnus summary mode, and +Message mode, respectively. +** Trigger Actions +After calling `gnorb-gnus-incoming-do-todo' on a message, or after +sending a message associated with an Org heading, you'll be taken to +the heading and asked to "trigger an action" on it. At the moment +there are four different possibilities: triggering a TODO state-change +on the heading, taking a note on the heading (both these options will +associate the message with the heading), associating the message but +doing nothing else, and lastly, doing nothing at all. + +More actions will be added in the future; it's also possible to +rearrange or delete existing actions, and add your own: see the +docstring of `gnorb-org-trigger-actions'. +** Viewing Tracked Messages in *Summary* Buffers +:PROPERTIES: +:END: +Call `gnorb-org-view' on an Org heading to open an nnir *Summary* +buffer showing all the messages associated with that heading (this +requires that you've added an nngnorb server to your Gnus backends). A +minor mode will be in effect, ensuring that any replies you send to +messages in this buffer will automatically be associated with the +original Org heading. You can also invoke +`gnorb-summary-disassociate-message' ("C-c d") to disassociate the +message with the Org heading. + +As a bonus, it's possible to go into Gnus' *Server* buffer, find the +line specifying your nngnorb server, and hit "G" (aka +`gnus-group-make-nnir-group'). At the query prompt, enter an Org-style +tags-todo Agenda query string (eg "+work-computer", or what have you). +Gnorb will find all headings matching this query, scan their subtrees +for gnus links, and then give you a Summary buffer containing all the +linked messages. This is dog-slow at the moment; it will get faster. + +** Hinting in Gnus +:PROPERTIES: +:END: +When you receive new mails that might be relevant to existing Org +TODOs, Gnorb can alert you to that fact. When +`gnorb-gnus-hint-relevant-article' is t (the default), Gnorb will +display a message in the minibuffer when opening potentially relevant +messages. You can then use `gnorb-gnus-incoming-to-todo' to trigger an +action on the relevant TODO. + +This hinting can happen in the Gnus summary buffer as well. If you use +the escape indicated by `gnorb-gnus-summary-mark-format-letter" as +part of your `gnus-summary-line-format', articles that are relevant to +TODOs will be marked with a special character in the Summary buffer, +as determined by `gnorb-gnus-summary-mark'. By default, the format +letter is "g" (meaning it is used as "%ug" in the format line), and +the mark is "¡". +** Message Attachments +:PROPERTIES: +:END: +Gnorb simplifies the handling of attachments that you receive in +emails. When you call `gnorb-gnus-incoming-do-todo' on a message, +you'll be prompted to re-attach the email's attachments onto the Org +heading, using the org-attach library. + +You can also do this as part of the capture process. Set the +new :gnus-attachments key to "t" in a capture template that you use on +mail messages, and you'll be queried to re-attach the message's +attachments onto the newly-captured heading. Or set +`gnorb-gnus-capture-always-attach' to "t" to have Gnorb do this for +all capture templates. + +You can also do this using the regular system of MIME commands, +without invoking the email tracking process. See [[id:de1b2579-86c2-4bb1-b77e-3467a3d2b3c7][Suggested +Keybindings]], below. + +The same process works in reverse: when you send a message from an Org +heading using `gnorb-org-handle-mail', Gnorb will ask if you want to +attach the files in the heading's org-attach directory to the outgoing +message. +** Likely Workflow +You receive an email from Jimmy, who wants to rent a room in your +house. "I'll respond to this later," you think. + +You capture an Org TODO from the email, call it "Jimmy renting a +room", and give it a REPLY keyword. Gnorb quietly records the +correspondence between the email and the TODO, using the Gnus +registry. + +The next day, looking at your Agenda, you see the TODO and decide to +respond to the email. You call `gnorb-org-handle-mail' on the heading, +and Gnorb opens Jimmy's email and starts a reply to it. + +You tell Jimmy the room's available in March, and send the message. +Gnorb takes you back to the heading, and asks you to trigger an action +on it. You choose "todo state", and change the heading keyword to +WAIT. + +Two days later, Jimmy replies to your message, saying that March is +perfect. When you open his response, Gnorb politely reminds you that +the message is relevant to an existing TODO. You call +`gnorb-gnus-incoming-do-todo' on the message, and are again taken to +the TODO and asked to trigger an action. Again you choose "todo +state", and change the heading keyword back to REPLY. + +You get another email, from Samantha, warning you not to rent the room +to Jimmy. She even attaches a picture of a room in her house, as it +looked after Jimmy had stayed there for six months. It's bad. You call +`gnorb-gnus-incoming-do-todo' on her message, and pick the "Jimmy +renting a room" heading. This time, you choose "take note" as the +trigger action, and make a brief note about how bad that room looked. +Gnorb asks if you'd like to attach the picture to the Org heading. You +decide you will. + +Now it's time to write to Jimmy and say something noncommittal. +Calling `gnorb-org-handle-mail' on the heading would respond to +Samantha's email, the most recent of the associated messages, which +isn't what you want. Instead you call `gnorb-org-view' on the heading, +which opens up a Gnus *Summary* buffer containing all four messages: +Jimmy's first, your response, his response to that, and Samantha's +message. You pick Jimmy's second email, and reply to it normally. +Gnorb asks if you'd like to send the picture of the room as an +attachment. You would not. When you send the reply Gnorb tracks that +as well, and does the "trigger an action" trick again. + +In this way Gnorb helps you manage an entire conversation, possibly +with multiple threads and multiple participants. Mostly all you need +to do is call `gnorb-gnus-incoming-do-todo' on newly-received +messages, and `gnorb-org-handle-mail' on the heading when it's time to +compose a new reply. +* Restoring Window Layout +Many Gnorb functions alter the window layout and value of point. In +most of these cases, you can restore the previous layout using the +interactive function `gnorb-restore-layout'. + +* Recent Mails From BBDB Contacts +:PROPERTIES: +:END: +If you're using a recent git version of BBDB (circa mid-May 2014 or +later), you can give your BBDB contacts a special field which will +collect links to recent emails from that contact. The default name of +the field is "messages", but you can customize that name using the +`gnorb-bbdb-messages-field' option. + +Gnorb will not collect links by default: you need to call +`gnorb-bbdb-open-link' on a contact once to start the process. +Thereafter, opening mails from that contact will store a link to the +message. + +Once some links are stored, `gnorb-bbdb-open-link' will open them: Use +a prefix arg to the function call to select particular messages to +open. There are several options controlling how all this works; see +the gnorb-bbdb user options section below for details. +* BBDB posting styles +:PROPERTIES: +:END: +Gnorb comes with a BBDB posting-style system, inspired by (copied +from) gnus-posting-styles. You can specify how messages are composed +to specific contacts, by matching on contact field values (the same +way gnus-posting-styles matches on group names). See the docstring of +`gnorb-bbdb-posting-styles' for details. + +In order not to be too intrusive, Gnorb doesn't alter the behavior of +`bbdb-mail', the usual mail-composition function. Instead it provides +an alternate `gnorb-bbdb-mail', which does exactly the same thing, but +first processes the new mail according to `gnorb-bbdb-posting-styles'. +If you want to use this feature regularly, you can remap `bbdb-mail' +to `gnorb-bbdb-mail' in the `bbdb-mode-map'. +* BBDB Org tagging +BBDB contacts can be tagged with the same tags you use in your Org +files. This allows you to pop up a *BBDB* buffer alongside your Org +Agenda when searching for certain tags. This can happen automatically +for all Org tags-todo searches, if you set the option +`gnorb-org-agenda-popup-bbdb' to t. Or you can do it manually, by +calling the command of the same name. This command only shows TODOs by +default: use a prefix argument to show all tagged headings. + +Tags are stored in an xfield named org-tags, by default. You can +customize the name of this field using `gnorb-bbdb-org-tag-field'. +* Misc BBDB +** Searching for messages from BBDB contacts +:PROPERTIES: +:END: +Call `gnorb-bbdb-mail-search' to search for all mail messages from the +record(s) displayed. Currently supports the notmuch, mairix, and +namazu search backends; set `gnorb-gnus-mail-search-backend' to one of +those symbol values. +** Citing BBDB contacts +:PROPERTIES: +:END: +Calling `gnorb-bbdb-cite-contact' will prompt for a BBDB record and +insert a string of the type "Bob Smith ". +** User Options +- `gnorb-bbdb-org-tag-field :: The name of the BBDB xfield, as a + symbol, that holds Org-related tags. Specified as a string with + the ":" separator between tags, same as for Org headings. + Defaults to org-tag. +- `gnorb-bbdb-messages-field' :: The name of the BBDB xfield that + holds links to recently-received messages from this contact. + Defaults to 'messages. +- `gnorb-bbdb-collect-N-messages' :: Collect at most this many links + to messages from this contact. Defaults to 5. +- `gnorb-bbdb-define-recent' :: What does "recently-received" mean? + Possible values are the symbols seen and received. When set to + seen, the most recently-opened messages are collected. When set + to received, the most recently-received (by Date header) messages + are collected. Defaults to seen. +- `gnorb-bbdb-message-link-format-multi' :: How is a single message's + link formatted in the multi-line BBDB layout format? Defaults to + "%:count. %D: %:subject" (see the docstring for details). +- ` gnorb-bbdb-message-link-format-one' :: How is a single message's + link formatted in the one-line BBDB layout format? Defaults to + nil (see the docstring for details). +- `gnorb-bbdb-posting-styles' :: Styles to use for influencing the + format of mails composed to the BBDB record(s) under point (see + the docstring for details). +* Misc Org +** Inserting BBDB links +:PROPERTIES: +:END: +Calling `gnorb-org-contact-link' will prompt for a BBDB record and +insert an Org link to that record at point. +** User Options +- `gnorb-org-after-message-setup-hook' :: Hook run in a message buffer + after setting up the message, from `gnorb-org-handle-mail' or + `gnorb-org-email-subtree'. +- `gnorb-org-trigger-actions' :: List of potential actions that can be + taken on headings after a message is sent. See docstring for + details. +- `gnorb-org-mail-scan-scope' :: The number of paragraphs to scan for + mail-related links. This comes into play when calling + `gnorb-org-handle-mail' on a heading with no associated messages, + or when `gnorb-org-handle-mail' is called with a prefix arg. +- `gnorb-org-find-candidates-match' :: When searching all Org files + for headings to collect messages from, this option can limit + which headings are searched. It is used as the second argument to + a call to `org-map-entries', and has the same syntax as that used + in an agenda tags view. +- `gnorb-org-email-subtree-text-parameters' :: A plist of export + parameters corresponding to the EXT-PLIST argument to the export + functions, for use when exporting to text. +- `gnorb-org-email-subtree-file-parameters' :: A plist of export + parameters corresponding to the EXT-PLIST argument to the export + functions, for use when exporting to a file. +- `gnorb-org-email-subtree-text-options' :: A list of ts and nils + corresponding to Org's export options, to be used when exporting + to text. The options, in order, are async, subtreep, + visible-only, and body-only. +- `gnorb-org-email-subtree-file-options' :: A list of ts and nils + corresponding to Org's export options, to be used when exporting + to a file. The options, in order, are async, subtreep, + visible-only, and body-only. +- `gnorb-org-export-extensions' :: Correspondence between export + backends and their respective (usual) file extensions. +- `gnorb-org-capture-collect-link-p' :: When this is set to t, the + capture process will always store a link to the Gnus message or + BBDB record under point, even when the link isn't part of the + capture template. It can then be added to the captured heading + with org-insert-link, as usual. +- `gnorb-org-agenda-popup-bbdb' :: Set to "t" to automatically pop up + the BBDB buffer displaying records corresponding to the Org + Agenda tags search underway. If this is nil you can always do it + manually with the command of the same name. +- `gnorb-org-bbdb-popup-layout' :: Controls the layout of the + Agenda-related BBDB popup, takes the same values as + bbdb-pop-up-layout. +* Misc Gnus +** Viewing Org headlines relevant to a message +:PROPERTIES: +:END: +Call `gnorb-gnus-view' on a message that is associated with an Org +heading to jump to that heading. +** User Options +- `gnorb-gnus-mail-search-backend' :: Specifies the search backend + that you use for searching mails. Currently supports notmuch, + mairix, and namazu: set this option to one of those symbols. +- `gnorb-gnus-capture-always-attach' :: Treat all capture templates as + if they had the :gnus-attachments key set to "t". This only has + any effect if you're capturing from a Gnus summary or article + buffer. +- `gnorb-trigger-todo-default' :: Set to either 'note or 'todo to tell + `gnorb-gnus-incoming-do-todo' what to do by default. You can + reach the non-default behavior by calling that function with a + prefix argument. Alternately, set to 'prompt to always prompt for + the appropriate action. +- `gnorb-gnus-trigger-refile-targets' :: If you use + `gnorb-gnus-incoming-do-todo' on an incoming message, Gnorb will + try to locate a TODO heading that's relevant to that message. If + it can't, it will prompt you for one, using the refile interface. + This option will be used as the value of `org-refile-targets' + during that process: see the docstring of `org-refile-targets' + for the appropriate syntax. +- `gnorb-gnus-new-todo-capture-key' :: Set this to a single-character + string pointing at an Org capture template to use when creating + TODOs from outgoing messages. The template is a regular capture + template, with a few exceptions. If Gnus helps you archive + outgoing messages (ie you have `gnus-message-archive-group' set + to something, and your outgoing messages have a "Fcc" header), a + link to that message will be made, and you'll be able to use all + the escapes related to gnus messages. If you don't archive + outgoing messages, you'll still be able to use the %:subject, + %:to, %:toname, %:toaddress, and %:date escapes in the capture + template. +- `gnorb-gnus-hint-relevant-article' :: Set to "t" (the default) to + have Gnorb give you a hint in the minibuffer when opening + messages that might be relevant to existing Org TODOs. +- `gnorb-gnus-summary-mark-format-letter' :: The formatting letter to + use as part of your `gnus-summary-line-format', to indicate + messages which might be relevant to Org TODOs. Defaults to "g", + meaning it should be used as "%ug" in the format line. +- `gnorb-gnus-summary-mark' :: The mark used to indicate relevant + messages in the Summary buffer, when + `gnorb-gnus-summary-mark-format-letter' is present in the format + line. Defaults to "¡". +* Suggested Keybindings +:PROPERTIES: +:ID: de1b2579-86c2-4bb1-b77e-3467a3d2b3c7 +:END: +#+BEGIN_SRC emacs-lisp + (eval-after-load "gnorb-bbdb" + '(progn + (define-key bbdb-mode-map (kbd "O") 'gnorb-bbdb-tag-agenda) + (define-key bbdb-mode-map (kbd "S") 'gnorb-bbdb-mail-search) + (define-key bbdb-mode-map [remap bbdb-mail] 'gnorb-bbdb-mail) + (define-key bbdb-mode-map (kbd "l") 'gnorb-bbdb-open-link) + (global-set-key (kbd "C-c C") 'gnorb-bbdb-cite-contact))) + + (eval-after-load "gnorb-org" + '(progn + (org-defkey org-mode-map (kbd "C-c C") 'gnorb-org-contact-link) + (org-defkey org-mode-map (kbd "C-c t") 'gnorb-org-handle-mail) + (org-defkey org-mode-map (kbd "C-c e") 'gnorb-org-view) + (org-defkey org-mode-map (kbd "C-c E") 'gnorb-org-email-subtree) + (org-defkey org-mode-map (kbd "C-c V") 'gnorb-org-popup-bbdb) + (setq gnorb-org-agenda-popup-bbdb t) + (eval-after-load "org-agenda" + '(progn (org-defkey org-agenda-mode-map (kbd "H") 'gnorb-org-handle-mail) + (org-defkey org-agenda-mode-map (kbd "V") 'gnorb-org-popup-bbdb))))) + + (eval-after-load "gnorb-gnus" + '(progn + (define-key gnus-summary-mime-map "a" 'gnorb-gnus-article-org-attach) + (define-key gnus-summary-mode-map (kbd "C-c t") 'gnorb-gnus-incoming-do-todo) + (push '("attach to org heading" . gnorb-gnus-mime-org-attach) + gnus-mime-action-alist) + ;; The only way to add mime button command keys is by redefining + ;; gnus-mime-button-map, possibly not ideal. Ideal would be a + ;; setter function in gnus itself. + (push '(gnorb-gnus-mime-org-attach "a" "Attach to Org heading") + gnus-mime-button-commands) + (setq gnus-mime-button-map + (let ((map (make-sparse-keymap))) + (define-key map gnus-mouse-2 'gnus-article-push-button) + (define-key map gnus-down-mouse-3 'gnus-mime-button-menu) + (dolist (c gnus-mime-button-commands) + (define-key map (cadr c) (car c))) + map)))) + + (eval-after-load "message" + '(progn + (define-key message-mode-map (kbd "C-c t") 'gnorb-gnus-outgoing-do-todo))) +#+END_SRC +* Wishlist/TODO +- Provide a command that, when in the Org Agenda, does an email search + for messages received in the visible date span, or day under point, + etc. Make it work in the calendar, as well? +- Add trigger actions that create new sibling or child headings on the + original Org heading. +- Allow tagging of Gnus messages, by giving the message's registry + entry an 'org-tags key. +- Provide persistent nngnorb search groups. +- Allow automatic org-tagging of BBDB contacts: when messages from a + contact are associated with an Org heading, make it possible for the + contact to inherit that heading's tags automatically. +- Provide completion when setting Org tags on a BBDB contact. +- Provide a `gnorb-bbdb-view' command that opens a *Summary* buffer + containing all the tracked messages from the contact(s) under point. +- Provide a `gnorb-view' command that takes a tags-todo search phrase + (or a single Org heading ID), finds all relevant messages, Org + headings, and BBDB records, and sets up a four-pane view: Org + Agenda, *BBDB* buffer, Gnus *Summary* buffer, and an *Article* + buffer. diff --git a/gnorb.texi b/gnorb.texi new file mode 100644 index 000000000..643552fbb --- /dev/null +++ b/gnorb.texi @@ -0,0 +1,654 @@ +\input texinfo @c -*- texinfo -*- +@c %**start of header +@setfilename ./gnorb.info +@settitle Gnorb Manual +@documentencoding UTF-8 +@documentlanguage en +@syncodeindex pg cp +@c %**end of header + +@dircategory Emacs +@direntry +* Gnorb: (gnorb). Glue code for Gnus, Org, and BBDB. +@end direntry + +@finalout +@titlepage +@title Gnorb Manual +@subtitle for version 1, updated 3 October, 2014 +@end titlepage + +@ifnottex +@node Top +@top Gnorb Manual +@end ifnottex + +@menu +* Introduction:: +* Installation:: +* Setup:: +* Email Tracking:: +* Restoring Window Layout:: +* Recent Mails From BBDB Contacts:: +* BBDB posting styles:: +* BBDB Org tagging:: +* Misc BBDB:: +* Misc Org:: +* Misc Gnus:: +* Suggested Keybindings:: +* Wishlist/TODO:: +* Index:: + +@detailmenu +--- The Detailed Node Listing --- + +Email Tracking + +* Email-Related Commands:: +* Trigger Actions:: +* Viewing Tracked Messages in *Summary* Buffers:: +* Hinting in Gnus:: +* Message Attachments:: + +Misc BBDB + +* Searching for messages from BBDB contacts:: +* Citing BBDB contacts:: +* User Options:: + +Misc Org + +* Inserting BBDB links:: +* User Options: User Optionsx. + +Misc Gnus + +* Viewing Org headlines relevant to a message:: +* User Options: User Optionsxx. +@end detailmenu +@end menu + +@node Introduction +@chapter Introduction + +Gnorb provides glue code between the Gnus, Org, and BBDB packages. +It's aimed at supporting email-based project management, and generally +making it easier to keep track of email communication. + +Much of the code consists of single-use convenience functions, but +tracking email conversations with Org requires is more complicated, +and requires a bit of setup. + +Gnorb can be used in a modular fashion, by selectively loading the +files ``gnorb-org'', ``gnorb-gnus'' or ``gnorb-bbdb'' instead of plain old +``gnorb''. The package as a whole is rather Org-centric, though, and it +won't do much of interest without ``gnorb-org''. + +This means that Gnorb doesn't have hard requirements to any of the +three base libraries. For the libraries you are using, however, you'll +get best results from using the most recent stable version (yes, that +means BBDB 3). Some of the features in Gnorb only work with +development versions of these libraries (those cases are noted below). + +@node Installation +@chapter Installation + +Gnorb is best installed via the Elpa package manager -- look for it in +`list-packages'. + +You can also clone the source code from +@uref{https://github.com/girzel/gnorb}, and put the ``gnorb'' directory on your +load-path. The Github site is also a good place to report bugs and +other issues. + +@node Setup +@chapter Setup + +Loading ``gnorb'' will make the basic functions available. Using Gnorb +for email tracking takes a bit more setup, however: + +@enumerate +@item +Email tracking is done via the Gnus registry, so that must be +activated with `gnus-registry-initialize'. +@item +It also requires the org-id package to be loaded, and +`org-id-track-globally' set to t (that's the default value, so +simply loading the package should be enough). +@item +Add a nngnorb entry to your `gnus-secondary-select-methods' +variable. It will look like (nngnorb ``Server name''). This does +nothing but provide a place to hang nnir searches. +@item +Then put a call to `gnorb-tracking-initialize' in your init files, +at some point after the Gnus registry is initialized. +@item +If you're not using a local archive method for saving your sent +messages (ie you're using IMAP), you'll also need to tell Gnorb +where to find your sent messages. Set the variable +`gnorb-gnus-sent-groups' to a list of strings; each string should +indicate a fully-qualified group name, eg ``nnimap+SERVER:GROUP''. +@end enumerate + +Lastly, Gnorb doesn't bind any keys by default; see the @ref{Suggested Keybindings,Suggested +Keybindings} section below for possibilities. + +@node Email Tracking +@chapter Email Tracking + +The most interesting thing Gnorb does is using Org headings to track +email conversations. This can mean anything from reminding yourself to +write to your mother, to conducting delicate business negotiations +over email, to running an email-based bug tracker. + +Gnorb assists in this process by using the Gnus registry to track +correspondences between emails and Org headings -- specifically, +message IDs are associated with Org heading ids. As a conversation +develops, messages are collected on a heading (and/or its children). +You can compose new messages directly from the Org heading, and Gnorb +will automatically associate your sent message with the conversation. +You can open temporary Gnus *Summary* buffers holding all the messages +associated with an Org subtree, and reply from there. When you receive +new messages relevant to a conversation, Gnorb will notice them and +prompt you to associate them with the appropriate Org heading. +Attachments on incoming messages can be automatically saved as +attachments on Org headings, using org-attach. + +In general, the goal is to keep track of whole conversations, reduce +friction when moving between Gnus and Org, and keep you in the Org +agenda rather than in Gnus. +@menu +* Email-Related Commands:: +* Trigger Actions:: +* Viewing Tracked Messages in *Summary* Buffers:: +* Hinting in Gnus:: +* Message Attachments:: +@end menu + +@node Email-Related Commands +@section Email-Related Commands + +Email tracking starts in one of three ways: + +@enumerate +@item +With an Org heading that represents an email TODO. Call +`gnorb-org-handle-mail' (see below) on the heading to compose a new +message, and start the tracking process. +@item +By calling org-capture on a received message. Any heading captured +from a message will automatically be associated with that message. +@item +By calling `gnorb-gnus-outgoing-do-todo' in a message composition +buffer -- see below. +@end enumerate + +There are three main email-related commands: + +@enumerate +@item +`gnorb-org-handle-mail' is called on an Org heading to compose a +new message. By default, this will begin a reply to the most recent +message in the conversation. If there are no associated messages to +reply to (or you call the function with a double prefix arg), Gnorb +will look for mailto: or bbdb: links in the heading, and compose a +new message to them. + +The sent message will be associated with the Org heading, and +you'll be brought back to the heading and asked to trigger an +action on it. + +`gnorb-email-subtree' is an alternative entry-point to +`gnorb-org-handle-mail'. It does the same thing as the latter, but +first exports the body of the subtree as either text or a file, +then inserts the text into the message body, or attaches the file +to the message, depending on what you've chosen. +@item +`gnorb-gnus-incoming-do-todo' is called on a message in a Gnus +*Summary* buffer. You'll be prompted for an Org heading, taken to +that heading, and asked to trigger an action on it. +@item +`gnorb-gnus-outgoing-do-todo' is called in message mode, while +composing a new message. + +If called without a prefix arg, a new Org heading will be created +after the message is sent, and the sent message associated with it. +The new heading will be created as a capture heading, using the +template specified by the `gnorb-gnus-new-todo-capture-key' option. + +If you call this function with a prefix arg, you'll be prompted to +choose an existing Org heading instead. After the the message is +sent, you'll be taken to that heading and prompted to trigger an +action on it. + +It's also possible to call this function *after* a message is sent, +in case you forgot. Gnorb saves information about the most recently +sent message for this purpose. +@end enumerate + +Because these three commands all express a similar intent, but are +called in different modes, it can make sense to give each of them the +same keybinding in the keymaps for Org mode, Gnus summary mode, and +Message mode, respectively. + +@node Trigger Actions +@section Trigger Actions + +After calling `gnorb-gnus-incoming-do-todo' on a message, or after +sending a message associated with an Org heading, you'll be taken to +the heading and asked to ``trigger an action'' on it. At the moment +there are four different possibilities: triggering a TODO state-change +on the heading, taking a note on the heading (both these options will +associate the message with the heading), associating the message but +doing nothing else, and lastly, doing nothing at all. + +More actions will be added in the future; it's also possible to add +your own action: see the docstring of `gnorb-org-trigger-actions'. + +@node Viewing Tracked Messages in *Summary* Buffers +@section Viewing Tracked Messages in *Summary* Buffers + +Call `gnorb-org-view' on an Org heading to open an nnir *Summary* +buffer showing all the messages associated with that heading (this +requires that you've added an nngnorb server to your Gnus backends). A +minor mode will be in effect, ensuring that any replies you send to +messages in this buffer will automatically be associated with the +original Org heading. You can also invoke +`gnorb-summary-disassociate-message' (``C-c d'') to disassociate the +message with the Org heading. + +As a bonus, it's possible to go into Gnus' *Server* buffer, find the +line specifying your nngnorb server, and hit ``G'' (aka +`gnus-group-make-nnir-group'). At the query prompt, enter an Org-style +tags-todo Agenda query string (eg ``+work-computer'', or what have you). +Gnorb will find all headings matching this query, scan their subtrees +for gnus links, and then give you a Summary buffer containing all the +linked messages. This is dog-slow at the moment; it will get faster. + +@node Hinting in Gnus +@section Hinting in Gnus + +When you receive new mails that might be relevant to existing Org +TODOs, Gnorb can alert you to that fact. When +`gnorb-gnus-hint-relevant-article' is t (the default), Gnorb will +display a message in the minibuffer when opening potentially relevant +messages. You can then use `gnorb-gnus-incoming-to-todo' to trigger an +action on the relevant TODO. + +This hinting can happen in the Gnus summary buffer as well. If you use +the escape indicated by `gnorb-gnus-summary-mark-format-letter'' as +part of your `gnus-summary-line-format', articles that are relevant to +TODOs will be marked with a special character in the Summary buffer, +as determined by `gnorb-gnus-summary-mark'. By default, the format +letter is ``g'' (meaning it is used as ``%ug'' in the format line), and +the mark is ``¡''. + +@node Message Attachments +@section Message Attachments + +Gnorb simplifies the handling of attachments that you receive in +emails. When you call `gnorb-gnus-incoming-do-todo' on a message, +you'll be prompted to re-attach the email's attachments onto the Org +heading, using the org-attach library. + +You can also do this as part of the capture process. Set the +new :gnus-attachments key to ``t'' in a capture template that you use on +mail messages, and you'll be queried to re-attach the message's +attachments onto the newly-captured heading. Or set +`gnorb-gnus-capture-always-attach' to ``t'' to have Gnorb do this for +all capture templates. + +You can also do this using the regular system of MIME commands, +without invoking the email tracking process. See @ref{Suggested Keybindings,Suggested +Keybindings}, below. + +The same process works in reverse: when you send a message from an Org +heading using `gnorb-org-handle-mail', Gnorb will ask if you want to +attach the files in the heading's org-attach directory to the outgoing +message. + +@node Restoring Window Layout +@chapter Restoring Window Layout + +Many Gnorb functions alter the window layout and value of point. In +most of these cases, you can restore the previous layout using the +interactive function `gnorb-restore-layout'. + +@node Recent Mails From BBDB Contacts +@chapter Recent Mails From BBDB Contacts + +If you're using a recent git version of BBDB (circa mid-May 2014 or +later), you can give your BBDB contacts a special field which will +collect links to recent emails from that contact. The default name of +the field is ``messages'', but you can customize that name using the +`gnorb-bbdb-messages-field' option. + +Gnorb will not collect links by default: you need to call +`gnorb-bbdb-open-link' on a contact once to start the process. +Thereafter, opening mails from that contact will store a link to the +message. + +Once some links are stored, `gnorb-bbdb-open-link' will open them: Use +a prefix arg to the function call to select particular messages to +open. There are several options controlling how all this works; see +the gnorb-bbdb user options section below for details. + +@node BBDB posting styles +@chapter BBDB posting styles + +Gnorb comes with a BBDB posting-style system, inspired by (copied +from) gnus-posting-styles. You can specify how messages are composed +to specific contacts, by matching on contact field values (the same +way gnus-posting-styles matches on group names). See the docstring of +`gnorb-bbdb-posting-styles' for details. + +In order not to be too intrusive, Gnorb doesn't alter the behavior of +`bbdb-mail', the usual mail-composition function. Instead it provides +an alternate `gnorb-bbdb-mail', which does exactly the same thing, but +first processes the new mail according to `gnorb-bbdb-posting-styles'. +If you want to use this feature regularly, you can remap `bbdb-mail' +to `gnorb-bbdb-mail' in the `bbdb-mode-map'. + +@node BBDB Org tagging +@chapter BBDB Org tagging + +BBDB contacts can be tagged with the same tags you use in your Org +files. This allows you to pop up a *BBDB* buffer alongside your Org +Agenda when searching for certain tags. This can happen automatically +for all Org tags-todo searches, if you set the option +`gnorb-org-agenda-popup-bbdb' to t. Or you can do it manually, by +calling the command of the same name. This command only shows TODOs by +default: use a prefix argument to show all tagged headings. + +Tags are stored in an xfield named org-tags, by default. You can +customize the name of this field using `gnorb-bbdb-org-tag-field'. + +@node Misc BBDB +@chapter Misc BBDB + +@menu +* Searching for messages from BBDB contacts:: +* Citing BBDB contacts:: +* User Options:: +@end menu + +@node Searching for messages from BBDB contacts +@section Searching for messages from BBDB contacts + +Call `gnorb-bbdb-mail-search' to search for all mail messages from the +record(s) displayed. Currently supports the notmuch, mairix, and +namazu search backends; set `gnorb-gnus-mail-search-backend' to one of +those symbol values. + +@node Citing BBDB contacts +@section Citing BBDB contacts + +Calling `gnorb-bbdb-cite-contact' will prompt for a BBDB record and +insert a string of the type ``Bob Smith ''. + +@node User Options +@section User Options + +@table @samp +@item `gnorb-bbdb-org-tag-field +The name of the BBDB xfield, as a +symbol, that holds Org-related tags. Specified as a string with +the ``:'' separator between tags, same as for Org headings. +Defaults to org-tag. +@item `gnorb-bbdb-messages-field' +The name of the BBDB xfield that +holds links to recently-received messages from this contact. +Defaults to `messages. +@item `gnorb-bbdb-collect-N-messages' +Collect at most this many links +to messages from this contact. Defaults to 5. +@item `gnorb-bbdb-define-recent' +What does ``recently-received'' mean? +Possible values are the symbols seen and received. When set to +seen, the most recently-opened messages are collected. When set +to received, the most recently-received (by Date header) messages +are collected. Defaults to seen. +@item `gnorb-bbdb-message-link-format-multi' +How is a single message's +link formatted in the multi-line BBDB layout format? Defaults to +``%:count. %D: %:subject'' (see the docstring for details). +@item ` gnorb-bbdb-message-link-format-one' +How is a single message's +link formatted in the one-line BBDB layout format? Defaults to +nil (see the docstring for details). +@item `gnorb-bbdb-posting-styles' +Styles to use for influencing the +format of mails composed to the BBDB record(s) under point (see +the docstring for details). +@end table + +@node Misc Org +@chapter Misc Org + +@menu +* Inserting BBDB links:: +* User Options: User Optionsx. +@end menu + +@node Inserting BBDB links +@section Inserting BBDB links + +Calling `gnorb-org-contact-link' will prompt for a BBDB record and +insert an Org link to that record at point. + +@node User Optionsx +@section User Options + +@table @samp +@item `gnorb-org-after-message-setup-hook' +Hook run in a message buffer +after setting up the message, from `gnorb-org-handle-mail' or +`gnorb-org-email-subtree'. +@item `gnorb-org-trigger-actions' +List of potential actions that can be +taken on headings after a message is sent. See docstring for +details. +@item `gnorb-org-mail-scan-scope' +The number of paragraphs to scan for +mail-related links. This comes into play when calling +`gnorb-org-handle-mail' on a heading with no associated messages, +or when `gnorb-org-handle-mail' is called with a prefix arg. +@item `gnorb-org-find-candidates-match' +When searching all Org files +for headings to collect messages from, this option can limit +which headings are searched. It is used as the second argument to +a call to `org-map-entries', and has the same syntax as that used +in an agenda tags view. +@item `gnorb-org-email-subtree-text-parameters' +A plist of export +parameters corresponding to the EXT-PLIST argument to the export +functions, for use when exporting to text. +@item `gnorb-org-email-subtree-file-parameters' +A plist of export +parameters corresponding to the EXT-PLIST argument to the export +functions, for use when exporting to a file. +@item `gnorb-org-email-subtree-text-options' +A list of ts and nils +corresponding to Org's export options, to be used when exporting +to text. The options, in order, are async, subtreep, +visible-only, and body-only. +@item `gnorb-org-email-subtree-file-options' +A list of ts and nils +corresponding to Org's export options, to be used when exporting +to a file. The options, in order, are async, subtreep, +visible-only, and body-only. +@item `gnorb-org-export-extensions' +Correspondence between export +backends and their respective (usual) file extensions. +@item `gnorb-org-capture-collect-link-p' +When this is set to t, the +capture process will always store a link to the Gnus message or +BBDB record under point, even when the link isn't part of the +capture template. It can then be added to the captured heading +with org-insert-link, as usual. +@item `gnorb-org-agenda-popup-bbdb' +Set to ``t'' to automatically pop up +the BBDB buffer displaying records corresponding to the Org +Agenda tags search underway. If this is nil you can always do it +manually with the command of the same name. +@item `gnorb-org-bbdb-popup-layout' +Controls the layout of the +Agenda-related BBDB popup, takes the same values as +bbdb-pop-up-layout. +@end table + +@node Misc Gnus +@chapter Misc Gnus + +@menu +* Viewing Org headlines relevant to a message:: +* User Options: User Optionsxx. +@end menu + +@node Viewing Org headlines relevant to a message +@section Viewing Org headlines relevant to a message + +Call `gnorb-gnus-view' on a message that is associated with an Org +heading to jump to that heading. + +@node User Optionsxx +@section User Options + +@table @samp +@item `gnorb-gnus-mail-search-backend' +Specifies the search backend +that you use for searching mails. Currently supports notmuch, +mairix, and namazu: set this option to one of those symbols. +@item `gnorb-gnus-capture-always-attach' +Treat all capture templates as +if they had the :gnus-attachments key set to ``t''. This only has +any effect if you're capturing from a Gnus summary or article +buffer. +@item `gnorb-trigger-todo-default' +Set to either `note or `todo to tell +`gnorb-gnus-incoming-do-todo' what to do by default. You can +reach the non-default behavior by calling that function with a +prefix argument. Alternately, set to `prompt to always prompt for +the appropriate action. +@item `gnorb-gnus-trigger-refile-targets' +If you use +`gnorb-gnus-incoming-do-todo' on an incoming message, Gnorb will +try to locate a TODO heading that's relevant to that message. If +it can't, it will prompt you for one, using the refile interface. +This option will be used as the value of `org-refile-targets' +during that process: see the docstring of `org-refile-targets' +for the appropriate syntax. +@item `gnorb-gnus-new-todo-capture-key' +Set this to a single-character +string pointing at an Org capture template to use when creating +TODOs from outgoing messages. The template is a regular capture +template, with a few exceptions. If Gnus helps you archive +outgoing messages (ie you have `gnus-message-archive-group' set +to something, and your outgoing messages have a ``Fcc'' header), a +link to that message will be made, and you'll be able to use all +the escapes related to gnus messages. If you don't archive +outgoing messages, you'll still be able to use the %:subject, +%:to, %:toname, %:toaddress, and %:date escapes in the capture +template. +@item `gnorb-gnus-hint-relevant-article' +Set to ``t'' (the default) to +have Gnorb give you a hint in the minibuffer when opening +messages that might be relevant to existing Org TODOs. +@item `gnorb-gnus-summary-mark-format-letter' +The formatting letter to +use as part of your `gnus-summary-line-format', to indicate +messages which might be relevant to Org TODOs. Defaults to ``g'', +meaning it should be used as ``%ug'' in the format line. +@item `gnorb-gnus-summary-mark' +The mark used to indicate relevant +messages in the Summary buffer, when +`gnorb-gnus-summary-mark-format-letter' is present in the format +line. Defaults to ``¡''. +@end table + +@node Suggested Keybindings +@chapter Suggested Keybindings + +@lisp +(eval-after-load "gnorb-bbdb" + '(progn + (define-key bbdb-mode-map (kbd "O") 'gnorb-bbdb-tag-agenda) + (define-key bbdb-mode-map (kbd "S") 'gnorb-bbdb-mail-search) + (define-key bbdb-mode-map [remap bbdb-mail] 'gnorb-bbdb-mail) + (define-key bbdb-mode-map (kbd "l") 'gnorb-bbdb-open-link) + (global-set-key (kbd "C-c C") 'gnorb-bbdb-cite-contact))) + +(eval-after-load "gnorb-org" + '(progn + (org-defkey org-mode-map (kbd "C-c C") 'gnorb-org-contact-link) + (org-defkey org-mode-map (kbd "C-c t") 'gnorb-org-handle-mail) + (org-defkey org-mode-map (kbd "C-c e") 'gnorb-org-view) + (org-defkey org-mode-map (kbd "C-c E") 'gnorb-org-email-subtree) + (org-defkey org-mode-map (kbd "C-c V") 'gnorb-org-popup-bbdb) + (setq gnorb-org-agenda-popup-bbdb t) + (eval-after-load "org-agenda" + '(progn (org-defkey org-agenda-mode-map (kbd "H") 'gnorb-org-handle-mail) + (org-defkey org-agenda-mode-map (kbd "V") 'gnorb-org-popup-bbdb))))) + +(eval-after-load "gnorb-gnus" + '(progn + (define-key gnus-summary-mime-map "a" 'gnorb-gnus-article-org-attach) + (define-key gnus-summary-mode-map (kbd "C-c t") 'gnorb-gnus-incoming-do-todo) + (push '("attach to org heading" . gnorb-gnus-mime-org-attach) + gnus-mime-action-alist) + ;; The only way to add mime button command keys is by redefining + ;; gnus-mime-button-map, possibly not ideal. Ideal would be a + ;; setter function in gnus itself. + (push '(gnorb-gnus-mime-org-attach "a" "Attach to Org heading") + gnus-mime-button-commands) + (setq gnus-mime-button-map + (let ((map (make-sparse-keymap))) + (define-key map gnus-mouse-2 'gnus-article-push-button) + (define-key map gnus-down-mouse-3 'gnus-mime-button-menu) + (dolist (c gnus-mime-button-commands) + (define-key map (cadr c) (car c))) + map)))) + +(eval-after-load "message" + '(progn + (define-key message-mode-map (kbd "C-c t") 'gnorb-gnus-outgoing-do-todo))) +@end lisp + +@node Wishlist/TODO +@chapter Wishlist/TODO + +@itemize +@item +Provide a command that, when in the Org Agenda, does an email search +for messages received in the visible date span, or day under point, +etc. Make it work in the calendar, as well? +@item +Add trigger actions that create new sibling or child headings on the +original Org heading. +@item +Allow tagging of Gnus messages, by giving the message's registry +entry an `org-tags key. +@item +Provide persistent nngnorb search groups. +@item +Allow automatic org-tagging of BBDB contacts: when messages from a +contact are associated with an Org heading, make it possible for the +contact to inherit that heading's tags automatically. +@item +Provide completion when setting Org tags on a BBDB contact. +@item +Provide a `gnorb-bbdb-view' command that opens a *Summary* buffer +containing all the tracked messages from the contact(s) under point. +@item +Provide a `gnorb-view' command that takes a tags-todo search phrase +(or a single Org heading ID), finds all relevant messages, Org +headings, and BBDB records, and sets up a four-pane view: Org +Agenda, **Article* SummaryBBDB* buffer, Gnus *buffer, and an * +buffer. +@end itemize + +@node Index +@chapter Index + +@c Emacs 25.0.50.8 (Org mode 8.3beta) +@bye \ No newline at end of file diff --git a/nngnorb.el b/nngnorb.el new file mode 100644 index 000000000..bdaf569bc --- /dev/null +++ b/nngnorb.el @@ -0,0 +1,375 @@ +;;; nngnorb.el --- Gnorb backend for Gnus + +;; This file is in the public domain. + +;; Author: Eric Abrahamsen + +;; This file is part of GNU Emacs. + +;; GNU Emacs is free software: you can redistribute it and/or modify +;; it under the terms of the GNU General Public License as published by +;; the Free Software Foundation, either version 3 of the License, or +;; (at your option) any later version. + +;; GNU Emacs is distributed in the hope that it will be useful, +;; but WITHOUT ANY WARRANTY; without even the implied warranty of +;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +;; GNU General Public License for more details. + +;; You should have received a copy of the GNU General Public License +;; along with GNU Emacs. If not, see . + +;;; Commentary: + +;; This is a backend for supporting Gnorb-related stuff. I'm going to +;; regret this, I know. + +;; It started off just with wanting to collect all the gnus links in a +;; subtree, and display all the messages in an ephemeral group. But it +;; doesn't seem possible to create ephemeral groups without +;; associating them with a server, and which server would that be? +;; Nnir also provides a nice interface to creating ephemeral groups, +;; but again, it relies on a server parameter to know which nnir +;; engine to use, and if you try to fake it it still craps out. + +;; So this file is a copy-pasta from nnnil.el -- I'm trying to keep +;; this as simple as possible. Right now it does nothing but serving +;; as a place to hang ephemeral groups made with nnir searches of +;; message from the rest of your gnus installation. Enjoy. + +;;; Code: + +(eval-and-compile + (require 'nnheader) + (require 'nnir)) + +(defvar nngnorb-status-string "") + +(defvar nngnorb-attachment-file-list nil + "A place to store Org attachments relevant to the subtree being + viewed.") + +(make-variable-buffer-local 'nngnorb-attachment-file-list) + +(gnus-declare-backend "nngnorb" 'none) + +(add-to-list 'nnir-method-default-engines '(nngnorb . gnorb)) + +(add-to-list 'nnir-engines + '(gnorb nnir-run-gnorb)) + +(defun nnir-run-gnorb (query server &optional group) + "Run the actual search for messages to display. See nnir.el for +some details of how this gets called. + +As things stand, the query string can be given as one of two +different things. First is the ID string of an Org heading, +prefixed with \"id+\". This was probably a bad choice as it could +conceivably look like an org tags search string. Fix that later. +If it's an ID, then the entire subtree text of that heading is +scanned for gnus links, and the messages relevant to the subtree +are collected from the registry, and all the resulting messages +are displayed in an ephemeral group. + +Otherwise, the query string can be a tags match string, a la the +Org agenda tags search. All headings matched by this string will +be scanned for gnus messages, and those messages displayed." + ;; During the transition period between using message-ids stored in + ;; a property, and the new registry-based system, we're going to use + ;; both methods to collect relevant messages. This could be a little + ;; slower, but for the time being it will be safer. + (save-excursion + (let ((q (cdr (assq 'query query))) + (buf (get-buffer-create nnir-tmp-buffer)) + msg-ids org-ids links vectors) + (with-current-buffer buf + (erase-buffer) + (setq nngnorb-attachment-file-list nil)) + (when (equal "5.13" gnus-version-number) + (setq q (car q))) + (cond ((string-match "id\\+\\([[:alnum:]-]+\\)$" q) + (with-demoted-errors "Error: %S" + (org-id-goto (match-string 1 q)) + (append-to-buffer + buf + (point) + (org-element-property + :end (org-element-at-point))) + (save-restriction + (org-narrow-to-subtree) + (setq org-ids + (append + (gnorb-collect-ids) + org-ids)) + (when org-ids + (with-current-buffer buf + ;; The file list var is buffer local, so set it + ;; (local to the nnir-tmp-buffer) to a full list + ;; of all files in the subtree. + (dolist (id org-ids) + (setq nngnorb-attachment-file-list + (append (gnorb-org-attachment-list id) + nngnorb-attachment-file-list)))))))) + ((listp q) + ;; be a little careful: this could be a list of links, or + ;; it could be the full plist + (setq links (if (plist-member q :gnus) + (plist-get q :gnus) + q))) + (t (org-map-entries + (lambda () + (push (org-id-get) org-ids) + (append-to-buffer + buf + (point) + (save-excursion + (outline-next-heading) + (point)))) + q + 'agenda))) + (with-current-buffer buf + (goto-char (point-min)) + (setq links (plist-get (gnorb-scan-links (point-max) 'gnus) + :gnus)) + (goto-char (point-min)) + (while (re-search-forward + (concat ":" gnorb-org-msg-id-key ": \\([^\n]+\\)") + (point-max) t) + (setq msg-ids (append (split-string (match-string 1)) msg-ids)))) + ;; Here's where we maybe do some duplicate work using the + ;; registry. Take our org ids and find all relevant message ids. + (dolist (i (delq nil org-ids)) + (let ((rel-msg-id (gnorb-registry-org-id-search i))) + (when rel-msg-id + (setq msg-ids (append rel-msg-id msg-ids))))) + (when msg-ids + (dolist (id msg-ids) + (let ((link (gnorb-msg-id-to-link id))) + (when link + (push link links))))) + (setq links (delete-dups links)) + (unless (gnus-alive-p) + (gnus)) + (dolist (m links (when vectors + (nreverse vectors))) + (let (server-group msg-id result artno) + (setq m (org-link-unescape m)) + (when (string-match "\\`\\([^#]+\\)\\(#\\(.*\\)\\)?" m) + (setq server-group (match-string 1 m) + msg-id (match-string 3 m) + result (ignore-errors (gnus-request-head msg-id server-group))) + (when result + (setq artno (cdr result)) + (when (and (integerp artno) (> artno 0)) + (push (vector server-group artno 100) vectors))))))))) + +(defvar gnorb-summary-minor-mode-map (make-sparse-keymap) + "Keymap for use in Gnorb's *Summary* minor mode.") + +(define-minor-mode gnorb-summary-minor-mode + "A minor mode for use in nnir *Summary* buffers created by Gnorb. + +These *Summary* buffers are usually created by calling +`gnorb-org-view', or by initiating an nnir search on a nngnorb server. + +While active, this mode provides some Gnorb-specific commands, +and also advises Gnus' reply-related commands in order to +continue to provide tracking of sent messages." + nil " Gnorb" gnorb-summary-minor-mode-map + (setq nngnorb-attachment-file-list + ;; Copy the list of attached files from the nnir-tmp-buffer to + ;; this summary buffer. + (buffer-local-value + 'nngnorb-attachment-file-list + (get-buffer nnir-tmp-buffer)))) + +(define-key gnorb-summary-minor-mode-map + [remap gnus-summary-exit] + 'gnorb-summary-exit) + +(define-key gnorb-summary-minor-mode-map (kbd "C-c d") + 'gnorb-summary-disassociate-message) + +;; All this is pretty horrible, but it's the only way to get sane +;; behavior, there are no appropriate hooks, and I want to avoid +;; advising functions. + +(define-key gnorb-summary-minor-mode-map + [remap gnus-summary-very-wide-reply-with-original] + 'gnorb-summary-very-wide-reply-with-original) + +(define-key gnorb-summary-minor-mode-map + [remap gnus-summary-wide-reply-with-original] + 'gnorb-summary-wide-reply-with-original) + +(define-key gnorb-summary-minor-mode-map + [remap gnus-summary-reply] + 'gnorb-summary-reply) + +(define-key gnorb-summary-minor-mode-map + [remap gnus-summary-very-wide-reply] + 'gnorb-summary-very-wide-reply) + +(define-key gnorb-summary-minor-mode-map + [remap gnus-summary-reply-with-original] + 'gnorb-summary-reply-with-original) + +(define-key gnorb-summary-minor-mode-map + [remap gnus-summary-wide-reply] + 'gnorb-summary-wide-reply) + +(define-key gnorb-summary-minor-mode-map + [remap gnus-summary-mail-forward] + 'gnorb-summary-mail-forward) + +(defun gnorb-summary-wide-reply (&optional yank) + (interactive + (list (and current-prefix-arg + (gnus-summary-work-articles 1)))) + (gnorb-summary-reply yank t)) + +(defun gnorb-summary-reply-with-original (n &optional wide) + (interactive "P") + (gnorb-summary-reply (gnus-summary-work-articles n) wide)) + +(defun gnorb-summary-very-wide-reply (&optional yank) + (interactive + (list (and current-prefix-arg + (gnus-summary-work-articles 1)))) + (gnorb-summary-reply yank t (gnus-summary-work-articles yank))) + +(defun gnorb-summary-reply (&optional yank wide very-wide) + (interactive) + (gnus-summary-reply yank wide very-wide) + (gnorb-summary-reply-hook)) + +(defun gnorb-summary-wide-reply-with-original (n) + (interactive "P") + (gnorb-summary-reply-with-original n t)) + +(defun gnorb-summary-very-wide-reply-with-original (n) + (interactive "P") + (gnorb-summary-reply + (gnus-summary-work-articles n) t (gnus-summary-work-articles n))) + +(defun gnorb-summary-mail-forward (n) + (interactive "P") + (gnus-summary-mail-forward n t) + (gnorb-summary-reply-hook)) + +(defun gnorb-summary-reply-hook (&rest args) + "Function that runs after any command that creates a reply." + ;; Not actually a "hook" + (let* ((msg-id (aref message-reply-headers 4)) + (org-id (car-safe (gnus-registry-get-id-key msg-id 'gnorb-ids))) + (compose-marker (make-marker)) + (attachments (buffer-local-value + 'nngnorb-attachment-file-list + (get-buffer nnir-tmp-buffer)))) + (when org-id + (move-marker compose-marker (point)) + (save-restriction + (widen) + (message-narrow-to-headers-or-head) + (goto-char (point-at-bol)) + (open-line 1) + (message-insert-header + (intern gnorb-mail-header) + org-id) + (add-to-list 'message-exit-actions + 'gnorb-org-restore-after-send t)) + (goto-char compose-marker)) + (when attachments + (map-y-or-n-p + (lambda (a) (format "Attach %s to outgoing message? " + (file-name-nondirectory a))) + (lambda (a) + (mml-attach-file a (mm-default-file-encoding a) + nil "attachment")) + attachments + '("file" "files" "attach"))))) + +(defun gnorb-summary-exit () + "Like `gnus-summary-exit', but restores the gnorb window conf." + (interactive) + (call-interactively 'gnus-summary-exit) + (gnorb-restore-layout)) + +(defun gnorb-summary-disassociate-message () + "Disassociate a message from its Org TODO. + +This is used in a Gnorb-created *Summary* buffer to remove the +connection between the message and whichever Org TODO resulted in +the message being included in this search." + (interactive) + (let* ((msg-id (gnus-fetch-original-field "message-id")) + (org-ids (gnus-registry-get-id-key msg-id 'gnorb-ids)) + chosen) + (when org-ids + (if (= (length org-ids) 1) + ;; Only one associated Org TODO. + (progn (gnus-registry-set-id-key msg-id 'gnorb-ids) + (setq chosen (car org-ids))) + ;; Multiple associated TODOs, prompt to choose one. + (setq chosen + (cdr + (org-completing-read + "Choose a TODO to disassociate from: " + (mapcar + (lambda (h) + (cons (gnorb-pretty-outline h) h)) + org-ids)))) + (gnus-registry-set-id-key msg-id 'gnorb-ids + (remove chosen org-ids))) + (message "Message disassociated from %s" + (gnorb-pretty-outline chosen))))) + +(defvar nngnorb-status-string "") + +(defun nngnorb-retrieve-headers (articles &optional group server fetch-old) + (with-current-buffer nntp-server-buffer + (erase-buffer)) + 'nov) + +(defun nngnorb-open-server (server &optional definitions) + t) + +(defun nngnorb-close-server (&optional server) + t) + +(defun nngnorb-request-close () + t) + +(defun nngnorb-server-opened (&optional server) + t) + +(defun nngnorb-status-message (&optional server) + nngnorb-status-string) + +(defun nngnorb-request-article (article &optional group server to-buffer) + (setq nngnorb-status-string "No such group") + nil) + +(defun nngnorb-request-group (group &optional server fast info) + (let (deactivate-mark) + (with-current-buffer nntp-server-buffer + (erase-buffer) + (insert "411 no such news group\n"))) + (setq nngnorb-status-string "No such group") + nil) + +(defun nngnorb-close-group (group &optional server) + t) + +(defun nngnorb-request-list (&optional server) + (with-current-buffer nntp-server-buffer + (erase-buffer)) + t) + +(defun nngnorb-request-post (&optional server) + (setq nngnorb-status-string "Read-only server") + nil) + +(provide 'nngnorb) + +;;; nnnil.el ends here