PHP使用socks5代理发送邮件

通过socks5代理使用smtp发邮件

起因

因为使用smtp发邮件,点击查看邮件原文能看到发送的服务器ip,而一般使用smtp的服务器为后台服务器,为了防止ip暴露,所以需要通过代理来进行发邮件的动作。
红色框中为连接smtp服务器的服务器ip地址
为了完成这个需求,首先百度、谷歌了一遍没找到有能’参考‘的例子,因此只能自己想办法去实现。

思路1

之前使用过php的curl扩展中添加代理,因此很容易就想到用curl,而curl使用代理的相关例子也是比较容易找到,在curl中设置相关参数就可以。

curl_setopt($curl, CURLOPT_PROXYAUTH, CURLAUTH_BASIC);
curl_setopt($curl, CURLOPT_PROXYTYPE, CURLPROXY_SOCKS5);
curl_setopt($curl, CURLOPT_PROXY, "代理服务器ip");
curl_setopt($curl, CURLOPT_PROXYPORT, "代理端口");
curl_setopt($curl, CURLOPT_PROXYUSERPWD, "账号:密码");

而使用curl去发送smtp邮件的例子我也找到,参考这个帖子的例子php中通过curl smtp发送邮件虽然我不知道他的代码自己是不是能实现的,反正在我的环境PHP Version 7.1.26;cURL Information 7.61.1是报错的,提示curl的url不合法)。然后花了不少时间去查出错的原因(因为在phpinfo中看到cURL支持的协议有smtp、telnet,我觉得还能抢救一下),但是相关的用法没找到文档或例子。百度能搜到的只是刚刚参考的例子,谷歌搜到的使用curl发smtp邮件的完全没有。唯一相关的帖子回答大概意思是curl虽然支持简单的SMTP连接,但是不适合进行这种持续发送应答的tcp连接,最好使用sockets去完成,于是我使用了思路二的方式。
在这里插入图片描述

思路2

没办法使用curl(目前的能力不足)完成就只能使用sockets去连接了。没有办法偷懒,只能先去看一下socks5的协议和smtp的协议。这里难点其实是如何让socks5服务器连接上smtp服务器,连上以后又要如何让socks5服务器发消息给smtp服务器。而使用sockets去完成smtp发邮件的一整套流程,有比较多可以参考的资料。

smtp协议

对smtp协议没什么概念的话,建议可以参考一下一些使用cmd的telnet发送邮件的例子(随便贴一个例子),然后尝试使用telnet去发一封邮件。以qq的smtp服务器为例:(为回复消息)

  • telnet smtp.qq.com 587(220 smtp.qq.com Esmtp QQ Mail Server)
  • EHLO smtp.qq.com250-smtp.qq.com 250-PIPELINING 250-SIZE 73400320 250-STARTTLS 250-AUTH LOGIN PLAIN 250-AUTH=LOGIN 250-MAILCOMPRESS 250 8BITMIME)
  • STARTTLS auth login(220 Ready to start TLS)
  • AUTH LOGIN(334 VXNlcm5hbWU6)
  • {base64加密后的qq邮箱账号}(334 VXNlcm5hbWU6)
  • {base64加密后的qq邮箱密码}(235 Authentication successful)
  • mail from: <发件人邮箱地址>(250 OK)
  • rcpt to:<接收人邮箱地址>(250 OK)
  • data(354 END DATA WITH <CR><LF>.<CR><LF>)(意思是空行加一个"." 代表结束)
  • 输入一些邮件的补充信息标题、日期、回复地址、版本 等等(其中标题如果是中文需要拼接一下成Subject:=?UTF-8?B?{base64加密标题}?= )
  • 正文
  • /*空一行*/
  • .(250 OK queued as)
    到这里就已经完成发送邮件了,可以看到自己的邮箱已经收到一封没有附件的邮件。我的代码中就是按照这种形式去完成的发送邮件。

socks5协议

接下来就是socks5协议,这里我找到一篇比较详细的帖子,对socks5服务器的一些应答有一定的解释说明socks5代理服务器协议说明。按照我需要的,我只需要完成几步:

  • 连上socks5服务器
  • 完成socks服务器认证
  • 让socks服务器连上smtp服务器

