Skip to main content
Max Kistner

News per Mail

Während der Corona-Pandemie habe ich mir eine blöde Sache angewöhnt: ständig auf bspw. tagesschau.de unterwegs zu sein, um nix zu verpassen. Wenn man dann schon mal im Browser ist, kann man dann ja noch auf anderen Seiten unterwegs sein... bspw. hackernews. Ich habe mir jetzt ein Skript geschrieben, das mir zu bestimmten Zeiten am Tag die aktuellen News per Mail schickt, damit ich nicht in den Browser muss.

Dafür gibt es bestimmt schon andere Lösungen, aber ich wollte auch einfach ein bisschen rumprobieren.

Ich verwende hierfür im Prinzip folgende Dinge:

Der Ablauf sieht im Prinzip so aus:

Diese Mail wird 4-mal am Tag verschickt. Ich kann dann kurz in meine Mails schauen, checken, ob die Welt bald untergeht und welchen neuen Hype es in der Tech-Welt gibt bzw. wann ich meinen Job an ein LLM verliere und dann (hoffentlich) produktiv weiterarbeiten, ohne vom Browser in das nächste Rabbithole gezogen zu werden.

Puppeteer #

Puppeteer ist im Prinzip ein Browser, den man ohne Fenster betreiben kann (sog. headlessly) und per Programmiersprache steuern kann (API).

Dadurch ergeben sich viele coole Use-Cases. Man kann sich im Prinzip alle möglichen Dinge, die man im Browser macht, bspw. automatisieren.

Ich habe ihn hier verwendet, um automatisch News-Webseiten zu besuchen und dort die aktuellen "Schlagzeilen" inkl. Teaser rauszuziehen, diese in eine E-Mail zu verpacken und mir dann selbst zuzuschicken.

Ich zeige das mal am Beispiel meiner Webseite. Damit das funktioniert, muss man NodeJS und NPM installiert haben. Dann muss man Puppeteer im Projekt installieren, also npm install puppeteer.

Für alle, die noch nie etwas mit NodeJS gemacht haben:

  1. Neuen Ordner erstellen
  2. Terminal darin öffnen
  3. Dann im Terminal npm init -y eingeben (initialisiert sozusagen ein NodeJS-Projekt)
  4. Dann Puppeteer installieren über npm install puppeteer
  5. JS-Datei erstellen, bspw. index.js.
  6. Etwas Einfaches reinschreiben, bspw. console.log("Hello World");
  7. Dann im Terminal node index.js und man sollte Hello World im Terminal sehen

Um auf meiner Webseite die Schlagzeilen und Teaser zu holen, könnte man folgenden Code verwenden:

index.js
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch();
const page = await browser.newPage();

// Sagt Chrome, dass es auf diese URL gehen soll
await page.goto('https://maxkistner.com');

// stellt die Fenstergröße ein (interessant für bestimmte Layouts)
await page.setViewport({ width: 1080, height: 1024 });

// Die Funktion, die `page.evaluate` erhält, ist JS, das im Kontext 
// der Seite ausgeführt wird, also in unserem Headless-Browser.
const teasers = await page.evaluate(() => {
    // holt sich alle Posts
    const teasers = document.querySelectorAll(".postlist-item");
    const infos = (Array.from(teasers)).map((teaser) => {
        // holt sich alle Titel
        const title = teaser.querySelector(".postlist-link").innerText;
        // holt sich alle Teaser
        const description = teaser.querySelector(".teaser").innerText;
        // macht ein Objekt daraus
        return { title, description };
    });
    // gibt das Objekt zurück
    return infos;
});

await browser.close();

console.log(teasers)

Wenn man das dann in der Konsole ausgibt, sieht das so aus (... habe ich eingefügt, damit die Lauflänge nicht zu groß wird):

[
  {
    title: 'News per Mail',
    description: 'Während der Corona-Pandemie habe ...'
  },
  {
    title: 'Willkommen!',
    description: 'Ich habe mal gelesen, dass Wissen...'
  }
]

Damit hat man eigentlich alles, um daraus eine E-Mail zu bauen.

E-Mail verschicken mit Nodemailer #

Auch nodemailer ist ein npm-Package. Die Installation läuft damit genauso wie bei Puppeteer (npm i nodemailer) und auch die vorbereitenden Schritte sind gleich wie oben.

Eine einfache Mail kann man dann so versenden:

index.js
import nodemailer from 'nodemailer';

function sendMail(subject, text) {
    const transporter = nodemailer.createTransport(
        {
            host: "<host>", // aus der Doku des Anbieters
            port: 465, // aus der Doku des Anbieters
            secure: true,
            auth: {
                user: '<user', // aus der Doku des Anbieters
                pass: '<pw>' // aus der Doku des Anbieters
            },
            logger: true,
            transactionLog: false,
            allowInternalNetworkInterfaces: false
        },
        {
            from: 'Mir <mail@example.com>'
        }
    );

    const message = {
        to: 'Dir <mail@example.com>',
        subject,
        html: `<p>${text}</p>`
    };

    transporter.sendMail(message, (error, info) => {
        // Falls es einen Fehler gab, hat `error` einen Wert und
        // wir fallen in dieses if
        if (error) {
            console.log('Error occurred');
            console.log(error.message);
            return process.exit(1);
        }
        console.log('Nachricht versendet.');
        console.log(nodemailer.getTestMessageUrl(info));
    });
}

sendMail("Test Mail", "Das ist eine Testmail.");

Und dann in der Konsole wieder node index.js und man sollte kurz darauf eine Mail erhalten.

