161 lines
6.3 KiB
C#
161 lines
6.3 KiB
C#
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}");
|
|
}
|
|
}
|