PHP调试技术手册

1内置api输出调试

1.1基本调试api

  • echo
  • printf
  • print_r
    /*
    %% - 返回百分比符号
    %b - 二进制数
    %c - 依照 ASCII 值的字符
    %d - 带符号十进制数
    %e - 可续计数法(比如 1.5e+3)
    %u - 无符号十进制数
    %f - 浮点数(local settings aware)
    %F - 浮点数(not local settings aware)
    %o - 八进制数
    %s - 字符串
    %x - 十六进制数(小写字母)
    %X - 十六进制数(大写字母)
    */
    
    $str = "Hello";
    $number = 123;
    $txt = sprintf("%s world. Day number %u", $str, $number);
    echo $txt;
    
  • var_dump
  • var_export
    • 所有的数据是可以作为组织好的变量输出的,都是能够作为直接赋值使用
    • 对资源型变量输出NULL
  • debug_zval_dump
    • 跟 var_dump 类似,唯一增加的一个值是 refcount,就是记录一个变量被引
      用了多少次
    • PHP COW机制(copy on write)
    foreach($arr as &$v){
        $count++;
        $v='bbbbbb';// 无论是否注释这行, 内存变化不会太大
    }
    
    foreach($arr as $v){
        $count++;
        $v='bbbbbb';// 不注释这行,内存使用上升,因为COW机制
    }    
    
    /*
    在复制一个对象的时候并不是真正的把原先的对象复制到内存的另外一个位置上,
    而是在新对象的内存映射表中设置一个指针,指向源对象的位置,
    并把那块内存的Copy-On-Write位设置为1.这样,在对新的对象执行读操作的时候,
    内存数据不发生任何变动,直接执行读操作;而在对新的对象执行写操作时,
    将真正的对象复制到新的内存地址中,并修改新对象的内存映射表指向这个新的位置,
    并在新的内存位置上执行写操作    
    */
    

1.2错误控制与日志记录调试

  • error_reporting

  • display_errors

  • log_errors 是否记录错误日志

  • error_log 指定错误日志位置

  • trigger_error(主要是能够触
    发的是 E_USER_ERROR, E_USER_WARNING, E_USER_NOTICE 三种级别的错误)

function e($num) {
    if (!is_int($num)) {
        trigger_error('num nust a int',E_USER_WARNING);
        return false;
    }
    echo 'ok';
}
e('test');
  • set_error_handler
    • 以下级别的错误不能由用户定义的函数来处理: E_ERROR 、 E_PARSE 、 E_CORE_ERROR 、 E_CORE_WARNING 、 E_COMPILE_ERROR 、 E_COMPILE_WARNING
set_error_handler('custom_error_handler');
function custom_error_handler($err_no,$err_str,$err_file,$err_line) {
    // 定义日志路径(区分ERROR,WARNING,NOTICE)
    $log_path = 'php_log_%s'.date('Ymd').'.log';
    // 错误信息模版(区分ERROR,WARNING,NOTICE)
    $log_template = date('Y-m-d H:i:s') . "Erro Type:%s,on $err_file->$err_line:$err_str";
    switch($err_no) {
        case E_USER_ERROR:
        $log_path = sprintf($log_path,'error');
        $log_str = sprintf($log_template,'E_USER_ERROR');
        break;
        case E_NOTICE:
        case E_USER_NOTICE:
        $log_path = sprintf($log_path,'notice');
        $log_str = sprintf($log_template,'E_NOTICE|E_USER_NOTICE');        
        break;
        case E_WARNING:
        case E_USER_WARINING:
        $log_path = sprintf($log_path,'warning');
        $log_str = sprintf($log_template,'E_WARNING|E_USER_WARNING');  
        break;
    }
    // file_put_contents 文件不存在则创建
    file_put_contents($log_path,$log_str,FILE_APPEND|LOCK_EX);
}
  • set_exception_handler : 处理出现未捕获的异
    常之后需要调用的处理方法

  • 日志记录

    • 日志记录除了PHP解析级别的错误,更多的是我们程序在执行过程中的一些错误,比如 文件资源打开错误(文件不存在,没有权限,文件格式不正确),远程服务资源访问失败(网络错误,协议不正确,用户/密码错误)等等, 任何你认为不会出错的地方都输出LOG
    • 简单日志记录类
    class MyLog 
    {
        const LOG_ERROR = 1;
        const LOG_WARNING = 2;
        const LOG_NOTICE = 3;
        private $str = '';
        private $log_path = 'php_log'.date('Ymd').'.log';
        
        public function log($msg,$level) 
        {
            switch ($level) {
                case self::LOG_ERROR:
                    $this->str .= date('Y-m-d H:i:s') . ' ERROR:' . $msg . "\n";
                break;
                case self::LOG_WARNING:
                    $this->str .= date('Y-m-d H:i:s') . ' WARNING:' . $msg . "\n";
                break;
                case self::LOG_NOTICE:
                    $this->str .= date('Y-m-d H:i:s') . ' NOTICE:' . $msg . "\n";
                break;
                default:
                    return;
            }
        }
        public function __destruct() 
        {
            if ($this->str) {
                file_put_contents($this->str,$this->log_path,FILE_APPEND|LOCK_EX);            
            }
        }
    }
    
    • 使用error_log作日志记录
    // 如果无法连接到数据库,发送通知到服务器日志
    if (! Ora_Logon ( $username ,  $password )) {
         error_log ( "Oracle database not available!" ,  0 );
    }
    
    // 如果用尽了 FOO,通过邮件通知管理员
    if (!( $foo  =  allocate_new_foo ())) {
         error_log ( "Big trouble, we're all out of FOOs!" ,  1 , "operator@example.com" );
    }
    
    // 调用 error_log() 的另一种方式:
    error_log ( "You messed up!" ,  3 ,  "/var/tmp/my-errors.log" );
    

2浏览器调试

2.1页面输出调试

...各种ide+调试

4PHP性能调试技术

4.1基本时间占用监测

4.2XDEBUG性能监测

  • 配置

    • xdebug.trace_output_dir trace 数据输出目录
    • xdebug.profiler_output_dir 输出监控结果文件目录
    • xdebug.profiler_output_name 如果设置本选项,那么每次结果都只会输出到一个文件
    • xdebug.auto_trace 是否打开 trace
    • xdebug.auto_profile 是否打开性能监测
  • 打开xdebug的性能监测后, 在config.php中指定目录

4.4 使用 Xhprof 进行性能分析

  • 安装xhprof

  • php-config 是一个简单的命令行脚本用于获取所安装的 PHP 配置的信息。

    • 在编译扩展时,如果安装有多个 PHP 版本,可以在配置时用 --with-php-config 选项来指定使用哪一个版本编译,该选项指定了相对应的 php-config 脚本的路径
    wget http://pecl.php.net/get/xhprof-0.9.3.tgz
    
    tar zxf xhprof-0.9.3.tgz
    
    cd xhprof-0.9.3/extension/
    
    phpize
    
    ./configure --with-php-config=/usr/local/php/bin/php-config
    
    make && make install
    
  • 配置

