Newsletter Management: Composing and sending
Write newsletters in the Sylius admin, check them with preview and test mail, send or schedule them - placeholders, audience, cron sending, delivery log, sender and mailer transport
Newsletter Management: Composing and sending
This documentation always describes the latest published version.
This page covers writing a newsletter in the admin, checking it with the preview and a test mail, and then sending or scheduling it. How opens and clicks are measured is described on Tracking and statistics.
Overview
A newsletter belongs to exactly one channel and has a subject, a preheader, an HTML version and a plain-text version. The recipients are fixed when sending starts. The mails themselves are sent by the console command mmd:newsletter:send, which a cron job runs regularly. Without that cron job not a single mail goes out, not even after clicking Send now.
graph LR
A["Draft"] --> B["Ready"]
B -->|"Schedule"| C["Scheduled"]
C -->|"Cancel"| B
B -->|"Send now"| D["Sending"]
C -->|"Time reached"| D
D -->|"all deliveries done"| E["Sent"]
D -->|"Cancel"| F["Cancelled"]
A newsletter can only be edited while it is a Draft or Ready. From Scheduled on it is read-only, because its content is what subscribers get (or are about to get).
Creating a newsletter
You reach the newsletters through the Newsletters button in the title bar of the newsletter settings (admin sidebar, MMKreativ Plugins, Newsletter management). The editor fields:
| Field | Description |
|---|---|
| Channel | The channel the newsletter is sent from. It determines the sender, the theme and the group of subscribers. Required. |
| Language | Optional. If a language is chosen, only subscribers with that language receive the newsletter. Empty = all languages. |
| Audience (categories) | Optional. Only shows the categories of the selected channel. Empty = all subscribers. See Audience. |
| Subject | Required, at most 255 characters. Placeholders are allowed. |
| Preheader | Optional, at most 255 characters. Many mail clients show it next to the subject, and it is added as an invisible first line of the mail. |
| HTML version | The content of the mail, with the visual editor or as plain HTML code. |
| Wrap in the shop's e-mail layout | On by default. The HTML content is then placed in your shop's e-mail layout (logo, colors and footer of the theme). Off = your HTML is sent as it is. |
| Text version | The plain-text alternative of the mail. If the field is empty when you save, the plugin generates the text from the HTML. |
| Regenerate the text version from the HTML (overwrites the text above) | A checkbox that overwrites the text above. Handy after you changed the HTML. |
| Status | Draft, Ready, Scheduled, Sending, Sent or Cancelled. To send, set the newsletter to Ready and save. |
The generated text version turns links into
label (URL)and paragraphs into line breaks. Read it through once before sending and adjust it if needed.
Visual editor
For the HTML field the plugin loads the visual editor TinyMCE (GPL) from cdn.jsdelivr.net, in a pinned version and with a subresource integrity (SRI) check. It has a Placeholders menu that inserts placeholders at the cursor and a source code view. The Visual editor switch above the field turns it off, for example when you paste finished e-mail HTML that a visual editor would rewrite. The browser remembers your choice. If the script cannot be loaded (offline or blocked), the field stays a normal text area and the switch disappears.
Placeholders
Write placeholders in double curly braces, {{ first_name }}; spaces inside the braces do not matter. This is deliberately not Twig: there is a fixed list that is filled in by plain string substitution.
| Placeholder | Replaced by |
|---|---|
{{ email }} |
The recipient's e-mail address |
{{ first_name }} |
The recipient's first name (empty if not given) |
{{ last_name }} |
The recipient's last name (empty if not given) |
{{ channel_name }} |
The name of the channel |
{{ manage_url }} |
Personal link to the page where the subscriber changes name and categories |
{{ unsubscribe_url }} |
Personal unsubscribe link |
In the HTML the inserted values are HTML-escaped; in the subject and the text version they stay plain text. A placeholder that an editor or browser URL-encoded inside a link target (%7B%7B%20manage_url%20%7D%7D) is turned back into {{ manage_url }} when you save.
An unknown placeholder such as
{{ frist_name }}is always rejected on save in the subject, HTML and text, even in a draft. That way a typo never reaches a subscriber as literal text.
Before a newsletter can be set to Ready, the HTML version and the text version must both be non-empty and both contain
{{ manage_url }}and{{ unsubscribe_url }}. A draft, on the other hand, may be saved incomplete.
Preview and test send
Both are on the newsletter's edit page.
| Function | Behavior |
|---|---|
| Preview | Shows the last saved version with sample data (Max Mustermann), HTML and text side by side. Save your changes first. The HTML runs in a sandboxed frame, so scripts are not executed. On the detail page two buttons open the HTML and the text in a new tab. |
| Test send | Sends the last saved version to an address of your choice, with the subject prefixed by [Test]. If the address is a real subscriber of this channel, the manage and unsubscribe links in the test mail work. Test and preview mails are never tracked, so they cannot distort any numbers. |
The test send needs a sender address (see Sender and transport). If it fails, the admin shows you the reason, for example that the SMTP server rejected the mail.
Duplicating and deleting
- Duplicate creates a copy as a draft with the subject
Copy of: .... Content, channel, language, audience and the layout choice are carried over; the schedule, the recipients and the delivery log are not. It works for an already sent newsletter too and is the usual way to start the next issue. - Delete is only possible in the status Draft or Ready. Everything from Scheduled on is the record of what subscribers received, including its delivery log. The guard applies to the delete button and to bulk delete alike.
Audience
The recipient list is fixed when sending starts. A subscriber receives the newsletter if all of these apply:
- Their status is confirmed (pending and unsubscribed subscribers never receive anything).
- They belong to the newsletter's channel.
- If the newsletter has a language: the subscriber's language is exactly that one.
- If the newsletter has categories: the subscriber has chosen no category (they want the general newsletter, i.e. everything) or at least one of the chosen categories. If the newsletter has no categories, this point does not apply.
Before sending, the sending section of the newsletter shows the current number (Goes to N subscriber(s) right now.). The final number is fixed as soon as sending starts and stored as the newsletter's recipient count. There is exactly one delivery log entry per newsletter and address, so nobody receives the same newsletter twice.
Sending, scheduling, cancelling
The Sending section on the edit page shows the sender, the audience size, progress and the matching buttons. All three actions are POST requests with CSRF protection.
| Action | When possible | Effect |
|---|---|---|
| Send now | Status Ready or Scheduled | After a confirmation prompt the plugin fixes the recipients and sets the status to Sending. The mails go out with the next run of mmd:newsletter:send. |
| Schedule | Status Ready or Scheduled | You choose a date and time (in the time zone the form shows). The time must be in the future. A scheduled newsletter can be rescheduled. |
| Cancel | Status Scheduled or Sending | A scheduled newsletter goes back to Ready. For a running send, what was already sent stays sent, all still pending deliveries are skipped, and the newsletter becomes Cancelled. |
These reasons prevent a start and are shown as an error message: the newsletter is not in the right status, there is no sender address, the audience is empty, or the time is in the past. If a scheduled newsletter can no longer start at its time (no recipients left, sender removed), it goes back to Ready instead of being retried forever.
The cron job
mmd:newsletter:send is the engine of sending. It first starts all scheduled newsletters whose time has come, then sends a batch of pending mails.
* * * * * cd /path/to/shop && php bin/console mmd:newsletter:send --limit=100
| Option | Default | Description |
|---|---|---|
--limit |
100 |
Maximum number of mails per run. At least 1. Stay below your host's mail limit. |
--delay-ms |
0 |
Pause between two mails in milliseconds. Must not be negative. |
The output reports how many scheduled newsletters were started, mails sent and skipped, failed attempts and newsletters finished.
Good to know:
- Overlapping runs are safe. Every delivery is claimed atomically.
- A failed send is retried on the following runs, at most three times in total. After that the delivery is Failed. The log only stores the class and code of the error, never its message, because that could contain the recipient's address.
- If a run crashes mid-send, a claimed but never finished delivery goes back to Waiting after 15 minutes.
- If a subscriber unsubscribes between the start and the delivery, their delivery is skipped.
- When no deliveries are open any more (neither waiting nor in progress), the newsletter moves to Sent.
- The plugin deliberately does not use Symfony Messenger: a worker for an async queue is infrastructure a shop does not have to provide, and an unrouted Messenger message would otherwise run inside the admin request. Cron is available everywhere.
Delivery log
On a newsletter's detail page (once sending has started) the delivery log lists every delivery with address, status, attempts, send time and a note. The filter buttons narrow it down by status.
| Status | Meaning |
|---|---|
| Waiting | Not sent yet (also: an attempt failed and will be retried) |
| Being sent | Claimed by a run |
| Sent | Handed to the mail server successfully |
| Failed | Given up after three attempts; the note names the error class and code |
| Skipped | Deliberately not sent: the subscriber unsubscribed or the newsletter was cancelled |
The log keeps its own copy of the address, so it survives even if the subscription is deleted later. The cleanup commands (see Tracking and statistics) do not touch it.
Sender and transport
- Sender: Address and name come from the channel's newsletter settings (sender e-mail, sender name). If the address is missing, the channel's contact e-mail is used; if the name is missing, the channel name. Without any address the newsletter can neither be sent nor tested, and the sending section points this out.
- Mailer transport: In the newsletter settings you can enter the name of a Symfony Mailer transport (
framework.mailer.transports) so newsletters run separately from the shop's own mails. Empty = default transport. The name is attached to the mail as anX-Transportheader; Symfony Mailer picks the transport by it and removes the header before sending. - Mail format: Every mail is sent as a real multipart message with an HTML and a text part, via the Symfony Mailer and not the Sylius mailer, which only sends HTML.
- Unsubscribe headers: Every mail carries
List-Unsubscribewith the personal unsubscribe link andList-Unsubscribe-Post: List-Unsubscribe=One-Click. Mail clients can thus offer an unsubscribe button whose POST unsubscribes without a further step (RFC 2369 and RFC 8058). - Theme: When the newsletter is embedded in the e-mail layout, the plugin always renders it with the theme of its channel, even from the cron job (which has no request) or from the admin of another channel.
If the sender address differs from the mailbox your SMTP account sends as, your mail server must permit that address (SPF/DKIM), otherwise the newsletters end up in spam or are rejected.
Worked example: from draft to sent
A shop with the channel Webshop sends its spring newsletter to everyone who chose the category Offers (or no category at all).
- Open the newsletter list in the admin and choose Create.
- Channel
Webshop, languageEnglish (United States), tick the audienceOffers. - Subject
Hello {{ first_name }}, spring is here, preheaderUp to 20 % off selected items. - Write the text in the visual editor and use the Placeholders menu to insert
{{ manage_url }}and{{ unsubscribe_url }}as link targets (for exampleChange your settingsandUnsubscribe). Leave the text version empty; the plugin generates it on save. - Save with the status Draft, check the Preview and use Send test mail to send a mail to your own address.
- If everything looks right, read the text version through, set the status to Ready and save. If that fails, one of the two versions is still missing a required placeholder.
- In the Sending section pick Thursday 09:00 under Send at and click Schedule. The status is now Scheduled.
- The cron job starts the newsletter at the set time and sends it in batches of 100 mails per minute. You can follow the progress in the sending section and the delivery log. When all deliveries are done, the status is Sent.
For the next issue, open the newsletter and choose Duplicate.