Events 

EXT:mail dispatches PSR-14 events at the places where an integrator may want to change its behavior. All events are final classes in the namespace MEDIAESSENZ\Mail\Events.

Event Dispatched
AdditionalMailHeadersEvent Before each single mail is sent. Adds or changes mail headers.
DeactivateRecipientsEvent When returned recipients are deactivated in the report module.
ManipulateCsvImportDataEvent Before the records of a csv import are written to the database.
ManipulateImportFolderEvent When the folder for the uploaded csv files is determined.
ManipulateImportStorageFolderEvent When the folders are collected, which can store imported records.
ManipulateMailRecipientsEvent When the recipients of a mailing are determined.
ManipulateMarkersEvent When the markers of a recipient are replaced in the mail content.
ManipulateRecipientEvent Whenever the data of a recipient is loaded.
RecipientBouncedEvent After a returned mail was analysed and stored by the bounce mail command.
RecipientEmailCorrectedEvent After the address of a failed delivery was corrected in the queue module.
RecipientsRestrictionEvent When the query for recipients of an extbase model source is built.
ScheduledSendBegunEvent When the delivery of a mailing begins.
ScheduledSendFinishedEvent When the delivery of a mailing is finished.

Registering a listener 

A listener is a class with an __invoke() method, whose argument is the event. The simplest way to register it is the PHP attribute AsEventListener. This is also how the listeners of EXT:mail itself are registered.

EXT:my_extension/Classes/EventListener/MyListener.php
<?php

declare(strict_types=1);

namespace Vendor\ExtensionKey\EventListener;

use MEDIAESSENZ\Mail\Events\ManipulateMarkersEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'vendor/extensionkey/my-listener')]
final class MyListener
{
    public function __invoke(ManipulateMarkersEvent $event): void
    {
        // change the event here
    }
}
Copied!

Alternatively, register the listener in the Configuration/Services.yaml of your extension:

EXT:my_extension/Configuration/Services.yaml
Vendor\ExtensionKey\EventListener\MyListener:
  tags:
    - name: 'event.listener'
      identifier: 'vendor/extensionkey/my-listener'
Copied!

If several listeners change the same event, you can set their order with the arguments before and after of the attribute (or the tag). The identifiers of the listeners of EXT:mail are the file names in kebab case with the prefix mediaessenz/mail-, for example mediaessenz/mail-normalize-recipient-data.

The recipient source configuration 

Several events carry a MEDIAESSENZ\Mail\Domain\Model\Dto\RecipientSourceConfigurationDTO. It describes the recipient source of a recipient, as it is configured in the recipient sources of your site. The most important public properties are:

identifier
The identifier of the source, for example tt_address or fe_users. Csv groups have the identifier tx_mail_domain_model_group:<uid of the group>.
title
The title of the source.
table and model
The database table, or the Extbase model class of the source.
ignoreMailActive
If checked, recipients are used even if they are not marked as active for mailings.
forceHtmlMail
If checked, HTML mails are sent to all recipients of the source, no matter what they chose.
custom
An array for own settings of the source.

The type of the source can be asked with the methods isTableSource(), isModelSource(), isCsv(), isPlain() and isCsvOrPlain(). Use them if your listener should only handle some of the sources.

AdditionalMailHeadersEvent 

Dispatched for every single mail, right before it is sent. It adds the possibility to add or change mail headers.

getHeaders()
The Symfony\Component\Mime\Header\Headers of the message. Change this object. The event has no setter.
getMail()
The mailing (MEDIAESSENZ\Mail\Domain\Model\Mail), which the message belongs to.
getTypo3MailId()
The value of the header X-TYPO3MID. It identifies the mailing, the recipient source and the recipient and is used by the bounce analysis.
getRecipientSourceIdentifier() and getRecipientUid()
The recipient source and the uid of the recipient, taken from the X-TYPO3MID. If they cannot be determined, the identifier is empty and the uid is 0. For csv and plain sources the uid is only a running number.
getOrganisation()
The organisation, which is sent in the header Organization.
getSiteIdentifier()
The identifier of the site, whose mail settings are used.
Example: add the headers of a one-click unsubscribe (RFC 8058)
<?php

