Email нотифікації
Email нотифікації
Push-нотифікації та SSE відмінно працюють коли браузер онлайн і дав дозвіл на сповіщення. Але що з користувачами, які відхилили push-дозвіл? Або нотифікація занадто важлива, щоб покластися лише на браузер?
Email — перевірений часом надійний канал. Він не залежить від того, чи відкритий браузер, чи увімкнені push-сповіщення. Email зберігається у поштовій скриньці і чекає, доки користувач його прочитає.
Email нотифікації доречні для:
- Підтвердження важливих дій (реєстрація, зміна пароля, замовлення)
- Нагадувань після тривалої відсутності
- Дайджестів (щотижневий підсумок активності)
- Критичних сповіщень безпеки (вхід з нового пристрою)
NotificationsDemo — додамо MailKit для SMTP-відправки, HTML-шаблони листів, абстракцію IEmailSender і Channel<T>-чергу для асинхронної відправки у фоні.Архітектура Email-системи
HTTP-запит або BackgroundService
|
| Enqueue(EmailTask)
↓
Channel<EmailTask> ← thead-safe черга
|
| DequeueAsync()
↓
EmailSenderService ← BackgroundService
|
| IEmailSender.SendAsync()
↓
SmtpEmailSender
|
| SMTP (TLS)
↓
Mailtrap / Gmail / SendGrid
Ключовий принцип: HTTP-обробник лише ставить завдання в чергу і одразу відповідає. Реальна відправка відбувається у фоновому сервісі — повільність SMTP не сповільнює HTTP.
Крок 1: Встановлення MailKit
dotnet add package MailKit
MailKit — сучасна, повнофunkціональна бібліотека для роботи з Email у .NET. Підтримує SMTP, IMAP, POP3 і є рекомендованою заміною застарілого SmtpClient зі стандартної бібліотеки.
Крок 2: Конфігурація
{
"Email": {
"SmtpHost": "smtp.mailtrap.io",
"SmtpPort": 587,
"Username": "your_mailtrap_username",
"Password": "your_mailtrap_password",
"FromAddress": "noreply@notificationsdemo.com",
"FromName": "NotificationsDemo"
}
}
Для розробки використовуємо Mailtrap — безкоштовний сервіс, який перехоплює всі листи і відображає їх у веб-інтерфейсі. Реальні отримувачі листів не отримують.
Крок 3: Абстракція IEmailSender
Визначаємо інтерфейс до реалізації. Це дозволяє легко підмінити SMTP на SendGrid, Mailgun або мок у тестах:
namespace NotificationsDemo.Services;
// Контракт — незалежний від конкретного поштового провайдера
public interface IEmailSender
{
Task SendAsync(EmailMessage message, CancellationToken cancellationToken = default);
}
// Структура Email-повідомлення
public record EmailMessage(
string To, // Адреса отримувача
string ToName, // Ім'я отримувача (для персоналізації)
string Subject, // Тема листа
string HtmlBody, // HTML-контент (допускає форматування)
string? PlainText = null // Текстовий варіант (для email-клієнтів без HTML)
);
Крок 4: SMTP-реалізація через MailKit
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
using NotificationsDemo.Services;
namespace NotificationsDemo.Services;
public class SmtpEmailSender : IEmailSender
{
private readonly IConfiguration _config;
private readonly ILogger<SmtpEmailSender> _logger;
public SmtpEmailSender(IConfiguration config, ILogger<SmtpEmailSender> logger)
{
_config = config;
_logger = logger;
}
public async Task SendAsync(EmailMessage message, CancellationToken cancellationToken = default)
{
// MimeMessage — MailKit-представлення Email-повідомлення
var email = new MimeMessage();
email.From.Add(new MailboxAddress(
_config["Email:FromName"], // "NotificationsDemo"
_config["Email:FromAddress"] // "noreply@notificationsdemo.com"
));
email.To.Add(new MailboxAddress(message.ToName, message.To));
email.Subject = message.Subject;
// BodyBuilder допомагає будувати multipart повідомлення (HTML + текст)
var builder = new BodyBuilder
{
HtmlBody = message.HtmlBody,
TextBody = message.PlainText ?? StripHtml(message.HtmlBody) // Текст як fallback
};
email.Body = builder.ToMessageBody();
// SmtpClient з MailKit (не плутати з System.Net.Mail.SmtpClient!)
using var client = new SmtpClient();
// Connect: SecureSocketOptions.StartTls відповідає STARTTLS (порт 587)
await client.ConnectAsync(
_config["Email:SmtpHost"],
int.Parse(_config["Email:SmtpPort"]!),
SecureSocketOptions.StartTls,
cancellationToken);
// Автентифікація на SMTP-сервері
await client.AuthenticateAsync(
_config["Email:Username"],
_config["Email:Password"],
cancellationToken);
await client.SendAsync(email, cancellationToken);
await client.DisconnectAsync(quit: true, cancellationToken);
_logger.LogInformation("Email надіслано до {To}: {Subject}", message.To, message.Subject);
}
// Простий стриппер HTML для текстового fallback
private static string StripHtml(string html)
=> System.Text.RegularExpressions.Regex.Replace(html, "<[^>]*>", "").Trim();
}
Крок 5: HTML-шаблон листа
Замість жорстко закодованого HTML виносимо шаблони в окремий сервіс:
namespace NotificationsDemo.Services;
public class EmailTemplateService
{
// Шаблон нотифікаційного листа
public string BuildNotificationEmail(string userName, string notificationType, string message, string? actionUrl)
{
var actionButton = actionUrl is not null
? $"<a href='{actionUrl}' style='background:#3b82f6;color:white;padding:10px 20px;border-radius:6px;text-decoration:none;display:inline-block;margin-top:16px;'>Переглянути</a>"
: string.Empty;
// String interpolation для простих шаблонів
// У реальних проєктах — Razor template (AddRazorPages + IViewRenderService)
return $"""
<!DOCTYPE html>
<html lang="uk">
<head><meta charset="UTF-8"><title>Нове сповіщення</title></head>
<body style="font-family:sans-serif;max-width:600px;margin:0 auto;padding:20px;background:#f8fafc;">
<div style="background:white;border-radius:12px;padding:32px;box-shadow:0 1px 3px rgba(0,0,0,0.1);">
<h1 style="color:#0f172a;font-size:24px;margin-bottom:8px;">🔔 Нове сповіщення</h1>
<p style="color:#475569;">Привіт, <strong>{userName}</strong>!</p>
<div style="background:#f1f5f9;border-left:4px solid #3b82f6;padding:16px;border-radius:0 8px 8px 0;margin:16px 0;">
<span style="font-size:12px;color:#64748b;text-transform:uppercase;">{notificationType}</span>
<p style="color:#0f172a;margin:4px 0 0;">{message}</p>
</div>
{actionButton}
<hr style="border:none;border-top:1px solid #e2e8f0;margin:24px 0;">
<p style="color:#94a3b8;font-size:12px;">
Це автоматичне сповіщення від NotificationsDemo.<br>
Якщо ви не хочете отримувати такі листи — змініть налаштування у профілі.
</p>
</div>
</body>
</html>
""";
}
// Шаблон дайджесту — кілька нотифікацій в одному листі
public string BuildDigestEmail(string userName, IEnumerable<(string Type, string Message)> notifications)
{
var items = string.Join("", notifications.Select(n =>
$"<li style='margin-bottom:8px;'><strong>{n.Type}</strong>: {n.Message}</li>"));
return $"""
<!DOCTYPE html><html lang="uk"><head><meta charset="UTF-8"></head>
<body style="font-family:sans-serif;max-width:600px;margin:0 auto;padding:20px;">
<h1>📬 Ваш дайджест</h1>
<p>Привіт, <strong>{userName}</strong>! Ось що сталося поки вас не було:</p>
<ul style="line-height:1.8;">{items}</ul>
</body></html>
""";
}
}
Крок 6: Черга та фоновий сервіс відправки
Повторно використовуємо IBackgroundTaskQueue зі статті 06. Лише ставимо Email-задачу в чергу:
// Ендпоінт, що одночасно зберігає нотифікацію і ставить Email у чергу
private static async Task<IResult> CreateNotificationWithEmail(
AppDbContext db,
NotificationBroadcaster broadcaster,
IBackgroundTaskQueue taskQueue,
CreateNotificationRequest request)
{
var notification = new Notification
{
UserId = request.UserId,
Type = request.Type,
Message = request.Message,
ActionUrl = request.ActionUrl,
IsRead = false,
CreatedAt = DateTime.UtcNow
};
db.Notifications.Add(notification);
await db.SaveChangesAsync();
var response = new NotificationResponse(
notification.Id, notification.Type, notification.Message,
notification.ActionUrl, notification.IsRead, notification.CreatedAt);
// Real-time: надсилаємо через SSE активним клієнтам
broadcaster.Broadcast(notification.UserId, response);
// Async Email: ставимо в чергу — не блокуємо HTTP-відповідь
taskQueue.Enqueue(async (services, ct) =>
{
var emailSender = services.GetRequiredService<IEmailSender>();
var templates = services.GetRequiredService<EmailTemplateService>();
// У реальному проєкті — отримуємо email із БД або User сервісу
var userEmail = $"user{request.UserId}@example.com"; // Демо
var htmlBody = templates.BuildNotificationEmail(
userName: $"Користувач {request.UserId}",
notificationType: request.Type,
message: request.Message,
actionUrl: request.ActionUrl);
await emailSender.SendAsync(new EmailMessage(
To: userEmail,
ToName: $"Користувач {request.UserId}",
Subject: $"🔔 Нове сповіщення: {request.Type}",
HtmlBody: htmlBody
), ct);
});
return Results.Created($"/notifications/{notification.Id}", response);
}
Крок 7: Реєстрація
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();
builder.Services.AddSingleton<EmailTemplateService>();
AddTransient для SmtpEmailSender — кожен раз отримуємо новий екземпляр, що гарантує чисте SMTP-з'єднання.
Захист від спаму (концептуально)
Якщо ваш Email-сервер надсилає багато листів, потрібно налаштувати DNS-записи:
| Запис | Призначення | Приклад |
|---|---|---|
| SPF | Дозволені IP для надсилання | v=spf1 include:sendgrid.net ~all |
| DKIM | Цифровий підпис листів | Публічний ключ у TXT-записі |
| DMARC | Політика при невдалій перевірці | v=DMARC1; p=reject; |
Без цих записів ваші листи потраплятимуть у спам. Налаштування залежить від DNS-провайдера та поштового сервісу.
Підсумок
Email нотифікації замикають коло каналів доставки:
IEmailSender— абстракція, що дозволяє підмінити провайдер без зміни бізнес-логіки- MailKit — сучасна SMTP-бібліотека з підтримкою TLS та OAuth
- HTML-шаблони — персоналізовані листи з чітким дизайном
Channel<T>-черга — HTTP не блокується, Email відправляється у фоні- Mailtrap — тестуємо без ризику надіслати реальні листи
У фінальній статті ми зводимо всі дев'ять вивчених підходів у єдину порівняльну таблицю та decision tree для вибору правильного інструменту.
Web Push нотифікації
Вивчаємо Web Push API — стандарт для надсилання нотифікацій до браузера навіть коли сторінка закрита. Реалізуємо повний цикл: VAPID-ключі, Service Worker lifecycle, підписка, шифрування payload та відправка через фоновий сервіс.
Порівняння підходів: Як вибрати правильну технологію нотифікацій
Підсумкова стаття модуля. Повна порівняльна таблиця всіх технологій нотифікацій, decision tree для вибору та типові сценарії у реальних застосунках.