(Sorry, didn't realize the forum wouldn't like ".md" files... here is the write up

I
TRIED to get the formatting fixed from my original markdown file, but I'm sure I missed some!)
QR Code Threat Detection on Proxmox Mail Gateway
Summary
Adds QR-code decoding to SpamAssassin on PMG using the community
(
https://github.com/mxguardian/Mail-SpamAssassin-Plugin-QRCode )
plugin. Any URL found encoded in a QR code image (attached or inline) is
extracted and registered with SpamAssassin the same way a normal in-body link
is - meaning it automatically flows through existing URIBL/SURBL/domain-reputation
checks, not just a flat static score.
Tested on PMG 9.1.2 (Debian 13.5 "Trixie", SpamAssassin 4.0.2).
What this does and doesn't do
- Decodes QR codes in image attachments (PNG/JPEG etc., via ImageMagick + ZBar)
- Registers the decoded URL via `
add_uri_detail_list` - the *
same* internal
API SpamAssassin uses for URLs found in message bodies, so it's evaluated by
your existing URIBL/SURBL/KAM domain-reputation rules for free
- Does
not decode QR codes in PDFs by default (disabled here -
`
qrcode_scan_pdf 0` - since PDF rasterization drags in Ghostscript and is a
separate can of worms; enable only if you specifically need it)
- Does
not insert the decoded URL into the visible message body or headers. It's
purely a scoring-pipeline addition. Getting the URL visible in-body would
require a separate MIME-rewriting content filter, but at a quick glance, it
should be possible to get the decoded URL inserted into the headers by adding
a few lines to the plugin..
- Cannot decode a QR code that's a *
remote-hosted image link* rather than
actual image bytes in the message (some "insert QR code" tools embed a
<img src="https://..."> reference instead of the image itself - nothing
for the plugin to scan in that case)
Prerequisites
PMG ships as a lean filtering appliance - no compiler, and ImageMagick isn't
installed. Both are needed. Get the following bits installed.
apt update
apt install --no-install-recommends imagemagick libimage-magick-perl \
libzbar0t64 libzbar-dev cpanminus build-essential
Gotcha - Recommends bloat: installing `imagemagick` without
`--no-install-recommends` pulls in a *
lot* of irrelevant desktop packages
(GTK3, `
adwaita-icon-theme`, `
at-spi2`, font packages, X11 libs) that exist
only to support ImageMagick's `
display/animate` GUI tools - meaningless on
a headless mail gateway. Always use `
--no-install-recommends` here.
Not a gotcha, just how it is: even with `
--no-install-recommends`, you'll
still see ~200 packages / ~300MB install. That's *hard* dependencies, not
Recommends - Debian's `imagemagick` package supports essentially every image
format via required delegate libraries (SVG via librsvg2/Cairo/Pango, HEIF,
DjVu, WebP, RAW, WMF, JPEG-XR, etc.). This is normal and not worth fighting;
building ImageMagick from source to strip delegates trades disk space for
losing automatic CVE patching via apt - not a good trade on a mail filter.
Gotcha - no compiler by default: `
Barcode::ZBar` (the Perl binding to
libzbar, not packaged for Debian) has to compile from CPAN, and PMG has no
`
gcc/make` out of the box. `
cpanm Barcode::ZBar` will fail with
`
Couldn't find your C compiler` until `
build-essential` is installed. Worth
a conscious call: this puts a compiler on an internet-facing filtering
appliance. If that's a concern in your environment, build the module on a
throwaway Debian 13 VM instead and copy the compiled module over rather than
installing `
build-essential` on PMG itself. Somewhere in-between, you can always
run `
apt purge build-essential` when installation is complete to remove the compiler.
Once the prerequisites are installed, check whether apt's `
libimage-magick-perl`
already gives a working `
Image::Magick` Perl binding before reaching for cpan:
bash
perl -MImage::Magick -e 'print "ok\n"'
If that prints `ok`, no cpan fallback needed for Image::Magick. Only
`
Barcode::ZBar` needs to come from CPAN - it isn't packaged for Debian at all.
cpanm Barcode::ZBar
If it fails, check the actual error rather than guessing:
tail -50 ~/.cpanm/work/*/build.log
Installing the plugin
Now that those pieces are in place, it is time to install the actual
SpamAssassin QRConde plugin:
wget -O /usr/share/perl5/Mail/SpamAssassin/Plugin/QRCode.pm \
https://raw.githubusercontent.com/mxguardian/Mail-SpamAssassin-Plugin-QRCode/main/lib/Mail/SpamAssassin/Plugin/QRCode.pm
Configuration
Gotcha - don't use `
local.cf` or `
init.pre`. PMG regenerates these from
its own templates on config sync / upgrades and will silently wipe manual
edits. PMG includes `
/etc/mail/spamassassin/custom.cf` by default, and that
file is left alone by PMG's own tooling - put everything there:
cat >> /etc/mail/spamassassin/custom.cf << 'EOF'
# QR code decoding
loadplugin Mail::SpamAssassin::Plugin::QRCode
ifplugin Mail::SpamAssassin::Plugin::QRCode
qrcode_min_width 100
qrcode_min_height 100
qrcode_scan_pdf 0
endif
uri_detail HAS_QRCODE_URI type =~ /^qrcode$/
describe HAS_QRCODE_URI Message contains a URI embedded in a QR code
score HAS_QRCODE_URI 3.0
EOF
Note the config option is `
qrcode_min_width` (no underscore after "qr") -
easy typo to make from memory.
Keep the score modest. The real detection value comes from the decoded URL
getting evaluated by your existing URIBL/SURBL/domain-reputation rules, not
from this flat rule alone - a high static score here is a blunt instrument
that will false-positive on the many legitimate QR codes in normal mail
(restaurants, event tickets, marketing, MFA setup, etc.). But it sure can be handy for testing!
Verify before going live
spamassassin --lint -D all 2>&1 | grep -i qrcode
Look for the plugin loading from `
@INC`, `
HAS_QRCODE_URI` being registered,
and both `
finish_parsing_start` and `
parsed_metadata` hooks being
implemented - no errors in between. A `
Can't locate` error means the `.pm`
landed in the wrong path or isn't in `@INC`.
Deploying
systemctl restart pmg-smtp-filter
systemctl status pmg-smtp-filter --no-pager | head -5
Biggest gotcha of the whole process: `
pmgconfig sync --restart 1` does
not reliably restart `
pmg-smtp-filter` after a manual `
custom.cf` edit.
That command syncs PMG's own database-driven config to its template files
and only restarts if it detects a change to config *it* manages - since
`
custom.cf` is deliberately left alone by PMG's tooling (that's why we use
it), editing it doesn't register as a change PMG notices. Confirm the
restart actually happened by checking the `
Active:` timestamp in
`
systemctl status` - if it's not from moments ago, the plugin is not
actually running yet even though it lints clean. When in doubt, just
`
systemctl restart pmg-smtp-filter` directly.
Testing
Send a real test message to yourself with a QR code image (≥100×100px,
PNG or JPEG - SVG won't work, and remote-hosted image links won't work)
encoding some throwaway test URL.
Check the SpamAssassin hit list in the mail log:
grep "SA score" /var/log/mail.log | tail -5
Look for `
HAS_QRCODE_URI` in the `hits=(...)` list, ideally alongside a
URIBL/SURBL/KAM hit if your test URL's domain has any reputation signal at
all - that's confirmation the decoded URL is really flowing through normal
link evaluation, not just triggering the static rule.
Quick troubleshooting checklist
If a test message doesn't show `
HAS_QRCODE_URI`:
1. Did `
pmg-smtp-filter` actually restart since the config change? Check the
`Active:` timestamp, not just that `pmgconfig sync` ran.
2. Is the QR image actually embedded in the message (attached or inline
`cid:`), or is it a remote URL reference the plugin never receives bytes
for?
3. Is the image ≥100×100px?
4. Is it a bitmap format (PNG/JPEG), not SVG?
5. Re-run `
spamassassin --lint -D all 2>&1 | grep -i qrcode` to confirm the
plugin and rule are still registered after any config changes.