Convert function `org-contacts--all-contacts` into variable `org-contacts-all-contacts`.
[org-contacts.git] / org-contacts.el
blobb7ac0bfa1e9ff0510ae0ccade893b8bad8472ab5
1 ;;; org-contacts.el --- Contacts management system for Org mode -*- lexical-binding: t; -*-
3 ;; Copyright (C) 2010-2022 Free Software Foundation, Inc.
5 ;; Author: Julien Danjou <julien@danjou.info>
6 ;; Maintainer: stardiviner <numbchild@gmail.com>
7 ;; Keywords: contacts, org-mode, outlines, hypermedia, calendar
8 ;; Version: 1.1
9 ;; Package-Requires: ((emacs "27.1") (org "9.3.4"))
10 ;; Homepage: https://repo.or.cz/org-contacts.git
12 ;; This file is not part of GNU Emacs.
14 ;; This program is free software: you can redistribute it and/or modify
15 ;; it under the terms of the GNU General Public License as published by
16 ;; the Free Software Foundation, either version 3 of the License, or
17 ;; (at your option) any later version.
19 ;; This program is distributed in the hope that it will be useful,
20 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
21 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
22 ;; GNU General Public License for more details.
24 ;; You should have received a copy of the GNU General Public License
25 ;; along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
26 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
28 ;;; Commentary:
30 ;; This file contains the code for managing your contacts into Org-mode.
32 ;; To enter new contacts, you can use `org-capture' and a minimal template just like
33 ;; this:
35 ;; ("c" "Contacts" entry (file "~/Org/contacts.org")
36 ;; "* %(org-contacts-template-name)
37 ;; :PROPERTIES:
38 ;; :EMAIL: %(org-contacts-template-email)
39 ;; :END:")))
41 ;; You can also use a complex template, for example:
43 ;; ("c" "Contacts" entry (file "~/Org/contacts.org")
44 ;; "* %(org-contacts-template-name)
45 ;; :PROPERTIES:
46 ;; :EMAIL: %(org-contacts-template-email)
47 ;; :PHONE:
48 ;; :ALIAS:
49 ;; :NICKNAME:
50 ;; :IGNORE:
51 ;; :ICON:
52 ;; :NOTE:
53 ;; :ADDRESS:
54 ;; :BIRTHDAY:
55 ;; :END:")))
57 ;;;; Usage:
59 ;; How to search?
60 ;; - You can use [M-x org-contacts] command to search.
62 ;; - You can use `org-sparse-tree' [C-c / p] to filter based on a
63 ;; specific property. Or other matcher on `org-sparse-tree'.
65 ;;; Code:
67 (require 'cl-lib)
68 (require 'org)
69 (require 'gnus-util)
70 (require 'gnus-art)
71 (require 'mail-utils)
72 (require 'org-agenda)
73 (require 'org-capture)
74 (require 'ol)
75 (autoload 'with-memoization "subr")
77 (declare-function diary-anniversary "diary-lib" (month day &optional year mark))
78 (declare-function erc-server-buffer-live-p "erc" ())
79 (declare-function erc-server-process-alive "erc" (&optional buffer))
80 (defvar erc-server-processing-p)
82 (defgroup org-contacts nil
83 "Options about contacts management."
84 :group 'org)
86 (defcustom org-contacts-directory nil
87 "Directory of Org files to use as contacts source.
88 When set to nil, all your Org files will be used."
89 :type 'string)
91 (defcustom org-contacts-files nil
92 "List of Org files to use as contacts source.
93 When set to nil, all your Org files will be used."
94 :type '(repeat file))
96 (defcustom org-contacts-email-property "EMAIL"
97 "Name of the property for contact email address."
98 :type 'string)
100 (defcustom org-contacts-tel-property "PHONE"
101 "Name of the property for contact phone number."
102 :type 'string)
104 (defcustom org-contacts-address-property "ADDRESS"
105 "Name of the property for contact address."
106 :type 'string)
108 (defcustom org-contacts-birthday-property "BIRTHDAY"
109 "Name of the property for contact birthday date."
110 :type 'string)
112 (defcustom org-contacts-note-property "NOTE"
113 "Name of the property for contact note."
114 :type 'string)
116 (defcustom org-contacts-alias-property "ALIAS"
117 "Name of the property for contact name alias."
118 :type 'string)
120 (defcustom org-contacts-ignore-property "IGNORE"
121 "Name of the property, which values will be ignored when
122 completing or exporting to vcard."
123 :type 'string)
126 (defcustom org-contacts-birthday-format "Birthday: %l (%Y)"
127 "Format of the anniversary agenda entry.
128 The following replacements are available:
130 %h - Heading name
131 %l - Link to the heading
132 %y - Number of year
133 %Y - Number of year (ordinal)"
134 :type 'string)
136 (defcustom org-contacts-last-read-mail-property "LAST_READ_MAIL"
137 "Name of the property for contact last read email link storage."
138 :type 'string)
140 (defcustom org-contacts-icon-property "ICON"
141 "Name of the property for contact icon."
142 :type 'string)
144 (defcustom org-contacts-nickname-property "NICKNAME"
145 "Name of the property for IRC nickname match."
146 :type 'string)
148 (defcustom org-contacts-icon-size 32
149 "Size of the contacts icons."
150 :type 'string)
152 (defcustom org-contacts-icon-use-gravatar (fboundp 'gravatar-retrieve)
153 "Whether use Gravatar to fetch contact icons."
154 :type 'boolean)
156 (defcustom org-contacts-completion-ignore-case t
157 "Ignore case when completing contacts."
158 :type 'boolean)
160 (defcustom org-contacts-group-prefix "+"
161 "Group prefix."
162 :type 'string)
164 (defcustom org-contacts-tags-props-prefix "#"
165 "Tags and properties prefix."
166 :type 'string)
168 (defcustom org-contacts-matcher
169 (mapconcat #'identity
170 (mapcar (lambda (x) (concat x "<>\"\""))
171 (list org-contacts-email-property
172 org-contacts-alias-property
173 org-contacts-tel-property
174 org-contacts-address-property
175 org-contacts-birthday-property))
176 "|")
177 "Matching rule for finding heading that are contacts.
178 This can be a tag name, or a property check."
179 :type 'string)
181 (defcustom org-contacts-email-link-description-format "%s (%d)"
182 "Format used to store links to email.
183 This overrides `org-email-link-description-format' if set."
184 :type 'string)
186 (defcustom org-contacts-vcard-file "contacts.vcf"
187 "Default file for vcard export."
188 :type 'file)
190 (defcustom org-contacts-enable-completion t
191 "Enable or not the completion in `message-mode' with `org-contacts'."
192 :type 'boolean)
194 (defcustom org-contacts-complete-functions
195 '(org-contacts-complete-group org-contacts-complete-tags-props org-contacts-complete-name)
196 "List of functions used to complete contacts in `message-mode'."
197 :type 'hook)
199 ;; Decalre external functions and variables
200 (declare-function org-reverse-string "org")
201 (declare-function diary-ordinal-suffix "ext:diary-lib")
202 (declare-function wl-summary-message-number "ext:wl-summary")
203 (declare-function wl-address-header-extract-address "ext:wl-address")
204 (declare-function wl-address-header-extract-realname "ext:wl-address")
205 (declare-function erc-buffer-list "ext:erc")
206 (declare-function erc-get-channel-user-list "ext:erc")
207 (declare-function google-maps-static-show "ext:google-maps-static")
208 (declare-function elmo-message-field "ext:elmo-pipe")
209 (declare-function std11-narrow-to-header "ext:std11")
210 (declare-function std11-fetch-field "ext:std11")
212 (defconst org-contacts-property-values-separators "[,; \f\t\n\r\v]+"
213 "The default value of separators for `org-contacts-split-property'.
215 A regexp matching strings of whitespace, `,' and `;'.")
217 (defvar org-contacts-keymap
218 (let ((map (make-sparse-keymap)))
219 (define-key map "M" #'org-contacts-view-send-email)
220 (define-key map "i" #'org-contacts-view-switch-to-irc-buffer)
221 map)
222 "The keymap used in `org-contacts' result list.")
224 (defvar org-contacts-db nil
225 "Org Contacts database.")
227 (defvar org-contacts-last-update nil
228 "Last time the Org Contacts database has been updated.")
230 (defun org-contacts-files ()
231 "Return list of Org files to use for contact management."
232 (if org-contacts-files
233 org-contacts-files
234 (message "[ERROR] Your custom variable `org-contacts-files' is nil. Revert to `org-agenda-files' now.")
235 (org-agenda-files t 'ifmode)))
237 (defun org-contacts-db-need-update-p ()
238 "Determine whether `org-contacts-db' needs to be refreshed."
239 (or (null org-contacts-last-update)
240 (cl-find-if (lambda (file)
241 (or (time-less-p org-contacts-last-update
242 (elt (file-attributes file) 5))))
243 (org-contacts-files))
244 (org-contacts-db-has-dead-markers-p org-contacts-db)))
246 (defun org-contacts-db-has-dead-markers-p (db)
247 "Return t if at least one dead marker is found in DB.
248 A dead marker in this case is a marker pointing to dead or no
249 buffer."
250 ;; Scan contacts list looking for dead markers, and return t at first found.
251 (catch 'dead-marker-found
252 (while db
253 (unless (marker-buffer (nth 1 (car db)))
254 (throw 'dead-marker-found t))
255 (setq db (cdr db)))
256 nil))
258 (defun org-contacts-db ()
259 "Return the latest Org Contacts Database."
260 (let* ((org--matcher-tags-todo-only nil)
261 (contacts-matcher (cdr (org-make-tags-matcher org-contacts-matcher)))
262 result)
263 (when (org-contacts-db-need-update-p)
264 (let ((progress-reporter
265 (make-progress-reporter "Updating Org Contacts Database..." 0 (length (org-contacts-files))))
266 (i 0))
267 (dolist (file (org-contacts-files))
268 (if (catch 'nextfile
269 ;; if file doesn't exist and the user agrees to removing it
270 ;; from org-agendas-list, 'nextfile is thrown. Catch it here
271 ;; and skip processing the file.
273 ;; TODO: suppose that the user has set an org-contacts-files
274 ;; list that contains an element that doesn't exist in the
275 ;; file system: in that case, the org-agenda-files list could
276 ;; be updated (and saved to the customizations of the user) if
277 ;; it contained the same file even though the org-agenda-files
278 ;; list wasn't actually used. I don't think it is normal that
279 ;; org-contacts updates org-agenda-files in this case, but
280 ;; short of duplicating org-check-agenda-files and
281 ;; org-remove-files, I don't know how to avoid it.
283 ;; A side effect of the TODO is that the faulty
284 ;; org-contacts-files list never gets updated and thus the
285 ;; user is always queried about the missing files when
286 ;; org-contacts-db-need-update-p returns true.
287 (org-check-agenda-file file))
288 (message "Skipped %s removed from org-agenda-files list."
289 (abbreviate-file-name file))
290 (with-current-buffer (org-get-agenda-file-buffer file)
291 (unless (eq major-mode 'org-mode)
292 (error "File %s is not in `org-mode'" file))
293 (setf result
294 (append result
295 (org-scan-tags 'org-contacts-at-point
296 contacts-matcher
297 org--matcher-tags-todo-only)))))
298 (progress-reporter-update progress-reporter (setq i (1+ i))))
299 (setf org-contacts-db result
300 org-contacts-last-update (current-time))
301 (progress-reporter-done progress-reporter)))
302 org-contacts-db))
304 (defun org-contacts-at-point (&optional pom)
305 "Return the contacts at point-or-marker POM or current position
306 if nil."
307 (setq pom (or pom (point)))
308 (org-with-point-at pom
309 (list (org-get-heading t) (set-marker (make-marker) pom) (org-entry-properties pom 'all))))
311 (defun org-contacts-filter (&optional name-match tags-match prop-match)
312 "Search for a contact matching any of NAME-MATCH, TAGS-MATCH, PROP-MATCH.
313 If all match values are nil, return all contacts.
315 The optional PROP-MATCH argument is a single (PROP . VALUE) cons
316 cell corresponding to the contact properties.
318 (if (and (null name-match)
319 (null prop-match)
320 (null tags-match))
321 (org-contacts-db)
322 (cl-loop for contact in (org-contacts-db)
323 if (or
324 (and name-match
325 (string-match-p name-match
326 (cl-first contact)))
327 (and prop-match
328 (cl-find-if (lambda (prop)
329 (and (string= (car prop-match) (car prop))
330 (string-match-p (cdr prop-match) (cdr prop))))
331 (caddr contact)))
332 (and tags-match
333 (cl-find-if (lambda (tag)
334 (string-match-p tags-match tag))
335 (org-split-string
336 (or (cdr (assoc-string "ALLTAGS" (caddr contact))) "") ":"))))
337 collect contact)))
339 (defun org-contacts-try-completion-prefix (to-match collection &optional predicate)
340 "Custom implementation of `try-completion'.
341 This version works only with list and alist and it looks at all
342 prefixes rather than just the beginning of the string."
343 (cl-loop with regexp = (concat "\\b" (regexp-quote to-match))
344 with ret = nil
345 with ret-start = nil
346 with ret-end = nil
348 for el in collection
349 for string = (if (listp el) (car el) el)
351 for start = (when (or (null predicate) (funcall predicate string))
352 (string-match regexp string))
354 if start
355 do (let ((end (match-end 0))
356 (len (length string)))
357 (if (= end len)
358 (cl-return t)
359 (cl-destructuring-bind (string start end)
360 (if (null ret)
361 (cl-values string start end)
362 (org-contacts-common-substring
363 ret ret-start ret-end
364 string start end))
365 (setf ret string
366 ret-start start
367 ret-end end))))
369 finally (cl-return
370 (replace-regexp-in-string "\\`[ \t\n]*" "" ret))))
372 (defun org-contacts-compare-strings (s1 start1 end1 s2 start2 end2 &optional ignore-case)
373 "Compare the contents of two strings, using `compare-strings'.
375 This function works like `compare-strings' excepted that it
376 returns a cons.
377 - The CAR is the number of characters that match at the beginning.
378 - The CDR is T is the two strings are the same and NIL otherwise."
379 (let ((ret (compare-strings s1 start1 end1 s2 start2 end2 ignore-case)))
380 (if (eq ret t)
381 (cons (or end1 (length s1)) t)
382 (cons (1- (abs ret)) nil))))
384 (defun org-contacts-common-substring (s1 start1 end1 s2 start2 end2)
385 "Extract the common substring between S1 and S2.
387 This function extracts the common substring between S1 and S2 and
388 adjust the part that remains common.
390 START1 and END1 delimit the part in S1 that we know is common
391 between the two strings. This applies to START2 and END2 for S2.
393 This function returns a list whose contains:
394 - The common substring found.
395 - The new value of the start of the known inner substring.
396 - The new value of the end of the known inner substring."
397 ;; Given two strings:
398 ;; s1: "foo bar baz"
399 ;; s2: "fooo bar baz"
400 ;; and the inner substring is "bar"
401 ;; then: start1 = 4, end1 = 6, start2 = 5, end2 = 7
403 ;; To find the common substring we will compare two substrings:
404 ;; " oof" and " ooof" to find the beginning of the common substring.
405 ;; " baz" and " baz" to find the end of the common substring.
406 (let* ((len1 (length s1))
407 (start1 (or start1 0))
408 (end1 (or end1 len1))
410 (len2 (length s2))
411 (start2 (or start2 0))
412 (end2 (or end2 len2))
414 (new-start (car (org-contacts-compare-strings
415 (substring (org-reverse-string s1) (- len1 start1)) nil nil
416 (substring (org-reverse-string s2) (- len2 start2)) nil nil)))
418 (new-end (+ end1 (car (org-contacts-compare-strings
419 (substring s1 end1) nil nil
420 (substring s2 end2) nil nil)))))
421 (list (substring s1 (- start1 new-start) new-end)
422 new-start
423 (+ new-start (- end1 start1)))))
425 (defun org-contacts-all-completions-prefix (to-match collection &optional predicate)
426 "Custom version of `all-completions'.
427 This version works only with list and alist and it looks at all
428 prefixes rather than just the beginning of the string."
429 (cl-loop with regexp = (concat "\\b" (regexp-quote to-match))
430 for el in collection
431 for string = (if (listp el) (car el) el)
432 for match? = (when (and (or (null predicate) (funcall predicate string)))
433 (string-match regexp string))
434 if match?
435 collect (progn
436 (let ((end (match-end 0)))
437 (org-no-properties string)
438 (when (< end (length string))
439 ;; Here we add a text property that will be used
440 ;; later to highlight the character right after
441 ;; the common part between each addresses.
442 ;; See `org-contacts-display-sort-function'.
443 (put-text-property end (1+ end) 'org-contacts-prefix 't string)))
444 string)))
446 (defun org-contacts-make-collection-prefix (collection)
447 "Make a collection function from COLLECTION which will match on prefixes."
448 (let ((collection collection))
449 (lambda (string predicate flag)
450 (cond ((eq flag nil)
451 (org-contacts-try-completion-prefix string collection predicate))
452 ((eq flag t)
453 ;; `org-contacts-all-completions-prefix' has already been
454 ;; used to compute `all-completions'.
455 collection)
456 ((eq flag 'lambda)
457 (org-contacts-test-completion-prefix string collection predicate))
458 ((and (listp flag) (eq (car flag) 'boundaries))
459 (org-contacts-boundaries-prefix string collection predicate (cdr flag)))
460 ((eq flag 'metadata)
461 (org-contacts-metadata-prefix))
462 (t nil ; operation unsupported
463 )))))
465 (defun org-contacts-display-sort-function (completions)
466 "Sort function for contacts display."
467 (mapcar (lambda (string)
468 (cl-loop with len = (1- (length string))
469 for i upfrom 0 to len
470 if (memq 'org-contacts-prefix
471 (text-properties-at i string))
472 do (set-text-properties
473 i (1+ i)
474 (list 'font-lock-face
475 (if (char-equal (aref string i)
476 (string-to-char " "))
477 ;; Spaces can't be bold.
478 'underline
479 'bold)) string)
480 else
481 do (set-text-properties i (1+ i) nil string)
482 finally (cl-return string)))
483 completions))
485 (defun org-contacts-test-completion-prefix (string collection predicate)
486 (cl-find-if (lambda (el)
487 (and (or (null predicate) (funcall predicate el))
488 (string= string el)))
489 collection))
491 (defun org-contacts-boundaries-prefix (string collection predicate suffix)
492 (cl-list* 'boundaries (completion-boundaries string collection predicate suffix)))
494 (defun org-contacts-metadata-prefix (&rest _)
495 '(metadata .
496 ((cycle-sort-function . org-contacts-display-sort-function)
497 (display-sort-function . org-contacts-display-sort-function))))
499 (defun org-contacts-complete-group (string)
500 "Complete text at START from a group.
502 A group FOO is composed of contacts with the tag FOO."
503 (let* ((completion-ignore-case org-contacts-completion-ignore-case)
504 (group-completion-p (string-match-p
505 (concat "^" org-contacts-group-prefix) string)))
506 (when group-completion-p
507 (let ((completion-list
508 (all-completions
509 string
510 (mapcar (lambda (group)
511 (propertize (concat org-contacts-group-prefix group)
512 'org-contacts-group group))
513 (org-uniquify
514 (cl-loop for contact in (org-contacts-filter)
515 nconc (org-split-string
516 (or (cdr (assoc-string "ALLTAGS" (caddr contact))) "") ":")))))))
518 (if (= (length completion-list) 1)
519 ;; We've found the correct group, returns the address
520 (let ((tag (get-text-property 0 'org-contacts-group
521 (car completion-list))))
522 (mapconcat #'identity
523 (cl-loop for contact in (org-contacts-filter
525 tag)
526 ;; The contact name is always the car of the assoc-list
527 ;; returned by `org-contacts-filter'.
528 for contact-name = (car contact)
529 ;; Grab the first email of the contact
530 for email = (org-contacts-strip-link
531 (or (car (org-contacts-split-property
533 (cdr (assoc-string org-contacts-email-property
534 (cl-caddr contact)))
535 ""))) ""))
536 ;; If the user has an email address, append USER <EMAIL>.
537 if email collect (org-contacts-format-email contact-name email))
538 ", "))
539 ;; We haven't found the correct group
540 (completion-table-case-fold completion-list
541 (not org-contacts-completion-ignore-case)))))))
543 (defun org-contacts-complete-tags-props (string)
544 "Insert emails that match the tags expression.
546 For example: FOO-BAR will match entries tagged with FOO but not
547 with BAR.
549 See (org) Matching tags and properties for a complete
550 description."
551 (let* ((completion-ignore-case org-contacts-completion-ignore-case)
552 (completion-p (string-match-p
553 (concat "^" org-contacts-tags-props-prefix) string)))
554 (when completion-p
555 (let ((result
556 (mapconcat
557 #'identity
558 (cl-loop for contact in (org-contacts-db)
559 for contact-name = (car contact)
560 for email = (org-contacts-strip-link
561 (or (car (org-contacts-split-property
563 (cdr (assoc-string org-contacts-email-property
564 (cl-caddr contact)))
565 "")))
566 ""))
567 ;; for tags = (cdr (assoc "TAGS" (nth 2 contact)))
568 ;; for tags-list = (if tags
569 ;; (split-string (substring (cdr (assoc "TAGS" (nth 2 contact))) 1 -1) ":")
570 ;; '())
571 for marker = (nth 1 contact)
572 if (with-current-buffer (marker-buffer marker)
573 (save-excursion
574 (goto-char marker)
575 ;; FIXME: AFAIK, `org-make-tags-matcher' returns
576 ;; a cons whose cdr is a function, so why do we
577 ;; pass it to `eval'?
578 (eval (cdr (org-make-tags-matcher (cl-subseq string 1)))
579 t)))
580 collect (org-contacts-format-email contact-name email))
581 ",")))
582 (when (not (string= "" result))
583 result)))))
585 (defun org-contacts-remove-ignored-property-values (ignore-list list)
586 "Remove all ignore-list's elements from list and you can use
587 regular expressions in the ignore list."
588 (cl-remove-if (lambda (el)
589 (cl-find-if (lambda (x)
590 (string-match-p x el))
591 ignore-list))
592 list))
594 (defun org-contacts-complete-name (string)
595 "Complete text at START with a user name and email."
596 (let* ((completion-ignore-case org-contacts-completion-ignore-case)
597 (completion-list
598 (cl-loop for contact in (org-contacts-filter)
599 ;; The contact name is always the car of the assoc-list
600 ;; returned by `org-contacts-filter'.
601 for contact-name = (car contact)
603 ;; Build the list of the email addresses which has
604 ;; been expired
605 for ignore-list = (org-contacts-split-property
606 (or (cdr (assoc-string org-contacts-ignore-property
607 (nth 2 contact))) ""))
608 ;; Build the list of the user email addresses.
609 for email-list = (org-contacts-remove-ignored-property-values
610 ignore-list
611 (org-contacts-split-property
612 (or (cdr (assoc-string org-contacts-email-property
613 (nth 2 contact))) "")))
614 ;; If the user has email addresses…
615 if email-list
616 ;; … append a list of USER <EMAIL>.
617 nconc (cl-loop for email in email-list
618 collect (org-contacts-format-email
619 contact-name (org-contacts-strip-link email)))))
620 (completion-list (org-contacts-all-completions-prefix
621 string
622 (org-uniquify completion-list))))
623 (when completion-list
624 (org-contacts-make-collection-prefix completion-list))))
626 (defun org-contacts-message-complete-function ()
627 "Function used in `completion-at-point-functions' in `message-mode'."
628 (let ((mail-abbrev-mode-regexp
629 "^\\(Resent-To\\|To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\|Disposition-Notification-To\\|Return-Receipt-To\\):"))
630 (when (mail-abbrev-in-expansion-header-p)
631 (let
632 ((beg
633 (save-excursion
634 (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
635 (goto-char (match-end 0))
636 (point)))
637 (end (point)))
638 (list beg
640 (completion-table-dynamic
641 (lambda (string)
642 (run-hook-with-args-until-success
643 'org-contacts-complete-functions string))))))))
645 (defun org-contacts-org-complete--annotation-function (candidate)
646 "Return org-contacts tags of contact candidate."
647 ;; TODO
648 (ignore candidate))
650 (defun org-contacts-org-complete--doc-function (candidate)
651 "Return org-contacts content of contact candidate."
652 (let* ((candidate (substring-no-properties candidate 1 nil))
653 (contact (seq-find
654 (lambda (contact) (string-equal (plist-get contact :name) candidate))
655 org-contacts-all-contacts))
656 (name (plist-get contact :name))
657 (file (plist-get contact :file))
658 (position (plist-get contact :position))
659 (doc-buffer (get-buffer-create " *org-contact*"))
660 (org-contact-buffer (get-buffer (find-file-noselect file)))
661 ;; get org-contact headline and property drawer.
662 (contents (with-current-buffer org-contact-buffer
663 (when (derived-mode-p 'org-mode)
664 (save-excursion
665 (goto-char position)
666 (cond ((ignore-errors (org-edit-src-code))
667 (delete-other-windows))
668 ((org-at-block-p)
669 (org-narrow-to-block))
670 (t (org-narrow-to-subtree)))
671 (let ((content (buffer-substring (point-min) (point-max))))
672 (when (buffer-narrowed-p) (widen))
673 content))))))
674 (ignore name)
675 (with-current-buffer doc-buffer
676 (read-only-mode 1)
677 (let ((inhibit-read-only t))
678 (erase-buffer)
679 (insert contents)
680 (org-mode)
681 (org-fold-show-all)
682 (font-lock-ensure)))
683 doc-buffer))
685 ;;; display company-mode doc buffer bellow current window.
686 (add-to-list 'display-buffer-alist '("^ \\*org-contact\\*" . (display-buffer-below-selected)))
688 (defun org-contacts-org-complete--location-function (candidate)
689 "Return org-contacts location of contact candidate."
690 (let* ((candidate (substring-no-properties candidate 1 nil))
691 (contact (seq-find
692 (lambda (contact) (string-equal (plist-get contact :name) candidate))
693 org-contacts-all-contacts))
694 (name (plist-get contact :name))
695 (file (plist-get contact :file))
696 (position (plist-get contact :position)))
697 (ignore name)
698 (with-current-buffer (find-file-noselect file)
699 (goto-char position)
700 (cons (current-buffer) position))))
702 ;;;###autoload
703 (defun org-contacts-org-complete-function ()
704 "completion-at-point function to complete @name in `org-mode'.
705 Usage: (add-hook \\='completion-at-point-functions
706 #\\='org-contacts-org-complete-function nil \\='local)"
707 (when-let* ((end (point))
708 (begin (save-excursion (skip-chars-backward "[:alnum:]@") (point)))
709 (symbol (buffer-substring-no-properties begin end))
710 (org-contacts-prefix-p (string-prefix-p "@" symbol)))
711 (when org-contacts-prefix-p
712 (list begin
714 (completion-table-dynamic
715 (lambda (_)
716 (mapcar
717 (lambda (contact) (concat "@" (plist-get contact :name)))
718 org-contacts-all-contacts)))
720 :predicate 'stringp
721 :exclusive 'no
722 ;; properties check out `completion-extra-properties'
723 :annotation-function #'org-contacts-org-complete--annotation-function
724 ;; :exit-function ; TODO change completion candidate inserted contact name into org-contact link??
726 :company-docsig #'identity ; metadata
727 :company-doc-buffer #'org-contacts-org-complete--doc-function ; doc popup
728 :company-location #'org-contacts-org-complete--location-function))))
730 (defun org-contacts-gnus-get-name-email ()
731 "Get name and email address from Gnus message."
732 (if (gnus-alive-p)
733 (gnus-with-article-headers
734 (mail-extract-address-components
735 (or (mail-fetch-field "From") "")))))
737 (defun org-contacts-gnus-article-from-get-marker ()
738 "Return a marker for a contact based on From."
739 (let* ((address (org-contacts-gnus-get-name-email))
740 (name (car address))
741 (email (cadr address)))
742 (cl-cadar (or (org-contacts-filter
745 (cons org-contacts-email-property (concat "\\b" (regexp-quote email) "\\b")))
746 (when name
747 (org-contacts-filter
748 (concat "^" name "$")))))))
750 (defun org-contacts-gnus-article-from-goto ()
751 "Go to contact in the From address of current Gnus message."
752 (interactive)
753 (let ((marker (org-contacts-gnus-article-from-get-marker)))
754 (when marker
755 (switch-to-buffer-other-window (marker-buffer marker))
756 (goto-char marker)
757 (when (eq major-mode 'org-mode)
758 (if (fboundp 'org-fold-show-context)
759 (org-fold-show-context 'agenda)
760 (org-fold-show-context 'agenda))))))
762 (with-no-warnings (defvar date)) ;; unprefixed, from calendar.el
763 (defun org-contacts-anniversaries (&optional field format)
764 "Compute FIELD anniversary for each contact, returning FORMAT.
765 Default FIELD value is \"BIRTHDAY\".
767 Format is a string matching the following format specification:
769 %h - Heading name
770 %l - Link to the heading
771 %y - Number of year
772 %Y - Number of year (ordinal)"
773 (let ((calendar-date-style 'american))
774 (unless format (setq format org-contacts-birthday-format))
775 (cl-loop for contact in (org-contacts-filter)
776 for anniv = (let ((anniv (cdr (assoc-string
777 (or field org-contacts-birthday-property)
778 (nth 2 contact)))))
779 (when anniv
780 (calendar-gregorian-from-absolute
781 (org-time-string-to-absolute anniv))))
782 ;; Use `diary-anniversary' to compute anniversary.
783 ;; FIXME: should we require `diary-lib' somewhere to be sure
784 ;; `diary-anniversary' is defined when we get here?
785 if (and anniv (apply #'diary-anniversary anniv))
786 collect (format-spec format
787 `((?l . ,(org-with-point-at (cadr contact) (org-store-link nil)))
788 (?h . ,(car contact))
789 (?y . ,(- (calendar-extract-year date)
790 (calendar-extract-year anniv)))
791 (?Y . ,(let ((years (- (calendar-extract-year date)
792 (calendar-extract-year anniv))))
793 (format "%d%s" years (diary-ordinal-suffix years)))))))))
795 (defun org-contacts--completing-read-date ( prompt _collection
796 &optional _predicate _require-match _initial-input
797 _hist def _inherit-input-method)
798 "Like `completing-read' but reads a date.
799 Only PROMPT and DEF are really used."
800 (org-read-date nil nil nil prompt nil def))
802 (add-to-list 'org-property-set-functions-alist
803 `(,org-contacts-birthday-property . org-contacts--completing-read-date))
805 (defun org-contacts-template-name (&optional return-value)
806 "Try to return the contact name for a template.
807 If not found return RETURN-VALUE or something that would ask the user."
808 (or (car (org-contacts-gnus-get-name-email))
809 return-value
810 "%^{Name}"))
812 (defun org-contacts-template-email (&optional return-value)
813 "Try to return the contact email for a template.
814 If not found return RETURN-VALUE or something that would ask the user."
815 (or (cadr (org-contacts-gnus-get-name-email))
816 return-value
817 (concat "%^{" org-contacts-email-property "}p")))
819 (defun org-contacts-gnus-store-last-mail ()
820 "Store a link between mails and contacts.
822 This function should be called from `gnus-article-prepare-hook'."
823 (let ((marker (org-contacts-gnus-article-from-get-marker)))
824 (when marker
825 (with-current-buffer (marker-buffer marker)
826 (save-excursion
827 (goto-char marker)
828 (let* ((org-link-email-description-format (or org-contacts-email-link-description-format
829 org-link-email-description-format))
830 (link (gnus-with-article-buffer (org-store-link nil))))
831 (org-set-property org-contacts-last-read-mail-property link)))))))
833 (defun org-contacts-icon-as-string ()
834 "Return the contact icon as a string."
835 (let ((image (org-contacts-get-icon)))
836 (concat
837 (propertize "-" 'display
838 (append
839 (if image
840 image
841 `'(space :width (,org-contacts-icon-size)))
842 '(:ascent center)))
843 " ")))
845 ;;====================================== org-contacts searching =====================================
847 (defcustom org-contacts-identity-properties-list
848 `(,org-contacts-email-property
849 ,org-contacts-alias-property
850 ,org-contacts-tel-property
851 ,org-contacts-address-property
852 ,org-contacts-birthday-property)
853 "Matching rule for finding heading that are contacts.
854 This can be property key checking."
855 :type 'list
856 :safe #'listp)
858 (defvar org-contacts-ahead-space-padding (make-string 5 ? )
859 "The space padding for align avatar image with contact name and properties.")
861 (defun org-contacts--candidate (headline)
862 "Return candidate string from Org HEADLINE epom element node."
863 (let* ((org-contacts-icon-size 32)
864 (contact-name (org-element-property :raw-value headline))
865 (tags (org-element-property :tags headline))
866 (properties (org-entry-properties headline 'standard))
867 ;; extra headline properties
868 (avatar-image-path
869 (when-let* ((avatar-value (car (org-entry-get-multivalued-property headline "AVATAR")))
870 (avatar-link-path (cond
871 ;; [[file:contact_dir/avatar.png]]
872 ((string-match org-link-plain-re avatar-value)
873 (when (string-equal (match-string 1 avatar-value) "file")
874 (match-string 2 avatar-value)))
875 ;; contact-name.jpg
876 ((string-match (concat (regexp-opt image-file-name-extensions) (rx line-end)) avatar-value)
877 (match-string 0 avatar-value))))
878 (avatar-absolute-path (file-name-concat
879 (or org-contacts-directory
880 (expand-file-name (file-name-directory (car org-contacts-files))))
881 avatar-link-path))
882 ( (org-file-image-p avatar-absolute-path))
883 ( (file-exists-p avatar-absolute-path)))
884 avatar-absolute-path))
885 (info (concat "\n"
886 (concat org-contacts-ahead-space-padding " ")
887 (string-join (let ((org-property-separators (list (cons org-contacts-nickname-property "[,\ ]"))))
888 (org-entry-get-multivalued-property headline org-contacts-nickname-property)) ", ")
889 (string-join (let ((org-property-separators (list (cons org-contacts-email-property "[,\ ]"))))
890 (org-entry-get-multivalued-property headline org-contacts-email-property)) ", ")
891 "\n"))
892 (middle-line-length (when-let* ((length (- (- org-tags-column)
893 (length (string-join tags ":"))
894 (length contact-name)))
895 (wholenump length))
896 length)))
897 ;; detect whether headline is an org-contacts entry?
898 (when (seq-intersection org-contacts-identity-properties-list (mapcar 'car properties))
899 (propertize
900 (concat
901 (if avatar-image-path
902 (propertize org-contacts-ahead-space-padding
903 'display (create-image avatar-image-path nil nil
904 :ascent 30 ; set image baseline to align image top with candidate line.
905 :width org-contacts-icon-size))
906 org-contacts-ahead-space-padding)
908 contact-name
909 (format " %s [%s]"
910 (make-string (or middle-line-length 0) ?―)
911 (string-join tags ":")))
912 'contact-name contact-name
913 'annotation info))))
915 (defun org-contacts--candidates (files)
916 "Return a list of candidates from FILES."
917 (with-temp-buffer
918 (dolist (file files)
919 (insert-file-contents file) ; don't need to actually open file.
920 (goto-char (point-max))
921 (newline 2))
922 (delay-mode-hooks ; This will prevent user hooks from running during parsing.
923 (org-mode)
924 (goto-char (point-min))
925 (let ((candidates nil))
926 (org-element-map (org-element-parse-buffer 'headline) 'headline
927 (lambda (headline)
928 (when-let ((candidate (org-contacts--candidate headline)))
929 (push candidate candidates))))
930 (nreverse candidates)))))
932 (defun org-contacts--annotator (candidate)
933 "Annotate contact completion CANDIDATE."
934 (concat (propertize " " 'display '(space :align-to center))
935 (get-text-property 0 'annotation candidate)))
937 (defun org-contacts--return-candidates (&optional files)
938 "Return org-contacts candidates which parsed from FILES."
939 (if-let ((files (or files org-contacts-files)))
940 (org-contacts--candidates files)
941 (user-error "Files does not exist: %S" files)))
943 (defvar org-contacts--candidates-cache nil
944 "A cache variable of org-contacts--candidates.")
946 (defun org-contacts-browse-function (contact-name)
947 "Jump to CONTACT-NAME headline."
948 (mapcar
949 (lambda (file)
950 (let ((buf (find-file-noselect (expand-file-name file))))
951 (with-current-buffer buf
952 ;; NOTE: `org-goto-marker-or-bmk' will display buffer in current window, not follow `display-buffer' rule.
953 (org-goto-marker-or-bmk (org-find-exact-headline-in-buffer contact-name))
954 ;; FIXME: `goto-char' not physically move point in buffer.
955 ;; (display-buffer buf '(display-buffer-below-selected))
956 ;; (goto-char (org-find-exact-headline-in-buffer contact-name nil t))
958 org-contacts-files))
960 ;;;###autoload
961 (defun org-contacts (&optional files)
962 "Search org-contacts from FILES and jump to contact location."
963 (interactive)
964 (unless org-contacts--candidates-cache
965 (setq org-contacts--candidates-cache
966 (org-contacts--return-candidates (or files org-contacts-files))))
967 (if-let* ((files (or files org-contacts-files))
968 ((seq-every-p 'file-exists-p files)))
969 (when-let* ((candidates org-contacts--candidates-cache)
970 (minibuffer-allow-text-properties t)
971 (completion-extra-properties
972 (list :category 'org-contacts
973 :annotation-function #'org-contacts--annotator))
974 (choice (completing-read "org-contacts: " candidates nil 'require-match))
975 (contact-name (get-text-property 0 'contact-name choice)))
976 ;; jump to org-contacts file contact position.
977 (org-contacts-browse-function contact-name))
978 (user-error "Files does not exist: %S" files)))
980 ;;;###autoload
981 (defun org-contacts-agenda (name)
982 "Create agenda view for contacts matching NAME."
983 (interactive (list (read-string "Name: ")))
984 (let ((org-agenda-files (org-contacts-files))
985 (org-agenda-skip-function
986 (lambda () (org-agenda-skip-if nil `(notregexp ,name))))
987 (org-agenda-prefix-format
988 (propertize
989 "%(org-contacts-icon-as-string)% s%(org-contacts-irc-number-of-unread-messages) "
990 'keymap org-contacts-keymap))
991 (org-agenda-overriding-header
992 (or org-agenda-overriding-header
993 (concat "List of contacts matching `" name "':"))))
994 (setq org-agenda-skip-regexp name)
995 (org-tags-view nil org-contacts-matcher)
996 (with-current-buffer org-agenda-buffer-name
997 (setq org-agenda-redo-command
998 (list 'org-contacts name)))))
1000 (defun org-contacts-completing-read (prompt
1001 &optional predicate
1002 initial-input hist def inherit-input-method)
1003 "Call `completing-read' with contacts name as collection."
1004 (org-completing-read
1005 prompt (org-contacts-filter) predicate t initial-input hist def inherit-input-method))
1007 (defun org-contacts-format-name (name)
1008 "Trim any local formatting to get a bare NAME."
1009 ;; Remove radio targets characters
1010 (replace-regexp-in-string org-radio-target-regexp "\\1" name))
1012 (defun org-contacts-format-email (name email)
1013 "Format an EMAIL address corresponding to NAME."
1014 (unless email
1015 (error "`email' cannot be nul"))
1016 (if name
1017 (concat (org-contacts-format-name name) " <" email ">")
1018 email))
1020 (defun org-contacts-check-mail-address (mail)
1021 "Add MAIL address to contact at point if it does not have it."
1022 (let ((mails (org-entry-get (point) org-contacts-email-property)))
1023 (unless (member mail (split-string mails))
1024 (when (yes-or-no-p
1025 (format "Do you want to add this address to %s?" (org-get-heading t)))
1026 (org-set-property org-contacts-email-property (concat mails " " mail))))))
1028 (defun org-contacts-gnus-check-mail-address ()
1029 "Check that contact has the current address recorded.
1030 This function should be called from `gnus-article-prepare-hook'."
1031 (let ((marker (org-contacts-gnus-article-from-get-marker)))
1032 (when marker
1033 (org-with-point-at marker
1034 (org-contacts-check-mail-address (cadr (org-contacts-gnus-get-name-email)))))))
1036 (defun org-contacts-gnus-insinuate ()
1037 "Add some hooks for Gnus user.
1038 This adds `org-contacts-gnus-check-mail-address' and
1039 `org-contacts-gnus-store-last-mail' to
1040 `gnus-article-prepare-hook'. It also adds a binding on `;' in
1041 `gnus-summary-mode-map' to `org-contacts-gnus-article-from-goto'"
1042 (require 'gnus)
1043 (require 'gnus-art)
1044 (define-key gnus-summary-mode-map ";" #'org-contacts-gnus-article-from-goto)
1045 (add-hook 'gnus-article-prepare-hook #'org-contacts-gnus-check-mail-address)
1046 (add-hook 'gnus-article-prepare-hook #'org-contacts-gnus-store-last-mail))
1048 ;;;###autoload
1049 (defun org-contacts-setup-completion-at-point ()
1050 "Add `org-contacts-message-complete-function' as a new function
1051 to complete the thing at point."
1052 (add-to-list 'completion-at-point-functions
1053 'org-contacts-message-complete-function))
1055 (defun org-contacts-unload-hook ()
1056 (remove-hook 'message-mode-hook #'org-contacts-setup-completion-at-point))
1058 (when (and org-contacts-enable-completion
1059 (boundp 'completion-at-point-functions))
1060 (add-hook 'message-mode-hook #'org-contacts-setup-completion-at-point))
1062 (defun org-contacts-wl-get-from-header-content ()
1063 "Retrieve the content of the `From' header of an email.
1064 Works from wl-summary-mode and mime-view-mode - that is while viewing email.
1065 Depends on Wanderlust been loaded."
1066 (with-current-buffer (org-capture-get :original-buffer)
1067 (cond
1068 ((eq major-mode 'wl-summary-mode) (when (and (boundp 'wl-summary-buffer-elmo-folder)
1069 wl-summary-buffer-elmo-folder)
1070 (elmo-message-field
1071 wl-summary-buffer-elmo-folder
1072 (wl-summary-message-number)
1073 'from)))
1074 ((eq major-mode 'mime-view-mode) (std11-narrow-to-header)
1075 (prog1
1076 (std11-fetch-field "From")
1077 (widen))))))
1079 (defun org-contacts-wl-get-name-email ()
1080 "Get name and email address from Wanderlust email.
1081 See `org-contacts-wl-get-from-header-content' for limitations."
1082 (let ((from (org-contacts-wl-get-from-header-content)))
1083 (when from
1084 (list (wl-address-header-extract-realname from)
1085 (wl-address-header-extract-address from)))))
1087 (defun org-contacts-template-wl-name (&optional return-value)
1088 "Try to return the contact name for a template from wl.
1089 If not found, return RETURN-VALUE or something that would ask the
1090 user."
1091 (or (car (org-contacts-wl-get-name-email))
1092 return-value
1093 "%^{Name}"))
1095 (defun org-contacts-template-wl-email (&optional return-value)
1096 "Try to return the contact email for a template from Wanderlust.
1097 If not found return RETURN-VALUE or something that would ask the user."
1098 (or (cadr (org-contacts-wl-get-name-email))
1099 return-value
1100 (concat "%^{" org-contacts-email-property "}p")))
1102 (defun org-contacts-view-send-email (&optional ask)
1103 "Send email to the contact at point.
1104 If ASK is set, ask for the email address even if there's only one
1105 address."
1106 (interactive "P")
1107 (let ((marker (org-get-at-bol 'org-hd-marker)))
1108 (org-with-point-at marker
1109 (let ((emails (org-entry-get (point) org-contacts-email-property)))
1110 (if emails
1111 (let ((email-list (org-contacts-split-property emails)))
1112 (if (and (= (length email-list) 1) (not ask))
1113 (compose-mail (org-contacts-format-email
1114 (org-get-heading t) emails))
1115 (let ((email (completing-read "Send mail to which address: " email-list)))
1116 (setq email (org-contacts-strip-link email))
1117 (org-contacts-check-mail-address email)
1118 (compose-mail (org-contacts-format-email (org-get-heading t) email)))))
1119 (error (format "This contact has no mail address set (no %s property)"
1120 org-contacts-email-property)))))))
1122 (defun org-contacts-get-icon (&optional pom)
1123 "Get icon for contact at POM."
1124 (setq pom (or pom (point)))
1125 (catch 'icon
1126 ;; Use `org-contacts-icon-property'
1127 (let* ((link-matcher-regexp
1128 "\\[\\[\\([^]]*\\)\\]\\(\\[\\(.*\\)\\]\\)?\\]")
1129 (contacts-dir (file-name-directory (car (org-contacts-files))))
1130 (image-path
1131 (if-let ((avatar (org-entry-get pom org-contacts-icon-property)))
1132 (cond
1133 ;; [[file:dir/filename.png]]
1134 ((string-match-p "\\[\\[.*\\]\\]" avatar)
1135 ;; FIXME: What if avatar matches the above regexp but the
1136 ;; one below?
1137 (when (string-match link-matcher-regexp avatar)
1138 ;; FIXME: 5 seems to be the length of `file:' but I can't
1139 ;; see anything that guarantees that the submatch 1 starts
1140 ;; with `file:'.
1141 (expand-file-name (substring (match-string-no-properties 1 avatar) 5 nil)
1142 contacts-dir)))
1143 ;; "" (empty string)
1144 ((string-empty-p avatar) nil)
1145 (t (expand-file-name avatar contacts-dir))))))
1146 (when image-path
1147 (throw 'icon
1148 (if (featurep 'imagemagick)
1149 (create-image image-path 'imagemagick nil
1150 :height org-contacts-icon-size)
1151 (create-image image-path nil nil
1152 :height org-contacts-icon-size)))))
1153 ;; Next, try Gravatar
1154 (when org-contacts-icon-use-gravatar
1155 (defvar gravatar-size)
1156 (let* ((gravatar-size org-contacts-icon-size)
1157 (email-list (org-entry-get pom org-contacts-email-property))
1158 (gravatar
1159 (when email-list
1160 (cl-loop for email in (org-contacts-split-property email-list)
1161 for gravatar = (gravatar-retrieve-synchronously (org-contacts-strip-link email))
1162 if (and gravatar
1163 (not (eq gravatar 'error)))
1164 return gravatar))))
1165 (when gravatar (throw 'icon gravatar))))))
1167 (defun org-contacts-irc-buffer (&optional pom)
1168 "Get the IRC buffer associated with the entry at POM."
1169 (setq pom (or pom (point)))
1170 (let ((nick (org-entry-get pom org-contacts-nickname-property)))
1171 (when nick
1172 (let ((buffer (get-buffer nick)))
1173 (when buffer
1174 (with-current-buffer buffer
1175 (when (eq major-mode 'erc-mode)
1176 buffer)))))))
1178 (defun org-contacts-irc-number-of-unread-messages (&optional pom)
1179 "Return the number of unread messages for contact at POM."
1180 (when (boundp 'erc-modified-channels-alist)
1181 (let ((number (cadr (assoc (org-contacts-irc-buffer pom) erc-modified-channels-alist))))
1182 (if number
1183 (format (concat "%3d unread message" (if (> number 1) "s" " ") " ") number)
1184 (make-string 21 ? )))))
1186 (defun org-contacts-view-switch-to-irc-buffer ()
1187 "Switch to the IRC buffer of the current contact if it has one."
1188 (interactive)
1189 (let ((marker (org-get-at-bol 'org-hd-marker)))
1190 (org-with-point-at marker
1191 (switch-to-buffer-other-window (org-contacts-irc-buffer)))))
1193 (defun org-contacts-completing-read-nickname
1194 (prompt collection
1195 &optional predicate require-match initial-input
1196 hist def inherit-input-method)
1197 "Like `completing-read' but reads a property \"NICKNAME\" value in PROMPT.
1198 Return a org-contacts \"NICKNAME\" as property's value after completion."
1199 (let* ((org-contacts-candidates-propertized
1200 (mapcar
1201 (lambda (plist)
1202 (let* ((name (plist-get plist :name))
1203 (name-english (plist-get plist :name-english))
1204 (nick (plist-get plist :nick)))
1205 (unless (or (null nick) (string-empty-p nick))
1206 (propertize nick ; <- The `completing-read' select candidate inserted value.
1207 'display (concat
1208 (when name (propertize (format "%s " name) :face '(:foreground "ForestGreen")))
1209 (unless (or (null name-english) (string-empty-p name-english))
1210 (propertize (format "%s " name-english) :face '(:foreground "LightSeaGreen")))
1211 (unless (or (null nick) (string-empty-p nick))
1212 (propertize (format "(%s) " nick) :face '(:foreground "LightGray"))))))))
1213 org-contacts-all-contacts))
1214 ;; (contact-names (mapcar (lambda (plist) (plist-get plist :name)) org-contacts-all-contacts))
1215 (contact-nick (substring-no-properties
1216 (org-completing-read (or prompt "org-contacts NICKNAME: ")
1217 (append org-contacts-candidates-propertized collection
1218 (when (or (require 'erc nil t)
1219 (erc-server-buffer-live-p)
1220 (erc-server-process-alive)
1221 erc-server-processing-p)
1222 (org-contacts-erc-nicknames-list)))
1223 predicate require-match initial-input
1224 hist def inherit-input-method))))
1225 ;; Detect whether input contact is in `org-contacts' existing list.
1226 contact-nick))
1228 (defun org-contacts-erc-nicknames-list ()
1229 "Return all nicknames of all ERC buffers."
1230 (cl-loop for buffer in (erc-buffer-list)
1231 nconc (with-current-buffer buffer
1232 (cl-loop for user-entry
1233 in (mapcar #'car (erc-get-channel-user-list))
1234 collect (elt user-entry 1)))))
1236 (add-to-list 'org-property-set-functions-alist
1237 `(,org-contacts-nickname-property . org-contacts-completing-read-nickname))
1239 (defun org-contacts-vcard-escape (str)
1240 "Escape ; , and \n in STR for the VCard format."
1241 ;; Thanks to this library for the regexp:
1242 ;; https://www.emacswiki.org/cgi-bin/wiki/bbdb-vcard-export.el
1243 (when str
1244 (replace-regexp-in-string
1245 "\n" "\\\\n"
1246 (replace-regexp-in-string "\\(;\\|,\\|\\\\\\)" "\\\\\\1" str))))
1248 (defun org-contacts-vcard-encode-name (name)
1249 "Try to encode NAME as VCard's N property.
1250 The N property expects
1252 FamilyName;GivenName;AdditionalNames;Prefix;Postfix.
1254 Org-contacts does not specify how to encode the name. So we try
1255 to do our best."
1256 (concat (replace-regexp-in-string "\\(\\w+\\) \\(.*\\)" "\\2;\\1" name) ";;;"))
1258 (defun org-contacts-vcard-format (contact)
1259 "Formats CONTACT in VCard 3.0 format."
1260 (let* ((properties (nth 2 contact))
1261 (name (org-contacts-vcard-escape (car contact)))
1262 (n (org-contacts-vcard-encode-name name))
1263 (email (cdr (assoc-string org-contacts-email-property properties)))
1264 (tel (cdr (assoc-string org-contacts-tel-property properties)))
1265 (ignore-list (cdr (assoc-string org-contacts-ignore-property properties)))
1266 (ignore-list (when ignore-list
1267 (org-contacts-split-property ignore-list)))
1268 (note (cdr (assoc-string org-contacts-note-property properties)))
1269 (bday (org-contacts-vcard-escape (cdr (assoc-string org-contacts-birthday-property properties))))
1270 (addr (cdr (assoc-string org-contacts-address-property properties)))
1271 (nick (org-contacts-vcard-escape (cdr (assoc-string org-contacts-nickname-property properties))))
1272 (head (format "BEGIN:VCARD\nVERSION:3.0\nN:%s\nFN:%s\n" n name))
1273 emails-list result phones-list)
1274 (concat
1275 head
1276 (when email
1277 (progn
1278 (setq emails-list (org-contacts-remove-ignored-property-values
1279 ignore-list (org-contacts-split-property email)))
1280 (setq result "")
1281 (while emails-list
1282 (setq result (concat result "EMAIL:" (org-contacts-strip-link (car emails-list)) "\n"))
1283 (setq emails-list (cdr emails-list)))
1284 result))
1285 (when addr
1286 (format "ADR:;;%s\n" (replace-regexp-in-string "\\, ?" ";" addr)))
1287 (when tel
1288 (progn
1289 (setq phones-list (org-contacts-remove-ignored-property-values
1290 ignore-list (org-contacts-split-property tel)))
1291 (setq result "")
1292 (while phones-list
1293 (setq result (concat result "TEL:" (org-contacts-strip-link
1294 (org-link-unescape (car phones-list))) "\n"))
1295 (setq phones-list (cdr phones-list)))
1296 result))
1297 (when bday
1298 (let ((cal-bday (calendar-gregorian-from-absolute (org-time-string-to-absolute bday))))
1299 (format "BDAY:%04d-%02d-%02d\n"
1300 (calendar-extract-year cal-bday)
1301 (calendar-extract-month cal-bday)
1302 (calendar-extract-day cal-bday))))
1303 (when nick (format "NICKNAME:%s\n" nick))
1304 (when note (format "NOTE:%s\n" note))
1305 "END:VCARD\n\n")))
1307 (defun org-contacts-export-as-vcard (&optional name file to-buffer)
1308 "Export org contacts to V-Card 3.0.
1310 By default, all contacts are exported to `org-contacts-vcard-file'.
1312 When NAME is \\[universal-argument], prompts for a contact name.
1314 When NAME is \\[universal-argument] \\[universal-argument],
1315 prompts for a contact name and a file name where to export.
1317 When NAME is \\[universal-argument] \\[universal-argument]
1318 \\[universal-argument], prompts for a contact name and a buffer where to export.
1320 If the function is not called interactively, all parameters are
1321 passed to `org-contacts-export-as-vcard-internal'."
1322 (interactive "P")
1323 (when (called-interactively-p 'any)
1324 (cl-psetf name
1325 (when name
1326 (read-string "Contact name: "
1327 (nth 0 (org-contacts-at-point))))
1328 file
1329 (when (equal name '(16))
1330 (read-file-name "File: " nil org-contacts-vcard-file))
1331 to-buffer
1332 (when (equal name '(64))
1333 (read-buffer "Buffer: "))))
1334 (org-contacts-export-as-vcard-internal name file to-buffer))
1336 (defun org-contacts-export-as-vcard-internal (&optional name file to-buffer)
1337 "Export all contacts matching NAME as VCard 3.0.
1338 If TO-BUFFER is nil, the content is written to FILE or
1339 `org-contacts-vcard-file'. If TO-BUFFER is non-nil, the buffer
1340 is created and the VCard is written into that buffer."
1341 (let* ((filename (or file org-contacts-vcard-file))
1342 (buffer (if to-buffer
1343 (get-buffer-create to-buffer)
1344 (find-file-noselect filename))))
1345 (message "Exporting...")
1346 (set-buffer buffer)
1347 (let ((inhibit-read-only t)) (erase-buffer))
1348 (fundamental-mode)
1349 (when (fboundp 'set-buffer-file-coding-system)
1350 (set-buffer-file-coding-system coding-system-for-write))
1351 (cl-loop for contact in (org-contacts-filter name)
1352 do (insert (org-contacts-vcard-format contact)))
1353 (if to-buffer
1354 (current-buffer)
1355 (progn (save-buffer) (kill-buffer)))))
1357 (defun org-contacts-show-map (&optional name)
1358 "Show contacts on a map.
1359 Requires google-maps-el."
1360 (interactive)
1361 (unless (fboundp 'google-maps-static-show)
1362 (error "`org-contacts-show-map' requires `google-maps-el'"))
1363 (google-maps-static-show
1364 :markers
1365 (cl-loop
1366 for contact in (org-contacts-filter name)
1367 for addr = (cdr (assoc-string org-contacts-address-property (nth 2 contact)))
1368 if addr
1369 collect (cons (list addr) (list :label (string-to-char (car contact)))))))
1371 (defun org-contacts-strip-link (link)
1372 "Remove brackets, description, link type and colon from an org
1373 link string and return the pure link target."
1374 (let (startpos colonpos endpos)
1375 (setq startpos (string-match (regexp-opt '("[[tel:" "[[mailto:")) link))
1376 (if startpos
1377 (progn
1378 (setq colonpos (string-match ":" link))
1379 (setq endpos (string-match "\\]" link))
1380 (if endpos (substring link (1+ colonpos) endpos) link))
1381 (progn
1382 (setq startpos (string-match "mailto:" link))
1383 (setq colonpos (string-match ":" link))
1384 (if startpos (substring link (1+ colonpos)) link)))))
1386 ;; Add the link type supported by org-contacts-strip-link
1387 ;; so everything is in order for its use in Org files
1388 (if (fboundp 'org-link-set-parameters)
1389 (org-link-set-parameters "tel")
1390 (if (fboundp 'org-add-link-type)
1391 (org-add-link-type "tel")))
1393 (defun org-contacts-split-property (string &optional separators omit-nulls)
1394 "Custom version of `split-string'.
1395 Split a property STRING into sub-strings bounded by matches
1396 for SEPARATORS but keep Org links intact.
1398 The beginning and end of STRING, and each match for SEPARATORS, are
1399 splitting points. The substrings matching SEPARATORS are removed, and
1400 the substrings between the splitting points are collected as a list,
1401 which is returned.
1403 If SEPARATORS is non-nil, it should be a regular expression
1404 matching text which separates, but is not part of, the
1405 substrings. If nil it defaults to `org-contacts-property-values-separators',
1406 normally \"[,; \\f\\t\\n\\r\\v]+\", and OMIT-NULLS is forced to t.
1408 If OMIT-NULLS is t, zero-length substrings are omitted from the list so
1409 that for the default value of SEPARATORS leading and trailing whitespace
1410 are effectively trimmed. If nil, all zero-length substrings are retained."
1411 (let* ((omit-nulls (if separators omit-nulls t))
1412 (rexp (or separators org-contacts-property-values-separators))
1413 (inputlist (split-string string rexp omit-nulls))
1414 (linkstring "")
1415 (bufferstring "")
1416 (proplist (list "")))
1417 (while inputlist
1418 (setq bufferstring (pop inputlist))
1419 (if (string-match "\\[\\[" bufferstring)
1420 (progn
1421 (setq linkstring (concat bufferstring " "))
1422 (while (not (string-match "\\]\\]" bufferstring))
1423 (setq bufferstring (pop inputlist))
1424 (setq linkstring (concat linkstring bufferstring " ")))
1425 (setq proplist (cons (org-trim linkstring) proplist)))
1426 (setq proplist (cons bufferstring proplist))))
1427 (cdr (reverse proplist))))
1429 ;;;###autoload
1430 ;; Add an Org link type `org-contact:' for easy jump to or searching org-contacts headline.
1431 ;; link spec: [[org-contact:query][desc]]
1432 (if (fboundp 'org-link-set-parameters)
1433 (org-link-set-parameters "org-contact"
1434 :follow #'org-contacts-link-open
1435 :complete #'org-contacts-link-complete
1436 :store #'org-contacts-link-store
1437 :face 'org-contacts-link-face)
1438 (if (fboundp 'org-add-link-type)
1439 (org-add-link-type "org-contact" 'org-contacts-link-open)))
1441 ;;;###autoload
1442 (defun org-contacts-link-store ()
1443 "Store the contact in `org-contacts-files' with a link."
1444 (when (and (eq major-mode 'org-mode)
1445 (member (buffer-file-name)
1446 (mapcar #'expand-file-name (org-contacts-files)))
1447 (not (org-before-first-heading-p))
1448 (let ((element (org-element-at-point)))
1449 (funcall (cdr (org-make-tags-matcher org-contacts-matcher))
1450 (org-element-property :todo-keyword element)
1451 (org-get-tags element)
1452 (org-element-property :level element))))
1453 (if (bound-and-true-p org-id-link-to-org-use-id)
1454 (org-id-store-link)
1455 (let ((headline-str (substring-no-properties (org-get-heading t t t t))))
1456 (org-link-store-props
1457 :type "org-contact"
1458 :link headline-str
1459 :description headline-str)
1460 (let ((link (concat "org-contact:" headline-str)))
1461 (org-link-add-props :link link :description headline-str)
1462 link)))))
1464 (defvar org-contacts-all-contacts nil
1465 "A data store variable of all contacts.")
1467 (defun org-contacts--all-contacts ()
1468 "Return a list of all contacts in `org-contacts-files'.
1469 Each element has the form (NAME . (FILE . POSITION))."
1470 (car (mapcar
1471 (lambda (file)
1472 (unless (buffer-live-p (get-buffer (file-name-nondirectory file)))
1473 (with-current-buffer (find-file-noselect file)
1474 (org-map-entries
1475 (lambda ()
1476 (let* ((name (substring-no-properties (org-get-heading t t t t)))
1477 (file (buffer-file-name))
1478 (position (point))
1479 ;; extract properties Org entry headline at `position' as data API for better contacts searching.
1480 (entry-properties (org-entry-properties position 'standard))
1481 (property-name-chinese (cdr (assoc (upcase "NAME(Chinese)") entry-properties)))
1482 (property-name-english (cdr (assoc (upcase "NAME(English)") entry-properties)))
1483 (property-nick (cdr (assoc "NICK" entry-properties)))
1484 (property-email (cdr (assoc "EMAIL" entry-properties)))
1485 ;; (property-mobile (cdr (assoc "MOBILE" entry-properties)))
1486 (property-wechat (cdr (assoc (upcase "WeChat") entry-properties)))
1487 (property-qq (cdr (assoc "QQ" entry-properties))))
1488 (list :name name :file file :position position
1489 :name-chinese property-name-chinese
1490 :name-english property-name-english
1491 :nick property-nick
1492 :email property-email
1493 :mobile property-email
1494 :wechat property-wechat
1495 :qq property-qq)))))))
1496 (org-contacts-files))))
1498 (setq org-contacts-all-contacts
1499 (with-memoization org-contacts-all-contacts
1500 (org-contacts--all-contacts)))
1502 ;;;###autoload
1503 (defun org-contacts-link-open (query)
1504 "Open contacts: link type with jumping or searching."
1505 (let* ((f (car (org-contacts-files)))
1506 (fname (file-name-nondirectory f))
1507 (buf (if (buffer-live-p (get-buffer fname)) (get-buffer fname) (find-file f))))
1508 (cond
1509 ;; /query/ format searching
1510 ((string-match "/.*/" query)
1511 (with-current-buffer buf
1512 (string-match "/\\(.*\\)/" query)
1513 (occur (match-string 1 query))))
1515 ;; jump to exact contact headline directly
1517 (with-current-buffer buf
1518 (if-let ((position (org-find-exact-headline-in-buffer query)))
1519 (goto-char (marker-position position))
1520 (user-error "[org-contacts] Can't find <%s> in your `org-contacts-files'." query)))
1521 (display-buffer buf '(display-buffer-below-selected))
1523 ;; FIXME:
1524 ;; (let* ((contact-entry (map-filter
1525 ;; (lambda (contact-plist)
1526 ;; (if (string-equal (plist-get contact-plist :name) query)
1527 ;; contact-plist))
1528 ;; org-contacts-all-contacts))
1529 ;; (contact-name (plist-get contact-entry :name))
1530 ;; (file (plist-get contact-entry :file))
1531 ;; (position (plist-get contact-entry :position))
1532 ;; (buf (get-buffer (file-name-nondirectory file))))
1533 ;; (with-current-buffer buf (goto-char position))
1534 ;; (display-buffer buf '(display-buffer-below-selected)))
1535 ))))
1537 ;;;###autoload
1538 (defun org-contacts-link-complete (&optional _arg)
1539 "Create a org-contacts link using completion."
1540 (let ((name (completing-read "org-contacts NAME: "
1541 (mapcar
1542 (lambda (plist) (plist-get plist :name))
1543 org-contacts-all-contacts))))
1544 (concat "org-contact:" name)))
1546 (defun org-contacts-link-face (path)
1547 "Different face color for different org-contacts link query."
1548 (cond
1549 ((string-match "/.*/" path)
1550 '(:background "sky blue" :overline t :slant 'italic))
1551 (t '(:inherit org-link))))
1554 ;;; org-mode link "mailto:" email completion.
1555 (if (fboundp 'org-link-set-parameters)
1556 (org-link-set-parameters "mailto" :complete #'org-contacts-mailto-link-completion)
1557 (if (fboundp 'org-add-link-type)
1558 (org-add-link-type "mailto")))
1560 (defvar org-contacts-emails-list nil
1561 "A list variable of all org-contacts emails.")
1563 (defun org-contacts-mailto-link--get-all-emails ()
1564 "Retrieve all org-contacts EMAIL property values."
1565 (setq org-contacts-emails-list
1566 (mapcar
1567 (lambda (contact)
1568 (let* ((org-contacts-buffer (find-file-noselect (car (org-contacts-files))))
1569 (name (plist-get contact :name))
1570 (position (plist-get contact :position))
1571 (email (save-excursion
1572 (with-current-buffer org-contacts-buffer
1573 (goto-char position)
1574 ;; (symbol-name (org-property-or-variable-value 'EMAIL))
1575 (when-let ((pvalue (org-entry-get (point) "EMAIL")))
1576 ;; handle `mailto:' link. e.g. "[[mailto:yantar92@posteo.net]]", "[[mailto:yantar92@posteo.net][yantar92@posteo.net]]"
1577 ;; Reference the testing file `test-org-contacts.el'.
1578 (if (string-match
1579 "\\[\\[mailto:\\(.*\\)\\]\\(\\[.*\\]\\)\\]\\(,\\ *\\[\\[mailto:\\(.*\\)\\]\\(\\[.*\\]\\)\\]\\)"
1580 pvalue)
1581 (match-string 1 pvalue)
1582 pvalue))))))
1583 (ignore name)
1584 ;; (cons name email)
1585 email))
1586 org-contacts-all-contacts))
1587 ;; clean nil and empty string "" from result.
1588 (delete ""
1589 (delete nil org-contacts-emails-list)))
1591 (defun org-contacts-mailto-link-completion (&optional _arg)
1592 "Org mode link `mailto:' completion with org-contacts emails."
1593 (let ((email (completing-read "org-contacts EMAIL: "
1594 (org-contacts-mailto-link--get-all-emails))))
1595 (concat "mailto:" email)))
1597 (provide 'org-contacts)
1599 ;;; org-contacts.el ends here