* gnus-registry.el (gnus-registry-user-format-function-M): Added
[gnus] / lisp / gnus-registry.el
index 7addf5c..b1f8a36 100644 (file)
@@ -1,7 +1,7 @@
 ;;; gnus-registry.el --- article registry for Gnus
 
 ;; Copyright (C) 1996, 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004,
-;;   2005, 2006 Free Software Foundation, Inc.
+;;   2005, 2006, 2007, 2008 Free Software Foundation, Inc.
 
 ;; Author: Ted Zlatanov <tzz@lifelogs.com>
 ;; Keywords: news
@@ -10,7 +10,7 @@
 
 ;; GNU Emacs is free software; you can redistribute it and/or modify
 ;; it under the terms of the GNU General Public License as published by
-;; the Free Software Foundation; either version 2, or (at your option)
+;; the Free Software Foundation; either version 3, or (at your option)
 ;; any later version.
 
 ;; GNU Emacs is distributed in the hope that it will be useful,
@@ -63,6 +63,8 @@
 (require 'gnus-util)
 (require 'nnmail)
 
+(defvar gnus-adaptive-word-syntax-table)
+
 (defvar gnus-registry-dirty t
  "Boolean set to t when the registry is modified")
 
                              :test 'equal)
   "*The article registry by Message ID.")
 
-(defcustom gnus-registry-unfollowed-groups '("delayed" "drafts" "queue")
-  "List of groups that gnus-registry-split-fancy-with-parent won't follow.
-The group names are matched, they don't have to be fully qualified."
+(defcustom gnus-registry-marks
+  '((Important
+     :char ?i
+     :image "summary_important")
+    (Work
+     :char ?w
+     :image "summary_work")
+    (Personal
+     :char ?p
+     :image "summary_personal")
+    (To-Do
+     :char ?t
+     :image "summary_todo")
+    (Later
+     :char ?l
+     :image "summary_later"))
+
+  "List of registry marks and their options.
+
+`gnus-registry-mark-article' will offer symbols from this list
+for completion.  
+
+Each entry must have a character to be useful for summary mode
+line display and for keyboard shortcuts.
+
+Each entry must have an image string to be useful for visual
+display."
+  :group 'gnus-registry
+  :type '(repeat :tag "Registry Marks"
+                (cons :tag "Mark"
+                      (symbol :tag "Name")
+                      (checklist :tag "Options" :greedy t
+                                 (group :inline t
+                                        (const :format "" :value :char)
+                                        (character :tag "Character code"))
+                                 (group :inline t
+                                        (const :format "" :value :image)
+                                        (string :tag "Image"))))))
+
+(defcustom gnus-registry-default-mark 'To-Do
+  "The default mark.  Should be a valid key for `gnus-registry-marks'."
   :group 'gnus-registry
-  :type '(repeat string))
+  :type 'symbol)
+
+(defcustom gnus-registry-unfollowed-groups 
+  '("delayed$" "drafts$" "queue$" "INBOX$")
+  "List of groups that gnus-registry-split-fancy-with-parent won't return.
+The group names are matched, they don't have to be fully
+qualified.  This parameter tells the Registry 'never split a
+message into a group that matches one of these, regardless of
+references.'"
+  :group 'gnus-registry
+  :type '(repeat regexp))
 
 (defcustom gnus-registry-install nil
   "Whether the registry should be installed."
@@ -124,6 +174,16 @@ way."
   :group 'gnus-registry
   :type 'boolean)
 
+(defcustom gnus-registry-extra-entries-precious '(marks)
+  "What extra entries are precious, meaning they won't get trimmed.
+When you save the Gnus registry, it's trimmed to be no longer
+than `gnus-registry-max-entries' (which is nil by default, so no
+trimming happens).  Any entries with extra data in this list (by
+default, marks are included, so articles with marks are
+considered precious) will not be trimmed."
+  :group 'gnus-registry
+  :type '(repeat symbol))
+
 (defcustom gnus-registry-cache-file 
   (nnheader-concat 
    (or gnus-dribble-directory gnus-home-directory "~/") 
@@ -153,6 +213,8 @@ way."
       (gnus-load file)
       (gnus-message 5 "Reading %s...done" file))))
 
+;; FIXME: Get rid of duplicated code, cf. `gnus-save-newsrc-file' in
+;; `gnus-start.el'.  --rsteib
 (defun gnus-registry-cache-save ()
   "Save the registry cache file."
   (interactive)
@@ -169,7 +231,8 @@ way."
     (if gnus-save-startup-file-via-temp-buffer
        (let ((coding-system-for-write gnus-ding-file-coding-system)
              (standard-output (current-buffer)))
-         (gnus-gnus-to-quick-newsrc-format t "gnus registry startup file" 'gnus-registry-alist)
+         (gnus-gnus-to-quick-newsrc-format 
+          t "gnus registry startup file" 'gnus-registry-alist)
          (gnus-registry-cache-whitespace file)
          (save-buffer))
       (let ((coding-system-for-write gnus-ding-file-coding-system)
@@ -193,7 +256,8 @@ way."
        (unwind-protect
            (progn
              (gnus-with-output-to-file working-file
-               (gnus-gnus-to-quick-newsrc-format t "gnus registry startup file" 'gnus-registry-alist))
+               (gnus-gnus-to-quick-newsrc-format 
+                t "gnus registry startup file" 'gnus-registry-alist))
 
              ;; These bindings will mislead the current buffer
              ;; into thinking that it is visiting the startup
@@ -306,36 +370,56 @@ way."
 
 (defun gnus-registry-trim (alist)
   "Trim alist to size, using gnus-registry-max-entries.
-Also, drop all gnus-registry-ignored-groups matches."
-  (if (null gnus-registry-max-entries)
+Any entries with extra data (marks, currently) are left alone."
+  (if (null gnus-registry-max-entries)      
       alist                             ; just return the alist
     ;; else, when given max-entries, trim the alist
     (let* ((timehash (make-hash-table
-                     :size 4096
+                     :size 20000
+                     :test 'equal))
+          (precious (make-hash-table
+                     :size 20000
                      :test 'equal))
           (trim-length (- (length alist) gnus-registry-max-entries))
-          (trim-length (if (natnump trim-length) trim-length 0)))
+          (trim-length (if (natnump trim-length) trim-length 0))
+          precious-list junk-list)
       (maphash
        (lambda (key value)
-         (puthash key (gnus-registry-fetch-extra key 'mtime) timehash))
+        (let ((extra (gnus-registry-fetch-extra key)))
+          (dolist (item gnus-registry-extra-entries-precious)
+            (dolist (e extra)
+              (when (equal (nth 0 e) item)
+                (puthash key t precious)
+                (return))))
+          (puthash key (gnus-registry-fetch-extra key 'mtime) timehash)))
        gnus-registry-hashtb)
-      
-      ;; we use the return value of this setq, which is the trimmed alist
-      (setq alist
-           (nthcdr
-            trim-length
-            (sort alist
-                  (lambda (a b)
-                    (time-less-p
-                     (or (cdr (gethash (car a) timehash)) '(0 0 0))
-                     (or (cdr (gethash (car b) timehash)) '(0 0 0))))))))))
 
+      (dolist (item alist)
+       (let ((key (nth 0 item)))
+         (if (gethash key precious)
+             (push item precious-list)
+           (push item junk-list))))
+
+      (sort 
+       junk-list
+       (lambda (a b)
+        (let ((t1 (or (cdr (gethash (car a) timehash)) 
+                      '(0 0 0)))
+              (t2 (or (cdr (gethash (car b) timehash)) 
+                      '(0 0 0))))
+          (time-less-p t1 t2))))
+
+      ;; we use the return value of this setq, which is the trimmed alist
+      (setq alist (append precious-list
+                         (nthcdr trim-length junk-list))))))
+  
 (defun gnus-registry-action (action data-header from &optional to method)
   (let* ((id (mail-header-id data-header))
         (subject (gnus-string-remove-all-properties
                   (gnus-registry-simplify-subject
                    (mail-header-subject data-header))))
-        (sender (gnus-string-remove-all-properties (mail-header-from data-header)))
+        (sender (gnus-string-remove-all-properties 
+                 (mail-header-from data-header)))
         (from (gnus-group-guess-full-name-from-command-method from))
         (to (if to (gnus-group-guess-full-name-from-command-method to) nil))
         (to-name (if to to "the Bit Bucket"))
@@ -376,132 +460,198 @@ This function tracks ALL backends, unlike
 messages.
 
 For a message to be split, it looks for the parent message in the
-References or In-Reply-To header and then looks in the registry to
-see which group that message was put in.  This group is returned.
+References or In-Reply-To header and then looks in the registry
+to see which group that message was put in.  This group is
+returned, unless `gnus-registry-follow-group-p' return nil for
+that group.
 
 See the Info node `(gnus)Fancy Mail Splitting' for more details."
-  (let* ((refstr (or (message-fetch-field "references") "")) ; guarantee string
-        (reply-to (message-fetch-field "in-reply-to"))      ; grab reply-to
+  (let* ((refstr (or (message-fetch-field "references") "")) ; guaranteed
+        (reply-to (message-fetch-field "in-reply-to"))      ; may be nil
         ;; now, if reply-to is valid, append it to the References
         (refstr (if reply-to 
                     (concat refstr " " reply-to)
                   refstr))
-       (nnmail-split-fancy-with-parent-ignore-groups
-        (if (listp nnmail-split-fancy-with-parent-ignore-groups)
-            nnmail-split-fancy-with-parent-ignore-groups
-          (list nnmail-split-fancy-with-parent-ignore-groups)))
-       references res)
-    ;; the references string must be valid and parse to valid references
-    (if (and refstr (gnus-extract-references refstr))
-       (progn
-         (setq references (nreverse (gnus-extract-references refstr)))
-         (mapcar (lambda (x)
-                   (setq res (or (gnus-registry-fetch-group x) res))
-                   (when (or (gnus-registry-grep-in-list
-                              res
-                              gnus-registry-unfollowed-groups)
-                             (gnus-registry-grep-in-list
-                              res
-                              nnmail-split-fancy-with-parent-ignore-groups))
-                     (setq res nil)))
-                 references))
-
-      ;; else: there were no references, now try the extra tracking
-      (let ((sender (gnus-string-remove-all-properties(message-fetch-field "from")))
-           (subject (gnus-string-remove-all-properties
-                     (gnus-registry-simplify-subject
-                      (message-fetch-field "subject"))))
-           (single-match t))
-       (when (and single-match
-                  (gnus-registry-track-sender-p)
-                  sender)
-         (maphash
-          (lambda (key value)
-            (let ((this-sender (cdr
-                                (gnus-registry-fetch-extra key 'sender))))
-              (when (and single-match
-                         this-sender
-                         (equal sender this-sender))
-                ;; too many matches, bail
-                (unless (equal res (gnus-registry-fetch-group key))
-                  (setq single-match nil))
-                (setq res (gnus-registry-fetch-group key))
-                (when (and sender res)
-                  (gnus-message
-                   ;; raise level of messaging if gnus-registry-track-extra
-                   (if gnus-registry-track-extra 7 9)
-                   "%s (extra tracking) traced sender %s to group %s"
-                   "gnus-registry-split-fancy-with-parent"
-                   sender
-                   res)))))
-          gnus-registry-hashtb))
-       (when (and single-match
-                  (gnus-registry-track-subject-p)
-                  subject
-                  (< gnus-registry-minimum-subject-length (length subject)))
-         (maphash
-          (lambda (key value)
-            (let ((this-subject (cdr
-                                 (gnus-registry-fetch-extra key 'subject))))
-              (when (and single-match
-                         this-subject
-                         (equal subject this-subject))
-                ;; too many matches, bail
-                (unless (equal res (gnus-registry-fetch-group key))
-                  (setq single-match nil))
-                (setq res (gnus-registry-fetch-group key))
-                (when (and subject res)
-                  (gnus-message
-                   ;; raise level of messaging if gnus-registry-track-extra
-                   (if gnus-registry-track-extra 7 9)
-                   "%s (extra tracking) traced subject %s to group %s"
-                   "gnus-registry-split-fancy-with-parent"
-                   subject
-                   res)))))
-          gnus-registry-hashtb))
-       (unless single-match
-         (gnus-message
-          3
-          "gnus-registry-split-fancy-with-parent: too many extra matches for %s"
-          refstr)
-         (setq res nil))))
-    (when (and refstr res)
-      (gnus-message
-       5
-       "gnus-registry-split-fancy-with-parent traced %s to group %s"
-       refstr res))
-
-    (when (and res gnus-registry-use-long-group-names)
-      (let ((m1 (gnus-find-method-for-group res))
-           (m2 (or gnus-command-method
-                   (gnus-find-method-for-group gnus-newsgroup-name)))
-           (short-res (gnus-group-short-name res)))
-      (if (gnus-methods-equal-p m1 m2)
-         (progn
+        ;; these may not be used, but the code is cleaner having them up here
+        (sender (gnus-string-remove-all-properties
+                 (message-fetch-field "from")))
+        (subject (gnus-string-remove-all-properties
+                  (gnus-registry-simplify-subject
+                   (message-fetch-field "subject"))))
+
+        (nnmail-split-fancy-with-parent-ignore-groups
+         (if (listp nnmail-split-fancy-with-parent-ignore-groups)
+             nnmail-split-fancy-with-parent-ignore-groups
+           (list nnmail-split-fancy-with-parent-ignore-groups)))
+        (log-agent "gnus-registry-split-fancy-with-parent")
+        found)
+
+    ;; this is a big if-else statement.  it uses
+    ;; gnus-registry-post-process-groups to filter the results after
+    ;; every step.
+    (cond
+     ;; the references string must be valid and parse to valid references
+     ((and refstr (gnus-extract-references refstr))
+      (dolist (reference (nreverse (gnus-extract-references refstr)))
+       (gnus-message
+        9
+        "%s is looking for matches for reference %s from [%s]"
+        log-agent reference refstr)
+       (dolist (group (gnus-registry-fetch-groups reference))
+         (when (and group (gnus-registry-follow-group-p group))
            (gnus-message
-            9
-            "gnus-registry-split-fancy-with-parent stripped group %s to %s"
-            res
-            short-res)
-           (setq res short-res))
-       ;; else...
+            7
+            "%s traced the reference %s from [%s] to group %s"
+            log-agent reference refstr group)
+           (push group found))))
+      ;; filter the found groups and return them
+      (setq found (gnus-registry-post-process-groups 
+                  "references" refstr found)))
+
+     ;; else: there were no matches, now try the extra tracking by sender
+     ((and (gnus-registry-track-sender-p) 
+          sender)
+      (maphash
+       (lambda (key value)
+        (let ((this-sender (cdr
+                            (gnus-registry-fetch-extra key 'sender)))
+              matches)
+          (when (and this-sender
+                     (equal sender this-sender))
+            (setq found (append (gnus-registry-fetch-groups key) found))
+            (push key matches)
+            (gnus-message
+             ;; raise level of messaging if gnus-registry-track-extra
+             (if gnus-registry-track-extra 7 9)
+             "%s (extra tracking) traced sender %s to groups %s (keys %s)"
+             log-agent sender found matches))))
+       gnus-registry-hashtb)
+      ;; filter the found groups and return them
+      (setq found (gnus-registry-post-process-groups "sender" sender found)))
+      
+     ;; else: there were no matches, now try the extra tracking by subject
+     ((and (gnus-registry-track-subject-p)
+          subject
+          (< gnus-registry-minimum-subject-length (length subject)))
+      (maphash
+       (lambda (key value)
+        (let ((this-subject (cdr
+                             (gnus-registry-fetch-extra key 'subject)))
+              matches)
+          (when (and this-subject
+                     (equal subject this-subject))
+            (setq found (append (gnus-registry-fetch-groups key) found))
+            (push key matches)
+            (gnus-message
+             ;; raise level of messaging if gnus-registry-track-extra
+             (if gnus-registry-track-extra 7 9)
+             "%s (extra tracking) traced subject %s to groups %s (keys %s)"
+             log-agent subject found matches))))
+       gnus-registry-hashtb)
+      ;; filter the found groups and return them
+      (setq found (gnus-registry-post-process-groups 
+                  "subject" subject found))))))
+
+(defun gnus-registry-post-process-groups (mode key groups)
+  "Modifies GROUPS found by MODE for KEY to determine which ones to follow.
+
+MODE can be 'subject' or 'sender' for example.  The KEY is the
+value by which MODE was searched.
+
+Transforms each group name to the equivalent short name.
+
+Checks if the current Gnus method (from `gnus-command-method' or
+from `gnus-newsgroup-name') is the same as the group's method.
+This is not possible if gnus-registry-use-long-group-names is
+false.  Foreign methods are not supported so they are rejected.
+
+Reduces the list to a single group, or complains if that's not
+possible."
+  (let ((log-agent "gnus-registry-post-process-group")
+       out)
+    (if gnus-registry-use-long-group-names
+       (dolist (group groups)
+         (let ((m1 (gnus-find-method-for-group group))
+               (m2 (or gnus-command-method
+                       (gnus-find-method-for-group gnus-newsgroup-name)))
+               (short-name (gnus-group-short-name group)))
+           (if (gnus-methods-equal-p m1 m2)
+               (progn
+                 ;; this is REALLY just for debugging
+                 (gnus-message
+                  10
+                  "%s stripped group %s to %s"
+                  log-agent group short-name)
+                 (unless (member short-name out)
+                   (push short-name out)))
+             ;; else...
+             (gnus-message
+              7
+              "%s ignored foreign group %s"
+              log-agent group))))
+      (setq out groups))
+    (when (cdr-safe out)
        (gnus-message
-        7
-        "gnus-registry-split-fancy-with-parent ignored foreign group %s"
-        res)
-       (setq res nil))))
-    res))
+        5
+        "%s: too many extra matches (%s) for %s %s.  Returning none."
+        log-agent out mode key)
+       (setq out nil))
+    out))
+
+(defun gnus-registry-follow-group-p (group)
+  "Determines if a group name should be followed.
+Consults `gnus-registry-unfollowed-groups' and
+`nnmail-split-fancy-with-parent-ignore-groups'."
+  (not (or (gnus-registry-grep-in-list
+           group
+           gnus-registry-unfollowed-groups)
+          (gnus-registry-grep-in-list
+           group
+           nnmail-split-fancy-with-parent-ignore-groups))))
+
+(defun gnus-registry-wash-for-keywords (&optional force)
+  (interactive)
+  (let ((id (gnus-registry-fetch-message-id-fast gnus-current-article))
+       word words)
+    (if (or (not (gnus-registry-fetch-extra id 'keywords))
+           force)
+       (save-excursion
+         (set-buffer gnus-article-buffer)
+         (article-goto-body)
+         (save-window-excursion
+           (save-restriction
+             (narrow-to-region (point) (point-max))
+             (with-syntax-table gnus-adaptive-word-syntax-table
+               (while (re-search-forward "\\b\\w+\\b" nil t)
+                 (setq word (gnus-registry-remove-alist-text-properties
+                             (downcase (buffer-substring
+                                        (match-beginning 0) (match-end 0)))))
+                 (if (> (length word) 3)
+                     (push word words))))))
+         (gnus-registry-store-extra-entry id 'keywords words)))))
+
+(defun gnus-registry-find-keywords (keyword)
+  (interactive "skeyword: ")
+  (let (articles)
+    (maphash
+     (lambda (key value)
+       (when (gnus-registry-grep-in-list
+             keyword
+             (cdr (gnus-registry-fetch-extra key 'keywords)))
+        (push key articles)))
+     gnus-registry-hashtb)
+    articles))
 
 (defun gnus-registry-register-message-ids ()
   "Register the Message-ID of every article in the group"
   (unless (gnus-parameter-registry-ignore gnus-newsgroup-name)
     (dolist (article gnus-newsgroup-articles)
       (let ((id (gnus-registry-fetch-message-id-fast article)))
-       (unless (gnus-registry-fetch-group id)
+       (unless (member gnus-newsgroup-name (gnus-registry-fetch-groups id))
          (gnus-message 9 "Registry: Registering article %d with group %s"
                        article gnus-newsgroup-name)
-         (gnus-registry-add-group
-          (gnus-registry-fetch-message-id-fast article)
+         (gnus-registry-add-group 
+          id 
           gnus-newsgroup-name
           (gnus-registry-fetch-simplified-message-subject-fast article)
           (gnus-registry-fetch-sender-fast article)))))))
@@ -537,15 +687,192 @@ See the Info node `(gnus)Fancy Mail Splitting' for more details."
                          (assoc article (gnus-data-list nil)))))
     nil))
 
