99 lines
3.6 KiB
C#
99 lines
3.6 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using ROLAC.API.Data;
|
|
using ROLAC.API.Entities.Notifications;
|
|
using ROLAC.API.Services.Logging;
|
|
|
|
namespace ROLAC.API.Services.Notifications;
|
|
|
|
/// <summary>
|
|
/// Resolves recipients (member emails + raw addresses, deduped), sends each via the SMTP
|
|
/// dispatcher, and writes a NotificationLog row per recipient. A single failure never aborts the
|
|
/// batch — it is recorded and reported in the summary.
|
|
/// </summary>
|
|
public sealed class EmailService : IEmailService
|
|
{
|
|
private readonly AppDbContext _db;
|
|
private readonly ISmtpDispatcher _dispatcher;
|
|
private readonly CurrentUserAccessor _currentUser;
|
|
|
|
public EmailService(AppDbContext db, ISmtpDispatcher dispatcher, CurrentUserAccessor currentUser)
|
|
{
|
|
_db = db;
|
|
_dispatcher = dispatcher;
|
|
_currentUser = currentUser;
|
|
}
|
|
|
|
public async Task<NotificationResult> SendAsync(EmailMessage message, CancellationToken ct = default)
|
|
{
|
|
var recipients = await ResolveRecipientsAsync(message, ct);
|
|
if (recipients.Count == 0) return NotificationResult.Empty;
|
|
|
|
var sentBy = message.SentByUserId ?? _currentUser.UserIdOrSystem;
|
|
var attachments = message.Attachments ?? Array.Empty<EmailAttachment>();
|
|
var failures = new List<NotificationFailure>();
|
|
var sentCount = 0;
|
|
|
|
foreach (var recipient in recipients)
|
|
{
|
|
var log = new NotificationLog
|
|
{
|
|
Channel = NotificationChannels.Email,
|
|
TargetType = NotificationTargetTypes.Email,
|
|
TargetExternalId = recipient.Address,
|
|
Subject = message.Subject,
|
|
MemberId = recipient.MemberId,
|
|
Body = NotificationLogText.Truncate(message.HtmlBody),
|
|
SentByUserId = sentBy,
|
|
SentAt = DateTime.UtcNow,
|
|
};
|
|
|
|
try
|
|
{
|
|
await _dispatcher.SendAsync(
|
|
new OutboundEmail(recipient.Address, message.Subject, message.HtmlBody, attachments), ct);
|
|
log.Status = NotificationStatuses.Sent;
|
|
sentCount++;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
log.Status = NotificationStatuses.Failed;
|
|
log.Error = ex.Message;
|
|
failures.Add(new NotificationFailure(recipient.Address, ex.Message));
|
|
}
|
|
|
|
_db.NotificationLogs.Add(log);
|
|
}
|
|
|
|
await _db.SaveChangesAsync(ct);
|
|
return new NotificationResult(sentCount, failures.Count, failures);
|
|
}
|
|
|
|
private async Task<IReadOnlyList<(string Address, int? MemberId)>> ResolveRecipientsAsync(
|
|
EmailMessage message, CancellationToken ct)
|
|
{
|
|
var resolved = new List<(string Address, int? MemberId)>();
|
|
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
|
|
|
if (message.MemberIds.Count > 0)
|
|
{
|
|
var members = await _db.Members
|
|
.Where(member => message.MemberIds.Contains(member.Id) && member.Email != null && member.Email != "")
|
|
.Select(member => new { member.Id, member.Email })
|
|
.ToListAsync(ct);
|
|
foreach (var member in members)
|
|
if (seen.Add(member.Email!))
|
|
resolved.Add((member.Email!, member.Id));
|
|
}
|
|
|
|
foreach (var address in message.Addresses)
|
|
{
|
|
var trimmed = address?.Trim();
|
|
if (!string.IsNullOrWhiteSpace(trimmed) && seen.Add(trimmed))
|
|
resolved.Add((trimmed, null));
|
|
}
|
|
|
|
return resolved;
|
|
}
|
|
|
|
}
|