[xhprof]
extension=xhprof.so
;
; directory used by default implementation of the iXHProfRuns
; interface (namely, the XHProfRuns_Default class) for storing
; XHProf runs.
;
xhprof.output_dir=<directory_for_storing_xhprof_runs>

// 确认web进程对目录具有写权限
  • 分析代码
// start profiling
xhprof_enable();

// run program
function test1() {
    sleep(3);

    return;
}

function test2() {
    test1();
}

function test3() {
    test2();
}

echo "<h3>Xdebug profiler test</h3>";
test3();

// stop profiler
$data = xhprof_disable();

include_once "path/to/xhprof_lib/utils/xhprof_lib.php";
include_once "path/to/xhprof_lib/utils/xhprof_runs.php";
$xhprof_runs = new XHProfRuns_Default();
$run_id = $xhprof_runs->save_run($data,'test');
var_dump($run_id);

// 访问 http://119.29.149.18/www/xhprof_html/?run=570d00e3007a0&source=test

5 使用php扩展taint(防止xss,sql inject)

+ https://github.com/laruence/taint

6. SocketLog -- 微信调试、API调试和AJAX的调试的工具

+ https://github.com/luofei614/SocketLog

Swoole入门到实战打造高性能赛事直播平台

PHP7.2源码编译

安装步骤
1. 解压
2. ./configure --prefix=/usr/local/php
3. make && make install

编辑~/.bash_profile 增加
alias php7=/usr/local/php/bin/php
运行 source ~/.bash_profile

查看php.ini默认路径
php --ini # ./configure –with-config-file-path=/usr/local/php/etc (编译时指定)

客户端和服务端

tcp服务端

<?php

// 构建server对象
$serv = new swoole_server('127.0.0.1', 9501);

// 设置运行参数
$serv->set(array(
    'worker_num' => 4,
//    'daemonize' => true,
    'backlog' => 128,
));

// 注册事件回调

/**
 * $fd: 客户端唯一标识
 * $reactor_id: 处理线程id
 */
$serv->on('connect', function ($serv, $fd, $reactor_id) {
        echo "client connect: {$fd}";
});

$serv->on('receive', function ($serv, $fd, $reactor_id, $data) {
        $serv->send($fd, 'Swoole: '.$data);
});

$serv->on('close', function ($serv, $fd) {
        echo "Client: Close.\n";
});

// 启动
$serv->start();

tcp客户端

<?php

// 构建客户端对象
$client = new swoole_client(SWOOLE_SOCK_TCP);

if (!$client->connect('127.0.0.1', 9501)) {
        exit('链接失败');
}

fwrite(STDOUT, "请输入\n");

while ($msg = trim(fgets(STDIN))) {
        $client->send($msg);
        $result = $client->recv();
        echo $result."\n";
}

http服务端

<?php

// 构建http_server对象

$http = new swoole_http_server('127.0.0.1', 9501);

// 设置静态资源目录
$http->set(array(
        'enable_static_handler' => true,
        'document_root' => '/path/to/document_root'
));


$http->on('request', function ($request, $response) {
        $response->end("收到浏览器信息:".json_encode($request->get));
});

$http->start();

websocket服务端

<?php
class Ws
{
    public function __construct() {
        $this->server = new swoole_websocket_server("0.0.0.0", 9501);
        $server->on('open', array($this, 'open'));
        $server->on('message', array($this, 'message'));
        $server->on('close', array($this, 'close'));
        // 注册任务处理事件
        $server->on('task', array($this, 'task'));
        $server->on('finih', array($this, 'finih'));
        $server->set(array(
            'task_worker_num' => 4
        ));
        $this->server->start();
    }

    public function open(swoole_websocket_server $server, $request) {
        echo "server: handshake success with fd{$request->fd}\n";
    }

    public function message(swoole_websocket_server $server, $frame) {
        // 投递任务处理
        $this->server->task(array('name' => 'zhorz', 'td' => $frame->td));
        echo "receive from {$frame->fd}:{$frame->data},opcode:{$frame->opcode},fin:{$frame->finish}\n";
        $server->push($frame->fd, "this is server");
    }

    public function close($ser, $fd) {
        echo "client {$fd} closed\n";
    }

    public function task(swoole_server $serv, int $task_id, int $src_worker_id, mixed $data) {
        // 处理耗时任务
        // $task_id和$src_worker_id组合起来全局唯一
    }

    public function finish(swoole_server $serv, int $task_id, string $data) {
        // 当worker进程投递的任务在task_worker中完成时,task进程会通过swoole_server->finish()方法将任务处理的结果发送给worker进程
    }
}

websocket客户端

// 协议标识
ws://example.com:80/some/path

// js例子
var ws = new WebSocket("wss://echo.websocket.org");

ws.onopen = function(evt) { 
  console.log("Connection open ..."); 
  ws.send("Hello WebSockets!");
};

ws.onmessage = function(evt) {
  console.log( "Received Message: " + evt.data);
  ws.close();
};

ws.onclose = function(evt) {
  console.log("Connection closed.");
};  

异步非阻塞IO场景

swoole毫秒定时器

int swoole_timer_tick(int $ms, callable $callback, mixed $user_param);
int swoole_timer_after(int $after_time_ms, mixed $callback_function, mixed $user_param);
bool swoole_timer_clear(int $timer_id)

异步文件IO读取文件

// 异步读取文件内容(不能用于大文件的读取, 最大可读取4M的文件)
swoole_async_readfile(__DIR__."/server.php", function($filename, $content) {
     echo "$filename: $content"; 
});

// 异步写文件(最大可写入4M)
swoole_async_writefile('test.log', $file_content, function($filename) {
    echo "wirte ok.\n";
}, $flags = 0);

// 异步读文件(支持大文件读取, 每次只读$size个字节,不会占用太多内存, 在读完后会自动回调$callback函数)
bool swoole_async_read(string $filename, mixed $callback, int $size = 8192, int $offset = 0);
// 回调函数接受2个参数:bool callback(string $filename, string $content);
// ($filename,文件名称; $content,读取到的分段内容,如果内容为空,表明文件已读完)
bool callback(string $filename, string $content);

// 异步写文件, 通过传入的offset参数来确定写入的位置
bool swoole_async_write(string $filename, string $content, int $offset = -1, mixed $callback = NULL);

异步mysql

$db = new swoole_mysql;
$server = array(
    'host' => '192.168.56.102',
    'port' => 3306,
    'user' => 'test',
    'password' => 'test',
    'database' => 'test',
    'charset' => 'utf8', //指定字符集
    'timeout' => 2,  // 可选:连接超时时间(非查询超时时间),默认为SW_MYSQL_CONNECT_TIMEOUT(1.0)
);

