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);
}