3 * Zend Framework (http://framework.zend.com/)
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
10 namespace Zend\Mail\Storage
;
13 use Zend\Mail\Protocol
;
15 class Imap
extends AbstractStorage
implements Folder\FolderInterface
, Writable\WritableInterface
17 // TODO: with an internal cache we could optimize this class, or create an extra class with
18 // such optimizations. Especially the various fetch calls could be combined to one cache call
22 * @var null|\Zend\Mail\Protocol\Imap
27 * name of current folder
30 protected $currentFolder = '';
33 * IMAP flags to constants translation
36 protected static $knownFlags = array('\Passed' => Mail\Storage
::FLAG_PASSED
,
37 '\Answered' => Mail\Storage
::FLAG_ANSWERED
,
38 '\Seen' => Mail\Storage
::FLAG_SEEN
,
39 '\Unseen' => Mail\Storage
::FLAG_UNSEEN
,
40 '\Deleted' => Mail\Storage
::FLAG_DELETED
,
41 '\Draft' => Mail\Storage
::FLAG_DRAFT
,
42 '\Flagged' => Mail\Storage
::FLAG_FLAGGED
);
45 * IMAP flags to search criteria
48 protected static $searchFlags = array('\Recent' => 'RECENT',
49 '\Answered' => 'ANSWERED',
51 '\Unseen' => 'UNSEEN',
52 '\Deleted' => 'DELETED',
54 '\Flagged' => 'FLAGGED');
57 * Count messages all messages in current box
60 * @throws Exception\RuntimeException
61 * @throws \Zend\Mail\Protocol\Exception\RuntimeException
62 * @return int number of messages
64 public function countMessages($flags = null)
66 if (!$this->currentFolder
) {
67 throw new Exception\
RuntimeException('No selected folder to count');
70 if ($flags === null) {
71 return count($this->protocol
->search(array('ALL')));
75 foreach ((array) $flags as $flag) {
76 if (isset(static::$searchFlags[$flag])) {
77 $params[] = static::$searchFlags[$flag];
79 $params[] = 'KEYWORD';
80 $params[] = $this->protocol
->escapeString($flag);
83 return count($this->protocol
->search($params));
87 * get a list of messages with number and size
89 * @param int $id number of message
90 * @return int|array size of given message of list with all messages as array(num => size)
91 * @throws \Zend\Mail\Protocol\Exception\RuntimeException
93 public function getSize($id = 0)
96 return $this->protocol
->fetch('RFC822.SIZE', $id);
98 return $this->protocol
->fetch('RFC822.SIZE', 1, INF
);
104 * @param int $id number of message
105 * @return \Zend\Mail\Storage\Message
106 * @throws \Zend\Mail\Protocol\Exception\RuntimeException
108 public function getMessage($id)
110 $data = $this->protocol
->fetch(array('FLAGS', 'RFC822.HEADER'), $id);
111 $header = $data['RFC822.HEADER'];
114 foreach ($data['FLAGS'] as $flag) {
115 $flags[] = isset(static::$knownFlags[$flag]) ?
static::$knownFlags[$flag] : $flag;
118 return new $this->messageClass(array('handler' => $this, 'id' => $id, 'headers' => $header, 'flags' => $flags));
122 * Get raw header of message or part
124 * @param int $id number of message
125 * @param null|array|string $part path to part or null for message header
126 * @param int $topLines include this many lines with header (after an empty line)
127 * @param int $topLines include this many lines with header (after an empty line)
128 * @return string raw header
129 * @throws Exception\RuntimeException
130 * @throws \Zend\Mail\Protocol\Exception\RuntimeException
132 public function getRawHeader($id, $part = null, $topLines = 0)
134 if ($part !== null) {
136 throw new Exception\
RuntimeException('not implemented');
140 return $this->protocol
->fetch('RFC822.HEADER', $id);
144 * Get raw content of message or part
146 * @param int $id number of message
147 * @param null|array|string $part path to part or null for message content
148 * @return string raw content
149 * @throws \Zend\Mail\Protocol\Exception\RuntimeException
150 * @throws Exception\RuntimeException
152 public function getRawContent($id, $part = null)
154 if ($part !== null) {
156 throw new Exception\
RuntimeException('not implemented');
159 return $this->protocol
->fetch('RFC822.TEXT', $id);
163 * create instance with parameters
164 * Supported parameters are
166 * - host hostname or ip address of IMAP server [optional, default = 'localhost']
167 * - password password for user 'username' [optional, default = '']
168 * - port port for IMAP server [optional, default = 110]
169 * - ssl 'SSL' or 'TLS' for secure sockets
170 * - folder select this folder [optional, default = 'INBOX']
172 * @param array $params mail reader specific parameters
173 * @throws Exception\RuntimeException
174 * @throws Exception\InvalidArgumentException
175 * @throws \Zend\Mail\Protocol\Exception\RuntimeException
177 public function __construct($params)
179 if (is_array($params)) {
180 $params = (object) $params;
183 $this->has
['flags'] = true;
185 if ($params instanceof Protocol\Imap
) {
186 $this->protocol
= $params;
188 $this->selectFolder('INBOX');
189 } catch (Exception\ExceptionInterface
$e) {
190 throw new Exception\
RuntimeException('cannot select INBOX, is this a valid transport?', 0, $e);
195 if (!isset($params->user
)) {
196 throw new Exception\
InvalidArgumentException('need at least user in params');
199 $host = isset($params->host
) ?
$params->host
: 'localhost';
200 $password = isset($params->password
) ?
$params->password
: '';
201 $port = isset($params->port
) ?
$params->port
: null;
202 $ssl = isset($params->ssl
) ?
$params->ssl
: false;
204 $this->protocol
= new Protocol\
Imap();
205 $this->protocol
->connect($host, $port, $ssl);
206 if (!$this->protocol
->login($params->user
, $password)) {
207 throw new Exception\
RuntimeException('cannot login, user or password wrong');
209 $this->selectFolder(isset($params->folder
) ?
$params->folder
: 'INBOX');
213 * Close resource for mail lib. If you need to control, when the resource
214 * is closed. Otherwise the destructor would call this.
216 public function close()
218 $this->currentFolder
= '';
219 $this->protocol
->logout();
223 * Keep the server busy.
225 * @throws Exception\RuntimeException
227 public function noop()
229 if (!$this->protocol
->noop()) {
230 throw new Exception\
RuntimeException('could not do nothing');
235 * Remove a message from server. If you're doing that from a web environment
236 * you should be careful and use a uniqueid as parameter if possible to
237 * identify the message.
239 * @param int $id number of message
240 * @throws Exception\RuntimeException
242 public function removeMessage($id)
244 if (!$this->protocol
->store(array(Mail\Storage
::FLAG_DELETED
), $id, null, '+')) {
245 throw new Exception\
RuntimeException('cannot set deleted flag');
247 // TODO: expunge here or at close? we can handle an error here better and are more fail safe
248 if (!$this->protocol
->expunge()) {
249 throw new Exception\
RuntimeException('message marked as deleted, but could not expunge');
254 * get unique id for one or all messages
256 * if storage does not support unique ids it's the same as the message number
258 * @param int|null $id message number
259 * @return array|string message number for given message or all messages as array
260 * @throws \Zend\Mail\Protocol\Exception\RuntimeException
262 public function getUniqueId($id = null)
265 return $this->protocol
->fetch('UID', $id);
268 return $this->protocol
->fetch('UID', 1, INF
);
272 * get a message number from a unique id
274 * I.e. if you have a webmailer that supports deleting messages you should use unique ids
275 * as parameter and use this method to translate it to message number right before calling removeMessage()
277 * @param string $id unique id
278 * @throws Exception\InvalidArgumentException
279 * @return int message number
281 public function getNumberByUniqueId($id)
283 // TODO: use search to find number directly
284 $ids = $this->getUniqueId();
285 foreach ($ids as $k => $v) {
291 throw new Exception\
InvalidArgumentException('unique id not found');
295 * get root folder or given folder
297 * @param string $rootFolder get folder structure for given folder, else root
298 * @throws Exception\RuntimeException
299 * @throws Exception\InvalidArgumentException
300 * @throws \Zend\Mail\Protocol\Exception\RuntimeException
301 * @return \Zend\Mail\Storage\Folder root or wanted folder
303 public function getFolders($rootFolder = null)
305 $folders = $this->protocol
->listMailbox((string) $rootFolder);
307 throw new Exception\
InvalidArgumentException('folder not found');
310 ksort($folders, SORT_STRING
);
311 $root = new Folder('/', '/', false);
312 $stack = array(null);
313 $folderStack = array(null);
314 $parentFolder = $root;
317 foreach ($folders as $globalName => $data) {
319 if (!$parent ||
strpos($globalName, $parent) === 0) {
320 $pos = strrpos($globalName, $data['delim']);
321 if ($pos === false) {
322 $localName = $globalName;
324 $localName = substr($globalName, $pos +
1);
326 $selectable = !$data['flags'] ||
!in_array('\\Noselect', $data['flags']);
328 array_push($stack, $parent);
329 $parent = $globalName . $data['delim'];
330 $folder = new Folder($localName, $globalName, $selectable);
331 $parentFolder->$localName = $folder;
332 array_push($folderStack, $parentFolder);
333 $parentFolder = $folder;
336 $parent = array_pop($stack);
337 $parentFolder = array_pop($folderStack);
341 throw new Exception\
RuntimeException('error while constructing folder tree');
349 * select given folder
351 * folder must be selectable!
353 * @param \Zend\Mail\Storage\Folder|string $globalName global name of folder or instance for subfolder
354 * @throws Exception\RuntimeException
355 * @throws \Zend\Mail\Protocol\Exception\RuntimeException
357 public function selectFolder($globalName)
359 $this->currentFolder
= $globalName;
360 if (!$this->protocol
->select($this->currentFolder
)) {
361 $this->currentFolder
= '';
362 throw new Exception\
RuntimeException('cannot change folder, maybe it does not exist');
367 * get \Zend\Mail\Storage\Folder instance for current folder
369 * @return \Zend\Mail\Storage\Folder instance of current folder
371 public function getCurrentFolder()
373 return $this->currentFolder
;
377 * create a new folder
379 * This method also creates parent folders if necessary. Some mail storages may restrict, which folder
380 * may be used as parent or which chars may be used in the folder name
382 * @param string $name global name of folder, local name if $parentFolder is set
383 * @param string|\Zend\Mail\Storage\Folder $parentFolder parent folder for new folder, else root folder is parent
384 * @throws Exception\RuntimeException
386 public function createFolder($name, $parentFolder = null)
388 // TODO: we assume / as the hierarchy delim - need to get that from the folder class!
389 if ($parentFolder instanceof Folder
) {
390 $folder = $parentFolder->getGlobalName() . '/' . $name;
391 } elseif ($parentFolder !== null) {
392 $folder = $parentFolder . '/' . $name;
397 if (!$this->protocol
->create($folder)) {
398 throw new Exception\
RuntimeException('cannot create folder');
405 * @param string|\Zend\Mail\Storage\Folder $name name or instance of folder
406 * @throws Exception\RuntimeException
408 public function removeFolder($name)
410 if ($name instanceof Folder
) {
411 $name = $name->getGlobalName();
414 if (!$this->protocol
->delete($name)) {
415 throw new Exception\
RuntimeException('cannot delete folder');
420 * rename and/or move folder
422 * The new name has the same restrictions as in createFolder()
424 * @param string|\Zend\Mail\Storage\Folder $oldName name or instance of folder
425 * @param string $newName new global name of folder
426 * @throws Exception\RuntimeException
428 public function renameFolder($oldName, $newName)
430 if ($oldName instanceof Folder
) {
431 $oldName = $oldName->getGlobalName();
434 if (!$this->protocol
->rename($oldName, $newName)) {
435 throw new Exception\
RuntimeException('cannot rename folder');
440 * append a new message to mail storage
442 * @param string $message message as string or instance of message class
443 * @param null|string|\Zend\Mail\Storage\Folder $folder folder for new message, else current folder is taken
444 * @param null|array $flags set flags for new message, else a default set is used
445 * @throws Exception\RuntimeException
447 public function appendMessage($message, $folder = null, $flags = null)
449 if ($folder === null) {
450 $folder = $this->currentFolder
;
453 if ($flags === null) {
454 $flags = array(Mail\Storage
::FLAG_SEEN
);
457 // TODO: handle class instances for $message
458 if (!$this->protocol
->append($folder, $message, $flags)) {
459 throw new Exception\
RuntimeException('cannot create message, please check if the folder exists and your flags');
464 * copy an existing message
466 * @param int $id number of message
467 * @param string|\Zend\Mail\Storage\Folder $folder name or instance of target folder
468 * @throws Exception\RuntimeException
470 public function copyMessage($id, $folder)
472 if (!$this->protocol
->copy($folder, $id)) {
473 throw new Exception\
RuntimeException('cannot copy message, does the folder exist?');
478 * move an existing message
480 * NOTE: IMAP has no native move command, thus it's emulated with copy and delete
482 * @param int $id number of message
483 * @param string|\Zend\Mail\Storage\Folder $folder name or instance of target folder
484 * @throws Exception\RuntimeException
486 public function moveMessage($id, $folder)
488 $this->copyMessage($id, $folder);
489 $this->removeMessage($id);
493 * set flags for message
495 * NOTE: this method can't set the recent flag.
497 * @param int $id number of message
498 * @param array $flags new flags for message
499 * @throws Exception\RuntimeException
501 public function setFlags($id, $flags)
503 if (!$this->protocol
->store($flags, $id)) {
504 throw new Exception\
RuntimeException('cannot set flags, have you tried to set the recent flag or special chars?');