Das Herzstück meines Dokumentenmanagement-Systems hat sich erst im Nachhinein entwickelt. Am Anfang ging ich eigentlich stark davon aus, dass das Herzstück die Anzeige und Suche von Dokumenten sein wird. Die Aufgabe, für die es auch geschaffen wurde.

Etwas später stellte sich aber heraus, dass das Herzstück die Automatisierung sein würde.

Der Upload von Dokumenten über das Webinterface klappte ja soweit gut. Ich wollte es aber irgendwann noch einfacher haben. Daher überlegte ich mir, wie die einzelnen Schritte aussehen, wenn ich einen Brief oder ein Dokument ablegen möchte:

Ich bekomme ein Brief, den ich im DMS ablegen möchte. Sobald er hier bei uns im Flur liegt, öffne ich ihn, lese ihn, scanne ihn ein, übertrage ihn auf meinen Laptop und lade ihn dort hoch.

Insbesondere die letzten drei Schritte haben immer noch ein wenig Überwindung und Zeit gekostet. Aber was habe ich daran ändern können?

Ganz einfach: Meine Drucker- / Scannerkombination (damals ein Brother MFC; heute ein Epson Workforce Gerät) beherrschten beide das Scannen eines Dokuments und dem automatischen Versand per E-Mail an eine Adresse.

Hier sah ich meine Möglichkeit, mir die lästigen Schritte des Speicherns und manuellen Hochladens abzunehmen: Der Scanner schickt mir die PDF einfach direkt an mein System und dieses macht dann mittels Magie aus der E-Mail mit Anhang ein neues Dokument in meinem DMS.

Prima, da hatten wir die nächste Baustelle. Auch hier überlegte ich: Was muss es wie oder wann machen und welche Aufgaben soll es am Ende erfüllen.

Daraus entstanden ist eine PHP Anwendung, welche per Cronjob auf meinem Server alle fünf Minuten gestartet wurde. Normalerweise verwendet man PHP um z.B. Inhalte von Webseiten dynamisch darstellen oder generieren zu lassen. In dem Fall sollte das ganze nur intern ablaufen und keine Ausgaben erzeugen. (OK, streng genommen erzeugt es doch ausgaben, aber auf Konsolenebene um ein Log zu schreiben.)

Also, was musste es machen:

  • In einem E-Mailpostfach E-nach neuen E-Mails sehen
  • E-Mails auswerten und E-Mails mit einem PDF als Anhang einlesen
  • Diese PDF dann einem Benutzer zuordnen und als neues Dokument ablegen

Mittlerweile habe ich das ganze noch erweitert:

  • OCR Texterkennung
  • Versenden von Bestätigungsemails an die Benutzer
  • Ausführliches Logging der einzelnen Schritte
  • Selbstreparatur, falls das Skript einmal abgebrochen ist

Aber beginnen wir am Anfang. Mein „Fetchmail“ getauftes Skript (ein wenig angelehnt an das gleichnamige Programm fetchmail) baut als ersten Schritt eine IMAP Verbindung zu einem Postfach auf, wo dann die E-Mails mit PDF Anhang warten:

$imap = imap_open('{' . $config['smtpHost'] . ':993/imap/ssl}INBOX', $config['smtpLogin'], $config['smtpPassword']) or die(date("Y-m-d H:i:s") ." ". $randomString ." [ERROR] IMAP Verbindung fehlgeschlagen\n");
$message_count = imap_num_msg($imap);
for ($m = 1; $m <= $message_count; ++$m) {
    $header = imap_header($imap, $m);
    $body = imap_fetchbody($imap, $m, 1);
    $message_count; ++$m) {
    $email[$m]['from'] = $header->from[0]->mailbox . '@' . $header->from[0]->host;
    $email[$m]['fromaddress'] = @$header->from[0]->personal;
    $email[$m]['to'] = $header->to[0]->mailbox;
    $email[$m]['subject'] = $header->subject;
    $email[$m]['message_id'] = $header->message_id;
    $email[$m]['date'] = $header->udate;
    ...
}

