254 lines
7.4 KiB
C#
254 lines
7.4 KiB
C#
|
|
using MailKit;
|
|||
|
|
using MailKit.Net.Imap;
|
|||
|
|
using MailKit.Search;
|
|||
|
|
using MailKit.Security;
|
|||
|
|
using MimeKit;
|
|||
|
|
|
|||
|
|
namespace Service;
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 邮件抓取服务接口
|
|||
|
|
/// </summary>
|
|||
|
|
public interface IEmailFetchService
|
|||
|
|
{
|
|||
|
|
/// <summary>
|
|||
|
|
/// 连接状态
|
|||
|
|
/// </summary>
|
|||
|
|
bool IsConnected { get; }
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 连接到邮件服务器
|
|||
|
|
/// </summary>
|
|||
|
|
Task<bool> ConnectAsync(string host, int port, bool useSsl, string email, string password);
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 从收件箱获取未读邮件
|
|||
|
|
/// </summary>
|
|||
|
|
Task<List<(MimeMessage Message, UniqueId Uid)>> FetchUnreadMessagesAsync();
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 获取所有邮件
|
|||
|
|
/// </summary>
|
|||
|
|
Task<List<(MimeMessage Message, UniqueId Uid)>> FetchAllMessagesAsync();
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 断开与邮件服务器的连接
|
|||
|
|
/// </summary>
|
|||
|
|
Task DisconnectAsync();
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 标记邮件为已读
|
|||
|
|
/// </summary>
|
|||
|
|
Task MarkAsReadAsync(UniqueId uid);
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 确保连接有效,如断开则自动重连
|
|||
|
|
/// </summary>
|
|||
|
|
Task<bool> EnsureConnectedAsync();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 邮件抓取服务实现
|
|||
|
|
/// </summary>
|
|||
|
|
public class EmailFetchService(ILogger<EmailFetchService> logger) : IEmailFetchService
|
|||
|
|
{
|
|||
|
|
private ImapClient? _imapClient;
|
|||
|
|
private string _host = string.Empty;
|
|||
|
|
private int _port;
|
|||
|
|
private bool _useSsl;
|
|||
|
|
private string _email = string.Empty;
|
|||
|
|
private string _password = string.Empty;
|
|||
|
|
private DateTime _lastKeepAlive = DateTime.MinValue;
|
|||
|
|
private const int KeepAliveIntervalSeconds = 300; // 5分钟发送一次KeepAlive
|
|||
|
|
private readonly ILogger<EmailFetchService> _logger = logger;
|
|||
|
|
|
|||
|
|
public bool IsConnected => _imapClient?.IsConnected == true;
|
|||
|
|
|
|||
|
|
public async Task<bool> ConnectAsync(string host, int port, bool useSsl, string email, string password)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 保存连接信息用于自动重连
|
|||
|
|
_host = host;
|
|||
|
|
_port = port;
|
|||
|
|
_useSsl = useSsl;
|
|||
|
|
_email = email;
|
|||
|
|
_password = password;
|
|||
|
|
|
|||
|
|
// 如果已连接,先断开
|
|||
|
|
if (_imapClient?.IsConnected == true)
|
|||
|
|
{
|
|||
|
|
await DisconnectAsync();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_imapClient = new ImapClient();
|
|||
|
|
|
|||
|
|
if (useSsl)
|
|||
|
|
{
|
|||
|
|
await _imapClient.ConnectAsync(host, port, SecureSocketOptions.SslOnConnect);
|
|||
|
|
}
|
|||
|
|
else
|
|||
|
|
{
|
|||
|
|
await _imapClient.ConnectAsync(host, port, SecureSocketOptions.StartTlsWhenAvailable);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
await _imapClient.AuthenticateAsync(email, password);
|
|||
|
|
_logger.LogInformation("邮箱 {Email} 连接成功", email);
|
|||
|
|
_lastKeepAlive = DateTime.UtcNow;
|
|||
|
|
return true;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError(ex, "邮件连接失败 ({Email}): {Message}", email, ex.Message);
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public async Task<List<(MimeMessage Message, UniqueId Uid)>> FetchUnreadMessagesAsync()
|
|||
|
|
{
|
|||
|
|
var result = new List<(MimeMessage, UniqueId)>();
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 确保连接有效
|
|||
|
|
if (!await EnsureConnectedAsync())
|
|||
|
|
return result;
|
|||
|
|
|
|||
|
|
var inbox = _imapClient?.Inbox;
|
|||
|
|
if (inbox == null)
|
|||
|
|
return result;
|
|||
|
|
|
|||
|
|
await inbox.OpenAsync(FolderAccess.ReadWrite);
|
|||
|
|
|
|||
|
|
// 查询未读邮件
|
|||
|
|
var unreadUids = await inbox.SearchAsync(SearchQuery.NotSeen);
|
|||
|
|
|
|||
|
|
foreach (var uid in unreadUids)
|
|||
|
|
{
|
|||
|
|
var message = await inbox.GetMessageAsync(uid);
|
|||
|
|
result.Add((message, uid));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError(ex, "获取未读邮件失败: {Message}", ex.Message);
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public async Task<List<(MimeMessage Message, UniqueId Uid)>> FetchAllMessagesAsync()
|
|||
|
|
{
|
|||
|
|
var result = new List<(MimeMessage, UniqueId)>();
|
|||
|
|
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
// 确保连接有效
|
|||
|
|
if (!await EnsureConnectedAsync())
|
|||
|
|
return result;
|
|||
|
|
|
|||
|
|
var inbox = _imapClient?.Inbox;
|
|||
|
|
if (inbox == null)
|
|||
|
|
return result;
|
|||
|
|
|
|||
|
|
await inbox.OpenAsync(FolderAccess.ReadWrite);
|
|||
|
|
|
|||
|
|
var uids = await inbox.SearchAsync(SearchQuery.All);
|
|||
|
|
|
|||
|
|
foreach (var uid in uids)
|
|||
|
|
{
|
|||
|
|
var message = await inbox.GetMessageAsync(uid);
|
|||
|
|
result.Add((message, uid));
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError(ex, "获取所有邮件失败: {Message}", ex.Message);
|
|||
|
|
return result;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public async Task DisconnectAsync()
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
if (_imapClient?.IsConnected == true)
|
|||
|
|
{
|
|||
|
|
await _imapClient.DisconnectAsync(true);
|
|||
|
|
_logger.LogInformation("邮箱 {Email} 已断开连接", _email);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_imapClient?.Dispose();
|
|||
|
|
_imapClient = null;
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError(ex, "断开连接失败 ({Email}): {Message}", _email, ex.Message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public async Task MarkAsReadAsync(UniqueId uid)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
if (!await EnsureConnectedAsync())
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
var inbox = _imapClient?.Inbox;
|
|||
|
|
if (inbox == null)
|
|||
|
|
return;
|
|||
|
|
|
|||
|
|
// 打开收件箱以读写模式
|
|||
|
|
await inbox.OpenAsync(FolderAccess.ReadWrite);
|
|||
|
|
|
|||
|
|
// 标记邮件为已读(设置Seen标记)
|
|||
|
|
await inbox.AddFlagsAsync(uid, MessageFlags.Seen, silent: false);
|
|||
|
|
|
|||
|
|
_logger.LogDebug("邮件 {Uid} 标记已读操作已提交", uid);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
_logger.LogError(ex, "标记邮件为已读失败: {Message}", ex.Message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/// <summary>
|
|||
|
|
/// 确保连接有效,如果断开则自动重连
|
|||
|
|
/// </summary>
|
|||
|
|
public async Task<bool> EnsureConnectedAsync()
|
|||
|
|
{
|
|||
|
|
if (_imapClient?.IsConnected == true)
|
|||
|
|
{
|
|||
|
|
// 定期发送NOOP保持连接活跃(防止超时断开)
|
|||
|
|
var timeSinceLastKeepAlive = (DateTime.UtcNow - _lastKeepAlive).TotalSeconds;
|
|||
|
|
if (timeSinceLastKeepAlive > KeepAliveIntervalSeconds)
|
|||
|
|
{
|
|||
|
|
try
|
|||
|
|
{
|
|||
|
|
await _imapClient.NoOpAsync();
|
|||
|
|
_lastKeepAlive = DateTime.UtcNow;
|
|||
|
|
_logger.LogDebug("邮箱 {Email} KeepAlive 保活信号已发送", _email);
|
|||
|
|
}
|
|||
|
|
catch (Exception ex)
|
|||
|
|
{
|
|||
|
|
// NOOP失败,说明连接已断开,继续重连逻辑
|
|||
|
|
_logger.LogWarning(ex, "KeepAlive 失败,连接已断开: {Message}", ex.Message);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
return _imapClient?.IsConnected == true;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if (string.IsNullOrEmpty(_host) || string.IsNullOrEmpty(_email))
|
|||
|
|
{
|
|||
|
|
_logger.LogWarning("未初始化连接信息,无法自动重连");
|
|||
|
|
return false;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
_logger.LogInformation("检测到连接断开,尝试重新连接到 {Email}...", _email);
|
|||
|
|
return await ConnectAsync(_host, _port, _useSsl, _email, _password);
|
|||
|
|
}
|
|||
|
|
}
|