├── LICENSE ├── Mail.php ├── Mail ├── RFC822.php ├── mail.php ├── mock.php ├── null.php ├── sendmail.php ├── smtp.php └── smtpmx.php ├── README.rst └── composer.json /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 1997-2017, Chuck Hagenbuch 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions 6 | are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright 9 | notice, this list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright 12 | notice, this list of conditions and the following disclaimer in the 13 | documentation and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 20 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 21 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 22 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 23 | HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 24 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 25 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 26 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 27 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 28 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Mail.php: -------------------------------------------------------------------------------- 1 | 42 | * @copyright 1997-2017 Chuck Hagenbuch 43 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 44 | * @version CVS: $Id$ 45 | * @link http://pear.php.net/package/Mail/ 46 | */ 47 | 48 | require_once 'PEAR.php'; 49 | 50 | /** 51 | * PEAR's Mail:: interface. Defines the interface for implementing 52 | * mailers under the PEAR hierarchy, and provides supporting functions 53 | * useful in multiple mailer backends. 54 | * 55 | * @version $Revision$ 56 | * @package Mail 57 | */ 58 | class Mail 59 | { 60 | /** 61 | * Line terminator used for separating header lines. 62 | * @var string 63 | */ 64 | public $sep = "\r\n"; 65 | 66 | /** 67 | * Provides an interface for generating Mail:: objects of various 68 | * types 69 | * 70 | * @param string $driver The kind of Mail:: object to instantiate. 71 | * @param array $params The parameters to pass to the Mail:: object. 72 | * 73 | * @return object Mail a instance of the driver class or if fails a PEAR Error 74 | */ 75 | public static function factory($driver, $params = array()) 76 | { 77 | $driver = strtolower($driver); 78 | @include_once 'Mail/' . $driver . '.php'; 79 | $class = 'Mail_' . $driver; 80 | if (class_exists($class)) { 81 | $mailer = new $class($params); 82 | return $mailer; 83 | } else { 84 | return PEAR::raiseError('Unable to find class for driver ' . $driver); 85 | } 86 | } 87 | 88 | /** 89 | * Implements Mail::send() function using php's built-in mail() 90 | * command. 91 | * 92 | * @param mixed $recipients Either a comma-seperated list of recipients 93 | * (RFC822 compliant), or an array of recipients, 94 | * each RFC822 valid. This may contain recipients not 95 | * specified in the headers, for Bcc:, resending 96 | * messages, etc. 97 | * 98 | * @param array $headers The array of headers to send with the mail, in an 99 | * associative array, where the array key is the 100 | * header name (ie, 'Subject'), and the array value 101 | * is the header value (ie, 'test'). The header 102 | * produced from those values would be 'Subject: 103 | * test'. 104 | * 105 | * @param string $body The full text of the message body, including any 106 | * Mime parts, etc. 107 | * 108 | * @return mixed Returns true on success, or a PEAR_Error 109 | * containing a descriptive error message on 110 | * failure. 111 | * 112 | * @deprecated use Mail_mail::send instead 113 | */ 114 | public function send($recipients, $headers, $body) 115 | { 116 | if (!is_array($headers)) { 117 | return PEAR::raiseError('$headers must be an array'); 118 | } 119 | 120 | $result = $this->_sanitizeHeaders($headers); 121 | if (is_a($result, 'PEAR_Error')) { 122 | return $result; 123 | } 124 | 125 | // if we're passed an array of recipients, implode it. 126 | if (is_array($recipients)) { 127 | $recipients = implode(', ', $recipients); 128 | } 129 | 130 | // get the Subject out of the headers array so that we can 131 | // pass it as a seperate argument to mail(). 132 | $subject = ''; 133 | if (isset($headers['Subject'])) { 134 | $subject = $headers['Subject']; 135 | unset($headers['Subject']); 136 | } 137 | 138 | // flatten the headers out. 139 | list(, $text_headers) = Mail::prepareHeaders($headers); 140 | 141 | return mail($recipients, $subject, $body, $text_headers); 142 | } 143 | 144 | /** 145 | * Sanitize an array of mail headers by removing any additional header 146 | * strings present in a legitimate header's value. The goal of this 147 | * filter is to prevent mail injection attacks. 148 | * 149 | * @param array $headers The associative array of headers to sanitize. 150 | */ 151 | protected function _sanitizeHeaders(&$headers) 152 | { 153 | foreach ($headers as $key => $value) { 154 | $headers[$key] = 155 | preg_replace('=((||0x0A/%0A|0x0D/%0D|\\n|\\r)\S).*=i', 156 | '', $value); 157 | } 158 | } 159 | 160 | /** 161 | * Take an array of mail headers and return a string containing 162 | * text usable in sending a message. 163 | * 164 | * @param array $headers The array of headers to prepare, in an associative 165 | * array, where the array key is the header name (ie, 166 | * 'Subject'), and the array value is the header 167 | * value (ie, 'test'). The header produced from those 168 | * values would be 'Subject: test'. 169 | * 170 | * @return mixed Returns false if it encounters a bad address, 171 | * otherwise returns an array containing two 172 | * elements: Any From: address found in the headers, 173 | * and the plain text version of the headers. 174 | */ 175 | protected function prepareHeaders($headers) 176 | { 177 | $lines = array(); 178 | $from = null; 179 | 180 | foreach ($headers as $key => $value) { 181 | if (strcasecmp($key, 'From') === 0) { 182 | include_once 'Mail/RFC822.php'; 183 | $parser = new Mail_RFC822(); 184 | $addresses = $parser->parseAddressList($value, 'localhost', false); 185 | if (is_a($addresses, 'PEAR_Error')) { 186 | return $addresses; 187 | } 188 | 189 | $from = $addresses[0]->mailbox . '@' . $addresses[0]->host; 190 | 191 | // Reject envelope From: addresses with spaces. 192 | if (strstr($from, ' ')) { 193 | return false; 194 | } 195 | 196 | $lines[] = $key . ': ' . $value; 197 | } elseif (strcasecmp($key, 'Received') === 0) { 198 | $received = array(); 199 | if (is_array($value)) { 200 | foreach ($value as $line) { 201 | $received[] = $key . ': ' . $line; 202 | } 203 | } 204 | else { 205 | $received[] = $key . ': ' . $value; 206 | } 207 | // Put Received: headers at the top. Spam detectors often 208 | // flag messages with Received: headers after the Subject: 209 | // as spam. 210 | $lines = array_merge($received, $lines); 211 | } else { 212 | // If $value is an array (i.e., a list of addresses), convert 213 | // it to a comma-delimited string of its elements (addresses). 214 | if (is_array($value)) { 215 | $value = implode(', ', $value); 216 | } 217 | $lines[] = $key . ': ' . $value; 218 | } 219 | } 220 | 221 | return array($from, join($this->sep, $lines)); 222 | } 223 | 224 | /** 225 | * Take a set of recipients and parse them, returning an array of 226 | * bare addresses (forward paths) that can be passed to sendmail 227 | * or an smtp server with the rcpt to: command. 228 | * 229 | * @param mixed Either a comma-seperated list of recipients 230 | * (RFC822 compliant), or an array of recipients, 231 | * each RFC822 valid. 232 | * 233 | * @return mixed An array of forward paths (bare addresses) or a PEAR_Error 234 | * object if the address list could not be parsed. 235 | */ 236 | protected function parseRecipients($recipients) 237 | { 238 | include_once 'Mail/RFC822.php'; 239 | 240 | // if we're passed an array, assume addresses are valid and 241 | // implode them before parsing. 242 | if (is_array($recipients)) { 243 | $recipients = implode(', ', $recipients); 244 | } 245 | 246 | // Parse recipients, leaving out all personal info. This is 247 | // for smtp recipients, etc. All relevant personal information 248 | // should already be in the headers. 249 | $Mail_RFC822 = new Mail_RFC822(); 250 | $addresses = $Mail_RFC822->parseAddressList($recipients, 'localhost', false); 251 | 252 | // If parseAddressList() returned a PEAR_Error object, just return it. 253 | if (is_a($addresses, 'PEAR_Error')) { 254 | return $addresses; 255 | } 256 | 257 | $recipients = array(); 258 | if (is_array($addresses)) { 259 | foreach ($addresses as $ob) { 260 | $recipients[] = $ob->mailbox . '@' . $ob->host; 261 | } 262 | } 263 | 264 | return $recipients; 265 | } 266 | 267 | } 268 | -------------------------------------------------------------------------------- /Mail/RFC822.php: -------------------------------------------------------------------------------- 1 | 42 | * @author Chuck Hagenbuch parseAddressList($address_string, 'example.com', true); 65 | * print_r($structure); 66 | * 67 | * @author Richard Heyes 68 | * @author Chuck Hagenbuch 69 | * @version $Revision$ 70 | * @license BSD 71 | * @package Mail 72 | */ 73 | class Mail_RFC822 { 74 | 75 | /** 76 | * The address being parsed by the RFC822 object. 77 | * @var string $address 78 | */ 79 | var $address = ''; 80 | 81 | /** 82 | * The default domain to use for unqualified addresses. 83 | * @var string $default_domain 84 | */ 85 | var $default_domain = 'localhost'; 86 | 87 | /** 88 | * Should we return a nested array showing groups, or flatten everything? 89 | * @var boolean $nestGroups 90 | */ 91 | var $nestGroups = true; 92 | 93 | /** 94 | * Whether or not to validate atoms for non-ascii characters. 95 | * @var boolean $validate 96 | */ 97 | var $validate = true; 98 | 99 | /** 100 | * The array of raw addresses built up as we parse. 101 | * @var array $addresses 102 | */ 103 | var $addresses = array(); 104 | 105 | /** 106 | * The final array of parsed address information that we build up. 107 | * @var array $structure 108 | */ 109 | var $structure = array(); 110 | 111 | /** 112 | * The current error message, if any. 113 | * @var string $error 114 | */ 115 | var $error = null; 116 | 117 | /** 118 | * An internal counter/pointer. 119 | * @var integer $index 120 | */ 121 | var $index = null; 122 | 123 | /** 124 | * The number of groups that have been found in the address list. 125 | * @var integer $num_groups 126 | * @access public 127 | */ 128 | var $num_groups = 0; 129 | 130 | /** 131 | * A variable so that we can tell whether or not we're inside a 132 | * Mail_RFC822 object. 133 | * @var boolean $mailRFC822 134 | */ 135 | var $mailRFC822 = true; 136 | 137 | /** 138 | * A limit after which processing stops 139 | * @var int $limit 140 | */ 141 | var $limit = null; 142 | 143 | /** 144 | * Sets up the object. The address must either be set here or when 145 | * calling parseAddressList(). One or the other. 146 | * 147 | * @param string $address The address(es) to validate. 148 | * @param string $default_domain Default domain/host etc. If not supplied, will be set to localhost. 149 | * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. 150 | * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. 151 | * 152 | * @return object Mail_RFC822 A new Mail_RFC822 object. 153 | */ 154 | public function __construct($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) 155 | { 156 | if (isset($address)) $this->address = $address; 157 | if (isset($default_domain)) $this->default_domain = $default_domain; 158 | if (isset($nest_groups)) $this->nestGroups = $nest_groups; 159 | if (isset($validate)) $this->validate = $validate; 160 | if (isset($limit)) $this->limit = $limit; 161 | } 162 | 163 | /** 164 | * Starts the whole process. The address must either be set here 165 | * or when creating the object. One or the other. 166 | * 167 | * @param string $address The address(es) to validate. 168 | * @param string $default_domain Default domain/host etc. 169 | * @param boolean $nest_groups Whether to return the structure with groups nested for easier viewing. 170 | * @param boolean $validate Whether to validate atoms. Turn this off if you need to run addresses through before encoding the personal names, for instance. 171 | * 172 | * @return array A structured array of addresses. 173 | */ 174 | public function parseAddressList($address = null, $default_domain = null, $nest_groups = null, $validate = null, $limit = null) 175 | { 176 | if (version_compare(PHP_VERSION, '8.0.0', '<')) { 177 | if (!isset($this) || !isset($this->mailRFC822)) { 178 | $warn = "Calling non-static methods statically is no longer supported since PHP 8"; 179 | trigger_error($warn, E_USER_NOTICE); 180 | $obj = new Mail_RFC822($address, $default_domain, $nest_groups, $validate, $limit); 181 | return $obj->parseAddressList(); 182 | } 183 | } 184 | 185 | if (isset($address)) $this->address = $address; 186 | if (isset($default_domain)) $this->default_domain = $default_domain; 187 | if (isset($nest_groups)) $this->nestGroups = $nest_groups; 188 | if (isset($validate)) $this->validate = $validate; 189 | if (isset($limit)) $this->limit = $limit; 190 | 191 | $this->structure = array(); 192 | $this->addresses = array(); 193 | $this->error = null; 194 | $this->index = null; 195 | 196 | // Unfold any long lines in $this->address. 197 | $this->address = preg_replace('/\r?\n/', "\r\n", $this->address); 198 | $this->address = preg_replace('/\r\n(\t| )+/', ' ', $this->address); 199 | 200 | while ($this->address = $this->_splitAddresses($this->address)); 201 | 202 | if ($this->address === false || isset($this->error)) { 203 | require_once 'PEAR.php'; 204 | return PEAR::raiseError($this->error); 205 | } 206 | 207 | // Validate each address individually. If we encounter an invalid 208 | // address, stop iterating and return an error immediately. 209 | foreach ($this->addresses as $address) { 210 | $valid = $this->_validateAddress($address); 211 | 212 | if ($valid === false || isset($this->error)) { 213 | require_once 'PEAR.php'; 214 | return PEAR::raiseError($this->error); 215 | } 216 | 217 | if (!$this->nestGroups) { 218 | $this->structure = array_merge($this->structure, $valid); 219 | } else { 220 | $this->structure[] = $valid; 221 | } 222 | } 223 | 224 | return $this->structure; 225 | } 226 | 227 | /** 228 | * Splits an address into separate addresses. 229 | * 230 | * @param string $address The addresses to split. 231 | * @return boolean Success or failure. 232 | */ 233 | protected function _splitAddresses($address) 234 | { 235 | $is_group = false; 236 | $split_char = ','; 237 | 238 | if (!empty($this->limit) && count($this->addresses) == $this->limit) { 239 | return ''; 240 | } 241 | 242 | if ($this->_isGroup($address) && !isset($this->error)) { 243 | $split_char = ';'; 244 | $is_group = true; 245 | } elseif (!isset($this->error)) { 246 | $split_char = ','; 247 | $is_group = false; 248 | } elseif (isset($this->error)) { 249 | return false; 250 | } 251 | 252 | // Split the string based on the above ten or so lines. 253 | $parts = explode($split_char, $address); 254 | $string = $this->_splitCheck($parts, $split_char); 255 | 256 | // If a group... 257 | if ($is_group) { 258 | // If $string does not contain a colon outside of 259 | // brackets/quotes etc then something's fubar. 260 | 261 | // First check there's a colon at all: 262 | if (strpos($string, ':') === false) { 263 | $this->error = 'Invalid address: ' . $string; 264 | return false; 265 | } 266 | 267 | // Now check it's outside of brackets/quotes: 268 | if (!$this->_splitCheck(explode(':', $string), ':')) { 269 | return false; 270 | } 271 | 272 | // We must have a group at this point, so increase the counter: 273 | $this->num_groups++; 274 | } 275 | 276 | // $string now contains the first full address/group. 277 | // Add to the addresses array. 278 | $this->addresses[] = array( 279 | 'address' => trim($string), 280 | 'group' => $is_group 281 | ); 282 | 283 | // Remove the now stored address from the initial line, the +1 284 | // is to account for the explode character. 285 | $address = trim(substr($address, strlen($string) + 1)); 286 | 287 | // If the next char is a comma and this was a group, then 288 | // there are more addresses, otherwise, if there are any more 289 | // chars, then there is another address. 290 | if ($is_group && substr($address, 0, 1) == ','){ 291 | $address = trim(substr($address, 1)); 292 | return $address; 293 | 294 | } elseif (strlen($address) > 0) { 295 | return $address; 296 | 297 | } else { 298 | return ''; 299 | } 300 | 301 | // If you got here then something's off 302 | return false; 303 | } 304 | 305 | /** 306 | * Checks for a group at the start of the string. 307 | * 308 | * @param string $address The address to check. 309 | * @return boolean Whether or not there is a group at the start of the string. 310 | */ 311 | protected function _isGroup($address) 312 | { 313 | // First comma not in quotes, angles or escaped: 314 | $parts = explode(',', $address); 315 | $string = $this->_splitCheck($parts, ','); 316 | 317 | // Now we have the first address, we can reliably check for a 318 | // group by searching for a colon that's not escaped or in 319 | // quotes or angle brackets. 320 | if (count($parts = explode(':', $string)) > 1) { 321 | $string2 = $this->_splitCheck($parts, ':'); 322 | return ($string2 !== $string); 323 | } else { 324 | return false; 325 | } 326 | } 327 | 328 | /** 329 | * A common function that will check an exploded string. 330 | * 331 | * @param array $parts The exloded string. 332 | * @param string $char The char that was exploded on. 333 | * @return mixed False if the string contains unclosed quotes/brackets, or the string on success. 334 | */ 335 | protected function _splitCheck($parts, $char) 336 | { 337 | $string = $parts[0]; 338 | 339 | for ($i = 0; $i < count($parts); $i++) { 340 | if ($this->_hasUnclosedQuotes($string) 341 | || $this->_hasUnclosedBrackets($string, '<>') 342 | || $this->_hasUnclosedBrackets($string, '[]') 343 | || $this->_hasUnclosedBrackets($string, '()') 344 | || substr($string, -1) == '\\') { 345 | if (isset($parts[$i + 1])) { 346 | $string = $string . $char . $parts[$i + 1]; 347 | } else { 348 | $this->error = 'Invalid address spec. Unclosed bracket or quotes'; 349 | return false; 350 | } 351 | } else { 352 | $this->index = $i; 353 | break; 354 | } 355 | } 356 | 357 | return $string; 358 | } 359 | 360 | /** 361 | * Checks if a string has unclosed quotes or not. 362 | * 363 | * @param string $string The string to check. 364 | * @return boolean True if there are unclosed quotes inside the string, 365 | * false otherwise. 366 | */ 367 | protected function _hasUnclosedQuotes($string) 368 | { 369 | $string = trim($string); 370 | $iMax = strlen($string); 371 | $in_quote = false; 372 | $i = $slashes = 0; 373 | 374 | for (; $i < $iMax; ++$i) { 375 | switch ($string[$i]) { 376 | case '\\': 377 | ++$slashes; 378 | break; 379 | 380 | case '"': 381 | if ($slashes % 2 == 0) { 382 | $in_quote = !$in_quote; 383 | } 384 | // Fall through to default action below. 385 | 386 | default: 387 | $slashes = 0; 388 | break; 389 | } 390 | } 391 | 392 | return $in_quote; 393 | } 394 | 395 | /** 396 | * Checks if a string has an unclosed brackets or not. IMPORTANT: 397 | * This function handles both angle brackets and square brackets; 398 | * 399 | * @param string $string The string to check. 400 | * @param string $chars The characters to check for. 401 | * @return boolean True if there are unclosed brackets inside the string, false otherwise. 402 | */ 403 | protected function _hasUnclosedBrackets($string, $chars) 404 | { 405 | $num_angle_start = substr_count($string, $chars[0]); 406 | $num_angle_end = substr_count($string, $chars[1]); 407 | 408 | $this->_hasUnclosedBracketsSub($string, $num_angle_start, $chars[0]); 409 | $this->_hasUnclosedBracketsSub($string, $num_angle_end, $chars[1]); 410 | 411 | if ($num_angle_start < $num_angle_end) { 412 | $this->error = 'Invalid address spec. Unmatched quote or bracket (' . $chars . ')'; 413 | return false; 414 | } else { 415 | return ($num_angle_start > $num_angle_end); 416 | } 417 | } 418 | 419 | /** 420 | * Sub function that is used only by hasUnclosedBrackets(). 421 | * 422 | * @param string $string The string to check. 423 | * @param integer &$num The number of occurences. 424 | * @param string $char The character to count. 425 | * @return integer The number of occurences of $char in $string, adjusted for backslashes. 426 | */ 427 | protected function _hasUnclosedBracketsSub($string, &$num, $char) 428 | { 429 | $parts = explode($char, $string); 430 | for ($i = 0; $i < count($parts); $i++){ 431 | if (substr($parts[$i], -1) == '\\' || $this->_hasUnclosedQuotes($parts[$i])) 432 | $num--; 433 | if (isset($parts[$i + 1])) 434 | $parts[$i + 1] = $parts[$i] . $char . $parts[$i + 1]; 435 | } 436 | 437 | return $num; 438 | } 439 | 440 | /** 441 | * Function to begin checking the address. 442 | * 443 | * @param string $address The address to validate. 444 | * @return mixed False on failure, or a structured array of address information on success. 445 | */ 446 | protected function _validateAddress($address) 447 | { 448 | $structure = null; 449 | $is_group = false; 450 | $addresses = array(); 451 | 452 | if ($address['group']) { 453 | $is_group = true; 454 | 455 | // Get the group part of the name 456 | $parts = explode(':', $address['address']); 457 | $groupname = $this->_splitCheck($parts, ':'); 458 | $structure = array(); 459 | 460 | // And validate the group part of the name. 461 | if (!$this->_validatePhrase($groupname)){ 462 | $this->error = 'Group name did not validate.'; 463 | return false; 464 | } else { 465 | // Don't include groups if we are not nesting 466 | // them. This avoids returning invalid addresses. 467 | if ($this->nestGroups) { 468 | $structure = new stdClass; 469 | $structure->groupname = $groupname; 470 | } 471 | } 472 | 473 | $address['address'] = ltrim(substr($address['address'], strlen($groupname . ':'))); 474 | } 475 | 476 | // If a group then split on comma and put into an array. 477 | // Otherwise, Just put the whole address in an array. 478 | if ($is_group) { 479 | while (strlen($address['address']) > 0) { 480 | $parts = explode(',', $address['address']); 481 | $addresses[] = $this->_splitCheck($parts, ','); 482 | $address['address'] = trim(substr($address['address'], strlen(end($addresses) . ','))); 483 | } 484 | } else { 485 | $addresses[] = $address['address']; 486 | } 487 | 488 | // Trim the whitespace from all of the address strings. 489 | $addresses = array_map('trim', $addresses); 490 | 491 | // Validate each mailbox. 492 | // Format could be one of: name 493 | // geezer@domain.com 494 | // geezer 495 | // ... or any other format valid by RFC 822. 496 | for ($i = 0; $i < count($addresses); $i++) { 497 | if (!$this->validateMailbox($addresses[$i])) { 498 | if (empty($this->error)) { 499 | $this->error = 'Validation failed for: ' . $addresses[$i]; 500 | } 501 | return false; 502 | } 503 | } 504 | 505 | // Nested format 506 | if ($this->nestGroups) { 507 | if ($is_group) { 508 | $structure->addresses = $addresses; 509 | } else { 510 | $structure = $addresses[0]; 511 | } 512 | 513 | // Flat format 514 | } else { 515 | if ($is_group) { 516 | $structure = array_merge($structure, $addresses); 517 | } else { 518 | $structure = $addresses; 519 | } 520 | } 521 | 522 | return $structure; 523 | } 524 | 525 | /** 526 | * Function to validate a phrase. 527 | * 528 | * @param string $phrase The phrase to check. 529 | * @return boolean Success or failure. 530 | */ 531 | protected function _validatePhrase($phrase) 532 | { 533 | // Splits on one or more Tab or space. 534 | $parts = preg_split('/[ \\x09]+/', $phrase, -1, PREG_SPLIT_NO_EMPTY); 535 | 536 | $phrase_parts = array(); 537 | while (count($parts) > 0){ 538 | $phrase_parts[] = $this->_splitCheck($parts, ' '); 539 | for ($i = 0; $i < $this->index + 1; $i++) 540 | array_shift($parts); 541 | } 542 | 543 | foreach ($phrase_parts as $part) { 544 | // If quoted string: 545 | if (substr($part, 0, 1) == '"') { 546 | if (!$this->_validateQuotedString($part)) { 547 | return false; 548 | } 549 | continue; 550 | } 551 | 552 | // Otherwise it's an atom: 553 | if (!$this->_validateAtom($part)) return false; 554 | } 555 | 556 | return true; 557 | } 558 | 559 | /** 560 | * Function to validate an atom which from rfc822 is: 561 | * atom = 1* 562 | * 563 | * If validation ($this->validate) has been turned off, then 564 | * validateAtom() doesn't actually check anything. This is so that you 565 | * can split a list of addresses up before encoding personal names 566 | * (umlauts, etc.), for example. 567 | * 568 | * @param string $atom The string to check. 569 | * @return boolean Success or failure. 570 | */ 571 | protected function _validateAtom($atom) 572 | { 573 | if (!$this->validate) { 574 | // Validation has been turned off; assume the atom is okay. 575 | return true; 576 | } 577 | 578 | // Check for any char from ASCII 0 - ASCII 127 579 | if (!preg_match('/^[\\x00-\\x7E]+$/i', $atom, $matches)) { 580 | return false; 581 | } 582 | 583 | // Check for specials: 584 | if (preg_match('/[][()<>@,;\\:". ]/', $atom)) { 585 | return false; 586 | } 587 | 588 | // Check for control characters (ASCII 0-31): 589 | if (preg_match('/[\\x00-\\x1F]+/', $atom)) { 590 | return false; 591 | } 592 | 593 | return true; 594 | } 595 | 596 | /** 597 | * Function to validate quoted string, which is: 598 | * quoted-string = <"> *(qtext/quoted-pair) <"> 599 | * 600 | * @param string $qstring The string to check 601 | * @return boolean Success or failure. 602 | */ 603 | protected function _validateQuotedString($qstring) 604 | { 605 | // Leading and trailing " 606 | $qstring = substr($qstring, 1, -1); 607 | 608 | // Perform check, removing quoted characters first. 609 | return !preg_match('/[\x0D\\\\"]/', preg_replace('/\\\\./', '', $qstring)); 610 | } 611 | 612 | /** 613 | * Function to validate a mailbox, which is: 614 | * mailbox = addr-spec ; simple address 615 | * / phrase route-addr ; name and route-addr 616 | * 617 | * @param string &$mailbox The string to check. 618 | * @return boolean Success or failure. 619 | */ 620 | public function validateMailbox(&$mailbox) 621 | { 622 | // A couple of defaults. 623 | $phrase = ''; 624 | $comment = ''; 625 | $comments = array(); 626 | $addr_spec = null; 627 | 628 | // Catch any RFC822 comments and store them separately. 629 | $_mailbox = $mailbox; 630 | while (strlen(trim($_mailbox)) > 0) { 631 | $parts = explode('(', $_mailbox); 632 | $before_comment = $this->_splitCheck($parts, '('); 633 | if ($before_comment != $_mailbox) { 634 | // First char should be a (. 635 | $comment = substr(str_replace($before_comment, '', $_mailbox), 1); 636 | $parts = explode(')', $comment); 637 | $comment = $this->_splitCheck($parts, ')'); 638 | $comments[] = $comment; 639 | 640 | // +2 is for the brackets 641 | $_mailbox = substr($_mailbox, strpos($_mailbox, '('.$comment)+strlen($comment)+2); 642 | } else { 643 | break; 644 | } 645 | } 646 | 647 | foreach ($comments as $comment) { 648 | $mailbox = str_replace("($comment)", '', $mailbox); 649 | } 650 | 651 | $mailbox = trim($mailbox); 652 | 653 | // Check for name + route-addr 654 | if (substr($mailbox, -1) == '>' && substr($mailbox, 0, 1) != '<') { 655 | $parts = explode('<', $mailbox); 656 | $name = $this->_splitCheck($parts, '<'); 657 | 658 | $phrase = trim($name); 659 | $route_addr = trim(substr($mailbox, strlen($name.'<'), -1)); 660 | 661 | if ($this->_validatePhrase($phrase) === false || ($route_addr = $this->_validateRouteAddr($route_addr)) === false) { 662 | return false; 663 | } 664 | 665 | // Only got addr-spec 666 | } else { 667 | // First snip angle brackets if present. 668 | if (substr($mailbox, 0, 1) == '<' && substr($mailbox, -1) == '>') { 669 | $addr_spec = substr($mailbox, 1, -1); 670 | } else { 671 | $addr_spec = $mailbox; 672 | } 673 | 674 | if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { 675 | return false; 676 | } 677 | } 678 | 679 | // Construct the object that will be returned. 680 | $mbox = new stdClass(); 681 | 682 | // Add the phrase (even if empty) and comments 683 | $mbox->personal = $phrase; 684 | $mbox->comment = isset($comments) ? $comments : array(); 685 | 686 | if (isset($route_addr)) { 687 | $mbox->mailbox = $route_addr['local_part']; 688 | $mbox->host = $route_addr['domain']; 689 | $route_addr['adl'] !== '' ? $mbox->adl = $route_addr['adl'] : ''; 690 | } else { 691 | $mbox->mailbox = $addr_spec['local_part']; 692 | $mbox->host = $addr_spec['domain']; 693 | } 694 | 695 | $mailbox = $mbox; 696 | return true; 697 | } 698 | 699 | /** 700 | * This function validates a route-addr which is: 701 | * route-addr = "<" [route] addr-spec ">" 702 | * 703 | * Angle brackets have already been removed at the point of 704 | * getting to this function. 705 | * 706 | * @param string $route_addr The string to check. 707 | * @return mixed False on failure, or an array containing validated address/route information on success. 708 | */ 709 | protected function _validateRouteAddr($route_addr) 710 | { 711 | // Check for colon. 712 | if (strpos($route_addr, ':') !== false) { 713 | $parts = explode(':', $route_addr); 714 | $route = $this->_splitCheck($parts, ':'); 715 | } else { 716 | $route = $route_addr; 717 | } 718 | 719 | // If $route is same as $route_addr then the colon was in 720 | // quotes or brackets or, of course, non existent. 721 | if ($route === $route_addr){ 722 | unset($route); 723 | $addr_spec = $route_addr; 724 | if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { 725 | return false; 726 | } 727 | } else { 728 | // Validate route part. 729 | if (($route = $this->_validateRoute($route)) === false) { 730 | return false; 731 | } 732 | 733 | $addr_spec = substr($route_addr, strlen($route . ':')); 734 | 735 | // Validate addr-spec part. 736 | if (($addr_spec = $this->_validateAddrSpec($addr_spec)) === false) { 737 | return false; 738 | } 739 | } 740 | 741 | if (isset($route)) { 742 | $return['adl'] = $route; 743 | } else { 744 | $return['adl'] = ''; 745 | } 746 | 747 | $return = array_merge($return, $addr_spec); 748 | return $return; 749 | } 750 | 751 | /** 752 | * Function to validate a route, which is: 753 | * route = 1#("@" domain) ":" 754 | * 755 | * @param string $route The string to check. 756 | * @return mixed False on failure, or the validated $route on success. 757 | */ 758 | protected function _validateRoute($route) 759 | { 760 | // Split on comma. 761 | $domains = explode(',', trim($route)); 762 | 763 | foreach ($domains as $domain) { 764 | $domain = str_replace('@', '', trim($domain)); 765 | if (!$this->_validateDomain($domain)) return false; 766 | } 767 | 768 | return $route; 769 | } 770 | 771 | /** 772 | * Function to validate a domain, though this is not quite what 773 | * you expect of a strict internet domain. 774 | * 775 | * domain = sub-domain *("." sub-domain) 776 | * 777 | * @param string $domain The string to check. 778 | * @return mixed False on failure, or the validated domain on success. 779 | */ 780 | protected function _validateDomain($domain) 781 | { 782 | // Note the different use of $subdomains and $sub_domains 783 | $subdomains = explode('.', $domain); 784 | $sub_domains = array(); 785 | 786 | while (count($subdomains) > 0) { 787 | $sub_domains[] = $this->_splitCheck($subdomains, '.'); 788 | for ($i = 0; $i < $this->index + 1; $i++) 789 | array_shift($subdomains); 790 | } 791 | 792 | foreach ($sub_domains as $sub_domain) { 793 | if (!$this->_validateSubdomain(trim($sub_domain))) 794 | return false; 795 | } 796 | 797 | // Managed to get here, so return input. 798 | return $domain; 799 | } 800 | 801 | /** 802 | * Function to validate a subdomain: 803 | * subdomain = domain-ref / domain-literal 804 | * 805 | * @param string $subdomain The string to check. 806 | * @return boolean Success or failure. 807 | */ 808 | protected function _validateSubdomain($subdomain) 809 | { 810 | if (preg_match('|^\[(.*)]$|', $subdomain, $arr)){ 811 | if (!$this->_validateDliteral($arr[1])) return false; 812 | } else { 813 | if (!$this->_validateAtom($subdomain)) return false; 814 | } 815 | 816 | // Got here, so return successful. 817 | return true; 818 | } 819 | 820 | /** 821 | * Function to validate a domain literal: 822 | * domain-literal = "[" *(dtext / quoted-pair) "]" 823 | * 824 | * @param string $dliteral The string to check. 825 | * @return boolean Success or failure. 826 | */ 827 | protected function _validateDliteral($dliteral) 828 | { 829 | return !preg_match('/(.)[][\x0D\\\\]/', $dliteral, $matches) && ((! isset($matches[1])) || $matches[1] != '\\'); 830 | } 831 | 832 | /** 833 | * Function to validate an addr-spec. 834 | * 835 | * addr-spec = local-part "@" domain 836 | * 837 | * @param string $addr_spec The string to check. 838 | * @return mixed False on failure, or the validated addr-spec on success. 839 | */ 840 | protected function _validateAddrSpec($addr_spec) 841 | { 842 | $addr_spec = trim($addr_spec); 843 | 844 | // Split on @ sign if there is one. 845 | if (strpos($addr_spec, '@') !== false) { 846 | $parts = explode('@', $addr_spec); 847 | $local_part = $this->_splitCheck($parts, '@'); 848 | $domain = substr($addr_spec, strlen($local_part . '@')); 849 | 850 | // No @ sign so assume the default domain. 851 | } else { 852 | $local_part = $addr_spec; 853 | $domain = $this->default_domain; 854 | } 855 | 856 | if (($local_part = $this->_validateLocalPart($local_part)) === false) return false; 857 | if (($domain = $this->_validateDomain($domain)) === false) return false; 858 | 859 | // Got here so return successful. 860 | return array('local_part' => $local_part, 'domain' => $domain); 861 | } 862 | 863 | /** 864 | * Function to validate the local part of an address: 865 | * local-part = word *("." word) 866 | * 867 | * @param string $local_part 868 | * @return mixed False on failure, or the validated local part on success. 869 | */ 870 | protected function _validateLocalPart($local_part) 871 | { 872 | $parts = explode('.', $local_part); 873 | $words = array(); 874 | 875 | // Split the local_part into words. 876 | while (count($parts) > 0) { 877 | $words[] = $this->_splitCheck($parts, '.'); 878 | for ($i = 0; $i < $this->index + 1; $i++) { 879 | array_shift($parts); 880 | } 881 | } 882 | 883 | // Validate each word. 884 | foreach ($words as $word) { 885 | // word cannot be empty (#17317) 886 | if ($word === '') { 887 | return false; 888 | } 889 | // If this word contains an unquoted space, it is invalid. (6.2.4) 890 | if (strpos($word, ' ') && $word[0] !== '"') 891 | { 892 | return false; 893 | } 894 | 895 | if ($this->_validatePhrase(trim($word)) === false) return false; 896 | } 897 | 898 | // Managed to get here, so return the input. 899 | return $local_part; 900 | } 901 | 902 | /** 903 | * Returns an approximate count of how many addresses are in the 904 | * given string. This is APPROXIMATE as it only splits based on a 905 | * comma which has no preceding backslash. Could be useful as 906 | * large amounts of addresses will end up producing *large* 907 | * structures when used with parseAddressList(). 908 | * 909 | * @param string $data Addresses to count 910 | * @return int Approximate count 911 | */ 912 | public function approximateCount($data) 913 | { 914 | return count(preg_split('/(?@. This can be sufficient for most 921 | * people. Optional stricter mode can be utilised which restricts 922 | * mailbox characters allowed to alphanumeric, full stop, hyphen 923 | * and underscore. 924 | * 925 | * @param string $data Address to check 926 | * @param boolean $strict Optional stricter mode 927 | * @return mixed False if it fails, an indexed array 928 | * username/domain if it matches 929 | */ 930 | public function isValidInetAddress($data, $strict = false) 931 | { 932 | $regex = $strict ? '/^([.0-9a-z_+-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i' : '/^([*+!.&#$|\'\\%\/0-9a-z^_`{}=?~:-]+)@(([0-9a-z-]+\.)+[0-9a-z]{2,})$/i'; 933 | if (preg_match($regex, trim($data), $matches)) { 934 | return array($matches[1], $matches[2]); 935 | } else { 936 | return false; 937 | } 938 | } 939 | 940 | } 941 | -------------------------------------------------------------------------------- /Mail/mail.php: -------------------------------------------------------------------------------- 1 | 42 | * @copyright 2010-2017 Chuck Hagenbuch 43 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 44 | * @version CVS: $Id$ 45 | * @link http://pear.php.net/package/Mail/ 46 | */ 47 | 48 | /** 49 | * internal PHP-mail() implementation of the PEAR Mail:: interface. 50 | * @package Mail 51 | * @version $Revision$ 52 | */ 53 | class Mail_mail extends Mail { 54 | 55 | /** 56 | * Any arguments to pass to the mail() function. 57 | * @var string 58 | */ 59 | var $_params = ''; 60 | 61 | /** 62 | * Constructor. 63 | * 64 | * Instantiates a new Mail_mail:: object based on the parameters 65 | * passed in. 66 | * 67 | * @param array $params Extra arguments for the mail() function. 68 | */ 69 | public function __construct($params = null) 70 | { 71 | // The other mail implementations accept parameters as arrays. 72 | // In the interest of being consistent, explode an array into 73 | // a string of parameter arguments. 74 | if (is_array($params)) { 75 | $this->_params = join(' ', $params); 76 | } else { 77 | $this->_params = $params; 78 | } 79 | } 80 | 81 | /** 82 | * Implements Mail_mail::send() function using php's built-in mail() 83 | * command. 84 | * 85 | * @param mixed $recipients Either a comma-seperated list of recipients 86 | * (RFC822 compliant), or an array of recipients, 87 | * each RFC822 valid. This may contain recipients not 88 | * specified in the headers, for Bcc:, resending 89 | * messages, etc. 90 | * 91 | * @param array $headers The array of headers to send with the mail, in an 92 | * associative array, where the array key is the 93 | * header name (ie, 'Subject'), and the array value 94 | * is the header value (ie, 'test'). The header 95 | * produced from those values would be 'Subject: 96 | * test'. 97 | * 98 | * @param string $body The full text of the message body, including any 99 | * Mime parts, etc. 100 | * 101 | * @return mixed Returns true on success, or a PEAR_Error 102 | * containing a descriptive error message on 103 | * failure. 104 | */ 105 | public function send($recipients, $headers, $body) 106 | { 107 | if (!is_array($headers)) { 108 | return PEAR::raiseError('$headers must be an array'); 109 | } 110 | 111 | $result = $this->_sanitizeHeaders($headers); 112 | if (is_a($result, 'PEAR_Error')) { 113 | return $result; 114 | } 115 | 116 | // If we're passed an array of recipients, implode it. 117 | if (is_array($recipients)) { 118 | $recipients = implode(', ', $recipients); 119 | } 120 | 121 | // Get the Subject out of the headers array so that we can 122 | // pass it as a seperate argument to mail(). 123 | $subject = ''; 124 | if (isset($headers['Subject'])) { 125 | $subject = $headers['Subject']; 126 | unset($headers['Subject']); 127 | } 128 | 129 | // Also remove the To: header. The mail() function will add its own 130 | // To: header based on the contents of $recipients. 131 | unset($headers['To']); 132 | 133 | // Flatten the headers out. 134 | $headerElements = $this->prepareHeaders($headers); 135 | if (is_a($headerElements, 'PEAR_Error')) { 136 | return $headerElements; 137 | } 138 | list(, $text_headers) = $headerElements; 139 | 140 | // We only use mail()'s optional fifth parameter if the additional 141 | // parameters have been provided and we're not running in safe mode. 142 | if (empty($this->_params) || ini_get('safe_mode')) { 143 | $result = mail($recipients, $subject, $body, $text_headers); 144 | } else { 145 | $result = mail($recipients, $subject, $body, $text_headers, 146 | $this->_params); 147 | } 148 | 149 | // If the mail() function returned failure, we need to create a 150 | // PEAR_Error object and return it instead of the boolean result. 151 | if ($result === false) { 152 | $result = PEAR::raiseError('mail() returned failure'); 153 | } 154 | 155 | return $result; 156 | } 157 | 158 | } 159 | -------------------------------------------------------------------------------- /Mail/mock.php: -------------------------------------------------------------------------------- 1 | 42 | * @copyright 2010-2017 Chuck Hagenbuch 43 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 44 | * @version CVS: $Id$ 45 | * @link http://pear.php.net/package/Mail/ 46 | */ 47 | 48 | /** 49 | * Mock implementation of the PEAR Mail:: interface for testing. 50 | * @access public 51 | * @package Mail 52 | * @version $Revision$ 53 | */ 54 | class Mail_mock extends Mail { 55 | 56 | /** 57 | * Array of messages that have been sent with the mock. 58 | * 59 | * @var array 60 | */ 61 | public $sentMessages = array(); 62 | 63 | /** 64 | * Callback before sending mail. 65 | * 66 | * @var callback 67 | */ 68 | protected $_preSendCallback; 69 | 70 | /** 71 | * Callback after sending mai. 72 | * 73 | * @var callback 74 | */ 75 | protected $_postSendCallback; 76 | 77 | /** 78 | * Constructor. 79 | * 80 | * Instantiates a new Mail_mock:: object based on the parameters 81 | * passed in. It looks for the following parameters, both optional: 82 | * preSendCallback Called before an email would be sent. 83 | * postSendCallback Called after an email would have been sent. 84 | * 85 | * @param array Hash containing any parameters. 86 | */ 87 | public function __construct($params) 88 | { 89 | if (isset($params['preSendCallback']) && 90 | is_callable($params['preSendCallback'])) { 91 | $this->_preSendCallback = $params['preSendCallback']; 92 | } 93 | 94 | if (isset($params['postSendCallback']) && 95 | is_callable($params['postSendCallback'])) { 96 | $this->_postSendCallback = $params['postSendCallback']; 97 | } 98 | } 99 | 100 | /** 101 | * Implements Mail_mock::send() function. Silently discards all 102 | * mail. 103 | * 104 | * @param mixed $recipients Either a comma-seperated list of recipients 105 | * (RFC822 compliant), or an array of recipients, 106 | * each RFC822 valid. This may contain recipients not 107 | * specified in the headers, for Bcc:, resending 108 | * messages, etc. 109 | * 110 | * @param array $headers The array of headers to send with the mail, in an 111 | * associative array, where the array key is the 112 | * header name (ie, 'Subject'), and the array value 113 | * is the header value (ie, 'test'). The header 114 | * produced from those values would be 'Subject: 115 | * test'. 116 | * 117 | * @param string $body The full text of the message body, including any 118 | * Mime parts, etc. 119 | * 120 | * @return mixed Returns true on success, or a PEAR_Error 121 | * containing a descriptive error message on 122 | * failure. 123 | */ 124 | public function send($recipients, $headers, $body) 125 | { 126 | if ($this->_preSendCallback) { 127 | call_user_func_array($this->_preSendCallback, 128 | array(&$this, $recipients, $headers, $body)); 129 | } 130 | 131 | $entry = array('recipients' => $recipients, 'headers' => $headers, 'body' => $body); 132 | $this->sentMessages[] = $entry; 133 | 134 | if ($this->_postSendCallback) { 135 | call_user_func_array($this->_postSendCallback, 136 | array(&$this, $recipients, $headers, $body)); 137 | } 138 | 139 | return true; 140 | } 141 | 142 | } 143 | -------------------------------------------------------------------------------- /Mail/null.php: -------------------------------------------------------------------------------- 1 | 42 | * @copyright 2010-2017 Phil Kernick 43 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 44 | * @version CVS: $Id$ 45 | * @link http://pear.php.net/package/Mail/ 46 | */ 47 | 48 | /** 49 | * Null implementation of the PEAR Mail:: interface. 50 | * @access public 51 | * @package Mail 52 | * @version $Revision$ 53 | */ 54 | class Mail_null extends Mail { 55 | 56 | /** 57 | * Implements Mail_null::send() function. Silently discards all 58 | * mail. 59 | * 60 | * @param mixed $recipients Either a comma-seperated list of recipients 61 | * (RFC822 compliant), or an array of recipients, 62 | * each RFC822 valid. This may contain recipients not 63 | * specified in the headers, for Bcc:, resending 64 | * messages, etc. 65 | * 66 | * @param array $headers The array of headers to send with the mail, in an 67 | * associative array, where the array key is the 68 | * header name (ie, 'Subject'), and the array value 69 | * is the header value (ie, 'test'). The header 70 | * produced from those values would be 'Subject: 71 | * test'. 72 | * 73 | * @param string $body The full text of the message body, including any 74 | * Mime parts, etc. 75 | * 76 | * @return mixed Returns true on success, or a PEAR_Error 77 | * containing a descriptive error message on 78 | * failure. 79 | */ 80 | public function send($recipients, $headers, $body) 81 | { 82 | return true; 83 | } 84 | 85 | } 86 | -------------------------------------------------------------------------------- /Mail/sendmail.php: -------------------------------------------------------------------------------- 1 | 42 | * @author Chuck Hagenbuch 43 | * @copyright 2010-2017 Chuck Hagenbuch 44 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 45 | * @version CVS: $Id$ 46 | * @link http://pear.php.net/package/Mail/ 47 | */ 48 | 49 | /** 50 | * Sendmail implementation of the PEAR Mail:: interface. 51 | * @access public 52 | * @package Mail 53 | * @version $Revision$ 54 | */ 55 | class Mail_sendmail extends Mail { 56 | 57 | /** 58 | * The location of the sendmail or sendmail wrapper binary on the 59 | * filesystem. 60 | * @var string 61 | */ 62 | var $sendmail_path = '/usr/sbin/sendmail'; 63 | 64 | /** 65 | * Any extra command-line parameters to pass to the sendmail or 66 | * sendmail wrapper binary. 67 | * @var string 68 | */ 69 | var $sendmail_args = '-i'; 70 | 71 | /** 72 | * Constructor. 73 | * 74 | * Instantiates a new Mail_sendmail:: object based on the parameters 75 | * passed in. It looks for the following parameters: 76 | * sendmail_path The location of the sendmail binary on the 77 | * filesystem. Defaults to '/usr/sbin/sendmail'. 78 | * 79 | * sendmail_args Any extra parameters to pass to the sendmail 80 | * or sendmail wrapper binary. 81 | * 82 | * If a parameter is present in the $params array, it replaces the 83 | * default. 84 | * 85 | * @param array $params Hash containing any parameters different from the 86 | * defaults. 87 | */ 88 | public function __construct($params) 89 | { 90 | if (isset($params['sendmail_path'])) { 91 | $this->sendmail_path = $params['sendmail_path']; 92 | } 93 | if (isset($params['sendmail_args'])) { 94 | $this->sendmail_args = $params['sendmail_args']; 95 | } 96 | 97 | /* 98 | * Because we need to pass message headers to the sendmail program on 99 | * the commandline, we can't guarantee the use of the standard "\r\n" 100 | * separator. Instead, we use the system's native line separator. 101 | */ 102 | if (defined('PHP_EOL')) { 103 | $this->sep = PHP_EOL; 104 | } else { 105 | $this->sep = (strpos(PHP_OS, 'WIN') === false) ? "\n" : "\r\n"; 106 | } 107 | } 108 | 109 | /** 110 | * Implements Mail::send() function using the sendmail 111 | * command-line binary. 112 | * 113 | * @param mixed $recipients Either a comma-seperated list of recipients 114 | * (RFC822 compliant), or an array of recipients, 115 | * each RFC822 valid. This may contain recipients not 116 | * specified in the headers, for Bcc:, resending 117 | * messages, etc. 118 | * 119 | * @param array $headers The array of headers to send with the mail, in an 120 | * associative array, where the array key is the 121 | * header name (ie, 'Subject'), and the array value 122 | * is the header value (ie, 'test'). The header 123 | * produced from those values would be 'Subject: 124 | * test'. 125 | * 126 | * @param string $body The full text of the message body, including any 127 | * Mime parts, etc. 128 | * 129 | * @return mixed Returns true on success, or a PEAR_Error 130 | * containing a descriptive error message on 131 | * failure. 132 | */ 133 | public function send($recipients, $headers, $body) 134 | { 135 | if (!is_array($headers)) { 136 | return PEAR::raiseError('$headers must be an array'); 137 | } 138 | 139 | $result = $this->_sanitizeHeaders($headers); 140 | if (is_a($result, 'PEAR_Error')) { 141 | return $result; 142 | } 143 | 144 | $recipients = $this->parseRecipients($recipients); 145 | if (is_a($recipients, 'PEAR_Error')) { 146 | return $recipients; 147 | } 148 | $recipients = implode(' ', array_map('escapeshellarg', $recipients)); 149 | 150 | $headerElements = $this->prepareHeaders($headers); 151 | if (is_a($headerElements, 'PEAR_Error')) { 152 | return $headerElements; 153 | } 154 | list($from, $text_headers) = $headerElements; 155 | 156 | /* Since few MTAs are going to allow this header to be forged 157 | * unless it's in the MAIL FROM: exchange, we'll use 158 | * Return-Path instead of From: if it's set. */ 159 | if (!empty($headers['Return-Path'])) { 160 | $from = $headers['Return-Path']; 161 | } 162 | 163 | if (!isset($from)) { 164 | return PEAR::raiseError('No from address given.'); 165 | } elseif (strpos($from, ' ') !== false || 166 | strpos($from, ';') !== false || 167 | strpos($from, '&') !== false || 168 | strpos($from, '`') !== false) { 169 | return PEAR::raiseError('From address specified with dangerous characters.'); 170 | } 171 | 172 | $from = escapeshellarg($from); // Security bug #16200 173 | 174 | $mail = @popen($this->sendmail_path . (!empty($this->sendmail_args) ? ' ' . $this->sendmail_args : '') . " -f$from -- $recipients", 'w'); 175 | if (!$mail) { 176 | return PEAR::raiseError('Failed to open sendmail [' . $this->sendmail_path . '] for execution.'); 177 | } 178 | 179 | // Write the headers following by two newlines: one to end the headers 180 | // section and a second to separate the headers block from the body. 181 | fputs($mail, $text_headers . $this->sep . $this->sep); 182 | 183 | fputs($mail, $body); 184 | $result = pclose($mail); 185 | if (version_compare(phpversion(), '4.2.3') == -1) { 186 | // With older php versions, we need to shift the pclose 187 | // result to get the exit code. 188 | $result = $result >> 8 & 0xFF; 189 | } 190 | 191 | if ($result != 0) { 192 | return PEAR::raiseError('sendmail returned error code ' . $result, 193 | $result); 194 | } 195 | 196 | return true; 197 | } 198 | 199 | } 200 | -------------------------------------------------------------------------------- /Mail/smtp.php: -------------------------------------------------------------------------------- 1 | 42 | * @author Chuck Hagenbuch 43 | * @copyright 2010-2021 Chuck Hagenbuch 44 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 45 | * @version CVS: $Id$ 46 | * @link http://pear.php.net/package/Mail/ 47 | */ 48 | 49 | /** Error: Failed to create a Net_SMTP object */ 50 | define('PEAR_MAIL_SMTP_ERROR_CREATE', 10000); 51 | 52 | /** Error: Failed to connect to SMTP server */ 53 | define('PEAR_MAIL_SMTP_ERROR_CONNECT', 10001); 54 | 55 | /** Error: SMTP authentication failure */ 56 | define('PEAR_MAIL_SMTP_ERROR_AUTH', 10002); 57 | 58 | /** Error: No From: address has been provided */ 59 | define('PEAR_MAIL_SMTP_ERROR_FROM', 10003); 60 | 61 | /** Error: Failed to set sender */ 62 | define('PEAR_MAIL_SMTP_ERROR_SENDER', 10004); 63 | 64 | /** Error: Failed to add recipient */ 65 | define('PEAR_MAIL_SMTP_ERROR_RECIPIENT', 10005); 66 | 67 | /** Error: Failed to send data */ 68 | define('PEAR_MAIL_SMTP_ERROR_DATA', 10006); 69 | 70 | /** 71 | * SMTP implementation of the PEAR Mail interface. Requires the Net_SMTP class. 72 | * @access public 73 | * @package Mail 74 | * @version $Revision$ 75 | */ 76 | class Mail_smtp extends Mail { 77 | 78 | /** 79 | * SMTP connection object. 80 | * 81 | * @var object 82 | * @access private 83 | */ 84 | var $_smtp = null; 85 | 86 | /** 87 | * The list of service extension parameters to pass to the Net_SMTP 88 | * mailFrom() command. 89 | * 90 | * @var array 91 | */ 92 | var $_extparams = array(); 93 | 94 | /** 95 | * The SMTP host to connect to. 96 | * 97 | * @var string 98 | */ 99 | var $host = 'localhost'; 100 | 101 | /** 102 | * The port the SMTP server is on. 103 | * 104 | * @var integer 105 | */ 106 | var $port = 25; 107 | 108 | /** 109 | * Should STARTTLS connection be used? 110 | * 111 | * This value may be set to true or false. 112 | * 113 | * If the value is set to true, the Net_SMTP package will attempt to use 114 | * a STARTTLS encrypted connection. 115 | * 116 | * If the value is set to false, the Net_SMTP package will avoid 117 | * a STARTTLS encrypted connection. 118 | * 119 | * NULL indicates only STARTTLS if $auth is set. 120 | * 121 | * PEAR/Net_SMTP >= 1.10.0 required. 122 | * 123 | * @var boolean 124 | */ 125 | var $starttls = null; 126 | 127 | /** 128 | * Should SMTP authentication be used? 129 | * 130 | * This value may be set to true, false or the name of a specific 131 | * authentication method. 132 | * 133 | * If the value is set to true, the Net_SMTP package will attempt to use 134 | * the best authentication method advertised by the remote SMTP server. 135 | * 136 | * @var mixed 137 | */ 138 | var $auth = false; 139 | 140 | /** 141 | * The username to use if the SMTP server requires authentication. 142 | * 143 | * @var string 144 | */ 145 | var $username = ''; 146 | 147 | /** 148 | * The password to use if the SMTP server requires authentication. 149 | * 150 | * @var string 151 | */ 152 | var $password = ''; 153 | 154 | /** 155 | * Hostname or domain that will be sent to the remote SMTP server in the 156 | * HELO / EHLO message. 157 | * 158 | * @var string 159 | */ 160 | var $localhost = 'localhost'; 161 | 162 | /** 163 | * SMTP connection timeout value. NULL indicates no timeout. 164 | * 165 | * @var integer 166 | */ 167 | var $timeout = null; 168 | 169 | /** 170 | * Turn on Net_SMTP debugging? 171 | * 172 | * @var boolean $debug 173 | */ 174 | var $debug = false; 175 | 176 | /** 177 | * Set debug_handler on Net_SMTP 178 | * 179 | * @var callable $debug_handler 180 | */ 181 | var $debug_handler = null; 182 | 183 | /** 184 | * we need the greeting; from it we can extract the authorative name of the mail 185 | * server we've really connected to. ideal if we're connecting to a round-robin 186 | * of relay servers and need to track which exact one took the email 187 | * 188 | * @var string 189 | */ 190 | var $greeting = null; 191 | 192 | /** 193 | * Indicates whether or not the SMTP connection should persist over 194 | * multiple calls to the send() method. 195 | * 196 | * @var boolean 197 | */ 198 | var $persist = false; 199 | 200 | /** 201 | * Use SMTP command pipelining (specified in RFC 2920) if the SMTP server 202 | * supports it. This speeds up delivery over high-latency connections. By 203 | * default, use the default value supplied by Net_SMTP. 204 | * 205 | * @var boolean 206 | */ 207 | var $pipelining; 208 | 209 | /** 210 | * The list of socket options 211 | * 212 | * @var array 213 | */ 214 | var $socket_options = array(); 215 | 216 | /** 217 | * SMTP response message 218 | * 219 | * @var string 220 | * @since 1.6.0 221 | */ 222 | var $response = null; 223 | 224 | /** 225 | * If the message ends up in the queue, on the recipient server, 226 | * the response will be saved here. 227 | * Some successfully delivered emails will include a “queued” 228 | * notation in the SMTP response, such as "250 OK; queued as 12345". 229 | * This indicates that the email was delivered to the recipient 230 | * as expected, but may require additional processing before it 231 | * lands in the recipient’s inbox. 232 | * 233 | * @var string 234 | */ 235 | var $queued_as = null; 236 | 237 | /** 238 | * Constructor. 239 | * 240 | * Instantiates a new Mail_smtp:: object based on the parameters 241 | * passed in. It looks for the following parameters: 242 | * host The server to connect to. Defaults to localhost. 243 | * port The port to connect to. Defaults to 25. 244 | * auth SMTP authentication. Defaults to none. 245 | * starttls Should STARTTLS connection be used? No default. PEAR/Net_SMTP >= 1.10.0 required. 246 | * username The username to use for SMTP auth. No default. 247 | * password The password to use for SMTP auth. No default. 248 | * localhost The local hostname / domain. Defaults to localhost. 249 | * timeout The SMTP connection timeout. Defaults to none. 250 | * verp Whether to use VERP or not. Defaults to false. 251 | * DEPRECATED as of 1.2.0 (use setMailParams()). 252 | * debug Activate SMTP debug mode? Defaults to false. 253 | * debug_handler Set SMTP debug handler function. Defaults to null. 254 | * persist Should the SMTP connection persist? 255 | * pipelining Use SMTP command pipelining 256 | * socket_options Socket stream_context_create() options. 257 | * 258 | * If a parameter is present in the $params array, it replaces the 259 | * default. 260 | * 261 | * @param array Hash containing any parameters different from the 262 | * defaults. 263 | */ 264 | public function __construct($params) 265 | { 266 | if (isset($params['host'])) $this->host = $params['host']; 267 | if (isset($params['port'])) $this->port = $params['port']; 268 | if (isset($params['auth'])) $this->auth = $params['auth']; 269 | if (isset($params['starttls'])) $this->starttls = $params['starttls']; 270 | if (isset($params['username'])) $this->username = $params['username']; 271 | if (isset($params['password'])) $this->password = $params['password']; 272 | if (isset($params['localhost'])) $this->localhost = $params['localhost']; 273 | if (isset($params['timeout'])) $this->timeout = $params['timeout']; 274 | if (isset($params['debug'])) $this->debug = (bool)$params['debug']; 275 | if (isset($params['debug_handler'])) $this->debug_handler = $params['debug_handler']; 276 | if (isset($params['persist'])) $this->persist = (bool)$params['persist']; 277 | if (isset($params['pipelining'])) $this->pipelining = (bool)$params['pipelining']; 278 | if (isset($params['socket_options'])) $this->socket_options = $params['socket_options']; 279 | // Deprecated options 280 | if (isset($params['verp'])) { 281 | $this->addServiceExtensionParameter('XVERP', is_bool($params['verp']) ? null : $params['verp']); 282 | } 283 | } 284 | 285 | /** 286 | * Destructor implementation to ensure that we disconnect from any 287 | * potentially-alive persistent SMTP connections. 288 | */ 289 | public function __destruct() 290 | { 291 | $this->disconnect(); 292 | } 293 | 294 | /** 295 | * Implements Mail::send() function using SMTP. 296 | * 297 | * @param mixed $recipients Either a comma-seperated list of recipients 298 | * (RFC822 compliant), or an array of recipients, 299 | * each RFC822 valid. This may contain recipients not 300 | * specified in the headers, for Bcc:, resending 301 | * messages, etc. 302 | * 303 | * @param array $headers The array of headers to send with the mail, in an 304 | * associative array, where the array key is the 305 | * header name (e.g., 'Subject'), and the array value 306 | * is the header value (e.g., 'test'). The header 307 | * produced from those values would be 'Subject: 308 | * test'. 309 | * 310 | * @param string $body The full text of the message body, including any 311 | * MIME parts, etc. 312 | * 313 | * @return mixed Returns true on success, or a PEAR_Error 314 | * containing a descriptive error message on 315 | * failure. 316 | */ 317 | public function send($recipients, $headers, $body) 318 | { 319 | $result = $this->send_or_fail($recipients, $headers, $body); 320 | 321 | /* If persistent connections are disabled, destroy our SMTP object. */ 322 | if ($this->persist === false) { 323 | $this->disconnect(); 324 | } 325 | 326 | return $result; 327 | } 328 | 329 | protected function send_or_fail($recipients, $headers, $body) 330 | { 331 | /* If we don't already have an SMTP object, create one. */ 332 | $result = $this->getSMTPObject(); 333 | if (PEAR::isError($result)) { 334 | return $result; 335 | } 336 | 337 | if (!is_array($headers)) { 338 | return PEAR::raiseError('$headers must be an array'); 339 | } 340 | 341 | $this->_sanitizeHeaders($headers); 342 | 343 | $headerElements = $this->prepareHeaders($headers); 344 | if (is_a($headerElements, 'PEAR_Error')) { 345 | $this->_smtp->rset(); 346 | return $headerElements; 347 | } 348 | list($from, $textHeaders) = $headerElements; 349 | 350 | /* Since few MTAs are going to allow this header to be forged 351 | * unless it's in the MAIL FROM: exchange, we'll use 352 | * Return-Path instead of From: if it's set. */ 353 | if (!empty($headers['Return-Path'])) { 354 | $from = $headers['Return-Path']; 355 | } 356 | 357 | if (!isset($from)) { 358 | $this->_smtp->rset(); 359 | return PEAR::raiseError('No From: address has been provided', 360 | PEAR_MAIL_SMTP_ERROR_FROM); 361 | } 362 | 363 | $params = ''; 364 | if (!empty($this->_extparams)) { 365 | foreach ($this->_extparams as $key => $val) { 366 | $params .= ' ' . $key . (is_null($val) ? '' : '=' . $val); 367 | } 368 | } 369 | if (PEAR::isError($res = $this->_smtp->mailFrom($from, ltrim($params)))) { 370 | $error = $this->_error("Failed to set sender: $from", $res); 371 | $this->_smtp->rset(); 372 | return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_SENDER); 373 | } 374 | 375 | $recipients = $this->parseRecipients($recipients); 376 | if (is_a($recipients, 'PEAR_Error')) { 377 | $this->_smtp->rset(); 378 | return $recipients; 379 | } 380 | 381 | foreach ($recipients as $recipient) { 382 | $res = $this->_smtp->rcptTo($recipient); 383 | if (is_a($res, 'PEAR_Error')) { 384 | $error = $this->_error("Failed to add recipient: $recipient", $res); 385 | $this->_smtp->rset(); 386 | return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_RECIPIENT); 387 | } 388 | } 389 | 390 | /* Send the message's headers and the body as SMTP data. */ 391 | $res = $this->_smtp->data($body, $textHeaders); 392 | 393 | list($code, $args) = $this->_smtp->getResponse(); 394 | 395 | $this->response = $code . ' ' . $args; 396 | 397 | if (preg_match("/ queued as (.*)/", $args, $queued)) { 398 | $this->queued_as = $queued[1]; 399 | } 400 | 401 | /* we need the greeting; from it we can extract the authorative name of the mail server we've really connected to. 402 | * ideal if we're connecting to a round-robin of relay servers and need to track which exact one took the email */ 403 | $this->greeting = $this->_smtp->getGreeting(); 404 | 405 | if (is_a($res, 'PEAR_Error')) { 406 | $error = $this->_error('Failed to send data', $res); 407 | $this->_smtp->rset(); 408 | return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_DATA); 409 | } 410 | 411 | return true; 412 | } 413 | 414 | /** 415 | * Connect to the SMTP server by instantiating a Net_SMTP object. 416 | * 417 | * @return mixed Returns a reference to the Net_SMTP object on success, or 418 | * a PEAR_Error containing a descriptive error message on 419 | * failure. 420 | * 421 | * @since 1.2.0 422 | */ 423 | public function getSMTPObject() 424 | { 425 | if (is_object($this->_smtp) !== false) { 426 | return $this->_smtp; 427 | } 428 | 429 | include_once 'Net/SMTP.php'; 430 | $this->_smtp = new Net_SMTP($this->host, 431 | $this->port, 432 | $this->localhost, 433 | $this->pipelining, 434 | 0, 435 | $this->socket_options); 436 | 437 | /* If we still don't have an SMTP object at this point, fail. */ 438 | if (is_object($this->_smtp) === false) { 439 | return PEAR::raiseError('Failed to create a Net_SMTP object', 440 | PEAR_MAIL_SMTP_ERROR_CREATE); 441 | } 442 | 443 | /* Configure the SMTP connection. */ 444 | if ($this->debug) { 445 | $this->_smtp->setDebug(true, $this->debug_handler); 446 | } 447 | 448 | /* Attempt to connect to the configured SMTP server. */ 449 | if (PEAR::isError($res = $this->_smtp->connect($this->timeout))) { 450 | $error = $this->_error('Failed to connect to ' . 451 | $this->host . ':' . $this->port, 452 | $res); 453 | return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_CONNECT); 454 | } 455 | 456 | /* Attempt to authenticate if authentication has been enabled. */ 457 | if ($this->auth) { 458 | $method = is_string($this->auth) ? $this->auth : ''; 459 | 460 | $tls = $this->starttls === false ? false : true; 461 | 462 | if (PEAR::isError($res = $this->_smtp->auth($this->username, 463 | $this->password, 464 | $method, 465 | $tls))) { 466 | $error = $this->_error("$method authentication failure", 467 | $res); 468 | $this->_smtp->rset(); 469 | return PEAR::raiseError($error, PEAR_MAIL_SMTP_ERROR_AUTH); 470 | } 471 | } 472 | 473 | /* Attempt to establish a TLS encrypted connection. PEAR/Net_SMTP >= 1.10.0 required. */ 474 | if ($this->starttls && !$this->auth) { 475 | $starttls = $this->_smtp->starttls(); 476 | if (PEAR::isError($starttls)) { 477 | return PEAR::raiseError($starttls); 478 | } elseif ($starttls === false) { 479 | return PEAR::raiseError('STARTTLS failed'); 480 | } 481 | } 482 | 483 | return $this->_smtp; 484 | } 485 | 486 | /** 487 | * Add parameter associated with a SMTP service extension. 488 | * 489 | * @param string Extension keyword. 490 | * @param string Any value the keyword needs. 491 | * 492 | * @since 1.2.0 493 | */ 494 | public function addServiceExtensionParameter($keyword, $value = null) 495 | { 496 | $this->_extparams[$keyword] = $value; 497 | } 498 | 499 | /** 500 | * Disconnect and destroy the current SMTP connection. 501 | * 502 | * @return boolean True if the SMTP connection no longer exists. 503 | * 504 | * @since 1.1.9 505 | */ 506 | public function disconnect() 507 | { 508 | /* If we have an SMTP object, disconnect and destroy it. */ 509 | if (is_object($this->_smtp) && $this->_smtp->disconnect()) { 510 | $this->_smtp = null; 511 | } 512 | 513 | /* We are disconnected if we no longer have an SMTP object. */ 514 | return ($this->_smtp === null); 515 | } 516 | 517 | /** 518 | * Returns the SMTP response message after sending. 519 | * 520 | * @return string SMTP response message or NULL. 521 | * 522 | * @since 1.6.0 523 | */ 524 | public function getResponse() 525 | { 526 | return $this->response; 527 | } 528 | 529 | /** 530 | * Returns the SMTP response message if includes "queue" after sending. 531 | * 532 | * @return string SMTP queue message or NULL. 533 | * 534 | * @since 1.6.0 535 | */ 536 | public function getQueuedAs() 537 | { 538 | return $this->queued_as; 539 | } 540 | 541 | /** 542 | * Build a standardized string describing the current SMTP error. 543 | * 544 | * @param string $text Custom string describing the error context. 545 | * @param object $error Reference to the current PEAR_Error object. 546 | * 547 | * @return string A string describing the current SMTP error. 548 | * 549 | * @since 1.1.7 550 | */ 551 | protected function _error($text, $error) 552 | { 553 | /* Split the SMTP response into a code and a response string. */ 554 | list($code, $response) = $this->_smtp->getResponse(); 555 | 556 | /* Build our standardized error string. */ 557 | return $text 558 | . ' [SMTP: ' . $error->getMessage() 559 | . " (code: $code, response: $response)]"; 560 | } 561 | 562 | } 563 | -------------------------------------------------------------------------------- /Mail/smtpmx.php: -------------------------------------------------------------------------------- 1 | 46 | * @copyright 2010-2017 gERD Schaufelberger 47 | * @license http://opensource.org/licenses/BSD-3-Clause New BSD License 48 | * @version CVS: $Id$ 49 | * @link http://pear.php.net/package/Mail/ 50 | */ 51 | 52 | require_once 'Net/SMTP.php'; 53 | 54 | /** 55 | * SMTP MX implementation of the PEAR Mail interface. Requires the Net_SMTP class. 56 | * 57 | * 58 | * @access public 59 | * @author gERD Schaufelberger 60 | * @package Mail 61 | * @version $Revision$ 62 | */ 63 | class Mail_smtpmx extends Mail { 64 | 65 | /** 66 | * SMTP connection object. 67 | * 68 | * @var object 69 | * @access private 70 | */ 71 | var $_smtp = null; 72 | 73 | /** 74 | * The port the SMTP server is on. 75 | * @var integer 76 | * @see getservicebyname() 77 | */ 78 | var $port = 25; 79 | 80 | /** 81 | * Hostname or domain that will be sent to the remote SMTP server in the 82 | * HELO / EHLO message. 83 | * 84 | * @var string 85 | * @see posix_uname() 86 | */ 87 | var $mailname = 'localhost'; 88 | 89 | /** 90 | * SMTP connection timeout value. NULL indicates no timeout. 91 | * 92 | * @var integer 93 | */ 94 | var $timeout = 10; 95 | 96 | /** 97 | * use either PEAR:Net_DNS or getmxrr 98 | * 99 | * @var boolean 100 | */ 101 | var $withNetDns = true; 102 | 103 | /** 104 | * PEAR:Net_DNS_Resolver 105 | * 106 | * @var object 107 | */ 108 | var $resolver; 109 | 110 | /** 111 | * Whether to use VERP or not. If not a boolean, the string value 112 | * will be used as the VERP separators. 113 | * 114 | * @var mixed boolean or string 115 | */ 116 | var $verp = false; 117 | 118 | /** 119 | * Whether to use VRFY or not. 120 | * 121 | * @var boolean $vrfy 122 | */ 123 | var $vrfy = false; 124 | 125 | /** 126 | * Switch to test mode - don't send emails for real 127 | * 128 | * @var boolean $test 129 | */ 130 | var $test = false; 131 | 132 | /** 133 | * Turn on Net_SMTP debugging? 134 | * 135 | * @var boolean $debug 136 | */ 137 | var $debug = false; 138 | 139 | /** 140 | * Set debug_handler on Net_SMTP 141 | * 142 | * @var string $debug_handler 143 | */ 144 | var $debug_handler = null; 145 | 146 | /** 147 | * internal error codes 148 | * 149 | * translate internal error identifier to PEAR-Error codes and human 150 | * readable messages. 151 | * 152 | * @var array $errorCode 153 | * @todo as I need unique error-codes to identify what exactly went wrond 154 | * I did not use intergers as it should be. Instead I added a "namespace" 155 | * for each code. This avoids conflicts with error codes from different 156 | * classes. How can I use unique error codes and stay conform with PEAR? 157 | */ 158 | var $errorCode = array( 159 | 'not_connected' => array( 160 | 'code' => 1, 161 | 'msg' => 'Could not connect to any mail server ({HOST}) at port {PORT} to send mail to {RCPT}.' 162 | ), 163 | 'failed_vrfy_rcpt' => array( 164 | 'code' => 2, 165 | 'msg' => 'Recipient "{RCPT}" could not be veryfied.' 166 | ), 167 | 'failed_set_from' => array( 168 | 'code' => 3, 169 | 'msg' => 'Failed to set sender: {FROM}.' 170 | ), 171 | 'failed_set_rcpt' => array( 172 | 'code' => 4, 173 | 'msg' => 'Failed to set recipient: {RCPT}.' 174 | ), 175 | 'failed_send_data' => array( 176 | 'code' => 5, 177 | 'msg' => 'Failed to send mail to: {RCPT}.' 178 | ), 179 | 'no_from' => array( 180 | 'code' => 5, 181 | 'msg' => 'No from address has be provided.' 182 | ), 183 | 'send_data' => array( 184 | 'code' => 7, 185 | 'msg' => 'Failed to create Net_SMTP object.' 186 | ), 187 | 'no_mx' => array( 188 | 'code' => 8, 189 | 'msg' => 'No MX-record for {RCPT} found.' 190 | ), 191 | 'no_resolver' => array( 192 | 'code' => 9, 193 | 'msg' => 'Could not start resolver! Install PEAR:Net_DNS or switch off "netdns"' 194 | ), 195 | 'failed_rset' => array( 196 | 'code' => 10, 197 | 'msg' => 'RSET command failed, SMTP-connection corrupt.' 198 | ), 199 | ); 200 | 201 | /** 202 | * Constructor. 203 | * 204 | * Instantiates a new Mail_smtp:: object based on the parameters 205 | * passed in. It looks for the following parameters: 206 | * mailname The name of the local mail system (a valid hostname which matches the reverse lookup) 207 | * port smtp-port - the default comes from getservicebyname() and should work fine 208 | * timeout The SMTP connection timeout. Defaults to 30 seconds. 209 | * vrfy Whether to use VRFY or not. Defaults to false. 210 | * verp Whether to use VERP or not. Defaults to false. 211 | * test Activate test mode? Defaults to false. 212 | * debug Activate SMTP and Net_DNS debug mode? Defaults to false. 213 | * debug_handler Set SMTP debug handler function. Defaults to null. 214 | * netdns whether to use PEAR:Net_DNS or the PHP build in function getmxrr, default is true 215 | * 216 | * If a parameter is present in the $params array, it replaces the 217 | * default. 218 | * 219 | * @access public 220 | * @param array Hash containing any parameters different from the 221 | * defaults. 222 | * @see _Mail_smtpmx() 223 | */ 224 | function __construct($params) 225 | { 226 | if (isset($params['mailname'])) { 227 | $this->mailname = $params['mailname']; 228 | } else { 229 | // try to find a valid mailname 230 | if (function_exists('posix_uname')) { 231 | $uname = posix_uname(); 232 | $this->mailname = $uname['nodename']; 233 | } 234 | } 235 | 236 | // port number 237 | if (isset($params['port'])) { 238 | $this->port = $params['port']; 239 | } else { 240 | $this->port = getservbyname('smtp', 'tcp'); 241 | } 242 | 243 | if (isset($params['timeout'])) $this->timeout = $params['timeout']; 244 | if (isset($params['vrfy'])) $this->vrfy = (bool)$params['vrfy']; 245 | if (isset($params['verp'])) $this->verp = $params['verp']; 246 | if (isset($params['test'])) $this->test = (bool)$params['test']; 247 | if (isset($params['peardebug'])) $this->debug = (bool)$params['peardebug']; 248 | if (isset($params['debug'])) $this->debug = (bool)$params['debug']; 249 | if (isset($params['debug_handler'])) $this->debug_handler = $params['debug_handler']; 250 | if (isset($params['netdns'])) $this->withNetDns = $params['netdns']; 251 | } 252 | 253 | /** 254 | * Constructor wrapper for PHP4 255 | * 256 | * @access public 257 | * @param array Hash containing any parameters different from the defaults 258 | * @see __construct() 259 | */ 260 | function Mail_smtpmx($params) 261 | { 262 | $this->__construct($params); 263 | register_shutdown_function(array(&$this, '__destruct')); 264 | } 265 | 266 | /** 267 | * Destructor implementation to ensure that we disconnect from any 268 | * potentially-alive persistent SMTP connections. 269 | */ 270 | function __destruct() 271 | { 272 | if (is_object($this->_smtp)) { 273 | $this->_smtp->disconnect(); 274 | $this->_smtp = null; 275 | } 276 | } 277 | 278 | /** 279 | * Implements Mail::send() function using SMTP direct delivery 280 | * 281 | * @access public 282 | * @param mixed $recipients in RFC822 style or array 283 | * @param array $headers The array of headers to send with the mail. 284 | * @param string $body The full text of the message body, 285 | * @return mixed Returns true on success, or a PEAR_Error 286 | */ 287 | function send($recipients, $headers, $body) 288 | { 289 | if (!is_array($headers)) { 290 | return PEAR::raiseError('$headers must be an array'); 291 | } 292 | 293 | $result = $this->_sanitizeHeaders($headers); 294 | if (is_a($result, 'PEAR_Error')) { 295 | return $result; 296 | } 297 | 298 | // Prepare headers 299 | $headerElements = $this->prepareHeaders($headers); 300 | if (is_a($headerElements, 'PEAR_Error')) { 301 | return $headerElements; 302 | } 303 | list($from, $textHeaders) = $headerElements; 304 | 305 | // use 'Return-Path' if possible 306 | if (!empty($headers['Return-Path'])) { 307 | $from = $headers['Return-Path']; 308 | } 309 | if (!isset($from)) { 310 | return $this->_raiseError('no_from'); 311 | } 312 | 313 | // Prepare recipients 314 | $recipients = $this->parseRecipients($recipients); 315 | if (is_a($recipients, 'PEAR_Error')) { 316 | return $recipients; 317 | } 318 | 319 | foreach ($recipients as $rcpt) { 320 | list($user, $host) = explode('@', $rcpt); 321 | 322 | $mx = $this->_getMx($host); 323 | if (is_a($mx, 'PEAR_Error')) { 324 | return $mx; 325 | } 326 | 327 | if (empty($mx)) { 328 | $info = array('rcpt' => $rcpt); 329 | return $this->_raiseError('no_mx', $info); 330 | } 331 | 332 | $connected = false; 333 | foreach ($mx as $mserver => $mpriority) { 334 | $this->_smtp = new Net_SMTP($mserver, $this->port, $this->mailname); 335 | 336 | // configure the SMTP connection. 337 | if ($this->debug) { 338 | $this->_smtp->setDebug(true, $this->debug_handler); 339 | } 340 | 341 | // attempt to connect to the configured SMTP server. 342 | $res = $this->_smtp->connect($this->timeout); 343 | if (is_a($res, 'PEAR_Error')) { 344 | $this->_smtp = null; 345 | continue; 346 | } 347 | 348 | // connection established 349 | if ($res) { 350 | $connected = true; 351 | break; 352 | } 353 | } 354 | 355 | if (!$connected) { 356 | $info = array( 357 | 'host' => implode(', ', array_keys($mx)), 358 | 'port' => $this->port, 359 | 'rcpt' => $rcpt, 360 | ); 361 | return $this->_raiseError('not_connected', $info); 362 | } 363 | 364 | // Verify recipient 365 | if ($this->vrfy) { 366 | $res = $this->_smtp->vrfy($rcpt); 367 | if (is_a($res, 'PEAR_Error')) { 368 | $info = array('rcpt' => $rcpt); 369 | return $this->_raiseError('failed_vrfy_rcpt', $info); 370 | } 371 | } 372 | 373 | // mail from: 374 | $args['verp'] = $this->verp; 375 | $res = $this->_smtp->mailFrom($from, $args); 376 | if (is_a($res, 'PEAR_Error')) { 377 | $info = array('from' => $from); 378 | return $this->_raiseError('failed_set_from', $info); 379 | } 380 | 381 | // rcpt to: 382 | $res = $this->_smtp->rcptTo($rcpt); 383 | if (is_a($res, 'PEAR_Error')) { 384 | $info = array('rcpt' => $rcpt); 385 | return $this->_raiseError('failed_set_rcpt', $info); 386 | } 387 | 388 | // Don't send anything in test mode 389 | if ($this->test) { 390 | $result = $this->_smtp->rset(); 391 | $res = $this->_smtp->rset(); 392 | if (is_a($res, 'PEAR_Error')) { 393 | return $this->_raiseError('failed_rset'); 394 | } 395 | 396 | $this->_smtp->disconnect(); 397 | $this->_smtp = null; 398 | return true; 399 | } 400 | 401 | // Send data 402 | $res = $this->_smtp->data($body, $textHeaders); 403 | if (is_a($res, 'PEAR_Error')) { 404 | $info = array('rcpt' => $rcpt); 405 | return $this->_raiseError('failed_send_data', $info); 406 | } 407 | 408 | $this->_smtp->disconnect(); 409 | $this->_smtp = null; 410 | } 411 | 412 | return true; 413 | } 414 | 415 | /** 416 | * Recieve mx rexords for a spciefied host 417 | * 418 | * The MX records 419 | * 420 | * @access private 421 | * @param string $host mail host 422 | * @return mixed sorted 423 | */ 424 | function _getMx($host) 425 | { 426 | $mx = array(); 427 | 428 | if ($this->withNetDns) { 429 | $res = $this->_loadNetDns(); 430 | if (is_a($res, 'PEAR_Error')) { 431 | return $res; 432 | } 433 | 434 | $response = $this->resolver->query($host, 'MX'); 435 | if (!$response) { 436 | return false; 437 | } 438 | 439 | foreach ($response->answer as $rr) { 440 | if ($rr->type == 'MX') { 441 | $mx[$rr->exchange] = $rr->preference; 442 | } 443 | } 444 | } else { 445 | $mxHost = array(); 446 | $mxWeight = array(); 447 | 448 | if (!getmxrr($host, $mxHost, $mxWeight)) { 449 | return false; 450 | } 451 | for ($i = 0; $i < count($mxHost); ++$i) { 452 | $mx[$mxHost[$i]] = $mxWeight[$i]; 453 | } 454 | } 455 | 456 | asort($mx); 457 | return $mx; 458 | } 459 | 460 | /** 461 | * initialize PEAR:Net_DNS_Resolver 462 | * 463 | * @access private 464 | * @return boolean true on success 465 | */ 466 | function _loadNetDns() 467 | { 468 | if (is_object($this->resolver)) { 469 | return true; 470 | } 471 | 472 | if (!include_once 'Net/DNS.php') { 473 | return $this->_raiseError('no_resolver'); 474 | } 475 | 476 | $this->resolver = new Net_DNS_Resolver(); 477 | if ($this->debug) { 478 | $this->resolver->debug = 1; 479 | } 480 | 481 | return true; 482 | } 483 | 484 | /** 485 | * raise standardized error 486 | * 487 | * include additional information in error message 488 | * 489 | * @access private 490 | * @param string $id maps error ids to codes and message 491 | * @param array $info optional information in associative array 492 | * @see _errorCode 493 | */ 494 | function _raiseError($id, $info = array()) 495 | { 496 | $code = $this->errorCode[$id]['code']; 497 | $msg = $this->errorCode[$id]['msg']; 498 | 499 | // include info to messages 500 | if (!empty($info)) { 501 | $search = array(); 502 | $replace = array(); 503 | 504 | foreach ($info as $key => $value) { 505 | array_push($search, '{' . strtoupper($key) . '}'); 506 | array_push($replace, $value); 507 | } 508 | 509 | $msg = str_replace($search, $replace, $msg); 510 | } 511 | 512 | return PEAR::raiseError($msg, $code); 513 | } 514 | 515 | } 516 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | **** 2 | Mail 3 | **** 4 | Class that provides multiple interfaces for sending emails. 5 | 6 | PEAR's Mail package defines an interface for implementing mailers under the 7 | PEAR hierarchy. 8 | It also provides supporting functions useful to multiple mailer backends. 9 | 10 | Currently supported backends include: 11 | 12 | - PHP's native ``mail()`` function 13 | - sendmail 14 | - SMTP 15 | 16 | This package also provides a `RFC 822`__ email address list validation utility class. 17 | 18 | Use Mail in combination with `Mail_Mime`__ to send HTML emails or emails with 19 | attachments - have a look at the example__. 20 | 21 | __ https://tools.ietf.org/html/rfc822 22 | __ http://pear.php.net/package/Mail_Mime 23 | __ http://pear.php.net/manual/en/package.mail.mail-mime.example.php 24 | 25 | ============ 26 | Installation 27 | ============ 28 | 29 | PEAR 30 | ==== 31 | :: 32 | 33 | $ pear install mail 34 | 35 | Composer 36 | ======== 37 | :: 38 | 39 | $ composer require pear/mail 40 | 41 | ===== 42 | Links 43 | ===== 44 | Homepage 45 | http://pear.php.net/package/Mail 46 | Source code 47 | https://github.com/pear/Mail 48 | Issue tracker 49 | http://pear.php.net/bugs/search.php?cmd=display&package_name[]=Mail 50 | Unit test status 51 | https://travis-ci.org/pear/Mail 52 | Packagist 53 | https://packagist.org/packages/pear/mail 54 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | { 4 | "email": "chuck@horde.org", 5 | "name": "Chuck Hagenbuch", 6 | "role": "Lead" 7 | }, 8 | { 9 | "email": "schengawegga@gmail.com", 10 | "name": "Armin Graefe", 11 | "role": "Lead" 12 | }, 13 | { 14 | "email": "richard@phpguru.org", 15 | "name": "Richard Heyes", 16 | "role": "Developer" 17 | }, 18 | { 19 | "email": "alec@alec.pl", 20 | "name": "Aleksander Machniak", 21 | "role": "Developer" 22 | } 23 | ], 24 | "autoload": { 25 | "psr-0": { 26 | "Mail": "./" 27 | } 28 | }, 29 | "description": "Class that provides multiple interfaces for sending emails.", 30 | "homepage": "http://pear.php.net/package/Mail", 31 | "include-path": [ 32 | "./" 33 | ], 34 | "license": "BSD-3-Clause", 35 | "name": "pear/mail", 36 | "support": { 37 | "issues": "http://pear.php.net/bugs/search.php?cmd=display&package_name[]=Mail", 38 | "source": "https://github.com/pear/Mail" 39 | }, 40 | "type": "library", 41 | "require": { 42 | "php": ">=5.2.1", 43 | "pear/pear-core-minimal": "~1.9" 44 | }, 45 | "require-dev": { 46 | "pear/pear": "*" 47 | }, 48 | "suggest": { 49 | "pear/net_smtp": "Install optionally via your project's composer.json" 50 | } 51 | } 52 | --------------------------------------------------------------------------------