using System.Text.Json;
using Microsoft.Extensions.Options;
using Microsoft.Identity.Client;
namespace OASystem.API.OAMethodLib.MicrosoftGraphMailbox;
///
/// 后台轮询收件箱:仅当 为 true 时才会登录并访问 Graph。
/// 首次无缓存时会通过交互式浏览器完成授权(重定向 URI 与配置一致)。
///
public sealed class MicrosoftGraphInboxPollerHostedService : BackgroundService
{
private readonly IOptionsMonitor _optionsMonitor;
private readonly IMicrosoftGraphMailboxService _graphMailbox;
private readonly ILogger _logger;
private readonly HashSet _processedMessageIds = new(StringComparer.Ordinal);
private DateTime? _monitorStartUtc;
private bool _startupProfileLogged;
public MicrosoftGraphInboxPollerHostedService(
IOptionsMonitor optionsMonitor,
IMicrosoftGraphMailboxService graphMailbox,
ILogger logger)
{
_optionsMonitor = optionsMonitor;
_graphMailbox = graphMailbox;
_logger = logger;
}
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
var options = _optionsMonitor.CurrentValue;
if (!options.Enabled)
{
await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken).ConfigureAwait(false);
continue;
}
if (_monitorStartUtc == null)
_monitorStartUtc = DateTime.UtcNow;
try
{
if (!_startupProfileLogged && options.LogProfileOnStartup)
{
var meJson = await _graphMailbox.GetMeRawJsonAsync(stoppingToken).ConfigureAwait(false);
if (!string.IsNullOrEmpty(meJson))
_logger.LogInformation("Graph 邮箱:当前用户 /me 响应(节选) {Snippet}",
meJson.Length > 500 ? meJson[..500] + "…" : meJson);
_startupProfileLogged = true;
}
await PollInboxAsync(stoppingToken).ConfigureAwait(false);
}
catch (MsalException ex)
{
_logger.LogError(ex, "Graph 邮箱 MSAL 认证失败");
}
catch (HttpRequestException ex)
{
_logger.LogError(ex, "Graph 邮箱 HTTP 请求失败");
}
catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
{
throw;
}
catch (Exception ex)
{
_logger.LogError(ex, "Graph 邮箱轮询异常");
}
var interval = TimeSpan.FromSeconds(Math.Clamp(options.PollIntervalSeconds, 5, 3600));
await Task.Delay(interval, stoppingToken).ConfigureAwait(false);
}
}
private async Task PollInboxAsync(CancellationToken cancellationToken)
{
var options = _optionsMonitor.CurrentValue;
var startUtc = _monitorStartUtc ?? DateTime.UtcNow;
var json = await _graphMailbox.GetInboxMessagesJsonSinceAsync(startUtc, cancellationToken)
.ConfigureAwait(false);
if (string.IsNullOrEmpty(json))
{
_logger.LogWarning("Graph 邮箱:未收到响应正文。");
return;
}
using var doc = JsonDocument.Parse(json);
if (!doc.RootElement.TryGetProperty("value", out var messages) ||
messages.ValueKind != JsonValueKind.Array)
{
if (json.Contains("error", StringComparison.OrdinalIgnoreCase))
_logger.LogWarning("Graph 邮箱接口错误响应: {Json}", json);
else
_logger.LogWarning("Graph 邮箱:响应中无 value 数组。");
return;
}
var newCount = 0;
foreach (var mail in messages.EnumerateArray())
{
var id = GetString(mail, "id");
if (string.IsNullOrWhiteSpace(id))
continue;
if (!_processedMessageIds.Add(id))
continue;
newCount++;
var subject = GetString(mail, "subject");
var from = GetNestedString(mail, "from", "emailAddress", "address");
var receivedDateTime = GetString(mail, "receivedDateTime");
var bodyPreview = GetString(mail, "bodyPreview");
var conversationId = GetString(mail, "conversationId");
_logger.LogInformation(
"Graph 新邮件 Id={Id} From={From} Subject={Subject} Received={Received} ConversationId={Conv} Preview={Preview}",
id, from, subject, receivedDateTime, conversationId, bodyPreview);
if (!string.IsNullOrWhiteSpace(subject) &&
subject.StartsWith("Re:", StringComparison.OrdinalIgnoreCase))
{
_logger.LogInformation("Graph 新邮件检测到回复主题: {Subject}", subject);
}
}
if (newCount == 0 && options.LogEachPollWhenEmpty)
_logger.LogDebug("Graph 邮箱轮询:无新邮件(起点 UTC {Start:O})", startUtc);
}
private static string GetString(JsonElement element, string propertyName)
{
if (element.TryGetProperty(propertyName, out var value))
{
return value.ValueKind switch
{
JsonValueKind.String => value.GetString() ?? string.Empty,
JsonValueKind.Null => string.Empty,
_ => value.ToString()
};
}
return string.Empty;
}
private static string GetNestedString(JsonElement element, params string[] propertyPath)
{
var current = element;
foreach (var property in propertyPath)
{
if (!current.TryGetProperty(property, out current))
return string.Empty;
}
return current.ValueKind == JsonValueKind.String
? current.GetString() ?? string.Empty
: current.ToString();
}
}