Customer Emails
Saleor does not send email to customers. Customer Emails listens for store events and delivers each message through an SMTP server you connect.
It is available on Saleor Cloud from Saleor 3.23. If you are self-hosting, or still on 3.22, use the SMTP app.
Open it from Configuration → Customer emails. Content and branding are written once; which mail server sends and in which language is set per channel.
Staff invite and staff password reset are sent by the Admin Emails plugin. This app never sends those.
How the page is organized​
The settings page is one list of cards:
| Card | What you do there |
|---|---|
| Delivery | SMTP connections, and which channel uses which one |
| Branding | Store name, logo, colors, help links, and the shared email layout |
| Notifications | Turn each email on or off, and edit the wording |
| Translations | The same emails in another language |
| Languages | Which language each channel uses |
| Suppressed addresses | Inboxes the mail server already refused |
| Advanced | Extra values every template can read |
On a new install, a checklist at the top tracks the three things that must be true before anything can send: a connection, a sender, and at least one notification on. Branding and translations are optional.
Before anything sends​
You need three things the app cannot invent:
- An SMTP server — Amazon SES, Mailgun, SendGrid, Mailtrap, or a local catcher. Enter host, port, encryption, and credentials. Encrypted connections (TLS and SSL) require TLS 1.3; a provider that only speaks 1.2 will fail the handshake.
- A sender name and address on that connection. Many providers reject the message when From is not the mailbox you authenticated as.
- For account emails, a storefront page that consumes the token URL Saleor puts in the message. The app only delivers. See Manage account as a customer.
After install, the recommended emails are already on. Nothing is delivered until a connection exists.
Delivery​
Add a connection: a name you will recognize, host, port, encryption, credentials, and the sender customers see.
- Port 587 with TLS (STARTTLS), or 465 with SSL (implicit TLS), is what most providers expect. The form flags a mismatch before you save.
- Test connection on the card proves the server answers. It does not send a customer email.
Each channel sends through exactly one connection, so a customer never receives the same email twice.
The first connection is the store default: every channel you have not assigned elsewhere sends through it. Assign a channel only when it should use a different connection. With two or more connections, Make default is in that connection's menu; it asks before channels that currently use the store default start sending through the new one. If the default mark is missing and you have exactly one active connection, that one is used.
Writing emails​
Templates are MJML. The app compiles them to HTML when it sends. You edit MJML in the Dashboard; you do not paste HTML or a design from another tool.
Dynamic values use Handlebars. Examples you will see in the shipped templates:
{{order.number}}
{{customer.firstName}}
{{branding.siteName}}
{{#each order.lines}}
{{this.productName}}
{{/each}}
The Sample data tab is only for the preview and for test emails. A real send uses the order or account that triggered it. The editor suggests fields from that tab. A helper the app does not ship fails the preview and the send.
Shared layout​
Edit layout on the Branding card. The layout is the MJML document every notification sits in: the <mjml> wrapper, styles, header, and footer.
Exactly one {{{content}}} marks where the notification body goes. Triple braces are required so the body is inserted as markup, not escaped text. You cannot save without that placeholder, or with more than one.
Preview the layout wrapped around any notification. Until you save a change, emails use the layout that ships with the app.
Notifications​
Open a row to edit the subject and body. The shipped body is an MJML fragment — sections and columns, not a full document. The layout wraps it.
Saving a change makes that text yours. Later app updates no longer change it. Reset to default (or Delete translation on a language that is not the store default) goes back to the shipped copy.
Send test email from the editor uses the version on screen, including unsaved edits, filled with sample data. Subjects start with [Test]. It does not depend on whether customers receive that notification. From Notifications you can also send every enabled email to one inbox; that path uses the saved templates, still with sample data.
Branding​
Store-wide values, not markup. Change them once and every template picks them up. Templates read:
| Field | Handlebars |
|---|---|
| Store name | {{branding.siteName}} |
| Logo | {{branding.logoUrl}} |
| Shop URL | {{branding.shopUrl}} |
| Accent color | {{branding.accentColor}} |
| Help centre | {{branding.helpUrl}} |
| Support address | {{branding.supportEmail}} |
| Postal address | {{branding.addressLine}} |
Empty store name and address fall back to Configuration → Store → Store details.
Use a PNG or JPEG on a URL that stays reachable. SVG and signed or expiring links look fine in the Dashboard preview and often fail in the inbox. The app warns; it does not block save.
The Order page URL field is a link pattern, not an email template. Put {{id}} or {{number}} in that field (or {{order.id}} / {{order.number}}) so the View order details button can point at a real order, for example https://example.com/order/{{id}} or /account/orders/{{number}}. A path is joined to Shop URL. The button is left out when the field is empty or the link cannot be built.
{{customer.firstName}} is filled from the account or the order address, so a greeting works for guests too.
Custom variables​
Under Advanced, add key-value pairs available in every template as {{customVariables.yourKey}}. Use this for a storefront URL or a support address you would otherwise repeat in sixteen templates.
Which emails exist​
On after install: Order created, Order confirmed, Order fulfilled, Order cancelled, Order refunded, and the six account emails.
These stay off because they stack on that set, or they fire from a staff action:
- Order fully paid can fire for the same purchase as Order created and Order confirmed.
- Shipment created fires in the same moment as Order fulfilled when the order ships in one go.
- Invoice sent, Order fulfillment updated, and Gift card sent are staff-triggered.
| Sends when | Storefront | |
|---|---|---|
| Order created | An order is placed | — |
| Order confirmed | The order is confirmed | — |
| Order fully paid | The order is fully paid | — |
| Order cancelled | The order is cancelled | — |
| Order refunded | A refund is issued | — |
| Invoice sent | Staff sends an invoice | — |
| Shipment created | Staff creates a shipment and leaves notify customer on | — |
| Order fulfilled | The order is fully fulfilled | — |
| Order fulfillment updated | A tracking number is updated | — |
| Customer account confirmation | A shopper registers, and Configuration → Site settings requires email confirmation. If that setting is off, nothing is sent. | Confirm account |
| Customer account password setup | Staff creates a customer with a password-setup link | Set password |
| Customer account password reset request | A customer requests a password reset | Reset password |
| Customer account change email request | A customer requests an email change | Change email |
| Customer account change email confirmation | The email change completed | — |
| Customer account delete request | A customer requests account deletion | Delete account |
| Gift card sent | A gift card is sent to a customer | — |
Account emails and the storefront​
Saleor builds the link from the mutation's redirectUrl. The app puts that link in the email. If the storefront route is wrong, the message arrives and the customer cannot finish.
Manage account as a customer covers confirmation, password reset, email change, and account deletion. Staff-created accounts that need a first password are in Customer management.
Language​
Saleor channels have a country, not a language. This app is where you wire the three together:
- Languages — connect each channel to a language (and turn on any others you want to write).
- Notifications — the emails themselves: Order created, Order confirmed, password reset, and the rest. Turn each one on or off, and edit the wording.
- Translations — the same emails in another language. Open a cell to edit Order confirmed in German, for example. The editor can show the store default beside it.
When something happens, the app picks a language, then the wording. It uses the language on the order or account. Turn Always on for a channel if everyone buying there should get that channel's language instead. If the order or account has no language, the channel language, then the store default.
A saved template in that language wins. If there is none, the base language can stand in — Brazilian Portuguese can use Portuguese. After that, a saved store-default template. If nothing is saved, the app sends the copy it ships in the customer's language when it has one: German, English, Spanish, Finnish, French, Japanese, Korean, Polish, Portuguese, and Simplified Chinese. A language it does not ship uses English, even when the store default is something else. They get mail, not an empty inbox.
Once you edit the store-default wording, later sends use that edit for every language you have not translated. The Translations card marks those languages as not translated so you can see the gap.
Suppressed addresses​
When the mail server refuses a recipient as unknown — SMTP 550, 551, or 553, or the address on the server's rejected list — the app stores that address and skips later emails to it. The skip is a success for Saleor: nothing to retry.
A Dashboard app problem points at Suppressed addresses in settings. Removing a visible address is how you undo a false positive; the next genuine refusal puts it back. Settings shows the 50 most recently refused and how many there are in total.
A 554 (bad message or sender refused) does not add the customer. A store outage does not block a valid send.
Saleor Cloud preview mail​
Send previews through Saleor's mail server runs only when no connection matches the channel — no assignment, no store default, and not a single active connection. Customers receive nothing: each email goes to the organization owner instead, marked as a preview, with the intended recipient in the subject.
It does not override a connection you already added. Channels that use the store default still send through that connection. On a new Cloud install the toggle is on, so you see mail before you add SMTP. Turn it off and, with no connection, nothing sends. Production needs your own connection.
If you used the SMTP app​
This is a different app (saleor.app.customer-emails). Installing it does not upgrade or replace saleor.app.smtp.
- Do not leave both sending. The old app can still send more than one email for the same event when multiple configurations match a channel.
- Settings do not move over from the Dashboard. Set this app up as new, or ask Saleor if you are on Cloud and need an import.
- Each channel now sends through exactly one connection. The old "all channels except" filter is gone.
- A customized template imported from the SMTP app arrives as a complete MJML document. It does not use the shared layout. Reset to default if you want the header and footer back.
If nothing arrives​
- A connection exists and is assigned to the channel — or it is the store default, or the only one. On Cloud, preview mail to the organization owner is not a substitute for that connection.
- That connection has a sender.
- The notification is on.
- The old SMTP app is not the one still sending, and both are not on.
- For account confirmation: Site settings still requires confirmation.
- The address is not on Suppressed addresses. Remove it there if a refusal was a false positive.
- The provider rejected the message. Permanent replies (unknown mailbox, rejected credentials) stop immediately. A 554 is the message or the sender, not the customer. Transient failures still retry.
Saleor webhook retries do not send a second copy of the same event to the same address.