> Documentation index: [Saleor](/llms.txt) · [This section](/developer/llms.txt)
> Source: https://docs.saleor.io/developer/app-store/apps/customer-emails/overview

# Customer Emails

Saleor version required: **3.23**

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](/cloud/overview.md) from Saleor 3.23. If you are self-hosting, or still on 3.22, use the [SMTP app](/developer/app-store/apps/smtp/overview.md).

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 emails

Staff invite and staff password reset are sent by the [Admin Emails](/developer/app-store/legacy-plugins/admin-emails.md) plugin. This app never sends those.

<a id="how-the-page-is-organized"></a>

## 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.

<a id="before-anything-sends"></a>

## 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](/developer/users/customer-account.md).

After install, the recommended emails are already on. Nothing is delivered until a connection exists.

<a id="delivery"></a>

## 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.

<a id="writing-emails"></a>

## Writing emails

Templates are [MJML](https://mjml.io/). 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](https://handlebarsjs.com/). Examples you will see in the shipped templates:

```text
{{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.

<a id="shared-layout"></a>

### 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.

<a id="notifications"></a>

### 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.

<a id="branding"></a>

### 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.

<a id="custom-variables"></a>

### 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.

<a id="which-emails-exist"></a>

## 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.

| Email | 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](/developer/users/customer-account.md#registration-with-email-confirmation) |
| Customer account password setup | Staff creates a customer with a password-setup link | [Set password](/developer/users/customer-management.md#creating-a-customer) |
| Customer account password reset request | A customer requests a password reset | [Reset password](/developer/users/customer-account.md#resetting-the-password) |
| Customer account change email request | A customer requests an email change | [Change email](/developer/users/customer-account.md#changing-the-email-address) |
| Customer account change email confirmation | The email change completed | — |
| Customer account delete request | A customer requests account deletion | [Delete account](/developer/users/customer-account.md#deleting-the-account) |
| Gift card sent | A gift card is sent to a customer | — |

<a id="account-emails-and-the-storefront"></a>

## 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](/developer/users/customer-account.md) covers confirmation, password reset, email change, and account deletion. Staff-created accounts that need a first password are in [Customer management](/developer/users/customer-management.md#creating-a-customer).

<a id="language"></a>

## Language

Saleor channels have a country, not a language. This app is where you wire the three together:

1.  **Languages** — connect each channel to a language (and turn on any others you want to write).
2.  **Notifications** — the emails themselves: Order created, Order confirmed, password reset, and the rest. Turn each one on or off, and edit the wording.
3.  **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.

<a id="suppressed-addresses"></a>

## 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.

<a id="saleor-cloud-preview-mail"></a>

## Saleor Cloud preview mail

warning

**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.

<a id="if-you-used-the-smtp-app"></a>

## 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.

<a id="if-nothing-arrives"></a>

## If nothing arrives

1.  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.
2.  That connection has a sender.
3.  The notification is on.
4.  The old SMTP app is not the one still sending, and both are not on.
5.  For account confirmation: Site settings still requires confirmation.
6.  The address is not on **Suppressed addresses**. Remove it there if a refusal was a false positive.
7.  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.