而我需要做的是:

  • 向代理服务器对应端口发送0x05 0x01 0x02 且服务器回复0x05 0x02 (5 1 2 说明是认证密码模式 还有不需要认证的模式 这里我就没有管这个 在上面的帖子有对不同的模式进行说明)
  • 向代理服务器发送0x01 代理服务器账号长度 代理服务器账号 代理服务器密码长度 代理服务器密码 且服务器回复0x01 0x00(发送0x01 固定,然后指定之后的多少个字节为账号 然后是账号 然后指定之后多少个字节为密码 然后是密码 服务器回复1 0说明认证成功)
  • 向代理服务器发送0x05 0x01 0x00 0x03 smtp服务器域名长度 smtp服务器域名 smtp服务器端口 且服务器回复0x05 0x00 0x00 0x01 再加上6位数字(发送5 1 0 3 说明让代理服务器 访问后面指定的域名和端口 如果是 ip 则发 5 1 0 1 回复的 前4位固定 后面6位表示代理服务器使用了哪个端口去进行连接)
测试时因为这个不能像smtp一样使用telnet去测试,所以这里我直接使用代码来测试
pfsockopen($this->proxyHost,$this->proxyPort,$errno,$errbuf, 60);//创建一个socket句柄
fwrite($this->sock, pack("C3", 0x05, 0x01, 0x02));//发送5 1 2 
$res=fread($this->sock,512);//代理服务器回复的内容 这里直接打印出来是乱码,通过打印unpack('C'.strlen($res),$res) 可以查看返回值

到这里其实就已经完成了代理服务器的部分(就是这么简单),后面就是按smtp的协议直接发送,接收。

testSMTP.php例子代码

<?php
use Mail\ProxySMTP;

require('ProxySMTP.php');
$SMTP=new ProxySMTP;
$SMTP->proxyHost="代理服务器ip";
$SMTP->proxyPort="代理服务器端口";
$SMTP->proxyUsername="代理服务器用户名";
$SMTP->proxyPassword= "代理服务器密码";
$SMTP->smtpHost='smtp.qq.com';//smtp服务器域名
$SMTP->smtpPort='587';//smtp服务器端口
$SMTP->smtpUsername='2592515244@qq.com';//填邮箱
$SMTP->smtpPassword='smtp服务器的账号对应的密码';//填设置里面的授权码 不是qq密码
$SMTP->title='测试标题';
$SMTP->content='测试内容';
$SMTP->from='2592515244@qq.com';//发信人 和账号一样
$SMTP->to='2592515244@qq.com';//接收人
$SMTP->attachment='TIM图片20190322105750.gif';//加附件路径
$return=$SMTP->send();
echo $return['msg'];
?>

ProxySMTP.php类的代码

<?php
namespace Mail;

class ProxySMTP{
    //代理服务器ip
    public $proxyHost;
    //代理服务器端口
    public $proxyPort;
    //代理服务器用户名
    public $proxyUsername;
    //代理服务器密码
    public $proxyPassword;
    //smtp 域名(不能是ip)ip需要改代理的报文前缀 5 1 0 3改成 5 1 0 1
    public $smtpHost;
    //smtp 端口
    public $smtpPort;
    //smtp用户名
    public $smtpUsername;
    //smtp授权码
    public $smtpPassword;
    //邮件标题
    public $title;
    //发件人邮箱
    public $from;
    //收件人邮箱
    public $to;
    //邮件正文
    public $content;
    //附件有效访问路径
    public $attachment='';
    //附件的相关变量
    protected $file_name;
    protected $file_type;
    protected $file_ext;
    //smtp判断指令结束符号 回车符+换行符
    protected $CRLF="\r\n";
    //socket句柄
    private $sock;
    //socks服务器连上smtp服务器状态
    private $connectSMTP=false;

