├── .circleci └── config.yml ├── .github ├── ISSUE_TEMPLATE │ └── bug_report.md ├── dependabot.yml └── workflows │ ├── main.workflow │ ├── main.yml │ ├── publish_docs.yml │ └── stale.yml ├── .gitignore ├── LICENSE ├── NEWS.md ├── README.md ├── build.sh ├── core ├── Dockerfile ├── Dockerfile.dev ├── README.md ├── VERSION ├── assets │ ├── exim │ │ ├── 25_mm3_macros │ │ ├── 455_mm3_router │ │ └── 55_mm3_transport │ ├── mailman-hyperkitty.cfg │ └── mailman.cfg ├── docker-entrypoint.sh └── requirements.txt ├── deploy.py ├── docker-compose-mysql.yaml ├── docker-compose-postorius.yaml ├── docker-compose.yaml ├── docs ├── core.md ├── index.md ├── news.md └── web.md ├── mkdocs.yml ├── postorius ├── Dockerfile ├── Dockerfile.dev ├── docker-entrypoint.sh └── mailman-web │ ├── __init__.py │ ├── manage.py │ ├── settings.py │ ├── urls.py │ ├── uwsgi.ini │ └── wsgi.py ├── tests ├── docker-test.yaml └── test.sh └── web ├── Dockerfile ├── Dockerfile.dev ├── README.md ├── VERSION ├── docker-entrypoint.sh ├── mailman-web ├── __init__.py ├── manage.py ├── settings.py ├── urls.py ├── uwsgi.ini └── wsgi.py └── requirements.txt /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | jobs: 4 | build: 5 | parameters: 6 | rolling: 7 | type: string 8 | default: "no" 9 | machine: 10 | image: default 11 | environment: 12 | DOCKER_BUILDKIT: 1 13 | BUILDKIT_PROGRESS: plain 14 | BUILD_ROLLING: << parameters.rolling >> 15 | steps: 16 | - checkout 17 | - run: 18 | name: Install Python dependencies 19 | command: python3 -m pip install packaging 20 | - restore_cache: 21 | keys: 22 | - python-deps-cache-v1 23 | - run: 24 | name: Building Container Images 25 | command: ./build.sh << parameters.rolling >> 26 | - save_cache: 27 | key: python-deps-cache-v1 28 | paths: 29 | - /root/.cache 30 | - run: 31 | environment: 32 | DB: postgres 33 | name: Postgres Tests 34 | command: bash tests/test.sh 35 | - run: 36 | environment: 37 | DB: mysql 38 | name: MySQL Test 39 | command: bash tests/test.sh 40 | - run: 41 | name: Run version 42 | command: | 43 | python3 --version 44 | python3 deploy.py 45 | - store_artifacts: 46 | path: /opt/mailman/web/logs/ 47 | 48 | - store_artifacts: 49 | path: /opt/mailman/core/var/logs 50 | 51 | workflows: 52 | version: 2 53 | test-stable: 54 | jobs: 55 | - build: 56 | rolling: "no" 57 | filters: 58 | tags: 59 | only: /^v\d+\.\d+\.\d+$/ 60 | 61 | cron-builds: 62 | triggers: 63 | - schedule: 64 | cron: "0 0 * * *" 65 | filters: 66 | branches: 67 | only: main 68 | jobs: 69 | - build: 70 | rolling: "yes" 71 | context: org-global 72 | 73 | test-rolling: 74 | jobs: 75 | - build: 76 | rolling: "yes" 77 | context: org-global 78 | filters: 79 | branches: 80 | # Forked pull requests have CIRCLE_BRANCH set to pull/XXX 81 | ignore: /pull\/[0-9]+/ 82 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Tag/version of Container Images** 11 | Choose from: rolling, 0.3, 0.3.*, 0.4.* 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "docker" # See documentation for possible values 9 | directory: "/core" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | - package-ecosystem: "docker" # See documentation for possible values 13 | directory: "/web" # Location of package manifests 14 | schedule: 15 | interval: "weekly" 16 | - package-ecosystem: "docker" # See documentation for possible values 17 | directory: "/postorius" # Location of package manifests 18 | schedule: 19 | interval: "weekly" 20 | # Enable version updates for Actions 21 | - package-ecosystem: "github-actions" 22 | # Look for `.github/workflows` in the `root` directory 23 | directory: "/" 24 | # Check for updates once a week 25 | schedule: 26 | interval: "weekly" 27 | - package-ecosystem: "pip" 28 | directory: "/core" 29 | schedule: 30 | interval: "daily" 31 | - package-ecosystem: "pip" 32 | directory: "/web" 33 | schedule: 34 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/main.workflow: -------------------------------------------------------------------------------- 1 | workflow "Add PR to release notes" { 2 | on = "pull_request" 3 | resolves = ["Chronicler"] 4 | } 5 | 6 | action "Chronicler" { 7 | uses = "crosscompile/chronicler-action@v1.0.0" 8 | secrets = ["GITHUB_TOKEN"] 9 | } 10 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | # This is a basic workflow to help you get started with Actions 2 | 3 | name: CI 4 | 5 | # Controls when the action will run. 6 | on: 7 | # Triggers the workflow on push or pull request events but only for the master branch 8 | push: 9 | branches: [ master ] 10 | pull_request: 11 | branches: [ master ] 12 | 13 | # Allows you to run this workflow manually from the Actions tab 14 | workflow_dispatch: 15 | 16 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel 17 | jobs: 18 | # This workflow contains a single job called "build" 19 | build: 20 | # The type of runner that the job will run on 21 | runs-on: ubuntu-latest 22 | 23 | # Steps represent a sequence of tasks that will be executed as part of the job 24 | steps: 25 | - name: Chronicler Action 26 | # You may pin to the exact commit or the version. 27 | # uses: crosscompile/chronicler-action@5c25dbce26b0789724a92902c69217b46f023b51 28 | uses: crosscompile/chronicler-action@v1.0.1 29 | -------------------------------------------------------------------------------- /.github/workflows/publish_docs.yml: -------------------------------------------------------------------------------- 1 | 2 | 3 | name: Publish docs via GitHub Pages 4 | on: 5 | push: 6 | branches: 7 | - main 8 | - master 9 | 10 | jobs: 11 | build: 12 | name: Deploy docs 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout main 16 | uses: actions/checkout@v4 17 | 18 | - name: Deploy docs 19 | uses: mhausenblas/mkdocs-deploy-gh-pages@master 20 | # Or use mhausenblas/mkdocs-deploy-gh-pages@nomaterial to build without the mkdocs-material theme 21 | env: 22 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 23 | CUSTOM_DOMAIN: asynchronous.in 24 | CONFIG_FILE: mkdocs.yml 25 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | # This workflow warns and then closes issues and PRs that have had no activity for a specified amount of time. 2 | # 3 | # You can adjust the behavior by modifying this file. 4 | # For more information, see: 5 | # https://github.com/actions/stale 6 | name: Mark stale issues and pull requests 7 | 8 | on: 9 | schedule: 10 | - cron: '19 22 * * *' 11 | 12 | jobs: 13 | stale: 14 | 15 | runs-on: ubuntu-latest 16 | permissions: 17 | issues: write 18 | pull-requests: write 19 | 20 | steps: 21 | - uses: actions/stale@v9 22 | with: 23 | repo-token: ${{ secrets.GITHUB_TOKEN }} 24 | stale-issue-message: 'This issue has not been updated for more than 1year' 25 | stale-pr-message: 'This Pull Request has not been updated for more than 1year' 26 | stale-issue-label: 'no-issue-activity' 27 | stale-pr-label: 'no-pr-activity' 28 | days-before-stale: 365 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | */.sass-cache/* 2 | *.log 3 | *.log 4 | *.sass-cache/ 5 | /web/mailman-web/settings_local.py 6 | pythonenv3.8/* 7 | .venv/* 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Abhilash Raj 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 | -------------------------------------------------------------------------------- /NEWS.md: -------------------------------------------------------------------------------- 1 | # NEWS 2 | 3 | ## Upgrading to 0.4.0 Release 4 | 5 | Some configurations in the release are backwards incompatible with 6 | what was working before. 7 | 8 | ### Web server configuration 9 | 10 | With 0.4.0 version, we added Port mapping from host's Port 8000/8080 11 | to mailman-web container's port 8000/8080. Make sure you have this 12 | in your docker-compose.yaml 13 | 14 | ```yaml 15 | mailman-web: 16 | ports: 17 | - "127.0.0.1:8000:8000" # HTTP 18 | - "127.0.0.1:8080:8080" # uwsgi 19 | ``` 20 | 21 | You should update your web server to proxy 127.0.0.1:8000. 22 | 23 | #### Nginx 24 | 25 | Update the Nginx configuration to look like this, notice the 26 | actual **URL for `proxy_pass` is the only thing that has changed** 27 | along with some options like `uwsgi_read_timeout` and `include uwsgi_params` 28 | that have been removed from the previous version. 29 | 30 | ``` 31 | location / { 32 | proxy_pass http://127.0.0.1:8000; 33 | proxy_set_header Host $host; 34 | proxy_set_header X-Forwarded-For $remote_addr; 35 | } 36 | ``` 37 | 38 | For other web servers like Apache2, update the URL accordingly. 39 | 40 | **Note** that if you are using `uwsgi_pass` instead of `proxy_pass` 41 | then you should update the URL accordingly to `https://127.0.0.1:8080`. 42 | 43 | ### MTA configuration 44 | 45 | MTA configuration needs updating to ensure that all IPs from the 46 | `172.19.199.0/24` subnet is added to `mynetworks` in Postfix configs. 47 | 48 | Please verify that the network configuration generated by the containers 49 | look like this: 50 | 51 | ```bash 52 | $ docker exec mailman-core cat /etc/mailman.cfg 53 | # This file is autogenerated at container startup. 54 | [database] 55 | class: mailman.database.postgresql.PostgreSQLDatabase 56 | url: postgres://mailman:mailmanpass@database/mailmandb 57 | [runner.retry] 58 | sleep_time: 10s 59 | 60 | [webservice] 61 | hostname: 172.19.199.3 62 | port: 8001 63 | admin_user: restadmin 64 | admin_pass: restpass 65 | configuration: /etc/gunicorn.cfg 66 | 67 | [mta] 68 | incoming: mailman.mta.postfix.LMTP 69 | outgoing: mailman.mta.deliver.deliver 70 | lmtp_host: 172.19.199.3 71 | lmtp_port: 8024 72 | smtp_host: 172.19.199.1 73 | smtp_port: 25 74 | configuration: /etc/postfix-mailman.cfg 75 | 76 | [archiver.hyperkitty] 77 | class: mailman_hyperkitty.Archiver 78 | enable: yes 79 | configuration: /etc/mailman-hyperkitty.cfg 80 | ``` 81 | 82 | **Note that lmtp_host and webserver hostname can be different than 83 | before since new containers don't have static IP addresses. They 84 | are automatically parsed from the output of "ip route" command 85 | from inside mailman-core container.** 86 | 87 | You can verify that the IP address of the containers by running the 88 | following commands, note that the **output can be different** and it is 89 | fine if that is the case. 90 | 91 | ```bash 92 | $ docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mailman-core 93 | 172.19.199.3 94 | $ docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mailman-web 95 | 172.19.199.4 96 | ``` 97 | 98 | ---- 99 | ## Mailman Core 100 | 101 | ### v1.1.1 (released Aug 9 2017) 102 | 103 | - The MM_HOSTNAME now defaults to output of `hostname -i` instead of `mailman-core`. This 104 | is the hostname Core binds to for Webservice. 105 | - Added pymysql to the image to use MySQL as database. 106 | - The default settings for using SQLITE are now more sane. 107 | - Postfix's transport maps are generated at the container startup now even when 108 | there is no lists exist. 109 | 110 | 111 | ## Mailman Web 112 | 113 | ### v1.1.1 (released Aug 9 2017) 114 | 115 | - The default search_index for whoosh now exists on persistent storage at 116 | `/opt/mailman-web-data` 117 | - Move to using Alpine instead of Debian for this image, python2.7:alpine-3.6 118 | image is now the base image 119 | - Django compressor is now using `sassc` from alpine repo. 120 | - Default value of SECRET_KEY is now removed. It is MUST to set SECRET_KEY 121 | environment variable to run this image now. 122 | - If a SERVE_FROM_DOMAIN environment variable is defined, the default Django's 123 | example.com site is renamed to this domain. The SITE_ID remains same so there 124 | is no change required to serve this domain. 125 | - If MAILMAN_ADMIN_USER and MAILMAN_ADMIN_EMAIL environment variables are 126 | defined a Django Superuser is created by default. The password for this user 127 | would have to be reset on the first login. 128 | - Fix cron configuration which would run them in wrong order. 129 | - Removed facebook as default social auth provider in the settings.py 130 | - Uwsgi now listens on port 8080 for uwsgi protocol and 8000 for http protocol. 131 | - Threads are enabled by default in the uwsgi configuration now. 132 | - Hyperkitty updated to v1.1.1 133 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: / 3 | --- 4 | 5 | 6 | # GNU Mailman 3 Deployment with Docker 7 | 8 | 9 | 10 | [![CircleCI](https://circleci.com/gh/maxking/docker-mailman/tree/main.svg?style=svg)](https://circleci.com/gh/maxking/docker-mailman/tree/main) 11 | 12 | This repository hosts code for two docker images `maxking/mailman-core` and 13 | `maxking/mailman-web` both of which are meant to deploy [GNU Mailman 3][1] in 14 | a production environment. 15 | 16 | [Docker][2] is a container ecosystem which can run containers on several 17 | platforms. It consists of a tool called [docker-compose][3] which can be used to 18 | run multi-container applications. This repository consists of a 19 | [`docker-compose.yaml`][19] file which is a set of 20 | configurations that can be used to deploy the [Mailman 3 Suite][4]. 21 | 22 | Please see [release page](https://github.com/maxking/docker-mailman/releases) 23 | for the releases and change log. 24 | 25 | ## Release 26 | 27 | 28 | The tags for the images are assumed to be release versions for images. This is 29 | going to be a somewhat common philosophy of distributing Container images where 30 | the images with same tags are usually updated with the new functionality. 31 | 32 | Releases will follow the following rules: 33 | 34 | * Images tagged like A.B.C will never change. If you want to pin down versions 35 | of Images, use these tags. 36 | 37 | * Images tagged with A.B will correspond to the latest A.B.C version 38 | released. Releases in A.B series are supposed to be backwards compatible, 39 | i.e., any existing installation should not break when upgrading between 40 | subversions of A.B.C. So, if you want the latest updates and want to 41 | frequently update your installation without having to change the version 42 | numbers, you can use this. 43 | 44 | * Any changes in the minor version of Mailman components of the images will 45 | cause a bump in the minor version, e.g., A.(B+1) can have one or more 46 | updated Mailman components from A.B. Also, significant change in functionality, 47 | that might change how Images work or how people interact with the containers 48 | can also cause a bump in the minor version. 49 | 50 | * Major versions will change either when there are backwards incompatible 51 | changes or when the releases reach a certain set milestone or when there are 52 | bugfix releases for the internal components or both. 53 | 54 | 55 | ## Container Registries 56 | 57 | The container images are available from multiple container registries. Do specify an [explicit version tag](https://hub.docker.com/r/maxking/mailman-web/tags?page=1&ordering=last_updated&name=0.) (e.g. `0.4.5` , MAJOR.MINOR like `0.4` also works as floating tag pointing to latest patch version) as tag `latest` is **not** updated anymore. 58 | 59 | ### Mailman Core 60 | 61 | - `ghcr.io/maxking/mailman-core` 62 | - `docker.io/maxking/mailman-core` 63 | 64 | ### Mailman Web 65 | 66 | - `ghcr.io/maxking/mailman-web` 67 | - `docker.io/maxking/mailman-web` 68 | 69 | ### Postorius 70 | 71 | - `ghcr.io/maxking/postorius` 72 | - `docker.io/maxking/postorius` 73 | 74 | ## Rolling Releases 75 | 76 | Rolling releases are made up of Mailman Components installed from [git 77 | source](https://gitlab.com/mailman). **Note that these releases are made up of 78 | un-released software and should be assumed to be beta quality.** 79 | 80 | Every commit is tested with Mailman's CI infrastructure and is included in 81 | rolling releases only if they have passed the complete test suite. 82 | 83 | ```bash 84 | $ docker pull docker.io/maxking/mailman-web:rolling 85 | $ docker pull docker.io/maxking/mailman-core:rolling 86 | ``` 87 | 88 | Rolling releases are built with every commit and also re-generated nightly. You 89 | can inspect the images to get which commit it was built using: 90 | 91 | ```bash 92 | $ docker inspect --format '{{json .Config.Labels }}' mailman-core | python -m json.tool 93 | { 94 | "version.git_commit": "45a4d7805b2b3d0e7c51679f59682d64ba02f05f", 95 | } 96 | 97 | $ docker inspect --format '{{json .Config.Labels }}' mailman-web | python -m json.tool 98 | { 99 | "version.git_commit": "45a4d7805b2b3d0e7c51679f59682d64ba02f05f", 100 | } 101 | 102 | ``` 103 | 104 | - `version.git_commit` : This is the commit hash of the Dockerfile in the 105 | [Github repo](https://github.com/maxking/docker-mailman) 106 | 107 | ## Dependencies 108 | 109 | - Docker 110 | - Docker-compose-plugin 111 | 112 | To install these on Ubuntu/Debian: 113 | 114 | ``` 115 | $ sudo apt install docker.io docker-compose-plugin 116 | ``` 117 | 118 | For other systems, you can read the official Docker documentation to install 119 | [Docker from here][5] and [docker compose from here][6]. 120 | 121 | 122 | ## Configuration 123 | 124 | Most of the common configuration is handled through environment variables in the 125 | `docker-compose.yaml`. However, there is need for some extra configuration that 126 | interacts directly with the application. There are two configuration files on 127 | the host that interact directly with Mailman's settings. These files exist on 128 | the host running the containers and are imported at runtime in the containers. 129 | 130 | * `/opt/mailman/core/mailman-extra.cfg` : This is the configuration for Mailman 131 | Core and anything that you add here will be added to Core's configuration. You 132 | need to restart your mailman-core container for the changes in this file to 133 | take effect. 134 | 135 | * `/opt/mailman/web/settings_local.py` : This is the Django configuration that 136 | is imported by the [existing configuration][2] 137 | provided by the mailman-web container. **This file is referred to as 138 | `settings.py` in most of the Postorius and Django documentation.** To change 139 | or override any settings in Django/Postorius, you need to create/edit this file. 140 | A useful configuration for troubleshooting is `DEBUG = True`. 141 | 142 | [2]: https://github.com/maxking/docker-mailman/blob/master/web/mailman-web/settings.py 143 | 144 | Also, note that if you need any other files to be accessible from the host to 145 | inside the container, you can place them at certain directories which are 146 | mounted inside the containers. 147 | 148 | 149 | * `/opt/mailman/core` in host maps to `/opt/mailman/` in mailman-core container. 150 | * `/opt/mailman/web` in host maps to `/opt/mailman-web-data` in mailman-web 151 | container. 152 | 153 | ### Mailman-web 154 | These are the settings that you MUST change in your docker-compose.yaml before deploying: 155 | 156 | - `SERVE_FROM_DOMAIN`: The domain name from which Django will be served. To be 157 | added to `ALLOWED_HOSTS` in django settings. Default value is not set. This 158 | also replaces Django's default `example.com` SITE and becomes the default SITE 159 | (with SITE_ID=1). 160 | 161 | - `HYPERKITTY_API_KEY`: Hyperkitty's API Key, should be set to the same value 162 | as set for the mailman-core. (Not needed in case of Postorius-only version.) 163 | 164 | - `MAILMAN_ADMIN_USER`: The username for the admin user to be created by default. 165 | 166 | - `MAILMAN_ADMIN_EMAIL`: The email for the admin user to be created by default. 167 | 168 | - `SECRET_KEY`: Django's secret key, mainly used for signing cookies and others. 169 | 170 | Please note here that if you choose to create the admin user using the 171 | environment variables mentioned above (`MAILMAN_ADMIN_USER` & 172 | `MAILMAN_ADMIN_EMAIL`), no password is set for your admin account. To set a 173 | password, plese follow the "Forgot Password" link on the "Sign In" page. 174 | 175 | Mailman web is already configured to send emails through `$SMTP_HOST` as the 176 | MTA's address. If you want to modify it, you can set the value in under 177 | docker-compose.yaml for mailman-web container. By default, `SMTP_HOST` points 178 | to the gateway of the web container, which is the host itself. 179 | 180 | You can also use the environment variables `SMTP_HOST` (defaults to 181 | the container's gateway), `SMTP_PORT` (defaults to `25`), `SMTP_HOST_USER` (defaults to 182 | an empty string), `SMTP_HOST_PASSWORD` (defaults to an empty string), 183 | `SMTP_USE_TLS` (defaults to `False`) and `SMTP_USE_SSL` (defaults to `False`). 184 | 185 | This is required in addition to the [Setup your MTA](#setting-up-your-mta) 186 | section below, which covers email setup for Mailman Core. 187 | 188 | For more details on how to configure this image, please look at 189 | [Mailman-web's Readme](web/) 190 | 191 | ### Mailman-Core 192 | 193 | These are the variables that you MUST change in your docker-compose.yaml before deploying: 194 | 195 | - `HYPERKITTY_API_KEY`: Hyperkitty's API Key, should be set to the same value as 196 | set for the mailman-web. Skip the variable in case of non-Hyperkitty deployment. 197 | 198 | - `DATABASE_URL`: URL of the type 199 | `driver://user:password@hostname:port/databasename` for the django to use. If 200 | not set, the default is set to 201 | `sqlite:///opt/mailman-web-data/mailmanweb.db`. The standard 202 | docker-compose.yaml comes with it set to a postgres database. There is no need 203 | to change this if you are happy with PostgreSQL. 204 | 205 | - `DATABASE_TYPE`: Its value can be one of `sqlite`, `postgres` or `mysql` as 206 | these are the only three database types that Mailman 3 supports. Its default 207 | value is set to `sqlite` along with the default database class and default 208 | database url above. 209 | 210 | - `DATABASE_CLASS`: Default value is 211 | `mailman.database.sqlite.SQLiteDatabase`. The values for this can be found in 212 | the mailman's documentation [here][11]. 213 | - `SMTP_HOST` : outgoing host for SMTP connections 214 | - `SMTP_PORT` : use this port. 25, 587, whatever your host asks for. 215 | - `SMTP_HOST_USER`: authenticate this user 216 | - `SMTP_HOST_PASSWORD`: and use this password 217 | - `SMTP_SECURE_MODE`: security mode for smtp connection - can be `smtp` (no encryption), `smtps` or `starttls` 218 | - `SMTP_VERIFY_HOSTNAME`: defaults to `true` - verify, that certificate hostname is identical to `SMTP_HOST` 219 | - `SMTP_VERIFY_CERT`: defaults to `true` - verify, that certificate is valid 220 | 221 | For more details on how to configure this image, please look [Mailman-core's 222 | Readme](core/) 223 | 224 | 225 | While the above configuration will allow you to run the images and possibly view 226 | the Web Frontend, it won't be functional until it is fully configured to to send 227 | emails. 228 | 229 | To configure the mailman-core container to send emails, see the [Setting your MTA 230 | section below](#setting-up-your-mta). 231 | 232 | ## Running 233 | 234 | To run the containers, simply run: 235 | 236 | ```bash 237 | $ mkdir -p /opt/mailman/core 238 | $ mkdir -p /opt/mailman/web 239 | $ git clone https://github.com/maxking/docker-mailman 240 | $ cd docker-mailman 241 | # Change some configuration variables as mentioned above. 242 | $ docker compose up -d 243 | ``` 244 | 245 | Note that the web frontend in the mailman-web container is, by default, only 246 | configured to serve dynamic content. Anything static like stylesheets, etc., is 247 | expected to be served directly by the web server. The static content exists at 248 | `/opt/mailman/web/static` and should be _aliased_ to `/static/` in the web 249 | server configuration. 250 | 251 | See [the nginx configuration][17] as an example. 252 | 253 | This command will do several things, most importantly: 254 | 255 | - Run a wsgi server using [`uwsgi`][7] for the Mailman's Django-based web 256 | frontend listening on port 8000. It will run 2 worker 257 | processes with 4 threads each. You may want to change the setting 258 | `ALLOWED_HOSTS` in the settings before deploying the application in 259 | production. 260 | 261 | - Run a PostgreSQL server with a default database, username, and password as 262 | mentioned in the `docker-compose.yaml`. You will have to change configuration 263 | files too if you change any of these. 264 | 265 | - Run mailman-core listening on port 8001 for REST API and port 8024 (LMTP 266 | server) for messages from your MTA. You will have to configure your MTA to 267 | send messages at this address. 268 | 269 | Some more details about what the above system achieves is mentioned below. If you 270 | are only going to deploy a simple configuration, you don't need to read 271 | this. However, these are very easy to understand if you know how docker works. 272 | 273 | - First create a bridge network called `mailman` in the 274 | `docker-compose.yaml`. It will probably be named something else in your 275 | machine. All the containers 276 | mentioned (mailman-core, mailman-web, database) will join this network and are 277 | assigned static IPs. The host operating system is the default gateway 278 | from within these containers. 279 | 280 | - Spin off a mailman-core container attached to the mailman bridge network created above. It has 281 | GNU Mailman 3 core running inside it. Mailman core's REST API is available at 282 | port 8001 and LMTP server listens at port 8024. 283 | 284 | - Spin off a mailman-web container which has a Django application running with 285 | both Mailman's web frontend Postorius and Mailman's web-based Archiver 286 | running. [Uwsgi][7] server is used to run a web server with the configuration 287 | provided in this repository [here][2]. You may want to 288 | change the setting `ALLOWED_HOSTS` in the settings before deploying the 289 | application in production. You can do that by adding a 290 | `/opt/mailman/web/settings_local.py` which is imported by the Django when 291 | running. 292 | 293 | - Spin off a PostgreSQL database container which is used by both mailman-core 294 | and mailman-web as their primary database. 295 | 296 | - mailman-core mounts `/opt/mailman/core` from host OS at `/opt/mailman` in the 297 | container. Mailman's var directory is stored there so that it is accessible 298 | from the host operating system. Configuration for Mailman core is generated on 299 | every run from the environment variables provided. Extra configuration can 300 | also be provided at `/opt/mailman/core/mailman-extra.cfg` (on host), and will 301 | be added to generated configuration file. Mailman also needs another 302 | configuration file called 303 | [mailman-hyperkitty.cfg][3] and is also 304 | expected to be at `/opt/mailman/core/` on the host OS. 305 | 306 | [3]: https://github.com/maxking/docker-mailman/blob/master/core/assets/mailman-hyperkitty.cfg 307 | 308 | - mailman-web mounts `/opt/mailman/web` from the host OS to 309 | `/opt/mailman-web-data` in the container. It consists of the logs and 310 | settings_local.py file for Django. 311 | 312 | - database mounts `/opt/mailman/database` at `/var/lib/postgresql/data` so that 313 | PostgreSQL can persist its data even if the database containers are 314 | updated/changed/removed. 315 | 316 | ## Setting up your MTA 317 | 318 | The provided docker containers do not have an MTA in-built. You can either run 319 | your own MTA inside a container and have them relay emails to the mailman-core 320 | container or just install an MTA on the host and have them relay emails. 321 | 322 | ### Exim4 323 | 324 | To use [Exim4][8], it should be setup to relay emails from mailman-core and 325 | mailman-web. The mailman specific configuration is provided in the 326 | repository at `core/assets/exim`. There are three files 327 | 328 | - [25_mm3_macros](core/assets/exim/25_mm3_macros) to be placed at 329 | `/etc/exim4/conf.d/main/25_mm3_macros` in a typical Debian install of 330 | exim4. Please change MY_DOMAIN_NAME to the domain name that will be used to 331 | serve mailman. Multi-domain setups will be added later. 332 | 333 | - [455_mm3_router](core/assets/exim/455_mm3_router) to be placed at 334 | `/etc/exim4/conf.d/router/455_mm3_router` in a typical Debian install of exim4. 335 | 336 | - [55_mm3_transport](core/assets/exim/55_mm3_transport) to be placed at 337 | `/etc/exim4/conf.d/transport/55_mm3_transport` in a typical Debian install of exim4. 338 | 339 | 340 | Also, the default configuration inside the mailman-core image has the MTA set 341 | to Exim, but just for reference, it looks like this: 342 | 343 | ``` 344 | # mailman.cfg 345 | [mta] 346 | incoming: mailman.mta.exim4.LMTP 347 | outgoing: mailman.mta.deliver.deliver 348 | lmtp_host: $MM_HOSTNAME 349 | lmtp_port: 8024 350 | smtp_host: $SMTP_HOST 351 | smtp_port: $SMTP_PORT 352 | configuration: python:mailman.config.exim4 353 | ``` 354 | 355 | ### Postfix 356 | 357 | To use [Postfix][12], edit the `main.cf` configuration file, which is typically 358 | at `/etc/postfix/main.cf` on Debian-based operating systems. Add 359 | mailman-core and mailman-web to `mynetworks` so it will relay emails from 360 | the containers and add the following configuration lines: 361 | 362 | ``` 363 | # main.cf 364 | 365 | # Support the default VERP delimiter. 366 | recipient_delimiter = + 367 | unknown_local_recipient_reject_code = 550 368 | owner_request_special = no 369 | 370 | transport_maps = 371 | regexp:/opt/mailman/core/var/data/postfix_lmtp 372 | local_recipient_maps = 373 | regexp:/opt/mailman/core/var/data/postfix_lmtp 374 | relay_domains = 375 | regexp:/opt/mailman/core/var/data/postfix_domains 376 | ``` 377 | 378 | To configure Mailman to use Postfix, add `MTA=postfix` under mailman-core's 379 | environment section in the `docker-compose.yaml`: 380 | 381 | ``` 382 | mailman-core: 383 | 384 | environment: 385 | - MTA=postfix 386 | ``` 387 | 388 | This will auto-generate the configuration to talk to Postfix assuming that 389 | Postfix is available at the gateway address for the container's bridge network 390 | at port 25. The final configuration can be found by executing: 391 | 392 | ``` 393 | $ docker exec mailman-core cat /etc/mailman.cfg 394 | ``` 395 | 396 | The postfix configuration that is generated looks like this: 397 | ``` 398 | [mta] 399 | incoming: mailman.mta.postfix.LMTP 400 | outgoing: mailman.mta.deliver.deliver 401 | lmtp_host: $MM_HOSTNAME 402 | lmtp_port: 8024 403 | smtp_host: $SMTP_HOST 404 | smtp_port: $SMTP_PORT 405 | configuration: /etc/postfix-mailman.cfg 406 | ``` 407 | 408 | So, if you need to update the values, you can set `SMTP_HOST`, `SMTP_PORT`, 409 | `MM_HOSTNAME` environment variables in `mailman-core` container. 410 | 411 | Please verify the output for `[mta]` section to ensure that it points to 412 | the right `smtp_host` (address to reach postfix from mailman-core container) 413 | and `lmtp_host` (address to reach mailman-core container from postfix). 414 | 415 | The configuration file `/etc/postfix-mailman.cfg` is also generated automatically 416 | inside the `mailman-core` container and contains the configuration specific 417 | for Postfix. 418 | 419 | ## Site Owner 420 | 421 | Setup site owner address. By default, mailman is setup with the site_owner set to 'changeme@example.com'. This should be pointing to a valid mailbox. Add the following to the '/opt/mailman/core/mailman-extra.cfg'. 422 | 423 | ``` 424 | [mailman] 425 | # This address is the "site owner" address. Certain messages which must be 426 | # delivered to a human, but which can't be delivered to a list owner (e.g. a 427 | # bounce from a list owner), will be sent to this address. It should point to 428 | # a human. 429 | site_owner: changeme@example.com 430 | ``` 431 | 432 | ## Setting up search indexing 433 | 434 | Hyperkitty in mailman-web image support full-text indexing. The current default 435 | indexing engine is [Whoosh](https://whoosh.readthedocs.io/en/latest/intro.html) 436 | for historical reasons. It is highly recommended that you instead use Xapian for 437 | production use cases. The default will change when the next major version bump 438 | happens. 439 | 440 | To configure your Mailman-web container to use Xapian, add the following to your 441 | `settings_local.py`: 442 | 443 | ```python 444 | HAYSTACK_CONNECTIONS = { 445 | 'default': { 446 | 'ENGINE': 'xapian_backend.XapianEngine', 447 | 'PATH': "/opt/mailman-web-data/fulltext_index", 448 | }, 449 | } 450 | ``` 451 | 452 | If you have been using the default search indexing engine, you might have to 453 | re-index emails using the following command: 454 | 455 | ```bash 456 | $ docker compose exec mailman-web ./manage.py rebuild_index 457 | ``` 458 | 459 | This command can take some time if you a lot of emails, so please be patient! 460 | 461 | ## Setting up your web server 462 | 463 | It is advisable to run your Django (interfaced through WSGI server) through an 464 | _actual_ webserver in production for better performance. 465 | 466 | If you are using v0.1.0, the uwsgi server is configured to listen to requests at 467 | port `8000` using the `HTTP` protocol. Make sure that you preserve the `HOST` 468 | header when you proxy the requests from your Web Server. In Nginx you can do 469 | that by adding the following to your configuration: 470 | 471 | ``` 472 | # Nginx configuration. 473 | location /static { 474 | alias /opt/mailman/web/static; 475 | autoindex off; 476 | } 477 | 478 | location / { 479 | proxy_pass http://127.0.0.1:8000; 480 | include uwsgi_params; 481 | uwsgi_read_timeout 300; 482 | proxy_set_header Host $host; 483 | proxy_set_header X-Forwarded-For $remote_addr; 484 | } 485 | 486 | ``` 487 | 488 | Make sure you are using `proxy_pass` for the `HTTP` protocol. 489 | 490 | ### uwsgi 491 | 492 | 493 | Starting from v0.1.1, the uwsgi server is configured to listen to requests at 494 | port `8000` with the http protocol and port `8080` for the uwsgi 495 | protocol. 496 | 497 | **Please make sure that you are using port 8080 for uwsgi protocol.** 498 | 499 | It is advised to use the uwsgi protocol as it has better performance. Both 500 | Apache and Nginx have native support for the uwsgi protocol through plugins which 501 | are generally included in the distro packages. 502 | 503 | To move to uwsgi protocol in the above nginx configuration use this 504 | 505 | ``` 506 | # Nginx configuration. 507 | location /static { 508 | alias /opt/mailman/web/static; 509 | autoindex off; 510 | } 511 | 512 | location / { 513 | uwsgi_pass localhost:8080; 514 | include uwsgi_params; 515 | uwsgi_read_timeout 300; 516 | } 517 | ``` 518 | 519 | Please make sure that you are using v0.1.1 or greater if you use this configuration. 520 | 521 | 522 | ### Serving static files 523 | 524 | UWSGI by default doesn't serve static files so, when running 525 | `mailman-web` using the provided `docker-compose.yaml` file, you won't see any 526 | CSS or JS files being served. 527 | 528 | To enable serving of static files using UWSGI, add the following environment 529 | variable to your `docker-compose.yaml` file under `mailman-web`: 530 | 531 | ``` 532 | UWSGI_STATIC_MAP=/static=/opt/mailman-web-data/static 533 | ``` 534 | 535 | It is recommended to use web-server to serve static files instead of UWSGI for 536 | better performance. You will have to add an alias rule in your web server to 537 | serve the static files. See [here][18] for instructions on how to configure your 538 | web server. The STATIC_ROOT for you would be `/opt/mailman/web/static`. 539 | 540 | ### SSL certificates 541 | 542 | SSL Certificates from Lets Encrypt need to be renewed every 90 days. You can 543 | setup a cron job to do the job. I have this small shell script (certbot-renew.sh) 544 | that you can put up in `/etc/cron.monthly` to get the job done. 545 | 546 | ``` 547 | #! /bin/bash 548 | 549 | cd /opt/letsencrypt/ 550 | ./certbot-auto --config /etc/letsencrypt/renewal/MY_DOMAIN_NAME.conf certonly 551 | 552 | if [ $? -ne 0 ] 553 | then 554 | ERRORLOG=`tail /var/log/letsencrypt/letsencrypt.log` 555 | echo -e "The Let's Encrypt cert has not been renewed! \n \n" \ 556 | $ERRORLOG 557 | else 558 | nginx -s reload 559 | fi 560 | 561 | exit 0 562 | ``` 563 | 564 | **Please do not forget to make the script executable (`chmod +x certbot-renew.sh`).** 565 | 566 | ## LICENSE 567 | 568 | This repository is licensed under the MIT License. Please see the LICENSE file for 569 | more details. 570 | 571 | [1]: http://list.org 572 | [2]: https://www.docker.com/ 573 | [3]: https://docs.docker.com/compose/ 574 | [4]: http://docs.mailman3.org/en/latest/ 575 | [5]: https://docs.docker.com/engine/installation/ 576 | [6]: https://docs.docker.com/compose/install/ 577 | [7]: https://uwsgi-docs.readthedocs.io/en/latest/ 578 | [8]: http://exim.org/ 579 | [9]: https://letsencrypt.org/ 580 | [10]: https://certbot.eff.org/ 581 | [11]: https://mailman.readthedocs.io/en/latest/src/mailman/docs/database.html 582 | [12]: http://www.postfix.org/ 583 | [13]: http://semver.org/ 584 | [14]: https://docs.docker.com/engine/security/trust/content_trust/ 585 | [15]: http://docs.mailman3.org/en/latest/config-web.html#setting-up-email 586 | [17]: https://docs.mailman3.org/en/latest/install/virtualenv.html#nginx-configuration 587 | [18]: http://docs.list.org/en/latest/pre-installation-guide.html#django-static-files 588 | [19]: https://github.com/maxking/docker-mailman/blob/master/docker-compose.yaml 589 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | # Since all the dockerfiles now require buildkit features. 6 | export DOCKER_BUILDKIT=1 7 | 8 | # Set the default value of BUILD_ROLLING to no. 9 | export BUILD_ROLLING="${1:-no}" 10 | 11 | DOCKER=docker 12 | 13 | # The following variables can be overridden using env-variables 14 | 15 | # Set tag namespace (i.e. ghcr.io/ for github package registry) 16 | TAG_NS="${TAG_NS:-maxking}" 17 | # Set default platforms to build 18 | BUILD_PLATFORM="${BUILD_PLATFORM:-linux/arm64/v8,linux/amd64}" 19 | # Platform to load into docker after build 20 | # Can only load one platform, should match host 21 | # mostly used: linux/amd64 and linux/arm64 22 | CURRENT_PLATFORM="${CURRENT_PLATFORM:-linux/amd64}" 23 | # set env-var PUSH to yes to push to registry 24 | PUSH="${PUSH:-no}" 25 | 26 | build() { 27 | if [ "$PUSH" = "yes" ]; then 28 | $DOCKER buildx build --platform $BUILD_PLATFORM $@ --push 29 | else 30 | $DOCKER buildx build --platform $BUILD_PLATFORM $@ 31 | fi 32 | $DOCKER buildx build --platform $CURRENT_PLATFORM $@ --load 33 | } 34 | 35 | # Check if the builder with name "multiarch" exists, if not create it 36 | if ! docker buildx ls | grep -q multiarch; then 37 | docker buildx create --name multiarch --driver docker-container --use 38 | fi 39 | 40 | if [ "$BUILD_ROLLING" = "yes" ]; then 41 | echo "Building rolling releases..." 42 | # Build the mailman-core image. 43 | build -f core/Dockerfile.dev \ 44 | --label version.git_commit="$COMMIT_ID" \ 45 | -t $TAG_NS/mailman-core:rolling core/ 46 | 47 | # Build the mailman-web image. 48 | build -f web/Dockerfile.dev \ 49 | --label version.git_commit="$COMMIT_ID" \ 50 | -t $TAG_NS/mailman-web:rolling web/ 51 | 52 | # build the postorius image. 53 | build -f postorius/Dockerfile.dev\ 54 | --label version.git_commit="$COMMIT_ID"\ 55 | -t $TAG_NS/postorius:rolling postorius/ 56 | else 57 | echo "Building stable releases..." 58 | # Build the stable releases. 59 | build --tag $TAG_NS/mailman-core:rolling core/ 60 | build --tag $TAG_NS/mailman-web:rolling web/ 61 | build --tag $TAG_NS/postorius:rolling postorius/ 62 | fi 63 | -------------------------------------------------------------------------------- /core/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1.3 2 | # Use 3.15 for Core since it has Python 3.9 3 | FROM alpine:3.22 4 | 5 | # Add requirements file. 6 | COPY requirements.txt /tmp/ 7 | 8 | #Install all required packages, add user for executing mailman and set execution rights for startup script 9 | RUN --mount=type=cache,target=/root/.cache \ 10 | apk update \ 11 | && apk add --virtual build-deps gcc python3-dev musl-dev postgresql-dev \ 12 | libffi-dev \ 13 | # Mailman html to plaintext conversion uses lynx. 14 | # psutil needs linux-headers to compile on musl c library. 15 | && apk add --no-cache bash su-exec postgresql-client mysql-client curl python3 py3-pip linux-headers py-cryptography mariadb-connector-c lynx tzdata \ 16 | && python3 -m pip install --break-system-packages -U pip setuptools wheel \ 17 | && python3 -m pip install --break-system-packages psycopg2 \ 18 | pymysql \ 19 | -r /tmp/requirements.txt \ 20 | 'importlib-resources<6.0.0' \ 21 | && apk del build-deps \ 22 | && adduser -S mailman 23 | 24 | #Add startup script to container 25 | COPY docker-entrypoint.sh /usr/local/bin/ 26 | 27 | # Change the working directory. 28 | WORKDIR /opt/mailman 29 | 30 | #Expose the ports for the api (8001) and lmtp (8024) 31 | EXPOSE 8001 8024 32 | 33 | ENV MAILMAN_CONFIG_FILE /etc/mailman.cfg 34 | 35 | ENTRYPOINT ["docker-entrypoint.sh"] 36 | CMD ["master", "--force"] 37 | -------------------------------------------------------------------------------- /core/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1.3 2 | # Use 3.15 for Core since it has Python 3.9 3 | FROM alpine:3.22 4 | 5 | # Set the commits that we are building. 6 | ARG CORE_REF 7 | ARG MM3_HK_REF 8 | 9 | #Install all required packages, add user for executing mailman and set execution 10 | #rights for startup script 11 | RUN --mount=type=cache,target=/root/.cache \ 12 | apk update \ 13 | && apk add --no-cache --virtual build-deps gcc python3-dev musl-dev \ 14 | postgresql-dev git libffi-dev g++ \ 15 | && apk add --no-cache bash su-exec postgresql-client mysql-client \ 16 | curl python3 py3-pip linux-headers py-cryptography mariadb-connector-c tzdata \ 17 | && python3 -m pip install -U --break-system-packages psycopg2 pymysql setuptools wheel \ 18 | && python3 -m pip install --break-system-packages \ 19 | git+https://gitlab.com/mailman/mailman \ 20 | git+https://gitlab.com/mailman/mailman-hyperkitty \ 21 | && apk del build-deps \ 22 | && adduser -S mailman 23 | 24 | #Add startup script to container 25 | COPY docker-entrypoint.sh /usr/local/bin/ 26 | 27 | # Change the working directory. 28 | WORKDIR /opt/mailman 29 | 30 | #Expose the ports for the api (8001) and lmtp (8024) 31 | EXPOSE 8001 8024 32 | 33 | # Set the default configuration file. 34 | ENV MAILMAN_CONFIG_FILE /etc/mailman.cfg 35 | 36 | ENTRYPOINT ["docker-entrypoint.sh"] 37 | 38 | CMD ["master"] 39 | -------------------------------------------------------------------------------- /core/README.md: -------------------------------------------------------------------------------- 1 | Mailman3 Core Docker Image 2 | ========================== 3 | 4 | When you spawn off this container, you must mount `/opt/mailman` to the 5 | container. Mailman's `var` directory will also be stored here so that it can 6 | persist across different sessions and containers. Any configuration at 7 | `/opt/mailman/core/mailman-extra.cfg` (on the host) will be added to the mailman's default 8 | generated confifguration (see below). 9 | 10 | It is not advised to run multiple mailman processes on the same host sharing the 11 | same `/opt/mailman` (`/opt/mailman/core` on the host) directory as this will 12 | almost certainly be dangerous. 13 | 14 | 15 | Configuration 16 | ============= 17 | 18 | These are the variables that you MUST change before deploying: 19 | 20 | - `HYPERKITTY_API_KEY`: Hyperkitty's API Key, should be set to the same value as 21 | set for the mailman-core. 22 | 23 | - `DATABASE_URL`: URL of the type 24 | `driver://user:password@hostname:port/databasename` for the django to use. If 25 | not set, the default is set to 26 | `sqlite:///opt/mailman-web-data/mailmanweb.db`. The standard 27 | docker-compose.yaml comes with it set to a postgres database. It is not must 28 | to change this if you are happy with PostgreSQL. 29 | 30 | - `DATABASE_TYPE`: Its value can be one of `sqlite`, `postgres` or `mysql` as 31 | these are the only three database types that Mailman 3 supports. Its default 32 | value is set to `sqlite` along with the default database class and default 33 | database url above. 34 | 35 | - `DATABASE_CLASS`: Default value is 36 | `mailman.database.sqlite.SQLiteDatabase`. The values for this can be found in 37 | the mailman's documentation [here][11]. 38 | 39 | 40 | These are the variables that you don't need to change if you are using a 41 | standard version of docker-compose.yaml from this repository. 42 | 43 | - `MM_HOSTNAME`: Which hostname or IP should Core bind to for REST API and 44 | LMTP. If not defined output from the `hostname -i` command is used. 45 | 46 | - `MAILMAN_REST_PORT`: Which port should Core use for the REST API. If not defined 47 | the default is `8001`. 48 | 49 | - `MAILMAN_REST_USER`: Which username should Core use for the REST API. If not 50 | defined the default is `restadmin`. 51 | 52 | - `MAILMAN_REST_PASSWORD`: Which password should Core use for the REST API. If 53 | not defined the default is `restpass`. 54 | 55 | - `MTA`: Mail Transfer Agent to use. Either `exim` or `postfix`. Default value is `exim`. 56 | 57 | - `SMTP_HOST`: IP Address/hostname from which you will be sending 58 | emails. Default value is the container's gateway retrieved from: 59 | /sbin/ip route | awk '/default/ { print $3 }' 60 | 61 | - `SMTP_PORT`: Port used for SMTP. Default is `25`. 62 | 63 | - `SMTP_SECURE_MODE`: Security mode (encryption) used for SMTP. Default is `smtp`. Can also be `starttls` or `smtps`. 64 | 65 | - `HYPERKITTY_URL`: Default value is `http://mailman-web:8000/hyperkitty` 66 | 67 | In case of a need for fine tuning of REST API web-server that uses [Gunicorn](https://docs.gunicorn.org/en/stable/settings.html) (e.g. for raising of timeouts) `/opt/mailman/core/gunicorn-extra.cfg` file could be provided holding necessary configuration options. 68 | 69 | Configuration file, [shipped with Mailman Core](https://gitlab.com/mailman/mailman/-/blob/master/src/mailman/config/gunicorn.cfg), is used by default. 70 | 71 | For example, to increase the default 30 sec timeout, which won't work for some API calls to highly populated lists, provide the following `gunicorn-extra.cfg` file: 72 | 73 | ``` 74 | [gunicorn] 75 | graceful_timeout = 30 76 | timeout = 300 77 | ``` 78 | 79 | Running Mailman-Core 80 | ==================== 81 | 82 | It is highly recomended that you run this image along with the 83 | docker-compose.yaml file provided at the [github repo][1] of this 84 | image. However, it is possible to run this image as a standalone container if 85 | you want just a mailman-core. 86 | 87 | ```bash 88 | $ mkdir -p /opt/mailman/core 89 | $ docker run -it -e "HYPERKITTY_API_KEY=changeme" -h mailman-core -v /opt/mailman/core:/opt/mailman mailman-core 90 | ``` 91 | 92 | However, if you don't provide the environment `DATABASE_URL`, the database _may_ 93 | not be persisted. All the configuration options are explained in more detail. 94 | 95 | If you need mode advanced configuration for mailman, you can create 96 | `/opt/mailman/mailman.cfg` and it be added to the configuration inside the 97 | container. Note that anything inside this configuration will override the 98 | settings provided via the environment variables and their default values. 99 | 100 | By default, the following settings are generated: 101 | 102 | ``` 103 | # mailman.cfg 104 | [mta] 105 | incoming: mailman.mta.exim4.LMTP 106 | outgoing: mailman.mta.deliver.deliver 107 | lmtp_host: $MM_HOSTNAME 108 | lmtp_port: 8024 109 | smtp_host: $SMTP_HOST 110 | smtp_port: $SMTP_PORT 111 | smtp_secure_mode: $SMTP_SECURE_MODE 112 | smtp_verify_hostname: $SMTP_VERIFY_HOSTNAME 113 | smtp_verify_cert: $SMTP_VERIFY_CERT 114 | configuration: python:mailman.config.exim4 115 | 116 | [runner.retry] 117 | sleep_time: 10s 118 | 119 | [webservice] 120 | hostname: $MM_HOSTNAME 121 | port: $MAILMAN_REST_PORT 122 | admin_user: $MAILMAN_REST_USER 123 | admin_pass: $MAILMAN_REST_PASSWORD 124 | configuration: /etc/gunicorn.cfg 125 | 126 | [archiver.hyperkitty] 127 | class: mailman_hyperkitty.Archiver 128 | enable: yes 129 | configuration: /etc/mailman-hyperkitty.cfg 130 | 131 | [database] 132 | class: $DATABASE_CLASS 133 | url: $DATABASE_URL 134 | ``` 135 | 136 | ``` 137 | # mailman-hyperkitty.cfg 138 | [general] 139 | base_url: $HYPERKITTY_URL 140 | api_key: $HYPERKITTY_API_KEY 141 | ``` 142 | 143 | MTA 144 | === 145 | 146 | You can use Postfix or [Exim][2] with this image to send emails. Mailman Core 147 | can interact with any modern MTA which can deliver emails over LMTP. The 148 | documentation for Mailman Core has configuration settigs for using them. 149 | 150 | Only Exim and Postfix have been tested with these images and are supported as of 151 | now. There _might_ be some limitations with using other MTAs in a containerized 152 | environments. Contributions are welcome for anything additional needed to 153 | support other MTAs. 154 | 155 | To setup Exim or Posfix, checkout the [documentation][3]. 156 | 157 | [1]: https://github.com/maxking/docker-mailman 158 | [2]: http://www.exim.org 159 | [3]: https://asynchronous.in/docker-mailman#setting-up-your-mta 160 | -------------------------------------------------------------------------------- /core/VERSION: -------------------------------------------------------------------------------- 1 | 0.1.1 2 | -------------------------------------------------------------------------------- /core/assets/exim/25_mm3_macros: -------------------------------------------------------------------------------- 1 | # Place this file at 2 | # /etc/exim4/conf.d/main/25_mm3_macros 3 | 4 | domainlist mm3_domains=MY_DOMAIN_NAME 5 | # Depending on your network configuration 6 | #MM3_LMTP_HOST=mailman-core 7 | MM3_LMTP_HOST=localhost 8 | MM3_LMTP_PORT=8024 9 | MM3_HOME=/opt/mailman/core/var 10 | 11 | ################################################################ 12 | # The configuration below is boilerplate: 13 | # you should not need to change it. 14 | 15 | # The path to the list receipt (used as the required file when 16 | # matching list addresses) 17 | MM3_LISTCHK=MM3_HOME/lists/${local_part}.${domain} 18 | -------------------------------------------------------------------------------- /core/assets/exim/455_mm3_router: -------------------------------------------------------------------------------- 1 | # Place this file at 2 | # /etc/exim4/conf.d/router/455_mm3_router 3 | 4 | mailman3_router: 5 | driver = accept 6 | domains = +mm3_domains 7 | require_files = MM3_LISTCHK 8 | local_part_suffix_optional 9 | local_part_suffix = -admin : \ 10 | -bounces : -bounces+* : \ 11 | -confirm : -confirm+* : \ 12 | -join : -leave : \ 13 | -owner : -request : \ 14 | -subscribe : -unsubscribe 15 | transport = mailman3_transport 16 | -------------------------------------------------------------------------------- /core/assets/exim/55_mm3_transport: -------------------------------------------------------------------------------- 1 | # Place this file at 2 | # /etc/exim4/conf.d/transport/55_mm3_transport 3 | 4 | mailman3_transport: 5 | debug_print = "Email for mailman" 6 | driver = smtp 7 | protocol = lmtp 8 | allow_localhost 9 | hosts = MM3_LMTP_HOST 10 | port = MM3_LMTP_PORT 11 | rcpt_include_affixes = true 12 | -------------------------------------------------------------------------------- /core/assets/mailman-hyperkitty.cfg: -------------------------------------------------------------------------------- 1 | [general] 2 | # This is your HyperKitty installation, preferably on the localhost. This 3 | # address will be used by Mailman to forward incoming emails to HyperKitty 4 | # for archiving. It does not need to be publicly available, in fact it's 5 | # better if it is not. 6 | base_url: http://mailman-web:8000/hyperkitty/ 7 | # Shared API key, must be the identical to the value in HyperKitty's 8 | # settings. 9 | api_key: ASmallAPIKey 10 | -------------------------------------------------------------------------------- /core/assets/mailman.cfg: -------------------------------------------------------------------------------- 1 | [mta] 2 | incoming: mailman.mta.exim4.LMTP 3 | outgoing: mailman.mta.deliver.deliver 4 | lmtp_host: mailman-core 5 | lmtp_port: 8024 6 | smtp_host: 172.19.199.1 7 | smtp_port: 25 8 | configuration: python:mailman.config.exim4 9 | 10 | # [archiver.mhonarc] 11 | # enable: yes 12 | 13 | # [archiver.mail_archive] 14 | # enable: yes 15 | 16 | # [archiver.prototype] 17 | # enable: yes 18 | 19 | [runner.retry] 20 | sleep_time: 10s 21 | 22 | [shell] 23 | use_ipython: yes 24 | 25 | [webservice] 26 | hostname: mailman-core 27 | 28 | [archiver.hyperkitty] 29 | class: mailman_hyperkitty.Archiver 30 | enable: yes 31 | configuration: /opt/mailman/mailman-hyperkitty.cfg 32 | 33 | [database] 34 | class: mailman.database.postgresql.PostgreSQLDatabase 35 | url: postgres://mailman:mailmanpass@database/mailmandb 36 | -------------------------------------------------------------------------------- /core/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -e 3 | 4 | function wait_for_postgres () { 5 | # Check if the postgres database is up and accepting connections before 6 | # moving forward. 7 | # TODO: Use python3's psycopg2 module to do this in python3 instead of 8 | # installing postgres-client in the image. 9 | until psql -P pager=off $DATABASE_URL -c '\l'; do 10 | >&2 echo "Postgres is unavailable - sleeping" 11 | sleep 1 12 | done 13 | >&2 echo "Postgres is up - continuing" 14 | } 15 | 16 | function wait_for_mysql () { 17 | # Check if MySQL is up and accepting connections. 18 | readarray -d' ' -t ENDPOINT <<< $(python3 -c "from urllib.parse import urlparse; o = urlparse('$DATABASE_URL'); print('%s %s' % (o.hostname, o.port if o.port else '3306'));") 19 | until mysqladmin ping --host ${ENDPOINT[0]} --port ${ENDPOINT[1]} --silent; do 20 | >&2 echo "MySQL is unavailable - sleeping" 21 | sleep 1 22 | done 23 | >&2 echo "MySQL is up - continuing" 24 | } 25 | 26 | # Empty the config file. 27 | echo "# This file is autogenerated at container startup." > /etc/mailman.cfg 28 | 29 | # Check if $MM_HOSTNAME is set, if not, set it to the value returned by 30 | # `hostname -i` command to set it to whatever IP address is assigned to the 31 | # container. 32 | if [[ ! -v MM_HOSTNAME ]]; then 33 | export MM_HOSTNAME=`hostname -i` 34 | fi 35 | 36 | # SMTP_HOST defaults to the gateway 37 | if [[ ! -v SMTP_HOST ]]; then 38 | export SMTP_HOST=$(/sbin/ip route | awk '/default/ { print $3 }') 39 | echo "SMTP_HOST not specified, using the gateway ($SMTP_HOST) as default" 40 | fi 41 | 42 | if [[ ! -v SMTP_PORT ]]; then 43 | export SMTP_PORT=25 44 | fi 45 | 46 | if [[ ! -v SMTP_SECURE_MODE ]]; then 47 | export SMTP_SECURE_MODE="smtp" 48 | fi 49 | 50 | if [[ ! -v SMTP_VERIFY_HOSTNAME ]]; then 51 | export SMTP_VERIFY_HOSTNAME="true" 52 | fi 53 | 54 | if [[ ! -v SMTP_VERIFY_CERT ]]; then 55 | export SMTP_VERIFY_CERT="true" 56 | fi 57 | 58 | # Check if REST port, username, and password are set, if not, set them 59 | # to default values. 60 | if [[ ! -v MAILMAN_REST_PORT ]]; then 61 | export MAILMAN_REST_PORT='8001' 62 | fi 63 | 64 | if [[ ! -v MAILMAN_REST_USER ]]; then 65 | export MAILMAN_REST_USER='restadmin' 66 | fi 67 | 68 | if [[ ! -v MAILMAN_REST_PASSWORD ]]; then 69 | export MAILMAN_REST_PASSWORD='restpass' 70 | fi 71 | 72 | function setup_database () { 73 | if [[ ! -v DATABASE_URL ]] 74 | then 75 | echo "Environment variable DATABASE_URL should be defined..." 76 | exit 1 77 | fi 78 | 79 | # Translate mysql:// urls to mysql+mysql:// backend: 80 | if [[ "$DATABASE_URL" == mysql://* ]]; then 81 | DATABASE_URL="mysql+pymysql://${DATABASE_URL:8}" 82 | echo "Database URL prefix was automatically rewritten to: mysql+pymysql://" 83 | fi 84 | 85 | # If DATABASE_CLASS is not set, guess it for common databases: 86 | if [ -z "$DATABASE_CLASS" ]; then 87 | if [[ ("$DATABASE_URL" == mysql:*) || 88 | ("$DATABASE_URL" == mysql+*) ]]; then 89 | DATABASE_CLASS=mailman.database.mysql.MySQLDatabase 90 | fi 91 | if [[ ("$DATABASE_URL" == postgres:*) || 92 | ("$DATABASE_URL" == postgres+*) ]]; then 93 | DATABASE_CLASS=mailman.database.postgresql.PostgreSQLDatabase 94 | fi 95 | fi 96 | 97 | cat >> /etc/mailman.cfg <> /etc/mailman.cfg << EOF 132 | [runner.retry] 133 | sleep_time: 10s 134 | 135 | [webservice] 136 | hostname: $MM_HOSTNAME 137 | port: $MAILMAN_REST_PORT 138 | admin_user: $MAILMAN_REST_USER 139 | admin_pass: $MAILMAN_REST_PASSWORD 140 | configuration: /etc/gunicorn.cfg 141 | 142 | EOF 143 | 144 | # Generate a basic gunicorn.cfg. 145 | SITE_DIR=$(python3 -c 'import site; print(site.getsitepackages()[0])') 146 | cp "${SITE_DIR}/mailman/config/gunicorn.cfg" /etc/gunicorn.cfg 147 | 148 | # Generate a basic configuration to use exim 149 | cat > /tmp/exim-mailman.cfg < /etc/postfix-mailman.cfg << EOF 167 | [postfix] 168 | transport_file_type: regex 169 | # While in regex mode, postmap_command is never used, a placeholder 170 | # is added here so that it doesn't break anything. 171 | postmap_command: true 172 | EOF 173 | 174 | # Generate a basic configuration to use postfix. 175 | cat > /tmp/postfix-mailman.cfg <> /etc/mailman.cfg 196 | elif [ "$MTA" == "postfix" ] 197 | then 198 | echo "Using Postfix configuration" 199 | cat /tmp/postfix-mailman.cfg >> /etc/mailman.cfg 200 | else 201 | echo "No MTA environment variable found, defaulting to Exim" 202 | cat /tmp/exim-mailman.cfg >> /etc/mailman.cfg 203 | fi 204 | 205 | rm -f /tmp/{postfix,exim}-mailman.cfg 206 | 207 | if [[ -e /opt/mailman/mailman-extra.cfg ]] 208 | then 209 | echo "Found configuration file at /opt/mailman/mailman-extra.cfg" 210 | cat /opt/mailman/mailman-extra.cfg >> /etc/mailman.cfg 211 | fi 212 | 213 | if [[ -e /opt/mailman/gunicorn-extra.cfg ]] 214 | then 215 | echo "Found [webserver] configuration file at /opt/mailman/gunicorn-extra.cfg" 216 | cat /opt/mailman/gunicorn-extra.cfg > /etc/gunicorn.cfg 217 | fi 218 | 219 | if [[ -v HYPERKITTY_API_KEY ]]; then 220 | 221 | echo "HYPERKITTY_API_KEY found, setting up HyperKitty archiver..." 222 | 223 | cat >> /etc/mailman.cfg << EOF 224 | [archiver.hyperkitty] 225 | class: mailman_hyperkitty.Archiver 226 | enable: yes 227 | configuration: /etc/mailman-hyperkitty.cfg 228 | 229 | EOF 230 | 231 | if [[ ! -v HYPERKITTY_URL ]]; then 232 | echo "HYPERKITTY_URL not set, using the default value of http://mailman-web:8000/hyperkitty" 233 | export HYPERKITTY_URL="http://mailman-web:8000/hyperkitty/" 234 | fi 235 | 236 | # Generate a basic mailman-hyperkitty.cfg. 237 | cat > /etc/mailman-hyperkitty.cfg <&2 echo "Postgres is unavailable - sleeping" 12 | sleep 1 13 | done 14 | >&2 echo "Postgres is up - continuing" 15 | } 16 | 17 | function wait_for_mysql () { 18 | # Check if MySQL is up and accepting connections. 19 | readarray -d' ' -t ENDPOINT <<< $(python3 -c "from urllib.parse import urlparse; o = urlparse('$DATABASE_URL'); print('%s %s' % (o.hostname, o.port if o.port else '3306'));") 20 | until mysqladmin ping --host ${ENDPOINT[0]} --port ${ENDPOINT[1]} --silent; do 21 | >&2 echo "MySQL is unavailable - sleeping" 22 | sleep 1 23 | done 24 | >&2 echo "MySQL is up - continuing" 25 | } 26 | 27 | function check_or_create () { 28 | # Check if the path exists, if not, create the directory. 29 | if [[ ! -e dir ]]; then 30 | echo "$1 does not exist, creating ..." 31 | mkdir "$1" 32 | fi 33 | } 34 | 35 | # function postgres_ready(){ 36 | # python << END 37 | # import sys 38 | # import psycopg2 39 | # try: 40 | # conn = psycopg2.connect(dbname="$POSTGRES_DB", user="$POSTGRES_USER", password="$POSTGRES_PASSWORD", host="postgres") 41 | # except psycopg2.OperationalError: 42 | # sys.exit(-1) 43 | # sys.exit(0) 44 | # END 45 | # } 46 | 47 | # SMTP_HOST defaults to the gateway 48 | if [[ ! -v SMTP_HOST ]]; then 49 | export SMTP_HOST=$(/sbin/ip route | awk '/default/ { print $3 }') 50 | fi 51 | 52 | # Check if $SECRET_KEY is defined, if not, bail out. 53 | if [[ ! -v SECRET_KEY ]]; then 54 | echo "SECRET_KEY is not defined. Aborting." 55 | exit 1 56 | fi 57 | 58 | # Check if $DATABASE_URL is defined, if not, use a standard sqlite database. 59 | # 60 | # If the $DATABASE_URL is defined and is postgres, check if it is available 61 | # yet. Do not start the container before the postgresql boots up. 62 | # 63 | # If the $DATABASE_URL is defined and is mysql, check if the database is 64 | # available before the container boots up. 65 | # 66 | # TODO: Check the database type and detect if it is up based on that. For now, 67 | # assume that postgres is being used if DATABASE_URL is defined. 68 | 69 | if [[ ! -v DATABASE_URL ]]; then 70 | echo "DATABASE_URL is not defined. Using sqlite database..." 71 | export DATABASE_URL=sqlite:////opt/mail-web-data/mailmanweb.db 72 | export DATABASE_TYPE='sqlite' 73 | fi 74 | 75 | if [[ "$DATABASE_TYPE" = 'postgres' ]] 76 | then 77 | wait_for_postgres 78 | elif [[ "$DATABASE_TYPE" = 'mysql' ]] 79 | then 80 | wait_for_mysql 81 | fi 82 | 83 | # Check if we are in the correct directory before running commands. 84 | if [[ ! $(pwd) == '/opt/mailman-web' ]]; then 85 | echo "Running in the wrong directory...switching to /opt/mailman-web" 86 | cd /opt/mailman-web 87 | fi 88 | 89 | # Check if the logs directory is setup. 90 | if [[ ! -e /opt/mailman-web-data/logs/mailmanweb.log ]]; then 91 | echo "Creating log file for mailman web" 92 | mkdir -p /opt/mailman-web-data/logs/ 93 | touch /opt/mailman-web-data/logs/mailmanweb.log 94 | fi 95 | 96 | if [[ ! -e /opt/mailman-web-data/logs/uwsgi.log ]]; then 97 | echo "Creating log file for uwsgi.." 98 | touch /opt/mailman-web-data/logs/uwsgi.log 99 | fi 100 | 101 | # Check if the settings_local.py file exists, if yes, copy it too. 102 | if [[ -e /opt/mailman-web-data/settings_local.py ]]; then 103 | echo "Copying settings_local.py ..." 104 | cp /opt/mailman-web-data/settings_local.py /opt/mailman-web/settings_local.py 105 | chown mailman:mailman /opt/mailman-web/settings_local.py 106 | else 107 | echo "settings_local.py not found, it is highly recommended that you provide one" 108 | echo "Using default configuration to run." 109 | fi 110 | 111 | # Collect static for the django installation. 112 | python3 manage.py collectstatic --noinput --clear --verbosity 0 113 | 114 | # Compile all the installed po files to mo. 115 | SITE_DIR=$(python3 -c 'import site; print(site.getsitepackages()[0])') 116 | echo "Compiling locale files in $SITE_DIR" 117 | cd $SITE_DIR && python3 /opt/mailman-web/manage.py compilemessages && cd - 118 | 119 | # Migrate all the data to the database if this is a new installation, otherwise 120 | # this command will upgrade the database. 121 | python3 manage.py migrate 122 | 123 | # If MAILMAN_ADMIN_USER and MAILMAN_ADMIN_EMAIL is defined create a new 124 | # superuser for Django. There is no password setup so it can't login yet unless 125 | # the password is reset. 126 | if [[ -v MAILMAN_ADMIN_USER ]] && [[ -v MAILMAN_ADMIN_EMAIL ]]; 127 | then 128 | echo "Creating admin user $MAILMAN_ADMIN_USER ..." 129 | python3 manage.py createsuperuser --noinput --username "$MAILMAN_ADMIN_USER"\ 130 | --email "$MAILMAN_ADMIN_EMAIL" 2> /dev/null || \ 131 | echo "Superuser $MAILMAN_ADMIN_USER already exists" 132 | fi 133 | 134 | # If SERVE_FROM_DOMAIN is defined then rename the default `example.com` 135 | # domain to the defined domain. 136 | if [[ -v SERVE_FROM_DOMAIN ]]; 137 | then 138 | echo "Setting $SERVE_FROM_DOMAIN as the default domain ..." 139 | python3 manage.py shell -c \ 140 | "from django.contrib.sites.models import Site; Site.objects.filter(domain='example.com').update(domain='$SERVE_FROM_DOMAIN', name='$SERVE_FROM_DOMAIN')" 141 | fi 142 | 143 | # Create a mailman user with the specific UID and GID and do not create home 144 | # directory for it. Also chown the logs directory to write the files. 145 | chown mailman:mailman /opt/mailman-web-data -R 146 | 147 | exec $@ 148 | -------------------------------------------------------------------------------- /postorius/mailman-web/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxking/docker-mailman/5b4202dd7622a6ba11b215a6b638f7b11a03f419/postorius/mailman-web/__init__.py -------------------------------------------------------------------------------- /postorius/mailman-web/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /postorius/mailman-web/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) 1998-2016 by the Free Software Foundation, Inc. 3 | # 4 | # This file is part of Mailman Suite. 5 | # 6 | # Mailman Suite is free sofware: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) 9 | # any later version. 10 | # 11 | # Mailman Suite is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 | # for more details. 15 | 16 | # You should have received a copy of the GNU General Public License along 17 | # with Mailman Suite. If not, see . 18 | """ 19 | Django Settings for Mailman Suite (hyperkitty + postorius) 20 | 21 | For more information on this file, see 22 | https://docs.djangoproject.com/en/1.8/topics/settings/ 23 | 24 | For the full list of settings and their values, see 25 | https://docs.djangoproject.com/en/1.8/ref/settings/ 26 | """ 27 | 28 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 29 | import os 30 | import dj_database_url 31 | import sys 32 | from socket import gethostbyname 33 | 34 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 35 | 36 | # SECURITY WARNING: keep the secret key used in production secret! 37 | SECRET_KEY = os.environ.get('SECRET_KEY') 38 | 39 | # SECURITY WARNING: don't run with debug turned on in production! 40 | DEBUG = False 41 | 42 | ADMINS = ( 43 | ('Mailman Suite Admin', 'root@localhost'), 44 | ) 45 | 46 | SITE_ID = 1 47 | 48 | # Hosts/domain names that are valid for this site; required if DEBUG is False 49 | # See https://docs.djangoproject.com/en/3.1/ref/settings/#allowed-hosts 50 | ALLOWED_HOSTS = [ 51 | "localhost", # Archiving API from Mailman, keep it. 52 | "mailman-web", 53 | gethostbyname("mailman-web"), 54 | os.environ.get('SERVE_FROM_DOMAIN'), 55 | ] 56 | ALLOWED_HOSTS.extend(os.getenv("DJANGO_ALLOWED_HOSTS", "").split(",")) 57 | 58 | # Mailman API credentials 59 | MAILMAN_REST_API_URL = os.environ.get('MAILMAN_REST_URL', 'http://mailman-core:8001') 60 | MAILMAN_REST_API_USER = os.environ.get('MAILMAN_REST_USER', 'restadmin') 61 | MAILMAN_REST_API_PASS = os.environ.get('MAILMAN_REST_PASSWORD', 'restpass') 62 | 63 | # Application definition 64 | 65 | INSTALLED_APPS = [] 66 | DEFAULT_APPS = [ 67 | 'postorius', 68 | 'django_mailman3', 69 | # Uncomment the next line to enable the admin: 70 | 'django.contrib.admin', 71 | # Uncomment the next line to enable admin documentation: 72 | # 'django.contrib.admindocs', 73 | 'django.contrib.auth', 74 | 'django.contrib.contenttypes', 75 | 'django.contrib.sessions', 76 | 'django.contrib.sites', 77 | 'django.contrib.messages', 78 | 'django.contrib.staticfiles', 79 | 'django.contrib.humanize', 80 | 'django_gravatar', 81 | 'allauth', 82 | 'allauth.account', 83 | 'allauth.socialaccount', 84 | ] 85 | MAILMAN_WEB_SOCIAL_AUTH = [ 86 | 'django_mailman3.lib.auth.fedora', 87 | 'allauth.socialaccount.providers.openid', 88 | 'allauth.socialaccount.providers.github', 89 | 'allauth.socialaccount.providers.gitlab', 90 | 'allauth.socialaccount.providers.google', 91 | ] 92 | 93 | MIDDLEWARE = ( 94 | 'django.contrib.sessions.middleware.SessionMiddleware', 95 | 'django.middleware.common.CommonMiddleware', 96 | 'django.middleware.csrf.CsrfViewMiddleware', 97 | 'django.middleware.locale.LocaleMiddleware', 98 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 99 | 'django.contrib.messages.middleware.MessageMiddleware', 100 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 101 | 'django.middleware.security.SecurityMiddleware', 102 | 'django_mailman3.middleware.TimezoneMiddleware', 103 | 'allauth.account.middleware.AccountMiddleware', 104 | 'postorius.middleware.PostoriusMiddleware', 105 | ) 106 | 107 | ROOT_URLCONF = 'urls' 108 | 109 | TEMPLATES = [ 110 | { 111 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 112 | 'DIRS': [], 113 | 'APP_DIRS': True, 114 | 'OPTIONS': { 115 | 'context_processors': [ 116 | 'django.template.context_processors.debug', 117 | 'django.template.context_processors.i18n', 118 | 'django.template.context_processors.media', 119 | 'django.template.context_processors.static', 120 | 'django.template.context_processors.tz', 121 | 'django.template.context_processors.csrf', 122 | 'django.template.context_processors.request', 123 | 'django.contrib.auth.context_processors.auth', 124 | 'django.contrib.messages.context_processors.messages', 125 | 'django_mailman3.context_processors.common', 126 | 'postorius.context_processors.postorius', 127 | ], 128 | }, 129 | }, 130 | ] 131 | 132 | WSGI_APPLICATION = 'wsgi.application' 133 | 134 | 135 | # Database 136 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 137 | 138 | 139 | # This uses $DATABASE_URL from the environment variable to create a 140 | # django-style-config-dict. 141 | # https://github.com/kennethreitz/dj-database-url 142 | DATABASES = { 143 | 'default': dj_database_url.config(conn_max_age=600) 144 | } 145 | 146 | # Avoid Django 3.2+ warning 147 | # https://github.com/maxking/docker-mailman/issues/595 148 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 149 | 150 | 151 | # If you're behind a proxy, use the X-Forwarded-Host header 152 | # See https://docs.djangoproject.com/en/1.8/ref/settings/#use-x-forwarded-host 153 | USE_X_FORWARDED_HOST = True 154 | 155 | 156 | # Password validation 157 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 158 | AUTH_PASSWORD_VALIDATORS = [ 159 | { 160 | 'NAME': 161 | 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 162 | }, 163 | { 164 | 'NAME': 165 | 'django.contrib.auth.password_validation.MinimumLengthValidator', 166 | }, 167 | { 168 | 'NAME': 169 | 'django.contrib.auth.password_validation.CommonPasswordValidator', 170 | }, 171 | { 172 | 'NAME': 173 | 'django.contrib.auth.password_validation.NumericPasswordValidator', 174 | }, 175 | ] 176 | 177 | # Internationalization 178 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 179 | 180 | LANGUAGE_CODE = 'en-us' 181 | 182 | TIME_ZONE = 'UTC' 183 | 184 | USE_I18N = True 185 | 186 | USE_L10N = True 187 | 188 | USE_TZ = True 189 | 190 | STATIC_ROOT = '/opt/mailman-web-data/static' 191 | 192 | STATIC_URL = '/static/' 193 | 194 | # Additional locations of static files 195 | 196 | 197 | # List of finder classes that know how to find static files in 198 | # various locations. 199 | STATICFILES_FINDERS = ( 200 | 'django.contrib.staticfiles.finders.FileSystemFinder', 201 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 202 | ) 203 | 204 | 205 | SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' 206 | 207 | LOGIN_URL = 'account_login' 208 | LOGIN_REDIRECT_URL = 'list_index' 209 | LOGOUT_URL = 'account_logout' 210 | 211 | 212 | # Use SERVE_FROM_DOMAIN as the default domain in the email. 213 | hostname = os.environ.get('SERVE_FROM_DOMAIN', 'localhost.local') 214 | DEFAULT_FROM_EMAIL = 'postorius@{}'.format(hostname) 215 | SERVER_EMAIL = 'root@{}'.format(hostname) 216 | 217 | # Change this when you have a real email backend 218 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 219 | EMAIL_HOST = os.environ.get('SMTP_HOST', '') 220 | EMAIL_PORT = os.environ.get('SMTP_PORT', 25) 221 | EMAIL_HOST_USER = os.environ.get('SMTP_HOST_USER', '') 222 | EMAIL_HOST_PASSWORD = os.environ.get('SMTP_HOST_PASSWORD', '') 223 | EMAIL_USE_TLS = os.environ.get('SMTP_USE_TLS', False) 224 | EMAIL_USE_SSL = os.environ.get('SMTP_USE_SSL', False) 225 | 226 | # Compatibility with Bootstrap 3 227 | from django.contrib.messages import constants as messages # flake8: noqa 228 | MESSAGE_TAGS = { 229 | messages.ERROR: 'danger' 230 | } 231 | 232 | 233 | # 234 | # Social auth 235 | # 236 | AUTHENTICATION_BACKENDS = ( 237 | 'django.contrib.auth.backends.ModelBackend', 238 | 'allauth.account.auth_backends.AuthenticationBackend', 239 | ) 240 | 241 | # Django Allauth 242 | ACCOUNT_AUTHENTICATION_METHOD = "username_email" 243 | ACCOUNT_EMAIL_REQUIRED = True 244 | ACCOUNT_EMAIL_VERIFICATION = "mandatory" 245 | # You probably want https in production, but this is a dev setup file 246 | ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https" 247 | ACCOUNT_UNIQUE_EMAIL = True 248 | 249 | SOCIALACCOUNT_PROVIDERS = { 250 | 'openid': { 251 | 'SERVERS': [ 252 | dict(id='yahoo', 253 | name='Yahoo', 254 | openid_url='http://me.yahoo.com'), 255 | ], 256 | }, 257 | 'google': { 258 | 'SCOPE': ['profile', 'email'], 259 | 'AUTH_PARAMS': {'access_type': 'online'}, 260 | }, 261 | 'facebook': { 262 | 'METHOD': 'oauth2', 263 | 'SCOPE': ['email'], 264 | 'FIELDS': [ 265 | 'email', 266 | 'name', 267 | 'first_name', 268 | 'last_name', 269 | 'locale', 270 | 'timezone', 271 | ], 272 | 'VERSION': 'v2.4', 273 | }, 274 | } 275 | 276 | 277 | import sys 278 | # A sample logging configuration. The only tangible logging 279 | # performed by this configuration is to send an email to 280 | # the site admins on every HTTP 500 error when DEBUG=False. 281 | # See http://docs.djangoproject.com/en/dev/topics/logging for 282 | # more details on how to customize your logging configuration. 283 | LOGGING = { 284 | 'version': 1, 285 | 'disable_existing_loggers': False, 286 | 'filters': { 287 | 'require_debug_false': { 288 | '()': 'django.utils.log.RequireDebugFalse' 289 | } 290 | }, 291 | 'handlers': { 292 | 'mail_admins': { 293 | 'level': 'ERROR', 294 | 'filters': ['require_debug_false'], 295 | 'class': 'django.utils.log.AdminEmailHandler' 296 | }, 297 | 'file':{ 298 | 'level': 'INFO', 299 | 'class': 'logging.handlers.RotatingFileHandler', 300 | #'class': 'logging.handlers.WatchedFileHandler', 301 | 'filename': os.environ.get('DJANGO_LOG_URL','/opt/mailman-web-data/logs/mailmanweb.log'), 302 | 'formatter': 'verbose', 303 | }, 304 | 'console': { 305 | 'class': 'logging.StreamHandler', 306 | 'formatter': 'simple', 307 | 'level': 'INFO', 308 | 'stream': sys.stdout, 309 | }, 310 | # TODO: use an environment variable $DJ_LOG_URL to configure the logging 311 | # using an environment variable. 312 | }, 313 | 'loggers': { 314 | 'django.request': { 315 | 'handlers': ['mail_admins', 'file'], 316 | 'level': 'INFO', 317 | 'propagate': True, 318 | }, 319 | 'django': { 320 | 'handlers': ['file'], 321 | 'level': 'INFO', 322 | 'propagate': True, 323 | }, 324 | 'postorius': { 325 | 'handlers': ['file'], 326 | 'level': 'INFO', 327 | 'propagate': True 328 | }, 329 | }, 330 | 'formatters': { 331 | 'verbose': { 332 | 'format': '%(levelname)s %(asctime)s %(process)d %(name)s %(message)s' 333 | }, 334 | 'simple': { 335 | 'format': '%(levelname)s %(message)s' 336 | }, 337 | }, 338 | #'root': { 339 | # 'handlers': ['file'], 340 | # 'level': 'INFO', 341 | #}, 342 | } 343 | 344 | 345 | if os.environ.get('LOG_TO_CONSOLE') == 'yes': 346 | LOGGING['loggers']['django']['handlers'].append('console') 347 | LOGGING['loggers']['django.request']['handlers'].append('console') 348 | POSTORIUS_TEMPLATE_BASE_URL = os.environ.get('POSTORIUS_TEMPLATE_BASE_URL', 'http://mailman-web:8000') 349 | 350 | try: 351 | from settings_local import * 352 | except ImportError: 353 | pass 354 | 355 | # Compatibility for older installs that override INSTALLED_APPS 356 | if not INSTALLED_APPS: 357 | INSTALLED_APPS = DEFAULT_APPS + MAILMAN_WEB_SOCIAL_AUTH 358 | -------------------------------------------------------------------------------- /postorius/mailman-web/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) 1998-2016 by the Free Software Foundation, Inc. 3 | # 4 | # This file is part of Postorius. 5 | # 6 | # Postorius is free software: you can redistribute it and/or modify it under 7 | # the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) 9 | # any later version. 10 | # 11 | # Postorius is distributed in the hope that it will be useful, but WITHOUT 12 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | # more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along with 17 | # Postorius. If not, see . 18 | 19 | from django.conf.urls import include 20 | from django.contrib import admin 21 | from django.urls import re_path, reverse_lazy 22 | from django.views.generic import RedirectView 23 | 24 | urlpatterns = [ 25 | re_path(r'^$', RedirectView.as_view( 26 | url=reverse_lazy('list_index'), 27 | permanent=True)), 28 | re_path(r'postorius/', include('postorius.urls')), 29 | re_path(r'', include('django_mailman3.urls')), 30 | re_path(r'accounts/', include('allauth.urls')), 31 | # Django admin 32 | re_path(r'^admin/', admin.site.urls), 33 | ] 34 | -------------------------------------------------------------------------------- /postorius/mailman-web/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | # Port on which uwsgi will be listening. 3 | uwsgi-socket = 0.0.0.0:8080 4 | http-socket = 0.0.0.0:8000 5 | 6 | # Move to the directory where the django files are. 7 | chdir = /opt/mailman-web 8 | 9 | # Use the wsgi file provided with the django project. 10 | wsgi-file = wsgi.py 11 | 12 | # Setup default number of processes and threads per process. 13 | master = true 14 | processes = 2 15 | threads = 2 16 | 17 | # Drop privileges and don't run as root. 18 | uid = mailman 19 | gid = mailman 20 | 21 | # Setup the request log. 22 | req-logger = file:/opt/mailman-web-data/logs/uwsgi.log 23 | 24 | # Last log and it logs the rest of the stuff. 25 | logger = file:/opt/mailman-web-data/logs/uwsgi-error.log 26 | -------------------------------------------------------------------------------- /postorius/mailman-web/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for HyperKitty project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | # import sys 13 | # import site 14 | 15 | # For some unknown reason, sometimes mod_wsgi fails to set the python paths to 16 | # the virtualenv, with the 'python-path' option. You can do it here too. 17 | # 18 | # # Remember original sys.path. 19 | # prev_sys_path = list(sys.path) 20 | # # Add here, for the settings module 21 | # site.addsitedir(os.path.abspath(os.path.dirname(__file__))) 22 | # # Add the virtualenv 23 | # venv = os.path.join(os.path.abspath(os.path.dirname(__file__)), 24 | # '..', 'lib', 'python2.6', 'site-packages') 25 | # site.addsitedir(venv) 26 | # # Reorder sys.path so new directories at the front. 27 | # new_sys_path = [] 28 | # for item in list(sys.path): 29 | # if item not in prev_sys_path: 30 | # new_sys_path.append(item) 31 | # sys.path.remove(item) 32 | # sys.path[:0] = new_sys_path 33 | 34 | from django.core.wsgi import get_wsgi_application 35 | 36 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 37 | 38 | application = get_wsgi_application() 39 | -------------------------------------------------------------------------------- /tests/docker-test.yaml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | mailman-core: 5 | image: maxking/mailman-core:rolling 6 | 7 | mailman-web: 8 | image: maxking/mailman-web:rolling 9 | environment: 10 | - SECRET_KEY=abcdefghijklmnopqrstuv 11 | - SERVE_FROM_DOMAIN=araj.me 12 | -------------------------------------------------------------------------------- /tests/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -e 3 | 4 | # If the DB environment variable is not set, use postgres.x 5 | if [ "$DB" = "postgres" ] || [ -z $DB ] 6 | then 7 | docker compose -f docker-compose.yaml -f tests/docker-test.yaml up -d 8 | elif [ "$DB" = "mysql" ] 9 | then 10 | docker compose -f docker-compose-mysql.yaml -f tests/docker-test.yaml up -d 11 | fi 12 | 13 | # Print the IP Addresses of the Containers. 14 | docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mailman-core 15 | docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' mailman-web 16 | 17 | # Make sure all the containers are running. 18 | docker compose ps 19 | 20 | # Sleep for a while and check again if the containers are up. 21 | sleep 60 22 | docker ps 23 | 24 | # Check if there is anything interesting in the logs. 25 | docker logs mailman-web 26 | docker logs mailman-core 27 | 28 | 29 | # Check to see if the core is working as expected. 30 | docker exec mailman-core curl -u restadmin:restpass http://mailman-core:8001/3.1/system | grep "GNU Mailman" 31 | 32 | # Check to see if postorius is working. 33 | docker exec mailman-web curl -L http://mailman-web:8000/postorius/lists | grep "Mailing List" 34 | 35 | # Check to see if hyperkitty is working. 36 | docker exec mailman-web curl -L http://mailman-web:8000/hyperkitty/ | grep "Available lists" 37 | -------------------------------------------------------------------------------- /web/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1.3 2 | FROM alpine:3.21.3 3 | 4 | # Add requirements file. 5 | COPY requirements.txt /tmp/ 6 | 7 | # Install packages and dependencies for postorius and hyperkitty Add user for 8 | # executing apps, change ownership for uwsgi+django files and set execution 9 | # rights for management script 10 | RUN --mount=type=cache,target=/root/.cache \ 11 | set -ex \ 12 | && apk add --no-cache --virtual .build-deps gcc libc-dev linux-headers \ 13 | postgresql-dev mariadb-dev mariadb-connector-c python3-dev libffi-dev openldap-dev cargo rust \ 14 | && apk add --no-cache --virtual .mailman-rundeps bash sassc tzdata libldap \ 15 | postgresql-client mysql-client py3-mysqlclient curl mailcap gettext \ 16 | python3 py3-pip xapian-core xapian-bindings-python3 libffi pcre-dev py-cryptography \ 17 | && python3 -m pip install --break-system-packages -U 'Django<4.3' pip setuptools wheel \ 18 | && pip install --break-system-packages -r /tmp/requirements.txt \ 19 | whoosh \ 20 | # later builds of uwsgi don't compile on aarch64 21 | uwsgi==2.0.25 \ 22 | psycopg2 \ 23 | dj-database-url \ 24 | mysqlclient \ 25 | typing \ 26 | xapian-haystack \ 27 | django-auth-ldap \ 28 | pymemcache \ 29 | diskcache \ 30 | django-utils-six \ 31 | tzdata \ 32 | pytz \ 33 | 'django-allauth[socialaccount,openid]' \ 34 | && apk del .build-deps \ 35 | && addgroup -S mailman \ 36 | && adduser -S -G mailman mailman 37 | 38 | # Add needed files for uwsgi server + settings for django 39 | COPY mailman-web /opt/mailman-web 40 | # Add startup script to container 41 | COPY docker-entrypoint.sh /usr/local/bin/ 42 | 43 | RUN chown -R mailman /opt/mailman-web/ \ 44 | && chmod u+x /opt/mailman-web/manage.py 45 | 46 | WORKDIR /opt/mailman-web 47 | 48 | # Expose port 8000 for http and port 8080 for uwsgi 49 | # (see web/mailman-web/uwsgi.ini#L2-L4) 50 | EXPOSE 8000 8080 51 | 52 | # Use stop signal for uwsgi server 53 | STOPSIGNAL SIGINT 54 | 55 | ENTRYPOINT ["docker-entrypoint.sh"] 56 | 57 | CMD ["uwsgi", "--ini", "/opt/mailman-web/uwsgi.ini"] 58 | -------------------------------------------------------------------------------- /web/Dockerfile.dev: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:1.3 2 | FROM alpine:3.21.3 3 | 4 | ARG POSTORIUS_REF 5 | ARG HYPERKITTY_REF 6 | ARG DJ_MM3_REF 7 | ARG CLIENT_REF 8 | 9 | # Install packages and dependencies for postorius and hyperkitty Add user for 10 | # executing apps, change ownership for uwsgi+django files and set execution 11 | # rights for management script 12 | RUN --mount=type=cache,target=/root/.cache \ 13 | set -ex \ 14 | && apk add --no-cache --virtual .build-deps gcc libc-dev linux-headers git \ 15 | postgresql-dev mariadb-dev mariadb-connector-c python3-dev libffi-dev openldap-dev cargo rust \ 16 | && apk add --no-cache --virtual .mailman-rundeps bash sassc pcre-dev tzdata libldap \ 17 | python3 py3-pip postgresql-client mysql-client py3-mysqlclient \ 18 | curl mailcap xapian-core xapian-bindings-python3 libffi gettext py-cryptography \ 19 | && python3 -m pip install --break-system-packages -U pip setuptools wheel \ 20 | && python3 -m pip install --break-system-packages -U \ 21 | git+https://gitlab.com/mailman/mailmanclient \ 22 | git+https://gitlab.com/mailman/postorius \ 23 | git+https://gitlab.com/mailman/hyperkitty \ 24 | whoosh \ 25 | uwsgi \ 26 | psycopg2 \ 27 | dj-database-url \ 28 | mysqlclient \ 29 | xapian-haystack \ 30 | django-auth-ldap \ 31 | pymemcache \ 32 | tzdata \ 33 | diskcache \ 34 | django-utils-six \ 35 | 'django-allauth[socialaccount,openid]' \ 36 | && python3 -m pip install --break-system-packages -U 'Django<4.3' \ 37 | && python3 -m pip install --break-system-packages -U \ 38 | git+https://gitlab.com/mailman/django-mailman3 \ 39 | && apk del .build-deps \ 40 | && addgroup -S mailman \ 41 | && adduser -S -G mailman mailman 42 | 43 | # Add needed files for uwsgi server + settings for django 44 | COPY mailman-web /opt/mailman-web 45 | # Add startup script to container 46 | COPY docker-entrypoint.sh /usr/local/bin/ 47 | 48 | RUN chown -R mailman /opt/mailman-web/ \ 49 | && chmod u+x /opt/mailman-web/manage.py 50 | 51 | WORKDIR /opt/mailman-web 52 | 53 | # Expose port 8000 for http and port 8080 for uwsgi 54 | # (see web/mailman-web/uwsgi.ini#L2-L4) 55 | EXPOSE 8000 8080 56 | 57 | # Use stop signal for uwsgi server 58 | STOPSIGNAL SIGINT 59 | 60 | ENTRYPOINT ["docker-entrypoint.sh"] 61 | 62 | CMD ["uwsgi", "--ini", "/opt/mailman-web/uwsgi.ini"] 63 | -------------------------------------------------------------------------------- /web/README.md: -------------------------------------------------------------------------------- 1 | # Mailman 3 Web UI 2 | 3 | This image consists of Mailman3's Web UI(Postorius) and Archiver 4 | (Hyperkitty). This image is built from latest sources on [gitlab][1]. In future, 5 | latest and stable releases will be seperate. I am looking forward to the release 6 | of Mailman Suite 3.1 before that. 7 | 8 | ## Configuration 9 | 10 | 11 | These are the settings that you MUST change before deploying: 12 | 13 | - `SERVE_FROM_DOMAIN`: The domain name from which Django will be served. To be 14 | added to `ALLOWED_HOSTS` in django settings. Default value is not set. This 15 | also replaces Django's default `example.com` SITE and becomes the default SITE 16 | (with SITE_ID=1). 17 | 18 | - `HYPERKITTY_API_KEY`: Hyperkitty's API Key, should be set to the same value as 19 | set for the mailman-core. 20 | 21 | - `MAILMAN_ADMIN_USER`: The username for the admin user to be created by default. 22 | 23 | - `MAILMAN_ADMIN_EMAIL`: The email for the admin user to be created by default. 24 | 25 | - `SECRET_KEY`: Django's secret key, mainly used for signing cookies and others. 26 | 27 | These are the settings that are set to sane default and you do not need to 28 | change them unless you know what you want. 29 | 30 | - `DATABASE_URL`: URL of the type 31 | `driver://user:password@hostname:port/databasename` for the django to use. If 32 | not set, the default is set to 33 | `sqlite:///opt/mailman-web-data/mailmanweb.db`. The standard 34 | docker-compose.yaml comes with it set to a postgres database. It is not must 35 | to change this if you are happy with PostgreSQL. 36 | 37 | - `MAILMAN_REST_URL`: The URL to the Mailman core's REST API server. Defaut 38 | value is `http://mailman-core:8001`. 39 | 40 | - `MAILMAN_REST_USER`: Mailman's REST API username. Default value is `restadmin` 41 | 42 | - `MAILMAN_REST_PASSWORD`: Mailman's REST API user's password. Default value is 43 | `restpass` 44 | 45 | - `MAILMAN_HOSTNAME`: IP of the Container from which Mailman will send emails to 46 | hyperkitty (django). Set to `mailman-core` by default. 47 | 48 | - `SMTP_HOST`: IP Address/hostname from which you will be sending 49 | emails. Default value is the container's gateway retrieved from: 50 | /sbin/ip route | awk '/default/ { print $3 }' 51 | 52 | - `SMTP_PORT`: Port used for SMTP. Default is `25`. 53 | 54 | - `SMTP_HOST_USER`: Used for SMTP authentication. Default is an empty string. 55 | 56 | - `SMTP_HOST_PASSWORD`: Default is an empty string. 57 | 58 | - `SMTP_USE_TLS`: Specifies wheather the SMTP connection is encrypted 59 | via TLS. Default is `False`. (`EMAIL_USE_TLS`/`EMAIL_USE_SSL` are mutually exclusive, so only set one of those settings.) 60 | 61 | - `SMTP_USE_SSL`: Specifies wheather the SMTP connection is encrypted 62 | via SSL. Default is `False`. (EMAIL_USE_TLS/EMAIL_USE_SSL are mutually exclusive, so only set one of those settings.) 63 | 64 | - `DJANGO_LOG_URL`: Path to the django's log file. Defaults to 65 | `/opt/mailman-web-data/logs/mailmanweb.log`. 66 | 67 | - `DJANGO_ALLOWED_HOSTS`: Entry to add to ALLOWED_HOSTS in Django 68 | configuration. Format as comma-separated list (no whitespace). This is a separate configuration from`SERVE_FROM_DOMAIN` as 69 | latter is used for other purposes too. 70 | 71 | - `POSTORIUS_TEMPLATE_BASE_URL`: The base url at which the `mailman-web` 72 | container can be reached from `mailman-core` container. This is set to 73 | `http://mailman-web:8000` by default so that Core can fetch templates from 74 | Web. 75 | 76 | - `DISKCACHE_PATH` and `DISKCACHE_SIZE`: Django Diskcache location path and 77 | size respectively. Defaults are `/opt/mailman-web-data/diskcache` and 1G. 78 | 79 | [1]: https://github.com/maxking/docker-mailman/blob/master/web/mailman-web/settings.py 80 | 81 | ## Social Auth 82 | 83 | In order to separate `INSTALLED_APPS` from the social authentication plugins a new settings `MAILMAN_WEB_SOCIAL_AUTH` is created. This includes all the enabled social auth plugins. 84 | 85 | ### Disable social auth 86 | 87 | In order to disable social auth, you can add the following to your 88 | settings_local.py 89 | 90 | ```python 91 | MAILMAN_WEB_SOCIAL_AUTH = [] 92 | ``` 93 | 94 | In older versions of continer images (0.3.*), you had to override 95 | `INSTALLED_APPS` in order to disable social auth, but addition of 96 | this new setting will make it easier to disable social auth making 97 | sure that you get any updates to the django apps that are added in 98 | future. 99 | 100 | The default behavior will remain the same as 0.3 release if you 101 | have not overriden `INSTALLED_APPS` though. 102 | 103 | ## Running 104 | 105 | It is highly recommended that you run this using the [docker-compose.yaml][2] 106 | provided in the [github repo][3] of this project. You will need to proxy the 107 | requests the container that you create with this image using an actual web 108 | server like Nginx. The [github repo][3] provides the setup instructions for 109 | Nginx. 110 | 111 | Since the setup has `USE_SSL` set to `True` in django's `settings.py`, you may 112 | also want to get a SSL certificate if you don't already have one. [Lets 113 | Encrypt][4] provides free SSL certiticates for everyone and there are _some_ 114 | instructions about that also. 115 | 116 | After the first run, you can create a superuser for django using the following 117 | command: 118 | 119 | ```bash 120 | $ docker exec -it mailman-web python3 manage.py createsuperuser 121 | ``` 122 | 123 | ## Django management commands 124 | 125 | In order to run Django management commands in the `mailman-web` container, you 126 | can run following: 127 | 128 | ```bash 129 | $ docker exec -it mailman-web python3 manage.py 130 | ``` 131 | 132 | And replace `` with the appropriate management command. 133 | 134 | 135 | ## Importing Archives from Mailman 2 136 | 137 | In order to import archvies from Mailman 2, you need to get the `listname.mbox` 138 | file in a location that is readable inside `mailman-web` container. 139 | 140 | Please place `listname.mbox` file at `/opt/mailman/web` **on the host**. Verify 141 | that the file is present inside the `mailman-web` contianer by running: 142 | 143 | ```bash 144 | $ docker exec -it mailman-web ls /opt/mailman-web-data 145 | ``` 146 | And verify that you can see `listname.mbox` in the `ls` output above. After you 147 | have verified that, you can then run the `hyperkitty_import` command to do the 148 | actual import: 149 | 150 | ```bash 151 | $ docker exec -it mailman-web python3 manage.py hyperkitty_import -l listname@domain /opt/mailman-web-data/listname.mbox 152 | ``` 153 | 154 | This should take some time to import depending on how many emails are in the 155 | archives. 156 | 157 | 158 | [1]: https://gitlab.com/mailman 159 | [3]: https://github.com/maxking/docker-mailman/ 160 | [2]: https://github.com/maxking/docker-mailman/blob/master/docker-compose.yaml 161 | [4]: https://letsencrypt.org 162 | -------------------------------------------------------------------------------- /web/VERSION: -------------------------------------------------------------------------------- 1 | 0.1.1 2 | -------------------------------------------------------------------------------- /web/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | set -e 3 | 4 | 5 | function wait_for_postgres () { 6 | # Check if the postgres database is up and accepting connections before 7 | # moving forward. 8 | # TODO: Use python's psycopg2 module to do this in python instead of 9 | # installing postgres-client in the image. 10 | until psql -P pager=off $DATABASE_URL -c '\l'; do 11 | >&2 echo "Postgres is unavailable - sleeping" 12 | sleep 1 13 | done 14 | >&2 echo "Postgres is up - continuing" 15 | } 16 | 17 | function wait_for_mysql () { 18 | # Check if MySQL is up and accepting connections. 19 | readarray -d' ' -t ENDPOINT <<< $(python3 -c "from urllib.parse import urlparse; o = urlparse('$DATABASE_URL'); print('%s %s' % (o.hostname, o.port if o.port else '3306'));") 20 | until mysqladmin ping --host ${ENDPOINT[0]} --port ${ENDPOINT[1]} --silent; do 21 | >&2 echo "MySQL is unavailable - sleeping" 22 | sleep 1 23 | done 24 | >&2 echo "MySQL is up - continuing" 25 | } 26 | 27 | function check_or_create () { 28 | # Check if the path exists, if not, create the directory. 29 | if [[ ! -e dir ]]; then 30 | echo "$1 does not exist, creating ..." 31 | mkdir "$1" 32 | fi 33 | } 34 | 35 | # function postgres_ready(){ 36 | # python << END 37 | # import sys 38 | # import psycopg2 39 | # try: 40 | # conn = psycopg2.connect(dbname="$POSTGRES_DB", user="$POSTGRES_USER", password="$POSTGRES_PASSWORD", host="postgres") 41 | # except psycopg2.OperationalError: 42 | # sys.exit(-1) 43 | # sys.exit(0) 44 | # END 45 | # } 46 | 47 | # SMTP_HOST defaults to the gateway 48 | if [[ ! -v SMTP_HOST ]]; then 49 | export SMTP_HOST=$(/sbin/ip route | awk '/default/ { print $3 }') 50 | fi 51 | 52 | # Check if $SECRET_KEY is defined, if not, bail out. 53 | if [[ ! -v SECRET_KEY ]]; then 54 | echo "SECRET_KEY is not defined. Aborting." 55 | exit 1 56 | fi 57 | 58 | # Check if $DATABASE_URL is defined, if not, use a standard sqlite database. 59 | # 60 | # If the $DATABASE_URL is defined and is postgres, check if it is available 61 | # yet. Do not start the container before the postgresql boots up. 62 | # 63 | # If the $DATABASE_URL is defined and is mysql, check if the database is 64 | # available before the container boots up. 65 | # 66 | # TODO: Check the database type and detect if it is up based on that. For now, 67 | # assume that postgres is being used if DATABASE_URL is defined. 68 | 69 | if [[ ! -v DATABASE_URL ]]; then 70 | echo "DATABASE_URL is not defined. Using sqlite database..." 71 | export DATABASE_URL=sqlite:////opt/mail-web-data/mailmanweb.db 72 | export DATABASE_TYPE='sqlite' 73 | fi 74 | 75 | if [[ "$DATABASE_TYPE" = 'postgres' ]] 76 | then 77 | wait_for_postgres 78 | elif [[ "$DATABASE_TYPE" = 'mysql' ]] 79 | then 80 | wait_for_mysql 81 | fi 82 | 83 | # Check if we are in the correct directory before running commands. 84 | if [[ ! $(pwd) == '/opt/mailman-web' ]]; then 85 | echo "Running in the wrong directory...switching to /opt/mailman-web" 86 | cd /opt/mailman-web 87 | fi 88 | 89 | # Check if the logs directory is setup. 90 | if [[ ! -e /opt/mailman-web-data/logs/mailmanweb.log ]]; then 91 | echo "Creating log file for mailman web" 92 | mkdir -p /opt/mailman-web-data/logs/ 93 | touch /opt/mailman-web-data/logs/mailmanweb.log 94 | fi 95 | 96 | if [[ ! -e /opt/mailman-web-data/logs/uwsgi.log ]]; then 97 | echo "Creating log file for uwsgi.." 98 | touch /opt/mailman-web-data/logs/uwsgi.log 99 | fi 100 | 101 | # Check if the settings_local.py file exists, if yes, copy it too. 102 | if [[ -e /opt/mailman-web-data/settings_local.py ]]; then 103 | echo "Copying settings_local.py ..." 104 | cp /opt/mailman-web-data/settings_local.py /opt/mailman-web/settings_local.py 105 | chown mailman:mailman /opt/mailman-web/settings_local.py 106 | else 107 | echo "settings_local.py not found, it is highly recommended that you provide one" 108 | echo "Using default configuration to run." 109 | fi 110 | 111 | # Collect static for the django installation. 112 | python3 manage.py collectstatic --noinput --clear --verbosity 0 113 | 114 | 115 | # Compile all the installed po files to mo. 116 | SITE_DIR=$(python3 -c 'import site; print(site.getsitepackages()[0])') 117 | echo "Compiling locale files in $SITE_DIR" 118 | cd $SITE_DIR && /opt/mailman-web/manage.py compilemessages && cd - 119 | 120 | # Compress static files. 121 | python3 /opt/mailman-web/manage.py compress --force 122 | 123 | 124 | # Migrate all the data to the database if this is a new installation, otherwise 125 | # this command will upgrade the database. 126 | python3 /opt/mailman-web/manage.py migrate 127 | 128 | # If MAILMAN_ADMIN_USER and MAILMAN_ADMIN_EMAIL is defined create a new 129 | # superuser for Django. There is no password setup so it can't login yet unless 130 | # the password is reset. 131 | if [[ -v MAILMAN_ADMIN_USER ]] && [[ -v MAILMAN_ADMIN_EMAIL ]]; 132 | then 133 | echo "Creating admin user $MAILMAN_ADMIN_USER ..." 134 | python3 /opt/mailman-web/manage.py createsuperuser --noinput --username "$MAILMAN_ADMIN_USER"\ 135 | --email "$MAILMAN_ADMIN_EMAIL" 2> /dev/null || \ 136 | echo "Superuser $MAILMAN_ADMIN_USER already exists" 137 | fi 138 | 139 | # If SERVE_FROM_DOMAIN is defined then rename the default `example.com` 140 | # domain to the defined domain. 141 | if [[ -v SERVE_FROM_DOMAIN ]]; 142 | then 143 | echo "Setting $SERVE_FROM_DOMAIN as the default domain ..." 144 | python3 /opt/mailman-web/manage.py shell -c \ 145 | "from django.contrib.sites.models import Site; Site.objects.filter(domain='example.com').update(domain='$SERVE_FROM_DOMAIN', name='$SERVE_FROM_DOMAIN')" 146 | fi 147 | 148 | # Create a mailman user with the specific UID and GID and do not create home 149 | # directory for it. Also chown the logs directory to write the files. 150 | chown mailman:mailman /opt/mailman-web-data -R 151 | 152 | [[ -v DISKCACHE_PATH ]] && chown mailman:mailman "${DISKCACHE_PATH}" -R 153 | 154 | exec $@ 155 | -------------------------------------------------------------------------------- /web/mailman-web/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/maxking/docker-mailman/5b4202dd7622a6ba11b215a6b638f7b11a03f419/web/mailman-web/__init__.py -------------------------------------------------------------------------------- /web/mailman-web/manage.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import os 3 | import sys 4 | 5 | if __name__ == "__main__": 6 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 7 | 8 | from django.core.management import execute_from_command_line 9 | 10 | execute_from_command_line(sys.argv) 11 | -------------------------------------------------------------------------------- /web/mailman-web/settings.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) 1998-2016 by the Free Software Foundation, Inc. 3 | # 4 | # This file is part of Mailman Suite. 5 | # 6 | # Mailman Suite is free sofware: you can redistribute it and/or modify it 7 | # under the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) 9 | # any later version. 10 | # 11 | # Mailman Suite is distributed in the hope that it will be useful, but 12 | # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 13 | # or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 | # for more details. 15 | 16 | # You should have received a copy of the GNU General Public License along 17 | # with Mailman Suite. If not, see . 18 | """ 19 | Django Settings for Mailman Suite (hyperkitty + postorius) 20 | 21 | For more information on this file, see 22 | https://docs.djangoproject.com/en/1.8/topics/settings/ 23 | 24 | For the full list of settings and their values, see 25 | https://docs.djangoproject.com/en/1.8/ref/settings/ 26 | """ 27 | 28 | # Build paths inside the project like this: os.path.join(BASE_DIR, ...) 29 | import os 30 | import dj_database_url 31 | import sys 32 | from socket import gethostbyname, gaierror 33 | 34 | BASE_DIR = os.path.dirname(os.path.abspath(__file__)) 35 | 36 | # SECURITY WARNING: keep the secret key used in production secret! 37 | SECRET_KEY = os.environ.get('SECRET_KEY') 38 | 39 | # SECURITY WARNING: don't run with debug turned on in production! 40 | DEBUG = False 41 | 42 | ADMINS = ( 43 | ('Mailman Suite Admin', 'root@localhost'), 44 | ) 45 | 46 | SITE_ID = 1 47 | 48 | # Hosts/domain names that are valid for this site; required if DEBUG is False 49 | # See https://docs.djangoproject.com/en/3.1/ref/settings/#allowed-hosts 50 | ALLOWED_HOSTS = [ 51 | "localhost", # Archiving API from Mailman, keep it. 52 | "mailman-web", 53 | os.environ.get('SERVE_FROM_DOMAIN'), 54 | ] 55 | 56 | try: 57 | ALLOWED_HOSTS.append(gethostbyname("mailman-web")) # only add if this resolves 58 | except gaierror: 59 | pass 60 | 61 | ALLOWED_HOSTS.extend(os.getenv("DJANGO_ALLOWED_HOSTS", "").split(",")) 62 | 63 | # Mailman API credentials 64 | MAILMAN_REST_API_URL = os.environ.get('MAILMAN_REST_URL', 'http://mailman-core:8001') 65 | MAILMAN_REST_API_USER = os.environ.get('MAILMAN_REST_USER', 'restadmin') 66 | MAILMAN_REST_API_PASS = os.environ.get('MAILMAN_REST_PASSWORD', 'restpass') 67 | MAILMAN_ARCHIVER_KEY = os.environ.get('HYPERKITTY_API_KEY') 68 | MAILMAN_ARCHIVER_FROM = (os.environ.get('MAILMAN_HOST_IP', gethostbyname(os.environ.get('MAILMAN_HOSTNAME', 'mailman-core'))),) 69 | 70 | # Application definition 71 | 72 | INSTALLED_APPS = [] 73 | DEFAULT_APPS = [ 74 | 'hyperkitty', 75 | 'postorius', 76 | 'django_mailman3', 77 | # Uncomment the next line to enable the admin: 78 | 'django.contrib.admin', 79 | # Uncomment the next line to enable admin documentation: 80 | # 'django.contrib.admindocs', 81 | 'django.contrib.auth', 82 | 'django.contrib.contenttypes', 83 | 'django.contrib.sessions', 84 | 'django.contrib.sites', 85 | 'django.contrib.messages', 86 | 'django.contrib.staticfiles', 87 | 'django.contrib.humanize', 88 | 'rest_framework', 89 | 'django_gravatar', 90 | 'compressor', 91 | 'haystack', 92 | 'django_extensions', 93 | 'django_q', 94 | 'allauth', 95 | 'allauth.account', 96 | 'allauth.socialaccount', 97 | ] 98 | 99 | MAILMAN_WEB_SOCIAL_AUTH = [ 100 | 'django_mailman3.lib.auth.fedora', 101 | 'allauth.socialaccount.providers.openid', 102 | 'allauth.socialaccount.providers.github', 103 | 'allauth.socialaccount.providers.gitlab', 104 | 'allauth.socialaccount.providers.google', 105 | ] 106 | 107 | MIDDLEWARE = ( 108 | 'django.contrib.sessions.middleware.SessionMiddleware', 109 | 'django.middleware.common.CommonMiddleware', 110 | 'django.middleware.csrf.CsrfViewMiddleware', 111 | 'django.middleware.locale.LocaleMiddleware', 112 | 'django.contrib.auth.middleware.AuthenticationMiddleware', 113 | 'django.contrib.messages.middleware.MessageMiddleware', 114 | 'django.middleware.clickjacking.XFrameOptionsMiddleware', 115 | 'django.middleware.security.SecurityMiddleware', 116 | 'allauth.account.middleware.AccountMiddleware', 117 | 'django_mailman3.middleware.TimezoneMiddleware', 118 | 'postorius.middleware.PostoriusMiddleware', 119 | ) 120 | 121 | ROOT_URLCONF = 'urls' 122 | 123 | TEMPLATES = [ 124 | { 125 | 'BACKEND': 'django.template.backends.django.DjangoTemplates', 126 | 'DIRS': [], 127 | 'APP_DIRS': True, 128 | 'OPTIONS': { 129 | 'context_processors': [ 130 | 'django.template.context_processors.debug', 131 | 'django.template.context_processors.i18n', 132 | 'django.template.context_processors.media', 133 | 'django.template.context_processors.static', 134 | 'django.template.context_processors.tz', 135 | 'django.template.context_processors.csrf', 136 | 'django.template.context_processors.request', 137 | 'django.contrib.auth.context_processors.auth', 138 | 'django.contrib.messages.context_processors.messages', 139 | 'django_mailman3.context_processors.common', 140 | 'hyperkitty.context_processors.common', 141 | 'postorius.context_processors.postorius', 142 | ], 143 | }, 144 | }, 145 | ] 146 | 147 | WSGI_APPLICATION = 'wsgi.application' 148 | 149 | 150 | # Database 151 | # https://docs.djangoproject.com/en/1.8/ref/settings/#databases 152 | # dj_database_url uses $DATABASE_URL environment variable to create a 153 | # django-style-config-dict. 154 | # https://github.com/kennethreitz/dj-database-url 155 | DATABASES = { 156 | 'default': dj_database_url.config(conn_max_age=600) 157 | } 158 | 159 | # Avoid Django 3.2+ warning 160 | # https://github.com/maxking/docker-mailman/issues/595 161 | DEFAULT_AUTO_FIELD = 'django.db.models.AutoField' 162 | 163 | 164 | # If you're behind a proxy, use the X-Forwarded-Host header 165 | # See https://docs.djangoproject.com/en/1.8/ref/settings/#use-x-forwarded-host 166 | USE_X_FORWARDED_HOST = True 167 | 168 | 169 | # Password validation 170 | # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators 171 | AUTH_PASSWORD_VALIDATORS = [ 172 | { 173 | 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', 174 | }, 175 | { 176 | 'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator', 177 | }, 178 | { 179 | 'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator', 180 | }, 181 | { 182 | 'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator', 183 | }, 184 | ] 185 | 186 | # Internationalization 187 | # https://docs.djangoproject.com/en/1.8/topics/i18n/ 188 | 189 | LANGUAGE_CODE = 'en-us' 190 | 191 | TIME_ZONE = os.environ.get('TZ', 'UTC') 192 | 193 | USE_I18N = True 194 | 195 | USE_L10N = True 196 | 197 | USE_TZ = True 198 | 199 | STATIC_ROOT = '/opt/mailman-web-data/static' 200 | 201 | STATIC_URL = '/static/' 202 | 203 | # Additional locations of static files 204 | 205 | 206 | # List of finder classes that know how to find static files in 207 | # various locations. 208 | STATICFILES_FINDERS = ( 209 | 'django.contrib.staticfiles.finders.FileSystemFinder', 210 | 'django.contrib.staticfiles.finders.AppDirectoriesFinder', 211 | 'compressor.finders.CompressorFinder', 212 | ) 213 | 214 | 215 | SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer' 216 | 217 | LOGIN_URL = 'account_login' 218 | LOGIN_REDIRECT_URL = 'list_index' 219 | LOGOUT_URL = 'account_logout' 220 | 221 | 222 | # Use SERVE_FROM_DOMAIN as the default domain in the email. 223 | hostname = os.environ.get('SERVE_FROM_DOMAIN', 'localhost.local') 224 | DEFAULT_FROM_EMAIL = 'postorius@{}'.format(hostname) 225 | SERVER_EMAIL = 'root@{}'.format(hostname) 226 | 227 | # Change this when you have a real email backend 228 | EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend' 229 | EMAIL_HOST = os.environ.get('SMTP_HOST', '') 230 | EMAIL_PORT = os.environ.get('SMTP_PORT', 25) 231 | EMAIL_HOST_USER = os.environ.get('SMTP_HOST_USER', '') 232 | EMAIL_HOST_PASSWORD = os.environ.get('SMTP_HOST_PASSWORD', '') 233 | EMAIL_USE_TLS = (os.environ.get('SMTP_USE_TLS', 'false').lower() == 'true') 234 | EMAIL_USE_SSL = (os.environ.get('SMTP_USE_SSL', 'false').lower() == 'true') 235 | 236 | # Compatibility with Bootstrap 3 237 | from django.contrib.messages import constants as messages # flake8: noqa 238 | MESSAGE_TAGS = { 239 | messages.ERROR: 'danger' 240 | } 241 | 242 | 243 | # 244 | # Social auth 245 | # 246 | AUTHENTICATION_BACKENDS = ( 247 | 'django.contrib.auth.backends.ModelBackend', 248 | 'allauth.account.auth_backends.AuthenticationBackend', 249 | ) 250 | 251 | # Django Allauth 252 | ACCOUNT_AUTHENTICATION_METHOD = "username_email" 253 | ACCOUNT_EMAIL_REQUIRED = True 254 | ACCOUNT_EMAIL_VERIFICATION = "mandatory" 255 | # You probably want https in production, but this is a dev setup file 256 | ACCOUNT_DEFAULT_HTTP_PROTOCOL = "https" 257 | ACCOUNT_UNIQUE_EMAIL = True 258 | 259 | SOCIALACCOUNT_PROVIDERS = { 260 | 'openid': { 261 | 'SERVERS': [ 262 | dict(id='yahoo', 263 | name='Yahoo', 264 | openid_url='http://me.yahoo.com'), 265 | ], 266 | }, 267 | 'google': { 268 | 'SCOPE': ['profile', 'email'], 269 | 'AUTH_PARAMS': {'access_type': 'online'}, 270 | }, 271 | 'facebook': { 272 | 'METHOD': 'oauth2', 273 | 'SCOPE': ['email'], 274 | 'FIELDS': [ 275 | 'email', 276 | 'name', 277 | 'first_name', 278 | 'last_name', 279 | 'locale', 280 | 'timezone', 281 | ], 282 | 'VERSION': 'v2.4', 283 | }, 284 | } 285 | 286 | 287 | # django-compressor 288 | # https://pypi.python.org/pypi/django_compressor 289 | # 290 | COMPRESS_PRECOMPILERS = ( 291 | ('text/less', 'lessc {infile} {outfile}'), 292 | ('text/x-scss', 'sassc -t compressed {infile} {outfile}'), 293 | ('text/x-sass', 'sassc -t compressed {infile} {outfile}'), 294 | ) 295 | 296 | # On a production setup, setting COMPRESS_OFFLINE to True will bring a 297 | # significant performance improvement, as CSS files will not need to be 298 | # recompiled on each requests. It means running an additional "compress" 299 | # management command after each code upgrade. 300 | # http://django-compressor.readthedocs.io/en/latest/usage/#offline-compression 301 | # COMPRESS_OFFLINE = True 302 | 303 | # 304 | # Full-text search engine 305 | # 306 | HAYSTACK_CONNECTIONS = { 307 | 'default': { 308 | 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine', 309 | 'PATH': "/opt/mailman-web-data/fulltext_index", 310 | # You can also use the Xapian engine, it's faster and more accurate, 311 | # but requires another library. 312 | # http://django-haystack.readthedocs.io/en/v2.4.1/installing_search_engines.html#xapian 313 | # Example configuration for Xapian: 314 | #'ENGINE': 'xapian_backend.XapianEngine' 315 | }, 316 | } 317 | 318 | import sys 319 | # A sample logging configuration. The only tangible logging 320 | # performed by this configuration is to send an email to 321 | # the site admins on every HTTP 500 error when DEBUG=False. 322 | # See http://docs.djangoproject.com/en/dev/topics/logging for 323 | # more details on how to customize your logging configuration. 324 | LOGGING = { 325 | 'version': 1, 326 | 'disable_existing_loggers': False, 327 | 'filters': { 328 | 'require_debug_false': { 329 | '()': 'django.utils.log.RequireDebugFalse' 330 | } 331 | }, 332 | 'handlers': { 333 | 'mail_admins': { 334 | 'level': 'ERROR', 335 | 'filters': ['require_debug_false'], 336 | 'class': 'django.utils.log.AdminEmailHandler' 337 | }, 338 | 'file':{ 339 | 'level': 'INFO', 340 | 'class': 'logging.handlers.RotatingFileHandler', 341 | #'class': 'logging.handlers.WatchedFileHandler', 342 | 'filename': os.environ.get('DJANGO_LOG_URL','/opt/mailman-web-data/logs/mailmanweb.log'), 343 | 'formatter': 'verbose', 344 | }, 345 | 'console': { 346 | 'class': 'logging.StreamHandler', 347 | 'formatter': 'simple', 348 | 'level': 'INFO', 349 | 'stream': sys.stdout, 350 | }, 351 | }, 352 | 'loggers': { 353 | 'django.request': { 354 | 'handlers': ['mail_admins', 'file'], 355 | 'level': 'INFO', 356 | 'propagate': True, 357 | }, 358 | 'django': { 359 | 'handlers': ['file'], 360 | 'level': 'INFO', 361 | 'propagate': True, 362 | }, 363 | 'hyperkitty': { 364 | 'handlers': ['file'], 365 | 'level': 'INFO', 366 | 'propagate': True, 367 | }, 368 | 'postorius': { 369 | 'handlers': ['file'], 370 | 'level': 'INFO', 371 | 'propagate': True 372 | }, 373 | }, 374 | 'formatters': { 375 | 'verbose': { 376 | 'format': '%(levelname)s %(asctime)s %(process)d %(name)s %(message)s' 377 | }, 378 | 'simple': { 379 | 'format': '%(levelname)s %(message)s' 380 | }, 381 | }, 382 | } 383 | 384 | 385 | if os.environ.get('LOG_TO_CONSOLE') == 'yes': 386 | LOGGING['loggers']['django']['handlers'].append('console') 387 | LOGGING['loggers']['django.request']['handlers'].append('console') 388 | 389 | # HyperKitty-specific 390 | # 391 | # Only display mailing-lists from the same virtual host as the webserver 392 | FILTER_VHOST = False 393 | 394 | 395 | Q_CLUSTER = { 396 | 'timeout': 300, 397 | 'retry': 300, 398 | 'save_limit': 100, 399 | 'orm': 'default', 400 | } 401 | 402 | POSTORIUS_TEMPLATE_BASE_URL = os.environ.get('POSTORIUS_TEMPLATE_BASE_URL', 'http://mailman-web:8000') 403 | 404 | DISKCACHE_PATH = os.environ.get('DISKCACHE_PATH', '/opt/mailman-web-data/diskcache') 405 | DISKCACHE_SIZE = os.environ.get('DISKCACHE_SIZE', 2 ** 30) # 1 gigabyte 406 | 407 | CACHES = { 408 | 'default': { 409 | 'BACKEND': 'diskcache.DjangoCache', 410 | 'LOCATION': DISKCACHE_PATH, 411 | 'OPTIONS': { 412 | 'size_limit': DISKCACHE_SIZE, 413 | }, 414 | }, 415 | } 416 | 417 | try: 418 | from settings_local import * 419 | except ImportError: 420 | pass 421 | 422 | # Compatibility for older installs that override INSTALLED_APPS 423 | if not INSTALLED_APPS: 424 | INSTALLED_APPS = DEFAULT_APPS + MAILMAN_WEB_SOCIAL_AUTH 425 | -------------------------------------------------------------------------------- /web/mailman-web/urls.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Copyright (C) 2023 by the Free Software Foundation, Inc. 3 | # 4 | # This file is part of mailman-web. 5 | # 6 | # Postorius is free software: you can redistribute it and/or modify it under 7 | # the terms of the GNU General Public License as published by the Free 8 | # Software Foundation, either version 3 of the License, or (at your option) 9 | # any later version. 10 | # 11 | # Postorius is distributed in the hope that it will be useful, but WITHOUT 12 | # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 | # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for 14 | # more details. 15 | # 16 | # You should have received a copy of the GNU General Public License along with 17 | # Postorius. If not, see . 18 | 19 | 20 | from django.conf.urls import include 21 | from django.contrib import admin 22 | from django.urls import path, reverse_lazy 23 | from django.views.generic import RedirectView 24 | 25 | urlpatterns = [ 26 | path( 27 | "", 28 | RedirectView.as_view(url=reverse_lazy("list_index"), permanent=True), 29 | ), 30 | # Include alternate Postorius and HyperKitty URLs. 31 | path("postorius/", include("postorius.urls")), 32 | path("hyperkitty/", include("hyperkitty.urls")), 33 | # Order counts for various links. Put the above first and the following 34 | # after so the suggested Apache config still works. 35 | path("mailman3/", include("postorius.urls")), 36 | path("archives/", include("hyperkitty.urls")), 37 | path("", include("django_mailman3.urls")), 38 | path("accounts/", include("allauth.urls")), 39 | path("admin/", admin.site.urls), 40 | ] 41 | -------------------------------------------------------------------------------- /web/mailman-web/uwsgi.ini: -------------------------------------------------------------------------------- 1 | [uwsgi] 2 | # Port on which uwsgi will be listening. 3 | uwsgi-socket = 0.0.0.0:8080 4 | http-socket = 0.0.0.0:8000 5 | 6 | # Enable threading for python 7 | enable-threads = true 8 | 9 | # Setting uwsgi buffer size to what Apache2 supports. 10 | buffer-size = 8190 11 | 12 | # Move to the directory where the django files are. 13 | chdir = /opt/mailman-web 14 | 15 | # Use the wsgi file provided with the django project. 16 | wsgi-file = wsgi.py 17 | 18 | # Setup default number of processes and threads per process. 19 | master = true 20 | processes = 2 21 | threads = 2 22 | 23 | # Drop privileges and don't run as root. 24 | uid = mailman 25 | gid = mailman 26 | 27 | # Setup the django_q related worker processes. 28 | attach-daemon = ./manage.py qcluster 29 | 30 | # Setup hyperkitty's cron jobs. 31 | # 'minutely' jobs are run hourly for perf reasons. 32 | # See https://github.com/maxking/docker-mailman/issues/327 33 | unique-cron = 0 -1 -1 -1 -1 ./manage.py runjobs minutely 34 | unique-cron = -15 -1 -1 -1 -1 ./manage.py runjobs quarter_hourly 35 | unique-cron = 0 -1 -1 -1 -1 ./manage.py runjobs hourly 36 | unique-cron = 0 0 -1 -1 -1 ./manage.py runjobs daily 37 | unique-cron = 0 0 1 -1 -1 ./manage.py runjobs monthly 38 | unique-cron = 0 0 -1 -1 0 ./manage.py runjobs weekly 39 | unique-cron = 0 0 1 1 -1 ./manage.py runjobs yearly 40 | 41 | # Setup the request log. 42 | req-logger = file:/opt/mailman-web-data/logs/uwsgi.log 43 | 44 | # Log cron seperately. 45 | logger = cron file:/opt/mailman-web-data/logs/uwsgi-cron.log 46 | log-route = cron uwsgi-cron 47 | 48 | # Log qcluster commands seperately. 49 | logger = qcluster file:/opt/mailman-web-data/logs/uwsgi-qcluster.log 50 | log-route = qcluster uwsgi-daemons 51 | 52 | # Last log and it logs the rest of the stuff. 53 | logger = file:/opt/mailman-web-data/logs/uwsgi-error.log 54 | -------------------------------------------------------------------------------- /web/mailman-web/wsgi.py: -------------------------------------------------------------------------------- 1 | """ 2 | WSGI config for HyperKitty project. 3 | 4 | It exposes the WSGI callable as a module-level variable named ``application``. 5 | 6 | For more information on this file, see 7 | https://docs.djangoproject.com/en/{{ docs_version }}/howto/deployment/wsgi/ 8 | """ 9 | 10 | import os 11 | 12 | # import sys 13 | # import site 14 | 15 | # For some unknown reason, sometimes mod_wsgi fails to set the python paths to 16 | # the virtualenv, with the 'python-path' option. You can do it here too. 17 | # 18 | # # Remember original sys.path. 19 | # prev_sys_path = list(sys.path) 20 | # # Add here, for the settings module 21 | # site.addsitedir(os.path.abspath(os.path.dirname(__file__))) 22 | # # Add the virtualenv 23 | # venv = os.path.join(os.path.abspath(os.path.dirname(__file__)), 24 | # '..', 'lib', 'python2.6', 'site-packages') 25 | # site.addsitedir(venv) 26 | # # Reorder sys.path so new directories at the front. 27 | # new_sys_path = [] 28 | # for item in list(sys.path): 29 | # if item not in prev_sys_path: 30 | # new_sys_path.append(item) 31 | # sys.path.remove(item) 32 | # sys.path[:0] = new_sys_path 33 | 34 | from django.core.wsgi import get_wsgi_application 35 | 36 | os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 37 | 38 | application = get_wsgi_application() 39 | -------------------------------------------------------------------------------- /web/requirements.txt: -------------------------------------------------------------------------------- 1 | mailmanclient==3.3.5 2 | postorius==1.3.13 3 | hyperkitty==1.3.12 4 | django-mailman3==1.3.15 --------------------------------------------------------------------------------