Email Notifications¶
The Email notification channel (EmailNotificationChannel) provides reliable email delivery using the Curiosity.EMail infrastructure with queued processing, post-processing capabilities, and comprehensive error handling.
How it works¶
Architecture¶
The email notification system consists of:
- EmailNotificationChannel - Background service that processes email notifications from a queue
- EmailNotification - Represents an email to be sent with subject, body, recipient, and optional parameters
- EmailNotificationBuilderBase - Abstract base class for building email notifications from metadata
- IEMailNotificationPostProcessor - Interface for post-processing after email sending
Processing Flow¶
sequenceDiagram
participant Client
participant Notificator
participant Builder
participant Channel
participant Provider
participant PostProcessor
Client->>Notificator: NotifyAsync(metadata)
Notificator->>Builder: BuildNotificationsAsync(metadata)
Builder->>Notificator: EmailNotification[]
Notificator->>Channel: SendNotificationAsync(notification)
Channel->>Provider: SendAsync(email, subject, body)
Provider->>Channel: Response
Channel->>PostProcessor: ProcessAsync(notification, result)
Channel->>Notificator: Task completion
Notificator->>Client: Result
Queue Processing¶
- Each
EmailNotificationChannelprocesses notifications sequentially - Uses
BlockingCollection<NotificationQueueItem<EmailNotification>>for thread-safe queuing - Provides
TaskCompletionSourcefor async completion tracking - Handles cancellation and shutdown gracefully
Email Structure¶
public class EmailNotification : INotification
{
public string ChannelType => "curiosity.notifications.email";
public string Email { get; } // Recipient email address
public string Subject { get; } // Email subject
public string Body { get; } // Email body content
public bool IsBodyHtml { get; } // Whether body is HTML
public IEMailExtraParams? ExtraParams { get; } // Provider-specific parameters
}
Available providers¶
The email notification channel uses the IEMailSender interface from Curiosity.EMail package, which supports multiple providers:
Mailgun¶
services.AddCuriosityMailgunSender(options =>
{
options.ApiKey = "your-api-key";
options.Domain = "your-domain.com";
options.FromEmail = "noreply@your-domain.com";
});
SendGrid¶
services.AddCuriositySendGridSender(options =>
{
options.ApiKey = "your-api-key";
options.FromEmail = "noreply@your-domain.com";
options.FromName = "Your App Name";
});
SMTP¶
services.AddCuriositySmtpSender(options =>
{
options.Host = "smtp.your-provider.com";
options.Port = 587;
options.Username = "your-username";
options.Password = "your-password";
options.EnableSsl = true;
});
In-Memory (Testing)¶
How to add custom provider?¶
Step 1: Implement IEMailSender¶
public class CustomEmailSender : IEMailSender
{
public async Task<Response> SendAsync(
string email,
string subject,
string body,
bool isBodyHtml = false,
CancellationToken cancellationToken = default)
{
try
{
// Your custom email sending logic here
await SendEmailThroughCustomProvider(email, subject, body, isBodyHtml);
return Response.Successful();
}
catch (AuthenticationException ex)
{
return Response.Failed(new Error((int)EmailError.Auth, ex.Message));
}
catch (RateLimitException ex)
{
return Response.Failed(new Error((int)EmailError.RateLimit, ex.Message));
}
catch (Exception ex)
{
return Response.Failed(new Error((int)EmailError.Unknown, ex.Message));
}
}
public async Task<Response> SendAsync(
string email,
string subject,
string body,
bool isBodyHtml,
IEMailExtraParams extraParams,
CancellationToken cancellationToken = default)
{
// Handle extra parameters specific to your provider
var customParams = extraParams as CustomEmailExtraParams;
// Implementation with extra parameters
return await SendAsync(email, subject, body, isBodyHtml, cancellationToken);
}
}
Step 2: Create Custom Extra Parameters (Optional)¶
public class CustomEmailExtraParams : IEMailExtraParams
{
public string Priority { get; set; } = "normal";
public string Category { get; set; }
public Dictionary<string, string> Tags { get; set; } = new();
}
Step 3: Register in IoC¶
public static class IoCExtensions
{
public static IServiceCollection AddCustomEmailSender(
this IServiceCollection services,
Action<CustomEmailOptions> configure)
{
services.Configure(configure);
services.AddSingleton<IEMailSender, CustomEmailSender>();
return services;
}
}
Step 4: Configure and Use¶
// In Startup.cs or Program.cs
services.AddCustomEmailSender(options =>
{
options.ApiEndpoint = "https://api.customprovider.com/email";
options.ApiKey = "your-api-key";
});
services.AddCuriosityEMailChannel();
Step 5: Create Custom Builder (Optional)¶
public class CustomEmailNotificationBuilder : EmailNotificationBuilderBase<YourNotificationMetadata>
{
protected override async Task<IReadOnlyList<EmailNotification>> BuildNotificationsAsync(
YourNotificationMetadata metadata,
CancellationToken cancellationToken = default)
{
var extraParams = new CustomEmailExtraParams
{
Priority = "high",
Category = "user-notifications",
Tags = new Dictionary<string, string> { { "user-id", metadata.UserId } }
};
var notification = new EmailNotification(
metadata.RecipientEmail,
await GenerateSubject(metadata),
await GenerateBody(metadata),
isBodyHtml: true,
extraParams: extraParams
);
return new[] { notification };
}
}
Error Handling¶
The email channel maps provider errors to standardized notification error codes:
var notificationCode = errorCode switch
{
EmailError.Auth => NotificationErrorCode.Auth,
EmailError.Communication => NotificationErrorCode.Communication,
EmailError.RateLimit => NotificationErrorCode.RateLimit,
EmailError.NoMoney => NotificationErrorCode.NoMoney,
EmailError.IncorrectRequestData => NotificationErrorCode.IncorrectRequestData,
_ => NotificationErrorCode.Unknown,
};
Post-Processing¶
Implement IEMailNotificationPostProcessor for actions after email sending:
public class EmailAnalyticsProcessor : IEMailNotificationPostProcessor
{
public async Task ProcessAsync(
EmailNotification notification,
Response result,
CancellationToken cancellationToken = default)
{
// Log analytics, update database, send webhooks, etc.
await analyticsService.TrackEmailSent(new EmailAnalytics
{
Recipient = notification.Email,
Subject = notification.Subject,
Success = result.IsSuccess,
ErrorCode = result.Errors?.FirstOrDefault()?.Code,
SentAt = DateTime.UtcNow
});
}
}
// Register in IoC
services.AddEMailNotificationPostProcessor<EmailAnalyticsProcessor>();