(nnheader-find-etc-directory): Find the newest one.
[gnus] / lisp / gnus-registry.el
1 ;;; gnus-registry.el --- article registry for Gnus
2 ;; Copyright (C) 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003
3 ;;        Free Software Foundation, Inc.
4
5 ;; Author: Ted Zlatanov <tzz@lifelogs.com>
6 ;; Keywords: news
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 2, or (at your option)
13 ;; 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; see the file COPYING.  If not, write to the
22 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
23 ;; Boston, MA 02111-1307, USA.
24
25 ;;; Commentary:
26
27 ;; This is the gnus-registry.el package, works with other backends
28 ;; besides nnmail.  The major issue is that it doesn't go across
29 ;; backends, so for instance if an article is in nnml:sys and you see
30 ;; a reference to it in nnimap splitting, the article will end up in
31 ;; nnimap:sys
32
33 ;; gnus-registry.el intercepts article respooling, moving, deleting,
34 ;; and copying for all backends.  If it doesn't work correctly for
35 ;; you, submit a bug report and I'll be glad to fix it.  It needs
36 ;; documentation in the manual (also on my to-do list).
37
38 ;; Put this in your startup file (~/.gnus.el for instance)
39
40 ;; (setq gnus-registry-max-entries 2500
41 ;;       gnus-registry-use-long-group-names t)
42
43 ;; (gnus-registry-initialize)
44
45 ;; Then use this in your fancy-split:
46
47 ;; (: gnus-registry-split-fancy-with-parent)
48
49 ;; TODO:
50
51 ;; - get the correct group on spool actions
52
53 ;; - articles that are spooled to a different backend should be handled
54
55 ;;; Code:
56
57 (eval-when-compile (require 'cl))
58
59 (require 'gnus)
60 (require 'gnus-int)
61 (require 'gnus-sum)
62 (require 'nnmail)
63
64 (defvar gnus-registry-dirty t
65  "Boolean set to t when the registry is modified")
66
67 (defgroup gnus-registry nil
68   "The Gnus registry."
69   :group 'gnus)
70
71 (defvar gnus-registry-hashtb nil
72   "*The article registry by Message ID.")
73
74 (defcustom gnus-registry-unfollowed-groups '("delayed" "drafts" "queue")
75   "List of groups that gnus-registry-split-fancy-with-parent won't follow.
76 The group names are matched, they don't have to be fully qualified."
77   :group 'gnus-registry
78   :type '(repeat string))
79
80 (defcustom gnus-registry-install nil
81   "Whether the registry should be installed."
82   :group 'gnus-registry
83   :type 'boolean)
84
85 (defcustom gnus-registry-clean-empty t
86   "Whether the empty registry entries should be deleted.
87 Registry entries are considered empty when they have no groups."
88   :group 'gnus-registry
89   :type 'boolean)
90
91 (defcustom gnus-registry-use-long-group-names nil
92   "Whether the registry should use long group names (BUGGY)."
93   :group 'gnus-registry
94   :type 'boolean)
95
96 (defcustom gnus-registry-track-extra nil
97   "Whether the registry should track extra data about a message.
98 The Subject and Sender (From:) headers are currently tracked this
99 way."
100   :group 'gnus-registry
101   :type      
102   '(set :tag "Tracking choices"
103     (const :tag "Track by subject (Subject: header)" subject)
104     (const :tag "Track by sender (From: header)"  sender)))
105
106 (defcustom gnus-registry-entry-caching t
107   "Whether the registry should cache extra information."
108   :group 'gnus-registry
109   :type 'boolean)
110
111 (defcustom gnus-registry-minimum-subject-length 5
112   "The minimum length of a subject before it's considered trackable."
113   :group 'gnus-registry
114   :type 'integer)
115
116 (defcustom gnus-registry-trim-articles-without-groups t
117   "Whether the registry should clean out message IDs without groups."
118   :group 'gnus-registry
119   :type 'boolean)
120
121 (defcustom gnus-registry-cache-file "~/.gnus.registry.eld"
122   "File where the Gnus registry will be stored."
123   :group 'gnus-registry
124   :type 'file)
125
126 (defcustom gnus-registry-max-entries nil
127   "Maximum number of entries in the registry, nil for unlimited."
128   :group 'gnus-registry
129   :type '(radio (const :format "Unlimited " nil)
130                 (integer :format "Maximum number: %v\n" :size 0)))
131
132 ;; Function(s) missing in Emacs 20
133 (when (memq nil (mapcar 'fboundp '(puthash)))
134   (require 'cl)
135   (unless (fboundp 'puthash)
136     ;; alias puthash is missing from Emacs 20 cl-extra.el
137     (defalias 'puthash 'cl-puthash)))
138
139 (defun gnus-registry-track-subject-p ()
140   (memq 'subject gnus-registry-track-extra))
141
142 (defun gnus-registry-track-sender-p ()
143   (memq 'sender gnus-registry-track-extra))
144
145 (defun gnus-registry-cache-read ()
146   "Read the registry cache file."
147   (interactive)
148   (let ((file gnus-registry-cache-file))
149     (when (file-exists-p file)
150       (gnus-message 5 "Reading %s..." file)
151       (gnus-load file)
152       (gnus-message 5 "Reading %s...done" file))))
153
154 (defun gnus-registry-cache-save ()
155   "Save the registry cache file."
156   (interactive)
157   (let ((file gnus-registry-cache-file))
158     (save-excursion
159       (set-buffer (gnus-get-buffer-create " *Gnus-registry-cache*"))
160       (make-local-variable 'version-control)
161     (setq version-control gnus-backup-startup-file)
162     (setq buffer-file-name file)
163     (setq default-directory (file-name-directory buffer-file-name))
164     (buffer-disable-undo)
165     (erase-buffer)
166     (gnus-message 5 "Saving %s..." file)
167     (if gnus-save-startup-file-via-temp-buffer
168         (let ((coding-system-for-write gnus-ding-file-coding-system)
169               (standard-output (current-buffer)))
170           (gnus-gnus-to-quick-newsrc-format t "gnus registry startup file" 'gnus-registry-alist)
171           (gnus-registry-cache-whitespace file)
172           (save-buffer))
173       (let ((coding-system-for-write gnus-ding-file-coding-system)
174             (version-control gnus-backup-startup-file)
175             (startup-file file)
176             (working-dir (file-name-directory file))
177             working-file
178             (i -1))
179         ;; Generate the name of a non-existent file.
180         (while (progn (setq working-file
181                             (format
182                              (if (and (eq system-type 'ms-dos)
183                                       (not (gnus-long-file-names)))
184                                  "%s#%d.tm#" ; MSDOS limits files to 8+3
185                                (if (memq system-type '(vax-vms axp-vms))
186                                    "%s$tmp$%d"
187                                  "%s#tmp#%d"))
188                              working-dir (setq i (1+ i))))
189                       (file-exists-p working-file)))
190         
191         (unwind-protect
192             (progn
193               (gnus-with-output-to-file working-file
194                 (gnus-gnus-to-quick-newsrc-format t "gnus registry startup file" 'gnus-registry-alist))
195               
196               ;; These bindings will mislead the current buffer
197               ;; into thinking that it is visiting the startup
198               ;; file.
199               (let ((buffer-backed-up nil)
200                     (buffer-file-name startup-file)
201                     (file-precious-flag t)
202                     (setmodes (file-modes startup-file)))
203                 ;; Backup the current version of the startup file.
204                 (backup-buffer)
205                 
206                 ;; Replace the existing startup file with the temp file.
207                 (rename-file working-file startup-file t)
208                 (set-file-modes startup-file setmodes)))
209           (condition-case nil
210               (delete-file working-file)
211             (file-error nil)))))
212     
213     (gnus-kill-buffer (current-buffer))
214     (gnus-message 5 "Saving %s...done" file))))
215
216 ;; Idea from Dan Christensen <jdc@chow.mat.jhu.edu>
217 ;; Save the gnus-registry file with extra line breaks.
218 (defun gnus-registry-cache-whitespace (filename)
219   (gnus-message 5 "Adding whitespace to %s" filename)
220   (save-excursion
221     (goto-char (point-min))
222     (while (re-search-forward "^(\\|(\\\"" nil t)
223       (replace-match "\n\\&" t))
224     (goto-char (point-min))
225     (while (re-search-forward " $" nil t)
226       (replace-match "" t t))))
227
228 (defun gnus-registry-save (&optional force)
229   (when (or gnus-registry-dirty force)
230     (let ((caching gnus-registry-entry-caching))
231       ;; turn off entry caching, so mtime doesn't get recorded
232       (setq gnus-registry-entry-caching nil)
233       ;; remove entry caches
234       (maphash
235        (lambda (key value)
236          (if (hash-table-p value)
237              (remhash key gnus-registry-hashtb)))
238        gnus-registry-hashtb)
239       ;; remove empty entries
240       (when gnus-registry-clean-empty 
241         (gnus-registry-clean-empty-function))
242       ;; now trim the registry appropriately
243       (setq gnus-registry-alist (gnus-registry-trim 
244                                  (hashtable-to-alist gnus-registry-hashtb)))
245       ;; really save
246       (gnus-registry-cache-save)
247       (setq gnus-registry-entry-caching caching)
248       (setq gnus-registry-dirty nil))))
249
250 (defun gnus-registry-clean-empty-function ()
251   "Remove all empty entries from the registry.  Returns count thereof."
252   (let ((count 0))
253     (maphash
254      (lambda (key value)
255        (unless (gnus-registry-fetch-group key)
256          (incf count)
257          (remhash key gnus-registry-hashtb)))
258      gnus-registry-hashtb)
259     count))
260
261 (defun gnus-registry-read ()
262   (gnus-registry-cache-read)
263   (setq gnus-registry-hashtb (alist-to-hashtable gnus-registry-alist))
264   (setq gnus-registry-dirty nil))
265
266 (defun gnus-registry-trim (alist)
267   "Trim alist to size, using gnus-registry-max-entries."
268   (if (null gnus-registry-max-entries)
269       alist                             ; just return the alist
270     ;; else, when given max-entries, trim the alist
271     (let ((timehash (make-hash-table                        
272                      :size 4096
273                      :test 'equal)))
274       (maphash
275        (lambda (key value)
276          (puthash key (gnus-registry-fetch-extra key 'mtime) timehash))
277        gnus-registry-hashtb)
278
279       ;; we use the return value of this setq, which is the trimmed alist
280       (setq alist
281             (nthcdr
282              (- (length alist) gnus-registry-max-entries)
283              (sort alist 
284                    (lambda (a b)
285                      (time-less-p 
286                       (cdr (gethash (car a) timehash))
287                       (cdr (gethash (car b) timehash))))))))))
288
289 (defun alist-to-hashtable (alist)
290   "Build a hashtable from the values in ALIST."
291   (let ((ht (make-hash-table                        
292              :size 4096
293              :test 'equal)))
294     (mapc
295      (lambda (kv-pair)
296        (puthash (car kv-pair) (cdr kv-pair) ht))
297      alist)
298      ht))
299
300 (defun hashtable-to-alist (hash)
301   "Build an alist from the values in HASH."
302   (let ((list nil))
303     (maphash
304      (lambda (key value)
305        (setq list (cons (cons key value) list)))
306      hash)
307     list))
308
309 (defun gnus-registry-action (action data-header from &optional to method)
310   (let* ((id (mail-header-id data-header))
311          (subject (gnus-registry-simplify-subject 
312                    (mail-header-subject data-header)))
313          (sender (mail-header-from data-header))
314          (from (gnus-group-guess-full-name-from-command-method from))
315          (to (if to (gnus-group-guess-full-name-from-command-method to) nil))
316          (to-name (if to to "the Bit Bucket"))
317          (old-entry (gethash id gnus-registry-hashtb)))
318     (gnus-message 5 "Registry: article %s %s from %s to %s"
319                   id
320                   (if method "respooling" "going")
321                   from
322                   to)
323
324     ;; All except copy will need a delete
325     (gnus-registry-delete-group id from)
326
327     (when (equal 'copy action) 
328       (gnus-registry-add-group id from subject sender)) ; undo the delete
329
330     (gnus-registry-add-group id to subject sender)))
331
332 (defun gnus-registry-spool-action (id group &optional subject sender)
333   (let ((group (gnus-group-guess-full-name-from-command-method group)))
334     (when (and (stringp id) (string-match "\r$" id))
335       (setq id (substring id 0 -1)))
336     (gnus-message 5 "Registry: article %s spooled to %s"
337                   id
338                   group)
339     (gnus-registry-add-group id group subject sender)))
340
341 ;; Function for nn{mail|imap}-split-fancy: look up all references in
342 ;; the cache and if a match is found, return that group.
343 (defun gnus-registry-split-fancy-with-parent ()
344   "Split this message into the same group as its parent.  The parent
345 is obtained from the registry.  This function can be used as an entry
346 in `nnmail-split-fancy' or `nnimap-split-fancy', for example like
347 this: (: gnus-registry-split-fancy-with-parent) 
348
349 For a message to be split, it looks for the parent message in the
350 References or In-Reply-To header and then looks in the registry to
351 see which group that message was put in.  This group is returned.
352
353 See the Info node `(gnus)Fancy Mail Splitting' for more details."
354   (let ((refstr (or (message-fetch-field "references")
355                     (message-fetch-field "in-reply-to")))
356         (nnmail-split-fancy-with-parent-ignore-groups
357          (if (listp nnmail-split-fancy-with-parent-ignore-groups)
358              nnmail-split-fancy-with-parent-ignore-groups
359            (list nnmail-split-fancy-with-parent-ignore-groups)))
360         references res)
361     (if refstr
362         (progn
363           (setq references (nreverse (gnus-split-references refstr)))
364           (mapcar (lambda (x)
365                     (setq res (or (gnus-registry-fetch-group x) res))
366                     (when (or (gnus-registry-grep-in-list
367                                res
368                                gnus-registry-unfollowed-groups)
369                               (gnus-registry-grep-in-list 
370                                res
371                                nnmail-split-fancy-with-parent-ignore-groups))
372                       (setq res nil)))
373                   references))
374
375       ;; else: there were no references, now try the extra tracking
376       (let ((sender (message-fetch-field "from"))
377             (subject (gnus-registry-simplify-subject
378                       (message-fetch-field "subject")))
379             (single-match t))
380         (when (and single-match
381                    (gnus-registry-track-sender-p)
382                    sender)
383           (maphash
384            (lambda (key value)
385              (let ((this-sender (cdr 
386                                  (gnus-registry-fetch-extra key 'sender))))
387                (when (and single-match
388                           this-sender
389                           (equal sender this-sender))
390                  ;; too many matches, bail
391                  (unless (equal res (gnus-registry-fetch-group key))
392                    (setq single-match nil))
393                  (setq res (gnus-registry-fetch-group key))
394                  (gnus-message
395                   ;; raise level of messaging if gnus-registry-track-extra
396                   (if gnus-registry-track-extra 5 9)
397                   "%s (extra tracking) traced sender %s to group %s"
398        &