1 ;;; gnorb-gnus.el --- The gnus-centric fuctions of gnorb
3 ;; Copyright (C) 2014 Free Software Foundation, Inc.
5 ;; Author: Eric Abrahamsen <eric@ericabrahamsen.net>
8 ;; This program is free software; you can redistribute it and/or modify
9 ;; it under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation, either version 3 of the License, or
11 ;; (at your option) any later version.
13 ;; This program is distributed in the hope that it will be useful,
14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 ;; GNU General Public License for more details.
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
28 (require 'gnorb-utils)
30 (declare-function org-gnus-article-link "org-gnus"
31 (group newsgroups message-id x-no-archive))
32 (declare-function org-gnus-follow-link "org-gnus"
35 (defgroup gnorb-gnus nil
36 "The Gnus bits of Gnorb."
41 (defcustom gnorb-gnus-mail-search-backends
42 '((notmuch (lambda (terms)
45 (replace-regexp-in-string "\\." "\\\\." m))
48 (mairix (lambda (terms)
52 (namazu (lambda (terms)
56 "Various backends for mail search.
58 An alist of backends, where each element consists of three parts:
59 the symbol name of the backend, a lambda form which receives a
60 list of email addresses and returns a properly-formatted search
61 string, and the symbol name of the function used to initiate the
66 (defcustom gnorb-gnus-mail-search-backend nil
67 "Mail search backend currently in use. One of the three symbols
68 notmuch, namazu, or mairix."
72 (defcustom gnorb-gnus-capture-always-attach nil
73 "Always prompt about attaching attachments when capturing from
74 a Gnus message, even if the template being used hasn't
75 specified the :gnus-attachments key.
77 Basically behave as if all attachments have \":gnus-attachments t\"."
81 (defcustom gnorb-gnus-new-todo-capture-key nil
82 "Key for the capture template to use when creating a new TODO
83 from an outgoing message."
87 (defcustom gnorb-gnus-hint-relevant-article t
88 "When opening a gnus message, should gnorb let you know if the
89 message is relevant to an existing TODO?"
93 (defcustom gnorb-gnus-summary-mark-format-letter "g"
94 "Format letter to be used as part of your
95 `gnus-summary-line-format', to indicate in the *Summary* buffer
96 which articles might be relevant to TODOs. Since this is a user
97 format code, it should be prefixed with %u, eg %ug. It will
98 result in the insertion of the value of
99 `gnorb-gnus-summary-mark', for relevant messages, or
104 (defcustom gnorb-gnus-summary-mark "ยก"
105 "Default mark to insert in the summary format line of articles
106 that are likely relevant to existing TODO headings."
110 (defcustom gnorb-gnus-summary-tracked-mark "&"
111 "Default mark to insert in the summary format line of articles
112 that are already tracked by TODO headings."
116 (defcustom gnorb-gnus-trigger-refile-targets
117 '((org-agenda-files :maxlevel . 4))
118 "A value to use as an equivalent of `org-refile-targets' (which
119 see) when offering trigger targets for
120 `gnorb-gnus-incoming-do-todo'."
124 (defcustom gnorb-gnus-sent-groups nil
125 "A list of strings indicating sent mail groups.
127 In some cases, Gnorb can't detect where your sent messages are
128 stored (ie if you're using IMAP sent mail folders instead of
129 local archiving. If you want Gnorb to be able to find sent
130 messages, this option can help it do that. It should be set to a
131 list of strings, which are assumed to be fully qualified
132 server+group combinations, ie \"nnimap+Server:[Gmail]/Sent
133 Mail\", or something similar. This only has to be done once for
138 (defvar gnorb-gnus-capture-attachments nil
139 "Holding place for attachment names during the capture
142 ;;; What follows is a very careful copy-pasta of bits and pieces from
143 ;;; mm-decode.el and gnus-art.el. Voodoo was involved.
146 (defun gnorb-gnus-article-org-attach (n)
147 "Save MIME part N, which is the numerical prefix, of the
148 article under point as an attachment to the specified org
151 (gnus-article-part-wrapper n 'gnorb-gnus-attach-part))
154 (defun gnorb-gnus-mime-org-attach ()
155 "Save the MIME part under point as an attachment to the
156 specified org heading."
158 (gnus-article-check-buffer)
159 (let ((data (get-text-property (point) 'gnus-data)))
161 (gnorb-gnus-attach-part data))))
163 (defun gnorb-gnus-attach-part (handle &optional org-heading)
164 "Attach HANDLE to an existing org heading."
165 (let* ((filename (gnorb-gnus-save-part handle))
166 (org-refile-targets gnorb-gnus-trigger-refile-targets)
167 (headers (gnus-data-header
169 (gnus-summary-article-number))))
170 (tracked-headings (gnorb-find-tracked-headings headers))
172 (gnorb-choose-trigger-heading tracked-headings)))
173 (require 'org-attach)
174 (save-window-excursion
175 (org-id-goto target-heading)
176 (org-attach-attach filename nil 'mv))))
178 (defun gnorb-gnus-save-part (handle)
179 (let ((filename (or (mail-content-type-get
180 (mm-handle-disposition handle) 'filename)
181 (mail-content-type-get
182 (mm-handle-type handle) 'name))))
184 (gnus-map-function mm-file-name-rewrite-functions
185 (file-name-nondirectory filename)))
186 (setq filename (expand-file-name filename gnorb-tmp-dir))
187 (mm-save-part-to-file handle filename)
190 (defun gnorb-gnus-collect-all-attachments (&optional capture-p store)
191 "Collect all the attachments from the message under point, and
192 save them into `gnorb-tmp-dir'."
193 (save-window-excursion
195 (set-buffer (org-capture-get :original-buffer)))
196 (unless (memq major-mode '(gnus-summary-mode gnus-article-mode))
197 (error "Only works in Gnus summary or article buffers"))
198 (let ((article (gnus-summary-article-number))
200 (when (or (null gnus-current-article)
201 (null gnus-article-current)
202 (/= article (cdr gnus-article-current))
203 (not (equal (car gnus-article-current) gnus-newsgroup-name)))
204 (gnus-summary-display-article article))
205 (gnus-eval-in-buffer-window gnus-article-buffer
206 (setq mime-handles (cl-remove-if-not
208 (let ((disp (mm-handle-disposition (cdr h))))
209 (and (member (car disp)
210 '("inline" "attachment"))
211 (mail-content-type-get disp 'filename))))
212 gnus-article-mime-handle-alist)))
214 (dolist (h mime-handles)
216 (gnorb-gnus-save-part (cdr h))))
217 (when (or capture-p store)
218 (push filename gnorb-gnus-capture-attachments))))))))
220 ;;; Make the above work in the capture process
222 (defun gnorb-gnus-capture-attach ()
223 (when (and (or gnorb-gnus-capture-always-attach
224 (org-capture-get :gnus-attachments))
226 (org-capture-get :original-buffer)
227 (memq major-mode '(gnus-summary-mode gnus-article-mode))))
228 (require 'org-attach)
229 (setq gnorb-gnus-capture-attachments nil)
230 (gnorb-gnus-collect-all-attachments t)
233 (format "Attach %s to capture heading? "
234 (file-name-nondirectory a)))
235 (lambda (a) (org-attach-attach a nil 'mv))
236 gnorb-gnus-capture-attachments
237 '("file" "files" "attach"))
238 (setq gnorb-gnus-capture-attachments nil)))
240 (add-hook 'org-capture-mode-hook 'gnorb-gnus-capture-attach)
242 (defvar org-note-abort)
244 (defun gnorb-gnus-capture-abort-cleanup ()
245 (with-no-warnings ; For `org-note-abort'
246 (when (and org-note-abort
247 (or gnorb-gnus-capture-always-attach
248 (org-capture-get :gnus-attachments)))
249 (condition-case error
250 (progn (org-attach-delete-all)
251 (setq abort-note 'clean)
252 ;; remove any gnorb-mail-header values here
255 (setq abort-note 'dirty))))))
257 (add-hook 'org-capture-prepare-finalize-hook
258 'gnorb-gnus-capture-abort-cleanup)
260 ;;; Storing, removing, and acting on Org headers in messages.
262 (defvar gnorb-gnus-message-info nil
263 "Place to store the To, Subject, Date, and Message-ID headers
264 of the currently-sending or last-sent message.")
266 (defun gnorb-gnus-check-outgoing-headers ()
267 "Save the value of the `gnorb-mail-header' for the current
268 message; multiple header values returned as a string. Also save
269 information about the outgoing message into
270 `gnorb-gnus-message-info'."
272 (message-narrow-to-headers)
273 (setq gnorb-gnus-message-info nil)
274 (let* ((org-ids (mail-fetch-field gnorb-mail-header nil nil t))
275 (msg-id (mail-fetch-field "Message-ID"))
276 (refs (mail-fetch-field "References"))
277 (in-reply-to (mail-fetch-field "In-Reply-To"))
278 (to (if (message-news-p)
279 (mail-fetch-field "Newsgroups")
280 (mail-fetch-field "To")))
281 (from (mail-fetch-field "From"))
282 (subject (mail-fetch-field "Subject"))
283 (date (mail-fetch-field "Date"))
284 ;; If we can get a link, that's awesome.
285 (gcc (mail-fetch-field "Gcc"))
287 (org-store-link nil))
289 (group (ignore-errors (car (split-string link "#")))))
290 ;; If we can't make a real link, then save some information so
293 (setq refs (concat refs " " in-reply-to)))
295 (setq refs (gnus-extract-references refs)))
296 (setq gnorb-gnus-message-info
297 `(:subject ,subject :msg-id ,msg-id
299 :link ,link :date ,date :refs ,refs
304 (setq gnorb-message-org-ids org-ids)
305 ;; `gnorb-org-setup-message' may have put this here, but
306 ;; if we're working from a draft, or triggering this from
307 ;; a reply, it might not be there yet.
308 (add-to-list 'message-send-actions
309 'gnorb-org-restore-after-send t))
310 (setq gnorb-message-org-ids nil)))))
312 ;; This sets the global value, but the hook is made buffer-local in
313 ;; `gnus-inews-add-send-actions', so this is ignored
314 ;(add-hook 'message-header-hook 'gnorb-gnus-check-outgoing-headers)
315 (add-hook 'message-send-hook 'gnorb-gnus-check-outgoing-headers t)
318 (defun gnorb-gnus-outgoing-do-todo (&optional arg)
319 "Use this command to use the message currently being composed
320 as an email todo action.
322 If it's a new message, or a reply to a message that isn't
323 referenced by any TODOs, a new TODO will be created.
325 If it references an existing TODO, you'll be prompted to trigger
326 a state-change or a note on that TODO after the message is sent.
328 You can call it with a prefix arg to force choosing an Org
329 subtree to associate with.
331 If you've already called this command, but realize you made a
332 mistake, you can call this command with a double prefix to reset
335 If a new todo is made, it needs a capture template: set
336 `gnorb-gnus-new-todo-capture-key' to the string key for the
337 appropriate capture template. If you're using a gnus-based
338 archive method (ie you have `gnus-message-archive-group' set to
339 something, and your outgoing messages have a \"Fcc\" header),
340 then a real link will be made to the outgoing message, and all
341 the gnus-type escapes will be available (see the Info
342 manual (org) Template expansion section). If you don't, then the
343 %:subject, %:to, %:toname, %:toaddress, and %:date escapes for
344 the outgoing message will still be available -- nothing else will
347 (let ((org-refile-targets gnorb-gnus-trigger-refile-targets)
348 (compose-marker (make-marker))
349 header-ids ref-ids rel-headings
350 gnorb-window-conf in-reply-to)
351 (when (equal arg '(4))
353 (org-refile-get-location "Trigger action on" nil t))
355 (list (list (save-window-excursion
356 (find-file (nth 1 rel-headings))
357 (goto-char (nth 3 rel-headings))
358 (org-id-get-create))))))
359 (if (not (eq major-mode 'message-mode))
360 ;; The message is already sent, so we're relying on whatever was
361 ;; stored into `gnorb-gnus-message-info'.
362 (if (equal arg '(16))
363 (user-error "A double prefix is only useful with an
367 (push (car rel-headings) gnorb-message-org-ids)
368 (gnorb-org-restore-after-send))
369 (setq ref-ids (plist-get gnorb-gnus-message-info :refs))
371 ;; the message might be relevant to some TODO
372 ;; heading(s). But if there had been org-id
373 ;; headers, they would already have been
374 ;; handled when the message was sent.
376 (setq rel-headings (gnorb-find-visit-candidates ref-ids))
377 (if (not rel-headings)
378 (gnorb-gnus-outgoing-make-todo-1)
379 (dolist (h rel-headings)
380 (push h gnorb-message-org-ids))
381 (gnorb-org-restore-after-send)))
382 ;; not relevant, just make a new TODO
383 (gnorb-gnus-outgoing-make-todo-1))))
384 ;; We are still in the message composition buffer, so let's see
387 (if (equal arg '(16))
388 ;; Double prefix arg means delete the association we already
393 (setq message-send-actions
394 (remove 'gnorb-gnus-outgoing-make-todo-1
395 message-send-actions))
396 (message-narrow-to-headers-or-head)
397 (message-remove-header
399 (message "Message associations have been reset")))
400 ;; Save-excursion won't work, because point will move if we
402 (move-marker compose-marker (point))
405 (message-narrow-to-headers-or-head)
406 (setq header-ids (mail-fetch-field gnorb-mail-header nil nil t))
407 ;; With a prefix arg we do not check references, because the
408 ;; whole point is to add new references. We still want to know
409 ;; what org id headers are present, though, so we don't add
411 (setq ref-ids (unless arg (mail-fetch-field "References" t)))
412 (setq in-reply-to (unless arg (mail-fetch-field "In-Reply-to" t)))
414 (setq ref-ids (concat ref-ids " " in-reply-to)))
416 ;; if the References header points to any message ids that are
417 ;; tracked by TODO headings...
418 (setq rel-headings (gnorb-find-visit-candidates ref-ids)))
420 (goto-char (point-min))
421 (dolist (h (delete-dups rel-headings))
422 ;; then get the org-ids of those headings, and insert
423 ;; them into this message as headers. If the id was
424 ;; already present in a header, don't add it again.
425 (unless (member h header-ids)
426 (goto-char (point-at-bol))
428 (message-insert-header
429 (intern gnorb-mail-header)
431 ;; tell the rest of the function that this is a relevant
433 (push h header-ids)))))
434 (goto-char compose-marker)
436 (add-to-list 'message-send-actions
437 'gnorb-gnus-outgoing-make-todo-1 t))
440 "Message will trigger TODO state-changes after sending"
441 "A TODO will be made from this message after it's sent"))))))
443 (defvar org-capture-link-is-already-stored)
445 (defun gnorb-gnus-outgoing-make-todo-1 ()
446 (unless gnorb-gnus-new-todo-capture-key
447 (error "No capture template key set, customize gnorb-gnus-new-todo-capture-key"))
448 (let* ((link (plist-get gnorb-gnus-message-info :link))
449 (group (plist-get gnorb-gnus-message-info :group))
450 (date (plist-get gnorb-gnus-message-info :date))
454 (org-time-stamp-format t)
455 (date-to-time date)))))
456 (date-ts-ia (and date
459 (org-time-stamp-format t t)
460 (date-to-time date)))))
461 (msg-id (plist-get gnorb-gnus-message-info :msg-id))
462 (sender (plist-get gnorb-gnus-message-info :from))
463 (subject (plist-get gnorb-gnus-message-info :subject))
464 ;; Convince Org we already have a link stored, even if we
466 (org-capture-link-is-already-stored t))
468 ;; Even if you make a link to not-yet-sent messages, even if
469 ;; you've saved the draft and it has a Date header, that
470 ;; header isn't saved into the link plist. So fake that, too.
473 :date-timestamp date-ts
474 :date-timestamp-inactive date-ts-ia
476 (org-store-link-props
477 :subject (plist-get gnorb-gnus-message-info :subject)
478 :to (plist-get gnorb-gnus-message-info :to)
480 :date-timestamp date-ts
481 :date-timestamp-inactive date-ts-ia
484 (org-capture nil gnorb-gnus-new-todo-capture-key)
486 (org-entry-put (point) gnorb-org-msg-id-key msg-id)
487 (gnorb-registry-make-entry msg-id sender subject (org-id-get-create) group))))
489 ;;; If an incoming message should trigger state-change for a Org todo,
490 ;;; call this function on it.
493 (defun gnorb-gnus-incoming-do-todo (arg &optional id)
494 "Call this function from a received gnus message to store a
495 link to the message, prompt for a related Org heading, visit the
496 heading, and trigger an action on it \(see
497 `gnorb-org-trigger-actions'\).
499 If you've set up message tracking \(with
500 `gnorb-tracking-initialize'\), Gnorb can guess which Org heading
501 you probably want to trigger, which can save some time. It does
502 this by looking in the References header, and seeing if any of
503 the messages referenced there are already being tracked by any
506 If you mark several messages before calling this function, or
507 call it with a numerical prefix arg, those messages will be
508 \"bulk associated\" with the chosen Org heading: associations
509 will be made, but you won't be prompted to trigger an action, and
510 you'll stay in the Gnus summary buffer."
512 (when (not (memq major-mode '(gnus-summary-mode gnus-article-mode)))
513 (user-error "Only works in gnus summary or article mode"))
514 ;; We should only store a link if it's not already at the head of
515 ;; `org-stored-links'. There's some duplicate storage, at
516 ;; present. Take a look at calling it non-interactively.
517 (setq gnorb-window-conf (current-window-configuration))
518 (move-marker gnorb-return-marker (point))
519 (setq gnorb-gnus-message-info nil)
520 (let* ((articles (gnus-summary-work-articles arg))
521 (art-no (gnus-summary-article-number))
522 (headers (gnus-data-header
523 (gnus-data-find art-no)))
524 (msg-id (mail-header-id headers))
525 (from (mail-header-from headers))
526 (subject (mail-header-subject headers))
527 (date (mail-header-date headers))
528 (to (cdr (assoc 'To (mail-header-extra headers))))
529 (group (gnorb-get-real-group-name
532 (link (call-interactively 'org-store-link))
533 (org-refile-targets gnorb-gnus-trigger-refile-targets)
534 (ref-msg-ids (concat (mail-header-references headers) " "
537 (when (and (null id) ref-msg-ids)
538 ;; Specifically ask for zombies, so the user has chance to
540 (gnorb-find-tracked-headings headers t)))
542 (setq gnorb-gnus-message-info
543 `(:subject ,subject :msg-id ,msg-id
545 :link ,link :date ,date :refs ,ref-msg-ids
547 (gnorb-gnus-collect-all-attachments nil t)
551 (delete-other-windows)
552 (gnorb-trigger-todo-action nil id))
553 ;; Flush out zombies (dead associations).
554 (setq related-headings
557 (when (null (org-id-find-id-file h))
560 "ID %s no longer exists, disassociate message?"
562 (gnorb-delete-association msg-id h))))
564 ;; See if one of the related headings is chosen.
565 (unless (catch 'target
566 (dolist (h related-headings nil)
568 (format "Trigger action on %s"
569 (gnorb-pretty-outline h)))
570 (throw 'target (setq targ h)))))
571 ;; If not, use the refile interface to choose one.
572 (setq targ (org-refile-get-location
573 "Trigger heading" nil t))
575 (save-window-excursion
576 (find-file (nth 1 targ))
577 (goto-char (nth 3 targ))
578 (org-id-get-create))))
579 ;; Either bulk associate multiple messages...
580 (if (> (length articles) 1)
583 (gnorb-registry-make-entry
587 from subject targ group)
588 (gnus-summary-remove-process-mark a))
589 (message "Associated %d messages with %s"
590 (length articles) (gnorb-pretty-outline targ)))
591 ;; ...or just trigger the one.
592 (delete-other-windows)
593 (gnorb-trigger-todo-action nil targ)))
595 ;; If these are left populated after an error, it plays hell
596 ;; with future trigger processes.
597 (setq gnorb-gnus-message-info nil)
598 (setq gnorb-gnus-capture-attachments nil)
599 (signal (car err) (cdr err))))))
602 (defun gnorb-gnus-quick-reply ()
603 "Compose a reply to the message under point, and associate both
604 the original message and the reply with the selected heading.
605 Take no other action.
607 Use this when you want to compose a reply to a message on the
608 spot, and track both messages, without having to go through the
609 hassle of triggering an action on a heading, and then starting a
612 (when (not (memq major-mode '(gnus-summary-mode gnus-article-mode)))
613 (user-error "Only works in gnus summary or article mode"))
614 (let* ((art-no (gnus-summary-article-number))
615 (headers (gnus-data-header
616 (gnus-data-find art-no)))
617 (msg-id (mail-header-id headers))
618 (from (mail-header-from headers))
619 (subject (mail-header-subject headers))
620 (group (gnorb-get-real-group-name
623 (ref-msg-ids (concat (mail-header-references headers) " "
627 (gnorb-find-tracked-headings headers t)))
628 (targ (car-safe related-headings)))
630 (let ((ret (make-marker)))
631 ;; Assume the first heading is the one we want.
632 (gnorb-registry-make-entry
633 msg-id from subject targ group)
634 (gnus-summary-wide-reply-with-original 1)
635 (move-marker ret (point))
638 (message-narrow-to-headers-or-head)
639 (goto-char (point-min))
641 (message-insert-header
642 (intern gnorb-mail-header) targ))
645 (format "Original message and reply will be associated with %s"
646 (gnorb-pretty-outline targ))))
647 (message "No associated headings found"))))
650 (defun gnorb-gnus-search-messages (str persist &optional head-text ret)
651 "Initiate a search for gnus message links in an org subtree.
652 The arg STR can be one of two things: an Org heading id value
653 \(IDs should be prefixed with \"id+\"\), in which case links will
654 be collected from that heading, or a string corresponding to an
655 Org tags search, in which case links will be collected from all
658 In either case, once a collection of links have been made, they
659 will all be displayed in an ephemeral group on the \"nngnorb\"
660 server. There must be an active \"nngnorb\" server for this to
665 (or (gnus-method-to-server '(nngnorb))
667 "Please add a \"nngnorb\" backend to your gnus installation.")))
669 (when (version= "5.13" gnus-version-number)
670 (with-no-warnings ; All these variables are available.
671 (setq nnir-current-query nil
672 nnir-current-server nil
673 nnir-current-group-marked nil
675 ;; In 24.4, the group name is mostly decorative, but in 24.3, the
676 ;; actual query is held there.
677 (setq name (if (version= "5.13" gnus-version-number)
678 (concat "nnir:" (prin1-to-string `((query ,str))))
681 (format "Name for group (default %s): " head-text)
683 (concat "gnorb-" str))))
684 (setq method (if (version= "5.13" gnus-version-number)
685 (list 'nnir nnir-address)
686 (list 'nnir "Gnorb")))
689 (cons 'nnir-specs (list (cons 'nnir-query-spec `((query . ,str)))
690 (cons 'nnir-group-spec `((,nnir-address nil)))))
691 (cons 'nnir-artlist nil)))
694 (switch-to-buffer gnus-group-buffer)
695 (gnus-group-make-group name method nil spec)
696 (gnus-group-select-group))
697 (gnus-group-read-ephemeral-group name method nil ret nil nil spec))))
699 (defun gnorb-gnus-summary-mode-hook ()
700 "Check if we've entered a Gnorb-generated group, and activate
701 `gnorb-summary-minor-mode', if so."
702 (let ((method (gnus-find-method-for-group gnus-newsgroup-name)))
703 (when (string-match-p "Gnorb" (cadr method))
704 (gnorb-summary-minor-mode))))
706 (add-hook 'gnus-summary-mode-hook #'gnorb-gnus-summary-mode-hook)
708 ;;; Automatic noticing of relevant messages
710 ;; likely hooks for the summary buffer include:
711 ;; `gnus-parse-headers-hook'
713 ;; BBDB puts its notice stuff in the `gnus-article-prepare-hook',
714 ;; which seems as good a spot as any.
716 (defun gnorb-gnus-hint-relevant-message ()
717 "When opening an article buffer, check the message to see if it
718 is relevant to any existing TODO headings. If so, flash a message
719 to that effect. This function is added to the
720 `gnus-article-prepare-hook'. It will only do anything if the
721 option `gnorb-gnus-hint-relevant-article' is non-nil."
722 (when (and gnorb-gnus-hint-relevant-article
723 (not (memq (car (gnus-find-method-for-group
724 gnus-newsgroup-name))
729 (gnus-summary-article-number))))
731 (gnus-registry-get-id-key
732 (gnus-fetch-original-field "message-id") 'gnorb-ids))
733 (tracked-headings (gnorb-find-tracked-headings headers))
735 (where-is-internal 'gnorb-gnus-incoming-do-todo
738 (message "Message is associated with %s"
739 (gnorb-pretty-outline (car assoc-heading) t)))
741 (message "Possible relevant todo %s, trigger with %s"
742 (gnorb-pretty-outline (car tracked-headings) t)
744 (key-description key)
745 "M-x gnorb-gnus-incoming-do-todo")))
748 (add-hook 'gnus-article-prepare-hook 'gnorb-gnus-hint-relevant-message)
750 (defun gnorb-gnus-insert-format-letter-maybe (header)
751 (if (not (memq (car (gnus-find-method-for-group
752 gnus-newsgroup-name))
754 (cond ((gnus-registry-get-id-key
755 (mail-header-message-id header) 'gnorb-ids)
756 gnorb-gnus-summary-tracked-mark)
757 ((gnorb-find-tracked-headings header)
758 gnorb-gnus-summary-mark)
762 (fset (intern (concat "gnus-user-format-function-"
763 gnorb-gnus-summary-mark-format-letter))
765 (gnorb-gnus-insert-format-letter-maybe header)))
768 (defun gnorb-gnus-view ()
769 "Display the first relevant TODO heading for the message under point"
771 (let* ((headers (gnus-data-header
773 (gnus-summary-article-number))))
775 (gnorb-find-tracked-headings headers)))
776 (when tracked-headings
777 (setq gnorb-window-conf (current-window-configuration))
778 (move-marker gnorb-return-marker (point))
779 (delete-other-windows)
780 (org-id-goto (car tracked-headings)))))
782 (provide 'gnorb-gnus)
783 ;;; gnorb-gnus.el ends here