Der; Subst. mask.

Kontaktformulare versenden mit MailyGo bei Hugo (Static) Sites

12.02.2023
Lesezeit: ~10 Minuten (2120 Wörter)

Fragestellung

Wie Kontaktformulare datenschutzkonform per E-Mail übermitteln?

Problem

Statisch gehostete Seiten unterstützen in der Regel keine serverseitigen Sprachen, wie PHP, mit denen sich ein Mailer für Kontaktformulare realisieren ließe (like so).

Daher sind wir auf Dienste, wie z.B. formspree.io (eine gute Liste gibt es hier), angewiesen. Die meisten dieser Dienste sind allerdings nicht DSGVO-konform, da die Daten in unsichere Drittstaaten (looking at you, US of A) übertragen werden. Ich selbst nutze Netlify zum Hosten dieser Seite und habe auch in der Vergangenheit deren an sich ganz tollen und für meine Zwecke kostenlosen Dienst für Kontaktformulare eingesetzt. Netlify sitzt leider aber auch außerhalb der EU und hat daher mit den Formularübermittlungen besser nichts zu schaffen. Bliebe noch der in Österreich sitzende Dienst form.taxi, der für maximal 40 übermittelte Formulare auch kostenlos ist. Mir ist form.taxi sehr sympathisch und ich komme eventuell für Kundenprojekte darauf zurück.

Ich ziehe es jedoch vor, bei übermittelten Daten so wenige Mittelsmenschen wie möglich zu haben und da ich sowieso einen VServer bei Contabo angemietet habe, war die Überlegung, ob sich das auch darüber realisieren ließe.

Lösung

Stellt sich raus: ließe sich! Wie der Zufall so will, bin ich beim Recherchieren nach Lösungen auf MailyGo von Jan-Lukas Else gestoßen. Es handelt sich um eine Go-binary, die als Endpunkt für POST-Requests aus Formularen dient. Das Programm versendet übermittelte Daten per Mail, ohne dass die Daten noch irgendwo zwischengespeichert werden – perfekt für meine Anforderungen.

Umsetzung

Im folgenden beschreibe ich euch, wie ihr MailyGo installiert und startet. Außerdem beschreibe ich mein Setup, bei dem ich MailyGo mehrmals mit unterschiedlichen Benutzern per systemd-Unit starte und unter verschiedenen Domain-Unterverzeichnissen zur Verfügung stelle, damit ich mehrerer Formulare auf unterschiedlichen Seiten realisiert bekomme.

Inhalt

Voraussetzungen

  • eine freie (Sub-)Domain, die ihr kontrolliert und bei der ihr DNS-Einträge setzen könnt
  • einen Linux-VServer mit Root-Zugriff, bei mir eine Ubuntu 20.04.5 LTS Installation
  • Einen E-Mail-Account, idealerweise richtet ihr euch einen ein, der nur zum Versand der Kontaktformulare dient
  • eine Website mit Kontaktformular ;)

Installation von Golang

Die Installation über Apt hat bei mir nicht funktioniert, weil keine aktuelle Version von Golang zur Verfügung stand und MailyGo damit nicht lief, daher folgt die manuelle Installation – ich folge im Wesentlichen einem Tutorial von Digitalocean.

Zunächst loggen wir uns auf dem Server mittels SSH ein:

$ ssh benutzer@ip_deines_vservers

Wir wechseln ins Homeverzeichnis:

$ cd ~

und laden uns die neueste Version von Golang runter:

$ curl -OL https://go.dev/dl/go1.20.linux-amd64.tar.gz

und entpacken das Archiv in /usr/local:

$ sudo tar -C /usr/local -xvf go1.20.linux-amd64.tar.gz

Pfade setzen

Damit go gefunden werden kann, geben wir noch den Pfad der binary bekannt und fügen ihn dafür in unsere .profile-Datei hinzu:

sudo nano ~/.profile

Hier setzen wir folgende Zeile am Ende der Datei ein:

export PATH=$PATH:/usr/local/go/bin und schließen/speichern mit Ctrl+x, y.

Jetzt müssen wir die aktualisierte .profile-Datei noch neu einlesen mit:

$ source ~/.profile

Das war’s – mit $ go version können wir prüfen, ob alles geklappt hat.

Tada:

go version go1.20 linux/amd64

Installation von MailyGo

In meinem Setup nutze ich nicht die Originalversion von MailyGo, sondern einen etwas erweiterten Fork von Koichi Matsumoto.

Ich bevorzuge manuell installierte Pakete in /opt/ zu installieren, daher wechseln wir zunächst in dieses Verzeichnis:

$ cd /opt/

Jetzt klonen wir das Git-Verzeichnis mittels:

$ git clone https://codeberg.org/mzch/mailygo.git

und wechseln in das neue Verzeichnis:

$ cd mailygo/

Ein:

$ go build ./

kompiliert uns die eigentliche Application mailygo. Ich belasse sie in diesem Verzeichnis und start sie im Folgenden über den vollen Pfad /opt/mailygo/mailygo.

Erster Test

mailygo müsst ihr bei jedem Start environment variables mitgeben, derer da wären:

NameTypeDefault valueUsage
SMTP_USERrequired-The SMTP user
SMTP_PASSrequired-The SMTP password
SMTP_HOSTrequired-The SMTP host
SMTP_PORToptional587The SMTP port
USE_STARTTLSoptionaltrueSMTP connection with STARTTLS
SMTP_HELOoptionallocalhostSMTP HELO hostname
EMAIL_FROMrequired-The sender mail address
EMAIL_TOrequired-Default recipient
ALLOWED_TOrequired-All allowed recipients (separated by ,)
PORToptional8080The port on which the server should listen
HONEYPOTSoptional_t_emailHoneypot form fields (separated by ,)
GOOGLE_API_KEYoptional-Google API Key for the Google Safe Browsing API
SPAMLISToptionalgambling,casinoList of spam words
DENYLISToptionalsubmitList of fields names to deny
TOKENoptional-A token to identify the origin of the submission
MESSAGE_HEADERoptional-Text to appear at the beginning of the email message, before the list of fields
MESSAGE_FOOTERoptional-Text to appear at the end of the email message, after the list of fields
MESSAGE_SUBMITTERoptionalfalseIf set to true and the form submitter provide an email address, a copy of the message is sent to him
MESSAGE_SUBMITTER_HEADERoptional-Text to appear at the beginning of the email message sent to submitter, before the list of fields
MESSAGE_SUBMITTER_FOOTERoptional-Text to appear at the end of the email message sent to submitter, after the list of fields

Die komplette Readme-Datei findet ihr hier.

Probieren wir das gleich mal aus. Da wir uns aktuell im richtigen Verzeichnis /opt/mailygo/ befinden, können wir die binary mit folgendem Befehl starten (setzt dabei natürlich eure Daten für euren verwendeten Mail-Account ein):

$ SMTP_HOST=mail.host.com PORT=587 SMTP_PASS=yourpassword SMTP_USER=user@host.com USE_STARTTLS=true EMAIL_FROM=user@host.com EMAIL_TO=otheruser@otherhost.com ALLOWED_TO=otheruser@otherhost.com PORT=8000 ./mailygo

Obiger Befehl startet mailygo - der Dienst lauscht wie unter PORT angegeben auf Port 8000. Sofern ihr keine Firewall einsetzt (living on the edge!), ist der Dienst jetzt schon unter der öffentlichen IP eures Servers erreichbar.

Falls ihr doch eine Firewall laufen habt, was ich hoffe, müsst ihr den Port zunächst öffnen. Ich setze ufw ein, da geht das so:

$ sudo ufw allow 8000

Prüfen wir das mal. Mit:

$ curl icanhazip.com erfahren wir unsere Public IP-Adresse. Wenn wir diese im Browser, gefolgt von :8000, einsetzen, sollte er uns ein lakonisches MailyGo works! zurückmelden.

Außerdem könnt ihr prüfen, ob mailygo ordentlich lauscht:

lsof -i TCP| fgrep mailygo

Was etwas wie

mailygo   762851           euerUser    3u  IPv6 3049673      0t0  TCP *:8000 (LISTEN)

zurückwerfen sollte. Perfekt!

Jetzt löschen wir wieder die Portfreigabe der Firewall, da wir einen Reverse Proxy (nginx) vor mailygo setzen, der den internen Port unter unserer Domain weiterleitet:

$ sudo ufw delete allow 8000

Reverse Proxy mit nginx

Damit wir in unserem Kontaktformular nicht die IP:Port-Kombination unserer mailygo-Installation angeben müssen und damit in den Genuss von SSL für unsere POST-Operation kommen, müssen wir einen virtuellen Proxy vor den Dienst setzen. Ich gehe hier davon aus, dass ihr nginx bereits laufen habt – sonst würde das den Rahmen sprengen.

Ich habe mich dafür entschieden, jeweils Unterverzeichnisse meiner Domain für die unterschiedlichen mailygo-Dienste zu verwenden. So erspare ich mir, für jede neue Website eine eigene Subdomain anlegen zu müssen und kann stattdessen einen beliebigen Unterordner meiner Domain setzen. mailygo läuft dabei unter verschiedenen Benutzern und öffnet pro Instanz einen anderen Port, den ich dann dort zur Verfügung stelle. Das klingt jetzt vielleicht komplizierter, als es ist. Und das war auch genau das Problem bei meinen Google-Suchen. Ihr habt dafür jetzt diesen Post, falls ihr ein ähnliches Problem habt ;)

