stefankittel
Goto Top

Web-GUI die eine Liste der ausgehenden Mails aus Postfix anzeigt gesucht

Hallo,

ich habe hier einen Postfix der als ausgehendes Relay läuft.
Nun möchte einige Nutzer gerne eine Web-GUI haben auf der sie sehen können ob/wann eine Mail rausgegangen ist.
Das Postfix-Log hat ja nun für eine Mail ca. 6-7 Zeilen. Bei mir kommen noch Info-Zeilen für Subject und Filenames hinzu.
Ein reiner ELK-Stack hilft hier nicht weil es für normale User zu komplex ist.

Es soll rein eine Liste mit den Felder From, To, Subject, Filesnames, Datum, Status sein.

Kennt Jemand so eine Software?
Ich will nicht etwas programmieren was es schon gibt.

Stefan

Content-ID: 671242

Url: https://administrator.de/forum/web-gui-die-eine-liste-der-ausgehenden-mails-aus-postfix-anzeigt-gesucht-671242.html

Ausgedruckt am: 12.03.2025 um 20:03 Uhr

LordGurke
LordGurke 09.02.2025 um 22:24:11 Uhr
Goto Top
Kannst du den Nutzern, die das Anfragen, nicht einfach dauerhaft DSN für den Client einschalten? Dann bekommen die eine Mail, wenn ihre E-Mail zugestellt wurde, mit genauem Datum und der Uhrzeit.
Harald99
Harald99 09.02.2025 um 22:31:53 Uhr
Goto Top
MDN und DSN existieren, kann der User mit einem guten Mailclient selbst anfordern.
StefanKittel
StefanKittel 10.02.2025 um 00:12:37 Uhr
Goto Top
Moin,
ja, der DSN wäre dafür eigentlich gut.
Sehr einfach umzusetzen und es gibt keine Probleme wegen Datenschutz etc.
Ich befürchte, dass es "diesem Kunden" nicht helfen wird.

Mal schauen was der Kunden wirklich will und was er ausgeben möchte.

Stefan
Harald99
Harald99 10.02.2025 um 09:16:29 Uhr
Goto Top
Die Mailqueue geht aber den einzelnen User nix an!
Lochkartenstanzer
Lochkartenstanzer 10.02.2025 aktualisiert um 09:33:17 Uhr
Goto Top
Moin,

Ich wußte jetzt keine Webgui dafür, weil sowas I.d.R. der Admin über das CLII macht.

Abgesehen davon sehe ich Datenschutzprobleme, weil dann die betreffenden Metadaten von allen Mails zu sehen waren und der User kein Admin mit entsprechender Verantwortung ist.

lks
ThePinky777
ThePinky777 10.02.2025 aktualisiert um 10:14:56 Uhr
Goto Top
ich hab das so gelöst mal:

- Copy Log vom Postfix auf nen SMB Share, sprich chronjob linux > script welches Share verbindet und dann Daten hochkopiert
- Script das es (Log FIle) dann zerlegt und die Daten aufbereitet
- Script das anschliessend die aufbereiteten Daten ins SQL einschiesst >> oder SLQ Express geht auch.
- Access Datenbank mit verlinkung auf Datenbank und dann kann man die Daten analysieren.
Denkbar in deinem Falls ein Script das dann für ein gesisses Datum dann einen Report für eine gewisse Emaildresse
raussucht und dann das ganze per Email an den User schickt oder so...

Postfix ist DUMM und die Logs schreiben nur Tag und Monat ins log, keine Jahreszahl,
somit hast du immer bei Jahresumstellung mega spass...
Harald99
Harald99 12.02.2025 um 09:55:34 Uhr
Goto Top
Das ist auch Aufgabe des Syslog, dass ordentlich zu erfassen.

2020 ist eine neu Software erschienen, die extra für Postfix die Logs zentral verwalten soll: Lightmeter
StefanKittel
StefanKittel 12.02.2025 um 10:01:58 Uhr
Goto Top
Moin,
es funktioniert alles so wie designed. Die Software muss nur zeitlich getrennte Einträge korrekt zuzeinander zuordnen.
Besonders wenn Mails erst nach Stunden zugestellt werden können.

Ich habe mir mit ChatGPT eine kleine Software zuzsammengestellt welche mir eine Auswertung als JSON zurückgibt.
Das reicht mit als Machbarkeitsstudie und nun spreche ich mit dem Kunden was er wirklich haben möchte und ob er es haben möchte.

Stefan
StefanKittel
Lösung StefanKittel 12.02.2025 aktualisiert um 10:04:10 Uhr
Goto Top
Erstell mit chatgpt o1
#!/usr/bin/env php
<?php
/**
 * parse_postfix_log.php
 *
 * Reads Postfix log lines in ISO-8601 format, aggregates the relevant data
 * by queue ID, and outputs a JSON file "postfix_mails.json" containing 
 * details about each processed email.
 *
 * Usage:
 *   php parse_postfix_log.php /path/to/mail.log /path/to/mail.log.1 [...]
 */
 
 
 // ---------------------------------------------------------------------
