Postmark mit Rails und Solidus: sichere Transaktionsmails in Development, Staging und Production
Kurz gesagt
Niveau: Fortgeschritten
- Production sendet über Postmark. Development öffnet Mails mit letter_opener im Browser. Test hält sie in einem Array fest. Staging kopiert Production, aber ein Interceptor schreibt jeden Empfänger auf ein Teampostfach um.
- Jeder Postmark Server Token liegt in den verschlüsselten Rails Credentials, eine Datei pro Umgebung, Schlüssel nicht im Git. Ein Postmark Server pro Umgebung.
- Solidus Mails laufen über den Standard Transactional Stream, Newsletter über einen Broadcast Stream.
- Bounces und Spambeschwerden per Webhook abfangen, DKIM, eigenen Return Path und DMARC einrichten und Bounce und Beschwerdequoten pro Stream lesen, nicht als Gesamtzahl.
Ein Solidus Shop verschickt Bestellbestätigungen, Versandnachrichten und Passwort Resets von selbst, und jede davon muss in Production beim Kunden ankommen und überall sonst bei niemandem. Dieser Leitfaden richtet das mit Postmark Schritt für Schritt ein, vom Ort, an dem der Token liegt, bis zur Bounce Quote im ersten Monat, am Beispiel einer erfundenen Kaffeerösterei in Leipzig.
Ein Shop, vier Orte, an denen Mails schiefgehen können
Ein Onlineshop verschickt zwei Arten von Mails. Da ist der Newsletter, den du schreibst und bewusst abschickst. Und da ist alles, was der Shop von selbst verschickt: die Bestellbestätigung, die Versandnachricht, den Link zum Zurücksetzen des Passworts. Das sind Transaktionsmails, weil jede von einem Vorgang ausgelöst wird (einer Bestellung, einem Login, einer Erstattung) und an genau eine Person geht.
Niemand lobt eine Transaktionsmail, aber jeder merkt, wenn sie fehlt. Wer 38 € für Kaffee bezahlt und keine Bestätigung bekommt, verdächtigt nicht deinen Mailserver. Er verdächtigt dich.
Derselbe Code läuft an vier Orten. Development ist dein Laptop. Test ist die automatische Testsuite. Staging ist eine private Kopie des Shops zum Ausprobieren. Production ist der echte Shop. Das Ziel: In Production erreicht jede Mail die Kundin oder den Kunden. Überall sonst erreicht keine einzige Mail jemals einen Kunden, und du siehst trotzdem genau, was verschickt worden wäre.
Für den Versand in Production nutze ich Postmark. Es macht eine Sache, nämlich Mails aus Anwendungen zustellen, und die Anbindung an Rails ist ein kleines Gem. Das meiste, was folgt, ist schlichtes Rails und passt deshalb auch zu jedem anderen Anbieter.
Durchgerechnetes Beispiel: was die Rösterei Plagwitz verschickt
Die Rösterei Plagwitz ist eine erfundene Kaffeerösterei in Leipzig. Jana führt sie mit zwei Packerinnen in Teilzeit, verkauft Bohnen und Abos über einen Solidus Shop unter roesterei-plagwitz.example und bekommt rund 1.450 Bestellungen im Monat, im Schnitt zu 34 €.
Ab Werk verschickt Solidus Mails, wenn der Checkout abgeschlossen ist, wenn eine Bestellung versendet wird, wenn eine Bestellung storniert wird und wenn ein Admin einen Stapel Gutscheincodes fertig erzeugt hat. Für jede gibt es eine eigene Mailer Klasse, die du über eine Einstellung austauschen kannst.1 Die Bestellbestätigung kommt aus Spree::OrderMailer. Verschickt wird sie von einem Subscriber, der auf das Event order_finalized hört, confirm_email(order).deliver_later aufruft und die Bestellung danach markiert, damit die Mail nie zweimal rausgeht.2
Wenn also eine Kundin in Connewitz ein Kilo Espressobohnen bezahlt, endet der Checkout, Solidus meldet order_finalized, der Subscriber stellt die Bestätigung als Hintergrundjob in die Warteschlange, und der Job übergibt die Mail an die Zustellmethode, die in der jeweiligen Umgebung eingestellt ist. Genau diesen letzten Schritt willst du im Griff haben.
Ein Detail wird später wichtig. Die Bestätigung geht an die Mailadresse der Bestellung und zusätzlich als Blindkopie an die bcc_email des Shops, falls eine hinterlegt ist.3 Jana hat eine hinterlegt, also landet jede Bestätigung auch in ihrem Postfach. Merk dir das für Staging.
Der Postmark Token gehört in die verschlüsselten Credentials, nie ins Git
Postmark erkennt deine Anwendung an einem Server API Token, der bei jeder Anfrage mitgeschickt wird.4 Wer ihn hat, kann im Namen deines Shops Mails verschicken.
Rails hat einen eingebauten Ort für Geheimnisse. Sie liegen in config/credentials.yml.enc, einer Datei, die mit einem Master Key verschlüsselt ist. Weil die Datei verschlüsselt ist, darf sie in die Versionsverwaltung, solange der Schlüssel selbst sicher bleibt. Der Rails Guide sagt es ohne Umschweife: Committe deinen Master Key nicht.5 Bearbeitet wird die Datei mit bin/rails credentials:edit.
Rails erlaubt außerdem eine eigene Credentials Datei pro Umgebung. Rufst du den Befehl mit --environment auf, legt Rails eine Datei mit eigenem Schlüssel an, die in dieser Umgebung Vorrang vor der globalen Datei hat.6 Den Production Token kann dann nur eine Maschine entschlüsseln, die den Production Schlüssel besitzt.
Das README von postmark-rails liest den Token als postmark_api_token aus den Credentials und übergibt ihn als api_token.7 In der ersten Fassung dieses Artikels stand api_key. Richtig ist api_token.
Eine .env Datei ist Klartext, sie wird herumkopiert, und irgendwann committet sie jemand. Ich habe aufgehört zu zählen, in wie vielen Repositories die erste Datei, die ich geöffnet habe, einen gültigen API Token enthielt.
Durchgerechnetes Beispiel: die zwei Credentials Dateien der Rösterei
Janas Entwickler führt den Befehl einmal pro Umgebung aus:
bin/rails credentials:edit --environment production
bin/rails credentials:edit --environment staging
In die Production Datei, hier entschlüsselt gezeigt, kommen der Token des Production Servers sowie Benutzername und Passwort für den Webhook:
postmark_api_token: "the production server token from Postmark"
postmark_webhook:
user: "postmark"
password: "a long random string from bin/rails secret"
Die Staging Datei hat dieselben Schlüssel mit Staging Werten. Development braucht überhaupt keinen Postmark Token.
Die zwei verschlüsselten Dateien kommen ins Git. Die zwei Schlüsseldateien nicht. Der Production Schlüssel liegt auf dem Production Server als RAILS_MASTER_KEY, den Rails vor jeder Schlüsseldatei liest.6 Der Staging Schlüssel liegt nur auf Staging. Ein Laptop, der in einem Leipziger Café geklaut wird, kann keine einzige Mail im Namen des Shops verschicken.
Ein Postmark Server pro Umgebung
Ein "Server" ist bei Postmark keine Maschine. Er ist ein Container in deinem Konto, und der Token, mit dem du sendest, gehört zum Server.4 Getrennte Umgebungen heißen also getrennte Server.
Postmark kennt zwei Arten. Ein Live Server stellt zu. Ein Sandbox Server verhält sich genau wie ein Live Server, Aktivitätsprotokoll, API und Webhooks funktionieren, aber er schickt nie eine Nachricht an einen Empfänger. Die Nachrichten verschwinden in einem schwarzen Loch und stehen in der Oberfläche trotzdem als zugestellt.8 Die Art legst du beim Anlegen des Servers fest, ändern lässt sie sich danach nicht mehr.8 Auch Sandbox Nachrichten zählen zu deinem monatlichen Versandvolumen.8
Durchgerechnetes Beispiel: die drei Server der Rösterei
Jana legt in ihrem Postmark Konto drei Server an:
- Rösterei Plagwitz Production, Live. Sein Token kommt in die Production Credentials.
- Rösterei Plagwitz Staging, Live. Jana will Staging Mails in ihrem echten Gmail auf dem Handy sehen. Der Interceptor sorgt dafür, dass nur sie sie bekommt.
- Rösterei Plagwitz CI, Sandbox. Damit prüft sie den ganzen Weg über die Postmark API, Webhooks eingeschlossen, ohne jede Zustellung.
Wäre es ihr egal, Staging Mails in einem echten Postfach zu sehen, wäre auch der Staging Server ein Sandbox Server. Das ist die strengere Wahl, und die würde ich bei einem Shop treffen, in dem mehrere Leute deployen.
Die Zustellmethode pro Umgebung festlegen
Action Mailer, der Teil von Rails, der Mails verschickt, hat eine Einstellung namens delivery_method. Eingebaut sind :smtp (der Standard), :sendmail, :test und :file, eigene Methoden lassen sich einhängen.9 Das Gem postmark-rails bringt :postmark mit.7 Stellst du sie in jeder Umgebungsdatei anders ein, muss sich niemand etwas merken.
In Development nutze ich das Gem letter_opener. Statt zu senden, öffnet es jede Mail in deinem Browser.10 Du siehst das echte Ergebnis, mit HTML Layout und Links, und nichts verlässt den Laptop.
In der Testsuite hält :test jede Mail im Array ActionMailer::Base.deliveries fest, sodass ein Test prüfen kann, was gebaut wurde.9 Production nutzt :postmark. Staging lädt die Production Datei und überschreibt nur, was anders sein muss.
Durchgerechnetes Beispiel: die Umgebungsdateien der Rösterei
Das Gemfile bekommt zwei Zeilen:
gem "postmark-rails"
gem "letter_opener", group: :development
Development, in config/environments/development.rb, öffnet jede Mail im Browser:
config.action_mailer.delivery_method = :letter_opener
config.action_mailer.perform_deliveries = true
config.action_mailer.default_url_options = { host: "localhost", port: 3000 }
Production, in config/environments/production.rb, sendet über Postmark:
config.action_mailer.delivery_method = :postmark
config.action_mailer.postmark_settings = {
api_token: Rails.application.credentials.postmark_api_token
}
config.action_mailer.default_url_options = {
host: "roesterei-plagwitz.example", protocol: "https"
}
Staging, in config/environments/staging.rb, übernimmt Production und ändert zwei Dinge:
require_relative "production"
Rails.application.configure do
config.action_mailer.default_url_options = {
host: "staging.roesterei-plagwitz.example", protocol: "https"
}
config.action_mailer.interceptors = %w[StagingRecipientInterceptor]
end
Achte darauf, was in der Staging Datei fehlt. Sie nennt nirgends einen Token. Sie erbt die Production Zeile, die postmark_api_token aus den Credentials liest, und weil Staging eine eigene Credentials Datei hat, findet sie dort den Token des Staging Servers. Eine Zeile Code, zwei Geheimnisse, und welches gilt, entscheidet der Schlüssel auf der Maschine.
Auf Staging jeden Empfänger mit einem Interceptor umschreiben
Auf Staging liegen echt wirkende Daten, und Leute geben bei Testbestellungen die Adresse ein, die ihnen gerade einfällt. Früher oder später gehört eine davon einem echten Menschen.
Genau dafür hat Action Mailer einen Haken. Ein Interceptor ist eine Klasse mit einer Methode delivering_email(message), die Rails aufruft, kurz bevor eine Mail an die Zustellmethode geht. Das Beispiel im Rails Guide schreibt den Empfänger auf eine Sandbox Adresse um und registriert den Interceptor nur auf Staging.11
Meine Fassung macht drei Dinge. Sie schickt jede Mail an ein Postfach, das dem Team gehört. Sie leert die Felder für Kopie und Blindkopie, weil Solidus die Shopadresse bei Bestätigungen in Blindkopie setzt und du dieses Postfach auch nicht fluten willst.3 Und sie schreibt den ursprünglichen Empfänger in den Betreff, damit man im Postfach sieht, für wen die Mail gedacht war.
Durchgerechnetes Beispiel: eine Testbestellung auf Staging
Der Interceptor liegt in app/mailers/staging_recipient_interceptor.rb:
class StagingRecipientInterceptor
INBOX = "jana+staging@roesterei-plagwitz.example"
def self.delivering_email(message)
original = Array(message.to).join(", ")
message.to = [INBOX]
message.cc = nil
message.bcc = nil
message.subject = "[STAGING for #{original}] #{message.subject}"
end
end
Janas Entwickler bestellt auf Staging als tobi.test@gmail.com, eine Adresse, die er sich spontan ausgedacht hat, ohne zu prüfen, ob es sie gibt. Das passiert, Schritt für Schritt:
- Der Checkout endet, und der Subscriber stellt
confirm_emailfür die Bestellung R123456789 in die Warteschlange. - Der Job baut die Mail: an
tobi.test@gmail.com, Blindkopie an die Shopadresse, Betreff "Rösterei Plagwitz Bestellbestätigung #R123456789". - Vor der Zustellung ruft Rails den Interceptor auf. Der Empfänger wird zu
jana+staging@roesterei-plagwitz.example, die Blindkopie fällt weg, und der Betreff lautet jetzt "[STAGING for tobi.test@gmail.com] Rösterei Plagwitz Bestellbestätigung #R123456789". - Der Staging Server bei Postmark stellt sie in Janas Postfach zu.
Wem die Gmail Adresse gehört, bekommt nichts. Und falls eines Tages jemand aus Versehen den Production Token in die Staging Credentials kopiert, läuft der Interceptor trotzdem, weil er an der Umgebung hängt und nicht am Token. Das Schlimmste, was dann passiert: Staging Mails tauchen im Production Aktivitätsprotokoll auf. Unordentlich, aber harmlos.
Jede Mail in einer Vorschau ansehen, bevor sie rausgeht
Der Interceptor verhindert, dass Mails an die falsche Person gehen. Die Vorschau verhindert, dass die falsche Mail an die richtige Person geht.
Eine Mailer Preview ist eine kleine Ruby Klasse, die eine Mail baut, ohne sie zu senden. Rails stellt alle Previews auf deinem Entwicklungsserver unter /rails/mailers bereit, sodass du jede Mail mit echt wirkenden Daten öffnen kannst.11 Preview Methoden geben das Mailobjekt zurück, du rufst die Mailer Methode also ohne deliver_now oder deliver_later auf.9 Der Solidus Guide bestätigt, dass sich auch die Solidus Mails so ansehen lassen.1
Eine Feinheit: Für Previews läuft kein Interceptor. Soll einer laufen, gibt es dafür in Rails das eigene register_preview_interceptor.9 Für das Umschreiben auf Staging brauchst du das nicht, denn Previews verlassen den Laptop nie.
Durchgerechnetes Beispiel: die Bestellbestätigung in der Vorschau
class OrderMailerPreview < ActionMailer::Preview
def confirm_email
order = Spree::Order.complete.last
Spree::OrderMailer.confirm_email(order)
end
def cancel_email
order = Spree::Order.complete.last
Spree::OrderMailer.cancel_email(order)
end
end
Der Standardordner für Previews ist test/mailers/previews. Die Rösterei nutzt RSpec, also kommt die Datei nach spec/mailers/previews, eingetragen in config.action_mailer.preview_paths.11
Jana öffnet localhost:3000/rails/mailers/order_mailer/confirm_email und liest ihre Bestätigung so, wie eine Kundin sie lesen würde. Dabei fällt ihr auf, dass die Lieferzeit noch mit "drei bis fünf Werktage" angegeben ist. Das stimmte, bevor sie den Paketdienst gewechselt hat. Eine Minute Arbeit. Ohne die Vorschau hätte es ihr eine Kundin gesagt.
Eine Bestellbestätigung durch alle vier Umgebungen verfolgen
Hier das ganze Zusammenspiel als ein einziger Weg. Dieselbe Bestätigung, derselbe Code, vier verschiedene Enden.
Lies es von oben. Die ersten drei Kästen sind überall gleich, und genau darum geht es: Du testest den echten Codepfad, keine Kopie davon. Nur der Kasten nach der Frage ändert sich, und zwar wegen einer Zeile in einer Umgebungsdatei, nicht weil jemand an etwas gedacht hat. Der letzte Kasten gilt für die zwei Umgebungen, die mit Postmark reden, Staging und Production.
Den echten Weg über Postmark testen, ohne etwas zu senden
Irgendwann willst du wissen, ob der Teil bei Postmark selbst funktioniert. Postmark gibt dir drei Werkzeuge, und sie tun Verschiedenes.
Das erste ist der Testtoken. Schickst du den Wert POSTMARK_API_TEST als Server Token, nimmt Postmark die Anfrage an und sendet nichts.4 Die Nachrichten erscheinen aber nicht in deinem Aktivitätsprotokoll, und einen Bounce kannst du damit nicht simulieren.12
Das zweite ist ein Sandbox Server: Aktivitätsprotokoll und Webhooks, keine Zustellung.8
Das dritte ist die Testdomain für Bounces. Schick eine Mail an hardbounce@bounce-testing.postmarkapp.com, und Postmark erzeugt einen Hard Bounce und setzt die Adresse auf die Sperrliste des Streams. Es gibt ähnliche Adressen für einen Soft Bounce, eine Spammeldung und eine Blockade durch den Provider.13 So erzeugte Bounces zählen nicht zu deinen Bounce Grenzen und schaden deinem Absenderruf nicht.12
Außerdem gibt es eine ältere Adresse, test@blackhole.postmarkapp.com, die Mails annimmt und wegwirft. Jede Mail dorthin zählt zu deinem Monatsvolumen.12 Sie ist ein Empfänger, kein Absender, und das wird im nächsten Beispiel wichtig.
Durchgerechnetes Beispiel: die Bounce Verarbeitung vor dem Start prüfen
Bevor der neue Shop der Rösterei live geht, will der Entwickler den Beweis, dass ein Bounce wirklich beim Webhook des Shops ankommt. Staging hilft hier nicht, denn der Interceptor schreibt jeden Empfänger um, auch die Testadresse. Also öffnet er auf seinem Laptop eine Rails Konsole und lässt Action Mailer nur für diese Sitzung über den CI Sandbox Server senden, dessen Webhook auf den Staging Shop zeigt:
ActionMailer::Base.delivery_method = :postmark
ActionMailer::Base.postmark_settings = { api_token: "the CI sandbox token" }
delivery = Spree::OrderMailer.confirm_email(Spree::Order.complete.last)
delivery.message.to = ["hardbounce@bounce-testing.postmarkapp.com"]
delivery.message.bcc = nil
delivery.deliver_now
Innerhalb einer Minute verbucht Postmark einen Hard Bounce, setzt die Adresse auf inaktiv und ruft den Webhook auf Staging auf, der das Ereignis speichert. Er wiederholt das mit den Adressen für Soft Bounce und Spammeldung. Kosten: drei Nachrichten aus dem Monatskontingent und kein Kratzer am Ruf von roesterei-plagwitz.example.
Der Testleitfaden von Postmark warnt vor der umgekehrten Angewohnheit, Testmails von der Production Domain an ausgedachte Adressen zu schicken, weil die Bounces den Ruf der Domain beschädigen.12 Genau diesen Fehler soll der Interceptor verhindern.
Transaktions und Massenmails in getrennten Streams halten
Postmark teilt die Mails in jedem Server in Message Streams auf. Ein Transactional Stream ist für einzelne, dringende Nachrichten an genau einen Empfänger. Ein Broadcast Stream ist für denselben Inhalt an viele Leute gleichzeitig, etwa einen Newsletter.14 Postmark trennt die beiden bis ganz nach unten: Transaktions und Broadcast Nachrichten gehen über unterschiedliche IP Adressen raus.14
Warum das zählt? Ein Newsletter erntet immer ein paar Spamklicks von Leuten, die vergessen haben, dass sie sich angemeldet haben. Bei gemeinsamen Absenderadressen würden diese Klicks deine Bestellbestätigungen mit Richtung Spamordner ziehen.
Nennst du beim Senden keinen Stream, nimmt Postmark den Standard Transactional Stream des Servers.15 Jede Solidus Mail landet also ohne Änderung am richtigen Ort. Für einen Mailer, der in Masse verschickt, nennst du den Broadcast Stream. Das Postmark Gem gibt dafür jeder Mail einen Setter message_stream.16
Durchgerechnetes Beispiel: der Newsletter der Rösterei
class NewsletterMailer < ApplicationMailer
def issue(subscriber, issue)
message.message_stream = "broadcasts" # the Stream ID set in Postmark
@issue = issue
mail(to: subscriber.email, subject: issue.subject)
end
end
Die Bestellbestätigung braucht keine Änderung. Der Oktober Newsletter geht über den Broadcast Stream raus. Drücken fünf von 2.400 Abonnenten auf den Spamknopf, trifft der Makel die Broadcast Adressen, und die nächste Bestätigung verlässt den Shop weiter über die Transaktionsadressen.
Ein Newsletter braucht außerdem die Einwilligung der Empfänger, und das ist ein eigenes Thema. In Deutschland verlangt § 7 Abs. 2 Nr. 2 UWG für Werbung per Mail eine vorherige ausdrückliche Einwilligung, mit einer engen Ausnahme in Abs. 3 für eigene ähnliche Waren an Bestandskunden, die du bei jeder Mail auf ihr Widerspruchsrecht hinweist.17 Wie ich die Anmeldung dafür baue, steht in Mailerfassung für Consent neu bauen.
Bounces und Spambeschwerden per Webhook abfangen
Ein Bounce ist eine fehlgeschlagene Zustellung. Ein Hard Bounce heißt, die Adresse existiert nicht, und Postmark deaktiviert sie automatisch. Ein Soft Bounce ist vorübergehend, etwa ein volles Postfach, und die Adresse bleibt aktiv.18 Eine Spambeschwerde wird verbucht, wenn der Empfänger auf "Das ist Spam" drückt. Auch diese Adresse deaktiviert Postmark, und du kannst sie nicht selbst wieder freischalten.19
Über jedes dieser Ereignisse kann Postmark deinen Shop sofort per Webhook informieren, einer HTTP Anfrage an eine Adresse in deiner Anwendung. Der Bounce Webhook enthält die betroffene Adresse, die Art des Bounces, den Stream und ob die Adresse deaktiviert wurde.20 Der Webhook für Spambeschwerden hat dieselbe Form mit dem Typ SpamComplaint.19
Schütz den Endpunkt. Postmark signiert seine Webhooks nicht und empfiehlt deshalb HTTPS, Benutzername und Passwort in der Webhook Adresse (Basic Authentication) und optional, nur den IP Bereich von Postmark zuzulassen.21 Fehlgeschlagene Aufrufe wiederholt Postmark nach einem Zeitplan, der knapp eine Stunde läuft, dann ist Schluss.21
Durchgerechnetes Beispiel: der Webhook Controller der Rösterei
Erst die Route, dann der Controller:
post "/webhooks/postmark", to: "postmark_webhooks#create"
class PostmarkWebhooksController < ActionController::API
include ActionController::HttpAuthentication::Basic::ControllerMethods
before_action :authenticate
def create
payload = JSON.parse(request.raw_post)
if %w[Bounce SpamComplaint].include?(payload["RecordType"])
EmailEvent.create!(
email: payload["Email"],
kind: payload["Type"],
message_stream: payload["MessageStream"],
inactive: payload["Inactive"],
occurred_at: payload["BouncedAt"] || Time.current
)
end
head :ok
end
private
def authenticate
expected = Rails.application.credentials.postmark_webhook
authenticate_or_request_with_http_basic do |user, password|
ActiveSupport::SecurityUtils.secure_compare(user, expected[:user]) &
ActiveSupport::SecurityUtils.secure_compare(password, expected[:password])
end
end
end
In Postmark steht als Webhook Adresse https://postmark:THE_PASSWORD@roesterei-plagwitz.example/webhooks/postmark, mit dem Passwort aus den Production Credentials. Der Staging Server bekommt denselben Pfad auf der Staging Domain mit dem Staging Passwort.
Spannend wird es bei dem, was Jana mit den Ereignissen macht. Jeden Morgen listet der Admin die Bestellungen vom Vortag, deren Bestätigung hart gebounct ist. Meistens sind es Tippfehler, gmial.com und Verwandte. Eine Packerin ruft die Kundin an und korrigiert die Adresse. Im ersten Monat standen 30 Bestellungen auf dieser Liste: 30 Kunden, die sonst gefragt hätten, wo ihr Kaffee bleibt.
Mit DKIM, Return Path und DMARC beweisen, dass die Mail von dir ist
Ein empfangender Server fragt bei jeder Nachricht: Kommt die wirklich von der Domain, die sie behauptet? Drei DNS Einträge geben die Antwort.
DKIM ist eine kryptografische Signatur. Postmark signiert jede Nachricht, und ein TXT Eintrag in deinem DNS veröffentlicht den öffentlichen Schlüssel, mit dem Empfänger die Signatur prüfen. Den genauen Hostnamen und Wert zeigt dir Postmark unter Sender Signatures, DNS Settings, und prüft den Eintrag innerhalb von 48 Stunden. Ist die Domain verifiziert, kannst du von jeder Adresse darauf senden.22
SPF listet, welche Server für eine Domain senden dürfen. Hier überrascht Postmark viele: Du musst Postmark nicht mehr in deinen eigenen SPF Eintrag aufnehmen. Empfänger prüfen SPF gegen die Return Path Domain, also die Adresse, an die Bounces zurückgehen, und die gehört standardmäßig Postmark selbst und besteht ohnehin.23
Dieser Standard hat eine Schwäche. DMARC, der dritte Eintrag, besteht nur, wenn SPF oder DKIM ausgerichtet ist, wenn also die geprüfte Domain zur Domain in der Absenderadresse passt.24 Der Standard Return Path von Postmark, pm_bounces@pm.mtasv.net, besteht SPF, passt aber nicht zu deiner Domain. Ein eigener Return Path behebt das: Du legst einen CNAME Eintrag an, standardmäßig auf der Subdomain pm-bounces, der auf pm.mtasv.net zeigt, und trägst ihn in Postmark ein.25 Bounces landen weiter bei Postmark, und SPF ist jetzt auf deine Domain ausgerichtet.
DMARC selbst ist ein TXT Eintrag unter _dmarc, der Empfängern sagt, was sie mit Mails tun sollen, die durchfallen, und wohin Berichte gehen. Der Leitfaden von Postmark beginnt bei p=none, was nur Berichte sammelt, und geht zu Quarantäne und dann Ablehnung über, sobald jeder legitime Absender besteht.24 Gmail verlangt von jedem Absender SPF oder DKIM, und von jedem, der mehr als 5.000 Nachrichten am Tag an Gmail Konten schickt, SPF, DKIM und DMARC.26 Ein kleiner Shop liegt weit unter dieser Grenze, aber die Einträge kosten zehn Minuten.
Durchgerechnetes Beispiel: die DNS Einträge der Rösterei
Janas Entwickler trägt bei ihrem DNS Anbieter drei Einträge ein:
Type Host Value
TXT (host shown by Postmark)._domainkey (public key shown by Postmark)
CNAME pm-bounces pm.mtasv.net
TXT _dmarc v=DMARC1; p=none; rua=mailto:dmarc@roesterei-plagwitz.example
Innerhalb eines Tages zeigt Postmark DKIM und Return Path als verifiziert. Eine Bestätigung an eine Gmail Adresse zeigt jetzt DKIM bestanden für roesterei-plagwitz.example, SPF bestanden für pm-bounces.roesterei-plagwitz.example und DMARC bestanden, weil beide zu ihrer Domain gehören.
Sie lässt DMARC einen Monat auf p=none und liest die Berichte. Darin taucht ein Absender auf, den sie vergessen hatte: das Buchhaltungsprogramm, das Rechnungen von ihrer Domain verschickt. Sobald auch das sauber eingerichtet ist, stellt sie auf p=quarantine um. Wäre sie direkt auf Ablehnung gegangen, wären ihre Rechnungen verschwunden.
Den ersten Monat in Quoten lesen, nicht in Stückzahlen
Eine Stückzahl allein sagt nichts. Hundert Bounces bei 100.000 Mails sind hervorragend. Hundert bei 1.000 sind ein Brand. Entscheidend ist die Quote.
Dabei ist die Bounce Quote in Prozent, die Quote der Spambeschwerden in Prozent, die Zahl der gebouncten Mails, die Zahl der Spambeschwerden und die Zahl der gesendeten Mails im selben Zeitraum und im selben Stream. Beides sind Quoten, Ereignisse geteilt durch Gelegenheiten, von Grund auf erklärt in Grundlagen: Verhältnisse und Quoten.
Postmark zieht für ein Konto zwei Linien: eine Bounce Quote unter 10 % aller gesendeten Mails und eine Beschwerdequote unter einer von 1.000 Mails, also 0,1 %. Ein Konto, das darüber rutscht, wird kontaktiert und pausiert, die Nachrichten warten in der Warteschlange, bis das Problem gelöst ist.27 Google bittet Absender, die in den Postmaster Tools gemeldete Spamquote unter 0,1 % zu halten und 0,3 % nie zu erreichen.26
Durchgerechnetes Beispiel: der erste Monat der Rösterei auf Postmark
Erfundene Zahlen für den erfundenen Shop, jeder Schritt gezeigt.
Transactional Stream:
- Bestellbestätigungen: 1.450 gesendet, 41 gebounct (30 hart, 11 weich), keine Beschwerde. Bounce Quote: 41 geteilt durch 1.450, mal 100, ergibt 2,8 %.
- Versandnachrichten: 1.420 gesendet, neun gebounct (alle weich, volle Postfächer), eine Beschwerde. Bounce Quote: 9 geteilt durch 1.420, mal 100, ergibt 0,6 %. Warum nur 1.420 bei 1.450 Bestellungen? Die 30 Adressen, die bei der Bestätigung hart gebounct sind, wurden deaktiviert, also hat Postmark nicht noch einmal an sie gesendet.
- Passwort Resets: 330 gesendet, 14 gebounct, keine Beschwerde. Bounce Quote: 14 geteilt durch 330, mal 100, ergibt 4,2 %. Alte Konten mit Adressen, die es nicht mehr gibt.
Transactional insgesamt: 3.200 gesendet, 64 gebounct, eine Beschwerde. Bounce Quote 64 geteilt durch 3.200, mal 100, ergibt 2,0 %. Beschwerdequote 1 geteilt durch 3.200, mal 100, ergibt 0,03 %. Zugestellt: 3.200 minus 64 sind 3.136.
Broadcast Stream:
- Newsletter: 2.400 gesendet, 120 gebounct (96 hart, 24 weich), fünf Beschwerden. Bounce Quote 120 geteilt durch 2.400, mal 100, ergibt 5,0 %. Beschwerdequote 5 geteilt durch 2.400, mal 100, ergibt 0,21 %. Zugestellt: 2.280.
Jeder Balken auf der waagerechten Achse ist eine Mailart. Die senkrechte Achse zeigt ihre Bounce Quote, gebounct geteilt durch gesendet für diese Art im Monat. Der Newsletter bounct am stärksten, mit 5,0 %, weil seine Liste mit 2.400 Adressen von Janas alter Plattform importiert wurde und teils vier Jahre alt war. Die Versandnachricht bounct am wenigsten, weil die Bestätigung davor die schlechten Adressen schon aussortiert hat.
Jetzt die Falle. Zähl alles zusammen: 5.600 gesendet, 184 gebounct, sechs Beschwerden. Bounce Quote 184 geteilt durch 5.600, mal 100, ergibt 3,3 %, bequem unter den 10 % von Postmark. Beschwerdequote 6 geteilt durch 5.600, mal 100, ergibt 0,1071 %, knapp über 0,1 %. Die Gesamtzahl versteckt, wo das Problem sitzt. Nach Stream getrennt liegen die Transaktionsmails mit 0,03 % gut, und der Newsletter ist mit 0,21 % derjenige über der Linie.
Getrennte Streams schützen die Bestätigungen bei den Mailanbietern, weil sie von anderen Adressen aus rausgehen. Das Konto schützen sie nicht. Die Schwellen von Postmark gelten für das Konto,27 also kann ein unsauberer Newsletter trotzdem das ganze Konto pausieren lassen, Quittungen inklusive. Janas Lösung kostet einen Nachmittag: Sie streicht die 96 hart gebouncten Adressen, fragt alle anderen, ob sie den Newsletter noch wollen, und behält die, die Ja sagen. Der Newsletter im zweiten Monat geht an 1.650 Leute, mit einer Beschwerde, 0,06 %. Weniger Empfänger, mehr Leser, und die Quittungen kommen weiter an.
Zwei Abkürzungen, die ich nicht nehmen würde
Die erste Fassung dieses Artikels hat zwei Dinge empfohlen, die ich hier korrigiere. Sie hat die Black Hole Adresse auf Development und Staging als Absender eingetragen. Das hält nichts auf. Das Black Hole funktioniert als Empfänger: Was dorthin geht, verschwindet. Die Absenderadresse sagt nur, von wem die Mail kommt. Postmark sendet von Adressen, die du verifiziert hast, einer einzelnen Sender Signature oder einer ganzen Domain,22 und eine Black Hole Domain, die Postmark gehört, kannst du nicht als deine verifizieren. Was die Kunden schützt, ist der Interceptor, weil er den Empfänger ändert.
Außerdem hat sie Development mit einem Token aus einer Umgebungsvariable an Postmark angebunden und dann in der Development Datei eine Prüfung auf Rails.env.production? gezeigt, die dort nie wahr sein kann. Lass Development überhaupt nicht mit Postmark reden. letter_opener und die Previews zeigen dir alles, und auf dem Laptop liegt kein Token, der durchsickern kann.
Bevor du live gehst
Die Liste, die ich bei einem neuen Shop durchgehe, eine Zeile pro Abschnitt dieses Leitfadens.
- Production und Staging Token in eigenen verschlüsselten Credentials Dateien, Schlüsseldateien nicht im Git.
- Ein Postmark Server pro Umgebung, Staging als Sandbox, außer jemand muss die Darstellung in einem echten Postfach sehen.
letter_openerin Development,:testim Test,:postmarkin Production, Staging erbt von Production.- Der Staging Interceptor schreibt den Empfänger um und leert Kopie und Blindkopie.
- Eine Preview für jede Mail, die ein Kunde bekommen kann, einmal von einem Menschen gelesen.
- Newsletter im Broadcast Stream, alles, was Solidus verschickt, im Standard Transactional Stream.
- Webhooks für Bounces und Spambeschwerden, hinter Basic Authentication, die eine Liste füllen, die jemand liest.
- DKIM verifiziert, ein eigener Return Path, DMARC einen Monat auf
p=none, dann verschärft. - Quoten pro Stream monatlich geprüft, gegen 10 % Bounces und 0,1 % Beschwerden.
Nichts davon ist raffiniert. Mails sollten der langweiligste Teil eines Shops sein, und langweilig braucht am Anfang ein bisschen Sorgfalt. Für die Testseite zeigt mein Artikel über TDD und BDD in Ruby on Rails, wie ich die Tests schreibe, die prüfen, ob jede dieser Mails richtig gebaut wird.
Footnotes
-
Solidus, Customizing emails, Solidus Guides, 2026, https://guides.solidus.io/customization/customizing-emails
-
Solidus, order_confirmation_mailer_subscriber.rb, solidusio/solidus auf GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/app/subscribers/spree/order_confirmation_mailer_subscriber.rb
-
Solidus, order_mailer.rb und base_mailer.rb, solidusio/solidus auf GitHub, 2026, https://github.com/solidusio/solidus/blob/main/core/app/mailers/spree/order_mailer.rb
-
Postmark, API overview: authentication, Postmark Developer Docs, 2026, https://postmarkapp.com/developer/api/overview
-
Ruby on Rails, Securing Rails Applications: Custom credentials, Rails Guides, 2026, https://guides.rubyonrails.org/security.html#custom-credentials
-
Ruby on Rails, credentials command usage, rails/rails auf GitHub, 2026, https://github.com/rails/rails/blob/main/railties/lib/rails/commands/credentials/USAGE
-
ActiveCampaign, postmark-rails README, GitHub, 2026, https://github.com/ActiveCampaign/postmark-rails
-
Postmark, Server sandbox mode, Postmark Developer Docs, 2026, https://postmarkapp.com/developer/user-guide/sandbox-mode/server-sandbox-mode
-
Ruby on Rails, ActionMailer::Base, Rails API documentation, 2026, https://api.rubyonrails.org/classes/ActionMailer/Base.html
-
Ryan Bates, letter_opener README, GitHub, 2026, https://github.com/ryanb/letter_opener
-
Ruby on Rails, Action Mailer Basics, Rails Guides, 2026, https://guides.rubyonrails.org/action_mailer_basics.html
-
Postmark, Best practices for testing your emails through Postmark, Postmark Support, 2026, https://postmarkapp.com/support/article/1213-best-practices-for-testing-your-emails-through-postmark
-
Postmark, Generate fake bounces, Postmark Developer Docs, 2026, https://postmarkapp.com/developer/user-guide/sandbox-mode/generate-fake-bounces
-
Postmark, Can I send bulk emails?, Postmark Support, 2026, https://postmarkapp.com/support/article/can-i-send-bulk-emails
-
Postmark, How to create and send through message streams, Postmark Support, 2026, https://postmarkapp.com/support/article/1207-how-to-create-and-send-through-message-streams
-
ActiveCampaign, postmark gem, lib/postmark/message_extensions/mail.rb, GitHub, 2026, https://github.com/ActiveCampaign/postmark-gem/blob/main/lib/postmark/message_extensions/mail.rb
-
Bundesministerium der Justiz, Gesetz gegen den unlauteren Wettbewerb (UWG), § 7 Unzumutbare Belästigungen, gesetze im internet, 2026, https://www.gesetze-im-internet.de/uwg_2004/__7.html
-
Postmark, What are bounces and spam complaints?, Postmark Support, 2026, https://postmarkapp.com/support/article/what-are-bounces-and-spam-complaints
-
Postmark, Spam complaint webhook, Postmark Developer Docs, 2026, https://postmarkapp.com/developer/webhooks/spam-complaint-webhook
-
Postmark, Bounce webhook, Postmark Developer Docs, 2026, https://postmarkapp.com/developer/webhooks/bounce-webhook
-
Postmark, Webhooks overview, Postmark Developer Docs, 2026, https://postmarkapp.com/developer/webhooks/webhooks-overview
-
Postmark, How do I set up DKIM for Postmark?, Postmark Support, 2026, https://postmarkapp.com/support/article/1091-how-do-i-set-up-dkim-for-postmark
-
Postmark, How do I set up SPF for Postmark?, Postmark Support, 2026, https://postmarkapp.com/support/article/1092-how-do-i-set-up-spf-for-postmark
-
Postmark, DMARC guide, Postmark, 2026, https://postmarkapp.com/guides/dmarc
-
Postmark, Adding a custom Return-Path domain, Postmark Support, 2026, https://postmarkapp.com/support/article/910-adding-a-custom-return-path-domain
-
Google, Email sender guidelines, Google Workspace Admin Help, 2026, https://support.google.com/a/answer/81126
-
Postmark, Does Postmark have a daily send limit?, Postmark Support, 2026, https://postmarkapp.com/support/article/1111-does-postmark-have-a-daily-send-limit