Skip to content

Link allowlist & content lock

Your public key is visible in your website’s code, so anyone can copy it and call the API directly. For a contact form that’s fine: the email always goes to you. It’s different when a template takes its recipient from params (a newsletter confirmation, an auto-reply, an order confirmation): whoever holds the public key could then send your template – from your domain, with your branding – to any address, and fill in the text and links. That’s how phishing works.

Frontmail’s rule is therefore: dynamic recipient ⇒ locked content. You can still send to a recipient chosen in the browser, but the caller can’t turn your template into a phishing email.

Request Locked
public key + template whose To/CC/BCC use params (Allow dynamic recipient on) yes
auto-reply (always goes to the address from the form) yes – links and sender fields
private key (your server) no, unless the template has Only allow links to allowed domains
public key + template with a fixed recipient (contact form) no – only the From rule applies

A template that has Allow dynamic recipient on but fixed addresses in To/CC/BCC isn’t locked.

  1. CAPTCHA is required. The template must have Require CAPTCHA on, otherwise the request fails with 403 dynamic_recipient_requires_captcha. CAPTCHA uses your own Turnstile keys. If you can’t use CAPTCHA, send the template from your server with a private key.
  2. Fixed sender. From address, From name and Reply-To must not use params – 403 dynamic_field_not_allowed.
  3. No caller markup, short texts. Params of type HTML can’t be filled in, and every text value is at most 500 characters (URL params 2,048; a smaller Max length on the param still applies) – 422 invalid_template_params with details.reason: "content_locked".
  4. Links only to allowed domains – see below – 403 link_not_allowed.
  5. Per-recipient limit. At most 5 emails per recipient address per day (UTC), configurable in Security → Emails per recipient per day (1–1,000). Over the limit: 429 rate_limited with details.scope: "recipient". Auto-replies share the counter – over the limit no auto-reply is created.

Every check runs before a credit is used. For auto-replies, a failed check marks the auto-reply as failed (with the error code in history), the credit is refunded and nothing is sent.

A link is allowed when its domain is:

  • written directly in the template (HTML, text version or subject) – not produced by a placeholder. https://acme.com/reset?token={{token}} allows acme.com; {{reset_url}} or https://{{sub}}.acme.com allow nothing;
  • on your allowed origins list (Security → Allowed websites);
  • on Security → Allowed link domains – one per line, example.com (exactly that host) or *.example.com (subdomains, not the apex) – the same rules as allowed origins;
  • Frontmail’s own domain (the Sent via Frontmail footer on the Free plan).

mailto:, tel: and any other non-web link (javascript:, data:, …) are allowed only when the exact same link is written in the template.

After rendering, Frontmail collects every URL a mail client could open or turn into a link:

  • href, src, srcset, action, formaction, background, poster and other URL attributes, also inside Outlook conditional comments;
  • CSS url() and @import in style attributes and <style>, and <meta http-equiv="refresh">;
  • URLs and bare domains in the HTML text, the text version and the subject – mail clients auto-link evil.com/login or www.evil.com, so these count as links too.

Before comparing, URLs are normalized the way a browser would read them: HTML entities and CSS escapes are decoded, invisible characters removed, \ treated as /, protocol-relative //host resolved, user info dropped (https://acme.com@evil.com is evil.com), percent-encoding decoded, upper case and trailing dots removed, and internationalized domains compared in punycode – a look-alike such as аcme.com with a Cyrillic а doesn’t match acme.com (the error marks it with idn: true).

The error lists only the offending domains in details.hosts – never full URLs, which may contain tokens.

  • Bare domains are detected with a list of known non-domain endings (report.pdf, index.html and Node.js aren’t links), so a text like Mr.Smith may count as the domain mr.smith. Phishing-favoured endings that are also real domains (.zip, .mov) always count.
  • Plain e-mail addresses in the text (jane@example.com) aren’t treated as links.
  • The check is about where links lead, not about the text around them. Keep texts short and your recipient limit low for public forms.

Turn on Only allow links to allowed domains in the template settings (Settings → Protection) to apply the link check to every request, including private-key sends. It’s on by default for gallery templates with a dynamic recipient.

For every public-key request, the From address of the template must not come from params (otherwise 403 dynamic_field_not_allowed). A contact form that puts the visitor’s address into Reply-To keeps working – that’s the right place for it. The template editor warns about all of this in Checks.