Architecture
The extension is small — two PHP classes — but the way configuration reaches the transport is subtle, and there are two parallel frontend configuration paths. This page documents both.
Components
| Class | Responsibility |
|---|---|
\Mail\ | Symfony
Abstract implementation. Resolves configuration,
authenticates, converts the message and calls Microsoft Graph. Keeps one
Graph client per credential set — see Graph client, timeouts and retries. |
\Lowlevel\ | Blinds the tenant ID, client ID and client secret in the backend
Configuration module, so the values render as ab******yz.
Covers both $GLOBALS (provider
conf) and the same settings in a site's configuration (provider
sites, nested or dotted keys). |
Message conversion itself lives in
oliverkroener/ok-typo3-helper
, whose
MSGraph turns the Symfony
Sent into a Graph message. Bugs in attachment, inline-image or
recipient handling usually belong in that package rather than here.
How the transport is selected
There is no DSN factory. TYPO3 activates this transport when
$GLOBALS is the
fully-qualified class name:
OliverKroener\OkExchange365\Mail\Transport\Exchange365Transport
TYPO3's mailer then instantiates the class directly, passing the whole
$GLOBALS array as the $mail
constructor argument.
Important
Because TYPO3 constructs the class itself, it must not be handled by the
Symfony DI container. It is therefore excluded from the autoloading resource in
Configuration/. Do not add it back to autowiring.
The
__ return value exchange365api is only a display name; it
plays no part in transport selection.
Configuration resolution
get builds the effective configuration from two sources:
- Mail settings are the baseline. The
$GLOBALSvalues, normalised into short keys (['TYPO3_ CONF_ VARS'] ['MAIL'] ['transport_ exchange365_*'] tenant,Id client, …) byId get. This is the only source available in backend, CLI and scheduler contexts.Mail Settings Configuration () - Frontend TypoScript overlays it per key. The values below
plugin., read only whentx_ okexchange365mailer. settings. exchange365 $GLOBALSreports['TYPO3_ REQUEST'] application. Site-set settings arrive through this same path, because site settings are flattened into TypoScript constants.Type === 1
Two rules govern the overlay, and both are deliberate:
if ($key === 'saveToSentItems' || ($value !== '' && $value !== null)) {
$conf[$key] = $value;
}
- An empty TypoScript value means "not configured here" and must not shadow
a value that is set in the mail settings. This is why the code uses an explicit
guard rather than a plain
??or an array merge. saveis exempt from that guard. A site setting ofTo Sent Items falseflattens to an empty constant, and there it genuinely means false.
get reads the frontend. request
attribute unconditionally. On TYPO3 12.4.0 — the one supported version that predates
that attribute — it is absent, the method returns
null
, and the mail-settings
baseline stands.
validate hard-requires tenant, client and
client. Every other setting has a fallback.
Sender resolution
graph — the mailbox in the /users/ path — is
deliberately decoupled from the message From header, so that Send As and
Send On Behalf work. It resolves in this order:
conf.graphSenderUserId
→ $graphMessage['from']
→ conf.fromEmail
→ MAIL.defaultMailFromAddress
→ RuntimeException
resolve walks these candidates and returns the first one
that is a non-empty scalar. An empty string counts as "unset" at every step —
including an empty message From — because
get returns empty strings, not nulls, for unset
values.
Note
The sender display name comes from the mailbox in Exchange Online, not from
TYPO3.
default has no effect on what recipients see.
See Configuring the Sender Display Name.
Two mutually exclusive frontend paths
| Path | TYPO3 | Files |
|---|---|---|
Site set oliverkroener/ (preferred, since 4.3.0) | 13 / 14 | Configuration/,
settings., setup. |
| Static template [kroener.DIGITAL] Exchange 365 Mailer | 12 | Configuration/,
setup., registered in
Configuration/ |
The set's setup. is a one-line @import of the static template's
setup.. The site-setting keys are named identically to the
TypoScript constants, so the mapping file is reused verbatim.
Warning
Use the set or the static template, never both. Site settings are applied to constants before template records, so a lingering static template overwrites the set's values with its own empty defaults.
When adding or renaming a setting, change all four places:
constants., setup.,
settings., and
get.
Graph client, timeouts and retries
get keeps one
Graph per
credential set (keyed by a hash of tenant ID, client ID and client secret). The
client holds its OAuth token in memory, so a request that sends several mails — a
form with a receiver and a confirmation mail, a scheduler run — authenticates once.
Different credentials, for example a frontend site with its own app registration,
get their own client.
create builds the client the same way the SDK does by
default, with two deliberate differences:
- The Graph call uses a 10 s connect and 30 s total timeout instead of the SDK's 30 s / 100 s, so a stalled connection cannot hold a frontend request for minutes.
- The OAuth token request gets the same timeouts through an injected HTTP client. The OAuth library the SDK uses otherwise sends it with no timeout at all — a stalled token request would block a CLI or scheduler run forever.
Throttling (429) and 503/504 responses are retried by the SDK's own middleware,
honouring Retry-. A request that stalled after connecting is not
retried: Graph may already have accepted the message, and a retry could send it
twice.
create is
protected
and is the one seam the unit
tests use to count or replace client creation.
Error handling
do catches every
\Throwable
, logs at error level, and
rethrows a Symfony
Transport with the original as previous.
Transport extends
\Runtime, so existing
catch blocks keep working. Graph's original message is
appended to the exception text — it is the only diagnostic an integrator gets, so
keep it when editing. The client secret and the token never appear in it.