Configuration
Settings live in two places, and which one you want depends on whether the answer could ever differ between two forms on the same site.
- Bandage → Settings — storage, retention, the honeypot field name, the digest. Global by nature.
- Bandage → Forms → a form — recipients, spam, validation, routing, attachments, the autoresponder, integrations. Per form.
Forms are project config. They are configuration, not content: reviewable in a pull request, deployed with the rest of the config, and identical in every environment.
Plugin settings
| Setting | Default | What it does |
|---|---|---|
| Store submissions | on | The master switch. Off means Bandage still filters spam and still validates, but keeps no record. |
| Store message body | on | Off keeps who wrote and when, and drops what they said. |
| Store metadata | on | IP address, user agent, referrer. |
| Retention (days) | 0 | Delete submissions older than this. Zero keeps them forever. |
| Spam retention (days) | 30 | Delete spam sooner than everything else. |
| Anonymise after (days) | 0 | Blank stored IP addresses, keep the message. Zero disables. |
| Honeypot field name | bandage_website | Change it per site — bots learn field names. |
| Timestamp field name | bandage_ts | Carries the signed render time. Must differ from the honeypot. |
| Unread badge | on | The count on the CP nav item. |
| Queue notifications | off | Send the admin email on the queue rather than in the request. |
| Digest | off | See below. |
| Spam rejection message | built in | Shown when the spam action is reject. |
| Duplicate rejection message | built in | Shown when a duplicate is rejected. |
Retention is off by default on purpose. It is a legal question with a technical answer, and the answer being "never" until somebody decides otherwise is the safe direction to be wrong in.
Form profiles
Lite has one form, default, created at install. Pro adds as many as you like, each addressed by
handle from the template:
{{ craft.bandage.form('support') }}
A profile holds:
Recipients. To, Cc and Bcc, one address per line or comma-separated. Leave To empty to
fall back to Contact Form's own toEmail setting. Addresses may be $ENV_VARS.
Subject and body templates. Twig object templates, rendered against the submission. Leave them empty and Contact Form's own subject and body are used unchanged. See Usage.
Everything below.
Spam
Every check scores. Nothing vetoes. When the total reaches the threshold (default 5), the
action applies:
| Action | Stored | Sent | Visitor sees |
|---|---|---|---|
| Quarantine (default) | ✓ | — | the ordinary success message |
| Reject | ✓ | — | an error |
| Flag | ✓ | ✓ | the ordinary success message |
All three store the message. A spam folder nobody can review is deletion with extra steps, and the only way to find a false positive is to be able to look at it.
Quarantine is the default because the bot must not learn it was caught. Reject exists for forms where a human false positive needs to be told to try another way.
The checks
| Check | Default score | Notes |
|---|---|---|
| Honeypot | 10 | A field people can't see. On by default. |
| Time trap | 8 | Minimum 3s, maximum 86400s. The timestamp is signed, so the minimum can't be edited in the page. |
| Rate limit | 10 | Off (0). Submissions per IP per window (default 3600s). |
| Blocked words | 6 | Literal phrases, one per line, case-insensitive. |
| Blocked patterns | 8 | Regular expressions, one per line, tested against the whole submission. |
| Blocked senders | 10 | Addresses, or *@domain.com wildcards. |
| Blocked IPs | 10 | Addresses or CIDR ranges. |
| Disposable email | 6 | Off. Throwaway-mail providers. |
| Require MX | 8 | Off. Costs a DNS lookup per submission. |
| Link count | 5 | Off (0). Scores past N links in the body. |
| CAPTCHA | 10 | See below. |
Scoring rather than vetoing is what makes a honeypot hit on its own forgivable — a password manager filling a hidden field is not a bot — while a honeypot hit plus a twelve-link body is not.
Set any score to 0 to run a check for the record without letting it decide anything, or raise one
above the threshold to make it decisive on its own.
A maximum age below the minimum makes every submission spam, so it is refused at save. The site owner would otherwise find out from the person who couldn't reach them.
CAPTCHA
reCAPTCHA v2 (checkbox), reCAPTCHA v3 (invisible), hCaptcha and Cloudflare Turnstile. Each needs a
site key and a secret; v3 also takes a minimum score (default 0.5) below which Google's
verdict counts against the sender.
The markup comes out of craft.bandage.form() automatically. To place it yourself:
{{ craft.bandage.form('support', { captcha: false }) }}
…
{{ craft.bandage.captcha('support') }}
Verification fails open on a provider outage and closed on a genuine failure. A network error reaching Google is not evidence about the visitor; a returned "no" is. An outage that silently blocked every enquiry would be worse than the spam it kept out.
Field rules (Pro)
Contact Form validates two things: that fromEmail is an email address and that message is not
empty. Rules cover the rest.
Types: required, email, URL, number, whole number, matches pattern, length, one of, not one of. Each takes an optional custom message and an optional condition.
Every type except required passes on an empty value. "Must be a valid email address" on a field
the visitor deliberately left blank is a rule fighting its own optionality — pair it with a
required rule if you mean both.
Attachments
Off by default. Switch it on and pick a volume, and uploads are copied there before the email goes out.
| Setting | Meaning |
|---|---|
| Volume | Required once attachments are on. |
| Subpath | Optional folder inside the volume. Accepts {{ now|date('Y/m') }}-style Twig. |
| Max attachments | Per submission. 0 for no limit. |
| Max size | Megabytes per file. 0 means whatever PHP allows. |
| Allowed extensions | Empty means Craft's own allowedFileExtensions. |
Bandage validates uploads during validation, so a rejected file produces an error the visitor can see. Contact Form checks extensions after the send event and returns false having added nothing to the model — which is how you get "there was a problem with your submission" about a form in which nothing is marked wrong.
The attachment record copies filename, size and kind rather than pointing at the asset, so it still says what was sent after somebody empties the volume.
Duplicates
| Handling | Effect |
|---|---|
| Allow (default) | Nothing. |
| Flag as spam | Scores at the threshold, so it lands in the same review queue as everything else. |
| Reject with an error | The visitor is told they already sent this. |
The window is 300 seconds by default. Compared fields defaults to sender plus body; name
specific value paths to narrow it.
Autoresponder (Pro)
A confirmation back to the person who wrote in, in Twig and Markdown, optionally conditional, with their own answers and their reference included.
Max per hour defaults to 3, per recipient address, and you should think before setting it to
zero. An autoresponder is a mail relay that takes its destination from the request body. Without
a cap, anyone can point the form at a third party and have your domain — with your real SPF record
— mail-bomb them.
Integrations (Pro)
Webhooks (signed JSON), Slack and Microsoft Teams. Everything is delivered on the queue: an endpoint that has gone down must not hold the visitor's browser open, and must never fail their submission.
| Setting | Notes |
|---|---|
| Endpoint | http:// or https:// only. $ENV_VAR supported. |
| Method | POST, PUT or PATCH. Webhook only. |
| Secret | Produces X-Bandage-Signature: sha256=…, an HMAC over the exact bytes sent. |
| Fields | Send only these value paths. Empty sends everything. |
| Include metadata | IP address and user agent. Off by default. |
| Include spam | Off — which is rather the point of flagging it. |
| Timeout | 1–60 seconds, default 10. |
| Attempts | 1–10, default 3. |
Only http(s) is accepted, and that is a security boundary rather than tidiness: a file://
endpoint handed to cURL is a request-forgery primitive with a settings screen in front of it, and
whoever can reach that screen is not necessarily the person who owns the server.
The digest
Off by default. Daily or weekly, to whichever addresses you name.
It exists for a failure that is otherwise silent. A form stops working — a mail server changed, a recipient bounced, a spam rule got too tight — and nobody notices, because no email arriving looks exactly like a quiet week. A digest that says "0 submissions, 3 undelivered" is the first thing that makes the difference visible.
Craft has no scheduler, so it needs a cron entry:
php craft bandage/digest/send
Next
- Usage — Twig, conditions, routing, exports, events
- Troubleshooting — when a message doesn't arrive