PageRenderTime 25ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/system/libraries/Email.php

http://github.com/EllisLab/CodeIgniter
PHP | 2417 lines | 2039 code | 63 blank | 315 comment | 9 complexity | 758d72107c744e0960aa40e051899fdf MD5 | raw file
Possible License(s): CC-BY-SA-3.0

Large files files are truncated, but you can click here to view the full file

  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. */
  38. defined('BASEPATH') OR exit('No direct script access allowed');
  39. /**
  40. * CodeIgniter Email Class
  41. *
  42. * Permits email to be sent using Mail, Sendmail, or SMTP.
  43. *
  44. * @package CodeIgniter
  45. * @subpackage Libraries
  46. * @category Libraries
  47. * @author EllisLab Dev Team
  48. * @link https://codeigniter.com/userguide3/libraries/email.html
  49. */
  50. class CI_Email {
  51. /**
  52. * Used as the User-Agent and X-Mailer headers' value.
  53. *
  54. * @var string
  55. */
  56. public $useragent = 'CodeIgniter';
  57. /**
  58. * Path to the Sendmail binary.
  59. *
  60. * @var string
  61. */
  62. public $mailpath = '/usr/sbin/sendmail'; // Sendmail path
  63. /**
  64. * Which method to use for sending e-mails.
  65. *
  66. * @var string 'mail', 'sendmail' or 'smtp'
  67. */
  68. public $protocol = 'mail'; // mail/sendmail/smtp
  69. /**
  70. * STMP Server host
  71. *
  72. * @var string
  73. */
  74. public $smtp_host = '';
  75. /**
  76. * SMTP Username
  77. *
  78. * @var string
  79. */
  80. public $smtp_user = '';
  81. /**
  82. * SMTP Password
  83. *
  84. * @var string
  85. */
  86. public $smtp_pass = '';
  87. /**
  88. * SMTP Server port
  89. *
  90. * @var int
  91. */
  92. public $smtp_port = 25;
  93. /**
  94. * SMTP connection timeout in seconds
  95. *
  96. * @var int
  97. */
  98. public $smtp_timeout = 5;
  99. /**
  100. * SMTP persistent connection
  101. *
  102. * @var bool
  103. */
  104. public $smtp_keepalive = FALSE;
  105. /**
  106. * SMTP Encryption
  107. *
  108. * @var string empty, 'tls' or 'ssl'
  109. */
  110. public $smtp_crypto = '';
  111. /**
  112. * Whether to apply word-wrapping to the message body.
  113. *
  114. * @var bool
  115. */
  116. public $wordwrap = TRUE;
  117. /**
  118. * Number of characters to wrap at.
  119. *
  120. * @see CI_Email::$wordwrap
  121. * @var int
  122. */
  123. public $wrapchars = 76;
  124. /**
  125. * Message format.
  126. *
  127. * @var string 'text' or 'html'
  128. */
  129. public $mailtype = 'text';
  130. /**
  131. * Character set (default: utf-8)
  132. *
  133. * @var string
  134. */
  135. public $charset = 'utf-8';
  136. /**
  137. * Alternative message (for HTML messages only)
  138. *
  139. * @var string
  140. */
  141. public $alt_message = '';
  142. /**
  143. * Whether to validate e-mail addresses.
  144. *
  145. * @var bool
  146. */
  147. public $validate = TRUE;
  148. /**
  149. * X-Priority header value.
  150. *
  151. * @var int 1-5
  152. */
  153. public $priority = 3; // Default priority (1 - 5)
  154. /**
  155. * Newline character sequence.
  156. * Use "\r\n" to comply with RFC 822.
  157. *
  158. * @link https://www.ietf.org/rfc/rfc822.txt
  159. * @var string "\r\n" or "\n"
  160. */
  161. public $newline = "\n"; // Default newline. "\r\n" or "\n" (Use "\r\n" to comply with RFC 822)
  162. /**
  163. * CRLF character sequence
  164. *
  165. * RFC 2045 specifies that for 'quoted-printable' encoding,
  166. * "\r\n" must be used. However, it appears that some servers
  167. * (even on the receiving end) don't handle it properly and
  168. * switching to "\n", while improper, is the only solution
  169. * that seems to work for all environments.
  170. *
  171. * @link https://www.ietf.org/rfc/rfc822.txt
  172. * @var string
  173. */
  174. public $crlf = "\n";
  175. /**
  176. * Whether to use Delivery Status Notification.
  177. *
  178. * @var bool
  179. */
  180. public $dsn = FALSE;
  181. /**
  182. * Whether to send multipart alternatives.
  183. * Yahoo! doesn't seem to like these.
  184. *
  185. * @var bool
  186. */
  187. public $send_multipart = TRUE;
  188. /**
  189. * Whether to send messages to BCC recipients in batches.
  190. *
  191. * @var bool
  192. */
  193. public $bcc_batch_mode = FALSE;
  194. /**
  195. * BCC Batch max number size.
  196. *
  197. * @see CI_Email::$bcc_batch_mode
  198. * @var int
  199. */
  200. public $bcc_batch_size = 200;
  201. // --------------------------------------------------------------------
  202. /**
  203. * Subject header
  204. *
  205. * @var string
  206. */
  207. protected $_subject = '';
  208. /**
  209. * Message body
  210. *
  211. * @var string
  212. */
  213. protected $_body = '';
  214. /**
  215. * Final message body to be sent.
  216. *
  217. * @var string
  218. */
  219. protected $_finalbody = '';
  220. /**
  221. * Final headers to send
  222. *
  223. * @var string
  224. */
  225. protected $_header_str = '';
  226. /**
  227. * SMTP Connection socket placeholder
  228. *
  229. * @var resource
  230. */
  231. protected $_smtp_connect = '';
  232. /**
  233. * Mail encoding
  234. *
  235. * @var string '8bit' or '7bit'
  236. */
  237. protected $_encoding = '8bit';
  238. /**
  239. * Whether to perform SMTP authentication
  240. *
  241. * @var bool
  242. */
  243. protected $_smtp_auth = FALSE;
  244. /**
  245. * Whether to send a Reply-To header
  246. *
  247. * @var bool
  248. */
  249. protected $_replyto_flag = FALSE;
  250. /**
  251. * Debug messages
  252. *
  253. * @see CI_Email::print_debugger()
  254. * @var string
  255. */
  256. protected $_debug_msg = array();
  257. /**
  258. * Recipients
  259. *
  260. * @var string[]
  261. */
  262. protected $_recipients = array();
  263. /**
  264. * CC Recipients
  265. *
  266. * @var string[]
  267. */
  268. protected $_cc_array = array();
  269. /**
  270. * BCC Recipients
  271. *
  272. * @var string[]
  273. */
  274. protected $_bcc_array = array();
  275. /**
  276. * Message headers
  277. *
  278. * @var string[]
  279. */
  280. protected $_headers = array();
  281. /**
  282. * Attachment data
  283. *
  284. * @var array
  285. */
  286. protected $_attachments = array();
  287. /**
  288. * Valid $protocol values
  289. *
  290. * @see CI_Email::$protocol
  291. * @var string[]
  292. */
  293. protected $_protocols = array('mail', 'sendmail', 'smtp');
  294. /**
  295. * Base charsets
  296. *
  297. * Character sets valid for 7-bit encoding,
  298. * excluding language suffix.
  299. *
  300. * @var string[]
  301. */
  302. protected $_base_charsets = array('us-ascii', 'iso-2022-');
  303. /**
  304. * Bit depths
  305. *
  306. * Valid mail encodings
  307. *
  308. * @see CI_Email::$_encoding
  309. * @var string[]
  310. */
  311. protected $_bit_depths = array('7bit', '8bit');
  312. /**
  313. * $priority translations
  314. *
  315. * Actual values to send with the X-Priority header
  316. *
  317. * @var string[]
  318. */
  319. protected $_priorities = array(
  320. 1 => '1 (Highest)',
  321. 2 => '2 (High)',
  322. 3 => '3 (Normal)',
  323. 4 => '4 (Low)',
  324. 5 => '5 (Lowest)'
  325. );
  326. /**
  327. * mbstring.func_overload flag
  328. *
  329. * @var bool
  330. */
  331. protected static $func_overload;
  332. // --------------------------------------------------------------------
  333. /**
  334. * Constructor - Sets Email Preferences
  335. *
  336. * The constructor can be passed an array of config values
  337. *
  338. * @param array $config = array()
  339. * @return void
  340. */
  341. public function __construct(array $config = array())
  342. {
  343. $this->charset = config_item('charset');
  344. $this->initialize($config);
  345. isset(self::$func_overload) OR self::$func_overload = (extension_loaded('mbstring') && ini_get('mbstring.func_overload'));
  346. log_message('info', 'Email Class Initialized');
  347. }
  348. // --------------------------------------------------------------------
  349. /**
  350. * Initialize preferences
  351. *
  352. * @param array $config
  353. * @return CI_Email
  354. */
  355. public function initialize(array $config = array())
  356. {
  357. $this->clear();
  358. foreach ($config as $key => $val)
  359. {
  360. if (isset($this->$key))
  361. {
  362. $method = 'set_'.$key;
  363. if (method_exists($this, $method))
  364. {
  365. $this->$method($val);
  366. }
  367. else
  368. {
  369. $this->$key = $val;
  370. }
  371. }
  372. }
  373. $this->charset = strtoupper($this->charset);
  374. $this->_smtp_auth = isset($this->smtp_user[0], $this->smtp_pass[0]);
  375. return $this;
  376. }
  377. // --------------------------------------------------------------------
  378. /**
  379. * Initialize the Email Data
  380. *
  381. * @param bool
  382. * @return CI_Email
  383. */
  384. public function clear($clear_attachments = FALSE)
  385. {
  386. $this->_subject = '';
  387. $this->_body = '';
  388. $this->_finalbody = '';
  389. $this->_header_str = '';
  390. $this->_replyto_flag = FALSE;
  391. $this->_recipients = array();
  392. $this->_cc_array = array();
  393. $this->_bcc_array = array();
  394. $this->_headers = array();
  395. $this->_debug_msg = array();
  396. $this->set_header('Date', $this->_set_date());
  397. if ($clear_attachments !== FALSE)
  398. {
  399. $this->_attachments = array();
  400. }
  401. return $this;
  402. }
  403. // --------------------------------------------------------------------
  404. /**
  405. * Set FROM
  406. *
  407. * @param string $from
  408. * @param string $name
  409. * @param string $return_path = NULL Return-Path
  410. * @return CI_Email
  411. */
  412. public function from($from, $name = '', $return_path = NULL)
  413. {
  414. if (preg_match('/\<(.*)\>/', $from, $match))
  415. {
  416. $from = $match[1];
  417. }
  418. if ($this->validate)
  419. {
  420. $this->validate_email($this->_str_to_array($from));
  421. if ($return_path)
  422. {
  423. $this->validate_email($this->_str_to_array($return_path));
  424. }
  425. }
  426. // prepare the display name
  427. if ($name !== '')
  428. {
  429. // only use Q encoding if there are characters that would require it
  430. if ( ! preg_match('/[\200-\377]/', $name))
  431. {
  432. // add slashes for non-printing characters, slashes, and double quotes, and surround it in double quotes
  433. $name = '"'.addcslashes($name, "\0..\37\177'\"\\").'"';
  434. }
  435. else
  436. {
  437. $name = $this->_prep_q_encoding($name);
  438. }
  439. }
  440. $this->set_header('From', $name.' <'.$from.'>');
  441. isset($return_path) OR $return_path = $from;
  442. $this->set_header('Return-Path', '<'.$return_path.'>');
  443. return $this;
  444. }
  445. // --------------------------------------------------------------------
  446. /**
  447. * Set Reply-to
  448. *
  449. * @param string
  450. * @param string
  451. * @return CI_Email
  452. */
  453. public function reply_to($replyto, $name = '')
  454. {
  455. if (preg_match('/\<(.*)\>/', $replyto, $match))
  456. {
  457. $replyto = $match[1];
  458. }
  459. if ($this->validate)
  460. {
  461. $this->validate_email($this->_str_to_array($replyto));
  462. }
  463. if ($name !== '')
  464. {
  465. // only use Q encoding if there are characters that would require it
  466. if ( ! preg_match('/[\200-\377]/', $name))
  467. {
  468. // add slashes for non-printing characters, slashes, and double quotes, and surround it in double quotes
  469. $name = '"'.addcslashes($name, "\0..\37\177'\"\\").'"';
  470. }
  471. else
  472. {
  473. $name = $this->_prep_q_encoding($name);
  474. }
  475. }
  476. $this->set_header('Reply-To', $name.' <'.$replyto.'>');
  477. $this->_replyto_flag = TRUE;
  478. return $this;
  479. }
  480. // --------------------------------------------------------------------
  481. /**
  482. * Set Recipients
  483. *
  484. * @param string
  485. * @return CI_Email
  486. */
  487. public function to($to)
  488. {
  489. $to = $this->_str_to_array($to);
  490. $to = $this->clean_email($to);
  491. if ($this->validate)
  492. {
  493. $this->validate_email($to);
  494. }
  495. if ($this->_get_protocol() !== 'mail')
  496. {
  497. $this->set_header('To', implode(', ', $to));
  498. }
  499. $this->_recipients = $to;
  500. return $this;
  501. }
  502. // --------------------------------------------------------------------
  503. /**
  504. * Set CC
  505. *
  506. * @param string
  507. * @return CI_Email
  508. */
  509. public function cc($cc)
  510. {
  511. $cc = $this->clean_email($this->_str_to_array($cc));
  512. if ($this->validate)
  513. {
  514. $this->validate_email($cc);
  515. }
  516. $this->set_header('Cc', implode(', ', $cc));
  517. if ($this->_get_protocol() === 'smtp')
  518. {
  519. $this->_cc_array = $cc;
  520. }
  521. return $this;
  522. }
  523. // --------------------------------------------------------------------
  524. /**
  525. * Set BCC
  526. *
  527. * @param string
  528. * @param string
  529. * @return CI_Email
  530. */
  531. public function bcc($bcc, $limit = '')
  532. {
  533. if ($limit !== '' && is_numeric($limit))
  534. {
  535. $this->bcc_batch_mode = TRUE;
  536. $this->bcc_batch_size = $limit;
  537. }
  538. $bcc = $this->clean_email($this->_str_to_array($bcc));
  539. if ($this->validate)
  540. {
  541. $this->validate_email($bcc);
  542. }
  543. if ($this->_get_protocol() === 'smtp' OR ($this->bcc_batch_mode && count($bcc) > $this->bcc_batch_size))
  544. {
  545. $this->_bcc_array = $bcc;
  546. }
  547. else
  548. {
  549. $this->set_header('Bcc', implode(', ', $bcc));
  550. }
  551. return $this;
  552. }
  553. // --------------------------------------------------------------------
  554. /**
  555. * Set Email Subject
  556. *
  557. * @param string
  558. * @return CI_Email
  559. */
  560. public function subject($subject)
  561. {
  562. $subject = $this->_prep_q_encoding($subject);
  563. $this->set_header('Subject', $subject);
  564. return $this;
  565. }
  566. // --------------------------------------------------------------------
  567. /**
  568. * Set Body
  569. *
  570. * @param string
  571. * @return CI_Email
  572. */
  573. public function message($body)
  574. {
  575. $this->_body = rtrim(str_replace("\r", '', $body));
  576. return $this;
  577. }
  578. // --------------------------------------------------------------------
  579. /**
  580. * Assign file attachments
  581. *
  582. * @param string $file Can be local path, URL or buffered content
  583. * @param string $disposition = 'attachment'
  584. * @param string $newname = NULL
  585. * @param string $mime = ''
  586. * @return CI_Email
  587. */
  588. public function attach($file, $disposition = '', $newname = NULL, $mime = '')
  589. {
  590. if ($mime === '')
  591. {
  592. if (strpos($file, '://') === FALSE && ! file_exists($file))
  593. {
  594. $this->_set_error_message('lang:email_attachment_missing', $file);
  595. return FALSE;
  596. }
  597. if ( ! $fp = @fopen($file, 'rb'))
  598. {
  599. $this->_set_error_message('lang:email_attachment_unreadable', $file);
  600. return FALSE;
  601. }
  602. $file_content = stream_get_contents($fp);
  603. $mime = $this->_mime_types(pathinfo($file, PATHINFO_EXTENSION));
  604. fclose($fp);
  605. }
  606. else
  607. {
  608. $file_content =& $file; // buffered file
  609. }
  610. $this->_attachments[] = array(
  611. 'name' => array($file, $newname),
  612. 'disposition' => empty($disposition) ? 'attachment' : $disposition, // Can also be 'inline' Not sure if it matters
  613. 'type' => $mime,
  614. 'content' => chunk_split(base64_encode($file_content)),
  615. 'multipart' => 'mixed'
  616. );
  617. return $this;
  618. }
  619. // --------------------------------------------------------------------
  620. /**
  621. * Set and return attachment Content-ID
  622. *
  623. * Useful for attached inline pictures
  624. *
  625. * @param string $filename
  626. * @return string
  627. */
  628. public function attachment_cid($filename)
  629. {
  630. for ($i = 0, $c = count($this->_attachments); $i < $c; $i++)
  631. {
  632. if ($this->_attachments[$i]['name'][0] === $filename)
  633. {
  634. $this->_attachments[$i]['multipart'] = 'related';
  635. $this->_attachments[$i]['cid'] = uniqid(basename($this->_attachments[$i]['name'][0]).'@');
  636. return $this->_attachments[$i]['cid'];
  637. }
  638. }
  639. return FALSE;
  640. }
  641. // --------------------------------------------------------------------
  642. /**
  643. * Add a Header Item
  644. *
  645. * @param string
  646. * @param string
  647. * @return CI_Email
  648. */
  649. public function set_header($header, $value)
  650. {
  651. $this->_headers[$header] = str_replace(array("\n", "\r"), '', $value);
  652. return $this;
  653. }
  654. // --------------------------------------------------------------------
  655. /**
  656. * Convert a String to an Array
  657. *
  658. * @param string
  659. * @return array
  660. */
  661. protected function _str_to_array($email)
  662. {
  663. if ( ! is_array($email))
  664. {
  665. return (strpos($email, ',') !== FALSE)
  666. ? preg_split('/[\s,]/', $email, -1, PREG_SPLIT_NO_EMPTY)
  667. : (array) trim($email);
  668. }
  669. return $email;
  670. }
  671. // --------------------------------------------------------------------
  672. /**
  673. * Set Multipart Value
  674. *
  675. * @param string
  676. * @return CI_Email
  677. */
  678. public function set_alt_message($str)
  679. {
  680. $this->alt_message = (string) $str;
  681. return $this;
  682. }
  683. // --------------------------------------------------------------------
  684. /**
  685. * Set Mailtype
  686. *
  687. * @param string
  688. * @return CI_Email
  689. */
  690. public function set_mailtype($type = 'text')
  691. {
  692. $this->mailtype = ($type === 'html') ? 'html' : 'text';
  693. return $this;
  694. }
  695. // --------------------------------------------------------------------
  696. /**
  697. * Set Wordwrap
  698. *
  699. * @param bool
  700. * @return CI_Email
  701. */
  702. public function set_wordwrap($wordwrap = TRUE)
  703. {
  704. $this->wordwrap = (bool) $wordwrap;
  705. return $this;
  706. }
  707. // --------------------------------------------------------------------
  708. /**
  709. * Set Protocol
  710. *
  711. * @param string
  712. * @return CI_Email
  713. */
  714. public function set_protocol($protocol = 'mail')
  715. {
  716. $this->protocol = in_array($protocol, $this->_protocols, TRUE) ? strtolower($protocol) : 'mail';
  717. return $this;
  718. }
  719. // --------------------------------------------------------------------
  720. /**
  721. * Set Priority
  722. *
  723. * @param int
  724. * @return CI_Email
  725. */
  726. public function set_priority($n = 3)
  727. {
  728. $this->priority = preg_match('/^[1-5]$/', $n) ? (int) $n : 3;
  729. return $this;
  730. }
  731. // --------------------------------------------------------------------
  732. /**
  733. * Set Newline Character
  734. *
  735. * @param string
  736. * @return CI_Email
  737. */
  738. public function set_newline($newline = "\n")
  739. {
  740. $this->newline = in_array($newline, array("\n", "\r\n", "\r")) ? $newline : "\n";
  741. return $this;
  742. }
  743. // --------------------------------------------------------------------
  744. /**
  745. * Set CRLF
  746. *
  747. * @param string
  748. * @return CI_Email
  749. */
  750. public function set_crlf($crlf = "\n")
  751. {
  752. $this->crlf = ($crlf !== "\n" && $crlf !== "\r\n" && $crlf !== "\r") ? "\n" : $crlf;
  753. return $this;
  754. }
  755. // --------------------------------------------------------------------
  756. /**
  757. * Get the Message ID
  758. *
  759. * @return string
  760. */
  761. protected function _get_message_id()
  762. {
  763. $from = str_replace(array('>', '<'), '', $this->_headers['Return-Path']);
  764. return '<'.uniqid('').strstr($from, '@').'>';
  765. }
  766. // --------------------------------------------------------------------
  767. /**
  768. * Get Mail Protocol
  769. *
  770. * @return mixed
  771. */
  772. protected function _get_protocol()
  773. {
  774. $this->protocol = strtolower($this->protocol);
  775. in_array($this->protocol, $this->_protocols, TRUE) OR $this->protocol = 'mail';
  776. return $this->protocol;
  777. }
  778. // --------------------------------------------------------------------
  779. /**
  780. * Get Mail Encoding
  781. *
  782. * @return string
  783. */
  784. protected function _get_encoding()
  785. {
  786. in_array($this->_encoding, $this->_bit_depths) OR $this->_encoding = '8bit';
  787. foreach ($this->_base_charsets as $charset)
  788. {
  789. if (strpos($this->charset, $charset) === 0)
  790. {
  791. $this->_encoding = '7bit';
  792. }
  793. }
  794. return $this->_encoding;
  795. }
  796. // --------------------------------------------------------------------
  797. /**
  798. * Get content type (text/html/attachment)
  799. *
  800. * @return string
  801. */
  802. protected function _get_content_type()
  803. {
  804. if ($this->mailtype === 'html')
  805. {
  806. return empty($this->_attachments) ? 'html' : 'html-attach';
  807. }
  808. elseif ($this->mailtype === 'text' && ! empty($this->_attachments))
  809. {
  810. return 'plain-attach';
  811. }
  812. return 'plain';
  813. }
  814. // --------------------------------------------------------------------
  815. /**
  816. * Set RFC 822 Date
  817. *
  818. * @return string
  819. */
  820. protected function _set_date()
  821. {
  822. $timezone = date('Z');
  823. $operator = ($timezone[0] === '-') ? '-' : '+';
  824. $timezone = abs($timezone);
  825. $timezone = floor($timezone/3600) * 100 + ($timezone % 3600) / 60;
  826. return sprintf('%s %s%04d', date('D, j M Y H:i:s'), $operator, $timezone);
  827. }
  828. // --------------------------------------------------------------------
  829. /**
  830. * Mime message
  831. *
  832. * @return string
  833. */
  834. protected function _get_mime_message()
  835. {
  836. return 'This is a multi-part message in MIME format.'.$this->newline.'Your email application may not support this format.';
  837. }
  838. // --------------------------------------------------------------------
  839. /**
  840. * Validate Email Address
  841. *
  842. * @param string
  843. * @return bool
  844. */
  845. public function validate_email($email)
  846. {
  847. if ( ! is_array($email))
  848. {
  849. $this->_set_error_message('lang:email_must_be_array');
  850. return FALSE;
  851. }
  852. foreach ($email as $val)
  853. {
  854. if ( ! $this->valid_email($val))
  855. {
  856. $this->_set_error_message('lang:email_invalid_address', $val);
  857. return FALSE;
  858. }
  859. }
  860. return TRUE;
  861. }
  862. // --------------------------------------------------------------------
  863. /**
  864. * Email Validation
  865. *
  866. * @param string
  867. * @return bool
  868. */
  869. public function valid_email($email)
  870. {
  871. if (function_exists('idn_to_ascii') && preg_match('#\A([^@]+)@(.+)\z#', $email, $matches))
  872. {
  873. $domain = defined('INTL_IDNA_VARIANT_UTS46')
  874. ? idn_to_ascii($matches[2], 0, INTL_IDNA_VARIANT_UTS46)
  875. : idn_to_ascii($matches[2]);
  876. if ($domain !== FALSE)
  877. {
  878. $email = $matches[1].'@'.$domain;
  879. }
  880. }
  881. return (bool) filter_var($email, FILTER_VALIDATE_EMAIL);
  882. }
  883. // --------------------------------------------------------------------
  884. /**
  885. * Clean Extended Email Address: Joe Smith <joe@smith.com>
  886. *
  887. * @param string
  888. * @return string
  889. */
  890. public function clean_email($email)
  891. {
  892. if ( ! is_array($email))
  893. {
  894. return preg_match('/\<(.*)\>/', $email, $match) ? $match[1] : $email;
  895. }
  896. $clean_email = array();
  897. foreach ($email as $addy)
  898. {
  899. $clean_email[] = preg_match('/\<(.*)\>/', $addy, $match) ? $match[1] : $addy;
  900. }
  901. return $clean_email;
  902. }
  903. // --------------------------------------------------------------------
  904. /**
  905. * Build alternative plain text message
  906. *
  907. * Provides the raw message for use in plain-text headers of
  908. * HTML-formatted emails.
  909. * If the user hasn't specified his own alternative message
  910. * it creates one by stripping the HTML
  911. *
  912. * @return string
  913. */
  914. protected function _get_alt_message()
  915. {
  916. if ( ! empty($this->alt_message))
  917. {
  918. return ($this->wordwrap)
  919. ? $this->word_wrap($this->alt_message, 76)
  920. : $this->alt_message;
  921. }
  922. $body = preg_match('/\<body.*?\>(.*)\<\/body\>/si', $this->_body, $match) ? $match[1] : $this->_body;
  923. $body = str_replace("\t", '', preg_replace('#<!--(.*)--\>#', '', trim(strip_tags($body))));
  924. for ($i = 20; $i >= 3; $i--)
  925. {
  926. $body = str_replace(str_repeat("\n", $i), "\n\n", $body);
  927. }
  928. // Reduce multiple spaces
  929. $body = preg_replace('| +|', ' ', $body);
  930. return ($this->wordwrap)
  931. ? $this->word_wrap($body, 76)
  932. : $body;
  933. }
  934. // --------------------------------------------------------------------
  935. /**
  936. * Word Wrap
  937. *
  938. * @param string
  939. * @param int line-length limit
  940. * @return string
  941. */
  942. public function word_wrap($str, $charlim = NULL)
  943. {
  944. // Set the character limit, if not already present
  945. if (empty($charlim))
  946. {
  947. $charlim = empty($this->wrapchars) ? 76 : $this->wrapchars;
  948. }
  949. // Standardize newlines
  950. if (strpos($str, "\r") !== FALSE)
  951. {
  952. $str = str_replace(array("\r\n", "\r"), "\n", $str);
  953. }
  954. // Reduce multiple spaces at end of line
  955. $str = preg_replace('| +\n|', "\n", $str);
  956. // If the current word is surrounded by {unwrap} tags we'll
  957. // strip the entire chunk and replace it with a marker.
  958. $unwrap = array();
  959. if (preg_match_all('|\{unwrap\}(.+?)\{/unwrap\}|s', $str, $matches))
  960. {
  961. for ($i = 0, $c = count($matches[0]); $i < $c; $i++)
  962. {
  963. $unwrap[] = $matches[1][$i];
  964. $str = str_replace($matches[0][$i], '{{unwrapped'.$i.'}}', $str);
  965. }
  966. }
  967. // Use PHP's native function to do the initial wordwrap.
  968. // We set the cut flag to FALSE so that any individual words that are
  969. // too long get left alone. In the next step we'll deal with them.
  970. $str = wordwrap($str, $charlim, "\n", FALSE);
  971. // Split the string into individual lines of text and cycle through them
  972. $output = '';
  973. foreach (explode("\n", $str) as $line)
  974. {
  975. // Is the line within the allowed character count?
  976. // If so we'll join it to the output and continue
  977. if (self::strlen($line) <= $charlim)
  978. {
  979. $output .= $line.$this->newline;
  980. continue;
  981. }
  982. $temp = '';
  983. do
  984. {
  985. // If the over-length word is a URL we won't wrap it
  986. if (preg_match('!\[url.+\]|://|www\.!', $line))
  987. {
  988. break;
  989. }
  990. // Trim the word down
  991. $temp .= self::substr($line, 0, $charlim - 1);
  992. $line = self::substr($line, $charlim - 1);
  993. }
  994. while (self::strlen($line) > $charlim);
  995. // If $temp contains data it means we had to split up an over-length
  996. // word into smaller chunks so we'll add it back to our current line
  997. if ($temp !== '')
  998. {
  999. $output .= $temp.$this->newline;
  1000. }
  1001. $output .= $line.$this->newline;
  1002. }
  1003. // Put our markers back
  1004. if (count($unwrap) > 0)
  1005. {
  1006. foreach ($unwrap as $key => $val)
  1007. {
  1008. $output = str_replace('{{unwrapped'.$key.'}}', $val, $output);
  1009. }
  1010. }
  1011. return $output;
  1012. }
  1013. // --------------------------------------------------------------------
  1014. /**
  1015. * Build final headers
  1016. *
  1017. * @return void
  1018. */
  1019. protected function _build_headers()
  1020. {
  1021. $this->set_header('User-Agent', $this->useragent);
  1022. $this->set_header('X-Sender', $this->clean_email($this->_headers['From']));
  1023. $this->set_header('X-Mailer', $this->useragent);
  1024. $this->set_header('X-Priority', $this->_priorities[$this->priority]);
  1025. $this->set_header('Message-ID', $this->_get_message_id());
  1026. $this->set_header('Mime-Version', '1.0');
  1027. }
  1028. // --------------------------------------------------------------------
  1029. /**
  1030. * Write Headers as a string
  1031. *
  1032. * @return void
  1033. */
  1034. protected function _write_headers()
  1035. {
  1036. if ($this->protocol === 'mail')
  1037. {
  1038. if (isset($this->_headers['Subject']))
  1039. {
  1040. $this->_subject = $this->_headers['Subject'];
  1041. unset($this->_headers['Subject']);
  1042. }
  1043. }
  1044. reset($this->_headers);
  1045. $this->_header_str = '';
  1046. foreach ($this->_headers as $key => $val)
  1047. {
  1048. $val = trim($val);
  1049. if ($val !== '')
  1050. {
  1051. $this->_header_str .= $key.': '.$val.$this->newline;
  1052. }
  1053. }
  1054. if ($this->_get_protocol() === 'mail')
  1055. {
  1056. $this->_header_str = rtrim($this->_header_str);
  1057. }
  1058. }
  1059. // --------------------------------------------------------------------
  1060. /**
  1061. * Build Final Body and attachments
  1062. *
  1063. * @return void
  1064. */
  1065. protected function _build_message()
  1066. {
  1067. if ($this->wordwrap === TRUE && $this->mailtype !== 'html')
  1068. {
  1069. $this->_body = $this->word_wrap($this->_body);
  1070. }
  1071. $this->_write_headers();
  1072. $hdr = ($this->_get_protocol() === 'mail') ? $this->newline : '';
  1073. $body = '';
  1074. switch ($this->_get_content_type())
  1075. {
  1076. case 'plain':
  1077. $hdr .= 'Content-Type: text/plain; charset='.$this->charset.$this->newline
  1078. .'Content-Transfer-Encoding: '.$this->_get_encoding();
  1079. if ($this->_get_protocol() === 'mail')
  1080. {
  1081. $this->_header_str .= $hdr;
  1082. $this->_finalbody = $this->_body;
  1083. }
  1084. else
  1085. {
  1086. $this->_finalbody = $hdr.$this->newline.$this->newline.$this->_body;
  1087. }
  1088. return;
  1089. case 'html':
  1090. if ($this->send_multipart === FALSE)
  1091. {
  1092. $hdr .= 'Content-Type: text/html; charset='.$this->charset.$this->newline
  1093. .'Content-Transfer-Encoding: quoted-printable';
  1094. }
  1095. else
  1096. {
  1097. $boundary = uniqid('B_ALT_');
  1098. $hdr .= 'Content-Type: multipart/alternative; boundary="'.$boundary.'"';
  1099. $body .= $this->_get_mime_message().$this->newline.$this->newline
  1100. .'--'.$boundary.$this->newline
  1101. .'Content-Type: text/plain; charset='.$this->charset.$this->newline
  1102. .'Content-Transfer-Encoding: '.$this->_get_encoding().$this->newline.$this->newline
  1103. .$this->_get_alt_message().$this->newline.$this->newline
  1104. .'--'.$boundary.$this->newline
  1105. .'Content-Type: text/html; charset='.$this->charset.$this->newline
  1106. .'Content-Transfer-Encoding: quoted-printable'.$this->newline.$this->newline;
  1107. }
  1108. $this->_finalbody = $body.$this->_prep_quoted_printable($this->_body).$this->newline.$this->newline;
  1109. if ($this->_get_protocol() === 'mail')
  1110. {
  1111. $this->_header_str .= $hdr;
  1112. }
  1113. else
  1114. {
  1115. $this->_finalbody = $hdr.$this->newline.$this->newline.$this->_finalbody;
  1116. }
  1117. if ($this->send_multipart !== FALSE)
  1118. {
  1119. $this->_finalbody .= '--'.$boundary.'--';
  1120. }
  1121. return;
  1122. case 'plain-attach':
  1123. $boundary = uniqid('B_ATC_');
  1124. $hdr .= 'Content-Type: multipart/mixed; boundary="'.$boundary.'"';
  1125. if ($this->_get_protocol() === 'mail')
  1126. {
  1127. $this->_header_str .= $hdr;
  1128. }
  1129. $body .= $this->_get_mime_message().$this->newline
  1130. .$this->newline
  1131. .'--'.$boundary.$this->newline
  1132. .'Content-Type: text/plain; charset='.$this->charset.$this->newline
  1133. .'Content-Transfer-Encoding: '.$this->_get_encoding().$this->newline
  1134. .$this->newline
  1135. .$this->_body.$this->newline.$this->newline;
  1136. $this->_append_attachments($body, $boundary);
  1137. break;
  1138. case 'html-attach':
  1139. $alt_boundary = uniqid('B_ALT_');
  1140. $last_boundary = NULL;
  1141. if ($this->_attachments_have_multipart('mixed'))
  1142. {
  1143. $atc_boundary = uniqid('B_ATC_');
  1144. $hdr .= 'Content-Type: multipart/mixed; boundary="'.$atc_boundary.'"';
  1145. $last_boundary = $atc_boundary;
  1146. }
  1147. if ($this->_attachments_have_multipart('related'))
  1148. {
  1149. $rel_boundary = uniqid('B_REL_');
  1150. $rel_boundary_header = 'Content-Type: multipart/related; boundary="'.$rel_boundary.'"';
  1151. if (isset($last_boundary))
  1152. {
  1153. $body .= '--'.$last_boundary.$this->newline.$rel_boundary_header;
  1154. }
  1155. else
  1156. {
  1157. $hdr .= $rel_boundary_header;
  1158. }
  1159. $last_boundary = $rel_boundary;
  1160. }
  1161. if ($this->_get_protocol() === 'mail')
  1162. {
  1163. $this->_header_str .= $hdr;
  1164. }
  1165. self::strlen($body) && $body .= $this->newline.$this->newline;
  1166. $body .= $this->_get_mime_message().$this->newline.$this->newline
  1167. .'--'.$last_boundary.$this->newline
  1168. .'Content-Type: multipart/alternative; boundary="'.$alt_boundary.'"'.$this->newline.$this->newline
  1169. .'--'.$alt_boundary.$this->newline
  1170. .'Content-Type: text/plain; charset='.$this->charset.$this->newline
  1171. .'Content-Transfer-Encoding: '.$this->_get_encoding().$this->newline.$this->newline
  1172. .$this->_get_alt_message().$this->newline.$this->newline
  1173. .'--'.$alt_boundary.$this->newline
  1174. .'Content-Type: text/html; charset='.$this->charset.$this->newline
  1175. .'Content-Transfer-Encoding: quoted-printable'.$this->newline.$this->newline
  1176. .$this->_prep_quoted_printable($this->_body).$this->newline.$this->newline
  1177. .'--'.$alt_boundary.'--'.$this->newline.$this->newline;
  1178. if ( ! empty($rel_boundary))
  1179. {
  1180. $body .= $this->newline.$this->newline;
  1181. $this->_append_attachments($body, $rel_boundary, 'related');
  1182. }
  1183. // multipart/mixed attachments
  1184. if ( ! empty($atc_boundary))
  1185. {
  1186. $body .= $this->newline.$this->newline;
  1187. $this->_append_attachments($body, $atc_boundary, 'mixed');
  1188. }
  1189. break;
  1190. }
  1191. $this->_finalbody = ($this->_get_protocol() === 'mail')
  1192. ? $body
  1193. : $hdr.$this->newline.$this->newline.$body;
  1194. }
  1195. // --------------------------------------------------------------------
  1196. protected function _attachments_have_multipart($type)
  1197. {
  1198. foreach ($this->_attachments as &$attachment)
  1199. {
  1200. if ($attachment['multipart'] === $type)
  1201. {
  1202. return TRUE;
  1203. }
  1204. }
  1205. return FALSE;
  1206. }
  1207. // --------------------------------------------------------------------
  1208. /**
  1209. * Prepares attachment string
  1210. *
  1211. * @param string $body Message body to append to
  1212. * @param string $boundary Multipart boundary
  1213. * @param string $multipart When provided, only attachments of this type will be processed
  1214. * @return string
  1215. */
  1216. protected function _append_attachments(&$body, $boundary, $multipart = null)
  1217. {
  1218. for ($i = 0, $c = count($this->_attachments); $i < $c; $i++)
  1219. {
  1220. if (isset($multipart) && $this->_attachments[$i]['multipart'] !== $multipart)
  1221. {
  1222. continue;
  1223. }
  1224. $name = isset($this->_attachments[$i]['name'][1])
  1225. ? $this->_attachments[$i]['name'][1]
  1226. : basename($this->_attachments[$i]['name'][0]);
  1227. $body .= '--'.$boundary.$this->newline
  1228. .'Content-Type: '.$this->_attachments[$i]['type'].'; name="'.$name.'"'.$this->newline
  1229. .'Content-Disposition: '.$this->_attachments[$i]['disposition'].';'.$this->newline
  1230. .'Content-Transfer-Encoding: base64'.$this->newline
  1231. .(empty($this->_attachments[$i]['cid']) ? '' : 'Content-ID: <'.$this->_attachments[$i]['cid'].'>'.$this->newline)
  1232. .$this->newline
  1233. .$this->_attachments[$i]['content'].$this->newline;
  1234. }
  1235. // $name won't be set if no attachments were appended,
  1236. // and therefore a boundary wouldn't be necessary
  1237. empty($name) OR $body .= '--'.$boundary.'--';
  1238. }
  1239. // --------------------------------------------------------------------
  1240. /**
  1241. * Prep Quoted Printable
  1242. *
  1243. * Prepares string for Quoted-Printable Content-Transfer-Encoding
  1244. * Refer to RFC 2045 https://www.ietf.org/rfc/rfc2045.txt
  1245. *
  1246. * @param string
  1247. * @return string
  1248. */
  1249. protected function _prep_quoted_printable($str)
  1250. {
  1251. // ASCII code numbers for "safe" characters that can always be
  1252. // used literally, without encoding, as described in RFC 2049.
  1253. // https://www.ietf.org/rfc/rfc2049.txt
  1254. static $ascii_safe_chars = array(
  1255. // ' ( ) + , - . / : = ?
  1256. 39, 40, 41, 43, 44, 45, 46, 47, 58, 61, 63,
  1257. // numbers
  1258. 48, 49, 50, 51, 52, 53, 54, 55, 56, 57,
  1259. // upper-case letters
  1260. 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,
  1261. // lower-case letters
  1262. 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
  1263. );
  1264. // We are intentionally wrapping so mail servers will encode characters
  1265. // properly and MUAs will behave, so {unwrap} must go!
  1266. $str = str_replace(array('{unwrap}', '{/unwrap}'), '', $str);
  1267. // RFC 2045 specifies CRLF as "\r\n".
  1268. // However, many developers choose to override that and violate
  1269. // the RFC rules due to (apparently) a bug in MS Exchange,
  1270. // which only works with "\n".
  1271. if ($this->crlf === "\r\n")
  1272. {
  1273. return quoted_printable_encode($str);
  1274. }
  1275. // Reduce multiple spaces & remove nulls
  1276. $str = preg_replace(array('| +|', '/\x00+/'), array(' ', ''), $str);
  1277. // Standardize newlines
  1278. if (strpos($str, "\r") !== FALSE)
  1279. {
  1280. $str = str_replace(array("\r\n", "\r"), "\n", $str);
  1281. }
  1282. $escape = '=';
  1283. $output = '';
  1284. foreach (explode("\n", $str) as $line)
  1285. {
  1286. $length = self::strlen($line);
  1287. $temp = '';
  1288. // Loop through each character in the line to add soft-wrap
  1289. // characters at the end of a line " =\r\n" and add the newly
  1290. // processed line(s) to the output (see comment on $crlf class property)
  1291. for ($i = 0; $i < $length; $i++)
  1292. {
  1293. // Grab the next character
  1294. $char = $line[$i];
  1295. $ascii = ord($char);
  1296. // Convert spaces and tabs but only if it's the end of the line
  1297. if ($ascii === 32 OR $ascii === 9)
  1298. {
  1299. if ($i === ($length - 1))
  1300. {
  1301. $char = $escape.sprintf('%02s', dechex($ascii));
  1302. }
  1303. }
  1304. // DO NOT move this below the $ascii_safe_chars line!
  1305. //
  1306. // = (equals) signs are allowed by RFC2049, but must be encoded
  1307. // as they are the encoding delimiter!
  1308. elseif ($ascii === 61)
  1309. {
  1310. $char = $escape.strtoupper(sprintf('%02s', dechex($ascii))); // =3D
  1311. }
  1312. elseif ( ! in_array($ascii, $ascii_safe_chars, TRUE))
  1313. {
  1314. $char = $escape.strtoupper(sprintf('%02s', dechex($ascii)));
  1315. }
  1316. // If we're at the character limit, add the line to the output,
  1317. // reset our temp variable, and keep on chuggin'
  1318. if ((self::strlen($temp) + self::strlen($char)) >= 76)
  1319. {
  1320. $output .= $temp.$escape.$this->crlf;
  1321. $temp = '';
  1322. }
  1323. // Add the character to our temporary line
  1324. $temp .= $char;
  1325. }
  1326. // Add our completed line to the output
  1327. $output .= $temp.$this->crlf;
  1328. }
  1329. // get rid of extra CRLF tacked onto the end
  1330. return self::substr($output, 0, self::strlen($this->crlf) * -1);
  1331. }
  1332. // --------------------------------------------------------------------
  1333. /**
  1334. * Prep Q Encoding
  1335. *
  1336. * Performs "Q Encoding" on a string for use in email headers.
  1337. * It's related but not identical to quoted-printable, so it has its
  1338. * own method.
  1339. *
  1340. * @param string
  1341. * @return string
  1342. */
  1343. protected function _prep_q_encoding($str)
  1344. {
  1345. $str = str_replace(array("\r", "\n"), '', $str);
  1346. if ($this->charset === 'UTF-8')
  1347. {
  1348. // Note: We used to have mb_encode_mimeheader() as the first choice
  1349. // here, but it turned out to be buggy and unreliable. DO NOT
  1350. // re-add it! -- Narf
  1351. if (ICONV_ENABLED === TRUE)
  1352. {
  1353. $output = @iconv_mime_encode('', $str,
  1354. array(
  1355. 'scheme' => 'Q',
  1356. 'line-length' => 76,
  1357. 'input-charset' => $this->charset,
  1358. 'output-charset' => $this->charset,
  1359. 'line-break-chars' => $this->crlf
  1360. )
  1361. );
  1362. // There are reports that iconv_mime_encode() might fail and return FALSE
  1363. if ($output !== FALSE)
  1364. {
  1365. // iconv_mime_encode() will always put a header field name.
  1366. // We've passed it an empty one, but it still prepends our
  1367. // encoded string with ': ', so we need to strip it.
  1368. return self::substr($output, 2);
  1369. }
  1370. $chars = iconv_strlen($str, 'UTF-8');
  1371. }
  1372. elseif (MB_ENABLED === TRUE)
  1373. {
  1374. $chars = mb_strlen($str, 'UTF-8');
  1375. }
  1376. }
  1377. // We might already have this set for UTF-8
  1378. isset($chars) OR $chars = self::strlen($str);
  1379. $output = '=?'.$this->charset.'?Q?';
  1380. for ($i = 0, $length = self::strlen($output); $i < $chars; $i++)
  1381. {
  1382. $chr = ($this->charset === 'UTF-8' && ICONV_ENABLED === TRUE)
  1383. ? '='.implode('=', str_split(strtoupper(bin2hex(iconv_substr($str, $i, 1, $this->charset))), 2))
  1384. : '='.strtoupper(bin2hex($str[$i]));
  1385. // RFC 2045 sets a limit of 76 characters per line.
  1386. // We'll append ?= to the end of each line though.
  1387. if ($length + ($l = self::strlen($chr)) > 74)
  1388. {
  1389. $output .= '?='.$this->crlf // EOL
  1390. .' =?'.$this->charset.'?Q?'.$chr; // New line
  1391. $length = 6 + self::strlen($this->charset) + $l; // Reset the length for the new line
  1392. }
  1393. else
  1394. {
  1395. $output .= $chr;
  1396. $length += $l;
  1397. }
  1398. }
  1399. // End the header
  1400. return $output.'?=';
  1401. }
  1402. // --------------------------------------------------------------------
  1403. /**
  1404. * Send Email
  1405. *
  1406. * @param bool $auto_clear = TRUE
  1407. * @return bool
  1408. */
  1409. public function send($auto_clear = TRUE)
  1410. {
  1411. if ( ! isset($this->_headers['From']))
  1412. {
  1413. $this->_set_error_message('lang:email_no_from');
  1414. return FALSE;
  1415. }
  1416. if ($this->_replyto_flag === FALSE)
  1417. {
  1418. $this->reply_to($this->_headers['From']);
  1419. }
  1420. if (empty($this->_recipients) && ! isset($this->_headers['To'])
  1421. && empty($this->_bcc_array) && ! isset($this->_headers['Bcc'])
  1422. && ! isset($this->_headers['Cc']))
  1423. {
  1424. $this->_set_error_message('lang:email_no_recipients');
  1425. return FALSE;
  1426. }
  1427. $this->_build_headers();
  1428. if ($this->bcc_batch_mode && count($this->_bcc_array) > $this->bcc_batch_size)
  1429. {
  1430. $this->batch_bcc_send();
  1431. if ($auto_clear)
  1432. {
  1433. $this->clear();
  1434. }
  1435. return TRUE;
  1436. }
  1437. $this->_build_message();
  1438. $result = $this->_spool_email();
  1439. if ($result && $auto_clear)
  1440. {
  1441. $this->clear();
  1442. }
  1443. return $result;
  1444. }
  1445. // --------------------------------------------------------------------
  1446. /**
  1447. * Batch Bcc Send. Sends groups of BCCs in batches
  1448. *
  1449. * @return void
  1450. */
  1451. public function batch_bcc_send()
  1452. {
  1453. $float = $this->bcc_batch_size - 1;
  1454. $set = '';
  1455. $chunk = array();
  1456. for ($i = 0, $c = count($this->_bcc_array); $i < $c; $i++)
  1457. {
  1458. if (isset($this->_bcc_array[$i]))
  1459. {
  1460. $set .= ', '.$this->_bcc_array[$i];
  1461. }
  1462. if ($i === $float)
  1463. {
  1464. $chunk[] = self::substr($set, 1);
  1465. $float += $this->bcc_batch_size;
  1466. $set = '';
  1467. }
  1468. if ($i === $c-1)
  1469. {
  1470. $chunk[] = self::substr($set, 1);
  1471. }
  1472. }
  1473. for ($i = 0, $c = count($chunk); $i < $c; $i++)
  1474. {
  1475. unset($this->_headers['Bcc']);
  1476. $bcc = $this->clean_email($this->_str_to_array($chunk[$i]));
  1477. if ($this->protocol !== 'smtp')
  1478. {
  1479. $this->set_header('Bcc', implode(', ', $bcc));
  1480. }
  1481. else
  1482. {
  1483. $this->_bcc_array = $bcc;
  1484. }
  1485. $this->_build_message();
  1486. $this->_spool_email();
  1487. }
  1488. }
  1489. // --------------------------------------------------------------------
  1490. /**
  1491. * Unwrap special elements
  1492. *
  1493. * @return void
  1494. */
  1495. protected function _unwrap_specials()
  1496. {
  1497. $this->_finalbody = preg_replace_callback('/\{unwrap\}(.*?)\{\/unwrap\}/si', array($this, '_remove_nl_callback'), $this->_finalbody);
  1498. }
  1499. // --------------------------------------------------------------------
  1500. /**
  1501. * Strip line-breaks via callback
  1502. *
  1503. * @param string $matches
  1504. * @return string
  1505. */
  1506. protected function _remove_nl_callback($matches)
  1507. {
  1508. if (strpos($matches[1], "\r") !== FALSE OR strpos($matches[1], "\n") !== FALSE)
  1509. {
  1510. $matches[1] = str_replace(array("\r\n", "\r", "\n"), '', $matches[1]);
  1511. }
  1512. return $matches[1];
  1513. }
  1514. // --------------------------------------------------------------------
  1515. /**
  1516. * Spool mail to the mail server
  1517. *
  1518. * @return bool
  1519. */
  1520. protected function _spool_email()
  1521. {
  1522. $this->_unwrap_specials();
  1523. $protocol = $this->_get_protocol();
  1524. $method = '_send_with_'.$protocol;
  1525. if ( ! $this->$method())
  1526. {
  1527. $this->_set_error_message('lang:email_send_failure_'.($protocol === 'mail' ? 'phpmail' : $protocol));
  1528. return FALSE;
  1529. }
  1530. $this->_set_error_message('lang:email_sent', $protocol);
  1531. return TRUE;
  1532. }
  1533. // --------------------------------------------------------------------
  1534. /**
  1535. * Validate email for shell
  1536. *
  1537. * Applies stricter, shell-safe validation to email addresses.
  1538. * Introduced to prevent RCE via sendmail's -f option.
  1539. *
  1540. * @see https://github.com/bcit-ci/CodeIgniter/issues/4963
  1541. * @see https://gist.github.com/Zenexer/40d02da5e07f151adeaeeaa11af9ab36
  1542. * @license https://creativecommons.org/publicdomain/zero/1.0/ CC0 1.0, Public Domain
  1543. *
  1544. * Credits for the base concept go to Paul Buonopane <paul@namepros.com>
  1545. *
  1546. * @param string $email
  1547. * @return bool
  1548. */
  1549. protected function _validate_email_for_shell(&$email)
  1550. {
  1551. if (function_exists('idn_to_ascii') && $atpos = strpos($email, '@'))
  1552. {
  1553. list($account, $domain) = explode('@', $email, 2);
  1554. $domain = defined('INTL_IDNA_VARIANT_UTS46')
  1555. ? idn_to_ascii($domain, 0, INTL_IDNA_VARIANT_UTS46)
  1556. : idn_to_ascii($domain);
  1557. if ($domain !== FALSE)
  1558. {
  1559. $email = $account.'@'.$domain;
  1560. }
  1561. }
  1562. return (filter_var($email, FILTER_VALIDATE_EMAIL) === $email && preg_match('#\A[a-z0-9._+-]+@[a-z0-9.-]{1,253}\z#i', $email));
  1563. }
  1564. // --------------------------------------------------------------------
  1565. /**
  1566. * Send using mail()
  1567. *
  1568. * @return bool
  1569. */
  1570. protected function _send_with_mail()
  1571. {
  1572. if (is_array($this->_recipients))
  1573. {
  1574. $this->_recipients = implode(', ', $this->_recipients);
  1575. }
  1576. // _validate_email_for_shell() below accepts by reference,
  1577. // so this needs to be assigned to a variable
  1578. $from = $this->clean_email($this->_headers['Return-Path']);
  1579. if ( ! $this->_validate_email_for_shell($from))
  1580. {
  1581. return mail($this->_recipients, $this->_subject, $this->_finalbody, $this->_header_str);
  1582. }
  1583. // most documentation of sendmail using the "-f" flag lacks a space after it, however
  1584. // we've encountered servers that seem to require it to be in place.
  1585. return mail($this->_recipients, $this->_subject, $this->_finalbody, $this->_header_str, '-f '.$from);
  1586. }
  1587. // --------------------------------------------------------------------
  1588. /**
  1589. * Send using Sendmail
  1590. *
  1591. * @return bool
  1592. */
  1593. protected function _send_with_sendmail()
  1594. {
  1595. // _validate_email_for_shell() below accepts by reference,
  1596. // so this needs to be assigned to a variable
  1597. $from = $this->clean_email($this->_headers['From']);
  1598. if ($this->_validate_email_for_shell($from))
  1599. {
  1600. $from = '-f '.$from;
  1601. }
  1602. else
  1603. {
  1604. $from = '';
  1605. }
  1606. // is popen() enabled?
  1607. if ( ! function_usable('popen') OR FALSE === ($fp = @popen($this->mailpath.' -oi '.$from.' -t', 'w')))
  1608. {
  1609. // server probably has popen disabled, so nothing we can do to get a verbose error.
  1610. return FALSE;
  1611. }
  1612. fputs($fp, $this->_header_str);
  1613. fputs($fp, $this->_finalbody);
  1614. $status = pclose($fp);
  1615. if ($status !== 0)
  1616. {
  1617. $this->_set_error_message('lang:email_exit_status', $status);
  1618. $this->_set_error_message('lang:email_no_socket');
  1619. return FALSE;
  1620. }
  1621. return TRUE;
  1622. }
  1623. // --------------------------------------------------------------------
  1624. /**
  1625. * Send using SMTP
  1626. *
  1627. * @return bool
  1628. */
  1629. protected function _send_with_smtp()
  1630. {
  1631. if ($this->smtp_host === '')
  1632. {
  1633. $this->_set_error_message('lang:email_no_hostname');
  1634. return FALSE;
  1635. }
  1636. if ( ! $this->_smtp_connect() OR ! $this->_smtp_authenticate())
  1637. {
  1638. return FALSE;
  1639. }
  1640. if ( ! $this->_send_command('from', $this->clean_email($this->_headers['From'])))
  1641. {
  1642. $this->_smtp_end();
  1643. return FALSE;
  1644. }
  1645. foreach ($this->_recipients as $val)
  1646. {
  1647. if ( ! $this->_send_command('to', $val))
  1648. {
  1649. $this->_smtp_end();
  1650. return FALSE;
  1651. }
  1652. }
  1653. foreach ($this->_cc_array as $val)
  1654. {
  1655. if ($val !== '' && ! $this->_send_command('to', $val))
  1656. {
  1657. $this->_smtp_end();
  1658. return FALSE;
  1659. }
  1660. }
  1661. foreach ($this->_bcc_array as $val)
  1662. {
  1663. if ($val !== '' && ! $this->_send_command('to', $val))
  1664. {
  1665. $this->_smtp_end();
  1666. return FALSE;
  1667. }
  1668. }
  1669. if ( ! $this->_send_command('data'))
  1670. {
  1671. $this->_smtp_end();
  1672. return FALSE;
  1673. }
  1674. // perform dot transformation on any lines that begin with a dot
  1675. $this->_send_data($this->_header_str.preg_replace('/^\./m', '..$1', $this->_finalbody));
  1676. $this->_send_data('.');
  1677. $reply = $this->_get_smtp_data();
  1678. $this->_set_error_message($reply);
  1679. $this->_smtp_end();
  1680. if (strpos($reply, '250') !== 0)
  1681. {
  1682. $this->_set_error_message('lang:email_smtp_error', $reply);
  1683. return FALSE;
  1684. }
  1685. return TRUE;
  1686. }
  1687. // --------------------------------------------------------------------
  1688. /**
  1689. * SMTP End
  1690. *
  1691. * Shortcut to send RSET or QUIT depending on keep-alive
  1692. *
  1693. * @return void
  1694. */
  1695. protected function _smtp_end()
  1696. {
  1697. $this->_send_command($this->smtp_keepalive ? 'reset' : 'quit');
  1698. }
  1699. // --------------------------------------------------------------------
  1700. /**
  1701. * SMTP Connect
  1702. *
  1703. * @return string
  1704. */
  1705. protected function _smtp_connect()
  1706. {
  1707. if (is_resource($this->_smtp_connect))
  1708. {
  1709. return TRUE;
  1710. }
  1711. $ssl = ($this->smtp_crypto === 'ssl') ? 'ssl://' : '';
  1712. $this->_smtp_connect = fsockopen(
  1713. $ssl.$this->smtp_host,
  1714. $this->smtp_port,
  1715. $errno,
  1716. $errstr,
  1717. $this->smtp_timeout
  1718. );
  1719. if ( ! is_resource($this->_smtp_connect))
  1720. {
  1721. $this->_set_error_message('lang:email_smtp_error', $errno.' '.$errstr);
  1722. return FALSE;
  1723. }
  1724. stream_set_timeout($this->_smtp_connect, $this->smtp_timeout);
  1725. $this->_set_error_message($this->_get_smtp_data());
  1726. if ($this->smtp_crypto === 'tls')
  1727. {
  1728. $this->_send_command('hello');
  1729. $this->_send_command('starttls');
  1730. /**
  1731. * STREAM_CRYPTO_METHOD_TLS_CLIENT is quite the mess ...
  1732. *
  1733. * - On PHP <5.6 it doesn't even mean TLS, but SSL 2.0, and there's no option to use actual TLS
  1734. * - On PHP 5.6.0-5.6.6, >=7.2 it means negotiation with any of TLS 1.0, 1.1, 1.2
  1735. * - On PHP 5.6.7-7.1.* it means only TLS 1.0
  1736. *
  1737. * We want the negotiation, so we'll force it below ...
  1738. */
  1739. $method = is_php('5.6')
  1740. ? STREAM_CRYPTO_METHOD_TLSv1_0_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT | STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT
  1741. : STREAM_CRYPTO_METHOD_TLS_CLIENT;
  1742. $crypto = stream_socket_enable_crypto($this->_smtp_connect, TRUE, $method);
  1743. if ($crypto !== TRUE)
  1744. {
  1745. $this->_set_error_message('lang:email_smtp_error', $this->_get_smtp_data());
  1746. return FALSE;
  1747. }
  1748. }
  1749. return $this->_send_command('hello');
  1750. }
  1751. // --------------------------------------------------------------------
  1752. /**
  1753. * Send SMTP command
  1754. *
  1755. * @param string
  1756. * @param string
  1757. * @return bool
  1758. */
  1759. protected function _send_command($cmd, $data = '')
  1760. {
  1761. switch ($cmd)
  1762. {
  1763. case 'hello':
  1764. if ($this->_smtp_auth OR $this->_get_encoding() === '8bit')
  1765. {
  1766. $this->_send_data('EHLO '.$this->_get_hostname());
  1767. }
  1768. else
  1769. {
  1770. $this->_send_data('HELO '.$this->_get_hostname());
  1771. }
  1772. $resp = 250;
  1773. break;
  1774. case 'starttls':
  1775. $this->_send_data('STARTTLS');
  1776. $resp = 220;
  1777. break;
  1778. case 'from':
  1779. $this->_send_data('MAIL FROM:<'.$data.'>');
  1780. $resp = 250;
  1781. break;
  1782. case 'to':
  1783. if ($this->dsn)
  1784. {
  1785. $this->_send_data('RCPT TO:<'.$data.'> NOTIFY=SUCCESS,DELAY,FAILURE ORCPT=rfc822;'.$data);
  1786. }
  1787. else
  1788. {
  1789. $this->_send_data('RCPT TO:<'.$data.'>');
  1790. }
  1791. $resp = 250;
  1792. break;
  1793. case 'data':
  1794. $this->_send_data('DATA');
  1795. $resp = 354;
  1796. break;
  1797. case 'reset':
  1798. $this->_send_data('RSET');
  1799. $resp = 250;
  1800. break;
  1801. case 'quit':
  1802. $this->_send_data('QUIT');
  1803. $resp = 221;
  1804. break;
  1805. }
  1806. $reply = $this->_get_smtp_data();
  1807. $this->_debug_msg[] = '<pre>'.$cmd.': '.$reply.'</pre>';
  1808. if ((int) self::substr($reply, 0, 3) !== $resp)
  1809. {
  1810. $this->_set_error_message('lang:email_smtp_error', $reply);
  1811. return FALSE;
  1812. }
  1813. if ($cmd === 'quit')
  1814. {
  1815. fclose($this->_smtp_connect);
  1816. }
  1817. return TRUE;
  1818. }
  1819. // --------------------------------------------------------------------
  1820. /**
  1821. * SMTP Authenticate
  1822. *
  1823. * @return bool
  1824. */
  1825. protected function _smtp_authenticate()
  1826. {
  1827. if ( ! $this->_smtp_auth)
  1828. {
  1829. return TRUE;
  1830. }
  1831. if ($this->smtp_user === '' && $this->smtp_pass === '')
  1832. {
  1833. $this->_set_error_message('lang:email_no_smtp_unpw');
  1834. return FALSE;
  1835. }
  1836. $this->_send_data('AUTH LOGIN');
  1837. $reply = $this->_get_smtp_data();
  1838. if (strpos($reply, '503') === 0) // Already authenticated
  1839. {
  1840. return TRUE;
  1841. }
  1842. elseif (strpos($reply, '334') !== 0)
  1843. {
  1844. $this->_set_error_message('lang:email_failed_smtp_login', $reply);
  1845. return FALSE;
  1846. }
  1847. $this->_send_data(base64_encode($this->smtp_user));
  1848. $reply = $this->_get_smtp_data();
  1849. if (strpos($reply, '334') !== 0)
  1850. {
  1851. $this->_set_error_message('lang:email_smtp_auth_un', $reply);
  1852. return FALSE;
  1853. }
  1854. $this->_send_data(base64_encode($this->smtp_pass));
  1855. $reply = $this->_get_smtp_data

Large files files are truncated, but you can click here to view the full file