CalDav synchronizer, new IMAP setup/edit page.
This commit is contained in:
@@ -54,6 +54,7 @@ public class ImapClientPool : IDisposable
|
||||
private readonly CancellationTokenSource _maintenanceCts = new();
|
||||
private readonly SemaphoreSlim _initializeSemaphore = new(1, 1);
|
||||
private readonly object _idleClientLock = new();
|
||||
private readonly object _initialWarmupLock = new();
|
||||
private readonly ImapServerQuirkProfile _quirks;
|
||||
private readonly ImapImplementation _implementation;
|
||||
private readonly int _maxConnections;
|
||||
@@ -64,6 +65,7 @@ public class ImapClientPool : IDisposable
|
||||
private bool _disposedValue;
|
||||
private bool _initialized;
|
||||
private Task _maintenanceTask;
|
||||
private Task _initialWarmupTask = Task.CompletedTask;
|
||||
|
||||
public bool ThrowOnSSLHandshakeCallback { get; set; }
|
||||
public ImapClientPoolOptions ImapClientPoolOptions { get; }
|
||||
@@ -112,30 +114,20 @@ public class ImapClientPool : IDisposable
|
||||
|
||||
_logger.Information("Initializing IMAP client pool with {MinimumConnections} minimum active connections (max: {MaxConnections})", _targetMinimumConnections, _maxConnections);
|
||||
|
||||
for (int i = 0; i < _targetMinimumConnections; i++)
|
||||
// Fast-path startup: create one client eagerly so first RentAsync() is not blocked by full warm-up.
|
||||
var initialClient = await CreateAndConnectClientAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (initialClient == null)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
var client = await CreateAndConnectClientAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (client != null)
|
||||
{
|
||||
_clientStates[client] = ImapClientState.Available;
|
||||
await _availableClients.Writer.WriteAsync(client, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
throw CreatePoolException("Failed to create initial IMAP connection for the pool.");
|
||||
}
|
||||
|
||||
if (CanCreateAdditionalConnection())
|
||||
{
|
||||
_dedicatedIdleClient = await CreateAndConnectClientAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (_dedicatedIdleClient != null)
|
||||
{
|
||||
_clientStates[_dedicatedIdleClient] = ImapClientState.Idle;
|
||||
}
|
||||
}
|
||||
_clientStates[initialClient] = ImapClientState.Available;
|
||||
await _availableClients.Writer.WriteAsync(initialClient, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
_maintenanceTask = Task.Run(() => MaintenanceLoopAsync(_maintenanceCts.Token), _maintenanceCts.Token);
|
||||
|
||||
_initialized = true;
|
||||
|
||||
ScheduleInitialWarmup();
|
||||
_logger.Information("IMAP client pool initialized. Health: {Health}", Health.Summary);
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -152,7 +144,21 @@ public class ImapClientPool : IDisposable
|
||||
/// <summary>
|
||||
/// Pre-warms the pool (legacy compatibility method).
|
||||
/// </summary>
|
||||
public Task PreWarmPoolAsync() => InitializeAsync(CancellationToken.None);
|
||||
public async Task PreWarmPoolAsync()
|
||||
{
|
||||
await InitializeAsync(CancellationToken.None).ConfigureAwait(false);
|
||||
|
||||
Task warmupTask;
|
||||
lock (_initialWarmupLock)
|
||||
{
|
||||
warmupTask = _initialWarmupTask;
|
||||
}
|
||||
|
||||
if (warmupTask != null)
|
||||
{
|
||||
await warmupTask.ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rents a client from the pool with the default timeout.
|
||||
@@ -440,6 +446,63 @@ public class ImapClientPool : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
private void ScheduleInitialWarmup()
|
||||
{
|
||||
lock (_initialWarmupLock)
|
||||
{
|
||||
if (_initialWarmupTask != null && !_initialWarmupTask.IsCompleted)
|
||||
return;
|
||||
|
||||
_initialWarmupTask = Task.Run(() => EnsureWarmBaselineAsync(_maintenanceCts.Token), _maintenanceCts.Token);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EnsureWarmBaselineAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
await EnsureMinimumConnectionsAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
lock (_idleClientLock)
|
||||
{
|
||||
if (_dedicatedIdleClient != null && _dedicatedIdleClient.IsConnected)
|
||||
return;
|
||||
}
|
||||
|
||||
if (!CanCreateAdditionalConnection())
|
||||
return;
|
||||
|
||||
var idleCandidate = await CreateAndConnectClientAsync(cancellationToken).ConfigureAwait(false);
|
||||
if (idleCandidate == null)
|
||||
return;
|
||||
|
||||
bool assignedAsIdle = false;
|
||||
lock (_idleClientLock)
|
||||
{
|
||||
if (_dedicatedIdleClient == null || !_dedicatedIdleClient.IsConnected)
|
||||
{
|
||||
_dedicatedIdleClient = idleCandidate;
|
||||
_clientStates[idleCandidate] = ImapClientState.Idle;
|
||||
assignedAsIdle = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!assignedAsIdle)
|
||||
{
|
||||
_clientStates[idleCandidate] = ImapClientState.Available;
|
||||
_availableClients.Writer.TryWrite(idleCandidate);
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
// Pool is shutting down.
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.Warning(ex, "Initial IMAP pool warm-up failed. Pool will continue with maintenance recovery.");
|
||||
}
|
||||
}
|
||||
|
||||
private Task CleanupFailedConnectionsAsync()
|
||||
{
|
||||
foreach (var kvp in _clientStates)
|
||||
|
||||
@@ -9,6 +9,7 @@ using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Enums;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.MailItem;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Core.Domain.Models.Synchronization;
|
||||
using Wino.Services;
|
||||
|
||||
@@ -117,6 +118,11 @@ public interface IImapChangeProcessor : IDefaultChangeProcessor
|
||||
/// <param name="folderId">Folder ID.</param>
|
||||
/// <param name="count">Number of recent mails to return.</param>
|
||||
Task<IEnumerable<string>> GetRecentMailIdsForFolderAsync(Guid folderId, int count);
|
||||
|
||||
Task ManageCalendarEventAsync(CalDavCalendarEvent calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount);
|
||||
Task SaveCalendarItemIcsAsync(Guid accountId, Guid calendarId, Guid calendarItemId, string remoteEventId, string remoteResourceHref, string eTag, string icsContent);
|
||||
Task DeleteCalendarItemIcsAsync(Guid accountId, Guid calendarItemId);
|
||||
Task DeleteCalendarIcsForCalendarAsync(Guid accountId, Guid calendarId);
|
||||
}
|
||||
|
||||
public class DefaultChangeProcessor(IDatabaseService databaseService,
|
||||
@@ -196,10 +202,10 @@ public class DefaultChangeProcessor(IDatabaseService databaseService,
|
||||
public Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId)
|
||||
=> CalendarService.GetAccountCalendarsAsync(accountId);
|
||||
|
||||
public Task DeleteCalendarItemAsync(Guid calendarItemId)
|
||||
public virtual Task DeleteCalendarItemAsync(Guid calendarItemId)
|
||||
=> CalendarService.DeleteCalendarItemAsync(calendarItemId);
|
||||
|
||||
public Task DeleteCalendarItemAsync(string calendarRemoteEventId, Guid calendarId)
|
||||
public virtual Task DeleteCalendarItemAsync(string calendarRemoteEventId, Guid calendarId)
|
||||
=> CalendarService.DeleteCalendarItemAsync(calendarRemoteEventId, calendarId);
|
||||
|
||||
public Task<CalendarItem> GetCalendarItemAsync(Guid calendarId, string remoteEventId)
|
||||
|
||||
@@ -1,24 +1,158 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Wino.Core.Domain.Entities.Calendar;
|
||||
using Wino.Core.Domain.Entities.Shared;
|
||||
using Wino.Core.Domain.Interfaces;
|
||||
using Wino.Core.Domain.Models.Calendar;
|
||||
using Wino.Services;
|
||||
|
||||
namespace Wino.Core.Integration.Processors;
|
||||
|
||||
public class ImapChangeProcessor : DefaultChangeProcessor, IImapChangeProcessor
|
||||
{
|
||||
private readonly ICalendarIcsFileService _calendarIcsFileService;
|
||||
|
||||
public ImapChangeProcessor(IDatabaseService databaseService,
|
||||
IFolderService folderService,
|
||||
IMailService mailService,
|
||||
IAccountService accountService,
|
||||
ICalendarService calendarService,
|
||||
IMimeFileService mimeFileService) : base(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
|
||||
IMimeFileService mimeFileService,
|
||||
ICalendarIcsFileService calendarIcsFileService) : base(databaseService, folderService, mailService, calendarService, accountService, mimeFileService)
|
||||
{
|
||||
_calendarIcsFileService = calendarIcsFileService;
|
||||
}
|
||||
|
||||
public Task<IList<uint>> GetKnownUidsForFolderAsync(Guid folderId) => FolderService.GetKnownUidsForFolderAsync(folderId);
|
||||
|
||||
public Task<IEnumerable<string>> GetRecentMailIdsForFolderAsync(Guid folderId, int count)
|
||||
=> MailService.GetRecentMailIdsForFolderAsync(folderId, count);
|
||||
|
||||
public async Task ManageCalendarEventAsync(CalDavCalendarEvent calendarEvent, AccountCalendar assignedCalendar, MailAccount organizerAccount)
|
||||
{
|
||||
if (calendarEvent == null || assignedCalendar == null)
|
||||
return;
|
||||
|
||||
var existingItem = await CalendarService.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.RemoteEventId).ConfigureAwait(false);
|
||||
var isNewItem = existingItem == null;
|
||||
var savingItemId = existingItem?.Id ?? Guid.NewGuid();
|
||||
var savingItem = existingItem ?? new CalendarItem { Id = savingItemId };
|
||||
|
||||
var start = calendarEvent.Start.UtcDateTime;
|
||||
var end = calendarEvent.End.UtcDateTime;
|
||||
|
||||
if (end <= start)
|
||||
end = start.AddHours(1);
|
||||
|
||||
savingItem.RemoteEventId = calendarEvent.RemoteEventId;
|
||||
savingItem.CalendarId = assignedCalendar.Id;
|
||||
savingItem.StartDate = start;
|
||||
savingItem.DurationInSeconds = (end - start).TotalSeconds;
|
||||
savingItem.StartTimeZone = calendarEvent.StartTimeZone;
|
||||
savingItem.EndTimeZone = calendarEvent.EndTimeZone;
|
||||
savingItem.Title = calendarEvent.Title;
|
||||
savingItem.Description = calendarEvent.Description;
|
||||
savingItem.Location = calendarEvent.Location;
|
||||
savingItem.Recurrence = calendarEvent.Recurrence;
|
||||
savingItem.Status = calendarEvent.Status;
|
||||
savingItem.Visibility = calendarEvent.Visibility;
|
||||
savingItem.ShowAs = calendarEvent.ShowAs;
|
||||
savingItem.IsHidden = calendarEvent.IsHidden;
|
||||
savingItem.HtmlLink = string.Empty;
|
||||
savingItem.IsLocked = false;
|
||||
savingItem.OrganizerDisplayName = !string.IsNullOrWhiteSpace(calendarEvent.OrganizerDisplayName)
|
||||
? calendarEvent.OrganizerDisplayName
|
||||
: organizerAccount?.SenderName ?? string.Empty;
|
||||
savingItem.OrganizerEmail = !string.IsNullOrWhiteSpace(calendarEvent.OrganizerEmail)
|
||||
? calendarEvent.OrganizerEmail
|
||||
: organizerAccount?.Address ?? string.Empty;
|
||||
savingItem.AssignedCalendar = assignedCalendar;
|
||||
|
||||
if (savingItem.CreatedAt == default)
|
||||
savingItem.CreatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
savingItem.UpdatedAt = DateTimeOffset.UtcNow;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(calendarEvent.SeriesMasterRemoteEventId))
|
||||
{
|
||||
var parentEvent = await CalendarService
|
||||
.GetCalendarItemAsync(assignedCalendar.Id, calendarEvent.SeriesMasterRemoteEventId)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (parentEvent != null)
|
||||
{
|
||||
savingItem.RecurringCalendarItemId = parentEvent.Id;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
savingItem.RecurringCalendarItemId = null;
|
||||
}
|
||||
|
||||
var attendees = calendarEvent.Attendees?
|
||||
.Where(a => !string.IsNullOrWhiteSpace(a.Email))
|
||||
.Select(a => new CalendarEventAttendee
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CalendarItemId = savingItemId,
|
||||
Name = a.Name,
|
||||
Email = a.Email,
|
||||
AttendenceStatus = a.AttendenceStatus,
|
||||
IsOrganizer = a.IsOrganizer,
|
||||
IsOptionalAttendee = a.IsOptionalAttendee
|
||||
})
|
||||
.ToList();
|
||||
|
||||
var reminders = calendarEvent.Reminders?
|
||||
.Where(r => r.DurationInSeconds > 0)
|
||||
.Select(r => new Reminder
|
||||
{
|
||||
Id = Guid.NewGuid(),
|
||||
CalendarItemId = savingItemId,
|
||||
DurationInSeconds = r.DurationInSeconds,
|
||||
ReminderType = r.ReminderType
|
||||
})
|
||||
.ToList();
|
||||
|
||||
if (isNewItem)
|
||||
{
|
||||
await CalendarService.CreateNewCalendarItemAsync(savingItem, attendees).ConfigureAwait(false);
|
||||
}
|
||||
else
|
||||
{
|
||||
await CalendarService.UpdateCalendarItemAsync(savingItem, attendees).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await CalendarService.SaveRemindersAsync(savingItemId, reminders).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public Task SaveCalendarItemIcsAsync(Guid accountId, Guid calendarId, Guid calendarItemId, string remoteEventId, string remoteResourceHref, string eTag, string icsContent)
|
||||
=> _calendarIcsFileService.SaveCalendarItemIcsAsync(accountId, calendarId, calendarItemId, remoteEventId, remoteResourceHref, eTag, icsContent);
|
||||
|
||||
public Task DeleteCalendarItemIcsAsync(Guid accountId, Guid calendarItemId)
|
||||
=> _calendarIcsFileService.DeleteCalendarItemIcsAsync(accountId, calendarItemId);
|
||||
|
||||
public Task DeleteCalendarIcsForCalendarAsync(Guid accountId, Guid calendarId)
|
||||
=> _calendarIcsFileService.DeleteCalendarIcsForCalendarAsync(accountId, calendarId);
|
||||
|
||||
public override async Task DeleteCalendarItemAsync(Guid calendarItemId)
|
||||
{
|
||||
var item = await CalendarService.GetCalendarItemAsync(calendarItemId).ConfigureAwait(false);
|
||||
if (item == null)
|
||||
return;
|
||||
|
||||
await _calendarIcsFileService.DeleteCalendarItemIcsAsync(item.AssignedCalendar?.AccountId ?? Guid.Empty, calendarItemId).ConfigureAwait(false);
|
||||
await base.DeleteCalendarItemAsync(calendarItemId).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override async Task DeleteCalendarItemAsync(string calendarRemoteEventId, Guid calendarId)
|
||||
{
|
||||
var item = await CalendarService.GetCalendarItemAsync(calendarId, calendarRemoteEventId).ConfigureAwait(false);
|
||||
if (item == null)
|
||||
return;
|
||||
|
||||
await DeleteCalendarItemAsync(item.Id).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user