first commit
This commit is contained in:
Binary file not shown.
@@ -0,0 +1,801 @@
|
||||
import de.vertico.starface.module.core.model.VariableType;
|
||||
import de.vertico.starface.module.core.model.Visibility;
|
||||
import de.vertico.starface.module.core.runtime.IBaseExecutable;
|
||||
import de.vertico.starface.module.core.runtime.IRuntimeEnvironment;
|
||||
import de.vertico.starface.module.core.runtime.annotations.Function;
|
||||
import de.vertico.starface.module.core.runtime.annotations.InputVar;
|
||||
import de.vertico.starface.module.core.runtime.annotations.OutputVar;
|
||||
|
||||
import javax.mail.*;
|
||||
import javax.mail.internet.MimeBodyPart;
|
||||
import javax.mail.search.FlagTerm;
|
||||
import java.io.*;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
/**
|
||||
* Mail2FaxBlock - STARFACE Custom Block
|
||||
*
|
||||
* Ruft E-Mails ab und sendet PDF-Anhänge als Fax.
|
||||
* Die Ziel-Faxnummer wird aus dem E-Mail-Betreff gelesen.
|
||||
*
|
||||
* Für STARFACE 8.x, 9.x, 10.x (Java 21)
|
||||
*/
|
||||
@Function(
|
||||
visibility = Visibility.Private,
|
||||
description = "Ruft E-Mails ab und sendet PDF-Anhänge als Fax"
|
||||
)
|
||||
public class Mail2FaxBlock implements IBaseExecutable {
|
||||
|
||||
// Fax result constants (instead of inner class to avoid separate .class file)
|
||||
private static final int FAX_SUCCESS = 0;
|
||||
private static final int FAX_RETRY = 1;
|
||||
private static final int FAX_FAIL = 2;
|
||||
|
||||
// Lock um parallele Ausführung zu verhindern
|
||||
private static final ReentrantLock executionLock = new ReentrantLock();
|
||||
|
||||
// Pfade für Tracking-Dateien
|
||||
private static final String DATA_DIR = "/var/starface/module-data";
|
||||
private static final String PROCESSED_FILE = DATA_DIR + "/mail2fax_processed.txt";
|
||||
private static final String RETRY_FILE = DATA_DIR + "/mail2fax_retry.txt";
|
||||
|
||||
// ============================================================
|
||||
// INPUT VARIABLEN - werden im Module Designer konfiguriert
|
||||
// ============================================================
|
||||
|
||||
@InputVar(
|
||||
label = "Mail Server",
|
||||
description = "IMAP oder POP3 Server Adresse",
|
||||
type = VariableType.STRING
|
||||
)
|
||||
public String mailServer = "";
|
||||
|
||||
@InputVar(
|
||||
label = "Mail Port",
|
||||
description = "Server Port (993 für IMAPS, 995 für POP3S, 143 für IMAP, 110 für POP3)",
|
||||
type = VariableType.NUMBER
|
||||
)
|
||||
public Integer mailPort = 993;
|
||||
|
||||
@InputVar(
|
||||
label = "Protokoll",
|
||||
description = "IMAP oder POP3",
|
||||
type = VariableType.STRING
|
||||
)
|
||||
public String mailProtocol = "IMAP";
|
||||
|
||||
@InputVar(
|
||||
label = "Benutzername",
|
||||
description = "E-Mail Benutzername",
|
||||
type = VariableType.STRING
|
||||
)
|
||||
public String mailUsername = "";
|
||||
|
||||
@InputVar(
|
||||
label = "Passwort",
|
||||
description = "E-Mail Passwort",
|
||||
type = VariableType.STRING
|
||||
)
|
||||
public String mailPassword = "";
|
||||
|
||||
@InputVar(
|
||||
label = "SSL verwenden",
|
||||
description = "SSL/TLS für die Verbindung aktivieren",
|
||||
type = VariableType.BOOLEAN
|
||||
)
|
||||
public Boolean mailUseSsl = true;
|
||||
|
||||
@InputVar(
|
||||
label = "Ordner",
|
||||
description = "E-Mail Ordner (Standard: INBOX)",
|
||||
type = VariableType.STRING
|
||||
)
|
||||
public String mailFolder = "INBOX";
|
||||
|
||||
@InputVar(
|
||||
label = "Nach Verarbeitung löschen",
|
||||
description = "E-Mails nach erfolgreicher Verarbeitung löschen (nur IMAP, bei POP3 werden E-Mails immer nach erfolgreichem Versand gelöscht)",
|
||||
type = VariableType.BOOLEAN
|
||||
)
|
||||
public Boolean deleteAfterProcess = false;
|
||||
|
||||
@InputVar(
|
||||
label = "Fax-Benutzer",
|
||||
description = "STARFACE Benutzer für den Fax-Versand (muss Fax-Berechtigung haben)",
|
||||
type = VariableType.STARFACE_USER
|
||||
)
|
||||
public Integer faxAccountId = 0;
|
||||
|
||||
@InputVar(
|
||||
label = "Absender-Faxnummer",
|
||||
description = "Ausgehende Faxnummer (z.B. +49721123456)",
|
||||
type = VariableType.STRING
|
||||
)
|
||||
public String faxSenderNumber = "";
|
||||
|
||||
@InputVar(
|
||||
label = "Erlaubte Absender",
|
||||
description = "Komma-getrennte Liste erlaubter Absender-Adressen (leer = alle erlaubt)",
|
||||
type = VariableType.STRING
|
||||
)
|
||||
public String authorizedSenders = "";
|
||||
|
||||
@InputVar(
|
||||
label = "PIN",
|
||||
description = "Sicherheits-PIN (optional). Wenn gesetzt, muss die PIN im E-Mail-Text enthalten sein um das Fax zu senden.",
|
||||
type = VariableType.STRING
|
||||
)
|
||||
public String pin = "";
|
||||
|
||||
@InputVar(
|
||||
label = "Max. Wiederholungen",
|
||||
description = "Maximale Anzahl Wiederholungsversuche bei besetzter Leitung",
|
||||
type = VariableType.NUMBER
|
||||
)
|
||||
public Integer maxRetries = 3;
|
||||
|
||||
@InputVar(
|
||||
label = "Wartezeit (Minuten)",
|
||||
description = "Minuten zwischen Wiederholungsversuchen",
|
||||
type = VariableType.NUMBER
|
||||
)
|
||||
public Integer retryDelayMinutes = 5;
|
||||
|
||||
// ============================================================
|
||||
// OUTPUT VARIABLEN - Ergebnisse der Ausführung
|
||||
// ============================================================
|
||||
|
||||
@OutputVar(
|
||||
label = "Verarbeitete E-Mails",
|
||||
description = "Anzahl der verarbeiteten E-Mails",
|
||||
type = VariableType.NUMBER
|
||||
)
|
||||
public Integer processedCount = 0;
|
||||
|
||||
@OutputVar(
|
||||
label = "Gesendete Faxe",
|
||||
description = "Anzahl erfolgreich gesendeter Faxe",
|
||||
type = VariableType.NUMBER
|
||||
)
|
||||
public Integer sentFaxCount = 0;
|
||||
|
||||
@OutputVar(
|
||||
label = "Fehleranzahl",
|
||||
description = "Anzahl der Fehler",
|
||||
type = VariableType.NUMBER
|
||||
)
|
||||
public Integer errorCount = 0;
|
||||
|
||||
@OutputVar(
|
||||
label = "Wartende Wiederholungen",
|
||||
description = "Anzahl der Faxe die auf Wiederholung warten",
|
||||
type = VariableType.NUMBER
|
||||
)
|
||||
public Integer pendingRetries = 0;
|
||||
|
||||
@OutputVar(
|
||||
label = "Status",
|
||||
description = "Status-Meldung der letzten Ausführung",
|
||||
type = VariableType.STRING
|
||||
)
|
||||
public String statusMessage = "";
|
||||
|
||||
// Runtime Environment
|
||||
private IRuntimeEnvironment runtime;
|
||||
private org.apache.logging.log4j.Logger log;
|
||||
|
||||
@Override
|
||||
public void execute(IRuntimeEnvironment runtime) throws Exception {
|
||||
this.runtime = runtime;
|
||||
this.log = runtime.getLog();
|
||||
|
||||
// Pflichtfelder validieren
|
||||
List<String> missingFields = new ArrayList<>();
|
||||
|
||||
if (mailServer == null || mailServer.trim().isEmpty()) {
|
||||
missingFields.add("mailServer (Mail Server)");
|
||||
}
|
||||
if (mailPort == null || mailPort <= 0) {
|
||||
missingFields.add("mailPort (Port)");
|
||||
}
|
||||
if (mailUsername == null || mailUsername.trim().isEmpty()) {
|
||||
missingFields.add("mailUsername (Benutzername)");
|
||||
}
|
||||
if (mailPassword == null || mailPassword.trim().isEmpty()) {
|
||||
missingFields.add("mailPassword (Passwort)");
|
||||
}
|
||||
if (faxAccountId == null || faxAccountId <= 0) {
|
||||
missingFields.add("faxAccountId (Fax-Benutzer)");
|
||||
}
|
||||
if (faxSenderNumber == null || faxSenderNumber.trim().isEmpty()) {
|
||||
missingFields.add("faxSenderNumber (Absender-Faxnummer)");
|
||||
}
|
||||
|
||||
if (!missingFields.isEmpty()) {
|
||||
statusMessage = "Konfigurationsfehler: Pflichtfelder nicht ausgefüllt";
|
||||
log.error("Mail2Fax: " + statusMessage + ": " + String.join(", ", missingFields));
|
||||
return;
|
||||
}
|
||||
|
||||
// Versuche Lock zu bekommen - wenn nicht verfügbar, läuft bereits eine Instanz
|
||||
if (!executionLock.tryLock()) {
|
||||
log.info("Mail2Fax: Bereits eine Instanz aktiv, überspringe Ausführung");
|
||||
statusMessage = "Übersprungen - bereits aktiv";
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
log.info("Mail2Fax: Starte E-Mail-Abruf von " + mailServer);
|
||||
|
||||
// Datenverzeichnis erstellen
|
||||
Files.createDirectories(Paths.get(DATA_DIR));
|
||||
|
||||
// Zuerst Retry-Queue verarbeiten
|
||||
processRetryQueue();
|
||||
|
||||
// Dann neue E-Mails abrufen
|
||||
fetchAndProcessEmails();
|
||||
|
||||
statusMessage = String.format(
|
||||
"Fertig: %d E-Mails verarbeitet, %d Faxe gesendet, %d Fehler, %d warten auf Retry",
|
||||
processedCount, sentFaxCount, errorCount, pendingRetries
|
||||
);
|
||||
log.info("Mail2Fax: " + statusMessage);
|
||||
|
||||
} finally {
|
||||
executionLock.unlock();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet die Retry-Queue für fehlgeschlagene Faxe
|
||||
*/
|
||||
private void processRetryQueue() {
|
||||
try {
|
||||
Path retryPath = Paths.get(RETRY_FILE);
|
||||
if (!Files.exists(retryPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
List<String> lines = Files.readAllLines(retryPath);
|
||||
List<String> remaining = new ArrayList<>();
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
for (String line : lines) {
|
||||
if (line.trim().isEmpty()) continue;
|
||||
|
||||
// Format: timestamp|retryCount|destination|senderNumber|pdfPath|messageId
|
||||
String[] parts = line.split("\\|", 6);
|
||||
if (parts.length < 6) continue;
|
||||
|
||||
long scheduledTime = Long.parseLong(parts[0]);
|
||||
int retryCount = Integer.parseInt(parts[1]);
|
||||
String destination = parts[2];
|
||||
String senderNumber = parts[3];
|
||||
String pdfPath = parts[4];
|
||||
String messageId = parts[5];
|
||||
|
||||
if (now < scheduledTime) {
|
||||
// Noch nicht Zeit für Retry
|
||||
remaining.add(line);
|
||||
pendingRetries++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Retry durchführen
|
||||
File pdfFile = new File(pdfPath);
|
||||
if (!pdfFile.exists()) {
|
||||
log.warn("Mail2Fax: Retry-PDF nicht mehr vorhanden: " + pdfPath);
|
||||
continue;
|
||||
}
|
||||
|
||||
log.info("Mail2Fax: Retry #" + retryCount + " für " + destination);
|
||||
int result = sendFax(destination, senderNumber, pdfFile);
|
||||
|
||||
if (result == FAX_SUCCESS) {
|
||||
sentFaxCount++;
|
||||
pdfFile.delete(); // Temp-Datei löschen
|
||||
markAsProcessed(messageId);
|
||||
} else if (result == FAX_RETRY && retryCount < maxRetries) {
|
||||
// Erneut in Queue
|
||||
long nextRetry = now + (retryDelayMinutes * 60 * 1000L);
|
||||
remaining.add(nextRetry + "|" + (retryCount + 1) + "|" + destination + "|" +
|
||||
senderNumber + "|" + pdfPath + "|" + messageId);
|
||||
pendingRetries++;
|
||||
} else {
|
||||
log.error("Mail2Fax: Endgültig fehlgeschlagen nach " + retryCount + " Versuchen: " + destination);
|
||||
errorCount++;
|
||||
pdfFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
// Aktualisierte Queue speichern
|
||||
Files.write(retryPath, remaining);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Mail2Fax: Fehler bei Retry-Queue: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ruft E-Mails ab und verarbeitet sie
|
||||
*/
|
||||
private void fetchAndProcessEmails() {
|
||||
Store store = null;
|
||||
Folder folder = null;
|
||||
|
||||
try {
|
||||
// Session konfigurieren
|
||||
Properties props = new Properties();
|
||||
String protocol = mailProtocol.toUpperCase();
|
||||
|
||||
if (protocol.equals("IMAP")) {
|
||||
if (mailUseSsl) {
|
||||
props.put("mail.store.protocol", "imaps");
|
||||
props.put("mail.imaps.host", mailServer);
|
||||
props.put("mail.imaps.port", String.valueOf(mailPort));
|
||||
props.put("mail.imaps.ssl.enable", "true");
|
||||
} else {
|
||||
props.put("mail.store.protocol", "imap");
|
||||
props.put("mail.imap.host", mailServer);
|
||||
props.put("mail.imap.port", String.valueOf(mailPort));
|
||||
}
|
||||
} else { // POP3
|
||||
if (mailUseSsl) {
|
||||
props.put("mail.store.protocol", "pop3s");
|
||||
props.put("mail.pop3s.host", mailServer);
|
||||
props.put("mail.pop3s.port", String.valueOf(mailPort));
|
||||
props.put("mail.pop3s.ssl.enable", "true");
|
||||
} else {
|
||||
props.put("mail.store.protocol", "pop3");
|
||||
props.put("mail.pop3.host", mailServer);
|
||||
props.put("mail.pop3.port", String.valueOf(mailPort));
|
||||
}
|
||||
}
|
||||
|
||||
Session session = Session.getInstance(props);
|
||||
store = session.getStore();
|
||||
store.connect(mailServer, mailPort, mailUsername, mailPassword);
|
||||
|
||||
folder = store.getFolder(mailFolder);
|
||||
folder.open(Folder.READ_WRITE);
|
||||
|
||||
// Nachrichten abrufen
|
||||
Message[] messages;
|
||||
if (protocol.equals("IMAP")) {
|
||||
// IMAP: Nur ungelesene
|
||||
messages = folder.search(new FlagTerm(new Flags(Flags.Flag.SEEN), false));
|
||||
} else {
|
||||
// POP3: Alle (Tracking über processed-Liste)
|
||||
messages = folder.getMessages();
|
||||
}
|
||||
|
||||
log.info("Mail2Fax: " + messages.length + " Nachrichten gefunden");
|
||||
|
||||
for (Message message : messages) {
|
||||
try {
|
||||
processMessage(message, protocol.equals("POP3"), folder);
|
||||
processedCount++;
|
||||
} catch (Exception e) {
|
||||
log.error("Mail2Fax: Fehler bei Nachricht: " + e.getMessage(), e);
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
// Bei Verbindungsfehlern nur eine saubere Meldung ohne Stacktrace
|
||||
String msg = e.getMessage();
|
||||
if (e instanceof com.sun.mail.util.MailConnectException ||
|
||||
e instanceof java.net.ConnectException ||
|
||||
e instanceof java.net.UnknownHostException ||
|
||||
e instanceof java.net.SocketTimeoutException ||
|
||||
(msg != null && (msg.contains("Connection refused") ||
|
||||
msg.contains("connect") ||
|
||||
msg.contains("timeout") ||
|
||||
msg.contains("Unknown host")))) {
|
||||
log.error("Mail2Fax: Verbindung zu " + mailServer + ":" + mailPort + " fehlgeschlagen - " + msg);
|
||||
} else {
|
||||
log.error("Mail2Fax: E-Mail-Abruf fehlgeschlagen: " + msg, e);
|
||||
}
|
||||
errorCount++;
|
||||
statusMessage = "Fehler: " + msg;
|
||||
} finally {
|
||||
try {
|
||||
if (folder != null && folder.isOpen()) {
|
||||
folder.close(true); // expunge deleted messages
|
||||
}
|
||||
if (store != null) {
|
||||
store.close();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Mail2Fax: Fehler beim Schließen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verarbeitet eine einzelne E-Mail
|
||||
*/
|
||||
private void processMessage(Message message, boolean isPop3, Folder folder) throws Exception {
|
||||
String messageId = getMessageId(message);
|
||||
|
||||
// Bei POP3: Prüfen ob bereits verarbeitet
|
||||
if (isPop3 && isAlreadyProcessed(messageId)) {
|
||||
log.debug("Mail2Fax: Nachricht bereits verarbeitet: " + messageId);
|
||||
return;
|
||||
}
|
||||
|
||||
String from = message.getFrom()[0].toString();
|
||||
String subject = message.getSubject();
|
||||
|
||||
log.info("Mail2Fax: Verarbeite E-Mail von " + from + " - Betreff: " + subject);
|
||||
|
||||
// Absender prüfen
|
||||
if (!isAuthorizedSender(from)) {
|
||||
log.warn("Mail2Fax: Nicht autorisierter Absender: " + from);
|
||||
if (!isPop3) {
|
||||
message.setFlag(Flags.Flag.SEEN, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// PIN prüfen (wenn gesetzt)
|
||||
if (pin != null && !pin.trim().isEmpty()) {
|
||||
String emailBody = getEmailBodyText(message);
|
||||
if (emailBody == null || !emailBody.contains(pin)) {
|
||||
log.error("Mail2Fax: PIN in Emailtext nicht vorhanden oder falsch");
|
||||
if (!isPop3) {
|
||||
message.setFlag(Flags.Flag.SEEN, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
log.debug("Mail2Fax: PIN erfolgreich validiert");
|
||||
}
|
||||
|
||||
// Ziel-Faxnummer aus Betreff extrahieren
|
||||
String destination = extractFaxNumber(subject);
|
||||
if (destination == null || destination.isEmpty()) {
|
||||
log.warn("Mail2Fax: Keine gültige Faxnummer im Betreff: " + subject);
|
||||
if (!isPop3) {
|
||||
message.setFlag(Flags.Flag.SEEN, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// PDF-Anhänge suchen und verarbeiten
|
||||
List<File> pdfFiles = extractPdfAttachments(message);
|
||||
if (pdfFiles.isEmpty()) {
|
||||
log.warn("Mail2Fax: Keine PDF-Anhänge gefunden");
|
||||
if (!isPop3) {
|
||||
message.setFlag(Flags.Flag.SEEN, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Jeden PDF-Anhang als Fax senden
|
||||
boolean allSuccess = true;
|
||||
for (File pdfFile : pdfFiles) {
|
||||
int result = sendFax(destination, faxSenderNumber, pdfFile);
|
||||
|
||||
if (result == FAX_SUCCESS) {
|
||||
sentFaxCount++;
|
||||
pdfFile.delete();
|
||||
} else if (result == FAX_RETRY) {
|
||||
// In Retry-Queue aufnehmen
|
||||
addToRetryQueue(destination, faxSenderNumber, pdfFile, messageId);
|
||||
allSuccess = false;
|
||||
} else {
|
||||
errorCount++;
|
||||
pdfFile.delete();
|
||||
allSuccess = false;
|
||||
}
|
||||
}
|
||||
|
||||
// E-Mail als verarbeitet markieren
|
||||
if (allSuccess) {
|
||||
if (isPop3) {
|
||||
// POP3: Als verarbeitet speichern und löschen
|
||||
markAsProcessed(messageId);
|
||||
message.setFlag(Flags.Flag.DELETED, true);
|
||||
log.info("Mail2Fax: POP3-Nachricht zum Löschen markiert");
|
||||
} else {
|
||||
// IMAP: Als gelesen markieren, optional löschen
|
||||
message.setFlag(Flags.Flag.SEEN, true);
|
||||
if (deleteAfterProcess) {
|
||||
message.setFlag(Flags.Flag.DELETED, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Ermittelt die Message-ID einer E-Mail
|
||||
*/
|
||||
private String getMessageId(Message message) {
|
||||
try {
|
||||
String[] headers = message.getHeader("Message-ID");
|
||||
if (headers != null && headers.length > 0) {
|
||||
return headers[0];
|
||||
}
|
||||
// Fallback: Hash aus Datum und Betreff
|
||||
return String.valueOf((message.getSentDate() + "|" + message.getSubject()).hashCode());
|
||||
} catch (Exception e) {
|
||||
return String.valueOf(System.currentTimeMillis());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob eine Nachricht bereits verarbeitet wurde (für POP3)
|
||||
*/
|
||||
private boolean isAlreadyProcessed(String messageId) {
|
||||
try {
|
||||
Path path = Paths.get(PROCESSED_FILE);
|
||||
if (!Files.exists(path)) {
|
||||
return false;
|
||||
}
|
||||
List<String> processed = Files.readAllLines(path);
|
||||
return processed.contains(messageId);
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Markiert eine Nachricht als verarbeitet (für POP3)
|
||||
*/
|
||||
private void markAsProcessed(String messageId) {
|
||||
try {
|
||||
Path path = Paths.get(PROCESSED_FILE);
|
||||
List<String> processed = Files.exists(path) ?
|
||||
new ArrayList<>(Files.readAllLines(path)) : new ArrayList<>();
|
||||
|
||||
if (!processed.contains(messageId)) {
|
||||
processed.add(messageId);
|
||||
// Maximal 1000 Einträge behalten
|
||||
while (processed.size() > 1000) {
|
||||
processed.remove(0);
|
||||
}
|
||||
Files.write(path, processed);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Mail2Fax: Konnte Nachricht nicht als verarbeitet markieren: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fügt ein fehlgeschlagenes Fax zur Retry-Queue hinzu
|
||||
*/
|
||||
private void addToRetryQueue(String destination, String senderNumber, File pdfFile, String messageId) {
|
||||
try {
|
||||
long nextRetry = System.currentTimeMillis() + (retryDelayMinutes * 60 * 1000L);
|
||||
String entry = nextRetry + "|1|" + destination + "|" + senderNumber + "|" +
|
||||
pdfFile.getAbsolutePath() + "|" + messageId;
|
||||
|
||||
Path path = Paths.get(RETRY_FILE);
|
||||
List<String> entries = Files.exists(path) ?
|
||||
new ArrayList<>(Files.readAllLines(path)) : new ArrayList<>();
|
||||
entries.add(entry);
|
||||
Files.write(path, entries);
|
||||
|
||||
pendingRetries++;
|
||||
log.info("Mail2Fax: Fax zur Retry-Queue hinzugefügt: " + destination);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("Mail2Fax: Konnte nicht zur Retry-Queue hinzufügen: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Prüft ob der Absender autorisiert ist
|
||||
*/
|
||||
private boolean isAuthorizedSender(String from) {
|
||||
if (authorizedSenders == null || authorizedSenders.trim().isEmpty()) {
|
||||
return true; // Keine Einschränkung
|
||||
}
|
||||
|
||||
String fromLower = from.toLowerCase();
|
||||
String[] authorized = authorizedSenders.split(",");
|
||||
|
||||
for (String auth : authorized) {
|
||||
if (fromLower.contains(auth.trim().toLowerCase())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert die Faxnummer aus dem E-Mail-Betreff
|
||||
*/
|
||||
private String extractFaxNumber(String subject) {
|
||||
if (subject == null || subject.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Entferne alle Zeichen außer Ziffern und +
|
||||
String cleaned = subject.replaceAll("[^0-9+]", "");
|
||||
|
||||
// Prüfe auf gültiges Format
|
||||
if (cleaned.matches("\\+?[0-9]{6,}")) {
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert den Textinhalt einer E-Mail (für PIN-Prüfung)
|
||||
*/
|
||||
private String getEmailBodyText(Message message) {
|
||||
try {
|
||||
Object content = message.getContent();
|
||||
|
||||
if (content instanceof String) {
|
||||
return (String) content;
|
||||
} else if (content instanceof Multipart) {
|
||||
Multipart multipart = (Multipart) content;
|
||||
StringBuilder bodyText = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < multipart.getCount(); i++) {
|
||||
BodyPart bodyPart = multipart.getBodyPart(i);
|
||||
String contentType = bodyPart.getContentType().toLowerCase();
|
||||
|
||||
// Nur Text-Parts auslesen, keine Anhänge
|
||||
if (contentType.startsWith("text/plain") || contentType.startsWith("text/html")) {
|
||||
Object partContent = bodyPart.getContent();
|
||||
if (partContent instanceof String) {
|
||||
bodyText.append((String) partContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
return bodyText.toString();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("Mail2Fax: Fehler beim Lesen des E-Mail-Textes: " + e.getMessage());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extrahiert PDF-Anhänge aus der E-Mail
|
||||
*/
|
||||
private List<File> extractPdfAttachments(Message message) throws Exception {
|
||||
List<File> pdfFiles = new ArrayList<>();
|
||||
|
||||
Object content = message.getContent();
|
||||
if (content instanceof Multipart) {
|
||||
Multipart multipart = (Multipart) content;
|
||||
|
||||
for (int i = 0; i < multipart.getCount(); i++) {
|
||||
BodyPart bodyPart = multipart.getBodyPart(i);
|
||||
|
||||
if (Part.ATTACHMENT.equalsIgnoreCase(bodyPart.getDisposition()) ||
|
||||
(bodyPart.getFileName() != null && bodyPart.getFileName().toLowerCase().endsWith(".pdf"))) {
|
||||
|
||||
String fileName = bodyPart.getFileName();
|
||||
if (fileName != null && fileName.toLowerCase().endsWith(".pdf")) {
|
||||
// Temp-Datei erstellen
|
||||
File tempFile = File.createTempFile("mail2fax_", ".pdf");
|
||||
|
||||
try (InputStream is = bodyPart.getInputStream();
|
||||
FileOutputStream fos = new FileOutputStream(tempFile)) {
|
||||
byte[] buffer = new byte[4096];
|
||||
int bytesRead;
|
||||
while ((bytesRead = is.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, bytesRead);
|
||||
}
|
||||
}
|
||||
|
||||
log.info("Mail2Fax: PDF extrahiert: " + fileName);
|
||||
pdfFiles.add(tempFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pdfFiles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sendet ein PDF als Fax über die STARFACE API
|
||||
*
|
||||
* Verwendet den internen FaxHandler über den StarfaceComponentProvider.
|
||||
*
|
||||
* @return FAX_SUCCESS, FAX_RETRY oder FAX_FAIL
|
||||
*/
|
||||
private int sendFax(String destination, String callingNumber, File pdfFile) {
|
||||
try {
|
||||
log.info("Mail2Fax: Sende Fax an " + destination + " von " + callingNumber);
|
||||
|
||||
// Den eingebauten SendFax-Block direkt instanziieren und aufrufen
|
||||
Class<?> sendFaxClass = Class.forName(
|
||||
"de.vertico.starface.module.core.runtime.functions.callHandling.call.SendFax"
|
||||
);
|
||||
|
||||
Object sendFaxBlock = sendFaxClass.getDeclaredConstructor().newInstance();
|
||||
|
||||
// Input-Variablen setzen via Reflection
|
||||
// accountId (Integer) - Benutzer-ID für Fax-Versand
|
||||
setFieldValue(sendFaxClass, sendFaxBlock, "accountId", faxAccountId);
|
||||
|
||||
// extention (String) - Ziel-Faxnummer (Tippfehler im Original!)
|
||||
setFieldValue(sendFaxClass, sendFaxBlock, "extention", destination);
|
||||
|
||||
// signalNumber (String) - Absender-Nummer
|
||||
setFieldValue(sendFaxClass, sendFaxBlock, "signalNumber", callingNumber);
|
||||
|
||||
// signalName (String) - Absender-Name (optional, gleich wie Nummer)
|
||||
setFieldValue(sendFaxClass, sendFaxBlock, "signalName", callingNumber);
|
||||
|
||||
// resource (String) - Pfad zur PDF-Datei
|
||||
setFieldValue(sendFaxClass, sendFaxBlock, "resource", pdfFile.getAbsolutePath());
|
||||
|
||||
// execute() aufrufen mit unserem Runtime
|
||||
java.lang.reflect.Method executeMethod = sendFaxClass.getMethod("execute",
|
||||
Class.forName("de.vertico.starface.module.core.runtime.IRuntimeEnvironment"));
|
||||
executeMethod.invoke(sendFaxBlock, runtime);
|
||||
|
||||
// Ergebnis prüfen (exitStatus Feld)
|
||||
try {
|
||||
java.lang.reflect.Field exitStatusField = sendFaxClass.getDeclaredField("exitStatus");
|
||||
exitStatusField.setAccessible(true);
|
||||
Object exitStatus = exitStatusField.get(sendFaxBlock);
|
||||
log.info("Mail2Fax: SendFax exitStatus: " + exitStatus);
|
||||
|
||||
if (exitStatus != null && exitStatus.toString().contains("FAILED")) {
|
||||
return FAX_RETRY;
|
||||
}
|
||||
} catch (NoSuchFieldException e) {
|
||||
log.debug("Mail2Fax: Kein exitStatus Feld");
|
||||
}
|
||||
|
||||
log.info("Mail2Fax: Fax erfolgreich gesendet an " + destination);
|
||||
return FAX_SUCCESS;
|
||||
|
||||
} catch (Exception e) {
|
||||
String error = e.getMessage() != null ? e.getMessage().toLowerCase() : "";
|
||||
log.error("Mail2Fax: Fax-Fehler: " + e.getMessage(), e);
|
||||
|
||||
// Prüfe ob Retry sinnvoll ist
|
||||
if (error.contains("busy") || error.contains("besetzt") ||
|
||||
error.contains("no answer") || error.contains("keine antwort") ||
|
||||
error.contains("temporarily") || error.contains("timeout")) {
|
||||
log.info("Mail2Fax: Temporärer Fehler, wird erneut versucht");
|
||||
return FAX_RETRY;
|
||||
}
|
||||
|
||||
return FAX_FAIL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Versucht ein Feld zu setzen, gibt true zurück wenn erfolgreich
|
||||
*/
|
||||
private boolean trySetField(Class<?> clazz, Object obj, String fieldName, Object value) {
|
||||
try {
|
||||
java.lang.reflect.Field field = clazz.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(obj, value);
|
||||
log.debug("Mail2Fax: " + fieldName + " gesetzt: " + value);
|
||||
return true;
|
||||
} catch (NoSuchFieldException e) {
|
||||
return false;
|
||||
} catch (Exception e) {
|
||||
log.warn("Mail2Fax: Fehler beim Setzen von " + fieldName + ": " + e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Setzt ein Feld oder wirft Exception
|
||||
*/
|
||||
private void setFieldValue(Class<?> clazz, Object obj, String fieldName, Object value) throws Exception {
|
||||
java.lang.reflect.Field field = clazz.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(obj, value);
|
||||
log.debug("Mail2Fax: " + fieldName + " gesetzt: " + value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
# Mail2FaxBlock - STARFACE Custom Block
|
||||
|
||||
Ein Custom Block für den STARFACE Module Designer, der E-Mails abruft und PDF-Anhänge als Fax versendet.
|
||||
|
||||
## Features
|
||||
|
||||
- **IMAP/POP3 Unterstützung** mit SSL/TLS
|
||||
- **POP3**: E-Mails werden IMMER nach erfolgreichem Versand gelöscht (+ Tracking um Duplikate zu vermeiden)
|
||||
- **IMAP**: E-Mails werden als gelesen markiert (optional löschen)
|
||||
- **Retry-Logik**: Bei besetzter Leitung oder Fehler wird automatisch erneut versucht
|
||||
- **Konfigurierbare Wiederholungen**: Anzahl und Wartezeit einstellbar
|
||||
- **PIN-Schutz**: Optionale Sicherheits-PIN im E-Mail-Text erforderlich
|
||||
|
||||
## Kompatibilität
|
||||
|
||||
- STARFACE 8.x, 9.x, 10.x (Java 21)
|
||||
|
||||
## Dateien
|
||||
|
||||
```
|
||||
v8-9-10/
|
||||
├── Mail2FaxBlock.java # Quellcode des Custom Blocks
|
||||
├── build-block.sh # Kompilier-Script
|
||||
├── libs/starface/ # STARFACE API JARs
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## Build
|
||||
|
||||
### 1. STARFACE APIs holen (falls noch nicht geschehen)
|
||||
|
||||
```bash
|
||||
cd ..
|
||||
./fetch-starface-libs.sh <starface-ip>
|
||||
```
|
||||
|
||||
### 2. Block kompilieren
|
||||
|
||||
```bash
|
||||
./build-block.sh
|
||||
```
|
||||
|
||||
Ergebnis: `Mail2FaxBlock.class`
|
||||
|
||||
## Installation im Module Designer
|
||||
|
||||
### 1. Neues Modul erstellen
|
||||
- STARFACE Admin → Module → Module Designer
|
||||
- "Neues Modul erstellen"
|
||||
- Name: "Mail2Fax"
|
||||
|
||||
### 2. Block hochladen
|
||||
- Tab "Ressourcen"
|
||||
- "Datei hochladen" → `Mail2FaxBlock.class`
|
||||
|
||||
### 3. Funktion erstellen
|
||||
- Tab "Funktionen"
|
||||
- Neue Funktion erstellen
|
||||
- Den hochgeladenen Block als Implementierung auswählen
|
||||
|
||||
### 4. Eingabe-Variablen konfigurieren
|
||||
|
||||
| Variable | Typ | Beschreibung | Default |
|
||||
|----------|-----|--------------|---------|
|
||||
| mailServer | STRING | IMAP/POP3 Server | |
|
||||
| mailPort | NUMBER | Port | 993 |
|
||||
| mailProtocol | STRING | "IMAP" oder "POP3" | IMAP |
|
||||
| mailUsername | STRING | E-Mail Benutzer | |
|
||||
| mailPassword | STRING | E-Mail Passwort | |
|
||||
| mailUseSsl | BOOLEAN | SSL aktivieren | true |
|
||||
| mailFolder | STRING | Ordner | INBOX |
|
||||
| deleteAfterProcess | BOOLEAN | E-Mails löschen (nur IMAP) | false |
|
||||
| faxAccountId | STARFACE_USER | Fax-Benutzer (Dropdown) | |
|
||||
| faxSenderNumber | STRING | Absender-Faxnummer | |
|
||||
| authorizedSenders | STRING | Erlaubte Absender (optional) | |
|
||||
| pin | STRING | Sicherheits-PIN (optional) | |
|
||||
| maxRetries | NUMBER | Max. Wiederholungsversuche | 3 |
|
||||
| retryDelayMinutes | NUMBER | Minuten zwischen Versuchen | 5 |
|
||||
|
||||
### 5. Timer konfigurieren
|
||||
|
||||
Der Block muss regelmäßig ausgeführt werden um E-Mails abzurufen. Dafür den Timer im Module Designer konfigurieren:
|
||||
|
||||
1. Tab **"Timer"** öffnen
|
||||
2. Auf **[+]** klicken um einen neuen Schedule hinzuzufügen
|
||||
3. Intervall festlegen (empfohlen: alle 60 Sekunden)
|
||||
|
||||

|
||||
|
||||
**Hinweis:** Der Block hat einen eingebauten Lock-Mechanismus. Wenn der Timer erneut auslöst während der Block noch läuft, wird die neue Ausführung automatisch übersprungen. Keine Gefahr von Duplikaten.
|
||||
|
||||
### 6. Modul aktivieren
|
||||
|
||||
## Benutzung
|
||||
|
||||
1. E-Mail an das konfigurierte Postfach senden
|
||||
2. **Betreff** = Ziel-Faxnummer (z.B. `+49721123456`)
|
||||
3. **Anhang** = PDF-Datei(en)
|
||||
4. **E-Mail-Text** = PIN (falls konfiguriert)
|
||||
|
||||
Das Modul ruft regelmäßig E-Mails ab und sendet PDFs als Fax.
|
||||
|
||||
## PIN-Schutz
|
||||
|
||||
Wenn eine PIN konfiguriert ist, muss diese im E-Mail-Text enthalten sein, damit das Fax gesendet wird.
|
||||
|
||||
- **PIN nicht gesetzt**: Alle E-Mails werden verarbeitet (nur Absender-Prüfung falls konfiguriert)
|
||||
- **PIN gesetzt**: Die PIN muss irgendwo im E-Mail-Text (plain text oder HTML) vorkommen
|
||||
- **PIN nicht gefunden**: E-Mail wird als gelesen markiert, kein Fax gesendet, Log-Eintrag "PIN in Emailtext nicht vorhanden oder falsch"
|
||||
|
||||
## Output-Variablen
|
||||
|
||||
| Variable | Typ | Beschreibung |
|
||||
|----------|-----|--------------|
|
||||
| processedCount | NUMBER | Verarbeitete E-Mails |
|
||||
| sentFaxCount | NUMBER | Gesendete Faxe |
|
||||
| errorCount | NUMBER | Fehleranzahl |
|
||||
| pendingRetries | NUMBER | Wartende Wiederholungsversuche |
|
||||
| statusMessage | STRING | Status-Meldung |
|
||||
|
||||
## Retry-Verhalten
|
||||
|
||||
Bei folgenden Fehlern wird automatisch erneut versucht:
|
||||
- Leitung besetzt
|
||||
- Keine Antwort
|
||||
- Übertragungsfehler
|
||||
|
||||
Die Retry-Daten werden gespeichert in:
|
||||
- `/var/starface/module-data/mail2fax_retry.txt`
|
||||
|
||||
Nach Erreichen von `maxRetries` wird der Fax-Versuch verworfen.
|
||||
|
||||
## POP3-Tracking
|
||||
|
||||
Da POP3 keine "gelesen"-Flags unterstützt, speichert der Block verarbeitete Message-IDs in:
|
||||
- `/var/starface/module-data/mail2fax_processed.txt`
|
||||
|
||||
So werden Duplikate vermieden, auch wenn E-Mails nicht sofort gelöscht werden können.
|
||||
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Kompiliert den Mail2FaxBlock für STARFACE Module Designer
|
||||
#
|
||||
|
||||
set -e
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
cd "$SCRIPT_DIR"
|
||||
|
||||
echo "========================================"
|
||||
echo " Mail2FaxBlock Kompilierung"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# JavaMail Bibliothek herunterladen falls nicht vorhanden
|
||||
DEPS_DIR="libs/deps"
|
||||
mkdir -p "$DEPS_DIR"
|
||||
|
||||
if [ ! -f "$DEPS_DIR/javax.mail.jar" ]; then
|
||||
echo "Lade JavaMail Bibliothek herunter..."
|
||||
curl -sL -o "$DEPS_DIR/javax.mail.jar" \
|
||||
"https://repo1.maven.org/maven2/com/sun/mail/javax.mail/1.6.2/javax.mail-1.6.2.jar"
|
||||
echo " javax.mail.jar heruntergeladen"
|
||||
fi
|
||||
|
||||
if [ ! -f "$DEPS_DIR/activation.jar" ]; then
|
||||
echo "Lade Activation Framework herunter..."
|
||||
curl -sL -o "$DEPS_DIR/activation.jar" \
|
||||
"https://repo1.maven.org/maven2/javax/activation/activation/1.1.1/activation-1.1.1.jar"
|
||||
echo " activation.jar heruntergeladen"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
|
||||
# Classpath zusammenbauen - STARFACE JARs
|
||||
CLASSPATH=""
|
||||
for jar in libs/starface/*.jar; do
|
||||
if [ -f "$jar" ]; then
|
||||
CLASSPATH="$CLASSPATH:$jar"
|
||||
fi
|
||||
done
|
||||
|
||||
# Zusätzliche Dependencies hinzufügen
|
||||
for jar in libs/deps/*.jar; do
|
||||
if [ -f "$jar" ]; then
|
||||
CLASSPATH="$CLASSPATH:$jar"
|
||||
fi
|
||||
done
|
||||
|
||||
CLASSPATH="${CLASSPATH:1}"
|
||||
|
||||
if [ -z "$CLASSPATH" ]; then
|
||||
echo "FEHLER: Keine STARFACE JARs gefunden in libs/starface/"
|
||||
echo "Führe zuerst aus: ../fetch-starface-libs.sh <starface-ip>"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Kompiliere Mail2FaxBlock.java..."
|
||||
|
||||
# Kompilieren - wichtig: nur die .class Datei, kein Package!
|
||||
javac -source 21 -target 21 \
|
||||
-cp "$CLASSPATH" \
|
||||
-proc:none \
|
||||
Mail2FaxBlock.java
|
||||
|
||||
if [ -f "Mail2FaxBlock.class" ]; then
|
||||
echo ""
|
||||
echo "========================================"
|
||||
echo " Erfolgreich!"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo "Datei: Mail2FaxBlock.class"
|
||||
ls -lh Mail2FaxBlock.class
|
||||
echo ""
|
||||
echo "WICHTIG: Die JavaMail-Bibliothek muss auch auf der STARFACE sein!"
|
||||
echo "Falls Fehler auftreten, kopiere javax.mail.jar nach STARFACE:"
|
||||
echo " scp libs/deps/javax.mail.jar root@<starface>:/opt/tomcat/webapps/localhost/starface/WEB-INF/lib/"
|
||||
echo ""
|
||||
echo "Nächste Schritte:"
|
||||
echo "1. STARFACE Admin öffnen"
|
||||
echo "2. Module → Module Designer → Neues Modul"
|
||||
echo "3. Unter 'Ressourcen' die Mail2FaxBlock.class hochladen"
|
||||
echo "4. Neuen Funktionsbaustein erstellen und Block verknüpfen"
|
||||
echo "5. Timer-Baustein hinzufügen für regelmäßige Ausführung"
|
||||
echo ""
|
||||
else
|
||||
echo "FEHLER: Kompilierung fehlgeschlagen"
|
||||
exit 1
|
||||
fi
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user