├── .github ├── dependabot.yml └── workflows │ └── image-build.yml ├── Dockerfile ├── README.md ├── entrypoint.sh └── examples ├── docker-stack └── docker-compose.yaml ├── imapfilter-config ├── daemon.lua ├── daemon_mail1.lua └── lib.lua └── k8s └── deployment.yaml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | 4 | updates: 5 | 6 | - package-ecosystem: github-actions 7 | directory: / 8 | schedule: 9 | interval: weekly 10 | 11 | - package-ecosystem: docker 12 | directory: / 13 | schedule: 14 | interval: "weekly" 15 | -------------------------------------------------------------------------------- /.github/workflows/image-build.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: build 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | pull_request: 9 | branches: 10 | - main 11 | schedule: 12 | - cron: '0 3 * * *' 13 | workflow_dispatch: 14 | 15 | jobs: 16 | grab-imapfilter-latest: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - id: imapfilter-latest-tag 20 | run: | 21 | # Can't find an endpoint that returns the latest tag - only 22 | # the latest release. And since lefcha doesn't use GitHub's 23 | # release feature the tags must be used. 24 | tag="$(curl --location --silent https://api.github.com/repos/lefcha/imapfilter/tags | jq -r '.[].name' | head -1)" 25 | echo "::set-output name=tag::$tag" 26 | outputs: 27 | tag: ${{ steps.imapfilter-latest-tag.outputs.tag }} 28 | 29 | build: 30 | needs: grab-imapfilter-latest 31 | runs-on: ubuntu-latest 32 | 33 | strategy: 34 | matrix: 35 | include: 36 | - flavor: | 37 | latest=false 38 | suffix= 39 | args: 40 | imapfilter_spec=master 41 | - flavor: | 42 | latest=false 43 | suffix=-tag 44 | args: | 45 | imapfilter_spec=${{ needs.grab-imapfilter-latest.outputs.tag }} 46 | 47 | steps: 48 | - uses: actions/checkout@v4 49 | - uses: docker/setup-qemu-action@v3 50 | with: 51 | # Spurious segfaults when compiling 52 | # https://github.com/docker/setup-qemu-action/issues/188 53 | image: tonistiigi/binfmt:qemu-v8.1.5 54 | - uses: docker/setup-buildx-action@v3 55 | 56 | - uses: docker/metadata-action@v5 57 | id: meta 58 | with: 59 | flavor: ${{ matrix.flavor }} 60 | images: | 61 | name=ntnn/imapfilter,enable=${{ github.repository == 'ntnn/docker-imapfilter' && github.ref == 'refs/heads/main' }} 62 | name=ghcr.io/${{ github.repository }},enable=true 63 | tags: | 64 | type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} 65 | type=raw,value=main,enable=${{ github.ref == 'refs/heads/main' }} 66 | type=ref,event=branch 67 | type=ref,event=pr 68 | type=sha 69 | 70 | - name: Login to GitHub Container Registry 71 | uses: docker/login-action@v3 72 | with: 73 | registry: ghcr.io 74 | username: ${{ github.actor }} 75 | password: ${{ secrets.GITHUB_TOKEN }} 76 | 77 | - name: Login to Docker Hub 78 | uses: docker/login-action@v3 79 | if: ${{ github.repository == 'ntnn/docker-imapfilter' && github.ref == 'refs/heads/main' }} 80 | with: 81 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 82 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 83 | 84 | - uses: docker/build-push-action@v6 85 | with: 86 | tags: ${{ steps.meta.outputs.tags }} 87 | push: true 88 | platforms: linux/amd64,linux/arm64,linux/arm/v7 89 | build-args: ${{ matrix.args }} 90 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine as builder 2 | 3 | # imapfilter_spec can be a specific commit or a version tag 4 | ARG imapfilter_spec=master 5 | 6 | # Original from simbelmas: 7 | # https://github.com/simbelmas/dockerfiles/tree/master/imapfilter 8 | 9 | RUN apk --no-cache add lua openssl pcre git \ 10 | && apk --no-cache add -t dev_tools lua-dev openssl-dev make gcc libc-dev pcre-dev pcre2-dev \ 11 | && git clone https://github.com/lefcha/imapfilter.git /imapfilter_build \ 12 | && cd /imapfilter_build \ 13 | && git checkout "${imapfilter_spec}" \ 14 | && make && make install 15 | 16 | FROM alpine 17 | 18 | COPY --from=builder /usr/local/bin/imapfilter /usr/local/bin/imapfilter 19 | COPY --from=builder /usr/local/share/imapfilter /usr/local/share/imapfilter 20 | COPY --from=builder /usr/local/man /usr/local/man 21 | 22 | COPY entrypoint.sh /entrypoint.sh 23 | RUN chmod a+x /entrypoint.sh && apk --no-cache add lua lua-dev openssl pcre git \ 24 | && adduser -D -u 1001 imapfilter \ 25 | && mkdir /opt/imapfilter \ 26 | && chown imapfilter: /opt/imapfilter 27 | 28 | USER imapfilter 29 | # create an empty config.lua to prevent an error when running imapfilter directly 30 | RUN mkdir -p /home/imapfilter/.imapfilter && touch /home/imapfilter/.imapfilter/config.lua 31 | 32 | ENTRYPOINT /entrypoint.sh 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imapfilter 2 | 3 | Docker image to run imapfilter as a daemon: [ntnn/imapfilter](https://hub.docker.com/r/ntnn/imapfilter) 4 | 5 | Can also be used to access an up-to-date imapfilter docker image: 6 | 7 | ```bash 8 | > docker run -it --rm --entrypoint imapfilter ntnn/imapfilter -V 9 | IMAPFilter 2.8.1 Copyright (c) 2001-2023 Eleftherios Chatzimparmpas 10 | ``` 11 | 12 | The intended way to use this image is to have your imapfilter 13 | configuration in a git repo, which will then be pulled in the 14 | entrypoint. 15 | 16 | ## Image tags 17 | 18 | The repository builds two versions of the image: 19 | 20 | 1. The `latest`/`main` tagged version, which is always build from the 21 | main branches of both [lefcha/imapfilter][imapfilter] and this 22 | repository. 23 | 24 | 2. The `latest-tag`/`vX.Y.Z` tagged version, which is always build from 25 | the main branch of this repository and the latest tag of the 26 | [lefcha/imapfilter][imapfilter] repository. 27 | 28 | ## Examples 29 | 30 | See the examples in the `examples` directory. 31 | 32 | The `imapfilter-config` directory contains an example imapfilter 33 | configuration. 34 | 35 | The `docker-stack` directory contains an arguably dated example for 36 | a docker stack. 37 | 38 | The `k8s` directory contains an example for a kubernetes deployment. 39 | 40 | Both `docker-stack` and `k8s` expect the configuration from 41 | `imapfilter-config`. 42 | 43 | ## Environment variables 44 | 45 | | Environment variable | Type | Description | 46 | | --- | --- | --- | 47 | | `GIT_USER` | string | Username for git | 48 | | `GIT_TOKEN` | string | Path to the file containing the secret for the `GIT_USER` | 49 | | `GIT_TOKEN_RAW` | string | The raw `GIT_TOKEN` to use | 50 | | `GIT_TARGET` | string | Git URI for the imapfilter config repo | 51 | | `IMAPFILTER_CONFIG` | string | The path of the imapfilter config relative to `IMAPFILTER_CONFIG_BASE` | 52 | | `IMAPFILTER_CONFIG_BASE` | string | If config is not git-based path to base of mounted config | 53 | | `IMAPFILTER_LOGFILE` | string | Optional; file name and full path to write log files to | 54 | | `IMAPFILTER_DAEMON` | string | If the imapfilter config is daemonized or not | 55 | | `IMAPFILTER_SLEEP` | integer | How many seconds the entrypoint should sleep between checking the git config for updated or run imapfilter | 56 | 57 | [imapfilter]: https://github.com/lefcha/imapfilter 58 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | vcs_token() { 4 | if [ -n "$GIT_TOKEN_RAW" ]; then 5 | echo "$GIT_TOKEN_RAW" 6 | return 7 | fi 8 | 9 | if [ -n "$GIT_TOKEN" ]; then 10 | cat "${GIT_TOKEN}" 11 | fi 12 | 13 | return 1 14 | } 15 | 16 | vcs_uri() { 17 | s="https://" 18 | if [ -n "$GIT_USER" ]; then 19 | # https://user: 20 | s="${s}${GIT_USER}:" 21 | fi 22 | 23 | # https://user:token@" 24 | token="$(vcs_token)" 25 | if [ -n "$token" ]; then 26 | s="${s}${token}@" 27 | fi 28 | 29 | # https://user:token@target 30 | echo "${s}${GIT_TARGET}" 31 | } 32 | 33 | config_in_vcs() { 34 | [ -n "$(vcs_token)" ] && [ -n "$GIT_TARGET" ] 35 | } 36 | 37 | config_target_base="${IMAPFILTER_CONFIG_BASE:-/opt/imapfilter/config}" 38 | config_target="${IMAPFILTER_CONFIG}" 39 | 40 | # If config_target is an absolute path strip the base. 41 | # Originally IMAPFILTER_CONFIG was allowed to be absolute and relative, 42 | # this handles the former absolute path (as long as 43 | # IMAPFILTER_CONFIG_BASE is correctly used). 44 | case "$config_target" in 45 | (/*) config_target="${config_target#${config_target_base}/}";; 46 | esac 47 | 48 | pull_config() { 49 | config_in_vcs || return 50 | 51 | printf ">>> Updating config\n" 52 | if [ ! -d "$config_target_base" ]; then 53 | printf ">>> Config has not been cloned yet, cloning\n" 54 | mkdir -p "$config_target_base" 55 | git clone "$(vcs_uri)" "$config_target_base" 56 | return 57 | else 58 | cd "$config_target_base" 59 | printf ">>> Pulling config\n" 60 | git remote update 61 | if [ "$(git rev-parse HEAD)" != "$(git rev-parse FETCH_HEAD)" ]; then 62 | git pull 63 | return 64 | fi 65 | cd - 66 | fi 67 | return 1 68 | } 69 | 70 | start_imapfilter() { 71 | # enter a subshell to not affect the pwd of the running process 72 | ( 73 | if ! [ -d "$config_target_base" ]; then 74 | echo "The directory '$config_target_base' does not exist, exiting" 75 | echo "Please validate IMAPFILTER_CONFIG_BASE" 76 | exit 1 77 | fi 78 | 79 | # Enter the basedir of the config. Required to allow relative 80 | # includes in the lua scripts. 81 | cd "$config_target_base" 82 | 83 | log_parameter= 84 | if [ -n "$IMAPFILTER_LOGFILE" ]; then 85 | log_parameter="-l $IMAPFILTER_LOGFILE" 86 | fi 87 | 88 | if ! [ -f "$config_target" ]; then 89 | echo "The file '$config_target' does not exist relative to '$config_target_base', exiting" 90 | echo "Please validate IMAPFILTER_CONFIG" 91 | exit 1 92 | fi 93 | 94 | imapfilter -c "$config_target" $log_parameter 95 | ) 96 | } 97 | 98 | imapfilter_pid= 99 | imapfilter_restart_daemon() { 100 | if [ -n "$imapfilter_pid" ]; then 101 | kill -TERM "$imapfilter_pid" 102 | wait "$imapfilter_pid" 103 | fi 104 | start_imapfilter & 105 | imapfilter_pid="$(jobs -p)" 106 | } 107 | 108 | loop_no_daemon() { 109 | while true; do 110 | pull_config 111 | 112 | printf ">>> Running imapfilter\n" 113 | if ! start_imapfilter; then 114 | printf ">>> imapfilter failed\n" 115 | exit 1 116 | fi 117 | 118 | printf ">>> Sleeping\n" 119 | sleep "${IMAPFILTER_SLEEP:-30}" 120 | done 121 | } 122 | 123 | loop_daemon() { 124 | imapfilter_restart_daemon 125 | while true; do 126 | if pull_config; then 127 | printf ">>> Update in VCS, restarting imapfilter daemon\n" 128 | imapfilter_restart_daemon 129 | fi 130 | 131 | printf ">>> Sleeping\n" 132 | sleep "${IMAPFILTER_SLEEP:-30}" 133 | 134 | if ! kill -0 "$imapfilter_pid" 2>/dev/null; then 135 | printf ">>> imapfilter daemon died, exiting\n" 136 | exit 1 137 | fi 138 | done 139 | } 140 | 141 | pull_config 142 | if [ "$IMAPFILTER_DAEMON" = "yes" ]; then 143 | loop_daemon 144 | else 145 | loop_no_daemon 146 | fi 147 | -------------------------------------------------------------------------------- /examples/docker-stack/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | version: '3.4' 3 | 4 | secrets: 5 | imapfilter-git_token: 6 | external: true 7 | # contains the password for the email 8 | imapfilter-: 9 | external: true 10 | 11 | services: 12 | : 13 | image: ntnn/imapfilter 14 | environment: 15 | GIT_TARGET: 16 | IMAPFILTER_CONFIG: entry_.lua 17 | IMAPFILTER_DAEMON: 'yes' 18 | GIT_USER: 19 | GIT_TOKEN: /secrets/imapfilter-token 20 | secrets: 21 | - source: imapfilter-git_token 22 | target: /secrets/imapfilter-token 23 | - source: imapfilter-: 24 | target: /secrets/imapfilter- 25 | deploy: 26 | mode: global 27 | -------------------------------------------------------------------------------- /examples/imapfilter-config/daemon.lua: -------------------------------------------------------------------------------- 1 | -- daemon.lib contains functions to be used for daemonized imapfilter 2 | -- runs. Useful if all email accounts are to be filtered in the same 3 | -- way. 4 | 5 | require "lib" 6 | 7 | function apply_rules(account) 8 | spam(account) 9 | archive(account) 10 | end 11 | 12 | function daemon(account) 13 | apply_rules(account) 14 | while true do 15 | account["INBOX"]:enter_idle() 16 | apply_rules(account) 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /examples/imapfilter-config/daemon_mail1.lua: -------------------------------------------------------------------------------- 1 | -- daemon_email1.lua is only the entrypoint for a container running 2 | -- imapfilter for the email1 account. Every additionaly email acocunt 3 | -- would need its own entry file. 4 | -- This could probably be done with a table storing the email accounts 5 | -- and a lookup from an env variable but for a handful of accounts this 6 | -- is enough. 7 | 8 | require "daemon" 9 | 10 | daemon(email1) 11 | -------------------------------------------------------------------------------- /examples/imapfilter-config/lib.lua: -------------------------------------------------------------------------------- 1 | -- lib.lua contains common functions and settings for all imapfilter 2 | -- scripts. Only useful if using imapfilter for multiple email accounts 3 | -- or to have one-off scripts to run manually 4 | 5 | options.create = false 6 | options.subscribe = true 7 | options.timeout = 120 8 | options.keeplive = 1 9 | options.wakeonany = true 10 | 11 | function spam(account, mbox) 12 | if mbox == nil then mbox = "INBOX" end 13 | 14 | log('Filtering spam ' .. mbox) 15 | account:create_mailbox('Spam') 16 | 17 | result = account[mbox]:contain_field('X-Spam_bar', '++++') + 18 | account[mbox]:contain_field('X-Spam_bar', '+++') + 19 | account[mbox]:contain_field('X-Spam_bar', '++') + 20 | account[mbox]:contain_field('X-Spam_bar', '+') 21 | result:move_messages(account['Spam']) 22 | end 23 | 24 | function archive(account, mbox) 25 | if mbox == nil then mbox = "INBOX" end 26 | log('Archiving ' .. mbox) 27 | account:create_mailbox("Archive") 28 | account[mbox]:is_older(365):move_messages(account["Archive"]) 29 | end 30 | 31 | email1 = IMAP { 32 | server = 'mail.server.one', 33 | port = 993, 34 | username = 'user1', 35 | password = os.getenv('IMAPFILTER_EMAIL1_PASS'), 36 | ssl = 'tls1.3', 37 | } 38 | -------------------------------------------------------------------------------- /examples/k8s/deployment.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | kind: Deployment 3 | apiVersion: apps/v1 4 | metadata: 5 | name: imapfilter 6 | labels: 7 | app: imapfilter 8 | spec: 9 | replicas: 1 10 | selector: 11 | matchLabels: 12 | app: imapfilter 13 | template: 14 | metadata: 15 | labels: 16 | app: imapfilter 17 | spec: 18 | containers: 19 | - name: email1 20 | image: ntnn/imapfilter:latest 21 | imagePullPolicy: Always 22 | env: 23 | - name: GIT_USER 24 | value: user1 25 | # The git repository containing the imapfilter config, 26 | # this example uses the layout from the 27 | # example/iampfilter-config directory. 28 | - name: GIT_TARGET 29 | value: vcs.example/imapfilter-config 30 | # GIT_TOKEN_RAW 31 | - name: GIT_TOKEN_RAW 32 | valueFrom: 33 | secretKeyRef: 34 | name: vcs-token 35 | key: token 36 | - name: IMAPFILTER_CONFIG 37 | value: daemon_email1.lua 38 | - name: IMAPFILTER_DAEMON 39 | value: yes 40 | envFrom: 41 | # The secret has the email account password at the key IMAPFILTER_EMAIL1_PASS. 42 | # Of course the key could be any key and instead 43 | # env.[x].valueFrom.secretKeyRef is used, as for 44 | # GIT_TOKEN_RAW. 45 | - secretRef: 46 | name: mail-mail1 47 | --------------------------------------------------------------------------------