Needs review
Project:
Drupal.org security advisory coverage applications
Component:
module
Priority:
Normal
Category:
Task
Assigned:
Unassigned
Reporter:
Created:
11 Oct 2026 at 23:18 UTC
Updated:
15 Oct 2026 at 23:07 UTC
Jump to comment: Most recent
Comments
Comment #2
jamestarletonComment #3
vishal.kadamComment #4
avpadernoThank you for applying!
Before giving links helpful to understand how the review process works, what to expect from a review, and what to do to avoid a review takes more time than needed, I would like to thank all the reviewers for the work they do.
These applications are volunters-driven, which also means it is not possible to predict when an application will be marked fixed and the applicant will get the permission to opt projects into security advisory policy. While we aim to make an application as quick as possible, it is also important for us that more people review the project used for an application. In this way, we make sure applications do not miss some important points that should be instead reported.
Applications are not meant to be complete debugging sessions that eliminate every existing bug, though. I apologize if sometimes applications seem to go into too-detailed reviews.
Please read Review process for security advisory coverage: What to expect for more details and Security advisory coverage application checklist to understand what reviewers look for. Tips for ensuring a smooth review gives some hints for a smoother review.
The important notes are the following.
Keep in mind that once the project is opted into security advisory coverage, only Security Team members may change coverage.
To the reviewers
Please read How to review security advisory coverage applications, Application workflow, What to cover in an application review, and Tools to use for reviews.
The important notes are the following.
For new reviewers, I would also suggest to first read In which way the issue queue for coverage applications is different from other project queues.
Comment #5
avpadernoComment #6
vishal.kadam1. FILE: html_mail.module, modules/html_mail_symfony/html_mail_symfony.module
For a new module that aims to be compatible with Drupal 10 and Drupal 11, I would rather implement hooks as class methods as described in Support for object oriented hook implementations using autowired services.
2. FILE: src/Form/SettingsForm.php
With Drupal 10 and Drupal 11, there is no longer need to use #default_value for each form element, when the parent class is ConfigFormBase: It is sufficient to use #config_target, as in the following code.
Using that code, it is no longer needed to save the configuration values in the form submission handler: The parent class will take care of that.
Comment #7
jamestarletonThank you for the review, @vishal.kadam. Both points are addressed in commit 94c36e7 on 1.0.x.
1. OOP hooks
Drupal\html_mail\Hook\HtmlMailHooks(help,mail,theme) andDrupal\html_mail_symfony\Hook\HtmlMailSymfonyHooks(mail_alter,mailer_init,form_html_mail_settings_alter). They use#[Hook]attributes and constructor-injected (autowired) services.^10.3 || ^11. So the.modulefiles keep thin procedural functions marked#[LegacyHook], which delegate to the hook classes. The hook classes are also registered as autowired services, for Drupal versions earlier than 11.1. This follows the backward-compatibility pattern in the change record https://www.drupal.org/node/3442349.2.
#config_targetin the settings formSettingsFormnow uses#config_target, with no#default_value. The customsubmitForm()is gone, andConfigFormBaseloads, validates and saves the values. The cleanup the old submit handler did (trimming, a site URL with no trailing slash, lowercase colors, an integer logo width) moved to statictoConfigcallbacks. They are static so that form caching keeps working.html_mail_symfonyadds through the form alter also uses#config_target, onhtml_mail_symfony.settings:modules. StaticfromConfig/toConfigcallbacks convert between the textarea and the sequence, so the extra submit handler is gone as well.html_mail.settingsneeded alangcode, because it contains translatable values. It was added to the install config, and a post-update function adds it on existing sites.PHPCS (Drupal, DrupalPractice) and PHPStan level 8 report no errors, and tests pass.
Comment #8
jamestarletonComment #9
avpadernoComment #10
shaker-gspikes commentedManual review of 1.0.x at 215bbc4 (14 October 2026). phpcs (Drupal, DrupalPractice) is clean locally and all commits are by the applicant. The changes from #6 are in: the hooks are methods of
HtmlMailHooks/HtmlMailSymfonyHookswith#[LegacyHook]wrappers, and the settings form uses#config_target.The output handling is careful: Twig autoescaping is never turned off, URL hints go through
UrlHelper::stripDangerousProtocols(), HTML hints throughXss::filter()with an allow-list, the footer setting throughXss::filterAdmin(), colours are validated by the form element and acolor_hexschema type, nothing is fetched from URLs or files, and the settings route uses arestrict accesspermission. The points below are about the mail contract and the submodules.1. Body lines are always treated as plain text (src/HtmlMailRenderer.php, paragraphs() and build()): every body line is cast to a string, split into paragraphs and passed through
Html::escape()for the HTML part, and the text part isMailFormatHelper::wrapMail()of the same strings, withouthtmlToText().MailInterface::format()documents that body lines can beMarkupInterface, and core'sPhpMail::format()runsMailFormatHelper::htmlToText()over the whole body. Two common cases break:- core's account emails:
user_mail()builds the body with$token_service->replace(), which HTML-escapes token values. With a site name such as "O'Brien Dental",[site:name]becomesO'Brien Dental; core's mail plugin decodes that, while this module shows the entity in the plain-text part and double-escapes it in the HTML part.useris in html_mail_symfony's default list of modules that always get the template, so this affects the default setup.- modules that send rendered markup (for example the contact module's message body) show literal tags in both parts.
Converting with
MailFormatHelper::htmlToText(implode("\n\n", $body))first, and building the HTML paragraphs from that text (or keepingMarkupInterfacelines as filtered markup), would match what core does.2. html_mail_ses needs amazon_ses 3.1 but does not say so (modules/html_mail_ses/html_mail_ses.info.yml):
HtmlMailSes::mail()uses$this->configFactory,$this->messageBuilderand$this->queueFactory, the promoted constructor properties of the 3.xAmazonSesplugin. Amazon SES 2.x, which also supports Drupal 10, keeps$this->configinstead, so every send fails with "Call to a member function get() on null". The^3.1constraint is only inrequire-devof composer.json, which is not applied when the project is installed. Useamazon_ses:amazon_ses (>=3.1)in the info file (and mention it in the README).3. Uninstalling html_mail_ses breaks outgoing mail (modules/html_mail_ses): the README tells site builders to run
drush cset system.mail interface.default html_mail_ses(or per module), but the submodule has no.installfile. After it is uninstalled,MailManager::getInstance()still asks for thehtml_mail_sesplugin and throwsPluginNotFoundExceptionfor every message routed to it, so password reset emails and the like end in an error. Ahook_uninstall()that resets thosesystem.mail:interfaceentries (toamazon_ses_mail, or removes them) would prevent it.4. html_mail_symfony takes its copy of the message too early (modules/html_mail_symfony/src/SymfonyMailerHtml.php, mailAlter()): the message array is stored by value when this module's
hook_mail_alter()runs, andpostRender()later replaces both bodies from that copy. Changes made byhook_mail_alter()implementations that run after it (same weight, module name sorting afterhtml_mail_symfony) are silently dropped. The stored entry is also never removed when a later alter cancels the send ($message['send'] = FALSE), so it can be picked up by a later message with the same id in a long-running cron or queue process. Running the alter last (hook_module_implements_alter(), ororder: Order::Laston Drupal 11.2+) and clearing the entry inmailerInit()would cover both.Minor:
- The
mail_styleTwig filter is declared'is_safe' => ['html']without'pre_escape' => 'html', so a theme override such as{{ notice|mail_style(...) }}prints the string unescaped. SECURITY.md documents the rule, but addingpre_escapeenforces it and keepsMarkupobjects unchanged.-
linkify()runs on already-escaped text, soSee <https://example.com/a>produceshref="https://example.com/a&gt"followed by a stray;.- The preheader is
strip_tags()of the escaped body, so apostrophes appear as'in the inbox preview and paragraphs are joined without a space.Status: Needs work (points 1 to 3).
Comment #11
jamestarletonThanks for the detailed review, @shaker-gspikes. Really appreciate it. All your findings were confirmed and valid, and each has been fixed in 1.0.x as of commit
210a98e.1. Body lines treated as plain text
Good catch, I didn't account for the token escaping. The body is now handled the same way core's
PhpMail::format()does it: the lines are joined and run throughMailFormatHelper::htmlToText(), and that is the plain-text part. The HTML part is built from that same text, so the two always match. "O'Brien Dental" now comes out right in both parts, and the contact module's rendered markup gets converted instead of showing raw tags. If a module wants real HTML, it can still use thebodyhint, which goes throughXss::filter(). Also have added a kernel test with an escaped token value and a Markup body line.2. html_mail_ses needs Amazon SES 3.1
The info file now says
amazon_ses:amazon_ses (>=3.1), and the README mentions it. I also did the same forhtml_mail_symfony, which now declaressymfony_mailer:symfony_mailer (>=1.5)to match what the README already said.3. Uninstalling html_mail_ses breaks mail
Added
html_mail_ses.installwith ahook_uninstall(). It points anysystem.mail:interfaceentries that usehtml_mail_sesback toamazon_ses_mail, so mail still goes out through SES. It skips this during a config sync, since the importedsystem.mailwins there anyway. Also have added a kernel test.4.
html_mail_symfonycopies the message too earlyThanks for the suggestion to move the hook to the end. I ended up with a slightly different fix, since
Order::Lastisn't available on D11.1, which the module still supports:Saved
$messageby reference -hook_mail_alter()now stores a reference to capture changes from all subsequent alter hooks.Moved the template check to post-render - the check for whether a message gets the template now runs in the post-render callback, alongside the build, so late changes (like
html_mailhints) are included regardless of hook execution order.Fixed leftover data from cancelled emails leaking into next send -
mailerInit()runs at the start of every send, before the alter hooks. It now also deletes any message still stored for that ID, so every send starts clean.Minor points
mail_stylenow has'pre_escape' => 'html'. Plain strings get escaped first, and Markup is left unescaped and just gets the inline styles. There's a kernel test for both cases.linkify()now finds URLs in the raw text and escapes afterwards, so<https://example.com/a>produceshttps://example.com/aas expected instead ofhttps://example.com/a&gtand without the stray;.PHPCS (Drupal, DrupalPractice) and PHPStan level 8 are clean, and all tests pass locally and in CI on both Drupal 10 and 11.
Setting back to Needs review.
Comment #12
jamestarleton