Network Programming

SMTP та протоколи електронної пошти

Детальне вивчення SMTP-протоколу — сесія, команди, аутентифікація, MIME, STARTTLS/SMTPS; відправлення листів через System.Net.Mail у .NET 10; огляд суміжних протоколів IMAP та POP3.

SMTP та протоколи електронної пошти

Екосистема протоколів електронної пошти

Електронна пошта — одна з найстаріших і найважливіших служб Інтернету. На відміну від HTTP, де один протокол обслуговує і запит, і відповідь, поштова інфраструктура розподілена між кількома спеціалізованими протоколами, кожен з яких вирішує окреме завдання в ланцюжку доставлення повідомлення від відправника до одержувача.

Ключова ідея: Відправлення і отримання пошти — це принципово різні операції, що обслуговуються різними протоколами. SMTP відповідає виключно за відправлення і ретрансляцію. POP3 та IMAP — за отримання клієнтом листів із поштового сервера.

Перш ніж заглибитися у деталі SMTP, розглянемо повну картину поштової інфраструктури:

ПротоколRFCПорт (plaintext / TLS)Призначення
SMTPRFC 532125 / 465 (SMTPS), 587 (Submission)Відправлення та ретрансляція між серверами
POP3RFC 1939110 / 995Завантаження листів із сервера на клієнт (з видаленням)
IMAP4RFC 3501143 / 993Синхронізація листів між сервером та клієнтами
MIMERFC 2045–2049Стандарт форматування вмісту листа (текст, HTML, вкладення)
Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

actor "Відправник\n(Alice)" as alice
participant "MUA\n(Outlook/Thunderbird)" as mua_a #e3f2fd
participant "MTA відправника\nsmtp.alice.com" as mta_a #e8f5e9
participant "MTA одержувача\nsmtp.bob.com" as mta_b #e8f5e9
participant "MDA\nmailbox store" as mda #fff3e0
participant "MUA\n(Outlook/Thunderbird)" as mua_b #e3f2fd
actor "Одержувач\n(Bob)" as bob

alice -> mua_a : Написав листа
mua_a -> mta_a : SMTP (port 587)\nSubmission + AUTH
mta_a -> mta_b : SMTP (port 25)\nServer-to-server relay
mta_b -> mda : Доставка до mailbox
mda --> mua_b : IMAP (port 993)\nабо POP3 (port 995)
mua_b --> bob : Читає листа

note over mta_a, mta_b
  Між MTA може бути
  декілька relay-серверів.
  Кожен додає заголовок
  "Received:" до листа.
end note

@enduml

Ролі агентів у доставці пошти

У поштовій екосистемі кожен компонент має власну назву та функцію:

MUA (Mail User Agent)
Клієнт
Поштовий клієнт користувача: Outlook, Thunderbird, Apple Mail, веб-інтерфейс Gmail. Саме тут користувач пише, надсилає та читає листи. Спілкується із сервером через SMTP (відправлення) і IMAP/POP3 (отримання).
MTA (Mail Transfer Agent)
Сервер ретрансляції
Поштовий сервер, що приймає листи від MUA або іншого MTA і пересилає їх далі. Виконує DNS MX-lookup для визначення наступного сервера в ланцюжку. Приклади: Postfix, Exim, Microsoft Exchange, SendGrid.
MDA (Mail Delivery Agent)
Агент доставки
Компонент, що фінально зберігає лист у поштовій скриньці одержувача. Іноді інтегрований у MTA. Форматування сховища: Maildir (окремі файли), mbox (один файл). Dovecot — типовий MDA для Linux-систем.
MX-запис (Mail eXchanger)
DNS
Запис у DNS, що вказує, який сервер відповідає за прийом пошти для даного домену. При відправленні MTA виконує DNS MX bob.com → отримує smtp.bob.com → підключається по SMTP (port 25).

Огляд IMAP та POP3 (суміжні протоколи)

Оскільки ці протоколи виходять за межі основної теми розділу, подамо їх описово, для повноти картини.

POP3 — Post Office Protocol version 3 (RFC 1939)

POP3 — найпростіший протокол отримання пошти, що дотримується моделі «завантажити і видалити». Клієнт підключається до сервера, завантажує всі нові листи у локальне сховище і, як правило, видаляє їх із сервера. Протокол є текстовим і надзвичайно простим: клієнт надсилає команди (USER, PASS, LIST, RETR, DELE, QUIT), сервер відповідає рядком +OK або -ERR.

