作者 邓超

1

要显示太多修改。

为保证性能只显示 14 of 14+ 个文件。

... ... @@ -29,9 +29,19 @@ class Demo extends Command
public function handle()
{
// run(function (){
//
// });
$i = 0;
$n = 'a';
run(function () use (&$i,&$n){
go(function () use (&$i,&$n){
$n = 'b';
$i = 2;
});
});
echo $i;
echo $n;
return Command::SUCCESS;
}
... ...
<?php
//
//namespace App\Console\Commands;
//
//use App\Models\Email;
//use App\Models\Folder;
//use Helper\Mail\Mail;
//use Illuminate\Console\Command;
//use Illuminate\Support\Facades\Cache;
//use Illuminate\Support\Facades\Log;
//use function Co\run;
//
//class PushSync extends Command
//{
// /**
// * The name and signature of the console command.
// *
// * @var string
// */
// protected $signature = 'PushSync';
//
// /**
// * The console command description.
// *
// * @var string
// */
// protected $description = '发布同步任务到队列中';
//
// /**
// * Execute the console command.
// *
// * @return int
// */
// public function handle()
// {
// // 最大协程 1000
// \co::set(['max_coroutine'=>1000]);
// // 开启协程池
// run(function (){
// // 统计邮箱总数量
// $size = Email::select(['id'])->count();
// if($size){
// // 开启协程的数量
// $i = ceil($size/100);
// while ($i >= 0){
// // 开启一个协程
// go(function () use (&$i){
// // 取得邮箱列表
// $emails = Email::getPushEmailList($i);
// if($emails){
// foreach ($emails as $email) {
//
// }
// }
// });
//
// $i--;
// }
// }
//
// });
//
// return Command::SUCCESS;
// }
//
// /**
// * 开始同步执行
// * @param int $id 邮箱id
// * @author:dc
// * @time 2023/2/5 17:21
// */
// private function push(int $id = 0){
//
// $email = Email::where(['status'=>1,'pwd_error'=>0])
// ->where('id','>',$id)
// ->orderBy('id','asc')
// ->select()
// ->first();
//
// if($email){
// // 登录imap服务器
// Mail::login($email->email,$email->password,$email->imap);
// // 设置id
// Mail::$client[$email]->setId($email->id);
// // 同步文件夹
// Mail::syncFolder($email->email);
//
// // 获取当前邮箱的所有文件夹
// $folders = Folder::_all($email->id);
// // 目前只发现最高2级
// foreach ($folders as $folder){
// if(empty($folder['_child'])){
// // 同步邮件
// Mail::syncMail($email->email,$folder['origin_folder']);
// }else{
// // 循环子级目录,有子级的情况,父级不可操作,且不会有邮件
// foreach ($folder['_child'] as $f){
// // 同步邮件
// Mail::syncMail($email->email,$folder['origin_folder'].'/'.$f['origin_folder']);
// }
// }
//
// }
//
// // 再次执行
// $this->push($email->id);
//
// }
//
// }
//
//
//
//}
... ...
<?php
//
//namespace App\Console\Commands;
//
//use App\Models\Email;
//use App\Models\Folder;
//use Helper\Mail\Mail;
//use Illuminate\Console\Command;
//use Illuminate\Support\Facades\Cache;
//use Illuminate\Support\Facades\Log;
//use function Co\run;
//
//class SyncFolder extends Command
//{
// /**
// * The name and signature of the console command.
// *
// * @var string
// */
// protected $signature = 'SyncFolder';
//
// /**
// * The console command description.
// *
// * @var string
// */
// protected $description = '同步邮箱的文件夹';
//
// /**
// * Execute the console command.
// *
// * @return int
// */
// public function handle()
// {
//
// // 这个是防止前一个任务没有完成,反而无法取得资源。
// $rand = date('dHi');
// // 携程数量越大,需要的内存越大
// $max_coroutine = 10000;
// // 最大携程数量
// \co::set(['max_coroutine'=>$max_coroutine]);
//
// run(function () use ($rand){
// // 获取邮箱总数量
// $size = Email::where(['status'=>1,'pwd_error'=>0])->count();
//// Cache::set('syncMail:total',$size);
// // 创建的携程数量
// $max_coroutine = $size > 3000 ? 3000 : $size;
// if ($max_coroutine){
// for ($i = $max_coroutine; $i > 0; $i--) {
// // 创建携程
// go(function () use ($size,$rand){
//// Log::error('携程:'.\co::getCid());
//
// $n = 0;
// while ($n <= $size){
// // 是否已存在
// if(Cache::add('syncMail'.$rand.':'.$n,$n,7200)) {
// $this->sync($n);
// }
// $n++;
// }
//
// });
// }
// }
//
// });
//
// return Command::SUCCESS;
// }
//
// /**
// * 开始同步执行
// * @param int $n 第几条数据
// * @author:dc
// * @time 2023/2/5 17:21
// */
// private function sync(int $n = 0){
//
// $email = Email::where(['status'=>1,'pwd_error'=>0])
// ->orderBy('id','asc')
// ->select()->limit($n,1)
// ->first();
//
// if($email){
// // 登录imap服务器
// Mail::login($email->email,$email->password,$email->imap);
// // 设置id
// Mail::$client[$email]->setId($email->id);
// // 同步文件夹
// Mail::syncFolder($email->email);
//
// // 获取当前邮箱的所有文件夹
// $folders = Folder::_all($email->id);
// // 目前只发现最高2级
// foreach ($folders as $folder){
// if(empty($folder['_child'])){
// // 同步邮件
// Mail::syncMail($email->email,$folder['origin_folder']);
// }else{
// // 循环子级目录,有子级的情况,父级不可操作,且不会有邮件
// foreach ($folder['_child'] as $f){
// // 同步邮件
// Mail::syncMail($email->email,$folder['origin_folder'].'/'.$f['origin_folder']);
// }
// }
//
// }
//
//
//
// }
//
// }
//
//
//
//}
... ...
<?php
namespace App\Console\Commands;
use App\Models\Email;
use App\Models\Folder;
use Helper\Mail\Mail;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
use function Co\run;
class SyncMailList extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'SyncMailList';
/**
* The console command description.
*
* @var string
*/
protected $description = '同步邮箱的邮件到本地';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
// 这个是防止前一个任务没有完成,反而无法取得资源。
$rand = date('dHi');
// 携程数量越大,需要的内存越大
$max_coroutine = 10000;
// 最大携程数量
\co::set(['max_coroutine'=>$max_coroutine]);
run(function () use ($rand,$max_coroutine){
// 获取邮箱总数量
$size = Email::where([])->count();
// 最后一条数据的id
$lastId = Email::where([])->orderBy('id','desc')->value('id');
// 创建的携程数量
$max_coroutine = $size > $max_coroutine ? $max_coroutine : $size;
if ($max_coroutine){
for ($i = $max_coroutine; $i > 0; $i--) {
// 创建携程
go(function () use ($size,$rand,$lastId){
$n = 1;
while ($n <= $lastId){
// 是否已存在
if(Cache::add('syncMail'.$rand.':'.$n,$n,7200)) {
$this->sync($n);
}
$n++;
}
});
}
}
});
return Command::SUCCESS;
}
/**
* 开始同步执行
* @param int $n 第几条数据
* @author:dc
* @time 2023/2/5 17:21
*/
private function sync(int $n = 0){
$email = Email::where(['id'=>$n])->first();
// 密码没有错误,且状态正常的
if ($email && $email->pwd_error == 0 && $email->status == Email::STATUS_ACTIVE){
// 登录imap服务器
Mail::login($email->email,$email->password,$email->imap);
// 设置id
Mail::$client[$email]->setId($email->id);
// 同步文件夹
Mail::syncFolder($email->email);
// 获取当前邮箱的所有文件夹
$folders = Folder::_all($email->id);
// 目前只发现最高2级
foreach ($folders as $folder){
if(empty($folder['_child'])){
// 同步邮件
Mail::syncMail([],$email->email,$email->id,$folder['id'],$folder['origin_folder']);
}else{
// 循环子级目录,有子级的情况,父级不可操作,且不会有邮件
foreach ($folder['_child'] as $f){
// 同步邮件
Mail::syncMail([],$email->email,$email->id,$f['id'],$folder['origin_folder'].'/'.$f['origin_folder']);
}
}
}
}
}
}
... ...
... ... @@ -4,8 +4,8 @@ namespace App\Http\Controllers;
use App\Models\Email;
use App\Models\Host;
use Helper\Fun;
use Helper\Mail\Imap;
use Helper\Mail\Mail;
/**
* 提供邮件各项数据
... ... @@ -35,10 +35,10 @@ class MailApi
]);
if($validator->fails()){
Fun::response()
return res()
->message($validator->errors()->first())
->status(400)
->throw();
->toJson();
}
// host
... ... @@ -50,30 +50,33 @@ class MailApi
$model->imap = $formData['imap'];
$model->smtp = $formData['smtp'];
$model->password = encrypt($formData['password']);
$model->password = @base64_encode($formData['password']);
$imap = new Imap();
// 是否初始成功
try {
$imap->login("ssl://{$formData['imap']}:993",$model->email,$model->password);
Mail::login($model->email,$model->password,$model->imap);
}catch (\Throwable $e){
Fun::response()
return res()
->message($e->getMessage())
->status(400)
->throw();
->toJson();
}
// 登录成功了,密码验证字段通过
$model->pass_error = 0;
$model->pwd_error = 0;
// 保存好邮箱
$model->save();
// 设置上id,方便后面使用
Mail::$client[$model->email]->setId($model->id);
// 开始同步文件夹
$folder = $imap->getFolder();
// $folder = Mail::syncFolder($model->email);
Fun::response()
->data($folder)
->throw();
return res()
->data([
'token' => token_en($model->id.','.$model->email.','.time())
])
->toJson();
}
... ... @@ -86,7 +89,7 @@ class MailApi
$host = Host::_all();
Fun::response()->data($host)->throw();
res()->data($host)->throw();
}
... ...
<?php
namespace App\Mail\Jobs;
/**
*
* @time 2022/7/29 15:11
* Class ImapApi
* @package App\Mail\Jobs
*/
class ImapApi {
/**
* @var ImapApi
*/
private static $Instance;
/**
* 如 imap.qq.com
* @var string
*/
private $host;
/**
* 邮箱地址
* @var string
*/
private $username;
/**
* 密码
* @var string
*/
private $password;
/**
* 端口
* @var int
*/
private $port = 993;
private function __construct(){}
/**
* @return ImapApi
*/
public static function getInstance(): ImapApi
{
if(!self::$Instance){
self::$Instance = new static();
}
return self::$Instance;
}
}
<?php
namespace App\Mail\Jobs;
use App\Http\Mail\lib\MailFun;
use App\Http\Mail\Models\Email;
use App\Http\Mail\Models\EmailSendJob;
use App\Http\Mail\Models\EmailSendJobStatu;
use App\Http\Models\EmailSendTemplate;
use App\Sk;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Cache;
/**
* @author:dc
* @time 2022/11/9 11:52
* Class SendJob
*/
class SendJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public $job_id;
/**
* SendServiceMsg constructor.
* @param $id
*/
public function __construct($id)
{
// 使用的链接
$this->connection = Sk::QUEUE_EMAIL_QUN;
$this->job_id = $id;
}
/**
* Execute the job.
*
* @return bool
*/
public function handle()
{
// 查询邮件任务主体
$jobData = EmailSendJob::where('id',$this->job_id)
->whereIn('status',[EmailSendJob::STATUS_WAIT,EmailSendJob::STATUS_RUNING])
->first();
if(!$jobData){
$this->log('任务不存在');
return false;
}
// 发送时间
if(trim($jobData->send_time) != 'now'){
// 定时发送
list($start,$end) = explode(',',$jobData->send_time);
// 系统是中国时区,按照美国时区要慢13个小时
if(!(date('H') >= $start || date('H') < $end)){
// 也就是 美国早上 8点到晚上10点
// $this->log('休息中');
SendJob::dispatch($this->job_id)->delay(600);
return true;
}
}
$this->log('开始检查任务:'.$this->job_id);
if(!$jobData){
$this->log('没有找到任务');
return true;
}
// 查询需要发送的邮件
$jobStdata = EmailSendJobStatu::where(['job_id'=>$this->job_id,'status'=>EmailSendJobStatu::STATUS_WAIT])->first();
if(!$jobStdata){
$this->log('完成所有了');
// 是否完成
$jobData->status = EmailSendJob::STATUS_SUCCESS;
$jobData->save();
return true;
}
$this->log('找到任务:'.$jobStdata['id']);
// 防止重复
$cachekey = 'email_send_job:'.$this->job_id.":".$jobStdata['id'];
// 存在
if(Cache::has($cachekey)){
$this->log("任务:{$jobStdata['id']}正在发送,跳过");
SendJob::dispatch($this->job_id)->delay(30);
return true;
}
// 占有2分钟
Cache::set($cachekey,$jobStdata['id'],120);
// 是否等待状态
if($jobData->status !== EmailSendJob::STATUS_RUNING){
$this->log('开始运行脚本了');
$jobData->status = EmailSendJob::STATUS_RUNING;;
$jobData->save();
}
// 当前管理账号下所有绑定的邮件
$emailinfos = Email::_get($jobData->user_id,Email::STATUS_ACTIVE,['e.id','e.email','e.email_name','e.smtp','e.password','e.pwd_error']);
$smtpErrorNum = 0;// 错误次数
// 标签
$tags = $jobData->tags;
$tags = is_array($tags) ? $tags : explode(',',$tags);
// 模板列表
$tempLists = EmailSendTemplate::getAdminTagsList($tags);
if(!$tempLists){
// 是否完成
$jobData->status = EmailSendJob::STATUS_SUCCESS;
$jobData->save();
$this->log('没有可用的模板');
return false;
}
// 随机一个模板
$template = $tempLists[array_rand($tempLists->toArray())];
// 节点
EMAILRESETRANG:
if(!$emailinfos){
$this->log('暂时没有可分配的账号');
// 再次发布任务,延时10分钟
SendJob::dispatch($this->job_id)->delay(120);
return true;
}
// 随机一个邮件来当发送
$key = array_rand($emailinfos,1);
$emailinfo = $emailinfos ? $emailinfos[$key] : [];
if(!$emailinfo){
$this->log('没有分配到账号');
// 再次发布任务,延时10分钟
SendJob::dispatch($this->job_id)->delay(600);
return true;
}
unset($emailinfos[$key]);
$emailinfos = array_values($emailinfos);
// 密码是否需要验证
if($emailinfo['pwd_error']){
$this->log('账号密码验证失败,需要修改密码 '.$emailinfo['email']);
// 重新随机一个,
goto EMAILRESETRANG;
}
// 每个小时不能超过20
$cacheEmailSuccesshkey = 'email_job_email:'.$emailinfo['email'].":success_h";
if(Cache::get($cacheEmailSuccesshkey,0) >= 8){
$this->log('账号超过每小时8封了'.$emailinfo['email']);
// 重新随机一个,
goto EMAILRESETRANG;
}
// 一天中是否超过100,
// $cacheEmailSuccessdkey = 'email_job_email:'.$emailinfo['email'].":success_d";
// if(Cache::get($cacheEmailSuccessdkey,0) >= 100){
// $this->log('账号超过每天100封了'.$emailinfo['email']);
// // 重新随机一个,
// goto EMAILRESETRANG;
// }
$cachemailikey = 'email_job_email:'.$emailinfo['email'].":success_i";
if (Cache::has($cachemailikey)){
$this->log('账号没超过10分钟间隔 '.$emailinfo['email']);
// 重新随机一个,
goto EMAILRESETRANG;
}
// 是否错误了
$cachemailierrordkey = 'email_job_email:'.$emailinfo['email'].":error_d";
if(Cache::has($cachemailierrordkey)){
$this->log('账号记录错误了,跳过 1小时'.$emailinfo['email']);
goto EMAILRESETRANG;
}
Cache::set($cachemailikey,$jobStdata['id'],550);
$this->log('找到账号:'.$emailinfo['email']);
// 数据发送的email
$data = json_decode($jobData->maildata,true);
$data['body'] = $template['body'];
$data['subject'] = $template['subject'];
try {
// 替换邮件规则,
$data['body'] = str_replace('[read][/read]','<div style="opacity: 0;width: 1px;height: 1px;overflow: hidden;"><img src="https://king.shopk.com/_shopk_?mail='.base64_encode('logo|'.$jobStdata['id']).'" /></div>',$data['body']);
// 匹配链接规则
if (preg_match_all("/\[link:(.*)\](.*)\[\/link\]/U",$data['body'],$m)){
foreach ($m[0] as $mk=>$item){
$data['body'] = str_replace($item,'<a target="_blank" href="https://king.shopk.com/_shopk_?mail='.base64_encode('link|'.$jobStdata['id'].'|'.urlencode($m[1][$mk])).'">'.$m[2][$mk].'</a>',$data['body']);
}
}
// 发送邮件
MailFun::sendEmail(
$emailinfo['smtp'],$emailinfo['email'],decrypt($emailinfo['password'])
,$emailinfo['email_name'],$jobStdata['to_email'],$data['subject'],
$data['body'],$data['file']??[],false
,($data['priority']??0) ? 1 : 3
);
// 记录
$jobStdata->status = EmailSendJobStatu::STATUS_SUCCESS;
$jobStdata->send_email = $emailinfo['email'];
$jobStdata->time = date('Y-m-d H:i:s');
$this->log('成功了');
// 成功
$jobData->success = $jobData->success+1;
// 24小时内不能超过100
// if(Cache::has($cacheEmailSuccessdkey)){
// // 加1
// Cache::increment($cacheEmailSuccessdkey);
// }else{
// Cache::set($cacheEmailSuccessdkey,1,86400);
// }
// 每小时内不能超过20
if(Cache::has($cacheEmailSuccesshkey)){
// 加1
Cache::increment($cacheEmailSuccesshkey);
}else{
Cache::set($cacheEmailSuccesshkey,1,3600);
}
} catch (\Exception $e) {
if($e->getMessage()=='SMTP Error: data not accepted.'){
// 下次跳过账号
if(!Cache::has($cachemailierrordkey)) {
Cache::set($cachemailierrordkey, $e->getMessage(), 3600);
}
}
// 无法验证,密码错误了,处理密码
if($e->getMessage()=='SMTP Error: Could not authenticate.'){
Email::_update(['id'=>$emailinfo['id']],['pwd_error'=>1]);
}
$smtpErrorNum++;
// 超过失败3次的
$error = $jobStdata->error;
$error[] = [
'email' => $emailinfo['email'],
'message' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine()
];
$jobStdata->time = date('Y-m-d H:i:s');
$jobStdata->error = json_encode($error);
// 如果失败了,重试3次
if($smtpErrorNum <= 20){
// 记录
$jobStdata->save();
$this->log('错误次数'.$smtpErrorNum);
goto EMAILRESETRANG;
}
$this->log('错误超过次数'.$smtpErrorNum);
// 记录
$jobStdata->status = EmailSendJobStatu::STATUS_ERROR;
$jobStdata->send_email = $emailinfo['email'];
// 错误
$jobData->error = $jobData->error+1;
}
// 保存
$jobStdata->save();
$jobData->save();
$this->log('完成');
// 再次发布任务
SendJob::dispatch($this->job_id);
}
protected function log($content){
@file_put_contents(storage_path('logs/send_email_job_'.$this->job_id.'_.log'),date('Y-m-d H:i:s ').$content.PHP_EOL,FILE_APPEND);
}
}
<?php
namespace App\Mail;
use App\Mail\Jobs\SendJob;
use App\Mail\lib\Lang;
use App\Mail\lib\MailFun;
use App\Mail\lib\MailParse\Body;
use App\Models\Email;
use App\Models\EmailBody;
use App\Models\EmailContact;
use App\Models\EmailContactGroup;
use App\Models\EmailFolder;
use App\Models\Host;
use App\Models\EmailList;
use App\Models\EmailLog;
use App\Models\EmailSendJob;
use Illuminate\Http\UploadedFile;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Validator;
/**
* 邮件服务
* @time 2022/7/29 15:05
* Class Mail
* @package app\Mail
*/
class Mail {
/**
* @var Mail
*/
private static $Instance;
/**
* 如 imap.qq.com
* @var array
*/
private $host = [
'imap'=>'',
'smtp'=>''
];
/**
* 用户email表的id
* @var int
*/
private $id = 0;
/**
* 邮箱地址
* @var string
*/
private $username = '';
private $nickname = '';
/**
* 密码
* @var string
*/
private $password = '';
/**
* 端口
* @var int 143 993
*/
private $port = 993;
/**
* 协议
* @var string
*/
private $ssl = 'ssl://';
/**
* 用户id
* @var int
*/
private $user_id = 0;
/**
* 附件保存目录
* @var string
*/
private $filePath;
/**
* imap服务器连接
* @var \App\Http\Mail\lib\client\Imap[]
*/
private $client;
/**
* 连接目录 INBOX
* @var string
*/
private $folder = 'INBOX';
/**
* 目录编号
* @var int
*/
private $folderId = 0;
/**
* @var array|\Illuminate\Contracts\Foundation\Application|\Illuminate\Http\Request|string|null
*/
private $request;
/**
* 所有绑定邮箱的id
* @var array
*/
private $email_ids = [];
private function __construct(){
$this->request = request();
}
/**
* @return Mail
*/
public static function getInstance(int $user_id, string $email=''): Mail
{
$key = md5($user_id.$email);
if(empty(self::$Instance[$key])){
$mail = self::$Instance[$key] = new static();
$mail->user_id = $user_id;
// 绑定的所有邮箱id
$mail->email_ids = array_column(Email::_getById($mail->user_id),'id');
if($email){
$data = Email::_first($email);
// 是否存在并绑定了
if($data && in_array($data['id'],$mail->email_ids)){
$mail->id = $data['id'];
$mail->username = $email;
$mail->nickname = $data['email_name']??'';
$mail->password = $data['password'];
// 当前选择的邮箱
$mail->host = [
'imap' => $data['imap'],
'smtp' => $data['smtp'],
];
// 设置目录
$mail->selectFolder($mail->folder);
}else{
throw new \Exception(Lang::__('email_not_bind',$email));
}
}
}
// 设置邮件附件地址
self::$Instance[$key]->setFilePath(storage_path('/imap'));
return self::$Instance[$key];
}
/**
* 设置目录
* @param $path
* @time 2022/8/1 15:39
*/
public function setFilePath($path){
$this->filePath = $path;
// if(!is_dir($this->filePath)){
// @mkdir($this->filePath,0775,true);
// }
}
/**
* @return string
*/
public function getFilePath(): string
{
$path = $this->filePath.'/'.$this->username.'/';
// if(is_dir($path)){
// mkdir($path,0775,true);
// }
return $path;
}
/**
* 选择使用的用户
* @param int $user_id
* @param string $email
* @return Mail
* @author:dc
* @time 2022/7/29 17:42
*/
public static function use(int $user_id, string $email=''):Mail {
$mail = self::getInstance($user_id,$email);
return $mail;
}
/**
* 选择文件夹
* @param string|int $folder
* @time 2022/8/4 14:54
*/
public function selectFolder($folder){
if($folder) {
$lists = EmailFolder::_all($this->id, false);
// 找到目标文件的id,pid
foreach ($lists as $list) {
if ($list[is_numeric($folder)?'id':'folder'] == $folder) {
$id = $list['id'];// id
$pid = $list['pid']; // 上级id
$folder = $list['origin_folder'];
}
}
// 不存在
if(!$lists){
$folder = 'INBOX';
$pid = 0;
// 这个名字是每个邮箱默认的,不可更改
$id = EmailFolder::_insert($this->user_id,$this->id,'INBOX','INBOX');
if($folder != 'INBOX'){
$id = 0;
}
}
// 是否存
if ($id ?? 0) {
// 拿到上级,成为 dir/dir/dir
if ($pid ?? 0) {
EmailFolder::_firstTree($lists, $pid, $folder,'origin_folder');
}
$this->folderId = $id;
$this->folder = $folder;
$this->folderInfo = EmailFolder::_first($id);
} else {
throw new \Exception(Lang::__('email_folder_not', $folder));
}
}
}
/**
* @return \App\Http\Mail\lib\client\Imap
* @throws \Exception
* @time 2022/8/5 17:22
*/
public function client($email=''){
$email = $email ? : $this->username;
if(empty($this->client[$email])){
if($email == $this->username){
$this->login();
}else{
$data = Email::_first($email);
if($data){
$this->loginImap($data['imap'],$data['email'],$data['password']);
}
}
}
return $this->client[$email];
}
/**
* 登录邮箱
* @param string $password
* @param string $imap
* @param string $smtp
* @return bool
* @throws \Exception
* @time 2022/8/5 17:21
*/
public function login(string $password='',string $imap='',string $smtp=''):bool{
// host
if($imap && $this->username){
$this->hostAdd(explode('@',$this->username)[1],$imap,$smtp);
}
if(!$password && !$this->password){
throw new \Exception(Lang::__('password_required'));
}
if($password){
// 如果密码不一致,更新
if($password != $this->password){
Email::_changePwd($this->username, $password);
}
$this->password = $password;
}
if(!$this->hostMy()){
throw new \Exception(Lang::__('login_host'));
}
// imap imap.qq.com
$this->loginImap($this->hostMy('imap'),$this->username,$this->password);
// 密码没验证成功
// if($this->id){
// Email::_update(['id'=>$this->id],['pwd_error'=>1]);
// }
return true;
}
/**
* 公用链接
* @param $host
* @param $email
* @param $password
* @throws \Exception
* @author:dc
* @time 2022/12/7 9:35
*/
public function loginImap($host,$email,$password):void {
$this->client[$email] = new \App\Http\Mail\lib\client\Imap();
// 是否初始成功
$this->client[$email]->login($this->ssl.$host.':'.$this->port,$email,$password);
}
/**
* 自动路由
* @return \Illuminate\Http\JsonResponse|string|\Symfony\Component\HttpFoundation\BinaryFileResponse
* @throws \Illuminate\Contracts\Container\BindingResolutionException
* @throws \Psr\SimpleCache\InvalidArgumentException
* @throws \Throwable
* @author:dc
* @time 2022/8/1 11:07
*/
public function autoRoute(){
$task = $this->request->get('_task');
$action = $this->request->get('_action');
$result = [];
switch ($task){
case 'sync': {
// 同步操作
switch ($action){
case 'list': {
// 同步邮件列表
$msgno = $this->request->post('msgno',[]);
if(is_string($msgno)){
$msgno = explode(',',$msgno);
}
$result = $this->syncEmailList($msgno);
break;
}
// 同步文件夹
case 'folder': {
$folder = $this->request->post('folder');
$result = $this->syncFolder($folder);
break;
}
// 更新最新的
case 'new':{
$result = $this->syncEmailList();
break;
}
}
break;
}
// 设置标签,已读,未读,删除等
case 'flags':{
$ids = $this->request->post('ids');
$ids = is_string($ids) ? explode(',',$ids) : $ids;
$ids = is_array($ids) ? $ids : [$ids];
// 获取到邮件的uid
$uids = EmailList::_getUidsByIds($ids,$this->id);
$filed = '';
$value = 0;
$mod = '';
$flags = '';
// 标记操作
switch ($action){
// 标记已读
case 'setSeen':{
$filed = 'seen';
$value = 1;
$flags = 'seen';
$mod = '+';
break;
}
// 标记未读
case 'delSeen':{
$filed = 'seen';
$value = 0;
$flags = 'seen';
$mod = '-';
break;
}
case 'setFlagged':{
$filed = 'flagged';
$value = 1;
$flags = 'flagged';
$mod = '+';
break;
}
case 'delFlagged':{
$filed = 'flagged';
$value = 0;
$flags = 'flagged';
$mod = '-';
break;
}
}
if($this->setflagged($uids,$flags,$mod)){
// 更新标签
if(EmailList::_setFlags(array_keys($uids),$this->id,$filed,$value)){
$result = array_keys($uids);
}
}
break;
}
case 'info': {
$id = (int) $this->request->get('id');
// 读取邮件
$result = $this->emailInfo($id);
if(!$result){
$result = new static();
}else{
// 是否是数组
if(!is_array($result)) $result = $result->toArray();
// 缓存一下
\Cache::set('app_email_info_'.$this->user_id.":".$result['id'],$result,3000);
// 渲染视图
$result['body'] = view('admin/email/info',[
'data' => $result
])->render();
// 有邮件编码bug,必须要转
$result['body'] = base64_encode($result['body']);
}
///////////////////////// 这里是测试
// print_r($result['body']['text_html']);exit();
// foreach ($result['body']['text_html'] as $item){
// if(!empty($item['type']) && ($item['type'] == 'text/html' || $item['type'] == 'text/plain')){
// header("content-type:text/html;charset=".($item['charset']??'utf-8'));
// echo $item['body'];
// }
// }
// exit();
///////////////////////
break;
}
case 'list': {
if($this->id){
$eids = [$this->id];
}else{
$eids = array_column(Email::_get($this->user_id),'id');
}
$total = 0;
if($this->folder=='INBOX'){
$fids = EmailFolder::_user_folders($this->email_ids);
// $total = array_sum(array_column($fids,'exsts'));
$fids = array_column($fids,'id');
$unseen = array_sum(array_column($fids,'unseen')); // 未读数量
// $unseen = EmailList::_getUnseenNum($eids); // 未读数量
}else{
$fids = [$this->folderId];
$total = $this->folderInfo['exsts']??0;
}
// 搜索
$search = [];
$search['search'] = $this->request->get('search');
$search['seen'] = $this->request->get('seen');
$result = $this->emailLists($eids,$fids,$total,$search)->toArray();
// if($this->folder == 'INBOX'){
$this->allemail = [];
foreach ($result['data'] as $k=>$datum){
if(!$datum['uid']){
// 邮件
if (empty($this->allemail[$datum['email_id']])){
$this->allemail[$datum['email_id']] = Email::_firstById($datum['email_id']);
}
if(empty($this->allemail[$datum['email_id']])){
continue;
}
// 同步
$id = $this->syncEmailList(
[$datum['msgno']],
$datum['email_id'],
$this->allemail[$datum['email_id']]['email'],
$datum['folder_id'],
$this->folder
);
if($id['ids']){
// 重新获取
$result['data'][$k] = EmailList::_first($id['ids'][0]);
}
}
// 干掉 -snv 和 (Failure)
if(preg_match("/(\-\ssnv)|(\(Failure\))|(\(Delay\))$/",$datum['subject'])){
unset($result['data'][$k]);
continue;
}
}
$result['data'] = array_values($result['data']);
// }
// 未读邮件数量
$result['unseen'] = $unseen??0;
break;
}
case 'mail': {
$result = $this->emails();
break;
}
case 'add': {
$result = $this->emailAdd();
break;
}
case 'folder': {
$email = $this->request->post('email');
if($email){
$ids = Email::_getIds($email);
}else{
$ids = $this->id;
}
$result = $this->folders($ids);
break;
}
case 'contact': {
switch ($action){
case 'info': {
$id = $this->request->get('id');
$result = $this->contactInfo($id);
break;
}
case 'add': {
$result = $this->contactAdd();
break;
}
case 'del': {
$result = $this->contactDel();
break;
}
case 'group':{
$is_contact = $this->request->get('is_contact');
$result = $this->contactGroup($is_contact);
break;
}
case 'group_save': {
$name = $this->request->post('group_name');
$id = $this->request->post('id',0);
$result = $this->contactGroupSave($name,$id);
break;
}
case 'group_del': {
$group_id = (int) $this->request->get('group_id');
$result = $this->contactGroupDel($group_id);
break;
}
default: {
$is_group = $this->request->get('is_group',false);
$result = $this->contact($is_group);
break;
}
}
break;
}
// 下载附件
case 'download':{
$name = $this->request->get('name');
$originname = $this->request->get('originname');
return $this->download($name,$originname);
}
case 'contact_view':{
return $this->contact_view();
}
// 发送邮件
case 'send_email':{
$data = $this->request->post('to');
// 文件收件人
$to = $this->request->file()['to']['to_email_file']??[];
if($to && $to instanceof UploadedFile){
$data['to'] = ($data['to']??'')."\n".$to->getContent();
}
// 附件
$data['file'] = $this->request->file()['to']['file']??[];
if($data['file']){
$upconfig = [
// 上传文件的大小范围
'size' => [
'max' => 1024 * 1024 * 100, // 100M大了服务器不知道会发生什么
'min' => 1, // 0k
],
// 扩展名
'ext' => [
'xlsx', 'xltx', 'potx', 'ppsx', 'pptx', 'sldx', 'docx', 'dotx', 'xlam', 'xlsb',
'apk', 'doc', 'pdf', 'xls', 'ppt', /*'jar', 'js', 'json', 'rpm',*/
'swf', 'tar', 'zip', 'gif', 'png', 'flv', 'avi', 'ai', 'gz',
'jpg', 'mov', 'mp3', 'mp4', 'txt', 'webm', 'webp',
],
// mime类型
'mime' => [
/*'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',
/*'apk' => */'application/vnd.android.package-archive',
/*'doc' => */'application/msword',
/*'pdf' => */'application/pdf',
/*'xls' => */'application/vnd.ms-excel',
/*'ppt' => */'application/vnd.ms-powerpoint',
// /*'jar' => */'application/java-archive',
// /*'js' => */'application/javascript',
// /*'json' => */'application/json',
// /*'rpm' => */'application/x-rpm',
/*'swf' => */'application/x-shockwave-flash',
/*'tar' => */'application/x-tar',
/*'zip' => */'application/zip',
/*'gif' => */'image/gif',
/*'png' => */'image/png',
/*'flv' => */'video/x-flv',
/*'avi' => */'video/x-msvideo',
/*'ai' => */'application/postscript',
/*'gz' => */'application/x-gzip',
/*'jpg' => */'image/jpeg',
/*'mov' => */'video/quicktime',
/*'mp3' => */'audio/mpeg',
/*'mp4' => */'video/mp4',
/*'txt' => */'text/plain',
/*'webm' => */'video/webm',
/*'webp' => */'image/webp',
],
// 磁盘
'disk' => 'local2',
// 目录
'path' => '/email/'.$this->username.'/',
];
// 设置配置
Config::set('upload.email_upload',$upconfig);
foreach ($data['file'] as $k=>$file){
if($file instanceof UploadedFile){
$data['file'][$k] = [
'path' => Upload::put($file,'email_upload'),
'origin_name' => $file->getClientOriginalName(),
];
}
}
// 保存的目录
$path = config('filesystems.disks.local2.root');
foreach ($data['file'] as &$f){
$f['path'] = realpath($path.'/'.$f['path']);
}
}
if($this->send($data)){
return $this->echoJson(1,'邮件发送成功');
}
}
}
return $this->echoJson($result);
}
/**
* 自动路由时
* @return array
* @time 2022/8/1 16:28
*/
public function getRoute(){
return [
[
'name' => '同步文件夹',
'route' => '?_task=sync&_action=folder'
],
[
'name' => '同步邮件列表',
'route' => '?_task=sync&_action=list'
],
[
'name' => '邮件详情',
'route' => '?_task=info&id=1972'
],
[
'name' => '邮件列表',
'route' => '?_task=list'
],
[
'name' => '邮箱',
'route' => '?_task=mail'
],
[
'name' => '添加邮箱',
'route' => '?_task=add'
],
[
'name' => '邮箱联系人',
'route' => '?_task=contact'
],
];
}
/**
* 同步文件夹
* 缺点,远程修改文件名称后,同步回来,就会新建文件夹,会导致邮件混乱,
* 处理方法,删除本地的,重新同步该文件夹下面的所有邮件,虽然不友好,也只能这样做。
* @time 2022/8/1 16:09
*/
public function syncFolder($folder=''){
// 同步单个文件夹
if($folder){
$status = $this->client()->selectFolder($this->folder);
// 更新数量
EmailFolder::_updateNum($this->folderId,$status['EXISTS']??null, $status['UNSEEN']??null);
return EmailFolder::_firstAndEmailId($this->folderId,$this->id);
}
// 读取所有文件夹,未解密
$folders = $this->client()->getFolder();
DB::beginTransaction();
foreach ($folders as $folder){
// 处理子父文件夹
$folder['id'] = explode('/',$folder['folder']);
$folder['name'] = explode('/',$folder['parseFolder']);
$pid = 0;
foreach ($folder['id'] as $k=>$item){
// 插入到数据库
$pid = EmailFolder::_insert(
$this->user_id,
$this->id,
$folder['name'][$k],
$item,
$pid
);
}
}
DB::commit();
return EmailFolder::_all($this->id);
}
/**
* 同步当前用户的邮件数量
* @return array
* [surplus_num] 剩余待拉取数量
* [ids] 本次拉取后保存后的id
* @time 2022/8/2 14:06
*/
public function syncEmailList($msgno=[],$use_email_id=0,$use_email='',$use_folder_id=0,$use_folder=''){
$use_email_id = $use_email_id ? : $this->id;
$use_email = $use_email ? : $this->username;
$use_folder_id = $use_folder_id ? : $this->folderId;
$use_folder = $use_folder ? : $this->folder;
// 零时增加时长
// set_time_limit(180);
// 选择文件夹
$status = $this->client($use_email)->selectFolder($use_folder);
if (!isset($status['EXISTS']) || !$status['EXISTS']){
return [
'ids' => [],
'folder' => $status,
'end' => true
];
}
$end = false;
if($msgno){
goto SYNCEMAILLIST;
}
// 最后拉取的时间,如果是第一次
$lastMsgno = EmailList::_lastMsgno($use_email_id, $use_folder_id);
$nu = 20;
if(!$msgno){
if(!$lastMsgno){
$msgno = range(1,$nu);
}else{
$msgno = range($lastMsgno,$lastMsgno+$nu);
if($lastMsgno > $status['EXISTS']){
$msgno = range($status['EXISTS'] > $nu ? $status['EXISTS'] - $nu : 1,$status['EXISTS']);
}
// 一样就不拉新的
if($lastMsgno == $status['EXISTS']){
return [
'ids' => [],
'folder' => $status,
'end' => true,
];
}
}
}
// 更新数量
EmailFolder::_updateNum($use_folder_id,$status['EXISTS'], $status['UNSEEN']??null);
// 说明是第一次
if(!$lastMsgno){
$pgnu = 1000;
DB::beginTransaction();
for ($n=0;$n<$status['EXISTS']/$pgnu;$n++){
$i = ($pgnu*$n)+1;
$max = $i+$pgnu;
$max = $max > $status['EXISTS'] ? $status['EXISTS']+1 : $max;
$sql = [];
while ($i<$max){
$sql[] = "(".$use_email_id.",{$i},".$use_folder_id.",1)";
$i++;
}
// bug 使用事务,无法插入数据
$ret = DB::insert("insert INTO email_lists (`email_id`,`msgno`,`folder_id`,`seen`) VALUES ".implode(',',$sql));
if(!$ret){
DB::rollBack();
abort(500,'同步失败');
}
}
DB::commit();
// 最新的数量
$msgno = range($status['EXISTS'] > $nu ? $status['EXISTS'] - $nu : 1,$status['EXISTS']);
// 不同步
return [
'ids' => [1],
'folder' => $status,
'end' => $end,
];
}
SYNCEMAILLIST:
// 是否有id
$dataids = EmailList::_getIdsByMsgno($use_email_id,$use_folder_id,$msgno);
$this->client($use_email)->debug(false,storage_path('logs'));
// 循环
$results = $this->client($use_email)->fetchHeader($msgno);
if($results){
DB::beginTransaction();
// 批量插入
foreach ($results as $key=>$result){
if($key == $status['EXISTS']){
$end = true;
}
// $header = $this->client($use_email)->getHeader($result['RFC822.HEADER']??'');
$header = &$result['HEADER.FIELDS'];
foreach ($result['FLAGS'] as $k=>$FLAG){
$result['FLAGS'][$k] = strtolower(str_replace('\\','',$FLAG));
}
try {
$file_header = &$result['BODYSTRUCTURE'];
// 没有收件人
if(!empty($header['To'])){
$header['To'] = MailFun::toOrFrom($header['To']);
}else{
$header['To'] = [];
}
$header['From'] = MailFun::toOrFrom($header['From']);
$data = [
'id' => $dataids[$key]??0,
'msgno' => $key,
'uid' => $result['UID'],
'subject' => $header['Subject'],
// 'cc' => $header['Cc']??'',
'from' => $header['From'][0]['email']??'',
'from_name' => $header['From'][0]['name']??'',
'to' => $header['To']?implode(',',array_column($header['To'],'email')):'',
'to_name' => json_encode($header['To']),
'date' => isset($header['Date'])&&$header['Date'] ? strtotime(is_array($header['Date']) ? $header['Date'][0] : $header['Date']) : strtotime($result['INTERNALDATE']),
'message_id' => $header['Message-ID']??'',
'udate' => strtotime($result['INTERNALDATE']),
// 'size' => $result['RFC822.SIZE'],
'recent' => in_array('recent',$result['FLAGS']),
'seen' => in_array('seen',$result['FLAGS']),
'draft' => in_array('draft',$result['FLAGS']),
'flagged' => in_array('flagged',$result['FLAGS']),
'answered' => in_array('answered',$result['FLAGS']),
'folder_id' => $use_folder_id,
'email_id' => $use_email_id,
'uuid' => $use_email_id.$use_folder_id.$result['UID'],
'is_file' => MailFun::isFile($file_header[$key]['BODYSTRUCTURE']??[]) //是否附件
];
}catch (\Throwable $e){
Log::error('邮件解析失败:'.$e->getMessage().print_r($result,true));
unset($results[$key]);
continue;
}
$results[$key] = $data;
}
$ids = EmailList::_insertAll(array_values($results));
// 提交
DB::commit();
}else{
$end = true;
}
return [
'ids' => $ids??[],
'folder' => $status,
'end' => $end,
];
}
/**
* 同步body
* @param int $uid
* @param int $id
* @return mixed
* @time 2022/8/2 15:11
*/
public function syncEMailBody(int $uid, $id = 0 ){
// $this->client()->debug(true,storage_path('logs'));
$this->client()->selectFolder($this->folder);
$body = $this->client()->fetch([$uid],'body',true);
/******* start ********/
// 记录原始数据,方便分析
$path = storage_path('logs/email/'.$this->username.'/');
if(!is_dir($path)){
mkdir($path,0775,true);
}
file_put_contents($path.$id.'.'.time().'.log',print_r($body,true));
/******** end *******/
if($body && $id){
$body = array_values($body);
$body = (new Body($body[0]['RFC822.TEXT'],$this->getFilePath()))->getItem();
EmailBody::_insert(
$id,
$body
);
}
return EmailBody::_first($id);
}
/**
* 设置标记
* @param array $uids
* @param string $flags
* @param string $mod +|-
* @return bool
* @throws \Exception
* @author:dc
* @time 2022/10/27 14:22
*/
public function setflagged(array $uids,string $flags,string $mod){
// 选择目录
$status = $this->client()->selectFolder($this->folder);
return $this->client()->flags($uids,[$flags],$mod,true);
}
/**
* 设置为已读
* @param int|array $uids
* @return bool
* @throws \Exception
* @author:dc
* @time 2022/10/26 17:08
*/
public function setSeen($uids):bool{
// 选择目录
$status = $this->client()->selectFolder($this->folder);
return $this->client()->flags($uids,[\App\Http\Mail\lib\client\Imap::FLAGS_SEEN],'+',true);
}
/**
* 设置为未读
* @param $uids
* @return bool
* @throws \Exception
* @author:dc
* @time 2022/10/26 17:11
*/
public function delSeen($uids):bool{
// 选择目录
$status = $this->client()->selectFolder($this->folder);
return $this->client()->flags($uids,[\App\Http\Mail\lib\client\Imap::FLAGS_SEEN],'-',true);
}
/**
* 邮件列表
* @param array $mail_id
* @param array $folder
* @param int $total
* @return mixed|object
* @throws \Illuminate\Contracts\Container\BindingResolutionException
* @time 2022/8/16 17:34
*/
public function emailLists($mail_id = [], $folder = [], $total = 0, $search = []){
return EmailList::_paginate(function ($query) use ($mail_id, $folder,$search){
$query->whereIn('email_id',$mail_id)->whereIn('folder_id',$folder);
// 搜索
if(!empty($search['search'])){
$search = htmlspecialchars($search);
$query->where('subject','like',"%{$search}%");
}
// 已读
if(!empty($search['seen']) && $search['seen']){
$query->where('seen',0);
}
// 过滤
$query->where([
['subject','not like','%- snv'],
['subject','not like','%(Failure)'],
['subject','not like','%(Delay)']
]);
},20,$total);
}
/**
* 读取邮件详情
* @param $id
* @return \Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Model|object|null
* @time 2022/8/2 15:13
*/
public function emailInfo($id){
$email = EmailList::_firstWithBody($id);
// 是否存在,并且是否属于自己
if($email && in_array($email->email_id,$this->email_ids)){
if(!$email->body && $email['uid']){
$email = $email->toArray();
try {
$email['body'] = $this->syncEMailBody($email['uid'],$email['id']);
}catch (\Throwable $e){
$email['body'] = $this->syncEMailBody($email['uid'],$email['id']);
}
}
return $email;
}
return null;
}
/**
* 获取邮件服务器
* @param string $suffix
* @return array
* @time 2022/7/29 16:24
*/
public function host(string $suffix=''):array {
if($suffix){
return EmailHost::_get($suffix);
}
return EmailHost::_all();
}
/**
* 获取自己的服务器地址
* @param null $name
* @return array|mixed|string
* @time 2022/8/1 15:46
*/
public function hostMy($name=null) {
// if(!$this->host){
// $data = Email::_first($this->id);
// $this->host = [
// 'imap' => $data['imap'],
// 'smtp' => $data['smtp'],
// ];
// }
if($name){
return $this->host[$name]??'';
}
return $this->host;
}
/**
* 添加邮件服务器
* @param string $suffix
* @param string $imap
* @param string $smtp
* @return int email host 表的id
* @time 2022/7/29 16:28
*/
public function hostAdd(string $suffix, string $imap, string $smtp):int{
return EmailHost::_insert($suffix, $imap, $smtp);
}
/**
* 添加一个邮箱地址
* @time 2022/7/29 16:51
*/
public function emailAdd($data=[]){
$data = $data ? $data : $this->request->post();
if(!$data){
throw new \Exception(Lang::__('empty_form'));
}
$validator = Validator::make($data,[
'email' => ['required','email'],
'password' => ['required'],
],[
'email.required' => 'email_required',
'email.email' => 'email_validator',
'password.required' => 'password_required',
]);
if($validator->fails()){
throw new \Exception(Lang::__($validator->errors()->first()));
}
$data['email'] = trim($data['email']);
$data['password'] = trim($data['password']);
$data['email_name'] = trim($data['email_name']??$data['email']);
// 检查imap服务器
if($data['host']??''){
$host = [
'imap' => trim($data['host']),
'smtp' => trim($data['smtp']??'')
];
}else{
// 获取数据库中有的
$host = $this->host(explode('@',$data['email'])[1]);
}
if(!$host){
// 是否存在imap服务器
throw new \Exception(Lang::__('host_required'));
}
// 登录名
$this->username = $data['email'];
// 进行远程登录
// imap imap.qq.com
$imap = new \App\Http\Mail\lib\client\Imap();
$imap->login($this->ssl.$host['imap'].':'.$this->port,$data['email'],$data['password']);
// 添加邮箱,并绑定user_id
$model = Email::_add($this->user_id, $data['email'], $data['password'], $data['email_name'],$host);
try {
// 添加一个联系人的默认分组
$this->contactGroupSave('默认分组');
}catch (\Throwable $e){
}
// 退出imap登录
$imap->loginOut();
return $model;
// throw new \Exception(Lang::__('email_insert_error'));
}
/**
* 读取所有邮箱
* @return array
* @time 2022/8/1 9:34
*/
public function emails():array {
return Email::_get($this->user_id);
}
/**
* 文件夹
* @return array
* @time 2022/8/4 17:34
*/
public function folders($id){
$result = EmailFolder::_all($id);
if(!$result){
$result = $this->syncFolder();
}
return $result;
}
/**
* 联系人列表
* @return mixed
* @time 2022/8/4 9:05
*/
public function contact($is_group=false){
return EmailContact::_all($this->id,$is_group);
}
/**
* 联系人管理视图
* @return \Illuminate\Contracts\Foundation\Application|\Illuminate\Contracts\View\Factory|\Illuminate\View\View
* @author:dc
* @time 2022/11/2 16:37
*/
public function contact_view(){
return view('admin/email/contacts',[
// 'emails' => $this->emails(),
'email' => $this->username,
// 'groups' => $this->contactGroup(),
// 'contacts' => $this->contact()
]);
}
/**
* 添加联系人
* @return EmailContact
* @throws \Exception
* @time 2022/8/4 9:46
*/
public function contactAdd(){
$data = $this->request->post();
$validator = Validator::make($data,[
'group_id' => ['required'],
'email' => ['required','email'],
'email_name' => ['required','max:100'],
'remark' => ['max:200'],
],[
'group_id.required' => 'contact_group_exists',
'email.required' => 'contact_email_required',
'email.email' => 'contact_email_error',
'email_name.required' => 'contact_email_name_required',
'email_name.max' => 'contact_email_name_max',
'remark.max' => 'contact_remark_max',
]);
// 验证数据
if($validator->fails()){
$p = '';
switch ($validator->errors()->first()){
case 'contact_email_name_max': $p = '100';break;
case 'contact_remark_max': $p = '200';break;
}
throw new \Exception(
Lang::__(
$validator->errors()->first(),
$p
)
);
}
$group = EmailContactGroup::_first($data['group_id']);
if(!$group || $group['email_id']!=$this->id){
abort(600,Lang::__('contact_group_exists'));
}
return EmailContact::_save($this->id,$data);
}
/**
* 联系人详情
* @param $id
* @return array
* @author:dc
* @time 2022/11/4 17:02
*/
public function contactInfo($id){
return EmailContact::_first($this->id,$id);
}
/**
* 删除联系人
* @time 2022/8/4 9:52
*/
public function contactDel(){
$contact_id = (int) $this->request->get('contact_id');
if(!$contact_id || EmailContact::_del($this->id,$contact_id)){
abort(600,Lang::__('del_error'));
}
return true;
}
/**
* 联系人分组
* @return mixed
* @time 2022/8/4 10:13
*/
public function contactGroup($is_contact=false){
return EmailContactGroup::_all($this->id,$is_contact);
}
/**
* 添加联系人分组
* @return EmailContactGroup
* @throws \Exception
* @time 2022/8/4 10:47
*/
public function contactGroupSave($name,$id=0){
if(!$name || mb_strlen($name)>100){
throw new \Exception(Lang::__('contact_group_name_error',1,100));
}
$name = htmlspecialchars($name);
$data = EmailContactGroup::_firstByName($this->id,$name);
if(($data && !$id) || ($data && $id && $data['id'] != $id)){
throw new \Exception(Lang::__('contact_group_name_unique'));
}
return EmailContactGroup::_save($this->id,$name,$id);
}
/**
* 删除
* @return false|mixed
* @time 2022/8/4 10:49
*/
public function contactGroupDel($group_id){
if(!$group_id){
GROUP_DEL_ABORT:
abort(600,Lang::__('contact_group_del_id'));
}
// 是否有联系人在其中
if(EmailContact::_count($group_id, $this->id)){
abort(600,Lang::__('contact_group_del_contact'));
}
// 删除
if(!EmailContactGroup::_del($group_id, $this->id)){
goto GROUP_DEL_ABORT;
}
return true;
}
/**
* 发送邮件
* @time 2022/8/3 16:08
*/
public function send($data=[]){
$data = $data ? $data : $this->request->post();
$data['is_text'] = intval($data['is_text']??0);//纯文本
// $data['save_send'] = intval($data['save_send']??0);//保存到已发送
$data['priority'] = intval($data['priority']??0);//紧急
$data['receipt'] = intval($data['receipt']??0);//需要回执
$data['encrypt'] = intval($data['encrypt']??0);//加密邮件
// 是纯文本还是html
$data['body'] = $data['is_text'] ? ($data['text']??'') : ($data['html']??'');
// 是否是回复邮件
if(!empty($data['reply']['id']) && !empty($data['reply']['email_id'])){
// 标题,主题
if(empty($data['subject'])){
throw new \Exception(Lang::__('send_email_subject_error',500));
}
// 查询
$originData = EmailList::_first($data['reply']['id']);
// 是否存在邮件
if($originData && $originData['email_id'] == $data['reply']['email_id'] && in_array($originData['email_id'],$this->email_ids)){
$emaildata = Email::_firstById($originData['email_id']);
if(!$emaildata){
throw new \Exception('发件人异常',600);
}
try {
// 立刻发送邮件
MailFun::sendEmail(
$emaildata['smtp'],$emaildata['email'],$emaildata['password'],
$emaildata['email_name'],['email'=>$originData['from'],'name'=>$originData['from_name']]
,$data['subject'],$data['body'],$data['file']??[],$data['receipt'],$data['priority']?1:3
);
// 记录下
EmailLog::error(print_r([
'manage_id'=>the_manage('id'),
'data' => $data,
'回复邮件'
],true));
return true;
}catch (\Throwable $e){
throw new \Exception($e->getMessage());
}
}
}
// 收件人
if(empty($data['to'])){
SEND_EMAIL_TO_ERROR:
throw new \Exception(Lang::__('send_email_to_error'));
}
// 是否是数组
$data['to'] = explode("\n",trim($data['to']));
if(!$data['to']){
goto SEND_EMAIL_TO_ERROR;
}
// $to = [
// 'email' => 'xxx@qq.com',
// 'name' => 'xxx'
// ];
foreach ($data['to'] as $k=>$to){
$to = trim($to);
$data['to'][$to] = [];
$data['to'][$to]['email'] = $to;
$data['to'][$to]['name'] = explode('@',$to)[0];
// 是否是邮箱
if(!preg_match('/^\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$/',$data['to'][$to]['email'])){
// throw new \Exception(Lang::__('send_email_to_error',$data['to'][$to]['email']));
unset($data['to'][$to]);
}
unset($data['to'][$k]);
}
$data['to'] = array_values($data['to']);
if(!$data['to']){
goto SEND_EMAIL_TO_ERROR;
}
// 时间
if($data['send_time'] != 'now'){
$data['send_time'] = explode(',',$data['send_time']);
if($data['send_time'][0] < 24 && $data['send_time'][1] < 24){
$data['send_time'] = implode(',',$data['send_time']);
}else{
$data['send_time'] = '21,11';
}
}
$tags = get('tags');
if (!$tags || !is_array($tags)){
throw new \Exception('请选择tag标签');
}
// 加入任务
$job_id = EmailSendJob::_insert([
'title' => $data['subject']??'',
'to' => $data['to'],
'the_manage_id' => the_manage('id'),
'email_id' => $this->id,
'user_id' => $this->user_id,
'data' => $data,
'send_time' => $data['send_time'],
'tags' => $tags
]);
if($job_id){
// 发送一个任务
SendJob::dispatch($job_id);
return $job_id;
}
throw new \Exception(Lang::__('email_send_error'));
}
/**
* 下载附件
* @param $name
* @param string $originname
* @return string|\Symfony\Component\HttpFoundation\BinaryFileResponse
* @author:dc
* @time 2022/11/2 10:59
*/
public function download($name,$originname=''){
$file = $this->getFilePath().$name;
if(is_file($file)){
return response()->download($file,$originname ? : $name);
}
return '';
}
/**
* @param mixed $data
* @param string $message
* @param int $status
* @return \Illuminate\Http\JsonResponse
* @author:dc
* @time 2022/8/1 11:05
*/
private function echoJson($data='', $message='', $status=200):\Illuminate\Http\JsonResponse{
$data = [
'data' => $data,
'message' => $message,
'status' => $status
];
return response()->json($data,200,[]);
}
}
<?php
return $message = [
'imap_server_error' => 'IMAP server error: %s',
'login_host' => 'Enter the IMAP server address.',
'empty_form' => 'The form data is empty.',
'password_required' => 'The password field is required.',
'password_error' => 'wrong password.',
'host_required' => 'The IMAP server address must be.',
'email_required' => 'Email address must be.',
'email_validator' => 'Email format error.',
'email_insert_error' => 'Failed to Add Mailbox.',
'contact_email_required' => 'Contact email address must be specified.',
'contact_group_exists' => 'The selected group does not exist.',
'contact_email_error' => 'The email format of the contact is incorrect.',
'contact_email_name_required' => 'Contact name must be entered.',
'contact_email_name_max' => 'Contact name Maximum character %s.',
'contact_group_name_error' => 'The contact group must be longer than %s and smaller than %s characters.',
'contact_group_name_unique' => 'The contact group already exists.',
'contact_group_del_id' => 'Grouping does not exist.',
'contact_remark_max' => 'The contact remarks must contain %s characters.',
'contact_group_del_contact' => 'There are contacts in the group and cannot be deleted.',
'email_folder_not' => 'Folder (%s) does not exist.',
'email_not_bind' => 'Mailbox (%s) is not bound.',
'email_send_error' => 'Failed to send message (%s).',
'del_error' => 'fail to delete.',
'send_email_subject_error' => 'The message subject must be within the %s character.',
'send_email_to_error' => 'The recipient must or is formatted incorrectly, %s.',
];
<?php
return $message = [
'imap_server_error' => 'IMAP服务器错误: %s',
'login_host' => '请输入imap服务器地址',
'empty_form' => '表单数据为空',
'password_required' => '密码必须',
'password_error' => '密码错误',
'host_required' => 'imap服务器地址必须',
'email_required' => '邮箱必须',
'email_validator' => '邮箱格式错误',
'email_insert_error' => '添加邮箱失败',
'contact_email_required' => '联系人邮箱必须填写',
'contact_group_exists' => '选择的分组不存在',
'contact_email_error' => '联系人邮箱格式错误',
'contact_email_name_required' => '联系人姓名必须填写',
'contact_email_name_max' => '联系人姓名最大字符%s',
'contact_group_name_error' => '联系人分组必须大于%s且小于%s字符',
'contact_group_name_unique' => '联系人分组已存在',
'contact_group_del_id' => '分组不存在',
'contact_group_del_contact' => '分组下还有联系人,无法删除',
'contact_remark_max' => '联系人备注在%s字符内',
'email_folder_not' => '文件夹(%s)不存在',
'email_not_bind' => '邮箱(%s)未绑定',
'email_send_error' => '邮件发送失败(%s)',
'del_error' => '删除失败',
'send_email_subject_error' => '邮件主题必须且在%s字符内',
'send_email_to_error' => '收件人必须或格式错误,%s',
];
<?php
namespace App\Mail\lib;
/**
* 中文情况
* @time 2022/8/1 15:32
* Class ImapUtf7
* @package App\Mail\lib
*/
class ImapUtf7 {
static $imap_base64 =
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+,';
static private function encode_b64imap($s) {
$a=0; $al=0; $res=''; $n=strlen($s);
for($i=0;$i<$n;$i++) {
$a=($a<<8)|ord($s[$i]); $al+=8;
for(;$al>=6;$al-=6) $res.=self::$imap_base64[($a>>($al-6))&0x3F];
}
if ($al>0) { $res.=self::$imap_base64[($a<<(6-$al))&0x3F]; }
return $res;
}
static private function encode_utf8_char($w) {
if ($w&0x80000000) return '';
if ($w&0xFC000000) $n=5; else
if ($w&0xFFE00000) $n=4; else
if ($w&0xFFFF0000) $n=3; else
if ($w&0xFFFFF800) $n=2; else
if ($w&0xFFFFFF80) $n=1; else return chr($w);
$res=chr(( (255<<(7-$n)) | ($w>>($n*6)) )&255);
while(--$n>=0) $res.=chr((($w>>($n*6))&0x3F)|0x80);
return $res;
}
static private function decode_b64imap($s) {
$a=0; $al=0; $res=''; $n=strlen($s);
for($i=0;$i<$n;$i++) {
$k=strpos(self::$imap_base64,$s[$i]); if ($k===FALSE) continue;
$a=($a<<6)|$k; $al+=6;
if ($al>=8) { $res.=chr(($a>>($al-8))&255);$al-=8; }
}
$r2=''; $n=strlen($res);
for($i=0;$i<$n;$i++) {
$c=ord($res[$i]); $i++;
if ($i<$n) $c=($c<<8) | ord($res[$i]);
$r2.=self::encode_utf8_char($c);
}
return $r2;
}
static function encode($s) {
$n=strlen($s);$err=0;$buf='';$res='';
for($i=0;$i<$n;) {
$x=ord($s[$i++]);
if (($x&0x80)==0x00) { $r=$x;$w=0; }
else if (($x&0xE0)==0xC0) { $w=1; $r=$x &0x1F; }
else if (($x&0xF0)==0xE0) { $w=2; $r=$x &0x0F; }
else if (($x&0xF8)==0xF0) { $w=3; $r=$x &0x07; }
else if (($x&0xFC)==0xF8) { $w=4; $r=$x &0x03; }
else if (($x&0xFE)==0xFC) { $w=5; $r=$x &0x01; }
else if (($x&0xC0)==0x80) { $w=0; $r=-1; $err++; }
else { $w=0;$r=-2;$err++; }
for($k=0;$k<$w && $i<$n; $k++) {
$x=ord($s[$i++]); if ($x&0xE0!=0x80) { $err++; }
$r=($r<<6)|($x&0x3F);
}
if ($r<0x20 || $r>0x7E ) {
$buf.=chr(($r>>8)&0xFF); $buf.=chr($r&0xFF);
} else {
if (strlen($buf)) {
$res.='&'.self::encode_b64imap($buf).'-';
$buf='';
}
if ($r==0x26) { $res.='&-'; } else $res.=chr($r);
}
}
if (strlen($buf)) $res.='&'.self::encode_b64imap($buf).'-';
return $res;
}
static function decode($s) {
$res=''; $n=strlen($s); $h=0;
while($h<$n) {
$t=strpos($s,'&',$h); if ($t===false) $t=$n;
$res.=substr($s,$h,$t-$h); $h=$t+1; if ($h>=$n) break;
$t=strpos($s,'-',$h); if ($t===false) $t=$n;
$k=$t-$h;
if ($k==0) $res.='&';
else $res.=self::decode_b64imap(substr($s,$h,$k));
$h=$t+1;
}
return $res;
}
}
<?php
namespace App\Mail\lib;
/**
* @time 2022/8/3 9:42
* Class Lang
* @package App\Mail\lib
*/
class Lang {
public static $l;
public static $data;
/**
* @param $key
* @param mixed ...$var
* @return mixed
* @time 2022/8/3 9:41
*/
public static function __($key,...$var){
// 是否加载语言
if (!isset(self::$data[self::$l])) self::load();
// 获取语言
$str = self::$data[self::$l][$key]??self::$data['en'][$key]??$key;
// 数组填充到10个元素,避免出现元素不够而返回空字符
$var = array_pad($var,10,'');
// 替换
return @sprintf($str, ...$var);
}
/**
* 加载
* @param string $l
* @time 2022/8/4 15:55
*/
public static function load(string $l = ''){
self::$l = strtolower($l ? $l : app()->getLocale());
self::$data[self::$l] = require_once __DIR__.'/../lang/'.self::$l.'.php';
// default
if(!isset(self::$data['en'])){
self::$data['en'] = require_once __DIR__.'/../lang/en.php';
}
}
}
<?php
namespace App\Mail\lib\MailParse;
use App\Mail\lib\MailFun;
/**
* 解析邮件body内容
* 通过 fetch msgno RFC822.text 获取到的内容
* 此内容包含html 文本 附件
* @author:dc
* @time 2022/8/12 9:15
* Class Body
* @package App\Mail\lib
*/
class Body {
/**
* @var string
*/
private $body;
/**
*
* @var array
*/
private $item = [];
/**
* 保存的目录
* @var string
*/
private $fileSavePath;
/**
* Body constructor.
* @param string $body
* @param string $fileSavePath
*/
public function __construct(string $body, string $fileSavePath='/')
{
$this->body = $body = trim($body);
$this->fileSavePath = $fileSavePath;
// 这个是描述特殊文本
if(strpos($body,'This is a multi-part message in MIME format.')===0){
$body = trim($body,'This is a multi-part message in MIME format.');
$body = trim($body);
}
// 163 有
if(strpos($body,'------=_Part')!==false){
$this->parse($body,'------=_Part');
}
elseif (mb_strpos($body,'------=_NextPart')!==false){
$this->parse($body,'------=_NextPart');
}
elseif (mb_strpos($body,'----_NmP')!==false){
$this->parse($body,'----_NmP');
}
elseif (mb_strpos($body,'--_=_swift')!==false){
$this->parse($body,'--_=_swift');
}
elseif (mb_strpos($body,'----==_mimepart')!==false){
$this->parse($body,'----==_mimepart');
}
elseif (mb_strpos($body,'--------------Boundary')!==false){
$this->parse($body,'--------------Boundary');
}
elseif (mb_strpos($body,'--=-')!==false){
$this->parse($body,'--=-');
}
// 很多--开始的,且不规则
elseif(strpos($body,'--')===0){
// 获取第一行
$tag = $this->body_get_tag($body,'--');
// 以第一行为标准
$this->parse($body,trim($tag));
}
// 直接html
elseif (mb_strpos($body,'<')===0){
$body = quoted_printable_decode($body);
// preg_match("/<meta(?!\s*(?:name|value)\s*=)(?:[^>]*?content\s*=[\s\"']*)?([^>]*?)[\s\"';]*charset\s*=[\s\"']*([^\s\"'\/>]*)/",$body,$icon);
// if(!empty($icon[2])){
// // 解码
// $body = mb_convert_encoding($body,'utf-8',$icon[2]);
// }
$this->setItem(['type'=>'text/html','body'=>$body]);
}
else{
// qq的是base64
if(rtrim($body,'=') == rtrim(base64_encode(base64_decode($body)),'=')){
$this->setItem(['type'=>'text/plain','body'=>base64_decode($body)]);
}else{
$this->setItem(['type'=>'text/plain','body'=>$body]);
}
}
}
/**
* 获取标签
* @param $body
* @param $tag
* @return mixed|string
* @author:dc
* @time 2022/8/12 10:49
*/
private function body_get_tag($body,$tag){
preg_match("/{$tag}[\w\W].*/i",$body,$result);
if(!empty($result[0])) {
return $result[0];
}
return '';
}
/**
* @param $item
*/
private function setItem($item): void
{
$this->item[] = $item;
}
/**
* @return []
*/
public function getItem(): array
{
return $this->item;
}
/**
* 开始解析
* @param string $body
* @param string $tag
* @return array
* @author:dc
* @time 2022/8/12 9:50
*/
private function parse(string $body, string $tag){
// 删除第一个标签前面的数据,一般情况无用
$body = mb_substr($this->body,strpos($this->body,$tag),99999999999);
// 有附件的情况
preg_match('/boundary="([-_A-Za-z0-9=\.]{1,})"/i',$body,$boundary);
if($boundary[0]??''){
$body = str_replace($boundary[0],'',$body);
// $body = mb_substr($body,mb_strpos($body,$boundary[0])+strlen($boundary[0]),99999999999);
}
// 附件情况
if(!empty($boundary[1])){
preg_match_all('/.*'.$boundary[1].'.*/i',$body,$boundary_tag);
$body = str_replace($boundary_tag[0],'{--tag--}',$body);
}
// 查找tag块
preg_match_all("/(".$tag.".*+\n)/i",$body."\r\n\r\n",$he);
// 把每个tag块分开成数组
if(!empty($he[0])){
foreach ($he[0] as $hk=>$h){
$he[0][$hk] = trim($h);
}
arsort($he[0]);
$body = str_replace($he[0],'{--tag--}',$body);
}
$body = explode('{--tag--}',$body);
// 处理
foreach ($body as $key=>$item){
$data = [];
$item = trim($item);
// 附件的头
if(!$item) { continue; }
// 邮件体包含邮件体
if(preg_match("/boundary=\"([-_a-z0-9]{5,})\"/Ui",$item,$bm)){
if (strpos($item,$bm[1].'--')!==false){
$data = (new self('--'.$bm[1]."\r\n".$item,$this->fileSavePath))->getItem();
// $this->setItem($data);
// 合并邮件体
$this->item = array_merge($this->item,$data);
}
continue;
}
// 先解码解码
$encode = $this->body_match_tag('Content-Transfer-Encoding:',$item);
if($encode){
$data['encode'] = strtolower($encode['text']);
$item = str_replace($encode['origin'],'',$item);
}
// 内容类型
$type = $this->preg_match_type($item);
if($type){
$data['type'] = strtolower($type['type']);
// 编码
if(isset($type['charset'])){
$data['charset'] = strtolower($type['charset']);
}
// nama。附件
if(isset($type['name'])){
$data['name'] = $type['name'];
}
// 删除
$item = str_replace($type['origin'],'',$item);
}
//
if(empty($data['charset'])){
// 编码
$code = $this->preg_match_charset($item);
if($code){
$data['charset'] = strtolower($code['charset']);
$item = str_replace($code['origin'],'',$item);
}
}
// 先匹配留存文件名称
preg_match('/filename="(\w?.*)"/',$item,$filename);
if(!empty($filename[1])){
$filename = MailFun::decodeMimeStr($filename[1]);
}
// 删除不需要的tag属性,如果需要进进行解析
$item = $this->body_remove_tag($item,'Content-Description:');
$item = $this->body_remove_tag($item,'Content-Disposition:');
$item = $this->body_remove_tag($item,'Mime-Version:');
$data['body'] = trim($item);
if(!empty($data['type'])){
// 邮件头
if($data['type'] == 'multipart/alternative'){
}
// 是文本还是附件
else if(strpos($data['type'],'text/') === 0 ){
// body解密
switch($data['encode']??''){
case 'base64': {
$data['body'] = base64_decode($data['body']);
break;
}
case 'quoted-printable': {
$data['body'] = quoted_printable_decode($data['body']);
break;
}
case '8bit': {
try {
$data['body'] = DeCoding::de8bit($data['body']);
$data['body'] = quoted_printable_decode($data['body']);
}catch (\Throwable $e){
}
break;
}
}
// 转码
// if(isset($data['charset']) && $data['charset']){
// $debody = @mb_convert_encoding($data['body'],'utf-8',$data['charset']);
// if($debody){
// $data['body'] = $debody;
// $debody = null;
// }
// }
}
// 系统退信//里面包含了发送邮件所有内容,这里不记录
elseif (strpos($data['type'],'message') === 0){
$data['body'] = '';// 一般不需要这些内容,如有需要就要重新解析
}
elseif (!empty($data['type']) && $data['body']){
// 解析附件
$data = $this->parseFile($data,$filename);
}
}
$this->setItem($data);
}
}
/**
* 解析文件
* @param $item
* @return array|mixed
* @author:dc
* @time 2022/8/12 10:40
*/
private function parseFile($item,$filename=''){
$data = [];
// 查找文件名
$data['filename'] = $this->file_save_name($item['body'],'filename');
$data['name'] = $this->file_save_name($item['body'],'name');
$data['name'] = $data['name'] ? : ($item['name']??$filename);
$data['filename'] = $data['filename'] ? : $data['name'];
// 是否有文件名
if(empty($data['filename']) || strpos($data['filename'],'.')===false){
return $item;
}
$ext = explode('.',$data['filename']);
$ext = end($ext);
// if(!empty($item['type'])){
// // 文件类型来判断后缀
// // // download it from http://svn.apache.org/repos/asf/httpd/httpd/trunk/docs/conf/mime.types
// if(is_readable(__DIR__.'/mime.types')){
// $f = fopen(__DIR__.'/mime.types','r');
// while(!feof($f)){
// $fext = fgets($f);
// if($fext){
// $fext = strtolower($fext);
// $item['type'] = strtolower($item['type']);
// // 找到了类型后缀
// if(strpos($fext,$item['type']) === 0){
// $ext = trim(str_replace($item['type'],'',$fext));
// break;//找到了要跳出循环
// }
// }
// }
// // 关闭文件
// fclose($f);
// }
// }
// 找不到后缀,说明不是文件
// if(empty($ext)){
// 文件后缀
// $ext = explode('.',$data['filename']);
// $ext = count($ext) > 1 ? ($ext[count($ext)-1]??'') : '';
// 直接返回
// return $item['body'];
// }
// content id
preg_match("/Content-ID:[\s].*<[\w\W]{1,}>/i",$item['body'],$result);
if (!empty($result[0])){
$data['content-id'] = explode('<',$result[0]);
$data['content-id'] = $data['content-id'][1];
$data['content-id'] = trim($data['content-id']);
$data['content-id'] = trim($data['content-id'],'>');
$item['body'] = str_replace($result[0],'',$item['body']);
}
$item['body'] = str_replace($result,'',$item['body']);
$content = base64_decode(trim($item['body']));
if($content){
// 目录
$data['path'] = $this->fileSavePath;
if(!is_dir($data['path'])){
mkdir($data['path'],0775,true);
}
$data['signName'] = md5($content).($ext ? '.'.$ext : '');
$data['path'] = $data['path'].'/'.$data['signName'];
// 保存文件
@file_put_contents($data['path'],$content);
}
return $data;
}
// 获取文件名称
private function file_save_name(&$body,$tag){
preg_match('/'.$tag.'="[(\S\W.*\s.*)]{1,}"/i',$body,$result);
if($result[0]??''){
$body = str_replace($result[0],'',$body);
}
$val = trim(str_replace([$tag.'=','"',"'"],'',$result[0]??''));
if ($val && strpos($val,'=?')===0){
$val = iconv_mime_decode($val,ICONV_MIME_DECODE_CONTINUE_ON_ERROR,'utf-8');
}
return $val;
}
/**
* 删除tag
* @param $body
* @param $tag
* @return mixed|string|string[]
* @author:dc
* @time 2022/8/12 10:34
*/
private function body_remove_tag($body,$tag){
preg_match("/{$tag}[\w\W].*/i",$body,$result);
if(!empty($result[0])) {
$body = str_replace($result, '', $body);
}
return $body;
}
/**
* 读取编码
* @param $item
* @return array
* @author:dc
* @time 2022/8/12 10:28
*/
private static function preg_match_charset($item){
// 匹配内容 type
preg_match('/charset[ \t]{0,}=[ \t]{0,}"?[ \t0-9a-zA-Z-]{1,}"?/i',$item,$result);
if(!empty($result[0])){
$ret['origin'] = trim($result[0]);
// charset
$ret['charset'] = trim(str_replace(['charset','=','"',"'"],'',$ret['origin']));
return $ret;
}
return [];
}
/**
* 解析type
* @param $item
* @return array
* @author:dc
* @time 2022/8/12 10:26
*/
private function preg_match_type($item){
// 匹配内容 type
preg_match("/Content-Type:[\w\W].*/i",$item,$result);
if(!empty($result[0])){
$ret['origin'] = trim($result[0]);
// type
$type = str_replace(['Content-Type:','"',"'"],'',$ret['origin']);
$type = explode(';',$type);
// 类型
$ret['type'] = trim($type[0]);
if(isset($type[1]) && $type[1]){
// 编码
$r = explode('=',$type[1]);
$ret[strtolower(trim($r[0]))] = trim($r[1]??'');
}
return $ret;
}
return [];
}
/**
* 匹配tag
* @param $tag
* @param $item
* @return array
* @author:dc
* @time 2022/8/12 10:05
*/
private function body_match_tag($tag,$item){
// tag Content-Transfer-Encoding:
preg_match("/".$tag."[\w\W].*/i",$item,$result);
if(!empty($result[0])){
$ret['origin'] = trim($result[0]);
// charset
$ret['text'] = trim(str_replace([$tag,'"',"'"],'',$ret['origin']));
return $ret;
}
return [];
}
}
<?php
namespace App\Mail\lib\MailParse;
/**
* 解码邮件内容
* @author:dc
* @time 2022/8/12 9:33
* Class DeCoding
* @package App\Mail\lib\MailParse
*/
class DeCoding {
/**
* @param $sText
* @param bool $bEmulate_imap_8bit
* @return string
* @author:dc
* @time 2022/8/12 9:34
*/
public static function de8bit($sText,$bEmulate_imap_8bit=true) {
// split text into lines
$aLines=explode(chr(13).chr(10),$sText);
for ($i=0;$i<count($aLines);$i++) {
$sLine =& $aLines[$i];
if (strlen($sLine)===0) continue; // do nothing, if empty
$sRegExp = '/[^\x09\x20\x21-\x3C\x3E-\x7E]/e';
// imap_8bit encodes x09 everywhere, not only at lineends,
// for EBCDIC safeness encode !"#$@[\]^`{|}~,
// for complete safeness encode every character :)
if ($bEmulate_imap_8bit)
$sRegExp = '/[^\x20\x21-\x3C\x3E-\x7E]/e';
$sReplmt = 'sprintf( "=%02X", ord ( "$0" ) ) ;';
$sLine = preg_replace( $sRegExp, $sReplmt, $sLine );
// encode x09,x20 at lineends
{
$iLength = strlen($sLine);
$iLastChar = ord($sLine{$iLength-1});
// !!!!!!!!
// imap_8_bit does not encode x20 at the very end of a text,
// here is, where I don't agree with imap_8_bit,
// please correct me, if I'm wrong,
// or comment next line for RFC2045 conformance, if you like
if (!($bEmulate_imap_8bit && ($i==count($aLines)-1)))
if (($iLastChar==0x09)||($iLastChar==0x20)) {
$sLine{$iLength-1}='=';
$sLine .= ($iLastChar==0x09)?'09':'20';
}
} // imap_8bit encodes x20 before chr(13), too
// although IMHO not requested by RFC2045, why not do it safer :)
// and why not encode any x20 around chr(10) or chr(13)
if ($bEmulate_imap_8bit) {
$sLine=str_replace(' =0D','=20=0D',$sLine);
//$sLine=str_replace(' =0A','=20=0A',$sLine);
//$sLine=str_replace('=0D ','=0D=20',$sLine);
//$sLine=str_replace('=0A ','=0A=20',$sLine);
}
// finally split into softlines no longer than 76 chars,
// for even more safeness one could encode x09,x20
// at the very first character of the line
// and after soft linebreaks, as well,
// but this wouldn't be caught by such an easy RegExp
preg_match_all( '/.{1,73}([^=]{0,2})?/', $sLine, $aMatch );
$sLine = implode( '=' . chr(13).chr(10), $aMatch[0] ); // add soft crlf's
}
// join lines into text
return implode(chr(13).chr(10),$aLines);
}
}