Organizational Signatures Organizational Signatures Pro Edition feature. Maps to Email Policies > Org Signatures ( view_org_signatures.cfm, edit_org_signature.cfm, org_signature_delete.cfm). Hermes attaches a centrally-managed signature to outbound mail at the gateway. Admins design the signature once per domain (and optionally per department); every user on that domain gets a personalized version of it on every outbound message — no per-user setup required. Two signature types, one pipeline Hermes ships two distinct signature concepts that run through the same body milter and the same resolver: Type Tier Owner Storage Per-domain control Personal Signature Community + Pro The user (in /users/2/view_signature.cfm) user_signatures table, one row per user Toggled via domains.allow_user_signatures Organizational Signature Pro only The admin (in Email Policies > Org Signatures) org_signatures table, one row per (domain_id, department_label) One default per domain + optional per-department variants The milter never decides which one to apply at message time. The CFML resolver picks a winner per mailbox at admin-action time and writes a precomputed sender → option map; the milter just looks up the option and applies whatever it finds. Department names — single source of truth Departments are defined once on the mailbox edit form (Email Server > Mailboxes > Edit Options > Personal Information > Department), as free-text values typed by the admin. There is no separate "Departments" table; a department exists as soon as one mailbox is in it. The Org Sig form's Department field is a strict dropdown sourced from the distinct mailboxes.department values for the selected domain. This means: You cannot create an Org Sig for a dept that has no mailboxes — the dept won't appear in the dropdown. The dept name on both sides is guaranteed to match exactly. No typo-class drift. Workflow: assign at least one mailbox to the new dept first, then come back and create the Org Sig targeting it. Changing the domain in the Org Sig form repopulates the dropdown with that domain's depts via JavaScript (no AJAX round-trip; the per-domain map is dumped into a JS const at page load). The mailbox edit form's Department field is a free-text input with a typeahead showing the same per-domain dept list. Admins can pick an existing dept (auto-completes) or type a brand-new dept name (which then appears in the dropdown next time). If you edit an existing Org Sig whose department_label no longer matches any current mailbox (the dept was renamed elsewhere, or all mailboxes in it were reassigned), the orphan value is preserved in the dropdown with a (no mailboxes) suffix so you can still see and edit/delete the row instead of silently losing the value. The resolver at send time does a case-insensitive trimmed match against mailboxes.department, so casing or whitespace drift across edits is forgiving even in the rare cases the dropdown is bypassed (e.g. direct SQL changes). Resolution order For every enabled mailbox, the resolver walks this priority chain top-down and stops at the first match: 1. Personal Signature └─ if domains.allow_user_signatures = 1 AND user_signatures has an enabled, non-empty row for this mailbox ─> wins. option = user_ 2. Department Organizational Signature └─ else if mailboxes.department is non-empty AND org_signatures has enabled = 1 row matching (domain_id, department_label) ─> wins. option = org_ 3. Domain Default Organizational Signature └─ else if org_signatures has enabled = 1 row matching (domain_id, department_label IS NULL) ─> wins. option = org_ 4. None └─ no map entry; the milter applies no signature to this sender's mail. The chain is per-mailbox, not per-message. The resolver runs at admin-action time (see Triggers), serializes the winning option for every mailbox into one map file, and the milter consults that map at send time. There is no per-message DB query and no per-message resolution logic in the milter. Pipeline placement Same chain as Disclaimers (#214) — see disclaimers.md for the full Postfix / OpenDKIM / Ciphermail picture. The summarized order: External MTA / MUA submission │ ▼ Postfix smtpd ├─ smtpd_milters chain (in order): │ 1. OpenDKIM (signs/verifies) │ 2. OpenDMARC (DMARC policy) │ 3. hermes_body_milter (THIS — signatures, then disclaimers) ▼ Amavis → Ciphermail → Postfix :10026 (DKIM sign) → external Inside the body milter, SignatureModifier runs before DisclaimerModifier, so the layered output on the outbound message is: [user body] [signature] ← Personal or Organizational, picked by resolver [disclaimer] ← if a disclaimer is configured for this sender OpenDKIM signs at the :10026 re-injection — after both modifiers — so Hermes' own DKIM signature always covers the recipient's view of the message (with signature and disclaimer baked in). Templates Phase 2A ships six bundled templates under /admin/2/inc/org_signature_templates/: Template key Layout modern_card Logo left, accent bar, contact stack right two_column_pro Left contact, right org block + CTA button with_social_bar Vertical contact + horizontal social-icon row banner_with_logo Full-width banner with logo top, contact below promo_footer Contact + bottom promotional image with link compact_text Minimal text-only, no images, no styling Each template is a .cfm file that declares its field schema (text / email / url / color / checkbox / image fields with optional showIf gating) and renders pixel-perfect HTML when the renderer is invoked. Admins fill in the form on edit_org_signature.cfm; the gallery thumbnail + live sandboxed-iframe preview show the result before save. All the auto-filled fields — Name, Title, Phone, Mobile, Email ( {{user.*}} from the mailbox row) plus Website and Address ( {{org.*}} from the domain row) — are collapsed under an "Override auto-filled fields" toggle by default. The admin doesn't see or edit them in the common case; the placeholders flow through to the rendered HTML unchanged and the milter fills in the recipient's data at send time. Toggling the override on exposes the fields for the rare cases that need literal text instead of substitution (shared mailboxes without personal info, seasonal URL overrides, etc.). The genuinely admin-supplied fields stay always visible — Logo, accent color, show/hide toggles for each line, CTA text and URL, social URLs, disclaimer text, and any template-specific extras (banner height, promo image, etc.). These are the admin's actual editing surface. Net workflow: pick a template, upload a logo, set the accent color, save. Done. Every mailbox on the domain gets a fully personalized signature on its next outbound message without any per-user form input. Templates are version-controlled in the repo, not in the database. To add a new template, drop a new .cfm file in org_signature_templates/, add its key to the registry in inc/org_signature_template_loader.cfm, and produce a 240×140 thumbnail PNG. No schema migration needed. Placeholder substitution at send time The signature HTML stored in org_signatures.rendered_html (and on disk in body.html) keeps {{namespace.field}} tokens literal. The body milter substitutes them per recipient at message-send time against the sender's row in sender_data.json. Available placeholders (Phase 2B): Token Source column {{user.first_name}} mailboxes.first_name {{user.last_name}} mailboxes.last_name {{user.title}} mailboxes.title {{user.phone}} mailboxes.phone {{user.mobile}} mailboxes.mobile {{user.department}} mailboxes.department {{user.email}} mailboxes.username {{org.name}} domains.org_name {{org.phone}} domains.org_phone {{org.address}} domains.org_address {{org.website}} domains.org_website {{org.logo_url}} domains.org_logo_path (raw URL — not cid: extracted) {{dept.name}} mailboxes.department Tokens whose corresponding field is empty resolve to empty string, not literal {{...}}. So if mailboxes.title is blank for a given user, the {{user.title}} token disappears cleanly from delivered mail. Unknown namespaces (anything outside user, org, dept) are also substituted to empty. The substitution is a single regex pass on the body html and body text inside SignatureModifier.modify() — well under a millisecond per message. The map and sender_data.json both live in process memory, refreshed only when their file mtime changes. No DB connection from the milter, ever. All resolution and substitution data is precomputed by CFML and dropped on disk; the milter consumes the file artifacts. Triggers — when the resolver re-runs Both signature_by_sender and sender_data.json are rewritten in full by inc/signature_regen_map.cfm on every event that could affect a winner or a substitution value: Event Why it matters Admin saves an Org Sig New / edited row may win for senders that previously had no match or a lower-tier match Admin deletes an Org Sig Losers fall back to the next tier (or none) Admin edits a domain ( allow_user_signatures or org_* columns) Per-domain toggle flips the Personal-vs-Org winner; org_* values feed {{org.*}} substitution Admin edits a mailbox (Pro fields: first_name, title, dept, etc.) {{user.*}} and {{dept.name}} substitution data changes; a department change can flip Default → Department winner Admin adds a mailbox New sender enters resolution and may pick up a domain-default Org Sig Admin deletes a mailbox Sender must drop from the map User saves their Personal Signature May now win over the previously-resolved Org Sig (or vice versa if disabling) Each trigger runs the same shared resolver. Full rebuild every time, not incremental. With low-thousands of mailboxes the rebuild is well under a second, and the simplicity rules out drift bugs ("did we forget to update X for sender Y" can't happen). The body milter mtime-watches both files and reloads in process memory on the next message after the file changes. No SIGHUP, no IPC, no container restart. Files generated on save The CFML resolver writes: /etc/hermes/body_milter/signatures/signature_by_sender sender → option map /etc/hermes/body_milter/signatures/sender_data.json sender → {{token}} dict /etc/hermes/body_milter/signatures/files/