PageRenderTime 40ms CodeModel.GetById 24ms RepoModel.GetById 0ms app.codeStats 0ms

/tine20/Felamimail/Model/Message.php

https://github.com/testruby/Tine-2.0-Open-Source-Groupware-and-CRM
PHP | 621 lines | 334 code | 76 blank | 211 comment | 105 complexity | 7cee9d023ee70bd987c4555ea2c948da MD5 | raw file
  1. <?php
  2. /**
  3. * class to hold message cache data
  4. *
  5. * @package Felamimail
  6. * @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
  7. * @author Lars Kneschke <l.kneschke@metaways.de>
  8. * @copyright Copyright (c) 2009-2011 Metaways Infosystems GmbH (http://www.metaways.de)
  9. *
  10. */
  11. /**
  12. * class to hold message cache data
  13. *
  14. * @package Felamimail
  15. * @property string $subject the subject of the email
  16. * @property string $from_email the address of the sender (from)
  17. * @property string $from_name the name of the sender (from)
  18. * @property string $sender the sender of the email
  19. * @property string $content_type the content type of the message
  20. * @property string $body_content_type the content type of the message body
  21. * @property array $to the to receipients
  22. * @property array $cc the cc receipients
  23. * @property array $bcc the bcc receipients
  24. * @property array $structure the message structure
  25. * @property string $messageuid the message uid on the imap server
  26. */
  27. class Felamimail_Model_Message extends Tinebase_Record_Abstract
  28. {
  29. /**
  30. * message content type (rfc822)
  31. *
  32. */
  33. const CONTENT_TYPE_MESSAGE_RFC822 = 'message/rfc822';
  34. /**
  35. * content type html
  36. *
  37. */
  38. const CONTENT_TYPE_HTML = 'text/html';
  39. /**
  40. * content type plain text
  41. *
  42. */
  43. const CONTENT_TYPE_PLAIN = 'text/plain';
  44. /**
  45. * content type multipart/alternative
  46. *
  47. */
  48. const CONTENT_TYPE_MULTIPART = 'multipart/alternative';
  49. /**
  50. * attachment filename regexp
  51. *
  52. */
  53. const ATTACHMENT_FILENAME_REGEXP = "/name=\"(.*)\"/";
  54. /**
  55. * email address regexp
  56. */
  57. const EMAIL_ADDRESS_REGEXP = '/([a-z0-9_\+-\.]+@[a-z0-9-\.]+\.[a-z]{2,5})/i';
  58. /**
  59. * quote string ("> ")
  60. *
  61. * @var string
  62. */
  63. const QUOTE = '&gt; ';
  64. /**
  65. * key in $_validators/$_properties array for the field which
  66. * represents the identifier
  67. *
  68. * @var string
  69. */
  70. protected $_identifier = 'id';
  71. /**
  72. * application the record belongs to
  73. *
  74. * @var string
  75. */
  76. protected $_application = 'Felamimail';
  77. /**
  78. * list of zend validator
  79. *
  80. * this validators get used when validating user generated content with Zend_Input_Filter
  81. *
  82. * @var array
  83. */
  84. protected $_validators = array(
  85. 'id' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  86. 'account_id' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  87. 'original_id' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  88. 'original_part_id' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  89. 'messageuid' => array(Zend_Filter_Input::ALLOW_EMPTY => false),
  90. 'folder_id' => array(Zend_Filter_Input::ALLOW_EMPTY => false),
  91. 'subject' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  92. 'from_email' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  93. 'from_name' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  94. 'sender' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  95. 'to' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  96. 'cc' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  97. 'bcc' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  98. 'received' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  99. 'sent' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  100. 'size' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  101. 'flags' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  102. 'timestamp' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  103. 'body' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  104. 'structure' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  105. 'text_partid' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  106. 'html_partid' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  107. 'has_attachment' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  108. 'headers' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  109. 'content_type' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  110. 'body_content_type' => array(
  111. Zend_Filter_Input::ALLOW_EMPTY => true,
  112. Zend_Filter_Input::DEFAULT_VALUE => self::CONTENT_TYPE_PLAIN,
  113. 'InArray' => array(self::CONTENT_TYPE_HTML, self::CONTENT_TYPE_PLAIN)
  114. ),
  115. 'attachments' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  116. // save email as contact note
  117. 'note' => array(Zend_Filter_Input::ALLOW_EMPTY => true, Zend_Filter_Input::DEFAULT_VALUE => 0),
  118. // Felamimail_Message object
  119. 'message' => array(Zend_Filter_Input::ALLOW_EMPTY => true),
  120. );
  121. /**
  122. * name of fields containing datetime or or an array of datetime information
  123. *
  124. * @var array list of datetime fields
  125. */
  126. protected $_datetimeFields = array(
  127. 'timestamp',
  128. 'received',
  129. 'sent',
  130. );
  131. /**
  132. * check if message has \SEEN flag
  133. *
  134. * @return boolean
  135. */
  136. public function hasSeenFlag()
  137. {
  138. return (is_array($this->flags) && in_array(Zend_Mail_Storage::FLAG_SEEN, $this->flags));
  139. }
  140. /**
  141. * parse headers and set 'date', 'from', 'to', 'cc', 'bcc', 'subject', 'sender' fields
  142. *
  143. * @param array $_headers
  144. * @return void
  145. */
  146. public function parseHeaders(array $_headers)
  147. {
  148. // remove duplicate headers (which can't be set twice in real life)
  149. foreach (array('date', 'from', 'to', 'cc', 'bcc', 'subject', 'sender') as $field) {
  150. if (isset($_headers[$field]) && is_array($_headers[$field])) {
  151. $_headers[$field] = $_headers[$field][0];
  152. }
  153. }
  154. $this->subject = (isset($_headers['subject'])) ? Felamimail_Message::convertText($_headers['subject']) : null;
  155. if (array_key_exists('date', $_headers)) {
  156. $this->sent = Felamimail_Message::convertDate($_headers['date']);
  157. } elseif (array_key_exists('resent-date', $_headers)) {
  158. $this->sent = Felamimail_Message::convertDate($_headers['resent-date']);
  159. }
  160. $punycodeConverter = Felamimail_Controller_Message::getInstance()->getPunycodeConverter();
  161. foreach (array('to', 'cc', 'bcc', 'from', 'sender') as $field) {
  162. if (isset($_headers[$field])) {
  163. $value = Felamimail_Message::convertAddresses($_headers[$field], $punycodeConverter);
  164. switch($field) {
  165. case 'from':
  166. $this->from_email = (isset($value[0]) && array_key_exists('email', $value[0])) ? $value[0]['email'] : '';
  167. $this->from_name = (isset($value[0]) && array_key_exists('name', $value[0]) && ! empty($value[0]['name'])) ? $value[0]['name'] : $this->from_email;
  168. break;
  169. case 'sender':
  170. $this->sender = (isset($value[0]) && array_key_exists('email', $value[0])) ? '<' . $value[0]['email'] . '>' : '';
  171. if ((isset($value[0]) && array_key_exists('name', $value[0]) && ! empty($value[0]['name']))) {
  172. $this->sender = '"' . $value[0]['name'] . '" ' . $this->sender;
  173. }
  174. break;
  175. default:
  176. $this->$field = $value;
  177. }
  178. }
  179. }
  180. }
  181. /**
  182. * parse message structure to get content types
  183. *
  184. * @param array $_structure
  185. * @return void
  186. */
  187. public function parseStructure($_structure = NULL)
  188. {
  189. if ($_structure !== NULL) {
  190. $this->structure = $_structure;
  191. }
  192. $this->content_type = isset($this->structure['contentType']) ? $this->structure['contentType'] : Zend_Mime::TYPE_TEXT;
  193. $this->_setBodyContentType();
  194. }
  195. /**
  196. * parse parts to set body content type
  197. */
  198. protected function _setBodyContentType()
  199. {
  200. if (array_key_exists('parts', $this->structure)) {
  201. $bodyContentTypes = $this->_getBodyContentTypes($this->structure['parts']);
  202. // HTML > plain
  203. $this->body_content_type = (in_array(self::CONTENT_TYPE_HTML, $bodyContentTypes)) ? self::CONTENT_TYPE_HTML : self::CONTENT_TYPE_PLAIN;
  204. } else {
  205. $this->body_content_type = $this->content_type;
  206. }
  207. if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Set body content type to ' . $this->body_content_type);
  208. }
  209. /**
  210. * get all content types of mail body
  211. *
  212. * @param array $_parts
  213. * @return array
  214. */
  215. protected function _getBodyContentTypes($_parts)
  216. {
  217. $_bodyContentTypes = array();
  218. foreach ($_parts as $part) {
  219. if (is_array($part) && array_key_exists('contentType', $part)) {
  220. if (in_array($part['contentType'], array(self::CONTENT_TYPE_HTML, self::CONTENT_TYPE_PLAIN)) && ! $this->_partIsAttachment($part)) {
  221. $_bodyContentTypes[] = $part['contentType'];
  222. } else if ($part['contentType'] == self::CONTENT_TYPE_MULTIPART && array_key_exists('parts', $part)) {
  223. $_bodyContentTypes = array_merge($_bodyContentTypes, $this->_getBodyContentTypes($part['parts']));
  224. }
  225. }
  226. }
  227. return $_bodyContentTypes;
  228. }
  229. /**
  230. * get message part structure
  231. *
  232. * @param string $_partId the part id to search for
  233. * @param boolean $_useMessageStructure if you want to get only the messageStructure part
  234. * @return array
  235. */
  236. public function getPartStructure($_partId, $_useMessageStructure = TRUE)
  237. {
  238. $result = NULL;
  239. if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
  240. . ' Getting structure for part ' . $_partId . ' / complete structure: ' . print_r($this->structure, TRUE));
  241. if ($_partId == null) {
  242. // maybe we want no part at all => just return the whole structure
  243. $result = $this->structure;
  244. } else if ($this->structure['partId'] == $_partId) {
  245. // maybe we want the first part => just return the whole structure
  246. $result = $this->structure;
  247. } else {
  248. // iterate structure to find the correct part
  249. $iterator = new RecursiveIteratorIterator(
  250. new RecursiveArrayIterator($this->structure),
  251. RecursiveIteratorIterator::SELF_FIRST
  252. );
  253. foreach ($iterator as $key => $value) {
  254. if ($key == $_partId && is_array($value) && array_key_exists('partId', $value)) {
  255. $result = ($_useMessageStructure && is_array($value) && array_key_exists('messageStructure', $value)) ? $value['messageStructure'] : $value;
  256. }
  257. }
  258. }
  259. if ($result === NULL) {
  260. throw new Felamimail_Exception("Structure for partId $_partId not found!");
  261. }
  262. return $result;
  263. }
  264. /**
  265. * get body parts
  266. *
  267. * @param array $_structure
  268. * @param string $_preferedMimeType
  269. * @return array
  270. */
  271. public function getBodyParts($_structure = NULL, $_preferedMimeType = Zend_Mime::TYPE_HTML)
  272. {
  273. $bodyParts = array();
  274. $structure = ($_structure !== NULL) ? $_structure : $this->structure;
  275. if (! is_array($structure)) {
  276. Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Structure should be an array (' . $structure . ')');
  277. return $bodyParts;
  278. }
  279. if (array_key_exists('parts', $structure)) {
  280. $bodyParts = $bodyParts + $this->_parseMultipart($structure, $_preferedMimeType);
  281. } else {
  282. $bodyParts = $bodyParts + $this->_parseSinglePart($structure);
  283. }
  284. return $bodyParts;
  285. }
  286. /**
  287. * parse single part message
  288. *
  289. * @param array $_structure
  290. * @return array
  291. */
  292. protected function _parseSinglePart(array $_structure)
  293. {
  294. $result = array();
  295. if (! array_key_exists('type', $_structure) || $_structure['type'] != 'text') {
  296. if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Structure has no type key or type != text: ' . print_r($_structure, TRUE));
  297. return $result;
  298. }
  299. if ($this->_partIsAttachment($_structure)) {
  300. if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' Part is attachment: ' . $_structure['disposition']);
  301. return $result;
  302. }
  303. $partId = !empty($_structure['partId']) ? $_structure['partId'] : 1;
  304. $result[$partId] = $_structure;
  305. return $result;
  306. }
  307. /**
  308. * checks if part is attachment
  309. *
  310. * @param array $_structure
  311. * @return boolean
  312. */
  313. protected function _partIsAttachment(array $_structure)
  314. {
  315. return (
  316. isset($_structure['disposition']['type']) &&
  317. ($_structure['disposition']['type'] == Zend_Mime::DISPOSITION_ATTACHMENT ||
  318. // treat as attachment if structure contains parameters
  319. ($_structure['disposition']['type'] == Zend_Mime::DISPOSITION_INLINE && array_key_exists("parameters", $_structure['disposition'])
  320. )
  321. ));
  322. }
  323. /**
  324. * parse multipart message
  325. *
  326. * @param array $_structure
  327. * @param string $_preferedMimeType
  328. * @return array
  329. */
  330. protected function _parseMultipart(array $_structure, $_preferedMimeType = Zend_Mime::TYPE_HTML)
  331. {
  332. $result = array();
  333. //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_structure, TRUE));
  334. if ($_structure['subType'] == 'alternative') {
  335. $alternativeType = $_preferedMimeType == Zend_Mime::TYPE_HTML ? Zend_Mime::TYPE_TEXT : Zend_Mime::TYPE_HTML;
  336. foreach ($_structure['parts'] as $part) {
  337. $foundParts[$part['contentType']] = $part['partId'];
  338. }
  339. if (array_key_exists($_preferedMimeType, $foundParts)) {
  340. $result[$foundParts[$_preferedMimeType]] = $_structure['parts'][$foundParts[$_preferedMimeType]];
  341. } elseif (array_key_exists($alternativeType, $foundParts)) {
  342. $result[$foundParts[$alternativeType]] = $_structure['parts'][$foundParts[$alternativeType]];
  343. }
  344. } else {
  345. foreach ($_structure['parts'] as $part) {
  346. $result = $result + $this->getBodyParts($part, $_preferedMimeType);
  347. }
  348. }
  349. return $result;
  350. }
  351. /**
  352. * parse structure to get text_partid and html_partid
  353. */
  354. public function parseBodyParts()
  355. {
  356. $bodyParts = $this->_getBodyPartIds($this->structure);
  357. if (isset($bodyParts['text'])) {
  358. $this->text_partid = $bodyParts['text'];
  359. }
  360. if (isset($bodyParts['html'])) {
  361. $this->html_partid = $bodyParts['html'];
  362. }
  363. }
  364. /**
  365. * get body part ids
  366. *
  367. * @param array $_structure
  368. * @return array
  369. */
  370. protected function _getBodyPartIds(array $_structure)
  371. {
  372. $result = array();
  373. if ($_structure['type'] == 'text') {
  374. $result = array_merge($result, $this->_getTextPartId($_structure));
  375. } elseif($_structure['type'] == 'multipart') {
  376. $result = array_merge($result, $this->_getMultipartIds($_structure));
  377. }
  378. return $result;
  379. }
  380. /**
  381. * get multipart ids
  382. *
  383. * @param array $_structure
  384. * @return array
  385. */
  386. protected function _getMultipartIds(array $_structure)
  387. {
  388. $result = array();
  389. if ($_structure['subType'] == 'alternative' || $_structure['subType'] == 'mixed' ||
  390. $_structure['subType'] == 'signed' || $_structure['subType'] == 'related') {
  391. foreach ($_structure['parts'] as $part) {
  392. $result = array_merge($result, $this->_getBodyPartIds($part));
  393. }
  394. } else {
  395. // ignore other types for now
  396. #var_dump($_structure);
  397. #throw new Exception('unsupported multipart');
  398. }
  399. return $result;
  400. }
  401. /**
  402. * get text part id
  403. *
  404. * @param array $_structure
  405. * @return array
  406. */
  407. protected function _getTextPartId(array $_structure)
  408. {
  409. $result = array();
  410. if ($this->_partIsAttachment($_structure)) {
  411. return $result;
  412. }
  413. if ($_structure['subType'] == 'plain') {
  414. $result['text'] = !empty($_structure['partId']) ? $_structure['partId'] : 1;
  415. } elseif($_structure['subType'] == 'html') {
  416. $result['html'] = !empty($_structure['partId']) ? $_structure['partId'] : 1;
  417. }
  418. return $result;
  419. }
  420. /**
  421. * fills a record from json data
  422. *
  423. * @param array $recordData
  424. *
  425. * @todo get/detect delimiter from row? could be ';' or ','
  426. * @todo add recipient names
  427. */
  428. protected function _setFromJson(array &$recordData)
  429. {
  430. // explode email addresses if multiple
  431. $recipientType = array('to', 'cc', 'bcc');
  432. $delimiter = ';';
  433. foreach ($recipientType as $field) {
  434. if (!empty($recordData[$field])) {
  435. $recipients = array();
  436. foreach ($recordData[$field] as $addresses) {
  437. if (substr_count($addresses, '@') > 1) {
  438. $recipients = array_merge($recipients, explode($delimiter, $addresses));
  439. } else {
  440. // single recipient
  441. $recipients[] = $addresses;
  442. }
  443. }
  444. foreach ($recipients as $key => &$recipient) {
  445. // get address
  446. // @todo get name here
  447. //<*([a-zA-Z@_\-0-9\.]+)>*/
  448. if (preg_match(self::EMAIL_ADDRESS_REGEXP, $recipient, $matches) > 0) {
  449. $recipient = $matches[1];
  450. }
  451. if (empty($recipient)) {
  452. unset($recipients[$key]);
  453. }
  454. }
  455. //if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($recipients, true));
  456. $recordData[$field] = array_unique($recipients);
  457. }
  458. }
  459. }
  460. /**
  461. * get body as plain text with replaced blockquotes, stripped tags and replaced <br>s
  462. * -> use DOM extension
  463. *
  464. * @return string
  465. */
  466. public function getPlainTextBody()
  467. {
  468. $result = '';
  469. $dom = new DOMDocument('1.0', 'utf-8');
  470. // use a hack to make sure html is loaded as utf8 (@see http://php.net/manual/en/domdocument.loadhtml.php#95251)
  471. $dom->loadHTML('<?xml encoding="UTF-8">' . $this->body);
  472. $bodyElements = $dom->getElementsByTagName('body');
  473. if ($bodyElements->length > 0) {
  474. $result = $this->_addQuotesAndStripTags($bodyElements->item(0));
  475. $result = html_entity_decode($result, ENT_COMPAT, 'UTF-8');
  476. } else {
  477. if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' No body element found.');
  478. }
  479. return $result;
  480. }
  481. /**
  482. * convert blockquotes to quotes ("> ") and strip tags
  483. *
  484. * this function uses tidy or DOM to recursivly walk the dom tree of the html mail
  485. * @see http://php.net/manual/de/tidy.root.php
  486. * @see http://php.net/manual/en/book.dom.php
  487. *
  488. * @param tidyNode|DOMNode $_node
  489. * @param integer $_quoteIndent
  490. * @return string
  491. *
  492. * @todo we can transform more tags here, i.e. the <strong>BOLDTEXT</strong> tag could be replaced with *BOLDTEXT*
  493. * @todo think about removing the tidy code
  494. */
  495. protected function _addQuotesAndStripTags($_node, $_quoteIndent = 0) {
  496. $result = '';
  497. $hasChildren = ($_node instanceof DOMNode) ? $_node->hasChildNodes() : $_node->hasChildren();
  498. $nameProperty = ($_node instanceof DOMNode) ? 'nodeName' : 'name';
  499. $valueProperty = ($_node instanceof DOMNode) ? 'nodeValue' : 'value';
  500. if ($hasChildren) {
  501. $lastChild = NULL;
  502. $children = ($_node instanceof DOMNode) ? $_node->childNodes : $_node->child;
  503. foreach ($children as $child) {
  504. $isTextLeaf = ($child instanceof DOMNode) ? $child->{$nameProperty} == '#text' : ! $child->{$nameProperty};
  505. if ($isTextLeaf) {
  506. // leaf -> add quotes and append to content string
  507. if ($_quoteIndent > 0) {
  508. $result .= str_repeat(self::QUOTE, $_quoteIndent) . $child->{$valueProperty};
  509. // add newline if parent is div
  510. if ($_node->{$nameProperty} == 'div') {
  511. $result .= "\n" . str_repeat(self::QUOTE, $_quoteIndent);
  512. }
  513. } else {
  514. // add newline if parent is div
  515. if ($_node->{$nameProperty} == 'div') {
  516. $result .= "\n";
  517. }
  518. $result .= $child->{$valueProperty};
  519. }
  520. } else if ($child->{$nameProperty} == 'blockquote') {
  521. // opening blockquote
  522. $_quoteIndent++;
  523. } else if ($child->{$nameProperty} == 'br') {
  524. // reset quoted state on newline
  525. if ($lastChild !== NULL && $lastChild->{$nameProperty} == 'br') {
  526. // add quotes to repeating newlines
  527. $result .= str_repeat(self::QUOTE, $_quoteIndent);
  528. }
  529. $result .= "\n";
  530. }
  531. $result .= $this->_addQuotesAndStripTags($child, $_quoteIndent);
  532. if ($child->{$nameProperty} == 'blockquote') {
  533. // closing blockquote
  534. $_quoteIndent--;
  535. // add newline after last closing blockquote
  536. if ($_quoteIndent == 0) {
  537. $result .= "\n";
  538. }
  539. }
  540. $lastChild = $child;
  541. }
  542. }
  543. return $result;
  544. }
  545. }