| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201 |
- 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;
- using Microsoft.Identity.Client.Extensions.Msal;
- namespace OASystem.API.OAMethodLib.MicrosoftGraphMailbox;
- public class MicrosoftGraphMailboxService : IMicrosoftGraphMailboxService
- {
- private static readonly string[] Scopes =
- {
- "Mail.Read",
- "User.Read",
- "Mail.Send"
- };
- private readonly IHttpClientFactory _httpClientFactory;
- private readonly IOptionsMonitor<MicrosoftGraphMailboxOptions> _options;
- private readonly ILogger<MicrosoftGraphMailboxService> _logger;
- private readonly SemaphoreSlim _initLock = new(1, 1);
- private readonly SemaphoreSlim _tokenLock = new(1, 1);
- private IPublicClientApplication? _pca;
- private MsalCacheHelper? _cacheHelper;
- 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> GetAccessTokenAsync(CancellationToken cancellationToken = default)
- {
- await EnsureInitializedAsync(cancellationToken).ConfigureAwait(false);
- await _tokenLock.WaitAsync(cancellationToken).ConfigureAwait(false);
- try
- {
- var app = _pca ?? throw new InvalidOperationException("MSAL 未初始化。");
- var accounts = await app.GetAccountsAsync().ConfigureAwait(false);
- var account = accounts.FirstOrDefault();
- try
- {
- var result = await app.AcquireTokenSilent(Scopes, account)
- .ExecuteAsync(cancellationToken)
- .ConfigureAwait(false);
- return result.AccessToken;
- }
- catch (MsalUiRequiredException)
- {
- _logger.LogInformation("Graph 邮箱:需要交互式登录(将打开浏览器),重定向: {Redirect}",
- _options.CurrentValue.RedirectUri);
- var result = await app.AcquireTokenInteractive(Scopes)
- .WithPrompt(Prompt.SelectAccount)
- .ExecuteAsync(cancellationToken)
- .ConfigureAwait(false);
- return result.AccessToken;
- }
- }
- finally
- {
- _tokenLock.Release();
- }
- }
- public async Task<string?> GetMeRawJsonAsync(CancellationToken cancellationToken = default)
- {
- using var client = await CreateAuthenticatedClientAsync(cancellationToken).ConfigureAwait(false);
- 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, 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 = await CreateAuthenticatedClientAsync(cancellationToken).ConfigureAwait(false);
- 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, 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 = await CreateAuthenticatedClientAsync(cancellationToken).ConfigureAwait(false);
- 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 async Task<HttpClient> CreateAuthenticatedClientAsync(CancellationToken cancellationToken)
- {
- var token = await GetAccessTokenAsync(cancellationToken).ConfigureAwait(false);
- var client = _httpClientFactory.CreateClient(HttpClientName);
- client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
- return client;
- }
- private async Task EnsureInitializedAsync(CancellationToken cancellationToken)
- {
- if (_pca != null)
- return;
- await _initLock.WaitAsync(cancellationToken).ConfigureAwait(false);
- try
- {
- if (_pca != null)
- return;
- var opt = _options.CurrentValue;
- if (string.IsNullOrWhiteSpace(opt.ClientId))
- throw new InvalidOperationException("MicrosoftGraphMailbox:ClientId 未配置。");
- var cacheDir = string.IsNullOrWhiteSpace(opt.CacheDirectory)
- ? Path.Combine(
- Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
- "OASystem",
- "MicrosoftGraphMailbox")
- : opt.CacheDirectory;
- Directory.CreateDirectory(cacheDir);
- var storage = new StorageCreationPropertiesBuilder(opt.CacheFileName, cacheDir)
- .Build();
- _cacheHelper = await MsalCacheHelper.CreateAsync(storage).ConfigureAwait(false);
- _pca = PublicClientApplicationBuilder
- .Create(opt.ClientId)
- .WithAuthority($"https://login.microsoftonline.com/{opt.Tenant}")
- .WithRedirectUri(opt.RedirectUri)
- .Build();
- _cacheHelper.RegisterCache(_pca.UserTokenCache);
- _logger.LogInformation("Graph 邮箱 MSAL 已初始化,缓存目录: {Dir}", cacheDir);
- }
- finally
- {
- _initLock.Release();
- }
- }
- }
|