using Api.Services.Contracts; using Microsoft.Extensions.Options; using MailKit.Net.Smtp; using MailKit.Security; using MimeKit; using Models.Settings; using Models.Requests; namespace Api.Services { public sealed class SmtpEmailSender : IEmailSender { private readonly SmtpSettings _smtp; private readonly ContactSettings _contact; private readonly SubscribeSettings _subscribe; private readonly FileStorageSettings _fileStorage; private readonly ILogger _log; private readonly string _environmentName; public SmtpEmailSender(IOptions smtp, IOptions contact, IOptions subscribe, IOptions fileStorage, ILogger log) { _smtp = smtp.Value; _contact = contact.Value; _subscribe = subscribe.Value; _fileStorage = fileStorage.Value; _log = log; // Use APP_ENVIRONMENT_NAME from environment variable (set in docker-compose) with fallback to "Development" _environmentName = Environment.GetEnvironmentVariable("APP_ENVIRONMENT_NAME") ?? "Development"; } public async Task SendContactAsync(ContactRequest req, CancellationToken ct) { // Throw error if ToEmail is not configured, since contact requests are important to process. if (string.IsNullOrWhiteSpace(_contact.ToEmail)) { _log.LogDebug("Contact email skipped - ToEmail not configured"); throw new InvalidOperationException("Contact email recipient is not configured."); } _log.LogInformation("Preparing contact email from {SenderEmail} to {RecipientEmail}", req.Email, _contact.ToEmail); var msg = new MimeMessage(); msg.From.Add(MailboxAddress.Parse(_smtp.Username)); msg.To.Add(MailboxAddress.Parse(_contact.ToEmail)); msg.ReplyTo.Add(MailboxAddress.Parse(req.Email)); msg.Subject = $"{_contact.SubjectPrefix} [{_environmentName}] {req.Subject}".Trim(); var body = $@"New contact form submission: Name: {req.Name} Email: {req.Email} Subject: {req.Subject} Message: {req.Message} "; msg.Body = new TextPart("plain") { Text = body }; await SendEmailAsync(msg, "contact email", ct); _log.LogInformation("Contact email sent successfully from {SenderEmail}", req.Email); } public async Task SendSubscribeAsync(SubscribeRequest req, CancellationToken ct) { // Throw error if ToEmail is not configured, since subscription requests are important to process. if (string.IsNullOrWhiteSpace(_subscribe.ToEmail)) { _log.LogDebug("Subscription email skipped - ToEmail not configured"); throw new InvalidOperationException("Subscription email recipient is not configured."); } _log.LogInformation("Processing subscription request for {Email}", req.Email); var msg = new MimeMessage(); msg.From.Add(MailboxAddress.Parse(_smtp.Username)); msg.To.Add(MailboxAddress.Parse(_subscribe.ToEmail)); msg.ReplyTo.Add(MailboxAddress.Parse(req.Email)); msg.Subject = $"{_subscribe.SubjectPrefix} [{_environmentName}]".Trim(); var body = $@"New subscription request: Email: {req.Email} "; msg.Body = new TextPart("plain") { Text = body }; await SendEmailAsync(msg, "subscription email", ct); _log.LogInformation("Subscription email sent successfully for {Email}", req.Email); } public async Task SendFileDownloadNotificationAsync(string fileName, string? userIp, CancellationToken ct) { // Skip sending if ToEmail is not configured if (string.IsNullOrWhiteSpace(_fileStorage.ToEmail)) { _log.LogDebug("File download notification skipped - ToEmail not configured"); return; } _log.LogInformation("Preparing file download notification for {FileName}", fileName); var msg = new MimeMessage(); msg.From.Add(MailboxAddress.Parse(_smtp.Username)); msg.To.Add(MailboxAddress.Parse(_fileStorage.ToEmail)); msg.Subject = $"{_fileStorage.SubjectPrefix} [{_environmentName}] {fileName}".Trim(); var body = $@"File download notification: File: {fileName} Downloaded at: {DateTime.UtcNow:yyyy-MM-dd HH:mm:ss} UTC IP Address: {userIp ?? "Unknown"} "; msg.Body = new TextPart("plain") { Text = body }; await SendEmailAsync(msg, "file download notification email", ct); _log.LogInformation("File download notification sent successfully for {FileName}", fileName); } /// /// Connects to the SMTP server and authenticates if credentials are configured. /// private async Task ConnectAndAuthenticateAsync(SmtpClient client, CancellationToken ct) { // If you're in enterprise environments, you may need to tweak certificate validation. // Don't disable it casually. var tls = _smtp.UseStartTls ? SecureSocketOptions.StartTls : SecureSocketOptions.Auto; _log.LogDebug("Connecting to SMTP server {Host}:{Port} with security={Security}", _smtp.Host, _smtp.Port, tls); await client.ConnectAsync(_smtp.Host, _smtp.Port, tls, ct); if (!string.IsNullOrWhiteSpace(_smtp.Username)) { _log.LogDebug("Authenticating with SMTP server as {Username}", _smtp.Username); await client.AuthenticateAsync(_smtp.Username, _smtp.Password, ct); } } /// /// Sends an email message using SMTP. /// /// The email message to send. /// Description of the message type for logging purposes. /// Cancellation token. private async Task SendEmailAsync(MimeMessage message, string messageType, CancellationToken ct) { using var client = new SmtpClient(); await ConnectAndAuthenticateAsync(client, ct); _log.LogDebug("Sending {MessageType} message", messageType); await client.SendAsync(message, ct); await client.DisconnectAsync(true, ct); } public Task SendMatchAsync(string? explicitTo, string subject, string body, CancellationToken ct) { throw new NotImplementedException(); } } }