Mail queues.
This commit is contained in:
@@ -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
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -59,7 +59,8 @@ public class DatabaseService : IDatabaseService
|
||||
typeof(CalendarItem),
|
||||
typeof(Reminder),
|
||||
typeof(Thumbnail),
|
||||
typeof(KeyboardShortcut)
|
||||
typeof(KeyboardShortcut),
|
||||
typeof(MailItemQueue)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user