*** empty log message ***
[gnus] / lisp / gnus-score.el
1 ;;; gnus-score.el --- scoring code for Gnus
2 ;; Copyright (C) 1995 Free Software Foundation, Inc.
3
4 ;; Author: Per Abrahamsen <amanda@iesd.auc.dk>
5 ;;      Lars Magne Ingebrigtsen <larsi@ifi.uio.no>
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
22 ;; the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
23
24 ;;; Commentary:
25
26 ;;; Code:
27
28 (require 'gnus)
29 (eval-when-compile (require 'cl))
30
31 (defvar gnus-global-score-files nil
32   "*List of global score files and directories.
33 Set this variable if you want to use people's score files.  One entry
34 for each score file or each score file directory.  Gnus will decide
35 by itself what score files are applicable to which group.
36
37 Say you want to use the single score file
38 \"/ftp.ifi.uio.no@ftp:/pub/larsi/ding/score/soc.motss.SCORE\" and all
39 score files in the \"/ftp.some-where:/pub/score\" directory.
40
41  (setq gnus-global-score-files
42        '(\"/ftp.ifi.uio.no:/pub/larsi/ding/score/soc.motss.SCORE\"
43          \"/ftp.some-where:/pub/score\"))")
44
45 (defvar gnus-score-file-single-match-alist nil
46   "*Alist mapping regexps to lists of score files.
47 Each element of this alist should be of the form
48         (\"REGEXP\" [ \"SCORE-FILE-1\" ] [ \"SCORE-FILE-2\" ] ... )
49
50 If the name of a group is matched by REGEXP, the corresponding scorefiles
51 will be used for that group.
52 The first match found is used, subsequent matching entries are ignored (to
53 use multiple matches, see gnus-score-file-multiple-match-alist).
54
55 These score files are loaded in addition to any files returned by
56 gnus-score-find-score-files-function (which see).")
57
58 (defvar gnus-score-file-multiple-match-alist nil
59   "*Alist mapping regexps to lists of score files.
60 Each element of this alist should be of the form
61         (\"REGEXP\" [ \"SCORE-FILE-1\" ] [ \"SCORE-FILE-2\" ] ... )
62
63 If the name of a group is matched by REGEXP, the corresponding scorefiles
64 will be used for that group.
65 If multiple REGEXPs match a group, the score files corresponding to each
66 match will be used (for only one match to be used, see
67 gnus-score-file-single-match-alist).
68
69 These score files are loaded in addition to any files returned by
70 gnus-score-find-score-files-function (which see).")
71
72 (defvar gnus-score-file-suffix "SCORE"
73   "*Suffix of the score files.")
74
75 (defvar gnus-adaptive-file-suffix "ADAPT"
76   "*Suffix of the adaptive score files.")
77
78 (defvar gnus-score-find-score-files-function 'gnus-score-find-bnews
79   "*Function used to find score files.
80 The function will be called with the group name as the argument, and
81 should return a list of score files to apply to that group.  The score
82 files do not actually have to exist.
83
84 Predefined values are:
85
86 gnus-score-find-single: Only apply the group's own score file.
87 gnus-score-find-hierarchical: Also apply score files from parent groups.
88 gnus-score-find-bnews: Apply score files whose names matches.
89
90 See the documentation to these functions for more information.
91
92 This variable can also be a list of functions to be called.  Each
93 function should either return a list of score files, or a list of
94 score alists.")
95
96 (defvar gnus-score-interactive-default-score 1000
97   "*Scoring commands will raise/lower the score with this number as the default.")
98
99 (defvar gnus-score-expiry-days 7
100   "*Number of days before unused score file entries are expired.")
101
102 (defvar gnus-orphan-score nil
103   "*All orphans get this score added. Set in the score file.")
104
105 (defvar gnus-default-adaptive-score-alist  
106   '((gnus-kill-file-mark)
107     (gnus-unread-mark)
108     (gnus-read-mark (from  3) (subject  30))
109     (gnus-catchup-mark (subject -10))
110     (gnus-killed-mark (from -1) (subject -20))
111     (gnus-del-mark (from -2) (subject -15)))
112 "*Alist of marks and scores.")
113
114 (defvar gnus-file-name-translation-table nil
115   "*Table for translating characters in file names.
116
117 Under OS/2 you'd typically set this variable to 
118
119   '(\?: \?_)")
120
121 (defvar gnus-score-mimic-keymap nil
122   "*Have the score entry functions pretend that they are a keymap.")
123
124 (defvar gnus-score-exact-adapt-limit 10
125   "*Number that says how long a match has to be before using substring matching.
126 When doing adaptive scoring, one normally uses fuzzy or substring
127 matching. However, if the header one matches is short, the possibility
128 for false positives is great, so if the length of the match is less
129 than this variable, exact matching will be used.
130
131 If this variable is nil, exact matching will always be used.")
132
133 (defvar gnus-score-uncacheable-files "ADAPT$"
134   "*All score files that match this regexp will not be cached.")
135
136 \f
137
138 ;; Internal variables.
139
140 (defvar gnus-internal-global-score-files nil)
141 (defvar gnus-score-file-list nil)
142
143 (defvar gnus-score-help-winconf nil)
144 (defvar gnus-adaptive-score-alist gnus-default-adaptive-score-alist)
145 (defvar gnus-score-trace nil)
146 (defvar gnus-score-edit-buffer nil)
147
148 (defvar gnus-score-alist nil
149   "Alist containing score information.
150 The keys can be symbols or strings.  The following symbols are defined. 
151
152 touched: If this alist has been modified.
153 mark:    Automatically mark articles below this.
154 expunge: Automatically expunge articles below this.
155 files:   List of other score files to load when loading this one.
156 eval:    Sexp to be evaluated when the score file is loaded.
157
158 String entries have the form (HEADER (MATCH TYPE SCORE DATE) ...) 
159 where HEADER is the header being scored, MATCH is the string we are
160 looking for, TYPE is a flag indicating whether it should use regexp or
161 substring matching, SCORE is the score to add and DATE is the date
162 of the last successful match.")
163
164 (defvar gnus-score-cache nil)
165 (defvar gnus-scores-articles nil)
166 (defvar gnus-header-index nil)
167 (defvar gnus-score-index nil)
168
169 (eval-and-compile
170   (autoload 'gnus-uu-ctl-map "gnus-uu" nil nil 'keymap)
171   (autoload 'appt-select-lowest-window "appt.el"))
172
173 ;;; Summary mode score maps.
174
175 (defvar gnus-summary-score-map nil)
176
177 (define-prefix-command 'gnus-summary-score-map)
178 (define-key gnus-summary-mode-map "V" 'gnus-summary-score-map)
179 (define-key gnus-summary-score-map "s" 'gnus-summary-set-score)
180 (define-key gnus-summary-score-map "a" 'gnus-summary-score-entry)
181 (define-key gnus-summary-score-map "S" 'gnus-summary-current-score)
182 (define-key gnus-summary-score-map "c" 'gnus-score-change-score-file)
183 (define-key gnus-summary-score-map "m" 'gnus-score-set-mark-below)
184 (define-key gnus-summary-score-map "x" 'gnus-score-set-expunge-below)
185 (define-key gnus-summary-score-map "R" 'gnus-summary-rescore)
186 (define-key gnus-summary-score-map "e" 'gnus-score-edit-alist)
187 (define-key gnus-summary-score-map "f" 'gnus-score-edit-file)
188 (define-key gnus-summary-score-map "t" 'gnus-score-find-trace)
189 (define-key gnus-summary-score-map "C" 'gnus-score-customize)
190
191
192
193 ;; Summary score file commands
194
195 ;; Much modification of the kill (ahem, score) code and lots of the
196 ;; functions are written by Per Abrahamsen <amanda@iesd.auc.dk>.
197
198 (defun gnus-summary-lower-score (&optional score)
199   "Make a score entry based on the current article.
200 The user will be prompted for header to score on, match type,
201 permanence, and the string to be used.  The numerical prefix will be
202 used as score."
203   (interactive "P")
204   (gnus-summary-increase-score (- (gnus-score-default score))))
205
206 (defun gnus-summary-increase-score (&optional score)
207   "Make a score entry based on the current article.
208 The user will be prompted for header to score on, match type,
209 permanence, and the string to be used.  The numerical prefix will be
210 used as score."
211   (interactive "P")
212   (gnus-set-global-variables)
213   (let* ((nscore (gnus-score-default score))
214          (prefix (if (< nscore 0) ?L ?I))
215          (increase (> nscore 0))
216          (char-to-header 
217           '((?a "from" nil nil string)
218             (?s "subject" nil nil string)
219             (?b "body" "" nil body-string)
220             (?h "head" "" nil body-string)
221             (?i "message-id" nil t string)
222             (?t "references" "message-id" t string)
223             (?x "xref" nil nil string)
224             (?l "lines" nil nil number)
225             (?d "date" nil nil date)
226             (?f "followup" nil nil string)))
227          (char-to-type
228           '((?s s "substring" string)
229             (?e e "exact string" string)
230             (?f f "fuzzy string" string)
231             (?r r "regexp string" string)
232             (?s s "substring" body-string)
233             (?r s "regexp string" body-string)
234             (?b before "before date" date)
235             (?a at "at date" date) 
236             (?n now "this date" date)
237             (?< < "less than number" number)
238             (?> > "greater than number" number) 
239             (?= = "equal to number" number)))
240          (char-to-perm
241           (list (list ?t (current-time-string) "temporary") 
242                 '(?p perm "permanent") '(?i now "immediate")))
243          (mimic gnus-score-mimic-keymap)
244          hchar entry temporary tchar pchar end type match)
245     ;; First we read the header to score.
246     (while (not hchar)
247       (if mimic
248           (progn 
249             (sit-for 1)
250             (message "%c-" prefix))
251         (message "%s header (%s?): " (if increase "Increase" "Lower")
252                  (mapconcat (lambda (s) (char-to-string (car s)))
253                             char-to-header "")))
254       (setq hchar (read-char))
255       (if (not (or (= hchar ??) (= hchar ?\C-h)))
256           ()
257         (setq hchar nil)
258         (gnus-score-insert-help "Match on header" char-to-header 1)))
259
260     (and (get-buffer "*Score Help*")
261          (progn
262            (kill-buffer "*Score Help*")
263            (and gnus-score-help-winconf
264                 (set-window-configuration gnus-score-help-winconf))))
265
266     (or (setq entry (assq (downcase hchar) char-to-header))
267         (progn
268           (ding)
269           (setq end t)
270           (if mimic (message "%c %c" prefix hchar) (message ""))))
271     (if (or end (/= (downcase hchar) hchar))
272         (progn
273           ;; This was a majuscle, so we end reading and set the defaults.
274           (if mimic (message "%c %c" prefix hchar) (message ""))
275           (setq type nil
276                 temporary (current-time-string)))
277
278       ;; We continue reading - the type.
279       (while (not tchar)
280         (if mimic
281             (progn
282               (sit-for 1)
283               (message "%c %c-" prefix hchar))
284           (message "%s header '%s' with match type (%s?): "
285                    (if increase "Increase" "Lower")
286                    (nth 1 entry)
287                    (mapconcat (lambda (s) 
288                                 (if (eq (nth 4 entry) 
289                                         (nth 3 s))
290                                     (char-to-string (car s))
291                                   ""))
292                               char-to-type "")))
293         (setq tchar (read-char))
294         (if (not (or (= tchar ??) (= tchar ?\C-h)))
295             ()
296           (setq tchar nil)
297           (gnus-score-insert-help "Match type" char-to-type 2)))
298
299       (and (get-buffer "*Score Help*")
300            (progn
301              (and gnus-score-help-winconf
302                   (set-window-configuration gnus-score-help-winconf))
303              (kill-buffer "*Score Help*")))
304       
305       (or (setq type (nth 1 (assq (downcase tchar) char-to-type)))
306           (progn
307             (ding)
308             (if mimic (message "%c %c" prefix hchar) (message ""))
309             (setq end t)))
310       (if (or end (/= (downcase tchar) tchar))
311           (progn
312             ;; It was a majuscle, so we end reading and the the default.
313             (if mimic (message "%c %c %c" prefix hchar tchar)
314               (message ""))
315             (setq temporary (current-time-string)))
316
317         ;; We continue reading.
318         (while (not pchar)
319           (if mimic
320               (progn
321                 (sit-for 1)
322                 (message "%c %c %c-" prefix hchar tchar))
323             (message "%s permanence (%s?): " (if increase "Increase" "Lower")
324                      (mapconcat (lambda (s) (char-to-string (car s)))
325                                 char-to-perm "")))
326           (setq pchar (read-char))
327           (if (not (or (= pchar ??) (= pchar ?\C-h)))
328               ()
329             (setq pchar nil)
330             (gnus-score-insert-help "Match permanence" char-to-perm 2)))
331
332         (and (get-buffer "*Score Help*")
333              (progn
334                (and gnus-score-help-winconf
335                     (set-window-configuration gnus-score-help-winconf))
336                (kill-buffer "*Score Help*")))
337
338         (if mimic (message "%c %c %c" prefix hchar tchar pchar)
339           (message ""))
340         (if (setq temporary (nth 1 (assq pchar char-to-perm)))
341             ()
342           (ding)
343           (setq end t)
344           (if mimic 
345               (message "%c %c %c %c" prefix hchar tchar pchar)
346             (message "")))))
347
348     ;; We have all the data, so we enter this score.
349     (if end
350         ()
351       (setq match (if (string= (nth 2 entry) "") ""
352                     (gnus-summary-header (or (nth 2 entry) (nth 1 entry)))))
353       
354       ;; Modify the match, perhaps.
355       (cond 
356        ((equal (nth 1 entry) "xref")
357         (when (string-match "^Xref: *" match)
358           (setq match (substring match (match-end 0))))
359         (when (string-match "^[^:]* +" match)
360           (setq match (substring match (match-end 0))))))
361
362       (gnus-summary-score-entry
363        (nth 1 entry)                    ; Header
364        match                            ; Match
365        type                             ; Type
366        (if (eq 's score) nil score)     ; Score
367        (if (eq 'perm temporary)         ; Temp
368            nil
369          temporary)
370        (not (nth 3 entry)))             ; Prompt
371       )))
372   
373 (defun gnus-score-insert-help (string alist idx)
374   (setq gnus-score-help-winconf (current-window-configuration))
375   (save-excursion
376     (set-buffer (get-buffer-create "*Score Help*"))
377     (buffer-disable-undo (current-buffer))
378     (delete-windows-on (current-buffer))
379     (erase-buffer)
380     (insert string ":\n\n")
381     (let ((max -1)
382           (list alist)
383           (i 0)
384           n width pad format)
385       ;; find the longest string to display
386       (while list
387         (setq n (length (nth idx (car list))))
388         (or (> max n)
389             (setq max n))
390         (setq list (cdr list)))
391       (setq max (+ max 4))              ; %c, `:', SPACE, a SPACE at end
392       (setq n (/ (window-width) max))   ; items per line
393       (setq width (/ (window-width) n)) ; width of each item
394       ;; insert `n' items, each in a field of width `width' 
395       (while alist
396         (if (< i n)
397             ()
398           (setq i 0)
399           (delete-char -1)              ; the `\n' takes a char
400           (insert "\n"))
401         (setq pad (- width 3))
402         (setq format (concat "%c: %-" (int-to-string pad) "s"))
403         (insert (format format (car (car alist)) (nth idx (car alist))))
404         (setq alist (cdr alist))
405         (setq i (1+ i))))
406     ;; display ourselves in a small window at the bottom
407     (appt-select-lowest-window)
408     (split-window)
409     (pop-to-buffer "*Score Help*")
410     (shrink-window-if-larger-than-buffer)
411     (select-window (get-buffer-window gnus-summary-buffer))))
412   
413 (defun gnus-summary-header (header &optional no-err)
414   ;; Return HEADER for current articles, or error.
415   (let ((article (gnus-summary-article-number))
416         headers)
417     (if article
418         (if (and (setq headers (gnus-summary-article-header article))
419                  (vectorp headers))
420             (aref headers (nth 1 (assoc header gnus-header-index)))
421           (if no-err
422               nil
423             (error "Pseudo-articles can't be scored")))
424       (if no-err
425           (error "No article on current line")
426         nil))))
427
428 (defsubst gnus-score-get (symbol &optional alist)
429   ;; Get SYMBOL's definition in ALIST.
430   (cdr (assoc symbol 
431               (or alist 
432                   gnus-score-alist
433                   (progn
434                     (gnus-score-load 
435                      (gnus-score-file-name gnus-newsgroup-name))
436                     gnus-score-alist)))))
437
438 (defun gnus-summary-score-entry 
439   (header match type score date &optional prompt silent)
440   "Enter score file entry.
441 HEADER is the header being scored.
442 MATCH is the string we are looking for.
443 TYPE is the match type: substring, regexp, exact, fuzzy.
444 SCORE is the score to add.
445 DATE is the expire date, or nil for no expire, or 'now for immediate expire.
446 If optional argument `PROMPT' is non-nil, allow user to edit match.
447 If optional argument `SILENT' is nil, show effect of score entry."
448   (interactive
449    (list (completing-read "Header: "
450                           gnus-header-index
451                           (lambda (x) (fboundp (nth 2 x)))
452                           t)
453          (read-string "Match: ")
454          (if (y-or-n-p "Use regexp match? ") 'r 's)
455          (and current-prefix-arg
456               (prefix-numeric-value current-prefix-arg))
457          (cond ((not (y-or-n-p "Add to score file? "))
458                 'now)
459                ((y-or-n-p "Expire kill? ")
460                 (current-time-string))
461                (t nil))))
462   ;; Regexp is the default type.
463   (if (eq type t) (setq type 'r))
464   ;; Simplify matches...
465   (cond ((or (eq type 'r) (eq type 's) (eq type nil))
466          (setq match (if match (gnus-simplify-subject-re match) "")))
467         ((eq type 'f)
468          (setq match (gnus-simplify-subject-fuzzy match))))
469   (let ((score (gnus-score-default score))
470         (header (downcase header)))
471     (and prompt (setq match (read-string 
472                              (format "Match %s on %s, %s: " 
473                                      (cond ((eq date 'now)
474                                             "now")
475                                            ((stringp date)
476                                             "temp")
477                                            (t "permanent"))
478                                      header
479                                      (if (< score 0) "lower" "raise"))
480                              (if (numberp match)
481                                  (int-to-string match)
482                                match))))
483     (and (>= (nth 1 (assoc header gnus-header-index)) 0)
484          (eq (nth 2 (assoc header gnus-header-index)) 'gnus-score-string)
485          (not silent)
486          (gnus-summary-score-effect header match type score))
487
488     ;; If this is an integer comparison, we transform from string to int. 
489     (and (eq (nth 2 (assoc header gnus-header-index)) 'gnus-score-integer)
490          (setq match (string-to-int match)))
491
492     (if (eq date 'now)
493         ()
494       (and (= score gnus-score-interactive-default-score)
495            (setq score nil))
496       (let ((new (cond 
497                   (type
498                    (list match score (and date (gnus-day-number date)) type))
499                   (date
500                    (list match score (gnus-day-number date)))
501                   (score
502                    (list match score))
503                   (t
504                    (list match))))
505             (old (gnus-score-get header))
506             elem)
507         ;; We see whether we can collapse some score entries.
508         ;; This isn't quite correct, because there may be more elements
509         ;; later on with the same key that have matching elems... Hm.
510         (if (and old
511                  (setq elem (assoc match old))
512                  (eq (nth 3 elem) (nth 3 new))
513                  (or (and (numberp (nth 2 elem)) (numberp (nth 2 new)))
514                      (and (not (nth 2 elem)) (not (nth 2 new)))))
515             ;; Yup, we just add this new score to the old elem.
516             (setcar (cdr elem) (+ (or (nth 1 elem) 
517                                       gnus-score-interactive-default-score)
518                                   (or (nth 1 new)
519                                       gnus-score-interactive-default-score)))
520           ;; Nope, we have to add a new elem.
521           (gnus-score-set header (if old (cons new old) (list new)))))
522       (gnus-score-set 'touched '(t)))))
523
524 (defun gnus-summary-score-effect (header match type score)
525   "Simulate the effect of a score file entry.
526 HEADER is the header being scored.
527 MATCH is the string we are looking for.
528 TYPE is a flag indicating if it is a regexp or substring.
529 SCORE is the score to add."
530   (interactive (list (completing-read "Header: "
531                                       gnus-header-index
532                                       (lambda (x) (fboundp (nth 2 x)))
533                                       t)
534                      (read-string "Match: ")
535                      (y-or-n-p "Use regexp match? ")
536                      (prefix-numeric-value current-prefix-arg)))
537   (save-excursion
538     (or (and (stringp match) (> (length match) 0))
539         (error "No match"))
540     (goto-char (point-min))
541     (let ((regexp (cond ((eq type 'f)
542                          (gnus-simplify-subject-fuzzy match))
543                         (type match)
544                         (t (concat "\\`.*" (regexp-quote match) ".*\\'")))))
545       (while (not (eobp))
546         (let ((content (gnus-summary-header header 'noerr))
547               (case-fold-search t))
548           (and content
549                (if (if (eq type 'f)
550                        (string-equal (gnus-simplify-subject-fuzzy content)
551                                      regexp)
552                      (string-match regexp content))
553                    (gnus-summary-raise-score score))))
554         (beginning-of-line 2)))))
555
556 (defun gnus-summary-score-crossposting (score date)
557   ;; Enter score file entry for current crossposting.
558   ;; SCORE is the score to add.
559   ;; DATE is the expire date.
560   (let ((xref (gnus-summary-header "xref"))
561         (start 0)
562         group)
563     (or xref (error "This article is not crossposted"))
564     (while (string-match " \\([^ \t]+\\):" xref start)
565       (setq start (match-end 0))
566       (if (not (string= 
567                 (setq group 
568                       (substring xref (match-beginning 1) (match-end 1)))
569                 gnus-newsgroup-name))
570           (gnus-summary-score-entry
571            "xref" (concat " " group ":") nil score date t)))))
572
573 \f
574 ;;;
575 ;;; Gnus Score Files
576 ;;;
577
578 ;; All score code written by Per Abrahamsen <abraham@iesd.auc.dk>.
579
580 ;; Added by Per Abrahamsen <amanda@iesd.auc.dk>.
581 (defun gnus-score-set-mark-below (score)
582   "Automatically mark articles with score below SCORE as read."
583   (interactive 
584    (list (or (and current-prefix-arg (prefix-numeric-value current-prefix-arg))
585              (string-to-int (read-string "Mark below: ")))))
586   (setq score (or score gnus-summary-default-score 0))
587   (gnus-score-set 'mark (list score))
588   (gnus-score-set 'touched '(t))
589   (setq gnus-summary-mark-below score)
590   (gnus-score-update-lines))
591
592 (defun gnus-score-update-lines ()
593   "Update all lines in the summary buffer."
594   (save-excursion
595     (goto-char (point-min))
596     (while (not (eobp))
597       (gnus-summary-update-line)
598       (forward-line 1))))
599
600 (defun gnus-score-set-expunge-below (score)
601   "Automatically expunge articles with score below SCORE."
602   (interactive 
603    (list (or (and current-prefix-arg (prefix-numeric-value current-prefix-arg))
604              (string-to-int (read-string "Expunge below: ")))))
605   (setq score (or score gnus-summary-default-score 0))
606   (gnus-score-set 'expunge (list score))
607   (gnus-score-set 'touched '(t)))
608
609 (defun gnus-score-followup-article (&optional score)
610   "Add SCORE to all followups to the article in the current buffer."
611   (interactive "P")
612   (setq score (gnus-score-default score))
613   (save-excursion
614     (save-restriction
615       (goto-char (point-min))
616       (let ((id (mail-fetch-field "message-id")))
617         (when id
618           (gnus-summary-score-entry
619            "references" (concat id "[ \t]*$") 'r
620            score (current-time-string)))))))
621
622 (defun gnus-score-followup-thread (&optional score)
623   "Add SCORE to all later articles in the thread the current buffer is part of."
624   (interactive "P")
625   (setq score (gnus-score-default score))
626   (save-excursion
627     (save-restriction
628       (goto-char (point-min))
629       (let ((id (mail-fetch-field "message-id")))
630         (when id
631           (gnus-summary-score-entry
632            "references" id 's
633            score (current-time-string)))))))
634
635 (defun gnus-score-set (symbol value &optional alist)
636   ;; Set SYMBOL to VALUE in ALIST.
637   (let* ((alist 
638           (or alist 
639               gnus-score-alist
640               (progn
641                 (gnus-score-load (gnus-score-file-name gnus-newsgroup-name))
642                 gnus-score-alist)))
643          (entry (assoc symbol alist)))
644     (cond ((gnus-score-get 'read-only alist)
645            ;; This is a read-only score file, so we do nothing.
646            )
647           (entry
648            (setcdr entry value))
649           ((null alist)
650            (error "Empty alist"))
651           (t
652            (setcdr alist
653                    (cons (cons symbol value) (cdr alist)))))))
654
655 (defun gnus-score-change-score-file (file)
656   "Change current score alist."
657   (interactive 
658    (list (read-file-name "Edit score file: " gnus-kill-files-directory)))
659   (gnus-score-load-file file)
660   (gnus-set-mode-line 'summary))
661
662 (defun gnus-score-edit-alist (file)
663   "Edit the current score alist."
664   (interactive (list gnus-current-score-file))
665   (let ((winconf (current-window-configuration)))
666     (and (buffer-name gnus-summary-buffer) (gnus-score-save))
667     (setq gnus-score-edit-buffer (find-file-noselect file))
668     (gnus-configure-windows 'edit-score)
669     (gnus-score-mode)
670     (make-local-variable 'gnus-prev-winconf)
671     (setq gnus-prev-winconf winconf))
672   (gnus-message 
673    4 (substitute-command-keys 
674       "\\<gnus-score-mode-map>\\[gnus-score-edit-done] to save edits")))
675   
676 (defun gnus-score-edit-file (file)
677   "Edit a score file."
678   (interactive 
679    (list (read-file-name "Edit score file: " gnus-kill-files-directory)))
680   (and (buffer-name gnus-summary-buffer) (gnus-score-save))
681   (let ((winconf (current-window-configuration)))
682     (setq gnus-score-edit-buffer (find-file-noselect file))
683     (gnus-configure-windows 'edit-score)
684     (gnus-score-mode)
685     (make-local-variable 'gnus-prev-winconf)
686     (setq gnus-prev-winconf winconf))
687   (gnus-message 
688    4 (substitute-command-keys 
689       "\\<gnus-score-mode-map>\\[gnus-score-edit-done] to save edits")))
690   
691 (defun gnus-score-load-file (file)
692   ;; Load score file FILE.  Returns a list a retrieved score-alists.
693   (setq gnus-kill-files-directory (or gnus-kill-files-directory "~/News/"))
694   (let* ((file (expand-file-name 
695                 (or (and (string-match
696                           (concat "^" (expand-file-name
697                                        gnus-kill-files-directory)) 
698                           (expand-file-name file))
699                          file)
700                     (concat gnus-kill-files-directory file))))
701          (cached (assoc file gnus-score-cache))
702          (global (member file gnus-internal-global-score-files))
703          lists alist)
704     (if cached
705         ;; The score file was already loaded.
706         (setq alist (cdr cached))
707       ;; We load the score file.
708       (setq gnus-score-alist nil)
709       (setq alist (gnus-score-load-score-alist file))
710       ;; We add '(touched) to the alist to signify that it hasn't been
711       ;; touched (yet). 
712       (or (assq 'touched alist) (setq alist (cons (list 'touched nil) alist)))
713       ;; If it is a global score file, we make it read-only.
714       (and global
715            (not (assq 'read-only alist))
716            (setq alist (cons (list 'read-only t) alist)))
717       (setq gnus-score-cache
718             (cons (cons file alist) gnus-score-cache)))
719     ;; If there are actual scores in the alist, we add it to the
720     ;; return value of this function.
721     (if (memq t (mapcar (lambda (e) (stringp (car e))) alist))
722         (setq lists (list alist)))
723     ;; Treat the other possible atoms in the score alist.
724     (let ((mark (car (gnus-score-get 'mark alist)))
725           (expunge (car (gnus-score-get 'expunge alist)))
726           (mark-and-expunge (car (gnus-score-get 'mark-and-expunge alist)))
727           (files (gnus-score-get 'files alist))
728           (exclude-files (gnus-score-get 'exclude-files alist))
729           (orphan (car (gnus-score-get 'orphan alist)))
730           (adapt (gnus-score-get 'adapt alist))
731           (thread-mark-and-expunge
732            (car (gnus-score-get 'thread-mark-and-expunge alist)))
733           (adapt-file (car (gnus-score-get 'adapt-file)))
734           (local (gnus-score-get 'local alist))
735           (eval (car (gnus-score-get 'eval alist))))
736       ;; We do not respect eval and files atoms from global score
737       ;; files. 
738       (and files (not global)
739            (setq lists (apply 'append lists
740                               (mapcar (lambda (file)
741                                         (gnus-score-load-file file)) 
742                                       (if adapt-file (cons adapt-file files)
743                                         files)))))
744       (and eval (not global) (eval eval))
745       ;; We then expand any exclude-file directives.
746       (setq gnus-scores-exclude-files 
747             (nconc 
748              (mapcar 
749               (lambda (sfile) 
750                 (expand-file-name sfile (file-name-directory file)))
751               exclude-files) gnus-scores-exclude-files))
752       (if (not local)
753           ()
754         (save-excursion
755           (set-buffer gnus-summary-buffer)
756           (while local
757             (and (consp (car local))
758                  (symbolp (car (car local)))
759                  (progn
760                    (make-local-variable (car (car local)))
761                    (set (car (car local)) (nth 1 (car local)))))
762             (setq local (cdr local)))))
763       (if orphan (setq gnus-orphan-score orphan))
764       (setq gnus-adaptive-score-alist
765             (cond ((equal adapt '(t))
766                    (setq gnus-newsgroup-adaptive t)
767                    gnus-default-adaptive-score-alist)
768                   ((equal adapt '(ignore))
769                    (setq gnus-newsgroup-adaptive nil))
770                   ((consp adapt)
771                    (setq gnus-newsgroup-adaptive t)
772                    adapt)
773                   (t
774                    ;;(setq gnus-newsgroup-adaptive gnus-use-adaptive-scoring)
775                    gnus-default-adaptive-score-alist)))
776       (setq gnus-thread-expunge-below 
777             (or thread-mark-and-expunge gnus-thread-expunge-below))
778       (setq gnus-summary-mark-below 
779             (or mark mark-and-expunge gnus-summary-mark-below))
780       (setq gnus-summary-expunge-below 
781             (or expunge mark-and-expunge gnus-summary-expunge-below))
782       (setq gnus-newsgroup-adaptive-score-file 
783             (or adapt-file gnus-newsgroup-adaptive-score-file)))
784     (setq gnus-current-score-file file)
785     (setq gnus-score-alist alist)
786     lists))
787
788 (defun gnus-score-load (file)
789   ;; Load score FILE.
790   (let ((cache (assoc file gnus-score-cache)))
791     (if cache
792         (setq gnus-score-alist (cdr cache))
793       (setq gnus-score-alist nil)
794       (gnus-score-load-score-alist file)
795       (or gnus-score-alist
796           (setq gnus-score-alist (copy-alist '((touched nil)))))
797       (setq gnus-score-cache
798             (cons (cons file gnus-score-alist) gnus-score-cache)))))
799
800 (defun gnus-score-remove-from-cache (file)
801   (setq gnus-score-cache 
802         (delq (assoc file gnus-score-cache) gnus-score-cache)))
803
804 (defun gnus-score-load-score-alist (file)
805   (let (alist)
806     (if (file-readable-p file)
807         (progn
808           (save-excursion
809             (gnus-set-work-buffer)
810             (insert-file-contents file)
811             (goto-char (point-min))
812             ;; Only do the loading if the score file isn't empty.
813             (if (save-excursion (re-search-forward "[()0-9a-zA-Z]" nil t))
814                 (setq alist
815                       (condition-case ()
816                           (read (current-buffer))
817                         (error 
818                          (progn
819                            (gnus-message 3 "Problem with score file %s" file)
820                            (ding) 
821                            (sit-for 2)
822                            nil))))))
823           (if (eq (car alist) 'setq)
824               (setq gnus-score-alist (gnus-score-transform-old-to-new alist))
825             (setq gnus-score-alist alist))
826           (setq gnus-score-alist
827                 (gnus-score-check-syntax gnus-score-alist file)))
828       (setq gnus-score-alist nil))))
829
830 (defun gnus-score-check-syntax (alist file)
831   (cond 
832    ((null alist)
833     nil)
834    ((not (consp alist))
835     (gnus-message 1 "Score file is not a list: %s" file)
836     (ding)
837     nil)
838    (t
839     (let ((a alist)
840           err)
841       (while (and a (not err))
842         (cond ((not (listp (car a)))
843                (gnus-message 3 "Illegal score element %s in %s" (car a) file)
844                (setq err t))
845               ((and (stringp (car (car a)))
846                     (not (listp (nth 1 (car a)))))
847                (gnus-message 3 "Illegal header match %s in %s" (nth 1 (car a)) file)
848                (setq err t))
849               (t
850                (setq a (cdr a)))))
851       (if err
852           (progn
853             (ding)
854             nil)
855         alist)))))    
856
857 (defun gnus-score-transform-old-to-new (alist)
858   (let* ((alist (nth 2 alist))
859          out entry)
860     (if (eq (car alist) 'quote)
861         (setq alist (nth 1 alist)))
862     (while alist
863       (setq entry (car alist))
864       (if (stringp (car entry))
865           (let ((scor (cdr entry)))
866             (setq out (cons entry out))
867             (while scor
868               (setcar scor
869                       (list (car (car scor)) (nth 2 (car scor))
870                             (and (nth 3 (car scor))
871                                  (gnus-day-number (nth 3 (car scor))))
872                             (if (nth 1 (car scor)) 'r 's)))
873               (setq scor (cdr scor))))
874         (setq out (cons (if (not (listp (cdr entry))) 
875                             (list (car entry) (cdr entry))
876                           entry)
877                         out)))
878       (setq alist (cdr alist)))
879     (cons (list 'touched t) (nreverse out))))
880   
881 (defun gnus-score-save ()
882   ;; Save all score information.
883   (let ((cache gnus-score-cache))
884     (save-excursion
885       (setq gnus-score-alist nil)
886       (set-buffer (get-buffer-create "*Score*"))
887       (buffer-disable-undo (current-buffer))
888       (let (entry score file)
889         (while cache
890           (setq entry (car cache)
891                 cache (cdr cache)
892                 file (car entry)
893                 score (cdr entry))
894           (if (or (not (equal (gnus-score-get 'touched score) '(t)))
895                   (gnus-score-get 'read-only score)
896                   (and (file-exists-p file)
897                        (not (file-writable-p file))))
898               ()
899             (setq score (setcdr entry (delq (assq 'touched score) score)))
900             (erase-buffer)
901             (let (emacs-lisp-mode-hook)
902               (if (string-match (concat gnus-adaptive-file-suffix "$") file)
903                   ;; This is an adaptive score file, so we do not run
904                   ;; it through `pp'.  These files can get huge, and
905                   ;; are not meant to be edited by human hands.
906                   (insert (format "%S" score))
907                 ;; This is a normal score file, so we print it very
908                 ;; prettily. 
909                 (pp score (current-buffer))))
910             (if (not (gnus-make-directory (file-name-directory file)))
911                 ()
912               ;; If the score file is empty, we delete it.
913               (if (zerop (buffer-size))
914                   (delete-file file)
915                 ;; There are scores, so we write the file. 
916                 (and (file-writable-p file)
917                      (write-region (point-min) (point-max) 
918                                    file nil 'silent))))
919             (and gnus-score-uncacheable-files
920                  (string-match gnus-score-uncacheable-files file)
921                  (gnus-score-remove-from-cache file)))))
922       (kill-buffer (current-buffer)))))
923   
924 (defun gnus-score-headers (score-files &optional trace)
925   ;; Score `gnus-newsgroup-headers'.
926   (let (scores)
927     ;; PLM: probably this is not the best place to clear orphan-score
928     (setq gnus-orphan-score nil)
929     (setq gnus-scores-articles nil)
930     (setq gnus-scores-exclude-files nil)
931     ;; Load the score files.
932     (while score-files
933       (if (stringp (car score-files))
934           ;; It is a string, which means that it's a score file name,
935           ;; so we load the score file and add the score alist to
936           ;; the list of alists.
937           (setq scores (nconc (gnus-score-load-file (car score-files)) scores))
938         ;; It is an alist, so we just add it to the list directly.
939         (setq scores (nconc (car score-files) scores)))
940       (setq score-files (cdr score-files)))
941     ;; Prune the score files that are to be excluded, if any.
942     (when gnus-scores-exclude-files
943       (let ((s scores)
944             c)
945         (while s
946           (and (setq c (rassq (car s) gnus-score-cache))
947                (member (car c) gnus-scores-exclude-files)
948                (setq scores (delq (car s) scores)))
949           (setq s (cdr s)))))
950     ;; Do the scoring.
951     (when (and gnus-summary-default-score
952                scores
953                (> (length gnus-newsgroup-headers)
954                   (length gnus-newsgroup-scored)))
955       (let* ((entries gnus-header-index)
956              (now (gnus-day-number (current-time-string)))
957              (expire (- now gnus-score-expiry-days))
958              (headers gnus-newsgroup-headers)
959              (current-score-file gnus-current-score-file)
960              entry header)
961         (gnus-message 5 "Scoring...")
962         ;; Create articles, an alist of the form `(HEADER . SCORE)'.
963         (while headers
964           (setq header (car headers)
965                 headers (cdr headers))
966           ;; WARNING: The assq makes the function O(N*S) while it could
967           ;; be written as O(N+S), where N is (length gnus-newsgroup-headers)
968           ;; and S is (length gnus-newsgroup-scored).
969           (or (assq (mail-header-number header) gnus-newsgroup-scored)
970               (setq gnus-scores-articles ;Total of 2 * N cons-cells used.
971                     (cons (cons header (or gnus-summary-default-score 0))
972                           gnus-scores-articles))))
973
974         (save-excursion
975           (set-buffer (get-buffer-create "*Headers*"))
976           (buffer-disable-undo (current-buffer))
977
978           ;; Set the global variant of this variable.
979           (setq gnus-current-score-file current-score-file)
980           ;; score orphans
981           (if gnus-orphan-score 
982               (progn
983                 (setq gnus-score-index 
984                       (nth 1 (assoc "references" gnus-header-index)))
985                 (gnus-score-orphans gnus-orphan-score)))
986           ;; Run each header through the score process.
987           (while entries
988             (setq entry (car entries)
989                   header (downcase (nth 0 entry))
990                   entries (cdr entries))
991             (setq gnus-score-index (nth 1 (assoc header gnus-header-index)))
992             (if (< 0 (apply 'max (mapcar
993                                   (lambda (score)
994                                     (length (gnus-score-get header score)))
995                                   scores)))
996                 ;; Call the scoring function for this type of "header".
997                 (funcall (nth 2 entry) scores header now expire trace)))
998           ;; Remove the buffer.
999           (kill-buffer (current-buffer)))
1000
1001         ;; Add articles to `gnus-newsgroup-scored'.
1002         (while gnus-scores-articles
1003           (or (= gnus-summary-default-score (cdr (car gnus-scores-articles)))
1004               (setq gnus-newsgroup-scored
1005                     (cons (cons (mail-header-number 
1006                                  (car (car gnus-scores-articles)))
1007                                 (cdr (car gnus-scores-articles)))
1008                           gnus-newsgroup-scored)))
1009           (setq gnus-scores-articles (cdr gnus-scores-articles)))
1010
1011         (gnus-message 5 "Scoring...done")))))
1012
1013
1014 (defun gnus-get-new-thread-ids (articles)
1015   (let ((index (nth 1 (assoc "message-id" gnus-header-index)))
1016         (refind gnus-score-index)
1017         id-list art this tref)
1018     (while articles
1019       (setq art (car articles)
1020             this (aref (car art) index)
1021             tref (aref (car art) refind)
1022             articles (cdr articles))
1023       (if (string-equal tref "")        ;no references line
1024           (setq id-list (cons this id-list))))
1025     id-list))
1026
1027 ;; Orphan functions written by plm@atcmp.nl (Peter Mutsaers).
1028 (defun gnus-score-orphans (score)
1029   (let ((new-thread-ids (gnus-get-new-thread-ids gnus-scores-articles))
1030         alike articles art arts this last this-id)
1031     
1032     (setq gnus-scores-articles (sort gnus-scores-articles 'gnus-score-string<)
1033           articles gnus-scores-articles)
1034
1035     ;;more or less the same as in gnus-score-string
1036     (erase-buffer)
1037     (while articles
1038       (setq art (car articles)
1039             this (aref (car art) gnus-score-index)
1040             articles (cdr articles))
1041       ;;completely skip if this is empty (not a child, so not an orphan)
1042       (if (not (string= this ""))
1043           (if (equal last this)
1044               ;; O(N*H) cons-cells used here, where H is the number of
1045               ;; headers.
1046               (setq alike (cons art alike))
1047             (if last
1048                 (progn
1049                   ;; Insert the line, with a text property on the
1050                   ;; terminating newline refering to the articles with
1051                   ;; this line.
1052                   (insert last ?\n)
1053                   (put-text-property (1- (point)) (point) 'articles alike)))
1054             (setq alike (list art)
1055                   last this))))
1056     (and last                           ; Bwadr, duplicate code.
1057          (progn
1058            (insert last ?\n)                    
1059            (put-text-property (1- (point)) (point) 'articles alike)))
1060
1061     ;; PLM: now delete those lines that contain an entry from new-thread-ids
1062     (while new-thread-ids
1063       (setq this-id (car new-thread-ids)
1064             new-thread-ids (cdr new-thread-ids))
1065       (goto-char (point-min))
1066       (while (search-forward this-id nil t)
1067         ;; found a match. remove this line
1068         (beginning-of-line)
1069         (kill-line 1)))
1070
1071     ;; now for each line: update its articles with score by moving to
1072     ;; every end-of-line in the buffer and read the articles property
1073     (goto-char (point-min))
1074     (while (eq 0 (progn
1075                    (end-of-line)
1076                    (setq arts (get-text-property (point) 'articles))
1077                    (while arts
1078                      (setq art (car arts)
1079                            arts (cdr arts))
1080                      (setcdr art (+ score (cdr art))))
1081                    (forward-line))))))
1082              
1083
1084 (defun gnus-score-integer (scores header now expire &optional trace)
1085   (let ((gnus-score-index (nth 1 (assoc header gnus-header-index)))
1086         entries alist)
1087
1088     ;; Find matches.
1089     (while scores
1090       (setq alist (car scores)
1091             scores (cdr scores)
1092             entries (assoc header alist))
1093       (while (cdr entries)              ;First entry is the header index.
1094         (let* ((rest (cdr entries))             
1095                (kill (car rest))
1096                (match (nth 0 kill))
1097                (type (or (nth 3 kill) '>))
1098                (score (or (nth 1 kill) gnus-score-interactive-default-score))
1099                (date (nth 2 kill))
1100                (found nil)
1101                (match-func (if (or (eq type '>) (eq type '<) (eq type '<=)
1102                                    (eq type '>=) (eq type '=))
1103                                type
1104                              (error "Illegal match type: %s" type)))
1105                (articles gnus-scores-articles))
1106           ;; Instead of doing all the clever stuff that
1107           ;; `gnus-score-string' does to minimize searches and stuff,
1108           ;; I will assume that people generally will put so few
1109           ;; matches on numbers that any cleverness will take more
1110           ;; time than one would gain.
1111           (while articles
1112             (and (funcall match-func 
1113                           (or (aref (car (car articles)) gnus-score-index) 0)
1114                           match)
1115                  (progn
1116                    (and trace (setq gnus-score-trace 
1117                                     (cons
1118                                      (cons
1119                                       (car-safe (rassq alist gnus-score-cache))
1120                                       kill)
1121                                      gnus-score-trace)))
1122                    (setq found t)
1123                    (setcdr (car articles) (+ score (cdr (car articles))))))
1124             (setq articles (cdr articles)))
1125           ;; Update expire date
1126           (cond ((null date))           ;Permanent entry.
1127                 (found                  ;Match, update date.
1128                  (gnus-score-set 'touched '(t) alist)
1129                  (setcar (nthcdr 2 kill) now))
1130                 ((< date expire)        ;Old entry, remove.
1131                  (gnus-score-set 'touched '(t) alist)
1132                  (setcdr entries (cdr rest))
1133                  (setq rest entries)))
1134           (setq entries rest))))))
1135
1136 (defun gnus-score-date (scores header now expire &optional trace)
1137   (let ((gnus-score-index (nth 1 (assoc header gnus-header-index)))
1138         entries alist)
1139
1140     ;; Find matches.
1141     (while scores
1142       (setq alist (car scores)
1143             scores (cdr scores)
1144             entries (assoc header alist))
1145       (while (cdr entries)              ;First entry is the header index.
1146         (let* ((rest (cdr entries))             
1147                (kill (car rest))
1148                (match (timezone-make-date-sortable (nth 0 kill)))
1149                (type (or (nth 3 kill) 'before))
1150                (score (or (nth 1 kill) gnus-score-interactive-default-score))
1151                (date (nth 2 kill))
1152                (found nil)
1153                (match-func 
1154                 (cond ((eq type 'after) 'string<)
1155                       ((eq type 'before) 'gnus-string>)
1156                       ((eq type 'at) 'string=)
1157                       (t (error "Illegal match type: %s" type))))
1158                (articles gnus-scores-articles)
1159                l)
1160           ;; Instead of doing all the clever stuff that
1161           ;; `gnus-score-string' does to minimize searches and stuff,
1162           ;; I will assume that people generally will put so few
1163           ;; matches on numbers that any cleverness will take more
1164           ;; time than one would gain.
1165           (while articles
1166             (and
1167              (setq l (aref (car (car articles)) gnus-score-index))
1168              (funcall match-func match (timezone-make-date-sortable l))
1169              (progn
1170                (and trace (setq gnus-score-trace 
1171                                 (cons
1172                                  (cons
1173                                   (car-safe (rassq alist gnus-score-cache))
1174                                   kill)
1175                                  gnus-score-trace)))
1176                (setq found t)
1177                (setcdr (car articles) (+ score (cdr (car articles))))))
1178             (setq articles (cdr articles)))
1179           ;; Update expire date
1180           (cond ((null date))           ;Permanent entry.
1181                 (found                  ;Match, update date.
1182                  (gnus-score-set 'touched '(t) alist)
1183                  (setcar (nthcdr 2 kill) now))
1184                 ((< date expire)        ;Old entry, remove.
1185                  (gnus-score-set 'touched '(t) alist)
1186                  (setcdr entries (cdr rest))
1187                  (setq rest entries)))
1188           (setq entries rest))))))
1189
1190 (defun gnus-score-body (scores header now expire &optional trace)
1191   (save-excursion
1192     (set-buffer nntp-server-buffer)
1193     (save-restriction
1194       (let* ((buffer-read-only nil)
1195              (articles gnus-scores-articles)
1196              (last (mail-header-number (car (car gnus-scores-articles))))
1197              (all-scores scores)
1198              (request-func (cond ((string= "head" (downcase header))
1199                                   'gnus-request-head)
1200                                  ((string= "body" (downcase header))
1201                                   'gnus-request-body)
1202                                  (t 'gnus-request-article)))
1203              entries alist ofunc article)
1204         ;; Not all backends support partial fetching.  In that case,
1205         ;; we just fetch the entire article.
1206         (or (gnus-check-backend-function 
1207              (and (string-match "^gnus-" (symbol-name request-func))
1208                   (intern (substring (symbol-name request-func)
1209                                      (match-end 0))))
1210              gnus-newsgroup-name)
1211             (progn
1212               (setq ofunc request-func)
1213               (setq request-func 'gnus-request-article)))
1214         (while articles
1215           (setq article (mail-header-number (car (car articles))))
1216           (gnus-message 7 "Scoring on article %s of %s..." article last)
1217           (if (not (funcall request-func article gnus-newsgroup-name))
1218               ()
1219             (widen)
1220             (goto-char (point-min))
1221             ;; If just parts of the article is to be searched, but the
1222             ;; backend didn't support partial fetching, we just narrow
1223             ;; to the relevant parts.
1224             (if ofunc
1225                 (if (eq ofunc 'gnus-request-head)
1226                     (narrow-to-region
1227                      (point)
1228                      (or (search-forward "\n\n" nil t) (point-max)))
1229                   (narrow-to-region
1230                    (or (search-forward "\n\n" nil t) (point))
1231                    (point-max))))
1232             (setq scores all-scores)
1233             ;; Find matches.
1234             (while scores
1235               (setq alist (car scores)
1236                     scores (cdr scores)
1237                     entries (assoc header alist))
1238               (while (cdr entries)      ;First entry is the header index.
1239                 (let* ((rest (cdr entries))             
1240                        (kill (car rest))
1241                        (match (nth 0 kill))
1242                        (type (or (nth 3 kill) 's))
1243                        (score (or (nth 1 kill) 
1244                                   gnus-score-interactive-default-score))
1245                        (date (nth 2 kill))
1246                        (found nil)
1247                        (case-fold-search 
1248                         (not (or (eq type 'R) (eq type 'S)
1249                                  (eq type 'Regexp) (eq type 'String))))
1250                        (search-func 
1251                         (cond ((or (eq type 'r) (eq type 'R)
1252                                    (eq type 'regexp) (eq type 'Regexp))
1253                                're-search-forward)
1254                               ((or (eq type 's) (eq type 'S)
1255                                    (eq type 'string) (eq type 'String))
1256                                'search-forward)
1257                               (t
1258                                (error "Illegal match type: %s" type)))))
1259                   (goto-char (point-min))
1260                   (if (funcall search-func match nil t)
1261                       ;; Found a match, update scores.
1262                       (progn
1263                         (setcdr (car articles) (+ score (cdr (car articles))))
1264                         (setq found t)
1265                         (and trace (setq gnus-score-trace 
1266                                          (cons
1267                                           (cons
1268                                            (car-safe
1269                                             (rassq alist gnus-score-cache))
1270                                            kill)
1271                                           gnus-score-trace)))))
1272                   ;; Update expire date
1273                   (cond ((null date))   ;Permanent entry.
1274                         (found          ;Match, update date.
1275                          (gnus-score-set 'touched '(t) alist)
1276                          (setcar (nthcdr 2 kill) now))
1277                         ((< date expire) ;Old entry, remove.
1278                          (gnus-score-set 'touched '(t) alist)
1279                          (setcdr entries (cdr rest))
1280                          (setq rest entries)))
1281                   (setq entries rest)))))
1282           (setq articles (cdr articles)))))))
1283
1284 (defun gnus-score-followup (scores header now expire &optional trace)
1285   ;; Insert the unique article headers in the buffer.
1286   (let ((gnus-score-index (nth 1 (assoc header gnus-header-index)))
1287         (current-score-file gnus-current-score-file)
1288         (all-scores scores)
1289         ;; gnus-score-index is used as a free variable.
1290         alike last this art entries alist articles)
1291
1292     ;; Change score file to the adaptive score file.  All entries that
1293     ;; this function makes will be put into this file.
1294     (gnus-score-load-file (gnus-score-file-name 
1295                            gnus-newsgroup-name gnus-adaptive-file-suffix))
1296
1297     (setq gnus-scores-articles (sort gnus-scores-articles 'gnus-score-string<)
1298           articles gnus-scores-articles)
1299
1300     (erase-buffer)
1301     (while articles
1302       (setq art (car articles)
1303             this (aref (car art) gnus-score-index)
1304             articles (cdr articles))
1305       (if (equal last this)
1306           (setq alike (cons art alike))
1307         (if last
1308             (progn
1309               (insert last ?\n)
1310               (put-text-property (1- (point)) (point) 'articles alike)))
1311         (setq alike (list art)
1312               last this)))
1313     (and last                           ; Bwadr, duplicate code.
1314          (progn
1315            (insert last ?\n)                    
1316            (put-text-property (1- (point)) (point) 'articles alike)))
1317   
1318     ;; Find matches.
1319     (while scores
1320       (setq alist (car scores)
1321             scores (cdr scores)
1322             entries (assoc header alist))
1323       (while (cdr entries)              ;First entry is the header index.
1324         (let* ((rest (cdr entries))             
1325                (kill (car rest))
1326                (match (nth 0 kill))
1327                (type (or (nth 3 kill) 's))
1328                (score (or (nth 1 kill) gnus-score-interactive-default-score))
1329                (date (nth 2 kill))
1330                (found nil)
1331                (mt (aref (symbol-name type) 0))
1332                (case-fold-search 
1333                 (not (or (= mt ?R) (= mt ?S) (= mt ?E) (= mt ?F))))
1334                (dmt (downcase mt))
1335                (search-func 
1336                 (cond ((= dmt ?r) 're-search-forward)
1337                       ((or (= dmt ?e) (= dmt ?s) (= dmt ?f)) 'search-forward)
1338                       (t (error "Illegal match type: %s" type))))
1339                arts art)
1340           (goto-char (point-min))
1341           (if (= dmt ?e)
1342               (while (funcall search-func match nil t)
1343                 (and (= (progn (beginning-of-line) (point))
1344                         (match-beginning 0))
1345                      (= (progn (end-of-line) (point))
1346                         (match-end 0))
1347                      (progn
1348                        (setq found (setq arts (get-text-property 
1349                                                (point) 'articles)))
1350                        ;; Found a match, update scores.
1351                        (while arts
1352                          (setq art (car arts)
1353                                arts (cdr arts))
1354                          (gnus-score-add-followups 
1355                           (car art) score all-scores)))))
1356             (while (funcall search-func match nil t)
1357               (end-of-line)
1358               (setq found (setq arts (get-text-property (point) 'articles)))
1359               ;; Found a match, update scores.
1360               (while arts
1361                 (setq art (car arts)
1362                       arts (cdr arts))
1363                 (gnus-score-add-followups (car art) score all-scores))))
1364           ;; Update expire date
1365           (cond ((null date))           ;Permanent entry.
1366                 (found                  ;Match, update date.
1367                  (gnus-score-set 'touched '(t) alist)
1368                  (setcar (nthcdr 2 kill) now))
1369                 ((< date expire)        ;Old entry, remove.
1370                  (gnus-score-set 'touched '(t) alist)
1371                  (setcdr entries (cdr rest))
1372                  (setq rest entries)))
1373           (setq entries rest))))
1374     ;; We change the score file back to the previous one.
1375     (gnus-score-load-file current-score-file)))
1376
1377 (defun gnus-score-add-followups (header score scores)
1378   (save-excursion
1379     (set-buffer gnus-summary-buffer)
1380     (let* ((id (mail-header-id header))
1381            (scores (car scores))
1382            entry dont)
1383       ;; Don't enter a score if there already is one.
1384       (while scores
1385         (setq entry (car scores))
1386         (and (equal "references" (car entry))
1387              (or (null (nth 3 (car (cdr entry))))
1388                  (eq 's (nth 3 (car (cdr entry)))))
1389              (progn
1390                (if (assoc id entry)
1391                    (setq dont t))))
1392         (setq scores (cdr scores)))
1393       (or dont
1394           (gnus-summary-score-entry 
1395            "references" id 's score (current-time-string) nil t)))))
1396
1397
1398 (defun gnus-score-string (score-list header now expire &optional trace)
1399   ;; Score ARTICLES according to HEADER in SCORE-LIST.
1400   ;; Update matches entries to NOW and remove unmatched entried older
1401   ;; than EXPIRE.
1402   
1403   ;; Insert the unique article headers in the buffer.
1404   (let ((gnus-score-index (nth 1 (assoc header gnus-header-index)))
1405         ;; gnus-score-index is used as a free variable.
1406         alike last this art entries alist articles scores fuzzy)
1407
1408     ;; Sorting the articles costs os O(N*log N) but will allow us to
1409     ;; only match with each unique header.  Thus the actual matching
1410     ;; will be O(M*U) where M is the number of strings to match with,
1411     ;; and U is the number of unique headers.  It is assumed (but
1412     ;; untested) this will be a net win because of the large constant
1413     ;; factor involved with string matching.
1414     (setq gnus-scores-articles (sort gnus-scores-articles 'gnus-score-string<)
1415           articles gnus-scores-articles)
1416
1417     (erase-buffer)
1418     (while articles
1419       (setq art (car articles)
1420             this (aref (car art) gnus-score-index)
1421             articles (cdr articles))
1422       (if (equal last this)
1423           ;; O(N*H) cons-cells used here, where H is the number of
1424           ;; headers.
1425           (setq alike (cons art alike))
1426         (if last
1427             (progn
1428               ;; Insert the line, with a text property on the
1429               ;; terminating newline refering to the articles with
1430               ;; this line.
1431               (insert last ?\n)
1432               (put-text-property (1- (point)) (point) 'articles alike)))
1433         (setq alike (list art)
1434               last this)))
1435     (and last                           ; Bwadr, duplicate code.
1436          (progn
1437            (insert last ?\n)                    
1438            (put-text-property (1- (point)) (point) 'articles alike)))
1439
1440     ;; Find ordinary matches.
1441     (setq scores score-list) 
1442     (while scores
1443       (setq alist (car scores)
1444             scores (cdr scores)
1445             entries (assoc header alist))
1446       (while (cdr entries)              ;First entry is the header index.
1447         (let* ((rest (cdr entries))             
1448                (kill (car rest))
1449                (match (nth 0 kill))
1450                (type (or (nth 3 kill) 's))
1451                (score (or (nth 1 kill) gnus-score-interactive-default-score))
1452                (date (nth 2 kill))
1453                (found nil)
1454                (mt (aref (symbol-name type) 0))
1455                (case-fold-search 
1456                 (not (or (= mt ?R) (= mt ?S) (= mt ?E) (= mt ?F))))
1457                (dmt (downcase mt))
1458                (search-func 
1459                 (cond ((= dmt ?r) 're-search-forward)
1460                       ((or (= dmt ?e) (= dmt ?s) (= dmt ?f)) 'search-forward)
1461                       (t (error "Illegal match type: %s" type))))
1462                arts art)
1463           (if (= dmt ?f)
1464               (setq fuzzy t)
1465             ;; Do non-fuzzy matching.
1466             (goto-char (point-min))
1467             (if (= dmt ?e)
1468                 ;; Do exact matching.
1469                 (while (and (not (eobp)) 
1470                             (funcall search-func match nil t))
1471                   (and (= (progn (beginning-of-line) (point))
1472                           (match-beginning 0))
1473                        (= (progn (end-of-line) (point))
1474                           (match-end 0))
1475                        (progn
1476                          (setq found (setq arts (get-text-property 
1477                                                  (point) 'articles)))
1478                          ;; Found a match, update scores.
1479                          (if trace
1480                              (while arts
1481                                (setq art (car arts)
1482                                      arts (cdr arts))
1483                                (setcdr art (+ score (cdr art)))
1484                                (setq gnus-score-trace
1485                                      (cons
1486                                       (cons
1487                                        (car-safe
1488                                         (rassq alist gnus-score-cache))
1489                                        kill)
1490                                       gnus-score-trace)))
1491                            (while arts
1492                              (setq art (car arts)
1493                                    arts (cdr arts))
1494                              (setcdr art (+ score (cdr art)))))))
1495                   (forward-line 1))
1496               ;; Do regexp and substring matching.
1497               (and (string= match "") (setq match "\n"))
1498               (while (and (not (eobp))
1499                           (funcall search-func match nil t))
1500                 (goto-char (match-beginning 0))
1501                 (end-of-line)
1502                 (setq found (setq arts (get-text-property (point) 'articles)))
1503                 ;; Found a match, update scores.
1504                 (if trace
1505                     (while arts
1506                       (setq art (pop arts))
1507                       (setcdr art (+ score (cdr art)))
1508                       (push (cons
1509                               (car-safe (rassq alist gnus-score-cache))
1510                               kill)
1511                             gnus-score-trace))
1512                   (while arts
1513                     (setq art (pop arts))
1514                     (setcdr art (+ score (cdr art)))))
1515                 (forward-line 1)))
1516             ;; Update expire date
1517             (cond ((null date))         ;Permanent entry.
1518                   (found                ;Match, update date.
1519                    (gnus-score-set 'touched '(t) alist)
1520                    (setcar (nthcdr 2 kill) now))
1521                   ((< date expire)      ;Old entry, remove.
1522                    (gnus-score-set 'touched '(t) alist)
1523                    (setcdr entries (cdr rest))
1524                    (setq rest entries))))
1525           (setq entries rest))))
1526
1527     ;; Find fuzzy matches.
1528     (when fuzzy
1529       (setq scores score-list)
1530       (gnus-simplify-buffer-fuzzy)
1531       (while scores
1532         (setq alist (car scores)
1533               scores (cdr scores)
1534               entries (assoc header alist))
1535         (while (cdr entries)            ;First entry is the header index.
1536           (let* ((rest (cdr entries))           
1537                  (kill (car rest))
1538                  (match (nth 0 kill))
1539                  (type (or (nth 3 kill) 's))
1540                  (score (or (nth 1 kill) gnus-score-interactive-default-score))
1541                  (date (nth 2 kill))
1542                  (found nil)
1543                  (mt (aref (symbol-name type) 0))
1544                  (case-fold-search (not (= mt ?F)))
1545                  (dmt (downcase mt))
1546                  arts art)
1547             (when (= dmt ?f)
1548               (goto-char (point-min))
1549               (while (and (not (eobp)) 
1550                           (search-forward match nil t))
1551                 (when (and (= (progn (beginning-of-line) (point))
1552                               (match-beginning 0))
1553                            (= (progn (end-of-line) (point))
1554                               (match-end 0)))
1555                   (setq found (setq arts (get-text-property 
1556                                           (point) 'articles)))
1557                   ;; Found a match, update scores.
1558                   (if trace
1559                       (while arts
1560                         (setq art (pop arts))
1561                         (setcdr art (+ score (cdr art)))
1562                         (push (cons
1563                                (car-safe (rassq alist gnus-score-cache))
1564                                kill)
1565                               gnus-score-trace))
1566                     (while arts
1567                       (setq art (pop arts))
1568                       (setcdr art (+ score (cdr art))))))
1569                 (forward-line 1))
1570               ;; Update expire date
1571               (unless trace
1572                 (cond ((null date))             ;Permanent entry.
1573                       (found            ;Match, update date.
1574                        (gnus-score-set 'touched '(t) alist)
1575                        (setcar (nthcdr 2 kill) now))
1576                       ((< date expire)  ;Old entry, remove.
1577                        (gnus-score-set 'touched '(t) alist)
1578                        (setcdr entries (cdr rest))
1579                        (setq rest entries)))))
1580             (setq entries rest)))))))
1581
1582 (defun gnus-score-string< (a1 a2)
1583   ;; Compare headers in articles A2 and A2.
1584   ;; The header index used is the free variable `gnus-score-index'.
1585   (string-lessp (aref (car a1) gnus-score-index)
1586                 (aref (car a2) gnus-score-index)))
1587
1588 (defun gnus-score-build-cons (article)
1589   ;; Build a `gnus-newsgroup-scored' type cons from ARTICLE.
1590   (cons (mail-header-number (car article)) (cdr article)))
1591
1592 (defconst gnus-header-index
1593   ;; Name to index alist.
1594   '(("number" 0 gnus-score-integer)
1595     ("subject" 1 gnus-score-string)
1596     ("from" 2 gnus-score-string)
1597     ("date" 3 gnus-score-date)
1598     ("message-id" 4 gnus-score-string) 
1599     ("references" 5 gnus-score-string) 
1600     ("chars" 6 gnus-score-integer) 
1601     ("lines" 7 gnus-score-integer) 
1602     ("xref" 8 gnus-score-string)
1603     ("head" -1 gnus-score-body)
1604     ("body" -1 gnus-score-body)
1605     ("all" -1 gnus-score-body)
1606     ("followup" 2 gnus-score-followup)))
1607
1608 (defun gnus-current-score-file-nondirectory (&optional score-file)
1609   (let ((score-file (or score-file gnus-current-score-file)))
1610     (if score-file 
1611         (gnus-short-group-name (file-name-nondirectory score-file))
1612       "none")))
1613
1614 (defun gnus-score-adaptive ()
1615   (save-excursion
1616     (let* ((malist (gnus-copy-sequence gnus-adaptive-score-alist))
1617            (alist malist)
1618            (date (current-time-string)) 
1619            (data gnus-newsgroup-data)
1620            elem headers match)
1621       ;; First we transform the adaptive rule alist into something
1622       ;; that's faster to process.
1623       (while malist
1624         (setq elem (car malist))
1625         (if (symbolp (car elem))
1626             (setcar elem (symbol-value (car elem))))
1627         (setq elem (cdr elem))
1628         (while elem
1629           (setcdr (car elem) 
1630                   (cons (if (eq (car (car elem)) 'followup)
1631                             "references"
1632                           (symbol-name (car (car elem))))
1633                         (cdr (car elem))))
1634           (setcar (car elem) 
1635                   (intern 
1636                    (concat "gnus-header-" 
1637                            (if (eq (car (car elem)) 'followup)
1638                                "message-id"
1639                              (downcase (symbol-name (car (car elem))))))))
1640           (setq elem (cdr elem)))
1641         (setq malist (cdr malist)))
1642       ;; We change the score file to the adaptive score file.
1643       (gnus-score-load-file 
1644        (or gnus-newsgroup-adaptive-score-file
1645            (gnus-score-file-name 
1646             gnus-newsgroup-name gnus-adaptive-file-suffix)))
1647       ;; The we score away.
1648       (while data
1649         (setq elem (cdr (assq (gnus-data-mark (car data)) alist)))
1650         (if (or (not elem)
1651                 (gnus-data-pseudo-p (car data)))
1652             ()
1653           (when (setq headers (gnus-data-header (car data)))
1654             (while elem 
1655               (setq match (funcall (car (car elem)) headers))
1656               (gnus-summary-score-entry 
1657                (nth 1 (car elem)) match
1658                (cond
1659                 ((numberp match)
1660                  '=)
1661                 ((equal (nth 1 (car elem)) "date")
1662                  'a)
1663                 (t
1664                  ;; Whether we use substring or exact matches are controlled
1665                  ;; here.  
1666                  (if (or (not gnus-score-exact-adapt-limit)
1667                          (< (length match) gnus-score-exact-adapt-limit))
1668                      'e 
1669                    (if (equal (nth 1 (car elem)) "subject")
1670                        'f 's))))
1671                (nth 2 (car elem)) date nil t)
1672               (setq elem (cdr elem)))))
1673         (setq data (cdr data))))))
1674
1675 ;;;
1676 ;;; Score mode.
1677 ;;;
1678
1679 (defvar gnus-score-mode-map nil)
1680 (defvar gnus-score-mode-hook nil)
1681
1682 (if gnus-score-mode-map
1683     ()
1684   (setq gnus-score-mode-map (copy-keymap emacs-lisp-mode-map))
1685   (define-key gnus-score-mode-map "\C-c\C-c" 'gnus-score-edit-done)
1686   (define-key gnus-score-mode-map "\C-c\C-d" 'gnus-score-edit-insert-date))
1687
1688 (defun gnus-score-mode ()
1689   "Mode for editing score files.
1690 This mode is an extended emacs-lisp mode.
1691
1692 \\{gnus-score-mode-map}"
1693   (interactive)
1694   (kill-all-local-variables)
1695   (use-local-map gnus-score-mode-map)
1696   (set-syntax-table emacs-lisp-mode-syntax-table)
1697   (setq major-mode 'gnus-score-mode)
1698   (setq mode-name "Score")
1699   (lisp-mode-variables nil)
1700   (run-hooks 'emacs-lisp-mode-hook 'gnus-score-mode-hook))
1701
1702 (defun gnus-score-edit-insert-date ()
1703   "Insert date in numerical format."
1704   (interactive)
1705   (insert (int-to-string (gnus-day-number (current-time-string)))))
1706
1707 (defun gnus-score-edit-done ()
1708   "Save the score file and return to the summary buffer."
1709   (interactive)
1710   (let ((bufnam (buffer-file-name (current-buffer)))
1711         (winconf gnus-prev-winconf))
1712     (gnus-make-directory (file-name-directory (buffer-file-name)))
1713     (save-buffer)
1714     (kill-buffer (current-buffer))
1715     (gnus-score-remove-from-cache bufnam)
1716     (gnus-score-load-file bufnam)
1717     (and winconf (set-window-configuration winconf))))
1718
1719 (defun gnus-score-find-trace ()
1720   "Find all score rules that applies to the current article."
1721   (interactive)
1722   (let ((gnus-newsgroup-headers
1723          (list (gnus-summary-article-header)))
1724         (gnus-newsgroup-scored nil)
1725         (buf (current-buffer))
1726         trace)
1727     (setq gnus-score-trace nil)
1728     (gnus-possibly-score-headers 'trace)
1729     (or (setq trace gnus-score-trace)
1730         (error "No score rules apply to the current article."))
1731     (pop-to-buffer "*Gnus Scores*")
1732     (gnus-add-current-to-buffer-list)
1733     (erase-buffer)
1734     (while trace
1735       (insert (format "%S  ->  %s\n"  (cdr (car trace))
1736                       (file-name-nondirectory (car (car trace)))))
1737       (setq trace (cdr trace)))
1738     (goto-char (point-min))
1739     (pop-to-buffer buf)))
1740
1741 (defun gnus-summary-rescore ()
1742   "Redo the entire scoring process in the current summary."
1743   (interactive)
1744   (setq gnus-newsgroup-scored nil)
1745   (setq gnus-score-cache nil)
1746   (setq gnus-newsgroup-scored nil)
1747   (gnus-possibly-score-headers)
1748   (gnus-score-update-lines))
1749   
1750 (defun gnus-score-flush-cache ()
1751   "Flush the cache of score files."
1752   (interactive)
1753   (setq gnus-score-cache nil)
1754   (gnus-message 6 "The score cache is now flushed"))
1755
1756 (defun gnus-score-close ()
1757   "Clear all internal score variables."
1758   (setq gnus-score-cache nil
1759         gnus-internal-global-score-files nil))
1760
1761 ;; Summary score marking commands.
1762
1763 (defun gnus-summary-raise-same-subject-and-select (score)
1764   "Raise articles which has the same subject with SCORE and select the next."
1765   (interactive "p")
1766   (let ((subject (gnus-summary-article-subject)))
1767     (gnus-summary-raise-score score)
1768     (while (gnus-summary-find-subject subject)
1769       (gnus-summary-raise-score score))
1770     (gnus-summary-next-article t)))
1771
1772 (defun gnus-summary-raise-same-subject (score)
1773   "Raise articles which has the same subject with SCORE."
1774   (interactive "p")
1775   (let ((subject (gnus-summary-article-subject)))
1776     (gnus-summary-raise-score score)
1777     (while (gnus-summary-find-subject subject)
1778       (gnus-summary-raise-score score))
1779     (gnus-summary-next-subject 1 t)))
1780
1781 (defun gnus-score-default (level)
1782   (if level (prefix-numeric-value level) 
1783     gnus-score-interactive-default-score))
1784
1785 (defun gnus-summary-raise-thread (&optional score)
1786   "Raise the score of the articles in the current thread with SCORE."
1787   (interactive "P")
1788   (setq score (gnus-score-default score))
1789   (let (e)
1790     (save-excursion
1791       (let ((articles (gnus-summary-articles-in-thread)))
1792         (while articles
1793           (gnus-summary-goto-subject (car articles))
1794           (gnus-summary-raise-score score)
1795           (setq articles (cdr articles))))
1796       (setq e (point)))
1797     (let ((gnus-summary-check-current t))
1798       (or (zerop (gnus-summary-next-subject 1 t))
1799           (goto-char e))))
1800   (gnus-summary-recenter)
1801   (gnus-summary-position-point)
1802   (gnus-set-mode-line 'summary))
1803
1804 (defun gnus-summary-lower-same-subject-and-select (score)
1805   "Raise articles which has the same subject with SCORE and select the next."
1806   (interactive "p")
1807   (gnus-summary-raise-same-subject-and-select (- score)))
1808
1809 (defun gnus-summary-lower-same-subject (score)
1810   "Raise articles which has the same subject with SCORE."
1811   (interactive "p")
1812   (gnus-summary-raise-same-subject (- score)))
1813
1814 (defun gnus-summary-lower-thread (&optional score)
1815   "Lower score of articles in the current thread with SCORE."
1816   (interactive "P")
1817   (gnus-summary-raise-thread (- (1- (gnus-score-default score)))))
1818
1819 ;;; Finding score files. 
1820
1821 (defun gnus-score-score-files (group)
1822   "Return a list of all possible score files."
1823   ;; Search and set any global score files.
1824   (and gnus-global-score-files 
1825        (or gnus-internal-global-score-files
1826            (gnus-score-search-global-directories gnus-global-score-files)))
1827   ;; Fix the kill-file dir variable.
1828   (setq gnus-kill-files-directory 
1829         (file-name-as-directory
1830          (or gnus-kill-files-directory "~/News/")))
1831   ;; If we can't read it, there are no score files.
1832   (if (not (file-exists-p (expand-file-name gnus-kill-files-directory)))
1833       (setq gnus-score-file-list nil)
1834     (if (not (gnus-use-long-file-name 'not-score))
1835         ;; We do not use long file names, so we have to do some
1836         ;; directory traversing.  
1837         (setq gnus-score-file-list 
1838               (cons nil (gnus-score-score-files-1 gnus-kill-files-directory)))
1839       ;; We want long file names.
1840       (when (or (not gnus-score-file-list)
1841                 (not (car gnus-score-file-list))
1842                 (gnus-file-newer-than gnus-kill-files-directory
1843                                       (car gnus-score-file-list)))
1844         (setq gnus-score-file-list 
1845               (cons (nth 5 (file-attributes gnus-kill-files-directory))
1846                     (nreverse 
1847                      (directory-files 
1848                       gnus-kill-files-directory t 
1849                       (gnus-score-file-regexp)))))))
1850     (cdr gnus-score-file-list)))
1851
1852 (defun gnus-score-score-files-1 (dir)
1853   "Return all possible score files under DIR."
1854   (let ((files (directory-files (expand-file-name dir) t nil t))
1855         (regexp (gnus-score-file-regexp))
1856         out file)
1857     (while files
1858       (setq file (pop files))
1859       (cond 
1860        ((string-match "/\\.\\.?\\'" file)
1861         nil)
1862        ((file-directory-p file)
1863         (setq out (nconc (gnus-score-score-files-1 file) out)))
1864        ((string-match regexp file)
1865         (push file out))))
1866     out))
1867        
1868 (defun gnus-score-file-regexp ()
1869   "Return a regexp that match all score files."
1870   (concat "\\(" gnus-score-file-suffix 
1871           "\\|" gnus-adaptive-file-suffix "\\)\\'"))
1872         
1873 (defun gnus-score-find-bnews (group)
1874   "Return a list of score files for GROUP.
1875 The score files are those files in the ~/News/ directory which matches
1876 GROUP using BNews sys file syntax."
1877   (let* ((sfiles (append (gnus-score-score-files group)
1878                          gnus-internal-global-score-files))
1879          (kill-dir (file-name-as-directory 
1880                     (expand-file-name gnus-kill-files-directory)))
1881          (klen (length kill-dir))
1882          (score-regexp (gnus-score-file-regexp))
1883          (trans (cdr (memq ?: gnus-file-name-translation-table)))
1884          ofiles not-match regexp)
1885     (save-excursion
1886       (set-buffer (get-buffer-create "*gnus score files*"))
1887       (buffer-disable-undo (current-buffer))
1888       ;; Go through all score file names and create regexp with them
1889       ;; as the source.  
1890       (while sfiles
1891         (erase-buffer)
1892         (insert (car sfiles))
1893         (goto-char (point-min))
1894         ;; First remove the suffix itself.
1895         (when (re-search-forward (concat "." score-regexp) nil t)
1896           (replace-match "" t t) 
1897           (goto-char (point-min))
1898           (if (looking-at (regexp-quote kill-dir))
1899               ;; If the file name was just "SCORE", `klen' is one character
1900               ;; too much.
1901               (delete-char (min (1- (point-max)) klen))
1902             (goto-char (point-max))
1903             (search-backward "/")
1904             (delete-region (1+ (point)) (point-min)))
1905           ;; If short file names were used, we have to translate slashes.
1906           (goto-char (point-min))
1907           (let ((regexp (concat
1908                          "[/:" (if trans (char-to-string trans) "") "]")))
1909             (while (re-search-forward regexp nil t)
1910               (replace-match "." t t)))
1911           ;; Cludge to get rid of "nntp+" problems.
1912           (goto-char (point-min))
1913           (and (looking-at "nn[a-z]+\\+")
1914                (progn
1915                  (search-forward "+")
1916                  (forward-char -1)
1917                  (insert "\\")))
1918           ;; Translate "all" to ".*".
1919           (while (search-forward "all" nil t)
1920             (replace-match ".*" t t))
1921           (goto-char (point-min))
1922           ;; Deal with "not."s.
1923           (if (looking-at "not.")
1924               (progn
1925                 (setq not-match t)
1926                 (setq regexp (buffer-substring 5 (point-max))))
1927             (setq regexp (buffer-substring 1 (point-max)))
1928             (setq not-match nil))
1929           ;; Finally - if this resulting regexp matches the group name,
1930           ;; we add this score file to the list of score files
1931           ;; applicable to this group.
1932           (if (or (and not-match
1933                        (not (string-match regexp group)))
1934                   (and (not not-match)
1935                        (string-match regexp group)))
1936               (setq ofiles (cons (car sfiles) ofiles))))
1937         (setq sfiles (cdr sfiles)))
1938       (kill-buffer (current-buffer))
1939       ;; Slight kludge here - the last score file returned should be
1940       ;; the local score file, whether it exists or not. This is so
1941       ;; that any score commands the user enters will go to the right
1942       ;; file, and not end up in some global score file.
1943       (let ((localscore (gnus-score-file-name group)))
1944         (setq ofiles (cons localscore (delete localscore ofiles))))
1945       (nreverse ofiles))))
1946
1947 (defun gnus-score-find-single (group)
1948   "Return list containing the score file for GROUP."
1949   (list (gnus-score-file-name group gnus-adaptive-file-suffix)
1950         (gnus-score-file-name group)))
1951
1952 (defun gnus-score-find-hierarchical (group)
1953   "Return list of score files for GROUP.
1954 This includes the score file for the group and all its parents."
1955   (let ((all (copy-sequence '(nil)))
1956         (start 0))
1957     (while (string-match "\\." group (1+ start))
1958       (setq start (match-beginning 0))
1959       (setq all (cons (substring group 0 start) all)))
1960     (setq all (cons group all))
1961     (nconc
1962      (mapcar (lambda (newsgroup)
1963                (gnus-score-file-name newsgroup gnus-adaptive-file-suffix))
1964              (setq all (nreverse all)))
1965      (mapcar 'gnus-score-file-name all))))
1966
1967 (defvar gnus-score-file-alist-cache nil)
1968
1969 (defun gnus-score-find-alist (group)
1970   "Return list of score files for GROUP.
1971 The list is determined from the variable gnus-score-file-alist."
1972   (let ((alist gnus-score-file-multiple-match-alist)
1973         score-files)
1974     ;; if this group has been seen before, return the cached entry
1975     (if (setq score-files (assoc group gnus-score-file-alist-cache))
1976         (cdr score-files)               ;ensures caching groups with no matches
1977       ;; handle the multiple match alist
1978       (while alist
1979         (and (string-match (car (car alist)) group)
1980              (setq score-files
1981                    (nconc score-files (copy-sequence (cdr (car alist))))))
1982         (setq alist (cdr alist)))
1983       (setq alist gnus-score-file-single-match-alist)
1984       ;; handle the single match alist
1985       (while alist
1986         (and (string-match (car (car alist)) group)
1987              ;; progn used just in case ("regexp") has no files
1988              ;; and score-files is still nil. -sj
1989              ;; this can be construed as a "stop searching here" feature :>
1990              ;; and used to simplify regexps in the single-alist 
1991              (progn
1992                (setq score-files
1993                      (nconc score-files (copy-sequence (cdr (car alist)))))
1994                (setq alist nil)))
1995         (setq alist (cdr alist)))
1996       ;; cache the score files
1997       (setq gnus-score-file-alist-cache
1998             (cons (cons group score-files) gnus-score-file-alist-cache))
1999       score-files)))
2000
2001 (defun gnus-possibly-score-headers (&optional trace)
2002   (let ((funcs gnus-score-find-score-files-function)
2003         score-files)
2004     ;; Make sure funcs is a list.
2005     (and funcs
2006          (not (listp funcs))
2007          (setq funcs (list funcs)))
2008     ;; Get the initial score files for this group.
2009     (when funcs 
2010       (setq score-files (gnus-score-find-alist gnus-newsgroup-name)))
2011     ;; Go through all the functions for finding score files (or actual
2012     ;; scores) and add them to a list.
2013     (while funcs
2014       (when (gnus-functionp (car funcs))
2015         (setq score-files 
2016               (nconc score-files (funcall (car funcs) gnus-newsgroup-name))))
2017       (setq funcs (cdr funcs)))
2018     ;; Check whether there is a `score-file' group parameter.
2019     (let ((param-file (gnus-group-get-parameter 
2020                        gnus-newsgroup-name 'score-file)))
2021       (when param-file
2022         (push param-file score-files)))
2023     ;; Do the scoring if there are any score files for this group.
2024     (when score-files
2025       (gnus-score-headers score-files trace))))
2026
2027 (defun gnus-score-file-name (newsgroup &optional suffix)
2028   "Return the name of a score file for NEWSGROUP."
2029   (let ((suffix (or suffix gnus-score-file-suffix)))
2030     (apply 
2031      'gnus-replace-chars-in-string
2032      (cond
2033       ((or (null newsgroup)
2034            (string-equal newsgroup ""))
2035        ;; The global score file is placed at top of the directory.
2036        (expand-file-name 
2037         suffix (or gnus-kill-files-directory "~/News")))
2038       ((gnus-use-long-file-name 'not-score)
2039        ;; Append ".SCORE" to newsgroup name.
2040        (expand-file-name (concat (gnus-newsgroup-saveable-name newsgroup)
2041                                  "." suffix)
2042                          (or gnus-kill-files-directory "~/News")))
2043       (t
2044        ;; Place "SCORE" under the hierarchical directory.
2045        (expand-file-name (concat (gnus-newsgroup-directory-form newsgroup)
2046                                  "/" suffix)
2047                          (or gnus-kill-files-directory "~/News"))))
2048      gnus-file-name-translation-table)))
2049
2050 (defun gnus-score-search-global-directories (files)
2051   "Scan all global score directories for score files."
2052   ;; Set the variable `gnus-internal-global-score-files' to all
2053   ;; available global score files.
2054   (interactive (list gnus-global-score-files))
2055   (let (out)
2056     (while files
2057       (if (string-match "/$" (car files))
2058           (setq out (nconc (directory-files 
2059                             (car files) t
2060                             (concat (gnus-score-file-regexp) "$"))))
2061         (setq out (cons (car files) out)))
2062       (setq files (cdr files)))
2063     (setq gnus-internal-global-score-files out)))
2064
2065 (provide 'gnus-score)
2066
2067 ;;; gnus-score.el ends here