diff --git a/library/roles/mailman/defaults/main.yml b/library/roles/mailman/defaults/main.yml
new file mode 100644
index 00000000..7c07f44c
--- /dev/null
+++ b/library/roles/mailman/defaults/main.yml
@@ -0,0 +1,256 @@
+---
+
+# TODO: make sure you point out to set django secret_key, django admins, django archive key?
+
+mailman3_install_method: pip
+mailman3_language: en
+mailman3_language_code: en-us
+mailman3_install_system_dependencies: "{{ __mailman3_debian or __mailman3_redhat }}"
+mailman3_python_uwsgi_package: pyuwsgi
+mailman3_backup_configs: false
+# list of dicts with keys `name`, `email`, `pass`
+mailman3_django_superusers: []
+# list of hosted domains
+#mailman3_domains: []
+# If mailman3_domains is set, mailman3_config.default_from_email is ignored since it's assumed you want per-domain
+# addresses. In this case, set the username portion of the email (the domain will be added automatically)
+mailman3_default_from_user: postorius
+
+# Distribute Postfix maps to MXs for use with relay_recipient_maps, so that MXs can reject mail to nonexistent
+# addresses. Installs Ansible and a playbook in a virtualenv. User/auth setup is up to you.
+#
+# list of dicts, required keys:
+# host: inventory_hostname of mx (or "all") for vars to apply to all hosts
+# mailman3_distribute_maps_dir: remote directory on mx to distribute maps to
+# all keys other than "host" are set as either host vars or in [all:vars] if host = "all"
+#mailman3_distribute_maps: []
+mailman3_distribute_maps_dir: "{{ mailman3_var_dir }}/distribute_maps"
+
+# For pip installs, the role creates a venv at this path
+mailman3_install_dir: /opt/mailman3
+
+# uWSGI/proxy communication socket (value only used for pip installs, Debian uses a hardcoded default)
+mailman3_uwsgi_socket: "{{ mailman3_django_var_dir }}/run/uwsgi.sock"
+
+# Optionally serve directly with uWSGI
+#mailman3_http_socket:
+mailman3_uwsgi_static: no
+
+# You should rarely need to set these
+#mailman3_virtualenv_python: python3
+#mailman3_virtualenv_command: python3 -m venv # https://github.com/ansible/ansible/issues/52275
+mailman3_virtualenv_command: pyvenv
+
+__mailman3_debian: "{{ ansible_os_family == 'Debian' }}"
+__mailman3_redhat: "{{ ansible_os_family == 'RedHat' }}"
+__mailman3_pip: "{{ mailman3_install_method == 'pip' }}"
+
+# pip needed packages if using the pip install method, system packages if using the package method
+__mailman3_pip_packages:
+ - whoosh
+ - django>=1.11
+ - mailman
+ - postorius
+ - hyperkitty
+ - mailman-hyperkitty
+ - "{{ mailman3_python_uwsgi_package }}"
+__mailman3_debian_packages:
+ - mailman3-full
+__mailman3_redhat_packages: null # currently nonexistent
+mailman3_packages: >-
+ {{
+ __mailman3_pip_packages if __mailman3_pip else (
+ __mailman3_debian_packages if __mailman3_debian else
+ __mailman3_redhat_packages)
+ }}
+# for e.g. psycopg2
+mailman3_extra_packages: []
+
+# Dependant system packages needed if using the pip install method
+__mailman3_debian_system_dependency_packages:
+ - python3 # requires Ubuntu >= 16.04, Debian >= stretch (for 3.5)
+ - python3-setuptools # Ansible pip module needs this despite having venv; UPDATE: no it doesn't if it can find python3; UPDATE2: well now it does again, wtf
+ - python3-venv
+ - sassc
+ - uwsgi
+ - uwsgi-plugin-python3
+__mailman3_redhat_system_dependency_packages:
+ # all require EPEL
+ - python36 # requires EL7+
+ - sassc
+ - uwsgi
+ - uwsgi-plugin-python36
+# TODO: as of `date`, compilers + python headers are required for these packages that don't have published cext wheels:
+# - rcssmin
+# - rjsmin
+# But this role will not install compilers in case wheels become available at a later date
+mailman3_system_dependency_packages: >-
+ {{
+ __mailman3_debian_system_dependency_packages if __mailman3_debian else
+ __mailman3_redhat_system_dependency_packages
+ }}
+
+# TODO: supervisor
+mailman3_process_manager: >-
+ {{
+ 'systemd' if ansible_virtualization_type != 'docker' else None
+ }}
+
+mailman3_core_service_name: >-
+ {{
+ 'mailman3-core' if __mailman3_pip else (
+ 'mailman3' if __mailman3_debian else
+ None)
+ }}
+
+mailman3_web_service_name: >-
+ {{
+ 'mailman3-web' if __mailman3_pip else (
+ 'mailman3-web' if __mailman3_debian else
+ None)
+ }}
+
+mailman3_etc_dir: >-
+ {{
+ '/etc/opt/mailman3' if __mailman3_pip else (
+ '/etc/mailman3' if __mailman3_debian else
+ None)
+ }}
+
+mailman3_var_dir: >-
+ {{
+ '/var/opt/mailman3/core' if __mailman3_pip else (
+ '/var/lib/mailman3' if __mailman3_debian else
+ None)
+ }}
+
+mailman3_log_dir: >-
+ {{
+ '/var/opt/mailman3/core/log' if __mailman3_pip else (
+ '/var/log/mailman3' if __mailman3_debian else
+ None)
+ }}
+
+mailman3_django_var_dir: >-
+ {{
+ '/var/opt/mailman3/web' if __mailman3_pip else (
+ '/var/lib/mailman3' if __mailman3_debian else
+ None)
+ }}
+
+mailman3_django_project_dir: >-
+ {{
+ '/var/opt/mailman3/web/project' if __mailman3_pip else (
+ '/usr/share/mailman3-web' if __mailman3_debian else
+ None)
+ }}
+
+mailman3_django_static_dir: >-
+ {{
+ '/var/opt/mailman3/web/static' if __mailman3_pip else (
+ '/var/lib/mailman3/web/static' if __mailman3_debian else
+ None)
+ }}
+
+mailman3_django_log_dir: >-
+ {{
+ '/var/opt/mailman3/web/log' if __mailman3_pip else (
+ '/var/log/mailman3/web' if __mailman3_debian else
+ None)
+ }}
+
+mailman3_django_settings_file: >-
+ {{
+ '/etc/opt/mailman3/django-settings.py' if __mailman3_pip else (
+ '/etc/mailman3/mailman-web.py' if __mailman3_debian else
+ None)
+ }}
+
+mailman3_web_user: >-
+ {{
+ 'www-data' if __mailman3_debian else (
+ None)
+ }}
+# TODO:
+#'httpd' if __mailman3_redhat and apache
+#'nginx' if __mailman3_redhat and nginx
+
+mailman3_web_group: >-
+ {{
+ 'www-data' if __mailman3_debian else (
+ None)
+ }}
+
+mailman3_core_api_hostname: localhost
+mailman3_core_api_port: 8001
+mailman3_core_api_admin_user: restadmin
+mailman3_core_api_admin_pass: restpass
+mailman3_archiver_key: SecretArchiverAPIKey
+
+__mailman3_config_default:
+ mailman:
+ layout: custom
+ paths.custom:
+ var_dir: "{{ mailman3_var_dir }}"
+ bin_dir: "$argv"
+ log_dir: "{{ mailman3_log_dir }}"
+ lock_dir: "{{ mailman3_var_dir }}/locks"
+ data_dir: "{{ mailman3_var_dir }}/data"
+ cache_dir: "{{ mailman3_var_dir }}/cache"
+ etc_dir: "{{ mailman3_etc_dir }}"
+ messages_dir: "{{ mailman3_var_dir }}/messages"
+ archives_dir: "{{ mailman3_var_dir }}/archives"
+ template_dir: "{{ mailman3_var_dir }}/templates"
+ pid_file: "{{ mailman3_var_dir }}/master.pid"
+ lock_file: "{{ mailman3_var_dir }}/master.lck"
+ webservice:
+ hostname: "{{ mailman3_core_api_hostname }}"
+ port: "{{ mailman3_core_api_port }}"
+ use_https: "no"
+ admin_user: "{{ mailman3_core_api_admin_user }}"
+ admin_pass: "{{ mailman3_core_api_admin_pass }}"
+ api_version: "3.1"
+ archiver.hyperkitty:
+ class: mailman_hyperkitty.Archiver
+ enable: "yes"
+ configuration: "{{ mailman3_etc_dir }}/hyperkitty.cfg"
+__mailman3_config_merged: "{{ __mailman3_config_default | combine(mailman3_config | default({}), recursive=True) }}"
+
+__mailman3_django_config_default:
+ admins: "{{ mailman3_django_superusers }}"
+ allowed_hosts: "{{ mailman3_domains | default([inventory_hostname]) }}"
+ rest_api_url: "http://{{ mailman3_core_api_hostname }}:{{ mailman3_core_api_port }}"
+ rest_api_user: "{{ mailman3_core_api_admin_user }}"
+ rest_api_pass: "{{ mailman3_core_api_admin_pass }}"
+ archiver_key: "{{ mailman3_archiver_key }}"
+ databases:
+ default:
+ ENGINE: django.db.backends.sqlite3
+ NAME: "{{ mailman3_django_var_dir }}/db/mailmansuite.db"
+ USER: ''
+ PASSWORD: ''
+ HOST: ''
+ PORT: ''
+ # Disable by default, recommended Django setup for nginx passes Host, not X-Forwarded-Host
+ #use_x_forwarded_host: true
+ secure_proxy_ssl_header: HTTP_X_FORWARDED_PROTO
+ default_http_protocol: https
+ default_from_email: postorius@{{ inventory_hostname }}
+ server_email: root@{{ inventory_hostname }}
+ compress_offline: true
+ socialaccount_providers: {}
+__mailman3_django_config_merged: "{{ __mailman3_django_config_default | combine(mailman3_django_config | default({}), recursive=True) }}"
+
+mailman3_postorius_root: 'postorius/'
+mailman3_hyperkitty_root: 'hyperkitty/'
+
+#mailman3_user: mailman
+mailman3_create_user: "{{ __mailman3_pip and not __mailman3_debian }}"
+
+__mailman3_debian_user_name: list
+__mailman3_user_name: >-
+ {{
+ (mailman3_user | default({})).name | default(
+ __mailman3_debian_user_name if __mailman3_debian else
+ 'mailman')
+ }}
diff --git a/library/roles/mailman/files/distribute_maps.ansible.cfg b/library/roles/mailman/files/distribute_maps.ansible.cfg
new file mode 100644
index 00000000..68a71145
--- /dev/null
+++ b/library/roles/mailman/files/distribute_maps.ansible.cfg
@@ -0,0 +1,8 @@
+[defaults]
+inventory = hosts
+retry_files_enabled = False
+transport = ssh
+
+[ssh_connection]
+# Automatically accept host keys
+ssh_args = -o StrictHostKeyChecking=no
diff --git a/library/roles/mailman/files/distribute_maps.playbook.yml b/library/roles/mailman/files/distribute_maps.playbook.yml
new file mode 100644
index 00000000..d70bf5b2
--- /dev/null
+++ b/library/roles/mailman/files/distribute_maps.playbook.yml
@@ -0,0 +1,21 @@
+---
+
+- name: Distribute Mailman transport maps to backup MX servers
+ hosts: all
+ tasks:
+ - name: Check postfix_lmtp
+ stat:
+ path: "{{ mailman3_var_dir }}/data/postfix_lmtp"
+ register: result
+ delegate_to: localhost
+ run_once: true
+ - name: Copy postfix_lmtp
+ copy:
+ src: "{{ mailman3_var_dir }}/data/postfix_lmtp"
+ dest: "{{ mailman3_distribute_maps_dir }}/postfix_lmtp"
+ when: result.stat.exists
+ notify:
+ - postmap
+ handlers:
+ - name: postmap
+ command: "{{ mailman3_postmap_command | default('postmap') }} {{ mailman3_distribute_maps_dir | quote }}/postfix_lmtp"
diff --git a/library/roles/mailman/handlers/main.yml b/library/roles/mailman/handlers/main.yml
new file mode 100644
index 00000000..ffeca058
--- /dev/null
+++ b/library/roles/mailman/handlers/main.yml
@@ -0,0 +1,18 @@
+---
+
+- name: reload systemd manager configuration
+ systemd:
+ daemon_reload: yes
+
+- name: restart mailman3-core service
+ service:
+ name: "{{ mailman3_core_service_name }}"
+ state: restarted
+ when: ansible_virtualization_type != "docker"
+
+- name: restart mailman3-web service
+ service:
+ name: "{{ mailman3_web_service_name }}{{ '@' if mailman3_domains is defined else '' }}{{ item }}"
+ state: restarted
+ loop: "{{ mailman3_domains | default(['']) }}"
+ when: ansible_virtualization_type != "docker"
diff --git a/library/roles/mailman/meta/main.yml b/library/roles/mailman/meta/main.yml
new file mode 100644
index 00000000..a7840e31
--- /dev/null
+++ b/library/roles/mailman/meta/main.yml
@@ -0,0 +1,28 @@
+galaxy_info:
+ author: natefoo
+ description: Mailman 3 installation, configuration, and management for Linux
+ company: The Galaxy Project
+ license: license (MIT)
+ min_ansible_version: 2.7
+ platforms:
+ - name: EL
+ versions:
+ - 7
+ - name: Debian
+ versions:
+ - stretch
+ - buster
+ - sid
+ - name: Ubuntu
+ versions:
+ - xenial
+ - bionic
+ galaxy_tags:
+ - mail
+ - mailing
+ - list
+ - lists
+ - mailman
+ - mailman3
+
+dependencies: []
diff --git a/library/roles/mailman/tasks/config.yml b/library/roles/mailman/tasks/config.yml
new file mode 100644
index 00000000..4a22875f
--- /dev/null
+++ b/library/roles/mailman/tasks/config.yml
@@ -0,0 +1,194 @@
+---
+
+- name: Create/update Mailman Core configuration
+ template:
+ src: mailman.cfg.j2
+ dest: "{{ mailman3_etc_dir }}/mailman.cfg"
+ group: "{{ __mailman3_group_name }}"
+ mode: "0640"
+ backup: "{{ mailman3_backup_configs }}"
+ notify:
+ - restart mailman3-core service
+
+- name: Create HyperKitty configuration file
+ copy:
+ content: |
+ [general]
+ base_url: http://localhost/{{ mailman3_hyperkitty_root }}
+ api_key: {{ mailman3_archiver_key }}
+ dest: "{{ mailman3_etc_dir }}/hyperkitty.cfg"
+ group: "{{ __mailman3_group_name }}"
+ notify:
+ - restart mailman3-core service
+
+- name: Create/update Django project
+ template:
+ src: "{{ item.name }}.j2"
+ mode: "{{ item.mode }}"
+ dest: "{{ mailman3_django_project_dir }}/{{ item.name }}"
+ backup: "{{ mailman3_backup_configs }}"
+ with_items:
+ - name: manage.py
+ mode: "0755"
+ - name: settings.py
+ mode: "0644"
+ - name: urls.py
+ mode: "0644"
+ - name: wsgi.py
+ mode: "0644"
+ when: __mailman3_pip
+ notify:
+ - restart mailman3-web service
+
+- name: Create/update Django local settings
+ template:
+ src: "settings_local.py.j2"
+ dest: "{{ mailman3_django_settings_file }}"
+ group: "{{ __mailman3_group_name }}"
+ mode: "0640"
+ backup: "{{ mailman3_backup_configs }}"
+ notify:
+ - restart mailman3-web service
+
+- name: Create Django local settings symlink
+ file:
+ src: "{{ mailman3_django_settings_file }}"
+ dest: "{{ mailman3_django_project_dir }}/settings_local.py"
+ state: link
+ when: __mailman3_pip
+ notify:
+ - restart mailman3-web service
+
+- name: Create Django site configs
+ copy:
+ content: |
+ # import project settings first
+ from settings import *
+ # override any configured SITE_ID
+ SITE_ID = {{ site_id + 1 }}
+ FILTER_VHOST = True
+ DEFAULT_FROM_EMAIL = '{{ mailman3_default_from_user }}@{{ domain }}'
+ dest: "{{ mailman3_django_project_dir }}/settings_{{ domain | replace('.', '_') | replace('-', '_') }}.py"
+ loop: "{{ mailman3_domains | default([]) }}"
+ loop_control:
+ index_var: site_id
+ loop_var: domain
+ when: mailman3_domains is defined
+ notify:
+ - restart mailman3-web service
+
+# This runs before collectstatic because it creates the log file, which must be created as the web user
+- name: Create/update Django DB schema
+ django_manage:
+ command: migrate
+ app_path: "{{ mailman3_django_project_dir }}"
+ virtualenv: "{{ mailman3_install_dir }}"
+ # FIXME:
+ become: yes
+ become_user: "{{ mailman3_web_user }}"
+ #become_method: su
+ #become_flags: '-s /bin/sh'
+ notify:
+ - restart mailman3-web service
+
+- name: Collect Django static files
+ django_manage:
+ command: collectstatic
+ app_path: "{{ mailman3_django_project_dir }}"
+ virtualenv: "{{ mailman3_install_dir }}"
+ when: __mailman3_pip
+
+- name: Check Django superusers
+ django_manage:
+ command: >-
+ shell -c 'import sys;
+ from django.contrib.auth.models import User;
+ sys.stdout.write("exists") if User.objects.filter(username="{{ item.name }}").count() > 0 else sys.stdout.write("missing")'
+ app_path: "{{ mailman3_django_project_dir }}"
+ virtualenv: "{{ mailman3_install_dir }}"
+ with_items: "{{ mailman3_django_superusers }}"
+ register: __mailman3_checksuperuser_result
+ changed_when: __mailman3_checksuperuser_result.out == "missing"
+ loop_control:
+ label: "{{ item.name }}"
+ # FIXME:
+ become: yes
+ become_user: "{{ mailman3_web_user }}"
+
+- name: Create Django superusers
+ django_manage:
+ command: >-
+ shell -c 'import sys;
+ from django.contrib.auth.models import User;
+ User.objects.create_superuser("{{ item.item.name }}", "{{ item.item.email }}", "{{ item.item.pass }}")'
+ #command: "createsuperuser --noinput --username={{ item.item.name }} --email={{ item.item.email }}"
+ app_path: "{{ mailman3_django_project_dir }}"
+ virtualenv: "{{ mailman3_install_dir }}"
+ when: item is changed
+ with_items: "{{ __mailman3_checksuperuser_result.results }}"
+ register: __mailman3_createsuperuser_result
+ changed_when: true
+ loop_control:
+ label: "{{ item.item.name }}"
+ # FIXME:
+ become: yes
+ become_user: "{{ mailman3_web_user }}"
+
+- name: Check Django sites
+ django_manage:
+ command: >-
+ shell -c 'import sys;
+ from django.contrib.sites.models import Site;
+ sys.stdout.write("exists") if Site.objects.filter(domain="example.com").count() > 0 else sys.stdout.write("missing")'
+ app_path: "{{ mailman3_django_project_dir }}"
+ virtualenv: "{{ mailman3_install_dir }}"
+ register: __mailman3_checkexamplesite_result
+ changed_when: __mailman3_checkexamplesite_result.out == "exists"
+ # FIXME:
+ become: yes
+ become_user: "{{ mailman3_web_user }}"
+
+- name: Correct default Django site
+ django_manage:
+ command: >-
+ shell -c 'from django.contrib.sites.models import Site;
+ Site.objects.filter(domain="example.com").update(
+ domain="{{ (mailman3_domains | default([])).0 | default(inventory_hostname) }}",
+ name="{{ (mailman3_domains | default([])).0 | default(inventory_hostname) }}"
+ )'
+ app_path: "{{ mailman3_django_project_dir }}"
+ virtualenv: "{{ mailman3_install_dir }}"
+ when: __mailman3_checkexamplesite_result is changed
+ changed_when: true
+ # FIXME:
+ become: yes
+ become_user: "{{ mailman3_web_user }}"
+
+# TODO: create additional domains (for right now the admin can do this in the UI)
+
+- name: Create/update uWSGI configuration file
+ template:
+ src: uwsgi.ini.j2
+ dest: "{{ mailman3_etc_dir }}/uwsgi.ini"
+ when: __mailman3_pip and mailman3_domains is not defined
+ notify:
+ - restart mailman3-web service
+
+- name: Create/update uWSGI domain configuration files
+ template:
+ src: uwsgi.ini.j2
+ dest: "{{ mailman3_etc_dir }}/uwsgi_{{ domain }}.ini"
+ loop: "{{ mailman3_domains | default([]) }}"
+ loop_control:
+ index_var: site_id
+ loop_var: domain
+ when: mailman3_domains is defined
+ notify:
+ - restart mailman3-web service
+
+# This is idempotent so it's safe to do as an always-run task
+- name: Compress CSS
+ django_manage:
+ app_path: "{{ mailman3_django_project_dir }}"
+ command: compress
+ virtualenv: "{{ mailman3_install_dir }}"
diff --git a/library/roles/mailman/tasks/distribute_maps.yml b/library/roles/mailman/tasks/distribute_maps.yml
new file mode 100644
index 00000000..aab4cb76
--- /dev/null
+++ b/library/roles/mailman/tasks/distribute_maps.yml
@@ -0,0 +1,44 @@
+---
+
+- name: Install Ansible
+ pip:
+ name: ansible
+ virtualenv: "{{ mailman3_distribute_maps_dir }}"
+ virtualenv_command: "{{ mailman3_virtualenv_command | default(omit) }}"
+ virtualenv_python: "{{ mailman3_virtualenv_python | default(omit) }}"
+
+- name: Create playbook directory
+ file:
+ path: "{{ mailman3_distribute_maps_dir }}/etc"
+ state: directory
+ group: "{{ __mailman3_group_name }}"
+ mode: "0750"
+
+- name: Create playbook files
+ copy:
+ src: "distribute_maps.{{ item }}"
+ dest: "{{ mailman3_distribute_maps_dir }}/etc/{{ item }}"
+ group: "{{ __mailman3_group_name }}"
+ mode: "0640"
+ loop:
+ - playbook.yml
+ - ansible.cfg
+
+# TODO: dig lookup plugin based inventory plugin
+- name: Create hosts file
+ template:
+ src: hosts.distribute_maps.j2
+ dest: "{{ mailman3_distribute_maps_dir }}/etc/hosts"
+ group: "{{ __mailman3_group_name }}"
+ mode: "0640"
+
+- name: Create cron job
+ cron:
+ name: Distribute Mailman 3 Postfix Maps
+ cron_file: ansible_mailman3_distribute_maps
+ user: "{{ __mailman3_user_name }}"
+ minute: "*/{{ mailman3_distribute_map_frequency | default(5) }}"
+ job: >-
+ cd {{ mailman3_distribute_maps_dir | quote }}/etc &&
+ {{ mailman3_distribute_maps_dir | quote }}/bin/ansible-playbook playbook.yml
+ >>{{ mailman3_log_dir }}/distribute_maps.log 2>&1
diff --git a/library/roles/mailman/tasks/group_discovery.yml b/library/roles/mailman/tasks/group_discovery.yml
new file mode 100644
index 00000000..705f25f5
--- /dev/null
+++ b/library/roles/mailman/tasks/group_discovery.yml
@@ -0,0 +1,15 @@
+---
+
+- name: Get Mailman user group ID
+ getent:
+ database: passwd
+ key: "{{ __mailman3_user_name }}"
+
+- name: Get Mailman user group name
+ getent:
+ database: group
+ key: "{{ getent_passwd[__mailman3_user_name][2] }}"
+
+- name: Set Mailman user group fact
+ set_fact:
+ __mailman3_group_name: "{{ getent_group | first }}"
diff --git a/library/roles/mailman/tasks/install_package.yml b/library/roles/mailman/tasks/install_package.yml
new file mode 100644
index 00000000..efc33cf6
--- /dev/null
+++ b/library/roles/mailman/tasks/install_package.yml
@@ -0,0 +1,35 @@
+---
+
+- name: Ensure supported OS for package installs
+ assert:
+ that:
+ - "ansible_os_family == 'Debian'"
+ success_msg: "OS is supported for Mailman 3 installation by package"
+ fail_msg: "OS is not supported for Mailman 3 installation by package, set `mailman3_install_method` to `pip`"
+
+# TODO: everything below untested with Mailman 3
+- name: Install debconf packages
+ apt:
+ name:
+ - debconf
+ - debconf-utils
+ when: ansible_os_family == "Debian"
+
+- name: Set client options in debconf
+ debconf:
+ name: mailman3
+ question: "{{ item.question }}"
+ value: "{{ item.value }}"
+ vtype: "{{ item.vtype }}"
+ when: ansible_os_family == "Debian"
+ with_items:
+ - question: "mailman/site_languages"
+ value: "{{ mailman3_language }}"
+ type: "multiselect"
+ - question: "mailman/default_server_language"
+ value: "{{ mailman3_language }}"
+ vtype: "multiselect"
+
+- name: Install Mailman and dependency packages
+ package:
+ name: "{{ mailman3_packages }}"
diff --git a/library/roles/mailman/tasks/install_pip.yml b/library/roles/mailman/tasks/install_pip.yml
new file mode 100644
index 00000000..845747cd
--- /dev/null
+++ b/library/roles/mailman/tasks/install_pip.yml
@@ -0,0 +1,98 @@
+---
+
+- name: Install system dependencies
+ package:
+ name: "{{ mailman3_system_dependency_packages }}"
+ when: mailman3_install_system_dependencies
+
+# This is a separate task from the next one because `python3 -m venv` doesn't install wheel and pip needs it to build
+# wheels (not strictly required, but preferred) and won't load it mid-invocation if installed during the next task
+- name: Create Mailman venv
+ pip:
+ name: wheel
+ virtualenv: "{{ mailman3_install_dir }}"
+ virtualenv_python: "{{ mailman3_virtualenv_python | default(omit) }}"
+ virtualenv_command: "{{ mailman3_virtualenv_command | default(omit) }}"
+
+- name: Install Mailman and Python dependencies
+ pip:
+ name: "{{ mailman3_packages + mailman3_extra_packages }}"
+ virtualenv: "{{ mailman3_install_dir }}"
+
+- name: Create configuration, data, and state directories
+ file:
+ path: "{{ item.path }}"
+ owner: "{{ item.owner | default(omit) }}"
+ group: "{{ item.group | default(omit) }}"
+ mode: "{{ item.mode | default(omit) }}"
+ state: directory
+ with_items:
+ - path: "{{ mailman3_etc_dir }}"
+ group: "{{ __mailman3_group_name }}"
+ mode: "0750"
+ # Creates the parent /var/opt/mailman3 with default permissions
+ - path: "{{ mailman3_var_dir | dirname }}"
+ - path: "{{ mailman3_var_dir }}"
+ owner: "{{ __mailman3_user_name }}"
+ group: "{{ __mailman3_group_name }}"
+ mode: "0750"
+ - path: "{{ mailman3_log_dir }}"
+ owner: "{{ __mailman3_user_name }}"
+ group: "{{ __mailman3_group_name }}"
+ mode: "0750"
+ - path: "{{ mailman3_django_var_dir }}"
+ - path: "{{ mailman3_django_var_dir }}/run"
+ owner: "{{ mailman3_web_user }}"
+ group: "{{ mailman3_web_group }}"
+ mode: "0750"
+ - path: "{{ mailman3_django_var_dir }}/db"
+ owner: "{{ mailman3_web_user }}"
+ group: "{{ mailman3_web_group }}"
+ mode: "0750"
+ - path: "{{ mailman3_django_var_dir }}/fulltext_index"
+ owner: "{{ mailman3_web_user }}"
+ group: "{{ mailman3_web_group }}"
+ mode: "0750"
+ - path: "{{ mailman3_django_var_dir }}/emails"
+ owner: "{{ mailman3_web_user }}"
+ group: "{{ mailman3_web_group }}"
+ mode: "0750"
+ - path: "{{ mailman3_django_project_dir }}"
+ group: "{{ mailman3_web_group }}"
+ mode: "0750"
+ - path: "{{ mailman3_django_log_dir }}"
+ mode: "0750"
+ owner: "{{ mailman3_web_user }}"
+ group: "{{ mailman3_web_group }}"
+
+- name: Create HyperKitty attachment directory
+ file:
+ path: "{{ __mailman3_django_config_merged.hyperkitty_attachment_folder }}"
+ owner: "{{ mailman3_web_user }}"
+ group: "{{ mailman3_web_group }}"
+ mode: "0750"
+ state: directory
+ when: __mailman3_django_config_merged.hyperkitty_attachment_folder is defined
+
+# TODO: This is needed to read settings_local.py from /etc/opt/mailman3, but will it be needed for anything else?
+# TODO: config option to control this?
+- name: Add web user to Mailman user group
+ user:
+ name: "{{ mailman3_web_user }}"
+ groups: "{{ __mailman3_group_name }}"
+
+- name: Install systemd service unit files
+ template:
+ src: "{{ item.src | default(item) }}.j2"
+ dest: /etc/systemd/system/{{ item.dest | default(item) }}
+ loop:
+ - mailman3-core.service
+ - src: mailman3-web.service
+ dest: mailman3-web{{ '@' if mailman3_domains is defined else '' }}.service
+ loop_control:
+ label: "{{ item.dest | default(item) }}"
+ when: mailman3_process_manager == 'systemd'
+ notify:
+ - reload systemd manager configuration
+
+# TODO: remove the non-instance service unit file if mailman3_domains is defined, remove the instance service unit file
diff --git a/library/roles/mailman/tasks/main.yml b/library/roles/mailman/tasks/main.yml
new file mode 100644
index 00000000..93d0a341
--- /dev/null
+++ b/library/roles/mailman/tasks/main.yml
@@ -0,0 +1,37 @@
+---
+
+- name: Include user creation tasks
+ include_tasks: user.yml
+ when: mailman3_create_user
+
+- name: Include group discovery tasks
+ import_tasks: group_discovery.yml
+
+- name: Include installation tasks
+ import_tasks: "install_{{ mailman3_install_method }}.yml"
+
+- name: Include configuration tasks
+ import_tasks: config.yml
+
+- name: Include Postfix map distribution tasks
+ include_tasks: distribute_maps.yml
+ when: mailman3_distribute_maps is defined
+
+# Perform whatever restarts are needed now, prevents double restart on first run
+- name: Flush handlers
+ meta: flush_handlers
+
+- name: Ensure Mailman Core is enabled and running
+ service:
+ name: "{{ mailman3_core_service_name }}"
+ enabled: yes
+ state: started
+ when: mailman3_process_manager != "supervisor"
+
+- name: Ensure Mailman Web is enabled and running
+ service:
+ name: "{{ mailman3_web_service_name }}{{ '@' if mailman3_domains is defined else '' }}{{ item }}"
+ enabled: yes
+ state: started
+ loop: "{{ mailman3_domains | default(['']) }}"
+ when: mailman3_process_manager != "supervisor"
diff --git a/library/roles/mailman/tasks/user.yml b/library/roles/mailman/tasks/user.yml
new file mode 100644
index 00000000..1e00b681
--- /dev/null
+++ b/library/roles/mailman/tasks/user.yml
@@ -0,0 +1,20 @@
+---
+
+- name: Create mailman3 group
+ group:
+ name: "{{ (mailman3_user | default({})).group }}"
+ gid: "{{ (mailman3_user | default({})).gid | default(omit) }}"
+ system: "{{ (mailman3_user | default({})).system | default('yes') }}"
+ when: (mailman3_user | default({})).group is defined
+
+- name: Create mailman3 user
+ user:
+ name: "{{ __mailman_user_name }}"
+ comment: "{{ (mailman3_user | default({})).comment | default(omit) }}"
+ uid: "{{ (mailman3_user | default({})).uid | default(omit) }}"
+ group: "{{ (mailman3_user | default({})).group | default(omit) }}"
+ groups: "{{ (mailman3_user | default({})).groups | default(omit) }}"
+ home: "{{ (mailman3_user | default({})).home | default(mailman3_var_dir) }}"
+ create_home: "{{ (mailman3_user | default({})).create_home | default('no') }}"
+ shell: "{{ (mailman3_user | default({})).shell | default(omit) }}"
+ system: "{{ (mailman3_user | default({})).system | default('yes') }}"
diff --git a/library/roles/mailman/templates/hosts.distribute_maps.j2 b/library/roles/mailman/templates/hosts.distribute_maps.j2
new file mode 100644
index 00000000..040e1dd0
--- /dev/null
+++ b/library/roles/mailman/templates/hosts.distribute_maps.j2
@@ -0,0 +1,12 @@
+#jinja2: trim_blocks: False
+{% for mx in mailman3_distribute_maps %}{% if mx.host != 'all' -%}
+{{ mx.host }}{% for opt in mx | sort %}{% if opt != 'host' %} {{ opt }}={{ mx[opt] }}{% endif %}{% endfor %}
+{% endif %}{% endfor %}
+[all:vars]
+mailman3_var_dir = {{ mailman3_var_dir }}
+{% set all_vars = mailman3_distribute_maps | selectattr("host", "eq", "all") | first -%}
+{% if all_vars is defined -%}
+{% for opt in all_vars | sort %}{% if opt != 'host' -%}
+{{ opt }} = {{ all_vars[opt] }}
+{% endif %}{% endfor -%}
+{% endif -%}
diff --git a/library/roles/mailman/templates/mailman.cfg.j2 b/library/roles/mailman/templates/mailman.cfg.j2
new file mode 100644
index 00000000..371832be
--- /dev/null
+++ b/library/roles/mailman/templates/mailman.cfg.j2
@@ -0,0 +1,11 @@
+;;
+;;
+;;
+
+{% for section in __mailman3_config_merged | sort %}
+[{{ section }}]
+{% for key in __mailman3_config_merged[section] | sort %}
+{{ key }}: {{ __mailman3_config_merged[section][key] }}
+{% endfor %}
+
+{% endfor %}
diff --git a/library/roles/mailman/templates/mailman3-core.service.j2 b/library/roles/mailman/templates/mailman3-core.service.j2
new file mode 100644
index 00000000..4fa62322
--- /dev/null
+++ b/library/roles/mailman/templates/mailman3-core.service.j2
@@ -0,0 +1,17 @@
+[Unit]
+Description=Mailman 3 Core service
+After=network.target
+Documentation=https://mailman.readthedocs.io/
+ConditionPathExists={{ mailman3_etc_dir }}/mailman.cfg
+
+[Service]
+ExecStart={{ mailman3_install_dir }}/bin/mailman -C {{ mailman3_etc_dir }}/mailman.cfg start
+ExecReload={{ mailman3_install_dir }}/bin/mailman -C {{ mailman3_etc_dir }}/mailman.cfg restart
+ExecStop={{ mailman3_install_dir }}/bin/mailman -C {{ mailman3_etc_dir }}/mailman.cfg stop
+Type=forking
+PIDFile={{ __mailman3_config_merged['paths.' ~ __mailman3_config_merged.mailman.layout].pid_file | default(mailman3_var_dir ~ '/master.pid') }}
+SyslogIdentifier=mailman3
+User={{ __mailman3_user_name }}
+Group={{ __mailman3_group_name }}
+
+[Install]
diff --git a/library/roles/mailman/templates/mailman3-web.service.j2 b/library/roles/mailman/templates/mailman3-web.service.j2
new file mode 100644
index 00000000..8e4e18ce
--- /dev/null
+++ b/library/roles/mailman/templates/mailman3-web.service.j2
@@ -0,0 +1,22 @@
+[Unit]
+Description=Mailman 3 Django/uWSGI {% if mailman3_domains is defined %}(domain %i) {% endif %}service
+After=network.target
+Documentation=https://mailman.readthedocs.io/
+ConditionPathExists={{ mailman3_etc_dir }}/uwsgi{% if mailman3_domains is defined %}_%i{% endif %}.ini
+
+[Service]
+ExecStart={{ mailman3_install_dir }}/bin/{{ mailman3_python_uwsgi_package }} --ini {{ mailman3_etc_dir }}/uwsgi{% if mailman3_domains is defined %}_%i{% endif %}.ini
+{# https://github.com/unbit/uwsgi/issues/1980 #}
+{% if mailman3_python_uwsgi_package == 'pyuwsgi' %}
+Environment=DJANGO_SETTINGS_MODULE=settings_%i
+{% endif %}
+Restart=on-failure
+KillSignal=SIGQUIT
+Type=notify
+StandardError=syslog
+NotifyAccess=all
+User=root
+Group=root
+
+[Install]
+WantedBy=multi-user.target
diff --git a/library/roles/mailman/templates/manage.py.j2 b/library/roles/mailman/templates/manage.py.j2
new file mode 100644
index 00000000..97ad7416
--- /dev/null
+++ b/library/roles/mailman/templates/manage.py.j2
@@ -0,0 +1,10 @@
+#!{{ mailman3_install_dir }}/bin/python3
+import os
+import sys
+
+if __name__ == "__main__":
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
+
+ from django.core.management import execute_from_command_line
+
+ execute_from_command_line(sys.argv)
diff --git a/library/roles/mailman/templates/settings.py.j2 b/library/roles/mailman/templates/settings.py.j2
new file mode 100644
index 00000000..2a45471e
--- /dev/null
+++ b/library/roles/mailman/templates/settings.py.j2
@@ -0,0 +1,294 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 1998-2016 by the Free Software Foundation, Inc.
+#
+# This file is part of Mailman Suite.
+#
+# Mailman Suite is free sofware: you can redistribute it and/or modify it
+# under the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# Mailman Suite is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
+# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
+# for more details.
+
+# You should have received a copy of the GNU General Public License along
+# with Mailman Suite. If not, see .
+"""
+Django Settings for Mailman Suite (hyperkitty + postorius)
+
+For more information on this file, see
+https://docs.djangoproject.com/en/1.8/topics/settings/
+
+For the full list of settings and their values, see
+https://docs.djangoproject.com/en/1.8/ref/settings/
+"""
+
+# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
+import os
+
+BASE_DIR = os.path.dirname(os.path.abspath(__file__))
+
+# SECURITY WARNING: don't run with debug turned on in production!
+DEBUG = {{ 'True' if (__mailman3_django_config_merged.debug | default(false) | bool) else 'False' }}
+
+# Application definition
+
+MIDDLEWARE = (
+ 'django.contrib.sessions.middleware.SessionMiddleware',
+ 'django.middleware.common.CommonMiddleware',
+ 'django.middleware.csrf.CsrfViewMiddleware',
+ 'django.middleware.locale.LocaleMiddleware',
+ 'django.contrib.auth.middleware.AuthenticationMiddleware',
+ 'django.contrib.messages.middleware.MessageMiddleware',
+ 'django.middleware.clickjacking.XFrameOptionsMiddleware',
+ 'django.middleware.security.SecurityMiddleware',
+ 'django_mailman3.middleware.TimezoneMiddleware',
+ 'postorius.middleware.PostoriusMiddleware',
+)
+
+ROOT_URLCONF = 'urls'
+
+
+TEMPLATES = [
+ {
+ 'BACKEND': 'django.template.backends.django.DjangoTemplates',
+ 'DIRS': [],
+ 'APP_DIRS': True,
+ 'OPTIONS': {
+ 'context_processors': [
+ 'django.template.context_processors.debug',
+ 'django.template.context_processors.i18n',
+ 'django.template.context_processors.media',
+ 'django.template.context_processors.static',
+ 'django.template.context_processors.tz',
+ 'django.template.context_processors.csrf',
+ 'django.template.context_processors.request',
+ 'django.contrib.auth.context_processors.auth',
+ 'django.contrib.messages.context_processors.messages',
+ 'django_mailman3.context_processors.common',
+ 'hyperkitty.context_processors.common',
+ 'postorius.context_processors.postorius',
+ ],
+ },
+ },
+]
+
+WSGI_APPLICATION = 'wsgi.application'
+
+
+# Password validation
+# https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
+
+AUTH_PASSWORD_VALIDATORS = [
+ {
+ 'NAME':
+'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
+ },
+ {
+ 'NAME':
+'django.contrib.auth.password_validation.MinimumLengthValidator',
+ },
+ {
+ 'NAME':
+'django.contrib.auth.password_validation.CommonPasswordValidator',
+ },
+ {
+ 'NAME':
+'django.contrib.auth.password_validation.NumericPasswordValidator',
+ },
+]
+
+
+# Static files (CSS, JavaScript, Images)
+# https://docs.djangoproject.com/en/1.8/howto/static-files/
+
+# Absolute path to the directory static files should be collected to.
+# Don't put anything in this directory yourself; store your static files
+# in apps' "static/" subdirectories and in STATICFILES_DIRS.
+# Example: "/var/www/example.com/static/"
+STATIC_ROOT = '{{ mailman3_django_static_dir }}'
+
+# URL prefix for static files.
+# Example: "http://example.com/static/", "http://static.example.com/"
+STATIC_URL = '/static/'
+
+# Additional locations of static files
+STATICFILES_DIRS = (
+ # Put strings here, like "/home/html/static" or "C:/www/django/static".
+ # Always use forward slashes, even on Windows.
+ # Don't forget to use absolute paths, not relative paths.
+ # BASE_DIR + '/static/',
+)
+
+# List of finder classes that know how to find static files in
+# various locations.
+STATICFILES_FINDERS = (
+ 'django.contrib.staticfiles.finders.FileSystemFinder',
+ 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
+ # 'django.contrib.staticfiles.finders.DefaultStorageFinder',
+ 'compressor.finders.CompressorFinder',
+)
+
+# Django 1.6+ defaults to a JSON serializer, but it won't work with
+# django-openid, see
+# https://bugs.launchpad.net/django-openid-auth/+bug/1252826
+SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
+
+
+LOGIN_URL = 'account_login'
+LOGIN_REDIRECT_URL = 'list_index'
+LOGOUT_URL = 'account_logout'
+
+
+# Compatibility with Bootstrap 3
+from django.contrib.messages import constants as messages # flake8: noqa
+MESSAGE_TAGS = {
+ messages.ERROR: 'danger'
+}
+
+
+#
+# Social auth
+#
+AUTHENTICATION_BACKENDS = (
+ 'django.contrib.auth.backends.ModelBackend',
+ 'allauth.account.auth_backends.AuthenticationBackend',
+)
+
+# Django Allauth
+ACCOUNT_AUTHENTICATION_METHOD = "username_email"
+ACCOUNT_EMAIL_REQUIRED = True
+ACCOUNT_EMAIL_VERIFICATION = "mandatory"
+ACCOUNT_UNIQUE_EMAIL = True
+
+#
+# django-compressor
+# https://pypi.python.org/pypi/django_compressor
+#
+COMPRESS_ENABLED = True
+COMPRESS_PRECOMPILERS = (
+ ('text/less', 'lessc {infile} {outfile}'),
+ ('text/x-scss', 'sassc -t compressed {infile} {outfile}'),
+ ('text/x-sass', 'sassc -t compressed {infile} {outfile}'),
+)
+
+# Needed for debug mode
+# INTERNAL_IPS = ('127.0.0.1',)
+
+
+#
+# Full-text search engine
+#
+HAYSTACK_CONNECTIONS = {
+ 'default': {
+ 'ENGINE': '{{ mailman3_django_haystack_engine | default("haystack.backends.whoosh_backend.WhooshEngine") }}',
+ 'PATH': os.path.join("{{ mailman3_django_var_dir }}", "fulltext_index"),
+ # You can also use the Xapian engine, it's faster and more accurate,
+ # but requires another library.
+ # http://django-haystack.readthedocs.io/en/v2.4.1/installing_search_engines.html#xapian
+ # Example configuration for Xapian:
+ #'ENGINE': 'xapian_backend.XapianEngine'
+ },
+}
+
+
+#
+# Asynchronous tasks
+#
+Q_CLUSTER = {
+ 'timeout': 300,
+ 'save_limit': 100,
+ 'orm': 'default',
+}
+
+
+# A sample logging configuration. The only tangible logging
+# performed by this configuration is to send an email to
+# the site admins on every HTTP 500 error when DEBUG=False.
+# See http://docs.djangoproject.com/en/dev/topics/logging for
+# more details on how to customize your logging configuration.
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'filters': {
+ 'require_debug_false': {
+ '()': 'django.utils.log.RequireDebugFalse'
+ }
+ },
+ 'handlers': {
+ 'mail_admins': {
+ 'level': 'ERROR',
+ 'filters': ['require_debug_false'],
+ 'class': 'django.utils.log.AdminEmailHandler'
+ },
+ 'file':{
+ 'level': 'INFO',
+ #'class': 'logging.handlers.RotatingFileHandler',
+ 'class': 'logging.handlers.WatchedFileHandler',
+ 'filename': os.path.join('{{ mailman3_django_log_dir }}', 'mailmansuite.log'),
+ 'formatter': 'verbose',
+ },
+ 'console': {
+ 'class': 'logging.StreamHandler',
+ 'formatter': 'simple',
+ },
+ },
+ 'loggers': {
+ 'django.request': {
+ #'handlers': ['mail_admins', 'file'],
+ 'handlers': ['file'],
+ 'level': 'ERROR',
+ 'propagate': True,
+ },
+ 'django': {
+ 'handlers': ['file'],
+ 'level': 'ERROR',
+ 'propagate': True,
+ },
+ 'hyperkitty': {
+ 'handlers': ['file'],
+ 'level': 'DEBUG',
+ 'propagate': True,
+ },
+ 'postorius': {
+ 'handlers': ['console', 'file'],
+ 'level': 'INFO',
+ },
+ },
+ 'formatters': {
+ 'verbose': {
+ 'format': '%(levelname)s %(asctime)s %(process)d %(name)s %(message)s'
+ },
+ 'simple': {
+ 'format': '%(levelname)s %(message)s'
+ },
+ },
+ #'root': {
+ # 'handlers': ['file'],
+ # 'level': 'INFO',
+ #},
+}
+
+
+# Using the cache infrastructure can significantly improve performance on a
+# production setup. This is an example with a local Memcached server.
+#CACHES = {
+# 'default': {
+# 'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
+# 'LOCATION': '127.0.0.1:11211',
+# }
+#}
+
+
+# When DEBUG is True, don't actually send emails to the SMTP server, just store
+# them in a directory. This way you won't accidentally spam your mailing-lists
+# while you're fiddling with the code.
+if DEBUG == True:
+ EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
+ EMAIL_FILE_PATH = os.path.join("{{ mailman3_django_var_dir }}", 'emails')
+
+
+# galaxyproject.mailman3: settings_local.py is required, no exception handling
+from settings_local import *
diff --git a/library/roles/mailman/templates/settings_local.py.j2 b/library/roles/mailman/templates/settings_local.py.j2
new file mode 100644
index 00000000..4b81f24a
--- /dev/null
+++ b/library/roles/mailman/templates/settings_local.py.j2
@@ -0,0 +1,206 @@
+# This file is imported by the Mailman Suite. It is used to override
+# the default settings from {{ mailman3_django_project_dir }}/settings.py.
+
+# SECURITY WARNING: keep the secret key used in production secret!
+SECRET_KEY = "{{ __mailman3_django_config_merged.secret_key | default('change-this-on-your-production-server') }}"
+
+ADMINS = (
+{% for admin in __mailman3_django_config_merged.admins %}
+ ('{{ admin.name }}', '{{ admin.email }}'),
+{% endfor %}
+)
+
+# If using multiple domains, this value is overridden in {{ mailman3_django_project_dir }}/settings_DOMAIN.py
+SITE_ID = {{ __mailman3_django_config_merged.site_id | default(1) }}
+
+# Hosts/domain names that are valid for this site; required if DEBUG is False
+# See https://docs.djangoproject.com/en/1.8/ref/settings/#allowed-hosts
+ALLOWED_HOSTS = [
+ "localhost", # Archiving API from Mailman, keep it.
+ # Add here all production URLs you may have.
+{% for host in __mailman3_django_config_merged.allowed_hosts %}
+ "{{ host }}",
+{% endfor %}
+]
+
+# Mailman API credentials
+MAILMAN_REST_API_URL = '{{ __mailman3_django_config_merged.rest_api_url | default('http://localhost:8001') }}'
+MAILMAN_REST_API_USER = '{{ __mailman3_django_config_merged.rest_api_user | default('restadmin') }}'
+MAILMAN_REST_API_PASS = '{{ __mailman3_django_config_merged.rest_api_pass | default('restpass') }}'
+MAILMAN_ARCHIVER_KEY = '{{ __mailman3_django_config_merged.archiver_key | default('SecretArchiverAPIKey') }}'
+MAILMAN_ARCHIVER_FROM = (
+{% for host in __mailman3_django_config_merged.archiver_from | default(['127.0.0.1', '::1']) %}
+ '{{ host }}',
+{% endfor %}
+)
+
+# Application definition
+
+INSTALLED_APPS = (
+ 'hyperkitty',
+ 'postorius',
+ 'django_mailman3',
+ # Uncomment the next line to enable the admin:
+ 'django.contrib.admin',
+ # Uncomment the next line to enable admin documentation:
+ # 'django.contrib.admindocs',
+ 'django.contrib.auth',
+ 'django.contrib.contenttypes',
+ 'django.contrib.sessions',
+ 'django.contrib.sites',
+ 'django.contrib.messages',
+ 'django.contrib.staticfiles',
+ 'rest_framework',
+ 'django_gravatar',
+ 'compressor',
+ 'haystack',
+ 'django_extensions',
+ 'django_q',
+ 'allauth',
+ 'allauth.account',
+ 'allauth.socialaccount',
+ #'django_mailman3.lib.auth.fedora',
+{% for provider in __mailman3_django_config_merged.socialaccount_providers | sort %}
+ 'allauth.socialaccount.providers.{{ provider }}',
+{% endfor %}
+)
+
+
+# Database
+# https://docs.djangoproject.com/en/1.8/ref/settings/#databases
+
+DATABASES = {
+{% for key in __mailman3_django_config_merged.databases | sort %}
+ '{{ key }}': {
+{% for opt in __mailman3_django_config_merged.databases[key] | sort %}
+ '{{ opt }}': '{{ __mailman3_django_config_merged.databases[key][opt] }}',
+{% endfor %}
+ }
+{% endfor %}
+}
+
+
+# If you're behind a proxy, use the X-Forwarded-Host header
+# See https://docs.djangoproject.com/en/1.8/ref/settings/#use-x-forwarded-host
+USE_X_FORWARDED_HOST = {{ 'True' if __mailman3_django_config_merged.use_x_forwarded_host | bool else 'False' }}
+
+# And if your proxy does your SSL encoding for you, set SECURE_PROXY_SSL_HEADER
+# https://docs.djangoproject.com/en/1.8/ref/settings/#secure-proxy-ssl-header
+SECURE_PROXY_SSL_HEADER = ('{{ __mailman3_django_config_merged.secure_proxy_ssl_header }}', 'https')
+
+# Other security settings
+# SECURE_SSL_REDIRECT = True
+# If you set SECURE_SSL_REDIRECT to True, make sure the SECURE_REDIRECT_EXEMPT
+# contains at least this line:
+# SECURE_REDIRECT_EXEMPT = [
+# "archives/api/mailman/.*", # Request from Mailman.
+# ]
+# SESSION_COOKIE_SECURE = True
+# SECURE_CONTENT_TYPE_NOSNIFF = True
+# SECURE_BROWSER_XSS_FILTER = True
+# CSRF_COOKIE_SECURE = True
+# CSRF_COOKIE_HTTPONLY = True
+# X_FRAME_OPTIONS = 'DENY'
+
+
+# Internationalization
+# https://docs.djangoproject.com/en/1.8/topics/i18n/
+
+LANGUAGE_CODE = '{{ mailman3_language_code }}'
+
+TIME_ZONE = '{{ __mailman3_django_config_merged.time_zone | default("UTC") }}'
+
+USE_I18N = True
+
+USE_L10N = True
+
+USE_TZ = True
+
+
+# If you enable internal authentication, this is the address that the emails
+# will appear to be coming from. Make sure you set a valid domain name,
+# otherwise the emails may get rejected.
+# https://docs.djangoproject.com/en/1.8/ref/settings/#default-from-email
+DEFAULT_FROM_EMAIL = '{{ __mailman3_django_config_merged.default_from_email }}'
+
+# If you enable email reporting for error messages, this is where those emails
+# will appear to be coming from. Make sure you set a valid domain name,
+# otherwise the emails may get rejected.
+# https://docs.djangoproject.com/en/1.8/ref/settings/#std:setting-SERVER_EMAIL
+SERVER_EMAIL = '{{ __mailman3_django_config_merged.server_email }}'
+
+# Change this when you have a real email backend
+EMAIL_BACKEND = 'django.core.mail.backends.smtp.EmailBackend'
+
+
+#
+# Social auth
+#
+
+# Django Allauth
+# You probably want https in production
+ACCOUNT_DEFAULT_HTTP_PROTOCOL = "{{ __mailman3_django_config_merged.default_http_protocol }}"
+
+SOCIALACCOUNT_PROVIDERS = {
+{% for key in __mailman3_django_config_merged.socialaccount_providers | sort %}
+{# This just dumps Ansible's Python representation of the value, which isn't ideal, but it probably works #}
+ '{{ key }}': {{ __mailman3_django_config_merged.socialaccount_providers[key] }}
+{% endfor %}
+}
+
+
+#
+# Gravatar
+# https://github.com/twaddington/django-gravatar
+#
+# Gravatar base url.
+# GRAVATAR_URL = 'http://cdn.libravatar.org/'
+# Gravatar base secure https url.
+# GRAVATAR_SECURE_URL = 'https://seccdn.libravatar.org/'
+# Gravatar size in pixels.
+# GRAVATAR_DEFAULT_SIZE = '80'
+# An image url or one of the following: 'mm', 'identicon', 'monsterid',
+# 'wavatar', 'retro'.
+# GRAVATAR_DEFAULT_IMAGE = 'mm'
+# One of the following: 'g', 'pg', 'r', 'x'.
+# GRAVATAR_DEFAULT_RATING = 'g'
+# True to use https by default, False for plain http.
+# GRAVATAR_DEFAULT_SECURE = True
+
+#
+# django-compressor
+# https://pypi.python.org/pypi/django_compressor
+#
+# On a production setup, setting COMPRESS_OFFLINE to True will bring a
+# significant performance improvement, as CSS files will not need to be
+# recompiled on each requests. It means running an additional "compress"
+# management command after each code upgrade.
+# http://django-compressor.readthedocs.io/en/latest/usage/#offline-compression
+COMPRESS_OFFLINE = {{ 'True' if __mailman3_django_config_merged.compress_offline | bool else 'False' }}
+
+# Needed for debug mode
+# INTERNAL_IPS = ('127.0.0.1',)
+
+
+# Using the cache infrastructure can significantly improve performance on a
+# production setup. This is an example with a local Memcached server.
+#CACHES = {
+# 'default': {
+# 'BACKEND': 'django.core.cache.backends.memcached.PyLibMCCache',
+# 'LOCATION': '127.0.0.1:11211',
+# }
+#}
+
+
+#
+# HyperKitty-specific
+#
+
+# Only display mailing-lists from the same virtual host as the webserver
+FILTER_VHOST = False
+
+{% if __mailman3_django_config_merged.hyperkitty_attachment_folder is defined %}
+HYPERKITTY_ATTACHMENT_FOLDER = '{{ __mailman3_django_config_merged.hyperkitty_attachment_folder }}'
+{% endif %}
+
+POSTORIUS_TEMPLATE_BASE_URL = 'http://localhost:8000'
diff --git a/library/roles/mailman/templates/urls.py.j2 b/library/roles/mailman/templates/urls.py.j2
new file mode 100644
index 00000000..e6f05835
--- /dev/null
+++ b/library/roles/mailman/templates/urls.py.j2
@@ -0,0 +1,37 @@
+# -*- coding: utf-8 -*-
+# Copyright (C) 1998-2016 by the Free Software Foundation, Inc.
+#
+# This file is part of Postorius.
+#
+# Postorius is free software: you can redistribute it and/or modify it under
+# the terms of the GNU General Public License as published by the Free
+# Software Foundation, either version 3 of the License, or (at your option)
+# any later version.
+#
+# Postorius is distributed in the hope that it will be useful, but WITHOUT
+# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+# FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
+# more details.
+#
+# You should have received a copy of the GNU General Public License along with
+# Postorius. If not, see .
+
+
+from django.conf.urls import include, url
+from django.contrib import admin
+from django.urls import reverse_lazy
+from django.views.generic import RedirectView
+
+urlpatterns = [
+{% if mailman3_postorius_root %}
+ url(r'^$', RedirectView.as_view(
+ url=reverse_lazy('list_index'),
+ permanent=True)),
+{% endif %}
+ url(r'^{{ mailman3_postorius_root }}', include('postorius.urls')),
+ url(r'^{{ mailman3_hyperkitty_root }}', include('hyperkitty.urls')),
+ url(r'', include('django_mailman3.urls')),
+ url(r'^accounts/', include('allauth.urls')),
+ # Django admin
+ url(r'^admin/', admin.site.urls),
+]
diff --git a/library/roles/mailman/templates/uwsgi.ini.j2 b/library/roles/mailman/templates/uwsgi.ini.j2
new file mode 100644
index 00000000..a7403c7b
--- /dev/null
+++ b/library/roles/mailman/templates/uwsgi.ini.j2
@@ -0,0 +1,78 @@
+{% macro getsock(sock) -%}
+ {% if domain is defined -%}
+ {% if ':' in sock -%}
+ {% set host, port = sock.rsplit(':', 1) -%}
+ {{ [host, (port | int) + site_id - 1] | join(':') }}
+ {% else -%}
+ {% set path, ext = sock | splitext -%}
+ {{ [path, '_' ~ domain, ext] | join('') }}
+ {% endif -%}
+ {% else -%}
+ {{ sock }}
+ {% endif -%}
+{% endmacro -%}
+
+[uwsgi]
+{% if domain is defined %}
+# Django settings module to load
+env = DJANGO_SETTINGS_MODULE=settings{{ '_' ~ domain | replace('.', '_') | replace('-', '_') }}
+{% endif %}
+
+# Port on which uwsgi will be listening.
+uwsgi-socket = {{ getsock(mailman3_uwsgi_socket) }}
+{% if mailman3_http_socket is defined %}
+http-socket = {{ getsock(mailman3_http_socket) }}
+{% endif %}
+
+# Enable threading for python
+enable-threads = true
+
+# Move to the directory wher the django files are.
+chdir = {{ mailman3_django_project_dir }}
+
+# Use the wsgi file provided with the django project.
+wsgi-file = wsgi.py
+
+# Setup default number of processes and threads per process.
+master = true
+process = 2
+threads = 2
+
+# Drop privielges and don't run as root.
+uid = {{ mailman3_web_user }}
+gid = {{ mailman3_web_group }}
+
+virtualenv = {{ mailman3_install_dir }}
+
+# Setup the django_q related worker processes.
+attach-daemon = ./manage.py qcluster
+
+{% if mailman3_domains is not defined or (site_id | default(-1)) == 0 %}
+# Setup hyperkitty's cron jobs.
+unique-cron = -1 -1 -1 -1 -1 ./manage.py runjobs minutely
+unique-cron = -15 -1 -1 -1 -1 ./manage.py runjobs quarter_hourly
+unique-cron = 0 -1 -1 -1 -1 ./manage.py runjobs hourly
+unique-cron = 0 0 -1 -1 -1 ./manage.py runjobs daily
+unique-cron = 0 0 1 -1 -1 ./manage.py runjobs monthly
+unique-cron = 0 0 -1 -1 0 ./manage.py runjobs weekly
+unique-cron = 0 0 1 1 -1 ./manage.py runjobs yearly
+
+{% endif %}
+{% if mailman3_uwsgi_static %}
+# Directly serve static content.
+static-map = /static={{ mailman3_django_static_dir }}
+
+{% endif %}
+# Setup the request log.
+req-logger = file:{{ mailman3_django_log_dir }}/uwsgi{{ '_' ~ domain if domain is defined else '' }}.log
+
+# Log cron seperately.
+logger = cron file:{{ mailman3_django_log_dir }}/uwsgi-cron{{ '_' ~ domain if domain is defined else '' }}.log
+log-route = cron uwsgi-cron
+
+# Log qcluster commands seperately.
+logger = qcluster file:{{ mailman3_django_log_dir }}/uwsgi-qcluster{{ '_' ~ domain if domain is defined else '' }}.log
+log-route = qcluster uwsgi-daemons
+
+# Last log and it logs the rest of the stuff.
+logger = file:{{ mailman3_django_log_dir }}/uwsgi-error{{ '_' ~ domain if domain is defined else '' }}.log
diff --git a/library/roles/mailman/templates/wsgi.py.j2 b/library/roles/mailman/templates/wsgi.py.j2
new file mode 100644
index 00000000..5535f996
--- /dev/null
+++ b/library/roles/mailman/templates/wsgi.py.j2
@@ -0,0 +1,40 @@
+"""
+WSGI config for HyperKitty project.
+
+It exposes the WSGI callable as a module-level variable named ``application``.
+
+For more information on this file, see
+https://docs.djangoproject.com/en/ docs_version /howto/deployment/wsgi/
+"""
+
+import os
+
+# import sys
+# import site
+
+# For some unknown reason, sometimes mod_wsgi fails to set the python paths to
+# the virtualenv, with the 'python-path' option. You can do it here too.
+#
+# # Remember original sys.path.
+# prev_sys_path = list(sys.path)
+# # Add here, for the settings module
+# site.addsitedir(os.path.abspath(os.path.dirname(__file__)))
+# # Add the virtualenv
+# venv = os.path.join(os.path.abspath(os.path.dirname(__file__)),
+# '..', 'lib', 'python2.6', 'site-packages')
+# site.addsitedir(venv)
+# # Reorder sys.path so new directories at the front.
+# new_sys_path = []
+# for item in list(sys.path):
+# if item not in prev_sys_path:
+# new_sys_path.append(item)
+# sys.path.remove(item)
+# sys.path[:0] = new_sys_path
+
+from django.core.wsgi import get_wsgi_application
+
+m = os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
+# https://github.com/unbit/uwsgi/issues/1980
+os.environ["DJANGO_SETTINGS_MODULE"] = m.replace('-', '_').replace('.', '_')
+
+application = get_wsgi_application()