Wysyłanie maili z aplikacji – SMTP, API, zwroty i webhooki
Ostatnia aktualizacja: 26:10:2026
Co to jest mail transakcyjny
Mail transakcyjny to wiadomość, którą aplikacja wysyła, bo konkretna osoba właśnie coś zrobiła albo musi się o czymś dowiedzieć: potwierdź adres e-mail, ustaw nowe hasło, oto Twoje potwierdzenie zamówienia, eksport jest gotowy, ktoś zalogował się z nowego urządzenia. Trafia do jednego odbiorcy, jest oczekiwana i często pilna – link do resetu hasła, który dochodzi po dwudziestu minutach, kończy się zgłoszeniem do pomocy technicznej.
Dlatego wymagania są inne niż przy newsletterze. Wolumen jest niewielki, ale liczy się każda wiadomość: musi dojść szybko, trafić do odebranych, a nie do spamu, nigdy nie zostać przypadkiem wysłana dwa razy, a gdy nie da się jej dostarczyć, aplikacja powinna się o tym dowiedzieć i rozsądnie zareagować.
SMTP czy API HTTP?
Wiadomość można przekazać dostawcy poczty na dwa sposoby. W obu przypadkach kończy się to taką samą dostawą SMTP do serwera odbiorcy; różni się tylko to, jak Twój kod rozmawia z dostawcą.
| SMTP | API HTTP | |
|---|---|---|
| Integracja | każdy język i framework ma bibliotekę do wysyłki; często wystarczy konfiguracja | biblioteka klienta albo zwykłe żądania HTTPS |
| Dane logowania | nazwa użytkownika i hasło skrzynki | klucz API, ograniczony do API |
| Błędy | kody odpowiedzi SMTP, czasem dopiero po DATA | ustrukturyzowane błędy (walidacja, limit, brak zasobu) zanim cokolwiek zostanie wysłane |
| Limity czasu i ponowienia | połączenie i ponowienia obsługujesz sam | zwykłe limity czasu HTTP; łatwo opakować w kolejkę zadań |
| Poza wysyłką | nic – SMTP tylko wysyła | zwykle także odczyt odpowiedzi, status dostarczenia i webhooki |
SMTP wybierz, gdy konfigurujesz oprogramowanie, które już go obsługuje (CMS, wbudowany mailer frameworka, urządzenie). W kodzie, który piszesz sam, API to zwykle mniej pracy i czytelniejsze błędy. AnyMail oferuje oba sposoby: uwierzytelnioną wysyłkę SMTP na anymail.tech:587 z hasłem aplikacji dla skrzynki (konfiguracja jak w programie pocztowym – zob. ustawienia IMAP i SMTP) oraz Developer API z oficjalnymi SDK dla TypeScriptu, Pythona i Go. W obu przypadkach każdy odbiorca wiadomości – z pól Do, DW czy UDW, do 50 osób – jest doręczany i raportowany osobno, więc jeden błędny adres nie blokuje pozostałych. Poczta transakcyjna zwykle i tak ma jednego odbiorcę.
Najpierw uwierzytelnij domenę nadawcy
Wiadomość, która podaje się za wysłaną z Twojej domeny, jest wiarygodna tylko wtedy, gdy domena to potwierdza. Przed pierwszą prawdziwą wiadomością opublikuj:
- rekord SPF obejmujący Twojego dostawcę,
- klucz DKIM, żeby każda wiadomość była podpisana z
d=Twojej domeny, - politykę DMARC – Gmail i Yahoo wymagają jej od nadawców masowych, a ona wiąże widoczny adres From z powyższymi kontrolami.
Potem wysyłaj z adresu w tej domenie, na który człowiek mógłby sensownie odpowiedzieć – pomoc@ czy konto@ budzą zwykle więcej zaufania niż noreply@, a odpowiedzi na potwierdzenie zamówienia to często prawdziwe pytania klientów. W AnyMail nadawcą musi być skrzynka utworzona na serwerze pocztowym, którego wszystkie kontrole DNS (własność domeny, MX, SPF, DKIM i DMARC) przeszły pomyślnie; do tego czasu wysyłka jest odrzucana, zamiast po cichu wychodzić bez uwierzytelnienia. Poczta wychodząca jest podpisywana DKIM kluczem Twojej domeny i używa Twojego adresu jako nadawcy z koperty, więc SPF i DKIM są wyrównane z From.
Wysyłka przez SDK
Z SDK dla TypeScriptu (npm install @anymail/sdk) wysyłka to jedno wywołanie. from to adres skrzynki – SDK samo ustali jej identyfikator. Klucze API tworzysz w zakładce API swojego konta; są częścią planu Pro.
import { Anymail } from '@anymail/sdk';
await Anymail.connect({ apiKey: process.env.ANYMAIL_API_KEY! });
export async function sendPasswordReset(email: string, resetUrl: string) {
const sent = await Anymail.sendMessage({
from: 'no-reply@example.com',
to: email,
subject: 'Reset your password',
text: `Someone asked to reset the password for this account.\n\nOpen this link within 60 minutes:\n${resetUrl}\n\nIf it wasn't you, you can ignore this email.`
});
if (sent.deliveryWarning) console.warn('Reset email not delivered:', sent.deliveryWarning);
return sent.id;
}Zwrócona wiadomość to kopia zapisana w folderze Wysłane skrzynki. Pole deliveryWarning jest ustawione, gdy przy pierwszej próbie w ogóle nie dało się dotrzeć do odbiorcy – domena nie ma serwera pocztowego, serwer odbiorcy od razu odrzucił adres albo odbiorca wypisał się z poczty od tego nadawcy – więc możesz od razu powiedzieć o tym użytkownikowi („nie udało się dostarczyć wiadomości na ten adres – sprawdź, czy nie ma literówki”).
SDK dla Pythona i Go działają tak samo:
Anymail.connect() # reads ANYMAIL_API_KEY from the environment
app = Flask(__name__)
def send_receipt(order):
sent = Anymail.send_message(
from_="orders@example.com",
to=order.email,
subject=f"Your receipt for order {order.number}",
html=render_receipt_html(order),
)
if sent.delivery_warning:
log.warning("receipt for order %s not delivered: %s", order.number, sent.delivery_warning)
return sent.id
@app.post("/webhooks/anymail")
def anymail_webhook():
try:
event = Anymail.webhooks.parse(request.get_data(), request.headers.get("X-AnyMail-Signature"), WEBHOOK_SECRET)
except WebhookSignatureError:
return "", 401
if not remember_event(event.id): # False when this event id was handled before
return "", 204
if event.type == "message.delivery_failed":
mark_undeliverable(event.data["to"], event.data["error"])
return "", 204func sendWelcome(ctx context.Context, to, confirmURL string) error {
mail, err := anymailsdk.Connect(ctx, anymailsdk.Options{APIKey: os.Getenv("ANYMAIL_API_KEY")})
if err != nil {
return err
}
sent, err := mail.SendMessage(ctx, anymailsdk.OutgoingMessage{
From: "no-reply@example.com",
To: to,
Subject: "Confirm your email address",
Text: "Welcome! Confirm your address by opening this link:\n" + confirmURL,
})
if err != nil {
return err
}
if sent.DeliveryWarning != "" {
log.Printf("confirmation to %s not delivered: %s", to, sent.DeliveryWarning)
}
return nil
}Każde SDK pozwala też czytać pocztę (odpowiedzi, wątki, załączniki) – pełny opis znajdziesz w dokumentacji dla programistów.
Ponowienia bez duplikatów
Wywołania sieciowe zawodzą, a najgroźniejsza jest porażka niejednoznaczna: żądanie przekroczyło czas i nie wiesz, czy dostawca wysłał wiadomość. Ponowisz na ślepo – użytkownik dostanie dwa linki do resetu hasła; nie ponowisz – może nie dostać żadnego. Kilka nawyków pozwala nad tym zapanować:
- Nie wysyłaj w trakcie obsługi żądania. Zapisz maila do kolejki albo tabeli „outbox” w tej samej transakcji bazy danych co zmianę, która go wywołała (zamówienie, rejestrację), i niech wyśle go osobny proces. Wolny dostawca poczty nie spowolni wtedy strony rejestracji, a awaria między „zamówienie zapisane” a „mail wysłany” nie zgubi maila.
- Ponawiaj tylko to, co bezpiecznie ponowić. Błąd walidacji sam się nie naprawi; limit żądań minie po czasie, o który prosi serwer. SDK AnyMail automatycznie ponawiają odczyty, ale celowo nigdy nie ponawiają wysyłki bez Twojej wiedzy – ta decyzja należy do Twojego kodu.
- Sprawdź, zanim wyślesz ponownie. API AnyMail nie ma klucza idempotencji, ale przechowuje w folderze Wysłane kopię każdej przyjętej wiadomości. Po niejednoznacznym błędzie poszukaj tej kopii, zanim wyślesz ponownie.
import { ApiError, NetworkError, RateLimitError, ValidationError } from '@anymail/sdk';
export async function processOutbox() {
for (const job of await db.outbox.claimDue(20)) {
if (job.attemptedAt) {
// A previous attempt ended without an answer. Look before sending again.
const since = new Date(job.attemptedAt.getTime() - 60_000); // allow for clock skew
const sentCopies = await Anymail.getMessages({ inbox: job.from, folder: 'sent', subject: job.subject, since });
const already = sentCopies.find((message) => message.to.includes(job.to));
if (already) {
await db.outbox.markSent(job.id, already.id);
continue;
}
}
await db.outbox.markAttempted(job.id, new Date());
try {
const sent = await Anymail.sendMessage({ from: job.from, to: job.to, subject: job.subject, text: job.text });
await db.outbox.markSent(job.id, sent.id);
} catch (error) {
if (error instanceof RateLimitError) await db.outbox.retryLater(job.id, error.retryAfterMs ?? 60_000);
else if (error instanceof ValidationError) await db.outbox.markFailed(job.id, error.message);
else if (error instanceof NetworkError || (error instanceof ApiError && (error.status ?? 0) >= 500)) {
await db.outbox.retryLater(job.id, 30_000); // outcome unknown: the next run checks Sent first
} else throw error;
}
}
}Limity są tu dwojakiego rodzaju. Zwykły limit żądań API odpowiada kodem 429 z nagłówkiem Retry-After, którego SDK przestrzega. Niezależnie od tego każdy serwer pocztowy AnyMail ma godzinny limit poczty wychodzącej – niższy dla nowego serwera w ciągu pierwszych dwóch tygodni, żeby reputacja nadawcy budowała się stopniowo – a jego osiągnięcie to również 429, z kodem SEND_VOLUME_THROTTLED. W obu przypadkach właściwa reakcja jest ta sama: zachowaj zadanie i spróbuj później.
Zwroty i status dostarczenia
Przyjęcie wiadomości przez dostawcę to jeszcze nie dostarczenie. Po przekazaniu serwer odbiorcy wciąż może wiadomość przyjąć, odłożyć („spróbuj później”, odpowiedź SMTP 4xx) albo odrzucić na dobre (5xx: nie ma takiego użytkownika, skrzynka zbyt długo pełna, odrzucenie z powodu polityki). Gdy wiadomości ostatecznie nie da się dostarczyć, standardową formą informacji jest powiadomienie o stanie doręczenia – zwrot (bounce) – w formacie z RFC 3464.
W przypadku poczty wysyłanej z aplikacji przez AnyMail wygląda to tak:
| Wynik | Co widzisz |
|---|---|
| Dostarczona przy pierwszej próbie | kopia w Wysłanych ze statusem delivered |
| Trwały błąd od razu | deliveryWarning w wyniku wysyłki, webhook message.delivery_failed i raport o niedostarczeniu w skrzynce nadawcy |
| Błąd tymczasowy | status queued; AnyMail ponawia próby z rosnącymi odstępami przez nieco ponad dwie doby, a po kilku godzinach wysyła do skrzynki nadawcy jedno powiadomienie o opóźnieniu |
| Wyczerpane ponowienia | status failed, webhook message.delivery_failed i raport o niedostarczeniu |
delivereddeliveryWarning w wyniku wysyłki, webhook message.delivery_failed i raport o niedostarczeniu w skrzynce nadawcyqueued; AnyMail ponawia próby z rosnącymi odstępami przez nieco ponad dwie doby, a po kilku godzinach wysyła do skrzynki nadawcy jedno powiadomienie o opóźnieniufailed, webhook message.delivery_failed i raport o niedostarczeniuCo aplikacja powinna zrobić z błędem: przestać wysyłać na adres, który dał trwały zwrot (i poprosić użytkownika o poprawienie go przy następnym logowaniu), nie ponawiać samodzielnie trwałych błędów i obserwować ich liczbę – nagły wzrost zwrotów oznacza zwykle błąd, który psuje adresy, albo formularz rejestracji nadużywany zmyślonymi adresami. Jedno i drugie szkodzi reputacji nadawcy.
Webhooki ze zdarzeniami o dostarczeniu
Odpytywanie o status działa, ale webhook jest prostszy: rejestrujesz adres HTTPS, a dostawca wysyła na niego żądanie POST ze zdarzeniem, gdy coś się wydarzy. AnyMail wysyła trzy typy zdarzeń – message.received (poczta dotarła do jednej z Twoich skrzynek), message.sent (wiadomość została wysłana; pole delivered mówi, jak poszła pierwsza próba dostarczenia) i message.delivery_failed (dostarczenie ostatecznie się nie powiodło). Webhooki są częścią planu Pro i zarządzasz nimi w zakładce Webhooks w części dla programistów.
Każde wywołanie jest podpisane: nagłówek X-AnyMail-Signature to sha256= i HMAC-SHA256 surowej treści żądania, obliczony z sekretem webhooka. Weryfikuj go na treści dokładnie w takiej postaci, w jakiej przyszła, przed parsowaniem JSON – ponowna serializacja sparsowanego JSON zmienia bajty i psuje podpis.
import express from 'express';
import { WebhookSignatureError } from '@anymail/sdk';
const app = express();
app.post('/webhooks/anymail', express.raw({ type: 'application/json' }), async (req, res) => {
let event;
try {
event = Anymail.webhooks.parse(req.body, req.header('X-AnyMail-Signature'), process.env.ANYMAIL_WEBHOOK_SECRET!);
} catch (error) {
if (error instanceof WebhookSignatureError) return res.sendStatus(401);
throw error;
}
// Deliveries can repeat: record the event id first and skip one you've already handled.
if (!(await db.webhookEvents.insertIfNew(event.id))) return res.sendStatus(204);
if (event.type === 'message.delivery_failed') {
await db.emails.markUndeliverable(event.data.to, event.data.error);
}
res.sendStatus(204);
});- Spodziewaj się powtórek. Wywołanie, które przekroczy czas albo dostanie odpowiedź inną niż 2xx, jest ponawiane – do sześciu prób w ciągu mniej więcej dwóch godzin – więc to samo zdarzenie może przyjść dwa razy. Deduplikuj po polu
idzdarzenia. - Odpowiadaj szybko. Zwróć
2xxw ciągu kilku sekund, a wolniejszą pracę wykonaj potem w osobnym zadaniu. AnyMail przerywa próbę po 8 sekundach i wyłącza webhook, gdy 10 kolejnych zdarzeń nie uda się dostarczyć mimo wszystkich ponowień. - Odrzucaj złe podpisy odpowiedzią
401i nigdy nie reaguj na niezweryfikowaną treść.
Rozdziel pocztę transakcyjną i marketingową
Serwery odbiorców oceniają pocztę między innymi po reputacji nadawcy. Jeśli newsletter zbiera zgłoszenia spamu i wychodzi z tego samego adresu i domeny co resety haseł, płacą za to resety. Wytyczne Google dla nadawców zalecają osobne adresy From dla różnych rodzajów poczty i niemieszanie treści promocyjnych z transakcyjnymi; wielu nadawców idzie dalej i do marketingu używa osobnej subdomeny, np. news.firma.pl.
- Nie dokładaj promocji do potwierdzenia zamówienia ani do powiadomienia o bezpieczeństwie. Pisz krótko i na temat.
- Daj każdemu strumieniowi osobny adres, a najlepiej osobną subdomenę z własnymi rekordami SPF, DKIM i DMARC.
- Poczta marketingowa wymaga zgody i łatwego wypisania się; transakcyjna musi docierać także do osób, które zrezygnowały z marketingu.
Ostatni punkt ma w AnyMail konkretne skutki. Każda wiadomość, którą AnyMail dostarcza poza platformę, ma nagłówek List-Unsubscribe z wypisaniem jednym kliknięciem, a wypisanie jest zapisywane dla pary serwer pocztowy – odbiorca. Jeśli newslettery i resety haseł wychodzą z tego samego serwera pocztowego, osoba, która wypisze się z newslettera, przestanie też dostawać resety haseł. Marketing na osobnym serwerze pocztowym (np. w subdomenie) sprawia, że oba strumienie są niezależne. AnyMail nie jest też platformą do masowych newsletterów: każdy odbiorca wlicza się do godzinnego limitu wysyłki, co pasuje do poczty transakcyjnej, a nie do dużych kampanii.
Krótka lista kontrolna
- Domena nadawcy ma SPF, DKIM i DMARC – wszystkie przechodzą i są wyrównane z From.
- Maile przechodzą przez kolejkę lub outbox, nigdy nie są wysyłane bezpośrednio z obsługi żądania.
- Niejednoznaczne błędy są sprawdzane przed ponowną wysyłką; błędów walidacji się nie ponawia.
- Trwałe zwroty wstrzymują wysyłkę na dany adres; zdarzenia o dostarczeniu przychodzą przez webhooki.
- Podpisy webhooków są weryfikowane na surowej treści, a zdarzenia deduplikowane po id.
- Marketing ma osobny adres, a najlepiej osobną subdomenę.
- Cały proces jest objęty testem automatycznym – zob. testowanie maili w testach automatycznych.
Niech AnyMail zajmie się tymi rekordami za Ciebie
AnyMail generuje rekordy MX, SPF, DKIM i DMARC dla Twojej domeny, automatycznie sprawdza każdy z nich i wskazuje, który trzeba poprawić – a potem utrzymuje Twoje skrzynki. Usługa działa z Polski, dane przechowujemy w UE.