PageRenderTime 60ms CodeModel.GetById 19ms 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

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

  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->getSMTPOb

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