*** empty log message ***
[gnus] / lisp / gnus-score.el
1 ;;; gnus-score.el --- scoring code for Gnus
2 ;; Copyright (C) 1995,96,97 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 the
22 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
23 ;; Boston, MA 02111-1307, USA.
24
25 ;;; Commentary:
26
27 ;;; Code:
28
29 (require 'gnus)
30 (require 'gnus-sum)
31 (require 'gnus-range)
32
33 (defcustom gnus-global-score-files nil
34   "List of global score files and directories.
35 Set this variable if you want to use people's score files.  One entry
36 for each score file or each score file directory.  Gnus will decide
37 by itself what score files are applicable to which group.
38
39 Say you want to use the single score file
40 \"/ftp.ifi.uio.no@ftp:/pub/larsi/ding/score/soc.motss.SCORE\" and all
41 score files in the \"/ftp.some-where:/pub/score\" directory.
42
43  (setq gnus-global-score-files
44        '(\"/ftp.ifi.uio.no:/pub/larsi/ding/score/soc.motss.SCORE\"
45          \"/ftp.some-where:/pub/score\"))"
46   :group 'gnus-score
47   :type '(repeat file))
48
49 (defcustom gnus-score-file-single-match-alist nil
50   "Alist mapping regexps to lists of score files.
51 Each element of this alist should be of the form
52         (\"REGEXP\" [ \"SCORE-FILE-1\" ] [ \"SCORE-FILE-2\" ] ... )
53
54 If the name of a group is matched by REGEXP, the corresponding scorefiles
55 will be used for that group.
56 The first match found is used, subsequent matching entries are ignored (to
57 use multiple matches, see gnus-score-file-multiple-match-alist).
58
59 These score files are loaded in addition to any files returned by
60 gnus-score-find-score-files-function (which see)."
61   :group 'gnus-score
62   :type '(repeat (cons regexp (repeat file))))
63
64 (defcustom gnus-score-file-multiple-match-alist nil
65   "Alist mapping regexps to lists of score files.
66 Each element of this alist should be of the form
67         (\"REGEXP\" [ \"SCORE-FILE-1\" ] [ \"SCORE-FILE-2\" ] ... )
68
69 If the name of a group is matched by REGEXP, the corresponding scorefiles
70 will be used for that group.
71 If multiple REGEXPs match a group, the score files corresponding to each
72 match will be used (for only one match to be used, see
73 gnus-score-file-single-match-alist).
74
75 These score files are loaded in addition to any files returned by
76 gnus-score-find-score-files-function (which see)."
77   :group 'gnus-score
78   :type '(repeat (cons regexp (repeat file))))
79
80 (defcustom gnus-score-file-suffix "SCORE"
81   "Suffix of the score files."
82   :group 'gnus-score
83   :type 'string)
84
85 (defcustom gnus-adaptive-file-suffix "ADAPT"
86   "Suffix of the adaptive score files."
87   :group 'gnus-score
88   :type 'string)
89
90 (defcustom gnus-score-find-score-files-function 'gnus-score-find-bnews
91   "Function used to find score files.
92 The function will be called with the group name as the argument, and
93 should return a list of score files to apply to that group.  The score
94 files do not actually have to exist.
95
96 Predefined values are:
97
98 gnus-score-find-single: Only apply the group's own score file.
99 gnus-score-find-hierarchical: Also apply score files from parent groups.
100 gnus-score-find-bnews: Apply score files whose names matches.
101
102 See the documentation to these functions for more information.
103
104 This variable can also be a list of functions to be called.  Each
105 function should either return a list of score files, or a list of
106 score alists."
107   :group 'gnus-score
108   :type '(radio (function-item gnus-score-find-single)
109                 (function-item gnus-score-find-hierarchical)
110                 (function-item gnus-score-find-bnews)
111                 (function :tag "Other")))
112
113 (defcustom gnus-score-interactive-default-score 1000
114   "*Scoring commands will raise/lower the score with this number as the default."
115   :group 'gnus-score
116   :type 'integer)
117
118 (defcustom gnus-score-expiry-days 7
119   "*Number of days before unused score file entries are expired.
120 If this variable is nil, no score file entries will be expired."
121   :group 'gnus-score
122   :type '(choice (const :tag "never" nil)
123                  number))
124
125 (defcustom gnus-update-score-entry-dates t
126   "*In non-nil, update matching score entry dates.
127 If this variable is nil, then score entries that provide matches
128 will be expired along with non-matching score entries."
129   :group 'gnus-score
130   :type 'boolean)
131
132 (defcustom gnus-orphan-score nil
133   "*All orphans get this score added.  Set in the score file."
134   :group 'gnus-score
135   :type 'integer)
136
137 (defcustom gnus-decay-scores nil
138   "*If non-nil, decay non-permanent scores."
139   :group 'gnus-score
140   :type 'boolean)
141
142 (defcustom gnus-decay-score-function 'gnus-decay-score
143   "*Function called to decay a score.
144 It is called with one parameter -- the score to be decayed."
145   :group 'gnus-score
146   :type '(radio (function-item gnus-decay-score)
147                 (function :tag "Other")))
148
149 (defcustom gnus-score-decay-constant 3
150   "*Decay all \"small\" scores with this amount."
151   :group 'gnus-score
152   :type 'integer)
153
154 (defcustom gnus-score-decay-scale .05
155   "*Decay all \"big\" scores with this factor."
156   :group 'gnus-score
157   :type 'number)
158
159 (defcustom gnus-home-score-file nil
160   "Variable to control where interactive score entries are to go.
161 It can be:
162
163  * A string
164    This file file will be used as the home score file.
165
166  * A function
167    The result of this function will be used as the home score file.
168    The function will be passed the name of the group as its
169    parameter.
170
171  * A list
172    The elements in this list can be:
173
174    * `(regexp file-name ...)'
175      If the `regexp' matches the group name, the first `file-name' will
176      will be used as the home score file.  (Multiple filenames are
177      allowed so that one may use gnus-score-file-single-match-alist to
178      set this variable.)
179
180    * A function.
181      If the function returns non-nil, the result will be used
182      as the home score file.  The function will be passed the 
183      name of the group as its parameter.
184
185    * A string.  Use the string as the home score file.
186
187    The list will be traversed from the beginning towards the end looking
188    for matches."
189   :group 'gnus-score
190   :type '(choice string
191                  (repeat (choice string
192                                  (cons regexp (repeat file))
193                                  function))
194                  function))
195
196 (defcustom gnus-home-adapt-file nil
197   "Variable to control where new adaptive score entries are to go.
198 This variable allows the same syntax as `gnus-home-score-file'."
199   :group 'gnus-score
200   :type '(choice string
201                  (repeat (choice string
202                                  (cons regexp (repeat file))
203                                  function))
204                  function))
205
206 (defcustom gnus-default-adaptive-score-alist  
207   '((gnus-kill-file-mark)
208     (gnus-unread-mark)
209     (gnus-read-mark (from 3) (subject 30))
210     (gnus-catchup-mark (subject -10))
211     (gnus-killed-mark (from -1) (subject -20))
212     (gnus-del-mark (from -2) (subject -15)))
213 "Alist of marks and scores."
214 :group 'gnus-score
215 :type '(repeat (cons (symbol :tag "Mark")
216                      (repeat (list (choice :tag "Header"
217                                            (const from)
218                                            (const subject)
219                                            (symbol :tag "other"))
220                                    (integer :tag "Score"))))))
221
222 (defcustom gnus-ignored-adaptive-words nil
223   "List of words to be ignored when doing adaptive word scoring."
224   :group 'gnus-score
225   :type '(repeat string))
226
227 (defcustom gnus-default-ignored-adaptive-words
228   '("a" "i" "the" "to" "of" "and" "in" "is" "it" "for" "that" "if" "you"
229     "this" "be" "on" "with" "not" "have" "are" "or" "as" "from" "can"
230     "but" "by" "at" "an" "will" "no" "all" "was" "do" "there" "my" "one"
231     "so" "we" "they" "what" "would" "any" "which" "about" "get" "your"
232     "use" "some" "me" "then" "name" "like" "out" "when" "up" "time"
233     "other" "more" "only" "just" "end" "also" "know" "how" "new" "should"
234     "been" "than" "them" "he" "who" "make" "may" "people" "these" "now"
235     "their" "here" "into" "first" "could" "way" "had" "see" "work" "well"
236     "were" "two" "very" "where" "while" "us" "because" "good" "same"
237     "even" "much" "most" "many" "such" "long" "his" "over" "last" "since"
238     "right" "before" "our" "without" "too" "those" "why" "must" "part"
239     "being" "current" "back" "still" "go" "point" "value" "each" "did"
240     "both" "true" "off" "say" "another" "state" "might" "under" "start"
241     "try" "re")
242   "Default list of words to be ignored when doing adaptive word scoring."
243   :group 'gnus-score
244   :type '(repeat string))
245
246 (defcustom gnus-default-adaptive-word-score-alist  
247   `((,gnus-read-mark . 30)
248     (,gnus-catchup-mark . -10)
249     (,gnus-killed-mark . -20)
250     (,gnus-del-mark . -15))
251 "Alist of marks and scores."
252 :group 'gnus-score
253 :type '(repeat (cons (character :tag "Mark")
254                      (integer :tag "Score"))))
255
256 (defcustom gnus-score-mimic-keymap nil
257   "*Have the score entry functions pretend that they are a keymap."
258   :group 'gnus-score
259   :type 'boolean)
260
261 (defcustom gnus-score-exact-adapt-limit 10
262   "*Number that says how long a match has to be before using substring matching.
263 When doing adaptive scoring, one normally uses fuzzy or substring
264 matching.  However, if the header one matches is short, the possibility
265 for false positives is great, so if the length of the match is less
266 than this variable, exact matching will be used.
267
268 If this variable is nil, exact matching will always be used."
269   :group 'gnus-score
270   :type '(choice (const nil) integer))
271
272 (defcustom gnus-score-uncacheable-files "ADAPT$"
273   "All score files that match this regexp will not be cached."
274   :group 'gnus-score
275   :type 'regexp)
276
277 (defcustom gnus-score-default-header nil
278   "Default header when entering new scores.
279
280 Should be one of the following symbols.
281
282  a: from
283  s: subject
284  b: body
285  h: head
286  i: message-id
287  t: references
288  x: xref
289  l: lines
290  d: date
291  f: followup
292
293 If nil, the user will be asked for a header."
294   :group 'gnus-score
295   :type '(choice (const :tag "from" a)
296                  (const :tag "subject" s)
297                  (const :tag "body" b)
298                  (const :tag "head" h)
299                  (const :tag "message-id" i)
300                  (const :tag "references" t)
301                  (const :tag "xref" x)
302                  (const :tag "lines" l)
303                  (const :tag "date" d)
304                  (const :tag "followup" f)))
305
306 (defcustom gnus-score-default-type nil
307   "Default match type when entering new scores.
308
309 Should be one of the following symbols.
310
311  s: substring
312  e: exact string
313  f: fuzzy string
314  r: regexp string
315  b: before date
316  a: at date
317  n: this date
318  <: less than number
319  >: greater than number
320  =: equal to number
321
322 If nil, the user will be asked for a match type."
323   :group 'gnus-score
324   :type '(choice (const :tag "substring" s)
325                  (const :tag "exact string" e)
326                  (const :tag "fuzzy string" f)
327                  (const :tag "regexp string" r)
328                  (const :tag "before date" b)
329                  (const :tag "at date" a)
330                  (const :tag "this date" n)
331                  (const :tag "less than number" <)
332                  (const :tag "greater than number" >)
333                  (const :tag "equal than number" =)))
334
335 (defcustom gnus-score-default-fold nil
336   "Use case folding for new score file entries iff not nil."
337   :group 'gnus-score
338   :type 'boolean)
339
340 (defcustom gnus-score-default-duration nil
341   "Default duration of effect when entering new scores.
342
343 Should be one of the following symbols.
344
345  t: temporary
346  p: permanent
347  i: immediate
348
349 If nil, the user will be asked for a duration."
350   :group 'gnus-score
351   :type '(choice (const :tag "temporary" t)
352                  (const :tag "permanent" p)
353                  (const :tag "immediate" i)))
354
355 (defcustom gnus-score-after-write-file-function nil
356   "Function called with the name of the score file just written to disk."
357   :group 'gnus-score
358   :type 'function)
359
360 \f
361
362 ;; Internal variables.
363
364 (defvar gnus-adaptive-word-syntax-table
365   (let ((table (copy-syntax-table (standard-syntax-table)))
366         (numbers '(?0 ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9)))
367     (while numbers
368       (modify-syntax-entry (pop numbers) " " table))
369     (modify-syntax-entry ?' "w" table)
370     table)
371   "Syntax table used when doing adaptive word scoring.")
372
373 (defvar gnus-scores-exclude-files nil)
374 (defvar gnus-internal-global-score-files nil)
375 (defvar gnus-score-file-list nil)
376
377 (defvar gnus-short-name-score-file-cache nil)
378
379 (defvar gnus-score-help-winconf nil)
380 (defvar gnus-adaptive-score-alist gnus-default-adaptive-score-alist)
381 (defvar gnus-adaptive-word-score-alist gnus-default-adaptive-word-score-alist)
382 (defvar gnus-score-trace nil)
383 (defvar gnus-score-edit-buffer nil)
384
385 (defvar gnus-score-alist nil
386   "Alist containing score information.
387 The keys can be symbols or strings.  The following symbols are defined. 
388
389 touched: If this alist has been modified.
390 mark:    Automatically mark articles below this.
391 expunge: Automatically expunge articles below this.
392 files:   List of other score files to load when loading this one.
393 eval:    Sexp to be evaluated when the score file is loaded.
394
395 String entries have the form (HEADER (MATCH TYPE SCORE DATE) ...)
396 where HEADER is the header being scored, MATCH is the string we are
397 looking for, TYPE is a flag indicating whether it should use regexp or
398 substring matching, SCORE is the score to add and DATE is the date
399 of the last successful match.")
400
401 (defvar gnus-score-cache nil)
402 (defvar gnus-scores-articles nil)
403 (defvar gnus-score-index nil)
404
405
406 (defconst gnus-header-index
407   ;; Name to index alist.
408   '(("number" 0 gnus-score-integer)
409     ("subject" 1 gnus-score-string)
410     ("from" 2 gnus-score-string)
411     ("date" 3 gnus-score-date)
412     ("message-id" 4 gnus-score-string)
413     ("references" 5 gnus-score-string)
414     ("chars" 6 gnus-score-integer)
415     ("lines" 7 gnus-score-integer)
416     ("xref" 8 gnus-score-string)
417     ("head" -1 gnus-score-body)
418     ("body" -1 gnus-score-body)
419     ("all" -1 gnus-score-body)
420     ("followup" 2 gnus-score-followup)
421     ("thread" 5 gnus-score-thread)))
422
423 ;;; Summary mode score maps.
424
425 (gnus-define-keys (gnus-summary-score-map "V" gnus-summary-mode-map)
426   "s" gnus-summary-set-score
427   "a" gnus-summary-score-entry
428   "S" gnus-summary-current-score
429   "c" gnus-score-change-score-file
430   "C" gnus-score-customize
431   "m" gnus-score-set-mark-below
432   "x" gnus-score-set-expunge-below
433   "R" gnus-summary-rescore
434   "e" gnus-score-edit-current-scores
435   "f" gnus-score-edit-file
436   "F" gnus-score-flush-cache
437   "t" gnus-score-find-trace
438   "w" gnus-score-find-favourite-words)
439
440 ;; Summary score file commands
441
442 ;; Much modification of the kill (ahem, score) code and lots of the
443 ;; functions are written by Per Abrahamsen <amanda@iesd.auc.dk>.
444
445 (defun gnus-summary-lower-score (&optional score)
446   "Make a score entry based on the current article.
447 The user will be prompted for header to score on, match type,
448 permanence, and the string to be used.  The numerical prefix will be
449 used as score."
450   (interactive "P")
451   (gnus-summary-increase-score (- (gnus-score-default score))))
452
453 (defun gnus-score-kill-help-buffer ()
454   (when (get-buffer "*Score Help*")
455     (kill-buffer "*Score Help*")
456     (when gnus-score-help-winconf
457       (set-window-configuration gnus-score-help-winconf))))
458
459 (defun gnus-summary-increase-score (&optional score)
460   "Make a score entry based on the current article.
461 The user will be prompted for header to score on, match type,
462 permanence, and the string to be used.  The numerical prefix will be
463 used as score."
464   (interactive "P")
465   (gnus-set-global-variables)
466   (let* ((nscore (gnus-score-default score))
467          (prefix (if (< nscore 0) ?L ?I))
468          (increase (> nscore 0))
469          (char-to-header 
470           '((?a "from" nil nil string)
471             (?s "subject" nil nil string)
472             (?b "body" "" nil body-string)
473             (?h "head" "" nil body-string)
474             (?i "message-id" nil t string)
475             (?t "references" "message-id" nil string)
476             (?x "xref" nil nil string)
477             (?l "lines" nil nil number)
478             (?d "date" nil nil date)
479             (?f "followup" nil nil string)
480             (?T "thread" nil nil string)))
481          (char-to-type
482           '((?s s "substring" string)
483             (?e e "exact string" string)
484             (?f f "fuzzy string" string)
485             (?r r "regexp string" string)
486             (?z s "substring" body-string)
487             (?p r "regexp string" body-string)
488             (?b before "before date" date)
489             (?a at "at date" date)
490             (?n now "this date" date)
491             (?< < "less than number" number)
492             (?> > "greater than number" number)
493             (?= = "equal to number" number)))
494          (char-to-perm
495           (list (list ?t (current-time-string) "temporary")
496                 '(?p perm "permanent") '(?i now "immediate")))
497          (mimic gnus-score-mimic-keymap)
498          (hchar (and gnus-score-default-header 
499                      (aref (symbol-name gnus-score-default-header) 0)))
500          (tchar (and gnus-score-default-type
501                      (aref (symbol-name gnus-score-default-type) 0)))
502          (pchar (and gnus-score-default-duration
503                      (aref (symbol-name gnus-score-default-duration) 0)))
504          entry temporary type match)
505     
506     (unwind-protect
507         (progn
508
509           ;; First we read the header to score.
510           (while (not hchar)
511             (if mimic
512                 (progn 
513                   (sit-for 1)
514                   (message "%c-" prefix))
515               (message "%s header (%s?): " (if increase "Increase" "Lower")
516                        (mapconcat (lambda (s) (char-to-string (car s)))
517                                   char-to-header "")))
518             (setq hchar (read-char))
519             (when (or (= hchar ??) (= hchar ?\C-h))
520               (setq hchar nil)
521               (gnus-score-insert-help "Match on header" char-to-header 1)))
522
523           (gnus-score-kill-help-buffer)
524           (unless (setq entry (assq (downcase hchar) char-to-header))
525             (if mimic (error "%c %c" prefix hchar) (error "")))
526
527           (when (/= (downcase hchar) hchar)
528             ;; This was a majuscule, so we end reading and set the defaults.
529             (if mimic (message "%c %c" prefix hchar) (message ""))
530             (setq tchar (or tchar ?s)
531                   pchar (or pchar ?t)))
532     
533           ;; We continue reading - the type.
534           (while (not tchar)
535             (if mimic
536                 (progn
537                   (sit-for 1) (message "%c %c-" prefix hchar))
538               (message "%s header '%s' with match type (%s?): "
539                        (if increase "Increase" "Lower")
540                        (nth 1 entry)
541                        (mapconcat (lambda (s)
542                                     (if (eq (nth 4 entry)
543                                             (nth 3 s))
544                                         (char-to-string (car s))
545                                       ""))
546                                   char-to-type "")))
547             (setq tchar (read-char))
548             (when (or (= tchar ??) (= tchar ?\C-h))
549               (setq tchar nil)
550               (gnus-score-insert-help
551                "Match type"
552                (delq nil
553                      (mapcar (lambda (s)
554                                (if (eq (nth 4 entry)
555                                        (nth 3 s))
556                                    s nil))
557                              char-to-type))
558                2)))
559
560           (gnus-score-kill-help-buffer)
561           (unless (setq type (nth 1 (assq (downcase tchar) char-to-type)))
562             (if mimic (error "%c %c" prefix hchar) (error "")))
563
564           (when (/= (downcase tchar) tchar)
565             ;; It was a majuscule, so we end reading and use the default.
566             (if mimic (message "%c %c %c" prefix hchar tchar)
567               (message ""))
568             (setq pchar (or pchar ?p)))
569
570           ;; We continue reading.
571           (while (not pchar)
572             (if mimic
573                 (progn
574                   (sit-for 1) (message "%c %c %c-" prefix hchar tchar))
575               (message "%s permanence (%s?): " (if increase "Increase" "Lower")
576                        (mapconcat (lambda (s) (char-to-string (car s)))
577                                   char-to-perm "")))
578             (setq pchar (read-char))
579             (when (or (= pchar ??) (= pchar ?\C-h))
580               (setq pchar nil)
581               (gnus-score-insert-help "Match permanence" char-to-perm 2)))
582
583           (gnus-score-kill-help-buffer)
584           (if mimic (message "%c %c %c" prefix hchar tchar pchar)
585             (message ""))
586           (unless (setq temporary (cadr (assq pchar char-to-perm)))
587             ;; Deal with der(r)ided superannuated paradigms.
588             (when (and (eq (1+ prefix) 77)
589                        (eq (+ hchar 12) 109)
590                        (eq tchar 114)
591                        (eq (- pchar 4) 111))
592               (error "You rang?"))
593             (if mimic 
594                 (error "%c %c %c %c" prefix hchar tchar pchar)
595               (error ""))))
596       ;; Always kill the score help buffer.
597       (gnus-score-kill-help-buffer))
598
599     ;; We have all the data, so we enter this score.
600     (setq match (if (string= (nth 2 entry) "") ""
601                   (gnus-summary-header (or (nth 2 entry) (nth 1 entry)))))
602       
603     ;; Modify the match, perhaps.
604     (cond 
605      ((equal (nth 1 entry) "xref")
606       (when (string-match "^Xref: *" match)
607         (setq match (substring match (match-end 0))))
608       (when (string-match "^[^:]* +" match)
609         (setq match (substring match (match-end 0))))))
610     
611     (when (memq type '(r R regexp Regexp))
612       (setq match (regexp-quote match)))
613
614     (gnus-summary-score-entry
615      (nth 1 entry)                      ; Header
616      match                              ; Match
617      type                               ; Type
618      (if (eq score 's) nil score)       ; Score
619      (if (eq temporary 'perm)           ; Temp
620          nil
621        temporary)
622      (not (nth 3 entry)))               ; Prompt
623     ))
624   
625 (defun gnus-score-insert-help (string alist idx)
626   (setq gnus-score-help-winconf (current-window-configuration))
627   (save-excursion
628     (set-buffer (get-buffer-create "*Score Help*"))
629     (buffer-disable-undo (current-buffer))
630     (delete-windows-on (current-buffer))
631     (erase-buffer)
632     (insert string ":\n\n")
633     (let ((max -1)
634           (list alist)
635           (i 0)
636           n width pad format)
637       ;; find the longest string to display
638       (while list
639         (setq n (length (nth idx (car list))))
640         (unless (> max n)
641           (setq max n))
642         (setq list (cdr list)))
643       (setq max (+ max 4))              ; %c, `:', SPACE, a SPACE at end
644       (setq n (/ (1- (window-width)) max)) ; items per line
645       (setq width (/ (1- (window-width)) n)) ; width of each item
646       ;; insert `n' items, each in a field of width `width' 
647       (while alist
648         (if (< i n)
649             ()
650           (setq i 0)
651           (delete-char -1)              ; the `\n' takes a char
652           (insert "\n"))
653         (setq pad (- width 3))
654         (setq format (concat "%c: %-" (int-to-string pad) "s"))
655         (insert (format format (caar alist) (nth idx (car alist))))
656         (setq alist (cdr alist))
657         (setq i (1+ i))))
658     ;; display ourselves in a small window at the bottom
659     (gnus-appt-select-lowest-window)
660     (split-window)
661     (pop-to-buffer "*Score Help*")
662     (let ((window-min-height 1))
663       (shrink-window-if-larger-than-buffer))
664     (select-window (get-buffer-window gnus-summary-buffer))))
665   
666 (defun gnus-summary-header (header &optional no-err)
667   ;; Return HEADER for current articles, or error.
668   (let ((article (gnus-summary-article-number))
669         headers)
670     (if article
671         (if (and (setq headers (gnus-summary-article-header article))
672                  (vectorp headers))
673             (aref headers (nth 1 (assoc header gnus-header-index)))
674           (if no-err
675               nil
676             (error "Pseudo-articles can't be scored")))
677       (if no-err
678           (error "No article on current line")
679         nil))))
680
681 (defun gnus-newsgroup-score-alist ()
682   (or
683    (let ((param-file (gnus-group-find-parameter 
684                       gnus-newsgroup-name 'score-file)))
685      (when param-file
686        (gnus-score-load param-file)))
687    (gnus-score-load
688     (gnus-score-file-name gnus-newsgroup-name)))
689   gnus-score-alist)
690
691 (defsubst gnus-score-get (symbol &optional alist)
692   ;; Get SYMBOL's definition in ALIST.
693   (cdr (assoc symbol 
694               (or alist 
695                   gnus-score-alist
696                   (gnus-newsgroup-score-alist)))))
697
698 (defun gnus-summary-score-entry (header match type score date
699                                         &optional prompt silent)
700   "Enter score file entry.
701 HEADER is the header being scored.
702 MATCH is the string we are looking for.
703 TYPE is the match type: substring, regexp, exact, fuzzy.
704 SCORE is the score to add.
705 DATE is the expire date, or nil for no expire, or 'now for immediate expire.
706 If optional argument `PROMPT' is non-nil, allow user to edit match.
707 If optional argument `SILENT' is nil, show effect of score entry."
708   (interactive
709    (list (completing-read "Header: "
710                           gnus-header-index
711                           (lambda (x) (fboundp (nth 2 x)))
712                           t)
713          (read-string "Match: ")
714          (if (y-or-n-p "Use regexp match? ") 'r 's)
715          (and current-prefix-arg
716               (prefix-numeric-value current-prefix-arg))
717          (cond ((not (y-or-n-p "Add to score file? "))
718                 'now)
719                ((y-or-n-p "Expire kill? ")
720                 (current-time-string))
721                (t nil))))
722   ;; Regexp is the default type.
723   (when (eq type t)
724     (setq type 'r))
725   ;; Simplify matches...
726   (cond ((or (eq type 'r) (eq type 's) (eq type nil))
727          (setq match (if match (gnus-simplify-subject-re match) "")))
728         ((eq type 'f)
729          (setq match (gnus-simplify-subject-fuzzy match))))
730   (let ((score (gnus-score-default score))
731         (header (format "%s" (downcase header)))
732         new)
733     (when prompt
734       (setq match (read-string 
735                    (format "Match %s on %s, %s: " 
736                            (cond ((eq date 'now)
737                                   "now")
738                                  ((stringp date)
739                                   "temp")
740                                  (t "permanent"))
741                            header
742                            (if (< score 0) "lower" "raise"))
743                    (if (numberp match)
744                        (int-to-string match)
745                      match))))
746
747     ;; Get rid of string props.
748     (setq match (format "%s" match))
749
750     ;; If this is an integer comparison, we transform from string to int. 
751     (when (eq (nth 2 (assoc header gnus-header-index)) 'gnus-score-integer)
752       (setq match (string-to-int match)))
753
754     (unless (eq date 'now)
755       ;; Add the score entry to the score file.
756       (when (= score gnus-score-interactive-default-score)
757         (setq score nil))
758       (let ((old (gnus-score-get header))
759             elem)
760         (setq new
761               (cond 
762                (type
763                 (list match score
764                       (and date (if (numberp date) date
765                                   (gnus-day-number date)))
766                       type))
767                (date (list match score (gnus-day-number date)))
768                (score (list match score))
769                (t (list match))))
770         ;; We see whether we can collapse some score entries.
771         ;; This isn't quite correct, because there may be more elements
772         ;; later on with the same key that have matching elems...  Hm.
773         (if (and old
774                  (setq elem (assoc match old))
775                  (eq (nth 3 elem) (nth 3 new))
776                  (or (and (numberp (nth 2 elem)) (numberp (nth 2 new)))
777                      (and (not (nth 2 elem)) (not (nth 2 new)))))
778             ;; Yup, we just add this new score to the old elem.
779             (setcar (cdr elem) (+ (or (nth 1 elem)
780                                       gnus-score-interactive-default-score)
781                                   (or (nth 1 new)
782                                       gnus-score-interactive-default-score)))
783           ;; Nope, we have to add a new elem.
784           (gnus-score-set header (if old (cons new old) (list new))))
785         (gnus-score-set 'touched '(t))))
786
787     ;; Score the current buffer.
788     (unless silent
789       (if (and (>= (nth 1 (assoc header gnus-header-index)) 0)
790                (eq (nth 2 (assoc header gnus-header-index))
791                    'gnus-score-string))
792           (gnus-summary-score-effect header match type score)
793         (gnus-summary-rescore)))
794
795     ;; Return the new scoring rule.
796     new))
797
798 (defun gnus-summary-score-effect (header match type score)
799   "Simulate the effect of a score file entry.
800 HEADER is the header being scored.
801 MATCH is the string we are looking for.
802 TYPE is the score type.
803 SCORE is the score to add."
804   (interactive (list (completing-read "Header: "
805                                       gnus-header-index
806                                       (lambda (x) (fboundp (nth 2 x)))
807                                       t)
808                      (read-string "Match: ")
809                      (y-or-n-p "Use regexp match? ")
810                      (prefix-numeric-value current-prefix-arg)))
811   (save-excursion
812     (unless (and (stringp match) (> (length match) 0))
813       (error "No match"))
814     (goto-char (point-min))
815     (let ((regexp (cond ((eq type 'f)
816                          (gnus-simplify-subject-fuzzy match))
817                         ((eq type 'r)
818                          match)
819                         ((eq type 'e)
820                          (concat "\\`" (regexp-quote match) "\\'"))
821                         (t 
822                          (regexp-quote match)))))
823       (while (not (eobp))
824         (let ((content (gnus-summary-header header 'noerr))
825               (case-fold-search t))
826           (and content
827                (when (if (eq type 'f)
828                          (string-equal (gnus-simplify-subject-fuzzy content)
829                                        regexp)
830                        (string-match regexp content))
831                  (gnus-summary-raise-score score))))
832         (beginning-of-line 2)))))
833
834 (defun gnus-summary-score-crossposting (score date)
835   ;; Enter score file entry for current crossposting.
836   ;; SCORE is the score to add.
837   ;; DATE is the expire date.
838   (let ((xref (gnus-summary-header "xref"))
839         (start 0)
840         group)
841     (unless xref
842       (error "This article is not crossposted"))
843     (while (string-match " \\([^ \t]+\\):" xref start)
844       (setq start (match-end 0))
845       (when (not (string= 
846                   (setq group 
847                         (substring xref (match-beginning 1) (match-end 1)))
848                   gnus-newsgroup-name))
849         (gnus-summary-score-entry
850          "xref" (concat " " group ":") nil score date t)))))
851
852 \f
853 ;;;
854 ;;; Gnus Score Files
855 ;;;
856
857 ;; All score code written by Per Abrahamsen <abraham@iesd.auc.dk>.
858
859 ;; Added by Per Abrahamsen <amanda@iesd.auc.dk>.
860 (defun gnus-score-set-mark-below (score)
861   "Automatically mark articles with score below SCORE as read."
862   (interactive 
863    (list (or (and current-prefix-arg (prefix-numeric-value current-prefix-arg))
864              (string-to-int (read-string "Mark below: ")))))
865   (setq score (or score gnus-summary-default-score 0))
866   (gnus-score-set 'mark (list score))
867   (gnus-score-set 'touched '(t))
868   (setq gnus-summary-mark-below score)
869   (gnus-score-update-lines))
870
871 (defun gnus-score-update-lines ()
872   "Update all lines in the summary buffer."
873   (save-excursion
874     (goto-char (point-min))
875     (while (not (eobp))
876       (gnus-summary-update-line)
877       (forward-line 1))))
878
879 (defun gnus-score-update-all-lines ()
880   "Update all lines in the summary buffer, even the hidden ones."
881   (save-excursion
882     (goto-char (point-min))
883     (let (hidden)
884       (while (not (eobp))
885         (when (gnus-summary-show-thread)
886           (push (point) hidden))
887         (gnus-summary-update-line)
888         (forward-line 1))
889       ;; Re-hide the hidden threads.
890       (while hidden
891         (goto-char (pop hidden))
892         (gnus-summary-hide-thread)))))
893
894 (defun gnus-score-set-expunge-below (score)
895   "Automatically expunge articles with score below SCORE."
896   (interactive 
897    (list (or (and current-prefix-arg (prefix-numeric-value current-prefix-arg))
898              (string-to-int (read-string "Set expunge below: ")))))
899   (setq score (or score gnus-summary-default-score 0))
900   (gnus-score-set 'expunge (list score))
901   (gnus-score-set 'touched '(t)))
902
903 (defun gnus-score-followup-article (&optional score)
904   "Add SCORE to all followups to the article in the current buffer."
905   (interactive "P")
906   (setq score (gnus-score-default score))
907   (when (gnus-buffer-live-p gnus-summary-buffer)
908     (save-excursion
909       (save-restriction
910         (message-narrow-to-headers)
911         (let ((id (mail-fetch-field "message-id")))
912           (when id
913             (set-buffer gnus-summary-buffer)
914             (gnus-summary-score-entry
915              "references" (concat id "[ \t]*$") 'r
916              score (current-time-string) nil t)))))))
917
918 (defun gnus-score-followup-thread (&optional score)
919   "Add SCORE to all later articles in the thread the current buffer is part of."
920   (interactive "P")
921   (setq score (gnus-score-default score))
922   (when (gnus-buffer-live-p gnus-summary-buffer)
923     (save-excursion
924       (save-restriction
925         (goto-char (point-min))
926         (let ((id (mail-fetch-field "message-id")))
927           (when id
928             (set-buffer gnus-summary-buffer)
929             (gnus-summary-score-entry
930              "references" id 's
931              score (current-time-string))))))))
932
933 (defun gnus-score-set (symbol value &optional alist)
934   ;; Set SYMBOL to VALUE in ALIST.
935   (let* ((alist 
936           (or alist 
937               gnus-score-alist
938               (gnus-newsgroup-score-alist)))
939          (entry (assoc symbol alist)))
940     (cond ((gnus-score-get 'read-only alist)
941            ;; This is a read-only score file, so we do nothing.
942            )
943           (entry
944            (setcdr entry value))
945           ((null alist)
946            (error "Empty alist"))
947           (t
948            (setcdr alist
949                    (cons (cons symbol value) (cdr alist)))))))
950
951 (defun gnus-summary-raise-score (n)
952   "Raise the score of the current article by N."
953   (interactive "p")
954   (gnus-set-global-variables)
955   (gnus-summary-set-score (+ (gnus-summary-article-score)
956                              (or n gnus-score-interactive-default-score ))))
957
958 (defun gnus-summary-set-score (n)
959   "Set the score of the current article to N."
960   (interactive "p")
961   (gnus-set-global-variables)
962   (save-excursion
963     (gnus-summary-show-thread)
964     (let ((buffer-read-only nil))
965       ;; Set score.
966       (gnus-summary-update-mark
967        (if (= n (or gnus-summary-default-score 0)) ? 
968          (if (< n (or gnus-summary-default-score 0))
969              gnus-score-below-mark gnus-score-over-mark))
970        'score))
971     (let* ((article (gnus-summary-article-number))
972            (score (assq article gnus-newsgroup-scored)))
973       (if score (setcdr score n)
974         (push (cons article n) gnus-newsgroup-scored)))
975     (gnus-summary-update-line)))
976
977 (defun gnus-summary-current-score ()
978   "Return the score of the current article."
979   (interactive)
980   (gnus-set-global-variables)
981   (gnus-message 1 "%s" (gnus-summary-article-score)))
982
983 (defun gnus-score-change-score-file (file)
984   "Change current score alist."
985   (interactive 
986    (list (read-file-name "Change to score file: " gnus-kill-files-directory)))
987   (gnus-score-load-file file)
988   (gnus-set-mode-line 'summary))
989
990 (defvar gnus-score-edit-exit-function)
991 (defun gnus-score-edit-current-scores (file)
992   "Edit the current score alist."
993   (interactive (list gnus-current-score-file))
994   (gnus-set-global-variables)
995   (let ((winconf (current-window-configuration)))
996     (when (buffer-name gnus-summary-buffer)
997       (gnus-score-save))
998     (gnus-make-directory (file-name-directory file))
999     (setq gnus-score-edit-buffer (find-file-noselect file))
1000     (gnus-configure-windows 'edit-score)
1001     (gnus-score-mode)
1002     (setq gnus-score-edit-exit-function 'gnus-score-edit-done)
1003     (make-local-variable 'gnus-prev-winconf)
1004     (setq gnus-prev-winconf winconf))
1005   (gnus-message 
1006    4 (substitute-command-keys 
1007       "\\<gnus-score-mode-map>\\[gnus-score-edit-exit] to save edits")))
1008   
1009 (defun gnus-score-edit-file (file)
1010   "Edit a score file."
1011   (interactive 
1012    (list (read-file-name "Edit score file: " gnus-kill-files-directory)))
1013   (gnus-make-directory (file-name-directory file))
1014   (when (buffer-name gnus-summary-buffer)
1015     (gnus-score-save))
1016   (let ((winconf (current-window-configuration)))
1017     (setq gnus-score-edit-buffer (find-file-noselect file))
1018     (gnus-configure-windows 'edit-score)
1019     (gnus-score-mode)
1020     (setq gnus-score-edit-exit-function 'gnus-score-edit-done)
1021     (make-local-variable 'gnus-prev-winconf)
1022     (setq gnus-prev-winconf winconf))
1023   (gnus-message 
1024    4 (substitute-command-keys 
1025       "\\<gnus-score-mode-map>\\[gnus-score-edit-exit] to save edits")))
1026   
1027 (defun gnus-score-load-file (file)
1028   ;; Load score file FILE.  Returns a list a retrieved score-alists.
1029   (let* ((file (expand-file-name 
1030                 (or (and (string-match
1031                           (concat "^" (expand-file-name
1032                                        gnus-kill-files-directory))
1033                           (expand-file-name file))
1034                          file)
1035                     (concat (file-name-as-directory gnus-kill-files-directory)
1036                             file))))
1037          (cached (assoc file gnus-score-cache))
1038          (global (member file gnus-internal-global-score-files))
1039          lists alist)
1040     (if cached
1041         ;; The score file was already loaded.
1042         (setq alist (cdr cached))
1043       ;; We load the score file.
1044       (setq gnus-score-alist nil)
1045       (setq alist (gnus-score-load-score-alist file))
1046       ;; We add '(touched) to the alist to signify that it hasn't been
1047       ;; touched (yet). 
1048       (unless (assq 'touched alist)
1049         (push (list 'touched nil) alist))
1050       ;; If it is a global score file, we make it read-only.
1051       (and global
1052            (not (assq 'read-only alist))
1053            (push (list 'read-only t) alist))
1054       (push (cons file alist) gnus-score-cache))
1055     (let ((a alist)
1056           found)
1057       (while a
1058         ;; Downcase all header names.
1059         (when (stringp (caar a))
1060           (setcar (car a) (downcase (caar a)))
1061           (setq found t))
1062         (pop a))
1063       ;; If there are actual scores in the alist, we add it to the
1064       ;; return value of this function.
1065       (when found
1066         (setq lists (list alist))))
1067     ;; Treat the other possible atoms in the score alist.
1068     (let ((mark (car (gnus-score-get 'mark alist)))
1069           (expunge (car (gnus-score-get 'expunge alist)))
1070           (mark-and-expunge (car (gnus-score-get 'mark-and-expunge alist)))
1071           (files (gnus-score-get 'files alist))
1072           (exclude-files (gnus-score-get 'exclude-files alist))
1073           (orphan (car (gnus-score-get 'orphan alist)))
1074           (adapt (gnus-score-get 'adapt alist))
1075           (thread-mark-and-expunge
1076            (car (gnus-score-get 'thread-mark-and-expunge alist)))
1077           (adapt-file (car (gnus-score-get 'adapt-file alist)))
1078           (local (gnus-score-get 'local alist))
1079           (decay (car (gnus-score-get 'decay alist)))
1080           (eval (car (gnus-score-get 'eval alist))))
1081       ;; Perform possible decays.
1082       (when (and gnus-decay-scores
1083                  (gnus-decay-scores 
1084                   alist (or decay (gnus-time-to-day (current-time)))))
1085         (gnus-score-set 'touched '(t) alist)
1086         (gnus-score-set 'decay (list (gnus-time-to-day (current-time)))))
1087       ;; We do not respect eval and files atoms from global score
1088       ;; files. 
1089       (and files (not global)
1090            (setq lists (apply 'append lists
1091                               (mapcar (lambda (file)
1092                                         (gnus-score-load-file file))
1093                                       (if adapt-file (cons adapt-file files)
1094                                         files)))))
1095       (and eval (not global) (eval eval))
1096       ;; We then expand any exclude-file directives.
1097       (setq gnus-scores-exclude-files 
1098             (nconc 
1099              (mapcar 
1100               (lambda (sfile)
1101                 (expand-file-name sfile (file-name-directory file)))
1102               exclude-files)
1103              gnus-scores-exclude-files))
1104       (if (not local)
1105           ()
1106         (save-excursion
1107           (set-buffer gnus-summary-buffer)
1108           (while local
1109             (and (consp (car local))
1110                  (symbolp (caar local))
1111                  (progn
1112                    (make-local-variable (caar local))
1113                    (set (caar local) (nth 1 (car local)))))
1114             (setq local (cdr local)))))
1115       (when orphan
1116         (setq gnus-orphan-score orphan))
1117       (setq gnus-adaptive-score-alist
1118             (cond ((equal adapt '(t))
1119                    (setq gnus-newsgroup-adaptive t)
1120                    gnus-default-adaptive-score-alist)
1121                   ((equal adapt '(ignore))
1122                    (setq gnus-newsgroup-adaptive nil))
1123                   ((consp adapt)
1124                    (setq gnus-newsgroup-adaptive t)
1125                    adapt)
1126                   (t
1127                    ;;(setq gnus-newsgroup-adaptive gnus-use-adaptive-scoring)
1128                    gnus-default-adaptive-score-alist)))
1129       (setq gnus-thread-expunge-below 
1130             (or thread-mark-and-expunge gnus-thread-expunge-below))
1131       (setq gnus-summary-mark-below 
1132             (or mark mark-and-expunge gnus-summary-mark-below))
1133       (setq gnus-summary-expunge-below 
1134             (or expunge mark-and-expunge gnus-summary-expunge-below))
1135       (setq gnus-newsgroup-adaptive-score-file 
1136             (or adapt-file gnus-newsgroup-adaptive-score-file)))
1137     (setq gnus-current-score-file file)
1138     (setq gnus-score-alist alist)
1139     lists))
1140
1141 (defun gnus-score-load (file)
1142   ;; Load score FILE.
1143   (let ((cache (assoc file gnus-score-cache)))
1144     (if cache
1145         (setq gnus-score-alist (cdr cache))
1146       (setq gnus-score-alist nil)
1147       (gnus-score-load-score-alist file)
1148       (unless gnus-score-alist
1149         (setq gnus-score-alist (copy-alist '((touched nil)))))
1150       (push (cons file gnus-score-alist) gnus-score-cache))))
1151
1152 (defun gnus-score-remove-from-cache (file)
1153   (setq gnus-score-cache 
1154         (delq (assoc file gnus-score-cache) gnus-score-cache)))
1155
1156 (defun gnus-score-load-score-alist (file)
1157   "Read score FILE."
1158   (let (alist)
1159     (if (not (file-readable-p file))
1160         ;; Couldn't read file.
1161         (setq gnus-score-alist nil)
1162       ;; Read file.
1163       (save-excursion
1164         (gnus-set-work-buffer)
1165         (insert-file-contents file)
1166         (goto-char (point-min))
1167         ;; Only do the loading if the score file isn't empty.
1168         (when (save-excursion (re-search-forward "[()0-9a-zA-Z]" nil t))
1169           (setq alist
1170                 (condition-case ()
1171                     (read (current-buffer))
1172                   (error 
1173                    (gnus-error 3.2 "Problem with score file %s" file))))))
1174       (if (eq (car alist) 'setq)
1175           ;; This is an old-style score file.
1176           (setq gnus-score-alist (gnus-score-transform-old-to-new alist))
1177         (setq gnus-score-alist alist))
1178       ;; Check the syntax of the score file.
1179       (setq gnus-score-alist
1180             (gnus-score-check-syntax gnus-score-alist file)))))
1181
1182 (defun gnus-score-check-syntax (alist file)
1183   "Check the syntax of the score ALIST."
1184   (cond 
1185    ((null alist)
1186     nil)
1187    ((not (consp alist))
1188     (gnus-message 1 "Score file is not a list: %s" file)
1189     (ding)
1190     nil)
1191    (t
1192     (let ((a alist)
1193           sr err s type)
1194       (while (and a (not err))
1195         (setq
1196          err
1197          (cond
1198           ((not (listp (car a)))
1199            (format "Illegal score element %s in %s" (car a) file))
1200           ((stringp (caar a))
1201            (cond 
1202             ((not (listp (setq sr (cdar a))))
1203              (format "Illegal header match %s in %s" (nth 1 (car a)) file))
1204             (t
1205              (setq type (caar a))
1206              (while (and sr (not err))
1207                (setq s (pop sr))
1208                (setq 
1209                 err
1210                 (cond
1211                  ((if (member (downcase type) '("lines" "chars"))
1212                       (not (numberp (car s)))
1213                     (not (stringp (car s))))
1214                   (format "Illegal match %s in %s" (car s) file))
1215                  ((and (cadr s) (not (integerp (cadr s))))
1216                   (format "Non-integer score %s in %s" (cadr s) file))
1217                  ((and (caddr s) (not (integerp (caddr s))))
1218                   (format "Non-integer date %s in %s" (caddr s) file))
1219                  ((and (cadddr s) (not (symbolp (cadddr s))))
1220                   (format "Non-symbol match type %s in %s" (cadddr s) file)))))
1221              err)))))
1222         (setq a (cdr a)))
1223       (if err
1224           (progn
1225             (ding)
1226             (gnus-message 3 err)
1227             (sit-for 2)
1228             nil)
1229         alist)))))
1230
1231 (defun gnus-score-transform-old-to-new (alist)
1232   (let* ((alist (nth 2 alist))
1233          out entry)
1234     (when (eq (car alist) 'quote)
1235       (setq alist (nth 1 alist)))
1236     (while alist
1237       (setq entry (car alist))
1238       (if (stringp (car entry))
1239           (let ((scor (cdr entry)))
1240             (push entry out)
1241             (while scor
1242               (setcar scor
1243                       (list (caar scor) (nth 2 (car scor))
1244                             (and (nth 3 (car scor))
1245                                  (gnus-day-number (nth 3 (car scor))))
1246                             (if (nth 1 (car scor)) 'r 's)))
1247               (setq scor (cdr scor))))
1248         (push (if (not (listp (cdr entry)))
1249                   (list (car entry) (cdr entry))
1250                 entry)
1251               out))
1252       (setq alist (cdr alist)))
1253     (cons (list 'touched t) (nreverse out))))
1254   
1255 (defun gnus-score-save ()
1256   ;; Save all score information.
1257   (let ((cache gnus-score-cache)
1258         entry score file)
1259     (save-excursion
1260       (setq gnus-score-alist nil)
1261       (nnheader-set-temp-buffer " *Gnus Scores*")
1262       (while cache
1263         (current-buffer)
1264         (setq entry (pop cache)
1265               file (car entry)
1266               score (cdr entry))
1267         (if (or (not (equal (gnus-score-get 'touched score) '(t)))
1268                 (gnus-score-get 'read-only score)
1269                 (and (file-exists-p file)
1270                      (not (file-writable-p file))))
1271             ()
1272           (setq score (setcdr entry (delq (assq 'touched score) score)))
1273           (erase-buffer)
1274           (let (emacs-lisp-mode-hook)
1275             (if (string-match 
1276                  (concat (regexp-quote gnus-adaptive-file-suffix)
1277                          "$")
1278                  file)
1279                 ;; This is an adaptive score file, so we do not run
1280                 ;; it through `pp'.  These files can get huge, and
1281                 ;; are not meant to be edited by human hands.
1282                 (gnus-prin1 score)
1283               ;; This is a normal score file, so we print it very
1284               ;; prettily. 
1285               (pp score (current-buffer))))
1286           (gnus-make-directory (file-name-directory file))
1287           ;; If the score file is empty, we delete it.
1288           (if (zerop (buffer-size))
1289               (delete-file file)
1290             ;; There are scores, so we write the file. 
1291             (when (file-writable-p file)
1292               (gnus-write-buffer file)
1293               (when gnus-score-after-write-file-function
1294                 (funcall gnus-score-after-write-file-function file)))))
1295         (and gnus-score-uncacheable-files
1296              (string-match gnus-score-uncacheable-files file)
1297              (gnus-score-remove-from-cache file)))
1298       (kill-buffer (current-buffer)))))
1299
1300 (defun gnus-score-load-files (score-files)
1301   "Load all score files in SCORE-FILES."
1302   ;; Load the score files.
1303   (let (scores)
1304     (while score-files
1305       (if (stringp (car score-files))
1306           ;; It is a string, which means that it's a score file name,
1307           ;; so we load the score file and add the score alist to
1308           ;; the list of alists.
1309           (setq scores (nconc (gnus-score-load-file (car score-files)) scores))
1310         ;; It is an alist, so we just add it to the list directly.
1311         (setq scores (nconc (car score-files) scores)))
1312       (setq score-files (cdr score-files)))
1313     ;; Prune the score files that are to be excluded, if any.
1314     (when gnus-scores-exclude-files
1315       (let ((s scores)
1316             c)
1317         (while s
1318           (and (setq c (rassq (car s) gnus-score-cache))
1319                (member (car c) gnus-scores-exclude-files)
1320                (setq scores (delq (car s) scores)))
1321           (setq s (cdr s)))))
1322     scores))
1323
1324 (defun gnus-score-headers (score-files &optional trace)
1325   ;; Score `gnus-newsgroup-headers'.
1326   (let (scores news)
1327     ;; PLM: probably this is not the best place to clear orphan-score
1328     (setq gnus-orphan-score nil
1329           gnus-scores-articles nil
1330           gnus-scores-exclude-files nil
1331           scores (gnus-score-load-files score-files))
1332     (setq news scores)
1333     ;; Do the scoring.
1334     (while news
1335       (setq scores news
1336             news nil)
1337       (when (and gnus-summary-default-score
1338                  scores)
1339         (let* ((entries gnus-header-index)
1340                (now (gnus-day-number (current-time-string)))
1341                (expire (and gnus-score-expiry-days
1342                             (- now gnus-score-expiry-days)))
1343                (headers gnus-newsgroup-headers)
1344                (current-score-file gnus-current-score-file)
1345                entry header new)
1346           (gnus-message 5 "Scoring...")
1347           ;; Create articles, an alist of the form `(HEADER . SCORE)'.
1348           (while (setq header (pop headers))
1349             ;; WARNING: The assq makes the function O(N*S) while it could
1350             ;; be written as O(N+S), where N is (length gnus-newsgroup-headers)
1351             ;; and S is (length gnus-newsgroup-scored).
1352             (unless (assq (mail-header-number header) gnus-newsgroup-scored)
1353               (setq gnus-scores-articles ;Total of 2 * N cons-cells used.
1354                     (cons (cons header (or gnus-summary-default-score 0))
1355                           gnus-scores-articles))))
1356
1357           (save-excursion
1358             (set-buffer (get-buffer-create "*Headers*"))
1359             (buffer-disable-undo (current-buffer))
1360
1361             ;; Set the global variant of this variable.
1362             (setq gnus-current-score-file current-score-file)
1363             ;; score orphans
1364             (when gnus-orphan-score 
1365               (setq gnus-score-index 
1366                     (nth 1 (assoc "references" gnus-header-index)))
1367               (gnus-score-orphans gnus-orphan-score))
1368             ;; Run each header through the score process.
1369             (while entries
1370               (setq entry (pop entries)
1371                     header (nth 0 entry)
1372                     gnus-score-index (nth 1 (assoc header gnus-header-index)))
1373               (when (< 0 (apply 'max (mapcar
1374                                       (lambda (score)
1375                                         (length (gnus-score-get header score)))
1376                                       scores)))
1377                 ;; Call the scoring function for this type of "header".
1378                 (when (setq new (funcall (nth 2 entry) scores header
1379                                          now expire trace))
1380                   (push new news))))
1381             ;; Remove the buffer.
1382             (kill-buffer (current-buffer)))
1383
1384           ;; Add articles to `gnus-newsgroup-scored'.
1385           (while gnus-scores-articles
1386             (when (or (/= gnus-summary-default-score
1387                           (cdar gnus-scores-articles))
1388                       gnus-save-score)
1389               (push (cons (mail-header-number (caar gnus-scores-articles))
1390                           (cdar gnus-scores-articles))
1391                     gnus-newsgroup-scored))
1392             (setq gnus-scores-articles (cdr gnus-scores-articles)))
1393
1394           (let (score)
1395             (while (setq score (pop scores))
1396               (while score
1397                 (when (listp (caar score))
1398                   (gnus-score-advanced (car score) trace))
1399                 (pop score))))
1400                 
1401           (gnus-message 5 "Scoring...done"))))))
1402
1403
1404 (defun gnus-get-new-thread-ids (articles)
1405   (let ((index (nth 1 (assoc "message-id" gnus-header-index)))
1406         (refind gnus-score-index)
1407         id-list art this tref)
1408     (while articles
1409       (setq art (car articles)
1410             this (aref (car art) index)
1411             tref (aref (car art) refind)
1412             articles (cdr articles))
1413       (when (string-equal tref "")      ;no references line
1414         (push this id-list)))
1415     id-list))
1416
1417 ;; Orphan functions written by plm@atcmp.nl (Peter Mutsaers).
1418 (defun gnus-score-orphans (score)
1419   (let ((new-thread-ids (gnus-get-new-thread-ids gnus-scores-articles))
1420         alike articles art arts this last this-id)
1421     
1422     (setq gnus-scores-articles (sort gnus-scores-articles 'gnus-score-string<)
1423           articles gnus-scores-articles)
1424
1425     ;;more or less the same as in gnus-score-string
1426     (erase-buffer)
1427     (while articles
1428       (setq art (car articles)
1429             this (aref (car art) gnus-score-index)
1430             articles (cdr articles))
1431       ;;completely skip if this is empty (not a child, so not an orphan)
1432       (when (not (string= this ""))
1433         (if (equal last this)
1434             ;; O(N*H) cons-cells used here, where H is the number of
1435             ;; headers.
1436             (push art alike)
1437           (when last
1438             ;; Insert the line, with a text property on the
1439             ;; terminating newline referring to the articles with
1440             ;; this line.
1441             (insert last ?\n)
1442             (put-text-property (1- (point)) (point) 'articles alike))
1443           (setq alike (list art)
1444                 last this))))
1445     (when last                          ; Bwadr, duplicate code.
1446       (insert last ?\n)
1447       (put-text-property (1- (point)) (point) 'articles alike))
1448
1449     ;; PLM: now delete those lines that contain an entry from new-thread-ids
1450     (while new-thread-ids
1451       (setq this-id (car new-thread-ids)
1452             new-thread-ids (cdr new-thread-ids))
1453       (goto-char (point-min))
1454       (while (search-forward this-id nil t)
1455         ;; found a match.  remove this line
1456         (beginning-of-line)
1457         (kill-line 1)))
1458
1459     ;; now for each line: update its articles with score by moving to
1460     ;; every end-of-line in the buffer and read the articles property
1461     (goto-char (point-min))
1462     (while (eq 0 (progn
1463                    (end-of-line)
1464                    (setq arts (get-text-property (point) 'articles))
1465                    (while arts
1466                      (setq art (car arts)
1467                            arts (cdr arts))
1468                      (setcdr art (+ score (cdr art))))
1469                    (forward-line))))))
1470              
1471
1472 (defun gnus-score-integer (scores header now expire &optional trace)
1473   (let ((gnus-score-index (nth 1 (assoc header gnus-header-index)))
1474         entries alist)
1475
1476     ;; Find matches.
1477     (while scores
1478       (setq alist (car scores)
1479             scores (cdr scores)
1480             entries (assoc header alist))
1481       (while (cdr entries)              ;First entry is the header index.
1482         (let* ((rest (cdr entries))
1483                (kill (car rest))
1484                (match (nth 0 kill))
1485                (type (or (nth 3 kill) '>))
1486                (score (or (nth 1 kill) gnus-score-interactive-default-score))
1487                (date (nth 2 kill))
1488                (found nil)
1489                (match-func (if (or (eq type '>) (eq type '<) (eq type '<=)
1490                                    (eq type '>=) (eq type '=))
1491                                type
1492                              (error "Illegal match type: %s" type)))
1493                (articles gnus-scores-articles))
1494           ;; Instead of doing all the clever stuff that
1495           ;; `gnus-score-string' does to minimize searches and stuff,
1496           ;; I will assume that people generally will put so few
1497           ;; matches on numbers that any cleverness will take more
1498           ;; time than one would gain.
1499           (while articles
1500             (when (funcall match-func 
1501                            (or (aref (caar articles) gnus-score-index) 0)
1502                            match)
1503               (when trace 
1504                 (push (cons (car-safe (rassq alist gnus-score-cache)) kill)
1505                       gnus-score-trace))
1506               (setq found t)
1507               (setcdr (car articles) (+ score (cdar articles))))
1508             (setq articles (cdr articles)))
1509           ;; Update expire date
1510           (cond ((null date))           ;Permanent entry.
1511                 ((and found gnus-update-score-entry-dates) ;Match, update date.
1512                  (gnus-score-set 'touched '(t) alist)
1513                  (setcar (nthcdr 2 kill) now))
1514                 ((and expire (< date expire)) ;Old entry, remove.
1515                  (gnus-score-set 'touched '(t) alist)
1516                  (setcdr entries (cdr rest))
1517                  (setq rest entries)))
1518           (setq entries rest)))))
1519   nil)
1520
1521 (defun gnus-score-date (scores header now expire &optional trace)
1522   (let ((gnus-score-index (nth 1 (assoc header gnus-header-index)))
1523         entries alist match match-func article)
1524
1525     ;; Find matches.
1526     (while scores
1527       (setq alist (car scores)
1528             scores (cdr scores)
1529             entries (assoc header alist))
1530       (while (cdr entries)              ;First entry is the header index.
1531         (let* ((rest (cdr entries))
1532                (kill (car rest))
1533                (type (or (nth 3 kill) 'before))
1534                (score (or (nth 1 kill) gnus-score-interactive-default-score))
1535                (date (nth 2 kill))
1536                (found nil)
1537                (articles gnus-scores-articles)
1538                l)
1539           (cond
1540            ((eq type 'after)
1541             (setq match-func 'string<
1542                   match (gnus-date-iso8601 (nth 0 kill))))
1543            ((eq type 'before)
1544             (setq match-func 'gnus-string>
1545                   match (gnus-date-iso8601 (nth 0 kill))))
1546            ((eq type 'at)
1547             (setq match-func 'string=
1548                   match (gnus-date-iso8601 (nth 0 kill))))
1549            ((eq type 'regexp)
1550             (setq match-func 'string-match
1551                   match (nth 0 kill)))
1552            (t (error "Illegal match type: %s" type)))
1553           ;; Instead of doing all the clever stuff that
1554           ;; `gnus-score-string' does to minimize searches and stuff,
1555           ;; I will assume that people generally will put so few
1556           ;; matches on numbers that any cleverness will take more
1557           ;; time than one would gain.
1558           (while (setq article (pop articles))
1559             (when (and
1560                    (setq l (aref (car article) gnus-score-index))
1561                    (funcall match-func match (gnus-date-iso8601 l)))
1562               (when trace
1563                 (push (cons (car-safe (rassq alist gnus-score-cache)) kill)
1564                       gnus-score-trace))
1565               (setq found t)
1566               (setcdr article (+ score (cdr article)))))
1567           ;; Update expire date
1568           (cond ((null date))           ;Permanent entry.
1569                 ((and found gnus-update-score-entry-dates) ;Match, update date.
1570                  (gnus-score-set 'touched '(t) alist)
1571                  (setcar (nthcdr 2 kill) now))
1572                 ((and expire (< date expire)) ;Old entry, remove.
1573                  (gnus-score-set 'touched '(t) alist)
1574                  (setcdr entries (cdr rest))
1575                  (setq rest entries)))
1576           (setq entries rest)))))
1577   nil)
1578
1579 (defun gnus-score-body (scores header now expire &optional trace)
1580   (save-excursion
1581     (setq gnus-scores-articles
1582           (sort gnus-scores-articles
1583                 (lambda (a1 a2)
1584                   (< (mail-header-number (car a1))
1585                      (mail-header-number (car a2))))))
1586     (set-buffer nntp-server-buffer)
1587     (save-restriction
1588       (let* ((buffer-read-only nil)
1589              (articles gnus-scores-articles)
1590              (all-scores scores)
1591              (request-func (cond ((string= "head" header)
1592                                   'gnus-request-head)
1593                                  ((string= "body" header)
1594                                   'gnus-request-body)
1595                                  (t 'gnus-request-article)))
1596              entries alist ofunc article last)
1597         (when articles
1598           (setq last (mail-header-number (caar (last articles))))
1599           ;; Not all backends support partial fetching.  In that case,
1600           ;; we just fetch the entire article.
1601           (unless (gnus-check-backend-function 
1602                    (and (string-match "^gnus-" (symbol-name request-func))
1603                         (intern (substring (symbol-name request-func)
1604                                            (match-end 0))))
1605                    gnus-newsgroup-name)
1606             (setq ofunc request-func)
1607             (setq request-func 'gnus-request-article))
1608           (while articles
1609             (setq article (mail-header-number (caar articles)))
1610             (gnus-message 7 "Scoring on article %s of %s..." article last)
1611             (when (funcall request-func article gnus-newsgroup-name)
1612               (widen)
1613               (goto-char (point-min))
1614               ;; If just parts of the article is to be searched, but the
1615               ;; backend didn't support partial fetching, we just narrow
1616               ;; to the relevant parts.
1617               (when ofunc
1618                 (if (eq ofunc 'gnus-request-head)
1619                     (narrow-to-region
1620                      (point)
1621                      (or (search-forward "\n\n" nil t) (point-max)))
1622                   (narrow-to-region
1623                    (or (search-forward "\n\n" nil t) (point))
1624                    (point-max))))
1625               (setq scores all-scores)
1626               ;; Find matches.
1627               (while scores
1628                 (setq alist (pop scores)
1629                       entries (assoc header alist))
1630                 (while (cdr entries)    ;First entry is the header index.
1631                   (let* ((rest (cdr entries))
1632                          (kill (car rest))
1633                          (match (nth 0 kill))
1634                          (type (or (nth 3 kill) 's))
1635                          (score (or (nth 1 kill)
1636                                     gnus-score-interactive-default-score))
1637                          (date (nth 2 kill))
1638                          (found nil)
1639                          (case-fold-search 
1640                           (not (or (eq type 'R) (eq type 'S)
1641                                    (eq type 'Regexp) (eq type 'String))))
1642                          (search-func 
1643                           (cond ((or (eq type 'r) (eq type 'R)
1644                                      (eq type 'regexp) (eq type 'Regexp))
1645                                  're-search-forward)
1646                                 ((or (eq type 's) (eq type 'S)
1647                                      (eq type 'string) (eq type 'String))
1648                                  'search-forward)
1649                                 (t
1650                                  (error "Illegal match type: %s" type)))))
1651                     (goto-char (point-min))
1652                     (when (funcall search-func match nil t)
1653                       ;; Found a match, update scores.
1654                       (setcdr (car articles) (+ score (cdar articles)))
1655                       (setq found t)
1656                       (when trace
1657                         (push
1658                          (cons (car-safe (rassq alist gnus-score-cache)) kill)
1659                          gnus-score-trace)))
1660                     ;; Update expire date
1661                     (unless trace
1662                       (cond
1663                        ((null date))    ;Permanent entry.
1664                        ((and found gnus-update-score-entry-dates) 
1665                         ;; Match, update date.
1666                         (gnus-score-set 'touched '(t) alist)
1667                         (setcar (nthcdr 2 kill) now))
1668                        ((and expire (< date expire)) ;Old entry, remove.
1669                         (gnus-score-set 'touched '(t) alist)
1670                         (setcdr entries (cdr rest))
1671                         (setq rest entries))))
1672                     (setq entries rest)))))
1673             (setq articles (cdr articles)))))))
1674   nil)
1675
1676 (defun gnus-score-thread (scores header now expire &optional trace)
1677   (gnus-score-followup scores header now expire trace t))
1678
1679 (defun gnus-score-followup (scores header now expire &optional trace thread)
1680   ;; Insert the unique article headers in the buffer.
1681   (let ((gnus-score-index (nth 1 (assoc header gnus-header-index)))
1682         (current-score-file gnus-current-score-file)
1683         (all-scores scores)
1684         ;; gnus-score-index is used as a free variable.
1685         alike last this art entries alist articles
1686         new news)
1687
1688     ;; Change score file to the adaptive score file.  All entries that
1689     ;; this function makes will be put into this file.
1690     (save-excursion
1691       (set-buffer gnus-summary-buffer)
1692       (gnus-score-load-file
1693        (or gnus-newsgroup-adaptive-score-file
1694            (gnus-score-file-name 
1695             gnus-newsgroup-name gnus-adaptive-file-suffix))))
1696
1697     (setq gnus-scores-articles (sort gnus-scores-articles 'gnus-score-string<)
1698           articles gnus-scores-articles)
1699
1700     (erase-buffer)
1701     (while articles
1702       (setq art (car articles)
1703             this (aref (car art) gnus-score-index)
1704             articles (cdr articles))
1705       (if (equal last this)
1706           (push art alike)
1707         (when last
1708           (insert last ?\n)
1709           (put-text-property (1- (point)) (point) 'articles alike))
1710         (setq alike (list art)
1711               last this)))
1712     (when last                          ; Bwadr, duplicate code.
1713       (insert last ?\n)
1714       (put-text-property (1- (point)) (point) 'articles alike))
1715   
1716     ;; Find matches.
1717     (while scores
1718       (setq alist (car scores)
1719             scores (cdr scores)
1720             entries (assoc header alist))
1721       (while (cdr entries)              ;First entry is the header index.
1722         (let* ((rest (cdr entries))
1723                (kill (car rest))
1724                (match (nth 0 kill))
1725                (type (or (nth 3 kill) 's))
1726                (score (or (nth 1 kill) gnus-score-interactive-default-score))
1727                (date (nth 2 kill))
1728                (found nil)
1729                (mt (aref (symbol-name type) 0))
1730                (case-fold-search 
1731                 (not (or (= mt ?R) (= mt ?S) (= mt ?E) (= mt ?F))))
1732                (dmt (downcase mt))
1733                (search-func 
1734                 (cond ((= dmt ?r) 're-search-forward)
1735                       ((or (= dmt ?e) (= dmt ?s) (= dmt ?f)) 'search-forward)
1736                       (t (error "Illegal match type: %s" type))))
1737                arts art)
1738           (goto-char (point-min))
1739           (if (= dmt ?e)
1740               (while (funcall search-func match nil t)
1741                 (and (= (progn (beginning-of-line) (point))
1742                         (match-beginning 0))
1743                      (= (progn (end-of-line) (point))
1744                         (match-end 0))
1745                      (progn
1746                        (setq found (setq arts (get-text-property 
1747                                                (point) 'articles)))
1748                        ;; Found a match, update scores.
1749                        (while arts
1750                          (setq art (car arts)
1751                                arts (cdr arts))
1752                          (gnus-score-add-followups 
1753                           (car art) score all-scores thread))))
1754                 (end-of-line))
1755             (while (funcall search-func match nil t)
1756               (end-of-line)
1757               (setq found (setq arts (get-text-property (point) 'articles)))
1758               ;; Found a match, update scores.
1759               (while (setq art (pop arts))
1760                 (when (setq new (gnus-score-add-followups
1761                                  (car art) score all-scores thread))
1762                   (push new news)))))
1763           ;; Update expire date
1764           (cond ((null date))           ;Permanent entry.
1765                 ((and found gnus-update-score-entry-dates) ;Match, update date.
1766                  (gnus-score-set 'touched '(t) alist)
1767                  (setcar (nthcdr 2 kill) now))
1768                 ((and expire (< date expire)) ;Old entry, remove.
1769                  (gnus-score-set 'touched '(t) alist)
1770                  (setcdr entries (cdr rest))
1771                  (setq rest entries)))
1772           (setq entries rest))))
1773     ;; We change the score file back to the previous one.
1774     (save-excursion
1775       (set-buffer gnus-summary-buffer)
1776       (gnus-score-load-file current-score-file))
1777     (list (cons "references" news))))
1778
1779 (defun gnus-score-add-followups (header score scores &optional thread)
1780   "Add a score entry to the adapt file."
1781   (save-excursion
1782     (set-buffer gnus-summary-buffer)
1783     (let* ((id (mail-header-id header))
1784            (scores (car scores))
1785            entry dont)
1786       ;; Don't enter a score if there already is one.
1787       (while (setq entry (pop scores))
1788         (and (equal "references" (car entry))
1789              (or (null (nth 3 (cadr entry)))
1790                  (eq 's (nth 3 (cadr entry))))
1791              (assoc id entry)
1792              (setq dont t)))
1793       (unless dont
1794         (gnus-summary-score-entry 
1795          (if thread "thread" "references")
1796          id 's score (current-time-string) nil t)))))
1797
1798 (defun gnus-score-string (score-list header now expire &optional trace)
1799   ;; Score ARTICLES according to HEADER in SCORE-LIST.
1800   ;; Update matching entries to NOW and remove unmatched entries older
1801   ;; than EXPIRE.
1802   
1803   ;; Insert the unique article headers in the buffer.
1804   (let ((gnus-score-index (nth 1 (assoc header gnus-header-index)))
1805         ;; gnus-score-index is used as a free variable.
1806         alike last this art entries alist articles 
1807         fuzzies arts words kill)
1808
1809     ;; Sorting the articles costs os O(N*log N) but will allow us to
1810     ;; only match with each unique header.  Thus the actual matching
1811     ;; will be O(M*U) where M is the number of strings to match with,
1812     ;; and U is the number of unique headers.  It is assumed (but
1813     ;; untested) this will be a net win because of the large constant
1814     ;; factor involved with string matching.
1815     (setq gnus-scores-articles (sort gnus-scores-articles 'gnus-score-string<)
1816           articles gnus-scores-articles)
1817
1818     (erase-buffer)
1819     (while (setq art (pop articles))
1820       (setq this (aref (car art) gnus-score-index))
1821       (if (equal last this)
1822           ;; O(N*H) cons-cells used here, where H is the number of
1823           ;; headers.
1824           (push art alike)
1825         (when last
1826           ;; Insert the line, with a text property on the
1827           ;; terminating newline referring to the articles with
1828           ;; this line.
1829           (insert last ?\n)
1830           (put-text-property (1- (point)) (point) 'articles alike))
1831         (setq alike (list art)
1832               last this)))
1833     (when last                          ; Bwadr, duplicate code.
1834       (insert last ?\n)
1835       (put-text-property (1- (point)) (point) 'articles alike))
1836
1837     ;; Go through all the score alists and pick out the entries
1838     ;; for this header.
1839     (while score-list
1840       (setq alist (pop score-list)
1841             ;; There's only one instance of this header for
1842             ;; each score alist.
1843             entries (assoc header alist))
1844       (while (cdr entries)              ;First entry is the header index.
1845         (let* ((kill (cadr entries))
1846                (match (nth 0 kill))
1847                (type (or (nth 3 kill) 's))
1848                (score (or (nth 1 kill) gnus-score-interactive-default-score))
1849                (date (nth 2 kill))
1850                (found nil)
1851                (mt (aref (symbol-name type) 0))
1852                (case-fold-search (not (memq mt '(?R ?S ?E ?F))))
1853                (dmt (downcase mt))
1854                (search-func 
1855                 (cond ((= dmt ?r) 're-search-forward)
1856                       ((or (= dmt ?e) (= dmt ?s) (= dmt ?f)) 'search-forward)
1857                       ((= dmt ?w) nil)
1858                       (t (error "Illegal match type: %s" type)))))
1859           (cond
1860            ;; Fuzzy matches.  We save these for later.
1861            ((= dmt ?f)
1862             (push (cons entries alist) fuzzies))
1863            ;; Word matches.  Save these for even later.
1864            ((= dmt ?w)
1865             (push (cons entries alist) words))
1866            ;; Exact matches.
1867            ((= dmt ?e)
1868             ;; Do exact matching.
1869             (goto-char (point-min))
1870             (while (and (not (eobp))
1871                         (funcall search-func match nil t))
1872               ;; Is it really exact?
1873               (and (eolp)
1874                    (= (gnus-point-at-bol) (match-beginning 0))
1875                    ;; Yup.
1876                    (progn
1877                      (setq found (setq arts (get-text-property 
1878                                              (point) 'articles)))
1879                      ;; Found a match, update scores.
1880                      (if trace
1881                          (while (setq art (pop arts))
1882                            (setcdr art (+ score (cdr art)))
1883                            (push
1884                             (cons 
1885                              (car-safe (rassq alist gnus-score-cache))
1886                              kill)
1887                             gnus-score-trace))
1888                        (while (setq art (pop arts))
1889                          (setcdr art (+ score (cdr art)))))))
1890               (forward-line 1)))
1891            ;; Regexp and substring matching.
1892            (t
1893             (goto-char (point-min))
1894             (when (string= match "")
1895               (setq match "\n"))
1896             (while (and (not (eobp))
1897                         (funcall search-func match nil t))
1898               (goto-char (match-beginning 0))
1899               (end-of-line)
1900               (setq found (setq arts (get-text-property (point) 'articles)))
1901               ;; Found a match, update scores.
1902               (if trace
1903                   (while (setq art (pop arts))
1904                     (setcdr art (+ score (cdr art)))
1905                     (push (cons (car-safe (rassq alist gnus-score-cache)) kill)
1906                           gnus-score-trace))
1907                 (while (setq art (pop arts))
1908                   (setcdr art (+ score (cdr art)))))
1909               (forward-line 1))))
1910           ;; Update expiry date
1911           (if trace
1912               (setq entries (cdr entries))
1913             (cond 
1914              ;; Permanent entry.
1915              ((null date)
1916               (setq entries (cdr entries)))
1917              ;; We have a match, so we update the date.
1918              ((and found gnus-update-score-entry-dates)
1919               (gnus-score-set 'touched '(t) alist)
1920               (setcar (nthcdr 2 kill) now)
1921               (setq entries (cdr entries)))
1922              ;; This entry has expired, so we remove it.
1923              ((and expire (< date expire))
1924               (gnus-score-set 'touched '(t) alist)
1925               (setcdr entries (cddr entries)))
1926              ;; No match; go to next entry.
1927              (t
1928               (setq entries (cdr entries))))))))
1929
1930     ;; Find fuzzy matches.
1931     (when fuzzies
1932       ;; Simplify the entire buffer for easy matching.
1933       (gnus-simplify-buffer-fuzzy)
1934       (while (setq kill (cadaar fuzzies))
1935         (let* ((match (nth 0 kill))
1936                (type (nth 3 kill))
1937                (score (or (nth 1 kill) gnus-score-interactive-default-score))
1938                (date (nth 2 kill))
1939                (mt (aref (symbol-name type) 0))
1940                (case-fold-search (not (= mt ?F)))
1941                found)
1942           (goto-char (point-min))
1943           (while (and (not (eobp))
1944                       (search-forward match nil t))
1945             (when (and (= (gnus-point-at-bol) (match-beginning 0))
1946                        (eolp))
1947               (setq found (setq arts (get-text-property (point) 'articles)))
1948               (if trace
1949                   (while (setq art (pop arts))
1950                     (setcdr art (+ score (cdr art)))
1951                     (push (cons
1952                            (car-safe (rassq (cdar fuzzies) gnus-score-cache)) 
1953                            kill)
1954                           gnus-score-trace))
1955                 ;; Found a match, update scores.
1956                 (while (setq art (pop arts))
1957                   (setcdr art (+ score (cdr art))))))
1958             (forward-line 1))
1959           ;; Update expiry date
1960           (cond
1961            ;; Permanent.
1962            ((null date)
1963             )
1964            ;; Match, update date.
1965            ((and found gnus-update-score-entry-dates)
1966             (gnus-score-set 'touched '(t) (cdar fuzzies))
1967             (setcar (nthcdr 2 kill) now))
1968            ;; Old entry, remove.
1969            ((and expire (< date expire))
1970             (gnus-score-set 'touched '(t) (cdar fuzzies))
1971             (setcdr (caar fuzzies) (cddaar fuzzies))))
1972           (setq fuzzies (cdr fuzzies)))))
1973
1974     (when words
1975       ;; Enter all words into the hashtb.
1976       (let ((hashtb (gnus-make-hashtable
1977                      (* 10 (count-lines (point-min) (point-max))))))
1978         (gnus-enter-score-words-into-hashtb hashtb)
1979         (while (setq kill (cadaar words))
1980           (let* ((score (or (nth 1 kill) gnus-score-interactive-default-score))
1981                  (date (nth 2 kill))
1982                  found)
1983             (when (setq arts (intern-soft (nth 0 kill) hashtb))
1984               (setq arts (symbol-value arts))
1985               (setq found t)
1986               (if trace
1987                   (while (setq art (pop arts))
1988                     (setcdr art (+ score (cdr art)))
1989                     (push (cons
1990                            (car-safe (rassq (cdar words) gnus-score-cache))
1991                            kill)
1992                           gnus-score-trace))
1993                 ;; Found a match, update scores.
1994                 (while (setq art (pop arts))
1995                   (setcdr art (+ score (cdr art))))))
1996             ;; Update expiry date
1997             (cond
1998              ;; Permanent.
1999              ((null date)
2000               )
2001              ;; Match, update date.
2002              ((and found gnus-update-score-entry-dates)
2003               (gnus-score-set 'touched '(t) (cdar words))
2004               (setcar (nthcdr 2 kill) now))
2005              ;; Old entry, remove.
2006              ((and expire (< date expire))
2007               (gnus-score-set 'touched '(t) (cdar words))
2008               (setcdr (caar words) (cddaar words))))
2009             (setq words (cdr words))))))
2010     nil))
2011
2012 (defun gnus-enter-score-words-into-hashtb (hashtb)
2013   ;; Find all the words in the buffer and enter them into
2014   ;; the hashtable.
2015   (let ((syntab (syntax-table))
2016         word val)
2017     (goto-char (point-min))
2018     (unwind-protect
2019         (progn
2020           (set-syntax-table gnus-adaptive-word-syntax-table)
2021           (while (re-search-forward "\\b\\w+\\b" nil t)
2022             (setq val
2023                   (gnus-gethash 
2024                    (setq word (downcase (buffer-substring
2025                                          (match-beginning 0) (match-end 0))))
2026                    hashtb))
2027             (gnus-sethash
2028              word
2029              (append (get-text-property (gnus-point-at-eol) 'articles) val)
2030              hashtb)))
2031       (set-syntax-table syntab))
2032     ;; Make all the ignorable words ignored.
2033     (let ((ignored (append gnus-ignored-adaptive-words
2034                            gnus-default-ignored-adaptive-words)))
2035       (while ignored
2036         (gnus-sethash (pop ignored) nil hashtb)))))
2037
2038 (defun gnus-score-string< (a1 a2)
2039   ;; Compare headers in articles A2 and A2.
2040   ;; The header index used is the free variable `gnus-score-index'.
2041   (string-lessp (aref (car a1) gnus-score-index)
2042                 (aref (car a2) gnus-score-index)))
2043
2044 (defun gnus-current-score-file-nondirectory (&optional score-file)
2045   (let ((score-file (or score-file gnus-current-score-file)))
2046     (if score-file 
2047         (gnus-short-group-name (file-name-nondirectory score-file))
2048       "none")))
2049
2050 (defun gnus-score-adaptive ()
2051   "Create adaptive score rules for this newsgroup."
2052   (when gnus-use-adaptive-scoring
2053     ;; We change the score file to the adaptive score file.
2054     (save-excursion
2055       (set-buffer gnus-summary-buffer)
2056       (gnus-score-load-file 
2057        (or gnus-newsgroup-adaptive-score-file
2058            (gnus-score-file-name 
2059             gnus-newsgroup-name gnus-adaptive-file-suffix))))
2060     ;; Perform ordinary line scoring.
2061     (when (or (not (listp gnus-use-adaptive-scoring))
2062               (memq 'line gnus-use-adaptive-scoring))
2063       (save-excursion
2064         (let* ((malist (gnus-copy-sequence gnus-adaptive-score-alist))
2065                (alist malist)
2066                (date (current-time-string))
2067                (data gnus-newsgroup-data)
2068                elem headers match)
2069           ;; First we transform the adaptive rule alist into something
2070           ;; that's faster to process.
2071           (while malist
2072             (setq elem (car malist))
2073             (when (symbolp (car elem))
2074               (setcar elem (symbol-value (car elem))))
2075             (setq elem (cdr elem))
2076             (while elem
2077               (setcdr (car elem)
2078                       (cons (if (eq (caar elem) 'followup)
2079                                 "references"
2080                               (symbol-name (caar elem)))
2081                             (cdar elem)))
2082               (setcar (car elem)
2083                       `(lambda (h)
2084                          (,(intern 
2085                             (concat "mail-header-" 
2086                                     (if (eq (caar elem) 'followup)
2087                                         "message-id"
2088                                       (downcase (symbol-name (caar elem))))))
2089                           h)))
2090               (setq elem (cdr elem)))
2091             (setq malist (cdr malist)))
2092           ;; Then we score away.
2093           (while data
2094             (setq elem (cdr (assq (gnus-data-mark (car data)) alist)))
2095             (if (or (not elem)
2096                     (gnus-data-pseudo-p (car data)))
2097                 ()
2098               (when (setq headers (gnus-data-header (car data)))
2099                 (while elem 
2100                   (setq match (funcall (caar elem) headers))
2101                   (gnus-summary-score-entry 
2102                    (nth 1 (car elem)) match
2103                    (cond
2104                     ((numberp match)
2105                      '=)
2106                     ((equal (nth 1 (car elem)) "date")
2107                      'a)
2108                     (t
2109                      ;; Whether we use substring or exact matches is
2110                      ;; controlled here.  
2111                      (if (or (not gnus-score-exact-adapt-limit)
2112                              (< (length match) gnus-score-exact-adapt-limit))
2113                          'e 
2114                        (if (equal (nth 1 (car elem)) "subject")
2115                            'f 's))))
2116                    (nth 2 (car elem)) date nil t)
2117                   (setq elem (cdr elem)))))
2118             (setq data (cdr data))))))
2119
2120     ;; Perform adaptive word scoring.
2121     (when (and (listp gnus-use-adaptive-scoring)
2122                (memq 'word gnus-use-adaptive-scoring))
2123       (nnheader-temp-write nil
2124         (let* ((hashtb (gnus-make-hashtable 1000))
2125                (date (gnus-day-number (current-time-string)))
2126                (data gnus-newsgroup-data)
2127                (syntab (syntax-table))
2128                word d score val)
2129           (unwind-protect
2130               (progn
2131                 (set-syntax-table gnus-adaptive-word-syntax-table)
2132                 ;; Go through all articles.
2133                 (while (setq d (pop data))
2134                   (when (and
2135                          (not (gnus-data-pseudo-p d))
2136                          (setq score
2137                                (cdr (assq 
2138                                      (gnus-data-mark d)
2139                                      gnus-adaptive-word-score-alist))))
2140                     ;; This article has a mark that should lead to
2141                     ;; adaptive word rules, so we insert the subject
2142                     ;; and find all words in that string.
2143                     (insert (mail-header-subject (gnus-data-header d)))
2144                     (downcase-region (point-min) (point-max))
2145                     (goto-char (point-min))
2146                     (while (re-search-forward "\\b\\w+\\b" nil t)
2147                       ;; Put the word and score into the hashtb.
2148                       (setq val (gnus-gethash (setq word (match-string 0))
2149                                               hashtb))
2150                       (gnus-sethash word (+ (or val 0) score) hashtb))
2151                     (erase-buffer))))
2152             (set-syntax-table syntab))
2153           ;; Make all the ignorable words ignored.
2154           (let ((ignored (append gnus-ignored-adaptive-words
2155                                  gnus-default-ignored-adaptive-words)))
2156             (while ignored
2157               (gnus-sethash (pop ignored) nil hashtb)))
2158           ;; Now we have all the words and scores, so we
2159           ;; add these rules to the ADAPT file.
2160           (set-buffer gnus-summary-buffer)
2161           (mapatoms
2162            (lambda (word)
2163              (when (symbol-value word)
2164                (gnus-summary-score-entry
2165                 "subject" (symbol-name word) 'w (symbol-value word)
2166                 date nil t)))
2167            hashtb))))))
2168
2169 (defun gnus-score-edit-done ()
2170   (let ((bufnam (buffer-file-name (current-buffer)))
2171         (winconf gnus-prev-winconf))
2172     (when winconf
2173       (set-window-configuration winconf))
2174     (gnus-score-remove-from-cache bufnam)
2175     (gnus-score-load-file bufnam)))
2176
2177 (defun gnus-score-find-trace ()
2178   "Find all score rules that applies to the current article."
2179   (interactive)
2180   (let ((gnus-newsgroup-headers
2181          (list (gnus-summary-article-header)))
2182         (gnus-newsgroup-scored nil)
2183         trace)
2184     (save-excursion
2185       (nnheader-set-temp-buffer "*Score Trace*"))
2186     (setq gnus-score-trace nil)
2187     (gnus-possibly-score-headers 'trace)
2188     (if (not (setq trace gnus-score-trace))
2189         (gnus-error 1 "No score rules apply to the current article.")
2190       (set-buffer "*Score Trace*")
2191       (gnus-add-current-to-buffer-list)
2192       (while trace
2193         (insert (format "%S  ->  %s\n" (cdar trace)
2194                         (file-name-nondirectory (caar trace))))
2195         (setq trace (cdr trace)))
2196       (goto-char (point-min))
2197       (gnus-configure-windows 'score-trace))))
2198
2199 (defun gnus-score-find-favourite-words ()
2200   "List words used in scoring."
2201   (interactive)
2202   (let ((alists (gnus-score-load-files (gnus-all-score-files)))
2203         alist rule rules kill)
2204     ;; Go through all the score alists for this group
2205     ;; and find all `w' rules.
2206     (while (setq alist (pop alists))
2207       (while (setq rule (pop alist))
2208         (when (and (stringp (car rule))
2209                    (equal "subject" (downcase (pop rule))))
2210           (while (setq kill (pop rule))
2211             (when (memq (nth 3 kill) '(w W word Word))
2212               (push (cons (or (nth 1 kill)
2213                               gnus-score-interactive-default-score)
2214                           (car kill))
2215                     rules))))))
2216     (setq rules (sort rules (lambda (r1 r2)
2217                               (string-lessp (cdr r1) (cdr r2)))))
2218     ;; Add up words that have appeared several times.
2219     (let ((r rules))
2220       (while (cdr r)
2221         (if (equal (cdar r) (cdadr r))
2222             (progn
2223               (setcar (car r) (+ (caar r) (caadr r)))
2224               (setcdr r (cddr r)))
2225           (pop r))))
2226     ;; Insert the words.
2227     (nnheader-set-temp-buffer "*Score Words*")
2228     (if (not (setq rules (sort rules (lambda (r1 r2) (> (car r1) (car r2))))))
2229         (gnus-error 3 "No word score rules")
2230       (while rules
2231         (insert (format "%-5d: %s\n" (caar rules) (cdar rules)))
2232         (pop rules))
2233       (gnus-add-current-to-buffer-list)
2234       (goto-char (point-min))
2235       (gnus-configure-windows 'score-words))))
2236
2237 (defun gnus-summary-rescore ()
2238   "Redo the entire scoring process in the current summary."
2239   (interactive)
2240   (gnus-score-save)
2241   (setq gnus-score-cache nil)
2242   (setq gnus-newsgroup-scored nil)
2243   (gnus-possibly-score-headers)
2244   (gnus-score-update-all-lines))
2245   
2246 (defun gnus-score-flush-cache ()
2247   "Flush the cache of score files."
2248   (interactive)
2249   (gnus-score-save)
2250   (setq gnus-score-cache nil
2251         gnus-score-alist nil
2252         gnus-short-name-score-file-cache nil)
2253   (gnus-message 6 "The score cache is now flushed"))
2254
2255 (gnus-add-shutdown 'gnus-score-close 'gnus)
2256
2257 (defvar gnus-score-file-alist-cache nil)
2258
2259 (defun gnus-score-close ()
2260   "Clear all internal score variables."
2261   (setq gnus-score-cache nil
2262         gnus-internal-global-score-files nil
2263         gnus-score-file-list nil
2264         gnus-score-file-alist-cache nil))
2265
2266 ;; Summary score marking commands.
2267
2268 (defun gnus-summary-raise-same-subject-and-select (score)
2269   "Raise articles which has the same subject with SCORE and select the next."
2270   (interactive "p")
2271   (let ((subject (gnus-summary-article-subject)))
2272     (gnus-summary-raise-score score)
2273     (while (gnus-summary-find-subject subject)
2274       (gnus-summary-raise-score score))
2275     (gnus-summary-next-article t)))
2276
2277 (defun gnus-summary-raise-same-subject (score)
2278   "Raise articles which has the same subject with SCORE."
2279   (interactive "p")
2280   (let ((subject (gnus-summary-article-subject)))
2281     (gnus-summary-raise-score score)
2282     (while (gnus-summary-find-subject subject)
2283       (gnus-summary-raise-score score))
2284     (gnus-summary-next-subject 1 t)))
2285
2286 (defun gnus-score-default (level)
2287   (if level (prefix-numeric-value level)
2288     gnus-score-interactive-default-score))
2289
2290 (defun gnus-summary-raise-thread (&optional score)
2291   "Raise the score of the articles in the current thread with SCORE."
2292   (interactive "P")
2293   (setq score (gnus-score-default score))
2294   (let (e)
2295     (save-excursion
2296       (let ((articles (gnus-summary-articles-in-thread)))
2297         (while articles
2298           (gnus-summary-goto-subject (car articles))
2299           (gnus-summary-raise-score score)
2300           (setq articles (cdr articles))))
2301       (setq e (point)))
2302     (let ((gnus-summary-check-current t))
2303       (unless (zerop (gnus-summary-next-subject 1 t))
2304         (goto-char e))))
2305   (gnus-summary-recenter)
2306   (gnus-summary-position-point)
2307   (gnus-set-mode-line 'summary))
2308
2309 (defun gnus-summary-lower-same-subject-and-select (score)
2310   "Raise articles which has the same subject with SCORE and select the next."
2311   (interactive "p")
2312   (gnus-summary-raise-same-subject-and-select (- score)))
2313
2314 (defun gnus-summary-lower-same-subject (score)
2315   "Raise articles which has the same subject with SCORE."
2316   (interactive "p")
2317   (gnus-summary-raise-same-subject (- score)))
2318
2319 (defun gnus-summary-lower-thread (&optional score)
2320   "Lower score of articles in the current thread with SCORE."
2321   (interactive "P")
2322   (gnus-summary-raise-thread (- (1- (gnus-score-default score)))))
2323
2324 ;;; Finding score files. 
2325
2326 (defun gnus-score-score-files (group)
2327   "Return a list of all possible score files."
2328   ;; Search and set any global score files.
2329   (when gnus-global-score-files 
2330     (unless gnus-internal-global-score-files
2331       (gnus-score-search-global-directories gnus-global-score-files)))
2332   ;; Fix the kill-file dir variable.
2333   (setq gnus-kill-files-directory 
2334         (file-name-as-directory gnus-kill-files-directory))
2335   ;; If we can't read it, there are no score files.
2336   (if (not (file-exists-p (expand-file-name gnus-kill-files-directory)))
2337       (setq gnus-score-file-list nil)
2338     (if (not (gnus-use-long-file-name 'not-score))
2339         ;; We do not use long file names, so we have to do some
2340         ;; directory traversing.  
2341         (setq gnus-score-file-list 
2342               (cons nil 
2343                     (or gnus-short-name-score-file-cache
2344                         (prog2
2345                             (gnus-message 6 "Finding all score files...")
2346                             (setq gnus-short-name-score-file-cache
2347                                   (gnus-score-score-files-1
2348                                    gnus-kill-files-directory))
2349                           (gnus-message 6 "Finding all score files...done")))))
2350       ;; We want long file names.
2351       (when (or (not gnus-score-file-list)
2352                 (not (car gnus-score-file-list))
2353                 (gnus-file-newer-than gnus-kill-files-directory
2354                                       (car gnus-score-file-list)))
2355         (setq gnus-score-file-list 
2356               (cons (nth 5 (file-attributes gnus-kill-files-directory))
2357                     (nreverse 
2358                      (directory-files 
2359                       gnus-kill-files-directory t 
2360                       (gnus-score-file-regexp)))))))
2361     (cdr gnus-score-file-list)))
2362
2363 (defun gnus-score-score-files-1 (dir)
2364   "Return all possible score files under DIR."
2365   (let ((files (list (expand-file-name dir)))
2366         (regexp (gnus-score-file-regexp))
2367         (case-fold-search nil)
2368         seen out file)
2369     (while (setq file (pop files))
2370       (cond 
2371        ;; Ignore "." and "..".
2372        ((member (file-name-nondirectory file) '("." ".."))
2373         nil)
2374        ;; Add subtrees of directory to also be searched.
2375        ((and (file-directory-p file)
2376              (not (member (file-truename file) seen)))
2377         (push (file-truename file) seen)
2378         (setq files (nconc (directory-files file t nil t) files)))
2379        ;; Add files to the list of score files.
2380        ((string-match regexp file)
2381         (push file out))))
2382     (or out
2383         ;; Return a dummy value.
2384         (list "~/News/this.file.does.not.exist.SCORE"))))
2385        
2386 (defun gnus-score-file-regexp ()
2387   "Return a regexp that match all score files."
2388   (concat "\\(" (regexp-quote gnus-score-file-suffix )
2389           "\\|" (regexp-quote gnus-adaptive-file-suffix) "\\)\\'"))
2390         
2391 (defun gnus-score-find-bnews (group)
2392   "Return a list of score files for GROUP.
2393 The score files are those files in the ~/News/ directory which matches
2394 GROUP using BNews sys file syntax."
2395   (let* ((sfiles (append (gnus-score-score-files group)
2396                          gnus-internal-global-score-files))
2397          (kill-dir (file-name-as-directory 
2398                     (expand-file-name gnus-kill-files-directory)))
2399          (klen (length kill-dir))
2400          (score-regexp (gnus-score-file-regexp))
2401          (trans (cdr (assq ?: nnheader-file-name-translation-alist)))
2402          ofiles not-match regexp)
2403     (save-excursion
2404       (set-buffer (get-buffer-create "*gnus score files*"))
2405       (buffer-disable-undo (current-buffer))
2406       ;; Go through all score file names and create regexp with them
2407       ;; as the source.  
2408       (while sfiles
2409         (erase-buffer)
2410         (insert (car sfiles))
2411         (goto-char (point-min))
2412         ;; First remove the suffix itself.
2413         (when (re-search-forward (concat "." score-regexp) nil t)
2414           (replace-match "" t t)
2415           (goto-char (point-min))
2416           (if (looking-at (regexp-quote kill-dir))
2417               ;; If the file name was just "SCORE", `klen' is one character
2418               ;; too much.
2419               (delete-char (min (1- (point-max)) klen))
2420             (goto-char (point-max))
2421             (search-backward "/")
2422             (delete-region (1+ (point)) (point-min)))
2423           ;; If short file names were used, we have to translate slashes.
2424           (goto-char (point-min))
2425           (let ((regexp (concat
2426                          "[/:" (if trans (char-to-string trans) "") "]")))
2427             (while (re-search-forward regexp nil t)
2428               (replace-match "." t t)))
2429           ;; Kludge to get rid of "nntp+" problems.
2430           (goto-char (point-min))
2431           (when (looking-at "nn[a-z]+\\+")
2432             (search-forward "+")
2433             (forward-char -1)
2434             (insert "\\")
2435             (forward-char 1))
2436           ;; Kludge to deal with "++".
2437           (while (search-forward "+" nil t)
2438             (replace-match "\\+" t t))
2439           ;; Translate "all" to ".*".
2440           (goto-char (point-min))
2441           (while (search-forward "all" nil t)
2442             (replace-match ".*" t t))
2443           (goto-char (point-min))
2444           ;; Deal with "not."s.
2445           (if (looking-at "not.")
2446               (progn
2447                 (setq not-match t)
2448                 (setq regexp (concat "^" (buffer-substring 5 (point-max)))))
2449             (setq regexp (concat "^" (buffer-substring 1 (point-max))))
2450             (setq not-match nil))
2451           ;; Finally - if this resulting regexp matches the group name,
2452           ;; we add this score file to the list of score files
2453           ;; applicable to this group.
2454           (when (or (and not-match
2455                          (not (string-match regexp group)))
2456                     (and (not not-match)
2457                          (string-match regexp group)))
2458             (push (car sfiles) ofiles)))
2459         (setq sfiles (cdr sfiles)))
2460       (kill-buffer (current-buffer))
2461       ;; Slight kludge here - the last score file returned should be
2462       ;; the local score file, whether it exists or not.  This is so
2463       ;; that any score commands the user enters will go to the right
2464       ;; file, and not end up in some global score file.
2465       (let ((localscore (gnus-score-file-name group)))
2466         (setq ofiles (cons localscore (delete localscore ofiles))))
2467       (gnus-sort-score-files (nreverse ofiles)))))
2468
2469 (defun gnus-score-find-single (group)
2470   "Return list containing the score file for GROUP."
2471   (list (or gnus-newsgroup-adaptive-score-file
2472             (gnus-score-file-name group gnus-adaptive-file-suffix))
2473         (gnus-score-file-name group)))
2474
2475 (defun gnus-score-find-hierarchical (group)
2476   "Return list of score files for GROUP.
2477 This includes the score file for the group and all its parents."
2478   (let* ((prefix (gnus-group-real-prefix group))
2479          (all (list nil))
2480          (group (gnus-group-real-name group))
2481          (start 0))
2482     (while (string-match "\\." group (1+ start))
2483       (setq start (match-beginning 0))
2484       (push (substring group 0 start) all))
2485     (push group all)
2486     (setq all
2487           (nconc
2488            (mapcar (lambda (group)
2489                      (gnus-score-file-name group gnus-adaptive-file-suffix))
2490                    (setq all (nreverse all)))
2491            (mapcar 'gnus-score-file-name all)))
2492     (if (equal prefix "")
2493         all
2494       (mapcar 
2495        (lambda (file)
2496          (concat (file-name-directory file) prefix
2497                  (file-name-nondirectory file)))
2498        all))))
2499
2500 (defun gnus-score-file-rank (file)
2501   "Return a number that says how specific score FILE is.
2502 Destroys the current buffer."
2503   (when (string-match
2504          (concat "^" (regexp-quote
2505                       (expand-file-name
2506                        (file-name-as-directory gnus-kill-files-directory))))
2507          file)
2508     (setq file (substring file (match-end 0))))
2509   (insert file)
2510   (goto-char (point-min))
2511   (let ((beg (point))
2512         elems)
2513     (while (re-search-forward "[./]" nil t)
2514       (push (buffer-substring beg (1- (point)))
2515             elems))
2516     (erase-buffer)
2517     (setq elems (delete "all" elems))
2518     (length elems)))
2519     
2520 (defun gnus-sort-score-files (files)
2521   "Sort FILES so that the most general files come first."
2522   (nnheader-temp-write nil
2523     (let ((alist
2524            (mapcar
2525             (lambda (file)
2526               (cons (inline (gnus-score-file-rank file)) file))
2527             files)))
2528       (mapcar
2529        (lambda (f) (cdr f))
2530        (sort alist (lambda (f1 f2) (< (car f1) (car f2))))))))
2531
2532 (defun gnus-score-find-alist (group)
2533   "Return list of score files for GROUP.
2534 The list is determined from the variable gnus-score-file-alist."
2535   (let ((alist gnus-score-file-multiple-match-alist)
2536         score-files)
2537     ;; if this group has been seen before, return the cached entry
2538     (if (setq score-files (assoc group gnus-score-file-alist-cache))
2539         (cdr score-files)               ;ensures caching groups with no matches
2540       ;; handle the multiple match alist
2541       (while alist
2542         (when (string-match (caar alist) group)
2543           (setq score-files
2544                 (nconc score-files (copy-sequence (cdar alist)))))
2545         (setq alist (cdr alist)))
2546       (setq alist gnus-score-file-single-match-alist)
2547       ;; handle the single match alist
2548       (while alist
2549         (when (string-match (caar alist) group)
2550           ;; progn used just in case ("regexp") has no files
2551           ;; and score-files is still nil.  -sj
2552           ;; this can be construed as a "stop searching here" feature :>
2553           ;; and used to simplify regexps in the single-alist 
2554           (setq score-files
2555                 (nconc score-files (copy-sequence (cdar alist))))
2556           (setq alist nil))
2557         (setq alist (cdr alist)))
2558       ;; cache the score files
2559       (push (cons group score-files) gnus-score-file-alist-cache)
2560       score-files)))
2561
2562 (defun gnus-all-score-files (&optional group)
2563   "Return a list of all score files for the current group."
2564   (let ((funcs gnus-score-find-score-files-function)
2565         (group (or group gnus-newsgroup-name))
2566         score-files)
2567     ;; Make sure funcs is a list.
2568     (and funcs
2569          (not (listp funcs))
2570          (setq funcs (list funcs)))
2571     ;; Get the initial score files for this group.
2572     (when funcs 
2573       (setq score-files (nreverse (gnus-score-find-alist group))))
2574     ;; Add any home adapt files.
2575     (let ((home (gnus-home-score-file group t)))
2576       (when home
2577         (push home score-files)
2578         (setq gnus-newsgroup-adaptive-score-file home)))
2579     ;; Check whether there is a `adapt-file' group parameter.
2580     (let ((param-file (gnus-group-find-parameter group 'adapt-file)))
2581       (when param-file
2582         (push param-file score-files)
2583         (setq gnus-newsgroup-adaptive-score-file param-file)))
2584     ;; Go through all the functions for finding score files (or actual
2585     ;; scores) and add them to a list.
2586     (while funcs
2587       (when (gnus-functionp (car funcs))
2588         (setq score-files 
2589               (nconc score-files (nreverse (funcall (car funcs) group)))))
2590       (setq funcs (cdr funcs)))
2591     ;; Add any home score files.
2592     (let ((home (gnus-home-score-file group)))
2593       (when home
2594         (push home score-files)))
2595     ;; Check whether there is a `score-file' group parameter.
2596     (let ((param-file (gnus-group-find-parameter group 'score-file)))
2597       (when param-file
2598         (push param-file score-files)))
2599     ;; Expand all files names.
2600     (let ((files score-files))
2601       (while files
2602         (when (stringp (car files))
2603           (setcar files (expand-file-name (car files) 
2604                                           gnus-kill-files-directory)))
2605         (pop files)))
2606     (setq score-files (nreverse score-files))
2607     ;; Remove any duplicate score files.
2608     (while (and score-files
2609                 (member (car score-files) (cdr score-files)))
2610       (pop score-files))
2611     (let ((files score-files))
2612       (while (cdr files)
2613         (when (member (cadr files) (cddr files))
2614           (setcdr files (cddr files)))
2615         (pop files)))
2616     ;; Do the scoring if there are any score files for this group.
2617     score-files))
2618     
2619 (defun gnus-possibly-score-headers (&optional trace)
2620   "Do scoring if scoring is required."
2621   (let ((score-files (gnus-all-score-files)))
2622     (when score-files
2623       (gnus-score-headers score-files trace))))
2624
2625 (defun gnus-score-file-name (newsgroup &optional suffix)
2626   "Return the name of a score file for NEWSGROUP."
2627   (let ((suffix (or suffix gnus-score-file-suffix)))
2628     (nnheader-translate-file-chars
2629      (cond
2630       ((or (null newsgroup)
2631            (string-equal newsgroup ""))
2632        ;; The global score file is placed at top of the directory.
2633        (expand-file-name 
2634         suffix gnus-kill-files-directory))
2635       ((gnus-use-long-file-name 'not-score)
2636        ;; Append ".SCORE" to newsgroup name.
2637        (expand-file-name (concat (gnus-newsgroup-savable-name newsgroup)
2638                                  "." suffix)
2639                          gnus-kill-files-directory))
2640       (t
2641        ;; Place "SCORE" under the hierarchical directory.
2642        (expand-file-name (concat (gnus-newsgroup-directory-form newsgroup)
2643                                  "/" suffix)
2644                          gnus-kill-files-directory))))))
2645
2646 (defun gnus-score-search-global-directories (files)
2647   "Scan all global score directories for score files."
2648   ;; Set the variable `gnus-internal-global-score-files' to all
2649   ;; available global score files.
2650   (interactive (list gnus-global-score-files))
2651   (let (out)
2652     (while files
2653       (if (string-match "/$" (car files))
2654           (setq out (nconc (directory-files 
2655                             (car files) t
2656                             (concat (gnus-score-file-regexp) "$"))))
2657         (push (car files) out))
2658       (setq files (cdr files)))
2659     (setq gnus-internal-global-score-files out)))
2660
2661 (defun gnus-score-default-fold-toggle ()
2662   "Toggle folding for new score file entries."
2663   (interactive)
2664   (setq gnus-score-default-fold (not gnus-score-default-fold))
2665   (if gnus-score-default-fold
2666       (gnus-message 1 "New score file entries will be case insensitive.")
2667     (gnus-message 1 "New score file entries will be case sensitive.")))
2668
2669 ;;; Home score file.
2670
2671 (defun gnus-home-score-file (group &optional adapt)
2672   "Return the home score file for GROUP.
2673 If ADAPT, return the home adaptive file instead."
2674   (let ((list (if adapt gnus-home-adapt-file gnus-home-score-file))
2675         elem found)
2676     ;; Make sure we have a list.
2677     (unless (listp list)
2678       (setq list (list list)))
2679     ;; Go through the list and look for matches.
2680     (while (and (not found)
2681                 (setq elem (pop list)))
2682       (setq found
2683             (cond
2684              ;; Simple string.
2685              ((stringp elem)
2686               elem)
2687              ;; Function.
2688              ((gnus-functionp elem)
2689               (funcall elem group))
2690              ;; Regexp-file cons
2691              ((consp elem)
2692               (when (string-match (car elem) group)
2693                 (cadr elem))))))
2694     (when found
2695       (nnheader-concat gnus-kill-files-directory found))))
2696
2697 (defun gnus-hierarchial-home-score-file (group)
2698   "Return the score file of the top-level hierarchy of GROUP."
2699   (if (string-match "^[^.]+\\." group)
2700       (concat (match-string 0 group) gnus-score-file-suffix)
2701     ;; Group name without any dots.
2702     (concat group "." gnus-score-file-suffix)))
2703       
2704 (defun gnus-hierarchial-home-adapt-file (group)
2705   "Return the adapt file of the top-level hierarchy of GROUP."
2706   (if (string-match "^[^.]+\\." group)
2707       (concat (match-string 0 group) gnus-adaptive-file-suffix)
2708     ;; Group name without any dots.
2709     (concat group "." gnus-adaptive-file-suffix)))
2710
2711 ;;;
2712 ;;; Score decays
2713 ;;;
2714
2715 (defun gnus-decay-score (score)
2716   "Decay SCORE."
2717   (floor
2718    (- score
2719       (* (if (< score 0) 1 -1)
2720          (min score
2721               (max gnus-score-decay-constant
2722                    (* (abs score)
2723                       gnus-score-decay-scale)))))))
2724
2725 (defun gnus-decay-scores (alist day)
2726   "Decay non-permanent scores in ALIST."
2727   (let ((times (- (gnus-time-to-day (current-time)) day))
2728         kill entry updated score n)
2729     (unless (zerop times)               ;Done decays today already?
2730       (while (setq entry (pop alist))
2731         (when (stringp (car entry))
2732           (setq entry (cdr entry))
2733           (while (setq kill (pop entry))
2734             (when (nth 2 kill)
2735               (setq updated t)
2736               (setq score (or (car kill) gnus-score-interactive-default-score)
2737                     n times)
2738               (while (natnump (decf n))
2739                 (setq score (funcall gnus-decay-score-function score)))
2740               (setcar kill score))))))
2741     ;; Return whether this score file needs to be saved.  By Je-haysuss!
2742     updated))
2743
2744 (provide 'gnus-score)
2745
2746 ;;; gnus-score.el ends here