reroute_email.module in Reroute Email 8
Same filename and directory in other branches
Intercepts all outgoing emails to be rerouted to a configurable destination.
File
reroute_email.moduleView source
<?php
/**
* @file
* Intercepts all outgoing emails to be rerouted to a configurable destination.
*/
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Egulias\EmailValidator\EmailParser;
use Egulias\EmailValidator\EmailLexer;
define('REROUTE_EMAIL_ENABLE', 'enable');
define('REROUTE_EMAIL_ADDRESS', 'address');
define('REROUTE_EMAIL_WHITELIST', 'whitelist');
define('REROUTE_EMAIL_DESCRIPTION', 'description');
define('REROUTE_EMAIL_MESSAGE', 'message');
define('REROUTE_EMAIL_MAILKEYS', 'mailkeys');
define('REROUTE_EMAIL_ADDRESS_EMPTY_PLACEHOLDER', '[No reroute email address configured]');
/**
* Implements hook_module_implements_alter().
*
* Ensure reroute_email runs last when hook_mail_alter is invoked.
*/
function reroute_email_module_implements_alter(&$implementations, $hook) {
// Testing with isset is only necessary if module doesn't implement the hook.
if ($hook == 'mail_alter') {
// Move our hook implementation to the bottom.
$group = $implementations['reroute_email'];
unset($implementations['reroute_email']);
$implementations['reroute_email'] = $group;
// If the queue_mail module is installed, ensure that comes after ours so
// queued emails are still rerouted.
if (isset($implementations['queue_mail'])) {
$group = $implementations['queue_mail'];
unset($implementations['queue_mail']);
$implementations['queue_mail'] = $group;
}
}
}
/**
* Implements hook_mail_alter().
*
* Alter destination of outgoing emails if reroute_email is enabled.
*/
function reroute_email_mail_alter(&$message) {
global $base_url;
$config = \Drupal::config('reroute_email.settings');
if (empty($message) || !is_array($message)) {
return;
}
// Allow other modules to decide whether the email should be rerouted by
// specify a special header 'X-Rerouted-Force' to TRUE or FALSE. Any module
// can add this header to any own emails in hook_mail or any other emails in
// hook_mail_alter() implementations.
if (!empty($message['headers']) && isset($message['headers']['X-Rerouted-Force'])) {
if (FALSE === (bool) $message['headers']['X-Rerouted-Force']) {
return;
}
// We ignore all module settings if X-Rerouted-Force header was set to TRUE.
}
elseif (reroute_email_check($message) === FALSE) {
return;
}
$mailkey = isset($message['id']) ? $message['id'] : t('[mail id] is missing');
$to = isset($message['to']) ? $message['to'] : t('[to] is missing');
$message['headers']['X-Rerouted-Mail-Key'] = $mailkey;
$message['headers']['X-Rerouted-Website'] = $base_url;
// Unset Bcc and Cc fields to prevent emails from going to those addresses.
if (isset($message['headers']) && is_array($message['headers'])) {
// Ensure we catch all Cc and Bcc headers, regardless of case,
// and protecting against multiple instances of the "same" header.
$header_keys = [];
foreach (array_keys($message['headers']) as $key) {
$header_keys[strtolower($key)][] = $key;
}
if (!empty($header_keys['cc'])) {
foreach ($header_keys['cc'] as $header) {
$message['headers']['X-Rerouted-Original-Cc'] = $message['headers'][$header];
unset($message['headers'][$header]);
}
}
if (!empty($header_keys['bcc'])) {
foreach ($header_keys['bcc'] as $header) {
$message['headers']['X-Rerouted-Original-Bcc'] = $message['headers'][$header];
unset($message['headers'][$header]);
}
}
}
// Get reroute_email_address, or use system.site.mail if not set.
$rerouting_addresses = $config
->get(REROUTE_EMAIL_ADDRESS);
if (NULL === $rerouting_addresses) {
$rerouting_addresses = \Drupal::config('system.site')
->get('mail');
}
$message['headers']['X-Rerouted-Original-To'] = $to;
$message['to'] = $rerouting_addresses;
// Format a message to show at the top.
if ($config
->get(REROUTE_EMAIL_DESCRIPTION)) {
$message_lines = [
t('This email was rerouted.'),
t('Web site: @site', [
'@site' => $base_url,
]),
t('Mail key: @key', [
'@key' => $mailkey,
]),
t('Originally to: @to', [
'@to' => $to,
]),
];
// Add Cc/Bcc values to the message only if they are set.
if (!empty($message['headers']['X-Rerouted-Original-Cc'])) {
$message_lines[] = t('Originally cc: @cc', [
'@cc' => $message['headers']['X-Rerouted-Original-Cc'],
]);
}
if (!empty($message['headers']['X-Rerouted-Original-Bcc'])) {
$message_lines[] = t('Originally bcc: @bcc', [
'@bcc' => $message['headers']['X-Rerouted-Original-Bcc'],
]);
}
// Simple separator between reroute and original messages.
$message_lines[] = '-----------------------';
$message_lines[] = '';
$msg = implode(PHP_EOL, $message_lines);
// Prepend explanation message to the body of the email. This must be
// handled differently depending on whether the body came in as a
// string or an array. If it came in as a string (despite the fact it
// should be an array) we'll respect that and leave it as a string.
if (is_string($message['body'])) {
$message['body'] = $msg . $message['body'];
}
else {
array_unshift($message['body'], $msg);
}
}
// Abort sending of the email if the no rerouting addresses provided.
if ($rerouting_addresses === '') {
$message['send'] = FALSE;
// Extensive params keys cause OOM error in var_export().
unset($message['params']);
// Simplify subject to avoid OOM error in var_export().
if ($message['subject'] instanceof TranslatableMarkup) {
$message['subject'] = $message['subject']
->render();
}
// Record a variable dump of the email in the recent log entries.
$message_string = var_export($message, TRUE);
\Drupal::logger('reroute_email')
->notice('Aborted email sending for <em>@message_id</em>. <br/>Detailed email data: Array $message <pre>@message</pre>', [
'@message_id' => $message['id'],
'@message' => $message_string,
]);
// Let users know email has been aborted, but logged.
if ($config
->get(REROUTE_EMAIL_MESSAGE)) {
\Drupal::messenger()
->addMessage(t('<em>@message_id</em> was aborted by reroute email; site administrators can check the recent log entries for complete details on the rerouted email.', [
'@message_id' => $message['id'],
]));
}
}
elseif ($config
->get(REROUTE_EMAIL_MESSAGE)) {
// Display a message to let users know email was rerouted.
\Drupal::messenger()
->addMessage(t('Submitted email, with ID: <em>@message_id</em>, was rerouted to configured address: <em>@reroute_target</em>. For more details please refer to Reroute Email settings.', [
'@message_id' => $message['id'],
'@reroute_target' => $message['to'],
]));
}
}
/**
* Implements hook_mail().
*/
function reroute_email_mail($key, &$message, $params) {
if ($message['id'] !== 'reroute_email_test_email_form') {
return;
}
$message['headers']['Cc'] = $params['cc'];
$message['headers']['Bcc'] = $params['bcc'];
$message['subject'] = $params['subject'];
$message['body'][] = $params['body'];
}
/**
* Helper function to determine a need to reroute.
*
* @param array &$message
* A message array, as described in hook_mail_alter().
*
* @return bool
* Return TRUE if should be rerouted, FALSE otherwise.
*/
function reroute_email_check(array &$message) {
// Disable rerouting according to admin settings.
$config = \Drupal::config('reroute_email.settings');
if (empty($config
->get(REROUTE_EMAIL_ENABLE))) {
return FALSE;
}
// Check configured mail keys filters.
$keys = reroute_email_split_string($config
->get(REROUTE_EMAIL_MAILKEYS, ''));
if (!empty($keys) && !(in_array($message['id'], $keys, TRUE) || in_array($message['module'], $keys, TRUE))) {
$message['headers']['X-Reroute-Status'] = 'MAILKEY-IGNORED';
return FALSE;
}
// Split addresses into arrays.
$original_addresses = reroute_email_split_string($message['to']);
$whitelisted_addresses = reroute_email_split_string($config
->get(REROUTE_EMAIL_WHITELIST));
$whitelisted_domains = [];
// Split whitelisted domains from whitelisted addresses.
foreach ($whitelisted_addresses as $key => $email) {
if (preg_match('/^\\*@(.*)$/', $email, $matches)) {
// The part after the @ sign is the domain and according to RFC 1035,
// section 3.1: "Name servers and resolvers must compare [domains] in a
// case-insensitive manner".
$domain = mb_strtolower($matches[1]);
$whitelisted_domains[$domain] = $domain;
unset($whitelisted_addresses[$key]);
}
}
// Compare original addresses with whitelisted.
$invalid = 0;
foreach ($original_addresses as $email) {
// Just ignore all invalid email addresses.
if (\Drupal::service('email.validator')
->isValid($email) === FALSE) {
$invalid++;
continue;
}
// Check whitelisted emails and domains.
$domain = mb_strtolower((new EmailParser(new EmailLexer()))
->parse($email)['domain']);
if (in_array($email, $whitelisted_addresses, TRUE) || in_array($domain, $whitelisted_domains, TRUE)) {
continue;
}
// No need to continue if at least one address should be rerouted.
$message['headers']['X-Reroute-Status'] = 'REROUTED';
return TRUE;
}
// Reroute if all addresses are invalid.
if (count($original_addresses) === $invalid) {
$message['headers']['X-Reroute-Status'] = 'INVALID-ADDRESSES';
return TRUE;
}
// All addresses passes whitelist checks.
$message['headers']['X-Reroute-Status'] = 'WHITELISTED';
return FALSE;
}
/**
* Split a string into an array by pre defined allowed delimiters.
*
* Items may be separated by any number and combination of:
* spaces, commas, semicolons, or newlines.
*
* @param string $string
* A string to be split into an array.
*
* @return array
* An array of unique values from a string.
*/
function reroute_email_split_string($string) {
$array = preg_split('/[\\s,;\\n]+/', $string, -1, PREG_SPLIT_NO_EMPTY);
// Remove duplications.
$array = array_unique($array);
return $array;
}
Functions
Name | Description |
---|---|
reroute_email_check | Helper function to determine a need to reroute. |
reroute_email_mail | Implements hook_mail(). |
reroute_email_mail_alter | Implements hook_mail_alter(). |
reroute_email_module_implements_alter | Implements hook_module_implements_alter(). |
reroute_email_split_string | Split a string into an array by pre defined allowed delimiters. |