diff --git a/MainForm.Designer.cs b/MainForm.Designer.cs
index 1aec113..4656aac 100644
--- a/MainForm.Designer.cs
+++ b/MainForm.Designer.cs
@@ -7,9 +7,7 @@ partial class MainForm
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
- {
components.Dispose();
- }
base.Dispose(disposing);
}
@@ -22,6 +20,7 @@ private void InitializeComponent()
this.txtDestPst = new System.Windows.Forms.TextBox();
this.btnBrowseDest = new System.Windows.Forms.Button();
this.btnFixRegistry = new System.Windows.Forms.Button();
+ this.chkRemoveDuplicates = new System.Windows.Forms.CheckBox();
this.btnStartMerge = new System.Windows.Forms.Button();
this.btnCancel = new System.Windows.Forms.Button();
this.btnAbout = new System.Windows.Forms.Button();
@@ -29,25 +28,22 @@ private void InitializeComponent()
this.txtLog = new System.Windows.Forms.TextBox();
this.label3 = new System.Windows.Forms.Label();
this.SuspendLayout();
- //
+
// label1
- //
this.label1.AutoSize = true;
this.label1.Location = new System.Drawing.Point(12, 15);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(124, 13);
this.label1.TabIndex = 0;
this.label1.Text = "Source Folder (PST files):";
- //
+
// txtSourceFolder
- //
this.txtSourceFolder.Location = new System.Drawing.Point(15, 31);
this.txtSourceFolder.Name = "txtSourceFolder";
this.txtSourceFolder.Size = new System.Drawing.Size(465, 20);
this.txtSourceFolder.TabIndex = 1;
- //
+
// btnBrowseSource
- //
this.btnBrowseSource.Location = new System.Drawing.Point(486, 29);
this.btnBrowseSource.Name = "btnBrowseSource";
this.btnBrowseSource.Size = new System.Drawing.Size(75, 23);
@@ -55,25 +51,22 @@ private void InitializeComponent()
this.btnBrowseSource.Text = "Browse...";
this.btnBrowseSource.UseVisualStyleBackColor = true;
this.btnBrowseSource.Click += new System.EventHandler(this.btnBrowseSource_Click);
- //
+
// label2
- //
this.label2.AutoSize = true;
this.label2.Location = new System.Drawing.Point(12, 65);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(117, 13);
this.label2.TabIndex = 3;
this.label2.Text = "Destination Master PST:";
- //
+
// txtDestPst
- //
this.txtDestPst.Location = new System.Drawing.Point(15, 81);
this.txtDestPst.Name = "txtDestPst";
this.txtDestPst.Size = new System.Drawing.Size(465, 20);
this.txtDestPst.TabIndex = 4;
- //
+
// btnBrowseDest
- //
this.btnBrowseDest.Location = new System.Drawing.Point(486, 79);
this.btnBrowseDest.Name = "btnBrowseDest";
this.btnBrowseDest.Size = new System.Drawing.Size(75, 23);
@@ -81,9 +74,8 @@ private void InitializeComponent()
this.btnBrowseDest.Text = "Browse...";
this.btnBrowseDest.UseVisualStyleBackColor = true;
this.btnBrowseDest.Click += new System.EventHandler(this.btnBrowseDest_Click);
- //
+
// btnFixRegistry
- //
this.btnFixRegistry.BackColor = System.Drawing.Color.LightGoldenrodYellow;
this.btnFixRegistry.Location = new System.Drawing.Point(15, 115);
this.btnFixRegistry.Name = "btnFixRegistry";
@@ -92,24 +84,33 @@ private void InitializeComponent()
this.btnFixRegistry.Text = "Fix PST Size Limits";
this.btnFixRegistry.UseVisualStyleBackColor = false;
this.btnFixRegistry.Click += new System.EventHandler(this.btnFixRegistry_Click);
- //
+
+ // chkRemoveDuplicates — placed between Fix button and Start Merge
+ this.chkRemoveDuplicates.AutoSize = false;
+ this.chkRemoveDuplicates.Location = new System.Drawing.Point(175, 120);
+ this.chkRemoveDuplicates.Name = "chkRemoveDuplicates";
+ this.chkRemoveDuplicates.Size = new System.Drawing.Size(130, 20);
+ this.chkRemoveDuplicates.TabIndex = 13;
+ this.chkRemoveDuplicates.Text = "Remove Duplicates";
+ this.chkRemoveDuplicates.Checked = true; // ON by default
+ this.chkRemoveDuplicates.UseVisualStyleBackColor = true;
+ this.chkRemoveDuplicates.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Regular);
+
// btnStartMerge
- //
this.btnStartMerge.BackColor = System.Drawing.Color.LightGreen;
- this.btnStartMerge.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
- this.btnStartMerge.Location = new System.Drawing.Point(310, 115);
+ this.btnStartMerge.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold);
+ this.btnStartMerge.Location = new System.Drawing.Point(315, 115);
this.btnStartMerge.Name = "btnStartMerge";
this.btnStartMerge.Size = new System.Drawing.Size(100, 30);
this.btnStartMerge.TabIndex = 7;
this.btnStartMerge.Text = "Start Merge";
this.btnStartMerge.UseVisualStyleBackColor = false;
this.btnStartMerge.Click += new System.EventHandler(this.btnStartMerge_Click);
- //
+
// btnCancel
- //
this.btnCancel.BackColor = System.Drawing.Color.LightCoral;
- this.btnCancel.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold, System.Drawing.GraphicsUnit.Point, ((byte)(0)));
- this.btnCancel.Location = new System.Drawing.Point(190, 115);
+ this.btnCancel.Font = new System.Drawing.Font("Microsoft Sans Serif", 8.25F, System.Drawing.FontStyle.Bold);
+ this.btnCancel.Location = new System.Drawing.Point(315, 115);
this.btnCancel.Name = "btnCancel";
this.btnCancel.Size = new System.Drawing.Size(100, 30);
this.btnCancel.TabIndex = 11;
@@ -117,9 +118,8 @@ private void InitializeComponent()
this.btnCancel.UseVisualStyleBackColor = false;
this.btnCancel.Visible = false;
this.btnCancel.Click += new System.EventHandler(this.btnCancel_Click);
- //
+
// btnAbout
- //
this.btnAbout.Location = new System.Drawing.Point(430, 115);
this.btnAbout.Name = "btnAbout";
this.btnAbout.Size = new System.Drawing.Size(75, 30);
@@ -127,42 +127,39 @@ private void InitializeComponent()
this.btnAbout.Text = "About";
this.btnAbout.UseVisualStyleBackColor = true;
this.btnAbout.Click += new System.EventHandler(this.btnAbout_Click);
- //
+
// progressBar
- //
- this.progressBar.Location = new System.Drawing.Point(15, 151);
+ this.progressBar.Location = new System.Drawing.Point(15, 155);
this.progressBar.Name = "progressBar";
this.progressBar.Size = new System.Drawing.Size(546, 23);
this.progressBar.TabIndex = 8;
- //
+
+ // label3
+ this.label3.AutoSize = true;
+ this.label3.Location = new System.Drawing.Point(12, 181);
+ this.label3.Name = "label3";
+ this.label3.Size = new System.Drawing.Size(28, 13);
+ this.label3.TabIndex = 10;
+ this.label3.Text = "Log:";
+
// txtLog
- //
this.txtLog.BackColor = System.Drawing.Color.Black;
this.txtLog.ForeColor = System.Drawing.Color.Lime;
- this.txtLog.Location = new System.Drawing.Point(15, 193);
+ this.txtLog.Location = new System.Drawing.Point(15, 197);
this.txtLog.Multiline = true;
this.txtLog.Name = "txtLog";
this.txtLog.ReadOnly = true;
this.txtLog.ScrollBars = System.Windows.Forms.ScrollBars.Vertical;
this.txtLog.Size = new System.Drawing.Size(546, 250);
this.txtLog.TabIndex = 9;
- //
- // label3
- //
- this.label3.AutoSize = true;
- this.label3.Location = new System.Drawing.Point(12, 177);
- this.label3.Name = "label3";
- this.label3.Size = new System.Drawing.Size(28, 13);
- this.label3.TabIndex = 10;
- this.label3.Text = "Log:";
- //
+
// MainForm
- //
this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F);
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
- this.ClientSize = new System.Drawing.Size(584, 461);
+ this.ClientSize = new System.Drawing.Size(584, 465);
this.Controls.Add(this.btnAbout);
this.Controls.Add(this.btnCancel);
+ this.Controls.Add(this.chkRemoveDuplicates);
this.Controls.Add(this.label3);
this.Controls.Add(this.txtLog);
this.Controls.Add(this.progressBar);
@@ -180,7 +177,6 @@ private void InitializeComponent()
this.Text = "PST Merge Tool";
this.ResumeLayout(false);
this.PerformLayout();
-
}
private System.Windows.Forms.Label label1;
@@ -190,6 +186,7 @@ private void InitializeComponent()
private System.Windows.Forms.TextBox txtDestPst;
private System.Windows.Forms.Button btnBrowseDest;
private System.Windows.Forms.Button btnFixRegistry;
+ private System.Windows.Forms.CheckBox chkRemoveDuplicates;
private System.Windows.Forms.Button btnStartMerge;
private System.Windows.Forms.Button btnCancel;
private System.Windows.Forms.Button btnAbout;
diff --git a/MainForm.cs b/MainForm.cs
index db17439..1f319a6 100644
--- a/MainForm.cs
+++ b/MainForm.cs
@@ -26,9 +26,7 @@ private void btnBrowseSource_Click(object sender, EventArgs e)
using (var fbd = new FolderBrowserDialog())
{
if (fbd.ShowDialog() == DialogResult.OK)
- {
txtSourceFolder.Text = fbd.SelectedPath;
- }
}
}
@@ -38,9 +36,7 @@ private void btnBrowseDest_Click(object sender, EventArgs e)
{
sfd.Filter = "Outlook Data File (*.pst)|*.pst";
if (sfd.ShowDialog() == DialogResult.OK)
- {
txtDestPst.Text = sfd.FileName;
- }
}
}
@@ -49,8 +45,7 @@ private void btnFixRegistry_Click(object sender, EventArgs e)
try
{
Log("Applying PST size limit fixes to registry...");
-
- // We target Outlook 15.0 and 16.0
+
string[] versions = { "15.0", "16.0" };
foreach (var v in versions)
{
@@ -59,7 +54,6 @@ private void btnFixRegistry_Click(object sender, EventArgs e)
{
if (key != null)
{
- // Values in MB. 2000000 MB = ~2 TB (effectively unlimited)
key.SetValue("MaxLargeFileSize", 2000000, RegistryValueKind.DWord);
key.SetValue("WarnLargeFileSize", 1900000, RegistryValueKind.DWord);
}
@@ -80,6 +74,7 @@ private async void btnStartMerge_Click(object sender, EventArgs e)
{
string sourceDir = txtSourceFolder.Text;
string destFile = txtDestPst.Text;
+ bool removeDups = chkRemoveDuplicates.Checked;
if (string.IsNullOrEmpty(sourceDir) || !Directory.Exists(sourceDir))
{
@@ -101,11 +96,14 @@ private async void btnStartMerge_Click(object sender, EventArgs e)
long totalSourceSize = 0;
var pstFilesCheck = Directory.GetFiles(sourceDir, "*.pst", SearchOption.TopDirectoryOnly);
foreach (var f in pstFilesCheck) totalSourceSize += new FileInfo(f).Length;
-
- if (di.AvailableFreeSpace < (totalSourceSize * 1.1)) // 10% buffer
+
+ if (di.AvailableFreeSpace < (totalSourceSize * 1.1))
{
- var msg = string.Format("Warning: You might not have enough disk space on {0}.\nAvailable: {1} GB\nRequired (est): {2} GB\n\nContinue anyway?",
- drive, di.AvailableFreeSpace / 1024 / 1024 / 1024, (totalSourceSize * 1.1) / 1024 / 1024 / 1024);
+ var msg = string.Format(
+ "Warning: You might not have enough disk space on {0}.\nAvailable: {1} GB\nRequired (est): {2} GB\n\nContinue anyway?",
+ drive,
+ di.AvailableFreeSpace / 1024 / 1024 / 1024,
+ (totalSourceSize * 1.1) / 1024 / 1024 / 1024);
if (MessageBox.Show(msg, "Disk Space Warning", MessageBoxButtons.YesNo, MessageBoxIcon.Warning) == DialogResult.No)
return;
}
@@ -121,26 +119,28 @@ private async void btnStartMerge_Click(object sender, EventArgs e)
btnStartMerge.Enabled = false;
btnFixRegistry.Enabled = false;
+ chkRemoveDuplicates.Enabled = false;
btnCancel.Visible = true;
btnCancel.Enabled = true;
progressBar.Value = 0;
progressBar.Maximum = pstFiles.Length;
- Log(string.Format("Starting merge of {0} files...", pstFiles.Length));
+ string dupMode = removeDups ? " (Remove Duplicates: ON)" : " (Remove Duplicates: OFF)";
+ Log(string.Format("Starting merge of {0} files...{1}", pstFiles.Length, dupMode));
try
{
_cts = new System.Threading.CancellationTokenSource();
- await Task.Run(() =>
+ await Task.Run(() =>
{
- _pstService.MergeFiles(pstFiles, destFile, _cts.Token, (progress, message) =>
- {
- this.Invoke(new Action(() =>
- {
- Log(message);
- if (progress > 0) progressBar.Value = Math.Min(progress, progressBar.Maximum);
- }));
- });
+ _pstService.MergeFiles(pstFiles, destFile, _cts.Token, (progress, message) =>
+ {
+ this.Invoke(new Action(() =>
+ {
+ Log(message);
+ if (progress > 0) progressBar.Value = Math.Min(progress, progressBar.Maximum);
+ }));
+ }, removeDups);
});
if (_cts.Token.IsCancellationRequested)
@@ -167,6 +167,7 @@ await Task.Run(() =>
{
btnStartMerge.Enabled = true;
btnFixRegistry.Enabled = true;
+ chkRemoveDuplicates.Enabled = true;
btnCancel.Visible = false;
if (_cts != null) _cts.Dispose();
}
@@ -191,8 +192,6 @@ private void Log(string message)
}
string line = string.Format("[{0:HH:mm:ss}] {1}", DateTime.Now, message);
txtLog.AppendText(line + Environment.NewLine);
-
- // Persistent File Logging
try { File.AppendAllText(_logFile, line + Environment.NewLine); } catch { }
}
@@ -203,6 +202,7 @@ private void btnAbout_Click(object sender, EventArgs e)
string about = string.Format("PST Merge Tool v{0}\n\n", displayVersion) +
"Developed by: Mithun\n" +
+ "Enhanced by: Eslam Omar (ADD REMOVE DUPLICATES)\n" +
"© DataGuardNXT 2026\n\n" +
"All Rights Reserved.\n\n" +
"Enterprise-grade PST merging solution\n" +
@@ -211,3 +211,4 @@ private void btnAbout_Click(object sender, EventArgs e)
}
}
}
+
diff --git a/PstMerger.csproj b/PstMerger.csproj
index 8a02eae..9175b4c 100644
--- a/PstMerger.csproj
+++ b/PstMerger.csproj
@@ -1,64 +1,64 @@
-
-
- Debug
- AnyCPU
- {A1B2C3D4-E5F6-47A8-90B1-C2D3E4F5A6B7}
- WinExe
- PstMerger
- PstMerger
- v4.5
- 512
- true
-
-
- AnyCPU
- true
- full
- false
- bin\Debug\
- DEBUG;TRACE
- prompt
- 4
-
-
- AnyCPU
- pdbonly
- true
- bin\Release\
- TRACE
- prompt
- 4
-
-
-
-
-
-
-
- C:\Windows\Microsoft.NET\assembly\GAC_MSIL\Microsoft.CSharp\v4.0_4.0.0.0__b03f5f7f11d50a3a\Microsoft.CSharp.dll
- True
-
-
- C:\Windows\assembly\GAC_MSIL\Microsoft.Office.Interop.Outlook\15.0.0.0__71e9bce111e9429c\Microsoft.Office.Interop.Outlook.dll
- True
-
-
- C:\Windows\assembly\GAC_MSIL\office\15.0.0.0__71e9bce111e9429c\office.dll
- True
-
-
-
-
- Form
-
-
- MainForm.cs
-
-
-
-
-
-
+
+
+ Debug
+ AnyCPU
+ {A1B2C3D4-E5F6-47A8-90B1-C2D3E4F5A6B7}
+ WinExe
+ PstMerger
+ PstMerger
+ v4.8
+ 512
+ true
+
+
+ AnyCPU
+ true
+ full
+ false
+ bin\Debug\
+ DEBUG;TRACE
+ prompt
+ 4
+
+
+ AnyCPU
+ pdbonly
+ true
+ bin\Release\
+ TRACE
+ prompt
+ 4
+
+
+
+
+
+
+
+ C:\Windows\Microsoft.NET\assembly\GAC_MSIL\Microsoft.CSharp\v4.0_4.0.0.0__b03f5f7f11d50a3a\Microsoft.CSharp.dll
+ True
+
+
+ C:\Windows\assembly\GAC_MSIL\Microsoft.Office.Interop.Outlook\15.0.0.0__71e9bce111e9429c\Microsoft.Office.Interop.Outlook.dll
+ True
+
+
+ C:\Windows\assembly\GAC_MSIL\office\15.0.0.0__71e9bce111e9429c\office.dll
+ True
+
+
+
+
+ Form
+
+
+ MainForm.cs
+
+
+
+
+
+
diff --git a/PstService.cs b/PstService.cs
index 5ba4cbd..056eb8f 100644
--- a/PstService.cs
+++ b/PstService.cs
@@ -2,14 +2,33 @@
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
+using System.Security.Cryptography;
+using System.Text;
using Outlook = Microsoft.Office.Interop.Outlook;
namespace PstMerger
{
public class PstService
{
- public void MergeFiles(string[] sourceFiles, string destinationPst, System.Threading.CancellationToken ct, Action onProgress)
+ // Stores hashes of items already copied into the destination PST.
+ // Key format: "FolderName|Hash" — folder-agnostic dedup (same item in
+ // any folder counts as a duplicate of the same item elsewhere).
+ private readonly HashSet _seenHashes = new HashSet(StringComparer.OrdinalIgnoreCase);
+
+ private int _duplicatesSkipped = 0;
+ private bool _removeDuplicates = false;
+
+ public void MergeFiles(
+ string[] sourceFiles,
+ string destinationPst,
+ System.Threading.CancellationToken ct,
+ Action onProgress,
+ bool removeDuplicates = false)
{
+ _removeDuplicates = removeDuplicates;
+ _seenHashes.Clear();
+ _duplicatesSkipped = 0;
+
Outlook.Application outlookApp = null;
Outlook.NameSpace ns = null;
Outlook.Folder destRoot = null;
@@ -35,11 +54,20 @@ public void MergeFiles(string[] sourceFiles, string destinationPst, System.Threa
destRoot = GetRootFolder(ns, destinationPst, onProgress);
if (destRoot == null) throw new Exception("Could not find destination root.");
+ // If deduplication is on, pre-seed hashes from destination PST
+ // so we don't duplicate items already present in an existing master PST.
+ if (_removeDuplicates && File.Exists(destinationPst))
+ {
+ onProgress(0, "Scanning destination PST for existing items (dedup pre-seed)...");
+ SeedHashesFromFolder(destRoot, onProgress);
+ onProgress(0, string.Format("Pre-seed complete. {0} existing items indexed.", _seenHashes.Count));
+ }
+
int count = 0;
foreach (string sourceFile in sourceFiles)
{
if (ct.IsCancellationRequested) break;
-
+
// Skip if it's the destination itself
if (string.Equals(Path.GetFullPath(sourceFile), Path.GetFullPath(destinationPst), StringComparison.OrdinalIgnoreCase))
continue;
@@ -51,6 +79,11 @@ public void MergeFiles(string[] sourceFiles, string destinationPst, System.Threa
}
ns.RemoveStore(destRoot);
+
+ if (_removeDuplicates)
+ {
+ onProgress(0, string.Format("Deduplication complete. {0} duplicate item(s) skipped.", _duplicatesSkipped));
+ }
}
finally
{
@@ -59,6 +92,99 @@ public void MergeFiles(string[] sourceFiles, string destinationPst, System.Threa
}
}
+ // ------------------------------------------------------------------
+ // Pre-seeds _seenHashes by walking an existing destination folder tree.
+ // Uses hash-only keys (no folder path) so duplicates are detected
+ // regardless of which folder they live in.
+ // ------------------------------------------------------------------
+ private void SeedHashesFromFolder(Outlook.Folder folder, Action onProgress)
+ {
+ Outlook.Items items = folder.Items;
+ int count = items.Count;
+
+ for (int i = 1; i <= count; i++)
+ {
+ object item = null;
+ try
+ {
+ item = items[i];
+ string hash = GetItemHash(item);
+ if (!string.IsNullOrEmpty(hash))
+ _seenHashes.Add(hash);
+ }
+ catch { /* ignore individual item errors during seeding */ }
+ finally
+ {
+ if (item != null) Marshal.ReleaseComObject(item);
+ }
+ }
+ Marshal.ReleaseComObject(items);
+
+ Outlook.Folders subFolders = folder.Folders;
+ foreach (Outlook.Folder sub in subFolders)
+ {
+ SeedHashesFromFolder(sub, onProgress);
+ Marshal.ReleaseComObject(sub);
+ }
+ Marshal.ReleaseComObject(subFolders);
+ }
+
+ // ------------------------------------------------------------------
+ // Generates a deterministic fingerprint for any Outlook item type.
+ // Fields: Subject, SenderName/From, SentOn/CreationTime, Size, BodyLen.
+ // These are stable across PST copies and cover all common item types.
+ // ------------------------------------------------------------------
+ private string GetItemHash(object item)
+ {
+ try
+ {
+ string subject = "";
+ string sender = "";
+ string sentOn = "";
+ string size = "";
+ string bodyLen = "";
+
+ dynamic dyn = item;
+
+ // Subject — present on almost every item type
+ try { subject = (string)dyn.Subject ?? ""; } catch { }
+
+ // Sender / organiser / owner depending on type
+ try { sender = (string)dyn.SenderName ?? ""; }
+ catch
+ {
+ try { sender = (string)dyn.Organizer ?? ""; }
+ catch { try { sender = (string)dyn.From ?? ""; } catch { } }
+ }
+
+ // Sent / created timestamp
+ try { sentOn = ((DateTime)dyn.SentOn).ToString("o"); }
+ catch
+ {
+ try { sentOn = ((DateTime)dyn.Start).ToString("o"); }
+ catch { try { sentOn = ((DateTime)dyn.CreationTime).ToString("o"); } catch { } }
+ }
+
+ // Item size in bytes
+ try { size = ((int)dyn.Size).ToString(); } catch { }
+
+ // Body length as secondary discriminator
+ try { bodyLen = ((string)dyn.Body ?? "").Length.ToString(); } catch { }
+
+ string raw = string.Join("|", subject, sender, sentOn, size, bodyLen);
+ using (var md5 = MD5.Create())
+ {
+ byte[] bytes = md5.ComputeHash(Encoding.UTF8.GetBytes(raw));
+ return BitConverter.ToString(bytes).Replace("-", "");
+ }
+ }
+ catch
+ {
+ return null; // If we can't hash, let the item through
+ }
+ }
+
+ // ------------------------------------------------------------------
private void ProcessSourcePst(Outlook.NameSpace ns, string filePath, Outlook.Folder destRoot, System.Threading.CancellationToken ct, Action onProgress)
{
Outlook.Folder sourceRoot = null;
@@ -79,14 +205,22 @@ private void ProcessSourcePst(Outlook.NameSpace ns, string filePath, Outlook.Fol
}
}
- private void CopyFolders(Outlook.Folder sourceFolder, Outlook.Folder destFolder, System.Threading.CancellationToken ct, Action onProgress)
+ // ------------------------------------------------------------------
+ // Recursively copies folders. Dedup uses hash-only keys so a duplicate
+ // email in Inbox vs. Sent Items is still correctly detected.
+ // ------------------------------------------------------------------
+ private void CopyFolders(
+ Outlook.Folder sourceFolder,
+ Outlook.Folder destFolder,
+ System.Threading.CancellationToken ct,
+ Action onProgress)
{
if (ct.IsCancellationRequested) return;
// 1. Copy items in the current folder
Outlook.Items sourceItems = sourceFolder.Items;
int itemCount = sourceItems.Count;
-
+
for (int i = itemCount; i >= 1; i--)
{
if (ct.IsCancellationRequested) break;
@@ -96,9 +230,23 @@ private void CopyFolders(Outlook.Folder sourceFolder, Outlook.Folder destFolder,
try
{
item = sourceItems[i];
-
- // We copy and then move to preserve the source PST in case of failure
- // Use dynamic to call Copy/Move on any Outlook item type
+
+ // ----- Deduplication check (hash only — no folder path) -----
+ if (_removeDuplicates)
+ {
+ string hash = GetItemHash(item);
+ if (!string.IsNullOrEmpty(hash))
+ {
+ if (_seenHashes.Contains(hash))
+ {
+ _duplicatesSkipped++;
+ continue;
+ }
+ _seenHashes.Add(hash);
+ }
+ }
+ // ------------------------------------------------------------
+
dynamic dynItem = item;
copy = dynItem.Copy();
copy.Move(destFolder);
@@ -123,15 +271,14 @@ private void CopyFolders(Outlook.Folder sourceFolder, Outlook.Folder destFolder,
Outlook.Folder destSubFolder = null;
Outlook.Folders destFolders = destFolder.Folders;
-
- // Try to find if subfolder exists in destination, with retry for transient COM errors
+
int maxRetries = 3;
for (int attempt = 1; attempt <= maxRetries; attempt++)
{
try
{
destSubFolder = FindFolderByName(destFolders, sourceSubFolder.Name);
-
+
if (destSubFolder == null)
{
try
@@ -140,18 +287,15 @@ private void CopyFolders(Outlook.Folder sourceFolder, Outlook.Folder destFolder,
}
catch
{
- // Fallback: Try adding without type (sometimes needed for Root folders or special stores)
destSubFolder = destFolders.Add(sourceSubFolder.Name) as Outlook.Folder;
}
}
- break; // Success, exit retry loop
+ break;
}
catch (Exception ex)
{
if (attempt == maxRetries)
- {
onProgress(-1, string.Format("Error creating folder {0} after {1} attempts: {2}", sourceSubFolder.Name, maxRetries, ex.Message));
- }
else
{
onProgress(-1, string.Format("Retry {0}/{1} for folder {2}: {3}", attempt, maxRetries, sourceSubFolder.Name, ex.Message));
@@ -165,29 +309,28 @@ private void CopyFolders(Outlook.Folder sourceFolder, Outlook.Folder destFolder,
CopyFolders(sourceSubFolder, destSubFolder, ct, onProgress);
Marshal.ReleaseComObject(destSubFolder);
}
-
+
if (destFolders != null) Marshal.ReleaseComObject(destFolders);
if (sourceSubFolder != null) Marshal.ReleaseComObject(sourceSubFolder);
}
if (sourceSubFolders != null) Marshal.ReleaseComObject(sourceSubFolders);
}
+ // ------------------------------------------------------------------
private Outlook.Folder FindFolderByName(Outlook.Folders folders, string name)
{
foreach (Outlook.Folder f in folders)
{
if (string.Equals(f.Name, name, StringComparison.OrdinalIgnoreCase))
- {
return f;
- }
Marshal.ReleaseComObject(f);
}
return null;
}
+ // ------------------------------------------------------------------
private Outlook.Folder GetRootFolder(Outlook.NameSpace ns, string filePath, Action onProgress)
{
- // 1. Find the Store object first
Outlook.Store targetStore = null;
foreach (Outlook.Store store in ns.Stores)
{
@@ -200,51 +343,34 @@ private Outlook.Folder GetRootFolder(Outlook.NameSpace ns, string filePath, Acti
if (targetStore != null)
{
- // Try to get PR_IPM_SUBTREE_ENTRYID (0x35E00102)
try
{
const string PR_IPM_SUBTREE_ENTRYID = "http://schemas.microsoft.com/mapi/proptag/0x35E00102";
object ipmProp = targetStore.PropertyAccessor.GetProperty(PR_IPM_SUBTREE_ENTRYID);
-
+
string ipmEntryId = null;
if (ipmProp is string)
- {
ipmEntryId = (string)ipmProp;
- }
else if (ipmProp is byte[])
- {
- byte[] bytes = (byte[])ipmProp;
- ipmEntryId = BitConverter.ToString(bytes).Replace("-", "");
- }
-
+ ipmEntryId = BitConverter.ToString((byte[])ipmProp).Replace("-", "");
+
if (!string.IsNullOrEmpty(ipmEntryId))
{
var ipmRoot = ns.GetFolderFromID(ipmEntryId, targetStore.StoreID) as Outlook.Folder;
- if (ipmRoot != null)
- {
- return ipmRoot;
- }
+ if (ipmRoot != null) return ipmRoot;
}
}
- catch (Exception ex)
- {
- // Log warning only if verbose logging is enabled or critical
- // onProgress(0, string.Format("Warning: Failed to resolve IPM Subtree: {0}. Implementation will fallback to Store Root.", ex.Message));
- }
-
- // Fallback to Store Root will happen in the legacy loop below
+ catch { }
}
- // Fallback: Legacy loop
+ // Fallback: legacy loop
foreach (Outlook.Folder folder in ns.Folders)
{
try
{
- if (folder.Store != null)
- {
- if (string.Equals(folder.Store.FilePath, filePath, StringComparison.OrdinalIgnoreCase))
- return folder;
- }
+ if (folder.Store != null &&
+ string.Equals(folder.Store.FilePath, filePath, StringComparison.OrdinalIgnoreCase))
+ return folder;
}
catch { }
Marshal.ReleaseComObject(folder);
diff --git a/README.md b/README.md
index cc7271b..7e4168e 100644
--- a/README.md
+++ b/README.md
@@ -24,6 +24,7 @@ It was built specifically to handle **large mailboxes (up to 2TB)** and includes
- ✅ **Persistent logging** - every action saved to a log file
- ✅ **Disk space validation** - warns before running out of space
- ✅ **Error resilience** - continues processing even if individual items fail
+- ✅ **Remove Duplicates** - automatically detects and skips duplicate items across all folders using MD5 fingerprinting (Subject, Sender, Date, Size, Body length). Pre-seeds hashes from an existing destination PST so re-runs never re-import already-merged items. Enabled by default via the "Remove Duplicates" checkbox.
## Requirements
@@ -38,7 +39,8 @@ It was built specifically to handle **large mailboxes (up to 2TB)** and includes
3. **Click "Fix PST Size Limits"** to remove the 50GB restriction (one-time setup)
4. **Select source folder** containing your PST files
5. **Select destination** for the merged PST
-6. **Click "Start Merge"** and wait for completion
+6. **(Optional) Check "Remove Duplicates"** to skip items already present in the destination PST — enabled by default
+7. **Click "Start Merge"** and wait for completion
> ⚠️ **Important**: Close Outlook before running the merge. Do not use the computer for heavy tasks during the merge process.
@@ -94,4 +96,8 @@ MIT License - see [LICENSE](LICENSE) file.
**Mithun**
+## Contributor
+
+**Eslam Omar** — Added Remove Duplicates feature
+
© 2026 All Rights Reserved.