Головна вада POP3 — відсутність синхронізації між пристроями. Якщо лист завантажено на ноутбук, він зникає з телефону. Через це POP3 майже витіснений IMAP у сучасних системах, проте досі використовується там, де потрібен офлайн-доступ без серверного сховища.

IMAP4 — Internet Message Access Protocol version 4 (RFC 3501)

IMAP є значно потужнішою альтернативою POP3. Ключова відмінність: листи залишаються на сервері, а клієнт синхронізує їхній стан (прочитаний/непрочитаний, переміщений, видалений). Це забезпечує однакове відображення поштової скриньки на всіх пристроях.

IMAP підтримує папки (mailbox), часткове завантаження (лише заголовки без тіла листа), серверний пошук, підписку на папки та IDLE-режим (push-нотифікації без постійного polling). Саме тому IMAP є стандартом у корпоративних системах та сучасних поштових клієнтах.

У .NET нативна підтримка IMAP та POP3 відсутня у System.Net.Mail. Для роботи з цими протоколами використовують бібліотеку MailKit — повноцінну реалізацію IMAP4, POP3 та SMTP із підтримкою OAuth 2.0, MIME та TLS. У межах цього розділу зосередимося на нативному SMTP.

SMTP — Simple Mail Transfer Protocol

SMTP (RFC 5321) — протокол прикладного рівня для відправлення та ретрансляції електронних повідомлень. Попри назву «Simple», протокол розвинувся у складну специфікацію з підтримкою аутентифікації, шифрування та розширень (ESMTP).

Коротка історія

  • 1982 — RFC 821: перша стандартизація SMTP. Команди HELO, MAIL FROM, RCPT TO, DATA, QUIT.
  • 1995 — ESMTP (Extended SMTP, RFC 1869): команда EHLO замість HELO, розширення через 250-KEYWORD.
  • 1998 — RFC 2554: розширення AUTH для аутентифікації (PLAIN, LOGIN, CRAM-MD5).
  • 2002 — RFC 3207: STARTTLS — механізм переходу до шифрованого з'єднання в рамках звичайного TCP-з'єднання.
  • 2008 — RFC 5321: сучасна специфікація SMTP, що об'єднала попередні RFC.
  • 2011 — RFC 6409: SMTP Submission (port 587) відокремлено від relay (port 25) — клієнти мають надсилати через 587.

Порти SMTP та їх призначення

