Auf meinem Raspberry laufen einige Skripte, die meine Systeme überwachen und bei bestimmten Events eine Mail verschicken sollen. In meinem Blog-Post Postfix Konfiguration für CLI Mail habe ich beschrieben, wie ich dazu Postfix als lokalen Mailserver installiert und konfiguriert habe. Hier möchte ich eine einfachere Möglichkeit vorstellen, wenn es nur darum geht, von einem Linux- oder Mac-System aus der Kommandozeile E‑Mails zu verschicken.
Wie funktioniert E‑Mail eigentlich?
Grundsätzlich ist für das Empfangen und Senden einer E‑Mail ein Mailserver notwendig, der natürlich aus dem Internet erreichbar und als Mailserver bekannt sein muss. In den meisten Fällen wird der heimische Rechner nicht direkt als Mailserver fungieren, sondern es wird ein E‑Mail-Programm wie Apple Mail oder Thunderbird genutzt, um Mails vom Server zu laden bzw. eine Mail über den Server zu senden.
Die Kommunikation zwischen dem Programm und dem Server geschieht über zwei Protokolle: SMTP zum Senden und IMAP zum Lesen und Verwalten der Mails auf dem Server. Mit IMAP wird der Inhalt des Servers sozusagen gespiegelt, ähnlich wie bei der Integration von Cloud-Speicher wie Dropbox oder iCloud. Die Daten werden auf dem Computer synchronisiert: Wird eine Mail in Thunderbird gelöscht, wird sie auch auf dem Server gelöscht. So kann ein Mail-Account mit verschiedenen Computern verbunden sein, und alle sehen die gleiche Ordnerstruktur und die entsprechenden Mails. Aber um diesen Teil der E‑Mail-Experience geht es hier nicht.
Für das Senden einer E‑Mail wird das SMTP-Protokoll verwendet. Wenn eine Mail im E‑Mail-Programm erstellt und der Senden-Button gedrückt wird, wird aus der Eingabe die Mail nach bestimmten Regeln aufgebaut und dann via SMTP an den jeweiligen Mailserver übertragen. Dieser sendet die E‑Mail dann an den Server des Empfängers weiter, der sie verarbeitet und im Mail-Account des Empfängers ablegt.
Header, Body und MIME: Der Aufbau einer E‑Mai
Eine E‑Mail besteht aus zwei Teilen: dem Header und dem Body, getrennt durch eine Leerzeile.
Im Header stehen die Pflichtfelder From (Absender) und To (Empfänger). Das Datum (Date) ist ebenfalls Pflicht und sollte vom sendenden Client gesetzt werden. Fehlt es, ergänzt es der Mailserver als Fallback. Optional sind der Betreff (Subject), Kopie- und Blindkopie-Empfänger (CC und BCC) sowie Reply-To, falls Antworten an eine andere Adresse gehen sollen. Die Message-ID sollte ebenfalls gesetzt werden, da Mailprogramme sie nutzen, um Mails einer Konversation zuzuordnen.
Bei Mails mit Anhängen oder gemischtem Inhalt muss im Header außerdem Content-Type mit einer boundary definiert werden, damit der empfangende Client den Mail-Body korrekt zusammensetzen kann. In diesem Fall muss auch MIME-Version: 1.0 im Header stehen.
Die MIME-Struktur bei HTML-Mails
Bei einer reinen Text-Mail ist der Body simpel. Sobald aber HTML, Bilder oder Anhänge ins Spiel kommen, wird die Mail in mehrere Teile aufgeteilt, die wiederum Teile enthalten können. Das sieht komplizierter aus, als es ist.
Hier ein Beispiel für eine Mail mit HTML-Part, Klartext-Fallback und einem eingebetteten Bild als Signatur:
From: sender@beispiel.de
To: empfaenger@beispiel.de
Subject: =?utf-8?Q?Sch=C3=B6ner_Betreff_mit_Uml=C3=A4uten?=
Date: Tue, 17 Mar 2026 10:00:00 +0100
Message-ID: <unique-id@beispiel.de>
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="==GRENZE_1=="
--==GRENZE_1==
Content-Type: multipart/related; boundary="==GRENZE_2=="
--==GRENZE_2==
Content-Type: multipart/alternative; boundary="==GRENZE_3=="
--==GRENZE_3==
Content-Type: text/plain; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
Hallo! Sch=C3=B6ne Gr=C3=BC=C3=9Fe!
--==GRENZE_3==
Content-Type: text/html; charset="utf-8"
Content-Transfer-Encoding: quoted-printable
<html>
<body>
<p>Hallo! Sch=C3=B6ne Gr=C3=BC=C3=9Fe!</p>
<img src="cid:signatur.png">
</body>
</html>
--==GRENZE_3==--
--==GRENZE_2==
Content-Type: image/png
Content-Transfer-Encoding: base64
Content-ID: <signatur.png>
Content-Disposition: inline
iVBORw0KGgoAAAANSUhEUgAA...
--==GRENZE_2==--
--==GRENZE_1==--
Die verschachtelte Struktur macht auf den ersten Blick etwas Eindruck, folgt aber einer klaren Logik: Der äußere Container multipart/mixed ist der Rahmen für die gesamte Mail und wäre der richtige Ort für echte Dateianhänge. Darin liegt multipart/related, das HTML und das eingebettete Bild zusammenhält, das im HTML über cid:signatur.png referenziert wird und daher kein eigenständiger Anhang ist. Innerhalb von related wiederum bietet multipart/alternative dem Mailclient die Wahl: Er rendert entweder den HTML-Part oder, falls er kein HTML unterstützt, den Klartext-Part.

