Initial commit

This commit is contained in:
2026-04-23 21:38:24 -04:00
commit 63e12a9636
1762 changed files with 1672620 additions and 0 deletions
@@ -0,0 +1,160 @@
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<EmailService> _logger;
private readonly IHostEnvironment _hostEnvironment;
public EmailService(IConfiguration configuration, ILogger<EmailService> logger, IHostEnvironment hostEnvironment)
{
_configuration = configuration;
_logger = logger;
_hostEnvironment = hostEnvironment;
}
/// <summary>
/// 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.
/// </summary>
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);
}
}
/// <summary>
/// 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.
/// </summary>
public async Task<(bool Success, string? ErrorMessage)> SendEmailWithAttachmentsAsync(
string toEmail,
string toName,
string subject,
string plainTextBody,
string? htmlBody = null,
IList<EmailAttachment>? 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);
}
}
/// <summary>
/// Sends the built SendGrid message and interprets the HTTP response. Extracted so both
/// send methods share identical dispatch and logging logic.
/// </summary>
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}");
}
}