9200987332daa43fe602c419fcc7f3eb595f9cc0
[elisp/gnus.git-] / lisp / gnus-bbdb.el
1 ;;; -*- Mode:Emacs-Lisp -*-
2
3 ;;; This file is part of Semi-gnus.
4 ;;; copyright (c) 1991, 1992, 1993 Jamie Zawinski <jwz@netscape.com>.
5 ;;;               1998             Keiichi Suzuki <kei-suzu@mail.wbs.ne.jp>
6
7 ;;; The Insidious Big Brother Database is free software; you can redistribute
8 ;;; it and/or modify it under the terms of the GNU General Public License as
9 ;;; published by the Free Software Foundation; either version 1, or (at your
10 ;;; option) any later version.
11 ;;;
12 ;;; BBDB is distributed in the hope that it will be useful, but WITHOUT ANY
13 ;;; WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14 ;;; FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
15 ;;; details.
16 ;;;
17 ;;; You should have received a copy of the GNU General Public License
18 ;;; along with GNU Emacs; see the file COPYING.  If not, write to
19 ;;; the Free Software Foundation, 675 Mass Ave, Cambridge, MA 02139, USA.
20
21 (require 'bbdb)
22 (require 'gnus)
23 (eval-when-compile
24   (require 'gnus-win))
25
26 ;;;###autoload
27 (defun gnus-bbdb/update-record (&optional offer-to-create)
28   "returns the record corresponding to the current GNUS message, creating 
29 or modifying it as necessary.  A record will be created if 
30 bbdb/news-auto-create-p is non-nil, or if OFFER-TO-CREATE is true and
31 the user confirms the creation."
32   (if bbdb-use-pop-up
33       (gnus-bbdb/pop-up-bbdb-buffer offer-to-create)
34     (let (from)
35       (save-restriction
36         (set-buffer gnus-original-article-buffer)
37         (setq from (mail-header-from mime-message-structure))
38         (when (or (null from)
39                   (string-match (bbdb-user-mail-names)
40                                 (mail-strip-quoted-names from)))
41           ;; if logged-in user sent this, use recipients.
42           (widen)
43           (narrow-to-region (point-min)
44                             (progn (goto-char (point-min))
45                                    (or (search-forward "\n\n" nil t)
46                                        (error "message unexists"))
47                                    (- (point) 2)))
48           (let ((to (mail-fetch-field "to")))
49             (when to
50              (setq from (mime-decode-field-body to 'To 'unfolding))))))
51       (when from
52         (bbdb-annotate-message-sender from t
53                                       (or (bbdb-invoke-hook-for-value
54                                            bbdb/news-auto-create-p)
55                                           offer-to-create)
56                                       offer-to-create)))))
57
58 ;;;###autoload
59 (defun gnus-bbdb/annotate-sender (string &optional replace)
60   "Add a line to the end of the Notes field of the BBDB record 
61 corresponding to the sender of this message.  If REPLACE is non-nil,
62 replace the existing notes entry (if any)."
63   (interactive (list (if bbdb-readonly-p
64                          (error "The Insidious Big Brother Database is read-only.")
65                          (read-string "Comments: "))))
66   (bbdb-annotate-notes (gnus-bbdb/update-record t) string 'notes replace))
67
68 (defun gnus-bbdb/edit-notes (&optional arg)
69   "Edit the notes field or (with a prefix arg) a user-defined field
70 of the BBDB record corresponding to the sender of this message."
71   (interactive "P")
72   (let ((record (or (gnus-bbdb/update-record t) (error ""))))
73     (bbdb-display-records (list record))
74     (if arg
75         (bbdb-record-edit-property record nil t)
76       (bbdb-record-edit-notes record t))))
77
78 ;;;###autoload
79 (defun gnus-bbdb/show-sender ()
80   "Display the contents of the BBDB for the sender of this message.
81 This buffer will be in bbdb-mode, with associated keybindings."
82   (interactive)
83   (let ((record (gnus-bbdb/update-record t)))
84     (if record
85         (bbdb-display-records (list record))
86         (error "unperson"))))
87
88
89 (defun gnus-bbdb/pop-up-bbdb-buffer (&optional offer-to-create)
90   "Make the *BBDB* buffer be displayed along with the GNUS windows,
91 displaying the record corresponding to the sender of the current message."
92   (let ((bbdb-gag-messages t)
93         (bbdb-use-pop-up nil)
94         (bbdb-electric-p nil))
95     (let ((record (gnus-bbdb/update-record offer-to-create))
96           (bbdb-elided-display (bbdb-pop-up-elided-display))
97           (b (current-buffer)))
98       ;; display the bbdb buffer iff there is a record for this article.
99       (cond (record
100              (bbdb-pop-up-bbdb-buffer
101               (function (lambda (w)
102                           (let ((b (current-buffer)))
103                             (set-buffer (window-buffer w))
104                             (prog1 (or (eq major-mode 'mime-veiw-mode)
105                                        (eq major-mode 'gnus-article-mode))
106                                    (set-buffer b))))))
107              (bbdb-display-records (list record)))
108             (t
109              (or bbdb-inside-electric-display
110                  (not (get-buffer-window bbdb-buffer-name))
111                  (let (w)
112                    (delete-other-windows)
113                    (if (assq 'article gnus-buffer-configuration)
114                        (gnus-configure-windows 'article)
115                      (gnus-configure-windows 'SelectArticle))
116                    (if (setq w (get-buffer-window gnus-summary-buffer))
117                        (select-window w))
118                    ))))
119       (set-buffer b)
120       record)))
121
122 ;;
123 ;; Announcing BBDB entries in the summary buffer
124 ;;
125
126 (defcustom gnus-bbdb/lines-and-from-length 18
127   "*The number of characters used to display From: info in Gnus, if you have
128 set gnus-optional-headers to 'gnus-bbdb/lines-and-from."
129   :group 'bbdb-mua-specific-gnus
130   :type 'integer)
131
132 (defcustom gnus-bbdb/summary-mark-known-posters t
133   "*If t, mark messages created by people with records in the BBDB.
134 In GNUS, this marking will take place in the subject list (assuming
135 `gnus-optional-headers' contains `gnus-bbdb/lines-and-from').  In Gnus, the
136 marking will take place in the Summary buffer if the format code defined by
137 `gnus-bbdb/summary-user-format-letter' is used in `gnus-summary-line-format'.
138 This variable has no effect on the marking controlled by
139 `gnus-bbdb/summary-in-bbdb-format-letter'."
140   :group 'bbdb-mua-specific-gnus
141   :type '(choice (const :tag "Mark known posters" t)
142                  (const :tag "Do not mark known posters" nil)))
143 (defvaralias 'gnus-bbdb/mark-known-posters
144   'gnus-bbdb/summary-mark-known-posters)
145
146 (defcustom gnus-bbdb/summary-known-poster-mark "+"
147   "This is the default character to prefix author names with if
148 gnus-bbdb/summary-mark-known-posters is t.  If the poster's record has
149 an entry in the field named by bbdb-message-marker-field, then that will
150 be used instead."
151   :group 'bbdb-mua-specific-gnus
152   :type 'character)
153
154 (defcustom gnus-bbdb/summary-show-bbdb-names t
155   "*If both this variable and `gnus-bbdb/summary-prefer-real-names' are true,
156 then for messages from authors who are in your database, the name
157 displayed will be the primary name in the database, rather than the
158 one in the From line of the message.  This doesn't affect the names of
159 people who aren't in the database, of course.  (`gnus-optional-headers'
160 must be `gnus-bbdb/lines-and-from' for GNUS users.)"
161   :group 'bbdb-mua-specific-gnus
162   :type 'boolean)
163 (defvaralias 'gnus-bbdb/header-show-bbdb-names
164   'gnus-bbdb/summary-show-bbdb-names)
165
166 (defcustom gnus-bbdb/summary-prefer-bbdb-data t
167   "If t, then for posters who are in our BBDB, replace the information
168 provided in the From header with data from the BBDB."
169   :group 'bbdb-mua-specific-gnus
170   :type 'boolean)
171
172 (defcustom gnus-bbdb/summary-prefer-real-names t
173   "If t, then display the poster's name from the BBDB if we have one,
174 otherwise display his/her primary net address if we have one.  If it
175 is set to the symbol bbdb, then real names will be used from the BBDB
176 if present, otherwise the net address in the post will be used.  If
177 gnus-bbdb/summary-prefer-bbdb-data is nil, then this has no effect.
178 See `gnus-bbdb/lines-and-from' for GNUS users, or
179 `gnus-bbdb/summary-user-format-letter' for Gnus users."
180   :group 'bbdb-mua-specific-gnus
181   :type '(choice (const :tag "Prefer real names" t)
182                  (const :tag "Prefer network addresses" nil)))
183 (defvaralias 'gnus-bbdb/header-prefer-real-names
184   'gnus-bbdb/summary-prefer-real-names)
185
186 (defcustom gnus-bbdb/summary-user-format-letter "B"
187   "This is the gnus-user-format-function- that will be used to insert
188 the information from the BBDB in the summary buffer (using
189 `gnus-bbdb/summary-get-author').  This format code is meant to replace
190 codes that insert sender names or addresses (like %A or %n). Unless
191 you've alread got other code using user format B, you might as well
192 stick with the default.  Additionally, if the value of this variable
193 is nil, no format function will be installed for
194 `gnus-bbdb/summary-get-author'.  See also
195 `gnus-bbdb/summary-in-bbdb-format-letter', which installs a format
196 code for `gnus-bbdb/summary-author-in-bbdb'"
197   :group 'bbdb-mua-specific-gnus
198   :type 'character)
199
200 (defcustom gnus-bbdb/summary-in-bbdb-format-letter "b"
201   "This is the gnus-user-format-function- that will be used to insert
202 `gnus-bbdb/summary-known-poster-mark' (using
203 `gnus-bbdb/summary-author-in-bbdb') if the poster is in the BBDB, and
204 \" \" if not.  If the value of this variable is nil, no format code
205 will be installed for `gnus-bbdb/summary-author-in-bbdb'.  See also
206 `gnus-bbdb/summary-user-format-letter', which installs a format code
207 for `gnus-bbdb/summary-get-author'."
208   :group 'bbdb-mua-specific-gnus
209   :type 'character)
210
211 (defcustom bbdb-message-marker-field 'mark-char
212   "*The field whose value will be used to mark messages by this user in Gnus."
213   :group 'bbdb-mua-specific-gnus
214   :type 'symbol)
215
216 ;;;###autoload
217 (defun gnus-bbdb/lines-and-from (header)
218   "Useful as the value of gnus-optional-headers in *GNUS* (not Gnus).
219 NOTE: This variable no longer seems to be present in Gnus.  It seems
220 to have been replaced by `message-default-headers', which only takes
221 strings.  In the future this should change."
222   (let* ((length gnus-bbdb/lines-and-from-length)
223          (lines (mail-header-lines header))
224          (from (mail-header-from header))
225          (data (and (or gnus-bbdb/summary-mark-known-posters
226                         gnus-bbdb/summary-show-bbdb-names)
227                     (condition-case ()
228                         (mail-extract-address-components from)
229                       (error nil))))
230          (name (car data))
231          (net (car (cdr data)))
232          (record (and data 
233                       (bbdb-search-simple name 
234                        (if (and net bbdb-canonicalize-net-hook)
235                            (bbdb-canonicalize-address net)
236                          net))))
237          string L)
238
239     (if (and record name (member (downcase name) (bbdb-record-net record)))
240         ;; bogon!
241         (setq record nil))
242
243     (setq name 
244           (or (and gnus-bbdb/summary-prefer-bbdb-data
245                    (or (and gnus-bbdb/summary-prefer-real-names
246                             (and record (bbdb-record-name record)))
247                        (and record (bbdb-record-net record)
248                             (nth 0 (bbdb-record-net record)))))
249               (and gnus-bbdb/summary-prefer-real-names
250                    (or (and (equal gnus-bbdb/summary-prefer-real-names 'bbdb)
251                             net)
252                        name))
253               net from "**UNKNOWN**"))
254       ;; GNUS can't cope with extra square-brackets appearing in the summary.
255       (if (and name (string-match "[][]" name))
256           (progn (setq name (copy-sequence name))
257                  (while (string-match "[][]" name)
258                    (aset name (match-beginning 0) ? ))))
259       (setq string (format "%s%3d:%s"
260                            (if (and record gnus-bbdb/summary-mark-known-posters)
261                                (or (bbdb-record-getprop
262                                     record bbdb-message-marker-field)
263                                    "*")
264                              " ")
265                            lines (or name from))
266             L (length string))
267       (cond ((> L length) (substring string 0 length))
268             ((< L length) (concat string (make-string (- length L) ? )))
269             (t string))))
270
271 (defun gnus-bbdb/summary-get-author (header)
272   "Given a Gnus message header, returns the appropriate piece of
273 information to identify the author in a Gnus summary line, depending on
274 the settings of the various configuration variables.  See the
275 documentation for the following variables for more details:
276   `gnus-bbdb/summary-mark-known-posters'
277   `gnus-bbdb/summary-known-poster-mark'
278   `gnus-bbdb/summary-prefer-bbdb-data'
279   `gnus-bbdb/summary-prefer-real-names'
280 This function is meant to be used with the user function defined in
281   `gnus-bbdb/summary-user-format-letter'"
282   (let* ((from (mail-header-from header))
283          (data (and gnus-bbdb/summary-show-bbdb-names
284                     (condition-case ()
285                         (mail-extract-address-components from)
286                       (error nil))))
287          (name (car data))
288          (net (car (cdr data)))
289          (record (and data 
290                       (bbdb-search-simple name 
291                        (if (and net bbdb-canonicalize-net-hook)
292                            (bbdb-canonicalize-address net)
293                          net)))))
294     (if (and record name (member (downcase name) (bbdb-record-net record)))
295         ;; bogon!
296         (setq record nil))
297     (setq name 
298           (or (and gnus-bbdb/summary-prefer-bbdb-data
299                    (or (and gnus-bbdb/summary-prefer-real-names
300                             (and record (bbdb-record-name record)))
301                        (and record (bbdb-record-net record)
302                             (nth 0 (bbdb-record-net record)))))
303               (and gnus-bbdb/summary-prefer-real-names
304                    (or (and (equal gnus-bbdb/summary-prefer-real-names 'bbdb)
305                             net)
306                        name))
307               net from "**UNKNOWN**"))
308     (format "%s%s"
309             (or (and record gnus-bbdb/summary-mark-known-posters
310                      (or (bbdb-record-getprop
311                           record bbdb-message-marker-field)
312                          gnus-bbdb/summary-known-poster-mark))
313                 " ")
314             name)))
315
316 ;; DEBUG: (gnus-bbdb/summary-author-in-bbdb "From: simmonmt@acm.org")
317 (defun gnus-bbdb/summary-author-in-bbdb (header)
318   "Given a Gnus message header, returns a mark if the poster is in the BBDB, \" \" otherwise.  The mark itself is the value of the field indicated by `bbdb-message-marker-field' (`mark-char' by default) if the indicated field is in the poster's record, and `gnus-bbdb/summary-known-poster-mark' otherwise."
319   (let* ((from (mail-header-from header))
320          (data (condition-case ()
321                    (mail-extract-address-components from)
322                  (error nil)))
323          (name (car data))
324          (net (cadr data))
325          record)
326     (if (and data
327              (setq record
328                    (bbdb-search-simple
329                     name (if (and net bbdb-canonicalize-net-hook)
330                              (bbdb-canonicalize-address net)
331                            net))))
332         (or (bbdb-record-getprop
333              record bbdb-message-marker-field)
334             gnus-bbdb/summary-known-poster-mark) " ")))
335
336 ;;
337 ;; Scoring
338 ;;
339
340 (defcustom gnus-bbdb/score-field 'gnus-score
341   "This variable contains the name of the BBDB field which should be
342 checked for a score to add to the net addresses in the same record."
343   :group 'bbdb-mua-specific-gnus-scoring
344   :type 'symbol)
345
346 (defcustom gnus-bbdb/score-default nil
347   "If this is set, then every net address in the BBDB that does not have
348 an associated score field will be assigned this score.  A value of nil
349 implies a default score of zero."
350   :group 'bbdb-mua-specific-gnus-scoring
351   :type '(choice (const :tag "Do not assign default score")
352                  (integer :tag "Assign this default score" 0)))
353
354 (defvar gnus-bbdb/score-default-internal nil
355   "Internal variable for detecting changes to
356 `gnus-bbdb/score-default'.  You should not set this variable directly -
357 set `gnus-bbdb/score-default' instead.")
358
359 (defvar gnus-bbdb/score-alist nil
360   "The text version of the scoring structure returned by
361 gnus-bbdb/score.  This is built automatically from the BBDB.")
362
363 (defvar gnus-bbdb/score-rebuild-alist t
364   "Set to t to rebuild gnus-bbdb/score-alist on the next call to
365 gnus-bbdb/score.  This will be set automatically if you change a BBDB
366 record which contains a gnus-score field.")
367
368 (defun gnus-bbdb/score-invalidate-alist (rec)
369   "This function is called through bbdb-after-change-hook, and sets
370 gnus-bbdb/score-rebuild-alist to t if the changed record contains a
371 gnus-score field."
372   (if (bbdb-record-getprop rec gnus-bbdb/score-field)
373       (setq gnus-bbdb/score-rebuild-alist t)))
374
375 ;;;###autoload
376 (defun gnus-bbdb/score (group)
377   "This returns a score alist for GNUS.  A score pair will be made for
378 every member of the net field in records which also have a gnus-score
379 field.  This allows the BBDB to serve as a supplemental global score
380 file, with the advantage that it can keep up with multiple and changing
381 addresses better than the traditionally static global scorefile."
382   (list (list
383    (condition-case nil
384        (read (gnus-bbdb/score-as-text group))
385      (error (setq gnus-bbdb/score-rebuild-alist t)
386             (message "Problem building BBDB score table.")
387             (ding) (sit-for 2)
388             nil)))))
389
390 (defun gnus-bbdb/score-as-text (group)
391   "Returns a SCORE file format string built from the BBDB."
392   (cond ((or (cond ((/= (or gnus-bbdb/score-default 0)
393                         (or gnus-bbdb/score-default-internal 0))
394                     (setq gnus-bbdb/score-default-internal
395                           gnus-bbdb/score-default)
396                     t))
397             (not gnus-bbdb/score-alist)
398             gnus-bbdb/score-rebuild-alist)
399     (setq gnus-bbdb/score-rebuild-alist nil)
400     (setq gnus-bbdb/score-alist
401           (concat "((touched nil) (\"from\"\n"
402                   (mapconcat
403                    (lambda (rec)
404                      (let ((score (or (bbdb-record-getprop rec
405                                                            gnus-bbdb/score-field)
406                                       gnus-bbdb/score-default))
407                            (net (bbdb-record-net rec)))
408                        (if (not (and score net)) nil
409                          (mapconcat
410                           (lambda (addr)
411                             (concat "(\"" addr "\" " score ")\n"))
412                           net ""))))
413                    (bbdb-records) "")
414                   "))"))))
415   gnus-bbdb/score-alist)
416
417 (defun gnus-bbdb/extract-field-value-init ()
418   (when (or (and (eq (current-buffer) (get-buffer gnus-article-buffer))
419                  (buffer-live-p gnus-original-article-buffer)
420                  (set-buffer gnus-original-article-buffer))
421             (eq (current-buffer) (get-buffer gnus-original-article-buffer)))
422     (widen)
423     (narrow-to-region (point-min)
424                       (progn (goto-char (point-min))
425                              (or (search-forward "\n\n" nil t)
426                                  (error "message unexists"))
427                              (- (point) 2)))
428     'gnus-bbdb/extract-field-value))
429
430 (defun gnus-bbdb/extract-field-value (field-name)
431   "Given the name of a field (like \"Subject\") this returns the value of
432 that field in the current message, or nil.  This works whether you're in
433 Semi-gnus, Rmail, or VM.  This works on multi-line fields, but if more than
434 one field of the same name is present, only the last is returned.  It is
435 expected that the current buffer has a message in it, and (point) is at the
436 beginning of the message headers."
437   ;; we can't special-case VM here to use its cache, because the cache has
438   ;; divided real-names from addresses; the actual From: and Subject: fields
439   ;; exist only in the message.
440   (let (value)
441     (when (setq value (mail-fetch-field field-name))
442       (mime-decode-field-body value
443                               (intern (capitalize field-name))
444                               'unfolding))))
445
446 ;;
447 ;; Insinuation
448 ;;
449
450 ;;;###autoload
451 (defun gnus-bbdb-insinuate ()
452   "Call this function to hook BBDB into Semi-gnus."
453 ;;  (setq gnus-optional-headers 'gnus-bbdb/lines-and-from)
454   (when (boundp 'bbdb-extract-field-value-function-list)
455     (add-to-list 'bbdb-extract-field-value-function-list
456                  'gnus-bbdb/extract-field-value-init))
457   (add-hook 'gnus-article-prepare-hook 'gnus-bbdb/update-record)
458   (add-hook 'gnus-save-newsrc-hook 'bbdb-offer-save)
459   (define-key gnus-summary-mode-map ":" 'gnus-bbdb/show-sender)
460   (define-key gnus-summary-mode-map ";" 'gnus-bbdb/edit-notes)
461
462   ;; Set up user field for use in gnus-summary-line-format
463   (let ((get-author-user-fun (intern
464                               (concat "gnus-user-format-function-"
465                                       gnus-bbdb/summary-user-format-letter)))
466         (in-bbdb-user-fun (intern
467                            (concat "gnus-user-format-function-"
468                                    gnus-bbdb/summary-in-bbdb-format-letter))))
469                                         ; The big one - whole name
470     (cond (gnus-bbdb/summary-user-format-letter
471            (if (and (fboundp get-author-user-fun)
472                     (not (eq (symbol-function get-author-user-fun)
473                              'gnus-bbdb/summary-get-author)))
474                (bbdb-warn
475                 (format "`gnus-user-format-function-%s' already seems to be in use.
476 Please redefine `gnus-bbdb/summary-user-format-letter' to a different letter."
477                         gnus-bbdb/summary-user-format-letter))
478              (fset get-author-user-fun 'gnus-bbdb/summary-get-author))))
479     
480     ; One tick.  One tick only, please
481     (cond (gnus-bbdb/summary-in-bbdb-format-letter
482            (if (and (fboundp in-bbdb-user-fun)
483                     (not (eq (symbol-function in-bbdb-user-fun)
484                              'gnus-bbdb/summary-author-in-bbdb)))
485                (bbdb-warn
486                 (format "`gnus-user-format-function-%s' already seems to be in use.
487 Redefine `gnus-bbdb/summary-in-bbdb-format-letter' to a different letter."
488                         gnus-bbdb/summary-in-bbdb-format-letter))
489              (fset in-bbdb-user-fun 'gnus-bbdb/summary-author-in-bbdb)))))
490   
491   ;; Scoring
492   (add-hook 'bbdb-after-change-hook 'gnus-bbdb/score-invalidate-alist)
493 ;  (setq gnus-score-find-score-files-function
494 ;       (if (boundp 'gnus-score-find-score-files-function)
495 ;           (cond ((functionp gnus-score-find-score-files-function)
496 ;                  (list gnus-score-find-score-files-function
497 ;                        'gnus-bbdb/score))
498 ;                 ((listp gnus-score-find-score-files-function)
499 ;                  (append gnus-score-find-score-files-function
500 ;                          'gnus-bbdb/score))
501 ;                 (t 'gnus-bbdb/score))
502 ;         'gnus-bbdb/score))
503   )
504
505 ;;;###autoload
506 (defun gnus-bbdb-insinuate-message ()
507   "Call this function to hook BBDB into message-mode."
508   (define-key message-mode-map "\M-\t" 'bbdb-complete-name))
509
510 (provide 'gnus-bbdb)