Connecting to ProtonMail Bridge with K-9 Mail on Android

Recently I decided to look into moving away from GMail and start using a different mail service and landed on ProtonMail. My one gripe is their Android App is not great and has some nagging issues. So I wanted to see if I can use a much more well-known mail client for Android, K-9 Mail.

The main issue with this is that ProtonMail doesn’t have native IMAP support. But, they do have a ProtonMail Bridge which simulates IMAP. They say ThunderBird is offically supported, which is a fairly robust Email client, so I was optimistic that I could get K-9 to work.

TL;DR Note: If you want to skip over my trys and only want to know how to get it working, you can skip to my solution.

My Setup

I run a linux based server that is on my home network. It is headless (no monitor) and runs a handful of docker microservices. Most of them do not matter for this write-up, but one that does play a a fairly large role is my Wireguard VPN container, which my phone is 100% always connected to. This allows my to connect directly to my server from my phone using it’s internal IP address and not needing to expose it to the internet. I highly suggest that you do not expose this to the internet.

My phone is Android 11 and has a kernal with wireguard built-in. This allows me to be connected to Wireguard all the time doesn’t effect battery that much. K-9 mail was installed through f-droid.

I have ProntonMail Plus, which is needed in order to use the ProtonMail Bridge.

Attempt 1 - Official ProtonMail Bridge (deb)

Install

First thing I did was download and install the .deb from ProntonMail’s website. My linux server is normally headless, so I decided to pull out an extra monitor I had around, temporarily setup a desktop and set up my account through the gui and was given a popup similar to this:

Example popup from the ProtonMail website

Example popup from the ProtonMail website

Looking like the bridge was up and running, I launched K-9 Mail and started adding the account.

K-9 Mail

I used the all the settings that ProtonMail gave me (username, password and port) but for IP address I used the internal address for my server instead of 127.0.0.1. I also set the Security to STARTTLS.

K-9 Mail Settings

K-9 Mail Settings

Issues

K-9 fails to connect. After investigating I found that the ProtonMail Bridge only listens for requests from 127.0.0.1 i.e. the same computer it’s running on. Alright, I know how to get arround that

Attempt 2 - Proxy to Official ProtonMail Bridge (deb)

Socat Proxy

I can use socat to proxy a different port that will accept connections from all IP addresses and proxy that to the ProtonBridge.

socat TCP-LISTEN:25,fork TCP:127.0.0.1:1025 &
socat TCP-LISTEN:143,fork TCP:127.0.0.1:1143 &

So now SERVER_IP:25 -> 127.0.0.1:1025 and SERVER_IP:143 -> 127.0.0.1:1143.

I updated the ports in my K-9 settings and it connected to the server!

Issues

It grabbed my folders/labels from ProntonMail correctly and I was able to send emails successfully, which was good. But there was no mail showing up in my inbox or any folder. I tried go through the K-9 settings to see if I was missing something but it all looked okay.

I started going through the Android logcat to see if there was any errors thing and noticed there was an error message that K-9 was handling, unsupported search query.

I wasn’t sure if this was from K-9 or from ProtonMail. Luckily both are open source so I could search and I found that it was from ProntMail

ProtonMail/proton-bridge/internal/imap/mailbox_messages.go#L316

// SearchMessages searches messages. The returned list must contain UIDs if
// uid is set to true, or sequence numbers otherwise.
func (im *imapMailbox) SearchMessages(isUID bool, criteria *imap.SearchCriteria) (ids []uint32, err error) { //nolint[gocyclo]
	// Called from go-imap in goroutines - we need to handle panics for each function.
	defer im.panicHandler.HandlePanic()

	if criteria.Not != nil || criteria.Or != nil {
		return nil, errors.New("unsupported search query")
	}

	if criteria.Body != nil || criteria.Text != nil {
		log.Warn("Body and Text criteria not applied")
	}

So either K-9 is passing criteria.Not or criteria.Or when searching for messages. It turns out it’s criteria.Not.

Now I can either edit the K-9 source or ProtonMail Bridge source and try again. I decided to ProtonMail Bridge source route.

Attempt 3 - Compile ProtonMail Bridge Myself

As I was looking around, I found this great docker setup by GitHub user shenxn. I wish I would have found this before trying the initial setup with a gui, as this one uses the cli to login and has some good documentation of how to set it up. They also have an example of how to build the ProtonMail Bridge from source and deploy it as a docker and funny enough was doing the same proxying I was doing.

My Repo

Using that repo as a reference, I decided to make my own repo for a docker image that I can apply some patches to fix stuff as I see fit.

The way that it works is

  1. I get the source code from https://github.com/ProtonMail/proton-bridge
  2. I apply the patches from the ./patches folder in my repo (currently just ignore criteria.Not for K-9)
  3. Build the source
  4. Build the docker image

Then when the docker image is spun up, it creates some socat proxies and starts the protonmail-bridge.

With that setup, I tore down my previous attempts (uninstalled the bridge, took down the socat proxies) just to avoid conflicts.

Solution

Setting up the docker is a two step process, first you need to do initialize to get your credentials

docker run --rm -it -v /path/to/protonmail:/root ghcr.io/tchilderhose/protonmail-bridge-docker init

and follow the steps in the readme from the repo

Wait for the bridge to startup, use login command and follow the instructions to add your account into the bridge. Then use info to see the configuration information (username and password). After that, use exit to exit the bridge. You may need CTRL+C to exit the docker entirely.

This will place a bunch of files needed for the bridge in the /path/to/protonmail folder (feel free to change this beforehand).

Then you can spin up the actual bridge for use. I use docker-compose so I just added

  protonmail:
    image:  ghcr.io/tchilderhose/protonmail-bridge-docker
    container_name: protonmail
    restart: unless-stopped
    ports:
      - "1025:25/tcp"
      - "1143:143/tcp"
    volumes:
      - /path/to/protonmail:/root

With that running I updated my K-9 settings with those port numbers (1025, 1143) and it worked! All my emails started showing up and it all seemed to be working.

Summary

The final connection path: (Again, I highly suggest that you do not expose the docker to the internet and connect to it through a secure VPN.)

K9 Mail -> Wireguard Tunnel to my Server -> protonmail-bridge-docker -> ProtonMail Bridge -> ProtonMail Servers

Getting this working took a few attempts, but it wasn’t overall too bad. While it’s only been a few days that it’s been setup, I’ve had no issues and have been really happy with it. There is a lot of good settings and documentation to play around with in K-9 to get it setup to my likeing.

The one thing that is missing is sending via an alias. This isn’t something I’ve needed to do yet, snor do I think I will really do, but it is something to consider.

ProtonMail keep saying a new mobile app will be rolling out by years end, so if/when that drops, I will need to reevaluate if I will switch to that or keep this setup. But currently I am really liking it so will probably be sticking with it for awhile.