Zeichenkodierung: Warum quoted-printable?
Das SMTP-Protokoll wurde zu einer Zeit definiert, als 7‑Bit-ASCII der zuverlässige Standard für die Übertragung war. In diesen 127 Zeichen sind Umlaute und andere Sonderzeichen nicht definiert. Ein ö wird in UTF‑8 als zwei Bytes kodiert, 0xC3 und 0xB6, beides Werte über 127. Alte Relay-Server konnten solche Bytes beschädigen oder abschneiden.
quoted-printable löst das, indem alle Nicht-ASCII-Zeichen in eine sichere ASCII-Darstellung umgewandelt werden. Aus öwird =C3=B6, aus ü wird =C3=BC. Der empfangende Server transportiert diese kodierten Bytes unverändert, und erst der Mailclient dekodiert sie zurück und interpretiert sie gemäß der charset=utf-8-Angabe im Content-Type.
Im Header funktioniert das etwas anders: Dort gibt es kein charset-Feld, stattdessen wird das Format RFC 2047 Encoded Word verwendet:
Subject: =?utf-8?Q?Sch=C3=B6ner_Betreff_mit_Uml=C3=A4uten?=
Das Format ist dann immer:
=?Zeichensatz?Kodierung?kodierter Text?=
Dabei steht das Q für Quoted-Printable und B für Base64. Der Mailclient dekodiert das automatisch und zeigt „Schöner Betreff mit Umlauten” an.
Moderne Mailserver unterstützen zwar oft die SMTP-Erweiterung SMTPUTF8 (RFC 6531), die rohe UTF-8-Bytes im Transport erlaubt, aber es ist nicht sicher, welche Server eine Mail auf ihrem Weg passiert. Wer sichergehen will, kodiert korrekt.
msmtp installieren und konfigurieren
msmtp ist ein schlanker SMTP-Client. Er verschickt E‑Mails über den SMTP-Server deines Providers, ohne selbst ein Mailserver zu sein. Er nimmt die fertige Nachricht von der Standardeingabe entgegen und leitet sie weiter. Das heißt: Die Nachricht muss bereits korrekt aufgebaut sein, inklusive Header, Leerzeile und Inhalt. msmtp baut daraus keine HTML-Mail, keine Anhänge und keine Struktur, es verschickt nur, was du ihm gibst. Und da eben kein Mailprogramm die Kodierung übernimmt, muss das, wie im vorherigen Abschnitt beschrieben, vorher erledigt sein.
Hier ein ganz einfaches Beispiel, mit dem du von der Kommandozeile einen Text verschicken kannst:
echo -e "Subject: Test\n\nHallo Welt" | msmtp test@example.com
Wichtig ist dabei, dass echo mit der Option -e aufgerufen wird, da zwischen Subject: Test und dem Inhalt der Mail eine Leerzeile sein muss. Die Option sorgt dafür, dass \n als Zeilenumbruch und nicht als Text übertragen wird.
Installation
Auf einem Linux-System wie Ubuntu gibt es zwei relevante Pakete:
msmtp: der eigentliche SMTP-Clientmsmtp-mta: ein Kompatibilitäts-Wrapper, dermsmtpals Ersatz für klassische MTAs wie sendmail oder Postfix einträgt
Für das einfache Versenden über die Kommandozeile reicht das erste Paket. Das zweite ist nur dann sinnvoll, wenn du andere Kommandozeilen-Programme wie mail zum Versenden nutzen möchtest und noch keinen MTA wie sendmail oder Postfix konfiguriert hast. Falls du einen Desktop-Mail-Client installiert hast, kommuniziert der normalerweise direkt mit deinem Mailserver, daher hat diese Installation keinen Einfluss auf Senden und Empfangen von Mails mit dem Mail-Client.
Für die Installation gibst du folgende Befehle ein:
sudo apt update sudo apt install msmtp
Wenn während der Installation vorgeschlagen wird, msmtp-mta zu installieren, einfach nicht zustimmen, wodurch nichts an bestehenden Konfigurationen verändert wird.
Nach dem Download wirst du wahrscheinlich gefragt, ob du msmtp in dein AppArmor-Profil aufnehmen möchtest. Dabei handelt es sich um einen Sicherheitsmechanismus in Ubuntu. In meiner Test-Installation habe ich die Option bestätigt und bisher keine Probleme damit gehabt.
Unter macOS kann msmtp mit Homebrew oder einem anderen Paketmanager installiert werden:
brew install msmtp
Konfiguration
Nun muss noch eine Konfigurationsdatei angelegt werden. Du hast die Wahl zwischen einer benutzerspezifischen Datei unter ~/.msmtprc oder einer globalen unter /etc/msmtprc. Bei einer globalen Konfiguration können alle Benutzer des Systems, inklusive root, Cronjobs und Systemdienste, msmtp mit den dort hinterlegten Mail-Zugängen nutzen, ohne eine eigene Konfiguration zu benötigen.
In meinem Setup ist die globale Variante sinnvoll, weil ich der einzige interaktive Benutzer bin und so auch Skripte, die als root laufen, problemlos Mails verschicken können. Wenn mehrere Benutzer jeweils ihren eigenen Mailaccount nutzen sollen, ist die benutzerspezifische ~/.msmtprc die bessere Wahl. Auf dem Mac ist das generell empfehlenswert, da das mit Homebrew installierte msmtp die systemweite Konfigurationsdatei an einem versionsspezifischen Pfad erwartet, der bei jedem Update von msmtp wechselt. Die benutzerspezifische ~/.msmtprc funktioniert dagegen auf macOS und Linux gleich und bleibt von Updates unberührt.
defaults auth on tls on tls_starttls on tls_trust_file /etc/ssl/certs/ca-certificates.crt # Auf macOS werden die Zertifikate aus der Keychain geladen, # deshalb muss die Zeile mit tls_trust_file gelöscht oder # auskommentiert werden. # Auf Ubuntu ist syslog meist sinnvoller als logfile syslog LOG_MAIL # Auf macOS kann das logfile direkt geschrieben werden # logfile /Users/MeinUser/Library/Logs/msmtp.log # Globales logfile, kann wegen der Schreibrechte Probleme machen # logfile /var/log/msmtp.log # User logfile, einfache Lösung wenn die Aufrufe von msmtp immer # von dem User ausgehen. # logfile ~/.msmtp.log account default host smtp.deinprovider.de port 587 from deinuser@domain.de user deinuser@domain.de password deinpasswort
Du kannst mehrere account-Einträge in der Konfiguration anlegen, um z.B. Mails über verschiedene Absenderadressen zu verschicken. Einer muss dabei den Namen default tragen, der dann verwendet wird, wenn msmtp ohne die explizite Angabe eines Accounts aufgerufen wird. Dazu weiter unten mehr.
Wenn du die Datei angelegt und gespeichert hast, kannst du Mails mit msmtp versenden.
Erste Tests auf der Kommandozeile
Nun kannst du die erste Testmail verschicken:
echo -e "Subject: Test\n\nHallo Welt" | msmtp test@example.com --debug
Eigentlich wird damit keine vollständige E‑Mail nach Standard erzeugt, da außer dem Subject alle anderen Header-Felder fehlen. msmtp erstellt aus der E‑Mail-Adresse im Argument und dem from-Eintrag in der msmtprc einen SMTP-Envelope, in den das Ergebnis von echo -e eingepackt und verschickt wird. Die meisten modernen Mailserver, die eine solche Mail empfangen, ergänzen die fehlenden Header-Felder wie From, To, Date und Message-ID, sodass in der Mailbox eine vollständige Mail landet, allerdings sollte man sich nicht darauf verlassen.
Ein wichtiger Aspekt fällt dabei vielleicht nicht sofort auf. Die Option -e stellt sicher, dass \n nicht als Text, sondern als echter Zeilenumbruch übertragen wird. Wichtig ist außerdem, dass der Header durch eine Leerzeile vom Body getrennt sein muss. Daher stehen an dieser Stelle zwei \n hintereinander: Das erste beendet die letzte Header-Zeile, das zweite erzeugt die erforderliche Leerzeile.
Aus diesem Grund ist der folgende Aufruf die bessere Wahl. Alle wichtigen Header-Felder werden vollständig aufgebaut, bevor die fertige Mail an msmtp übergeben wird:
printf '%s\n' \ "From: Server <server@example.com>" \ "To: Leif <leif@example.org>" \ "Subject: Test via msmtp" \ "Date: $(LC_ALL=C date -R)" \ "MIME-Version: 1.0" \ "Content-Type: text/plain; charset=UTF-8" \ "" \ "Hallo," \ "das ist ein Test." \ | msmtp test@example.com
Falls du dich über das \ am Ende jeder Zeile wunderst: Es verhindert, dass das Terminal jede Zeile als eigenes Kommando interpretiert. Eigentlich müsste der gesamte Aufruf in einer einzigen Zeile stehen, was aber schnell unlesbar wird. Auf der anderen Seite muss sichergestellt sein, dass die einzelnen Header-Felder bei der Übertragung durch Zeilenumbrüche getrennt sind. Das übernimmt der Formatstring '%s\n' des printf-Befehls, der besagt, dass jeder Text zwischen "" mit einem Zeilenumbruch abgeschlossen wird.
Skript zum Senden von Systeminfos
Nachdem getestet wurde, dass das Versenden einer Mail von der Kommandozeile funktioniert, möchte ich hier ein Skript als Blaupause vorstellen, mit dem einige aktuelle Systemparameter als Mail verschickt werden.
Es wird eine Mail erstellt, in der im Header als Content-Type multipart/alternative definiert ist. Das bedeutet, dass der äußere Container zwei oder mehr Teile beinhaltet. In diesem Fall einen HTML-Teil, der die Daten als Tabelle darstellt, und einen reinen Text-Teil als Fallback für Mailclients ohne HTML-Unterstützung.
Das Skript funktioniert sowohl unter macOS als auch unter Linux.
Es ist in drei Teile aufgebaut. Im ersten Teil werden Variablen mit Werten gefüllt, die für den Aufbau der Mail notwendig sind. Das sind zum einen die Header-Daten, wobei der Betreff aus dem Text „System-Report” und dem aktuellen Datum zusammengesetzt wird. Auch die Variable für die Boundary wird aus dem Text ALT_ und den Sekunden zusammengesetzt, die seit dem 01.01.1970 vergangen sind. Das ist das Datum, an dem die Unix-Zeitrechnung beginnt. Mit diesem Trick wird sichergestellt, dass der Boundary-Name eindeutig ist, damit keine Kollisionen auftreten, wenn z.B. eine Mail in einer anderen Mail eingebettet ist.
Im zweiten Teil werden die Systemdaten in Variablen geschrieben. Mit HOSTNAME=$(hostname) wird der Befehl hostnameausgeführt und sein Ergebnis in der Variable HOSTNAME gespeichert. Die Schreibweise $(...) ist dabei das Muster: Was auch immer zwischen den Klammern steht, wird als Befehl ausgeführt und das Ergebnis direkt verwendet. Das gleiche Muster findet sich im Skript noch öfter, zum Beispiel bei $(date) oder $(uname). So kann auch das Ergebnis eines komplexen Aufrufs an eine Variable übergeben werden, wie:
DISK=$(df -h / | awk 'NR==2 {print $3 " von " $2 " belegt (" $5 ")"}')
Hier wird der Befehl df -h / aufgerufen, der den Speicherplatz des Wurzelverzeichnisses / anzeigt. Das -h sorgt dafür, dass das Ergebnis „human readable” ist, also die Größen in lesbaren Einheiten wie GB oder MB ausgegeben werden statt in Bytes. Die Ausgabe sieht dann ungefähr so aus:
Filesystem Size Used Avail Use% Mounted on /dev/sda1 30G 8.2G 22G 28% /
Das | leitet das Ergebnis an awk weiter, ein sehr mächtiges Werkzeug, das Textmuster erkennen und verarbeiten kann. In diesem Fall sorgt NR==2 dafür, dass nur die zweite Zeile verarbeitet wird, also die, in der die Daten stehen. $3, $2 und $5adressieren die Werte in den jeweiligen Spalten, sodass mit:
{print $3 " von " $2 " belegt (" $5 ")"}
der Text 8.2G von 30G belegt (28%) erzeugt und in die Variable DISK geschrieben wird.
Ähnlich verhält es sich mit den anderen Systemparametern. Das Skript unterscheidet dabei, ob es unter Linux oder macOS läuft, da RAM und CPU-Informationen auf beiden Systemen unterschiedlich abgefragt werden.
Im dritten Teil wird der Inhalt der Mail definiert. Der reine Textteil wird in die Variable PLAIN geschrieben, der HTML-Part in die Variable HTML. Anschließend wird die Mail zusammengebaut, wobei für jeden Teil, also Header, Plaintext und HTML, ein eigenes printf-Kommando verwendet wird.
Damit der Plaintext und der HTML-Teil korrekt kodiert werden, wird ein Python-Einzeiler genutzt, der sowohl unter Linux als auch unter macOS funktioniert:
printf '%s\n' "$PLAIN" | python3 -c " import sys, quopri sys.stdout.buffer.write(quopri.encodestring(sys.stdin.buffer.read())) "
Mit den printf-Kommandos wird die Mail zusammengebaut und am Ende an msmtp weitergeleitet, das sie an $TOverschickt.
#!/bin/bash
TO="empfaenger@example.com"
FROM="sender@example.com"
SUBJECT="System-Report $(date '+%d.%m.%Y')"
BOUNDARY_ALT="==ALT_$(date +%s)=="
# Systemdaten
HOSTNAME=$(hostname)
OS=$(uname)
# Speicherplatz (funktioniert auf beiden Systemen gleich)
DISK=$(df -h / | awk 'NR==2 {print $3 " von " $2 " belegt (" $5 ")"}')
# Uptime (funktioniert auf beiden Systemen)
UPTIME=$(uptime | sed 's/.*up //' | sed 's/,.*//')
# RAM und CPU je nach Betriebssystem
if [ "$OS" = "Darwin" ]; then
RAM_TOTAL=$(sysctl -n hw.memsize | awk '{printf "%.0f MB", $1/1024/1024}')
RAM_USED=$(vm_stat | awk '
/Pages active/ {active=$3}
/Pages wired/ {wired=$4}
END {printf "%.0f MB", (active+wired)*4096/1024/1024}')
RAM="${RAM_USED} belegt von ${RAM_TOTAL}"
CPU_INFO=$(sysctl -n machdep.cpu.brand_string)
else
RAM=$(free -m | awk 'NR==2 {printf "%d MB von %d MB belegt", $3, $2}')
CPU_INFO=$(grep "model name" /proc/cpuinfo | head -1 | cut -d: -f2 | xargs)
fi
# Plaintext-Part
PLAIN="System-Report: $HOSTNAME
$(date)
Betriebssystem : $OS
CPU : $CPU_INFO
Speicher : $DISK
RAM : $RAM
Uptime : $UPTIME"
# HTML-Part
HTML="<!DOCTYPE html>
<html>
<head><meta charset=\"utf-8\"></head>
<body style=\"font-family: sans-serif; font-size: 14px;\">
<h2>System-Report: $HOSTNAME</h2>
<p style=\"color: #666;\">$(date)</p>
<table style=\"border-collapse: collapse; width: 100%;\">
<tr style=\"background: #f0f0f0;\">
<td style=\"padding: 8px; border: 1px solid #ccc;\"><strong>Betriebssystem</strong></td>
<td style=\"padding: 8px; border: 1px solid #ccc;\">$OS</td>
</tr>
<tr>
<td style=\"padding: 8px; border: 1px solid #ccc;\"><strong>CPU</strong></td>
<td style=\"padding: 8px; border: 1px solid #ccc;\">$CPU_INFO</td>
</tr>
<tr style=\"background: #f0f0f0;\">
<td style=\"padding: 8px; border: 1px solid #ccc;\"><strong>Speicher</strong></td>
<td style=\"padding: 8px; border: 1px solid #ccc;\">$DISK</td>
</tr>
<tr>
<td style=\"padding: 8px; border: 1px solid #ccc;\"><strong>RAM</strong></td>
<td style=\"padding: 8px; border: 1px solid #ccc;\">$RAM</td>
</tr>
<tr style=\"background: #f0f0f0;\">
<td style=\"padding: 8px; border: 1px solid #ccc;\"><strong>Uptime</strong></td>
<td style=\"padding: 8px; border: 1px solid #ccc;\">$UPTIME</td>
</tr>
</table>
</body>
</html>"
# Mail zusammenbauen und verschicken
# Header - am Ende eine Leerzeile
{
printf '%s\n' \
"From: $FROM" \
"To: $TO" \
"Subject: $SUBJECT" \
"Date: $(LC_ALL=C date -R)" \
"MIME-Version: 1.0" \
"Content-Type: multipart/alternative; boundary=\"$BOUNDARY_ALT\"" \
""
# Plaintext-Part (beginnt mit Boundary und Leerzeile)
printf '%s\n' \
"--$BOUNDARY_ALT" \
"Content-Type: text/plain; charset=utf-8" \
"Content-Transfer-Encoding: quoted-printable" \
""
printf '%s\n' "$PLAIN" | python3 -c "
import sys, quopri
sys.stdout.buffer.write(quopri.encodestring(sys.stdin.buffer.read()))
"
# HTML-Part (beginnt mit Boundary und Leerzeile)
printf '%s\n' \
"" \
"--$BOUNDARY_ALT" \
"Content-Type: text/html; charset=utf-8" \
"Content-Transfer-Encoding: quoted-printable" \
""
printf '%s\n' "$HTML" | python3 -c "
import sys, quopri
sys.stdout.buffer.write(quopri.encodestring(sys.stdin.buffer.read()))
"
# Abschliessende Boundary
printf '%s\n' \
"" \
"--$BOUNDARY_ALT--"
} | msmtp "$TO"
Dieses Beispiel zeigt anhand einiger Systemdaten, wie eine Multipart-Mail zusammengebaut wird. Im nächsten Abschnitt wird das gleiche Prinzip angewendet, allerdings mit einem externen Wetter-Service.
Mail mit aktuellem Wetter
Open-Meteo ist ein kostenloser Wetterdienst mit einer einfachen API, also einer Schnittstelle, mit der ein Skript oder Programm Wetterdaten abfragen kann. Ein einziger curl-Aufruf mit den Geo-Koordinaten und einigen weiteren Parametern liefert das aktuelle Wetter als JSON zurück. Open-Meteo bietet sich als Beispiel an, da die Wetterdaten ohne Account oder Zugangsschlüssel abgerufen werden können. Das Skript fragt die aktuellen Wetterdaten für den Ort an, dessen Geo-Koordinaten im Skript angegeben sind. Achtung: die Koordinaten verwenden einen Dezimalpunkt, kein Komma.
Da der Ortsname neben dem Datum im Mail-Subject angezeigt werden soll und, wie im Beispiel, Sonderzeichen enthalten kann, muss in diesem Skript sichergestellt werden, dass diese Zeichen korrekt kodiert sind.
Im Gegensatz zum Plaintext- und HTML-Part, bei denen Content-Type und Content-Transfer-Encoding dem empfangenden Mailprogramm mitteilen, wie der folgende Text kodiert ist, und daher der gesamte Text mit der Python-Funktion quopri.encodestring kodiert werden kann, muss das Subject mit einer eigenen Funktion behandelt werden.
Dazu wird die Funktion encode_subject definiert:
encode_subject() {
python3 -c "
import sys
subject = sys.argv[1]
if all(ord(c) < 128 for c in subject):
print(subject)
else:
encoded = subject.encode('utf-8')
qp = ''.join(
'_' if b == 32 else
'={:02X}'.format(b) if b > 127 or chr(b) in '?=_' else
chr(b)
for b in encoded
)
print('=?utf-8?Q?' + qp + '?=')
" "$1"
}
Wenn Nicht-ASCII-Zeichen vorhanden sind, wird der Text in UTF-8-Bytes umgewandelt. So wird aus dem ö in Mönsheim in der UTF-8-Darstellung zwei Bytes: 0xC3 und 0xB6. Anschließend wird jedes dieser Bytes verarbeitet. Eine kompakte Schleife entscheidet für jedes Byte, wie es im Ergebnisstring dargestellt werden soll. Dabei gelten drei Regeln:
- Ein Leerzeichen (Byte 32) wird durch einen Unterstrich
_ersetzt, da Leerzeichen in Header-Feldern als Trennzeichen gelten. - Bytes über 127 sowie die Sonderzeichen
?,=und_werden als=XXdargestellt, wobeiXXder Hexadezimalwert des Bytes ist. Aus0xC3wird=C3, aus0xB6wird=B6. - Alle anderen Bytes, also normale ASCII-Zeichen wie Buchstaben oder Zahlen, werden direkt übernommen.
Die so entstandenen Textstücke werden mit join zu einem einzigen String zusammengefügt. Das Ergebnis für Mönsheimwäre:
M=C3=B6nsheim
Zum Schluss wird dieser kodierte Text in den RFC-2047-Rahmen gesetzt:
=?utf-8?Q?M=C3=B6nsheim?=
Dieser Rahmen teilt dem Mailclient mit, dass der Text UTF-8-kodiert ist und Q‑Encoding verwendet. Der Client wandelt das beim Anzeigen automatisch zurück in „Mönsheim”.
Nachdem das Subject entsprechend definiert ist, wird der API-Aufruf zusammengebaut. Im Skript geschieht das, indem in jeder Zeile ein weiterer Teil zur Variable API_URL hinzugefügt wird. Am Ende entspricht der Aufruf diesem Kommando:
curl "https://api.open-meteo.com/v1/forecast?latitude=48.85&longitude=8.93¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code&wind_speed_unit=kmh&timezone=Europe/Berlin"
Das gibt als Ergebnis folgendes JSON zurück:
{
"latitude": 48.84,
"longitude": 8.940001,
"generationtime_ms": 0.10466575622558594,
"utc_offset_seconds": 3600,
"timezone": "Europe/Berlin",
"timezone_abbreviation": "GMT+1",
"elevation": 410.0,
"current_units": {
"time": "iso8601",
"interval": "seconds",
"temperature_2m": "°C",
"relative_humidity_2m": "%",
"wind_speed_10m": "km/h",
"weather_code": "wmo code"
},
"current": {
"time": "2026-03-20T01:15",
"interval": 900,
"temperature_2m": 5.3,
"relative_humidity_2m": 57,
"wind_speed_10m": 1.1,
"weather_code": 0
}
}
Als nächstes wird der Wert eines einzelnen Feldes aus der JSON-Antwort in die entsprechende Variable geschrieben, hier am Beispiel der Temperatur:
TEMP=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['current']['temperature_2m'])")
Damit werden die JSON-Daten aus der Bash-Variable $RESPONSE an einen Python-Prozess übergeben. Python liest den Wert des Feldes temperature_2m unter dem Knoten current aus und gibt ihn als Text zurück, sodass er in der Bash-Variable TEMP weiterverwendet werden kann.
Im gleichen Muster werden dann auch die Werte für Luftfeuchtigkeit, Wind und Wettercode abgerufen. Der Wettercode wird anschließend noch in einen lesbaren Text übersetzt.
Anschließend werden die einzelnen Teile, wie im vorherigen Beispiel, zu einer korrekten E‑Mail zusammengesetzt und an msmtp übergeben.
Hier das gesamte Skript:
#!/bin/bash
TO="empfaenger@example.org"
FROM="sender@example.com"
BOUNDARY_ALT="==ALT_$(date +%s)=="
# Koordinaten und Ort anpassen
LAT="48.85"
LON="8.93"
ORT="Mönsheim"
TIMEZONE="Europe%2FBerlin"
# Das Subject wird per RFC 2047 kodiert, falls Nicht-ASCII-Zeichen
# enthalten sind (z.B. Umlaute im Ortsnamen). Ohne diese Kodierung
# würden Umlaute in der Mailübersicht des Clients korrumpiert dargestellt.
encode_subject() {
python3 -c "
import sys
subject = sys.argv[1]
if all(ord(c) < 128 for c in subject):
print(subject)
else:
encoded = subject.encode('utf-8')
qp = ''.join(
'_' if b == 32 else
'={:02X}'.format(b) if b > 127 or chr(b) in '?=_' else
chr(b)
for b in encoded
)
print('=?utf-8?Q?' + qp + '?=')
" "$1"
}
SUBJECT=$(encode_subject "Wetter $ORT $(date '+%d.%m.%Y')")
# Wetterdaten von Open-Meteo holen (kein API-Key nötig)
API_URL="https://api.open-meteo.com/v1/forecast"
API_URL="${API_URL}?latitude=${LAT}&longitude=${LON}"
API_URL="${API_URL}¤t=temperature_2m,relative_humidity_2m,wind_speed_10m,weather_code"
API_URL="${API_URL}&wind_speed_unit=kmh&timezone=${TIMEZONE}"
RESPONSE=$(curl -s "$API_URL")
if [ -z "$RESPONSE" ]; then
echo "Fehler: Keine Antwort von der API." >&2
exit 1
fi
# Werte aus JSON extrahieren
TEMP=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['current']['temperature_2m'])")
HUMIDITY=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['current']['relative_humidity_2m'])")
WIND=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['current']['wind_speed_10m'])")
WMO=$(echo "$RESPONSE" | python3 -c "import sys,json; d=json.load(sys.stdin); print(d['current']['weather_code'])")
# WMO-Wettercode in lesbaren Text übersetzen
WETTER=$(python3 -c "
code = int('$WMO')
codes = {
0: 'Klarer Himmel',
1: 'Überwiegend klar', 2: 'Teilweise bewölkt', 3: 'Bedeckt',
45: 'Nebel', 48: 'Raureif',
51: 'Leichter Nieselregen', 53: 'Nieselregen', 55: 'Starker Nieselregen',
61: 'Leichter Regen', 63: 'Regen', 65: 'Starker Regen',
71: 'Leichter Schneefall', 73: 'Schneefall', 75: 'Starker Schneefall',
80: 'Leichte Schauer', 81: 'Schauer', 82: 'Starke Schauer',
95: 'Gewitter', 96: 'Gewitter mit Hagel', 99: 'Gewitter mit starkem Hagel',
}
print(codes.get(code, 'Unbekannt (Code: ' + str(code) + ')'))
")
DATUM=$(LC_ALL=de_DE.UTF-8 date '+%A, %d. %B %Y %H:%M Uhr' 2>/dev/null || date)
# Plaintext-Part
PLAIN="Wetterbericht: $ORT
$DATUM
Wetter : $WETTER
Temperatur : ${TEMP}°C
Luftfeuchte : ${HUMIDITY}%
Wind : ${WIND} km/h"
# HTML-Part
HTML="<!DOCTYPE html>
<html>
<head><meta charset=\"utf-8\"></head>
<body style=\"font-family: sans-serif; font-size: 14px; max-width: 500px;\">
<h2>Wetterbericht: $ORT</h2>
<p style=\"color: #666;\">$DATUM</p>
<table style=\"border-collapse: collapse; width: 100%;\">
<tr style=\"background: #f0f0f0;\">
<td style=\"padding: 8px; border: 1px solid #ccc;\"><strong>Wetter</strong></td>
<td style=\"padding: 8px; border: 1px solid #ccc;\">$WETTER</td>
</tr>
<tr>
<td style=\"padding: 8px; border: 1px solid #ccc;\"><strong>Temperatur</strong></td>
<td style=\"padding: 8px; border: 1px solid #ccc;\">${TEMP}°C</td>
</tr>
<tr style=\"background: #f0f0f0;\">
<td style=\"padding: 8px; border: 1px solid #ccc;\"><strong>Luftfeuchtigkeit</strong></td>
<td style=\"padding: 8px; border: 1px solid #ccc;\">${HUMIDITY}%</td>
</tr>
<tr>
<td style=\"padding: 8px; border: 1px solid #ccc;\"><strong>Wind</strong></td>
<td style=\"padding: 8px; border: 1px solid #ccc;\">${WIND} km/h</td>
</tr>
</table>
<p style=\"font-size: 11px; color: #999; margin-top: 16px;\">
Wetterdaten: <a href=\"https://open-meteo.com\">Open-Meteo</a>
</p>
</body>
</html>"
# Mail zusammenbauen und verschicken
{
printf '%s\n' \
"From: $FROM" \
"To: $TO" \
"Subject: $SUBJECT" \
"Date: $(LC_ALL=C date -R)" \
"MIME-Version: 1.0" \
"Content-Type: multipart/alternative; boundary=\"$BOUNDARY_ALT\"" \
""
# Plaintext-Part
printf '%s\n' \
"--$BOUNDARY_ALT" \
"Content-Type: text/plain; charset=utf-8" \
"Content-Transfer-Encoding: quoted-printable" \
""
printf '%s\n' "$PLAIN" | python3 -c "
import sys, quopri
sys.stdout.buffer.write(quopri.encodestring(sys.stdin.buffer.read()))
"
# HTML-Part
printf '%s\n' \
"" \
"--$BOUNDARY_ALT" \
"Content-Type: text/html; charset=utf-8" \
"Content-Transfer-Encoding: quoted-printable" \
""
printf '%s\n' "$HTML" | python3 -c "
import sys, quopri
sys.stdout.buffer.write(quopri.encodestring(sys.stdin.buffer.read()))
"
# Abschliessende Boundary
printf '%s\n' \
"" \
"--$BOUNDARY_ALT--"
} | msmtp "$TO"
Die beiden Beispiele zeigen das Grundprinzip: Eine Mail ist letztlich nichts anderes als strukturierter Text, den msmtpentgegennimmt und weiterleitet. Wer das einmal verstanden hat, kann die Skripte nicht nur nachbauen, sondern auch leicht anpassen. Beim Wetter-Skript bietet Open-Meteo allein schon eine Vielzahl weiterer Möglichkeiten: Vorhersagen für die nächsten Tage, Niederschlagsmengen, UV-Index oder Sonnenauf- und untergang lassen sich mit einem zusätzlichen Parameter im API-Aufruf abfragen. Die vollständige Dokumentation dazu findet sich direkt auf open-meteo.com.”
Automatisches Versenden mit einem Zeitplaner
Natürlich sind solche Skripte erst dann nützlich, wenn sie automatisch und regelmäßig laufen. Auf Linux übernimmt das traditionell Cron, auf macOS stellt Apple mit launchd einen eigenen Mechanismus bereit. Dazu sollte der Computer dann natürlich 24/7 laufen und online sein.
Cron unter Linux
Cron ist ein Dienst, der im Hintergrund läuft und Aufgaben zu festgelegten Zeitpunkten ausführt. Die Zeitpläne werden in der sogenannten Crontab eingetragen. Mit folgendem Terminal-Befehl öffnest du die Crontab des aktuellen Benutzers in einem Text-Editor, meist ist der Editor nanovoreingestellt:
crontab -e
Die Syntax eines Eintrags besteht aus fünf Zeitfeldern, gefolgt vom auszuführenden Befehl:
Minute Stunde Tag Monat Wochentag Befehl
Ein Stern * in einem Feld steht für „jeden möglichen Wert”. Hier zwei Beispiele:
# System-Report jeden Morgen um 7 Uhr 0 7 * * * /home/leif/scripts/system-report.sh # Wetterbericht täglich um 9 Uhr 0 9 * * * /home/leif/scripts/wetter-report.sh
Eine praktische Hilfe zum Zusammenstellen von Cron-Ausdrücken bietet crontab.guru.
Ein wichtiger Hinweis: Cron läuft in einer minimalen Umgebung ohne die üblichen Shell-Variablen. Insbesondere der PATH ist stark eingeschränkt, sodass Befehle wie curl oder python3 unter Umständen nicht gefunden werden. Eine sichere Lösung ist, im Skript absolute Pfade zu verwenden. Welchen Pfad ein Befehl hat, lässt sich mit which herausfinden:
which curl which python3 which msmtp
Alternativ kann am Anfang des Skripts die PATH-Variable gesetzt werden. Hier ein Beispiel für den macOS:
export PATH="/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:$PATH"
launchd unter macOS
Unter macOS ist launchd der Systemdienst, der sowohl Hintergrundprozesse als auch geplante Aufgaben verwaltet. Zeitpläne werden als XML-Dateien im sogenannten Property-List-Format (.plist) definiert und an einem bestimmten Ort abgelegt, damit launchd sie einliest.
Bevor es losgeht, noch ein wichtiger Hinweis zum Speicherort der Skripte. macOS schützt bestimmte Ordner wie den Desktop, Dokumente und Downloads vor dem Zugriff durch automatisierte Prozesse. Skripte die von launchd ausgeführt werden sollen, gehören daher in einen ungeschützten Ordner, zum Beispiel ~/scripts/. Den kannst du einfach anlegen, falls er noch nicht existiert:
mkdir ~/scripts
Für Aufgaben des aktuellen Benutzers ist der Ordner ~/Library/LaunchAgents/ der richtige Ablageort für die plist-Dateien. Lege dort eine neue Datei an, zum Beispiel de.ileif.wetter-report.plist:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>de.ileif.wetter-report</string>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>/Users/leif/scripts/wetter-report.sh</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>RunAtLoad</key>
<false/>
<key>StandardErrorPath</key>
<string>/tmp/wetter-report.err</string>
</dict>
</plist>
Der Label-Eintrag ist der eindeutige Name des Jobs und sollte der Dateibezeichnung entsprechen. ProgramArguments gibt an, welches Skript ausgeführt werden soll. StartCalendarInterval definiert den Zeitplan, hier täglich um 9:00 Uhr. StandardErrorPath leitet eventuelle Fehlermeldungen in eine Datei um, was die Fehlersuche erleichtert.
Damit launchd die neue Datei einliest, muss sie einmalig registriert werden:
launchctl load ~/Library/LaunchAgents/de.ileif.wetter-report.plist
Ab sofort läuft das Skript täglich zur angegebenen Zeit. Mit unload lässt sich der Job wieder deaktivieren:
launchctl unload ~/Library/LaunchAgents/de.ileif.wetter-report.plist
Mit launchd wird ein verpasster Zeitpunkt nachgeholt. Wenn der Mac zur geplanten Zeit ausgeschaltet war, führt launchdden Job beim nächsten Start aus, sofern man RunAtLoad auf true setzt. Cron hingegen überspringt verpasste Ausführungen stillschweigend.
Fazit
Für das Versenden von E‑Mails aus der Kommandozeile oder aus einem Skript ist msmtp ein schlankes und zuverlässiges Werkzeug, das sich ohne großen Konfigurationsaufwand einbinden lässt. Einmal verstanden, wie eine E‑Mail intern aufgebaut ist, ist das Versenden von der Kommandozeile gar nicht so kompliziert. Die MIME-Struktur, die Zeichenkodierung und der Aufbau des Headers folgen klaren Regeln, und die gezeigten Skripte lassen sich als Ausgangspunkt für eigene Ideen verwenden.
Die Beispiele in diesem Artikel sind bewusst einfach gehalten. Ein System-Report, ein Wetterbericht, aber das Prinzip lässt sich auf vieles übertragen: Monitoring-Alerts, tägliche Zusammenfassungen aus eigenen Datenquellen oder Benachrichtigungen aus Automatisierungsskripten. Wer die Skripte einmal zum Laufen gebracht hat, wird schnell eigene Anwendungsfälle finden. Allein Open-Meteo bietet neben den aktuellen Wetterdaten noch Vorhersagen, Niederschlagsmengen, UV-Index und vieles mehr, alles mit demselben einfachen API-Aufruf. Die vollständige Dokumentation dazu findet sich auf open-meteo.com.


Antworte auf den Kommentar von leifjp Antwort abbrechen