Compare commits

...

2 Commits

Author SHA1 Message Date
wellington c680bb5571 General error: cancel, multiple popup and startup rule to see the folders
Enhance OutlookCaseHelper with performance and stability fixes

Improved threading for Outlook COM objects to use STA, preventing threading violations. Added guards to manage dialog instances and introduced a helper method to reduce code duplication for form handling. Enhanced error handling for better user feedback and implemented caching for folder references to optimize performance. Refined rule management logic, fixed regex for tracking ID extraction, and ensured proper COM object management to prevent memory leaks. General code cleanup for improved readability and maintainability.
2026-03-30 19:58:15 +01:00
wellington 9be729101b Refactoring cancel buttons, also removing memory leak 2026-03-27 00:14:20 +00:00
2 changed files with 352 additions and 129 deletions
+96 -64
View File
@@ -4,7 +4,7 @@ using System.Windows.Forms;
using System.IO; using System.IO;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text.Json; using System.Text.Json;
using System.Threading.Tasks; using System.Threading;
using Microsoft.Win32; using Microsoft.Win32;
using Outlook = Microsoft.Office.Interop.Outlook; using Outlook = Microsoft.Office.Interop.Outlook;
@@ -37,10 +37,16 @@ namespace OutlookCaseHelper
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"OutlookCaseHelper", "hotkeys.json"); "OutlookCaseHelper", "hotkeys.json");
// Singleton windows
private DashboardForm? _dashboardForm; private DashboardForm? _dashboardForm;
private HotkeySettingsForm? _settingsForm; private HotkeySettingsForm? _settingsForm;
private CreateRuleForm? _createRuleForm; private CreateRuleForm? _createRuleForm;
private InputForm? _removeRuleForm; private InputForm? _removeRuleForm;
private AboutForm? _aboutForm;
// FIX #3: Guard to prevent multiple ProcessEmail dialogs from opening
// (e.g., hotkey spam or rapid tray double-clicks)
private Form? _activeProcessDialog;
// --- Constructor & Init --- // --- Constructor & Init ---
@@ -67,6 +73,18 @@ namespace OutlookCaseHelper
monitorTimer.Start(); 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 --- // --- Hotkeys ---
protected override void OnHandleCreated(EventArgs e) protected override void OnHandleCreated(EventArgs e)
@@ -83,13 +101,6 @@ namespace OutlookCaseHelper
RegisterHotKey(this.Handle, HOTKEY_REMOVE, hotkeyRemoveMod, hotkeyRemoveKey); 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) protected override void WndProc(ref Message m)
{ {
if (m.Msg == 0x0312) if (m.Msg == 0x0312)
@@ -198,11 +209,28 @@ namespace OutlookCaseHelper
private void Exit_Click(object? sender, EventArgs e) private void Exit_Click(object? sender, EventArgs e)
{ {
monitorTimer.Stop();
trayIcon.Visible = false;
Application.Exit(); Application.Exit();
} }
// --- Singleton Window Helper ---
// FIX #6: Now actively used by ViewRules_Click and About_Click
private void ShowSingletonForm<T>(Func<T?> getter, Action<T?> setter, Func<T> 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 --- // --- Tray Menu Handlers ---
private void ToggleStartup_Click(object? sender, EventArgs e) private void ToggleStartup_Click(object? sender, EventArgs e)
@@ -246,31 +274,43 @@ namespace OutlookCaseHelper
_settingsForm.Show(); _settingsForm.Show();
} }
// FIX #6: Using ShowSingletonForm helper instead of duplicated manual logic
private void ViewRules_Click(object? sender, EventArgs e) private void ViewRules_Click(object? sender, EventArgs e)
{ => ShowSingletonForm(
if (_dashboardForm != null && !_dashboardForm.IsDisposed) () => _dashboardForm,
{ v => _dashboardForm = v,
_dashboardForm.BringToFront(); () => { var f = new DashboardForm(outlookHelper); f.Owner = this; return f; });
_dashboardForm.WindowState = FormWindowState.Normal;
return;
}
_dashboardForm = new DashboardForm(outlookHelper); // FIX #6: Using ShowSingletonForm helper instead of duplicated manual logic
_dashboardForm.Owner = this; private void About_Click(object? sender, EventArgs e)
_dashboardForm.FormClosed += (s, args) => _dashboardForm = null; => ShowSingletonForm(
_dashboardForm.Show(); () => _aboutForm,
} v => _aboutForm = v,
() => new AboutForm());
// FIX #5: Use STA thread instead of Task.Run to avoid COM threading violation.
// Outlook COM objects require STA (Single-Threaded Apartment).
// Task.Run uses ThreadPool which is MTA, causing silent failures or crashes.
private void RunAllRules_Click(object? sender, EventArgs e) private void RunAllRules_Click(object? sender, EventArgs e)
{ {
try try
{ {
trayIcon.ShowBalloonTip(2000, "Outlook Case Manager", "Running all rules...", ToolTipIcon.Info); trayIcon.ShowBalloonTip(2000, "Outlook Case Manager", "Running all rules...", ToolTipIcon.Info);
Task.Run(() => var thread = new Thread(() =>
{ {
outlookHelper.RunAllRules(); try
this.Invoke(() => trayIcon.ShowBalloonTip(3000, "Outlook Case Manager", "All rules applied successfully!", ToolTipIcon.Info)); {
outlookHelper.RunAllRules();
this.Invoke(() => trayIcon.ShowBalloonTip(3000, "Outlook Case Manager", "All rules applied successfully!", ToolTipIcon.Info));
}
catch (Exception ex)
{
this.Invoke(() => MessageBox.Show($"Error running rules: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error));
}
}); });
thread.SetApartmentState(ApartmentState.STA);
thread.IsBackground = true;
thread.Start();
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -278,12 +318,18 @@ namespace OutlookCaseHelper
} }
} }
private void About_Click(object? sender, EventArgs e) => new AboutForm().ShowDialog();
// --- Rule Actions --- // --- Rule Actions ---
// FIX #3: Added _activeProcessDialog guard to prevent multiple dialogs opening
// when hotkey is pressed rapidly or tray icon is double-clicked quickly.
private void ProcessEmail_Click(object? sender, EventArgs e) private void ProcessEmail_Click(object? sender, EventArgs e)
{ {
if (_activeProcessDialog != null && !_activeProcessDialog.IsDisposed)
{
_activeProcessDialog.BringToFront();
return;
}
try try
{ {
var trackingId = outlookHelper.GetSelectedEmailTrackingId(); var trackingId = outlookHelper.GetSelectedEmailTrackingId();
@@ -307,8 +353,10 @@ namespace OutlookCaseHelper
} }
var ruleForm = new CreateRuleForm(trackingId, readonlyId: true); var ruleForm = new CreateRuleForm(trackingId, readonlyId: true);
if (ruleForm.ShowDialog() != DialogResult.OK) return; _activeProcessDialog = ruleForm;
ruleForm.FormClosed += (s, args) => _activeProcessDialog = null;
if (ruleForm.ShowDialog() != DialogResult.OK) return;
HandleCreate(trackingId, ruleForm.FolderName); HandleCreate(trackingId, ruleForm.FolderName);
} }
catch (Exception ex) { MessageBox.Show($"Error: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } catch (Exception ex) { MessageBox.Show($"Error: {ex.Message}", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); }
@@ -334,7 +382,7 @@ namespace OutlookCaseHelper
string folderName = _createRuleForm.FolderName; string folderName = _createRuleForm.FolderName;
if (outlookHelper.FindRuleByTrackingId(trackingId) != null) 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)) else if (outlookHelper.ExistsInClosed(trackingId))
HandleReopen(trackingId); HandleReopen(trackingId);
else else
@@ -423,7 +471,7 @@ namespace OutlookCaseHelper
private void HandleRemove(string folderName) private void HandleRemove(string folderName)
{ {
if (outlookHelper.RemoveRuleAndMoveToClosed(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); "Success", MessageBoxButtons.OK, MessageBoxIcon.Information);
else else
MessageBox.Show("Error removing rule. Check if folder exists.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); MessageBox.Show("Error removing rule. Check if folder exists.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
@@ -485,7 +533,6 @@ namespace OutlookCaseHelper
{ {
listView.Items.Clear(); listView.Items.Clear();
var rules = outlookHelper.GetActiveRulesInfo(); var rules = outlookHelper.GetActiveRulesInfo();
if (rules.Count == 0) if (rules.Count == 0)
{ {
var empty = new ListViewItem("No active rules found."); var empty = new ListViewItem("No active rules found.");
@@ -493,7 +540,6 @@ namespace OutlookCaseHelper
listView.Items.Add(empty); listView.Items.Add(empty);
return; return;
} }
foreach (var rule in rules) foreach (var rule in rules)
{ {
var item = new ListViewItem(rule.FolderName); var item = new ListViewItem(rule.FolderName);
@@ -508,39 +554,28 @@ namespace OutlookCaseHelper
{ {
var ruleForm = new CreateRuleForm("", readonlyId: false); var ruleForm = new CreateRuleForm("", readonlyId: false);
if (ruleForm.ShowDialog() != DialogResult.OK) return; if (ruleForm.ShowDialog() != DialogResult.OK) return;
string trackingId = ruleForm.TrackingId; string trackingId = ruleForm.TrackingId;
string folderName = ruleForm.FolderName; string folderName = ruleForm.FolderName;
if (outlookHelper.FindRuleByTrackingId(trackingId) != null) 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)) if (outlookHelper.ExistsInClosed(trackingId))
{ {
if (outlookHelper.ReopenFromClosed(trackingId)) if (outlookHelper.ReopenFromClosed(trackingId))
{ MessageBox.Show($"Case reopened!\n\nTrackingID: {trackingId}", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); LoadRules(); } { MessageBox.Show($"Case reopened!\n\nTrackingID: {trackingId}", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); LoadRules(); }
else else MessageBox.Show("Error reopening case.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
MessageBox.Show("Error reopening case.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
return; return;
} }
if (outlookHelper.CreateFolderAndMoveEmails(trackingId, folderName)) if (outlookHelper.CreateFolderAndMoveEmails(trackingId, folderName))
{ MessageBox.Show($"Rule created!\n\nFolder: {folderName}", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); LoadRules(); } { MessageBox.Show($"Rule created!\n\nFolder: {folderName}", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); LoadRules(); }
else else MessageBox.Show("Error creating rule. Make sure Outlook is open.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
MessageBox.Show("Error creating rule. Make sure Outlook is open.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
private void BtnRename_Click(object? sender, EventArgs e) private void BtnRename_Click(object? sender, EventArgs e)
{ {
if (listView.SelectedItems.Count == 0) if (listView.SelectedItems.Count == 0)
{ MessageBox.Show("Please select a rule to rename.", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } { MessageBox.Show("Please select a rule to rename.", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }
var rule = listView.SelectedItems[0].Tag as OutlookHelper.RuleInfo; var rule = listView.SelectedItems[0].Tag as OutlookHelper.RuleInfo;
if (rule == null) return; if (rule == null) return;
var inputForm = new Form var inputForm = new Form
{ {
Text = "Rename Rule", Text = "Rename Rule",
@@ -553,40 +588,33 @@ namespace OutlookCaseHelper
StartPosition = FormStartPosition.CenterScreen StartPosition = FormStartPosition.CenterScreen
}; };
try { inputForm.Icon = new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "casenew.ico")); } catch { } try { inputForm.Icon = new Icon(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "casenew.ico")); } catch { }
var lbl = new Label { Text = $"New name for TrackingID#{rule.TrackingId}:", Left = 20, Top = 15, Width = 370, Height = 20 }; var lbl = new Label { Text = $"New name for TrackingID#{rule.TrackingId}:", Left = 20, Top = 15, Width = 370, Height = 20 };
var txt = new TextBox { Left = 20, Top = 40, Width = 370, Height = 24, Text = rule.FolderName }; 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 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 }; 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.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; if (inputForm.ShowDialog() != DialogResult.OK) return;
string newName = txt.Text.Trim(); string newName = txt.Text.Trim();
if (string.IsNullOrEmpty(newName) || newName == rule.FolderName) return; if (string.IsNullOrEmpty(newName) || newName == rule.FolderName) return;
if (outlookHelper.RenameRule(rule.FolderName, newName)) if (outlookHelper.RenameRule(rule.FolderName, newName))
{ MessageBox.Show($"Rule renamed!\n\n{rule.FolderName} {newName}", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); LoadRules(); } { MessageBox.Show($"Rule renamed!\n\n{rule.FolderName} -> {newName}", "Success", MessageBoxButtons.OK, MessageBoxIcon.Information); LoadRules(); }
else else MessageBox.Show("Error renaming rule.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
MessageBox.Show("Error renaming rule.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
private void BtnClose_Click(object? sender, EventArgs e) private void BtnClose_Click(object? sender, EventArgs e)
{ {
if (listView.SelectedItems.Count == 0) if (listView.SelectedItems.Count == 0)
{ MessageBox.Show("Please select a rule to close.", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } { MessageBox.Show("Please select a rule to close.", "Warning", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; }
var rule = listView.SelectedItems[0].Tag as OutlookHelper.RuleInfo; var rule = listView.SelectedItems[0].Tag as OutlookHelper.RuleInfo;
if (rule == null) return; if (rule == null) return;
if (MessageBox.Show( 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; "Close Case", MessageBoxButtons.YesNo, MessageBoxIcon.Question) != DialogResult.Yes) return;
if (outlookHelper.RemoveRuleAndMoveToClosed(rule.FolderName)) 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 else MessageBox.Show("Error closing case.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
MessageBox.Show("Error closing case.", "Error", MessageBoxButtons.OK, MessageBoxIcon.Error);
} }
private void BtnReload_Click(object? sender, EventArgs e) private void BtnReload_Click(object? sender, EventArgs e)
@@ -668,6 +696,7 @@ namespace OutlookCaseHelper
var btnOk = new Button { Text = "Save", Left = 216, Top = 200, Width = 80, DialogResult = DialogResult.OK }; 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 }; 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[] { this.Controls.AddRange(new Control[] {
lblCreate, chkCreateAlt, chkCreateCtrl, chkCreateShift, cmbCreateKey, lblCreate, chkCreateAlt, chkCreateCtrl, chkCreateShift, cmbCreateKey,
@@ -720,7 +749,8 @@ namespace OutlookCaseHelper
uint rm = GetMod(chkRemoveAlt, chkRemoveCtrl, chkRemoveShift); uint rm = GetMod(chkRemoveAlt, chkRemoveCtrl, chkRemoveShift);
if (cm == 0 || rm == 0) 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; e.Cancel = true; return;
} }
CreateMod = cm; CreateKey = GetKeyCode(cmbCreateKey); CreateMod = cm; CreateKey = GetKeyCode(cmbCreateKey);
@@ -761,6 +791,7 @@ namespace OutlookCaseHelper
var btnOk = new Button { Text = "OK", Left = 220, Top = 158, Width = 80, DialogResult = DialogResult.OK }; 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 }; 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()); txtId.TextChanged += (s, e) => UpdatePreview(txtId.Text.Trim());
txtName.TextChanged += (s, e) => UpdatePreview(txtId.Text.Trim()); txtName.TextChanged += (s, e) => UpdatePreview(txtId.Text.Trim());
@@ -812,6 +843,7 @@ namespace OutlookCaseHelper
txtInput = new TextBox { Left = 20, Top = 45, Width = 330, Height = 24 }; 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 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 }; 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.Controls.AddRange(new Control[] { label, txtInput, btnOk, btnCancel });
this.AcceptButton = btnOk; this.AcceptButton = btnOk;
@@ -848,15 +880,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 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 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 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 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) => 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 { } try { System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo { FileName = "mailto:wribeiro@microsoft.com?subject=Outlook Case Manager - Feedback", UseShellExecute = true }); } catch { }
}; };
btnClose.Click += (s, e) => this.Close();
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 };
this.Controls.AddRange(new Control[] { pictureBox, lblName, lblVersion, lblDate, lblDesc, separator, lblCreatedBy, lblEmail, lblSuggestions, btnClose }); this.Controls.AddRange(new Control[] { pictureBox, lblName, lblVersion, lblDate, lblDesc, separator, lblCreatedBy, lblEmail, lblSuggestions, btnClose });
this.CancelButton = btnClose; this.CancelButton = btnClose;
+256 -65
View File
@@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.IO; using System.IO;
using System.Text.Json; using System.Text.Json;
@@ -8,7 +10,7 @@ using Outlook = Microsoft.Office.Interop.Outlook;
namespace OutlookCaseHelper namespace OutlookCaseHelper
{ {
public class OutlookHelper public class OutlookHelper : IDisposable
{ {
private Outlook.Application? outlookApp; private Outlook.Application? outlookApp;
private Outlook.NameSpace? outlookNamespace; private Outlook.NameSpace? outlookNamespace;
@@ -16,6 +18,13 @@ namespace OutlookCaseHelper
private readonly string rulesFilePath; private readonly string rulesFilePath;
private Outlook.Items? inboxItems; private Outlook.Items? inboxItems;
private Outlook.Items? sentItems; private Outlook.Items? sentItems;
private bool disposed = false;
// PERF FIX: Cache "Active" and "Closed" folder references to avoid
// traversing the full folder tree on every operation.
// Cache is validated before each use (COM object can go stale).
private Outlook.Folder? _cachedActiveFolder;
private Outlook.Folder? _cachedClosedFolder;
public class RuleInfo public class RuleInfo
{ {
@@ -28,10 +37,51 @@ namespace OutlookCaseHelper
{ {
rulesFilePath = Path.Combine( rulesFilePath = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
"OutlookCaseHelper", "OutlookCaseHelper", "active_rules.json");
"active_rules.json");
Directory.CreateDirectory(Path.GetDirectoryName(rulesFilePath)!); Directory.CreateDirectory(Path.GetDirectoryName(rulesFilePath)!);
LoadRules(); LoadRules();
// FIX #2: Scan inbox on startup so emails that arrived while the app
// was closed are moved into their correct folders immediately.
ScanInboxOnStartup();
}
// --- Dispose ---
public void Dispose()
{
if (disposed) return;
disposed = true;
try
{
if (inboxItems != null)
{
inboxItems.ItemAdd -= OnEmailReceived;
Marshal.ReleaseComObject(inboxItems);
inboxItems = null;
}
if (sentItems != null)
{
sentItems.ItemAdd -= OnEmailSent;
Marshal.ReleaseComObject(sentItems);
sentItems = null;
}
_cachedActiveFolder = null;
_cachedClosedFolder = null;
if (outlookNamespace != null)
{
Marshal.ReleaseComObject(outlookNamespace);
outlookNamespace = null;
}
if (outlookApp != null)
{
Marshal.ReleaseComObject(outlookApp);
outlookApp = null;
}
}
catch { }
GC.SuppressFinalize(this);
} }
// --- Outlook Connection --- // --- Outlook Connection ---
@@ -39,7 +89,6 @@ namespace OutlookCaseHelper
private bool EnsureOutlookConnected() private bool EnsureOutlookConnected()
{ {
if (outlookApp != null && outlookNamespace != null) return true; if (outlookApp != null && outlookNamespace != null) return true;
try try
{ {
if (System.Diagnostics.Process.GetProcessesByName("OUTLOOK").Length == 0) if (System.Diagnostics.Process.GetProcessesByName("OUTLOOK").Length == 0)
@@ -59,7 +108,7 @@ namespace OutlookCaseHelper
try try
{ {
var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
var sent = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderSentMail); var sent = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderSentMail);
if (inbox != null) if (inbox != null)
{ {
@@ -78,26 +127,25 @@ namespace OutlookCaseHelper
// --- Email Events --- // --- Email Events ---
private void OnEmailReceived(object item) => MoveEmailIfRuleExists(item); private void OnEmailReceived(object item) => MoveEmailIfRuleExists(item);
private void OnEmailSent(object item) => MoveEmailIfRuleExists(item); private void OnEmailSent(object item) => MoveEmailIfRuleExists(item);
private void MoveEmailIfRuleExists(object item) private void MoveEmailIfRuleExists(object item)
{ {
try try
{ {
if (outlookNamespace == null) return; if (outlookNamespace == null || activeRules.Count == 0) return;
if (activeRules.Count == 0) return; if (item is not Outlook.MailItem mail || mail.Subject == null) return;
if (item is not Outlook.MailItem mail) return;
if (mail.Subject == null) return; // PERF FIX: Resolve activeFolder once outside the loop
var activeFolder = GetCachedActiveFolder();
if (activeFolder == null) return;
foreach (var folderName in activeRules) foreach (var folderName in activeRules)
{ {
string trackingId = ExtractTrackingId(folderName); string trackingId = ExtractTrackingId(folderName);
if (!mail.Subject.Contains($"TrackingID#{trackingId}")) continue; if (!mail.Subject.Contains($"TrackingID#{trackingId}")) continue;
var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); var target = GetOrCreateFolder(activeFolder, folderName);
if (inbox == null) return;
var target = GetOrCreateFolder(GetOrCreateFolder(GetOrCreateFolder(inbox, "Cases"), "Active"), folderName);
mail.Move(target); mail.Move(target);
break; break;
} }
@@ -119,10 +167,21 @@ namespace OutlookCaseHelper
public void ReloadRules() public void ReloadRules()
{ {
activeRules.Clear(); activeRules.Clear();
// FIX: Invalidate folder cache when rules are reloaded
_cachedActiveFolder = null;
_cachedClosedFolder = null;
LoadRules(); LoadRules();
// FIX #7: Prune stale rules whose folders no longer exist in Outlook
PruneStaleRules();
} }
public void ProcessActiveRules() { } // FIX #1: ProcessActiveRules was empty — it now delegates to RunAllRules
// so the 60-second timer actually does something useful.
public void ProcessActiveRules()
{
if (activeRules.Count == 0) return;
RunAllRules();
}
private void LoadRules() private void LoadRules()
{ {
@@ -141,8 +200,36 @@ namespace OutlookCaseHelper
catch { } catch { }
} }
// FIX #7: Remove rules whose folders no longer exist in Outlook.
// Prevents infinite silent failures in RunAllRules and GetActiveRulesInfo
// when a folder was deleted manually from Outlook.
private void PruneStaleRules()
{
if (!EnsureOutlookConnected()) return;
try
{
var activeFolder = GetCachedActiveFolder();
if (activeFolder == null) return;
var stale = activeRules
.Where(r => GetFolder(activeFolder, r) == null)
.ToList();
if (stale.Count == 0) return;
foreach (var r in stale)
activeRules.Remove(r);
SaveRules();
}
catch { }
}
// --- Outlook Operations --- // --- Outlook Operations ---
// FIX #4: Regex was @"TrackingID#(\\d+)" in a verbatim string.
// In a verbatim string, \\d is a literal backslash + 'd', NOT the \d digit class.
// Fixed to @"TrackingID#(\d+)" so it correctly matches numeric IDs.
public string? GetSelectedEmailTrackingId() public string? GetSelectedEmailTrackingId()
{ {
if (!EnsureOutlookConnected()) return null; if (!EnsureOutlookConnected()) return null;
@@ -168,9 +255,7 @@ namespace OutlookCaseHelper
try try
{ {
if (!EnsureOutlookConnected()) return false; if (!EnsureOutlookConnected()) return false;
var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); var closed = GetCachedClosedFolder();
if (inbox == null) return false;
var closed = GetFolder(GetFolder(inbox, "Cases"), "Closed");
return closed != null && GetFolderStartingWith(closed, trackingId) != null; return closed != null && GetFolderStartingWith(closed, trackingId) != null;
} }
catch { return false; } catch { return false; }
@@ -181,23 +266,37 @@ namespace OutlookCaseHelper
if (!EnsureOutlookConnected()) return false; if (!EnsureOutlookConnected()) return false;
try try
{ {
var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); var activeFolder = GetCachedActiveFolder();
if (inbox == null) return false; if (activeFolder == null)
{
var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
if (inbox == null) return false;
var cases = GetOrCreateFolder(inbox, "Cases");
activeFolder = GetOrCreateFolder(cases, "Active");
_cachedActiveFolder = activeFolder;
}
var target = GetOrCreateFolder(GetOrCreateFolder(GetOrCreateFolder(inbox, "Cases"), "Active"), folderName); var target = GetOrCreateFolder(activeFolder, folderName);
// FIX: Correct DASL filter — original had \\\" producing \"
// (backslash+quote), but Outlook DASL needs plain double quotes.
string filter = $"@SQL=\"urn:schemas:httpmail:subject\" LIKE '%TrackingID#{trackingId}%'"; 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);
var sent = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderSentMail); if (inbox2 != null) MoveFilteredEmails(inbox2, filter, target);
if (sent != null) MoveFilteredEmails(sent, filter, target); if (sent != null) MoveFilteredEmails(sent, filter, target);
foreach (Outlook.Folder folder in inbox.Parent.Folders) if (inbox2 != null)
{ {
if (folder.Name != "Cases" && foreach (Outlook.Folder folder in inbox2.Parent.Folders)
folder.EntryID != inbox.EntryID && {
folder.EntryID != sent?.EntryID) if (folder.Name != "Cases" &&
try { MoveFilteredEmails(folder, filter, target); } catch { } folder.EntryID != inbox2.EntryID &&
folder.EntryID != sent?.EntryID)
try { MoveFilteredEmails(folder, filter, target); } catch { }
}
} }
activeRules.Add(folderName); activeRules.Add(folderName);
@@ -216,15 +315,23 @@ namespace OutlookCaseHelper
if (!EnsureOutlookConnected()) return false; if (!EnsureOutlookConnected()) return false;
try try
{ {
var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); var activeFolder = GetCachedActiveFolder();
if (inbox == null) return false; if (activeFolder == null) return false;
var cases = GetFolder(inbox, "Cases"); var folder = GetFolder(activeFolder, folderName);
var active = GetFolder(cases, "Active");
var folder = GetFolder(active, folderName);
if (folder == null) return false; if (folder == null) return false;
folder.MoveTo(GetOrCreateFolder(cases!, "Closed")); var closedFolder = GetCachedClosedFolder();
if (closedFolder == null)
{
var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
if (inbox == null) return false;
var cases = GetOrCreateFolder(inbox, "Cases");
closedFolder = GetOrCreateFolder(cases, "Closed");
_cachedClosedFolder = closedFolder;
}
folder.MoveTo(closedFolder);
activeRules.Remove(folderName); activeRules.Remove(folderName);
SaveRules(); SaveRules();
return true; return true;
@@ -237,9 +344,8 @@ namespace OutlookCaseHelper
if (!EnsureOutlookConnected()) return false; if (!EnsureOutlookConnected()) return false;
try try
{ {
var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); var activeFolder = GetCachedActiveFolder();
var active = GetFolder(GetFolder(inbox, "Cases"), "Active"); var folder = GetFolder(activeFolder, oldFolderName);
var folder = GetFolder(active, oldFolderName);
if (folder == null) return false; if (folder == null) return false;
folder.Name = newFolderName; folder.Name = newFolderName;
@@ -256,20 +362,25 @@ namespace OutlookCaseHelper
if (!EnsureOutlookConnected()) return false; if (!EnsureOutlookConnected()) return false;
try try
{ {
var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); var closedFolder = GetCachedClosedFolder();
if (inbox == null) return false; if (closedFolder == null) return false;
var cases = GetOrCreateFolder(inbox, "Cases"); var closedTracking = GetFolderStartingWith(closedFolder, trackingId);
var active = GetOrCreateFolder(cases, "Active");
var closed = GetFolder(cases, "Closed");
if (closed == null) return false;
var closedTracking = GetFolderStartingWith(closed, trackingId);
if (closedTracking == null) return false; if (closedTracking == null) return false;
string existingName = closedTracking.Name; string existingName = closedTracking.Name;
var existingActive = GetFolder(active, existingName);
var activeFolder = GetCachedActiveFolder();
if (activeFolder == null)
{
var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
if (inbox == null) return false;
var cases = GetOrCreateFolder(inbox, "Cases");
activeFolder = GetOrCreateFolder(cases, "Active");
_cachedActiveFolder = activeFolder;
}
var existingActive = GetFolder(activeFolder, existingName);
if (existingActive != null) if (existingActive != null)
{ {
var toMove = new List<Outlook.MailItem>(); var toMove = new List<Outlook.MailItem>();
@@ -280,12 +391,12 @@ namespace OutlookCaseHelper
} }
else else
{ {
closedTracking.MoveTo(active); closedTracking.MoveTo(activeFolder);
} }
try try
{ {
var target = GetFolder(active, existingName); var target = GetFolder(activeFolder, existingName);
if (triggerEmail != null && target != null) if (triggerEmail != null && target != null)
triggerEmail.Move(target); triggerEmail.Move(target);
} }
@@ -299,9 +410,8 @@ namespace OutlookCaseHelper
{ {
try try
{ {
var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); var active = GetCachedActiveFolder();
var active = GetFolder(GetFolder(inbox, "Cases"), "Active"); var moved = GetFolderStartingWith(active!, trackingId);
var moved = GetFolderStartingWith(active!, trackingId);
if (moved != null) { activeRules.Add(moved.Name); SaveRules(); return true; } if (moved != null) { activeRules.Add(moved.Name); SaveRules(); return true; }
} }
catch { } catch { }
@@ -317,16 +427,23 @@ namespace OutlookCaseHelper
try try
{ {
var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
var sent = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderSentMail); var sent = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderSentMail);
if (inbox == null) return; if (inbox == null) return;
var active = GetOrCreateFolder(GetOrCreateFolder(inbox, "Cases"), "Active"); var activeFolder = GetCachedActiveFolder();
if (activeFolder == null)
{
var cases = GetOrCreateFolder(inbox, "Cases");
activeFolder = GetOrCreateFolder(cases, "Active");
_cachedActiveFolder = activeFolder;
}
foreach (var folderName in activeRules) foreach (var folderName in activeRules)
{ {
string trackingId = ExtractTrackingId(folderName); string trackingId = ExtractTrackingId(folderName);
// FIX: Correct DASL filter syntax (plain double quotes)
string filter = $"@SQL=\"urn:schemas:httpmail:subject\" LIKE '%TrackingID#{trackingId}%'"; string filter = $"@SQL=\"urn:schemas:httpmail:subject\" LIKE '%TrackingID#{trackingId}%'";
var target = GetOrCreateFolder(active, folderName); var target = GetOrCreateFolder(activeFolder, folderName);
MoveFilteredEmails(inbox, filter, target); MoveFilteredEmails(inbox, filter, target);
if (sent != null) MoveFilteredEmails(sent, filter, target); if (sent != null) MoveFilteredEmails(sent, filter, target);
@@ -343,18 +460,25 @@ namespace OutlookCaseHelper
var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox);
if (inbox == null) return; if (inbox == null) return;
var cases = GetOrCreateFolder(inbox, "Cases"); var activeFolder = GetCachedActiveFolder();
var active = GetOrCreateFolder(cases, "Active"); if (activeFolder == null)
{
var cases = GetOrCreateFolder(inbox, "Cases");
activeFolder = GetOrCreateFolder(cases, "Active");
_cachedActiveFolder = activeFolder;
}
// PERF FIX: Build the full folder list once, outside the per-rule loop
var allFolders = new List<Outlook.Folder>(); var allFolders = new List<Outlook.Folder>();
if (inbox.Parent is Outlook.Folder root) if (inbox.Parent is Outlook.Folder root)
GetAllFolders(root, allFolders, cases.EntryID); GetAllFolders(root, allFolders, activeFolder.EntryID);
foreach (var folderName in activeRules) foreach (var folderName in activeRules)
{ {
string trackingId = ExtractTrackingId(folderName); string trackingId = ExtractTrackingId(folderName);
// FIX: Correct DASL filter syntax (plain double quotes)
string filter = $"@SQL=\"urn:schemas:httpmail:subject\" LIKE '%TrackingID#{trackingId}%'"; 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) foreach (var folder in allFolders)
try { MoveFilteredEmails(folder, filter, target); } catch { } try { MoveFilteredEmails(folder, filter, target); } catch { }
@@ -363,19 +487,20 @@ namespace OutlookCaseHelper
catch { } catch { }
} }
// PERF FIX: Resolve activeFolder once before the loop instead of inside it.
// Original called EnsureOutlookConnected + FindFolderByNameAnywhere per iteration.
public List<RuleInfo> GetActiveRulesInfo() public List<RuleInfo> GetActiveRulesInfo()
{ {
var result = new List<RuleInfo>(); var result = new List<RuleInfo>();
if (!EnsureOutlookConnected()) return result; if (!EnsureOutlookConnected()) return result;
try try
{ {
var inbox = GetDefaultFolder(Outlook.OlDefaultFolders.olFolderInbox); var activeFolder = GetCachedActiveFolder();
var active = GetFolder(GetFolder(inbox, "Cases"), "Active"); if (activeFolder == null) return result;
if (active == null) return result;
foreach (var folderName in activeRules) foreach (var folderName in activeRules)
{ {
var folder = GetFolder(active, folderName); var folder = GetFolder(activeFolder, folderName);
result.Add(new RuleInfo result.Add(new RuleInfo
{ {
FolderName = folderName, FolderName = folderName,
@@ -388,24 +513,90 @@ namespace OutlookCaseHelper
return result; return result;
} }
// --- Folder Cache Helpers ---
// PERF FIX: Caches the "Active" folder reference. Validates the COM object
// before returning it; falls back to a fresh lookup if it has gone stale.
private Outlook.Folder? GetCachedActiveFolder()
{
if (_cachedActiveFolder != null)
{
try { var _ = _cachedActiveFolder.Name; return _cachedActiveFolder; }
catch { _cachedActiveFolder = null; }
}
_cachedActiveFolder = FindFolderByNameAnywhere("Active");
return _cachedActiveFolder;
}
// PERF FIX: Caches the "Closed" folder reference with same staleness guard.
private Outlook.Folder? GetCachedClosedFolder()
{
if (_cachedClosedFolder != null)
{
try { var _ = _cachedClosedFolder.Name; return _cachedClosedFolder; }
catch { _cachedClosedFolder = null; }
}
_cachedClosedFolder = FindFolderByNameAnywhere("Closed");
return _cachedClosedFolder;
}
// --- 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 --- // --- Helpers ---
// FIX #4: Regex was @"^(\\d+)" in a verbatim string = literal "\\d".
// Fixed to @"^(\d+)" so it actually matches leading digits in folder names.
private string ExtractTrackingId(string folderName) private string ExtractTrackingId(string folderName)
{ {
var match = Regex.Match(folderName, @"^(\d+)"); var match = Regex.Match(folderName, @"^(\d+)");
return match.Success ? match.Groups[1].Value : folderName; return match.Success ? match.Groups[1].Value : folderName;
} }
// PERF FIX: Release the COM object returned by Restrict() after use.
// Failing to do so causes COM reference leaks when many rules are active.
private void MoveFilteredEmails(Outlook.Folder source, string filter, Outlook.Folder dest) private void MoveFilteredEmails(Outlook.Folder source, string filter, Outlook.Folder dest)
{ {
Outlook.Items? restricted = null;
try try
{ {
restricted = source.Items.Restrict(filter);
var toMove = new List<Outlook.MailItem>(); var toMove = new List<Outlook.MailItem>();
foreach (object item in source.Items.Restrict(filter)) foreach (object item in restricted)
if (item is Outlook.MailItem mail) toMove.Add(mail); if (item is Outlook.MailItem mail) toMove.Add(mail);
foreach (var mail in toMove) mail.Move(dest); foreach (var mail in toMove) mail.Move(dest);
} }
catch { } catch { }
finally
{
if (restricted != null)
Marshal.ReleaseComObject(restricted);
}
} }
private void GetAllFolders(Outlook.Folder parent, List<Outlook.Folder> result, string excludeEntryId) private void GetAllFolders(Outlook.Folder parent, List<Outlook.Folder> result, string excludeEntryId)
@@ -449,4 +640,4 @@ namespace OutlookCaseHelper
return null; return null;
} }
} }
} }