1: <?php
2:
3: /**
4: * SMTP libs
5: * @package modules
6: * @subpackage smtp
7: */
8:
9: /**
10: * SMTP connection manager
11: * @subpackage smtp/lib
12: */
13: class Hm_SMTP_List {
14:
15: use Hm_Server_List;
16:
17: protected static $user_config;
18: protected static $session;
19:
20: public static function init($user_config, $session) {
21: self::initRepo('smtp_servers', $user_config, $session, self::$server_list);
22: self::$user_config = $user_config;
23: self::$session = $session;
24: }
25:
26: public static function service_connect($id, $server, $user, $pass, $cache=false) {
27: $config = array(
28: 'id' => $id,
29: 'server' => $server['server'],
30: 'port' => $server['port'],
31: 'tls' => $server['tls'],
32: 'username' => $user,
33: 'password' => $pass,
34: 'type' => array_key_exists('type', $server) && !empty($server['type']) ? $server['type'] : 'smtp',
35: );
36: if (array_key_exists('auth', $server)) {
37: $config['auth'] = $server['auth'];
38: }
39: if (array_key_exists('no_auth', $server)) {
40: $config['no_auth'] = true;
41: }
42: self::$server_list[$id]['object'] = new Hm_Mailbox($id, self::$user_config, self::$session, $config);
43: if (! self::$server_list[$id]['object']->connect()) {
44: return self::$server_list[$id]['object'];
45: }
46: return false;
47: }
48:
49: public static function get_cache($session, $id) {
50: return false;
51: }
52:
53: public static function address_list() {
54: $addrs = array();
55: foreach (self::$server_list as $server) {
56: $addrs[] = $server['user'];
57: }
58: return $addrs;
59: }
60: }
61:
62: /**
63: * Connect to and interact with SMTP servers
64: * @subpackage smtp/lib
65: */
66: class Hm_SMTP {
67: private $config;
68: private $server;
69: private $starttls;
70: private $port;
71: private $tls;
72: private $auth;
73: private $handle;
74: private $debug;
75: private $hostname;
76: private $command_count;
77: private $commands;
78: private $responses;
79: private $smtp_err;
80: private $banner;
81: private $capability;
82: private $connected;
83: private $crlf;
84: private $line_length;
85: private $username;
86: private $password;
87: public $state;
88: private $request_auths = array();
89: private $scramAuthenticator;
90: private $supports_tls;
91: private $supports_auth;
92: private $max_message_size;
93:
94: function __construct($conf) {
95: $this->scramAuthenticator = new ScramAuthenticator();
96: $this->hostname = php_uname('n');
97: if (preg_match("/:\d+$/", $this->hostname)) {
98: $this->hostname = mb_substr($this->hostname, 0, mb_strpos($this->hostname, ':'));
99: }
100: $this->debug = array();
101: if (isset($conf['server'])) {
102: $this->server = $conf['server'];
103: }
104: else {
105: $this->server = '127.0.0.1';
106: }
107: if (isset($conf['port'])) {
108: $this->port = $conf['port'];
109: }
110: else {
111: $this->port = 25;
112: }
113: if (isset($conf['tls']) && $conf['tls']) {
114: $this->tls = true;
115: }
116: else {
117: $this->tls = false;
118: }
119: if (!$this->tls) {
120: $this->starttls = true;
121: }
122: $this->request_auths = array(
123: 'scram-sha-1',
124: 'scram-sha-1-plus',
125: 'scram-sha-256',
126: 'scram-sha-256-plus',
127: 'scram-sha-224',
128: 'scram-sha-224-plus',
129: 'scram-sha-384',
130: 'scram-sha-384-plus',
131: 'scram-sha-512',
132: 'scram-sha-512-plus',
133: 'cram-md5',
134: 'login',
135: 'plain');
136: if (isset($conf['auth'])) {
137: array_unshift($this->request_auths, $conf['auth']);
138: }
139: $this->auth = true;
140: if (isset($conf['no_auth'])) {
141: $this->auth = false;
142: }
143: $this->smtp_err = '';
144: $this->supports_tls = false;
145: $this->supports_auth = array();
146: $this->handle = false;
147: $this->state = 'started';
148: $this->command_count = 0;
149: $this->commands = array();
150: $this->responses = array();
151: $this->banner = '';
152: $this->crlf = "\r\n";
153: $this->capability = '';
154: $this->line_length = 2048;
155: $this->connected = false;
156: $this->username = $conf['username'];
157: $this->password = $conf['password'];
158: $this->max_message_size = 0;
159: }
160:
161: /* send command to the server. Append "\r\n" to the end. */
162: function send_command($command) {
163: if (is_resource($this->handle)) {
164: fputs($this->handle, $command.$this->crlf);
165: }
166: $this->commands[] = trim($command);
167: }
168:
169: /* loop through "lines" returned from smtp and parse
170: them. It can return the lines in a raw format, or
171: parsed into atoms.
172: */
173: function get_response($chunked=true) {
174: $n = -1;
175: $result = array();
176: $chunked_result = array();
177: do {
178: $n++;
179: if (!is_resource($this->handle)) {
180: break;
181: }
182: $result[$n] = fgets($this->handle, $this->line_length);
183: $chunks = $this->parse_line($result[$n]);
184: if ($chunked) {
185: $chunked_result[] = $chunks;
186: }
187: if (!trim($result[$n])) {
188: unset($result[$n]);
189: break;
190: }
191: $cont = false;
192: if (mb_strlen($result[$n]) > 3 && mb_substr($result[$n], 3, 1) == '-') {
193: $cont = true;
194: }
195: } while ($cont);
196: $this->responses[] = $result;
197: if ($chunked) {
198: $result = $chunked_result;
199: }
200: return $result;
201: }
202:
203: /* parse out a line */
204: function parse_line($line) {
205: $parts = array();
206:
207: $code = mb_substr($line, 0, 3);
208: $parts[] = $code;
209:
210: $remainder = explode(' ',mb_substr($line, 4));
211: $parts[] = $remainder;
212:
213: return $parts;
214:
215: }
216: /* Checks if the numeric response matches the code in $check.
217: The return value is simalar to strcmp
218: Returns <0 if $check is less than the response
219: Returns 0 if $check is equal to the response
220: Returns >0 if $check is greater than the response
221: */
222: function compare_response($chunked_response, $check) {
223: $size = count($chunked_response);
224: if ($size) {
225: $last = $chunked_response[$size-1];
226: $code = $last[0];
227: }
228: else {
229: $code = false;
230: }
231: $return_val = strcmp($check,$code);
232: if ($return_val) {
233: if (isset($chunked_response[0][1])) {
234: $this->smtp_err = join(' ', $chunked_response[0][1]);
235: }
236: }
237: return $return_val;
238: }
239:
240: /* determine what capabilities the server has.
241: Pass it the chunked response from EHLO */
242: function capabilities($ehlo_response) {
243: foreach($ehlo_response as $line) {
244: $feature = trim($line[1][0]);
245: switch(mb_strtolower($feature)) {
246: case 'starttls': // supports starttls
247: $this->supports_tls = true;
248: break;
249: case 'auth': // supported auth mechanisims
250: $auth_mecs = array_slice($line[1], 1);
251: $this->supports_auth = array_map(function($v) { return mb_strtolower($v); }, $auth_mecs);
252: break;
253: case 'size': // advisary maximum message size
254: if(isset($line[1][1]) && is_numeric($line[1][1])) {
255: $this->max_message_size = $line[1][1];
256: }
257: break;
258: }
259: }
260:
261: }
262:
263: /* establish a connection to the server. */
264: function connect() {
265: $certfile = false;
266: $certpass = false;
267: $result = "We couldn't connect to the SMTP server. Please check your internet connection or server settings, and try again.";
268: $server = $this->server;
269:
270: if ($this->tls) {
271: $server = 'tls://'.$server;
272: }
273: $this->debug[] = 'Connecting to '.$server.' on port '.$this->port;
274: $ctx = stream_context_create();
275: stream_context_set_option($ctx, 'ssl', 'verify_peer_name', false);
276: stream_context_set_option($ctx, 'ssl', 'verify_peer', false);
277: $this->handle = Hm_Functions::stream_socket_client($server, $this->port, $errorno, $errorstr, 30, STREAM_CLIENT_CONNECT, $ctx);
278: if (is_resource($this->handle)) {
279: $this->debug[] = 'Successfully opened port to the SMTP server';
280: $this->connected = true;
281: $this->state = 'connected';
282: }
283: else {
284: $this->debug[] = 'Could not connect to the SMTP server';
285: $this->debug[] = 'fsockopen errors #'.$errorno.'. '.$errorstr;
286: // Log technical details for debugging
287: error_log("SMTP connection failed to {$this->server}:{$this->port} - Error #{$errorno}: {$errorstr}");
288: $result = "Unable to connect to the SMTP server. Please check your internet connection or server settings, and try again.";
289: }
290: $this->banner = $this->get_response();
291: $command = 'EHLO '.$this->hostname;
292: $this->send_command($command);
293: $response = $this->get_response();
294: $this->capabilities($response);
295: if ($this->starttls && $this->supports_tls) {
296: $command = 'STARTTLS';
297: $this->send_command($command);
298: $response = $this->get_response();
299: if ($this->compare_response($response, '220') != 0) {
300: // Log technical details for debugging
301: error_log("SMTP STARTTLS command failed. Expected 220, got: " . print_r($response, true));
302: $result = "We couldn't secure the connection to the SMTP server (STARTTLS failed). Please try again later.";
303: }
304: if(isset($certfile) && $certfile) {
305: stream_context_set_option($this->handle, 'tls', 'local_cert', $certfile);
306: if($certpass) {
307: stream_context_set_option($this->handle, 'tls', 'passphrase', $certpass);
308: }
309: }
310: Hm_Functions::stream_socket_enable_crypto($this->handle, get_tls_stream_type());
311: $command = 'EHLO '.$this->hostname;
312: $this->send_command($command);
313: $response = $this->get_response();
314: $this->capabilities($response);
315: }
316: if($this->compare_response($response,'250') != 0) {
317: // Log technical details for debugging
318: error_log("SMTP EHLO command failed. Expected 250, got: " . print_r($response, true));
319: $result = "We couldn't complete the connection to the SMTP server (EHLO command failed). Please try again.";
320: }
321: else {
322: if($this->auth) {
323: $mech = $this->choose_auth();
324: if ($mech) {
325: $result = $this->authenticate($this->username, $this->password, $mech);
326: }
327: }
328: else {
329: if ($this->state == 'connected') {
330: $this->state = 'authed';
331: $result = false;
332: }
333: }
334: }
335: return $result;
336: }
337:
338: function choose_auth() {
339: if (empty($this->supports_auth)) {
340: return false;
341: }
342: $intersect = array_intersect($this->request_auths, $this->supports_auth);
343: if(count($intersect) > 0) {
344: return array_shift($intersect);
345: }
346: return trim($this->supports_auth[0]);
347: }
348: function authenticate($username, $password, $mech) {
349: $mech = mb_strtolower($mech);
350: if (mb_substr($mech, 0, 6) == 'scram-') {
351: $result = $this->scramAuthenticator->authenticateScram(
352: mb_strtoupper($mech),
353: $username,
354: $password,
355: [$this, 'get_response'],
356: [$this, 'send_command']
357: );
358: if ($result) {
359: return 'Authentication successful';
360: }
361: return "Login to the email server failed. Please check your username and password";
362: } else {
363: switch ($mech) {
364: case 'external':
365: $command = 'AUTH EXTERNAL '.base64_encode($username);
366: $this->send_command($command);
367: break;
368: case 'xoauth2':
369: $challenge = 'user='.$username.chr(1).'auth=Bearer '.$password.chr(1).chr(1);
370: $command = 'AUTH XOAUTH2 '.base64_encode($challenge);
371: $this->send_command($command);
372: break;
373: case 'cram-md5':
374: $command = 'AUTH CRAM-MD5';
375: $this->send_command($command);
376: $response = $this->get_response();
377: if (empty($response) || !isset($response[0][1][0]) || $this->compare_response($response,'334') != 0) {
378: $result = 'FATAL: SMTP server does not support AUTH CRAM-MD5';
379: } else {
380: $challenge = base64_decode(trim($response[0][1][0]));
381: $password .= str_repeat(chr(0x00), (64-strlen($password)));
382: $ipad = str_repeat(chr(0x36), 64);
383: $opad = str_repeat(chr(0x5c), 64);
384: $digest = bin2hex(pack('H*', md5(($password ^ $opad).pack('H*', md5(($password ^ $ipad).$challenge)))));
385: $command = base64_encode($username.' '.$digest);
386: $this->send_command($command);
387: }
388: break;
389: case 'ntlm':
390: $command = 'AUTH NTLM '.$this->build_ntlm_type_one();
391: $this->send_command($command);
392: $response = $this->get_response();
393: if (empty($response) || !isset($response[0][1][0]) || $this->compare_response($response,'334') != 0) {
394: $result = 'FATAL: SMTP server does not support AUTH NTLM';
395: } else {
396: $ntlm_res = $this->parse_ntlm_type_two($response[0][1][0]);
397: $command = $this->build_ntlm_type_three($ntlm_res, $username, $password);
398: $this->send_command($command);
399: }
400: break;
401: case 'login':
402: $command = 'AUTH LOGIN';
403: $this->send_command($command);
404: $response = $this->get_response();
405: if (empty($response) || $this->compare_response($response,'334') != 0) {
406: $result = 'FATAL: SMTP server does not support AUTH LOGIN';
407: } else {
408: $command = base64_encode($username);
409: $this->send_command($command);
410: $response = $this->get_response();
411: if (empty($response) || $this->compare_response($response,'334') != 0) {
412: $result = 'FATAL: SMTP server does not support AUTH LOGIN';
413: }
414: $command = base64_encode($password);
415: $this->send_command($command);
416: }
417: break;
418: case 'plain':
419: $command = 'AUTH PLAIN '.base64_encode("\0".$username."\0".$password);
420: $this->send_command($command);
421: break;
422: default:
423: $result = 'FATAL: Unknown SMTP AUTH mechanism: '.$mech;
424: break;
425: }
426: }
427: if (!isset($result)) {
428: $result = "We couldn't log in to the SMTP server. Please check your username and password.";
429: $res = $this->get_response();
430: if ($this->compare_response($res, '235') == 0) {
431: $this->state = 'authed';
432: $result = false;
433: } else {
434: // Log technical details for debugging
435: error_log("SMTP authentication failed. Expected 235, got: " . print_r($res, true));
436: $result = "Login to the SMTP server was not authorized. Please check your username and password, and try again.";
437: if (isset($res[0][1])) {
438: $result .= ': '.implode(' ', $res[0][1]);
439: }
440: }
441: }
442: return $result;
443: }
444:
445: /* parse NTLM challenge string */
446: function parse_ntlm_type_two($bin_str) {
447: $res = array();
448: $res['vals'] = unpack('a8prefix/Vtype/vname_len/vname_space/Vname_offset/Vflags/A8challenge/A8context/vtarget_len/vtarget_space/Vtarget_offset', base64_decode($bin_str));
449: $res['name'] = unpack('A'.$res['vals']['name_len'].'name', substr(base64_decode($bin_str), $res['vals']['name_offset'], $res['vals']['name_len']));
450: $target = substr(base64_decode($bin_str), $res['vals']['target_offset'], $res['vals']['target_len']);
451: $flds = array(2 => 'domain', 1 => 'server', 4 => 'dns_domain', 3 => 'dns_server');
452: $names = array('domain' => '', 'server' => '', 'dns_domain' => '', 'dns_server' => '');
453: while ($target) {
454: $atts = unpack('vfld/vlen', $target);
455: if ($atts['fld'] == 0) {
456: break;
457: }
458: $fld = unpack('A'.$atts['len'], substr($target, 4));
459: if (isset($flds[$atts['fld']])) {
460: $names[$flds[$atts['fld']]] = $fld;
461: }
462: $target = substr($target, (4 + $atts['len']));
463: }
464: $res['names'] = $names;
465: return $res;
466: }
467:
468: /* build initial NTLM message string */
469: function build_ntlm_type_one() {
470: $pre = 'NTLMSSP'.chr(0);
471: $type = pack('V', 1);
472: $flags = pack('V', 0x00000201);
473: return base64_encode($pre.$type.$flags);
474: }
475:
476: /* build NTLM challenge response string */
477: function build_ntlm_type_three($msg_data, $username, $password) {
478: $username = iconv('UTF-8', 'UTF-16LE', $username);
479: $target = $msg_data['name']['name'];
480: $host = iconv('UTF-8', 'UTF-16LE', php_uname('n'));
481: $pre = 'NTLMSSP'.chr(0);
482: $type = pack('V', 3);
483: $lm_response = $this->build_lm_response($msg_data, $username, $password);
484: $ntlm_response = $this->build_ntlm_response($msg_data, $username, $password);
485: $flags = pack('V', 0x00000201);
486: $offset = strlen($pre.$type)+52;
487: $target_sec = $this->ntlm_security_buffer(strlen($target), $offset);
488: $offset += strlen($target);
489: $user_sec = $this->ntlm_security_buffer(mb_strlen($username), $offset);
490: $offset += mb_strlen($username);
491: $host_sec = $this->ntlm_security_buffer(strlen($host), $offset);
492: $offset += mb_strlen($host);
493: $lm_sec = $this->ntlm_security_buffer(strlen($lm_response), $offset);
494: $offset += strlen($lm_response);
495: $ntlm_sec = $this->ntlm_security_buffer(strlen($ntlm_response), $offset);
496: $offset += strlen($ntlm_response);
497: $sess_sec = $this->ntlm_security_buffer(0, $offset);
498: return base64_encode($pre.$type.$lm_sec.$ntlm_sec.$target_sec.$user_sec.$host_sec.$sess_sec.$flags.$target.$username.$host.$lm_response.$ntlm_response);
499: }
500:
501: /* build an NTLM "security buffer" for the type 3 response string */
502: function ntlm_security_buffer($len, $offset) {
503: return pack('vvV', $len, $len, $offset);
504: }
505:
506: /* build the NTLM lm hash then ecnrypt the challenge string with it */
507: function build_lm_response($msg_data, $username, $password){
508: $pass = strtoupper($password);
509: while (strlen($pass) < 14) {
510: $pass .= chr(0);
511: }
512: if (strlen($pass) > 14) {
513: return str_repeat(chr(0), 16);
514: }
515: $p1 = substr($pass, 0, 7);
516: $p2 = substr($pass, 7);
517: $lm_hash = $this->des_encrypt($p1).$this->des_encrypt($p2);
518: while (strlen($lm_hash) < 21) {
519: $lm_hash .= chr(0);
520: }
521: return $this->apply_ntlm_hash($msg_data['vals']['challenge'], $lm_hash);
522: }
523:
524: /* build the NTLM ntlm hash then ecnrypt the challenge string with it */
525: function build_ntlm_response($msg_data, $username, $password){
526: $password = iconv('UTF-8', 'UTF-16LE', $password);
527: $ntlm_hash = hash('md4', $password, true);
528: while (strlen($ntlm_hash) < 21) {
529: $ntlm_hash .= chr(0);
530: }
531: return $this->apply_ntlm_hash($msg_data['vals']['challenge'], $ntlm_hash);
532: }
533:
534: /* encrypt the challenge string with the lm/ntlm hash */
535: function apply_ntlm_hash($challenge, $hash) {
536: $p1 = substr($hash, 0, 7);
537: $p2 = substr($hash, 7, 7);
538: $p3 = substr($hash, 14, 7);
539: return $this->des_encrypt($p1, $challenge).
540: $this->des_encrypt($p2, $challenge).
541: $this->des_encrypt($p3, $challenge);
542: }
543:
544: /* NTLM compatible DES encryption */
545: function des_encrypt($string, $challenge='KGS!@#$%') {
546: $key = array();
547: $tmp = array();
548: $len = strlen($string);
549: for ($i=0; $i<7; ++$i)
550: $tmp[] = $i < $len ? ord($string[$i]) : 0;
551: $key[] = $tmp[0] & 254;
552: $key[] = ($tmp[0] << 7) | ($tmp[1] >> 1);
553: $key[] = ($tmp[1] << 6) | ($tmp[2] >> 2);
554: $key[] = ($tmp[2] << 5) | ($tmp[3] >> 3);
555: $key[] = ($tmp[3] << 4) | ($tmp[4] >> 4);
556: $key[] = ($tmp[4] << 3) | ($tmp[5] >> 5);
557: $key[] = ($tmp[5] << 2) | ($tmp[6] >> 6);
558: $key[] = $tmp[6] << 1;
559: $is = mcrypt_get_iv_size(MCRYPT_DES, MCRYPT_MODE_ECB);
560: $iv = mcrypt_create_iv($is, MCRYPT_RAND);
561: $key0 = "";
562: foreach ($key as $k)
563: $key0 .= chr($k);
564: $crypt = mcrypt_encrypt(MCRYPT_DES, $key0, $challenge, MCRYPT_MODE_ECB, $iv);
565: return $crypt;
566: }
567:
568: /* Send a message */
569: function send_message($from, $recipients, $message, $from_params = '', $recipients_params = '') {
570: $this->clean($from);
571: if ($from_params) {
572: $from_params = ' ' . $from_params;
573: }
574: $from_params = $from_params ? ' ' . $from_params : '';
575: $command = 'MAIL FROM:<'.$from.'>' . $from_params;
576: $this->send_command($command);
577: $res = $this->get_response();
578: $bail = false;
579: $result = "Sorry, we couldn't send your message through the SMTP server right now. Please check your connection and try again.";
580: if(is_array($recipients)) {
581: if ($recipients_params) {
582: $recipients_params = ' ' . $recipients_params;
583: }
584: foreach($recipients as $rcpt) {
585: $this->clean($rcpt);
586: $command = 'RCPT TO:<'.$rcpt.'>'.$recipients_params;
587: $this->send_command($command);
588: $res = $this->get_response();
589: if ($this->compare_response($res, '250') != 0) {
590: $bail = true;
591: break;
592: }
593: }
594: }
595: else {
596: $this->clean($recipients);
597: $command = 'RCPT TO:<'.$recipients.'>';
598: $this->send_command($command);
599: $res = $this->get_response();
600: if ($this->compare_response($res, '250') != 0) {
601: $bail = true;
602: }
603: }
604: if (!$bail) {
605: $command = 'DATA';
606: $this->send_command($command);
607: $res = $this->get_response();
608: if ($this->compare_response($res, '354') != 0) {
609: // Log technical details for debugging
610: error_log("SMTP DATA command failed. Expected 354, got: " . print_r($res, true));
611: $result = "Sorry, we couldn't send your message right now. The SMTP server didn't accept the message for delivery (DATA command failed). Please try again later.";
612: }
613: else {
614: $this->send_command($message);
615: /* TODO: process attachments */
616: $command = $this->crlf.'.';
617: $this->send_command($command);
618: $res = $this->get_response();
619: if ($this->compare_response($res, '250') == 0) {
620: $result = false;
621: }
622: else {
623: // Log technical details for debugging
624: error_log("SMTP message delivery failed. Expected 250, got: " . print_r($res, true));
625: $result = "Your message could not be sent. The SMTP server did not confirm delivery. Please try again later.";
626: }
627: }
628: }
629: else {
630: // Log technical details for debugging
631: error_log("SMTP RCPT command failed for one or more recipients");
632: $result = "There was an error sending your message. One or more of the recipient addresses may be invalid (RCPT command failed). Please check the email addresses and try again.";
633: }
634: return $result;
635: }
636:
637: function puke() {
638: return
639: print_r($this->debug, true).
640: print_r($this->commands, true).
641: print_r($this->responses, true);
642: }
643:
644: /* issue a logout and close the socket to the server */
645: function disconnect() {
646: $command = 'QUIT';
647: $this->send_command($command);
648: $this->state = 'disconnected';
649: $result = $this->get_response();
650: if (is_resource($this->handle)) {
651: fclose($this->handle);
652: }
653: }
654:
655: function clean($val) {
656: if (!preg_match("/^[^\r\n]+$/", $val)) {
657: print_r("INVALID SMTP INPUT DETECTED: <b>$val</b>");
658: exit;
659: }
660: }
661: }
662: