545 lines
16 KiB
PHP
545 lines
16 KiB
PHP
<?php
|
||
/**
|
||
* @copyright (C)2016-2099 Hnaoyun Inc.
|
||
* @author XingMeng
|
||
* @email hnxsh@foxmail.com
|
||
* @date 2018年3月21日
|
||
* 邮件发送类
|
||
*/
|
||
namespace core\basic;
|
||
|
||
class Smtp
|
||
{
|
||
|
||
// 邮件传输代理服务器地址
|
||
protected $sendServer;
|
||
|
||
// 邮件传输代理服务器端口
|
||
protected $port;
|
||
|
||
// 是否是安全连接
|
||
protected $isSecurity;
|
||
|
||
// 邮件传输代理用户名
|
||
protected $userName;
|
||
|
||
// 邮件传输代理密码
|
||
protected $password;
|
||
|
||
// 发件人
|
||
protected $from;
|
||
|
||
// 收件人
|
||
protected $to = array();
|
||
|
||
// 抄送
|
||
protected $cc = array();
|
||
|
||
// 秘密抄送
|
||
protected $bcc = array();
|
||
|
||
// 主题
|
||
protected $subject;
|
||
|
||
// 邮件正文
|
||
protected $body;
|
||
|
||
// 附件
|
||
protected $attachment = array();
|
||
|
||
// 调试模式
|
||
protected $debug;
|
||
|
||
// 错误信息
|
||
protected $errorMessage;
|
||
|
||
// 资源句柄
|
||
protected $socket;
|
||
|
||
/**
|
||
* 设置邮件传输代理,默认为安全链接
|
||
*
|
||
* @param string $server
|
||
* 代理服务器的ip或者域名
|
||
* @param string $username
|
||
* 认证账号
|
||
* @param string $password
|
||
* 认证密码
|
||
* @param int $port
|
||
* 代理服务器的端口,smtp默认25号端口
|
||
* @param boolean $isSecurity
|
||
* 到服务器的连接是否为安全连接,默认false
|
||
* @return boolean
|
||
*/
|
||
public function __construct($server = "", $username = "", $password = "", $port = 465, $isSecurity = true, $debug = false)
|
||
{
|
||
if ($server) {
|
||
$this->sendServer = $server;
|
||
$this->port = $port;
|
||
$this->isSecurity = $isSecurity ? true : false;
|
||
$this->debug = $debug;
|
||
$this->userName = empty($username) ? "" : base64_encode($username);
|
||
$this->password = empty($password) ? "" : base64_encode($password);
|
||
$this->from = $username;
|
||
} else {
|
||
$smtp = Config::get();
|
||
$this->sendServer = $smtp['smtp_server'];
|
||
$this->port = $smtp['smtp_port'];
|
||
$this->isSecurity = $smtp['smtp_ssl'] ? true : false;
|
||
$this->debug = $debug;
|
||
$this->userName = base64_encode($smtp['smtp_username']);
|
||
$this->password = base64_encode($smtp['smtp_password']);
|
||
$this->from = $smtp['smtp_username'];
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 设置收件人,多个收件人,调用多次或用逗号隔开.
|
||
*
|
||
* @param string $to
|
||
* 收件人地址
|
||
* @return boolean
|
||
*/
|
||
public function setReceiver($to)
|
||
{
|
||
if (strpos($to, ',')) {
|
||
$this->to = explode(',', $to);
|
||
} else {
|
||
$this->to[] = $to;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 设置抄送,多个抄送,调用多次或用逗号隔开.
|
||
*
|
||
* @param string $cc
|
||
* 抄送地址
|
||
* @return boolean
|
||
*/
|
||
public function setCc($cc)
|
||
{
|
||
if (strpos($cc, ',')) {
|
||
$this->cc = explode(',', $cc);
|
||
} else {
|
||
$this->cc[] = $cc;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 设置秘密抄送,多个秘密抄送,调用多次或用逗号隔开.
|
||
*
|
||
* @param string $bcc
|
||
* 秘密抄送地址
|
||
* @return boolean
|
||
*/
|
||
public function setBcc($bcc)
|
||
{
|
||
if (strpos($bcc, ',')) {
|
||
$this->bcc = explode(',', $bcc);
|
||
} else {
|
||
$this->bcc[] = $bcc;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 设置邮件附件,多个附件,调用多次
|
||
*
|
||
* @param string $file
|
||
* 文件地址
|
||
* @return boolean
|
||
*/
|
||
public function addAttachment($file)
|
||
{
|
||
if (! file_exists($file)) {
|
||
$this->errorMessage = "file " . $file . " does not exist.";
|
||
return false;
|
||
}
|
||
$this->attachment[] = $file;
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 设置邮件信息
|
||
*
|
||
* @param string $body
|
||
* 邮件主题
|
||
* @param string $subject
|
||
* 邮件主体内容,可以是纯文本,也可是是HTML文本
|
||
* @return boolean
|
||
*/
|
||
public function setMail($subject, $body)
|
||
{
|
||
$this->subject = base64_encode($subject);
|
||
$this->body = base64_encode($body);
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 发送邮件
|
||
*
|
||
* @return boolean
|
||
*/
|
||
public function sendMail($to = '', $subject = '', $body = '')
|
||
{
|
||
if ($to) {
|
||
$this->setReceiver($to);
|
||
$this->setMail($subject, $body);
|
||
}
|
||
$command = $this->getCommand();
|
||
if (! $this->socket($this->isSecurity)) {
|
||
return false;
|
||
}
|
||
foreach ($command as $value) {
|
||
$result = $this->sendCommand($value[0], $value[1]);
|
||
if ($result) {
|
||
continue;
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
// 关闭连接
|
||
$this->close();
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 返回错误信息
|
||
*
|
||
* @return string
|
||
*/
|
||
public function error()
|
||
{
|
||
if (! isset($this->errorMessage)) {
|
||
$this->errorMessage = "";
|
||
}
|
||
return $this->errorMessage;
|
||
}
|
||
|
||
/**
|
||
* 返回mail命令
|
||
*
|
||
* @access protected
|
||
* @return array
|
||
*/
|
||
protected function getCommand()
|
||
{
|
||
$separator = "----=_Part_" . md5($this->from . time()) . uniqid(); // 分隔符
|
||
$command = array(
|
||
array(
|
||
"HELO sendmail\r\n",
|
||
250
|
||
)
|
||
);
|
||
if (! empty($this->userName)) {
|
||
$command[] = array(
|
||
"AUTH LOGIN\r\n",
|
||
334
|
||
);
|
||
$command[] = array(
|
||
$this->userName . "\r\n",
|
||
334
|
||
);
|
||
$command[] = array(
|
||
$this->password . "\r\n",
|
||
235
|
||
);
|
||
}
|
||
// 设置发件人
|
||
$command[] = array(
|
||
"MAIL FROM: <" . $this->from . ">\r\n",
|
||
250
|
||
);
|
||
$header = "FROM: <" . $this->from . ">\r\n";
|
||
// 设置收件人
|
||
if (! empty($this->to)) {
|
||
$count = count($this->to);
|
||
if ($count == 1) {
|
||
$command[] = array(
|
||
"RCPT TO: <" . $this->to[0] . ">\r\n",
|
||
250
|
||
);
|
||
$header .= "TO: <" . $this->to[0] . ">\r\n";
|
||
} else {
|
||
for ($i = 0; $i < $count; $i ++) {
|
||
$command[] = array(
|
||
"RCPT TO: <" . $this->to[$i] . ">\r\n",
|
||
250
|
||
);
|
||
if ($i == 0) {
|
||
$header .= "TO: <" . $this->to[$i] . ">";
|
||
} elseif ($i + 1 == $count) {
|
||
$header .= ",<" . $this->to[$i] . ">\r\n";
|
||
} else {
|
||
$header .= ",<" . $this->to[$i] . ">";
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 设置抄送
|
||
if (! empty($this->cc)) {
|
||
$count = count($this->cc);
|
||
if ($count == 1) {
|
||
$command[] = array(
|
||
"RCPT TO: <" . $this->cc[0] . ">\r\n",
|
||
250
|
||
);
|
||
$header .= "CC: <" . $this->cc[0] . ">\r\n";
|
||
} else {
|
||
for ($i = 0; $i < $count; $i ++) {
|
||
$command[] = array(
|
||
"RCPT TO: <" . $this->cc[$i] . ">\r\n",
|
||
250
|
||
);
|
||
if ($i == 0) {
|
||
$header .= "CC: <" . $this->cc[$i] . ">";
|
||
} elseif ($i + 1 == $count) {
|
||
$header .= ",<" . $this->cc[$i] . ">\r\n";
|
||
} else {
|
||
$header .= ",<" . $this->cc[$i] . ">";
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 设置秘密抄送
|
||
if (! empty($this->bcc)) {
|
||
$count = count($this->bcc);
|
||
if ($count == 1) {
|
||
$command[] = array(
|
||
"RCPT TO: <" . $this->bcc[0] . ">\r\n",
|
||
250
|
||
);
|
||
$header .= "BCC: <" . $this->bcc[0] . ">\r\n";
|
||
} else {
|
||
for ($i = 0; $i < $count; $i ++) {
|
||
$command[] = array(
|
||
"RCPT TO: <" . $this->bcc[$i] . ">\r\n",
|
||
250
|
||
);
|
||
if ($i == 0) {
|
||
$header .= "BCC: <" . $this->bcc[$i] . ">";
|
||
} elseif ($i + 1 == $count) {
|
||
$header .= ",<" . $this->bcc[$i] . ">\r\n";
|
||
} else {
|
||
$header .= ",<" . $this->bcc[$i] . ">";
|
||
}
|
||
}
|
||
}
|
||
}
|
||
// 主题
|
||
$header .= "Subject: =?UTF-8?B?" . $this->subject . "?=\r\n";
|
||
if (isset($this->attachment)) {
|
||
// 含有附件的邮件头需要声明成这个
|
||
$header .= "Content-Type: multipart/mixed;\r\n";
|
||
} elseif (false) {
|
||
// 邮件体含有图片资源的,且包含的图片在邮件内部时声明成这个,如果是引用的远程图片,就不需要了
|
||
$header .= "Content-Type: multipart/related;\r\n";
|
||
} else {
|
||
// html或者纯文本的邮件声明成这个
|
||
$header .= "Content-Type: multipart/alternative;\r\n";
|
||
}
|
||
// 邮件头分隔符
|
||
$header .= "\t" . 'boundary="' . $separator . '"';
|
||
$header .= "\r\nMIME-Version: 1.0\r\n";
|
||
// 这里开始是邮件的body部分,body部分分成几段发送
|
||
$header .= "\r\n--" . $separator . "\r\n";
|
||
$header .= "Content-Type:text/html; charset=utf-8\r\n";
|
||
$header .= "Content-Transfer-Encoding: base64\r\n\r\n";
|
||
$header .= $this->body . "\r\n";
|
||
$header .= "--" . $separator . "\r\n";
|
||
// 加入附件
|
||
if (! empty($this->attachment)) {
|
||
$count = count($this->attachment);
|
||
for ($i = 0; $i < $count; $i ++) {
|
||
$header .= "\r\n--" . $separator . "\r\n";
|
||
$header .= "Content-Type: " . $this->getMIMEType($this->attachment[$i]) . '; name="=?UTF-8?B?' . base64_encode(basename($this->attachment[$i])) . '?="' . "\r\n";
|
||
$header .= "Content-Transfer-Encoding: base64\r\n";
|
||
$header .= 'Content-Disposition: attachment; filename="=?UTF-8?B?' . base64_encode(basename($this->attachment[$i])) . '?="' . "\r\n";
|
||
$header .= "\r\n";
|
||
$header .= $this->readFile($this->attachment[$i]);
|
||
$header .= "\r\n--" . $separator . "\r\n";
|
||
}
|
||
}
|
||
// 结束邮件数据发送
|
||
$header .= "\r\n.\r\n";
|
||
|
||
$command[] = array(
|
||
"DATA\r\n",
|
||
354
|
||
);
|
||
$command[] = array(
|
||
$header,
|
||
250
|
||
);
|
||
$command[] = array(
|
||
"QUIT\r\n",
|
||
221
|
||
);
|
||
return $command;
|
||
}
|
||
|
||
/**
|
||
* 发送命令
|
||
*
|
||
* @param string $command
|
||
* 发送到服务器的smtp命令
|
||
* @param int $code
|
||
* 期望服务器返回的响应吗
|
||
* @return boolean
|
||
*/
|
||
protected function sendCommand($command, $code)
|
||
{
|
||
if ($this->debug) {
|
||
echo 'Send command:' . $command . ',expected code:' . $code . '<br />';
|
||
}
|
||
try {
|
||
if (fwrite($this->socket, $command)) {
|
||
// 当邮件内容分多次发送时,没有$code,服务器没有返回
|
||
if (empty($code)) {
|
||
return true;
|
||
}
|
||
// 读取服务器返回
|
||
$data = trim(fread($this->socket, 1024));
|
||
if (! mb_check_encoding($data, 'utf-8')) {
|
||
$data = iconv('gbk', 'utf-8', $data);
|
||
}
|
||
if ($this->debug) {
|
||
echo 'response:' . $data . '<br /><br />';
|
||
}
|
||
if ($data) {
|
||
$pattern = "/^" . $code . "+?/";
|
||
if (preg_match($pattern, $data)) {
|
||
return true;
|
||
} else {
|
||
$this->errorMessage = $data;
|
||
return false;
|
||
}
|
||
} else {
|
||
return false;
|
||
}
|
||
} else {
|
||
$this->errorMessage = "Error: " . $command . " send failed";
|
||
return false;
|
||
}
|
||
} catch (\Exception $e) {
|
||
$this->errorMessage = "Error:" . $e->getMessage();
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 读取附件文件内容,返回base64编码后的文件内容
|
||
*
|
||
* @param string $file
|
||
* 文件
|
||
* @return mixed
|
||
*/
|
||
protected function readFile($file)
|
||
{
|
||
if (file_exists($file)) {
|
||
$file_obj = file_get_contents($file);
|
||
return base64_encode($file_obj);
|
||
} else {
|
||
$this->errorMessage = "file " . $file . " dose not exist";
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取附件MIME类型
|
||
*
|
||
* @param string $file
|
||
* 文件
|
||
* @return mixed
|
||
*/
|
||
protected function getMIMEType($file)
|
||
{
|
||
if (file_exists($file)) {
|
||
$mime = mime_content_type($file);
|
||
return $mime;
|
||
} else {
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 建立到服务器的网络连接
|
||
*
|
||
* @return boolean
|
||
*/
|
||
protected function socket($ssl = true)
|
||
{
|
||
if ($ssl && ! extension_loaded('openssl')) {
|
||
$this->errorMessage = '服务器未启用openssl扩展,无法使用加密方式发送邮件!';
|
||
return false;
|
||
}
|
||
|
||
if (function_exists('stream_socket_client')) {
|
||
|
||
if (! function_exists('stream_socket_enable_crypto')) {
|
||
$this->errorMessage = '服务器已经禁用stream_socket_enable_crypto函数,无法发送邮件!';
|
||
return false;
|
||
}
|
||
|
||
if (! function_exists('stream_set_blocking')) {
|
||
$this->errorMessage = '服务器已经禁用stream_set_blocking函数,无法发送邮件!';
|
||
return false;
|
||
}
|
||
|
||
// 建立连接
|
||
$remoteAddr = "tcp://" . $this->sendServer . ":" . $this->port;
|
||
$this->socket = stream_socket_client($remoteAddr, $errno, $errstr, 30);
|
||
if (! $this->socket) {
|
||
$this->errorMessage = $errstr;
|
||
return false;
|
||
}
|
||
|
||
// 设置加密方式
|
||
$crypto_method = STREAM_CRYPTO_METHOD_TLS_CLIENT;
|
||
if (defined('STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT')) {
|
||
$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_2_CLIENT;
|
||
$crypto_method |= STREAM_CRYPTO_METHOD_TLSv1_1_CLIENT;
|
||
}
|
||
stream_socket_enable_crypto($this->socket, $ssl, $crypto_method);
|
||
} elseif (function_exists('fsockopen')) {
|
||
if ($ssl) {
|
||
$remoteAddr = "ssl://" . $this->sendServer;
|
||
} else {
|
||
$remoteAddr = "tcp://" . $this->sendServer;
|
||
}
|
||
$this->socket = fsockopen($remoteAddr, $this->port, $errno, $errstr, 30);
|
||
if (! $this->socket) {
|
||
$this->errorMessage = $errstr;
|
||
return false;
|
||
}
|
||
} else {
|
||
$this->errorMessage = '服务器已经禁用stream_socket_client和fsockopen函数,请至少开启一个才能发送邮件!';
|
||
return false;
|
||
}
|
||
|
||
stream_set_blocking($this->socket, 1); // 设置阻塞模式
|
||
$str = fread($this->socket, 1024);
|
||
if (! preg_match("/220+?/", $str)) {
|
||
$this->errorMessage = $str;
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
/**
|
||
* 关闭安全socket
|
||
*
|
||
* @return boolean
|
||
*/
|
||
protected function close()
|
||
{
|
||
if (isset($this->socket) && is_object($this->socket)) {
|
||
stream_socket_shutdown($this->socket, STREAM_SHUT_WR);
|
||
return true;
|
||
}
|
||
$this->errorMessage = "No resource can to be close";
|
||
return false;
|
||
}
|
||
} |