1 ;;; gnus-score.el --- scoring code for Gnus
2 ;; Copyright (C) 1995,96 Free Software Foundation, Inc.
4 ;; Author: Per Abrahamsen <amanda@iesd.auc.dk>
5 ;; Lars Magne Ingebrigtsen <larsi@ifi.uio.no>
8 ;; This file is part of GNU Emacs.
10 ;; GNU Emacs is free software; you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation; either version 2, or (at your option)
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs; see the file COPYING. If not, write to the
22 ;; Free Software Foundation, Inc., 59 Temple Place - Suite 330,
23 ;; Boston, MA 02111-1307, USA.
33 (defvar gnus-global-score-files nil
34 "*List of global score files and directories.
35 Set this variable if you want to use people's score files. One entry
36 for each score file or each score file directory. Gnus will decide
37 by itself what score files are applicable to which group.
39 Say you want to use the single score file
40 \"/ftp.ifi.uio.no@ftp:/pub/larsi/ding/score/soc.motss.SCORE\" and all
41 score files in the \"/ftp.some-where:/pub/score\" directory.
43 (setq gnus-global-score-files
44 '(\"/ftp.ifi.uio.no:/pub/larsi/ding/score/soc.motss.SCORE\"
45 \"/ftp.some-where:/pub/score\"))")
47 (defvar gnus-score-file-single-match-alist nil
48 "*Alist mapping regexps to lists of score files.
49 Each element of this alist should be of the form
50 (\"REGEXP\" [ \"SCORE-FILE-1\" ] [ \"SCORE-FILE-2\" ] ... )
52 If the name of a group is matched by REGEXP, the corresponding scorefiles
53 will be used for that group.
54 The first match found is used, subsequent matching entries are ignored (to
55 use multiple matches, see gnus-score-file-multiple-match-alist).
57 These score files are loaded in addition to any files returned by
58 gnus-score-find-score-files-function (which see).")
60 (defvar gnus-score-file-multiple-match-alist nil
61 "*Alist mapping regexps to lists of score files.
62 Each element of this alist should be of the form
63 (\"REGEXP\" [ \"SCORE-FILE-1\" ] [ \"SCORE-FILE-2\" ] ... )
65 If the name of a group is matched by REGEXP, the corresponding scorefiles
66 will be used for that group.
67 If multiple REGEXPs match a group, the score files corresponding to each
68 match will be used (for only one match to be used, see
69 gnus-score-file-single-match-alist).
71 These score files are loaded in addition to any files returned by
72 gnus-score-find-score-files-function (which see).")
74 (defvar gnus-score-file-suffix "SCORE"
75 "*Suffix of the score files.")
77 (defvar gnus-adaptive-file-suffix "ADAPT"
78 "*Suffix of the adaptive score files.")
80 (defvar gnus-score-find-score-files-function 'gnus-score-find-bnews
81 "*Function used to find score files.
82 The function will be called with the group name as the argument, and
83 should return a list of score files to apply to that group. The score
84 files do not actually have to exist.
86 Predefined values are:
88 gnus-score-find-single: Only apply the group's own score file.
89 gnus-score-find-hierarchical: Also apply score files from parent groups.
90 gnus-score-find-bnews: Apply score files whose names matches.
92 See the documentation to these functions for more information.
94 This variable can also be a list of functions to be called. Each
95 function should either return a list of score files, or a list of
98 (defvar gnus-score-interactive-default-score 1000
99 "*Scoring commands will raise/lower the score with this number as the default.")
101 (defvar gnus-score-expiry-days 7
102 "*Number of days before unused score file entries are expired.
103 If this variable is nil, no score file entries will be expired.")
105 (defvar gnus-update-score-entry-dates t
106 "*In non-nil, update matching score entry dates.
107 If this variable is nil, then score entries that provide matches
108 will be expired along with non-matching score entries.")
110 (defvar gnus-orphan-score nil
111 "*All orphans get this score added. Set in the score file.")
113 (defvar gnus-decay-scores nil
114 "*If non-nil, decay non-permanent scores.")
116 (defvar gnus-decay-score-function 'gnus-decay-score
117 "*Function called to decay a score.
118 It is called with one parameter -- the score to be decayed.")
120 (defvar gnus-score-decay-constant 3
121 "*Decay all \"small\" scores with this amount.")
123 (defvar gnus-score-decay-scale .05
124 "*Decay all \"big\" scores with this factor.")
126 (defvar gnus-home-score-file nil
127 "Variable to control where interactive score entries are to go.
131 This file file will be used as the home score file.
134 The result of this function will be used as the home score file.
137 The elements in this list can be:
139 * `(regexp file-name ...)'
140 If the `regexp' matches the group name, the first `file-name' will
141 will be used as the home score file. (Multiple filenames are
142 allowed so that one may use gnus-score-file-single-match-alist to
146 If the function returns non-nil, the result will be used
147 as the home score file.
150 Use the string as the home score file.
152 The list will be traversed from the beginning towards the end looking
155 (defvar gnus-home-adapt-file nil
156 "Variable to control where new adaptive score entries are to go.
157 This variable allows the same syntax as `gnus-home-score-file'.")
159 (defvar gnus-default-adaptive-score-alist
160 '((gnus-kill-file-mark)
162 (gnus-read-mark (from 3) (subject 30))
163 (gnus-catchup-mark (subject -10))
164 (gnus-killed-mark (from -1) (subject -20))
165 (gnus-del-mark (from -2) (subject -15)))
166 "*Alist of marks and scores.")
168 (defvar gnus-ignored-adaptive-words nil
169 "*List of words to be ignored when doing adaptive word scoring.")
171 (defvar gnus-default-ignored-adaptive-words
172 '("a" "i" "the" "to" "of" "and" "in" "is" "it" "for" "that" "if" "you"
173 "this" "be" "on" "with" "not" "have" "are" "or" "as" "from" "can"
174 "but" "by" "at" "an" "will" "no" "all" "was" "do" "there" "my" "one"
175 "so" "we" "they" "what" "would" "any" "which" "about" "get" "your"
176 "use" "some" "me" "then" "name" "like" "out" "when" "up" "time"
177 "other" "more" "only" "just" "end" "also" "know" "how" "new" "should"
178 "been" "than" "them" "he" "who" "make" "may" "people" "these" "now"
179 "their" "here" "into" "first" "could" "way" "had" "see" "work" "well"
180 "were" "two" "very" "where" "while" "us" "because" "good" "same"
181 "even" "much" "most" "many" "such" "long" "his" "over" "last" "since"
182 "right" "before" "our" "without" "too" "those" "why" "must" "part"
183 "being" "current" "back" "still" "go" "point" "value" "each" "did"
184 "both" "true" "off" "say" "another" "state" "might" "under" "start"
186 "Default list of words to be ignored when doing adaptive word scoring.")
188 (defvar gnus-default-adaptive-word-score-alist
189 `((,gnus-read-mark . 30)
190 (,gnus-catchup-mark . -10)
191 (,gnus-killed-mark . -20)
192 (,gnus-del-mark . -15))
193 "*Alist of marks and scores.")
195 (defvar gnus-score-mimic-keymap nil
196 "*Have the score entry functions pretend that they are a keymap.")
198 (defvar gnus-score-exact-adapt-limit 10
199 "*Number that says how long a match has to be before using substring matching.
200 When doing adaptive scoring, one normally uses fuzzy or substring
201 matching. However, if the header one matches is short, the possibility
202 for false positives is great, so if the length of the match is less
203 than this variable, exact matching will be used.
205 If this variable is nil, exact matching will always be used.")
207 (defvar gnus-score-uncacheable-files "ADAPT$"
208 "*All score files that match this regexp will not be cached.")
210 (defvar gnus-score-default-header nil
211 "Default header when entering new scores.
213 Should be one of the following symbols.
226 If nil, the user will be asked for a header.")
228 (defvar gnus-score-default-type nil
229 "Default match type when entering new scores.
231 Should be one of the following symbols.
241 >: greater than number
244 If nil, the user will be asked for a match type.")
246 (defvar gnus-score-default-fold nil
247 "Use case folding for new score file entries iff not nil.")
249 (defvar gnus-score-default-duration nil
250 "Default duration of effect when entering new scores.
252 Should be one of the following symbols.
258 If nil, the user will be asked for a duration.")
260 (defvar gnus-score-after-write-file-function nil
261 "*Function called with the name of the score file just written to disk.")
265 ;; Internal variables.
267 (defvar gnus-adaptive-word-syntax-table
268 (let ((table (copy-syntax-table (standard-syntax-table)))
269 (numbers '(?0 ?1 ?2 ?3 ?4 ?5 ?6 ?7 ?8 ?9)))
271 (modify-syntax-entry (pop numbers) " " table))
272 (modify-syntax-entry ?' "w" table)
274 "Syntax table used when doing adaptive word scoring.")
276 (defvar gnus-scores-exclude-files nil)
277 (defvar gnus-internal-global-score-files nil)
278 (defvar gnus-score-file-list nil)
280 (defvar gnus-short-name-score-file-cache nil)
282 (defvar gnus-score-help-winconf nil)
283 (defvar gnus-adaptive-score-alist gnus-default-adaptive-score-alist)
284 (defvar gnus-adaptive-word-score-alist gnus-default-adaptive-word-score-alist)
285 (defvar gnus-score-trace nil)
286 (defvar gnus-score-edit-buffer nil)
288 (defvar gnus-score-alist nil
289 "Alist containing score information.
290 The keys can be symbols or strings. The following symbols are defined.
292 touched: If this alist has been modified.
293 mark: Automatically mark articles below this.
294 expunge: Automatically expunge articles below this.
295 files: List of other score files to load when loading this one.
296 eval: Sexp to be evaluated when the score file is loaded.
298 String entries have the form (HEADER (MATCH TYPE SCORE DATE) ...)
299 where HEADER is the header being scored, MATCH is the string we are
300 looking for, TYPE is a flag indicating whether it should use regexp or
301 substring matching, SCORE is the score to add and DATE is the date
302 of the last successful match.")
304 (defvar gnus-score-cache nil)
305 (defvar gnus-scores-articles nil)
306 (defvar gnus-score-index nil)
309 (defconst gnus-header-index
310 ;; Name to index alist.
311 '(("number" 0 gnus-score-integer)
312 ("subject" 1 gnus-score-string)
313 ("from" 2 gnus-score-string)
314 ("date" 3 gnus-score-date)
315 ("message-id" 4 gnus-score-string)
316 ("references" 5 gnus-score-string)
317 ("chars" 6 gnus-score-integer)
318 ("lines" 7 gnus-score-integer)
319 ("xref" 8 gnus-score-string)
320 ("head" -1 gnus-score-body)
321 ("body" -1 gnus-score-body)
322 ("all" -1 gnus-score-body)
323 ("followup" 2 gnus-score-followup)
324 ("thread" 5 gnus-score-thread)))
326 ;;; Summary mode score maps.
328 (gnus-define-keys (gnus-summary-score-map "V" gnus-summary-mode-map)
329 "s" gnus-summary-set-score
330 "a" gnus-summary-score-entry
331 "S" gnus-summary-current-score
332 "c" gnus-score-change-score-file
333 "C" gnus-score-customize
334 "m" gnus-score-set-mark-below
335 "x" gnus-score-set-expunge-below
336 "R" gnus-summary-rescore
337 "e" gnus-score-edit-current-scores
338 "f" gnus-score-edit-file
339 "F" gnus-score-flush-cache
340 "t" gnus-score-find-trace
341 "w" gnus-score-find-favourite-words)
343 ;; Summary score file commands
345 ;; Much modification of the kill (ahem, score) code and lots of the
346 ;; functions are written by Per Abrahamsen <amanda@iesd.auc.dk>.
348 (defun gnus-summary-lower-score (&optional score)
349 "Make a score entry based on the current article.
350 The user will be prompted for header to score on, match type,
351 permanence, and the string to be used. The numerical prefix will be
354 (gnus-summary-increase-score (- (gnus-score-default score))))
356 (defvar gnus-score-default-header nil
357 "*The default header to score on when entering a score rule interactively.")
359 (defvar gnus-score-default-type nil
360 "*The default score type to use when entering a score rule interactively.")
362 (defvar gnus-score-default-duration nil
363 "*The default score duration to use on when entering a score rule interactively.")
365 (defun gnus-score-kill-help-buffer ()
366 (when (get-buffer "*Score Help*")
367 (kill-buffer "*Score Help*")
368 (when gnus-score-help-winconf
369 (set-window-configuration gnus-score-help-winconf))))
371 (defun gnus-summary-increase-score (&optional score)
372 "Make a score entry based on the current article.
373 The user will be prompted for header to score on, match type,
374 permanence, and the string to be used. The numerical prefix will be
377 (gnus-set-global-variables)
378 (let* ((nscore (gnus-score-default score))
379 (prefix (if (< nscore 0) ?L ?I))
380 (increase (> nscore 0))
382 '((?a "from" nil nil string)
383 (?s "subject" nil nil string)
384 (?b "body" "" nil body-string)
385 (?h "head" "" nil body-string)
386 (?i "message-id" nil t string)
387 (?t "references" "message-id" nil string)
388 (?x "xref" nil nil string)
389 (?l "lines" nil nil number)
390 (?d "date" nil nil date)
391 (?f "followup" nil nil string)
392 (?T "thread" nil nil string)))
394 '((?s s "substring" string)
395 (?e e "exact string" string)
396 (?f f "fuzzy string" string)
397 (?r r "regexp string" string)
398 (?z s "substring" body-string)
399 (?p r "regexp string" body-string)
400 (?b before "before date" date)
401 (?a at "at date" date)
402 (?n now "this date" date)
403 (?< < "less than number" number)
404 (?> > "greater than number" number)
405 (?= = "equal to number" number)))
407 (list (list ?t (current-time-string) "temporary")
408 '(?p perm "permanent") '(?i now "immediate")))
409 (mimic gnus-score-mimic-keymap)
410 (hchar (and gnus-score-default-header
411 (aref (symbol-name gnus-score-default-header) 0)))
412 (tchar (and gnus-score-default-type
413 (aref (symbol-name gnus-score-default-type) 0)))
414 (pchar (and gnus-score-default-duration
415 (aref (symbol-name gnus-score-default-duration) 0)))
416 entry temporary type match)
421 ;; First we read the header to score.
426 (message "%c-" prefix))
427 (message "%s header (%s?): " (if increase "Increase" "Lower")
428 (mapconcat (lambda (s) (char-to-string (car s)))
430 (setq hchar (read-char))
431 (when (or (= hchar ??) (= hchar ?\C-h))
433 (gnus-score-insert-help "Match on header" char-to-header 1)))
435 (gnus-score-kill-help-buffer)
436 (unless (setq entry (assq (downcase hchar) char-to-header))
437 (if mimic (error "%c %c" prefix hchar) (error "")))
439 (when (/= (downcase hchar) hchar)
440 ;; This was a majuscule, so we end reading and set the defaults.
441 (if mimic (message "%c %c" prefix hchar) (message ""))
442 (setq tchar (or tchar ?s)
443 pchar (or pchar ?t)))
445 ;; We continue reading - the type.
449 (sit-for 1) (message "%c %c-" prefix hchar))
450 (message "%s header '%s' with match type (%s?): "
451 (if increase "Increase" "Lower")
453 (mapconcat (lambda (s)
454 (if (eq (nth 4 entry)
456 (char-to-string (car s))
459 (setq tchar (read-char))
460 (when (or (= tchar ??) (= tchar ?\C-h))
462 (gnus-score-insert-help
466 (if (eq (nth 4 entry)
472 (gnus-score-kill-help-buffer)
473 (unless (setq type (nth 1 (assq (downcase tchar) char-to-type)))
474 (if mimic (error "%c %c" prefix hchar) (error "")))
476 (when (/= (downcase tchar) tchar)
477 ;; It was a majuscule, so we end reading and use the default.
478 (if mimic (message "%c %c %c" prefix hchar tchar)
480 (setq pchar (or pchar ?p)))
482 ;; We continue reading.
486 (sit-for 1) (message "%c %c %c-" prefix hchar tchar))
487 (message "%s permanence (%s?): " (if increase "Increase" "Lower")
488 (mapconcat (lambda (s) (char-to-string (car s)))
490 (setq pchar (read-char))
491 (when (or (= pchar ??) (= pchar ?\C-h))
493 (gnus-score-insert-help "Match permanence" char-to-perm 2)))
495 (gnus-score-kill-help-buffer)
496 (if mimic (message "%c %c %c" prefix hchar tchar pchar)
498 (unless (setq temporary (cadr (assq pchar char-to-perm)))
499 ;; Deal with der(r)ided superannuated paradigms.
500 (when (and (eq (1+ prefix) 77)
501 (eq (+ hchar 12) 109)
503 (eq (- pchar 4) 111))
506 (error "%c %c %c %c" prefix hchar tchar pchar)
508 ;; Always kill the score help buffer.
509 (gnus-score-kill-help-buffer))
511 ;; We have all the data, so we enter this score.
512 (setq match (if (string= (nth 2 entry) "") ""
513 (gnus-summary-header (or (nth 2 entry) (nth 1 entry)))))
515 ;; Modify the match, perhaps.
517 ((equal (nth 1 entry) "xref")
518 (when (string-match "^Xref: *" match)
519 (setq match (substring match (match-end 0))))
520 (when (string-match "^[^:]* +" match)
521 (setq match (substring match (match-end 0))))))
523 (when (memq type '(r R regexp Regexp))
524 (setq match (regexp-quote match)))
526 (gnus-summary-score-entry
527 (nth 1 entry) ; Header
530 (if (eq score 's) nil score) ; Score
531 (if (eq temporary 'perm) ; Temp
534 (not (nth 3 entry))) ; Prompt
537 (defun gnus-score-insert-help (string alist idx)
538 (setq gnus-score-help-winconf (current-window-configuration))
540 (set-buffer (get-buffer-create "*Score Help*"))
541 (buffer-disable-undo (current-buffer))
542 (delete-windows-on (current-buffer))
544 (insert string ":\n\n")
549 ;; find the longest string to display
551 (setq n (length (nth idx (car list))))
554 (setq list (cdr list)))
555 (setq max (+ max 4)) ; %c, `:', SPACE, a SPACE at end
556 (setq n (/ (1- (window-width)) max)) ; items per line
557 (setq width (/ (1- (window-width)) n)) ; width of each item
558 ;; insert `n' items, each in a field of width `width'
563 (delete-char -1) ; the `\n' takes a char
565 (setq pad (- width 3))
566 (setq format (concat "%c: %-" (int-to-string pad) "s"))
567 (insert (format format (caar alist) (nth idx (car alist))))
568 (setq alist (cdr alist))
570 ;; display ourselves in a small window at the bottom
571 (gnus-appt-select-lowest-window)
573 (pop-to-buffer "*Score Help*")
574 (let ((window-min-height 1))
575 (shrink-window-if-larger-than-buffer))
576 (select-window (get-buffer-window gnus-summary-buffer))))
578 (defun gnus-summary-header (header &optional no-err)
579 ;; Return HEADER for current articles, or error.
580 (let ((article (gnus-summary-article-number))
583 (if (and (setq headers (gnus-summary-article-header article))
585 (aref headers (nth 1 (assoc header gnus-header-index)))
588 (error "Pseudo-articles can't be scored")))
590 (error "No article on current line")
593 (defun gnus-newsgroup-score-alist ()
595 (let ((param-file (gnus-group-find-parameter
596 gnus-newsgroup-name 'score-file)))
598 (gnus-score-load param-file)))
600 (gnus-score-file-name gnus-newsgroup-name)))
603 (defsubst gnus-score-get (symbol &optional alist)
604 ;; Get SYMBOL's definition in ALIST.
608 (gnus-newsgroup-score-alist)))))
610 (defun gnus-summary-score-entry (header match type score date
611 &optional prompt silent)
612 "Enter score file entry.
613 HEADER is the header being scored.
614 MATCH is the string we are looking for.
615 TYPE is the match type: substring, regexp, exact, fuzzy.
616 SCORE is the score to add.
617 DATE is the expire date, or nil for no expire, or 'now for immediate expire.
618 If optional argument `PROMPT' is non-nil, allow user to edit match.
619 If optional argument `SILENT' is nil, show effect of score entry."
621 (list (completing-read "Header: "
623 (lambda (x) (fboundp (nth 2 x)))
625 (read-string "Match: ")
626 (if (y-or-n-p "Use regexp match? ") 'r 's)
627 (and current-prefix-arg
628 (prefix-numeric-value current-prefix-arg))
629 (cond ((not (y-or-n-p "Add to score file? "))
631 ((y-or-n-p "Expire kill? ")
632 (current-time-string))
634 ;; Regexp is the default type.
637 ;; Simplify matches...
638 (cond ((or (eq type 'r) (eq type 's) (eq type nil))
639 (setq match (if match (gnus-simplify-subject-re match) "")))
641 (setq match (gnus-simplify-subject-fuzzy match))))
642 (let ((score (gnus-score-default score))
643 (header (format "%s" (downcase header)))
646 (setq match (read-string
647 (format "Match %s on %s, %s: "
648 (cond ((eq date 'now)
654 (if (< score 0) "lower" "raise"))
656 (int-to-string match)
659 ;; Get rid of string props.
660 (setq match (format "%s" match))
662 ;; If this is an integer comparison, we transform from string to int.
663 (when (eq (nth 2 (assoc header gnus-header-index)) 'gnus-score-integer)
664 (setq match (string-to-int match)))
666 (unless (eq date 'now)
667 ;; Add the score entry to the score file.
668 (when (= score gnus-score-interactive-default-score)
670 (let ((old (gnus-score-get header))
676 (and date (if (numberp date) date
677 (gnus-day-number date)))
679 (date (list match score (gnus-day-number date)))
680 (score (list match score))
682 ;; We see whether we can collapse some score entries.
683 ;; This isn't quite correct, because there may be more elements
684 ;; later on with the same key that have matching elems... Hm.
686 (setq elem (assoc match old))
687 (eq (nth 3 elem) (nth 3 new))
688 (or (and (numberp (nth 2 elem)) (numberp (nth 2 new)))
689 (and (not (nth 2 elem)) (not (nth 2 new)))))
690 ;; Yup, we just add this new score to the old elem.
691 (setcar (cdr elem) (+ (or (nth 1 elem)
692 gnus-score-interactive-default-score)
694 gnus-score-interactive-default-score)))
695 ;; Nope, we have to add a new elem.
696 (gnus-score-set header (if old (cons new old) (list new))))
697 (gnus-score-set 'touched '(t))))
699 ;; Score the current buffer.
701 (if (and (>= (nth 1 (assoc header gnus-header-index)) 0)
702 (eq (nth 2 (assoc header gnus-header-index))
704 (gnus-summary-score-effect header match type score)
705 (gnus-summary-rescore)))
707 ;; Return the new scoring rule.
710 (defun gnus-summary-score-effect (header match type score)
711 "Simulate the effect of a score file entry.
712 HEADER is the header being scored.
713 MATCH is the string we are looking for.
714 TYPE is a flag indicating if it is a regexp or substring.
715 SCORE is the score to add."
716 (interactive (list (completing-read "Header: "