├── .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/badge/resolution/anonaddy/docker.svg)](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 | Latest Version 5 | Build Status 6 | Docker Stars 7 | Docker Pulls 8 |
Become a sponsor 9 | Donate Paypal 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 | --------------------------------------------------------------------------------