Provide full to/cc/bcc email configuration

Created on 6 April 2022, over 2 years ago
Updated 22 January 2024, 10 months ago

Problem/Motivation

This module has a fixed method of sending emails: all outgoing mail is via the 'bcc' header, except for the site email, which appears under 'to'. However, 'to' is empty if the "Disable the site email" configuration is checked. This is overly limiting in how site managers can format outgoing emails. Because the use of 'bcc' is not well documented and 'bcc' recipients are not visible in emails that are sent out, it can lead to confusion by users of the module. But perhaps the biggest problem is that the empty 'to' field when "Disable site email address" is checked can lead to mail being refused and dropped (not just flagged as spam).

There are examples of all these scenarios in the issue queue. Recently, I experienced email failures when our host changed email services to AWS SES, which invokes a "no-empty-to-field" rule, and it took a fair amount of debugging and collaboration with different departments and vendors to work out the source of the issue.

A number of issues have been created to address this problem in various ways. Each tackles the problem in a different way, and in the process, ends up setting limits and hard-coding other behaviors that site managers may not want (often presumably in the name of keeping changes and the patch simple). But if we are going to solve the problem in a robust way that meets most peoples' needs, we should consider a more comprehensive update that grants the ability to specify to/cc/bcc headers for all outgoing mail recipients that this module handles.

This issue proposes such a comprehensive solution. Working code is available for review and critique, but requires additional work such as tests.

Proposed resolution

We should give site managers the ability to decide how each of the configurable email addresses (author, site email, role based users, ad hoc) should be sorted into each of the available email address headers (to, cc, bcc). Each of these headers has a reason for being used, and it would improve the module to give people the ability to choose those that best meet their needs, even without the empty-to failure issue.

For each recipient configuration, this proposal recommends:

  • Author: convert current boolean yes/no that puts email in 'bcc' into a series of options that includes a no-email option: to, cc, bcc, none.
  • Site email: convert current negative-assertion boolean ("Disable the site email?" yes/no) hard-coded to the 'to' header into a series of options that includes a no-email option: to, cc, bcc, none.
  • Role emails: keep the existing role selection configuration, but add a "Role Header" configuration that allows the site manager to choose to send the role-based emails via to, cc, or bcc. (Configuring headers on a per-role basis would be possible, but considerably more complex, so out of scope for this project.)
  • Ad hoc emails: convert the single text field that currently places all recipients under the 'bcc' header into separate fields for each of the to, cc and bcc options, allowing a mix of headers to be used for ad hoc email addresses.

Completed tasks

These steps have been preliminarily completed, though all are open to review and comment.

  • Update config properties, adding new ones and deprecating old ones in schema and the ContentModerationNotification class annotation.
  • Update the ContentModerationNotification class properties and methods to accommodate the new configs.
  • Update the edit form (ContentModerationNotificationsFormBase) to handle the new properties and remove deprecated ones.
  • Update the ContentModerationNotificationsListBuilder controller to display the new properties.
  • Update Notification::sendNotification() to process the new configs and assign them to the proper email headers.
  • Create an update hook that converts existing notification configurations to use the new config properties in a way that retains the to/bcc recipient header lists.

Remaining tasks

  • Update tests: deprecated and removed config properties cause some tests to now fail. Tests should be updated to utilize the new properties.
  • New tests should be added to cover the new properties and logic.
  • Edit form: group the form elements related to setting recipients visually and provide overall help text. Provide guidance (importance of at least one 'to' recipient) and reduce redundancy in some field help text (such as info on tokens and Twig processing).
  • Edit form Role Headers field: make field appear conditionally only when at least one role is checked.

Related Issues

  • ✨ Option to put email addresses in To: instead of in Bcc: Needs work adds a new "To: instead of Bcc:" configuration parameter that solves the empty 'to' field problem by moving all 'bcc' emails to the 'to' field. In the process, it renders the "Disable site email address" field irrelevant if the new field is checked. No option is available to add recipients to 'cc' or 'bcc'.
  • #3208996: "Disable site mail" feature not compatible with PHP Mailer library β†’ is similar to the previous issue, but instead of being configurable, it hard-codes the move of all 'bcc' recipients to 'to'.
  • #2946360: Make the recipient configurable β†’ adds a "Recipient" configuration parameter that is explicitly for managing the 'to' field. Patch appears to be from before the "Disable the site email address" field was added. Instead, the 'to' field is set to this new Recipient property, if set, otherwise uses the site email address.
  • #3159790: Email the author not working β†’ moves the author email to the 'to' field (if send mail to author is checked), overriding/replacing the site email (if present) and removes the author email from the 'bcc' headers list. This behavior is enforced for all and not configurable.
  • 🌱 Support Twig, TO, CC, BCC, FROM, REPLY-TO, SUBJECT, Message, Abort, Debugging and Tips Active is an even more ambitious issue than this one that includes configurable to/cc/bcc as one of its goals. This approach appears to use complex Twig template logic to handle assignment of recipients into to/cc/bcc headers. Role-assigned emails remain hard-coded to 'bcc'.

