替换 System.Net.Mail,正确支持隐式 SSL 与 STARTTLS,解决 Syntax error 测试连接失败。

fix: 批量邮件 SMTP 改用 MailKit 修复 SSL 465 连接
This commit is contained in:
biybuy 2026-07-11 12:00:46 +08:00 committed by GitHub
commit 4e9a79e58e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 66 additions and 43 deletions

View File

@ -22,6 +22,7 @@
<PackageReference Include="Fake.Rbac.Application" Version="$(FakePackageVersion)" /> <PackageReference Include="Fake.Rbac.Application" Version="$(FakePackageVersion)" />
<PackageReference Include="Fake.Rbac.Infrastructure" Version="$(FakePackageVersion)" /> <PackageReference Include="Fake.Rbac.Infrastructure" Version="$(FakePackageVersion)" />
<PackageReference Include="DocumentFormat.OpenXml" Version="3.0.2" /> <PackageReference Include="DocumentFormat.OpenXml" Version="3.0.2" />
<PackageReference Include="MailKit" Version="4.17.0" />
<PackageReference Include="MiniExcel" Version="1.43.1" /> <PackageReference Include="MiniExcel" Version="1.43.1" />
<PackageReference Include="RabbitMQ.Client" Version="6.8.1" /> <PackageReference Include="RabbitMQ.Client" Version="6.8.1" />
</ItemGroup> </ItemGroup>

View File