Wenn wir jetzt das Ergebnis vom Skript mit Puppeteer noch etwas zu HTML verwurschteln und das dann an die Funktion oben übergeben, können wir uns schon eine Mail mit aktuellen News zusenden.

Jetzt fehlt noch das automatische Versenden zu einem gewissen Zeitpunkt.

Cronjobs zum automatischen Versenden von Mails #

Cronjobs können auf Linux eingesetzt werden, um wiederkehrende Aufgaben zu erledigen.

Die Erstellung eines Cronjobs ist relativ einfach:

  1. Terminal öffnen
  2. crontab -e eingeben
  3. Cronjob erstellen
  4. Datei speichern und schließen
  5. Warten bis Zeitfenster des Cronjobs aktiv ist

"Cronjob erstellen" ist natürlich trotzdem ein bisschen schwieriger, aber auch nicht so schlimm. Um die Zeit einzustellen, gibt es hier einfach eine bestimmte Syntax. Am Ende muss man dann noch das Skript eintragen, das ausgeführt werden soll. Bei mir soll bspw. um 7:00 Uhr, 12:15 Uhr, 17:00 Uhr und 20:15 Uhr das Skript laufen:

crontab -e
00 07 * * * . ~/cron-jobs/cronjob.env.sh; cd ~/_dev/news-scrape/out-tsc/src; $(which node) index.js >> ~/cron-jobs/log.log 2>&1
15 12 * * * . ~/cron-jobs/cronjob.env.sh; cd ~/_dev/news-scrape/out-tsc/src; $(which node) index.js >> ~/cron-jobs/log.log 2>&1
00 17 * * * . ~/cron-jobs/cronjob.env.sh; cd ~/_dev/news-scrape/out-tsc/src; $(which node) index.js >> ~/cron-jobs/log.log 2>&1
15 20 * * * . ~/cron-jobs/cronjob.env.sh; cd ~/_dev/news-scrape/out-tsc/src; $(which node) index.js >> ~/cron-jobs/log.log 2>&1

Was bedeutet jetzt das hier genau? ⬇️

. ~/cron-jobs/cronjob.env.sh; cd ~/_dev/news-scrape/out-tsc/src; $(which node) index.js >> ~/cron-jobs/log.log 2>&1

Dieser erste Part hier . ~/cron-jobs/cronjob.env.sh; führt die Datei cronjob.env.sh im Ordner ~/cron-jobs/cronjob.env.sh aus (~/ steht für den Nutzerordner des aktuellen Users). Der Inhalt dieser Datei sieht so aus:

~/cron-jobs/cronjob.env.sh
#!/bin/bash

# Quelle: https://gist.github.com/simov/cdbebe2d65644279db1323042fcf7624

# NVM_DIR finden man in der .bashrc des Nutzers - Nutzername (bei mir pi) entsprechend anpassen!
export NVM_DIR="/home/pi/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"

Diese Datei ist nicht zwingend notwendig. Ich verwende nvm zum Verwalten meiner NodeJS-Version. Das bedeutet, dass sich meine NodeJS-Installation nicht immer an einem fixen Ort befindet, sondern je nach Version, die gerade gewählt ist, an einer anderen Stelle liegt. Den Pfad zur aktuellen NodeJS-Version kriegt man im Terminal über diesen Aufruf hier $(which node), ABER das klappt nicht ohne Weiteres, wenn der Cronjob läuft. Durch Ausführen der Datei oben liefert $(which node) aber den korrekten Pfad zurück und sollte das eigentlich auch tun, egal welche NodeJS-Version ausgewählt ist.

cd ~/_dev/news-scrape/out-tsc/src wechselt in den Ordner, indem sich das JS befindet und über $(which node) index.js klebt man sozusagen den aktuellen NodeJS-Pfad mit dem Skript, das man ausführen will, zusammen. So kann man das Skript über NodeJS laufen lassen.

Der letzte Teil des Skripts hier >> ~/cron-jobs/log.log 2>&1 leitet alle Ausgaben der Aufrufe des Skripts in die Datei log.log um. Das erleichtert die Fehlersuche, falls etwas nicht funktioniert. Interessanterweise habe ich gerade in die Datei reingeschaut, weil manchmal Mails nicht zugestellt wurden und siehe da: In irgendeiner Schlagzeile in der Mail stand scheinbar Text drin, der dem SPAM-Filter von Strato nicht gefiel, weshalb dann die Mail nicht gesendet werden durfte. Wie ich das verhindern soll, weiß ich noch nicht.

Fertig #

Das ist eigentlich alles. Hiermit kann man, wie gesagt, coole Sachen machen.

Eine Sache, die ich auch noch in meinem Skript laufen lasse, ist das Abrufen des Wetterberichts. Der wird aber auf der Seite, auf der ich ihn mir hole, über ein cooles Widget dargestellt, weshalb das Rausziehen von Text nicht so zielführend wäre.

Puppeteer erlaubt es aber auch, Screenshots von Elementen zu machen. So könnte der Code hierfür aussehen:

const weatherWidget = await page.waitForSelector('#id-eines-widgets');
if (!weatherWidget) {
    console.error("selector not found")
    return;
};

await weatherWidget.screenshot({
    path: __dirname + '/weather.png',
});

Dieses Bild schicke ich mir dann einfach als eigebetteten Anhang mit.

Ich hoffe das hier hilft irgendjemandem weiter. Falls sich jemand das anschaut und Fragen hat, Hilfe braucht oder Anregungen hat: einfach eine Mail schicken.