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