$db->connect($server, function ($db, $r) {
    if ($r === false) {
        var_dump($db->connect_errno, $db->connect_error);
        die;
    }
    $sql = 'show tables';
    $db->query($sql, function(swoole_mysql $db, $r) {
        if ($r === false)
        {
            var_dump($db->error, $db->errno);
        }
        elseif ($r === true )
        {
            var_dump($db->affected_rows, $db->insert_id);
        }
        var_dump($r);
        $db->close();
    });
});

异步redis

// 安装redis
// 安装hiredis库
// 重新编译swoole支持redis --enable-anysc-redis
// ./configure -> make clean -> make && make install -> 查看是否成功: php --ri swoole (查看扩展的版本信息)

php命令行查看信息:

  --rf <name>      Show information about function <name>.
  --rc <name>      Show information about class <name>.
  --re <name>      Show information about extension <name>.
  --rz <name>      Show information about Zend extension <name>.
  --ri <name>      Show configuration for extension <name>.

swoole process

linux下检测进程是否存在

使用"kill -0 pid",kill -0不会向进程发送任何信号,但是会进行错误检查。如果进程存在,命令返回0,如果不存在返回1。
对于普通用户来说只能用于检查自己的进程,因为向其它用户的进程发送信号会因为没有权限而出错,返回值也是1

wait如何处理多个子进程

https://blog.csdn.net/sl1248/article/details/50936823
调用一次wait只能回收一个子进程
1. 父进程while循环
2. 父进程注册SIGCHLD信号处理

swoole_table内存共享(解决多进程/多线程数据共享和同步加锁问)

$table = new swoole_table(1024); // $size行数
$table->column('data',Table::TYPE_STRING, 1); // 增加一列
$table->create();// 申请内存创建表
$table->set('zhorz', array('data' => 'A')); // 设置行数据
$table->get('zhorz', 'data'); // 获取某行某字段值
$table->del('zhorz');// 删除行数据

swoole协程Swoole\Coroutine*

// 创建协程
go(function () {
    co::sleep(0.5);
    echo "hello";
});
go("test");
go([$object, "method"]);
// 通道操作
$c = new chan(1);
$c->push($data);
$c->pop();
// 协程客户端
$redis = new Co\Redis;
$mysql = new Co\MySQL;
$http = new Co\Http\Client;
$tcp = new Co\Client;
$http2 = new Co\Http2\Client;
// 协程的让出和恢复
use Swoole\Coroutine as co;
$id = go(function(){
    $id = co::getUid();
    echo "start coro $id\n";
    co::suspend();
    echo "resume coro $id @1\n";
    co::suspend();
    echo "resume coro $id @2\n";
});
echo "start to resume $id @1\n";
co::resume($id);
echo "start to resume $id @2\n";
co::resume($id);
echo "main\n";

协程之channel

// 协程间同步
use Swoole\Coroutine as co;
$chan = new co\Channel(1);
co::create(function () use ($chan) {
    for($i = 0; $i < 100000; $i++) {
        co::sleep(1.0);
        $chan->push(['rand' => rand(1000, 9999), 'index' => $i]);
        echo "$i\n";
    }
});
co::create(function () use ($chan) {
    while(1) {
        $data = $chan->pop();
        var_dump($data);
    }
});
swoole_event::wait();

协程之select

$c1 = new chan();
$c2 = new chan();
function fibonacci($c1, $c2)
{
    go(function () use ($c1, $c2) {
        $a = 0;
        $b = 1;
        while(1) {
            $read_list = [$c2];
            $write_list = [$c1];
            $result = chan::select($read_list, $write_list, 2);
            // 当$read或$write数组中有部分channel对象处于可读或可写状态,select会立即返回
            if ($write_list) {
                $t = $a + $b;
                $a = $b;
                $b = $t;
                $c1->push($a);
            }
            if ($read_list) {
                $ret = $c2->pop();
                if ($ret === 1) {
                    return 1;
                }
            }
        }
    });
}
$num = 10;
go(function () use ($c1, $c2, $num) {
    for ($i = 0; $i < $num; $i ++) {
        $ret = $c1->pop();
        echo "fibonacci @$i $ret\n";
    }
    $c2->push(1);
});    
fibonacci($c1, $c2);

协程并发请求的列子

$serv = new \swoole_http_server("127.0.0.1", 9503, SWOOLE_BASE);

$serv->on('request', function ($req, $resp) {
    $chan = new chan(2);
    go(function () use ($chan) {
        $cli = new Swoole\Coroutine\Http\Client('www.qq.com', 80);
            $cli->set(['timeout' => 10]);
            $cli->setHeaders([
            'Host' => "www.qq.com",
            "User-Agent" => 'Chrome/49.0.2587.3',
            'Accept' => 'text/html,application/xhtml+xml,application/xml',
            'Accept-Encoding' => 'gzip',
        ]);
        $ret = $cli->get('/');
        $chan->push(['www.qq.com' => $cli->body]);
    });

    go(function () use ($chan) {
        $cli = new Swoole\Coroutine\Http\Client('www.163.com', 80);
        $cli->set(['timeout' => 10]);
        $cli->setHeaders([
            'Host' => "www.163.com",
            "User-Agent" => 'Chrome/49.0.2587.3',
            'Accept' => 'text/html,application/xhtml+xml,application/xml',
            'Accept-Encoding' => 'gzip',
        ]);
        $ret = $cli->get('/');
        $chan->push(['www.163.com' => $cli->body]);
    });

    $result = [];
    for ($i = 0; $i < 2; $i++)
    {
        $result += $chan->pop();
    }
    $resp->end(json_encode($result));
});
$serv->start();

系统服务监控和优化

# 调用linux命令监听
netstat -anp 2>/dev/null | grep 8811 | grep LISTEN | wc -l

# 搜索文件关键词
find . -type f -name '*.php' | xargs grep 'echo 111'

# 平滑重启服务
swoole_set_process_name -> 设置进程名称
pidof 进程名称 -> 获取进程id
kill -USR1 `pidof 进程名称` -> 平滑重启??

# nginx负载均衡说明
http://tengine.taobao.org/nginx_docs/cn/docs/http/ngx_http_upstream_module.html

PHP中socket的使用

SOCKET编程

简单的TCP/IP服务器

class SocketServer 
{
    private $ip = '';
    private $port = '';
    private $err_msg = '';
    public function __construct($ip,$port)
    {
        $this->ip = $ip;
        $this->port = $port;
        $this->create_socket();
    }

