---
title: "Feature: #91724 - Introduce TemplatedEmailFactory for centralized email creation"
manual: "TYPO3 Core Changelog"
version: "main"
permalink: "https://docs.typo3.org/permalink/changelog:feature-91724-1737200000"
source: "Changelog/14.2/Feature-91724-TemplatedEmailFactory.rst"
typo3-version: "14.2"
typo3-major: 14
type: "feature"
issue: 91724
forge: "https://forge.typo3.org/issues/91724"
tags: ["PHP-API", "YAML", "ext:core"]
rendered: "2026-09-20T18:31:11+00:00"
---

# Feature: #91724 - Introduce TemplatedEmailFactory for centralized email creation {#feature-91724-1737200000}

See [forge#91724](https://forge.typo3.org/issues/91724)

## Description {#description}

A new `TemplatedEmailFactory` class has been
introduced to provide a centralized creation of
`FluidEmail` instances.

The factory provides three methods for different use cases:

-   **`create()`**

    For backend and CLI contexts, such as login notifications, Scheduler
    tasks, and the Install Tool, where only the global configuration
    `$GLOBALS['TYPO3_CONF_VARS']['MAIL'][...]` is used.

-   **`createFromRequest()`**

    For frontend contexts, such as form submissions and `EXT:felogin`, where
    site-specific email templates should be applied. It merges site settings
    from `typo3/email` with the global configuration
    `$GLOBALS['TYPO3_CONF_VARS']['MAIL'][...]`.

-   **`createWithOverrides()`**

    For extensions that need to provide custom template paths merged on top of
    the base configuration, optionally taking a request context into account.
    Two cases of template resolution are possible, ordered by priority:

    -   Request without site attribute: 1. Provided override arguments ->
        2\. global configuration
    -   Request with site attribute: 1. Provided override arguments ->
        2\. site settings -> 3. global configuration

    Note that you can also use the numerical priority of template paths so
    that site settings with a higher priority number can override paths in the
    provided arguments with a lower priority number.

## Site settings {#site-settings}

A new site set, `typo3/email`, is available in EXT:core and defines the
settings below. These are applied automatically when a request with a site
attribute is passed to `createFromRequest()` or
`createWithOverrides()`. This means extensions running in a frontend
context, such as EXT:form email finishers, benefit from site-specific email
configuration:

-   **`email.format`**

    The email format to use (`html`, `plain`, `both`). If empty, the global
    configuration is used.

-   **`email.templateRootPaths`**

    An array of paths to email templates. These are merged with the global
    mail template paths.

-   **`email.layoutRootPaths`**

    An array of paths to email layouts. These are merged with the global mail
    layout paths.

-   **`email.partialRootPaths`**

    An array of paths to email partials. These are merged with the global mail
    partial paths.

> [!TIP]
> **Hint**
>
> Please note that entering these paths via the site settings GUI adds
> entries as a sequential array, numbered 0, 1, 2, and so on. In this case,
> all paths are appended to the array, giving all entries the highest
> priority.
> When editing `settings.yaml` manually, specific numerical array keys can
> be assigned.

## Usage {#usage}

### Frontend usage (site-aware) {#frontend-usage-site-aware}

For frontend contexts where site-specific templates are desired, use
`createFromRequest()`. Include the `typo3/email` site set in your
site configuration:

**config/sites/my-site/config.yaml**

```yaml
dependencies:
  - typo3/email

settings:
  email:
    templateRootPaths:
      100: 'EXT:my_sitepackage/Resources/Private/Templates/Email/'
    layoutRootPaths:
      100: 'EXT:my_sitepackage/Resources/Private/Layouts/Email/'
    format: 'html'
```

**EXT:my_extension/Classes/Service/MyFrontendEmailService.php**

```php
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Mail\MailerInterface;
use TYPO3\CMS\Core\Mail\TemplatedEmailFactory;

final class MyFrontendEmailService
{
    public function __construct(
        private readonly TemplatedEmailFactory $templatedEmailFactory,
        private readonly MailerInterface $mailer,
    ) {}

    public function sendEmail(ServerRequestInterface $request): void
    {
        // Uses site-specific template paths if configured
        $email = $this->templatedEmailFactory->createFromRequest($request);
        $email
            ->setTemplate('MyTemplate')
            ->to('recipient@example.com')
            ->from('sender@example.com')
            ->subject('My Subject')
            ->assign('name', 'World');

        $this->mailer->send($email);
    }
}
```

### Backend and CLI usage {#backend-and-cli-usage}

For backend contexts where no site-specific templates are needed, use
`create()`:

**EXT:my_extension/Classes/Service/MyBackendEmailService.php**

```php
use Psr\Http\Message\ServerRequestInterface;
use TYPO3\CMS\Core\Mail\MailerInterface;
use TYPO3\CMS\Core\Mail\TemplatedEmailFactory;

final class MyBackendEmailService
{
    public function __construct(
        private readonly TemplatedEmailFactory $templatedEmailFactory,
        private readonly MailerInterface $mailer,
    ) {}

    public function sendNotification(
        ?ServerRequestInterface $request = null,
    ): void {
        // Uses only global $GLOBALS['TYPO3_CONF_VARS']['MAIL'] configuration
        $email = $this->templatedEmailFactory->create($request);
        $email
            ->setTemplate('SystemNotification')
            ->to('admin@example.com')
            ->from('system@example.com')
            ->subject('System Notification');

        $this->mailer->send($email);
    }
}
```

### Custom template path overrides {#custom-template-path-overrides}

For extensions that need their own email templates merged with the global
configuration, use `createWithOverrides()`:

**EXT:my_extension/Classes/Task/MySchedulerTask.php**

```php
use TYPO3\CMS\Core\Mail\MailerInterface;
use TYPO3\CMS\Core\Mail\TemplatedEmailFactory;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Scheduler\Task\AbstractTask;

final class MySchedulerTask extends AbstractTask
{
    public function sendReport(): void
    {
        // This example shows how to use this when constructor-based
        // dependency injection is not possible, as in AbstractTask
        // (EXT:scheduler). Always use dependency injection where possible.
        $mailer = GeneralUtility::makeInstance(MailerInterface::class);
        $templatedEmailFactory = GeneralUtility::makeInstance(
            TemplatedEmailFactory::class
        );

        // Merge extension-specific paths with the global configuration.
        // Note that if you do not pass a `$request` argument here, no site
        // context is evaluated. You may want to check
        // `$GLOBALS['TYPO3_REQUEST']` if you need this fallback, or use a
        // custom request object.
        $email = $templatedEmailFactory->createWithOverrides(
            templateRootPaths: [
                20 => 'EXT:my_extension/Resources/Private/Templates/Email/',
            ],
            layoutRootPaths: [
                20 => 'EXT:my_extension/Resources/Private/Layouts/',
            ],
        );
        $email
            ->setTemplate('Report')
            ->to('admin@example.com')
            ->from('system@example.com')
            ->subject('Scheduled Report');

        $mailer->send($email);
    }
}
```

> [!TIP]
> **Hint**
>
> You can use the
> `BeforeMailerSentMessageEvent`
> to change a `FluidEmail` object before it is sent. You can
> assign Fluid variables or modify parts of the email, such as setting
> `From` or the email subject prefix, depending on your site:
>
> **EXT:my_extension/Classes/Listener/MyMailerListener.php**
>
> ```php
> <?php
> declare(strict_types=1);
>
> use TYPO3\CMS\Core\Attribute\AsEventListener;
> use TYPO3\CMS\Core\Mail\Event\BeforeMailerSentMessageEvent;
>
> final class MyMailerListener
> {
>     #[AsEventListener('my-extension/mymailerlistener')]
>     public function __invoke(BeforeMailerSentMessageEvent $event): void
>     {
>         $message = $event->getMessage();
>         $message->from('customized@example.com');
>         $message->assign('mySpecialVariable', 'mySpecialContent');
>         $event->setMessage($message);
>     }
> }
> ```

## Core migrations {#core-migrations}

All core extensions that send emails have been migrated to use
`TemplatedEmailFactory`. This includes:

-   **EXT:form** Email finishers now use `createWithOverrides()` with the
    request from the form runtime, so site-specific email settings are applied
    automatically.
-   **EXT:felogin** Password recovery emails now use
    `createWithOverrides()`, making them site-aware. The method
    `RecoveryConfiguration::getMailTemplatePaths()` has been removed, as
    template path resolution is now handled by the factory.
-   **EXT:backend** Login notifications, failed login and MFA attempt
    notifications, and password reset emails use `create()`.
-   **EXT:install** Test email sending uses `create()`.
-   **EXT:workspaces** Stage change notifications use
    `createWithOverrides()`.
-   **EXT:linkvalidator** Broken link report emails use
    `createWithOverrides()`.
-   **EXT:reports** System status emails use `create()`.

## Impact {#impact}

Extensions that send emails are encouraged to use the
`TemplatedEmailFactory` to create `FluidEmail` instances instead of
instantiating them directly. When a request with a site attribute is passed,
template paths and format from the `typo3/email` site set are applied.
The merge priority, with the highest priority winning, is:

1.  Global `$GLOBALS['TYPO3_CONF_VARS']['MAIL']` paths as the base
1.  Site settings from `typo3/email`, when a site-based request is
    available and site settings are applied
1.  Caller-provided override paths when using
    `createWithOverrides()`
