| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169 |
- using System.Net.Http.Headers;
- using System.Text;
- using System.Text.Json;
- using System.Text.Json.Serialization;
- using Microsoft.Extensions.Options;
- using Microsoft.Identity.Client;
- namespace OASystem.API.OAMethodLib.MicrosoftGraphMailbox;
- public class MicrosoftGraphMailboxService : IMicrosoftGraphMailboxService
- {
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly IOptionsMonitor<MicrosoftGraphMailboxOptions> _options;
- private readonly ILogger<MicrosoftGraphMailboxService> _logger;
- private const string HttpClientName = "MicrosoftGraph";
- public MicrosoftGraphMailboxService(
- IHttpClientFactory httpClientFactory,
- IOptionsMonitor<MicrosoftGraphMailboxOptions> options,
- ILogger<MicrosoftGraphMailboxService> logger)
- {
- _httpClientFactory = httpClientFactory;
- _options = options;
- _logger = logger;
- }
- public async Task<string?> GetMeRawJsonAsync(string graphAccessToken, CancellationToken cancellationToken = default)
- {
- using var client = CreateAuthenticatedClient(graphAccessToken);
- using var response = await client.GetAsync("me", cancellationToken).ConfigureAwait(false);
- var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- _logger.LogWarning("Graph GET /me 失败: {Status} {Body}", (int)response.StatusCode, body);
- return body;
- }
- public async Task<string?> GetInboxMessagesJsonSinceAsync(DateTime startUtc, string graphAccessToken, CancellationToken cancellationToken = default)
- {
- var opt = _options.CurrentValue;
- var startTime = startUtc.ToString("o");
- var url =
- "me/mailFolders/inbox/messages" +
- "?$select=id,subject,from,receivedDateTime,bodyPreview,conversationId" +
- $"&$filter=receivedDateTime ge {startTime}" +
- "&$orderby=receivedDateTime desc" +
- $"&$top={opt.TopMessages}";
- using var client = CreateAuthenticatedClient(graphAccessToken);
- using var response = await client.GetAsync(url, cancellationToken).ConfigureAwait(false);
- var body = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- _logger.LogWarning("Graph 拉取收件箱失败: {Status} {Body}", (int)response.StatusCode, body);
- return body;
- }
- public async Task SendMailAsync(string toEmail, string subject, string textBody, string graphAccessToken, CancellationToken cancellationToken = default)
- {
- var payload = new GraphSendMailRequest
- {
- Message = new GraphSendMailMessage
- {
- Subject = subject,
- Body = new GraphSendMailBody
- {
- ContentType = "Text",
- Content = textBody
- },
- ToRecipients = new List<GraphSendMailRecipient>
- {
- new()
- {
- EmailAddress = new GraphSendMailEmail { Address = toEmail }
- }
- }
- },
- SaveToSentItems = true
- };
- var json = System.Text.Json.JsonSerializer.Serialize(payload, new JsonSerializerOptions
- {
- DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
- });
- using var client = CreateAuthenticatedClient(graphAccessToken);
- using var content = new StringContent(json, Encoding.UTF8, "application/json");
- using var response = await client.PostAsync("me/sendMail", content, cancellationToken).ConfigureAwait(false);
- var responseText = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
- if (!response.IsSuccessStatusCode)
- {
- _logger.LogError("Graph sendMail 失败: {Status} {Body}", (int)response.StatusCode, responseText);
- response.EnsureSuccessStatusCode();
- }
- }
- private HttpClient CreateAuthenticatedClient(string graphAccessToken)
- {
- if (string.IsNullOrWhiteSpace(graphAccessToken))
- throw new ArgumentException("Graph 访问令牌不能为空。", nameof(graphAccessToken));
- var client = _httpClientFactory.CreateClient(HttpClientName);
- client.DefaultRequestHeaders.Authorization =
- new AuthenticationHeaderValue("Bearer", graphAccessToken.Trim());
- return client;
- }
- public async Task<string> RefreshAccessTokenAsync(
- string clientId,
- string tenant,
- string[] scopes,
- string tokenCacheBase64, // 从Redis拿到的缓存(Base64字符串)
- string homeAccountId // 用户标识(建议存这个)
- )
- {
- // 1️⃣ 创建 MSAL 应用
- var app = PublicClientApplicationBuilder
- .Create(clientId)
- .WithAuthority($"https://login.microsoftonline.com/{tenant}")
- .WithDefaultRedirectUri()
- .Build();
- // 2️⃣ 恢复 TokenCache
- if (!string.IsNullOrEmpty(tokenCacheBase64))
- {
- var cacheBytes = Convert.FromBase64String(tokenCacheBase64);
- app.UserTokenCache.SetBeforeAccess(args =>
- {
- args.TokenCache.DeserializeMsalV3(cacheBytes, false);
- });
- app.UserTokenCache.SetAfterAccess(args =>
- {
- });
- }
- else
- {
- throw new InvalidOperationException("TokenCache 为空,无法刷新");
- }
- // 3️⃣ 找到对应用户
- var accounts = await app.GetAccountsAsync();
- var account = accounts.FirstOrDefault(a =>
- string.Equals(a.HomeAccountId?.Identifier, homeAccountId, StringComparison.Ordinal));
- if (account == null)
- {
- throw new InvalidOperationException("未找到匹配的用户账号,需要重新登录");
- }
- // 4️⃣ 刷新(核心)
- try
- {
- var result = await app
- .AcquireTokenSilent(scopes, account)
- .ExecuteAsync();
- return result.AccessToken;
- }
- catch (MsalUiRequiredException ex)
- {
- throw new InvalidOperationException("刷新失败,需要重新登录", ex);
- }
- }
- }
|