ПортНазваШифруванняПризначення
25SMTP RelayPlaintext або STARTTLSServer-to-server (MTA↔MTA). ISP блокують для клієнтів
465SMTPS (Legacy)TLS від початку (Implicit TLS)Застаріла схема; деякі провайдери досі підтримують
587SubmissionSTARTTLS (обов'язково)Клієнт→Сервер. Стандарт для MUA з AUTH
Порт 465 офіційно «звільнений» для SMTPS і знову стандартизований у RFC 8314 (2018) як Implicit TLS. Однак порт 587 з STARTTLS залишається найпоширенішим стандартом для submission. Google, Microsoft, SendGrid — всі підтримують обидва варіанти.

SMTP-сесія: покроковий протокол

SMTP є текстовим протоколом на базі TCP. Кожна команда — це рядок ASCII, що завершується \r\n. Сервер відповідає тризначним кодом із необов'язковим текстовим поясненням. Розглянемо повну SMTP-сесію:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

participant "SMTP Client\n(MUA / MTA)" as client #e3f2fd
participant "SMTP Server\nsmtp.example.com:587" as server #e8f5e9

== Встановлення з'єднання ==
client -> server : TCP SYN (port 587)
server --> client : 220 smtp.example.com ESMTP Postfix (Ubuntu)

== ESMTP Greeting ==
client -> server : EHLO mail.myapp.com
server --> client : 250-smtp.example.com Hello\n250-SIZE 52428800\n250-STARTTLS\n250-AUTH PLAIN LOGIN\n250 ENHANCEDSTATUSCODES

== Ініціалізація TLS ==
client -> server : STARTTLS
server --> client : 220 2.0.0 Ready to start TLS
note over client, server: TLS Handshake (TLS 1.3)
client -> server : EHLO mail.myapp.com (знову, після TLS!)
server --> client : 250-smtp.example.com Hello\n250-AUTH PLAIN LOGIN\n250 SIZE 52428800

== Аутентифікація ==
client -> server : AUTH PLAIN AHVzZXJAZXhhbXBsZS5jb20AcGFzc3dvcmQ=
server --> client : 235 2.7.0 Authentication successful

== Конверт (Envelope) ==
client -> server : MAIL FROM:<alice@myapp.com> SIZE=1024
server --> client : 250 2.1.0 Ok

client -> server : RCPT TO:<bob@example.com>
server --> client : 250 2.1.5 Ok

client -> server : RCPT TO:<carol@example.com>
server --> client : 250 2.1.5 Ok

== Дані листа (DATA) ==
client -> server : DATA
server --> client : 354 End data with <CR><LF>.<CR><LF>

client -> server : From: Alice <alice@myapp.com>\r\n\
To: Bob <bob@example.com>\r\n\
Subject: Test Message\r\n\
Date: Fri, 23 May 2025 21:00:00 +0300\r\n\
MIME-Version: 1.0\r\n\
Content-Type: text/plain; charset=utf-8\r\n\
\r\n\
Тіло листа.\r\n\
.
server --> client : 250 2.0.0 Ok: queued as A3B4C5D6

== Завершення сесії ==
client -> server : QUIT
server --> client : 221 2.0.0 Bye

@enduml

Основні команди SMTP

EHLO / HELO
Greeting (обов'язково першою)
Представлення клієнта серверу. EHLO (Extended Hello) — сучасна версія для ESMTP; сервер відповідає списком підтримуваних розширень. HELO — застаріла версія для базового SMTP без розширень. Завжди надсилайте EHLO, і знову після STARTTLS — бо розширення можуть відрізнятись після встановлення TLS.Формат: EHLO <FQDN або IP клієнта>
MAIL FROM
Envelope Sender
Визначає адресу конверта відправника (envelope sender / return path). Це не те саме, що заголовок From: у листі! Якщо лист відхилено або збій доставки — bounce-повідомлення надсилається на цю адресу. Можна передати SIZE=<bytes> для попередньої перевірки сервером.Формат: MAIL FROM:<alice@example.com> SIZE=4096
RCPT TO
Envelope Recipients
Визначає адресу(и) одержувача(ів). Команду можна повторити для кількох одержувачів — кожен RCPT TO обробляється незалежно. Сервер може прийняти одних одержувачів і відхилити інших — необхідно перевіряти код відповіді для кожного.Формат: RCPT TO:<bob@example.com> → Відповідь: 250 Ok або 550 No such user
DATA
Початок передачі тіла листа
Переводить сервер у режим прийому даних. Сервер відповідає 354 Start input. Клієнт надсилає заголовки та тіло листа (формат MIME). Ознака завершення — рядок, що містить лише одну крапку: \r\n.\r\n (CRLF dot CRLF). Якщо в тілі листа є рядок, що починається з ., він екранується подвоєнням: ...
STARTTLS
TLS Upgrade (розширення RFC 3207)
Ініціює перехід від plaintext-з'єднання до TLS-шифрованого. Після успішного TLS Handshake необхідно повторно надіслати EHLO — бо сервер може оголосити інший набір розширень (особливо AUTH, який не пропонується до TLS). Доступне лише якщо сервер оголосив 250-STARTTLS у відповіді на EHLO.
AUTH
Аутентифікація (розширення RFC 4954)
Аутентифікація клієнта. Механізми: PLAIN (base64 від \0user\0password), LOGIN (окремо логін і пароль в base64), CRAM-MD5 (challenge-response, складніше). Сучасні сервери підтримують також OAuth 2.0 Bearer через OAUTHBEARER або XOAUTH2. Завжди виконуйте AUTH після STARTTLS — інакше credentials передаються у відкритому вигляді.
QUIT
Завершення сесії
Коректне завершення TCP-з'єднання. Сервер підтверджує 221 Bye і закриває з'єднання. Якщо клієнт закриває з'єднання без QUIT — сервер вважає сесію аварійно перерваною і може не прийняти лист у чергу.

Коди відповідей SMTP

Перша цифра визначає клас відповіді, друга — категорію, третя — специфіку:

КодЗначенняПриклад ситуації
2xxУспіх250 Ok, 221 Bye, 235 Auth successful
354ПроміжнийЧекаю дані листа (відповідь на DATA)
4xxТимчасова помилка421 Service unavailable — спробуйте пізніше
5xxПостійна помилка550 User unknown, 552 Message too large
220ГотовийПривітання сервера після TCP-підключення
250Команда прийнятаСтандартна відповідь на EHLO, MAIL FROM, RCPT TO
354Починайте введенняВідповідь на DATA — сервер чекає тіло листа
421Сервіс недоступнийПеревантаження; MTA повторить спробу
550Ящик не існуєПостійна помилка — не повторювати
552Ліміт розміруЛист перевищує SIZE сервера

MIME — Multipurpose Internet Mail Extensions

MIME (RFC 2045–2049) — стандарт форматування вмісту листа. Базовий SMTP передає лише 7-bit ASCII текст. MIME розширив цю можливість: UTF-8 текст, HTML, вкладені файли, зображення, аудіо — все це стало доступним завдяки MIME-кодуванню.

Анатомія MIME-листа

Кожен лист складається з заголовків (Headers) та тіла (Body), розділених порожнім рядком (\r\n\r\n):

From: Alice <alice@example.com>
To: Bob <bob@example.com>
Subject: =?UTF-8?B?0KLQtdGB0YLQvtCy0LjQuSDQu9C40YHRgg==?=
Date: Fri, 23 May 2025 21:00:00 +0300
Message-ID: <20250523180000.A1B2C3@myapp.com>
MIME-Version: 1.0
Content-Type: multipart/alternative; boundary="boundary_abc123"

--boundary_abc123
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable

=D0=A2=D0=B5=D1=81=D1=82=D0=BE=D0=B2=D0=B8=D0=B9 =D0=BB=D0=B8=D1=81=D1=82

--boundary_abc123
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: base64

PCFET0NUWVBFIGh0bWw+PGh0bWw+PGJvZHk+PGgxPlRlc3QgTWVzc2FnZTwvaDE+PC9ib2R5PjwvaHRtbD4=

--boundary_abc123--

Ключові заголовки листа

From
string (RFC 5322)
Адреса відправника у заголовку листа. Відрізняється від Envelope Sender (MAIL FROM:). Саме цей заголовок відображається клієнту. Формат: Name <email@domain.com> або просто email@domain.com. Може бути підроблений у звичайного SMTP-клієнта — SPF/DKIM/DMARC перевіряють саме цю відповідність.
To / Cc / Bcc
string (RFC 5322)
Одержувачі в заголовку листа. To — основні, Cc (Carbon Copy) — копія, Bcc (Blind Carbon Copy) — прихована копія. Bcc не потрапляє до заголовка листа — одержувачі Bcc не бачать один одного. Проте всі вони перераховуються у RCPT TO: команди SMTP-конверта.
Subject
encoded-word (RFC 2047)
Тема листа. Якщо містить не-ASCII символи (кирилиця, японська тощо) — кодується у форматі =?charset?encoding?encoded_text?=. Кодування: B = Base64, Q = Quoted-Printable. Приклад: =?UTF-8?B?0KLQtdGB0YLQvtCy...?=. SmtpClient у .NET кодує Subject автоматично.
Message-ID
string (RFC 5322)
Унікальний ідентифікатор листа. Формат: <local-part@domain>. Генерується MUA або MTA. Використовується для з'єднання листів у ланцюжки (threading) через In-Reply-To: та References: заголовки.
Content-Type
MIME type; parameters
Визначає тип вмісту. Для простого тексту: text/plain; charset=utf-8. Для HTML: text/html; charset=utf-8. Для мультичастинних листів: multipart/alternative; boundary="..." або multipart/mixed; boundary="...". Параметр boundary — унікальний рядок-роздільник між частинами.
Content-Transfer-Encoding
7bit | quoted-printable | base64
Кодування передачі вмісту. 7bit — лише ASCII (застаріло для unicode). quoted-printable — ASCII-сумісний, не-ASCII символи замінюються =HH (hex). base64 — повне Base64-кодування, збільшує розмір на ~33% але гарантує безпечну передачу бінарних даних.

Типи MIME multipart

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

rectangle "Лист із вкладенням та HTML" #f5f5f5 {
    rectangle "multipart/mixed\nboundary=outer" #e3f2fd {
        rectangle "multipart/alternative\nboundary=inner" #e8f5e9 {
            rectangle "text/plain\nutf-8" as plain #fff9c4
            rectangle "text/html\nutf-8" as html #fff9c4
            plain -[hidden]right-> html
        }
        rectangle "application/pdf\nfilename=report.pdf\nContent-Transfer-Encoding: base64" as attach #fce4ec
    }
}

