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 _options; private readonly ILogger _logger; private const string HttpClientName = "MicrosoftGraph"; public MicrosoftGraphMailboxService( IHttpClientFactory httpClientFactory, IOptionsMonitor options, ILogger logger) { _httpClientFactory = httpClientFactory; _options = options; _logger = logger; } public async Task 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 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 { 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 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); } } }