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