declare(strict_types=1);

namespace Vendor\ExtensionKey\EventListener;

use MEDIAESSENZ\Mail\Events\AdditionalMailHeadersEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'vendor/extensionkey/add-unsubscribe-headers')]
final class AddUnsubscribeHeaders
{
    public function __invoke(AdditionalMailHeadersEvent $event): void
    {
        if ($event->getRecipientUid() === 0) {
            return;
        }

        // This address has to be handled by your own code. It must accept a POST request.
        $url = sprintf(
            'https://example.com/unsubscribe?mail=%d&source=%s&recipient=%d',
            $event->getMail()->getUid(),
            rawurlencode($event->getRecipientSourceIdentifier()),
            $event->getRecipientUid()
        );

        $headers = $event->getHeaders();
        $headers->addTextHeader('List-Unsubscribe', '<' . $url . '>');
        $headers->addTextHeader('List-Unsubscribe-Post', 'List-Unsubscribe=One-Click');
    }
}
Copied!

DeactivateRecipientsEvent 

Dispatched when a user deactivates the recipients of a mailing, whose mails were returned, in the report module.

EXT:mail ships listeners for the sources tt_address, fe_users and for csv and plain lists, see MEDIAESSENZ\Mail\EventListener\DeactivateAddresses, MEDIAESSENZ\Mail\EventListener\DeactivateFeUsers and MEDIAESSENZ\Mail\EventListener\InvalidateCsvRecipients. To deactivate the recipients of any other source, it is necessary to register a listener for it.

getData()
The returned recipients, grouped by the identifier of their recipient source. For each source the array contains recipients (for table and model sources the records of the recipients, including uid and email, for csv and plain sources the email addresses), returnCodes (the values of MEDIAESSENZ\Mail\Type\Enumeration\BounceReason, which were used to select them, empty for all) and configuration (the recipient source configuration).
getRecipientSources()
All recipient source configurations of the site, with their identifier as key.
getNumberOfAffectedRecipients() and setNumberOfAffectedRecipients()
The number of recipients, which were deactivated so far. It is shown to the user. Every listener adds the number of the recipients it deactivated.
Example: deactivate the recipients of an own table
<?php

declare(strict_types=1);

namespace Vendor\ExtensionKey\EventListener;

use MEDIAESSENZ\Mail\Events\DeactivateRecipientsEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Database\ConnectionPool;

#[AsEventListener(identifier: 'vendor/extensionkey/deactivate-subscribers')]
final class DeactivateSubscribers
{
    private const RECIPIENT_SOURCE = 'tx_myextension_subscriber';

    public function __construct(private readonly ConnectionPool $connectionPool)
    {
    }

    public function __invoke(DeactivateRecipientsEvent $event): void
    {
        $recipients = $event->getData()[self::RECIPIENT_SOURCE]['recipients'] ?? [];
        $connection = $this->connectionPool->getConnectionForTable(self::RECIPIENT_SOURCE);

        $affected = 0;
        foreach ($recipients as $recipient) {
            $affected += $connection->update(self::RECIPIENT_SOURCE, ['mail_active' => 0], ['uid' => (int)$recipient['uid']]);
        }

        $event->setNumberOfAffectedRecipients($event->getNumberOfAffectedRecipients() + $affected);
    }
}
Copied!

ManipulateCsvImportDataEvent 

Dispatched during the csv import, right before the records are written to the database with the DataHandler. It adds the possibility to change the data of the imported records. This needs tt_address to be installed.

