Guides / Guide

Central email signatures in Microsoft 365: there's no API for it, so here are the routes that work

By Michael Shreeves · 13 October 2026 · M365 & Security · 6 min read

Background

The ask sounds small: "one signature for everyone, kept up to date from the directory, and staff can't break it". It's the brief I gave myself for the Microsoft 365 side of the signature tool on my projects page.

The first thing you hit is that Microsoft has no API for setting someone's Outlook signature. Microsoft Graph has no signature endpoint. Exchange Online PowerShell has Set-MailboxMessageConfiguration -SignatureHtml, but it only ever applied to Outlook on the web, and it's ignored while roaming signatures are on, which is now the default. Microsoft's own documentation says it has no plans to add signature management to Graph.

So "push the signature to everyone's mailbox" isn't an option. What's left are three routes, and each one gives something up:

Route Where the signature goes on What it can't do
Transport rule disclaimer Server-side, on outbound mail Only merges a fixed list of directory attributes, so no proper templates
Outlook add-in In the message, while it's being written Only runs where an admin has deployed the add-in, on Outlook versions new enough to support it; mobile needs its own manifest
Mail-flow connector Server-side, by routing outbound mail through your own service Your service is now in every message's path, and it reads every outbound email

I ruled out the transport rule as the way to add signatures, because it can't render the templates the tool already builds. (A transport rule still has a smaller job later, deciding whose mail goes through the connector.) I designed for both of the others: the add-in for people who see their signature as they write, and the connector to catch everything the add-in can't reach.

Setup

Graph's real job: feeding the data, not applying anything

Graph still does the heavy lifting. It just isn't the part that touches mail.

  • One application permission: User.Read.All. Enough to read names, job titles, departments and phone numbers. Nothing that writes.
  • Use the users delta query, hourly. /users/delta returns only what changed since the last run, so a large tenant isn't re-read in full every hour. A big first sync works through pages within a time budget and carries on at the next run, instead of holding a web request open.
  • Decide who counts as staff on arrival. The delta query can't filter the way you'd want (shared mailboxes, room accounts, disabled users), so every record comes in and the rule is applied when it lands.
  • Rate-limit your own "Sync now" button. Graph throttles, and an impatient admin pressing it ten times shouldn't get the whole tenant throttled. Six an hour per organisation is what I set.

This is the bit that would have been a security hole.

Microsoft's admin consent link reports the tenant back in a query parameter, and anything in the browser's address bar can be edited. Trust it, and one organisation's admin can claim another company's tenant that has already consented.

The fix is two trips to Microsoft, not one:

  1. Sign the admin in first. The verified ID token proves the tenant, and its wids claim proves they hold a role that can grant Graph application permissions (Global Administrator or Privileged Role Administrator). wids only appears if the app registration is set to include directory roles in its tokens, so set that first.
  2. Then send them to admin consent, pinned to that tenant.

Two more decisions worth copying:

  • A separate app registration for directory access. If one sign-in registration is shared by several of your products, adding directory read to it gives every product that access. Give the directory feature its own multi-tenant app.
  • Verify ID token signatures even where the spec lets you skip it. The add-in later needs the same validator for tokens that arrive from a browser, and those must be checked.

The connector loop

The connector is the route that catches every outbound message. The loop goes:

  1. A mail flow rule and an outbound connector in Exchange Online send mail to your relay.
  2. The relay accepts TLS connections from Microsoft's infrastructure only.
  3. It stamps the sender's signature into both the HTML and plain-text parts.
  4. It hands the message back through an inbound connector in the same tenant, and Exchange delivers it.

Microsoft documents this exact pattern for add-on services, including how to stop a loop: the service adds a header, and the mail flow rule skips anything that already carries it.

Rules the stamping step has to follow:

  • Skip anything the add-in already signed. The add-in leaves a marker; the connector looks for it, so nobody gets two signatures.
  • Pass signed or encrypted mail through untouched. Changing an S/MIME message breaks the signature or the encryption.
  • Never log or store message bodies. Headers you need for routing, yes. Content, no.

Before any product code, I scoped a spike on a test tenant to prove the loop end to end: that SPF and DKIM still pass for the customer's domain, that nothing loops, and what Exchange does while the relay is down.

Edge Cases & BCP

  • You're now part of every customer's outbound mail. If the relay is down, their mail is affected. Know exactly what Exchange does when the connector's target doesn't answer before you promise anyone anything, and run the relay on its own small server rather than on a box shared with everything else.
  • Port 25 is blocked by default at many hosts. Mine included. It's a support request, not a setting, so ask early.
  • Reading every outbound email is a different data protection position. For a healthcare-adjacent customer especially. That means a DPIA, processor terms, a privacy notice rewrite and a trust page update, all before the first real message goes through.
  • Unverified publishers get refused. A multi-tenant Entra app shows as "unverified" without Microsoft publisher verification, and some enterprise admins won't consent to one. Verification needs a Microsoft AI Cloud Partner Program ID and your own publisher domain, so find out early.
  • The client secret expires, and every connected tenant's sync stops with it. I added a warning from 30 days out, on the admin page, in the logs and by a daily email. A certificate credential is the better long-term answer.
  • The add-in isn't everywhere. Event-based activation (signing a new message automatically) needs an admin-deployed add-in and recent enough Outlook versions, with minimum versions on mobile. Anyone on an older or personal setup falls through, and that's what the connector is there for.
  • Where my build stands. Connecting a tenant and syncing people are built and tested, but only against test doubles; it hasn't yet run against a real tenant. The add-in and the connector are designed and scoped, not built. Everything under "The connector loop" is the design, and the spike is what proves or breaks it.

Checked against Microsoft Learn on 14 October 2026: Set-MailboxMessageConfiguration, Set-OrganizationConfig, organisation-wide disclaimers, event-based activation, admin consent, user: delta, publisher verification and integrating an email add-on service.

Related: M365 joiners, movers and leavers also splits Graph application permissions so each part can only do its own job.

← All guides