using System.Net; using System.Net.Mail; using bitforum.Models.Log; using bitforum.Models.Account; using bitforum.Constants; using bitforum.Models; using bitforum.Repository; using System.Web; namespace bitforum.Services { public readonly struct SendData { public string ToAddress { get; init; } public string Subject { get; init; } public string Message { get; init; } public Member? Member { get; init; } public SendData(string toAddress, string subject, string message, Member? member = null) { ToAddress = toAddress; Subject = subject; Message = message; Member = member; } } public interface IMailService { public Task SendTemplatedEmailAsync(string toAddress, TemplateKey templateKey, Placeholders placeholder, Member? member = null); public Task SendEmailAsync(SendData sendData); public Task SetEmailLogAsync(SendData sendData); public Task VerifyTokenAsync(string token, VerificationType type); public Task VerifyCodeAsync(string email, string code, VerificationType type); public Task SendRegisterEmailAsync(Member member); public Task SendRegistrationEmailAsync(Member member); public Task SendForgotPasswordEmailAsync(Member member); public Task SendChangedPasswordEmailAsync(Member member); public Task SendWithdrawEmailAsync(Member member); public Task SendEmailVerifyAsync(string email, Member member, AdditionalData? additional = null); public Task SendChangedEmailAsync(Member member); } public class MailService: IMailService { private readonly ILogger _logger; private readonly DefaultDbContext _db; private readonly IEmailVerifyTokenRepository _emailVerifyTokenRepository; private readonly IEmailVerifyNumberRepository _emailVerifyNumberRepository; private readonly string? _smtpServer; private readonly int _smtpPort; private readonly bool _smtpEnableSSL; private readonly string? _smtpUsername; private readonly string? _smtpPassword; private readonly string _fromEmail; private readonly string? _fromName; public MailService(ILogger logger, DefaultDbContext db, IEmailVerifyTokenRepository emailVerifyTokenRepository, IEmailVerifyNumberRepository emailVerifyNumberRepository) { _logger = logger; _db = db; _emailVerifyTokenRepository = emailVerifyTokenRepository; _emailVerifyNumberRepository = emailVerifyNumberRepository; // 1차로 Config 값 SMTP 조회 _smtpServer = Config.Basic.SmtpServer; _smtpPort = Config.Basic.SmtpPort ?? 0; _smtpEnableSSL = Config.Basic.SmtpEnableSSL == 'Y'; _smtpUsername = Config.Basic.SmtpUsername; _smtpPassword = Config.Basic.SmtpPassword; // 보낸 사람 표시 값 _fromEmail = Config.Basic.FromEmail ?? string.Empty; _fromName = Config.Basic.FromName; // 2차로 appsettings.json 값 SMTP 조회 if (string.IsNullOrEmpty(_smtpServer)) { _smtpServer = Setting.Smtp.Server; } if (_smtpPort <= 0) { _smtpPort = Setting.Smtp.Port; } if (string.IsNullOrEmpty(Config.Basic.SmtpEnableSSL.ToString().Trim())) { _smtpEnableSSL = Setting.Smtp.EnableSSL; } if (string.IsNullOrEmpty(_smtpUsername)) { _smtpUsername = Setting.Smtp.Username; } if (string.IsNullOrEmpty(_smtpPassword)) { _smtpPassword = Setting.Smtp.Password; } if (string.IsNullOrEmpty(_fromEmail)) { _fromEmail = Setting.Smtp.FromEmail; } if (string.IsNullOrEmpty(_fromName)) { _fromName = Setting.Smtp.FromName; } _logger.LogInformation("MailService 초기화 완료! SMTP 설정 완료."); } // 이메일 양식을 사용하여 이메일 전송 public async Task SendTemplatedEmailAsync(string toAddress, TemplateKey templateKey, Placeholders placeholder, Member? member = null) { Config.Map.TryGetValue(templateKey.Subject, out var subject); Config.Map.TryGetValue(templateKey.Content, out var content); subject = subject ?? string.Empty; content = content ?? string.Empty; if (string.IsNullOrEmpty(content)) { string error = $"Template 양식 `{templateKey}`를 찾을 수 없습니다."; _logger.LogError(error); throw new Exception(error); } string message = content; foreach (var p in placeholder.ToDictionary()) { message = message.Replace($"{{{{{p.Key}}}}}", p.Value); } var sendData = new SendData(toAddress, subject, message, member); // 이메일 발송 기록 await SetEmailLogAsync(sendData); } // 이메일 발송 public async Task SendEmailAsync(SendData sendData) { try { var client = new SmtpClient(_smtpServer, _smtpPort) { Credentials = new NetworkCredential(_smtpUsername, _smtpPassword), EnableSsl = _smtpEnableSSL }; var mailMessage = new MailMessage { From = new MailAddress(_fromEmail, _fromName), Subject = sendData.Subject, Body = sendData.Message, IsBodyHtml = true }; mailMessage.To.Add(new MailAddress(sendData.ToAddress, sendData.Member?.Name)); _logger.LogInformation($"Sending email to {sendData.ToAddress} with subject {sendData.Subject}."); await client.SendMailAsync(mailMessage); _logger.LogInformation($"Email sent successfully to {sendData.ToAddress}."); } catch (SmtpException e) { Console.WriteLine($"SMTP error: {e.Message}"); _logger.LogError($"Failed to smtp error: {e.StatusCode} - {e.Message}"); throw; } catch (Exception e) { Console.WriteLine($"Email sending failed: {e.Message}"); _logger.LogError($"Failed to send email to {sendData.ToAddress}: {e.Message}"); throw; } } // 이메일 발송 정보 기록 public async Task SetEmailLogAsync(SendData sendData) { _db.EmailLog.Add(new EmailLog { MemberID = sendData.Member?.ID, Status = MailStatus.Pending, Subject = sendData.Subject, Message = sendData.Message, ToAddress = sendData.ToAddress, ToName = sendData.Member?.Name, FromAddress = _fromEmail, FromName = _fromName }); var affectedRows = await _db.SaveChangesAsync(); if (affectedRows > 0) { Console.WriteLine("이메일 발송 기록함"); } else { Console.WriteLine("이메일 발송 기록 실패"); } } // 인증주소 유효성 확인 public async Task VerifyTokenAsync(string token, VerificationType type) { return await _emailVerifyTokenRepository.ValidateTokenAsync(token, type); } // 인증번호 유효성 확인 public async Task VerifyCodeAsync(string email, string code, VerificationType type) { return await _emailVerifyNumberRepository.ValidateCodeAsync(email, code, type); } /** * 회원가입 인증 메일 전송 */ public async Task SendRegisterEmailAsync(Member member) { var verifyCode = await _emailVerifyNumberRepository.GenerateCodeAsync(member.Email, VerificationType.Registration); var placeholder = Placeholders.RegisterEmailForm( email: member.Email, code: verifyCode ); await SendTemplatedEmailAsync(member.Email, Template.RegisterEmailForm, placeholder, member); } /** * 회원가입 완료 메일 전송 */ public async Task SendRegistrationEmailAsync(Member member) { var placeholder = Placeholders.RegistrationEmailForm( email: member.Email, createdAt: DateTime.UtcNow.ToString("yyyy.MM.dd HH:mm:ss") ); await SendTemplatedEmailAsync(member.Email, Template.RegistrationEmailForm, placeholder, member); } /** * 비밀번호 인증 메일 전송 */ public async Task SendForgotPasswordEmailAsync(Member member) { var verifyCode = await _emailVerifyNumberRepository.GenerateCodeAsync(member.Email, VerificationType.ForgotPassword); var placeholder = Placeholders.ForgotPasswordEmailForm( email: member.Email, code: verifyCode ); await SendTemplatedEmailAsync(member.Email, Template.ForgotPasswordEmailForm, placeholder, member); } /** * 비밀번호 재설정 완료 메일 전송 */ public async Task SendChangedPasswordEmailAsync(Member member) { var placeholder = Placeholders.ChangedPasswordEmailForm( email: member.Email, createdAt: DateTime.UtcNow.ToString("yyyy.MM.dd HH:mm:ss") ); await SendTemplatedEmailAsync(member.Email, Template.ChangedPasswordEmailForm, placeholder, member); } /** * 회원탈퇴 시 메일 전송 */ public async Task SendWithdrawEmailAsync(Member member) { var placeholder = Placeholders.RegistrationEmailForm( email: member.Email, createdAt: DateTime.UtcNow.ToString("yyyy.MM.dd HH:mm:ss") ); await SendTemplatedEmailAsync(member.Email, Template.WithdrawEmailForm, placeholder, member); } /** * 이메일 변경 인증 메일 전송 */ public async Task SendEmailVerifyAsync(string email, Member member, AdditionalData? additional = null) { var verifyCode = HttpUtility.UrlEncode(await _emailVerifyTokenRepository.GenerateTokenAsync(email, VerificationType.ChangedEmail, additional)); var verifyLink = $"{Setting.AppConfig.BaseURL}/api/auth/verify-email/{verifyCode}"; var placeholder = Placeholders.EmailVerifyForm( email: member.Email, link: verifyLink ); await SendTemplatedEmailAsync(email, Template.EmailVerifyForm, placeholder, member); } /** * 이메일 변경 완료 메일 전송 */ public async Task SendChangedEmailAsync(Member member) { var placeholder = Placeholders.RegistrationEmailForm( email: member.Email, createdAt: DateTime.UtcNow.ToString("yyyy.MM.dd HH:mm:ss") ); await SendTemplatedEmailAsync(member.Email, Template.ChangedEmailForm, placeholder, member); } } }