Download the PHP package bloomidea/mautic-plugin-ai-email-sections without Composer

On this page you can find all versions of the php package bloomidea/mautic-plugin-ai-email-sections. It is possible to download/install these versions without Composer. Possible dependencies are resolved automatically.

FAQ

After the download, you have to make one include require_once('vendor/autoload.php');. After that you have to import the classes with use statements.

Example:
If you use only one package a project is not needed. But if you use more then one package, without a project it is not possible to import the classes with use statements.

In general, it is recommended to use always a project to download your libraries. In an application normally there is more than one library needed.
Some PHP packages are not free to download and because of that hosted in private repositories. In this case some credentials are needed to access such packages. Please use the auth.json textarea to insert credentials, if a package is coming from a private repository. You can look here for more information.

  • Some hosting areas are not accessible by a terminal or SSH. Then it is not possible to use Composer.
  • To use Composer is sometimes complicated. Especially for beginners.
  • Composer needs much resources. Sometimes they are not available on a simple webspace.
  • If you are using private repositories you don't need to share your credentials. You can set up everything on our site and then you provide a simple download link to your team member.
  • Simplify your Composer build process. Use our own command line tool to download the vendor folder as binary. This makes your build process faster and you don't need to expose your credentials for private repositories.
Please rate this library. Is it a good library?

Informations about the package mautic-plugin-ai-email-sections

Mautic AI Email Sections

Generates MJML sections in Mautic's email builder from a plain text description, and edits sections that already exist.

It does not generate whole emails and it does not replace the theme. The theme still owns the brand and the skeleton; this fills one section at a time.

Requirements

No Composer dependency of its own, and no frontend build step. Both are deliberate: the runtime Mautic image has no composer.json, so a plugin-level dependency would never be autoloaded in production.

Installation

With Composer

Without Composer

The official mautic/mautic Docker image ships no composer.json, so there composer require is not an option. Clone by tag instead:

The directory name must be AiEmailSectionsBundle. Mautic derives the bundle namespace from it.

The reload is what creates the ai_email_sections_generation table. Mautic plugins carry no Doctrine migrations: PluginModel diffs the metadata of the entities under MauticPlugin\*\Entity and installs the schema during the reload.

Note the two plugin layouts. In development, inside the mautic/mautic monorepo, plugins live in plugins/. At runtime, inside the mautic/mautic:7-apache image, they live in docroot/plugins/. Both are correct, and mixing them up is the single most common way to waste an afternoon.

Local development

Develop against a clone of the mautic/mautic monorepo (plugins live in plugins/ there) with DDEV:

If you keep this plugin in a sibling directory and symlink it into plugins/, beware that a symlink built from an absolute host path resolves to nothing inside the DDEV container, which only mounts the Mautic root: mautic:plugins:reload finds the plugin on the host and Mautic never sees it in the container. A bind mount over the same path fixes it, in .ddev/docker-compose.customplugins.yaml:

Configuration

Settings → Plugins → AI Email Sections. While the integration is unpublished nothing is injected into the builder at all.

  1. Open the plugin and publish it.
  2. Enabled/Auth tab: paste the API key. It is stored encrypted here, and this is the only place it may go. The Features tab writes to featureSettings, which is plain text in the database.
  3. Features tab: pick the Provider. Leaving API base URL and Model name empty uses that provider's defaults, which is the normal case.
  4. Leave Offer personalisation tokens on unless you have a reason not to; see below for what it changes.
  5. Optionally fill Brand and tone of voice. In a 12-case comparison this was the single change that most improved the copy.
  6. Save, then confirm the block appears in the builder. If it does not, follow Verifying it works below rather than guessing.

The two tabs are built differently by the IntegrationsBundle and store to different places, which is why the key cannot simply be moved next to the rest of the connection settings.

Tab Field Default
Authentication (encrypted) API key empty
Features Provider OpenAI-compatible
Features Brand and tone of voice empty
Features Offer personalisation tokens on
Features Tokens to leave out empty
Features API base URL follows the provider
Features Model name follows the provider
Features Temperature 0.4
Features Max tokens 1500
Features Placeholder image URL placehold.co
Features Generations per hour, per user 30
Features Model timeout, in seconds 180
Features Theme tokens file (the default; the builder panel can override it per generation) default

