Merge branch 'codex/mail-categories-v1'
This commit is contained in:
@@ -41,6 +41,7 @@ using Wino.Core.Integration.Processors;
|
||||
using Wino.Core.Misc;
|
||||
using Wino.Core.Requests.Bundles;
|
||||
using Wino.Core.Requests.Calendar;
|
||||
using Wino.Core.Requests.Category;
|
||||
using Wino.Core.Requests.Folder;
|
||||
using Wino.Core.Requests.Mail;
|
||||
using Wino.Messaging.UI;
|
||||
@@ -107,6 +108,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
"ParentFolderId",
|
||||
"InternetMessageId",
|
||||
"InternetMessageHeaders",
|
||||
"Categories",
|
||||
];
|
||||
|
||||
private readonly SemaphoreSlim _handleItemRetrievalSemaphore = new(1);
|
||||
@@ -116,6 +118,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
private readonly IOutlookChangeProcessor _outlookChangeProcessor;
|
||||
private readonly GraphServiceClient _graphClient;
|
||||
private readonly IOutlookSynchronizerErrorHandlerFactory _errorHandlingFactory;
|
||||
private readonly IMailCategoryService _mailCategoryService;
|
||||
private bool _isFolderStructureChanged;
|
||||
|
||||
private readonly SemaphoreSlim _concurrentDownloadSemaphore = new(10); // Limit to 10 concurrent downloads
|
||||
@@ -123,7 +126,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
public OutlookSynchronizer(MailAccount account,
|
||||
IAuthenticator authenticator,
|
||||
IOutlookChangeProcessor outlookChangeProcessor,
|
||||
IOutlookSynchronizerErrorHandlerFactory errorHandlingFactory) : base(account, WeakReferenceMessenger.Default)
|
||||
IOutlookSynchronizerErrorHandlerFactory errorHandlingFactory,
|
||||
IMailCategoryService mailCategoryService) : base(account, WeakReferenceMessenger.Default)
|
||||
{
|
||||
var tokenProvider = new MicrosoftTokenProvider(Account, authenticator);
|
||||
|
||||
@@ -138,6 +142,7 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
|
||||
_outlookChangeProcessor = outlookChangeProcessor;
|
||||
_errorHandlingFactory = errorHandlingFactory;
|
||||
_mailCategoryService = mailCategoryService;
|
||||
}
|
||||
|
||||
#region MS Graph Handlers
|
||||
@@ -1152,6 +1157,11 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
_logger.Debug("Updating flag status for mail {MessageId}: IsFlagged={IsFlagged}", item.Id, isFlagged);
|
||||
await _outlookChangeProcessor.ChangeFlagStatusAsync(item.Id, isFlagged).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (item.Categories != null)
|
||||
{
|
||||
await ReplaceMailAssignmentsAsync(item.Id, item.Categories).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1208,6 +1218,43 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task SynchronizeCategoriesAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var response = await _graphClient.Me.Outlook.MasterCategories
|
||||
.GetAsync(cancellationToken: cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
var categories = response?.Value?
|
||||
.Where(a => !string.IsNullOrWhiteSpace(a?.DisplayName))
|
||||
.Select(a =>
|
||||
{
|
||||
var colorOption = GetMailCategoryColorOption(a.Color);
|
||||
|
||||
return new MailCategory
|
||||
{
|
||||
MailAccountId = Account.Id,
|
||||
RemoteId = a.Id,
|
||||
Name = a.DisplayName,
|
||||
BackgroundColorHex = colorOption.BackgroundColorHex,
|
||||
TextColorHex = colorOption.TextColorHex,
|
||||
Source = MailCategorySource.Outlook
|
||||
};
|
||||
})
|
||||
.ToList() ?? [];
|
||||
|
||||
await _mailCategoryService.ReplaceCategoriesAsync(Account.Id, categories).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task ReplaceMailAssignmentsAsync(string messageId, IEnumerable<string> categoryNames)
|
||||
{
|
||||
var localMailCopies = await _outlookChangeProcessor.GetMailCopiesAsync([messageId]).ConfigureAwait(false);
|
||||
|
||||
foreach (var localMailCopy in localMailCopies)
|
||||
{
|
||||
await _mailCategoryService.ReplaceMailAssignmentsAsync(Account.Id, localMailCopy.UniqueId, categoryNames ?? []).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<OutlookSpecialFolderIdInformation> GetSpecialFolderIdsAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var localFolders = await _outlookChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
|
||||
@@ -1767,6 +1814,87 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
return Move(batchMoveRequest);
|
||||
}
|
||||
|
||||
public override List<IRequestBundle<RequestInformation>> UpdateCategories(BatchMailCategoryAssignmentRequest request)
|
||||
=> ForEachRequest(request, item => CreateMessageCategoryPatchRequest(item.Item.Id, item.CategoryNames));
|
||||
|
||||
public override List<IRequestBundle<RequestInformation>> CreateCategory(MailCategoryCreateRequest request)
|
||||
{
|
||||
var outlookCategory = new OutlookCategory
|
||||
{
|
||||
DisplayName = request.Category.Name,
|
||||
Color = GetOutlookCategoryColor(request.Category)
|
||||
};
|
||||
|
||||
var requestInfo = _graphClient.Me.Outlook.MasterCategories.ToPostRequestInformation(outlookCategory);
|
||||
return [new HttpRequestBundle<RequestInformation>(requestInfo, request)];
|
||||
}
|
||||
|
||||
public override List<IRequestBundle<RequestInformation>> UpdateCategory(MailCategoryUpdateRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.PreviousRemoteId))
|
||||
return CreateCategory(new MailCategoryCreateRequest(request.Category));
|
||||
|
||||
var hasNameChanged = !string.Equals(request.PreviousName, request.Category.Name, StringComparison.Ordinal);
|
||||
if (!hasNameChanged)
|
||||
{
|
||||
var requestInfo = _graphClient.Me.Outlook.MasterCategories[request.PreviousRemoteId].ToPatchRequestInformation(new OutlookCategory
|
||||
{
|
||||
Color = GetOutlookCategoryColor(request.Category)
|
||||
});
|
||||
|
||||
return [new HttpRequestBundle<RequestInformation>(requestInfo, request)];
|
||||
}
|
||||
|
||||
var bundles = new List<IRequestBundle<RequestInformation>>();
|
||||
var createRequestInfo = _graphClient.Me.Outlook.MasterCategories.ToPostRequestInformation(new OutlookCategory
|
||||
{
|
||||
DisplayName = request.Category.Name,
|
||||
Color = GetOutlookCategoryColor(request.Category)
|
||||
});
|
||||
|
||||
bundles.Add(new HttpRequestBundle<RequestInformation>(createRequestInfo, request));
|
||||
|
||||
foreach (var target in request.AffectedMessages ?? [])
|
||||
{
|
||||
bundles.Add(new HttpRequestBundle<RequestInformation>(
|
||||
CreateMessageCategoryPatchRequest(target.MessageId, target.CategoryNames),
|
||||
request));
|
||||
}
|
||||
|
||||
bundles.Add(new HttpRequestBundle<RequestInformation>(
|
||||
_graphClient.Me.Outlook.MasterCategories[request.PreviousRemoteId].ToDeleteRequestInformation(),
|
||||
request));
|
||||
|
||||
return bundles;
|
||||
}
|
||||
|
||||
public override List<IRequestBundle<RequestInformation>> DeleteCategory(MailCategoryDeleteRequest request)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(request.PreviousRemoteId))
|
||||
return [];
|
||||
|
||||
var bundles = new List<IRequestBundle<RequestInformation>>();
|
||||
|
||||
foreach (var target in request.AffectedMessages ?? [])
|
||||
{
|
||||
bundles.Add(new HttpRequestBundle<RequestInformation>(
|
||||
CreateMessageCategoryPatchRequest(target.MessageId, target.CategoryNames),
|
||||
request));
|
||||
}
|
||||
|
||||
bundles.Add(new HttpRequestBundle<RequestInformation>(
|
||||
_graphClient.Me.Outlook.MasterCategories[request.PreviousRemoteId].ToDeleteRequestInformation(),
|
||||
request));
|
||||
|
||||
return bundles;
|
||||
}
|
||||
|
||||
private RequestInformation CreateMessageCategoryPatchRequest(string messageId, IReadOnlyList<string> categoryNames)
|
||||
=> _graphClient.Me.Messages[messageId].ToPatchRequestInformation(new Message
|
||||
{
|
||||
Categories = categoryNames?.ToList() ?? []
|
||||
});
|
||||
|
||||
public override async Task DownloadMissingMimeMessageAsync(MailCopy mailItem,
|
||||
MailKit.ITransferProgress transferProgress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
@@ -1962,7 +2090,9 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
for (int i = 0; i < itemCount; i++)
|
||||
{
|
||||
var bundle = batch.ElementAt(i);
|
||||
requiresSerial |= bundle.UIChangeRequest is SendDraftRequest;
|
||||
requiresSerial |= bundle.UIChangeRequest is SendDraftRequest
|
||||
or MailCategoryUpdateRequest
|
||||
or MailCategoryDeleteRequest;
|
||||
|
||||
// UI changes are already applied in ExecuteNativeRequestsAsync before batching.
|
||||
var batchRequestId = await batchContent.AddBatchRequestStepAsync(bundle.NativeRequest);
|
||||
@@ -2110,7 +2240,10 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
|| request is ChangeFlagRequest
|
||||
|| request is MarkReadRequest
|
||||
|| request is ArchiveRequest
|
||||
|| request is MailCategoryAssignmentRequest
|
||||
|| request is RenameFolderRequest
|
||||
|| request is MailCategoryUpdateRequest
|
||||
|| request is MailCategoryDeleteRequest
|
||||
|| request is DeleteFolderRequest
|
||||
|| request is AcceptEventRequest
|
||||
|| request is DeclineEventRequest
|
||||
@@ -2165,6 +2298,26 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
return;
|
||||
|
||||
await UploadCalendarEventAttachmentsAsync(createCalendarEventRequest, createdEventId, CancellationToken.None).ConfigureAwait(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (bundle?.UIChangeRequest is MailCategoryCreateRequest createCategoryRequest)
|
||||
{
|
||||
var createdCategoryId = json?["id"]?.GetValue<string>();
|
||||
if (!string.IsNullOrWhiteSpace(createdCategoryId))
|
||||
{
|
||||
await _mailCategoryService.UpdateRemoteIdAsync(createCategoryRequest.Category.Id, createdCategoryId).ConfigureAwait(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (bundle?.UIChangeRequest is MailCategoryUpdateRequest updateCategoryRequest)
|
||||
{
|
||||
var updatedCategoryId = json?["id"]?.GetValue<string>();
|
||||
if (!string.IsNullOrWhiteSpace(updatedCategoryId))
|
||||
{
|
||||
await _mailCategoryService.UpdateRemoteIdAsync(updateCategoryRequest.Category.Id, updatedCategoryId).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -2367,11 +2520,68 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
// Outlook messages can only be assigned to 1 folder at a time.
|
||||
// Therefore we don't need to create multiple copies of the same message for different folders.
|
||||
var contacts = ExtractContactsFromOutlookMessage(message);
|
||||
var package = new NewMailItemPackage(mailCopy, mimeMessage, assignedFolder.RemoteFolderId, contacts);
|
||||
var package = new NewMailItemPackage(mailCopy, mimeMessage, assignedFolder.RemoteFolderId, contacts, message.Categories);
|
||||
|
||||
return [package];
|
||||
}
|
||||
|
||||
private static MailCategoryColorOption GetMailCategoryColorOption(CategoryColor? color)
|
||||
=> color switch
|
||||
{
|
||||
CategoryColor.Preset0 => new("#FEE2E2", "#991B1B"),
|
||||
CategoryColor.Preset1 => new("#FFEDD5", "#9A3412"),
|
||||
CategoryColor.Preset2 => new("#FEF3C7", "#92400E"),
|
||||
CategoryColor.Preset3 => new("#ECFCCB", "#3F6212"),
|
||||
CategoryColor.Preset4 => new("#DCFCE7", "#166534"),
|
||||
CategoryColor.Preset5 => new("#CCFBF1", "#115E59"),
|
||||
CategoryColor.Preset6 => new("#CFFAFE", "#155E75"),
|
||||
CategoryColor.Preset7 => new("#DBEAFE", "#1D4ED8"),
|
||||
CategoryColor.Preset8 => new("#E0E7FF", "#4338CA"),
|
||||
CategoryColor.Preset9 => new("#F3E8FF", "#7E22CE"),
|
||||
CategoryColor.Preset10 => new("#FCE7F3", "#9D174D"),
|
||||
CategoryColor.Preset11 => new("#FECACA", "#7F1D1D"),
|
||||
CategoryColor.Preset12 => new("#FED7AA", "#7C2D12"),
|
||||
CategoryColor.Preset13 => new("#FDE68A", "#78350F"),
|
||||
CategoryColor.Preset14 => new("#D9F99D", "#365314"),
|
||||
CategoryColor.Preset15 => new("#BBF7D0", "#14532D"),
|
||||
CategoryColor.Preset16 => new("#99F6E4", "#134E4A"),
|
||||
CategoryColor.Preset17 => new("#A5F3FC", "#164E63"),
|
||||
CategoryColor.Preset18 => new("#BFDBFE", "#1E3A8A"),
|
||||
CategoryColor.Preset19 => new("#DDD6FE", "#5B21B6"),
|
||||
CategoryColor.Preset20 => new("#E5E7EB", "#374151"),
|
||||
CategoryColor.Preset21 => new("#D1D5DB", "#1F2937"),
|
||||
CategoryColor.Preset22 => new("#F3F4F6", "#111827"),
|
||||
CategoryColor.Preset23 => new("#E2E8F0", "#334155"),
|
||||
CategoryColor.Preset24 => new("#F8FAFC", "#475569"),
|
||||
_ => new("#E5E7EB", "#374151")
|
||||
};
|
||||
|
||||
private static CategoryColor GetOutlookCategoryColor(MailCategory category)
|
||||
=> (category.BackgroundColorHex?.ToUpperInvariant(), category.TextColorHex?.ToUpperInvariant()) switch
|
||||
{
|
||||
("#FEE2E2", "#991B1B") => CategoryColor.Preset0,
|
||||
("#FFEDD5", "#9A3412") => CategoryColor.Preset1,
|
||||
("#FEF3C7", "#92400E") => CategoryColor.Preset2,
|
||||
("#ECFCCB", "#3F6212") => CategoryColor.Preset3,
|
||||
("#DCFCE7", "#166534") => CategoryColor.Preset4,
|
||||
("#CCFBF1", "#115E59") => CategoryColor.Preset5,
|
||||
("#CFFAFE", "#155E75") => CategoryColor.Preset6,
|
||||
("#DBEAFE", "#1D4ED8") => CategoryColor.Preset7,
|
||||
("#E0E7FF", "#4338CA") => CategoryColor.Preset8,
|
||||
("#F3E8FF", "#7E22CE") => CategoryColor.Preset9,
|
||||
("#FCE7F3", "#9D174D") => CategoryColor.Preset10,
|
||||
("#FECACA", "#7F1D1D") => CategoryColor.Preset11,
|
||||
("#FED7AA", "#7C2D12") => CategoryColor.Preset12,
|
||||
("#FDE68A", "#78350F") => CategoryColor.Preset13,
|
||||
("#D9F99D", "#365314") => CategoryColor.Preset14,
|
||||
("#BBF7D0", "#14532D") => CategoryColor.Preset15,
|
||||
("#99F6E4", "#134E4A") => CategoryColor.Preset16,
|
||||
("#A5F3FC", "#164E63") => CategoryColor.Preset17,
|
||||
("#BFDBFE", "#1E3A8A") => CategoryColor.Preset18,
|
||||
("#DDD6FE", "#5B21B6") => CategoryColor.Preset19,
|
||||
_ => CategoryColor.Preset0
|
||||
};
|
||||
|
||||
private async Task TryMapCalendarInvitationAsync(MailCopy mailCopy, MimeMessage mimeMessage, CancellationToken cancellationToken)
|
||||
{
|
||||
if (mailCopy.ItemType != MailItemType.CalendarInvitation || mimeMessage == null)
|
||||
|
||||
@@ -20,6 +20,7 @@ using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Core.Requests.Bundles;
|
||||
using Wino.Core.Requests.Calendar;
|
||||
using Wino.Core.Requests.Category;
|
||||
using Wino.Core.Requests.Folder;
|
||||
using Wino.Core.Requests.Mail;
|
||||
using Wino.Messaging.UI;
|
||||
@@ -63,6 +64,7 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
/// Only available for Gmail right now.
|
||||
/// </summary>
|
||||
protected virtual Task SynchronizeAliasesAsync() => Task.CompletedTask;
|
||||
protected virtual Task SynchronizeCategoriesAsync(CancellationToken cancellationToken = default) => Task.CompletedTask;
|
||||
|
||||
/// <summary>
|
||||
/// Queues all mail ids for initial synchronization for a specific folder.
|
||||
@@ -194,6 +196,9 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
case MailSynchronizerOperation.Archive:
|
||||
nativeRequests.AddRange(Archive(new BatchArchiveRequest(group.Cast<ArchiveRequest>())));
|
||||
break;
|
||||
case MailSynchronizerOperation.UpdateCategories:
|
||||
nativeRequests.AddRange(UpdateCategories(new BatchMailCategoryAssignmentRequest(group.Cast<MailCategoryAssignmentRequest>())));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -221,6 +226,23 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
break;
|
||||
}
|
||||
}
|
||||
else if (key is CategorySynchronizerOperation categorySynchronizerOperation)
|
||||
{
|
||||
switch (categorySynchronizerOperation)
|
||||
{
|
||||
case CategorySynchronizerOperation.CreateCategory:
|
||||
nativeRequests.AddRange(CreateCategory(group.ElementAt(0) as MailCategoryCreateRequest));
|
||||
break;
|
||||
case CategorySynchronizerOperation.UpdateCategory:
|
||||
nativeRequests.AddRange(UpdateCategory(group.ElementAt(0) as MailCategoryUpdateRequest));
|
||||
break;
|
||||
case CategorySynchronizerOperation.DeleteCategory:
|
||||
nativeRequests.AddRange(DeleteCategory(group.ElementAt(0) as MailCategoryDeleteRequest));
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
changeRequestQueue.Clear();
|
||||
@@ -322,6 +344,30 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
}
|
||||
}
|
||||
|
||||
// Category definition sync.
|
||||
if (options.Type == MailSynchronizationType.Categories)
|
||||
{
|
||||
if (!Account.IsCategorySyncSupported) return MailSynchronizationResult.Empty;
|
||||
|
||||
try
|
||||
{
|
||||
await SynchronizeCategoriesAsync(activeSynchronizationCancellationToken);
|
||||
|
||||
return FinalizeMailResult(MailSynchronizationResult.Empty);
|
||||
}
|
||||
catch (AuthenticationAttentionException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Failed to update categories for {Name}", Account.Name);
|
||||
|
||||
CaptureSynchronizationIssue(SynchronizationIssue.FromException(ex, "CategorySync"));
|
||||
return FinalizeMailResult(MailSynchronizationResult.Failed(ex));
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldDelayExecution)
|
||||
{
|
||||
await Task.Delay(maxExecutionDelay);
|
||||
@@ -526,6 +572,16 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
/// <returns>New synchronization options with minimal HTTP effort.</returns>
|
||||
private MailSynchronizationOptions GetSynchronizationOptionsAfterRequestExecution(List<IRequestBase> requests, Guid existingSynchronizationId)
|
||||
{
|
||||
if (requests.All(a => a is ICategoryActionRequest or MailCategoryAssignmentRequest))
|
||||
{
|
||||
return new MailSynchronizationOptions
|
||||
{
|
||||
AccountId = Account.Id,
|
||||
Id = existingSynchronizationId,
|
||||
Type = MailSynchronizationType.FoldersOnly
|
||||
};
|
||||
}
|
||||
|
||||
List<Guid> synchronizationFolderIds = requests
|
||||
.Where(a => a is ICustomFolderSynchronizationRequest)
|
||||
.Cast<ICustomFolderSynchronizationRequest>()
|
||||
@@ -602,6 +658,10 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
|
||||
public virtual List<IRequestBundle<TBaseRequest>> MarkFolderAsRead(MarkFolderAsReadRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> DeleteFolder(DeleteFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> CreateSubFolder(CreateSubFolderRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> UpdateCategories(BatchMailCategoryAssignmentRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> CreateCategory(MailCategoryCreateRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> UpdateCategory(MailCategoryUpdateRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
public virtual List<IRequestBundle<TBaseRequest>> DeleteCategory(MailCategoryDeleteRequest request) => throw new NotSupportedException(string.Format(Translator.Exception_UnsupportedSynchronizerOperation, this.GetType()));
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
Reference in New Issue
Block a user