История коммитов

.
docs: bump the documentation submodule
.
fix(mail): retry a failed delivery instead of losing the message
A transport error stamped the message as sent, so it was never tried again and nobody could tell it had not arrived. Delivery is now tracked: a message that could not be sent keeps its place and is tried again after a configurable delay, and once the attempts run out it is recorded as failed with its error - never as sent. A message that cannot be built at all (no recipient, an address no server would accept, a template that does not render) is given up on at once, because every future run would fail the same way, and the rest of the batch still goes out.

A batch is now claimed before it is read, so two workers - the cron of a busy site firing again before the previous run finished - cannot pick the same message, and a claim nobody released within the timeout goes back into circulation. The claim needs no row locks, so it works on the older MySQL versions as well.

EmailSender is a service with its dependencies injected rather than a static method reaching into the container, and reports what the run did. The queue lives behind EmailQueueInterface. The table is defined once in MailSchema, shared by the installer, the new mail:upgrade-schema command and the tests; mail:cleanup removes delivered messages, never the failed ones.

Also fixes MailRenderer dying with a TypeError when no translator was registered yet, which is what a process rendering a message outside the usual bootstrap runs into.
.
feat(mail): configure the mailer through a dsn
The mail settings are now compiled into a Symfony Mailer DSN by MailDsnResolver, and a site can write that DSN directly instead: every transport the mailer supports becomes reachable, including the provider bridges and the failover/roundrobin schemes. The structured transport + options form keeps working and is compiled down to the same string.

Fixes of the previous builder: the encryption setting was passed as the Swiftmailer options encryption/auth_mode, which Symfony ignores, so port 465 could never connect - the scheme is now smtps where implicit TLS is meant; a username without a password was dropped entirely; verify_peer, auto_tls, require_tls and local_domain are supported; sendmail without a command falls back to sendmail_path of php.ini; the native and null transports were added.

The transport also receives the logger and the http client, the latter being what the API transports of the provider bridges send through.
.
docs: bump the documentation submodule
Points at docs(security): describe the html sanitizer.
.
feat(security): let a module declare its own html policy
A policy was an enum case, so the only way for a module to have one was to patch
the core and lose the patch on the next update. A module now registers a service
implementing HtmlPolicyProviderInterface — PSRContainerFactory tags it, the same
way it does for voters and permission providers — and asks for the policy by
name:

$this->sanitizer->sanitize($text, 'my-module.signature');

HtmlPolicyDefinition describes what the content may carry without naming a
library: elements with their attributes, allowed classes, linkify, link schemes,
frame targets. The shape maps onto symfony/html-sanitizer as directly as onto
HTMLPurifier, so it does not tie the abstraction to the current library.

It is an allow list and cannot be widened into something unsafe: elements that
carry behaviour, attributes starting with 'on' and executable schemes are
refused where they are declared, with an exception that names the mistake.
Asking for an undeclared policy throws instead of falling back, and the built-in
policies stay reachable only through the enum, so a module cannot claim the name
the whole site is cleaned by.
.
docs(agents): describe the html sanitizer and its policies
escaping.md gains a section on sanitizing rich content: which interface to take,
what each policy is for, that toPlainText sanitizes before stripping, and that a
URL is validated by scheme rather than run through a markup sanitizer.

The security checklist checks the policy fits the content and flags a caller
that builds a sanitizer of its own.
.
refactor(security): drop the raw purifier from the container
Nothing asks for \HTMLPurifier any more, so the service, its alias and the
Johncms\Security\HTMLPurifier factory are gone. The library is now reachable
only through HtmlPurifierFactory, behind HtmlSanitizerInterface.
.
fix(users): build the profile website link from a validated address
The field holds an address, not markup, but it was run through the HTML
sanitizer and printed as whatever came back; a link appeared only because the
rich-content policy linkifies bare URLs.

It is now built explicitly: http(s) becomes a link with rel="nofollow noopener",
anything else is printed as escaped text. An allow list of schemes is what keeps
javascript: out of an href, which a markup sanitizer was never the tool for.
.
refactor(consent): sanitize the title and the banner by policy
Both formatters built an HTMLPurifier config of their own with an HTML.Allowed
string; the same allowances now live in HtmlPolicy::Inline and
HtmlPolicy::InlineWithParagraphs, and the formatters only choose between them.

toPlainText of a title now sanitizes before stripping the tags, so the body of
an element that was never allowed cannot reach a breadcrumb as text; it also
collapses runs of whitespace, which a one-line title does not notice.

Adds the first tests of the consent module: which policy each formatter asks
for, and what survives it.
.
refactor(security): move the remaining purifier callers to the sanitizer
The models and the legacy classes that reach for the container by hand now ask
for HtmlSanitizerInterface: ForumMessage, DownloadFile, NewsArticle,
UserMutators, Comments, the news comments controller and ArticleTextRenderer.

News\Application\Utils\Helpers::purifyHtml is gone; NewsArticle sanitizes on
its own and no longer reaches from Domain into the Application layer for it.