MicrosoftGraphMailboxService.cs 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. using System.Net.Http.Headers;
  2. using System.Text;
  3. using System.Text.Json;
  4. using System.Text.Json.Serialization;
  5. using Microsoft.Extensions.Options;
  6. using Microsoft.Identity.Client;
  7. namespace OASystem.API.OAMethodLib.MicrosoftGraphMailbox;
  8. public class MicrosoftGraphMailboxService : IMicrosoftGraphMailboxService
  9. {
  10. private readonly IHttpClientFactory _httpClientFactory;
  11. private readonly IOptionsMonitor<MicrosoftGraphMailboxOptions> _options;
  12. private readonly ILogger<MicrosoftGraphMailboxService> _logger;
  13. private const string HttpClientName = "MicrosoftGraph";
  14. public MicrosoftGraphMailboxService(
  15. IHttpClientFactory httpClientFactory,
  16. IOptionsMonitor<MicrosoftGraphMailboxOptions> options,
  17. ILogger<MicrosoftGraphMailboxService> logger)
  18. {
  19. _httpClientFactory = httpClientFactory;
  20. _options = options;
  21. _logger = logger;
  22. }
  23. public async Task<string?> GetMeRawJsonAsync(string graphAccessToken, CancellationToken cancellationToken = default)
  24. {
  25. using var client = CreateAuthenticatedClient(graphAccessToken);
  26. using var response = await client.GetAsync("me", cancellationToken).ConfigureAwait(false);
  27. var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
  28. if (!response.IsSuccessStatusCode)
  29. _logger.LogWarning("Graph GET /me 失败: {Status} {Body}", (int)response.StatusCode, body);
  30. return body;
  31. }
  32. public async Task<string?> GetInboxMessagesJsonSinceAsync(DateTime startUtc, string graphAccessToken, CancellationToken cancellationToken = default)
  33. {
  34. var opt = _options.CurrentValue;
  35. var startTime = startUtc.ToString("o");
  36. var url =
  37. "me/mailFolders/inbox/messages" +
  38. "?$select=id,subject,from,receivedDateTime,bodyPreview,conversationId" +
  39. $"&$filter=receivedDateTime ge {startTime}" +
  40. "&$orderby=receivedDateTime desc" +
  41. $"&$top={opt.TopMessages}";
  42. using var client = CreateAuthenticatedClient(graphAccessToken);
  43. using var response = await client.GetAsync(url, cancellationToken).ConfigureAwait(false);
  44. var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
  45. if (!response.IsSuccessStatusCode)
  46. _logger.LogWarning("Graph 拉取收件箱失败: {Status} {Body}", (int)response.StatusCode, body);
  47. return body;
  48. }
  49. public async Task SendMailAsync(string toEmail, string subject, string textBody, string graphAccessToken, CancellationToken cancellationToken = default)
  50. {
  51. var payload = new GraphSendMailRequest
  52. {
  53. Message = new GraphSendMailMessage
  54. {
  55. Subject = subject,
  56. Body = new GraphSendMailBody
  57. {
  58. ContentType = "Text",
  59. Content = textBody
  60. },
  61. ToRecipients = new List<GraphSendMailRecipient>
  62. {
  63. new()
  64. {
  65. EmailAddress = new GraphSendMailEmail { Address = toEmail }
  66. }
  67. }
  68. },
  69. SaveToSentItems = true
  70. };
  71. var json = System.Text.Json.JsonSerializer.Serialize(payload, new JsonSerializerOptions
  72. {
  73. DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
  74. });
  75. using var client = CreateAuthenticatedClient(graphAccessToken);
  76. using var content = new StringContent(json, Encoding.UTF8, "application/json");
  77. using var response = await client.PostAsync("me/sendMail", content, cancellationToken).ConfigureAwait(false);
  78. var responseText = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
  79. if (!response.IsSuccessStatusCode)
  80. {
  81. _logger.LogError("Graph sendMail 失败: {Status} {Body}", (int)response.StatusCode, responseText);
  82. response.EnsureSuccessStatusCode();
  83. }
  84. }
  85. private HttpClient CreateAuthenticatedClient(string graphAccessToken)
  86. {
  87. if (string.IsNullOrWhiteSpace(graphAccessToken))
  88. throw new ArgumentException("Graph 访问令牌不能为空。", nameof(graphAccessToken));
  89. var client = _httpClientFactory.CreateClient(HttpClientName);
  90. client.DefaultRequestHeaders.Authorization =
  91. new AuthenticationHeaderValue("Bearer", graphAccessToken.Trim());
  92. return client;
  93. }
  94. public async Task<string> RefreshAccessTokenAsync(
  95. string clientId,
  96. string tenant,
  97. string[] scopes,
  98. string tokenCacheBase64, // 从Redis拿到的缓存(Base64字符串)
  99. string homeAccountId // 用户标识(建议存这个)
  100. )
  101. {
  102. // 1️⃣ 创建 MSAL 应用
  103. var app = PublicClientApplicationBuilder
  104. .Create(clientId)
  105. .WithAuthority($"https://login.microsoftonline.com/{tenant}")
  106. .WithDefaultRedirectUri()
  107. .Build();
  108. // 2️⃣ 恢复 TokenCache
  109. if (!string.IsNullOrEmpty(tokenCacheBase64))
  110. {
  111. var cacheBytes = Convert.FromBase64String(tokenCacheBase64);
  112. app.UserTokenCache.SetBeforeAccess(args =>
  113. {
  114. args.TokenCache.DeserializeMsalV3(cacheBytes, false);
  115. });
  116. app.UserTokenCache.SetAfterAccess(args =>
  117. {
  118. });
  119. }
  120. else
  121. {
  122. throw new InvalidOperationException("TokenCache 为空,无法刷新");
  123. }
  124. // 3️⃣ 找到对应用户
  125. var accounts = await app.GetAccountsAsync();
  126. var account = accounts.FirstOrDefault(a =>
  127. string.Equals(a.HomeAccountId?.Identifier, homeAccountId, StringComparison.Ordinal));
  128. if (account == null)
  129. {
  130. throw new InvalidOperationException("未找到匹配的用户账号,需要重新登录");
  131. }
  132. // 4️⃣ 刷新(核心)
  133. try
  134. {
  135. var result = await app
  136. .AcquireTokenSilent(scopes, account)
  137. .ExecuteAsync();
  138. return result.AccessToken;
  139. }
  140. catch (MsalUiRequiredException ex)
  141. {
  142. throw new InvalidOperationException("刷新失败,需要重新登录", ex);
  143. }
  144. }
  145. }