    /**
     * 连接代理服务器,让代理服务器连接smtp服务器
     *
     *
     * */
    protected function connect()
    {
        $this->sock = pfsockopen($this->proxyHost,$this->proxyPort,$errno,$errbuf, 60);
        if(!$this->sock){
            //连接失败
            return ['status'=>false,'errno'=>$errno,'errstr'=>$errbuf,'msg'=>'socks服务器连接失败'];
        }
        fwrite($this->sock, pack("C3", 0x05, 0x01, 0x02));
        if(fread($this->sock,512)!==pack('C2',0x05,0x02)){
            //socks服务器不支持密码认证模式
            return ['status'=>false,'errno'=>$errno,'errstr'=>$errbuf,'msg'=>'socks服务器不支持密码认证模式'];
        }
        fwrite($this->sock, pack('C2',0x01,strlen($this->proxyUsername)).$this->proxyUsername.pack('C1',strlen($this->proxyPassword)).$this->proxyPassword);
        if(fread($this->sock,512)!==pack('C2',0x01,0x00)){
            //socks服务器账号密码认证失败
            return ['status'=>false,'errno'=>$errno,'errstr'=>$errbuf,'msg'=>'socks服务器账号密码认证失败'];
        }
        fwrite($this->sock, pack("C5", 0x05 , 0x01 , 0x00 , 0x03, strlen($this->smtpHost)).$this->smtpHost.pack("n", $this->smtpPort));
        if(substr(fread($this->sock,512),0,4)!==pack('C4',0x05,0x00,0x00,0x01)){
            //socks服务器连接smtp服务器失败
            return ['status'=>false,'errno'=>$errno,'errstr'=>$errbuf,'msg'=>'socks服务器连接smtp服务器失败'];
        }else{
            $return=fread($this->sock,512);
            if(substr($return,0,3)!='220'){
                //smtp连接错误
                return ['status'=>false,'errno'=>$errno,'errstr'=>$errbuf,'msg'=>$return];
            }
            $this->connectSMTP=true;
            return ['status'=>true];
        }
    }

    /**
     *检查必传字段是否设置好
     *
     *
     * */
    protected function beforeSend(){
        $reflect =new \ReflectionClass($this);
        $arg=$reflect->getProperties(\ReflectionProperty::IS_PUBLIC);
        foreach($arg as $v){
            if($v->getName()!='attachment'){
                if(!$v->getValue($this)){
                    return ['status'=>false,'msg'=>$v->getName().'不能为空'];
                }
            }
        }
        return ['status'=>true];
    }

    /**
     * 发邮件方法,连接代理服务器,让代理服务器连接smtp服务器并验证权限后发送
     *
     *
     * */
    public function send()
    {
        $beforeSendCheck=$this->beforeSend();
        if(!$beforeSendCheck['status']){
            return $beforeSendCheck;
        }
        $connectStatus=$this->connect();
        if(!$connectStatus['status']){
            return $connectStatus;
        }
        $authStatus=$this->smtpAuth();
        if(!$authStatus['status']){
            return $authStatus;
        }
        $headerStatus=$this->smtpHeader();
        if(!$headerStatus['status']){
            return $headerStatus;
        }
        $bodyStatus=$this->smtpBody();
        if(!$bodyStatus['status']){
            return $bodyStatus;
        }
        return ['status'=>true,'msg'=>'发送成功'];
    }

    /**
     * 邮箱账号密码认证
     *
     *
     * */
    protected function smtpAuth()
    {
        fputs($this->sock,'EHLO '.$this->smtpHost.$this->CRLF);
        fread($this->sock,512);
        fputs($this->sock,'STARTTLS auth login'.$this->CRLF);
        $return=fread($this->sock,512);
        if(substr($return,0,3)!='220'){
            //不支持TLS
            return ['status'=>false,'msg'=>$return];
        }
        fputs($this->sock,'AUTH LOGIN'.$this->CRLF);
        $return=fread($this->sock,512);
        if(substr($return,0,3)!='334'){
            //不支持TLS
            return ['status'=>false,'msg'=>$return];
        }
        fputs($this->sock,base64_encode($this->smtpUsername).$this->CRLF);
        $return=fread($this->sock,512);
        if(substr($return,0,3)!='334'){
            //不支持TLS
            return ['status'=>false,'msg'=>$return];
        }
        fputs($this->sock,base64_encode($this->smtpPassword).$this->CRLF);
        $return=fread($this->sock,512);
        if(substr($return,0,3)!='235'){
            //账号密码认证不成功
            return ['status'=>false,'msg'=>'账号密码认证不成功'];
        }
        return ['status'=>true];
    }