// 1) Helper function: Convert ISO-8601 date string to Unix timestamp
// ---------------------------------------------------------------------
function isoToTimestamp($isoString) {
    // If the string is empty or null, return null
    if (!$isoString) {
        return null;
    }

    try {
        $dt = new DateTime($isoString);
        // getTimestamp() returns the integer seconds since epoch
        return $dt->getTimestamp();
    } catch (Exception $e) {
        // On parsing error, return null or handle as needed
        return null;
    }
}

// Check if log files are provided
if ($argc < 2) {
    fwrite(STDERR, "Usage: php parse_postfix_log.php /path/to/mail.log [...]\n");  
    exit(1);
}

// Prepare a regular expression to match ISO-8601 timestamps and Postfix queue lines.
//
// Example line:
// 2025-02-09T19:45:01.585459+00:00 serverXYZ postfix/qmgr[17934]: 5B2423E652: removed
//
// Explanation of the pattern:
// ^(?<timestamp>...)         -> Matches the full date/time with microseconds and timezone
// \s+(?<host>\S+)            -> Captures the hostname
// \s+postfix\/(?<process>...) -> Matches "postfix/<something>[PID]:"  
// \[(?<pid>\d+)\]:\s+        -> Captures the PID, then a colon
// (?<queueid>[A-Za-z0-9]+):\s+ -> Captures the queue ID (up to next colon)
// (?<msg>.*)$                -> The rest of the line ("from=..., to=..., status=..., etc.")  
$logLineRegex = '/^'  
    . '(?<timestamp>\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d+(?:[+\-]\d{2}:\d{2}|Z))'  
    . '\s+(?<host>\S+)'  
    . '\s+postfix\/(?<process>\S+)\[(?<pid>\d+)\]:'  
    . '\s+(?<queueid>[A-Za-z0-9]+):'  
    . '\s+(?<msg>.*)'  
    . '$/';  

// This array will map queue IDs to their collected information
// Example structure:
// $messages['QUEUEID'] = [  
//   'queue_id'      => 'QUEUEID',  
//   'time_received' => '2025-02-09T19:44:59.390574+00:00',  
//   'time_removed'  => null,  
//   'from'          => 'user@example.com',  
//   'tos'           => [  
//       [ 'recipient' => 'rcpt@example.net', 'time_sent' => '...', 'status' => 'sent' ],  
//       ...
//   ],
//   'status'        => null,  
//   'message_id'    => null,  
// ];
$messages = [];

/**
 * Ensures that a queue ID entry exists in $messages.
 */
function ensureMessageEntry(&$messages, $queueId) {
    if (!isset($messages[$queueId])) {
        $messages[$queueId] = [
            'queue_id'      => $queueId,  
            'time_received' => null,  
            'time_removed'  => null,  
            'from'          => null,  
            'tos'           => [],  
            //'status'        => null,  
            'message_id'    => null,  
        ];
    }
}

// Remove the script name from the argument list
array_shift($argv);
$logFiles = $argv;

