using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using PowderCoating.Application.Interfaces; using SendGrid; using SendGrid.Helpers.Mail; namespace PowderCoating.Infrastructure.Services; public class EmailService : IEmailService { private readonly IConfiguration _configuration; private readonly ILogger _logger; private readonly IHostEnvironment _hostEnvironment; public EmailService(IConfiguration configuration, ILogger logger, IHostEnvironment hostEnvironment) { _configuration = configuration; _logger = logger; _hostEnvironment = hostEnvironment; } /// /// Sends an email via SendGrid. In non-production environments the subject is prefixed with /// "[ENV]" (e.g. "[Development]", "[Staging]") so recipients immediately know the email came /// from a test environment and don't act on it as if it were real. /// public async Task<(bool Success, string? ErrorMessage)> SendEmailAsync( string toEmail, string toName, string subject, string plainTextBody, string? htmlBody = null, byte[]? attachmentData = null, string? attachmentFilename = null, string? attachmentContentType = null, string? replyToEmail = null, string? replyToName = null) { var apiKey = _configuration["SendGrid:ApiKey"]; if (string.IsNullOrWhiteSpace(apiKey) || apiKey.StartsWith("your-") || apiKey == "SG.placeholder") { _logger.LogWarning("SendGrid API key is not configured. Email to {ToEmail} skipped.", toEmail); return (false, "SendGrid not configured"); } if (!_hostEnvironment.IsProduction()) subject = $"[{_hostEnvironment.EnvironmentName}] {subject}"; var fromEmail = _configuration["SendGrid:FromEmail"] ?? "noreply@example.com"; var fromName = _configuration["SendGrid:FromName"] ?? "Powder Coating"; try { var client = new SendGridClient(apiKey); var msg = MailHelper.CreateSingleEmail( new EmailAddress(fromEmail, fromName), new EmailAddress(toEmail, toName), subject, plainTextBody, htmlBody ?? plainTextBody); if (!string.IsNullOrWhiteSpace(replyToEmail)) msg.SetReplyTo(new EmailAddress(replyToEmail, replyToName)); if (attachmentData != null && attachmentData.Length > 0 && !string.IsNullOrWhiteSpace(attachmentFilename)) { await msg.AddAttachmentAsync( attachmentFilename, new MemoryStream(attachmentData), attachmentContentType ?? "application/octet-stream"); } return await DispatchAsync(client, msg, toEmail, subject); } catch (Exception ex) { _logger.LogError(ex, "Exception sending email to {ToEmail}", toEmail); return (false, ex.Message); } } /// /// Sends an email with one or more file attachments via SendGrid. Intended for scenarios /// such as attaching job photos to a ready-for-pickup notification. Callers are responsible /// for keeping total attachment size under SendGrid's 30 MB per-message limit; this method /// skips any attachment whose Data array is empty. /// public async Task<(bool Success, string? ErrorMessage)> SendEmailWithAttachmentsAsync( string toEmail, string toName, string subject, string plainTextBody, string? htmlBody = null, IList? attachments = null, string? replyToEmail = null, string? replyToName = null) { var apiKey = _configuration["SendGrid:ApiKey"]; if (string.IsNullOrWhiteSpace(apiKey) || apiKey.StartsWith("your-") || apiKey == "SG.placeholder") { _logger.LogWarning("SendGrid API key is not configured. Email to {ToEmail} skipped.", toEmail); return (false, "SendGrid not configured"); } if (!_hostEnvironment.IsProduction()) subject = $"[{_hostEnvironment.EnvironmentName}] {subject}"; var fromEmail = _configuration["SendGrid:FromEmail"] ?? "noreply@example.com"; var fromName = _configuration["SendGrid:FromName"] ?? "Powder Coating"; try { var client = new SendGridClient(apiKey); var msg = MailHelper.CreateSingleEmail( new EmailAddress(fromEmail, fromName), new EmailAddress(toEmail, toName), subject, plainTextBody, htmlBody ?? plainTextBody); if (!string.IsNullOrWhiteSpace(replyToEmail)) msg.SetReplyTo(new EmailAddress(replyToEmail, replyToName)); if (attachments != null) { foreach (var attachment in attachments.Where(a => a.Data.Length > 0)) { await msg.AddAttachmentAsync( attachment.Filename, new MemoryStream(attachment.Data), attachment.ContentType); } } return await DispatchAsync(client, msg, toEmail, subject); } catch (Exception ex) { _logger.LogError(ex, "Exception sending email with attachments to {ToEmail}", toEmail); return (false, ex.Message); } } /// /// Sends the built SendGrid message and interprets the HTTP response. Extracted so both /// send methods share identical dispatch and logging logic. /// private async Task<(bool Success, string? ErrorMessage)> DispatchAsync( SendGridClient client, SendGridMessage msg, string toEmail, string subject) { var response = await client.SendEmailAsync(msg); if ((int)response.StatusCode >= 200 && (int)response.StatusCode < 300) { _logger.LogInformation("Email sent to {ToEmail}: {Subject}", toEmail, subject); return (true, null); } var body = await response.Body.ReadAsStringAsync(); _logger.LogWarning("SendGrid returned {StatusCode} for {ToEmail}: {Body}", response.StatusCode, toEmail, body); return (false, $"HTTP {(int)response.StatusCode}"); } }