Ihr erstellt euch also zunächst bei eurem Domain-Anbieter im DNS-Panel eine Subdomain (A-Record) für euren Dienst, der auf euren Server zeigt also etwas wie mailygo.eure-domain.de.

Dann wechselt ihr auf eurem Sever in das Verzeichnis /etc/nginx/sites-available/ und erzeugt gleichzeitig eine neue nginx-Konfiguration:

$ cd /etc/nginx/sites-available/ && nano mailygo

Im neuen Editor-Fenster fügt ihr folgendes für eure neue Domain mailygo.eure-domain.de ein:

server {
    server_name mailygo.eure-domain.de;
        location /eurewebsite {
                proxy_pass http://localhost:8000;
                proxy_set_header X-Forwarded-For $remote_addr;
    }
}

eurewebsite kann beliebig heißen, ich nehme dafür eine Kurzbezeichnung meiner Websites, hier wäre das vgnsm. Achtet drauf, dass ihr den richtigen Port in Zeile 4 eingebt (wir starten mailygo ja mit Port 8000).

Jetzt erzeugen wir noch einen Symlink, damit die neue Konfig auch von nginx ausgeführt wird:

ln -s /etc/nginx/sites-available/mailygo /etc/nginx/sites-enabled/mailygo

Startet den Service neu mit:

$ sudo systemctl restart nginx

SSL mit Certbot

Ich empfehle ja immer den Einsatz von letsencrypt für euer SSL. Wenn ihr das benutzen wollt, könnt ihr euch ein Zertifikat holen und das installieren:

$ sudo certbot --nginx

Danach sieht eure nginx-Konfig so aus:

server {
    server_name mailygo.eure-domain.de;
        location /eurewebsite {
                proxy_pass http://localhost:8000;
                proxy_set_header X-Forwarded-For $remote_addr;
    }


    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/mailygo.eure-domain.de/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/mailygo.eure-domain.de/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}

server {

    if ($host = mailygo.eure-domain.de) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    server_name mailygo.eure-domain.de;
    listen 80;
    return 404; # managed by Certbot

}

Das war’s zunächst für nginx. Startet den Service nochmals neu mit:

$ sudo systemctl restart nginx

Starten von mailygo mittels systemd

Damit wir mailygo automatisiert laufen lassen können, macht es Sinn, diese Aufgabe systemd zu übergeben. Aber zunächst erzeugen wir uns einen neuen Benutzer, unter dem mailygo für diese Webiste laufen soll:

$ sudo useradd eurewebsite

Ich wähle als Benutzername wiederum den Kurznamen für eure Website, den wir weiter oben in den nginx-Konfig schon verwendet haben, aber ihr könnt auch einfach „Thomas“ nehmen :D

Jetzt erzeugen wir uns eine neue systemd-Unit.

$ sudo nano /etc/systemd/system/mailygo.eure-domain.de

und fügen dort folgendes ein:

[Unit]
Description = runs mailygo for mailygo.eure-domain.de

[Service]
Type = simple
User = eurewebsite
Group = eurewebsite
ExecStart = /opt/mailygo/mailygo
Restart=always
StandardOutput=syslog
EnvironmentFile=/opt/mailygo/env/eurewebsite

[Install]
WantedBy=multi-user.target

Bei User, Group und EnvironmentFile müsst ihr noch eure passenden Namen einsetzen. Das EnvironmentFile (env) dient uns dazu, die Environment Variables übersichtlich beim Startbefehl mitgeben zu können. Auch diese Datei können wir pro laufenden Dienst hinterlegen und entsprechend anpassen.

Environment Variables über env-Datei

Zunächst wechseln wir wieder ins Verzeichnis der mailygo-Installation und erzeugen dort einen neuen Ordner env:

$ cd /opt/mailygo/ && mkdir ./env

Im neuen Oder env erstellen wir mit nano eine neue Datei eurewebsite mit folgendem Inhalt:

SMTP_HOST=euer_Mailserver
USE_STARTTLS=true (oder false)
PORT=587 (euer Port)
SMTP_USER=Benutzername
SMTP_PASS=2Passwort
EMAIL_FROM=absender@mailserver.com
EMAIL_TO=empfänger@mailserver.com
ALLOWED_TO=empfänger@mailserver.com
PORT=8000 (Port, auf dem mailygo lauschen soll)
MESSAGE_HEADER="Ein Besucher hat folgende Nachricht hinterlassen"
MESSAGE_FOOTER="Diese Mail wurde mittels mailygo gesandt"
TOKEN=qdhiofh4t5d2lkehwc323 (Beliebige Zeichenkette)
SPAMLIST=gambling,casino,lottery
HONEYPOTS=_t_email

