Contact Form
The contact form on the public site (www.nortonshop.net) submits to admin.nortonshop.net/send-mail.php, which authenticates to Migadu’s SMTP server using PHPMailer and sends the message to the shop’s mailbox. PHP’s built-in mail() is not used — the PHP container has no local MTA, so outbound mail is relayed entirely through Migadu.
Mail flow
- Customer fills the contact form on the public site.
- Browser POSTs to
https://admin.nortonshop.net/send-mail.phpover HTTPS (CORS allowswww.nortonshop.netas an origin). - PHP validates input (captcha, honeypot, rate limit, file type) then hands the message to PHPMailer.
- PHPMailer opens an authenticated TLS connection to
smtp.migadu.com:465, logs in asinfo@nortonshop.net, and submits the message. - Migadu delivers the email to the
info@nortonshop.netmailbox (same address on both ends — the customer’s email is set asReply-To, so hitting reply in Mail.app goes to the customer).
PHPMailer location
Three source files are vendored at admin/lib/PHPMailer/, committed to the repo so they deploy via git pull:
PHPMailer.phpSMTP.phpException.php
Pinned to a release tag (currently v6.9.3). To upgrade, download fresh copies from https://github.com/PHPMailer/PHPMailer/raw/<tag>/src/ and commit.
Environment variables (docker-compose.yml)
The php service in /opt/server-stack/docker-compose.yml passes SMTP credentials to send-mail.php:
environment:
CONTACT_TO: info@nortonshop.net
CONTACT_FROM: info@nortonshop.net
SMTP_HOST: smtp.migadu.com
SMTP_PORT: "465"
SMTP_SECURE: ssl
SMTP_USER: info@nortonshop.net
SMTP_PASS: "${SMTP_PASS}"
${SMTP_PASS} is substituted at container start from /opt/server-stack/.env:
SMTP_PASS=the-migadu-mailbox-password
$ character, escape it as $$ in .env — Docker Compose treats $ as variable substitution, and each $$ collapses to a single $ when passed into the container. After editing .env, run docker compose up -d php and verify with docker compose exec php env | grep SMTP_PASS.
/opt/server-stack/ is not a git repository, so .env is safe there. Do not commit .env to the site repo.
Built-in protections
- Honeypot field (hidden input that bots fill in) — returns fake success
- Microproof captcha — self-hosted, blocks headless browsers that don’t execute JS (no external dependencies)
- Rate limiting — 3 emails per 5 minutes per IP (file-based, with
LOCK_EX) - Control character sanitisation on name and email fields (prevents header injection)
- Length limits — 200 chars on name/email, 10,000 on message
- Attachment validation — JPG, PNG, or WebP, up to 5 MB, verified by MIME sniff +
getimagesize() - CORS — only
www.nortonshop.net,admin.nortonshop.net, and localhost origins can POST
Changing SMTP provider
To switch from Migadu to any other SMTP provider (Mailjet, Fastmail, Brevo, etc.): update the five SMTP_* vars in docker-compose.yml, change the password in .env, and run docker compose up -d php. No code changes required. For providers that use STARTTLS on port 587, set SMTP_PORT: "587" and SMTP_SECURE: tls.
Diagnosing delivery failures
docker compose logs php --since=5m | grep -i 'contact\|smtp\|error'
PHPMailer errors are written to PHP's error log prefixed with Contact form SMTP error:. The most common causes are wrong password (Migadu returns 535 Authentication failed), missing $$ escaping in .env (blank password), or Migadu rate limits (10 outbound/day on the Micro tier).