    protected function create_socket()
    {
        // 三个返回socket资源的方法 : socket_create socket_accept socket_create_listen
        
        $sock = socket_create(AF_INET,SOCK_STREAM,getprotobyname('tcp'));
        if ($sock === false) {
            $this->fail();
        }

        // 绑定ip,port
        $retval = socket_bind($sock,$this->ip,$this->port);
        if ($retval === false) {
            $this->fail();
        }

        // 监听端口
        $retval = socket_listen($sock);
        if ($retval === false) {
            $this->fail();
        }     

        while (true) {
            // socket_accept会阻塞直至有连接或失败才返回
            if (($msg_sock = socket_accept($sock)) === false) {
                break;
            }
            if (!$msg_sock) {
                echo "有客户端链接\n";
            }
            $msg = "\nwelcome\n";
            socket_write($msg_sock,$msg,strlen($msg));

            // 循环判断socket是否有新的数据,没数据返回""
            while (true) {
                if (($msg = socket_read($msg_sock,1024,PHP_NORMAL_READ)) === false) {
                    break 2; // 跳出两层循环
                }
                // trim 去取 \n \r \0 空格..
                if (!$msg = trim($msg)) {
                    continue; // 空数据
                }
                if ($msg == 'quit') {
                    break;
                } elseif ($msg == 'shutdown') {
                    socket_close($msg_sock); 
                    break 2; // 跳出两层循环                   
                } else {
                    $retval = "\nYou just say " . $msg . "\n";
                    socket_write($msg_sock,$retval,strlen($retval));   
                    echo 'server receive msg : ' . $msg . "\n";           
                }
            }
            socket_close($msg_sock);
        }
        socket_close($sock);
    }

    protected function fail()
    {
        $err_no = socket_last_error();
        $this->err_msg = socket_strerror($err_no);
        echo "\n" . $this->err_msg ."\n";
        exit;
    }
}

set_time_limit(0);
$ip = "0.0.0.0";
$port = 8000;
$socket = new SocketServer($ip,$port);

简单的TCP/IP客户端

class SocketClient 
{
    private $ip = '';
    private $port = '';
    public function __construct($ip,$port,$uri)
    {
        $this->ip = $ip;
        $this->port = $port;
        $this->uri = $uri;
        $this->connect();
    }   

    protected function connect()
    {
        $sock = socket_create(AF_INET,SOCK_STREAM,getprotobyname('tcp'));
        $retval = socket_connect($sock,$this->ip,$this->port);
        if ($retval === false) {
            $this->fail();
        }
        // 写
        $in  =  "HEAD {$this->uri} HTTP/1.1\r\n";
        $in  .=  "Host: {$this->ip}\r\n";
        $in  .=  "Connection: Close\r\n\r\n"; // 注意两个\r\n表示结束
        socket_write($sock,$in,strlen($in));
        // 读
        $out = '';
        while ($retval = socket_read($sock,1024)) {
            $out .= $retval;
        }
        echo $out;
        socket_close($sock);
    } 

    protected function fail()
    {
        $err_no = socket_last_error();
        $this->err_msg = socket_strerror($err_no);
        echo "\n" . $this->err_msg ."\n";
        exit;
    }
}

$ip = "119.29.149.18";
$port = 80;
$uri = "/contract/";
$socket = new SocketClient($ip,$port,$uri);

PHP发送邮件

smtp原理

+ smtp邮件协议 : http://www.ietf.org/rfc/rfc2821.txt
+ internet邮件协议 : http://www.ietf.org/rfc/rfc2822.txt
+ 代码参考 : http://blog.csdn.net/kerry0071/article/details/28604267
+ 代码参考 : http://chenall.net/post/cs_smtp/
+ http1.1协议: http://www.ietf.org/rfc/rfc2616.txt
+ 在Internet邮件头非ASCII文本表示 : http://www.ietf.org/rfc/rfc1342.txt

简易smtp实现类

<?php

/*
    简易的SMTP发送邮件类,代码比较少,有助于学习SMTP协议,
    可以带附件,支持需要验证的SMTP服务器(目前的SMTP基本都需要验证)
    编写: chenall
    时间: 2012-12-04
    网址: http://chenall.net/post/cs_smtp/
    修订记录:
        2012-12-08
            添加AddURL函数,可以直接从某个网址上下载文件并作为附件发送。
            修正由于发送人和接收人邮件地址没有使用"<>"126邮箱SMTP无法使用的问题。
        2012-12-06
            添加reset函数,重置连接,这样可以发送多个邮件。
        2012-12-05
           发送附件的代码整合到send函数中,减少变量的使用,快速输出,节省内存占用;
        2012-12-04
           第一个版本

    使用方法:

        1. 初始化:连接到服务器(默认是QQ邮箱)
           $mail = new cs_smtp('smtp.qq.com',25)
           if ($mail->errstr) //如果连接出错
               die($mail->errstr;
        2. 登录到服务器验证,如果失败返回FALSE;
           if (!$mail->login('USERNAME','PASSWORD'))
                die($mail->errstr;
        3. 添加附件如果不指定name自动从指定的文件中取文件名
           $mail->AddFile($file,$name) //服务器上的文件,可以指定文件名;
        4. 发送邮件
            $mail->send($to,$subject,$body)
            $to 收件人,多个使用','分隔
            $subject 邮件主题,可选。
            $body  邮件主体内容,可选
*/
class cs_smtp
{
    private $CRLF = "\r\n";
    private $from;
    private $smtp = null;
    private $attach = array();
    public $debug = true;//调试开关
    public $errstr = '';

    function __construct($host='smtp.qq.com',$port = 25) {
        if (empty($host))
            die('SMTP服务器未指定!');
        $this->smtp = fsockopen($host,$port,$errno,$errstr,5);
        if (empty($this->smtp))
        {
            $this->errstr = '错误'.$errno.':'.$errstr;
            return;
        }
        $this->smtp_log(fread($this->smtp, 515));
        if (intval($this->smtp_cmd('EHLO '.$host)) != 250 && intval($this->smtp_cmd('HELO '.$host)))
            return $this->errstr = '服务器不支持!';
        $this->errstr = '';
    }

    private function AttachURL($url,$name)
    {
        $info = parse_url($url);
        isset($info['port']) || $info['port'] = 80;
        isset($info['path']) || $info['path'] = '/';
        isset($info['query']) || $info['query'] = '';
        $down = fsockopen($info['host'],$info['port'],$errno,$errstr,5);
        if (!$down)
            return false;
        $out = "GET ".$info['path'].'?'.$info['query']." HTTP/1.1\r\n";
        $out .="Host: ".$info['host']."\r\n";
        $out .= "Connection: Close\r\n\r\n";// 头结束
        fwrite($down, $out);
        $filesize = 0;
        while (!feof($down)) {
            $a = fgets($down,515);
            if ($a == "\r\n")  /* fgets 读取一行遇到换行符"\n"结束, "\r"是回车 */
                break;
            $a = explode(':',$a);
            if (strcasecmp($a[0],'Content-Length') == 0)
                $filesize = intval($a[1]);
        }
        $sendsize = 0;
        echo "TotalSize: ".$filesize."\r\n";
        $i = 0;
        // 读取body
        while (!feof($down)) {
            $data = fread($down,0x2000);
            $sendsize += strlen($data);
            if ($filesize)
            {
                echo "$i Send:".$sendsize."\r";
                ob_flush();
                flush();
            }
            ++$i;
            // 使用 RFC 2045 语义格式化 $data
            fwrite($this->smtp,chunk_split(base64_encode($data)));
        }
        echo "\r\n";
        fclose($down);
        return ($filesize>0)?$filesize==$sendsize:true; // content-length 的大小 是否等于 读取body的大小判断是否成功
    }

