using Microsoft.AspNetCore.WebUtilities;
using Microsoft.Graph;
using Microsoft.Graph.Models;
using Microsoft.Graph.Models.ODataErrors;
using Microsoft.Kiota.Abstractions.Authentication;
using StackExchange.Redis;
using System.Text.Json;
using System.Text.Json.Serialization;
using static OASystem.API.OAMethodLib.Hotmail.HotmailService;
namespace OASystem.API.OAMethodLib.Hotmail
{
public class HotmailService
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IConfiguration _config;
private readonly SqlSugarClient _sqlSugar;
private const string RedisKeyPrefix = "MailAlchemy:Token:";
public HotmailService(IHttpClientFactory httpClientFactory, IConfiguration config, SqlSugarClient sqlSugar)
{
_httpClientFactory = httpClientFactory;
_config = config;
_sqlSugar = sqlSugar;
}
///
/// hotmail 信息验证
///
///
///
public (bool, string) ConfigVerify(HotmailConfig? config)
{
if (config == null) return (true, "当前用户未配置 hotmail 基础信息。");
if (string.IsNullOrEmpty(config.UserName)) return (true, "当前用户未配置 hotmail 基础信息。");
if (string.IsNullOrEmpty(config.ClientId)) return (true, "当前用户未配置 hotmail 租户标识符 (Guid)。");
if (string.IsNullOrEmpty(config.TenantId)) return (true, "当前用户未配置 hotmail 应用程序的客户端标识。");
if (string.IsNullOrEmpty(config.ClientSecret)) return (true, "当前用户未配置 hotmail 应用程序密钥。");
if (string.IsNullOrEmpty(config.RedirectUri)) return (true, "当前用户未配置 hotmail OAuth2 回调重定向地址。");
return (true, "");
}
///
/// Microsoft 鉴权预处理 - 生产增强版
///
public async Task<(int status, string msg)> PrepareAuth(int userId)
{
// 1. 获取用户信息,支持空合并优化
var userName = await _sqlSugar.Queryable()
.Where(x => x.IsDel == 0 && x.Id == userId)
.Select(x => x.CnName)
.FirstAsync() ?? "未知用户";
var userConfig = await GetUserMailConfig(userId);
if (userConfig == null)
return (-1, $"[{userName}] Hotmail 基础配置缺失");
if (string.IsNullOrWhiteSpace(userConfig.UserName))
return (-1, $"[{userName}] 未配置邮箱账号");
// 2. 验证状态检查
var redisKey = $"{RedisKeyPrefix}{userConfig.UserName.Trim()}";
var cachedJson = await RedisRepository.RedisFactory.CreateRedisRepository().StringGetAsync(redisKey);
// 修正:已通过验证应返回 0
if (!string.IsNullOrWhiteSpace(cachedJson))
return (0, $"{userName} 已通过验证,无需重复操作");
// 3. 授权参数深度净化
var clientId = userConfig.ClientId?.Trim();
var redirectUri = userConfig.RedirectUri?.Trim().Replace("\r", "").Replace("\n", ""); // 彻底剔除换行符
//var redirectUri = "http://localhost:5256/api/microsoft/auth/callback";
if (string.IsNullOrWhiteSpace(clientId))
return (-1, $"[{userName}] 客户端 ID (ClientId) 未配置");
if (string.IsNullOrWhiteSpace(redirectUri))
return (-1, $"[{userName}] 回调地址 (RedirectUri) 未配置");
// 4. 构建授权 URL
const string authEndpoint = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
var queryParams = new Dictionary
{
{ "client_id", clientId },
{ "response_type", "code" },
{ "redirect_uri", redirectUri },
{ "response_mode", "query" },
{ "scope", "offline_access Mail.Read Mail.Send User.Read" },
{ "state", userId.ToString() }
//{ "state", Guid.NewGuid().ToString("N") }
};
// QueryHelpers 会处理 URL 编码,确保 RedirectUri 不会被二次破坏
var authUrl = QueryHelpers.AddQueryString(authEndpoint, queryParams);
return (1, authUrl);
}
///
/// 获取多个账户的合并收件箱 (并行处理)
///
public async Task> GetMergedMessagesAsync(List emails, DateTime cstStart, DateTime cstEnd)
{
var tasks = emails.Select(async email =>
{
try
{
var client = await GetClientAsync(email);
// 转换北京时间为 UTC 字符串
string startFilter = CommonFun.ToGraphUtcString(cstStart);
string endFilter = CommonFun.ToGraphUtcString(cstEnd);
var response = await client.Me.Messages.GetAsync(q =>
{
q.QueryParameters.Filter = $"receivedDateTime ge {startFilter} and receivedDateTime le {endFilter}";
q.QueryParameters.Select = new[] { "id", "subject", "from", "toRecipients", "body", "receivedDateTime" };
q.QueryParameters.Orderby = new[] { "receivedDateTime desc" };
});
return response?.Value?.Select(m => new MailDto
{
MessageId = m.Id,
Subject = m.Subject,
Content = m.Body?.Content,
From = m.From?.EmailAddress?.Address,
// 关键:将 Graph 返回的 UTC 时间转回北京时间给前端显示
ReceivedTime = TimeZoneInfo.ConvertTimeFromUtc(m.ReceivedDateTime.Value.DateTime,TimeZoneInfo.FindSystemTimeZoneById("China Standard Time")),
Source = email
}).ToList() ?? Enumerable.Empty();
}
catch (Exception ex)
{
Console.WriteLine($"[Error] Account {email}: {ex.Message}");
return Enumerable.Empty();
}
});
var results = await Task.WhenAll(tasks);
return results.SelectMany(x => x).OrderByDescending(m => m.ReceivedTime).ToList();
}
///
/// 指定账户发送邮件
///
public async Task SendMailAsync(string fromEmail, MailDto mail)
{
try
{
var client = await GetClientAsync(fromEmail);
var requestBody = new Microsoft.Graph.Me.SendMail.SendMailPostRequestBody
{
Message = new Message
{
Subject = mail.Subject,
Body = new ItemBody
{
Content = mail.Content,
ContentType = BodyType.Html
},
ToRecipients = new List
{
new Recipient { EmailAddress = new EmailAddress { Address = mail.To } }
}
}
};
// 执行发送
await client.Me.SendMail.PostAsync(requestBody);
return new MailSendResult { IsSuccess = true, Message = "邮件发送成功!" };
}
catch (ODataError odataError) // 捕获 Graph 特有异常
{
// 常见的错误:ErrorInvalidUser, ErrorQuotaExceeded, ErrorMessageSubmissionBlocked
var code = odataError.Error?.Code ?? "Unknown";
var msg = odataError.Error?.Message ?? "微软 API 调用异常";
return new MailSendResult
{
IsSuccess = false,
ErrorCode = code,
Message = $"发送失败: {msg}"
};
}
catch (Exception ex)
{
return new MailSendResult
{
IsSuccess = false,
ErrorCode = "InternalError",
Message = $"系统内部错误: {ex.Message}"
};
}
}
///
/// 获取邮箱配置信息 - single
///
///
public async Task GetUserMailConfig(int userId)
{
var allConfigs = await GetUserMailConfigListAsync();
if (allConfigs == null || !allConfigs.Any()) return null;
var userConfig = allConfigs.FirstOrDefault(x => x.UserId == userId);
return userConfig;
}
///
/// 获取邮箱配置信息 - ALL
///
///
public async Task?> GetUserMailConfigListAsync()
{
var remark = await _sqlSugar.Queryable()
.Where(x => x.IsDel == 0 && x.Id == 1555 && x.STid == 137)
.Select(x => x.Remark)
.FirstAsync();
if (string.IsNullOrWhiteSpace(remark)) return null;
try
{
var allConfigs = JsonConvert.DeserializeObject>(remark);
return allConfigs;
}
catch (Exception)
{
return null;
}
}
///
/// 获取 Graph 客户端,处理 Token 自动刷新
///
private async Task GetClientAsync(string email)
{
var cachedJson = await RedisRepository.RedisFactory.CreateRedisRepository().StringGetAsync($"{RedisKeyPrefix}{email}");
if (string.IsNullOrEmpty(cachedJson)) throw new UnauthorizedAccessException($"Account {email} not initialized in Redis.");
var token = System.Text.Json.JsonSerializer.Deserialize(cachedJson!)!;
// 令牌过期预校验 (提前 5 分钟)
if (token.ExpiresAt < DateTime.UtcNow.AddMinutes(5))
{
token = await RefreshAndSaveTokenAsync(token);
}
var authProvider = new BaseBearerTokenAuthenticationProvider(new StaticTokenProvider(token.AccessToken));
return new GraphServiceClient(authProvider);
}
public async Task RefreshAndSaveTokenAsync(UserToken oldToken)
{
var httpClient = _httpClientFactory.CreateClient();
var kvp = new Dictionary {
{ "client_id", _config["AzureAd:ClientId"] },
{ "client_secret", _config["AzureAd:ClientSecret"] },
{ "grant_type", "refresh_token" },
{ "refresh_token", oldToken.RefreshToken },
{ "scope", "offline_access Mail.Read Mail.Send" }
};
var response = await httpClient.PostAsync("https://login.microsoftonline.com/common/oauth2/v2.0/token", new FormUrlEncodedContent(kvp));
if (!response.IsSuccessStatusCode) throw new Exception("Token refresh failed.");
using var doc = await JsonDocument.ParseAsync(await response.Content.ReadAsStreamAsync());
var root = doc.RootElement;
var newToken = new UserToken
{
Email = oldToken.Email,
AccessToken = root.GetProperty("access_token").GetString()!,
RefreshToken = root.GetProperty("refresh_token").GetString()!,
ExpiresAt = DateTime.UtcNow.AddSeconds(root.GetProperty("expires_in").GetInt32())
};
// 存入 Redis,持久化 90 天(RefreshToken 的典型寿命)
await RedisRepository.RedisFactory.CreateRedisRepository().StringSetAsync($"{RedisKeyPrefix}{oldToken.Email}", System.Text.Json.JsonSerializer.Serialize(newToken), TimeSpan.FromDays(90));
return newToken;
}
///
/// 静态 Token 提供者辅助类
///
public class StaticTokenProvider : IAccessTokenProvider
{
private readonly string _token;
public StaticTokenProvider(string token) => _token = token;
public Task GetAuthorizationTokenAsync(Uri uri, Dictionary? context = null, CancellationToken ct = default) => Task.FromResult(_token);
public AllowedHostsValidator AllowedHostsValidator { get; } = new();
}
#region 数据模型
public class MailSendResult
{
public bool IsSuccess { get; set; }
public string Message { get; set; } = string.Empty;
public string? ErrorCode { get; set; } // Microsoft 提供的错误码
public string Source => "Microsoft_Graph_API";
}
///
/// Hotmail 邮件服务 OAuth2 配置信息实体
///
public class HotmailConfig
{
///
/// 用户唯一标识
///
[JsonPropertyName("userId")]
public int UserId { get; set; }
///
/// 账号用户名
///
[JsonPropertyName("userName")]
public string UserName { get; set; }
///
/// Azure AD 租户标识符 (Guid)
///
[JsonPropertyName("tenantId")]
public string TenantId { get; set; }
///
/// 注册应用程序的客户端标识
///
[JsonPropertyName("clientId")]
public string ClientId { get; set; }
///
/// 客户端密钥(敏感数据建议加密存储)
///
[JsonPropertyName("clientSecret")]
public string ClientSecret { get; set; }
///
/// 租户类型(如 common, organizations 或具体域名)
///
[JsonPropertyName("tenant")]
public string Tenant { get; set; } = "common";
///
/// OAuth2 回调重定向地址
///
[JsonPropertyName("redirectUri")]
public string RedirectUri { get; set; }
}
public class UserToken
{
public string Email { get; set; }
public string AccessToken { get; set; }
public string RefreshToken { get; set; }
public DateTime ExpiresAt { get; set; }
}
///
/// 邮件请求对象
///
public class MailDto
{
///
/// 邮件唯一标识符 (UID/Message-ID)
///
[JsonPropertyName("messageId")]
public string? MessageId { get; set; }
///
/// 邮件主题
///
[JsonPropertyName("subject")]
public string? Subject { get; set; }
///
/// 发件人地址 (e.g. "sender@example.com")
///
[JsonPropertyName("from")]
public string? From { get; set; }
///
/// 收件人地址
///
[JsonPropertyName("to")]
public string? To { get; set; }
///
/// 邮件正文内容 (HTML 或纯文本)
///
[JsonPropertyName("content")]
public string? Content { get; set; }
///
/// 接收时间 - 使用 DateTimeOffset 以确保跨时区准确性
///
[JsonPropertyName("receivedTime")]
public DateTimeOffset? ReceivedTime { get; set; }
///
/// 数据来源标识 (用于区分不同配置源或采集渠道,如 "Hotmail", "Gmail", "Sys_SetData")
///
[JsonPropertyName("source")]
public string? Source { get; set; } = "Hotmail";
}
#endregion
}
}