note right of plain
  Клієнт обирає
  кращий варіант
  (зазвичай HTML)
end note

note right of attach
  Файл прикріплений
  до листа
end note

@enduml
MIME TypeПризначення
multipart/mixedЛист із вкладеннями. Частини є незалежними — текст + файли
multipart/alternativeОдин вміст у кількох форматах. Клієнт обирає найкращий (зазвичай HTML)
multipart/relatedHTML із вбудованими ресурсами (inline зображення через cid:)
text/plainЗвичайний текст
text/htmlHTML-вміст
application/octet-streamДовільний бінарний файл (вкладення)
image/png, image/jpegЗображення (для inline або вкладення)
Правильна ієрархія для листа з HTML і вкладеннями: multipart/mixed (зовнішній) → multipart/alternative (всередині) → text/plain + text/html. Зображення вбудовані в HTML (cid:) розміщуються у multipart/related, що вкладається всередину multipart/alternative.

System.Net.Mail — нативна відправка SMTP у .NET 10

Простір імен System.Net.Mail надає вбудовані засоби для відправки електронної пошти без залежностей від сторонніх бібліотек. Основні класи:

SmtpClient
System.Net.Mail
Клієнт SMTP-протоколу. Керує підключенням до сервера, аутентифікацією, шифруванням і надсиланням. Підтримує STARTTLS (EnableSsl = true). Реалізує IDisposable. Важлива примітка: Microsoft позначила SmtpClient як [Obsolete] у документації з рекомендацією використовувати MailKit для нових проектів, однак клас залишається повністю функціональним у .NET 10 для базових сценаріїв.
MailMessage
System.Net.Mail
Представляє один електронний лист. Містить відправника, одержувачів, тему, тіло, вкладення та альтернативні подання. Реалізує IDisposable — звільняє ресурси вкладень (потоки файлів).
MailAddress
System.Net.Mail
Представляє поштову адресу із необов'язковим ім'ям. new MailAddress("alice@example.com", "Alice")Alice <alice@example.com>. Виконує базову валідацію формату адреси.
Attachment
System.Net.Mail
Представляє вкладення листа. Може бути створений із файлу (new Attachment("path/to/file.pdf")) або потоку (new Attachment(stream, "report.pdf", "application/pdf")). Реалізує IDisposable.
AlternateView
System.Net.Mail
Альтернативне подання тіла листа. Використовується для multipart/alternative (текст + HTML) та multipart/related (HTML із inline-зображеннями через LinkedResource).