The API key is stored on the encrypted auth tab and never reaches the browser.

Brand and tone of voice is optional free text describing what you sell, who you are writing to, how formal to be, and anything to avoid. It is appended to the end of the system prompt, capped at 2000 characters because it travels on every generation. Leave it empty and the assistant still works, it just writes more generic copy. In a 12-case comparison, filling it in removed every exclamation mark from the output and roughly doubled how often the copy mentioned what the company actually makes.

It sits at the end of the prompt on purpose: ahead of the allowlist, an unlucky phrasing could talk the model out of it, and the symptom would be every generation failing validation for no visible reason.

Personalisation tokens

With Offer personalisation tokens on, every generation is told which tokens this install actually has. The list is collected from EmailEvents::EMAIL_ON_BUILD, the same event that fills the builder's own token picker, so a token added by another plugin appears here with no work on our side, and a token that is removed disappears. It is also scoped to the user making the request: someone without permission to view contact data is not offered contact fields.

Size depends on how many contact fields the install has. With Mautic's default field set the list is 58 tokens, about 2 KB, or roughly 550 input tokens per generation; an install with 38 contact and 14 company fields published comes to about 67. Count yours with SELECT COUNT(*) FROM lead_fields WHERE is_published = 1, and add roughly 15 for the global tokens.

Each token carries the field's label, not a description written for the model: `{contactfield=city}` City. Labels are not unique, because contact and company fields can share one, so {contactfield=city} and {contactfield=companycity} both read City. The token name itself is what disambiguates them.

What it changes, measured over 8 personalisation-flavoured prompts run twice through the local model, with and without the list:

Without the list With the list
Distinct tokens ever used 1 7
Runs that personalised at all 13/15 14/14
Tokens that do not exist none none
Mean latency 18.6 s 20.0 s

Without the list the model only ever reaches for {contactfield=firstname}, which it copied from the examples in the prompt. With the list it also uses {signature}, {ownerfield=...} and, most usefully, {unsubscribe_url} and {webview_url}.

The footer case is the one that makes the difference concrete. Asked for a newsletter footer with an unsubscribe link and a view-in-browser link, the two arms produced:

Neither arm invented a token that does not exist, so the list is not what keeps the model honest; it is what lets it be useful.

Tokens to leave out takes one token per line, or a comma separated list, with or without braces. Anything listed there is never offered. Use it to keep a token out of generated copy, not as a security control: the tokens are not secret and a user can always type one by hand in the builder.

Turning the setting off entirely saves those ~550 input tokens per call and returns the assistant to {contactfield=firstname} only.

Providers

OpenAI-compatible is the default and covers every endpoint that speaks the OpenAI chat completions shape.

Default
Base URL http://localhost:4000/v1
Model qwen2.5:32b

For OpenAI itself, set the base URL to https://api.openai.com/v1 and the model to gpt-4o. Their newer reasoning models replaced max_tokens with max_completion_tokens and stopped accepting temperature, so they need a client change and will otherwise return a 400.

Anthropic talks to the Messages API directly.

Default
Base URL https://api.anthropic.com
Model claude-sonnet-5

The default is Sonnet rather than Opus on the strength of a 12-case comparison: identical first-attempt and validator-warning rates, comparable copy, roughly half the latency and half the cost. Set claude-opus-5 if you would rather have the larger model. Leave the fields blank to accept the defaults; an explicit value always wins.

Two settings behave differently under Anthropic, because the API does:

A proxy that fronts Anthropic with an OpenAI-compatible facade (LiteLLM) also works: leave the provider on OpenAI-compatible and point the base URL at the proxy. That is the better choice when you want several providers behind one endpoint, with a single place for cost logging and rate limiting.

Verifying it works

After installing or configuring, the block should appear in the builder's AI category. When it does not, walk this ladder in order. Each rung tells you which layer is at fault, and every check is a command rather than a guess.

1. Is the integration published? The subscriber injects nothing while it is not, by design.

is_published must be 1. A non-zero key length confirms the key was stored encrypted.

