Встроенный System.Net.Mail.SmtpClient объявлен устаревшим для новой разработки, поэтому здесь используется MailKit — библиотека, на которую ссылается сама документация .NET. Ниже — сервис отправки поверх fmailer, HTML-письма, вложения и фоновая отправка.
| Параметр | Значение |
|---|---|
| SMTP-хост | smtp.fmailer.ru |
| Порт | 587 |
| Шифрование | STARTTLS (соединение переводится в TLS до авторизации) |
| Логин | Логин SMTP-токена вашего домена |
| Пароль | Пароль этого токена — показывается один раз, при создании |
dotnet add package MailKit{
"EmailSettings": {
"Host": "smtp.fmailer.ru",
"Port": 587,
"UseStartTls": true,
"SenderEmail": "noreply@mail.example.ru",
"SenderName": "Магазин"
}
}Сам токен держите вне файла, который уезжает вместе со сборкой: в разработке — user secrets, в продакшене — переменные окружения:
dotnet user-secrets set "EmailSettings:Username" "<логин токена>"
dotnet user-secrets set "EmailSettings:Password" "<пароль токена>"using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.Extensions.Options;
using MimeKit;
public class EmailService
{
private readonly EmailSettings _settings;
public EmailService(IOptions<EmailSettings> settings) => _settings = settings.Value;
public async Task SendAsync(
string to,
string subject,
string html,
string text,
CancellationToken ct = default)
{
var message = new MimeMessage();
message.From.Add(new MailboxAddress(_settings.SenderName, _settings.SenderEmail));
message.To.Add(MailboxAddress.Parse(to));
message.Subject = subject;
// Обе части, чтобы письмо было multipart/alternative, а не только HTML.
message.Body = new BodyBuilder { HtmlBody = html, TextBody = text }.ToMessageBody();
using var client = new SmtpClient { Timeout = 10_000 };
var option = _settings.UseStartTls
? SecureSocketOptions.StartTls // порт 587
: SecureSocketOptions.SslOnConnect; // порт 465
await client.ConnectAsync(_settings.Host, _settings.Port, option, ct);
await client.AuthenticateAsync(_settings.Username, _settings.Password, ct);
await client.SendAsync(message, ct);
await client.DisconnectAsync(true, ct);
}
}ServerCertificateValidationCallback на возврат true — частое «решение» из интернета, которое принимает любой сертификат, в том числе поддельный. Обычно настоящая причина в отсутствии корневых сертификатов в образе контейнера. builder.Services.Configure<EmailSettings>(
builder.Configuration.GetSection("EmailSettings"));
builder.Services.AddTransient<EmailService>();[HttpPost]
public async Task<IActionResult> Create(OrderRequest request, CancellationToken ct)
{
var order = await _orders.CreateAsync(request, ct);
await _email.SendAsync(
to: order.CustomerEmail,
subject: $"Подтверждение заказа №{order.Id}",
html: "<h1>Заказ принят</h1><p>Доставка — завтра до 18:00.</p>",
text: "Заказ принят. Доставка — завтра до 18:00.",
ct);
return Ok(order);
}var builder = new BodyBuilder
{
HtmlBody = "<p>Счёт за март во вложении.</p>",
TextBody = "Счёт за март во вложении.",
};
await builder.Attachments.AddAsync("/srv/invoices/2026-03.pdf", ct);
builder.Attachments.Add("invoice.pdf", pdfBytes, new ContentType("application", "pdf"));
// Картинка в теле письма — в HTML как <img src="cid:logo">
var logo = builder.LinkedResources.Add("logo.png", logoBytes);
logo.ContentId = "logo";
message.Body = builder.ToMessageBody();Ожидание SMTP внутри действия контроллера привязывает время ответа к почтовому серверу. Передайте письмо фоновому сервису:
// Кладём письмо в канал, чтобы HTTP-ответ не ждал SMTP.
public class EmailBackgroundService : BackgroundService
{
protected override async Task ExecuteAsync(CancellationToken ct)
{
await foreach (var job in _queue.ReadAllAsync(ct))
{
try { await _email.SendAsync(job.To, job.Subject, job.Html, job.Text, ct); }
catch (Exception ex) { _logger.LogError(ex, "Письмо на {To} не отправлено", job.To); }
}
}
}| Ошибка | Причина и решение |
|---|---|
AuthenticationException (535) | Неверные доступы токена или секрет не подгрузился в конфигурацию. |
SocketException, таймаут подключения | Порт заблокирован. Используйте 8587 или 8465. |
SslHandshakeException | SslOnConnect на 587 или StartTls на 465. |
The remote certificate is invalid | Нет корневых сертификатов в контейнере — поставьте ca-certificates. |
| Случайные сбои под нагрузкой | Общий экземпляр SmtpClient: он не потокобезопасен. |
SmtpCommandException (550) | Домен отправителя не подтверждён. |
Не стоит. Microsoft помечает System.Net.Mail.SmtpClient как устаревший для новой разработки: он плохо работает с современным согласованием TLS и не имеет нормальной асинхронной модели. Рекомендованная замена — MailKit, на который ссылается и сама документация .NET.
SecureSocketOptions.StartTls для порта 587 и SecureSocketOptions.SslOnConnect для порта 465. Вариант Auto тоже работает, но выполняет согласование при каждом подключении — явное значение позволяет неправильной настройке падать сразу, а не молча понижать защиту.
Нет. Так принимается любой сертификат, включая подставленный при перехвате трафика, и заодно маскируется настоящая причина — обычно устаревший набор корневых сертификатов в образе контейнера. Правильнее починить хранилище доверенных сертификатов.
Большинство облачных провайдеров блокируют исходящие порты 25, 465 и 587. Поменяйте порт на 8587 для STARTTLS или 8465 для шифрования с первого байта. Заданный SmtpClient.Timeout заставит ошибку проявиться быстро, а не держать поток запроса.
Небезопасно: SmtpClient в MailKit не потокобезопасен, и общий экземпляр ломает параллельные отправки. Создавайте клиент на каждую отправку, как в примере выше, либо стройте пул, выдающий клиент по одному.
Положите сообщение в Channel и разбирайте его в BackgroundService, либо используйте устойчивую очередь вроде Hangfire, если отправка должна пережить перезапуск. Ожидание SMTP внутри действия контроллера привязывает время ответа к задержкам почтового сервера.