php 进行统一邮箱登陆的代理实现(swoole)
在工作的过程中,经常会有很多应用有发邮件的需求,这个时候需要在每个应用中配置smtp服务器。一旦公司调整了smtp服务器的配置,比如修改了密码等,这个时候对于维护的人员来说要逐一修改应用中smtp的配置。这样的情况虽然不多见,但遇上了还是很头痛的一件事情。
知道了问题,解决起来就有了方向。于是就有了自己开发一个简单的smtp代理的想法,这个代理主要的功能(参照问题)主要是:
1.接受指定ip应用的smtp请求;
2.应用不需要知道smtp的用户和密码;
3.转发应用的smtp请求。
开发的环境:linux,php(swoole);
代码如下:
php/** * * smtp proxy server * @author terry zhang, 2015-11-13 * * @version 1.0 * * 注意:本程序只能运行在cli模式,且需要扩展swoole 1.7.20+的支持。 * * swoole的源代码及安装请参考 https://github.com/swoole/swoole-src/ * * 本程序的使用场景: * * 在多个分散的系统中使用同一的邮件地址进行系统邮件发送时,一旦邮箱密码修改,则要修改每个系统的邮件配置参数。 * 同时,在每个系统中配置邮箱参数,使得邮箱的密码容易外泄。 * * 通过本代理进行邮件发送的客户端,可以随便指定用户名和密码。 * * *///error_reporting(0);defined('debug_on') or define('debug_on', false);//主目录defined('base_path') or define('base_path', __dir__);class csmtpproxy{ //软件版本 const version = '1.0'; const eof = \r\n; public static $software = smtp-proxy-server; private static $server_mode = swoole_process; private static $pid_file; private static $log_file; private $smtp_host = 'localhost'; private $smtp_port = 25; private $smtp_user = ''; private $smtp_pass = ''; private $smtp_from = ''; //待写入文件的日志队列(缓冲区) private $queue = array(); public $host = '0.0.0.0'; public $port = 25; public $setting = array(); //最大连接数 public $max_connection = 50; /** * @var swoole_server */ protected $server; protected $connection = array(); public static function setpidfile($pid_file){ self::$pid_file = $pid_file; } public static function start($startfunc){ if(!extension_loaded('swoole')){ exit(require extension `swoole`.\n); } $pid_file = self::$pid_file; $server_pid = 0; if(is_file($pid_file)){ $server_pid = file_get_contents($pid_file); } global $argv; if(empty($argv[1])){ goto usage; }elseif($argv[1] == 'reload'){ if (empty($server_pid)){ exit(smtp proxy server is not running\n); } posix_kill($server_pid, sigusr1); exit; }elseif ($argv[1] == 'stop'){ if (empty($server_pid)){ exit(smtp proxy is not running\n); } posix_kill($server_pid, sigterm); exit; }elseif ($argv[1] == 'start'){ //已存在serverpid,并且进程存在 if (!empty($server_pid) and posix_kill($server_pid,(int) 0)){ exit(smtp proxy is already running.\n); } //启动服务器 $startfunc(); }else{ usage: exit(usage: php {$argv[0]} start|stop|reload\n); } } public function __construct($host,$port){ $flag = swoole_sock_tcp; $this->server = new swoole_server($host,$port,self::$server_mode,$flag); $this->host = $host; $this->port = $port; $this->setting = array( 'backlog' => 128, 'dispatch_mode' => 2, ); } public function daemonize(){ $this->setting['daemonize'] = 1; } public function getconnectioninfo($fd){ return $this->server->connection_info($fd); } /** * 启动服务进程 * @param array $setting * @throws exception */ public function run($setting = array()){ $this->setting = array_merge($this->setting,$setting); //不使用swoole的默认日志 if(isset($this->setting['log_file'])){ self::$log_file = $this->setting['log_file']; unset($this->setting['log_file']); } if(isset($this->setting['max_connection'])){ $this->max_connection = $this->setting['max_connection']; unset($this->setting['max_connection']); } if(isset($this->setting['smtp_host'])){ $this->smtp_host = $this->setting['smtp_host']; unset($this->setting['smtp_host']); } if(isset($this->setting['smtp_port'])){ $this->smtp_port = $this->setting['smtp_port']; unset($this->setting['smtp_port']); } if(isset($this->setting['smtp_user'])){ $this->smtp_user = $this->setting['smtp_user']; unset($this->setting['smtp_user']); } if(isset($this->setting['smtp_pass'])){ $this->smtp_pass = $this->setting['smtp_pass']; unset($this->setting['smtp_pass']); } if(isset($this->setting['smtp_from'])){ $this->smtp_from = $this->setting['smtp_from']; unset($this->setting['smtp_from']); } $this->server->set($this->setting); $version = explode('.', swoole_version); if($version[0] == 1 && $version[1] $version[2] ){ throw new exception('swoole version require 1.7.20 +.'); } //事件绑定 $this->server->on('start',array($this,'onmasterstart')); $this->server->on('shutdown',array($this,'onmasterstop')); $this->server->on('managerstart',array($this,'onmanagerstart')); $this->server->on('managerstop',array($this,'onmanagerstop')); $this->server->on('workerstart',array($this,'onworkerstart')); $this->server->on('workerstop',array($this,'onworkerstop')); $this->server->on('workererror',array($this,'onworkererror')); $this->server->on('connect',array($this,'onconnect')); $this->server->on('receive',array($this,'onreceive')); $this->server->on('close',array($this,'onclose')); $this->server->start(); } public function log($msg,$level = 'debug',$flush = false){ if(debug_on){ $log = date('y-m-d h:i:s').' ['.$level.]\t .$msg.\n; if(!empty(self::$log_file)){ $debug_file = dirname(self::$log_file).'/debug.log'; file_put_contents($debug_file, $log,file_append); if(filesize($debug_file) > 10485760){//10m unlink($debug_file); } } echo $log; } if($level != 'debug'){ //日志记录 $this->queue[] = date('y-m-d h:i:s').\t[.$level.]\t.$msg; } if(count($this->queue)>10 && !empty(self::$log_file) || $flush){ if (filesize(self::$log_file) > 209715200){ //200m rename(self::$log_file,self::$log_file.'.'.date('his')); } $logs = ''; foreach ($this->queue as $q){ $logs .= $q.\n; } file_put_contents(self::$log_file, $logs,file_append); $this->queue = array(); } } public function shutdown(){ return $this->server->shutdown(); } public function close($fd){ return $this->server->close($fd); } public function send($fd,$data){ $data = strtr($data,array(\n => , \0 => , \r => )); $this->log([p --> c]\t . $data); return $this->server->send($fd,$data.self::eof); } /*+++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + 事件回调 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++*/ public function onmasterstart($serv){ global $argv; swoole_set_process_name('php '.$argv[0].': master -host='.$this->host.' -port='.$this->port); if(!empty($this->setting['pid_file'])){ file_put_contents(self::$pid_file, $serv->master_pid); } $this->log('master started.'); } public function onmasterstop($serv){ if (!empty($this->setting['pid_file'])){ unlink(self::$pid_file); } $this->shm->delete(); $this->log('master stop.'); } public function onmanagerstart($serv){ global $argv; swoole_set_process_name('php '.$argv[0].': manager'); $this->log('manager started.'); } public function onmanagerstop($serv){ $this->log('manager stop.'); } public function onworkerstart($serv,$worker_id){ global $argv; if($worker_id >= $serv->setting['worker_num']) { swoole_set_process_name(php {$argv[0]}: worker [task]); } else { swoole_set_process_name(php {$argv[0]}: worker [{$worker_id}]); } $this->log(worker {$worker_id} started.); } public function onworkerstop($serv,$worker_id){ $this->log(worker {$worker_id} stop.); } public function onworkererror($serv,$worker_id,$worker_pid,$exit_code){ $this->log(worker {$worker_id} error:{$exit_code}.); } public function onconnect($serv,$fd,$from_id){ if(count($this->server->connections) $this->max_connection){ $info = $this->getconnectioninfo($fd); if($this->isipallow($info['remote_ip'])){ //建立服务器连接 $cli = new client(swoole_sock_tcp, swoole_sock_async); //异步非阻塞 $cli->on('connect',array($this,'onserverconnect')); $cli->on('receive',array($this,'onserverreceive')); $cli->on('error',array($this,'onservererror')); $cli->on('close',array($this,'onserverclose')); $cli->fd = $fd; $ip = gethostbyname($this->smtp_host); if($cli->connect($ip,$this->smtp_port) !== false){ $this->connection[$fd] = $cli; }else{ $this->close($fd); $this->log('cannot connect to smtp server. connection #'.$fd.' close.'); } }else{ $this->log('blocked clinet connection, ip deny : '.$info['remote_ip'],'warn'); $this->server->close($fd); $this->log('connection #'.$fd.' close.'); } }else{ $this->log('blocked clinet connection, too many connections.','warn'); $this->server->close($fd); } } public function onreceive($serv,$fd,$from_id,$recv_data){ $info = $this->getconnectioninfo($fd); $this->log([p trim($recv_data)); //禁止使用starttls if(strtoupper(trim($recv_data)) == 'starttls'){ $this->server->send($fd,502 not implemented.self::eof); $this->log([p --> c]\t502 not implemented); }else{ //重置登陆验证 if(preg_match('/^auth\s+login(.*)/', $recv_data,$m)){ $m[1] = trim($m[1]); if(empty($m[1])){ //只发送auth login 接下来将发送用户名 $this->connection[$fd]->user = $this->smtp_user; }else{ $recv_data = 'auth login '.base64_encode($this->smtp_user).self::eof; $this->connection[$fd]->pass = $this->smtp_pass; } }else{ if(preg_match('/^helo.*|^ehlo.*/', $recv_data)){ $recv_data = 'helo '.$this->smtp_host.self::eof; } //重置密码 if(!empty($this->connection[$fd]->pass)){ $recv_data = base64_encode($this->connection[$fd]->pass).self::eof; $this->connection[$fd]->pass = ''; } //重置用户名 if(!empty($this->connection[$fd]->user)){ $recv_data = base64_encode($this->connection[$fd]->user).self::eof; $this->connection[$fd]->user = ''; $this->connection[$fd]->pass = $this->smtp_pass; } //重置mail from if(preg_match('/^mail\s+from:.*/', $recv_data)){ $recv_data = 'mail from:$this->smtp_from.'>'.self::eof; } } if($this->connection[$fd]->isconnected()){ $this->connection[$fd]->send($recv_data); $this->log([p --> s]\t.trim($recv_data)); } } } public function onclose($serv,$fd,$from_id){ if(isset($this->connection[$fd])){ if($this->connection[$fd]->isconnected()){ $this->connection[$fd]->close(); $this->log('connection on smtp server close.'); } } $this->log('connection #'.$fd.' close. flush the logs.','debug',true); } /*--------------------------------------------- * * 服务器连接事件回调 * ----------------------------------------------*/ public function onserverconnect($cli){ $this->log('connected to smtp server.'); } public function onserverreceive($cli,$data){ $this->log([p trim($data)); if($this->server->send($cli->fd,$data)){ $this->log([p --> c]\t.trim($data)); } } public function onservererror($cli){ $this->server->close($cli->fd); $this->log('connection on smtp server error: '.$cli->errcode.' '.socket_strerror($cli->errcode),'warn'); } public function onserverclose($cli){ $this->log('connection on smtp server close.'); $this->server->close($cli->fd); } /** * ip地址过滤 * @param unknown $ip * @return boolean */ public function isipallow($ip){ $pass = false; if(isset($this->setting['ip']['allow'])){ foreach ($this->setting['ip']['allow'] as $addr){ $pattern = '/'.str_replace('*','\d+',str_replace('.', '\.', $addr)).'/'; if(preg_match($pattern, $ip) && !empty($addr)){ $pass = true; break; } } } if($pass){ if(isset($this->setting['ip']['deny'])){ foreach ($this->setting['ip']['deny'] as $addr){ $pattern = '/'.str_replace('*','\d+',str_replace('.', '\.', $addr)).'/'; if(preg_match($pattern, $ip) && !empty($addr)){ $pass = false; break; } } } } return $pass; } }class client extends swoole_client{ /** * 记录当前连接 * @var unknown */ public $fd ; public $user = ''; /** * smtp登陆密码 * @var unknown */ public $pass = '';}
配置文件例子:
/** * 运行配置*/return array( 'worker_num' => 12, 'log_file' => base_path.'/logs/proxyserver.log', 'pid_file' => base_path.'/logs/proxyserver.pid', 'heartbeat_idle_time' => 300, 'heartbeat_check_interval' => 60, 'max_connection' => 50,
//配置真实的smtp信息 'smtp_host' => '', 'smtp_port' => 25, 'smtp_user' => '', 'smtp_pass' => '', 'smtp_from' => '', 'ip' => array( 'allow' => array('192.168.0.*'), 'deny' => array('192.168.10.*','192.168.100.*'), ));
运行例子:
defined('base_path') or define('base_path', __dir__);defined('debug_on') or define('debug_on', true);//服务器配置require base_path.'/csmtpproxy.php';$settings = require base_path.'/conf/config.php';csmtpproxy::setpidfile($settings['pid_file']);csmtpproxy::start(function(){ global $settings; $serv = new csmtpproxy('0.0.0.0', 25); $serv->daemonize(); $serv->run($settings);});
应用配置:
smtp host: 192.168.0.* //指定smtpproxy 运行的服务器ip。
port: 25
user: xxxx //随意填写
pass: xxxx //随意填写
from: [email protected] // 根据情况填写
——————————————————————————————————————————————————————
存在的问题:
1、不支持ssl模式;
2、应用的from还是要填写正确,否则发出的邮件发件人会显示错误。