---
url: /modules/email-campaigns.md
---
# Email campaigns&#x20;

Newsletter-style emails built from blocks in the panel, rendered as email-safe HTML, checked for the usual pitfalls, and either copied into any sending tool or pushed to Brevo as a campaign draft. Evolve does not send campaign mail itself. Since 2.17 the module also syncs form leads to Brevo as [contacts](#contacts).

The module is off by default since 2.22. The [mail templates](/panel/mail-templates) for form mails, the email blocks and their renderer are part of the core and work without it. A site that uses campaigns or the contact sync sets `EVOLVE_MODULE_EMAIL=true`, see [To 2.22](/getting-started/upgrading#to-2-22).

::: glance

* **Module key** `email`
* **Default** off (on until 2.21)
* **Switch** `EVOLVE_MODULE_EMAIL`, see [Modules](/modules/#switching-a-module-on-or-off)
* **Role access** `evolve.roles.modules.email`
* **Panel** `/evolve/email/campaign`
* **Since** 2.6
  :::

## Configuration

| Variable | Default | Effect |
|---|---|---|
| `EMAIL_DRIVER` | unset | Provider. Unset means copy-paste only: the push button does not exist and there is no contact sync. `brevo` enables the push and the [contact sync](#contacts) with [Brevo](/integrations/brevo). `log` writes contact operations to the log instead, for local work (no push). |
| `EMAIL_BREVO_API_KEY` | falls back to `BREVO_API_KEY` | Brevo API key (v3). |
| `EMAIL_BREVO_BASE_URL` | `https://api.brevo.com/v3` | Brevo API base. |
| `EMAIL_SENDER_NAME`, `EMAIL_SENDER_EMAIL` | unset | Default sender when a campaign sets none. The address must be a verified sender in Brevo. |
| `EMAIL_MERGE_DIALECT` | `brevo` | Which merge tags the unsubscribe, mirror and profile links use: `brevo`, `mailcoach` or `mailchimp`. Read by the core (`evolve.mail.dialect`) since 2.22. |

The contact sync variables (`EMAIL_BREVO_WEBHOOK_*`, `EMAIL_BREVO_DOI_TEMPLATE_ID`, `EMAIL_CONTACTS_*`, `EMAIL_ALERT_ADDRESS`) are listed on [Brevo](/integrations/brevo#variables). None of these are edited in the panel. The look of the rendered mail (`EMAIL_BRAND_COLOR`, `EMAIL_CONTAINER_WIDTH`, `EMAIL_ASSET_BASE` and the theme values) is set in `evolve.mail` of the core since 2.22, see [Mail templates](/panel/mail-templates#look-of-the-mails); a published `config/email.php` that still carries `theme`, `dialect` or `asset-base` keeps winning.

## In the panel

**Email > Campaigns** at `/evolve/email/campaign`. A campaign has a name, a subject, a preheader (the preview text in the inbox), an optional sender name and address, a status (`draft` or `final`), and its blocks. Cmd-K: "Campaigns", "New campaign" (also found with "newsletter").

Content is built from the [email blocks](/panel/mail-templates#the-email-blocks) of the core, the same ones the mail templates use (the lead summary block is left out, a campaign has no submission). They never appear in the page block picker. 2.22 adds four blocks for object mails: Kopfzeile (with the "view in browser" link of the merge tag dialect), Abschnittstitel, Objektdaten and Ansprechperson, plus a centred layout with pill buttons through `evolve.mail.theme.alignment` and `button-style`.

Three actions sit on a campaign:

* **HTML** (`/evolve/email/campaign/{id}/html`) renders the final document for copy and paste into any sending tool. The same screen lists the checks below.
* **Preview** (`.../preview`) shows the rendered mail; `?draft=1` includes unpublished blocks.
* **Push** sends the campaign to the configured driver, see below.

![The campaigns list of the Email module](/screenshots/modules-email-campaigns.png)

## Checks before sending

Every render is checked. Errors block the push, warnings do not:

| Check | Level | What it means |
|---|---|---|
| Placeholder links | error | Buttons or links still point at `#` or a placeholder. |
| Layout table | error | The document structure is broken, usually a block without its wrapper. |
| Gmail clipping | error above, warning near the limit | Gmail cuts mails above about 100 KB of HTML. Shorten the campaign or split it. |
| Unsubscribe | error | No unsubscribe line. Every campaign needs the Abmelde-Zeile block. |
| Relative assets | error | An image points at a relative path; set `EMAIL_ASSET_BASE` or `APP_URL`. |
| Image alt text | warning | Images without alt text. |
| Image width | warning | Images without an explicit width, which Outlook renders at natural size. |

## Pushing to Brevo

With `EMAIL_DRIVER=brevo` and a key, the push creates an email campaign in Brevo with the campaign's name, subject (the name when empty), sender, preview text and the rendered HTML. Evolve stores Brevo's campaign ID; a second push updates the same Brevo campaign instead of creating another. Recipients, scheduling and the actual send happen in Brevo. A push with open errors is refused with a message; a Brevo API error is shown verbatim and reported to the error tracker.

## Contacts&#x20;

With a driver set, the module also syncs form leads to the provider as contacts. Which forms sync, to which lists and under which consent is set per form in the [Marketing card](/panel/forms-and-leads#marketing-contact-sync); the provider setup (key, double opt in template, webhook, environment variables, health check) is on [Brevo](/integrations/brevo#contact-sync).

The site keeps its own record of every synced address in `email_contacts`: consent, lists, the attributes last sent, and a status. Leads point to their contact (`contact_id`), so several leads of one address share one contact. The lead drawer shows the sync state of each lead, and since 2.18 the panel lists the contacts (see below).

| Status | Meaning |
|---|---|
| `pending` | Double opt in mail sent, the click is missing. |
| `active` | Synced; with consent once the box was ticked or the double opt in confirmed. |
| `unsubscribed` | Unsubscribed at the provider. `CONSENT_MARKETING` is set to `false` there. |
| `invalid` | Hard bounce, too many soft bounces (5 by default) or a spam complaint. The address is removed from every list it was synced to. |

A failed sync that is retried later never undoes an unsubscribe or a bounce that arrived in the meantime.

### Overview and contacts in the panel&#x20;

With a driver set, **Email** in the sidebar has two more entries. Both only read: nothing there writes to the provider, and contacts cannot be edited or deleted in the panel, since leads and the webhook keep them up to date.

**Übersicht** (Overview) at `/evolve/email/dashboard` shows the nine newest contacts of the main list as the provider has them, with name, address and when they were added. Addresses the provider has blocklisted carry "Gesperrt" (Blocked), and contacts the site also knows link to their drawer. The main list is `EMAIL_CONTACTS_MAIN_LIST` (a list ID from the provider); without it the card shows the newest contacts of the whole account, including imports and sign ups from outside the site. Next to it are the local contacts per status and the open sync failures. When the provider is unreachable, the card shows its error and the local numbers stay. With the `log` driver only the local numbers appear.

**Kontakte** (Contacts) at `/evolve/email/contact` lists `email_contacts` with search (name, address) and a status filter. The drawer shows status with the unsubscribe or bounce time, consent with time and hash, the lists by name, the attributes last sent and the linked leads, each opening its lead drawer. Cmd-K: "Overview", "Contacts" (also found with "brevo").

A driver can supply the overview list by implementing `Racerfish\Email\Contracts\ListsRecentContacts` next to `ContactDirectory`; without it the overview shows only the local numbers.

### Webhook events

The provider reports back through the webhook at `api/email/webhook`:

| Event | Effect |
|---|---|
| unsubscribed | status `unsubscribed`, consent removed |
| hard bounce | status `invalid`, removed from the lists |
| soft bounce | counted; at `contacts.soft-bounce-threshold` (5) handled like a hard bounce |
| spam | like a hard bounce, plus an alert mail |
| contact deleted | the contact is deleted on the site too; its leads stay |

Other events are acknowledged and ignored.

### Failures and retries

A provider that refuses a request or cannot be reached never affects the visitor. The lead is stored, the error is kept on the lead (shown as "Sync fehlgeschlagen" in the drawer) and in `email_sync_failures`. With a queue other than `sync`, the sync runs in the queue worker instead of the request.

`php artisan email:retry-sync` replays the open failures. The scheduler runs it daily; run it by hand after fixing the cause, for example a wrong key or the missing phone verification at Brevo. A failed lead is replayed from the lead itself, so a corrected field mapping applies on the retry. When one failure has failed `contacts.alert-after-attempts` times (10), one alert mail goes to `EMAIL_ALERT_ADDRESS`, falling back to `HEALTH_WEBMASTER_ADDRESS`. Spam complaints mail the same address. Without either address the alert only goes to the log. The health check counts the open failures.

Every request to the provider and every webhook call is logged in `email_request_logs` without the API key or webhook token, and pruned daily after `contacts.request-log-retention-days` (30).

### For module developers

Attributes a module sends (for example the computed attributes of its [form types](/custom-modules/extending-evolve#form-types)) can be registered so that `email:setup` creates them and the health check expects them:

```php
use Racerfish\Email\Contacts\AttributeSchemas;
use Racerfish\Email\Enums\AttributeType;

app(AttributeSchemas::class)->register(['BUDGET_MIN' => AttributeType::Number]);
```

Types are `Text`, `Number`, `Date` and `Boolean`.

## Publishing

`email.config` publishes `config/email.php`. `email.views` publishes `render/campaign.blade.php`, the campaign wrapper around the core's mail document, to `theme/views/vendor/email/render`. The document layout and the block views belong to the core since 2.22 and publish with `evolve.mail-views` into `theme/views/vendor/evolve/email`; block copies that an earlier `email.views` left under `theme/views/vendor/email/render/blocks` are still read while the module is on. The email blueprints are package blueprints and are not published.

## Verifying

Build a campaign with a logo header, a headline, a paragraph, a button and the unsubscribe line, open HTML and confirm the checks are green. Send the HTML to yourself through Brevo's test send, or push and use Brevo's preview. On staging, nothing is sent by Evolve, so the mail redirect from [Mail](/getting-started/mail) does not apply here.
