1<?php
2// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
3//
4// All Rights Reserved. See copyright.txt for details and a complete list of authors.
5// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
6// $Id$
7
8// +-----------------------------------------------------------------------+
9// | Copyright (c) 2002, Richard Heyes                                     |
10// | All rights reserved.                                                  |
11// |                                                                       |
12// | Redistribution and use in source and binary forms, with or without    |
13// | modification, are permitted provided that the following conditions    |
14// | are met:                                                              |
15// |                                                                       |
16// | o Redistributions of source code must retain the above copyright      |
17// |   notice, this list of conditions and the following disclaimer.       |
18// | o Redistributions in binary form must reproduce the above copyright   |
19// |   notice, this list of conditions and the following disclaimer in the |
20// |   documentation and/or other materials provided with the distribution.|
21// | o The names of the authors may not be used to endorse or promote      |
22// |   products derived from this software without specific prior written  |
23// |   permission.                                                         |
24// |                                                                       |
25// | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS   |
26// | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT     |
27// | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
28// | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT  |
29// | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
30// | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT      |
31// | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
32// | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
33// | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT   |
34// | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
35// | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.  |
36// |                                                                       |
37// +-----------------------------------------------------------------------+
38// | Author: Richard Heyes <richard@phpguru.org>                           |
39// | Co-Author: Damian Fernandez Sosa <damlists@cnba.uba.ar>               |
40// +-----------------------------------------------------------------------+
41
42/**
43 *  +----------------------------- IMPORTANT ------------------------------+
44 *  | Usage of this class compared to native php extensions such as IMAP   |
45 *  | is slow and may be feature deficient. If available you are STRONGLY  |
46 *  | recommended to use the php extensions.                               |
47 *  +----------------------------------------------------------------------+
48 *
49 * POP3 Access Class
50 *
51 * For usage see the example script
52 */
53
54define('NET_POP3_STATE_DISCONNECTED', 1, true);
55define('NET_POP3_STATE_AUTHORISATION', 2, true);
56define('NET_POP3_STATE_TRANSACTION', 4, true);
57
58class Net_POP3
59{
60
61	/*
62	 * Some basic information about the mail drop
63	 * garnered from the STAT command
64	 *
65	 * @var array
66	 */
67	var $_maildrop;
68
69	/*
70	 * Used for APOP to store the timestamp
71	 *
72	 * @var string
73	 */
74	var $_timestamp;
75
76	/*
77	 * Timeout that is passed to the socket object
78	 *
79	 * @var integer
80	 */
81	var $_timeout;
82
83	/*
84	 * Socket object
85	 *
86	 * @var object
87	 */
88	var $_socket;
89
90	/*
91	 * Current state of the connection. Used with the
92	 * constants defined above.
93	 *
94	 * @var integer
95	 */
96	var $_state;
97
98	/*
99	 * Hostname to connect to
100	 *
101	 * @var string
102	 */
103	var $_host;
104
105	/*
106	 * Port to connect to
107	 *
108	 * @var integer
109	 */
110	var $_port;
111
112	/**
113	 * To allow class debuging
114	 * @var boolean
115	 */
116	var $_debug = false;
117
118
119	/**
120	 * The auth methods this class support
121	 * @var array
122	 */
123	//var $supportedAuthMethods=array('DIGEST-MD5', 'CRAM-MD5', 'APOP' , 'PLAIN' , 'LOGIN', 'USER');
124	//Disabling DIGEST-MD5 for now
125	var $supportedAuthMethods = array('CRAM-MD5', 'APOP', 'PLAIN', 'LOGIN', 'USER');
126	//var $supportedAuthMethods=array( 'CRAM-MD5', 'PLAIN' , 'LOGIN');
127	//var $supportedAuthMethods=array( 'PLAIN' , 'LOGIN');
128
129
130	/**
131	 * The auth methods this class support
132	 * @var array
133	 */
134	var $supportedSASLAuthMethods = array('DIGEST-MD5', 'CRAM-MD5');
135
136
137	/**
138	 * The capability response
139	 * @var array
140	 */
141	var $_capability;
142
143	/**
144	 * Constructor. Sets up the object variables, and instantiates
145	 * the socket object.
146	 *
147	 */
148	function __construct()
149	{
150		$this->_timestamp =  ''; // Used for APOP
151		$this->_maildrop  =  array();
152		$this->_timeout   =  3;
153		$this->_state     =  NET_POP3_STATE_DISCONNECTED;
154		$this->_socket    =  new Net_Socket();
155		/*
156		 * Include the Auth_SASL package.  If the package is not available,
157		 * we disable the authentication methods that depend upon it.
158		 */
159		if ((@include_once 'Auth/SASL.php') == false) {
160			if ($this->_debug) {
161				echo "AUTH_SASL NOT PRESENT!\n";
162			}
163			foreach ($this->supportedSASLAuthMethods as $SASLMethod) {
164				$pos = array_search($SASLMethod, $this->supportedAuthMethods);
165				if ($this->_debug) {
166					echo "DISABLING METHOD $SASLMethod\n";
167				}
168				unset($this->supportedAuthMethods[$pos]);
169			}
170		}
171	}
172
173
174	/**
175	 * Handles the errors the class can find
176	 * on the server
177	 *
178	 * @access private
179	 * @return PEAR_Error
180	 */
181
182	function _raiseError($msg, $code =-1)
183	{
184		return $this->_socket->raiseError($msg, $code);
185	}
186
187
188	/**
189	 * Connects to the given host on the given port.
190	 * Also looks for the timestamp in the greeting
191	 * needed for APOP authentication
192	 *
193	 * @param  string $host Hostname/IP address to connect to
194	 * @param  string $port Port to use to connect to on host
195	 * @return bool  Success/Failure
196	 */
197	function connect($host = 'localhost', $port = 110)
198	{
199		$this->_host = $host;
200		$this->_port = $port;
201
202		$result = $this->_socket->connect($host, $port, false, $this->_timeout);
203		if ($result === true) {
204			$data = $this->_recvLn();
205
206			if ($this->_checkResponse($data)) {
207				// if the response begins with '+OK' ...
208				//            if (@substr(strtoupper($data), 0, 3) == '+OK') {
209				// Check for string matching apop timestamp
210				if (preg_match('/<.+@.+>/U', $data, $matches)) {
211					$this->_timestamp = $matches[0];
212				}
213				$this->_maildrop = array();
214				$this->_state    = NET_POP3_STATE_AUTHORISATION;
215
216				return true;
217			}
218		}
219
220		$this->_socket->disconnect();
221		return false;
222	}
223
224
225	/**
226	 * Disconnect function. Sends the QUIT command
227	 * and closes the socket.
228	 *
229	 * @return bool Success/Failure
230	 */
231	function disconnect()
232	{
233		return $this->_cmdQuit();
234	}
235
236
237	/**
238	 * Performs the login procedure. If there is a timestamp
239	 * stored, APOP will be tried first, then basic USER/PASS.
240	 *
241	 * @param  string $user Username to use
242	 * @param  string $pass Password to use
243	 * @param  mixed $apop Whether to try APOP first, if used as string you can select the auth methd to use ( $pop3->login('validlogin', 'validpass', "CRAM-MD5");
244	 *          Valid methods are: 'DIGEST-MD5','CRAM-MD5','LOGIN','PLAIN','APOP','USER'
245	 * @return mixed  true on Success/ PEAR_ERROR on error
246	 */
247	function login($user, $pass, $apop = true)
248	{
249		if ($this->_state == NET_POP3_STATE_AUTHORISATION) {
250
251			if ($this->_socket->isError($ret = $this->_cmdAuthenticate($user, $pass, $apop))) {
252				return $ret;
253			}
254			if (!$this->_socket->isError($ret)) {
255				$this->_state = NET_POP3_STATE_TRANSACTION;
256				return true;
257			}
258
259		}
260		return $this->_raiseError('Generic login error', 1);
261	}
262
263
264	/**
265	 * Parses the response from the capability command. Stores
266	 * the result in $this->_capability
267	 *
268	 * @access private
269	 */
270	function _parseCapability()
271	{
272
273		if (!$this->_socket->isError($data = $this->_sendCmd('CAPA'))) {
274			$data = $this->_getMultiline();
275		} else {
276			// CAPA command not supported, reset data var
277			//  to avoid Notice errors of preg_split on an object
278			$data = '';
279		}
280		$data = preg_split('/\r?\n/', $data, -1, PREG_SPLIT_NO_EMPTY);
281
282		for ($i = 0, $count_data = count($data); $i < $count_data; $i++) {
283
284			$capa = '';
285			if (preg_match('/^([a-z,\-]+)( ((.*))|$)$/i', $data[$i], $matches)) {
286
287				$capa = strtolower($matches[1]);
288				switch ($capa) {
289					case 'implementation':
290						$this->_capability['implementation'] = $matches[1];
291						break;
292
293					case 'sasl':
294						if (isset($matches[3])) {
295							$this->_capability['sasl'] = preg_split('/\s+/', $matches[3]);
296						} else {
297							$this->_capability['sasl'] = $matches[1];
298						}
299						break;
300
301					default :
302						$this->_capability[$capa] = $matches[1];
303						break;
304				}
305			}
306		}
307	}
308
309
310	/**
311	 * Returns the name of the best authentication method that the server
312	 * has advertised.
313	 *
314	 * @param string if !=null,authenticate with this method ($userMethod).
315	 *
316	 * @return mixed    Returns a string containing the name of the best
317	 *                  supported authentication method or a PEAR_Error object
318	 *                  if a failure condition is encountered.
319	 * @access private
320	 * @since  1.0
321	 */
322	function _getBestAuthMethod($userMethod = null)
323	{
324
325		/*
326			 return 'USER';
327			 return 'APOP';
328			 return 'DIGEST-MD5';
329			 return 'CRAM-MD5';
330		 */
331
332		$this->_parseCapability();
333
334		//unset($this->_capability['sasl']);
335
336		$serverMethods = array();
337
338		if (isset($this->_capability['sasl'])) {
339			$serverMethods[] = $this->_capability['sasl'];
340		}
341
342		$serverMethods[]='USER';
343
344		// Check for timestamp before attempting APOP
345		if ($this->_timestamp != null) {
346			$serverMethods[] = 'APOP';
347		}
348
349		if ($userMethod !== null && $userMethod !== true) {
350			$methods = array();
351			$methods[] = $userMethod;
352			return $userMethod;
353		} else {
354			$methods = $this->supportedAuthMethods;
355		}
356
357		if (($methods != null) && ($serverMethods != null)) {
358
359			foreach ($methods as $method) {
360
361				if (in_array($method, $serverMethods)) {
362					return $method;
363				}
364			}
365			$serverMethods = implode(',', $serverMethods);
366			$myMethods = implode(',', $this->supportedAuthMethods);
367
368			return $this->_raiseError(
369				"$method NOT supported authentication method!. This server " .
370				"supports these methods: $serverMethods, but I support $myMethods"
371			);
372		} else {
373			return $this->_raiseError("This server don't support any Auth methods");
374		}
375	}
376
377
378	/**
379	 * Handles the authentication using any known method
380	 *
381	 * @param string The userid to authenticate as.
382	 * @param string The password to authenticate with.
383	 * @param string The method to use ( if $usermethod == '' then the class chooses the best method (the stronger is the best ) )
384	 *
385	 * @return mixed  string or PEAR_Error
386	 *
387	 * @access private
388	 * @since  1.0
389	 */
390	function _cmdAuthenticate($uid, $pwd, $userMethod = null)
391	{
392
393
394		if ($this->_socket->isError($method = $this->_getBestAuthMethod($userMethod))) {
395			return $method;
396		}
397
398		switch ($method) {
399			case 'DIGEST-MD5':
400				$result = $this->_authDigest_MD5($uid, $pwd);
401				break;
402
403			case 'CRAM-MD5':
404				$result = $this->_authCRAM_MD5($uid, $pwd);
405				break;
406
407			case 'LOGIN':
408				$result = $this->_authLOGIN($uid, $pwd);
409				break;
410
411			case 'PLAIN':
412				$result = $this->_authPLAIN($uid, $pwd);
413				break;
414
415			case 'APOP':
416				$result = $this->_cmdApop($uid, $pwd);
417				// if APOP fails fallback to USER auth
418				if ($this->_socket->isError($result)) {
419					//echo "APOP FAILED!!!\n";
420					$result=$this->_authUSER($uid, $pwd);
421				}
422				break;
423
424			case 'USER':
425				$result = $this->_authUSER($uid, $pwd);
426				break;
427
428			default :
429				$result = $this->_raiseError("$method is not a supported authentication method");
430				break;
431		}
432		return $result;
433	}
434
435
436	/**
437	 * Authenticates the user using the USER-PASS method.
438	 *
439	 * @param string The userid to authenticate as.
440	 * @param string The password to authenticate with.
441	 *
442	 * @return mixed    true on success or PEAR_Error on failure
443	 *
444	 * @access private
445	 * @since  1.0
446	 */
447	function _authUSER($user, $pass)
448	{
449		if ($this->_socket->isError($ret = $this->_cmdUser($user))) {
450			return $ret;
451		}
452		if ($this->_socket->isError($ret = $this->_cmdPass($pass))) {
453			return $ret;
454		}
455		return true;
456	}
457
458
459	/**
460	 * Authenticates the user using the PLAIN method.
461	 *
462	 * @param string The userid to authenticate as.
463	 * @param string The password to authenticate with.
464	 *
465	 * @return array Returns an array containing the response
466	 *
467	 * @access private
468	 * @since  1.0
469	 */
470	function _authPLAIN($user, $pass)
471	{
472		$cmd = sprintf('AUTH PLAIN %s', base64_encode(chr(0) . $user . chr(0) . $pass));
473
474		if ($this->_socket->isError($ret = $this->_send($cmd))) {
475			return $ret;
476		}
477		if ($this->_socket->isError($challenge = $this->_recvLn())) {
478			return $challenge;
479		}
480		if ($this->_socket->isError($ret = $this->_checkResponse($challenge))) {
481			return $ret;
482		}
483
484		return true;
485	}
486
487
488	/**
489	 * Authenticates the user using the PLAIN method.
490	 *
491	 * @param string The userid to authenticate as.
492	 * @param string The password to authenticate with.
493	 *
494	 * @return array Returns an array containing the response
495	 *
496	 * @access private
497	 * @since  1.0
498	 */
499	function _authLOGIN($user, $pass)
500	{
501		$this->_send('AUTH LOGIN');
502
503		if ($this->_socket->isError($challenge = $this->_recvLn())) {
504			return $challenge;
505		}
506		if ($this->_socket->isError($ret = $this->_checkResponse($challenge))) {
507			return $ret;
508		}
509
510
511		if ($this->_socket->isError($ret = $this->_send(sprintf('%s', base64_encode($user))))) {
512			return $ret;
513		}
514
515		if ($this->_socket->isError($challenge = $this->_recvLn())) {
516			return $challenge;
517		}
518
519		if ($this->_socket->isError($ret = $this->_checkResponse($challenge))) {
520			return $ret;
521		}
522
523		if ($this->_socket->isError($ret = $this->_send(sprintf('%s', base64_encode($pass))))) {
524			return $ret;
525		}
526
527		if ($this->_socket->isError($challenge = $this->_recvLn())) {
528			return $challenge;
529		}
530		return $this->_checkResponse($challenge);
531	}
532
533
534	/**
535	 * Authenticates the user using the CRAM-MD5 method.
536	 *
537	 * @param string The userid to authenticate as.
538	 * @param string The password to authenticate with.
539	 *
540	 * @return array Returns an array containing the response
541	 *
542	 * @access private
543	 * @since  1.0
544	 */
545	function _authCRAM_MD5($uid, $pwd)
546	{
547		if ($this->_socket->isError($ret = $this->_send('AUTH CRAM-MD5'))) {
548			return $ret;
549		}
550
551		if ($this->_socket->isError($challenge = $this->_recvLn())) {
552			return $challenge;
553		}
554
555		if ($this->_socket->isError($ret = $this->_checkResponse($challenge))) {
556			return $ret;
557		}
558
559		// remove '+ '
560
561		$challenge = substr($challenge, 2);
562
563		$challenge = base64_decode($challenge);
564
565		$cram = &Auth_SASL::factory('crammd5');
566		$auth_str = base64_encode($cram->getResponse($uid, $pwd, $challenge));
567
568
569		if ($this->_socket->isError($error = $this->_send($auth_str))) {
570			return $error;
571		}
572		if ($this->_socket->isError($ret = $this->_recvLn())) {
573			return $ret;
574		}
575		//echo "RET:$ret\n";
576		return $this->_checkResponse($ret);
577	}
578
579
580	/**
581	 * Authenticates the user using the DIGEST-MD5 method.
582	 *
583	 * @param string The userid to authenticate as.
584	 * @param string The password to authenticate with.
585	 * @param string The efective user
586	 *
587	 * @return array Returns an array containing the response
588	 *
589	 * @access private
590	 * @since  1.0
591	 */
592	function _authDigest_MD5($uid, $pwd)
593	{
594		if ($this->_socket->isError($ret = $this->_send('AUTH DIGEST-MD5'))) {
595			return $ret;
596		}
597
598		if ($this->_socket->isError($challenge = $this->_recvLn())) {
599			return $challenge;
600		}
601		if ($this->_socket->isError($ret = $this->_checkResponse($challenge))) {
602			return $ret;
603		}
604
605		// remove '+ '
606		$challenge = substr($challenge, 2);
607
608		$challenge = base64_decode($challenge);
609		$digest = &Auth_SASL::factory('digestmd5');
610		$auth_str = base64_encode($digest->getResponse($uid, $pwd, $challenge, "localhost", "pop3"));
611
612		if ($this->_socket->isError($error = $this->_send($auth_str))) {
613			return $error;
614		}
615
616		if ($this->_socket->isError($challenge = $this->_recvLn())) {
617			return $challenge;
618		}
619
620		if ($this->_socket->isError($ret = $this->_checkResponse($challenge))) {
621			return $ret;
622		}
623		/*
624		 * We don't use the protocol's third step because POP3 doesn't allow
625		 * subsequent authentication, so we just silently ignore it.
626		 */
627
628		if ($this->_socket->isError($challenge = $this->_send("\r\n"))) {
629			return $challenge;
630		}
631
632		if ($this->_socket->isError($challenge = $this->_recvLn())) {
633			return $challenge;
634		}
635
636		return $this->_checkResponse($challenge);
637
638	}
639
640
641	/**
642	 * Sends the APOP command
643	 *
644	 * @param  $user Username to send
645	 * @param  $pass Password to send
646	 * @return bool Success/Failure
647	 */
648	function _cmdApop($user, $pass)
649	{
650		if ($this->_state == NET_POP3_STATE_AUTHORISATION) {
651
652			if (!empty($this->_timestamp)) {
653				if ($this->_socket->isError($data = $this->_sendCmd('APOP ' . $user . ' ' . md5($this->_timestamp . $pass)))) {
654					return $data;
655				}
656				$this->_state = NET_POP3_STATE_TRANSACTION;
657				return true;
658			}
659		}
660		return $this->_raiseError('Not In NET_POP3_STATE_AUTHORISATION State1');
661	}
662
663
664	/**
665	 * Returns the raw headers of the specified message.
666	 *
667	 * @param  integer $msg_id Message number
668	 * @return mixed   Either raw headers or false on error
669	 */
670	function getRawHeaders($msg_id)
671	{
672		if ($this->_state == NET_POP3_STATE_TRANSACTION) {
673			return $this->_cmdTop($msg_id, 0);
674		}
675
676		return false;
677	}
678
679
680	/**
681	 * Returns the  headers of the specified message in an
682	 * associative array. Array keys are the header names, array
683	 * values are the header values. In the case of multiple headers
684	 * having the same names, eg Received:, the array value will be
685	 * an indexed array of all the header values.
686	 *
687	 * @param  integer $msg_id Message number
688	 * @return mixed   Either array of headers or false on error
689	 */
690	function getParsedHeaders($msg_id)
691	{
692		if ($this->_state == NET_POP3_STATE_TRANSACTION) {
693
694			$raw_headers = rtrim($this->getRawHeaders($msg_id));
695
696			$raw_headers = preg_replace("/\r\n[ \t]+/", ' ', $raw_headers); // Unfold headers
697			$raw_headers = explode("\r\n", $raw_headers);
698			foreach ($raw_headers as $value) {
699				$name  = substr($value, 0, $pos = strpos($value, ':'));
700				$value = ltrim(substr($value, $pos + 1));
701
702				// 21/09/08 MatWho Prevent capitalisation problems with Message-ID mail header
703				if (preg_match('/message-id/i', $name)) {
704					$name  = "Message-ID";
705				}
706
707				// Fix Subject encoding
708				if ($name == 'Subject') {
709					$subject = str_replace("_"," ", mb_decode_mimeheader($value));
710					$value = $subject;
711				}
712
713				if (isset($headers[$name]) AND is_array($headers[$name])) {
714					$headers[$name][] = $value;
715				} elseif (isset($headers[$name])) {
716					$headers[$name] = array($headers[$name], $value);
717				} else {
718					$headers[$name] = $value;
719				}
720			}
721			return $headers;
722		}
723
724		return false;
725	}
726
727
728	/**
729	 * Returns the body of the message with given message number.
730	 *
731	 * @param  integer $msg_id Message number
732	 * @return mixed   Either message body or false on error
733	 */
734	function getBody($msg_id)
735	{
736		if ($this->_state == NET_POP3_STATE_TRANSACTION) {
737			$msg = $this->_cmdRetr($msg_id);
738			return substr($msg, strpos($msg, "\r\n\r\n")+4);
739		}
740
741		return false;
742	}
743
744
745	/**
746	 * Returns the entire message with given message number.
747	 *
748	 * @param  integer $msg_id Message number
749	 * @return mixed   Either entire message or false on error
750	 */
751	function getMsg($msg_id)
752	{
753		if ($this->_state == NET_POP3_STATE_TRANSACTION) {
754			return $this->_cmdRetr($msg_id);
755		}
756
757		return false;
758	}
759
760
761	/**
762	 * Returns the size of the maildrop
763	 *
764	 * @return mixed Either size of maildrop or false on error
765	 */
766	function getSize()
767	{
768		if ($this->_state == NET_POP3_STATE_TRANSACTION) {
769			if (isset($this->_maildrop['size'])) {
770				return $this->_maildrop['size'];
771			} else {
772				list(, $size) = $this->_cmdStat();
773				return $size;
774			}
775		}
776
777		return false;
778	}
779
780
781	/**
782	 * Returns number of messages in this maildrop
783	 *
784	 * @return mixed Either number of messages or false on error
785	 */
786	function numMsg()
787	{
788		if ($this->_state == NET_POP3_STATE_TRANSACTION) {
789			if (isset($this->_maildrop['num_msg'])) {
790				return $this->_maildrop['num_msg'];
791			} else {
792				list($num_msg, ) = $this->_cmdStat();
793				return $num_msg;
794			}
795		}
796
797		return false;
798	}
799
800
801	/**
802	 * Marks a message for deletion. Only will be deleted if the
803	 * disconnect() method is called.
804	 *
805	 * @param  integer $msg_id Message to delete
806	 * @return bool Success/Failure
807	 */
808	function deleteMsg($msg_id)
809	{
810		if ($this->_state == NET_POP3_STATE_TRANSACTION) {
811			return $this->_cmdDele($msg_id);
812		}
813
814		return false;
815	}
816
817
818	/**
819	 * Combination of LIST/UIDL commands, returns an array
820	 * of data
821	 *
822	 * @param  integer $msg_id Optional message number
823	 * @return mixed Array of data or false on error
824	 */
825	function getListing($msg_id = null)
826	{
827
828		if ($this->_state == NET_POP3_STATE_TRANSACTION) {
829			if (!isset($msg_id)) {
830
831				$list=array();
832				if ($list = $this->_cmdList()) {
833					if ($uidl = $this->_cmdUidl()) {
834						foreach ($uidl as $i => $value) {
835							$list[$i]['uidl'] = $value['uidl'];
836						}
837					}
838					return $list;
839				} else {
840					return array();
841				}
842			} else {
843				if ($list = $this->_cmdList($msg_id) AND $uidl = $this->_cmdUidl($msg_id)) {
844					return array_merge($list, $uidl);
845				}
846			}
847		}
848
849		return false;
850	}
851
852
853	/**
854	 * Sends the USER command
855	 *
856	 * @param  string $user Username to send
857	 * @return bool  Success/Failure
858	 */
859	function _cmdUser($user)
860	{
861		if ($this->_state == NET_POP3_STATE_AUTHORISATION) {
862			return $this->_sendCmd('USER ' . $user);
863		}
864		return $this->_raiseError('Not In NET_POP3_STATE_AUTHORISATION State');
865	}
866
867
868	/**
869	 * Sends the PASS command
870	 *
871	 * @param  string $pass Password to send
872	 * @return bool  Success/Failure
873	 */
874	function _cmdPass($pass)
875	{
876		if ($this->_state == NET_POP3_STATE_AUTHORISATION) {
877			return $this->_sendCmd('PASS ' . $pass);
878		}
879		return $this->_raiseError('Not In NET_POP3_STATE_AUTHORISATION State');
880	}
881
882
883	/**
884	 * Sends the STAT command
885	 *
886	 * @return mixed Indexed array of number of messages and
887	 *               maildrop size, or false on error.
888	 */
889	function _cmdStat()
890	{
891		if ($this->_state == NET_POP3_STATE_TRANSACTION) {
892			if (!$this->_socket->isError($data = $this->_sendCmd('STAT'))) {
893				sscanf($data, '+OK %d %d', $msg_num, $size);
894				$this->_maildrop['num_msg'] = $msg_num;
895				$this->_maildrop['size']    = $size;
896
897				return array($msg_num, $size);
898			}
899		}
900		return false;
901	}
902
903
904	/**
905	 * Sends the LIST command
906	 *
907	 * @param  integer $msg_id Optional message number
908	 * @return mixed   Indexed array of msg_id/msg size or
909	 *                 false on error
910	 */
911	function _cmdList($msg_id = null)
912	{
913		$return = array();
914		if ($this->_state == NET_POP3_STATE_TRANSACTION) {
915			if (!isset($msg_id)) {
916				if (!$this->_socket->isError($data = $this->_sendCmd('LIST'))) {
917					$data = $this->_getMultiline();
918					$data = explode("\r\n", $data);
919					foreach ($data as $line) {
920						if ($line != '') {
921							sscanf($line, '%s %s', $msg_id, $size);
922							$return[] = array('msg_id' => $msg_id, 'size' => $size);
923						}
924					}
925					return $return;
926				}
927			} else {
928				if (!$this->_socket->isError($data = $this->_sendCmd('LIST ' . $msg_id))) {
929					if ($data!='') {
930						sscanf($data, '+OK %d %d', $msg_id, $size);
931						return array('msg_id' => $msg_id, 'size' => $size);
932					}
933					return array();
934				}
935			}
936		}
937
938
939		return false;
940	}
941
942
943	/**
944	 * Sends the RETR command
945	 *
946	 * @param  integer $msg_id The message number to retrieve
947	 * @return mixed   The message or false on error
948	 */
949	function _cmdRetr($msg_id)
950	{
951		if ($this->_state == NET_POP3_STATE_TRANSACTION) {
952			if (!$this->_socket->isError($data = $this->_sendCmd('RETR ' . $msg_id))) {
953				$data = $this->_getMultiline();
954				return $data;
955			}
956		}
957
958		return false;
959	}
960
961
962	/**
963	 * Sends the DELE command
964	 *
965	 * @param  integer $msg_id Message number to mark as deleted
966	 * @return bool Success/Failure
967	 */
968	function _cmdDele($msg_id)
969	{
970		if ($this->_state == NET_POP3_STATE_TRANSACTION) {
971			return $this->_sendCmd('DELE ' . $msg_id);
972		}
973
974		return false;
975	}
976
977
978	/**
979	 * Sends the NOOP command
980	 *
981	 * @return bool Success/Failure
982	 */
983	function _cmdNoop()
984	{
985		if ($this->_state == NET_POP3_STATE_TRANSACTION) {
986			if (!$this->_socket->isError($data = $this->_sendCmd('NOOP'))) {
987				return true;
988			}
989		}
990
991		return false;
992	}
993
994
995	/**
996	 * Sends the RSET command
997	 *
998	 * @return bool Success/Failure
999	 */
1000	function _cmdRset()
1001	{
1002		if ($this->_state == NET_POP3_STATE_TRANSACTION) {
1003			if (!$this->_socket->isError($data = $this->_sendCmd('RSET'))) {
1004				return true;
1005			}
1006		}
1007
1008		return false;
1009	}
1010
1011
1012	/**
1013	 * Sends the QUIT command
1014	 *
1015	 * @return bool Success/Failure
1016	 */
1017	function _cmdQuit()
1018	{
1019		$data = $this->_sendCmd('QUIT');
1020		$this->_state = NET_POP3_STATE_DISCONNECTED;
1021		$this->_socket->disconnect();
1022
1023		return (bool)$data;
1024	}
1025
1026
1027	/**
1028	 * Sends the TOP command
1029	 *
1030	 * @param  integer  $msg_id    Message number
1031	 * @param  integer  $num_lines Number of lines to retrieve
1032	 * @return mixed Message data or false on error
1033	 */
1034	function _cmdTop($msg_id, $num_lines)
1035	{
1036		if ($this->_state == NET_POP3_STATE_TRANSACTION) {
1037
1038			if (!$this->_socket->isError($data = $this->_sendCmd('TOP ' . $msg_id . ' ' . $num_lines))) {
1039				return $this->_getMultiline();
1040			}
1041		}
1042
1043		return false;
1044	}
1045
1046
1047	/**
1048	 * Sends the UIDL command
1049	 *
1050	 * @param  integer $msg_id Message number
1051	 * @return mixed indexed array of msg_id/uidl or false on error
1052	 */
1053	function _cmdUidl($msg_id = null)
1054	{
1055		if ($this->_state == NET_POP3_STATE_TRANSACTION) {
1056
1057			if (!isset($msg_id)) {
1058				if (!$this->_socket->isError($data = $this->_sendCmd('UIDL'))) {
1059					$data = $this->_getMultiline();
1060					$data = explode("\r\n", $data);
1061					foreach ($data as $line) {
1062						sscanf($line, '%d %s', $msg_id, $uidl);
1063						$return[] = array('msg_id' => $msg_id, 'uidl' => $uidl);
1064					}
1065
1066					return $return;
1067				}
1068			} else {
1069
1070				$data = $this->_sendCmd('UIDL ' . $msg_id);
1071				sscanf($data, '+OK %d %s', $msg_id, $uidl);
1072				return array('msg_id' => $msg_id, 'uidl' => $uidl);
1073			}
1074		}
1075
1076		return false;
1077	}
1078
1079
1080	/**
1081	 * Sends a command, checks the reponse, and
1082	 * if good returns the reponse, other wise
1083	 * returns false.
1084	 *
1085	 * @param  string $cmd  Command to send (\r\n will be appended)
1086	 * @return mixed First line of response if successful, otherwise false
1087	 */
1088	function _sendCmd($cmd)
1089	{
1090		if ($this->_socket->isError($result = $this->_send($cmd))) {
1091			return $result ;
1092		}
1093
1094		if ($this->_socket->isError($data = $this->_recvLn())) {
1095			return $data;
1096		}
1097
1098		if (strtoupper(substr($data, 0, 3)) == '+OK') {
1099			return $data;
1100		}
1101
1102		return $this->_raiseError($data);
1103	}
1104
1105
1106	/**
1107	 * Reads a multiline reponse and returns the data
1108	 *
1109	 * @return string The reponse.
1110	 */
1111	function _getMultiline()
1112	{
1113		$data = '';
1114		while (!is_a($tmp = $this->_recvLn(), 'PEAR_Error')) {
1115			if ($tmp == '.') {
1116				return substr($data, 0, -2);
1117			}
1118			if (substr($tmp, 0, 2) == '..') {
1119				$tmp = substr($tmp, 1);
1120			}
1121			$data .= $tmp . "\r\n";
1122		}
1123		return substr($data, 0, -2);
1124	}
1125
1126
1127	/**
1128	 * Sets the bebug state
1129	 *
1130	 * @param  bool $debug
1131	 * @access public
1132	 * @return void
1133	 */
1134	function setDebug($debug=true)
1135	{
1136		$this->_debug = $debug;
1137	}
1138
1139
1140	/**
1141	 * Send the given string of data to the server.
1142	 *
1143	 * @param   string  $data       The string of data to send.
1144	 *
1145	 * @return  mixed   True on success or a PEAR_Error object on failure.
1146	 *
1147	 * @access  private
1148	 * @since   1.0
1149	 */
1150	function _send($data)
1151	{
1152		if ($this->_debug) {
1153			echo "C: $data\n";
1154		}
1155
1156		if ($this->_socket->isError($error = $this->_socket->writeLine($data))) {
1157			return $this->_raiseError('Failed to write to socket: ' . $error->getMessage());
1158		}
1159		return true;
1160	}
1161
1162
1163	/**
1164	 * Receive the given string of data from the server.
1165	 *
1166	 * @return  mixed   a line of response on success or a PEAR_Error object on failure.
1167	 *
1168	 * @access  private
1169	 * @since  1.0
1170	 */
1171	function _recvLn()
1172	{
1173		if (is_a($lastline = $this->_socket->readLine(8192), 'PEAR_Error')) {
1174			return $this->_raiseError('Failed to write to socket: ' . $this->lastline->getMessage());
1175		}
1176		if ($this->_debug) {
1177			// S: means this data was sent by  the POP3 Server
1178			echo "S:$lastline\n" ;
1179		}
1180		return $lastline;
1181	}
1182
1183
1184	/**
1185	 * Checks de server Response
1186	 *
1187	 * @param  string $response the response
1188	 * @return  mixed   true on success or a PEAR_Error object on failure.
1189	 *
1190	 * @access  private
1191	 * @since  1.3.3
1192	 */
1193
1194	function _checkResponse($response)
1195	{
1196		if (@substr(strtoupper($response), 0, 3) == '+OK') {
1197			return true;
1198		} else {
1199			if (@substr(strtoupper($response), 0, 4) == '-ERR') {
1200				return $this->_raiseError($response);
1201			} else {
1202				if (@substr(strtoupper($response), 0, 2) == '+ ') {
1203					return true;
1204				}
1205			}
1206
1207		}
1208		return $this->_raiseError("Unknown Response ($response)");
1209	}
1210
1211}
1212