|
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-2010 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 20096 2010-01-06 02:05:09Z bkarwin $ |
|
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-2010 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 $params array 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 } |