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:
| Package | File patched | Effect |
|---|---|---|
| postorius | postorius/forms/list_forms.py | CAPTCHA on anonymous list subscription form |
| django-allauth | allauth/account/forms.py | CAPTCHA on login, signup, and password-reset forms |
| Django (form) | django/contrib/admin/forms.py | CAPTCHA field added to admin auth form |
| Django (template) | django/contrib/admin/templates/admin/login.html | CAPTCHA widget rendered on /admin/ login page |
/opt/mailman/venv/)/etc/mailman3/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.
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:
mailman3-postorius-forms-list_forms.py-CAPTCHA.patchmailman3-allauth-account-forms.py-CAPTCHA.patchmailman3-django-contrib-admin-forms.py-CAPTCHA.patchmailman3-django-contrib-admin-templates-admin-login.html-CAPTCHA.patchdjango_multi_captcha_support.py (the dispatcher module — not a patch file)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).
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.
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
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).
# 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.
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.
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:
| Value | Service | Required pip package |
|---|---|---|
'recaptchaV2C' | Google reCAPTCHA v2 Checkbox | django-recaptcha |
'recaptchaV2I' | Google reCAPTCHA v2 Invisible | django-recaptcha |
'recaptchaV3' | Google reCAPTCHA v3 (score-based) | django-recaptcha |
'hcaptcha' | hCaptcha | django-hcaptcha |
'friendlycaptcha' | Friendly Captcha | django-friendly-captcha |
'turnstile' | Cloudflare Turnstile | django-turnstile |
mailman-web collectstatic --noinput
systemctl restart mailman3 mailmanweb
Check the service came up cleanly:
systemctl status mailman3 mailmanweb
journalctl -u mailmanweb -n 20
After restarting, confirm the integration is working:
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)
"
Visit the following pages on your site and look for the reCAPTCHA badge (bottom-right corner):
https://lists.yourdomain/mm/lists/<listname>/https://lists.yourdomain/accounts/login/https://lists.yourdomain/admin/login/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.
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."
~/mailman3-rpm/django_multi_captcha_support.py copied to site-packages.rej files foundCAPTCHA_SERVICE and keys set in /etc/mailman3/settings.pydjango_recaptcha (or chosen service) in INSTALLED_APPSThe 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
| Symptom | Cause | Fix |
|---|---|---|
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 |