wrap long line comments
[org-contacts.git] / org-contacts.el
blob6778c94089e08a4f0c40b09bb792e67ca2e12340
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 "29.1") (org "9.7"))
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 ignored when completing or exporting to vCard."
122 :type 'string)
124 (defcustom org-contacts-birthday-format "Birthday: %l (%Y)"
125 "Format of the anniversary agenda entry.
126 The following replacements are available:
128 %h - Heading name
129 %l - Link to the heading
130 %y - Number of year
131 %Y - Number of year (ordinal)"
132 :type 'string)
134 (defcustom org-contacts-last-read-mail-property "LAST_READ_MAIL"
135 "Name of the property for contact last read email link storage."
136 :type 'string)
138 (defcustom org-contacts-icon-property "ICON"
139 "Name of the property for contact icon."
140 :type 'string)
142 (defcustom org-contacts-nickname-property "NICKNAME"
143 "Name of the property for IRC nickname match."
144 :type 'string)
146 (defcustom org-contacts-icon-size 64
147 "Size of the contacts icons."
148 :type 'string)
150 (defcustom org-contacts-icon-use-gravatar (fboundp 'gravatar-retrieve)
151 "Whether use Gravatar to fetch contact icons."
152 :type 'boolean)
154 (defcustom org-contacts-completion-ignore-case t
155 "Ignore case when completing contacts."
156 :type 'boolean)
158 (defcustom org-contacts-group-prefix "+"
159 "Group prefix."
160 :type 'string)
162 (defcustom org-contacts-tags-props-prefix "#"
163 "Tags and properties prefix."
164 :type 'string)
166 (defcustom org-contacts-matcher
167 (mapconcat #'identity
168 (mapcar (lambda (x) (concat x "<>\"\""))
169 (list org-contacts-email-property
170 org-contacts-alias-property
171 org-contacts-tel-property
172 org-contacts-address-property
173 org-contacts-birthday-property))
174 "|")
175 "Matching rule for finding heading that are contacts.
176 This can be a tag name, or a property check."
177 :type 'string)
179 (defcustom org-contacts-email-link-description-format "%s (%d)"
180 "Format used to store links to email.
181 This overrides `org-email-link-description-format' if set."
182 :type 'string)
184 (defcustom org-contacts-vcard-file "contacts.vcf"
185 "Default file for vcard export."
186 :type 'file)
188 (defcustom org-contacts-completion-enabled-mode-list '(org-mode message-mode mu4e-compose-mode)
189 "Enable or not the completion in `message-mode' with `org-contacts'."
190 :type '(repeat symbol))
192 (defcustom org-contacts-complete-functions
193 '(org-contacts-complete-group org-contacts-complete-tags-props org-contacts-complete-name)
194 "List of functions used to complete contacts in `message-mode'."
195 :type 'hook)
197 ;; Decalre external functions and variables
198 (declare-function org-reverse-string "org")
199 (declare-function diary-ordinal-suffix "ext:diary-lib")
200 (declare-function wl-summary-message-number "ext:wl-summary")
201 (declare-function wl-address-header-extract-address "ext:wl-address")
202 (declare-function wl-address-header-extract-realname "ext:wl-address")
203 (declare-function erc-buffer-list "ext:erc")
204 (declare-function erc-get-channel-user-list "ext:erc")
205 (declare-function google-maps-static-show "ext:google-maps-static")
206 (declare-function elmo-message-field "ext:elmo-pipe")
207 (declare-function std11-narrow-to-header "ext:std11")
208 (declare-function std11-fetch-field "ext:std11")
210 ;;;###autoload
211 (defun org-contacts-files ()
212 "Return list of Org files to use for contact management."
213 (if org-contacts-files
214 org-contacts-files
215 (message "[org-contacts] ERROR: Your custom variable `org-contacts-files' is nil.
216 Revert to `org-agenda-files' now.")
217 (org-agenda-files t 'ifmode)))
219 (defconst org-contacts-property-values-separators "[,; \f\t\n\r\v]+"
220 "The default value of separators for `org-contacts-split-property'.
222 A regexp matching strings of whitespace, `,' and `;'.")
224 (defvar org-contacts-keymap
225 (let ((map (make-sparse-keymap)))
226 (define-key map "M" #'org-contacts-view-send-email)
227 (define-key map "i" #'org-contacts-view-switch-to-irc-buffer)
228 map)
229 "The keymap used in `org-contacts' result list.")
231 (defvar org-contacts-db nil
232 "Org Contacts database.")
234 (defvar org-contacts-last-update nil
235 "Last time the Org Contacts database has been updated.")
237 (defvar org-contacts-all-contacts nil
238 "A data store variable of all contacts.")
240 (defun org-contacts-all-contacts ()
241 "Return the data of all contacts."
242 (setq org-contacts-all-contacts
243 (with-memoization org-contacts-all-contacts
244 (org-contacts--all-contacts))))
246 (defun org-contacts-db-need-update-p ()
247 "Determine whether `org-contacts-db' needs to be refreshed."
248 (or (null org-contacts-last-update)
249 (cl-find-if (lambda (file)
250 (or (time-less-p org-contacts-last-update
251 (elt (file-attributes file) 5))))
252 (org-contacts-files))
253 (org-contacts-db-has-dead-markers-p org-contacts-db)))
255 (defun org-contacts-db-has-dead-markers-p (db)
256 "Return t if at least one dead marker is found in DB.
257 A dead marker in this case is a marker pointing to dead or no
258 buffer."
259 ;; Scan contacts list looking for dead markers, and return t at first found.
260 (catch 'dead-marker-found
261 (while db
262 (unless (marker-buffer (nth 1 (car db)))
263 (throw 'dead-marker-found t))
264 (setq db (cdr db)))
265 nil))
267 (defun org-contacts-db ()
268 "Return the latest Org Contacts Database."
269 (let* ((org--matcher-tags-todo-only nil)
270 (contacts-matcher (cdr (org-make-tags-matcher org-contacts-matcher)))
271 result org-agenda-skip-function org-agenda-skip-function-global)
272 (when (org-contacts-db-need-update-p)
273 (let ((progress-reporter
274 (make-progress-reporter "Updating Org Contacts Database..." 0 (length (org-contacts-files))))
275 (i 0))
276 (dolist (file (org-contacts-files))
277 (if (catch 'nextfile
278 ;; if file doesn't exist and the user agrees to removing it
279 ;; from org-agendas-list, 'nextfile is thrown. Catch it here
280 ;; and skip processing the file.
282 ;; TODO: suppose that the user has set an org-contacts-files
283 ;; list that contains an element that doesn't exist in the
284 ;; file system: in that case, the org-agenda-files list could
285 ;; be updated (and saved to the customizations of the user) if
286 ;; it contained the same file even though the org-agenda-files
287 ;; list wasn't actually used. I don't think it is normal that
288 ;; org-contacts updates org-agenda-files in this case, but
289 ;; short of duplicating org-check-agenda-files and
290 ;; org-remove-files, I don't know how to avoid it.
292 ;; A side effect of the TODO is that the faulty
293 ;; org-contacts-files list never gets updated and thus the
294 ;; user is always queried about the missing files when
295 ;; org-contacts-db-need-update-p returns true.
296 (org-check-agenda-file file))
297 (message "Skipped %s removed from org-agenda-files list."
298 (abbreviate-file-name file))
299 (with-current-buffer (org-get-agenda-file-buffer file)
300 (unless (eq major-mode 'org-mode)
301 (error "File %s is not in `org-mode'" file))
302 (setf result
303 (append result
304 (org-scan-tags 'org-contacts-at-point
305 contacts-matcher
306 org--matcher-tags-todo-only)))))
307 (progress-reporter-update progress-reporter (setq i (1+ i))))
308 (setf org-contacts-db result
309 org-contacts-last-update (current-time))
310 (progress-reporter-done progress-reporter)))
311 org-contacts-db))
313 (defun org-contacts-search-contact (name)
314 "Search contact NAME in cached database and return org element POM."
315 (let (epom)
316 (dolist (contact (org-contacts-all-contacts) epom)
317 (when (string-equal (plist-get contact :name) name)
318 (with-current-buffer (find-file-noselect (expand-file-name (car org-contacts-files)))
319 (or (save-excursion
320 (goto-char (plist-get contact :position))
321 (setq epom (org-element-context)))
322 (progn
323 (org-goto-marker-or-bmk (org-find-exact-headline-in-buffer name))
324 (setq epom (org-element-context)))))))
325 epom))
327 ;;; TEST:
328 ;; (org-contacts-search-contact "stardiviner")
329 ;; (org-element-property :title (org-contacts-search-contact "stardiviner"))
331 (defun org-contacts-at-point (&optional pom)
332 "Return the contacts at point or marker POM or current position."
333 (setq pom (or pom (point)))
334 (org-with-point-at pom
335 (list (org-get-heading t) (set-marker (make-marker) pom) (org-entry-properties pom 'all))))
337 (defun org-contacts-filter (&optional name-match tags-match prop-match)
338 "Search for a contact matching any of NAME-MATCH, TAGS-MATCH, PROP-MATCH.
339 If all match values are nil, return all contacts.
341 The optional PROP-MATCH argument is a single (PROP . VALUE) cons
342 cell corresponding to the contact properties."
343 (if (and (null name-match)
344 (null prop-match)
345 (null tags-match))
346 (org-contacts-db)
347 (cl-loop for contact in (org-contacts-db)
348 if (or
349 (and name-match
350 (string-match-p name-match
351 (cl-first contact)))
352 (and prop-match
353 (cl-find-if (lambda (prop)
354 (and (string= (car prop-match) (car prop))
355 (string-match-p (cdr prop-match) (cdr prop))))
356 (caddr contact)))
357 (and tags-match
358 (cl-find-if (lambda (tag)
359 (string-match-p tags-match tag))
360 (org-split-string
361 (or (cdr (assoc-string "ALLTAGS" (caddr contact))) "") ":"))))
362 collect contact)))
364 (defun org-contacts-try-completion-prefix (to-match collection &optional predicate)
365 "Custom implementation of `try-completion'.
366 This version works only with list and alist and it looks at all
367 prefixes rather than just the beginning of the string."
368 (cl-loop with regexp = (concat "\\b" (regexp-quote to-match))
369 with ret = nil
370 with ret-start = nil
371 with ret-end = nil
373 for el in collection
374 for string = (if (listp el) (car el) el)
376 for start = (when (or (null predicate) (funcall predicate string))
377 (string-match regexp string))
379 if start
380 do (let ((end (match-end 0))
381 (len (length string)))
382 (if (= end len)
383 (cl-return t)
384 (cl-destructuring-bind (string start end)
385 (if (null ret)
386 (cl-values string start end)
387 (org-contacts-common-substring
388 ret ret-start ret-end
389 string start end))
390 (setf ret string
391 ret-start start
392 ret-end end))))
394 finally (cl-return
395 (replace-regexp-in-string "\\`[ \t\n]*" "" ret))))
397 (defun org-contacts-compare-strings (s1 start1 end1 s2 start2 end2 &optional ignore-case)
398 "Compare the contents of two strings, using `compare-strings'.
400 This function works like `compare-strings' excepted that it
401 returns a cons.
402 - The CAR is the number of characters that match at the beginning.
403 - The CDR is T is the two strings are the same and NIL otherwise."
404 (let ((ret (compare-strings s1 start1 end1 s2 start2 end2 ignore-case)))
405 (if (eq ret t)
406 (cons (or end1 (length s1)) t)
407 (cons (1- (abs ret)) nil))))
409 (defun org-contacts-common-substring (s1 start1 end1 s2 start2 end2)
410 "Extract the common substring between S1 and S2.
412 This function extracts the common substring between S1 and S2 and
413 adjust the part that remains common.
415 START1 and END1 delimit the part in S1 that we know is common
416 between the two strings. This applies to START2 and END2 for S2.
418 This function returns a list whose contains:
419 - The common substring found.
420 - The new value of the start of the known inner substring.
421 - The new value of the end of the known inner substring."
422 ;; Given two strings:
423 ;; s1: "foo bar baz"
424 ;; s2: "fooo bar baz"
425 ;; and the inner substring is "bar"
426 ;; then: start1 = 4, end1 = 6, start2 = 5, end2 = 7
428 ;; To find the common substring we will compare two substrings:
429 ;; " oof" and " ooof" to find the beginning of the common substring.
430 ;; " baz" and " baz" to find the end of the common substring.
431 (let* ((len1 (length s1))
432 (start1 (or start1 0))
433 (end1 (or end1 len1))
435 (len2 (length s2))
436 (start2 (or start2 0))
437 (end2 (or end2 len2))
439 (new-start (car (org-contacts-compare-strings
440 (substring (org-reverse-string s1) (- len1 start1)) nil nil
441 (substring (org-reverse-string s2) (- len2 start2)) nil nil)))
443 (new-end (+ end1 (car (org-contacts-compare-strings
444 (substring s1 end1) nil nil
445 (substring s2 end2) nil nil)))))
446 (list (substring s1 (- start1 new-start) new-end)
447 new-start
448 (+ new-start (- end1 start1)))))
450 (defun org-contacts-all-completions-prefix (to-match collection &optional predicate)
451 "Custom version of `all-completions'.
452 This version works only with list and alist and it looks at all
453 prefixes rather than just the beginning of the string."
454 (cl-loop with regexp = (concat "\\b" (regexp-quote to-match))
455 for el in collection
456 for string = (if (listp el) (car el) el)
457 for match? = (when (and (or (null predicate) (funcall predicate string)))
458 (string-match regexp string))
459 if match?
460 collect (progn
461 (let ((end (match-end 0)))
462 (org-no-properties string)
463 (when (< end (length string))
464 ;; Here we add a text property that will be used
465 ;; later to highlight the character right after
466 ;; the common part between each addresses.
467 ;; See `org-contacts-display-sort-function'.
468 (put-text-property end (1+ end) 'org-contacts-prefix 't string)))
469 string)))
471 (defun org-contacts-make-collection-prefix (collection)
472 "Make a collection function from COLLECTION which will match on prefixes."
473 (let ((collection collection))
474 (lambda (string predicate flag)
475 (cond ((eq flag nil)
476 (org-contacts-try-completion-prefix string collection predicate))
477 ((eq flag t)
478 ;; `org-contacts-all-completions-prefix' has already been
479 ;; used to compute `all-completions'.
480 collection)
481 ((eq flag 'lambda)
482 (org-contacts-test-completion-prefix string collection predicate))
483 ((and (listp flag) (eq (car flag) 'boundaries))
484 (org-contacts-boundaries-prefix string collection predicate (cdr flag)))
485 ((eq flag 'metadata)
486 (org-contacts-metadata-prefix))
487 (t nil ; operation unsupported
488 )))))
490 (defun org-contacts-display-sort-function (completions)
491 "Sort function for contacts COMPLETIONS."
492 (mapcar (lambda (string)
493 (cl-loop with len = (1- (length string))
494 for i upfrom 0 to len
495 if (memq 'org-contacts-prefix
496 (text-properties-at i string))
497 do (set-text-properties
498 i (1+ i)
499 (list 'font-lock-face
500 (if (char-equal (aref string i)
501 (string-to-char " "))
502 ;; Spaces can't be bold.
503 'underline
504 'bold)) string)
505 else
506 do (set-text-properties i (1+ i) nil string)
507 finally (cl-return string)))
508 completions))
510 (defun org-contacts-test-completion-prefix (string collection predicate)
511 (cl-find-if (lambda (el)
512 (and (or (null predicate) (funcall predicate el))
513 (string= string el)))
514 collection))
516 (defun org-contacts-boundaries-prefix (string collection predicate suffix)
517 (cl-list* 'boundaries (completion-boundaries string collection predicate suffix)))
519 (defun org-contacts-metadata-prefix (&rest _)
520 '(metadata .
521 ((cycle-sort-function . org-contacts-display-sort-function)
522 (display-sort-function . org-contacts-display-sort-function))))
524 (defun org-contacts-complete-group (string)
525 "Complete STRING as start from a group.
527 A group FOO is composed of contacts with the tag FOO."
528 (let* ((completion-ignore-case org-contacts-completion-ignore-case)
529 (group-completion-p (string-match-p
530 (concat "^" org-contacts-group-prefix) string)))
531 (when group-completion-p
532 (let ((completion-list
533 (all-completions
534 string
535 (mapcar (lambda (group)
536 (propertize (concat org-contacts-group-prefix group)
537 'org-contacts-group group))
538 (org-uniquify
539 (cl-loop for contact in (org-contacts-filter)
540 nconc (org-split-string
541 (or (cdr (assoc-string "ALLTAGS" (caddr contact))) "") ":")))))))
543 (if (= (length completion-list) 1)
544 ;; We've found the correct group, returns the address
545 (let ((tag (get-text-property 0 'org-contacts-group
546 (car completion-list))))
547 (mapconcat #'identity
548 (cl-loop for contact in (org-contacts-filter
550 tag)
551 ;; The contact name is always the car of the assoc-list
552 ;; returned by `org-contacts-filter'.
553 for contact-name = (car contact)
554 ;; Grab the first email of the contact
555 for email = (org-contacts-strip-link
556 (or (car (org-contacts-split-property
558 (cdr (assoc-string org-contacts-email-property
559 (cl-caddr contact)))
560 ""))) ""))
561 ;; If the user has an email address, append USER <EMAIL>.
562 if email collect (org-contacts-format-email contact-name email))
563 ", "))
564 ;; We haven't found the correct group
565 (completion-table-case-fold completion-list
566 (not org-contacts-completion-ignore-case)))))))
568 (defun org-contacts-complete-tags-props (string)
569 "Insert emails that match the tags expression beginning with STRING.
571 For example: FOO-BAR will match entries tagged with FOO but not with BAR.
572 See (org) Matching tags and properties for a complete description."
573 (let* ((completion-ignore-case org-contacts-completion-ignore-case)
574 (completion-p (string-match-p
575 (concat "^" org-contacts-tags-props-prefix) string)))
576 (when completion-p
577 (let ((result
578 (mapconcat
579 #'identity
580 (cl-loop for contact in (org-contacts-db)
581 for contact-name = (car contact)
582 for email = (org-contacts-strip-link
583 (or (car (org-contacts-split-property
585 (cdr (assoc-string org-contacts-email-property
586 (cl-caddr contact)))
587 "")))
588 ""))
589 ;; for tags = (cdr (assoc "TAGS" (nth 2 contact)))
590 ;; for tags-list = (if tags
591 ;; (split-string (substring (cdr (assoc "TAGS" (nth 2 contact))) 1 -1) ":")
592 ;; '())
593 for marker = (nth 1 contact)
594 if (with-current-buffer (marker-buffer marker)
595 (save-excursion
596 (goto-char marker)
597 ;; FIXME: AFAIK, `org-make-tags-matcher' returns
598 ;; a cons whose cdr is a function, so why do we
599 ;; pass it to `eval'?
600 (eval (cdr (org-make-tags-matcher (cl-subseq string 1)))
601 t)))
602 collect (org-contacts-format-email contact-name email))
603 ",")))
604 (when (not (string= "" result))
605 result)))))
607 (defun org-contacts-remove-ignored-property-values (ignore-list list)
608 "Remove all IGNORE-LIST's elements from LIST and you can use regular expressions in the ignore list."
609 (cl-remove-if (lambda (el)
610 (cl-find-if (lambda (x)
611 (string-match-p x el))
612 ignore-list))
613 list))
615 (defun org-contacts-complete-name (string)
616 "Complete STRING at start with a user name and email."
617 (let* ((completion-ignore-case org-contacts-completion-ignore-case)
618 (completion-list
619 (cl-loop for contact in (org-contacts-filter)
620 ;; The contact name is always the car of the assoc-list
621 ;; returned by `org-contacts-filter'.
622 for contact-name = (car contact)
624 ;; Build the list of the email addresses which has
625 ;; been expired
626 for ignore-list = (org-contacts-split-property
627 (or (cdr (assoc-string org-contacts-ignore-property
628 (nth 2 contact))) ""))
629 ;; Build the list of the user email addresses.
630 for email-list = (org-contacts-remove-ignored-property-values
631 ignore-list
632 (org-contacts-split-property
633 (or (cdr (assoc-string org-contacts-email-property
634 (nth 2 contact))) "")))
635 ;; If the user has email addresses…
636 if email-list
637 ;; … append a list of USER <EMAIL>.
638 nconc (cl-loop for email in email-list
639 collect (org-contacts-format-email
640 contact-name (org-contacts-strip-link email)))))
641 (completion-list (org-contacts-all-completions-prefix
642 string
643 (org-uniquify completion-list))))
644 (when completion-list
645 (org-contacts-make-collection-prefix completion-list))))
647 (defun org-contacts-message-complete-function ()
648 "Function used in `completion-at-point-functions' in `message-mode'."
649 (let ((mail-abbrev-mode-regexp
650 "^\\(Resent-To\\|To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\|Disposition-Notification-To\\|Return-Receipt-To\\):"))
651 (when (mail-abbrev-in-expansion-header-p)
652 (let
653 ((beg
654 (save-excursion
655 (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
656 (goto-char (match-end 0))
657 (point)))
658 (end (point)))
659 (list beg
661 (completion-table-dynamic
662 (lambda (string)
663 (run-hook-with-args-until-success
664 'org-contacts-complete-functions string))))))))
666 (defun org-contacts-org-complete--annotation-function (candidate)
667 "Return `org-contacts' tags of contact CANDIDATE."
668 ;; TODO
669 (ignore candidate))
671 (defun org-contacts--candidates-org-complete-get-doc (candidate)
672 "Return `org-contacts' content of contact CANDIDATE."
673 (let* ((contact (seq-find
674 (lambda (contact) (string-equal (plist-get contact :name) (get-text-property 0 'contact-name candidate)))
675 (org-contacts-all-contacts)))
676 (name (plist-get contact :name))
677 (file (plist-get contact :file))
678 (position (plist-get contact :position))
679 (org-contact-buffer (get-buffer (find-file-noselect file)))
680 ;; get org-contact headline and property drawer.
681 (contact-content (with-current-buffer org-contact-buffer
682 (when (derived-mode-p 'org-mode)
683 (save-excursion
684 (goto-char position)
685 (cond
686 ((org-at-block-p)
687 (org-narrow-to-block))
688 (t (org-narrow-to-subtree)))
689 (let ((content (buffer-substring-no-properties (point-min) (point-max))))
690 (when (buffer-narrowed-p) (widen))
691 content))))))
692 (cons name contact-content)))
694 ;; TEST:
695 ;; (setq org-contacts--candidates-complete-doc-cache nil)
696 ;; (org-contacts--candidates-complete-doc-cache-setting)
698 (defvar org-contacts--candidates-complete-doc-cache nil
699 "A list of contact candidates completion doc cache.")
701 (defun org-contacts--candidates-complete-doc-cache-setting ()
702 "Generate cache for contact candidates completion doc."
703 (if (null org-contacts--candidates-complete-doc-cache)
704 (let* ((candidates (org-contacts--candidates-cache-setting))
705 (candidates-complete-doc-list
706 (mapcar
707 (lambda (candidate)
708 (org-contacts--candidates-org-complete-get-doc candidate))
709 candidates)))
710 (setq org-contacts--candidates-complete-doc-cache candidates-complete-doc-list))
711 org-contacts--candidates-complete-doc-cache))
713 ;; TEST:
714 ;; (let* ((candidate (car (org-contacts--candidates-complete-doc-cache-setting)))
715 ;; (name (car candidate)))
716 ;; ;; (pp candidate)
717 ;; ;; (type-of candidate)
718 ;; (print name)
719 ;; (princ (text-properties-at 0 (cdr candidate)))
720 ;; (get-text-property 0 'contact-name (cdr candidate))
721 ;; (get-text-property 0 'annotation (cdr candidate)))
723 ;; (cdr (assoc "stardiviner" (org-contacts--candidates-complete-doc-cache-setting)))
725 (defun org-contacts-org-complete--doc-function (candidate)
726 "Populates *org-contact* with the documentation for the content of contact CANDIDATE."
727 (let* ((name (substring-no-properties candidate 1 nil))
728 (contact (seq-find
729 (lambda (contact) (string-equal (plist-get contact :name) name))
730 (org-contacts-all-contacts)))
731 (contact-content (cdr (assoc name (org-contacts--candidates-complete-doc-cache-setting))))
732 (doc-buffer (get-buffer-create " *org-contact*")))
733 (with-current-buffer doc-buffer
734 (read-only-mode 1)
735 (let ((inhibit-read-only t))
736 (erase-buffer)
737 (insert contact-content)
738 (org-mode)
739 (org-fold-show-all)
740 (font-lock-ensure))
741 (current-buffer))))
743 ;; TEST:
744 ;; (org-contacts-org-complete--doc-function "@stardiviner")
745 ;; (org-contacts-org-complete--doc-function (car org-contacts--candidates-cache-list))
746 ;; (benchmark 1 '(alist-get (car org-contacts--candidates-cache-list) (org-contacts--candidates-complete-doc-cache-setting)))
747 ;; (benchmark 1 '(org-contacts-org-complete--doc-function (nth 10 org-contacts--candidates-cache-list)))
749 ;; display company-mode doc buffer bellow current window.
750 (add-to-list 'display-buffer-alist '("^ \\*org-contact\\*" . (display-buffer-below-selected)))
752 (defun org-contacts-org-complete--exit-function (candidate)
753 (message "org-contacts: %s" (get-text-property 0 'contact-name candidate)))
755 (defun org-contacts-org-complete--location-function (candidate)
756 "Return `org-contacts' location of contact CANDIDATE."
757 (let* ((candidate (substring-no-properties candidate 1 nil))
758 (contact (seq-find
759 (lambda (contact) (string-equal (plist-get contact :name) candidate))
760 (org-contacts-all-contacts)))
761 (name (plist-get contact :name))
762 (file (plist-get contact :file))
763 (position (plist-get contact :position)))
764 (ignore name)
765 (with-current-buffer (find-file-noselect file)
766 (goto-char position)
767 (cons (current-buffer) position))))
769 ;;;###autoload
770 (defun org-contacts-org-complete-function ()
771 "`completion-at-point' function to complete @name in `org-mode'.
772 Usage: (add-hook \\='completion-at-point-functions
773 #\\='org-contacts-org-complete-function nil \\='local)"
774 (when-let* ((end (point))
775 (begin (save-excursion (skip-chars-backward "[:alnum:]@") (point)))
776 (symbol (buffer-substring-no-properties begin end))
777 (org-contacts-prefix-p (string-prefix-p "@" symbol)))
778 (when org-contacts-prefix-p
779 (list begin
781 (completion-table-dynamic
782 (lambda (_)
783 (mapcar
784 (lambda (contact) (concat "@" (plist-get contact :name)))
785 (org-contacts-all-contacts))))
786 :predicate 'stringp
787 :exclusive 'no
788 ;; properties check out `completion-extra-properties'
789 :annotation-function #'org-contacts-org-complete--annotation-function
790 ;; TODO: change completion candidate inserted contact name into org-contact link
791 :exit-function #'org-contacts-org-complete--exit-function
792 :company-docsig #'identity ; metadata
793 :company-doc-buffer #'org-contacts-org-complete--doc-function ; doc popup
794 :company-location #'org-contacts-org-complete--location-function))))
796 ;;;###autoload
797 (defun org-contacts-org-complete-setup ()
798 "Setup `completion-at-point-functions' with `org-contacts' in buffer local."
799 (when (member major-mode org-contacts-completion-enabled-mode-list)
800 (add-hook 'completion-at-point-functions 'org-contacts-org-complete-function nil 'local)))
801 ;;;###autoload
802 (add-hook 'org-mode-hook #'org-contacts-org-complete-setup)
804 (defun org-contacts-gnus-get-name-email ()
805 "Get name and email address from Gnus message."
806 (if (gnus-alive-p)
807 (gnus-with-article-headers
808 (mail-extract-address-components
809 (or (mail-fetch-field "From") "")))))
811 (defun org-contacts-gnus-article-from-get-marker ()
812 "Return a marker for a contact based on From."
813 (let* ((address (org-contacts-gnus-get-name-email))
814 (name (car address))
815 (email (cadr address)))
816 (cl-cadar (or (org-contacts-filter
819 (cons org-contacts-email-property (concat "\\b" (regexp-quote email) "\\b")))
820 (when name
821 (org-contacts-filter
822 (concat "^" name "$")))))))
824 (defun org-contacts-gnus-article-from-goto ()
825 "Go to contact in the From address of current Gnus message."
826 (interactive)
827 (let ((marker (org-contacts-gnus-article-from-get-marker)))
828 (when marker
829 (switch-to-buffer-other-window (marker-buffer marker))
830 (goto-char marker)
831 (when (eq major-mode 'org-mode)
832 (if (fboundp 'org-fold-show-context)
833 (org-fold-show-context 'agenda)
834 (org-fold-show-context 'agenda))))))
836 (with-no-warnings (defvar date)) ;; unprefixed, from calendar.el
838 ;;;###autoload
839 (defun org-contacts-anniversaries (&optional field format)
840 "Compute FIELD anniversary for each contact, returning FORMAT.
841 Default FIELD value is \"BIRTHDAY\".
843 Format is a string matching the following format specification:
845 %h - Heading name
846 %l - Link to the heading
847 %y - Number of year
848 %Y - Number of year (ordinal)"
849 (let ((calendar-date-style 'american))
850 (unless format (setq format org-contacts-birthday-format))
851 (cl-loop for contact in (org-contacts-filter)
852 for anniv = (let ((anniv (cdr (assoc-string
853 (or field org-contacts-birthday-property)
854 (nth 2 contact)))))
855 (when anniv
856 (calendar-gregorian-from-absolute
857 (org-time-string-to-absolute anniv))))
858 ;; Use `diary-anniversary' to compute anniversary.
859 ;; FIXME: should we require `diary-lib' somewhere to be sure
860 ;; `diary-anniversary' is defined when we get here?
861 if (and anniv (apply #'diary-anniversary anniv))
862 collect (format-spec format
863 `((?l . ,(org-with-point-at (cadr contact) (org-store-link nil)))
864 (?h . ,(car contact))
865 (?y . ,(- (calendar-extract-year date)
866 (calendar-extract-year anniv)))
867 (?Y . ,(let ((years (- (calendar-extract-year date)
868 (calendar-extract-year anniv))))
869 (format "%d%s" years (diary-ordinal-suffix years)))))))))
871 (defun org-contacts--completing-read-date ( prompt _collection
872 &optional _predicate _require-match _initial-input
873 _hist def _inherit-input-method)
874 "Like `completing-read' but reads a date.
875 Only PROMPT and DEF are really used."
876 (org-read-date nil nil nil prompt nil def))
878 (add-to-list 'org-property-set-functions-alist
879 `(,org-contacts-birthday-property . org-contacts--completing-read-date))
881 (defun org-contacts-template-name (&optional return-value)
882 "Try to return the contact name for a template.
883 If not found return RETURN-VALUE or something that would ask the user."
884 (or (car (org-contacts-gnus-get-name-email))
885 return-value
886 "%^{Name}"))
888 (defun org-contacts-template-email (&optional return-value)
889 "Try to return the contact email for a template.
890 If not found return RETURN-VALUE or something that would ask the user."
891 (or (cadr (org-contacts-gnus-get-name-email))
892 return-value
893 (concat "%^{" org-contacts-email-property "}p")))
895 (defun org-contacts-gnus-store-last-mail ()
896 "Store a link between mails and contacts.
898 This function should be called from `gnus-article-prepare-hook'."
899 (let ((marker (org-contacts-gnus-article-from-get-marker)))
900 (when marker
901 (with-current-buffer (marker-buffer marker)
902 (save-excursion
903 (goto-char marker)
904 (let* ((org-link-email-description-format (or org-contacts-email-link-description-format
905 org-link-email-description-format))
906 (link (gnus-with-article-buffer (org-store-link nil))))
907 (org-set-property org-contacts-last-read-mail-property link)))))))
909 (defun org-contacts-icon-as-string ()
910 "Return the contact icon as a string."
911 (let ((image (org-contacts-get-avatar-icon)))
912 (concat
913 (propertize "-" 'display
914 (append
915 (if image
916 image
917 `'(space :width (,org-contacts-icon-size)))
918 '(:ascent 100)))
919 " ")))
921 ;;====================================== org-contacts searching =====================================
923 (defcustom org-contacts-identity-properties-list
924 `(,org-contacts-email-property
925 ,org-contacts-alias-property
926 ,org-contacts-tel-property
927 ,org-contacts-address-property
928 ,org-contacts-birthday-property)
929 "Matching rule for finding heading that are contacts.
930 This can be property key checking."
931 :type '(repeat symbol)
932 :safe 'listp)
934 (defvar org-contacts-ahead-space-padding (make-string 5 ? )
935 "The space padding for align avatar image with contact name and properties.")
937 (defun org-contacts--candidate (headline)
938 "Return candidate string from Org HEADLINE epom element node."
939 (let* ((org-contacts-icon-size 32)
940 (contact-name (org-element-property :raw-value headline))
941 (tags (org-element-property :tags headline))
942 (properties (org-entry-properties headline 'standard))
943 ;; extra headline properties
944 (avatar-image-path
945 (when-let* ((avatar-value (org-entry-get headline "AVATAR"))
946 (avatar-link-path (cond
947 ;; bracket link: [[file:contact dir/avatar image.png]]
948 ;; TEST:
949 ;; (when (string-match org-link-bracket-re "[[file:contact_dir/avatar image.png]]")
950 ;; (match-string 1 "[[file:contact_dir/avatar image.png]]"))
951 ;; (when (string-match org-link-bracket-re "[[attachment:avatar image.png]]")
952 ;; (match-string 1 "[[attachment:avatar image.png]]"))
953 ;; (when (string-match org-link-bracket-re "[[file:contact_dir/avatar image.png]]")
954 ;; (when-let* ((link-internal (match-string 1 "[[file:contact_dir/avatar image.png]]"))
955 ;; (_ (string-match "\\([file\\|attachment]\\):\\(.*\\)" link-internal)))
956 ;; (match-string 2 link-internal)))
957 ((or (string-match org-link-bracket-re avatar-value)
958 (string-match org-link-any-re avatar-value))
959 (when-let* ((link-internal (or (match-string 1 avatar-value) (match-string 2 avatar-value)))
960 (_ (and (or (string-prefix-p "file:" link-internal)
961 (string-prefix-p "attachment:" link-internal))
962 (seq-some
963 (lambda (image-extension) (string-suffix-p image-extension link-internal))
964 image-file-name-extensions))))
965 (when (string-match "\\([file\\|attachment]\\):\\(.*\\)" link-internal)
966 (match-string 2 link-internal))))
967 ;; plain link: file:/path/to/image.jpg
968 ;; TEST:
969 ;; (when (string-match org-link-plain-re "file:/path/to/image.jpg")
970 ;; (match-string 1 "file:/path/to/image.jpg")
971 ;; (match-string 2 "file:/path/to/image.jpg"))
972 ((string-match org-link-plain-re avatar-value)
973 (match-string 2 avatar-value))
974 ;; just file-name: contact-name.jpg
975 ((string-match (concat (regexp-opt image-file-name-extensions) (rx line-end)) avatar-value)
976 (match-string 0 avatar-value))))
977 (avatar-absolute-path (file-name-concat
978 (or org-contacts-directory
979 (expand-file-name (file-name-directory (car org-contacts-files))))
980 avatar-link-path))
981 (_ (org-file-image-p avatar-absolute-path))
982 (_ (file-exists-p avatar-absolute-path)))
983 avatar-absolute-path))
984 (info (concat "\n"
985 (concat org-contacts-ahead-space-padding " ")
986 (let ((org-property-separators (list (cons org-contacts-nickname-property "[,\ ]"))))
987 (org-entry-get headline org-contacts-nickname-property))
988 (let ((org-property-separators (list (cons org-contacts-email-property "[,\ ]"))))
989 (org-entry-get headline org-contacts-email-property))
990 "\n"))
991 (middle-line-length (let ((length (- (abs org-tags-column)
992 (length (string-join tags ":"))
993 (length contact-name))))
994 (if (> length 0) length 0))))
995 ;; detect whether headline is an org-contacts entry?
996 (when (seq-intersection org-contacts-identity-properties-list (mapcar 'car properties))
997 (propertize
998 (concat
999 (if avatar-image-path
1000 (propertize org-contacts-ahead-space-padding
1001 'display (create-image avatar-image-path nil nil
1002 :ascent 30 ; set image baseline to align image top with candidate line.
1003 :width org-contacts-icon-size))
1004 org-contacts-ahead-space-padding)
1006 contact-name
1007 (format " %s [%s]"
1008 (make-string (or middle-line-length 0) ?―)
1009 (string-join tags ":")))
1010 'contact-name contact-name
1011 'annotation info))))
1013 (defun org-contacts--candidates (files)
1014 "Return a list of candidates from FILES."
1015 (with-temp-buffer
1016 (dolist (file files)
1017 (insert-file-contents file) ; don't need to actually open file.
1018 (goto-char (point-max))
1019 (newline 2))
1020 (delay-mode-hooks ; This will prevent user hooks from running during parsing.
1021 (org-mode)
1022 (goto-char (point-min))
1023 (let ((candidates nil))
1024 (org-element-map (org-element-parse-buffer 'headline) 'headline
1025 (lambda (headline)
1026 (when-let* ((candidate (org-contacts--candidate headline)))
1027 (push candidate candidates))))
1028 (nreverse candidates)))))
1030 (defun org-contacts--annotator (candidate)
1031 "Annotate contact completion CANDIDATE."
1032 (concat (propertize " " 'display '(space :align-to center))
1033 (get-text-property 0 'annotation candidate)))
1035 (defun org-contacts--candidates-return (&optional files)
1036 "Return `org-contacts' candidates which parsed from FILES."
1037 (if-let* ((files (or files org-contacts-files)))
1038 (org-contacts--candidates files)
1039 (user-error "Files does not exist: %S" files)))
1041 (defvar org-contacts--candidates-cache-list nil
1042 "A cache variable of `org-contacts--candidates' list.")
1044 (defun org-contacts--candidates-cache-reset ()
1045 "Reset `org-contacts--candidates-cache-list'."
1046 (interactive)
1047 (setq org-contacts-all-contacts nil)
1048 (setq org-contacts--candidates-cache-list nil))
1050 (defun org-contacts--candidates-cache-setting (&optional files)
1051 "Cache the internal variable `org-contacts--candidates-cache-list' of org-contacts candidates."
1052 (if (null org-contacts--candidates-cache-list)
1053 (setq org-contacts--candidates-cache-list (org-contacts--candidates-return (or files org-contacts-files)))
1054 org-contacts--candidates-cache-list))
1056 (defun org-contacts-browse-function (contact-name)
1057 "Jump to CONTACT-NAME headline."
1058 (mapcar
1059 (lambda (file)
1060 (let ((buf (find-file-noselect (expand-file-name file))))
1061 (with-current-buffer buf
1062 ;; NOTE: `org-goto-marker-or-bmk' will display buffer in current window, not follow `display-buffer' rule.
1063 (when-let* ((found-contact (org-find-exact-headline-in-buffer contact-name)))
1064 (org-goto-marker-or-bmk found-contact)
1065 ;; FIXME: `goto-char' not physically move point in buffer.
1066 ;; (display-buffer buf '(display-buffer-below-selected))
1067 ;; (goto-char (org-find-exact-headline-in-buffer contact-name nil t))
1068 (org-fold-show-context)))))
1069 org-contacts-files))
1071 ;;;###autoload
1072 (defun org-contacts (&optional files)
1073 "Search `org-contacts' from FILES and jump to contact location."
1074 (interactive)
1075 (org-contacts--candidates-cache-setting files)
1076 (if-let* ((files (or files org-contacts-files))
1077 ((seq-every-p 'file-exists-p files)))
1078 (when-let* ((candidates org-contacts--candidates-cache-list)
1079 (minibuffer-allow-text-properties t)
1080 (completion-extra-properties
1081 (list :category 'org-contacts
1082 :annotation-function #'org-contacts--annotator))
1083 (choice (completing-read "org-contacts: " candidates nil 'require-match))
1084 (contact-name (get-text-property 0 'contact-name choice)))
1085 ;; jump to org-contacts file contact position.
1086 (org-contacts-browse-function contact-name))
1087 (user-error "Files does not exist: %S" files)))
1089 ;;;###autoload
1090 (defun org-contacts-agenda (name)
1091 "Create agenda view for contacts matching NAME."
1092 (interactive (list (read-string "Name: ")))
1093 (let ((org-agenda-files (org-contacts-files))
1094 (org-agenda-skip-function
1095 (lambda () (org-agenda-skip-if nil `(notregexp ,name))))
1096 (org-agenda-prefix-format
1097 (propertize
1098 "%(org-contacts-icon-as-string)% s%(org-contacts-irc-number-of-unread-messages) "
1099 'keymap org-contacts-keymap))
1100 (org-agenda-overriding-header
1101 (or org-agenda-overriding-header
1102 (concat "List of contacts matching `" name "':"))))
1103 (setq org-agenda-skip-regexp name)
1104 (org-tags-view nil org-contacts-matcher)
1105 (with-current-buffer org-agenda-buffer-name
1106 (setq org-agenda-redo-command
1107 (list 'org-contacts name)))))
1109 (defun org-contacts-completing-read (prompt
1110 &optional predicate
1111 initial-input hist def inherit-input-method)
1112 "Call `completing-read' with contacts name as collection."
1113 (org-completing-read
1114 prompt (org-contacts-filter) predicate t initial-input hist def inherit-input-method))
1116 (defun org-contacts-format-name (name)
1117 "Trim any local formatting to get a bare NAME."
1118 ;; Remove radio targets characters
1119 (replace-regexp-in-string org-radio-target-regexp "\\1" name))
1121 (defun org-contacts-format-email (name email)
1122 "Format an EMAIL address corresponding to NAME."
1123 (unless email
1124 (error "`email' cannot be nul"))
1125 (if name
1126 (concat (org-contacts-format-name name) " <" email ">")
1127 email))
1129 (defun org-contacts-check-mail-address (mail)
1130 "Add MAIL address to contact at point if it does not have it."
1131 (let ((mails (org-entry-get (point) org-contacts-email-property)))
1132 (unless (member mail (split-string mails))
1133 (when (yes-or-no-p
1134 (format "Do you want to add this address to %s?" (org-get-heading t)))
1135 (org-set-property org-contacts-email-property (concat mails " " mail))))))
1137 (defun org-contacts-gnus-check-mail-address ()
1138 "Check that contact has the current address recorded.
1139 This function should be called from `gnus-article-prepare-hook'."
1140 (let ((marker (org-contacts-gnus-article-from-get-marker)))
1141 (when marker
1142 (org-with-point-at marker
1143 (org-contacts-check-mail-address (cadr (org-contacts-gnus-get-name-email)))))))
1145 (defun org-contacts-gnus-insinuate ()
1146 "Add some hooks for Gnus user.
1147 This adds `org-contacts-gnus-check-mail-address' and
1148 `org-contacts-gnus-store-last-mail' to
1149 `gnus-article-prepare-hook'. It also adds a binding on `;' in
1150 `gnus-summary-mode-map' to `org-contacts-gnus-article-from-goto'"
1151 (require 'gnus)
1152 (require 'gnus-art)
1153 (define-key gnus-summary-mode-map ";" #'org-contacts-gnus-article-from-goto)
1154 (add-hook 'gnus-article-prepare-hook #'org-contacts-gnus-check-mail-address)
1155 (add-hook 'gnus-article-prepare-hook #'org-contacts-gnus-store-last-mail))
1157 ;;;###autoload
1158 (defun org-contacts-email-setup-completion-at-point ()
1159 "Add `org-contacts-message-complete-function' to capf for completing contact at point."
1160 (when (member major-mode org-contacts-completion-enabled-mode-list)
1161 (add-hook 'completion-at-point-functions 'org-contacts-message-complete-function nil 'local)))
1163 (add-hook 'message-mode-hook #'org-contacts-email-setup-completion-at-point)
1164 (when (featurep 'mu4e)
1165 (add-hook 'mu4e-compose-mode-hook 'org-contacts-email-setup-completion-at-point))
1167 (defun org-contacts-wl-get-from-header-content ()
1168 "Retrieve the content of the `From' header of an email.
1169 Works from wl-summary-mode and mime-view-mode - that is while viewing email.
1170 Depends on Wanderlust been loaded."
1171 (with-current-buffer (org-capture-get :original-buffer)
1172 (cond
1173 ((eq major-mode 'wl-summary-mode) (when (and (boundp 'wl-summary-buffer-elmo-folder)
1174 wl-summary-buffer-elmo-folder)
1175 (elmo-message-field
1176 wl-summary-buffer-elmo-folder
1177 (wl-summary-message-number)
1178 'from)))
1179 ((eq major-mode 'mime-view-mode) (std11-narrow-to-header)
1180 (prog1
1181 (std11-fetch-field "From")
1182 (widen))))))
1184 (defun org-contacts-wl-get-name-email ()
1185 "Get name and email address from Wanderlust email.
1186 See `org-contacts-wl-get-from-header-content' for limitations."
1187 (let ((from (org-contacts-wl-get-from-header-content)))
1188 (when from
1189 (list (wl-address-header-extract-realname from)
1190 (wl-address-header-extract-address from)))))
1192 (defun org-contacts-template-wl-name (&optional return-value)
1193 "Try to return the contact name for a template from wl.
1194 If not found, return RETURN-VALUE or something that would ask the
1195 user."
1196 (or (car (org-contacts-wl-get-name-email))
1197 return-value
1198 "%^{Name}"))
1200 (defun org-contacts-template-wl-email (&optional return-value)
1201 "Try to return the contact email for a template from Wanderlust.
1202 If not found return RETURN-VALUE or something that would ask the user."
1203 (or (cadr (org-contacts-wl-get-name-email))
1204 return-value
1205 (concat "%^{" org-contacts-email-property "}p")))
1207 (defun org-contacts-view-send-email (&optional ask)
1208 "Send email to the contact at point.
1209 If ASK is set, ask for the email address even if there's only one
1210 address."
1211 (interactive "P")
1212 (let ((marker (org-get-at-bol 'org-hd-marker)))
1213 (org-with-point-at marker
1214 (let ((emails (org-entry-get (point) org-contacts-email-property)))
1215 (if emails
1216 (let ((email-list (org-contacts-split-property emails)))
1217 (if (and (= (length email-list) 1) (not ask))
1218 (compose-mail (org-contacts-format-email
1219 (org-get-heading t) emails))
1220 (let ((email (completing-read "Send mail to which address: " email-list)))
1221 (setq email (org-contacts-strip-link email))
1222 (org-contacts-check-mail-address email)
1223 (compose-mail (org-contacts-format-email (org-get-heading t) email)))))
1224 (error (format "This contact has no mail address set (no %s property)"
1225 org-contacts-email-property)))))))
1227 (defun org-contacts-get-avatar-icon (&optional pom)
1228 "Get icon for contact at POM and return the avatar icon image object."
1229 (setq pom (or pom (point)))
1230 (catch 'icon
1231 ;; Use `org-contacts-icon-property'
1232 (let* ((link-matcher-regexp
1233 "\\[\\[\\([^]]*\\)\\]\\(\\[\\(.*\\)\\]\\)?\\]")
1234 (contacts-dir (file-name-directory (car (org-contacts-files))))
1235 (image-path
1236 (if-let* ((avatar (org-entry-get pom org-contacts-icon-property)))
1237 (cond
1238 ;; [[file:dir/filename.png]]
1239 ((string-match-p "\\[\\[.*\\]\\]" avatar)
1240 ;; FIXME: What if avatar matches the above regexp but the one below?
1241 (when (string-match link-matcher-regexp avatar)
1242 ;; FIXME: 5 seems to be the length of `file:' but I can't see anything that
1243 ;; guarantees that the submatch 1 starts with `file:'.
1244 (expand-file-name (substring (match-string-no-properties 1 avatar) 5 nil)
1245 contacts-dir)))
1246 ;; "" (empty string)
1247 ((string-empty-p avatar) nil)
1248 (t (expand-file-name avatar contacts-dir))))))
1249 (when image-path
1250 (throw 'icon
1251 (if (featurep 'imagemagick)
1252 (create-image image-path 'imagemagick nil :ascent 100 :height org-contacts-icon-size)
1253 (create-image image-path nil nil :ascent 100 :height org-contacts-icon-size)))))
1254 ;; Next, try Gravatar
1255 (when org-contacts-icon-use-gravatar
1256 (defvar gravatar-size)
1257 (let* ((gravatar-size org-contacts-icon-size)
1258 (email-list (org-entry-get pom org-contacts-email-property))
1259 (gravatar
1260 (when email-list
1261 (cl-loop for email in (org-contacts-split-property email-list)
1262 for gravatar = (gravatar-retrieve-synchronously (org-contacts-strip-link email))
1263 if (and gravatar
1264 (not (eq gravatar 'error)))
1265 return gravatar))))
1266 (when gravatar (throw 'icon gravatar))))))
1268 (defun org-contacts-irc-buffer (&optional pom)
1269 "Get the IRC buffer associated with the entry at POM."
1270 (setq pom (or pom (point)))
1271 (let ((nick (org-entry-get pom org-contacts-nickname-property)))
1272 (when nick
1273 (let ((buffer (get-buffer nick)))
1274 (when buffer
1275 (with-current-buffer buffer
1276 (when (eq major-mode 'erc-mode)
1277 buffer)))))))
1279 (defun org-contacts-irc-number-of-unread-messages (&optional pom)
1280 "Return the number of unread messages for contact at POM."
1281 (when (boundp 'erc-modified-channels-alist)
1282 (let ((number (cadr (assoc (org-contacts-irc-buffer pom) erc-modified-channels-alist))))
1283 (if number
1284 (format (concat "%3d unread message" (if (> number 1) "s" " ") " ") number)
1285 (make-string 21 ? )))))
1287 (defun org-contacts-view-switch-to-irc-buffer ()
1288 "Switch to the IRC buffer of the current contact if it has one."
1289 (interactive)
1290 (let ((marker (org-get-at-bol 'org-hd-marker)))
1291 (org-with-point-at marker
1292 (switch-to-buffer-other-window (org-contacts-irc-buffer)))))
1294 (defun org-contacts-completing-read-nickname
1295 (prompt collection
1296 &optional predicate require-match initial-input
1297 hist def inherit-input-method)
1298 "Like `completing-read' but reads a property \"NICKNAME\" value in PROMPT.
1299 Return a org-contacts \"NICKNAME\" as property's value after completion."
1300 (let* ((org-contacts-candidates-propertized
1301 (mapcar
1302 (lambda (plist)
1303 (let* ((name (plist-get plist :name))
1304 (name-english (plist-get plist :name-english))
1305 (nick (plist-get plist :nick)))
1306 (unless (or (null nick) (string-empty-p nick))
1307 (propertize nick ; <- The `completing-read' select candidate inserted value.
1308 'display (concat
1309 (when name (propertize (format "%s " name) :face '(:foreground "ForestGreen")))
1310 (unless (or (null name-english) (string-empty-p name-english))
1311 (propertize (format "%s " name-english) :face '(:foreground "LightSeaGreen")))
1312 (unless (or (null nick) (string-empty-p nick))
1313 (propertize (format "(%s) " nick) :face '(:foreground "LightGray"))))))))
1314 (org-contacts-all-contacts)))
1315 ;; (contact-names (mapcar (lambda (plist) (plist-get plist :name)) (org-contacts-all-contacts)))
1316 (contact-nick (substring-no-properties
1317 (org-completing-read (or prompt "org-contacts NICKNAME: ")
1318 (append org-contacts-candidates-propertized collection
1319 (when (or (require 'erc nil t)
1320 (erc-server-buffer-live-p)
1321 (erc-server-process-alive)
1322 erc-server-processing-p)
1323 (org-contacts-erc-nicknames-list)))
1324 predicate require-match initial-input
1325 hist def inherit-input-method))))
1326 ;; Detect whether input contact is in `org-contacts' existing list.
1327 contact-nick))
1329 (defun org-contacts-erc-nicknames-list ()
1330 "Return all nicknames of all ERC buffers."
1331 (cl-loop for buffer in (erc-buffer-list)
1332 nconc (with-current-buffer buffer
1333 (cl-loop for user-entry
1334 in (mapcar #'car (erc-get-channel-user-list))
1335 collect (elt user-entry 1)))))
1337 (add-to-list 'org-property-set-functions-alist
1338 `(,org-contacts-nickname-property . org-contacts-completing-read-nickname))
1340 (defun org-contacts-vcard-escape (str)
1341 "Escape ; , and \n in STR for the VCard format."
1342 ;; Thanks to this library for the regexp:
1343 ;; https://www.emacswiki.org/cgi-bin/wiki/bbdb-vcard-export.el
1344 (when str
1345 (replace-regexp-in-string
1346 "\n" "\\\\n"
1347 (replace-regexp-in-string "\\(;\\|,\\|\\\\\\)" "\\\\\\1" str))))
1349 (defun org-contacts-vcard-encode-name (name)
1350 "Try to encode NAME as VCard's N property.
1351 The N property expects
1353 FamilyName;GivenName;AdditionalNames;Prefix;Postfix.
1355 Org-contacts does not specify how to encode the name. So we try
1356 to do our best."
1357 (concat (replace-regexp-in-string "\\(\\w+\\) \\(.*\\)" "\\2;\\1" name) ";;;"))
1359 (defun org-contacts-vcard-format (contact)
1360 "Formats CONTACT in VCard 3.0 format."
1361 (let* ((properties (nth 2 contact))
1362 (name (org-contacts-vcard-escape (car contact)))
1363 (n (org-contacts-vcard-encode-name name))
1364 (email (cdr (assoc-string org-contacts-email-property properties)))
1365 (tel (cdr (assoc-string org-contacts-tel-property properties)))
1366 (ignore-list (cdr (assoc-string org-contacts-ignore-property properties)))
1367 (ignore-list (when ignore-list
1368 (org-contacts-split-property ignore-list)))
1369 (note (cdr (assoc-string org-contacts-note-property properties)))
1370 (bday (org-contacts-vcard-escape (cdr (assoc-string org-contacts-birthday-property properties))))
1371 (addr (cdr (assoc-string org-contacts-address-property properties)))
1372 (nick (org-contacts-vcard-escape (cdr (assoc-string org-contacts-nickname-property properties))))
1373 (categories (mapconcat (lambda (str) (concat "" str))
1374 (delq "" (string-split (string-trim (cdr (assoc-string "TAGS" properties)) ":" ":") ":"))
1375 ","))
1376 (head (format "BEGIN:VCARD\nVERSION:3.0\nN:%s\nFN:%s\n" n name))
1377 emails-list result phones-list)
1378 (concat
1379 head
1380 (when email
1381 (progn
1382 (setq emails-list (org-contacts-remove-ignored-property-values
1383 ignore-list (org-contacts-split-property email)))
1384 (setq result "")
1385 (while emails-list
1386 (setq result (concat result "EMAIL:" (org-contacts-strip-link (car emails-list)) "\n"))
1387 (setq emails-list (cdr emails-list)))
1388 result))
1389 (when addr
1390 (format "ADR:;;%s\n" (replace-regexp-in-string "\\, ?" ";" addr)))
1391 (when tel
1392 (progn
1393 (setq phones-list (org-contacts-remove-ignored-property-values
1394 ignore-list (org-contacts-split-property tel)))
1395 (setq result "")
1396 (while phones-list
1397 (setq result (concat result "TEL:" (org-contacts-strip-link
1398 (org-link-unescape (car phones-list))) "\n"))
1399 (setq phones-list (cdr phones-list)))
1400 result))
1401 (when bday
1402 (let ((cal-bday (calendar-gregorian-from-absolute (org-time-string-to-absolute bday))))
1403 (format "BDAY:%04d-%02d-%02d\n"
1404 (calendar-extract-year cal-bday)
1405 (calendar-extract-month cal-bday)
1406 (calendar-extract-day cal-bday))))
1407 (when nick (format "NICKNAME:%s\n" nick))
1408 (when categories (format "CATEGORIES:%s\n" categories))
1409 (when note (format "NOTE:%s\n" note))
1410 "END:VCARD\n\n")))
1412 (defun org-contacts-export-as-vcard (&optional name file to-buffer)
1413 "Export org-contacts to V-Card 3.0.
1415 By default, all contacts are exported to `org-contacts-vcard-file'.
1417 When NAME is \\[universal-argument], prompts for a contact name.
1419 When NAME is \\[universal-argument] \\[universal-argument],
1420 prompts for a contact name and a file name where to export.
1422 When NAME is \\[universal-argument] \\[universal-argument]
1423 \\[universal-argument], prompts for a contact name and a buffer where to export.
1425 If the function is not called interactively, all parameters are
1426 passed to `org-contacts-export-as-vcard-internal'."
1427 (interactive "P")
1428 (when (called-interactively-p 'any)
1429 (cl-psetf name
1430 (when name
1431 (read-string "Contact name: "
1432 (nth 0 (org-contacts-at-point))))
1433 file
1434 (when (equal name '(16))
1435 (read-file-name "File: " nil org-contacts-vcard-file))
1436 to-buffer
1437 (when (equal name '(64))
1438 (read-buffer "Buffer: "))))
1439 (org-contacts-export-as-vcard-internal name file to-buffer))
1441 (defun org-contacts-export-as-vcard-internal (&optional name file to-buffer)
1442 "Export all contacts matching NAME as VCard 3.0.
1443 If TO-BUFFER is nil, the content is written to FILE or
1444 `org-contacts-vcard-file'. If TO-BUFFER is non-nil, the buffer
1445 is created and the VCard is written into that buffer."
1446 (let* ((filename (or file org-contacts-vcard-file))
1447 (buffer (if to-buffer
1448 (get-buffer-create to-buffer)
1449 (find-file-noselect filename))))
1450 (message "Exporting...")
1451 (set-buffer buffer)
1452 (let ((inhibit-read-only t)) (erase-buffer))
1453 (fundamental-mode)
1454 (when (fboundp 'set-buffer-file-coding-system)
1455 (set-buffer-file-coding-system coding-system-for-write))
1456 (cl-loop for contact in (org-contacts-filter name)
1457 do (insert (org-contacts-vcard-format contact)))
1458 (if to-buffer
1459 (current-buffer)
1460 (progn (save-buffer) (kill-buffer)))))
1462 (defun org-contacts-show-map (&optional name)
1463 "Show contacts on a map.
1464 Requires google-maps.el."
1465 (interactive)
1466 (unless (fboundp 'google-maps-static-show)
1467 (error "`org-contacts-show-map' requires `google-maps.el'"))
1468 (google-maps-static-show
1469 :markers
1470 (cl-loop
1471 for contact in (org-contacts-filter name)
1472 for addr = (cdr (assoc-string org-contacts-address-property (nth 2 contact)))
1473 if addr
1474 collect (cons (list addr) (list :label (string-to-char (car contact)))))))
1476 (defun org-contacts-strip-link (link)
1477 "Strip Org LINK extra delimiters.
1479 Remove brackets, description, link type and colon from an Org link
1480 string and return the pure link target."
1481 (let (startpos colonpos endpos)
1482 (setq startpos (string-match (regexp-opt '("[[tel:" "[[mailto:")) link))
1483 (if startpos
1484 (progn
1485 (setq colonpos (string-match ":" link))
1486 (setq endpos (string-match "\\]" link))
1487 (if endpos (substring link (1+ colonpos) endpos) link))
1488 (progn
1489 (setq startpos (string-match "mailto:" link))
1490 (setq colonpos (string-match ":" link))
1491 (if startpos (substring link (1+ colonpos)) link)))))
1493 ;; Add the link type supported by org-contacts-strip-link
1494 ;; so everything is in order for its use in Org files
1495 (if (fboundp 'org-link-set-parameters)
1496 (org-link-set-parameters "tel")
1497 (if (fboundp 'org-add-link-type)
1498 (org-add-link-type "tel")))
1500 (defun org-contacts-split-property (string &optional separators omit-nulls)
1501 "Custom version of `split-string'.
1502 Split a property STRING into sub-strings bounded by matches
1503 for SEPARATORS but keep Org links intact.
1505 The beginning and end of STRING, and each match for SEPARATORS, are
1506 splitting points. The substrings matching SEPARATORS are removed, and
1507 the substrings between the splitting points are collected as a list,
1508 which is returned.
1510 If SEPARATORS is non-nil, it should be a regular expression
1511 matching text which separates, but is not part of, the
1512 substrings. If nil it defaults to `org-contacts-property-values-separators',
1513 normally \"[,; \\f\\t\\n\\r\\v]+\", and OMIT-NULLS is forced to t.
1515 If OMIT-NULLS is t, zero-length substrings are omitted from the list so
1516 that for the default value of SEPARATORS leading and trailing whitespace
1517 are effectively trimmed. If nil, all zero-length substrings are retained."
1518 (let* ((omit-nulls (if separators omit-nulls t))
1519 (rexp (or separators org-contacts-property-values-separators))
1520 (inputlist (split-string string rexp omit-nulls))
1521 (linkstring "")
1522 (bufferstring "")
1523 (proplist (list "")))
1524 (while inputlist
1525 (setq bufferstring (pop inputlist))
1526 (if (string-match "\\[\\[" bufferstring)
1527 (progn
1528 (setq linkstring (concat bufferstring " "))
1529 (while (not (string-match "\\]\\]" bufferstring))
1530 (setq bufferstring (pop inputlist))
1531 (setq linkstring (concat linkstring bufferstring " ")))
1532 (setq proplist (cons (org-trim linkstring) proplist)))
1533 (setq proplist (cons bufferstring proplist))))
1534 (cdr (reverse proplist))))
1537 ;;; Add an Org link type `org-contact:' for easy jump to or searching org-contacts headline.
1538 ;; link spec: [[org-contact:query][desc]]
1539 ;;;###autoload
1540 (if (fboundp 'org-link-set-parameters)
1541 (org-link-set-parameters "org-contact"
1542 :follow #'org-contacts-link-open
1543 :complete #'org-contacts-link-complete
1544 :store #'org-contacts-link-store
1545 :face 'org-contacts-link-face)
1546 (when (fboundp 'org-add-link-type)
1547 (org-add-link-type "org-contact" 'org-contacts-link-open)))
1549 ;;;###autoload
1550 (defun org-contacts-link-store ()
1551 "Store the contact in `org-contacts-files' with a link."
1552 (when (and (eq major-mode 'org-mode)
1553 (member (buffer-file-name)
1554 (mapcar #'expand-file-name (org-contacts-files)))
1555 (not (org-before-first-heading-p))
1556 (let ((element (org-element-at-point)))
1557 (funcall (cdr (org-make-tags-matcher org-contacts-matcher))
1558 (org-element-property :todo-keyword element)
1559 (org-get-tags element)
1560 (org-element-property :level element))))
1561 (if (bound-and-true-p org-id-link-to-org-use-id)
1562 (org-id-store-link)
1563 (let ((headline-str (substring-no-properties (org-get-heading t t t t))))
1564 (org-link-store-props
1565 :type "org-contact"
1566 :link headline-str
1567 :description headline-str)
1568 (let ((link (concat "org-contact:" headline-str)))
1569 (org-link-add-props :link link :description headline-str)
1570 link)))))
1572 (defun org-contacts--all-contacts ()
1573 "Return a list of all contacts in `org-contacts-files'.
1574 Each element has the form (NAME . (FILE . POSITION))."
1575 (car (mapcar
1576 (lambda (file)
1577 (unless (buffer-live-p (get-buffer (file-name-nondirectory file)))
1578 (find-file-noselect file))
1579 (with-current-buffer (find-file-noselect file)
1580 (org-map-entries
1581 (lambda ()
1582 (let* ((name (substring-no-properties (org-get-heading t t t t)))
1583 (file (buffer-file-name))
1584 (position (point))
1585 ;; extract properties Org entry headline at `position' as data API for better contacts searching.
1586 (entry-properties (org-entry-properties position 'standard))
1587 (property-name-chinese (cdr (assoc (upcase "NAME(Chinese)") entry-properties)))
1588 (property-name-english (cdr (assoc (upcase "NAME(English)") entry-properties)))
1589 (property-nick (cdr (assoc "NICK" entry-properties)))
1590 (property-email (cdr (assoc "EMAIL" entry-properties)))
1591 ;; (property-mobile (cdr (assoc "MOBILE" entry-properties)))
1592 (property-wechat (cdr (assoc (upcase "WeChat") entry-properties)))
1593 (property-qq (cdr (assoc "QQ" entry-properties))))
1594 (list :name name :file file :position position
1595 :name-chinese property-name-chinese
1596 :name-english property-name-english
1597 :nick property-nick
1598 :email property-email
1599 :mobile property-email
1600 :wechat property-wechat
1601 :qq property-qq))))))
1602 (org-contacts-files))))
1604 ;;;###autoload
1605 (defun org-contacts-link-open (query)
1606 "Open org-contacts: link with jumping or searching QUERY."
1607 (let* ((file-path (car (org-contacts-files)))
1608 (file-name (file-name-nondirectory file-path))
1609 (buf (or (get-buffer file-name) (get-buffer (find-file-noselect file-path)))))
1610 (cond
1611 ;; /query/ format searching
1612 ((string-match "/.*/" query)
1613 (with-current-buffer buf
1614 (string-match "/\\(.*\\)/" query)
1615 (occur (match-string 1 query))))
1617 ;; jump to exact contact headline directly
1619 (with-current-buffer buf
1620 (if-let* ((position (org-find-exact-headline-in-buffer query)))
1621 (progn
1622 (goto-char (marker-position position))
1623 (org-fold-show-context))
1624 (user-error "[org-contacts] Can't find <%s> in your `org-contacts-files'" query)))
1625 (display-buffer buf '(display-buffer-below-selected))
1627 ;; FIXME:
1628 ;; (let* ((contact-entry (map-filter
1629 ;; (lambda (contact-plist)
1630 ;; (if (string-equal (plist-get contact-plist :name) query)
1631 ;; contact-plist))
1632 ;; (org-contacts-all-contacts)))
1633 ;; (contact-name (plist-get contact-entry :name))
1634 ;; (file (plist-get contact-entry :file))
1635 ;; (position (plist-get contact-entry :position))
1636 ;; (buf (get-buffer (file-name-nondirectory file))))
1637 ;; (with-current-buffer buf (goto-char position))
1638 ;; (display-buffer buf '(display-buffer-below-selected)))
1639 ))))
1641 ;;;###autoload
1642 (defun org-contacts-link-complete (&optional _arg)
1643 "Create a `org-contacts' link using completion."
1644 (let ((name (completing-read "org-contacts NAME: "
1645 (mapcar
1646 (lambda (plist) (plist-get plist :name))
1647 (org-contacts-all-contacts)))))
1648 (concat "org-contact:" name)))
1650 (defun org-contacts-link-face (path)
1651 "Different face color for different org-contacts: link PATH."
1652 (cond
1653 ((string-match "/.*/" path)
1654 '(:background "sky blue" :overline t :slant 'italic))
1655 (t '(:inherit org-link))))
1658 ;;; org-mode link "mailto:" email completion.
1659 (if (fboundp 'org-link-set-parameters)
1660 (org-link-set-parameters "mailto" :complete #'org-contacts-mailto-link-completion)
1661 (if (fboundp 'org-add-link-type)
1662 (org-add-link-type "mailto")))
1664 (defvar org-contacts-emails-list nil
1665 "A list variable of all `org-contacts' emails.")
1667 (defun org-contacts-mailto-link--get-all-emails ()
1668 "Retrieve all `org-contacts' EMAIL property values."
1669 (setq org-contacts-emails-list
1670 (mapcar
1671 (lambda (contact)
1672 (let* ((org-contacts-buffer (find-file-noselect (car (org-contacts-files))))
1673 (name (plist-get contact :name))
1674 (position (plist-get contact :position))
1675 (email (save-excursion
1676 (with-current-buffer org-contacts-buffer
1677 (goto-char position)
1678 ;; (symbol-name (org-property-or-variable-value 'EMAIL))
1679 (when-let* ((pvalue (org-entry-get (point) "EMAIL")))
1680 ;; handle `mailto:' link. e.g. "[[mailto:yantar92@posteo.net]]", "[[mailto:yantar92@posteo.net][yantar92@posteo.net]]"
1681 ;; Reference the testing file `test-org-contacts.el'.
1682 (if (string-match
1683 "\\[\\[mailto:\\(.*\\)\\]\\(\\[.*\\]\\)\\]\\(,\\ *\\[\\[mailto:\\(.*\\)\\]\\(\\[.*\\]\\)\\]\\)"
1684 pvalue)
1685 (match-string 1 pvalue)
1686 pvalue))))))
1687 (ignore name)
1688 ;; (cons name email)
1689 email))
1690 (org-contacts-all-contacts)))
1691 ;; clean nil and empty string "" from result.
1692 (delete ""
1693 (delete nil org-contacts-emails-list)))
1695 (defun org-contacts-mailto-link-completion (&optional _arg)
1696 "Org mode link `mailto:' completion with `org-contacts' emails."
1697 (let ((email (completing-read "org-contacts EMAIL: "
1698 (org-contacts-mailto-link--get-all-emails))))
1699 (concat "mailto:" email)))
1701 (provide 'org-contacts)
1703 ;;; org-contacts.el ends here