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

/concreteOLD/libraries/3rdparty/Zend/Mail/Storage/Writable/Maildir.php

https://bitbucket.org/selfeky/xclusivescardwebsite
PHP | 1049 lines | 595 code | 116 blank | 338 comment | 179 complexity | 6f9c8779fccc498de933eefe832e6319 MD5 | raw file
  1. <?php
  2. /**
  3. * Zend Framework
  4. *
  5. * LICENSE
  6. *
  7. * This source file is subject to the new BSD license that is bundled
  8. * with this package in the file LICENSE.txt.
  9. * It is also available through the world-wide-web at this URL:
  10. * http://framework.zend.com/license/new-bsd
  11. * If you did not receive a copy of the license and are unable to
  12. * obtain it through the world-wide-web, please send an email
  13. * to license@zend.com so we can send you a copy immediately.
  14. *
  15. * @category Zend
  16. * @package Zend_Mail
  17. * @subpackage Storage
  18. * @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
  19. * @license http://framework.zend.com/license/new-bsd New BSD License
  20. * @version $Id: Maildir.php 23775 2011-03-01 17:25:24Z ralph $
  21. */
  22. /**
  23. * @see Zend_Mail_Storage_Folder_Maildir
  24. */
  25. require_once 'Zend/Mail/Storage/Folder/Maildir.php';
  26. /**
  27. * @see Zend_Mail_Storage_Writable_Interface
  28. */
  29. require_once 'Zend/Mail/Storage/Writable/Interface.php';
  30. /**
  31. * @category Zend
  32. * @package Zend_Mail
  33. * @subpackage Storage
  34. * @copyright Copyright (c) 2005-2011 Zend Technologies USA Inc. (http://www.zend.com)
  35. * @license http://framework.zend.com/license/new-bsd New BSD License
  36. */
  37. class Zend_Mail_Storage_Writable_Maildir extends Zend_Mail_Storage_Folder_Maildir
  38. implements Zend_Mail_Storage_Writable_Interface
  39. {
  40. // TODO: init maildir (+ constructor option create if not found)
  41. /**
  42. * use quota and size of quota if given
  43. * @var bool|int
  44. */
  45. protected $_quota;
  46. /**
  47. * create a new maildir
  48. *
  49. * If the given dir is already a valid maildir this will not fail.
  50. *
  51. * @param string $dir directory for the new maildir (may already exist)
  52. * @return null
  53. * @throws Zend_Mail_Storage_Exception
  54. */
  55. public static function initMaildir($dir)
  56. {
  57. if (file_exists($dir)) {
  58. if (!is_dir($dir)) {
  59. /**
  60. * @see Zend_Mail_Storage_Exception
  61. */
  62. require_once 'Zend/Mail/Storage/Exception.php';
  63. throw new Zend_Mail_Storage_Exception('maildir must be a directory if already exists');
  64. }
  65. } else {
  66. if (!mkdir($dir)) {
  67. /**
  68. * @see Zend_Mail_Storage_Exception
  69. */
  70. require_once 'Zend/Mail/Storage/Exception.php';
  71. $dir = dirname($dir);
  72. if (!file_exists($dir)) {
  73. throw new Zend_Mail_Storage_Exception("parent $dir not found");
  74. } else if (!is_dir($dir)) {
  75. throw new Zend_Mail_Storage_Exception("parent $dir not a directory");
  76. } else {
  77. throw new Zend_Mail_Storage_Exception('cannot create maildir');
  78. }
  79. }
  80. }
  81. foreach (array('cur', 'tmp', 'new') as $subdir) {
  82. if (!@mkdir($dir . DIRECTORY_SEPARATOR . $subdir)) {
  83. // ignore if dir exists (i.e. was already valid maildir or two processes try to create one)
  84. if (!file_exists($dir . DIRECTORY_SEPARATOR . $subdir)) {
  85. /**
  86. * @see Zend_Mail_Storage_Exception
  87. */
  88. require_once 'Zend/Mail/Storage/Exception.php';
  89. throw new Zend_Mail_Storage_Exception('could not create subdir ' . $subdir);
  90. }
  91. }
  92. }
  93. }
  94. /**
  95. * Create instance with parameters
  96. * Additional parameters are (see parent for more):
  97. * - create if true a new maildir is create if none exists
  98. *
  99. * @param array $params mail reader specific parameters
  100. * @throws Zend_Mail_Storage_Exception
  101. */
  102. public function __construct($params) {
  103. if (is_array($params)) {
  104. $params = (object)$params;
  105. }
  106. if (!empty($params->create) && isset($params->dirname) && !file_exists($params->dirname . DIRECTORY_SEPARATOR . 'cur')) {
  107. self::initMaildir($params->dirname);
  108. }
  109. parent::__construct($params);
  110. }
  111. /**
  112. * create a new folder
  113. *
  114. * This method also creates parent folders if necessary. Some mail storages may restrict, which folder
  115. * may be used as parent or which chars may be used in the folder name
  116. *
  117. * @param string $name global name of folder, local name if $parentFolder is set
  118. * @param string|Zend_Mail_Storage_Folder $parentFolder parent folder for new folder, else root folder is parent
  119. * @return string only used internally (new created maildir)
  120. * @throws Zend_Mail_Storage_Exception
  121. */
  122. public function createFolder($name, $parentFolder = null)
  123. {
  124. if ($parentFolder instanceof Zend_Mail_Storage_Folder) {
  125. $folder = $parentFolder->getGlobalName() . $this->_delim . $name;
  126. } else if ($parentFolder != null) {
  127. $folder = rtrim($parentFolder, $this->_delim) . $this->_delim . $name;
  128. } else {
  129. $folder = $name;
  130. }
  131. $folder = trim($folder, $this->_delim);
  132. // first we check if we try to create a folder that does exist
  133. $exists = null;
  134. try {
  135. $exists = $this->getFolders($folder);
  136. } catch (Zend_Mail_Exception $e) {
  137. // ok
  138. }
  139. if ($exists) {
  140. /**
  141. * @see Zend_Mail_Storage_Exception
  142. */
  143. require_once 'Zend/Mail/Storage/Exception.php';
  144. throw new Zend_Mail_Storage_Exception('folder already exists');
  145. }
  146. if (strpos($folder, $this->_delim . $this->_delim) !== false) {
  147. /**
  148. * @see Zend_Mail_Storage_Exception
  149. */
  150. require_once 'Zend/Mail/Storage/Exception.php';
  151. throw new Zend_Mail_Storage_Exception('invalid name - folder parts may not be empty');
  152. }
  153. if (strpos($folder, 'INBOX' . $this->_delim) === 0) {
  154. $folder = substr($folder, 6);
  155. }
  156. $fulldir = $this->_rootdir . '.' . $folder;
  157. // check if we got tricked and would create a dir outside of the rootdir or not as direct child
  158. if (strpos($folder, DIRECTORY_SEPARATOR) !== false || strpos($folder, '/') !== false
  159. || dirname($fulldir) . DIRECTORY_SEPARATOR != $this->_rootdir) {
  160. /**
  161. * @see Zend_Mail_Storage_Exception
  162. */
  163. require_once 'Zend/Mail/Storage/Exception.php';
  164. throw new Zend_Mail_Storage_Exception('invalid name - no directory seprator allowed in folder name');
  165. }
  166. // has a parent folder?
  167. $parent = null;
  168. if (strpos($folder, $this->_delim)) {
  169. // let's see if the parent folder exists
  170. $parent = substr($folder, 0, strrpos($folder, $this->_delim));
  171. try {
  172. $this->getFolders($parent);
  173. } catch (Zend_Mail_Exception $e) {
  174. // does not - create parent folder
  175. $this->createFolder($parent);
  176. }
  177. }
  178. if (!@mkdir($fulldir) || !@mkdir($fulldir . DIRECTORY_SEPARATOR . 'cur')) {
  179. /**
  180. * @see Zend_Mail_Storage_Exception
  181. */
  182. require_once 'Zend/Mail/Storage/Exception.php';
  183. throw new Zend_Mail_Storage_Exception('error while creating new folder, may be created incompletly');
  184. }
  185. mkdir($fulldir . DIRECTORY_SEPARATOR . 'new');
  186. mkdir($fulldir . DIRECTORY_SEPARATOR . 'tmp');
  187. $localName = $parent ? substr($folder, strlen($parent) + 1) : $folder;
  188. $this->getFolders($parent)->$localName = new Zend_Mail_Storage_Folder($localName, $folder, true);
  189. return $fulldir;
  190. }
  191. /**
  192. * remove a folder
  193. *
  194. * @param string|Zend_Mail_Storage_Folder $name name or instance of folder
  195. * @return null
  196. * @throws Zend_Mail_Storage_Exception
  197. */
  198. public function removeFolder($name)
  199. {
  200. // TODO: This could fail in the middle of the task, which is not optimal.
  201. // But there is no defined standard way to mark a folder as removed and there is no atomar fs-op
  202. // to remove a directory. Also moving the folder to a/the trash folder is not possible, as
  203. // all parent folders must be created. What we could do is add a dash to the front of the
  204. // directory name and it should be ignored as long as other processes obey the standard.
  205. if ($name instanceof Zend_Mail_Storage_Folder) {
  206. $name = $name->getGlobalName();
  207. }
  208. $name = trim($name, $this->_delim);
  209. if (strpos($name, 'INBOX' . $this->_delim) === 0) {
  210. $name = substr($name, 6);
  211. }
  212. // check if folder exists and has no children
  213. if (!$this->getFolders($name)->isLeaf()) {
  214. /**
  215. * @see Zend_Mail_Storage_Exception
  216. */
  217. require_once 'Zend/Mail/Storage/Exception.php';
  218. throw new Zend_Mail_Storage_Exception('delete children first');
  219. }
  220. if ($name == 'INBOX' || $name == DIRECTORY_SEPARATOR || $name == '/') {
  221. /**
  222. * @see Zend_Mail_Storage_Exception
  223. */
  224. require_once 'Zend/Mail/Storage/Exception.php';
  225. throw new Zend_Mail_Storage_Exception('wont delete INBOX');
  226. }
  227. if ($name == $this->getCurrentFolder()) {
  228. /**
  229. * @see Zend_Mail_Storage_Exception
  230. */
  231. require_once 'Zend/Mail/Storage/Exception.php';
  232. throw new Zend_Mail_Storage_Exception('wont delete selected folder');
  233. }
  234. foreach (array('tmp', 'new', 'cur', '.') as $subdir) {
  235. $dir = $this->_rootdir . '.' . $name . DIRECTORY_SEPARATOR . $subdir;
  236. if (!file_exists($dir)) {
  237. continue;
  238. }
  239. $dh = opendir($dir);
  240. if (!$dh) {
  241. /**
  242. * @see Zend_Mail_Storage_Exception
  243. */
  244. require_once 'Zend/Mail/Storage/Exception.php';
  245. throw new Zend_Mail_Storage_Exception("error opening $subdir");
  246. }
  247. while (($entry = readdir($dh)) !== false) {
  248. if ($entry == '.' || $entry == '..') {
  249. continue;
  250. }
  251. if (!unlink($dir . DIRECTORY_SEPARATOR . $entry)) {
  252. /**
  253. * @see Zend_Mail_Storage_Exception
  254. */
  255. require_once 'Zend/Mail/Storage/Exception.php';
  256. throw new Zend_Mail_Storage_Exception("error cleaning $subdir");
  257. }
  258. }
  259. closedir($dh);
  260. if ($subdir !== '.') {
  261. if (!rmdir($dir)) {
  262. /**
  263. * @see Zend_Mail_Storage_Exception
  264. */
  265. require_once 'Zend/Mail/Storage/Exception.php';
  266. throw new Zend_Mail_Storage_Exception("error removing $subdir");
  267. }
  268. }
  269. }
  270. if (!rmdir($this->_rootdir . '.' . $name)) {
  271. // at least we should try to make it a valid maildir again
  272. mkdir($this->_rootdir . '.' . $name . DIRECTORY_SEPARATOR . 'cur');
  273. /**
  274. * @see Zend_Mail_Storage_Exception
  275. */
  276. require_once 'Zend/Mail/Storage/Exception.php';
  277. throw new Zend_Mail_Storage_Exception("error removing maindir");
  278. }
  279. $parent = strpos($name, $this->_delim) ? substr($name, 0, strrpos($name, $this->_delim)) : null;
  280. $localName = $parent ? substr($name, strlen($parent) + 1) : $name;
  281. unset($this->getFolders($parent)->$localName);
  282. }
  283. /**
  284. * rename and/or move folder
  285. *
  286. * The new name has the same restrictions as in createFolder()
  287. *
  288. * @param string|Zend_Mail_Storage_Folder $oldName name or instance of folder
  289. * @param string $newName new global name of folder
  290. * @return null
  291. * @throws Zend_Mail_Storage_Exception
  292. */
  293. public function renameFolder($oldName, $newName)
  294. {
  295. // TODO: This is also not atomar and has similar problems as removeFolder()
  296. if ($oldName instanceof Zend_Mail_Storage_Folder) {
  297. $oldName = $oldName->getGlobalName();
  298. }
  299. $oldName = trim($oldName, $this->_delim);
  300. if (strpos($oldName, 'INBOX' . $this->_delim) === 0) {
  301. $oldName = substr($oldName, 6);
  302. }
  303. $newName = trim($newName, $this->_delim);
  304. if (strpos($newName, 'INBOX' . $this->_delim) === 0) {
  305. $newName = substr($newName, 6);
  306. }
  307. if (strpos($newName, $oldName . $this->_delim) === 0) {
  308. /**
  309. * @see Zend_Mail_Storage_Exception
  310. */
  311. require_once 'Zend/Mail/Storage/Exception.php';
  312. throw new Zend_Mail_Storage_Exception('new folder cannot be a child of old folder');
  313. }
  314. // check if folder exists and has no children
  315. $folder = $this->getFolders($oldName);
  316. if ($oldName == 'INBOX' || $oldName == DIRECTORY_SEPARATOR || $oldName == '/') {
  317. /**
  318. * @see Zend_Mail_Storage_Exception
  319. */
  320. require_once 'Zend/Mail/Storage/Exception.php';
  321. throw new Zend_Mail_Storage_Exception('wont rename INBOX');
  322. }
  323. if ($oldName == $this->getCurrentFolder()) {
  324. /**
  325. * @see Zend_Mail_Storage_Exception
  326. */
  327. require_once 'Zend/Mail/Storage/Exception.php';
  328. throw new Zend_Mail_Storage_Exception('wont rename selected folder');
  329. }
  330. $newdir = $this->createFolder($newName);
  331. if (!$folder->isLeaf()) {
  332. foreach ($folder as $k => $v) {
  333. $this->renameFolder($v->getGlobalName(), $newName . $this->_delim . $k);
  334. }
  335. }
  336. $olddir = $this->_rootdir . '.' . $folder;
  337. foreach (array('tmp', 'new', 'cur') as $subdir) {
  338. $subdir = DIRECTORY_SEPARATOR . $subdir;
  339. if (!file_exists($olddir . $subdir)) {
  340. continue;
  341. }
  342. // using copy or moving files would be even better - but also much slower
  343. if (!rename($olddir . $subdir, $newdir . $subdir)) {
  344. /**
  345. * @see Zend_Mail_Storage_Exception
  346. */
  347. require_once 'Zend/Mail/Storage/Exception.php';
  348. throw new Zend_Mail_Storage_Exception('error while moving ' . $subdir);
  349. }
  350. }
  351. // create a dummy if removing fails - otherwise we can't read it next time
  352. mkdir($olddir . DIRECTORY_SEPARATOR . 'cur');
  353. $this->removeFolder($oldName);
  354. }
  355. /**
  356. * create a uniqueid for maildir filename
  357. *
  358. * This is nearly the format defined in the maildir standard. The microtime() call should already
  359. * create a uniqueid, the pid is for multicore/-cpu machine that manage to call this function at the
  360. * exact same time, and uname() gives us the hostname for multiple machines accessing the same storage.
  361. *
  362. * If someone disables posix we create a random number of the same size, so this method should also
  363. * work on Windows - if you manage to get maildir working on Windows.
  364. * Microtime could also be disabled, altough I've never seen it.
  365. *
  366. * @return string new uniqueid
  367. */
  368. protected function _createUniqueId()
  369. {
  370. $id = '';
  371. $id .= function_exists('microtime') ? microtime(true) : (time() . ' ' . rand(0, 100000));
  372. $id .= '.' . (function_exists('posix_getpid') ? posix_getpid() : rand(50, 65535));
  373. $id .= '.' . php_uname('n');
  374. return $id;
  375. }
  376. /**
  377. * open a temporary maildir file
  378. *
  379. * makes sure tmp/ exists and create a file with a unique name
  380. * you should close the returned filehandle!
  381. *
  382. * @param string $folder name of current folder without leading .
  383. * @return array array('dirname' => dir of maildir folder, 'uniq' => unique id, 'filename' => name of create file
  384. * 'handle' => file opened for writing)
  385. * @throws Zend_Mail_Storage_Exception
  386. */
  387. protected function _createTmpFile($folder = 'INBOX')
  388. {
  389. if ($folder == 'INBOX') {
  390. $tmpdir = $this->_rootdir . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
  391. } else {
  392. $tmpdir = $this->_rootdir . '.' . $folder . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
  393. }
  394. if (!file_exists($tmpdir)) {
  395. if (!mkdir($tmpdir)) {
  396. /**
  397. * @see Zend_Mail_Storage_Exception
  398. */
  399. require_once 'Zend/Mail/Storage/Exception.php';
  400. throw new Zend_Mail_Storage_Exception('problems creating tmp dir');
  401. }
  402. }
  403. // we should retry to create a unique id if a file with the same name exists
  404. // to avoid a script timeout we only wait 1 second (instead of 2) and stop
  405. // after a defined retry count
  406. // if you change this variable take into account that it can take up to $max_tries seconds
  407. // normally we should have a valid unique name after the first try, we're just following the "standard" here
  408. $max_tries = 5;
  409. for ($i = 0; $i < $max_tries; ++$i) {
  410. $uniq = $this->_createUniqueId();
  411. if (!file_exists($tmpdir . $uniq)) {
  412. // here is the race condition! - as defined in the standard
  413. // to avoid having a long time between stat()ing the file and creating it we're opening it here
  414. // to mark the filename as taken
  415. $fh = fopen($tmpdir . $uniq, 'w');
  416. if (!$fh) {
  417. /**
  418. * @see Zend_Mail_Storage_Exception
  419. */
  420. require_once 'Zend/Mail/Storage/Exception.php';
  421. throw new Zend_Mail_Storage_Exception('could not open temp file');
  422. }
  423. break;
  424. }
  425. sleep(1);
  426. }
  427. if (!$fh) {
  428. /**
  429. * @see Zend_Mail_Storage_Exception
  430. */
  431. require_once 'Zend/Mail/Storage/Exception.php';
  432. throw new Zend_Mail_Storage_Exception("tried $max_tries unique ids for a temp file, but all were taken"
  433. . ' - giving up');
  434. }
  435. return array('dirname' => $this->_rootdir . '.' . $folder, 'uniq' => $uniq, 'filename' => $tmpdir . $uniq,
  436. 'handle' => $fh);
  437. }
  438. /**
  439. * create an info string for filenames with given flags
  440. *
  441. * @param array $flags wanted flags, with the reference you'll get the set flags with correct key (= char for flag)
  442. * @return string info string for version 2 filenames including the leading colon
  443. * @throws Zend_Mail_Storage_Exception
  444. */
  445. protected function _getInfoString(&$flags)
  446. {
  447. // accessing keys is easier, faster and it removes duplicated flags
  448. $wanted_flags = array_flip($flags);
  449. if (isset($wanted_flags[Zend_Mail_Storage::FLAG_RECENT])) {
  450. /**
  451. * @see Zend_Mail_Storage_Exception
  452. */
  453. require_once 'Zend/Mail/Storage/Exception.php';
  454. throw new Zend_Mail_Storage_Exception('recent flag may not be set');
  455. }
  456. $info = ':2,';
  457. $flags = array();
  458. foreach (Zend_Mail_Storage_Maildir::$_knownFlags as $char => $flag) {
  459. if (!isset($wanted_flags[$flag])) {
  460. continue;
  461. }
  462. $info .= $char;
  463. $flags[$char] = $flag;
  464. unset($wanted_flags[$flag]);
  465. }
  466. if (!empty($wanted_flags)) {
  467. $wanted_flags = implode(', ', array_keys($wanted_flags));
  468. /**
  469. * @see Zend_Mail_Storage_Exception
  470. */
  471. require_once 'Zend/Mail/Storage/Exception.php';
  472. throw new Zend_Mail_Storage_Exception('unknown flag(s): ' . $wanted_flags);
  473. }
  474. return $info;
  475. }
  476. /**
  477. * append a new message to mail storage
  478. *
  479. * @param string|stream $message message as string or stream resource
  480. * @param null|string|Zend_Mail_Storage_Folder $folder folder for new message, else current folder is taken
  481. * @param null|array $flags set flags for new message, else a default set is used
  482. * @param bool $recent handle this mail as if recent flag has been set,
  483. * should only be used in delivery
  484. * @throws Zend_Mail_Storage_Exception
  485. */
  486. // not yet * @param string|Zend_Mail_Message|Zend_Mime_Message $message message as string or instance of message class
  487. public function appendMessage($message, $folder = null, $flags = null, $recent = false)
  488. {
  489. if ($this->_quota && $this->checkQuota()) {
  490. /**
  491. * @see Zend_Mail_Storage_Exception
  492. */
  493. require_once 'Zend/Mail/Storage/Exception.php';
  494. throw new Zend_Mail_Storage_Exception('storage is over quota!');
  495. }
  496. if ($folder === null) {
  497. $folder = $this->_currentFolder;
  498. }
  499. if (!($folder instanceof Zend_Mail_Storage_Folder)) {
  500. $folder = $this->getFolders($folder);
  501. }
  502. if ($flags === null) {
  503. $flags = array(Zend_Mail_Storage::FLAG_SEEN);
  504. }
  505. $info = $this->_getInfoString($flags);
  506. $temp_file = $this->_createTmpFile($folder->getGlobalName());
  507. // TODO: handle class instances for $message
  508. if (is_resource($message) && get_resource_type($message) == 'stream') {
  509. stream_copy_to_stream($message, $temp_file['handle']);
  510. } else {
  511. fputs($temp_file['handle'], $message);
  512. }
  513. fclose($temp_file['handle']);
  514. // we're adding the size to the filename for maildir++
  515. $size = filesize($temp_file['filename']);
  516. if ($size !== false) {
  517. $info = ',S=' . $size . $info;
  518. }
  519. $new_filename = $temp_file['dirname'] . DIRECTORY_SEPARATOR;
  520. $new_filename .= $recent ? 'new' : 'cur';
  521. $new_filename .= DIRECTORY_SEPARATOR . $temp_file['uniq'] . $info;
  522. // we're throwing any exception after removing our temp file and saving it to this variable instead
  523. $exception = null;
  524. if (!link($temp_file['filename'], $new_filename)) {
  525. /**
  526. * @see Zend_Mail_Storage_Exception
  527. */
  528. require_once 'Zend/Mail/Storage/Exception.php';
  529. $exception = new Zend_Mail_Storage_Exception('cannot link message file to final dir');
  530. }
  531. @unlink($temp_file['filename']);
  532. if ($exception) {
  533. throw $exception;
  534. }
  535. $this->_files[] = array('uniq' => $temp_file['uniq'],
  536. 'flags' => $flags,
  537. 'filename' => $new_filename);
  538. if ($this->_quota) {
  539. $this->_addQuotaEntry((int)$size, 1);
  540. }
  541. }
  542. /**
  543. * copy an existing message
  544. *
  545. * @param int $id number of message
  546. * @param string|Zend_Mail_Storage_Folder $folder name or instance of targer folder
  547. * @return null
  548. * @throws Zend_Mail_Storage_Exception
  549. */
  550. public function copyMessage($id, $folder)
  551. {
  552. if ($this->_quota && $this->checkQuota()) {
  553. /**
  554. * @see Zend_Mail_Storage_Exception
  555. */
  556. require_once 'Zend/Mail/Storage/Exception.php';
  557. throw new Zend_Mail_Storage_Exception('storage is over quota!');
  558. }
  559. if (!($folder instanceof Zend_Mail_Storage_Folder)) {
  560. $folder = $this->getFolders($folder);
  561. }
  562. $filedata = $this->_getFileData($id);
  563. $old_file = $filedata['filename'];
  564. $flags = $filedata['flags'];
  565. // copied message can't be recent
  566. while (($key = array_search(Zend_Mail_Storage::FLAG_RECENT, $flags)) !== false) {
  567. unset($flags[$key]);
  568. }
  569. $info = $this->_getInfoString($flags);
  570. // we're creating the copy as temp file before moving to cur/
  571. $temp_file = $this->_createTmpFile($folder->getGlobalName());
  572. // we don't write directly to the file
  573. fclose($temp_file['handle']);
  574. // we're adding the size to the filename for maildir++
  575. $size = filesize($old_file);
  576. if ($size !== false) {
  577. $info = ',S=' . $size . $info;
  578. }
  579. $new_file = $temp_file['dirname'] . DIRECTORY_SEPARATOR . 'cur' . DIRECTORY_SEPARATOR . $temp_file['uniq'] . $info;
  580. // we're throwing any exception after removing our temp file and saving it to this variable instead
  581. $exception = null;
  582. if (!copy($old_file, $temp_file['filename'])) {
  583. /**
  584. * @see Zend_Mail_Storage_Exception
  585. */
  586. require_once 'Zend/Mail/Storage/Exception.php';
  587. $exception = new Zend_Mail_Storage_Exception('cannot copy message file');
  588. } else if (!link($temp_file['filename'], $new_file)) {
  589. /**
  590. * @see Zend_Mail_Storage_Exception
  591. */
  592. require_once 'Zend/Mail/Storage/Exception.php';
  593. $exception = new Zend_Mail_Storage_Exception('cannot link message file to final dir');
  594. }
  595. @unlink($temp_file['filename']);
  596. if ($exception) {
  597. throw $exception;
  598. }
  599. if ($folder->getGlobalName() == $this->_currentFolder
  600. || ($this->_currentFolder == 'INBOX' && $folder->getGlobalName() == '/')) {
  601. $this->_files[] = array('uniq' => $temp_file['uniq'],
  602. 'flags' => $flags,
  603. 'filename' => $new_file);
  604. }
  605. if ($this->_quota) {
  606. $this->_addQuotaEntry((int)$size, 1);
  607. }
  608. }
  609. /**
  610. * move an existing message
  611. *
  612. * @param int $id number of message
  613. * @param string|Zend_Mail_Storage_Folder $folder name or instance of targer folder
  614. * @return null
  615. * @throws Zend_Mail_Storage_Exception
  616. */
  617. public function moveMessage($id, $folder) {
  618. if (!($folder instanceof Zend_Mail_Storage_Folder)) {
  619. $folder = $this->getFolders($folder);
  620. }
  621. if ($folder->getGlobalName() == $this->_currentFolder
  622. || ($this->_currentFolder == 'INBOX' && $folder->getGlobalName() == '/')) {
  623. /**
  624. * @see Zend_Mail_Storage_Exception
  625. */
  626. require_once 'Zend/Mail/Storage/Exception.php';
  627. throw new Zend_Mail_Storage_Exception('target is current folder');
  628. }
  629. $filedata = $this->_getFileData($id);
  630. $old_file = $filedata['filename'];
  631. $flags = $filedata['flags'];
  632. // moved message can't be recent
  633. while (($key = array_search(Zend_Mail_Storage::FLAG_RECENT, $flags)) !== false) {
  634. unset($flags[$key]);
  635. }
  636. $info = $this->_getInfoString($flags);
  637. // reserving a new name
  638. $temp_file = $this->_createTmpFile($folder->getGlobalName());
  639. fclose($temp_file['handle']);
  640. // we're adding the size to the filename for maildir++
  641. $size = filesize($old_file);
  642. if ($size !== false) {
  643. $info = ',S=' . $size . $info;
  644. }
  645. $new_file = $temp_file['dirname'] . DIRECTORY_SEPARATOR . 'cur' . DIRECTORY_SEPARATOR . $temp_file['uniq'] . $info;
  646. // we're throwing any exception after removing our temp file and saving it to this variable instead
  647. $exception = null;
  648. if (!rename($old_file, $new_file)) {
  649. /**
  650. * @see Zend_Mail_Storage_Exception
  651. */
  652. require_once 'Zend/Mail/Storage/Exception.php';
  653. $exception = new Zend_Mail_Storage_Exception('cannot move message file');
  654. }
  655. @unlink($temp_file['filename']);
  656. if ($exception) {
  657. throw $exception;
  658. }
  659. unset($this->_files[$id - 1]);
  660. // remove the gap
  661. $this->_files = array_values($this->_files);
  662. }
  663. /**
  664. * set flags for message
  665. *
  666. * NOTE: this method can't set the recent flag.
  667. *
  668. * @param int $id number of message
  669. * @param array $flags new flags for message
  670. * @throws Zend_Mail_Storage_Exception
  671. */
  672. public function setFlags($id, $flags)
  673. {
  674. $info = $this->_getInfoString($flags);
  675. $filedata = $this->_getFileData($id);
  676. // NOTE: double dirname to make sure we always move to cur. if recent flag has been set (message is in new) it will be moved to cur.
  677. $new_filename = dirname(dirname($filedata['filename'])) . DIRECTORY_SEPARATOR . 'cur' . DIRECTORY_SEPARATOR . "$filedata[uniq]$info";
  678. if (!@rename($filedata['filename'], $new_filename)) {
  679. /**
  680. * @see Zend_Mail_Storage_Exception
  681. */
  682. require_once 'Zend/Mail/Storage/Exception.php';
  683. throw new Zend_Mail_Storage_Exception('cannot rename file');
  684. }
  685. $filedata['flags'] = $flags;
  686. $filedata['filename'] = $new_filename;
  687. $this->_files[$id - 1] = $filedata;
  688. }
  689. /**
  690. * stub for not supported message deletion
  691. *
  692. * @return null
  693. * @throws Zend_Mail_Storage_Exception
  694. */
  695. public function removeMessage($id)
  696. {
  697. $filename = $this->_getFileData($id, 'filename');
  698. if ($this->_quota) {
  699. $size = filesize($filename);
  700. }
  701. if (!@unlink($filename)) {
  702. /**
  703. * @see Zend_Mail_Storage_Exception
  704. */
  705. require_once 'Zend/Mail/Storage/Exception.php';
  706. throw new Zend_Mail_Storage_Exception('cannot remove message');
  707. }
  708. unset($this->_files[$id - 1]);
  709. // remove the gap
  710. $this->_files = array_values($this->_files);
  711. if ($this->_quota) {
  712. $this->_addQuotaEntry(0 - (int)$size, -1);
  713. }
  714. }
  715. /**
  716. * enable/disable quota and set a quota value if wanted or needed
  717. *
  718. * You can enable/disable quota with true/false. If you don't have
  719. * a MDA or want to enforce a quota value you can also set this value
  720. * here. Use array('size' => SIZE_QUOTA, 'count' => MAX_MESSAGE) do
  721. * define your quota. Order of these fields does matter!
  722. *
  723. * @param bool|array $value new quota value
  724. * @return null
  725. */
  726. public function setQuota($value) {
  727. $this->_quota = $value;
  728. }
  729. /**
  730. * get currently set quota
  731. *
  732. * @see Zend_Mail_Storage_Writable_Maildir::setQuota()
  733. *
  734. * @return bool|array
  735. */
  736. public function getQuota($fromStorage = false) {
  737. if ($fromStorage) {
  738. $fh = @fopen($this->_rootdir . 'maildirsize', 'r');
  739. if (!$fh) {
  740. /**
  741. * @see Zend_Mail_Storage_Exception
  742. */
  743. require_once 'Zend/Mail/Storage/Exception.php';
  744. throw new Zend_Mail_Storage_Exception('cannot open maildirsize');
  745. }
  746. $definition = fgets($fh);
  747. fclose($fh);
  748. $definition = explode(',', trim($definition));
  749. $quota = array();
  750. foreach ($definition as $member) {
  751. $key = $member[strlen($member) - 1];
  752. if ($key == 'S' || $key == 'C') {
  753. $key = $key == 'C' ? 'count' : 'size';
  754. }
  755. $quota[$key] = substr($member, 0, -1);
  756. }
  757. return $quota;
  758. }
  759. return $this->_quota;
  760. }
  761. /**
  762. * @see http://www.inter7.com/courierimap/README.maildirquota.html "Calculating maildirsize"
  763. */
  764. protected function _calculateMaildirsize() {
  765. $timestamps = array();
  766. $messages = 0;
  767. $total_size = 0;
  768. if (is_array($this->_quota)) {
  769. $quota = $this->_quota;
  770. } else {
  771. try {
  772. $quota = $this->getQuota(true);
  773. } catch (Zend_Mail_Storage_Exception $e) {
  774. throw new Zend_Mail_Storage_Exception('no quota definition found', 0, $e);
  775. }
  776. }
  777. $folders = new RecursiveIteratorIterator($this->getFolders(), RecursiveIteratorIterator::SELF_FIRST);
  778. foreach ($folders as $folder) {
  779. $subdir = $folder->getGlobalName();
  780. if ($subdir == 'INBOX') {
  781. $subdir = '';
  782. } else {
  783. $subdir = '.' . $subdir;
  784. }
  785. if ($subdir == 'Trash') {
  786. continue;
  787. }
  788. foreach (array('cur', 'new') as $subsubdir) {
  789. $dirname = $this->_rootdir . $subdir . DIRECTORY_SEPARATOR . $subsubdir . DIRECTORY_SEPARATOR;
  790. if (!file_exists($dirname)) {
  791. continue;
  792. }
  793. // NOTE: we are using mtime instead of "the latest timestamp". The latest would be atime
  794. // and as we are accessing the directory it would make the whole calculation useless.
  795. $timestamps[$dirname] = filemtime($dirname);
  796. $dh = opendir($dirname);
  797. // NOTE: Should have been checked in constructor. Not throwing an exception here, quotas will
  798. // therefore not be fully enforeced, but next request will fail anyway, if problem persists.
  799. if (!$dh) {
  800. continue;
  801. }
  802. while (($entry = readdir()) !== false) {
  803. if ($entry[0] == '.' || !is_file($dirname . $entry)) {
  804. continue;
  805. }
  806. if (strpos($entry, ',S=')) {
  807. strtok($entry, '=');
  808. $filesize = strtok(':');
  809. if (is_numeric($filesize)) {
  810. $total_size += $filesize;
  811. ++$messages;
  812. continue;
  813. }
  814. }
  815. $size = filesize($dirname . $entry);
  816. if ($size === false) {
  817. // ignore, as we assume file got removed
  818. continue;
  819. }
  820. $total_size += $size;
  821. ++$messages;
  822. }
  823. }
  824. }
  825. $tmp = $this->_createTmpFile();
  826. $fh = $tmp['handle'];
  827. $definition = array();
  828. foreach ($quota as $type => $value) {
  829. if ($type == 'size' || $type == 'count') {
  830. $type = $type == 'count' ? 'C' : 'S';
  831. }
  832. $definition[] = $value . $type;
  833. }
  834. $definition = implode(',', $definition);
  835. fputs($fh, "$definition\n");
  836. fputs($fh, "$total_size $messages\n");
  837. fclose($fh);
  838. rename($tmp['filename'], $this->_rootdir . 'maildirsize');
  839. foreach ($timestamps as $dir => $timestamp) {
  840. if ($timestamp < filemtime($dir)) {
  841. unlink($this->_rootdir . 'maildirsize');
  842. break;
  843. }
  844. }
  845. return array('size' => $total_size, 'count' => $messages, 'quota' => $quota);
  846. }
  847. /**
  848. * @see http://www.inter7.com/courierimap/README.maildirquota.html "Calculating the quota for a Maildir++"
  849. */
  850. protected function _calculateQuota($forceRecalc = false) {
  851. $fh = null;
  852. $total_size = 0;
  853. $messages = 0;
  854. $maildirsize = '';
  855. if (!$forceRecalc && file_exists($this->_rootdir . 'maildirsize') && filesize($this->_rootdir . 'maildirsize') < 5120) {
  856. $fh = fopen($this->_rootdir . 'maildirsize', 'r');
  857. }
  858. if ($fh) {
  859. $maildirsize = fread($fh, 5120);
  860. if (strlen($maildirsize) >= 5120) {
  861. fclose($fh);
  862. $fh = null;
  863. $maildirsize = '';
  864. }
  865. }
  866. if (!$fh) {
  867. $result = $this->_calculateMaildirsize();
  868. $total_size = $result['size'];
  869. $messages = $result['count'];
  870. $quota = $result['quota'];
  871. } else {
  872. $maildirsize = explode("\n", $maildirsize);
  873. if (is_array($this->_quota)) {
  874. $quota = $this->_quota;
  875. } else {
  876. $definition = explode(',', $maildirsize[0]);
  877. $quota = array();
  878. foreach ($definition as $member) {
  879. $key = $member[strlen($member) - 1];
  880. if ($key == 'S' || $key == 'C') {
  881. $key = $key == 'C' ? 'count' : 'size';
  882. }
  883. $quota[$key] = substr($member, 0, -1);
  884. }
  885. }
  886. unset($maildirsize[0]);
  887. foreach ($maildirsize as $line) {
  888. list($size, $count) = explode(' ', trim($line));
  889. $total_size += $size;
  890. $messages += $count;
  891. }
  892. }
  893. $over_quota = false;
  894. $over_quota = $over_quota || (isset($quota['size']) && $total_size > $quota['size']);
  895. $over_quota = $over_quota || (isset($quota['count']) && $messages > $quota['count']);
  896. // NOTE: $maildirsize equals false if it wasn't set (AKA we recalculated) or it's only
  897. // one line, because $maildirsize[0] gets unsetted.
  898. // Also we're using local time to calculate the 15 minute offset. Touching a file just for known the
  899. // local time of the file storage isn't worth the hassle.
  900. if ($over_quota && ($maildirsize || filemtime($this->_rootdir . 'maildirsize') > time() - 900)) {
  901. $result = $this->_calculateMaildirsize();
  902. $total_size = $result['size'];
  903. $messages = $result['count'];
  904. $quota = $result['quota'];
  905. $over_quota = false;
  906. $over_quota = $over_quota || (isset($quota['size']) && $total_size > $quota['size']);
  907. $over_quota = $over_quota || (isset($quota['count']) && $messages > $quota['count']);
  908. }
  909. if ($fh) {
  910. // TODO is there a safe way to keep the handle open for writing?
  911. fclose($fh);
  912. }
  913. return array('size' => $total_size, 'count' => $messages, 'quota' => $quota, 'over_quota' => $over_quota);
  914. }
  915. protected function _addQuotaEntry($size, $count = 1) {
  916. if (!file_exists($this->_rootdir . 'maildirsize')) {
  917. // TODO: should get file handler from _calculateQuota
  918. }
  919. $size = (int)$size;
  920. $count = (int)$count;
  921. file_put_contents($this->_rootdir . 'maildirsize', "$size $count\n", FILE_APPEND);
  922. }
  923. /**
  924. * check if storage is currently over quota
  925. *
  926. * @param bool $detailedResponse return known data of quota and current size and message count @see _calculateQuota()
  927. * @return bool|array over quota state or detailed response
  928. */
  929. public function checkQuota($detailedResponse = false, $forceRecalc = false) {
  930. $result = $this->_calculateQuota($forceRecalc);
  931. return $detailedResponse ? $result : $result['over_quota'];
  932. }
  933. }