HTML-E-Mails mit Mailgun-Go versenden
In diesem Tutorial zeige ich Ihnen, wie Sie mit mailgun-go HTML-E-Mails mit eingebetteten Bildern versenden. Bevor wir uns dem Code widmen, definieren wir zunächst den Problembereich und wie wir Mailgun nutzen, um das Nutzererlebnis unserer Anwendung zu verbessern.
Wir stellen vor: Channel-stats
Channel-stats ist ein Slack-Bot zur Erfassung von Statistiken zu Nachrichten, die an einen Slack-Kanal gesendet wurden. Neben der Erfassung von Emojis und Links, die im Kanal geteilt werden, führt der Bot auch eine Stimmungsanalyse der Nachrichten durch und vergibt eine positive oder negative Punktzahl. Diese Punktzahl kann später von den Nutzern überprüft und als Prozentsatz der Gesamtnachrichten grafisch dargestellt werden.

Wir möchten diese Funktion um einen wöchentlichen E-Mail-Bericht zu den Kanalstatistiken für unsere Nutzer erweitern. Da unsere E-Mail grafisch dargestellte Daten enthält, wären reine Text-E-Mails ziemlich langweilig. Stattdessen möchten wir ansprechende HTML-E-Mails mit Grafiken an den Posteingang unserer Nutzer senden. Um dies zu erreichen, müssen wir etwas HTML mit Inline-CSS und Bildern erstellen, um es optisch ansprechend zu gestalten.
HTML-E-Mail
Über den Versand von HTML-Inhalten in E-Mails wurde bereits viel geschrieben. Hier sind jedoch einige wichtige Regeln, die Sie befolgen sollten:
- JA: Verwenden Sie Inline-CSS
- JA: Verwenden Sie HTML-Tabellen für das Layout
- JA: Verwenden Sie Bilder (vorzugsweise .png)
- JA: Verwenden Sie Inline-Bilder
- NEIN: Verwenden Sie kein HTML5
- NEIN: Verwenden Sie kein Animations-CSS
- NEIN: Verlinken Sie nicht auf ein externes Stylesheet
- NEIN: Verwenden Sie keine CSS-Stile im HEAD-Bereich
- NEIN: Verwenden Sie kein JavaScript
- NEIN: Verwenden Sie kein Flash
Es reicht oft nicht aus, die oben genannten Regeln zu befolgen, da es keine einheitlichen Standards dafür gibt, wie HTML in E-Mails gerendert wird. Wenn Sie sicherstellen möchten, dass Ihre E-Mails auf möglichst vielen Clients korrekt gerendert werden, bietet sich ein Dienst wie Litmus an, um Ihre E-Mail zu erstellen, in der Vorschau anzuzeigen und über verschiedene Clients hinweg zu testen. Für unsere Zwecke und da es sich bei Channel-stats um ein Open-Source-Projekt handelt, halte ich die Produktionskosten jedoch niedrig und verwende einige kostenlose Vorlagen von Mailgun (Ich habe dabei etwas Unterstützung aus unserem UX/UI-Designteam erhalten). Das Ergebnis sieht wie folgt aus:

