refactor(mail): 邮件发送弃用 guanguans/notify 改用 symfony/mailer

guanguans/notify 在本项目仅用于 SMTP 发信,但其 1.x 线已停更、email 渠道
自 3.x 起被上游移除(无升级路径)。改用项目已自带的 symfony/mailer(Laravel
13 传递依赖),零新增依赖,并一并移除孤儿依赖 overtrue/http、symfony/options-resolver。

- EmailNoticeTask / UserEmailVerification / SystemController 三处发信改为
  new Mailer(Transport::fromDsn(...)) + new Email();API 1:1 等价
  (from/to/subject/html 同名,verify_peer=0 仍受 symfony 8.x 支持,
  notify 本就裸调 symfony 故异常透传不变、getCode()===550 仍成立)
- 移除 UserTransfer 未使用的 notify import
- 顺带修复既有 bug:超时判断字面量 "Timed Out" 与 symfony 实际消息
  "timed out" 大小写不匹配,改 stripos 大小写不敏感

验证:phpstan 0 错误、composer audit 无公告;邮箱验证码、系统邮件测试两条
链路实测发信成功。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
kuaifan 2026-06-15 06:10:47 +00:00
parent 468abe9902
commit 2f8dee44c2
6 changed files with 36 additions and 319 deletions

View File