getData() and setData()
The data in the format of a DataHandler datamap: [table => ['NEW1' => [field => value, ...], ...]]. The table is usually tt_address.
getConfiguration()
The configuration of the import, which consists of the page TSconfig settings of the import and the values chosen in the import wizard. It cannot be changed.
Example: add a note to every imported address
<?php

declare(strict_types=1);

namespace Vendor\ExtensionKey\EventListener;

use MEDIAESSENZ\Mail\Events\ManipulateCsvImportDataEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'vendor/extensionkey/mark-imported-addresses')]
final class MarkImportedAddresses
{
    public function __invoke(ManipulateCsvImportDataEvent $event): void
    {
        $data = $event->getData();

        foreach ($data['tt_address'] ?? [] as $id => $record) {
            $data['tt_address'][$id]['description'] = 'Imported on ' . date('Y-m-d');
        }

        $event->setData($data);
    }
}
Copied!

ManipulateImportFolderEvent 

Dispatched when the folder for the uploaded csv files is determined. By default this is the default upload folder of the backend user, or the first folder of one of their file mounts. The csv files are stored in a sub folder importexport of the folder, which is set here.

getImportFolder() and setImportFolder()
The folder (TYPO3\CMS\Core\Resource\Folder) or null, if the backend user has no folder.
Example: store the csv files in an own folder
<?php

declare(strict_types=1);

namespace Vendor\ExtensionKey\EventListener;

use MEDIAESSENZ\Mail\Events\ManipulateImportFolderEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\Resource\Exception\FolderDoesNotExistException;
use TYPO3\CMS\Core\Resource\ResourceFactory;

#[AsEventListener(identifier: 'vendor/extensionkey/use-newsletter-import-folder')]
final class UseNewsletterImportFolder
{
    public function __construct(private readonly ResourceFactory $resourceFactory)
    {
    }

    public function __invoke(ManipulateImportFolderEvent $event): void
    {
        try {
            // the folder "newsletter" of the storage with the uid 1, which is usually fileadmin
            $event->setImportFolder($this->resourceFactory->getFolderObjectFromCombinedIdentifier('1:/newsletter/'));
        } catch (FolderDoesNotExistException) {
            // keep the folder of the user
        }
    }
}
Copied!

ManipulateImportStorageFolderEvent 

Dispatched when the folders are collected, which the user can choose in the csv import to store the imported records. These are the folders (pages of the type folder) the backend user is allowed to edit.

getSubFolders() and setSubFolders()
The pages of these folders as an array of records. Each record contains at least the uid and the title.
Example: only offer folders whose title starts with "Newsletter"
<?php

declare(strict_types=1);

namespace Vendor\ExtensionKey\EventListener;

use MEDIAESSENZ\Mail\Events\ManipulateImportStorageFolderEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'vendor/extensionkey/limit-import-storage-folders')]
final class LimitImportStorageFolders
{
    public function __invoke(ManipulateImportStorageFolderEvent $event): void
    {
        $event->setSubFolders(array_values(array_filter(
            $event->getSubFolders(),
            static fn (array $folder): bool => str_starts_with((string)$folder['title'], 'Newsletter')
        )));
    }
}
Copied!

ManipulateMailRecipientsEvent 

Dispatched when the recipients of a mailing are determined. This happens when the mailing is finished in the mail wizard and once more when the delivery starts, because the recipients are calculated again then. Only if the extension setting specifyFinalRecipientsInWizard is checked, the list from the wizard is kept. It adds the possibility to change the recipients list.

getMail()
The mailing (MEDIAESSENZ\Mail\Domain\Model\Mail).
getRecipients() and setRecipients()
The recipients grouped by the identifier of their recipient source. For table and model sources the list contains the uids of the records, for csv and plain sources the recipients as arrays (email, name and the other fields).
Example: never send to some addresses of tt_address
<?php

declare(strict_types=1);

namespace Vendor\ExtensionKey\EventListener;

