Wysyłanie wiadomości e-mail z aplikacji C# sprowadza się do połączenia z serwerem SMTP, zbudowania poprawnej wiadomości MIME i bezpiecznego uwierzytelnienia. Pokażę praktyczne rozwiązanie z biblioteką MailKit, obsługę HTML i załączników, konfigurację sekretów oraz sytuacje, w których lepiej użyć API dostawcy albo Microsoft Graph.
Najprostszy stabilny model wysyłki wiadomości z C#
- MailKit jest rozsądnym wyborem dla nowych aplikacji .NET korzystających z SMTP.
- Port 587 zwykle łączymy z szyfrowaniem STARTTLS, a port 465 z TLS od początku połączenia.
- Hasła i tokeny trzymaj poza kodem, najlepiej w Secret Managerze, Key Vault lub zmiennych środowiskowych.
- Wiadomości HTML, tekst alternatywny i załączniki obsługuje klasa BodyBuilder.
- W aplikacji produkcyjnej dodaj logowanie, timeouty, kontrolowane ponowienia i kolejkę dla większego ruchu.

Jak działa wysyłanie e-maila w C#
Najczęściej aplikacja nie dostarcza wiadomości bezpośrednio do skrzynki odbiorcy. Łączy się z serwerem SMTP dostawcy, uwierzytelnia konto, przekazuje przygotowaną wiadomość, a serwer pocztowy zajmuje się dalszą dostawą.
W kodzie trzeba więc rozwiązać cztery osobne problemy: połączenie z SMTP, uwierzytelnienie, budowę treści oraz obsługę błędów. Samo wywołanie metody wysyłającej jest najłatwiejszą częścią całego zadania.
Do nowego projektu wybrałbym MailKit. Klasa System.Net.Mail.SmtpClient nadal istnieje i może być potrzebna w starszych aplikacjach, ale Microsoft nie rekomenduje jej do nowego kodu, ponieważ nie obsługuje wielu współczesnych mechanizmów.
MailKit i pierwszy działający przykład
Bibliotekę zainstalujesz z NuGeta pakietem MailKit. Zawiera ona również elementy potrzebne do tworzenia wiadomości MIME, czyli formatu pozwalającego łączyć tekst, HTML, obrazy i załączniki w jednym e-mailu.
Poniższy przykład korzysta z portu 587 i STARTTLS. To popularny wariant dla aplikacji serwerowych, ale dokładne dane zależą od dostawcy skrzynki lub usługi transakcyjnej.
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
public sealed record SmtpSettings(
string Host,
int Port,
string Username,
string Password,
string FromAddress);
public static class EmailSender
{
public static async Task SendAsync(
SmtpSettings settings,
string recipient,
CancellationToken cancellationToken = default)
{
var message = new MimeMessage();
message.From.Add(
MailboxAddress.Parse(settings.FromAddress));
message.To.Add(
MailboxAddress.Parse(recipient));
message.Subject = "Potwierdzenie zamówienia";
var body = new BodyBuilder
{
TextBody =
"Twoje zamówienie zostało przyjęte do realizacji.",
HtmlBody = """
Dziękujemy za zamówienie
Twoje zamówienie zostało
przyjęte do realizacji.
"""
};
message.Body = body.ToMessageBody();
using var smtp = new SmtpClient();
await smtp.ConnectAsync(
settings.Host,
settings.Port,
SecureSocketOptions.StartTls,
cancellationToken);
await smtp.AuthenticateAsync(
settings.Username,
settings.Password,
cancellationToken);
await smtp.SendAsync(message, cancellationToken);
await smtp.DisconnectAsync(
true,
cancellationToken);
}
}
Właściwość TextBody jest ważna nawet wtedy, gdy wiadomość ma wersję HTML. Część klientów pocztowych może nie wyświetlić HTML, a tekstowa wersja poprawia również dostępność i dostarczalność.
Adres nadawcy powinien zwykle należeć do konta, które uwierzytelniło się na serwerze. Ustawienie dowolnego adresu w polu From może zakończyć się błędem albo zwiększyć ryzyko uznania wiadomości za podejrzaną.
Załącznik z pliku lub pamięci
Załącznik dodasz do tego samego obiektu BodyBuilder. W praktyce lepiej wcześniej sprawdzić rozmiar i typ pliku, zamiast pozwalać użytkownikowi przesłać dowolne dane bez ograniczeń.
var body = new BodyBuilder
{
TextBody = "W załączniku znajduje się raport.",
HtmlBody = "W załączniku znajduje się raport.
"
};
body.Attachments.Add(
"raport.pdf",
await File.ReadAllBytesAsync("raport.pdf", cancellationToken),
new ContentType("application", "pdf"));
message.Body = body.ToMessageBody();
Dla dużych plików trzeba uwzględnić limity dostawcy SMTP. Załącznik jest kodowany w wiadomości, więc jego rozmiar po wysłaniu może być większy niż rozmiar pliku na dysku.
Konfiguracja SMTP bez haseł w kodzie
Najgorszy możliwy wariant to wpisanie hasła bezpośrednio w klasie wysyłającej. Taki sekret może trafić do repozytorium, logów CI/CD albo kopii zapasowej. Konfigurację oddziel od logiki biznesowej i pobieraj ją z bezpiecznego źródła.
Lokalnie może to być .NET User Secrets albo zmienna środowiskowa. Na platformie Azure rozsądny wybór stanowi Azure Key Vault, a w prostszym wdrożeniu sekret zarządzany przez system CI/CD.
var settings = new SmtpSettings(
Host: Environment.GetEnvironmentVariable("SMTP_HOST")
?? throw new InvalidOperationException("Brak SMTP_HOST"),
Port: int.Parse(
Environment.GetEnvironmentVariable("SMTP_PORT") ?? "587"),
Username: Environment.GetEnvironmentVariable("SMTP_USERNAME")
?? throw new InvalidOperationException("Brak SMTP_USERNAME"),
Password: Environment.GetEnvironmentVariable("SMTP_PASSWORD")
?? throw new InvalidOperationException("Brak SMTP_PASSWORD"),
FromAddress: Environment.GetEnvironmentVariable("SMTP_FROM")
?? throw new InvalidOperationException("Brak SMTP_FROM"));
W aplikacji ASP.NET Core warto zarejestrować ustawienia przez IOptions i wstrzyknąć osobny serwis, na przykład IEmailSender. Dzięki temu kontroler nie zna szczegółów SMTP, a testy mogą korzystać z atrap zamiast prawdziwego serwera.
SMTP, API dostawcy czy Microsoft Graph
SMTP jest uniwersalne i łatwo zmienić dostawcę bez przepisywania całej logiki wiadomości. Z drugiej strony trzeba samodzielnie obsłużyć połączenia, uwierzytelnienie, limity oraz część problemów z dostarczalnością.
| Rozwiązanie | Kiedy ma sens | Najważniejszy kompromis |
|---|---|---|
| MailKit i SMTP | Własny serwer, dostawca poczty lub usługa transakcyjna | Więcej konfiguracji po stronie aplikacji |
| API dostawcy e-mail | Dużo wiadomości, statystyki, webhooki i kontrola dostarczalności | Większe uzależnienie od konkretnej usługi |
| Microsoft Graph | Organizacja korzysta z Microsoft 365 i wysyła z konkretnych skrzynek | Trzeba skonfigurować Entra ID, uprawnienia i tokeny |
Jeśli aplikacja działa w ekosystemie Microsoft 365, Microsoft Graph bywa lepszy od klasycznego hasła SMTP. Operacja wysyłki wymaga uprawnienia Mail.Send, a poprawna odpowiedź oznacza przyjęcie żądania do przetworzenia, nie gwarantowane dostarczenie do skrzynki odbiorcy.
Przy dużej liczbie wiadomości rozważyłbym API wyspecjalizowanej usługi e-mail. Zwykle oferuje ono statystyki odbić, blokad i skarg, których sam SMTP nie rozwiązuje. Dla kilku wiadomości dziennie taki wybór może być jednak niepotrzebnym komplikowaniem projektu.
Bezpieczeństwo i dostarczalność wiadomości
Szyfrowanie połączenia nie jest dodatkiem, tylko podstawą. Dla portu 587 używaj STARTTLS, a dla portu 465 zwykle TLS od początku połączenia. Port 25 zostaw dla komunikacji serwer-serwer lub środowisk, które wyraźnie tego wymagają.
Nie wyłączaj walidacji certyfikatu, nawet jeśli lokalnie rozwiązuje to problem z połączeniem. Taki skrót otwiera drogę do ataku typu man-in-the-middle i może ukryć rzeczywistą przyczynę błędu konfiguracji.
Domena nadawcy powinna mieć poprawnie skonfigurowane rekordy SPF, DKIM i DMARC. Są to mechanizmy, które pomagają serwerom odbiorczym ocenić, czy wiadomość rzeczywiście pochodzi od uprawnionego nadawcy. Sam poprawny kod C# nie wystarczy, jeśli domena ma złą reputację.
W logach zapisuj identyfikator operacji, adres serwera, czas trwania i typ błędu. Nie zapisuj hasła, tokena ani pełnej treści wiadomości zawierającej dane osobowe. W przypadku danych klientów logi powinny podlegać takim samym zasadom ochrony jak reszta systemu.
Typowe błędy i sposób diagnozy
| Objaw | Najczęstsza przyczyna | Co sprawdzić |
|---|---|---|
| Connection refused | Zły host, port lub blokada sieciowa | Adres SMTP, reguły zapory i wymagany port |
| Authentication failed | Błędne dane lub wyłączone logowanie hasłem | Token, hasło aplikacji, OAuth i uprawnienia konta |
| Certificate validation failed | Problem z certyfikatem albo zegarem systemowym | Certyfikat, DNS i datę na serwerze |
| Message rejected | Limit, zły nadawca lub polityka antyspamowa | Limity dostawcy, SPF, DKIM, DMARC i reputację domeny |
| Wiadomość nie dociera | Serwer przyjął e-mail, ale dostawa jest opóźniona lub odrzucona | Kolejkę dostawcy, odbicia i folder spamu |
Nie traktuj braku wyjątku w kodzie jako dowodu, że wiadomość znalazła się w skrzynce. Metoda wysyłająca potwierdza najczęściej przekazanie wiadomości do serwera SMTP. Dalsza dostawa zależy od wielu systemów pośrednich.
Przeczytaj również: C# collections - jak dobrać kolekcję do operacji?
Ponowienia wymagają ostrożności
Retry ma sens przy chwilowym timeoutcie albo błędzie sieci, ale ponowienie po niejednoznacznym zerwaniu połączenia może wysłać ten sam e-mail dwa razy. W komunikatach biznesowych przydaje się własny identyfikator operacji oraz zapis statusu w bazie.
W aplikacji webowej nie blokowałbym żądania użytkownika przez kilka sekund tylko po to, aby wysłać wiadomość. Lepszy model to zapisanie zadania w kolejce i wysłanie go przez proces działający w tle. Użytkownik dostaje szybką odpowiedź, a system może kontrolować ponowienia i limity.
Co wybrać w nowym projekcie .NET
Dla małej aplikacji lub panelu administracyjnego zacząłbym od MailKit, portu 587 i osobnego serwisu e-mail. To rozwiązanie jest proste, przenośne i wystarczające dla resetów haseł, powiadomień czy raportów wysyłanych kilka razy dziennie.
Jeśli firma używa Microsoft 365 i wiadomości mają wychodzić z firmowych skrzynek, sprawdziłbym Microsoft Graph. Przy większym wolumenie, wymaganiu analityki i obsłudze odbić wybrałbym API usługi transakcyjnej zamiast budować cały mechanizm od podstaw.
Najważniejsza praktyczna rada brzmi: oddziel kod tworzący wiadomość od kodu odpowiedzialnego za transport. Dzięki temu można zmienić SMTP na API, dodać kolejkę albo przejść na OAuth bez przebudowy całej aplikacji.
