Contacts, thread animation and image preview control improvements.
This commit is contained in:
@@ -289,6 +289,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
var folders = await _gmailChangeProcessor.GetLocalFoldersAsync(Account.Id).ConfigureAwait(false);
|
||||
var syncableFolders = folders
|
||||
.Where(f => f.IsSynchronizationEnabled && f.RemoteFolderId != ServiceConstants.ARCHIVE_LABEL_ID)
|
||||
.OrderByDescending(f => f.SpecialFolderType == SpecialFolderType.Draft || f.RemoteFolderId == ServiceConstants.DRAFT_LABEL_ID)
|
||||
.ToList();
|
||||
|
||||
var totalFolders = syncableFolders.Count;
|
||||
@@ -328,8 +329,9 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
|
||||
if (newMessageIds.Count > 0)
|
||||
{
|
||||
// Download metadata in batches (no raw MIME during initial sync)
|
||||
await DownloadMessagesInBatchAsync(newMessageIds, downloadRawMime: false, cancellationToken).ConfigureAwait(false);
|
||||
// Draft folder needs MIME during initial sync so compose can open immediately.
|
||||
bool shouldDownloadRawMime = folder.SpecialFolderType == SpecialFolderType.Draft || folder.RemoteFolderId == ServiceConstants.DRAFT_LABEL_ID;
|
||||
await DownloadMessagesInBatchAsync(newMessageIds, downloadRawMime: shouldDownloadRawMime, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
foreach (var id in newMessageIds)
|
||||
{
|
||||
@@ -1848,6 +1850,88 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
return ("", emailOnly);
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AccountContact> ExtractContactsFromGmailMessage(Message message, MimeMessage mimeMessage)
|
||||
{
|
||||
var contacts = new Dictionary<string, AccountContact>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
AddFromHeaders(message?.Payload?.Headers);
|
||||
|
||||
if (mimeMessage != null)
|
||||
{
|
||||
AddFromInternetAddressList(mimeMessage.From);
|
||||
AddFromInternetAddressList(mimeMessage.To);
|
||||
AddFromInternetAddressList(mimeMessage.Cc);
|
||||
AddFromInternetAddressList(mimeMessage.Bcc);
|
||||
AddFromInternetAddressList(mimeMessage.ReplyTo);
|
||||
|
||||
if (mimeMessage.Sender is MailboxAddress senderMailbox)
|
||||
{
|
||||
AddContact(senderMailbox.Address, senderMailbox.Name);
|
||||
}
|
||||
}
|
||||
|
||||
return contacts.Values.ToList();
|
||||
|
||||
void AddFromHeaders(IList<MessagePartHeader> headers)
|
||||
{
|
||||
if (headers == null || headers.Count == 0) return;
|
||||
|
||||
AddFromHeader("From");
|
||||
AddFromHeader("Sender");
|
||||
AddFromHeader("To");
|
||||
AddFromHeader("Cc");
|
||||
AddFromHeader("Bcc");
|
||||
AddFromHeader("Reply-To");
|
||||
|
||||
void AddFromHeader(string headerName)
|
||||
{
|
||||
var headerValue = headers
|
||||
.FirstOrDefault(h => h.Name.Equals(headerName, StringComparison.OrdinalIgnoreCase))
|
||||
?.Value;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(headerValue)) return;
|
||||
|
||||
try
|
||||
{
|
||||
var addresses = InternetAddressList.Parse(headerValue);
|
||||
foreach (var mailbox in addresses.Mailboxes)
|
||||
{
|
||||
AddContact(mailbox.Address, mailbox.Name);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
var (name, email) = ExtractNameAndEmailFromHeader(headerValue);
|
||||
AddContact(email, name);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void AddFromInternetAddressList(InternetAddressList addresses)
|
||||
{
|
||||
if (addresses == null) return;
|
||||
|
||||
foreach (var mailbox in addresses.Mailboxes)
|
||||
{
|
||||
AddContact(mailbox.Address, mailbox.Name);
|
||||
}
|
||||
}
|
||||
|
||||
void AddContact(string address, string name)
|
||||
{
|
||||
var trimmedAddress = address?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(trimmedAddress)) return;
|
||||
|
||||
var displayName = string.IsNullOrWhiteSpace(name) ? trimmedAddress : name.Trim();
|
||||
|
||||
contacts[trimmedAddress] = new AccountContact
|
||||
{
|
||||
Address = trimmedAddress,
|
||||
Name = displayName
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates new mail packages for the given message.
|
||||
/// AssignedFolder is null since the LabelId is parsed out of the Message.
|
||||
@@ -1887,6 +1971,8 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
EnrichMailCopyFromMime(baseMailCopy, mimeMessage);
|
||||
}
|
||||
|
||||
var extractedContacts = ExtractContactsFromGmailMessage(message, mimeMessage);
|
||||
|
||||
// Check for local draft mapping using X-Wino-Draft-Id header.
|
||||
// For Metadata format we read from Payload.Headers.
|
||||
// For Raw format (Payload is null), we read from parsed MIME headers.
|
||||
@@ -1962,7 +2048,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
|
||||
mailCopyForLabel.Id = sharedId;
|
||||
mailCopyForLabel.FileId = sharedFileId;
|
||||
|
||||
packageList.Add(new NewMailItemPackage(mailCopyForLabel, mimeMessage, labelId));
|
||||
packageList.Add(new NewMailItemPackage(mailCopyForLabel, mimeMessage, labelId, extractedContacts));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using MailKit;
|
||||
using MailKit.Net.Imap;
|
||||
using MailKit.Search;
|
||||
using MimeKit;
|
||||
using MoreLinq;
|
||||
using Serilog;
|
||||
using Wino.Core.Domain.Entities.Mail;
|
||||
@@ -334,7 +335,8 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
||||
// Local copy doesn't exists. Continue execution to insert mail copy.
|
||||
}
|
||||
|
||||
var package = new NewMailItemPackage(mailCopy, message.MimeMessage, assignedFolder.RemoteFolderId);
|
||||
var contacts = ExtractContactsFromMimeMessage(message.MimeMessage);
|
||||
var package = new NewMailItemPackage(mailCopy, message.MimeMessage, assignedFolder.RemoteFolderId, contacts);
|
||||
|
||||
return
|
||||
[
|
||||
@@ -342,6 +344,50 @@ public class ImapSynchronizer : WinoSynchronizer<ImapRequest, ImapMessageCreatio
|
||||
];
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AccountContact> ExtractContactsFromMimeMessage(MimeMessage mimeMessage)
|
||||
{
|
||||
if (mimeMessage == null) return [];
|
||||
|
||||
var contacts = new Dictionary<string, AccountContact>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
AddFromInternetAddressList(mimeMessage.From);
|
||||
AddFromInternetAddressList(mimeMessage.To);
|
||||
AddFromInternetAddressList(mimeMessage.Cc);
|
||||
AddFromInternetAddressList(mimeMessage.Bcc);
|
||||
AddFromInternetAddressList(mimeMessage.ReplyTo);
|
||||
|
||||
if (mimeMessage.Sender is MailboxAddress senderMailbox)
|
||||
{
|
||||
AddContact(senderMailbox.Address, senderMailbox.Name);
|
||||
}
|
||||
|
||||
return contacts.Values.ToList();
|
||||
|
||||
void AddFromInternetAddressList(InternetAddressList addresses)
|
||||
{
|
||||
if (addresses == null) return;
|
||||
|
||||
foreach (var mailbox in addresses.Mailboxes)
|
||||
{
|
||||
AddContact(mailbox.Address, mailbox.Name);
|
||||
}
|
||||
}
|
||||
|
||||
void AddContact(string address, string name)
|
||||
{
|
||||
var trimmedAddress = address?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(trimmedAddress)) return;
|
||||
|
||||
var displayName = string.IsNullOrWhiteSpace(name) ? trimmedAddress : name.Trim();
|
||||
|
||||
contacts[trimmedAddress] = new AccountContact
|
||||
{
|
||||
Address = trimmedAddress,
|
||||
Name = displayName
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task<MailSynchronizationResult> SynchronizeMailsInternalAsync(MailSynchronizationOptions options, CancellationToken cancellationToken = default)
|
||||
{
|
||||
var downloadedMessageIds = new List<string>();
|
||||
|
||||
@@ -90,6 +90,11 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
"Id",
|
||||
"ConversationId",
|
||||
"From",
|
||||
"Sender",
|
||||
"ToRecipients",
|
||||
"CcRecipients",
|
||||
"BccRecipients",
|
||||
"ReplyTo",
|
||||
"Subject",
|
||||
"ParentFolderId",
|
||||
"InternetMessageId",
|
||||
@@ -375,28 +380,50 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
|
||||
if (!mailExists)
|
||||
{
|
||||
// Create MailCopy from metadata
|
||||
var mailCopy = await CreateMailCopyFromMessageAsync(message, folder).ConfigureAwait(false);
|
||||
|
||||
if (mailCopy != null)
|
||||
// For drafts, download MIME during initial sync like delta sync.
|
||||
if (folder.SpecialFolderType == SpecialFolderType.Draft)
|
||||
{
|
||||
// Create package without MIME
|
||||
var package = new NewMailItemPackage(mailCopy, null, folder.RemoteFolderId);
|
||||
bool isInserted = await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
|
||||
var draftPackages = await CreateNewMailPackagesAsync(message, folder, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
if (isInserted)
|
||||
if (draftPackages != null)
|
||||
{
|
||||
downloadedMessageIds.Add(mailCopy.Id);
|
||||
totalProcessed++;
|
||||
|
||||
// Update progress periodically
|
||||
if (totalProcessed % 50 == 0)
|
||||
foreach (var package in draftPackages)
|
||||
{
|
||||
var statusMessage = string.Format(Translator.Sync_DownloadedMessages, totalProcessed, folder.FolderName);
|
||||
UpdateSyncProgress(0, 0, statusMessage);
|
||||
bool isInserted = await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
|
||||
if (isInserted)
|
||||
{
|
||||
downloadedMessageIds.Add(package.Copy.Id);
|
||||
totalProcessed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Create MailCopy from metadata
|
||||
var mailCopy = await CreateMailCopyFromMessageAsync(message, folder).ConfigureAwait(false);
|
||||
|
||||
if (mailCopy != null)
|
||||
{
|
||||
// Create package without MIME
|
||||
var contacts = ExtractContactsFromOutlookMessage(message);
|
||||
var package = new NewMailItemPackage(mailCopy, null, folder.RemoteFolderId, contacts);
|
||||
bool isInserted = await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
|
||||
|
||||
if (isInserted)
|
||||
{
|
||||
downloadedMessageIds.Add(mailCopy.Id);
|
||||
totalProcessed++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update progress periodically
|
||||
if (totalProcessed > 0 && totalProcessed % 50 == 0)
|
||||
{
|
||||
var statusMessage = string.Format(Translator.Sync_DownloadedMessages, totalProcessed, folder.FolderName);
|
||||
UpdateSyncProgress(0, 0, statusMessage);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -551,7 +578,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
if (mailCopy != null)
|
||||
{
|
||||
// Create package without MIME
|
||||
var package = new NewMailItemPackage(mailCopy, null, folder.RemoteFolderId);
|
||||
var contacts = ExtractContactsFromOutlookMessage(message);
|
||||
var package = new NewMailItemPackage(mailCopy, null, folder.RemoteFolderId, contacts);
|
||||
bool isInserted = await _outlookChangeProcessor.CreateMailAsync(Account.Id, package).ConfigureAwait(false);
|
||||
|
||||
if (isInserted)
|
||||
@@ -686,6 +714,64 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
return mailCopy;
|
||||
}
|
||||
|
||||
private static IReadOnlyList<AccountContact> ExtractContactsFromOutlookMessage(Message message)
|
||||
{
|
||||
if (message == null) return [];
|
||||
|
||||
var contacts = new Dictionary<string, AccountContact>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
AddRecipient(message.From?.EmailAddress);
|
||||
AddRecipient(message.Sender?.EmailAddress);
|
||||
|
||||
if (message.ToRecipients != null)
|
||||
{
|
||||
foreach (var recipient in message.ToRecipients)
|
||||
{
|
||||
AddRecipient(recipient?.EmailAddress);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.CcRecipients != null)
|
||||
{
|
||||
foreach (var recipient in message.CcRecipients)
|
||||
{
|
||||
AddRecipient(recipient?.EmailAddress);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.BccRecipients != null)
|
||||
{
|
||||
foreach (var recipient in message.BccRecipients)
|
||||
{
|
||||
AddRecipient(recipient?.EmailAddress);
|
||||
}
|
||||
}
|
||||
|
||||
if (message.ReplyTo != null)
|
||||
{
|
||||
foreach (var recipient in message.ReplyTo)
|
||||
{
|
||||
AddRecipient(recipient?.EmailAddress);
|
||||
}
|
||||
}
|
||||
|
||||
return contacts.Values.ToList();
|
||||
|
||||
void AddRecipient(EmailAddress emailAddress)
|
||||
{
|
||||
var address = emailAddress?.Address?.Trim();
|
||||
if (string.IsNullOrWhiteSpace(address)) return;
|
||||
|
||||
var displayName = string.IsNullOrWhiteSpace(emailAddress.Name) ? address : emailAddress.Name.Trim();
|
||||
|
||||
contacts[address] = new AccountContact
|
||||
{
|
||||
Address = address,
|
||||
Name = displayName
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private string GetDeltaTokenFromDeltaLink(string deltaLink)
|
||||
=> Regex.Split(deltaLink, "deltatoken=")[1];
|
||||
|
||||
@@ -1790,7 +1876,8 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
||||
|
||||
// Outlook messages can only be assigned to 1 folder at a time.
|
||||
// Therefore we don't need to create multiple copies of the same message for different folders.
|
||||
var package = new NewMailItemPackage(mailCopy, mimeMessage, assignedFolder.RemoteFolderId);
|
||||
var contacts = ExtractContactsFromOutlookMessage(message);
|
||||
var package = new NewMailItemPackage(mailCopy, mimeMessage, assignedFolder.RemoteFolderId, contacts);
|
||||
|
||||
return [package];
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user