PHP原生Socket实现SMTP邮件发送完整示例
在PHP开发中,有时需要脱离庞大的邮件库(如PHPMailer)直接底层控制邮件发送。本文提供基于PHP Socket扩展的SMTP客户端实现方案,通过手动构建TCP命令序列,兼容纯文本与HTML格式的邮件收发。
SMTP协议通信原理
使用Socket直接发送邮件的核心在于遵循SMTP(简单邮件传输协议)的标准交互流程。服务器监听指定端口(通常为25或加密协议的465/587),客户端建立连接后需按顺序执行握手、身份验证、信封定义和数据传输等指令。
主要交互步骤包括:
- HELO/EHLO:客户端向服务器标识自身主机名。
- AUTH LOGIN:进入登录模式,随后依次发送Base64编码后的用户名和密码以完成认证。
- MAIL FROM / RCPT TO:分别定义发件人和收件人地址。
- DATA:开始传输邮件头部(MIME版本、类型、主题)及正文内容。
- QUIT:结束会话并关闭连接。
环境准备
本方案依赖PHP内置的Sockets扩展。请在执行前确认您的服务器已启用该扩展:
- 打开
php.ini配置文件。 - 确保
extension=sockets行未被注释(无分号;开头)。 - 重启Web服务使配置生效。
- 检查函数
socket_create()是否存在:运行var_dump(function_exists('socket_create'));。
第1步:构建SMTP连接管理类
目的:封装Socket生命周期,管理连接状态并处理异常响应码。
该类包含属性初始化、连接建立、命令执行校验及资源释放方法。关键参数包括服务器地址、端口、账号密码及邮件元数据。
class MySendMail {
private $_userName;
private $_password;
protected $_sendServer;
protected $_port = 25;
protected $_from;
protected $_to;
protected $_subject;
protected $_body;
protected $_isHTML = false;
protected $_socket;
protected $_errorMessage;
// 构造函数用于接收初始化参数
public function __construct($from="", $to="", $subject="", $body="",
$server="", $username="", $password="", $isHTML="", $port="") {
if(!empty($from)) $this->_from = $from;
if(!empty($to)) $this->_to = $to;
if(!empty($subject)) $this->_subject = $subject;
if(!empty($body)) $this->_body = $body;
if(!empty($isHTML)) $this->_isHTML = $isHTML;
if(!empty($server)) $this->_sendServer = $server;
if(!empty($port)) $this->_port = $port;
if(!empty($username)) $this->_userName = $username;
if(!empty($password)) $this->_password = $password;
}
/**
* 设置SMTP服务器信息
*/
public function setServer($server, $port=25) {
if(!isset($server) || empty($server) || !is_string($server)) {
$this->_errorMessage = "Invalid server parameter";
return false;
}
if(!is_numeric($port)) {
$this->_errorMessage = "Invalid port parameter";
return false;
}
$this->_sendServer = $server;
$this->_port = $port;
return true;
}
/**
* 设置邮件内容及认证信息
*/
public function setMailInfo($config) {
if(!is_array($config) || count($config) < 6){
$this->_errorMessage = "Missing parameters";
return false;
}
$this->_from = $config['from'];
$this->_to = $config['to'];
$this->_subject = $config['subject'];
$this->_body = $config['body'];
$this->_userName = $config['username'];
$this->_password = $config['password'];
if(isset($config['isHTML'])){
$this->_isHTML = $config['isHTML'];
}
return true;
}
// ... (其他辅助方法见下文代码块)
}
第2步:实现Socket连接与命令分发
目的:建立TCP通道,读取初始 Banner,并解析后续响应。
使用 socket_create(AF_INET, SOCK_STREAM, getprotobyname('tcp')) 创建流式套接字,并通过 socket_connect() 连接目标SMTP服务器。连接成功后需读取服务器欢迎信息。
/**
* 建立网络连接
*/
private function socket() {
if(!function_exists("socket_create")) {
$this->_errorMessage = "Extension php-sockets must be enabled";
return false;
}
$this->_socket = socket_create(AF_INET, SOCK_STREAM, getprotobyname('tcp'));
if(!$this->_socket) {
$this->_errorMessage = socket_strerror(socket_last_error());
return false;
}
if(!socket_connect($this->_socket, $this->_sendServer, $this->_port)) {
$this->_errorMessage = socket_strerror(socket_last_error());
return false;
}
// 读取服务器Banner
socket_read($this->_socket, 1024);
return true;
}
第3步:发送命令并校验响应
目的:构造符合RFC规范的指令序列,并严格匹配服务器返回的状态码(如250表示成功,334表示等待输入)。
getCommand() 方法根据是否启用HTML模式生成邮件头部结构。若为HTML邮件,需添加 MIME-Version 和 Content-type: text/html; charset=utf-8 头。用户名和密码在发送前需进行 Base64 编码。
sendCommand() 负责单次指令的发送与校验。它对比实际收到的状态码是否与预期一致,不一致则记录错误。
/**
* 获取完整的SMTP命令序列
*/
protected function getCommand() {
// 构建邮件体头部
if($this->_isHTML) {
$mail = "MIME-Version:1.0\r\n";
$mail .= "Content-type:text/html;charset=utf-8\r\n";
$mail .= "FROM:test<" . $this->_from . ">\r\n";
$mail .= "TO:<" . $this->_to . ">\r\n";
$mail .= "Subject:" . $this->_subject ."\r\n\r\n";
$mail .= $this->_body . "\r\n.\r\n";
} else {
$mail = "FROM:test<" . $this->_from . ">\r\n";
$mail .= "TO:<" . $this->_to . ">\r\n";
$mail .= "Subject:" . $this->_subject ."\r\n\r\n";
$mail .= $this->_body . "\r\n.\r\n";
}
// 定义命令序列及期望响应码
$command = array(
array("HELO sendmail\r\n", 250),
array("AUTH LOGIN\r\n", 334),
array(base64_encode($this->_userName) . "\r\n", 334),
array(base64_encode($this->_password) . "\r\n", 235), // 235通常表示认证成功
array("MAIL FROM:<" . $this->_from . ">\r\n", 250),
array("RCPT TO:<" . $this->_to . ">\r\n", 250),
array("DATA\r\n", 354),
array($mail, 250),
array("QUIT\r\n", 221)
);
return $command;
}
/**
* 发送单条命令并验证返回值
*/
protected function sendCommand($command, $code) {
echo 'Send command:' . $command . ',expected code:' . $code . '<br />';
try{
if(socket_write($this->_socket, $command, strlen($command))){
$data = trim(socket_read($this->_socket, 1024));
echo 'response:' . $data . '<br /><br />';
if($data) {
$pattern = "/^".$code."/";
if(preg_match($pattern, $data)) {
return true;
} else{
$this->_errorMessage = "Error:" . $data . "|**| command:";
return false;
}
} else{
$this->_errorMessage = "Error:" . socket_strerror(socket_last_error());
return false;
}
} else{
$this->_errorMessage = "Error:" . socket_strerror(socket_last_error());
return false;
}
}catch(Exception $e) {
$this->_errorMessage = "Error:" . $e->getMessage();
}
}
public function close() {
if(isset($this->_socket) && is_object($this->_socket)) {
$this->_socket->close();
return true;
}
return false;
}
}
第4步:调用测试脚本
目的:实例化类并填入真实的SMTP服务器凭据进行测试。
请替换示例中的邮箱账号、密码及目标收件人为有效值。对于国内常用邮箱(如126、QQ、163),通常需要使用开启SMTP服务后生成的“授权码”作为密码。
$config = array(
"from" => "your_email@126.com",
"to" => "recipient@example.com",
"subject" => "Test via Socket",
"body" => "<b>Hello World</b>", // HTML内容
"username" => "your_username",
"password" => "your_auth_code",
"isHTML" => true
);
$mail = new MySendMail();
$mail->setServer("smtp.126.com"); // 修改为你的SMTP服务器
$mail->setMailInfo($config);
if(!$mail->sendMail()) {
echo "Send Failed: " . $mail->error();
} else {
echo 'Mail OK!';
}
常见问题排查
如果发送失败,请按以下逻辑检查:
- 端口连接超时:默认端口
25在许多云服务器上被运营商封堵。尝试改用465(SSL) 或587(TLS)。注意:原代码未实现STARTTLS或SSL封装,升级至加密协议需引入ssl://前缀并调整上下文。 - 认证失败 (535/550):检查用户名是否为完整邮箱地址;密码是否为第三方登录授权码而非登录密码。
- 语法错误:确保字符串拼接时使用正确的换行符
\r\n,部分老旧服务器对\n识别不佳。
总结
通过上述步骤,您可以完全掌握基于Socket的原生邮件发送机制。此方法轻量且透明,适合学习网络协议或在不允许安装Composer依赖的受限环境中部署。对于生产环境的大规模邮件服务,仍推荐使用成熟的库以更好地处理反垃圾邮件策略和多线程并发。
以上就是PHP基于Socket实现SMTP发送邮件的详细内容,更多关于PHP邮件开发的资料请关注本站其它相关文章!
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