2. Is the subscriber in the compiled container?

AiEmailSectionsBundle\EventSubscriber\AssetsSubscriber::injectAssets() must be listed. If it is not, the container predates the plugin: run mautic:plugins:reload, then cache:clear.

Order matters on the deploy that installs the plugin. The configured post_deployment_command runs cache:clear when the container starts, which is before anyone runs mautic:plugins:reload. On that first deploy the subscriber can be missing from the compiled container even though every file is in place, so clear the cache again after the reload. Later deploys are fine: the plugin is already registered in the database before the container boots, and the subscriber survives on its own.

3. Is the asset served?

Expect 200. A 404 means the files did not reach docroot/plugins/.

4. Is a CDN serving the old file? Ask the origin and the edge separately, and compare the byte count:

Different sizes mean the file shipped correctly and the edge is stale.

5. Then it is the browser. Reload ignoring the cache, or check in devtools that index.js is actually requested on the builder page.

Changing an asset needs a cache purge, every time

Mautic appends ?v<hash> to asset URLs, and that hash is derived from configuration, not from file contents. Editing index.js or the icon therefore changes the file while leaving the URL identical, so neither the browser nor a CDN has any reason to refetch it. The new file sits on the server, correct and unreachable.

Behind Cloudflare, purging by URL means purging the exact URL including the query string: icon.png?v9fbd16a and icon.png are separate objects, and purging the second leaves the first, which is the one the page requests. Purging by URL failed for us on 2026-07-25 and a "Purge Everything" was what worked.

What is not needed

mautic:assets:generate does not help here. It concatenates the bundles under docroot/media/js/, and assets added through the VIEW_INJECT_CUSTOM_ASSETS event never enter them: they render as their own <script src> tag at request time. GrapesJsBuilderBundle works the same way and is likewise absent from those bundles. Running it is harmless but it is not the fix, and looking for the plugin's name in media/js/ will send you down a dead end.

How it works

Model output is treated as untrusted until it clears the validator. A generation that never validates changes nothing on the canvas.

The endpoint is POST /s/ai-email-sections/generate. Core's RequestSubscriber only validates X-CSRF-Token when X-Requested-With: XMLHttpRequest is present, so the frontend sends both headers explicitly and the controller validates again on its own.

Using it in the builder

  1. Create an email, pick an MJML theme, open the Builder.
  2. The block panel has an AI category holding the AI Section block. Drag it onto the canvas. The settings panel opens on the prompt automatically.
  3. Write what you want and press Generate. The placeholder's whole section is replaced in place, keeping its position among its siblings.
  4. Write the prompt in the language you want the copy in. The system prompt and the examples are English; the copy follows the request, not the instructions. A hint under the textarea says so.
  5. To change an existing section, select it and use the wand icon in the toolbar. The prompt describes the change; the current section is sent as the source. Edit mode keeps the language the section is already written in, whatever language you type the request in.
  6. The whole replacement collapses into a single undo step.

Theme tokens

One YAML file per theme in Resources/prompts/themes/. default.yaml is generic and the plugin works with nothing else, so a fresh install needs no client-specific configuration.

Nothing parses the file. The comments are stripped and the rest is pasted into the system prompt under a heading that tells the model to respect these values and invent no others. So the key names are a convention the model reads, not a schema the code validates: a key that is useful to a designer is useful here, and a key nobody needs costs input tokens on every generation for nothing.

That said, the model has the easiest time when the shape matches default.yaml:

Choosing a theme when generating

The builder panel carries a theme selector above the Generate button, so a section can match the email it is being inserted into rather than a single install-wide choice. It opens on the theme named after the email's own template when a file of that name exists, brienz.yaml for an email built on the Brienz theme, and on the configured default otherwise. That is a name match and nothing else: no reading of the email's MJML and no guessing. The selector is hidden when only one theme exists.

The plugin ships token files for the five email themes Mautic builds in MJML: attract, blank, brienz, confirmme and creative. Mautic's other email themes are the older HTML ones, and they get no file: this plugin generates MJML and cannot target them.

