Files
ost2pst/Convert.java
Simon Cloutier cfee611fdc
Some checks failed
Build & Push Multi-Arch Image / build-and-push (push) Has been cancelled
feat: initial commit — OST to PST/MBOX converter with Docker support
2026-05-20 13:38:46 -04:00

333 lines
15 KiB
Java

import com.aspose.email.FileFormatVersion;
import com.aspose.email.FolderInfo;
import com.aspose.email.MapiMessage;
import com.aspose.email.MessageInfo;
import com.aspose.email.PersonalStorage;
import com.aspose.email.MailMessage;
import com.aspose.email.MailConversionOptions;
import com.aspose.email.MboxrdStorageWriter;
import com.aspose.email.MapiContact;
import com.aspose.email.MapiItemType;
import com.aspose.email.ContactSaveFormat;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
public class Convert {
private static PersonalStorage sourceStorage;
public static void main(String[] args) {
if (args.length < 2) {
System.err.println("Usage: java -cp <classpath> Convert <input.ost> <output.pst/zip> [format]");
System.exit(1);
}
String inputPath = args[0];
String outputPath = args[1];
String format = (args.length >= 3) ? args[2].toLowerCase() : "pst";
boolean combineVcf = (args.length >= 4) && args[3].equalsIgnoreCase("true");
System.out.println("Starting manual copy conversion of OST: " + inputPath + " to format: " + format.toUpperCase());
long startTime = System.currentTimeMillis();
try {
// Delete existing output file if it exists
File outFile = new File(outputPath);
if (outFile.exists()) {
System.out.println("Destination file already exists. Deleting " + outputPath + "...");
if (outFile.delete()) {
System.out.println("Deleted existing file successfully.");
} else {
System.err.println("Warning: Failed to delete existing file.");
}
}
// Load the source OST file
System.out.println("Loading OST file...");
sourceStorage = PersonalStorage.fromFile(inputPath);
if (format.equals("mbox")) {
// Export folders to a temporary directory of .mbox files, then zip them
File tempDir = new File("mbox_temp_" + System.currentTimeMillis());
if (!tempDir.exists()) {
tempDir.mkdirs();
}
System.out.println("Exporting folder structure, email messages, and contacts...");
exportFoldersToMbox(sourceStorage.getRootFolder(), "", tempDir, combineVcf);
// If combineVcf was requested, merge all individual VCFs in the contacts subdirs
if (combineVcf) {
System.out.println("Merging contact VCF files into a single contacts.vcf...");
mergeVcfFiles(tempDir);
}
System.out.println("Packaging MBOX files and VCards into ZIP archive...");
zipDirectory(tempDir, outputPath);
System.out.println("Cleaning up temporary MBOX/VCard files...");
cleanDirectory(tempDir);
tempDir.delete();
long duration = System.currentTimeMillis() - startTime;
System.out.println("Conversion completed successfully in " + (duration / 1000.0) + " seconds.");
} else {
// Default PST format
System.out.println("Creating destination PST file...");
try (PersonalStorage destPst = PersonalStorage.create(outputPath, FileFormatVersion.Unicode)) {
System.out.println("Copying folder structure and messages...");
copyFolders(sourceStorage.getRootFolder(), destPst.getRootFolder());
long duration = System.currentTimeMillis() - startTime;
System.out.println("Conversion completed successfully in " + (duration / 1000.0) + " seconds.");
}
}
sourceStorage.close();
System.exit(0);
} catch (Exception e) {
System.err.println("Error during conversion: " + e.getMessage());
e.printStackTrace();
if (sourceStorage != null) {
try {
sourceStorage.close();
} catch (Exception ex) {
// Ignore
}
}
System.exit(1);
}
}
private static void copyFolders(FolderInfo sourceFolder, FolderInfo destParentFolder) {
for (FolderInfo subFolder : sourceFolder.getSubFolders()) {
String folderName = subFolder.getDisplayName();
System.out.println("Processing Folder: " + folderName);
// Recreate folder in destination
FolderInfo newDestFolder = destParentFolder.addSubFolder(folderName);
// Copy messages
int copiedCount = 0;
for (MessageInfo msgInfo : subFolder.enumerateMessages()) {
try {
MapiMessage msg = sourceStorage.extractMessage(msgInfo);
newDestFolder.addMessage(msg);
copiedCount++;
if (copiedCount % 100 == 0) {
System.out.println(" Copied " + copiedCount + " messages...");
}
} catch (Exception ex) {
System.err.println(" Warning: Failed to copy message '" + msgInfo.getSubject() + "': " + ex.getMessage());
}
}
if (copiedCount > 0) {
System.out.println(" Successfully copied " + copiedCount + " messages to folder: " + folderName);
}
// Recursively copy subfolders
copyFolders(subFolder, newDestFolder);
}
}
private static void exportFoldersToMbox(FolderInfo sourceFolder, String currentPath, File tempDir, boolean combineVcf) {
for (FolderInfo subFolder : sourceFolder.getSubFolders()) {
String folderName = subFolder.getDisplayName();
String fullPath = currentPath.isEmpty() ? folderName : currentPath + " - " + folderName;
System.out.println("Processing Folder: " + folderName);
boolean isContactsFolder = folderName.toLowerCase().contains("contacts");
// Skip other non-email folders
if (folderName.toLowerCase().contains("calendrier") ||
folderName.toLowerCase().contains("journal") ||
folderName.toLowerCase().contains("tâches") ||
folderName.toLowerCase().contains("notes")) {
System.out.println(" Skipping non-email/non-contact folder: " + folderName);
// Still process children in case there are nested folders
exportFoldersToMbox(subFolder, fullPath, tempDir, combineVcf);
continue;
}
if (isContactsFolder) {
System.out.println(" Processing contacts in folder: " + folderName);
String safeSubDirName = "Contacts - " + folderName.replaceAll("[\\\\/:*?\"<>|]", "_");
File contactsDir = new File(tempDir, safeSubDirName);
if (!contactsDir.exists()) {
contactsDir.mkdirs();
}
int contactCount = 0;
for (MessageInfo msgInfo : subFolder.enumerateMessages()) {
try {
MapiMessage msg = sourceStorage.extractMessage(msgInfo);
if (msg.getSupportedType() == MapiItemType.Contact) {
MapiContact contact = (MapiContact) msg.toMapiMessageItem();
// Determine a suitable name for the .vcf file
String displayName = null;
if (contact.getNameInfo() != null) {
displayName = contact.getNameInfo().getDisplayName();
}
if (displayName == null || displayName.trim().isEmpty()) {
if (contact.getElectronicAddresses() != null &&
contact.getElectronicAddresses().getEmail1() != null) {
displayName = contact.getElectronicAddresses().getEmail1().getEmailAddress();
}
}
if (displayName == null || displayName.trim().isEmpty()) {
displayName = "Contact_" + (contactCount + 1);
}
String safeContactName = displayName.replaceAll("[\\\\/:*?\"<>|]", "_") + ".vcf";
File vcfFile = new File(contactsDir, safeContactName);
// Check for duplicates
int index = 1;
while (vcfFile.exists()) {
String nameWithIndex = displayName.replaceAll("[\\\\/:*?\"<>|]", "_") + "_" + index + ".vcf";
vcfFile = new File(contactsDir, nameWithIndex);
index++;
}
contact.save(vcfFile.getAbsolutePath(), ContactSaveFormat.VCard);
contactCount++;
}
} catch (Exception ex) {
System.err.println(" Warning: Failed to export contact: " + ex.getMessage());
}
}
if (contactCount > 0) {
System.out.println(" Successfully exported " + contactCount + " contacts to folder: " + folderName);
} else {
contactsDir.delete();
}
// Recursively walk subfolders
exportFoldersToMbox(subFolder, fullPath, tempDir, combineVcf);
continue;
}
// Otherwise, it's an email folder. Export to MBOX...
int copiedCount = 0;
String safeFileName = fullPath.replaceAll("[\\\\/:*?\"<>|]", "_") + ".mbox";
File mboxFile = new File(tempDir, safeFileName);
MailConversionOptions options = new MailConversionOptions();
MboxrdStorageWriter writer = null;
for (MessageInfo msgInfo : subFolder.enumerateMessages()) {
try {
if (writer == null) {
writer = new MboxrdStorageWriter(mboxFile.getAbsolutePath(), false);
}
MapiMessage msg = sourceStorage.extractMessage(msgInfo);
MailMessage mailMsg = msg.toMailMessage(options);
writer.writeMessage(mailMsg);
copiedCount++;
if (copiedCount % 100 == 0) {
System.out.println(" Exported " + copiedCount + " messages...");
}
} catch (Exception ex) {
System.err.println(" Warning: Failed to export message '" + msgInfo.getSubject() + "': " + ex.getMessage());
}
}
if (writer != null) {
try {
writer.close();
} catch (Exception ex) {
// Ignore
}
}
if (copiedCount > 0) {
System.out.println(" Successfully copied " + copiedCount + " messages to folder: " + folderName);
} else {
// Delete empty mbox file if created
if (mboxFile.exists()) {
mboxFile.delete();
}
}
// Recursively walk subfolders
exportFoldersToMbox(subFolder, fullPath, tempDir, combineVcf);
}
}
/**
* Walk all contact subdirectories, read each .vcf, and write them all
* into a single contacts.vcf at the root of tempDir.
* Then delete the now-merged individual files and empty subdirs.
*/
private static void mergeVcfFiles(File tempDir) throws Exception {
File combinedVcf = new File(tempDir, "contacts.vcf");
try (FileOutputStream fos = new FileOutputStream(combinedVcf)) {
for (File child : tempDir.listFiles()) {
if (child.isDirectory() && child.getName().startsWith("Contacts")) {
for (File vcf : child.listFiles()) {
if (vcf.isFile() && vcf.getName().endsWith(".vcf")) {
byte[] data = java.nio.file.Files.readAllBytes(vcf.toPath());
fos.write(data);
// Ensure each vCard ends with a newline separator
if (data.length > 0 && data[data.length - 1] != '\n') {
fos.write('\n');
}
}
}
// Remove the now-merged individual VCF directory
cleanDirectory(child);
child.delete();
}
}
}
System.out.println(" Merged VCF written: " + combinedVcf.getName());
}
private static void zipDirectory(File dir, String zipFilePath) throws Exception {
try (FileOutputStream fos = new FileOutputStream(zipFilePath);
ZipOutputStream zos = new ZipOutputStream(fos)) {
zipFolder(dir, dir, zos);
}
}
private static void zipFolder(File rootDir, File currentDir, ZipOutputStream zos) throws Exception {
File[] files = currentDir.listFiles();
if (files != null) {
byte[] buffer = new byte[8192];
for (File file : files) {
if (file.isDirectory()) {
zipFolder(rootDir, file, zos);
} else {
String relativePath = rootDir.toURI().relativize(file.toURI()).getPath();
try (FileInputStream fis = new FileInputStream(file)) {
ZipEntry ze = new ZipEntry(relativePath);
zos.putNextEntry(ze);
int len;
while ((len = fis.read(buffer)) > 0) {
zos.write(buffer, 0, len);
}
zos.closeEntry();
}
}
}
}
}
private static void cleanDirectory(File dir) {
File[] files = dir.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
cleanDirectory(file);
}
file.delete();
}
}
}
}