Skip to content

Sending email

Your CMS sends email for two things: the “Send an email” step in a workflow, and notifications that people chose to get by email (for example “A document awaits your approval”). Signing in and signing up do not depend on email, so the CMS works fine without it.

How mail leaves the CMS is set in .env. You picked a driver when you created the project (the --mail option): Mailpit for a local project, none for a docker project, unless you chose otherwise.

With MAIL_DRIVER=none nothing breaks. A workflow’s email step fails with the message workflow.mail.notConfigured, which you see in the workflow’s run log, and notifications reach people in the admin and by browser push only. In your profile, on the Notifications tab, the By email column then says “not set up here”.

A driver is the way the CMS hands a mail over for delivery. Pick one with MAIL_DRIVER:

DriverWhat it isGood for
mailpitMailpit, a test inbox that catches every mail and delivers noneYour own computer, while building
smtpAny mail server that speaks SMTP, the standard mail protocolYour own mail server, or the SMTP relay of your mail provider
gmailA Google mailbox, through the Gmail APIGoogle Workspace or a Gmail account
microsoftA Microsoft 365 mailbox, through Microsoft GraphMicrosoft 365, where SMTP sign-in is often switched off
resendThe Resend email serviceAutomated mail from your own domain
sendgridThe SendGrid email serviceAutomated mail from your own domain
postmarkThe Postmark email serviceAutomated mail from your own domain
mailgunThe Mailgun email serviceAutomated mail from your own domain
noneNo mailWhen you do not need it

Each driver reads its own variables, listed below. A variable without a default is required: if it is empty, the CMS refuses to start and prints Missing required environment variable: with the name of the first one missing. That way a half-finished setup shows up at once, not when the first important mail should go out.

MAIL_FROM is the sender shown on every mail, written like Website Team <cms@example.com> or just cms@example.com.

  • For gmail and microsoft you can leave it empty: the mail is then sent as the mailbox itself.
  • For every other driver set an address your provider lets you send from, usually one on a domain you verified there. The default, Manablox <no-reply@localhost>, is fine for Mailpit, but a real provider will refuse it.

Mailpit is a mail catcher. The CMS sends to it like to a real mail server, but instead of delivering anything, Mailpit shows every mail in a web inbox. That makes it perfect for trying out workflows: nobody gets a test mail by accident.

In a local project created with Mailpit, pnpm services:up starts it together with the other services, and .env has:

Terminal window
MAIL_DRIVER=mailpit
MAIL_FROM=Manablox <no-reply@localhost>
MAILPIT_HOST=localhost
MAILPIT_PORT=1025
MAILPIT_UI_PORT=8025
VariableDefaultWhat it means
MAILPIT_HOSTlocalhostWhere Mailpit runs
MAILPIT_PORT1025The port the CMS sends mail to
MAILPIT_UI_PORT8025The port of the web inbox. Used by compose.yml

Open http://localhost:8025 to see the inbox.

If you created the project with another mail choice and want Mailpit later, add this service to compose.yml, below the valkey service and above the volumes: line, keeping the indentation:

mailpit:
image: axllent/mailpit:latest
restart: unless-stopped
ports:
- '${MAILPIT_PORT:-1025}:1025'
- '${MAILPIT_UI_PORT:-8025}:8025'

Then set the Mailpit lines above in .env, run pnpm services:up and restart pnpm dev.

VariableDefaultWhat it means
SMTP_HOSTnone, requiredThe address of the mail server, for example smtp.example.com
SMTP_PORT465 with SMTP_SECURE=true, else 587The port of the mail server
SMTP_SECUREfalsetrue for an encrypted connection from the start (usually port 465). false starts plain and switches to encryption (STARTTLS, usually port 587)
SMTP_USERemptyThe user name, if the server wants a login
SMTP_PASSWORDemptyThe password
SMTP_URLemptyEverything in one line instead, for example smtps://user:pass@smtp.example.com:465. Replaces the five above

With SMTP_URL set and MAIL_DRIVER left out, the CMS uses smtp on its own. The separate variables are easier when the password contains characters like @ or /, which would have to be escaped in a URL.

VariableDefaultWhat it means
GMAIL_CLIENT_IDnone, requiredThe client id of an OAuth client in a Google Cloud project with the Gmail API switched on
GMAIL_CLIENT_SECRETnone, requiredThat client’s secret
GMAIL_REFRESH_TOKENnone, requiredA refresh token for the scope https://www.googleapis.com/auth/gmail.send
GMAIL_USERmeThe mailbox to send as. me is the account the token belongs to

You get the refresh token once, for example with Google’s OAuth 2.0 Playground set to use your own client. A MAIL_FROM other than the mailbox must be one of its “Send mail as” addresses in Gmail.

VariableDefaultWhat it means
MICROSOFT_TENANT_IDnone, requiredThe id of your Microsoft Entra directory (tenant)
MICROSOFT_CLIENT_IDnone, requiredThe id of an app registration
MICROSOFT_CLIENT_SECRETnone, requiredA client secret of that app registration
MICROSOFT_SENDERnone, requiredThe mailbox that sends, for example cms@contoso.com
MICROSOFT_SAVE_TO_SENT_ITEMSfalsetrue keeps a copy in the mailbox’s Sent Items

In the Microsoft Entra admin center, register an app, give it the Microsoft Graph application permission Mail.Send with admin consent, and create a client secret. No person has to stay signed in. A MAIL_FROM other than the sender needs Send As rights on that address.

VariableDefaultWhat it means
RESEND_API_KEYnone, requiredA Resend API key with sending access
SENDGRID_API_KEYnone, requiredA SendGrid API key with Mail Send access
SENDGRID_REGIONglobaleu for an account with EU data residency
POSTMARK_SERVER_TOKENnone, requiredThe server API token
POSTMARK_MESSAGE_STREAMoutboundThe message stream to send through
MAILGUN_API_KEYnone, requiredA sending API key
MAILGUN_DOMAINnone, requiredThe sending domain
MAILGUN_REGIONuseu for a domain in Mailgun’s EU region

These services only send from addresses or domains you verified with them, so set MAIL_FROM to one of those.

  1. In .env, set MAIL_DRIVER to the new driver.
  2. Add that driver’s variables from the tables above and fill them in. You can delete the lines of the old driver.
  3. Set MAIL_FROM to an address the new driver may send from.
  4. Restart the CMS: Ctrl+C, then pnpm dev. In a docker project, run docker compose up -d instead; Compose hands every variable in .env to the CMS container.

If the CMS stops right away with Missing required environment variable:, fill in the variable it names. An unknown driver name also stops it, with a message listing the valid ones.

The quickest check is a tiny workflow that mails you:

  1. In the admin, open Workflows and create a new workflow.
  2. Set the trigger to On an event and tick Published under Events.
  3. Add a “Send an email” step, with your own address under To.
  4. Save the workflow and switch it on.
  5. Publish any page.
  6. With Mailpit, open http://localhost:8025: the mail should be there within a few seconds. With a real driver, check your inbox (and the spam folder).

If nothing arrives, open the workflow’s Runs tab in the admin: a failed email step shows the error message from the mail service. The CMS log in the pnpm dev terminal shows a warning workflow node failed with the same error; see Logs. Workflows explains the workflow editor in detail.