Those five were sampled from each theme's own email.html.twig rather than written by hand, and each file's header says which values were observed and which are neutral fill-ins. Expect the observed set to be thin: the themes declare almost nothing in mj-attributes and put their typography inline on each element, the same way real emails do.

Adding a theme

  1. Copy default.yaml next to itself and give it the client's name.
  2. Fill it from the MJML the client actually sends, not from a style guide. The two drift, and the template is the one telling the truth.
  3. Select it in Settings → Plugins → AI Email Sections → Features → Theme tokens file, which sets the default, or pick it per generation in the builder panel. Both lists come from ThemeCatalog, which globs the directory, so a new file appears with no cache clear needed, labelled by its name: field.

Creating a theme needs filesystem access, which means a Mautic administrator without a shell cannot add one. That is deliberate: the file belongs in version control next to the code, not in a database column where nothing can review it.

The prompt files under Resources/prompts/ are inputs to a language model rather than repository prose, but they are written in English like everything else here, and the copy the model produces follows the language of the request, not the language of the prompt.

Validator

This is the load-bearing piece and the one with serious test coverage.

Allowed tags: mj-section, mj-column, mj-group, mj-text, mj-image, mj-button, mj-divider, mj-spacer. Inline HTML inside mj-text and mj-button: p, br, strong, em, b, i, u, a, ul, ol, li, span, h1h4.

Before the strict XML parse there is a preprocessing pass, without which perfectly good MJML would be rejected: inline HTML inside mj-text is lifted out into opaque placeholders, repeated attributes collapse to the first, and named entities become numeric. Afterwards, empty tags are always written with an explicit closing tag (<mj-image></mj-image>), because grapesjs-mjml breaks on self-closing ones (GrapesJS/mjml#149).

After serialisation, percent-encoded braces in a token position are written back as literal braces. Both the model and libxml produce %7Bunsubscribe_url%7D in a link target, and Mautic replaces the literal form only, so without this pass a token used as a link ships as a link to a page that does not exist.

Edit mode adds preservation invariants. Images and {...} tokens are hard rejections: losing either fails the attempt. Links and text volume are soft, and on the final attempt they degrade into a visible warning rather than a failure.

Telemetry

Every generation is recorded in ai_email_sections_generation: prompt, mode, theme, model, attempt count, validation errors, warnings, latency and the response that finally passed. That is what makes it possible to compare providers and models with data instead of impressions.

Retention is enforced by a command, which is not scheduled for you:

Run it daily from cron. It refuses a window below one day, since zero would empty the table rather than enforce a policy. Until it is scheduled, the table grows without bound: every row holds a prompt, a source section and a generated document.

Permissions

The plugin registers its own permission bundle, aiemailsections:generations, so the assistant can be restricted per role. Without it, anyone with builder access can spend model tokens.

Testing

135 PHP tests and 38 JS tests. The functional ones prove that the script is only injected while the integration is published, that the API key never reaches the browser, and that the endpoint refuses requests without CSRF.

Two things to know before writing tests here:

PHPStan needs the Symfony test container at var/cache/test/AppKernelTestDebugContainer.xml. If you have cleared the test cache, regenerate it with APP_ENV=test APP_DEBUG=1 bin/console cache:warmup or PHPStan refuses to start.

Frontend

Assets/dist/index.js is served as-is. The plugin imports nothing (it receives the editor as an argument), so npm run build is a copy from Assets/src/ and there is no bundler. If imports ever become necessary, move to grapesjs-cli build the way GrapesJsCustomPluginBundle does.

Registration goes through window.MauticGrapesJsPlugins with context: ['email-mjml'], the documented extension point of GrapesJsBuilderBundle. Custom plugins load last in grapesjs.init(), after grapesjs-mjml has run its resetBlocks.

Two traps when working on this file:

Known gaps


All versions of mautic-plugin-ai-email-sections with dependencies

PHP Build Version
Package Version
Requires mautic/core-lib Version ^7.0
Composer command for our command line client (download client) This client runs in each environment. You don't need a specific PHP version etc. The first 20 API calls are free. Standard composer command

The package bloomidea/mautic-plugin-ai-email-sections contains the following files

Loading the files please wait ...