new(); * $message->subject('Hello world') * ->to('user@domain.com') * ->from('you@company.com') * ->body('Hello there big world') * ->bodyHTML('

Hello there big world

'); * $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('

Hello there big world

') * ->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 ` * @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 ` * @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' => '

Message HTML body

', * '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 " * - 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 " 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 "

Email address is not blacklisted

"; * } else { * echo "

Email is blacklisted by rule: $result

"; * } * ~~~~~ * * @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; } }