New address book
I’ve had a kludgy mess of electronic address books for most of two decades, and have got rather fed up with it. My stack consisted of:
~/.mutt/aliases
, a flat text file consisting ofmutt
alias
commands- lbdb configuration to query
~/.mutt/aliases
, Debian’s LDAP database, and Canonical’s LDAP database, so that I can search by name with Ctrl-t inmutt
when composing a new message - Google Contacts, which I used from Android and was completely separate from all of the above
The biggest practical problem with this was that I had the address book that was most convenient for me to add things to (Google Contacts) and the one I used when sending email, and no sensible way to merge them or move things between them. I also wasn’t especially comfortable with having all my contact information in a proprietary web service.
My goals for a replacement address book system were:
- free software throughout
- storage under my control
- single common database
- minimal manual transcription when consolidating existing databases
- integration with Android such that I can continue using the same contacts, messaging, etc. apps
- integration with
mutt
such that I can continue using the same query interface - not having to write my own software, because honestly
I think I have all this now!
New stack
The obvious basic technology to use is
CardDAV: it’s fairly complex,
admittedly, but lots of software supports it and one of my goals was not
having to write my own thing. This meant I needed a CardDAV server, some
way to sync the database to and from both Android and the system where I run
mutt
, and whatever query glue was necessary to get mutt
to understand vCards.
There are lots of different alternatives here, and if anything the problem was an embarrassment of choice. In the end I just decided to go for things that looked roughly the right shape for me and tried not to spend too much time in analysis paralysis.
CardDAV server
I went with Xandikos for the server, largely because I know Jelmer and have generally had pretty good experiences with their software, but also because using Git for history of the backend storage seems like something my future self will thank me for.
It isn’t packaged in stretch, but it’s in Debian unstable, so I installed it from there.
Rather than the standalone mode suggested on the web page, I decided to set
it up in what felt like a more robust way using WSGI. I installed
gunicorn
and python3-gunicorn
, created the following file in
/etc/systemd/system/xandikos.socket
:
[Unit]
Description=Xandikos socket
[Socket]
ListenStream=/run/xandikos.socket
[Install]
WantedBy=sockets.target
… and the following file in /etc/systemd/system/xandikos.service
:
[Unit]
Description=Xandikos CalDAV/CardDAV server
Documentation=man:xandikos(1)
Requires=xandikos.socket
[Service]
User=xandikos
Group=xandikos
Restart=on-failure
ExecStart=/usr/bin/python3 /usr/bin/gunicorn --bind=unix:/run/xandikos.socket xandikos.wsgi:app
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
Environment=XANDIKOSPATH=/srv/xandikos/collections
ProtectSystem=strict
ProtectKernelTunables=yes
ProtectControlGroups=yes
PrivateDevices=yes
PrivateTmp=yes
ReadWritePaths=/run/xandikos.socket /srv/xandikos
The path (/srv/xandikos/collections
) was arbitrary. You need to create
the xandikos
user and group first (adduser --system --group
--no-create-home --disabled-login xandikos
). I created /srv/xandikos
owned by xandikos:xandikos
and mode 0700. You should also run sudo -u
xandikos xandikos -d /srv/xandikos/collections --autocreate
and then Ctrl-c
it after a short time (I think it would be nicer if there were a way to ask
the WSGI wrapper to do this). If you
aren’t using systemd then you can of course write equivalent init scripts instead.
For Apache setup, I kept it reasonably simple: I ran a2enmod proxy_http
,
used htpasswd
to create /etc/apache2/xandikos.passwd
with a username and
password for myself, added a virtual host in
/etc/apache2/sites-available/xandikos.conf
, and enabled it with a2ensite
xandikos
:
<VirtualHost *:443>
ServerName xandikos.example.org
ServerAdmin me@example.org
ErrorLog /var/log/apache2/xandikos-error.log
TransferLog /var/log/apache2/xandikos-access.log
<Location />
ProxyPass "unix:/run/xandikos.socket|http://xandikos.riva.dynamic.greenend.org.uk/"
AuthType Basic
AuthName "Xandikos"
AuthBasicProvider file
AuthUserFile "/etc/apache2/xandikos.passwd"
Require valid-user
</Location>
</VirtualHost>
You should of course adjust the ProxyPass
line to match your own deployment.
Then service apache2 reload
, set the new virtual host up with Let’s
Encrypt, reloaded again, and off we go.
Android integration
I installed DAVx⁵ from the Play Store: it cost a few pounds, but I was OK with that since it’s GPLv3 and I’m happy to help fund free software. I created two accounts, one for my existing Google Contacts database (and in fact calendaring as well, although I don’t intend to switch over to self-hosting that just yet), and one for the new Xandikos instance. The Google setup was a bit fiddly because I have two-step verification turned on so I had to create an app-specific password. The Xandikos setup was straightforward: base URL, username, password, and done.
Since I didn’t completely trust the new setup yet, I followed what seemed
like the most robust option from the DAVx⁵ contacts syncing
documentation,
and used the stock contacts app to export my Google Contacts account to a
.vcf
file and then import that into the appropriate DAVx⁵ account (which
showed up automatically). This seemed straightforward and everything got
pushed to Xandikos. There are some weird delays in syncing contacts that I
don’t entirely understand, but it all seems to get there in the end.
2019-06-13: Followed rename of DAVdroid to DAVx⁵. At the moment Google Contacts support seems to be flaky at best; see the DAVx⁵ forums for tips.
mutt integration
First off I needed to sync the contacts. (In fact I happen to run mutt
on
the same system where I run Xandikos at the moment, but I don’t want to rely
on that, and going through the CardDAV server means that I don’t have to
poke holes for myself using filesystem permissions.) I used
vdirsyncer for this. In
~/.vdirsyncer/config
:
[general]
status_path = "~/.vdirsyncer/status/"
[pair contacts]
a = "contacts_local"
b = "contacts_remote"
collections = ["from a", "from b"]
[storage contacts_local]
type = "filesystem"
path = "~/.contacts/"
fileext = ".vcf"
[storage contacts_remote]
type = "carddav"
url = "<Xandikos base URL>"
username = "<my username>"
password = "<my password>"
Running vdirsyncer discover
and vdirsyncer sync
then synced everything
into ~/.contacts/
. I added an hourly crontab
entry to run vdirsyncer
-v WARNING sync
.
Next, I needed a command-line address book tool based on this.
khard looked about right and is in
stretch, so I installed that. In ~/.config/khard/khard.conf
(this is
mostly just the example configuration, but I preferred to sort by first name
since not all my contacts have neat first/last names):
[addressbooks]
[[contacts]]
path = ~/.contacts/<UUID of my contacts collection>/
[general]
debug = no
default_action = list
editor = vim
merge_editor = vimdiff
[contact table]
# display names by first or last name: first_name / last_name
display = first_name
# group by address book: yes / no
group_by_addressbook = no
# reverse table ordering: yes / no
reverse = no
# append nicknames to name column: yes / no
show_nicknames = no
# show uid table column: yes / no
show_uids = yes
# sort by first or last name: first_name / last_name
sort = first_name
[vcard]
# extend contacts with your own private objects
# these objects are stored with a leading "X-" before the object name in the vcard files
# every object label may only contain letters, digits and the - character
# example:
# private_objects = Jabber, Skype, Twitter
private_objects = Jabber, Skype, Twitter
# preferred vcard version: 3.0 / 4.0
preferred_version = 3.0
# Look into source vcf files to speed up search queries: yes / no
search_in_source_files = no
# skip unparsable vcard files: yes / no
skip_unparsable = no
Now khard list
shows all my contacts. So far so good. Apparently there
are some awkward vCard compatibility
issues with creating or modifying
contacts from the khard
end. I’ve tried adding one address from
~/.mutt/aliases
using khard
and it seems to at least minimally work for
me, but I haven’t explored this very much yet.
I had to install python3-vobject 0.9.4.1-1 from experimental to fix eventable/vobject#39 saving certain vCard files.
Finally, mutt
integration. I already had set query_command="lbdbq '%s'"
in ~/.muttrc
, and I wanted to keep that in place since I still wanted to
use LDAP querying as well. I had to write a very small amount of code for
this (perhaps I should contribute this to lbdb
upstream?), in
~/.lbdb/modules/m_khard
:
#! /bin/sh
m_khard_query () {
khard email --parsable --remove-first-line --search-in-source-files "$1"
}
My full ~/.lbdb/rc
now reads as follows (you probably won’t want the LDAP
stuff, but I’ve included it here for completeness):
MODULES_PATH="$MODULES_PATH $HOME/.lbdb/modules"
METHODS='m_muttalias m_khard m_ldap'
LDAP_NICKS='debian canonical'
Next steps
I’ve deleted one account from Google Contacts just to make sure that everything still works (e.g. I can still search for it when composing a new message), but I haven’t yet deleted everything. I won’t be adding anything new there though.
I need to push everything from ~/.mutt/aliases
into the new system. This
is only about 30 contacts so shouldn’t take too long.
Overall this feels like a big improvement! It wasn’t a trivial amount of setup for just me, but it means I have both better usability for myself and more independence from proprietary services, and I think I can add extra users with much less effort if I need to.
Postscript
A day later and I’ve consolidated all my accounts from Google Contacts and
~/.mutt/aliases
into the new system, with the exception of one group that
I had defined as a mutt
alias and need to work out what to do with. This
all went smoothly.
I’ve filed the new lbdb
module as
#866178, and the python3-vobject
bug as
#866181.