PageRenderTime 58ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

/lib/horde/framework/Horde/Mime/Part.php

http://github.com/moodle/moodle
PHP | 2528 lines | 1390 code | 286 blank | 852 comment | 224 complexity | caee7b98b9a202c510bf9de7715d2f15 MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause
  1. <?php
  2. /**
  3. * Copyright 1999-2017 Horde LLC (http://www.horde.org/)
  4. *
  5. * See the enclosed file LICENSE for license information (LGPL). If you
  6. * did not receive this file, see http://www.horde.org/licenses/lgpl21.
  7. *
  8. * @category Horde
  9. * @copyright 1999-2017 Horde LLC
  10. * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
  11. * @package Mime
  12. */
  13. /**
  14. * Object-oriented representation of a MIME part (RFC 2045-2049).
  15. *
  16. * @author Chuck Hagenbuch <chuck@horde.org>
  17. * @author Michael Slusarz <slusarz@horde.org>
  18. * @category Horde
  19. * @copyright 1999-2017 Horde LLC
  20. * @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
  21. * @package Mime
  22. */
  23. class Horde_Mime_Part
  24. implements ArrayAccess, Countable, RecursiveIterator, Serializable
  25. {
  26. /* Serialized version. */
  27. const VERSION = 2;
  28. /* The character(s) used internally for EOLs. */
  29. const EOL = "\n";
  30. /* The character string designated by RFC 2045 to designate EOLs in MIME
  31. * messages. */
  32. const RFC_EOL = "\r\n";
  33. /* The default encoding. */
  34. const DEFAULT_ENCODING = 'binary';
  35. /* Constants indicating the valid transfer encoding allowed. */
  36. const ENCODE_7BIT = 1;
  37. const ENCODE_8BIT = 2;
  38. const ENCODE_BINARY = 4;
  39. /* MIME nesting limit. */
  40. const NESTING_LIMIT = 100;
  41. /* Status mask value: Need to reindex the current part. */
  42. const STATUS_REINDEX = 1;
  43. /* Status mask value: This is the base MIME part. */
  44. const STATUS_BASEPART = 2;
  45. /**
  46. * The default charset to use when parsing text parts with no charset
  47. * information.
  48. *
  49. * @todo Make this a non-static property or pass as parameter to static
  50. * methods in Horde 6.
  51. *
  52. * @var string
  53. */
  54. public static $defaultCharset = 'us-ascii';
  55. /**
  56. * The memory limit for use with the PHP temp stream.
  57. *
  58. * @var integer
  59. */
  60. public static $memoryLimit = 2097152;
  61. /**
  62. * Parent object. Value only accurate when iterating.
  63. *
  64. * @since 2.8.0
  65. *
  66. * @var Horde_Mime_Part
  67. */
  68. public $parent = null;
  69. /**
  70. * Default value for this Part's size.
  71. *
  72. * @var integer
  73. */
  74. protected $_bytes;
  75. /**
  76. * The body of the part. Always stored in binary format.
  77. *
  78. * @var resource
  79. */
  80. protected $_contents;
  81. /**
  82. * The sequence to use as EOL for this part.
  83. *
  84. * The default is currently to output the EOL sequence internally as
  85. * just "\n" instead of the canonical "\r\n" required in RFC 822 & 2045.
  86. * To be RFC complaint, the full <CR><LF> EOL combination should be used
  87. * when sending a message.
  88. *
  89. * @var string
  90. */
  91. protected $_eol = self::EOL;
  92. /**
  93. * The MIME headers for this part.
  94. *
  95. * @var Horde_Mime_Headers
  96. */
  97. protected $_headers;
  98. /**
  99. * The charset to output the headers in.
  100. *
  101. * @var string
  102. */
  103. protected $_hdrCharset = null;
  104. /**
  105. * Metadata.
  106. *
  107. * @var array
  108. */
  109. protected $_metadata = array();
  110. /**
  111. * The MIME ID of this part.
  112. *
  113. * @var string
  114. */
  115. protected $_mimeid = null;
  116. /**
  117. * The subparts of this part.
  118. *
  119. * @var array
  120. */
  121. protected $_parts = array();
  122. /**
  123. * Status mask for this part.
  124. *
  125. * @var integer
  126. */
  127. protected $_status = 0;
  128. /**
  129. * Temporary array.
  130. *
  131. * @var array
  132. */
  133. protected $_temp = array();
  134. /**
  135. * The desired transfer encoding of this part.
  136. *
  137. * @var string
  138. */
  139. protected $_transferEncoding = self::DEFAULT_ENCODING;
  140. /**
  141. * Flag to detect if a message failed to send at least once.
  142. *
  143. * @var boolean
  144. */
  145. protected $_failed = false;
  146. /**
  147. * Constructor.
  148. */
  149. public function __construct()
  150. {
  151. $this->_headers = new Horde_Mime_Headers();
  152. /* Mandatory MIME headers. */
  153. $this->_headers->addHeaderOb(
  154. new Horde_Mime_Headers_ContentParam_ContentDisposition(null, '')
  155. );
  156. $ct = Horde_Mime_Headers_ContentParam_ContentType::create();
  157. $ct['charset'] = self::$defaultCharset;
  158. $this->_headers->addHeaderOb($ct);
  159. }
  160. /**
  161. * Function to run on clone.
  162. */
  163. public function __clone()
  164. {
  165. foreach ($this->_parts as $k => $v) {
  166. $this->_parts[$k] = clone $v;
  167. }
  168. $this->_headers = clone $this->_headers;
  169. if (!empty($this->_contents)) {
  170. $this->_contents = $this->_writeStream($this->_contents);
  171. }
  172. }
  173. /**
  174. * Set the content-disposition of this part.
  175. *
  176. * @param string $disposition The content-disposition to set ('inline',
  177. * 'attachment', or an empty value).
  178. */
  179. public function setDisposition($disposition = null)
  180. {
  181. $this->_headers['content-disposition']->setContentParamValue(
  182. strval($disposition)
  183. );
  184. }
  185. /**
  186. * Get the content-disposition of this part.
  187. *
  188. * @return string The part's content-disposition. An empty string means
  189. * no desired disposition has been set for this part.
  190. */
  191. public function getDisposition()
  192. {
  193. return $this->_headers['content-disposition']->value;
  194. }
  195. /**
  196. * Add a disposition parameter to this part.
  197. *
  198. * @param string $label The disposition parameter label.
  199. * @param string $data The disposition parameter data. If null, removes
  200. * the parameter (@since 2.8.0).
  201. */
  202. public function setDispositionParameter($label, $data)
  203. {
  204. $cd = $this->_headers['content-disposition'];
  205. if (is_null($data)) {
  206. unset($cd[$label]);
  207. } elseif (strlen($data)) {
  208. $cd[$label] = $data;
  209. if (strcasecmp($label, 'size') === 0) {
  210. // RFC 2183 [2.7] - size parameter
  211. $this->_bytes = $cd[$label];
  212. } elseif ((strcasecmp($label, 'filename') === 0) &&
  213. !strlen($cd->value)) {
  214. /* Set part to attachment if not already explicitly set to
  215. * 'inline'. */
  216. $cd->setContentParamValue('attachment');
  217. }
  218. }
  219. }
  220. /**
  221. * Get a disposition parameter from this part.
  222. *
  223. * @param string $label The disposition parameter label.
  224. *
  225. * @return string The data requested.
  226. * Returns null if $label is not set.
  227. */
  228. public function getDispositionParameter($label)
  229. {
  230. $cd = $this->_headers['content-disposition'];
  231. return $cd[$label];
  232. }
  233. /**
  234. * Get all parameters from the Content-Disposition header.
  235. *
  236. * @return array An array of all the parameters
  237. * Returns the empty array if no parameters set.
  238. */
  239. public function getAllDispositionParameters()
  240. {
  241. return $this->_headers['content-disposition']->params;
  242. }
  243. /**
  244. * Set the name of this part.
  245. *
  246. * @param string $name The name to set.
  247. */
  248. public function setName($name)
  249. {
  250. $this->setDispositionParameter('filename', $name);
  251. $this->setContentTypeParameter('name', $name);
  252. }
  253. /**
  254. * Get the name of this part.
  255. *
  256. * @param boolean $default If the name parameter doesn't exist, should we
  257. * use the default name from the description
  258. * parameter?
  259. *
  260. * @return string The name of the part.
  261. */
  262. public function getName($default = false)
  263. {
  264. if (!($name = $this->getDispositionParameter('filename')) &&
  265. !($name = $this->getContentTypeParameter('name')) &&
  266. $default) {
  267. $name = preg_replace('|\W|', '_', $this->getDescription(false));
  268. }
  269. return $name;
  270. }
  271. /**
  272. * Set the body contents of this part.
  273. *
  274. * @param mixed $contents The part body. Either a string or a stream
  275. * resource, or an array containing both.
  276. * @param array $options Additional options:
  277. * - encoding: (string) The encoding of $contents.
  278. * DEFAULT: Current transfer encoding value.
  279. * - usestream: (boolean) If $contents is a stream, should we directly
  280. * use that stream?
  281. * DEFAULT: $contents copied to a new stream.
  282. */
  283. public function setContents($contents, $options = array())
  284. {
  285. if (is_resource($contents) && ($contents === $this->_contents)) {
  286. return;
  287. }
  288. if (empty($options['encoding'])) {
  289. $options['encoding'] = $this->_transferEncoding;
  290. }
  291. $fp = (empty($options['usestream']) || !is_resource($contents))
  292. ? $this->_writeStream($contents)
  293. : $contents;
  294. /* Properly close the existing stream. */
  295. $this->clearContents();
  296. $this->setTransferEncoding($options['encoding']);
  297. $this->_contents = $this->_transferDecode($fp, $options['encoding']);
  298. }
  299. /**
  300. * Add to the body contents of this part.
  301. *
  302. * @param mixed $contents The part body. Either a string or a stream
  303. * resource, or an array containing both.
  304. * - encoding: (string) The encoding of $contents.
  305. * DEFAULT: Current transfer encoding value.
  306. * - usestream: (boolean) If $contents is a stream, should we directly
  307. * use that stream?
  308. * DEFAULT: $contents copied to a new stream.
  309. */
  310. public function appendContents($contents, $options = array())
  311. {
  312. if (empty($this->_contents)) {
  313. $this->setContents($contents, $options);
  314. } else {
  315. $fp = (empty($options['usestream']) || !is_resource($contents))
  316. ? $this->_writeStream($contents)
  317. : $contents;
  318. $this->_writeStream((empty($options['encoding']) || ($options['encoding'] == $this->_transferEncoding)) ? $fp : $this->_transferDecode($fp, $options['encoding']), array('fp' => $this->_contents));
  319. unset($this->_temp['sendTransferEncoding']);
  320. }
  321. }
  322. /**
  323. * Clears the body contents of this part.
  324. */
  325. public function clearContents()
  326. {
  327. if (!empty($this->_contents)) {
  328. fclose($this->_contents);
  329. $this->_contents = null;
  330. unset($this->_temp['sendTransferEncoding']);
  331. }
  332. }
  333. /**
  334. * Return the body of the part.
  335. *
  336. * @param array $options Additional options:
  337. * - canonical: (boolean) Returns the contents in strict RFC 822 &
  338. * 2045 output - namely, all newlines end with the
  339. * canonical <CR><LF> sequence.
  340. * DEFAULT: No
  341. * - stream: (boolean) Return the body as a stream resource.
  342. * DEFAULT: No
  343. *
  344. * @return mixed The body text (string) of the part, null if there is no
  345. * contents, and a stream resource if 'stream' is true.
  346. */
  347. public function getContents($options = array())
  348. {
  349. return empty($options['canonical'])
  350. ? (empty($options['stream']) ? $this->_readStream($this->_contents) : $this->_contents)
  351. : $this->replaceEOL($this->_contents, self::RFC_EOL, !empty($options['stream']));
  352. }
  353. /**
  354. * Decodes the contents of the part to binary encoding.
  355. *
  356. * @param resource $fp A stream containing the data to decode.
  357. * @param string $encoding The original file encoding.
  358. *
  359. * @return resource A new file resource with the decoded data.
  360. */
  361. protected function _transferDecode($fp, $encoding)
  362. {
  363. /* If the contents are empty, return now. */
  364. fseek($fp, 0, SEEK_END);
  365. if (ftell($fp)) {
  366. switch ($encoding) {
  367. case 'base64':
  368. try {
  369. return $this->_writeStream($fp, array(
  370. 'error' => true,
  371. 'filter' => array(
  372. 'convert.base64-decode' => array()
  373. )
  374. ));
  375. } catch (ErrorException $e) {}
  376. rewind($fp);
  377. return $this->_writeStream(base64_decode(stream_get_contents($fp)));
  378. case 'quoted-printable':
  379. try {
  380. return $this->_writeStream($fp, array(
  381. 'error' => true,
  382. 'filter' => array(
  383. 'convert.quoted-printable-decode' => array()
  384. )
  385. ));
  386. } catch (ErrorException $e) {}
  387. // Workaround for Horde Bug #8747
  388. rewind($fp);
  389. return $this->_writeStream(quoted_printable_decode(stream_get_contents($fp)));
  390. case 'uuencode':
  391. case 'x-uuencode':
  392. case 'x-uue':
  393. /* Support for uuencoded encoding - although not required by
  394. * RFCs, some mailers may still encode this way. */
  395. $res = Horde_Mime::uudecode($this->_readStream($fp));
  396. return $this->_writeStream($res[0]['data']);
  397. }
  398. }
  399. return $fp;
  400. }
  401. /**
  402. * Encodes the contents of the part as necessary for transport.
  403. *
  404. * @param resource $fp A stream containing the data to encode.
  405. * @param string $encoding The encoding to use.
  406. *
  407. * @return resource A new file resource with the encoded data.
  408. */
  409. protected function _transferEncode($fp, $encoding)
  410. {
  411. $this->_temp['transferEncodeClose'] = true;
  412. switch ($encoding) {
  413. case 'base64':
  414. /* Base64 Encoding: See RFC 2045, section 6.8 */
  415. return $this->_writeStream($fp, array(
  416. 'filter' => array(
  417. 'convert.base64-encode' => array(
  418. 'line-break-chars' => $this->getEOL(),
  419. 'line-length' => 76
  420. )
  421. )
  422. ));
  423. case 'quoted-printable':
  424. // PHP Bug 65776 - Must normalize the EOL characters.
  425. stream_filter_register('horde_eol', 'Horde_Stream_Filter_Eol');
  426. $stream = new Horde_Stream_Existing(array(
  427. 'stream' => $fp
  428. ));
  429. $stream->stream = $this->_writeStream($stream->stream, array(
  430. 'filter' => array(
  431. 'horde_eol' => array('eol' => $stream->getEOL()
  432. )
  433. )));
  434. /* Quoted-Printable Encoding: See RFC 2045, section 6.7 */
  435. return $this->_writeStream($fp, array(
  436. 'filter' => array(
  437. 'convert.quoted-printable-encode' => array_filter(array(
  438. 'line-break-chars' => $stream->getEOL(),
  439. 'line-length' => 76
  440. ))
  441. )
  442. ));
  443. default:
  444. $this->_temp['transferEncodeClose'] = false;
  445. return $fp;
  446. }
  447. }
  448. /**
  449. * Set the MIME type of this part.
  450. *
  451. * @param string $type The MIME type to set (ex.: text/plain).
  452. */
  453. public function setType($type)
  454. {
  455. /* RFC 2045: Any entity with unrecognized encoding must be treated
  456. * as if it has a Content-Type of "application/octet-stream"
  457. * regardless of what the Content-Type field actually says. */
  458. if (!is_null($this->_transferEncoding)) {
  459. $this->_headers['content-type']->setContentParamValue($type);
  460. }
  461. }
  462. /**
  463. * Get the full MIME Content-Type of this part.
  464. *
  465. * @param boolean $charset Append character set information to the end
  466. * of the content type if this is a text/* part?
  467. *`
  468. * @return string The MIME type of this part.
  469. */
  470. public function getType($charset = false)
  471. {
  472. $ct = $this->_headers['content-type'];
  473. return $charset
  474. ? $ct->type_charset
  475. : $ct->value;
  476. }
  477. /**
  478. * If the subtype of a MIME part is unrecognized by an application, the
  479. * default type should be used instead (See RFC 2046). This method
  480. * returns the default subtype for a particular primary MIME type.
  481. *
  482. * @return string The default MIME type of this part (ex.: text/plain).
  483. */
  484. public function getDefaultType()
  485. {
  486. switch ($this->getPrimaryType()) {
  487. case 'text':
  488. /* RFC 2046 (4.1.4): text parts default to text/plain. */
  489. return 'text/plain';
  490. case 'multipart':
  491. /* RFC 2046 (5.1.3): multipart parts default to multipart/mixed. */
  492. return 'multipart/mixed';
  493. default:
  494. /* RFC 2046 (4.2, 4.3, 4.4, 4.5.3, 5.2.4): all others default to
  495. application/octet-stream. */
  496. return 'application/octet-stream';
  497. }
  498. }
  499. /**
  500. * Get the primary type of this part.
  501. *
  502. * @return string The primary MIME type of this part.
  503. */
  504. public function getPrimaryType()
  505. {
  506. return $this->_headers['content-type']->ptype;
  507. }
  508. /**
  509. * Get the subtype of this part.
  510. *
  511. * @return string The MIME subtype of this part.
  512. */
  513. public function getSubType()
  514. {
  515. return $this->_headers['content-type']->stype;
  516. }
  517. /**
  518. * Set the character set of this part.
  519. *
  520. * @param string $charset The character set of this part.
  521. */
  522. public function setCharset($charset)
  523. {
  524. $this->setContentTypeParameter('charset', $charset);
  525. }
  526. /**
  527. * Get the character set to use for this part.
  528. *
  529. * @return string The character set of this part (lowercase). Returns
  530. * null if there is no character set.
  531. */
  532. public function getCharset()
  533. {
  534. return $this->getContentTypeParameter('charset')
  535. ?: (($this->getPrimaryType() === 'text') ? 'us-ascii' : null);
  536. }
  537. /**
  538. * Set the character set to use when outputting MIME headers.
  539. *
  540. * @param string $charset The character set.
  541. */
  542. public function setHeaderCharset($charset)
  543. {
  544. $this->_hdrCharset = $charset;
  545. }
  546. /**
  547. * Get the character set to use when outputting MIME headers.
  548. *
  549. * @return string The character set. If no preferred character set has
  550. * been set, returns null.
  551. */
  552. public function getHeaderCharset()
  553. {
  554. return is_null($this->_hdrCharset)
  555. ? $this->getCharset()
  556. : $this->_hdrCharset;
  557. }
  558. /**
  559. * Set the language(s) of this part.
  560. *
  561. * @param mixed $lang A language string, or an array of language
  562. * strings.
  563. */
  564. public function setLanguage($lang)
  565. {
  566. $this->_headers->addHeaderOb(
  567. new Horde_Mime_Headers_ContentLanguage('', $lang)
  568. );
  569. }
  570. /**
  571. * Get the language(s) of this part.
  572. *
  573. * @param array The list of languages.
  574. */
  575. public function getLanguage()
  576. {
  577. return $this->_headers['content-language']->langs;
  578. }
  579. /**
  580. * Set the content duration of the data contained in this part (see RFC
  581. * 3803).
  582. *
  583. * @param integer $duration The duration of the data, in seconds. If
  584. * null, clears the duration information.
  585. */
  586. public function setDuration($duration)
  587. {
  588. if (is_null($duration)) {
  589. unset($this->_headers['content-duration']);
  590. } else {
  591. if (!($hdr = $this->_headers['content-duration'])) {
  592. $hdr = new Horde_Mime_Headers_Element_Single(
  593. 'Content-Duration',
  594. ''
  595. );
  596. $this->_headers->addHeaderOb($hdr);
  597. }
  598. $hdr->setValue($duration);
  599. }
  600. }
  601. /**
  602. * Get the content duration of the data contained in this part (see RFC
  603. * 3803).
  604. *
  605. * @return integer The duration of the data, in seconds. Returns null if
  606. * there is no duration information.
  607. */
  608. public function getDuration()
  609. {
  610. return ($hdr = $this->_headers['content-duration'])
  611. ? intval($hdr->value)
  612. : null;
  613. }
  614. /**
  615. * Set the description of this part.
  616. *
  617. * @param string $description The description of this part. If null,
  618. * deletes the description (@since 2.8.0).
  619. */
  620. public function setDescription($description)
  621. {
  622. if (is_null($description)) {
  623. unset($this->_headers['content-description']);
  624. } else {
  625. if (!($hdr = $this->_headers['content-description'])) {
  626. $hdr = new Horde_Mime_Headers_ContentDescription(null, '');
  627. $this->_headers->addHeaderOb($hdr);
  628. }
  629. $hdr->setValue($description);
  630. }
  631. }
  632. /**
  633. * Get the description of this part.
  634. *
  635. * @param boolean $default If the description parameter doesn't exist,
  636. * should we use the name of the part?
  637. *
  638. * @return string The description of this part.
  639. */
  640. public function getDescription($default = false)
  641. {
  642. if (($ob = $this->_headers['content-description']) &&
  643. strlen($ob->value)) {
  644. return $ob->value;
  645. }
  646. return $default
  647. ? $this->getName()
  648. : '';
  649. }
  650. /**
  651. * Set the transfer encoding to use for this part.
  652. *
  653. * Only needed in the following circumstances:
  654. * 1.) Indicate what the transfer encoding is if the data has not yet been
  655. * set in the object (can only be set if there presently are not
  656. * any contents).
  657. * 2.) Force the encoding to a certain type on a toString() call (if
  658. * 'send' is true).
  659. *
  660. * @param string $encoding The transfer encoding to use.
  661. * @param array $options Additional options:
  662. * - send: (boolean) If true, use $encoding as the sending encoding.
  663. * DEFAULT: $encoding is used to change the base encoding.
  664. */
  665. public function setTransferEncoding($encoding, $options = array())
  666. {
  667. if (empty($encoding) ||
  668. (empty($options['send']) && !empty($this->_contents))) {
  669. return;
  670. }
  671. switch ($encoding = Horde_String::lower($encoding)) {
  672. case '7bit':
  673. case '8bit':
  674. case 'base64':
  675. case 'binary':
  676. case 'quoted-printable':
  677. // Non-RFC types, but old mailers may still use
  678. case 'uuencode':
  679. case 'x-uuencode':
  680. case 'x-uue':
  681. if (empty($options['send'])) {
  682. $this->_transferEncoding = $encoding;
  683. } else {
  684. $this->_temp['sendEncoding'] = $encoding;
  685. }
  686. break;
  687. default:
  688. if (empty($options['send'])) {
  689. /* RFC 2045: Any entity with unrecognized encoding must be
  690. * treated as if it has a Content-Type of
  691. * "application/octet-stream" regardless of what the
  692. * Content-Type field actually says. */
  693. $this->setType('application/octet-stream');
  694. $this->_transferEncoding = null;
  695. }
  696. break;
  697. }
  698. }
  699. /**
  700. * Get a list of all MIME subparts.
  701. *
  702. * @return array An array of the Horde_Mime_Part subparts.
  703. */
  704. public function getParts()
  705. {
  706. return $this->_parts;
  707. }
  708. /**
  709. * Add/remove a content type parameter to this part.
  710. *
  711. * @param string $label The content-type parameter label.
  712. * @param string $data The content-type parameter data. If null, removes
  713. * the parameter (@since 2.8.0).
  714. */
  715. public function setContentTypeParameter($label, $data)
  716. {
  717. $ct = $this->_headers['content-type'];
  718. if (is_null($data)) {
  719. unset($ct[$label]);
  720. } elseif (strlen($data)) {
  721. $ct[$label] = $data;
  722. }
  723. }
  724. /**
  725. * Get a content type parameter from this part.
  726. *
  727. * @param string $label The content type parameter label.
  728. *
  729. * @return string The data requested.
  730. * Returns null if $label is not set.
  731. */
  732. public function getContentTypeParameter($label)
  733. {
  734. $ct = $this->_headers['content-type'];
  735. return $ct[$label];
  736. }
  737. /**
  738. * Get all parameters from the Content-Type header.
  739. *
  740. * @return array An array of all the parameters
  741. * Returns the empty array if no parameters set.
  742. */
  743. public function getAllContentTypeParameters()
  744. {
  745. return $this->_headers['content-type']->params;
  746. }
  747. /**
  748. * Sets a new string to use for EOLs.
  749. *
  750. * @param string $eol The string to use for EOLs.
  751. */
  752. public function setEOL($eol)
  753. {
  754. $this->_eol = $eol;
  755. }
  756. /**
  757. * Get the string to use for EOLs.
  758. *
  759. * @return string The string to use for EOLs.
  760. */
  761. public function getEOL()
  762. {
  763. return $this->_eol;
  764. }
  765. /**
  766. * Returns a Horde_Mime_Header object containing all MIME headers needed
  767. * for the part.
  768. *
  769. * @param array $options Additional options:
  770. * - encode: (integer) A mask of allowable encodings.
  771. * DEFAULT: Auto-determined
  772. * - headers: (Horde_Mime_Headers) The object to add the MIME headers
  773. * to.
  774. * DEFAULT: Add headers to a new object
  775. *
  776. * @return Horde_Mime_Headers A Horde_Mime_Headers object.
  777. */
  778. public function addMimeHeaders($options = array())
  779. {
  780. if (empty($options['headers'])) {
  781. $headers = new Horde_Mime_Headers();
  782. } else {
  783. $headers = $options['headers'];
  784. $headers->removeHeader('Content-Disposition');
  785. $headers->removeHeader('Content-Transfer-Encoding');
  786. }
  787. /* Add the mandatory Content-Type header. */
  788. $ct = $this->_headers['content-type'];
  789. $headers->addHeaderOb($ct);
  790. /* Add the language(s), if set. (RFC 3282 [2]) */
  791. if ($hdr = $this->_headers['content-language']) {
  792. $headers->addHeaderOb($hdr);
  793. }
  794. /* Get the description, if any. */
  795. if ($hdr = $this->_headers['content-description']) {
  796. $headers->addHeaderOb($hdr);
  797. }
  798. /* Set the duration, if it exists. (RFC 3803) */
  799. if ($hdr = $this->_headers['content-duration']) {
  800. $headers->addHeaderOb($hdr);
  801. }
  802. /* Per RFC 2046[4], this MUST appear in the base message headers. */
  803. if ($this->_status & self::STATUS_BASEPART) {
  804. $headers->addHeaderOb(Horde_Mime_Headers_MimeVersion::create());
  805. }
  806. /* message/* parts require no additional header information. */
  807. if ($ct->ptype === 'message') {
  808. return $headers;
  809. }
  810. /* RFC 2183 [2] indicates that default is no requested disposition -
  811. * the receiving MUA is responsible for display choice. */
  812. $cd = $this->_headers['content-disposition'];
  813. if (!$cd->isDefault()) {
  814. $headers->addHeaderOb($cd);
  815. }
  816. /* Add transfer encoding information. RFC 2045 [6.1] indicates that
  817. * default is 7bit. No need to send the header in this case. */
  818. $cte = new Horde_Mime_Headers_ContentTransferEncoding(
  819. null,
  820. $this->_getTransferEncoding(
  821. empty($options['encode']) ? null : $options['encode']
  822. )
  823. );
  824. if (!$cte->isDefault()) {
  825. $headers->addHeaderOb($cte);
  826. }
  827. /* Add content ID information. */
  828. if ($hdr = $this->_headers['content-id']) {
  829. $headers->addHeaderOb($hdr);
  830. }
  831. return $headers;
  832. }
  833. /**
  834. * Return the entire part in MIME format.
  835. *
  836. * @param array $options Additional options:
  837. * - canonical: (boolean) Returns the encoded part in strict RFC 822 &
  838. * 2045 output - namely, all newlines end with the
  839. * canonical <CR><LF> sequence.
  840. * DEFAULT: false
  841. * - defserver: (string) The default server to use when creating the
  842. * header string.
  843. * DEFAULT: none
  844. * - encode: (integer) A mask of allowable encodings.
  845. * DEFAULT: self::ENCODE_7BIT
  846. * - headers: (mixed) Include the MIME headers? If true, create a new
  847. * headers object. If a Horde_Mime_Headers object, add MIME
  848. * headers to this object. If a string, use the string
  849. * verbatim.
  850. * DEFAULT: true
  851. * - id: (string) Return only this MIME ID part.
  852. * DEFAULT: Returns the base part.
  853. * - stream: (boolean) Return a stream resource.
  854. * DEFAULT: false
  855. *
  856. * @return mixed The MIME string (returned as a resource if $stream is
  857. * true).
  858. */
  859. public function toString($options = array())
  860. {
  861. $eol = $this->getEOL();
  862. $isbase = true;
  863. $oldbaseptr = null;
  864. $parts = $parts_close = array();
  865. if (isset($options['id'])) {
  866. $id = $options['id'];
  867. if (!($part = $this[$id])) {
  868. return $part;
  869. }
  870. unset($options['id']);
  871. $contents = $part->toString($options);
  872. $prev_id = Horde_Mime::mimeIdArithmetic($id, 'up', array('norfc822' => true));
  873. $prev_part = ($prev_id == $this->getMimeId())
  874. ? $this
  875. : $this[$prev_id];
  876. if (!$prev_part) {
  877. return $contents;
  878. }
  879. $boundary = trim($this->getContentTypeParameter('boundary'), '"');
  880. $parts = array(
  881. $eol . '--' . $boundary . $eol,
  882. $contents
  883. );
  884. if (!isset($this[Horde_Mime::mimeIdArithmetic($id, 'next')])) {
  885. $parts[] = $eol . '--' . $boundary . '--' . $eol;
  886. }
  887. } else {
  888. if ($isbase = empty($options['_notbase'])) {
  889. $headers = !empty($options['headers'])
  890. ? $options['headers']
  891. : false;
  892. if (empty($options['encode'])) {
  893. $options['encode'] = null;
  894. }
  895. if (empty($options['defserver'])) {
  896. $options['defserver'] = null;
  897. }
  898. $options['headers'] = true;
  899. $options['_notbase'] = true;
  900. } else {
  901. $headers = true;
  902. $oldbaseptr = &$options['_baseptr'];
  903. }
  904. $this->_temp['toString'] = '';
  905. $options['_baseptr'] = &$this->_temp['toString'];
  906. /* Any information about a message is embedded in the message
  907. * contents themself. Simply output the contents of the part
  908. * directly and return. */
  909. $ptype = $this->getPrimaryType();
  910. if ($ptype == 'message') {
  911. $parts[] = $this->_contents;
  912. } else {
  913. if (!empty($this->_contents)) {
  914. $encoding = $this->_getTransferEncoding($options['encode']);
  915. switch ($encoding) {
  916. case '8bit':
  917. if (empty($options['_baseptr'])) {
  918. $options['_baseptr'] = '8bit';
  919. }
  920. break;
  921. case 'binary':
  922. $options['_baseptr'] = 'binary';
  923. break;
  924. }
  925. $parts[] = $this->_transferEncode($this->_contents, $encoding);
  926. /* If not using $this->_contents, we can close the stream
  927. * when finished. */
  928. if ($this->_temp['transferEncodeClose']) {
  929. $parts_close[] = end($parts);
  930. }
  931. }
  932. /* Deal with multipart messages. */
  933. if ($ptype == 'multipart') {
  934. if (empty($this->_contents)) {
  935. $parts[] = 'This message is in MIME format.' . $eol;
  936. }
  937. $boundary = trim($this->getContentTypeParameter('boundary'), '"');
  938. /* If base part is multipart/digest, children should not
  939. * have content-type (automatically treated as
  940. * message/rfc822; RFC 2046 [5.1.5]). */
  941. if ($this->getSubType() === 'digest') {
  942. $options['is_digest'] = true;
  943. }
  944. foreach ($this as $part) {
  945. $parts[] = $eol . '--' . $boundary . $eol;
  946. $tmp = $part->toString($options);
  947. if ($part->getEOL() != $eol) {
  948. $tmp = $this->replaceEOL($tmp, $eol, !empty($options['stream']));
  949. }
  950. if (!empty($options['stream'])) {
  951. $parts_close[] = $tmp;
  952. }
  953. $parts[] = $tmp;
  954. }
  955. $parts[] = $eol . '--' . $boundary . '--' . $eol;
  956. }
  957. }
  958. if (is_string($headers)) {
  959. array_unshift($parts, $headers);
  960. } elseif ($headers) {
  961. $hdr_ob = $this->addMimeHeaders(array(
  962. 'encode' => $options['encode'],
  963. 'headers' => ($headers === true) ? null : $headers
  964. ));
  965. if (!$isbase && !empty($options['is_digest'])) {
  966. unset($hdr_ob['content-type']);
  967. }
  968. if (!empty($this->_temp['toString'])) {
  969. $hdr_ob->addHeader(
  970. 'Content-Transfer-Encoding',
  971. $this->_temp['toString']
  972. );
  973. }
  974. array_unshift($parts, $hdr_ob->toString(array(
  975. 'canonical' => ($eol == self::RFC_EOL),
  976. 'charset' => $this->getHeaderCharset(),
  977. 'defserver' => $options['defserver']
  978. )));
  979. }
  980. }
  981. $newfp = $this->_writeStream($parts);
  982. array_map('fclose', $parts_close);
  983. if (!is_null($oldbaseptr)) {
  984. switch ($this->_temp['toString']) {
  985. case '8bit':
  986. if (empty($oldbaseptr)) {
  987. $oldbaseptr = '8bit';
  988. }
  989. break;
  990. case 'binary':
  991. $oldbaseptr = 'binary';
  992. break;
  993. }
  994. }
  995. if ($isbase && !empty($options['canonical'])) {
  996. return $this->replaceEOL($newfp, self::RFC_EOL, !empty($options['stream']));
  997. }
  998. return empty($options['stream'])
  999. ? $this->_readStream($newfp)
  1000. : $newfp;
  1001. }
  1002. /**
  1003. * Get the transfer encoding for the part based on the user requested
  1004. * transfer encoding and the current contents of the part.
  1005. *
  1006. * @param integer $encode A mask of allowable encodings.
  1007. *
  1008. * @return string The transfer-encoding of this part.
  1009. */
  1010. protected function _getTransferEncoding($encode = self::ENCODE_7BIT)
  1011. {
  1012. if (!empty($this->_temp['sendEncoding'])) {
  1013. return $this->_temp['sendEncoding'];
  1014. } elseif (!empty($this->_temp['sendTransferEncoding'][$encode])) {
  1015. return $this->_temp['sendTransferEncoding'][$encode];
  1016. }
  1017. if (empty($this->_contents)) {
  1018. $encoding = '7bit';
  1019. } else {
  1020. switch ($this->getPrimaryType()) {
  1021. case 'message':
  1022. case 'multipart':
  1023. /* RFC 2046 [5.2.1] - message/rfc822 messages only allow 7bit,
  1024. * 8bit, and binary encodings. If the current encoding is
  1025. * either base64 or q-p, switch it to 8bit instead.
  1026. * RFC 2046 [5.2.2, 5.2.3, 5.2.4] - All other messages
  1027. * only allow 7bit encodings.
  1028. *
  1029. * TODO: What if message contains 8bit characters and we are
  1030. * in strict 7bit mode? Not sure there is anything we can do
  1031. * in that situation, especially for message/rfc822 parts.
  1032. *
  1033. * These encoding will be figured out later (via toString()).
  1034. * They are limited to 7bit, 8bit, and binary. Default to
  1035. * '7bit' per RFCs. */
  1036. $default_8bit = 'base64';
  1037. $encoding = '7bit';
  1038. break;
  1039. case 'text':
  1040. $default_8bit = 'quoted-printable';
  1041. $encoding = '7bit';
  1042. break;
  1043. default:
  1044. $default_8bit = 'base64';
  1045. /* If transfer encoding has changed from the default, use that
  1046. * value. */
  1047. $encoding = ($this->_transferEncoding == self::DEFAULT_ENCODING)
  1048. ? 'base64'
  1049. : $this->_transferEncoding;
  1050. break;
  1051. }
  1052. switch ($encoding) {
  1053. case 'base64':
  1054. case 'binary':
  1055. break;
  1056. default:
  1057. $encoding = $this->_scanStream($this->_contents);
  1058. break;
  1059. }
  1060. switch ($encoding) {
  1061. case 'base64':
  1062. case 'binary':
  1063. /* If the text is longer than 998 characters between
  1064. * linebreaks, use quoted-printable encoding to ensure the
  1065. * text will not be chopped (i.e. by sendmail if being
  1066. * sent as mail text). */
  1067. $encoding = $default_8bit;
  1068. break;
  1069. case '8bit':
  1070. $encoding = (($encode & self::ENCODE_8BIT) || ($encode & self::ENCODE_BINARY))
  1071. ? '8bit'
  1072. : $default_8bit;
  1073. break;
  1074. }
  1075. }
  1076. $this->_temp['sendTransferEncoding'][$encode] = $encoding;
  1077. return $encoding;
  1078. }
  1079. /**
  1080. * Replace newlines in this part's contents with those specified by either
  1081. * the given newline sequence or the part's current EOL setting.
  1082. *
  1083. * @param mixed $text The text to replace. Either a string or a
  1084. * stream resource. If a stream, and returning
  1085. * a string, will close the stream when done.
  1086. * @param string $eol The EOL sequence to use. If not present, uses
  1087. * the part's current EOL setting.
  1088. * @param boolean $stream If true, returns a stream resource.
  1089. *
  1090. * @return string The text with the newlines replaced by the desired
  1091. * newline sequence (returned as a stream resource if
  1092. * $stream is true).
  1093. */
  1094. public function replaceEOL($text, $eol = null, $stream = false)
  1095. {
  1096. if (is_null($eol)) {
  1097. $eol = $this->getEOL();
  1098. }
  1099. stream_filter_register('horde_eol', 'Horde_Stream_Filter_Eol');
  1100. $fp = $this->_writeStream($text, array(
  1101. 'filter' => array(
  1102. 'horde_eol' => array('eol' => $eol)
  1103. )
  1104. ));
  1105. return $stream ? $fp : $this->_readStream($fp, true);
  1106. }
  1107. /**
  1108. * Determine the size of this MIME part and its child members.
  1109. *
  1110. * @todo Remove $approx parameter.
  1111. *
  1112. * @param boolean $approx If true, determines an approximate size for
  1113. * parts consisting of base64 encoded data.
  1114. *
  1115. * @return integer Size of the part, in bytes.
  1116. */
  1117. public function getBytes($approx = false)
  1118. {
  1119. if ($this->getPrimaryType() == 'multipart') {
  1120. if (isset($this->_bytes)) {
  1121. return $this->_bytes;
  1122. }
  1123. $bytes = 0;
  1124. foreach ($this as $part) {
  1125. $bytes += $part->getBytes($approx);
  1126. }
  1127. return $bytes;
  1128. }
  1129. if ($this->_contents) {
  1130. fseek($this->_contents, 0, SEEK_END);
  1131. $bytes = ftell($this->_contents);
  1132. } else {
  1133. $bytes = $this->_bytes;
  1134. /* Base64 transfer encoding is approx. 33% larger than original
  1135. * data size (RFC 2045 [6.8]). */
  1136. if ($approx && ($this->_transferEncoding == 'base64')) {
  1137. $bytes *= 0.75;
  1138. }
  1139. }
  1140. return intval($bytes);
  1141. }
  1142. /**
  1143. * Explicitly set the size (in bytes) of this part. This value will only
  1144. * be returned (via getBytes()) if there are no contents currently set.
  1145. *
  1146. * This function is useful for setting the size of the part when the
  1147. * contents of the part are not fully loaded (i.e. creating a
  1148. * Horde_Mime_Part object from IMAP header information without loading the
  1149. * data of the part).
  1150. *
  1151. * @param integer $bytes The size of this part in bytes.
  1152. */
  1153. public function setBytes($bytes)
  1154. {
  1155. /* Consider 'size' disposition parameter to be the canonical size.
  1156. * Only set bytes if that value doesn't exist. */
  1157. if (!$this->getDispositionParameter('size')) {
  1158. $this->setDispositionParameter('size', $bytes);
  1159. }
  1160. }
  1161. /**
  1162. * Output the size of this MIME part in KB.
  1163. *
  1164. * @todo Remove $approx parameter.
  1165. *
  1166. * @param boolean $approx If true, determines an approximate size for
  1167. * parts consisting of base64 encoded data.
  1168. *
  1169. * @return string Size of the part in KB.
  1170. */
  1171. public function getSize($approx = false)
  1172. {
  1173. if (!($bytes = $this->getBytes($approx))) {
  1174. return 0;
  1175. }
  1176. $localeinfo = Horde_Nls::getLocaleInfo();
  1177. // TODO: Workaround broken number_format() prior to PHP 5.4.0.
  1178. return str_replace(
  1179. array('X', 'Y'),
  1180. array($localeinfo['decimal_point'], $localeinfo['thousands_sep']),
  1181. number_format(ceil($bytes / 1024), 0, 'X', 'Y')
  1182. );
  1183. }
  1184. /**
  1185. * Sets the Content-ID header for this part.
  1186. *
  1187. * @param string $cid Use this CID (if not already set). Else, generate
  1188. * a random CID.
  1189. *
  1190. * @return string The Content-ID for this part.
  1191. */
  1192. public function setContentId($cid = null)
  1193. {
  1194. if (!is_null($id = $this->getContentId())) {
  1195. return $id;
  1196. }
  1197. $this->_headers->addHeaderOb(
  1198. is_null($cid)
  1199. ? Horde_Mime_Headers_ContentId::create()
  1200. : new Horde_Mime_Headers_ContentId(null, $cid)
  1201. );
  1202. return $this->getContentId();
  1203. }
  1204. /**
  1205. * Returns the Content-ID for this part.
  1206. *
  1207. * @return string The Content-ID for this part (null if not set).
  1208. */
  1209. public function getContentId()
  1210. {
  1211. return ($hdr = $this->_headers['content-id'])
  1212. ? trim($hdr->value, '<>')
  1213. : null;
  1214. }
  1215. /**
  1216. * Alter the MIME ID of this part.
  1217. *
  1218. * @param string $mimeid The MIME ID.
  1219. */
  1220. public function setMimeId($mimeid)
  1221. {
  1222. $this->_mimeid = $mimeid;
  1223. }
  1224. /**
  1225. * Returns the MIME ID of this part.
  1226. *
  1227. * @return string The MIME ID.
  1228. */
  1229. public function getMimeId()
  1230. {
  1231. return $this->_mimeid;
  1232. }
  1233. /**
  1234. * Build the MIME IDs for this part and all subparts.
  1235. *
  1236. * @param string $id The ID of this part.
  1237. * @param boolean $rfc822 Is this a message/rfc822 part?
  1238. */
  1239. public function buildMimeIds($id = null, $rfc822 = false)
  1240. {
  1241. $this->_status &= ~self::STATUS_REINDEX;
  1242. if (is_null($id)) {
  1243. $rfc822 = true;
  1244. $id = '';
  1245. }
  1246. if ($rfc822) {
  1247. if (empty($this->_parts) &&
  1248. ($this->getPrimaryType() != 'multipart')) {
  1249. $this->setMimeId($id . '1');
  1250. } else {
  1251. if (empty($id) && ($this->getType() == 'message/rfc822')) {
  1252. $this->setMimeId('1.0');
  1253. } else {
  1254. $this->setMimeId($id . '0');
  1255. }
  1256. $i = 1;
  1257. foreach ($this as $val) {
  1258. $val->buildMimeIds($id . ($i++));
  1259. }
  1260. }
  1261. } else {
  1262. $this->setMimeId($id);
  1263. $id = $id
  1264. ? ((substr($id, -2) === '.0') ? substr($id, 0, -1) : ($id . '.'))
  1265. : '';
  1266. if (count($this)) {
  1267. if ($this->getType() == 'message/rfc822') {
  1268. $this->rewind();
  1269. $this->current()->buildMimeIds($id, true);
  1270. } else {
  1271. $i = 1;
  1272. foreach ($this as $val) {
  1273. $val->buildMimeIds($id . ($i++));
  1274. }
  1275. }
  1276. }
  1277. }
  1278. }
  1279. /**
  1280. * Is this the base MIME part?
  1281. *
  1282. * @param boolean $base True if this is the base MIME part.
  1283. */
  1284. public function isBasePart($base)
  1285. {
  1286. if (empty($base)) {
  1287. $this->_status &= ~self::STATUS_BASEPART;
  1288. } else {
  1289. $this->_status |= self::STATUS_BASEPART;
  1290. }
  1291. }
  1292. /**
  1293. * Determines if this MIME part is an attachment for display purposes.
  1294. *
  1295. * @since Horde_Mime 2.10.0
  1296. *
  1297. * @return boolean True if this part should be considered an attachment.
  1298. */
  1299. public function isAttachment()
  1300. {
  1301. $type = $this->getType();
  1302. switch ($type) {
  1303. case 'application/ms-tnef':
  1304. case 'application/pgp-keys':
  1305. case 'application/vnd.ms-tnef':
  1306. return false;
  1307. }
  1308. if ($this->parent) {
  1309. switch ($this->parent->getType()) {
  1310. case 'multipart/encrypted':
  1311. switch ($type) {
  1312. case 'application/octet-stream':
  1313. return false;
  1314. }
  1315. break;
  1316. case 'multipart/signed':
  1317. switch ($type) {
  1318. case 'application/pgp-signature':
  1319. case 'application/pkcs7-signature':
  1320. case 'application/x-pkcs7-signature':
  1321. return false;
  1322. }
  1323. break;
  1324. }
  1325. }
  1326. switch ($this->getDisposition()) {
  1327. case 'attachment':
  1328. return true;
  1329. }
  1330. switch ($this->getPrimaryType()) {
  1331. case 'application':
  1332. if (strlen($this->getName())) {
  1333. return true;
  1334. }
  1335. break;
  1336. case 'audio':
  1337. case 'video':
  1338. return true;
  1339. case 'multipart':
  1340. return false;
  1341. }
  1342. return false;
  1343. }
  1344. /**
  1345. * Set a piece of metadata on this object.
  1346. *
  1347. * @param string $key The metadata key.
  1348. * @param mixed $data The metadata. If null, clears the key.
  1349. */
  1350. public function setMetadata($key, $data = null)
  1351. {
  1352. if (is_null($data)) {
  1353. unset($this->_metadata[$key]);
  1354. } else {
  1355. $this->_metadata[$key] = $data;
  1356. }
  1357. }
  1358. /**
  1359. * Retrieves metadata from this object.
  1360. *
  1361. * @param string $key The metadata key.
  1362. *
  1363. * @return mixed The metadata, or null if it doesn't exist.
  1364. */
  1365. public function getMetadata($key)
  1366. {
  1367. return isset($this->_metadata[$key])
  1368. ? $this->_metadata[$key]
  1369. : null;
  1370. }
  1371. /**
  1372. * Sends this message.
  1373. *
  1374. * @param string $email The address list to send to.
  1375. * @param Horde_Mime_Headers $headers The Horde_Mime_Headers object
  1376. * holding this message's headers.
  1377. * @param Horde_Mail_Transport $mailer A Horde_Mail_Transport object.
  1378. * @param array $opts Additional options:
  1379. * <pre>
  1380. * - broken_rfc2231: (boolean) Attempt to work around non-RFC
  1381. * 2231-compliant MUAs by generating both a RFC
  1382. * 2047-like parameter name and also the correct RFC
  1383. * 2231 parameter (@since 2.5.0).
  1384. * DEFAULT: false
  1385. * - encode: (integer) The encoding to use. A mask of self::ENCODE_*
  1386. * values.
  1387. * DEFAULT: Auto-determined based on transport driver.
  1388. * </pre>
  1389. *
  1390. * @throws Horde_Mime_Exception
  1391. * @throws InvalidArgumentException
  1392. */
  1393. public function send($email, $headers, Horde_Mail_Transport $mailer,
  1394. array $opts = array())
  1395. {
  1396. $old_status = $this->_status;
  1397. $this->isBasePart(true);
  1398. /* Does the SMTP backend support 8BITMIME (RFC 1652)? */
  1399. $canonical = true;
  1400. $encode = self::ENCODE_7BIT;
  1401. if (isset($opts['encode'])) {
  1402. /* Always allow 7bit encoding. */
  1403. $encode |= $opts['encode'];
  1404. } elseif ($mailer instanceof Horde_Mail_Transport_Smtp) {
  1405. try {
  1406. $smtp_ext = $mailer->getSMTPObject()->getServiceExtensions();
  1407. if (isset($smtp_ext['8BITMIME'])) {
  1408. $encode |= self::ENCODE_8BIT;
  1409. }
  1410. } catch (Horde_Mail_Exception $e) {}
  1411. $canonical = false;
  1412. } elseif ($mailer instanceof Horde_Mail_Transport_Smtphorde) {
  1413. try {
  1414. if ($mailer->getSMTPObject()->data_8bit) {
  1415. $encode |= self::ENCODE_8BIT;
  1416. }
  1417. } catch (Horde_Mail_Exception $e) {}
  1418. $canonical = false;
  1419. }
  1420. $msg = $this->toString(array(
  1421. 'canonical' => $canonical,
  1422. 'encode' => $encode,
  1423. 'headers' => false,
  1424. 'stream' => true
  1425. ));
  1426. /* Add MIME Headers if they don't already exist. */
  1427. if (!isset($headers['MIME-Version'])) {
  1428. $headers = $this->addMimeHeaders(array(
  1429. 'encode' => $encode,
  1430. 'headers' => $headers
  1431. ));
  1432. }
  1433. if (!empty($this->_temp['toString'])) {
  1434. $headers->addHeader(
  1435. 'Content-Transfer-Encoding',
  1436. $this->_temp['toString']
  1437. );
  1438. switch ($this->_temp['toString']) {
  1439. case '8bit':
  1440. if ($mailer instanceof Horde_Mail_Transport_Smtp) {
  1441. $mailer->addServiceExtensionParameter('BODY', '8BITMIME');
  1442. }
  1443. break;
  1444. }
  1445. }
  1446. $this->_status = $old_status;
  1447. $rfc822 = new Horde_Mail_Rfc822();
  1448. try {
  1449. $mailer->send($rfc822->parseAddressList($email)->writeAddress(array(
  1450. 'encode' => $this->getHeaderCharset() ?: true,
  1451. 'idn' => true
  1452. )), $headers->toArray(array(
  1453. 'broken_rfc2231' => !empty($opts['broken_rfc2231']),
  1454. 'canonical' => $canonical,
  1455. 'charset' => $this->getHeaderCharset()
  1456. )), $msg);
  1457. } catch (InvalidArgumentException $e) {
  1458. // Try to rebuild the part in case it was due to
  1459. // an invalid line length in a rfc822/message attachment.
  1460. if ($this->_failed) {
  1461. throw $e;
  1462. }
  1463. $this->_failed = true;
  1464. $this->_sanityCheckRfc822Attachments();
  1465. try {
  1466. $this->send($email, $headers, $mailer, $opts);
  1467. } catch (Horde_Mail_Exception $e) {
  1468. throw new Horde_Mime_Exception($e);
  1469. }
  1470. } catch (Horde_Mail_Exception $e) {
  1471. throw new Horde_Mime_Exception($e);
  1472. }
  1473. }
  1474. /**
  1475. * Finds the main "body" text part (if any) in a message.
  1476. * "Body" data is the first text part under this part.
  1477. *
  1478. * @param string $subtype Specifically search for this subtype.
  1479. *
  1480. * @return mixed The MIME ID of the main body part, or null if a body
  1481. * part is not found.
  1482. */
  1483. public function findBody($subtype = null)
  1484. {
  1485. $this->buildMimeIds();
  1486. foreach ($this->partIterator() as $val) {
  1487. $id = $val->getMimeId();
  1488. if (($val->getPrimaryType() == 'text') &&
  1489. ((intval($id) === 1) || !$this->getMimeId()) &&
  1490. (is_null($subtype) || ($val->getSubType() == $subtype)) &&
  1491. ($val->getDisposition() !== 'attachment')) {
  1492. return $id;
  1493. }
  1494. }
  1495. return null;
  1496. }
  1497. /**
  1498. * Returns the recursive iterator needed to iterate through this part.
  1499. *
  1500. * @since 2.8.0
  1501. *
  1502. * @param boolean $current Include the current part as the base?
  1503. *
  1504. * @return Iterator Recursive iterator.
  1505. */
  1506. public function partIterator($current = true)
  1507. {
  1508. $this->_reindex(true);
  1509. return new Horde_Mime_Part_Iterator($this, $current);
  1510. }
  1511. /**
  1512. * Returns a subpart by index.
  1513. *
  1514. * @return Horde_Mime_Part Part, or null if not found.
  1515. */
  1516. public function getPartByIndex($index)
  1517. {
  1518. if (!isset($this->_parts[$index])) {
  1519. return null;
  1520. }
  1521. $part = $this->_parts[$index];
  1522. $part->parent = $this;
  1523. return $part;
  1524. }
  1525. /**
  1526. * Reindexes the MIME IDs, if necessary.
  1527. *
  1528. * @param boolean $force Reindex if the current part doesn't have an ID.
  1529. */
  1530. protected function _reindex($force = false)
  1531. {
  1532. $id = $this->getMimeId();
  1533. if (($this->_status & self::STATUS_REINDEX) ||
  1534. ($force && is_null($id))) {
  1535. $this->buildMimeIds(
  1536. is_null($id)
  1537. ? (($this->getPrimaryType() === 'multipart') ? '0' : '1')
  1538. : $id
  1539. );
  1540. }
  1541. }
  1542. /**
  1543. * Write data to a stream.
  1544. *
  1545. * @param array $data The data to write. Either a stream resource or
  1546. * a string.
  1547. * @param array $options Additional options:
  1548. * - error: (boolean) Catch errors when writing to the stream. Throw an
  1549. * ErrorException if an error is found.
  1550. * DEFAULT: false
  1551. * - filter: (array) Filter(s) to apply to the string. Keys are the
  1552. * filter names, values are filter params.
  1553. * - fp: (resource) Use this stream instead of creating a new one.
  1554. *
  1555. * @return resource The stream resource.
  1556. * @throws ErrorException
  1557. */
  1558. protected function _writeStream($data, $options = array())
  1559. {
  1560. if (empty($options['fp'])) {
  1561. $fp = fopen('php://temp/maxmemory:' . self::$memoryLimit, 'r+');
  1562. } else {
  1563. $fp = $options['fp'];
  1564. fseek($fp, 0, SEEK_END);
  1565. }
  1566. if (!is_array($data)) {
  1567. $data = array($data);
  1568. }
  1569. $append_filter = array();
  1570. if (!empty($options['filter'])) {
  1571. foreach ($options['filter'] as $key => $val) {
  1572. $append_filter[] = stream_filter_append($fp, $key, STREAM_FILTER_WRITE, $val);
  1573. }
  1574. }
  1575. if (!empty($options['error'])) {
  1576. set_error_handler(function($errno, $errstr) {
  1577. throw new ErrorException($errstr, $errno);
  1578. });
  1579. $error = null;
  1580. }
  1581. try {
  1582. foreach ($data as $d) {
  1583. if (is_resource($d)) {
  1584. rewind($d);
  1585. while (!feof($d)) {
  1586. fwrite($fp, fread($d, 8192));
  1587. }
  1588. } elseif (is_string($d)) {
  1589. $len = strlen($d);
  1590. $i = 0;
  1591. while ($i < $len) {
  1592. fwrite($fp, substr($d, $i, 8192));
  1593. $i += 8192;
  1594. }
  1595. }
  1596. }
  1597. } catch (ErrorException $e) {
  1598. $error = $e;
  1599. }
  1600. foreach ($append_filter as $val) {
  1601. stream_filter_remove($val);
  1602. }
  1603. if (!empty($options['error'])) {
  1604. restore_error_handler();
  1605. if ($error) {
  1606. throw $error;
  1607. }
  1608. }
  1609. return $fp;
  1610. }
  1611. /**
  1612. * Read data from a stream.
  1613. *
  1614. * @param resource $fp An active stream.
  1615. * @param boolean $close Close the stream when done reading?
  1616. *
  1617. * @return string The data from the stream.
  1618. */
  1619. protected function _readStream($fp, $close = false)
  1620. {
  1621. $out = '';
  1622. if (!is_resource($fp)) {
  1623. return $out;
  1624. }
  1625. rewind($fp);
  1626. while (!feof($fp)) {
  1627. $out .= fread($fp, 8192);
  1628. }
  1629. if ($close) {
  1630. fclose($fp);
  1631. }
  1632. return $out;
  1633. }
  1634. /**
  1635. * Scans a stream for content type.
  1636. *
  1637. * @param resource $fp A stream resource.
  1638. *
  1639. * @return mixed Either 'binary', '8bit', or false.
  1640. */
  1641. protected function _scanStream($fp)
  1642. {
  1643. rewind($fp);
  1644. stream_filter_register(
  1645. 'horde_mime_scan_stream',
  1646. 'Horde_Mime_Filter_Encoding'
  1647. );
  1648. $filter_params = new stdClass;
  1649. $filter = stream_filter_append(
  1650. $fp,
  1651. 'horde_mime_scan_stream',
  1652. STREAM_FILTER_READ,
  1653. $filter_params
  1654. );
  1655. while (!feof($fp)) {
  1656. fread($fp, 8192);
  1657. }
  1658. stream_filter_remove($filter);
  1659. return $filter_params->body;
  1660. }
  1661. /* Static methods. */
  1662. /**
  1663. * Attempts to build a Horde_Mime_Part object from message text.
  1664. *
  1665. * @param string $text The text of the MIME message.
  1666. * @param array $opts Additional options:
  1667. * - forcemime: (boolean) If true, the message data is assumed to be
  1668. * MIME data. If not, a MIME-Version header must exist (RFC
  1669. * 2045 [4]) to be parsed as a MIME message.
  1670. * DEFAULT: false
  1671. * - level: (integer) Current nesting level of the MIME data.
  1672. * DEFAULT: 0
  1673. * - no_body: (boolean) If true, don't set body contents of parts (since
  1674. * 2.2.0).
  1675. * DEFAULT: false
  1676. *
  1677. * @return Horde_Mime_Part A MIME Part object.
  1678. * @throws Horde_Mime_Exception
  1679. */
  1680. public static function parseMessage($text, array $opts = array())
  1681. {
  1682. /* Mini-hack to get a blank Horde_Mime part so we can call
  1683. * replaceEOL(). Convert to EOL, since that is the expected EOL for
  1684. * use internally within a Horde_Mime_Part object. */
  1685. $part = new Horde_Mime_Part();
  1686. $rawtext = $part->replaceEOL($text, self::EOL);
  1687. /* Find the header. */
  1688. $hdr_pos = self::_findHeader($rawtext, self::EOL);
  1689. unset($opts['ctype']);
  1690. $ob = self::_getStructure(substr($rawtext, 0, $hdr_pos), substr($rawtext, $hdr_pos + 2), $opts);
  1691. $ob->buildMimeIds();
  1692. return $ob;
  1693. }
  1694. /**
  1695. * Creates a MIME object from the text of one part of a MIME message.
  1696. *
  1697. * @param string $header The header text.
  1698. * @param string $body The body text.
  1699. * @param array $opts Additional options:
  1700. * <pre>
  1701. * - ctype: (string) The default content-type.
  1702. * - forcemime: (boolean) If true, the message data is assumed to be
  1703. * MIME data. If not, a MIME-Version header must exist to
  1704. * be parsed as a MIME message.
  1705. * - level: (integer) Current nesting level.
  1706. * - no_body: (boolean) If true, don't set body contents of parts.
  1707. * </pre>
  1708. *
  1709. * @return Horde_Mime_Part The MIME part object.
  1710. */
  1711. protected static function _getStructure($header, $body,
  1712. array $opts = array())
  1713. {
  1714. $opts = array_merge(array(
  1715. 'ctype' => 'text/plain',
  1716. 'forcemime' => false,
  1717. 'level' => 0,
  1718. 'no_body' => false
  1719. ), $opts);
  1720. /* Parse headers text into a Horde_Mime_Headers object. */
  1721. $hdrs = Horde_Mime_Headers::parseHeaders($header);
  1722. $ob = new Horde_Mime_Part();
  1723. /* This is not a MIME message. */
  1724. if (!$opts['forcemime'] && !isset($hdrs['MIME-Version'])) {
  1725. $ob->setType('text/plain');
  1726. if ($len = strlen($body)) {
  1727. if ($opts['no_body']) {
  1728. $ob->setBytes($len);
  1729. } else {
  1730. $ob->setContents($body);
  1731. }
  1732. }
  1733. return $ob;
  1734. }
  1735. /* Content type. */
  1736. if ($tmp = $hdrs['Content-Type']) {
  1737. $ob->setType($tmp->value);
  1738. foreach ($tmp->params as $key => $val) {
  1739. $ob->setContentTypeParameter($key, $val);
  1740. }
  1741. } else {
  1742. $ob->setType($opts['ctype']);
  1743. }
  1744. /* Content transfer encoding. */
  1745. if ($tmp = $hdrs['Content-Transfer-Encoding']) {
  1746. $ob->setTransferEncoding(strval($tmp));
  1747. }
  1748. /* Content-Description. */
  1749. if ($tmp = $hdrs['Content-Description']) {
  1750. $ob->setDescription(strval($tmp));
  1751. }
  1752. /* Content-Disposition. */
  1753. if ($tmp = $hdrs['Content-Disposition']) {
  1754. $ob->setDisposition($tmp->value);
  1755. foreach ($tmp->params as $key => $val) {
  1756. $ob->setDispositionParameter($key, $val);
  1757. }
  1758. }
  1759. /* Content-Duration */
  1760. if ($tmp = $hdrs['Content-Duration']) {
  1761. $ob->setDuration(strval($tmp));
  1762. }
  1763. /* Content-ID. */
  1764. if ($tmp = $hdrs['Content-Id']) {
  1765. $ob->setContentId(strval($tmp));
  1766. }
  1767. if (($len = strlen($body)) && ($ob->getPrimaryType() != 'multipart')) {
  1768. if ($opts['no_body']) {
  1769. $ob->setBytes($len);
  1770. } else {
  1771. $ob->setContents($body);
  1772. }
  1773. }
  1774. if (++$opts['level'] >= self::NESTING_LIMIT) {
  1775. return $ob;
  1776. }
  1777. /* Process subparts. */
  1778. switch ($ob->getPrimaryType()) {
  1779. case 'message':
  1780. if ($ob->getSubType() == 'rfc822') {
  1781. $ob[] = self::parseMessage($body, array(
  1782. 'forcemime' => true,
  1783. 'no_body' => $opts['no_body']
  1784. ));
  1785. }
  1786. break;
  1787. case 'multipart':
  1788. $boundary = $ob->getContentTypeParameter('boundary');
  1789. if (!is_null($boundary)) {
  1790. foreach (self::_findBoundary($body, 0, $boundary) as $val) {
  1791. if (!isset($val['length'])) {
  1792. break;
  1793. }
  1794. $subpart = substr($body, $val['start'], $val['length']);
  1795. $hdr_pos = self::_findHeader($subpart, self::EOL);
  1796. $ob[] = self::_getStructure(
  1797. substr($subpart, 0, $hdr_pos),
  1798. substr($subpart, $hdr_pos + 2),
  1799. array(
  1800. 'ctype' => ($ob->getSubType() == 'digest') ? 'message/rfc822' : 'text/plain',
  1801. 'forcemime' => true,
  1802. 'level' => $opts['level'],
  1803. 'no_body' => $opts['no_body']
  1804. )
  1805. );
  1806. }
  1807. }
  1808. break;
  1809. }
  1810. return $ob;
  1811. }
  1812. /**
  1813. * Attempts to obtain the raw text of a MIME part.
  1814. *
  1815. * @param mixed $text The full text of the MIME message. The text is
  1816. * assumed to be MIME data (no MIME-Version checking
  1817. * is performed). It can be either a stream or a
  1818. * string.
  1819. * @param string $type Either 'header' or 'body'.
  1820. * @param string $id The MIME ID.
  1821. *
  1822. * @return string The raw text.
  1823. * @throws Horde_Mime_Exception
  1824. */
  1825. public static function getRawPartText($text, $type, $id)
  1826. {
  1827. /* Mini-hack to get a blank Horde_Mime part so we can call
  1828. * replaceEOL(). From an API perspective, getRawPartText() should be
  1829. * static since it is not working on MIME part data. */
  1830. $part = new Horde_Mime_Part();
  1831. $rawtext = $part->replaceEOL($text, self::RFC_EOL);
  1832. /* We need to carry around the trailing "\n" because this is needed
  1833. * to correctly find the boundary string. */
  1834. $hdr_pos = self::_findHeader($rawtext, self::RFC_EOL);
  1835. $curr_pos = $hdr_pos + 3;
  1836. if ($id == 0) {
  1837. switch ($type) {
  1838. case 'body':
  1839. return substr($rawtext, $curr_pos + 1);
  1840. case 'header':
  1841. return trim(substr($rawtext, 0, $hdr_pos));
  1842. }
  1843. }
  1844. $hdr_ob = Horde_Mime_Headers::parseHeaders(trim(substr($rawtext, 0, $hdr_pos)));
  1845. /* If this is a message/rfc822, pass the body into the next loop.
  1846. * Don't decrement the ID here. */
  1847. if (($ct = $hdr_ob['Content-Type']) && ($ct == 'message/rfc822')) {
  1848. return self::getRawPartText(
  1849. substr($rawtext, $curr_pos + 1),
  1850. $type,
  1851. $id
  1852. );
  1853. }
  1854. $base_pos = strpos($id, '.');
  1855. $orig_id = $id;
  1856. if ($base_pos !== false) {
  1857. $id = substr($id, $base_pos + 1);
  1858. $base_pos = substr($orig_id, 0, $base_pos);
  1859. } else {
  1860. $base_pos = $id;
  1861. $id = 0;
  1862. }
  1863. if ($ct && !isset($ct->params['boundary'])) {
  1864. if ($orig_id == '1') {
  1865. return substr($rawtext, $curr_pos + 1);
  1866. }
  1867. throw new Horde_Mime_Exception('Could not find MIME part.');
  1868. }
  1869. $b_find = self::_findBoundary(
  1870. $rawtext,
  1871. $curr_pos,
  1872. $ct->params['boundary'],
  1873. $base_pos
  1874. );
  1875. if (!isset($b_find[$base_pos])) {
  1876. throw new Horde_Mime_Exception('Could not find MIME part.');
  1877. }
  1878. return self::getRawPartText(
  1879. substr(
  1880. $rawtext,
  1881. $b_find[$base_pos]['start'],
  1882. $b_find[$base_pos]['length'] - 1
  1883. ),
  1884. $type,
  1885. $id
  1886. );
  1887. }
  1888. /**
  1889. * Find the location of the end of the header text.
  1890. *
  1891. * @param string $text The text to search.
  1892. * @param string $eol The EOL string.
  1893. *
  1894. * @return integer Header position.
  1895. */
  1896. protected static function _findHeader($text, $eol)
  1897. {
  1898. $hdr_pos = strpos($text, $eol . $eol);
  1899. return ($hdr_pos === false)
  1900. ? strlen($text)
  1901. : $hdr_pos;
  1902. }
  1903. /**
  1904. * Find the location of the next boundary string.
  1905. *
  1906. * @param string $text The text to search.
  1907. * @param integer $pos The current position in $text.
  1908. * @param string $boundary The boundary string.
  1909. * @param integer $end If set, return after matching this many
  1910. * boundaries.
  1911. *
  1912. * @return array Keys are the boundary number, values are an array with
  1913. * two elements: 'start' and 'length'.
  1914. */
  1915. protected static function _findBoundary($text, $pos, $boundary,
  1916. $end = null)
  1917. {
  1918. $i = 0;
  1919. $out = array();
  1920. $search = "--" . $boundary;
  1921. $search_len = strlen($search);
  1922. while (($pos = strpos($text, $search, $pos)) !== false) {
  1923. /* Boundary needs to appear at beginning of string or right after
  1924. * a LF. */
  1925. if (($pos != 0) && ($text[$pos - 1] != "\n")) {
  1926. continue;
  1927. }
  1928. if (isset($out[$i])) {
  1929. $out[$i]['length'] = $pos - $out[$i]['start'] - 1;
  1930. }
  1931. if (!is_null($end) && ($end == $i)) {
  1932. break;
  1933. }
  1934. $pos += $search_len;
  1935. if (isset($text[$pos])) {
  1936. switch ($text[$pos]) {
  1937. case "\r":
  1938. $pos += 2;
  1939. $out[++$i] = array('start' => $pos);
  1940. break;
  1941. case "\n":
  1942. $out[++$i] = array('start' => ++$pos);
  1943. break;
  1944. case '-':
  1945. return $out;
  1946. }
  1947. }
  1948. }
  1949. return $out;
  1950. }
  1951. /**
  1952. * Re-enocdes message/rfc822 parts in case there was e.g., some broken
  1953. * line length in the headers of the message in the part. Since we shouldn't
  1954. * alter the original message in any way, we simply reset cause the part to
  1955. * be encoded as base64 and sent as a application/octet part.
  1956. */
  1957. protected function _sanityCheckRfc822Attachments()
  1958. {
  1959. if ($this->getType() == 'message/rfc822') {
  1960. $this->_reEncodeMessageAttachment($this);
  1961. return;
  1962. }
  1963. foreach ($this->getParts() as $part) {
  1964. if ($part->getType() == 'message/rfc822') {
  1965. $this->_reEncodeMessageAttachment($part);
  1966. }
  1967. }
  1968. return;
  1969. }
  1970. /**
  1971. * Rebuilds $part and forces it to be a base64 encoded
  1972. * application/octet-stream part.
  1973. *
  1974. * @param Horde_Mime_Part $part The MIME part.
  1975. */
  1976. protected function _reEncodeMessageAttachment(Horde_Mime_Part $part)
  1977. {
  1978. $new_part = Horde_Mime_Part::parseMessage($part->getContents());
  1979. $part->setContents($new_part->getContents(array('stream' => true)), array('encoding' => self::ENCODE_BINARY));
  1980. $part->setTransferEncoding('base64', array('send' => true));
  1981. }
  1982. /* ArrayAccess methods. */
  1983. /**
  1984. */
  1985. public function offsetExists($offset)
  1986. {
  1987. return ($this[$offset] !== null);
  1988. }
  1989. /**
  1990. */
  1991. public function offsetGet($offset)
  1992. {
  1993. $this->_reindex();
  1994. if (strcmp($offset, $this->getMimeId()) === 0) {
  1995. $this->parent = null;
  1996. return $this;
  1997. }
  1998. foreach ($this->_parts as $val) {
  1999. if (strcmp($offset, $val->getMimeId()) === 0) {
  2000. $val->parent = $this;
  2001. return $val;
  2002. }
  2003. if ($found = $val[$offset]) {
  2004. return $found;
  2005. }
  2006. }
  2007. return null;
  2008. }
  2009. /**
  2010. */
  2011. public function offsetSet($offset, $value)
  2012. {
  2013. if (is_null($offset)) {
  2014. $this->_parts[] = $value;
  2015. $this->_status |= self::STATUS_REINDEX;
  2016. } elseif ($part = $this[$offset]) {
  2017. if ($part->parent === $this) {
  2018. if (($k = array_search($part, $this->_parts, true)) !== false) {
  2019. $value->setMimeId($part->getMimeId());
  2020. $this->_parts[$k] = $value;
  2021. }
  2022. } else {
  2023. $this->parent[$offset] = $value;
  2024. }
  2025. }
  2026. }
  2027. /**
  2028. */
  2029. public function offsetUnset($offset)
  2030. {
  2031. if ($part = $this[$offset]) {
  2032. if ($part->parent === $this) {
  2033. if (($k = array_search($part, $this->_parts, true)) !== false) {
  2034. unset($this->_parts[$k]);
  2035. $this->_parts = array_values($this->_parts);
  2036. }
  2037. } else {
  2038. unset($part->parent[$offset]);
  2039. }
  2040. $this->_status |= self::STATUS_REINDEX;
  2041. }
  2042. }
  2043. /* Countable methods. */
  2044. /**
  2045. * Returns the number of child message parts (doesn't include
  2046. * grandchildren or more remote ancestors).
  2047. *
  2048. * @return integer Number of message parts.
  2049. */
  2050. public function count()
  2051. {
  2052. return count($this->_parts);
  2053. }
  2054. /* RecursiveIterator methods. */
  2055. /**
  2056. * @since 2.8.0
  2057. */
  2058. public function current()
  2059. {
  2060. return (($key = $this->key()) === null)
  2061. ? null
  2062. : $this->getPartByIndex($key);
  2063. }
  2064. /**
  2065. * @since 2.8.0
  2066. */
  2067. public function key()
  2068. {
  2069. return (isset($this->_temp['iterate']) && isset($this->_parts[$this->_temp['iterate']]))
  2070. ? $this->_temp['iterate']
  2071. : null;
  2072. }
  2073. /**
  2074. * @since 2.8.0
  2075. */
  2076. public function next()
  2077. {
  2078. ++$this->_temp['iterate'];
  2079. }
  2080. /**
  2081. * @since 2.8.0
  2082. */
  2083. public function rewind()
  2084. {
  2085. $this->_reindex();
  2086. reset($this->_parts);
  2087. $this->_temp['iterate'] = key($this->_parts);
  2088. }
  2089. /**
  2090. * @since 2.8.0
  2091. */
  2092. public function valid()
  2093. {
  2094. return ($this->key() !== null);
  2095. }
  2096. /**
  2097. * @since 2.8.0
  2098. */
  2099. public function hasChildren()
  2100. {
  2101. return (($curr = $this->current()) && count($curr));
  2102. }
  2103. /**
  2104. * @since 2.8.0
  2105. */
  2106. public function getChildren()
  2107. {
  2108. return $this->current();
  2109. }
  2110. /* Serializable methods. */
  2111. /**
  2112. * Serialization.
  2113. *
  2114. * @return string Serialized data.
  2115. */
  2116. public function serialize()
  2117. {
  2118. $data = array(
  2119. // Serialized data ID.
  2120. self::VERSION,
  2121. $this->_bytes,
  2122. $this->_eol,
  2123. $this->_hdrCharset,
  2124. $this->_headers,
  2125. $this->_metadata,
  2126. $this->_mimeid,
  2127. $this->_parts,
  2128. $this->_status,
  2129. $this->_transferEncoding
  2130. );
  2131. if (!empty($this->_contents)) {
  2132. $data[] = $this->_readStream($this->_contents);
  2133. }
  2134. return serialize($data);
  2135. }
  2136. /**
  2137. * Unserialization.
  2138. *
  2139. * @param string $data Serialized data.
  2140. *
  2141. * @throws Exception
  2142. */
  2143. public function unserialize($data)
  2144. {
  2145. $data = @unserialize($data);
  2146. if (!is_array($data) ||
  2147. !isset($data[0]) ||
  2148. ($data[0] != self::VERSION)) {
  2149. switch ($data[0]) {
  2150. case 1:
  2151. $convert = new Horde_Mime_Part_Upgrade_V1($data);
  2152. $data = $convert->data;
  2153. break;
  2154. default:
  2155. $data = null;
  2156. break;
  2157. }
  2158. if (is_null($data)) {
  2159. throw new Exception('Cache version change');
  2160. }
  2161. }
  2162. $key = 0;
  2163. $this->_bytes = $data[++$key];
  2164. $this->_eol = $data[++$key];
  2165. $this->_hdrCharset = $data[++$key];
  2166. $this->_headers = $data[++$key];
  2167. $this->_metadata = $data[++$key];
  2168. $this->_mimeid = $data[++$key];
  2169. $this->_parts = $data[++$key];
  2170. $this->_status = $data[++$key];
  2171. $this->_transferEncoding = $data[++$key];
  2172. if (isset($data[++$key])) {
  2173. $this->setContents($data[$key]);
  2174. }
  2175. }
  2176. /* Deprecated elements. */
  2177. /**
  2178. * @deprecated
  2179. */
  2180. const UNKNOWN = 'x-unknown';
  2181. /**
  2182. * @deprecated
  2183. */
  2184. public static $encodingTypes = array(
  2185. '7bit', '8bit', 'base64', 'binary', 'quoted-printable',
  2186. // Non-RFC types, but old mailers may still use
  2187. 'uuencode', 'x-uuencode', 'x-uue'
  2188. );
  2189. /**
  2190. * @deprecated
  2191. */
  2192. public static $mimeTypes = array(
  2193. 'text', 'multipart', 'message', 'application', 'audio', 'image',
  2194. 'video', 'model'
  2195. );
  2196. /**
  2197. * @deprecated Use setContentTypeParameter with a null $data value.
  2198. */
  2199. public function clearContentTypeParameter($label)
  2200. {
  2201. $this->setContentTypeParam($label, null);
  2202. }
  2203. /**
  2204. * @deprecated Use iterator instead.
  2205. */
  2206. public function contentTypeMap($sort = true)
  2207. {
  2208. $map = array();
  2209. foreach ($this->partIterator() as $val) {
  2210. $map[$val->getMimeId()] = $val->getType();
  2211. }
  2212. return $map;
  2213. }
  2214. /**
  2215. * @deprecated Use array access instead.
  2216. */
  2217. public function addPart($mime_part)
  2218. {
  2219. $this[] = $mime_part;
  2220. }
  2221. /**
  2222. * @deprecated Use array access instead.
  2223. */
  2224. public function getPart($id)
  2225. {
  2226. return $this[$id];
  2227. }
  2228. /**
  2229. * @deprecated Use array access instead.
  2230. */
  2231. public function alterPart($id, $mime_part)
  2232. {
  2233. $this[$id] = $mime_part;
  2234. }
  2235. /**
  2236. * @deprecated Use array access instead.
  2237. */
  2238. public function removePart($id)
  2239. {
  2240. unset($this[$id]);
  2241. }
  2242. }