MicrosoftGraphInboxPollerHostedService.cs 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170
  1. using System.Text.Json;
  2. using Microsoft.Extensions.Options;
  3. using Microsoft.Identity.Client;
  4. namespace OASystem.API.OAMethodLib.MicrosoftGraphMailbox;
  5. /// <summary>
  6. /// 后台轮询收件箱:仅当 <see cref="MicrosoftGraphMailboxOptions.Enabled"/> 为 true 时才会登录并访问 Graph。
  7. /// 首次无缓存时会通过交互式浏览器完成授权(重定向 URI 与配置一致)。
  8. /// </summary>
  9. public sealed class MicrosoftGraphInboxPollerHostedService : BackgroundService
  10. {
  11. private readonly IOptionsMonitor<MicrosoftGraphMailboxOptions> _optionsMonitor;
  12. private readonly IMicrosoftGraphMailboxService _graphMailbox;
  13. private readonly ILogger<MicrosoftGraphInboxPollerHostedService> _logger;
  14. private readonly HashSet<string> _processedMessageIds = new(StringComparer.Ordinal);
  15. private DateTime? _monitorStartUtc;
  16. private bool _startupProfileLogged;
  17. public MicrosoftGraphInboxPollerHostedService(
  18. IOptionsMonitor<MicrosoftGraphMailboxOptions> optionsMonitor,
  19. IMicrosoftGraphMailboxService graphMailbox,
  20. ILogger<MicrosoftGraphInboxPollerHostedService> logger)
  21. {
  22. _optionsMonitor = optionsMonitor;
  23. _graphMailbox = graphMailbox;
  24. _logger = logger;
  25. }
  26. protected override async Task ExecuteAsync(CancellationToken stoppingToken)
  27. {
  28. while (!stoppingToken.IsCancellationRequested)
  29. {
  30. var options = _optionsMonitor.CurrentValue;
  31. if (!options.Enabled)
  32. {
  33. await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken).ConfigureAwait(false);
  34. continue;
  35. }
  36. if (_monitorStartUtc == null)
  37. _monitorStartUtc = DateTime.UtcNow;
  38. try
  39. {
  40. if (!_startupProfileLogged && options.LogProfileOnStartup)
  41. {
  42. var meJson = await _graphMailbox.GetMeRawJsonAsync(stoppingToken).ConfigureAwait(false);
  43. if (!string.IsNullOrEmpty(meJson))
  44. _logger.LogInformation("Graph 邮箱:当前用户 /me 响应(节选) {Snippet}",
  45. meJson.Length > 500 ? meJson[..500] + "…" : meJson);
  46. _startupProfileLogged = true;
  47. }
  48. await PollInboxAsync(stoppingToken).ConfigureAwait(false);
  49. }
  50. catch (MsalException ex)
  51. {
  52. _logger.LogError(ex, "Graph 邮箱 MSAL 认证失败");
  53. }
  54. catch (HttpRequestException ex)
  55. {
  56. _logger.LogError(ex, "Graph 邮箱 HTTP 请求失败");
  57. }
  58. catch (OperationCanceledException) when (stoppingToken.IsCancellationRequested)
  59. {
  60. throw;
  61. }
  62. catch (Exception ex)
  63. {
  64. _logger.LogError(ex, "Graph 邮箱轮询异常");
  65. }
  66. var interval = TimeSpan.FromSeconds(Math.Clamp(options.PollIntervalSeconds, 5, 3600));
  67. await Task.Delay(interval, stoppingToken).ConfigureAwait(false);
  68. }
  69. }
  70. private async Task PollInboxAsync(CancellationToken cancellationToken)
  71. {
  72. var options = _optionsMonitor.CurrentValue;
  73. var startUtc = _monitorStartUtc ?? DateTime.UtcNow;
  74. var json = await _graphMailbox.GetInboxMessagesJsonSinceAsync(startUtc, cancellationToken)
  75. .ConfigureAwait(false);
  76. if (string.IsNullOrEmpty(json))
  77. {
  78. _logger.LogWarning("Graph 邮箱:未收到响应正文。");
  79. return;
  80. }
  81. using var doc = JsonDocument.Parse(json);
  82. if (!doc.RootElement.TryGetProperty("value", out var messages) ||
  83. messages.ValueKind != JsonValueKind.Array)
  84. {
  85. if (json.Contains("error", StringComparison.OrdinalIgnoreCase))
  86. _logger.LogWarning("Graph 邮箱接口错误响应: {Json}", json);
  87. else
  88. _logger.LogWarning("Graph 邮箱:响应中无 value 数组。");
  89. return;
  90. }
  91. var newCount = 0;
  92. foreach (var mail in messages.EnumerateArray())
  93. {
  94. var id = GetString(mail, "id");
  95. if (string.IsNullOrWhiteSpace(id))
  96. continue;
  97. if (!_processedMessageIds.Add(id))
  98. continue;
  99. newCount++;
  100. var subject = GetString(mail, "subject");
  101. var from = GetNestedString(mail, "from", "emailAddress", "address");
  102. var receivedDateTime = GetString(mail, "receivedDateTime");
  103. var bodyPreview = GetString(mail, "bodyPreview");
  104. var conversationId = GetString(mail, "conversationId");
  105. _logger.LogInformation(
  106. "Graph 新邮件 Id={Id} From={From} Subject={Subject} Received={Received} ConversationId={Conv} Preview={Preview}",
  107. id, from, subject, receivedDateTime, conversationId, bodyPreview);
  108. if (!string.IsNullOrWhiteSpace(subject) &&
  109. subject.StartsWith("Re:", StringComparison.OrdinalIgnoreCase))
  110. {
  111. _logger.LogInformation("Graph 新邮件检测到回复主题: {Subject}", subject);
  112. }
  113. }
  114. if (newCount == 0 && options.LogEachPollWhenEmpty)
  115. _logger.LogDebug("Graph 邮箱轮询:无新邮件(起点 UTC {Start:O})", startUtc);
  116. }
  117. private static string GetString(JsonElement element, string propertyName)
  118. {
  119. if (element.TryGetProperty(propertyName, out var value))
  120. {
  121. return value.ValueKind switch
  122. {
  123. JsonValueKind.String => value.GetString() ?? string.Empty,
  124. JsonValueKind.Null => string.Empty,
  125. _ => value.ToString()
  126. };
  127. }
  128. return string.Empty;
  129. }
  130. private static string GetNestedString(JsonElement element, params string[] propertyPath)
  131. {
  132. var current = element;
  133. foreach (var property in propertyPath)
  134. {
  135. if (!current.TryGetProperty(property, out current))
  136. return string.Empty;
  137. }
  138. return current.ValueKind == JsonValueKind.String
  139. ? current.GetString() ?? string.Empty
  140. : current.ToString();
  141. }
  142. }