Allow scoring on decoded (base64, etc) bodies.
[gnus] / lisp / gnus-score.el
index 9bbfbfb..948c1c3 100644 (file)
@@ -1,6 +1,6 @@
 ;;; gnus-score.el --- scoring code for Gnus
 
-;; Copyright (C) 1995-2011 Free Software Foundation, Inc.
+;; Copyright (C) 1995-2012 Free Software Foundation, Inc.
 
 ;; Author: Per Abrahamsen <amanda@iesd.auc.dk>
 ;;     Lars Magne Ingebrigtsen <larsi@gnus.org>
@@ -522,9 +522,10 @@ of the last successful match.")
 (defun gnus-summary-lower-score (&optional score symp)
   "Make a score entry based on the current article.
 The user will be prompted for header to score on, match type,
-permanence, and the string to be used.  The numerical prefix will be
-used as score.  A symbolic prefix of `a' says to use the `all.SCORE'
-file for the command instead of the current score file."
+permanence, and the string to be used.  The numerical prefix will
+be used as SCORE.  A symbolic prefix of `a' (the SYMP parameter)
+says to use the `all.SCORE' file for the command instead of the
+current score file."
   (interactive (gnus-interactive "P\ny"))
   (gnus-summary-increase-score (- (gnus-score-delta-default score)) symp))
 