Schaut euch die Readme zur Erläuterung an.

Die Unit ist ready, sobald wir folgende beiden Befehle ausgeführt haben:

$ sudo systemctl daemon-reload && sudo systemctl restart mailygo.eure-domain.de.service

Wenn ihr nun auf mailygo.eure-domain.de/eurewebsite surft, sollte wieder das bekannte lakonische „MailyGo works!“ zurückkommen. Super!

Das Kontaktformular

Das Repository enthält eine Beispiel-Datei für ein Kontaktformular, das ihr beliebig anpassen könnt:

<form action="//localhost:8080" method="post">
  <!-- <input type="hidden" name="_to" value="test@example.com" /> -->
  <input type="hidden" name="_formName" value="Test Form" />
  <input type="hidden" name="_redirectTo" value="https://example.com" />
  <input type="hidden" name="_token" value="a3B2c1" />
  <label for="TEmail" style="display: none;">Test</label>
  <input id="TEmail" type="email" name="_t_email" style="display: none;" />
  <label for="Email">Email</label>
  <br />
  <input id="Email" type="email" name="_replyTo" required />
  <br />
  <label for="Name">Name</label>
  <br />
  <input id="Name" type="text" name="_name" required />
  <br />
  <label for="Subject">Subject</label>
  <br />
  <input id="Subject" type="text" name="_subject" required />
  <br />
  <label for="Message">Message</label>
  <br />
  <textarea id="Message" name="_message"></textarea>
  <br />
  <label for="Website">Website</label>
  <br />
  <input id="Website" type="text" name="Website" required />
  <br />
  <input type="submit" value="Send" />
</form>

Ihr müsst nur die erste Zeile wie folgt anpassen:

<form action="https://mailygo.eure-domain.de/eurewebsite" method="post">

Die zweite Zeile <input type="hidden" name="_to" value="test@example.com" /> habe ich bei meinen Formularen rausgeschmissen, damit meine Mail-Adresse nicht von Spambots gefressen wird. Wir haben den Empfänger ja in unserem env-File bereits hinterlegt:

EMAIL_TO=empfänger@mailserver.com
ALLOWED_TO=empfänger@mailserver.com

Das war’s – mailygo läuft nun mit env-File unter dem User „eurewebsite“ und der Port 8000 wird mittels nginx unter einem Unterverzeichnis eurer Subdomain vermittelt.

Mehrere Prozesse für unterschiedliche Domains

Wir haben jetzt die Voraussetzungen geschaffen, mailygo für verschiedene Domains/Websites zur Verfügung zu stellen.

Um eine zweite Instanz von mailygo laufen zu lassen, müssen wir nur einen weiteren Benutzer erzeugen (siehe „Starten von mailygo mittels systemd“) und für diesen eine neue Unit anlegen mit den entsprechenden Einträgen für User, Group und EnvironmentFile.

Der neue env-File muss einen anderen Port enthalten, zum Beispiel 8001, sonst kann mailygo nicht noch einmal gestartet werden, da der Port schon belegt ist.

Anschließend wechseln wir wieder in unserer nginx-Konfig und fügen dort einen neuen Eintrag für das neue Unterverzeichnis eureanderewebsite (Zeilen 8 und 10) ein:

server {
    server_name mailygo.eure-domain.de;
        location /eurewebsite {
                proxy_pass http://localhost:8000;
                proxy_set_header X-Forwarded-For $remote_addr;
    }
<!-- zweiter Eintrag für weiteren mailygo-Prozess! -->
        location /eureanderewebsite {
<!-- neuer Port 8001! -->
                proxy_pass http://localhost:8001;
                proxy_set_header X-Forwarded-For $remote_addr;
    }


    listen 443 ssl; # managed by Certbot
    ssl_certificate /etc/letsencrypt/live/mailygo.eure-domain.de/fullchain.pem; # managed by Certbot
    ssl_certificate_key /etc/letsencrypt/live/mailygo.eure-domain.de/privkey.pem; # managed by Certbot
    include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}

server {

    if ($host = mailygo.eure-domain.de) {
        return 301 https://$host$request_uri;
    } # managed by Certbot


    server_name mailygo.eure-domain.de;
    listen 80;
    return 404; # managed by Certbot

}

In eurem Formular auf der anderen Website ändert ihr wieder die Zeile:

<form action="//localhost:8080" method="post">

zu

<form action="https://mailygo.eure-domain.de/eureanderewebsite" method="post">

und voilà.