From 0773fef2ac2cbc65646771d05c3770e1c57516c7 Mon Sep 17 00:00:00 2001 From: Andrea Dell'Amico Date: Tue, 15 Sep 2026 12:34:16 +0200 Subject: [PATCH] See #616. Broken hostname in the mailman footer. --- README.md | 23 ++++++ defaults/main.yml | 7 ++ tasks/postorius-hyperkitty.yml | 24 ++++++ templates/mailman-configure-django-site.py.j2 | 73 +++++++++++++++++++ templates/mailman-hyperkitty.cfg.j2 | 2 +- ...lman-legacy-hyperkitty-compatibility.py.j2 | 55 +++++++++++++- .../site/en/list:member:generic:footer.txt.j2 | 1 + .../site/it/list:member:generic:footer.txt.j2 | 1 + 8 files changed, 184 insertions(+), 2 deletions(-) create mode 100644 templates/mailman-configure-django-site.py.j2 diff --git a/README.md b/README.md index d63c46a..e606a3b 100644 --- a/README.md +++ b/README.md @@ -91,6 +91,29 @@ Disable this option before upgrading HyperKitty, django-mailman3, or mailman-hyperkitty, then review whether the compatibility patches are still needed with the new versions. +The legacy Mailman 3.3.1 decorator is also patched so the optional +`${hyperkitty_url}` footer placeholder expands to an empty value when the +list's archive policy is `never` or its HyperKitty archiver is disabled. The +site-level generic footer can therefore include a permalink only for messages +that have an active archive. A custom per-list footer still overrides this +site default. + +HyperKitty builds `List-Archive`, `Archived-At`, and `${hyperkitty_url}` from +the Django Site associated with each mail domain. On installations with one +canonical web frontend, manage that Site explicitly: + +```yaml +mailman_manage_postorius_site: true +mailman_postorius_site_id: 2 +mailman_postorius_site_domain: 'mailman.example.org' +mailman_postorius_site_name: 'Example Mailman' +``` + +The helper refuses a domain already assigned to another Django Site and only +updates the configured Site ID. Existing django-mailman3 mail-domain mappings +remain attached to that Site. Preview the database change with +`sudo -u mailman /usr/local/sbin/mailman-configure-django-site --check`. + ## uWSGI availability safeguards The Mailman web application runs with multiple uWSGI worker processes. A diff --git a/defaults/main.yml b/defaults/main.yml index 9523bff..8d31966 100644 --- a/defaults/main.yml +++ b/defaults/main.yml @@ -47,6 +47,7 @@ mailman_weekly_verified_restart_start_timeout: 120 # Compatibility fixes for the legacy HyperKitty stack. These are deliberately # opt-in and pinned: a package upgrade must be reviewed before patching sources. mailman_enable_legacy_hyperkitty_compatibility_patches: false +mailman_legacy_core_version: '3.3.1' mailman_legacy_hyperkitty_version: '1.3.3' mailman_legacy_django_mailman3_version: '1.3.4' mailman_legacy_mailman_hyperkitty_version: '1.1.0' @@ -219,6 +220,12 @@ mailman_postorius_uwsgi_reload_on_rss: 512 mailman_postorius_uwsgi_plugins: 'systemd_logger,python36' # 1 is the predefined one, that must be deleted mailman_postorius_site_id: 2 +# Optionally keep the Django Site used by django-mailman3 aligned with the +# canonical web frontend. HyperKitty uses this value in archive URLs. +mailman_manage_postorius_site: false +mailman_postorius_site_domain: 'localhost' +mailman_postorius_site_name: 'Mailman' +mailman_postorius_site_config_script: '/usr/local/sbin/mailman-configure-django-site' mailman_postorius_allowed_hosts: - 'localhost' - '{{ ansible_fqdn }}' diff --git a/tasks/postorius-hyperkitty.yml b/tasks/postorius-hyperkitty.yml index 9daa545..e8fd683 100644 --- a/tasks/postorius-hyperkitty.yml +++ b/tasks/postorius-hyperkitty.yml @@ -38,10 +38,34 @@ args: creates: '{{ mailman_postorius_dir }}/static/admin/js/actions.js' + - name: Install the canonical Django Site configuration helper + ansible.builtin.template: + src: mailman-configure-django-site.py.j2 + dest: '{{ mailman_postorius_site_config_script }}' + owner: root + group: '{{ mailman_user }}' + mode: '0750' + when: mailman_manage_postorius_site | bool + tags: [ 'mailman_postorius_site' ] + + - name: Configure the canonical Django Site used by archive links + become: true + become_user: '{{ mailman_user }}' + ansible.builtin.command: + argv: + - '{{ mailman_postorius_site_config_script }}' + register: mailman_postorius_site_config_result + changed_when: "'CHANGED:' in mailman_postorius_site_config_result.stdout" + when: + - mailman_manage_postorius_site | bool + - not ansible_check_mode + tags: [ 'mailman_postorius_site' ] + - name: Install the hyperkitty configuration file template: src=mailman-hyperkitty.cfg.j2 dest={{ mailman_conf_dir }}/mailman-hyperkitty.cfg owner=root group={{ mailman_user }} mode=0440 when: mailman_use_hyperkitty_archiver | bool register: mailman_hyperkitty_install + notify: Restart mailman tags: [ 'mailman', 'postorius', 'hyperkitty', 'mailman_conf' ] diff --git a/templates/mailman-configure-django-site.py.j2 b/templates/mailman-configure-django-site.py.j2 new file mode 100644 index 0000000..12dcf98 --- /dev/null +++ b/templates/mailman-configure-django-site.py.j2 @@ -0,0 +1,73 @@ +#!{{ mailman_bindir }}/python3 + +"""Configure the canonical Django Site used by Mailman web applications.""" + +import argparse +import os +import sys + + +PROJECT_DIR = {{ mailman_postorius_dir | to_json }} +SITE_ID = {{ mailman_postorius_site_id | int }} +SITE_DOMAIN = {{ mailman_postorius_site_domain | to_json }} +SITE_NAME = {{ mailman_postorius_site_name | to_json }} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + '--check', action='store_true', + help='report the required change without updating the database') + arguments = parser.parse_args() + + os.chdir(PROJECT_DIR) + sys.path.insert(0, PROJECT_DIR) + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'settings') + + import django + django.setup() + + from django.contrib.sites.models import Site + from django.db import transaction + + if (not SITE_DOMAIN or + any(character in SITE_DOMAIN for character in '/: \t\r\n')): + raise RuntimeError( + 'site domain must be a hostname without scheme or path: {!r}'.format( + SITE_DOMAIN)) + + with transaction.atomic(): + conflicting = Site.objects.select_for_update().filter( + domain=SITE_DOMAIN).exclude(pk=SITE_ID).first() + if conflicting is not None: + raise RuntimeError( + 'site domain {!r} is already assigned to Site {}'.format( + SITE_DOMAIN, conflicting.pk)) + + site = Site.objects.select_for_update().get(pk=SITE_ID) + changes = [] + if site.domain != SITE_DOMAIN: + changes.append('domain {!r} -> {!r}'.format(site.domain, SITE_DOMAIN)) + site.domain = SITE_DOMAIN + if site.name != SITE_NAME: + changes.append('name {!r} -> {!r}'.format(site.name, SITE_NAME)) + site.name = SITE_NAME + if changes: + if arguments.check: + print('WOULD CHANGE: Site {}: {}'.format( + SITE_ID, ', '.join(changes))) + else: + site.save(update_fields=('domain', 'name')) + print('CHANGED: Site {}: {}'.format( + SITE_ID, ', '.join(changes))) + else: + print('OK: Site {} already uses {} ({})'.format( + SITE_ID, SITE_DOMAIN, SITE_NAME)) + + +if __name__ == '__main__': + try: + main() + except Exception as error: + print('ERROR: {}'.format(error), file=sys.stderr) + sys.exit(1) diff --git a/templates/mailman-hyperkitty.cfg.j2 b/templates/mailman-hyperkitty.cfg.j2 index d44368f..6c85623 100644 --- a/templates/mailman-hyperkitty.cfg.j2 +++ b/templates/mailman-hyperkitty.cfg.j2 @@ -15,7 +15,7 @@ # better if it is not. # However, if your Mailman installation is accessed via HTTPS, the URL needs # to match your SSL certificate (e.g. https://lists.example.com/hyperkitty). -base_url: {{ mailman_site_url }}/hyperkitty/ +base_url: {{ mailman_site_url | regex_replace('/+$', '') }}/hyperkitty/ # Shared API key, must be the identical to the value in HyperKitty's # settings. diff --git a/templates/mailman-legacy-hyperkitty-compatibility.py.j2 b/templates/mailman-legacy-hyperkitty-compatibility.py.j2 index 52b6110..60cc1f2 100644 --- a/templates/mailman-legacy-hyperkitty-compatibility.py.j2 +++ b/templates/mailman-legacy-hyperkitty-compatibility.py.j2 @@ -17,6 +17,7 @@ import pkg_resources EXPECTED_VERSIONS = { + 'mailman': {{ mailman_legacy_core_version | to_json }}, 'HyperKitty': {{ mailman_legacy_hyperkitty_version | to_json }}, 'django-mailman3': {{ mailman_legacy_django_mailman3_version | to_json }}, 'mailman-hyperkitty': {{ mailman_legacy_mailman_hyperkitty_version | to_json }}, @@ -157,6 +158,21 @@ MAILMAN_HYPERKITTY_SEND_NEW = ''' message_text = msg.as_bytes() except (MessageError, KeyError, UnicodeEncodeError) as error: ''' +MAILMAN_DECORATE_IMPORT_OLD = ( + "from mailman.interfaces.handler import IHandler\n" +) +MAILMAN_DECORATE_IMPORT_NEW = ( + "from mailman.interfaces.archiver import ArchivePolicy\n" + "from mailman.interfaces.handler import IHandler\n" +) +MAILMAN_DECORATE_DATA_OLD = " d = {}\n" +MAILMAN_DECORATE_DATA_NEW = " d = {'hyperkitty_url': ''}\n" +MAILMAN_DECORATE_ARCHIVER_OLD = " if archiver.is_enabled:\n" +MAILMAN_DECORATE_ARCHIVER_NEW = ( + " if (archiver.is_enabled and\n" + " mlist.archive_policy is not ArchivePolicy.never):\n" +) + HYPERKITTY_TASKS_WITH_MAILING_LIST = ( '_rebuild_mailinglist_cache_recent', '_rebuild_mailinglist_cache_for_month', @@ -270,6 +286,33 @@ def patch_mailman_hyperkitty(source): 'mailman-hyperkitty byte delivery'), True +def patch_mailman_decorate(source): + """Make the HyperKitty footer placeholder safely optional.""" + updated = source + changed = False + + if MAILMAN_DECORATE_IMPORT_NEW not in updated: + updated = replace_once( + updated, MAILMAN_DECORATE_IMPORT_OLD, MAILMAN_DECORATE_IMPORT_NEW, + 'Mailman decorate ArchivePolicy import') + changed = True + + replacements = ( + (MAILMAN_DECORATE_DATA_OLD, MAILMAN_DECORATE_DATA_NEW, + 'Mailman decorate optional HyperKitty placeholder'), + (MAILMAN_DECORATE_ARCHIVER_OLD, MAILMAN_DECORATE_ARCHIVER_NEW, + 'Mailman decorate archive-policy check'), + ) + for old, new, description in replacements: + if new in updated: + if old in updated: + raise RuntimeError('{} is ambiguous'.format(description)) + continue + updated = replace_once(updated, old, new, description) + changed = True + return updated, changed + + def patch_hyperkitty_tasks(source): """Backport the upstream stale-list guards to all affected tasks.""" updated = source @@ -351,6 +394,7 @@ def quarantine_catalog(path): def main(): + mailman = distribution('mailman') hyperkitty = distribution('HyperKitty') django_mailman3 = distribution('django-mailman3') mailman_hyperkitty = distribution('mailman-hyperkitty') @@ -361,18 +405,23 @@ def main(): django_mailman3.location, 'django_mailman3', 'lib', 'scrub.py') mailman_hyperkitty_path = os.path.join( mailman_hyperkitty.location, 'mailman_hyperkitty', '__init__.py') + mailman_decorate_path = os.path.join( + mailman.location, 'mailman', 'handlers', 'decorate.py') incoming_source, incoming_changed = patch_hyperkitty(load_source(incoming_path)) tasks_source, tasks_changed = patch_hyperkitty_tasks(load_source(tasks_path)) scrub_source, scrub_changed = patch_scrubber(load_source(scrub_path)) mailman_hyperkitty_source, mailman_hyperkitty_changed = ( patch_mailman_hyperkitty(load_source(mailman_hyperkitty_path))) + mailman_decorate_source, mailman_decorate_changed = ( + patch_mailman_decorate(load_source(mailman_decorate_path))) # Compile all transformed files before replacing any one of them. compile(incoming_source, incoming_path, 'exec') compile(tasks_source, tasks_path, 'exec') compile(scrub_source, scrub_path, 'exec') compile(mailman_hyperkitty_source, mailman_hyperkitty_path, 'exec') + compile(mailman_decorate_source, mailman_decorate_path, 'exec') try: import django_extensions @@ -393,13 +442,17 @@ def main(): if mailman_hyperkitty_changed: atomic_write(mailman_hyperkitty_path, mailman_hyperkitty_source) print('CHANGED: patched {}'.format(mailman_hyperkitty_path)) + if mailman_decorate_changed: + atomic_write(mailman_decorate_path, mailman_decorate_source) + print('CHANGED: patched {}'.format(mailman_decorate_path)) for catalog_path, error in catalogs: destination = quarantine_catalog(catalog_path) print('CHANGED: quarantined {} as {} ({})'.format( catalog_path, destination, error)) if (not incoming_changed and not tasks_changed and not scrub_changed and - not mailman_hyperkitty_changed and not catalogs): + not mailman_hyperkitty_changed and not mailman_decorate_changed and + not catalogs): print('OK: compatibility fixes are already applied') diff --git a/templates/site/en/list:member:generic:footer.txt.j2 b/templates/site/en/list:member:generic:footer.txt.j2 index 34a3ae5..0a31035 100644 --- a/templates/site/en/list:member:generic:footer.txt.j2 +++ b/templates/site/en/list:member:generic:footer.txt.j2 @@ -2,3 +2,4 @@ $display_name mailing list $listname {{ mailman_site_url }}postorius/lists/$list_id/ +${hyperkitty_url} diff --git a/templates/site/it/list:member:generic:footer.txt.j2 b/templates/site/it/list:member:generic:footer.txt.j2 index 34a3ae5..0a31035 100644 --- a/templates/site/it/list:member:generic:footer.txt.j2 +++ b/templates/site/it/list:member:generic:footer.txt.j2 @@ -2,3 +2,4 @@ $display_name mailing list $listname {{ mailman_site_url }}postorius/lists/$list_id/ +${hyperkitty_url}