    /**
     * 设置邮件发送人和收件人
     *
     *
     * */
    protected function smtpHeader()
    {
        fputs($this->sock,'mail from:<'.$this->from.'>'.$this->CRLF);
        $return=fread($this->sock,512);
        if(substr($return,0,3)!='250'){
            //发送不成功
            return ['status'=>false,'msg'=>'发件人邮箱发送不成功'];
        }
        fputs($this->sock,'rcpt to:<'.$this->to.'>'.$this->CRLF);
        $return=fread($this->sock,512);
        if(substr($return,0,3)!='250'){
            //发送不成功
            return ['status'=>false,'msg'=>'收件人邮箱发送不成功'];
        }
        return ['status'=>true];
    }

    /**
     * 设置邮件正文
     *
     *
     * */
    protected function smtpBody()
    {
        fputs($this->sock,'data'.$this->CRLF);
        $return=fread($this->sock,512);
        if(substr($return,0,3)!='354'){
            //发送不成功
            return ['status'=>false,'msg'=>'正文发送不成功'];
        }
        fputs($this->sock,'Return-path:<'.$this->from.'>'.$this->CRLF);
        fputs($this->sock,'Date:'.date('r').$this->CRLF);
        fputs($this->sock,'From:<'.$this->from.'>'.$this->CRLF);
        fputs($this->sock,'MIME-Version:1.0'.$this->CRLF);
        fputs($this->sock,'Subject:=?UTF-8?B?'.base64_encode(trim($this->title)).'?= '.$this->CRLF);
        fputs($this->sock,'To:<'.$this->to.'>'.$this->CRLF);
        if($this->attachment){
            //有附件
            $this->file_name=basename($this->attachment);
            $this->file_ext=$this->mb_pathinfo($this->file_name, PATHINFO_EXTENSION);
            $this->file_type=$this->_mime_types($this->file_ext);
            $boundary=$this->generateId();
            fputs($this->sock,'Content-type: multipart/mixed; boundary="'.$boundary.'"'.$this->CRLF.$this->CRLF);
            fputs($this->sock,'This is a multi-part message in MIME format.'.$this->CRLF);
            fputs($this->sock,'--'.$boundary.$this->CRLF);
            fputs($this->sock,'Content-type:text/plain; charset=UTF-8'.$this->CRLF);
            fputs($this->sock,'Content-Transfer-Encoding:base64'.$this->CRLF.$this->CRLF);
            fputs($this->sock,base64_encode($this->content).$this->CRLF.$this->CRLF);
            //附件
            fputs($this->sock,'--'.$boundary.$this->CRLF);
            fputs($this->sock,'Content-type:'.$this->file_type.'; name==?UTF-8?B?'.base64_encode($this->file_name).'?='.$this->CRLF);
            fputs($this->sock,'Content-Transfer-Encoding:base64'.$this->CRLF);
            fputs($this->sock,'Content-Disposition: attachment; filename==?UTF-8?B?'.base64_encode($this->file_name).'?='.$this->CRLF.$this->CRLF);
            $file_string=chunk_split(base64_encode(file_get_contents($this->attachment)),76,$this->CRLF);
            fputs($this->sock,$file_string.$this->CRLF);
            fputs($this->sock,'--'.$boundary.$this->CRLF);
        }else{
            //没附件
            fputs($this->sock,'Content-Type:text/html; charset=UTF-8; format=flowed'.$this->CRLF);
            fputs($this->sock,'Content-Transfer-Encoding:base64'.$this->CRLF.$this->CRLF);
            fputs($this->sock,base64_encode($this->content).$this->CRLF.$this->CRLF);
        }
        //结束符号 换行+制表+.+换行+制表
        fputs($this->sock,'.'.$this->CRLF);
        $return=fread($this->sock,512);
        if(substr($return,0,3)!='250'){
            //发送不成功
            return ['status'=>false,'msg'=>'正文发送不成功'];
        }
        return ['status'=>true];
    }

