PHPackages                             mspacemedia/silverstripe-newsletter - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [Mail &amp; Notifications](/categories/mail)
4. /
5. mspacemedia/silverstripe-newsletter

ActiveSilverstripe-vendormodule[Mail &amp; Notifications](/categories/mail)

mspacemedia/silverstripe-newsletter
===================================

Reusable newsletter module: compose MailChimp-style drag-and-drop emails with Elemental blocks, manage CMS-defined audiences with CSV import/export, and send in batches over SMTP.

1.0.5.1(4w ago)033↓50%BSD-3-ClausePHP ^8.1

Since Jun 21Compare

[ Source](https://github.com/mspacemedia/silverstripe-newsletter)[ Packagist](https://packagist.org/packages/mspacemedia/silverstripe-newsletter)[ RSS](/packages/mspacemedia-silverstripe-newsletter/feed)WikiDiscussions Synced 2w ago

READMEChangelogDependencies (16)Versions (9)Used By (0)

SilverStripe Newsletter
=======================

[](#silverstripe-newsletter)

Compose MailChimp-style, drag-and-drop HTML newsletters in the CMS from Elemental blocks, manage CMS-defined audiences, brand them from a central theme, send them in batches over SMTP, and track opens/clicks/bounces using Silverstripe's native CMS preview pane in the editor.

**Version:** 1.0.5 **Requires:** SilverStripe 5.4 · PHP 8.1+

---

Contents
--------

[](#contents)

- [Features](#features)
- [Installation](#installation)
- [Concepts](#concepts)
- [The block library](#the-block-library)
- [Branding / theme](#branding--theme)
- [Audiences, subscribers &amp; CSV import/export](#audiences-subscribers--csv-importexport)
- [Dynamic audiences (source providers)](#dynamic-audiences-source-providers)
- [Computed merge fields](#computed-merge-fields)
- [Audience segments](#audience-segments)
- [Sending](#sending)
- [Tracking (opens &amp; clicks)](#tracking-opens--clicks)
- [Bounce handling](#bounce-handling)
- [Preview](#preview)
- [Routes](#routes)
- [Developing the module](#developing-the-module)

---

Features
--------

[](#features)

- **Elemental block composer** with a full block set (header, headings, text, images, buttons, columns, social, video, code, footer, …) and inline editing.
- **Central brand/theme** (`NewsletterBrand`) — fonts, colours, button shape, logo; every block inherits it and can override per-block.
- **CMS-defined audiences** with CSV import/export, plus pluggable **source providers**to populate audiences from a host project's own models.
- **Instant test send** and **batched bulk send** (QueuedJobs) over SMTP, with retry + throttle (`MailHelper`) and a `List-Unsubscribe` header.
- **Open &amp; click tracking**, per-issue engagement stats, and a CMS stats panel.
- **Bounce handling** by piping DSNs to a task (phase-1; webhook-free).
- **Native CMS preview** with Silverstripe's built-in device switching, dirty block refreshes, and an unsaved-changes banner.
- **Sent issue snapshots** so delivered newsletters and view-online pages do not change after a live send starts.

---

Installation
------------

[](#installation)

1. In the project root `composer.json`, add the path repository and require it, or via command line:

    `composer require mspacemedia/silverstripe-newsletter`

A default `NewsletterBrand` record is created automatically on `dev/build`.

> **Permissions:** everything is gated behind the `MANAGE_NEWSLETTERS` permission. Grant it to the groups that should manage newsletters (Security → Groups → Roles).

---

Concepts
--------

[](#concepts)

ModelRole`NewsletterIssue`A single newsletter. Holds an Elemental area, targets audiences, carries send status + stats.`NewsletterAudience`A mailing list. Subscribers are attached manually, by CSV, or by a source provider.`NewsletterSubscriber`A recipient (unique by email). Belongs to many audiences. Status: Active / Unsubscribed / Bounced (global suppression).`NewsletterBrand`Global styling tokens (the "theme"). One record per install, editable in the CMS.`NewsletterSendRecord`Per-recipient delivery + engagement ledger (sent/failed/bounced, opens, clicks).All are managed under the **Newsletters** admin section.

---

The block library
-----------------

[](#the-block-library)

Issues are composed from Elemental blocks (edited inline). Available blocks:

`Header` (brand logo, overridable) · `Heading` · `Text` · `Boxed text` · `Columns`(2–3) · `Image` · `Image + caption` · `Image group` · `Image card` · `Button` · `Video` (links out) · `Social follow` · `Logo` · `Custom HTML` · `Divider` · `Spacer`· `Footer` (address + view-online/unsubscribe links).

Every block has an **Appearance** tab with per-block overrides — padding, alignment, **font family, background colour, text colour, link colour** (blank = inherit the brand), full-width **edge-to-edge** toggle (used by the Image block for full-bleed images), and hide-on-mobile. Buttons additionally allow a per-block **corner radius**.

The blocks render to email-safe, table-based HTML; CSS is inlined at send time (Emogrifier). These blocks are restricted to newsletters — they don't appear in normal page editors.

---

Branding / theme
----------------

[](#branding--theme)

`NewsletterBrand` holds the global look: font family, primary/link/heading/body colours, page + content background, content width, button colour/text/radius/padding, divider colour, footer colour, and a header logo.

- Edit it in **Newsletters → Brands / themes**.
- Blocks **inherit** these values; a block only overrides what you set on it.
- An issue can pick a specific brand (the **Brand / theme** dropdown), or use the default.
- The module ships neutral defaults. A host project can seed its own palette by overriding the config defaults, e.g.:

    ```
    # app/_config/newsletter.yml
    MSpaceMedia\Newsletter\Model\NewsletterBrand:
      defaults:
        PrimaryColor: '#9c1d44'
        LinkColor: '#9c1d44'
        ButtonColor: '#9c1d44'
        BodyBackground: '#faf6ee'
    ```

    The seeded record remains fully editable in the CMS afterwards.

---

Audiences, subscribers &amp; CSV import/export
----------------------------------------------

[](#audiences-subscribers--csv-importexport)

- Create audiences in **Newsletters → Audiences**.
- **Export**: each audience's subscriber grid has an Export button.
- **Import**: if `i-lateral/silverstripe-importexport` is installed, each audience's subscriber grid gains a CSV importer that adds rows **to that audience**, deduped by email (existing subscribers are reused, not duplicated).
- Suppression is **global per email** — an unsubscribed or bounced subscriber is skipped by every audience's sends and is never silently re-activated.

Subscribers carry name/email plus arbitrary **merge data** (custom JSON keys). These personalise content through the `{{ … }}` [merge-field engine](#computed-merge-fields) — e.g. `{{ FirstName }}`, `{{ Email }}`, and any custom key as `{{ MYKEY }}`. Unsubscribe and view-online links are provided by the **Footer** block.

Personalisation resolves **late**: a live send stores the rendered issue HTML before recipient-specific values are substituted, then each delivery (and recipient-specific view-online request) resolves the merge fields for that subscriber.

---

Dynamic audiences (source providers)
------------------------------------

[](#dynamic-audiences-source-providers)

A host project can feed an audience from its own data by implementing `MSpaceMedia\Newsletter\Source\AudienceSourceProvider`:

```
interface AudienceSourceProvider
{
    public function getKey(): string;     // matches NewsletterAudience.SourceKey
    public function getTitle(): string;   // used if the audience is auto-created
    public function getSubscribers(): iterable; // yields rows (see below)
}
```

Each yielded row:

```
[
    'Email'     => 'person@example.com', // required
    'FirstName' => 'Jane',               // optional
    'Surname'   => 'Doe',                // optional
    'MergeData' => ['CITY' => 'Leeds'],  // optional, custom merge tags
    'Anchor'    => $member,              // optional DataObject; see Computed merge fields
    'Consent'   => true,                 // optional, default true; false = skip
]
```

Register providers in config and run the refresh task:

```
MSpaceMedia\Newsletter\Task\NewsletterAudienceRefreshTask:
  providers:
    - App\Newsletter\MyOrderAudienceProvider
```

```
php vendor/bin/sake dev/tasks/NewsletterAudienceRefreshTask
```

The task upserts subscribers (deduped by email) into the matching audience, refreshes name/merge data, links the optional `Anchor`, and never re-activates unsubscribed/bounced records. Audiences with no provider are simply manual/CSV-only.

Returning an `Anchor` (any `DataObject`, typically the project's `Member`) is what lights up the [computed merge fields](#computed-merge-fields) below — it stores the polymorphic `NewsletterSubscriber.Anchor` that `{{ … }}` expressions traverse. A provider that builds an audience from members therefore needs nothing extra:

```
public function getSubscribers(): iterable
{
    foreach (Member::get()->filter('NewsletterOptIn', true) as $member) {
        yield [
            'Email'     => $member->Email,
            'FirstName' => $member->FirstName,
            'Surname'   => $member->Surname,
            'Anchor'    => $member,           // {{ Orders.Sum(...) }} now resolves per recipient
        ];
    }
}
```

> **Subscription API** (for project glue such as account pages / checkout): `MSpaceMedia\Newsletter\Service\NewsletterSubscriptionManager` provides `subscribe($email, $audienceKey, $data)`, `unsubscribe($email)`, `bounce($email)` and `isSubscribed($email)`. The `$data` array also accepts an `'Anchor' => $record` to link the subscriber for computed merge fields. `unsubscribe()` fires an `onNewsletterUnsubscribe`extension hook so a project can reflect the change back onto its own models.

---

Computed merge fields
---------------------

[](#computed-merge-fields)

Editors personalise content with `{{ … }}` merge fields — both simple values (`{{ FirstName }}`) and **computed** fields that derive a value per recipient from the project's own data: totals, counts, filtered aggregates, with maths and conditionals. The drag-and-drop-friendly equivalent of MailChimp's merge tags.

Examples:

```
{{ Orders.Sum(TotalDonation) | currency }}        → £1,330.00
{{ Orders.Count }}                                → 35
{{ Orders.Where(Status = 'Paid').Count }}         → 12
{{ Concat(FirstName, ' ', Surname) }}             → Jane Doe

{{#if Orders.Count}}You've donated {{ Orders.Count }} times{{else}}no orders placed{{/if}}

```

### The anchor

[](#the-anchor)

Every computed field traverses from the subscriber's **anchor** — a record linked via the polymorphic `NewsletterSubscriber.Anchor` (set by a source provider or the subscription manager, above). The anchor is usually a `Member`; configure its type so the visual builder knows what to introspect:

```
MSpaceMedia\Newsletter\Model\NewsletterSubscriber:
  anchor_class: SilverStripe\Security\Member
```

A subscriber with no anchor isn't an error: every computed field resolves to empty, so `| default(…)`and `{{#if}}` fallbacks take over (e.g. "no orders placed").

### Defining a field

[](#defining-a-field)

Create fields under **Newsletters → Merge fields**. Each has a **Tag** (used as `{{ TAG }}`) and an **Expression**. The expression editor offers relation/field pickers and a **live preview** that evaluates against a random sample record — so you build the output visually and see a real value as you go. A defined tag can be referenced from other expressions, and tags resolve in this order: defined field → built-in (`FirstName`, `Surname`, `Email`, plus the subscriber's custom merge data) → a field/relation on the anchor.

### Expression language

[](#expression-language)

ConstructExampleNotesField`FirstName`Built-in or an anchor fieldRelation count`Orders.Count``has_many` / `many_many`Aggregate`Orders.Sum(Amount)`also `Avg` / `Min` / `Max`Filtered`Orders.Where(Status = 'Paid').Sum(Amount)``=`, `!=`, `>`, `=`, ` Requires a mail-forwarder rule and `proc_open` on the host.

---

Preview
-------

[](#preview)

Newsletter issues use Silverstripe's native `CMSPreviewable` support inside `NewsletterAdmin`, so the preview appears in the standard CMS preview pane rather than a custom floating panel. The built-in Silverstripe preview controls, including device switching, remain available.

Preview rendering is handled by two ModelAdmin actions:

- `cmsPreview/` renders the current persisted issue state.
- `cmsPreviewUnsaved/` accepts dirty Elemental block form data, applies it to the in-memory block instance only, and renders the iframe without writing the block or issue.

### Preview as a subscriber

[](#preview-as-a-subscriber)

A toolbar at the top of the preview lets you render the issue **as a real subscriber** — like MailChimp's "Preview as subscriber". `cmsPreview` accepts `?previewSubscriber=random` (or a subscriber ID), draws from the issue's recipients (falling back to any active subscriber), and renders through `renderEmail($issue, $subscriber)` so `{{ … }}` merge fields are resolved for that person (tracking pixels/links are omitted in preview). The toolbar's **Another** / **No personalisation** links simply reload the iframe — no parent-frame scripting.

`client/dist/newsletter-preview.js` only coordinates the native preview:

- It watches Elemental block dirty/status changes, including editor text changes before the block is saved as draft or published.
- It posts the dirty block payload once per changed payload, preventing repeat `cmsPreview` loops after the first edit.
- It injects the returned HTML into the native preview iframe and shows the translated "Contains unsaved changes" banner when the iframe represents unsaved block data.
- Once the block is saved and the dirty state clears, it refreshes the normal native preview URL.

Sent issues render from the locked `SentHTML` snapshot when available, so the preview and public view-online pages reflect the sent content rather than later edits.

---

Routes
------

[](#routes)

RoutePurpose`newsletter/view/`Public "view online" page for an issue; uses the sent snapshot when present.`newsletter/viewrecord/`Recipient-specific "view online" page for a send record; resolves merge tags for that subscriber.`newsletter/unsubscribe/`One-click unsubscribe (per-subscriber token).`newsletter/open/.png`Open-tracking pixel.`newsletter/click/?u=`Click redirector.`newsletter/preview/`Legacy/admin preview route; native CMS preview uses `NewsletterAdmin` `cmsPreview` actions.---

### Layout

[](#layout)

```
src/
  Model/    NewsletterIssue, NewsletterAudience, NewsletterSubscriber,
            NewsletterBrand, NewsletterSendRecord, NewsletterPermissions
  Elements/ NewsletterBlockElemental (base) + concrete blocks, ScaledImageTrait
  Service/  NewsletterRenderService, NewsletterSubscriptionManager, NewsletterSender
  Job/      NewsletterSendJob
  Task/     NewsletterSendTask, NewsletterAudienceRefreshTask, NewsletterBounceTask
  Control/  NewsletterController
  Admin/    NewsletterAdmin
  Email/    MailHelper
  Source/   AudienceSourceProvider
templates/MSpaceMedia/Newsletter/Email/   Wrapper.ss + Blocks/*.ss
client/dist/   newsletter-preview.js
lang/          Translation strings

```

---

Out of scope (v0.1)
-------------------

[](#out-of-scope-v01)

- Bounce capture via IMAP/POP polling or ESP webhooks (only piped-DSN handling so far).
- A/B testing, scheduled-future sends beyond a queued start, full open/click drill-down dashboards.
- Per-recipient preview selection inside the CMS; recipient-specific output is rendered by `newsletter/viewrecord/` after a send record exists.

###  Health Score

41

—

FairBetter than 87% of packages

Maintenance94

Actively maintained with recent releases

Popularity10

Limited adoption so far

Community2

Small or concentrated contributor base

Maturity48

Maturing project, gaining track record

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Every ~2 days

Total

7

Last Release

29d ago

### Community

Maintainers

![](https://avatars.githubusercontent.com/u/858675?v=4)[Mark Muller](/maintainers/mspacemedia)[@mspacemedia](https://github.com/mspacemedia)

---

Tags

emailsilverstripenewslettermailingelemental

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/mspacemedia-silverstripe-newsletter/health.svg)

```
[![Health](https://phpackages.com/badges/mspacemedia-silverstripe-newsletter/health.svg)](https://phpackages.com/packages/mspacemedia-silverstripe-newsletter)
```

###  Alternatives

[putyourlightson/craft-campaign

Send and manage email campaigns, contacts and mailing lists.

6538.0k2](/packages/putyourlightson-craft-campaign)[sunnysideup/ecommerce

Silverstripe E-commerce Application

247.4k84](/packages/sunnysideup-ecommerce)[mediaessenz/mail

Powerful newsletter system for TYPO3

1112.2k3](/packages/mediaessenz-mail)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
