web/lib/Zend/Mail/Storage/Writable/Maildir.php
changeset 64 162c1de6545a
parent 19 1c2f13fd785c
child 68 ecaf28ffe26e
equal deleted inserted replaced
63:5b37998e522e 64:162c1de6545a
       
     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 }