]> code.delx.au - gnu-emacs-elpa/blob - packages/gnorb/gnorb-bbdb.el
24049856f1619ac14c3763bb1b4c4f29c80da9ad
[gnu-emacs-elpa] / packages / gnorb / gnorb-bbdb.el
1 ;;; gnorb-bbdb.el --- The BBDB-centric functions of gnorb
2
3 ;; Copyright (C) 2014 Free Software Foundation, Inc.
4
5 ;; Author: Eric Abrahamsen <eric@ericabrahamsen.net>
6 ;; Keywords:
7
8 ;; This program is free software; you can redistribute it and/or modify
9 ;; it under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation, either version 3 of the License, or
11 ;; (at your option) any later version.
12
13 ;; This program is distributed in the hope that it will be useful,
14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 ;; GNU General Public License for more details.
17
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with this program. If not, see <http://www.gnu.org/licenses/>.
20
21 ;;; Commentary:
22
23 ;;
24
25 ;;; Code:
26
27 (require 'gnorb-utils)
28 (require 'cl-lib)
29
30 (defgroup gnorb-bbdb nil
31 "The BBDB bits of gnorb."
32 :tag "Gnorb BBDB"
33 :group 'gnorb)
34
35 (defcustom gnorb-bbdb-org-tag-field 'org-tags
36 "The name (as a symbol) of the field to use for org tags."
37 :group 'gnorb-bbdb
38 :type 'symbol)
39
40 (when (boundp 'bbdb-separator-alist) ;Allow compilation if BBDB is absent!
41 (unless (assoc gnorb-bbdb-org-tag-field bbdb-separator-alist)
42 (push `(,gnorb-bbdb-org-tag-field ":" ":") bbdb-separator-alist)))
43
44 (defcustom gnorb-bbdb-messages-field 'messages
45 "The name (as a symbol) of the field where links to recent gnus
46 messages from this record are stored.
47
48 \\<bbdb-mode-map>Records that do not have this field defined
49 will not collect links to messages: you have to call
50 \"\\[gnorb-bbdb-open-link]\" on the record once -- after that,
51 message links will be collected and updated automatically."
52 :group 'gnorb-bbdb
53 :type 'symbol)
54
55 (defcustom gnorb-bbdb-collect-N-messages 5
56 "For records with a `gnorb-bbdb-messages-field' defined,
57 collect links to a maximum of this many messages."
58 :group 'gnorb-bbdb
59 :type 'integer)
60
61 (defcustom gnorb-bbdb-define-recent 'seen
62 "For records with a `gnorb-bbdb-message-tag-field' defined,
63 this variable controls how gnorb defines a \"recent\" message.
64 Setting it to the symbol seen will collect the messages most
65 recently opened and viewed. The symbol received means gnorb will
66 collect the most recent messages by Date header.
67
68 In other words, if this variable is set to 'received, and a
69 record's messages field is already full of recently-received
70 messages, opening a five-year-old message (for instance) from
71 this record will not push a link to the message into the field."
72 :group 'gnorb-bbdb
73 :type '(choice (const :tag "Most recently seen" 'seen)
74 (const :tag "Most recently received" 'received)))
75
76 (defcustom gnorb-bbdb-message-link-format-multi "%:count. %D: %:subject"
77 "How a single message is formatted in the list of recent messages.
78 This format string is used in multi-line record display.
79
80 Available information for each message includes the subject, the
81 date, and the message's count in the list, as an integer. You can
82 access subject and count using the %:subject and %:count escapes.
83 The message date can be formatted using any of the escapes
84 mentioned in the docstring of `format-time-string', which see."
85 :group 'gnorb-bbdb
86 :type 'string)
87
88 (defcustom gnorb-bbdb-message-link-format-one "%:count"
89 "How a single message is formatted in the list of recent messages.
90 This format string is used in single-line display -- note that by
91 default, no user-created xfields are displayed in the 'one-line
92 layout found in `bbdb-layout-alist'. If you want this field to
93 appear there, put its name in the \"order\" list of the 'one-line
94 layout.
95
96 Available information for each message includes the subject, the
97 date, and the message's count in the list, as an integer. You can
98 access subject and count using the %:subject and %:count escapes.
99 The message date can be formatted using any of the escapes
100 mentioned in the docstring of `format-time-string', which see."
101 :group 'gnorb-bbdb
102 :type 'string)
103
104 (defface gnorb-bbdb-link (org-compatible-face 'org-link nil)
105 "Custom face for displaying message links in the *BBDB* buffer.
106 Defaults to org-link."
107 :group 'gnorb-bbdb)
108
109 (cl-defstruct gnorb-bbdb-link
110 subject date group id)
111
112 (defcustom gnorb-bbdb-posting-styles nil
113 "An alist of styles to use when composing messages to the BBDB
114 record(s) under point. This is entirely analogous to
115 `gnus-posting-styles', it simply works by examining record fields
116 rather than group names.
117
118 When composing a message to multiple contacts (using the \"*\"
119 prefix), the records will be scanned in order, with the record
120 initially under point (if any) set aside for last. That means
121 that, in the case of conflicting styles, the record under point
122 will override the others.
123
124 In order not to be too intrusive, this option has no effect on
125 the usual `bbdb-mail' command. Instead, the wrapper command
126 `gnorb-bbdb-mail' is provided, which consults this option and
127 then hands off to `bbdb-compose-mail'. If you'd always like to
128 use `gnorb-bbdb-mail', you can simply bind it to \"m\" in the
129 `bbdb-mode-map'.
130
131 The value of the option should be a list of sexps, each one
132 matching a single field. The first element should match a field
133 name: one of the built-in fields like lastname, or an xfield.
134 Field names should be given as symbols.
135
136 The second element is a regexp used to match against the value of
137 the field (non-string field values will be cast to strings, if
138 possible). It can also be a cons of two strings, the first of
139 which matches the field label, the second the field value.
140
141 Alternately, the first element can be the name of a custom
142 function that is called with the record as its only argument, and
143 returns either t or nil. In this case, the second element of the
144 list is disregarded.
145
146 All following elements should be field setters for the message to
147 be composed, just as in `gnus-posting-styles'.
148
149 An example value might look like:"
150 :group 'gnorb-bbdb)
151
152 (defvar message-mode-hook)
153
154 ;;;###autoload
155 (defun gnorb-bbdb-mail (records &optional subject n verbose)
156 "\\<bbdb-mode-map>Acts just like `bbdb-mail', except runs
157 RECORDS through `gnorb-bbdb-posting-styles', allowing
158 customization of message styles for certain records. From the
159 `bbdb-mail' docstring:
160
161 Compose a mail message to RECORDS (optional: using SUBJECT).
162 Interactively, use BBDB prefix \\[bbdb-do-all-records], see
163 `bbdb-do-all-records'. By default, the first mail addresses of
164 RECORDS are used. If prefix N is a number, use Nth mail address
165 of RECORDS (starting from 1). If prefix N is C-u (t
166 noninteractively) use all mail addresses of RECORDS. If VERBOSE
167 is non-nil (as in interactive calls) be verbose."
168 ;; see the function `gnus-configure-posting-styles' for tips on how
169 ;; to actually do this.
170 (interactive (list (bbdb-do-records) nil
171 (or (consp current-prefix-arg)
172 current-prefix-arg)
173 t))
174 (setq records (bbdb-record-list records))
175 (if (not records)
176 (user-error "No records displayed")
177 (let ((head (bbdb-current-record))
178 (to (bbdb-mail-address records n nil verbose))
179 (message-mode-hook (copy-sequence message-mode-hook)))
180 (setq records (remove head records))
181 (when gnorb-bbdb-posting-styles
182 (add-hook 'message-mode-hook
183 `(lambda ()
184 (gnorb-bbdb-configure-posting-styles (quote ,records))
185 (gnorb-bbdb-configure-posting-styles (list ,head)))))
186 (bbdb-compose-mail to subject))))
187
188 (defun gnorb-bbdb-configure-posting-styles (recs)
189 ;; My most magnificent work of copy pasta!
190 (dolist (r recs)
191 (let (field val label rec-val element filep
192 element v value results name address)
193 (dolist (style gnorb-bbdb-posting-styles)
194 (setq field (pop style)
195 val (pop style))
196 (when (consp val) ;; (label value)
197 (setq label (pop val)
198 val (pop val)))
199 (unless (fboundp field)
200 ;; what's the record's existing value for this field?
201 (setq rec-val (bbdb-record-field r field)))
202 (when (cond
203 ((eq field 'address)
204 (dolist (a rec-val)
205 (unless (and label
206 (not (string-match label (car a))))
207 (string-match val (bbdb-format-address-default a)))))
208 ((eq field 'phone)
209 (dolist (p rec-val)
210 (unless (and label
211 (not (string-match label (car p))))
212 (string-match val (bbdb-phone-string p)))))
213 ((consp rec-val)
214 (dolist (f rec-val)
215 (string-match val f)))
216 ((fboundp field)
217 (funcall field r))
218 ((stringp rec-val)
219 (string-match val rec-val)))
220 ;; there are matches, run through the field setters in last
221 ;; element of the sexp
222 (dolist (attribute style)
223 (setq element (pop attribute)
224 filep nil)
225 (setq value
226 (cond
227 ((eq (car attribute) :file)
228 (setq filep t)
229 (cadr attribute))
230 ((eq (car attribute) :value)
231 (cadr attribute))
232 (t
233 (car attribute))))
234 ;; We get the value.
235 (setq v
236 (cond
237 ((stringp value)
238 value)
239 ((or (symbolp value)
240 (functionp value))
241 (cond ((functionp value)
242 (funcall value))
243 ((boundp value)
244 (symbol-value value))))
245 ((listp value)
246 (eval value))))
247 ;; Post-processing for the signature posting-style:
248 (and (eq element 'signature) filep
249 message-signature-directory
250 ;; don't actually use the signature directory
251 ;; if message-signature-file contains a path.
252 (not (file-name-directory v))
253 (setq v (nnheader-concat message-signature-directory v)))
254 ;; Get the contents of file elems.
255 (when (and filep v)
256 (setq v (with-temp-buffer
257 (insert-file-contents v)
258 (buffer-substring
259 (point-min)
260 (progn
261 (goto-char (point-max))
262 (if (zerop (skip-chars-backward "\n"))
263 (point)
264 (1+ (point))))))))
265 (setq results (delq (assoc element results) results))
266 (push (cons element v) results))))
267 (setq name (assq 'name results)
268 address (assq 'address results))
269 (setq results (delq name (delq address results)))
270 (gnus-make-local-hook 'message-setup-hook)
271 (setq results (sort results (lambda (x y)
272 (string-lessp (car x) (car y)))))
273 (dolist (result results)
274 (add-hook 'message-setup-hook
275 (cond
276 ((eq 'eval (car result))
277 'ignore)
278 ((eq 'body (car result))
279 `(lambda ()
280 (save-excursion
281 (message-goto-body)
282 (insert ,(cdr result)))))
283 ((eq 'signature (car result))
284 (set (make-local-variable 'message-signature) nil)
285 (set (make-local-variable 'message-signature-file) nil)
286 (if (not (cdr result))
287 'ignore
288 `(lambda ()
289 (save-excursion
290 (let ((message-signature ,(cdr result)))
291 (when message-signature
292 (message-insert-signature)))))))
293 (t
294 (let ((header
295 (if (symbolp (car result))
296 (capitalize (symbol-name (car result)))
297 (car result))))
298 `(lambda ()
299 (save-excursion
300 (message-remove-header ,header)
301 (let ((value ,(cdr result)))
302 (when value
303 (message-goto-eoh)
304 (insert ,header ": " value)
305 (unless (bolp)
306 (insert "\n")))))))))
307 t 'local))
308 (when (or name address)
309 (add-hook 'message-setup-hook
310 `(lambda ()
311 (set (make-local-variable 'user-mail-address)
312 ,(or (cdr address) user-mail-address))
313 (let ((user-full-name ,(or (cdr name) (user-full-name)))
314 (user-mail-address
315 ,(or (cdr address) user-mail-address)))
316 (save-excursion
317 (message-remove-header "From")
318 (message-goto-eoh)
319 (insert "From: " (message-make-from) "\n"))))
320 t 'local)))))
321
322 ;;;###autoload
323 (defun gnorb-bbdb-tag-agenda (records)
324 "Open an Org agenda tags view from the BBDB buffer, using the
325 value of the record's org-tags field. This shows only TODOs by
326 default; a prefix argument shows all tagged headings; a \"*\"
327 prefix operates on all currently visible records. If you want
328 both, use \"C-u\" before the \"*\"."
329 (interactive (list (bbdb-do-records)))
330 (require 'org-agenda)
331 (unless (and (eq major-mode 'bbdb-mode)
332 (equal (buffer-name) bbdb-buffer-name))
333 (error "Only works in the BBDB buffer"))
334 (setq records (bbdb-record-list records))
335 (let ((tag-string
336 (mapconcat
337 'identity
338 (delete-dups
339 (cl-mapcan (lambda (r)
340 (bbdb-record-xfield-split r gnorb-bbdb-org-tag-field))
341 records))
342 "|")))
343 (if tag-string
344 ;; C-u = all headings, not just todos
345 (org-tags-view (not (equal current-prefix-arg '(4)))
346 tag-string)
347 (error "No org-tags field present"))))
348
349 ;;;###autoload
350 (defun gnorb-bbdb-mail-search (records)
351 "Initiate a mail search from the BBDB buffer.
352
353 Use the prefix arg to edit the search string first, and the \"*\"
354 prefix to search mails from all visible contacts. When using both
355 a prefix arg and \"*\", the prefix arg must come first."
356 (interactive (list (bbdb-do-records)))
357 (unless (and (eq major-mode 'bbdb-mode)
358 (equal (buffer-name) bbdb-buffer-name))
359 (error "Only works in the BBDB buffer"))
360 (setq records (bbdb-record-list records))
361 (require 'gnorb-gnus)
362 (let* ((backend (or (assoc gnorb-gnus-mail-search-backend
363 gnorb-gnus-mail-search-backends)
364 (error "No search backend specified")))
365 (search-string
366 (funcall (cl-second backend)
367 (cl-mapcan 'bbdb-record-mail records))))
368 (when (equal current-prefix-arg '(4))
369 (setq search-string
370 (read-from-minibuffer
371 (format "%s search string: " (cl-first backend)) search-string)))
372 (funcall (cl-third backend) search-string)
373 (delete-other-windows)))
374
375 ;;;###autoload
376 (defun gnorb-bbdb-cite-contact (rec)
377 (interactive (list (gnorb-prompt-for-bbdb-record)))
378 (let ((mail-string (bbdb-dwim-mail rec)))
379 (if (called-interactively-p 'any)
380 (insert mail-string)
381 mail-string)))
382
383 ;;; Field containing links to recent messages
384
385 (when (boundp 'bbdb-xfield-label-list)
386 (add-to-list 'bbdb-xfield-label-list gnorb-bbdb-messages-field nil 'eq))
387
388 (defun gnorb-bbdb-display-messages (record format)
389 "Show links to the messages collected in the
390 `gnorb-bbdb-messages-field' field of a BBDB record. Each link
391 will be formatted using the format string in
392 `gnorb-bbdb-message-link-format-multi' or
393 `gnorb-bbdb-message-link-format-one', depending on the current
394 layout type."
395 (let ((full-field (assq gnorb-bbdb-messages-field
396 (bbdb-record-xfields record)))
397 (val (bbdb-record-xfield record gnorb-bbdb-messages-field))
398 (map (make-sparse-keymap))
399 (count 1)) ; one-indexed to fit with prefix arg to `gnorb-bbdb-open-link'
400 (define-key map [mouse-1] 'gnorb-bbdb-mouse-open-link)
401 (define-key map (kbd "<RET>") 'gnorb-bbdb-RET-open-link)
402 (when val
403 ;; indent and fmt are dynamically bound
404 (when (eq format 'multi)
405 (bbdb-display-text (format fmt gnorb-bbdb-messages-field)
406 `(xfields ,full-field field-label)
407 'bbdb-field-name))
408 (insert (cond ((and (stringp val)
409 (eq format 'multi))
410 (bbdb-indent-string (concat val "\n") indent))
411 ((listp val)
412 (concat
413 (bbdb-indent-string
414 (mapconcat
415 (lambda (m)
416 (prog1
417 (org-propertize
418 (concat
419 (format-time-string
420 (replace-regexp-in-string
421 "%:subject" (gnorb-bbdb-link-subject m)
422 (replace-regexp-in-string
423 "%:count" (number-to-string count)
424 (if (eq format 'multi)
425 gnorb-bbdb-message-link-format-multi
426 gnorb-bbdb-message-link-format-one)))
427 (gnorb-bbdb-link-date m)))
428 'face 'gnorb-bbdb-link
429 'mouse-face 'highlight
430 'gnorb-bbdb-link-count count
431 'keymap map)
432 (cl-incf count)))
433 val (if (eq format 'multi)
434 "\n" ", "))
435 indent)
436 (if (eq format 'multi) "\n" "")))
437 (t
438 ""))))))
439
440 (fset (intern (format "bbdb-display-%s-multi-line"
441 gnorb-bbdb-messages-field))
442 (lambda (record)
443 (gnorb-bbdb-display-messages record 'multi)))
444
445 (fset (intern (format "bbdb-display-%s-one-line"
446 gnorb-bbdb-messages-field))
447 (lambda (record)
448 (gnorb-bbdb-display-messages record 'one)))
449
450 ;; Don't allow direct editing of this field
451
452 (fset (intern (format "bbdb-read-xfield-%s"
453 gnorb-bbdb-messages-field))
454 (lambda (&optional init)
455 (user-error "This field shouldn't be edited manually")))
456
457 ;; Open links from the *BBDB* buffer.
458
459 ;;;###autoload
460 (defun gnorb-bbdb-open-link (record arg)
461 "\\<bbdb-mode-map>Call this on a BBDB record to open one of the
462 links in the message field. By default, the first link will be
463 opened. Use a prefix arg to open different links. For instance,
464 M-3 \\[gnorb-bbdb-open-link] will open the third link in the
465 list. If the %:count escape is present in the message formatting
466 string (see `gnorb-bbdb-message-link-format-multi' and
467 `gnorb-bbdb-message-link-format-one'), that's the number to use.
468
469 This function also needs to be called on a contact once before
470 that contact will start collecting links to messages."
471 (interactive (list
472 (or (bbdb-current-record)
473 (user-error "No record under point"))
474 current-prefix-arg))
475 (unless (fboundp 'bbdb-record-xfield-string)
476 (user-error "This function only works with the git version of BBDB"))
477 (let* ((record (bbdb-current-record))
478 msg-list target-msg)
479 (if (not (memq gnorb-bbdb-messages-field
480 (mapcar 'car (bbdb-record-xfields record))))
481 (when (y-or-n-p
482 (format "Start collecting message links for %s?"
483 (bbdb-record-name record)))
484 (bbdb-record-set-xfield record gnorb-bbdb-messages-field "no links yet")
485 (message "Opening messages from %s will add links to the %s field"
486 (bbdb-record-name record)
487 gnorb-bbdb-messages-field)
488 (bbdb-change-record record))
489 (setq msg-list
490 (bbdb-record-xfield record gnorb-bbdb-messages-field))
491 (setq target-msg
492 (or (and arg
493 (nth (1- arg) msg-list))
494 (car msg-list)))
495 (when target-msg
496 (org-gnus-follow-link (gnorb-bbdb-link-group target-msg)
497 (gnorb-bbdb-link-id target-msg))))))
498
499 (defun gnorb-bbdb-mouse-open-link (event)
500 (interactive "e")
501 (mouse-set-point event)
502 (let ((rec (bbdb-current-record))
503 (num (get-text-property (point) 'gnorb-bbdb-link-count)))
504 (if (not num)
505 (user-error "No link under point")
506 (gnorb-bbdb-open-link rec num))))
507
508 (defun gnorb-bbdb-RET-open-link ()
509 (interactive)
510 (let ((rec (bbdb-current-record))
511 (num (get-text-property (point) 'gnorb-bbdb-link-count)))
512 (if (not num)
513 (user-error "No link under point")
514 (gnorb-bbdb-open-link rec num))))
515
516 (defun gnorb-bbdb-store-message-link (record)
517 "Used in the `bbdb-notice-record-hook' to possibly save a link
518 to a message into the record's `gnorb-bbdb-messages-field'."
519
520 (when (not (fboundp 'bbdb-record-xfield-string))
521 (user-error "This function only works with the git version of BBDB"))
522 (unless (or (not (and (memq gnorb-bbdb-messages-field
523 (mapcar 'car (bbdb-record-xfields record)))
524 (memq major-mode '(gnus-summary-mode gnus-article-mode))))
525 (with-current-buffer gnus-article-buffer
526 (not ; only store messages if the record is the sender
527 (member (nth 1 (car (bbdb-get-address-components 'sender)))
528 (bbdb-record-mail record)))))
529 (with-current-buffer gnus-summary-buffer
530 (let* ((val (bbdb-record-xfield record gnorb-bbdb-messages-field))
531 (art-no (gnus-summary-article-number))
532 (heads (gnus-summary-article-header art-no))
533 (date (apply 'encode-time
534 (parse-time-string (mail-header-date heads))))
535 (subject (mail-header-subject heads))
536 (id (mail-header-id heads))
537 (group gnus-newsgroup-name)
538 link)
539 ;; check for both nnvirtual and nnir, and link to the real
540 ;; group in those cases
541 (when (eq (car (gnus-find-method-for-group group))
542 'nnvirtual)
543 (setq group (car (nnvirtual-map-article art-no))))
544 (when (eq (car (gnus-find-method-for-group group))
545 'nnir)
546 (setq group (nnir-article-group art-no)))
547 (if (not (and date subject id group))
548 (message "Could not save a link to this message")
549 (setq link (make-gnorb-bbdb-link :subject subject :date date
550 :group group :id id))
551 (when (stringp val)
552 (setq val nil))
553 (setq val (cons link (delete link val)))
554 (when (eq gnorb-bbdb-define-recent 'received)
555 (setq val (sort val
556 (lambda (a b)
557 (time-less-p
558 (gnorb-bbdb-link-date b)
559 (gnorb-bbdb-link-date a))))))
560 (setq val (cl-subseq val 0 gnorb-bbdb-collect-N-messages))
561 (bbdb-record-set-xfield record
562 gnorb-bbdb-messages-field
563 (delq nil val))
564 (bbdb-change-record record))))))
565
566 (add-hook 'bbdb-notice-record-hook 'gnorb-bbdb-store-message-link)
567
568 (provide 'gnorb-bbdb)
569 ;;; gnorb-bbdb.el ends here