Базове надсилання листа

Розпочнемо із найпростішого сценарію — надсилання текстового листа через SMTP із аутентифікацією та STARTTLS:

using System.Net;
using System.Net.Mail;

// SmtpClient налаштовується один раз і може надсилати кілька листів
using var smtpClient = new SmtpClient("smtp.gmail.com")
{
    Port = 587,                        // Submission port (STARTTLS)
    EnableSsl = true,                  // STARTTLS: спочатку з'єднання, потім TLS upgrade
    DeliveryMethod = SmtpDeliveryMethod.Network,
    UseDefaultCredentials = false,
    Credentials = new NetworkCredential(
        userName: "alice@gmail.com",
        password: "app-specific-password" // Gmail: App Password (не основний пароль!)
    )
};

// MailMessage реалізує IDisposable — звільняє потоки вкладень
using var message = new MailMessage
{
    From = new MailAddress("alice@gmail.com", "Alice"),
    Subject = "Тестовий лист із .NET 10",
    Body = """
        Привіт, Боб!
        
        Це тестовий лист, надісланий через System.Net.Mail у .NET 10.
        
        З повагою,
        Alice
        """,
    IsBodyHtml = false,          // false = text/plain, true = text/html
    BodyEncoding = System.Text.Encoding.UTF8,
    SubjectEncoding = System.Text.Encoding.UTF8
};

message.To.Add(new MailAddress("bob@example.com", "Bob"));
message.Cc.Add("carol@example.com");   // Копія (видима в заголовках)