use MEDIAESSENZ\Mail\Events\ManipulateMailRecipientsEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'vendor/extensionkey/remove-blocked-addresses')]
final class RemoveBlockedAddresses
{
    // the uids of the addresses, which must not get any newsletter
    private const BLOCKED_UIDS = [17, 42];

    public function __invoke(ManipulateMailRecipientsEvent $event): void
    {
        $recipients = $event->getRecipients();

        if (isset($recipients['tt_address'])) {
            $recipients['tt_address'] = array_values(array_diff($recipients['tt_address'], self::BLOCKED_UIDS));
        }

        $event->setRecipients($recipients);
    }
}
Copied!

ManipulateMarkersEvent 

Dispatched whenever the markers (placeholders) of a recipient are replaced in the content of a mail. As the HTML and the plain text part are replaced separately, the event is dispatched twice for each recipient. It adds the possibility to add markers, for example a salutation, or to change their values.

getMarkers() and setMarkers()
The markers as an array. The keys are the markers including the hashes, for example ###USER_name###, the values are the texts they are replaced with.
getRecipient()
The data of the recipient.
getRecipientSourceConfiguration()
The recipient source configuration of the recipient.

EXT:mail uses this event itself to add the markers in upper case, see MEDIAESSENZ\Mail\EventListener\AddUpperCaseMarkers.

Example: add the marker ###USER_salutation### depending on the gender of tt_address
<?php

declare(strict_types=1);

namespace Vendor\ExtensionKey\EventListener;

use MEDIAESSENZ\Mail\Events\ManipulateMarkersEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'vendor/extensionkey/add-salutation-marker')]
final class AddSalutationMarker
{
    public function __invoke(ManipulateMarkersEvent $event): void
    {
        if ($event->getRecipientSourceConfiguration()->identifier !== 'tt_address') {
            return;
        }

        $markers = $event->getMarkers();
        $markers['###USER_salutation###'] = match ($event->getRecipient()['gender'] ?? '') {
            'f' => 'Dear Ms.',
            'm' => 'Dear Mr.',
            default => 'Dear',
        };

        $event->setMarkers($markers);
    }
}
Copied!

Use the new marker in the mail content like the other ones, for example ###USER_salutation### ###USER_name###,.

ManipulateRecipientEvent 

Dispatched whenever the data of a recipient is loaded: for the recipient lists in the backend, right before a mail is sent to the recipient and when a tracked link is resolved. This way all of them work with the same data. It adds the possibility to change or complete the data of a recipient.

getRecipientData() and setRecipientData()
The data of the recipient as an array, for example uid, email and name.
getRecipientSourceIdentifier()
The identifier of the recipient source, for example tt_address.
getRecipientSourceConfiguration()
The recipient source configuration of the recipient.

EXT:mail uses this event itself to compose the name from the first, middle and last name (MEDIAESSENZ\Mail\EventListener\NormalizeRecipientData) and to add the phone number of frontend users (MEDIAESSENZ\Mail\EventListener\ManipulateFrontendUserRecipient).

Example: use a fallback if an address has no name
<?php

declare(strict_types=1);

namespace Vendor\ExtensionKey\EventListener;

use MEDIAESSENZ\Mail\Events\ManipulateRecipientEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(
    identifier: 'vendor/extensionkey/name-fallback',
    after: 'mediaessenz/mail-normalize-recipient-data'
)]
final class NameFallback
{
    public function __invoke(ManipulateRecipientEvent $event): void
    {
        if ($event->getRecipientSourceIdentifier() !== 'tt_address') {
            return;
        }

        $recipientData = $event->getRecipientData();
        if (trim((string)($recipientData['name'] ?? '')) === '') {
            $recipientData['name'] = 'customer';
            $event->setRecipientData($recipientData);
        }
    }
}
Copied!

RecipientBouncedEvent 

Is dispatched by the command mail:analyze-bounce-mail after a returned mail was analysed and stored in the log of the mailing. It is not dispatched for auto replies, which are ignored, or for messages that cannot be assigned to a mailing.

