isamert.net
About Feeds

Managing your contacts in org-mode and syncing them to your phone (Android, iOS, whatever)


I store my contacts in an org file called people.org. The file has the following structure:

* John Doe
:PROPERTIES:
:ID: some-generated-uuid
:GROUP:    Work
:PHONE:    +1234567890
:ADDRESS_HOME: Foo bar street, no 5
:EMAIL:    john@doe.com
:END:
* Dohn Joe
:PROPERTIES:
:GROUP:    High school
:PHONE:    +1334567890
:END:
- Some notes about this person.

The nice part is, it's just plain org-mode. I only use top-level headings in this file, instead of creating header hierarchies. I utilize :GROUP: property to categorize people, this way a person may belong to multiple categories. I use org-ql if I need to find people related to one group or if I want to filter them based on some specific property.

However the main use case is that I reference these headers in my other org files. For example, I also keep my diary in org-mode and I may write about some event that I participated with John Doe from above. I simply reference (using org links) to that person. The benefit of this is being able to recollect all of your notes about a particular person using one simple search.

Anyway, lets jump how I synchronize these contact information with my phone.

Synchronization

I simply create .vcf file, a format that most of the contacts apps that are aware of, based on my people.org file. Then I synchronize this .vcf file to my phone, using Syncthing. The following snippet creates the .vcf file.

(defun isamert/build-contact-item (template-string contact-property)
  (if-let ((stuff (org-entry-get nil contact-property)))
      (concat (format template-string stuff) "\n")
    ""))

(defun isamert/vcard ()
  "Create a .vcf file containing all contact information."
  (interactive)
  (write-region
   (string-join
    (org-map-entries
     (lambda ()
       (string-join
        `("BEGIN:VCARD\nVERSION:2.1\n"
          ,(format "UID:urn:uuid:%s\n" (org-id-get nil t))
          ,(isamert/build-contact-item "FN:%s" "ITEM")
          ,(isamert/build-contact-item "TEL;CELL:%s" "PHONE")
          ,(isamert/build-contact-item "EMAIL:%s" "EMAIL")
          ,(isamert/build-contact-item "ORG:%s" "GROUP")
          ,(isamert/build-contact-item "ADR;HOME:;;%s" "ADDRESS_HOME")
          ,(isamert/build-contact-item "ADR;WORK:;;%s" "ADDRESS_WORK")
          ,(format "REV:%s\n" (format-time-string "%Y-%m-%dT%T"))
          "END:VCARD")
        ""))
     "LEVEL=1")
    "\n")
   nil
   (read-file-name
    "Where to save the .vcf file?"
    "~/Documents/sync/"
    "contacts.vcf")))

Simply call the isamert/vcard function in your people.org file and you get a .vcf file. By default, it creates the file under ~/Documents/sync. This folder is automatically synced with my phone using Syncthing. Then I open my contacts app and import the file. That's it.

I used to earliest possible .vcf format that is available so that every contacts app can import them. You can add/remove fields to your .vcf export quite easily, just take a look at this wikipedia page for vCard and the relevant line to your function.

Appendix: Interactively copy email of a contact from anywhere in Emacs

Here is an example, just to demonstrate how you obtain/copy email of one of your contacts interactively. A use case might be:

  • You open your mail client to send an email to John Doe.
  • You call isamert/contacts-select-email which presents you all of your contact's names.
  • You select one of your contacts, and their email gets copied into your kill-ring.
  • You paste that email into To: field of your email client.

Don't forget to point find-file-noselect to your people.org file.

(defun isamert/contacts-email-alist ()
  "Get an alist of contact name and emails."
  (seq-filter
   (lambda (it) it)
   (org-map-entries
    (lambda ()
      (when-let ((email (org-entry-get nil "EMAIL")))
        `(,(org-entry-get nil "ITEM") . ,email)))
    "LEVEL=1")))

(defun isamert/contacts-select-email ()
  "Search through your contacts interactively and copy their email."
  (interactive)
  (with-current-buffer (find-file-noselect "~/Documents/notes/people.org")
    (let ((email-alist (isamert/contacts-email-alist)))
      (kill-new
       (cdr
        (assoc
         (completing-read "Copy email of: " email-alist)
         email-alist))))))

Appendix

  • Reddit discussion
  • [2022-01-29 Sat] Added UID section to entries so that when you re-import your contacts after an update to the vcf file, already-existing contacts won't get duplicated.