替换 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.Infrastructure" Version="$(FakePackageVersion)" />
<PackageReference Include="DocumentFormat.OpenXml" Version="3.0.2" />
<PackageReference Include="MailKit" Version="4.17.0" />
<PackageReference Include="MiniExcel" Version="1.43.1" />
<PackageReference Include="RabbitMQ.Client" Version="6.8.1" />
</ItemGroup>

View File

@ -1,6 +1,8 @@
using System.Net;
using System.Net.Mail;
using System.Net.Sockets;
using Fake.Application;
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
namespace Oms.Application.Services.System;
@ -11,17 +13,17 @@ internal static class BatchEmailSmtpHelper
internal static bool IsSecretMask(string? value) =>
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)
{
return (serverType ?? string.Empty).Trim().ToLowerInvariant() switch
{
"qiye_ssl" => new SmtpEndpoint("smtp.mxhichina.com", 465, true),
"aliyun" => new SmtpEndpoint("smtp.aliyun.com", 25, false),
"aliyun_ssl" => new SmtpEndpoint("smtp.aliyun.com", 465, true),
"mxhichina" => new SmtpEndpoint("smtp.mxhichina.com", 25, false),
_ => new SmtpEndpoint("smtp.mxhichina.com", 25, false)
"qiye_ssl" => new SmtpEndpoint("smtp.mxhichina.com", 465, SecureSocketOptions.SslOnConnect),
"aliyun" => new SmtpEndpoint("smtp.aliyun.com", 25, SecureSocketOptions.StartTlsWhenAvailable),
"aliyun_ssl" => new SmtpEndpoint("smtp.aliyun.com", 465, SecureSocketOptions.SslOnConnect),
"mxhichina" => new SmtpEndpoint("smtp.mxhichina.com", 25, SecureSocketOptions.StartTlsWhenAvailable),
_ => new SmtpEndpoint("smtp.mxhichina.com", 25, SecureSocketOptions.StartTlsWhenAvailable)
};
}
@ -33,14 +35,14 @@ internal static class BatchEmailSmtpHelper
{
ValidateCredentials(smtpUsername, smtpPassword);
var username = smtpUsername.Trim();
var endpoint = ResolveEndpoint(serverType);
using var client = CreateClient(endpoint, username, smtpPassword);
using var message = new MailMessage(username, username)
{
Subject = "OMS SMTP Test",
Body = "OMS batch email connection test"
};
await SendWithClientAsync(client, message, cancellationToken);
var message = BuildMessage(
username,
username,
username,
"OMS SMTP Test",
"OMS batch email connection test",
"plain");
await SendWithMailKitAsync(username, smtpPassword, serverType, message, cancellationToken);
}
internal static async Task SendAsync(
@ -62,20 +64,31 @@ internal static class BatchEmailSmtpHelper
}
var username = smtpUsername.Trim();
var endpoint = ResolveEndpoint(serverType);
using var client = CreateClient(endpoint, username, smtpPassword);
var fromAddress = new MailAddress(
username,
string.IsNullOrWhiteSpace(fromName) ? username : fromName.Trim());
using var message = new MailMessage
var displayName = string.IsNullOrWhiteSpace(fromName) ? username : fromName.Trim();
var message = BuildMessage(username, displayName, recipient, subject, body, bodyType);
await SendWithMailKitAsync(username, smtpPassword, serverType, message, cancellationToken);
}
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,
Body = body ?? string.Empty,
IsBodyHtml = string.Equals(bodyType, "html", StringComparison.OrdinalIgnoreCase)
Subject = subject ?? string.Empty
};
message.To.Add(recipient);
await SendWithClientAsync(client, message, cancellationToken);
message.From.Add(new MailboxAddress(displayName, username));
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)
@ -91,32 +104,41 @@ internal static class BatchEmailSmtpHelper
}
}
private static SmtpClient CreateClient(SmtpEndpoint endpoint, string username, string password) =>
new(endpoint.Host, endpoint.Port)
private static async Task SendWithMailKitAsync(
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
};
private static async Task SendWithClientAsync(
SmtpClient client,
MailMessage message,
CancellationToken cancellationToken)
{
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}");
}
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}");
}
}
}