Как отправить письмо из ASP.NET с помощью SmtpClient и MailMessage

РазделASP.NETУровеньНовичок Чтение8 минОпубликовано ТегиASP.NETVB.NETSMTPE-mail

Уведомления, подтверждения, отчёты: почта остаётся главным каналом связи веб-приложения с людьми. Разбираем, как собрать и отправить письмо из ASP.NET штатными классами.

Главное. MailMessage описывает письмо, SmtpClient его доставляет, а все параметры сервера лучше держать в web.config. Освобождайте объекты через Using и явно задавайте UTF-8, и большинство проблем с почтой исчезнет.

Где живёт SMTP-сервер и зачем ему пароль

В типичной инсталляции почтовый сервер стоит отдельно от веб-сервера: это может быть корпоративный Exchange, сервер хостинга или внешний сервис рассылок. Приложение подключается к нему по сети и просит переслать письмо дальше. Такая пересылка называется relay, и почти любой нормально настроенный сервер разрешает её только авторизованным клиентам.

Поэтому под приложение обычно заводят отдельный почтовый ящик с логином и паролем. Эти данные передаются серверу при каждом подключении. Без них вы получите ошибку вида «Mailbox unavailable» или «Relaying denied», хотя сам код формально правильный. Если письма не уходят, первым делом проверьте именно учётку и порт, а уже потом код.

В .NET за отправку отвечают два класса из пространства имён System.Net.Mail: MailMessage описывает само письмо (отправитель, получатели, тема, тело, вложения), а SmtpClient умеет подключиться к серверу и передать сообщение. Старый System.Web.Mail на базе CDO считается устаревшим с .NET 2.0, его в новом коде не используют.

Адреса: From, To, CC и BCC

Каждый адрес в письме представлен объектом MailAddress. Конструктор MailMessage принимает отправителя и первого получателя строками, а дополнительные адреса добавляются в коллекции To, CC и Bcc. Коллекции поддерживают сколько угодно элементов, так что рассылка на несколько адресов сводится к циклу.

Удобно принимать список копий одной строкой через запятую, как его обычно и вводят в форму. Строку режем методом Split, лишние пробелы убираем через Trim, пустые фрагменты пропускаем. MailAddress сам проверит формат и бросит FormatException, если адрес кривой: это стоит перехватить и показать пользователю внятное сообщение, а не трассировку стека.

  • To: основные получатели, видны всем.
  • CC: копия, адреса тоже видны всем получателям.
  • Bcc: скрытая копия, остальные получатели её не видят. Именно сюда ставят массовые рассылки, чтобы не раскрывать список адресов.
  • ReplyToList: куда пойдёт ответ, если он должен отличаться от From.

Вложения и формат тела

Вложение создаётся классом Attachment, которому достаточно передать путь к файлу. Тип содержимого он определит по расширению. Если файл генерируется на лету (например, PDF-отчёт), не обязательно сохранять его на диск: у Attachment есть конструктор, принимающий Stream и имя файла.

По умолчанию тело письма считается простым текстом. Для HTML-писем выставьте IsBodyHtml = True и обязательно задайте BodyEncoding = UTF8, иначе кириллица в части почтовых клиентов превратится в знаки вопроса. Тему тоже стоит кодировать явно через SubjectEncoding.

Важная деталь: Attachment держит файл открытым, пока вы не освободите MailMessage. Если после отправки код пытается удалить временный файл, он получит ошибку доступа. Решение простое: оборачивайте MailMessage в блок Using.

Рабочий пример на VB.NET

Ниже переработанная версия классической процедуры отправки. Параметры сервера берутся из конфигурации, адреса разбираются одной вспомогательной функцией, а объекты освобождаются через Using. Исключение не оборачивается в новое с потерей стека, а пробрасывается как есть.

MailSender.vbVB.NET
Imports System.Net
Imports System.Net.Mail
Imports System.Text

