Die Herausforderung

Ich bekomme die Gehaltszettel meiner Firma per eMail. Das pdf ist Passwort-geschützt, mit einem statischen, von mir gewähltem Passwort.

Ich möchte dieses Passwort entfernen, weil ich das Dokument in Paperless sichern möchte. Hier sind die Daten unter meiner Kontrolle, und der Passwort-Schutz ist eher kontraproduktiv.

Die Lösung

Zuerst benötige ich das Dokument an einem Ort wo ich es weiterverarbeiten kann. Primär geht es per Mail an meinen Firmen-Mailaccount, ich habe es von dort aus automatisiert weitergeleitet an eine eMail Adresse paperless-preprocess@tech-tales.blog1. Dadurch habe ich es in meiner Infrastruktur und kann damit weiterarbeiten.

Node-Red

Der tatsächliche Prozess passiert dann in Node-Red, da ich Node-Red bereits für einige andere automatisierte Tasks verwende. Dafür benutze ich die node-red-node-email Palette, um Mails zu empfangen und zu senden. Alle anderen Nodes im Workflow sind direkt Teil von Node-Red.

Der Flow arbeitet wie folgt:

  • Lies die Mails von der oben genannten Mailadresse
  • Mache einen Filter, basierend auf dem Betreff der eMail. Ich bekomme verschiedene Typen von Mails an diese Adresse, und verarbeite die unterschiedlich.
  • Finde das korrekte Attachment. Das eMail hat mehrere Attachments, zum Beispiel Bilder im Body, die Nodered formal ebenfalls als Attachment versteht. Für diesen Task habe ich eine Funktion geschrieben. Zusätzlich habe ich in dieser Funktinon gleich den Request an Stirling PDF vorbereitet:
    // Gehe über alle Attachments
    for (const attachment of msg.attachments) {
      // Ich suche nach PDFs; andere interessieren mich nicht
      if (attachment.contentType != "application/pdf")
          continue;
    
      // Sagen wir mal, mein Dokument heißt
      // gehaltszettel-2023-12-chris.pdf...
      if (!attachment.filename.startsWith("gehaltszettel") ||
          !attachment.filename.endsWith(".pdf"))
          continue;
    
      msg.document_buffer = attachment.content;
    }
    
    // An dieser Stelle sollte (TM) ich ein
    // Fehlerhandling dafür haben, dass das Attachment
    // nicht gefunden wurde...
    
    // Request für Stirling PDF vorbereiten
    msg.headers = {
      "Content-Type": "multipart/form-data",
      "accept": "*/*"
    };
    // Behalte die alte Payload, falls ich die später
    // noch einmal brauche
    msg.old_payload = msg.payload;
    msg.payload = {
      "fileInput": {
          value: msg.document_buffer,
          options: {
              type: "application/pdf",
              // Nicht ganz sicher, ob ein Dateiname
              // gesetzt sein *muss*
              filename: "in.pdf"
          }
      },
      "password": "my_password"
    };
    
    return msg;
    
  • Der nächste Schritt ist der Request auf Stirling pdf - ein pdf-Tool, das coole Dinge machen kann. Für diesen Use Case ist am wichtigsten, dass ich Passworte von pdfs entfernen kann - offensichtlich.
    Es gibt eine API dokumentation für Stirling PDF, und ich habe diesen Endpoint für mein Projekt genutzt. Die größte Challenge dabei war, herauszufinden wie ich ein Formular über Node-Red korrekt ansprechen kann, aber das war am Ende auch möglich. Wie das geht, ist ja bereits im vorigen Codeblock sichtbar.
    Der nächste Knoten im Flow ist dann also ein simpler Node-Red HTTP Request Node. Ich sende einen Post Request und definiere die URL als https://stirling-pdf.tech-tales.blog/api/v1/security/remove-password. Zusätzlich sage ich dem Node, dass der Return-Wert ein Binary Buffer sein soll.
  • Das Resultat sende ich per Mail an paperless@tech-tales.blog. Dieser Mailaccount wird von Paperless verarbeitet, also muss ich garnicht mit Paperless selbst interagieren.
    Am Weg habe ich noch ein bisschen Vorbereitung gemacht:
    msg.attachments = [{
      type: "attachment",
      content: msg.payload,
      contentType: "application/pdf",
      filename: "Gehaltszettel.pdf"
    }];
    
    msg.topic = "Neuer Gehaltszettel";
    // Überschreibe die Payload mit einem neuen String, sonst
    // ist Paperless verwirrt
    msg.payload = "Hier ist der entschlüsselte Gehaltszettel!";
    
  • Schlussendlich folgt noch ein “Send eMail” Node im Flow.

Stirling pdf

Wie bereits geschrieben war der komplizierteste Teil der Arbeit das Decrypten der Datei via Stirling PDF. Die Instanz lief bereits, und ich hatte auch kein Problem damit, per UI ein Passwort zu entfernen. Aber ich habe einige Zeit benötigt bis ich den POST Request richtig codiert hatte. Schlussendlich waren die folgenden Dinge wichtig:

  • Nicht sehr überraschend, der Header Content-Type: multipart/form-data.
  • Die tatsächlichen Inputs für den Form müssen korrekt codiert werden. Prinzipiell haben wir ein (JSON) Objekt mit den typischen key: value Paaren. Das wird allerdings kompliziert wenn ein Dokument geschickt werden soll - dann lautet das Paar nämlich key: {value: ..., options: {...}}.
    • Der value ist dann einfach der Bytestring des Dokuments, das war leicht.
    • Die Option type: aplication/pdf war auch leicht zu finden, und zu erwarten.
    • Zusätzlich ist die Option filename: "in.pdf" notwendig. Das wiederum war für mich überraschend, und es hat lange gedauert, bis ich das verstanden habe. Der definierte Name muss nicht zwangsläufig etwas mit der Datei zu tun haben, aber das muss gesetzt sein.
      Stirling pdf hat zwar eine Fehlermeldung geworfen, ich habe es aber nicht geschafft, daraus das Problem festzustellen.

Resultat

Schlussendlich habe ich es geschafft, das Passwort zu entfernen! Zusätzlich konnte ich einige meiner selbst gehosteten Services verbinden: Node-Red, Stirling pdf, meinen eMail Server, und Paperless.

Am stärksten habe ich gelernt, wie ich Formulare per POST Request bedienen kann, und dass ich Javascript nicht mag. Entsprechend möchte ich gerne von Node-Red zu etwas anderem wechseln. Ich kenne bereits Apache Airflow, das in Python geschrieben ist (und ich mag Python!), aber ich weiß noch nicht, ob das jeden Use Case abdecken kann den ich gerne abgedeckt hätte. Insbesondere scheint eine durchgehende Interaktion mit MQTT schwierig zu sein…


  1. Wie üblich: Die genutzten Mailadressen existieren nicht. Schicke gerne Nachrichten dorthin, aber bitte wundere dich nicht wenn ich nicht reagiere. ↩︎