Mail queues.

This commit is contained in:
Burak Kaan Köse
2025-10-30 17:15:05 +01:00
parent b0ac6e4e55
commit 2d81d07c0a
13 changed files with 579 additions and 472 deletions
+2 -2
View File
@@ -52,7 +52,7 @@
<PackageVersion Include="System.Text.Encoding.CodePages" Version="9.0.10" />
<PackageVersion Include="System.Text.Json" Version="9.0.10" />
<PackageVersion Include="H.NotifyIcon.Wpf" Version="2.3.0" />
<PackageVersion Include="H.NotifyIcon.WinUI" Version="2.3.1" />
<PackageVersion Include="H.NotifyIcon.WinUI" Version="2.3.2" />
<PackageVersion Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
<PackageVersion Include="Google.Apis.Auth" Version="1.72.0" />
<PackageVersion Include="Google.Apis.Calendar.v3" Version="1.69.0.3746" />
@@ -64,7 +64,7 @@
<PackageVersion Include="System.Reactive" Version="6.1.0" />
<PackageVersion Include="Microsoft.Bcl.AsyncInterfaces" Version="9.0.10" />
<PackageVersion Include="System.Text.Encodings.Web" Version="9.0.10" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6584" />
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6901" />
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.0.250930001-experimental1" />
<PackageVersion Include="WinUIEx" Version="2.9.0" />
</ItemGroup>
@@ -0,0 +1,19 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities.Mail;
public class MailItemQueue
{
[PrimaryKey]
public Guid Id { get; set; }
public Guid AccountId { get; set; }
public string RemoteServerId { get; set; }
public bool IsProcessed { get; set; }
public int FailedCount { get; set; }
public DateTime CreatedAt { get; set; }
public DateTime? ProcessedAt { get; set; }
public bool IsRecent() => (DateTime.UtcNow - CreatedAt).TotalDays <= 7;
public bool ShouldDelete() => IsProcessed || FailedCount >= 30;
}
@@ -33,6 +33,11 @@ public class MailAccount
/// </summary>
public MailProviderType ProviderType { get; set; }
/// <summary>
/// Gets or sets the initial mail sync status for the account.
/// </summary>
public InitialSynchronizationStatus SynchronizationStatus { get; set; }
/// <summary>
/// For tracking mail change delta.
/// Gmail : historyId
@@ -0,0 +1,8 @@
namespace Wino.Core.Domain.Enums;
public enum InitialSynchronizationStatus
{
None,
IdsFetched,
Completed
}
@@ -162,4 +162,9 @@ public interface IMailService
/// <param name="onlineArchiveMailIds">Retrieved MailCopy ids from search result.</param>
/// <returns>Result model that contains added and removed mail copy ids.</returns>
Task<GmailArchiveComparisonResult> GetGmailArchiveComparisonResultAsync(Guid archiveFolderId, List<string> onlineArchiveMailIds);
Task ClearMailItemQueueAsync(Guid accountId);
Task AddMailItemQueueItemsAsync(IEnumerable<MailItemQueue> queueItems);
Task<int> GetMailItemQueueCountAsync(Guid accountId);
Task<List<MailItemQueue>> GetMailItemQueueAsync(Guid accountId, int take);
Task UpdateMailItemQueueAsync(IEnumerable<MailItemQueue> queueItems);
}
@@ -1,17 +1,14 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Google.Apis.Calendar.v3.Data;
using Google.Apis.Gmail.v1.Data;
using MimeKit;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Calendar;
using Wino.Core.Domain.Entities.Mail;
using Wino.Core.Domain.Enums;
using Wino.Core.Misc;
using Wino.Services;
using Wino.Services.Extensions;
namespace Wino.Core.Extensions;
@@ -121,41 +118,6 @@ public static class GoogleIntegratorExtensions
return GetNormalizedLabelName(lastPart);
}
/// <summary>
/// Returns MailCopy out of native Gmail message and converted MimeMessage of that native messaage.
/// </summary>
/// <param name="gmailMessage">Gmail Message</param>
/// <param name="mimeMessage">MimeMessage representation of that native message.</param>
/// <returns>MailCopy object that is ready to be inserted to database.</returns>
public static MailCopy AsMailCopy(this Message gmailMessage, MimeMessage mimeMessage)
{
bool isUnread = gmailMessage.GetIsUnread();
bool isFocused = gmailMessage.GetIsFocused();
bool isFlagged = gmailMessage.GetIsFlagged();
bool isDraft = gmailMessage.GetIsDraft();
return new MailCopy()
{
CreationDate = mimeMessage.Date.UtcDateTime,
Subject = HttpUtility.HtmlDecode(mimeMessage.Subject),
FromName = MailkitClientExtensions.GetActualSenderName(mimeMessage),
FromAddress = MailkitClientExtensions.GetActualSenderAddress(mimeMessage),
PreviewText = HttpUtility.HtmlDecode(gmailMessage.Snippet),
ThreadId = gmailMessage.ThreadId,
Importance = (MailImportance)mimeMessage.Importance,
Id = gmailMessage.Id,
IsDraft = isDraft,
HasAttachments = mimeMessage.Attachments.Any(),
IsRead = !isUnread,
IsFlagged = isFlagged,
IsFocused = isFocused,
InReplyTo = mimeMessage.InReplyTo,
MessageId = mimeMessage.MessageId,
References = mimeMessage.References.GetReferences(),
FileId = Guid.NewGuid()
};
}
public static List<RemoteAccountAlias> GetRemoteAliases(this ListSendAsResponse response)
{
return response?.SendAs?.Select(a => new RemoteAccountAlias()
@@ -65,6 +65,11 @@ public interface IDefaultChangeProcessor
Task<bool> IsMailExistsInFolderAsync(string messageId, Guid folderId);
Task<List<string>> AreMailsExistsAsync(IEnumerable<string> mailCopyIds);
Task<string> UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string synchronizationDeltaIdentifier);
Task ClearMailItemQueueAsync(Guid accountId);
Task AddMailItemQueueItemsAsync(IEnumerable<MailItemQueue> queueItems);
Task<int> GetMailItemQueueCountAsync(Guid accountId);
Task<List<MailItemQueue>> GetMailItemQueueAsync(Guid accountId, int take);
Task UpdateMailItemQueueAsync(IEnumerable<MailItemQueue> queueItems);
}
public interface IGmailChangeProcessor : IDefaultChangeProcessor
@@ -208,6 +213,21 @@ public class DefaultChangeProcessor(IDatabaseService databaseService,
public Task UpdateCalendarDeltaSynchronizationToken(Guid calendarId, string deltaToken)
=> CalendarService.UpdateCalendarDeltaSynchronizationToken(calendarId, deltaToken);
public Task ClearMailItemQueueAsync(Guid accountId)
=> MailService.ClearMailItemQueueAsync(accountId);
public Task AddMailItemQueueItemsAsync(IEnumerable<MailItemQueue> queueItems)
=> MailService.AddMailItemQueueItemsAsync(queueItems);
public Task<int> GetMailItemQueueCountAsync(Guid accountId)
=> MailService.GetMailItemQueueCountAsync(accountId);
public Task<List<MailItemQueue>> GetMailItemQueueAsync(Guid accountId, int take)
=> MailService.GetMailItemQueueAsync(accountId, take);
public Task UpdateMailItemQueueAsync(IEnumerable<MailItemQueue> queueItems)
=> MailService.UpdateMailItemQueueAsync(queueItems);
public async Task DeleteUserMailCacheAsync(Guid accountId)
{
await _mimeFileService.DeleteUserMimeCacheAsync(accountId).ConfigureAwait(false);
File diff suppressed because it is too large Load Diff
+16 -9
View File
@@ -41,22 +41,27 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
/// <summary>
/// How many items must be downloaded per folder when the folder is first synchronized.
/// Only metadata is downloaded during sync - MIME content is fetched on-demand when user reads mail.
/// </summary>
public abstract uint InitialMessageDownloadCountPerFolder { get; }
/// <summary>
/// Number of MIME messages to download during initial synchronization per folder.
/// For the first messages in each folder during initial sync, both metadata and MIME content will be downloaded.
/// Subsequent messages will only have metadata downloaded, with MIME content fetched on-demand.
/// DEPRECATED: MIME messages are no longer downloaded during synchronization.
/// MIME content is only downloaded when explicitly needed (e.g., when user reads a message).
/// This property is kept for backward compatibility but is no longer used.
/// </summary>
public virtual int InitialSyncMimeDownloadCount => 50;
[Obsolete("MIME messages are no longer downloaded during sync. Use DownloadMissingMimeMessageAsync instead.")]
public virtual int InitialSyncMimeDownloadCount => 0;
/// <summary>
/// Creates a new Wino Mail Item package out of native message type with full Mime.
/// Creates a new Wino Mail Item package out of native message type with metadata only.
/// NO MIME content is downloaded during synchronization - only headers and essential metadata.
/// MIME will be downloaded on-demand when user explicitly reads the message.
/// </summary>
/// <param name="message">Native message type for the synchronizer.</param>
/// <param name="assignedFolder">Folder to assign the mail to.</param>
/// <param name="cancellationToken">Cancellation token</param>
/// <returns>Package that encapsulates downloaded Mime and additional information for adding new mail.</returns>
/// <returns>Package with MailCopy metadata. MimeMessage will be null during sync.</returns>
public abstract Task<List<NewMailItemPackage>> CreateNewMailPackagesAsync(TMessageType message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default);
/// <summary>
@@ -86,13 +91,15 @@ public abstract class WinoSynchronizer<TBaseRequest, TMessageType, TCalendarEven
/// <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.
/// This is used during synchronization to create mail entries WITHOUT downloading MIME content.
/// Only metadata (headers, labels, flags) is extracted from the native message format.
/// MIME content will be downloaded later on-demand when user reads the message.
/// Only overridden by synchronizers that support metadata-only synchronization.
/// </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>
/// <returns>MailCopy with minimal properties populated from metadata</returns>
protected virtual Task<MailCopy> CreateMinimalMailCopyAsync(TMessageType message, MailItemFolder assignedFolder, CancellationToken cancellationToken = default) => Task.FromResult<MailCopy>(null);
/// <summary>
@@ -543,6 +543,7 @@ public partial class MailListPageViewModel : MailBaseViewModel,
{
if (IsInitializingFolder || IsOnlineSearchEnabled) return;
Debug.WriteLine("Loading more...");
await ExecuteUIThread(() => { IsInitializingFolder = true; });
var initializationOptions = new MailListInitializationOptions(ActiveFolder.HandlingFolders,
+2 -3
View File
@@ -92,8 +92,8 @@
ContextRequested="MailItemContextRequested"
CreationDate="{x:Bind CreationDate}"
DisplayMode="{Binding ElementName=root, Path=ViewModel.PreferencesService.MailItemDisplayMode, Mode=OneWay}"
FromAddress="{x:Bind FromAddress}"
FromName="{x:Bind FromName}"
FromAddress="{x:Bind FromAddress, Mode=OneWay}"
FromName="{x:Bind FromName, Mode=OneWay}"
HasAttachments="{x:Bind HasAttachments, Mode=OneWay}"
HoverActionExecutedCommand="{Binding ElementName=root, Path=ViewModel.ExecuteHoverActionCommand}"
IsAvatarVisible="{Binding ElementName=root, Path=ViewModel.PreferencesService.IsShowSenderPicturesEnabled, Mode=OneWay}"
@@ -302,7 +302,6 @@
Grid.Row="1"
Grid.Column="2"
Orientation="Horizontal">
<Button Command="{x:Bind ViewModel.RemoveFirstCommand}" Content="T" />
<Button
Width="36"
Height="36"
+2 -1
View File
@@ -59,7 +59,8 @@ public class DatabaseService : IDatabaseService
typeof(CalendarItem),
typeof(Reminder),
typeof(Thumbnail),
typeof(KeyboardShortcut)
typeof(KeyboardShortcut),
typeof(MailItemQueue)
);
}
}
+42
View File
@@ -697,6 +697,11 @@ public class MailService : BaseDatabaseService, IMailService
await Task.WhenAll(mimeSaveTask, contactSaveTask, insertMailTask).ConfigureAwait(false);
}
public async Task CreateMailAsyncEx(Guid accountId, NewMailItemPackage package)
{
}
public async Task<bool> CreateMailAsync(Guid accountId, NewMailItemPackage package)
{
var account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
@@ -784,6 +789,43 @@ public class MailService : BaseDatabaseService, IMailService
}
}
#region Mail Queue
public Task ClearMailItemQueueAsync(Guid accountId)
=> Connection.ExecuteAsync("DELETE FROM MailItemQueue WHERE AccountId = ?", accountId);
public Task<int> GetMailItemQueueCountAsync(Guid accountId)
=> Connection.Table<MailItemQueue>().Where(a => a.AccountId == accountId).CountAsync();
public Task UpdateMailItemQueueAsync(IEnumerable<MailItemQueue> queueItems)
{
if (queueItems == null || !queueItems.Any())
return Task.CompletedTask;
return Connection.UpdateAllAsync(queueItems);
}
public Task AddMailItemQueueItemsAsync(IEnumerable<MailItemQueue> queueItems)
{
if (queueItems == null || !queueItems.Any())
return Task.CompletedTask;
return Connection.InsertAllAsync(queueItems);
}
public Task<List<MailItemQueue>> GetMailItemQueueAsync(Guid accountId, int take)
{
// Skip not needed. Items are removed as they are processed.
return Connection.Table<MailItemQueue>()
.Where(a => a.AccountId == accountId && !a.IsProcessed)
.OrderBy(a => a.CreatedAt)
.Take(take)
.ToListAsync();
}
#endregion
private async Task<MimeMessage> CreateDraftMimeAsync(MailAccount account, DraftCreationOptions draftCreationOptions)
{
// This unique id is stored in mime headers for Wino to identify remote message with local copy.