@ -1,6 +1,8 @@
using System.Net; using System.Net.Sockets;
using System.Net.Mail;
using Fake.Application; using Fake.Application;
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
namespace Oms.Application.Services.System; namespace Oms.Application.Services.System;
@ -11,17 +13,17 @@ internal static class BatchEmailSmtpHelper
internal static bool IsSecretMask(string? value) => internal static bool IsSecretMask(string? value) =>
string.Equals(value?.Trim(), SecretMask, StringComparison.Ordinal); string.Equals(value?.Trim(), SecretMask, StringComparison.Ordinal);
internal sealed record SmtpEndpoint(string Host, int Port, bool EnableSsl); internal sealed record SmtpEndpoint(string Host, int Port, SecureSocketOptions SocketOptions);
internal static SmtpEndpoint ResolveEndpoint(string? serverType) internal static SmtpEndpoint ResolveEndpoint(string? serverType)
{ {
return (serverType ?? string.Empty).Trim().ToLowerInvariant() switch return (serverType ?? string.Empty).Trim().ToLowerInvariant() switch
{ {
"qiye_ssl" => new SmtpEndpoint("smtp.mxhichina.com", 465, true), "qiye_ssl" => new SmtpEndpoint("smtp.mxhichina.com", 465, SecureSocketOptions.SslOnConnect),
"aliyun" => new SmtpEndpoint("smtp.aliyun.com", 25, false), "aliyun" => new SmtpEndpoint("smtp.aliyun.com", 25, SecureSocketOptions.StartTlsWhenAvailable),
"aliyun_ssl" => new SmtpEndpoint("smtp.aliyun.com", 465, true), "aliyun_ssl" => new SmtpEndpoint("smtp.aliyun.com", 465, SecureSocketOptions.SslOnConnect),
"mxhichina" => new SmtpEndpoint("smtp.mxhichina.com", 25, false), "mxhichina" => new SmtpEndpoint("smtp.mxhichina.com", 25, SecureSocketOptions.StartTlsWhenAvailable),
_ => new SmtpEndpoint("smtp.mxhichina.com", 25, false) _ => new SmtpEndpoint("smtp.mxhichina.com", 25, SecureSocketOptions.StartTlsWhenAvailable)
}; };
} }
@ -33,14 +35,14 @@ internal static class BatchEmailSmtpHelper
{ {
ValidateCredentials(smtpUsername, smtpPassword); ValidateCredentials(smtpUsername, smtpPassword);
var username = smtpUsername.Trim(); var username = smtpUsername.Trim();
var endpoint = ResolveEndpoint(serverType); var message = BuildMessage(
using var client = CreateClient(endpoint, username, smtpPassword); username,
using var message = new MailMessage(username, username) username,
{ username,
Subject = "OMS SMTP Test", "OMS SMTP Test",
Body = "OMS batch email connection test" "OMS batch email connection test",
}; "plain");
await SendWithClientAsync(client, message, cancellationToken); await SendWithMailKitAsync(username, smtpPassword, serverType, message, cancellationToken);
} }
internal static async Task SendAsync( internal static async Task SendAsync(
@ -62,20 +64,31 @@ internal static class BatchEmailSmtpHelper
} }
var username = smtpUsername.Trim(); var username = smtpUsername.Trim();
var endpoint = ResolveEndpoint(serverType); var displayName = string.IsNullOrWhiteSpace(fromName) ? username : fromName.Trim();
using var client = CreateClient(endpoint, username, smtpPassword); var message = BuildMessage(username, displayName, recipient, subject, body, bodyType);
var fromAddress = new MailAddress( await SendWithMailKitAsync(username, smtpPassword, serverType, message, cancellationToken);
username, }
string.IsNullOrWhiteSpace(fromName) ? username : fromName.Trim());
using var message = new MailMessage private static MimeMessage BuildMessage(
string username,
string displayName,
string to,
string? subject,
string? body,
string? bodyType)
{
var message = new MimeMessage
{ {
From = fromAddress, Subject = subject ?? string.Empty
Subject = subject ?? string.Empty,
Body = body ?? string.Empty,
IsBodyHtml = string.Equals(bodyType, "html", StringComparison.OrdinalIgnoreCase)
}; };
message.To.Add(recipient); message.From.Add(new MailboxAddress(displayName, username));
await SendWithClientAsync(client, message, cancellationToken); message.To.Add(MailboxAddress.Parse(to));
var isHtml = string.Equals(bodyType, "html", StringComparison.OrdinalIgnoreCase);
message.Body = new TextPart(isHtml ? "html" : "plain")
{
Text = body ?? string.Empty
};
return message;
} }
private static void ValidateCredentials(string smtpUsername, string smtpPassword) private static void ValidateCredentials(string smtpUsername, string smtpPassword)
@ -91,32 +104,41 @@ internal static class BatchEmailSmtpHelper
} }
} }
private static SmtpClient CreateClient(SmtpEndpoint endpoint, string username, string password) => private static async Task SendWithMailKitAsync(
new(endpoint.Host, endpoint.Port) string username,
string password,
string? serverType,
MimeMessage message,
CancellationToken cancellationToken)
{
var endpoint = ResolveEndpoint(serverType);
using var client = new SmtpClient
{ {
EnableSsl = endpoint.EnableSsl,
DeliveryMethod = SmtpDeliveryMethod.Network,
UseDefaultCredentials = false,
Credentials = new NetworkCredential(username, password),
Timeout = 30_000 Timeout = 30_000
}; };
private static async Task SendWithClientAsync(
SmtpClient client,
MailMessage message,
CancellationToken cancellationToken)
{
try try
{ {
await client.SendMailAsync(message, cancellationToken); await client.ConnectAsync(endpoint.Host, endpoint.Port, endpoint.SocketOptions, cancellationToken);
await client.AuthenticateAsync(username, password, cancellationToken);
await client.SendAsync(message, cancellationToken);
await client.DisconnectAsync(true, cancellationToken);
} }
catch (SmtpException ex) catch (AuthenticationException ex)
{
throw new BusinessException($"SMTP 认证失败:{ex.Message}");
}
catch (SmtpCommandException ex)
{ {
throw new BusinessException($"SMTP 发送失败:{ex.Message}"); throw new BusinessException($"SMTP 发送失败:{ex.Message}");
} }
catch (Exception ex) when (ex is InvalidOperationException or FormatException) catch (SmtpProtocolException ex)
{ {
throw new BusinessException($"SMTP 发送失败:{ex.Message}"); throw new BusinessException($"SMTP 协议错误:{ex.Message}");
}
catch (Exception ex) when (ex is IOException or SocketException or TimeoutException)
{
throw new BusinessException($"SMTP 连接失败:{ex.Message}");
} }
} }
} }