diff --git a/OutlookCaseHelper/Form1.cs b/OutlookCaseHelper/Form1.cs index 4832685..b91493a 100644 --- a/OutlookCaseHelper/Form1.cs +++ b/OutlookCaseHelper/Form1.cs @@ -37,10 +37,12 @@ namespace OutlookCaseHelper Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "OutlookCaseHelper", "hotkeys.json"); + // Singleton windows private DashboardForm? _dashboardForm; private HotkeySettingsForm? _settingsForm; private CreateRuleForm? _createRuleForm; private InputForm? _removeRuleForm; + private AboutForm? _aboutForm; // --- Constructor & Init --- @@ -67,6 +69,18 @@ namespace OutlookCaseHelper monitorTimer.Start(); } + protected override void OnFormClosed(FormClosedEventArgs e) + { + UnregisterHotKey(this.Handle, HOTKEY_CREATE); + UnregisterHotKey(this.Handle, HOTKEY_REMOVE); + monitorTimer.Stop(); + monitorTimer.Dispose(); + outlookHelper.Dispose(); + trayIcon.Visible = false; + trayIcon.Dispose(); + base.OnFormClosed(e); + } + // --- Hotkeys --- protected override void OnHandleCreated(EventArgs e) @@ -83,13 +97,6 @@ namespace OutlookCaseHelper RegisterHotKey(this.Handle, HOTKEY_REMOVE, hotkeyRemoveMod, hotkeyRemoveKey); } - protected override void OnFormClosed(FormClosedEventArgs e) - { - UnregisterHotKey(this.Handle, HOTKEY_CREATE); - UnregisterHotKey(this.Handle, HOTKEY_REMOVE); - base.OnFormClosed(e); - } - protected override void WndProc(ref Message m) { if (m.Msg == 0x0312) @@ -198,11 +205,27 @@ namespace OutlookCaseHelper private void Exit_Click(object? sender, EventArgs e) { - monitorTimer.Stop(); - trayIcon.Visible = false; Application.Exit(); } + // --- Singleton Window Helper --- + + private void ShowSingletonForm(Func getter, Action setter, Func create) where T : Form + { + var field = getter(); + if (field != null && !field.IsDisposed) + { + field.BringToFront(); + field.WindowState = FormWindowState.Normal; + return; + } + + var created = create(); + setter(created); + created.FormClosed += (s, e) => setter(null); + created.Show(); + } + // --- Tray Menu Handlers --- private void ToggleStartup_Click(object? sender, EventArgs e) @@ -254,13 +277,25 @@ namespace OutlookCaseHelper _dashboardForm.WindowState = FormWindowState.Normal; return; } - _dashboardForm = new DashboardForm(outlookHelper); _dashboardForm.Owner = this; _dashboardForm.FormClosed += (s, args) => _dashboardForm = null; _dashboardForm.Show(); } + private void About_Click(object? sender, EventArgs e) + { + if (_aboutForm != null && !_aboutForm.IsDisposed) + { + _aboutForm.BringToFront(); + _aboutForm.WindowState = FormWindowState.Normal; + return; + } + _aboutForm = new AboutForm(); + _aboutForm.FormClosed += (s, args) => _aboutForm = null; + _aboutForm.Show(); + } + private void RunAllRules_Click(object? sender, EventArgs e) { try @@ -278,8 +313,6 @@ namespace OutlookCaseHelper } } - private void About_Click(object? sender, EventArgs e) => new AboutForm().ShowDialog(); - // --- Rule Actions --- private void ProcessEmail_Click(object? sender, EventArgs e) @@ -308,7 +341,6 @@ namespace OutlookCaseHelper var ruleForm = new CreateRuleForm(trackingId, readonlyId: true); if (ruleForm.ShowDialog() != DialogResult.OK) return; - HandleCreate(trackingId, ruleForm.FolderName); } catch (Exception ex) { MessageBox.Show($"Error: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } @@ -334,7 +366,7 @@ namespace OutlookCaseHelper string folderName = _createRuleForm.FolderName; if (outlookHelper.FindRuleByTrackingId(trackingId) != null) - { MessageBox.Show($"Rule for TrackingID#{trackingId} already exists!", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); } + MessageBox.Show($"Rule for TrackingID#{trackingId} already exists!", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); else if (outlookHelper.ExistsInClosed(trackingId)) HandleReopen(trackingId); else @@ -423,7 +455,7 @@ namespace OutlookCaseHelper private void HandleRemove(string folderName) { if (outlookHelper.RemoveRuleAndMoveToClosed(folderName)) - MessageBox.Show($"Rule removed!\n\nFolder moved to: Inbox > Cases > Closed\nFolder: {folderName}\nMonitoring stopped.", + MessageBox.Show($"Rule removed!\n\nFolder moved to Closed.\nFolder: {folderName}\nMonitoring stopped.", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); else MessageBox.Show("Error removing rule. Check if folder exists.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); @@ -513,10 +545,7 @@ namespace OutlookCaseHelper string folderName = ruleForm.FolderName; if (outlookHelper.FindRuleByTrackingId(trackingId) != null) - { - MessageBox.Show($"Rule for TrackingID#{trackingId} already exists!", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); - return; - } + { MessageBox.Show($"Rule for TrackingID#{trackingId} already exists!", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (outlookHelper.ExistsInClosed(trackingId)) { @@ -558,8 +587,10 @@ namespace OutlookCaseHelper var txt = new TextBox { Left = 20, Top = 40, Width = 370, Height = 24, Text = rule.FolderName }; var btnOk = new Button { Text = "Rename", Left = 210, Top = 80, Width = 80, DialogResult = DialogResult.OK }; var btnCancel = new Button { Text = "Cancel", Left = 300, Top = 80, Width = 80, DialogResult = DialogResult.Cancel }; + btnCancel.Click += (s, e) => inputForm.Close(); inputForm.Controls.AddRange(new Control[] { lbl, txt, btnOk, btnCancel }); - inputForm.AcceptButton = btnOk; inputForm.CancelButton = btnCancel; + inputForm.AcceptButton = btnOk; + inputForm.CancelButton = btnCancel; if (inputForm.ShowDialog() != DialogResult.OK) return; string newName = txt.Text.Trim(); @@ -580,11 +611,11 @@ namespace OutlookCaseHelper if (rule == null) return; if (MessageBox.Show( - $"Are you sure you want to close this case?\n\nFolder: {rule.FolderName}\n\nThe folder will be moved to Inbox > Cases > Closed.", + $"Are you sure you want to close this case?\n\nFolder: {rule.FolderName}\n\nThe folder will be moved to Closed.", "Close Case", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return; if (outlookHelper.RemoveRuleAndMoveToClosed(rule.FolderName)) - { MessageBox.Show("Case closed!\n\nFolder moved to: Inbox > Cases > Closed.", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); LoadRules(); } + { MessageBox.Show("Case closed!\n\nFolder moved to Closed.", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); LoadRules(); } else MessageBox.Show("Error closing case.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } @@ -668,6 +699,7 @@ namespace OutlookCaseHelper var btnOk = new Button { Text = "Save", Left = 216, Top = 200, Width = 80, DialogResult = DialogResult.OK }; var btnCancel = new Button { Text = "Cancel", Left = 304, Top = 200, Width = 80, DialogResult = DialogResult.Cancel }; + btnCancel.Click += (s, e) => this.Close(); this.Controls.AddRange(new Control[] { lblCreate, chkCreateAlt, chkCreateCtrl, chkCreateShift, cmbCreateKey, @@ -720,7 +752,8 @@ namespace OutlookCaseHelper uint rm = GetMod(chkRemoveAlt, chkRemoveCtrl, chkRemoveShift); if (cm == 0 || rm == 0) { - MessageBox.Show("Please select at least one modifier (Alt, Ctrl or Shift) for each shortcut.", "Validation", MessageBoxButtons.OK, MessageBoxIcon.Warning); + MessageBox.Show("Please select at least one modifier (Alt, Ctrl or Shift) for each shortcut.", + "Validation", MessageBoxButtons.OK, MessageBoxIcon.Warning); e.Cancel = true; return; } CreateMod = cm; CreateKey = GetKeyCode(cmbCreateKey); @@ -761,6 +794,7 @@ namespace OutlookCaseHelper var btnOk = new Button { Text = "OK", Left = 220, Top = 158, Width = 80, DialogResult = DialogResult.OK }; var btnCancel = new Button { Text = "Cancel", Left = 310, Top = 158, Width = 80, DialogResult = DialogResult.Cancel }; + btnCancel.Click += (s, e) => this.Close(); txtId.TextChanged += (s, e) => UpdatePreview(txtId.Text.Trim()); txtName.TextChanged += (s, e) => UpdatePreview(txtId.Text.Trim()); @@ -812,6 +846,7 @@ namespace OutlookCaseHelper txtInput = new TextBox { Left = 20, Top = 45, Width = 330, Height = 24 }; var btnOk = new Button { Text = "OK", Left = 155, Top = 80, Width = 80, DialogResult = DialogResult.OK }; var btnCancel = new Button { Text = "Cancel", Left = 245, Top = 80, Width = 80, DialogResult = DialogResult.Cancel }; + btnCancel.Click += (s, e) => this.Close(); this.Controls.AddRange(new Control[] { label, txtInput, btnOk, btnCancel }); this.AcceptButton = btnOk; @@ -848,15 +883,15 @@ namespace OutlookCaseHelper var lblDesc = new Label { Text = "Automatically organizes Outlook emails by TrackingID\ninto folders, keeping your inbox clean and cases managed.", Left = 30, Top = 178, Width = 330, Height = 40, TextAlign = ContentAlignment.MiddleCenter, Font = new Font("Segoe UI", 9) }; var separator = new Label { Left = 20, Top = 228, Width = 350, Height = 1, BorderStyle = BorderStyle.Fixed3D }; var lblCreatedBy = new Label { Text = "Created by Wellington Ribeiro", Left = 20, Top = 238, Width = 350, Height = 18, TextAlign = ContentAlignment.MiddleCenter, Font = new Font("Segoe UI", 9, FontStyle.Bold) }; - var lblEmail = new LinkLabel { Text = "wribeiro@microsoft.com", Left = 20, Top = 258, Width = 350, Height = 18, TextAlign = ContentAlignment.MiddleCenter, Font = new Font("Segoe UI", 9) }; + var lblSuggestions = new Label { Text = "For suggestions or bugs, contact the email above.", Left = 20, Top = 278, Width = 350, Height = 18, TextAlign = ContentAlignment.MiddleCenter, Font = new Font("Segoe UI", 8, FontStyle.Italic), ForeColor = Color.Gray }; + var btnClose = new Button { Text = "Close", Left = 150, Top = 302, Width = 90, DialogResult = DialogResult.Cancel }; + lblEmail.LinkClicked += (s, e) => { try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = "mailto:wribeiro@microsoft.com?subject=Outlook Case Manager - Feedback", UseShellExecute = true }); } catch { } }; - - var lblSuggestions = new Label { Text = "For suggestions or bugs, contact the email above.", Left = 20, Top = 278, Width = 350, Height = 18, TextAlign = ContentAlignment.MiddleCenter, Font = new Font("Segoe UI", 8, FontStyle.Italic), ForeColor = Color.Gray }; - var btnClose = new Button { Text = "Close", Left = 150, Top = 302, Width = 90, DialogResult = DialogResult.Cancel }; + btnClose.Click += (s, e) => this.Close(); this.Controls.AddRange(new Control[] { pictureBox, lblName, lblVersion, lblDate, lblDesc, separator, lblCreatedBy, lblEmail, lblSuggestions, btnClose }); this.CancelButton = btnClose; diff --git a/OutlookCaseHelper/OutlookHelper.cs b/OutlookCaseHelper/OutlookHelper.cs index 95fc6e4..aa8cec4 100644 --- a/OutlookCaseHelper/OutlookHelper.cs +++ b/OutlookCaseHelper/OutlookHelper.cs @@ -8,7 +8,7 @@ using Outlook = Microsoft.Office.Interop.Outlook; namespace OutlookCaseHelper { - public class OutlookHelper + public class OutlookHelper : IDisposable { private Outlook.Application? outlookApp; private Outlook.NameSpace? outlookNamespace; @@ -16,6 +16,7 @@ namespace OutlookCaseHelper private readonly string rulesFilePath; private Outlook.Items? inboxItems; private Outlook.Items? sentItems; + private bool disposed = false; public class RuleInfo { @@ -28,18 +29,53 @@ namespace OutlookCaseHelper { rulesFilePath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), - "OutlookCaseHelper", - "active_rules.json"); + "OutlookCaseHelper", "active_rules.json"); Directory.CreateDirectory(Path.GetDirectoryName(rulesFilePath)!); LoadRules(); } + // --- Dispose --- + + public void Dispose() + { + if (disposed) return; + disposed = true; + + try + { + if (inboxItems != null) + { + inboxItems.ItemAdd -= OnEmailReceived; + System.Runtime.InteropServices.Marshal.ReleaseComObject(inboxItems); + inboxItems = null; + } + if (sentItems != null) + { + sentItems.ItemAdd -= OnEmailSent; + System.Runtime.InteropServices.Marshal.ReleaseComObject(sentItems); + sentItems = null; + } + if (outlookNamespace != null) + { + System.Runtime.InteropServices.Marshal.ReleaseComObject(outlookNamespace); + outlookNamespace = null; + } + if (outlookApp != null) + { + System.Runtime.InteropServices.Marshal.ReleaseComObject(outlookApp); + outlookApp = null; + } + } + catch { } + + GC.SuppressFinalize(this); + } + // --- Outlook Connection --- private bool EnsureOutlookConnected() { if (outlookApp != null && outlookNamespace != null) return true; - try { if (System.Diagnostics.Process.GetProcessesByName("OUTLOOK").Length == 0) @@ -84,20 +120,18 @@ namespace OutlookCaseHelper { try { - if (outlookNamespace == null) return; - if (activeRules.Count == 0) return; - if (item is not Outlook.MailItem mail) return; - if (mail.Subject == null) return; + if (outlookNamespace == null || activeRules.Count == 0) return; + if (item is not Outlook.MailItem mail || mail.Subject == null) return; foreach (var folderName in activeRules) { string trackingId = ExtractTrackingId(folderName); if (!mail.Subject.Contains($"TrackingID#{trackingId}")) continue; - var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); - if (inbox == null) return; + var activeFolder = FindFolderByNameAnywhere("Active"); + if (activeFolder == null) return; - var target = GetOrCreateFolder(GetOrCreateFolder(GetOrCreateFolder(inbox, "Cases"), "Active"), folderName); + var target = GetOrCreateFolder(activeFolder, folderName); mail.Move(target); break; } @@ -168,9 +202,7 @@ namespace OutlookCaseHelper try { if (!EnsureOutlookConnected()) return false; - var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); - if (inbox == null) return false; - var closed = GetFolder(GetFolder(inbox, "Cases"), "Closed"); + var closed = FindFolderByNameAnywhere("Closed"); return closed != null && GetFolderStartingWith(closed, trackingId) != null; } catch { return false; } @@ -181,23 +213,33 @@ namespace OutlookCaseHelper if (!EnsureOutlookConnected()) return false; try { - var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); - if (inbox == null) return false; + var activeFolder = FindFolderByNameAnywhere("Active"); + if (activeFolder == null) + { + var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); + if (inbox == null) return false; + var cases = GetOrCreateFolder(inbox, "Cases"); + activeFolder = GetOrCreateFolder(cases, "Active"); + } - var target = GetOrCreateFolder(GetOrCreateFolder(GetOrCreateFolder(inbox, "Cases"), "Active"), folderName); + var target = GetOrCreateFolder(activeFolder, folderName); string filter = $"@SQL=\"urn:schemas:httpmail:subject\" LIKE '%TrackingID#{trackingId}%'"; - MoveFilteredEmails(inbox, filter, target); - + var inbox2 = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); var sent = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderSentMail); + + if (inbox2 != null) MoveFilteredEmails(inbox2, filter, target); if (sent != null) MoveFilteredEmails(sent, filter, target); - foreach (Outlook.Folder folder in inbox.Parent.Folders) + if (inbox2 != null) { - if (folder.Name != "Cases" && - folder.EntryID != inbox.EntryID && - folder.EntryID != sent?.EntryID) - try { MoveFilteredEmails(folder, filter, target); } catch { } + foreach (Outlook.Folder folder in inbox2.Parent.Folders) + { + if (folder.Name != "Cases" && + folder.EntryID != inbox2.EntryID && + folder.EntryID != sent?.EntryID) + try { MoveFilteredEmails(folder, filter, target); } catch { } + } } activeRules.Add(folderName); @@ -216,15 +258,22 @@ namespace OutlookCaseHelper if (!EnsureOutlookConnected()) return false; try { - var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); - if (inbox == null) return false; + var activeFolder = FindFolderByNameAnywhere("Active"); + if (activeFolder == null) return false; - var cases = GetFolder(inbox, "Cases"); - var active = GetFolder(cases, "Active"); - var folder = GetFolder(active, folderName); + var folder = GetFolder(activeFolder, folderName); if (folder == null) return false; - folder.MoveTo(GetOrCreateFolder(cases!, "Closed")); + var closedFolder = FindFolderByNameAnywhere("Closed"); + if (closedFolder == null) + { + var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); + if (inbox == null) return false; + var cases = GetOrCreateFolder(inbox, "Cases"); + closedFolder = GetOrCreateFolder(cases, "Closed"); + } + + folder.MoveTo(closedFolder); activeRules.Remove(folderName); SaveRules(); return true; @@ -237,9 +286,8 @@ namespace OutlookCaseHelper if (!EnsureOutlookConnected()) return false; try { - var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); - var active = GetFolder(GetFolder(inbox, "Cases"), "Active"); - var folder = GetFolder(active, oldFolderName); + var activeFolder = FindFolderByNameAnywhere("Active"); + var folder = GetFolder(activeFolder, oldFolderName); if (folder == null) return false; folder.Name = newFolderName; @@ -256,20 +304,24 @@ namespace OutlookCaseHelper if (!EnsureOutlookConnected()) return false; try { - var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); - if (inbox == null) return false; + var closedFolder = FindFolderByNameAnywhere("Closed"); + if (closedFolder == null) return false; - var cases = GetOrCreateFolder(inbox, "Cases"); - var active = GetOrCreateFolder(cases, "Active"); - var closed = GetFolder(cases, "Closed"); - if (closed == null) return false; - - var closedTracking = GetFolderStartingWith(closed, trackingId); + var closedTracking = GetFolderStartingWith(closedFolder, trackingId); if (closedTracking == null) return false; string existingName = closedTracking.Name; - var existingActive = GetFolder(active, existingName); + var activeFolder = FindFolderByNameAnywhere("Active"); + if (activeFolder == null) + { + var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); + if (inbox == null) return false; + var cases = GetOrCreateFolder(inbox, "Cases"); + activeFolder = GetOrCreateFolder(cases, "Active"); + } + + var existingActive = GetFolder(activeFolder, existingName); if (existingActive != null) { var toMove = new List(); @@ -280,12 +332,12 @@ namespace OutlookCaseHelper } else { - closedTracking.MoveTo(active); + closedTracking.MoveTo(activeFolder); } try { - var target = GetFolder(active, existingName); + var target = GetFolder(activeFolder, existingName); if (triggerEmail != null && target != null) triggerEmail.Move(target); } @@ -299,8 +351,7 @@ namespace OutlookCaseHelper { try { - var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); - var active = GetFolder(GetFolder(inbox, "Cases"), "Active"); + var active = FindFolderByNameAnywhere("Active"); var moved = GetFolderStartingWith(active!, trackingId); if (moved != null) { activeRules.Add(moved.Name); SaveRules(); return true; } } @@ -320,13 +371,18 @@ namespace OutlookCaseHelper var sent = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderSentMail); if (inbox == null) return; - var active = GetOrCreateFolder(GetOrCreateFolder(inbox, "Cases"), "Active"); + var activeFolder = FindFolderByNameAnywhere("Active"); + if (activeFolder == null) + { + var cases = GetOrCreateFolder(inbox, "Cases"); + activeFolder = GetOrCreateFolder(cases, "Active"); + } foreach (var folderName in activeRules) { string trackingId = ExtractTrackingId(folderName); string filter = $"@SQL=\"urn:schemas:httpmail:subject\" LIKE '%TrackingID#{trackingId}%'"; - var target = GetOrCreateFolder(active, folderName); + var target = GetOrCreateFolder(activeFolder, folderName); MoveFilteredEmails(inbox, filter, target); if (sent != null) MoveFilteredEmails(sent, filter, target); @@ -343,18 +399,22 @@ namespace OutlookCaseHelper var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); if (inbox == null) return; - var cases = GetOrCreateFolder(inbox, "Cases"); - var active = GetOrCreateFolder(cases, "Active"); + var activeFolder = FindFolderByNameAnywhere("Active"); + if (activeFolder == null) + { + var cases = GetOrCreateFolder(inbox, "Cases"); + activeFolder = GetOrCreateFolder(cases, "Active"); + } var allFolders = new List(); if (inbox.Parent is Outlook.Folder root) - GetAllFolders(root, allFolders, cases.EntryID); + GetAllFolders(root, allFolders, activeFolder.EntryID); foreach (var folderName in activeRules) { string trackingId = ExtractTrackingId(folderName); string filter = $"@SQL=\"urn:schemas:httpmail:subject\" LIKE '%TrackingID#{trackingId}%'"; - var target = GetOrCreateFolder(active, folderName); + var target = GetOrCreateFolder(activeFolder, folderName); foreach (var folder in allFolders) try { MoveFilteredEmails(folder, filter, target); } catch { } @@ -369,13 +429,12 @@ namespace OutlookCaseHelper if (!EnsureOutlookConnected()) return result; try { - var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); - var active = GetFolder(GetFolder(inbox, "Cases"), "Active"); - if (active == null) return result; + var activeFolder = FindFolderByNameAnywhere("Active"); + if (activeFolder == null) return result; foreach (var folderName in activeRules) { - var folder = GetFolder(active, folderName); + var folder = GetFolder(activeFolder, folderName); result.Add(new RuleInfo { FolderName = folderName, @@ -388,6 +447,34 @@ namespace OutlookCaseHelper return result; } + // --- Folder Search (any hierarchy level) --- + + private Outlook.Folder? FindFolderByNameAnywhere(string name) + { + try + { + var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); + if (inbox?.Parent is not Outlook.Folder root) return null; + return SearchFolderRecursive(root, name); + } + catch { return null; } + } + + private Outlook.Folder? SearchFolderRecursive(Outlook.Folder parent, string name) + { + foreach (Outlook.Folder folder in parent.Folders) + { + try + { + if (folder.Name == name) return folder; + var found = SearchFolderRecursive(folder, name); + if (found != null) return found; + } + catch { } + } + return null; + } + // --- Helpers --- private string ExtractTrackingId(string folderName)