mimePart.php 44 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260
  1. <?php
  2. /**
  3. * The Mail_mimePart class is used to create MIME E-mail messages
  4. *
  5. * This class enables you to manipulate and build a mime email
  6. * from the ground up. The Mail_Mime class is a userfriendly api
  7. * to this class for people who aren't interested in the internals
  8. * of mime mail.
  9. * This class however allows full control over the email.
  10. *
  11. * Compatible with PHP versions 4 and 5
  12. *
  13. * LICENSE: This LICENSE is in the BSD license style.
  14. * Copyright (c) 2002-2003, Richard Heyes <richard@phpguru.org>
  15. * Copyright (c) 2003-2006, PEAR <pear-group@php.net>
  16. * All rights reserved.
  17. *
  18. * Redistribution and use in source and binary forms, with or
  19. * without modification, are permitted provided that the following
  20. * conditions are met:
  21. *
  22. * - Redistributions of source code must retain the above copyright
  23. * notice, this list of conditions and the following disclaimer.
  24. * - Redistributions in binary form must reproduce the above copyright
  25. * notice, this list of conditions and the following disclaimer in the
  26. * documentation and/or other materials provided with the distribution.
  27. * - Neither the name of the authors, nor the names of its contributors
  28. * may be used to endorse or promote products derived from this
  29. * software without specific prior written permission.
  30. *
  31. * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
  32. * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
  33. * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  34. * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
  35. * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
  36. * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
  37. * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
  38. * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
  39. * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
  40. * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
  41. * THE POSSIBILITY OF SUCH DAMAGE.
  42. *
  43. * @category Mail
  44. * @package Mail_Mime
  45. * @author Richard Heyes <richard@phpguru.org>
  46. * @author Cipriano Groenendal <cipri@php.net>
  47. * @author Sean Coates <sean@php.net>
  48. * @author Aleksander Machniak <alec@php.net>
  49. * @copyright 2003-2006 PEAR <pear-group@php.net>
  50. * @license http://www.opensource.org/licenses/bsd-license.php BSD License
  51. * @version CVS: $Id$
  52. * @link http://pear.php.net/package/Mail_mime
  53. */
  54. /**
  55. * The Mail_mimePart class is used to create MIME E-mail messages
  56. *
  57. * This class enables you to manipulate and build a mime email
  58. * from the ground up. The Mail_Mime class is a userfriendly api
  59. * to this class for people who aren't interested in the internals
  60. * of mime mail.
  61. * This class however allows full control over the email.
  62. *
  63. * @category Mail
  64. * @package Mail_Mime
  65. * @author Richard Heyes <richard@phpguru.org>
  66. * @author Cipriano Groenendal <cipri@php.net>
  67. * @author Sean Coates <sean@php.net>
  68. * @author Aleksander Machniak <alec@php.net>
  69. * @copyright 2003-2006 PEAR <pear-group@php.net>
  70. * @license http://www.opensource.org/licenses/bsd-license.php BSD License
  71. * @version Release: @package_version@
  72. * @link http://pear.php.net/package/Mail_mime
  73. */
  74. class Mail_mimePart
  75. {
  76. /**
  77. * The encoding type of this part
  78. *
  79. * @var string
  80. * @access private
  81. */
  82. var $_encoding;
  83. /**
  84. * An array of subparts
  85. *
  86. * @var array
  87. * @access private
  88. */
  89. var $_subparts;
  90. /**
  91. * The output of this part after being built
  92. *
  93. * @var string
  94. * @access private
  95. */
  96. var $_encoded;
  97. /**
  98. * Headers for this part
  99. *
  100. * @var array
  101. * @access private
  102. */
  103. var $_headers;
  104. /**
  105. * The body of this part (not encoded)
  106. *
  107. * @var string
  108. * @access private
  109. */
  110. var $_body;
  111. /**
  112. * The location of file with body of this part (not encoded)
  113. *
  114. * @var string
  115. * @access private
  116. */
  117. var $_body_file;
  118. /**
  119. * The end-of-line sequence
  120. *
  121. * @var string
  122. * @access private
  123. */
  124. var $_eol = "\r\n";
  125. /**
  126. * Constructor.
  127. *
  128. * Sets up the object.
  129. *
  130. * @param string $body The body of the mime part if any.
  131. * @param array $params An associative array of optional parameters:
  132. * content_type - The content type for this part eg multipart/mixed
  133. * encoding - The encoding to use, 7bit, 8bit,
  134. * base64, or quoted-printable
  135. * charset - Content character set
  136. * cid - Content ID to apply
  137. * disposition - Content disposition, inline or attachment
  138. * filename - Filename parameter for content disposition
  139. * description - Content description
  140. * name_encoding - Encoding of the attachment name (Content-Type)
  141. * By default filenames are encoded using RFC2231
  142. * Here you can set RFC2047 encoding (quoted-printable
  143. * or base64) instead
  144. * filename_encoding - Encoding of the attachment filename (Content-Disposition)
  145. * See 'name_encoding'
  146. * headers_charset - Charset of the headers e.g. filename, description.
  147. * If not set, 'charset' will be used
  148. * eol - End of line sequence. Default: "\r\n"
  149. * headers - Hash array with additional part headers. Array keys can be
  150. * in form of <header_name>:<parameter_name>
  151. * body_file - Location of file with part's body (instead of $body)
  152. *
  153. * @access public
  154. */
  155. function Mail_mimePart($body = '', $params = array())
  156. {
  157. if (!empty($params['eol'])) {
  158. $this->_eol = $params['eol'];
  159. } else if (defined('MAIL_MIMEPART_CRLF')) { // backward-copat.
  160. $this->_eol = MAIL_MIMEPART_CRLF;
  161. }
  162. // Additional part headers
  163. if (!empty($params['headers']) && is_array($params['headers'])) {
  164. $headers = $params['headers'];
  165. }
  166. foreach ($params as $key => $value) {
  167. switch ($key) {
  168. case 'encoding':
  169. $this->_encoding = $value;
  170. $headers['Content-Transfer-Encoding'] = $value;
  171. break;
  172. case 'cid':
  173. $headers['Content-ID'] = '<' . $value . '>';
  174. break;
  175. case 'location':
  176. $headers['Content-Location'] = $value;
  177. break;
  178. case 'body_file':
  179. $this->_body_file = $value;
  180. break;
  181. // for backward compatibility
  182. case 'dfilename':
  183. $params['filename'] = $value;
  184. break;
  185. }
  186. }
  187. // Default content-type
  188. if (empty($params['content_type'])) {
  189. $params['content_type'] = 'text/plain';
  190. }
  191. // Content-Type
  192. $headers['Content-Type'] = $params['content_type'];
  193. if (!empty($params['charset'])) {
  194. $charset = "charset={$params['charset']}";
  195. // place charset parameter in the same line, if possible
  196. if ((strlen($headers['Content-Type']) + strlen($charset) + 16) <= 76) {
  197. $headers['Content-Type'] .= '; ';
  198. } else {
  199. $headers['Content-Type'] .= ';' . $this->_eol . ' ';
  200. }
  201. $headers['Content-Type'] .= $charset;
  202. // Default headers charset
  203. if (!isset($params['headers_charset'])) {
  204. $params['headers_charset'] = $params['charset'];
  205. }
  206. }
  207. // header values encoding parameters
  208. $h_charset = !empty($params['headers_charset']) ? $params['headers_charset'] : 'US-ASCII';
  209. $h_language = !empty($params['language']) ? $params['language'] : null;
  210. $h_encoding = !empty($params['name_encoding']) ? $params['name_encoding'] : null;
  211. if (!empty($params['filename'])) {
  212. $headers['Content-Type'] .= ';' . $this->_eol;
  213. $headers['Content-Type'] .= $this->_buildHeaderParam(
  214. 'name', $params['filename'], $h_charset, $h_language, $h_encoding
  215. );
  216. }
  217. // Content-Disposition
  218. if (!empty($params['disposition'])) {
  219. $headers['Content-Disposition'] = $params['disposition'];
  220. if (!empty($params['filename'])) {
  221. $headers['Content-Disposition'] .= ';' . $this->_eol;
  222. $headers['Content-Disposition'] .= $this->_buildHeaderParam(
  223. 'filename', $params['filename'], $h_charset, $h_language,
  224. !empty($params['filename_encoding']) ? $params['filename_encoding'] : null
  225. );
  226. }
  227. // add attachment size
  228. $size = $this->_body_file ? filesize($this->_body_file) : strlen($body);
  229. if ($size) {
  230. $headers['Content-Disposition'] .= ';' . $this->_eol . ' size=' . $size;
  231. }
  232. }
  233. if (!empty($params['description'])) {
  234. $headers['Content-Description'] = $this->encodeHeader(
  235. 'Content-Description', $params['description'], $h_charset, $h_encoding,
  236. $this->_eol
  237. );
  238. }
  239. // Search and add existing headers' parameters
  240. foreach ($headers as $key => $value) {
  241. $items = explode(':', $key);
  242. if (count($items) == 2) {
  243. $header = $items[0];
  244. $param = $items[1];
  245. if (isset($headers[$header])) {
  246. $headers[$header] .= ';' . $this->_eol;
  247. }
  248. $headers[$header] .= $this->_buildHeaderParam(
  249. $param, $value, $h_charset, $h_language, $h_encoding
  250. );
  251. unset($headers[$key]);
  252. }
  253. }
  254. // Default encoding
  255. if (!isset($this->_encoding)) {
  256. $this->_encoding = '7bit';
  257. }
  258. // Assign stuff to member variables
  259. $this->_encoded = array();
  260. $this->_headers = $headers;
  261. $this->_body = $body;
  262. }
  263. /**
  264. * Encodes and returns the email. Also stores
  265. * it in the encoded member variable
  266. *
  267. * @param string $boundary Pre-defined boundary string
  268. *
  269. * @return An associative array containing two elements,
  270. * body and headers. The headers element is itself
  271. * an indexed array. On error returns PEAR error object.
  272. * @access public
  273. */
  274. function encode($boundary=null)
  275. {
  276. $encoded =& $this->_encoded;
  277. if (count($this->_subparts)) {
  278. $boundary = $boundary ? $boundary : '=_' . md5(rand() . microtime());
  279. $eol = $this->_eol;
  280. $this->_headers['Content-Type'] .= ";$eol boundary=\"$boundary\"";
  281. $encoded['body'] = '';
  282. for ($i = 0; $i < count($this->_subparts); $i++) {
  283. $encoded['body'] .= '--' . $boundary . $eol;
  284. $tmp = $this->_subparts[$i]->encode();
  285. if ($this->_isError($tmp)) {
  286. return $tmp;
  287. }
  288. foreach ($tmp['headers'] as $key => $value) {
  289. $encoded['body'] .= $key . ': ' . $value . $eol;
  290. }
  291. $encoded['body'] .= $eol . $tmp['body'] . $eol;
  292. }
  293. $encoded['body'] .= '--' . $boundary . '--' . $eol;
  294. } else if ($this->_body) {
  295. $encoded['body'] = $this->_getEncodedData($this->_body, $this->_encoding);
  296. } else if ($this->_body_file) {
  297. // Temporarily reset magic_quotes_runtime for file reads and writes
  298. if ($magic_quote_setting = get_magic_quotes_runtime()) {
  299. @ini_set('magic_quotes_runtime', 0);
  300. }
  301. $body = $this->_getEncodedDataFromFile($this->_body_file, $this->_encoding);
  302. if ($magic_quote_setting) {
  303. @ini_set('magic_quotes_runtime', $magic_quote_setting);
  304. }
  305. if ($this->_isError($body)) {
  306. return $body;
  307. }
  308. $encoded['body'] = $body;
  309. } else {
  310. $encoded['body'] = '';
  311. }
  312. // Add headers to $encoded
  313. $encoded['headers'] =& $this->_headers;
  314. return $encoded;
  315. }
  316. /**
  317. * Encodes and saves the email into file. File must exist.
  318. * Data will be appended to the file.
  319. *
  320. * @param string $filename Output file location
  321. * @param string $boundary Pre-defined boundary string
  322. * @param boolean $skip_head True if you don't want to save headers
  323. *
  324. * @return array An associative array containing message headers
  325. * or PEAR error object
  326. * @access public
  327. * @since 1.6.0
  328. */
  329. function encodeToFile($filename, $boundary=null, $skip_head=false)
  330. {
  331. if (file_exists($filename) && !is_writable($filename)) {
  332. $err = $this->_raiseError('File is not writeable: ' . $filename);
  333. return $err;
  334. }
  335. if (!($fh = fopen($filename, 'ab'))) {
  336. $err = $this->_raiseError('Unable to open file: ' . $filename);
  337. return $err;
  338. }
  339. // Temporarily reset magic_quotes_runtime for file reads and writes
  340. if ($magic_quote_setting = get_magic_quotes_runtime()) {
  341. @ini_set('magic_quotes_runtime', 0);
  342. }
  343. $res = $this->_encodePartToFile($fh, $boundary, $skip_head);
  344. fclose($fh);
  345. if ($magic_quote_setting) {
  346. @ini_set('magic_quotes_runtime', $magic_quote_setting);
  347. }
  348. return $this->_isError($res) ? $res : $this->_headers;
  349. }
  350. /**
  351. * Encodes given email part into file
  352. *
  353. * @param string $fh Output file handle
  354. * @param string $boundary Pre-defined boundary string
  355. * @param boolean $skip_head True if you don't want to save headers
  356. *
  357. * @return array True on sucess or PEAR error object
  358. * @access private
  359. */
  360. function _encodePartToFile($fh, $boundary=null, $skip_head=false)
  361. {
  362. $eol = $this->_eol;
  363. if (count($this->_subparts)) {
  364. $boundary = $boundary ? $boundary : '=_' . md5(rand() . microtime());
  365. $this->_headers['Content-Type'] .= ";$eol boundary=\"$boundary\"";
  366. }
  367. if (!$skip_head) {
  368. foreach ($this->_headers as $key => $value) {
  369. fwrite($fh, $key . ': ' . $value . $eol);
  370. }
  371. $f_eol = $eol;
  372. } else {
  373. $f_eol = '';
  374. }
  375. if (count($this->_subparts)) {
  376. for ($i = 0; $i < count($this->_subparts); $i++) {
  377. fwrite($fh, $f_eol . '--' . $boundary . $eol);
  378. $res = $this->_subparts[$i]->_encodePartToFile($fh);
  379. if ($this->_isError($res)) {
  380. return $res;
  381. }
  382. $f_eol = $eol;
  383. }
  384. fwrite($fh, $eol . '--' . $boundary . '--' . $eol);
  385. } else if ($this->_body) {
  386. fwrite($fh, $f_eol . $this->_getEncodedData($this->_body, $this->_encoding));
  387. } else if ($this->_body_file) {
  388. fwrite($fh, $f_eol);
  389. $res = $this->_getEncodedDataFromFile(
  390. $this->_body_file, $this->_encoding, $fh
  391. );
  392. if ($this->_isError($res)) {
  393. return $res;
  394. }
  395. }
  396. return true;
  397. }
  398. /**
  399. * Adds a subpart to current mime part and returns
  400. * a reference to it
  401. *
  402. * @param string $body The body of the subpart, if any.
  403. * @param array $params The parameters for the subpart, same
  404. * as the $params argument for constructor.
  405. *
  406. * @return Mail_mimePart A reference to the part you just added. In PHP4, it is
  407. * crucial if using multipart/* in your subparts that
  408. * you use =& in your script when calling this function,
  409. * otherwise you will not be able to add further subparts.
  410. * @access public
  411. */
  412. function &addSubpart($body, $params)
  413. {
  414. $this->_subparts[] = $part = new Mail_mimePart($body, $params);
  415. return $part;
  416. }
  417. /**
  418. * Returns encoded data based upon encoding passed to it
  419. *
  420. * @param string $data The data to encode.
  421. * @param string $encoding The encoding type to use, 7bit, base64,
  422. * or quoted-printable.
  423. *
  424. * @return string
  425. * @access private
  426. */
  427. function _getEncodedData($data, $encoding)
  428. {
  429. switch ($encoding) {
  430. case 'quoted-printable':
  431. return $this->_quotedPrintableEncode($data);
  432. break;
  433. case 'base64':
  434. return rtrim(chunk_split(base64_encode($data), 76, $this->_eol));
  435. break;
  436. case '8bit':
  437. case '7bit':
  438. default:
  439. return $data;
  440. }
  441. }
  442. /**
  443. * Returns encoded data based upon encoding passed to it
  444. *
  445. * @param string $filename Data file location
  446. * @param string $encoding The encoding type to use, 7bit, base64,
  447. * or quoted-printable.
  448. * @param resource $fh Output file handle. If set, data will be
  449. * stored into it instead of returning it
  450. *
  451. * @return string Encoded data or PEAR error object
  452. * @access private
  453. */
  454. function _getEncodedDataFromFile($filename, $encoding, $fh=null)
  455. {
  456. if (!is_readable($filename)) {
  457. $err = $this->_raiseError('Unable to read file: ' . $filename);
  458. return $err;
  459. }
  460. if (!($fd = fopen($filename, 'rb'))) {
  461. $err = $this->_raiseError('Could not open file: ' . $filename);
  462. return $err;
  463. }
  464. $data = '';
  465. switch ($encoding) {
  466. case 'quoted-printable':
  467. while (!feof($fd)) {
  468. $buffer = $this->_quotedPrintableEncode(fgets($fd));
  469. if ($fh) {
  470. fwrite($fh, $buffer);
  471. } else {
  472. $data .= $buffer;
  473. }
  474. }
  475. break;
  476. case 'base64':
  477. while (!feof($fd)) {
  478. // Should read in a multiple of 57 bytes so that
  479. // the output is 76 bytes per line. Don't use big chunks
  480. // because base64 encoding is memory expensive
  481. $buffer = fread($fd, 57 * 9198); // ca. 0.5 MB
  482. $buffer = base64_encode($buffer);
  483. $buffer = chunk_split($buffer, 76, $this->_eol);
  484. if (feof($fd)) {
  485. $buffer = rtrim($buffer);
  486. }
  487. if ($fh) {
  488. fwrite($fh, $buffer);
  489. } else {
  490. $data .= $buffer;
  491. }
  492. }
  493. break;
  494. case '8bit':
  495. case '7bit':
  496. default:
  497. while (!feof($fd)) {
  498. $buffer = fread($fd, 1048576); // 1 MB
  499. if ($fh) {
  500. fwrite($fh, $buffer);
  501. } else {
  502. $data .= $buffer;
  503. }
  504. }
  505. }
  506. fclose($fd);
  507. if (!$fh) {
  508. return $data;
  509. }
  510. }
  511. /**
  512. * Encodes data to quoted-printable standard.
  513. *
  514. * @param string $input The data to encode
  515. * @param int $line_max Optional max line length. Should
  516. * not be more than 76 chars
  517. *
  518. * @return string Encoded data
  519. *
  520. * @access private
  521. */
  522. function _quotedPrintableEncode($input , $line_max = 76)
  523. {
  524. $eol = $this->_eol;
  525. /*
  526. // imap_8bit() is extremely fast, but doesn't handle properly some characters
  527. if (function_exists('imap_8bit') && $line_max == 76) {
  528. $input = preg_replace('/\r?\n/', "\r\n", $input);
  529. $input = imap_8bit($input);
  530. if ($eol != "\r\n") {
  531. $input = str_replace("\r\n", $eol, $input);
  532. }
  533. return $input;
  534. }
  535. */
  536. $lines = preg_split("/\r?\n/", $input);
  537. $escape = '=';
  538. $output = '';
  539. while (list($idx, $line) = each($lines)) {
  540. $newline = '';
  541. $i = 0;
  542. while (isset($line[$i])) {
  543. $char = $line[$i];
  544. $dec = ord($char);
  545. $i++;
  546. if (($dec == 32) && (!isset($line[$i]))) {
  547. // convert space at eol only
  548. $char = '=20';
  549. } elseif ($dec == 9 && isset($line[$i])) {
  550. ; // Do nothing if a TAB is not on eol
  551. } elseif (($dec == 61) || ($dec < 32) || ($dec > 126)) {
  552. $char = $escape . sprintf('%02X', $dec);
  553. } elseif (($dec == 46) && (($newline == '')
  554. || ((strlen($newline) + strlen("=2E")) >= $line_max))
  555. ) {
  556. // Bug #9722: convert full-stop at bol,
  557. // some Windows servers need this, won't break anything (cipri)
  558. // Bug #11731: full-stop at bol also needs to be encoded
  559. // if this line would push us over the line_max limit.
  560. $char = '=2E';
  561. }
  562. // Note, when changing this line, also change the ($dec == 46)
  563. // check line, as it mimics this line due to Bug #11731
  564. // EOL is not counted
  565. if ((strlen($newline) + strlen($char)) >= $line_max) {
  566. // soft line break; " =\r\n" is okay
  567. $output .= $newline . $escape . $eol;
  568. $newline = '';
  569. }
  570. $newline .= $char;
  571. } // end of for
  572. $output .= $newline . $eol;
  573. unset($lines[$idx]);
  574. }
  575. // Don't want last crlf
  576. $output = substr($output, 0, -1 * strlen($eol));
  577. return $output;
  578. }
  579. /**
  580. * Encodes the parameter of a header.
  581. *
  582. * @param string $name The name of the header-parameter
  583. * @param string $value The value of the paramter
  584. * @param string $charset The characterset of $value
  585. * @param string $language The language used in $value
  586. * @param string $encoding Parameter encoding. If not set, parameter value
  587. * is encoded according to RFC2231
  588. * @param int $maxLength The maximum length of a line. Defauls to 75
  589. *
  590. * @return string
  591. *
  592. * @access private
  593. */
  594. function _buildHeaderParam($name, $value, $charset=null, $language=null,
  595. $encoding=null, $maxLength=75
  596. ) {
  597. // RFC 2045:
  598. // value needs encoding if contains non-ASCII chars or is longer than 78 chars
  599. if (!preg_match('#[^\x20-\x7E]#', $value)) {
  600. $token_regexp = '#([^\x21\x23-\x27\x2A\x2B\x2D'
  601. . '\x2E\x30-\x39\x41-\x5A\x5E-\x7E])#';
  602. if (!preg_match($token_regexp, $value)) {
  603. // token
  604. if (strlen($name) + strlen($value) + 3 <= $maxLength) {
  605. return " {$name}={$value}";
  606. }
  607. } else {
  608. // quoted-string
  609. $quoted = addcslashes($value, '\\"');
  610. if (strlen($name) + strlen($quoted) + 5 <= $maxLength) {
  611. return " {$name}=\"{$quoted}\"";
  612. }
  613. }
  614. }
  615. // RFC2047: use quoted-printable/base64 encoding
  616. if ($encoding == 'quoted-printable' || $encoding == 'base64') {
  617. return $this->_buildRFC2047Param($name, $value, $charset, $encoding);
  618. }
  619. // RFC2231:
  620. $encValue = preg_replace_callback(
  621. '/([^\x21\x23\x24\x26\x2B\x2D\x2E\x30-\x39\x41-\x5A\x5E-\x7E])/',
  622. array($this, '_encodeReplaceCallback'), $value
  623. );
  624. $value = "$charset'$language'$encValue";
  625. $header = " {$name}*={$value}";
  626. if (strlen($header) <= $maxLength) {
  627. return $header;
  628. }
  629. $preLength = strlen(" {$name}*0*=");
  630. $maxLength = max(16, $maxLength - $preLength - 3);
  631. $maxLengthReg = "|(.{0,$maxLength}[^\%][^\%])|";
  632. $headers = array();
  633. $headCount = 0;
  634. while ($value) {
  635. $matches = array();
  636. $found = preg_match($maxLengthReg, $value, $matches);
  637. if ($found) {
  638. $headers[] = " {$name}*{$headCount}*={$matches[0]}";
  639. $value = substr($value, strlen($matches[0]));
  640. } else {
  641. $headers[] = " {$name}*{$headCount}*={$value}";
  642. $value = '';
  643. }
  644. $headCount++;
  645. }
  646. $headers = implode(';' . $this->_eol, $headers);
  647. return $headers;
  648. }
  649. /**
  650. * Encodes header parameter as per RFC2047 if needed
  651. *
  652. * @param string $name The parameter name
  653. * @param string $value The parameter value
  654. * @param string $charset The parameter charset
  655. * @param string $encoding Encoding type (quoted-printable or base64)
  656. * @param int $maxLength Encoded parameter max length. Default: 76
  657. *
  658. * @return string Parameter line
  659. * @access private
  660. */
  661. function _buildRFC2047Param($name, $value, $charset,
  662. $encoding='quoted-printable', $maxLength=76
  663. ) {
  664. // WARNING: RFC 2047 says: "An 'encoded-word' MUST NOT be used in
  665. // parameter of a MIME Content-Type or Content-Disposition field",
  666. // but... it's supported by many clients/servers
  667. $quoted = '';
  668. if ($encoding == 'base64') {
  669. $value = base64_encode($value);
  670. $prefix = '=?' . $charset . '?B?';
  671. $suffix = '?=';
  672. // 2 x SPACE, 2 x '"', '=', ';'
  673. $add_len = strlen($prefix . $suffix) + strlen($name) + 6;
  674. $len = $add_len + strlen($value);
  675. while ($len > $maxLength) {
  676. // We can cut base64-encoded string every 4 characters
  677. $real_len = floor(($maxLength - $add_len) / 4) * 4;
  678. $_quote = substr($value, 0, $real_len);
  679. $value = substr($value, $real_len);
  680. $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' ';
  681. $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';'
  682. $len = strlen($value) + $add_len;
  683. }
  684. $quoted .= $prefix . $value . $suffix;
  685. } else {
  686. // quoted-printable
  687. $value = $this->encodeQP($value);
  688. $prefix = '=?' . $charset . '?Q?';
  689. $suffix = '?=';
  690. // 2 x SPACE, 2 x '"', '=', ';'
  691. $add_len = strlen($prefix . $suffix) + strlen($name) + 6;
  692. $len = $add_len + strlen($value);
  693. while ($len > $maxLength) {
  694. $length = $maxLength - $add_len;
  695. // don't break any encoded letters
  696. if (preg_match("/^(.{0,$length}[^\=][^\=])/", $value, $matches)) {
  697. $_quote = $matches[1];
  698. }
  699. $quoted .= $prefix . $_quote . $suffix . $this->_eol . ' ';
  700. $value = substr($value, strlen($_quote));
  701. $add_len = strlen($prefix . $suffix) + 4; // 2 x SPACE, '"', ';'
  702. $len = strlen($value) + $add_len;
  703. }
  704. $quoted .= $prefix . $value . $suffix;
  705. }
  706. return " {$name}=\"{$quoted}\"";
  707. }
  708. /**
  709. * Encodes a header as per RFC2047
  710. *
  711. * @param string $name The header name
  712. * @param string $value The header data to encode
  713. * @param string $charset Character set name
  714. * @param string $encoding Encoding name (base64 or quoted-printable)
  715. * @param string $eol End-of-line sequence. Default: "\r\n"
  716. *
  717. * @return string Encoded header data (without a name)
  718. * @access public
  719. * @since 1.6.1
  720. */
  721. function encodeHeader($name, $value, $charset='ISO-8859-1',
  722. $encoding='quoted-printable', $eol="\r\n"
  723. ) {
  724. // Structured headers
  725. $comma_headers = array(
  726. 'from', 'to', 'cc', 'bcc', 'sender', 'reply-to',
  727. 'resent-from', 'resent-to', 'resent-cc', 'resent-bcc',
  728. 'resent-sender', 'resent-reply-to',
  729. 'mail-reply-to', 'mail-followup-to',
  730. 'return-receipt-to', 'disposition-notification-to',
  731. );
  732. $other_headers = array(
  733. 'references', 'in-reply-to', 'message-id', 'resent-message-id',
  734. );
  735. $name = strtolower($name);
  736. if (in_array($name, $comma_headers)) {
  737. $separator = ',';
  738. } else if (in_array($name, $other_headers)) {
  739. $separator = ' ';
  740. }
  741. if (!$charset) {
  742. $charset = 'ISO-8859-1';
  743. }
  744. // Structured header (make sure addr-spec inside is not encoded)
  745. if (!empty($separator)) {
  746. // Simple e-mail address regexp
  747. $email_regexp = '([^\s<]+|("[^\r\n"]+"))@\S+';
  748. $parts = Mail_mimePart::_explodeQuotedString("[\t$separator]", $value);
  749. $value = '';
  750. foreach ($parts as $part) {
  751. $part = preg_replace('/\r?\n[\s\t]*/', $eol . ' ', $part);
  752. $part = trim($part);
  753. if (!$part) {
  754. continue;
  755. }
  756. if ($value) {
  757. $value .= $separator == ',' ? $separator . ' ' : ' ';
  758. } else {
  759. $value = $name . ': ';
  760. }
  761. // let's find phrase (name) and/or addr-spec
  762. if (preg_match('/^<' . $email_regexp . '>$/', $part)) {
  763. $value .= $part;
  764. } else if (preg_match('/^' . $email_regexp . '$/', $part)) {
  765. // address without brackets and without name
  766. $value .= $part;
  767. } else if (preg_match('/<*' . $email_regexp . '>*$/', $part, $matches)) {
  768. // address with name (handle name)
  769. $address = $matches[0];
  770. $word = str_replace($address, '', $part);
  771. $word = trim($word);
  772. // check if phrase requires quoting
  773. if ($word) {
  774. // non-ASCII: require encoding
  775. if (preg_match('#([^\s\x21-\x7E]){1}#', $word)) {
  776. if ($word[0] == '"' && $word[strlen($word)-1] == '"') {
  777. // de-quote quoted-string, encoding changes
  778. // string to atom
  779. $search = array("\\\"", "\\\\");
  780. $replace = array("\"", "\\");
  781. $word = str_replace($search, $replace, $word);
  782. $word = substr($word, 1, -1);
  783. }
  784. // find length of last line
  785. if (($pos = strrpos($value, $eol)) !== false) {
  786. $last_len = strlen($value) - $pos;
  787. } else {
  788. $last_len = strlen($value);
  789. }
  790. $word = Mail_mimePart::encodeHeaderValue(
  791. $word, $charset, $encoding, $last_len, $eol
  792. );
  793. } else if (($word[0] != '"' || $word[strlen($word)-1] != '"')
  794. && preg_match('/[\(\)\<\>\\\.\[\]@,;:"]/', $word)
  795. ) {
  796. // ASCII: quote string if needed
  797. $word = '"'.addcslashes($word, '\\"').'"';
  798. }
  799. }
  800. $value .= $word.' '.$address;
  801. } else {
  802. // addr-spec not found, don't encode (?)
  803. $value .= $part;
  804. }
  805. // RFC2822 recommends 78 characters limit, use 76 from RFC2047
  806. $value = wordwrap($value, 76, $eol . ' ');
  807. }
  808. // remove header name prefix (there could be EOL too)
  809. $value = preg_replace(
  810. '/^'.$name.':('.preg_quote($eol, '/').')* /', '', $value
  811. );
  812. } else {
  813. // Unstructured header
  814. // non-ASCII: require encoding
  815. if (preg_match('#([^\s\x21-\x7E]){1}#', $value)) {
  816. if ($value[0] == '"' && $value[strlen($value)-1] == '"') {
  817. // de-quote quoted-string, encoding changes
  818. // string to atom
  819. $search = array("\\\"", "\\\\");
  820. $replace = array("\"", "\\");
  821. $value = str_replace($search, $replace, $value);
  822. $value = substr($value, 1, -1);
  823. }
  824. $value = Mail_mimePart::encodeHeaderValue(
  825. $value, $charset, $encoding, strlen($name) + 2, $eol
  826. );
  827. } else if (strlen($name.': '.$value) > 78) {
  828. // ASCII: check if header line isn't too long and use folding
  829. $value = preg_replace('/\r?\n[\s\t]*/', $eol . ' ', $value);
  830. $tmp = wordwrap($name.': '.$value, 78, $eol . ' ');
  831. $value = preg_replace('/^'.$name.':\s*/', '', $tmp);
  832. // hard limit 998 (RFC2822)
  833. $value = wordwrap($value, 998, $eol . ' ', true);
  834. }
  835. }
  836. return $value;
  837. }
  838. /**
  839. * Explode quoted string
  840. *
  841. * @param string $delimiter Delimiter expression string for preg_match()
  842. * @param string $string Input string
  843. *
  844. * @return array String tokens array
  845. * @access private
  846. */
  847. function _explodeQuotedString($delimiter, $string)
  848. {
  849. $result = array();
  850. $strlen = strlen($string);
  851. for ($q=$p=$i=0; $i < $strlen; $i++) {
  852. if ($string[$i] == "\""
  853. && (empty($string[$i-1]) || $string[$i-1] != "\\")
  854. ) {
  855. $q = $q ? false : true;
  856. } else if (!$q && preg_match("/$delimiter/", $string[$i])) {
  857. $result[] = substr($string, $p, $i - $p);
  858. $p = $i + 1;
  859. }
  860. }
  861. $result[] = substr($string, $p);
  862. return $result;
  863. }
  864. /**
  865. * Encodes a header value as per RFC2047
  866. *
  867. * @param string $value The header data to encode
  868. * @param string $charset Character set name
  869. * @param string $encoding Encoding name (base64 or quoted-printable)
  870. * @param int $prefix_len Prefix length. Default: 0
  871. * @param string $eol End-of-line sequence. Default: "\r\n"
  872. *
  873. * @return string Encoded header data
  874. * @access public
  875. * @since 1.6.1
  876. */
  877. function encodeHeaderValue($value, $charset, $encoding, $prefix_len=0, $eol="\r\n")
  878. {
  879. // #17311: Use multibyte aware method (requires mbstring extension)
  880. if ($result = Mail_mimePart::encodeMB($value, $charset, $encoding, $prefix_len, $eol)) {
  881. return $result;
  882. }
  883. // Generate the header using the specified params and dynamicly
  884. // determine the maximum length of such strings.
  885. // 75 is the value specified in the RFC.
  886. $encoding = $encoding == 'base64' ? 'B' : 'Q';
  887. $prefix = '=?' . $charset . '?' . $encoding .'?';
  888. $suffix = '?=';
  889. $maxLength = 75 - strlen($prefix . $suffix);
  890. $maxLength1stLine = $maxLength - $prefix_len;
  891. if ($encoding == 'B') {
  892. // Base64 encode the entire string
  893. $value = base64_encode($value);
  894. // We can cut base64 every 4 characters, so the real max
  895. // we can get must be rounded down.
  896. $maxLength = $maxLength - ($maxLength % 4);
  897. $maxLength1stLine = $maxLength1stLine - ($maxLength1stLine % 4);
  898. $cutpoint = $maxLength1stLine;
  899. $output = '';
  900. while ($value) {
  901. // Split translated string at every $maxLength
  902. $part = substr($value, 0, $cutpoint);
  903. $value = substr($value, $cutpoint);
  904. $cutpoint = $maxLength;
  905. // RFC 2047 specifies that any split header should
  906. // be separated by a CRLF SPACE.
  907. if ($output) {
  908. $output .= $eol . ' ';
  909. }
  910. $output .= $prefix . $part . $suffix;
  911. }
  912. $value = $output;
  913. } else {
  914. // quoted-printable encoding has been selected
  915. $value = Mail_mimePart::encodeQP($value);
  916. // This regexp will break QP-encoded text at every $maxLength
  917. // but will not break any encoded letters.
  918. $reg1st = "|(.{0,$maxLength1stLine}[^\=][^\=])|";
  919. $reg2nd = "|(.{0,$maxLength}[^\=][^\=])|";
  920. if (strlen($value) > $maxLength1stLine) {
  921. // Begin with the regexp for the first line.
  922. $reg = $reg1st;
  923. $output = '';
  924. while ($value) {
  925. // Split translated string at every $maxLength
  926. // But make sure not to break any translated chars.
  927. $found = preg_match($reg, $value, $matches);
  928. // After this first line, we need to use a different
  929. // regexp for the first line.
  930. $reg = $reg2nd;
  931. // Save the found part and encapsulate it in the
  932. // prefix & suffix. Then remove the part from the
  933. // $value_out variable.
  934. if ($found) {
  935. $part = $matches[0];
  936. $len = strlen($matches[0]);
  937. $value = substr($value, $len);
  938. } else {
  939. $part = $value;
  940. $value = '';
  941. }
  942. // RFC 2047 specifies that any split header should
  943. // be separated by a CRLF SPACE
  944. if ($output) {
  945. $output .= $eol . ' ';
  946. }
  947. $output .= $prefix . $part . $suffix;
  948. }
  949. $value = $output;
  950. } else {
  951. $value = $prefix . $value . $suffix;
  952. }
  953. }
  954. return $value;
  955. }
  956. /**
  957. * Encodes the given string using quoted-printable
  958. *
  959. * @param string $str String to encode
  960. *
  961. * @return string Encoded string
  962. * @access public
  963. * @since 1.6.0
  964. */
  965. function encodeQP($str)
  966. {
  967. // Bug #17226 RFC 2047 restricts some characters
  968. // if the word is inside a phrase, permitted chars are only:
  969. // ASCII letters, decimal digits, "!", "*", "+", "-", "/", "=", and "_"
  970. // "=", "_", "?" must be encoded
  971. $regexp = '/([\x22-\x29\x2C\x2E\x3A-\x40\x5B-\x60\x7B-\x7E\x80-\xFF])/';
  972. $str = preg_replace_callback(
  973. $regexp, array('Mail_mimePart', '_qpReplaceCallback'), $str
  974. );
  975. return str_replace(' ', '_', $str);
  976. }
  977. /**
  978. * Encodes the given string using base64 or quoted-printable.
  979. * This method makes sure that encoded-word represents an integral
  980. * number of characters as per RFC2047.
  981. *
  982. * @param string $str String to encode
  983. * @param string $charset Character set name
  984. * @param string $encoding Encoding name (base64 or quoted-printable)
  985. * @param int $prefix_len Prefix length. Default: 0
  986. * @param string $eol End-of-line sequence. Default: "\r\n"
  987. *
  988. * @return string Encoded string
  989. * @access public
  990. * @since 1.8.0
  991. */
  992. function encodeMB($str, $charset, $encoding, $prefix_len=0, $eol="\r\n")
  993. {
  994. if (!function_exists('mb_substr') || !function_exists('mb_strlen')) {
  995. return;
  996. }
  997. $encoding = $encoding == 'base64' ? 'B' : 'Q';
  998. // 75 is the value specified in the RFC
  999. $prefix = '=?' . $charset . '?'.$encoding.'?';
  1000. $suffix = '?=';
  1001. $maxLength = 75 - strlen($prefix . $suffix);
  1002. // A multi-octet character may not be split across adjacent encoded-words
  1003. // So, we'll loop over each character
  1004. // mb_stlen() with wrong charset will generate a warning here and return null
  1005. $length = mb_strlen($str, $charset);
  1006. $result = '';
  1007. $line_length = $prefix_len;
  1008. if ($encoding == 'B') {
  1009. // base64
  1010. $start = 0;
  1011. $prev = '';
  1012. for ($i=1; $i<=$length; $i++) {
  1013. // See #17311
  1014. $chunk = mb_substr($str, $start, $i-$start, $charset);
  1015. $chunk = base64_encode($chunk);
  1016. $chunk_len = strlen($chunk);
  1017. if ($line_length + $chunk_len == $maxLength || $i == $length) {
  1018. if ($result) {
  1019. $result .= "\n";
  1020. }
  1021. $result .= $chunk;
  1022. $line_length = 0;
  1023. $start = $i;
  1024. } else if ($line_length + $chunk_len > $maxLength) {
  1025. if ($result) {
  1026. $result .= "\n";
  1027. }
  1028. if ($prev) {
  1029. $result .= $prev;
  1030. }
  1031. $line_length = 0;
  1032. $start = $i - 1;
  1033. } else {
  1034. $prev = $chunk;
  1035. }
  1036. }
  1037. } else {
  1038. // quoted-printable
  1039. // see encodeQP()
  1040. $regexp = '/([\x22-\x29\x2C\x2E\x3A-\x40\x5B-\x60\x7B-\x7E\x80-\xFF])/';
  1041. for ($i=0; $i<=$length; $i++) {
  1042. $char = mb_substr($str, $i, 1, $charset);
  1043. // RFC recommends underline (instead of =20) in place of the space
  1044. // that's one of the reasons why we're not using iconv_mime_encode()
  1045. if ($char == ' ') {
  1046. $char = '_';
  1047. $char_len = 1;
  1048. } else {
  1049. $char = preg_replace_callback(
  1050. $regexp, array('Mail_mimePart', '_qpReplaceCallback'), $char
  1051. );
  1052. $char_len = strlen($char);
  1053. }
  1054. if ($line_length + $char_len > $maxLength) {
  1055. if ($result) {
  1056. $result .= "\n";
  1057. }
  1058. $line_length = 0;
  1059. }
  1060. $result .= $char;
  1061. $line_length += $char_len;
  1062. }
  1063. }
  1064. if ($result) {
  1065. $result = $prefix
  1066. .str_replace("\n", $suffix.$eol.' '.$prefix, $result).$suffix;
  1067. }
  1068. return $result;
  1069. }
  1070. /**
  1071. * Callback function to replace extended characters (\x80-xFF) with their
  1072. * ASCII values (RFC2047: quoted-printable)
  1073. *
  1074. * @param array $matches Preg_replace's matches array
  1075. *
  1076. * @return string Encoded character string
  1077. * @access private
  1078. */
  1079. function _qpReplaceCallback($matches)
  1080. {
  1081. return sprintf('=%02X', ord($matches[1]));
  1082. }
  1083. /**
  1084. * Callback function to replace extended characters (\x80-xFF) with their
  1085. * ASCII values (RFC2231)
  1086. *
  1087. * @param array $matches Preg_replace's matches array
  1088. *
  1089. * @return string Encoded character string
  1090. * @access private
  1091. */
  1092. function _encodeReplaceCallback($matches)
  1093. {
  1094. return sprintf('%%%02X', ord($matches[1]));
  1095. }
  1096. /**
  1097. * PEAR::isError implementation
  1098. *
  1099. * @param mixed $data Object
  1100. *
  1101. * @return bool True if object is an instance of PEAR_Error
  1102. * @access private
  1103. */
  1104. function _isError($data)
  1105. {
  1106. // PEAR::isError() is not PHP 5.4 compatible (see Bug #19473)
  1107. if (is_object($data) && is_a($data, 'PEAR_Error')) {
  1108. return true;
  1109. }
  1110. return false;
  1111. }
  1112. /**
  1113. * PEAR::raiseError implementation
  1114. *
  1115. * @param $message A text error message
  1116. *
  1117. * @return PEAR_Error Instance of PEAR_Error
  1118. * @access private
  1119. */
  1120. function _raiseError($message)
  1121. {
  1122. // PEAR::raiseError() is not PHP 5.4 compatible
  1123. return new PEAR_Error($message);
  1124. }
  1125. } // End of class