Skip to content
Permalink
Browse files

Add support for email blacklists via $config->wireMail('blacklist') p…

…roperty
  • Loading branch information...
Ryan Cramer
Ryan Cramer committed Apr 1, 2019
1 parent b87404a commit dac7be6af4212d6ae7213e458631069a11754ff8
Showing with 170 additions and 12 deletions.
  1. +28 −0 wire/config.php
  2. +38 −11 wire/core/WireMail.php
  3. +104 −1 wire/core/WireMailTools.php
@@ -1066,12 +1066,39 @@
* Note you can add any other properties to the wireMail array that are supported by WireMail settings
* like we’ve done with from, fromName and headers here. Any values set here become defaults for the
* WireMail module.
*
* Blacklist property
* ==================
* The blacklist property lets you specify email addresses, domains, partial host names or regular
* expressions that prevent sending to certain email addresses. This is demonstrated by example:
* ~~~~~
* // Example of blacklist definition
* $config->wireMail('blacklist', [
* 'email@domain.com', // blacklist this email address
* '@host.domain.com', // blacklist all emails ending with @host.domain.com
* '@domain.com', // blacklist all emails ending with @domain.com
* 'domain.com', // blacklist any email address ending with domain.com (would include mydomain.com too).
* '.domain.com', // blacklist any email address at any host off domain.com (domain.com, my.domain.com, but NOT mydomain.com).
* '/something/', // blacklist any email containing "something". PCRE regex assumed when "/" is used as opening/closing delimiter.
* '/.+@really\.bad\.com$/', // another example of using a PCRE regular expression (blocks all "@really.bad.com").
* ]);
*
* // Test out the blacklist
* $email = 'somebody@bad-domain.com';
* $result = $mail->isBlacklistEmail($email, [ 'why' => true ]);
* if($result === false) {
* echo "<p>Email address is not blacklisted</p>";
* } else {
* echo "<p>Email is blacklisted by rule: $result</p>";
* }
* ~~~~~
*
* #property string module Name of WireMail module to use or blank to auto-detect. (default='')
* #property string from Default from email address, when none provided at runtime. (default=$config->adminEmail)
* #property string fromName Default from name string, when none provided at runtime. (default='')
* #property string newline What to use for newline if different from RFC standard of "\r\n" (optional).
* #property array headers Default additional headers to send in email, key=value. (default=[])
* #property array blacklist Email blacklist addresses or rules. (default=[])
*
* @var array
*
@@ -1081,6 +1108,7 @@
'from' => '',
'fromName' => '',
'headers' => array(),
'blacklist' => array()
);
/**
@@ -3,7 +3,7 @@
/**
* ProcessWire WireMail
*
* ProcessWire 3.x, Copyright 2016 by Ryan Cramer
* ProcessWire 3.x, Copyright 2019 by Ryan Cramer
* https://processwire.com
*
* #pw-summary A module type that handles sending of email in ProcessWire
@@ -91,17 +91,37 @@ class WireMail extends WireData implements WireMailInterface {
'attachments' => array(),
);
/**
* Construct
*
*/
public function __construct() {
$this->mail['header']['X-Mailer'] = "ProcessWire/" . $this->className();
parent::__construct();
}
/**
* Get property
*
* @param string $key
* @return mixed|null
*
*/
public function get($key) {
if($key === 'headers') $key = 'header';
if(array_key_exists($key, $this->mail)) return $this->mail[$key];
return parent::get($key);
}
/**
* Set property
*
* @param string $key
* @param mixed $value
*
* @return $this|WireData
*
*/
public function set($key, $value) {
if($key === 'headers' || $key === 'header') {
if(is_array($value)) $this->headers($value);
@@ -117,7 +137,7 @@ public function __get($key) { return $this->get($key); }
public function __set($key, $value) { return $this->set($key, $value); }
/**
* Sanitize an email address or throw WireException if invalid
* Sanitize an email address or throw WireException if invalid or in blacklist
*
* @param string $email
* @return string
@@ -127,9 +147,13 @@ public function __set($key, $value) { return $this->set($key, $value); }
protected function sanitizeEmail($email) {
$email = strtolower(trim($email));
$clean = $this->wire('sanitizer')->email($email);
if($email != $clean) {
$clean = $this->wire('sanitizer')->entities($email);
throw new WireException("Invalid email address ($clean)");
if($email !== $clean) {
throw new WireException("Invalid email address: " . $this->wire('sanitizer')->entities($email));
}
/** @var WireMailTools $mail */
$mail = $this->wire('mail');
if($mail && $mail->isBlacklistEmail($email)) {
throw new WireException("Email address not allowed: " . $this->wire('sanitizer')->entities($email));
}
return $clean;
}
@@ -204,7 +228,7 @@ protected function bundleEmailAndName($email, $name) {
* @param string $name Optionally provide a TO name, applicable
* only when specifying #1 (single email) for the first argument.
* @return $this
* @throws WireException if any provided emails were invalid
* @throws WireException if any provided emails were invalid or in blacklist
*
*/
public function to($email = null, $name = null) {
@@ -238,8 +262,10 @@ public function to($email = null, $name = null) {
if(empty($toName)) $toName = $name; // use function arg if not overwritten
$toEmail = $this->sanitizeEmail($toEmail);
$this->mail['to'][$toEmail] = $toEmail;
$this->mail['toName'][$toEmail] = $this->sanitizeHeader($toName);
if(strlen($toEmail)) {
$this->mail['to'][$toEmail] = $toEmail;
$this->mail['toName'][$toEmail] = $this->sanitizeHeader($toName);
}
}
return $this;
@@ -272,7 +298,7 @@ public function toName($name) {
* @param string $email Must be a single email address or "User Name <user@example.com>" string.
* @param string|null An optional FROM name (same as setting/calling fromName)
* @return $this
* @throws WireException if provided email was invalid
* @throws WireException if provided email was invalid or in blacklist
*
*/
public function from($email, $name = null) {
@@ -307,7 +333,7 @@ public function fromName($name) {
* @param string $email Must be a single email address or "User Name <user@example.com>" string.
* @param string|null An optional Reply-To name (same as setting/calling replyToName method)
* @return $this
* @throws WireException if provided email was invalid
* @throws WireException if provided email was invalid or in blacklist
*
*/
public function replyTo($email, $name = null) {
@@ -827,4 +853,5 @@ protected function findBestEncodePart($input, $maxlen = 63, $isFirst = false) {
public function quotedPrintableString($text) {
return '=?utf-8?Q?' . quoted_printable_encode($text) . '?=';
}
}
@@ -3,7 +3,7 @@
/**
* ProcessWire Mail Tools ($mail API variable)
*
* ProcessWire 3.x, Copyright 2018 by Ryan Cramer
* ProcessWire 3.x, Copyright 2019 by Ryan Cramer
* https://processwire.com
*
* #pw-summary Provides an API interface to email and WireMail.
@@ -29,6 +29,7 @@
* #pw-body
*
* @method WireMail new($options = array()) Create a new WireMail() instance
* @method bool|string isBlacklistEmail($email, array $options = array())
* @property WireMail new Get a new WireMail() instance (same as method version)
*
*
@@ -386,5 +387,107 @@ public function __get($key) {
if($key === 'new') return $this->new();
return parent::__get($key);
}
/**
* Is given email address in the blacklist?
*
* - Returns boolean false if not blacklisted, true if it is.
* - Uses `$config->wireMail['blacklist']` array unless given another blacklist array in $options.
* - Always independently verify that your blacklist rules are working before assuming they do.
* - Specify true for the `why` option if you want to return the matching rule when email is in blacklist.
* - Specify true for the `throw` option if you want a WireException thrown when email is blacklisted.
*
* ~~~~~
* // Define blacklist in /site/config.php
* $config->wireMail('blacklist', [
* 'email@domain.com', // blacklist this email address
* '@host.domain.com', // blacklist all emails ending with @host.domain.com
* '@domain.com', // blacklist all emails ending with @domain.com
* 'domain.com', // blacklist any email address ending with domain.com (would include mydomain.com too).
* '.domain.com', // blacklist any email address at any host off domain.com (domain.com, my.domain.com, but NOT mydomain.com).
* '/something/', // blacklist any email containing "something". PCRE regex assumed when "/" is used as opening/closing delimiter.
* '/.+@really\.bad\.com$/', // another example of using a PCRE regular expression (blocks all "@really.bad.com").
* ]);
*
* // Test if email in blacklist
* $email = 'somebody@domain.com';
* $result = $mail->isBlacklistEmail($email, [ 'why' => true ]);
* if($result === false) {
* echo "<p>Email address is not blacklisted</p>";
* } else {
* echo "<p>Email is blacklisted by rule: $result</p>";
* }
* ~~~~~
*
* @param string $email Email to check
* @param array $options
* - `blacklist` (array): Use this blacklist rather than `$config->emailBlacklist` (default=[])
* - `throw` (bool): Throw WireException if email is blacklisted? (default=false)
* - `why` (bool): Return string containing matching rule when email is blacklisted? (default=false)
* @return bool|string Returns true if email is blacklisted, false if not. Returns string if `why` option specified + email blacklisted.
* @throws WireException if given a blacklist that is not an array, or if requested to via `throw` option.
* @since 3.0.129
*
*/
public function ___isBlacklistEmail($email, array $options = array()) {
$defaults = array(
'blacklist' => array(),
'throw' => false,
'why' => false,
);
$options = count($options) ? array_merge($defaults, $options) : $defaults;
$blacklist = $options['blacklist'];
if(empty($blacklist)) $blacklist = $this->wire('config')->wireMail('blacklist');
if(empty($blacklist)) return false;
if(!is_array($blacklist)) throw new WireException("Email blacklist must be array");
$inBlacklist = false;
$tt = $this->wire('sanitizer')->getTextTools();
$email = trim($tt->strtolower($email));
foreach($blacklist as $line) {
$line = $tt->strtolower(trim($line));
if(!strlen($line)) continue;
if(strpos($line, '/') === 0) {
// perform a regex match
if(preg_match($line, $email)) $inBlacklist = $line;
} else if(strpos($line, '@')) {
// full email (@ is present and is not first char)
if($email === $line) $inBlacklist = $line;
} else if(strpos($line, '.') === 0) {
// any hostname at domain (.domain.com)
list(,$emailDomain) = explode('@', $email);
if($emailDomain === ltrim($line, '.')) {
$inBlacklist = $line;
} else if($tt->substr($emailDomain, -1 * $tt->strlen($line)) === $line ) {
$inBlacklist = $line;
}
} else {
// match ending string, host or domain name (host.domain.com, domain.com)
if($tt->substr($email, -1 * $tt->strlen($line)) === $line) $inBlacklist = $line;
}
if($inBlacklist) break;
}
if(!$inBlacklist && strpos($email, '+')) {
// leading part of email contains a plus, so check again without the "+portion"
// i.e. ryan+test@domain.com
list($prefix, $rest) = explode('+', $email, 2);
list(,$hostname) = explode('@', $rest, 2);
$email = "$prefix@$hostname";
$inBlacklist = $this->isBlacklistEmail($email, $options);
}
if($inBlacklist !== false && $options['throw']) {
throw new WireException("Email matches blacklist" . ($options['why'] ? " ($inBlacklist)" : ""));
}
if(!$options['why'] && $inBlacklist !== false) $inBlacklist = true;
return $inBlacklist;
}
}

0 comments on commit dac7be6

Please sign in to comment.
You can’t perform that action at this time.