--- /dev/null
+<?php
+/**
+ * POP client class
+ *
+ * Class depends on PHP pcre extension and fsockopen() function. Some features
+ * might require PHP 4.3.0 with OpenSSL or PHP 5.1.0+. Class checks those extra
+ * dependencies internally, if used function needs it.
+ * @copyright © 2006 The SquirrelMail Project Team
+ * @license http://opensource.org/licenses/gpl-license.php GNU Public License
+ * @version $Id$
+ * @package plugins
+ * @subpackage mail_fetch
+ * @link http://www.ietf.org/rfc/rfc1939.txt RFC1939
+ * @link http://www.ietf.org/rfc/rfc2449.txt POP3EXT
+ * @link http://www.ietf.org/rfc/rfc2595.txt STARTTLS
+ * @link http://www.ietf.org/rfc/rfc1734.txt AUTH command (unsupported)
+ */
+
+/**
+ * POP3 client class
+ *
+ * POP connection is opened when class is constructed. All command_* methods
+ * execute specific POP commands on server. Most of other methods should be
+ * used only internally. Only login() method is public. If command returns
+ * mixed content and you expect message text, ids or something else, make sure
+ * that it is not boolean false.
+ *
+ * Basic use:
+ * 1. create object with connection params, see mail_fetch method.
+ * 2. check error buffer
+ * 3. login($username,$password) - true = login successful, false = login error.
+ * 4. command_stat() - get number of messages
+ * 5. command_list() - get message ids, use command_uidl(), if you implement
+ * 'keep mess on server' functions.
+ * 6. command_retr($some_message_id) - get message contents
+ * 7. command_dele($some_message_id) - mark message for deletion
+ * 8. command_quit() - close connection. You must close connection in order
+ * to delete messages and remove mailbox lock.
+ * @package plugins
+ * @subpackage mail_fetch
+ */
+class mail_fetch {
+ /**
+ * Server name
+ * @var string
+ */
+ var $host = '';
+
+ /**
+ * POP connection port.
+ * Defaults to 110 on plain text connections and to 995 on TLS
+ * @var integer
+ */
+ var $port = 0;
+
+ /**
+ * Connection type
+ * 0 - plain text (default)
+ * 1 - tls (php 4.3 and openssl extension requirement)
+ * 2 - stls (stream_socket_enable_crypto() requirement. PHP 5.1.0, POP3
+ * server with POP3EXT and STLS support) (untested)
+ * @var integer
+ */
+ var $tls = 0;
+
+ /**
+ * Authentication type
+ *
+ * Bitwise variable. If variable covers more than one authentication method,
+ * login() tries to use all of them until first successful auth.
+ * 1 - user/pass (rfc1939, default)
+ * 2 - apop (rfc1939, timestamp must be present in greeting)
+ * 3 - apop or user/pass
+ * @var integer
+ */
+ var $auth = 1;
+
+ /**
+ * Connection timeout
+ * @var integer
+ */
+ var $timeout = 60;
+
+ /**
+ * Connection resource
+ * @var stream
+ */
+ var $conn = false;
+
+ /**
+ * Server greeting
+ * @var string
+ */
+ var $greeting = '';
+
+ /**
+ * Timestamp (with <> or empty string)
+ * @var string
+ */
+ var $timestamp = '';
+
+ /**
+ * Capabilities (POP3EXT capa)
+ * @var array
+ */
+ var $capabilities = array();
+
+ /**
+ * Error message buffer
+ * @var string
+ */
+ var $error = '';
+
+ /**
+ * Variable is used to store last positive POP server response
+ * checked in check_response() method. Used internally to handle
+ * mixed single and multiline command responses.
+ * @var string
+ */
+ var $response = '';
+
+ /**
+ * Constructor function
+ *
+ * parameter array keys
+ * 'host' - required string, address of server. ip or fqn
+ * 'port' - optional integer, port of server.
+ * 'tls' - optional integer, connection type
+ * 'timeout' - optional integer, connection timeout
+ * 'auth' - optional integer, used authentication mechanism.
+ * See description of class properties
+ * @param array $aParams connection params
+ */
+ function mail_fetch($aParams=array()) {
+ // hostname
+ if (isset($aParams['host'])) {
+ $this->host = $aParams['host'];
+ } else {
+ return $this->set_error('Server name is not set');
+ }
+ // tls
+ if (isset($aParams['tls'])) {
+ $this->tls = (int) $aParams['tls'];
+ }
+ // port
+ if (isset($aParams['port'])) {
+ $this->port = (int) $aParams['port'];
+ }
+ // set default ports
+ if ($this->port == 0) {
+ if ($this->tls===1) {
+ // pops
+ $this->port = 995;
+ } else {
+ // pop3
+ $this->port = 110;
+ }
+ }
+ // timeout
+ if (isset($aParams['timeout'])) {
+ $this->timeout = (int) $aParams['timeout'];
+ }
+ // authentication mech
+ if (isset($aParams['auth'])) {
+ $this->auth = (int) $aParams['auth'];
+ }
+
+ // open connection
+ $this->open();
+ }
+
+ // Generic methods to handle connection and login operations.
+
+ /**
+ * Opens pop connection
+ *
+ * Command handles TLS and STLS connection differences and fills capabilities
+ * array with RFC2449 CAPA data.
+ * @return boolean
+ */
+ function open() {
+ if ($this->conn) {
+ return true;
+ }
+
+ if ($this->tls===1) {
+ if (! $this->check_php_version(4,3) || ! extension_loaded('openssl')) {
+ return $this->set_error('Used PHP version does not support functions required for POP TLS.');
+ }
+ $target = 'tls://' . $this->host;
+ } else {
+ $target = $this->host;
+ }
+
+ $this->conn = @fsockopen($target, $this->port, $errno, $errstr, $this->timeout);
+
+ if (!$this->conn) {
+ $error = sprintf('Error %d: ',$errno) . $errstr;
+ return $this->set_error($error);
+ }
+
+ // read greeting
+ $this->greeting = trim(fgets($this->conn));
+
+ // check greeting for errors and extract timestamp
+ if (preg_match('/^-ERR (.+)/',$this->greeting,$matches)) {
+ return $this->set_error($matches[1],true);
+ } elseif (preg_match('/^\+OK.+(<.+>)/',$this->greeting,$matches)) {
+ $this->timestamp = $matches[1];
+ }
+
+ /**
+ * fill capability only when connection uses some non-rfc1939
+ * authentication type (currently unsupported) or STARTTLS.
+ * Command is not part of rfc1939 and we don't have to use it
+ * in simple POP connection.
+ */
+ if ($this->auth > 3 || $this->tls===2) {
+ $this->command_capa();
+ }
+
+ // STARTTLS support
+ if ($this->tls===2) {
+ return $this->command_stls();
+ }
+
+ return true;
+ }
+
+ /**
+ * Reads first response line and checks it for errors
+ * @return boolean true = success, false = failure, check error buffer
+ */
+ function check_response() {
+ $line = fgets($this->conn);
+ if (preg_match('/^-ERR (.+)/',$line,$matches)) {
+ return $this->set_error($matches[1]);
+ } elseif (preg_match('/^\+OK/',$line)) {
+ $this->response = trim($line);
+ return true;
+ } else {
+ $this->response = trim($line);
+ return $this->set_error('Unknown response');
+ }
+ }
+
+ /**
+ * Standard SquirrelMail function copied to class in order to make class
+ * independent from SquirrelMail.
+ */
+ function check_php_version ($a = '0', $b = '0', $c = '0') {
+ return version_compare ( PHP_VERSION, "$a.$b.$c", 'ge' );
+ }
+
+ /**
+ * Generic login wrapper
+ *
+ * Connection is not closed on login error (unless POP server drops
+ * connection)
+ * @param string $username
+ * @param string $password
+ * @return boolean
+ */
+ function login($username,$password) {
+ $ret = false;
+
+ // RFC1939 APOP authentication
+ if (! $ret && $this->auth & 2) {
+ // clean error buffer
+ $this->error = '';
+ // APOP login
+ $ret = $this->command_apop($username,$password);
+ }
+
+ // RFC1939 USER authentication
+ if (! $ret && $this->auth & 1) {
+ // clean error buffer
+ $this->error = '';
+ // Default to USER/PASS login
+ if (! $this->command_user($username)) {
+ // error is already in error buffer
+ $ret = false;
+ } else {
+ $ret = $this->command_pass($password);
+ }
+ }
+ return $ret;
+ }
+
+ /**
+ * Sets error in error buffer and returns boolean false
+ * @param string $error Error message
+ * @param boolean $close_conn Do we have to close connection
+ * @return boolean false
+ */
+ function set_error($error,$close_conn=false) {
+ $this->error = $error;
+ if ($close_conn) {
+ $this->command_quit();
+ }
+ return false;
+ }
+
+ // POP (rfc 1939) commands
+
+ /**
+ * Gets mailbox status
+ * array with 'count' and 'size' keys
+ * @return mixed array or boolean false
+ */
+ function command_stat() {
+ fwrite($this->conn,"STAT\r\n");
+ $response = fgets($this->conn);
+ if (preg_match('/^\+OK (\d+) (\d+)/',$response,$matches)) {
+ return array('count' => $matches[1],
+ 'size' => $matches[2]);
+ } else {
+ return $this->set_error('stat command failed');
+ }
+ }
+
+ /**
+ * List mailbox messages
+ * @param integer $msg
+ * @return mixed array with message ids (keys) and sizes (values) or boolean false
+ */
+ function command_list($msg='') {
+ fwrite($this->conn,"LIST $msg\r\n");
+
+ if($this->check_response()) {
+ $ret = array();
+ if (!empty($msg)) {
+ list($ok,$msg_id,$size) = explode(' ',trim($this->response));
+ $ret[$msg_id] = $size;
+ } else {
+ while($line = fgets($this->conn)) {
+ if (trim($line)=='.') {
+ break;
+ } else {
+ list($msg_id,$size) = explode(' ',trim($line));
+ $ret[$msg_id] = $size;
+ }
+ }
+ }
+ return $ret;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Gets message text
+ * @param integer $msg message id
+ * @return mixed rfc822 message (CRLF line endings) or boolean false
+ */
+ function command_retr($msg) {
+ fwrite($this->conn,"RETR $msg\r\n");
+
+ if($this->check_response()) {
+ $ret = '';
+ while($line = fgets($this->conn)) {
+ if (trim($line)=='.') {
+ break;
+ } else {
+ $ret.= $line;
+ }
+ }
+ return $ret;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * @param integer $msg
+ * @return boolean
+ */
+ function command_dele($msg) {
+ fwrite($this->conn,"DELE $msg\r\n");
+ return $this->check_response();
+ }
+
+ /**
+ * POP noop command
+ * @return boolean
+ */
+ function command_noop() {
+ fwrite($this->conn,"NOOP\r\n");
+ return $this->check_response();
+ }
+
+ /**
+ * Resets message state
+ * @return boolean
+ */
+ function command_rset() {
+ fwrite($this->conn,"RSET\r\n");
+ return $this->check_response();
+ }
+
+ /**
+ * Closes POP connection
+ */
+ function command_quit() {
+ fwrite($this->conn,"QUIT\r\n");
+ fclose($this->conn);
+ $this->conn = false;
+ }
+
+ // Optional RFC1939 commands
+
+ /**
+ * Gets message headers and $n of body lines.
+ *
+ * Command is optional and not required by rfc1939
+ * @param integer $msg
+ * @param integer $n
+ * @return string or boolean false
+ */
+ function command_top($msg,$n) {
+ fwrite($this->conn,"TOP $msg $n\r\n");
+
+ if($this->check_response()) {
+ $ret = '';
+ while($line = fgets($this->conn)) {
+ if (trim($line)=='.') {
+ break;
+ } else {
+ $ret.= $line;
+ }
+ }
+ return $ret;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Gets unique message ids
+ * Command is optional and not required by rfc1939
+ * @param integer $msg message id
+ * @return mixed array with message ids (keys) and unique ids (values)
+ * or boolean false
+ */
+ function command_uidl($msg='') {
+ fwrite($this->conn,"UIDL $msg\r\n");
+ if($this->check_response()) {
+ $ids = array();
+ if (!empty($msg)) {
+ list($ok,$msg_id,$unique_id) = explode(' ',trim($this->response));
+ $ids[$msg_id] = $unique_id;
+ } else {
+ while($line = fgets($this->conn)) {
+ if (trim($line)=='.') {
+ break;
+ } else {
+ list($msg_id,$unique_id) = explode(' ',trim($line));
+ $ids[$msg_id] = $unique_id;
+ }
+ }
+ }
+ return $ids;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * USER authentication (username command)
+ *
+ * Command is optional and not required by rfc1939. If command
+ * is successful, pass command must be executed after it.
+ * @param string $username
+ * @return boolean true = success, false = failure.
+ */
+ function command_user($username) {
+ fwrite($this->conn,"USER $username\r\n");
+ return $this->check_response();
+ }
+
+ /**
+ * USER authentication (password command)
+ *
+ * Command is optional and not required by rfc1939. Requires
+ * successful user command.
+ * @param string $password
+ * @return boolean true = success, false = failure.
+ */
+ function command_pass($password) {
+ fwrite($this->conn,"PASS $password\r\n");
+ return $this->check_response();
+ }
+
+ /**
+ * APOP authentication
+ *
+ * Command is optional and not required by rfc1939. APOP support
+ * requires plain text passwords stored on server and some servers
+ * don't support it. Standard qmail pop3d declares apop support
+ * without checking if checkpassword supports it.
+ * @param string $username
+ * @param string $password
+ * @return boolean true = success, false = failure.
+ */
+ function command_apop($username,$password) {
+ if (empty($this->timestamp)) {
+ return $this->set_error('APOP is not supported by selected server.');
+ }
+ $digest = md5($this->timestamp . $password);
+
+ fwrite($this->conn,"APOP $username $digest\r\n");
+ return $this->check_response();
+ }
+
+ // RFC2449 POP3EXT
+
+ /**
+ * Checks pop server capabilities
+ *
+ * RFC2449. Fills capabilities array.
+ * @return void
+ */
+ function command_capa() {
+ fwrite($this->conn,"CAPA\r\n");
+ if ($this->check_response()) {
+ // reset array. capabilities depend on authorization state
+ $this->capabilities = array();
+ while($line = fgets($this->conn)) {
+ if (trim($line)=='.') {
+ break;
+ } else {
+ $this->capabilities[] = trim($line);
+ }
+ }
+ } else {
+ // if capa fails, error buffer contains error message.
+ // Clean error buffer,
+ // if POP3EXT is not supported, capability array will be empty
+ $this->error = '';
+ }
+ }
+
+ // RFC2595 STARTTLS
+
+ /**
+ * RFC 2595 POP STARTTLS support
+ * @return boolean
+ */
+ function command_stls() {
+ if (! function_exists('stream_socket_enable_crypto')) {
+ return $this->set_error('Used PHP version does not support functions required for POP STLS.',true);
+ } elseif (in_array('STLS',$this->capabilities)) {
+ return $this->set_error('Selected POP3 server does not support STLS.',true);
+ }
+ fwrite($this->conn,"STLS\r\n");
+ if (! $this->check_response()) {
+ return false;
+ }
+
+ if (@stream_socket_enable_crypto($this->conn,true,STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
+ // starttls was successful (rfc2595 4. POP3 STARTTLS extension.)
+ // get new CAPA response
+ $this->command_capa();
+ } else {
+ /** stream_socket_enable_crypto() call failed. */
+ return $this->set_error('Unable to start TLS.',true);
+ }
+ return true;
+ }
+}