
Как отправить письмо из ASP.NET с помощью SmtpClient и MailMessage
Уведомления, подтверждения, отчёты: почта остаётся главным каналом связи веб-приложения с людьми. Разбираем, как собрать и отправить письмо из ASP.NET штатными классами.
Где живёт 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. Исключение не оборачивается в новое с потерей стека, а пробрасывается как есть.
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Настройки сервера в web.config
Хранить хост, порт и пароль прямо в коде неудобно: при переезде на другой сервер придётся пересобирать проект. У SmtpClient есть встроенная поддержка секции system.net/mailSettings. Если она заполнена, конструктор без параметров сам подхватит адрес сервера, порт, учётные данные и отправителя по умолчанию.
Параметр enableSsl включает шифрование STARTTLS, которое сегодня требуют практически все серверы на порту 587. Порт 25 часто закрыт провайдерами для исходящих соединений, а 465 (неявный SSL) классом SmtpClient не поддерживается вообще: попытка подключиться к нему просто зависнет до таймаута.
<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>Отладка без реального сервера и типичные ошибки
Для разработки удобно переключить 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.
Как это делают сегодня
Вопросы и ответы
Почему кириллица в теме письма приходит знаками вопроса?
Не задана кодировка темы. Установите SubjectEncoding и BodyEncoding в Encoding.UTF8, и почтовые клиенты корректно покажут русский текст.
Можно ли отправить письмо через порт 465?
Классический SmtpClient поддерживает только STARTTLS, обычно на порту 587. Для неявного SSL на 465 нужна сторонняя библиотека, например MailKit.
Как проверить письма, не отправляя их?
Укажите deliveryMethod="SpecifiedPickupDirectory" и папку pickupDirectoryLocation. Письма будут сохраняться в файлы .eml, их можно открыть в Outlook или Thunderbird.