using Microsoft.Extensions.Options; using SendGrid; using SendGrid.Helpers.Mail; namespace OTSSignsOrchestrator.Server.Services; public class EmailOptions { public const string Section = "Email"; public string SendGridApiKey { get; set; } = string.Empty; public string SenderEmail { get; set; } = "noreply@otssigns.com"; public string SenderName { get; set; } = "OTS Signs"; } /// /// Email service backed by SendGrid. All methods catch exceptions, log as Error, /// and return bool success — they never throw. /// public class EmailService { private readonly ILogger _logger; private readonly EmailOptions _options; private readonly SendGridClient? _client; public EmailService(ILogger logger, IOptions options) { _logger = logger; _options = options.Value; if (!string.IsNullOrWhiteSpace(_options.SendGridApiKey)) { _client = new SendGridClient(_options.SendGridApiKey); } else { _logger.LogWarning("SendGrid API key not configured — emails will be logged only"); } } public async Task SendWelcomeEmailAsync( string toEmail, string firstName, string instanceUrl, string invitationLink) { var subject = "Welcome to OTS Signs — Your CMS is Ready!"; var html = $"""

Welcome to OTS Signs, {HtmlEncode(firstName)}!

Your Xibo CMS instance has been provisioned and is ready to use.

Instance URL: {HtmlEncode(instanceUrl)}

Accept your invitation to get started:

Accept Invitation

If you have any questions, please contact our support team.

— The OTS Signs Team

"""; return await SendEmailAsync(toEmail, subject, html); } public async Task SendPaymentFailedEmailAsync( string toEmail, string companyName, int failedCount) { var daysSinceFirst = failedCount * 3; // approximate string subject; string urgency; if (daysSinceFirst >= 21) { subject = $"FINAL NOTICE — Payment Required for {companyName}"; urgency = "

⚠️ FINAL NOTICE: Your service will be suspended if payment is not received immediately.

"; } else if (daysSinceFirst >= 7) { subject = $"URGENT — Payment Failed for {companyName}"; urgency = "

⚠️ URGENT: Please update your payment method to avoid service interruption.

"; } else { subject = $"Payment Failed for {companyName}"; urgency = "

Please update your payment method at your earliest convenience.

"; } var html = $"""

Payment Failed — {HtmlEncode(companyName)}

{urgency}

We were unable to process your payment (attempt #{failedCount}).

Please update your payment method to keep your OTS Signs service active.

— The OTS Signs Team

"""; return await SendEmailAsync(toEmail, subject, html); } public async Task SendTrialEndingEmailAsync( string toEmail, string firstName, DateTime trialEndDate) { var daysLeft = (int)Math.Ceiling((trialEndDate - DateTime.UtcNow).TotalDays); var subject = $"Your OTS Signs Trial Ends in {Math.Max(0, daysLeft)} Day{(daysLeft != 1 ? "s" : "")}"; var html = $"""

Hi {HtmlEncode(firstName)},

Your OTS Signs trial ends on {trialEndDate:MMMM dd, yyyy}.

To continue using your CMS without interruption, please subscribe before your trial expires.

— The OTS Signs Team

"""; return await SendEmailAsync(toEmail, subject, html); } public async Task SendReportEmailAsync( string toEmail, string attachmentName, byte[] attachment, string mimeType) { var subject = $"OTS Signs Report — {attachmentName}"; var html = $"""

OTS Signs Report

Please find the attached report: {HtmlEncode(attachmentName)}

This report was generated automatically by the OTS Signs Orchestrator.

— The OTS Signs Team

"""; return await SendEmailAsync(toEmail, subject, html, attachmentName, attachment, mimeType); } private async Task SendEmailAsync( string toEmail, string subject, string htmlContent, string? attachmentName = null, byte[]? attachmentData = null, string? attachmentMimeType = null) { try { if (_client is null) { _logger.LogWarning( "SendGrid not configured — would send email to {Email}: {Subject}", toEmail, subject); return true; // Not a failure — just not configured } var from = new EmailAddress(_options.SenderEmail, _options.SenderName); var to = new EmailAddress(toEmail); var msg = MailHelper.CreateSingleEmail(from, to, subject, null, htmlContent); if (attachmentData is not null && attachmentName is not null) { msg.AddAttachment( attachmentName, Convert.ToBase64String(attachmentData), attachmentMimeType ?? "application/octet-stream"); } var response = await _client.SendEmailAsync(msg); if (response.IsSuccessStatusCode) { _logger.LogInformation("Email sent to {Email}: {Subject}", toEmail, subject); return true; } var body = await response.Body.ReadAsStringAsync(); _logger.LogError( "SendGrid returned {StatusCode} for email to {Email}: {Body}", response.StatusCode, toEmail, body); return false; } catch (Exception ex) { _logger.LogError(ex, "Failed to send email to {Email}: {Subject}", toEmail, subject); return false; } } private static string HtmlEncode(string value) => System.Net.WebUtility.HtmlEncode(value); }