    function __destruct()
    {
        if ($this->smtp)
            $this->smtp_cmd('QUIT');//发送退出命令
    }

    private function smtp_log($msg)//即时输出调试使用
    {
        if ($this->debug == false)
            return;
        echo $msg."\r\n";
        ob_flush();
        flush();
    }

    function reset()
    {
        $this->attach = null;
        $this->smtp_cmd('RSET');
    }

    function smtp_cmd($msg)//SMTP命令发送和收收
    {
        fputs($this->smtp,$msg.$this->CRLF);// \r\n 结束输入
        $this->smtp_log('SEND:'. substr($msg,0,80));
        $res = fread($this->smtp, 515);
        $this->smtp_log($res);
        $this->errstr = $res;
        return $res;
    }

    function AddURL($url,$name)
    {
        $this->attach[$name] = $url;
    }

    function AddFile($file,$name = '')//添加文件附件
    {
        if (file_exists($file))
        {
            if (!empty($name))
                return $this->attach[$name] = $file;
            $fn = pathinfo($file);
            return $this->attach[$fn['basename']] = $file;
        }
        return false;
    }

    function send($to,$subject='',$body = '')
    {
        $this->smtp_cmd("MAIL FROM: <".$this->from.'>');
        $mailto = explode(',',$to);
        foreach($mailto as $email_to)
            $this->smtp_cmd("RCPT TO: <".$email_to.">");
        if (intval($this->smtp_cmd("DATA")) != 354)//正确的返回必须是354
            return false;
        fwrite($this->smtp,"To:$to\nFrom: ".$this->from."\nSubject: $subject\n");

        $boundary = uniqid("--BY_CHENALL_",true);
        $headers = "MIME-Version: 1.0".$this->CRLF;
        $headers .= "From: <".$this->from.">".$this->CRLF;
        $headers .= "Content-type: multipart/mixed; boundary= $boundary\n\n".$this->CRLF;//headers结束要至少两个换行
        fwrite($this->smtp,$headers);

        $msg = "--$boundary\nContent-Type: text/html;charset=\"ISO-8859-1\"\nContent-Transfer-Encoding: base64\n\n";
        $msg .= chunk_split(base64_encode($body));
        // base64_encode设计此种编码是为了使二进制数据可以通过非纯 8-bit 的传输层传输,例如电子邮件的主体
        fwrite($this->smtp,$msg);
        $files = '';
        $errinfo = '';
        foreach($this->attach as $name=>$file)
        {
            $files .= $name;
            $msg = "--$boundary\n--$boundary\n";
            $msg .= "Content-Type: application/octet-stream; name=\"".$name."\"\n";
            $msg .= "Content-Disposition: attachment; filename=\"".$name."\"\n";
            $msg .= "Content-transfer-encoding: base64\n\n";
            fwrite($this->smtp,$msg);
            if (substr($file,4,1) == ':')//URL like http:///file://
            {
                if (!$this->AttachURL($file,$name))
                    $errinfo .= '文件下载错误:'.$name.",文件可能是错误的\r\n$file";
            }
            else
                fwrite($this->smtp,chunk_split(base64_encode(file_get_contents($file))));//使用BASE64编码,再用chunk_split大卸八块(每行76个字符)
        }
        if (!empty($errinfo))
        {
            $msg = "--$boundary\n--$boundary\n";
            $msg .= "Content-Type: application/octet-stream; name=Error.log\n";
            $msg .= "Content-Disposition: attachment; filename=Error.log\n";
            $msg .= "Content-transfer-encoding: base64\n\n";
            fwrite($this->smtp,$msg);
            fwrite($this->smtp,chunk_split(base64_encode($errinfo)));
        }
        return intval($this->smtp_cmd("--$boundary--\n\r\n.")) == 250;//结束DATA发送,服务器会返回执行结果,如果代码不是250则出错。

        // \r\n.\r\n 表示发送内容结束
    }

    function login($su,$sp)
    {
        if (empty($this->smtp))
            return false;
        $res = $this->smtp_cmd("AUTH LOGIN");
        if (intval($res)>400)
            return !$this->errstr = $res;
        $res = $this->smtp_cmd(base64_encode($su));
        if (intval($res)>400)
            return !$this->errstr = $res;
        $res = $this->smtp_cmd(base64_encode($sp));
        if (intval($res)>400)
            return !$this->errstr = $res;
        $this->from = $su;
        return true;
    }
}

mail+stmp函数

<?
!defined('M_COM') && exit('No Permisson');

