Adding reCAPTCHA v3 to Mailman3

Complete implementation guide for a Python virtualenv install on Debian 12
📦 Postorius 1.3.13 🐍 Django 4.2.27 🔐 django-allauth 65.14.0 📅 May 2026

1 · Overview

The pbiering/mailman3-rpm repository ships patch files that inject CAPTCHA support into Mailman3's web interface. The patches work by importing a small dispatcher module (django_multi_captcha_support.py) that routes to whichever CAPTCHA service is configured in settings.py.

Four locations are patched:

PackageFile patchedEffect
postoriuspostorius/forms/list_forms.pyCAPTCHA on anonymous list subscription form
django-allauthallauth/account/forms.pyCAPTCHA on login, signup, and password-reset forms
Django (form)django/contrib/admin/forms.pyCAPTCHA field added to admin auth form
Django (template)django/contrib/admin/templates/admin/login.htmlCAPTCHA widget rendered on /admin/ login page
ℹ️ reCAPTCHA v3 is invisible
v3 scores user behaviour silently — there is no checkbox or puzzle shown to users. The only visible element is a small badge in the bottom-right corner of the page.

2 · Prerequisites

3 · Step-by-Step Implementation

Step 1 — Activate the virtualenv and check package versions
source /opt/mailman/venv/bin/activate
python -m pip show postorius django django-allauth

This guide was tested against: Postorius 1.3.13 · Django 4.2.27 · django-allauth 65.14.0. If your versions differ significantly, patch offsets may vary.

Step 2 — Clone the patch repository
cd ~
git clone https://github.com/pbiering/mailman3-rpm.git
cd mailman3-rpm
ls *.patch   # verify patch files are present

The CAPTCHA-related patch files are:

Step 3 — Install CAPTCHA Python libraries

Install all four libraries so the dispatcher module's imports always succeed regardless of which service is configured:

pip install django-recaptcha django-hcaptcha django-friendly-captcha django-turnstile

Alternatively, install only the library for your chosen service (e.g. pip install django-recaptcha for reCAPTCHA), but then you must also deploy the patched django_multi_captcha_support.py that uses lazy imports (see Appendix A).

Step 4 — Deploy the multicaptcha dispatcher module
cp ~/mailman3-rpm/django_multi_captcha_support.py \
   /opt/mailman/venv/lib/python3.11/site-packages/

This module is imported by every patched file. It reads CAPTCHA_SERVICE from Django settings and returns the appropriate field object.

Step 5 — Apply the Postorius patch
VENV=/opt/mailman/venv/lib/python3.11/site-packages

patch --directory="$VENV" -p0 \
  < ~/mailman3-rpm/mailman3-postorius-forms-list_forms.py-CAPTCHA.patch

Expected output: patching file postorius/forms/list_forms.py

Step 6 — Apply the django-allauth patch

django-allauth 65.x has structural differences from the version the patch was written for. The --fuzz=4 flag is required; all changes still land in the correct locations.

patch --directory="$VENV" -p0 --fuzz=4 \
  < ~/mailman3-rpm/mailman3-allauth-account-forms.py-CAPTCHA.patch

Expected output (offset warnings are normal and expected):

patching file allauth/account/forms.py
Hunk #1 succeeded at 35 (offset 4 lines).
Hunk #2 succeeded at 67 with fuzz 4 (offset -23 lines).
Hunk #3 succeeded at 276 with fuzz 2 (offset -12 lines).
Hunk #4 succeeded at 544 with fuzz 2 (offset -27 lines).
Step 7 — Apply the Django admin patches
# Patch the Python authentication form
patch --directory="$VENV" -p0 \
  < ~/mailman3-rpm/mailman3-django-contrib-admin-forms.py-CAPTCHA.patch

# Patch the HTML login template
patch --directory="$VENV" -p0 \
  < ~/mailman3-rpm/mailman3-django-contrib-admin-templates-admin-login.html-CAPTCHA.patch

Both should apply cleanly with no fuzz or offset warnings.

Step 8 — Verify no failed hunks
find /opt/mailman/venv -name "*.rej" 2>/dev/null

This should return no output. If .rej files are found, open them — they show exactly which lines the patch couldn't apply. Apply those edits manually in the target file, then delete the .rej file.

Step 9 — Configure CAPTCHA in /etc/mailman3/settings.py

Add the following block at the end of /etc/mailman3/settings.py. This example uses reCAPTCHA v3:

### CAPTCHA support
CAPTCHA_SERVICE = 'recaptchaV3'
RECAPTCHA_PUBLIC_KEY_V3  = 'your-site-key-here'
RECAPTCHA_PRIVATE_KEY_V3 = 'your-secret-key-here'
INSTALLED_APPS += ['django_recaptcha']

Other supported values for CAPTCHA_SERVICE:

ValueServiceRequired pip package
'recaptchaV2C'Google reCAPTCHA v2 Checkboxdjango-recaptcha
'recaptchaV2I'Google reCAPTCHA v2 Invisibledjango-recaptcha
'recaptchaV3'Google reCAPTCHA v3 (score-based)django-recaptcha
'hcaptcha'hCaptchadjango-hcaptcha
'friendlycaptcha'Friendly Captchadjango-friendly-captcha
'turnstile'Cloudflare Turnstiledjango-turnstile
Step 10 — Collect static files and restart services
mailman-web collectstatic --noinput
systemctl restart mailman3 mailmanweb

Check the service came up cleanly:

systemctl status mailman3 mailmanweb
journalctl -u mailmanweb -n 20

4 · Verification

After restarting, confirm the integration is working:

