Документация / ASP.NET

Отправка email в ASP.NET Core через SMTP

Встроенный System.Net.Mail.SmtpClient объявлен устаревшим для новой разработки, поэтому здесь используется MailKit — библиотека, на которую ссылается сама документация .NET. Ниже — сервис отправки поверх fmailer, HTML-письма, вложения и фоновая отправка.

Логин и пароль можно получить на странице управления токенами домена.

Параметры подключения

ПараметрЗначение
SMTP-хостsmtp.fmailer.ru
Порт587
ШифрованиеSTARTTLS (соединение переводится в TLS до авторизации)
ЛогинЛогин SMTP-токена вашего домена
ПарольПароль этого токена — показывается один раз, при создании
Каждый режим доступен и на высоком порту: 8465 (SSL/TLS), 8587 (STARTTLS) и 8025 (без шифрования). Многие хостинги и облака блокируют исходящие 25, 465 и 587 — если соединение отваливается по таймауту, используйте соответствующий высокий порт

Установка

bash
dotnet add package MailKit

Настройка appsettings.json

appsettings.jsonjson
{
  "EmailSettings": {
    "Host": "smtp.fmailer.ru",
    "Port": 587,
    "UseStartTls": true,
    "SenderEmail": "noreply@mail.example.ru",
    "SenderName": "Магазин"
  }
}

Сам токен держите вне файла, который уезжает вместе со сборкой: в разработке — user secrets, в продакшене — переменные окружения:

bash
dotnet user-secrets set "EmailSettings:Username" "<логин токена>"
dotnet user-secrets set "EmailSettings:Password" "<пароль токена>"

Сервис отправки

EmailService.cscsharp
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 — частое «решение» из интернета, которое принимает любой сертификат, в том числе поддельный. Обычно настоящая причина в отсутствии корневых сертификатов в образе контейнера.

Регистрация в Program.cs

Program.cscsharp
builder.Services.Configure<EmailSettings>(
    builder.Configuration.GetSection("EmailSettings"));

builder.Services.AddTransient<EmailService>();

Отправка из контроллера

csharp
[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);
}

Вложения

csharp
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 внутри действия контроллера привязывает время ответа к почтовому серверу. Передайте письмо фоновому сервису:

csharp
// Кладём письмо в канал, чтобы 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.
SslHandshakeExceptionSslOnConnect на 587 или StartTls на 465.
The remote certificate is invalid Нет корневых сертификатов в контейнере — поставьте ca-certificates.
Случайные сбои под нагрузкой Общий экземпляр SmtpClient: он не потокобезопасен.
SmtpCommandException (550)Домен отправителя не подтверждён.

Частые вопросы

Можно ли использовать SmtpClient из System.Net.Mail?

Не стоит. Microsoft помечает System.Net.Mail.SmtpClient как устаревший для новой разработки: он плохо работает с современным согласованием TLS и не имеет нормальной асинхронной модели. Рекомендованная замена — MailKit, на который ссылается и сама документация .NET.

Какое значение SecureSocketOptions выбрать?

SecureSocketOptions.StartTls для порта 587 и SecureSocketOptions.SslOnConnect для порта 465. Вариант Auto тоже работает, но выполняет согласование при каждом подключении — явное значение позволяет неправильной настройке падать сразу, а не молча понижать защиту.

Можно ли вернуть true из ServerCertificateValidationCallback?

Нет. Так принимается любой сертификат, включая подставленный при перехвате трафика, и заодно маскируется настоящая причина — обычно устаревший набор корневых сертификатов в образе контейнера. Правильнее починить хранилище доверенных сертификатов.

Почему ConnectAsync зависает или падает по таймауту на проде?

Большинство облачных провайдеров блокируют исходящие порты 25, 465 и 587. Поменяйте порт на 8587 для STARTTLS или 8465 для шифрования с первого байта. Заданный SmtpClient.Timeout заставит ошибку проявиться быстро, а не держать поток запроса.

Можно ли зарегистрировать SmtpClient из MailKit как singleton?

Небезопасно: SmtpClient в MailKit не потокобезопасен, и общий экземпляр ломает параллельные отправки. Создавайте клиент на каждую отправку, как в примере выше, либо стройте пул, выдающий клиент по одному.

Как отправлять письма в фоне в ASP.NET Core?

Положите сообщение в Channel и разбирайте его в BackgroundService, либо используйте устойчивую очередь вроде Hangfire, если отправка должна пережить перезапуск. Ожидание SMTP внутри действия контроллера привязывает время ответа к задержкам почтового сервера.

Что дальше