Outlook sync improvements.

This commit is contained in:
Burak Kaan Köse
2025-10-12 16:23:33 +02:00
parent 309e891594
commit 7ca6a65559
22 changed files with 853 additions and 111 deletions
+151
View File
@@ -0,0 +1,151 @@
# Mail Synchronization Queue-Based Implementation
This document summarizes the changes made to implement the new queue-based mail synchronization system for Wino Mail.
## Overview
The new system changes the mail synchronization approach from downloading everything immediately during initial sync to queuing mail IDs first and then downloading mail content progressively. This makes initial synchronization much more efficient and responsive.
## Changes Made
### 1. New Database Entity: MailItemQueue
**File:** `Wino.Core.Domain/Entities/Mail/MailItemQueue.cs`
Created a new table to store mail IDs that need to be downloaded from the server:
- `Id`: Primary key (auto-increment)
- `AccountId`: Account that owns the mail
- `FolderId`: Local folder ID
- `RemoteFolderId`: Server-specific folder ID
- `MailCopyId`: Mail ID from the remote server
- `QueuedDate`: When the item was queued
- `Priority`: Priority for processing (lower number = higher priority)
### 2. Enhanced MailItemFolder Entity
**File:** `Wino.Core.Domain/Entities/Mail/MailItemFolder.cs`
Added new property:
- `IsInitialSyncCompleted`: Boolean flag to track whether initial mail ID synchronization is complete for the folder
### 3. New Queue Management Service
**Files:**
- `Wino.Core.Domain/Interfaces/IMailItemQueueService.cs`
- `Wino.Services/MailItemQueueService.cs`
Created a comprehensive service for managing the mail queue with methods to:
- Queue mail items for download
- Get next batch of items to process
- Remove processed items from queue
- Check queue counts and existence
- Clear queue for folders/accounts
### 4. Updated Database Service
**File:** `Wino.Services/DatabaseService.cs`
Added `MailItemQueue` to the database table creation list.
### 5. Enhanced Base Synchronizer
**File:** `Wino.Core/Synchronizers/WinoSynchronizer.cs`
Added new virtual methods that synchronizers can override to support queue-based sync:
- `QueueMailIdsForInitialSyncAsync()`: Queue all mail IDs for initial sync
- `DownloadMailsFromQueueAsync()`: Download mails from queue in batches
- `CreateMinimalMailCopyAsync()`: Create MailCopy with minimal properties (no MIME download)
### 6. OutlookSynchronizer Implementation
**File:** `Wino.Core/Synchronizers/OutlookSynchronizer.cs`
Major changes to implement the new synchronization logic:
#### Constructor Changes
- Added `IMailItemQueueService` dependency injection
#### New Synchronization Algorithm
The `SynchronizeFolderAsync` method now implements the new algorithm:
1. **Check Initial Sync Status**: If `IsInitialSyncCompleted` is false:
- Clear existing queue items for the folder
- Queue all mail IDs using `QueueMailIdsForInitialSyncAsync()`
- Mark initial sync as completed
2. **Process Queue**: Download mails from queue in batches (50 at a time):
- Get queued items for the folder
- Download each mail with minimal properties (no MIME)
- Create MailCopy objects with essential fields only
- Remove processed items from queue
3. **Process Delta Changes**: Handle incremental changes using existing delta sync logic
#### New Methods Implemented
- `QueueMailIdsForInitialSyncAsync()`: Uses PageIterator to efficiently get all message IDs
- `CreateMinimalMailCopyAsync()`: Creates MailCopy without downloading MIME content
- `GetMessageByIdAsync()`: Downloads individual messages with selected properties only
- `ProcessDeltaChangesAsync()`: Handles incremental sync with delta tokens
### 7. Enhanced Change Processor Interface
**File:** `Wino.Core/Integration/Processors/DefaultChangeProcessor.cs`
Added new method to `IOutlookChangeProcessor`:
- `UpdateFolderInitialSyncCompletedAsync()`: Updates the initial sync completion status
**File:** `Wino.Core/Integration/Processors/OutlookChangeProcessor.cs`
Implemented the new method to update the `IsInitialSyncCompleted` field in the database.
### 8. Dependency Injection Updates
**Files:**
- `Wino.Core/Services/SynchronizerFactory.cs`: Added `IMailItemQueueService` dependency and updated OutlookSynchronizer creation
- `Wino.Services/ServicesContainerSetup.cs`: Registered `IMailItemQueueService` as transient service
## Key Benefits
1. **Faster Initial Sync**: Only mail IDs are downloaded initially, making the sync much faster
2. **Progressive Loading**: Mail content is downloaded progressively based on queue
3. **Better User Experience**: Users see folder structure and mail list faster
4. **Efficient Resource Usage**: Avoids downloading full MIME messages during initial sync
5. **Prioritization Support**: Queue system supports priority-based processing
6. **Resilient**: Can handle sync interruptions and resume from where it left off
## Implementation Details
### Queue-Based Processing
- Initial sync: Download only message IDs and queue them
- Progressive download: Process queue items in batches
- Minimal properties: Download only essential mail properties (Subject, Preview, etc.)
- No MIME download: Full MIME messages are not downloaded during initial sync
### Delta Sync Integration
- Existing delta sync logic is preserved for incremental changes
- Delta tokens are properly managed and updated
- Expired tokens trigger queue reset and re-sync
### Error Handling
- Robust error handling for individual mail downloads
- Failed items don't block the entire batch
- Proper logging for debugging and monitoring
## Future Enhancements
The architecture is designed to be easily extended to other synchronizers:
- Gmail and IMAP synchronizers can adopt the same pattern
- Common functionality is in the base `WinoSynchronizer` class
- Queue service is provider-agnostic
## Testing Recommendations
1. Test initial synchronization with large mailboxes
2. Verify progressive loading of mail content
3. Test interruption and resume scenarios
4. Validate delta sync functionality
5. Test with multiple accounts and folders
6. Verify queue management operations
7. Test error scenarios and recovery
This implementation significantly improves the initial synchronization experience while maintaining all existing functionality for incremental syncing.
@@ -36,6 +36,12 @@ public class MailItemFolder : IMailItemFolder
/// </summary>
public string DeltaToken { get; set; }
/// <summary>
/// Whether initial synchronization of mail ids is completed for this folder.
/// Used to determine if we should queue all mail ids first or start downloading from queue.
/// </summary>
public bool IsInitialSyncCompleted { get; set; }
// For GMail Labels
public string TextColorHex { get; set; }
public string BackgroundColorHex { get; set; }
@@ -1,11 +0,0 @@
namespace Wino.Core.Domain.Enums;
/// <summary>
/// Enumeration for the source of synchronization.
/// Right now it can either be from the client or the server.
/// </summary>
public enum SynchronizationSource
{
Client,
Server
}
@@ -10,7 +10,7 @@ namespace Wino.Core.WinUI.Services;
public class StartupBehaviorService : IStartupBehaviorService
{
private const string WinoServerTaskId = "WinoServer";
private const string WinoServerTaskId = "WinoStartupId";
public async Task<StartupBehaviorResult> ToggleStartupBehavior(bool isEnabled)
{
+1
View File
@@ -40,6 +40,7 @@ public static class CoreContainerSetup
// Register error factory handlers
services.AddTransient<ObjectCannotBeDeletedHandler>();
services.AddTransient<DeltaTokenExpiredHandler>();
services.AddTransient<IOutlookSynchronizerErrorHandlerFactory, OutlookSynchronizerErrorHandlingFactory>();
services.AddTransient<IGmailSynchronizerErrorHandlerFactory, GmailSynchronizerErrorHandlingFactory>();
@@ -87,6 +87,14 @@ public interface IOutlookChangeProcessor : IDefaultChangeProcessor
/// <returns>New identifier if success.</returns>
Task UpdateFolderDeltaSynchronizationIdentifierAsync(Guid folderId, string deltaSynchronizationIdentifier);
/// <summary>
/// Updates the initial synchronization completion status for a folder.
/// Used to track whether mail ids have been queued for initial sync.
/// </summary>
/// <param name="folderId">Folder id</param>
/// <param name="isCompleted">Whether initial sync is completed</param>
Task UpdateFolderInitialSyncCompletedAsync(Guid folderId, bool isCompleted);
/// <summary>
/// Outlook may expire folder's delta token after a while.
/// Recommended action for this scenario is to reset token and do full sync.
@@ -38,6 +38,9 @@ public class OutlookChangeProcessor(IDatabaseService databaseService,
public Task UpdateFolderDeltaSynchronizationIdentifierAsync(Guid folderId, string synchronizationIdentifier)
=> Connection.ExecuteAsync("UPDATE MailItemFolder SET DeltaToken = ? WHERE Id = ?", synchronizationIdentifier, folderId);
public Task UpdateFolderInitialSyncCompletedAsync(Guid folderId, bool isCompleted)
=> Connection.ExecuteAsync("UPDATE MailItemFolder SET IsInitialSyncCompleted = ? WHERE Id = ?", isCompleted, folderId);
public async Task ManageCalendarEventAsync(Event calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount)
{
// We parse the occurrences based on the parent event.
@@ -7,9 +7,11 @@ namespace Wino.Core.Services;
public class OutlookSynchronizerErrorHandlingFactory : SynchronizerErrorHandlingFactory, IOutlookSynchronizerErrorHandlerFactory
{
public OutlookSynchronizerErrorHandlingFactory(ObjectCannotBeDeletedHandler objectCannotBeDeleted)
public OutlookSynchronizerErrorHandlingFactory(ObjectCannotBeDeletedHandler objectCannotBeDeleted,
DeltaTokenExpiredHandler deltaTokenExpiredHandler)
{
RegisterHandler(objectCannotBeDeleted);
RegisterHandler(deltaTokenExpiredHandler);
}
public bool CanHandle(SynchronizerErrorContext error) => CanHandle(error);
+1 -1
View File
@@ -147,7 +147,7 @@ public class WinoRequestDelegator : IWinoRequestDelegator
Type = MailSynchronizationType.ExecuteRequests
};
WeakReferenceMessenger.Default.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client));
WeakReferenceMessenger.Default.Send(new NewMailSynchronizationRequested(options));
return Task.CompletedTask;
}
}
@@ -0,0 +1,71 @@
using System.Threading.Tasks;
using Microsoft.Graph.Models.ODataErrors;
using Microsoft.Kiota.Abstractions;
using Serilog;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Errors;
using Wino.Core.Integration.Processors;
namespace Wino.Core.Synchronizers.Errors.Outlook;
/// <summary>
/// Handles 410 Gone errors for Outlook synchronization, which indicates that delta tokens have expired.
/// When this occurs, all local mail cache should be deleted and initial synchronization should be reset.
/// </summary>
public class DeltaTokenExpiredHandler : ISynchronizerErrorHandler
{
private readonly ILogger _logger = Log.ForContext<DeltaTokenExpiredHandler>();
private readonly IOutlookChangeProcessor _outlookChangeProcessor;
public DeltaTokenExpiredHandler(IOutlookChangeProcessor outlookChangeProcessor)
{
_outlookChangeProcessor = outlookChangeProcessor;
}
public bool CanHandle(SynchronizerErrorContext error)
{
// Handle 410 Gone responses which indicate delta token expiration
return error.ErrorCode == 410 ||
(error.Exception is ODataError oDataError && oDataError.ResponseStatusCode == 410) ||
(error.Exception is ApiException apiException && apiException.ResponseStatusCode == 410);
}
public async Task<bool> HandleAsync(SynchronizerErrorContext error)
{
_logger.Warning("Delta token has expired for account {AccountName} ({AccountId}). Deleting all local mail cache and resetting synchronization.",
error.Account.Name, error.Account.Id);
try
{
// Delete all local mail cache for the account
await _outlookChangeProcessor.DeleteUserMailCacheAsync(error.Account.Id).ConfigureAwait(false);
// Reset the account's delta synchronization identifier
await _outlookChangeProcessor.UpdateAccountDeltaSynchronizationIdentifierAsync(error.Account.Id, string.Empty).ConfigureAwait(false);
// Get all folders for the account and reset their delta tokens and initial sync status
var folders = await _outlookChangeProcessor.GetLocalFoldersAsync(error.Account.Id).ConfigureAwait(false);
foreach (var folder in folders)
{
// Reset folder delta token
await _outlookChangeProcessor.UpdateFolderDeltaSynchronizationIdentifierAsync(folder.Id, string.Empty).ConfigureAwait(false);
// Reset initial sync completion status to force full re-sync
await _outlookChangeProcessor.UpdateFolderInitialSyncCompletedAsync(folder.Id, false).ConfigureAwait(false);
}
_logger.Information("Successfully reset synchronization state for account {AccountName} ({AccountId}). Next sync will be a full re-sync.",
error.Account.Name, error.Account.Id);
return true;
}
catch (System.Exception ex)
{
_logger.Error(ex, "Failed to handle delta token expiration for account {AccountName} ({AccountId})",
error.Account.Name, error.Account.Id);
return false;
}
}
}
+1 -1
View File
@@ -857,7 +857,7 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
Type = MailSynchronizationType.IMAPIdle
};
WeakReferenceMessenger.Default.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client));
WeakReferenceMessenger.Default.Send(new NewMailSynchronizationRequested(options));
}
private void IdleNotificationTriggered(object sender, EventArgs e)
+525 -69
View File
@@ -14,6 +14,7 @@ using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Graph;
using Microsoft.Graph.Me.MailFolders.Item.Messages.Delta;
using Microsoft.Graph.Models;
using Microsoft.Graph.Models.ODataErrors;
using Microsoft.Kiota.Abstractions;
@@ -52,7 +53,7 @@ public partial class OutlookSynchronizerJsonContext : JsonSerializerContext;
public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message, Event>
{
public override uint BatchModificationSize => 20;
public override uint InitialMessageDownloadCountPerFolder => 250;
public override uint InitialMessageDownloadCountPerFolder => 1000;
private const uint MaximumAllowedBatchRequestSize = 20;
private const string INBOX_NAME = "inbox";
@@ -87,6 +88,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
private readonly GraphServiceClient _graphClient;
private readonly IOutlookSynchronizerErrorHandlerFactory _errorHandlingFactory;
private readonly SemaphoreSlim _concurrentDownloadSemaphore = new(10); // Limit to 10 concurrent downloads
public OutlookSynchronizer(MailAccount account,
IAuthenticator authenticator,
IOutlookChangeProcessor outlookChangeProcessor,
@@ -229,101 +232,520 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
cancellationToken.ThrowIfCancellationRequested();
retry:
string latestDeltaLink = string.Empty;
_logger.Debug("Synchronizing {FolderName} with direct download approach", folder.FolderName);
bool isInitialSync = string.IsNullOrEmpty(folder.DeltaToken);
Microsoft.Graph.Me.MailFolders.Item.Messages.Delta.DeltaGetResponse messageCollectionPage = null;
_logger.Debug("Synchronizing {FolderName}", folder.FolderName);
if (isInitialSync)
// Check if initial sync is completed for this folder
if (!folder.IsInitialSyncCompleted)
{
_logger.Debug("No sync identifier for Folder {FolderName}. Performing initial sync.", folder.FolderName);
_logger.Debug("Initial sync not completed for folder {FolderName}. Starting mail synchronization.", folder.FolderName);
// No delta link. Performing initial sync.
// Download mails for initial sync
await DownloadMailsForInitialSyncAsync(folder, downloadedMessageIds, cancellationToken).ConfigureAwait(false);
messageCollectionPage = await _graphClient.Me.MailFolders[folder.RemoteFolderId].Messages.Delta.GetAsDeltaGetResponseAsync((config) =>
{
config.QueryParameters.Top = (int)InitialMessageDownloadCountPerFolder;
config.QueryParameters.Select = outlookMessageSelectParameters;
config.QueryParameters.Orderby = ["receivedDateTime desc"];
}, cancellationToken).ConfigureAwait(false);
// Mark initial sync as completed
await _outlookChangeProcessor.UpdateFolderInitialSyncCompletedAsync(folder.Id, true).ConfigureAwait(false);
folder.IsInitialSyncCompleted = true;
}
else
{
// Initial sync is completed, process delta changes and download new mails
_logger.Debug("Initial sync completed for folder {FolderName}. Processing delta changes and downloading new mails.", folder.FolderName);
await ProcessDeltaChangesAndDownloadMailsAsync(folder, downloadedMessageIds, cancellationToken).ConfigureAwait(false);
}
await _outlookChangeProcessor.UpdateFolderLastSyncDateAsync(folder.Id).ConfigureAwait(false);
if (downloadedMessageIds.Any())
{
_logger.Information("Downloaded {Count} messages for folder {FolderName}", downloadedMessageIds.Count, folder.FolderName);
}
return downloadedMessageIds;
}
/// <summary>
/// Downloads mails for initial synchronization using Delta API and direct download with concurrency control.
/// </summary>
private async Task DownloadMailsForInitialSyncAsync(MailItemFolder folder, List<string> downloadedMessageIds, CancellationToken cancellationToken)
{
_logger.Debug("Starting initial mail download for folder {FolderName}", folder.FolderName);
var mailIds = new List<string>();
try
{
// Always use Delta API for initial sync - this ensures proper delta token setup for future incremental syncs
DeltaGetResponse messageCollectionPage = null;
if (string.IsNullOrEmpty(folder.DeltaToken))
{
messageCollectionPage = await _graphClient.Me.MailFolders[folder.RemoteFolderId].Messages.Delta.GetAsDeltaGetResponseAsync((config) =>
{
config.QueryParameters.Select = ["Id"]; // Only get the message Ids
config.QueryParameters.Orderby = ["receivedDateTime desc"]; // Sort by received date desc
config.QueryParameters.Top = (int)InitialMessageDownloadCountPerFolder;
}, cancellationToken).ConfigureAwait(false);
}
else
{
var requestInformation = _graphClient.Me.MailFolders[folder.RemoteFolderId].Messages.Delta.ToGetRequestInformation((config) =>
{
config.QueryParameters.Select = ["Id"]; // Only get the message Ids
config.QueryParameters.Orderby = ["receivedDateTime desc"]; // Sort by received date desc
});
requestInformation.UrlTemplate = requestInformation.UrlTemplate.Insert(requestInformation.UrlTemplate.Length - 1, ",%24deltatoken");
requestInformation.QueryParameters.Add("%24deltatoken", folder.DeltaToken);
messageCollectionPage = await _graphClient.RequestAdapter.SendAsync(requestInformation, DeltaGetResponse.CreateFromDiscriminatorValue, cancellationToken: cancellationToken);
}
// Use PageIterator<DeltaGetResponse> for iterating through the messages
var messageIterator = PageIterator<Message, DeltaGetResponse>.CreatePageIterator(_graphClient, messageCollectionPage, (message) =>
{
if (!IsResourceDeleted(message.AdditionalData))
{
mailIds.Add(message.Id);
}
// Iterator must continue all the time to recieve delta token at the end.
return true;
});
await messageIterator.IterateAsync(cancellationToken).ConfigureAwait(false);
// Extract delta token from the iterator's delta link
string deltaToken = null;
if (!string.IsNullOrEmpty(messageIterator.Deltalink))
{
deltaToken = GetDeltaTokenFromDeltaLink(messageIterator.Deltalink);
}
// Download mails concurrently with semaphore control
if (mailIds.Any())
{
_logger.Information("Starting concurrent download of {Count} mails for folder {FolderName}", mailIds.Count, folder.FolderName);
await DownloadMailsConcurrentlyAsync(mailIds, folder, downloadedMessageIds, cancellationToken).ConfigureAwait(false);
}
else
{
_logger.Information("No mail ids found to download for folder {FolderName}", folder.FolderName);
}
// Store the delta token for future incremental syncs - always store when available
if (!string.IsNullOrEmpty(deltaToken))
{
await _outlookChangeProcessor.UpdateFolderDeltaSynchronizationIdentifierAsync(folder.Id, deltaToken).ConfigureAwait(false);
await _outlookChangeProcessor.UpdateFolderLastSyncDateAsync(folder.Id).ConfigureAwait(false);
folder.DeltaToken = deltaToken;
_logger.Information("Stored delta token for folder {FolderName} - future syncs will be incremental", folder.FolderName);
}
else
{
_logger.Warning("No delta token received for folder {FolderName} - future syncs may re-download messages", folder.FolderName);
}
}
catch (ApiException apiException)
{
// Try to handle the error using the error handling factory
var errorContext = new SynchronizerErrorContext
{
Account = Account,
ErrorCode = (int?)apiException.ResponseStatusCode,
ErrorMessage = $"API error during initial sync: {apiException.Message}",
Exception = apiException
};
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
if (handled)
{
// The error handler has processed the error (e.g., DeltaTokenExpiredHandler for 410)
// Update in-memory folder state if it was a delta token expiration
if (apiException.ResponseStatusCode == 410)
{
folder.DeltaToken = string.Empty;
folder.IsInitialSyncCompleted = false;
_logger.Information("API error handled successfully for folder {FolderName} during initial sync. Error: {ErrorCode}", folder.FolderName, apiException.ResponseStatusCode);
}
}
else
{
// No handler could process this error, log and re-throw
_logger.Error(apiException, "Unhandled API error during initial sync for folder {FolderName}. Error: {ErrorCode}", folder.FolderName, apiException.ResponseStatusCode);
}
// Re-throw the exception so the synchronization can be retried
throw;
}
catch (Exception ex)
{
_logger.Error(ex, "Error occurred during initial mail download for folder {FolderName}", folder.FolderName);
throw;
}
}
/// <summary>
/// Downloads mails concurrently with semaphore control to limit concurrent downloads to 10.
/// </summary>
private async Task DownloadMailsConcurrentlyAsync(List<string> mailIds, MailItemFolder folder, List<string> downloadedMessageIds, CancellationToken cancellationToken)
{
var downloadTasks = mailIds.Select(async mailId =>
{
await _concurrentDownloadSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
var downloaded = await DownloadSingleMailAsync(mailId, folder, cancellationToken).ConfigureAwait(false);
if (downloaded != null)
{
lock (downloadedMessageIds)
{
downloadedMessageIds.Add(downloaded);
}
}
}
finally
{
_concurrentDownloadSemaphore.Release();
}
});
await Task.WhenAll(downloadTasks).ConfigureAwait(false);
}
/// <summary>
/// Downloads a single mail by ID and creates it in the database.
/// </summary>
private async Task<string> DownloadSingleMailAsync(string mailId, MailItemFolder folder, CancellationToken cancellationToken)
{
try
{
// Check if mail already exists in database before downloading
// to avoid unnecessary API calls and reprocessing existing mails
bool mailExists = await _outlookChangeProcessor.IsMailExistsInFolderAsync(mailId, folder.Id).ConfigureAwait(false);
if (mailExists)
{
_logger.Debug("Mail {MailId} already exists in folder {FolderName}, skipping download", mailId, folder.FolderName);
return null; // Not a new download
}
// Download the message with minimal properties
var message = await GetMessageByIdAsync(mailId, cancellationToken).ConfigureAwait(false);
if (message != null)
{
// Create minimal MailCopy without downloading MIME
var mailCopy = await CreateMinimalMailCopyAsync(message, folder, cancellationToken).ConfigureAwait(false);
if (mailCopy != null)
{
// Create a minimal package without MIME for direct sync
var package = new NewMailItemPackage(mailCopy, null, folder.RemoteFolderId);
bool isInserted = await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
if (isInserted)
{
return mailCopy.Id; // Successfully created
}
else
{
_logger.Warning("Failed to insert mail {MailId} for folder {FolderName}", mailId, folder.FolderName);
}
}
else
{
_logger.Debug("Could not create MailCopy for {MailId} in folder {FolderName} (might be unsupported message type)", mailId, folder.FolderName);
}
}
else
{
_logger.Debug("Message {MailId} is null for folder {FolderName} (filtered out)", mailId, folder.FolderName);
}
}
catch (ServiceException serviceException)
{
// Try to handle the error using the error handling factory first
var errorContext = new SynchronizerErrorContext
{
Account = Account,
ErrorCode = (int?)serviceException.ResponseStatusCode,
ErrorMessage = $"Service error during mail download: {serviceException.Message}",
Exception = serviceException
};
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
if (!handled)
{
// No handler could process this error, log appropriately
if (serviceException.ResponseStatusCode == 404)
{
_logger.Warning("Mail {MailId} not found on server (404) for folder {FolderName}", mailId, folder.FolderName);
}
else
{
_logger.Error(serviceException, "Unhandled service error while downloading mail {MailId} for folder {FolderName}. Error: {ErrorCode}", mailId, folder.FolderName, serviceException.ResponseStatusCode);
}
}
else
{
_logger.Information("Service error handled successfully during mail download. Mail: {MailId}, Folder: {FolderName}, Error: {ErrorCode}", mailId, folder.FolderName, serviceException.ResponseStatusCode);
}
}
catch (Exception ex)
{
_logger.Error(ex, "Error occurred while downloading mail {MailId} for folder {FolderName}", mailId, folder.FolderName);
}
return null;
}
private string GetDeltaTokenFromDeltaLink(string deltaLink)
=> Regex.Split(deltaLink, "deltatoken=")[1];
protected override async Task QueueMailIdsForInitialSyncAsync(MailItemFolder folder, CancellationToken cancellationToken = default)
{
// This method is now replaced by direct downloading logic
// Instead of queuing mail IDs, we now directly download them with concurrency control
var downloadedMessageIds = new List<string>();
await DownloadMailsForInitialSyncAsync(folder, downloadedMessageIds, cancellationToken).ConfigureAwait(false);
}
protected override Task<MailCopy> CreateMinimalMailCopyAsync(Message message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default)
{
if (message == null) return Task.FromResult<MailCopy>(null);
// Create MailCopy with minimal properties - no MIME download
var mailCopy = message.AsMailCopy();
mailCopy.FolderId = assignedFolder.Id;
mailCopy.UniqueId = Guid.NewGuid();
mailCopy.FileId = Guid.NewGuid();
return Task.FromResult(mailCopy);
}
private async Task<Message> GetMessageByIdAsync(string messageId, CancellationToken cancellationToken = default)
{
try
{
return await _graphClient.Me.Messages[messageId].GetAsync((config) =>
{
config.QueryParameters.Select = outlookMessageSelectParameters;
}, cancellationToken).ConfigureAwait(false);
}
catch (ServiceException serviceException)
{
// Try to handle the error using the error handling factory first
var errorContext = new SynchronizerErrorContext
{
Account = Account,
ErrorCode = (int?)serviceException.ResponseStatusCode,
ErrorMessage = $"Service error during message retrieval: {serviceException.Message}",
Exception = serviceException
};
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
if (!handled)
{
// No handler could process this error, log and handle appropriately
if (serviceException.ResponseStatusCode == 404)
{
// Re-throw 404 errors to be handled by the caller for queue cleanup
throw;
}
else
{
_logger.Error(serviceException, "Unhandled service error while getting message {MessageId}. Error: {ErrorCode}", messageId, serviceException.ResponseStatusCode);
return null;
}
}
else
{
_logger.Information("Service error handled successfully during message retrieval. Message: {MessageId}, Error: {ErrorCode}", messageId, serviceException.ResponseStatusCode);
return null; // Return null since the error was handled but we couldn't get the message
}
}
catch (Exception ex)
{
_logger.Error(ex, "Failed to get message {MessageId}", messageId);
return null;
}
}
private async Task ProcessDeltaChangesAndDownloadMailsAsync(MailItemFolder folder, List<string> downloadedMessageIds, CancellationToken cancellationToken = default)
{
// Process delta changes and directly download new mails
if (string.IsNullOrEmpty(folder.DeltaToken))
{
_logger.Debug("No delta token available for folder {FolderName}. Skipping delta sync.", folder.FolderName);
return;
}
try
{
var currentDeltaToken = folder.DeltaToken;
_logger.Debug("Processing delta changes for folder {FolderName} with token {DeltaToken}", folder.FolderName, currentDeltaToken.Substring(0, Math.Min(10, currentDeltaToken.Length)) + "...");
// Always use Delta endpoint with proper configuration
var requestInformation = _graphClient.Me.MailFolders[folder.RemoteFolderId].Messages.Delta.ToGetRequestInformation((config) =>
{
config.QueryParameters.Top = (int)InitialMessageDownloadCountPerFolder;
config.QueryParameters.Select = outlookMessageSelectParameters;
config.QueryParameters.Orderby = ["receivedDateTime desc"];
config.QueryParameters.Select = ["Id"]; // Only get IDs for direct download
config.QueryParameters.Orderby = ["receivedDateTime desc"]; // Sort by received date desc
});
requestInformation.UrlTemplate = requestInformation.UrlTemplate.Insert(requestInformation.UrlTemplate.Length - 1, ",%24deltatoken");
requestInformation.QueryParameters.Add("%24deltatoken", currentDeltaToken);
try
{
messageCollectionPage = await _graphClient.RequestAdapter.SendAsync(requestInformation, Microsoft.Graph.Me.MailFolders.Item.Messages.Delta.DeltaGetResponse.CreateFromDiscriminatorValue, cancellationToken: cancellationToken);
}
catch (ApiException apiException) when (apiException.ResponseStatusCode == 410)
{
folder.DeltaToken = string.Empty;
var messageCollectionPage = await _graphClient.RequestAdapter.SendAsync(requestInformation,
DeltaGetResponse.CreateFromDiscriminatorValue,
cancellationToken: cancellationToken);
goto retry;
var newMailIds = new List<string>();
// Use PageIterator<DeltaGetResponse> for iterating through delta changes
var messageIterator = PageIterator<Message, DeltaGetResponse>
.CreatePageIterator(_graphClient, messageCollectionPage, (message) =>
{
// Only process new messages, not deleted ones
if (!IsResourceDeleted(message.AdditionalData))
{
newMailIds.Add(message.Id);
}
return true;
});
await messageIterator.IterateAsync(cancellationToken).ConfigureAwait(false);
// Download new mails directly with concurrency control
if (newMailIds.Any())
{
_logger.Information("Starting direct download of {Count} new mails from delta sync for folder {FolderName}", newMailIds.Count, folder.FolderName);
await DownloadMailsConcurrentlyAsync(newMailIds, folder, downloadedMessageIds, cancellationToken).ConfigureAwait(false);
}
// Update delta token for next sync - always store when there are no nextPageToken remaining
if (!string.IsNullOrEmpty(messageIterator.Deltalink))
{
var deltaToken = GetDeltaTokenFromDeltaLink(messageIterator.Deltalink);
await _outlookChangeProcessor.UpdateFolderDeltaSynchronizationIdentifierAsync(folder.Id, deltaToken).ConfigureAwait(false);
folder.DeltaToken = deltaToken; // Update in-memory object too
_logger.Debug("Updated delta token for folder {FolderName} after processing delta changes", folder.FolderName);
}
}
var messageIteratorAsync = PageIterator<Message, Microsoft.Graph.Me.MailFolders.Item.Messages.Delta.DeltaGetResponse>.CreatePageIterator(_graphClient, messageCollectionPage, async (item) =>
catch (ApiException apiException)
{
try
// Try to handle the error using the error handling factory
var errorContext = new SynchronizerErrorContext
{
await _handleItemRetrievalSemaphore.WaitAsync();
return await HandleItemRetrievedAsync(item, folder, downloadedMessageIds, cancellationToken);
}
catch (Exception ex)
Account = Account,
ErrorCode = (int?)apiException.ResponseStatusCode,
ErrorMessage = $"API error during delta sync: {apiException.Message}",
Exception = apiException
};
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
if (handled)
{
_logger.Error(ex, "Error occurred while handling item {Id} for folder {FolderName}", item.Id, folder.FolderName);
// The error handler has processed the error (e.g., DeltaTokenExpiredHandler for 410)
// Update in-memory folder state if it was a delta token expiration
if (apiException.ResponseStatusCode == 410)
{
folder.DeltaToken = string.Empty;
folder.IsInitialSyncCompleted = false;
_logger.Information("API error handled successfully for folder {FolderName} during delta sync. Error: {ErrorCode}", folder.FolderName, apiException.ResponseStatusCode);
}
}
finally
else
{
_handleItemRetrievalSemaphore.Release();
// No handler could process this error, log and re-throw
_logger.Error(apiException, "Unhandled API error during delta sync for folder {FolderName}. Error: {ErrorCode}", folder.FolderName, apiException.ResponseStatusCode);
}
return true;
});
await messageIteratorAsync
.IterateAsync(cancellationToken)
.ConfigureAwait(false);
latestDeltaLink = messageIteratorAsync.Deltalink;
if (downloadedMessageIds.Any())
{
_logger.Debug("Downloaded {Count} messages for folder {FolderName}", downloadedMessageIds.Count, folder.FolderName);
}
//Store delta link for tracking new changes.
if (!string.IsNullOrEmpty(latestDeltaLink))
catch (Exception ex)
{
// Parse Delta Token from Delta Link since v5 of Graph SDK works based on the token, not the link.
var deltaToken = GetDeltaTokenFromDeltaLink(latestDeltaLink);
await _outlookChangeProcessor.UpdateFolderDeltaSynchronizationIdentifierAsync(folder.Id, deltaToken).ConfigureAwait(false);
_logger.Error(ex, "Error processing delta changes for folder {FolderName}", folder.FolderName);
}
await _outlookChangeProcessor.UpdateFolderLastSyncDateAsync(folder.Id).ConfigureAwait(false);
return downloadedMessageIds;
}
private string GetDeltaTokenFromDeltaLink(string deltaLink)
=> Regex.Split(deltaLink, "deltatoken=")[1];
private async Task ProcessDeltaChangesAsync(MailItemFolder folder, List<string> downloadedMessageIds, CancellationToken cancellationToken = default)
{
// Only process delta changes if we have a delta token (not initial sync)
if (string.IsNullOrEmpty(folder.DeltaToken))
return;
try
{
var currentDeltaToken = folder.DeltaToken;
// Always use Delta endpoint with proper configuration
var requestInformation = _graphClient.Me.MailFolders[folder.RemoteFolderId].Messages.Delta.ToGetRequestInformation((config) =>
{
config.QueryParameters.Select = outlookMessageSelectParameters;
config.QueryParameters.Orderby = ["receivedDateTime desc"]; // Sort by received date desc
});
requestInformation.UrlTemplate = requestInformation.UrlTemplate.Insert(requestInformation.UrlTemplate.Length - 1, ",%24deltatoken");
requestInformation.QueryParameters.Add("%24deltatoken", currentDeltaToken);
var messageCollectionPage = await _graphClient.RequestAdapter.SendAsync(requestInformation,
DeltaGetResponse.CreateFromDiscriminatorValue,
cancellationToken: cancellationToken);
// Use PageIterator<DeltaGetResponse> for iterating mails
var messageIterator = PageIterator<Message, DeltaGetResponse>
.CreatePageIterator(_graphClient, messageCollectionPage, async (item) =>
{
try
{
await _handleItemRetrievalSemaphore.WaitAsync();
return await HandleItemRetrievedAsync(item, folder, downloadedMessageIds, cancellationToken);
}
catch (Exception ex)
{
_logger.Error(ex, "Error occurred while handling delta item {Id} for folder {FolderName}", item.Id, folder.FolderName);
}
finally
{
_handleItemRetrievalSemaphore.Release();
}
return true;
});
await messageIterator.IterateAsync(cancellationToken).ConfigureAwait(false);
// Update delta token for next sync - store delta token when there are no nextPageToken remaining
if (!string.IsNullOrEmpty(messageIterator.Deltalink))
{
var deltaToken = GetDeltaTokenFromDeltaLink(messageIterator.Deltalink);
await _outlookChangeProcessor.UpdateFolderDeltaSynchronizationIdentifierAsync(folder.Id, deltaToken).ConfigureAwait(false);
_logger.Debug("Updated delta token for folder {FolderName} after processing delta changes", folder.FolderName);
}
}
catch (ApiException apiException)
{
// Try to handle the error using the error handling factory
var errorContext = new SynchronizerErrorContext
{
Account = Account,
ErrorCode = (int?)apiException.ResponseStatusCode,
ErrorMessage = $"API error during legacy delta sync: {apiException.Message}",
Exception = apiException
};
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
if (!handled)
{
// No handler could process this error, log and re-throw
_logger.Error(apiException, "Unhandled API error during legacy delta sync for folder {FolderName}. Error: {ErrorCode}", folder.FolderName, apiException.ResponseStatusCode);
}
}
}
private bool IsResourceDeleted(IDictionary<string, object> additionalData)
=> additionalData != null && additionalData.ContainsKey("@removed");
@@ -551,10 +973,44 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
Microsoft.Graph.Me.MailFolders.Delta.DeltaGetResponse.CreateFromDiscriminatorValue,
cancellationToken: cancellationToken).ConfigureAwait(false);
}
catch (ApiException apiException) when (apiException.ResponseStatusCode == 410)
catch (ApiException apiException)
{
Account.SynchronizationDeltaIdentifier = string.Empty;
return await GetDeltaFoldersAsync(cancellationToken);
// Try to handle the error using the error handling factory
var errorContext = new SynchronizerErrorContext
{
Account = Account,
ErrorCode = (int?)apiException.ResponseStatusCode,
ErrorMessage = $"API error during folder synchronization: {apiException.Message}",
Exception = apiException
};
var handled = await _errorHandlingFactory.HandleErrorAsync(errorContext).ConfigureAwait(false);
if (handled)
{
// The error handler has processed the error (e.g., DeltaTokenExpiredHandler for 410)
// Update in-memory account state if it was a delta token expiration
if (apiException.ResponseStatusCode == 410)
{
Account.SynchronizationDeltaIdentifier = string.Empty;
_logger.Information("API error handled successfully for account {AccountName} during folder sync. Error: {ErrorCode}", Account.Name, apiException.ResponseStatusCode);
}
}
else
{
// No handler could process this error, log and re-throw
_logger.Error(apiException, "Unhandled API error during folder synchronization for account {AccountName}. Error: {ErrorCode}", Account.Name, apiException.ResponseStatusCode);
throw;
}
// If a handler processed the error and it was 410, retry with fresh token
if (apiException.ResponseStatusCode == 410)
{
return await GetDeltaFoldersAsync(cancellationToken);
}
// For other handled errors, we still need to throw since we can't return a meaningful response
throw;
}
}
@@ -58,6 +58,36 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
/// </summary>
protected virtual Task SynchronizeAliasesAsync() => Task.CompletedTask;
/// <summary>
/// Queues all mail ids for initial synchronization for a specific folder.
/// Only overridden by synchronizers that support the new queue-based sync.
/// </summary>
/// <param name="folder">Folder to queue mail ids for</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Task</returns>
protected virtual Task QueueMailIdsForInitialSyncAsync(MailItemFolder folder, CancellationToken cancellationToken = default) => Task.CompletedTask;
/// <summary>
/// Downloads mail items from the queue in batches.
/// Only overridden by synchronizers that support the new queue-based sync.
/// </summary>
/// <param name="folder">Folder to download mails for</param>
/// <param name="batchSize">Number of items to download in each batch</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>List of downloaded mail ids</returns>
protected virtual Task<List<string>> DownloadMailsFromQueueAsync(MailItemFolder folder, int batchSize, CancellationToken cancellationToken = default) => Task.FromResult(new List<string>());
/// <summary>
/// Creates a MailCopy object with minimal properties from the native message type.
/// This is used for queue-based sync to avoid downloading full MIME messages.
/// Only overridden by synchronizers that support the new queue-based sync.
/// </summary>
/// <param name="message">Native message type</param>
/// <param name="assignedFolder">Folder this message belongs to</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>MailCopy with minimal properties</returns>
protected virtual Task<MailCopy> CreateMinimalMailCopyAsync(TMessageType message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default) => Task.FromResult<MailCopy>(null);
/// <summary>
/// Internally synchronizes the account's mails with the given options.
/// Not exposed and overriden for each synchronizer.
+2 -3
View File
@@ -5,7 +5,6 @@ using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using MoreLinq;
using MoreLinq.Extensions;
@@ -283,7 +282,7 @@ public partial class AppShellViewModel : MailBaseViewModel,
Type = MailSynchronizationType.FullFolders
};
Messenger.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client));
Messenger.Send(new NewMailSynchronizationRequested(options));
}
}
@@ -867,7 +866,7 @@ public partial class AppShellViewModel : MailBaseViewModel,
Type = MailSynchronizationType.FullFolders,
};
Messenger.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client));
Messenger.Send(new NewMailSynchronizationRequested(options));
try
{
@@ -504,7 +504,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
GroupedSynchronizationTrackingId = trackingSynchronizationId
};
Messenger.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client));
Messenger.Send(new NewMailSynchronizationRequested(options));
}
}
+6 -3
View File
@@ -15,6 +15,8 @@ namespace Wino.Mail.WinUI;
public partial class App : WinoApplication, IRecipient<NewMailSynchronizationRequested>
{
private ISynchronizationManager _synchronizationManager;
public App()
{
InitializeComponent();
@@ -77,7 +79,6 @@ public partial class App : WinoApplication, IRecipient<NewMailSynchronizationReq
return services.BuildServiceProvider();
}
protected override async void OnLaunched(Microsoft.UI.Xaml.LaunchActivatedEventArgs args)
{
// TODO: Check app relaunch mutex before loading anything.
@@ -87,6 +88,8 @@ public partial class App : WinoApplication, IRecipient<NewMailSynchronizationReq
var configService = Services.GetRequiredService<IConfigurationService>();
var nativeAppService = Services.GetRequiredService<INativeAppService>();
_synchronizationManager = Services.GetRequiredService<ISynchronizationManager>();
// Load saved backdrop type before creating window
var savedBackdropType = (WindowBackdropType)configService.Get("WindowBackdropTypeKey", (int)WindowBackdropType.Mica);
@@ -111,8 +114,8 @@ public partial class App : WinoApplication, IRecipient<NewMailSynchronizationReq
MainWindow.Activate();
}
public async void Receive(NewMailSynchronizationRequested message)
public void Receive(NewMailSynchronizationRequested message)
{
// TODO: Trigger new sync.
_synchronizationManager.SynchronizeMailAsync(message.Options);
}
}
+9 -1
View File
@@ -4,6 +4,7 @@
xmlns="http://schemas.microsoft.com/appx/manifest/foundation/windows10"
xmlns:mp="http://schemas.microsoft.com/appx/2014/phone/manifest"
xmlns:uap="http://schemas.microsoft.com/appx/manifest/uap/windows10"
xmlns:uap5="http://schemas.microsoft.com/appx/manifest/uap/windows10/5"
xmlns:rescap="http://schemas.microsoft.com/appx/manifest/foundation/windows10/restrictedcapabilities"
IgnorableNamespaces="uap rescap">
@@ -24,7 +25,7 @@
<mp:PhoneIdentity PhoneProductId="3879fcfb-a561-4599-9103-e0c9b35a271f" PhonePublisherId="00000000-0000-0000-0000-000000000000"/>
<Properties>
<DisplayName>Wino Mail (Preview)</DisplayName>
<DisplayName>Wino Mail</DisplayName>
<PublisherDisplayName>Burak KÖSE</PublisherDisplayName>
<Logo>Assets\StoreLogo.png</Logo>
</Properties>
@@ -53,6 +54,13 @@
</uap:VisualElements>
<Extensions>
<uap5:Extension Category="windows.startupTask">
<uap5:StartupTask
TaskId="WinoStartupId"
Enabled="true"
DisplayName="Wino Startup Service" />
</uap5:Extension>
<!-- Protocol activation: mailto -->
<uap:Extension Category="windows.protocol">
<uap:Protocol Name="mailto" />
+1 -1
View File
@@ -99,7 +99,7 @@ public class DialogService : DialogServiceBase, IMailDialogService
Type = MailSynchronizationType.FullFolders,
};
WeakReferenceMessenger.Default.Send(new NewMailSynchronizationRequested(options, SynchronizationSource.Client));
WeakReferenceMessenger.Default.Send(new NewMailSynchronizationRequested(options));
}
}
catch (Exception ex)
+11 -1
View File
@@ -49,19 +49,29 @@ public sealed partial class MailListPage : MailListPageAbstract,
{
base.OnNavigatedTo(e);
// Bindings.Update();
Bindings.Update();
// Delegate to ViewModel.
if (e.Parameter is NavigateMailFolderEventArgs folderNavigationArgs)
{
WeakReferenceMessenger.Default.Send(new ActiveMailFolderChangedEvent(folderNavigationArgs.BaseFolderMenuItem, folderNavigationArgs.FolderInitLoadAwaitTask));
}
WeakReferenceMessenger.Default.Register<ClearMailSelectionsRequested>(this);
WeakReferenceMessenger.Default.Register<ActiveMailItemChangedEvent>(this);
WeakReferenceMessenger.Default.Register<SelectMailItemContainerEvent>(this);
WeakReferenceMessenger.Default.Register<DisposeRenderingFrameRequested>(this);
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
base.OnNavigatedFrom(e);
WeakReferenceMessenger.Default.Unregister<ClearMailSelectionsRequested>(this);
WeakReferenceMessenger.Default.Unregister<ActiveMailItemChangedEvent>(this);
WeakReferenceMessenger.Default.Unregister<SelectMailItemContainerEvent>(this);
WeakReferenceMessenger.Default.Unregister<DisposeRenderingFrameRequested>(this);
// Dispose all WinoListView items.
MailListView.Dispose();
@@ -1,5 +1,4 @@
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Synchronization;
namespace Wino.Messaging.Server;
@@ -8,10 +7,10 @@ namespace Wino.Messaging.Server;
/// Triggers a new mail synchronization if possible.
/// </summary>
/// <param name="Options">Options for synchronization.</param>
public record NewMailSynchronizationRequested(MailSynchronizationOptions Options, SynchronizationSource Source) : IClientMessage, IUIMessage;
public record NewMailSynchronizationRequested(MailSynchronizationOptions Options) : IClientMessage, IUIMessage;
/// <summary>
/// Triggers a new calendar synchronization if possible.
/// </summary>
/// <param name="Options">Options for synchronization.</param>
public record NewCalendarSynchronizationRequested(CalendarSynchronizationOptions Options, SynchronizationSource Source) : IClientMessage;
public record NewCalendarSynchronizationRequested(CalendarSynchronizationOptions Options) : IClientMessage;
+14 -10
View File
@@ -730,21 +730,25 @@ public class MailService : BaseDatabaseService, IMailService
// This is because 1 mail may have multiple copies in different folders.
// but only single MIME to represent all.
// Save mime file to disk.
var isMimeExists = await _mimeFileService.IsMimeExistAsync(accountId, mailCopy.FileId).ConfigureAwait(false);
// Save mime file to disk if provided.
if (!isMimeExists)
if (mimeMessage != null)
{
bool isMimeSaved = await _mimeFileService.SaveMimeMessageAsync(mailCopy.FileId, mimeMessage, accountId).ConfigureAwait(false);
var isMimeExists = await _mimeFileService.IsMimeExistAsync(accountId, mailCopy.FileId).ConfigureAwait(false);
if (!isMimeSaved)
if (!isMimeExists)
{
_logger.Warning("Failed to save mime file for {MailCopyId}.", mailCopy.Id);
}
}
bool isMimeSaved = await _mimeFileService.SaveMimeMessageAsync(mailCopy.FileId, mimeMessage, accountId).ConfigureAwait(false);
// Save contact information.
await _contactService.SaveAddressInformationAsync(mimeMessage).ConfigureAwait(false);
if (!isMimeSaved)
{
_logger.Warning("Failed to save mime file for {MailCopyId}.", mailCopy.Id);
}
}
// Save contact information.
await _contactService.SaveAddressInformationAsync(mimeMessage).ConfigureAwait(false);
}
// Create mail copy in the database.
// Update if exists.
+5 -3
View File
@@ -5,6 +5,11 @@
<RuntimeIdentifiers>win-x86;win-x64;win-arm64</RuntimeIdentifiers>
<ProduceReferenceAssembly>true</ProduceReferenceAssembly>
</PropertyGroup>
<ItemGroup>
<Compile Remove="Misc\**" />
<EmbeddedResource Remove="Misc\**" />
<None Remove="Misc\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" />
<PackageReference Include="Ical.Net" />
@@ -20,7 +25,4 @@
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />
<ProjectReference Include="..\Wino.Messages\Wino.Messaging.csproj" />
</ItemGroup>
<ItemGroup>
<Folder Include="Misc\" />
</ItemGroup>
</Project>