Skip to content

Email and Tokens

Email sending goes through the drupal/easy_email contrib module. Each template is an EasyEmailType config entity (a bundle); sending an email means building an EasyEmail content entity of that bundle (EmailHandlerInterface::createEmail()) and handing it to EmailHandlerInterface::sendEmail(). There is no bundled-in categorization of templates by purpose - the site maintainer picks whichever EasyEmailType makes sense for each config field; ConReg does not filter the options.

Email composition

Component Responsibility
ConregEmailSender Central service used by nearly every call site: builds an EasyEmail entity in the right language, stamps the conreg context fields, populates a real plain-text body, and sends
RegistrationConfirmationMailer Shared "confirmation + copy_us + copy_email_to" three-email pattern used by Checkout and FanTable
EmailTokenContext Rebuilds the event/members token data (the same shape ConReg has always resolved tokens against) from an EasyEmail entity's field_conreg_eid/field_conreg_mid
ConregEasyEmailFieldHooks hook_entity_base_field_info() - adds field_conreg_eid, field_conreg_mid, field_conreg_extra_token_data to the easy_email entity type
ConregEasyEmailTemplateValidationHooks Warns (non-blocking) on the EasyEmailType edit form if [conreg:member-details] is present but the chosen text format would strip its <table> markup
MemberPresenter Loads/resolves member data (labels, currency, login links) for tokens
ConregTokenHooks Implements hook_token_info()/hook_tokens() for the conreg/conreg-member token types
MemberDetailsFormatter Builds the [conreg:member-details] HTML table / plain-text report

Submodules that send their own invitation mail (Discord, PlanZ) implement their own hook_token_info()/hook_tokens() (types conreg-discord, conreg-planz) and pass their extra context through field_conreg_extra_token_data (JSON, set via ConregEmailSender::build()'s $extraTokenData argument) rather than the token hooks knowing about them directly.

Why the token data needs bridging

Easy Email's EmailTokenEvaluator::replaceTokens() always resolves tokens against ['easy_email' => $email] only - it has no concept of ConReg's event/members token data. ConregTokenHooks::tokens() detects this case ($data['easy_email'] present, $data['event']/$data['members'] absent) and calls EmailTokenContext::buildFromEmail($email), which reads field_conreg_eid/field_conreg_mid off the entity and rebuilds the same event/members array EmailTokenContext::build() would produce directly. The submodule token hooks (ConregPlanzHooks, ConregDiscordTokenHooks) follow the same pattern, reading their own extra context from field_conreg_extra_token_data instead.

Sending a conreg-flavored email

The common pattern, used by Checkout, FanTable, BulkMailController, CheckMember, conreg_planz, and conreg_discord:

$email = $this->emailSender->send(
  bundle: $easyEmailTypeId,   // an EasyEmailType ID, e.g. from event config
  recipient: $address,
  eid: $eid,
  mids: $mids,                // member ID(s) for [conreg:member(s):*] tokens
  langcode: $member->language, // resolves translated EasyEmailType text
  extraTokenData: [],          // e.g. ['planz' => [...]] for submodule tokens
);

ConregEmailSender::build() wraps createEmail() in LanguageManagerInterface::setConfigOverrideLanguage() so a member's own language picks up the matching translated EasyEmailType text (mirroring the pattern core's user module uses in UserHooks::mail()), then calls populatePlainBody() to resolve [conreg:*] tokens a second time with the plain_text token option before running the result through MailFormatHelper::htmlToText() - Easy Email's own HTML-to-plain-text conversion runs too late (after tokens are already resolved into markup) to render [conreg:member-details] as readable indented text. Because of this, every conreg-authored EasyEmailType must have "Generate plain text body" turned off.

MemberEmail.php (the "email a member" admin form) additionally passes saveEmailEntity: TRUE when sending, so each ad hoc admin-sent email is kept as a log entry (/admin/content/email, Easy Email's own log), unlike the other, higher-volume call sites.

Email-driven workflows

  • Registration confirmation and admin notification (RegistrationConfirmationMailer).
  • Membership check confirmation email (CheckMember).
  • Portal/login access links (embedded via [conreg:member:login-url]).
  • Ad hoc "email a member" from the admin members list, with a live, debounced preview.
  • Bulk email from admin routes.
  • Submodule invitation mails (PlanZ, Discord).

Token usage

Tokens are expanded via Drupal core's Token API (\Drupal::token()), not a custom parser. Event/member tokens are namespaced under conreg (examples: [conreg:event-name], [conreg:member:first-name], [conreg:member-details], [conreg:member:login-url]); Discord/PlanZ add their own conreg-discord/conreg-planz types ([conreg-discord:invite-url], [conreg-planz:user]/[conreg-planz:password]/[conreg-planz:url]). The EasyEmailType edit form shows the contrib Token module's own token browser, so authors can browse every available token, not just conreg's.

[conreg:member-details] renders as an HTML <table> with inline CSS (email clients don't reliably honor <style> blocks), grouped by registration date - members who joined on the same date share one "Registered on ..." heading and table; a date change breaks into a new heading and table, but member numbering (Member 1, Member 2, ...) keeps counting across the break. When resolved with the plain_text token option (as populatePlainBody() does), it renders as an indented plain-text block instead.

Transport

ConReg does not choose a mail transport itself - production sites configure their own Symfony Mailer-based transport for Easy Email to use (via the mailsystem module or equivalent). drupal/symfony_mailer_lite is a require-dev dependency only, providing a transport for local development and automated tests.