在工作的過程中,經常會有很多應用有發郵件的需求,這個時候需要在每個應用中配置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] < 7 && $version[2] <20){ 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 <-- C]\t".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 <-- S]\t".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還是要填寫正確,否則發出的郵件發件人會顯示錯誤。