// Синхронне надсилання (блокуюче) — для простих сценаріїв
smtpClient.Send(message);
Console.WriteLine("Лист надіслано успішно.");

HTML-листи та вкладення

Більшість реальних листів використовують HTML для форматування та можуть містити файлові вкладення. MailMessage підтримує обидва сценарії через IsBodyHtml, Attachments та AlternateViews.

using System.Net;
using System.Net.Mail;
using System.Text;

using var smtp = new SmtpClient("smtp.gmail.com")
{
    Port = 587,
    EnableSsl = true,
    Credentials = new NetworkCredential("alice@gmail.com", "app-password")
};

// HTML-тіло листа — SmtpClient встановить Content-Type: text/html; charset=utf-8
string htmlBody = """
    <!DOCTYPE html>
    <html lang="uk">
    <head><meta charset="UTF-8"></head>
    <body style="font-family: Arial, sans-serif; color: #333;">
        <h2 style="color: #1a73e8;">Привіт, Боб!</h2>
        <p>Це <strong>HTML-лист</strong>, надісланий через <em>.NET 10</em>.</p>
        <table border="1" cellpadding="8" style="border-collapse: collapse;">
            <tr><th>Продукт</th><th>Кількість</th><th>Ціна</th></tr>
            <tr><td>Товар A</td><td>5</td><td>250 грн</td></tr>
            <tr><td>Товар B</td><td>2</td><td>180 грн</td></tr>
        </table>
        <p style="color: #888; font-size: 12px;">Цей лист згенеровано автоматично.</p>
    </body>
    </html>
    """;

using var message = new MailMessage
{
    From = new MailAddress("alice@gmail.com", "Alice"),
    Subject = "Замовлення #12345",
    Body = htmlBody,
    IsBodyHtml = true,    // Content-Type: text/html
    BodyEncoding = Encoding.UTF8
};

message.To.Add("bob@example.com");
await smtp.SendMailAsync(message);
При використанні AlternateViews не встановлюйте message.Body і message.IsBodyHtml — вони конфліктують із AlternateViews. Або використовуйте Body/IsBodyHtml (для простих HTML без inline-зображень), або AlternateViews (для multipart/alternative і multipart/related).

Конфігурація SmtpClient через appsettings

У реальних застосунках параметри SMTP зберігаються у конфігурації, а не хардкодяться у коді:

{
  "SmtpSettings": {
    "Host": "smtp.gmail.com",
    "Port": 587,
    "EnableSsl": true,
    "Username": "alice@gmail.com",
    "Password": "app-password",
    "FromName": "Alice",
    "FromEmail": "alice@gmail.com"
  }
}
using Microsoft.Extensions.Configuration;
using System.Net;
using System.Net.Mail;

// Клас налаштувань (зв'язується з секцією SmtpSettings)
public record SmtpSettings(
    string Host,
    int Port,
    bool EnableSsl,
    string Username,
    string Password,
    string FromName,
    string FromEmail
);

// Реєстрація у DI-контейнері (Program.cs або Startup.cs)
builder.Services.Configure<SmtpSettings>(
    builder.Configuration.GetSection("SmtpSettings")
);
builder.Services.AddTransient<IEmailSender, SmtpEmailSender>();

// Реалізація сервісу відправки
public class SmtpEmailSender(IOptions<SmtpSettings> options) : IEmailSender
{
    private readonly SmtpSettings _settings = options.Value;

    public async Task SendAsync(string to, string subject, string htmlBody,
        CancellationToken ct = default)
    {
        using var smtp = new SmtpClient(_settings.Host)
        {
            Port = _settings.Port,
            EnableSsl = _settings.EnableSsl,
            Credentials = new NetworkCredential(_settings.Username, _settings.Password)
        };

        using var message = new MailMessage
        {
            From = new MailAddress(_settings.FromEmail, _settings.FromName),
            Subject = subject,
            Body = htmlBody,
            IsBodyHtml = true,
            BodyEncoding = System.Text.Encoding.UTF8
        };

        message.To.Add(to);
        await smtp.SendMailAsync(message, ct);
    }
}

Обробка помилок SmtpClient

Детальна обробка помилок є критично важливою у поштових системах — частина помилок є тимчасовими (слід повторити), частина постійними (слід зафіксувати й не повторювати):

