;;; gnus-registry.el --- article registry for Gnus
-;;; Copyright (C) 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010
-;;; Free Software Foundation, Inc.
+;; Copyright (C) 2002-2012 Free Software Foundation, Inc.
;; Author: Ted Zlatanov <tzz@lifelogs.com>
;; Keywords: news registry
;;; Commentary:
;; This is the gnus-registry.el package, which works with all
-;; backends, not just nnmail (e.g. NNTP). The major issue is that it
+;; Gnus backends, not just nnmail. The major issue is that it
;; doesn't go across backends, so for instance if an article is in
;; nnml:sys and you see a reference to it in nnimap splitting, the
;; article will end up in nnimap:sys
;; gnus-registry.el intercepts article respooling, moving, deleting,
;; and copying for all backends. If it doesn't work correctly for
;; you, submit a bug report and I'll be glad to fix it. It needs
-;; documentation in the manual (also on my to-do list).
+;; better documentation in the manual (also on my to-do list).
-;; Put this in your startup file (~/.gnus.el for instance)
+;; If you want to track recipients (and you should to make the
+;; gnus-registry splitting work better), you need the To and Cc
+;; headers collected by Gnus. Note that in more recent Gnus versions
+;; this is already the case: look at `gnus-extra-headers' to be sure.
+
+;; ;;; you may also want Gcc Newsgroups Keywords X-Face
+;; (add-to-list 'gnus-extra-headers 'To)
+;; (add-to-list 'gnus-extra-headers 'Cc)
+;; (setq nnmail-extra-headers gnus-extra-headers)
+
+;; Put this in your startup file (~/.gnus.el for instance) or use Customize:
;; (setq gnus-registry-max-entries 2500
-;; gnus-registry-use-long-group-names t)
+;; gnus-registry-track-extra '(sender subject recipient))
;; (gnus-registry-initialize)
;; (: gnus-registry-split-fancy-with-parent)
+;; You should also consider using the nnregistry backend to look up
+;; articles. See the Gnus manual for more information.
+
+;; Finally, you can put %uM in your summary line format to show the
+;; registry marks if you do this:
+
+;; show the marks as single characters (see the :char property in
+;; `gnus-registry-marks'):
+;; (defalias 'gnus-user-format-function-M 'gnus-registry-article-marks-to-chars)
+
+;; show the marks by name (see `gnus-registry-marks'):
+;; (defalias 'gnus-user-format-function-M 'gnus-registry-article-marks-to-names)
+
;; TODO:
;; - get the correct group on spool actions
-;; - articles that are spooled to a different backend should be handled
+;; - articles that are spooled to a different backend should be moved
+;; after splitting
;;; Code:
(eval-when-compile (require 'cl))
+(eval-when-compile
+ (when (null (ignore-errors (require 'ert)))
+ (defmacro* ert-deftest (name () &body docstring-keys-and-body))))
+
+(ignore-errors
+ (require 'ert))
(require 'gnus)
(require 'gnus-int)
(require 'gnus-sum)
+(require 'gnus-art)
(require 'gnus-util)
(require 'nnmail)
(require 'easymenu)
+(require 'registry)
(defvar gnus-adaptive-word-syntax-table)
:version "22.1"
:group 'gnus)
-(defvar gnus-registry-hashtb (make-hash-table
- :size 256
- :test 'equal)
- "*The article registry by Message ID.")
-
-(defcustom gnus-registry-marks
+(defvar gnus-registry-marks
'((Important
:char ?i
:image "summary_important")
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"))))))
+display.")
(defcustom gnus-registry-default-mark 'To-Do
"The default mark. Should be a valid key for `gnus-registry-marks'."
:group 'gnus-registry
:type 'symbol)
+(defcustom gnus-registry-unfollowed-addresses
+ (list (regexp-quote user-mail-address))
+ "List of addresses that gnus-registry-split-fancy-with-parent won't trace.
+The addresses are matched, they don't have to be fully qualified.
+In the messages, these addresses can be the sender or the
+recipients."
+ :group 'gnus-registry
+ :type '(repeat regexp))
+
(defcustom gnus-registry-unfollowed-groups
- '("delayed$" "drafts$" "queue$" "INBOX$")
+ '("delayed$" "drafts$" "queue$" "INBOX$" "^nnmairix:" "archive")
"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
+qualified. This parameter tells the Gnus registry 'never split a
message into a group that matches one of these, regardless of
-references.'"
+references.'
+
+nnmairix groups are specifically excluded because they are ephemeral."
:group 'gnus-registry
:type '(repeat regexp))
"Whether the registry should be installed."
:group 'gnus-registry
:type '(choice (const :tag "Never Install" nil)
- (const :tag "Always Install" t)
- (const :tag "Ask Me" ask)))
-
-(defvar gnus-summary-misc-menu) ;; Avoid byte compiler warning.
+ (const :tag "Always Install" t)
+ (const :tag "Ask Me" ask)))
-(defvar gnus-registry-misc-menus nil) ; ugly way to keep the menus
+(defvar gnus-registry-enabled nil)
-(defcustom gnus-registry-clean-empty t
- "Whether the empty registry entries should be deleted.
-Registry entries are considered empty when they have no groups
-and no extra data."
- :group 'gnus-registry
- :type 'boolean)
+(defvar gnus-summary-misc-menu) ;; Avoid byte compiler warning.
-(defcustom gnus-registry-use-long-group-names t
- "Whether the registry should use long group names."
- :group 'gnus-registry
- :type 'boolean)
+(defvar gnus-registry-misc-menus nil) ; ugly way to keep the menus
-(defcustom gnus-registry-max-track-groups 20
- "The maximum number of non-unique group matches to check for a message ID."
- :group 'gnus-registry
- :type '(radio (const :format "Unlimited " nil)
- (integer :format "Maximum non-unique matches: %v")))
+(make-obsolete-variable 'gnus-registry-clean-empty nil "23.4")
+(make-obsolete-variable 'gnus-registry-use-long-group-names nil "23.4")
+(make-obsolete-variable 'gnus-registry-max-track-groups nil "23.4")
+(make-obsolete-variable 'gnus-registry-entry-caching nil "23.4")
+(make-obsolete-variable 'gnus-registry-trim-articles-without-groups nil "23.4")
-(defcustom gnus-registry-track-extra nil
+(defcustom gnus-registry-track-extra '(subject sender recipient)
"Whether the registry should track extra data about a message.
-The Subject and Sender (From:) headers are currently tracked this
-way."
+The subject, recipients (To: and Cc:), and Sender (From:) headers
+are tracked this way by default."
:group 'gnus-registry
:type
'(set :tag "Tracking choices"
(const :tag "Track by subject (Subject: header)" subject)
+ (const :tag "Track by recipient (To: and Cc: headers)" recipient)
(const :tag "Track by sender (From: header)" sender)))
(defcustom gnus-registry-split-strategy nil
- "Whether the registry should track extra data about a message.
-The Subject and Sender (From:) headers are currently tracked this
-way."
- :group 'gnus-registry
- :type
- '(choice :tag "Tracking choices"
- (const :tag "Only use single choices, discard multiple matches" nil)
- (const :tag "Majority of matches wins" majority)
- (const :tag "First found wins" first)))
+ "The splitting strategy applied to the keys in `gnus-registry-track-extra'.
+
+Given a set of unique found groups G and counts for each element
+of G, and a key K (typically 'sender or 'subject):
+
+When nil, if G has only one element, use it. Otherwise give up.
+This is the fastest but also least useful strategy.
-(defcustom gnus-registry-entry-caching t
- "Whether the registry should cache extra information."
+When 'majority, use the majority by count. So if there is a
+group with the most articles counted by K, use that. Ties are
+resolved in no particular order, simply the first one found wins.
+This is the slowest strategy but also the most accurate one.
+
+When 'first, the first element of G wins. This is fast and
+should be OK if your senders and subjects don't \"bleed\" across
+groups."
:group 'gnus-registry
- :type 'boolean)
+ :type
+ '(choice :tag "Splitting strategy"
+ (const :tag "Only use single choices, discard multiple matches" nil)
+ (const :tag "Majority of matches wins" majority)
+ (const :tag "First found wins" first)))
(defcustom gnus-registry-minimum-subject-length 5
"The minimum length of a subject before it's considered trackable."
:group 'gnus-registry
:type 'integer)
-(defcustom gnus-registry-trim-articles-without-groups t
- "Whether the registry should clean out message IDs without groups."
- :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."
+(defcustom gnus-registry-extra-entries-precious '(mark)
+ "What extra keys are precious, meaning entries with them won't get pruned.
+By default, 'mark is included, so articles with marks are
+considered precious.
+
+Before you save the Gnus registry, it's pruned. Any entries with
+keys in this list will not be pruned. All other entries go to
+the Bit Bucket."
:group 'gnus-registry
:type '(repeat symbol))
(defcustom gnus-registry-cache-file
(nnheader-concat
(or gnus-dribble-directory gnus-home-directory "~/")
- ".gnus.registry.eld")
+ ".gnus.registry.eioio")
"File where the Gnus registry will be stored."
:group 'gnus-registry
:type 'file)
"Maximum number of entries in the registry, nil for unlimited."
:group 'gnus-registry
:type '(radio (const :format "Unlimited " nil)
- (integer :format "Maximum number: %v")))
-
-(defun gnus-registry-track-subject-p ()
- (memq 'subject gnus-registry-track-extra))
+ (integer :format "Maximum number: %v")))
-(defun gnus-registry-track-sender-p ()
- (memq 'sender gnus-registry-track-extra))
+(defcustom gnus-registry-max-pruned-entries nil
+ "Maximum number of pruned entries in the registry, nil for unlimited."
+ :group 'gnus-registry
+ :type '(radio (const :format "Unlimited " nil)
+ (integer :format "Maximum number: %v")))
+
+(defun gnus-registry-fixup-registry (db)
+ (when db
+ (let ((old (oref db :tracked)))
+ (oset db :precious
+ (append gnus-registry-extra-entries-precious
+ '()))
+ (oset db :max-hard
+ (or gnus-registry-max-entries
+ most-positive-fixnum))
+ (oset db :prune-factor
+ 0.1)
+ (oset db :max-soft
+ (or gnus-registry-max-pruned-entries
+ most-positive-fixnum))
+ (oset db :tracked
+ (append gnus-registry-track-extra
+ '(mark group keyword)))
+ (when (not (equal old (oref db :tracked)))
+ (gnus-message 9 "Reindexing the Gnus registry (tracked change)")
+ (registry-reindex db))))
+ db)
+
+(defun gnus-registry-make-db (&optional file)
+ (interactive "fGnus registry persistence file: \n")
+ (gnus-registry-fixup-registry
+ (registry-db
+ "Gnus Registry"
+ :file (or file gnus-registry-cache-file)
+ ;; these parameters are set in `gnus-registry-fixup-registry'
+ :max-hard most-positive-fixnum
+ :max-soft most-positive-fixnum
+ :precious nil
+ :tracked nil)