-
Notifications
You must be signed in to change notification settings - Fork 15
Email Template Sender Rules Developer Guide
How a template is chosen for a recipient, why specificity replaced ordering, and the three places that record the consequence.
The user-facing page is Limiting automatic replies to particular senders.
| File | Role | |
|---|---|---|
| βοΈ | includes/template_email.php |
templateSelectForRecipient() β the only place specificity is decided. templateRulesByTemplate(), and getActiveTemplate() kept as a thin wrapper |
| π | includes/email_log.php |
emailLogSkipped() β the deliberate non-send |
| π | api/tickets/save_email_template.php |
Validates rules before any write |
| π | api/tickets/get_email_templates.php |
Attaches rules to every template |
| π | api/tickets/delete_email_template.php |
Removes the rules too |
| π | api/tickets/simulate_email_template.php |
The simulator β calls the matcher, implements nothing |
| π₯οΈ | tickets/settings/index.php |
Scope editor, scope column, no-catch-all warning, simulator |
| ποΈ | ticket_email_template_rules |
template_id, match_type, match_value
|
| π | lang/en/tickets.php |
settings.scope.* |
SELECT * FROM ticket_email_templates
WHERE event_trigger = ? AND is_active = 1
ORDER BY display_order ASC, id ASC
LIMIT 1LIMIT 1. Exactly one template per event has ever been sent β whichever sorted first β and every other template for that event was silently ignored. display_order looked like a display preference and was in fact the entire selection algorithm.
That is worth knowing before you conclude the new design added a risk. It replaced an invisible one.
templateSelectForRecipient($conn, $eventTrigger, $recipientEmail)
β ['template' => ?array, 'reason' => string, 'matched' => ?array]Resolution, most specific first:
reason |
Wins when |
|---|---|
address |
a rule names this exact address |
domain |
a rule names this domain |
everyone |
a template has no rules at all |
no_match |
templates exist, all restricted, none matched |
no_active_template |
nothing active for this event |
The alternative β evaluate top to bottom, first match wins β requires the administrator to get the rules right and the ordering right, and gives no sign when the second is wrong. Here dragging rows cannot change what is sent. display_order survives only as a tie-break between two rules of equal specificity.
The permissive case is the empty case. Three consequences, all deliberate:
- a template nobody has restricted behaves exactly as it did before this feature existed;
- a new template starts unrestricted, so an install always has a catch-all unless one is deliberately removed;
-
templateRulesByTemplate()returns[]when the table is missing, so a part-upgraded install sends exactly as before rather than falling silent.
Invert any of those and "forgot to add a rule" starts meaning "nobody gets an email".
It is what the simulator displays and what the send log records. Selection that could only answer which template would leave "why did nobody get a reply?" unanswerable twelve months later.
// merge data FIRST β the template now depends on who the email is going to
$mergeData = buildTicketMergeData($conn, $ticketId);
$choice = templateSelectForRecipient($conn, $eventTrigger, $mergeData['requester_email'] ?? '');It used to select the template and then build the merge data. Anything added to this function must not reintroduce that order.
emailLogSkipped($conn, templateGetMailboxForTicket($conn, $ticketId), 'template',
$recipient, $subject, $reason, $ticketId);Three decisions inside those five arguments:
-
Only
no_matchis logged, neverno_active_template. Nobody configured a template, so nobody is expecting an email; a row there would be noise on every install that never wanted the feature. - The mailbox is resolved even though nothing is sent. Logged against no mailbox it lands in the "sends with no mailbox" bucket, which is not where somebody asking "why did this mailbox not reply?" will look. One extra query, only on the path that sends nothing.
-
statusis'skipped', a third value in aVARCHAR(10)column β no schema change.
const failed = e.status === 'failed';
...
${failed ? 'Failed' : 'Sent'}A skipped row would have rendered as Sent β a confident wrong answer in the one place somebody goes looking, which is worse than no answer. Any new status must be added to that ternary and to the in_array() whitelist in get_mailbox_outbound.php, or it becomes invisible, mislabelled, or both.
Validate everything before writing anything. There is no transaction here:
$cleanRules = null; // absent key = leave rules alone
if (array_key_exists('rules', $data) && is_array($data['rules'])) {
$cleanRules = []; // empty array = applies to everyone
... throw on anything invalid ...
}
$conn = connectToDatabase();
// everything from here either writes or cannot throwFound by testing the rejection path: an invalid rule used to be rejected after the template UPDATE, leaving the template saved while the caller was told the save failed.
Two more:
-
Absent
rulesβ emptyrules. Collapsing them would let any older client that posts no rules silently unrestrict every template it saves. -
A domain with no dot, or an
@still in it, is refused rather than stored. A rule that can never match looks exactly like a working rule on screen and quietly does nothing β precisely the fault this feature exists to avoid.
Rules are replaced wholesale rather than diffed. The set is tiny, and a diff is where "removed a rule that stayed anyway" comes from β which here means somebody keeps receiving an email the admin believes they stopped.
delete_email_template.php deletes the rules first: there is no foreign key, and an AUTO_INCREMENT id can be reissued after a restart, which would attach a deleted template's rules to a new one.
- The simulator posts to the server. It must never re-implement matching in the browser β an answer that can disagree with reality is worse than none.
-
The warning is gated on
baseUrlState.loaded. Until the settings have actually been read we do not know whether it was dismissed, and guessing "not dismissed" nags somebody who already said they know. - The warning is per event, because a gap in one event says nothing about the others, and it only fires when templates for that event exist and none is unrestricted.
| Check | Result |
|---|---|
Restricted template at display_order 5, catch-all at 0 |
Restricted one wins β order is genuinely not consulted |
alerts@a.com with both an address and a domain rule |
reason: address |
ALERTS@A.COM |
Same match β folded to lower case |
| Catch-all deactivated, unknown sender | reason: no_match |
| The real send path, no template matching | One skipped row, with the reason, against the ticket |
| Rules round-tripped through the API | Deduped, lower-cased, @ stripped |
| Invalid domain posted with a changed template name | Refused and the name confirmed unchanged |
A new match type (a company, a group) needs: a match_type value, a branch in templateSelectForRecipient() placed at the right specificity level, validation in save_email_template.php, and a chip in the editor. Company was considered and parked β it needs the requester resolved to a company before the reply is chosen, and templates are currently install-wide with no tenant_id.
- Limiting automatic replies to particular senders β the user page
-
The public web address β
[ticket_url], from the same discussion -
Email send log β Developer Guide β where
skippedis displayed - Canned Responses β Developer Guide β the shared merge-code vocabulary
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)