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