    protected function mb_pathinfo($path, $options = null)
    {
        $ret = ['dirname' => '', 'basename' => '', 'extension' => '', 'filename' => ''];
        $pathinfo = [];
        if (preg_match('#^(.*?)[\\\\/]*(([^/\\\\]*?)(\.([^\.\\\\/]+?)|))[\\\\/\.]*$#im', $path, $pathinfo)) {
            if (array_key_exists(1, $pathinfo)) {
                $ret['dirname'] = $pathinfo[1];
            }
            if (array_key_exists(2, $pathinfo)) {
                $ret['basename'] = $pathinfo[2];
            }
            if (array_key_exists(5, $pathinfo)) {
                $ret['extension'] = $pathinfo[5];
            }
            if (array_key_exists(3, $pathinfo)) {
                $ret['filename'] = $pathinfo[3];
            }
        }
        switch ($options) {
            case PATHINFO_DIRNAME:
            case 'dirname':
                return $ret['dirname'];
            case PATHINFO_BASENAME:
            case 'basename':
                return $ret['basename'];
            case PATHINFO_EXTENSION:
            case 'extension':
                return $ret['extension'];
            case PATHINFO_FILENAME:
            case 'filename':
                return $ret['filename'];
            default:
                return $ret;
        }
    }

    /**
     * 根据后缀判断mime type
     * @param $ext 后缀
     *
     * */
    protected function _mime_types($ext = '')
    {
        $mimes = [
            'xl' => 'application/excel',
            'js' => 'application/javascript',
            'hqx' => 'application/mac-binhex40',
            'cpt' => 'application/mac-compactpro',
            'bin' => 'application/macbinary',
            'doc' => 'application/msword',
            'word' => 'application/msword',
            'xlsx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
            'xltx' => 'application/vnd.openxmlformats-officedocument.spreadsheetml.template',
            'potx' => 'application/vnd.openxmlformats-officedocument.presentationml.template',
            'ppsx' => 'application/vnd.openxmlformats-officedocument.presentationml.slideshow',
            'pptx' => 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
            'sldx' => 'application/vnd.openxmlformats-officedocument.presentationml.slide',
            'docx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
            'dotx' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.template',
            'xlam' => 'application/vnd.ms-excel.addin.macroEnabled.12',
            'xlsb' => 'application/vnd.ms-excel.sheet.binary.macroEnabled.12',
            'class' => 'application/octet-stream',
            'dll' => 'application/octet-stream',
            'dms' => 'application/octet-stream',
            'exe' => 'application/octet-stream',
            'lha' => 'application/octet-stream',
            'lzh' => 'application/octet-stream',
            'psd' => 'application/octet-stream',
            'sea' => 'application/octet-stream',
            'so' => 'application/octet-stream',
            'oda' => 'application/oda',
            'pdf' => 'application/pdf',
            'ai' => 'application/postscript',
            'eps' => 'application/postscript',
            'ps' => 'application/postscript',
            'smi' => 'application/smil',
            'smil' => 'application/smil',
            'mif' => 'application/vnd.mif',
            'xls' => 'application/vnd.ms-excel',
            'ppt' => 'application/vnd.ms-powerpoint',
            'wbxml' => 'application/vnd.wap.wbxml',
            'wmlc' => 'application/vnd.wap.wmlc',
            'dcr' => 'application/x-director',
            'dir' => 'application/x-director',
            'dxr' => 'application/x-director',
            'dvi' => 'application/x-dvi',
            'gtar' => 'application/x-gtar',
            'php3' => 'application/x-httpd-php',
            'php4' => 'application/x-httpd-php',
            'php' => 'application/x-httpd-php',
            'phtml' => 'application/x-httpd-php',
            'phps' => 'application/x-httpd-php-source',
            'swf' => 'application/x-shockwave-flash',
            'sit' => 'application/x-stuffit',
            'tar' => 'application/x-tar',
            'tgz' => 'application/x-tar',
            'xht' => 'application/xhtml+xml',
            'xhtml' => 'application/xhtml+xml',
            'zip' => 'application/zip',
            'mid' => 'audio/midi',
            'midi' => 'audio/midi',
            'mp2' => 'audio/mpeg',
            'mp3' => 'audio/mpeg',
            'm4a' => 'audio/mp4',
            'mpga' => 'audio/mpeg',
            'aif' => 'audio/x-aiff',
            'aifc' => 'audio/x-aiff',
            'aiff' => 'audio/x-aiff',
            'ram' => 'audio/x-pn-realaudio',
            'rm' => 'audio/x-pn-realaudio',
            'rpm' => 'audio/x-pn-realaudio-plugin',
            'ra' => 'audio/x-realaudio',
            'wav' => 'audio/x-wav',
            'mka' => 'audio/x-matroska',
            'bmp' => 'image/bmp',
            'gif' => 'image/gif',
            'jpeg' => 'image/jpeg',
            'jpe' => 'image/jpeg',
            'jpg' => 'image/jpeg',
            'png' => 'image/png',
            'tiff' => 'image/tiff',
            'tif' => 'image/tiff',
            'webp' => 'image/webp',
            'heif' => 'image/heif',
            'heifs' => 'image/heif-sequence',
            'heic' => 'image/heic',
            'heics' => 'image/heic-sequence',
            'eml' => 'message/rfc822',
            'css' => 'text/css',
            'html' => 'text/html',
            'htm' => 'text/html',
            'shtml' => 'text/html',
            'log' => 'text/plain',
            'text' => 'text/plain',
            'txt' => 'text/plain',
            'rtx' => 'text/richtext',
            'rtf' => 'text/rtf',
            'vcf' => 'text/vcard',
            'vcard' => 'text/vcard',
            'ics' => 'text/calendar',
            'xml' => 'text/xml',
            'xsl' => 'text/xml',
            'wmv' => 'video/x-ms-wmv',
            'mpeg' => 'video/mpeg',
            'mpe' => 'video/mpeg',
            'mpg' => 'video/mpeg',
            'mp4' => 'video/mp4',
            'm4v' => 'video/mp4',
            'mov' => 'video/quicktime',
            'qt' => 'video/quicktime',
            'rv' => 'video/vnd.rn-realvideo',
            'avi' => 'video/x-msvideo',
            'movie' => 'video/x-sgi-movie',
            'webm' => 'video/webm',
            'mkv' => 'video/x-matroska',
        ];
        $ext = strtolower($ext);
        if (array_key_exists($ext, $mimes)) {
            return $mimes[$ext];
        }
        return 'application/octet-stream';
    }

    /**
     * 生成随机字符串 用于有附件时 邮件boundary 区分多个部分
     *
     *
     * */
    protected function generateId()
    {
        $len = 32; //256 bits
        if(function_exists('random_bytes')){
            $bytes = random_bytes($len);
        }else if(function_exists('openssl_random_pseudo_bytes')) {
        $bytes = openssl_random_pseudo_bytes($len);
        }else{
            $bytes = hash('sha256', uniqid((string) mt_rand(), true), true);
        }
        return 'b1_'.str_replace(['=', '+', '/'], '', base64_encode(hash('sha256', $bytes, true)));
    }

    /**
     *断开smtp服务器连接 释放句柄
     *
     *
     * */
    protected function close(){
        if($this->sock!==null){
            if($this->connectSMTP){
                fputs($this->sock,'QUIT'.$this->CRLF);
                if(substr(fread($this->sock,512),0,3)!='221'){
                    //发送不成功
                    return ['status'=>false,'msg'=>'退出指令发送失败'];
                }
            }
            if(is_resource($this->sock)){
                fclose($this->sock);
            }
        }
        return ['status'=>true];
    }

    /**
     * 析构函数 断开连接
     *
     *
     */
    public function __destruct()
    {
        $this->close();
    }
}
?>
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值