DirectMailService.cs 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123
  1. using Application.Abstractions.Cache;
  2. using Application.Abstractions.Messaging.Email;
  3. using Application.Features.Config.Get;
  4. using Infrastructure.Persistence;
  5. using SharedKernel;
  6. using MailKit.Net.Smtp;
  7. using MailKit.Security;
  8. using MimeKit;
  9. using Microsoft.EntityFrameworkCore;
  10. using Microsoft.Extensions.Logging;
  11. using Microsoft.Extensions.Options;
  12. namespace Infrastructure.Messaging.Email;
  13. /// <summary>
  14. /// SMTP 직접 발송. Admin 단에서만 사용. (API는 QueuedMailService로 큐잉)
  15. /// </summary>
  16. public class DirectMailService : IMailService
  17. {
  18. private readonly ILogger<DirectMailService> _logger;
  19. private readonly ICacheService _cache;
  20. private readonly AppSettings.SmtpSection _smtp;
  21. private readonly AppDbContext _db;
  22. // SMTP 정보
  23. public string Host { get; set; } = default!;
  24. public int Port { get; set; } = 587;
  25. public string? User { get; set; }
  26. public string? Password { get; set; }
  27. public bool UseStartTls { get; set; } = true;
  28. public string FromEmail { get; set; } = default!;
  29. public string FromName { get; set; } = "no-reply";
  30. public DirectMailService(ILogger<DirectMailService> logger, ICacheService cache, IOptions<AppSettings> options, AppDbContext db)
  31. {
  32. _logger = logger;
  33. _cache = cache;
  34. _smtp = options.Value.SMTP;
  35. _db = db;
  36. }
  37. public async Task SendAsync(SendData data, CancellationToken ct = default)
  38. {
  39. _logger.LogInformation("Sending email to {ToAddress} with subject {Subject}", data.ToAddress, data.Subject);
  40. // 1. Redis 캐시에서 Config 조회
  41. var config = await _cache.GetAsync<Response>(CacheKeys.Config, ct);
  42. if (config is null)
  43. {
  44. var entity = await _db.Config.AsNoTracking().OrderByDescending(x => x.ID).FirstOrDefaultAsync();
  45. if (entity is null)
  46. {
  47. Host = _smtp.Host;
  48. Port = _smtp.Port;
  49. User = _smtp.User;
  50. Password = _smtp.Password;
  51. UseStartTls = _smtp.UseStartTls;
  52. FromEmail = _smtp.FromEmail;
  53. FromName = _smtp.FromName;
  54. }
  55. else
  56. {
  57. Host = entity.Basic.SmtpServer ?? "";
  58. Port = entity.Basic.SmtpPort ?? 587;
  59. UseStartTls = entity.Basic.SmtpEnableSSL;
  60. User = entity.Basic.SmtpUsername;
  61. Password = entity.Basic.SmtpPassword;
  62. FromEmail = entity.Basic.FromEmail ?? "";
  63. FromName = entity.Basic.FromName ?? "";
  64. }
  65. }
  66. else
  67. {
  68. Host = config.Basic.SmtpServer ?? _smtp.Host;
  69. Port = config.Basic.SmtpPort ?? _smtp.Port;
  70. UseStartTls = config.Basic.SmtpEnableSSL;
  71. User = config.Basic.SmtpUsername ?? _smtp.User;
  72. Password = config.Basic.SmtpPassword ?? _smtp.Password;
  73. FromEmail = config.Basic.FromEmail ?? _smtp.FromEmail;
  74. FromName = config.Basic.FromName ?? _smtp.FromName;
  75. }
  76. // FromEmail 의 도메인을 SMTP HELO/EHLO + Message-Id FQDN 으로 사용한다.
  77. // - 기본 hostname (예: "prod", "localhost") 으로 HELO 보내면 mail.dpot.dev rspamd 가 milter-reject (5.7.1).
  78. // - Message-Id 의 host part 가 FQDN 이 아니면 (예: <...@prod>) Gmail 등이 스팸함行 신호로 사용.
  79. var atIndex = FromEmail.IndexOf('@');
  80. var fromDomain = atIndex >= 0 && atIndex < FromEmail.Length - 1
  81. ? FromEmail[(atIndex + 1)..]
  82. : "dpot.dev";
  83. var message = new MimeMessage();
  84. message.From.Add(new MailboxAddress(FromName, FromEmail));
  85. message.To.Add(MailboxAddress.Parse(data.ToAddress));
  86. message.Subject = data.Subject;
  87. message.MessageId = MimeKit.Utils.MimeUtils.GenerateMessageId(fromDomain);
  88. var body = new BodyBuilder
  89. {
  90. HtmlBody = data.MessageHtml,
  91. TextBody = data.MessageText
  92. };
  93. message.Body = body.ToMessageBody();
  94. using var client = new SmtpClient
  95. {
  96. LocalDomain = fromDomain
  97. };
  98. var secure = Port == 465 ? SecureSocketOptions.SslOnConnect : UseStartTls ? SecureSocketOptions.StartTls : SecureSocketOptions.Auto;
  99. await client.ConnectAsync(Host, Port, secure, ct);
  100. if (!string.IsNullOrWhiteSpace(User))
  101. {
  102. await client.AuthenticateAsync(User, Password, ct);
  103. }
  104. await client.SendAsync(message, ct);
  105. await client.DisconnectAsync(true, ct);
  106. _logger.LogInformation("Email sent to {ToAddress} with subject {Subject}", data.ToAddress, data.Subject);
  107. }
  108. }