Email Template Sender Rules - edmozley/freeitsm GitHub Wiki
Limiting automatic replies to particular senders
An email template can be scoped to an address, a domain, or several of either — so the auto-reply your team sees is not the one a stranger gets.
Set at Tickets → Settings → Email Templates, on each template.
Asked for in discussion #80, released as update #1126.
1. The problem this had to solve first
Scoping templates is easy. Scoping them without creating a trap is the whole job.
The obvious design is a rule list evaluated top to bottom, first match wins — how firewalls work, and how most people expect it to. It has a failure mode that fits this feature exactly:
Set up two templates, one for
@a.comand one for@b.com. Both correct, both tested, everybody happy. Twelve months laterc.comis onboarded by somebody who has never seen this screen. They match nothing. They get silence. Nothing is broken, nothing is logged, and there is no error to search for — just a customer who says they never heard back.
And separately: it asks an administrator to get two things right, the rules and the ordering, when only one of them is visible in what they are configuring.
2. The most specific template wins
There is no ordering to get wrong, because ordering does not decide anything:
| Level | Example | Beats |
|---|---|---|
| An exact address | [email protected] |
everything below |
| A domain | a.com |
Everyone |
| Everyone (no rules) | — | — |
So a template naming [email protected] wins for that sender even when a template for a.com exists, and both win over the general one. Dragging templates up and down cannot change which one is sent. display_order survives only as a tie-break between two templates of identical specificity, which the screen flags rather than resolving quietly.
This is also the rule you would guess. A template written for one customer's domain obviously ought to beat the generic one, and nobody expects to have to say so.
No rules means everyone, not nobody
The empty case is the permissive one, deliberately. A template nobody has restricted keeps behaving exactly as it did before this feature existed, and a new template starts unrestricted — so narrowing is always a deliberate act, and an installation has a catch-all unless somebody removes it on purpose.
Inverting that would make "forgot to add a rule" mean silence, which is the failure this design is built to avoid.
3. Something worth knowing about how it used to work
Before this, the template picker was:
SELECT * FROM ticket_email_templates
WHERE event_trigger = ? AND is_active = 1
ORDER BY display_order ASC, id ASC
LIMIT 1
Exactly one template per event was ever used — whichever sorted first — and any others were silently ignored. If you have ever added a second template for New ticket from email and wondered why nothing changed, that is why. So ordering was already deciding everything; it just looked like a display preference.
4. Three things guard the gap
Restricting templates makes it possible for a sender to match nothing. Each of these catches that at a different moment, and the third is the one that matters most.
Check what a sender would get
Type an address, pick an event, and FreeITSM names the template that would be sent and why:
[email protected] -> "Customer reply" would be sent, because a rule
names the domain @a.com.
[email protected] -> No reply would be sent. This address matches none
of the templates for that event, and none of them
applies to everyone.
It calls the same function that chooses the template for real (templateSelectForRecipient()), rather than a second implementation of the matching in the browser. A simulator that can disagree with reality is worse than none.
A warning when there is no catch-all
If every active template for an event is restricted, the screen says so and names the events affected. It can be dismissed, because it is not always wrong: an installation that deliberately only auto-replies to known customers is doing this on purpose, and a warning that cannot be silenced is one people stop reading.
The warning is suppressed entirely if the screen could not load its settings — an administrator who has already acknowledged it should not be nagged again because a request failed.
The one that works when nobody is looking 🔑
Both of the above require somebody to be on the settings screen. In the twelve-months-later scenario nobody is — that is the entire premise.
So when an automatic email is not sent because no template matched, that is written down at the moment it happens, against the ticket and in the mailbox's outbound log:
| Time | To | Route | Result |
|---|---|---|---|
| 09:14 | [email protected] | Ticket template | Not sent |
No email template applies to this sender. Every template for this event is limited to particular senders, and none of them covers this one.
The question is then answerable by anybody looking at the ticket, without knowing this feature exists.
The outbound log had two outcomes before this — sent and failed — and rendered anything that was not a failure as Sent. A deliberate non-send would have been labelled Sent, which is worse than saying nothing: it puts a confident wrong answer in the one place somebody goes looking. It is now a third state.
5. Writing the rules
Type either form into the box; the @ decides which it is.
| You type | Stored as | Matches |
|---|---|---|
[email protected] |
address | that one sender |
a.com |
domain | anyone at a.com |
@a.com |
domain | the same — the @ is stripped |
Case and surrounding spaces do not matter; everything is folded to lower case on the way in. Duplicates are dropped rather than stored twice.
A domain with no dot in it, or an address that is not one, is refused at save time rather than stored. A rule that can never match would look exactly like a working rule on the screen, and quietly do nothing — which is the fault this whole feature is trying not to have.
6. 📁 For developers
| File | Role |
|---|---|
includes/template_email.php |
templateSelectForRecipient() — the matcher, and the only place specificity is decided. getActiveTemplate() remains as a thin wrapper |
includes/email_log.php |
emailLogSkipped() — the deliberate non-send, status = 'skipped' |
api/tickets/simulate_email_template.php |
The simulator. Calls the matcher; implements nothing itself |
api/tickets/save_email_template.php |
Validates every rule before the template is written — there is no transaction, so a rule rejected afterwards would leave the template saved while the caller is told the save failed |
api/tickets/delete_email_template.php |
Removes the rules too. There is no foreign key, and an AUTO_INCREMENT id can be reissued after a restart |
ticket_email_template_rules |
template_id, match_type (address|domain), match_value |
Two behaviours are load-bearing and easy to undo by accident:
- An absent
ruleskey means "leave them alone"; an empty array means "everyone". Collapsing the two would let any client that posts no rules silently unrestrict every template it saves. - A missing
ticket_email_template_rulestable yields no rules, which means every template applies to everyone — i.e. a part-upgraded install behaves exactly as it did before the feature, rather than falling silent.
sendTemplateEmail() now builds the merge data before choosing the template, because the choice depends on the recipient. That order used to be the other way round.
Related pages
- Sender rules — Developer Guide — how the matcher works, and what not to undo
- The public web address — the other half of discussion #80, and
[ticket_url] - Email signatures — request 3 from the same discussion
- Email Send Log — where Not sent appears
- Mailbox authentication — where email tickets come from in the first place