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 = "Betreff-Präfix", description = "Präfix im Betreff (optional). Wenn gesetzt, muss der Betreff mit diesem Präfix beginnen (z.B. 'FAXMaik:'). Ermöglicht Filter-Regeln im E-Mail-Client für mehrere Fax-Instanzen.", type = VariableType.STRING ) public String subjectPrefix = ""; @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 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 lines = Files.readAllLines(retryPath); List 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()) { // Spezifische Fehlermeldung je nach Grund if (subjectPrefix != null && !subjectPrefix.trim().isEmpty() && !subject.startsWith(subjectPrefix.trim())) { // Präfix stimmt nicht überein → Email NICHT als gelesen markieren // So können andere Instanzen mit passendem Präfix die Email verarbeiten log.debug("Mail2Fax: Betreff-Präfix stimmt nicht überein. Erwartet: '" + subjectPrefix.trim() + "', Betreff: " + subject + " - Email bleibt ungelesen für andere Instanzen"); return; // Ohne setFlag(SEEN) - andere Instanz könnte passen } else { // Keine gültige Faxnummer → als gelesen markieren 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 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 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 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 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 * * Wenn ein Betreff-Präfix konfiguriert ist, muss der Betreff damit beginnen. * Der Präfix wird dann entfernt, bevor die Faxnummer extrahiert wird. * * Beispiel: subjectPrefix="FAXMaik:", subject="FAXMaik: +49721123456" → "+49721123456" */ private String extractFaxNumber(String subject) { if (subject == null || subject.isEmpty()) { return null; } // Wenn Präfix gesetzt ist, prüfen und entfernen if (subjectPrefix != null && !subjectPrefix.trim().isEmpty()) { String prefix = subjectPrefix.trim(); if (!subject.startsWith(prefix)) { log.debug("Mail2Fax: Betreff beginnt nicht mit Präfix '" + prefix + "': " + subject); return null; } // Präfix entfernen subject = subject.substring(prefix.length()).trim(); log.debug("Mail2Fax: Präfix entfernt, verbleibender Betreff: " + subject); } // 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 extractPdfAttachments(Message message) throws Exception { List 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) { String status = exitStatus.toString(); // BUSY, FAILED oder andere temporäre Fehler → Retry if (status.contains("BUSY") || status.contains("FAILED") || status.contains("NO_ANSWER") || status.contains("TIMEOUT")) { log.info("Mail2Fax: Temporärer Fehler (" + status + "), wird zur Retry-Queue hinzugefügt"); return FAX_RETRY; } // SENT → Erfolg if (status.contains("SENT")) { log.info("Mail2Fax: Fax erfolgreich gesendet an " + destination); return FAX_SUCCESS; } // Unbekannter Status → als Fehler behandeln log.warn("Mail2Fax: Unbekannter exitStatus: " + status); return FAX_FAIL; } } catch (NoSuchFieldException e) { log.debug("Mail2Fax: Kein exitStatus Feld"); } // Kein exitStatus gefunden → als Erfolg annehmen (Fallback) log.info("Mail2Fax: Fax gesendet an " + destination + " (kein exitStatus verfügbar)"); 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); } }