29d0df53ea040df5ee4d9bb083342c94910853b5
[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 (autoload 'ffap-string-at-point "ffap")
40
41 (defcustom gnus-global-score-files nil
42   "List of global score files and directories.
43 Set this variable if you want to use people's score files.  One entry
44 for each score file or each score file directory.  Gnus will decide
45 by itself what score files are applicable to which group.
46
47 Say you want to use the single score file
48 \"/ftp.gnus.org@ftp:/pub/larsi/ding/score/soc.motss.SCORE\" and all
49 score files in the \"/ftp.some-where:/pub/score\" directory.
50
51  (setq gnus-global-score-files
52        '(\"/ftp.gnus.org:/pub/larsi/ding/score/soc.motss.SCORE\"
53          \"/ftp.some-where:/pub/score\"))"
54   :group 'gnus-score-files
55   :type '(repeat file))
56
57 (defcustom gnus-score-file-single-match-alist nil
58   "Alist mapping regexps to lists of score files.
59 Each element of this alist should be of the form
60         (\"REGEXP\" [ \"SCORE-FILE-1\" ] [ \"SCORE-FILE-2\" ] ... )
61
62 If the name of a group is matched by REGEXP, the corresponding scorefiles
63 will be used for that group.
64 The first match found is used, subsequent matching entries are ignored (to
65 use multiple matches, see `gnus-score-file-multiple-match-alist').
66
67 These score files are loaded in addition to any files returned by
68 `gnus-score-find-score-files-function'."
69   :group 'gnus-score-files
70   :type '(repeat (cons regexp (repeat file))))
71
72 (defcustom gnus-score-file-multiple-match-alist nil
73   "Alist mapping regexps to lists of score files.
74 Each element of this alist should be of the form
75         (\"REGEXP\" [ \"SCORE-FILE-1\" ] [ \"SCORE-FILE-2\" ] ... )
76
77 If the name of a group is matched by REGEXP, the corresponding scorefiles
78 will be used for that group.
79 If multiple REGEXPs match a group, the score files corresponding to each
80 match will be used (for only one match to be used, see
81 `gnus-score-file-single-match-alist').
82
83 These score files are loaded in addition to any files returned by
84 `gnus-score-find-score-files-function'."
85   :group 'gnus-score-files
86   :type '(repeat (cons regexp (repeat file))))
87
88 (defcustom gnus-score-file-suffix "SCORE"
89   "Suffix of the score files."
90   :group 'gnus-score-files
91   :type 'string)
92
93 (defcustom gnus-adaptive-file-suffix "ADAPT"
94   "Suffix of the adaptive score files."
95   :group 'gnus-score-files
96   :group 'gnus-score-adapt
97   :type 'string)
98
99 (defcustom gnus-score-find-score-files-function 'gnus-score-find-bnews
100   "Function used to find score files.
101 The function will be called with the group name as the argument, and
102 should return a list of score files to apply to that group.  The score
103 files do not actually have to exist.
104
105 Predefined values are:
106
107 `gnus-score-find-single': Only apply the group's own score file.
108 `gnus-score-find-hierarchical': Also apply score files from parent groups.
109 `gnus-score-find-bnews': Apply score files whose names matches.
110
111 See the documentation to these functions for more information.
112
113 This variable can also be a list of functions to be called.  Each
114 function is given the group name as argument and should either return
115 a list of score files, or a list of score alists.
116
117 If functions other than these pre-defined functions are used,
118 the `a' symbolic prefix to the score commands will always use
119 \"all.SCORE\"."
120   :group 'gnus-score-files
121   :type '(radio (function-item gnus-score-find-single)
122                 (function-item gnus-score-find-hierarchical)
123                 (function-item gnus-score-find-bnews)
124                 (repeat :tag "List of functions"
125                         (choice (function :tag "Other" :value 'ignore)
126                                 (function-item gnus-score-find-single)
127                                 (function-item gnus-score-find-hierarchical)
128                                 (function-item gnus-score-find-bnews)))
129                 (function :tag "Other" :value 'ignore)))
130
131 (defcustom gnus-score-interactive-default-score 1000
132   "*Scoring commands will raise/lower the score with this number as the default."
133   :group 'gnus-score-default
134   :type 'integer)
135
136 (defcustom gnus-score-expiry-days 7
137   "*Number of days before unused score file entries are expired.
138 If this variable is nil, no score file entries will be expired."
139   :group 'gnus-score-expire
140   :type '(choice (const :tag "never" nil)
141                  number))
142
143 (defcustom gnus-update-score-entry-dates t
144   "*In non-nil, update matching score entry dates.
145 If this variable is nil, then score entries that provide matches
146 will be expired along with non-matching score entries."
147   :group 'gnus-score-expire
148   :type 'boolean)
149
150 (defcustom gnus-decay-scores nil
151   "*If non-nil, decay non-permanent scores."
152   :group 'gnus-score-decay
153   :type 'boolean)
154
155 (defcustom gnus-decay-score-function 'gnus-decay-score
156   "*Function called to decay a score.
157 It is called with one parameter -- the score to be decayed."
158   :group 'gnus-score-decay
159   :type '(radio (function-item gnus-decay-score)
160                 (function :tag "Other")))
161
162 (defcustom gnus-score-decay-constant 3
163   "*Decay all \"small\" scores with this amount."
164   :group 'gnus-score-decay
165   :type 'integer)
166
167 (defcustom gnus-score-decay-scale .05
168   "*Decay all \"big\" scores with this factor."
169   :group 'gnus-score-decay
170   :type 'number)
171
172 (defcustom gnus-home-score-file nil
173   "Variable to control where interactive score entries are to go.
174 It can be:
175
176  * A string
177    This file file will be used as the home score file.
178
179  * A function
180    The result of this function will be used as the home score file.
181    The function will be passed the name of the group as its
182    parameter.
183
184  * A list
185    The elements in this list can be:
186
187    * `(regexp file-name ...)'
188      If the `regexp' matches the group name, the first `file-name' will
189      will be used as the home score file.  (Multiple filenames are
190      allowed so that one may use gnus-score-file-single-match-alist to
191      set this variable.)
192
193    * A function.
194      If the function returns non-nil, the result will be used
195      as the home score file.  The function will be passed the
196      name of the group as its parameter.
197
198    * A string.  Use the string as the home score file.
199
200    The list will be traversed from the beginning towards the end looking
201    for matches."
202   :group 'gnus-score-files
203   :type '(choice string
204                  (repeat (choice string
205                                  (cons regexp (repeat file))
206                                  (function :value fun)))
207                  (function-item gnus-hierarchial-home-score-file)
208                  (function-item gnus-current-home-score-file)
209                  (function :value fun)))
210
211 (defcustom gnus-home-adapt-file nil
212   "Variable to control where new adaptive score entries are to go.
213 This variable allows the same syntax as `gnus-home-score-file'."
214   :group 'gnus-score-adapt
215   :group 'gnus-score-files
216   :type '(choice string
217                  (repeat (choice string
218                                  (cons regexp (repeat file))
219                                  (function :value fun)))
220                  (function :value fun)))
221
222 (defcustom gnus-default-adaptive-score-alist
223   '((gnus-kill-file-mark)
224     (gnus-unread-mark)
225     (gnus-read-mark (from 3) (subject 30))
226     (gnus-catchup-mark (subject -10))
227     (gnus-killed-mark (from -1) (subject -20))
228     (gnus-del-mark (from -2) (subject -15)))
229   "*Alist of marks and scores."
230   :group 'gnus-score-adapt
231   :type '(repeat (cons (symbol :tag "Mark")
232                        (repeat (list (choice :tag "Header"
233                                              (const from)
234                                              (const subject)
235                                              (symbol :tag "other"))
236                                      (integer :tag "Score"))))))
237
238 (defcustom gnus-adaptive-word-length-limit nil
239   "*Words of a length lesser than this limit will be ignored when doing adaptive scoring."
240   :group 'gnus-score-adapt
241   :type '(radio (const :format "Unlimited " nil)
242                 (integer :format "Maximum length: %v\n" :size 0)))
243
244 (defcustom gnus-ignored-adaptive-words nil
245   "List of words to be ignored when doing adaptive word scoring."
246   :group 'gnus-score-adapt
247   :type '(repeat string))
248
249 (defcustom gnus-default-ignored-adaptive-words
250   '("a" "i" "the" "to" "of" "and" "in" "is" "it" "for" "that" "if" "you"
251     "this" "be" "on" "with" "not" "have" "are" "or" "as" "from" "can"
252     "but" "by" "at" "an" "will" "no" "all" "was" "do" "there" "my" "one"
253     "so" "we" "they" "what" "would" "any" "which" "about" "get" "your"
254     "use" "some" "me" "then" "name" "like" "out" "when" "up" "time"
255     "other" "more" "only" "just" "end" "also" "know" "how" "new" "should"
256     "been" "than" "them" "he" "who" "make" "may" "people" "these" "now"
257     "their" "here" "into" "first" "could" "way" "had" "see" "work" "well"
258     "were" "two" "very" "where" "while" "us" "because" "good" "same"
259     "even" "much" "most" "many" "such" "long" "his" "over" "last" "since"
260     "right" "before" "our" "without" "too" "those" "why" "must" "part"
261     "being" "current" "back" "still" "go" "point" "value" "each" "did"
262     "both" "true" "off" "say" "another" "state" "might" "under" "start"
263     "try" "re")
264   "*Default list of words to be ignored when doing adaptive word scoring."
265   :group 'gnus-score-adapt
266   :type '(repeat string))
267
268 (defcustom gnus-default-adaptive-word-score-alist
269   `((,gnus-read-mark . 30)
270     (,gnus-catchup-mark . -10)
271     (,gnus-killed-mark . -20)
272     (,gnus-del-mark . -15))
273   "*Alist of marks and scores."
274   :group 'gnus-score-adapt
275   :type '(repeat (cons (character :tag "Mark")
276                        (integer :tag "Score"))))
277
278 (defcustom gnus-adaptive-word-minimum nil
279   "If a number, this is the minimum score value that can be assigned to a word."
280   :group 'gnus-score-adapt
281   :type '(choice (const nil) integer))
282
283 (defcustom gnus-adaptive-word-no-group-words nil
284   "If t, don't adaptively score words included in the group name."
285   :group 'gnus-score-adapt
286   :type 'boolean)
287
288 (defcustom gnus-score-mimic-keymap nil
289   "*Have the score entry functions pretend that they are a keymap."
290   :group 'gnus-score-default
291   :type 'boolean)
292
293 (defcustom gnus-score-exact-adapt-limit 10
294   "*Number that says how long a match has to be before using substring matching.
295 When doing adaptive scoring, one normally uses fuzzy or substring
296 matching.  However, if the header one matches is short, the possibility
297 for false positives is great, so if the length of the match is less
298 than this variable, exact matching will be used.
299
300 If this variable is nil, exact matching will always be used."
301   :group 'gnus-score-adapt
302   :type '(choice (const nil) integer))
303
304 (defcustom gnus-score-uncacheable-files "ADAPT$"
305   "All score files that match this regexp will not be cached."
306   :group 'gnus-score-adapt
307   :group 'gnus-score-files
308   :type 'regexp)
309
310 (defcustom gnus-score-default-header nil
311   "Default header when entering new scores.
312
313 Should be one of the following symbols.
314
315  a: from
316  s: subject
317  b: body
318  h: head
319  i: message-id
320  t: references
321  x: xref
322  e: `extra' (non-standard overview)
323  l: lines
324  d: date
325  f: followup
326
327 If nil, the user will be asked for a header."
328   :group 'gnus-score-default
329   :type '(choice (const :tag "from" a)
330                  (const :tag "subject" s)
331                  (const :tag "body" b)
332                  (const :tag "head" h)
333                  (const :tag "message-id" i)
334                  (const :tag "references" t)
335                  (const :tag "xref" x)
336                  (const :tag "extra" e)
337                  (const :tag "lines" l)
338                  (const :tag "date" d)
339                  (const :tag "followup" f)
340                  (const :tag "ask" nil)))
341
342 (defcustom gnus-score-default-type nil
343   "Default match type when entering new scores.
344
345 Should be one of the following symbols.
346
347  s: substring
348  e: exact string
349  f: fuzzy string
350  r: regexp string
351  b: before date
352  a: after date
353  n: this date
354  <: less than number
355  >: greater than number
356  =: equal to number
357
358 If nil, the user will be asked for a match type."
359   :group 'gnus-score-default
360   :type '(choice (const :tag "substring" s)
361                  (const :tag "exact string" e)
362                  (const :tag "fuzzy string" f)
363                  (const :tag "regexp string" r)
364                  (const :tag "before date" b)
365                  (const :tag "after date" a)
366                  (const :tag "this date" n)
367                  (const :tag "less than number" <)
368                  (const :tag "greater than number" >)
369                  (const :tag "equal than number" =)
370                  (const :tag "ask" nil)))
371
372 (defcustom gnus-score-default-fold nil
373   "Use case folding for new score file entries iff not nil."
374   :group 'gnus-score-default
375   :type 'boolean)
376
377 (defcustom gnus-score-default-duration nil
378   "Default duration of effect when entering new scores.
379
380 Should be one of the following symbols.
381
382  t: temporary
383  p: permanent
384  i: immediate
385
386 If nil, the user will be asked for a duration."
387   :group 'gnus-score-default
388   :type '(choice (const :tag "temporary" t)
389                  (const :tag "permanent" p)
390                  (const :tag "immediate" i)
391                  (const :tag "ask" nil)))
392
393 (defcustom gnus-score-after-write-file-function nil
394   "Function called with the name of the score file just written to disk."
395   :group 'gnus-score-files
396   :type '(choice (const nil) function))
397
398 (defcustom gnus-score-thread-simplify nil
399   "If non-nil, subjects will simplified as in threading."
400   :group 'gnus-score-various
401   :type 'boolean)
402
403 \f
404
405 ;; Internal variables.
406
407 (defvar gnus-score-use-all-scores t
408   "If nil, only `gnus-score-find-score-files-function' is used.")
409
410 (defvar gnus-adaptive-word-syntax-table
411   (let ((table (copy-syntax-table (standard-syntax-table)))
412         (numbers '(?0 ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9)))
413     (while numbers
414       (modify-syntax-entry (pop numbers) " " table))
415     (modify-syntax-entry ?' "w" table)
416     table)
417   "Syntax table used when doing adaptive word scoring.")
418
419 (defvar gnus-scores-exclude-files nil)
420 (defvar gnus-internal-global-score-files nil)
421 (defvar gnus-score-file-list nil)
422
423 (defvar gnus-short-name-score-file-cache nil)
424
425 (defvar gnus-score-help-winconf nil)
426 (defvar gnus-adaptive-score-alist gnus-default-adaptive-score-alist)
427 (defvar gnus-adaptive-word-score-alist gnus-default-adaptive-word-score-alist)
428 (defvar gnus-score-trace nil)
429 (defvar gnus-score-edit-buffer nil)
430
431 (defvar gnus-score-alist nil
432   "Alist containing score information.
433 The keys can be symbols or strings.  The following symbols are defined.
434
435 touched: If this alist has been modified.
436 mark:    Automatically mark articles below this.
437 expunge: Automatically expunge articles below this.
438 files:   List of other score files to load when loading this one.
439 eval:    Sexp to be evaluated when the score file is loaded.
440
441 String entries have the form (HEADER (MATCH TYPE SCORE DATE) ...)
442 where HEADER is the header being scored, MATCH is the string we are
443 looking for, TYPE is a flag indicating whether it should use regexp or
444 substring matching, SCORE is the score to add and DATE is the date
445 of the last successful match.")
446
447 (defvar gnus-score-cache nil)
448 (defvar gnus-scores-articles nil)
449 (defvar gnus-score-index nil)
450
451
452 (defconst gnus-header-index
453   ;; Name to index alist.
454   '(("number" 0 gnus-score-integer)
455     ("subject" 1 gnus-score-string)
456     ("from" 2 gnus-score-string)
457     ("date" 3 gnus-score-date)
458     ("message-id" 4 gnus-score-string)
459     ("references" 5 gnus-score-string)
460     ("chars" 6 gnus-score-integer)
461     ("lines" 7 gnus-score-integer)
462     ("xref" 8 gnus-score-string)
463     ("extra" 9 gnus-score-string)
464     ("head" -1 gnus-score-body)
465     ("body" -1 gnus-score-body)
466     ("all" -1 gnus-score-body)
467     ("followup" 2 gnus-score-followup)
468     ("thread" 5 gnus-score-thread)))
469
470 ;;; Summary mode score maps.
471
472 (gnus-define-keys (gnus-summary-score-map "V" gnus-summary-mode-map)
473   "s" gnus-summary-set-score
474   "S" gnus-summary-current-score
475   "c" gnus-score-change-score-file
476   "C" gnus-score-customize
477   "m" gnus-score-set-mark-below
478   "x" gnus-score-set-expunge-below
479   "R" gnus-summary-rescore
480   "e" gnus-score-edit-current-scores
481   "f" gnus-score-edit-file
482   "F" gnus-score-flush-cache
483   "t" gnus-score-find-trace
484   "w" gnus-score-find-favourite-words)
485
486 ;; Summary score file commands
487
488 ;; Much modification of the kill (ahem, score) code and lots of the
489 ;; functions are written by Per Abrahamsen <amanda@iesd.auc.dk>.
490
491 (defun gnus-summary-lower-score (&optional score symp)
492   "Make a score entry based on the current article.
493 The user will be prompted for header to score on, match type,
494 permanence, and the string to be used.  The numerical prefix will be
495 used as score.  A symbolic prefix of `a' says to use the `all.SCORE'
496 file for the command instead of the current score file."
497   (interactive (gnus-interactive "P\ny"))
498   (gnus-summary-increase-score (- (gnus-score-delta-default score)) symp))
499
500 (defun gnus-score-kill-help-buffer ()
501   (when (get-buffer "*Score Help*")
502     (kill-buffer "*Score Help*")
503     (when gnus-score-help-winconf
504       (set-window-configuration gnus-score-help-winconf))))
505
506 (defun gnus-summary-increase-score (&optional score symp)
507   "Make a score entry based on the current article.
508 The user will be prompted for header to score on, match type,
509 permanence, and the string to be used.  The numerical prefix will be
510 used as score.  A symbolic prefix of `a' says to use the `all.SCORE'
511 file for the command instead of the current score file."
512   (interactive (gnus-interactive "P\ny"))
513   (let* ((nscore (gnus-score-delta-default score))
514          (prefix (if (< nscore 0) ?L ?I))
515          (increase (> nscore 0))
516          (char-to-header
517           '((?a "from" nil nil string)
518             (?s "subject" nil nil string)
519             (?b "body" "" nil body-string)
520             (?h "head" "" nil body-string)
521             (?i "message-id" nil nil string)
522             (?r "references" "message-id" nil string)
523             (?x "xref" nil nil string)
524             (?e "extra" nil nil string)
525             (?l "lines" nil nil number)
526             (?d "date" nil nil date)
527             (?f "followup" nil nil string)
528             (?t "thread" "message-id" nil string)))
529          (char-to-type
530           '((?s s "substring" string)
531             (?e e "exact string" string)
532             (?f f "fuzzy string" string)
533             (?r r "regexp string" string)
534             (?z s "substring" body-string)
535             (?p r "regexp string" body-string)
536             (?b before "before date" date)
537             (?a after "after date" date)
538             (?n at "this date" date)
539             (?< < "less than number" number)
540             (?> > "greater than number" number)
541             (?= = "equal to number" number)))
542          (current-score-file gnus-current-score-file)
543          (char-to-perm
544           (list (list ?t (current-time-string) "temporary")
545                 '(?p perm "permanent") '(?i now "immediate")))
546          (mimic gnus-score-mimic-keymap)
547          (hchar (and gnus-score-default-header
548                      (aref (symbol-name gnus-score-default-header) 0)))
549          (tchar (and gnus-score-default-type
550                      (aref (symbol-name gnus-score-default-type) 0)))
551          (pchar (and gnus-score-default-duration
552                      (aref (symbol-name gnus-score-default-duration) 0)))
553          entry temporary type match extra)
554
555     (unwind-protect
556         (progn
557
558           ;; First we read the header to score.
559           (while (not hchar)
560             (if mimic
561                 (progn
562                   (sit-for 1)
563                   (message "%c-" prefix))
564               (message "%s header (%s?): " (if increase "Increase" "Lower")
565                        (mapconcat (lambda (s) (char-to-string (car s)))
566                                   char-to-header "")))
567             (setq hchar (read-char))
568             (when (or (= hchar ??) (= hchar ?\C-h))
569               (setq hchar nil)
570               (gnus-score-insert-help "Match on header" char-to-header 1)))
571
572           (gnus-score-kill-help-buffer)
573           (unless (setq entry (assq (downcase hchar) char-to-header))
574             (if mimic (error "%c %c" prefix hchar)
575               (error "Invalid header type")))
576
577           (when (/= (downcase hchar) hchar)
578             ;; This was a majuscule, so we end reading and set the defaults.
579             (if mimic (message "%c %c" prefix hchar) (message ""))
580             (setq tchar (or tchar ?s)
581                   pchar (or pchar ?t)))
582
583           (let ((legal-types
584                  (delq nil
585                        (mapcar (lambda (s)
586                                  (if (eq (nth 4 entry)
587                                          (nth 3 s))
588                                      s nil))
589                                char-to-type))))
590             ;; We continue reading - the type.
591             (while (not tchar)
592               (if mimic
593                   (progn
594                     (sit-for 1) (message "%c %c-" prefix hchar))
595                 (message "%s header '%s' with match type (%s?): "
596                          (if increase "Increase" "Lower")
597                          (nth 1 entry)
598                          (mapconcat (lambda (s) (char-to-string (car s)))
599                                     legal-types "")))
600               (setq tchar (read-char))
601               (when (or (= tchar ??) (= tchar ?\C-h))
602                 (setq tchar nil)
603                 (gnus-score-insert-help "Match type" legal-types 2)))
604
605             (gnus-score-kill-help-buffer)
606             (unless (setq type (nth 1 (assq (downcase tchar) legal-types)))
607               (if mimic (error "%c %c" prefix hchar)
608                 (error "Invalid match type"))))
609
610           (when (/= (downcase tchar)&n