e52e9926 |
1 | <?php |
2 | /** |
3 | * POP client class |
4 | * |
5 | * Class depends on PHP pcre extension and fsockopen() function. Some features |
6 | * might require PHP 4.3.0 with OpenSSL or PHP 5.1.0+. Class checks those extra |
7 | * dependencies internally, if used function needs it. |
8 | * @copyright © 2006 The SquirrelMail Project Team |
9 | * @license http://opensource.org/licenses/gpl-license.php GNU Public License |
10 | * @version $Id$ |
11 | * @package plugins |
12 | * @subpackage mail_fetch |
13 | * @link http://www.ietf.org/rfc/rfc1939.txt RFC1939 |
14 | * @link http://www.ietf.org/rfc/rfc2449.txt POP3EXT |
15 | * @link http://www.ietf.org/rfc/rfc2595.txt STARTTLS |
16 | * @link http://www.ietf.org/rfc/rfc1734.txt AUTH command (unsupported) |
17 | */ |
18 | |
19 | /** |
20 | * POP3 client class |
21 | * |
22 | * POP connection is opened when class is constructed. All command_* methods |
23 | * execute specific POP commands on server. Most of other methods should be |
24 | * used only internally. Only login() method is public. If command returns |
25 | * mixed content and you expect message text, ids or something else, make sure |
26 | * that it is not boolean false. |
27 | * |
28 | * Basic use: |
29 | * 1. create object with connection params, see mail_fetch method. |
30 | * 2. check error buffer |
31 | * 3. login($username,$password) - true = login successful, false = login error. |
32 | * 4. command_stat() - get number of messages |
33 | * 5. command_list() - get message ids, use command_uidl(), if you implement |
34 | * 'keep mess on server' functions. |
35 | * 6. command_retr($some_message_id) - get message contents |
36 | * 7. command_dele($some_message_id) - mark message for deletion |
37 | * 8. command_quit() - close connection. You must close connection in order |
38 | * to delete messages and remove mailbox lock. |
39 | * @package plugins |
40 | * @subpackage mail_fetch |
41 | */ |
42 | class mail_fetch { |
43 | /** |
44 | * Server name |
45 | * @var string |
46 | */ |
47 | var $host = ''; |
48 | |
49 | /** |
50 | * POP connection port. |
51 | * Defaults to 110 on plain text connections and to 995 on TLS |
52 | * @var integer |
53 | */ |
54 | var $port = 0; |
55 | |
56 | /** |
57 | * Connection type |
58 | * 0 - plain text (default) |
59 | * 1 - tls (php 4.3 and openssl extension requirement) |
60 | * 2 - stls (stream_socket_enable_crypto() requirement. PHP 5.1.0, POP3 |
61 | * server with POP3EXT and STLS support) (untested) |
62 | * @var integer |
63 | */ |
64 | var $tls = 0; |
65 | |
66 | /** |
67 | * Authentication type |
68 | * |
69 | * Bitwise variable. If variable covers more than one authentication method, |
70 | * login() tries to use all of them until first successful auth. |
71 | * 1 - user/pass (rfc1939, default) |
72 | * 2 - apop (rfc1939, timestamp must be present in greeting) |
73 | * 3 - apop or user/pass |
74 | * @var integer |
75 | */ |
76 | var $auth = 1; |
77 | |
78 | /** |
79 | * Connection timeout |
80 | * @var integer |
81 | */ |
82 | var $timeout = 60; |
83 | |
84 | /** |
85 | * Connection resource |
86 | * @var stream |
87 | */ |
88 | var $conn = false; |
89 | |
90 | /** |
91 | * Server greeting |
92 | * @var string |
93 | */ |
94 | var $greeting = ''; |
95 | |
96 | /** |
97 | * Timestamp (with <> or empty string) |
98 | * @var string |
99 | */ |
100 | var $timestamp = ''; |
101 | |
102 | /** |
103 | * Capabilities (POP3EXT capa) |
104 | * @var array |
105 | */ |
106 | var $capabilities = array(); |
107 | |
108 | /** |
109 | * Error message buffer |
110 | * @var string |
111 | */ |
112 | var $error = ''; |
113 | |
114 | /** |
115 | * Variable is used to store last positive POP server response |
116 | * checked in check_response() method. Used internally to handle |
117 | * mixed single and multiline command responses. |
118 | * @var string |
119 | */ |
120 | var $response = ''; |
121 | |
122 | /** |
123 | * Constructor function |
124 | * |
125 | * parameter array keys |
126 | * 'host' - required string, address of server. ip or fqn |
127 | * 'port' - optional integer, port of server. |
128 | * 'tls' - optional integer, connection type |
129 | * 'timeout' - optional integer, connection timeout |
130 | * 'auth' - optional integer, used authentication mechanism. |
131 | * See description of class properties |
132 | * @param array $aParams connection params |
133 | */ |
134 | function mail_fetch($aParams=array()) { |
135 | // hostname |
136 | if (isset($aParams['host'])) { |
137 | $this->host = $aParams['host']; |
138 | } else { |
139 | return $this->set_error('Server name is not set'); |
140 | } |
141 | // tls |
142 | if (isset($aParams['tls'])) { |
143 | $this->tls = (int) $aParams['tls']; |
144 | } |
145 | // port |
146 | if (isset($aParams['port'])) { |
147 | $this->port = (int) $aParams['port']; |
148 | } |
149 | // set default ports |
150 | if ($this->port == 0) { |
151 | if ($this->tls===1) { |
152 | // pops |
153 | $this->port = 995; |
154 | } else { |
155 | // pop3 |
156 | $this->port = 110; |
157 | } |
158 | } |
159 | // timeout |
160 | if (isset($aParams['timeout'])) { |
161 | $this->timeout = (int) $aParams['timeout']; |
162 | } |
163 | // authentication mech |
164 | if (isset($aParams['auth'])) { |
165 | $this->auth = (int) $aParams['auth']; |
166 | } |
167 | |
168 | // open connection |
169 | $this->open(); |
170 | } |
171 | |
172 | // Generic methods to handle connection and login operations. |
173 | |
174 | /** |
175 | * Opens pop connection |
176 | * |
177 | * Command handles TLS and STLS connection differences and fills capabilities |
178 | * array with RFC2449 CAPA data. |
179 | * @return boolean |
180 | */ |
181 | function open() { |
182 | if ($this->conn) { |
183 | return true; |
184 | } |
185 | |
186 | if ($this->tls===1) { |
187 | if (! $this->check_php_version(4,3) || ! extension_loaded('openssl')) { |
188 | return $this->set_error('Used PHP version does not support functions required for POP TLS.'); |
189 | } |
190 | $target = 'tls://' . $this->host; |
191 | } else { |
192 | $target = $this->host; |
193 | } |
194 | |
195 | $this->conn = @fsockopen($target, $this->port, $errno, $errstr, $this->timeout); |
196 | |
197 | if (!$this->conn) { |
198 | $error = sprintf('Error %d: ',$errno) . $errstr; |
199 | return $this->set_error($error); |
200 | } |
201 | |
202 | // read greeting |
203 | $this->greeting = trim(fgets($this->conn)); |
204 | |
205 | // check greeting for errors and extract timestamp |
206 | if (preg_match('/^-ERR (.+)/',$this->greeting,$matches)) { |
207 | return $this->set_error($matches[1],true); |
208 | } elseif (preg_match('/^\+OK.+(<.+>)/',$this->greeting,$matches)) { |
209 | $this->timestamp = $matches[1]; |
210 | } |
211 | |
212 | /** |
213 | * fill capability only when connection uses some non-rfc1939 |
214 | * authentication type (currently unsupported) or STARTTLS. |
215 | * Command is not part of rfc1939 and we don't have to use it |
216 | * in simple POP connection. |
217 | */ |
218 | if ($this->auth > 3 || $this->tls===2) { |
219 | $this->command_capa(); |
220 | } |
221 | |
222 | // STARTTLS support |
223 | if ($this->tls===2) { |
224 | return $this->command_stls(); |
225 | } |
226 | |
227 | return true; |
228 | } |
229 | |
230 | /** |
231 | * Reads first response line and checks it for errors |
232 | * @return boolean true = success, false = failure, check error buffer |
233 | */ |
234 | function check_response() { |
235 | $line = fgets($this->conn); |
236 | if (preg_match('/^-ERR (.+)/',$line,$matches)) { |
237 | return $this->set_error($matches[1]); |
238 | } elseif (preg_match('/^\+OK/',$line)) { |
239 | $this->response = trim($line); |
240 | return true; |
241 | } else { |
242 | $this->response = trim($line); |
243 | return $this->set_error('Unknown response'); |
244 | } |
245 | } |
246 | |
247 | /** |
248 | * Standard SquirrelMail function copied to class in order to make class |
249 | * independent from SquirrelMail. |
250 | */ |
251 | function check_php_version ($a = '0', $b = '0', $c = '0') { |
252 | return version_compare ( PHP_VERSION, "$a.$b.$c", 'ge' ); |
253 | } |
254 | |
255 | /** |
256 | * Generic login wrapper |
257 | * |
258 | * Connection is not closed on login error (unless POP server drops |
259 | * connection) |
260 | * @param string $username |
261 | * @param string $password |
262 | * @return boolean |
263 | */ |
264 | function login($username,$password) { |
265 | $ret = false; |
266 | |
267 | // RFC1939 APOP authentication |
268 | if (! $ret && $this->auth & 2) { |
269 | // clean error buffer |
270 | $this->error = ''; |
271 | // APOP login |
272 | $ret = $this->command_apop($username,$password); |
273 | } |
274 | |
275 | // RFC1939 USER authentication |
276 | if (! $ret && $this->auth & 1) { |
277 | // clean error buffer |
278 | $this->error = ''; |
279 | // Default to USER/PASS login |
280 | if (! $this->command_user($username)) { |
281 | // error is already in error buffer |
282 | $ret = false; |
283 | } else { |
284 | $ret = $this->command_pass($password); |
285 | } |
286 | } |
287 | return $ret; |
288 | } |
289 | |
290 | /** |
291 | * Sets error in error buffer and returns boolean false |
292 | * @param string $error Error message |
293 | * @param boolean $close_conn Do we have to close connection |
294 | * @return boolean false |
295 | */ |
296 | function set_error($error,$close_conn=false) { |
297 | $this->error = $error; |
298 | if ($close_conn) { |
299 | $this->command_quit(); |
300 | } |
301 | return false; |
302 | } |
303 | |
304 | // POP (rfc 1939) commands |
305 | |
306 | /** |
307 | * Gets mailbox status |
308 | * array with 'count' and 'size' keys |
309 | * @return mixed array or boolean false |
310 | */ |
311 | function command_stat() { |
312 | fwrite($this->conn,"STAT\r\n"); |
313 | $response = fgets($this->conn); |
314 | if (preg_match('/^\+OK (\d+) (\d+)/',$response,$matches)) { |
315 | return array('count' => $matches[1], |
316 | 'size' => $matches[2]); |
317 | } else { |
318 | return $this->set_error('stat command failed'); |
319 | } |
320 | } |
321 | |
322 | /** |
323 | * List mailbox messages |
324 | * @param integer $msg |
325 | * @return mixed array with message ids (keys) and sizes (values) or boolean false |
326 | */ |
327 | function command_list($msg='') { |
328 | fwrite($this->conn,"LIST $msg\r\n"); |
329 | |
330 | if($this->check_response()) { |
331 | $ret = array(); |
332 | if (!empty($msg)) { |
333 | list($ok,$msg_id,$size) = explode(' ',trim($this->response)); |
334 | $ret[$msg_id] = $size; |
335 | } else { |
336 | while($line = fgets($this->conn)) { |
337 | if (trim($line)=='.') { |
338 | break; |
339 | } else { |
340 | list($msg_id,$size) = explode(' ',trim($line)); |
341 | $ret[$msg_id] = $size; |
342 | } |
343 | } |
344 | } |
345 | return $ret; |
346 | } else { |
347 | return false; |
348 | } |
349 | } |
350 | |
351 | /** |
352 | * Gets message text |
353 | * @param integer $msg message id |
354 | * @return mixed rfc822 message (CRLF line endings) or boolean false |
355 | */ |
356 | function command_retr($msg) { |
357 | fwrite($this->conn,"RETR $msg\r\n"); |
358 | |
359 | if($this->check_response()) { |
360 | $ret = ''; |
361 | while($line = fgets($this->conn)) { |
362 | if (trim($line)=='.') { |
363 | break; |
364 | } else { |
365 | $ret.= $line; |
366 | } |
367 | } |
368 | return $ret; |
369 | } else { |
370 | return false; |
371 | } |
372 | } |
373 | |
374 | /** |
375 | * @param integer $msg |
376 | * @return boolean |
377 | */ |
378 | function command_dele($msg) { |
379 | fwrite($this->conn,"DELE $msg\r\n"); |
380 | return $this->check_response(); |
381 | } |
382 | |
383 | /** |
384 | * POP noop command |
385 | * @return boolean |
386 | */ |
387 | function command_noop() { |
388 | fwrite($this->conn,"NOOP\r\n"); |
389 | return $this->check_response(); |
390 | } |
391 | |
392 | /** |
393 | * Resets message state |
394 | * @return boolean |
395 | */ |
396 | function command_rset() { |
397 | fwrite($this->conn,"RSET\r\n"); |
398 | return $this->check_response(); |
399 | } |
400 | |
401 | /** |
402 | * Closes POP connection |
403 | */ |
404 | function command_quit() { |
405 | fwrite($this->conn,"QUIT\r\n"); |
406 | fclose($this->conn); |
407 | $this->conn = false; |
408 | } |
409 | |
410 | // Optional RFC1939 commands |
411 | |
412 | /** |
413 | * Gets message headers and $n of body lines. |
414 | * |
415 | * Command is optional and not required by rfc1939 |
416 | * @param integer $msg |
417 | * @param integer $n |
418 | * @return string or boolean false |
419 | */ |
420 | function command_top($msg,$n) { |
421 | fwrite($this->conn,"TOP $msg $n\r\n"); |
422 | |
423 | if($this->check_response()) { |
424 | $ret = ''; |
425 | while($line = fgets($this->conn)) { |
426 | if (trim($line)=='.') { |
427 | break; |
428 | } else { |
429 | $ret.= $line; |
430 | } |
431 | } |
432 | return $ret; |
433 | } else { |
434 | return false; |
435 | } |
436 | } |
437 | |
438 | /** |
439 | * Gets unique message ids |
440 | * Command is optional and not required by rfc1939 |
441 | * @param integer $msg message id |
442 | * @return mixed array with message ids (keys) and unique ids (values) |
443 | * or boolean false |
444 | */ |
445 | function command_uidl($msg='') { |
446 | fwrite($this->conn,"UIDL $msg\r\n"); |
447 | if($this->check_response()) { |
448 | $ids = array(); |
449 | if (!empty($msg)) { |
450 | list($ok,$msg_id,$unique_id) = explode(' ',trim($this->response)); |
451 | $ids[$msg_id] = $unique_id; |
452 | } else { |
453 | while($line = fgets($this->conn)) { |
454 | if (trim($line)=='.') { |
455 | break; |
456 | } else { |
457 | list($msg_id,$unique_id) = explode(' ',trim($line)); |
458 | $ids[$msg_id] = $unique_id; |
459 | } |
460 | } |
461 | } |
462 | return $ids; |
463 | } else { |
464 | return false; |
465 | } |
466 | } |
467 | |
468 | /** |
469 | * USER authentication (username command) |
470 | * |
471 | * Command is optional and not required by rfc1939. If command |
472 | * is successful, pass command must be executed after it. |
473 | * @param string $username |
474 | * @return boolean true = success, false = failure. |
475 | */ |
476 | function command_user($username) { |
477 | fwrite($this->conn,"USER $username\r\n"); |
478 | return $this->check_response(); |
479 | } |
480 | |
481 | /** |
482 | * USER authentication (password command) |
483 | * |
484 | * Command is optional and not required by rfc1939. Requires |
485 | * successful user command. |
486 | * @param string $password |
487 | * @return boolean true = success, false = failure. |
488 | */ |
489 | function command_pass($password) { |
490 | fwrite($this->conn,"PASS $password\r\n"); |
491 | return $this->check_response(); |
492 | } |
493 | |
494 | /** |
495 | * APOP authentication |
496 | * |
497 | * Command is optional and not required by rfc1939. APOP support |
498 | * requires plain text passwords stored on server and some servers |
499 | * don't support it. Standard qmail pop3d declares apop support |
500 | * without checking if checkpassword supports it. |
501 | * @param string $username |
502 | * @param string $password |
503 | * @return boolean true = success, false = failure. |
504 | */ |
505 | function command_apop($username,$password) { |
506 | if (empty($this->timestamp)) { |
507 | return $this->set_error('APOP is not supported by selected server.'); |
508 | } |
509 | $digest = md5($this->timestamp . $password); |
510 | |
511 | fwrite($this->conn,"APOP $username $digest\r\n"); |
512 | return $this->check_response(); |
513 | } |
514 | |
515 | // RFC2449 POP3EXT |
516 | |
517 | /** |
518 | * Checks pop server capabilities |
519 | * |
520 | * RFC2449. Fills capabilities array. |
521 | * @return void |
522 | */ |
523 | function command_capa() { |
524 | fwrite($this->conn,"CAPA\r\n"); |
525 | if ($this->check_response()) { |
526 | // reset array. capabilities depend on authorization state |
527 | $this->capabilities = array(); |
528 | while($line = fgets($this->conn)) { |
529 | if (trim($line)=='.') { |
530 | break; |
531 | } else { |
532 | $this->capabilities[] = trim($line); |
533 | } |
534 | } |
535 | } else { |
536 | // if capa fails, error buffer contains error message. |
537 | // Clean error buffer, |
538 | // if POP3EXT is not supported, capability array will be empty |
539 | $this->error = ''; |
540 | } |
541 | } |
542 | |
543 | // RFC2595 STARTTLS |
544 | |
545 | /** |
546 | * RFC 2595 POP STARTTLS support |
547 | * @return boolean |
548 | */ |
549 | function command_stls() { |
550 | if (! function_exists('stream_socket_enable_crypto')) { |
551 | return $this->set_error('Used PHP version does not support functions required for POP STLS.',true); |
552 | } elseif (in_array('STLS',$this->capabilities)) { |
553 | return $this->set_error('Selected POP3 server does not support STLS.',true); |
554 | } |
555 | fwrite($this->conn,"STLS\r\n"); |
556 | if (! $this->check_response()) { |
557 | return false; |
558 | } |
559 | |
560 | if (@stream_socket_enable_crypto($this->conn,true,STREAM_CRYPTO_METHOD_TLS_CLIENT)) { |
561 | // starttls was successful (rfc2595 4. POP3 STARTTLS extension.) |
562 | // get new CAPA response |
563 | $this->command_capa(); |
564 | } else { |
565 | /** stream_socket_enable_crypto() call failed. */ |
566 | return $this->set_error('Unable to start TLS.',true); |
567 | } |
568 | return true; |
569 | } |
570 | } |