function sys_mail(&$to,&$subject,&$msg,&$from){
     
    global $mail_smtp,$mail_mode,$mail_port,$mail_auth,$mail_from,$mail_user,$mail_pwd,$mail_delimiter,$mail_silent,$adminemail,$mcharset,$cmsname;

    if($mail_silent) 
        error_reporting(0);

    $delimiter = $mail_delimiter == 1 ? "\r\n" : ($mail_delimiter == 2 ? "\r" : "\n");

    $user = isset($mail_user) ? $mail_user : 1;

    $subject = '=?'.$mcharset.'?B?'.base64_encode(str_replace(array("\r","\n"),'','['.$cmsname.'] '.$subject)).'?=';

    $msg = chunk_split(base64_encode(str_replace(array("\n\r","\r\n","\r","\n","\r\n.",),array("\r","\n","\n","\r\n"," \r\n..",),$msg)));

    /*
        1. 在Internet邮件头非ASCII文本表示 : http://www.ietf.org/rfc/rfc1342.txt

        2. encoded-word := "=?" charset "?" encoding "?" encoded-text "?="
        
        3. 具体解析:

            encoded-word = "=" "?" charset "?" encoding "?" encoded-text "?" "="
            
            charset      = token    ; legal charsets defined by RFC 1341
            
            encoding     = token   ; Either "B" or "Q"

            token        = 1*<Any CHAR except SPACE, CTLs, and tspecials>

            tspecials    = "(" / ")" / "<" / ">" / "@" / "," / ";" / ":" / "\" / <"> / "/" / "[" / "]" / "?" / "." / "="

            encoded-text = 1*<Any printable ASCII character other than "?" or
                          ; SPACE> (but see "Use of encoded-words in message
                          ; headers", below)
     */
    
    /*
        Examples

            From: =?US-ASCII?Q?Keith_Moore?= <moore@cs.utk.edu>
            To: =?ISO-8859-1?Q?Keld_J=F8rn_Simonsen?= <keld@dkuug.dk>
            CC: =?ISO-8859-1?Q?Andr=E9_?= Pirard <PIRARD@vm1.ulg.ac.be>
            Subject: =?ISO-8859-1?B?SWYgeW91IGNhbiByZWFkIHRoaXMgeW8=?=
            =?ISO-8859-2?B?dSB1bmRlcnN0YW5kIHRoZSBleGFtcGxlLg==?=

            From: =?ISO-8859-1?Q?Olle_J=E4rnefors?= <ojarnef@admin.kth.se>
            To: ietf-822@dimacs.rutgers.edu, ojarnef@admin.kth.se
            Subject: Time for ISO 10646?

            To: Dave Crocker <dcrocker@mordor.stanford.edu>
            Cc: ietf-822@dimacs.rutgers.edu, paf@comsol.se
            From: =?ISO-8859-1?Q?Patrik_F=E4ltstr=F6m?= <paf@nada.kth.se>
            Subject: Re: RFC-HDR care and feeding    
     */
    
    $from = $from == '' ? '=?'.$mcharset.'?B?'.base64_encode($cmsname)."?= <$adminemail>" : (preg_match('/^(.+?)\<(.+?)\>$/',$from, $v) ? '=?'.$mcharset.'?B?'.base64_encode($v[1])."?= <$v[2]>" : $from);

    $toarr = array();

    foreach(explode(',',$to) as $k) 
        $toarr[] = preg_match('/^(.+?)\<(.+?)\>$/',$k,$v) ? ($user ? '=?'.$mcharset.'?B?'.base64_encode($v[1])."?= <$v[2]>" : $v[2]) : $k;

    $to = implode(',',$toarr);
    $headers = "From: $from{$delimiter}X-Priority: 3{$delimiter}X-Mailer: 08CMS{$delimiter}MIME-Version: 1.0{$delimiter}Content-type: text/html; charset=$mcharset{$delimiter}Content-Transfer-Encoding: base64{$delimiter}";

    $mail_port = $mail_port ? $mail_port : 25;

    if($mail_mode == 1 && function_exists('mail')){
        // 使用mail函数发送
        @mail($to,$subject,$msg,$headers);
     
    }elseif($mail_mode == 2){
        // 使用smtp协议+fsockopen发送
    
        if(!$fp = fsockopen($mail_smtp, $mail_port,$errno,$errstr,30)) 
            return "($mail_smtp:$mail_port) CONNECT - Unable to connect to the SMTP server";

        stream_set_blocking($fp,true);
         
        $nmsg = fgets($fp,512);
        if(substr($nmsg,0,3) != '220') // 220 <domain> Service ready 
            return "$mail_smtp:$mail_port CONNECT - $nmsg";
        
        fputs($fp, ($mail_auth ? 'EHLO' : 'HELO')." 08CMS\r\n");
        $nmsg = fgets($fp,512);
        if(substr($nmsg,0,3) != 220 && substr($nmsg,0,3) != 250) // 250 Requested mail action okay, completed
            return "($mail_smtp:$mail_port) HELO/EHLO - $nmsg";
         
        while(1){
            if(substr($nmsg,3,1) != '-' || empty($nmsg))
                break;
            $nmsg = fgets($fp,512);
        }
         
        if($mail_auth) {
         
            fputs($fp,"AUTH LOGIN\r\n");
            $nmsg = fgets($fp,512);
            if(substr($nmsg, 0, 3) != 334)
                return "($mail_smtp:$mail_port) AUTH LOGIN - $nmsg";
             
            fputs($fp,base64_encode($mail_user)."\r\n");
            $nmsg = fgets($fp,512);
            if(substr($nmsg,0,3) != 334) 
                return "($mail_smtp:$mail_port) USERNAME - $nmsg";
             
            fputs($fp,base64_encode($mail_pwd)."\r\n");
            $nmsg = fgets($fp,512);
            if(substr($nmsg,0,3) != 235) 
                return "($mail_smtp:$mail_port) PASSWORD - $nmsg";
            $from = $mail_from;
        
        }
         
        fputs($fp,"MAIL FROM: <".preg_replace("/.*\<(.+?)\>.*/","\\1",$from).">\r\n");
        $nmsg = fgets($fp,512);
        if(substr($nmsg,0,3) != 250) {
            // 重试了一下?
            fputs($fp,"MAIL FROM: <".preg_replace("/.*\<(.+?)\>.*/","\\1",$from).">\r\n");
            $nmsg = fgets($fp,512);
            if(substr($nmsg,0,3) != 250) 
                return "($mail_smtp:$mail_port) MAIL FROM - $nmsg";
        }
         
        foreach(explode(',',$to) as $v){
            $v = trim($v);
            if($v){
                fputs($fp,"RCPT TO: <".preg_replace("/.*\<(.+?)\>.*/", "\\1", $v).">\r\n");
                $nmsg = fgets($fp,512);
                // 重试了一下?
                if(substr($nmsg,0,3) != 250){
                    fputs($fp, "RCPT TO: <".preg_replace("/.*\<(.+?)\>.*/","\\1",$v).">\r\n");
                    $nmsg = fgets($fp,512);
                    return "($mail_smtp:$mail_port) RCPT TO - $nmsg";
                }
            }
        }
        
        // 邮件内容
        fputs($fp,"DATA\r\n");
        $nmsg = fgets($fp,512);
        // 354 Start mail input; end with <CRLF>.<CRLF>
        if(substr($nmsg,0,3) != 354) 
            return "($mail_smtp:$mail_port) DATA - $nmsg";

        $headers .= 'Message-ID: <'.gmdate('YmdHs').'.'.substr(md5($msg.microtime()),0,6).rand(100000, 999999).'@'.$_SERVER['HTTP_HOST'].">{$delimiter}";
        fputs($fp,"Date: ".gmdate('r')."\r\n");
        fputs($fp,"To: ".$to."\r\n");
        fputs($fp,"Subject: ".$subject."\r\n");
        fputs($fp,$headers."\r\n");
        fputs($fp,"\r\n\r\n");
        // header之后\r\n\r\n?

        fputs($fp,"$msg\r\n.\r\n"); // TODO 增加附件下载功能
        $nmsg = fgets($fp, 512);
        if(substr($nmsg, 0, 3) != 250) 
            return "($mail_smtp:$mail_port) END - $nmsg";
        fputs($fp, "QUIT\r\n");
     
    }elseif($mail_mode == 3){
     
        ini_set('SMTP',$mail_smtp);
        ini_set('smtp_port',$mail_port);
        ini_set('sendmail_from',$from);
        @mail($to,$subject,$msg,$headers);
    
    }
    return '';
}