Public NotInheritable Class MailSender
    Public Shared Sub Send(fromAddr As String, toAddr As String, subject As String,
                           body As String, Optional cc As String = "",
                           Optional bcc As String = "", Optional attachPath As String = "")
        Using msg As New MailMessage(fromAddr, toAddr)
            AddAddresses(msg.CC, cc)
            AddAddresses(msg.Bcc, bcc)
            msg.Subject = subject
            msg.SubjectEncoding = Encoding.UTF8
            msg.Body = body
            msg.BodyEncoding = Encoding.UTF8
            msg.IsBodyHtml = True
            If Not String.IsNullOrEmpty(attachPath) Then
                msg.Attachments.Add(New Attachment(attachPath))
            End If
            Using client As New SmtpClient() ' хост и порт из web.config
                client.Send(msg)
            End Using
        End Using
    End Sub

    ' Разбирает строку "a@x.ru, b@y.ru" в коллекцию адресов
    Private Shared Sub AddAddresses(target As MailAddressCollection, list As String)
        If String.IsNullOrEmpty(list) Then Return
        For Each part In list.Split(","c, ";"c)
            Dim addr = part.Trim()
            If addr.Length > 0 Then target.Add(New MailAddress(addr))
        Next
    End Sub
End Class
Using для SmtpClient появился в .NET 4.0, когда класс стал реализовывать IDisposable. В проектах на .NET 2.0/3.5 создавайте клиент обычным Dim.

Настройки сервера в web.config

Хранить хост, порт и пароль прямо в коде неудобно: при переезде на другой сервер придётся пересобирать проект. У SmtpClient есть встроенная поддержка секции system.net/mailSettings. Если она заполнена, конструктор без параметров сам подхватит адрес сервера, порт, учётные данные и отправителя по умолчанию.

Параметр enableSsl включает шифрование STARTTLS, которое сегодня требуют практически все серверы на порту 587. Порт 25 часто закрыт провайдерами для исходящих соединений, а 465 (неявный SSL) классом SmtpClient не поддерживается вообще: попытка подключиться к нему просто зависнет до таймаута.

web.configXML
<configuration>
  <system.net>
    <mailSettings>
      <smtp from="noreply@example.ru" deliveryMethod="Network">
        <network host="smtp.example.ru" port="587"
                 userName="noreply@example.ru" password="***"
                 enableSsl="true" defaultCredentials="false" />
      </smtp>
    </mailSettings>
  </system.net>
</configuration>
Пароль в web.config шифруется командой aspnet_regiis -pef "system.net/mailSettings/smtp". Так он не попадёт в открытом виде в архивы и репозиторий.

Отладка без реального сервера и типичные ошибки

Для разработки удобно переключить deliveryMethod на SpecifiedPickupDirectory и указать папку. Тогда каждое письмо будет сохраняться в файл .eml, который открывается любым почтовым клиентом. Так можно проверить вёрстку и кодировку, не рискуя случайно отправить тестовую рассылку настоящим клиентам.

Синхронный Send блокирует поток запроса на всё время общения с сервером, а это может быть несколько секунд. Если письмо отправляется после клика пользователя, страница будет заметно «думать». Для массовых рассылок такой подход вообще не годится: письма лучше складывать в очередь (таблицу в базе) и отправлять отдельной службой.

  • SmtpException «The operation has timed out»: неверный порт или закрытый файрвол.
  • «5.7.1 Client was not authenticated»: не переданы учётные данные или defaultCredentials оставлен в true.
  • «The remote certificate is invalid»: самоподписанный сертификат на сервере, его нужно добавить в доверенные, а не отключать проверку.
  • Письма уходят, но падают в спам: у домена отправителя нет SPF и DKIM.

Как это делают сегодня

Сама Microsoft пометила System.Net.Mail.SmtpClient как не рекомендуемый для нового кода: он не поддерживает современные механизмы аутентификации вроде OAuth2. В .NET 8 стандартом стала библиотека MailKit с асинхронным API и полной поддержкой TLS. Для транзакционных писем всё чаще используют HTTP API сервисов доставки, а отправку выносят в фоновую очередь через IHostedService или Hangfire.

Вопросы и ответы

Почему кириллица в теме письма приходит знаками вопроса?

Не задана кодировка темы. Установите SubjectEncoding и BodyEncoding в Encoding.UTF8, и почтовые клиенты корректно покажут русский текст.

Можно ли отправить письмо через порт 465?

Классический SmtpClient поддерживает только STARTTLS, обычно на порту 587. Для неявного SSL на 465 нужна сторонняя библиотека, например MailKit.

Как проверить письма, не отправляя их?

Укажите deliveryMethod="SpecifiedPickupDirectory" и папку pickupDirectoryLocation. Письма будут сохраняться в файлы .eml, их можно открыть в Outlook или Thunderbird.

Читайте также