<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Routes, relays and smart hosts, OAuth2 relaying, restricted delivery and recall]]></title><description><![CDATA[<p dir="auto"><em>Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) holds a credential confined to named domains - such as a domain administrator, or an API key issued for its domains - to a public address when it changes a domain's relay host, so it cannot point the server's delivery at the server's own network, and makes a domain's or an account's rule's Send using route and Bind to address actions the server administrator's to set; an account given the serveradmin role with a list of domains is, over COM as over REST, confined to those domains, so a script's account that edits the routes or the relayer needs the role with no list. See <a href="/topic/28277">the 6.3.7 announcement</a>.</em></p>
<p dir="auto">There are four ways this server can decide where an outbound message goes, and they are tried in a fixed order. Almost every "it went to the wrong server" question is answered by knowing that order, so this topic starts with it. It then covers the smart host and its failover, per-domain relays, routes and what they do to an unknown recipient, sending an OAuth2 token to a relay and what 6.3.4 changed about it, the COM change that is in effect only after <code>Save()</code>, and three things new in 6.3.4 (27 September 2026, schema 6104) that are not about where mail goes: remote domain policies (one paragraph here; the fuller treatment is <a href="/topic/28257">Requiring TLS: MTA-STS, DANE and remote domain policies</a>) and <strong>restricted delivery</strong>, which decide whether mail may go, and <strong>message recall</strong>, which withdraws unread copies already delivered on this server. The one thing to know first: <strong>a route is a statement about the destination and beats everything; a per-domain relay is a statement about the sender; the server-wide relayer is the fallback for mail nothing else has an opinion about.</strong></p>
<h3><a class="anchor-offset" name="the-order-of-decision"></a>The order of decision</h3>
<p dir="auto">When a message is ready to leave, its recipients are grouped by domain and the server asks:</p>
<ol>
<li><strong>Did a global rule apply the <em>Send using route</em> action?</strong> Then every recipient goes to that route's target host and nothing else is consulted.</li>
<li><strong>Is there a route whose domain matches the recipient domain?</strong> Wildcards are allowed. The route's host, port, connection security and credentials are used.</li>
<li><strong>Does the sender's local domain have a relay host of its own?</strong> That domain's relay host, port, security and credentials. Port <code>0</code> means 25.</li>
<li><strong>Is a server-wide SMTP relayer configured?</strong> The relayer's host, port, security and credentials. Port <code>0</code> means 25.</li>
<li>Otherwise, an ordinary MX lookup for the recipient domain, then A or AAAA for each MX host.</li>
</ol>
<p dir="auto">Targets that turn out to be identical are merged: a message for four domains that all relay through one smart host opens one connection, not four. <strong>A bounce never picks up a per-domain relay</strong>, because a bounce has no envelope sender, so there is no sending domain to consult.</p>
<p dir="auto">Since 6.3.4 one question comes before all of these for inbound mail: an address or a hosted domain routed to a <strong>webhook</strong> has each message posted to an application, instead of being delivered (<code>deliver</code> off) or as well (<code>deliver</code> on). That is a different thing from the routes below, which send a domain's mail to another mail server; <a href="/topic/28266">mail in to a webhook</a> has the webhook routes.</p>
<h3><a class="anchor-offset" name="sending-through-a-smart-host"></a>Sending through a smart host</h3>
<p dir="auto">On Windows, configure it in the Control Panel at <strong>Mail flow &amp; delivery → Delivery of e-mail → Relayer</strong>. The Control Panel and the COM API are Windows-only; over REST, which Linux has too, the relayer's settings are keys of <code>PUT /api/v1/settings</code> (<code>smtp_relayer_password</code> is write-only).</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Setting</th>
<th>COM name</th>
<th>Default</th>
<th>When it is wrong</th>
</tr>
</thead>
<tbody>
<tr>
<td>SMTP relayer</td>
<td><code>SMTPRelayer</code></td>
<td>empty: MX lookup</td>
<td>A host that does not resolve gives the bounce text <em>"The host specified as SMTP relay server could not be found"</em> with <code>4.3.5</code>, a temporary failure, so the message is retried</td>
</tr>
<tr>
<td>Port</td>
<td><code>SMTPRelayerPort</code></td>
<td><code>25</code></td>
<td>Looks like a connection timeout in the SMTP log</td>
</tr>
<tr>
<td>Connection security</td>
<td><code>SMTPRelayerConnectionSecurity</code></td>
<td>None</td>
<td><em>Required</em> against a host without STARTTLS defers every message</td>
</tr>
<tr>
<td>Relayer requires authentication</td>
<td><code>SMTPRelayerRequiresAuthentication</code></td>
<td>off</td>
<td>Off against a host that requires it gives <code>530</code>/<code>535</code> and the mail stays queued</td>
</tr>
<tr>
<td>Number of delivery retries</td>
<td><code>SMTPNoOfTries</code></td>
<td><code>4</code></td>
<td><code>0</code> means one attempt and then a bounce</td>
</tr>
<tr>
<td>Minutes between retries</td>
<td><code>SMTPMinutesBetweenTry</code></td>
<td><code>60</code></td>
<td></td>
</tr>
<tr>
<td>Outbound delivery security</td>
<td><code>SMTPConnectionSecurity</code></td>
<td>STARTTLS (optional)</td>
<td>Applies to MX-resolved delivery, not to the relayer</td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>Several smart hosts.</strong> Separate the names with a vertical bar:</p>
<pre><code class="language-text">smtp1.example.net|smtp2.example.net
</code></pre>
<p dir="auto">Each name is resolved in turn and every address it resolves to is appended, giving one ordered candidate list tried top to bottom. A single name with several A records already gives that failover. All hosts in the list share the port, security and credentials. <strong>A permanent <code>5xx</code> rejection is a real delivery failure and the next host is not tried</strong>: trying it would deliver the message twice if the first server had in fact accepted it.</p>
<p dir="auto"><strong>Relaying everything through an ISP:</strong></p>
<ol>
<li>Open <strong>Mail flow &amp; delivery → Delivery of e-mail → Relayer</strong>.</li>
<li>SMTP relayer: <code>smtp.isp.example</code>. Port: <code>587</code>.</li>
<li>Connection security: <strong>STARTTLS (required)</strong> - required, not optional, so a downgrade cannot send your credentials in clear.</li>
<li>Tick <strong>Relayer requires authentication</strong> and fill in the user name and password.</li>
<li><strong>Save changes</strong>, send one message, and read <strong>Monitoring &amp; troubleshooting → Live logs</strong>.</li>
</ol>
<p dir="auto">The line that proves the relayer was used is:</p>
<pre><code class="language-text">"SMTPDeliverer - Message 41: Relaying to host smtp.isp.example."
</code></pre>
<p dir="auto">If the message went to an MX host instead, the relayer field is empty, or a route or a per-domain relay took precedence.</p>
<p dir="auto"><strong>Per-domain relay.</strong> <strong>Accounts &amp; domains → Domains → Properties → Relay</strong> sets where mail <em>from</em> that domain leaves through. It is for a server hosting several independent domains where each has bought its own delivery provider. An empty host means the domain has no opinion. Since 6.3.7 a credential confined to named domains - such as a domain administrator, or an API key issued for its domains - may set the host to a public address only, over REST and over COM; a host on the server's own network is the server administrator's to set, and one the server administrator set stays the domain administrator's to save, since only a change of the host is judged.</p>
<h3><a class="anchor-offset" name="routes"></a>Routes</h3>
<p dir="auto">A <strong>route</strong> overrides delivery for a specific recipient domain. <strong>Mail flow &amp; delivery → Routes</strong>: type the domain and target host, press <strong>Add</strong>, then <strong>Properties</strong> for everything else. Over REST the routes are <code>/api/v1/routes</code>.</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Field</th>
<th>Default</th>
<th>Note</th>
</tr>
</thead>
<tbody>
<tr>
<td>Domain name</td>
<td>-</td>
<td>Wildcards are matched, so <code>*.partner.example</code> works. The <strong>first</strong> matching route in name order wins; a wildcard broader than you meant silently captures other domains</td>
</tr>
<tr>
<td>Target SMTP host, port</td>
<td>-, <code>25</code></td>
<td>A name or a literal IP address; the <code>host1\|host2</code> failover applies here too</td>
</tr>
<tr>
<td>Number of delivery retries</td>
<td><code>3</code></td>
<td>Replaces <code>SMTPNoOfTries</code>. The delivery path applies it as <em>tries − 1</em>, so a route set to 3 schedules 2 further attempts</td>
</tr>
<tr>
<td>Minutes between retries</td>
<td><code>10</code></td>
<td>Where one message has recipients on several routes, the largest count and the largest interval win</td>
</tr>
<tr>
<td>Deliver to all addresses</td>
<td><strong>on</strong></td>
<td>Off: only the addresses listed are routed. Off with an incomplete list is the single most common route mistake</td>
</tr>
<tr>
<td>Connection security</td>
<td>None</td>
<td>None, SSL/TLS, STARTTLS optional, STARTTLS required</td>
</tr>
<tr>
<td>Treat sender domain as local</td>
<td>off</td>
<td>Turn it on only for a domain you actually control</td>
</tr>
<tr>
<td>Treat recipient domain as local</td>
<td>off</td>
<td>On, unauthenticated senders may send to it from the Internet range</td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>What a route does to an unknown recipient:</strong></p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Route setting</th>
<th>Domain has a catch-all?</th>
<th>Address in the route's list?</th>
<th>Result</th>
</tr>
</thead>
<tbody>
<tr>
<td>Deliver to all addresses</td>
<td>-</td>
<td>-</td>
<td>Routed</td>
</tr>
<tr>
<td>Specific addresses only</td>
<td>no</td>
<td>no</td>
<td><code>550 Recipient not in route list.</code></td>
</tr>
<tr>
<td>Specific addresses only</td>
<td>yes</td>
<td>no</td>
<td>Delivered to the <strong>catch-all</strong>, not routed</td>
</tr>
<tr>
<td>Specific addresses only</td>
<td>yes</td>
<td>yes</td>
<td>Routed; the catch-all does not intercept it</td>
</tr>
</tbody>
</table>
<p dir="auto">Routes also make <code>ETRN</code> work: when a remote site issues <code>ETRN &lt;domain&gt;</code> and a route matches, the messages held for it are released for immediate delivery.</p>
<h3><a class="anchor-offset" name="oauth2-to-a-relay"></a>OAuth2 to a relay</h3>
<p dir="auto">A route or the SMTP relay whose host is on <code>OutboundOAuth2Hosts</code> (default <code>smtp.office365.com</code>) is sent <code>AUTH XOAUTH2</code> with the server's own token: a client-credentials token fetched from <code>OutboundOAuth2TokenUrl</code> with <code>OutboundOAuth2ClientId</code>, <code>OutboundOAuth2ClientSecret</code> and <code>OutboundOAuth2Scope</code> (default <code>https://outlook.office365.com/.default</code>), or <code>OutboundOAuth2FixedToken</code> used verbatim instead of fetching. The default host list is inert on its own: with <code>OutboundOAuth2TokenUrl</code> and <code>OutboundOAuth2FixedToken</code> both empty, no bearer is set and the delivery proceeds with LOGIN and the password, with no log line.</p>
<p dir="auto"><strong>Since 6.3.4 the token is sent over TLS only.</strong> A route or the SMTP relay whose host is on <code>OutboundOAuth2Hosts</code> and whose connection security is none - or STARTTLS optional against a server that does not offer it - used to send <code>AUTH XOAUTH2</code> with the token in the clear. Such a delivery now sends nothing in its place, fails with a temporary <code>450 4.7.0</code>, and the error log says to set the security to TLS or STARTTLS required (<code>HM5912</code>); the message waits for the retries the route allows. Exchange Online offers STARTTLS on 587, so a route set up as Microsoft documents it is not affected.</p>
<p dir="auto">Also since 6.3.4, a relay that refuses the login no longer gets the token into the bounce, which now reads <code>hMailServer sent: AUTH XOAUTH2 &lt;Token removed&gt;</code>; the logs write <code>AUTH XOAUTH2 ***</code>.</p>
<p dir="auto">These are <code>[Settings]</code> keys, and <strong>since 6.3.4 the database is the settings store</strong>: a line edited in the file changes nothing, the stored value is used and named as <code>HM5804</code>, and the line is put back where the service can write the file (<a href="/topic/28222">when a saved setting takes effect</a>). Change them in the Control Panel, with <code>PUT /api/v1/settings/ini/{name}</code> or with <code>hmctl ini set</code>.</p>
<h3><a class="anchor-offset" name="a-com-change-is-in-effect-only-after-code-save-code"></a>A COM change is in effect only after <code>Save()</code></h3>
<p dir="auto">Since 6.3.4 a COM change to a route or an incoming relay is in effect only after the object's <code>Save()</code>, and a property set and never saved changes nothing; the list of objects this applies to does not name the server-wide SMTP relayer's settings. COM is Windows-only, so this concerns scripts on Windows: <a href="/topic/28262">the COM API topic</a> has the whole list, what an object read before the save shows, and error 7390.</p>
<h3><a class="anchor-offset" name="remote-domain-policies-in-one-paragraph"></a>Remote domain policies, in one paragraph</h3>
<p dir="auto"><em>Since 6.3.4 (schema 6045).</em> A route says <em>where</em> mail for a domain goes; a <strong>remote domain policy</strong> says what this server will and will not do when it talks to one domain it does not host, however that domain's server was found, and it applies to a routed delivery exactly as to an MX one. One record per name or pattern (<code>bank.example</code>, <code>*.example</code>, <code>*</code>), the most specific active one governing, holds the outbound TLS the domain demands (<code>none</code>, <code>encrypted</code>, <code>verified</code> or <code>dane</code>; a delivery that cannot meet it is deferred with <code>4.7.0</code>, never sent in the clear), TLS required of its senders (<code>530 5.7.0</code>), a largest message (<code>5.3.4</code>), connection and rate ceilings (deferred with <code>4.4.5</code>), whether automatic replies and forwards may go there, a recipient callout for a domain this server is a backup MX for, and gateway encryption to the partner's own key. It is server-administrator only, and <code>GET /api/v1/remote-domains/effective?domain=mail.bank.example</code> answers which record a delivery will use.</p>
<h3><a class="anchor-offset" name="restricted-delivery-since-6.3.4"></a>Restricted delivery, since 6.3.4</h3>
<p dir="auto"><em>Schema 6084.</em> Accounts, or a whole domain, that may <strong>only exchange mail with named domains</strong>: a school whose pupils write to the school and to nobody else. It is not a route: it decides whether mail may go, not where.</p>
<p dir="auto"><strong>The list is the domain's</strong>: <code>Domain.RestrictedDomains</code> over COM, <code>restricted_domains</code> on <code>GET</code>/<code>PUT /api/v1/domains/{domain}</code>, the <strong>Restricted delivery</strong> tab of the Control Panel's domain dialog, the same group in the Deck's domain editor. At most 2,000 characters, names separated by commas or spaces, ASCII only, an internationalised name in its <code>xn--...</code> form.</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Written as</th>
<th>Covers</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>partner.example</code></td>
<td>that domain, and nothing beneath it</td>
</tr>
<tr>
<td><code>.example</code></td>
<td><code>example</code> and everything beneath it, anchored at a label boundary</td>
</tr>
</tbody>
</table>
<p dir="auto"><strong>An account follows its domain unless it says otherwise</strong> (<code>Account.RestrictedDelivery</code>, <code>restricted_delivery</code> on the account routes):</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Value</th>
<th>Word</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td><code>domain</code></td>
<td>Follows the domain. The default</td>
</tr>
<tr>
<td>1</td>
<td><code>restricted</code></td>
<td>Restricted even when the domain names nothing; the account is then confined to its own domain</td>
</tr>
<tr>
<td>2</td>
<td><code>exempt</code></td>
<td>May exchange mail with anyone</td>
</tr>
</tbody>
</table>
<p dir="auto">An empty list, the default, means nothing runs, so an upgraded server delivers exactly as it did. <strong>Always allowed</strong>: the account's own domain under its own name or any alias domain, the domain's postmaster, the null sender <code>MAIL FROM:&lt;&gt;</code>, and mail this server writes itself. <strong>Other domains this server hosts are not allowed unless named.</strong></p>
<p dir="auto">It is enforced in three places:</p>
<ol>
<li><strong>Inbound, at <code>RCPT TO</code>.</strong> A restricted recipient whose envelope sender is not allowed is answered <code>550 5.7.1 &lt;address&gt;: this mailbox accepts mail from named domains only.</code>, per recipient, so one refused <code>RCPT</code> does not end the transaction. Every session is judged, authenticated or not.</li>
<li><strong>Outbound, at <code>RCPT TO</code>.</strong> A restricted submitting account is answered <code>550 5.7.1 &lt;address&gt;: this account may send to named domains only.</code> for a recipient outside. The webmail's <code>POST /api/v1/me/messages</code> answers <code>400</code> with the sentence and queues nothing.</li>
<li><strong>At local delivery</strong>, for a recipient reached by expansion: a distribution list member, an alias target, a catch-all. The restricted recipient is dropped, and a non-delivery report goes to the sender only when the sender is local.</li>
</ol>
<p dir="auto">A restricted account's <strong>forward</strong> - the account forward, an account rule's forward, a Sieve <code>redirect</code> - to a domain not allowed is not made, and the message is still delivered to the mailbox. Its <strong>out-of-office reply</strong> is not sent to a sender who is not allowed. Every refusal is a <code>DEBUG</code> log line; nothing goes to the error log, since a refusal is the feature working.</p>
<h3><a class="anchor-offset" name="message-recall-within-the-server-since-6.3.4"></a>Message recall within the server, since 6.3.4</h3>
<p dir="auto"><em>Schema 6085.</em> An unread message sent to accounts on this server can be withdrawn by its sender. Nothing does this across the Internet: a message that has left for another server is that server's. <strong>No recall notice reaches the recipients</strong>; the Sent copy's own mark is the record.</p>
<p dir="auto"><strong>What is recorded.</strong> A message accepted from one of the server's own accounts - an <strong>authenticated SMTP submission</strong>, credited to the account that signed in, or the <strong>webmail's send</strong> - gets one row in <code>hm_recall</code> per recipient that resolved to a local account. Nothing is recorded for:</p>
<ul>
<li>mail from outside, and mail this server writes itself;</li>
<li>an <strong>unauthenticated submission</strong> from a local IP range in a local account's name, because a recall keyed on a claim would let anybody delete mail from other people's mailboxes;</li>
<li>a message with a <code>Message-ID</code> longer than 255 characters, and a message that expires;</li>
<li>the 201st and later recipient rows of one sender in a rolling day.</li>
</ul>
<p dir="auto"><strong>The recall.</strong> In the webmail, <strong>Recall</strong> in the menu of a message in the Sent folder. For a program, <code>POST /api/v1/me/recall</code> with <code>{"message_id": "&lt;the Message-ID&gt;", "sent_id": n}</code>; it is refused with 403 on an app password. Each recipient is judged on their own:</p>
<table class="table table-bordered table-striped">
<thead>
<tr>
<th>Outcome</th>
<th>Meaning</th>
</tr>
</thead>
<tbody>
<tr>
<td><code>read</code></td>
<td>Some live copy of theirs carries <code>\Seen</code>. Nothing was done: whoever has read it keeps it</td>
</tr>
<tr>
<td><code>recalled</code></td>
<td>No copy was read; every unread copy has been deleted the way the webmail deletes a message</td>
</tr>
<tr>
<td><code>gone</code></td>
<td>No copy of theirs is there any more: deleted by the reader, never delivered, or still in the queue</td>
</tr>
<tr>
<td><code>failed</code></td>
<td>The database did not answer; a later call decides them again</td>
</tr>
</tbody>
</table>
<p dir="auto">The answer also counts <code>elsewhere</code>, the <code>To</code> and <code>Cc</code> addresses whose domain this server does not host, and says in <code>sent_marked</code> whether the Sent copy carries the keyword <code>$Recalled</code>. <strong>Read is <code>\Seen</code> and nothing more</strong>: a POP3 client, or an IMAP client that peeks, has the message and the recall cannot know. <code>RecallDays</code> (<code>recall_days</code> on <code>PUT /api/v1/settings</code>) is 14 by default, 1 to 365; a domain or an account may hold a shorter window, never a longer one. There is no COM property and no Control Panel page.</p>
<h3><a class="anchor-offset" name="what-it-does-not-do"></a>What it does not do</h3>
<ul>
<li><strong>A route does not decide whether mail may go</strong>, and restricted delivery and a remote domain policy do not decide where. A per-policy SMTP port or credential is a route's job.</li>
<li><strong>Restricted delivery does not check the <code>From</code> header</strong>, does not filter what is already in the mailbox, and does not gate a journal rule's copy or an alias or list in the domain that reaches outside.</li>
<li><strong>Recall does not reach another server</strong>, does not touch the queue, and does not recall a message submitted without authentication.</li>
<li><strong>A <code>5xx</code> from one smart host is not retried on the next.</strong></li>
</ul>
<h3><a class="anchor-offset" name="where-to-read-more"></a>Where to read more</h3>
<ul>
<li>Wiki: <a href="https://gitlab.com/hmailserver/hmailserver/-/wikis/Routes-and-Relays" rel="nofollow ugc">Routes and Relays</a>, <a href="https://gitlab.com/hmailserver/hmailserver/-/wikis/Settings-Reference" rel="nofollow ugc">Settings Reference</a> for the <code>OutboundOAuth2*</code> keys, <a href="https://gitlab.com/hmailserver/hmailserver/-/wikis/APIs-Reference" rel="nofollow ugc">APIs Reference</a>, <a href="https://gitlab.com/hmailserver/hmailserver/-/wikis/Diagnosing-Stalled-Mail" rel="nofollow ugc">Diagnosing Stalled Mail</a></li>
<li>Documents at the 6.3.4 tag: <a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/RemoteDomains.md" rel="nofollow ugc">RemoteDomains.md</a>, <a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/RestrictedDelivery.md" rel="nofollow ugc">RestrictedDelivery.md</a>, <a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/MessageRecall.md" rel="nofollow ugc">MessageRecall.md</a>, <a href="https://gitlab.com/hmailserver/hmailserver/-/blob/v6.3.4/hmailserver/docs/Upgrading.md" rel="nofollow ugc">Upgrading.md</a></li>
<li>The public documentation: <a href="https://www.progressiverobot.com/hmailserver-documentation/#14-routes-and-relays" rel="nofollow ugc">chapter 14, Routes and relays</a></li>
<li>On this forum: <a href="/topic/28188">Being trusted by the servers you send to</a>, the <a href="/topic/28218">6.3.4 release announcement</a>, <a href="/topic/28257">requiring TLS and remote domain policies</a>, and <a href="/topic/28219">diagnosing stalled mail</a> for a message that will not leave the queue</li>
</ul>
]]></description><link>https://www.hmailserver.co.uk/topic/28269/routes-relays-and-smart-hosts-oauth2-relaying-restricted-delivery-and-recall</link><generator>RSS for Node</generator><lastBuildDate>Fri, 06 Nov 2026 09:05:57 GMT</lastBuildDate><atom:link href="https://www.hmailserver.co.uk/topic/28269.rss" rel="self" type="application/rss+xml"/><pubDate>Sun, 27 Sep 2026 19:51:13 GMT</pubDate><ttl>60</ttl></channel></rss>