Was passiert hier? In der Zeile 1 wird die IMAP Verbindung aufgebaut. Die Variablen im Quelltext sind in einer separaten Konfigurationsdatei gespeichert. Sollte die IMAP Verbindung nicht klappen, wird das ganze Skript beendet und es gibt eine Rückmeldung (die dann im Log landet).

Nachdem die IMAP Verbindung aufgebaut wurde, wird in der Zeile 2 die Anzahl der E-Mails gezählt.

Zeile 3 führt eine Schleife durch und zählt dabei die Durchläufe und vergleicht diese mit der der Anzahl der E-Mails. Ist das maximum erreicht, steigt das Skript aus der Schleife aus.

Zeile 4 und 5  legen alle IMAP Daten in eine bzw. zwei Variablen, auf die ich dann danach zugreifen kann.
Zum Beispiel findet man die meisten Metadaten einer E-Mail im Header, so erfahre ich z.B. „Wo kommt die Mail her?“ oder „Wo soll die Mail hin?“. Damit das ganze etwas übersichtlicher wirkt, wandert alles in eine Variable $email die jeweils einen Index mit der zuvor durchgezählten E-Mail erhält.

Dadurch kann ich später jederzeit auf die E-Mails zugreifen (innerhalb des Skriptes).

$structure = imap_fetchstructure($imap, $m);
$attachments = array();

if (isset($structure->parts) && count($structure->parts)) {
    for ($i = 0; $i < count($structure->parts); $i++) {
        $attachments[$i] = array(
            'is_attachment' => false,
            'filename' => '',
            'name' => '',
            'attachment' => ''
        );

        if ($structure->parts[$i]->ifdparameters) {
            foreach ($structure->parts[$i]->dparameters as $object) {
                if (strtolower($object->attribute) == 'filename') {
                    $attachments[$i]['is_attachment'] = true;
                    $attachments[$i]['filename'] = $object->value;
                }
            }
        }

        if ($structure->parts[$i]->ifparameters) {
            foreach ($structure->parts[$i]->parameters as $object) {
                if (strtolower($object->attribute) == 'name') {
                    $attachments[$i]['is_attachment'] = true;
                    $attachments[$i]['name'] = $object->value;
                }
            }
        }

        if ($attachments[$i]['is_attachment']) {
            echo date("Y-m-d H:i:s") ." ". $randomString ." [INFO] E-Mail Anhang vorhanden MailCountID: ". $m ." MessageID: ". $header->message_id ."\n";
            $attachments[$i]['attachment'] = imap_fetchbody($imap, $m, $i + 1);
            if ($structure->parts[$i]->encoding == 3) { // 3 = BASE64
                $attachments[$i]['attachment'] = base64_decode($attachments[$i]['attachment']);
            } elseif ($structure->parts[$i]->encoding == 4) { // 4 = QUOTED-PRINTABLE
                $attachments[$i]['attachment'] = quoted_printable_decode($attachments[$i]['attachment']);
            }
        }else{
            echo date("Y-m-d H:i:s") ." ". $randomString ." [WARNING] Kein E-Mail Anhang vorhanden MailCountID: ". $m ." MessageID: ". $header->message_id ."\n";
        }
    }
}							

Hier passiert folgendes: Es wird geschaut ob Anhänge in der E-Mail vorhanden sind. Es wird sozusagen die Gesamte Mailstruktur in die Variable $attachments gespeichert. 

Diese kann ich unter anderem darauf prüfen, ob ein Anhang vorhanden ist. Das passiert in der letzten IF-Struktur, in der auch gleichzeitig geprüft wird wie der Anhang kodiert ist. Denn: Je nach genutztem Standard, kann der Anhang unterschiedlich kodiert sein. Es ist z.B. auch möglich, dass der Anhang gar kein Anhang an sich ist, sondern die Datei im Base64 Format in der E-Mail geschrieben steht (sogenanntes Inline). Die E-Mailprogramme selbst machen daraus dann aber wieder ein „anklickbaren“ Anhang wie man es kennt.