The first four issues here would be resolved by this issue's approach, though in different (and more flexible) ways than each issue proposes. This issue's approach would conflict with the last meta-issue. If this issue's approach is implemented, the last issue above could perhaps be split into separate child issues to tackle the additional requests separately.

User interface changes

Config property changes require updates to the list of configured notifications (/admin/config/workflow/notifications) and the notification configuration edit form. (/admin/config/workflow/notifications/manage/{notification}). Preliminary work on this has been completed, but the edit form could use some additional polish.

API changes

This fork/proposal may impact current users of hook_content_moderation_notification_mail_data_alter due to changed email processing logic. Work has already been done to preserve this hook's original functionality. An update hook is also required to migrate existing configurations to the new configuration properties, and such a hook has been developed that is available for review and testing.

Data model changes

To avoid changing existing configuration property typing (e.g., boolean to string), this issue removes deprecated configuration properties and adds new ones that provide greater configuration possibilities. The schema and annotation have been updated for these changes, and the ContentModerationNotification class has been updated to accommodate the new properties and associated methods.

Incidental changes

  • Token/Twig handling is not consistent, but could be. Current stable (8.x-3.3) has token processing in the subject and message fields. Dev (8.x-3.x) recently added Twig template parsing to these fields as well as the ad hoc field #2953489: Add Twig support in Subject, Email, and Message fields β†’ . This means that the ad hoc field is inconsistent with subject and message. Two issues asking for this functionality in the ad hoc field have been closed on the presumption that Twig offers the same functionality, but responses indicate users still want basic tokens. #2952409: Tokens in AdHoc address field β†’ & #3088428: Support tokens in adhoc To email addresses β†’ This fork/project unifies token and Twig template processing across all text input fields (specifically adding token processing to the ad hoc fields).
  • This module's hook_content_moderation_notification_mail_data_alter is awkward and limited because the hook fires before logic is applied that sends all $data['to'] (recipients) to 'bcc', so you can't (easily) override this behavior. Some extra logic has been applied to this fork/project to try to preserve the hook's original behavior for those who have implemented it already. However, Drupal core's hook_mail_alter provides greater flexibility because it fires after the module's logic has completed, allowing one full control of any of the outgoing email's headers. Some language has been added to deprecate the old hook and to suggest hook_mail_alter() instead.
✨ Feature request
Status

Needs work

Version

3.0

Component

Code

Created by

πŸ‡ΊπŸ‡ΈUnited States daletrexel Minnesota, USA

Live updates comments and jobs are added and updated live.
  • Needs tests

    The change is currently missing an automated test that fails when run with the original code, and succeeds when the bug has been fixed.

Sign in to follow issues

Comments & Activities

Not all content is available!