+;;; this should be redone with catch/throw
 (defun gnus-registry-grep-in-list (word list)
   (when word
     (memq nil
          (mapcar 'not
                  (mapcar
                   (lambda (x)
-                    (string-match x word))
+                    (string-match word x))
                   list)))))
 
+(defun gnus-registry-do-marks (type function)
+  "For each known mark, call FUNCTION for each cell of type TYPE.
+
+FUNCTION should take two parameters, a mark symbol and the cell value."
+  (dolist (mark-info gnus-registry-marks)
+    (let* ((mark (car-safe mark-info))
+          (data (cdr-safe mark-info))
+          (cell-data (plist-get data type)))
+      (when cell-data
+       (funcall function mark cell-data)))))
+
+;;; this is ugly code, but I don't know how to do it better
+
+;;; TODO: clear the gnus-registry-mark-map before running (but I think
+;;; gnus-define-keys does it by default)
+(defun gnus-registry-install-shortcuts-and-menus ()
+  "Install the keyboard shortcuts and menus for the registry.
+Uses `gnus-registry-marks' to find what shortcuts to install."
+  (gnus-registry-do-marks 
+   :char
+   (lambda (mark data)
+     (let ((function-format
+           (format "gnus-registry-%%s-article-%s-mark" mark)))
+
+;;; The following generates these functions:
+;;; (defun gnus-registry-set-article-Important-mark (&rest articles)
+;;;   "Apply the Important mark to process-marked ARTICLES."
+;;;   (interactive (gnus-summary-work-articles current-prefix-arg))
+;;;   (gnus-registry-set-article-mark-internal 'Important articles nil t))
+;;; (defun gnus-registry-remove-article-Important-mark (&rest articles)
+;;;   "Apply the Important mark to process-marked ARTICLES."
+;;;   (interactive (gnus-summary-work-articles current-prefix-arg))
+;;;   (gnus-registry-set-article-mark-internal 'Important articles t t))
+
+       (dolist (remove '(t nil))
+        (let* ((variant-name (if remove "remove" "set"))
+               (function-name (format function-format variant-name))
+               (shortcut (format "%c" data))
+               (shortcut (if remove (upcase shortcut) shortcut)))
+          (unintern function-name)
+          (eval
+           `(defun 
+              ;; function name
+              ,(intern function-name) 
+              ;; parameter definition
+              (&rest articles)
+              ;; documentation
+              ,(format 
+                "%s the %s mark over process-marked ARTICLES."
+                (upcase-initials variant-name)
+                mark)
+              ;; interactive definition
+              (interactive 
+               (gnus-summary-work-articles current-prefix-arg))
+              ;; actual code
+              (gnus-registry-set-article-mark-internal 
+               ;; all this just to get the mark, I must be doing it wrong
+               (intern ,(symbol-name mark))
+               articles ,remove t)))
+          (gnus-message 9 "Defined mark handling function %s" function-name))))))
+  ;; I don't know how to do this inside the loop above, because
+  ;; gnus-define-keys is a macro
+  (gnus-define-keys (gnus-registry-mark-map "M" gnus-summary-mark-map)
+    "i" gnus-registry-set-article-Important-mark
+    "I" gnus-registry-remove-article-Important-mark
+    "w" gnus-registry-set-article-Work-mark
+    "W" gnus-registry-remove-article-Work-mark
+    "l" gnus-registry-set-article-Later-mark
+    "L" gnus-registry-remove-article-Later-mark
+    "p" gnus-registry-set-article-Personal-mark
+    "P" gnus-registry-remove-article-Personal-mark
+    "t" gnus-registry-set-article-To-Do-mark
+    "T" gnus-registry-remove-article-To-Do-mark))
+
+;;; use like this:
+;;; (defalias 'gnus-user-format-function-M 'gnus-registry-user-format-function-M)
+(defun gnus-registry-user-format-function-M (headers)
+  (let* ((id (mail-header-message-id headers))
+        (marks (when id (gnus-registry-fetch-extra-marks id)))
+        (out ""))
+    (dolist (mark marks)
+      (let ((c (plist-get
+               (cdr-safe
+                (assoc mark gnus-registry-marks)) :char)))
+       (setq out (format "%s%s"
+                         out
+                         (if c
+                             (char-to-string c)
+                           "")))))
+      out))
+
+(defun gnus-registry-read-mark ()
+  "Read a mark name from the user with completion."
+  (let ((mark (gnus-completing-read-with-default 
+              (symbol-name gnus-registry-default-mark)
+              "Label" 
+              (mapcar (lambda (x)      ; completion list
+                        (cons (symbol-name (car-safe x)) (car-safe x)))
+                      gnus-registry-marks))))
+    (when (stringp mark)
+      (intern mark))))
+
+(defun gnus-registry-set-article-mark (&rest articles)
+  "Apply a mark to process-marked ARTICLES."
+  (interactive (gnus-summary-work-articles current-prefix-arg))
+  (gnus-registry-set-article-mark-internal (gnus-registry-read-mark) articles nil t))
+
+(defun gnus-registry-remove-article-mark (&rest articles)
+  "Remove a mark from process-marked ARTICLES."
+  (interactive (gnus-summary-work-articles current-prefix-arg))
+  (gnus-registry-set-article-mark-internal (gnus-registry-read-mark) articles t t))
+
+(defun gnus-registry-set-article-mark-internal (mark articles &optional remove show-message)
+  "Apply a mark to a list of ARTICLES."
+  (let ((article-id-list
+        (mapcar 'gnus-registry-fetch-message-id-fast articles)))
+    (dolist (id article-id-list)
+      (let* (
+            ;; all the marks for this article without the mark of
+            ;; interest
+            (marks
+             (delq mark (gnus-registry-fetch-extra-marks id)))
+            ;; the new marks we want to use
+            (new-marks (if remove
+                           marks
+                         (cons mark marks))))
+       (when show-message
+         (gnus-message 1 "%s mark %s with message ID %s, resulting in %S"
+                       (if remove "Removing" "Adding")
+                       mark id new-marks))
+       
+       (apply 'gnus-registry-store-extra-marks ; set the extra marks
+              id                               ; for the message ID
+              new-marks)))))
+
+(defun gnus-registry-get-article-marks (&rest articles)
+  "Get the Gnus registry marks for ARTICLES and show them if interactive.
+Uses process/prefix conventions.  For multiple articles,
+only the last one's marks are returned."
+  (interactive (gnus-summary-work-articles 1))
+  (let (marks)
+    (dolist (article articles)
+      (let ((article-id
+            (gnus-registry-fetch-message-id-fast article)))
+       (setq marks (gnus-registry-fetch-extra-marks article-id))))
+    (when (interactive-p)
+       (gnus-message 1 "Marks are %S" marks))
+    marks))
+
+;;; if this extends to more than 'marks, it should be improved to be more generic.
+(defun gnus-registry-fetch-extra-marks (id)
+  "Get the marks of a message, based on the message ID.
+Returns a list of symbol marks or nil."
+  (car-safe (cdr (gnus-registry-fetch-extra id 'marks))))
+
+(defun gnus-registry-has-extra-mark (id mark)
+  "Checks if a message has `mark', based on the message ID `id'."
+  (memq mark (gnus-registry-fetch-extra-marks id)))
+
+(defun gnus-registry-store-extra-marks (id &rest mark-list)
+  "Set the marks of a message, based on the message ID.
+The `mark-list' can be nil, in which case no marks are left."
+  (gnus-registry-store-extra-entry id 'marks (list mark-list)))
+
+(defun gnus-registry-delete-extra-marks (id &rest mark-delete-list)
+  "Delete the message marks in `mark-delete-list', based on the message ID."
+  (let ((marks (gnus-registry-fetch-extra-marks id)))
+    (when marks
+      (dolist (mark mark-delete-list)
+       (setq marks (delq mark marks))))
+    (gnus-registry-store-extra-marks id (car marks))))
+
+(defun gnus-registry-delete-all-extra-marks (id)
+  "Delete all the marks for a message ID."
+  (gnus-registry-store-extra-marks id nil))
+
 (defun gnus-registry-fetch-extra (id &optional entry)
   "Get the extra data of a message, based on the message ID.
 Returns the first place where the trail finds a nonstring."
@@ -603,12 +930,20 @@ The message must have at least one group name."
               gnus-registry-hashtb)
       (setq gnus-registry-dirty t)))))
 
+(defun gnus-registry-delete-extra-entry (id key)
+  "Delete a specific entry in the extras field of the registry entry for id."
+  (gnus-registry-store-extra-entry id key nil))
+
 (defun gnus-registry-store-extra-entry (id key value)
   "Put a specific entry in the extras field of the registry entry for id."
   (let* ((extra (gnus-registry-fetch-extra id))
-        (alist (gnus-registry-remove-alist-text-properties 
-                (cons (cons key value)
-                      (gnus-assq-delete-all key (gnus-registry-fetch-extra id))))))
+        ;; all the entries except the one for `key'
+        (the-rest (gnus-assq-delete-all key (gnus-registry-fetch-extra id))) 
+        (alist (if value
+                   (gnus-registry-remove-alist-text-properties
+                    (cons (cons key value)
+                          the-rest))
+                 the-rest)))
     (gnus-registry-store-extra id alist)))
 
 (defun gnus-registry-fetch-group (id)
@@ -721,6 +1056,7 @@ Returns the first place where the trail finds a group name."
   (interactive)
   (setq gnus-registry-install t)
   (gnus-registry-install-hooks)
+  (gnus-registry-install-shortcuts-and-menus)
   (gnus-registry-read))
 
 ;;;###autoload