11 Commits

Author SHA1 Message Date
Xiaonan Shen
967beefb2f Add linux/arm/v7 2021-10-05 10:20:07 +08:00
Xiaonan Shen
7b72698ad5 Add github actions to build next 2021-09-13 20:12:06 +08:00
Xiaonan Shen
9c56bb2861 Improve dockerfile 2021-09-04 18:00:23 +08:00
Xiaonan Shen
0fb64c241c Remove build images 2021-09-04 13:17:19 +08:00
Xiaonan Shen
73ed583bf2 Fix gpg-agent run and provide automated login process (#35) 2021-09-04 11:22:58 +08:00
Xiaonan Shen
6fd883204d Merge branch 'next' into master 2021-09-04 11:15:36 +08:00
Xiaonan Shen
eb44376525 run as non-root in docker (#38) 2021-09-04 11:10:58 +08:00
Thibault Godouet
07d1004e09 run as non-root in docker 2021-07-25 17:42:10 +01:00
Jeffrey Stoke
83c08489c7 build: hide password input (best effort)
Signed-off-by: Jeffrey Stoke <me@arhat.dev>
2021-06-15 04:08:24 +02:00
Jeffrey Stoke
dbcf6d27b0 build: fix environment name
Signed-off-by: Jeffrey Stoke <me@arhat.dev>
2021-06-15 04:07:56 +02:00
Jeffrey Stoke
da0dfab9d3 build: gpg-agent run fix & automated login process
The gpg-agent could not start if the /root is mounted by docker when
the external path is long enough, a workaround is to use a different
GNUPGHOME for gpg key generation, and copy generated files to
/root/.gnupg afterwards

bonus: thanks to expect, we can a automated login process, if both
PROTONMAIL_USERNAME and PROTONMAIL_PASSWORD are set

Signed-off-by: Jeffrey Stoke <me@arhat.dev>
2021-06-14 22:38:44 +02:00
23 changed files with 286 additions and 773 deletions

View File

@@ -1,17 +1,24 @@
name: Build docker image name: build next
on: on:
push: push:
branches: branches:
- master - next
- beta paths:
- dev - .github/workflows/next.yaml
- http-rest - docker/*
- protonmail_version/*
- VERSION
pull_request: pull_request:
paths:
- .github/workflows/next.yaml
- docker/*
- protonmail_version/*
- VERSION
env: env:
DOCKER_REPO: shenxn/protonmail-bridge DOCKER_REPO: shenxn/protonmail-bridge-ng
DOCKER_REPO_DEV: ghcr.io/shenxn/protonmail-bridge-dev DOCKER_REPO_DEV: ghcr.io/shenxn/protonmail-bridge-ng-dev
PLATFORMS: linux/amd64,linux/arm64/v8,linux/arm/v7 PLATFORMS: linux/amd64,linux/arm64/v8,linux/arm/v7
jobs: jobs:
@@ -25,19 +32,14 @@ jobs:
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@master uses: actions/checkout@master
- name: Set bridge version - name: Set version
id: version id: version
run: | run: |
echo "::set-output name=bridge_version::$(cat BRIDGE_VERSION)" echo "::set-output name=image_version::$(cat VERSION)" && \
echo "::set-output name=version::$(cat VERSION)" echo "::set-output name=protonmail_bridge_version::$(cat protonmail_bridge_version/VERSION_LATEST)"
- name: Set repo - name: Set repo
id: repo id: repo
run: | run: if [[ $GITHUB_REF == "refs/heads/master" ]]; then echo "::set-output name=repo::${DOCKER_REPO}"; else echo "::set-output name=repo::${DOCKER_REPO_DEV}"; fi
if [[ $GITHUB_REF == "refs/heads/master" || $GITHUB_REF == "refs/heads/beta" ]]; then
echo "::set-output name=repo::${DOCKER_REPO}"
else
echo "::set-output name=repo::${DOCKER_REPO_DEV}"
fi
- name: Docker meta - name: Docker meta
id: docker_meta id: docker_meta
uses: crazy-max/ghaction-docker-meta@v1 uses: crazy-max/ghaction-docker-meta@v1
@@ -52,8 +54,10 @@ jobs:
- name: Build image without push to registry - name: Build image without push to registry
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
context: ./docker
file: ./docker/Dockerfile
build-args: PROTONMAIL_BRIDGE_VERSION=${{ steps.version.outputs.protonmail_bridge_version }}
platforms: ${{ env.PLATFORMS }} platforms: ${{ env.PLATFORMS }}
build-args: BRIDGE_VERSION=${{ steps.version.outputs.bridge_version }}
push: true push: true
tags: localhost:5000/protonmail-bridge:latest tags: localhost:5000/protonmail-bridge:latest
- name: Scan image - name: Scan image
@@ -65,18 +69,18 @@ jobs:
severity-cutoff: critical severity-cutoff: critical
acs-report-enable: true acs-report-enable: true
- name: Upload Anchore scan SARIF report - name: Upload Anchore scan SARIF report
uses: github/codeql-action/upload-sarif@v2 uses: github/codeql-action/upload-sarif@v1
with: with:
sarif_file: ${{ steps.scan.outputs.sarif }} sarif_file: ${{ steps.scan.outputs.sarif }}
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v1 uses: docker/login-action@v1
if: ${{ github.event_name != 'pull_request' && steps.repo.outputs.repo == env.DOCKER_REPO }} if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/master' }}
with: with:
username: ${{ secrets.REGISTRY_USERNAME }} username: ${{ secrets.REGISTRY_USERNAME }}
password: ${{ secrets.REGISTRY_PASSWORD }} password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Login to GitHub Container Registry - name: Login to GitHub Container Registry
uses: docker/login-action@v1 uses: docker/login-action@v1
if: ${{ github.event_name != 'pull_request' && steps.repo.outputs.repo == env.DOCKER_REPO_DEV }} if: ${{ github.event_name != 'pull_request' && github.ref == 'refs/heads/next' }}
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -84,10 +88,12 @@ jobs:
- name: Push image - name: Push image
uses: docker/build-push-action@v2 uses: docker/build-push-action@v2
with: with:
context: ./docker
file: ./docker/Dockerfile
build-args: PROTONMAIL_BRIDGE_VERSION=${{ steps.version.outputs.protonmail_bridge_version }}
platforms: ${{ env.PLATFORMS }} platforms: ${{ env.PLATFORMS }}
build-args: BRIDGE_VERSION=${{ steps.version.outputs.bridge_version }}
tags: | tags: |
${{ steps.repo.outputs.repo }}:beta ${{ steps.repo.outputs.repo }}:latest
${{ steps.repo.outputs.repo }}:${{ steps.version.outputs.version }} ${{ steps.repo.outputs.repo }}:${{ steps.version.outputs.image_version }}-${{ steps.version.outputs.protonmail_bridge_version }}
labels: ${{ steps.docker_meta.outputs.labels }} labels: ${{ steps.docker_meta.outputs.labels }}
push: ${{ github.event_name != 'pull_request' }} push: ${{ github.event_name != 'pull_request' }}

1
.gitignore vendored
View File

@@ -1 +0,0 @@
.vscode

View File

@@ -1 +0,0 @@
v2.2.0

View File

@@ -1,37 +0,0 @@
FROM golang:1.15 AS build
# Install dependencies
RUN apt-get update && apt-get install -y libsecret-1-dev
ARG BRIDGE_VERSION
# Build
WORKDIR /build/
COPY build.sh /build/
COPY http_rest_frontend /build/http_rest_frontend
RUN bash build.sh
FROM ubuntu:bionic
LABEL maintainer="Xiaonan Shen <s@sxn.dev>"
EXPOSE 25/tcp
EXPOSE 143/tcp
HEALTHCHECK --timeout=2s CMD nc -z 127.0.0.1 1025 && nc -z 127.0.0.1 1143
# Install dependencies and protonmail bridge
RUN apt-get update \
&& apt-get install -y --no-install-recommends socat pass libsecret-1-0 ca-certificates curl gosu netcat \
&& rm -rf /var/lib/apt/lists/*
RUN curl -sSL https://github.com/krallin/tini/releases/download/v0.19.0/tini-$(dpkg --print-architecture) -o /tini \
&& chmod +x /tini
# Copy bash scripts
COPY gpgparams entrypoint.sh run_protonmail_bridge.sh cli.sh /protonmail/script/
RUN ln -s /protonmail/script/cli.sh /usr/local/bin/cli
# Copy protonmail
COPY --from=build /build/proton-bridge/proton-bridge /usr/local/bin/
VOLUME [ "/protonmail/data" ]
ENTRYPOINT ["/tini", "--", "/protonmail/script/entrypoint.sh"]

126
README.md
View File

@@ -12,137 +12,81 @@ Docker Hub: [https://hub.docker.com/r/shenxn/protonmail-bridge](https://hub.dock
GitHub: [https://github.com/shenxn/protonmail-bridge-docker](https://github.com/shenxn/protonmail-bridge-docker) GitHub: [https://github.com/shenxn/protonmail-bridge-docker](https://github.com/shenxn/protonmail-bridge-docker)
## ProtonMail Bridge Docker 2.0 Announcement ## ARMv7 Builds
ProtonMail Bridge Docker 2.0 uses a completely different approach that makes the whole container much easier to use. First of all, it now supports setting up with environmental variables only. No longer messing with terminal commands. Secondly, it now provides a easy way to interact with the bridge, include getting account information, adding, and removing accounts, all after the first initialization. This version is now in beta. Feel free to test it by using the `beta` tag (`shenxn/protonmail-bridge:beta`). Currently there is a problem building the new parser targeting 32-bit architectures (i.e. `arm/v7`). The latest working build is `1.4.5-build`. Therefore, if you are using an `arm/v7` device like Raspberry Pi, do not upgrade to newer version. More information about the problem can be found [here](https://www.reddit.com/r/ProtonMail/comments/jvzm12/issue_building_bridge_150/). If you have any idea on how to fix this, a PR is welcome.
### Environmental Variables ## ARM Support
name | default | description We now support ARM devices (`arm64` and `arm/v7`)! Use the images tagged with `build`. See next section for details.
-- | -- | --
`PROTON_USERNAME` | | Username of your Proton account.
`PROTON_PASSWORD` | | Password of your Proton account.
`PROTON_2FA` | | Two-factor authentication code of your Proton account. Only needed if you have two factor authentication enabled.
`PROTON_MAILBOX_PASSWORD` | | Mailbox password of your Proton account. Only needed if you have two-password mode enabled.
`PROTON_PRINT_ACCOUNT_INFO` | `true` | Whether to print the local connection information (username, password, ports, security, etc.) on successful auto logins. Set to `false` to turn it off.
`PROTON_IMAP_PORT` | `25` | IMAP port of the bridge.
`PROTON_SMTP_PORT` | `143` | SMTP port of the bridge.
`PROTON_SMTP_SECURITY` | `STARTTLS` | Connection security of SMTP. Can be set to `STARTTLS` or `SSL`.
`PROTON_MANAGEMENT_PORT` | `1080` | Port of the management HTTP server.
`PROTON_ALLOW_PROXY` | `true` | Allow or disallow the Bridge client to securely connect to Proton via a third party when it is being blocked.
`PROTON_UID` | `1001` | UID of the user to run the bridge.
`PROTON_GID` | `1001` | GID of the group to run the bridge.
### Data Volume ## Tags
All data are stored under `/protonmail/data`.
### Auto Login
2.0 comes with auto login, a highly demanded feature. By set `PROTON_USERNAME`, `PROTON_PASSWORD`, `PROTON_2FA`, and `PROTON_MAILBOX_PASSWORD`, the container will automatically add your account to the bridge and print local connection info to the log. Note:
- The auto login feature only kicks in when the bridge has zero account added. If there is at least one account, auto login will be skipped. Changing the environmental variables will neither add another account to the bridge nor replace the account with the new one. To do so, use [CLI](#cli).
- Two factor authentication codes time out quickly (in 30 seconds). It is highly possible that the code is already expired at the time login take place. To help with that, you can try pull the image first before setting it. You can also wait for a new code so that the container will have full 30 seconds to boot up. If non of them works, try manual login with [CLI](#cli).
- If your container logs can be accessed by someone else, it is recommended to turn of the account info printing by set `PROTON_PRINT_ACCOUNT_INFO` to `false`. Then you can use [CLI](#cli) to fetch the account info.
### CLI
2.0 comes with a CLI to easily interact with the bridge. There is no need to modify the entrypoint, manually kill any process, or restart the container when you are done. Everything you need is to `exec` into the container and run `cli`. With that, you can add, remove, list accounts and print account info. Changing the address mode of an account is currently not supported. To do so, simply delete the account and add it back again. Example:
```
➜ docker exec -it protonmail cli
CLI to interacte with Proton Bridge HTTP REST interface
Available commands:
login: Calls up the login procedure to add or connect accounts.
delete <account>: Remove the account from keychain. You can use index or account name as the parameter.
list: Print list of your accounts.
info <account>: Print account configuration. You can use index or account name as the parameter.
help: Print help messages.
exit: Exit the CLI
>> info bob
Configuration for bob@proton.me
IMAP port: 143
IMAP security: STARTTLS
SMTP port: 25
SMTP security: STARTTLS
Username: bob@proton.me
Password: xxxxxxxxxxxxxxxxxxxxxx
>> exit
```
### Migrate from old version
To migrate, you just need to change the mount path from `/root` to `/protonmail/data` and the container will do the rest of the work. Note that this is a one way trip. There is no way to get back to the old version. You'll need to set up everything again from scratch to go back. Also, any bridge preferences (e.g. whether to use a proxy) will not be migrated. You'll need to set them again with the environmental variables. Not all preferences are supported yet. Feel free to open an issue or PR for options you want to change. If anything goes sideways, it can be caused by problematic migrating steps. You can try to clear your volume and start everything from the scratch.
## Supported architectures
architecture | status | note
-- | --
`amd64` | supported |
`arm64/v8` | supported |
`arm/v7` | supported | 32-bit platforms are no longer officially supported by Proton. Thanks to blumberg for the workaround [#40](https://github.com/shenxn/protonmail-bridge-docker/pull/40)
`riscv64` | WIP | Check progress at [#54](https://github.com/shenxn/protonmail-bridge-docker/pull/54)
## Legacy Documentation
This section contains information for the old version. They are kept here for reference and will be removed once 2.0 graduates from beta.
### Tags
There are two types of images. There are two types of images.
- `deb`: Images based on the official [.deb release](https://protonmail.com/bridge/install). It only supports the `amd64` architecture. - `deb`: Images based on the official [.deb release](https://protonmail.com/bridge/install). It only supports the `amd64` architecture.
- `build`: Images based on the [source code](https://github.com/ProtonMail/proton-bridge). It supports `amd64`, `arm64`, and `arm/v7`. Supporting to more architectures is possible. PRs are welcome. - `build`: Images based on the [source code](https://github.com/ProtonMail/proton-bridge). It supports `amd64`, `arm64`, and `arm/v7`. Supporting to more architectures is possible. PRs are welcome.
tag | description | tag | description |
-- | -- | ----------------- | -------------------- |
`latest` | latest `deb` image | `latest` | latest `deb` image |
`[version]` | `deb` images | `[version]` | `deb` images |
`build` | latest `build` image | `build` | latest `build` image |
`[version]-build` | `build` images | `[version]-build` | `build` images |
### Initialization ## Environment Variables for images with `build` in tag
| Name | Description | Default Value |
| --------------------- | --------------------------------------------------------------------------------- | ------------- |
| `PROTONMAIL_USERNAME` | your protonmail account username | (not set) |
| `PROTONMAIL_PASSWORD` | the password for your protonmail account | (not set) |
| `KEY_ID` | the pgp-id for the internal password manager required by protonmail-bridge | `pass-key` |
| `GNUPGHOME` | change the defulat `/root/.gnupg` to this directory as a workaround for gpg error | `/tmp/gnupg` |
## Initialization
To initialize and add account to the bridge, run the following command. To initialize and add account to the bridge, run the following command.
``` ```bash
docker run --rm -it -v protonmail:/root shenxn/protonmail-bridge init docker run --rm -it -v protonmail:/home/protonmail shenxn/protonmail-bridge init
``` ```
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. 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.
### Run __NOTE:__ If you have `PROTONMAIL_USERNAME` and `PROTONMAIL_PASSWORD` set for `docker run` with `-e`, the login process will finish automatically.
## Run
To run the container, use the following command. To run the container, use the following command.
``` ```bash
docker run -d --name=protonmail-bridge -v protonmail:/root -p 1025:25/tcp -p 1143:143/tcp --restart=unless-stopped shenxn/protonmail-bridge docker run -d --name=protonmail-bridge -v protonmail:/home/protonmail -p 1025:25/tcp -p 1143:143/tcp --restart=unless-stopped shenxn/protonmail-bridge
``` ```
### Kubernetes ## Kubernetes
If you want to run this image in a Kubernetes environment. You can use the [Helm](https://helm.sh/) chart (https://github.com/k8s-at-home/charts/tree/master/charts/stable/protonmail-bridge) created by [@Eagleman7](https://github.com/Eagleman7). More details can be found in [#23](https://github.com/shenxn/protonmail-bridge-docker/issues/23). If you want to run this image in a Kubernetes environment. You can use the [Helm](https://helm.sh/) chart (https://github.com/k8s-at-home/charts/tree/master/charts/stable/protonmail-bridge) created by [@Eagleman7](https://github.com/Eagleman7). More details can be found in [#23](https://github.com/shenxn/protonmail-bridge-docker/issues/23).
If you don't want to use Helm, you can also reference to the guide ([#6](https://github.com/shenxn/protonmail-bridge-docker/issues/6)) written by [@ghudgins](https://github.com/ghudgins). If you don't want to use Helm, you can also reference to the guide ([#6](https://github.com/shenxn/protonmail-bridge-docker/issues/6)) written by [@ghudgins](https://github.com/ghudgins).
### Security ## Security
Please be aware that running the command above will expose your bridge to the network. Remember to use firewall if you are going to run this in an untrusted network or on a machine that has public IP address. You can also use the following command to publish the port to only localhost, which is the same behavior as the official bridge package. Please be aware that running the command above will expose your bridge to the network. Remember to use firewall if you are going to run this in an untrusted network or on a machine that has public IP address. You can also use the following command to publish the port to only localhost, which is the same behavior as the official bridge package.
``` ```bash
docker run -d --name=protonmail-bridge -v protonmail:/root -p 127.0.0.1:1025:25/tcp -p 127.0.0.1:1143:143/tcp --restart=unless-stopped shenxn/protonmail-bridge docker run -d --name=protonmail-bridge -v protonmail:/home/protonmail -p 127.0.0.1:1025:25/tcp -p 127.0.0.1:1143:143/tcp --restart=unless-stopped shenxn/protonmail-bridge
``` ```
Besides, you can publish only port 25 (SMTP) if you don't need to receive any email (e.g. as a email notification service). Besides, you can publish only port 25 (SMTP) if you don't need to receive any email (e.g. as a email notification service).
### Compatibility ## Compatibility
The bridge currently only supports some of the email clients. More details can be found on the official website. I've tested this on a Synology DiskStation and it runs well. However, you may need ssh onto it to run the interactive docker command to add your account. The main reason of using this instead of environment variables is that it seems to be the best way to support two-factor authentication. The bridge currently only supports some of the email clients. More details can be found on the official website. I've tested this on a Synology DiskStation and it runs well. However, you may need ssh onto it to run the interactive docker command to add your account. The main reason of using this instead of environment variables is that it seems to be the best way to support two-factor authentication.
### Bridge CLI Guide ## Bridge CLI Guide
The initialization step exposes the bridge CLI so you can do things like switch between combined and split mode, change proxy, etc. The [official guide](https://protonmail.com/support/knowledge-base/bridge-cli-guide/) gives more information on to use the CLI. The initialization step exposes the bridge CLI so you can do things like switch between combined and split mode, change proxy, etc. The [official guide](https://protonmail.com/support/knowledge-base/bridge-cli-guide/) gives more information on to use the CLI.
### Build ## Build
For anyone who want to build this container on your own (for development or security concerns), here is the guide to do so. First, you need to `cd` into the directory (`deb` or `build`, depending on which type of image you want). Then just run the docker build command For anyone who want to build this container on your own (for development or security concerns), here is the guide to do so. First, you need to `cd` into the directory (`deb` or `build`, depending on which type of image you want). Then just run the docker build command
``` ```

View File

@@ -1 +1 @@
2.0.0-beta 0.1

View File

@@ -1,24 +0,0 @@
#!/bin/bash
set -ex
# Clone new code
git clone https://github.com/ProtonMail/proton-bridge.git
cd proton-bridge
git checkout ${BRIDGE_VERSION}
# Patch HTTP REST frontend
rm -rf internal/frontend/cli
cp -r /build/http_rest_frontend/cli internal/frontend/
# Build
if (! make build-nogui) && [[ $(getconf LONG_BIT) == "32" ]]; then
# If build fails it's probably because it is a 32bit
# system and there was a overflow error on the parser
# This is a workaround for this problem found at:
# https://github.com/antlr/antlr4/issues/2433#issuecomment-774514106
find $(go env GOPATH)/pkg/mod/github.com/\!proton\!mail/go-rfc5322*/ -type f -exec sed -i.bak 's/(1<</(int64(1)<</g' {} +
# Try again after implementing the workaround
make build-nogui
fi

79
cli.sh
View File

@@ -1,79 +0,0 @@
#!/bin/bash
set -e
URL_BASE=http://127.0.0.1:${PROTON_MANAGEMENT_PORT:-1080}
print_help() {
echo "Available commands:"
echo " login: Calls up the login procedure to add or connect accounts."
echo " delete <account>: Remove the account from keychain. You can use index or account name as the parameter."
echo " list: Print list of your accounts."
echo " info <account>: Print account configuration. You can use index or account name as the parameter."
echo " help: Print help messages."
echo " exit: Exit the CLI"
}
account_list() {
curl ${URL_BASE}/accounts
}
account_login() {
read -p "Username: " USERNAME
read -sp "Password: " PASSWORD
echo
read -p "2FA Code (leave empty if not set): " TWO_FACTOR
read -p "Mailbox Password (leave empty if not set): " MAILBOX_PASSWORD
read -p "Address Mode (combined / split): " ADDRESS_MODE
curl ${URL_BASE}/accounts -XPUT \
--data-urlencode "username=${USERNAME}" \
--data-urlencode "password=${PASSWORD}" \
--data-urlencode "two-factor=${TWO_FACTOR}" \
--data-urlencode "mailbox-password=${MAILBOX_PASSWORD}" \
--data-urlencode "address-mode=${ADDRESS_MODE}"
}
account_delete() {
if [[ -z $1 ]]; then
echo "Error: delete requires one parameter, which is the index or account name."
else
read -p "Are you sure you want to delete account $1? " REPLY
if [[ $REPLY =~ ^[Yy] ]]; then
curl ${URL_BASE}/accounts/$1 -XDELETE
else
echo "Abort"
fi
fi
}
account_info() {
if [[ -z $1 ]]; then
echo "Error: info requires one parameter, which is the index or account name."
else
curl ${URL_BASE}/accounts/$1
fi
}
echo "CLI to interacte with Proton Bridge HTTP REST interface"
print_help
while true; do
echo
read -p ">> " COMMAND ARG
case "$COMMAND" in
login)
account_login;;
delete)
account_delete $ARG;;
list)
account_list;;
info)
account_info $ARG;;
help)
print_help;;
exit)
exit 0;;
*)
echo "Invalid command"
esac
done

