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.