Skip to content
Portfolio: <Bantonis/>
HomeProfileInternshipProjects
Back to projects

Email MSG Attachment Extractor

Summary

A JavaFX desktop tool that extracts attachments from Outlook .MSG files into structured local folders.

Role
Java Desktop Developer
Timeline
Personal project: 2025
Tags
JavaJavaFXFile Processing

Preview

Application icon for the Email MSG Attachment Extractor.
Screenshot of the Email MSG Attachment Extractor application on macOS, showing the JavaFX interface with drag-and-drop functionality.
Windows version running on my friend's laptop, with drag-and-drop input and a selected work folder.
PreviousNext

Overview

  • JavaFX app with drag-and-drop and file chooser input.
  • Batch-processes .MSG files from selected folders.
  • Filters true attachments and writes safe, non-overwriting filenames.

My contribution

  • Built a JavaFX interface with drag-and-drop, file chooser support and Dutch messages.
  • Parsed .MSG files with the Simple Java Mail Outlook message parser.
  • Added folder processing, attachment extraction, safe filenames and duplicate handling.

Outcome

  • Desktop utility for extracting true attachments from single .MSG files or folders.
  • Predictable output folders under a user-selected base directory.
  • Working Windows version now used on my friend's laptop, where the earlier PowerShell script was no longer reliable.

Goal

  • Help a friend process customer and supplier emails with many attachments.
  • Replace a PowerShell script that became unreliable on an ARM64 Windows laptop.
  • Process .MSG files locally without depending on Outlook automation.

Approach

  • Mapped the manual workflow: open .MSG files and save attachments one by one.
  • Kept the scope narrow: choose a work folder, drop or select files and extract attachments.
  • Validated Java libraries for local .MSG parsing.

Artifacts

  • JavaFX extraction workflow code example.
  • .MSG parsing, attachment extraction and filename handling code example.
  • Settings persistence code example for the selected output folder.

Code examples/ Source Code

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);
    }
}

Settings persistence

SettingsManager.java

java / 68 lines

Small settings manager that stores and reloads the selected base output directory.

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Properties;

public class SettingsManager {
    private static final String APP_DIR_NAME = ".emailextractor";
    private static final String FILE_NAME = "config.properties";
    private static final String KEY_BASEDIR = "baseDir";

    private final Path configDir;
    private final Path configFile;
    private final Properties props = new Properties();

    public SettingsManager() {
        Path home = Path.of(System.getProperty("user.home"));
        this.configDir = home.resolve(APP_DIR_NAME);
        this.configFile = configDir.resolve(FILE_NAME);
        load();
    }

    private void load() {
        if (!Files.exists(configFile)) {
            return;
        }

        try (InputStream in = Files.newInputStream(configFile)) {
            props.load(in);
        } catch (IOException ignored) {
            // Invalid or unreadable? Stop!
        }
    }

    public void save() {
        try {
            Files.createDirectories(configDir);

            try (OutputStream out = Files.newOutputStream(configFile)) {
                props.store(out, "Email Extractor settings");
            }
        } catch (IOException ignored) {
            // Settings persistence...
        }
    }

    public Path getBaseDir() {
        String value = props.getProperty(KEY_BASEDIR);

        if (value == null || value.isBlank()) {
            return null;
        }

        return Path.of(value);
    }

    public void setBaseDir(Path dir) {
        if (dir != null) {
            props.setProperty(KEY_BASEDIR, dir.toString());
        }
    }

    public boolean hasValidBaseDir() {
        Path baseDir = getBaseDir();
        return baseDir != null && Files.isDirectory(baseDir);
    }
}
Technology stack
JavaJavaFXSimple Java MailOutlook MSG ParserFile I/ODrag and Drop
© 2026 Bryan Antonis — Built with Next.js + React-Bootstrap + AI assistance for styling and insight