-
Notifications
You must be signed in to change notification settings - Fork 15
Email Template Sender Rules
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.
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.
There is no ordering to get wrong, because ordering does not decide anything:
| Level | Example | Beats |
|---|---|---|
| An exact address | alerts@a.com |
everything below |
| A domain | a.com |
Everyone |
| Everyone (no rules) | β | β |
So a template naming alerts@a.com 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.
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.
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 1Exactly 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.
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.
Type an address, pick an event, and FreeITSM names the template that would be sent and why:
someone@a.com -> "Customer reply" would be sent, because a rule
names the domain @a.com.
someone@c.com -> 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.
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.
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 | someone@c.com | 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.
Type either form into the box; the @ decides which it is.
| You type | Stored as | Matches |
|---|---|---|
someone@a.com |
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.
| 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.
- 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
FreeITSM β an open-source IT Service Management platform Β· github.com/edmozley/freeitsm Β· MIT licence
- Installation
- β° Scheduled tasks (cron jobs)
- Architecture
- AI Providers
- Internationalisation (i18n)
- Timezones & Time Handling
- Theming & Dark Mode
- β¨οΈ Command palette (βK)
- π Searching inside tickets
- π Attached documents
- MobileβFriendly
-
Security
- Layer 1 β which modules you can enter
- β³ π§© Module Access Control
- β³ π οΈ Module Access β Developer Guide
- Layer 2 β what you can administer
- β³ π Roles & Permissions
- β³ π οΈ Roles β Developer Guide
- β³ π€ Why capabilities are constants
- Layer 3 β the System module
- β³ π Admin Access Control
- Hardening
- β³ π Security review response 2026-08
- β³ π‘οΈ Security hardening 2026-08
- β³ π οΈ Security hardening 2026-08 β Developer Guide
- β³ π‘οΈ Round three β plain English
- β³ π οΈ Round three β Developer Guide
- Single Sign-On (SSO)
- ποΈ LDAP & Active Directory
- Browser Extension
- API Reference
-
π REST API β how it works
- β³ π« REST API: Tickets
- β³ π» REST API: Assets
- β³ π΄ REST API: Problems
- β³ π REST API: Changes
- β³ π REST API: Knowledge
- β³ β REST API: Tasks
- β³ ποΈ REST API: CMDB
- β³ π REST API: Contracts
- β³ ποΈ REST API: Calendar
- β³ πΏ REST API: Software
- β³ π¦ REST API: Service Status
- β³ βοΈ REST API: Morning Checks
- β³ π REST API: Forms
- β³ βοΈ REST API: Workflow
- β³ πΊοΈ REST API: Network Mapper
- β³ π§ Using the API docs page
- β³ π OpenAPI specification
- β³ β OpenAPI: kept correct
- β³ π οΈ Maintaining the catalogue
- Watchtower
-
Tickets
- β³ Mailbox Authentication
- β³ π€ Email send log
- β³ Basic IMAP mailboxes
- β³ Email rendering & images
- β³ SLA Management
- β³ WhatsApp channel
- β³ π¬ Web chat channel
- β³ π£ Slack channel
- β³ π Linking tickets
- β³ ποΈ Canned responses
- β³ βοΈ Limiting replies to particular senders
- β³ βοΈ Email signatures
- β³ π The public web address
- β³ π Raising a ticket for someone else
- β³ π Merging tickets
- β³ β Splitting tickets
- β³ β Selecting several tickets
- β³ π οΈ Snoozing tickets β Developer Guide
- β³ π₯ Collision detection
- β³ β±οΈ Time tracking
- Problem Management
- Tasks
- Assets
- Knowledge
- Change Management
- Calendar
- Morning Checks
- Reporting
- Software
- Forms
- Contracts
- Service Status
- π Notifications
- π¨ War Room
- Self-Service Portal
- LMS
- Process Mapper
- CMDB
- Network Mapper
- Workflows
- Issue trackers (Jira, Azure DevOps)
- System
-
Overview
- β³ π Progress tracker
- β³ Concepts & vocabulary
- β³ Email routing & mailboxes
- β³ Settings: global vs per-company
- β³ Users & self-service
- β³ Staff cross-company access
- β³ Worked examples
- β³ Pitfalls & gotchas
- β³ Scope: what it's for
- β³ π οΈ Developer Guide (make a module multi-company)
- β³ ποΈ Case study: CMDB (a linked graph)
- β³ π§ͺ Test harness (prove it's isolated)