PageRenderTime 59ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

/plugins/emails/imap_source.php

https://github.com/zpartakov/pmCake
PHP | 949 lines | 615 code | 110 blank | 224 comment | 94 complexity | dc3fff3bb988f1e383f1f8c193691d8c MD5 | raw file
Possible License(s): LGPL-3.0, GPL-3.0
  1. <?php
  2. /**
  3. * Get emails in your app with cake like finds.
  4. *
  5. * Copyright (c) 2010 Carl Sutton ( dogmatic69 )
  6. * Copyright (c) 2011 Kevin van Zonneveld ( kvz )
  7. *
  8. * @filesource
  9. * @copyright Copyright (c) 2010 Carl Sutton ( dogmatic69 )
  10. * @copyright Copyright (c) 2011 Kevin van Zonneveld ( kvz )
  11. * @link http://www.infinitas-cms.org
  12. * @link https://github.com/kvz/cakephp-emails-plugin
  13. * @package libs
  14. * @subpackage libs.models.datasources.reader
  15. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  16. * @since 0.9a
  17. *
  18. * @author dogmatic69
  19. * @author kvz
  20. *
  21. * Modifications since 0.8a (when code was stripped from Infinitas):
  22. * https://github.com/kvz/cakephp-emails-plugin/compare/10767bee59dd425ced5b97ae9604acf7f3c0d27a...master
  23. *
  24. * Licensed under The MIT License
  25. * Redistributions of files must retain the above copyright notice.
  26. */
  27. class ImapSource extends DataSource {
  28. protected $_isConnected = false;
  29. protected $_connectionString = null;
  30. protected $_connectionType = '';
  31. protected $_defaultConfigs = array(
  32. 'global' => array(
  33. 'username' => false,
  34. 'password' => false,
  35. 'email' => false,
  36. 'server' => 'localhost',
  37. 'type' => 'imap',
  38. 'ssl' => false,
  39. 'retry' => 3,
  40. 'error_handler' => 'php',
  41. 'auto_mark_as' => array('seen'),
  42. ),
  43. 'imap' => array(
  44. 'port' => 143,
  45. ),
  46. 'pop3' => array(
  47. 'port' => 110,
  48. ),
  49. );
  50. public $marks = array(
  51. '\Seen',
  52. '\Answered',
  53. '\Flagged',
  54. '\Deleted',
  55. '\Draft',
  56. );
  57. public $config = array();
  58. public $driver = null;
  59. /**
  60. * Default array of field list for imap mailbox.
  61. *
  62. * @var array
  63. */
  64. protected $_schema = array(
  65. 'id' => array('type' => 'integer', 'default' => NULL, 'length' => 15, 'key' => 'primary',),
  66. 'message_id' => array('type' => 'string', 'default' => NULL, 'length' => 255,),
  67. 'email_number' => array('type' => 'integer', 'default' => NULL, 'length' => 15,),
  68. 'to' => array('type' => 'string', 'default' => NULL, 'length' => 255,),
  69. 'to_name' => array('type' => 'string', 'default' => NULL, 'length' => 255,),
  70. 'from' => array('type' => 'string', 'default' => NULL, 'length' => 255,),
  71. 'from_name' => array('type' => 'string', 'default' => NULL, 'length' => 255,),
  72. 'reply_to' => array('type' => 'string', 'default' => NULL, 'length' => 255,),
  73. 'reply_to_name' => array('type' => 'string', 'default' => NULL, 'length' => 255,),
  74. 'sender' => array('type' => 'string', 'default' => NULL, 'length' => 255,),
  75. 'sender_name' => array('type' => 'string', 'default' => NULL, 'length' => 255,),
  76. 'subject' => array('type' => 'string', 'default' => NULL, 'length' => 255,),
  77. 'slug' => array('type' => 'string', 'default' => NULL, 'length' => 255,),
  78. 'body' => array('type' => 'text', 'default' => NULL,),
  79. 'plainmsg' => array('type' => 'text', 'default' => NULL,),
  80. 'size' => array('type' => 'string', 'default' => NULL, 'length' => 255,),
  81. 'recent' => array('type' => 'boolean', 'default' => NULL, 'length' => 1,),
  82. 'seen' => array('type' => 'boolean', 'default' => NULL, 'length' => 1,),
  83. 'flagged' => array('type' => 'boolean', 'default' => NULL, 'length' => 1,),
  84. 'answered' => array('type' => 'boolean', 'default' => NULL, 'length' => 1,),
  85. 'draft' => array('type' => 'boolean', 'default' => NULL, 'length' => 1,),
  86. 'deleted' => array('type' => 'boolean', 'default' => NULL, 'length' => 1,),
  87. 'thread_count' => array('type' => 'integer', 'default' => NULL, 'length' => 15, 'key' => 'primary',),
  88. 'attachments' => array('type' => 'text', 'default' => NULL,),
  89. 'in_reply_to' => array('type' => 'string', 'default' => NULL, 'length' => 255,),
  90. 'reference' => array('type' => 'string', 'default' => NULL, 'length' => 255,),
  91. 'new' => array('type' => 'boolean', 'default' => NULL, 'length' => 1,),
  92. 'created' => array('type' => 'datetime', 'default' => NULL,),
  93. );
  94. public $columns = array(
  95. 'primary_key' => array('name' => 'NOT NULL AUTO_INCREMENT'),
  96. 'string' => array('name' => 'varchar', 'limit' => '255'),
  97. 'text' => array('name' => 'text'),
  98. 'integer' => array('name' => 'int', 'limit' => '11', 'formatter' => 'intval'),
  99. 'float' => array('name' => 'float', 'formatter' => 'floatval'),
  100. 'datetime' => array('name' => 'datetime', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'),
  101. 'timestamp' => array('name' => 'timestamp', 'format' => 'Y-m-d H:i:s', 'formatter' => 'date'),
  102. 'time' => array('name' => 'time', 'format' => 'H:i:s', 'formatter' => 'date'),
  103. 'date' => array('name' => 'date', 'format' => 'Y-m-d', 'formatter' => 'date'),
  104. 'binary' => array('name' => 'blob'),
  105. 'boolean' => array('name' => 'tinyint', 'limit' => '1')
  106. );
  107. public $dataTypes = array(
  108. 0 => 'text',
  109. 1 => 'multipart',
  110. 2 => 'message',
  111. 3 => 'application',
  112. 4 => 'audio',
  113. 5 => 'image',
  114. 6 => 'video',
  115. 7 => 'other',
  116. );
  117. public $encodingTypes = array(
  118. 0 => '7bit',
  119. 1 => '8bit',
  120. 2 => 'binary',
  121. 3 => 'base64',
  122. 4 => 'quoted-printable',
  123. 5 => 'other',
  124. );
  125. /**
  126. * __construct()
  127. *
  128. * @param mixed $config
  129. */
  130. public function __construct ($config) {
  131. parent::__construct($config);
  132. if (!isset($config['type'])) {
  133. $type = $this->_defaultConfigs['global']['type'];
  134. } else {
  135. $type = $config['type'];
  136. }
  137. $newConfig = array_merge($this->_defaultConfigs['global'], $this->_defaultConfigs[$type], $this->config);
  138. $newConfig['email'] = !empty($newConfig['email']) ? $newConfig['email'] : $newConfig['username'];
  139. $this->config = $newConfig;
  140. }
  141. /**
  142. * Expunge messages marked for deletion
  143. */
  144. public function __destruct () {
  145. if ($this->_isConnected && $this->Stream) {
  146. $this->_isConnected = false;
  147. // If set to CL_EXPUNGE, the function will silently expunge the
  148. // mailbox before closing, removing all messages marked for deletion.
  149. // You can achieve the same thing by using imap_expunge()
  150. imap_close($this->Stream, CL_EXPUNGE);
  151. }
  152. }
  153. /**
  154. * describe the data
  155. *
  156. * @param mixed $Model
  157. * @return array the shcema of the model
  158. */
  159. public function describe ($Model) {
  160. return $this->_schema;
  161. }
  162. /**
  163. * listSources
  164. *
  165. * list the sources???
  166. *
  167. * @return array sources
  168. */
  169. public function listSources () {
  170. return array('listSources');
  171. }
  172. /**
  173. * Returns a query condition, or null if it wasn't found
  174. *
  175. * @param object $Model
  176. * @param array $query
  177. * @param string $field
  178. *
  179. * @return mixed or null
  180. */
  181. protected function _cond ($Model, $query, $field) {
  182. $keys = array(
  183. '`' . $Model->alias . '`.`' . $field . '`',
  184. $Model->alias . '.' . $field,
  185. $field,
  186. );
  187. foreach ($keys as $key) {
  188. if (array_key_exists($key, @$query['conditions'])) {
  189. return $query['conditions'][$key];
  190. }
  191. }
  192. return null;
  193. }
  194. /**
  195. * Returns ids from searchCriteria or false if there's other criteria involved
  196. *
  197. * @param array $searchCriteria
  198. *
  199. * @return false or array
  200. */
  201. protected function _uidsByCriteria ($searchCriteria) {
  202. if (is_numeric($searchCriteria) || Set::numeric($searchCriteria)) {
  203. // We already know the id, or list of ids
  204. $results = $searchCriteria;
  205. if (!is_array($results)) {
  206. $results = array($results);
  207. }
  208. return $results;
  209. }
  210. return false;
  211. }
  212. /**
  213. * Tranform search criteria from CakePHP -> Imap
  214. * Does AND, not OR
  215. *
  216. * Supported:
  217. * FROM "string" - match messages with "string" in the From: field
  218. *
  219. * ANSWERED - match messages with the \\ANSWERED flag set
  220. * UNANSWERED - match messages that have not been answered
  221. *
  222. * SEEN - match messages that have been read (the \\SEEN flag is set)
  223. * UNSEEN - match messages which have not been read yet
  224. *
  225. * DELETED - match deleted messages
  226. * UNDELETED - match messages that are not deleted
  227. *
  228. * FLAGGED - match messages with the \\FLAGGED (sometimes referred to as Important or Urgent) flag set
  229. * UNFLAGGED - match messages that are not flagged
  230. *
  231. * RECENT - match messages with the \\RECENT flag set
  232. *
  233. * @todo:
  234. * A string, delimited by spaces, in which the following keywords are allowed. Any multi-word arguments (e.g. FROM "joey smith") must be quoted.
  235. * ALL - return all messages matching the rest of the criteria
  236. * BCC "string" - match messages with "string" in the Bcc: field
  237. * BEFORE "date" - match messages with Date: before "date"
  238. * BODY "string" - match messages with "string" in the body of the message
  239. * CC "string" - match messages with "string" in the Cc: field
  240. * KEYWORD "string" - match messages with "string" as a keyword
  241. * NEW - match new messages
  242. * OLD - match old messages
  243. * ON "date" - match messages with Date: matching "date"
  244. * SINCE "date" - match messages with Date: after "date"
  245. * SUBJECT "string" - match messages with "string" in the Subject:
  246. * TEXT "string" - match messages with text "string"
  247. * TO "string" - match messages with "string" in the To:
  248. * UNKEYWORD "string" - match messages that do not have the keyword "string"
  249. *
  250. * @param object $Model
  251. * @param array $query
  252. *
  253. * @return array
  254. */
  255. protected function _makeSearch ($Model, $query) {
  256. $searchCriteria = array();
  257. if (!@$query['conditions']) {
  258. $query['conditions'] = array();
  259. }
  260. // Special case. When somebody specifies primaryKey(s),
  261. // We don't have to do an actual search
  262. if (($id = $this->_cond($Model, $query, $Model->primaryKey))) {
  263. return $this->_toUid($id);
  264. }
  265. // Flag search parameters
  266. $flags = array(
  267. 'recent',
  268. 'seen',
  269. 'flagged',
  270. 'answered',
  271. 'draft',
  272. 'deleted',
  273. );
  274. foreach ($flags as $flag) {
  275. if (null !== ($val = $this->_cond($Model, $query, $flag))) {
  276. $upper = strtoupper($flag);
  277. $unupper = 'UN' . $upper;
  278. if (!$val && ($flag === 'recent')) {
  279. // There is no UNRECENT :/
  280. // Just don't set the condition
  281. continue;
  282. }
  283. $searchCriteria[] = $val ? $upper : $unupper;
  284. }
  285. }
  286. // String search parameters
  287. if (($val = $this->_cond($Model, $query, 'from'))) {
  288. $searchCriteria[] = 'FROM "' . $val . '"';
  289. }
  290. return $searchCriteria;
  291. }
  292. /**
  293. * Tranform order criteria from CakePHP -> Imap
  294. *
  295. * For now always sorts on date descending.
  296. * @todo: Support the following sort parameters:
  297. * SORTDATE - message Date
  298. * SORTARRIVAL - arrival date
  299. * SORTFROM - mailbox in first From address
  300. * SORTSUBJECT - message subject
  301. * SORTTO - mailbox in first To address
  302. * SORTCC - mailbox in first cc address
  303. * SORTSIZE - size of message in octets
  304. *
  305. * @param object $Model
  306. * @param array $query
  307. *
  308. * @return array
  309. */
  310. protected function _makeOrder($Model, $query) {
  311. // Tranform order criteria
  312. $orderReverse = 1;
  313. $orderCriteria = SORTDATE;
  314. return array($orderReverse, $orderCriteria);
  315. }
  316. public function delete ($Model, $conditions = null) {
  317. $query = compact('conditions');
  318. $searchCriteria = $this->_makeSearch($Model, $query);
  319. $uids = $this->_uidsByCriteria($searchCriteria);
  320. if ($uids === false) {
  321. $uids = $Model->find('list', $query);
  322. }
  323. // Nothing was found
  324. if (empty($uids)) {
  325. return false;
  326. }
  327. $success = true;
  328. foreach ($uids as $uid) {
  329. if (!imap_delete($this->Stream, $uid, FT_UID)) {
  330. $this->err($Model, 'Unable to delete email with uid: %s', $uid);
  331. $success = false;
  332. }
  333. }
  334. return $success;
  335. }
  336. /**
  337. * read data
  338. *
  339. * this is the main method that reads data from the datasource and
  340. * formats it according to the request from the model.
  341. *
  342. * @param mixed $model the model that is requesting data
  343. * @param mixed $query the qurey that was sent
  344. *
  345. * @return the data requested by the model
  346. */
  347. public function read ($Model, $query) {
  348. if (!$this->connect($Model, $query)) {
  349. return $this->err($Model, 'Cannot connect to server');
  350. }
  351. $searchCriteria = $this->_makeSearch($Model, $query);
  352. $uids = $this->_uidsByCriteria($searchCriteria);
  353. if ($uids === false) {
  354. // Perform Search & Order. Returns list of ids
  355. list($orderReverse, $orderCriteria) = $this->_makeOrder($Model, $query);
  356. $uids = imap_sort(
  357. $this->Stream,
  358. $orderCriteria,
  359. $orderReverse,
  360. SE_UID,
  361. join(' ', $searchCriteria)
  362. );
  363. }
  364. // Nothing was found
  365. if (empty($uids)) {
  366. return array();
  367. }
  368. // Trim resulting ids based on pagination / limitation
  369. if (@$query['start'] && @$query['end']) {
  370. $uids = array_slice($uids, @$query['start'], @$query['end'] - @$query['start']);
  371. } elseif (@$query['limit']) {
  372. $uids = array_slice($uids, @$query['start'] ? @$query['start'] : 0, @$query['limit']);
  373. } elseif ($Model->findQueryType === 'first') {
  374. $uids = array_slice($uids, 0, 1);
  375. }
  376. // Format output depending on findQueryType
  377. if ($Model->findQueryType === 'list') {
  378. return $uids;
  379. } else if ($Model->findQueryType === 'count') {
  380. return array(
  381. array(
  382. $Model->alias => array(
  383. 'count' => count($uids),
  384. ),
  385. ),
  386. );
  387. } else if ($Model->findQueryType === 'all' || $Model->findQueryType === 'first') {
  388. $attachments = @$query['recursive'] > 0;
  389. $mails = array();
  390. foreach ($uids as $uid) {
  391. if (($mail = $this->_getFormattedMail($Model, $uid, $attachments))) {
  392. $mails[] = $mail;
  393. }
  394. }
  395. return $mails;
  396. }
  397. return $this->err(
  398. $Model,
  399. 'Unknown find type %s for query %s',
  400. $Model->findQueryType,
  401. $query
  402. );
  403. }
  404. /**
  405. * no clue
  406. * @param <type> $Model
  407. * @param <type> $func
  408. * @param <type> $params
  409. * @return <type>
  410. */
  411. public function calculate ($Model, $func, $params = array()) {
  412. $params = (array) $params;
  413. switch (strtolower($func)) {
  414. case 'count':
  415. return 'count';
  416. break;
  417. }
  418. }
  419. /**
  420. * connect to the mail server
  421. */
  422. public function connect ($Model, $query) {
  423. if ($this->_isConnected) {
  424. return true;
  425. }
  426. $this->_connectionType = $this->config['type'];
  427. switch ($this->config['type']) {
  428. case 'imap':
  429. $this->_connectionString = sprintf(
  430. '{%s:%s%s%s}',
  431. $this->config['server'],
  432. $this->config['port'],
  433. @$this->config['ssl'] ? '/ssl' : '',
  434. @$this->config['connect'] ? '/' . @$this->config['connect'] : '',
  435. @$this->config['mailbox'] ? @$this->config['mailbox'] : ''
  436. );
  437. break;
  438. case 'pop3':
  439. $this->_connectionString = sprintf(
  440. '{%s:%s/pop3%s%s}',
  441. $this->config['server'],
  442. $this->config['port'],
  443. @$this->config['ssl'] ? '/ssl' : '',
  444. @$this->config['connect'] ? '/' . @$this->config['connect'] : ''
  445. );
  446. break;
  447. }
  448. try {
  449. $this->thread = null;
  450. $retries = 0;
  451. while (($retries++) < $this->config['retry'] && !$this->thread) {
  452. $this->Stream = imap_open($this->_connectionString, $this->config['username'], $this->config['password']);
  453. $this->thread = @imap_thread($this->Stream);
  454. }
  455. if (!$this->thread) {
  456. return $this->err(
  457. $Model,
  458. 'Unable to get imap_thread after %s retries. %s',
  459. $retries,
  460. imap_last_error()
  461. );
  462. }
  463. } catch (Exception $Exception) {
  464. return $this->err(
  465. $Model,
  466. 'Unable to get imap_thread after %s retries. %s',
  467. $retries,
  468. $Exception->getMessage() . ' ' . imap_last_error()
  469. );
  470. }
  471. return $this->_isConnected = true;
  472. }
  473. public function name ($data) {
  474. return $data;
  475. }
  476. public function sensible ($arguments) {
  477. if (is_object($arguments)) {
  478. return get_class($arguments);
  479. }
  480. if (!is_array($arguments)) {
  481. if (!is_numeric($arguments) && !is_bool($arguments)) {
  482. $arguments = "'".$arguments."'";
  483. }
  484. return $arguments;
  485. }
  486. $arr = array();
  487. foreach($arguments as $key=>$val) {
  488. if (is_array($val)) {
  489. $val = json_encode($val);
  490. } elseif (!is_numeric($val) && !is_bool($val)) {
  491. $val = "'".$val."'";
  492. }
  493. if (strlen($val) > 33) {
  494. $val = substr($val, 0, 30) . '...';
  495. }
  496. $arr[] = $key.': '.$val;
  497. }
  498. return join(', ', $arr);
  499. }
  500. public function err ($Model, $format, $arg1 = null, $arg2 = null, $arg3 = null) {
  501. $arguments = func_get_args();
  502. $Model = array_shift($arguments);
  503. $format = array_shift($arguments);
  504. $str = $format;
  505. if (count($arguments)) {
  506. foreach($arguments as $k => $v) {
  507. $arguments[$k] = $this->sensible($v);
  508. }
  509. $str = vsprintf($str, $arguments);
  510. }
  511. $this->error = $str;
  512. $Model->onError();
  513. if ($this->config['error_handler'] === 'php') {
  514. trigger_error($str, E_USER_ERROR);
  515. }
  516. return false;
  517. }
  518. public function lastError () {
  519. if (($lastError = $this->error)) {
  520. return $lastError;
  521. }
  522. if (($lastError = imap_last_error())) {
  523. $this->error = $lastError;
  524. return $lastError;
  525. }
  526. return false;
  527. }
  528. /**
  529. * Tries to parse mail & name data from Mail object for to, from, etc.
  530. * Gracefully degrades where needed
  531. *
  532. * Type: to, from, sender, reply_to
  533. * Need: box, name, host, address, full
  534. *
  535. * @param object $Mail
  536. * @param string $type
  537. * @param string $need
  538. *
  539. * @return mixed string or array
  540. */
  541. protected function _personId ($Mail, $type = 'to', $need = null) {
  542. if ($type === 'sender' && !isset($Mail->sender)) {
  543. $type = 'from';
  544. }
  545. $info['box'] = '';
  546. if (isset($Mail->{$type}[0]->mailbox)) {
  547. $info['box'] = $Mail->{$type}[0]->mailbox;
  548. }
  549. $info['name'] = $info['box'];
  550. if (isset($Mail->{$type}[0]->personal)) {
  551. $info['name'] = $Mail->{$type}[0]->personal;
  552. }
  553. $info['host'] = '';
  554. if (isset($Mail->{$type}[0]->host)) {
  555. $info['host'] = $Mail->{$type}[0]->host;
  556. }
  557. $info['address'] = '';
  558. if ($info['box'] && $info['host']) {
  559. $info['address'] = $info['box'] . '@' . $info['host'];
  560. }
  561. $info['full'] = $info['address'];
  562. if ($info['name']) {
  563. $info['full'] = sprintf('"%s" <%s>', $info['name'], $info['address']);
  564. }
  565. if ($need !== null) {
  566. return $info[$need];
  567. }
  568. return $info;
  569. }
  570. /**
  571. * get the basic details like sender and reciver with flags like attatchments etc
  572. *
  573. * @param int $uid the number of the message
  574. * @return array empty on error/nothing or array of formatted details
  575. */
  576. protected function _getFormattedMail ($Model, $uid, $fetchAttachments = false) {
  577. // Translate uid to msg_no. Has no decent fail
  578. $msg_number = imap_msgno($this->Stream, $uid);
  579. // A hack to detect if imap_msgno failed, and we're in fact looking at the wrong mail
  580. if ($uid != ($mailuid = imap_uid($this->Stream, $msg_number))) {
  581. pr(compact('Mail'));
  582. return $this->err(
  583. $Model,
  584. 'Mail id mismatch. parameter id: %s vs mail id: %s',
  585. $uid,
  586. $mailuid
  587. );
  588. }
  589. // Get Mail with a property: 'date' or fail
  590. if (!($Mail = imap_headerinfo($this->Stream, $msg_number)) || !property_exists($Mail, 'date')) {
  591. pr(compact('Mail'));
  592. return $this->err(
  593. $Model,
  594. 'Unable to find mail date property in Mail corresponding with uid: %s. Something must be wrong',
  595. $uid
  596. );
  597. }
  598. // Get Mail with a property: 'type' or fail
  599. if (!($flatStructure = $this->_flatStructure($Model, $uid))) {
  600. return $this->err(
  601. $Model,
  602. 'Unable to find structure type property in Mail corresponding with uid: %s. Something must be wrong',
  603. $uid
  604. );
  605. }
  606. $plain = $this->_fetchFirstByMime($flatStructure, 'text/plain');
  607. $html = $this->_fetchFirstByMime($flatStructure, 'text/html');
  608. $return[$Model->alias] = array(
  609. 'id' => $this->_toId($uid),
  610. 'message_id' => $Mail->message_id,
  611. 'email_number' => $Mail->Msgno,
  612. 'to' => $this->_personId($Mail, 'to', 'address'),
  613. 'to_name' => $this->_personId($Mail, 'to', 'name'),
  614. 'from' => $this->_personId($Mail, 'from', 'address'),
  615. 'from_name' => $this->_personId($Mail, 'from', 'name'),
  616. 'reply_to' => $this->_personId($Mail, 'reply_to', 'address'),
  617. 'reply_to_name' => $this->_personId($Mail, 'reply_to', 'name'),
  618. 'sender' => $this->_personId($Mail, 'sender', 'address'),
  619. 'sender_name' => $this->_personId($Mail, 'sender', 'name'),
  620. 'subject' => htmlspecialchars(@$Mail->subject),
  621. 'slug' => Inflector::slug(@$Mail->subject, '-'),
  622. 'header' => @imap_fetchheader($this->Stream, $uid, FT_UID),
  623. 'body' => $html,
  624. 'plainmsg' => $plain ? $plain : $html,
  625. 'size' => @$Mail->Size,
  626. 'recent' => @$Mail->Recent === 'R' ? 1 : 0,
  627. 'seen' => @$Mail->Unseen === 'U' ? 0 : 1,
  628. 'flagged' => @$Mail->Flagged === 'F' ? 1 : 0,
  629. 'answered' => @$Mail->Answered === 'A' ? 1 : 0,
  630. 'draft' => @$Mail->Draft === 'X' ? 1 : 0,
  631. 'deleted' => @$Mail->Deleted === 'D' ? 1 : 0,
  632. 'thread_count' => $this->_getThreadCount($Mail),
  633. 'in_reply_to' => @$Mail->in_reply_to,
  634. 'reference' => @$Mail->references,
  635. 'new' => (int)@$Mail->in_reply_to,
  636. 'created' => date('Y-m-d H:i:s', strtotime($Mail->date)),
  637. );
  638. if ($fetchAttachments) {
  639. $return['Attachment'] = $this->_fetchAttachments($flatStructure, $Model);
  640. }
  641. // Auto mark after read
  642. if (!empty($this->config['auto_mark_as'])) {
  643. $marks = '\\' . join(' \\', $this->config['auto_mark_as']);
  644. if (!imap_setflag_full($this->Stream, $uid, $marks, ST_UID)) {
  645. $this->err($Model, 'Unable to mark email %s as %s', $uid, $marks);
  646. }
  647. }
  648. return $return;
  649. }
  650. protected function _awesomePart($Part, $uid) {
  651. if (!($Part->format = @$this->encodingTypes[$Part->type])) {
  652. $Part->format = $this->encodingTypes[0];
  653. }
  654. if (!($Part->datatype = @$this->dataTypes[$Part->type])) {
  655. $Part->datatype = $this->dataTypes[0];
  656. }
  657. $Part->mimeType = strtolower($Part->datatype . '/' . $Part->subtype);
  658. $Part->is_attachment = false;
  659. $Part->filename = '';
  660. $Part->name = '';
  661. $Part->uid = $uid;
  662. if ($Part->ifdparameters) {
  663. foreach ($Part->dparameters as $Object) {
  664. if (strtolower($Object->attribute) === 'filename') {
  665. #$Part->is_attachment = true;
  666. $Part->filename = $Object->value;
  667. }
  668. }
  669. }
  670. if ($Part->ifparameters) {
  671. foreach ($Part->parameters as $Object) {
  672. if (strtolower($Object->attribute) === 'name') {
  673. #$Part->is_attachment = true;
  674. $Part->name = $Object->value;
  675. }
  676. }
  677. }
  678. if (false !== strpos($Part->path, '.')) {
  679. $Part->is_attachment = true;
  680. }
  681. return $Part;
  682. }
  683. /**
  684. *
  685. * Contains parts of:
  686. * http://p2p.wrox.com/pro-php/8658-fyi-parsing-imap_fetchstructure.html
  687. * http://www.php.net/manual/en/function.imap-fetchstructure.php#86685
  688. *
  689. * @param <type> $uid
  690. * @param <type> $mixed
  691. * @param <type> $Structure
  692. * @param <type> $partnr
  693. *
  694. * @return <type>
  695. */
  696. protected function _flatStructure ($Model, $uid, $Structure = false, $partnr = 1) {
  697. $mainRun = false;
  698. if (!$Structure) {
  699. $mainRun = true;
  700. $Structure = imap_fetchstructure($this->Stream, $uid, FT_UID);
  701. if (!property_exists($Structure, 'type')) {
  702. return $this->err($Model, 'No type in structure');
  703. }
  704. }
  705. $flatParts = array();
  706. if (!empty($Structure->parts)) {
  707. $decimas = explode('.', $partnr);
  708. $decimas[count($decimas)-1] -= 1;
  709. $Structure->path = join('.', $decimas);
  710. } else {
  711. $Structure->path = $partnr;
  712. }
  713. $flatParts[$Structure->path] = $this->_awesomePart($Structure, $uid);
  714. if (!empty($Structure->parts)) {
  715. foreach ($Structure->parts as $n => $Part) {
  716. if ($n >= 1){
  717. $arr_decimas = explode('.', $partnr);
  718. $arr_decimas[count($arr_decimas) - 1] += 1;
  719. $partnr = join('.', $arr_decimas);
  720. }
  721. $Part->path = $partnr;
  722. $flatParts[$Part->path] = $this->_awesomePart($Part, $uid);
  723. if (!empty($Part->parts)) {
  724. if ($Part->type == 1){
  725. $flatParts = Set::merge(
  726. $flatParts,
  727. $this->_flatStructure($Model, $uid, $Part, $partnr.'.'.($n+1))
  728. );
  729. } else {
  730. foreach ($Part->parts as $idx => $Part2){
  731. $flatParts = Set::merge(
  732. $flatParts,
  733. $this->_flatStructure($Model, $uid, $Part2, $partnr.'.'.($idx+1))
  734. );
  735. }
  736. }
  737. }
  738. }
  739. }
  740. // Filter mixed
  741. if ($mainRun) {
  742. foreach ($flatParts as $path => $Part) {
  743. if ($Part->mimeType === 'multipart/mixed') {
  744. unset($flatParts[$path]);
  745. }
  746. if ($Part->mimeType === 'multipart/alternative') {
  747. unset($flatParts[$path]);
  748. }
  749. if ($Part->mimeType === 'multipart/related') {
  750. unset($flatParts[$path]);
  751. }
  752. if ($Part->mimeType === 'message/rfc822') {
  753. unset($flatParts[$path]);
  754. }
  755. }
  756. }
  757. // Flatten more (remove childs)
  758. if ($mainRun) {
  759. foreach ($flatParts as $path => $Part) {
  760. unset($Part->parts);
  761. }
  762. }
  763. return $flatParts;
  764. }
  765. protected function _fetchAttachments ($flatStructure, $Model) {
  766. $attachments = array();
  767. foreach ($flatStructure as $path => $Part) {
  768. if (!$Part->is_attachment) {
  769. continue;
  770. }
  771. $attachments[] = array(
  772. strtolower(Inflector::singularize($Model->alias) . '_id') => $this->_toId($Part->uid),
  773. 'message_id' => $Part->uid,
  774. 'is_attachment' => $Part->is_attachment,
  775. 'filename' => $Part->filename,
  776. 'mime_type' => $Part->mimeType,
  777. 'type' => strtolower($Part->subtype),
  778. 'datatype' => $Part->datatype,
  779. 'format' => $Part->format,
  780. 'name' => $Part->name,
  781. 'size' => $Part->bytes,
  782. 'attachment' => $this->_fetchPart($Part),
  783. );
  784. }
  785. return $attachments;
  786. }
  787. protected function _fetchPart ($Part) {
  788. $data = imap_fetchbody($this->Stream, $Part->uid, $Part->path, FT_UID | FT_PEEK);
  789. if ($Part->format === 'quoted-printable' && $data) {
  790. $data = quoted_printable_decode($data);
  791. }
  792. return $data;
  793. }
  794. protected function _fetchFirstByMime ($flatStructure, $mime_type) {
  795. foreach ($flatStructure as $path => $Part) {
  796. if ($mime_type === $Part->mimeType) {
  797. return $this->_fetchPart($Part);
  798. }
  799. }
  800. }
  801. /**
  802. * get id for use in the mail protocol
  803. *
  804. * @param <type> $id
  805. */
  806. protected function _toUid ($id) {
  807. if (is_array($id)) {
  808. return array_map(array($this, __FUNCTION__), $id);
  809. };
  810. $uid = $id;
  811. return $uid;
  812. }
  813. /**
  814. * get id for use in the code
  815. *
  816. * @param string $uid in the format <.*@.*> from the email
  817. *
  818. * @return mixed on imap its the unique id (int) and for others its a base64_encoded string
  819. */
  820. protected function _toId ($uid) {
  821. if (is_array($uid)) {
  822. return array_map(array($this, __FUNCTION__), $uid);
  823. };
  824. $id = $uid;
  825. return $id;
  826. }
  827. /**
  828. * Figure out how many emails there are in the thread for this mail.
  829. *
  830. * @param object $Mail the imap header of the mail
  831. * @return int the number of mails in the thred
  832. */
  833. protected function _getThreadCount ($Mail) {
  834. if (isset($Mail->reference) || isset($Mail->in_reply_to)) {
  835. return '?';
  836. }
  837. return 0;
  838. }
  839. }