using System.Net;
using System.Net.Mail;
using System.Net.Sockets;

async Task SendWithRetryAsync(MailMessage message, int maxRetries = 3)
{
    using var smtp = new SmtpClient("smtp.gmail.com")
    {
        Port = 587,
        EnableSsl = true,
        Credentials = new NetworkCredential("alice@gmail.com", "app-password"),
        Timeout = 30_000 // Таймаут 30 секунд на операцію
    };

    for (int attempt = 1; attempt <= maxRetries; attempt++)
    {
        try
        {
            await smtp.SendMailAsync(message);
            Console.WriteLine($"Лист надіслано (спроба {attempt})");
            return; // Успіх — виходимо з циклу
        }
        catch (SmtpException ex)
        {
            int code = (int)ex.StatusCode;

            // 5xx — постійна помилка: не повторювати
            if (code >= 500)
            {
                Console.Error.WriteLine($"Постійна SMTP-помилка {code}: {ex.Message}");
                // 550 = Mailbox unavailable, 552 = Too large, 554 = Transaction failed
                throw;
            }

            // 4xx — тимчасова помилка: повторити з паузою
            if (code >= 400 && attempt < maxRetries)
            {
                // Exponential backoff: 2s, 4s, 8s...
                var delay = TimeSpan.FromSeconds(Math.Pow(2, attempt));
                Console.Warning($"Тимчасова помилка {code}. Повтор через {delay.TotalSeconds}с...");
                await Task.Delay(delay);
                continue;
            }

            throw; // Вичерпали спроби або невідомий код
        }
        catch (SmtpFailedRecipientException ex)
        {
            // Один або кілька одержувачів відхилені
            // ex.FailedRecipient — адреса, що спричинила помилку
            Console.Error.WriteLine($"Одержувача відхилено: {ex.FailedRecipient}");
            Console.Error.WriteLine($"Код: {ex.StatusCode}, Деталі: {ex.Message}");
            throw;
        }
        catch (SmtpFailedRecipientsException ex)
        {
            // Всі одержувачі відхилені (при масовому відправленні)
            foreach (SmtpFailedRecipientException inner in ex.InnerExceptions)
                Console.Error.WriteLine($"  Відхилено: {inner.FailedRecipient} ({inner.StatusCode})");
            throw;
        }
        catch (SocketException ex)
        {
            // Мережева помилка: сервер недоступний, DNS-помилка
            Console.Error.WriteLine($"Помилка мережі: {ex.Message}");
            if (attempt < maxRetries)
            {
                await Task.Delay(TimeSpan.FromSeconds(5));
                continue;
            }
            throw;
        }
    }
}

Локальне тестування: SpecifiedPickupDirectory

При розробці та тестуванні небажано надсилати реальні листи. SmtpDeliveryMethod.SpecifiedPickupDirectory зберігає листи як .eml файли у вказану директорію — без жодного мережевого з'єднання:

using System.Net.Mail;

// Зберігає листи як .eml файли — ідеально для unit-тестів та розробки
using var smtp = new SmtpClient
{
    DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory,
    PickupDirectoryLocation = "/tmp/email-outbox",  // Директорія для .eml файлів
    EnableSsl = false                               // TLS не потрібен для файлового збереження
};

using var message = new MailMessage
{
    From = new MailAddress("test@myapp.com", "Test App"),
    Subject = "Тестовий лист",
    Body = "<h1>Тест</h1>",
    IsBodyHtml = true
};

message.To.Add("user@example.com");

smtp.Send(message);
// Лист збережено у /tmp/email-outbox/xxxxxxxx.eml
// Відкрийте у Outlook або Thunderbird для перегляду
Для локального тестування поштових функцій також чудово підходить MailHog — SMTP-сервер із веб-інтерфейсом, що перехоплює всі листи і відображає їх у браузері. Запуск: docker run -p 1025:1025 -p 8025:8025 mailhog/mailhog. Налаштовуйте SmtpClient на Host = "localhost", Port = 1025.

Безпека поштових систем: SPF, DKIM, DMARC

Сучасна поштова безпека базується на трьох стандартах, що разом забезпечують автентифікацію відправника та захист від підробки:

Loading diagram...
@startuml
skinparam style plain
skinparam backgroundColor #ffffff