EXT:mail does not change the recipient record (tt_address, fe_users, a csv list and so on). Use this event, if a recipient should be deactivated right after a permanent problem, without waiting for the manual action in the report module.

The event provides:

getType()
BounceType::HARD for a permanent problem (status 5.x.x), BounceType::SOFT for a temporary one (status 4.x.x, for example greylisting or a mailbox that is temporarily full).
getReason()
A BounceReason: RECIPIENT_UNKNOWN, MAILBOX_FULL, HOST_UNREACHABLE, POLICY_BLOCKED (rejected as spam or by a policy, the recipient itself is fine), MESSAGE_ERROR or UNKNOWN.
getSmtpCode() and getDsnStatus()
The SMTP reply code (e.g. 550) and the enhanced status code (e.g. 5.1.1), which the receiving server reported. They are 0 or empty, if the returned mail contains none.
getReasonText()
The explanation of the receiving server.
getRecipientSourceIdentifier(), getRecipientUid(), getEmail() and getMailUid()
The recipient, to whom the mail was sent, and the mailing. For sources without records, like a csv or plain list, the uid is only a running number of the list. A listener should ignore such sources.
Example: deactivate a tt_address record after a permanent bounce
<?php

declare(strict_types=1);

namespace Vendor\ExtensionKey\EventListener;

use MEDIAESSENZ\Mail\Events\RecipientBouncedEvent;
use MEDIAESSENZ\Mail\Type\Enumeration\BounceReason;
use MEDIAESSENZ\Mail\Type\Enumeration\BounceType;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

#[AsEventListener(identifier: 'vendor/extensionkey/deactivate-bounced-address')]
final class DeactivateBouncedAddress
{
    public function __invoke(RecipientBouncedEvent $event): void
    {
        if ($event->getRecipientSourceIdentifier() !== 'tt_address'
            || $event->getType() !== BounceType::HARD
            || $event->getReason() !== BounceReason::RECIPIENT_UNKNOWN
        ) {
            return;
        }

        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
        $dataHandler->start(['tt_address' => [$event->getRecipientUid() => ['hidden' => 1]]], []);
        $dataHandler->process_datamap();
    }
}
Copied!

RecipientEmailCorrectedEvent 

Is dispatched after the email address of a failed or waiting delivery was corrected in the queue module. The correction has to be enabled with the extension setting enableEmailCorrection, which is off by default. Without it, no address can be changed and the event is never dispatched.

The correction only applies to this delivery. EXT:mail does not change the recipient record (tt_address, fe_users, a csv list and so on). Use this event, if the corrected address should be applied to the recipient record as well.

The event is only dispatched, if the address was really changed. It is dispatched after the queue entry was saved, so an exception in a listener cannot undo the correction.

The event provides:

getRecipientSourceIdentifier()
The identifier of the recipient source, e.g. tt_address or fe_users, like in the recipient sources configuration of your site.
getRecipientUid()
The uid of the record in this source. For sources without records, like a csv or plain list, this is only a running number of the list. A listener should ignore such sources.
getOldEmail() and getNewEmail()
The address before and after the correction.
getMailUid(), getQueueItemUid() and getRecipientData()
The mail, the queue entry and the recipient data of the queue entry, which already contains the new address.
Example: apply the corrected address to a tt_address record
<?php

declare(strict_types=1);

namespace Vendor\ExtensionKey\EventListener;

use MEDIAESSENZ\Mail\Events\RecipientEmailCorrectedEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;
use TYPO3\CMS\Core\DataHandling\DataHandler;
use TYPO3\CMS\Core\Utility\GeneralUtility;