ecshop mail + smtp 发送邮件

/**
 * 邮件发送
 *
 * @param: $name[string]        接收人姓名
 * @param: $email[string]       接收人邮件地址
 * @param: $subject[string]     邮件标题
 * @param: $content[string]     邮件内容
 * @param: $type[int]           0 普通邮件, 1 HTML邮件
 * @param: $notification[bool]  true 要求回执, false 不用回执
 *
 * @return boolean
 */
function send_mail($name, $email, $subject, $content, $type = 0, $notification=false)
{
    /* 如果邮件编码不是EC_CHARSET,创建字符集转换对象,转换编码 */
    if ($GLOBALS['_CFG']['mail_charset'] != EC_CHARSET)
    {
        $name      = ecs_iconv(EC_CHARSET, $GLOBALS['_CFG']['mail_charset'], $name);
        $subject   = ecs_iconv(EC_CHARSET, $GLOBALS['_CFG']['mail_charset'], $subject);
        $content   = ecs_iconv(EC_CHARSET, $GLOBALS['_CFG']['mail_charset'], $content);
        $shop_name = ecs_iconv(EC_CHARSET, $GLOBALS['_CFG']['mail_charset'], $GLOBALS['_CFG']['shop_name']);
    }
    $charset   = $GLOBALS['_CFG']['mail_charset'];
    /**
     * 使用mail函数发送邮件
     */
    if ($GLOBALS['_CFG']['mail_service'] == 0 && function_exists('mail'))
    {
        /* 邮件的头部信息 */
        $content_type = ($type == 0) ? 'Content-Type: text/plain; charset=' . $charset : 'Content-Type: text/html; charset=' . $charset;
        $headers = array();
        $headers[] = 'From: "' . '=?' . $charset . '?B?' . base64_encode($shop_name) . '?='.'" <' . $GLOBALS['_CFG']['smtp_mail'] . '>';
        $headers[] = $content_type . '; format=flowed';
        if ($notification)
        {
            $headers[] = 'Disposition-Notification-To: ' . '=?' . $charset . '?B?' . base64_encode($shop_name) . '?='.'" <' . $GLOBALS['_CFG']['smtp_mail'] . '>';
            // 回执可以确认对方是否收到该邮件
        }

        $res = @mail($email, '=?' . $charset . '?B?' . base64_encode($subject) . '?=', $content, implode("\r\n", $headers));

        if (!$res)
        {
            $GLOBALS['err'] ->add($GLOBALS['_LANG']['sendemail_false']);

            return false;
        }
        else
        {
            return true;
        }
    }
    /**
     * 使用smtp服务发送邮件
     */
    else
    {
        /* 邮件的头部信息 */
        $content_type = ($type == 0) ?
            'Content-Type: text/plain; charset=' . $charset : 'Content-Type: text/html; charset=' . $charset;
        $content   =  base64_encode($content);

        $headers = array();
        $headers[] = 'Date: ' . gmdate('D, j M Y H:i:s') . ' +0000';
        $headers[] = 'To: "' . '=?' . $charset . '?B?' . base64_encode($name) . '?=' . '" <' . $email. '>';
        $headers[] = 'From: "' . '=?' . $charset . '?B?' . base64_encode($shop_name) . '?='.'" <' . $GLOBALS['_CFG']['smtp_mail'] . '>';
        $headers[] = 'Subject: ' . '=?' . $charset . '?B?' . base64_encode($subject) . '?=';
        $headers[] = $content_type . '; format=flowed';
        $headers[] = 'Content-Transfer-Encoding: base64';
        $headers[] = 'Content-Disposition: inline';
        if ($notification)
        {
            $headers[] = 'Disposition-Notification-To: ' . '=?' . $charset . '?B?' . base64_encode($shop_name) . '?='.'" <' . $GLOBALS['_CFG']['smtp_mail'] . '>';
        }

        /* 获得邮件服务器的参数设置 */
        $params['host'] = $GLOBALS['_CFG']['smtp_host'];
        $params['port'] = $GLOBALS['_CFG']['smtp_port'];
        $params['user'] = $GLOBALS['_CFG']['smtp_user'];
        $params['pass'] = $GLOBALS['_CFG']['smtp_pass'];

        if (empty($params['host']) || empty($params['port']))
        {
            // 如果没有设置主机和端口直接返回 false
            $GLOBALS['err'] ->add($GLOBALS['_LANG']['smtp_setting_error']);

            return false;
        }
        else
        {
            // 发送邮件
            if (!function_exists('fsockopen'))
            {
                //如果fsockopen被禁用,直接返回
                $GLOBALS['err']->add($GLOBALS['_LANG']['disabled_fsockopen']);

                return false;
            }

            include_once(ROOT_PATH . 'includes/cls_smtp.php');
            static $smtp;

            $send_params['recipients'] = $email;
            $send_params['headers']    = $headers;
            $send_params['from']       = $GLOBALS['_CFG']['smtp_mail'];
            $send_params['body']       = $content;

            if (!isset($smtp))
            {
                $smtp = new smtp($params);
            }

            if ($smtp->connect() && $smtp->send($send_params))
            {
                return true;
            }
            else
            {
                $err_msg = $smtp->error_msg();
                if (empty($err_msg))
                {
                    $GLOBALS['err']->add('Unknown Error');
                }
                else
                {
                    if (strpos($err_msg, 'Failed to connect to server') !== false)
                    {
                        $GLOBALS['err']->add(sprintf($GLOBALS['_LANG']['smtp_connect_failure'], $params['host'] . ':' . $params['port']));
                    }
                    else if (strpos($err_msg, 'AUTH command failed') !== false)
                    {
                        $GLOBALS['err']->add($GLOBALS['_LANG']['smtp_login_failure']);
                    }
                    elseif (strpos($err_msg, 'bad sequence of commands') !== false)
                    {
                        $GLOBALS['err']->add($GLOBALS['_LANG']['smtp_refuse']);
                    }
                    else
                    {
                        $GLOBALS['err']->add($err_msg);
                    }
                }

                return false;
            }
        }
    }
}
<?php

/**
 * ECSHOP SMTP 邮件类
 * ============================================================================
 * * 版权所有 2005-2012 上海商派网络科技有限公司,并保留所有权利。
 * 网站地址: http://www.ecshop.com;
 * ----------------------------------------------------------------------------
 * 这不是一个自由软件!您只能在不用于商业目的的前提下对程序代码进行修改和
 * 使用;不允许对程序代码以任何形式任何目的的再发布。
 * ============================================================================
 * $Author: liubo $
 * $Id: cls_smtp.php 17217 2011-01-19 06:29:08Z liubo $
*/

if (!defined('IN_ECS'))
{
    die('Hacking attempt');
}

