PageRenderTime 48ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/fsn-site-central/library/Yab/Mail.php

https://gitlab.com/team_fsn/fsn-php
PHP | 624 lines | 365 code | 242 blank | 17 comment | 55 complexity | 0b4593013d8bacd1e29716dbeddbb91f MD5 | raw file
  1. <?php
  2. /**
  3. * Yab Framework
  4. *
  5. * @category Yab
  6. * @package Yab_Mail
  7. * @author Yann BELLUZZI
  8. * @copyright (c) 2010 YBellu
  9. * @license http://www.ybellu.com/yab-framework/license.html
  10. * @link http://www.ybellu.com/yab-framework
  11. */
  12. class Yab_Mail {
  13. const CRLF = "\n";
  14. const TEXT_PART_INDEX = 0;
  15. const HTML_PART_INDEX = 1;
  16. const FILES_PARTS_START_INDEX = 2;
  17. private $_boundary = null;
  18. private $_headers = array();
  19. private $_parts = array();
  20. private $_charset = 'utf-8';
  21. private $_encoding = 'quoted-printable';
  22. private $_addresses_headers = array('return-path', 'sender', 'x-sender', 'from', 'to', 'cc', 'bcc', 'reply-to');
  23. public function __construct() {
  24. $this->_boundary = '-----='.md5(uniqid(mt_rand()));
  25. }
  26. /*
  27. * Proxy methods
  28. */
  29. public function setCharset($charset) {
  30. $this->_charset = (string) $charset;
  31. return $this;
  32. }
  33. public function setEncoding($encoding) {
  34. $this->_encoding = (string) $encoding;
  35. return $this;
  36. }
  37. public function setFrom($from) {
  38. return $this->setHeader('From', $from);
  39. }
  40. public function setTo($to) {
  41. return $this->setHeader('To', $to);
  42. }
  43. public function setCc($cc) {
  44. return $this->setHeader('cc', $cc);
  45. }
  46. public function setBcc($bcc) {
  47. return $this->setHeader('bcc', $bcc);
  48. }
  49. public function setSubject($subject) {
  50. return $this->setHeader('Subject', $subject);
  51. }
  52. public function setText($text) {
  53. return $this->setPart(0, $text, 'text/plain');
  54. }
  55. public function getText() {
  56. $text = $this->getPart(self::TEXT_PART_INDEX);
  57. if($text === null)
  58. return null;
  59. return (string) $text['content'];
  60. }
  61. public function setHtml($html) {
  62. return $this->setPart(1, $html, 'text/html');
  63. }
  64. public function getHtml() {
  65. $html = $this->getPart(self::HTML_PART_INDEX);
  66. if($html === null)
  67. return null;
  68. return (string) $html['content'];
  69. }
  70. public function attach($file, $file_content = null) {
  71. if(!($file instanceof Yab_File))
  72. $file = new Yab_File($file);
  73. if($file_content === null)
  74. $file_content = (string) $file->read();
  75. return $this->setPart($this->getNextPartFileIndex(), (string) $file_content, $file->getMimeType(), $file->getName());
  76. }
  77. public function send() {
  78. $to = $this->getHeader('To');
  79. $subject = $this->getHeader('Subject');
  80. $this->remHeader('To')->remHeader('Subject');
  81. mail($to, $this->encodeHeader($subject), $this->_parts(), $this->_headers());
  82. return $this->setHeader('To', $to)->setHeader('Subject', $subject);
  83. }
  84. /*
  85. * internal methods
  86. */
  87. public function getNbParts() {
  88. return count($this->_parts);
  89. }
  90. public function getNbFilesParts() {
  91. $nb_parts = 0;
  92. foreach($this->_parts as $part_index => $part) {
  93. if(in_array($part_index, array(self::TEXT_PART_INDEX, self::HTML_PART_INDEX)))
  94. continue;
  95. $nb_parts++;
  96. }
  97. return $nb_parts;
  98. }
  99. public function getMaxPartIndex() {
  100. $max_part_index = -1;
  101. foreach(array_keys($this->_parts) as $part_index)
  102. $max_part_index = max($max_part_index, $part_index);
  103. return $max_part_index;
  104. }
  105. public function getNextPartIndex() {
  106. return $this->getMaxPartIndex() + 1;
  107. }
  108. public function getNextPartFileIndex() {
  109. return max($this->getMaxPartIndex() + 1, self::FILES_PARTS_START_INDEX);
  110. }
  111. public function isMultipart() {
  112. return 1 < $this->getNbParts();
  113. }
  114. public function isAlternative() {
  115. return array_key_exists(self::TEXT_PART_INDEX, $this->_parts) && array_key_exists(self::HTML_PART_INDEX, $this->_parts);
  116. }
  117. public function isMixed() {
  118. return array_key_exists(self::FILES_PARTS_START_INDEX, $this->_parts);
  119. }
  120. public function setHeader($name, $header) {
  121. $this->_headers[$name] = $header;
  122. return $this;
  123. }
  124. public function remHeader($name) {
  125. if(array_key_exists($name, $this->_headers))
  126. unset($this->_headers[$name]);
  127. return $this;
  128. }
  129. public function getHeader($name, $encoded = false) {
  130. if(!array_key_exists($name, $this->_headers))
  131. return null;
  132. if(in_array($name, $this->_addresses_headers))
  133. return $this->formatRfc822($this->_headers[$name], $encoded);
  134. return $encoded ? $this->encodeHeader($this->_headers[$name]) : $this->_headers[$name];
  135. }
  136. public function setPart($id, $content, $mime_type, $filename = null) {
  137. $this->_parts[$id] = array(
  138. 'headers' => array(
  139. 'Content-Disposition' => $filename === null ? 'inline' : 'attachment; filename="'.$filename.'"',
  140. 'Content-Type' => $mime_type.'; charset="'.$this->_charset.'"',
  141. 'Content-Transfer-Encoding' => $filename ? 'base64' : $this->_encoding,
  142. 'Content-ID' => $filename ? '<'.$filename.'>' : null,
  143. ),
  144. 'content' => (string) $content,
  145. );
  146. return $this;
  147. }
  148. public function getPart($id) {
  149. return array_key_exists($id, $this->_parts) ? $this->_parts[$id] : null;
  150. }
  151. public function remPart($id) {
  152. if(array_key_exists($id, $this->_parts))
  153. unset($this->_parts[$id]);
  154. return $this;
  155. }
  156. public function __toString() {
  157. try {
  158. return $this->_headers().self::CRLF.self::CRLF.$this->_parts();
  159. } catch(Yab_Exception $e) {
  160. return $e->getMessage();
  161. }
  162. }
  163. private function _headers() {
  164. if(!$this->getHeader('MIME-Version'))
  165. $this->setHeader('MIME-Version', '1.0');
  166. if(!$this->getHeader('Return-Path'))
  167. $this->setHeader('Return-Path', $this->getHeader('From'));
  168. if(!$this->getHeader('Date'))
  169. $this->setHeader('Date', date('D, j M Y H:i:s O'));
  170. $headers = '';
  171. foreach($this->_headers as $name => $header) {
  172. if(in_array(strtolower($name), array_map('strtolower', $this->_addresses_headers))) {
  173. $headers .= $name.': '.$this->formatRfc822($header, true).self::CRLF;
  174. } else {
  175. $headers .= $name.': '.$this->encodeHeader($header).self::CRLF;
  176. }
  177. }
  178. if($this->isMultipart()) {
  179. if($this->isMixed()) {
  180. $headers .= 'Content-Type: multipart/mixed; boundary="'.$this->_boundary.'"'.self::CRLF;
  181. } else {
  182. $headers .= 'Content-Type: multipart/alternative; boundary="'.$this->_boundary.'"'.self::CRLF;
  183. }
  184. } else {
  185. foreach($this->_parts as $part_index => $part)
  186. $headers .= $this->_partHeaders($part_index).self::CRLF;
  187. }
  188. return trim($headers);
  189. }
  190. private function _partHeaders($id) {
  191. if(!$part = $this->getPart($id))
  192. return '';
  193. $string = '';
  194. foreach($part['headers'] as $key => $value)
  195. $string .= $value ? $key.': '.$value.self::CRLF : '';
  196. return trim($string);
  197. }
  198. private function _partContent($id) {
  199. if(!$part = $this->getPart($id))
  200. return '';
  201. return $this->encodePart($part['content'], $this->_charset, $part['headers']['Content-Transfer-Encoding']);
  202. }
  203. private function _part($id, $boundary) {
  204. $headers = $this->_partHeaders($id);
  205. $content = $this->_partContent($id);
  206. if(!$headers && !$content)
  207. return '';
  208. return '--'.$boundary.self::CRLF.$headers.self::CRLF.self::CRLF.$content;
  209. }
  210. private function _parts() {
  211. if(!$this->isMultipart()) {
  212. foreach($this->_parts as $part_index => $part)
  213. return $this->_partContent($part_index);
  214. }
  215. $parts = 'This is a message with multiple parts in MIME format.'.self::CRLF.self::CRLF;
  216. if($this->isMixed() && $this->isAlternative()) {
  217. $parts .= '--'.$this->_boundary.self::CRLF;
  218. $alternative_boundary = '-----='.md5($this->_boundary);
  219. $parts .= 'Content-Type: multipart/alternative; boundary="'.$alternative_boundary.'"'.self::CRLF.self::CRLF;
  220. $parts .= $this->_part(self::TEXT_PART_INDEX, $alternative_boundary).self::CRLF;
  221. $parts .= $this->_part(self::HTML_PART_INDEX, $alternative_boundary).self::CRLF;
  222. $parts .= '--'.$alternative_boundary.'--'.self::CRLF;
  223. foreach($this->_parts as $part_index => $part) {
  224. if(in_array($part_index, array(self::TEXT_PART_INDEX, self::HTML_PART_INDEX)))
  225. continue;
  226. $parts .= $this->_part($part_index, $this->_boundary).self::CRLF;
  227. }
  228. } else {
  229. foreach($this->_parts as $part_index => $part)
  230. $parts .= $this->_part($part_index, $this->_boundary).self::CRLF;
  231. }
  232. return $parts.'--'.$this->_boundary.'--';
  233. }
  234. public function splitAddresses($string, $split_chars = array(',', ';'), $quote_chars = array('"'), $escape_chars = array('\\')) {
  235. if(is_array($string))
  236. return $string;
  237. $length = strlen($string);
  238. $part = '';
  239. $parts = array();
  240. $escaped_char = false;
  241. $quoted_string = false;
  242. for($i = 0; $i < $length; $i++) {
  243. $char = $string[$i];
  244. if(in_array($char, $split_chars)) {
  245. if(!$quoted_string) {
  246. array_push($parts, $part);
  247. $part = '';
  248. $char = null;
  249. }
  250. } elseif(in_array($char, $quote_chars)) {
  251. if(!$escaped_char)
  252. $quoted_string = !$quoted_string;
  253. }
  254. $escaped_char = false;
  255. if(in_array($char, $escape_chars) && !$escaped_char)
  256. $escaped_char = true;
  257. $part .= $char;
  258. }
  259. array_push($parts, $part);
  260. return $parts;
  261. }
  262. public function formatRfc822($addresses, $encoded = false) {
  263. $validator = new Yab_Validator_Email();
  264. $addresses = $this->splitAddresses($addresses);
  265. $rfc822_addresses = array();
  266. foreach($addresses as $address) {
  267. $address = trim($address);
  268. $address = preg_replace('#(<[^>]+>)#s', ' $1', $address);
  269. $address = preg_replace('#"([^\s"]+@[^\s"]+)#s', '" $1', $address);
  270. $address = preg_split('#\s+#s', $address);
  271. $rfc822_address = array_pop($address);
  272. $rfc822_address = trim($rfc822_address, '<>');
  273. if(!$validator->validate($rfc822_address))
  274. throw new Yab_Exception('"'.$rfc822_address.'" is not a valid email');
  275. $rfc822_address = '<'.$rfc822_address.'>';
  276. $rfc822_alias = implode(' ', $address);
  277. $rfc822_alias = trim($rfc822_alias);
  278. if($rfc822_alias) {
  279. if(!preg_match('#^".+"$#s', $rfc822_alias))
  280. $rfc822_alias = '"'.str_replace('"', '\"', $rfc822_alias).'"';
  281. if($encoded)
  282. $rfc822_alias = $this->encodeHeader($rfc822_alias);
  283. $rfc822_address = $rfc822_alias.' '.$rfc822_address;
  284. }
  285. array_push($rfc822_addresses, $rfc822_address);
  286. }
  287. return implode(', ', $rfc822_addresses);
  288. }
  289. public function encodeHeader($header, $charset = null, $encoding = null, $max_length = 76) {
  290. $html = new Yab_Filter_Html();
  291. $regexp = '#([\\x00-\\x1F\\x3D\\x3F\\x7F-\\xFF])#e';
  292. if(!preg_match($regexp, $header))
  293. return $header;
  294. if($encoding === null)
  295. $encoding = $this->_encoding;
  296. if(!in_array($encoding, array('base64', 'quoted-printable')))
  297. return $header;
  298. if($charset === null)
  299. $charset = $this->_charset;
  300. $prefix = '=?'.$charset.'?'.strtoupper(substr($encoding, 0, 1)).'?';
  301. $suffix = '?=';
  302. $line_length = $max_length - strlen($prefix) - strlen($suffix);
  303. $header = trim($header);
  304. if($encoding == 'quoted-printable') {
  305. $header = preg_replace($regexp, '"=".strtoupper(dechex(ord("\1")))', $header);
  306. $header = preg_replace('#\s#', '_', $header);
  307. preg_match_all('#.{1,'.($line_length - 2).'}([^=]{0,2})?#', $header, $header);
  308. $header = $header[0];
  309. foreach($header as $key => $value)
  310. $header[$key] = $prefix.$value.$suffix;
  311. } elseif($encoding == 'base64') {
  312. $header = base64_encode($header);
  313. $header = str_split($header, $line_length);
  314. foreach($header as $key => $value)
  315. $header[$key] = $prefix.$value.$suffix;
  316. }
  317. return implode(self::CRLF."\t", $header);
  318. }
  319. public function encodePart($part, $charset = null, $encoding = null, $max_length = 76) {
  320. if($encoding === null)
  321. $encoding = $this->_encoding;
  322. if($encoding == 'base64')
  323. return chunk_split(base64_encode($part), $max_length);
  324. if($encoding == 'quoted-printable') {
  325. $emulate_imap_8bit = true;
  326. $regexp = '#[^\x09\x20\x21-\x3C\x3E-\x7E]#e';
  327. if($emulate_imap_8bit)
  328. $regexp = '#[^\x20\x21-\x3C\x3E-\x7E]#e';
  329. $lines = preg_split('#(\r\n|\r|\n)#', $part);
  330. foreach($lines as $line_number => $line) {
  331. if(strlen($line) === 0)
  332. continue;
  333. $line = preg_replace($regexp, 'sprintf("=%02X", ord("$0"));', $line);
  334. $line_length = strlen($line);
  335. $last_char = ord($line[$line_length - 1]);
  336. if(!($emulate_imap_8bit && ($line_number == count($lines) - 1))) {
  337. if(($last_char == 0x09) || ($last_char == 0x20)) {
  338. $line[$line_length - 1] = '=';
  339. $line .= $last_char == 0x09 ? '09' : '20';
  340. }
  341. }
  342. if($emulate_imap_8bit)
  343. $line = str_replace(' =0D', '=20=0D', $line);
  344. preg_match_all('#.{1,'.($max_length - 3).'}([^=]{0,2})?#', $line, $match);
  345. $line = implode('='.self::CRLF, $match[0]);
  346. $lines[$line_number] = $line;
  347. }
  348. return implode(self::CRLF, $lines);
  349. }
  350. return $part;
  351. }
  352. }
  353. // Do not clause PHP tags unless it is really necessary