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(); } }