mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-11-09 12:30:49 +08:00
feat(notice): PHPMailer 邮箱驱动与短信公众号骨架
This commit is contained in:
@@ -36,7 +36,8 @@
|
|||||||
"mcp/sdk": "^0.7",
|
"mcp/sdk": "^0.7",
|
||||||
"psr/simple-cache": ">=1.0",
|
"psr/simple-cache": ">=1.0",
|
||||||
"league/flysystem-aws-s3-v3": "^3.0",
|
"league/flysystem-aws-s3-v3": "^3.0",
|
||||||
"league/flysystem-webdav": "^3.0"
|
"league/flysystem-webdav": "^3.0",
|
||||||
|
"phpmailer/phpmailer": "^7.1"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"symfony/var-dumper": "*",
|
"symfony/var-dumper": "*",
|
||||||
|
|||||||
84
composer.lock
generated
84
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "e99cd9f4e74aad9a5d34050bab8d0c2a",
|
"content-hash": "3ed07ae29f248003750e594a22070cca",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "aliyuncs/oss-sdk-php",
|
"name": "aliyuncs/oss-sdk-php",
|
||||||
@@ -2233,6 +2233,82 @@
|
|||||||
},
|
},
|
||||||
"time": "2026-01-06T21:53:42+00:00"
|
"time": "2026-01-06T21:53:42+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "phpmailer/phpmailer",
|
||||||
|
"version": "v7.1.1",
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://nexus.hl7.top:1243/repository/composer-proxy/phpmailer/phpmailer/v7.1.1/phpmailer-phpmailer-v7.1.1.zip",
|
||||||
|
"reference": "1bc1716a507a65e039d4ac9d9adebbbd0d346e15",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-ctype": "*",
|
||||||
|
"ext-filter": "*",
|
||||||
|
"ext-hash": "*",
|
||||||
|
"php": ">=5.5.0"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"dealerdirect/phpcodesniffer-composer-installer": "^1.0",
|
||||||
|
"doctrine/annotations": "^1.2.6 || ^1.13.3",
|
||||||
|
"php-parallel-lint/php-console-highlighter": "^1.0.0",
|
||||||
|
"php-parallel-lint/php-parallel-lint": "^1.3.2",
|
||||||
|
"phpcompatibility/php-compatibility": "^10.0.0@dev",
|
||||||
|
"squizlabs/php_codesniffer": "^3.13.5",
|
||||||
|
"yoast/phpunit-polyfills": "^1.0.4"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication",
|
||||||
|
"directorytree/imapengine": "For uploading sent messages via IMAP, see gmail example",
|
||||||
|
"ext-imap": "Needed to support advanced email address parsing according to RFC822",
|
||||||
|
"ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses",
|
||||||
|
"ext-openssl": "Needed for secure SMTP sending and DKIM signing",
|
||||||
|
"greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication",
|
||||||
|
"hayageek/oauth2-yahoo": "Needed for Yahoo XOAUTH2 authentication",
|
||||||
|
"league/oauth2-google": "Needed for Google XOAUTH2 authentication",
|
||||||
|
"psr/log": "For optional PSR-3 debug logging",
|
||||||
|
"symfony/polyfill-mbstring": "To support UTF-8 if the Mbstring PHP extension is not enabled (^1.2)",
|
||||||
|
"thenetworg/oauth2-azure": "Needed for Microsoft XOAUTH2 authentication"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"PHPMailer\\PHPMailer\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"license": [
|
||||||
|
"LGPL-2.1-only"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Marcus Bointon",
|
||||||
|
"email": "phpmailer@synchromedia.co.uk"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Jim Jagielski",
|
||||||
|
"email": "jimjag@gmail.com"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Andy Prevost",
|
||||||
|
"email": "codeworxtech@users.sourceforge.net"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Brent R. Matzelle"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHPMailer is a full-featured email creation and transfer class for PHP",
|
||||||
|
"support": {
|
||||||
|
"issues": "https://github.com/PHPMailer/PHPMailer/issues",
|
||||||
|
"source": "https://github.com/PHPMailer/PHPMailer/tree/v7.1.1"
|
||||||
|
},
|
||||||
|
"funding": [
|
||||||
|
{
|
||||||
|
"url": "https://github.com/Synchro",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"time": "2026-05-18T08:06:14+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "phpoffice/phpspreadsheet",
|
"name": "phpoffice/phpspreadsheet",
|
||||||
"version": "1.30.3",
|
"version": "1.30.3",
|
||||||
@@ -6174,7 +6250,7 @@
|
|||||||
],
|
],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"minimum-stability": "stable",
|
"minimum-stability": "stable",
|
||||||
"stability-flags": [],
|
"stability-flags": {},
|
||||||
"prefer-stable": false,
|
"prefer-stable": false,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
@@ -6182,6 +6258,6 @@
|
|||||||
"ext-json": "*",
|
"ext-json": "*",
|
||||||
"ext-fileinfo": "*"
|
"ext-fileinfo": "*"
|
||||||
},
|
},
|
||||||
"platform-dev": [],
|
"platform-dev": {},
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.9.0"
|
||||||
}
|
}
|
||||||
|
|||||||
85
extend/base/common/notice/channel/EmailChannel.php
Normal file
85
extend/base/common/notice/channel/EmailChannel.php
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace base\common\notice\channel;
|
||||||
|
|
||||||
|
use base\common\notice\NoticeChannelInterface;
|
||||||
|
use PHPMailer\PHPMailer\Exception as PHPMailerException;
|
||||||
|
use PHPMailer\PHPMailer\PHPMailer;
|
||||||
|
use RuntimeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮件渠道驱动(PHPMailer SMTP).
|
||||||
|
*
|
||||||
|
* 读取 sysconfig notify 组 SMTP 参数执行真实投递:
|
||||||
|
* 抛出异常视为投递失败,异常消息携带 PHPMailer ErrorInfo(含 SMTP 会话 lastError),
|
||||||
|
* timer 消费者据此退避重试直至终态。
|
||||||
|
*/
|
||||||
|
class EmailChannel implements NoticeChannelInterface
|
||||||
|
{
|
||||||
|
public static function name(): string
|
||||||
|
{
|
||||||
|
return 'email';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(array $target, array $notice): bool
|
||||||
|
{
|
||||||
|
// 全局总闸:DB 值为字符串,(int) 强转判断;仅 '1' 视为启用(缺省/0 均为关,
|
||||||
|
// 与系统配置 notify 组 notify_email_enable 默认值 0 一致),关闭即抛异常。
|
||||||
|
if ((int) sysconfig('notify', 'notify_email_enable') !== 1) {
|
||||||
|
throw new RuntimeException('邮件发送失败: 渠道未启用(notify_email_enable != 1)');
|
||||||
|
}
|
||||||
|
|
||||||
|
$host = (string) sysconfig('notify', 'notify_smtp_host');
|
||||||
|
$port = (int) sysconfig('notify', 'notify_smtp_port');
|
||||||
|
$secure = strtolower((string) sysconfig('notify', 'notify_smtp_secure'));
|
||||||
|
$user = (string) sysconfig('notify', 'notify_smtp_user');
|
||||||
|
$pass = (string) sysconfig('notify', 'notify_smtp_pass');
|
||||||
|
$from = (string) sysconfig('notify', 'notify_smtp_from', 'notice@localhost');
|
||||||
|
$fromName = (string) sysconfig('notify', 'notify_smtp_from_name');
|
||||||
|
|
||||||
|
$title = (string) ($notice['title'] ?? '');
|
||||||
|
$content = (string) ($notice['content'] ?? '');
|
||||||
|
|
||||||
|
$mailer = new PHPMailer(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$mailer->isSMTP();
|
||||||
|
$mailer->Host = $host;
|
||||||
|
$mailer->Port = $port;
|
||||||
|
// SMTPSecure 映射:ssl→隐式 SMTPS;tls→STARTTLS;none/未知→明文。
|
||||||
|
// 明文场景必须显式关闭 SMTPAutoTLS:PHPMailer 默认会对明文会话自动尝试
|
||||||
|
// STARTTLS 升级,Mailpit 等纯明文本地服务会被误判为失败。
|
||||||
|
if ($secure === 'ssl') {
|
||||||
|
$mailer->SMTPSecure = PHPMailer::ENCRYPTION_SMTPS;
|
||||||
|
} elseif ($secure === 'tls') {
|
||||||
|
$mailer->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
|
||||||
|
} else {
|
||||||
|
$mailer->SMTPSecure = '';
|
||||||
|
$mailer->SMTPAutoTLS = false;
|
||||||
|
}
|
||||||
|
// SMTP 认证仅在配置了用户名时启用(Mailpit 等本地无认证服务不设置)
|
||||||
|
if ($user !== '') {
|
||||||
|
$mailer->SMTPAuth = true;
|
||||||
|
$mailer->Username = $user;
|
||||||
|
$mailer->Password = $pass;
|
||||||
|
}
|
||||||
|
$mailer->CharSet = 'UTF-8';
|
||||||
|
$mailer->isHTML(true);
|
||||||
|
$mailer->setFrom($from, $fromName);
|
||||||
|
$mailer->addAddress((string) ($target['target_addr'] ?? ''));
|
||||||
|
$mailer->Subject = $title;
|
||||||
|
$mailer->Body = '<div style="font-family:Arial,sans-serif;">'
|
||||||
|
. '<h3>' . $title . '</h3>'
|
||||||
|
. '<div>' . nl2br($content) . '</div>'
|
||||||
|
. '</div>';
|
||||||
|
|
||||||
|
$mailer->send();
|
||||||
|
} catch (PHPMailerException $e) {
|
||||||
|
// send() 抛出的 Exception 消息即 $mailer->ErrorInfo(setError 已拼接
|
||||||
|
// SMTP lastError/detail/smtp_code),异常链保留原始异常供上游追溯
|
||||||
|
throw new RuntimeException('邮件发送失败: ' . $e->getMessage(), 0, $e);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
extend/base/common/notice/channel/SmsChannel.php
Normal file
46
extend/base/common/notice/channel/SmsChannel.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace base\common\notice\channel;
|
||||||
|
|
||||||
|
use base\common\notice\NoticeChannelInterface;
|
||||||
|
use think\facade\Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 短信渠道驱动(骨架).
|
||||||
|
*
|
||||||
|
* 骨架形态:读开关、记日志、恒成功,不执行真实外呼。
|
||||||
|
* 正常业务流不会产生 sms 投递行(scenes() 注册渠道不含 sms),
|
||||||
|
* 行进队列的场景是 app 层注册新场景引入 sms 渠道之后。
|
||||||
|
*/
|
||||||
|
class SmsChannel implements NoticeChannelInterface
|
||||||
|
{
|
||||||
|
public static function name(): string
|
||||||
|
{
|
||||||
|
return 'sms';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(array $target, array $notice): bool
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* TODO 下游对接指引(真实短信发送):
|
||||||
|
* - 使用既有 guzzlehttp/guzzle 客户端调用短信服务商 API(阿里云/腾讯云 SMS 等);
|
||||||
|
* - 读取 sysconfig('notify','notify_sms_sign'/'notify_sms_appid'/'notify_sms_secret')
|
||||||
|
* 组装签名与模板参数,POST 服务商接口;
|
||||||
|
* - 服务商接口非成功码(HTTP 非 2xx 或业务失败码)时抛出 RuntimeException,
|
||||||
|
* 异常消息携带服务商返回的失败信息,交由 timer 消费者退避重试;
|
||||||
|
* - target_addr 为手机号,notice 为通知主体(title/content)。
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 骨架驱动模拟发送:开关状态仅记入日志(关闭也恒成功),不拦截投递、不真实外呼
|
||||||
|
$enabled = (int) sysconfig('notify', 'notify_sms_enable');
|
||||||
|
Log::info('notice sms channel skeleton simulated send', [
|
||||||
|
'channel' => self::name(),
|
||||||
|
'enabled' => $enabled,
|
||||||
|
'notice_id' => $notice['id'] ?? 0,
|
||||||
|
'target_addr' => $target['target_addr'] ?? '',
|
||||||
|
'title' => $notice['title'] ?? '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
46
extend/base/common/notice/channel/WechatChannel.php
Normal file
46
extend/base/common/notice/channel/WechatChannel.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace base\common\notice\channel;
|
||||||
|
|
||||||
|
use base\common\notice\NoticeChannelInterface;
|
||||||
|
use think\facade\Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 公众号渠道驱动(骨架).
|
||||||
|
*
|
||||||
|
* 骨架形态:读开关、记日志、恒成功,不执行真实外呼。
|
||||||
|
* 正常业务流不会产生 wechat 投递行(scenes() 注册渠道不含 wechat),
|
||||||
|
* 行进队列的场景是 app 层注册新场景引入 wechat 渠道之后。
|
||||||
|
*/
|
||||||
|
class WechatChannel implements NoticeChannelInterface
|
||||||
|
{
|
||||||
|
public static function name(): string
|
||||||
|
{
|
||||||
|
return 'wechat';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function send(array $target, array $notice): bool
|
||||||
|
{
|
||||||
|
/*
|
||||||
|
* TODO 下游对接指引(公众号模板消息):
|
||||||
|
* - 实现 access_token 管理:用 sysconfig('notify','notify_wechat_appid'/'notify_wechat_secret')
|
||||||
|
* 调用微信 token 接口获取凭证,缓存至过期前(token 有效期 7200s,需集中缓存防刷);
|
||||||
|
* - 用 guzzlehttp/guzzle 调用模板消息接口(POST /cgi-bin/message/template/send),
|
||||||
|
* target_addr 为接收用户 openid,notice(title/content)映射模板字段;
|
||||||
|
* - 微信返回 errcode 非 0 时抛出 RuntimeException(携带 errcode/errmsg),
|
||||||
|
* 交由 timer 消费者退避重试。
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 骨架驱动模拟发送:开关状态仅记入日志(关闭也恒成功),不拦截投递、不真实外呼
|
||||||
|
$enabled = (int) sysconfig('notify', 'notify_wechat_enable');
|
||||||
|
Log::info('notice wechat channel skeleton simulated send', [
|
||||||
|
'channel' => self::name(),
|
||||||
|
'enabled' => $enabled,
|
||||||
|
'notice_id' => $notice['id'] ?? 0,
|
||||||
|
'target_addr' => $target['target_addr'] ?? '',
|
||||||
|
'title' => $notice['title'] ?? '',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
123
tests/Feature/NoticeChannelTest.php
Normal file
123
tests/Feature/NoticeChannelTest.php
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace tests\Feature;
|
||||||
|
|
||||||
|
use app\common\test\TestCase;
|
||||||
|
use base\common\notice\channel\EmailChannel;
|
||||||
|
use base\common\notice\channel\SmsChannel;
|
||||||
|
use base\common\notice\channel\WechatChannel;
|
||||||
|
use app\common\service\NoticeService;
|
||||||
|
use think\facade\Cache;
|
||||||
|
use think\facade\Db;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渠道驱动集成测试(任务 4 增补).
|
||||||
|
*
|
||||||
|
* 覆盖:
|
||||||
|
* 1. EmailChannel 在 notify_email_enable=0 时抛异常(渠道总闸关闭=投递失败信号)
|
||||||
|
* 2. SmsChannel send() 返回 true(骨架恒成功)
|
||||||
|
* 3. WechatChannel send() 返回 true(骨架恒成功)
|
||||||
|
* 4. 驱动 name() 标识与 channels() 映射一致
|
||||||
|
*
|
||||||
|
* fixture 均为事务内自造数据,tearDown 自动回滚;
|
||||||
|
* sysconfig 缓存不受事务回滚影响,配置行写入后显式清 tag(见 setNotifyConfig/tearDown)。
|
||||||
|
*/
|
||||||
|
class NoticeChannelTest extends TestCase
|
||||||
|
{
|
||||||
|
/** 本测试类是否写过 notify 配置行(决定 tearDown 是否清 sysconfig 缓存) */
|
||||||
|
private bool $touchedSysconfig = false;
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 1. EmailChannel 总闸关闭抛异常
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_email_channel_throws_when_disabled(): void
|
||||||
|
{
|
||||||
|
// 插入关闭配置行(value='0')并清 sysconfig 缓存(缓存不受事务回滚影响)
|
||||||
|
$this->setNotifyConfig('notify_email_enable', '0');
|
||||||
|
|
||||||
|
$this->expectException(\RuntimeException::class);
|
||||||
|
$this->expectExceptionMessage('渠道未启用');
|
||||||
|
|
||||||
|
(new EmailChannel())->send(
|
||||||
|
['channel' => 'email', 'target_addr' => 'disabled@test.com', 'status' => 0],
|
||||||
|
['id' => 1, 'scene' => 'common', 'title' => '标题', 'content' => '内容']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 2. SmsChannel 骨架恒成功
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_sms_channel_send_returns_true(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue((new SmsChannel())->send(
|
||||||
|
['channel' => 'sms', 'target_addr' => '13800138000', 'status' => 0],
|
||||||
|
['id' => 1, 'scene' => 'common', 'title' => '标题', 'content' => '内容']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 3. WechatChannel 骨架恒成功
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_wechat_channel_send_returns_true(): void
|
||||||
|
{
|
||||||
|
$this->assertTrue((new WechatChannel())->send(
|
||||||
|
['channel' => 'wechat', 'target_addr' => 'openid_demo', 'status' => 0],
|
||||||
|
['id' => 1, 'scene' => 'common', 'title' => '标题', 'content' => '内容']
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// 4. name() 契约与 channels() 映射一致
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
public function test_channel_names_match_service_mapping(): void
|
||||||
|
{
|
||||||
|
$this->assertSame('email', EmailChannel::name());
|
||||||
|
$this->assertSame('sms', SmsChannel::name());
|
||||||
|
$this->assertSame('wechat', WechatChannel::name());
|
||||||
|
|
||||||
|
$channels = NoticeService::channels();
|
||||||
|
$this->assertSame(EmailChannel::class, $channels['email']);
|
||||||
|
$this->assertSame(SmsChannel::class, $channels['sms']);
|
||||||
|
$this->assertSame(WechatChannel::class, $channels['wechat']);
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// fixture 工厂(事务内运行,tearDown 自动回滚;缓存显式清理)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 写 notify 组配置行并清 sysconfig 缓存.
|
||||||
|
*
|
||||||
|
* sysconfig 缓存不受事务回滚影响:必须显式清 tag 才能让后续读取
|
||||||
|
* 看到事务内的新值(value='0' 因 empty('0') 恒不写缓存,但清理是
|
||||||
|
* 防御后续用例被前序非 '0' 缓存值污染的统一口径)。
|
||||||
|
*/
|
||||||
|
private function setNotifyConfig(string $name, string $value): void
|
||||||
|
{
|
||||||
|
Db::name('system_config')->insert([
|
||||||
|
'name' => $name,
|
||||||
|
'group' => 'notify',
|
||||||
|
'value' => $value,
|
||||||
|
'remark' => 'NoticeChannelTest fixture',
|
||||||
|
'sort' => 100,
|
||||||
|
'create_time' => time(),
|
||||||
|
'update_time' => time(),
|
||||||
|
]);
|
||||||
|
$this->touchedSysconfig = true;
|
||||||
|
Cache::tag('sysconfig')->clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
if ($this->touchedSysconfig) {
|
||||||
|
Cache::tag('sysconfig')->clear();
|
||||||
|
}
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user