1<?php 2/** 3 * Zend Framework (http://framework.zend.com/) 4 * 5 * @link http://github.com/zendframework/zf2 for the canonical source repository 6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com) 7 * @license http://framework.zend.com/license/new-bsd New BSD License 8 */ 9 10namespace Zend\Mail\Storage; 11 12use Zend\Mail; 13use Zend\Stdlib\ErrorHandler; 14 15class Maildir extends AbstractStorage 16{ 17 /** 18 * used message class, change it in an extended class to extend the returned message class 19 * @var string 20 */ 21 protected $messageClass = '\Zend\Mail\Storage\Message\File'; 22 23 /** 24 * data of found message files in maildir dir 25 * @var array 26 */ 27 protected $files = array(); 28 29 /** 30 * known flag chars in filenames 31 * 32 * This list has to be in alphabetical order for setFlags() 33 * 34 * @var array 35 */ 36 protected static $knownFlags = array('D' => Mail\Storage::FLAG_DRAFT, 37 'F' => Mail\Storage::FLAG_FLAGGED, 38 'P' => Mail\Storage::FLAG_PASSED, 39 'R' => Mail\Storage::FLAG_ANSWERED, 40 'S' => Mail\Storage::FLAG_SEEN, 41 'T' => Mail\Storage::FLAG_DELETED); 42 43 // TODO: getFlags($id) for fast access if headers are not needed (i.e. just setting flags)? 44 45 /** 46 * Count messages all messages in current box 47 * 48 * @param mixed $flags 49 * @return int number of messages 50 */ 51 public function countMessages($flags = null) 52 { 53 if ($flags === null) { 54 return count($this->files); 55 } 56 57 $count = 0; 58 if (!is_array($flags)) { 59 foreach ($this->files as $file) { 60 if (isset($file['flaglookup'][$flags])) { 61 ++$count; 62 } 63 } 64 return $count; 65 } 66 67 $flags = array_flip($flags); 68 foreach ($this->files as $file) { 69 foreach ($flags as $flag => $v) { 70 if (!isset($file['flaglookup'][$flag])) { 71 continue 2; 72 } 73 } 74 ++$count; 75 } 76 return $count; 77 } 78 79 /** 80 * Get one or all fields from file structure. Also checks if message is valid 81 * 82 * @param int $id message number 83 * @param string|null $field wanted field 84 * @throws Exception\InvalidArgumentException 85 * @return string|array wanted field or all fields as array 86 */ 87 protected function _getFileData($id, $field = null) 88 { 89 if (!isset($this->files[$id - 1])) { 90 throw new Exception\InvalidArgumentException('id does not exist'); 91 } 92 93 if (!$field) { 94 return $this->files[$id - 1]; 95 } 96 97 if (!isset($this->files[$id - 1][$field])) { 98 throw new Exception\InvalidArgumentException('field does not exist'); 99 } 100 101 return $this->files[$id - 1][$field]; 102 } 103 104 /** 105 * Get a list of messages with number and size 106 * 107 * @param int|null $id number of message or null for all messages 108 * @return int|array size of given message of list with all messages as array(num => size) 109 */ 110 public function getSize($id = null) 111 { 112 if ($id !== null) { 113 $filedata = $this->_getFileData($id); 114 return isset($filedata['size']) ? $filedata['size'] : filesize($filedata['filename']); 115 } 116 117 $result = array(); 118 foreach ($this->files as $num => $data) { 119 $result[$num + 1] = isset($data['size']) ? $data['size'] : filesize($data['filename']); 120 } 121 122 return $result; 123 } 124 125 /** 126 * Fetch a message 127 * 128 * @param int $id number of message 129 * @return \Zend\Mail\Storage\Message\File 130 * @throws \Zend\Mail\Storage\Exception\ExceptionInterface 131 */ 132 public function getMessage($id) 133 { 134 // TODO that's ugly, would be better to let the message class decide 135 if (strtolower($this->messageClass) == '\zend\mail\storage\message\file' 136 || is_subclass_of($this->messageClass, '\Zend\Mail\Storage\Message\File')) { 137 return new $this->messageClass(array('file' => $this->_getFileData($id, 'filename'), 138 'flags' => $this->_getFileData($id, 'flags'))); 139 } 140 141 return new $this->messageClass(array('handler' => $this, 'id' => $id, 'headers' => $this->getRawHeader($id), 142 'flags' => $this->_getFileData($id, 'flags'))); 143 } 144 145 /* 146 * Get raw header of message or part 147 * 148 * @param int $id number of message 149 * @param null|array|string $part path to part or null for message header 150 * @param int $topLines include this many lines with header (after an empty line) 151 * @throws Exception\RuntimeException 152 * @return string raw header 153 */ 154 public function getRawHeader($id, $part = null, $topLines = 0) 155 { 156 if ($part !== null) { 157 // TODO: implement 158 throw new Exception\RuntimeException('not implemented'); 159 } 160 161 $fh = fopen($this->_getFileData($id, 'filename'), 'r'); 162 163 $content = ''; 164 while (!feof($fh)) { 165 $line = fgets($fh); 166 if (!trim($line)) { 167 break; 168 } 169 $content .= $line; 170 } 171 172 fclose($fh); 173 return $content; 174 } 175 176 /* 177 * Get raw content of message or part 178 * 179 * @param int $id number of message 180 * @param null|array|string $part path to part or null for message content 181 * @throws Exception\RuntimeException 182 * @return string raw content 183 */ 184 public function getRawContent($id, $part = null) 185 { 186 if ($part !== null) { 187 // TODO: implement 188 throw new Exception\RuntimeException('not implemented'); 189 } 190 191 $fh = fopen($this->_getFileData($id, 'filename'), 'r'); 192 193 while (!feof($fh)) { 194 $line = fgets($fh); 195 if (!trim($line)) { 196 break; 197 } 198 } 199 200 $content = stream_get_contents($fh); 201 fclose($fh); 202 return $content; 203 } 204 205 /** 206 * Create instance with parameters 207 * Supported parameters are: 208 * - dirname dirname of mbox file 209 * 210 * @param $params array mail reader specific parameters 211 * @throws Exception\InvalidArgumentException 212 */ 213 public function __construct($params) 214 { 215 if (is_array($params)) { 216 $params = (object) $params; 217 } 218 219 if (!isset($params->dirname) || !is_dir($params->dirname)) { 220 throw new Exception\InvalidArgumentException('no valid dirname given in params'); 221 } 222 223 if (!$this->_isMaildir($params->dirname)) { 224 throw new Exception\InvalidArgumentException('invalid maildir given'); 225 } 226 227 $this->has['top'] = true; 228 $this->has['flags'] = true; 229 $this->_openMaildir($params->dirname); 230 } 231 232 /** 233 * check if a given dir is a valid maildir 234 * 235 * @param string $dirname name of dir 236 * @return bool dir is valid maildir 237 */ 238 protected function _isMaildir($dirname) 239 { 240 if (file_exists($dirname . '/new') && !is_dir($dirname . '/new')) { 241 return false; 242 } 243 if (file_exists($dirname . '/tmp') && !is_dir($dirname . '/tmp')) { 244 return false; 245 } 246 return is_dir($dirname . '/cur'); 247 } 248 249 /** 250 * open given dir as current maildir 251 * 252 * @param string $dirname name of maildir 253 * @throws Exception\RuntimeException 254 */ 255 protected function _openMaildir($dirname) 256 { 257 if ($this->files) { 258 $this->close(); 259 } 260 261 ErrorHandler::start(E_WARNING); 262 $dh = opendir($dirname . '/cur/'); 263 $error = ErrorHandler::stop(); 264 if (!$dh) { 265 throw new Exception\RuntimeException('cannot open maildir', 0, $error); 266 } 267 $this->_getMaildirFiles($dh, $dirname . '/cur/'); 268 closedir($dh); 269 270 ErrorHandler::start(E_WARNING); 271 $dh = opendir($dirname . '/new/'); 272 $error = ErrorHandler::stop(); 273 if ($dh) { 274 $this->_getMaildirFiles($dh, $dirname . '/new/', array(Mail\Storage::FLAG_RECENT)); 275 closedir($dh); 276 } elseif (file_exists($dirname . '/new/')) { 277 throw new Exception\RuntimeException('cannot read recent mails in maildir', 0, $error); 278 } 279 } 280 281 /** 282 * find all files in opened dir handle and add to maildir files 283 * 284 * @param resource $dh dir handle used for search 285 * @param string $dirname dirname of dir in $dh 286 * @param array $defaultFlags default flags for given dir 287 */ 288 protected function _getMaildirFiles($dh, $dirname, $defaultFlags = array()) 289 { 290 while (($entry = readdir($dh)) !== false) { 291 if ($entry[0] == '.' || !is_file($dirname . $entry)) { 292 continue; 293 } 294 295 ErrorHandler::start(E_NOTICE); 296 list($uniq, $info) = explode(':', $entry, 2); 297 list(, $size) = explode(',', $uniq, 2); 298 ErrorHandler::stop(); 299 if ($size && $size[0] == 'S' && $size[1] == '=') { 300 $size = substr($size, 2); 301 } 302 if (!ctype_digit($size)) { 303 $size = null; 304 } 305 306 ErrorHandler::start(E_NOTICE); 307 list($version, $flags) = explode(',', $info, 2); 308 ErrorHandler::stop(); 309 if ($version != 2) { 310 $flags = ''; 311 } 312 313 $namedFlags = $defaultFlags; 314 $length = strlen($flags); 315 for ($i = 0; $i < $length; ++$i) { 316 $flag = $flags[$i]; 317 $namedFlags[$flag] = isset(static::$knownFlags[$flag]) ? static::$knownFlags[$flag] : $flag; 318 } 319 320 $data = array('uniq' => $uniq, 321 'flags' => $namedFlags, 322 'flaglookup' => array_flip($namedFlags), 323 'filename' => $dirname . $entry); 324 if ($size !== null) { 325 $data['size'] = (int) $size; 326 } 327 $this->files[] = $data; 328 } 329 } 330 331 /** 332 * Close resource for mail lib. If you need to control, when the resource 333 * is closed. Otherwise the destructor would call this. 334 * 335 */ 336 public function close() 337 { 338 $this->files = array(); 339 } 340 341 /** 342 * Waste some CPU cycles doing nothing. 343 * 344 * @return bool always return true 345 */ 346 public function noop() 347 { 348 return true; 349 } 350 351 /** 352 * stub for not supported message deletion 353 * 354 * @param $id 355 * @throws Exception\RuntimeException 356 */ 357 public function removeMessage($id) 358 { 359 throw new Exception\RuntimeException('maildir is (currently) read-only'); 360 } 361 362 /** 363 * get unique id for one or all messages 364 * 365 * if storage does not support unique ids it's the same as the message number 366 * 367 * @param int|null $id message number 368 * @return array|string message number for given message or all messages as array 369 */ 370 public function getUniqueId($id = null) 371 { 372 if ($id) { 373 return $this->_getFileData($id, 'uniq'); 374 } 375 376 $ids = array(); 377 foreach ($this->files as $num => $file) { 378 $ids[$num + 1] = $file['uniq']; 379 } 380 return $ids; 381 } 382 383 /** 384 * get a message number from a unique id 385 * 386 * I.e. if you have a webmailer that supports deleting messages you should use unique ids 387 * as parameter and use this method to translate it to message number right before calling removeMessage() 388 * 389 * @param string $id unique id 390 * @throws Exception\InvalidArgumentException 391 * @return int message number 392 */ 393 public function getNumberByUniqueId($id) 394 { 395 foreach ($this->files as $num => $file) { 396 if ($file['uniq'] == $id) { 397 return $num + 1; 398 } 399 } 400 401 throw new Exception\InvalidArgumentException('unique id not found'); 402 } 403} 404