42
docker/Dockerfile Normal file
View File

@@ -0,0 +1,42 @@
FROM golang:1.16 AS build
# Install dependencies
RUN apt-get update && apt-get install -y libsecret-1-dev
ARG PROTONMAIL_BRIDGE_VERSION
# Build
WORKDIR /build/
RUN curl -L https://github.com/ProtonMail/proton-bridge/archive/refs/tags/${PROTONMAIL_BRIDGE_VERSION}.tar.gz \
| tar zx --strip-component 1
RUN make build-nogui
FROM ubuntu:bionic
LABEL maintainer="Xiaonan Shen <s@sxn.dev>"
EXPOSE 25/tcp
EXPOSE 143/tcp
# Install dependencies
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
expect socat pass libsecret-1-0 ca-certificates \
&& rm -rf /var/lib/apt/lists/*
ENV PATH="/srv/protonmail:$PATH"
# Copy protonmail
COPY --from=build /build/proton-bridge /srv/protonmail/
# Copy bash scripts
COPY gpgparams entrypoint.sh auto-login.exp login.sh /srv/protonmail/
# Create use and group for protonmail
RUN groupadd --gid 8535 protonmail \
&& useradd --uid 8535 --gid 8535 --home-dir /protonmail protonmail \
&& mkdir /protonmail \
&& chown protonmail:protonmail /protonmail
USER protonmail
WORKDIR /protonmail
ENTRYPOINT ["bash", "/srv/protonmail/entrypoint.sh"]

1
docker/VERSION Normal file
View File

@@ -0,0 +1 @@
v1.8.9

98
docker/auto-login.exp Executable file
View File

@@ -0,0 +1,98 @@
#!/usr/bin/expect -f
set timeout 15;
spawn proton-bridge -cli {*}$argv ;
# wait for inital prompt
expect {
">>> " {
# protonmail-bridge started without error, do nothing
}
timeout {
puts "Timed out"
exit 2
}
}
send "login\n"
expect {
"Username: " {
# login start, enter username
}
timeout {
puts "Timed out"
exit 2
}
}
send "$::env(PROTONMAIL_USERNAME)\n"
expect {
"Password: " {
# username entered, enter password
}
timeout {
puts "Timed out"
exit 2
}
}
stty -echo
sleep 1
send "$::env(PROTONMAIL_PASSWORD)\n"
stty echo
expect {
"was added successfully." {
# login ok
}
"Two factor code: " {
# 2FA enabled, enter OTP
if ![info exists ::env(PROTONMAIL_OTP)] {
puts "\n2FA enabled but PROTONMAIL_OTP is not set. Exiting"
exit 1
}
send "$::env(PROTONMAIL_OTP)\n"
expect {
"was added successfully." {
# login ok
}
"Server error" {
# login failed
exit 1
}
timeout {
puts "Timed out"
exit 2
}
}
}
"Server error" {
# login failed
exit 1
}
timeout {
puts "Timed out"
exit 2
}
}
send "info\n"
expect {
"Configuration for " {
# successfully got info. Wait for printing
sleep 1
}
timeout {
puts "Timed out"
exit 2
}
}

62
docker/entrypoint.sh Normal file
View File

@@ -0,0 +1,62 @@
#!/bin/bash
set -e
# Generate gpg keys
if [ ! -f ${HOME}/.gnupg ]; then
echo "Generateing gpg keys..."
# set GNUPGHOME as a workaround for
#
# gpg-agent[106]: error binding socket to '/root/.gnupg/S.gpg-agent': File name too long
#
# when using docker volume mount
#
# ref: https://dev.gnupg.org/T2964
#
export GNUPGHOME=/tmp/gnupg
mkdir ${GNUPGHOME}
chmod 700 ${GNUPGHOME}
gpg --generate-key --batch /srv/protonmail/gpgparams
pkill gpg-agent
mv ${GNUPGHOME} ${HOME}/.gnupg
export GNUPGHOME=""
fi
# Initialize pass
if [ ! -f ${HOME}/.password-store/.gpg-id ]; then
echo "Initializing pass"
pass init pass-key
fi
# Login
if [ ! -f ${HOME}/.logged-in ]; then
if [[ -n ${PROTONMAIL_USERNAME} && -n ${PROTONMAIL_PASSWORD} ]]; then
echo "Logging in"
auto-login.exp $@
echo "" > ${HOME}/.logged-in
else
# Wait for manual login
echo "=============================================================================="
echo "PROTONMAIL_USERNAME or PROTONMAIL_PASSWORD is not set. Will not do auto login."
echo "Run docker exec -it protonmail login.sh to login manually."
echo "Waiting for manual login..."
while [ ! -f ${HOME}/.logged-in ]; do
sleep 5
done
fi
fi
echo "Logged in flag detected. Starting protonmail bridge"
# socat will make the conn appear to come from 127.0.0.1
# ProtonMail Bridge currently expects that.
# It also allows us to bind to the real ports :)
socat TCP-LISTEN:2025,fork TCP:127.0.0.1:1025 &
socat TCP-LISTEN:2143,fork TCP:127.0.0.1:1143 &
# Start protonmail
# Fake a terminal, so it does not quit because of EOF...
rm -f faketty
mkfifo faketty
cat faketty | proton-bridge --cli $@

12
docker/login.sh Executable file
View File

@@ -0,0 +1,12 @@
#!/bin/bash
echo "Start manual login"
echo "========================================================================"
echo "IMPORTANT: Use `exit` instead of CTRL-C when you successfully logged in."
echo "Otherwise protonmail bridge will not start."
echo "========================================================================"
proton-bridge -cli
echo "Consider logged in. Add flag."
echo "" > $HOME/.logged-in

View File

@@ -1,37 +0,0 @@
#!/bin/bash
set -e
echo "Welcome to ProtonMail Bridge Docker 2.0. This version comes with plently of"
echo "improvements. This version is not compatible with old configs and data volumes."
echo "If you are coming from the old version, please clean up your volumes and set up"
echo "everything from scratch. For more information about the new version, check"
echo "https://github.com/shenxn/protonmail-bridge-docker"
echo
if [ -d /root/.config/protonmail ]; then
echo "Volume mount at `/root` for old version found. Please change your mount path to `/protonmail/data`."
echo "See https://github.com/shenxn/protonmail-bridge-docker for detailed migration guide."
echo "Exit in 30 seconds..."
sleep 30
exit 1
fi
groupadd -g ${PROTON_GID:-1001} proton
useradd -g proton -u ${PROTON_UID:-1001} -m proton
chown proton:proton /protonmail/data
# Migrate old version data
if [ -d /protonmail/data/.config/protonmail ]; then
echo "Migrating legacy data. Note that this operation is irreversible so it is impossible"
echo "to go back to the old version without clearing the volume."
echo
mv /protonmail/data/.config/protonmail /protonmail/data/config
rm -rf /protonmail/data/.config
mv /protonmail/data/.cache/protonmail /protonmail/data/cache
rm -rf /protonmail/data/.cache
chown proton:proton -R /protonmail/data
fi
exec gosu proton:proton /protonmail/script/run_protonmail_bridge.sh "$@"

View File

@@ -1,163 +0,0 @@
// Package cli provides HTTP interface of the bridge
package cli
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strings"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/julienschmidt/httprouter"
"github.com/sirupsen/logrus"
)
func (f *frontendCLI) loginAccount(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
if err := r.ParseForm(); err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "ParseForm() err: %v", err)
return
}
username := r.FormValue("username")
password := r.FormValue("password")
twoFactor := r.FormValue("two-factor")
mailboxPassword := r.FormValue("mailbox-password")
addressMode := r.FormValue("address-mode")
if addressMode == "" {
addressMode = "combined"
}
if addressMode != "combined" && addressMode != "split" {
http.Error(w, fmt.Sprintf("%s is not a valid address mode. Choose from 'combined' and 'split'."), http.StatusBadRequest)
return
}
client, auth, err := f.bridge.Login(username, []byte(password))
if err != nil {
f.processAPIError(err)
http.Error(w, fmt.Sprintf("Server error: %s", err.Error()), http.StatusUnauthorized)
return
}
if auth.HasTwoFactor() {
if twoFactor == "" {
w.WriteHeader(http.StatusUnauthorized)
http.Error(w, "2FA enabled for the account but a 2FA code was not provided.", http.StatusUnauthorized)
return
}
err = client.Auth2FA(context.Background(), twoFactor)
if err != nil {
f.processAPIError(err)
http.Error(w, fmt.Sprintf("Server error: %s", err.Error()), http.StatusUnauthorized)
return
}
}
if auth.HasMailboxPassword() {
if mailboxPassword == "" {
http.Error(w, "Two password mode enabled but a mailbox password was not provided.", http.StatusUnauthorized)
return
}
} else {
mailboxPassword = password
}
user, err := f.bridge.FinishLogin(client, auth, []byte(mailboxPassword))
if err != nil {
f.processAPIError(err)
http.Error(w, fmt.Sprintf("Server error: %s", err.Error()), http.StatusUnauthorized)
return
}
fmt.Fprintf(w, "Account %s was added successfully.\n", user.Username())
if addressMode == "split" {
err = user.SwitchAddressMode()
if err != nil {
logrus.Errorf("Failed to switch address mode of %s to split: %s", user.Username(), err.Error())
http.Error(w, "Failed to switch address mode to split", http.StatusInternalServerError)
return
}
}
f.printAccountInfo(w, user)
}
func (f *frontendCLI) deleteAccount(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
account := params.ByName("account")
user := f.getUserByIndexOrName(account)
if user == nil {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Account %s does not exist.\n", account)
return
}
account = user.Username()
if err := f.bridge.DeleteUser(user.ID(), true); err != nil {
w.WriteHeader(http.StatusInternalServerError)
fmt.Fprintln(w, "Cannot delete account: ", err)
return
}
fmt.Fprintf(w, "Account %s was deleted successfully.\n", account)
}
func (f *frontendCLI) listAccounts(w http.ResponseWriter, r *http.Request, _ httprouter.Params) {
users := f.bridge.GetUsers()
if len(users) == 0 {
fmt.Fprintln(w, "No account found.")
return
}
spacing := "%-2d: %-20s (%-15s, %-15s)\n"
fmt.Fprintf(w, strings.ReplaceAll(spacing, "d", "s"), "#", "account", "status", "address mode")
for idx, user := range users {
connected := "disconnected"
if user.IsConnected() {
connected = "connected"
}
mode := "split"
if user.IsCombinedAddressMode() {
mode = "combined"
}
fmt.Fprintf(w, spacing, idx, user.Username(), connected, mode)
}
}
func (f *frontendCLI) showAccountInfo(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
account := params.ByName("account")
user := f.getUserByIndexOrName(account)
if user == nil {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintf(w, "Account %s does not exist.\n", account)
return
}
if !user.IsConnected() {
fmt.Fprintf(w, "Please login to %s to get email client configuration.\n", user.Username())
return
}
f.printAccountInfo(w, user)
}
func (f *frontendCLI) printAccountInfo(w io.Writer, user types.User) {
if user.IsCombinedAddressMode() {
f.printAccountAddressInfo(w, user, user.GetPrimaryAddress())
} else {
for _, address := range user.GetAddresses() {
f.printAccountAddressInfo(w, user, address)
}
}
}
func (f *frontendCLI) printAccountAddressInfo(w io.Writer, user types.User, address string) {
fmt.Fprintln(w, "Configuration for", address)
smtpSecurity := "STARTTLS"
if f.settings.GetBool(settings.SMTPSSLKey) {
smtpSecurity = "SSL"
}
fmt.Fprintf(w, "IMAP port: %s\nIMAP security: %s\nSMTP port: %s\nSMTP security: %s\nUsername: %s\nPassword: %s\n",
os.Getenv("PROTON_IMAP_PORT"),
"STARTTLS",
os.Getenv("PROTON_SMTP_PORT"),
smtpSecurity,
address,
user.GetBridgePassword(),
)
fmt.Fprintln(w, "")
}

View File

@@ -1,24 +0,0 @@
package cli
import (
"strconv"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
)
func (f *frontendCLI) getUserByIndexOrName(account string) types.User {
users := f.bridge.GetUsers()
numberOfAccounts := len(users)
if index, err := strconv.Atoi(account); err == nil {
if index < 0 || index >= numberOfAccounts {
return nil
}
return users[index]
}
for _, user := range users {
if user.Username() == account {
return user
}
}
return nil
}

View File

@@ -1,163 +0,0 @@
// Package cli provides HTTP interface of the bridge
package cli
import (
"context"
"fmt"
"net/http"
"os"
"strings"
"github.com/ProtonMail/proton-bridge/internal/config/settings"
"github.com/ProtonMail/proton-bridge/internal/events"
"github.com/ProtonMail/proton-bridge/internal/frontend/types"
"github.com/ProtonMail/proton-bridge/internal/locations"
"github.com/ProtonMail/proton-bridge/internal/updater"
"github.com/ProtonMail/proton-bridge/pkg/listener"
"github.com/julienschmidt/httprouter"
"github.com/sirupsen/logrus"
)
type frontendCLI struct {
*httprouter.Router
locations *locations.Locations
settings *settings.Settings
eventListener listener.Listener
updater types.Updater
bridge types.Bridger
restarter types.Restarter
}
func New(
panicHandler types.PanicHandler,
locations *locations.Locations,
settings *settings.Settings,
eventListener listener.Listener,
updater types.Updater,
bridge types.Bridger,
restarter types.Restarter,
) *frontendCLI {
fe := &frontendCLI{
Router: httprouter.New(),
locations: locations,
settings: settings,
eventListener: eventListener,
updater: updater,
bridge: bridge,
restarter: restarter,
}
fe.PUT("/accounts", fe.loginAccount)
fe.GET("/accounts", fe.listAccounts)
fe.GET("/accounts/:account", fe.showAccountInfo)
fe.DELETE("/accounts/:account", fe.deleteAccount)
return fe
}
func (f *frontendCLI) loginWithEnv() {
if len(f.bridge.GetUsers()) > 0 {
fmt.Println("More than 0 accounts found. Skip auto login.")
return
}
username := os.Getenv("PROTON_USERNAME")
password := os.Getenv("PROTON_PASSWORD")
if username == "" {
logrus.Info("PROTON_USERNAME and PROTON_PASSWORD are not set. Skip auto login.")
return
}
client, auth, err := f.bridge.Login(username, []byte(password))
if err != nil {
f.processAPIError(err)
logrus.WithError(err).Warn("Login failed.")
return
}
if auth.HasTwoFactor() {
twoFactor := os.Getenv("PROTON_2FA")
if twoFactor == "" {
logrus.Warn("Login failed: 2FA enabled for the account but PROTON_2FA was not set.")
return
}
err = client.Auth2FA(context.Background(), twoFactor)
if err != nil {
f.processAPIError(err)
logrus.WithError(err).Warn("Login failed.")
return
}
}
mailboxPassword := password
if auth.HasMailboxPassword() {
mailboxPassword = os.Getenv("PROTON_MAILBOX_PASSWORD")
if mailboxPassword == "" {
logrus.Warn("Login failed: Two password mode enabled but PROTON_MAILBOX_PASSWORD was not set.")
return
}
}
user, err := f.bridge.FinishLogin(client, auth, []byte(mailboxPassword))
if err != nil {
f.processAPIError(err)
logrus.WithError(err).Warn("Login failed.")
return
}
logrus.Infof("Account %s was added successfully.\n", user.Username())
if strings.ToLower(os.Getenv("PROTON_PRINT_ACCOUNT_INFO")) != "false" {
f.printAccountInfo(os.Stdout, user)
}
}
func (f *frontendCLI) watchEvents() {
errorCh := f.eventListener.ProvideChannel(events.ErrorEvent)
credentialsErrorCh := f.eventListener.ProvideChannel(events.CredentialsErrorEvent)
internetConnChangedCh := f.eventListener.ProvideChannel(events.InternetConnChangedEvent)
addressChangedCh := f.eventListener.ProvideChannel(events.AddressChangedEvent)
addressChangedLogoutCh := f.eventListener.ProvideChannel(events.AddressChangedLogoutEvent)
logoutCh := f.eventListener.ProvideChannel(events.LogoutEvent)
certIssue := f.eventListener.ProvideChannel(events.TLSCertIssue)
for {
select {
case errorDetails := <-errorCh:
logrus.Error("Bridge failed:", errorDetails)
case <-credentialsErrorCh:
f.notifyCredentialsError()
case stat := <-internetConnChangedCh:
if stat == events.InternetOff {
f.notifyInternetOff()
}
if stat == events.InternetOn {
f.notifyInternetOn()
}
case address := <-addressChangedCh:
fmt.Printf("Address changed for %s. You may need to reconfigure your email client.", address)
case address := <-addressChangedLogoutCh:
f.notifyLogout(address)
case userID := <-logoutCh:
user, err := f.bridge.GetUser(userID)
if err != nil {
return
}
f.notifyLogout(user.Username())
case <-certIssue:
f.notifyCertIssue()
}
}
}
func (f *frontendCLI) Loop() error {
f.loginWithEnv()
managementPort := os.Getenv("PROTON_MANAGEMENT_PORT")
if managementPort == "" {
managementPort = "1080"
}
http.ListenAndServe(":"+managementPort, f)
return nil
}
func (f *frontendCLI) NotifyManualUpdate(update updater.VersionInfo, canInstall bool) {}
func (f *frontendCLI) WaitUntilFrontendIsReady() {}
func (f *frontendCLI) SetVersion(version updater.VersionInfo) {}
func (f *frontendCLI) NotifySilentUpdateInstalled() {}
func (f *frontendCLI) NotifySilentUpdateError(err error) {}

View File

@@ -1,57 +0,0 @@
package cli
import (
"github.com/ProtonMail/proton-bridge/pkg/pmapi"
"github.com/sirupsen/logrus"
)
func (f *frontendCLI) processAPIError(err error) {
switch err {
case pmapi.ErrNoConnection:
f.notifyInternetOff()
case pmapi.ErrUpgradeApplication:
f.notifyNeedUpgrade()
}
}
func (f *frontendCLI) notifyInternetOff() {
logrus.Warn("Internet connection is not available.")
}
func (f *frontendCLI) notifyInternetOn() {
logrus.Info("Internet connection is available again.")
}
func (f *frontendCLI) notifyLogout(address string) {
logrus.Infof("Account %s is disconnected. Login to continue using this account with email client.", address)
}
func (f *frontendCLI) notifyNeedUpgrade() {
logrus.Info("Upgrade needed. Please download and install the newest version of application.")
}
func (f *frontendCLI) notifyCredentialsError() {
logrus.Error(`ProtonMail Bridge is not able to detect a supported password manager
(secret-service or pass). Please install and set up a supported password manager
and restart the application.
`)
}
func (f *frontendCLI) notifyCertIssue() {
// Print in 80-column width.
logrus.Error(`Connection security error: Your network connection to Proton services may
be insecure.
Description:
ProtonMail Bridge was not able to establish a secure connection to Proton
servers due to a TLS certificate error. This means your connection may
potentially be insecure and susceptible to monitoring by third parties.
Recommendation:
* If you trust your network operator, you can continue to use ProtonMail
as usual.
* If you don't trust your network operator, reconnect to ProtonMail over a VPN
(such as ProtonVPN) which encrypts your Internet connection, or use
a different network to access ProtonMail.
`)
}

View File

@@ -0,0 +1 @@
v1.8.7

View File

@@ -0,0 +1 @@
v1.8.9

View File

@@ -1,72 +0,0 @@
#!/bin/bash
set -e
# Generate gpg keys
GNUPG_PATH=/protonmail/data/.gnupg
export GNUPGHOME=${GNUPG_PATH}
if [ ! -d ${GNUPG_PATH} ]; then
echo "Generateing gpg keys..."
# set GNUPGHOME to a temp directory as a workaround for
#
# gpg-agent[106]: error binding socket to '/root/.gnupg/S.gpg-agent': File name too long
#
# when using docker volume mount
#
# ref: https://dev.gnupg.org/T2964
#
export GNUPGHOME=/tmp/gnupg
mkdir ${GNUPGHOME}
chmod 700 ${GNUPGHOME}
gpg --generate-key --batch /protonmail/script/gpgparams
pkill gpg-agent
mv ${GNUPGHOME} ${GNUPG_PATH}
export GNUPGHOME=${GNUPG_PATH}
fi
# Initialize pass
PASSWORD_STORE=/protonmail/data/.password-store
if [ ! -d ${PASSWORD_STORE} ]; then
echo "Initializing pass..."
pass init pass-key
# Move password store to /protonmail/data
mv ${HOME}/.password-store ${PASSWORD_STORE}
fi
# Link the password store back to ~/.password-store
# There is no easy way to change the path used by pass
ln -s ${PASSWORD_STORE} ${HOME}/.password-store
# Link config and cache folders to /protonmail/data
PROTON_CONFIG_PATH=/protonmail/data/config
PROTON_CACHE_PATH=/protonmail/data/cache
mkdir -p ${PROTON_CONFIG_PATH}
mkdir -p ${HOME}/.config
ln -s ${PROTON_CONFIG_PATH} ${HOME}/.config/protonmail
mkdir -p ${PROTON_CACHE_PATH}
mkdir ${HOME}/.cache
ln -s ${PROTON_CACHE_PATH} ${HOME}/.cache/protonmail
# Generateing perfs.json
mkdir -p ${PROTON_CONFIG_PATH}/bridge
if [ ${PROTON_SMTP_SECURITY:-STARTTLS} == "SSL" ]; then
PROTON_SSL_SMTP="true"
else
PROTON_SSL_SMTP="false"
fi
cat <<EOF > ${PROTON_CONFIG_PATH}/bridge/prefs.json
{
"allow_proxy": "${PROTON_ALLOW_PROXY:-true}",
"autoupdate": "false",
"user_ssl_smtp": "${PROTON_SSL_SMTP}"
}
EOF
export PROTON_SMTP_PORT=${PROTON_SMTP_PORT:-25}
export PROTON_IMAP_PORT=${PROTON_IMAP_PORT:=143}
# socat will make the conn appear to come from 127.0.0.1
# ProtonMail Bridge currently expects that.
# It also allows us to bind to the real ports :)
socat TCP-LISTEN:${PROTON_SMTP_PORT},fork TCP:127.0.0.1:1025 &
socat TCP-LISTEN:${PROTON_IMAP_PORT},fork TCP:127.0.0.1:1143 &
exec proton-bridge --cli "$@"

4
version-config.yaml Normal file
View File

@@ -0,0 +1,4 @@
image_version: 0.1
protonmail_version:
latest: v1.8.7 # Latest stable version
pre: v1.8.9 # Latest preview version