Skip to content

Commit

Permalink
Add XOAuth2 support for GMail (#42)
Browse files Browse the repository at this point in the history
* Add XOAuth2 support for GMail

* Attempt to support integration tests
  • Loading branch information
imartinezortiz committed Nov 6, 2020
1 parent 4ba3145 commit 16771d4
Show file tree
Hide file tree
Showing 14 changed files with 577 additions and 7 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.idea
helm/fixtures
public
gh-pages
gh-pages
.env
19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,19 @@
ARG ALPINE_VERSION=latest
FROM alpine:${ALPINE_VERSION} as build

ARG SASL_XOAUTH2_REPO_URL=https://github.com/tarickb/sasl-xoauth2.git
ARG SASL_XOAUTH2_GIT_REF=release-0.9

RUN true && \
apk add --no-cache --upgrade git && \
apk add --no-cache --upgrade cmake clang make gcc g++ libc-dev pkgconfig curl-dev jsoncpp-dev cyrus-sasl-dev && \
git clone --depth 1 --branch ${SASL_XOAUTH2_GIT_REF} ${SASL_XOAUTH2_REPO_URL} /sasl-xoauth2 && \
cd /sasl-xoauth2 && \
mkdir build && \
cd build && \
cmake -DCMAKE_INSTALL_PREFIX=/ .. && \
make

FROM alpine:${ALPINE_VERSION}
LABEL maintaner="Bojan Cekrlic - https://github.com/bokysan/docker-postfix/"

Expand All @@ -10,8 +25,12 @@ RUN true && \
apk add --no-cache postfix && \
apk add --no-cache opendkim && \
apk add --no-cache --upgrade ca-certificates tzdata supervisor rsyslog musl musl-utils bash opendkim-utils && \
apk add --no-cache --upgrade libcurl jsoncpp && \
(rm "/tmp/"* 2>/dev/null || true) && (rm -rf /var/cache/apk/* 2>/dev/null || true)

# Copy SASL-XOAUTH2 plugin
COPY --from=build /sasl-xoauth2/build/src/libsasl-xoauth2.so /usr/lib/sasl2/

# Set up configuration
COPY /configs/supervisord.conf /etc/supervisord.conf
COPY /configs/rsyslog*.conf /etc/
Expand Down
79 changes: 74 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ Simple postfix relay host ("postfix null client") for your Docker containers. Ba
* [Postfix-specific options](#postfix-specific-options)
* [RELAYHOST, RELAYHOST_USERNAME and RELAYHOST_PASSWORD](#relayhost-relayhost_username-and-relayhost_password)
* [RELAYHOST_TLS_LEVEL](#relayhost_tls_level)
* [XOAUTH2_CLIENT_ID, XOAUTH2_SECRET, XOAUTH2_INITIAL_ACCESS_TOKEN and XOAUTH2_INITIAL_REFRESH_TOKEN](#xoauth2_client_id-xoauth2_secret-xoauth2_initial_access_token-and-xoauth2_initial_refresh_token)
* [MASQUERADED_DOMAINS](#masqueraded_domains)
* [SMTP_HEADER_CHECKS](#smtp_header_checks)
* [POSTFIX_hostname](#postfix_hostname)
Expand All @@ -27,6 +28,7 @@ Simple postfix relay host ("postfix null client") for your Docker containers. Ba
* [Changing the DKIM selector](#changing-the-dkim-selector)
* [Overriding specific OpenDKIM settings](#overriding-specific-opendkim-settings)
* [Verifying your DKIM setup](#verifying-your-dkim-setup)
* [Docker Secrets](#docker-secrets)
* [Helm chart](#helm-chart)
* [Extending the image](#extending-the-image)
* [Using custom init scripts](#using-custom-init-scripts)
Expand Down Expand Up @@ -137,7 +139,11 @@ To change the log format, set the (unsurprisingly named) variable `LOG_FORMAT=js
* `RELAYHOST` = Host that relays your messages
* `RELAYHOST_USERNAME` = An (optional) username for the relay server
* `RELAYHOST_PASSWORD` = An (optional) login password for the relay server
* `RELAYHOST_TLS_LEVEL` = Relay host TLS connection leve
* `RELAYHOST_TLS_LEVEL` = Relay host TLS connection level
* `XOAUTH2_CLIENT_ID` = OAuth2 client id used when configured as a relayhost.
* `XOAUTH2_SECRET` = OAuth2 secret used when configured as a relayhost.
* `XOAUTH2_INITIAL_ACCESS_TOKEN` = Initial OAuth2 access token.
* `XOAUTH2_INITIAL_REFRESH_TOKEN` = Initial OAuth2 refresh token.
* `MASQUERADED_DOMAINS` = domains where you want to masquerade internal hosts
* `SMTP_HEADER_CHECKS`= Set to `1` to enable header checks of to a location of the file for header checks
* `POSTFIX_hostname` = Set tha name of this postfix server
Expand Down Expand Up @@ -181,6 +187,48 @@ Define relay host TLS connection level. See [smtp_tls_security_level](http://www

This level defines how the postfix will connect to your upstream server.

#### `XOAUTH2_CLIENT_ID`, `XOAUTH2_SECRET`, `XOAUTH2_INITIAL_ACCESS_TOKEN` and `XOAUTH2_INITIAL_REFRESH_TOKEN`

> Note: These parameters are used when `RELAYHOST` and `RELAYHOST_USERNAME` are provided.
These parameters allow you to configure a relayhost that requires (or recommends) the [XOAuth2 authentication method](https://github.com/tarickb/sasl-xoauth2) (e.g. GMail).
- `XOAUTH2_CLIENT_ID` and `XOAUTH2_SECRET` are the [OAuth2 client credentials](#oauth2-client-credentials-gmail).
- `XOAUTH2_INITIAL_ACCESS_TOKEN` and `XOAUTH2_INITIAL_REFRESH_TOKEN` are the [initial access token and refresh tokens](#obtain-initial-access-token-gmail). These values are only required to initialize the token file `/var/spool/postfix/xoauth2-tokens/$RELAYHOST_USERNAME`.

Example:
```
docker run --rm --name pruebas-postfix \
-e RELAYHOST="[smtp.gmail.com]:587" \
-e RELAYHOST_USERNAME="<put.your.account>@gmail.com" \
-e RELAYHOST_TLS_LEVEL="encrypt" \
-e XOAUTH2_CLIENT_ID="<put_your_oauth2_client_id>" \
-e XOAUTH2_SECRET="<put_your_oauth2_secret>" \
-e ALLOW_EMPTY_SENDER_DOMAINS="true" \
-e XOAUTH2_INITIAL_ACCESS_TOKEN="<put_your_acess_token>" \
-e XOAUTH2_INITIAL_REFRESH_TOKEN="<put_your_refresh_token>" \
boky/postfix
```
Next sections describe how to obtain these values.

##### OAuth2 Client Credentials (GMail)

Visit the [Google API Console](https://console.developers.google.com/) to obtain OAuth 2 credentials (a client ID and client secret) for an "Installed application" application type.

Save the client ID and secret and use them to initialize `XOAUTH2_CLIENT_ID` and `XOAUTH2_SECRET` respectively.

We'll also need these credentials in the next step.

##### Obtain Initial Access Token (GMail)

Use the [Gmail OAuth2 developer tools](https://github.com/google/gmail-oauth2-tools/) to obtain an OAuth token by following the [Creating and Authorizing an OAuth Token](https://github.com/google/gmail-oauth2-tools/wiki/OAuth2DotPyRunThrough#creating-and-authorizing-an-oauth-token) instructions.

Save the resulting tokens and use them to initialize `XOAUTH2_INITIAL_ACCESS_TOKEN` and `XOAUTH2_INITIAL_REFRESH_TOKEN`.

##### Debug XOAuth2 issues

If you have XOAuth2 authentication issues you can enable XOAuth2 debug message setting `XOAUTH2_SYSLOG_ON_FAILURE` to `"yes"` (default: `"no"`). If you need a more detailed log trace about XOAuth2 you can set `XOAUTH2_FULL_TRACE` to `"yes"` (default: `"no"`).


#### `MASQUERADED_DOMAINS`

If you don't want outbound mails to expose hostnames, you can use this variable to enable Postfix's
Expand Down Expand Up @@ -333,6 +381,26 @@ variable from OpenDKIM config.
I strongly suggest using a service such as [dkimvalidator](https://dkimvalidator.com/) to make sure your keys are set up
properly and your DNS server is serving them with the correct records.
### Docker Secrets
As an alternative to passing sensitive information via environment variables, _FILE may be appended to some environment variables (see below), causing the initialization script to load the values for those variables from files present in the container. In particular, this can be used to load passwords from Docker secrets stored in /run/secrets/<secret_name> files. For example:
```
docker run --rm --name pruebas-postfix \
-e RELAYHOST="[smtp.gmail.com]:587" \
-e RELAYHOST_USERNAME="<put.your.account>@gmail.com" \
-e RELAYHOST_TLS_LEVEL="encrypt" \
-e XOAUTH2_CLIENT_ID_FILE="/run/secrets/xoauth2-client-id" \
-e XOAUTH2_SECRET_FILE="/run/secrets/xoauth2-secret" \
-e ALLOW_EMPTY_SENDER_DOMAINS="true" \
-e XOAUTH2_INITIAL_ACCESS_TOKEN_FILE="/run/secrets/xoauth2-access-token" \
-e XOAUTH2_INITIAL_REFRESH_TOKEN_FILE="/run/secrets/xoauth2-refresh-token" \
boky/postfix
```
Currently, this is only supported for `XOAUTH2_CLIENT_ID`, `XOAUTH2_SECRET`, `XOAUTH2_INITIAL_ACCESS_TOKEN` and `XOAUTH2_INITIAL_REFRESH_TOKEN`.
## Helm chart
This image comes with its own helm chart. The chart versions are aligned with the releases of the image. Charts are hosted
Expand Down Expand Up @@ -428,11 +496,12 @@ account which will use `UID:GID` of `100:101`. `opendkim` will run under account
### Relaying messages through your Gmail account
Please note that Gmail does not support using your password with non-OAuth2 clients, which -- technically -- postfix is.
You will need to enable [Less secure apps](https://support.google.com/accounts/answer/6010255?hl=en) in your account
and assign an "app password". You'll also need to use (only) your email as the sender address.
Please note that Gmail does not support using your password with non-OAuth2 clients. You will need to either enable
[Less secure apps](https://support.google.com/accounts/answer/6010255?hl=en) in your account and assign an "app password"
or [configure postfix support for XOAuth2 authentication](#xoauth2_client_id-xoauth2_secret-xoauth2_initial_access_token-and-xoauth2_initial_refresh_token).
You'll also need to use (only) your email as the sender address.
Your configuration would be as follows:
If you follow the *less than secure* route, your configuration would be as follows:
```shell script
RELAYHOST=smtp.gmail.com:587
Expand Down
3 changes: 2 additions & 1 deletion integration-tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ run_test() {
set +e
docker-compose up --build --abort-on-container-exit --exit-code-from tests
exit_code="$?"
docker-compose down

docker-compose down -v
if [[ "$exit_code" != 0 ]]; then
exit "$exit_code"
fi
Expand Down
49 changes: 49 additions & 0 deletions integration-tests/xoauth2-error/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
version: '3.7'

volumes:
logs-volume:

services:
postfix_test_587:
hostname: "postfix"
image: "boky/postfix"
build:
context: ../..
restart: always
healthcheck:
test: [ "CMD", "sh", "-c", "netstat -an | fgrep 587 | fgrep -q LISTEN" ]
interval: 10s
timeout: 5s
start_period: 10s
retries: 2
environment:
FORCE_COLOR: "1"
ALLOW_EMPTY_SENDER_DOMAINS: "true"
RELAYHOST: "[smtp.gmail.com]:587"
RELAYHOST_USERNAME: "${RELAYHOST_USERNAME}"
RELAYHOST_TLS_LEVEL: "encrypt"
XOAUTH2_CLIENT_ID: "${XOAUTH2_CLIENT_ID}"
XOAUTH2_SECRET: "${XOAUTH2_SECRET}"
XOAUTH2_INITIAL_ACCESS_TOKEN: "${XOAUTH2_INITIAL_ACCESS_TOKEN}"
XOAUTH2_INITIAL_REFRESH_TOKEN: "${XOAUTH2_INITIAL_REFRESH_TOKEN}"
XOAUTH2_SYSLOG_ON_FAILURE: "no"
XOAUTH2_FULL_TRACE: "no"
LOG_FORMAT: "json"
POSTFIX_maillog_file: "/logs/postfix.log"
POSTFIX_maillog_file_prefixes: "/var,/dev/sdout,/logs"
volumes:
- "logs-volume:/logs"
tests:
image: "boky/postfix-integration-test"
restart: "no"
volumes:
- "../xoauth2/common:/common"
- "./it:/code"
- "logs-volume:/logs"
build:
context: ../tester
command: "/"
environment:
FROM: "${FROM}"
TO: "${TO}"
SKIP_INVALID_DOMAIN_SEND: "1"
42 changes: 42 additions & 0 deletions integration-tests/xoauth2-error/it/test.bats
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env bats

if [ -z "$FROM" ]; then
FROM=$1
shift
fi

if [ -z "$TO" ]; then
TO=$1
shift
fi

# Wait for postfix to startup
wait-for-service -q tcp://postfix_test_587:587

SMTP_DATA="-smtp postfix_test_587 -port 587"

load /common/common-xoauth2.sh

@test "Relay email with proper XOAuth2 credentials" {
local message_id="12345.test@example.com"
local postfix_message_id=''
local smtp_result=''
local status=''

mailsend \
-sub "Test email 1" $SMTP_DATA \
-from $FROM \
-to $TO \
header \
-name "Message-ID" \
-value "${message_id}" \
body \
-msg "Hello world!\nThis is a simple test message!"

postfix_message_id=$(get_postfix_message_id '/logs/postfix.log' ${message_id})
smtp_result=$(get_smtp_result '/logs/postfix.log' "${postfix_message_id}")
status=$(get_param_value "${smtp_result}" 'status')

[ -n "$status" ]
echo "$status" | grep -q -E "^deferred"
}
86 changes: 86 additions & 0 deletions integration-tests/xoauth2/common/common-xoauth2.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#!/usr/bin/env bash

get_postfix_message_id() {
local log_file=$1
local message_id=$2
local postfix_message_id=''
local result='ko'
local count=0;
local max_wait=15
while [[ $result == 'ko' ]] && [[ $count -lt $max_wait ]]; do
postfix_message_id=$(cat ${log_file} | grep -E -e 'postfix/cleanup' | cut -d':' -f4- | sed -r -e "s/\s*([^:]+)\s*:\s*message-id=${message_id}/\1/")
if [[ -n "$postfix_message_id" ]]; then
result='ok'
else
sleep 1
count=$((count+1))
fi
done

if [[ $count -eq $max_wait ]]; then
echo >&2 "Message with message-id='${message_id}' not found"
return 1
fi

echo ${postfix_message_id}
return 0
}

get_smtp_result() {
local log_file=$1
local postfix_message_id=$2
local smtp_result=''
local result='ko'
local count=0
local max_wait=15

while [[ $result == 'ko' ]] && [[ $count -lt $max_wait ]]; do
smtp_result=$(cat ${log_file} | grep -E -e 'postfix/smtp\[' | cut -d':' -f4- | sed -r -e "s/${postfix_message_id}:(.*)/\1/g")
if [[ -n "$smtp_result" ]]; then
result='ok'
else
sleep 1
count=$((count+1))
fi
done

if [[ $count -eq $max_wait ]]; then
echo >&2 "Message with postfix id='${postfix_message_id}' not found"
return 1
fi

echo ${smtp_result}
return 0
}

get_param_value() {
local smtp_result=$1
local param_name_to_search=$2
local params=''
local param_name=''
local param_value=''
local status=''

local old_ifs=$IFS
IFS=','
read -ra params <<< "${smtp_result}"
IFS=$old_ifs

for i in "${params[@]}"; do
param_name=$(echo $i | cut -d'=' -f1)
param_value=$(echo $i | cut -d'=' -f2)
if [[ "${param_name}" == "${param_name_to_search}" ]]; then
status="${param_value}"
break;
fi
echo $i;
done

if [[ -z "${status}" ]]; then
echo "${param_name_to_search} not found!."
return 1
fi

echo "${status}"
return 0
}

0 comments on commit 16771d4

Please sign in to comment.