using Application.Abstractions.Cache;
using Application.Abstractions.Messaging.Email;
using Application.Features.Config.Get;
using Infrastructure.Persistence;
using SharedKernel;
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Infrastructure.Messaging.Email;
///
/// SMTP 직접 발송. Admin 단에서만 사용. (API는 QueuedMailService로 큐잉)
///
public class DirectMailService : IMailService
{
private readonly ILogger _logger;
private readonly ICacheService _cache;
private readonly AppSettings.SmtpSection _smtp;
private readonly AppDbContext _db;
// SMTP 정보
public string Host { get; set; } = default!;
public int Port { get; set; } = 587;
public string? User { get; set; }
public string? Password { get; set; }
public bool UseStartTls { get; set; } = true;
public string FromEmail { get; set; } = default!;
public string FromName { get; set; } = "no-reply";
public DirectMailService(ILogger logger, ICacheService cache, IOptions options, AppDbContext db)
{
_logger = logger;
_cache = cache;
_smtp = options.Value.SMTP;
_db = db;
}
public async Task SendAsync(SendData data, CancellationToken ct = default)
{
_logger.LogInformation("Sending email to {ToAddress} with subject {Subject}", data.ToAddress, data.Subject);
// 1. Redis 캐시에서 Config 조회
var config = await _cache.GetAsync(CacheKeys.Config, ct);
if (config is null)
{
var entity = await _db.Config.AsNoTracking().OrderByDescending(x => x.ID).FirstOrDefaultAsync();
if (entity is null)
{
Host = _smtp.Host;
Port = _smtp.Port;
User = _smtp.User;
Password = _smtp.Password;
UseStartTls = _smtp.UseStartTls;
FromEmail = _smtp.FromEmail;
FromName = _smtp.FromName;
}
else
{
Host = entity.Basic.SmtpServer ?? "";
Port = entity.Basic.SmtpPort ?? 587;
UseStartTls = entity.Basic.SmtpEnableSSL;
User = entity.Basic.SmtpUsername;
Password = entity.Basic.SmtpPassword;
FromEmail = entity.Basic.FromEmail ?? "";
FromName = entity.Basic.FromName ?? "";
}
}
else
{
Host = config.Basic.SmtpServer ?? _smtp.Host;
Port = config.Basic.SmtpPort ?? _smtp.Port;
UseStartTls = config.Basic.SmtpEnableSSL;
User = config.Basic.SmtpUsername ?? _smtp.User;
Password = config.Basic.SmtpPassword ?? _smtp.Password;
FromEmail = config.Basic.FromEmail ?? _smtp.FromEmail;
FromName = config.Basic.FromName ?? _smtp.FromName;
}
// FromEmail 의 도메인을 SMTP HELO/EHLO + Message-Id FQDN 으로 사용한다.
// - 기본 hostname (예: "prod", "localhost") 으로 HELO 보내면 mail.dpot.dev rspamd 가 milter-reject (5.7.1).
// - Message-Id 의 host part 가 FQDN 이 아니면 (예: <...@prod>) Gmail 등이 스팸함行 신호로 사용.
var atIndex = FromEmail.IndexOf('@');
var fromDomain = atIndex >= 0 && atIndex < FromEmail.Length - 1
? FromEmail[(atIndex + 1)..]
: "dpot.dev";
var message = new MimeMessage();
message.From.Add(new MailboxAddress(FromName, FromEmail));
message.To.Add(MailboxAddress.Parse(data.ToAddress));
message.Subject = data.Subject;
message.MessageId = MimeKit.Utils.MimeUtils.GenerateMessageId(fromDomain);
var body = new BodyBuilder
{
HtmlBody = data.MessageHtml,
TextBody = data.MessageText
};
message.Body = body.ToMessageBody();
using var client = new SmtpClient
{
LocalDomain = fromDomain
};
var secure = Port == 465 ? SecureSocketOptions.SslOnConnect : UseStartTls ? SecureSocketOptions.StartTls : SecureSocketOptions.Auto;
await client.ConnectAsync(Host, Port, secure, ct);
if (!string.IsNullOrWhiteSpace(User))
{
await client.AuthenticateAsync(User, Password, ct);
}
await client.SendAsync(message, ct);
await client.DisconnectAsync(true, ct);
_logger.LogInformation("Email sent to {ToAddress} with subject {Subject}", data.ToAddress, data.Subject);
}
}