Da wir nun unser HTML und CSS haben, müssen wir das CSS einbetten, damit die meisten E-Mail-Clients unsere E-Mail korrekt rendern. Dafür gibt es eine Reihe von Online-Tools, wir empfehlen jedoch Dialect Premailer für diesen Zweck.
Der Code
Da die E-Mail wöchentlich gesendet werden soll, verwenden wir eine Cron-Bibliothek , um eine Funktion zu erstellen, die jeden Sonntag um Mitternacht ausgeführt wird. Als Nächstes müssen wir die Bilder generieren, die in unserer E-Mail enthalten sein werden. Channel-stats verwendet bereits go-chart , um .png-Diagrammbilder für die UI zu rendern, sodass wir dies einfach für unsere Zwecke anpassen können.
Da wir bei der Auslieferung unseres finalen Projekts HTML- und CSS-Dateien nicht getrennt von unserem finalen kompilierten Golang-Binary bereitstellen möchten, verwendet Channel-stats das go-bindata -Projekt, um HTML und CSS in einem einzigen Channel-stats-Binary zu bündeln.
Werfen wir nun einen Blick auf den Render-Code.
func NewReporter(conf Config, list ChanLister, notify Mailer, store Storer) (Reporter, error) {rn r := Report{rn log: GetLogger().WithField("prefix", "reporter"),rn cron: cron.New(),rn mail: notify,rn store: store,rn conf: conf,rn list: list,rn}rn return &r, r.start()rn}rnrnfunc (r *Report) start() error {rn err := r.cron.AddFunc(r.conf.Report.Schedule, func() {rn timeRange := toTimeRange(r.conf.Report.ReportDuration.Duration)rn r.log.Debugf("Creating report for %s to %s", timeRange.Start, timeRange.End)rnrn for _, channel := range r.list.Channels() {rn // Skip channels the bot is not inrn if !channel.IsMember {rn continuern }rnrn html, err := r.genHtml("html/templates/email.tmpl", channel.Name)rn if err != nil {rn r.log.Errorf("during email generate: %s", err)rn returnrn }rnrn data := ReportData{rn Images: make(map[string][]byte),rn Html: html,rn }rnrn // Generate the images for the reportrn data.Images["most-active.png"] = r.genImage(RenderSum, timeRange, channel.Id, "messages")rn data.Images["top-links.png"] = r.genImage(RenderSum, timeRange, channel.Id, "link")rn data.Images["top-emoji.png"] = r.genImage(RenderSum, timeRange, channel.Id, "emoji")rn data.Images["most-negative.png"] = r.genImage(RenderPercentage, timeRange, channel.Id, "negative")rn data.Images["most-positive.png"] = r.genImage(RenderPercentage, timeRange, channel.Id, "positive")rnrn // Email the reportrn if err := r.mail.Report(channel.Name, data); err != nil {rn r.log.Errorf("while sending report: %s", err)rn }rn }rn })rn if err != nil {rn return errrn }rnrn r.cron.Start()rn return nilrn}
In der Methode Start() durchlaufen wir alle Kanäle, in denen der Bot Mitglied ist, und generieren für jeden Kanal einen Bericht. Anschließend rufen wir genHtml() auf, das unsere HTML-E-Mail als Vorlage namens templates/email.tmpl aus unserem kompilierten Asset-Speicher im HTML-Paket abruft. Dann führen wir die Vorlage durch die Standard-Engine HTML/template von Golang aus, um das finale HTML zu erzeugen. Als Nächstes ruft genImage() die Render-Funktion mit dem Zeitraum in Stunden und dem Zählertyp auf, den wir aus dem Datenspeicher abrufen möchten. Sobald ReportData vollständig ist, übergeben wir die Daten zur Zustellung an mail.Report().
Da wir nun unsere Bilder und das HTML haben, lassen Sie uns kurz über HTML-MIME und Bildkodierung sprechen. MIME ist das Format, in das E-Mail-Inhalte beim Versand über das SMTP-Protokoll kodiert werden. Dieses Format ermöglicht es E-Mail-Clients, HTML zu kodieren sowie Dateien und Bilder in einer E-Mail anzuhängen und abzurufen.
Damit unsere Bilder im HTML richtig angezeigt werden, müssen wir sie in den MIME-Standard kodieren. Dafür haben wir zwei Optionen: Wir könnten die Bilder als Anhang hinzufügen oder sie inline einbetten. Der RFC zur Content-Disposition besagt, dass inline anzeigt, dass die Entität dem Nutzer sofort angezeigt werden soll, während attachment bedeutet, dass dieser eine zusätzliche Aktion ausführen muss, um die Entität anzuzeigen. Da unsere Bilder dem Nutzer sofort über HTML angezeigt werden sollen, wählen wir „inline“.
An diesem Punkt könnten wir verschiedene MIME-Bibliotheken für Golang verwenden, um unsere Bilder einzubetten und den E-Mail-Inhalt im MIME-Format zu generieren. Mit Mailgun müssen wir das jedoch nicht. Mailgun generiert das MIME für uns und bietet Optionen zum Einbetten von Dateien und Bildern über die öffentliche API.
Da wir nun wissen, wie Bilder ins MIME eingebettet werden, müssen wir sie aus unserem HTML heraus referenzieren. Dazu verwenden wir das Präfix cid: in unseren -Tags. Wenn unser eingebettetes Bild also most-active.png heißt, sähe unser Bild-Tag folgendermaßen aus:
Da unser HTML nun bereit ist, schauen wir uns an, wie wir die E-Mail und die Bilder mit mailgun-go versenden.
func NewMailgunNotifier(conf Config) (Mailer, error) {rn return &Mailgun{rn mg: mailgun.NewMailgun(conf.Mailgun.Domain, conf.Mailgun.APIKey),rn log: GetLogger().WithField("prefix", "mailer"),rn conf: conf,rn }, nilrn}rnrn// Send a report to the designated email address (could be mailing list)rnfunc (m *Mailgun) Report(channelName string, data ReportData) error {rn if m.conf.Mailgun.ReportAddr == "" {rn m.log.Errorf("mailgun.enabled = true; however mailgun.report-address is empty; skipping..")rn return nilrn }rnrn // Create a subject for the reportrn subject := fmt.Sprintf("[channel-stats] Report for %s", channelName)rn // Create a message with no text bodyrn message := m.mg.NewMessage(m.conf.Mailgun.From, subject, "", m.conf.Mailgun.ReportAddr)rn // Send the HTML to mailgun for MIME encodingrn message.SetHtml(string(data.Html))rnrn for file, contents := range data.Images {rn message.AddReaderInline(file, ioutil.NopCloser(bytes.NewBuffer(contents)))rn }rnrn ctx, cancel := context.WithTimeout(context.Background(), m.conf.Mailgun.Timeout.Duration)rn defer cancel()rnrn _, id, err := m.mg.Send(ctx, message)rn if err != nil {rn return errrn }rn m.log.Infof("Sent report via mailgun (%s)", id)rn return nilrn}
Zuerst erstellen wir mit unserem Domainnamen und API-Schlüssel eine neue Instanz von Mailgun in NewMailgunNotifier(). Als Nächstes rufen wir in der Methode Report() die Funktion NewMessage() auf, um ein Objekt zu erstellen, dem wir unser HTML und unsere Bilder hinzufügen. Beachten Sie, dass das Argument text in NewMessage() ein leerer String ist. Obwohl es möglich ist, sowohl reinen Text als auch HTML in die MIME-Nachricht zu kodieren, stellen wir hier nur HTML bereit, da eingebettete Diagrammbilder für einen reinen Text-Client nutzlos wären. Als Nächstes rufen wir SetHtml() auf und hängen unsere eingebetteten Bilder über ein ReadCloser-Objekt an, das wir direkt aus unserem []byte buffer erstellen. Schließlich senden wir unsere erstellte Anfrage mit der Methode Send() an die Mailgun-API, um das MIME zu generieren und zuzustellen.
Fazit
Dieses Tutorial hat Ihnen hoffentlich einen Einblick gegeben, wie Sie mit mailgun-go und der Mailgun-API hochwertige HTML-E-Mails versenden. Sollten Sie Feedback haben oder Fehler in einem der Projekte finden, stehen der vollständige Code und die Bibliothek unten zur Verfügung.
Möchten Sie bei Mailgun arbeiten? Wir stellen ein. Und es sind mehrere Stellen im Entwicklungsteam offen. Aktuelle Stellenangebote hier ansehen.
Vorgestelltes Webinar – Prognosen und Vorsätze: Versand im Jahr 2019
Haben Sie dieses Webinar Anfang 2019 verpasst? Keine Sorge, wir haben es aufgezeichnet. Sehen Sie sich noch einmal an, wie Nick und Natalie über einige Entwicklungen im E-Mail-Bereich 2018 sprachen und was ihrer Meinung nach 2019 bevorstand. Hier trafen Technik und Marketing aufeinander, und Sie können es sich hier noch einmal ansehen.

