Add initial mail sync range selection
This commit is contained in:
@@ -112,6 +112,16 @@ public class MailAccount
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DateTime? LastFolderStructureSyncDate { get; set; }
|
public DateTime? LastFolderStructureSyncDate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets when the account was created in Wino.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? CreatedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the timespan used for the account's initial mail synchronization.
|
||||||
|
/// </summary>
|
||||||
|
public InitialSynchronizationRange InitialSynchronizationRange { get; set; } = InitialSynchronizationRange.SixMonths;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets whether the account can perform ProfileInformation sync type.
|
/// Gets whether the account can perform ProfileInformation sync type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
namespace Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
|
public enum InitialSynchronizationRange
|
||||||
|
{
|
||||||
|
SixMonths = 0,
|
||||||
|
ThreeMonths = 1,
|
||||||
|
NineMonths = 2,
|
||||||
|
OneYear = 3,
|
||||||
|
Everything = 4
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
using System;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Wino.Core.Domain.Extensions;
|
||||||
|
|
||||||
|
public static class InitialSynchronizationRangeExtensions
|
||||||
|
{
|
||||||
|
public static DateTime? ToCutoffDateUtc(this InitialSynchronizationRange range, DateTime utcNow)
|
||||||
|
{
|
||||||
|
var normalizedUtcNow = utcNow.Kind == DateTimeKind.Utc
|
||||||
|
? utcNow
|
||||||
|
: utcNow.ToUniversalTime();
|
||||||
|
|
||||||
|
return range switch
|
||||||
|
{
|
||||||
|
InitialSynchronizationRange.ThreeMonths => normalizedUtcNow.AddMonths(-3),
|
||||||
|
InitialSynchronizationRange.SixMonths => normalizedUtcNow.AddMonths(-6),
|
||||||
|
InitialSynchronizationRange.NineMonths => normalizedUtcNow.AddMonths(-9),
|
||||||
|
InitialSynchronizationRange.OneYear => normalizedUtcNow.AddYears(-1),
|
||||||
|
_ => null
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Models.Accounts;
|
namespace Wino.Core.Domain.Models.Accounts;
|
||||||
|
|
||||||
public record AccountCreationDialogResult(MailProviderType ProviderType, string AccountName, SpecialImapProviderDetails SpecialImapProviderDetails, string AccountColorHex);
|
public record AccountCreationDialogResult(
|
||||||
|
MailProviderType ProviderType,
|
||||||
|
string AccountName,
|
||||||
|
SpecialImapProviderDetails SpecialImapProviderDetails,
|
||||||
|
string AccountColorHex,
|
||||||
|
InitialSynchronizationRange InitialSynchronizationRange);
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
|
namespace Wino.Core.Domain.Models.Accounts;
|
||||||
|
|
||||||
|
public sealed class InitialSynchronizationRangeOption
|
||||||
|
{
|
||||||
|
public InitialSynchronizationRange Range { get; }
|
||||||
|
public string DisplayText { get; }
|
||||||
|
|
||||||
|
public bool IsEverything => Range == InitialSynchronizationRange.Everything;
|
||||||
|
|
||||||
|
public InitialSynchronizationRangeOption(InitialSynchronizationRange range, string displayText)
|
||||||
|
{
|
||||||
|
Range = range;
|
||||||
|
DisplayText = displayText;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,6 +23,14 @@
|
|||||||
"AccountCreationDialog_Initializing": "initializing",
|
"AccountCreationDialog_Initializing": "initializing",
|
||||||
"AccountCreationDialog_PreparingFolders": "We are getting folder information at the moment.",
|
"AccountCreationDialog_PreparingFolders": "We are getting folder information at the moment.",
|
||||||
"AccountCreationDialog_SigninIn": "Account information is being saved.",
|
"AccountCreationDialog_SigninIn": "Account information is being saved.",
|
||||||
|
"AccountCreation_InitialSynchronization_Title": "Mail synchronization range",
|
||||||
|
"AccountCreation_InitialSynchronization_Description": "Choose how far back Wino should download your mail during the first synchronization.",
|
||||||
|
"AccountCreation_InitialSynchronization_3Months": "3 Months",
|
||||||
|
"AccountCreation_InitialSynchronization_6Months": "6 Months",
|
||||||
|
"AccountCreation_InitialSynchronization_9Months": "9 Months",
|
||||||
|
"AccountCreation_InitialSynchronization_Year": "Year",
|
||||||
|
"AccountCreation_InitialSynchronization_Everything": "Everything",
|
||||||
|
"AccountCreation_InitialSynchronization_EverythingWarning": "This will synchronize all your mails to your computer. Extensive use of disk storage is needed. This is not recommended. For optimal performance use smaller synchronization timespan and use online search to access your mails.",
|
||||||
"Purchased": "Purchased",
|
"Purchased": "Purchased",
|
||||||
"AccountEditDialog_Message": "Account Name",
|
"AccountEditDialog_Message": "Account Name",
|
||||||
"AccountEditDialog_Title": "Edit Account",
|
"AccountEditDialog_Title": "Edit Account",
|
||||||
@@ -37,6 +45,8 @@
|
|||||||
"AccountDetailsPage_TabMail": "Mail",
|
"AccountDetailsPage_TabMail": "Mail",
|
||||||
"AccountDetailsPage_TabCalendar": "Calendar",
|
"AccountDetailsPage_TabCalendar": "Calendar",
|
||||||
"AccountDetailsPage_CalendarListDescription": "Select a calendar to configure its settings",
|
"AccountDetailsPage_CalendarListDescription": "Select a calendar to configure its settings",
|
||||||
|
"AccountDetailsPage_InitialSynchronization_Title": "Initial synchronization",
|
||||||
|
"AccountDetailsPage_InitialSynchronization_Description": "Wino synchronized your mails until {0} going back.",
|
||||||
"AddHyperlink": "Add",
|
"AddHyperlink": "Add",
|
||||||
"AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization",
|
"AppCloseBackgroundSynchronizationWarningTitle": "Background Synchronization",
|
||||||
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.",
|
"AppCloseStartupLaunchDisabledWarningMessageFirstLine": "Application has not been set to launch on Windows startup.",
|
||||||
|
|||||||
@@ -81,9 +81,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
public override uint BatchModificationSize => 1000;
|
public override uint BatchModificationSize => 1000;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Maximum messages to fetch per folder during initial sync (1500).
|
/// Legacy page size hint kept for compatibility with shared synchronizer contracts.
|
||||||
/// All messages are downloaded with METADATA ONLY - no raw MIME content.
|
/// Gmail initial sync now downloads all messages inside the selected cutoff window.
|
||||||
/// Uses Gmail API's Metadata format which includes headers, labels, and snippet but NOT full message body.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override uint InitialMessageDownloadCountPerFolder => 1500;
|
public override uint InitialMessageDownloadCountPerFolder => 1500;
|
||||||
|
|
||||||
@@ -304,13 +303,18 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Performs initial synchronization by downloading messages per-folder.
|
/// Performs initial synchronization by downloading messages per-folder.
|
||||||
/// Each folder gets up to 1500 messages, but we track already downloaded message IDs globally
|
/// Messages are filtered by the account's configured initial synchronization cutoff date when present,
|
||||||
/// to avoid downloading the same message multiple times (Gmail messages can have multiple labels).
|
/// and duplicates are avoided globally because Gmail messages can have multiple labels.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task<List<string>> PerformInitialSyncAsync(CancellationToken cancellationToken)
|
private async Task<List<string>> PerformInitialSyncAsync(CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// Track all downloaded message IDs globally to avoid duplicate downloads
|
// Track all downloaded message IDs globally to avoid duplicate downloads
|
||||||
var downloadedMessageIds = new HashSet<string>();
|
var downloadedMessageIds = new HashSet<string>();
|
||||||
|
var referenceDateUtc = Account.CreatedAt ?? DateTime.UtcNow;
|
||||||
|
var initialSynchronizationCutoffDateUtc = Account.InitialSynchronizationRange.ToCutoffDateUtc(referenceDateUtc);
|
||||||
|
var queryText = initialSynchronizationCutoffDateUtc.HasValue
|
||||||
|
? $"after:{initialSynchronizationCutoffDateUtc.Value.ToUniversalTime():yyyy/MM/dd}"
|
||||||
|
: null;
|
||||||
|
|
||||||
_logger.Information("Performing initial sync for {Name} - downloading messages per folder", Account.Name);
|
_logger.Information("Performing initial sync for {Name} - downloading messages per folder", Account.Name);
|
||||||
|
|
||||||
@@ -337,7 +341,6 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
|
|
||||||
var folderDownloaded = 0;
|
var folderDownloaded = 0;
|
||||||
string pageToken = null;
|
string pageToken = null;
|
||||||
var remainingToDownload = (int)InitialMessageDownloadCountPerFolder;
|
|
||||||
|
|
||||||
do
|
do
|
||||||
{
|
{
|
||||||
@@ -345,8 +348,9 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
|
|
||||||
var request = _gmailService.Users.Messages.List("me");
|
var request = _gmailService.Users.Messages.List("me");
|
||||||
request.LabelIds = new Google.Apis.Util.Repeatable<string>(new[] { folder.RemoteFolderId });
|
request.LabelIds = new Google.Apis.Util.Repeatable<string>(new[] { folder.RemoteFolderId });
|
||||||
request.MaxResults = Math.Min(remainingToDownload, 500); // API max is 500
|
request.MaxResults = 500; // API max is 500
|
||||||
request.PageToken = pageToken;
|
request.PageToken = pageToken;
|
||||||
|
request.Q = queryText;
|
||||||
|
|
||||||
var response = await request.ExecuteAsync(cancellationToken);
|
var response = await request.ExecuteAsync(cancellationToken);
|
||||||
|
|
||||||
@@ -373,19 +377,12 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
|||||||
totalMessagesDownloaded += newMessageIds.Count;
|
totalMessagesDownloaded += newMessageIds.Count;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Count all messages (including duplicates) toward the folder limit
|
|
||||||
remainingToDownload -= response.Messages.Count;
|
|
||||||
|
|
||||||
_logger.Debug("Folder {FolderName}: Downloaded {New} new messages ({Total} total in folder)",
|
_logger.Debug("Folder {FolderName}: Downloaded {New} new messages ({Total} total in folder)",
|
||||||
folder.FolderName, newMessageIds.Count, folderDownloaded);
|
folder.FolderName, newMessageIds.Count, folderDownloaded);
|
||||||
}
|
}
|
||||||
|
|
||||||
pageToken = response.NextPageToken;
|
pageToken = response.NextPageToken;
|
||||||
|
|
||||||
// Stop if we've processed enough messages for this folder or no more pages
|
|
||||||
if (remainingToDownload <= 0 || string.IsNullOrEmpty(pageToken))
|
|
||||||
break;
|
|
||||||
|
|
||||||
} while (!string.IsNullOrEmpty(pageToken));
|
} while (!string.IsNullOrEmpty(pageToken));
|
||||||
|
|
||||||
_logger.Information("Folder {FolderName}: Downloaded {Count} messages", folder.FolderName, folderDownloaded);
|
_logger.Information("Folder {FolderName}: Downloaded {Count} messages", folder.FolderName, folderDownloaded);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ using MailKit.Search;
|
|||||||
using MoreLinq;
|
using MoreLinq;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
|
using Wino.Core.Domain.Extensions;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Domain.Models.MailItem;
|
using Wino.Core.Domain.Models.MailItem;
|
||||||
@@ -252,9 +253,20 @@ public class UnifiedImapSynchronizer
|
|||||||
.OpenAsync(FolderAccess.ReadOnly, folder.UidValidity, localHighestModSeq, knownUidStructs, cancellationToken)
|
.OpenAsync(FolderAccess.ReadOnly, folder.UidValidity, localHighestModSeq, knownUidStructs, cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
|
|
||||||
var changedUids = await remoteFolder
|
IList<UniqueId> changedUids;
|
||||||
.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken)
|
|
||||||
.ConfigureAwait(false);
|
if (folder.HighestModeSeq == 0)
|
||||||
|
{
|
||||||
|
changedUids = await remoteFolder
|
||||||
|
.SearchAsync(BuildInitialSyncQuery(synchronizer), cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
changedUids = await remoteFolder
|
||||||
|
.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, changedUids, synchronizer, cancellationToken).ConfigureAwait(false);
|
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, changedUids, synchronizer, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
@@ -308,25 +320,26 @@ public class UnifiedImapSynchronizer
|
|||||||
{
|
{
|
||||||
IList<UniqueId> changedUids;
|
IList<UniqueId> changedUids;
|
||||||
|
|
||||||
if (client.Capabilities.HasFlag(ImapCapabilities.Sort))
|
if (isInitialSync)
|
||||||
{
|
{
|
||||||
changedUids = await remoteFolder
|
changedUids = await remoteFolder
|
||||||
.SortAsync(SearchQuery.ChangedSince(localHighestModSeq), [OrderBy.ReverseDate], cancellationToken)
|
.SearchAsync(BuildInitialSyncQuery(synchronizer), cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
changedUids = await remoteFolder
|
if (client.Capabilities.HasFlag(ImapCapabilities.Sort))
|
||||||
.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken)
|
{
|
||||||
.ConfigureAwait(false);
|
changedUids = await remoteFolder
|
||||||
}
|
.SortAsync(SearchQuery.ChangedSince(localHighestModSeq), [OrderBy.ReverseDate], cancellationToken)
|
||||||
|
.ConfigureAwait(false);
|
||||||
if (isInitialSync)
|
}
|
||||||
{
|
else
|
||||||
changedUids = changedUids
|
{
|
||||||
.OrderByDescending(a => a.Id)
|
changedUids = await remoteFolder
|
||||||
.Take((int)synchronizer.InitialMessageDownloadCountPerFolder)
|
.SearchAsync(SearchQuery.ChangedSince(localHighestModSeq), cancellationToken)
|
||||||
.ToList();
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, changedUids, synchronizer, cancellationToken).ConfigureAwait(false);
|
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, changedUids, synchronizer, cancellationToken).ConfigureAwait(false);
|
||||||
@@ -367,15 +380,12 @@ public class UnifiedImapSynchronizer
|
|||||||
|
|
||||||
if (folder.HighestKnownUid == 0)
|
if (folder.HighestKnownUid == 0)
|
||||||
{
|
{
|
||||||
var remoteUids = await remoteFolder.SearchAsync(SearchQuery.All, cancellationToken).ConfigureAwait(false);
|
var initialUids = await remoteFolder
|
||||||
|
.SearchAsync(BuildInitialSyncQuery(synchronizer), cancellationToken)
|
||||||
var initialUids = remoteUids
|
.ConfigureAwait(false);
|
||||||
.OrderByDescending(a => a.Id)
|
|
||||||
.Take((int)synchronizer.InitialMessageDownloadCountPerFolder)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, initialUids, synchronizer, cancellationToken).ConfigureAwait(false);
|
downloadedMessageIds = await DownloadMessagesByUidsAsync(client, remoteFolder, folder, initialUids, synchronizer, cancellationToken).ConfigureAwait(false);
|
||||||
UpdateHighestKnownUid(folder, remoteFolder, remoteUids.Select(a => a.Id));
|
UpdateHighestKnownUid(folder, remoteFolder, initialUids.Select(a => a.Id));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -410,6 +420,22 @@ public class UnifiedImapSynchronizer
|
|||||||
|
|
||||||
#region Shared Helpers
|
#region Shared Helpers
|
||||||
|
|
||||||
|
private static SearchQuery BuildInitialSyncQuery(IImapSynchronizer synchronizer)
|
||||||
|
{
|
||||||
|
if (synchronizer is IBaseSynchronizer { Account: { } account })
|
||||||
|
{
|
||||||
|
var referenceDateUtc = account.CreatedAt ?? DateTime.UtcNow;
|
||||||
|
var cutoffDateUtc = account.InitialSynchronizationRange.ToCutoffDateUtc(referenceDateUtc);
|
||||||
|
|
||||||
|
if (cutoffDateUtc.HasValue)
|
||||||
|
{
|
||||||
|
return SearchQuery.DeliveredAfter(cutoffDateUtc.Value.ToUniversalTime().Date);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SearchQuery.All;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task EnsureUidValidityStateAsync(MailItemFolder folder, IMailFolder remoteFolder)
|
private async Task EnsureUidValidityStateAsync(MailItemFolder folder, IMailFolder remoteFolder)
|
||||||
{
|
{
|
||||||
if (folder.UidValidity != 0 && remoteFolder.UidValidity != folder.UidValidity)
|
if (folder.UidValidity != 0 && remoteFolder.UidValidity != folder.UidValidity)
|
||||||
|
|||||||
@@ -55,14 +55,14 @@ public partial class OutlookSynchronizerJsonContext : JsonSerializerContext;
|
|||||||
///
|
///
|
||||||
/// SYNCHRONIZATION STRATEGY:
|
/// SYNCHRONIZATION STRATEGY:
|
||||||
/// - Uses delta API for both initial and incremental sync
|
/// - Uses delta API for both initial and incremental sync
|
||||||
/// - Initial sync: Downloads last 30 days of emails with metadata only
|
/// - Initial sync: Downloads messages using the account's configured cutoff date with metadata only
|
||||||
/// - Incremental sync: Uses delta token to get only changes since last sync
|
/// - Incremental sync: Uses delta token to get only changes since last sync
|
||||||
/// - Messages are downloaded with metadata only (no MIME content during sync)
|
/// - Messages are downloaded with metadata only (no MIME content during sync)
|
||||||
/// - MIME files are downloaded on-demand when user explicitly reads a message
|
/// - MIME files are downloaded on-demand when user explicitly reads a message
|
||||||
///
|
///
|
||||||
/// Key implementation details:
|
/// Key implementation details:
|
||||||
/// - SynchronizeFolderAsync: Main entry point for per-folder synchronization
|
/// - SynchronizeFolderAsync: Main entry point for per-folder synchronization
|
||||||
/// - DownloadMailsForInitialSyncAsync: Downloads last 30 days using delta API with filter
|
/// - DownloadMailsForInitialSyncAsync: Downloads messages using delta API with an optional cutoff filter
|
||||||
/// - ProcessDeltaChangesAsync: Processes incremental changes using delta token
|
/// - ProcessDeltaChangesAsync: Processes incremental changes using delta token
|
||||||
/// - DownloadMessageMetadataBatchAsync: Downloads metadata in batches using Graph batch API
|
/// - DownloadMessageMetadataBatchAsync: Downloads metadata in batches using Graph batch API
|
||||||
/// - CreateMailCopyFromMessageAsync: Creates MailCopy from Message metadata
|
/// - CreateMailCopyFromMessageAsync: Creates MailCopy from Message metadata
|
||||||
@@ -343,9 +343,9 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
// Check if we have a delta token
|
// Check if we have a delta token
|
||||||
if (string.IsNullOrEmpty(folder.DeltaToken))
|
if (string.IsNullOrEmpty(folder.DeltaToken))
|
||||||
{
|
{
|
||||||
_logger.Debug("No delta token for folder {FolderName}. Starting initial sync (last 30 days).", folder.FolderName);
|
_logger.Debug("No delta token for folder {FolderName}. Starting initial sync.", folder.FolderName);
|
||||||
|
|
||||||
// Download mails for initial sync (last 30 days)
|
// Download mails for initial sync using the account's configured cutoff date.
|
||||||
await DownloadMailsForInitialSyncAsync(folder, downloadedMessageIds, cancellationToken).ConfigureAwait(false);
|
await DownloadMailsForInitialSyncAsync(folder, downloadedMessageIds, cancellationToken).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -367,27 +367,37 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Downloads mails for initial synchronization using Delta API with 30-day filter.
|
/// Downloads mails for initial synchronization using Delta API with the account's configured cutoff date.
|
||||||
/// Downloads metadata only (no MIME content) for messages received in the last 30 days.
|
/// Downloads metadata only (no MIME content) for messages received after that date.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task DownloadMailsForInitialSyncAsync(MailItemFolder folder, List<string> downloadedMessageIds, CancellationToken cancellationToken)
|
private async Task DownloadMailsForInitialSyncAsync(MailItemFolder folder, List<string> downloadedMessageIds, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
_logger.Debug("Starting initial mail download for folder {FolderName} (last 6 months)", folder.FolderName);
|
_logger.Debug("Starting initial mail download for folder {FolderName}", folder.FolderName);
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
// Calculate date 6 months ago
|
var referenceDateUtc = Account.CreatedAt ?? DateTime.UtcNow;
|
||||||
var sixMonthsAgo = DateTime.UtcNow.AddMonths(-6);
|
var initialSynchronizationCutoffDateUtc = Account.InitialSynchronizationRange.ToCutoffDateUtc(referenceDateUtc);
|
||||||
var filterDate = sixMonthsAgo.ToString("yyyy-MM-ddTHH:mm:ssZ");
|
var filterDate = initialSynchronizationCutoffDateUtc?.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ");
|
||||||
|
|
||||||
_logger.Information("Downloading messages received after {FilterDate} for folder {FolderName}", filterDate, folder.FolderName);
|
if (filterDate != null)
|
||||||
|
{
|
||||||
|
_logger.Information("Downloading messages received after {FilterDate} for folder {FolderName}", filterDate, folder.FolderName);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_logger.Information("Downloading all available messages for folder {FolderName}", folder.FolderName);
|
||||||
|
}
|
||||||
|
|
||||||
// Use Delta API with receivedDateTime filter for last 6 months
|
|
||||||
var messageCollectionPage = await _graphClient.Me.MailFolders[folder.RemoteFolderId].Messages.Delta.GetAsDeltaGetResponseAsync((config) =>
|
var messageCollectionPage = await _graphClient.Me.MailFolders[folder.RemoteFolderId].Messages.Delta.GetAsDeltaGetResponseAsync((config) =>
|
||||||
{
|
{
|
||||||
config.QueryParameters.Select = outlookMessageSelectParameters;
|
config.QueryParameters.Select = outlookMessageSelectParameters;
|
||||||
config.QueryParameters.Orderby = ["receivedDateTime desc"];
|
config.QueryParameters.Orderby = ["receivedDateTime desc"];
|
||||||
config.QueryParameters.Filter = $"receivedDateTime ge {filterDate}";
|
|
||||||
|
if (filterDate != null)
|
||||||
|
{
|
||||||
|
config.QueryParameters.Filter = $"receivedDateTime ge {filterDate}";
|
||||||
|
}
|
||||||
}, cancellationToken).ConfigureAwait(false);
|
}, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
var totalProcessed = 0;
|
var totalProcessed = 0;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
@@ -12,6 +13,7 @@ using Wino.Core.Domain.Entities.Calendar;
|
|||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Entities.Shared;
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
|
using Wino.Core.Domain.Extensions;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
using Wino.Core.Domain.Models.Accounts;
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Core.Domain.Models.Folders;
|
using Wino.Core.Domain.Models.Folders;
|
||||||
@@ -101,6 +103,14 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
|||||||
? $"ms-appx:///Assets/Providers/{Account.SpecialImapProvider}.png"
|
? $"ms-appx:///Assets/Providers/{Account.SpecialImapProvider}.png"
|
||||||
: $"ms-appx:///Assets/Providers/{Account?.ProviderType}.png";
|
: $"ms-appx:///Assets/Providers/{Account?.ProviderType}.png";
|
||||||
public string Address => Account?.Address ?? string.Empty;
|
public string Address => Account?.Address ?? string.Empty;
|
||||||
|
public bool IsInitialSynchronizationSummaryVisible => Account?.CreatedAt.HasValue == true && Account.InitialSynchronizationRange != InitialSynchronizationRange.Everything;
|
||||||
|
public string InitialSynchronizationSummary => Account?.CreatedAt is not DateTime createdAtUtc
|
||||||
|
? string.Empty
|
||||||
|
: Account.InitialSynchronizationRange.ToCutoffDateUtc(createdAtUtc) is not DateTime cutoffDateUtc
|
||||||
|
? string.Empty
|
||||||
|
: string.Format(
|
||||||
|
Translator.AccountDetailsPage_InitialSynchronization_Description,
|
||||||
|
cutoffDateUtc.ToLocalTime().ToString("D", CultureInfo.CurrentUICulture));
|
||||||
|
|
||||||
public List<ImapAuthenticationMethodModel> AvailableAuthenticationMethods { get; } =
|
public List<ImapAuthenticationMethodModel> AvailableAuthenticationMethods { get; } =
|
||||||
[
|
[
|
||||||
@@ -363,6 +373,8 @@ public partial class AccountDetailsPageViewModel : MailBaseViewModel
|
|||||||
OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount));
|
OnPropertyChanged(nameof(IsFocusedInboxSupportedForAccount));
|
||||||
OnPropertyChanged(nameof(ProviderIconPath));
|
OnPropertyChanged(nameof(ProviderIconPath));
|
||||||
OnPropertyChanged(nameof(Address));
|
OnPropertyChanged(nameof(Address));
|
||||||
|
OnPropertyChanged(nameof(IsInitialSynchronizationSummaryVisible));
|
||||||
|
OnPropertyChanged(nameof(InitialSynchronizationSummary));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async void OnPropertyChanged(PropertyChangedEventArgs e)
|
protected override async void OnPropertyChanged(PropertyChangedEventArgs e)
|
||||||
|
|||||||
@@ -170,6 +170,7 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
CustomServerInformation customServerInformation = null;
|
CustomServerInformation customServerInformation = null;
|
||||||
|
var accountCreatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
// Build account in memory
|
// Build account in memory
|
||||||
_createdAccount = new MailAccount
|
_createdAccount = new MailAccount
|
||||||
@@ -179,6 +180,8 @@ public partial class AccountSetupProgressPageViewModel : MailBaseViewModel
|
|||||||
Name = WizardContext.AccountName,
|
Name = WizardContext.AccountName,
|
||||||
SpecialImapProvider = WizardContext.SelectedProvider.SpecialImapProvider,
|
SpecialImapProvider = WizardContext.SelectedProvider.SpecialImapProvider,
|
||||||
AccountColorHex = WizardContext.AccountColorHex,
|
AccountColorHex = WizardContext.AccountColorHex,
|
||||||
|
CreatedAt = accountCreatedAt,
|
||||||
|
InitialSynchronizationRange = WizardContext.SelectedInitialSynchronizationRange,
|
||||||
IsCalendarAccessGranted = true
|
IsCalendarAccessGranted = true
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,9 @@ public partial class WelcomeWizardContext : ObservableObject
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial string AccountColorHex { get; set; }
|
public partial string AccountColorHex { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
public partial InitialSynchronizationRange SelectedInitialSynchronizationRange { get; set; } = InitialSynchronizationRange.SixMonths;
|
||||||
|
|
||||||
// Special IMAP fields (iCloud/Yahoo)
|
// Special IMAP fields (iCloud/Yahoo)
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial string DisplayName { get; set; }
|
public partial string DisplayName { get; set; }
|
||||||
@@ -62,7 +65,8 @@ public partial class WelcomeWizardContext : ObservableObject
|
|||||||
SelectedProvider.Type,
|
SelectedProvider.Type,
|
||||||
AccountName,
|
AccountName,
|
||||||
BuildSpecialImapProviderDetails(),
|
BuildSpecialImapProviderDetails(),
|
||||||
AccountColorHex);
|
AccountColorHex,
|
||||||
|
SelectedInitialSynchronizationRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Reset()
|
public void Reset()
|
||||||
@@ -70,6 +74,7 @@ public partial class WelcomeWizardContext : ObservableObject
|
|||||||
SelectedProvider = null;
|
SelectedProvider = null;
|
||||||
AccountName = null;
|
AccountName = null;
|
||||||
AccountColorHex = null;
|
AccountColorHex = null;
|
||||||
|
SelectedInitialSynchronizationRange = InitialSynchronizationRange.SixMonths;
|
||||||
DisplayName = null;
|
DisplayName = null;
|
||||||
EmailAddress = null;
|
EmailAddress = null;
|
||||||
AppSpecificPassword = null;
|
AppSpecificPassword = null;
|
||||||
|
|||||||
@@ -997,7 +997,12 @@ public partial class ImapCalDavSettingsPageViewModel : MailBaseViewModel
|
|||||||
SpecialImapProvider = _editingSpecialImapProvider,
|
SpecialImapProvider = _editingSpecialImapProvider,
|
||||||
IsCalendarAccessGranted = mode != ImapCalendarSupportMode.Disabled
|
IsCalendarAccessGranted = mode != ImapCalendarSupportMode.Disabled
|
||||||
},
|
},
|
||||||
new AccountCreationDialogResult(MailProviderType.IMAP4, DisplayName.Trim(), providerDetails, string.Empty));
|
new AccountCreationDialogResult(
|
||||||
|
MailProviderType.IMAP4,
|
||||||
|
DisplayName.Trim(),
|
||||||
|
providerDetails,
|
||||||
|
string.Empty,
|
||||||
|
_wizardContext.SelectedInitialSynchronizationRange));
|
||||||
|
|
||||||
if (serverInformation == null)
|
if (serverInformation == null)
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ using CommunityToolkit.Mvvm.Messaging;
|
|||||||
using Wino.Core.Domain;
|
using Wino.Core.Domain;
|
||||||
using Wino.Core.Domain.Enums;
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Domain.Models.Accounts;
|
||||||
using Wino.Core.Domain.Models.Navigation;
|
using Wino.Core.Domain.Models.Navigation;
|
||||||
using Wino.Core.ViewModels.Data;
|
using Wino.Core.ViewModels.Data;
|
||||||
using Wino.Mail.ViewModels.Data;
|
using Wino.Mail.ViewModels.Data;
|
||||||
@@ -22,13 +23,26 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
|
|||||||
|
|
||||||
public List<IProviderDetail> Providers { get; private set; } = [];
|
public List<IProviderDetail> Providers { get; private set; } = [];
|
||||||
public List<AppColorViewModel> AvailableColors { get; private set; } = [];
|
public List<AppColorViewModel> AvailableColors { get; private set; } = [];
|
||||||
|
public List<InitialSynchronizationRangeOption> InitialSynchronizationRanges { get; } =
|
||||||
|
[
|
||||||
|
new(InitialSynchronizationRange.ThreeMonths, Translator.AccountCreation_InitialSynchronization_3Months),
|
||||||
|
new(InitialSynchronizationRange.SixMonths, Translator.AccountCreation_InitialSynchronization_6Months),
|
||||||
|
new(InitialSynchronizationRange.NineMonths, Translator.AccountCreation_InitialSynchronization_9Months),
|
||||||
|
new(InitialSynchronizationRange.OneYear, Translator.AccountCreation_InitialSynchronization_Year),
|
||||||
|
new(InitialSynchronizationRange.Everything, Translator.AccountCreation_InitialSynchronization_Everything)
|
||||||
|
];
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial IProviderDetail SelectedProvider { get; set; }
|
public partial IProviderDetail SelectedProvider { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(IsColorSelected))]
|
||||||
public partial AppColorViewModel SelectedColor { get; set; }
|
public partial AppColorViewModel SelectedColor { get; set; }
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(IsInitialSynchronizationWarningVisible))]
|
||||||
|
public partial InitialSynchronizationRangeOption SelectedInitialSynchronizationRange { get; set; }
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
public partial string AccountName { get; set; }
|
public partial string AccountName { get; set; }
|
||||||
|
|
||||||
@@ -36,6 +50,7 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
|
|||||||
public partial bool CanProceed { get; set; }
|
public partial bool CanProceed { get; set; }
|
||||||
|
|
||||||
public bool IsColorSelected => SelectedColor != null;
|
public bool IsColorSelected => SelectedColor != null;
|
||||||
|
public bool IsInitialSynchronizationWarningVisible => SelectedInitialSynchronizationRange?.IsEverything == true;
|
||||||
|
|
||||||
public ProviderSelectionPageViewModel(
|
public ProviderSelectionPageViewModel(
|
||||||
IProviderService providerService,
|
IProviderService providerService,
|
||||||
@@ -45,6 +60,7 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
|
|||||||
_providerService = providerService;
|
_providerService = providerService;
|
||||||
_themeService = themeService;
|
_themeService = themeService;
|
||||||
WizardContext = wizardContext;
|
WizardContext = wizardContext;
|
||||||
|
SelectedInitialSynchronizationRange = InitialSynchronizationRanges.First(option => option.Range == InitialSynchronizationRange.SixMonths);
|
||||||
}
|
}
|
||||||
|
|
||||||
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
public override void OnNavigatedTo(NavigationMode mode, object parameters)
|
||||||
@@ -56,6 +72,10 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
|
|||||||
.Select(hex => new AppColorViewModel(hex))
|
.Select(hex => new AppColorViewModel(hex))
|
||||||
.ToList();
|
.ToList();
|
||||||
|
|
||||||
|
SelectedInitialSynchronizationRange = InitialSynchronizationRanges
|
||||||
|
.FirstOrDefault(option => option.Range == WizardContext.SelectedInitialSynchronizationRange)
|
||||||
|
?? InitialSynchronizationRanges.First(option => option.Range == InitialSynchronizationRange.SixMonths);
|
||||||
|
|
||||||
// Restore from wizard context if navigating back
|
// Restore from wizard context if navigating back
|
||||||
if (WizardContext.SelectedProvider != null)
|
if (WizardContext.SelectedProvider != null)
|
||||||
{
|
{
|
||||||
@@ -71,9 +91,12 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
|
|||||||
Validate();
|
Validate();
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnSelectedProviderChanged(IProviderDetail value) => Validate();
|
partial void OnSelectedProviderChanged(IProviderDetail value)
|
||||||
|
{
|
||||||
|
Validate();
|
||||||
|
}
|
||||||
|
|
||||||
partial void OnAccountNameChanged(string value) => Validate();
|
partial void OnAccountNameChanged(string value) => Validate();
|
||||||
partial void OnSelectedColorChanged(AppColorViewModel value) => OnPropertyChanged(nameof(IsColorSelected));
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void ClearColor() => SelectedColor = null;
|
private void ClearColor() => SelectedColor = null;
|
||||||
@@ -92,6 +115,7 @@ public partial class ProviderSelectionPageViewModel : MailBaseViewModel
|
|||||||
WizardContext.SelectedProvider = SelectedProvider;
|
WizardContext.SelectedProvider = SelectedProvider;
|
||||||
WizardContext.AccountName = AccountName?.Trim();
|
WizardContext.AccountName = AccountName?.Trim();
|
||||||
WizardContext.AccountColorHex = SelectedColor?.Hex ?? string.Empty;
|
WizardContext.AccountColorHex = SelectedColor?.Hex ?? string.Empty;
|
||||||
|
WizardContext.SelectedInitialSynchronizationRange = SelectedInitialSynchronizationRange?.Range ?? InitialSynchronizationRange.SixMonths;
|
||||||
|
|
||||||
if (WizardContext.IsGenericImap)
|
if (WizardContext.IsGenericImap)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -55,6 +55,7 @@
|
|||||||
<Grid MinWidth="400" RowSpacing="12">
|
<Grid MinWidth="400" RowSpacing="12">
|
||||||
<Grid Visibility="{x:Bind IsProviderSelectionVisible, Mode=OneWay}">
|
<Grid Visibility="{x:Bind IsProviderSelectionVisible, Mode=OneWay}">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="*" />
|
<RowDefinition Height="*" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
@@ -96,6 +97,48 @@
|
|||||||
|
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
x:Name="InitialSynchronizationPanel"
|
||||||
|
Grid.Row="1"
|
||||||
|
Margin="0,12,0,0"
|
||||||
|
Padding="12"
|
||||||
|
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||||
|
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="8"
|
||||||
|
Visibility="Collapsed">
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<StackPanel Spacing="2">
|
||||||
|
<TextBlock FontWeight="SemiBold" Text="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_Title}" />
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_Description}"
|
||||||
|
TextWrapping="WrapWholeWords" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<ComboBox
|
||||||
|
x:Name="InitialSynchronizationComboBox"
|
||||||
|
HorizontalAlignment="Stretch"
|
||||||
|
ItemsSource="{x:Bind InitialSynchronizationRanges, Mode=OneWay}"
|
||||||
|
SelectionChanged="InitialSynchronizationSelectionChanged">
|
||||||
|
<ComboBox.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="accounts:InitialSynchronizationRangeOption">
|
||||||
|
<TextBlock Text="{x:Bind DisplayText}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ComboBox.ItemTemplate>
|
||||||
|
</ComboBox>
|
||||||
|
|
||||||
|
<muxc:InfoBar
|
||||||
|
x:Name="InitialSynchronizationWarningBar"
|
||||||
|
IsOpen="True"
|
||||||
|
Message="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_EverythingWarning}"
|
||||||
|
Severity="Warning"
|
||||||
|
Title="{x:Bind domain:Translator.GeneralTitle_Warning}"
|
||||||
|
Visibility="Collapsed" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
|
||||||
<ListView
|
<ListView
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ namespace Wino.Mail.WinUI.Dialogs;
|
|||||||
|
|
||||||
public sealed partial class NewAccountDialog : ContentDialog
|
public sealed partial class NewAccountDialog : ContentDialog
|
||||||
{
|
{
|
||||||
private readonly Dictionary<SpecialImapProvider, string> helpingLinks = new Dictionary<SpecialImapProvider, string>()
|
private readonly Dictionary<SpecialImapProvider, string> helpingLinks = new()
|
||||||
{
|
{
|
||||||
{ SpecialImapProvider.iCloud, "https://support.apple.com/en-us/102654" },
|
{ SpecialImapProvider.iCloud, "https://support.apple.com/en-us/102654" },
|
||||||
{ SpecialImapProvider.Yahoo, "http://help.yahoo.com/kb/SLN15241.html" },
|
{ SpecialImapProvider.Yahoo, "http://help.yahoo.com/kb/SLN15241.html" },
|
||||||
@@ -27,7 +27,6 @@ public sealed partial class NewAccountDialog : ContentDialog
|
|||||||
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(AppColorViewModel), typeof(NewAccountDialog), new PropertyMetadata(null, new PropertyChangedCallback(OnSelectedColorChanged)));
|
public static readonly DependencyProperty SelectedColorProperty = DependencyProperty.Register(nameof(SelectedColor), typeof(AppColorViewModel), typeof(NewAccountDialog), new PropertyMetadata(null, new PropertyChangedCallback(OnSelectedColorChanged)));
|
||||||
public static readonly DependencyProperty SelectedCalendarModeIndexProperty = DependencyProperty.Register(nameof(SelectedCalendarModeIndex), typeof(int), typeof(NewAccountDialog), new PropertyMetadata(0));
|
public static readonly DependencyProperty SelectedCalendarModeIndexProperty = DependencyProperty.Register(nameof(SelectedCalendarModeIndex), typeof(int), typeof(NewAccountDialog), new PropertyMetadata(0));
|
||||||
|
|
||||||
|
|
||||||
public AppColorViewModel? SelectedColor
|
public AppColorViewModel? SelectedColor
|
||||||
{
|
{
|
||||||
get { return (AppColorViewModel?)GetValue(SelectedColorProperty); }
|
get { return (AppColorViewModel?)GetValue(SelectedColorProperty); }
|
||||||
@@ -49,7 +48,6 @@ public sealed partial class NewAccountDialog : ContentDialog
|
|||||||
set { SetValue(SelectedMailProviderProperty, value); }
|
set { SetValue(SelectedMailProviderProperty, value); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public bool IsProviderSelectionVisible
|
public bool IsProviderSelectionVisible
|
||||||
{
|
{
|
||||||
get { return (bool)GetValue(IsProviderSelectionVisibleProperty); }
|
get { return (bool)GetValue(IsProviderSelectionVisibleProperty); }
|
||||||
@@ -63,10 +61,16 @@ public sealed partial class NewAccountDialog : ContentDialog
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List of available mail providers for now.
|
// List of available mail providers for now.
|
||||||
|
|
||||||
public List<IProviderDetail> Providers { get; set; } = [];
|
public List<IProviderDetail> Providers { get; set; } = [];
|
||||||
|
|
||||||
public List<AppColorViewModel> AvailableColors { get; set; } = [];
|
public List<AppColorViewModel> AvailableColors { get; set; } = [];
|
||||||
|
public List<InitialSynchronizationRangeOption> InitialSynchronizationRanges { get; } =
|
||||||
|
[
|
||||||
|
new(InitialSynchronizationRange.ThreeMonths, Translator.AccountCreation_InitialSynchronization_3Months),
|
||||||
|
new(InitialSynchronizationRange.SixMonths, Translator.AccountCreation_InitialSynchronization_6Months),
|
||||||
|
new(InitialSynchronizationRange.NineMonths, Translator.AccountCreation_InitialSynchronization_9Months),
|
||||||
|
new(InitialSynchronizationRange.OneYear, Translator.AccountCreation_InitialSynchronization_Year),
|
||||||
|
new(InitialSynchronizationRange.Everything, Translator.AccountCreation_InitialSynchronization_Everything)
|
||||||
|
];
|
||||||
public List<string> CalendarModeOptions { get; } =
|
public List<string> CalendarModeOptions { get; } =
|
||||||
[
|
[
|
||||||
Translator.ImapCalDavSettingsPage_CalendarModeCalDav,
|
Translator.ImapCalDavSettingsPage_CalendarModeCalDav,
|
||||||
@@ -74,7 +78,6 @@ public sealed partial class NewAccountDialog : ContentDialog
|
|||||||
Translator.ImapCalDavSettingsPage_CalendarModeDisabled
|
Translator.ImapCalDavSettingsPage_CalendarModeDisabled
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
public AccountCreationDialogResult? Result = null;
|
public AccountCreationDialogResult? Result = null;
|
||||||
|
|
||||||
public NewAccountDialog()
|
public NewAccountDialog()
|
||||||
@@ -85,6 +88,8 @@ public sealed partial class NewAccountDialog : ContentDialog
|
|||||||
AvailableColors = themeService.Select(a => new AppColorViewModel(a)).ToList();
|
AvailableColors = themeService.Select(a => new AppColorViewModel(a)).ToList();
|
||||||
|
|
||||||
UpdateSelectedColor();
|
UpdateSelectedColor();
|
||||||
|
InitialSynchronizationComboBox.SelectedItem = InitialSynchronizationRanges.First(option => option.Range == InitialSynchronizationRange.SixMonths);
|
||||||
|
UpdateInitialSynchronizationState();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void OnSelectedProviderChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
private static void OnSelectedProviderChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
|
||||||
@@ -105,6 +110,19 @@ public sealed partial class NewAccountDialog : ContentDialog
|
|||||||
SelectedColorEllipse.Fill = SelectedColor == null ? null : XamlHelpers.GetSolidColorBrushFromHex(SelectedColor.Hex);
|
SelectedColorEllipse.Fill = SelectedColor == null ? null : XamlHelpers.GetSolidColorBrushFromHex(SelectedColor.Hex);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void UpdateInitialSynchronizationState()
|
||||||
|
{
|
||||||
|
InitialSynchronizationPanel.Visibility = SelectedMailProvider == null ? Visibility.Collapsed : Visibility.Visible;
|
||||||
|
var selectedOption = InitialSynchronizationComboBox.SelectedItem as InitialSynchronizationRangeOption;
|
||||||
|
InitialSynchronizationWarningBar.Visibility = selectedOption?.IsEverything == true ? Visibility.Visible : Visibility.Collapsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private InitialSynchronizationRange GetInitialSynchronizationRange()
|
||||||
|
{
|
||||||
|
var selectedRange = (InitialSynchronizationComboBox.SelectedItem as InitialSynchronizationRangeOption)?.Range
|
||||||
|
?? InitialSynchronizationRange.SixMonths;
|
||||||
|
return selectedRange;
|
||||||
|
}
|
||||||
|
|
||||||
private void CancelClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
private void CancelClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
|
||||||
{
|
{
|
||||||
@@ -116,9 +134,11 @@ public sealed partial class NewAccountDialog : ContentDialog
|
|||||||
if (SelectedMailProvider == null)
|
if (SelectedMailProvider == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
var initialSynchronizationRange = GetInitialSynchronizationRange();
|
||||||
|
|
||||||
if (IsSpecialImapServerPartVisible)
|
if (IsSpecialImapServerPartVisible)
|
||||||
{
|
{
|
||||||
// Special imap detail input.
|
// Special IMAP detail input.
|
||||||
var calendarSupportMode = SelectedCalendarModeIndex switch
|
var calendarSupportMode = SelectedCalendarModeIndex switch
|
||||||
{
|
{
|
||||||
1 => ImapCalendarSupportMode.LocalOnly,
|
1 => ImapCalendarSupportMode.LocalOnly,
|
||||||
@@ -132,7 +152,12 @@ public sealed partial class NewAccountDialog : ContentDialog
|
|||||||
DisplayNameTextBox.Text.Trim(),
|
DisplayNameTextBox.Text.Trim(),
|
||||||
SelectedMailProvider.SpecialImapProvider,
|
SelectedMailProvider.SpecialImapProvider,
|
||||||
calendarSupportMode);
|
calendarSupportMode);
|
||||||
Result = new AccountCreationDialogResult(SelectedMailProvider.Type, AccountNameTextbox.Text.Trim(), details, SelectedColor?.Hex ?? string.Empty);
|
Result = new AccountCreationDialogResult(
|
||||||
|
SelectedMailProvider.Type,
|
||||||
|
AccountNameTextbox.Text.Trim(),
|
||||||
|
details,
|
||||||
|
SelectedColor?.Hex ?? string.Empty,
|
||||||
|
initialSynchronizationRange);
|
||||||
Hide();
|
Hide();
|
||||||
|
|
||||||
return;
|
return;
|
||||||
@@ -140,11 +165,11 @@ public sealed partial class NewAccountDialog : ContentDialog
|
|||||||
|
|
||||||
Validate();
|
Validate();
|
||||||
|
|
||||||
if (IsSecondaryButtonEnabled)
|
if (IsPrimaryButtonEnabled)
|
||||||
{
|
{
|
||||||
if (SelectedMailProvider.SpecialImapProvider != SpecialImapProvider.None)
|
if (SelectedMailProvider.SpecialImapProvider != SpecialImapProvider.None)
|
||||||
{
|
{
|
||||||
// This step requires app-sepcific password login for some providers.
|
// This step requires app-specific password login for some providers.
|
||||||
args.Cancel = true;
|
args.Cancel = true;
|
||||||
|
|
||||||
IsProviderSelectionVisible = false;
|
IsProviderSelectionVisible = false;
|
||||||
@@ -154,7 +179,12 @@ public sealed partial class NewAccountDialog : ContentDialog
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Result = new AccountCreationDialogResult(SelectedMailProvider.Type, AccountNameTextbox.Text.Trim(), null, SelectedColor?.Hex ?? string.Empty);
|
Result = new AccountCreationDialogResult(
|
||||||
|
SelectedMailProvider.Type,
|
||||||
|
AccountNameTextbox.Text.Trim(),
|
||||||
|
null,
|
||||||
|
SelectedColor?.Hex ?? string.Empty,
|
||||||
|
initialSynchronizationRange);
|
||||||
Hide();
|
Hide();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -167,6 +197,7 @@ public sealed partial class NewAccountDialog : ContentDialog
|
|||||||
{
|
{
|
||||||
ValidateCreateButton();
|
ValidateCreateButton();
|
||||||
ValidateNames();
|
ValidateNames();
|
||||||
|
UpdateInitialSynchronizationState();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns whether we can create account or not.
|
// Returns whether we can create account or not.
|
||||||
@@ -199,6 +230,9 @@ public sealed partial class NewAccountDialog : ContentDialog
|
|||||||
|
|
||||||
private void ImapPasswordChanged(object sender, RoutedEventArgs e) => Validate();
|
private void ImapPasswordChanged(object sender, RoutedEventArgs e) => Validate();
|
||||||
|
|
||||||
|
private void InitialSynchronizationSelectionChanged(object sender, SelectionChangedEventArgs e)
|
||||||
|
=> UpdateInitialSynchronizationState();
|
||||||
|
|
||||||
private async void AppSpecificHelpButtonClicked(object sender, RoutedEventArgs e)
|
private async void AppSpecificHelpButtonClicked(object sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
if (SelectedMailProvider == null ||
|
if (SelectedMailProvider == null ||
|
||||||
|
|||||||
@@ -210,6 +210,13 @@
|
|||||||
</TransitionCollection>
|
</TransitionCollection>
|
||||||
</StackPanel.ChildrenTransitions>
|
</StackPanel.ChildrenTransitions>
|
||||||
|
|
||||||
|
<muxc:InfoBar
|
||||||
|
IsOpen="True"
|
||||||
|
Margin="0,0,0,8"
|
||||||
|
Message="{x:Bind ViewModel.InitialSynchronizationSummary, Mode=OneWay}"
|
||||||
|
Severity="Informational"
|
||||||
|
Title="{x:Bind domain:Translator.AccountDetailsPage_InitialSynchronization_Title}"
|
||||||
|
Visibility="{x:Bind ViewModel.IsInitialSynchronizationSummaryVisible, Mode=OneWay}" />
|
||||||
|
|
||||||
<controls:SettingsCard
|
<controls:SettingsCard
|
||||||
Command="{x:Bind ViewModel.EditAliasesCommand}"
|
Command="{x:Bind ViewModel.EditAliasesCommand}"
|
||||||
|
|||||||
@@ -10,15 +10,12 @@
|
|||||||
xmlns:helpers="using:Wino.Helpers"
|
xmlns:helpers="using:Wino.Helpers"
|
||||||
xmlns:interfaces="using:Wino.Core.Domain.Interfaces"
|
xmlns:interfaces="using:Wino.Core.Domain.Interfaces"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:muxc="using:Microsoft.UI.Xaml.Controls"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
|
|
||||||
<ScrollViewer
|
<ScrollViewer HorizontalAlignment="Center" VerticalScrollBarVisibility="Auto">
|
||||||
HorizontalAlignment="Center"
|
|
||||||
VerticalAlignment="Center"
|
|
||||||
VerticalScrollBarVisibility="Auto">
|
|
||||||
<StackPanel
|
<StackPanel
|
||||||
MaxWidth="480"
|
Margin="0,12"
|
||||||
Margin="0,24,0,24"
|
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
Spacing="20">
|
Spacing="20">
|
||||||
|
|
||||||
@@ -76,6 +73,50 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<Border
|
||||||
|
MaxWidth="600"
|
||||||
|
Padding="12"
|
||||||
|
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
|
||||||
|
BorderBrush="{ThemeResource CardStrokeColorDefaultBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
CornerRadius="8">
|
||||||
|
<StackPanel Spacing="10">
|
||||||
|
<StackPanel Spacing="2">
|
||||||
|
<TextBlock FontWeight="SemiBold" Text="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_Title}" />
|
||||||
|
<TextBlock
|
||||||
|
Foreground="{ThemeResource TextFillColorSecondaryBrush}"
|
||||||
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
|
Text="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_Description}"
|
||||||
|
TextWrapping="WrapWholeWords" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<ListView
|
||||||
|
HorizontalAlignment="Center"
|
||||||
|
HorizontalContentAlignment="Stretch"
|
||||||
|
ItemsSource="{x:Bind ViewModel.InitialSynchronizationRanges, Mode=OneWay}"
|
||||||
|
SelectedItem="{x:Bind ViewModel.SelectedInitialSynchronizationRange, Mode=TwoWay}">
|
||||||
|
<ListView.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<ItemsStackPanel Orientation="Horizontal" />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ListView.ItemsPanel>
|
||||||
|
<ListView.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="accounts:InitialSynchronizationRangeOption">
|
||||||
|
<TextBlock Text="{x:Bind DisplayText}" />
|
||||||
|
</DataTemplate>
|
||||||
|
</ListView.ItemTemplate>
|
||||||
|
</ListView>
|
||||||
|
|
||||||
|
<muxc:InfoBar
|
||||||
|
Title="{x:Bind domain:Translator.GeneralTitle_Warning}"
|
||||||
|
Margin="0,2,0,0"
|
||||||
|
IsOpen="True"
|
||||||
|
Message="{x:Bind domain:Translator.AccountCreation_InitialSynchronization_EverythingWarning}"
|
||||||
|
Severity="Warning"
|
||||||
|
Visibility="{x:Bind ViewModel.IsInitialSynchronizationWarningVisible, Mode=OneWay}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- Provider List -->
|
<!-- Provider List -->
|
||||||
<ItemsView
|
<ItemsView
|
||||||
HorizontalContentAlignment="Stretch"
|
HorizontalContentAlignment="Stretch"
|
||||||
|
|||||||
@@ -590,6 +590,11 @@ public class AccountService : BaseDatabaseService, IAccountService
|
|||||||
{
|
{
|
||||||
Guard.IsNotNull(account);
|
Guard.IsNotNull(account);
|
||||||
|
|
||||||
|
if (!account.CreatedAt.HasValue)
|
||||||
|
{
|
||||||
|
account.CreatedAt = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
var accountCount = await Connection.Table<MailAccount>().CountAsync();
|
var accountCount = await Connection.Table<MailAccount>().CountAsync();
|
||||||
|
|
||||||
// If there are no accounts before this one, set it as startup account.
|
// If there are no accounts before this one, set it as startup account.
|
||||||
|
|||||||
@@ -79,6 +79,22 @@ public class DatabaseService : IDatabaseService
|
|||||||
{
|
{
|
||||||
await EnsureKeyboardShortcutSchemaAsync().ConfigureAwait(false);
|
await EnsureKeyboardShortcutSchemaAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
var accountColumns = await Connection.GetTableInfoAsync(nameof(MailAccount)).ConfigureAwait(false);
|
||||||
|
|
||||||
|
if (!accountColumns.Any(c => c.Name == nameof(MailAccount.CreatedAt)))
|
||||||
|
{
|
||||||
|
await Connection
|
||||||
|
.ExecuteAsync($"ALTER TABLE {nameof(MailAccount)} ADD COLUMN {nameof(MailAccount.CreatedAt)} TEXT NULL")
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountColumns.Any(c => c.Name == nameof(MailAccount.InitialSynchronizationRange)))
|
||||||
|
{
|
||||||
|
await Connection
|
||||||
|
.ExecuteAsync($"ALTER TABLE {nameof(MailAccount)} ADD COLUMN {nameof(MailAccount.InitialSynchronizationRange)} INTEGER NOT NULL DEFAULT {(int)InitialSynchronizationRange.SixMonths}")
|
||||||
|
.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
var folderColumns = await Connection.GetTableInfoAsync(nameof(MailItemFolder)).ConfigureAwait(false);
|
var folderColumns = await Connection.GetTableInfoAsync(nameof(MailItemFolder)).ConfigureAwait(false);
|
||||||
|
|
||||||
if (!folderColumns.Any(c => c.Name == nameof(MailItemFolder.HighestKnownUid)))
|
if (!folderColumns.Any(c => c.Name == nameof(MailItemFolder.HighestKnownUid)))
|
||||||
|
|||||||
@@ -211,6 +211,8 @@ public sealed class WinoAccountDataSyncService : IWinoAccountDataSyncService
|
|||||||
SpecialImapProvider = (SpecialImapProvider)mailbox.SpecialImapProvider,
|
SpecialImapProvider = (SpecialImapProvider)mailbox.SpecialImapProvider,
|
||||||
AccountColorHex = mailbox.AccountColorHex?.Trim(),
|
AccountColorHex = mailbox.AccountColorHex?.Trim(),
|
||||||
Base64ProfilePictureData = string.Empty,
|
Base64ProfilePictureData = string.Empty,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
InitialSynchronizationRange = InitialSynchronizationRange.SixMonths,
|
||||||
IsCalendarAccessGranted = mailbox.IsCalendarAccessGranted,
|
IsCalendarAccessGranted = mailbox.IsCalendarAccessGranted,
|
||||||
SynchronizationDeltaIdentifier = string.Empty,
|
SynchronizationDeltaIdentifier = string.Empty,
|
||||||
CalendarSynchronizationDeltaIdentifier = string.Empty,
|
CalendarSynchronizationDeltaIdentifier = string.Empty,
|
||||||
|
|||||||
Reference in New Issue
Block a user