File indexing completed on 2024-12-29 05:27:49

0001 <?php
0002 /**
0003  * Zend Framework
0004  *
0005  * LICENSE
0006  *
0007  * This source file is subject to the new BSD license that is bundled
0008  * with this package in the file LICENSE.txt.
0009  * It is also available through the world-wide-web at this URL:
0010  * http://framework.zend.com/license/new-bsd
0011  * If you did not receive a copy of the license and are unable to
0012  * obtain it through the world-wide-web, please send an email
0013  * to license@zend.com so we can send you a copy immediately.
0014  *
0015  * @category   Zend
0016  * @package    Zend_Mail
0017  * @subpackage Storage
0018  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0019  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0020  * @version    $Id$
0021  */
0022 
0023 
0024 /**
0025  * @see Zend_Mail_Storage_Folder_Maildir
0026  */
0027 // require_once 'Zend/Mail/Storage/Folder/Maildir.php';
0028 
0029 /**
0030  * @see Zend_Mail_Storage_Writable_Interface
0031  */
0032 // require_once 'Zend/Mail/Storage/Writable/Interface.php';
0033 
0034 
0035 /**
0036  * @category   Zend
0037  * @package    Zend_Mail
0038  * @subpackage Storage
0039  * @copyright  Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
0040  * @license    http://framework.zend.com/license/new-bsd     New BSD License
0041  */
0042 class Zend_Mail_Storage_Writable_Maildir extends    Zend_Mail_Storage_Folder_Maildir
0043                                          implements Zend_Mail_Storage_Writable_Interface
0044 {
0045     // TODO: init maildir (+ constructor option create if not found)
0046 
0047     /**
0048      * use quota and size of quota if given
0049      * @var bool|int
0050      */
0051     protected $_quota;
0052 
0053     /**
0054      * create a new maildir
0055      *
0056      * If the given dir is already a valid maildir this will not fail.
0057      *
0058      * @param string $dir directory for the new maildir (may already exist)
0059      * @return null
0060      * @throws Zend_Mail_Storage_Exception
0061      */
0062     public static function initMaildir($dir)
0063     {
0064         if (file_exists($dir)) {
0065             if (!is_dir($dir)) {
0066                 /**
0067                  * @see Zend_Mail_Storage_Exception
0068                  */
0069                 // require_once 'Zend/Mail/Storage/Exception.php';
0070                 throw new Zend_Mail_Storage_Exception('maildir must be a directory if already exists');
0071             }
0072         } else {
0073             if (!mkdir($dir)) {
0074                 /**
0075                  * @see Zend_Mail_Storage_Exception
0076                  */
0077                 // require_once 'Zend/Mail/Storage/Exception.php';
0078                 $dir = dirname($dir);
0079                 if (!file_exists($dir)) {
0080                     throw new Zend_Mail_Storage_Exception("parent $dir not found");
0081                 } else if (!is_dir($dir)) {
0082                     throw new Zend_Mail_Storage_Exception("parent $dir not a directory");
0083                 } else {
0084                     throw new Zend_Mail_Storage_Exception('cannot create maildir');
0085                 }
0086             }
0087         }
0088 
0089         foreach (array('cur', 'tmp', 'new') as $subdir) {
0090             if (!@mkdir($dir . DIRECTORY_SEPARATOR . $subdir)) {
0091                 // ignore if dir exists (i.e. was already valid maildir or two processes try to create one)
0092                 if (!file_exists($dir . DIRECTORY_SEPARATOR . $subdir)) {
0093                     /**
0094                      * @see Zend_Mail_Storage_Exception
0095                      */
0096                     // require_once 'Zend/Mail/Storage/Exception.php';
0097                     throw new Zend_Mail_Storage_Exception('could not create subdir ' . $subdir);
0098                 }
0099             }
0100         }
0101     }
0102 
0103     /**
0104      * Create instance with parameters
0105      * Additional parameters are (see parent for more):
0106      *   - create if true a new maildir is create if none exists
0107      *
0108      * @param array $params mail reader specific parameters
0109      * @throws Zend_Mail_Storage_Exception
0110      */
0111     public function __construct($params) {
0112         if (is_array($params)) {
0113             $params = (object)$params;
0114         }
0115 
0116         if (!empty($params->create) && isset($params->dirname) && !file_exists($params->dirname . DIRECTORY_SEPARATOR . 'cur')) {
0117             self::initMaildir($params->dirname);
0118         }
0119 
0120         parent::__construct($params);
0121     }
0122 
0123     /**
0124      * create a new folder
0125      *
0126      * This method also creates parent folders if necessary. Some mail storages may restrict, which folder
0127      * may be used as parent or which chars may be used in the folder name
0128      *
0129      * @param   string                          $name         global name of folder, local name if $parentFolder is set
0130      * @param   string|Zend_Mail_Storage_Folder $parentFolder parent folder for new folder, else root folder is parent
0131      * @return  string only used internally (new created maildir)
0132      * @throws  Zend_Mail_Storage_Exception
0133      */
0134     public function createFolder($name, $parentFolder = null)
0135     {
0136         if ($parentFolder instanceof Zend_Mail_Storage_Folder) {
0137             $folder = $parentFolder->getGlobalName() . $this->_delim . $name;
0138         } else if ($parentFolder != null) {
0139             $folder = rtrim($parentFolder, $this->_delim) . $this->_delim . $name;
0140         } else {
0141             $folder = $name;
0142         }
0143 
0144         $folder = trim($folder, $this->_delim);
0145 
0146         // first we check if we try to create a folder that does exist
0147         $exists = null;
0148         try {
0149             $exists = $this->getFolders($folder);
0150         } catch (Zend_Mail_Exception $e) {
0151             // ok
0152         }
0153         if ($exists) {
0154             /**
0155              * @see Zend_Mail_Storage_Exception
0156              */
0157             // require_once 'Zend/Mail/Storage/Exception.php';
0158             throw new Zend_Mail_Storage_Exception('folder already exists');
0159         }
0160 
0161         if (strpos($folder, $this->_delim . $this->_delim) !== false) {
0162             /**
0163              * @see Zend_Mail_Storage_Exception
0164              */
0165             // require_once 'Zend/Mail/Storage/Exception.php';
0166             throw new Zend_Mail_Storage_Exception('invalid name - folder parts may not be empty');
0167         }
0168 
0169         if (strpos($folder, 'INBOX' . $this->_delim) === 0) {
0170             $folder = substr($folder, 6);
0171         }
0172 
0173         $fulldir = $this->_rootdir . '.' . $folder;
0174 
0175         // check if we got tricked and would create a dir outside of the rootdir or not as direct child
0176         if (strpos($folder, DIRECTORY_SEPARATOR) !== false || strpos($folder, '/') !== false
0177             || dirname($fulldir) . DIRECTORY_SEPARATOR != $this->_rootdir) {
0178             /**
0179              * @see Zend_Mail_Storage_Exception
0180              */
0181             // require_once 'Zend/Mail/Storage/Exception.php';
0182             throw new Zend_Mail_Storage_Exception('invalid name - no directory seprator allowed in folder name');
0183         }
0184 
0185         // has a parent folder?
0186         $parent = null;
0187         if (strpos($folder, $this->_delim)) {
0188             // let's see if the parent folder exists
0189             $parent = substr($folder, 0, strrpos($folder, $this->_delim));
0190             try {
0191                 $this->getFolders($parent);
0192             } catch (Zend_Mail_Exception $e) {
0193                 // does not - create parent folder
0194                 $this->createFolder($parent);
0195             }
0196         }
0197 
0198         if (!@mkdir($fulldir) || !@mkdir($fulldir . DIRECTORY_SEPARATOR . 'cur')) {
0199             /**
0200              * @see Zend_Mail_Storage_Exception
0201              */
0202             // require_once 'Zend/Mail/Storage/Exception.php';
0203             throw new Zend_Mail_Storage_Exception('error while creating new folder, may be created incompletly');
0204         }
0205 
0206         mkdir($fulldir . DIRECTORY_SEPARATOR . 'new');
0207         mkdir($fulldir . DIRECTORY_SEPARATOR . 'tmp');
0208 
0209         $localName = $parent ? substr($folder, strlen($parent) + 1) : $folder;
0210         $this->getFolders($parent)->$localName = new Zend_Mail_Storage_Folder($localName, $folder, true);
0211 
0212         return $fulldir;
0213     }
0214 
0215     /**
0216      * remove a folder
0217      *
0218      * @param   string|Zend_Mail_Storage_Folder $name      name or instance of folder
0219      * @return  null
0220      * @throws  Zend_Mail_Storage_Exception
0221      */
0222     public function removeFolder($name)
0223     {
0224         // TODO: This could fail in the middle of the task, which is not optimal.
0225         // But there is no defined standard way to mark a folder as removed and there is no atomar fs-op
0226         // to remove a directory. Also moving the folder to a/the trash folder is not possible, as
0227         // all parent folders must be created. What we could do is add a dash to the front of the
0228         // directory name and it should be ignored as long as other processes obey the standard.
0229 
0230         if ($name instanceof Zend_Mail_Storage_Folder) {
0231             $name = $name->getGlobalName();
0232         }
0233 
0234         $name = trim($name, $this->_delim);
0235         if (strpos($name, 'INBOX' . $this->_delim) === 0) {
0236             $name = substr($name, 6);
0237         }
0238 
0239         // check if folder exists and has no children
0240         if (!$this->getFolders($name)->isLeaf()) {
0241             /**
0242              * @see Zend_Mail_Storage_Exception
0243              */
0244             // require_once 'Zend/Mail/Storage/Exception.php';
0245             throw new Zend_Mail_Storage_Exception('delete children first');
0246         }
0247 
0248         if ($name == 'INBOX' || $name == DIRECTORY_SEPARATOR || $name == '/') {
0249             /**
0250              * @see Zend_Mail_Storage_Exception
0251              */
0252             // require_once 'Zend/Mail/Storage/Exception.php';
0253             throw new Zend_Mail_Storage_Exception('wont delete INBOX');
0254         }
0255 
0256         if ($name == $this->getCurrentFolder()) {
0257             /**
0258              * @see Zend_Mail_Storage_Exception
0259              */
0260             // require_once 'Zend/Mail/Storage/Exception.php';
0261             throw new Zend_Mail_Storage_Exception('wont delete selected folder');
0262         }
0263 
0264         foreach (array('tmp', 'new', 'cur', '.') as $subdir) {
0265             $dir = $this->_rootdir . '.' . $name . DIRECTORY_SEPARATOR . $subdir;
0266             if (!file_exists($dir)) {
0267                 continue;
0268             }
0269             $dh = opendir($dir);
0270             if (!$dh) {
0271                 /**
0272                  * @see Zend_Mail_Storage_Exception
0273                  */
0274                 // require_once 'Zend/Mail/Storage/Exception.php';
0275                 throw new Zend_Mail_Storage_Exception("error opening $subdir");
0276             }
0277             while (($entry = readdir($dh)) !== false) {
0278                 if ($entry == '.' || $entry == '..') {
0279                     continue;
0280                 }
0281                 if (!unlink($dir . DIRECTORY_SEPARATOR . $entry)) {
0282                     /**
0283                      * @see Zend_Mail_Storage_Exception
0284                      */
0285                     // require_once 'Zend/Mail/Storage/Exception.php';
0286                     throw new Zend_Mail_Storage_Exception("error cleaning $subdir");
0287                 }
0288             }
0289             closedir($dh);
0290             if ($subdir !== '.') {
0291                 if (!rmdir($dir)) {
0292                     /**
0293                      * @see Zend_Mail_Storage_Exception
0294                      */
0295                     // require_once 'Zend/Mail/Storage/Exception.php';
0296                     throw new Zend_Mail_Storage_Exception("error removing $subdir");
0297                 }
0298             }
0299         }
0300 
0301         if (!rmdir($this->_rootdir . '.' . $name)) {
0302             // at least we should try to make it a valid maildir again
0303             mkdir($this->_rootdir . '.' . $name . DIRECTORY_SEPARATOR . 'cur');
0304             /**
0305              * @see Zend_Mail_Storage_Exception
0306              */
0307             // require_once 'Zend/Mail/Storage/Exception.php';
0308             throw new Zend_Mail_Storage_Exception("error removing maindir");
0309         }
0310 
0311         $parent = strpos($name, $this->_delim) ? substr($name, 0, strrpos($name, $this->_delim)) : null;
0312         $localName = $parent ? substr($name, strlen($parent) + 1) : $name;
0313         unset($this->getFolders($parent)->$localName);
0314     }
0315 
0316     /**
0317      * rename and/or move folder
0318      *
0319      * The new name has the same restrictions as in createFolder()
0320      *
0321      * @param   string|Zend_Mail_Storage_Folder $oldName name or instance of folder
0322      * @param   string                          $newName new global name of folder
0323      * @return  null
0324      * @throws  Zend_Mail_Storage_Exception
0325      */
0326     public function renameFolder($oldName, $newName)
0327     {
0328         // TODO: This is also not atomar and has similar problems as removeFolder()
0329 
0330         if ($oldName instanceof Zend_Mail_Storage_Folder) {
0331             $oldName = $oldName->getGlobalName();
0332         }
0333 
0334         $oldName = trim($oldName, $this->_delim);
0335         if (strpos($oldName, 'INBOX' . $this->_delim) === 0) {
0336             $oldName = substr($oldName, 6);
0337         }
0338 
0339         $newName = trim($newName, $this->_delim);
0340         if (strpos($newName, 'INBOX' . $this->_delim) === 0) {
0341             $newName = substr($newName, 6);
0342         }
0343 
0344         if (strpos($newName, $oldName . $this->_delim) === 0) {
0345             /**
0346              * @see Zend_Mail_Storage_Exception
0347              */
0348             // require_once 'Zend/Mail/Storage/Exception.php';
0349             throw new Zend_Mail_Storage_Exception('new folder cannot be a child of old folder');
0350         }
0351 
0352         // check if folder exists and has no children
0353         $folder = $this->getFolders($oldName);
0354 
0355         if ($oldName == 'INBOX' || $oldName == DIRECTORY_SEPARATOR || $oldName == '/') {
0356             /**
0357              * @see Zend_Mail_Storage_Exception
0358              */
0359             // require_once 'Zend/Mail/Storage/Exception.php';
0360             throw new Zend_Mail_Storage_Exception('wont rename INBOX');
0361         }
0362 
0363         if ($oldName == $this->getCurrentFolder()) {
0364             /**
0365              * @see Zend_Mail_Storage_Exception
0366              */
0367             // require_once 'Zend/Mail/Storage/Exception.php';
0368             throw new Zend_Mail_Storage_Exception('wont rename selected folder');
0369         }
0370 
0371         $newdir = $this->createFolder($newName);
0372 
0373         if (!$folder->isLeaf()) {
0374             foreach ($folder as $k => $v) {
0375                 $this->renameFolder($v->getGlobalName(), $newName . $this->_delim . $k);
0376             }
0377         }
0378 
0379         $olddir = $this->_rootdir . '.' . $folder;
0380         foreach (array('tmp', 'new', 'cur') as $subdir) {
0381             $subdir = DIRECTORY_SEPARATOR . $subdir;
0382             if (!file_exists($olddir . $subdir)) {
0383                 continue;
0384             }
0385             // using copy or moving files would be even better - but also much slower
0386             if (!rename($olddir . $subdir, $newdir . $subdir)) {
0387                 /**
0388                  * @see Zend_Mail_Storage_Exception
0389                  */
0390                 // require_once 'Zend/Mail/Storage/Exception.php';
0391                 throw new Zend_Mail_Storage_Exception('error while moving ' . $subdir);
0392             }
0393         }
0394         // create a dummy if removing fails - otherwise we can't read it next time
0395         mkdir($olddir . DIRECTORY_SEPARATOR . 'cur');
0396         $this->removeFolder($oldName);
0397     }
0398 
0399     /**
0400      * create a uniqueid for maildir filename
0401      *
0402      * This is nearly the format defined in the maildir standard. The microtime() call should already
0403      * create a uniqueid, the pid is for multicore/-cpu machine that manage to call this function at the
0404      * exact same time, and uname() gives us the hostname for multiple machines accessing the same storage.
0405      *
0406      * If someone disables posix we create a random number of the same size, so this method should also
0407      * work on Windows - if you manage to get maildir working on Windows.
0408      * Microtime could also be disabled, altough I've never seen it.
0409      *
0410      * @return string new uniqueid
0411      */
0412     protected function _createUniqueId()
0413     {
0414         $id = '';
0415         $id .= function_exists('microtime') ? microtime(true) : (time() . ' ' . rand(0, 100000));
0416         $id .= '.' . (function_exists('posix_getpid') ? posix_getpid() : rand(50, 65535));
0417         $id .= '.' . php_uname('n');
0418 
0419         return $id;
0420     }
0421 
0422     /**
0423      * open a temporary maildir file
0424      *
0425      * makes sure tmp/ exists and create a file with a unique name
0426      * you should close the returned filehandle!
0427      *
0428      * @param   string $folder name of current folder without leading .
0429      * @return  array array('dirname' => dir of maildir folder, 'uniq' => unique id, 'filename' => name of create file
0430      *                     'handle'  => file opened for writing)
0431      * @throws  Zend_Mail_Storage_Exception
0432      */
0433     protected function _createTmpFile($folder = 'INBOX')
0434     {
0435         if ($folder == 'INBOX') {
0436             $tmpdir = $this->_rootdir . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
0437         } else {
0438             $tmpdir = $this->_rootdir . '.' . $folder . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
0439         }
0440         if (!file_exists($tmpdir)) {
0441             if (!mkdir($tmpdir)) {
0442                 /**
0443                  * @see Zend_Mail_Storage_Exception
0444                  */
0445                 // require_once 'Zend/Mail/Storage/Exception.php';
0446                 throw new Zend_Mail_Storage_Exception('problems creating tmp dir');
0447             }
0448         }
0449 
0450         // we should retry to create a unique id if a file with the same name exists
0451         // to avoid a script timeout we only wait 1 second (instead of 2) and stop
0452         // after a defined retry count
0453         // if you change this variable take into account that it can take up to $max_tries seconds
0454         // normally we should have a valid unique name after the first try, we're just following the "standard" here
0455         $max_tries = 5;
0456         for ($i = 0; $i < $max_tries; ++$i) {
0457             $uniq = $this->_createUniqueId();
0458             if (!file_exists($tmpdir . $uniq)) {
0459                 // here is the race condition! - as defined in the standard
0460                 // to avoid having a long time between stat()ing the file and creating it we're opening it here
0461                 // to mark the filename as taken
0462                 $fh = fopen($tmpdir . $uniq, 'w');
0463                 if (!$fh) {
0464                     /**
0465                      * @see Zend_Mail_Storage_Exception
0466                      */
0467                     // require_once 'Zend/Mail/Storage/Exception.php';
0468                     throw new Zend_Mail_Storage_Exception('could not open temp file');
0469                 }
0470                 break;
0471             }
0472             sleep(1);
0473         }
0474 
0475         if (!$fh) {
0476             /**
0477              * @see Zend_Mail_Storage_Exception
0478              */
0479             // require_once 'Zend/Mail/Storage/Exception.php';
0480             throw new Zend_Mail_Storage_Exception("tried $max_tries unique ids for a temp file, but all were taken"
0481                                                 . ' - giving up');
0482         }
0483 
0484         return array('dirname' => $this->_rootdir . '.' . $folder, 'uniq' => $uniq, 'filename' => $tmpdir . $uniq,
0485                      'handle' => $fh);
0486     }
0487 
0488     /**
0489      * create an info string for filenames with given flags
0490      *
0491      * @param   array $flags wanted flags, with the reference you'll get the set flags with correct key (= char for flag)
0492      * @return  string info string for version 2 filenames including the leading colon
0493      * @throws  Zend_Mail_Storage_Exception
0494      */
0495     protected function _getInfoString(&$flags)
0496     {
0497         // accessing keys is easier, faster and it removes duplicated flags
0498         $wanted_flags = array_flip($flags);
0499         if (isset($wanted_flags[Zend_Mail_Storage::FLAG_RECENT])) {
0500             /**
0501              * @see Zend_Mail_Storage_Exception
0502              */
0503             // require_once 'Zend/Mail/Storage/Exception.php';
0504             throw new Zend_Mail_Storage_Exception('recent flag may not be set');
0505         }
0506 
0507         $info = ':2,';
0508         $flags = array();
0509         foreach (Zend_Mail_Storage_Maildir::$_knownFlags as $char => $flag) {
0510             if (!isset($wanted_flags[$flag])) {
0511                 continue;
0512             }
0513             $info .= $char;
0514             $flags[$char] = $flag;
0515             unset($wanted_flags[$flag]);
0516         }
0517 
0518         if (!empty($wanted_flags)) {
0519             $wanted_flags = implode(', ', array_keys($wanted_flags));
0520             /**
0521              * @see Zend_Mail_Storage_Exception
0522              */
0523             // require_once 'Zend/Mail/Storage/Exception.php';
0524             throw new Zend_Mail_Storage_Exception('unknown flag(s): ' . $wanted_flags);
0525         }
0526 
0527         return $info;
0528     }
0529 
0530     /**
0531      * append a new message to mail storage
0532      *
0533      * @param   string|stream                              $message message as string or stream resource
0534      * @param   null|string|Zend_Mail_Storage_Folder       $folder  folder for new message, else current folder is taken
0535      * @param   null|array                                 $flags   set flags for new message, else a default set is used
0536      * @param   bool                                       $recent  handle this mail as if recent flag has been set,
0537      *                                                              should only be used in delivery
0538      * @throws  Zend_Mail_Storage_Exception
0539      */
0540      // not yet * @param string|Zend_Mail_Message|Zend_Mime_Message $message message as string or instance of message class
0541 
0542     public function appendMessage($message, $folder = null, $flags = null, $recent = false)
0543     {
0544         if ($this->_quota && $this->checkQuota()) {
0545             /**
0546              * @see Zend_Mail_Storage_Exception
0547              */
0548             // require_once 'Zend/Mail/Storage/Exception.php';
0549             throw new Zend_Mail_Storage_Exception('storage is over quota!');
0550         }
0551 
0552         if ($folder === null) {
0553             $folder = $this->_currentFolder;
0554         }
0555 
0556         if (!($folder instanceof Zend_Mail_Storage_Folder)) {
0557             $folder = $this->getFolders($folder);
0558         }
0559 
0560         if ($flags === null) {
0561             $flags = array(Zend_Mail_Storage::FLAG_SEEN);
0562         }
0563         $info = $this->_getInfoString($flags);
0564         $temp_file = $this->_createTmpFile($folder->getGlobalName());
0565 
0566         // TODO: handle class instances for $message
0567         if (is_resource($message) && get_resource_type($message) == 'stream') {
0568             stream_copy_to_stream($message, $temp_file['handle']);
0569         } else {
0570             fputs($temp_file['handle'], $message);
0571         }
0572         fclose($temp_file['handle']);
0573 
0574         // we're adding the size to the filename for maildir++
0575         $size = filesize($temp_file['filename']);
0576         if ($size !== false) {
0577             $info = ',S=' . $size . $info;
0578         }
0579         $new_filename = $temp_file['dirname'] . DIRECTORY_SEPARATOR;
0580         $new_filename .= $recent ? 'new' : 'cur';
0581         $new_filename .= DIRECTORY_SEPARATOR . $temp_file['uniq'] . $info;
0582 
0583         // we're throwing any exception after removing our temp file and saving it to this variable instead
0584         $exception = null;
0585 
0586         if (!link($temp_file['filename'], $new_filename)) {
0587             /**
0588              * @see Zend_Mail_Storage_Exception
0589              */
0590             // require_once 'Zend/Mail/Storage/Exception.php';
0591             $exception = new Zend_Mail_Storage_Exception('cannot link message file to final dir');
0592         }
0593         @unlink($temp_file['filename']);
0594 
0595         if ($exception) {
0596             throw $exception;
0597         }
0598 
0599         $this->_files[] = array('uniq'     => $temp_file['uniq'],
0600                                 'flags'    => $flags,
0601                                 'filename' => $new_filename);
0602         if ($this->_quota) {
0603             $this->_addQuotaEntry((int)$size, 1);
0604         }
0605     }
0606 
0607     /**
0608      * copy an existing message
0609      *
0610      * @param   int                             $id     number of message
0611      * @param   string|Zend_Mail_Storage_Folder $folder name or instance of targer folder
0612      * @return  null
0613      * @throws  Zend_Mail_Storage_Exception
0614      */
0615     public function copyMessage($id, $folder)
0616     {
0617         if ($this->_quota && $this->checkQuota()) {
0618             /**
0619              * @see Zend_Mail_Storage_Exception
0620              */
0621             // require_once 'Zend/Mail/Storage/Exception.php';
0622             throw new Zend_Mail_Storage_Exception('storage is over quota!');
0623         }
0624 
0625         if (!($folder instanceof Zend_Mail_Storage_Folder)) {
0626             $folder = $this->getFolders($folder);
0627         }
0628 
0629         $filedata = $this->_getFileData($id);
0630         $old_file = $filedata['filename'];
0631         $flags = $filedata['flags'];
0632 
0633         // copied message can't be recent
0634         while (($key = array_search(Zend_Mail_Storage::FLAG_RECENT, $flags)) !== false) {
0635             unset($flags[$key]);
0636         }
0637         $info = $this->_getInfoString($flags);
0638 
0639         // we're creating the copy as temp file before moving to cur/
0640         $temp_file = $this->_createTmpFile($folder->getGlobalName());
0641         // we don't write directly to the file
0642         fclose($temp_file['handle']);
0643 
0644         // we're adding the size to the filename for maildir++
0645         $size = filesize($old_file);
0646         if ($size !== false) {
0647             $info = ',S=' . $size . $info;
0648         }
0649 
0650         $new_file = $temp_file['dirname'] . DIRECTORY_SEPARATOR . 'cur' . DIRECTORY_SEPARATOR . $temp_file['uniq'] . $info;
0651 
0652         // we're throwing any exception after removing our temp file and saving it to this variable instead
0653         $exception = null;
0654 
0655         if (!copy($old_file, $temp_file['filename'])) {
0656             /**
0657              * @see Zend_Mail_Storage_Exception
0658              */
0659             // require_once 'Zend/Mail/Storage/Exception.php';
0660             $exception = new Zend_Mail_Storage_Exception('cannot copy message file');
0661         } else if (!link($temp_file['filename'], $new_file)) {
0662             /**
0663              * @see Zend_Mail_Storage_Exception
0664              */
0665             // require_once 'Zend/Mail/Storage/Exception.php';
0666             $exception = new Zend_Mail_Storage_Exception('cannot link message file to final dir');
0667         }
0668         @unlink($temp_file['filename']);
0669 
0670         if ($exception) {
0671             throw $exception;
0672         }
0673 
0674         if ($folder->getGlobalName() == $this->_currentFolder
0675             || ($this->_currentFolder == 'INBOX' && $folder->getGlobalName() == '/')) {
0676             $this->_files[] = array('uniq'     => $temp_file['uniq'],
0677                                     'flags'    => $flags,
0678                                     'filename' => $new_file);
0679         }
0680 
0681         if ($this->_quota) {
0682             $this->_addQuotaEntry((int)$size, 1);
0683         }
0684     }
0685 
0686     /**
0687      * move an existing message
0688      *
0689      * @param  int                             $id     number of message
0690      * @param  string|Zend_Mail_Storage_Folder $folder name or instance of targer folder
0691      * @return null
0692      * @throws Zend_Mail_Storage_Exception
0693      */
0694     public function moveMessage($id, $folder) {
0695         if (!($folder instanceof Zend_Mail_Storage_Folder)) {
0696             $folder = $this->getFolders($folder);
0697         }
0698 
0699         if ($folder->getGlobalName() == $this->_currentFolder
0700             || ($this->_currentFolder == 'INBOX' && $folder->getGlobalName() == '/')) {
0701             /**
0702              * @see Zend_Mail_Storage_Exception
0703              */
0704             // require_once 'Zend/Mail/Storage/Exception.php';
0705             throw new Zend_Mail_Storage_Exception('target is current folder');
0706         }
0707 
0708         $filedata = $this->_getFileData($id);
0709         $old_file = $filedata['filename'];
0710         $flags = $filedata['flags'];
0711 
0712         // moved message can't be recent
0713         while (($key = array_search(Zend_Mail_Storage::FLAG_RECENT, $flags)) !== false) {
0714             unset($flags[$key]);
0715         }
0716         $info = $this->_getInfoString($flags);
0717 
0718         // reserving a new name
0719         $temp_file = $this->_createTmpFile($folder->getGlobalName());
0720         fclose($temp_file['handle']);
0721 
0722         // we're adding the size to the filename for maildir++
0723         $size = filesize($old_file);
0724         if ($size !== false) {
0725             $info = ',S=' . $size . $info;
0726         }
0727 
0728         $new_file = $temp_file['dirname'] . DIRECTORY_SEPARATOR . 'cur' . DIRECTORY_SEPARATOR . $temp_file['uniq'] . $info;
0729 
0730         // we're throwing any exception after removing our temp file and saving it to this variable instead
0731         $exception = null;
0732 
0733         if (!rename($old_file, $new_file)) {
0734             /**
0735              * @see Zend_Mail_Storage_Exception
0736              */
0737             // require_once 'Zend/Mail/Storage/Exception.php';
0738             $exception = new Zend_Mail_Storage_Exception('cannot move message file');
0739         }
0740         @unlink($temp_file['filename']);
0741 
0742         if ($exception) {
0743             throw $exception;
0744         }
0745 
0746         unset($this->_files[$id - 1]);
0747         // remove the gap
0748         $this->_files = array_values($this->_files);
0749     }
0750 
0751 
0752     /**
0753      * set flags for message
0754      *
0755      * NOTE: this method can't set the recent flag.
0756      *
0757      * @param   int   $id    number of message
0758      * @param   array $flags new flags for message
0759      * @throws  Zend_Mail_Storage_Exception
0760      */
0761     public function setFlags($id, $flags)
0762     {
0763         $info = $this->_getInfoString($flags);
0764         $filedata = $this->_getFileData($id);
0765 
0766         // 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.
0767         $new_filename = dirname(dirname($filedata['filename'])) . DIRECTORY_SEPARATOR . 'cur' . DIRECTORY_SEPARATOR . "$filedata[uniq]$info";
0768 
0769         if (!@rename($filedata['filename'], $new_filename)) {
0770             /**
0771              * @see Zend_Mail_Storage_Exception
0772              */
0773             // require_once 'Zend/Mail/Storage/Exception.php';
0774             throw new Zend_Mail_Storage_Exception('cannot rename file');
0775         }
0776 
0777         $filedata['flags']    = $flags;
0778         $filedata['filename'] = $new_filename;
0779 
0780         $this->_files[$id - 1] = $filedata;
0781     }
0782 
0783 
0784     /**
0785      * stub for not supported message deletion
0786      *
0787      * @return  null
0788      * @throws  Zend_Mail_Storage_Exception
0789      */
0790     public function removeMessage($id)
0791     {
0792         $filename = $this->_getFileData($id, 'filename');
0793 
0794         if ($this->_quota) {
0795             $size = filesize($filename);
0796         }
0797 
0798         if (!@unlink($filename)) {
0799             /**
0800              * @see Zend_Mail_Storage_Exception
0801              */
0802             // require_once 'Zend/Mail/Storage/Exception.php';
0803             throw new Zend_Mail_Storage_Exception('cannot remove message');
0804         }
0805         unset($this->_files[$id - 1]);
0806         // remove the gap
0807         $this->_files = array_values($this->_files);
0808         if ($this->_quota) {
0809             $this->_addQuotaEntry(0 - (int)$size, -1);
0810         }
0811     }
0812 
0813     /**
0814      * enable/disable quota and set a quota value if wanted or needed
0815      *
0816      * You can enable/disable quota with true/false. If you don't have
0817      * a MDA or want to enforce a quota value you can also set this value
0818      * here. Use array('size' => SIZE_QUOTA, 'count' => MAX_MESSAGE) do
0819      * define your quota. Order of these fields does matter!
0820      *
0821      * @param bool|array $value new quota value
0822      * @return null
0823      */
0824     public function setQuota($value) {
0825         $this->_quota = $value;
0826     }
0827 
0828     /**
0829      * get currently set quota
0830      *
0831      * @see Zend_Mail_Storage_Writable_Maildir::setQuota()
0832      *
0833      * @return bool|array
0834      */
0835     public function getQuota($fromStorage = false) {
0836         if ($fromStorage) {
0837             $fh = @fopen($this->_rootdir . 'maildirsize', 'r');
0838             if (!$fh) {
0839                 /**
0840                  * @see Zend_Mail_Storage_Exception
0841                  */
0842                 // require_once 'Zend/Mail/Storage/Exception.php';
0843                 throw new Zend_Mail_Storage_Exception('cannot open maildirsize');
0844             }
0845             $definition = fgets($fh);
0846             fclose($fh);
0847             $definition = explode(',', trim($definition));
0848             $quota = array();
0849             foreach ($definition as $member) {
0850                 $key = $member[strlen($member) - 1];
0851                 if ($key == 'S' || $key == 'C') {
0852                     $key = $key == 'C' ? 'count' : 'size';
0853                 }
0854                 $quota[$key] = substr($member, 0, -1);
0855             }
0856             return $quota;
0857         }
0858 
0859         return $this->_quota;
0860     }
0861 
0862     /**
0863      * @see http://www.inter7.com/courierimap/README.maildirquota.html "Calculating maildirsize"
0864      */
0865     protected function _calculateMaildirsize() {
0866         $timestamps = array();
0867         $messages = 0;
0868         $total_size = 0;
0869 
0870         if (is_array($this->_quota)) {
0871             $quota = $this->_quota;
0872         } else {
0873             try {
0874                 $quota = $this->getQuota(true);
0875             } catch (Zend_Mail_Storage_Exception $e) {
0876                 throw new Zend_Mail_Storage_Exception('no quota definition found', 0, $e);
0877             }
0878         }
0879 
0880         $folders = new RecursiveIteratorIterator($this->getFolders(), RecursiveIteratorIterator::SELF_FIRST);
0881         foreach ($folders as $folder) {
0882             $subdir = $folder->getGlobalName();
0883             if ($subdir == 'INBOX') {
0884                 $subdir = '';
0885             } else {
0886                 $subdir = '.' . $subdir;
0887             }
0888             if ($subdir == 'Trash') {
0889                 continue;
0890             }
0891 
0892             foreach (array('cur', 'new') as $subsubdir) {
0893                 $dirname = $this->_rootdir . $subdir . DIRECTORY_SEPARATOR . $subsubdir . DIRECTORY_SEPARATOR;
0894                 if (!file_exists($dirname)) {
0895                     continue;
0896                 }
0897                 // NOTE: we are using mtime instead of "the latest timestamp". The latest would be atime
0898                 // and as we are accessing the directory it would make the whole calculation useless.
0899                 $timestamps[$dirname] = filemtime($dirname);
0900 
0901                 $dh = opendir($dirname);
0902                 // NOTE: Should have been checked in constructor. Not throwing an exception here, quotas will
0903                 // therefore not be fully enforeced, but next request will fail anyway, if problem persists.
0904                 if (!$dh) {
0905                     continue;
0906                 }
0907 
0908 
0909                 while (($entry = readdir()) !== false) {
0910                     if ($entry[0] == '.' || !is_file($dirname . $entry)) {
0911                         continue;
0912                     }
0913 
0914                     if (strpos($entry, ',S=')) {
0915                         strtok($entry, '=');
0916                         $filesize = strtok(':');
0917                         if (is_numeric($filesize)) {
0918                             $total_size += $filesize;
0919                             ++$messages;
0920                             continue;
0921                         }
0922                     }
0923                     $size = filesize($dirname . $entry);
0924                     if ($size === false) {
0925                         // ignore, as we assume file got removed
0926                         continue;
0927                     }
0928                     $total_size += $size;
0929                     ++$messages;
0930                 }
0931             }
0932         }
0933 
0934         $tmp = $this->_createTmpFile();
0935         $fh = $tmp['handle'];
0936         $definition = array();
0937         foreach ($quota as $type => $value) {
0938             if ($type == 'size' || $type == 'count') {
0939                 $type = $type == 'count' ? 'C' : 'S';
0940             }
0941             $definition[] = $value . $type;
0942         }
0943         $definition = implode(',', $definition);
0944         fputs($fh, "$definition\n");
0945         fputs($fh, "$total_size $messages\n");
0946         fclose($fh);
0947         rename($tmp['filename'], $this->_rootdir . 'maildirsize');
0948         foreach ($timestamps as $dir => $timestamp) {
0949             if ($timestamp < filemtime($dir)) {
0950                 unlink($this->_rootdir . 'maildirsize');
0951                 break;
0952             }
0953         }
0954 
0955         return array('size' => $total_size, 'count' => $messages, 'quota' => $quota);
0956     }
0957 
0958     /**
0959      * @see http://www.inter7.com/courierimap/README.maildirquota.html "Calculating the quota for a Maildir++"
0960      */
0961     protected function _calculateQuota($forceRecalc = false) {
0962         $fh = null;
0963         $total_size = 0;
0964         $messages   = 0;
0965         $maildirsize = '';
0966         if (!$forceRecalc && file_exists($this->_rootdir . 'maildirsize') && filesize($this->_rootdir . 'maildirsize') < 5120) {
0967             $fh = fopen($this->_rootdir . 'maildirsize', 'r');
0968         }
0969         if ($fh) {
0970             $maildirsize = fread($fh, 5120);
0971             if (strlen($maildirsize) >= 5120) {
0972                 fclose($fh);
0973                 $fh = null;
0974                 $maildirsize = '';
0975             }
0976         }
0977         if (!$fh) {
0978             $result = $this->_calculateMaildirsize();
0979             $total_size = $result['size'];
0980             $messages   = $result['count'];
0981             $quota      = $result['quota'];
0982         } else {
0983             $maildirsize = explode("\n", $maildirsize);
0984             if (is_array($this->_quota)) {
0985                 $quota = $this->_quota;
0986             } else {
0987                 $definition = explode(',', $maildirsize[0]);
0988                 $quota = array();
0989                 foreach ($definition as $member) {
0990                     $key = $member[strlen($member) - 1];
0991                     if ($key == 'S' || $key == 'C') {
0992                         $key = $key == 'C' ? 'count' : 'size';
0993                     }
0994                     $quota[$key] = substr($member, 0, -1);
0995                 }
0996             }
0997             unset($maildirsize[0]);
0998             foreach ($maildirsize as $line) {
0999                 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 }