Skip to content

Commit cbb45cd

Browse files
committed
Added CC and BCC support to notify-email.
- Added VORTEX_NOTIFY_EMAIL_CC and VORTEX_NOTIFY_EMAIL_BCC environment variables - Updated mail mock infrastructure to validate headers (both array and string formats) - Parse CC/BCC recipients with same format as TO recipients (comma-separated with optional names) - Added Cc and Bcc headers to mail() calls when CC/BCC recipients are specified - Display CC/BCC in notification summary output - Added 8 comprehensive tests covering single/multiple CC/BCC scenarios - All 257 tests pass with 1,283 assertions and 100% code coverage maintained
1 parent 185a290 commit cbb45cd

File tree

3 files changed

+328
-13
lines changed

3 files changed

+328
-13
lines changed

.vortex/tooling/src/notify-email

Lines changed: 68 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,20 @@ $email_from = getenv_required('VORTEX_NOTIFY_EMAIL_FROM', 'DRUPAL_SITE_EMAIL');
4242
// Example: "[email protected]|Jane Doe, [email protected]|John Doe".
4343
$email_recipients = getenv_required('VORTEX_NOTIFY_EMAIL_RECIPIENTS');
4444

45+
// Email notification CC recipients.
46+
//
47+
// Multiple names can be specified as a comma-separated list of email addresses
48+
// with optional names in the format "email|name".
49+
// Example: "[email protected]|Jane Doe, [email protected]|John Doe".
50+
$email_cc = getenv_default('VORTEX_NOTIFY_EMAIL_CC', '');
51+
52+
// Email notification BCC recipients.
53+
//
54+
// Multiple names can be specified as a comma-separated list of email addresses
55+
// with optional names in the format "email|name".
56+
// Example: "[email protected]|Jane Doe, [email protected]|John Doe".
57+
$email_bcc = getenv_default('VORTEX_NOTIFY_EMAIL_BCC', '');
58+
4559
// Email notification subject template.
4660
//
4761
// Available tokens:
@@ -62,7 +76,37 @@ $email_subject = getenv_default('VORTEX_NOTIFY_EMAIL_SUBJECT', '%project% deploy
6276
// - %login_url% - Login URL.
6377
$email_message = getenv_default('VORTEX_NOTIFY_EMAIL_MESSAGE', "## This is an automated message ##\nSite %project% %label% has been deployed at %timestamp% and is available at %environment_url%.\nLogin at: %login_url%");
6478

65-
// ------------------------------------------------------------------------------
79+
// -----------------------------------------------------------------------------
80+
81+
/**
82+
* Parse email recipients from a comma-separated string.
83+
*
84+
* @param string $recipients
85+
* Comma-separated list of recipients. Format: "email1, email2|Name, email3".
86+
*
87+
* @return array<string>
88+
* Array of formatted email addresses.
89+
*/
90+
function parse_email_recipients(string $recipients): array {
91+
if (empty($recipients)) {
92+
return [];
93+
}
94+
95+
$parsed = [];
96+
$recipient_list = array_map(trim(...), explode(',', $recipients));
97+
98+
foreach ($recipient_list as $recipient) {
99+
$parts = array_map(trim(...), explode('|', $recipient, 2));
100+
$email = $parts[0];
101+
$name = $parts[1] ?? '';
102+
103+
$parsed[] = empty($name) ? $email : sprintf('"%s" <%s>', $name, $email);
104+
}
105+
106+
return $parsed;
107+
}
108+
109+
// -----------------------------------------------------------------------------
66110

67111
info('Started email notification.');
68112

@@ -95,28 +139,42 @@ note('Environment URL: ' . $notify_env_url);
95139
note('Login URL : ' . $notify_login_url);
96140
note('From : ' . $email_from);
97141
note('Recipients : ' . $email_recipients);
142+
if (!empty($email_cc)) {
143+
note('CC : ' . $email_cc);
144+
}
145+
if (!empty($email_bcc)) {
146+
note('BCC : ' . $email_bcc);
147+
}
98148
note('Subject : ' . $email_subject);
99149
note('Content : ' . $email_message);
100150

101151
task('Sending email notification');
102152

103-
$email_recipients = array_map(trim(...), explode(',', $email_recipients));
153+
// Parse recipients.
154+
$to_list = parse_email_recipients($email_recipients);
155+
$cc_list = parse_email_recipients($email_cc);
156+
$bcc_list = parse_email_recipients($email_bcc);
104157

105158
$sent = [];
106-
foreach ($email_recipients as $email_recipient) {
107-
$parts = array_map(trim(...), explode('|', $email_recipient, 2));
108-
$email = $parts[0];
109-
$name = $parts[1] ?? '';
110-
111-
$to = empty($name) ? $email : sprintf('"%s" <%s>', $name, $email);
112-
159+
foreach ($to_list as $to) {
113160
$headers = [
114161
'From: ' . $email_from,
115162
'Content-Type: text/plain; charset=UTF-8',
116163
];
117164

165+
// Add CC header if there are CC recipients.
166+
if (!empty($cc_list)) {
167+
$headers[] = 'Cc: ' . implode(', ', $cc_list);
168+
}
169+
170+
// Add BCC header if there are BCC recipients.
171+
if (!empty($bcc_list)) {
172+
$headers[] = 'Bcc: ' . implode(', ', $bcc_list);
173+
}
174+
118175
if (mail($to, $email_subject, $email_message, $headers)) {
119-
$sent[] = $email;
176+
// Extract just the email address from formatted string for tracking.
177+
$sent[] = preg_match('/<(.+?)>/', $to, $matches) ? $matches[1] : $to;
120178
}
121179
}
122180

.vortex/tooling/tests/Traits/MockTrait.php

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -253,7 +253,7 @@ protected function mockQuit(int $code = 0, string $namespace = 'DrevOps\\VortexT
253253
* - to: Expected recipient (required)
254254
* - subject: Expected subject (required)
255255
* - message: Expected message (required)
256-
* - result: Return value (TRUE for success, FALSE for failure, default: TRUE).
256+
* - result: Return value (defaults to TRUE).
257257
* @param string $namespace
258258
* Namespace to mock the function in (defaults to DrevOps\VortexTooling).
259259
*
@@ -318,15 +318,38 @@ protected function mockMailMultiple(array $responses, string $namespace = 'DrevO
318318
throw new \RuntimeException(sprintf('mail() called with unexpected message. Expected "%s", got "%s".', $response['message'], $message));
319319
}
320320

321+
// Validate headers if specified in response.
322+
// @phpstan-ignore-next-line isset.offset
323+
if (isset($response['headers'])) {
324+
$expected_headers = $response['headers'];
325+
$actual_headers = $additional_headers;
326+
327+
// Normalize both to arrays for comparison.
328+
if (is_string($expected_headers)) {
329+
$expected_headers = array_filter(array_map(trim(...), explode("\r\n", $expected_headers)));
330+
}
331+
if (is_string($actual_headers)) {
332+
$actual_headers = array_filter(array_map(trim(...), explode("\r\n", $actual_headers)));
333+
}
334+
335+
// Sort both arrays for consistent comparison.
336+
sort($expected_headers);
337+
sort($actual_headers);
338+
339+
if ($expected_headers !== $actual_headers) {
340+
throw new \RuntimeException(sprintf('mail() called with unexpected headers. Expected "%s", got "%s".', print_r($expected_headers, TRUE), print_r($actual_headers, TRUE)));
341+
}
342+
}
343+
321344
return $response['result'];
322345
});
323346
}
324347

325348
/**
326349
* Mock single mail call.
327350
*
328-
* @param array{to: string, subject: string, message: string, result?: bool} $response
329-
* Response with recipient, subject, message, and result.
351+
* @param array{to: string, subject: string, message: string, headers?: array<string>|string, result?: bool} $response
352+
* Response with recipient, subject, message, optional headers, and result.
330353
* @param string $namespace
331354
* Namespace to mock the function in.
332355
*/

0 commit comments

Comments
 (0)