blob: 451fefcd816634d144293f0bb3f4306bb554d4d9 [file] [log] [blame]
<?php
/**
* File containing the ezcMailImapTransport class.
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
* @package Mail
* @version //autogen//
* @copyright Copyright (C) 2005-2010 eZ Systems AS. All rights reserved.
* @license http://www.apache.org/licenses/LICENSE-2.0 Apache License, Version 2.0
*/
/**
* The class ezcMailImapTransport implements functionality for handling IMAP
* mail servers.
*
* The implementation supports most of the commands specified in:
* - {@link http://www.faqs.org/rfcs/rfc1730.html} (IMAP4)
* - {@link http://www.faqs.org/rfcs/rfc2060.html} (IMAP4rev1)
*
* Each user account on the IMAP server has it's own folders (mailboxes).
* Mailboxes can be created, renamed or deleted. All accounts have a special
* mailbox called Inbox which cannot be deleted or renamed.
*
* Messages are organized in mailboxes, and are identified by a message number
* (which can change over time) and a unique ID (which does not change under
* normal circumstances). The commands operating on messages can handle both
* modes (message numbers or unique IDs).
*
* Messages are marked by certain flags (SEEN, DRAFT, etc). Deleting a message
* actually sets it's DELETED flag, and a later call to {@link expunge()} will
* delete all the messages marked with the DELETED flag.
*
* The IMAP server can be in different states. Most IMAP commands require
* that a connection is established and a user is authenticated. Certain
* commands require in addition that a mailbox is selected.
*
* The IMAP transport class allows developers to interface with an IMAP server.
* The commands which support unique IDs to refer to messages are marked with
* [*] (see {@link ezcMailImapTransportOptions} to find out how to enable
* unique IDs referencing):
*
* Basic commands:
* - connect to an IMAP server ({@link __construct()})
* - authenticate a user with a username and password ({@link authenticate()})
* - select a mailbox ({@link selectMailbox()})
* - disconnect from the IMAP server ({@link disconnect()})
*
* Work with mailboxes:
* - get the list of mailboxes of the user ({@link listMailboxes()})
* - create a mailbox ({@link createMailbox()})
* - rename a mailbox ({@link renameMailbox()})
* - delete a mailbox ({@link deleteMailbox()})
* - append a message to a mailbox ({@link append()})
* - select a mailbox ({@link selectMailbox()})
* - get the status of messages in the current mailbox ({@link status()})
* - get the number of messages with a certain flag ({@link countByFlag()})
*
* Work with message numbers (on the currently selected mailbox):
* - get the message numbers and sizes of all the messages ({@link listMessages()})
* - get the message numbers and IDs of all the messages ({@link listUniqueIdentifiers()})
* - [*] get the headers of a certain message ({@link top()})
* - [*] delete a message ({@link delete()} and {@link expunge()})
* - [*] copy messages to another mailbox ({@link copyMessages()})
* - [*] get the sizes of the specified messages ({@link fetchSizes()})
*
* Work with flags (on the currently selected mailbox):
* - [*] get the flags of the specified messages ({@link fetchFlags()})
* - [*] set a flag on the specified messages ({@link setFlag()})
* - [*] clear a flag from the specified messages ({@link clearFlag()})
*
* Work with {@link ezcMailImapSet} sets (parseable with {@link ezcMailParser})
* (on the currently selected mailbox):
* - [*] create a set from all messages ({@link fetchAll()})
* - [*] create a set from a certain message ({@link fetchByMessageNr()})
* - [*] create a set from a range of messages ({@link fetchFromOffset()})
* - [*] create a set from messages with a certain flag ({@link fetchByFlag()})
* - [*] create a set from a sorted range of messages ({@link sortFromOffset()})
* - [*] create a set from a sorted list of messages ({@link sortMessages()})
* - [*] create a set from a free-form search ({@link searchMailbox()})
*
* Miscellaneous commands:
* - get the capabilities of the IMAP server ({@link capability()})
* - get the hierarchy delimiter (useful for nested mailboxes) ({@link getHierarchyDelimiter()})
* - issue a NOOP command to keep the connection alive ({@link noop()})
*
* The usual operation with an IMAP server is illustrated by this example:
* <code>
* // create a new IMAP transport object by specifying the server name, optional port
* // and optional SSL mode
* $options = new ezcMailImapTransportOptions();
* $options->ssl = true;
* $imap = new ezcMailImapTransport( 'imap.example.com', null, $options );
*
* // Authenticate to the IMAP server
* $imap->authenticate( 'username', 'password' );
*
* // Select a mailbox (here 'Inbox')
* $imap->selectMailbox( 'Inbox' );
*
* // issue commands to the IMAP server
* // for example get the number of RECENT messages
* $recent = $imap->countByFlag( 'RECENT' );
*
* // see the above list of commands or consult the online documentation for
* // the full list of commands you can issue to an IMAP server and examples
*
* // disconnect from the IMAP server
* $imap->disconnect();
* </code>
*
* See {@link ezcMailImapTransportOptions} for other options you can specify
* for IMAP.
*
* @todo ignore messages of a certain size?
* @todo // support for signing?
* @todo listUniqueIdentifiers(): add UIVALIDITY value to UID (like in POP3).
* (if necessary).
*
* @property ezcMailImapTransportOptions $options
* Holds the options you can set to the IMAP transport.
*
* @package Mail
* @version //autogen//
* @mainclass
*/
class ezcMailImapTransport
{
/**
* Internal state when the IMAP transport is not connected to a server.
*
* @access private
*/
const STATE_NOT_CONNECTED = 1;
/**
* Internal state when the IMAP transport is connected to a server,
* but no successful authentication has been performed.
*
* @access private
*/
const STATE_NOT_AUTHENTICATED = 2;
/**
* Internal state when the IMAP transport is connected to a server
* and authenticated, but no mailbox is selected yet.
*
* @access private
*/
const STATE_AUTHENTICATED = 3;
/**
* Internal state when the IMAP transport is connected to a server,
* authenticated, and a mailbox is selected.
*
* @access private
*/
const STATE_SELECTED = 4;
/**
* Internal state when the IMAP transport is connected to a server,
* authenticated, and a mailbox is selected read only.
*
* @access private
*/
const STATE_SELECTED_READONLY = 5;
/**
* Internal state when the LOGOUT command has been issued to the IMAP
* server, but before the disconnect has taken place.
*
* @access private
*/
const STATE_LOGOUT = 6;
/**
* The response sent from the IMAP server is "OK".
*
* @access private
*/
const RESPONSE_OK = 1;
/**
* The response sent from the IMAP server is "NO".
*
* @access private
*/
const RESPONSE_NO = 2;
/**
* The response sent from the IMAP server is "BAD".
*
* @access private
*/
const RESPONSE_BAD = 3;
/**
* The response sent from the IMAP server is untagged (starts with "*").
*
* @access private
*/
const RESPONSE_UNTAGGED = 4;
/**
* The response sent from the IMAP server requires the client to send
* information (starts with "+").
*
* @access private
*/
const RESPONSE_FEEDBACK = 5;
/**
* Use UID commands (access messages by their unique ID).
*
* @access private
*/
const UID = 'UID ';
/**
* Use message number commands (access messages by their message numbers).
*
* @access private
*/
const NO_UID = '';
/**
* The string returned by Google IMAP servers when at connection time.
*
* @access private
*/
const SERVER_GIMAP = 'Gimap';
/**
* Basic flags are used by {@link setFlag()} and {@link clearFlag()}
*
* Basic flags:
* - ANSWERED - message has been answered
* - DELETED - message is marked to be deleted by later EXPUNGE
* - DRAFT - message is marked as a draft
* - FLAGGED - message is "flagged" for urgent/special attention
* - SEEN - message has been read
*
* @var array(string)
*/
protected static $basicFlags = array( 'ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'SEEN' );
/**
* Extended flags are used by {@link searchByFlag()}
*
* Basic flags:
* - ANSWERED - message has been answered
* - DELETED - message is marked to be deleted by later EXPUNGE
* - DRAFT - message is marked as a draft
* - FLAGGED - message is "flagged" for urgent/special attention
* - RECENT - message is recent
* - SEEN - message has been read
*
* Opposites of the above flags:
* - UNANSWERED
* - UNDELETED
* - UNDRAFT
* - UNFLAGGED
* - OLD
* - UNSEEN
*
* Composite flags:
* - NEW - equivalent to RECENT + UNSEEN
* - ALL - all the messages
*
* @var array(string)
*/
protected static $extendedFlags = array( 'ALL', 'ANSWERED', 'DELETED', 'DRAFT', 'FLAGGED', 'NEW', 'OLD', 'RECENT', 'SEEN', 'UNANSWERED', 'UNDELETED', 'UNDRAFT', 'UNFLAGGED', 'UNRECENT', 'UNSEEN' );
/**
* Used to generate a tag for sending commands to the IMAP server.
*
* @var string
*/
protected $currentTag = 'A0000';
/**
* Holds the connection state.
*
* @var int {@link STATE_NOT_CONNECTED},
* {@link STATE_NOT_AUTHENTICATED},
* {@link STATE_AUTHENTICATED},
* {@link STATE_SELECTED},
* {@link STATE_SELECTED_READONLY} or
* {@link STATE_LOGOUT}.
*/
protected $state = self::STATE_NOT_CONNECTED;
/**
* Holds the currently selected mailbox.
*
* @var string
*/
protected $selectedMailbox = null;
/**
* Holds the connection to the IMAP server.
*
* @var ezcMailTransportConnection
*/
protected $connection = null;
/**
* Holds the string which identifies the IMAP server type.
*
* Used for fixing problems with Google IMAP (see issue #14360). Possible
* values are {@link self::SERVER_GIMAP} or null for all other servers.
*
* @todo Add identification strings for each existing IMAP server?
*
* @var string
*/
protected $serverType = null;
/**
* Holds the options for an IMAP transport connection.
*
* @var ezcMailImapTransportOptions
*/
private $options;
/**
* Creates a new IMAP transport and connects to the $server at $port.
*
* You can specify the $port if the IMAP server is not on the default port
* 993 (for SSL connections) or 143 (for plain connections). Use the $options
* parameter to specify an SSL connection.
*
* See {@link ezcMailImapTransportOptions} for options you can specify for
* IMAP.
*
* Example of creating an IMAP transport:
* <code>
* // replace with your IMAP server address
* $imap = new ezcMailImapTransport( 'imap.example.com' );
*
* // if you want to use SSL:
* $options = new ezcMailImapTransportOptions();
* $options->ssl = true;
*
* $imap = new ezcMailImapTransport( 'imap.example.com', null, $options );
* </code>
*
* @throws ezcMailTransportException
* if it was not possible to connect to the server
* @throws ezcBaseExtensionNotFoundException
* if trying to use SSL and the extension openssl is not installed
* @throws ezcBasePropertyNotFoundException
* if $options contains a property not defined
* @throws ezcBaseValueException
* if $options contains a property with a value not allowed
* @param string $server
* @param int $port
* @param ezcMailImapTransportOptions|array(string=>mixed) $options
*/
public function __construct( $server, $port = null, $options = array() )
{
if ( $options instanceof ezcMailImapTransportOptions )
{
$this->options = $options;
}
else if ( is_array( $options ) )
{
$this->options = new ezcMailImapTransportOptions( $options );
}
else
{
throw new ezcBaseValueException( "options", $options, "ezcMailImapTransportOptions|array" );
}
if ( $port === null )
{
$port = ( $this->options->ssl === true ) ? 993 : 143;
}
$this->connection = new ezcMailTransportConnection( $server, $port, $this->options );
// get the server greeting
$response = $this->connection->getLine();
if ( strpos( $response, "* OK" ) === false )
{
throw new ezcMailTransportException( "The connection to the IMAP server is ok, but a negative response from server was received. Try again later." );
}
if ( strpos( $response, self::SERVER_GIMAP ) !== false )
{
$this->serverType = self::SERVER_GIMAP; // otherwise it is null
}
$this->state = self::STATE_NOT_AUTHENTICATED;
}
/**
* Destructs the IMAP transport.
*
* If there is an open connection to the IMAP server it is closed.
*/
public function __destruct()
{
try
{
$this->disconnect();
}
catch ( ezcMailTransportException $e )
{
// Ignore occuring transport exceptions.
}
}
/**
* Sets the value of the property $name to $value.
*
* @throws ezcBasePropertyNotFoundException
* if the property $name does not exist
* @throws ezcBaseValueException
* if $value is not accepted for the property $name
* @param string $name
* @param mixed $value
* @ignore
*/
public function __set( $name, $value )
{
switch ( $name )
{
case 'options':
if ( !( $value instanceof ezcMailImapTransportOptions ) )
{
throw new ezcBaseValueException( 'options', $value, 'instanceof ezcMailImapTransportOptions' );
}
$this->options = $value;
break;
default:
throw new ezcBasePropertyNotFoundException( $name );
}
}
/**
* Returns the value of the property $name.
*
* @throws ezcBasePropertyNotFoundException
* if the property $name does not exist
* @param string $name
* @ignore
*/
public function __get( $name )
{
switch ( $name )
{
case 'options':
return $this->options;
default:
throw new ezcBasePropertyNotFoundException( $name );
}
}
/**
* Returns true if the property $name is set, otherwise false.
*
* @param string $name
* @return bool
* @ignore
*/
public function __isset( $name )
{
switch ( $name )
{
case 'options':
return true;
default:
return false;
}
}
/**
* Disconnects the transport from the IMAP server.
*/
public function disconnect()
{
if ( $this->state !== self::STATE_NOT_CONNECTED
&& $this->connection->isConnected() === true )
{
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} LOGOUT" );
// discard the "bye bye" message ("{$tag} OK Logout completed.")
$this->getResponse( $tag );
$this->state = self::STATE_LOGOUT;
$this->selectedMailbox = null;
$this->connection->close();
$this->connection = null;
$this->state = self::STATE_NOT_CONNECTED;
}
}
/**
* Authenticates the user to the IMAP server with $user and $password.
*
* This method should be called directly after the construction of this
* object.
*
* If the server is waiting for the authentication process to respond, the
* connection with the IMAP server will be closed, and false is returned,
* and it is the application's task to reconnect and reauthenticate.
*
* Example of creating an IMAP transport and authenticating:
* <code>
* // replace with your IMAP server address
* $imap = new ezcMailImapTransport( 'imap.example.com' );
*
* // replace the values with your username and password for the IMAP server
* $imap->authenticate( 'username', 'password' );
* </code>
*
* @throws ezcMailTransportException
* if already authenticated
* or if the provided username/password combination did not work
* @param string $user
* @param string $password
* @return bool
*/
public function authenticate( $user, $password )
{
if ( $this->state != self::STATE_NOT_AUTHENTICATED )
{
throw new ezcMailTransportException( "Tried to authenticate when there was no connection or when already authenticated." );
}
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} LOGIN {$user} {$password}" );
$response = trim( $this->connection->getLine() );
// hack for gmail, to fix issue #15837: imap.google.com (google gmail) changed IMAP response
if ( $this->serverType === self::SERVER_GIMAP && strpos( $response, "* CAPABILITY" ) === 0 )
{
$response = trim( $this->connection->getLine() );
}
if ( strpos( $response, '* OK' ) !== false )
{
// the server is busy waiting for authentication process to
// respond, so it is a good idea to just close the connection,
// otherwise the application will be halted until the server
// recovers
$this->connection->close();
$this->connection = null;
$this->state = self::STATE_NOT_CONNECTED;
return false;
}
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server did not accept the username and/or password: {$response}." );
}
else
{
$this->state = self::STATE_AUTHENTICATED;
$this->selectedMailbox = null;
}
return true;
}
/**
* Returns an array with the names of the available mailboxes for the user
* currently authenticated on the IMAP server.
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully.
*
* For more information about $reference and $mailbox, consult
* the IMAP RFCs documents ({@link http://www.faqs.org/rfcs/rfc1730.html}
* or {@link http://www.faqs.org/rfcs/rfc2060.html}, section 7.2.2.).
*
* By default, $reference is "" and $mailbox is "*".
*
* The array returned contains the mailboxes available for the connected
* user on this IMAP server. Inbox is a special mailbox, and it can be
* specified upper-case or lower-case or mixed-case. The other mailboxes
* should be specified as they are (to the {@link selectMailbox()} method).
*
* Example of listing mailboxes:
* <code>
* $imap = new ezcMailImapTransport( 'imap.example.com' );
* $imap->authenticate( 'username', 'password' );
*
* $mailboxes = $imap->listMailboxes();
* </code>
*
* @throws ezcMailMailTransportException
* if the current server state is not accepted
* or if the server sent a negative response
* @param string $reference
* @param string $mailbox
* @return array(string)
*/
public function listMailboxes( $reference = '', $mailbox = '*' )
{
if ( $this->state != self::STATE_AUTHENTICATED &&
$this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can't call listMailboxes() when not successfully logged in." );
}
$result = array();
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} LIST \"{$reference}\" \"{$mailbox}\"" );
$response = trim( $this->connection->getLine() );
while ( strpos( $response, '* LIST (' ) !== false )
{
// only consider the selectable mailboxes
if ( strpos( $response, "\\Noselect" ) === false )
{
$response = substr( $response, strpos( $response, "\" " ) + 2 );
$response = trim( $response );
$response = trim( $response, "\"" );
$result[] = $response;
}
$response = $this->connection->getLine();
}
$response = $this->getResponse( $tag, $response );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "Could not list mailboxes with the parameters '\"{$reference}\"' and '\"{$mailbox}\"': {$response}." );
}
return $result;
}
/**
* Returns the hierarchy delimiter of the IMAP server, useful for handling
* nested IMAP folders.
*
* For more information about the hierarchy delimiter, consult the IMAP RFCs
* {@link http://www.faqs.org/rfcs/rfc1730.html} or
* {@link http://www.faqs.org/rfcs/rfc2060.html}, section 6.3.8.
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully.
*
* Example of returning the hierarchy delimiter:
* <code>
* $imap = new ezcMailImapTransport( 'imap.example.com' );
* $imap->authenticate( 'username', 'password' );
*
* $delimiter = $imap->getDelimiter();
* </code>
*
* After running the above code, $delimiter should be something like "/".
*
* @throws ezcMailMailTransportException
* if the current server state is not accepted
* or if the server sent a negative response
* @return string
*/
public function getHierarchyDelimiter()
{
if ( $this->state != self::STATE_AUTHENTICATED &&
$this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can't call getDelimiter() when not successfully logged in." );
}
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} LIST \"\" \"\"" );
// there should be only one * LIST response line from IMAP
$response = trim( $this->getResponse( '* LIST' ) );
$parts = explode( '"', $response );
if ( count( $parts ) >= 2 )
{
$result = $parts[1];
}
else
{
throw new ezcMailTransportException( "Could not retrieve the hierarchy delimiter: {$response}." );
}
$response = $this->getResponse( $tag, $response );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "Could not retrieve the hierarchy delimiter: {$response}." );
}
return $result;
}
/**
* Selects the mailbox $mailbox, which will be the active mailbox for the
* subsequent commands until it is changed.
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully.
*
* Inbox is a special mailbox and can be specified with any case.
*
* This method should be called after authentication, and before fetching
* any messages.
*
* Example of selecting a mailbox:
* <code>
* $imap = new ezcMailImapTransport( 'imap.example.com' );
* $imap->authenticate( 'username', 'password' );
*
* $imap->selectMailbox( 'Reports 2006' );
* </code>
*
* @throws ezcMailMailTransportException
* if the current server state is not accepted
* or if the server sent a negative response
* @param string $mailbox
* @param bool $readOnly
*/
public function selectMailbox( $mailbox, $readOnly = false )
{
if ( $this->state != self::STATE_AUTHENTICATED &&
$this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can't call selectMailbox() when not successfully logged in." );
}
$tag = $this->getNextTag();
// if the mailbox selection will be successful, $state will be STATE_SELECTED
// or STATE_SELECTED_READONLY, depending on the $readOnly parameter
if ( $readOnly !== true )
{
$this->connection->sendData( "{$tag} SELECT \"{$mailbox}\"" );
$state = self::STATE_SELECTED;
}
else
{
$this->connection->sendData( "{$tag} EXAMINE \"{$mailbox}\"" );
$state = self::STATE_SELECTED_READONLY;
}
// if the selecting of the mailbox fails (with "NO" or "BAD" response
// from the server), $state reverts to STATE_AUTHENTICATED
$response = trim( $this->getResponse( $tag ) );
if ( $this->responseType( $response ) == self::RESPONSE_OK )
{
$this->state = $state;
$this->selectedMailbox = $mailbox;
}
else
{
$this->state = self::STATE_AUTHENTICATED;
$this->selectedMailbox = null;
throw new ezcMailTransportException( "Could not select mailbox '{$mailbox}': {$response}." );
}
}
/**
* Creates the mailbox $mailbox.
*
* Inbox cannot be created.
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully.
*
* @throws ezcMailTransportException
* if the current server state is not accepted
* or if the server sent a negative response
* @param string $mailbox
* @return bool
*/
public function createMailbox( $mailbox )
{
if ( $this->state != self::STATE_AUTHENTICATED &&
$this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can't call createMailbox() when not successfully logged in." );
}
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} CREATE \"{$mailbox}\"" );
$response = trim( $this->getResponse( $tag ) );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not create mailbox '{$mailbox}': {$response}." );
}
return true;
}
/**
* Renames the mailbox $mailbox to $newName.
*
* Inbox cannot be renamed.
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully.
*
* @throws ezcMailTransportException
* if the current server state is not accepted
* or if trying to rename the currently selected mailbox
* or if the server sent a negative response
* @param string $mailbox
* @param string $newName
* @return bool
*/
public function renameMailbox( $mailbox, $newName )
{
if ( $this->state != self::STATE_AUTHENTICATED &&
$this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can't call renameMailbox() when not successfully logged in." );
}
if ( strtolower( $this->selectedMailbox ) == strtolower( $mailbox ) )
{
throw new ezcMailTransportException( "Can't rename the currently selected mailbox." );
}
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} RENAME \"{$mailbox}\" \"{$newName}\"" );
$response = trim( $this->getResponse( $tag ) );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not rename the mailbox '{$mailbox}' to '{$newName}': {$response}." );
}
return true;
}
/**
* Deletes the mailbox $mailbox.
*
* Inbox and the the currently selected mailbox cannot be deleted.
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully.
*
* @throws ezcMailTransportException
* if the current server state is not accepted
* or if trying to delete the currently selected mailbox
* or if the server sent a negative response
* @param string $mailbox
* @return bool
*/
public function deleteMailbox( $mailbox )
{
if ( $this->state != self::STATE_AUTHENTICATED &&
$this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can't call deleteMailbox() when not successfully logged in." );
}
if ( strtolower( $this->selectedMailbox ) == strtolower( $mailbox ) )
{
throw new ezcMailTransportException( "Can't delete the currently selected mailbox." );
}
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} DELETE \"{$mailbox}\"" );
$response = trim( $this->getResponse( $tag ) );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not delete the mailbox '{$mailbox}': {$response}." );
}
return true;
}
/**
* Copies message(s) from the currently selected mailbox to mailbox
* $destination.
*
* This method supports unique IDs instead of message numbers. See
* {@link ezcMailImapTransportOptions} for how to enable unique IDs
* referencing.
*
* Warning! When using unique IDs referencing and trying to copy a message
* with an ID that does not exist, this method will not throw an exception.
*
* @todo Find out if it is possible to catch this IMAP bug.
*
* $messages can be:
* - a single message number (eg: '1')
* - a message range (eg. '1:4')
* - a message list (eg. '1,2,4')
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected (the mailbox from which messages will be copied).
*
* Example of copying 3 messages to a mailbox:
* <code>
* $imap = new ezcMailImapTransport( 'imap.example.com' );
* $imap->authenticate( 'username', 'password' );
* $imap->selectMailbox( 'Inbox' );
*
* $imap->copyMessages( '1,2,4', 'Reports 2006' );
* </code>
*
* The above code will copy the messages with numbers 1, 2 and 4 from Inbox
* to Reports 2006.
*
* @throws ezcMailTransportException
* if the current server state is not accepted
* or if the server sent a negative response
* @param string $messages
* @param string $destination
* @return bool
*/
public function copyMessages( $messages, $destination )
{
$uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
if ( $this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can't call copyMessages() on the IMAP transport when a mailbox is not selected." );
}
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} {$uid}COPY {$messages} \"{$destination}\"" );
$response = trim( $this->getResponse( $tag ) );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not copy '{$messages}' to '{$destination}': {$response}." );
}
return true;
}
/**
* Returns a list of the not deleted messages in the current mailbox.
*
* It returns only the messages with the flag DELETED not set.
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* The format of the returned array is
* <code>
* array( message_id => size );
* </code>
*
* Example:
* <code>
* array( 2 => 1700, 5 => 1450, 6 => 21043 );
* </code>
*
* If $contentType is set, it returns only the messages with
* $contentType in the Content-Type header.
*
* For example $contentType can be "multipart/mixed" to return only the
* messages with attachments.
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* @param string $contentType
* @return array(int)
*/
public function listMessages( $contentType = null )
{
if ( $this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can't call listMessages() on the IMAP transport when a mailbox is not selected." );
}
$messageList = array();
$messages = array();
// get the numbers of the existing messages
$tag = $this->getNextTag();
$command = "{$tag} SEARCH UNDELETED";
if ( !is_null( $contentType ) )
{
$command .= " HEADER \"Content-Type\" \"{$contentType}\"";
}
$this->connection->sendData( $command );
$response = trim( $this->getResponse( '* SEARCH' ) );
if ( strpos( $response, '* SEARCH' ) !== false )
{
$ids = trim( substr( $response, 9 ) );
if ( $ids !== "" )
{
$messageList = explode( ' ', $ids );
}
}
// skip the OK response ("{$tag} OK Search completed.")
$response = trim( $this->getResponse( $tag, $response ) );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not list messages: {$response}." );
}
if ( !empty( $messageList ) )
{
// get the sizes of the messages
$tag = $this->getNextTag();
$query = trim( implode( ',', $messageList ) );
$this->connection->sendData( "{$tag} FETCH {$query} RFC822.SIZE" );
$response = $this->getResponse( 'FETCH (' );
$currentMessage = trim( reset( $messageList ) );
while ( strpos( $response, 'FETCH (' ) !== false )
{
$line = $response;
$line = explode( ' ', $line );
$line = trim( $line[count( $line ) - 1] );
$line = substr( $line, 0, strlen( $line ) - 1 );
$messages[$currentMessage] = intval( $line );
$currentMessage = next( $messageList );
$response = $this->connection->getLine();
}
// skip the OK response ("{$tag} OK Fetch completed.")
$response = trim( $this->getResponse( $tag, $response ) );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not list messages: {$response}." );
}
}
return $messages;
}
/**
* Fetches the sizes in bytes for messages $messages.
*
* This method supports unique IDs instead of message numbers. See
* {@link ezcMailImapTransportOptions} for how to enable unique IDs
* referencing.
*
* $messages is an array of message numbers, for example:
* <code>
* array( 1, 2, 4 );
* </code>
*
* The format of the returned array is:
* <code>
* array( message_number => size )
* </code>
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* Example:
* <code>
* $imap = new ezcMailImapTransport( 'imap.example.com' );
* $imap->authenticate( 'username', 'password' );
* $imap->selectMailbox( 'mailbox' ); // Inbox or another mailbox
*
* $sizes = $imap->fetchSizes( array( 1, 2, 4 ) );
* </code>
*
* The returned array $sizes will be something like:
* <code>
* array( 1 => 1043,
* 2 => 203901,
* 4 => 14277
* );
* </code>
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* @param array $messages
* @return array(int)
*/
public function fetchSizes( $messages )
{
$uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
if ( $this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can't call fetchSizes() on the IMAP transport when a mailbox is not selected." );
}
$sizes = array();
$ids = implode( $messages, ',' );
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} {$uid}FETCH {$ids} (RFC822.SIZE)" );
$response = trim( $this->connection->getLine() );
while ( strpos( $response, $tag ) === false )
{
if ( strpos( $response, ' FETCH (' ) !== false )
{
if ( $this->options->uidReferencing )
{
preg_match( '/\*\s.*\sFETCH\s\(RFC822\.SIZE\s(.*)\sUID\s(.*)\)/U', $response, $matches );
$sizes[intval( $matches[2] )] = (int) $matches[1];
}
else
{
preg_match( '/\*\s(.*)\sFETCH\s\(RFC822\.SIZE\s(.*)\)/U', $response, $matches );
$sizes[intval( $matches[1] )] = (int) $matches[2];
}
}
$response = trim( $this->connection->getLine() );
}
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not fetch flags for the messages '{$messages}': {$response}." );
}
return $sizes;
}
/**
* Returns information about the messages in the current mailbox.
*
* The information returned through the parameters is:
* - $numMessages = number of not deleted messages in the selected mailbox
* - $sizeMessages = sum of the not deleted messages sizes
* - $recent = number of recent and not deleted messages
* - $unseen = number of unseen and not deleted messages
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* Example of returning the status of the currently selected mailbox:
* <code>
* $imap = new ezcMailImapTransport( 'imap.example.com' );
* $imap->authenticate( 'username', 'password' );
* $imap->selectMailbox( 'Inbox' );
*
* $imap->status( $numMessages, $sizeMessages, $recent, $unseen );
* </code>
*
* After running the above code, $numMessages, $sizeMessages, $recent
* and $unseen will be populated with values.
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* @param int &$numMessages
* @param int &$sizeMessages
* @param int &$recent
* @param int &$unseen
* @return bool
*/
public function status( &$numMessages, &$sizeMessages, &$recent = 0, &$unseen = 0 )
{
if ( $this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can't call status() on the IMAP transport when a mailbox is not selected." );
}
$messages = $this->listMessages();
$numMessages = count( $messages );
$sizeMessages = array_sum( $messages );
$messages = array_keys( $messages );
$recentMessages = array_intersect( $this->searchByFlag( "RECENT" ), $messages );
$unseenMessages = array_intersect( $this->searchByFlag( "UNSEEN" ), $messages );
$recent = count( $recentMessages );
$unseen = count( $unseenMessages );
return true;
}
/**
* Deletes the message with the message number $msgNum from the current mailbox.
*
* This method supports unique IDs instead of message numbers. See
* {@link ezcMailImapTransportOptions} for how to enable unique IDs
* referencing.
*
* The message number $msgNum must be a valid identifier fetched with e.g.
* {@link listMessages()}.
*
* The message is not physically deleted, but has its DELETED flag set,
* and can be later undeleted by clearing its DELETED flag with
* {@link clearFlag()}.
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* @param int $msgNum
* @return bool
*/
public function delete( $msgNum )
{
$uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
if ( $this->state != self::STATE_SELECTED )
{
throw new ezcMailTransportException( "Can't call delete() when a mailbox is not selected." );
}
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} {$uid}STORE {$msgNum} +FLAGS (\\Deleted)" );
// get the response (should be "{$tag} OK Store completed.")
$response = trim( $this->getResponse( $tag ) );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not delete the message '{$msgNum}': {$response}." );
}
return true;
}
/**
* Returns the headers and the first characters from message $msgNum,
* without setting the SEEN flag.
*
* This method supports unique IDs instead of message numbers. See
* {@link ezcMailImapTransportOptions} for how to enable unique IDs
* referencing.
*
* If the command failed or if it was not supported by the server an empty
* string is returned.
*
* This method is useful for retrieving the headers of messages from the
* mailbox as strings, which can be later parsed with {@link ezcMailParser}
* and {@link ezcMailVariableSet}. In this way the retrieval of the full
* messages from the server is avoided when building a list of messages.
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* Example of listing the mail headers of all the messages in the current
* mailbox:
* <code>
* $imap = new ezcMailImapTransport( 'imap.example.com' );
* $imap->authenticate( 'username', 'password' );
* $imap->selectMailbox( 'Inbox' );
*
* $parser = new ezcMailParser();
* $messages = $imap->listMessages();
* foreach ( $messages as $messageNr => $size )
* {
* $set = new ezcMailVariableSet( $imap->top( $messageNr ) );
* $mail = $parser->parseMail( $set );
* $mail = $mail[0];
* echo "From: {$mail->from}, Subject: {$mail->subject}, Size: {$size}\n";
* }
* </code>
*
* For a more advanced example see the "Mail listing example" in the online
* documentation.
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* @param int $msgNum
* @param int $chars
* @return string
*/
public function top( $msgNum, $chars = 0 )
{
$uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
if ( $this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can't call top() on the IMAP transport when a mailbox is not selected." );
}
$tag = $this->getNextTag();
if ( $chars === 0 )
{
$command = "{$tag} {$uid}FETCH {$msgNum} (BODY.PEEK[HEADER] BODY.PEEK[TEXT])";
}
else
{
$command = "{$tag} {$uid}FETCH {$msgNum} (BODY.PEEK[HEADER] BODY.PEEK[TEXT]<0.{$chars}>)";
}
$this->connection->sendData( $command );
if ( $this->options->uidReferencing )
{
// special case (BUG?) where "UID FETCH {$msgNum}" returns nothing
$response = trim( $this->connection->getLine() );
if ( $this->responseType( $response ) === self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not fetch the message '{$msgNum}': {$response}." );
}
}
else
{
$response = $this->getResponse( 'FETCH (' );
}
$message = "";
if ( strpos( $response, 'FETCH (' ) !== false )
{
// Added hack for issue #14360: problems with $imap->top() command in gmail.
if ( $this->serverType === self::SERVER_GIMAP )
{
// Google IMAP servers return the body first, then the headers (!)
$bytesToRead = $this->getMessageSectionSize( $response );
$response = "";
while ( $bytesToRead >= 0 )
{
$data = $this->connection->getLine();
$lastResponse = $data;
$bytesToRead -= strlen( $data );
// in case reading too much and the string "BODY[HEADER] {size}"
// is at the end of the last line
if ( $bytesToRead <= 0 )
{
if ( $bytesToRead < 0 )
{
$lastResponse = substr( $data, $bytesToRead );
$data = substr( $data, 0, strlen( $data ) + $bytesToRead );
}
}
$message .= $data;
}
// Read the headers
$headers = '';
$response = $this->connection->getLine();
$bytesToRead = $this->getMessageSectionSize( $lastResponse );
$response = $this->connection->getLine();
while ( strpos( $response, $tag ) === false )
{
$headers .= $response;
$response = $this->connection->getLine();
}
$headers = trim( $headers, ")\r\n" );
// Append the body AFTER the headers as it should be
$message = $headers . "\r\n\r\n" . $message;
}
else
{
// Other IMAP servers return the headers first, then the body
$response = "";
while ( strpos( $response, 'BODY[TEXT]' ) === false )
{
$message .= $response;
$response = $this->connection->getLine();
}
$response = $this->connection->getLine();
while ( strpos( $response, $tag ) === false )
{
$message .= $response;
$response = $this->connection->getLine();
}
}
}
// skip the OK response ("{$tag} OK Fetch completed.")
$response = trim( $this->getResponse( $tag, $response ) );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not fetch the message '{$msgNum}': {$response}." );
}
return $message;
}
/**
* Returns the unique identifiers for the messages from the current mailbox.
*
* You can fetch the unique identifier for a specific message by
* providing the $msgNum parameter.
*
* The unique identifier can be used to recognize mail from servers
* between requests. In contrast to the message numbers the unique
* numbers assigned to an email usually never changes.
*
* The format of the returned array is:
* <code>
* array( message_num => unique_id );
* </code>
*
* Example:
* <code>
* array( 1 => 216, 2 => 217, 3 => 218, 4 => 219 );
* </code>
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* @todo add UIVALIDITY value to UID (like in POP3) (if necessary).
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* @param int $msgNum
* @return array(string)
*/
public function listUniqueIdentifiers( $msgNum = null )
{
if ( $this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can't call listUniqueIdentifiers() on the IMAP transport when a mailbox is not selected." );
}
$result = array();
if ( $msgNum !== null )
{
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} UID SEARCH {$msgNum}" );
$response = $this->getResponse( '* SEARCH' );
if ( strpos( $response, '* SEARCH' ) !== false )
{
$result[(int)$msgNum] = trim( substr( $response, 9 ) );
}
$response = trim( $this->getResponse( $tag, $response ) );
}
else
{
$uids = array();
$messages = array_keys( $this->listMessages() );
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} UID SEARCH UNDELETED" );
$response = $this->getResponse( '* SEARCH' );
if ( strpos( $response, '* SEARCH' ) !== false )
{
$response = trim( substr( $response, 9 ) );
if ( $response !== "" )
{
$uids = explode( ' ', $response );
}
for ( $i = 0; $i < count( $messages ); $i++ )
{
$result[trim( $messages[$i] )] = $uids[$i];
}
}
$response = trim( $this->getResponse( $tag ) );
}
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not fetch the unique identifiers: {$response}." );
}
return $result;
}
/**
* Returns an {@link ezcMailImapSet} with all the messages from the current mailbox.
*
* This method supports unique IDs instead of message numbers. See
* {@link ezcMailImapTransportOptions} for how to enable unique IDs
* referencing.
*
* If $deleteFromServer is set to true the mail will be marked for deletion
* after retrieval. If not it will be left intact.
*
* The set returned can be parsed with {@link ezcMailParser}.
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* Example:
* <code>
* $imap = new ezcMailImapTransport( 'imap.example.com' );
* $imap->authenticate( 'username', 'password' );
* $imap->selectMailbox( 'Inbox' );
*
* $set = $imap->fetchAll();
*
* // parse $set with ezcMailParser
* $parser = new ezcMailParser();
* $mails = $parser->parseMail( $set );
* foreach ( $mails as $mail )
* {
* // process $mail which is an ezcMail object
* }
* </code>
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* @param bool $deleteFromServer
* @return ezcMailParserSet
*/
public function fetchAll( $deleteFromServer = false )
{
if ( $this->options->uidReferencing )
{
$messages = array_values( $this->listUniqueIdentifiers() );
}
else
{
$messages = array_keys( $this->listMessages() );
}
return new ezcMailImapSet( $this->connection, $messages, $deleteFromServer, array( 'uidReferencing' => $this->options->uidReferencing ) );
}
/**
* Returns an {@link ezcMailImapSet} containing only the $number -th message in
* the current mailbox.
*
* This method supports unique IDs instead of message numbers. See
* {@link ezcMailImapTransportOptions} for how to enable unique IDs
* referencing.
*
* If $deleteFromServer is set to true the mail will be marked for deletion
* after retrieval. If not it will be left intact.
*
* Note: for IMAP the first message is 1 (so for $number = 0 an exception
* will be thrown).
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* Example:
* <code>
* $imap = new ezcMailImapTransport( 'imap.example.com' );
* $imap->authenticate( 'username', 'password' );
* $imap->selectMailbox( 'Inbox' );
*
* $set = $imap->fetchByMessageNr( 1 );
*
* // $set can be parsed with ezcMailParser
* </code>
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* @throws ezcMailNoSuchMessageException
* if the message $number is out of range
* @param int $number
* @param bool $deleteFromServer
* @return ezcMailImapSet
*/
public function fetchByMessageNr( $number, $deleteFromServer = false )
{
if ( $this->options->uidReferencing )
{
$messages = array_flip( $this->listUniqueIdentifiers() );
}
else
{
$messages = $this->listMessages();
}
if ( !isset( $messages[$number] ) )
{
throw new ezcMailNoSuchMessageException( $number );
}
return new ezcMailImapSet( $this->connection, array( 0 => $number ), $deleteFromServer, array( 'uidReferencing' => $this->options->uidReferencing ) );
}
/**
* Returns an {@link ezcMailImapSet} with $count messages starting from $offset from
* the current mailbox.
*
* This method supports unique IDs instead of message numbers. See
* {@link ezcMailImapTransportOptions} for how to enable unique IDs
* referencing.
*
* Fetches $count messages starting from the $offset and returns them as a
* {@link ezcMailImapSet}. If $count is not specified or if it is 0, it fetches
* all messages starting from the $offset.
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* Example:
* <code>
* $imap = new ezcMailImapTransport( 'imap.example.com' );
* $imap->authenticate( 'username', 'password' );
* $imap->selectMailbox( 'Inbox' );
*
* $set = $imap->fetchFromOffset( 1, 10 );
*
* // $set can be parsed with ezcMailParser
* </code>
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* @throws ezcMailInvalidLimitException
* if $count is negative
* @throws ezcMailOffsetOutOfRangeException
* if $offset is outside of the existing range of messages
* @param int $offset
* @param int $count
* @param bool $deleteFromServer
* @return ezcMailImapSet
*/
public function fetchFromOffset( $offset, $count = 0, $deleteFromServer = false )
{
if ( $count < 0 )
{
throw new ezcMailInvalidLimitException( $offset, $count );
}
if ( $this->options->uidReferencing )
{
$messages = array_values( $this->listUniqueIdentifiers() );
$ids = array_flip( $messages );
if ( $count === 0 )
{
$count = count( $messages );
}
if ( !isset( $ids[$offset] ) )
{
throw new ezcMailOffsetOutOfRangeException( $offset, $count );
}
$range = array();
for ( $i = $ids[$offset]; $i < min( $count, count( $messages ) ); $i++ )
{
$range[] = $messages[$i];
}
}
else
{
$messages = array_keys( $this->listMessages() );
if ( $count === 0 )
{
$count = count( $messages );
}
$range = array_slice( $messages, $offset - 1, $count, true );
if ( !isset( $range[$offset - 1] ) )
{
throw new ezcMailOffsetOutOfRangeException( $offset, $count );
}
}
return new ezcMailImapSet( $this->connection, $range, $deleteFromServer, array( 'uidReferencing' => $this->options->uidReferencing ) );
}
/**
* Returns an {@link ezcMailImapSet} containing the messages which match the
* provided $criteria from the current mailbox.
*
* This method supports unique IDs instead of message numbers. See
* {@link ezcMailImapTransportOptions} for how to enable unique IDs
* referencing.
*
* See {@link http://www.faqs.org/rfcs/rfc1730.html} - 6.4.4. (or
* {@link http://www.faqs.org/rfcs/rfc1730.html} - 6.4.4.) for criterias
* which can be used for searching. The criterias can be combined in the
* same search string (separate the criterias with spaces).
*
* If $criteria is null or empty then it will default to 'ALL' (returns all
* messages in the mailbox).
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* Examples:
* <code>
* $imap = new ezcMailImapTransport( 'imap.example.com' );
* $imap->authenticate( 'username', 'password' );
* $imap->selectMailbox( 'mailbox' ); // Inbox or another mailbox
*
* // return an ezcMailImapSet containing all messages flagged as 'SEEN'
* $set = $imap->searchMailbox( 'SEEN' );
*
* // return an ezcMailImapSet containing messages with 'release' in their Subject
* $set = $imap->searchMailbox( 'SUBJECT "release"' );
*
* // criterias can be combined:
* // return an ezcMailImapSet containing messages flagged as 'SEEN' and
* // with 'release' in their Subject
* $set = $imap->searchMailbox( 'SEEN SUBJECT "release"' );
*
* // $set can be parsed with ezcMailParser
* </code>
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* @param string $criteria
* @return ezcMailImapSet
*/
public function searchMailbox( $criteria = null )
{
$uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
if ( $this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can't call searchMailbox() on the IMAP transport when a mailbox is not selected." );
}
$criteria = trim( $criteria );
if ( empty( $criteria ) )
{
$criteria = 'ALL';
}
$matchingMessages = array();
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} {$uid}SEARCH {$criteria}" );
$response = $this->getResponse( '* SEARCH' );
if ( strpos( $response, '* SEARCH' ) !== false )
{
$ids = substr( trim( $response ), 9 );
if ( trim( $ids ) !== "" )
{
$matchingMessages = explode( ' ', $ids );
}
}
$response = trim( $this->getResponse( $tag, $response ) );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not search the messages by the specified criteria: {$response}." );
}
return new ezcMailImapSet( $this->connection, array_values( $matchingMessages ), false, array( 'uidReferencing' => $this->options->uidReferencing ) );
}
/**
* Returns an {@link ezcMailImapSet} containing $count messages starting
* from $offset sorted by $sortCriteria from the current mailbox.
*
* This method supports unique IDs instead of message numbers. See
* {@link ezcMailImapTransportOptions} for how to enable unique IDs
* referencing.
*
* It is useful for paging through a mailbox.
*
* Fetches $count messages starting from the $offset and returns them as a
* {@link ezcMailImapSet}. If $count is is 0, it fetches all messages
* starting from the $offset.
*
* $sortCriteria is an email header like: Subject, To, From, Date, Sender, etc.
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* Example:
* <code>
* $imap = new ezcMailImapTransport( 'imap.example.com' );
* $imap->authenticate( 'username', 'password' );
* $imap->selectMailbox( 'mailbox' ); // Inbox or another mailbox
*
* // Fetch a range of messages sorted by Date
* $set = $imap->sortFromOffset( 1, 10, "Date" );
*
* // $set can be parsed with ezcMailParser
* </code>
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* @throws ezcMailInvalidLimitException
* if $count is negative
* @throws ezcMailOffsetOutOfRangeException
* if $offset is outside of the existing range of messages
* @param int $offset
* @param int $count
* @param string $sortCriteria
* @param bool $reverse
* @return ezcMailImapSet
*/
public function sortFromOffset( $offset, $count = 0, $sortCriteria, $reverse = false )
{
if ( $count < 0 )
{
throw new ezcMailInvalidLimitException( $offset, $count );
}
$range = array();
if ( $this->options->uidReferencing )
{
$uids = array_values( $this->listUniqueIdentifiers() );
$flip = array_flip( $uids );
if ( !isset( $flip[$offset] ) )
{
throw new ezcMailOffsetOutOfRangeException( $offset, $count );
}
$start = $flip[$offset];
$messages = $this->sort( $uids, $sortCriteria, $reverse );
if ( $count === 0 )
{
$count = count( $messages );
}
$ids = array_keys( $messages );
for ( $i = $start; $i < $count; $i++ )
{
$range[] = $ids[$i];
}
}
else
{
$messageCount = $this->countByFlag( 'ALL' );
$messages = array_keys( $this->sort( range( 1, $messageCount ), $sortCriteria, $reverse ) );
if ( $count === 0 )
{
$count = count( $messages );
}
$range = array_slice( $messages, $offset - 1, $count, true );
if ( !isset( $range[$offset - 1] ) )
{
throw new ezcMailOffsetOutOfRangeException( $offset, $count );
}
}
return new ezcMailImapSet( $this->connection, $range, false, array( 'uidReferencing' => $this->options->uidReferencing ) );
}
/**
* Returns an {@link ezcMailImapSet} containing messages $messages sorted by
* $sortCriteria from the current mailbox.
*
* This method supports unique IDs instead of message numbers. See
* {@link ezcMailImapTransportOptions} for how to enable unique IDs
* referencing.
*
* $messages is an array of message numbers, for example:
* <code>
* array( 1, 2, 4 );
* </code>
*
* $sortCriteria is an email header like: Subject, To, From, Date, Sender, etc.
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* Example:
* <code>
* $imap = new ezcMailImapTransport( 'imap.example.com' );
* $imap->authenticate( 'username', 'password' );
* $imap->selectMailbox( 'mailbox' ); // Inbox or another mailbox
*
* // Fetch the list of messages sorted by Date
* $set = $imap->sortMessages( 1, 10, "Date" );
*
* // $set can be parsed with ezcMailParser
* </code>
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* or if array $messages is empty
* @param array(int) $messages
* @param string $sortCriteria
* @param bool $reverse
* @return ezcMailImapSet
*/
public function sortMessages( $messages, $sortCriteria, $reverse = false )
{
$messages = $this->sort( $messages, $sortCriteria, $reverse );
return new ezcMailImapSet( $this->connection, array_keys ( $messages ), false, array( 'uidReferencing' => $this->options->uidReferencing ) );
}
/**
* Returns an {@link ezcMailImapSet} containing messages with a certain flag from
* the current mailbox.
*
* This method supports unique IDs instead of message numbers. See
* {@link ezcMailImapTransportOptions} for how to enable unique IDs
* referencing.
*
* $flag can be one of:
*
* Basic flags:
* - ANSWERED - message has been answered
* - DELETED - message is marked to be deleted by later EXPUNGE
* - DRAFT - message is marked as a draft
* - FLAGGED - message is "flagged" for urgent/special attention
* - RECENT - message is recent
* - SEEN - message has been read
*
* Opposites of the above flags:
* - UNANSWERED
* - UNDELETED
* - UNDRAFT
* - UNFLAGGED
* - OLD
* - UNSEEN
*
* Composite flags:
* - NEW - equivalent to RECENT + UNSEEN
* - ALL - all the messages
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* Example:
* <code>
* $imap = new ezcMailImapTransport( 'imap.example.com' );
* $imap->authenticate( 'username', 'password' );
* $imap->selectMailbox( 'mailbox' ); // Inbox or another mailbox
*
* // Fetch the messages marked with the RECENT flag
* $set = $imap->fetchByFlag( 'RECENT' );
*
* // $set can be parsed with ezcMailParser
* </code>
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* or if $flag is not valid
* @param string $flag
* @return ezcMailImapSet
*/
public function fetchByFlag( $flag )
{
$messages = $this->searchByFlag( $flag );
return new ezcMailImapSet( $this->connection, $messages, false, array( 'uidReferencing' => $this->options->uidReferencing ) );
}
/**
* Wrapper function to fetch count of messages by a certain flag.
*
* $flag can be one of:
*
* Basic flags:
* - ANSWERED - message has been answered
* - DELETED - message is marked to be deleted by later EXPUNGE
* - DRAFT - message is marked as a draft
* - FLAGGED - message is "flagged" for urgent/special attention
* - RECENT - message is recent
* - SEEN - message has been read
*
* Opposites of the above flags:
* - UNANSWERED
* - UNDELETED
* - UNDRAFT
* - UNFLAGGED
* - OLD
* - UNSEEN
*
* Composite flags:
* - NEW - equivalent to RECENT + UNSEEN
* - ALL - all the messages
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* or if $flag is not valid
* @param string $flag
* @return int
*/
public function countByFlag( $flag )
{
$flag = $this->normalizeFlag( $flag );
$messages = $this->searchByFlag( $flag );
return count( $messages );
}
/**
* Fetches IMAP flags for messages $messages.
*
* This method supports unique IDs instead of message numbers. See
* {@link ezcMailImapTransportOptions} for how to enable unique IDs
* referencing.
*
* $messages is an array of message numbers, for example:
* <code>
* array( 1, 2, 4 );
* </code>
*
* The format of the returned array is:
* <code>
* array( message_number => array( flags ) )
* </code>
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* Example:
* <code>
* $imap = new ezcMailImapTransport( 'imap.example.com' );
* $imap->authenticate( 'username', 'password' );
* $imap->selectMailbox( 'mailbox' ); // Inbox or another mailbox
*
* $flags = $imap->fetchFlags( array( 1, 2, 4 ) );
* </code>
*
* The returned array $flags will be something like:
* <code>
* array( 1 => array( '\Seen' ),
* 2 => array( '\Seen' ),
* 4 => array( '\Seen', 'NonJunk' )
* );
* </code>
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* @param array $messages
* @return array(mixed)
*/
public function fetchFlags( $messages )
{
$uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
if ( $this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can't call fetchFlags() on the IMAP transport when a mailbox is not selected." );
}
$flags = array();
$ids = implode( $messages, ',' );
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} {$uid}FETCH {$ids} (FLAGS)" );
$response = trim( $this->connection->getLine() );
while ( strpos( $response, $tag ) === false )
{
if ( strpos( $response, ' FETCH (' ) !== false )
{
if ( $this->options->uidReferencing )
{
preg_match( '/\*\s.*\sFETCH\s\(FLAGS \((.*)\)\sUID\s(.*)\)/U', $response, $matches );
$parts = explode( ' ', $matches[1] );
$flags[intval( $matches[2] )] = $parts;
}
else
{
preg_match( '/\*\s(.*)\sFETCH\s\(FLAGS \((.*)\)/U', $response, $matches );
$parts = explode( ' ', $matches[2] );
$flags[intval( $matches[1] )] = $parts;
}
}
$response = trim( $this->connection->getLine() );
}
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not fetch flags for the messages '{$messages}': {$response}." );
}
return $flags;
}
/**
* Sets $flag on $messages.
*
* This method supports unique IDs instead of message numbers. See
* {@link ezcMailImapTransportOptions} for how to enable unique IDs
* referencing.
*
* $messages can be:
* - a single message number (eg. 1)
* - a message range (eg. 1:4)
* - a message list (eg. 1,2,4)
*
* $flag can be one of:
* - ANSWERED - message has been answered
* - DELETED - message is marked to be deleted by later EXPUNGE
* - DRAFT - message is marked as a draft
* - FLAGGED - message is "flagged" for urgent/special attention
* - SEEN - message has been read
*
* This function automatically adds the '\' in front of the flag when
* calling the server command.
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* Example:
* <code>
* $imap = new ezcMailImapTransport( 'imap.example.com' );
* $imap->authenticate( 'username', 'password' );
* $imap->selectMailbox( 'mailbox' ); // Inbox or another mailbox
*
* $imap->setFlag( '1:4', 'DRAFT' );
* </code>
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* or if $flag is not valid
* @param string $messages
* @param string $flag
* @return bool
*/
public function setFlag( $messages, $flag )
{
$uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
if ( $this->state != self::STATE_SELECTED )
{
throw new ezcMailTransportException( "Can't call setFlag() when a mailbox is not selected." );
}
$flag = $this->normalizeFlag( $flag );
if ( in_array( $flag, self::$basicFlags ) )
{
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} {$uid}STORE {$messages} +FLAGS (\\{$flag})" );
$response = trim( $this->getResponse( $tag ) );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not set flag '{$flag}' on the messages '{$messages}': {$response}." );
}
}
else
{
throw new ezcMailTransportException( "Flag '{$flag}' is not allowed for setting." );
}
return true;
}
/**
* Clears $flag from $messages.
*
* This method supports unique IDs instead of message numbers. See
* {@link ezcMailImapTransportOptions} for how to enable unique IDs
* referencing.
*
* $messages can be:
* - a single message number (eg. '1')
* - a message range (eg. '1:4')
* - a message list (eg. '1,2,4')
*
* $flag can be one of:
* - ANSWERED - message has been answered
* - DELETED - message is marked to be deleted by later EXPUNGE
* - DRAFT - message is marked as a draft
* - FLAGGED - message is "flagged" for urgent/special attention
* - SEEN - message has been read
*
* This function automatically adds the '\' in front of the flag when
* calling the server command.
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* Example:
* <code>
* $imap = new ezcMailImapTransport( 'imap.example.com' );
* $imap->authenticate( 'username', 'password' );
* $imap->selectMailbox( 'mailbox' ); // Inbox or another mailbox
*
* $imap->clearFlag( '1:4', 'DRAFT' );
* </code>
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* or if $flag is not valid
* @param string $messages
* @param string $flag
* @return bool
*/
public function clearFlag( $messages, $flag )
{
$uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
if ( $this->state != self::STATE_SELECTED )
{
throw new ezcMailTransportException( "Can't call clearFlag() when a mailbox is not selected." );
}
$flag = $this->normalizeFlag( $flag );
if ( in_array( $flag, self::$basicFlags ) )
{
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} {$uid}STORE {$messages} -FLAGS (\\{$flag})" );
$response = trim( $this->getResponse( $tag ) );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not clear flag '{$flag}' on the messages '{$messages}': {$response}." );
}
}
else
{
throw new ezcMailTransportException( "Flag '{$flag}' is not allowed for clearing." );
}
return true;
}
/**
* Returns an array of message numbers from the selected mailbox which have a
* certain flag set.
*
* This method supports unique IDs instead of message numbers. See
* {@link ezcMailImapTransportOptions} for how to enable unique IDs
* referencing.
*
* $flag can be one of:
*
* Basic flags:
* - ANSWERED - message has been answered
* - DELETED - message is marked to be deleted by later EXPUNGE
* - DRAFT - message is marked as a draft
* - FLAGGED - message is "flagged" for urgent/special attention
* - RECENT - message is recent
* - SEEN - message has been read
*
* Opposites of the above flags:
* - UNANSWERED
* - UNDELETED
* - UNDRAFT
* - UNFLAGGED
* - OLD
* - UNSEEN
*
* Composite flags:
* - NEW - equivalent to RECENT + UNSEEN
* - ALL - all the messages
*
* The returned array is something like this:
* <code>
* array( 0 => 1, 1 => 5 );
* </code>
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* or if $flag is not valid
* @param string $flag
* @return array(int)
*/
protected function searchByFlag( $flag )
{
$uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
if ( $this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can't call searchByFlag() on the IMAP transport when a mailbox is not selected." );
}
$matchingMessages = array();
$flag = $this->normalizeFlag( $flag );
if ( in_array( $flag, self::$extendedFlags ) )
{
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} {$uid}SEARCH ({$flag})" );
$response = $this->getResponse( '* SEARCH' );
if ( strpos( $response, '* SEARCH' ) !== false )
{
$ids = substr( trim( $response ), 9 );
if ( trim( $ids ) !== "" )
{
$matchingMessages = explode( ' ', $ids );
}
}
$response = trim( $this->getResponse( $tag, $response ) );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not search the messages by flags: {$response}." );
}
}
else
{
throw new ezcMailTransportException( "Flag '{$flag}' is not allowed for searching." );
}
return $matchingMessages;
}
/**
* Sends a NOOP command to the server, use it to keep the connection alive.
*
* Before calling this method, a connection to the IMAP server must be
* established.
*
* @throws ezcMailTransportException
* if there was no connection to the server
* or if the server sent a negative response
*/
public function noop()
{
if ( $this->state != self::STATE_NOT_AUTHENTICATED &&
$this->state != self::STATE_AUTHENTICATED &&
$this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can not issue NOOP command if not connected." );
}
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} NOOP" );
$response = trim( $this->getResponse( $tag ) );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "NOOP failed: {$response}." );
}
}
/**
* Returns an array with the capabilities of the IMAP server.
*
* The returned array will be something like this:
* <code>
* array( 'IMAP4rev1', 'SASL-IR SORT', 'THREAD=REFERENCES', 'MULTIAPPEND',
* 'UNSELECT', 'LITERAL+', 'IDLE', 'CHILDREN', 'NAMESPACE',
* 'LOGIN-REFERRALS'
* );
* </code>
*
* Before calling this method, a connection to the IMAP server must be
* established.
*
* @throws ezcMailTransportException
* if there was no connection to the server
* or if the server sent a negative response
* @return array(string)
*/
public function capability()
{
if ( $this->state != self::STATE_NOT_AUTHENTICATED &&
$this->state != self::STATE_AUTHENTICATED &&
$this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Trying to request capability when not connected to server." );
}
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} CAPABILITY" );
$response = $this->connection->getLine();
while ( $this->responseType( $response ) != self::RESPONSE_UNTAGGED &&
strpos( $response, '* CAPABILITY ' ) === false )
{
$response = $this->connection->getLine();
}
$result = trim( $response );
$response = trim( $this->getResponse( $tag ) );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server responded negative to the CAPABILITY command: {$response}." );
}
return explode( ' ', str_replace( '* CAPABILITY ', '', $result ) );
}
/**
* Sends an EXPUNGE command to the server.
*
* This method permanently deletes the messages marked for deletion by
* the method {@link delete()}.
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* @throws ezcMailTransportException
* if a mailbox was not selected
* or if the server sent a negative response
*/
public function expunge()
{
if ( $this->state != self::STATE_SELECTED )
{
throw new ezcMailTransportException( "Can not issue EXPUNGE command if a mailbox is not selected." );
}
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} EXPUNGE" );
$response = trim( $this->getResponse( $tag ) );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "EXPUNGE failed: {$response}." );
}
}
/**
* Appends $mail to the $mailbox mailbox.
*
* Use this method to create email messages in a mailbox such as Sent or
* Draft.
*
* $flags is an array of flags to be set to the $mail (if provided):
*
* $flag can be one of:
* - ANSWERED - message has been answered
* - DELETED - message is marked to be deleted by later EXPUNGE
* - DRAFT - message is marked as a draft
* - FLAGGED - message is "flagged" for urgent/special attention
* - SEEN - message has been read
*
* This function automatically adds the '\' in front of each flag when
* calling the server command.
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully.
*
* @throws ezcMailTransportException
* if user is not authenticated
* or if the server sent a negative response
* or if $mailbox does not exists
* @param string $mailbox
* @param string $mail
* @param array(string) $flags
*/
public function append( $mailbox, $mail, $flags = null )
{
if ( $this->state != self::STATE_AUTHENTICATED &&
$this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can't call append() if not authenticated." );
}
$tag = $this->getNextTag();
$mailSize = strlen( $mail );
if ( !is_null( $flags ) )
{
for ( $i = 0; $i < count( $flags ); $i++ )
{
$flags[$i] = '\\' . $this->normalizeFlag( $flags[$i] );
}
$flagList = implode( ' ', $flags );
$command = "{$tag} APPEND {$mailbox} ({$flagList}) {{$mailSize}}";
}
else
{
$command = "{$tag} APPEND {$mailbox} {{$mailSize}}";
}
$this->connection->sendData( $command );
$response = trim( $this->connection->getLine() );
if ( strpos( $response, 'TRYCREATE' ) !== false )
{
throw new ezcMailTransportException( "Mailbox does not exist: {$response}." );
}
if ( $this->responseType( $response ) == self::RESPONSE_FEEDBACK )
{
$this->connection->sendData( $mail );
$response = trim( $this->getResponse( $tag ) );
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not append message to mailbox '{$mailbox}': {$response}." );
}
}
elseif ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not append message to mailbox '{$mailbox}': {$response}." );
}
}
/**
* Clears $flag of unwanted characters and makes it uppercase.
*
* @param string $flag
* @return string
*/
protected function normalizeFlag( $flag )
{
$flag = strtoupper( $flag );
$flag = str_replace( '\\', '', $flag );
return trim( $flag );
}
/**
* Sorts message numbers array $messages by the specified $sortCriteria.
*
* This method supports unique IDs instead of message numbers. See
* {@link ezcMailImapTransportOptions} for how to enable unique IDs
* referencing.
*
* $messages is an array of message numbers, for example:
* <code>
* array( 1, 2, 4 );
* </code>
*
* $sortCriteria is an email header like: Subject, To, From, Date, Sender.
*
* The sorting is done with the php function natcasesort().
*
* Before calling this method, a connection to the IMAP server must be
* established and a user must be authenticated successfully, and a mailbox
* must be selected.
*
* @throws ezcMailTransportException
* if a mailbox is not selected
* or if the server sent a negative response
* or if the array $messages is empty
* @param array(int) $messages
* @param string $sortCriteria
* @param bool $reverse
* @return array(string)
*/
protected function sort( $messages, $sortCriteria, $reverse = false )
{
$uid = ( $this->options->uidReferencing ) ? self::UID : self::NO_UID;
if ( $this->state != self::STATE_SELECTED &&
$this->state != self::STATE_SELECTED_READONLY )
{
throw new ezcMailTransportException( "Can't call sort() on the IMAP transport when a mailbox is not selected." );
}
$result = array();
$query = ucfirst( strtolower( $sortCriteria ) );
$messageNumbers = implode( ',', $messages );
$tag = $this->getNextTag();
$this->connection->sendData( "{$tag} {$uid}FETCH {$messageNumbers} (BODY.PEEK[HEADER.FIELDS ({$query})])" );
$response = trim( $this->connection->getLine() );
while ( strpos( $response, $tag ) === false )
{
if ( strpos( $response, ' FETCH (' ) !== false )
{
if ( $this->options->uidReferencing )
{
preg_match('/^\* [0-9]+ FETCH \(UID ([0-9]+)/', $response, $matches );
}
else
{
preg_match('/^\* ([0-9]+) FETCH/', $response, $matches );
}
$messageNumber = $matches[1];
}
if ( strpos( $response, $query ) !== false )
{
$strippedResponse = trim( trim( str_replace( "{$query}: ", '', $response ) ), '"' );
switch ( $query )
{
case 'Date':
$strippedResponse = strtotime( $strippedResponse );
break;
case 'Subject':
case 'From':
case 'Sender':
case 'To':
$strippedResponse = ezcMailTools::mimeDecode( $strippedResponse );
break;
default:
break;
}
$result[$messageNumber] = $strippedResponse;
}
// in case the mail doesn't have the $sortCriteria header (like junk mail missing Subject header)
if ( strpos( $response, ')' ) !== false && !isset( $result[$messageNumber] ) )
{
$result[$messageNumber] = '';
}
$response = trim( $this->connection->getLine() );
}
if ( $this->responseType( $response ) != self::RESPONSE_OK )
{
throw new ezcMailTransportException( "The IMAP server could not sort the messages: {$response}." );
}
if ( $reverse === true )
{
natcasesort( $result );
$result = array_reverse( $result, true );
}
else
{
natcasesort( $result );
}
return $result;
}
/**
* Parses $line to return the response code.
*
* Returns one of the following:
* - {@link RESPONSE_OK}
* - {@link RESPONSE_NO}
* - {@link RESPONSE_BAD}
* - {@link RESPONSE_UNTAGGED}
* - {@link RESPONSE_FEEDBACK}
*
* @throws ezcMailTransportException
* if the IMAP response ($line) is not recognized
* @param string $line
* @return int
*/
protected function responseType( $line )
{
if ( strpos( $line, 'OK ' ) !== false && strpos( $line, 'OK ' ) == 6 )
{
return self::RESPONSE_OK;
}
if ( strpos( $line, 'NO ' ) !== false && strpos( $line, 'NO ' ) == 6 )
{
return self::RESPONSE_NO;
}
if ( strpos( $line, 'BAD ' ) !== false && strpos( $line, 'BAD ' ) == 6 )
{
return self::RESPONSE_BAD;
}
if ( strpos( $line, '* ' ) !== false && strpos( $line, '* ' ) == 0 )
{
return self::RESPONSE_UNTAGGED;
}
if ( strpos( $line, '+ ' ) !== false && strpos( $line, '+ ' ) == 0 )
{
return self::RESPONSE_FEEDBACK;
}
throw new ezcMailTransportException( "Unrecognized IMAP response in line: {$line}" );
}
/**
* Reads the responses from the server until encountering $tag.
*
* In IMAP, each command sent by the client is prepended with a
* alphanumeric tag like 'A1234'. The server sends the response
* to the client command as lines, and the last line in the response
* is prepended with the same tag, and it contains the status of
* the command completion ('OK', 'NO' or 'BAD').
*
* Sometimes the server sends alerts and response lines from other
* commands before sending the tagged line, so this method just
* reads all the responses until it encounters $tag.
*
* It returns the tagged line to be processed by the calling method.
*
* If $response is specified, then it will not read the response
* from the server before searching for $tag in $response.
*
* Before calling this method, a connection to the IMAP server must be
* established.
*
* @param string $tag
* @param string $response
* @return string
*/
protected function getResponse( $tag, $response = null )
{
if ( is_null( $response ) )
{
$response = $this->connection->getLine();
}
while ( strpos( $response, $tag ) === false )
{
if ( strpos( $response, ' BAD ' ) !== false ||
strpos( $response, ' NO ' ) !== false )
{
break;
}
$response = $this->connection->getLine();
}
return $response;
}
/**
* Generates the next IMAP tag to prepend to client commands.
*
* The structure of the IMAP tag is Axxxx, where:
* - A is a letter (uppercase for conformity)
* - x is a digit from 0 to 9
*
* example of generated tag: T5439
*
* It uses the class variable $this->currentTag.
*
* Everytime it is called, the tag increases by 1.
*
* If it reaches the last tag, it wraps around to the first tag.
*
* By default, the first generated tag is A0001.
*
* @return string
*/
protected function getNextTag()
{
$tagLetter = substr( $this->currentTag, 0, 1 );
$tagNumber = intval( substr( $this->currentTag, 1 ) );
$tagNumber++;
if ( $tagLetter == 'Z' && $tagNumber == 10000 )
{
$tagLetter = 'A';
$tagNumber = 1;
}
if ( $tagNumber == 10000 )
{
$tagLetter++;
$tagNumber = 0;
}
$this->currentTag = $tagLetter . sprintf( "%04s", $tagNumber );
return $this->currentTag;
}
/**
* Returns the size of a FETCH section in bytes.
*
* The section header looks like: * id FETCH (BODY[TEXT] {size}
* where size is the size in bytes and id is the message number or ID.
*
* Example: for " * 2 FETCH (BODY[TEXT] {377}" this function returns 377.
*
* @return int
*/
protected function getMessageSectionSize( $response )
{
$size = 0;
preg_match( '/\{(.*)\}/', $response, $matches );
if ( count( $matches ) > 0 )
{
$size = (int) $matches[1];
}
return $size;
}
}
?>