PageRenderTime 30ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/tine20/Expressomail/Backend/Imap.php

https://gitlab.com/rsilveira1987/Expresso
PHP | 1290 lines | 725 code | 151 blank | 414 comment | 158 complexity | 9f9678b67a2571d5283cc5fa4cfe9a6d MD5 | raw file
  1. <?php
  2. /**
  3. * Tine 2.0
  4. *
  5. * @package Expressomail
  6. * @subpackage Backend
  7. * @license http://www.gnu.org/licenses/agpl.html AGPL Version 3
  8. * @author Cassiano Dal Pizzol <cassiano.dalpizzol@serpro.gov.br>
  9. * @author Bruno Costa Vieira <bruno.vieira-costa@serpro.gov.br>
  10. * @author Mario Cesar Kolling <mario.kolling@serpro.gov.br>
  11. * @copyright Copyright (c) 2009-2016 SERPRO (http://www.serpro.gov.br)
  12. *
  13. */
  14. /**
  15. * Expressomail IMAP backend
  16. *
  17. * @package Expressomail
  18. * @subpackage Backend
  19. */
  20. class Expressomail_Backend_Imap extends Expressomail_Backend_Imap_Abstract
  21. {
  22. /**
  23. * create instance with parameters
  24. * Supported parameters are
  25. * - user username
  26. * - host hostname or ip address of IMAP server [optional, default = 'localhost']
  27. * - password password for user 'username' [optional, default = '']
  28. * - port port for IMAP server [optional, default = 110]
  29. * - ssl 'SSL' or 'TLS' for secure sockets
  30. * - folder select this folder [optional, default = 'INBOX']
  31. *
  32. * @param object $params mail reader specific parameters
  33. */
  34. public function __construct($params,$_readOnly = FALSE)
  35. {
  36. $this->_has['flags'] = true;
  37. $this->_messageClass = 'Expressomail_Message';
  38. $this->_useUid = true;
  39. $this->_protocol = new Expressomail_Protocol_Imap();
  40. if(!isset($params->port)) {
  41. $params->port = null;
  42. }
  43. if(!isset($params->ssl)) {
  44. $params->ssl = null;
  45. }
  46. $this->connectAndLogin($params);
  47. if(!$_readOnly){
  48. $folderToSelect = isset($params->folder) ? $params->folder : 'INBOX';
  49. try {
  50. $this->selectFolder($folderToSelect);
  51. } catch (Exception $e) {
  52. if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->info('Could not select ' . $folderToSelect . '(' . $e->getMessage() . ')');
  53. }
  54. }
  55. }
  56. /**
  57. * create mailbox and all default system folders if it doesn't exist
  58. * @return true if a new inbox was created, otherwise false is returned
  59. * @throws Zend_Mail_Storage_Exception and Expressomail_Exception_IMAP
  60. */
  61. public function createDefaultImapSystemFoldersIfNecessary($params) {
  62. if ($this->getFolderStatus('INBOX') === false) {
  63. try {
  64. // connect with admin imap account
  65. $this->close();
  66. $this->_protocol->connect($params->host, $params->port, $params->ssl);
  67. $imapConfig = Tinebase_Config::getInstance()->get(Tinebase_Config::IMAP);
  68. $loginResult = $this->_protocol->login($imapConfig->cyrus['admin'], $imapConfig->cyrus['password']);
  69. $inbox = 'user' . Expressomail_Backend_Folder::IMAPDELIMITER . $params->user;
  70. try {
  71. $this->createFolder($inbox);
  72. // set default quota if the inbox was created
  73. $this->setQuota($inbox, 'STORAGE', Expressomail_Backend_Imap::DEFAULT_IMAP_QUOTA);
  74. if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
  75. Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Create the INBOX system folder');
  76. } catch (Exception $e) {
  77. if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
  78. Tinebase_Core::getLogger()->info('Could not create INBOX ' . $inbox . '(' . $e->getMessage() . ')');
  79. }
  80. // reconnect the user
  81. $this->close();
  82. $this->connectAndLogin($params);
  83. // create all default sub folders
  84. foreach (Expressomail_Backend_Folder::$SYSTEM_FOLDERS_DEFAULT as $folder) {
  85. try {
  86. $this->createFolder($folder, 'INBOX');
  87. if (Tinebase_Core::isLogLevel(Zend_Log::INFO))
  88. Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__ . ' Create new default system folder: ' . 'INBOX' . Expressomail_Backend_Folder::IMAPDELIMITER . $folder);
  89. } catch (Exception $e) {
  90. if (Tinebase_Core::isLogLevel(Zend_Log::WARN))
  91. Tinebase_Core::getLogger()->info('Could not create INBOX subfolder ' . $folder . '(' . $e->getMessage() . ')');
  92. }
  93. }
  94. $this->selectFolder('INBOX');
  95. return true;
  96. } catch (Expressomail_Exception_IMAP $zme) {
  97. if (Tinebase_Core::isLogLevel(Zend_Log::ERR))
  98. Tinebase_Core::getLogger()->info('Problem with the IMAP connection ' . ' (' . $zme->getMessage() . ')');
  99. // disconnect and raise up exception
  100. $this->close();
  101. throw $zme;
  102. } catch (Zend_Mail_Storage_Exception $zme) {
  103. if (Tinebase_Core::isLogLevel(Zend_Log::ERR))
  104. Tinebase_Core::getLogger()->info('Could not create default system folders ' . ' (' . $zme->getMessage() . ')');
  105. $this->close();
  106. throw $zme;
  107. }
  108. }
  109. $this->selectFolder('INBOX');
  110. return false;
  111. }
  112. /**
  113. * Check specific permission on folder
  114. *
  115. * @param string $_folder
  116. * @param integer $_acl
  117. * @return boolean
  118. */
  119. public function checkACL($_folder, $_acl){
  120. $rights = $this->_protocol->getMyRights($_folder);
  121. $result = false;
  122. switch ($_acl)
  123. {
  124. case self::ACLREAD : // read lsr
  125. $result = (stripos($rights,"l") !== false && stripos($rights,"r") !== false
  126. && stripos($rights,"s"));
  127. break;
  128. case self::ACLWRITE : // write wikxte
  129. $result = (stripos($rights,"w") !== false && stripos($rights,"i") !== false &&
  130. stripos($rights,"k") !== false && stripos($rights,"x") !== false &&
  131. stripos($rights,"t") !== false && stripos($rights,"e") !== false);
  132. break;
  133. case self::ACLSENDAS : // sendas p
  134. $result = (stripos($rights,"p") !== false);
  135. break;
  136. }
  137. return $result;
  138. }
  139. /**
  140. * login to imap server
  141. *
  142. * @param object $_params
  143. * @return void
  144. * @throws Expressomail_Exception_IMAPInvalidCredentials
  145. * @throws Expressomail_Exception_IMAPServiceUnavailable
  146. */
  147. public function connectAndLogin($_params)
  148. {
  149. $timeStartConnect = microtime(true);
  150. try {
  151. $this->_protocol->connect($_params->host, $_params->port, $_params->ssl);
  152. } catch (Exception $e) {
  153. throw new Expressomail_Exception_IMAPServiceUnavailable($e->getMessage());
  154. }
  155. $timeEndConnect = microtime(true);
  156. $connectTime = $timeEndConnect - $timeStartConnect;
  157. try {
  158. //TODO: set at account config and use it here????
  159. $imapConfig = Tinebase_Config::getInstance()->get(Tinebase_Config::IMAP);
  160. if ($imapConfig->backend === 'cyrus' && $imapConfig->cyrus['useProxyAuth']) {
  161. $params = array(
  162. 'authzid' => $_params->user,
  163. 'authcid' => $imapConfig->cyrus['admin'],
  164. 'password' => $imapConfig->cyrus['password'],
  165. );
  166. $loginResult = $this->_protocol->saslAuthenticate($params);
  167. } else {
  168. $loginResult = $this->_protocol->login($_params->user, $_params->password);
  169. }
  170. } catch (Exception $e) {
  171. throw new Expressomail_Exception_IMAPServiceUnavailable($e->getMessage());
  172. }
  173. if (! $loginResult) {
  174. throw new Expressomail_Exception_IMAPInvalidCredentials('Cannot login, user or password wrong.');
  175. }
  176. $timeEndLogin = microtime(true);
  177. $loginTime = $timeEndLogin - $timeEndConnect;
  178. if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' CONNECT TIME: ' . $connectTime . ' seconds');
  179. if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' LOGIN TIME: ' . $loginTime . ' seconds');
  180. }
  181. /**
  182. * select given folder
  183. *
  184. * - overwritten to get results (UIDNEXT, UIDVALIDITY, ...)
  185. *
  186. * folder must be selectable!
  187. *
  188. * @param Zend_Mail_Storage_Folder|string $globalName global name of folder or instance for subfolder
  189. * @return array with folder values
  190. * @throws Zend_Mail_Storage_Exception
  191. * @throws Zend_Mail_Protocol_Exception
  192. */
  193. public function selectFolder($globalName)
  194. {
  195. $this->_currentFolder = $globalName;
  196. if (!$result = $this->_protocol->select($this->_currentFolder)) {
  197. $this->_currentFolder = null;
  198. /**
  199. * @see Zend_Mail_Storage_Exception
  200. */
  201. $translation = Tinebase_Translation::getTranslation('Expressomail');
  202. require_once 'Zend/Mail/Storage/Exception.php';
  203. throw new Zend_Mail_Storage_Exception($translation->_('cannot change folder, maybe it does not exist'));
  204. }
  205. return $result;
  206. }
  207. /**
  208. * examine given folder
  209. *
  210. * - overwritten to get results (UIDNEXT, UIDVALIDITY, ...)
  211. *
  212. * folder must be selectable!
  213. *
  214. * @param Zend_Mail_Storage_Folder|string $globalName global name of folder or instance for subfolder
  215. * @return array with folder values
  216. * @throws Zend_Mail_Storage_Exception
  217. * @throws Zend_Mail_Protocol_Exception
  218. */
  219. public function examineFolder($globalName)
  220. {
  221. $this->_currentFolder = $globalName;
  222. if (!$result = $this->_protocol->examine($this->_currentFolder)) {
  223. $this->_currentFolder = null;
  224. /**
  225. * @see Zend_Mail_Storage_Exception
  226. */
  227. $translation = Tinebase_Translation::getTranslation('Expressomail');
  228. require_once 'Zend/Mail/Storage/Exception.php';
  229. throw new Zend_Mail_Storage_Exception($translation->_('cannot change folder, maybe it does not exist'));
  230. }
  231. return $result;
  232. }
  233. /**
  234. * get folder status
  235. *
  236. * @param Zend_Mail_Storage_Folder|string $globalName global name of folder or instance for subfolder
  237. * @return array with folder values
  238. */
  239. public function getFolderStatus($globalName)
  240. {
  241. $this->_currentFolder = $globalName;
  242. $result = $this->_protocol->getFolderStatus($this->_currentFolder);
  243. return $result;
  244. }
  245. /**
  246. * create a new folder
  247. *
  248. * This method also creates parent folders if necessary. Some mail storages may restrict, which folder
  249. * may be used as parent or which chars may be used in the folder name
  250. *
  251. * @param string $name global name of folder, local name if $parentFolder is set
  252. * @param string|Zend_Mail_Storage_Folder $parentFolder parent folder for new folder, else root folder is parent
  253. * @param string
  254. * @return null
  255. * @throws Zend_Mail_Storage_Exception
  256. */
  257. public function createFolder($name, $parentFolder = null, $_delimiter = '/')
  258. {
  259. if ($parentFolder instanceof Zend_Mail_Storage_Folder) {
  260. $folder = $parentFolder->getGlobalName() . $_delimiter . $name;
  261. } else if ($parentFolder != null) {
  262. $folder = $parentFolder . $_delimiter . $name;
  263. } else {
  264. $folder = $name;
  265. }
  266. if (!$this->_protocol->create($folder)) {
  267. /**
  268. * @see Zend_Mail_Storage_Exception
  269. */
  270. $translation = Tinebase_Translation::getTranslation('Expressomail');
  271. require_once 'Zend/Mail/Storage/Exception.php';
  272. throw new Zend_Mail_Storage_Exception($translation->_('cannot create folder') . ' ' . $folder . ', ' .$translation->_('it already exists.'));
  273. }
  274. }
  275. /**
  276. * Fetch a message
  277. *
  278. * @param int $id number of message
  279. * @return Zend_Mail_Message
  280. * @throws Zend_Mail_Protocol_Exception
  281. */
  282. public function getMessage($id)
  283. {
  284. $data = $this->_protocol->fetch(array('FLAGS', 'RFC822.HEADER'), $id, null, $this->_useUid);
  285. $header = $this->_fixHeader($data['RFC822.HEADER'], $id, $spaces);
  286. $flags = array();
  287. foreach ($data['FLAGS'] as $flag) {
  288. $flags[] = isset(self::$_knownFlags[$flag]) ? self::$_knownFlags[$flag] : $flag;
  289. }
  290. return new $this->_messageClass(array('handler' => $this, 'id' => $id, 'headers' => $header, 'flags' => $flags, 'spaces' => $spaces));
  291. }
  292. /**
  293. * Get raw content of message or part
  294. *
  295. * @param int $id number of message
  296. * @param null|array|string $part path to part, TEXT for message content or null for headers and body (@see http://www.faqs.org/rfcs/rfc3501.html / 6.4.5. FETCH Command)
  297. * @param boolean $peek use BODY.PEEK to not set the seen flag
  298. * @return string raw content
  299. * @throws Expressomail_Exception_IMAPMessageNotFound
  300. * @throws Expressomail_Exception_IMAP
  301. */
  302. public function getRawContent($id, $part = 'TEXT', $peek = false)
  303. {
  304. if ($peek === false) {
  305. $item = 'BODY';
  306. } else {
  307. $item = 'BODY.PEEK';
  308. }
  309. $item = $item . "[$part]";
  310. try {
  311. $result = $this->_protocol->fetch($item, $id, null, $this->_useUid);
  312. } catch (Zend_Mail_Protocol_Exception $zmpe) {
  313. if ($zmpe->getMessage() == 'the single id was not found in response') {
  314. throw new Expressomail_Exception_IMAPMessageNotFound('Message with id ' . $id . ' not found on IMAP server.');
  315. } else {
  316. throw new Expressomail_Exception_IMAP($zmpe->getMessage());
  317. }
  318. }
  319. return $result;
  320. }
  321. /**
  322. * set flags for message
  323. *
  324. * NOTE: this method can't set the recent flag.
  325. *
  326. * @param int $id number of message
  327. * @param array $flags new flags for message
  328. * @throws Zend_Mail_Storage_Exception
  329. */
  330. public function setFlags($id, $flags)
  331. {
  332. if (!$this->_protocol->store($flags, $id, null, null, true, $this->_useUid)) {
  333. /**
  334. * @see Zend_Mail_Storage_Exception
  335. */
  336. $translation = Tinebase_Translation::getTranslation('Expressomail');
  337. require_once 'Zend/Mail/Storage/Exception.php';
  338. throw new Zend_Mail_Storage_Exception($translation->_('cannot set flags, have you tried to set the recent flag or special chars?'));
  339. }
  340. }
  341. /**
  342. * do a search request
  343. *
  344. * This method is currently marked as internal as the API might change and is not
  345. * safe if you don't take precautions.
  346. *
  347. * @return array message ids
  348. */
  349. public function search(array $params)
  350. {
  351. $result = $this->_protocol->search($params, $this->_useUid);
  352. return $result;
  353. }
  354. /**
  355. * add flags
  356. *
  357. * @param int $id
  358. * @param array $flags
  359. */
  360. public function addFlags($id, $flags)
  361. {
  362. if (!$this->_protocol->store($flags, $id, null, '+', true, $this->_useUid)) {
  363. /**
  364. * @see Zend_Mail_Storage_Exception
  365. */
  366. $translation = Tinebase_Translation::getTranslation('Expressomail');
  367. require_once 'Zend/Mail/Storage/Exception.php';
  368. throw new Zend_Mail_Storage_Exception($translation->_('cannot set flags, have you tried to set the recent flag or special chars?'));
  369. }
  370. }
  371. /**
  372. * clear flags
  373. *
  374. * @param int $id
  375. * @param array $flags
  376. */
  377. public function clearFlags($id, $flags)
  378. {
  379. if (!$this->_protocol->store($flags, $id, null, '-', true, $this->_useUid)) {
  380. /**
  381. * @see Zend_Mail_Storage_Exception
  382. */
  383. $translation = Tinebase_Translation::getTranslation('Expressomail');
  384. require_once 'Zend/Mail/Storage/Exception.php';
  385. throw new Zend_Mail_Storage_Exception($translation->_('cannot set flags, have you tried to set the recent flag or special chars?'));
  386. }
  387. }
  388. /**
  389. * get root folder or given folder
  390. *
  391. * @param string $reference mailbox reference for list
  392. * @param string $mailbox mailbox name match with wildcards
  393. * @return Zend_Mail_Storage_Folder root or wanted folder
  394. * @throws Zend_Mail_Storage_Exception
  395. * @throws Zend_Mail_Protocol_Exception
  396. */
  397. public function getFolders($reference = '', $mailbox = '*', Expressomail_Model_Account $_account = NULL)
  398. {
  399. $folders = $this->_protocol->listMailbox((string)$reference, $mailbox);
  400. if (!$folders) {
  401. $translation = Tinebase_Translation::getTranslation('Expressomail');
  402. throw new Zend_Mail_Storage_Exception($translation->_('folder not found'));
  403. }
  404. // change the sort function
  405. $callback = new Expressomail_Backend_FolderComparator($_account);
  406. uksort($folders, array($callback, 'compare'));
  407. //ksort($folders, SORT_STRING);
  408. $result = array();
  409. foreach ($folders as $globalName => $data) {
  410. $pos = strrpos($globalName, $data['delim']);
  411. if ($pos === false) {
  412. $localName = $globalName;
  413. } else {
  414. $localName = substr($globalName, $pos + 1);
  415. }
  416. $data['flags'] = array_map('strtolower', $data['flags']);
  417. if($data['flags']) {
  418. $selectable = in_array('\\noselect', $data['flags']) ? false : true;
  419. $hasChildren = in_array('\\haschildren', $data['flags']) ? true : false;
  420. } else {
  421. $selectable = true;
  422. $hasChildren = true;
  423. }
  424. $folder = array(
  425. 'localName' => $localName,
  426. 'globalName' => $globalName,
  427. 'delimiter' => $data['delim'],
  428. 'isSelectable' => $selectable,
  429. 'hasChildren' => $hasChildren
  430. );
  431. $result[$globalName] = $folder;
  432. }
  433. return $result;
  434. }
  435. /**
  436. * return uid for given message numbers
  437. *
  438. * @param int $from
  439. * @param int|null $to
  440. * @return array
  441. */
  442. public function getUid($from, $to = null)
  443. {
  444. $data = $this->_protocol->fetch('UID', $from, $to);
  445. if(!is_array($data)) {
  446. return array($from => $data);
  447. } else {
  448. return $data;
  449. }
  450. }
  451. /**
  452. * get messages summary
  453. *
  454. * @param int $from
  455. * @param int|null $to
  456. * @return array with $this->_messageClass (Expressomail_Message)
  457. */
  458. public function getSummary($from, $to = null, $_useUid = null, $_folderId = NULL)
  459. {
  460. $useUid = ($_useUid === null) ? $this->_useUid : (bool) $_useUid;
  461. $summary = $this->_protocol->fetch(array('UID', 'FLAGS', 'RFC822.HEADER', 'INTERNALDATE', 'RFC822.SIZE', 'BODYSTRUCTURE'), $from, $to, $useUid);
  462. // fetch returns a different structure when fetching one or multiple messages
  463. if($to === null && ctype_digit("$from")) {
  464. $summary = array(
  465. $from => $summary
  466. );
  467. }
  468. $messages = array();
  469. foreach($summary as $id => $data) {
  470. $header = $this->_fixHeader($data['RFC822.HEADER'], $id, $spaces);
  471. Zend_Mime_Decode::splitMessage($header, $header, $null);
  472. $structure = $this->parseStructure($data['BODYSTRUCTURE']);
  473. $flags = array();
  474. foreach ($data['FLAGS'] as $flag) {
  475. $flags[] = isset(self::$_knownFlags[$flag]) ? self::$_knownFlags[$flag] : $flag;
  476. }
  477. if($this->_useUid === true) {
  478. $key = $data['UID'];
  479. } else {
  480. $key = $id;
  481. }
  482. $messages[$key] = array(
  483. 'header' => $header,
  484. 'flags' => $flags,
  485. 'received' => $data['INTERNALDATE'],
  486. 'size' => $data['RFC822.SIZE'],
  487. 'structure' => $structure,
  488. 'uid' => $data['UID']
  489. );
  490. if (!empty($_folderId))
  491. {
  492. $messages[$key]['folder_id'] = $_folderId;
  493. }
  494. }
  495. if($to === null && ctype_digit("$from")) {
  496. // only one message requested
  497. return $messages[$from];
  498. } else {
  499. // multiple messages requested
  500. return $messages;
  501. }
  502. }
  503. /**
  504. * get messages flags
  505. *
  506. * @param int $from
  507. * @param int|null $to
  508. * @return array of flags
  509. */
  510. public function getFlags($from, $to = null, $_useUid = null)
  511. {
  512. $useUid = ($_useUid === null) ? $this->_useUid : (bool) $_useUid;
  513. $summary = $this->_protocol->fetch(array('UID', 'FLAGS'), $from, $to, $useUid);
  514. // fetch returns a different structure when fetching one or multiple messages
  515. if($to === null && ctype_digit("$from")) {
  516. $summary = array(
  517. $from => $summary
  518. );
  519. }
  520. $messages = array();
  521. foreach($summary as $id => $data) {
  522. $flags = array();
  523. foreach ($data['FLAGS'] as $flag) {
  524. $flags[] = isset(self::$_knownFlags[$flag]) ? self::$_knownFlags[$flag] : $flag;
  525. }
  526. if($this->_useUid === true) {
  527. $key = $data['UID'];
  528. } else {
  529. $key = $id;
  530. }
  531. $messages[$key] = array(
  532. 'flags' => $flags,
  533. 'uid' => $data['UID']
  534. );
  535. }
  536. if($to === null && ctype_digit("$from")) {
  537. // only one message requested
  538. return $messages[$from];
  539. } else {
  540. // multiple messages requested
  541. return $messages;
  542. }
  543. }
  544. /**
  545. * parse message structure
  546. *
  547. * @param array $_structure
  548. * @param integer $_partId
  549. * @return array structure
  550. */
  551. public function parseStructure($_structure, $_partId = null)
  552. {
  553. try {
  554. $structure = $this->_parsePartStructure($_structure, $_partId);
  555. } catch (Expressomail_Exception_IMAP $fei) {
  556. if (Tinebase_Core::isLogLevel(Zend_Log::WARN)) Tinebase_Core::getLogger()->warn(__METHOD__ . '::' . __LINE__ . ' Could not parse structure. Assuming text/plain default structure.');
  557. $structure = $this->_getBasicNonMultipartStructure($_partId);
  558. }
  559. if ($structure['partId'] === null && empty($structure['parts'])) {
  560. $structure['partId'] = 1;
  561. }
  562. return $structure;
  563. }
  564. /**
  565. * returns basic non multipart message structure
  566. *
  567. * @param integer $_partId
  568. * @return array
  569. */
  570. protected function _getBasicNonMultipartStructure($_partId)
  571. {
  572. $structure = array(
  573. 'partId' => $_partId,
  574. 'contentType' => Expressomail_Model_Message::CONTENT_TYPE_PLAIN,
  575. 'type' => 'text',
  576. 'subType' => 'plain',
  577. 'parameters' => array(),
  578. 'id' => null,
  579. 'description' => null,
  580. 'encoding' => null,
  581. 'size' => null,
  582. 'lines' => null,
  583. 'disposition' => null,
  584. 'language' => null,
  585. 'location' => null
  586. );
  587. return $structure;
  588. }
  589. /**
  590. * parse message part structure (this is called recursivly by _parseStructureMultiPart() and _parseStructureNonMultiPart())
  591. *
  592. * @param array $_structure
  593. * @param integer $_partId
  594. * @return array structure
  595. */
  596. protected function _parsePartStructure($_structure, $_partId)
  597. {
  598. if (is_array($_structure[0])) {
  599. $structure = $this->_parseStructureMultiPart($_structure, $_partId);
  600. } else {
  601. $structure = $this->_parseStructureNonMultiPart($_structure, $_partId);
  602. }
  603. return $structure;
  604. }
  605. /**
  606. * parse multipart message structure
  607. *
  608. * @param array $_structure
  609. * @param integer $_partId
  610. */
  611. protected function _parseStructureMultiPart($_structure, $_partId)
  612. {
  613. $structure = array(
  614. 'partId' => $_partId,
  615. 'contentType' => null,
  616. 'type' => null,
  617. 'subType' => null,
  618. 'parts' => array(),
  619. 'parameters' => array(),
  620. 'disposition' => null,
  621. 'language' => null,
  622. 'location' => null
  623. );
  624. $index = 0;
  625. // all arrays until the first non array value are parts
  626. foreach ($_structure as $part) {
  627. if (!is_array($part)) {
  628. break;
  629. }
  630. $index++;
  631. $partId = ($_partId === null) ? $index : $_partId . '.' . $index;
  632. $structure['parts'][$partId] = $this->_parsePartStructure($part, $partId);
  633. }
  634. // content type
  635. $type = 'multipart';
  636. $subType = strtolower($_structure[$index]);
  637. $structure['contentType'] = $type . '/' . $subType;
  638. $structure['type'] = $type;
  639. $structure['subType'] = $subType;
  640. $index++;
  641. // body parameters
  642. if(isset($_structure[$index]) && is_array($_structure[$index])) {
  643. $parameters = array();
  644. for($i=0; $i<count($_structure[$index]); $i++) {
  645. $key = strtolower($_structure[$index][$i]);
  646. $value = $_structure[$index][++$i];
  647. $parameters[$key] = $this->_mimeDecodeHeader($value);
  648. }
  649. $structure['parameters'] = $parameters;
  650. }
  651. $index++;
  652. // body disposition
  653. if (isset($_structure[$index]) && $_structure[$index] != 'NIL') {
  654. $structure['disposition']['type'] = $_structure[$index][0];
  655. if ($_structure[$index][1] != 'NIL') {
  656. $parameters = array();
  657. for($i=0; $i<count($_structure[$index][1]); $i++) {
  658. if (is_array($_structure[$index][1]) && $_structure[$index][1][$i] != 'NIL') {
  659. $key = strtolower($_structure[$index][1][$i]);
  660. $value = $_structure[$index][1][++$i];
  661. $parameters[$key] = $this->_mimeDecodeHeader($value);
  662. }
  663. }
  664. $structure['disposition']['parameters'] = $parameters;
  665. }
  666. }
  667. $index++;
  668. // body language
  669. if (isset($_structure[$index]) && $_structure[$index] != 'NIL') {
  670. $structure['language'] = $_structure[$index];
  671. }
  672. $index++;
  673. // body location
  674. if (isset($_structure[$index]) && $_structure[$index] != 'NIL') {
  675. $structure['location'] = strtolower($_structure[$index]);
  676. }
  677. return $structure;
  678. }
  679. /**
  680. * parse non multipart message structure
  681. *
  682. * @param array $_structure
  683. * @param integer $_partId
  684. * @return array
  685. * @throws Expressomail_Exception_IMAP
  686. */
  687. protected function _parseStructureNonMultiPart($_structure, $_partId)
  688. {
  689. if (is_array($_structure[0]) || is_array($_structure[1])) {
  690. if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__ . ' ' . print_r($_structure, TRUE));
  691. throw new Expressomail_Exception_IMAP('Invalid structure. String expected, got array.');
  692. }
  693. $structure = $this->_getBasicNonMultipartStructure($_partId);
  694. /** basic fields begin **/
  695. // contentType
  696. $type = strtolower($_structure[0]);
  697. $subType = strtolower($_structure[1]);
  698. $structure['contentType'] = $type . '/' . $subType;
  699. $structure['type'] = $type;
  700. $structure['subType'] = $subType;
  701. // body parameters
  702. if(is_array($_structure[2])) {
  703. $parameters = array();
  704. for($i=0; $i<count($_structure[2]); $i++) {
  705. $key = strtolower($_structure[2][$i]);
  706. $value = $_structure[2][++$i];
  707. $parameters[$key] = $this->_mimeDecodeHeader($value);
  708. }
  709. $structure['parameters'] = $parameters;
  710. }
  711. // body id
  712. if($_structure[3] != 'NIL') {
  713. $structure['id'] = $_structure[3];
  714. }
  715. // body description
  716. if($_structure[4] != 'NIL') {
  717. $structure['description'] = $_structure[4];
  718. }
  719. // body encoding
  720. if($_structure[5] != 'NIL') {
  721. $structure['encoding'] = strtolower($_structure[5]);
  722. }
  723. // body size
  724. if($_structure[6] != 'NIL') {
  725. $structure['size'] = strtolower($_structure[6]);
  726. }
  727. /** basic fields end **/
  728. $index = 7;
  729. if($type == 'message' && $subType == 'rfc822') {
  730. $structure['messageEnvelop'] = $_structure[7];
  731. $structure['messageStructure'] = $this->_parsePartStructure($_structure[8], $_partId);
  732. $structure['messageLines'] = $_structure[9];
  733. // index of the first element containing extension data
  734. $index = 10;
  735. } elseif($type == 'text') {
  736. if($_structure[7] != 'NIL') {
  737. $structure['lines'] = $_structure[7];
  738. }
  739. // index of the first element containing extension data
  740. $index = 8;
  741. }
  742. // body md5
  743. if(array_key_exists($index, $_structure) && $_structure[$index] != 'NIL') {
  744. $structure['md5'] = strtolower($_structure[$index]);
  745. }
  746. $index++;
  747. // body disposition
  748. if (array_key_exists($index, $_structure) && $_structure[$index] != 'NIL') {
  749. $structure['disposition']['type'] = $_structure[$index][0];
  750. if (isset($_structure[$index][1]) && $_structure[$index][1] != 'NIL' && is_array($_structure[$index][1])) {
  751. $parameters = array();
  752. for ($i=0; $i<count($_structure[$index][1]); $i++) {
  753. $key = strtolower($_structure[$index][1][$i]);
  754. $value = $_structure[$index][1][++$i];
  755. $parameters[$key] = $this->_mimeDecodeHeader($value);
  756. }
  757. $structure['disposition']['parameters'] = $parameters;
  758. }
  759. }
  760. $index++;
  761. // body language
  762. if(array_key_exists($index, $_structure) && $_structure[$index] != 'NIL' && ! is_array($_structure[$index])) {
  763. $structure['language'] = strtolower($_structure[$index]);
  764. }
  765. $index++;
  766. // body location
  767. if(array_key_exists($index, $_structure) && $_structure[$index] != 'NIL' && ! is_array($_structure[$index])) {
  768. $structure['location'] = strtolower($_structure[$index]);
  769. }
  770. return $structure;
  771. }
  772. /**
  773. * validates that messageUid still exists on imap server
  774. * @param $from
  775. * @param $to
  776. */
  777. public function messageUidExists($from, $to = null)
  778. {
  779. $result = $this->_protocol->fetch('UID', $from, $to, true);
  780. return $result;
  781. }
  782. /**
  783. * get uids by uid
  784. *
  785. * @param int $from
  786. * @param int|null $to
  787. * @return array with uids
  788. */
  789. public function getUidbyUid($from, $to = null)
  790. {
  791. $result = $this->_protocol->fetch('UID', $from, $to, true);
  792. // @todo check if this is really needed
  793. // sanitize result, sometimes the fetch command can return wrong results :(
  794. if (is_numeric($from) && is_numeric($to)) {
  795. foreach ($result as $key => $value) {
  796. // check if out of bounds
  797. if ($value < min($to, $from) || $value > max($to, $from)) {
  798. Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__ . ' Uid out of bounds detected: '
  799. . $key . ' (' . min($to, $from) . ' - ' . max($to, $from) . ')');
  800. unset($result[$key]);
  801. }
  802. }
  803. } else {
  804. // @todo perhaps we should do an array_intersect here as well
  805. }
  806. return array_values($result);
  807. }
  808. public function resolveMessageSequence($from, $to = null)
  809. {
  810. $result = $this->_protocol->fetch('UID', $from, $to, false);
  811. return $result;
  812. }
  813. public function resolveMessageUid($from, $to = null)
  814. {
  815. // we always need to ask for multiple values(array), because that's the only way to retrieve the message sequence
  816. if ($to === null && !is_array($from)) {
  817. $from = (array) $from;
  818. }
  819. $result = $this->_protocol->fetch('UID', $from, $to, true);
  820. if (count($result) === 0) {
  821. throw new Zend_Mail_Protocol_Exception('the single id was not found in response');
  822. }
  823. if ($to === null && count($from) === 1) {
  824. return key($result);
  825. } else {
  826. return array_keys($result);
  827. }
  828. }
  829. /**
  830. * Remove a message from server. If you're doing that from a web enviroment
  831. * you should be careful and use a uniqueid as parameter if possible to
  832. * identify the message.
  833. *
  834. * @param int $id number of message
  835. * @return void
  836. * @throws Expressomail_Exception_IMAP
  837. */
  838. public function removeMessage($id)
  839. {
  840. if (!$this->_protocol->store(array(Zend_Mail_Storage::FLAG_DELETED), $id, null, '+', true, $this->_useUid)) {
  841. throw new Expressomail_Exception_IMAP('cannot set deleted flag');
  842. }
  843. // TODO: expunge here or at close? we can handle an error here better and are more fail safe
  844. if (!$this->_protocol->expunge()) {
  845. throw new Expressomail_Exception_IMAP('message marked as deleted, but could not expunge');
  846. }
  847. }
  848. /**
  849. * copy an existing message
  850. *
  851. * @param int|array $id number of message(s)
  852. * @param string|Zend_Mail_Storage_Folder $folder name or instance of targer folder
  853. * @return void
  854. * @throws Expressomail_Exception_IMAP
  855. */
  856. public function copyMessage($id, $folder)
  857. {
  858. if (!$this->_protocol->copy($folder, $id, null, $this->_useUid)) {
  859. throw new Expressomail_Exception_IMAP('Cannot copy message, does the target folder "' . $folder . '" exist? Or maybe you exceeded your Quota.');
  860. }
  861. }
  862. /**
  863. * get server capabilities and namespace
  864. *
  865. * @return array
  866. */
  867. public function getCapabilityAndNamespace()
  868. {
  869. $capabilities = $this->_protocol->capability();
  870. $result = array('capabilities' => $capabilities);
  871. if (in_array('NAMESPACE', $capabilities)) {
  872. if ($namespace = $this->_protocol->getNamespace()) {
  873. $result['namespace'] = $namespace;
  874. }
  875. }
  876. return $result;
  877. }
  878. /**
  879. * empty complete folder by setting \Deleted flag and expunge afterwards
  880. *
  881. * @param string $globalName
  882. * @return void
  883. * @throws Zend_Mail_Storage_Exception
  884. */
  885. public function emptyFolder($globalName)
  886. {
  887. $this->selectFolder($globalName);
  888. if (! $this->_protocol->store(array(Zend_Mail_Storage::FLAG_DELETED), 1, INF, null, true)) {
  889. /**
  890. * @see Zend_Mail_Storage_Exception
  891. */
  892. $translation = Tinebase_Translation::getTranslation('Expressomail');
  893. require_once 'Zend/Mail/Storage/Exception.php';
  894. throw new Zend_Mail_Storage_Exception($translation->_('cannot set Deleted flags'));
  895. }
  896. $this->_protocol->expunge();
  897. }
  898. /**
  899. * remove all messages marked as deleted
  900. *
  901. * @param string $globalName
  902. * @return void
  903. * @throws Zend_Mail_Storage_Exception
  904. */
  905. public function expunge($globalName)
  906. {
  907. $this->selectFolder($globalName);
  908. $this->_protocol->expunge();
  909. }
  910. protected function _mimeDecodeHeader($_header)
  911. {
  912. $result = iconv_mime_decode($_header, ICONV_MIME_DECODE_CONTINUE_ON_ERROR);
  913. return $result;
  914. }
  915. /**
  916. * get header (remove spaces if needed)
  917. * NOTE: this fixes a bug in Zend_Mime_Decode: headers with leading spaces are not parsed correctly,
  918. * we remove the spaces here to make it work again.
  919. *
  920. * @param string $_header
  921. * @param string $_messageId
  922. * @param int $_leadingSpaces
  923. * @return string
  924. */
  925. protected function _fixHeader($_header, $_messageId, &$_leadingSpaces = 0)
  926. {
  927. $header = $this->_replaceHeaderSpaces($_header, $_messageId, $_leadingSpaces);
  928. $result = $this->_fixHeaderEncoding($header);
  929. return $result;
  930. }
  931. /**
  932. * remove leading spaces from headers
  933. *
  934. * @param string $_header
  935. * @param string $_messageId
  936. * @param integer $_leadingSpaces
  937. * @return string
  938. */
  939. protected function _replaceHeaderSpaces($_header, $_messageId, &$_leadingSpaces = 0)
  940. {
  941. // check for valid header at first line (this is done again in Zend_Mime_Decode)
  942. $firstline = strtok($_header, "\n");
  943. if (preg_match('/^([\s]+)[^:]+:/', $firstline, $matches)) {
  944. // replace all spaces before headers
  945. if (Tinebase_Core::isLogLevel(Zend_Log::NOTICE)) Tinebase_Core::getLogger()->notice(__METHOD__ . '::' . __LINE__
  946. . ' No headers found. Removing leading spaces from headers for message ' . $_messageId . '.');
  947. $_leadingSpaces = strlen($matches[1]);
  948. $result = preg_replace("/^[\s]{1," . $_leadingSpaces . "}/m", "", $_header);
  949. } else {
  950. $_leadingSpaces = 0;
  951. $result = $_header;
  952. }
  953. return $result;
  954. }
  955. /**
  956. * (mime) encode some headers ('subject', 'from', 'to', ...)
  957. *
  958. * @param string $_header
  959. * @return string
  960. *
  961. * @todo support multiple to, ... headers
  962. */
  963. protected function _fixHeaderEncoding($_header)
  964. {
  965. $result = $_header;
  966. $encoding = (extension_loaded('mbstring')) ? mb_detect_encoding($result) : 'unknown';
  967. if ($encoding !== 'ASCII' && preg_match('/[^\x20-\x7E]*/', $result)) {
  968. if (Tinebase_Core::isLogLevel(Zend_Log::DEBUG)) Tinebase_Core::getLogger()->debug(__METHOD__ . '::' . __LINE__
  969. . ' Non-ASCII character (encoding:' . $encoding .') detected, mime encode some headers.');
  970. if(strtolower($encoding) == 'utf-8'){
  971. $result = utf8_decode(imap_utf8($result));
  972. }
  973. foreach (array('subject', 'from', 'to', 'cc', 'bcc') as $field) {
  974. if (preg_match('/' . $field . ': (.*?[\n][\s]*?)/i', $result, $matches)) {
  975. if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
  976. . ' ' . print_r($matches, TRUE));
  977. $headerValue = str_replace("\n", '', $matches[1]);
  978. $headerValue = Tinebase_Helper::mbConvertTo($headerValue);
  979. $headerString = iconv_mime_encode(ucfirst($field), $headerValue);
  980. $result = str_replace($matches[0], $headerString . "\n", $result);
  981. }
  982. }
  983. if (Tinebase_Core::isLogLevel(Zend_Log::TRACE)) Tinebase_Core::getLogger()->trace(__METHOD__ . '::' . __LINE__
  984. . ' ' .$result);
  985. }
  986. return $result;
  987. }
  988. /**
  989. * get quota for mailbox
  990. *
  991. * @param string $_mailbox
  992. * @return array quota info
  993. */
  994. public function getQuota($_mailbox)
  995. {
  996. return $this->_protocol->getQuotaRoot($_mailbox);
  997. }
  998. /**
  999. * set quota for specified mailbox
  1000. *
  1001. * @see http://tools.ietf.org/html/rfc2087
  1002. * @param string $mailbox the mailbox (user/example)
  1003. * @param string $resource the resource (STORAGE or MESSAGE)
  1004. * @param int $limit the limit (set to null to remove limit)
  1005. */
  1006. public function setQuota($mailbox = '*', $resource = 'STORAGE', $limit=null)
  1007. {
  1008. return $this->_protocol->setQuota($mailbox, $resource, $limit);
  1009. }
  1010. /**
  1011. * Get sorted messages through the Impa sort command
  1012. *
  1013. * @param array $params
  1014. * @param boolean $uid
  1015. * @param boolean $descending
  1016. * @param array $search
  1017. * @param string $charset
  1018. * @return array
  1019. *
  1020. * @todo verify capabilities and throw an exception if server don't implement sort extension
  1021. */
  1022. public function sort(array $params, array $search = NULL, $charset = 'UTF-8')
  1023. {
  1024. $result = $this->_protocol->sort($params, $this->_useUid, $search, $charset);
  1025. return $result;
  1026. }
  1027. /**
  1028. * get folder Acls
  1029. *
  1030. * @param Zend_Mail_Storage_Folder|string $globalName global name of folder or instance for subfolder
  1031. * @param bool $returnOwnerACL true if it will return owner's ACL
  1032. * @return array with folder values
  1033. */
  1034. public function getFolderAcls($_globalName, $returnOwnerACL = FALSE)
  1035. {
  1036. $this->_currentFolder = $_globalName;
  1037. $result = $this->_protocol->getFolderAcls($this->_currentFolder, $returnOwnerACL);
  1038. return $result;
  1039. }
  1040. /**
  1041. * get folder Acls
  1042. *
  1043. * @param Zend_Mail_Storage_Folder|string $globalName global name of folder or instance for subfolder
  1044. * @return array with folder values
  1045. */
  1046. public function getUsersWithSendAsAcl($_folders)
  1047. {
  1048. $result = $this->_protocol->getUsersWithSendAsAcl($_folders);
  1049. return $result;
  1050. }
  1051. /**
  1052. * set folder Acls
  1053. *
  1054. * @param Zend_Mail_Storage_Folder|string $globalName global name of folder or instance for subfolder
  1055. * @param string $acls acls
  1056. * @return array with folder values
  1057. */
  1058. public function setFolderAcls($globalName,$acls)
  1059. {
  1060. $this->_currentFolder = $globalName;
  1061. $result = $this->_protocol->setFolderAcls($this->_currentFolder,$acls);
  1062. return $result;
  1063. }
  1064. /**
  1065. * Fetch Messages Ids changed since $modseq
  1066. *
  1067. * @param string $box - Folder to select
  1068. * @param integer $modseq - $modSeq to search messages since
  1069. * @return array list of messages ids, flags changed since last modseq
  1070. * @throws Zend_Mail_Protocol_Exception
  1071. */
  1072. public function fetchIdsChangedSinceModSeq($box, $modseq)
  1073. {
  1074. return $this->_protocol->fetchIdsChangedSinceModSeq($box, $modseq);
  1075. }
  1076. /**
  1077. * Returns the current usernamespace
  1078. *
  1079. * @return type string
  1080. */
  1081. public function getUserNameSpace()
  1082. {
  1083. return $this->_userNameSpace;
  1084. }
  1085. /**
  1086. * Set shared seen value to imap
  1087. *
  1088. * @param string $value
  1089. * @return boolean return operation's success status
  1090. */
  1091. public function setSharedSeen($value)
  1092. {
  1093. $stringValue = $value ? '"true"' : '"false"';
  1094. $annotation[] = '"INBOX"';
  1095. $annotation[] = '"/vendor/cmu/cyrus-imapd/sharedseen"';
  1096. $annotation[] = '("value.shared" ' . $stringValue . ')';
  1097. return $this->_protocol->requestAndResponse('SETANNOTATION', $annotation);
  1098. }
  1099. /**
  1100. * Get Shared seen value
  1101. *
  1102. * @return boolean shared seen value
  1103. */
  1104. public function getSharedSeen()
  1105. {
  1106. $annotation[] = '"INBOX"';
  1107. $annotation[] = '"/vendor/cmu/cyrus-imapd/sharedseen"';
  1108. $annotation[] = '"value.shared"';
  1109. $result = $this->_protocol->requestAndResponse('GETANNOTATION', $annotation);
  1110. if (is_array($result) && isset($result[0][3][1])) {
  1111. Tinebase_Core::getLogger()->info(__METHOD__ . '::' . __LINE__
  1112. . ' Shared Seen value from Imap ' . $result[0][3][1]);
  1113. return strtolower($result[0][3][1]) === 'true' ? TRUE : FALSE;
  1114. } else {
  1115. Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__
  1116. . ' Imap command failed when getting sharedseen value!');
  1117. return FALSE;
  1118. }
  1119. }
  1120. /**
  1121. * Getting cyrus murder backend hostname
  1122. *
  1123. * @return mixed
  1124. */
  1125. public function getCyrusMurderBackend()
  1126. {
  1127. $annotation[] = '"INBOX"';
  1128. $annotation[] = '"/vendor/cmu/cyrus-imapd/server"';
  1129. $annotation[] = '"value.shared"';
  1130. $result = $this->_protocol->requestAndResponse('GETANNOTATION', $annotation);
  1131. if (is_array($result) && isset($result[0][3][1])) {
  1132. return $result[0][3][1];
  1133. } else {
  1134. Tinebase_Core::getLogger()->err(__METHOD__ . '::' . __LINE__
  1135. . ' Imap command failed when getting cyrus murder backend hostname!');
  1136. return FALSE;
  1137. }
  1138. }
  1139. }