JavaFX extraction workflow
EmailExtractorApp.java
java / 436 lines
Main application file with drag-and-drop, MSG parsing and attachment extraction.
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.*;
import javafx.scene.text.Text;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.simplejavamail.outlookmessageparser.OutlookMessageParser;
import org.simplejavamail.outlookmessageparser.model.OutlookFileAttachment;
import org.simplejavamail.outlookmessageparser.model.OutlookMessage;
import java.io.File;
import java.io.FileOutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
import java.util.Optional;
public class EmailExtractorApp extends Application {
private final SettingsManager settings = new SettingsManager();
private Stage primaryStage;
private Label lblCurrentBase;
@Override
public void start(Stage stage) {
this.primaryStage = stage;
stage.setTitle("Email Attachment Extractor (.MSG)");
Text txtDropMessage = new Text("Sleep hier .MSG bestanden of mappen om bijlagen te extraheren");
StackPane paneDropZone = new StackPane(txtDropMessage);
paneDropZone.setPadding(new Insets(24));
paneDropZone.setStyle(
"-fx-border-color: #4285f4;" +
"-fx-border-width: 3;" +
"-fx-border-radius: 12;" +
"-fx-background-radius: 12;"
);
StackPane.setAlignment(txtDropMessage, Pos.CENTER);
Button btnPickFile = new Button("Selecteer .MSG-bestand…");
btnPickFile.setOnAction(e -> openFileChooserAndHandle(stage));
Button btnSettings = new Button("Stel werkmap in");
btnSettings.setOnAction(e -> openSettingsDialog(stage));
HBox boxActions = new HBox(12, btnPickFile, btnSettings);
boxActions.setAlignment(Pos.CENTER);
Path baseNow = settings.getBaseDir();
lblCurrentBase = new Label("Huidige werkmap: " + (baseNow != null ? baseNow : "(nog niet ingesteld)"));
lblCurrentBase.setWrapText(true);
lblCurrentBase.setStyle("-fx-font-size: 12px; -fx-text-fill: #444;");
VBox boxCenter = new VBox(10, paneDropZone, boxActions, lblCurrentBase);
boxCenter.setPadding(new Insets(12));
boxCenter.setFillWidth(true);
boxCenter.setAlignment(Pos.CENTER);
BorderPane paneRoot = new BorderPane(boxCenter);
Scene scene = new Scene(paneRoot, 600, 240);
paneDropZone.setOnDragOver(e -> {
Dragboard dragboard = e.getDragboard();
if (dragboard.hasFiles()) {
e.acceptTransferModes(TransferMode.COPY);
}
e.consume();
});
paneDropZone.setOnDragDropped(e -> {
Dragboard dragboard = e.getDragboard();
if (dragboard.hasFiles()) {
for (File file : dragboard.getFiles()) {
Path path = file.toPath();
if (Files.isDirectory(path)) {
processDirectory(path);
} else if (file.getName().toLowerCase().endsWith(".msg")) {
handleMsgFile(path);
} else {
showAlert(
Alert.AlertType.WARNING,
"Ongeldig bestand",
"Enkel .MSG of mappen met .MSG worden ondersteund:\n" + file.getName()
);
}
}
}
e.setDropCompleted(true);
e.consume();
});
stage.setScene(scene);
stage.setAlwaysOnTop(false);
stage.show();
if (!settings.hasValidBaseDir()) {
showAlert(
Alert.AlertType.INFORMATION,
"Welkom bij Email Extractor",
"Welkom! Kies eerst een werkmap voor bijlagen."
);
boolean picked = openSettingsDialog(stage);
if (!picked || !settings.hasValidBaseDir()) {
showAlert(
Alert.AlertType.ERROR,
"Geen werkmap ingesteld",
"Er is geen werkmap ingesteld. De applicatie wordt afgesloten."
);
Platform.exit();
}
}
}
private boolean openSettingsDialog(Stage owner) {
DirectoryChooser chooserDirectory = new DirectoryChooser();
chooserDirectory.setTitle("Kies werkmap voor opslag");
Path current = settings.getBaseDir();
if (current != null && Files.isDirectory(current)) {
chooserDirectory.setInitialDirectory(current.toFile());
}
boolean previousAlwaysOnTop = owner.isAlwaysOnTop();
owner.setAlwaysOnTop(false);
File chosen = chooserDirectory.showDialog(owner);
owner.setAlwaysOnTop(previousAlwaysOnTop);
if (chosen == null) {
return false;
}
settings.setBaseDir(chosen.toPath());
settings.save();
if (lblCurrentBase != null) {
lblCurrentBase.setText("Huidige werkmap: " + chosen.getAbsolutePath());
}
showAlert(
Alert.AlertType.INFORMATION,
"Instellingen opgeslagen",
"Werkmap ingesteld op:\n" + chosen.getAbsolutePath()
);
return true;
}
private void openFileChooserAndHandle(Stage owner) {
FileChooser chooserFile = new FileChooser();
chooserFile.setTitle("Kies een .MSG-bestand");
chooserFile.getExtensionFilters().add(
new FileChooser.ExtensionFilter("Outlook-bericht (*.msg)", "*.msg")
);
Path base = settings.getBaseDir();
if (base == null || !Files.isDirectory(base)) {
showAlert(
Alert.AlertType.INFORMATION,
"Werkmap nodig",
"Kies eerst een werkmap via 'Stel werkmap in'."
);
boolean picked = openSettingsDialog(owner);
base = settings.getBaseDir();
if (!picked || base == null || !Files.isDirectory(base)) {
return;
}
}
try {
chooserFile.setInitialDirectory(base.toFile());
} catch (Exception ignored) {
// Keep the file chooser usable even if the initial directory cannot be applied.
}
boolean previousAlwaysOnTop = owner.isAlwaysOnTop();
owner.setAlwaysOnTop(false);
File chosen = chooserFile.showOpenDialog(owner);
owner.setAlwaysOnTop(previousAlwaysOnTop);
if (chosen == null) {
return;
}
if (chosen.getName().toLowerCase().endsWith(".msg")) {
handleMsgFile(chosen.toPath());
} else {
showAlert(
Alert.AlertType.WARNING,
"Ongeldig bestand",
"Kies een bestand met de extensie .MSG."
);
}
}
private void processDirectory(Path dir) {
try {
Files.walk(dir)
.filter(path -> !Files.isDirectory(path))
.filter(path -> path.getFileName().toString().toLowerCase().endsWith(".msg"))
.forEach(this::handleMsgFile);
} catch (Exception ex) {
showAlert(Alert.AlertType.ERROR, "Fout bij mapverwerking", ex.getMessage());
}
}
private void handleMsgFile(Path msgPath) {
Path baseDir = settings.getBaseDir();
if (baseDir == null || !Files.isDirectory(baseDir)) {
showAlert(
Alert.AlertType.INFORMATION,
"Werkmap nodig",
"Kies eerst een werkmap via 'Stel werkmap in'."
);
boolean picked = openSettingsDialog(primaryStage);
baseDir = settings.getBaseDir();
if (!picked || baseDir == null || !Files.isDirectory(baseDir)) {
return;
}
}
String defaultName = stripExt(msgPath.getFileName().toString());
String folderName = promptFolderName(defaultName);
if (folderName == null || folderName.isBlank()) {
return;
}
Path target = baseDir.resolve(folderName).resolve("Attachments");
try {
Files.createDirectories(target);
OutlookMessageParser parser = new OutlookMessageParser();
OutlookMessage message = parser.parseMsg(msgPath.toFile());
List<OutlookFileAttachment> fileAttachments = message.fetchTrueAttachments();
int saved = 0;
for (OutlookFileAttachment attachment : fileAttachments) {
String name = safeName(attachment, saved);
Path outputPath = unique(target, name);
try (FileOutputStream fos = new FileOutputStream(outputPath.toFile())) {
fos.write(attachment.getData());
}
saved++;
}
if (saved > 0) {
showAlert(
Alert.AlertType.INFORMATION,
"Succes",
"Opgeslagen: " + saved + " bijlage(n) in:\n" + target
);
} else {
showAlert(
Alert.AlertType.INFORMATION,
"Geen bijlagen",
"Geen bijlagen gevonden in:\n" + msgPath.getFileName()
);
}
} catch (Exception ex) {
showAlert(
Alert.AlertType.ERROR,
"Fout bij MSG-extractie",
ex.getClass().getSimpleName() + ": " + ex.getMessage()
);
}
}
private static String safeName(OutlookFileAttachment attachment, int index) {
String name = attachment.getLongFilename();
if (name == null || name.isBlank()) {
name = attachment.getFilename();
}
if (name == null) {
name = "";
}
name = name.replace('\\', '/');
int slash = name.lastIndexOf('/');
if (slash >= 0) {
name = name.substring(slash + 1);
}
name = name.replaceAll("\\p{C}", "");
name = name.replaceAll("[\\\\/:*?\"<>|]", "_");
name = name.replaceAll("^[\\s.]+", "").replaceAll("[\\s.]+$", "");
if (name.isBlank()) {
String extension = ".bin";
String sourceName = attachment.getLongFilename();
if (sourceName == null || sourceName.isBlank()) {
sourceName = attachment.getFilename();
}
if (sourceName != null) {
int dot = sourceName.lastIndexOf('.');
if (dot > 0 && dot < sourceName.length() - 1) {
extension = sourceName.substring(dot);
}
}
name = "attachment_" + (index + 1) + extension;
}
final int maxLength = 180;
if (name.length() > maxLength) {
int dot = name.lastIndexOf('.');
String baseName = dot > 0 ? name.substring(0, dot) : name;
String extension = dot > 0 ? name.substring(dot) : "";
if (baseName.length() > maxLength) {
baseName = baseName.substring(0, maxLength);
}
name = baseName + extension;
}
return name;
}
private static Path unique(Path folder, String name) {
Path outputPath = folder.resolve(name);
String baseName = name;
String extension = "";
int dot = name.lastIndexOf('.');
if (dot > 0) {
baseName = name.substring(0, dot);
extension = name.substring(dot);
}
int index = 1;
while (Files.exists(outputPath)) {
outputPath = folder.resolve(baseName + " (" + index + ")" + extension);
index++;
}
return outputPath;
}
private static String stripExt(String name) {
int index = name.lastIndexOf('.');
return index > 0 ? name.substring(0, index) : name;
}
private String promptFolderName(String defaultName) {
TextInputDialog dialogFolderName = new TextInputDialog(defaultName);
dialogFolderName.setTitle("Mapnaam");
dialogFolderName.setHeaderText(null);
dialogFolderName.setContentText("Voer mapnaam in:");
if (primaryStage == null) {
Optional<String> result = dialogFolderName.showAndWait();
return result.map(String::trim).orElse(null);
}
dialogFolderName.initOwner(primaryStage);
boolean previousAlwaysOnTop = primaryStage.isAlwaysOnTop();
primaryStage.setAlwaysOnTop(false);
Optional<String> result = dialogFolderName.showAndWait();
primaryStage.setAlwaysOnTop(previousAlwaysOnTop);
return result.map(String::trim).orElse(null);
}
private void showAlert(Alert.AlertType type, String title, String message) {
Alert alertMessage = new Alert(type);
alertMessage.setTitle(title);
alertMessage.setHeaderText(null);
alertMessage.setContentText(message);
if (primaryStage != null) {
alertMessage.initOwner(primaryStage);
}
boolean previousAlwaysOnTop = primaryStage != null && primaryStage.isAlwaysOnTop();
if (primaryStage != null) {
primaryStage.setAlwaysOnTop(false);
}
alertMessage.showAndWait();
if (primaryStage != null) {
primaryStage.setAlwaysOnTop(previousAlwaysOnTop);
}
}
public static void main(String[] args) {
launch(args);
}
}