1<?php 2/** 3 * CodeIgniter 4 * 5 * An open source application development framework for PHP 6 * 7 * This content is released under the MIT License (MIT) 8 * 9 * Copyright (c) 2014 - 2019, British Columbia Institute of Technology 10 * 11 * Permission is hereby granted, free of charge, to any person obtaining a copy 12 * of this software and associated documentation files (the "Software"), to deal 13 * in the Software without restriction, including without limitation the rights 14 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 15 * copies of the Software, and to permit persons to whom the Software is 16 * furnished to do so, subject to the following conditions: 17 * 18 * The above copyright notice and this permission notice shall be included in 19 * all copies or substantial portions of the Software. 20 * 21 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 24 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 25 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 26 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 27 * THE SOFTWARE. 28 * 29 * @package CodeIgniter 30 * @author EllisLab Dev Team 31 * @copyright Copyright (c) 2008 - 2014, EllisLab, Inc. (https://ellislab.com/) 32 * @copyright Copyright (c) 2014 - 2019, British Columbia Institute of Technology (https://bcit.ca/) 33 * @license https://opensource.org/licenses/MIT MIT License 34 * @link https://codeigniter.com 35 * @since Version 1.0.0 36 * @filesource 37 */ 38defined('BASEPATH') OR exit('No direct script access allowed'); 39 40/** 41 * CodeIgniter Email Class 42 * 43 * Permits email to be sent using Mail, Sendmail, or SMTP. 44 * 45 * @package CodeIgniter 46 * @subpackage Libraries 47 * @category Libraries 48 * @author EllisLab Dev Team 49 * @link https://codeigniter.com/user_guide/libraries/email.html 50 */ 51class CI_Email { 52 53 /** 54 * Used as the User-Agent and X-Mailer headers' value. 55 * 56 * @var string 57 */ 58 public $useragent = 'CodeIgniter'; 59 60 /** 61 * Path to the Sendmail binary. 62 * 63 * @var string 64 */ 65 public $mailpath = '/usr/sbin/sendmail'; // Sendmail path 66 67 /** 68 * Which method to use for sending e-mails. 69 * 70 * @var string 'mail', 'sendmail' or 'smtp' 71 */ 72 public $protocol = 'mail'; // mail/sendmail/smtp 73 74 /** 75 * STMP Server host 76 * 77 * @var string 78 */ 79 public $smtp_host = ''; 80 81 /** 82 * SMTP Username 83 * 84 * @var string 85 */ 86 public $smtp_user = ''; 87 88 /** 89 * SMTP Password 90 * 91 * @var string 92 */ 93 public $smtp_pass = ''; 94 95 /** 96 * SMTP Server port 97 * 98 * @var int 99 */ 100 public $smtp_port = 25; 101 102 /** 103 * SMTP connection timeout in seconds 104 * 105 * @var int 106 */ 107 public $smtp_timeout = 5; 108 109 /** 110 * SMTP persistent connection 111 * 112 * @var bool 113 */ 114 public $smtp_keepalive = FALSE; 115 116 /** 117 * SMTP Encryption 118 * 119 * @var string empty, 'tls' or 'ssl' 120 */ 121 public $smtp_crypto = ''; 122 123 /** 124 * Whether to apply word-wrapping to the message body. 125 * 126 * @var bool 127 */ 128 public $wordwrap = TRUE; 129 130 /** 131 * Number of characters to wrap at. 132 * 133 * @see CI_Email::$wordwrap 134 * @var int 135 */ 136 public $wrapchars = 76; 137 138 /** 139 * Message format. 140 * 141 * @var string 'text' or 'html' 142 */ 143 public $mailtype = 'text'; 144 145 /** 146 * Character set (default: utf-8) 147 * 148 * @var string 149 */ 150 public $charset = 'UTF-8'; 151 152 /** 153 * Alternative message (for HTML messages only) 154 * 155 * @var string 156 */ 157 public $alt_message = ''; 158 159 /** 160 * Whether to validate e-mail addresses. 161 * 162 * @var bool 163 */ 164 public $validate = FALSE; 165 166 /** 167 * X-Priority header value. 168 * 169 * @var int 1-5 170 */ 171 public $priority = 3; // Default priority (1 - 5) 172 173 /** 174 * Newline character sequence. 175 * Use "\r\n" to comply with RFC 822. 176 * 177 * @link http://www.ietf.org/rfc/rfc822.txt 178 * @var string "\r\n" or "\n" 179 */ 180 public $newline = "\n"; // Default newline. "\r\n" or "\n" (Use "\r\n" to comply with RFC 822) 181 182 /** 183 * CRLF character sequence 184 * 185 * RFC 2045 specifies that for 'quoted-printable' encoding, 186 * "\r\n" must be used. However, it appears that some servers 187 * (even on the receiving end) don't handle it properly and 188 * switching to "\n", while improper, is the only solution 189 * that seems to work for all environments. 190 * 191 * @link http://www.ietf.org/rfc/rfc822.txt 192 * @var string 193 */ 194 public $crlf = "\n"; 195 196 /** 197 * Whether to use Delivery Status Notification. 198 * 199 * @var bool 200 */ 201 public $dsn = FALSE; 202 203 /** 204 * Whether to send multipart alternatives. 205 * Yahoo! doesn't seem to like these. 206 * 207 * @var bool 208 */ 209 public $send_multipart = TRUE; 210 211 /** 212 * Whether to send messages to BCC recipients in batches. 213 * 214 * @var bool 215 */ 216 public $bcc_batch_mode = FALSE; 217 218 /** 219 * BCC Batch max number size. 220 * 221 * @see CI_Email::$bcc_batch_mode 222 * @var int 223 */ 224 public $bcc_batch_size = 200; 225 226 // -------------------------------------------------------------------- 227 228 /** 229 * Whether PHP is running in safe mode. Initialized by the class constructor. 230 * 231 * @var bool 232 */ 233 protected $_safe_mode = FALSE; 234 235 /** 236 * Subject header 237 * 238 * @var string 239 */ 240 protected $_subject = ''; 241 242 /** 243 * Message body 244 * 245 * @var string 246 */ 247 protected $_body = ''; 248 249 /** 250 * Final message body to be sent. 251 * 252 * @var string 253 */ 254 protected $_finalbody = ''; 255 256 /** 257 * Final headers to send 258 * 259 * @var string 260 */ 261 protected $_header_str = ''; 262 263 /** 264 * SMTP Connection socket placeholder 265 * 266 * @var resource 267 */ 268 protected $_smtp_connect = ''; 269 270 /** 271 * Mail encoding 272 * 273 * @var string '8bit' or '7bit' 274 */ 275 protected $_encoding = '8bit'; 276 277 /** 278 * Whether to perform SMTP authentication 279 * 280 * @var bool 281 */ 282 protected $_smtp_auth = FALSE; 283 284 /** 285 * Whether to send a Reply-To header 286 * 287 * @var bool 288 */ 289 protected $_replyto_flag = FALSE; 290 291 /** 292 * Debug messages 293 * 294 * @see CI_Email::print_debugger() 295 * @var string 296 */ 297 protected $_debug_msg = array(); 298 299 /** 300 * Recipients 301 * 302 * @var string[] 303 */ 304 protected $_recipients = array(); 305 306 /** 307 * CC Recipients 308 * 309 * @var string[] 310 */ 311 protected $_cc_array = array(); 312 313 /** 314 * BCC Recipients 315 * 316 * @var string[] 317 */ 318 protected $_bcc_array = array(); 319 320 /** 321 * Message headers 322 * 323 * @var string[] 324 */ 325 protected $_headers = array(); 326 327 /** 328 * Attachment data 329 * 330 * @var array 331 */ 332 protected $_attachments = array(); 333 334 /** 335 * Valid $protocol values 336 * 337 * @see CI_Email::$protocol 338 * @var string[] 339 */ 340 protected $_protocols = array('mail', 'sendmail', 'smtp'); 341 342 /** 343 * Base charsets 344 * 345 * Character sets valid for 7-bit encoding, 346 * excluding language suffix. 347 * 348 * @var string[] 349 */ 350 protected $_base_charsets = array('us-ascii', 'iso-2022-'); 351 352 /** 353 * Bit depths 354 * 355 * Valid mail encodings 356 * 357 * @see CI_Email::$_encoding 358 * @var string[] 359 */ 360 protected $_bit_depths = array('7bit', '8bit'); 361 362 /** 363 * $priority translations 364 * 365 * Actual values to send with the X-Priority header 366 * 367 * @var string[] 368 */ 369 protected $_priorities = array( 370 1 => '1 (Highest)', 371 2 => '2 (High)', 372 3 => '3 (Normal)', 373 4 => '4 (Low)', 374 5 => '5 (Lowest)' 375 ); 376 377 /** 378 * mbstring.func_overload flag 379 * 380 * @var bool 381 */ 382 protected static $func_overload; 383 384 // -------------------------------------------------------------------- 385 386 /** 387 * Constructor - Sets Email Preferences 388 * 389 * The constructor can be passed an array of config values 390 * 391 * @param array $config = array() 392 * @return void 393 */ 394 public function __construct(array $config = array()) 395 { 396 $this->charset = config_item('charset'); 397 $this->initialize($config); 398 $this->_safe_mode = ( ! is_php('5.4') && ini_get('safe_mode')); 399 400 isset(self::$func_overload) OR self::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload')); 401 402 log_message('info', 'Email Class Initialized'); 403 } 404 405 // -------------------------------------------------------------------- 406 407 /** 408 * Initialize preferences 409 * 410 * @param array $config 411 * @return CI_Email 412 */ 413 public function initialize(array $config = array()) 414 { 415 $this->clear(); 416 417 foreach ($config as $key => $val) 418 { 419 if (isset($this->$key)) 420 { 421 $method = 'set_'.$key; 422 423 if (method_exists($this, $method)) 424 { 425 $this->$method($val); 426 } 427 else 428 { 429 $this->$key = $val; 430 } 431 } 432 } 433 434 $this->charset = strtoupper($this->charset); 435 $this->_smtp_auth = isset($this->smtp_user[0], $this->smtp_pass[0]); 436 437 return $this; 438 } 439 440 // -------------------------------------------------------------------- 441 442 /** 443 * Initialize the Email Data 444 * 445 * @param bool 446 * @return CI_Email 447 */ 448 public function clear($clear_attachments = FALSE) 449 { 450 $this->_subject = ''; 451 $this->_body = ''; 452 $this->_finalbody = ''; 453 $this->_header_str = ''; 454 $this->_replyto_flag = FALSE; 455 $this->_recipients = array(); 456 $this->_cc_array = array(); 457 $this->_bcc_array = array(); 458 $this->_headers = array(); 459 $this->_debug_msg = array(); 460 461 $this->set_header('Date', $this->_set_date()); 462 463 if ($clear_attachments !== FALSE) 464 { 465 $this->_attachments = array(); 466 } 467 468 return $this; 469 } 470 471 // -------------------------------------------------------------------- 472 473 /** 474 * Set FROM 475 * 476 * @param string $from 477 * @param string $name 478 * @param string $return_path = NULL Return-Path 479 * @return CI_Email 480 */ 481 public function from($from, $name = '', $return_path = NULL) 482 { 483 if (preg_match('/\<(.*)\>/', $from, $match)) 484 { 485 $from = $match[1]; 486 } 487 488 if ($this->validate) 489 { 490 $this->validate_email($this->_str_to_array($from)); 491 if ($return_path) 492 { 493 $this->validate_email($this->_str_to_array($return_path)); 494 } 495 } 496 497 // prepare the display name 498 if ($name !== '') 499 { 500 // only use Q encoding if there are characters that would require it 501 if ( ! preg_match('/[\200-\377]/', $name)) 502 { 503 // add slashes for non-printing characters, slashes, and double quotes, and surround it in double quotes 504 $name = '"'.addcslashes($name, "\0..\37\177'\"\\").'"'; 505 } 506 else 507 { 508 $name = $this->_prep_q_encoding($name); 509 } 510 } 511 512 $this->set_header('From', $name.' <'.$from.'>'); 513 514 isset($return_path) OR $return_path = $from; 515 $this->set_header('Return-Path', '<'.$return_path.'>'); 516 517 return $this; 518 } 519 520 // -------------------------------------------------------------------- 521 522 /** 523 * Set Reply-to 524 * 525 * @param string 526 * @param string 527 * @return CI_Email 528 */ 529 public function reply_to($replyto, $name = '') 530 { 531 if (preg_match('/\<(.*)\>/', $replyto, $match)) 532 { 533 $replyto = $match[1]; 534 } 535 536 if ($this->validate) 537 { 538 $this->validate_email($this->_str_to_array($replyto)); 539 } 540 541 if ($name !== '') 542 { 543 // only use Q encoding if there are characters that would require it 544 if ( ! preg_match('/[\200-\377]/', $name)) 545 { 546 // add slashes for non-printing characters, slashes, and double quotes, and surround it in double quotes 547 $name = '"'.addcslashes($name, "\0..\37\177'\"\\").'"'; 548 } 549 else 550 { 551 $name = $this->_prep_q_encoding($name); 552 } 553 } 554 555 $this->set_header('Reply-To', $name.' <'.$replyto.'>'); 556 $this->_replyto_flag = TRUE; 557 558 return $this; 559 } 560 561 // -------------------------------------------------------------------- 562 563 /** 564 * Set Recipients 565 * 566 * @param string 567 * @return CI_Email 568 */ 569 public function to($to) 570 { 571 $to = $this->_str_to_array($to); 572 $to = $this->clean_email($to); 573 574 if ($this->validate) 575 { 576 $this->validate_email($to); 577 } 578 579 if ($this->_get_protocol() !== 'mail') 580 { 581 $this->set_header('To', implode(', ', $to)); 582 } 583 584 $this->_recipients = $to; 585 586 return $this; 587 } 588 589 // -------------------------------------------------------------------- 590 591 /** 592 * Set CC 593 * 594 * @param string 595 * @return CI_Email 596 */ 597 public function cc($cc) 598 { 599 $cc = $this->clean_email($this->_str_to_array($cc)); 600 601 if ($this->validate) 602 { 603 $this->validate_email($cc); 604 } 605 606 $this->set_header('Cc', implode(', ', $cc)); 607 608 if ($this->_get_protocol() === 'smtp') 609 { 610 $this->_cc_array = $cc; 611 } 612 613 return $this; 614 } 615 616 // -------------------------------------------------------------------- 617 618 /** 619 * Set BCC 620 * 621 * @param string 622 * @param string 623 * @return CI_Email 624 */ 625 public function bcc($bcc, $limit = '') 626 { 627 if ($limit !== '' && is_numeric($limit)) 628 { 629 $this->bcc_batch_mode = TRUE; 630 $this->bcc_batch_size = $limit; 631 } 632 633 $bcc = $this->clean_email($this->_str_to_array($bcc)); 634 635 if ($this->validate) 636 { 637 $this->validate_email($bcc); 638 } 639 640 if ($this->_get_protocol() === 'smtp' OR ($this->bcc_batch_mode && count($bcc) > $this->bcc_batch_size)) 641 { 642 $this->_bcc_array = $bcc; 643 } 644 else 645 { 646 $this->set_header('Bcc', implode(', ', $bcc)); 647 } 648 649 return $this; 650 } 651 652 // -------------------------------------------------------------------- 653 654 /** 655 * Set Email Subject 656 * 657 * @param string 658 * @return CI_Email 659 */ 660 public function subject($subject) 661 { 662 $subject = $this->_prep_q_encoding($subject); 663 $this->set_header('Subject', $subject); 664 return $this; 665 } 666 667 // -------------------------------------------------------------------- 668 669 /** 670 * Set Body 671 * 672 * @param string 673 * @return CI_Email 674 */ 675 public function message($body) 676 { 677 $this->_body = rtrim(str_replace("\r", '', $body)); 678 679 /* strip slashes only if magic quotes is ON 680 if we do it with magic quotes OFF, it strips real, user-inputted chars. 681 682 NOTE: In PHP 5.4 get_magic_quotes_gpc() will always return 0 and 683 it will probably not exist in future versions at all. 684 */ 685 if ( ! is_php('5.4') && get_magic_quotes_gpc()) 686 { 687 $this->_body = stripslashes($this->_body); 688 } 689 690 return $this; 691 } 692 693 // -------------------------------------------------------------------- 694 695 /** 696 * Assign file attachments 697 * 698 * @param string $file Can be local path, URL or buffered content 699 * @param string $disposition = 'attachment' 700 * @param string $newname = NULL 701 * @param string $mime = '' 702 * @return CI_Email 703 */ 704 public function attach($file, $disposition = '', $newname = NULL, $mime = '') 705 { 706 if ($mime === '') 707 { 708 if (strpos($file, '://') === FALSE && ! file_exists($file)) 709 { 710 $this->_set_error_message('lang:email_attachment_missing', $file); 711 return FALSE; 712 } 713 714 if ( ! $fp = @fopen($file, 'rb')) 715 { 716 $this->_set_error_message('lang:email_attachment_unreadable', $file); 717 return FALSE; 718 } 719 720 $file_content = stream_get_contents($fp); 721 $mime = $this->_mime_types(pathinfo($file, PATHINFO_EXTENSION)); 722 fclose($fp); 723 } 724 else 725 { 726 $file_content =& $file; // buffered file 727 } 728 729 $this->_attachments[] = array( 730 'name' => array($file, $newname), 731 'disposition' => empty($disposition) ? 'attachment' : $disposition, // Can also be 'inline' Not sure if it matters 732 'type' => $mime, 733 'content' => chunk_split(base64_encode($file_content)), 734 'multipart' => 'mixed' 735 ); 736 737 return $this; 738 } 739 740 // -------------------------------------------------------------------- 741 742 /** 743 * Set and return attachment Content-ID 744 * 745 * Useful for attached inline pictures 746 * 747 * @param string $filename 748 * @return string 749 */ 750 public function attachment_cid($filename) 751 { 752 for ($i = 0, $c = count($this->_attachments); $i < $c; $i++) 753 { 754 if ($this->_attachments[$i]['name'][0] === $filename) 755 { 756 $this->_attachments[$i]['multipart'] = 'related'; 757 $this->_attachments[$i]['cid'] = uniqid(basename($this->_attachments[$i]['name'][0]).'@'); 758 return $this->_attachments[$i]['cid']; 759 } 760 } 761 762 return FALSE; 763 } 764 765 // -------------------------------------------------------------------- 766 767 /** 768 * Add a Header Item 769 * 770 * @param string 771 * @param string 772 * @return CI_Email 773 */ 774 public function set_header($header, $value) 775 { 776 $this->_headers[$header] = str_replace(array("\n", "\r"), '', $value); 777 return $this; 778 } 779 780 // -------------------------------------------------------------------- 781 782 /** 783 * Convert a String to an Array 784 * 785 * @param string 786 * @return array 787 */ 788 protected function _str_to_array($email) 789 { 790 if ( ! is_array($email)) 791 { 792 return (strpos($email, ',') !== FALSE) 793 ? preg_split('/[\s,]/', $email, -1, PREG_SPLIT_NO_EMPTY) 794 : (array) trim($email); 795 } 796 797 return $email; 798 } 799 800 // -------------------------------------------------------------------- 801 802 /** 803 * Set Multipart Value 804 * 805 * @param string 806 * @return CI_Email 807 */ 808 public function set_alt_message($str) 809 { 810 $this->alt_message = (string) $str; 811 return $this; 812 } 813 814 // -------------------------------------------------------------------- 815 816 /** 817 * Set Mailtype 818 * 819 * @param string 820 * @return CI_Email 821 */ 822 public function set_mailtype($type = 'text') 823 { 824 $this->mailtype = ($type === 'html') ? 'html' : 'text'; 825 return $this; 826 } 827 828 // -------------------------------------------------------------------- 829 830 /** 831 * Set Wordwrap 832 * 833 * @param bool 834 * @return CI_Email 835 */ 836 public function set_wordwrap($wordwrap = TRUE) 837 { 838 $this->wordwrap = (bool) $wordwrap; 839 return $this; 840 } 841 842 // -------------------------------------------------------------------- 843 844 /** 845 * Set Protocol 846 * 847 * @param string 848 * @return CI_Email 849 */ 850 public function set_protocol($protocol = 'mail') 851 { 852 $this->protocol = in_array($protocol, $this->_protocols, TRUE) ? strtolower($protocol) : 'mail'; 853 return $this; 854 } 855 856 // -------------------------------------------------------------------- 857 858 /** 859 * Set Priority 860 * 861 * @param int 862 * @return CI_Email 863 */ 864 public function set_priority($n = 3) 865 { 866 $this->priority = preg_match('/^[1-5]$/', $n) ? (int) $n : 3; 867 return $this; 868 } 869 870 // -------------------------------------------------------------------- 871 872 /** 873 * Set Newline Character 874 * 875 * @param string 876 * @return CI_Email 877 */ 878 public function set_newline($newline = "\n") 879 { 880 $this->newline = in_array($newline, array("\n", "\r\n", "\r")) ? $newline : "\n"; 881 return $this; 882 } 883 884 // -------------------------------------------------------------------- 885 886 /** 887 * Set CRLF 888 * 889 * @param string 890 * @return CI_Email 891 */ 892 public function set_crlf($crlf = "\n") 893 { 894 $this->crlf = ($crlf !== "\n" && $crlf !== "\r\n" && $crlf !== "\r") ? "\n" : $crlf; 895 return $this; 896 } 897 898 // -------------------------------------------------------------------- 899 900 /** 901 * Get the Message ID 902 * 903 * @return string 904 */ 905 protected function _get_message_id() 906 { 907 $from = str_replace(array('>', '<'), '', $this->_headers['Return-Path']); 908 return '<'.uniqid('').strstr($from, '@').'>'; 909 } 910 911 // -------------------------------------------------------------------- 912 913 /** 914 * Get Mail Protocol 915 * 916 * @return mixed 917 */ 918 protected function _get_protocol() 919 { 920 $this->protocol = strtolower($this->protocol); 921 in_array($this->protocol, $this->_protocols, TRUE) OR $this->protocol = 'mail'; 922 return $this->protocol; 923 } 924 925 // -------------------------------------------------------------------- 926 927 /** 928 * Get Mail Encoding 929 * 930 * @return string 931 */ 932 protected function _get_encoding() 933 { 934 in_array($this->_encoding, $this->_bit_depths) OR $this->_encoding = '8bit'; 935 936 foreach ($this->_base_charsets as $charset) 937 { 938 if (strpos($this->charset, $charset) === 0) 939 { 940 $this->_encoding = '7bit'; 941 } 942 } 943 944 return $this->_encoding; 945 } 946 947 // -------------------------------------------------------------------- 948 949 /** 950 * Get content type (text/html/attachment) 951 * 952 * @return string 953 */ 954 protected function _get_content_type() 955 { 956 if ($this->mailtype === 'html') 957 { 958 return empty($this->_attachments) ? 'html' : 'html-attach'; 959 } 960 elseif ($this->mailtype === 'text' && ! empty($this->_attachments)) 961 { 962 return 'plain-attach'; 963 } 964 965 return 'plain'; 966 } 967 968 // -------------------------------------------------------------------- 969 970 /** 971 * Set RFC 822 Date 972 * 973 * @return string 974 */ 975 protected function _set_date() 976 { 977 $timezone = date('Z'); 978 $operator = ($timezone[0] === '-') ? '-' : '+'; 979 $timezone = abs($timezone); 980 $timezone = floor($timezone/3600) * 100 + ($timezone % 3600) / 60; 981 982 return sprintf('%s %s%04d', date('D, j M Y H:i:s'), $operator, $timezone); 983 } 984 985 // -------------------------------------------------------------------- 986 987 /** 988 * Mime message 989 * 990 * @return string 991 */ 992 protected function _get_mime_message() 993 { 994 return 'This is a multi-part message in MIME format.'.$this->newline.'Your email application may not support this format.'; 995 } 996 997 // -------------------------------------------------------------------- 998 999 /** 1000 * Validate Email Address 1001 * 1002 * @param string 1003 * @return bool 1004 */ 1005 public function validate_email($email) 1006 { 1007 if ( ! is_array($email)) 1008 { 1009 $this->_set_error_message('lang:email_must_be_array'); 1010 return FALSE; 1011 } 1012 1013 foreach ($email as $val) 1014 { 1015 if ( ! $this->valid_email($val)) 1016 { 1017 $this->_set_error_message('lang:email_invalid_address', $val); 1018 return FALSE; 1019 } 1020 } 1021 1022 return TRUE; 1023 } 1024 1025 // -------------------------------------------------------------------- 1026 1027 /** 1028 * Email Validation 1029 * 1030 * @param string 1031 * @return bool 1032 */ 1033 public function valid_email($email) 1034 { 1035 if (function_exists('idn_to_ascii') && strpos($email, '@')) 1036 { 1037 list($account, $domain) = explode('@', $email, 2); 1038 $domain = defined('INTL_IDNA_VARIANT_UTS46') 1039 ? idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46) 1040 : idn_to_ascii($domain); 1041 1042 if ($domain !== FALSE) 1043 { 1044 $email = $account.'@'.$domain; 1045 } 1046 } 1047 1048 return (bool) filter_var($email, FILTER_VALIDATE_EMAIL); 1049 } 1050 1051 // -------------------------------------------------------------------- 1052 1053 /** 1054 * Clean Extended Email Address: Joe Smith <joe@smith.com> 1055 * 1056 * @param string 1057 * @return string 1058 */ 1059 public function clean_email($email) 1060 { 1061 if ( ! is_array($email)) 1062 { 1063 return preg_match('/\<(.*)\>/', $email, $match) ? $match[1] : $email; 1064 } 1065 1066 $clean_email = array(); 1067 1068 foreach ($email as $addy) 1069 { 1070 $clean_email[] = preg_match('/\<(.*)\>/', $addy, $match) ? $match[1] : $addy; 1071 } 1072 1073 return $clean_email; 1074 } 1075 1076 // -------------------------------------------------------------------- 1077 1078 /** 1079 * Build alternative plain text message 1080 * 1081 * Provides the raw message for use in plain-text headers of 1082 * HTML-formatted emails. 1083 * If the user hasn't specified his own alternative message 1084 * it creates one by stripping the HTML 1085 * 1086 * @return string 1087 */ 1088 protected function _get_alt_message() 1089 { 1090 if ( ! empty($this->alt_message)) 1091 { 1092 return ($this->wordwrap) 1093 ? $this->word_wrap($this->alt_message, 76) 1094 : $this->alt_message; 1095 } 1096 1097 $body = preg_match('/\<body.*?\>(.*)\<\/body\>/si', $this->_body, $match) ? $match[1] : $this->_body; 1098 $body = str_replace("\t", '', preg_replace('#<!--(.*)--\>#', '', trim(strip_tags($body)))); 1099 1100 for ($i = 20; $i >= 3; $i--) 1101 { 1102 $body = str_replace(str_repeat("\n", $i), "\n\n", $body); 1103 } 1104 1105 // Reduce multiple spaces 1106 $body = preg_replace('| +|', ' ', $body); 1107 1108 return ($this->wordwrap) 1109 ? $this->word_wrap($body, 76) 1110 : $body; 1111 } 1112 1113 // -------------------------------------------------------------------- 1114 1115 /** 1116 * Word Wrap 1117 * 1118 * @param string 1119 * @param int line-length limit 1120 * @return string 1121 */ 1122 public function word_wrap($str, $charlim = NULL) 1123 { 1124 // Set the character limit, if not already present 1125 if (empty($charlim)) 1126 { 1127 $charlim = empty($this->wrapchars) ? 76 : $this->wrapchars; 1128 } 1129 1130 // Standardize newlines 1131 if (strpos($str, "\r") !== FALSE) 1132 { 1133 $str = str_replace(array("\r\n", "\r"), "\n", $str); 1134 } 1135 1136 // Reduce multiple spaces at end of line 1137 $str = preg_replace('| +\n|', "\n", $str); 1138 1139 // If the current word is surrounded by {unwrap} tags we'll 1140 // strip the entire chunk and replace it with a marker. 1141 $unwrap = array(); 1142 if (preg_match_all('|\{unwrap\}(.+?)\{/unwrap\}|s', $str, $matches)) 1143 { 1144 for ($i = 0, $c = count($matches[0]); $i < $c; $i++) 1145 { 1146 $unwrap[] = $matches[1][$i]; 1147 $str = str_replace($matches[0][$i], '{{unwrapped'.$i.'}}', $str); 1148 } 1149 } 1150 1151 // Use PHP's native function to do the initial wordwrap. 1152 // We set the cut flag to FALSE so that any individual words that are 1153 // too long get left alone. In the next step we'll deal with them. 1154 $str = wordwrap($str, $charlim, "\n", FALSE); 1155 1156 // Split the string into individual lines of text and cycle through them 1157 $output = ''; 1158 foreach (explode("\n", $str) as $line) 1159 { 1160 // Is the line within the allowed character count? 1161 // If so we'll join it to the output and continue 1162 if (self::strlen($line) <= $charlim) 1163 { 1164 $output .= $line.$this->newline; 1165 continue; 1166 } 1167 1168 $temp = ''; 1169 do 1170 { 1171 // If the over-length word is a URL we won't wrap it 1172 if (preg_match('!\[url.+\]|://|www\.!', $line)) 1173 { 1174 break; 1175 } 1176 1177 // Trim the word down 1178 $temp .= self::substr($line, 0, $charlim - 1); 1179 $line = self::substr($line, $charlim - 1); 1180 } 1181 while (self::strlen($line) > $charlim); 1182 1183 // If $temp contains data it means we had to split up an over-length 1184 // word into smaller chunks so we'll add it back to our current line 1185 if ($temp !== '') 1186 { 1187 $output .= $temp.$this->newline; 1188 } 1189 1190 $output .= $line.$this->newline; 1191 } 1192 1193 // Put our markers back 1194 if (count($unwrap) > 0) 1195 { 1196 foreach ($unwrap as $key => $val) 1197 { 1198 $output = str_replace('{{unwrapped'.$key.'}}', $val, $output); 1199 } 1200 } 1201 1202 return $output; 1203 } 1204 1205 // -------------------------------------------------------------------- 1206 1207 /** 1208 * Build final headers 1209 * 1210 * @return void 1211 */ 1212 protected function _build_headers() 1213 { 1214 $this->set_header('User-Agent', $this->useragent); 1215 $this->set_header('X-Sender', $this->clean_email($this->_headers['From'])); 1216 $this->set_header('X-Mailer', $this->useragent); 1217 $this->set_header('X-Priority', $this->_priorities[$this->priority]); 1218 $this->set_header('Message-ID', $this->_get_message_id()); 1219 $this->set_header('Mime-Version', '1.0'); 1220 } 1221 1222 // -------------------------------------------------------------------- 1223 1224 /** 1225 * Write Headers as a string 1226 * 1227 * @return void 1228 */ 1229 protected function _write_headers() 1230 { 1231 if ($this->protocol === 'mail') 1232 { 1233 if (isset($this->_headers['Subject'])) 1234 { 1235 $this->_subject = $this->_headers['Subject']; 1236 unset($this->_headers['Subject']); 1237 } 1238 } 1239 1240 reset($this->_headers); 1241 $this->_header_str = ''; 1242 1243 foreach ($this->_headers as $key => $val) 1244 { 1245 $val = trim($val); 1246 1247 if ($val !== '') 1248 { 1249 $this->_header_str .= $key.': '.$val.$this->newline; 1250 } 1251 } 1252 1253 if ($this->_get_protocol() === 'mail') 1254 { 1255 $this->_header_str = rtrim($this->_header_str); 1256 } 1257 } 1258 1259 // -------------------------------------------------------------------- 1260 1261 /** 1262 * Build Final Body and attachments 1263 * 1264 * @return bool 1265 */ 1266 protected function _build_message() 1267 { 1268 if ($this->wordwrap === TRUE && $this->mailtype !== 'html') 1269 { 1270 $this->_body = $this->word_wrap($this->_body); 1271 } 1272 1273 $this->_write_headers(); 1274 1275 $hdr = ($this->_get_protocol() === 'mail') ? $this->newline : ''; 1276 $body = ''; 1277 1278 switch ($this->_get_content_type()) 1279 { 1280 case 'plain': 1281 1282 $hdr .= 'Content-Type: text/plain; charset='.$this->charset.$this->newline 1283 .'Content-Transfer-Encoding: '.$this->_get_encoding(); 1284 1285 if ($this->_get_protocol() === 'mail') 1286 { 1287 $this->_header_str .= $hdr; 1288 $this->_finalbody = $this->_body; 1289 } 1290 else 1291 { 1292 $this->_finalbody = $hdr.$this->newline.$this->newline.$this->_body; 1293 } 1294 1295 return; 1296 1297 case 'html': 1298 1299 if ($this->send_multipart === FALSE) 1300 { 1301 $hdr .= 'Content-Type: text/html; charset='.$this->charset.$this->newline 1302 .'Content-Transfer-Encoding: quoted-printable'; 1303 } 1304 else 1305 { 1306 $boundary = uniqid('B_ALT_'); 1307 $hdr .= 'Content-Type: multipart/alternative; boundary="'.$boundary.'"'; 1308 1309 $body .= $this->_get_mime_message().$this->newline.$this->newline 1310 .'--'.$boundary.$this->newline 1311 1312 .'Content-Type: text/plain; charset='.$this->charset.$this->newline 1313 .'Content-Transfer-Encoding: '.$this->_get_encoding().$this->newline.$this->newline 1314 .$this->_get_alt_message().$this->newline.$this->newline 1315 .'--'.$boundary.$this->newline 1316 1317 .'Content-Type: text/html; charset='.$this->charset.$this->newline 1318 .'Content-Transfer-Encoding: quoted-printable'.$this->newline.$this->newline; 1319 } 1320 1321 $this->_finalbody = $body.$this->_prep_quoted_printable($this->_body).$this->newline.$this->newline; 1322 1323 if ($this->_get_protocol() === 'mail') 1324 { 1325 $this->_header_str .= $hdr; 1326 } 1327 else 1328 { 1329 $this->_finalbody = $hdr.$this->newline.$this->newline.$this->_finalbody; 1330 } 1331 1332 if ($this->send_multipart !== FALSE) 1333 { 1334 $this->_finalbody .= '--'.$boundary.'--'; 1335 } 1336 1337 return; 1338 1339 case 'plain-attach': 1340 1341 $boundary = uniqid('B_ATC_'); 1342 $hdr .= 'Content-Type: multipart/mixed; boundary="'.$boundary.'"'; 1343 1344 if ($this->_get_protocol() === 'mail') 1345 { 1346 $this->_header_str .= $hdr; 1347 } 1348 1349 $body .= $this->_get_mime_message().$this->newline 1350 .$this->newline 1351 .'--'.$boundary.$this->newline 1352 .'Content-Type: text/plain; charset='.$this->charset.$this->newline 1353 .'Content-Transfer-Encoding: '.$this->_get_encoding().$this->newline 1354 .$this->newline 1355 .$this->_body.$this->newline.$this->newline; 1356 1357 $this->_append_attachments($body, $boundary); 1358 1359 break; 1360 case 'html-attach': 1361 1362 $alt_boundary = uniqid('B_ALT_'); 1363 $last_boundary = NULL; 1364 1365 if ($this->_attachments_have_multipart('mixed')) 1366 { 1367 $atc_boundary = uniqid('B_ATC_'); 1368 $hdr .= 'Content-Type: multipart/mixed; boundary="'.$atc_boundary.'"'; 1369 $last_boundary = $atc_boundary; 1370 } 1371 1372 if ($this->_attachments_have_multipart('related')) 1373 { 1374 $rel_boundary = uniqid('B_REL_'); 1375 $rel_boundary_header = 'Content-Type: multipart/related; boundary="'.$rel_boundary.'"'; 1376 1377 if (isset($last_boundary)) 1378 { 1379 $body .= '--'.$last_boundary.$this->newline.$rel_boundary_header; 1380 } 1381 else 1382 { 1383 $hdr .= $rel_boundary_header; 1384 } 1385 1386 $last_boundary = $rel_boundary; 1387 } 1388 1389 if ($this->_get_protocol() === 'mail') 1390 { 1391 $this->_header_str .= $hdr; 1392 } 1393 1394 self::strlen($body) && $body .= $this->newline.$this->newline; 1395 $body .= $this->_get_mime_message().$this->newline.$this->newline 1396 .'--'.$last_boundary.$this->newline 1397 1398 .'Content-Type: multipart/alternative; boundary="'.$alt_boundary.'"'.$this->newline.$this->newline 1399 .'--'.$alt_boundary.$this->newline 1400 1401 .'Content-Type: text/plain; charset='.$this->charset.$this->newline 1402 .'Content-Transfer-Encoding: '.$this->_get_encoding().$this->newline.$this->newline 1403 .$this->_get_alt_message().$this->newline.$this->newline 1404 .'--'.$alt_boundary.$this->newline 1405 1406 .'Content-Type: text/html; charset='.$this->charset.$this->newline 1407 .'Content-Transfer-Encoding: quoted-printable'.$this->newline.$this->newline 1408 1409 .$this->_prep_quoted_printable($this->_body).$this->newline.$this->newline 1410 .'--'.$alt_boundary.'--'.$this->newline.$this->newline; 1411 1412 if ( ! empty($rel_boundary)) 1413 { 1414 $body .= $this->newline.$this->newline; 1415 $this->_append_attachments($body, $rel_boundary, 'related'); 1416 } 1417 1418 // multipart/mixed attachments 1419 if ( ! empty($atc_boundary)) 1420 { 1421 $body .= $this->newline.$this->newline; 1422 $this->_append_attachments($body, $atc_boundary, 'mixed'); 1423 } 1424 1425 break; 1426 } 1427 1428 $this->_finalbody = ($this->_get_protocol() === 'mail') 1429 ? $body 1430 : $hdr.$this->newline.$this->newline.$body; 1431 1432 return TRUE; 1433 } 1434 1435 // -------------------------------------------------------------------- 1436 1437 protected function _attachments_have_multipart($type) 1438 { 1439 foreach ($this->_attachments as &$attachment) 1440 { 1441 if ($attachment['multipart'] === $type) 1442 { 1443 return TRUE; 1444 } 1445 } 1446 1447 return FALSE; 1448 } 1449 1450 // -------------------------------------------------------------------- 1451 1452 /** 1453 * Prepares attachment string 1454 * 1455 * @param string $body Message body to append to 1456 * @param string $boundary Multipart boundary 1457 * @param string $multipart When provided, only attachments of this type will be processed 1458 * @return string 1459 */ 1460 protected function _append_attachments(&$body, $boundary, $multipart = null) 1461 { 1462 for ($i = 0, $c = count($this->_attachments); $i < $c; $i++) 1463 { 1464 if (isset($multipart) && $this->_attachments[$i]['multipart'] !== $multipart) 1465 { 1466 continue; 1467 } 1468 1469 $name = isset($this->_attachments[$i]['name'][1]) 1470 ? $this->_attachments[$i]['name'][1] 1471 : basename($this->_attachments[$i]['name'][0]); 1472 1473 $body .= '--'.$boundary.$this->newline 1474 .'Content-Type: '.$this->_attachments[$i]['type'].'; name="'.$name.'"'.$this->newline 1475 .'Content-Disposition: '.$this->_attachments[$i]['disposition'].';'.$this->newline 1476 .'Content-Transfer-Encoding: base64'.$this->newline 1477 .(empty($this->_attachments[$i]['cid']) ? '' : 'Content-ID: <'.$this->_attachments[$i]['cid'].'>'.$this->newline) 1478 .$this->newline 1479 .$this->_attachments[$i]['content'].$this->newline; 1480 } 1481 1482 // $name won't be set if no attachments were appended, 1483 // and therefore a boundary wouldn't be necessary 1484 empty($name) OR $body .= '--'.$boundary.'--'; 1485 } 1486 1487 // -------------------------------------------------------------------- 1488 1489 /** 1490 * Prep Quoted Printable 1491 * 1492 * Prepares string for Quoted-Printable Content-Transfer-Encoding 1493 * Refer to RFC 2045 http://www.ietf.org/rfc/rfc2045.txt 1494 * 1495 * @param string 1496 * @return string 1497 */ 1498 protected function _prep_quoted_printable($str) 1499 { 1500 // ASCII code numbers for "safe" characters that can always be 1501 // used literally, without encoding, as described in RFC 2049. 1502 // http://www.ietf.org/rfc/rfc2049.txt 1503 static $ascii_safe_chars = array( 1504 // ' ( ) + , - . / : = ? 1505 39, 40, 41, 43, 44, 45, 46, 47, 58, 61, 63, 1506 // numbers 1507 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 1508 // upper-case letters 1509 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 1510 // lower-case letters 1511 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109, 110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122 1512 ); 1513 1514 // We are intentionally wrapping so mail servers will encode characters 1515 // properly and MUAs will behave, so {unwrap} must go! 1516 $str = str_replace(array('{unwrap}', '{/unwrap}'), '', $str); 1517 1518 // RFC 2045 specifies CRLF as "\r\n". 1519 // However, many developers choose to override that and violate 1520 // the RFC rules due to (apparently) a bug in MS Exchange, 1521 // which only works with "\n". 1522 if ($this->crlf === "\r\n") 1523 { 1524 return quoted_printable_encode($str); 1525 } 1526 1527 // Reduce multiple spaces & remove nulls 1528 $str = preg_replace(array('| +|', '/\x00+/'), array(' ', ''), $str); 1529 1530 // Standardize newlines 1531 if (strpos($str, "\r") !== FALSE) 1532 { 1533 $str = str_replace(array("\r\n", "\r"), "\n", $str); 1534 } 1535 1536 $escape = '='; 1537 $output = ''; 1538 1539 foreach (explode("\n", $str) as $line) 1540 { 1541 $length = self::strlen($line); 1542 $temp = ''; 1543 1544 // Loop through each character in the line to add soft-wrap 1545 // characters at the end of a line " =\r\n" and add the newly 1546 // processed line(s) to the output (see comment on $crlf class property) 1547 for ($i = 0; $i < $length; $i++) 1548 { 1549 // Grab the next character 1550 $char = $line[$i]; 1551 $ascii = ord($char); 1552 1553 // Convert spaces and tabs but only if it's the end of the line 1554 if ($ascii === 32 OR $ascii === 9) 1555 { 1556 if ($i === ($length - 1)) 1557 { 1558 $char = $escape.sprintf('%02s', dechex($ascii)); 1559 } 1560 } 1561 // DO NOT move this below the $ascii_safe_chars line! 1562 // 1563 // = (equals) signs are allowed by RFC2049, but must be encoded 1564 // as they are the encoding delimiter! 1565 elseif ($ascii === 61) 1566 { 1567 $char = $escape.strtoupper(sprintf('%02s', dechex($ascii))); // =3D 1568 } 1569 elseif ( ! in_array($ascii, $ascii_safe_chars, TRUE)) 1570 { 1571 $char = $escape.strtoupper(sprintf('%02s', dechex($ascii))); 1572 } 1573 1574 // If we're at the character limit, add the line to the output, 1575 // reset our temp variable, and keep on chuggin' 1576 if ((self::strlen($temp) + self::strlen($char)) >= 76) 1577 { 1578 $output .= $temp.$escape.$this->crlf; 1579 $temp = ''; 1580 } 1581 1582 // Add the character to our temporary line 1583 $temp .= $char; 1584 } 1585 1586 // Add our completed line to the output 1587 $output .= $temp.$this->crlf; 1588 } 1589 1590 // get rid of extra CRLF tacked onto the end 1591 return self::substr($output, 0, self::strlen($this->crlf) * -1); 1592 } 1593 1594 // -------------------------------------------------------------------- 1595 1596 /** 1597 * Prep Q Encoding 1598 * 1599 * Performs "Q Encoding" on a string for use in email headers. 1600 * It's related but not identical to quoted-printable, so it has its 1601 * own method. 1602 * 1603 * @param string 1604 * @return string 1605 */ 1606 protected function _prep_q_encoding($str) 1607 { 1608 $str = str_replace(array("\r", "\n"), '', $str); 1609 1610 if ($this->charset === 'UTF-8') 1611 { 1612 // Note: We used to have mb_encode_mimeheader() as the first choice 1613 // here, but it turned out to be buggy and unreliable. DO NOT 1614 // re-add it! -- Narf 1615 if (ICONV_ENABLED === TRUE) 1616 { 1617 $output = @iconv_mime_encode('', $str, 1618 array( 1619 'scheme' => 'Q', 1620 'line-length' => 76, 1621 'input-charset' => $this->charset, 1622 'output-charset' => $this->charset, 1623 'line-break-chars' => $this->crlf 1624 ) 1625 ); 1626 1627 // There are reports that iconv_mime_encode() might fail and return FALSE 1628 if ($output !== FALSE) 1629 { 1630 // iconv_mime_encode() will always put a header field name. 1631 // We've passed it an empty one, but it still prepends our 1632 // encoded string with ': ', so we need to strip it. 1633 return self::substr($output, 2); 1634 } 1635 1636 $chars = iconv_strlen($str, 'UTF-8'); 1637 } 1638 elseif (MB_ENABLED === TRUE) 1639 { 1640 $chars = mb_strlen($str, 'UTF-8'); 1641 } 1642 } 1643 1644 // We might already have this set for UTF-8 1645 isset($chars) OR $chars = self::strlen($str); 1646 1647 $output = '=?'.$this->charset.'?Q?'; 1648 for ($i = 0, $length = self::strlen($output); $i < $chars; $i++) 1649 { 1650 $chr = ($this->charset === 'UTF-8' && ICONV_ENABLED === TRUE) 1651 ? '='.implode('=', str_split(strtoupper(bin2hex(iconv_substr($str, $i, 1, $this->charset))), 2)) 1652 : '='.strtoupper(bin2hex($str[$i])); 1653 1654 // RFC 2045 sets a limit of 76 characters per line. 1655 // We'll append ?= to the end of each line though. 1656 if ($length + ($l = self::strlen($chr)) > 74) 1657 { 1658 $output .= '?='.$this->crlf // EOL 1659 .' =?'.$this->charset.'?Q?'.$chr; // New line 1660 $length = 6 + self::strlen($this->charset) + $l; // Reset the length for the new line 1661 } 1662 else 1663 { 1664 $output .= $chr; 1665 $length += $l; 1666 } 1667 } 1668 1669 // End the header 1670 return $output.'?='; 1671 } 1672 1673 // -------------------------------------------------------------------- 1674 1675 /** 1676 * Send Email 1677 * 1678 * @param bool $auto_clear = TRUE 1679 * @return bool 1680 */ 1681 public function send($auto_clear = TRUE) 1682 { 1683 if ( ! isset($this->_headers['From'])) 1684 { 1685 $this->_set_error_message('lang:email_no_from'); 1686 return FALSE; 1687 } 1688 1689 if ($this->_replyto_flag === FALSE) 1690 { 1691 $this->reply_to($this->_headers['From']); 1692 } 1693 1694 if ( ! isset($this->_recipients) && ! isset($this->_headers['To']) 1695 && ! isset($this->_bcc_array) && ! isset($this->_headers['Bcc']) 1696 && ! isset($this->_headers['Cc'])) 1697 { 1698 $this->_set_error_message('lang:email_no_recipients'); 1699 return FALSE; 1700 } 1701 1702 $this->_build_headers(); 1703 1704 if ($this->bcc_batch_mode && count($this->_bcc_array) > $this->bcc_batch_size) 1705 { 1706 $result = $this->batch_bcc_send(); 1707 1708 if ($result && $auto_clear) 1709 { 1710 $this->clear(); 1711 } 1712 1713 return $result; 1714 } 1715 1716 if ($this->_build_message() === FALSE) 1717 { 1718 return FALSE; 1719 } 1720 1721 $result = $this->_spool_email(); 1722 1723 if ($result && $auto_clear) 1724 { 1725 $this->clear(); 1726 } 1727 1728 return $result; 1729 } 1730 1731 // -------------------------------------------------------------------- 1732 1733 /** 1734 * Batch Bcc Send. Sends groups of BCCs in batches 1735 * 1736 * @return void 1737 */ 1738 public function batch_bcc_send() 1739 { 1740 $float = $this->bcc_batch_size - 1; 1741 $set = ''; 1742 $chunk = array(); 1743 1744 for ($i = 0, $c = count($this->_bcc_array); $i < $c; $i++) 1745 { 1746 if (isset($this->_bcc_array[$i])) 1747 { 1748 $set .= ', '.$this->_bcc_array[$i]; 1749 } 1750 1751 if ($i === $float) 1752 { 1753 $chunk[] = self::substr($set, 1); 1754 $float += $this->bcc_batch_size; 1755 $set = ''; 1756 } 1757 1758 if ($i === $c-1) 1759 { 1760 $chunk[] = self::substr($set, 1); 1761 } 1762 } 1763 1764 for ($i = 0, $c = count($chunk); $i < $c; $i++) 1765 { 1766 unset($this->_headers['Bcc']); 1767 1768 $bcc = $this->clean_email($this->_str_to_array($chunk[$i])); 1769 1770 if ($this->protocol !== 'smtp') 1771 { 1772 $this->set_header('Bcc', implode(', ', $bcc)); 1773 } 1774 else 1775 { 1776 $this->_bcc_array = $bcc; 1777 } 1778 1779 if ($this->_build_message() === FALSE) 1780 { 1781 return FALSE; 1782 } 1783 1784 $this->_spool_email(); 1785 } 1786 } 1787 1788 // -------------------------------------------------------------------- 1789 1790 /** 1791 * Unwrap special elements 1792 * 1793 * @return void 1794 */ 1795 protected function _unwrap_specials() 1796 { 1797 $this->_finalbody = preg_replace_callback('/\{unwrap\}(.*?)\{\/unwrap\}/si', array($this, '_remove_nl_callback'), $this->_finalbody); 1798 } 1799 1800 // -------------------------------------------------------------------- 1801 1802 /** 1803 * Strip line-breaks via callback 1804 * 1805 * @param string $matches 1806 * @return string 1807 */ 1808 protected function _remove_nl_callback($matches) 1809 { 1810 if (strpos($matches[1], "\r") !== FALSE OR strpos($matches[1], "\n") !== FALSE) 1811 { 1812 $matches[1] = str_replace(array("\r\n", "\r", "\n"), '', $matches[1]); 1813 } 1814 1815 return $matches[1]; 1816 } 1817 1818 // -------------------------------------------------------------------- 1819 1820 /** 1821 * Spool mail to the mail server 1822 * 1823 * @return bool 1824 */ 1825 protected function _spool_email() 1826 { 1827 $this->_unwrap_specials(); 1828 1829 $protocol = $this->_get_protocol(); 1830 $method = '_send_with_'.$protocol; 1831 if ( ! $this->$method()) 1832 { 1833 $this->_set_error_message('lang:email_send_failure_'.($protocol === 'mail' ? 'phpmail' : $protocol)); 1834 return FALSE; 1835 } 1836 1837 $this->_set_error_message('lang:email_sent', $protocol); 1838 return TRUE; 1839 } 1840 1841 // -------------------------------------------------------------------- 1842 1843 /** 1844 * Validate email for shell 1845 * 1846 * Applies stricter, shell-safe validation to email addresses. 1847 * Introduced to prevent RCE via sendmail's -f option. 1848 * 1849 * @see https://github.com/bcit-ci/CodeIgniter/issues/4963 1850 * @see https://gist.github.com/Zenexer/40d02da5e07f151adeaeeaa11af9ab36 1851 * @license https://creativecommons.org/publicdomain/zero/1.0/ CC0 1.0, Public Domain 1852 * 1853 * Credits for the base concept go to Paul Buonopane <paul@namepros.com> 1854 * 1855 * @param string $email 1856 * @return bool 1857 */ 1858 protected function _validate_email_for_shell(&$email) 1859 { 1860 if (function_exists('idn_to_ascii') && strpos($email, '@')) 1861 { 1862 list($account, $domain) = explode('@', $email, 2); 1863 $domain = defined('INTL_IDNA_VARIANT_UTS46') 1864 ? idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46) 1865 : idn_to_ascii($domain); 1866 1867 if ($domain !== FALSE) 1868 { 1869 $email = $account.'@'.$domain; 1870 } 1871 } 1872 1873 return (filter_var($email, FILTER_VALIDATE_EMAIL) === $email && preg_match('#\A[a-z0-9._+-]+@[a-z0-9.-]{1,253}\z#i', $email)); 1874 } 1875 1876 // -------------------------------------------------------------------- 1877 1878 /** 1879 * Send using mail() 1880 * 1881 * @return bool 1882 */ 1883 protected function _send_with_mail() 1884 { 1885 if (is_array($this->_recipients)) 1886 { 1887 $this->_recipients = implode(', ', $this->_recipients); 1888 } 1889 1890 // _validate_email_for_shell() below accepts by reference, 1891 // so this needs to be assigned to a variable 1892 $from = $this->clean_email($this->_headers['Return-Path']); 1893 1894 if ($this->_safe_mode === TRUE || ! $this->_validate_email_for_shell($from)) 1895 { 1896 return mail($this->_recipients, $this->_subject, $this->_finalbody, $this->_header_str); 1897 } 1898 else 1899 { 1900 // most documentation of sendmail using the "-f" flag lacks a space after it, however 1901 // we've encountered servers that seem to require it to be in place. 1902 return mail($this->_recipients, $this->_subject, $this->_finalbody, $this->_header_str, '-f '.$from); 1903 } 1904 } 1905 1906 // -------------------------------------------------------------------- 1907 1908 /** 1909 * Send using Sendmail 1910 * 1911 * @return bool 1912 */ 1913 protected function _send_with_sendmail() 1914 { 1915 // _validate_email_for_shell() below accepts by reference, 1916 // so this needs to be assigned to a variable 1917 $from = $this->clean_email($this->_headers['From']); 1918 if ($this->_validate_email_for_shell($from)) 1919 { 1920 $from = '-f '.$from; 1921 } 1922 else 1923 { 1924 $from = ''; 1925 } 1926 1927 // is popen() enabled? 1928 if ( ! function_usable('popen') OR FALSE === ($fp = @popen($this->mailpath.' -oi '.$from.' -t', 'w'))) 1929 { 1930 // server probably has popen disabled, so nothing we can do to get a verbose error. 1931 return FALSE; 1932 } 1933 1934 fputs($fp, $this->_header_str); 1935 fputs($fp, $this->_finalbody); 1936 1937 $status = pclose($fp); 1938 1939 if ($status !== 0) 1940 { 1941 $this->_set_error_message('lang:email_exit_status', $status); 1942 $this->_set_error_message('lang:email_no_socket'); 1943 return FALSE; 1944 } 1945 1946 return TRUE; 1947 } 1948 1949 // -------------------------------------------------------------------- 1950 1951 /** 1952 * Send using SMTP 1953 * 1954 * @return bool 1955 */ 1956 protected function _send_with_smtp() 1957 { 1958 if ($this->smtp_host === '') 1959 { 1960 $this->_set_error_message('lang:email_no_hostname'); 1961 return FALSE; 1962 } 1963 1964 if ( ! $this->_smtp_connect() OR ! $this->_smtp_authenticate()) 1965 { 1966 return FALSE; 1967 } 1968 1969 if ( ! $this->_send_command('from', $this->clean_email($this->_headers['From']))) 1970 { 1971 $this->_smtp_end(); 1972 return FALSE; 1973 } 1974 1975 foreach ($this->_recipients as $val) 1976 { 1977 if ( ! $this->_send_command('to', $val)) 1978 { 1979 $this->_smtp_end(); 1980 return FALSE; 1981 } 1982 } 1983 1984 if (count($this->_cc_array) > 0) 1985 { 1986 foreach ($this->_cc_array as $val) 1987 { 1988 if ($val !== '' && ! $this->_send_command('to', $val)) 1989 { 1990 $this->_smtp_end(); 1991 return FALSE; 1992 } 1993 } 1994 } 1995 1996 if (count($this->_bcc_array) > 0) 1997 { 1998 foreach ($this->_bcc_array as $val) 1999 { 2000 if ($val !== '' && ! $this->_send_command('to', $val)) 2001 { 2002 $this->_smtp_end(); 2003 return FALSE; 2004 } 2005 } 2006 } 2007 2008 if ( ! $this->_send_command('data')) 2009 { 2010 $this->_smtp_end(); 2011 return FALSE; 2012 } 2013 2014 // perform dot transformation on any lines that begin with a dot 2015 $this->_send_data($this->_header_str.preg_replace('/^\./m', '..$1', $this->_finalbody)); 2016 2017 $this->_send_data('.'); 2018 2019 $reply = $this->_get_smtp_data(); 2020 $this->_set_error_message($reply); 2021 2022 $this->_smtp_end(); 2023 2024 if (strpos($reply, '250') !== 0) 2025 { 2026 $this->_set_error_message('lang:email_smtp_error', $reply); 2027 return FALSE; 2028 } 2029 2030 return TRUE; 2031 } 2032 2033 // -------------------------------------------------------------------- 2034 2035 /** 2036 * SMTP End 2037 * 2038 * Shortcut to send RSET or QUIT depending on keep-alive 2039 * 2040 * @return void 2041 */ 2042 protected function _smtp_end() 2043 { 2044 ($this->smtp_keepalive) 2045 ? $this->_send_command('reset') 2046 : $this->_send_command('quit'); 2047 } 2048 2049 // -------------------------------------------------------------------- 2050 2051 /** 2052 * SMTP Connect 2053 * 2054 * @return string 2055 */ 2056 protected function _smtp_connect() 2057 { 2058 if (is_resource($this->_smtp_connect)) 2059 { 2060 return TRUE; 2061 } 2062 2063 $ssl = ($this->smtp_crypto === 'ssl') ? 'ssl://' : ''; 2064 2065 $this->_smtp_connect = fsockopen($ssl.$this->smtp_host, 2066 $this->smtp_port, 2067 $errno, 2068 $errstr, 2069 $this->smtp_timeout); 2070 2071 if ( ! is_resource($this->_smtp_connect)) 2072 { 2073 $this->_set_error_message('lang:email_smtp_error', $errno.' '.$errstr); 2074 return FALSE; 2075 } 2076 2077 stream_set_timeout($this->_smtp_connect, $this->smtp_timeout); 2078 $this->_set_error_message($this->_get_smtp_data()); 2079 2080 if ($this->smtp_crypto === 'tls') 2081 { 2082 $this->_send_command('hello'); 2083 $this->_send_command('starttls'); 2084 2085 /** 2086 * STREAM_CRYPTO_METHOD_TLS_CLIENT is quite the mess ... 2087 * 2088 * - On PHP <5.6 it doesn't even mean TLS, but SSL 2.0, and there's no option to use actual TLS 2089 * - On PHP 5.6.0-5.6.6, >=7.2 it means negotiation with any of TLS 1.0, 1.1, 1.2 2090 * - On PHP 5.6.7-7.1.* it means only TLS 1.0 2091 * 2092 * We want the negotiation, so we'll force it below ... 2093 */ 2094 $method = is_php('5.6') 2095 ? STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT 2096 : STREAM_CRYPTO_METHOD_TLS_CLIENT; 2097 $crypto = stream_socket_enable_crypto($this->_smtp_connect, TRUE, $method); 2098 2099 if ($crypto !== TRUE) 2100 { 2101 $this->_set_error_message('lang:email_smtp_error', $this->_get_smtp_data()); 2102 return FALSE; 2103 } 2104 } 2105 2106 return $this->_send_command('hello'); 2107 } 2108 2109 // -------------------------------------------------------------------- 2110 2111 /** 2112 * Send SMTP command 2113 * 2114 * @param string 2115 * @param string 2116 * @return bool 2117 */ 2118 protected function _send_command($cmd, $data = '') 2119 { 2120 switch ($cmd) 2121 { 2122 case 'hello' : 2123 2124 if ($this->_smtp_auth OR $this->_get_encoding() === '8bit') 2125 { 2126 $this->_send_data('EHLO '.$this->_get_hostname()); 2127 } 2128 else 2129 { 2130 $this->_send_data('HELO '.$this->_get_hostname()); 2131 } 2132 2133 $resp = 250; 2134 break; 2135 case 'starttls' : 2136 2137 $this->_send_data('STARTTLS'); 2138 $resp = 220; 2139 break; 2140 case 'from' : 2141 2142 $this->_send_data('MAIL FROM:<'.$data.'>'); 2143 $resp = 250; 2144 break; 2145 case 'to' : 2146 2147 if ($this->dsn) 2148 { 2149 $this->_send_data('RCPT TO:<'.$data.'> NOTIFY=SUCCESS,DELAY,FAILURE ORCPT=rfc822;'.$data); 2150 } 2151 else 2152 { 2153 $this->_send_data('RCPT TO:<'.$data.'>'); 2154 } 2155 2156 $resp = 250; 2157 break; 2158 case 'data' : 2159 2160 $this->_send_data('DATA'); 2161 $resp = 354; 2162 break; 2163 case 'reset': 2164 2165 $this->_send_data('RSET'); 2166 $resp = 250; 2167 break; 2168 case 'quit' : 2169 2170 $this->_send_data('QUIT'); 2171 $resp = 221; 2172 break; 2173 } 2174 2175 $reply = $this->_get_smtp_data(); 2176 2177 $this->_debug_msg[] = '<pre>'.$cmd.': '.$reply.'</pre>'; 2178 2179 if ((int) self::substr($reply, 0, 3) !== $resp) 2180 { 2181 $this->_set_error_message('lang:email_smtp_error', $reply); 2182 return FALSE; 2183 } 2184 2185 if ($cmd === 'quit') 2186 { 2187 fclose($this->_smtp_connect); 2188 } 2189 2190 return TRUE; 2191 } 2192 2193 // -------------------------------------------------------------------- 2194 2195 /** 2196 * SMTP Authenticate 2197 * 2198 * @return bool 2199 */ 2200 protected function _smtp_authenticate() 2201 { 2202 if ( ! $this->_smtp_auth) 2203 { 2204 return TRUE; 2205 } 2206 2207 if ($this->smtp_user === '' && $this->smtp_pass === '') 2208 { 2209 $this->_set_error_message('lang:email_no_smtp_unpw'); 2210 return FALSE; 2211 } 2212 2213 $this->_send_data('AUTH LOGIN'); 2214 2215 $reply = $this->_get_smtp_data(); 2216 2217 if (strpos($reply, '503') === 0) // Already authenticated 2218 { 2219 return TRUE; 2220 } 2221 elseif (strpos($reply, '334') !== 0) 2222 { 2223 $this->_set_error_message('lang:email_failed_smtp_login', $reply); 2224 return FALSE; 2225 } 2226 2227 $this->_send_data(base64_encode($this->smtp_user)); 2228 2229 $reply = $this->_get_smtp_data(); 2230 2231 if (strpos($reply, '334') !== 0) 2232 { 2233 $this->_set_error_message('lang:email_smtp_auth_un', $reply); 2234 return FALSE; 2235 } 2236 2237 $this->_send_data(base64_encode($this->smtp_pass)); 2238 2239 $reply = $this->_get_smtp_data(); 2240 2241 if (strpos($reply, '235') !== 0) 2242 { 2243 $this->_set_error_message('lang:email_smtp_auth_pw', $reply); 2244 return FALSE; 2245 } 2246 2247 if ($this->smtp_keepalive) 2248 { 2249 $this->_smtp_auth = FALSE; 2250 } 2251 2252 return TRUE; 2253 } 2254 2255 // -------------------------------------------------------------------- 2256 2257 /** 2258 * Send SMTP data 2259 * 2260 * @param string $data 2261 * @return bool 2262 */ 2263 protected function _send_data($data) 2264 { 2265 $data .= $this->newline; 2266 for ($written = $timestamp = 0, $length = self::strlen($data); $written < $length; $written += $result) 2267 { 2268 if (($result = fwrite($this->_smtp_connect, self::substr($data, $written))) === FALSE) 2269 { 2270 break; 2271 } 2272 // See https://bugs.php.net/bug.php?id=39598 and http://php.net/manual/en/function.fwrite.php#96951 2273 elseif ($result === 0) 2274 { 2275 if ($timestamp === 0) 2276 { 2277 $timestamp = time(); 2278 } 2279 elseif ($timestamp < (time() - $this->smtp_timeout)) 2280 { 2281 $result = FALSE; 2282 break; 2283 } 2284 2285 usleep(250000); 2286 continue; 2287 } 2288 2289 $timestamp = 0; 2290 } 2291 2292 if ($result === FALSE) 2293 { 2294 $this->_set_error_message('lang:email_smtp_data_failure', $data); 2295 return FALSE; 2296 } 2297 2298 return TRUE; 2299 } 2300 2301 // -------------------------------------------------------------------- 2302 2303 /** 2304 * Get SMTP data 2305 * 2306 * @return string 2307 */ 2308 protected function _get_smtp_data() 2309 { 2310 $data = ''; 2311 2312 while ($str = fgets($this->_smtp_connect, 512)) 2313 { 2314 $data .= $str; 2315 2316 if ($str[3] === ' ') 2317 { 2318 break; 2319 } 2320 } 2321 2322 return $data; 2323 } 2324 2325 // -------------------------------------------------------------------- 2326 2327 /** 2328 * Get Hostname 2329 * 2330 * There are only two legal types of hostname - either a fully 2331 * qualified domain name (eg: "mail.example.com") or an IP literal 2332 * (eg: "[1.2.3.4]"). 2333 * 2334 * @link https://tools.ietf.org/html/rfc5321#section-2.3.5 2335 * @link http://cbl.abuseat.org/namingproblems.html 2336 * @return string 2337 */ 2338 protected function _get_hostname() 2339 { 2340 if (isset($_SERVER['SERVER_NAME'])) 2341 { 2342 return $_SERVER['SERVER_NAME']; 2343 } 2344 2345 return isset($_SERVER['SERVER_ADDR']) ? '['.$_SERVER['SERVER_ADDR'].']' : '[127.0.0.1]'; 2346 } 2347 2348 // -------------------------------------------------------------------- 2349 2350 /** 2351 * Get Debug Message 2352 * 2353 * @param array $include List of raw data chunks to include in the output 2354 * Valid options are: 'headers', 'subject', 'body' 2355 * @return string 2356 */ 2357 public function print_debugger($include = array('headers', 'subject', 'body')) 2358 { 2359 $msg = ''; 2360 2361 if (count($this->_debug_msg) > 0) 2362 { 2363 foreach ($this->_debug_msg as $val) 2364 { 2365 $msg .= $val; 2366 } 2367 } 2368 2369 // Determine which parts of our raw data needs to be printed 2370 $raw_data = ''; 2371 is_array($include) OR $include = array($include); 2372 2373 if (in_array('headers', $include, TRUE)) 2374 { 2375 $raw_data = htmlspecialchars($this->_header_str)."\n"; 2376 } 2377 2378 if (in_array('subject', $include, TRUE)) 2379 { 2380 $raw_data .= htmlspecialchars($this->_subject)."\n"; 2381 } 2382 2383 if (in_array('body', $include, TRUE)) 2384 { 2385 $raw_data .= htmlspecialchars($this->_finalbody); 2386 } 2387 2388 return $msg.($raw_data === '' ? '' : '<pre>'.$raw_data.'</pre>'); 2389 } 2390 2391 // -------------------------------------------------------------------- 2392 2393 /** 2394 * Set Message 2395 * 2396 * @param string $msg 2397 * @param string $val = '' 2398 * @return void 2399 */ 2400 protected function _set_error_message($msg, $val = '') 2401 { 2402 $CI =& get_instance(); 2403 $CI->lang->load('email'); 2404 2405 if (sscanf($msg, 'lang:%s', $line) !== 1 OR FALSE === ($line = $CI->lang->line($line))) 2406 { 2407 $this->_debug_msg[] = str_replace('%s', $val, $msg).'<br />'; 2408 } 2409 else 2410 { 2411 $this->_debug_msg[] = str_replace('%s', $val, $line).'<br />'; 2412 } 2413 } 2414 2415 // -------------------------------------------------------------------- 2416 2417 /** 2418 * Mime Types 2419 * 2420 * @param string 2421 * @return string 2422 */ 2423 protected function _mime_types($ext = '') 2424 { 2425 $ext = strtolower($ext); 2426 2427 $mimes =& get_mimes(); 2428 2429 if (isset($mimes[$ext])) 2430 { 2431 return is_array($mimes[$ext]) 2432 ? current($mimes[$ext]) 2433 : $mimes[$ext]; 2434 } 2435 2436 return 'application/x-unknown-content-type'; 2437 } 2438 2439 // -------------------------------------------------------------------- 2440 2441 /** 2442 * Destructor 2443 * 2444 * @return void 2445 */ 2446 public function __destruct() 2447 { 2448 is_resource($this->_smtp_connect) && $this->_send_command('quit'); 2449 } 2450 2451 // -------------------------------------------------------------------- 2452 2453 /** 2454 * Byte-safe strlen() 2455 * 2456 * @param string $str 2457 * @return int 2458 */ 2459 protected static function strlen($str) 2460 { 2461 return (self::$func_overload) 2462 ? mb_strlen($str, '8bit') 2463 : strlen($str); 2464 } 2465 2466 // -------------------------------------------------------------------- 2467 2468 /** 2469 * Byte-safe substr() 2470 * 2471 * @param string $str 2472 * @param int $start 2473 * @param int $length 2474 * @return string 2475 */ 2476 protected static function substr($str, $start, $length = NULL) 2477 { 2478 if (self::$func_overload) 2479 { 2480 // mb_substr($str, $start, null, '8bit') returns an empty 2481 // string on PHP 5.3 2482 isset($length) OR $length = ($start >= 0 ? self::strlen($str) - $start : -$start); 2483 return mb_substr($str, $start, $length, '8bit'); 2484 } 2485 2486 return isset($length) 2487 ? substr($str, $start, $length) 2488 : substr($str, $start); 2489 } 2490} 2491