@@ -537,9 +538,10 @@ file for the command instead of the current score file."
 (defun gnus-summary-increase-score (&optional score symp)
   "Make a score entry based on the current article.
 The user will be prompted for header to score on, match type,
-permanence, and the string to be used.  The numerical prefix will be
-used as score.  A symbolic prefix of `a' says to use the `all.SCORE'
-file for the command instead of the current score file."
+permanence, and the string to be used.  The numerical prefix will
+be used as SCORE.  A symbolic prefix of `a' (the SYMP parameter)
+says to use the `all.SCORE' file for the command instead of the
+current score file."
   (interactive (gnus-interactive "P\ny"))
   (let* ((nscore (gnus-score-delta-default score))
         (prefix (if (< nscore 0) ?L ?I))
@@ -945,25 +947,6 @@ EXTRA is the possible non-standard header."
                 (gnus-summary-raise-score score))))
        (beginning-of-line 2))))
   (gnus-set-mode-line 'summary))
-
-(defun gnus-summary-score-crossposting (score date)
-  ;; Enter score file entry for current crossposting.
-  ;; SCORE is the score to add.
-  ;; DATE is the expire date.
-  (let ((xref (gnus-summary-header "xref"))
-       (start 0)
-       group)
-    (unless xref
-      (error "This article is not crossposted"))
-    (while (string-match " \\([^ \t]+\\):" xref start)
-      (setq start (match-end 0))
-      (when (not (string=
-                 (setq group
-                       (substring xref (match-beginning 1) (match-end 1)))
-                 gnus-newsgroup-name))
-       (gnus-summary-score-entry
-        "xref" (concat " " group ":") nil score date t)))))
-
 \f
 ;;;
 ;;; Gnus Score Files
@@ -1734,105 +1717,140 @@ score in `gnus-newsgroup-scored' by SCORE."
          (setq entries rest)))))
   nil)
 
+(defun gnus-score-decode-text-parts ()
+  (labels ((mm-text-parts (handle)
+                        (cond ((stringp (car handle))
+                               (let ((parts (mapcan 'mm-text-parts (cdr handle))))
+                                 (if (equal "multipart/alternative" (car handle))
+                                     ;; pick the first supported alternative
+                                     (list (car parts))
+                                   parts)))
+
+                              ((bufferp (car handle))
+                               (when (string-match "^text/" (mm-handle-media-type handle))
+                                 (list handle)))
+
+                              (t (mapcan 'mm-text-parts handle))))
+           (my-mm-display-part (handle)
+                               (when handle
+                                 (save-restriction
+                                   (narrow-to-region (point) (point))
+                                   (mm-display-inline handle)
+                                   (goto-char (point-max))))))
+
+    (let (;(mm-text-html-renderer 'w3m-standalone)
+          (handles (mm-dissect-buffer t)))
+      (save-excursion
+        (article-goto-body)
+        (delete-region (point) (point-max))
+        (mapc 'my-mm-display-part (mm-text-parts handles))
+        handles))))
+
 (defun gnus-score-body (scores header now expire &optional trace)
-  (if gnus-agent-fetching
-      nil
-    (save-excursion
-      (setq gnus-scores-articles
-           (sort gnus-scores-articles
-                 (lambda (a1 a2)
-                   (< (mail-header-number (car a1))
-                      (mail-header-number (car a2))))))
-      (set-buffer nntp-server-buffer)
-      (save-restriction
-       (let* ((buffer-read-only nil)
-              (articles gnus-scores-articles)
-              (all-scores scores)
-              (request-func (cond ((string= "head" header)
-                                   'gnus-request-head)
-                                  ((string= "body" header)
-                                   'gnus-request-body)
-                                  (t 'gnus-request-article)))
-              entries alist ofunc article last)
-         (when articles
-           (setq last (mail-header-number (caar (last articles))))
-         ;; Not all backends support partial fetching.  In that case,
-           ;; we just fetch the entire article.
-           (unless (gnus-check-backend-function
-                    (and (string-match "^gnus-" (symbol-name request-func))
-                         (intern (substring (symbol-name request-func)
-                                            (match-end 0))))
-                    gnus-newsgroup-name)
-             (setq ofunc request-func)
-             (setq request-func 'gnus-request-article))
-           (while articles
-             (setq article (mail-header-number (caar articles)))
-             (gnus-message 7 "Scoring article %s of %s..." article last)
-             (widen)
-             (when (funcall request-func article gnus-newsgroup-name)
-               (goto-char (point-min))
-           ;; If just parts of the article is to be searched, but the
-           ;; backend didn't support partial fetching, we just narrow
-               ;; to the relevant parts.
-               (when ofunc
-                 (if (eq ofunc 'gnus-request-head)
-                     (narrow-to-region
-                      (point)
-                      (or (search-forward "\n\n" nil t) (point-max)))
-                   (narrow-to-region
-                    (or (search-forward "\n\n" nil t) (point))
-                    (point-max))))
-               (setq scores all-scores)
-               ;; Find matches.
-               (while scores
-                 (setq alist (pop scores)
-                       entries (assoc header alist))
-                 (while (cdr entries) ;First entry is the header index.
-                   (let* ((rest (cdr entries))
-                          (kill (car rest))
-                          (match (nth 0 kill))
-                          (type (or (nth 3 kill) 's))
-                          (score (or (nth 1 kill)
-                                     gnus-score-interactive-default-score))
-                          (date (nth 2 kill))
-                          (found nil)
-                          (case-fold-search
-                           (not (or (eq type 'R) (eq type 'S)
-                                    (eq type 'Regexp) (eq type 'String))))
-                          (search-func
-                           (cond ((or (eq type 'r) (eq type 'R)
-                                      (eq type 'regexp) (eq type 'Regexp))
-                                  're-search-forward)
-                                 ((or (eq type 's) (eq type 'S)
-                                      (eq type 'string) (eq type 'String))
-                                  'search-forward)
-                                 (t
-                                  (error "Invalid match type: %s" type)))))
-                     (goto-char (point-min))
-                     (when (funcall search-func match nil t)
-                       ;; Found a match, update scores.
-                       (setcdr (car articles) (+ score (cdar articles)))
-                       (setq found t)
-                       (when trace
-                         (push
-                          (cons (car-safe (rassq alist gnus-score-cache))
-                                kill)
-                          gnus-score-trace)))
-                     ;; Update expire date
-                     (unless trace
-                       (cond
-                        ((null date))  ;Permanent entry.
-                        ((and found gnus-update-score-entry-dates)
-                         ;; Match, update date.
-                         (gnus-score-set 'touched '(t) alist)
-                         (setcar (nthcdr 2 kill) now))
-                        ((and expire (< date expire)) ;Old entry, remove.
-                         (gnus-score-set 'touched '(t) alist)
-                         (setcdr entries (cdr rest))
-                         (setq rest entries))))
-                     (setq entries rest)))))
-             (setq articles (cdr articles)))))))
-    nil))
+    (if gnus-agent-fetching
+       nil
+     (save-excursion
+       (setq gnus-scores-articles
+             (sort gnus-scores-articles
+                   (lambda (a1 a2)
+                     (< (mail-header-number (car a1))
+                        (mail-header-number (car a2))))))
+       (set-buffer nntp-server-buffer)
+       (save-restriction
+         (let* ((buffer-read-only nil)
+                (articles gnus-scores-articles)
+                (all-scores scores)
+                (request-func (cond ((string= "head" header)
+                                     'gnus-request-head)
+                                    ;; We need to peek at the headers to detect
+                                    ;; the content encoding
+                                    ((string= "body" header)
+                                     'gnus-request-article)
+                                    (t 'gnus-request-article)))
+                entries alist ofunc article last)
+           (when articles
+             (setq last (mail-header-number (caar (last articles))))
+             ;; Not all backends support partial fetching.  In that case,
+             ;; we just fetch the entire article.
+             (unless (gnus-check-backend-function
+                      (and (string-match "^gnus-" (symbol-name request-func))
+                           (intern (substring (symbol-name request-func)
+                                              (match-end 0))))
+                      gnus-newsgroup-name)
+               (setq ofunc request-func)
+               (setq request-func 'gnus-request-article))
+             (while articles
+               (setq article (mail-header-number (caar articles)))
+               (gnus-message 7 "Scoring article %s of %s..." article last)
+               (widen)
+               (let (handles)
+                 (when (funcall request-func article gnus-newsgroup-name)
+                  (when (string= "body" header)
+                    (setq handles (gnus-score-decode-text-parts)))
+                  (goto-char (point-min))
+                  ;; If just parts of the article is to be searched, but the
+                  ;; backend didn't support partial fetching, we just narrow
+                  ;; to the relevant parts.
+                  (when ofunc
+                    (if (eq ofunc 'gnus-request-head)
+                        (narrow-to-region
+                         (point)
+                         (or (search-forward "\n\n" nil t) (point-max)))
+                      (narrow-to-region
+                       (or (search-forward "\n\n" nil t) (point))
+                       (point-max))))
+                  (setq scores all-scores)
+                  ;; Find matches.
+                  (while scores
+                    (setq alist (pop scores)
+                          entries (assoc header alist))
+                    (while (cdr entries) ;First entry is the header index.
+                      (let* ((rest (cdr entries))
+                             (kill (car rest))
+                             (match (nth 0 kill))
+                             (type (or (nth 3 kill) 's))
+                             (score (or (nth 1 kill)
+                                        gnus-score-interactive-default-score))
+                             (date (nth 2 kill))
+                             (found nil)
+                             (case-fold-search
+                              (not (or (eq type 'R) (eq type 'S)
+                                       (eq type 'Regexp) (eq type 'String))))
+                             (search-func
+                              (cond ((or (eq type 'r) (eq type 'R)
+                                         (eq type 'regexp) (eq type 'Regexp))
+                                     're-search-forward)
+                                    ((or (eq type 's) (eq type 'S)
+                                         (eq type 'string) (eq type 'String))
+                                     'search-forward)
+                                    (t
+                                     (error "Invalid match type: %s" type)))))
+                        (goto-char (point-min))
+                        (when (funcall search-func match nil t)
+                          ;; Found a match, update scores.
+                          (setcdr (car articles) (+ score (cdar articles)))
+                          (setq found t)
+                          (when trace
+                            (push
+                             (cons (car-safe (rassq alist gnus-score-cache))
+                                   kill)
+                             gnus-score-trace)))
+                        ;; Update expire date
+                        (unless trace
+                          (cond
+                           ((null date)) ;Permanent entry.
+                           ((and found gnus-update-score-entry-dates)
+                            ;; Match, update date.
+                            (gnus-score-set 'touched '(t) alist)
+                            (setcar (nthcdr 2 kill) now))
+                           ((and expire (< date expire)) ;Old entry, remove.
+                            (gnus-score-set 'touched '(t) alist)
+                            (setcdr entries (cdr rest))
+                            (setq rest entries))))
+                        (setq entries rest))))
+                  (when handles (mm-destroy-parts handles))))
+               (setq articles (cdr articles)))))))
+     nil))
 
 (defun gnus-score-thread (scores header now expire &optional trace)
   (gnus-score-followup scores header now expire trace t))
@@ -2834,8 +2852,7 @@ The list is determined from the variable `gnus-score-file-alist'."
       ;; handle the multiple match alist
       (while alist
        (when (string-match (caar alist) group)
-         (setq score-files
-               (nconc score-files (copy-sequence (cdar alist)))))
+         (setq score-files (append (cdar alist) score-files)))
        (setq alist (cdr alist)))
       (setq alist gnus-score-file-single-match-alist)
       ;; handle the single match alist
