Przejdź do treści

Wysyłanie maili z aplikacji – SMTP, API, zwroty i webhooki

Ostatnia aktualizacja: 26:10:2026

English version

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ą.

SMTPAPI HTTP
Integracjakażdy język i framework ma bibliotekę do wysyłki; często wystarczy konfiguracjabiblioteka klienta albo zwykłe żądania HTTPS
Dane logowanianazwa użytkownika i hasło skrzynkiklucz API, ograniczony do API
Błędykody odpowiedzi SMTP, czasem dopiero po DATAustrukturyzowane błędy (walidacja, limit, brak zasobu) zanim cokolwiek zostanie wysłane
Limity czasu i ponowieniapołączenie i ponowienia obsługujesz samzwykłe limity czasu HTTP; łatwo opakować w kolejkę zadań
Poza wysyłkąnic – SMTP tylko wysyłazwykle także odczyt odpowiedzi, status dostarczenia i webhooki
Integracja
SMTP: każdy język i framework ma bibliotekę do wysyłki; często wystarczy konfiguracja
API HTTP: biblioteka klienta albo zwykłe żądania HTTPS
Dane logowania
SMTP: nazwa użytkownika i hasło skrzynki
API HTTP: klucz API, ograniczony do API
Błędy
SMTP: kody odpowiedzi SMTP, czasem dopiero po DATA
API HTTP: ustrukturyzowane błędy (walidacja, limit, brak zasobu) zanim cokolwiek zostanie wysłane
Limity czasu i ponowienia
SMTP: połączenie i ponowienia obsługujesz sam
API HTTP: zwykłe limity czasu HTTP; łatwo opakować w kolejkę zadań
Poza wysyłką
SMTP: nic – SMTP tylko wysyła
API HTTP: 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.

TypeScript: wysyłka linku do resetu hasła
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:

Python: wysyłka potwierdzenia zamówienia (i odbiór zdarzeń – zob. niżej)
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 "", 204
Go: wysyłka maila z potwierdzeniem adresu
func 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.
TypeScript: proces obsługujący outbox, który nigdy nie wysyła dwa razy
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:

WynikCo widzisz
Dostarczona przy pierwszej próbiekopia w Wysłanych ze statusem delivered
Trwały błąd od razudeliveryWarning w wyniku wysyłki, webhook message.delivery_failed i raport o niedostarczeniu w skrzynce nadawcy
Błąd tymczasowystatus 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 ponowieniastatus failed, webhook message.delivery_failed i raport o niedostarczeniu
Dostarczona przy pierwszej próbie
Co widzisz: kopia w Wysłanych ze statusem delivered
Trwały błąd od razu
Co widzisz: deliveryWarning w wyniku wysyłki, webhook message.delivery_failed i raport o niedostarczeniu w skrzynce nadawcy
Błąd tymczasowy
Co widzisz: 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
Co widzisz: status failed, webhook message.delivery_failed i raport o niedostarczeniu

Co 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.

TypeScript (Express): weryfikacja, deduplikacja i obsługa zdarzenia
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 id zdarzenia.
  • Odpowiadaj szybko. Zwróć 2xx w 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ą 401 i 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.