summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBlaise Thompson <blaise@untzag.com>2020-01-01 19:32:40 -0600
committerBlaise Thompson <blaise@untzag.com>2020-01-01 19:32:40 -0600
commit776351f867ffb7c5464facf3a733cf0f92a4b992 (patch)
tree7eacb26aa62b65d663f67dbb3473a135276ef06c
parent21f7af391075a0ae06739269cd1e151bceb2b04b (diff)
2020-01-01 19:32
-rw-r--r--emacs/init.el69
-rw-r--r--emacs/org-contacts.el1150
2 files changed, 1196 insertions, 23 deletions
diff --git a/emacs/init.el b/emacs/init.el
index 3477e96..a4a1b2b 100644
--- a/emacs/init.el
+++ b/emacs/init.el
@@ -10,9 +10,19 @@
("reg" "%(binary) -f %(ledger-file) reg")
("payee" "%(binary) -f %(ledger-file) reg @%(payee)")
("account" "%(binary) -f %(ledger-file) reg %(account)"))))
- '(org-agenda-files
+ '(org-agenda-files nil)
+ '(org-capture-templates
(quote
- ("/home/blaise/org/agenda.org" "/home/blaise/org/anniversaries.org" "/home/blaise/org/repeat.org" "/home/blaise/org/todo.org" "/home/blaise/org/clocked.org" "/home/blaise/uw-madison/")))
+ (("c" "Contact" entry
+ (file "~/org/contacts.org")
+ "* %(org-contacts-template-name)
+:PROPERTIES:
+:ADDRESS:
+:EMAIL: %(org-contacts-template-email)
+:PHONE:
+:NOTE:
+:END:" :empty-lines 0))))
+ '(org-contacts-files (quote ("~/org/contacts.org")))
'(package-selected-packages
(quote
(exec-path-from-shell ledger-mode evil-ledger ## ledger-import treemacs-evil treemacs avy move-text es-lib helm-flyspell flycheck which-key helm-swoop evil-leader helm spaceline evil use-package)))
@@ -115,26 +125,17 @@
)
(setq org-todo-keywords '((sequence "TODO" "WAITING" "|" "DONE" "DELEGATED" "CANCELED")))
(setq org-tags-column -99)
-(use-package org-brain
- :ensure t
- :init
- (setq org-brain-path "~/brain")
- ;; For Evil users
- (with-eval-after-load 'evil
- (evil-set-initial-state 'org-brain-visualize-mode 'emacs))
- :config
- (setq org-id-track-globally t)
- (setq org-id-locations-file "~/.emacs.d/.org-id-locations")
- (setq org-brain-visualize-default-choices 'all)
- (setq org-brain-title-max-length 12)
- (setq org-agenda-files '("~/org/"))
- (setq org-agenda-span 1)
- (setq org-agenda-start-with-log-mode t)
- (setq org-agenda-start-day "0d")
- (setq org-agenda-window-setup 'only-window)
- (setq org-clock-mode-line-total 'today)
- (setq org-duration-format (quote h:mm))
- )
+(setq org-id-track-globally t)
+(setq org-id-locations-file "~/.emacs.d/.org-id-locations")
+(setq org-brain-visualize-default-choices 'all)
+(setq org-brain-title-max-length 12)
+(setq org-agenda-files '("~/org/"))
+(setq org-agenda-span 1)
+(setq org-agenda-start-with-log-mode t)
+(setq org-agenda-start-day "0d")
+(setq org-agenda-window-setup 'only-window)
+(setq org-clock-mode-line-total 'today)
+(setq org-duration-format (quote h:mm))
(setq org-agenda-prefix-format '(
(agenda . "%i %-12.12:c %?-12t %s") ;; file name + org-agenda-entry-type
(timeline . " % s")
@@ -142,7 +143,29 @@
(tags . " %i %-12:c")
(search . " %i %-12:c")
)
-)
+ )
+(load "~/source/dotfiles/emacs/org-contacts.el")
+(use-package org-contacts
+ :ensure nil
+ :after org
+ :custom (org-contacts-files '("~/org/contacts.org"))
+ )
+(use-package org-capture
+ :ensure nil
+ :after org
+ :preface
+ (defvar my/org-contacts-template "* %(org-contacts-template-name)
+:PROPERTIES:
+:ADDRESS:
+:EMAIL:
+:PHONE:
+:NOTE:
+:END:" "Template for org-contacts.")
+ :custom
+ (org-capture-templates
+ `(("c" "Contact" entry (file "~/org/contacts.org"),
+ my/org-contacts-template
+ :empty-lines 0))))
;; spaceline
(use-package spaceline :ensure t)
diff --git a/emacs/org-contacts.el b/emacs/org-contacts.el
new file mode 100644
index 0000000..a7c268c
--- /dev/null
+++ b/emacs/org-contacts.el
@@ -0,0 +1,1150 @@
+;;; org-contacts.el --- Contacts management
+
+;; Copyright (C) 2010-2014 Julien Danjou <julien@danjou.info>
+
+;; Author: Julien Danjou <julien@danjou.info>
+;; Keywords: outlines, hypermedia, calendar
+;;
+;; This file is NOT part of GNU Emacs.
+;;
+;; This program is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs. If not, see <http://www.gnu.org/licenses/>.
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;
+;;; Commentary:
+
+;; This file contains the code for managing your contacts into Org-mode.
+
+;; To enter new contacts, you can use `org-capture' and a minimal template just like
+;; this:
+
+;; ("c" "Contacts" entry (file "~/Org/contacts.org")
+;; "* %(org-contacts-template-name)
+;; :PROPERTIES:
+;; :EMAIL: %(org-contacts-template-email)
+;; :END:")))
+;;
+;; You can also use a complex template, for example:
+;;
+;; ("c" "Contacts" entry (file "~/Org/contacts.org")
+;; "* %(org-contacts-template-name)
+;; :PROPERTIES:
+;; :EMAIL: %(org-contacts-template-email)
+;; :PHONE:
+;; :ALIAS:
+;; :NICKNAME:
+;; :IGNORE:
+;; :ICON:
+;; :NOTE:
+;; :ADDRESS:
+;; :BIRTHDAY:
+;; :END:")))
+;;
+;;; Code:
+
+(require 'cl)
+(require 'org)
+(require 'gnus-util)
+(require 'gnus-art)
+(require 'mail-utils)
+(require 'org-agenda)
+(require 'org-capture)
+
+(defgroup org-contacts nil
+ "Options about contacts management."
+ :group 'org)
+
+(defcustom org-contacts-files nil
+ "List of Org files to use as contacts source.
+When set to nil, all your Org files will be used."
+ :type '(repeat file)
+ :group 'org-contacts)
+
+(defcustom org-contacts-email-property "EMAIL"
+ "Name of the property for contact email address."
+ :type 'string
+ :group 'org-contacts)
+
+(defcustom org-contacts-tel-property "PHONE"
+ "Name of the property for contact phone number."
+ :type 'string
+ :group 'org-contacts)
+
+(defcustom org-contacts-address-property "ADDRESS"
+ "Name of the property for contact address."
+ :type 'string
+ :group 'org-contacts)
+
+(defcustom org-contacts-birthday-property "BIRTHDAY"
+ "Name of the property for contact birthday date."
+ :type 'string
+ :group 'org-contacts)
+
+(defcustom org-contacts-note-property "NOTE"
+ "Name of the property for contact note."
+ :type 'string
+ :group 'org-contacts)
+
+(defcustom org-contacts-alias-property "ALIAS"
+ "Name of the property for contact name alias."
+ :type 'string
+ :group 'org-contacts)
+
+(defcustom org-contacts-ignore-property "IGNORE"
+ "Name of the property, which values will be ignored when
+completing or exporting to vcard."
+ :type 'string
+ :group 'org-contacts)
+
+
+(defcustom org-contacts-birthday-format "Birthday: %l (%Y)"
+ "Format of the anniversary agenda entry.
+The following replacements are available:
+
+ %h - Heading name
+ %l - Link to the heading
+ %y - Number of year
+ %Y - Number of year (ordinal)"
+ :type 'string
+ :group 'org-contacts)
+
+(defcustom org-contacts-last-read-mail-property "LAST_READ_MAIL"
+ "Name of the property for contact last read email link storage."
+ :type 'string
+ :group 'org-contacts)
+
+(defcustom org-contacts-icon-property "ICON"
+ "Name of the property for contact icon."
+ :type 'string
+ :group 'org-contacts)
+
+(defcustom org-contacts-nickname-property "NICKNAME"
+ "Name of the property for IRC nickname match."
+ :type 'string
+ :group 'org-contacts)
+
+(defcustom org-contacts-icon-size 32
+ "Size of the contacts icons."
+ :type 'string
+ :group 'org-contacts)
+
+(defcustom org-contacts-icon-use-gravatar (fboundp 'gravatar-retrieve)
+ "Whether use Gravatar to fetch contact icons."
+ :type 'boolean
+ :group 'org-contacts)
+
+(defcustom org-contacts-completion-ignore-case t
+ "Ignore case when completing contacts."
+ :type 'boolean
+ :group 'org-contacts)
+
+(defcustom org-contacts-group-prefix "+"
+ "Group prefix."
+ :type 'string
+ :group 'org-contacts)
+
+(defcustom org-contacts-tags-props-prefix "#"
+ "Tags and properties prefix."
+ :type 'string
+ :group 'org-contacts)
+
+(defcustom org-contacts-matcher
+ (mapconcat #'identity
+ (mapcar (lambda (x) (concat x "<>\"\""))
+ (list org-contacts-email-property
+ org-contacts-alias-property
+ org-contacts-tel-property
+ org-contacts-address-property
+ org-contacts-birthday-property))
+ "|")
+ "Matching rule for finding heading that are contacts.
+This can be a tag name, or a property check."
+ :type 'string
+ :group 'org-contacts)
+
+(defcustom org-contacts-email-link-description-format "%s (%d)"
+ "Format used to store links to email.
+This overrides `org-email-link-description-format' if set."
+ :group 'org-contacts
+ :type 'string)
+
+(defcustom org-contacts-vcard-file "contacts.vcf"
+ "Default file for vcard export."
+ :group 'org-contacts
+ :type 'file)
+
+(defcustom org-contacts-enable-completion t
+ "Enable or not the completion in `message-mode' with `org-contacts'."
+ :group 'org-contacts
+ :type 'boolean)
+
+(defcustom org-contacts-complete-functions
+ '(org-contacts-complete-group org-contacts-complete-tags-props org-contacts-complete-name)
+ "List of functions used to complete contacts in `message-mode'."
+ :group 'org-contacts
+ :type 'hook)
+
+;; Decalre external functions and variables
+(declare-function org-reverse-string "org")
+(declare-function diary-ordinal-suffix "ext:diary-lib")
+(declare-function wl-summary-message-number "ext:wl-summary")
+(declare-function wl-address-header-extract-address "ext:wl-address")
+(declare-function wl-address-header-extract-realname "ext:wl-address")
+(declare-function erc-buffer-list "ext:erc")
+(declare-function erc-get-channel-user-list "ext:erc")
+(declare-function google-maps-static-show "ext:google-maps-static")
+(declare-function elmo-message-field "ext:elmo-pipe")
+(declare-function std11-narrow-to-header "ext:std11")
+(declare-function std11-fetch-field "ext:std11")
+
+(defconst org-contacts-property-values-separators "[,; \f\t\n\r\v]+"
+ "The default value of separators for `org-contacts-split-property'.
+
+A regexp matching strings of whitespace, `,' and `;'.")
+
+(defvar org-contacts-keymap
+ (let ((map (make-sparse-keymap)))
+ (define-key map "M" 'org-contacts-view-send-email)
+ (define-key map "i" 'org-contacts-view-switch-to-irc-buffer)
+ map)
+ "The keymap used in `org-contacts' result list.")
+
+(defvar org-contacts-db nil
+ "Org Contacts database.")
+
+(defvar org-contacts-last-update nil
+ "Last time the Org Contacts database has been updated.")
+
+(defun org-contacts-files ()
+ "Return list of Org files to use for contact management."
+ (or org-contacts-files (org-agenda-files t 'ifmode)))
+
+(defun org-contacts-db-need-update-p ()
+ "Determine whether `org-contacts-db' needs to be refreshed."
+ (or (null org-contacts-last-update)
+ (cl-find-if (lambda (file)
+ (or (time-less-p org-contacts-last-update
+ (elt (file-attributes file) 5))))
+ (org-contacts-files))
+ (org-contacts-db-has-dead-markers-p org-contacts-db)))
+
+(defun org-contacts-db-has-dead-markers-p (org-contacts-db)
+ "Returns t if at least one dead marker is found in
+ORG-CONTACTS-DB. A dead marker in this case is a marker pointing
+to dead or no buffer."
+ ;; Scan contacts list looking for dead markers, and return t at first found.
+ (catch 'dead-marker-found
+ (while org-contacts-db
+ (unless (marker-buffer (nth 1 (car org-contacts-db)))
+ (throw 'dead-marker-found t))
+ (setq org-contacts-db (cdr org-contacts-db)))
+ nil))
+
+(defun org-contacts-db ()
+ "Return the latest Org Contacts Database."
+ (let* ((org--matcher-tags-todo-only nil)
+ (contacts-matcher (cdr (org-make-tags-matcher org-contacts-matcher)))
+ result)
+ (when (org-contacts-db-need-update-p)
+ (let ((progress-reporter
+ (make-progress-reporter "Updating Org Contacts Database..." 0 (length org-contacts-files)))
+ (i 0))
+ (dolist (file (org-contacts-files))
+ (if (catch 'nextfile
+ ;; if file doesn't exist and the user agrees to removing it
+ ;; from org-agendas-list, 'nextfile is thrown. Catch it here
+ ;; and skip processing the file.
+ ;;
+ ;; TODO: suppose that the user has set an org-contacts-files
+ ;; list that contains an element that doesn't exist in the
+ ;; file system: in that case, the org-agenda-files list could
+ ;; be updated (and saved to the customizations of the user) if
+ ;; it contained the same file even though the org-agenda-files
+ ;; list wasn't actually used. I don't think it is normal that
+ ;; org-contacts updates org-agenda-files in this case, but
+ ;; short of duplicating org-check-agenda-files and
+ ;; org-remove-files, I don't know how to avoid it.
+ ;;
+ ;; A side effect of the TODO is that the faulty
+ ;; org-contacts-files list never gets updated and thus the
+ ;; user is always queried about the missing files when
+ ;; org-contacts-db-need-update-p returns true.
+ (org-check-agenda-file file))
+ (message "Skipped %s removed from org-agenda-files list."
+ (abbreviate-file-name file))
+ (with-current-buffer (org-get-agenda-file-buffer file)
+ (unless (eq major-mode 'org-mode)
+ (error "File %s is not in `org-mode'" file))
+ (setf result
+ (append result
+ (org-scan-tags 'org-contacts-at-point
+ contacts-matcher
+ org--matcher-tags-todo-only)))))
+ (progress-reporter-update progress-reporter (setq i (1+ i))))
+ (setf org-contacts-db result
+ org-contacts-last-update (current-time))
+ (progress-reporter-done progress-reporter)))
+ org-contacts-db))
+
+(defun org-contacts-at-point (&optional pom)
+ "Return the contacts at point-or-marker POM or current position
+if nil."
+ (setq pom (or pom (point)))
+ (org-with-point-at pom
+ (list (org-get-heading t) (set-marker (make-marker) pom) (org-entry-properties pom 'all))))
+
+(defun org-contacts-filter (&optional name-match tags-match prop-match)
+ "Search for a contact matching any of NAME-MATCH, TAGS-MATCH, PROP-MATCH.
+If all match values are nil, return all contacts.
+
+The optional PROP-MATCH argument is a single (PROP . VALUE) cons
+cell corresponding to the contact properties.
+"
+ (if (and (null name-match)
+ (null prop-match)
+ (null tags-match))
+ (org-contacts-db)
+ (cl-loop for contact in (org-contacts-db)
+ if (or
+ (and name-match
+ (string-match-p name-match
+ (first contact)))
+ (and prop-match
+ (cl-find-if (lambda (prop)
+ (and (string= (car prop-match) (car prop))
+ (string-match-p (cdr prop-match) (cdr prop))))
+ (caddr contact)))
+ (and tags-match
+ (cl-find-if (lambda (tag)
+ (string-match-p tags-match tag))
+ (org-split-string
+ (or (cdr (assoc-string "ALLTAGS" (caddr contact))) "") ":"))))
+ collect contact)))
+
+(when (not (fboundp 'completion-table-case-fold))
+ ;; That function is new in Emacs 24...
+ (defun completion-table-case-fold (table &optional dont-fold)
+ (lambda (string pred action)
+ (let ((completion-ignore-case (not dont-fold)))
+ (complete-with-action action table string pred)))))
+
+(defun org-contacts-try-completion-prefix (to-match collection &optional predicate)
+ "Custom implementation of `try-completion'.
+This version works only with list and alist and it looks at all
+prefixes rather than just the beginning of the string."
+ (cl-loop with regexp = (concat "\\b" (regexp-quote to-match))
+ with ret = nil
+ with ret-start = nil
+ with ret-end = nil
+
+ for el in collection
+ for string = (if (listp el) (car el) el)
+
+ for start = (when (or (null predicate) (funcall predicate string))
+ (string-match regexp string))
+
+ if start
+ do (let ((end (match-end 0))
+ (len (length string)))
+ (if (= end len)
+ (cl-return t)
+ (cl-destructuring-bind (string start end)
+ (if (null ret)
+ (values string start end)
+ (org-contacts-common-substring
+ ret ret-start ret-end
+ string start end))
+ (setf ret string
+ ret-start start
+ ret-end end))))
+
+ finally (cl-return
+ (replace-regexp-in-string "\\`[ \t\n]*" "" ret))))
+
+(defun org-contacts-compare-strings (s1 start1 end1 s2 start2 end2 &optional ignore-case)
+ "Compare the contents of two strings, using `compare-strings'.
+
+This function works like `compare-strings' excepted that it
+returns a cons.
+- The CAR is the number of characters that match at the beginning.
+- The CDR is T is the two strings are the same and NIL otherwise."
+ (let ((ret (compare-strings s1 start1 end1 s2 start2 end2 ignore-case)))
+ (if (eq ret t)
+ (cons (or end1 (length s1)) t)
+ (cons (1- (abs ret)) nil))))
+
+(defun org-contacts-common-substring (s1 start1 end1 s2 start2 end2)
+ "Extract the common substring between S1 and S2.
+
+This function extracts the common substring between S1 and S2 and
+adjust the part that remains common.
+
+START1 and END1 delimit the part in S1 that we know is common
+between the two strings. This applies to START2 and END2 for S2.
+
+This function returns a list whose contains:
+- The common substring found.
+- The new value of the start of the known inner substring.
+- The new value of the end of the known inner substring."
+ ;; Given two strings:
+ ;; s1: "foo bar baz"
+ ;; s2: "fooo bar baz"
+ ;; and the inner substring is "bar"
+ ;; then: start1 = 4, end1 = 6, start2 = 5, end2 = 7
+ ;;
+ ;; To find the common substring we will compare two substrings:
+ ;; " oof" and " ooof" to find the beginning of the common substring.
+ ;; " baz" and " baz" to find the end of the common substring.
+ (let* ((len1 (length s1))
+ (start1 (or start1 0))
+ (end1 (or end1 len1))
+
+ (len2 (length s2))
+ (start2 (or start2 0))
+ (end2 (or end2 len2))
+
+ (new-start (car (org-contacts-compare-strings
+ (substring (org-reverse-string s1) (- len1 start1)) nil nil
+ (substring (org-reverse-string s2) (- len2 start2)) nil nil)))
+
+ (new-end (+ end1 (car (org-contacts-compare-strings
+ (substring s1 end1) nil nil
+ (substring s2 end2) nil nil)))))
+ (list (substring s1 (- start1 new-start) new-end)
+ new-start
+ (+ new-start (- end1 start1)))))
+
+(defun org-contacts-all-completions-prefix (to-match collection &optional predicate)
+ "Custom version of `all-completions'.
+This version works only with list and alist and it looks at all
+prefixes rather than just the beginning of the string."
+ (cl-loop with regexp = (concat "\\b" (regexp-quote to-match))
+ for el in collection
+ for string = (if (listp el) (car el) el)
+ for match? = (when (and (or (null predicate) (funcall predicate string)))
+ (string-match regexp string))
+ if match?
+ collect (progn
+ (let ((end (match-end 0)))
+ (org-no-properties string)
+ (when (< end (length string))
+ ;; Here we add a text property that will be used
+ ;; later to highlight the character right after
+ ;; the common part between each addresses.
+ ;; See `org-contacts-display-sort-function'.
+ (put-text-property end (1+ end) 'org-contacts-prefix 't string)))
+ string)))
+
+(defun org-contacts-make-collection-prefix (collection)
+ "Make a collection function from COLLECTION which will match on prefixes."
+ (lexical-let ((collection collection))
+ (lambda (string predicate flag)
+ (cond ((eq flag nil)
+ (org-contacts-try-completion-prefix string collection predicate))
+ ((eq flag t)
+ ;; `org-contacts-all-completions-prefix' has already been
+ ;; used to compute `all-completions'.
+ collection)
+ ((eq flag 'lambda)
+ (org-contacts-test-completion-prefix string collection predicate))
+ ((and (listp flag) (eq (car flag) 'boundaries))
+ (cl-destructuring-bind (to-ignore &rest suffix)
+ flag
+ (org-contacts-boundaries-prefix string collection predicate suffix)))
+ ((eq flag 'metadata)
+ (org-contacts-metadata-prefix string collection predicate))
+ (t nil ; operation unsupported
+ )))))
+
+(defun org-contacts-display-sort-function (completions)
+ "Sort function for contacts display."
+ (mapcar (lambda (string)
+ (cl-loop with len = (1- (length string))
+ for i upfrom 0 to len
+ if (memq 'org-contacts-prefix
+ (text-properties-at i string))
+ do (set-text-properties
+ i (1+ i)
+ (list 'font-lock-face
+ (if (char-equal (aref string i)
+ (string-to-char " "))
+ ;; Spaces can't be bold.
+ 'underline
+ 'bold)) string)
+ else
+ do (set-text-properties i (1+ i) nil string)
+ finally (cl-return string)))
+ completions))
+
+(defun org-contacts-test-completion-prefix (string collection predicate)
+ (cl-find-if (lambda (el)
+ (and (or (null predicate) (funcall predicate el))
+ (string= string el)))
+ collection))
+
+(defun org-contacts-boundaries-prefix (string collection predicate suffix)
+ (list* 'boundaries (completion-boundaries string collection predicate suffix)))
+
+(defun org-contacts-metadata-prefix (string collection predicate)
+ '(metadata .
+ ((cycle-sort-function . org-contacts-display-sort-function)
+ (display-sort-function . org-contacts-display-sort-function))))
+
+(defun org-contacts-complete-group (start end string)
+ "Complete text at START from a group.
+
+A group FOO is composed of contacts with the tag FOO."
+ (let* ((completion-ignore-case org-contacts-completion-ignore-case)
+ (group-completion-p (string-match-p
+ (concat "^" org-contacts-group-prefix) string)))
+ (when group-completion-p
+ (let ((completion-list
+ (all-completions
+ string
+ (mapcar (lambda (group)
+ (propertize (concat org-contacts-group-prefix group)
+ 'org-contacts-group group))
+ (org-uniquify
+ (cl-loop for contact in (org-contacts-filter)
+ nconc (org-split-string
+ (or (cdr (assoc-string "ALLTAGS" (caddr contact))) "") ":")))))))
+ (list start end
+ (if (= (length completion-list) 1)
+ ;; We've found the correct group, returns the address
+ (lexical-let ((tag (get-text-property 0 'org-contacts-group
+ (car completion-list))))
+ (lambda (string pred &optional to-ignore)
+ (mapconcat 'identity
+ (cl-loop for contact in (org-contacts-filter
+ nil
+ tag)
+ ;; The contact name is always the car of the assoc-list
+ ;; returned by `org-contacts-filter'.
+ for contact-name = (car contact)
+ ;; Grab the first email of the contact
+ for email = (org-contacts-strip-link
+ (or (car (org-contacts-split-property
+ (or
+ (cdr (assoc-string org-contacts-email-property
+ (cl-caddr contact)))
+ ""))) ""))
+ ;; If the user has an email address, append USER <EMAIL>.
+ if email collect (org-contacts-format-email contact-name email))
+ ", ")))
+ ;; We haven't found the correct group
+ (completion-table-case-fold completion-list
+ (not org-contacts-completion-ignore-case))))))))
+
+(defun org-contacts-complete-tags-props (start end string)
+ "Insert emails that match the tags expression.
+
+For example: FOO-BAR will match entries tagged with FOO but not
+with BAR.
+
+See (org) Matching tags and properties for a complete
+description."
+ (let* ((completion-ignore-case org-contacts-completion-ignore-case)
+ (completion-p (string-match-p
+ (concat "^" org-contacts-tags-props-prefix) string)))
+ (when completion-p
+ (let ((result
+ (mapconcat
+ 'identity
+ (cl-loop for contact in (org-contacts-db)
+ for contact-name = (car contact)
+ for email = (org-contacts-strip-link (or (car (org-contacts-split-property
+ (or
+ (cdr (assoc-string org-contacts-email-property
+ (cl-caddr contact)))
+ ""))) ""))
+ for tags = (cdr (assoc "TAGS" (nth 2 contact)))
+ for tags-list = (if tags
+ (split-string (substring (cdr (assoc "TAGS" (nth 2 contact))) 1 -1) ":")
+ '())
+ for marker = (nth 1 contact)
+ if (with-current-buffer (marker-buffer marker)
+ (save-excursion
+ (goto-char marker)
+ (let (todo-only)
+ (eval (cdr (org-make-tags-matcher (cl-subseq string 1)))))))
+ collect (org-contacts-format-email contact-name email))
+ ",")))
+ (when (not (string= "" result))
+ ;; return (start end function)
+ (lexical-let* ((to-return result))
+ (list start end
+ (lambda (string pred &optional to-ignore) to-return))))))))
+
+(defun org-contacts-remove-ignored-property-values (ignore-list list)
+ "Remove all ignore-list's elements from list and you can use
+ regular expressions in the ignore list."
+ (cl-remove-if (lambda (el)
+ (cl-find-if (lambda (x)
+ (string-match-p x el))
+ ignore-list))
+ list))
+
+(defun org-contacts-complete-name (start end string)
+ "Complete text at START with a user name and email."
+ (let* ((completion-ignore-case org-contacts-completion-ignore-case)
+ (completion-list
+ (cl-loop for contact in (org-contacts-filter)
+ ;; The contact name is always the car of the assoc-list
+ ;; returned by `org-contacts-filter'.
+ for contact-name = (car contact)
+
+ ;; Build the list of the email addresses which has
+ ;; been expired
+ for ignore-list = (org-contacts-split-property
+ (or (cdr (assoc-string org-contacts-ignore-property
+ (nth 2 contact))) ""))
+ ;; Build the list of the user email addresses.
+ for email-list = (org-contacts-remove-ignored-property-values
+ ignore-list
+ (org-contacts-split-property
+ (or (cdr (assoc-string org-contacts-email-property
+ (nth 2 contact))) "")))
+ ;; If the user has email addresses…
+ if email-list
+ ;; … append a list of USER <EMAIL>.
+ nconc (cl-loop for email in email-list
+ collect (org-contacts-format-email contact-name (org-contacts-strip-link email)))))
+ (completion-list (org-contacts-all-completions-prefix
+ string
+ (org-uniquify completion-list))))
+ (when completion-list
+ (list start end
+ (org-contacts-make-collection-prefix completion-list)))))
+
+(defun org-contacts-message-complete-function (&optional start)
+ "Function used in `completion-at-point-functions' in `message-mode'."
+ ;; Avoid to complete in `post-command-hook'.
+ (when completion-in-region-mode
+ (remove-hook 'post-command-hook #'completion-in-region--postch))
+ (let ((mail-abbrev-mode-regexp
+ "^\\(Resent-To\\|To\\|B?Cc\\|Reply-To\\|From\\|Mail-Followup-To\\|Mail-Copies-To\\|Disposition-Notification-To\\|Return-Receipt-To\\):"))
+ (when (mail-abbrev-in-expansion-header-p)
+ (lexical-let*
+ ((end (point))
+ (start (or start
+ (save-excursion
+ (re-search-backward "\\(\\`\\|[\n:,]\\)[ \t]*")
+ (goto-char (match-end 0))
+ (point))))
+ (string (buffer-substring start end)))
+ (run-hook-with-args-until-success
+ 'org-contacts-complete-functions start end string)))))
+
+(defun org-contacts-gnus-get-name-email ()
+ "Get name and email address from Gnus message."
+ (if (gnus-alive-p)
+ (gnus-with-article-headers
+ (mail-extract-address-components
+ (or (mail-fetch-field "From") "")))))
+
+(defun org-contacts-gnus-article-from-get-marker ()
+ "Return a marker for a contact based on From."
+ (let* ((address (org-contacts-gnus-get-name-email))
+ (name (car address))
+ (email (cadr address)))
+ (cl-cadar (or (org-contacts-filter
+ nil
+ nil
+ (cons org-contacts-email-property (concat "\\b" (regexp-quote email) "\\b")))
+ (when name
+ (org-contacts-filter
+ (concat "^" name "$")))))))
+
+(defun org-contacts-gnus-article-from-goto ()
+ "Go to contact in the From address of current Gnus message."
+ (interactive)
+ (let ((marker (org-contacts-gnus-article-from-get-marker)))
+ (when marker
+ (switch-to-buffer-other-window (marker-buffer marker))
+ (goto-char marker)
+ (when (eq major-mode 'org-mode) (org-show-context 'agenda)))))
+
+(with-no-warnings (defvar date)) ;; unprefixed, from calendar.el
+(defun org-contacts-anniversaries (&optional field format)
+ "Compute FIELD anniversary for each contact, returning FORMAT.
+Default FIELD value is \"BIRTHDAY\".
+
+Format is a string matching the following format specification:
+
+ %h - Heading name
+ %l - Link to the heading
+ %y - Number of year
+ %Y - Number of year (ordinal)"
+ (let ((calendar-date-style 'ISO)
+ (entry ""))
+ (unless format (setq format org-contacts-birthday-format))
+ (cl-loop for contact in (org-contacts-filter)
+ for anniv = (let ((anniv (cdr (assoc-string
+ (or field org-contacts-birthday-property)
+ (nth 2 contact)))))
+ (when anniv
+ (calendar-gregorian-from-absolute
+ (org-time-string-to-absolute anniv))))
+ ;; Use `diary-anniversary' to compute anniversary.
+ if (and anniv (apply 'diary-anniversary anniv))
+ collect (format-spec format
+ `((?l . ,(org-with-point-at (cadr contact) (org-store-link nil)))
+ (?h . ,(car contact))
+ (?y . ,(- (calendar-extract-year date)
+ (calendar-extract-year anniv)))
+ (?Y . ,(let ((years (- (calendar-extract-year date)
+ (calendar-extract-year anniv))))
+ (format "%d%s" years (diary-ordinal-suffix years)))))))))
+
+(defun org-completing-read-date (prompt collection
+ &optional predicate require-match initial-input
+ hist def inherit-input-method)
+ "Like `completing-read' but reads a date.
+Only PROMPT and DEF are really used."
+ (org-read-date nil nil nil prompt nil def))
+
+(add-to-list 'org-property-set-functions-alist
+ `(,org-contacts-birthday-property . org-completing-read-date))
+
+(defun org-contacts-template-name (&optional return-value)
+ "Try to return the contact name for a template.
+If not found return RETURN-VALUE or something that would ask the user."
+ (or (car (org-contacts-gnus-get-name-email))
+ return-value
+ "%^{Name}"))
+
+(defun org-contacts-template-email (&optional return-value)
+ "Try to return the contact email for a template.
+If not found return RETURN-VALUE or something that would ask the user."
+ (or (cadr (org-contacts-gnus-get-name-email))
+ return-value
+ (concat "%^{" org-contacts-email-property "}p")))
+
+(defun org-contacts-gnus-store-last-mail ()
+ "Store a link between mails and contacts.
+
+This function should be called from `gnus-article-prepare-hook'."
+ (let ((marker (org-contacts-gnus-article-from-get-marker)))
+ (when marker
+ (with-current-buffer (marker-buffer marker)
+ (save-excursion
+ (goto-char marker)
+ (let* ((org-email-link-description-format (or org-contacts-email-link-description-format
+ org-email-link-description-format))
+ (link (gnus-with-article-buffer (org-store-link nil))))
+ (org-set-property org-contacts-last-read-mail-property link)))))))
+
+(defun org-contacts-icon-as-string ()
+ "Return the contact icon as a string."
+ (let ((image (org-contacts-get-icon)))
+ (concat
+ (propertize "-" 'display
+ (append
+ (if image
+ image
+ `'(space :width (,org-contacts-icon-size)))
+ '(:ascent center)))
+ " ")))
+
+;;;###autoload
+(defun org-contacts (name)
+ "Create agenda view for contacts matching NAME."
+ (interactive (list (read-string "Name: ")))
+ (let ((org-agenda-files (org-contacts-files))
+ (org-agenda-skip-function
+ (lambda () (org-agenda-skip-if nil `(notregexp ,name))))
+ (org-agenda-prefix-format (propertize
+ "%(org-contacts-icon-as-string)% s%(org-contacts-irc-number-of-unread-messages) "
+ 'keymap org-contacts-keymap))
+ (org-agenda-overriding-header
+ (or org-agenda-overriding-header
+ (concat "List of contacts matching `" name "':"))))
+ (setq org-agenda-skip-regexp name)
+ (org-tags-view nil org-contacts-matcher)
+ (with-current-buffer org-agenda-buffer-name
+ (setq org-agenda-redo-command
+ (list 'org-contacts name)))))
+
+(defun org-contacts-completing-read (prompt
+ &optional predicate
+ initial-input hist def inherit-input-method)
+ "Call `completing-read' with contacts name as collection."
+ (org-completing-read
+ prompt (org-contacts-filter) predicate t initial-input hist def inherit-input-method))
+
+(defun org-contacts-format-name (name)
+ "Trim any local formatting to get a bare NAME."
+ ;; Remove radio targets characters
+ (replace-regexp-in-string org-radio-target-regexp "\\1" name))
+
+(defun org-contacts-format-email (name email)
+ "Format an EMAIL address corresponding to NAME."
+ (unless email
+ (error "`email' cannot be nul"))
+ (if name
+ (concat (org-contacts-format-name name) " <" email ">")
+ email))
+
+(defun org-contacts-check-mail-address (mail)
+ "Add MAIL address to contact at point if it does not have it."
+ (let ((mails (org-entry-get (point) org-contacts-email-property)))
+ (unless (member mail (split-string mails))
+ (when (yes-or-no-p
+ (format "Do you want to add this address to %s?" (org-get-heading t)))
+ (org-set-property org-contacts-email-property (concat mails " " mail))))))
+
+(defun org-contacts-gnus-check-mail-address ()
+ "Check that contact has the current address recorded.
+This function should be called from `gnus-article-prepare-hook'."
+ (let ((marker (org-contacts-gnus-article-from-get-marker)))
+ (when marker
+ (org-with-point-at marker
+ (org-contacts-check-mail-address (cadr (org-contacts-gnus-get-name-email)))))))
+
+(defun org-contacts-gnus-insinuate ()
+ "Add some hooks for Gnus user.
+This adds `org-contacts-gnus-check-mail-address' and
+`org-contacts-gnus-store-last-mail' to
+`gnus-article-prepare-hook'. It also adds a binding on `;' in
+`gnus-summary-mode-map' to `org-contacts-gnus-article-from-goto'"
+ (require 'gnus)
+ (require 'gnus-art)
+ (define-key gnus-summary-mode-map ";" 'org-contacts-gnus-article-from-goto)
+ (add-hook 'gnus-article-prepare-hook 'org-contacts-gnus-check-mail-address)
+ (add-hook 'gnus-article-prepare-hook 'org-contacts-gnus-store-last-mail))
+
+(defun org-contacts-setup-completion-at-point ()
+ "Add `org-contacts-message-complete-function' as a new function
+to complete the thing at point."
+ (add-to-list 'completion-at-point-functions
+ 'org-contacts-message-complete-function))
+
+(defun org-contacts-unload-hook ()
+ (remove-hook 'message-mode-hook 'org-contacts-setup-completion-at-point))
+
+(when (and org-contacts-enable-completion
+ (boundp 'completion-at-point-functions))
+ (add-hook 'message-mode-hook 'org-contacts-setup-completion-at-point))
+
+(defun org-contacts-wl-get-from-header-content ()
+ "Retrieve the content of the `From' header of an email.
+Works from wl-summary-mode and mime-view-mode - that is while viewing email.
+Depends on Wanderlust been loaded."
+ (with-current-buffer (org-capture-get :original-buffer)
+ (cond
+ ((eq major-mode 'wl-summary-mode) (when (and (boundp 'wl-summary-buffer-elmo-folder)
+ wl-summary-buffer-elmo-folder)
+ (elmo-message-field
+ wl-summary-buffer-elmo-folder
+ (wl-summary-message-number)
+ 'from)))
+ ((eq major-mode 'mime-view-mode) (std11-narrow-to-header)
+ (prog1
+ (std11-fetch-field "From")
+ (widen))))))
+
+(defun org-contacts-wl-get-name-email ()
+ "Get name and email address from Wanderlust email.
+See `org-contacts-wl-get-from-header-content' for limitations."
+ (let ((from (org-contacts-wl-get-from-header-content)))
+ (when from
+ (list (wl-address-header-extract-realname from)
+ (wl-address-header-extract-address from)))))
+
+(defun org-contacts-template-wl-name (&optional return-value)
+ "Try to return the contact name for a template from wl.
+If not found, return RETURN-VALUE or something that would ask the
+user."
+ (or (car (org-contacts-wl-get-name-email))
+ return-value
+ "%^{Name}"))
+
+(defun org-contacts-template-wl-email (&optional return-value)
+ "Try to return the contact email for a template from Wanderlust.
+If not found return RETURN-VALUE or something that would ask the user."
+ (or (cadr (org-contacts-wl-get-name-email))
+ return-value
+ (concat "%^{" org-contacts-email-property "}p")))
+
+(defun org-contacts-view-send-email (&optional ask)
+ "Send email to the contact at point.
+If ASK is set, ask for the email address even if there's only one
+address."
+ (interactive "P")
+ (let ((marker (org-get-at-bol 'org-hd-marker)))
+ (org-with-point-at marker
+ (let ((emails (org-entry-get (point) org-contacts-email-property)))
+ (if emails
+ (let ((email-list (org-contacts-split-property emails)))
+ (if (and (= (length email-list) 1) (not ask))
+ (compose-mail (org-contacts-format-email
+ (org-get-heading t) emails))
+ (let ((email (completing-read "Send mail to which address: " email-list)))
+ (setq email (org-contacts-strip-link email))
+ (org-contacts-check-mail-address email)
+ (compose-mail (org-contacts-format-email (org-get-heading t) email)))))
+ (error (format "This contact has no mail address set (no %s property)"
+ org-contacts-email-property)))))))
+
+(defun org-contacts-get-icon (&optional pom)
+ "Get icon for contact at POM."
+ (setq pom (or pom (point)))
+ (catch 'icon
+ ;; Use `org-contacts-icon-property'
+ (let ((image-data (org-entry-get pom org-contacts-icon-property)))
+ (when image-data
+ (throw 'icon
+ (if (fboundp 'gnus-rescale-image)
+ (gnus-rescale-image (create-image image-data)
+ (cons org-contacts-icon-size org-contacts-icon-size))
+ (create-image image-data)))))
+ ;; Next, try Gravatar
+ (when org-contacts-icon-use-gravatar
+ (let* ((gravatar-size org-contacts-icon-size)
+ (email-list (org-entry-get pom org-contacts-email-property))
+ (gravatar
+ (when email-list
+ (loop for email in (org-contacts-split-property email-list)
+ for gravatar = (gravatar-retrieve-synchronously (org-contacts-strip-link email))
+ if (and gravatar
+ (not (eq gravatar 'error)))
+ return gravatar))))
+ (when gravatar (throw 'icon gravatar))))))
+
+(defun org-contacts-irc-buffer (&optional pom)
+ "Get the IRC buffer associated with the entry at POM."
+ (setq pom (or pom (point)))
+ (let ((nick (org-entry-get pom org-contacts-nickname-property)))
+ (when nick
+ (let ((buffer (get-buffer nick)))
+ (when buffer
+ (with-current-buffer buffer
+ (when (eq major-mode 'erc-mode)
+ buffer)))))))
+
+(defun org-contacts-irc-number-of-unread-messages (&optional pom)
+ "Return the number of unread messages for contact at POM."
+ (when (boundp 'erc-modified-channels-alist)
+ (let ((number (cadr (assoc (org-contacts-irc-buffer pom) erc-modified-channels-alist))))
+ (if number
+ (format (concat "%3d unread message" (if (> number 1) "s" " ") " ") number)
+ (make-string 21 ? )))))
+
+(defun org-contacts-view-switch-to-irc-buffer ()
+ "Switch to the IRC buffer of the current contact if it has one."
+ (interactive)
+ (let ((marker (org-get-at-bol 'org-hd-marker)))
+ (org-with-point-at marker
+ (switch-to-buffer-other-window (org-contacts-irc-buffer)))))
+
+(defun org-contacts-completing-read-nickname (prompt collection
+ &optional predicate require-match initial-input
+ hist def inherit-input-method)
+ "Like `completing-read' but reads a nickname."
+ (org-completing-read prompt (append collection (erc-nicknames-list)) predicate require-match
+ initial-input hist def inherit-input-method))
+
+(defun erc-nicknames-list ()
+ "Return all nicknames of all ERC buffers."
+ (loop for buffer in (erc-buffer-list)
+ nconc (with-current-buffer buffer
+ (loop for user-entry in (mapcar 'car (erc-get-channel-user-list))
+ collect (elt user-entry 1)))))
+
+(add-to-list 'org-property-set-functions-alist
+ `(,org-contacts-nickname-property . org-contacts-completing-read-nickname))
+
+(defun org-contacts-vcard-escape (str)
+ "Escape ; , and \n in STR for the VCard format."
+ ;; Thanks to this library for the regexp:
+ ;; http://www.emacswiki.org/cgi-bin/wiki/bbdb-vcard-export.el
+ (when str
+ (replace-regexp-in-string
+ "\n" "\\\\n"
+ (replace-regexp-in-string "\\(;\\|,\\|\\\\\\)" "\\\\\\1" str))))
+
+(defun org-contacts-vcard-encode-name (name)
+ "Try to encode NAME as VCard's N property.
+The N property expects
+
+ FamilyName;GivenName;AdditionalNames;Prefix;Postfix.
+
+Org-contacts does not specify how to encode the name. So we try
+to do our best."
+ (concat (replace-regexp-in-string "\\(\\w+\\) \\(.*\\)" "\\2;\\1" name) ";;;"))
+
+(defun org-contacts-vcard-format (contact)
+ "Formats CONTACT in VCard 3.0 format."
+ (let* ((properties (nth 2 contact))
+ (name (org-contacts-vcard-escape (car contact)))
+ (n (org-contacts-vcard-encode-name name))
+ (email (cdr (assoc-string org-contacts-email-property properties)))
+ (tel (cdr (assoc-string org-contacts-tel-property properties)))
+ (ignore-list (cdr (assoc-string org-contacts-ignore-property properties)))
+ (ignore-list (when ignore-list
+ (org-contacts-split-property ignore-list)))
+ (note (cdr (assoc-string org-contacts-note-property properties)))
+ (bday (org-contacts-vcard-escape (cdr (assoc-string org-contacts-birthday-property properties))))
+ (addr (cdr (assoc-string org-contacts-address-property properties)))
+ (nick (org-contacts-vcard-escape (cdr (assoc-string org-contacts-nickname-property properties))))
+ (head (format "BEGIN:VCARD\nVERSION:3.0\nN:%s\nFN:%s\n" n name))
+ emails-list result phones-list)
+ (concat head
+ (when email (progn
+ (setq emails-list (org-contacts-remove-ignored-property-values ignore-list (org-contacts-split-property email)))
+ (setq result "")
+ (while emails-list
+ (setq result (concat result "EMAIL:" (org-contacts-strip-link (car emails-list)) "\n"))
+ (setq emails-list (cdr emails-list)))
+ result))
+ (when addr
+ (format "ADR:;;%s\n" (replace-regexp-in-string "\\, ?" ";" addr)))
+ (when tel (progn
+ (setq phones-list (org-contacts-remove-ignored-property-values ignore-list (org-contacts-split-property tel)))
+ (setq result "")
+ (while phones-list
+ (setq result (concat result "TEL:" (org-contacts-strip-link (org-link-unescape (car phones-list))) "\n"))
+ (setq phones-list (cdr phones-list)))
+ result))
+ (when bday
+ (let ((cal-bday (calendar-gregorian-from-absolute (org-time-string-to-absolute bday))))
+ (format "BDAY:%04d-%02d-%02d\n"
+ (calendar-extract-year cal-bday)
+ (calendar-extract-month cal-bday)
+ (calendar-extract-day cal-bday))))
+ (when nick (format "NICKNAME:%s\n" nick))
+ (when note (format "NOTE:%s\n" note))
+ "END:VCARD\n\n")))
+
+(defun org-contacts-export-as-vcard (&optional name file to-buffer)
+ "Export org contacts to V-Card 3.0.
+
+By default, all contacts are exported to `org-contacts-vcard-file'.
+
+When NAME is \\[universal-argument], prompts for a contact name.
+
+When NAME is \\[universal-argument] \\[universal-argument],
+prompts for a contact name and a file name where to export.
+
+When NAME is \\[universal-argument] \\[universal-argument]
+\\[universal-argument], prompts for a contact name and a buffer where to export.
+
+If the function is not called interactively, all parameters are
+passed to `org-contacts-export-as-vcard-internal'."
+ (interactive "P")
+ (when (called-interactively-p 'any)
+ (cl-psetf name
+ (when name
+ (read-string "Contact name: "
+ (nth 0 (org-contacts-at-point))))
+ file
+ (when (equal name '(16))
+ (read-file-name "File: " nil org-contacts-vcard-file))
+ to-buffer
+ (when (equal name '(64))
+ (read-buffer "Buffer: "))))
+ (org-contacts-export-as-vcard-internal name file to-buffer))
+
+(defun org-contacts-export-as-vcard-internal (&optional name file to-buffer)
+ "Export all contacts matching NAME as VCard 3.0.
+If TO-BUFFER is nil, the content is written to FILE or
+`org-contacts-vcard-file'. If TO-BUFFER is non-nil, the buffer
+is created and the VCard is written into that buffer."
+ (let* ((filename (or file org-contacts-vcard-file))
+ (buffer (if to-buffer
+ (get-buffer-create to-buffer)
+ (find-file-noselect filename))))
+ (message "Exporting...")
+ (set-buffer buffer)
+ (let ((inhibit-read-only t)) (erase-buffer))
+ (fundamental-mode)
+ (when (fboundp 'set-buffer-file-coding-system)
+ (set-buffer-file-coding-system coding-system-for-write))
+ (loop for contact in (org-contacts-filter name)
+ do (insert (org-contacts-vcard-format contact)))
+ (if to-buffer
+ (current-buffer)
+ (progn (save-buffer) (kill-buffer)))))
+
+(defun org-contacts-show-map (&optional name)
+ "Show contacts on a map.
+Requires google-maps-el."
+ (interactive)
+ (unless (fboundp 'google-maps-static-show)
+ (error "`org-contacts-show-map' requires `google-maps-el'"))
+ (google-maps-static-show
+ :markers
+ (cl-loop
+ for contact in (org-contacts-filter name)
+ for addr = (cdr (assoc-string org-contacts-address-property (nth 2 contact)))
+ if addr
+ collect (cons (list addr) (list :label (string-to-char (car contact)))))))
+
+(defun org-contacts-strip-link (link)
+ "Remove brackets, description, link type and colon from an org
+link string and return the pure link target."
+ (let (startpos colonpos endpos)
+ (setq startpos (string-match (regexp-opt '("[[tel:" "[[mailto:")) link))
+ (if startpos
+ (progn
+ (setq colonpos (string-match ":" link))
+ (setq endpos (string-match "\\]" link))
+ (if endpos (substring link (1+ colonpos) endpos) link))
+ (progn
+ (setq startpos (string-match "mailto:" link))
+ (setq colonpos (string-match ":" link))
+ (if startpos (substring link (1+ colonpos)) link)))))
+
+;; Add the link type supported by org-contacts-strip-link
+;; so everything is in order for its use in Org files
+(org-link-set-parameters "tel")
+
+(defun org-contacts-split-property (string &optional separators omit-nulls)
+ "Custom version of `split-string'.
+Split a property STRING into sub-strings bounded by matches
+for SEPARATORS but keep Org links intact.
+
+The beginning and end of STRING, and each match for SEPARATORS, are
+splitting points. The substrings matching SEPARATORS are removed, and
+the substrings between the splitting points are collected as a list,
+which is returned.
+
+If SEPARATORS is non-nil, it should be a regular expression
+matching text which separates, but is not part of, the
+substrings. If nil it defaults to `org-contacts-property-values-separators',
+normally \"[,; \f\t\n\r\v]+\", and OMIT-NULLS is forced to t.
+
+If OMIT-NULLS is t, zero-length substrings are omitted from the list \(so
+that for the default value of SEPARATORS leading and trailing whitespace
+are effectively trimmed). If nil, all zero-length substrings are retained."
+ (let* ((omit-nulls (if separators omit-nulls t))
+ (rexp (or separators org-contacts-property-values-separators))
+ (inputlist (split-string string rexp omit-nulls))
+ (linkstring "")
+ (bufferstring "")
+ (proplist (list "")))
+ (while inputlist
+ (setq bufferstring (pop inputlist))
+ (if (string-match "\\[\\[" bufferstring)
+ (progn
+ (setq linkstring (concat bufferstring " "))
+ (while (not (string-match "\\]\\]" bufferstring))
+ (setq bufferstring (pop inputlist))
+ (setq linkstring (concat linkstring bufferstring " ")))
+ (setq proplist (cons (org-trim linkstring) proplist)))
+ (setq proplist (cons bufferstring proplist))))
+ (cdr (reverse proplist))))
+
+(provide 'org-contacts)
+
+;;; org-contacts.el ends here