PageRenderTime 62ms CodeModel.GetById 12ms app.highlight 39ms RepoModel.GetById 1ms app.codeStats 0ms

/framework/vendor/zend/Zend/Mail/Storage/Writable/Maildir.php

http://zoop.googlecode.com/
PHP | 1049 lines | 595 code | 116 blank | 338 comment | 179 complexity | 7aede92ff40f9dbabeeb3f6edc77f842 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-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 */
  27require_once 'Zend/Mail/Storage/Folder/Maildir.php';
  28
  29/**
  30 * @see Zend_Mail_Storage_Writable_Interface
  31 */
  32require_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 */
  42class 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}