Compare commits
257 commits
renovate/s
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| b3b1a094de | |||
| 809bca02e4 | |||
| 1b8627ef86 | |||
| 369a13526f | |||
| 3161e392e8 | |||
| 1516991057 | |||
| 0de67af847 | |||
| e324e6cbf9 | |||
| 3e53fae713 | |||
| b3137f7d37 | |||
| bfe6ff81fa | |||
| 5ffc731297 | |||
| 454da476eb | |||
| eaab446504 | |||
| cf63276a07 | |||
| 09d777634c | |||
| 15120d8598 | |||
| 6f6211e833 | |||
| 42cf6f450d | |||
| 31a27c120b | |||
| 396c51974a | |||
| 3eec5ce966 | |||
| 7422f6ed0a | |||
| e540377bd9 | |||
| 16b7dcb057 | |||
| dfa38e8a26 | |||
| dee848d887 | |||
| b158336451 | |||
| a36824cf27 | |||
| 7d3009d7d0 | |||
| 5c104f3c99 | |||
| 3c192f17fd | |||
| 35fc997390 | |||
| 2fcee1b885 | |||
| 26025c96a2 | |||
| 76ee50a100 | |||
| 71e0832416 | |||
| c96a8b92b8 | |||
| b1c18a3894 | |||
| c8e28c31ee | |||
| 1d8ee637da | |||
| 968f42761f | |||
| 2b70115834 | |||
| d65840000a | |||
| 61503a1c1f | |||
| 26025644b0 | |||
| bd02b8f9ba | |||
| a3b539179e | |||
| 8b6154c183 | |||
| 56e6494a75 | |||
| 0176c3803d | |||
| 21e16fd847 | |||
| edfe498b27 | |||
| 27650a3496 | |||
| d9b9ea87c6 | |||
| bb47bb7c29 | |||
| da93d6d706 | |||
| 2a2bfe46a8 | |||
| 55e9bcd3d0 | |||
| 28424729a5 | |||
| 3cc39c9c54 | |||
| f9c5c815d1 | |||
| 4245f93ce4 | |||
| 9679b381c7 | |||
| 7b9c45fb68 | |||
| b619ebf8c3 | |||
| a146940a65 | |||
| e811d02b3b | |||
| 8fda7746a1 | |||
| 96e83ff70d | |||
| 6b983f0506 | |||
| c50e18a347 | |||
| 054cd8ae25 | |||
| c2917f8580 | |||
| b39a9dc625 | |||
| 88553cd3c8 | |||
| 8a10eef2f5 | |||
| 64ba6932f7 | |||
| 5453c09420 | |||
| 6b4ca126b0 | |||
| ac9b567025 | |||
| 035e864de4 | |||
| a6efd7710e | |||
| e6746a1382 | |||
| d1e48b9885 | |||
| 9ac3e165fa | |||
| dc21b72f52 | |||
| 1ba35c6f9f | |||
| 0fda0f88c1 | |||
| 57a3774d28 | |||
| 11d46de033 | |||
| 6081e486bf | |||
| 528a65ca04 | |||
| 926796b79e | |||
| 5d02070100 | |||
| 5701070cc1 | |||
| 954cbe29fc | |||
| ca2ac3df7c | |||
| 016ed7180e | |||
| 3e76692448 | |||
| e23afcc77c | |||
| d81ff1731c | |||
| eef6480e75 | |||
| f2261adb54 | |||
| 3bcbb5814d | |||
| 5ac0e2a8bf | |||
| a1e8dd35bd | |||
| e194fcc5b1 | |||
| c19f545df0 | |||
| 03b58b6f19 | |||
| a3ca8ffb48 | |||
| 27d5220687 | |||
| 723bec622a | |||
| ee9fa59dbc | |||
| e05c6d0bc2 | |||
| 04d8b150b4 | |||
| e28a96508d | |||
| ea71074cc8 | |||
| 644dfda223 | |||
| 447a666ae7 | |||
| 2172603ad5 | |||
| c91ab96642 | |||
| 18c8622513 | |||
| deb9fd4f51 | |||
| c52a3aa8a7 | |||
| 5b179e7b93 | |||
| 465da6d16a | |||
| d870fc8130 | |||
| 1c4eb0653e | |||
| 372c9c5153 | |||
| 3b301a415f | |||
| 7231669362 | |||
| bc6a6397ad | |||
| e166e75e42 | |||
| d3f69630c9 | |||
| 9e9e76cf42 | |||
| 65c8e9a528 | |||
| 718b624fb8 | |||
| 099965c1f9 | |||
| 90dda126ad | |||
| 3a8a25ddeb | |||
| b01ca9b38c | |||
| 20fe4e5b97 | |||
| 706dc6eed9 | |||
| 164b2a98ab | |||
| dccf75b238 | |||
| f0dbc29da4 | |||
| 8769514f1c | |||
| 871f4e62f6 | |||
| 86ec7a6100 | |||
| 2d3316eaaf | |||
| 730b43cad1 | |||
| ff5ac0fe1f | |||
| b95e5d6732 | |||
| 0325139461 | |||
| 07c7e63ee7 | |||
| faf860f4a1 | |||
| 6d2a59dd7b | |||
| 39185f82bd | |||
| dd14d7a814 | |||
| 924c54912a | |||
| 20513bfc00 | |||
| b98988c153 | |||
| 386951b1eb | |||
| 1a373d1942 | |||
| 426fad9856 | |||
| 2bedd0ed75 | |||
| cdfdeb74fc | |||
| 3c5bd5cbbd | |||
| c5bb9e46ac | |||
| 4c71dd1d53 | |||
| 76de5f60d6 | |||
| 82373cdaac | |||
| edb172c4bc | |||
| 53a48cba07 | |||
| 08c6e0eef2 | |||
| 932bc981b5 | |||
| b2dc479a79 | |||
| 6e2e403873 | |||
| eb28499dfd | |||
| ff1a958220 | |||
| 38b2be58fe | |||
| 52f43c6bc5 | |||
| 3ea958b2fd | |||
| a700db0873 | |||
| 115da72874 | |||
| 255027d00b | |||
| 9970e957d5 | |||
| 8fe8581b78 | |||
| aa35ab223d | |||
| 29cb2cf1f9 | |||
| 7ed347c86e | |||
| 4bbba66a81 | |||
| 8b3ab541ba | |||
| 3588af3267 | |||
| c1063cb4aa | |||
| 3d03bfc4fa | |||
| 84a504d668 | |||
| 7bc7e7b7a2 | |||
| 326abc0744 | |||
| a6448a1533 | |||
| e5c678174c | |||
| 5d335c6a6c | |||
| a97729fea6 | |||
| 4149a5de92 | |||
| 8ca4bed875 | |||
| f6a1ea73a2 | |||
| a64b866cfa | |||
| 33d394a27b | |||
| 41013d8af2 | |||
| e77bffb04f | |||
| abfd1f0155 | |||
| fdb43533cd | |||
| eb210e7bed | |||
| c51f8e5904 | |||
| 866cf2e5db | |||
| eadc7ff8ca | |||
| 0581e0cf6b | |||
| ec1ab7886e | |||
| d87b0cbcb0 | |||
| 954a9d705e | |||
| 1be917136c | |||
| 74aee54432 | |||
| dfc0eeb323 | |||
| 0ac51ac06d | |||
| e3d89dc953 | |||
| 8bf3b500d9 | |||
| 0107858ee6 | |||
| 4d637214de | |||
| 849bdb53c5 | |||
| 3f5e2c6dd4 | |||
| 0084fd9660 | |||
| a687f5cb6b | |||
| 243ca4ba11 | |||
| 3c58f5ccd5 | |||
| 1fa7af4c2b | |||
| 30f774c1fb | |||
| cd40b7c3ea | |||
| 8313fd7d98 | |||
| 6097eb54c6 | |||
| 7e603ddf4a | |||
| e7aa80bef4 | |||
| b2bbf0ee78 | |||
| 6096e043c6 | |||
| 9ff2ca30cc | |||
| 4304784796 | |||
| 6565c6fda4 | |||
| 079dc6a813 | |||
| 16a0f3a158 | |||
| 20f5b37e5e | |||
| 3867fa36a2 | |||
| 18c2f95112 | |||
| 4b9733531e | |||
| 682ca6bb20 | |||
| 3d823dedd8 | |||
| 62bb85ebec | |||
| 449a8a2c67 |
171 changed files with 35902 additions and 6734 deletions
27
.dockerignore
Normal file
27
.dockerignore
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
# Git files
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Documentation
|
||||
*.md
|
||||
!README.md
|
||||
|
||||
# Build artifacts
|
||||
happyDeliver
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# IDE and editor files
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Logs files
|
||||
logs/
|
||||
|
||||
# Test files
|
||||
*_test.go
|
||||
testdata/
|
||||
22
.drone-manifest.yml
Normal file
22
.drone-manifest.yml
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
image: happydomain/happydeliver:{{#if build.tag}}{{trimPrefix "v" build.tag}}{{else}}latest{{/if}}
|
||||
{{#if build.tags}}
|
||||
tags:
|
||||
{{#each build.tags}}
|
||||
- {{this}}
|
||||
{{/each}}
|
||||
{{/if}}
|
||||
manifests:
|
||||
- image: happydomain/happydeliver:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-amd64
|
||||
platform:
|
||||
architecture: amd64
|
||||
os: linux
|
||||
- image: happydomain/happydeliver:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm64
|
||||
platform:
|
||||
architecture: arm64
|
||||
os: linux
|
||||
variant: v8
|
||||
- image: happydomain/happydeliver:{{#if build.tag}}{{trimPrefix "v" build.tag}}-{{/if}}linux-arm
|
||||
platform:
|
||||
architecture: arm
|
||||
os: linux
|
||||
variant: v7
|
||||
156
.drone.yml
Normal file
156
.drone.yml
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-arm64
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: frontend
|
||||
image: node:24-alpine
|
||||
commands:
|
||||
- cd web
|
||||
- npm install --network-timeout=100000
|
||||
- npm run generate:api
|
||||
- npm run build
|
||||
|
||||
- name: backend-commit
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go generate ./...
|
||||
- go build -tags netgo -ldflags '-w -X git.happydns.org/happyDeliver/internal/version.Version=${DRONE_BRANCH}-${DRONE_COMMIT} -X main.build=${DRONE_BUILD_NUMBER}' -o happydeliver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDeliver
|
||||
- ln happydeliver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydeliver
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: backend-tag
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go generate ./...
|
||||
- go build -tags netgo -ldflags '-w -X git.happydns.org/happyDeliver/internal/version.Version=${DRONE_TAG##v} -X main.build=${DRONE_BUILD_NUMBER}' -o happydeliver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} ./cmd/happyDeliver/
|
||||
- ln happydeliver-${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH} happydeliver
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: build-commit macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo -ldflags '-w -X "git.happydns.org/happyDeliver/internal/version.Version=${DRONE_BRANCH}-${DRONE_COMMIT}" -X main.build=${DRONE_BUILD_NUMBER}' -o happydeliver-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDeliver/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: arm64
|
||||
when:
|
||||
event:
|
||||
exclude:
|
||||
- tag
|
||||
|
||||
- name: build-tag macOS
|
||||
image: golang:1-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- go build -tags netgo -ldflags '-w -X "git.happydns.org/happyDeliver/internal/version.Version=${DRONE_TAG##v}" -X main.build=${DRONE_BUILD_NUMBER}' -o happydeliver-darwin-${DRONE_STAGE_ARCH} ./cmd/happyDeliver/
|
||||
environment:
|
||||
CGO_ENABLED: 0
|
||||
GOOS: darwin
|
||||
GOARCH: arm64
|
||||
when:
|
||||
event:
|
||||
- tag
|
||||
|
||||
- name: publish on Docker Hub
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: happydomain/happydeliver
|
||||
auto_tag: true
|
||||
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
dockerfile: Dockerfile
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
exclude:
|
||||
- renovate/*
|
||||
event:
|
||||
- cron
|
||||
- push
|
||||
- tag
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
type: docker
|
||||
name: build-amd64
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
- name: publish on Docker Hub
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: happydomain/happydeliver
|
||||
auto_tag: true
|
||||
auto_tag_suffix: ${DRONE_STAGE_OS}-${DRONE_STAGE_ARCH}
|
||||
dockerfile: Dockerfile
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
exclude:
|
||||
- renovate/*
|
||||
event:
|
||||
- cron
|
||||
- push
|
||||
- tag
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: docker-manifest
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
- name: publish on Docker Hub
|
||||
image: plugins/manifest
|
||||
settings:
|
||||
auto_tag: true
|
||||
ignore_missing: true
|
||||
spec: .drone-manifest.yml
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
|
||||
trigger:
|
||||
branch:
|
||||
exclude:
|
||||
- renovate/*
|
||||
event:
|
||||
- cron
|
||||
- push
|
||||
- tag
|
||||
|
||||
depends_on:
|
||||
- build-amd64
|
||||
- build-arm64
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -17,11 +17,14 @@ vendor/
|
|||
.env.local
|
||||
*.local
|
||||
|
||||
# Logs files
|
||||
logs/
|
||||
|
||||
# Database files
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# OpenAPI generated files
|
||||
internal/api/models.gen.go
|
||||
internal/api/server.gen.go
|
||||
internal/model/types.gen.go
|
||||
|
|
|
|||
193
Dockerfile
Normal file
193
Dockerfile
Normal file
|
|
@ -0,0 +1,193 @@
|
|||
# Multi-stage Dockerfile for happyDeliver with integrated MTA
|
||||
# Stage 1: Build the Svelte application
|
||||
FROM node:24-alpine AS nodebuild
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY api/ api/
|
||||
COPY web/ web/
|
||||
|
||||
RUN yarn --cwd web install && \
|
||||
yarn --cwd web run generate:api && \
|
||||
yarn --cwd web --offline build
|
||||
|
||||
# Stage 2: Build the Go application
|
||||
FROM golang:1-alpine AS builder
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
# Install build dependencies
|
||||
RUN apk add --no-cache ca-certificates git gcc musl-dev
|
||||
|
||||
# Copy go mod files
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
# Copy source code
|
||||
COPY . .
|
||||
COPY --from=nodebuild /build/web/build/ ./web/build/
|
||||
|
||||
# Build the application
|
||||
RUN go generate ./... && \
|
||||
CGO_ENABLED=1 GOOS=linux go build -a -installsuffix cgo -ldflags="-w -s" -o happyDeliver ./cmd/happyDeliver
|
||||
|
||||
# Stage 3: Prepare perl and spamass-milt
|
||||
FROM alpine:3 AS pl
|
||||
|
||||
RUN echo "@edge https://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \
|
||||
apk add --no-cache \
|
||||
build-base \
|
||||
libmilter-dev \
|
||||
musl-obstack-dev \
|
||||
openssl \
|
||||
openssl-dev \
|
||||
perl-app-cpanminus \
|
||||
perl-alien-libxml2 \
|
||||
perl-class-load-xs \
|
||||
perl-cpanel-json-xs \
|
||||
perl-crypt-openssl-rsa \
|
||||
perl-crypt-openssl-random \
|
||||
perl-crypt-openssl-verify \
|
||||
perl-crypt-openssl-x509 \
|
||||
perl-cryptx \
|
||||
perl-dbd-sqlite \
|
||||
perl-dbi \
|
||||
perl-email-address-xs \
|
||||
perl-json-xs \
|
||||
perl-list-moreutils \
|
||||
perl-moose \
|
||||
perl-net-idn-encode@edge \
|
||||
perl-net-ssleay \
|
||||
perl-netaddr-ip \
|
||||
perl-package-stash \
|
||||
perl-params-util \
|
||||
perl-params-validate \
|
||||
perl-proc-processtable \
|
||||
perl-sereal-decoder \
|
||||
perl-sereal-encoder \
|
||||
perl-socket6 \
|
||||
perl-sub-identify \
|
||||
perl-variable-magic \
|
||||
perl-xml-libxml \
|
||||
perl-dev \
|
||||
spamassassin-client \
|
||||
zlib-dev \
|
||||
&& \
|
||||
ln -s /usr/bin/ld /bin/ld
|
||||
|
||||
RUN cpanm --notest Mail::SPF && \
|
||||
cpanm --notest Mail::DKIM && \
|
||||
cpanm --notest Mail::Milter::Authentication
|
||||
|
||||
RUN wget https://download.savannah.nongnu.org/releases/spamass-milt/spamass-milter-0.4.0.tar.gz && \
|
||||
tar xzf spamass-milter-0.4.0.tar.gz && \
|
||||
cd spamass-milter-0.4.0 && \
|
||||
./configure && make install
|
||||
|
||||
# Stage 4: Runtime image with Postfix and all filters
|
||||
FROM alpine:3
|
||||
|
||||
# Install all required packages
|
||||
RUN echo "@edge https://dl-cdn.alpinelinux.org/alpine/edge/community" >> /etc/apk/repositories && \
|
||||
apk add --no-cache \
|
||||
bash \
|
||||
ca-certificates \
|
||||
libmilter \
|
||||
openssl \
|
||||
perl \
|
||||
perl-alien-libxml2 \
|
||||
perl-class-load-xs \
|
||||
perl-cpanel-json-xs \
|
||||
perl-crypt-openssl-rsa \
|
||||
perl-crypt-openssl-random \
|
||||
perl-crypt-openssl-verify \
|
||||
perl-crypt-openssl-x509 \
|
||||
perl-cryptx \
|
||||
perl-dbd-sqlite \
|
||||
perl-dbi \
|
||||
perl-email-address-xs \
|
||||
perl-json-xs \
|
||||
perl-list-moreutils \
|
||||
perl-moose \
|
||||
perl-net-idn-encode@edge \
|
||||
perl-net-ssleay \
|
||||
perl-netaddr-ip \
|
||||
perl-package-stash \
|
||||
perl-params-util \
|
||||
perl-params-validate \
|
||||
perl-proc-processtable \
|
||||
perl-sereal-decoder \
|
||||
perl-sereal-encoder \
|
||||
perl-socket6 \
|
||||
perl-sub-identify \
|
||||
perl-variable-magic \
|
||||
perl-xml-libxml \
|
||||
postfix \
|
||||
postfix-pcre \
|
||||
rspamd \
|
||||
spamassassin \
|
||||
spamassassin-client \
|
||||
supervisor \
|
||||
sqlite \
|
||||
tzdata \
|
||||
&& rm -rf /var/cache/apk/*
|
||||
|
||||
# Copy Mail::Milter::Authentication and its dependancies
|
||||
COPY --from=pl /usr/local/ /usr/local/
|
||||
|
||||
# Create happydeliver user and group
|
||||
RUN addgroup -g 1000 happydeliver && \
|
||||
adduser -D -u 1000 -G happydeliver happydeliver
|
||||
|
||||
# Create necessary directories
|
||||
RUN mkdir -p /etc/happydeliver \
|
||||
/var/lib/happydeliver \
|
||||
/var/log/happydeliver \
|
||||
/var/cache/authentication_milter \
|
||||
/var/lib/authentication_milter \
|
||||
/var/spool/postfix/authentication_milter \
|
||||
/var/spool/postfix/spamassassin \
|
||||
/var/spool/postfix/rspamd \
|
||||
&& chown -R happydeliver:happydeliver /var/lib/happydeliver /var/log/happydeliver \
|
||||
&& chown -R mail:mail /var/spool/postfix/authentication_milter /var/spool/postfix/spamassassin \
|
||||
&& chown rspamd:mail /var/spool/postfix/rspamd \
|
||||
&& chmod 750 /var/spool/postfix/rspamd
|
||||
|
||||
# Copy the built application
|
||||
COPY --from=builder /build/happyDeliver /usr/local/bin/happyDeliver
|
||||
RUN chmod +x /usr/local/bin/happyDeliver
|
||||
|
||||
# Copy configuration files
|
||||
COPY docker/postfix/ /etc/postfix/
|
||||
COPY docker/authentication_milter/authentication_milter.json /etc/authentication_milter.json
|
||||
COPY docker/spamassassin/ /etc/mail/spamassassin/
|
||||
COPY docker/rspamd/local.d/ /etc/rspamd/local.d/
|
||||
COPY docker/supervisor/ /etc/supervisor/
|
||||
COPY docker/entrypoint.sh /entrypoint.sh
|
||||
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Expose ports
|
||||
# 25 - SMTP
|
||||
# 8080 - API server
|
||||
EXPOSE 25 8080
|
||||
|
||||
# Default configuration
|
||||
ENV HAPPYDELIVER_DATABASE_TYPE=sqlite \
|
||||
HAPPYDELIVER_DATABASE_DSN=/var/lib/happydeliver/happydeliver.db \
|
||||
HAPPYDELIVER_DOMAIN=happydeliver.local \
|
||||
HAPPYDELIVER_ADDRESS_PREFIX=test- \
|
||||
HAPPYDELIVER_DNS_TIMEOUT=5s \
|
||||
HAPPYDELIVER_HTTP_TIMEOUT=10s \
|
||||
HAPPYDELIVER_RSPAMD_API_URL=http://127.0.0.1:11334
|
||||
|
||||
# Volume for persistent data
|
||||
VOLUME ["/var/lib/happydeliver", "/var/log/happydeliver"]
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
|
||||
CMD wget --quiet --tries=1 --spider http://localhost:8080/api/status || exit 1
|
||||
|
||||
# Set entrypoint
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
CMD ["supervisord", "-c", "/etc/supervisor/supervisord.conf"]
|
||||
329
README.md
Normal file
329
README.md
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
# happyDeliver - Email Deliverability Tester
|
||||
|
||||

|
||||
|
||||
An open-source email deliverability testing platform that analyzes test emails and provides detailed deliverability reports with scoring.
|
||||
|
||||
## Features
|
||||
|
||||
- **Complete Email Analysis**: Analyzes SPF, DKIM, DMARC, BIMI, ARC, SpamAssassin and rspamd scores, DNS records, blacklist status, content quality, and more
|
||||
- **REST API**: Full-featured API for creating tests and retrieving reports
|
||||
- **LMTP Server**: Built-in LMTP server for seamless MTA integration
|
||||
- **Scoring System**: Gives A to F grades and scoring with weighted factors across dns, authentication, spam, blacklists, content, and headers
|
||||
- **Database Storage**: SQLite or PostgreSQL support
|
||||
- **Configurable**: via environment or config file for all settings
|
||||
|
||||

|
||||
|
||||
## Quick Start
|
||||
|
||||
### With Docker (Recommended)
|
||||
|
||||
The easiest way to run happyDeliver is using the all-in-one Docker container that includes Postfix, authentication_milter, SpamAssassin, and the happyDeliver application.
|
||||
|
||||
#### What's included in the Docker container:
|
||||
|
||||
- **Postfix MTA**: Receives emails on port 25
|
||||
- **authentication_milter**: Entreprise grade email authentication
|
||||
- **SpamAssassin**: Spam scoring and analysis
|
||||
- **rspamd**: Second spam filter for cross-validated scoring
|
||||
- **happyDeliver API**: REST API server on port 8080
|
||||
- **SQLite Database**: Persistent storage for tests and reports
|
||||
|
||||
#### 1. Using docker-compose
|
||||
|
||||
```bash
|
||||
# Clone the repository
|
||||
git clone https://git.nemunai.re/happyDomain/happyDeliver.git
|
||||
cd happydeliver
|
||||
|
||||
# Edit docker-compose.yml to set your domain
|
||||
# Change HAPPYDELIVER_DOMAIN environment variable and hostname
|
||||
|
||||
# Build and start
|
||||
docker-compose up -d
|
||||
|
||||
# View logs
|
||||
docker-compose logs -f
|
||||
|
||||
# Stop
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
The API will be available at `http://localhost:8080` and SMTP at `localhost:25`.
|
||||
|
||||
#### 2. Using docker build directly
|
||||
|
||||
```bash
|
||||
# Build the image
|
||||
docker build -t happydeliver:latest .
|
||||
|
||||
# Run the container
|
||||
docker run -d \
|
||||
--name happydeliver \
|
||||
-p 25:25 \
|
||||
-p 8080:8080 \
|
||||
-e HAPPYDELIVER_DOMAIN=yourdomain.com \
|
||||
--hostname mail.yourdomain.com \
|
||||
-v $(pwd)/data:/var/lib/happydeliver \
|
||||
-v $(pwd)/logs:/var/log/happydeliver \
|
||||
happydeliver:latest
|
||||
```
|
||||
|
||||
#### 3. Configure TLS Certificates (Optional but Recommended)
|
||||
|
||||
To enable TLS encryption for incoming SMTP connections, you can configure Postfix to use your SSL/TLS certificates. This is highly recommended for production deployments.
|
||||
|
||||
##### Using docker-compose
|
||||
|
||||
Add the certificate paths to your `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
- POSTFIX_CERT_FILE=/etc/ssl/certs/mail.yourdomain.com.crt
|
||||
- POSTFIX_KEY_FILE=/etc/ssl/private/mail.yourdomain.com.key
|
||||
volumes:
|
||||
- /path/to/your/certificate.crt:/etc/ssl/certs/mail.yourdomain.com.crt:ro
|
||||
- /path/to/your/private.key:/etc/ssl/private/mail.yourdomain.com.key:ro
|
||||
```
|
||||
|
||||
##### Using docker run
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
--name happydeliver \
|
||||
-p 25:25 \
|
||||
-p 8080:8080 \
|
||||
-e HAPPYDELIVER_DOMAIN=yourdomain.com \
|
||||
-e POSTFIX_CERT_FILE=/etc/ssl/certs/mail.yourdomain.com.crt \
|
||||
-e POSTFIX_KEY_FILE=/etc/ssl/private/mail.yourdomain.com.key \
|
||||
--hostname mail.yourdomain.com \
|
||||
-v /path/to/your/certificate.crt:/etc/ssl/certs/mail.yourdomain.com.crt:ro \
|
||||
-v /path/to/your/private.key:/etc/ssl/private/mail.yourdomain.com.key:ro \
|
||||
-v $(pwd)/data:/var/lib/happydeliver \
|
||||
-v $(pwd)/logs:/var/log/happydeliver \
|
||||
happydeliver:latest
|
||||
```
|
||||
|
||||
**Notes:**
|
||||
- The certificate file should contain the full certificate chain (certificate + intermediate CAs)
|
||||
- The private key file must be readable by the postfix user inside the container
|
||||
- TLS is configured with `smtpd_tls_security_level = may`, which means it's opportunistic (STARTTLS supported but not required)
|
||||
- If both environment variables are not set, Postfix will run without TLS support
|
||||
|
||||
#### 4. Configure Network and DNS
|
||||
|
||||
##### Open SMTP Port
|
||||
|
||||
Port 25 (SMTP) must be accessible from the internet to receive test emails:
|
||||
|
||||
```bash
|
||||
# Check if port 25 is listening
|
||||
netstat -ln | grep :25
|
||||
|
||||
# Allow port 25 through firewall (example with ufw)
|
||||
sudo ufw allow 25/tcp
|
||||
|
||||
# For iptables
|
||||
sudo iptables -A INPUT -p tcp --dport 25 -j ACCEPT
|
||||
```
|
||||
|
||||
**Note:** Many ISPs and cloud providers block port 25 by default to prevent spam. You may need to request port 25 to be unblocked through your provider's support.
|
||||
|
||||
##### Configure DNS Records
|
||||
|
||||
Point your domain to the server's IP address.
|
||||
|
||||
```
|
||||
yourdomain.com. IN A 203.0.113.10
|
||||
yourdomain.com. IN AAAA 2001:db8::10
|
||||
```
|
||||
|
||||
Replace `yourdomain.com` with the value you set for `HAPPYDELIVER_DOMAIN` and IPs accordingly.
|
||||
|
||||
There is no need for an MX record here since the same host will serve both HTTP and SMTP.
|
||||
|
||||
|
||||
### Manual Build
|
||||
|
||||
#### 1. Build
|
||||
|
||||
```bash
|
||||
go generate
|
||||
go build -o happyDeliver ./cmd/happyDeliver
|
||||
```
|
||||
|
||||
### 2. Run the API Server
|
||||
|
||||
```bash
|
||||
./happyDeliver server
|
||||
```
|
||||
|
||||
The server will start on `http://localhost:8080` by default.
|
||||
|
||||
#### 3. Integrate with your existing e-mail setup
|
||||
|
||||
It is expected your setup annotate the email with eg. opendkim, spamassassin, rspamd, ...
|
||||
happyDeliver will not perform thoses checks, it relies instead on standard software to have real world annotations.
|
||||
|
||||
#### Receiver Hostname
|
||||
|
||||
happyDeliver filters `Authentication-Results` headers by hostname to only trust headers added by your MTA (and not headers that may have been injected by the sender). By default, it uses the system hostname (`os.Hostname()`).
|
||||
|
||||
If your MTA's `authserv-id` (the hostname at the beginning of `Authentication-Results` headers) differs from the machine running happyDeliver, you must set it explicitly:
|
||||
|
||||
```bash
|
||||
./happyDeliver server -receiver-hostname mail.example.com
|
||||
```
|
||||
|
||||
Or via environment variable:
|
||||
```bash
|
||||
HAPPYDELIVER_RECEIVER_HOSTNAME=mail.example.com ./happyDeliver server
|
||||
```
|
||||
|
||||
**How to find the correct value:** look at the `Authentication-Results` headers in a received email. They start with the authserv-id, e.g. `Authentication-Results: mail.example.com; spf=pass ...` — in this case, use `mail.example.com`.
|
||||
|
||||
If the value is misconfigured, happyDeliver will log a warning when the last `Received` hop doesn't match the expected hostname.
|
||||
|
||||
#### Postfix LMTP Transport
|
||||
|
||||
You'll obtain the best results with a custom [transport rule](https://www.postfix.org/transport.5.html) using LMTP.
|
||||
|
||||
1. Start the happyDeliver server with LMTP enabled (default listens on `127.0.0.1:2525`):
|
||||
|
||||
```bash
|
||||
./happyDeliver server
|
||||
```
|
||||
|
||||
You can customize the LMTP address with the `-lmtp-addr` flag or in the config file.
|
||||
|
||||
2. Create the file `/etc/postfix/transport_happydeliver` with the following content:
|
||||
|
||||
```
|
||||
# Transport map - route test emails to happyDeliver LMTP server
|
||||
# Pattern: test-<base32-uuid>@yourdomain.com -> LMTP on localhost:2525
|
||||
|
||||
/^test-[a-zA-Z2-7-]{26,30}@yourdomain\.com$/ lmtp:inet:127.0.0.1:2525
|
||||
```
|
||||
|
||||
3. Append the created file to `transport_maps` in your `main.cf`:
|
||||
|
||||
```diff
|
||||
-transport_maps = texthash:/etc/postfix/transport
|
||||
+transport_maps = texthash:/etc/postfix/transport, pcre:/etc/postfix/transport_happydeliver
|
||||
```
|
||||
|
||||
If your `transport_maps` option is not set, just append this line:
|
||||
|
||||
```
|
||||
transport_maps = pcre:/etc/postfix/transport_happydeliver
|
||||
```
|
||||
|
||||
Note: to use the `pcre:` type, you need to have `postfix-pcre` installed.
|
||||
|
||||
4. Reload Postfix configuration:
|
||||
|
||||
```bash
|
||||
postfix reload
|
||||
```
|
||||
|
||||
#### 4. Create a Test
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/test
|
||||
```
|
||||
|
||||
Response:
|
||||
```json
|
||||
{
|
||||
"id": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"email": "test-kfauqaao-ukj2if3n-fgrfkiafaa@localhost",
|
||||
"status": "pending",
|
||||
"message": "Send your test email to the address above"
|
||||
}
|
||||
```
|
||||
|
||||
#### 5. Send Test Email
|
||||
|
||||
Send a test email to the address provided (you'll need to configure your MTA to route emails to the analyzer - see MTA Integration below).
|
||||
|
||||
#### 6. Get Report
|
||||
|
||||
```bash
|
||||
curl http://localhost:8080/api/report/550e8400-e29b-41d4-a716-446655440000
|
||||
```
|
||||
|
||||
## API Endpoints
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
|----------|--------|-------------|
|
||||
| `/api/test` | POST | Create a new deliverability test |
|
||||
| `/api/test/{id}` | GET | Get test metadata and status |
|
||||
| `/api/report/{id}` | GET | Get detailed analysis report |
|
||||
| `/api/report/{id}/raw` | GET | Get raw annotated email |
|
||||
| `/api/status` | GET | Service health and status |
|
||||
|
||||
## Email Analyzer (CLI Mode)
|
||||
|
||||
For manual testing or debugging, you can analyze emails from the command line:
|
||||
|
||||
```bash
|
||||
cat email.eml | ./happyDeliver analyze
|
||||
```
|
||||
|
||||
Or specify recipient explicitly:
|
||||
|
||||
```bash
|
||||
cat email.eml | ./happyDeliver analyze -recipient test-uuid@yourdomain.com
|
||||
```
|
||||
|
||||
**Note:** In production, emails are delivered via LMTP (see integration instructions above).
|
||||
|
||||
## Use with happyDomain
|
||||
|
||||
happyDeliver can be driven by [happyDomain](https://happydomain.org) through
|
||||
the [`checker-happydeliver`](https://git.nemunai.re/happyDomain/checker-happydeliver)
|
||||
plugin, so the deliverability of a domain you manage is monitored alongside
|
||||
its DNS and inbound SMTP posture.
|
||||
|
||||
How it works:
|
||||
|
||||
1. Attach the **Outbound deliverability** checker to the mail service of a zone
|
||||
in happyDomain. Point it at a happyDeliver instance via `happydeliver_url`;
|
||||
operators can configure a default instance globally.
|
||||
2. On each run, the checker calls `POST /api/test` to allocate a fresh
|
||||
recipient address, prompts the user (or an automated sender) to mail it from
|
||||
the tested domain, then polls `GET /api/test/{id}` until the report is
|
||||
ready.
|
||||
3. The structured report from `GET /api/report/{id}` is translated into
|
||||
happyDomain rule states: CRIT/WARN/INFO on SPF, DKIM, DMARC, alignment, spam
|
||||
score, blacklists and headers, plus an overall score threshold
|
||||
(`min_score`/`warn_score`).
|
||||
4. Runs repeat on a configurable interval so a regression in deliverability (a
|
||||
new RBL listing, a DKIM key rotation gone wrong, a broken SPF include, ...)
|
||||
surfaces as a domain-level alert in happyDomain.
|
||||
|
||||
See the [`checker-happydeliver` repository](https://git.nemunai.re/happyDomain/checker-happydeliver)
|
||||
for build instructions and the full list of run options.
|
||||
|
||||
## Scoring System
|
||||
|
||||
The deliverability score is calculated from A to F based on:
|
||||
|
||||
- **DNS**: Step-by-step analysis of PTR, Forward-Confirmed Reverse DNS, MX, SPF, DKIM, DMARC and BIMI records
|
||||
- **Authentication**: IPRev, SPF, DKIM, DMARC, BIMI and ARC validation
|
||||
- **Blacklist**: RBL/DNSBL checks
|
||||
- **Headers**: Required headers, MIME structure, Domain alignment
|
||||
- **Spam**: SpamAssassin and rspamd scores (combined 50/50)
|
||||
- **Content**: HTML quality, links, images, unsubscribe
|
||||
|
||||
## Funding
|
||||
|
||||
This project is funded through [NGI Zero Core](https://nlnet.nl/core), a fund established by [NLnet](https://nlnet.nl) with financial support from the European Commission's [Next Generation Internet](https://ngi.eu) program. Learn more at the [NLnet project page](https://nlnet.nl/project/happyDomain).
|
||||
|
||||
[<img src="https://nlnet.nl/logo/banner.png" alt="NLnet foundation logo" width="20%" />](https://nlnet.nl)
|
||||
[<img src="https://nlnet.nl/image/logos/NGI0_tag.svg" alt="NGI Zero Logo" width="20%" />](https://nlnet.nl/core)
|
||||
|
||||
## License
|
||||
|
||||
GNU Affero General Public License v3.0 or later (AGPL-3.0-or-later)
|
||||
|
|
@ -1,5 +1,9 @@
|
|||
package: api
|
||||
package: model
|
||||
generate:
|
||||
models: true
|
||||
embedded-spec: false
|
||||
output: internal/api/models.gen.go
|
||||
embedded-spec: true
|
||||
output: internal/model/types.gen.go
|
||||
output-options:
|
||||
skip-prune: true
|
||||
import-mapping:
|
||||
./schemas.yaml: "-"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
package: api
|
||||
generate:
|
||||
gin-server: true
|
||||
models: true
|
||||
embedded-spec: true
|
||||
output: internal/api/server.gen.go
|
||||
import-mapping:
|
||||
./schemas.yaml: git.happydns.org/happyDeliver/internal/model
|
||||
|
|
|
|||
576
api/openapi.yaml
576
api/openapi.yaml
|
|
@ -31,11 +31,11 @@ paths:
|
|||
tags:
|
||||
- tests
|
||||
summary: Create a new deliverability test
|
||||
description: Generates a unique test email address for sending test emails
|
||||
description: Generates a unique test email address for sending test emails. No database record is created until an email is received.
|
||||
operationId: createTest
|
||||
responses:
|
||||
'201':
|
||||
description: Test created successfully
|
||||
description: Test email address generated successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
|
|
@ -51,8 +51,8 @@ paths:
|
|||
get:
|
||||
tags:
|
||||
- tests
|
||||
summary: Get test metadata
|
||||
description: Retrieve test status and metadata
|
||||
summary: Get test status
|
||||
description: Check if a report exists for the given test ID (base32-encoded). Returns pending if no report exists, analyzed if a report is available.
|
||||
operationId: getTest
|
||||
parameters:
|
||||
- name: id
|
||||
|
|
@ -60,16 +60,60 @@ paths:
|
|||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
pattern: '^[a-z0-9-]+$'
|
||||
description: Base32-encoded test ID (with hyphens)
|
||||
responses:
|
||||
'200':
|
||||
description: Test metadata retrieved successfully
|
||||
description: Test status retrieved successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Test'
|
||||
'404':
|
||||
description: Test not found
|
||||
'500':
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
/tests:
|
||||
get:
|
||||
tags:
|
||||
- tests
|
||||
summary: List all tests
|
||||
description: Returns a paginated list of test summaries with scores and grades. Can be disabled via server configuration.
|
||||
operationId: listTests
|
||||
parameters:
|
||||
- name: offset
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 0
|
||||
default: 0
|
||||
description: Number of items to skip
|
||||
- name: limit
|
||||
in: query
|
||||
schema:
|
||||
type: integer
|
||||
minimum: 1
|
||||
maximum: 100
|
||||
default: 20
|
||||
description: Maximum number of items to return
|
||||
responses:
|
||||
'200':
|
||||
description: List of test summaries
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/TestListResponse'
|
||||
'403':
|
||||
description: Test listing is disabled
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'500':
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
|
|
@ -88,7 +132,8 @@ paths:
|
|||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
pattern: '^[a-z0-9-]+$'
|
||||
description: Base32-encoded test ID (with hyphens)
|
||||
responses:
|
||||
'200':
|
||||
description: Report retrieved successfully
|
||||
|
|
@ -116,7 +161,8 @@ paths:
|
|||
required: true
|
||||
schema:
|
||||
type: string
|
||||
format: uuid
|
||||
pattern: '^[a-z0-9-]+$'
|
||||
description: Base32-encoded test ID (with hyphens)
|
||||
responses:
|
||||
'200':
|
||||
description: Raw email retrieved successfully
|
||||
|
|
@ -131,6 +177,107 @@ paths:
|
|||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
/report/{id}/reanalyze:
|
||||
post:
|
||||
tags:
|
||||
- reports
|
||||
summary: Reanalyze email and regenerate report
|
||||
description: Re-run the analysis on the stored raw email to regenerate the report with the latest analyzer version. This is useful after analyzer improvements or bug fixes.
|
||||
operationId: reanalyzeReport
|
||||
parameters:
|
||||
- name: id
|
||||
in: path
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
pattern: '^[a-z0-9-]+$'
|
||||
description: Base32-encoded test ID (with hyphens)
|
||||
responses:
|
||||
'200':
|
||||
description: Report regenerated successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Report'
|
||||
'404':
|
||||
description: Email not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'500':
|
||||
description: Internal server error during reanalysis
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
/domain:
|
||||
post:
|
||||
tags:
|
||||
- tests
|
||||
summary: Test a domain's email configuration
|
||||
description: Analyzes DNS records (MX, SPF, DMARC, BIMI) for a domain without requiring an actual email to be sent. Returns results immediately.
|
||||
operationId: testDomain
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DomainTestRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Domain test completed successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/DomainTestResponse'
|
||||
'400':
|
||||
description: Invalid request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'500':
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
/blacklist:
|
||||
post:
|
||||
tags:
|
||||
- tests
|
||||
summary: Check an IP address against DNS blacklists
|
||||
description: Tests a single IP address (IPv4 or IPv6) against configured DNS-based blacklists (RBLs) without requiring an actual email to be sent. Returns results immediately.
|
||||
operationId: checkBlacklist
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BlacklistCheckRequest'
|
||||
responses:
|
||||
'200':
|
||||
description: Blacklist check completed successfully
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/BlacklistCheckResponse'
|
||||
'400':
|
||||
description: Invalid request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
'500':
|
||||
description: Internal server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/Error'
|
||||
|
||||
/status:
|
||||
get:
|
||||
tags:
|
||||
|
|
@ -149,359 +296,74 @@ paths:
|
|||
components:
|
||||
schemas:
|
||||
Test:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- email
|
||||
- status
|
||||
- created_at
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Unique test identifier
|
||||
example: "550e8400-e29b-41d4-a716-446655440000"
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
description: Unique test email address
|
||||
example: "test-550e8400@example.com"
|
||||
status:
|
||||
type: string
|
||||
enum: [pending, received, analyzed, failed]
|
||||
description: Current test status
|
||||
example: "analyzed"
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Test creation timestamp
|
||||
updated_at:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Last update timestamp
|
||||
|
||||
$ref: './schemas.yaml#/components/schemas/Test'
|
||||
TestResponse:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- email
|
||||
- status
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
example: "550e8400-e29b-41d4-a716-446655440000"
|
||||
email:
|
||||
type: string
|
||||
format: email
|
||||
example: "test-550e8400@example.com"
|
||||
status:
|
||||
type: string
|
||||
enum: [pending]
|
||||
example: "pending"
|
||||
message:
|
||||
type: string
|
||||
example: "Send your test email to the address above"
|
||||
|
||||
$ref: './schemas.yaml#/components/schemas/TestResponse'
|
||||
Report:
|
||||
type: object
|
||||
required:
|
||||
- id
|
||||
- test_id
|
||||
- score
|
||||
- checks
|
||||
- created_at
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Report identifier
|
||||
test_id:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Associated test ID
|
||||
score:
|
||||
type: number
|
||||
format: float
|
||||
minimum: 0
|
||||
maximum: 10
|
||||
description: Overall deliverability score (0-10)
|
||||
example: 8.5
|
||||
summary:
|
||||
$ref: '#/components/schemas/ScoreSummary'
|
||||
checks:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/Check'
|
||||
authentication:
|
||||
$ref: '#/components/schemas/AuthenticationResults'
|
||||
spamassassin:
|
||||
$ref: '#/components/schemas/SpamAssassinResult'
|
||||
dns_records:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/DNSRecord'
|
||||
blacklists:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/BlacklistCheck'
|
||||
raw_headers:
|
||||
type: string
|
||||
description: Raw email headers
|
||||
created_at:
|
||||
type: string
|
||||
format: date-time
|
||||
|
||||
$ref: './schemas.yaml#/components/schemas/Report'
|
||||
ScoreSummary:
|
||||
type: object
|
||||
required:
|
||||
- authentication_score
|
||||
- spam_score
|
||||
- blacklist_score
|
||||
- content_score
|
||||
- header_score
|
||||
properties:
|
||||
authentication_score:
|
||||
type: number
|
||||
format: float
|
||||
minimum: 0
|
||||
maximum: 3
|
||||
description: SPF/DKIM/DMARC score (max 3 pts)
|
||||
example: 2.8
|
||||
spam_score:
|
||||
type: number
|
||||
format: float
|
||||
minimum: 0
|
||||
maximum: 2
|
||||
description: SpamAssassin score (max 2 pts)
|
||||
example: 1.5
|
||||
blacklist_score:
|
||||
type: number
|
||||
format: float
|
||||
minimum: 0
|
||||
maximum: 2
|
||||
description: Blacklist check score (max 2 pts)
|
||||
example: 2.0
|
||||
content_score:
|
||||
type: number
|
||||
format: float
|
||||
minimum: 0
|
||||
maximum: 2
|
||||
description: Content quality score (max 2 pts)
|
||||
example: 1.8
|
||||
header_score:
|
||||
type: number
|
||||
format: float
|
||||
minimum: 0
|
||||
maximum: 1
|
||||
description: Header quality score (max 1 pt)
|
||||
example: 0.9
|
||||
|
||||
Check:
|
||||
type: object
|
||||
required:
|
||||
- category
|
||||
- name
|
||||
- status
|
||||
- score
|
||||
- message
|
||||
properties:
|
||||
category:
|
||||
type: string
|
||||
enum: [authentication, dns, content, blacklist, headers, spam]
|
||||
description: Check category
|
||||
example: "authentication"
|
||||
name:
|
||||
type: string
|
||||
description: Check name
|
||||
example: "DKIM Signature"
|
||||
status:
|
||||
type: string
|
||||
enum: [pass, fail, warn, info, error]
|
||||
description: Check result status
|
||||
example: "pass"
|
||||
score:
|
||||
type: number
|
||||
format: float
|
||||
description: Points contributed to total score
|
||||
example: 1.0
|
||||
message:
|
||||
type: string
|
||||
description: Human-readable result message
|
||||
example: "DKIM signature is valid"
|
||||
details:
|
||||
type: string
|
||||
description: Additional details (may be JSON)
|
||||
severity:
|
||||
type: string
|
||||
enum: [critical, high, medium, low, info]
|
||||
description: Issue severity
|
||||
example: "info"
|
||||
advice:
|
||||
type: string
|
||||
description: Remediation advice
|
||||
example: "Your DKIM configuration is correct"
|
||||
|
||||
$ref: './schemas.yaml#/components/schemas/ScoreSummary'
|
||||
ContentAnalysis:
|
||||
$ref: './schemas.yaml#/components/schemas/ContentAnalysis'
|
||||
ContentIssue:
|
||||
$ref: './schemas.yaml#/components/schemas/ContentIssue'
|
||||
LinkCheck:
|
||||
$ref: './schemas.yaml#/components/schemas/LinkCheck'
|
||||
ImageCheck:
|
||||
$ref: './schemas.yaml#/components/schemas/ImageCheck'
|
||||
HeaderAnalysis:
|
||||
$ref: './schemas.yaml#/components/schemas/HeaderAnalysis'
|
||||
HeaderCheck:
|
||||
$ref: './schemas.yaml#/components/schemas/HeaderCheck'
|
||||
ReceivedHop:
|
||||
$ref: './schemas.yaml#/components/schemas/ReceivedHop'
|
||||
DKIMDomainInfo:
|
||||
$ref: './schemas.yaml#/components/schemas/DKIMDomainInfo'
|
||||
DomainAlignment:
|
||||
$ref: './schemas.yaml#/components/schemas/DomainAlignment'
|
||||
HeaderIssue:
|
||||
$ref: './schemas.yaml#/components/schemas/HeaderIssue'
|
||||
AuthenticationResults:
|
||||
type: object
|
||||
properties:
|
||||
spf:
|
||||
$ref: '#/components/schemas/AuthResult'
|
||||
dkim:
|
||||
type: array
|
||||
items:
|
||||
$ref: '#/components/schemas/AuthResult'
|
||||
dmarc:
|
||||
$ref: '#/components/schemas/AuthResult'
|
||||
|
||||
$ref: './schemas.yaml#/components/schemas/AuthenticationResults'
|
||||
AuthResult:
|
||||
type: object
|
||||
required:
|
||||
- result
|
||||
properties:
|
||||
result:
|
||||
type: string
|
||||
enum: [pass, fail, none, neutral, softfail, temperror, permerror]
|
||||
description: Authentication result
|
||||
example: "pass"
|
||||
domain:
|
||||
type: string
|
||||
description: Domain being authenticated
|
||||
example: "example.com"
|
||||
selector:
|
||||
type: string
|
||||
description: DKIM selector (for DKIM only)
|
||||
example: "default"
|
||||
details:
|
||||
type: string
|
||||
description: Additional details about the result
|
||||
|
||||
$ref: './schemas.yaml#/components/schemas/AuthResult'
|
||||
ARCResult:
|
||||
$ref: './schemas.yaml#/components/schemas/ARCResult'
|
||||
IPRevResult:
|
||||
$ref: './schemas.yaml#/components/schemas/IPRevResult'
|
||||
SpamAssassinResult:
|
||||
type: object
|
||||
required:
|
||||
- score
|
||||
- required_score
|
||||
- is_spam
|
||||
properties:
|
||||
score:
|
||||
type: number
|
||||
format: float
|
||||
description: SpamAssassin spam score
|
||||
example: 2.3
|
||||
required_score:
|
||||
type: number
|
||||
format: float
|
||||
description: Threshold for spam classification
|
||||
example: 5.0
|
||||
is_spam:
|
||||
type: boolean
|
||||
description: Whether message is classified as spam
|
||||
example: false
|
||||
tests:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
description: List of triggered SpamAssassin tests
|
||||
example: ["BAYES_00", "DKIM_SIGNED"]
|
||||
report:
|
||||
type: string
|
||||
description: Full SpamAssassin report
|
||||
|
||||
DNSRecord:
|
||||
type: object
|
||||
required:
|
||||
- domain
|
||||
- record_type
|
||||
- status
|
||||
properties:
|
||||
domain:
|
||||
type: string
|
||||
description: Domain name
|
||||
example: "example.com"
|
||||
record_type:
|
||||
type: string
|
||||
enum: [MX, SPF, DKIM, DMARC]
|
||||
description: DNS record type
|
||||
example: "SPF"
|
||||
status:
|
||||
type: string
|
||||
enum: [found, missing, invalid]
|
||||
description: Record status
|
||||
example: "found"
|
||||
value:
|
||||
type: string
|
||||
description: Record value
|
||||
example: "v=spf1 include:_spf.example.com ~all"
|
||||
|
||||
$ref: './schemas.yaml#/components/schemas/SpamAssassinResult'
|
||||
SpamTestDetail:
|
||||
$ref: './schemas.yaml#/components/schemas/SpamTestDetail'
|
||||
RspamdResult:
|
||||
$ref: './schemas.yaml#/components/schemas/RspamdResult'
|
||||
DNSResults:
|
||||
$ref: './schemas.yaml#/components/schemas/DNSResults'
|
||||
MXRecord:
|
||||
$ref: './schemas.yaml#/components/schemas/MXRecord'
|
||||
SPFRecord:
|
||||
$ref: './schemas.yaml#/components/schemas/SPFRecord'
|
||||
DKIMRecord:
|
||||
$ref: './schemas.yaml#/components/schemas/DKIMRecord'
|
||||
DMARCRecord:
|
||||
$ref: './schemas.yaml#/components/schemas/DMARCRecord'
|
||||
BIMIRecord:
|
||||
$ref: './schemas.yaml#/components/schemas/BIMIRecord'
|
||||
BlacklistCheck:
|
||||
type: object
|
||||
required:
|
||||
- ip
|
||||
- rbl
|
||||
- listed
|
||||
properties:
|
||||
ip:
|
||||
type: string
|
||||
description: IP address checked
|
||||
example: "192.0.2.1"
|
||||
rbl:
|
||||
type: string
|
||||
description: RBL/DNSBL name
|
||||
example: "zen.spamhaus.org"
|
||||
listed:
|
||||
type: boolean
|
||||
description: Whether IP is listed
|
||||
example: false
|
||||
response:
|
||||
type: string
|
||||
description: RBL response code or message
|
||||
example: "127.0.0.2"
|
||||
|
||||
$ref: './schemas.yaml#/components/schemas/BlacklistCheck'
|
||||
Status:
|
||||
type: object
|
||||
required:
|
||||
- status
|
||||
- version
|
||||
properties:
|
||||
status:
|
||||
type: string
|
||||
enum: [healthy, degraded, unhealthy]
|
||||
description: Overall service status
|
||||
example: "healthy"
|
||||
version:
|
||||
type: string
|
||||
description: Service version
|
||||
example: "0.1.0-dev"
|
||||
components:
|
||||
type: object
|
||||
properties:
|
||||
database:
|
||||
type: string
|
||||
enum: [up, down]
|
||||
example: "up"
|
||||
mta:
|
||||
type: string
|
||||
enum: [up, down]
|
||||
example: "up"
|
||||
uptime:
|
||||
type: integer
|
||||
description: Service uptime in seconds
|
||||
example: 3600
|
||||
|
||||
$ref: './schemas.yaml#/components/schemas/Status'
|
||||
Error:
|
||||
type: object
|
||||
required:
|
||||
- error
|
||||
- message
|
||||
properties:
|
||||
error:
|
||||
type: string
|
||||
description: Error code
|
||||
example: "not_found"
|
||||
message:
|
||||
type: string
|
||||
description: Human-readable error message
|
||||
example: "Test not found"
|
||||
details:
|
||||
type: string
|
||||
description: Additional error details
|
||||
$ref: './schemas.yaml#/components/schemas/Error'
|
||||
DomainTestRequest:
|
||||
$ref: './schemas.yaml#/components/schemas/DomainTestRequest'
|
||||
DomainTestResponse:
|
||||
$ref: './schemas.yaml#/components/schemas/DomainTestResponse'
|
||||
BlacklistCheckRequest:
|
||||
$ref: './schemas.yaml#/components/schemas/BlacklistCheckRequest'
|
||||
BlacklistCheckResponse:
|
||||
$ref: './schemas.yaml#/components/schemas/BlacklistCheckResponse'
|
||||
TestSummary:
|
||||
$ref: './schemas.yaml#/components/schemas/TestSummary'
|
||||
TestListResponse:
|
||||
$ref: './schemas.yaml#/components/schemas/TestListResponse'
|
||||
|
|
|
|||
1221
api/schemas.yaml
Normal file
1221
api/schemas.yaml
Normal file
File diff suppressed because it is too large
Load diff
BIN
banner.webp
Normal file
BIN
banner.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 35 KiB |
|
|
@ -22,31 +22,50 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"git.happydns.org/happyDeliver/internal/app"
|
||||
"git.happydns.org/happyDeliver/internal/config"
|
||||
"git.happydns.org/happyDeliver/internal/version"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Println("Mail Tester - Email Deliverability Testing Platform")
|
||||
fmt.Println("Version: 0.1.0-dev")
|
||||
fmt.Fprintln(os.Stderr, "happyDeliver - Email Deliverability Testing Platform")
|
||||
fmt.Fprintf(os.Stderr, "Version: %s\n", version.Version)
|
||||
|
||||
if len(os.Args) < 2 {
|
||||
printUsage()
|
||||
os.Exit(1)
|
||||
cfg, err := config.ConsolidateConfig()
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
command := os.Args[1]
|
||||
command := flag.Arg(0)
|
||||
|
||||
switch command {
|
||||
case "server":
|
||||
log.Println("Starting API server...")
|
||||
// TODO: Start API server
|
||||
if err := app.RunServer(cfg); err != nil {
|
||||
log.Fatalf("Server error: %v", err)
|
||||
}
|
||||
case "analyze":
|
||||
log.Println("Starting email analyzer...")
|
||||
// TODO: Start email analyzer (LMTP/pipe mode)
|
||||
if err := app.RunAnalyzer(cfg, flag.Args()[1:], os.Stdin, os.Stdout); err != nil {
|
||||
log.Fatalf("Analyzer error: %v", err)
|
||||
}
|
||||
case "backup":
|
||||
if err := app.RunBackup(cfg); err != nil {
|
||||
log.Fatalf("Backup error: %v", err)
|
||||
}
|
||||
case "restore":
|
||||
inputFile := ""
|
||||
if len(flag.Args()) >= 2 {
|
||||
inputFile = flag.Args()[1]
|
||||
}
|
||||
if err := app.RunRestore(cfg, inputFile); err != nil {
|
||||
log.Fatalf("Restore error: %v", err)
|
||||
}
|
||||
case "version":
|
||||
fmt.Println("0.1.0-dev")
|
||||
fmt.Println(version.Version)
|
||||
default:
|
||||
fmt.Printf("Unknown command: %s\n", command)
|
||||
printUsage()
|
||||
|
|
@ -55,8 +74,12 @@ func main() {
|
|||
}
|
||||
|
||||
func printUsage() {
|
||||
fmt.Println("\nUsage:")
|
||||
fmt.Println(" mailtester server - Start the API server")
|
||||
fmt.Println(" mailtester analyze - Start the email analyzer (MDA mode)")
|
||||
fmt.Println(" mailtester version - Print version information")
|
||||
fmt.Println("\nCommand availables:")
|
||||
fmt.Println(" happyDeliver server - Start the API server")
|
||||
fmt.Println(" happyDeliver analyze [-json] - Analyze email from stdin and output results to terminal")
|
||||
fmt.Println(" happyDeliver backup - Backup database to stdout as JSON")
|
||||
fmt.Println(" happyDeliver restore [file] - Restore database from JSON file or stdin")
|
||||
fmt.Println(" happyDeliver version - Print version information")
|
||||
fmt.Println("")
|
||||
flag.Usage()
|
||||
}
|
||||
|
|
|
|||
78
docker-compose.yml
Normal file
78
docker-compose.yml
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
services:
|
||||
unbound:
|
||||
image: alpinelinux/unbound
|
||||
restart: unless-stopped
|
||||
|
||||
configs:
|
||||
- source: unbound_conf
|
||||
target: /etc/unbound/unbound.conf
|
||||
uid: "100"
|
||||
gid: "101"
|
||||
|
||||
networks:
|
||||
default:
|
||||
ipv4_address: 172.28.0.53
|
||||
|
||||
happydeliver:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: Dockerfile
|
||||
image: happydomain/happydeliver:latest
|
||||
container_name: happydeliver
|
||||
# Set a hostname
|
||||
hostname: mail.happydeliver.local
|
||||
|
||||
environment:
|
||||
# Set your domain
|
||||
HAPPYDELIVER_DOMAIN: happydeliver.local
|
||||
|
||||
ports:
|
||||
# SMTP port
|
||||
- "25:25"
|
||||
# API port
|
||||
- "8080:8080"
|
||||
|
||||
volumes:
|
||||
# Persistent database storage
|
||||
- ./data:/var/lib/happydeliver
|
||||
# Log files
|
||||
- ./logs:/var/log/happydeliver
|
||||
|
||||
dns:
|
||||
- 172.28.0.53
|
||||
restart: unless-stopped
|
||||
|
||||
configs:
|
||||
unbound_conf:
|
||||
content: |
|
||||
server:
|
||||
verbosity: 1
|
||||
interface: 0.0.0.0
|
||||
port: 53
|
||||
do-ip4: yes
|
||||
do-ip6: no
|
||||
do-udp: yes
|
||||
do-tcp: yes
|
||||
|
||||
access-control: 127.0.0.0/8 allow
|
||||
access-control: 172.28.0.0/24 allow
|
||||
|
||||
# Short cache for a testing resolver
|
||||
cache-max-ttl: 60
|
||||
|
||||
# Buffers: let the system decide
|
||||
so-sndbuf: 0
|
||||
so-rcvbuf: 0
|
||||
|
||||
# Trust anchor (static, ships with the image)
|
||||
trust-anchor-file: "/etc/unbound/root.key"
|
||||
|
||||
volumes:
|
||||
data:
|
||||
logs:
|
||||
|
||||
networks:
|
||||
default:
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.28.0.0/24
|
||||
189
docker/README.md
Normal file
189
docker/README.md
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
# happyDeliver Docker Configuration
|
||||
|
||||
This directory contains all configuration files for the all-in-one Docker container.
|
||||
|
||||
## Architecture
|
||||
|
||||
The Docker container integrates multiple components:
|
||||
|
||||
- **Postfix**: Mail Transfer Agent (MTA) that receives emails on port 25
|
||||
- **OpenDKIM**: DKIM signature verification
|
||||
- **OpenDMARC**: DMARC policy validation
|
||||
- **SpamAssassin**: Spam scoring and content analysis
|
||||
- **happyDeliver**: Go application (API server + email analyzer)
|
||||
- **Supervisor**: Process manager that runs all services
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
docker/
|
||||
├── postfix/
|
||||
│ ├── main.cf # Postfix main configuration
|
||||
│ ├── master.cf # Postfix service definitions
|
||||
│ └── transport_maps # Email routing rules
|
||||
├── opendkim/
|
||||
│ └── opendkim.conf # DKIM verification config
|
||||
├── opendmarc/
|
||||
│ └── opendmarc.conf # DMARC validation config
|
||||
├── spamassassin/
|
||||
│ └── local.cf # SpamAssassin rules and scoring
|
||||
├── supervisor/
|
||||
│ └── supervisord.conf # Supervisor service definitions
|
||||
├── entrypoint.sh # Container initialization script
|
||||
└── config.docker.yaml # happyDeliver default config
|
||||
```
|
||||
|
||||
## Configuration Details
|
||||
|
||||
### Postfix (postfix/)
|
||||
|
||||
**main.cf**: Core Postfix settings
|
||||
- Configures hostname, domain, and network interfaces
|
||||
- Sets up milter integration for OpenDKIM and OpenDMARC
|
||||
- Configures SPF policy checking
|
||||
- Routes emails through SpamAssassin content filter
|
||||
- Uses transport_maps to route test emails to happyDeliver
|
||||
|
||||
**master.cf**: Service definitions
|
||||
- Defines SMTP service with content filtering
|
||||
- Sets up SPF policy service (postfix-policyd-spf-perl)
|
||||
- Configures SpamAssassin content filter
|
||||
- Defines happydeliver pipe for email analysis
|
||||
|
||||
**transport_maps**: PCRE-based routing
|
||||
- Matches test-UUID@domain emails
|
||||
- Routes them to the happydeliver pipe
|
||||
|
||||
### OpenDKIM (opendkim/)
|
||||
|
||||
**opendkim.conf**: DKIM verification settings
|
||||
- Operates in verification-only mode
|
||||
- Adds Authentication-Results headers
|
||||
- Socket communication with Postfix via milter
|
||||
- 5-second DNS timeout
|
||||
|
||||
### OpenDMARC (opendmarc/)
|
||||
|
||||
**opendmarc.conf**: DMARC validation settings
|
||||
- Validates DMARC policies
|
||||
- Adds results to Authentication-Results headers
|
||||
- Does not reject emails (analysis mode only)
|
||||
- Socket communication with Postfix via milter
|
||||
|
||||
### SpamAssassin (spamassassin/)
|
||||
|
||||
**local.cf**: Spam detection rules
|
||||
- Enables network tests (RBL checks)
|
||||
- SPF and DKIM checking
|
||||
- Required score: 5.0 (standard threshold)
|
||||
- Adds detailed spam report headers
|
||||
- 5-second RBL timeout
|
||||
|
||||
### Supervisor (supervisor/)
|
||||
|
||||
**supervisord.conf**: Service orchestration
|
||||
- Runs all services as daemons
|
||||
- Start order: OpenDKIM → OpenDMARC → SpamAssassin → Postfix → API
|
||||
- Automatic restart on failure
|
||||
- Centralized logging
|
||||
|
||||
### Entrypoint Script (entrypoint.sh)
|
||||
|
||||
Initialization script that:
|
||||
1. Creates required directories and sets permissions
|
||||
2. Replaces configuration placeholders with environment variables
|
||||
3. Initializes Postfix (aliases, transport maps)
|
||||
4. Updates SpamAssassin rules
|
||||
5. Starts Supervisor to launch all services
|
||||
|
||||
### happyDeliver Config (config.docker.yaml)
|
||||
|
||||
Default configuration for the Docker environment:
|
||||
- API server on 0.0.0.0:8080
|
||||
- SQLite database at /var/lib/happydeliver/happydeliver.db
|
||||
- Configurable domain for test emails
|
||||
- RBL servers for blacklist checking
|
||||
- Timeouts for DNS and HTTP checks
|
||||
|
||||
## Environment Variables
|
||||
|
||||
The container accepts these environment variables:
|
||||
|
||||
- `HAPPYDELIVER_DOMAIN`: Email domain for test addresses (default: happydeliver.local)
|
||||
- `HAPPYDELIVER_RECEIVER_HOSTNAME`: Hostname used to filter `Authentication-Results` headers (see below)
|
||||
- `POSTFIX_CERT_FILE` / `POSTFIX_KEY_FILE`: TLS certificate and key paths for Postfix SMTP
|
||||
|
||||
### Receiver Hostname
|
||||
|
||||
happyDeliver filters `Authentication-Results` headers by hostname to only trust results from the expected MTA. By default, it uses the system hostname (i.e., the container's `--hostname`).
|
||||
|
||||
In the all-in-one Docker container, the container hostname is also used as the `authserv-id` in the embedded Postfix and authentication_milter, so everything matches automatically.
|
||||
|
||||
**When bypassing the embedded Postfix** (e.g., routing emails from your own MTA via LMTP), your MTA's `authserv-id` will likely differ from the container hostname. In that case, set `HAPPYDELIVER_RECEIVER_HOSTNAME` to your MTA's hostname:
|
||||
|
||||
```bash
|
||||
docker run -d \
|
||||
-e HAPPYDELIVER_DOMAIN=example.com \
|
||||
-e HAPPYDELIVER_RECEIVER_HOSTNAME=mail.example.com \
|
||||
...
|
||||
```
|
||||
|
||||
To find the correct value, look at the `Authentication-Results` headers in a received email — they start with the authserv-id, e.g. `Authentication-Results: mail.example.com; spf=pass ...`.
|
||||
|
||||
If the value is misconfigured, happyDeliver will log a warning when the last `Received` hop doesn't match the expected hostname.
|
||||
|
||||
Example (all-in-one, no override needed):
|
||||
```bash
|
||||
docker run -e HAPPYDELIVER_DOMAIN=example.com --hostname mail.example.com ...
|
||||
```
|
||||
|
||||
Example (external MTA integration):
|
||||
```bash
|
||||
docker run -e HAPPYDELIVER_DOMAIN=example.com -e HAPPYDELIVER_RECEIVER_HOSTNAME=mail.example.com ...
|
||||
```
|
||||
|
||||
## Volumes
|
||||
|
||||
**Required volumes:**
|
||||
- `/var/lib/happydeliver`: Database and persistent data
|
||||
- `/var/log/happydeliver`: Log files from all services
|
||||
|
||||
**Optional volumes:**
|
||||
- `/etc/happydeliver/config.yaml`: Custom configuration file
|
||||
|
||||
## Ports
|
||||
|
||||
- **25**: SMTP (Postfix)
|
||||
- **8080**: HTTP API (happyDeliver)
|
||||
|
||||
## Service Startup Order
|
||||
|
||||
Supervisor ensures services start in the correct order:
|
||||
|
||||
1. **OpenDKIM** (priority 10): DKIM verification milter
|
||||
2. **OpenDMARC** (priority 11): DMARC validation milter
|
||||
3. **SpamAssassin** (priority 12): Spam scoring daemon
|
||||
4. **Postfix** (priority 20): MTA that uses the above services
|
||||
5. **happyDeliver API** (priority 30): REST API server
|
||||
|
||||
## Email Processing Flow
|
||||
|
||||
1. Email arrives at Postfix on port 25
|
||||
2. Postfix sends to OpenDKIM milter
|
||||
- Verifies DKIM signature
|
||||
- Adds `Authentication-Results: ... dkim=pass/fail`
|
||||
3. Postfix sends to OpenDMARC milter
|
||||
- Validates DMARC policy
|
||||
- Adds `Authentication-Results: ... dmarc=pass/fail`
|
||||
4. Postfix routes through SpamAssassin content filter
|
||||
- Checks SPF record
|
||||
- Scores email for spam
|
||||
- Adds `X-Spam-Status` and `X-Spam-Report` headers
|
||||
5. Postfix checks transport_maps
|
||||
- If recipient matches test-UUID pattern, route to happydeliver pipe
|
||||
6. happyDeliver analyzer receives email
|
||||
- Extracts test ID from recipient
|
||||
- Parses all headers added by filters
|
||||
- Performs additional analysis (DNS, RBL, content)
|
||||
- Generates deliverability score
|
||||
- Stores report in database
|
||||
75
docker/authentication_milter/authentication_milter.json
Normal file
75
docker/authentication_milter/authentication_milter.json
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
{
|
||||
"logtoerr" : "1",
|
||||
"error_log" : "",
|
||||
"connection" : "unix:/var/spool/postfix/authentication_milter/authentication_milter.sock",
|
||||
"umask" : "0007",
|
||||
"runas" : "mail",
|
||||
"rungroup" : "mail",
|
||||
"authserv_id" : "__HOSTNAME__",
|
||||
|
||||
"connect_timeout" : 30,
|
||||
"command_timeout" : 30,
|
||||
"content_timeout" : 300,
|
||||
"dns_timeout" : 10,
|
||||
"dns_retry" : 2,
|
||||
|
||||
"handlers" : {
|
||||
|
||||
"Sanitize" : {
|
||||
"hosts_to_remove" : [
|
||||
"__HOSTNAME__"
|
||||
],
|
||||
"extra_auth_results_types" : [
|
||||
"X-Spam-Status",
|
||||
"X-Spam-Report",
|
||||
"X-Spam-Level",
|
||||
"X-Spam-Checker-Version"
|
||||
]
|
||||
},
|
||||
|
||||
"SPF" : {
|
||||
"hide_none" : 0
|
||||
},
|
||||
|
||||
"DKIM" : {
|
||||
"hide_none" : 0,
|
||||
},
|
||||
|
||||
"XGoogleDKIM" : {
|
||||
"hide_none" : 1,
|
||||
},
|
||||
|
||||
"ARC" : {
|
||||
"hide_none" : 0,
|
||||
},
|
||||
|
||||
"DMARC" : {
|
||||
"hide_none" : 0,
|
||||
"detect_list_id" : "1"
|
||||
},
|
||||
|
||||
"BIMI" : {},
|
||||
|
||||
"PTR" : {},
|
||||
|
||||
"SenderID" : {
|
||||
"hide_none" : 1
|
||||
},
|
||||
|
||||
"IPRev" : {},
|
||||
|
||||
"Auth" : {},
|
||||
|
||||
"AlignedFrom" : {},
|
||||
|
||||
"LocalIP" : {},
|
||||
|
||||
"TrustedIP" : {
|
||||
"trusted_ip_list" : []
|
||||
},
|
||||
|
||||
"!AddID" : {},
|
||||
|
||||
"ReturnOK" : {}
|
||||
}
|
||||
}
|
||||
58
docker/authentication_milter/mail-dmarc.ini
Normal file
58
docker/authentication_milter/mail-dmarc.ini
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
; This is YOU. DMARC reports include information about the reports. Enter it here.
|
||||
[organization]
|
||||
domain = example.com
|
||||
org_name = My Company Limited
|
||||
email = admin@example.com
|
||||
extra_contact_info = http://example.com
|
||||
|
||||
; aggregate DMARC reports need to be stored somewhere. Any database
|
||||
; with a DBI module (MySQL, SQLite, DBD, etc.) should work.
|
||||
; SQLite and MySQL are tested.
|
||||
; Default is sqlite.
|
||||
[report_store]
|
||||
backend = SQL
|
||||
;dsn = dbi:SQLite:dbname=dmarc_reports.sqlite
|
||||
dsn = dbi:mysql:database=dmarc_reporting_database;host=localhost;port=3306
|
||||
user = authmilterusername
|
||||
pass = authmiltpassword
|
||||
|
||||
; backend can be perl or libopendmarc
|
||||
[dmarc]
|
||||
backend = perl
|
||||
|
||||
[dns]
|
||||
timeout = 5
|
||||
public_suffix_list = share/public_suffix_list
|
||||
|
||||
[smtp]
|
||||
; hostname is the external FQDN of this MTA
|
||||
hostname = mx1.example.com
|
||||
cc = dmarc.copy@example.com
|
||||
|
||||
; list IP addresses to whitelist (bypass DMARC reject/quarantine)
|
||||
; see sample whitelist in share/dmarc_whitelist
|
||||
whitelist = /path/to/etc/dmarc_whitelist
|
||||
|
||||
; By default, we attempt to email directly to the report recipient.
|
||||
; Set these to relay via a SMTP smart host.
|
||||
smarthost = mx2.example.com
|
||||
smartuser = dmarccopyusername
|
||||
smartpass = dmarccopypassword
|
||||
|
||||
[imap]
|
||||
server = mail.example.com
|
||||
user =
|
||||
pass =
|
||||
; the imap folder where new dmarc messages will be found
|
||||
folder = dmarc
|
||||
; the folders to store processed reports (a=aggregate, f=forensic)
|
||||
f_done = dmarc.forensic
|
||||
a_done = dmarc.aggregate
|
||||
|
||||
[http]
|
||||
port = 8080
|
||||
|
||||
[https]
|
||||
port = 8443
|
||||
ssl_crt =
|
||||
ssl_key =
|
||||
74
docker/entrypoint.sh
Normal file
74
docker/entrypoint.sh
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "Starting happyDeliver container..."
|
||||
|
||||
# Get environment variables with defaults
|
||||
[ -n "${HOSTNAME}" ] || HOSTNAME=$(hostname)
|
||||
HAPPYDELIVER_DOMAIN="${HAPPYDELIVER_DOMAIN:-happydeliver.local}"
|
||||
|
||||
echo "Hostname: $HOSTNAME"
|
||||
echo "Domain: $HAPPYDELIVER_DOMAIN"
|
||||
|
||||
# Create socket directories
|
||||
mkdir -p /var/spool/postfix/authentication_milter
|
||||
chown mail:mail /var/spool/postfix/authentication_milter
|
||||
chmod 750 /var/spool/postfix/authentication_milter
|
||||
|
||||
mkdir -p /var/spool/postfix/rspamd
|
||||
chown rspamd:mail /var/spool/postfix/rspamd
|
||||
chmod 750 /var/spool/postfix/rspamd
|
||||
|
||||
# Create log directory
|
||||
mkdir -p /var/log/happydeliver /var/cache/authentication_milter /var/spool/authentication_milter /var/lib/authentication_milter /run/authentication_milter
|
||||
chown happydeliver:happydeliver /var/log/happydeliver
|
||||
chown mail:mail /var/cache/authentication_milter /run/authentication_milter /var/spool/authentication_milter /var/lib/authentication_milter
|
||||
|
||||
# Replace placeholders in Postfix configuration
|
||||
echo "Configuring Postfix..."
|
||||
sed -i "s/__HOSTNAME__/${HOSTNAME}/g" /etc/postfix/main.cf
|
||||
sed -i "s/__DOMAIN__/${HAPPYDELIVER_DOMAIN}/g" /etc/postfix/main.cf
|
||||
|
||||
# Add certificates to postfix
|
||||
[ -n "${POSTFIX_CERT_FILE}" ] && [ -n "${POSTFIX_KEY_FILE}" ] && {
|
||||
cat <<EOF >> /etc/postfix/main.cf
|
||||
smtpd_tls_cert_file = ${POSTFIX_CERT_FILE}
|
||||
smtpd_tls_key_file = ${POSTFIX_KEY_FILE}
|
||||
smtpd_tls_security_level = may
|
||||
EOF
|
||||
}
|
||||
|
||||
# Replace placeholders in configurations
|
||||
sed -i "s/__HOSTNAME__/${HOSTNAME}/g" /etc/authentication_milter.json
|
||||
|
||||
# Initialize Postfix aliases
|
||||
if [ -f /etc/postfix/aliases ]; then
|
||||
echo "Initializing Postfix aliases..."
|
||||
postalias /etc/postfix/aliases || true
|
||||
fi
|
||||
|
||||
# Compile transport maps
|
||||
if [ -f /etc/postfix/transport_maps ]; then
|
||||
echo "Compiling transport maps..."
|
||||
postmap /etc/postfix/transport_maps
|
||||
fi
|
||||
|
||||
# Update SpamAssassin rules
|
||||
echo "Updating SpamAssassin rules..."
|
||||
sa-update || echo "SpamAssassin rules update failed (might be first run)"
|
||||
|
||||
# Compile SpamAssassin rules
|
||||
sa-compile || echo "SpamAssassin compilation skipped"
|
||||
|
||||
# Initialize database if it doesn't exist
|
||||
if [ ! -f /var/lib/happydeliver/happydeliver.db ]; then
|
||||
echo "Database will be initialized on first API startup..."
|
||||
fi
|
||||
|
||||
# Set proper permissions
|
||||
chown -R happydeliver:happydeliver /var/lib/happydeliver
|
||||
|
||||
echo "Configuration complete, starting services..."
|
||||
|
||||
# Execute the main command (supervisord)
|
||||
exec "$@"
|
||||
10
docker/postfix/aliases
Normal file
10
docker/postfix/aliases
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
# Postfix aliases for happyDeliver
|
||||
# This file is processed by postalias to create aliases.db
|
||||
|
||||
# Standard aliases
|
||||
postmaster: root
|
||||
abuse: root
|
||||
mailer-daemon: postmaster
|
||||
|
||||
# Root mail can be redirected if needed
|
||||
# root: admin@example.com
|
||||
40
docker/postfix/main.cf
Normal file
40
docker/postfix/main.cf
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
# Postfix main configuration for happyDeliver
|
||||
# This configuration receives emails and routes them through authentication filters
|
||||
|
||||
# Basic settings
|
||||
compatibility_level = 3.6
|
||||
myhostname = __HOSTNAME__
|
||||
mydomain = __DOMAIN__
|
||||
myorigin = $mydomain
|
||||
inet_interfaces = all
|
||||
inet_protocols = ipv4
|
||||
|
||||
# Recipient settings
|
||||
mydestination = localhost.$mydomain, localhost
|
||||
mynetworks = 127.0.0.0/8 [::1]/128
|
||||
|
||||
# Relay settings - accept mail for our test domain
|
||||
relay_domains = $mydomain
|
||||
|
||||
# Queue and size limits
|
||||
message_size_limit = 10485760
|
||||
mailbox_size_limit = 0
|
||||
queue_minfree = 50000000
|
||||
|
||||
# Transport maps - route test emails to happyDeliver analyzer
|
||||
transport_maps = pcre:/etc/postfix/transport_maps
|
||||
|
||||
# Authentication milters
|
||||
# OpenDKIM for DKIM verification
|
||||
milter_default_action = accept
|
||||
milter_protocol = 6
|
||||
smtpd_milters = unix:/var/spool/postfix/authentication_milter/authentication_milter.sock unix:/var/spool/postfix/spamassassin/spamass-milter.sock unix:/var/spool/postfix/rspamd/rspamd-milter.sock
|
||||
non_smtpd_milters = $smtpd_milters
|
||||
|
||||
# SPF policy checking
|
||||
smtpd_recipient_restrictions =
|
||||
permit_mynetworks,
|
||||
reject_unauth_destination
|
||||
|
||||
# Logging
|
||||
debug_peer_level = 2
|
||||
78
docker/postfix/master.cf
Normal file
78
docker/postfix/master.cf
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# Postfix master process configuration for happyDeliver
|
||||
|
||||
# SMTP service
|
||||
smtp inet n - n - - smtpd
|
||||
|
||||
# Pickup service
|
||||
pickup unix n - n 60 1 pickup
|
||||
|
||||
# Cleanup service
|
||||
cleanup unix n - n - 0 cleanup
|
||||
|
||||
# Queue manager
|
||||
qmgr unix n - n 300 1 qmgr
|
||||
|
||||
# Rewrite service
|
||||
rewrite unix - - n - - trivial-rewrite
|
||||
|
||||
# Bounce service
|
||||
bounce unix - - n - 0 bounce
|
||||
|
||||
# Defer service
|
||||
defer unix - - n - 0 bounce
|
||||
|
||||
# Trace service
|
||||
trace unix - - n - 0 bounce
|
||||
|
||||
# Verify service
|
||||
verify unix - - n - 1 verify
|
||||
|
||||
# Flush service
|
||||
flush unix n - n 1000? 0 flush
|
||||
|
||||
# Proxymap service
|
||||
proxymap unix - - n - - proxymap
|
||||
|
||||
# Proxywrite service
|
||||
proxywrite unix - - n - 1 proxymap
|
||||
|
||||
# SMTP client
|
||||
smtp unix - - n - - smtp
|
||||
|
||||
# Relay service
|
||||
relay unix - - n - - smtp
|
||||
|
||||
# Showq service
|
||||
showq unix n - n - - showq
|
||||
|
||||
# Error service
|
||||
error unix - - n - - error
|
||||
|
||||
# Retry service
|
||||
retry unix - - n - - error
|
||||
|
||||
# Discard service
|
||||
discard unix - - n - - discard
|
||||
|
||||
# Local delivery
|
||||
local unix - n n - - local
|
||||
|
||||
# Virtual delivery
|
||||
virtual unix - n n - - virtual
|
||||
|
||||
# LMTP delivery
|
||||
lmtp unix - - n - - lmtp
|
||||
|
||||
# Anvil service
|
||||
anvil unix - - n - 1 anvil
|
||||
|
||||
# Scache service
|
||||
scache unix - - n - 1 scache
|
||||
|
||||
# Maildrop service
|
||||
maildrop unix - n n - - pipe
|
||||
flags=DRXhu user=vmail argv=/usr/bin/maildrop -d ${recipient}
|
||||
|
||||
# SpamAssassin content filter
|
||||
spamassassin unix - n n - - pipe
|
||||
user=mail argv=/usr/bin/spamc -f -e /usr/sbin/sendmail -oi -f ${sender} ${recipient}
|
||||
4
docker/postfix/transport_maps
Normal file
4
docker/postfix/transport_maps
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Transport map - route test emails to happyDeliver LMTP server
|
||||
# Pattern: test-<base32-uuid>@domain.com -> LMTP on localhost:2525
|
||||
|
||||
/^test-[a-zA-Z2-7-]{26,30}@.*$/ lmtp:inet:127.0.0.1:2525
|
||||