@ -17,12 +17,13 @@ use App\Module\Timer;
use App\Models\Setting;
use LdapRecord\Container;
use App\Module\BillExport;
use Guanguans\Notify\Factory;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mime\Email;
use App\Models\UserCheckinRecord;
use App\Module\Apps;
use App\Module\BillMultipleExport;
use LdapRecord\LdapRecordException;
use Guanguans\Notify\Messages\EmailMessage;
use Swoole\Coroutine;
/**
@ -1232,21 +1233,19 @@ class SystemController extends AbstractController
}
try {
Setting::validateAddr($all['to'], function($to) use ($all) {
Factory::mailer()
->setDsn("smtp://{$all['account']}:{$all['password']}@{$all['smtp_server']}:{$all['port']}?verify_peer=0")
->setMessage(EmailMessage::create()
->from(Base::settingFind('system', 'system_alias', 'Task') . " <{$all['account']}>")
->to($to)
->subject('Mail sending test')
->html('<p>' . Doo::translate('收到此电子邮件意味着您的邮箱配置正确。') . '</p>'))
->send();
$mailer = new Mailer(Transport::fromDsn("smtp://{$all['account']}:{$all['password']}@{$all['smtp_server']}:{$all['port']}?verify_peer=0"));
$mailer->send((new Email())
->from(Base::settingFind('system', 'system_alias', 'Task') . " <{$all['account']}>")
->to($to)
->subject('Mail sending test')
->html('<p>' . Doo::translate('收到此电子邮件意味着您的邮箱配置正确。') . '</p>'));
}, function () {
throw new \Exception("收件人地址错误或已被忽略");
});
return Base::retSuccess('成功发送');
} catch (\Throwable $e) {
// 一般是请求超时
if (str_contains($e->getMessage(), "Timed Out")) {
if (stripos($e->getMessage(), "timed out") !== false) {
return Base::retError("邮件发送超时,请检查邮箱配置是否正确");
} elseif ($e->getCode() === 550) {
return Base::retError('邮件内容被拒绝,请检查邮箱是否开启接收功能');

View File

@ -7,8 +7,9 @@ use App\Module\Base;
use App\Module\Doo;
use App\Module\Timer;
use Carbon\Carbon;
use Guanguans\Notify\Factory;
use Guanguans\Notify\Messages\EmailMessage;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mime\Email;
/**
* App\Models\UserEmailVerification
@ -97,16 +98,14 @@ class UserEmailVerification extends AbstractModel
);
break;
}
Factory::mailer()
->setDsn("smtp://{$setting['account']}:{$setting['password']}@{$setting['smtp_server']}:{$setting['port']}?verify_peer=0")
->setMessage(EmailMessage::create()
->from($alias . " <{$setting['account']}>")
->to($email)
->subject($subject)
->html($content))
->send();
$mailer = new Mailer(Transport::fromDsn("smtp://{$setting['account']}:{$setting['password']}@{$setting['smtp_server']}:{$setting['port']}?verify_peer=0"));
$mailer->send((new Email())
->from($alias . " <{$setting['account']}>")
->to($email)
->subject($subject)
->html($content));
} catch (\Throwable $e) {
if (str_contains($e->getMessage(), "Timed Out")) {
if (stripos($e->getMessage(), "timed out") !== false) {
throw new ApiException("邮件发送超时,请检查邮箱配置是否正确");
} elseif ($e->getCode() === 550) {
throw new ApiException('邮件内容被拒绝,请检查邮箱是否开启接收功能');

View File

@ -5,8 +5,6 @@ namespace App\Models;
use App\Exceptions\ApiException;
use App\Module\Base;
use Carbon\Carbon;
use Guanguans\Notify\Factory;
use Guanguans\Notify\Messages\EmailMessage;
/**
* App\Models\UserTransfer

View File

@ -9,8 +9,9 @@ use App\Module\Base;
use App\Module\Doo;
use App\Module\Timer;
use Carbon\Carbon;
use Guanguans\Notify\Factory;
use Guanguans\Notify\Messages\EmailMessage;
use Symfony\Component\Mailer\Mailer;
use Symfony\Component\Mailer\Transport;
use Symfony\Component\Mime\Email;
/**
* 未读消息邮件通知任务
@ -258,20 +259,18 @@ class EmailNoticeTask extends AbstractTask
private function sendEmail($user, $emailData): void
{
Setting::validateAddr($user->email, function($to) use ($emailData) {
Factory::mailer()
->setDsn(sprintf(
'smtp://%s:%s@%s:%s?verify_peer=0',
$this->emailSetting['account'],
$this->emailSetting['password'],
$this->emailSetting['smtp_server'],
$this->emailSetting['port']
))
->setMessage(EmailMessage::create()
->from(sprintf('%s <%s>', Base::settingFind('system', 'system_alias', 'Task'), $this->emailSetting['account']))
->to($to)
->subject($emailData['subject'])
->html($emailData['content']))
->send();
$mailer = new Mailer(Transport::fromDsn(sprintf(
'smtp://%s:%s@%s:%s?verify_peer=0',
$this->emailSetting['account'],
$this->emailSetting['password'],
$this->emailSetting['smtp_server'],
$this->emailSetting['port']
)));
$mailer->send((new Email())
->from(sprintf('%s <%s>', Base::settingFind('system', 'system_alias', 'Task'), $this->emailSetting['account']))
->to($to)
->subject($emailData['subject'])
->html($emailData['content']));
});
}

View File

@ -22,7 +22,6 @@
"ext-zip": "*",
"directorytree/ldaprecord-laravel": "^4.0",
"firebase/php-jwt": "^7.1",
"guanguans/notify": "~1.28.0",
"guzzlehttp/guzzle": "^7.3.0",
"hedeqiang/umeng": "^2.1",
"laravel/framework": "^13.0",

279
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "6e15c4192dce5dc139bbff83fc0f8db8",
"content-hash": "394ccedcb5eb9fcf3ebadc4c65689688",
"packages": [
{
"name": "brick/math",
@ -1064,154 +1064,6 @@
],
"time": "2025-12-27T19:43:20+00:00"
},
{
"name": "guanguans/notify",
"version": "1.28.0",
"source": {
"type": "git",
"url": "https://github.com/guanguans/notify.git",
"reference": "e2dac64cf99ba3e41abe7aefaf59ef5cd3acf161"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guanguans/notify/zipball/e2dac64cf99ba3e41abe7aefaf59ef5cd3acf161",
"reference": "e2dac64cf99ba3e41abe7aefaf59ef5cd3acf161",
"shasum": ""
},
"require": {
"ext-json": "*",
"overtrue/http": "^1.2",
"php": ">=7.2.5",
"psr/log": "^1.1 || ^2.0 || ^3.0",
"symfony/options-resolver": "^5.4 || ^6.0 || ^7.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.8",
"brainmaestro/composer-git-hooks": "^2.8 || ^3.0",
"dms/phpunit-arraysubset-asserts": "^0.5",
"ergebnis/composer-normalize": "^2.19",
"friendsofphp/php-cs-fixer": "^3.4",
"mockery/mockery": "^1.3",
"php-mock/php-mock-phpunit": "^2.9",
"phpstan/extension-installer": "^1.3",
"phpstan/phpstan": "^1.10",
"phpstan/phpstan-deprecation-rules": "^1.1",
"phpunit/phpunit": "^8.5 || ^9.0",
"rector/rector": "^0.19",
"symfony/mailer": "^5.4 || ^6.0 || ^7.0",
"symfony/var-dumper": "^5.4 || ^6.0 || ^7.0",
"textalk/websocket": "^1.5",
"vimeo/psalm": "^4.30 || ^5.0"
},
"suggest": {
"symfony/mailer": "Required to use the email.",
"textalk/websocket": "Required to use the QQ channel bot."
},
"type": "library",
"extra": {
"hooks": {
"post-merge": [
"composer checks"
],
"pre-commit": [
"composer checks"
]
},
"bamarni-bin": {
"bin-links": true,
"forward-command": true,
"target-directory": "vendor-bin"
},
"branch-alias": {
"dev-main": "1.x-dev"
}
},
"autoload": {
"files": [
"src/Support/helpers.php"
],
"psr-4": {
"Guanguans\\Notify\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "guanguans",
"email": "ityaozm@gmail.com",
"homepage": "https://www.guanguans.cn",
"role": "developer"
}
],
"description": "Push notification sdk(Bark、Chanify、DingTalk、Discord、Email、FeiShu、Gitter、Google Chat、iGot、Logger、Mattermost、Microsoft Teams、Now Push、Ntfy、PushBack、Push、PushDeer、Pushover、PushPlus、QQ Channel Bot、Rocket Chat、ServerChan、Showdoc Push、Slack、Telegram、Webhook、WeWork、XiZhi、YiFengChuanHua、Zulip).com",
"homepage": "https://github.com/guanguans/notify",
"keywords": [
"Bark",
"Feishu",
"Mattermost",
"Ntfy",
"PushDeer",
"QQ Bot",
"QQ 机器人",
"QQ 频道",
"QQ 频道机器人",
"Server酱",
"chanify",
"dingtalk",
"discord",
"email",
"gitter",
"googleChat",
"iGot",
"logger",
"microsoft teams",
"notification",
"notifier",
"notify",
"now push",
"push",
"pushBack",
"pushPlus",
"pushover",
"qq",
"rocketchat",
"sdk",
"serverChan",
"showdoc push",
"slack",
"telegram",
"webhook",
"wework",
"xiZhi",
"zulip",
"一封传话",
"企业微信",
"企业微信群机器人",
"微信",
"息知",
"机器人",
"邮件",
"钉钉",
"钉钉群",
"钉钉群机器人",
"飞书",
"飞书群机器人"
],
"support": {
"issues": "https://github.com/guanguans/notify/issues",
"source": "https://github.com/guanguans/notify"
},
"funding": [
{
"url": "https://www.guanguans.cn/images/wechat.jpeg",
"type": "wechat"
}
],
"time": "2024-01-17T06:56:35+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "7.11.1",
@ -4027,64 +3879,6 @@
},
"time": "2026-02-23T07:40:35+00:00"
},
{
"name": "overtrue/http",
"version": "1.2.3",
"source": {
"type": "git",
"url": "https://github.com/overtrue/http.git",
"reference": "e6e4c2ff274b1050d681288495878ee8fd3f1209"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/overtrue/http/zipball/e6e4c2ff274b1050d681288495878ee8fd3f1209",
"reference": "e6e4c2ff274b1050d681288495878ee8fd3f1209",
"shasum": ""
},
"require": {
"ext-curl": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-simplexml": "*",
"guzzlehttp/guzzle": "^6.3 || ^7.0",
"php": ">=7.0"
},
"require-dev": {
"brainmaestro/composer-git-hooks": "^2.7",
"friendsofphp/php-cs-fixer": "^2.15 || ^3.0",
"mockery/mockery": "^1.0",
"overtrue/phplint": "^1.1 || ^2.0 || ^3.0",
"phpunit/phpunit": "^6.5 || ^8.5"
},
"type": "library",
"autoload": {
"psr-4": {
"Overtrue\\Http\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "overtrue",
"email": "anzhengchao@gmail.com"
}
],
"description": "A simple http client wrapper.",
"support": {
"issues": "https://github.com/overtrue/http/issues",
"source": "https://github.com/overtrue/http/tree/1.2.3"
},
"funding": [
{
"url": "https://github.com/overtrue",
"type": "github"
}
],
"time": "2022-03-14T06:24:13+00:00"
},
{
"name": "overtrue/pinyin",
"version": "5.3.4",
@ -6458,77 +6252,6 @@
],
"time": "2026-05-29T05:06:50+00:00"
},
{
"name": "symfony/options-resolver",
"version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
"reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/options-resolver/zipball/2888fcdc4dc2fd5f7c7397be78631e8af12e02b4",
"reference": "2888fcdc4dc2fd5f7c7397be78631e8af12e02b4",
"shasum": ""
},
"require": {
"php": ">=8.2",
"symfony/deprecation-contracts": "^2.5|^3"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\OptionsResolver\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Provides an improved replacement for the array_replace PHP function",
"homepage": "https://symfony.com",
"keywords": [
"config",
"configuration",
"options"
],
"support": {
"source": "https://github.com/symfony/options-resolver/tree/v7.4.8"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.37.0",