PageRenderTime 81ms CodeModel.GetById 49ms RepoModel.GetById 0ms app.codeStats 0ms

/tine20/Felamimail/Model/Message.php

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