Compare commits

...

2 Commits

Author SHA1 Message Date
PointStar 9b451c9cbe feat(save-attachments): nested-folder tree picker + top-most message boxes
1.2.15

Folder picker: replace the flat subfolder dropdown with a lazy-loading
TreeView so the user can drill into nested subfolders to any depth.
New FolderNodeViewModel lists each folder's children on first expand;
the case-root node is selected/expanded by default and seeded with the
default subfolders (kept even if the share listing fails). ChosenSubfolderName
now returns a multi-segment relative path, appended to the case folder
as before.

Message boxes: route all of AddInHost's MessageBox calls through a new
ShowMessage helper that anchors a transient top-most owner window to the
foreground Outlook window. A bare MessageBox has no owner once our dialog
closes, so it sank behind Outlook and the user never saw the success/error
result.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 13:35:43 +03:00
PointStar 35ce8facff fix(attachments): bind upload to Note.attachments to satisfy EspoCRM
The EspoCRM tenant now rejects role=Attachment uploads that carry
neither `field` nor `parentType` (400 "No `field` and `parentType`."),
which broke the entire save-attachments flow on v1.2.13 — every file,
including PDFs, failed at Step 1.

Bind the upload to the documented Attachment-Multiple example
parentType=Note, field=attachments. Note.attachments is a built-in
field with no allowed-file-types restriction, so all types pass
(including images) — unlike Document.file, which rejected images with
403. No parentId is set; the blob stays standalone and is still just
pushed to the network share.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-09 12:28:22 +03:00
7 changed files with 322 additions and 84 deletions
+17 -10
View File
@@ -214,21 +214,28 @@ namespace MarcusLaw.OutlookAddin.Core.Services
// "contents" alias was an undocumented legacy that strict tenants
// reject with 400.
//
// We deliberately DO NOT bind the upload to Document.file
// (relatedType=Document / field=file). EspoCRM only enforces the
// per-field "allowed file types" list when a field is supplied, and
// Marcus-Law's Document.file field is configured to accept document
// types only (PDF/Word) — images (JPEG/PNG) were rejected with
// 403 "Not allowed file type". This flow never creates a Document;
// the attachment is a standalone blob we immediately push to the
// network share, so an unbound role=Attachment upload is the
// correct shape and lets every file type through. (Server-side fix:
// widen Document.file's accepted types in the Entity Manager.)
// We bind the upload to Note.attachments (parentType=Note,
// field=attachments). EspoCRM's attachment access checker now
// REJECTS a role=Attachment upload that carries neither `field`
// nor `parentType` ("No `field` and `parentType`." → 400) — the
// earlier "unbound blob" shape no longer passes. Note.attachments
// is the documented Attachment-Multiple example and a built-in
// field that carries NO "allowed file types" restriction, so every
// file type (PDF, JPEG, PNG, …) passes — unlike Document.file,
// which Marcus-Law limits to PDF/Word and which rejected images
// with 403 "Not allowed file type".
//
// We never set parentId / create a Note: the attachment stays a
// standalone blob whose id we immediately push to the network
// share. The (parentType, field) pair only satisfies the access
// checker and selects an unrestricted file-type policy.
var payload = new Dictionary<string, object?>
{
["name"] = fileName,
["type"] = mime,
["role"] = "Attachment",
["parentType"] = "Note",
["field"] = "attachments",
["file"] = dataUri
};
@@ -3,8 +3,8 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:MarcusLaw.OutlookAddin.UI.Converters"
Title="שמור קבצים מצורפים ל-Klear"
Height="560" Width="620"
MinHeight="420" MinWidth="500"
Height="640" Width="620"
MinHeight="480" MinWidth="500"
FlowDirection="RightToLeft"
WindowStartupLocation="CenterOwner"
ShowInTaskbar="False"
@@ -144,24 +144,38 @@
<!-- Folder -->
<DockPanel Grid.Row="2" Margin="0,10,0,0" LastChildFill="True">
<TextBlock DockPanel.Dock="Left" Text="תיקייה:" VerticalAlignment="Center"
Margin="0,0,8,0" Foreground="{StaticResource Muted}" FontSize="12" />
<TextBlock DockPanel.Dock="Right" Text="טוען…" VerticalAlignment="Center"
Margin="8,0,0,0" Foreground="{StaticResource Muted}" FontSize="12"
Visibility="{Binding IsLoadingSubfolders, Converter={StaticResource BoolToVis}}" />
<ComboBox ItemsSource="{Binding Subfolders}"
SelectedItem="{Binding SelectedSubfolder}"
Padding="6,4">
<ComboBox.Style>
<Style TargetType="ComboBox">
<Style.Triggers>
<DataTrigger Binding="{Binding IsLoadingSubfolders}" Value="True">
<Setter Property="IsEnabled" Value="False" />
</DataTrigger>
</Style.Triggers>
</Style>
</ComboBox.Style>
</ComboBox>
<DockPanel DockPanel.Dock="Top" LastChildFill="True" Margin="0,0,0,4">
<TextBlock DockPanel.Dock="Left" Text="תיקיית יעד:" VerticalAlignment="Center"
Margin="0,0,8,0" Foreground="{StaticResource Muted}" FontSize="12" />
<TextBlock DockPanel.Dock="Right" Text="טוען…" VerticalAlignment="Center"
Margin="8,0,0,0" Foreground="{StaticResource Muted}" FontSize="12"
Visibility="{Binding IsLoadingSubfolders, Converter={StaticResource BoolToVis}}" />
<TextBlock Text="בחר/י תיקייה או תת-תיקייה לשמירה (ניתן להרחיב רמות)"
VerticalAlignment="Center" Foreground="{StaticResource Muted}" FontSize="11"
TextTrimming="CharacterEllipsis" />
</DockPanel>
<Border Style="{StaticResource Card}" MaxHeight="150">
<TreeView x:Name="FolderTreeView"
ItemsSource="{Binding FolderTree}"
BorderThickness="0"
Background="Transparent"
SelectedItemChanged="FolderTreeView_SelectedItemChanged">
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}" />
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}" />
</Style>
</TreeView.ItemContainerStyle>
<TreeView.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Margin="0,1">
<TextBlock Text="📁" Margin="0,0,4,0" FontSize="13" VerticalAlignment="Center" />
<TextBlock Text="{Binding Name}" VerticalAlignment="Center" FontSize="13" />
</StackPanel>
</HierarchicalDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
</Border>
</DockPanel>
<!-- Attachments header -->
@@ -32,5 +32,15 @@ namespace MarcusLaw.OutlookAddin.UI.Dialogs
DialogResult = _viewModel?.DialogResult;
Close();
}
// WPF's TreeView.SelectedItem is read-only, so we mirror the selection
// into the view-model here (the chosen node drives the save target).
private void FolderTreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e)
{
if (_viewModel != null)
{
_viewModel.SelectedFolder = e.NewValue as FolderNodeViewModel;
}
}
}
}
@@ -0,0 +1,150 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using MarcusLaw.OutlookAddin.Core.Services;
using Serilog;
namespace MarcusLaw.OutlookAddin.UI.ViewModels
{
/// <summary>
/// One node in the network-storage folder tree shown by the
/// save-attachments dialog. Children are listed lazily the first time the
/// node is expanded, so the user can drill into nested subfolders to any
/// depth without us listing the whole tree up front.
/// </summary>
public sealed partial class FolderNodeViewModel : ObservableObject
{
private readonly IEspoCrmClient? _client;
private readonly ILogger? _logger;
private bool _childrenLoaded;
private bool _loadingStarted;
/// <summary>Label shown in the tree.</summary>
public string Name { get; }
/// <summary>
/// Path relative to the share root (e.g. "56-ליאור זייני/מסמכים").
/// Used to list this folder's children. Empty for placeholders.
/// </summary>
public string FullPath { get; }
/// <summary>
/// Path relative to the case-root folder (e.g. "מסמכים/רפואי"), or
/// null for the case-root node itself. This is what the save target
/// appends to the resolved case folder.
/// </summary>
public string? RelativePath { get; }
/// <summary>True for the transient "loading…" child placeholder.</summary>
public bool IsPlaceholder { get; }
public bool IsRoot => !IsPlaceholder && RelativePath == null;
public ObservableCollection<FolderNodeViewModel> Children { get; } =
new ObservableCollection<FolderNodeViewModel>();
[ObservableProperty]
private bool isExpanded;
[ObservableProperty]
private bool isSelected;
[ObservableProperty]
private bool isLoading;
/// <summary>Real folder node.</summary>
public FolderNodeViewModel(
IEspoCrmClient client,
ILogger logger,
string name,
string fullPath,
string? relativePath)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
Name = name;
FullPath = fullPath;
RelativePath = relativePath;
// A placeholder child makes the expand chevron appear before we've
// listed; expanding swaps it for the real children (or, if there
// are none, the node becomes a leaf).
Children.Add(new FolderNodeViewModel());
}
/// <summary>Placeholder ("loading…") node constructor.</summary>
private FolderNodeViewModel()
{
Name = "טוען…";
FullPath = string.Empty;
RelativePath = string.Empty;
IsPlaceholder = true;
_childrenLoaded = true;
}
partial void OnIsExpandedChanged(bool value)
{
if (value && !_childrenLoaded && !_loadingStarted)
{
_loadingStarted = true;
_ = LoadChildrenAsync();
}
}
/// <summary>
/// Lists this folder's subfolders and replaces the placeholder with
/// real child nodes. <paramref name="seedNames"/> (used for the root)
/// guarantees the default case subfolders show even if the share
/// listing lags or omits a not-yet-created folder.
/// </summary>
public async Task LoadChildrenAsync(IEnumerable<string>? seedNames = null)
{
if (_client == null) return;
IsLoading = true;
// Build into a local list and swap once at the end. The seeds are
// added first so that even if the share listing fails the default
// subfolders still show; on failure for a non-seeded node the list
// is empty and the node reads as a leaf rather than "loading…".
var seen = new HashSet<string>(StringComparer.Ordinal);
var loaded = new List<FolderNodeViewModel>();
if (seedNames != null)
{
foreach (var n in seedNames)
{
if (seen.Add(n)) loaded.Add(MakeChild(n));
}
}
try
{
var items = await _client.ListNetworkStorageFolderAsync(FullPath).ConfigureAwait(true);
foreach (var item in items)
{
if (!item.IsFolder) continue;
if (!seen.Add(item.Name)) continue;
loaded.Add(MakeChild(item.Name));
}
}
catch (Exception ex)
{
_logger?.Warning(ex, "Listing subfolders under '{Path}' failed", FullPath);
}
finally
{
Children.Clear();
foreach (var child in loaded) Children.Add(child);
_childrenLoaded = true;
IsLoading = false;
}
}
private FolderNodeViewModel MakeChild(string name)
{
var fullPath = string.IsNullOrEmpty(FullPath) ? name : FullPath + "/" + name;
var relativePath = IsRoot ? name : RelativePath + "/" + name;
return new FolderNodeViewModel(_client!, _logger!, name, fullPath, relativePath);
}
}
}
@@ -40,7 +40,7 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
private CaseEntity? selectedCase;
[ObservableProperty]
private string? selectedSubfolder;
private FolderNodeViewModel? selectedFolder;
[ObservableProperty]
private string? statusMessage;
@@ -58,7 +58,12 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
public ObservableCollection<CaseEntity> Cases { get; } = new ObservableCollection<CaseEntity>();
public ObservableCollection<string> Subfolders { get; } = new ObservableCollection<string>();
/// <summary>
/// Folder tree for the selected case. Holds a single root node (the
/// case folder); its children are the subfolders, loaded lazily so the
/// user can drill into any nesting depth.
/// </summary>
public ObservableCollection<FolderNodeViewModel> FolderTree { get; } = new ObservableCollection<FolderNodeViewModel>();
public ObservableCollection<AttachmentRowViewModel> Attachments { get; } = new ObservableCollection<AttachmentRowViewModel>();
@@ -78,14 +83,20 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
}
/// <summary>
/// Subfolder string the user picked, or null to save to the case
/// folder root.
/// Folder the user picked, as a path relative to the case folder
/// (e.g. "מסמכים/רפואי"), or null to save to the case folder root.
/// Multi-segment paths are honoured by the save target, which appends
/// this to <see cref="ResolvedCaseFolderPath"/>.
/// </summary>
public string? ChosenSubfolderName =>
string.IsNullOrEmpty(SelectedSubfolder) ||
string.Equals(SelectedSubfolder, RootSentinel, StringComparison.Ordinal)
? null
: SelectedSubfolder;
public string? ChosenSubfolderName
{
get
{
var f = SelectedFolder;
if (f == null || f.IsPlaceholder || f.IsRoot) return null;
return string.IsNullOrEmpty(f.RelativePath) ? null : f.RelativePath;
}
}
public CaseEntity? PickedCase => SelectedCase;
@@ -99,7 +110,7 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
_client = client ?? throw new ArgumentNullException(nameof(client));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
ResetSubfolders();
ResetFolderTree();
foreach (var att in attachments)
{
@@ -139,12 +150,10 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
}
}
private void ResetSubfolders()
private void ResetFolderTree()
{
Subfolders.Clear();
Subfolders.Add(RootSentinel);
foreach (var f in DefaultCaseSubfolders) Subfolders.Add(f);
SelectedSubfolder = Subfolders[0];
FolderTree.Clear();
SelectedFolder = null;
}
partial void OnSearchTextChanged(string value)
@@ -169,7 +178,7 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
/// </summary>
public async Task RefreshSubfoldersForSelectedCaseAsync()
{
ResetSubfolders();
ResetFolderTree();
ResolvedCaseFolderPath = null;
var c = SelectedCase;
@@ -220,21 +229,25 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
c.Id, c.Number ?? "(none)", primaryContactName ?? "(none)", c.NetworkStorageFolderPath ?? "(none)", folderPath);
if (string.IsNullOrEmpty(folderPath)) return;
// Build the tree rooted at the case folder. The root node is
// selected by default (→ save to case root, matching the previous
// default), expanded, and seeded with the default subfolders so
// they appear even before the share listing returns. Deeper levels
// load lazily as the user expands them.
var root = new FolderNodeViewModel(_client, _logger, RootSentinel, folderPath, relativePath: null);
FolderTree.Add(root);
SelectedFolder = root;
root.IsSelected = true;
try
{
var items = await _client.ListNetworkStorageFolderAsync(folderPath).ConfigureAwait(true);
foreach (var item in items)
{
if (!item.IsFolder) continue;
if (Subfolders.Contains(item.Name)) continue;
if (string.Equals(item.Name, RootSentinel, StringComparison.Ordinal)) continue;
Subfolders.Add(item.Name);
}
await root.LoadChildrenAsync(DefaultCaseSubfolders).ConfigureAwait(true);
}
catch (Exception ex)
{
_logger.Warning(ex, "Listing subfolders for case {CaseId} failed", c.Id);
}
root.IsExpanded = true;
}
/// <summary>
+2 -2
View File
@@ -33,6 +33,6 @@ using System.Security;
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("1.2.13.0")]
[assembly: AssemblyFileVersion("1.2.13.0")]
[assembly: AssemblyVersion("1.2.15.0")]
[assembly: AssemblyFileVersion("1.2.15.0")]
+72 -28
View File
@@ -127,6 +127,62 @@ namespace OutlookAddin.Services
}
}
/// <summary>
/// Shows a message box that is guaranteed to render above Outlook and
/// any other window. A bare <see cref="MessageBox.Show(string)"/> has
/// no owner — once our WPF dialog closes there is no active window on
/// the add-in thread, so the box sinks behind Outlook and the user
/// never sees the success/error result. We anchor it to a transient,
/// invisible top-most owner window parented to the foreground Outlook
/// window so it always surfaces on top. Caption is always "Klear".
/// </summary>
private MessageBoxResult ShowMessage(
string text,
MessageBoxButton button = MessageBoxButton.OK,
MessageBoxImage icon = MessageBoxImage.None)
{
Window? owner = null;
try
{
// A 1×1 borderless window at screen centre: imperceptible, but
// gives the MessageBox a top-most owner to z-order above. The
// box centres on this point → effectively centred on screen.
owner = new Window
{
Width = 1,
Height = 1,
WindowStyle = WindowStyle.None,
ResizeMode = ResizeMode.NoResize,
ShowInTaskbar = false,
Topmost = true,
WindowStartupLocation = WindowStartupLocation.CenterScreen,
};
var hwnd = GetForegroundWindow();
if (hwnd == IntPtr.Zero)
{
hwnd = System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle;
}
if (hwnd != IntPtr.Zero)
{
new WindowInteropHelper(owner).Owner = hwnd;
}
owner.Show();
owner.Activate();
return MessageBox.Show(owner, text, "Klear", button, icon);
}
catch (Exception ex)
{
Logger.Warning(ex, "ShowMessage top-most owner failed; falling back to ownerless box");
return MessageBox.Show(text, "Klear", button, icon);
}
finally
{
try { owner?.Close(); } catch { /* ignore */ }
}
}
private static void EnableModernTls()
{
try
@@ -402,9 +458,8 @@ namespace OutlookAddin.Services
if (mail == null) return;
if (!IsConfigured)
{
MessageBox.Show(
ShowMessage(
"התוסף עדיין לא מוגדר. פתח את ההגדרות תחילה.",
"Klear",
MessageBoxButton.OK,
MessageBoxImage.Information);
return;
@@ -423,9 +478,8 @@ namespace OutlookAddin.Services
catch (Exception ex)
{
Logger.Error(ex, "LaunchComposeFromCaseAsync failed");
MessageBox.Show(
ShowMessage(
"טעינת התיק נכשלה: " + ex.Message,
"Klear",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
@@ -444,9 +498,8 @@ namespace OutlookAddin.Services
if (mail == null) return;
if (!IsConfigured)
{
var result = MessageBox.Show(
var result = ShowMessage(
"התוסף עדיין לא מוגדר. לפתוח את חלון ההגדרות עכשיו?",
"Klear",
MessageBoxButton.YesNo,
MessageBoxImage.Information);
if (result == MessageBoxResult.Yes) LaunchSettings();
@@ -463,9 +516,8 @@ namespace OutlookAddin.Services
catch (Exception ex)
{
Logger.Error(ex, "LaunchSaveAttachmentsAsync: extract failed");
MessageBox.Show(
ShowMessage(
"קריאת הקבצים המצורפים נכשלה: " + ex.Message,
"Klear",
MessageBoxButton.OK,
MessageBoxImage.Error);
return;
@@ -473,9 +525,8 @@ namespace OutlookAddin.Services
if (envelope.Attachments.Count == 0)
{
MessageBox.Show(
ShowMessage(
"אין במייל הזה קבצים מצורפים לשמירה.",
"Klear",
MessageBoxButton.OK,
MessageBoxImage.Information);
return;
@@ -510,9 +561,8 @@ namespace OutlookAddin.Services
: SaveAttachmentsViewModel.ResolveCaseFolderPath(picked);
if (string.IsNullOrEmpty(caseFolder))
{
MessageBox.Show(
ShowMessage(
"לא ניתן לחשב את נתיב התיקייה של התיק. ייתכן שהתיק עוד לא הוגדר ב-Klear.",
"Klear",
MessageBoxButton.OK,
MessageBoxImage.Warning);
return;
@@ -540,9 +590,8 @@ namespace OutlookAddin.Services
catch (Exception ex)
{
Logger.Error(ex, "LaunchSaveAttachmentsAsync failed");
MessageBox.Show(
ShowMessage(
"שמירת הקבצים נכשלה: " + ex.Message,
"Klear",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
@@ -572,7 +621,7 @@ namespace OutlookAddin.Services
}
}
private static void ShowAttachmentSummary(AttachmentSaveSummary summary, string caseLabel, string targetPath)
private void ShowAttachmentSummary(AttachmentSaveSummary summary, string caseLabel, string targetPath)
{
string message;
MessageBoxImage icon;
@@ -589,7 +638,7 @@ namespace OutlookAddin.Services
message = parts.Count == 0 ? "לא נשמרו פריטים." : string.Join(" · ", parts);
icon = summary.FailedCount > 0 ? MessageBoxImage.Warning : MessageBoxImage.Information;
}
MessageBox.Show(message, "Klear", MessageBoxButton.OK, icon);
ShowMessage(message, MessageBoxButton.OK, icon);
}
/// <summary>
@@ -601,9 +650,8 @@ namespace OutlookAddin.Services
{
if (!IsConfigured)
{
MessageBox.Show(
ShowMessage(
"התוסף עדיין לא מוגדר. פתח את ההגדרות תחילה.",
"Klear",
MessageBoxButton.OK,
MessageBoxImage.Information);
return;
@@ -620,9 +668,8 @@ namespace OutlookAddin.Services
{
Logger.Error(ex, "LaunchComposeFromCaseByIdAsync failed for caseId={CaseId}", caseId);
if (mail != null) try { Marshal.ReleaseComObject(mail); } catch { }
MessageBox.Show(
ShowMessage(
"פתיחת המייל מתיק נכשלה: " + ex.Message,
"Klear",
MessageBoxButton.OK,
MessageBoxImage.Error);
}
@@ -685,9 +732,8 @@ namespace OutlookAddin.Services
{
if (!IsConfigured)
{
var result = MessageBox.Show(
var result = ShowMessage(
"התוסף עדיין לא מוגדר. לפתוח את חלון ההגדרות עכשיו?",
"Klear",
MessageBoxButton.YesNo,
MessageBoxImage.Information);
if (result == MessageBoxResult.Yes)
@@ -700,9 +746,8 @@ namespace OutlookAddin.Services
var mailItems = ReadCurrentSelection();
if (mailItems.Count == 0)
{
MessageBox.Show(
ShowMessage(
"לא נבחר מייל לתיוק. סמן מייל אחד או יותר ונסה שוב.",
"Klear",
MessageBoxButton.OK,
MessageBoxImage.Information);
return;
@@ -743,9 +788,8 @@ namespace OutlookAddin.Services
Logger.Warning(
"Filing skipped — selected EspoEntityRef has no usable Id/EntityType (id={Id}, name={Name}, type={Type})",
vm.SelectedTarget?.Id, vm.SelectedTarget?.Name, vm.SelectedTarget?.EntityType);
MessageBox.Show(
ShowMessage(
"התיק שנבחר לא הוחזר עם סוג ישות (entityType) תקין. ייתכן ש-Klear שלך מחזיר שדה אחר; הלוג מכיל את הפרטים. שלח לאדמין.",
"Klear",
MessageBoxButton.OK,
MessageBoxImage.Warning);
return;
@@ -917,7 +961,7 @@ namespace OutlookAddin.Services
}
}
private static void ShowSummary(FilingBatchSummary summary, FilingTarget target)
private void ShowSummary(FilingBatchSummary summary, FilingTarget target)
{
string message;
MessageBoxImage icon;
@@ -938,7 +982,7 @@ namespace OutlookAddin.Services
icon = summary.FailedCount > 0 ? MessageBoxImage.Warning : MessageBoxImage.Information;
}
MessageBox.Show(message, "Klear", MessageBoxButton.OK, icon);
ShowMessage(message, MessageBoxButton.OK, icon);
}
private static string? TryGetSenderName(Outlook.MailItem item)