3 use Civi\Test\EventCheck
;
4 use Civi\Test\HookInterface
;
6 return new class() extends EventCheck
implements HookInterface
{
8 private $paramSpecs = [
10 // ## Envelope: Common
12 'toName' => ['type' => 'string|NULL'],
13 'toEmail' => ['type' => 'string|NULL'],
14 'cc' => ['type' => 'string|NULL'],
15 'bcc' => ['type' => 'string|NULL'],
16 'headers' => ['type' => 'array'],
17 'attachments' => ['type' => 'array|NULL'],
18 'isTest' => ['type' => 'bool|int'],
20 // ## Envelope: singleEmail/messageTemplate
22 'from' => ['type' => 'string|NULL', 'for' => ['messageTemplate', 'singleEmail']],
23 'replyTo' => ['type' => 'string|NULL', 'for' => ['messageTemplate', 'singleEmail']],
24 'returnPath' => ['type' => 'string|NULL', 'for' => ['messageTemplate', 'singleEmail']],
25 'isEmailPdf' => ['type' => 'bool', 'for' => 'messageTemplate'],
26 'PDFFilename' => ['type' => 'string|NULL', 'for' => 'messageTemplate'],
27 'autoSubmitted' => ['type' => 'bool', 'for' => 'messageTemplate'],
28 'Message-ID' => ['type' => 'string', 'for' => ['messageTemplate', 'singleEmail']],
29 'messageId' => ['type' => 'string', 'for' => ['messageTemplate', 'singleEmail']],
30 'contactId' => ['type' => 'int|NULL', 'for' => ['messageTemplate' /* deprecated in favor of tokenContext[contactId] */, 'singleEmail']],
32 // ## Envelope: CiviMail/Flexmailer
34 'Reply-To' => ['type' => 'string|NULL', 'for' => ['civimail', 'flexmailer']],
35 'Return-Path' => ['type' => 'string|NULL', 'for' => ['civimail', 'flexmailer']],
36 'From' => ['type' => 'string|NULL', 'for' => ['civimail', 'flexmailer']],
37 'Subject' => ['type' => 'string|NULL', 'for' => ['civimail', 'flexmailer']],
38 'List-Unsubscribe' => ['type' => 'string|NULL', 'for' => ['civimail', 'flexmailer']],
39 'X-CiviMail-Bounce' => ['type' => 'string|NULL', 'for' => ['civimail', 'flexmailer']],
40 'Precedence' => ['type' => 'string|NULL', 'for' => ['civimail', 'flexmailer'], 'regex' => '/(bulk|first-class|list)/'],
41 'job_id' => ['type' => 'int|NULL', 'for' => ['civimail', 'flexmailer']],
45 'subject' => ['for' => ['messageTemplate', 'singleEmail'], 'type' => 'string'],
46 'text' => ['type' => 'string|NULL'],
47 'html' => ['type' => 'string|NULL'],
49 // ## Model: messageTemplate
51 'tokenContext' => ['type' => 'array', 'for' => 'messageTemplate'],
52 'tplParams' => ['type' => 'array', 'for' => 'messageTemplate'],
54 'regex' => '/^([a-zA-Z_]+)$/',
56 'for' => 'messageTemplate',
59 'regex' => '/^([a-zA-Z_]+)$/',
61 'for' => 'messageTemplate',
64 // This field is generally deprecated. Historically, this was tied to various option-groups (`msg_*`),
65 // but it also seems to have been used with a few long-form English names.
66 'regex' => '/^(msg_[a-zA-Z_]+|Scheduled Reminder Sender|Activity Email Sender|Report Email Sender|Mailing Event Welcome|CRM_Core_Config_MailerTest)$/',
68 'for' => ['messageTemplate', 'singleEmail'],
71 // The model is not passed into this hook because it would create ambiguity when you alter properties.
72 // If you want to expose it via hook, add another hook.
73 'model' => ['for' => 'messageTemplate', 'type' => 'NULL'],
74 'modelProps' => ['for' => 'messageTemplate', 'type' => 'NULL'],
76 // ## Model: Adhoc/incomplete/needs attention
78 'contributionId' => ['type' => 'int', 'for' => 'messageTemplate'],
79 'petitionId' => ['type' => 'int', 'for' => 'messageTemplate'],
80 'petitionTitle' => ['type' => 'string', 'for' => 'messageTemplate'],
81 'table' => ['type' => 'string', 'for' => 'messageTemplate', 'regex' => '/civicrm_msg_template/'],
82 'entity' => ['type' => 'string|NULL', 'for' => 'singleEmail'],
83 'entity_id' => ['type' => 'int|NULL', 'for' => 'singleEmail'],
85 // ## View: messageTemplate
87 'messageTemplateID' => ['type' => 'int|NULL', 'for' => 'messageTemplate'],
88 'messageTemplate' => ['type' => 'array|NULL', 'for' => 'messageTemplate'],
89 'disableSmarty' => ['type' => 'bool|int', 'for' => 'messageTemplate'],
92 public function isSupported($test): bool {
93 // MailTest does intentionally breaky things to provoke+ensure decent error-handling.
94 //So we will not enforce generic rules on it.
95 return !($test instanceof CRM_Utils_MailTest
);
99 * Ensure that the hook data is always well-formed.
101 * @see \CRM_Utils_Hook::alterMailParams()
103 public function hook_civicrm_alterMailParams(&$params, $context = NULL): void
{
104 $msg = 'Non-conforming hook_civicrm_alterMailParams(..., $context)';
105 $dump = print_r($params, 1);
107 $this->assertRegExp('/^(messageTemplate|civimail|singleEmail|flexmailer)$/',
108 $context, "$msg: Unrecognized context ($context)\n$dump");
110 $contexts = [$context];
111 if ($context === 'singleEmail' && array_key_exists('tokenContext', $params)) {
112 // Don't look now, but `sendTemplate()` fires this hook twice for the message! Once with $context=messageTemplate; again with $context=singleEmail.
113 $contexts[] = 'messageTemplate';
116 $paramSpecs = array_filter($this->paramSpecs
, function ($f) use ($contexts) {
117 return !isset($f['for']) ||
array_intersect((array) $f['for'], $contexts);
120 $unknownKeys = array_diff(array_keys($params), array_keys($paramSpecs));
121 if ($unknownKeys !== []) {
124 $this->assertEquals([], $unknownKeys, "$msg: Unrecognized keys: " . implode(', ', $unknownKeys) . "\n$dump");
126 foreach ($params as $key => $value) {
127 if (isset($paramSpecs[$key]['type'])) {
128 $this->assertType($paramSpecs[$key]['type'], $value, "$msg: Bad data-type found in param ($key)\n$dump");
130 if (isset($paramSpecs[$key]['regex']) && $value !== NULL) {
131 $this->assertRegExp($paramSpecs[$key]['regex'], $value, "Parameter [$key => $value] should match regex ({$paramSpecs[$key]['regex']})");
135 if ($context === 'messageTemplate') {
136 $this->assertNotEmpty($params['workflow'], "$msg: Message templates must always specify a symbolic name of the step/task\n$dump");
137 if (isset($params['valueName'])) {
138 // This doesn't require that valueName be supplied - but if it is supplied, it must match the workflow name.
139 $this->assertEquals($params['workflow'], $params['valueName'], "$msg: If given, workflow and valueName must match\n$dump");
141 $this->assertEquals($params['contactId'] ??
NULL, $params['tokenContext']['contactId'] ??
NULL, "$msg: contactId moved to tokenContext, but legacy value should be equivalent\n$dump");
143 // This assertion is surprising -- yet true. We should perhaps check if it was true in past releases...
144 $this->assertTrue(empty($params['text']) && empty($params['html']) && empty($params['subject']), "$msg: Content is not given if context==messageTemplate\n$dump");
147 if ($context !== 'messageTemplate') {
148 $this->assertTrue(!empty($params['text']) ||
!empty($params['html']) ||
!empty($params['subject']), "$msg: Must provide at least one of: text, html, subject\n$dump");
151 if (isset($params['groupName']) && $params['groupName'] === 'Scheduled Reminder Sender') {
152 $this->assertNotEmpty($params['entity'], "$msg: Scheduled reminders should have entity\n$dump");
153 $this->assertNotEmpty($params['entity_id'], "$msg: Scheduled reminders should have entity_id\n$dump");