// Iterate through all provided log files
foreach ($logFiles as $logFile) {
    if (!is_readable($logFile)) {
        fwrite(STDERR, "File not readable: $logFile\n");  
        continue;
    }

    $fh = fopen($logFile, 'r');  
    if (!$fh) {
        fwrite(STDERR, "Failed to open file: $logFile\n");  
        continue;
    }

    while (($line = fgets($fh)) !== false) {
        $line = rtrim($line, "\n");  

        // Match the line against our ISO-8601 Postfix regex
        if (preg_match($logLineRegex, $line, $m)) {
            $queueId   = $m['queueid'];  
            //$timestamp = $m['timestamp'];  
			$isoTime   = $m['timestamp'];  
            $msg       = $m['msg'];  
			
			// Convert to Unix timestamp
            $timestamp = isoToTimestamp($isoTime);

            ensureMessageEntry($messages, $queueId);

            // If this is the first time we see this queue ID, record the 'time_received'  
            if ($messages[$queueId]['time_received'] === null) {  
                $messages[$queueId]['time_received'] = $timestamp;  
            }
			
			$FromUsedInArray =  "";  
			$isInfoHeaderLine = (strpos($msg, 'info: header') !== false);  
			if (!$isInfoHeaderLine)
			{
				// Extract "from=<...>"  
				if (preg_match('/from=<([^>]+)>/', $msg, $fm)) {  
					$messages[$queueId]['from'] = $fm[1];  
				}

				// Extract "to=<...>"  
				if (preg_match('/to=<([^>]+)>/', $msg, $tm)) {  
					// Add a new entry in the "tos" array  
					$FromUsedInArray = $tm[1];
					$messages[$queueId]['tos'][$FromUsedInArray]['events'][] = [  
						//'recipient' => $tm[1],  
						'time_sent' => $timestamp,  
						'status'    => null,  
					];
				}
			}

			/*
            // Extract "status=..." 
            if (preg_match('/status=(\w+)/', $msg, $st)) { 
                $statusFound = $st[1];
                $messages[$queueId]['status'] = $statusFound; 

                // (Optional) update the last "to" entry, if you want per-recipient status 
                $lastToIndex = count($messages[$queueId]['tos']) - 1; 
                if ($lastToIndex >= 0) {
                    $messages[$queueId]['tos'][$lastToIndex]['status'] = $statusFound; 
                }
            }
			*/
			
			// Extract status=... plus optional detail in parentheses
			if (preg_match('/status=(\w+)\s*\((.*?)\)/', $msg, $st)) {  
				// st[1] = "sent" (oder "bounced", etc.)  
				// st[2] = Inhalt in den Klammern, z.B. "250 2.0.0 OK <...> Message queued"  

				$statusFound = $st[1];
				$statusDetail = $st[2];
				
				if (strcasecmp($statusFound, "sent") === 0)  
				{
					$messages[$queueId]['tos'][$FromUsedInArray]['send_send'] = 1;  
					$messages[$queueId]['tos'][$FromUsedInArray]['send_status_detail'] = $statusDetail;  
				}

				//$messages[$queueId]['status'] = $statusFound;  
				//$messages[$queueId]['status_detail'] = $statusDetail;  

				// Optionale Zuordnung zum letzten Empfänger
				if ($FromUsedInArray != "")  
				{
					$lastToIndex = count($messages[$queueId]['tos'][$FromUsedInArray]['events']) - 1;  
					if ($lastToIndex >= 0)
					{
						$messages[$queueId]['tos'][$FromUsedInArray]['events'][$lastToIndex]['status'] = $statusFound;  
						//$messages[$queueId]['tos'][$FromUsedInArray]['status_time'] = $timestamp;  

						// Falls du den Detail-Text je Empfänger tracken möchtest:
						$messages[$queueId]['tos'][$FromUsedInArray]['events'][$lastToIndex]['status_detail'] = $statusDetail;  
						
					}
				}

			} elseif (preg_match('/status=(\w+)/', $msg, $st)) {  
				// Hier gibt es keinen Klammerteil, nur "status=sent" o. Ä.  
				$statusFound = $st[1];
				//$messages[$queueId]['status'] = $statusFound;  
				//$messages[$queueId]['status_time'] = $timestamp;  
				
				if (strcasecmp($statusFound, "sent") === 0)  
				{
					$messages[$queueId]['tos'][$FromUsedInArray]['send_send'] = 1;  
					$messages[$queueId]['tos'][$FromUsedInArray]['send_status_detail'] = $statusDetail;  
				}
				

				// Optional dem letzten Empfänger zuordnen
				if ($FromUsedInArray != "")  
				{
					$lastToIndex = count($messages[$queueId]['tos'][$FromUsedInArray]['events']) - 1;  
					if ($lastToIndex >= 0) {
						$messages[$queueId]['tos'][$FromUsedInArray]['events'][$lastToIndex]['status'] = $statusFound;  
						//$messages[$queueId]['tos'][$lastToIndex]['status_time'] = $timestamp;  
					}
				}
			}
			

            // Extract "message-id=<...>"  
            if (preg_match('/message-id=<([^>]+)>/', $msg, $mid)) {  
                $messages[$queueId]['message_id'] = $mid[1];  
            }
			
			// Extract Subject from "!!SUBJECT_START!!...!!SUBJECT_END!!"  
            if (preg_match('/!!SUBJECT_START!!(.*?)!!SUBJECT_END!!/', $msg, $sub)) {  
                $messages[$queueId]['subject'] = $sub[1];  
            }
			
			if (!array_key_exists('files', $messages[$queueId]))  
			{
				$messages[$queueId]['files']=array();  
			}
			
			// Extract filenames
            if (preg_match('/!!ATTACHMENT1_START!!(.*?)!!ATTACHMENT1_END!!/', $msg, $sub)) {  
				if (!in_array($sub[1], $messages[$queueId]['files']))  
				{
					$messages[$queueId]['files'][] = $sub[1];  
				}
            }
            if (preg_match('/!!ATTACHMENT2_START!!(.*?)!!ATTACHMENT2_END!!/', $msg, $sub)) {  
				if (!in_array($sub[1], $messages[$queueId]['files']))  
				{
					$messages[$queueId]['files'][] = $sub[1];  
				}
            }
			
			
			
			

            // Check if the mail is removed from the queue
            if (strpos($msg, 'removed') !== false) {  
                $messages[$queueId]['time_removed'] = $timestamp;  
            }
        }
    }
    fclose($fh);
}

// Convert the associative array to a simple numerical array
$result = array_values($messages);

// Write the results to a JSON file
file_put_contents('postfix_mails.json', json_encode($result, JSON_PRETTY_PRINT));  

// Print a confirmation message
echo "Done. Results stored in postfix_mails.json\n";  
exit(0);