participant "Відправник\nalice@myapp.com" as sender #e3f2fd
participant "MTA відправника\nsmtp.myapp.com" as mta_s #e8f5e9
participant "DNS\nmyapp.com" as dns #fff3e0
participant "MTA одержувача\nsmtp.gmail.com" as mta_r #e8f5e9

sender -> mta_s : Надсилає лист
mta_s -> mta_s : Підписує листа\nDKIM-підписом

mta_r -> dns : DNS TXT?\nmyapp.com SPF
dns --> mta_r : v=spf1 include:_spf.myapp.com ~all
mta_r -> mta_r : SPF: smtp.myapp.com\nдозволений? ✅

mta_r -> dns : DNS TXT?\nmail._domainkey.myapp.com
dns --> mta_r : v=DKIM1; k=rsa; p=MIGfMA...
mta_r -> mta_r : DKIM: перевіряємо\nпідпис заголовків ✅

mta_r -> dns : DNS TXT?\n_dmarc.myapp.com
dns --> mta_r : v=DMARC1; p=reject;\nrua=mailto:dmarc@myapp.com
mta_r -> mta_r : DMARC: SPF✅ + DKIM✅\nFrom:=SPF domain? ✅\nДоставити!

mta_s --> mta_r : Лист доставлено

@enduml
Без SPF/DKIM/DMARC ваші листи будуть потрапляти до спаму або відхилятися більшістю сучасних поштових провайдерів. Gmail та Microsoft відхиляють листи від доменів без SPF із серпня 2024 року.

Налаштування популярних SMTP-провайдерів

// Gmail SMTP — ВАЖЛИВО: потрібен App Password, не основний пароль!
// Google вимкнув "Less secure apps" з травня 2022 року.
// Налаштування: Google Account → Security → 2-Step Verification → App Passwords
// Або використовуйте OAuth 2.0 Bearer через XOAUTH2 (MailKit)

using var smtp = new SmtpClient("smtp.gmail.com")
{
    Port = 587,           // Або 465 для Implicit TLS (SSL від початку)
    EnableSsl = true,     // STARTTLS для port 587
    Credentials = new NetworkCredential(
        "yourname@gmail.com",
        "xxxx xxxx xxxx xxxx"   // App Password (16 символів із пробілами)
    )
};

Обмеження Gmail SMTP:

  • 500 листів/день для звичайних акаунтів
  • 2000 листів/день для Google Workspace
  • Максимальний розмір листа: 25 MB
smtp.gmail.com
Google Gmail
Порт 587 (STARTTLS) або 465 (Implicit TLS). Вимагає App Password або OAuth 2.0. Ліміт: 500/день (звичайний), 2000/день (Workspace).
smtp.office365.com
Microsoft Office 365
Порт 587 (STARTTLS). Basic Auth вимкнено з 2022 — потрібен OAuth 2.0 для Exchange Online.
smtp.sendgrid.net
SendGrid (Twilio)
Порт 587 або 465. Username завжди apikey, password — SendGrid API Key. Безкоштовно: 100 листів/день.
email-smtp.eu-west-1.amazonaws.com
Amazon SES
Порт 587 або 465. Credentials: SMTP credentials із консолі SES (не IAM!). Від 0.10$ за 1000 листів.
smtp.mailersend.com
MailerSend
Порт 587 або 465. Безкоштовно: 3000 листів/місяць. Підтримує шаблони, аналітику, webhooks.

Підсумок

Електронна пошта — це складна екосистема взаємодіючих протоколів. У цьому розділі ми вивчили:

  • Архітектуру поштової інфраструктури: ролі MUA, MTA, MDA, DNS MX-записів
  • SMTP-протокол (RFC 5321): команди, відповіді, сесія, порти 25/465/587
  • MIME-стандарт (RFC 2045–2049): структуру листа, заголовки, типи multipart
  • System.Net.Mail: SmtpClient, MailMessage, Attachment, AlternateView, LinkedResource
  • Безпеку: SPF, DKIM, DMARC — три рівні захисту від підробки адреси відправника
  • Практику: налаштування Gmail, Office 365, SendGrid, локального тестування
Для продакшн-систем із великим обсягом листів або потребою OAuth 2.0 розгляньте бібліотеку MailKit — вона забезпечує повну підтримку IMAP4, POP3, SMTP, MIME, OAuth 2.0 та NTLM, і є рекомендованою Microsoft для нових .NET-проектів.
Copyright © 2026