498 lines
18 KiB
PHP
498 lines
18 KiB
PHP
<?php namespace ProcessWire;
|
||
|
||
/**
|
||
* ProcessWire Mail Tools ($mail API variable)
|
||
*
|
||
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
|
||
* https://processwire.com
|
||
*
|
||
* #pw-summary Provides an API interface to email and WireMail.
|
||
* #pw-body =
|
||
* ~~~~~
|
||
* // Simple usage example
|
||
* $message = $mail->new();
|
||
* $message->subject('Hello world')
|
||
* ->to('user@domain.com')
|
||
* ->from('you@company.com')
|
||
* ->body('Hello there big world')
|
||
* ->bodyHTML('<h2>Hello there big world</h2>');
|
||
* $numSent = $message->send();
|
||
*
|
||
* // ProcessWire 3.0.113 lets you skip the $mail->new() call if you want:
|
||
* $numSent = $mail->subject('Hello world')
|
||
* ->to('user@domain.com')
|
||
* ->from('you@company.com')
|
||
* ->body('Hello there big world')
|
||
* ->bodyHTML('<h2>Hello there big world</h2>')
|
||
* ->send();
|
||
* ~~~~~
|
||
* #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)
|
||
*
|
||
*
|
||
*/
|
||
|
||
class WireMailTools extends Wire {
|
||
|
||
/**
|
||
* Get a new WireMail instance for sending email
|
||
*
|
||
* Note: The `$options` argument added in 3.0.123, previous versions had no $options argument.
|
||
*
|
||
* ~~~~~
|
||
* $message = $mail->new();
|
||
* $message->to('user@domain.com')->from('you@company.com');
|
||
* $message->subject('Mail Subject')->body('Mail Body Text')->bodyHTML('Body HTML');
|
||
* $numSent = $message->send();
|
||
* ~~~~~
|
||
*
|
||
*
|
||
* @param array|string $options Optional settings to override defaults, or string for `module` option:
|
||
* - `module` (string): Class name of WireMail module you want to use rather than auto detect, or 'WireMail' to force using default PHP mail.
|
||
* If requested module is not available, it will fall-back to one that is (or PHP mail), so check class name of returned value if there
|
||
* is any doubt about what WireMail module is being used.
|
||
* - You may also specify: subject, from, fromName, to, toName, subject or any other WireMail property and it will be populated.
|
||
* @return WireMail
|
||
*
|
||
*/
|
||
public function ___new($options = array()) {
|
||
|
||
if(is_string($options) && !empty($options)) $options = array('module' => $options);
|
||
if(!is_array($options)) $options = array();
|
||
|
||
/** @var WireMail|null $mail */
|
||
$mail = null;
|
||
|
||
$modules = $this->wire()->modules;
|
||
|
||
// merge config settings with requested options
|
||
$settings = $this->wire()->config->wireMail;
|
||
if(!is_array($settings)) $settings = array();
|
||
if(count($options)) $settings = array_merge($settings, $options);
|
||
|
||
// see if a specific WireMail module is requested
|
||
if(!empty($settings['module'])) {
|
||
if(strtolower($settings['module']) === 'wiremail') {
|
||
$mail = $this->wire(new WireMail());
|
||
} else {
|
||
$mail = $modules->getModule($settings['module']);
|
||
}
|
||
unset($settings['module']);
|
||
}
|
||
|
||
if(!$mail) {
|
||
// attempt to locate an installed module that overrides WireMail
|
||
foreach($modules->findByPrefix('WireMail') as $module) {
|
||
$parents = wireClassParents("$module");
|
||
if(in_array('WireMail', $parents) && $modules->isInstalled("$module")) {
|
||
$mail = $modules->get("$module");
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// if no module found, default to WireMail base class
|
||
if(!$mail) {
|
||
$mail = $this->wire(new WireMail());
|
||
}
|
||
|
||
// if anything left in settings, apply as a default setting
|
||
if(!empty($settings)) {
|
||
foreach($settings as $key => $value) {
|
||
$mail->set($key, $value);
|
||
}
|
||
}
|
||
|
||
// reset just in case module was not singular
|
||
$mail->to();
|
||
|
||
return $mail;
|
||
}
|
||
|
||
/**
|
||
* Send an email
|
||
*
|
||
* - Note that the order of arguments is slightly different from PHP's `mail()` function.
|
||
* - If no arguments are specified it simply returns a `WireMail` object (see #5 in examples).
|
||
* - This function will attempt to use an installed module that extends `WireMail`.
|
||
* If no module is installed, `WireMail` (which uses PHP mail) will be used instead.
|
||
*
|
||
* ~~~~~
|
||
* // 1. Default usage:
|
||
* $mail->send($to, $from, $subject, $body);
|
||
*
|
||
* // 2. Default usage with options array:
|
||
* $mail->send($to, $from, $subject, $body, $options);
|
||
*
|
||
* // 3. Specify body and/or bodyHTML in $options array (perhaps with other options):
|
||
* $mail->send($to, $from, $subject, $options);
|
||
*
|
||
* // 4. Specify both $body and $bodyHTML as arguments, but no $options:
|
||
* $mail->send($to, $from, $subject, $body, $bodyHTML);
|
||
*
|
||
* // 5. Specify a blank call to wireMail() to get the WireMail sending module:
|
||
* $wireMail = $mail->send();
|
||
* ~~~~~
|
||
*
|
||
* @param string|array $to Email address TO. For multiple, specify CSV string or array.
|
||
* @param string $from Email address FROM. This may be an email address, or a combined name and email address.
|
||
* Example of combined name and email: `Karen Cramer <karen@processwire.com>`
|
||
* @param string $subject Email subject
|
||
* @param string|array $body Email body or omit to move straight to $options
|
||
* @param array|string $options Array of options OR the $bodyHTML string. Array $options are:
|
||
* - `body` (string): Email body (text)
|
||
* - `bodyHTML` (string): Email body (HTML)
|
||
* - `replyTo` (string): Reply-to email address
|
||
* - `headers` (array): Associative array of header name => header value
|
||
* - Any additional options will be sent along to the WireMail module or class, in tact.
|
||
* @return int|WireMail Returns number of messages sent or WireMail object if no arguments specified.
|
||
*
|
||
*/
|
||
public function send($to = '', $from = '', $subject = '', $body = '', $options = array()) {
|
||
|
||
$mail = $this->new();
|
||
|
||
// if no $to address specified, return WireMail object
|
||
if(empty($to)) return $mail;
|
||
|
||
$defaults = array(
|
||
'body' => is_string($body) ? $body : '',
|
||
'bodyHTML' => '',
|
||
'replyTo' => '', // email address
|
||
'headers' => array(),
|
||
);
|
||
|
||
if(is_array($body)) {
|
||
// use case #2: body is provided in $options
|
||
$options = $body;
|
||
} else if(is_string($options)) {
|
||
// use case #3: body and bodyHTML are provided, but no $options
|
||
$options = array('bodyHTML' => $options);
|
||
} else {
|
||
// use case #1: default behavior
|
||
}
|
||
|
||
$options = array_merge($defaults, $options);
|
||
|
||
if(!empty($options['replyTo'])) {
|
||
$replyTo = $this->wire()->sanitizer->email($options['replyTo']);
|
||
if($replyTo) $options['headers']['Reply-to'] = $replyTo;
|
||
unset($options['replyTo']);
|
||
}
|
||
|
||
try {
|
||
// configure the mail
|
||
$mail->to($to)->subject($subject);
|
||
if(strlen($from)) $mail->from($from);
|
||
if(strlen($options['bodyHTML'])) $mail->bodyHTML($options['bodyHTML']);
|
||
if(strlen($options['body'])) $mail->body($options['body']);
|
||
if(count($options['headers'])) {
|
||
foreach($options['headers'] as $k => $v) $mail->header($k, $v);
|
||
}
|
||
// send along any options we don't recognize
|
||
foreach($options as $key => $value) {
|
||
if(!array_key_exists($key, $defaults)) $mail->$key = $value;
|
||
}
|
||
$numSent = $mail->send();
|
||
|
||
} catch(\Exception $e) {
|
||
if($this->wire()->config->debug) $mail->error($e->getMessage());
|
||
$mail->trackException($e, false);
|
||
$numSent = 0;
|
||
}
|
||
|
||
return $numSent;
|
||
}
|
||
|
||
/**
|
||
* Send an email with given message text assumed to be HTML
|
||
*
|
||
* This is just like the `$mail->send()` method with the exception that the body argument
|
||
* is assumed to be HTML rather than text. Note that the text version of the email is auto
|
||
* generated from the HTML, unless a `body` is provided in the `$options` array.
|
||
*
|
||
* @param string|array $to Email address TO. For multiple, specify CSV string or array.
|
||
* @param string $from Email address FROM. This may be an email address, or a combined name and email address.
|
||
* Example of combined name and email: `Karen Cramer <karen@processwire.com>`
|
||
* @param string $subject Email subject
|
||
* @param string $bodyHTML Email body in HTML
|
||
* @param array|string $options Array of options OR the $bodyHTML string. Array $options are:
|
||
* - `body` (string): Email body (text)
|
||
* - `replyTo` (string): Reply-to email address
|
||
* - `headers` (array): Associative array of header name => header value
|
||
* - Any additional options will be sent along to the WireMail module or class, in tact.
|
||
* @return int|WireMail Returns number of messages sent or WireMail object if no arguments specified.
|
||
*
|
||
*/
|
||
public function sendHTML($to = '', $from = '', $subject = '', $bodyHTML = '', $options = array()) {
|
||
$options['bodyHTML'] = $bodyHTML;
|
||
return $this->send($to, $from, $subject, $options);
|
||
}
|
||
|
||
/**
|
||
* Send an email, drop-in replacement for PHP mail() that uses the same arguments
|
||
*
|
||
* This is an alternative to using the `$mail->send()` method, and may be simpler for those converting
|
||
* existing PHP `mail()` calls to WireMail calls.
|
||
*
|
||
* This function duplicates the same arguments as PHP’s mail function, enabling you to replace an existing
|
||
* PHP `mail(…)` call with `$mail->mail(…)`.
|
||
*
|
||
* But unlike PHP’s mail function, this one can also send HTML (or multipart) emails if you provide
|
||
* an `$options` array for the `$message` argument (rather than a string). See the options array for
|
||
* the `$mail->send()` method for details.
|
||
* ~~~~~
|
||
* // 1. Basic PHP mail() style usage
|
||
* $mail->mail('ryan@processwire.com', 'Subject', 'Message body');
|
||
*
|
||
* // 2. PHP mail() style usage with with $headers argument
|
||
* $mail->mail('ryan@processwire.com', 'Subject', 'Message body', 'From: hello@world.com');
|
||
*
|
||
* // 3. Alternate usage with html and text body
|
||
* $mail->mail('ryan@processwire.com', 'Subject', [
|
||
* 'bodyHTML' => '<html><body><h1>Message HTML body</h1></body</html>',
|
||
* 'body' => 'Message text body',
|
||
* 'from' => 'hello@world.com',
|
||
* ]);
|
||
* ~~~~~
|
||
*
|
||
* @param string|array $to Email address TO. For multiple, specify CSV string or array.
|
||
* @param string $subject Email subject
|
||
* @param string|array $message Email body (PHP mail style), OR specify $options array with any of the following:
|
||
* - `bodyHTML` (string): Email body (HTML)
|
||
* - `body` (string): Email body (text). If not specified, and bodyHTML is, then text body will be auto-generated.
|
||
* - `from` (string): From email address
|
||
* - `replyTo` (string): Reply-to email address
|
||
* - `headers` (array): Associative array of header name => header value
|
||
* @param array $headers Optional additional headers as [name=value] array or "Name: Value" newline-separated string.
|
||
* Use this argument to duplicate PHP mail() style arguments. No need to use if you used $options array for the $message argument.
|
||
* @return bool True on success, false on fail.
|
||
*
|
||
*/
|
||
public function mail($to, $subject, $message, $headers = array()) {
|
||
$from = '';
|
||
|
||
if(is_string($headers)) {
|
||
$_headers = explode("\n", $headers);
|
||
$headers = array();
|
||
foreach($_headers as $header) {
|
||
if(!strpos($header, ':')) continue;
|
||
list($key, $val) = explode(':', $header, 2);
|
||
$headers[trim($key)] = trim($val);
|
||
}
|
||
}
|
||
|
||
foreach($headers as $key => $val) {
|
||
if(strtolower($key) !== 'from') continue;
|
||
$from = $val;
|
||
unset($headers[$key]);
|
||
break;
|
||
}
|
||
|
||
if(is_array($message)) {
|
||
// message is $options array
|
||
$options = $message;
|
||
if(!empty($options['headers'])) $headers = array_merge($headers, $options['headers']);
|
||
$options['headers'] = $headers;
|
||
if(isset($options['from'])) {
|
||
if(empty($from)) $from = $options['from'];
|
||
unset($options['from']);
|
||
}
|
||
$qty = $this->send($to, $from, $subject, $options);
|
||
|
||
} else {
|
||
// regular PHP style mail() call converted to $mail->send() call
|
||
$qty = $this->send($to, $from, $subject, $message, $headers);
|
||
}
|
||
|
||
return $qty > 0;
|
||
}
|
||
|
||
/**
|
||
* Send an email with message assumed to be in HTML
|
||
*
|
||
* This is the same as the `$mail->mail()` method except that the message argument is
|
||
* assumed to be HTML rather than text. The text version of the email will be auto-generated
|
||
* from the given HTML.
|
||
*
|
||
* @param string|array $to Email address TO. For multiple, specify CSV string or array.
|
||
* @param string $subject Email subject
|
||
* @param string|array Email message in HTML
|
||
* @param array $headers Optional additional headers as [name=value] array or "Name: Value" newline-separated string.
|
||
* Use this argument to duplicate PHP mail() style arguments. No need to use if you used $options array for the $message argument.
|
||
* @return bool True on success, false on fail.
|
||
* @since 3.0.109
|
||
*
|
||
*/
|
||
public function mailHTML($to, $subject, $messageHTML, $headers = array()) {
|
||
if(is_array($messageHTML)) {
|
||
$options = $messageHTML;
|
||
if(!empty($headers) && empty($options['headers'])) $options['headers'] = $headers;
|
||
} else {
|
||
$options = array(
|
||
'bodyHTML' => $messageHTML,
|
||
'headers' => $headers
|
||
);
|
||
}
|
||
return $this->mail($to, $subject, $options);
|
||
}
|
||
|
||
/**
|
||
* Return new WireMail instance populated with “to” email
|
||
*
|
||
* @param string|array $email Email to send to–specify any one of the following:
|
||
* - Single email address
|
||
* - String like: "John Smith <user@example.com>"
|
||
* - CSV string of either of the above.
|
||
* - Regular PHP array of email addresses.
|
||
* - Associative array of ['user@xample.com' => 'John Smith'].
|
||
* @param string $name An optional TO name, applies only if your $email argument was just an email address.
|
||
* @return WireMail
|
||
* @throws WireException if given invalid email address
|
||
* @since 3.0.113
|
||
*
|
||
*/
|
||
public function to($email, $name = null) {
|
||
return $this->new()->to($email, $name);
|
||
}
|
||
|
||
/**
|
||
* Return new WireMail instance populated with “from” email
|
||
*
|
||
* @param string $email Must be a single email address or "User Name <user@example.com>" string.
|
||
* @param string|null An optional FROM name
|
||
* @return WireMail
|
||
* @since 3.0.113
|
||
*
|
||
*/
|
||
public function from($email, $name = null) {
|
||
return $this->new()->from($email, $name);
|
||
}
|
||
|
||
/**
|
||
* Return new WireMail instance populated with subject
|
||
*
|
||
* @param string $subject
|
||
* @return WireMail
|
||
* @since 3.0.113
|
||
*
|
||
*/
|
||
public function subject($subject) {
|
||
return $this->new()->subject($subject);
|
||
}
|
||
|
||
public function __get($name) {
|
||
if($name === 'new') return $this->new();
|
||
return parent::__get($name);
|
||
}
|
||
|
||
/**
|
||
* 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));
|
||
|
||
if(strpos($email, '@') === false) {
|
||
return $options['why'] ? "Invalid email address" : true;
|
||
}
|
||
|
||
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;
|
||
}
|
||
|
||
|
||
}
|