├── .editorconfig
├── .gitattributes
├── .github
├── CODEOWNERS
├── FUNDING.yml
├── ISSUE_TEMPLATE
│ ├── bug.yml
│ ├── config.yml
│ └── feature.yml
├── SUPPORT.md
├── dependabot.yml
├── docker-anonaddy.jpg
├── labels.yml
└── workflows
│ ├── build.yml
│ ├── labels.yml
│ └── test.yml
├── Dockerfile
├── LICENSE
├── README.md
├── docker-bake.hcl
├── examples
├── compose
│ ├── .env
│ ├── addy.env
│ └── compose.yml
├── nginx
│ ├── README.md
│ ├── addy.env
│ ├── compose.yml
│ └── nginx
│ │ └── templates
│ │ ├── default.conf.template
│ │ └── mta-sts.conf.template
├── rspamd
│ ├── .env
│ ├── addy.env
│ └── compose.yml
└── traefik
│ ├── .env
│ ├── README.md
│ ├── anonaddy.env
│ └── compose.yml
├── rootfs
├── etc
│ ├── cont-init.d
│ │ ├── 00-env
│ │ ├── 00-fix-logs.sh
│ │ ├── 01-fix-uidgid.sh
│ │ ├── 02-fix-perms.sh
│ │ ├── 10-config.sh
│ │ ├── 11-config-php.sh
│ │ ├── 12-config-nginx.sh
│ │ ├── 13-config-anonaddy.sh
│ │ ├── 14-config-rspamd.sh
│ │ ├── 15-config-postfix.sh
│ │ ├── 50-svc-main.sh
│ │ ├── 60-svc-rspamd.sh
│ │ ├── 61-svc-postfix.sh
│ │ ├── 80-svc-cron.sh
│ │ └── 99-clean.sh
│ ├── my.cnf.d
│ │ └── skip-ssl.cnf
│ └── socklog.rules
│ │ └── mail
├── tpls
│ └── etc
│ │ ├── nginx
│ │ └── nginx.conf
│ │ └── php83
│ │ ├── conf.d
│ │ ├── opcache.ini
│ │ └── sendmail.ini
│ │ └── php-fpm.d
│ │ └── www.conf
└── usr
│ └── local
│ └── bin
│ ├── anonaddy
│ └── gen-dkim
└── test
├── .env
├── addy.env
└── compose.yml
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | indent_size = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [rootfs/**]
13 | insert_final_newline = false
14 |
15 | [*.md]
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.gitattributes:
--------------------------------------------------------------------------------
1 | /rootfs/** linguist-detectable=false
2 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @crazy-max
2 | examples/nginx/ @eleith
3 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | github: crazy-max
2 | custom: https://www.paypal.me/crazyws
3 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/bug.yml:
--------------------------------------------------------------------------------
1 | # https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema
2 | name: Bug Report
3 | description: Report a bug
4 | labels:
5 | - kind/bug
6 | - status/triage
7 |
8 | body:
9 | - type: checkboxes
10 | attributes:
11 | label: Support guidelines
12 | description: Please read the support guidelines before proceeding.
13 | options:
14 | - label: I've read the [support guidelines](https://github.com/anonaddy/docker/blob/master/.github/SUPPORT.md)
15 | required: true
16 |
17 | - type: checkboxes
18 | attributes:
19 | label: I've found a bug and checked that ...
20 | description: |
21 | Make sure that your request fulfills all of the following requirements. If one requirement cannot be satisfied, explain in detail why.
22 | options:
23 | - label: ... the documentation does not mention anything about my problem
24 | - label: ... there are no open or closed issues that are related to my problem
25 |
26 | - type: textarea
27 | attributes:
28 | label: Description
29 | description: |
30 | Please provide a brief description of the bug in 1-2 sentences.
31 | validations:
32 | required: true
33 |
34 | - type: textarea
35 | attributes:
36 | label: Expected behaviour
37 | description: |
38 | Please describe precisely what you'd expect to happen.
39 | validations:
40 | required: true
41 |
42 | - type: textarea
43 | attributes:
44 | label: Actual behaviour
45 | description: |
46 | Please describe precisely what is actually happening.
47 | validations:
48 | required: true
49 |
50 | - type: textarea
51 | attributes:
52 | label: Steps to reproduce
53 | description: |
54 | Please describe the steps to reproduce the bug.
55 | placeholder: |
56 | 1. ...
57 | 2. ...
58 | 3. ...
59 | validations:
60 | required: true
61 |
62 | - type: textarea
63 | attributes:
64 | label: Docker info
65 | description: |
66 | Output of `docker info` command.
67 | render: text
68 | validations:
69 | required: true
70 |
71 | - type: textarea
72 | attributes:
73 | label: Docker Compose config
74 | description: |
75 | Output of `docker compose config` command.
76 | render: yaml
77 |
78 | - type: textarea
79 | attributes:
80 | label: Logs
81 | description: |
82 | Please provide the container logs (set `LOG_LEVEL=debug` if applicable).
83 | render: text
84 | validations:
85 | required: true
86 |
87 | - type: textarea
88 | attributes:
89 | label: Additional info
90 | description: |
91 | Please provide any additional information that seem useful.
92 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/config.yml:
--------------------------------------------------------------------------------
1 | # https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser
2 | blank_issues_enabled: true
3 | contact_links:
4 | - name: Questions and Discussions
5 | url: https://github.com/anonaddy/docker/discussions/new
6 | about: Use Github Discussions to ask questions and/or open discussion topics.
7 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature.yml:
--------------------------------------------------------------------------------
1 | # https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema
2 | name: Feature request
3 | description: Missing functionality? Come tell us about it!
4 | labels:
5 | - kind/enhancement
6 | - status/triage
7 |
8 | body:
9 | - type: textarea
10 | id: description
11 | attributes:
12 | label: Description
13 | description: What is the feature you want to see?
14 | validations:
15 | required: true
16 |
--------------------------------------------------------------------------------
/.github/SUPPORT.md:
--------------------------------------------------------------------------------
1 | # Support [](https://isitmaintained.com/project/anonaddy/docker)
2 |
3 | ## Reporting an issue
4 |
5 | Please do a search in [open issues](https://github.com/anonaddy/docker/issues?utf8=%E2%9C%93&q=) to see if the issue or feature request has already been filed.
6 |
7 | If you find your issue already exists, make relevant comments and add your [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). Use a reaction in place of a "+1" comment.
8 |
9 | :+1: - upvote
10 |
11 | :-1: - downvote
12 |
13 | If you cannot find an existing issue that describes your bug or feature, submit an issue using the guidelines below.
14 |
15 | ## Writing good bug reports and feature requests
16 |
17 | File a single issue per problem and feature request.
18 |
19 | * Do not enumerate multiple bugs or feature requests in the same issue.
20 | * Do not add your issue as a comment to an existing issue unless it's for the identical input. Many issues look similar, but have different causes.
21 |
22 | The more information you can provide, the more likely someone will be successful reproducing the issue and finding a fix.
23 |
24 | You are now ready to [create a new issue](https://github.com/anonaddy/docker/issues/new/choose)!
25 |
26 | ## Closure policy
27 |
28 | * Support directly related to addy.io will not be provided if your problem is not related to the operation of this image.
29 | * Issues that don't have the information requested above (when applicable) will be closed immediately and the poster directed to the support guidelines.
30 | * Issues that go a week without a response from original poster are subject to closure at my discretion.
31 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: "github-actions"
4 | directory: "/"
5 | schedule:
6 | interval: "daily"
7 | time: "08:00"
8 | timezone: "Europe/Paris"
9 | labels:
10 | - "kind/dependencies"
11 | - "bot"
12 |
--------------------------------------------------------------------------------
/.github/docker-anonaddy.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anonaddy/docker/df8b415dff1b493e21ddc439444efd224d60d442/.github/docker-anonaddy.jpg
--------------------------------------------------------------------------------
/.github/labels.yml:
--------------------------------------------------------------------------------
1 | ## more info https://github.com/crazy-max/ghaction-github-labeler
2 | -
3 | name: "bot"
4 | color: "69cde9"
5 | description: ""
6 | -
7 | name: "good first issue"
8 | color: "7057ff"
9 | description: ""
10 | -
11 | name: "help wanted"
12 | color: "4caf50"
13 | description: ""
14 | -
15 | name: "area/ci"
16 | color: "ed9ca9"
17 | description: ""
18 | -
19 | name: "area/dockerfile"
20 | color: "03a9f4"
21 | description: ""
22 | -
23 | name: "kind/bug"
24 | color: "b60205"
25 | description: ""
26 | -
27 | name: "kind/dependencies"
28 | color: "0366d6"
29 | description: ""
30 | -
31 | name: "kind/docs"
32 | color: "c5def5"
33 | description: ""
34 | -
35 | name: "kind/duplicate"
36 | color: "cccccc"
37 | description: ""
38 | -
39 | name: "kind/enhancement"
40 | color: "0054ca"
41 | description: ""
42 | -
43 | name: "kind/invalid"
44 | color: "e6e6e6"
45 | description: ""
46 | -
47 | name: "kind/upstream"
48 | color: "fbca04"
49 | description: ""
50 | -
51 | name: "kind/wontfix"
52 | color: "ffffff"
53 | description: ""
54 | -
55 | name: "status/automerge"
56 | color: "8f4fbc"
57 | description: ""
58 | -
59 | name: "status/needs-investigation"
60 | color: "e6625b"
61 | description: ""
62 | -
63 | name: "status/needs-more-info"
64 | color: "795548"
65 | description: ""
66 | -
67 | name: "status/stale"
68 | color: "237da0"
69 | description: ""
70 | -
71 | name: "status/triage"
72 | color: "dde4b7"
73 | description: ""
74 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: build
2 |
3 | concurrency:
4 | group: ${{ github.workflow }}-${{ github.ref }}
5 | cancel-in-progress: true
6 |
7 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
8 | permissions:
9 | contents: read
10 |
11 | on:
12 | push:
13 | branches:
14 | - 'master'
15 | tags:
16 | - '*'
17 | paths-ignore:
18 | - '**.md'
19 | pull_request:
20 | paths-ignore:
21 | - '**.md'
22 |
23 | env:
24 | DOCKERHUB_SLUG: anonaddy/anonaddy
25 |
26 | jobs:
27 | prepare:
28 | runs-on: ubuntu-latest
29 | outputs:
30 | matrix: ${{ steps.platforms.outputs.matrix }}
31 | steps:
32 | -
33 | name: Checkout
34 | uses: actions/checkout@v4
35 | -
36 | name: Create matrix
37 | id: platforms
38 | run: |
39 | echo "matrix=$(docker buildx bake image-all --print | jq -cr '.target."image-all".platforms')" >>${GITHUB_OUTPUT}
40 | -
41 | name: Show matrix
42 | run: |
43 | echo ${{ steps.platforms.outputs.matrix }}
44 | -
45 | name: Docker meta
46 | id: meta
47 | uses: docker/metadata-action@v5
48 | with:
49 | images: |
50 | ${{ env.DOCKERHUB_SLUG }}
51 | tags: |
52 | type=match,pattern=(.*)-r,group=1
53 | type=ref,event=pr
54 | type=edge
55 | labels: |
56 | org.opencontainers.image.title=addy.io
57 | org.opencontainers.image.description=Anonymous Email Forwarding
58 | org.opencontainers.image.vendor=CrazyMax
59 | -
60 | name: Rename meta bake definition file
61 | run: |
62 | mv "${{ steps.meta.outputs.bake-file }}" "/tmp/bake-meta.json"
63 | -
64 | name: Upload meta bake definition
65 | uses: actions/upload-artifact@v4
66 | with:
67 | name: bake-meta
68 | path: /tmp/bake-meta.json
69 | if-no-files-found: error
70 | retention-days: 1
71 |
72 | build:
73 | runs-on: ${{ startsWith(matrix.platform, 'linux/arm') && 'ubuntu-24.04-arm' || 'ubuntu-latest' }}
74 | needs:
75 | - prepare
76 | strategy:
77 | fail-fast: false
78 | matrix:
79 | platform: ${{ fromJson(needs.prepare.outputs.matrix) }}
80 | steps:
81 | -
82 | name: Prepare
83 | run: |
84 | platform=${{ matrix.platform }}
85 | echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
86 | -
87 | name: Download meta bake definition
88 | uses: actions/download-artifact@v4
89 | with:
90 | name: bake-meta
91 | path: /tmp
92 | -
93 | name: Set up Docker Buildx
94 | uses: docker/setup-buildx-action@v3
95 | with:
96 | buildkitd-flags: "--debug"
97 | -
98 | name: Login to DockerHub
99 | if: github.event_name != 'pull_request'
100 | uses: docker/login-action@v3
101 | with:
102 | username: ${{ secrets.DOCKER_USERNAME }}
103 | password: ${{ secrets.DOCKER_PASSWORD }}
104 | -
105 | name: Build
106 | id: bake
107 | uses: docker/bake-action@v6
108 | with:
109 | files: |
110 | ./docker-bake.hcl
111 | cwd:///tmp/bake-meta.json
112 | targets: image
113 | set: |
114 | *.tags=
115 | *.platform=${{ matrix.platform }}
116 | *.cache-from=type=gha,scope=build-${{ env.PLATFORM_PAIR }}
117 | *.cache-to=type=gha,scope=build-${{ env.PLATFORM_PAIR }}
118 | *.output=type=image,"name=${{ env.DOCKERHUB_SLUG }}",push-by-digest=true,name-canonical=true,push=${{ github.event_name != 'pull_request' }}
119 | -
120 | name: Export digest
121 | run: |
122 | mkdir -p /tmp/digests
123 | digest="${{ fromJSON(steps.bake.outputs.metadata).image['containerimage.digest'] }}"
124 | touch "/tmp/digests/${digest#sha256:}"
125 | -
126 | name: Upload digest
127 | uses: actions/upload-artifact@v4
128 | with:
129 | name: digests-${{ env.PLATFORM_PAIR }}
130 | path: /tmp/digests/*
131 | if-no-files-found: error
132 | retention-days: 1
133 |
134 | merge:
135 | runs-on: ubuntu-latest
136 | if: github.event_name != 'pull_request'
137 | needs:
138 | - build
139 | steps:
140 | -
141 | name: Download meta bake definition
142 | uses: actions/download-artifact@v4
143 | with:
144 | name: bake-meta
145 | path: /tmp
146 | -
147 | name: Download digests
148 | uses: actions/download-artifact@v4
149 | with:
150 | path: /tmp/digests
151 | pattern: digests-*
152 | merge-multiple: true
153 | -
154 | name: Set up Docker Buildx
155 | uses: docker/setup-buildx-action@v3
156 | -
157 | name: Login to DockerHub
158 | uses: docker/login-action@v3
159 | with:
160 | username: ${{ secrets.DOCKER_USERNAME }}
161 | password: ${{ secrets.DOCKER_PASSWORD }}
162 | -
163 | name: Create manifest list and push
164 | working-directory: /tmp/digests
165 | run: |
166 | docker buildx imagetools create $(jq -cr '.target."docker-metadata-action".tags | map(select(startswith("${{ env.DOCKERHUB_SLUG }}")) | "-t " + .) | join(" ")' /tmp/bake-meta.json) \
167 | $(printf '${{ env.DOCKERHUB_SLUG }}@sha256:%s ' *)
168 | -
169 | name: Inspect image
170 | run: |
171 | tag=$(jq -r '.target."docker-metadata-action".args.DOCKER_META_VERSION' /tmp/bake-meta.json)
172 | docker buildx imagetools inspect ${{ env.DOCKERHUB_SLUG }}:${tag}
173 |
--------------------------------------------------------------------------------
/.github/workflows/labels.yml:
--------------------------------------------------------------------------------
1 | name: labels
2 |
3 | concurrency:
4 | group: ${{ github.workflow }}-${{ github.ref }}
5 | cancel-in-progress: true
6 |
7 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
8 | permissions:
9 | contents: read
10 |
11 | on:
12 | push:
13 | branches:
14 | - 'master'
15 | paths:
16 | - '.github/labels.yml'
17 | - '.github/workflows/labels.yml'
18 | pull_request:
19 | paths:
20 | - '.github/labels.yml'
21 | - '.github/workflows/labels.yml'
22 |
23 | jobs:
24 | labeler:
25 | runs-on: ubuntu-latest
26 | permissions:
27 | # same as global permissions
28 | contents: read
29 | # required to update labels
30 | issues: write
31 | steps:
32 | -
33 | name: Checkout
34 | uses: actions/checkout@v4
35 | -
36 | name: Run Labeler
37 | uses: crazy-max/ghaction-github-labeler@v5
38 | with:
39 | dry-run: ${{ github.event_name == 'pull_request' }}
40 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 |
3 | concurrency:
4 | group: ${{ github.workflow }}-${{ github.ref }}
5 | cancel-in-progress: true
6 |
7 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions
8 | permissions:
9 | contents: read
10 |
11 | on:
12 | push:
13 | branches:
14 | - 'master'
15 | paths-ignore:
16 | - '**.md'
17 | pull_request:
18 | paths-ignore:
19 | - '**.md'
20 |
21 | env:
22 | BUILD_TAG: addy:test
23 | CONTAINER_NAME: addy
24 |
25 | jobs:
26 | test:
27 | runs-on: ubuntu-latest
28 | steps:
29 | -
30 | name: Checkout
31 | uses: actions/checkout@v4
32 | -
33 | name: Set up Docker Buildx
34 | uses: docker/setup-buildx-action@v3
35 | -
36 | name: Build
37 | uses: docker/bake-action@v6
38 | with:
39 | source: .
40 | targets: image-local
41 | env:
42 | DEFAULT_TAG: ${{ env.BUILD_TAG }}
43 | -
44 | name: Generate DKIM private key
45 | run: |
46 | docker compose run --rm gen-dkim
47 | working-directory: test
48 | env:
49 | ANONADDY_IMAGE: ${{ env.BUILD_TAG }}
50 | ANONADDY_CONTAINER: ${{ env.CONTAINER_NAME }}
51 | -
52 | name: Start
53 | run: |
54 | docker compose up -d
55 | working-directory: test
56 | env:
57 | ANONADDY_IMAGE: ${{ env.BUILD_TAG }}
58 | ANONADDY_CONTAINER: ${{ env.CONTAINER_NAME }}
59 | -
60 | name: Check container logs
61 | uses: crazy-max/.github/.github/actions/container-logs-check@main
62 | with:
63 | container_name: ${{ env.CONTAINER_NAME }}
64 | log_check: "ready to handle connections"
65 | timeout: 120
66 | -
67 | name: Logs
68 | if: always()
69 | run: |
70 | docker compose logs
71 | working-directory: test
72 | env:
73 | ANONADDY_IMAGE: ${{ env.BUILD_TAG }}
74 | ANONADDY_CONTAINER: ${{ env.CONTAINER_NAME }}
75 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | # syntax=docker/dockerfile:1
2 |
3 | ARG ANONADDY_VERSION=1.3.2
4 | ARG ALPINE_VERSION=3.21
5 |
6 | FROM crazymax/yasu:latest AS yasu
7 | FROM crazymax/alpine-s6:${ALPINE_VERSION}-2.2.0.3
8 |
9 | COPY --from=yasu / /
10 | RUN apk --no-cache add \
11 | bash \
12 | ca-certificates \
13 | curl \
14 | gnupg \
15 | gpgme \
16 | imagemagick \
17 | libgd \
18 | mysql-client \
19 | nginx \
20 | openssl \
21 | php83 \
22 | php83-cli \
23 | php83-ctype \
24 | php83-curl \
25 | php83-dom \
26 | php83-fileinfo \
27 | php83-fpm \
28 | php83-gd \
29 | php83-gmp \
30 | php83-iconv \
31 | php83-intl \
32 | php83-json \
33 | php83-mbstring \
34 | php83-opcache \
35 | php83-openssl \
36 | php83-pdo \
37 | php83-pdo_mysql \
38 | php83-pecl-imagick \
39 | php83-phar \
40 | php83-redis \
41 | php83-session \
42 | php83-simplexml \
43 | php83-sodium \
44 | php83-tokenizer \
45 | php83-xml \
46 | php83-xmlreader \
47 | php83-xmlwriter \
48 | php83-zip \
49 | php83-zlib \
50 | postfix \
51 | postfix-mysql \
52 | rspamd \
53 | rspamd-controller \
54 | rspamd-proxy \
55 | shadow \
56 | tar \
57 | tzdata \
58 | cyrus-sasl \
59 | cyrus-sasl-login \
60 | && cp /etc/postfix/master.cf /etc/postfix/master.cf.orig \
61 | && cp /etc/postfix/main.cf /etc/postfix/main.cf.orig \
62 | && apk --no-cache add -t build-dependencies \
63 | autoconf \
64 | automake \
65 | build-base \
66 | gpgme-dev \
67 | libtool \
68 | pcre-dev \
69 | php83-dev \
70 | php83-pear \
71 | && pecl83 install gnupg \
72 | && echo "extension=gnupg.so" > /etc/php83/conf.d/60_gnupg.ini \
73 | && pecl83 install mailparse \
74 | && echo "extension=mailparse.so" > /etc/php83/conf.d/60_mailparse.ini \
75 | && apk del build-dependencies \
76 | && rm -rf /tmp/* /var/www/*
77 |
78 | ARG ANONADDY_VERSION
79 | ENV ANONADDY_VERSION=$ANONADDY_VERSION \
80 | S6_BEHAVIOUR_IF_STAGE2_FAILS="2" \
81 | SOCKLOG_TIMESTAMP_FORMAT="" \
82 | TZ="UTC" \
83 | PUID="1000" \
84 | PGID="1000"
85 |
86 | WORKDIR /var/www/anonaddy
87 | RUN apk --no-cache add -t build-dependencies \
88 | git \
89 | nodejs \
90 | npm \
91 | && node --version \
92 | && npm --version \
93 | && addgroup -g ${PGID} anonaddy \
94 | && adduser -D -h /var/www/anonaddy -u ${PUID} -G anonaddy -s /bin/sh -D anonaddy \
95 | && addgroup anonaddy mail \
96 | && curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer \
97 | && git config --global --add safe.directory /var/www/anonaddy \
98 | && git init . && git remote add origin "https://github.com/anonaddy/anonaddy.git" \
99 | && git fetch --depth 1 origin "v${ANONADDY_VERSION}" && git checkout -q FETCH_HEAD \
100 | && composer install --optimize-autoloader --no-dev --no-interaction --no-ansi --ignore-platform-req=php-64bit \
101 | && chown -R anonaddy:anonaddy /var/www/anonaddy \
102 | && npm ci --ignore-scripts \
103 | && APP_URL=https://addy-sh.test npm run production \
104 | && npm prune --production \
105 | && chown -R nobody:nogroup /var/www/anonaddy \
106 | && apk del build-dependencies \
107 | && rm -rf /root/.composer \
108 | /root/.config \
109 | /root/.npm \
110 | /var/www/anonaddy/.git \
111 | /var/www/anonaddy/node_modules \
112 | /tmp/*
113 |
114 | COPY rootfs /
115 |
116 | EXPOSE 25 8000 11334
117 | VOLUME [ "/data" ]
118 |
119 | ENTRYPOINT [ "/init" ]
120 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019-2024 CrazyMax
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |

2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | ## About
13 |
14 | Docker image for [addy.io](https://addy.io/), an anonymous email forwarding service.
15 |
16 | > [!TIP]
17 | > Want to be notified of new releases? Check out 🔔 [Diun (Docker Image Update Notifier)](https://github.com/crazy-max/diun)
18 | > project!
19 |
20 | ___
21 |
22 | * [Features](#features)
23 | * [Build locally](#build-locally)
24 | * [Image](#Image)
25 | * [Environment variables](#environment-variables)
26 | * [General](#general)
27 | * [App](#app)
28 | * [Database](#database)
29 | * [Redis](#redis)
30 | * [Mail](#mail)
31 | * [Postfix](#postfix)
32 | * [RSPAMD](#rspamd)
33 | * [Volumes](#volumes)
34 | * [Ports](#ports)
35 | * [Usage](#usage)
36 | * [Docker Compose](#docker-compose)
37 | * [Upgrade](#upgrade)
38 | * [Notes](#notes)
39 | * [`anonaddy` command](#anonaddy-command)
40 | * [Create user](#create-user)
41 | * [Generate DKIM private/public keypair](#generate-dkim-privatepublic-keypair)
42 | * [Generate GPG key](#generate-gpg-key)
43 | * [Define additional env vars](#define-additional-env-vars)
44 | * [Override Postfix main configuration](#override-postfix-main-configuration)
45 | * [Spamhaus DQS configuration](#spamhaus-dqs-configuration)
46 | * [Contributing](#contributing)
47 | * [License](#license)
48 |
49 | ## Features
50 |
51 | * Run as non-root user
52 | * Multi-platform image
53 | * [s6-overlay](https://github.com/just-containers/s6-overlay/) as process supervisor
54 | * [Traefik](https://github.com/containous/traefik-library-image) as reverse proxy and creation/renewal of Let's Encrypt certificates (see [this template](examples/traefik))
55 |
56 | ## Build locally
57 |
58 | ```console
59 | git clone https://github.com/anonaddy/docker.git docker-addy
60 | cd docker-addy
61 |
62 | # Build image and output to docker (default)
63 | docker buildx bake
64 |
65 | # Build multi-platform image
66 | docker buildx bake image-all
67 | ```
68 |
69 | ## Image
70 |
71 | Following platforms for this image are available:
72 |
73 | ```
74 | $ docker buildx imagetools inspect anonaddy/anonaddy --format "{{json .Manifest}}" | \
75 | jq -r '.manifests[] | select(.platform.os != null and .platform.os != "unknown") | .platform | "\(.os)/\(.architecture)\(if .variant then "/" + .variant else "" end)"'
76 |
77 | linux/amd64
78 | linux/arm/v6
79 | linux/arm/v7
80 | linux/arm64
81 | ```
82 |
83 | ## Environment variables
84 |
85 | ### General
86 |
87 | * `TZ`: The timezone assigned to the container (default `UTC`)
88 | * `PUID`: user id (default `1000`)
89 | * `PGID`: group id (default `1000`)
90 | * `MEMORY_LIMIT`: PHP memory limit (default `256M`)
91 | * `UPLOAD_MAX_SIZE`: Upload max size (default `16M`)
92 | * `CLEAR_ENV`: Clear environment in FPM workers (default `yes`)
93 | * `OPCACHE_MEM_SIZE`: PHP OpCache memory consumption (default `128`)
94 | * `LISTEN_IPV6`: Enable IPv6 for Nginx and Postfix (default `true`)
95 | * `REAL_IP_FROM`: Trusted addresses that are known to send correct replacement addresses (default `0.0.0.0/32`)
96 | * `REAL_IP_HEADER`: Request header field whose value will be used to replace the client address (default `X-Forwarded-For`)
97 | * `LOG_IP_VAR`: Use another variable to retrieve the remote IP address for access [log_format](http://nginx.org/en/docs/http/ngx_http_log_module.html#log_format) on Nginx. (default `remote_addr`)
98 | * `LOG_CROND`: Enable crond logging. (default `true`)
99 |
100 | ### App
101 |
102 | * `APP_NAME`: Name of the application (default `addy.io`)
103 | * `APP_KEY`: Application key for encrypter service. You can generate one through `anonaddy key:generate --show` or `echo "base64:$(openssl rand -base64 32)"`. **required**
104 | * `APP_DEBUG`: Enables or disables debug mode, used to troubleshoot issues (default `false`)
105 | * `APP_URL`: The URL of your installation
106 | * `ANONADDY_RETURN_PATH`: Return-path header for outbound emails
107 | * `ANONADDY_ADMIN_USERNAME`: If set this value will be used and allow you to receive forwarded emails at the root domain
108 | * `ANONADDY_ENABLE_REGISTRATION`: If set to false this will prevent new users from registering on the site (default `true`)
109 | * `ANONADDY_DOMAIN`: Root domain to receive email from **required**
110 | * `ANONADDY_HOSTNAME`: FQDN hostname for your server used to validate records on custom domains that are added by users
111 | * `ANONADDY_DNS_RESOLVER`: Custom domains that are added by users to validate records (default `127.0.0.1`)
112 | * `ANONADDY_ALL_DOMAINS`: If you would like to have other domains to use (e.g. `@username.example2.com`), set a comma separated list like so, `example.com,example2.com` (default `$ANONADDY_DOMAIN`)
113 | * `ANONADDY_NON_ADMIN_SHARED_DOMAINS`: If set to false this will prevent any non-admin users from being able to create shared domain aliases at any domains that have been set for `$ANONADDY_ALL_DOMAINS` (default `true`)
114 | * `ANONADDY_SECRET`: Long random string used when hashing data for the anonymous replies **required**
115 | * `ANONADDY_LIMIT`: Number of emails a user can forward and reply per hour (default `200`)
116 | * `ANONADDY_BANDWIDTH_LIMIT`: Monthly bandwidth limit for users in bytes domains to use (default `104857600`)
117 | * `ANONADDY_NEW_ALIAS_LIMIT`: Number of new aliases a user can create each hour (default `10`)
118 | * `ANONADDY_ADDITIONAL_USERNAME_LIMIT`: Number of additional usernames a user can add to their account (default `10`)
119 | * `ANONADDY_SIGNING_KEY_FINGERPRINT`: GPG key used to sign forwarded emails. Should be the same as your mail from email address
120 | * `ANONADDY_DKIM_SIGNING_KEY`: Path to the private DKIM signing key to be used to sign emails for custom domains.
121 | * `ANONADDY_DKIM_SELECTOR`: Selector for the current DKIM signing key (default `default`)
122 |
123 | > [!NOTE]
124 | > `APP_KEY_FILE`, `ANONADDY_SECRET_FILE` and `ANONADDY_SIGNING_KEY_FINGERPRINT_FILE`
125 | > can be used to fill in the value from a file, especially for Docker's secrets
126 | > feature.
127 |
128 | ### Database
129 |
130 | * `DB_HOST`: MySQL database hostname / IP address **required**
131 | * `DB_PORT`: MySQL database port (default `3306`)
132 | * `DB_DATABASE`: MySQL database name (default `anonaddy`)
133 | * `DB_USERNAME`: MySQL user (default `anonaddy`)
134 | * `DB_PASSWORD`: MySQL password
135 | * `DB_TIMEOUT`: Time in seconds after which we stop trying to reach the MySQL server (useful for clusters, default `60`)
136 |
137 | > [!NOTE]
138 | > `DB_USERNAME_FILE` and `DB_PASSWORD_FILE` can be used to fill in the value
139 | > from a file, especially for Docker's secrets feature.
140 |
141 | ### Redis
142 |
143 | * `REDIS_HOST`: Redis hostname / IP address
144 | * `REDIS_PORT`: Redis port (default `6379`)
145 | * `REDIS_PASSWORD`: Redis password
146 |
147 | > [!NOTE]
148 | > `REDIS_PASSWORD_FILE` can be used to fill in the value from a file, especially
149 | > for Docker's secrets feature.
150 |
151 | ### Mail
152 |
153 | * `MAIL_FROM_NAME`: From name (default `addy.io`)
154 | * `MAIL_FROM_ADDRESS`: From email address (default `addy@${ANONADDY_DOMAIN}`)
155 | * `MAIL_ENCRYPTION`: Encryption protocol to send e-mail messages (default `null`)
156 |
157 | ### Postfix
158 |
159 | * `POSTFIX_DEBUG`: Enable debug (default `false`)
160 | * `POSTFIX_MESSAGE_SIZE_LIMIT`: The maximal size in bytes of a message, including envelope information (default `26214400`)
161 | * `POSTFIX_SMTPD_TLS`: Enabling TLS in the Postfix SMTP server (default `false`)
162 | * `POSTFIX_SMTPD_TLS_CERT_FILE`: File with the Postfix SMTP server RSA certificate in PEM format
163 | * `POSTFIX_SMTPD_TLS_KEY_FILE`: File with the Postfix SMTP server RSA private key in PEM format
164 | * `POSTFIX_SMTP_TLS`: Enabling TLS in the Postfix SMTP client (default `false`)
165 | * `POSTFIX_RELAYHOST`: Default host to send mail to
166 | * `POSTFIX_RELAYHOST_AUTH_ENABLE`: Enable client-side authentication for relayhost (default `false`)
167 | * `POSTFIX_RELAYHOST_USERNAME`: Postfix SMTP Client username for relayhost authentication
168 | * `POSTFIX_RELAYHOST_PASSWORD`: Postfix SMTP Client password for relayhost authentication
169 | * `POSTFIX_RELAYHOST_SSL_ENCRYPTION`: enable SSL encrpytion over SMTP where TLS is not available. (default `false`)
170 | * `POSTFIX_SPAMAUS_DQS_KEY`: Personal key for [Spamhaus DQS](#spamhaus-dqs-configuration)
171 |
172 | > [!NOTE]
173 | > `POSTFIX_RELAYHOST_USERNAME_FILE` and `POSTFIX_RELAYHOST_PASSWORD_FILE` can be
174 | > used to fill in the value from a file, especially for Docker's secrets feature.
175 |
176 | ### RSPAMD
177 |
178 | * `RSPAMD_ENABLE`: Enable Rspamd service. (default `false`)
179 | * `RSPAMD_WEB_PASSWORD`: Rspamd web password (default `null`)
180 | * `RSPAMD_NO_LOCAL_ADDRS`: Disable Rspamd local networks (default `false`)
181 | * `RSPAMD_SMTPD_MILTERS`: A list of Milter (space or comma as separated) applications for new mail that arrives (default `inet:127.0.0.1:11332`)
182 |
183 | > [!NOTE]
184 | > `RSPAMD_WEB_PASSWORD_FILE` can be used to fill in the value from a file,
185 | > especially for Docker's secrets feature.
186 |
187 | > [!WARNING]
188 | > DKIM private key must be located in `/data/dkim/${ANONADDY_DOMAIN}.private`.
189 | > You can generate a DKIM private/public keypair by following [this note](#generate-dkim-privatepublic-keypair).
190 |
191 | > [!WARNING]
192 | > Rspamd service is disabled if DKIM private key is not found
193 |
194 | > [!WARNING]
195 | > Rspamd service needs to be enabled for the reply anonymously feature to work.
196 | > See [#169](https://github.com/anonaddy/docker/issues/169#issuecomment-1232577449) for more details.
197 |
198 |
199 | ## Volumes
200 |
201 | * `/data`: Contains storage
202 |
203 | > [!WARNING]
204 | > Note that the volume should be owned by the user/group with the specified
205 | > `PUID` and `PGID`. If you don't give the volume correct permissions, the
206 | > container may not start.
207 |
208 | ## Ports
209 |
210 | * `8000`: HTTP port (addy.io)
211 | * `11334`: HTTP port (rspamd web dashboard)
212 | * `25`: SMTP port (postfix)
213 |
214 | ## Usage
215 |
216 | ### Docker Compose
217 |
218 | Docker compose is the recommended way to run this image. You can use the following
219 | [docker compose template](examples/compose/compose.yml), then run the container:
220 |
221 | ```console
222 | docker compose up -d
223 | docker compose logs -f
224 | ```
225 |
226 | ## Upgrade
227 |
228 | ```console
229 | docker compose pull
230 | docker compose up -d
231 | ```
232 |
233 | ## Notes
234 |
235 | ### `anonaddy` command
236 |
237 | If you want to use the artisan command to perform common server operations like
238 | manage users, passwords and more, type:
239 |
240 | ```console
241 | docker compose exec addy anonaddy
242 | ```
243 |
244 | For example to list all available commands:
245 |
246 | ```console
247 | docker compose exec addy anonaddy list
248 | ```
249 |
250 | ### Create user
251 |
252 | ```console
253 | docker compose exec addy anonaddy anonaddy:create-user "username" "webmaster@example.com"
254 | ```
255 |
256 | ### Generate DKIM private/public keypair
257 |
258 | ```console
259 | docker compose run --entrypoint '' addy gen-dkim
260 | ```
261 |
262 | ```text
263 | generating private and storing in data/dkim/example.com.private
264 | generating DNS TXT record with public key and storing it in data/dkim/example.com.txt
265 |
266 | default._domainkey IN TXT ( "v=DKIM1; k=rsa; "
267 | "p=***"
268 | "***"
269 | ) ;
270 | ```
271 |
272 | The keypair will be available in `/data/dkim`.
273 |
274 | ### Generate GPG key
275 |
276 | If you don't have an existing GPG key, you can generate a new GPG key with the
277 | following command:
278 |
279 | ```console
280 | docker compose exec --user anonaddy addy gpg --full-gen-key
281 | ```
282 |
283 | Keys will be stored in `/data/.gnupg` folder.
284 |
285 | ### Define additional env vars
286 |
287 | You can define additional environment variables that will be used by the app
288 | by creating a file named `.env` in `/data`.
289 |
290 | ### Override Postfix main configuration
291 |
292 | In some cases you may want to override the default Postfix main configuration
293 | to fit your infrastructure. To do so, you can create a file named
294 | `postfix-main.alt.cf` in `/data` and it will be used instead of the generated
295 | configuration. **Use at your own risk**.
296 |
297 | > [!WARNING]
298 | > Container has to be restarted to propagate changes
299 |
300 | ### Spamhaus DQS configuration
301 |
302 | If a public DNS resolver is used, it may be blocked by Spamhaus and return a
303 | 'non-existent domain' (NXDOMAIN), and soon will start to return an error code:
304 |
305 | ```text
306 | Aug 3 10:15:40 mail01 postfix/smtpd[23645]: NOQUEUE: reject: RCPT from sender.example.com[xx.xx.xx.xx]: 554 5.7.1 Service unavailable;
307 | Client host [xx.xx.xx.xx] blocked using zen.spamhaus.org; Error: open resolver; https://www.spamhaus.org/returnc/pub/162.158.148.77;
308 | from= to= proto=ESMTP helo=
309 | ```
310 |
311 | To fix this issue, you can register a DQS key [here](https://www.spamhaustech.com/dqs/)
312 | and complete the registration procedure. After you register an account, you can find the DQS key in the "Access" section of
313 | [this page](https://portal.spamhaustech.com/dqs/).
314 |
315 | ## Contributing
316 |
317 | Want to contribute? Awesome! The most basic way to show your support is to star
318 | the project, or to raise issues. You can also support this project by [**becoming a sponsor on GitHub**](https://github.com/sponsors/crazy-max)
319 | or by making a [PayPal donation](https://www.paypal.me/crazyws) to ensure this
320 | journey continues indefinitely!
321 |
322 | Thanks again for your support, it is much appreciated! :pray:
323 |
324 | ## License
325 |
326 | MIT. See `LICENSE` for more details.
327 |
--------------------------------------------------------------------------------
/docker-bake.hcl:
--------------------------------------------------------------------------------
1 | variable "DEFAULT_TAG" {
2 | default = "addy:local"
3 | }
4 |
5 | // Special target: https://github.com/docker/metadata-action#bake-definition
6 | target "docker-metadata-action" {
7 | tags = ["${DEFAULT_TAG}"]
8 | }
9 |
10 | // Default target if none specified
11 | group "default" {
12 | targets = ["image-local"]
13 | }
14 |
15 | target "image" {
16 | inherits = ["docker-metadata-action"]
17 | }
18 |
19 | target "image-local" {
20 | inherits = ["image"]
21 | output = ["type=docker"]
22 | }
23 |
24 | target "image-all" {
25 | inherits = ["image"]
26 | platforms = [
27 | "linux/amd64",
28 | "linux/arm/v6",
29 | "linux/arm/v7",
30 | "linux/arm64"
31 | ]
32 | }
33 |
--------------------------------------------------------------------------------
/examples/compose/.env:
--------------------------------------------------------------------------------
1 | MYSQL_DATABASE=addy
2 | MYSQL_USER=addy
3 | MYSQL_PASSWORD=addy
4 |
--------------------------------------------------------------------------------
/examples/compose/addy.env:
--------------------------------------------------------------------------------
1 | TZ=Europe/Paris
2 | PUID=1000
3 | PGID=1000
4 |
5 | MEMORY_LIMIT=256M
6 | UPLOAD_MAX_SIZE=16M
7 | OPCACHE_MEM_SIZE=128
8 | REAL_IP_FROM=0.0.0.0/32
9 | REAL_IP_HEADER=X-Forwarded-For
10 | LOG_IP_VAR=remote_addr
11 |
12 | APP_KEY=
13 | APP_DEBUG=false
14 | APP_URL=http://127.0.0.1:8000
15 |
16 | ANONADDY_RETURN_PATH=bounces@example.com
17 | ANONADDY_ADMIN_USERNAME=addy
18 | ANONADDY_ENABLE_REGISTRATION=true
19 | ANONADDY_DOMAIN=example.com
20 | ANONADDY_ALL_DOMAINS=example.com
21 | ANONADDY_HOSTNAME=mail.example.com
22 | ANONADDY_DNS_RESOLVER=127.0.0.1
23 | ANONADDY_SECRET=
24 | ANONADDY_LIMIT=200
25 | ANONADDY_BANDWIDTH_LIMIT=104857600
26 | ANONADDY_NEW_ALIAS_LIMIT=10
27 | ANONADDY_ADDITIONAL_USERNAME_LIMIT=3
28 |
29 | MAIL_FROM_NAME=addy.io
30 | MAIL_FROM_ADDRESS=addy@example.com
31 |
32 | POSTFIX_DEBUG=false
33 | POSTFIX_SMTPD_TLS=false
34 | POSTFIX_SMTP_TLS=false
35 |
--------------------------------------------------------------------------------
/examples/compose/compose.yml:
--------------------------------------------------------------------------------
1 | name: addy
2 |
3 | services:
4 | db:
5 | image: mariadb:10
6 | container_name: addy_db
7 | command:
8 | - "mysqld"
9 | - "--character-set-server=utf8mb4"
10 | - "--collation-server=utf8mb4_unicode_ci"
11 | volumes:
12 | - "./db:/var/lib/mysql"
13 | environment:
14 | - "MARIADB_RANDOM_ROOT_PASSWORD=yes"
15 | - "MYSQL_DATABASE"
16 | - "MYSQL_USER"
17 | - "MYSQL_PASSWORD"
18 | restart: always
19 |
20 | redis:
21 | image: redis:4.0-alpine
22 | container_name: addy_redis
23 | restart: always
24 |
25 | addy:
26 | image: anonaddy/anonaddy:latest
27 | container_name: addy
28 | depends_on:
29 | - db
30 | - redis
31 | ports:
32 | - target: 25
33 | published: 25
34 | protocol: tcp
35 | - target: 8000
36 | published: 8000
37 | protocol: tcp
38 | volumes:
39 | - "./data:/data"
40 | env_file:
41 | - "./addy.env"
42 | environment:
43 | - "DB_HOST=db"
44 | - "DB_DATABASE=${MYSQL_DATABASE}"
45 | - "DB_USERNAME=${MYSQL_USER}"
46 | - "DB_PASSWORD=${MYSQL_PASSWORD}"
47 | - "REDIS_HOST=redis"
48 | restart: always
49 |
--------------------------------------------------------------------------------
/examples/nginx/README.md:
--------------------------------------------------------------------------------
1 | # Prerequisites
2 |
3 | Read [self-hosting docs](https://addy.io/self-hosting/)
4 |
5 | ## Let's Encrypt
6 |
7 | Generate your certificates and make note of where they are stored. if you use
8 | certbot, they are generally in `/etc/letsencrypt/live`.
9 |
10 | ## Generate strong dhparam
11 |
12 | ```sh
13 | sudo openssl dhparam -out dhparam.pem 4096
14 | ```
15 |
16 | ## Configure mounts for nginx
17 |
18 | The `compose.yml` may need some adjusting to properly mount your specific
19 | let's encrypt and dhparam certs.
20 |
21 | ## Rspamd web ui
22 |
23 | This nginx configuration supports rspamd web ui out of the box. if you choose
24 | to not run rspamd, make sure to remove the `RSPAMD_ENABLE` variable in
25 | `addy.env` and remove the proxy block in `nginx/templates/default.conf.template`.
26 |
--------------------------------------------------------------------------------
/examples/nginx/addy.env:
--------------------------------------------------------------------------------
1 | TZ=Europe/Paris
2 | PUID=1000
3 | PGID=1000
4 |
5 | MEMORY_LIMIT=256M
6 | UPLOAD_MAX_SIZE=16M
7 | OPCACHE_MEM_SIZE=128
8 | REAL_IP_FROM=0.0.0.0/32
9 | REAL_IP_HEADER=X-Forwarded-For
10 | LOG_IP_VAR=remote_addr
11 |
12 | APP_KEY=base64:KJ1LX0w15ItOoMWdC+DNW2Bt0Z4sT98zu0XQ8Zfaf9o=
13 | APP_DEBUG=false
14 | APP_URL=http://127.0.0.1:8000
15 |
16 | ANONADDY_RETURN_PATH=bounces@example.com
17 | ANONADDY_ADMIN_USERNAME=addy
18 | ANONADDY_ENABLE_REGISTRATION=true
19 | ANONADDY_DOMAIN=example.com
20 | ANONADDY_ALL_DOMAINS=example.com
21 | ANONADDY_HOSTNAME=mail.example.com
22 | ANONADDY_DNS_RESOLVER=127.0.0.1
23 | ANONADDY_SECRET=lksjflk2u3j4oij2elkru23oi4uj2lkjflsakfjoi23u4
24 | ANONADDY_LIMIT=200
25 | ANONADDY_BANDWIDTH_LIMIT=104857600
26 | ANONADDY_NEW_ALIAS_LIMIT=10
27 | ANONADDY_ADDITIONAL_USERNAME_LIMIT=3
28 |
29 | MAIL_FROM_NAME=addy.io
30 | MAIL_FROM_ADDRESS=addy@example.com
31 |
32 | POSTFIX_DEBUG=false
33 | POSTFIX_SMTPD_TLS=false
34 | POSTFIX_SMTP_TLS=false
35 |
36 | RSPAMD_ENABLE=true
37 | RSPAMD_WEB_PASSWORD=abc
38 |
--------------------------------------------------------------------------------
/examples/nginx/compose.yml:
--------------------------------------------------------------------------------
1 | name: addy
2 |
3 | services:
4 | db:
5 | image: mariadb:10
6 | container_name: addy_db
7 | command:
8 | - "mysqld"
9 | - "--character-set-server=utf8mb4"
10 | - "--collation-server=utf8mb4_unicode_ci"
11 | volumes:
12 | - "./db:/var/lib/mysql"
13 | environment:
14 | - "MARIADB_RANDOM_ROOT_PASSWORD=yes"
15 | - "MYSQL_DATABASE"
16 | - "MYSQL_USER"
17 | - "MYSQL_PASSWORD"
18 | restart: always
19 |
20 | redis:
21 | image: redis:4.0-alpine
22 | container_name: addy_redis
23 | restart: always
24 |
25 | addy:
26 | image: anonaddy/anonaddy:latest
27 | container_name: addy
28 | depends_on:
29 | - db
30 | - redis
31 | ports:
32 | - target: 25
33 | published: 25
34 | protocol: tcp
35 | volumes:
36 | - "./data:/data"
37 | env_file:
38 | - "./addy.env"
39 | environment:
40 | - "DB_HOST=db"
41 | - "DB_DATABASE=${MYSQL_DATABASE}"
42 | - "DB_USERNAME=${MYSQL_USER}"
43 | - "DB_PASSWORD=${MYSQL_PASSWORD}"
44 | - "REDIS_HOST=redis"
45 | restart: always
46 |
47 | nginx:
48 | image: nginx:1.20.1-alpine
49 | container_name: addy_nginx
50 | restart: unless-stopped
51 | ports:
52 | - '443:443'
53 | volumes:
54 | - /etc/ssl/dhparam.pem:/etc/ssl/dhparam.pem
55 | - ./nginx/templates:/etc/nginx/templates
56 | - /etc/letsencrypt:/etc/letsencrypt
57 | depends_on:
58 | - addy
59 |
--------------------------------------------------------------------------------
/examples/nginx/nginx/templates/default.conf.template:
--------------------------------------------------------------------------------
1 | server {
2 | listen 80;
3 | listen [::]:80;
4 |
5 | server_name example.com;
6 | return 301 https://$server_name$request_uri;
7 | }
8 |
9 | server {
10 | listen 443 ssl;
11 | listen [::]:443 ssl;
12 | http2 on;
13 | server_name example.com;
14 | server_tokens off;
15 |
16 | add_header X-Frame-Options "SAMEORIGIN";
17 | add_header X-XSS-Protection "1; mode=block";
18 | add_header X-Content-Type-Options "nosniff";
19 | add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
20 | add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; font-src 'self'; object-src 'none'";
21 | add_header Referrer-Policy "origin-when-cross-origin";
22 | add_header Expect-CT "enforce, max-age=604800";
23 |
24 | charset utf-8;
25 |
26 | ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
27 | ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
28 | ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
29 |
30 | ssl_prefer_server_ciphers on;
31 | ssl_session_timeout 5m;
32 | ssl_protocols TLSv1.2 TLSv1.3;
33 | ssl_stapling on;
34 | ssl_stapling_verify on;
35 | ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384";
36 | ssl_ecdh_curve secp384r1;
37 | ssl_session_cache shared:SSL:20m;
38 | ssl_session_tickets off;
39 | ssl_dhparam /etc/ssl/dhparam.pem;
40 |
41 | location = /robots.txt {
42 | add_header Content-Type text/plain;
43 | return 200 "User-agent: *\nDisallow: /\n";
44 | }
45 |
46 | location /rspamd {
47 | proxy_pass http://addy:11334;
48 | proxy_set_header Host $host;
49 | proxy_set_header X-Real-IP $remote_addr;
50 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
51 | proxy_set_header X-Forwarded-Proto $scheme;
52 | }
53 |
54 | location / {
55 | proxy_pass http://addy:8000;
56 | proxy_redirect off;
57 | proxy_set_header Host $host;
58 | proxy_set_header X-Real-IP $remote_addr;
59 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
60 | proxy_set_header X-Forwarded-Proto $scheme;
61 | proxy_read_timeout 90s;
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/examples/nginx/nginx/templates/mta-sts.conf.template:
--------------------------------------------------------------------------------
1 | server {
2 | listen 443 ssl;
3 | server_name mta-sts.example.com;
4 | error_log /var/log/nginx/error.log;
5 | access_log /var/log/nginx/access.log;
6 |
7 | ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
8 | ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
9 | ssl_trusted_certificate /etc/letsencrypt/live/example.com/chain.pem;
10 |
11 | ssl_prefer_server_ciphers On;
12 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
13 | ssl_ciphers ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS;
14 | ssl_session_cache shared:SSL:20m;
15 | ssl_session_timeout 10m;
16 | add_header Strict-Transport-Security "max-age=31536000";
17 |
18 | location = /robots.txt {
19 | add_header Content-Type text/plain;
20 | return 200 "User-agent: *\nDisallow: /\n";
21 | }
22 |
23 | location ^~ /.well-known/mta-sts.txt {
24 | try_files $uri @mta-sts;
25 | }
26 |
27 | location @mta-sts {
28 | return 200 "version: STSv1
29 | mode: enforce
30 | max_age: 86400
31 | mx: example.com
32 | mx: example.com\n";
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/examples/rspamd/.env:
--------------------------------------------------------------------------------
1 | MYSQL_DATABASE=addy
2 | MYSQL_USER=addy
3 | MYSQL_PASSWORD=addy
4 |
--------------------------------------------------------------------------------
/examples/rspamd/addy.env:
--------------------------------------------------------------------------------
1 | TZ=Europe/Paris
2 | PUID=1000
3 | PGID=1000
4 |
5 | MEMORY_LIMIT=256M
6 | UPLOAD_MAX_SIZE=16M
7 | OPCACHE_MEM_SIZE=128
8 | REAL_IP_FROM=0.0.0.0/32
9 | REAL_IP_HEADER=X-Forwarded-For
10 | LOG_IP_VAR=remote_addr
11 |
12 | APP_KEY=base64:KJ1LX0w15ItOoMWdC+DNW2Bt0Z4sT98zu0XQ8Zfaf9o=
13 | APP_DEBUG=false
14 | APP_URL=http://127.0.0.1:8000
15 |
16 | ANONADDY_RETURN_PATH=bounces@example.com
17 | ANONADDY_ADMIN_USERNAME=addy
18 | ANONADDY_ENABLE_REGISTRATION=true
19 | ANONADDY_DOMAIN=example.com
20 | ANONADDY_ALL_DOMAINS=example.com
21 | ANONADDY_HOSTNAME=mail.example.com
22 | ANONADDY_DNS_RESOLVER=127.0.0.1
23 | ANONADDY_SECRET=lksjflk2u3j4oij2elkru23oi4uj2lkjflsakfjoi23u4
24 | ANONADDY_LIMIT=200
25 | ANONADDY_BANDWIDTH_LIMIT=104857600
26 | ANONADDY_NEW_ALIAS_LIMIT=10
27 | ANONADDY_ADDITIONAL_USERNAME_LIMIT=3
28 |
29 | MAIL_FROM_NAME=addy.io
30 | MAIL_FROM_ADDRESS=addy@example.com
31 |
32 | POSTFIX_DEBUG=false
33 | POSTFIX_SMTPD_TLS=false
34 | POSTFIX_SMTP_TLS=false
35 |
36 | RSPAMD_ENABLE=true
37 | RSPAMD_WEB_PASSWORD=abc
38 |
--------------------------------------------------------------------------------
/examples/rspamd/compose.yml:
--------------------------------------------------------------------------------
1 | name: annoaddy
2 |
3 | services:
4 | db:
5 | image: mariadb:10
6 | container_name: addy_db
7 | command:
8 | - "mysqld"
9 | - "--character-set-server=utf8mb4"
10 | - "--collation-server=utf8mb4_unicode_ci"
11 | volumes:
12 | - "./db:/var/lib/mysql"
13 | environment:
14 | - "MARIADB_RANDOM_ROOT_PASSWORD=yes"
15 | - "MYSQL_DATABASE"
16 | - "MYSQL_USER"
17 | - "MYSQL_PASSWORD"
18 | restart: always
19 |
20 | redis:
21 | image: redis:4.0-alpine
22 | container_name: addy_redis
23 | restart: always
24 |
25 | addy:
26 | image: anonaddy/anonaddy:latest
27 | container_name: addy
28 | depends_on:
29 | - db
30 | - redis
31 | ports:
32 | - target: 25
33 | published: 25
34 | protocol: tcp
35 | - target: 8000
36 | published: 8000
37 | protocol: tcp
38 | - target: 11334
39 | published: 11334
40 | protocol: tcp
41 | volumes:
42 | - "./data:/data"
43 | env_file:
44 | - "./addy.env"
45 | environment:
46 | - "DB_HOST=db"
47 | - "DB_DATABASE=${MYSQL_DATABASE}"
48 | - "DB_USERNAME=${MYSQL_USER}"
49 | - "DB_PASSWORD=${MYSQL_PASSWORD}"
50 | - "REDIS_HOST=redis"
51 | restart: always
52 |
--------------------------------------------------------------------------------
/examples/traefik/.env:
--------------------------------------------------------------------------------
1 | MYSQL_DATABASE=addy
2 | MYSQL_USER=addy
3 | MYSQL_PASSWORD=addy
4 |
--------------------------------------------------------------------------------
/examples/traefik/README.md:
--------------------------------------------------------------------------------
1 | ## Usage
2 |
3 | ```bash
4 | touch acme.json
5 | chmod 600 acme.json
6 | docker compose up -d
7 | docker compose logs -f
8 | ```
9 |
--------------------------------------------------------------------------------
/examples/traefik/anonaddy.env:
--------------------------------------------------------------------------------
1 | TZ=Europe/Paris
2 | PUID=1000
3 | PGID=1000
4 |
5 | MEMORY_LIMIT=256M
6 | UPLOAD_MAX_SIZE=16M
7 | OPCACHE_MEM_SIZE=128
8 | REAL_IP_FROM=0.0.0.0/32
9 | REAL_IP_HEADER=X-Forwarded-For
10 | LOG_IP_VAR=http_x_forwarded_for
11 |
12 | APP_KEY=
13 | APP_DEBUG=false
14 | APP_URL=https://addy.example.com
15 |
16 | ANONADDY_RETURN_PATH=bounces@example.com
17 | ANONADDY_ADMIN_USERNAME=addy
18 | ANONADDY_ENABLE_REGISTRATION=true
19 | ANONADDY_DOMAIN=example.com
20 | ANONADDY_ALL_DOMAINS=example.com
21 | ANONADDY_HOSTNAME=addy.example.com
22 | ANONADDY_DNS_RESOLVER=127.0.0.1
23 | ANONADDY_SECRET=
24 | ANONADDY_LIMIT=200
25 | ANONADDY_BANDWIDTH_LIMIT=104857600
26 | ANONADDY_NEW_ALIAS_LIMIT=10
27 | ANONADDY_ADDITIONAL_USERNAME_LIMIT=3
28 |
29 | MAIL_FROM_NAME=addy.io
30 | MAIL_FROM_ADDRESS=addy@example.com
31 |
32 | POSTFIX_DEBUG=false
33 | POSTFIX_SMTPD_TLS=false
34 | POSTFIX_SMTP_TLS=false
35 |
--------------------------------------------------------------------------------
/examples/traefik/compose.yml:
--------------------------------------------------------------------------------
1 | name: addy
2 |
3 | services:
4 | traefik:
5 | image: traefik:2.5
6 | container_name: traefik
7 | command:
8 | - "--global.checknewversion=false"
9 | - "--global.sendanonymoususage=false"
10 | - "--log=true"
11 | - "--log.level=INFO"
12 | - "--entrypoints.http=true"
13 | - "--entrypoints.http.address=:80"
14 | - "--entrypoints.http.http.redirections.entrypoint.to=https"
15 | - "--entrypoints.http.http.redirections.entrypoint.scheme=https"
16 | - "--entrypoints.https=true"
17 | - "--entrypoints.https.address=:443"
18 | - "--certificatesresolvers.letsencrypt"
19 | - "--certificatesresolvers.letsencrypt.acme.storage=acme.json"
20 | - "--certificatesresolvers.letsencrypt.acme.email=webmaster@example.com"
21 | - "--certificatesresolvers.letsencrypt.acme.httpchallenge"
22 | - "--certificatesresolvers.letsencrypt.acme.httpchallenge.entrypoint=http"
23 | - "--providers.docker"
24 | - "--providers.docker.watch=true"
25 | - "--providers.docker.exposedbydefault=false"
26 | ports:
27 | - target: 80
28 | published: 80
29 | protocol: tcp
30 | - target: 443
31 | published: 443
32 | protocol: tcp
33 | volumes:
34 | - "./acme.json:/acme.json"
35 | - "/var/run/docker.sock:/var/run/docker.sock"
36 | restart: always
37 |
38 | db:
39 | image: mariadb:10
40 | container_name: addy_db
41 | command:
42 | - "mysqld"
43 | - "--character-set-server=utf8mb4"
44 | - "--collation-server=utf8mb4_unicode_ci"
45 | volumes:
46 | - "./db:/var/lib/mysql"
47 | environment:
48 | - "MARIADB_RANDOM_ROOT_PASSWORD=yes"
49 | - "MYSQL_DATABASE"
50 | - "MYSQL_USER"
51 | - "MYSQL_PASSWORD"
52 | restart: always
53 |
54 | redis:
55 | image: redis:4.0-alpine
56 | container_name: addy_redis
57 | restart: always
58 |
59 | addy:
60 | image: anonaddy/anonaddy:latest
61 | container_name: addy
62 | depends_on:
63 | - db
64 | - redis
65 | ports:
66 | - target: 25
67 | published: 25
68 | protocol: tcp
69 | volumes:
70 | - "./data:/data"
71 | labels:
72 | - "traefik.enable=true"
73 | - "traefik.http.routers.addy.entrypoints=https"
74 | - "traefik.http.routers.addy.rule=Host(`addy.example.com`)"
75 | - "traefik.http.routers.addy.tls=true"
76 | - "traefik.http.routers.addy.tls.certresolver=letsencrypt"
77 | - "traefik.http.routers.addy.tls.domains[0].main=addy.example.com"
78 | - "traefik.http.services.addy.loadbalancer.server.port=8000"
79 | env_file:
80 | - "./addy.env"
81 | environment:
82 | - "DB_HOST=db"
83 | - "DB_DATABASE=${MYSQL_DATABASE}"
84 | - "DB_USERNAME=${MYSQL_USER}"
85 | - "DB_PASSWORD=${MYSQL_PASSWORD}"
86 | - "REDIS_HOST=redis"
87 | restart: always
88 |
--------------------------------------------------------------------------------
/rootfs/etc/cont-init.d/00-env:
--------------------------------------------------------------------------------
1 | #!/usr/bin/with-contenv bash
2 | # shellcheck shell=bash
3 |
4 | # From https://github.com/docker-library/mariadb/blob/master/docker-entrypoint.sh#L21-L41
5 | # usage: file_env VAR [DEFAULT]
6 | # ie: file_env 'XYZ_DB_PASSWORD' 'example'
7 | # (will allow for "$XYZ_DB_PASSWORD_FILE" to fill in the value of
8 | # "$XYZ_DB_PASSWORD" from a file, especially for Docker's secrets feature)
9 | file_env() {
10 | local var="$1"
11 | local fileVar="${var}_FILE"
12 | local def="${2:-}"
13 | if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
14 | echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
15 | exit 1
16 | fi
17 | local val="$def"
18 | if [ "${!var:-}" ]; then
19 | val="${!var}"
20 | elif [ "${!fileVar:-}" ]; then
21 | if [ ! -f "${!fileVar}" ]; then
22 | echo >&2 "error: ${!fileVar} file not found for ${fileVar}"
23 | exit 1
24 | fi
25 | val="$(<"${!fileVar}")"
26 | fi
27 | export "$var"="$val"
28 | unset "$fileVar"
29 | }
30 |
31 | TZ=${TZ:-UTC}
32 | MEMORY_LIMIT=${MEMORY_LIMIT:-256M}
33 | UPLOAD_MAX_SIZE=${UPLOAD_MAX_SIZE:-16M}
34 | CLEAR_ENV=${CLEAR_ENV:-yes}
35 | OPCACHE_MEM_SIZE=${OPCACHE_MEM_SIZE:-128}
36 | LISTEN_IPV6=${LISTEN_IPV6:-true}
37 | REAL_IP_FROM=${REAL_IP_FROM:-0.0.0.0/32}
38 | REAL_IP_HEADER=${REAL_IP_HEADER:-X-Forwarded-For}
39 | LOG_IP_VAR=${LOG_IP_VAR:-remote_addr}
40 | LOG_CROND=${LOG_CROND:-true}
41 |
42 | APP_NAME=${APP_NAME:-addy.io}
43 | #APP_KEY=${APP_KEY:-base64:Gh8/RWtNfXTmB09pj6iEflt/L6oqDf9ZxXIh4I9MS7A=}
44 | APP_DEBUG=${APP_DEBUG:-false}
45 | APP_URL=${APP_URL:-http://localhost}
46 |
47 | #DB_HOST=${DB_HOST:-localhost}
48 | DB_PORT=${DB_PORT:-3306}
49 | DB_DATABASE=${DB_DATABASE:-anonaddy}
50 | #DB_USERNAME=${DB_USERNAME:-anonaddy}
51 | #DB_PASSWORD=${DB_PASSWORD:-asupersecretpassword}
52 | DB_TIMEOUT=${DB_TIMEOUT:-60}
53 |
54 | REDIS_HOST=${REDIS_HOST:-null}
55 | #REDIS_PASSWORD=${REDIS_PASSWORD:-null}
56 | REDIS_PORT=${REDIS_PORT:-6379}
57 |
58 | #PUSHER_APP_ID=${PUSHER_APP_ID}
59 | #PUSHER_APP_KEY=${PUSHER_APP_KEY}
60 | #PUSHER_APP_SECRET=${PUSHER_APP_SECRET}
61 | PUSHER_APP_CLUSTER=${PUSHER_APP_CLUSTER:-mt1}
62 |
63 | ANONADDY_RETURN_PATH=${ANONADDY_RETURN_PATH:-null}
64 | ANONADDY_ADMIN_USERNAME=${ANONADDY_ADMIN_USERNAME:-null}
65 | ANONADDY_ENABLE_REGISTRATION=${ANONADDY_ENABLE_REGISTRATION:-true}
66 | #ANONADDY_DOMAIN=${ANONADDY_DOMAIN:-null}
67 | ANONADDY_HOSTNAME=${ANONADDY_HOSTNAME:-null}
68 | ANONADDY_DNS_RESOLVER=${ANONADDY_DNS_RESOLVER:-127.0.0.1}
69 | ANONADDY_ALL_DOMAINS=${ANONADDY_ALL_DOMAINS:-$ANONADDY_DOMAIN}
70 | ANONADDY_NON_ADMIN_SHARED_DOMAINS=${ANONADDY_NON_ADMIN_SHARED_DOMAINS:-true}
71 | #ANONADDY_SECRET=${ANONADDY_SECRET:-long-random-string}
72 | ANONADDY_LIMIT=${ANONADDY_LIMIT:-200}
73 | ANONADDY_BANDWIDTH_LIMIT=${ANONADDY_BANDWIDTH_LIMIT:-104857600}
74 | ANONADDY_NEW_ALIAS_LIMIT=${ANONADDY_NEW_ALIAS_LIMIT:-10}
75 | ANONADDY_ADDITIONAL_USERNAME_LIMIT=${ANONADDY_ADDITIONAL_USERNAME_LIMIT:-10}
76 | #ANONADDY_SIGNING_KEY_FINGERPRINT=${ANONADDY_SIGNING_KEY_FINGERPRINT:-your-signing-key-fingerprint}
77 | #ANONADDY_DKIM_SIGNING_KEY=${ANONADDY_DKIM_SIGNING_KEY:-dkim-signing-key}
78 | ANONADDY_DKIM_SELECTOR=${ANONADDY_DKIM_SELECTOR:-default}
79 |
80 | MAIL_FROM_NAME=${MAIL_FROM_NAME:-addy.io}
81 | MAIL_FROM_ADDRESS=${MAIL_FROM_ADDRESS:-addy@${ANONADDY_DOMAIN}}
82 | MAIL_ENCRYPTION=${MAIL_ENCRYPTION:-null}
83 |
84 | POSTFIX_DEBUG=${POSTFIX_DEBUG:-false}
85 | POSTFIX_MESSAGE_SIZE_LIMIT=${POSTFIX_MESSAGE_SIZE_LIMIT:-26214400}
86 | POSTFIX_SMTPD_TLS=${POSTFIX_SMTPD_TLS:-false}
87 | POSTFIX_SMTP_TLS=${POSTFIX_SMTP_TLS:-false}
88 | POSTFIX_RELAYHOST_AUTH_ENABLE=${POSTFIX_RELAYHOST_AUTH_ENABLE:-false}
89 | POSTFIX_RELAYHOST_SSL_ENCRYPTION=${POSTFIX_RELAYHOST_SSL_ENCRYPTION:-false}
90 | #POSTFIX_SPAMHAUS_DQS_KEY=${POSTFIX_SPAMHAUS_DQS_KEY:-null}
91 | #POSTFIX_RELAYHOST_USERNAME=${POSTFIX_RELAYHOST_USERNAME:-null}
92 | #POSTFIX_RELAYHOST_PASSWORD=${POSTFIX_RELAYHOST_PASSWORD:-null}
93 |
94 | RSPAMD_ENABLE=${RSPAMD_ENABLE:-false}
95 | #RSPAMD_WEB_PASSWORD=${RSPAMD_WEB_PASSWORD:-null}
96 | RSPAMD_NO_LOCAL_ADDRS=${RSPAMD_NO_LOCAL_ADDRS:-false}
97 |
98 | DKIM_PRIVATE_KEY=/data/dkim/${ANONADDY_DOMAIN}.private
99 |
100 | SMTPD_MILTERS=""
101 | if [ "$RSPAMD_ENABLE" = "true" ] && [ -f "$DKIM_PRIVATE_KEY" ]; then
102 | SMTPD_MILTERS=${RSPAMD_SMTPD_MILTERS:-inet:127.0.0.1:11332}
103 | fi
104 |
105 | # Keep them to check if users are still using an old configuration
106 | DKIM_ENABLE=${DKIM_ENABLE:-false}
107 | DMARC_ENABLE=${DMARC_ENABLE:-false}
108 |
109 | file_env 'APP_KEY'
110 | file_env 'DB_USERNAME' 'anonaddy'
111 | file_env 'DB_PASSWORD'
112 | file_env 'REDIS_PASSWORD'
113 | file_env 'PUSHER_APP_SECRET'
114 | file_env 'ANONADDY_SECRET'
115 | file_env 'ANONADDY_SIGNING_KEY_FINGERPRINT'
116 | file_env 'POSTFIX_RELAYHOST_USERNAME' 'null'
117 | file_env 'POSTFIX_RELAYHOST_PASSWORD' 'null'
118 | file_env 'RSPAMD_WEB_PASSWORD' 'null'
119 |
--------------------------------------------------------------------------------
/rootfs/etc/cont-init.d/00-fix-logs.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/with-contenv sh
2 | # shellcheck shell=sh
3 |
4 | # Fix access rights to stdout and stderr
5 | chown ${PUID}:${PGID} /proc/self/fd/1 /proc/self/fd/2 || true
6 |
--------------------------------------------------------------------------------
/rootfs/etc/cont-init.d/01-fix-uidgid.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/with-contenv sh
2 | # shellcheck shell=sh
3 | set -e
4 |
5 | if [ -n "${PGID}" ] && [ "${PGID}" != "$(id -g anonaddy)" ]; then
6 | echo "Switching to PGID ${PGID}..."
7 | sed -i -e "s/^anonaddy:\([^:]*\):[0-9]*/anonaddy:\1:${PGID}/" /etc/group
8 | sed -i -e "s/^anonaddy:\([^:]*\):\([0-9]*\):[0-9]*/anonaddy:\1:\2:${PGID}/" /etc/passwd
9 | fi
10 | if [ -n "${PUID}" ] && [ "${PUID}" != "$(id -u anonaddy)" ]; then
11 | echo "Switching to PUID ${PUID}..."
12 | sed -i -e "s/^anonaddy:\([^:]*\):[0-9]*:\([0-9]*\)/anonaddy:\1:${PUID}:\2/" /etc/passwd
13 | fi
14 |
--------------------------------------------------------------------------------
/rootfs/etc/cont-init.d/02-fix-perms.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/with-contenv sh
2 | # shellcheck shell=sh
3 | set -e
4 |
5 | echo "Fixing perms..."
6 | mkdir -p /data \
7 | /data/dkim \
8 | /data/postfix/queue \
9 | /var/run/nginx \
10 | /var/run/php-fpm
11 | chown anonaddy:anonaddy /data
12 | chown -R anonaddy:anonaddy \
13 | /data/dkim \
14 | /tpls \
15 | /var/lib/nginx \
16 | /var/log/nginx \
17 | /var/log/php83 \
18 | /var/run/nginx \
19 | /var/run/php-fpm \
20 | /var/www/anonaddy/bootstrap/cache \
21 | /var/www/anonaddy/config \
22 | /var/www/anonaddy/storage
23 |
--------------------------------------------------------------------------------
/rootfs/etc/cont-init.d/10-config.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/with-contenv bash
2 | # shellcheck shell=bash
3 | set -e
4 |
5 | . $(dirname $0)/00-env
6 |
7 | if [[ "$DKIM_ENABLE" = "true" || "$DMARC_ENABLE" = "true" ]]; then
8 | echo >&2 "ERROR: OpenDKIM/OpenDMARC are not supported anymore. Please use Rspamd instead."
9 | exit 1
10 | fi
11 |
12 | echo "Setting timezone to ${TZ}..."
13 | ln -snf /usr/share/zoneinfo/${TZ} /etc/localtime
14 | echo ${TZ} >/etc/timezone
15 |
16 | echo "Initializing files and folders"
17 | mkdir -p /data/config
18 | if [ ! -L /var/www/anonaddy/.config ]; then
19 | ln -sf /data/config /var/www/anonaddy/.config
20 | fi
21 | chown -h anonaddy:anonaddy /var/www/anonaddy/config
22 | chown -R anonaddy:anonaddy /data/config
23 | mkdir -p /data/storage
24 | if [ ! -L /var/www/anonaddy/storage ]; then
25 | cp -Rf /var/www/anonaddy/storage /data
26 | rm -rf /var/www/anonaddy/storage
27 | ln -sf /data/storage /var/www/anonaddy/storage
28 | fi
29 | chown -h anonaddy:anonaddy /var/www/anonaddy/storage
30 | chown -R anonaddy:anonaddy /data/storage
31 | mkdir -p /data/.gnupg
32 | if [ ! -L /var/www/anonaddy/.gnupg ]; then
33 | ln -sf /data/.gnupg /var/www/anonaddy/.gnupg
34 | fi
35 | chown -h anonaddy:anonaddy /var/www/anonaddy/.gnupg
36 | chown -R anonaddy:anonaddy /data/.gnupg
37 | chmod 700 /data/.gnupg
38 |
39 | echo "Checking database connection..."
40 | if [ -z "$DB_HOST" ]; then
41 | echo >&2 "ERROR: DB_HOST must be defined"
42 | exit 1
43 | fi
44 | if [ -z "$DB_PASSWORD" ]; then
45 | echo >&2 "ERROR: Either DB_PASSWORD or DB_PASSWORD_FILE must be defined"
46 | exit 1
47 | fi
48 | dbcmd="mariadb -h ${DB_HOST} -P ${DB_PORT} -u "${DB_USERNAME}" "-p${DB_PASSWORD}""
49 |
50 | echo "Waiting ${DB_TIMEOUT}s for database to be ready..."
51 | counter=1
52 | while ! ${dbcmd} -e "show databases;" >/dev/null 2>&1; do
53 | sleep 1
54 | counter=$((counter + 1))
55 | if [ ${counter} -gt ${DB_TIMEOUT} ]; then
56 | echo >&2 "ERROR: Failed to connect to database on $DB_HOST"
57 | exit 1
58 | fi
59 | done
60 | echo "Database ready!"
61 |
--------------------------------------------------------------------------------
/rootfs/etc/cont-init.d/11-config-php.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/with-contenv bash
2 | # shellcheck shell=bash
3 | set -e
4 |
5 | . $(dirname $0)/00-env
6 |
7 | echo "Init PHP extensions"
8 | cp -Rf /tpls/etc/php83/conf.d /etc/php83
9 |
10 | echo "Setting PHP-FPM configuration"
11 | sed -e "s/@MEMORY_LIMIT@/$MEMORY_LIMIT/g" \
12 | -e "s/@UPLOAD_MAX_SIZE@/$UPLOAD_MAX_SIZE/g" \
13 | -e "s/@CLEAR_ENV@/$CLEAR_ENV/g" \
14 | /tpls/etc/php83/php-fpm.d/www.conf >/etc/php83/php-fpm.d/www.conf
15 |
16 | echo "Setting PHP INI configuration"
17 | sed -i "s|memory_limit.*|memory_limit = ${MEMORY_LIMIT}|g" /etc/php83/php.ini
18 | sed -i "s|;date\.timezone.*|date\.timezone = ${TZ}|g" /etc/php83/php.ini
19 |
20 | echo "Setting OpCache configuration"
21 | sed -e "s/@OPCACHE_MEM_SIZE@/$OPCACHE_MEM_SIZE/g" \
22 | /tpls/etc/php83/conf.d/opcache.ini >/etc/php83/conf.d/opcache.ini
23 |
--------------------------------------------------------------------------------
/rootfs/etc/cont-init.d/12-config-nginx.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/with-contenv bash
2 | # shellcheck shell=bash
3 | set -e
4 |
5 | . $(dirname $0)/00-env
6 |
7 | echo "Setting Nginx configuration"
8 | sed -e "s#@UPLOAD_MAX_SIZE@#$UPLOAD_MAX_SIZE#g" \
9 | -e "s#@REAL_IP_FROM@#$REAL_IP_FROM#g" \
10 | -e "s#@REAL_IP_HEADER@#$REAL_IP_HEADER#g" \
11 | -e "s#@LOG_IP_VAR@#$LOG_IP_VAR#g" \
12 | /tpls/etc/nginx/nginx.conf >/etc/nginx/nginx.conf
13 |
14 | if [ "$LISTEN_IPV6" != "true" ]; then
15 | sed -e '/listen \[::\]:/d' -i /etc/nginx/nginx.conf
16 | fi
17 |
--------------------------------------------------------------------------------
/rootfs/etc/cont-init.d/13-config-anonaddy.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/with-contenv bash
2 | # shellcheck shell=bash
3 | set -e
4 |
5 | . $(dirname $0)/00-env
6 |
7 | if [ -z "$APP_KEY" ]; then
8 | echo >&2 "ERROR: Either APP_KEY or APP_KEY_FILE must be defined"
9 | exit 1
10 | fi
11 | if [ -z "$ANONADDY_DOMAIN" ]; then
12 | echo >&2 "ERROR: ANONADDY_DOMAIN must be defined"
13 | exit 1
14 | fi
15 |
16 | if [ -z "$ANONADDY_SECRET" ]; then
17 | echo >&2 "ERROR: Either ANONADDY_SECRET or ANONADDY_SECRET_FILE must be defined"
18 | exit 1
19 | fi
20 |
21 | echo "Creating env file"
22 | cat >/var/www/anonaddy/.env <> /var/www/anonaddy/.env
87 | fi
88 |
89 | chown anonaddy:anonaddy /var/www/anonaddy/.env
90 |
91 | echo "Trust all proxies"
92 | sed -i "s|^ protected \$proxies.*| protected \$proxies = '\*';|g" /var/www/anonaddy/vendor/laravel/framework/src/Illuminate/Http/Middleware/TrustProxies.php
93 |
--------------------------------------------------------------------------------
/rootfs/etc/cont-init.d/14-config-rspamd.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/with-contenv bash
2 | # shellcheck shell=bash
3 | set -e
4 |
5 | . $(dirname $0)/00-env
6 |
7 | if [ "$RSPAMD_ENABLE" != "true" ]; then
8 | echo "INFO: Rspamd service disabled."
9 | exit 0
10 | fi
11 |
12 | echo "Determining shared domains"
13 | CHECK_DOMAINS="${ANONADDY_ALL_DOMAINS}"
14 | if [[ "${CHECK_DOMAINS}" != *"${ANONADDY_DOMAIN}"* ]]; then
15 | CHECK_DOMAINS="${ANONADDY_DOMAIN} ${CHECK_DOMAINS}"
16 | fi
17 |
18 | echo "Building DKIM tables"
19 | CONFIG_SIGNING_TABLE=
20 | CONFIG_KEY_TABLE=
21 | for DOM in ${CHECK_DOMAINS//,/ }; do
22 | CONFIG_SIGNING_TABLE=$( printf '%s\n"*@%s %s",\n"*@*.%s %s",' "${CONFIG_SIGNING_TABLE}" "${DOM}" "${DOM}" "${DOM}" "${DOM}")
23 | CONFIG_KEY_TABLE=$( printf '%s\n"%s %s:%s:/var/lib/rspamd/dkim/%s.%s.key",' "${CONFIG_KEY_TABLE}" "${DOM}" "${DOM}" "${ANONADDY_DKIM_SELECTOR}" "${DOM}" "${ANONADDY_DKIM_SELECTOR}")
24 | # try to register a new dkim and if it fails don't exit this script.
25 | # failure can occur when the files have already been generated.
26 | /bin/sh /usr/local/bin/gen-dkim "${DOM}" >/dev/null 2>/dev/null && true
27 | done
28 | CONFIG_SIGNING_TABLE="${CONFIG_SIGNING_TABLE#*$'\n'}"
29 | CONFIG_KEY_TABLE="${CONFIG_KEY_TABLE#*$'\n'}"
30 |
31 | echo "Setting Rspamd dkim_signing.conf"
32 | cat >/etc/rspamd/local.d/dkim_signing.conf </etc/rspamd/local.d/classifier-bayes.conf </etc/rspamd/local.d/logging.inc </etc/rspamd/local.d/redis.conf </etc/rspamd/local.d/greylist.conf </etc/rspamd/local.d/history_redis.conf </etc/rspamd/local.d/groups.conf </etc/rspamd/local.d/worker-controller.inc </etc/rspamd/local.d/dmarc.conf </etc/rspamd/local.d/milter_headers.conf < /etc/rspamd/override.d/fuzzy_check.conf
199 | echo "enabled = false;" > /etc/rspamd/override.d/asn.conf
200 | echo "enabled = false;" > /etc/rspamd/override.d/metadata_exporter.conf
201 | echo "enabled = false;" > /etc/rspamd/override.d/trie.conf
202 | echo "enabled = false;" > /etc/rspamd/override.d/neural.conf
203 | echo "enabled = false;" > /etc/rspamd/override.d/chartable.conf
204 | echo "enabled = false;" > /etc/rspamd/override.d/ratelimit.conf
205 | echo "enabled = false;" > /etc/rspamd/override.d/replies.conf
206 |
--------------------------------------------------------------------------------
/rootfs/etc/cont-init.d/15-config-postfix.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/with-contenv bash
2 | # shellcheck shell=bash
3 | set -e
4 |
5 | . $(dirname $0)/00-env
6 |
7 | # Restore original config
8 | cp -f /etc/postfix/master.cf.orig /etc/postfix/master.cf
9 | cp -f /etc/postfix/main.cf.orig /etc/postfix/main.cf
10 |
11 | echo "Setting Postfix master configuration"
12 | POSTFIX_DEBUG_ARG=""
13 | if [ "$POSTFIX_DEBUG" = "true" ]; then
14 | POSTFIX_DEBUG_ARG=" -v"
15 | fi
16 | sed -i "s|^smtp.*inet.*|25 inet n - - - - smtpd${POSTFIX_DEBUG_ARG}|g" /etc/postfix/master.cf
17 | cat >>/etc/postfix/master.cf <>/etc/postfix/main.cf <>/etc/postfix/main.cf <>/etc/postfix/main.cf <>/etc/postfix/main.cf
147 | fi
148 | if [ -n "$POSTFIX_SMTPD_TLS_KEY_FILE" ]; then
149 | echo "smtpd_tls_key_file=${POSTFIX_SMTPD_TLS_KEY_FILE}" >>/etc/postfix/main.cf
150 | fi
151 | fi
152 |
153 | if [ "$POSTFIX_SMTP_TLS" = "true" ]; then
154 | echo "Setting Postfix smtp TLS configuration"
155 | cat >>/etc/postfix/main.cf <>/etc/postfix/main.cf <>/etc/postfix/main.cf <>/etc/postfix/main.cf </etc/postfix/sasl_passwd </etc/postfix/mysql-virtual-alias-domains-and-subdomains.cf < /etc/postfix/main.cf
226 | fi
227 |
228 | echo "Display Postfix config"
229 | postconf | sed -e 's/^/[postfix-config] /'
230 |
--------------------------------------------------------------------------------
/rootfs/etc/cont-init.d/50-svc-main.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/with-contenv bash
2 | # shellcheck shell=bash
3 | set -e
4 |
5 | echo "DB migration"
6 | anonaddy migrate --no-interaction --force
7 |
8 | echo "Clear cache"
9 | anonaddy cache:clear --no-interaction
10 | anonaddy config:cache --no-interaction
11 | anonaddy view:cache --no-interaction
12 | anonaddy route:cache --no-interaction
13 | anonaddy queue:restart --no-interaction
14 |
15 | mkdir -p /etc/services.d/nginx
16 | cat > /etc/services.d/nginx/run < /etc/services.d/php-fpm/run </etc/services.d/rspamd/run < /etc/services.d/postfix/run <> ${CRONTAB_PATH}/anonaddy
20 |
21 | # Fix perms
22 | echo "Fixing crontabs permissions..."
23 | chmod -R 0644 ${CRONTAB_PATH}
24 |
25 | # Create service
26 | mkdir -p /etc/services.d/cron
27 | cat > /etc/services.d/cron/run <&2 "ERROR: ANONADDY_DOMAIN must be defined"
11 | exit 1
12 | fi
13 | if [ -f "$DKIM_PRIVATE_KEY" ]; then
14 | echo >&2 "ERROR: $DKIM_PRIVATE_KEY already exists"
15 | exit 1
16 | fi
17 |
18 | mkdir -p /data/dkim
19 | echo "generating private and storing in ${DKIM_PRIVATE_KEY}"
20 | echo "generating DNS TXT record with public key and storing it in /data/dkim/${ANONADDY_DOMAIN}.txt"
21 | echo ""
22 | rspamadm dkim_keygen -s "${ANONADDY_DKIM_SELECTOR}" -b 2048 -d "${ANONADDY_DOMAIN}" -k "${DKIM_PRIVATE_KEY}" | tee -a "/data/dkim/${ANONADDY_DOMAIN}.txt"
23 | chown -R anonaddy:anonaddy /data/dkim
24 |
--------------------------------------------------------------------------------
/test/.env:
--------------------------------------------------------------------------------
1 | MYSQL_DATABASE=addy
2 | MYSQL_USER=addy
3 | MYSQL_PASSWORD=addy
4 |
--------------------------------------------------------------------------------
/test/addy.env:
--------------------------------------------------------------------------------
1 | TZ=Europe/Paris
2 | PUID=1000
3 | PGID=1000
4 |
5 | MEMORY_LIMIT=256M
6 | UPLOAD_MAX_SIZE=16M
7 | OPCACHE_MEM_SIZE=128
8 | REAL_IP_FROM=0.0.0.0/32
9 | REAL_IP_HEADER=X-Forwarded-For
10 | LOG_IP_VAR=remote_addr
11 |
12 | APP_KEY=base64:Gh8/RWtNfXTmB09pj6iEflt/L6oqDf9ZxXIh4I9MS7A=
13 | APP_DEBUG=false
14 | APP_URL=http://127.0.0.1:8000
15 |
16 | ANONADDY_RETURN_PATH=bounces@example.com
17 | ANONADDY_ADMIN_USERNAME=addy
18 | ANONADDY_ENABLE_REGISTRATION=true
19 | ANONADDY_DOMAIN=example.com
20 | ANONADDY_ALL_DOMAINS=example.com
21 | ANONADDY_HOSTNAME=mail.example.com
22 | ANONADDY_DNS_RESOLVER=127.0.0.1
23 | ANONADDY_SECRET=0123456789abcdefghijklmnopqrstuvwxyz
24 | ANONADDY_LIMIT=200
25 | ANONADDY_BANDWIDTH_LIMIT=104857600
26 | ANONADDY_NEW_ALIAS_LIMIT=10
27 | ANONADDY_ADDITIONAL_USERNAME_LIMIT=3
28 |
29 | MAIL_FROM_NAME=addy.io
30 | MAIL_FROM_ADDRESS=addy@example.com
31 |
32 | POSTFIX_DEBUG=false
33 | POSTFIX_SMTPD_TLS=false
34 | POSTFIX_SMTP_TLS=false
35 |
36 | RSPAMD_ENABLE=true
37 |
--------------------------------------------------------------------------------
/test/compose.yml:
--------------------------------------------------------------------------------
1 | name: addy
2 |
3 | services:
4 | db:
5 | image: mariadb:10
6 | container_name: addy_db
7 | networks:
8 | - addy
9 | command:
10 | - "mysqld"
11 | - "--character-set-server=utf8mb4"
12 | - "--collation-server=utf8mb4_unicode_ci"
13 | volumes:
14 | - "db:/var/lib/mysql"
15 | environment:
16 | - "MARIADB_RANDOM_ROOT_PASSWORD=yes"
17 | - "MYSQL_DATABASE"
18 | - "MYSQL_USER"
19 | - "MYSQL_PASSWORD"
20 | restart: always
21 |
22 | redis:
23 | image: redis:4.0-alpine
24 | container_name: addy_redis
25 | networks:
26 | - addy
27 | restart: always
28 |
29 | addy:
30 | image: ${ANONADDY_IMAGE:-anonaddy/anonaddy}
31 | container_name: ${ANONADDY_CONTAINER:-addy}
32 | depends_on:
33 | - db
34 | - redis
35 | networks:
36 | - addy
37 | ports:
38 | - target: 25
39 | published: 25
40 | protocol: tcp
41 | - target: 8000
42 | published: 8000
43 | protocol: tcp
44 | volumes:
45 | - "addy:/data"
46 | env_file:
47 | - "./addy.env"
48 | environment:
49 | - "DB_HOST=db"
50 | - "DB_DATABASE=${MYSQL_DATABASE}"
51 | - "DB_USERNAME=${MYSQL_USER}"
52 | - "DB_PASSWORD=${MYSQL_PASSWORD}"
53 | - "REDIS_HOST=redis"
54 | restart: always
55 |
56 | gen-dkim:
57 | image: ${ANONADDY_IMAGE:-anonaddy/anonaddy}
58 | profiles: ["bootstrap"]
59 | entrypoint: ''
60 | command: gen-dkim
61 | volumes:
62 | - "addy:/data"
63 | env_file:
64 | - "./addy.env"
65 | restart: no
66 |
67 | volumes:
68 | db:
69 | addy:
70 |
71 | networks:
72 | addy:
73 | name: addy
74 |
--------------------------------------------------------------------------------