#[AsEventListener(identifier: 'vendor/extensionkey/apply-corrected-email-to-address')]
final class ApplyCorrectedEmailToAddress
{
    public function __invoke(RecipientEmailCorrectedEvent $event): void
    {
        if ($event->getRecipientSourceIdentifier() !== 'tt_address') {
            return;
        }

        $dataHandler = GeneralUtility::makeInstance(DataHandler::class);
        $dataHandler->start(
            ['tt_address' => [$event->getRecipientUid() => ['email' => $event->getNewEmail()]]],
            []
        );
        $dataHandler->process_datamap();
    }
}
Copied!

RecipientsRestrictionEvent 

Dispatched when the query for the recipients of an Extbase model source is built. It adds the possibility to restrict the recipients with further constraints. EXT:mail has already added the constraints for active recipients, a not empty email address and the selected categories.

getRecipientSourceConfiguration()
The recipient source configuration of the source.
getQuery()
The TYPO3\CMS\Extbase\Persistence\QueryInterface, which is used to create new constraints.
getConstraints() and setConstraints()
The constraints, which are combined with a logical and. Add your constraints to this list.
Example: exclude all addresses of a domain
<?php

declare(strict_types=1);

namespace Vendor\ExtensionKey\EventListener;

use MEDIAESSENZ\Mail\Events\RecipientsRestrictionEvent;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'vendor/extensionkey/exclude-domain')]
final class ExcludeDomain
{
    public function __invoke(RecipientsRestrictionEvent $event): void
    {
        if ($event->getRecipientSourceConfiguration()->identifier !== 'tt_address') {
            return;
        }

        $query = $event->getQuery();
        $constraints = $event->getConstraints();
        $constraints[] = $query->logicalNot($query->like('email', '%@example.invalid'));

        $event->setConstraints($constraints);
    }
}
Copied!

ScheduledSendBegunEvent 

Dispatched once for each mailing, when its delivery begins. This is before the start is stored in the mailing and before the recipients are calculated, so the number of recipients is not final yet.

getMail() and setMail()
The mailing (MEDIAESSENZ\Mail\Domain\Model\Mail).
Example: write a log entry when a mailing begins
<?php

declare(strict_types=1);

namespace Vendor\ExtensionKey\EventListener;

use MEDIAESSENZ\Mail\Events\ScheduledSendBegunEvent;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'vendor/extensionkey/log-send-begun')]
final class LogSendBegun
{
    public function __construct(private readonly LoggerInterface $logger)
    {
    }

    public function __invoke(ScheduledSendBegunEvent $event): void
    {
        $mail = $event->getMail();
        $this->logger->info('Mailing {uid} "{subject}" begins to send', [
            'uid' => $mail->getUid(),
            'subject' => $mail->getSubject(),
        ]);
    }
}
Copied!

ScheduledSendFinishedEvent 

Dispatched once for each mailing, when its delivery is finished, either because all recipients were handled or because the mailing has no recipients. This is before the end is stored in the mailing.

getMail() and setMail()
The mailing (MEDIAESSENZ\Mail\Domain\Model\Mail).
Example: write a log entry when a mailing is finished
<?php

declare(strict_types=1);

namespace Vendor\ExtensionKey\EventListener;

use MEDIAESSENZ\Mail\Events\ScheduledSendFinishedEvent;
use Psr\Log\LoggerInterface;
use TYPO3\CMS\Core\Attribute\AsEventListener;

#[AsEventListener(identifier: 'vendor/extensionkey/log-send-finished')]
final class LogSendFinished
{
    public function __construct(private readonly LoggerInterface $logger)
    {
    }

    public function __invoke(ScheduledSendFinishedEvent $event): void
    {
        $mail = $event->getMail();
        $this->logger->info('Mailing {uid} "{subject}" finished: {handled} of {total} recipients handled', [
            'uid' => $mail->getUid(),
            'subject' => $mail->getSubject(),
            'handled' => $mail->getNumberOfRecipientsHandled(),
            'total' => $mail->getNumberOfRecipients(),
        ]);
    }
}
Copied!

Need more? 

Please contact me