Kurze Anmerkung an dieser Stelle: Ich habe bisher noch nicht alle erdenklich und möglichen Szenarien abgedeckt. Ich habe mich daher dazu entschlossen, das Skript nach Bedarf zu erweitern oder anzupassen, wenn z.B. ein E-Mailanhang nicht erkannt oder richtig verarbeitet wird. Da es sich eben um ein privates Projekt handelt, musste ich hier nicht auf alle Eventualitäten Rücksicht nehmen :).

So, jetzt habe ich die E-Mails also im System und das Skript geht die E-Mails nach und nach durch und schaut ob Anhänge vorhanden sind. Nun musst mit den E-Mails aber noch etwas passieren.

$attName = utf8_decode(imap_utf8($attachment['filename']));
$attPdfaName = $attName .".pdfa";
$contents = $attachment['attachment'];
$contentsB64 = base64_encode($attachment['attachment']);

$isattachment = $attachment['is_attachment'];
echo date("Y-m-d H:i:s") ." ". $randomString ." [INFO] Speichere Anhang temporär ab: ". $config['fetchmailRoot']."uploads/" . $attName."\n";
file_put_contents($config['fetchmailRoot']."uploads/" . $attName, $contents);

Hier lege ich den zuvor erkannten Anhang in einen temporären Ordner (ja, direkt ins Filesystem – ganz ohne komme ich dann doch nicht aus – aber ist ja nur kurzfristig ;D).

$shellexec = shell_exec("ocrmypdf -l deu+eng --force-ocr --output-type pdfa ". $config['fetchmailRoot']."uploads/". $attName ." ". $config['fetchmailRoot'] ."uploads/". $attPdfaName ." 2>&1");

Hier passiert nun die Magie der Texterkennung. Ich nutze dafür ein Programm unter Linux und rufe es mittels shell_exec (diese Funktion kann man verwenden um einen Befehl auf der Shell auszuführen) auf und lasse aus dem starren PDF ein PDF/A inklusive Textlayer generieren. Das Ziel-PDF bekommt dann noch die Dateiendung .pdfa (Dateiendungen unter Linux sind übrigens nur reine Kosmetik für uns Menschen).

$pdfaFile = file_get_contents($config['fetchmailRoot']."uploads/". $attPdfaName);
$pdfaFile = base64_encode($pdfaFile);
$parser = new \Smalot\PdfParser\Parser();
$pdfGetText    = $parser->parseFile($config['fetchmailRoot']."uploads/". $attPdfaName);
$documentIndex = $pdfGetText->getText();

Der neu erzeugte Textlayer wird mit einer Klasse von PDF Parser (vielen Dank für diese tolle PHP Klasse!) extrahiert und schlussendlich in eine Variable ($documentIndex) abgelegt. In dieser Variable befindet sich nun alles an Text, was ocrmypdf vorher erkannt hat.

Übrigens: Texterkennung ist grundsätzlich immer eine Sache für sich. Die Texterkennung ist allgemein nur so gut, wie das eingescannte PDF.  Texterkennung funktioniert in der Regel über eine Matrix, sprich es werden erst die Unterschiede der unterschiedlichen Pixel digitalisiert und dann werden diese „Pixelgebilde“ mit einer Matrix verglichen um daraus Buchstaben zu bilden. 

Tesseract ist eine kostenfreie Anwendung, die für den allgemeinen Gebrauch recht gute Dienste liefert. Ich habe aber auch immer mal wieder Dokumente, da ist die Erkennung komplett unbrauchbar oder es werden nur wenige Worte erkannt. Für mich ist das eine Einschränkung mit der ich Leben kann. Alles in Allem habe ich aber durchweg gute Ergebnisse.

Und nun? Was machen wir nun mit den gewonnenen Informationen? Ich lege Sie mit in die Datenbank. Sie werden später mit in die Tabelle aufgenommen, die Spalte der Tabelle aber versteckt (Danke geht an Datatables!). Dadurch fließen diese Daten mit in die Tabelle ein, die dann wiederum durchsucht werden kann.

Fortsetzung folgt…


0 Kommentare

Schreibe einen Kommentar

Avatar-Platzhalter

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Diese Website verwendet Akismet, um Spam zu reduzieren. Erfahre mehr darüber, wie deine Kommentardaten verarbeitet werden.

DSGVO Cookie Consent mit Real Cookie Banner