It's likely this issue predates Contrib.social: some issue and comment data are missing.

  • πŸ‡¬πŸ‡§United Kingdom PQ

    Just a note that the current solution here seems to break when used with the sendgrid_integration module which validates the email addresses of the bcc and cc headers. If empty it throws an error.

    It might be resolved by changing this:

          $data['to'] = implode(',', $recipients['to']);
          $data['params']['headers']['Cc'] = implode(',', $recipients['cc']);
          $data['params']['headers']['Bcc'] = implode(',', $recipients['bcc']);
    

    (in src/Notification.php line ~213) to something like:

          $data['to'] = implode(',', $recipients['to']);
          if (!empty($recipients['cc'])) {
            $data['params']['headers']['Cc'] = implode(',', $recipients['cc']);
          }
          if (!empty($recipients['bcc'])) {
            $data['params']['headers']['Bcc'] = implode(',', $recipients['bcc']);
          }
    
  • πŸ‡¨πŸ‡­Switzerland zilloww

    Zilloww β†’ changed the visibility of the branch 3273627-toccbcc-email-config to hidden.

  • πŸ‡¨πŸ‡­Switzerland zilloww

    Zilloww β†’ changed the visibility of the branch 3273627-toccbcc-email-config to active.

  • πŸ‡ͺπŸ‡ΈSpain aleix

    Just reporting that without the 'to' sendgrid transport that comes with symfony_mailer cannot work.

    The to array is required for all personalization objects, and must have at least one email object with a valid email address.

    `http://sendgrid.com/docs/API_Reference/Web_API_v3/Mail/errors.html#messa...`

  • πŸ‡ΊπŸ‡ΈUnited States ja09

    What's the status of this? Ever since upgrading to D10, Sendgrid is throwing the error below and content can't transition. If I disable "Email Author" on the notification, the error goes away and everything works normally.

    --

    SendGrid\Exception\TypeException: "$emailAddress" must be a valid email address. Got: in SendGrid\Helper\Assert::email() (line 68 of /app/vendor/fastglass/sendgrid/src/Helper/Assert.php).

    #0 /app/vendor/fastglass/sendgrid/src/Mail/EmailAddress.php(75): SendGrid\Helper\Assert::email('', 'emailAddress')
    #1 /app/vendor/fastglass/sendgrid/src/Mail/EmailAddress.php(54): SendGrid\Mail\EmailAddress->setEmailAddress('')
    #2 /app/web/modules/contrib/sendgrid_integration/src/Plugin/Mail/SendGridMail.php(272): SendGrid\Mail\EmailAddress->__construct('', NULL)
    #3 /app/web/modules/contrib/mailsystem/src/Adapter.php(50): Drupal\sendgrid_integration\Plugin\Mail\SendGridMail->mail(Array)
    #4 /app/web/core/lib/Drupal/Core/Mail/MailManager.php(308): Drupal\mailsystem\Adapter->mail(Array)
    #5 /app/web/core/lib/Drupal/Core/Mail/MailManager.php(181): Drupal\Core\Mail\MailManager->doMail('content_moderat...', 'content_moderat...', '', 'en', Array, NULL, true)
    #6 /app/web/core/lib/Drupal/Core/Render/Renderer.php(638): Drupal\Core\Mail\MailManager->Drupal\Core\Mail\{closure}()
    #7 /app/web/core/lib/Drupal/Core/Mail/MailManager.php(180): Drupal\Core\Render\Renderer->executeInRenderContext(Object(Drupal\Core\Render\RenderContext), Object(Closure))
    #8 /app/web/modules/contrib/mailsystem/src/MailsystemManager.php(70): Drupal\Core\Mail\MailManager->mail('content_moderat...', 'content_moderat...', '', 'en', Array, NULL, true)
    #9 /app/web/modules/contrib/content_moderation_notifications/src/Notification.php(223): Drupal\mailsystem\MailsystemManager->mail('content_moderat...', 'content_moderat...', '', 'en', Array, NULL, true)
    #10 /app/web/modules/contrib/content_moderation_notifications/src/Notification.php(93): Drupal\content_moderation_notifications\Notification->sendNotification(Object(Drupal\mnpl\Entity\ShowNode), Array)
    #11 /app/web/modules/contrib/content_moderation_notifications/content_moderation_notifications.module(29): Drupal\content_moderation_notifications\Notification->processEntity(Object(Drupal\mnpl\Entity\ShowNode))
    #12 [internal function]: content_moderation_notifications_entity_update(Object(Drupal\mnpl\Entity\ShowNode))
    #13 /app/web/core/lib/Drupal/Core/Extension/ModuleHandler.php(416): call_user_func_array(Object(Closure), Array)
    #14 /app/web/core/lib/Drupal/Core/Extension/ModuleHandler.php(395): Drupal\Core\Extension\ModuleHandler->Drupal\Core\Extension\{closure}(Object(Closure), 'content_moderat...')
    #15 /app/web/core/lib/Drupal/Core/Extension/ModuleHandler.php(415): Drupal\Core\Extension\ModuleHandler->invokeAllWith('entity_update', Object(Closure))
    #16 /app/web/core/lib/Drupal/Core/Entity/EntityStorageBase.php(217): Drupal\Core\Extension\ModuleHandler->invokeAll('entity_update', Array)
    #17 /app/web/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php(900): Drupal\Core\Entity\EntityStorageBase->invokeHook('update', Object(Drupal\mnpl\Entity\ShowNode))
    #18 /app/web/core/lib/Drupal/Core/Entity/EntityStorageBase.php(564): Drupal\Core\Entity\ContentEntityStorageBase->invokeHook('update', Object(Drupal\mnpl\Entity\ShowNode))
    #19 /app/web/core/lib/Drupal/Core/Entity/ContentEntityStorageBase.php(781): Drupal\Core\Entity\EntityStorageBase->doPostSave(Object(Drupal\mnpl\Entity\ShowNode), true)
    #20 /app/web/core/lib/Drupal/Core/Entity/EntityStorageBase.php(489): Drupal\Core\Entity\ContentEntityStorageBase->doPostSave(Object(Drupal\mnpl\Entity\ShowNode), true)
    #21 /app/web/core/lib/Drupal/Core/Entity/Sql/SqlContentEntityStorage.php(806): Drupal\Core\Entity\EntityStorageBase->save(Object(Drupal\mnpl\Entity\ShowNode))
    #22 /app/web/core/lib/Drupal/Core/Entity/EntityBase.php(354): Drupal\Core\Entity\Sql\SqlContentEntityStorage->save(Object(Drupal\mnpl\Entity\ShowNode))
    #23 /app/web/core/modules/node/src/NodeForm.php(277): Drupal\Core\Entity\EntityBase->save()
    #24 [internal function]: Drupal\node\NodeForm->save(Array, Object(Drupal\Core\Form\FormState))
    #25 /app/web/core/lib/Drupal/Core/Form/FormSubmitter.php(129): call_user_func_array(Array, Array)
    #26 /app/web/core/lib/Drupal/Core/Form/FormSubmitter.php(67): Drupal\Core\Form\FormSubmitter->executeSubmitHandlers(Array, Object(Drupal\Core\Form\FormState))
    #27 /app/web/core/lib/Drupal/Core/Form/FormBuilder.php(597): Drupal\Core\Form\FormSubmitter->doSubmitForm(Array, Object(Drupal\Core\Form\FormState))
    #28 /app/web/core/lib/Drupal/Core/Form/FormBuilder.php(326): Drupal\Core\Form\FormBuilder->processForm('node_show_edit_...', Array, Object(Drupal\Core\Form\FormState))
    #29 /app/web/core/lib/Drupal/Core/Controller/FormController.php(73): Drupal\Core\Form\FormBuilder->buildForm(Object(Drupal\node\NodeForm), Object(Drupal\Core\Form\FormState))
    #30 [internal function]: Drupal\Core\Controller\FormController->getContentResult(Object(Symfony\Component\HttpFoundation\Request), Object(Drupal\Core\Routing\RouteMatch))
    #31 /app/web/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(123): call_user_func_array(Array, Array)
    #32 /app/web/core/lib/Drupal/Core/Render/Renderer.php(638): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}()
    #33 /app/web/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(121): Drupal\Core\Render\Renderer->executeInRenderContext(Object(Drupal\Core\Render\RenderContext), Object(Closure))
    #34 /app/web/core/lib/Drupal/Core/EventSubscriber/EarlyRenderingControllerWrapperSubscriber.php(97): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->wrapControllerExecutionInRenderContext(Array, Array)
    #35 /app/vendor/symfony/http-kernel/HttpKernel.php(181): Drupal\Core\EventSubscriber\EarlyRenderingControllerWrapperSubscriber->Drupal\Core\EventSubscriber\{closure}()
    #36 /app/vendor/symfony/http-kernel/HttpKernel.php(76): Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object(Symfony\Component\HttpFoundation\Request), 1)
    #37 /app/web/core/lib/Drupal/Core/StackMiddleware/Session.php(53): Symfony\Component\HttpKernel\HttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #38 /app/web/core/lib/Drupal/Core/StackMiddleware/KernelPreHandle.php(48): Drupal\Core\StackMiddleware\Session->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #39 /app/web/core/lib/Drupal/Core/StackMiddleware/ContentLength.php(28): Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #40 /app/web/core/modules/page_cache/src/StackMiddleware/PageCache.php(106): Drupal\Core\StackMiddleware\ContentLength->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #41 /app/web/core/modules/page_cache/src/StackMiddleware/PageCache.php(85): Drupal\page_cache\StackMiddleware\PageCache->pass(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #42 /app/web/core/lib/Drupal/Core/StackMiddleware/ReverseProxyMiddleware.php(48): Drupal\page_cache\StackMiddleware\PageCache->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #43 /app/web/core/lib/Drupal/Core/StackMiddleware/NegotiationMiddleware.php(51): Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #44 /app/web/core/lib/Drupal/Core/StackMiddleware/AjaxPageState.php(36): Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #45 /app/web/core/lib/Drupal/Core/StackMiddleware/StackedHttpKernel.php(51): Drupal\Core\StackMiddleware\AjaxPageState->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #46 /app/web/core/lib/Drupal/Core/DrupalKernel.php(741): Drupal\Core\StackMiddleware\StackedHttpKernel->handle(Object(Symfony\Component\HttpFoundation\Request), 1, true)
    #47 /app/web/index.php(19): Drupal\Core\DrupalKernel->handle(Object(Symfony\Component\HttpFoundation\Request))
    #48 {main}

Production build 0.71.5 2024