511012df577410c7b8fb7732fd29453c6a6f0d32
[gnus] / lisp / gnus-registry.el
1 ;;; gnus-registry.el --- article registry for Gnus
2
3 ;; Copyright (C) 2002-2011  Free Software Foundation, Inc.
4
5 ;; Author: Ted Zlatanov <tzz@lifelogs.com>
6 ;; Keywords: news registry
7
8 ;; This file is part of GNU Emacs.
9
10 ;; GNU Emacs is free software: you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
14
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
22
23 ;;; Commentary:
24
25 ;; This is the gnus-registry.el package, which works with all
26 ;; Gnus backends, not just nnmail.  The major issue is that it
27 ;; doesn't go across backends, so for instance if an article is in
28 ;; nnml:sys and you see a reference to it in nnimap splitting, the
29 ;; article will end up in nnimap:sys
30
31 ;; gnus-registry.el intercepts article respooling, moving, deleting,
32 ;; and copying for all backends.  If it doesn't work correctly for
33 ;; you, submit a bug report and I'll be glad to fix it.  It needs
34 ;; documentation in the manual (also on my to-do list).
35
36 ;; Put this in your startup file (~/.gnus.el for instance)
37
38 ;; (setq gnus-registry-max-entries 2500)
39
40 ;; (gnus-registry-initialize)
41
42 ;; Then use this in your fancy-split:
43
44 ;; (: gnus-registry-split-fancy-with-parent)
45
46 ;; You should also consider using the nnregistry backend to look up
47 ;; articles.  See the Gnus manual for more information.
48
49 ;; TODO:
50
51 ;; - get the correct group on spool actions
52
53 ;; - articles that are spooled to a different backend should be moved
54 ;;   after splitting
55
56 ;;; Code:
57
58 (eval-when-compile (require 'cl))
59
60 (eval-when-compile
61   (when (null (require 'ert nil t))
62     (defmacro* ert-deftest (name () &body docstring-keys-and-body))))
63
64 (require 'gnus)
65 (require 'gnus-int)
66 (require 'gnus-sum)
67 (require 'gnus-art)
68 (require 'gnus-util)
69 (require 'nnmail)
70 (require 'easymenu)
71 (require 'registry)
72
73 (defvar gnus-adaptive-word-syntax-table)
74
75 (defvar gnus-registry-dirty t
76  "Boolean set to t when the registry is modified")
77
78 (defgroup gnus-registry nil
79   "The Gnus registry."
80   :version "22.1"
81   :group 'gnus)
82
83 (defvar gnus-registry-marks
84   '((Important
85      :char ?i
86      :image "summary_important")
87     (Work
88      :char ?w
89      :image "summary_work")
90     (Personal
91      :char ?p
92      :image "summary_personal")
93     (To-Do
94      :char ?t
95      :image "summary_todo")
96     (Later
97      :char ?l
98      :image "summary_later"))
99
100   "List of registry marks and their options.
101
102 `gnus-registry-mark-article' will offer symbols from this list
103 for completion.
104
105 Each entry must have a character to be useful for summary mode
106 line display and for keyboard shortcuts.
107
108 Each entry must have an image string to be useful for visual
109 display.")
110
111 (defcustom gnus-registry-default-mark 'To-Do
112   "The default mark.  Should be a valid key for `gnus-registry-marks'."
113   :group 'gnus-registry
114   :type 'symbol)
115
116 (defcustom gnus-registry-unfollowed-addresses
117   (list (regexp-quote user-mail-address))
118   "List of addresses that gnus-registry-split-fancy-with-parent won't trace.
119 The addresses are matched, they don't have to be fully qualified."
120   :group 'gnus-registry
121   :type '(repeat regexp))
122
123 (defcustom gnus-registry-unfollowed-groups
124   '("delayed$" "drafts$" "queue$" "INBOX$" "^nnmairix:" "archive")
125   "List of groups that gnus-registry-split-fancy-with-parent won't return.
126 The group names are matched, they don't have to be fully
127 qualified.  This parameter tells the Gnus registry 'never split a
128 message into a group that matches one of these, regardless of
129 references.'
130
131 nnmairix groups are specifically excluded because they are ephemeral."
132   :group 'gnus-registry
133   :type '(repeat regexp))
134
135 (defcustom gnus-registry-install 'ask
136   "Whether the registry should be installed."
137   :group 'gnus-registry
138   :type '(choice (const :tag "Never Install" nil)
139                  (const :tag "Always Install" t)
140                  (const :tag "Ask Me" ask)))
141
142 (defvar gnus-summary-misc-menu) ;; Avoid byte compiler warning.
143
144 (defvar gnus-registry-misc-menus nil)   ; ugly way to keep the menus
145
146 (make-obsolete-variable 'gnus-registry-clean-empty nil "23.4")
147 (make-obsolete-variable 'gnus-registry-use-long-group-names nil "23.4")
148 (make-obsolete-variable 'gnus-registry-max-track-groups nil "23.4")
149 (make-obsolete-variable 'gnus-registry-entry-caching nil "23.4")
150 (make-obsolete-variable 'gnus-registry-trim-articles-without-groups nil "23.4")
151
152 (defcustom gnus-registry-track-extra '(subject sender)
153   "Whether the registry should track extra data about a message.
154 The Subject and Sender (From:) headers are tracked this way by
155 default."
156   :group 'gnus-registry
157   :type
158   '(set :tag "Tracking choices"
159     (const :tag "Track by subject (Subject: header)" subject)
160     (const :tag "Track by sender (From: header)"  sender)))
161
162 (defcustom gnus-registry-split-strategy nil
163   "The splitting strategy applied to the keys in `gnus-registry-track-extra'.
164
165 Given a set of unique found groups G and counts for each element
166 of G, and a key K (typically 'sender or 'subject):
167
168 When nil, if G has only one element, use it.  Otherwise give up.
169 This is the fastest but also least useful strategy.
170
171 When 'majority, use the majority by count.  So if there is a
172 group with the most articles counted by K, use that.  Ties are
173 resolved in no particular order, simply the first one found wins.
174 This is the slowest strategy but also the most accurate one.
175
176 When 'first, the first element of G wins.  This is fast and
177 should be OK if your senders and subjects don't \"bleed\" across
178 groups."
179   :group 'gnus-registry
180   :type
181   '(choice :tag "Splitting strategy"
182            (const :tag "Only use single choices, discard multiple matches" nil)
183            (const :tag "Majority of matches wins" majority)
184            (const :tag "First found wins"  first)))
185
186 (defcustom gnus-registry-minimum-subject-length 5
187   "The minimum length of a subject before it's considered trackable."
188   :group 'gnus-registry
189   :type 'integer)
190
191 (defcustom gnus-registry-extra-entries-precious '(mark)
192   "What extra keys are precious, meaning entries with them won't get pruned.
193 By default, 'mark is included, so articles with marks are
194 considered precious.
195
196 Before you save the Gnus registry, it's pruned.  Any entries with
197 keys in this list will not be pruned.  All other entries go to
198 the Bit Bucket."
199   :group 'gnus-registry
200   :type '(repeat symbol))
201
202 (defcustom gnus-registry-cache-file
203   (nnheader-concat
204    (or gnus-dribble-directory gnus-home-directory "~/")
205    ".gnus.registry.eioio")
206   "File where the Gnus registry will be stored."
207   :group 'gnus-registry
208   :type 'file)
209
210 (defcustom gnus-registry-max-entries nil
211   "Maximum number of entries in the registry, nil for unlimited."
212   :group 'gnus-registry
213   :type '(radio (const :format "Unlimited " nil)
214                 (integer :format "Maximum number: %v")))
215
216 (defcustom gnus-registry-max-pruned-entries nil
217   "Maximum number of pruned entries in the registry, nil for unlimited."
218   :group 'gnus-registry
219   :type '(radio (const :format "Unlimited " nil)
220                 (integer :format "Maximum number: %v")))
221
222 (defun gnus-registry-fixup-registry (db)
223   (when db
224     (oset db :precious
225           (append gnus-registry-extra-entries-precious
226                   '()))
227     (oset db :max-hard
228           (or gnus-registry-max-entries
229               most-positive-fixnum))
230     (oset db :max-soft
231           (or gnus-registry-max-pruned-entries
232               most-positive-fixnum))
233     (oset db :tracked
234           (append gnus-registry-track-extra
235                   '(mark group keyword))))
236   db)
237
238 (defun gnus-registry-make-db (&optional file)
239   (interactive "fGnus registry persistence file: \n")
240   (gnus-registry-fixup-registry
241    (registry-db
242     "Gnus Registry"
243     :file (or file gnus-registry-cache-file)
244     ;; these parameters are set in `gnus-registry-fixup-registry'
245     :max-hard most-positive-fixnum
246     :max-soft most-positive-fixnum
247     :precious nil
248     :tracked nil)))
249
250 (defvar gnus-registry-db (gnus-registry-make-db)
251   "*The article registry by Message ID.  See `registry-db'")
252
253 ;; top-level registry data management
254 (defun gnus-registry-remake-db (&optional forsure)
255   "Remake the registry database after customization.
256 This is not required after changing `gnus-registry-cache-file'."
257   (interactive (list (y-or-n-p "Remake and CLEAR the Gnus registry? ")))
258   (when forsure
259     (gnus-message 1 "Remaking the Gnus registry")
260     (setq gnus-registry-db (gnus-registry-make-db))))
261
262 (defun gnus-registry-read ()
263   "Read the registry cache file."
264   (interactive)
265   (let ((file gnus-registry-cache-file))
266     (condition-case nil
267         (progn
268           (gnus-message 5 "Reading Gnus registry from %s..." file)
269           (setq gnus-registry-db (gnus-registry-fixup-registry
270                                   (eieio-persistent-read file)))
271           (gnus-message 5 "Reading Gnus registry from %s...done" file))
272       (error
273        (gnus-message
274         1
275         "The Gnus registry could not be loaded from %s, creating a new one"
276         file)
277        (gnus-registry-remake-db t)))))
278
279 (defun gnus-registry-save (&optional file db)
280   "Save the registry cache file."
281   (interactive)
282   (let ((file (or file gnus-registry-cache-file))
283         (db (or db gnus-registry-db)))
284     (gnus-message 5 "Saving Gnus registry (%d entries) to %s..."
285                   (registry-size db) file)
286     (registry-prune db)
287     ;; TODO: call (gnus-string-remove-all-properties v) on all elements?
288     (eieio-persistent-save db file)
289     (gnus-message 5 "Saving Gnus registry (size %d) to %s...done"
290                   (registry-size db) file)))
291
292 ;; article move/copy/spool/delete actions
293 (defun gnus-registry-action (action data-header from &optional to method)
294   (let* ((id (mail-header-id data-header))
295          (subject (gnus-string-remove-all-properties
296                    (gnus-registry-simplify-subject
297                     (mail-header-subject data-header))))
298          (sender (gnus-string-remove-all-properties
299                   (mail-header-from data-header)))
300          (from (gnus-group-guess-full-name-from-command-method from))
301          (to (if to (gnus-group-guess-full-name-from-command-method to) nil))
302          (to-name (if to to "the Bit Bucket")))
303     (gnus-message 7 "Gnus registry: article %s %s from %s to %s"
304                   id (if method "respooling" "going") from to)
305
306     (gnus-registry-handle-action
307      id
308      ;; unless copying, remove the old "from" group
309      (if (not (equal 'copy action)) from nil)
310      to subject sender)))
311
312 (defun gnus-registry-spool-action (id group &optional subject sender)
313   (let ((to (gnus-group-guess-full-name-from-command-method group)))
314     (when (and (stringp id) (string-match "\r$" id))
315       (setq id (substring id 0 -1)))
316     (gnus-message 7 "Gnus registry: article %s spooled to %s"
317                   id
318                   to)
319     (gnus-registry-handle-action id nil to subject sender)))
320
321 (defun gnus-registry-handle-action (id from to subject sender)
322   (let ((db gnus-registry-db)
323         ;; safe if not found
324         (entry (gnus-registry-get-or-make-entry id)))
325
326     ;; this could be done by calling `gnus-registry-set-id-key'
327     ;; several times but it's better to bunch the transactions
328     ;; together
329
330     (registry-delete db (list id) nil)
331     (when from
332       (setq entry (cons (delete from (assoc 'group entry))
333                         (assq-delete-all 'group entry))))
334
335     (dolist (kv `((group ,to) (sender ,sender) (subject ,subject)))
336       (when (second kv)
337         (let ((new (or (assq (first kv) entry)
338                        (list (first kv)))))
339           (add-to-list 'new (second kv) t)
340           (setq entry (cons new
341                             (assq-delete-all (first kv) entry))))))
342     (gnus-message 10 "Gnus registry: new entry for %s is %S"
343                   id
344                   entry)
345     (registry-insert db id entry)))
346
347 ;; Function for nn{mail|imap}-split-fancy: look up all references in
348 ;; the cache and if a match is found, return that group.
349 (defun gnus-registry-split-fancy-with-parent ()
350   "Split this message into the same group as its parent.  The parent
351 is obtained from the registry.  This function can be used as an entry
352 in `nnmail-split-fancy' or `nnimap-split-fancy', for example like
353 this: (: gnus-registry-split-fancy-with-parent)
354
355 This function tracks ALL backends, unlike
356 `nnmail-split-fancy-with-parent' which tracks only nnmail
357 messages.
358
359 For a message to be split, it looks for the parent message in the
360 References or In-Reply-To header and then looks in the registry
361 to see which group that message was put in.  This group is
362 returned, unless `gnus-registry-follow-group-p' return nil for
363 that group.
364
365 See the Info node `(gnus)Fancy Mail Splitting' for more details."
366   (let* ((refstr (or (message-fetch-field "references") "")) ; guaranteed
367          (reply-to (message-fetch-field "in-reply-to"))      ; may be nil
368          ;; now, if reply-to is valid, append it to the References
369          (refstr (if reply-to
370                      (concat refstr " " reply-to)
371                    refstr))
372          (references (and refstr (gnus-extract-references refstr)))
373          ;; these may not be used, but the code is cleaner having them up here
374          (sender (gnus-string-remove-all-properties
375                   (message-fetch-field "from")))
376          (subject (gnus-string-remove-all-properties
377                    (gnus-registry-simplify-subject
378                     (message-fetch-field "subject"))))
379
380          (nnmail-split-fancy-with-parent-ignore-groups
381           (if (listp nnmail-split-fancy-with-parent-ignore-groups)
382               nnmail-split-fancy-with-parent-ignore-groups
383             (list nnmail-split-fancy-with-parent-ignore-groups))))
384     (gnus-registry--split-fancy-with-parent-internal
385      :references references
386      :refstr refstr
387      :sender sender
388      :subject subject
389      :log-agent "Gnus registry fancy splitting with parent")))
390
391 (defun* gnus-registry--split-fancy-with-parent-internal
392     (&rest spec
393            &key references refstr sender subject log-agent
394            &allow-other-keys)
395   (gnus-message
396    10
397    "gnus-registry--split-fancy-with-parent-internal: %S" spec)
398   (let ((db gnus-registry-db)
399         found)
400     ;; this is a big if-else statement.  it uses
401     ;; gnus-registry-post-process-groups to filter the results after
402     ;; every step.
403     (cond
404      ;; the references string must be valid and parse to valid references
405      (references
406       (dolist (reference (nreverse references))
407         (gnus-message
408          9
409          "%s is looking for matches for reference %s from [%s]"
410          log-agent reference refstr)
411         (setq found
412               (loop for group in (gnus-registry-get-id-key reference 'group)
413                     when (gnus-registry-follow-group-p group)
414                     do (gnus-message
415                         7
416                         "%s traced the reference %s from [%s] to group %s"
417                         log-agent reference refstr group)
418                     collect group)))
419       ;; filter the found groups and return them
420       ;; the found groups are the full groups
421       (setq found (gnus-registry-post-process-groups
422                    "references" refstr found)))
423
424      ;; else: there were no matches, try the extra tracking by sender
425      ((and (memq 'sender gnus-registry-track-extra)
426            sender
427            (gnus-grep-in-list
428             sender
429             gnus-registry-unfollowed-addresses))
430       (let ((groups (apply
431                      'append
432                      (mapcar
433                       (lambda (reference)
434                         (gnus-registry-get-id-key reference 'group))
435                       (registry-lookup-secondary-value db 'sender sender)))))
436         (setq found
437               (loop for group in groups
438                     when (gnus-registry-follow-group-p group)
439                   do (gnus-message
440                       ;; raise level of messaging if gnus-registry-track-extra
441                       (if gnus-registry-track-extra 7 9)
442                       "%s (extra tracking) traced sender '%s' to groups %s"
443                       log-agent sender found)
444                   collect group)))
445
446       ;; filter the found groups and return them
447       ;; the found groups are NOT the full groups
448       (setq found (gnus-registry-post-process-groups
449                    "sender" sender found)))
450
451      ;; else: there were no matches, now try the extra tracking by subject
452      ((and (memq 'subject gnus-registry-track-extra)
453            subject
454            (< gnus-registry-minimum-subject-length (length subject)))
455       (let ((groups (apply
456                      'append
457                      (mapcar
458                       (lambda (reference)
459                         (gnus-registry-get-id-key reference 'group))
460                       (registry-lookup-secondary-value db 'subject subject)))))
461         (setq found
462               (loop for group in groups
463                     when (gnus-registry-follow-group-p group)
464                     do (gnus-message
465                         ;; raise level of messaging if gnus-registry-track-extra
466                         (if gnus-registry-track-extra 7 9)
467                         "%s (extra tracking) traced subject '%s' to groups %s"
468                         log-agent subject found)
469                     collect group))
470       ;; filter the found groups and return them
471       ;; the found groups are NOT the full groups
472       (setq found (gnus-registry-post-process-groups
473                    "subject" subject found)))))
474     ;; after the (cond) we extract the actual value safely
475     (car-safe found)))
476
477 (defun gnus-registry-post-process-groups (mode key groups)
478   "Inspects GROUPS found by MODE for KEY to determine which ones to follow.
479
480 MODE can be 'subject' or 'sender' for example.  The KEY is the
481 value by which MODE was searched.
482
483 Transforms each group name to the equivalent short name.
484
485 Checks if the current Gnus method (from `gnus-command-method' or
486 from `gnus-newsgroup-name') is the same as the group's method.
487 Foreign methods are not supported so they are rejected.
488
489 Reduces the list to a single group, or complains if that's not
490 possible.  Uses `gnus-registry-split-strategy'."
491   (let ((log-agent "gnus-registry-post-process-group")
492         out)
493
494     ;; the strategy can be nil, in which case groups is nil
495     (setq groups
496           (case gnus-registry-split-strategy
497             ;; first strategy
498             ((first)
499              (and groups (list (car-safe groups))))
500
501             ((majority)
502              (let ((freq (make-hash-table
503                           :size 256
504                           :test 'equal)))
505                (mapc (lambda (x) (puthash x (1+ (gethash x freq 0)) freq))
506                      groups)
507                (list (car-safe
508                       (sort groups (lambda (a b)
509                                      (> (gethash a freq 0)
510                                         (gethash b freq 0))))))))))
511
512     (dolist (group groups)
513       (let ((m1 (gnus-find-method-for-group group))
514             (m2 (or gnus-command-method
515                     (gnus-find-method-for-group gnus-newsgroup-name)))
516             (short-name (gnus-group-short-name group)))
517         (if (gnus-methods-equal-p m1 m2)
518             (progn
519               ;; this is REALLY just for debugging
520               (gnus-message
521                10
522                "%s stripped group %s to %s"
523                log-agent group short-name)
524               (add-to-list 'out short-name))
525           ;; else...
526           (gnus-message
527            7
528            "%s ignored foreign group %s"
529            log-agent group))))
530
531     ;; is there just one group?
532     (cond
533      ((= (length out) 1) out)
534      ((null out)
535       (gnus-message
536        5
537        "%s: no matches for %s %s."
538        log-agent out mode key)
539       nil)
540      (t (gnus-message
541          5
542          "%s: too many extra matches (%s) for %s %s.  Returning none."
543          log-agent out mode key)
544         nil))))
545
546 (defun gnus-registry-follow-group-p (group)
547   "Determines if a group name should be followed.
548 Consults `gnus-registry-unfollowed-groups' and
549 `nnmail-split-fancy-with-parent-ignore-groups'."
550   (and group
551        (not (or (gnus-grep-in-list
552                  group
553                  gnus-registry-unfollowed-groups)
554                 (gnus-grep-in-list
555                  group
556                  nnmail-split-fancy-with-parent-ignore-groups)))))
557
558 (defun gnus-registry-wash-for-keywords (&optional force)
559   "Get the keywords of the current article.
560 Overrides existing keywords with FORCE set non-nil."
561   (interactive)
562   (let ((id (gnus-registry-fetch-message-id-fast gnus-current-article))
563         word words)
564     (if (or (not (gnus-registry-get-id-key id 'keyword))
565             force)
566         (with-current-buffer gnus-article-buffer
567           (article-goto-body)
568           (save-window-excursion
569             (save-restriction
570               (narrow-to-region (point) (point-max))
571               (with-syntax-table gnus-adaptive-word-syntax-table
572                 (while (re-search-forward "\\b\\w+\\b" nil t)
573                   (setq word (gnus-string-remove-all-properties
574                               (downcase (buffer-substring
575                                          (match-beginning 0) (match-end 0)))))
576                   (if (> (length word) 2)
577                       (push word words))))))
578           (gnus-registry-set-id-key id 'keyword words)))))
579
580 (defun gnus-registry-keywords ()
581   (let ((table (registry-lookup-secondary gnus-registry-db 'keyword)))
582     (when table (maphash (lambda (k v) k) table))))
583
584 (defun gnus-registry-find-keywords (keyword)
585   (interactive (list
586                 (completing-read "Keyword: " (gnus-registry-keywords) nil t)))
587   (registry-lookup-secondary-value gnus-registry-db 'keyword keyword))
588
589 (defun gnus-registry-register-message-ids ()
590   "Register the Message-ID of every article in the group"
591   (unless (gnus-parameter-registry-ignore gnus-newsgroup-name)
592     (dolist (article gnus-newsgroup-articles)
593       (let* ((id (gnus-registry-fetch-message-id-fast article))
594              (groups (gnus-registry-get-id-key id 'group)))
595         (unless (member gnus-newsgroup-name groups)
596           (gnus-message 9 "Registry: Registering article %d with group %s"
597                         article gnus-newsgroup-name)
598           (gnus-registry-handle-action id nil gnus-newsgroup-name
599            (gnus-registry-fetch-simplified-message-subject-fast article)
600            (gnus-registry-fetch-sender-fast article)))))))
601
602 ;; message field fetchers
603 (defun gnus-registry-fetch-message-id-fast (article)
604   "Fetch the Message-ID quickly, using the internal gnus-data-list function"
605   (if (and (numberp article)
606            (assoc article (gnus-data-list nil)))
607       (mail-header-id (gnus-data-header (assoc article (gnus-data-list nil))))
608     nil))
609
610 (defun gnus-registry-simplify-subject (subject)
611   (if (stringp subject)
612       (gnus-simplify-subject subject)
613     nil))
614
615 (defun gnus-registry-fetch-simplified-message-subject-fast (article)
616   "Fetch the Subject quickly, using the internal gnus-data-list function"
617   (if (and (numberp article)
618            (assoc article (gnus-data-list nil)))
619       (gnus-string-remove-all-properties
620        (gnus-registry-simplify-subject
621         (mail-header-subject (gnus-data-header
622                               (assoc article (gnus-data-list nil))))))
623     nil))
624
625 (defun gnus-registry-fetch-sender-fast (article)
626   "Fetch the Sender quickly, using the internal gnus-data-list function"
627   (if (and (numberp article)
628            (assoc article (gnus-data-list nil)))
629       (gnus-string-remove-all-properties
630        (mail-header-from (gnus-data-header
631                           (assoc article (gnus-data-list nil)))))
632     nil))
633
634 ;; registry marks glue
635 (defun gnus-registry-do-marks (type function)
636   "For each known mark, call FUNCTION for each cell of type TYPE.
637
638 FUNCTION should take two parameters, a mark symbol and the cell value."
639   (dolist (mark-info gnus-registry-marks)
640     (let* ((mark (car-safe mark-info))
641            (data (cdr-safe mark-info))
642            (cell-data (plist-get data type)))
643       (when cell-data
644         (funcall function mark cell-data)))))
645
646 ;;; this is ugly code, but I don't know how to do it better
647 (defun gnus-registry-install-shortcuts ()
648   "Install the keyboard shortcuts and menus for the registry.
649 Uses `gnus-registry-marks' to find what shortcuts to install."
650   (let (keys-plist)
651     (setq gnus-registry-misc-menus nil)
652     (gnus-registry-do-marks
653      :char
654      (lambda (mark data)
655        (let ((function-format
656               (format "gnus-registry-%%s-article-%s-mark" mark)))
657
658 ;;; The following generates these functions:
659 ;;; (defun gnus-registry-set-article-Important-mark (&rest articles)
660 ;;;   "Apply the Important mark to process-marked ARTICLES."
661 ;;;   (interactive (gnus-summary-work-articles current-prefix-arg))
662 ;;;   (gnus-registry-set-article-mark-internal 'Important articles nil t))
663 ;;; (defun gnus-registry-remove-article-Important-mark (&rest articles)
664 ;;;   "Apply the Important mark to process-marked ARTICLES."
665 ;;;   (interactive (gnus-summary-work-articles current-prefix-arg))
666 ;;;   (gnus-registry-set-article-mark-internal 'Important articles t t))
667
668          (dolist (remove '(t nil))
669            (let* ((variant-name (if remove "remove" "set"))
670                   (function-name (format function-format variant-name))
671                   (shortcut (format "%c" data))
672                   (shortcut (if remove (upcase shortcut) shortcut)))
673              (unintern function-name obarray)
674              (eval
675               `(defun
676                  ;; function name
677                  ,(intern function-name)
678                  ;; parameter definition
679                  (&rest articles)
680                  ;; documentation
681                  ,(format
682                    "%s the %s mark over process-marked ARTICLES."
683                    (upcase-initials variant-name)
684                    mark)
685                  ;; interactive definition
686                  (interactive
687                   (gnus-summary-work-articles current-prefix-arg))
688                  ;; actual code
689
690                  ;; if this is called and the user doesn't want the
691                  ;; registry enabled, we'll ask anyhow
692                  (when (eq gnus-registry-install nil)
693                    (setq gnus-registry-install 'ask))
694
695                  ;; now the user is asked if gnus-registry-install is 'ask
696                  (when (gnus-registry-install-p)
697                    (gnus-registry-set-article-mark-internal
698                     ;; all this just to get the mark, I must be doing it wrong
699                     (intern ,(symbol-name mark))
700                     articles ,remove t)
701                    (gnus-message
702                     9
703                     "Applying mark %s to %d articles"
704                     ,(symbol-name mark) (length articles))
705                    (dolist (article articles)
706                      (gnus-summary-update-article
707                       article
708                       (assoc article (gnus-data-list nil)))))))
709              (push (intern function-name) keys-plist)
710              (push shortcut keys-plist)
711              (push (vector (format "%s %s"
712                                    (upcase-initials variant-name)
713                                    (symbol-name mark))
714                            (intern function-name) t)
715                    gnus-registry-misc-menus)
716              (gnus-message
717               9
718               "Defined mark handling function %s"
719               function-name))))))
720     (gnus-define-keys-1
721      '(gnus-registry-mark-map "M" gnus-summary-mark-map)
722      keys-plist)
723     (add-hook 'gnus-summary-menu-hook
724               (lambda ()
725                 (easy-menu-add-item
726                  gnus-summary-misc-menu
727                  nil
728                  (cons "Registry Marks" gnus-registry-misc-menus))))))
729
730 ;;; use like this:
731 ;;; (defalias 'gnus-user-format-function-M
732 ;;;           'gnus-registry-user-format-function-M)
733 (defun gnus-registry-user-format-function-M (headers)
734   (let* ((id (mail-header-message-id headers))
735          (marks (when id (gnus-registry-get-id-key id 'mark))))
736     (apply 'concat (mapcar (lambda (mark)
737                              (let ((c
738                                     (plist-get
739                                      (cdr-safe
740                                       (assoc mark gnus-registry-marks))
741                                      :char)))
742                                (if c
743                                    (list c)
744                                  nil)))
745                            marks))))
746
747 (defun gnus-registry-read-mark ()
748   "Read a mark name from the user with completion."
749   (let ((mark (gnus-completing-read
750                "Label"
751                (mapcar 'symbol-name (mapcar 'car gnus-registry-marks))
752                nil nil nil
753                (symbol-name gnus-registry-default-mark))))
754     (when (stringp mark)
755       (intern mark))))
756
757 (defun gnus-registry-set-article-mark (&rest articles)
758   "Apply a mark to process-marked ARTICLES."
759   (interactive (gnus-summary-work-articles current-prefix-arg))
760   (gnus-registry-set-article-mark-internal (gnus-registry-read-mark)
761                                            articles nil t))
762
763 (defun gnus-registry-remove-article-mark (&rest articles)
764   "Remove a mark from process-marked ARTICLES."
765   (interactive (gnus-summary-work-articles current-prefix-arg))
766   (gnus-registry-set-article-mark-internal (gnus-registry-read-mark)
767                                            articles t t))
768
769 (defun gnus-registry-set-article-mark-internal (mark
770                                                 articles
771                                                 &optional remove
772                                                 show-message)
773   "Apply or remove MARK across a list of ARTICLES."
774   (let ((article-id-list
775          (mapcar 'gnus-registry-fetch-message-id-fast articles)))
776     (dolist (id article-id-list)
777       (let* ((marks (delq mark (gnus-registry-get-id-key id 'mark)))
778              (marks (if remove marks (cons mark marks))))
779         (when show-message
780           (gnus-message 1 "%s mark %s with message ID %s, resulting in %S"
781                         (if remove "Removing" "Adding")
782                         mark id marks))
783         (gnus-registry-set-id-key id 'mark marks)))))
784
785 (defun gnus-registry-get-article-marks (&rest articles)
786   "Get the Gnus registry marks for ARTICLES and show them if interactive.
787 Uses process/prefix conventions.  For multiple articles,
788 only the last one's marks are returned."
789   (interactive (gnus-summary-work-articles 1))
790   (let* ((article (last articles))
791          (id (gnus-registry-fetch-message-id-fast article))
792          (marks (when id (gnus-registry-get-id-key id 'mark))))
793     (when (interactive-p)
794       (gnus-message 1 "Marks are %S" marks))
795     marks))
796
797 (defun gnus-registry-group-count (id)
798   "Get the number of groups of a message, based on the message ID."
799   (length (gnus-registry-get-id-key id 'group)))
800
801 (defun gnus-registry-get-or-make-entry (id)
802   (let* ((db gnus-registry-db)
803          ;; safe if not found
804          (entries (registry-lookup db (list id))))
805
806     (when (null entries)
807       (registry-insert db id (list (list 'creation-time (current-time))
808                                    '(group) '(sender) '(subject)))
809       (setq entries (registry-lookup db (list id))))
810
811     (nth 1 (assoc id entries))))
812
813 (defun gnus-registry-delete-entries (idlist)
814   (registry-delete gnus-registry-db idlist nil))
815
816 (defun gnus-registry-get-id-key (id key)
817   (cdr-safe (assq key (gnus-registry-get-or-make-entry id))))
818
819 (defun gnus-registry-set-id-key (id key vals)
820   (let* ((db gnus-registry-db)
821          (entry (gnus-registry-get-or-make-entry id)))
822     (registry-delete db (list id) nil)
823     (setq entry (cons (cons key vals) (assq-delete-all key entry)))
824     (registry-insert db id entry)
825     entry))
826
827 (defun gnus-registry-import-eld (file)
828   (interactive "fOld registry file to import? ")
829   ;; example content:
830   ;;   (setq gnus-registry-alist '(
831   ;; ("<messageID>" ((marks nil)
832   ;;                 (mtime 19365 1776 440496)
833   ;;                 (sender . "root (Cron Daemon)")
834   ;;                 (subject . "Cron"))
835   ;;  "cron" "nnml+private:cron")
836   (load file t)
837   (when (boundp 'gnus-registry-alist)
838     (let* ((old (symbol-value 'gnus-registry-alist))
839            (count 0)
840            (expected (length old))
841            entry)
842       (while (car-safe old)
843         (incf count)
844         ;; don't use progress reporters for backwards compatibility
845         (when (and (< 0 expected)
846                    (= 0 (mod count 100)))
847           (message "importing: %d of %d (%.2f%%)"
848                    count expected (/ (* 100 count) expected)))
849         (setq entry (car-safe old)
850               old (cdr-safe old))
851         (let* ((id (car-safe entry))
852                (new-entry (gnus-registry-get-or-make-entry id))
853                (rest (cdr-safe entry))
854                (groups (loop for p in rest
855                              when (stringp p)
856                              collect p))
857                extra-cell key val)
858           ;; remove all the strings from the entry
859           (delete* nil rest :test (lambda (a b) (stringp b)))
860           (gnus-registry-set-id-key id 'group groups)
861           ;; just use the first extra element
862           (setq rest (car-safe rest))
863           (while (car-safe rest)
864             (setq extra-cell (car-safe rest)
865                   key (car-safe extra-cell)
866                   val (cdr-safe extra-cell)
867                   rest (cdr-safe rest))
868             (when (and val (atom val))
869               (setq val (list val)))
870             (gnus-registry-set-id-key id key val))))
871       (message "Import done, collected %d entries" count))))
872
873 (ert-deftest gnus-registry-usage-test ()
874   (let* ((n 100)
875          (tempfile (make-temp-file "gnus-registry-persist"))
876          (db (gnus-registry-make-db tempfile))
877          (gnus-registry-db db)
878          back size)
879     (message "Adding %d keys to the test Gnus registry" n)
880     (dotimes (i n)
881       (let ((id (number-to-string i)))
882         (gnus-registry-handle-action id
883                                      (if (>= 50 i) "fromgroup" nil)
884                                      "togroup"
885                                      (when (>= 70 i)
886                                        (format "subject %d" (mod i 10)))
887                                      (when (>= 80 i)
888                                        (format "sender %d" (mod i 10))))))
889     (message "Testing Gnus registry size is %d" n)
890     (should (= n (registry-size db)))
891     (message "Looking up individual keys (registry-lookup)")
892     (should (equal (loop for e
893                          in (mapcar 'cadr
894                                     (registry-lookup db '("20" "83" "72")))
895                          collect (assq 'subject e)
896                          collect (assq 'sender e)
897                          collect (assq 'group e))
898                    '((subject "subject 0") (sender "sender 0") (group "togroup")
899                      (subject) (sender) (group "togroup")
900                      (subject) (sender "sender 2") (group "togroup"))))
901
902     (message "Looking up individual keys (gnus-registry-id-key)")
903     (should (equal (gnus-registry-get-id-key "34" 'group) '("togroup")))
904     (should (equal (gnus-registry-get-id-key "34" 'subject) '("subject 4")))
905     (message "Trying to insert a duplicate key")
906     (should-error (registry-insert db "55" '()))
907     (message "Looking up individual keys (gnus-registry-get-or-make-entry)")
908     (should (gnus-registry-get-or-make-entry "22"))
909     (message "Saving the Gnus registry to %s" tempfile)
910     (should (gnus-registry-save tempfile db))
911     (setq size (nth 7 (file-attributes tempfile)))
912     (message "Saving the Gnus registry to %s: size %d" tempfile size)
913     (should (< 0 size))
914     (with-temp-buffer
915       (insert-file-contents-literally tempfile)
916       (should (looking-at (concat ";; Object "
917                                   "Gnus Registry"
918                                   "\n;; EIEIO PERSISTENT OBJECT"))))
919     (message "Reading Gnus registry back")
920     (setq back (eieio-persistent-read tempfile))
921     (should back)
922     (message "Read Gnus registry back: %d keys, expected %d==%d"
923              (registry-size back) n (registry-size db))
924     (should (= (registry-size back) n))
925     (should (= (registry-size back) (registry-size db)))
926     (delete-file tempfile)
927     (message "Pruning Gnus registry to 0 by setting :max-soft")
928     (oset db :max-soft 0)
929     (registry-prune db)
930     (should (= (registry-size db) 0)))
931   (message "Done with Gnus registry usage testing."))
932
933 ;;;###autoload
934 (defun gnus-registry-initialize ()
935 "Initialize the Gnus registry."
936   (interactive)
937   (gnus-message 5 "Initializing the registry")
938   (setq gnus-registry-install t)        ; in case it was 'ask or nil
939   (gnus-registry-install-hooks)
940   (gnus-registry-install-shortcuts)
941   (gnus-registry-read))
942
943 ;;;###autoload
944 (defun gnus-registry-install-hooks ()
945   "Install the registry hooks."
946   (interactive)
947   (add-hook 'gnus-summary-article-move-hook 'gnus-registry-action)
948   (add-hook 'gnus-summary-article-delete-hook 'gnus-registry-action)
949   (add-hook 'gnus-summary-article-expire-hook 'gnus-registry-action)
950   (add-hook 'nnmail-spool-hook 'gnus-registry-spool-action)
951
952   (add-hook 'gnus-save-newsrc-hook 'gnus-registry-save)
953   (add-hook 'gnus-read-newsrc-el-hook 'gnus-registry-read)
954
955   (add-hook 'gnus-summary-prepare-hook 'gnus-registry-register-message-ids))
956
957 (defun gnus-registry-unload-hook ()
958   "Uninstall the registry hooks."
959   (interactive)
960   (remove-hook 'gnus-summary-article-move-hook 'gnus-registry-action)
961   (remove-hook 'gnus-summary-article-delete-hook 'gnus-registry-action)
962   (remove-hook 'gnus-summary-article-expire-hook 'gnus-registry-action)
963   (remove-hook 'nnmail-spool-hook 'gnus-registry-spool-action)
964
965   (remove-hook 'gnus-save-newsrc-hook 'gnus-registry-save)
966   (remove-hook 'gnus-read-newsrc-el-hook 'gnus-registry-read)
967
968   (remove-hook 'gnus-summary-prepare-hook 'gnus-registry-register-message-ids))
969
970 (add-hook 'gnus-registry-unload-hook 'gnus-registry-unload-hook)
971
972 (defun gnus-registry-install-p ()
973   (interactive)
974   (when (eq gnus-registry-install 'ask)
975     (setq gnus-registry-install
976           (gnus-y-or-n-p
977            (concat "Enable the Gnus registry?  "
978                    "See the variable `gnus-registry-install' "
979                    "to get rid of this query permanently. ")))
980     (when gnus-registry-install
981       ;; we just set gnus-registry-install to t, so initialize the registry!
982       (gnus-registry-initialize)))
983 ;;; we could call it here: (customize-variable 'gnus-registry-install)
984   gnus-registry-install)
985
986 ;; TODO: a few things
987
988 (provide 'gnus-registry)
989
990 ;;; gnus-registry.el ends here