Since one of the recent upgrades of Enigmail (the GPG extension for Thunderbird) and completely switching to GPG version 2 on my Macbook I ended up with the situation that Enigmail did not cache the key passphrases anymore and I had to enter them over and over again. This is caused by the fact that GPG2 requires to use gpg-agent and Enigmail’s internal passphrase management cannot be used anymore. Therefore, a setup is required that enabled the gpg processes spawned by Enigmail to talk to a running gpg-agent instance.

gpg-agent is a small utility daemon that handles passphrase caching. A gpg process can talk to a running gpg-agent once it knows where the agent can be reached. This information is obtained from the environment variable GPG_AGENT_INFO. The content of this variable looks something like this: /Users/youruser/.gnupg/S.gpg-agent:38959:1. In case of programs started in a shell, starting gpg-agent on demand and exporting the correct environment variable is easily possible with a few lines of shell configuration code. However, Thunderbird and therefore also Enigmail are usually started via the OSX GUI and not from within a shell. Therefore, the GPG_AGENT_INFO variable needs to be exported by the process which manages launching graphical programs (via Spotlight). This is launchd on OSX. Fortunately, launchd has command line options to control the environment it uses to start new processes, which we can take advantage of.

For the setup I am using now, I start a gpg-agent process with my graphical login. To do so, I have adapted this solution, which starts gpg-agent at login, but does not export the environment variables inside launchd.

The first step is to create a plist-file, which is a configuration for launchd instructing it to start gpg-agent at login. Create ~/Library/LaunchAgents/org.gnupg.gpg-agent.plist with the following contents (shamelessly stolen from the aforementioned blog post):

<xml version="1.0" encoding="UTF-8"?>
<DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
    "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
   <dict>
   <key>Label</key>
   <string>org.gnupg.gpg-agent</string>
   <key>ProgramArguments</key>
   <array>
       <string>/Users/youruser/bin/start-gpg-agent.sh</string>
   </array>
   <key>RunAtLoad</key>
   <true/>
   </dict>
</plist>

Replace youruser with your actual OSX username. launchd automatically processes plist files from the ~/Library/LaunchAgents directory.

As the plist only dispatches to a shell script called /Users/youruser/bin/start-gpg-agent.sh, we need to create this script, which does the real magic of starting gpg-agent and exporting the variables. Create it with the following contents (adapt the gpg-agent path to your installation):

#!/bin/bash
if test -f "$HOME/.gpg-agent-info" && \
   kill -0 "$(cut -d: -f 2 "$HOME/.gpg-agent-info")" 2>/dev/null
then
    echo "already running" > /dev/null
else
    /usr/local/bin/gpg-agent -c --daemon --write-env-file > /dev/null
fi

if [ -f "${HOME}/.gpg-agent-info" ]
then
    socket="$(cut -d= -f2 "$HOME/.gpg-agent-info")"
    launchctl setenv GPG_AGENT_INFO "${socket}"
else
    echo "gpg-agent did not write info file"
fi

The first part of the script starts gpg-agent in case no other gpg-agent instance is already running. A running instance of gpg-agent puts its connection information in a file called ~/.gpg-agent-info in case it was started with the --write-env-file option. The second half of the shell script parses this file and exports the GPG_AGENT_INFO variable inside launchd with the launchctl setenv command. Afterwards, every graphically launched program has knowledge about the running gpg-agent instance and can connect to it.

In case a gpg process (e.g. spawned by Enigmail) now wants to interact with one of your keys, it will dispatch the passphrase work to gpg-agent. If a passphrase has not been provided to gpg-agent yet, or the last entry is longer ago than the configure TTL (time to live), gpg-agent needs a way to prompt for a new passphrase. Therefore it is important that a graphical pinentry program is configured for gpg-agent. This is done inside the file ~/.gnupg/gpg-agent.conf. Ensure that in this file at least the following line is present (adapt the path as required):

pinentry-program /usr/local/bin/pinentry-mac

This instructs gpg-agent to use the pinentry-mac program (from the GPGTools project, can e.g. be installed via homebrew) for requesting a passphrase.

After performing all these steps, you can logout and back in again. In a terminal you should now see that gpg-agent is running, e.g. via ps -ef | grep gpg-agent and also the GPG_AGENT_INFO variable should be present (echo $GPG_AGENT_INFO). Enigmail should be able to interact with gpg-agent and passphrases will only be requested once per TTL.

Update:

Starting with GPG 2.2 this is probably not necessary anymore, since it seems that GPG itself now implemented a system to start the agent if it is not running.