@@ -2845,8 +2862,7 @@ The list is determined from the variable `gnus-score-file-alist'."
          ;; and score-files is still nil.  -sj
          ;; this can be construed as a "stop searching here" feature :>
          ;; and used to simplify regexps in the single-alist
-         (setq score-files
-               (nconc score-files (copy-sequence (cdar alist))))
+         (setq score-files (append (cdar alist) score-files))
          (setq alist nil))
        (setq alist (cdr alist)))
       ;; cache the score files
@@ -2866,7 +2882,7 @@ The list is determined from the variable `gnus-score-file-alist'."
       (when gnus-score-use-all-scores
        ;; Get the initial score files for this group.
        (when funcs
-         (setq score-files (nreverse (gnus-score-find-alist group))))
+         (setq score-files (copy-sequence (gnus-score-find-alist group))))
        ;; Add any home adapt files.
        (let ((home (gnus-home-score-file group t)))
          (when home
@@ -3013,7 +3029,7 @@ If ADAPT, return the home adaptive file instead."
 
 (defun gnus-current-home-score-file (group)
   "Return the \"current\" regular score file."
-  (car (nreverse (gnus-score-find-alist group))))
+  (car (gnus-score-find-alist group)))
 
 ;;;
 ;;; Score decays
@@ -3028,7 +3044,7 @@ If ADAPT, return the home adaptive file instead."
                           (* (abs score)
                              gnus-score-decay-scale)))))))
     (if (and (featurep 'xemacs)
-            ;; XEmacs' floor can handle only the floating point
+            ;; XEmacs's floor can handle only the floating point
             ;; number below the half of the maximum integer.
             (> (abs n) (lsh -1 -2)))
        (string-to-number
@@ -3056,62 +3072,6 @@ If ADAPT, return the home adaptive file instead."
     ;; Return whether this score file needs to be saved.  By Je-haysuss!
     updated))
 
-(defun gnus-score-regexp-bad-p (regexp)
-  "Test whether REGEXP is safe for Gnus scoring.
-A regexp is unsafe if it matches newline or a buffer boundary.
-
-If the regexp is good, return nil.  If the regexp is bad, return a
-cons cell (SYM . STRING), where the symbol SYM is `new' or `bad'.
-In the `new' case, the string is a safe replacement for REGEXP.
-In the `bad' case, the string is a unsafe subexpression of REGEXP,
-and we do not have a simple replacement to suggest.
-
-See Info node `(gnus)Scoring Tips' for examples of good regular expressions."
-  (let (case-fold-search)
-    (and
-     ;; First, try a relatively fast necessary condition.
-     ;; Notice ranges (like [^:] or [\t-\r]), \s>, \Sw, \W, \', \`:
-     (string-match "\n\\|\\\\[SsW`']\\|\\[\\^\\|[\0-\n]-" regexp)
-     ;; Now break the regexp into tokens, and check each:
-     (let ((tail regexp)               ; remaining regexp to check
-          tok                          ; current token
-          bad                          ; nil, or bad subexpression
-          new                          ; nil, or replacement regexp so far
-          end)                         ; length of current token
-       (while (and (not bad)
-                  (string-match
-                   "\\`\\(\\\\[sS]?.\\|\\[\\^?]?[^]]*]\\|[^\\]\\)"
-                   tail))
-        (setq end (match-end 0)
-              tok (substring tail 0 end)
-              tail (substring tail end))
-        (if;; Is token `bad' (matching newline or buffer ends)?
-            (or (member tok '("\n" "\\W" "\\`" "\\'"))
-                ;; This next handles "[...]", "\\s.", and "\\S.":
-                (and (> end 2) (string-match tok "\n")))
-            (let ((newtok
-                   ;; Try to suggest a replacement for tok ...
-                   (cond ((string-equal tok "\\`") "^") ; or "\\(^\\)"
-                         ((string-equal tok "\\'") "$") ; or "\\($\\)"
-                         ((string-match "\\[\\^" tok) ; very common
-                          (concat (substring tok 0 -1) "\n]")))))
-              (if newtok
-                  (setq new
-                        (concat
-                         (or new
-                             ;; good prefix so far:
-                             (substring regexp 0 (- (+ (length tail) end))))
-                         newtok))
-                ;; No replacement idea, so give up:
-                (setq bad tok)))
-          ;; tok is good, may need to extend new
-          (and new (setq new (concat new tok)))))
-       ;; Now return a value:
-       (cond
-       (bad (cons 'bad bad))
-       (new (cons 'new new))
-       (t nil))))))
-
 (provide 'gnus-score)
 
 ;;; gnus-score.el ends here