define('SMTP_STATUS_NOT_CONNECTED', 1, true);
define('SMTP_STATUS_CONNECTED',     2, true);

class smtp
{
    var $connection;
    var $recipients;
    var $headers;
    var $timeout;
    var $errors;
    var $status;
    var $body;
    var $from;
    var $host;
    var $port;
    var $helo;
    var $auth;
    var $user;
    var $pass;

    /**
     *  参数为一个数组
     *  host        SMTP 服务器的主机       默认:localhost
     *  port        SMTP 服务器的端口       默认:25
     *  helo        发送HELO命令的名称      默认:localhost
     *  user        SMTP 服务器的用户名     默认:空值
     *  pass        SMTP 服务器的登陆密码   默认:空值
     *  timeout     连接超时的时间          默认:5
     *  @return  bool
     */
    function smtp($params = array())
    {
        if (!defined('CRLF'))
        {
            define('CRLF', "\r\n", true);
        }

        $this->timeout  = 10;
        $this->status   = SMTP_STATUS_NOT_CONNECTED;
        $this->host     = 'localhost';
        $this->port     = 25;
        $this->auth     = false;
        $this->user     = '';
        $this->pass     = '';
        $this->errors   = array();

        foreach ($params AS $key => $value)
        {
            $this->$key = $value;
        }

        $this->helo     = $this->host;

        //  如果没有设置用户名则不验证
        $this->auth = ('' == $this->user) ? false : true;
    }

    function connect($params = array())
    {
        if (!isset($this->status))
        {
            $obj = new smtp($params);

            if ($obj->connect())
            {
                $obj->status = SMTP_STATUS_CONNECTED;
            }

            return $obj;
        }
        else
        {
            if (!empty($GLOBALS['_CFG']['smtp_ssl']))
            {
                // ssl协议链接
                $this->host = "ssl://" . $this->host;
            }
            $this->connection = @fsockopen($this->host, $this->port, $errno, $errstr, $this->timeout);

            if ($this->connection === false)
            {
                $this->errors[] = 'Access is denied.';

                return false;
            }

            @socket_set_timeout($this->connection, 0, 250000);

            $greeting = $this->get_data();

            if (is_resource($this->connection))
            {
                $this->status = 2;

                return $this->auth ? $this->ehlo() : $this->helo();
            }
            else
            {
                log_write($errstr, __FILE__, __LINE__);
                $this->errors[] = 'Failed to connect to server: ' . $errstr;

                return false;
            }
        }
    }

    /**
     * 参数为数组
     * recipients      接收人的数组
     * from            发件人的地址,也将作为回复地址
     * headers         头部信息的数组
     * body            邮件的主体
     */

    function send($params = array())
    {
        foreach ($params AS $key => $value)
        {
            $this->$key = $value;
        }

        if ($this->is_connected())
        {
            //  服务器是否需要验证
            if ($this->auth)
            {
                if (!$this->auth())
                {
                    return false;
                }
            }

            $this->mail($this->from);

            if (is_array($this->recipients))
            {
                foreach ($this->recipients AS $value)
                {
                    $this->rcpt($value);
                }
            }
            else
            {
                $this->rcpt($this->recipients);
            }

            if (!$this->data())
            {
                return false;
            }

            $headers = str_replace(CRLF . '.', CRLF . '..', trim(implode(CRLF, $this->headers)));
            $body    = str_replace(CRLF . '.', CRLF . '..', $this->body);
            $body    = substr($body, 0, 1) == '.' ? '.' . $body : $body;

            $this->send_data($headers);
            $this->send_data('');
            $this->send_data($body);
            $this->send_data('.');

            return (substr($this->get_data(), 0, 3) === '250');
        }
        else
        {
            $this->errors[] = 'Not connected!';

            return false;
        }
    }

    function helo()
    {
        if (is_resource($this->connection)
                AND $this->send_data('HELO ' . $this->helo)
                AND substr($error = $this->get_data(), 0, 3) === '250' )
        {
            return true;
        }
        else
        {
            $this->errors[] = 'HELO command failed, output: ' . trim(substr($error, 3));

            return false;
        }
    }

    function ehlo()
    {
        if (is_resource($this->connection)
                AND $this->send_data('EHLO ' . $this->helo)
                AND substr($error = $this->get_data(), 0, 3) === '250' )
        {
            return true;
        }
        else
        {
            $this->errors[] = 'EHLO command failed, output: ' . trim(substr($error, 3));

            return false;
        }
    }

    function auth()
    {
        if (is_resource($this->connection)
                AND $this->send_data('AUTH LOGIN')
                AND substr($error = $this->get_data(), 0, 3) === '334'
                AND $this->send_data(base64_encode($this->user))            // Send username
                AND substr($error = $this->get_data(),0,3) === '334'
                AND $this->send_data(base64_encode($this->pass))            // Send password
                AND substr($error = $this->get_data(),0,3) === '235' )
        {
            return true;
        }
        else
        {
            $this->errors[] = 'AUTH command failed: ' . trim(substr($error, 3));

            return false;
        }
    }

    function mail($from)
    {
        if ($this->is_connected()
            AND $this->send_data('MAIL FROM:<' . $from . '>')
            AND substr($this->get_data(), 0, 2) === '250' )
        {
            return true;
        }
        else
        {
            return false;
        }
    }

    function rcpt($to)
    {
        if ($this->is_connected()
            AND $this->send_data('RCPT TO:<' . $to . '>')
            AND substr($error = $this->get_data(), 0, 2) === '25')
        {
            return true;
        }
        else
        {
            $this->errors[] = trim(substr($error, 3));

            return false;
        }
    }

    function data()
    {
        if ($this->is_connected()
            AND $this->send_data('DATA')
            AND substr($error = $this->get_data(), 0, 3) === '354' )
        {
            return true;
        }
        else
        {
            $this->errors[] = trim(substr($error, 3));

            return false;
        }
    }

    function is_connected()
    {
        return (is_resource($this->connection) AND ($this->status === SMTP_STATUS_CONNECTED));
    }

    function send_data($data)
    {
        if (is_resource($this->connection))
        {
            return fwrite($this->connection, $data . CRLF, strlen($data) + 2);
        }
        else
        {
            return false;
        }
    }

    function get_data()
    {
        $return = '';
        $line   = '';

        if (is_resource($this->connection))
        {
            // 返回以\r\n结束 或 250 OK
            while (strpos($return, CRLF) === false OR $line{3} !== ' ')
            {
                $line    = fgets($this->connection, 512);
                $return .= $line;
            }

            return trim($return);
        }
        else
        {
            return '';
        }
    }

    /**
     * 获得最后一个错误信息
     *
     * @access  public
     * @return  string
     */
    function error_msg()
    {
        if (!empty($this->errors))
        {
            $len = count($this->errors) - 1;
            return $this->errors[$len];
        }
        else
        {
            return '';
        }
    }
}