Improve `completion-at-point' for `org-contacts.el' in mail
[org-mode/org-tableheadings.git] / contrib / lisp / org-contacts.el
blobf23d938bece7f4aaaddb7876dedf35b4306c745b
1 ;;; org-contacts.el --- Contacts management
3 ;; Copyright (C) 2010-2013 Julien Danjou <julien@danjou.info>
5 ;; Author: Julien Danjou <julien@danjou.info>
6 ;; Keywords: outlines, hypermedia, calendar
7 ;;
8 ;; This file is NOT part of GNU Emacs.
9 ;;
10 ;; GNU Emacs is free software: you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
18 ;; GNU General Public License for more details.
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
22 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
24 ;;; Commentary:
26 ;; This file contains the code for managing your contacts into Org-mode.
28 ;; To enter new contacts, you can use `org-capture' and a template just like
29 ;; this:
31 ;; ("c" "Contacts" entry (file "~/Org/contacts.org")
32 ;; "* %(org-contacts-template-name)
33 ;; :PROPERTIES:
34 ;; :EMAIL: %(org-contacts-template-email)
35 ;; :END:")))
37 ;;; Code:
39 (eval-when-compile
40 (require 'cl))
42 (eval-and-compile
43 (require 'org))
44 (require 'gnus-util)
45 (require 'org-agenda)
47 (defgroup org-contacts nil
48 "Options about contacts management."
49 :group 'org)
51 (defcustom org-contacts-files nil
52 "List of Org files to use as contacts source.
53 When set to nil, all your Org files will be used."
54 :type '(repeat file)
55 :group 'org-contacts)
57 (defcustom org-contacts-email-property "EMAIL"
58 "Name of the property for contact email address."
59 :type 'string
60 :group 'org-contacts)
62 (defcustom org-contacts-address-property "ADDRESS"
63 "Name of the property for contact address."
64 :type 'string
65 :group 'org-contacts)
67 (defcustom org-contacts-birthday-property "BIRTHDAY"
68 "Name of the property for contact birthday date."
69 :type 'string
70 :group 'org-contacts)
72 (defcustom org-contacts-birthday-format "Birthday: %l (%Y)"
73 "Format of the anniversary agenda entry.
74 The following replacements are available:
76 %h - Heading name
77 %l - Link to the heading
78 %y - Number of year
79 %Y - Number of year (ordinal)"
80 :type 'string
81 :group 'org-contacts)
83 (defcustom org-contacts-last-read-mail-property "LAST_READ_MAIL"
84 "Name of the property for contact last read email link storage."
85 :type 'string
86 :group 'org-contacts)
88 (defcustom org-contacts-icon-property "ICON"
89 "Name of the property for contact icon."
90 :type 'string
91 :group 'org-contacts)
93 (defcustom org-contacts-nickname-property "NICKNAME"
94 "Name of the property for IRC nickname match."
95 :type 'string
96 :group 'org-contacts)
98 (defcustom org-contacts-icon-size 32
99 "Size of the contacts icons."
100 :type 'string
101 :group 'org-contacts)
103 (defcustom org-contacts-icon-use-gravatar (fboundp 'gravatar-retrieve)
104 "Whether use Gravatar to fetch contact icons."
105 :type 'boolean
106 :group 'org-contacts)
108 (defcustom org-contacts-completion-ignore-case t
109 "Ignore case when completing contacts."
110 :type 'boolean
111 :group 'org-contacts)
113 (defcustom org-contacts-group-prefix "+"
114 "Group prefix."
115 :type 'string
116 :group 'org-contacts)
118 (defcustom org-contacts-matcher (concat org-contacts-email-property "<>\"\"")
119 "Matching rule for finding heading that are contacts.
120 This can be a tag name, or a property check."
121 :type 'string
122 :group 'org-contacts)
124 (defcustom org-contacts-email-link-description-format "%s (%d)"
125 "Format used to store links to email.
126 This overrides `org-email-link-description-format' if set."
127 :group 'org-contacts
128 :type 'string)
130 (defcustom org-contacts-vcard-file "contacts.vcf"
131 "Default file for vcard export."
132 :group 'org-contacts
133 :type 'file)
135 (defvar org-contacts-keymap
136 (let ((map (make-sparse-keymap)))
137 (define-key map "M" 'org-contacts-view-send-email)
138 (define-key map "i" 'org-contacts-view-switch-to-irc-buffer)
139 map)
140 "The keymap used in `org-contacts' result list.")
142 (defun org-contacts-files ()
143 "Return list of Org files to use for contact management."
144 (or org-contacts-files (org-agenda-files t 'ifmode)))
146 (defun org-contacts-filter (&optional name-match tags-match)
147 "Search for a contact maching NAME-MATCH and TAGS-MATCH.
148 If both match values are nil, return all contacts."
149 (let* (todo-only
150 (tags-matcher
151 (if tags-match
152 (cdr (org-make-tags-matcher tags-match))
154 (name-matcher
155 (if name-match
156 '(org-string-match-p name-match (org-get-heading t))
158 (contacts-matcher
159 (cdr (org-make-tags-matcher org-contacts-matcher)))
160 markers result)
161 (dolist (file (org-contacts-files))
162 (org-check-agenda-file file)
163 (with-current-buffer (org-get-agenda-file-buffer file)
164 (unless (eq major-mode 'org-mode)
165 (error "File %s is no in `org-mode'" file))
166 (org-scan-tags
167 '(add-to-list 'markers (set-marker (make-marker) (point)))
168 `(and ,contacts-matcher ,tags-matcher ,name-matcher)
169 todo-only)))
170 (dolist (marker markers result)
171 (org-with-point-at marker
172 (add-to-list 'result
173 (list (org-get-heading t) marker (org-entry-properties marker 'all)))))))
175 (when (not (fboundp 'completion-table-case-fold))
176 ;; That function is new in Emacs 24...
177 (defun completion-table-case-fold (table &optional dont-fold)
178 (lambda (string pred action)
179 (let ((completion-ignore-case (not dont-fold)))
180 (complete-with-action action table string pred)))))
182 (defun org-contacts-try-completion-prefix (to-match collection &optional predicate)
183 "Like `try-completion' but:
184 - works only with list and alist;
185 - looks at all prefixes rather than just the beginning of the string;"
186 (loop with regexp = (concat "\\b" (regexp-quote to-match))
187 with ret = nil
188 with ret-start = nil
189 with ret-end = nil
191 for el in collection
192 for string = (if (listp el) (car el) el)
194 for start = (when (or (null predicate) (funcall predicate string))
195 (string-match regexp string))
197 if start
198 do (let ((end (match-end 0))
199 (len (length string)))
200 (if (= end len)
201 (return t)
202 (destructuring-bind (string start end)
203 (if (null ret)
204 (values string start end)
205 (org-contacts-common-substring
206 ret ret-start ret-end
207 string start end))
208 (setf ret string
209 ret-start start
210 ret-end end))))
212 finally (return
213 (replace-regexp-in-string "\\`[ \t\n]*" "" ret))))
215 (defun org-contacts-compare-strings (s1 start1 end1 s2 start2 end2 &optional ignore-case)
216 "Compare the contents of two strings, using `compare-strings'.
218 This function works like `compare-strings' excepted that it
219 returns a cons.
220 - The CAR is the number of characters that match at the beginning.
221 - The CDR is T is the two strings are the same and NIL otherwise."
222 (let ((ret (compare-strings s1 start1 end1 s2 start2 end2 ignore-case)))
223 (if (eq ret t)
224 (cons (or end1 (length s1)) t)
225 (cons (1- (abs ret)) nil))))
227 (defun org-contacts-common-substring (s1 start1 end1 s2 start2 end2)
228 "Extract the common substring between S1 and S2.
230 This function extracts the common substring between S1 and S2 and
231 adjust the part that remains common.
233 START1 and END1 delimit the part in S1 that we know is common
234 between the two strings. This applies to START2 and END2 for S2.
236 This function returns a list whose contains:
237 - The common substring found.
238 - The new value of the start of the known inner substring.
239 - The new value of the end of the known inner substring."
240 ;; Given two strings:
241 ;; s1: "foo bar baz"
242 ;; s2: "fooo bar baz"
243 ;; and the inner substring is "bar"
244 ;; then: start1 = 4, end1 = 6, start2 = 5, end2 = 7
246 ;; To find the common substring we will compare two substrings:
247 ;; " oof" and " ooof" to find the beginning of the common substring.
248 ;; " baz" and " baz" to find the end of the common substring.
249 (let* ((len1 (length s1))
250 (start1 (or start1 0))
251 (end1 (or end1 len1))
253 (len2 (length s2))
254 (start2 (or start2 0))
255 (end2 (or end2 len2))
257 (new-start (car (org-contacts-compare-strings
258 (substring (org-reverse-string s1) (- len1 start1)) nil nil
259 (substring (org-reverse-string s2) (- len2 start2)) nil nil)))
261 (new-end (+ end1 (car (org-contacts-compare-strings
262 (substring s1 end1) nil nil
263 (substring s2 end2) nil nil)))))
264 (list (substring s1 (- start1 new-start) new-end)
265 new-start
266 (+ new-start (- end1 start1)))))
268 (defun org-contacts-all-completions-prefix (to-match collection &optional predicate)
269 "Like `all-completions' but:
270 - works only with list and alist;
271 - looks at all prefixes rather than just the beginning of the string;"
272 (loop with regexp = (concat "\\b" (regexp-quote to-match))
273 for el in collection
274 for string = (if (listp el) (car el) el)
275 for match? = (when (and (or (null predicate) (funcall predicate string)))
276 (string-match regexp string))
277 if match?
278 collect (progn
279 (let ((end (match-end 0)))
280 (org-no-properties string)
281 (when (< end (length string))
282 ;; Here we add a text property that will be used
283 ;; later to highlight the character right after
284 ;; the common part between each addresses.
285 ;; See `org-contacts-display-sort-function'.
286 (put-text-property end (1+ end) 'org-contacts-prefix 't string)))
287 string)))
289 (defun org-contacts-make-collection-prefix (collection)
290 "Makes a collection function from COLLECTION which will match
291 on prefixes."
292 (lexical-let ((collection collection))
293 (lambda (string predicate flag)
294 (cond ((eq flag nil)
295 (org-contacts-try-completion-prefix string collection predicate))
296 ((eq flag t)
297 ;; `org-contacts-all-completions-prefix' has already been
298 ;; used to compute `all-completions'.
299 collection)
300 ((eq flag 'lambda)
301 (org-contacts-test-completion-prefix string collection predicate))
302 ((and (listp flag) (eq (car flag) 'boundaries))
303 (destructuring-bind (to-ignore &rest suffix)
304 flag
305 (org-contacts-boundaries-prefix string collection predicate suffix)))
306 ((eq flag 'metadata)
307 (org-contacts-metadata-prefix string collection predicate))
308 (t nil ; operation unsupported
309 )))))
311 (defun org-contacts-display-sort-function (completions)
312 (mapcar (lambda (string)
313 (loop with len = (1- (length string))
314 for i upfrom 0 to len
315 if (memq 'org-contacts-prefix
316 (text-properties-at i string))
317 do (set-text-properties
318 i (1+ i)
319 (list 'font-lock-face
320 (if (char-equal (aref string i)
321 (string-to-char " "))
322 ;; Spaces can't be bold.
323 'underline
324 'bold)) string)
325 else
326 do (set-text-properties i (1+ i) nil string)
327 finally (return string)))
328 completions))
330 (defun org-contacts-test-completion-prefix (string collection predicate)
331 (find-if (lambda (el)
332 (and (or (null predicate) (funcall predicate el))
333 (string= string el)))
334 collection))
336 (defun org-contacts-boundaries-prefix (string collection predicate suffix)
337 (list* 'boundaries (completion-boundaries string collection predicate suffix)))
339 (defun org-contacts-metadata-prefix (string collection predicate)
340 '(metadata .
341 ((display-sort-function . org-contacts-display-sort-function))))
343 (defun org-contacts-complete-group (start end string)
344 "Complete text at START from a group.
346 A group FOO is composed of contacts with the tag FOO."
347 (let* ((completion-ignore-case org-contacts-completion-ignore-case)
348 (group-completion-p (org-string-match-p
349 (concat "^" org-contacts-group-prefix) string)))
350 (when group-completion-p
351 (let ((completion-list
352 (all-completions
353 string
354 (mapcar (lambda (group)
355 (propertize (concat org-contacts-group-prefix group)
356 'org-contacts-group group))
357 (org-uniquify
358 (loop for contact in (org-contacts-filter)
359 nconc (org-split-string
360 (or (cdr (assoc-string "ALLTAGS" (caddr contact))) "") ":")))))))
361 (list start end
362 (if (= (length completion-list) 1)
363 ;; We've foudn the correct group, returns the address
364 (lexical-let ((tag (get-text-property 0 'org-contacts-group
365 (car completion-list))))
366 (lambda (string pred &optional to-ignore)
367 (mapconcat 'identity
368 (loop for contact in (org-contacts-filter
370 tag)
371 ;; The contact name is always the car of the assoc-list
372 ;; returned by `org-contacts-filter'.
373 for contact-name = (car contact)
374 ;; Grab the first email of the contact
375 for email = (car (split-string
377 (cdr (assoc-string org-contacts-email-property
378 (caddr contact)))
379 "")))
380 ;; If the user has an email address, append USER <EMAIL>.
381 if email collect (org-contacts-format-email contact-name email))
382 ", ")))
383 ;; We haven't found the correct group
384 (completion-table-case-fold completion-list
385 (not org-contacts-completion-ignore-case))))))))
387 (defun org-contacts-complete-name (start end string)
388 "Complete text at START with a user name and email."
389 (let* ((completion-ignore-case org-contacts-completion-ignore-case)
390 (completion-list
391 (loop for contact in (org-contacts-filter)
392 ;; The contact name is always the car of the assoc-list
393 ;; returned by `org-contacts-filter'.
394 for contact-name = (car contact)
395 ;; Build the list of the user email addresses.
396 for email-list = (split-string (or
397 (cdr (assoc-string org-contacts-email-property
398 (caddr contact))) ""))
399 ;; If the user has email addresses…
400 if email-list
401 ;; … append a list of USER <EMAIL>.
402 nconc (loop for email in email-list
403 collect (org-contacts-format-email contact-name email)))))
404 (when completion-list
405 (list start end
406 (org-contacts-make-collection-prefix
407 (org-contacts-all-completions-prefix
408 string
409 (remove-duplicates completion-list :test #'equalp)))))))
411 (defun org-contacts-message-complete-function (&optional start)
412 "Function used in `completion-at-point-functions' in `message-mode'."
413 (let ((mail-abbrev-mode-regexp
414 "^\\(Resent-To\\|To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\|Disposition-Notification-To\\|Return-Receipt-To\\):"))
415 (when (mail-abbrev-in-expansion-header-p)
416 (lexical-let*
417 ((end (point))
418 (start (or start
419 (save-excursion
420 (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
421 (goto-char (match-end 0))
422 (point))))
423 (string (buffer-substring start end)))
424 (or (org-contacts-complete-group start end string)
425 (org-contacts-complete-name start end string))))))
427 (defun org-contacts-gnus-get-name-email ()
428 "Get name and email address from Gnus message."
429 (if (gnus-alive-p)
430 (gnus-with-article-headers
431 (mail-extract-address-components
432 (or (mail-fetch-field "From") "")))))
434 (defun org-contacts-gnus-article-from-get-marker ()
435 "Return a marker for a contact based on From."
436 (let* ((address (org-contacts-gnus-get-name-email))
437 (name (car address))
438 (email (cadr address)))
439 (cadar (or (org-contacts-filter
441 (concat org-contacts-email-property "={\\b" (regexp-quote email) "\\b}"))
442 (when name
443 (org-contacts-filter
444 (concat "^" name "$")))))))
446 (defun org-contacts-gnus-article-from-goto ()
447 "Go to contact in the From address of current Gnus message."
448 (interactive)
449 (let ((marker (org-contacts-gnus-article-from-get-marker)))
450 (when marker
451 (switch-to-buffer-other-window (marker-buffer marker))
452 (goto-char marker)
453 (when (eq major-mode 'org-mode)
454 (org-show-context 'agenda)
455 (save-excursion
456 (and (outline-next-heading)
457 ;; show the next heading
458 (org-flag-heading nil)))))))
460 (defun org-contacts-anniversaries (&optional field format)
461 "Compute FIELD anniversary for each contact, returning FORMAT.
462 Default FIELD value is \"BIRTHDAY\".
464 Format is a string matching the following format specification:
466 %h - Heading name
467 %l - Link to the heading
468 %y - Number of year
469 %Y - Number of year (ordinal)"
470 (let ((calendar-date-style 'american)
471 (entry ""))
472 (unless format (setq format org-contacts-birthday-format))
473 (loop for contact in (org-contacts-filter)
474 for anniv = (let ((anniv (cdr (assoc-string
475 (or field org-contacts-birthday-property)
476 (caddr contact)))))
477 (when anniv
478 (calendar-gregorian-from-absolute
479 (org-time-string-to-absolute anniv))))
480 ;; Use `diary-anniversary' to compute anniversary.
481 if (and anniv (apply 'diary-anniversary anniv))
482 collect (format-spec format
483 `((?l . ,(org-with-point-at (cadr contact) (org-store-link nil)))
484 (?h . ,(car contact))
485 (?y . ,(- (calendar-extract-year date)
486 (calendar-extract-year anniv)))
487 (?Y . ,(let ((years (- (calendar-extract-year date)
488 (calendar-extract-year anniv))))
489 (format "%d%s" years (diary-ordinal-suffix years)))))))))
491 (defun org-completing-read-date (prompt collection
492 &optional predicate require-match initial-input
493 hist def inherit-input-method)
494 "Like `completing-read' but reads a date.
495 Only PROMPT and DEF are really used."
496 (org-read-date nil nil nil prompt nil def))
498 (add-to-list 'org-property-set-functions-alist
499 `(,org-contacts-birthday-property . org-completing-read-date))
501 (defun org-contacts-template-name (&optional return-value)
502 "Try to return the contact name for a template.
503 If not found return RETURN-VALUE or something that would ask the user."
504 (or (car (org-contacts-gnus-get-name-email))
505 return-value
506 "%^{Name}"))
508 (defun org-contacts-template-email (&optional return-value)
509 "Try to return the contact email for a template.
510 If not found return RETURN-VALUE or something that would ask the user."
511 (or (cadr (org-contacts-gnus-get-name-email))
512 return-value
513 (concat "%^{" org-contacts-email-property "}p")))
515 (defun org-contacts-gnus-store-last-mail ()
516 "Store a link between mails and contacts.
518 This function should be called from `gnus-article-prepare-hook'."
519 (let ((marker (org-contacts-gnus-article-from-get-marker)))
520 (when marker
521 (with-current-buffer (marker-buffer marker)
522 (save-excursion
523 (goto-char marker)
524 (let* ((org-email-link-description-format (or org-contacts-email-link-description-format
525 org-email-link-description-format))
526 (link (gnus-with-article-buffer (org-store-link nil))))
527 (org-set-property org-contacts-last-read-mail-property link)))))))
529 (defun org-contacts-icon-as-string ()
530 (let ((image (org-contacts-get-icon)))
531 (concat
532 (propertize "-" 'display
533 (append
534 (if image
535 image
536 `'(space :width (,org-contacts-icon-size)))
537 '(:ascent center)))
538 " ")))
540 ;;;###autoload
541 (defun org-contacts (name)
542 "Create agenda view for contacts matching NAME."
543 (interactive (list (read-string "Name: ")))
544 (let ((org-agenda-files (org-contacts-files))
545 (org-agenda-skip-function
546 (lambda () (org-agenda-skip-if nil `(notregexp ,name))))
547 (org-agenda-format (propertize
548 "%(org-contacts-icon-as-string)% p% s%(org-contacts-irc-number-of-unread-messages)%+T"
549 'keymap org-contacts-keymap))
550 (org-agenda-overriding-header
551 (or org-agenda-overriding-header
552 (concat "List of contacts matching `" name "':"))))
553 (setq org-agenda-skip-regexp name)
554 (org-tags-view nil org-contacts-matcher)
555 (with-current-buffer org-agenda-buffer-name
556 (setq org-agenda-redo-command
557 (list 'org-contacts name)))))
559 (defun org-contacts-completing-read (prompt
560 &optional predicate
561 initial-input hist def inherit-input-method)
562 "Call `completing-read' with contacts name as collection."
563 (org-completing-read
564 prompt (org-contacts-filter) predicate t initial-input hist def inherit-input-method))
566 (defun org-contacts-format-name (name)
567 "Trim any local formatting to get a bare name."
568 ;; Remove radio targets characters
569 (replace-regexp-in-string org-radio-target-regexp "\\1" name))
571 (defun org-contacts-format-email (name email)
572 "Format a mail address."
573 (unless email
574 (error "`email' cannot be nul"))
575 (if name
576 (concat (org-contacts-format-name name) " <" email ">")
577 email))
579 (defun org-contacts-check-mail-address (mail)
580 "Add MAIL address to contact at point if it does not have it."
581 (let ((mails (org-entry-get (point) org-contacts-email-property)))
582 (unless (member mail (split-string mails))
583 (when (yes-or-no-p
584 (format "Do you want to add this address to %s?" (org-get-heading t)))
585 (org-set-property org-contacts-email-property (concat mails " " mail))))))
587 (defun org-contacts-gnus-check-mail-address ()
588 "Check that contact has the current address recorded.
589 This function should be called from `gnus-article-prepare-hook'."
590 (let ((marker (org-contacts-gnus-article-from-get-marker)))
591 (when marker
592 (org-with-point-at marker
593 (org-contacts-check-mail-address (cadr (org-contacts-gnus-get-name-email)))))))
595 (defun org-contacts-gnus-insinuate ()
596 "Add some hooks for Gnus user.
597 This adds `org-contacts-gnus-check-mail-address' and
598 `org-contacts-gnus-store-last-mail' to
599 `gnus-article-prepare-hook'. It also adds a binding on `;' in
600 `gnus-summary-mode-map' to `org-contacts-gnus-article-from-goto'"
601 (require 'gnus)
602 (require 'gnus-art)
603 (define-key gnus-summary-mode-map ";" 'org-contacts-gnus-article-from-goto)
604 (add-hook 'gnus-article-prepare-hook 'org-contacts-gnus-check-mail-address)
605 (add-hook 'gnus-article-prepare-hook 'org-contacts-gnus-store-last-mail))
607 (when (boundp 'completion-at-point-functions)
608 (add-hook 'message-mode-hook
609 (lambda ()
610 (add-to-list 'completion-at-point-functions
611 'org-contacts-message-complete-function))))
613 (defun org-contacts-wl-get-from-header-content ()
614 "Retrieve the content of the `From' header of an email.
615 Works from wl-summary-mode and mime-view-mode - that is while viewing email.
616 Depends on Wanderlust been loaded."
617 (with-current-buffer (org-capture-get :original-buffer)
618 (cond
619 ((eq major-mode 'wl-summary-mode) (when wl-summary-buffer-elmo-folder
620 (elmo-message-field
621 wl-summary-buffer-elmo-folder
622 (wl-summary-message-number)
623 'from)))
624 ((eq major-mode 'mime-view-mode) (std11-narrow-to-header)
625 (prog1
626 (std11-fetch-field "From")
627 (widen))))))
629 (defun org-contacts-wl-get-name-email ()
630 "Get name and email address from Wanderlust email.
631 See `org-contacts-wl-get-from-header-content' for limitations."
632 (let ((from (org-contacts-wl-get-from-header-content)))
633 (when from
634 (list (wl-address-header-extract-realname from)
635 (wl-address-header-extract-address from)))))
637 (defun org-contacts-template-wl-name (&optional return-value)
638 "Try to return the contact name for a template from wl.
639 If not found, return RETURN-VALUE or something that would ask the
640 user."
641 (or (car (org-contacts-wl-get-name-email))
642 return-value
643 "%^{Name}"))
645 (defun org-contacts-template-wl-email (&optional return-value)
646 "Try to return the contact email for a template from Wanderlust.
647 If not found return RETURN-VALUE or something that would ask the user."
648 (or (cadr (org-contacts-wl-get-name-email))
649 return-value
650 (concat "%^{" org-contacts-email-property "}p")))
652 (defun org-contacts-view-send-email (&optional ask)
653 "Send email to the contact at point.
654 If ASK is set, ask for the email address even if there's only one
655 address."
656 (interactive "P")
657 (let ((marker (org-get-at-bol 'org-hd-marker)))
658 (org-with-point-at marker
659 (let ((emails (org-entry-get (point) org-contacts-email-property)))
660 (if emails
661 (let ((email-list (split-string emails)))
662 (if (and (= (length email-list) 1) (not ask))
663 (compose-mail (org-contacts-format-email
664 (org-get-heading t) emails))
665 (let ((email (completing-read "Send mail to which address: " email-list)))
666 (org-contacts-check-mail-address email)
667 (compose-mail (org-contacts-format-email (org-get-heading t) email)))))
668 (error (format "This contact has no mail address set (no %s property)."
669 org-contacts-email-property)))))))
671 (defun org-contacts-get-icon (&optional pom)
672 "Get icon for contact at POM."
673 (setq pom (or pom (point)))
674 (catch 'icon
675 ;; Use `org-contacts-icon-property'
676 (let ((image-data (org-entry-get pom org-contacts-icon-property)))
677 (when image-data
678 (throw 'icon
679 (if (fboundp 'gnus-rescale-image)
680 (gnus-rescale-image (create-image image-data)
681 (cons org-contacts-icon-size org-contacts-icon-size))
682 (create-image image-data)))))
683 ;; Next, try Gravatar
684 (when org-contacts-icon-use-gravatar
685 (let* ((gravatar-size org-contacts-icon-size)
686 (email-list (org-entry-get pom org-contacts-email-property))
687 (gravatar
688 (when email-list
689 (loop for email in (split-string email-list)
690 for gravatar = (gravatar-retrieve-synchronously email)
691 if (and gravatar
692 (not (eq gravatar 'error)))
693 return gravatar))))
694 (when gravatar (throw 'icon gravatar))))))
696 (defun org-contacts-irc-buffer (&optional pom)
697 "Get the IRC buffer associated with the entry at POM."
698 (setq pom (or pom (point)))
699 (let ((nick (org-entry-get pom org-contacts-nickname-property)))
700 (when nick
701 (let ((buffer (get-buffer nick)))
702 (when buffer
703 (with-current-buffer buffer
704 (when (eq major-mode 'erc-mode)
705 buffer)))))))
707 (defun org-contacts-irc-number-of-unread-messages (&optional pom)
708 "Return the number of unread messages for contact at POM."
709 (when (boundp 'erc-modified-channels-alist)
710 (let ((number (cadr (assoc (org-contacts-irc-buffer pom) erc-modified-channels-alist))))
711 (if number
712 (format (concat "%3d unread message" (if (> number 1) "s" " ") " ") number)
713 (make-string 21 ? )))))
715 (defun org-contacts-view-switch-to-irc-buffer ()
716 "Switch to the IRC buffer of the current contact if it has one."
717 (interactive)
718 (let ((marker (org-get-at-bol 'org-hd-marker)))
719 (org-with-point-at marker
720 (switch-to-buffer-other-window (org-contacts-irc-buffer)))))
722 (defun org-contacts-completing-read-nickname (prompt collection
723 &optional predicate require-match initial-input
724 hist def inherit-input-method)
725 "Like `completing-read' but reads a nickname."
726 (org-completing-read prompt (append collection (erc-nicknames-list)) predicate require-match
727 initial-input hist def inherit-input-method))
729 (defun erc-nicknames-list ()
730 "Return all nicknames of all ERC buffers."
731 (if (fboundp 'erc-buffer-list)
732 (loop for buffer in (erc-buffer-list)
733 nconc (with-current-buffer buffer
734 (loop for user-entry in (mapcar 'car (erc-get-channel-user-list))
735 collect (elt user-entry 1))))))
737 (add-to-list 'org-property-set-functions-alist
738 `(,org-contacts-nickname-property . org-contacts-completing-read-nickname))
740 (defun org-contacts-vcard-escape (str)
741 "Escape ; , and \n in STR for the VCard format."
742 ;; Thanks to this library for the regexp:
743 ;; http://www.emacswiki.org/cgi-bin/wiki/bbdb-vcard-export.el
744 (when str
745 (replace-regexp-in-string
746 "\n" "\\\\n"
747 (replace-regexp-in-string "\\(;\\|,\\|\\\\\\)" "\\\\\\1" str))))
749 (defun org-contacts-vcard-encode-name (name)
750 "Try to encode NAME as VCard's N property.
751 The N property expects
753 FamilyName;GivenName;AdditionalNames;Prefix;Postfix.
755 Org-contacts does not specify how to encode the name. So we try
756 to do our best."
757 (concat (replace-regexp-in-string "\\(\\w+\\) \\(.*\\)" "\\2;\\1" name) ";;;"))
759 (defun org-contacts-vcard-format (contact)
760 "Formats CONTACT in VCard 3.0 format."
761 (let* ((properties (caddr contact))
762 (name (org-contacts-vcard-escape (car contact)))
763 (n (org-contacts-vcard-encode-name name))
764 (email (org-contacts-vcard-escape (cdr (assoc-string org-contacts-email-property properties))))
765 (bday (org-contacts-vcard-escape (cdr (assoc-string org-contacts-birthday-property properties))))
766 (addr (cdr (assoc-string org-contacts-address-property properties)))
767 (nick (org-contacts-vcard-escape (cdr (assoc-string org-contacts-nickname-property properties))))
768 (head (format "BEGIN:VCARD\nVERSION:3.0\nN:%s\nFN:%s\n" n name)))
769 (concat head
770 (when email (format "EMAIL:%s\n" email))
771 (when addr
772 (format "ADR:;;%s\n" (replace-regexp-in-string "\\, ?" ";" addr)))
773 (when bday
774 (let ((cal-bday (calendar-gregorian-from-absolute (org-time-string-to-absolute bday))))
775 (format "BDAY:%04d-%02d-%02d\n"
776 (calendar-extract-year cal-bday)
777 (calendar-extract-month cal-bday)
778 (calendar-extract-day cal-bday))))
779 (when nick (format "NICKNAME:%s\n" nick))
780 "END:VCARD\n\n")))
782 (defun org-contacts-export-as-vcard (&optional name file to-buffer)
783 "Export all contacts matching NAME as VCard 3.0.
784 If TO-BUFFER is nil, the content is written to FILE or
785 `org-contacts-vcard-file'. If TO-BUFFER is non-nil, the buffer
786 is created and the VCard is written into that buffer."
787 (interactive) ; TODO ask for name?
788 (let* ((filename (or file org-contacts-vcard-file))
789 (buffer (if to-buffer
790 (get-buffer-create to-buffer)
791 (find-file-noselect filename))))
793 (message "Exporting...")
795 (set-buffer buffer)
796 (let ((inhibit-read-only t)) (erase-buffer))
797 (fundamental-mode)
798 (org-install-letbind)
800 (when (fboundp 'set-buffer-file-coding-system)
801 (set-buffer-file-coding-system coding-system-for-write))
803 (loop for contact in (org-contacts-filter name)
804 do (insert (org-contacts-vcard-format contact)))
806 (if to-buffer
807 (current-buffer)
808 (progn (save-buffer) (kill-buffer)))))
810 (defun org-contacts-show-map (&optional name)
811 "Show contacts on a map.
812 Requires google-maps-el."
813 (interactive)
814 (unless (fboundp 'google-maps-static-show)
815 (error "`org-contacts-show-map' requires `google-maps-el'"))
816 (google-maps-static-show
817 :markers
818 (loop
819 for contact in (org-contacts-filter name)
820 for addr = (cdr (assoc-string org-contacts-address-property (caddr contact)))
821 if addr
822 collect (cons (list addr) (list :label (string-to-char (car contact)))))))
824 (provide 'org-contacts)