4.1 Check INSTALLED_APPS

source /opt/mailman/venv/bin/activate
python -c "
import django, os
os.environ.setdefault('DJANGO_SETTINGS_MODULE','mailman_web.settings')
django.setup()
from django.conf import settings
print('django_recaptcha in INSTALLED_APPS:', 'django_recaptcha' in settings.INSTALLED_APPS)
print('CAPTCHA_SERVICE:', settings.CAPTCHA_SERVICE)
"

4.2 Visual check

Visit the following pages on your site and look for the reCAPTCHA badge (bottom-right corner):

✅ Confirmed working
The reCAPTCHA badge appearing in the bottom-right corner of the page confirms the JavaScript is loading and v3 scoring is active. v3 requires no user interaction — scoring happens silently in the background.

5 · Important Maintenance Notes

⚠️ Patches are lost on pip upgrades
The patches modify files directly inside the virtualenv. Running pip install --upgrade postorius (or django, or django-allauth) will overwrite the patched files and silently remove CAPTCHA support. Re-run the patch commands after every upgrade.

Re-apply script

Save the following as /opt/mailman/apply-captcha-patches.sh and run it after any package upgrade:

#!/bin/bash
# Re-apply CAPTCHA patches after pip upgrades
set -e
VENV=/opt/mailman/venv/lib/python3.11/site-packages
PATCHES=~/mailman3-rpm

source /opt/mailman/venv/bin/activate

cp "$PATCHES/django_multi_captcha_support.py" "$VENV/"

patch --directory="$VENV" -p0 \
  < "$PATCHES/mailman3-postorius-forms-list_forms.py-CAPTCHA.patch"

patch --directory="$VENV" -p0 --fuzz=4 \
  < "$PATCHES/mailman3-allauth-account-forms.py-CAPTCHA.patch"

patch --directory="$VENV" -p0 \
  < "$PATCHES/mailman3-django-contrib-admin-forms.py-CAPTCHA.patch"

patch --directory="$VENV" -p0 \
  < "$PATCHES/mailman3-django-contrib-admin-templates-admin-login.html-CAPTCHA.patch"

mailman-web collectstatic --noinput
systemctl restart mailmanweb

echo "Done."

6 · Completion Checklist

Appendix A — Lazy-import variant of django_multi_captcha_support.py

The original django_multi_captcha_support.py imports all four captcha libraries at module load time. If only one library is installed, Django will refuse to start with a ModuleNotFoundError. The patched version below uses lazy imports — only the configured service's library is imported at runtime:

#### Multi CAPTCHA Support for Django — lazy-import variant
# Only imports the library for the configured CAPTCHA_SERVICE.
from django.conf import settings

def multicaptcha(action):
    captcha_service = getattr(settings, 'CAPTCHA_SERVICE', None)
    if captcha_service is not None:
        if captcha_service in ('recaptcha', 'recaptchaV2C'):
            from django_recaptcha.fields import ReCaptchaField
            from django_recaptcha.widgets import ReCaptchaV2Checkbox
            pub = getattr(settings, 'RECAPTCHA_PUBLIC_KEY_V2C', None)
            prv = getattr(settings, 'RECAPTCHA_PRIVATE_KEY_V2C', None)
            return ReCaptchaField(widget=ReCaptchaV2Checkbox,
                **({'public_key': pub, 'private_key': prv} if pub else {}))
        elif captcha_service == 'recaptchaV2I':
            from django_recaptcha.fields import ReCaptchaField
            from django_recaptcha.widgets import ReCaptchaV2Invisible
            pub = getattr(settings, 'RECAPTCHA_PUBLIC_KEY_V2I', None)
            prv = getattr(settings, 'RECAPTCHA_PRIVATE_KEY_V2I', None)
            return ReCaptchaField(widget=ReCaptchaV2Invisible,
                **({'public_key': pub, 'private_key': prv} if pub else {}))
        elif captcha_service == 'recaptchaV3':
            from django_recaptcha.fields import ReCaptchaField
            from django_recaptcha.widgets import ReCaptchaV3
            pub = getattr(settings, 'RECAPTCHA_PUBLIC_KEY_V3', None)
            prv = getattr(settings, 'RECAPTCHA_PRIVATE_KEY_V3', None)
            return ReCaptchaField(widget=ReCaptchaV3(action=action),
                **({'public_key': pub, 'private_key': prv} if pub else {}))
        elif captcha_service == 'hcaptcha':
            from hcaptcha.fields import hCaptchaField
            return hCaptchaField()
        elif captcha_service == 'friendlycaptcha':
            from friendly_captcha.fields import FrcCaptchaField
            return FrcCaptchaField()
        elif captcha_service == 'turnstile':
            from turnstile.fields import TurnstileField
            return TurnstileField()
    return None

Appendix B — Troubleshooting

SymptomCauseFix
No module named 'django_recaptcha' in journalctl Library not installed in venv pip install django-recaptcha
Service starts but CAPTCHA badge not visible INSTALLED_APPS not updated Confirm INSTALLED_APPS += ['django_recaptcha'] in settings.py
Patch fails: Hunk FAILED Version drift from patch target Try --fuzz=4; if still fails, apply .rej manually
CAPTCHA badge disappears after upgrade pip upgrade overwrote patched files Re-run the apply-captcha-patches.sh script
All submissions blocked / CAPTCHA always fails Keys not registered for the correct domain Check domain list at google.com/recaptcha/admin
gunicorn: Worker failed to boot Import error at startup Run journalctl -u mailmanweb -n 50 to find the specific module error