作者 邓超

xxx

要显示太多修改。

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

root = true
[*]
charset = utf-8
end_of_line = lf
indent_size = 4
indent_style = space
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[docker-compose.yml]
indent_size = 4
... ...
APP_NAME=Laravel
APP_ENV=local
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
LOG_CHANNEL=stack
LOG_DEPRECATIONS_CHANNEL=null
LOG_LEVEL=debug
DB_CONNECTION=mysql
DB_HOST=127.0.0.1
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=root
DB_PASSWORD=
BROADCAST_DRIVER=log
CACHE_DRIVER=file
FILESYSTEM_DISK=local
QUEUE_CONNECTION=sync
SESSION_DRIVER=file
SESSION_LIFETIME=120
MEMCACHED_HOST=127.0.0.1
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
MAIL_MAILER=smtp
MAIL_HOST=mailpit
MAIL_PORT=1025
MAIL_USERNAME=null
MAIL_PASSWORD=null
MAIL_ENCRYPTION=null
MAIL_FROM_ADDRESS="hello@example.com"
MAIL_FROM_NAME="${APP_NAME}"
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_DEFAULT_REGION=us-east-1
AWS_BUCKET=
AWS_USE_PATH_STYLE_ENDPOINT=false
PUSHER_APP_ID=
PUSHER_APP_KEY=
PUSHER_APP_SECRET=
PUSHER_HOST=
PUSHER_PORT=443
PUSHER_SCHEME=https
PUSHER_APP_CLUSTER=mt1
VITE_PUSHER_APP_KEY="${PUSHER_APP_KEY}"
VITE_PUSHER_HOST="${PUSHER_HOST}"
VITE_PUSHER_PORT="${PUSHER_PORT}"
VITE_PUSHER_SCHEME="${PUSHER_SCHEME}"
VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
... ...
* text=auto
*.blade.php diff=html
*.css diff=css
*.html diff=html
*.md diff=markdown
*.php diff=php
/.github export-ignore
CHANGELOG.md export-ignore
.styleci.yml export-ignore
... ...
/node_modules
/public/build
/public/hot
/public/storage
/storage/*.key
/vendor
.env
.env.backup
.env.production
.phpunit.result.cache
Homestead.json
Homestead.yaml
auth.json
npm-debug.log
yarn-error.log
/.fleet
/.idea
/.vscode
... ...
<p align="center"><a href="https://laravel.com" target="_blank"><img src="https://raw.githubusercontent.com/laravel/art/master/logo-lockup/5%20SVG/2%20CMYK/1%20Full%20Color/laravel-logolockup-cmyk-red.svg" width="400" alt="Laravel Logo"></a></p>
<p align="center">
<a href="https://github.com/laravel/framework/actions"><img src="https://github.com/laravel/framework/workflows/tests/badge.svg" alt="Build Status"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/dt/laravel/framework" alt="Total Downloads"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/v/laravel/framework" alt="Latest Stable Version"></a>
<a href="https://packagist.org/packages/laravel/framework"><img src="https://img.shields.io/packagist/l/laravel/framework" alt="License"></a>
</p>
## About Laravel
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
- [Simple, fast routing engine](https://laravel.com/docs/routing).
- [Powerful dependency injection container](https://laravel.com/docs/container).
- Multiple back-ends for [session](https://laravel.com/docs/session) and [cache](https://laravel.com/docs/cache) storage.
- Expressive, intuitive [database ORM](https://laravel.com/docs/eloquent).
- Database agnostic [schema migrations](https://laravel.com/docs/migrations).
- [Robust background job processing](https://laravel.com/docs/queues).
- [Real-time event broadcasting](https://laravel.com/docs/broadcasting).
Laravel is accessible, powerful, and provides tools required for large, robust applications.
## Learning Laravel
Laravel has the most extensive and thorough [documentation](https://laravel.com/docs) and video tutorial library of all modern web application frameworks, making it a breeze to get started with the framework.
You may also try the [Laravel Bootcamp](https://bootcamp.laravel.com), where you will be guided through building a modern Laravel application from scratch.
If you don't feel like reading, [Laracasts](https://laracasts.com) can help. Laracasts contains over 2000 video tutorials on a range of topics including Laravel, modern PHP, unit testing, and JavaScript. Boost your skills by digging into our comprehensive video library.
## Laravel Sponsors
We would like to extend our thanks to the following sponsors for funding Laravel development. If you are interested in becoming a sponsor, please visit the Laravel [Patreon page](https://patreon.com/taylorotwell).
### Premium Partners
- **[Vehikl](https://vehikl.com/)**
- **[Tighten Co.](https://tighten.co)**
- **[Kirschbaum Development Group](https://kirschbaumdevelopment.com)**
- **[64 Robots](https://64robots.com)**
- **[Cubet Techno Labs](https://cubettech.com)**
- **[Cyber-Duck](https://cyber-duck.co.uk)**
- **[Many](https://www.many.co.uk)**
- **[Webdock, Fast VPS Hosting](https://www.webdock.io/en)**
- **[DevSquad](https://devsquad.com)**
- **[Curotec](https://www.curotec.com/services/technologies/laravel/)**
- **[OP.GG](https://op.gg)**
- **[WebReinvent](https://webreinvent.com/?utm_source=laravel&utm_medium=github&utm_campaign=patreon-sponsors)**
- **[Lendio](https://lendio.com)**
## Contributing
Thank you for considering contributing to the Laravel framework! The contribution guide can be found in the [Laravel documentation](https://laravel.com/docs/contributions).
## Code of Conduct
In order to ensure that the Laravel community is welcoming to all, please review and abide by the [Code of Conduct](https://laravel.com/docs/contributions#code-of-conduct).
## Security Vulnerabilities
If you discover a security vulnerability within Laravel, please send an e-mail to Taylor Otwell via [taylor@laravel.com](mailto:taylor@laravel.com). All security vulnerabilities will be promptly addressed.
## License
The Laravel framework is open-sourced software licensed under the [MIT license](https://opensource.org/licenses/MIT).
... ...
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use function Co\run;
class Demo extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'demo';
/**
* The console command description.
*
* @var string
*/
protected $description = '测试命令';
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
// run(function (){
//
// });
return Command::SUCCESS;
}
}
... ...
<?php
namespace App\Console;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Foundation\Console\Kernel as ConsoleKernel;
class Kernel extends ConsoleKernel
{
/**
* Define the application's command schedule.
*
* @param \Illuminate\Console\Scheduling\Schedule $schedule
* @return void
*/
protected function schedule(Schedule $schedule)
{
// $schedule->command('inspire')->hourly();
}
/**
* Register the commands for the application.
*
* @return void
*/
protected function commands()
{
$this->load(__DIR__.'/Commands');
require base_path('routes/console.php');
}
}
... ...
<?php
namespace App\Exceptions;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Throwable;
class Handler extends ExceptionHandler
{
/**
* A list of exception types with their corresponding custom log levels.
*
* @var array<class-string<\Throwable>, \Psr\Log\LogLevel::*>
*/
protected $levels = [
//
];
/**
* A list of the exception types that are not reported.
*
* @var array<int, class-string<\Throwable>>
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed to the session on validation exceptions.
*
* @var array<int, string>
*/
protected $dontFlash = [
'current_password',
'password',
'password_confirmation',
];
/**
* Register the exception handling callbacks for the application.
*
* @return void
*/
public function register()
{
$this->reportable(function (Throwable $e) {
//
});
}
}
... ...
<?php
namespace App\Http\Controllers;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Foundation\Bus\DispatchesJobs;
use Illuminate\Foundation\Validation\ValidatesRequests;
use Illuminate\Routing\Controller as BaseController;
class Controller extends BaseController
{
use AuthorizesRequests, DispatchesJobs, ValidatesRequests;
}
... ...
<?php
namespace App\Http\Controllers;
use App\Models\Email;
use App\Models\Host;
use Helper\Fun;
use Helper\Mail\Imap;
/**
* 提供邮件各项数据
* @author:dc
* @time 2023/2/4 11:18
* Class MailApi
* @package App\Http\Controllers
*/
class MailApi
{
/**
* 添加新的邮箱
* @author:dc
* @time 2023/2/4 15:37
*/
public function login(){
// $mail,$password,$imap,$smtp
$formData = request()->only(['email','password','imap','smtp']);
$validator = validator($formData,[
'email' => ['required','email'],
'password' => ['required','min:8','max:32'],
'imap' => ['required'],
'smtp' => ['required'],
],[
]);
if($validator->fails()){
Fun::response()
->message($validator->errors()->first())
->status(400)
->throw();
}
// host
$model = Email::_first($formData['email']);
if(!$model){
$model = new Email();
$model->email = $formData['email'];
}
$model->imap = $formData['imap'];
$model->smtp = $formData['smtp'];
$model->password = encrypt($formData['password']);
$imap = new Imap();
// 是否初始成功
try {
$imap->login("ssl://{$formData['imap']}:993",$model->email,$model->password);
}catch (\Throwable $e){
Fun::response()
->message($e->getMessage())
->status(400)
->throw();
}
// 登录成功了,密码验证字段通过
$model->pass_error = 0;
// 保存好邮箱
$model->save();
// 开始同步文件夹
$folder = $imap->getFolder();
Fun::response()
->data($folder)
->throw();
}
/**
* 读取服务器上已记录的各个邮箱的服务器地址
* @author:dc
* @time 2023/2/4 15:12
*/
public function host(){
$host = Host::_all();
Fun::response()->data($host)->throw();
}
public function lists(){
}
}
... ...
<?php
namespace App\Http;
use Illuminate\Foundation\Http\Kernel as HttpKernel;
class Kernel extends HttpKernel
{
/**
* The application's global HTTP middleware stack.
*
* These middleware are run during every request to your application.
*
* @var array<int, class-string|string>
*/
protected $middleware = [
// \App\Http\Middleware\TrustHosts::class,
\App\Http\Middleware\TrustProxies::class,
\Illuminate\Http\Middleware\HandleCors::class,
\App\Http\Middleware\PreventRequestsDuringMaintenance::class,
\Illuminate\Foundation\Http\Middleware\ValidatePostSize::class,
\App\Http\Middleware\TrimStrings::class,
\Illuminate\Foundation\Http\Middleware\ConvertEmptyStringsToNull::class,
];
/**
* The application's route middleware groups.
*
* @var array<string, array<int, class-string|string>>
*/
protected $middlewareGroups = [
'web' => [
\App\Http\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\App\Http\Middleware\VerifyCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
'api' => [
// \Laravel\Sanctum\Http\Middleware\EnsureFrontendRequestsAreStateful::class,
'throttle:api',
\Illuminate\Routing\Middleware\SubstituteBindings::class,
],
];
/**
* The application's route middleware.
*
* These middleware may be assigned to groups or used individually.
*
* @var array<string, class-string|string>
*/
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'auth.session' => \Illuminate\Session\Middleware\AuthenticateSession::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \App\Http\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
];
}
... ...
<?php
namespace App\Http\Middleware;
use Illuminate\Auth\Middleware\Authenticate as Middleware;
class Authenticate extends Middleware
{
/**
* Get the path the user should be redirected to when they are not authenticated.
*
* @param \Illuminate\Http\Request $request
* @return string|null
*/
protected function redirectTo($request)
{
if (! $request->expectsJson()) {
return route('login');
}
}
}
... ...
<?php
namespace App\Http\Middleware;
use Illuminate\Cookie\Middleware\EncryptCookies as Middleware;
class EncryptCookies extends Middleware
{
/**
* The names of the cookies that should not be encrypted.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
... ...
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\PreventRequestsDuringMaintenance as Middleware;
class PreventRequestsDuringMaintenance extends Middleware
{
/**
* The URIs that should be reachable while maintenance mode is enabled.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
... ...
<?php
namespace App\Http\Middleware;
use App\Providers\RouteServiceProvider;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Auth;
class RedirectIfAuthenticated
{
/**
* Handle an incoming request.
*
* @param \Illuminate\Http\Request $request
* @param \Closure(\Illuminate\Http\Request): (\Illuminate\Http\Response|\Illuminate\Http\RedirectResponse) $next
* @param string|null ...$guards
* @return \Illuminate\Http\Response|\Illuminate\Http\RedirectResponse
*/
public function handle(Request $request, Closure $next, ...$guards)
{
$guards = empty($guards) ? [null] : $guards;
foreach ($guards as $guard) {
if (Auth::guard($guard)->check()) {
return redirect(RouteServiceProvider::HOME);
}
}
return $next($request);
}
}
... ...
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\TrimStrings as Middleware;
class TrimStrings extends Middleware
{
/**
* The names of the attributes that should not be trimmed.
*
* @var array<int, string>
*/
protected $except = [
'current_password',
'password',
'password_confirmation',
];
}
... ...
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustHosts as Middleware;
class TrustHosts extends Middleware
{
/**
* Get the host patterns that should be trusted.
*
* @return array<int, string|null>
*/
public function hosts()
{
return [
$this->allSubdomainsOfApplicationUrl(),
];
}
}
... ...
<?php
namespace App\Http\Middleware;
use Illuminate\Http\Middleware\TrustProxies as Middleware;
use Illuminate\Http\Request;
class TrustProxies extends Middleware
{
/**
* The trusted proxies for this application.
*
* @var array<int, string>|string|null
*/
protected $proxies;
/**
* The headers that should be used to detect proxies.
*
* @var int
*/
protected $headers =
Request::HEADER_X_FORWARDED_FOR |
Request::HEADER_X_FORWARDED_HOST |
Request::HEADER_X_FORWARDED_PORT |
Request::HEADER_X_FORWARDED_PROTO |
Request::HEADER_X_FORWARDED_AWS_ELB;
}
... ...
<?php
namespace App\Http\Middleware;
use Illuminate\Routing\Middleware\ValidateSignature as Middleware;
class ValidateSignature extends Middleware
{
/**
* The names of the query string parameters that should be ignored.
*
* @var array<int, string>
*/
protected $except = [
// 'fbclid',
// 'utm_campaign',
// 'utm_content',
// 'utm_medium',
// 'utm_source',
// 'utm_term',
];
}
... ...
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array<int, string>
*/
protected $except = [
//
];
}
... ...
<?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;
use PHPMailer\PHPMailer\PHPMailer;
use PHPMailer\PHPMailer\SMTP;
/**
* 函数
* @time 2022/8/1 16:02
* Class MailFun
* @package App\Mail\lib
*/
class MailFun {
/**
* 列表 转 树数据
* @param $list
* @param string $pk
* @param string $pid
* @param string $child
* @param int $root
* @param bool $empty_child
* @return array
* @time 2022/8/1 16:05
*/
public static function list2Tree($list, $pk='id',$pid = 'pid',$child = '_child',$root=0, $empty_child=true) {
// 创建Tree
$tree = array();
if(is_array($list)) {
// 创建基于主键的数组引用
$refer = array();
foreach ($list as $key => $data) {
if($empty_child){
$list[$key][$child] = [];
}
$refer[$data[$pk]] = &$list[$key];
}
foreach ($list as $key => $data) {
// 判断是否存在parent
$parentId = $data[$pid];
if ($root == $parentId) {
$tree[] = &$list[$key];
}else{
if (isset($refer[$parentId])) {
$refer[$parentId][$child][] = &$list[$key];
}
}
}
}
return $tree;
}
/**
* json encode
* @param $data
* @param int $option
* @return false|string
* @time 2022/8/2 15:57
*/
public static function json_en($data,$option=\JSON_UNESCAPED_UNICODE){
return \json_encode($data,$option);
}
/**
* 解码
* @param $string
* @param string $charset
* @return string
* @time 2022/8/15 9:31
*/
public static function decodeMimeStr($string, $charset = 'utf-8') {
$newString = '';
$elements = imap_mime_header_decode($string);
// print_r($elements);
for($i = 0; $i < count($elements); $i++) {
if($elements[$i]->charset == 'default') {
$elements[$i]->charset = 'iso-8859-1';
}
$newString .= self::convertStringEncoding($elements[$i]->text, $elements[$i]->charset, $charset);
}
return $newString;
}
public static function convertStringEncoding($string, $fromEncoding, $toEncoding) {
$convertedString = null;
if($string && $fromEncoding != $toEncoding) {
$convertedString = @iconv($fromEncoding, $toEncoding . '//IGNORE', $string);
if(!$convertedString && extension_loaded('mbstring')) {
$convertedString = @mb_convert_encoding($string, $toEncoding, $fromEncoding);
}
}
return $convertedString ?: $string;
}
/**
* 验证是否有附件 BODYSTRUCTURE值
* @param array $BODYSTRUCTURE
* @return int
* @author:dc
* @time 2022/11/1 10:57
*/
public static function isFile(array $BODYSTRUCTURE):int {
// foreach ($BODYSTRUCTURE as $item){
// if($item[0] === 'APPLICATION'){
// return 1;
// }
// }
// return 0;
$json = json_encode($BODYSTRUCTURE);
return strpos($json,'"attachment"')!==false;
}
/**
* 邮件收件人/发件人
* @param $str
* @return array
* @author:dc
* @time 2022/11/8 9:36
*/
public static function toOrFrom($str){
$strs = explode(',',$str);
foreach ($strs as $k=>$s){
preg_match('/\w+([-+.]\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*/',$s,$email);
if(empty($email[0])){
$s = [
'email' => '',
'name' => $s
];
}else{
$s = str_replace([$email[0],'"','<','>','&gt;','&lt;'],'',$s);
$s = trim($s);
$s = [
'email' => $email[0],
'name' => $s
];
}
if(empty($s['name'])){
$s['name'] = explode('@',$s['email'])[0]??'';
}
if(!empty($s['email'])){
$strs[$k] = $s;
}else{
unset($strs[$k]);
}
}
return $strs;
}
/**
* @param string $smtp smtp服务器地址
* @param string $username 发件人
* @param string $password 发件人密码
* @param string $nickname 昵称
* @param string|array $to_email 收件人,邮件或['email'=>'','name'=>'']
* @param string $subject 标题,主题
* @param string $body 文本内容
* @param array $files 文件 ['origin_name'=>'','path'=>'']
* @param false $receipt 是否回执
* @param int $priority 是否紧急 1紧急 3正常 5慢
* @return bool
* @throws \PHPMailer\PHPMailer\Exception
* @author:dc
* @time 2022/11/11 14:26
*/
public static function sendEmail(string $smtp,string $username,string $password,string $nickname,$to_email,string $subject,string $body,$files=[],$receipt=false,$priority=3){
// 邮件对象
$mail = new PHPMailer(true);
//Server settings
$mail->SMTPDebug = SMTP::DEBUG_CLIENT;//调试输出 SMTP::DEBUG_SERVER; //Enable verbose debug output
$mail->isSMTP(); //Send using SMTP
$mail->Host = $smtp; //Set the SMTP server to send through
$mail->SMTPAuth = true; //Enable SMTP authentication
$mail->Username = $username; //SMTP username
$mail->Password = $password; //SMTP password
$mail->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS; //Enable implicit TLS encryption
$mail->Port = 465; //TCP port to connect to; use 587 if you have set `SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS`
$mail->CharSet = 'utf-8';
$mail->Encoding = PHPMailer::ENCODING_QUOTED_PRINTABLE;
//Recipients,设置发件人
$mail->setFrom($username, $nickname);// 显示邮件来自谁
// //Add a recipient,设置收件人 这里必须是一对一发送
if(is_array($to_email)){
$mail->addAddress($to_email['email'], $to_email['name']);
}else{
$mail->addAddress($to_email, '');
}
// //回复到那个邮件
// $mail->addAddress($reply_to['email'], $reply_to['name']); //Add a recipient
// // 抄送
// $mail->addCC($cc['email'],$cc['name']);//
// // 密送
// $mail->addBCC($bcc['email'],$bcc['name']);
//Attachments 附件
if($files){
foreach ($files as $file){
// 添加到邮箱中
$mail->addAttachment($file['path'], $file['origin_name']); //Add attachments
}
}
// 回执,阅读后收回执的邮箱
if($receipt){
$mail->ConfirmReadingTo = $receipt;
}
// 是否紧急邮件
// Options: null (default), 1 = High, 3 = Normal, 5 = low.
$mail->Priority = $priority;
//Content 主题,标题
$mail->Subject = $subject;
$mail->isHTML(true); //Set email format to HTML
$mail->Body = $body;// html格式的内容
// 发送
if($mail->send()){
return true;
}
throw new \Exception($mail->ErrorInfo,500);
}
}
... ...
<?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);
}
}
... ...