Gmail drafting

This commit is contained in:
Burak Kaan Köse
2026-02-06 21:46:30 +01:00
parent 4374d19ac2
commit 1ec8d5bbf2
2 changed files with 77 additions and 16 deletions
+75 -14
View File
@@ -243,6 +243,11 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
folderResults.Add(FolderSyncResult.Successful(folder.Id, folder.FolderName, 0));
}
}
// Map Gmail Draft resource IDs for all drafts.
// Gmail's Messages API doesn't expose Draft IDs, so we query the Drafts API separately.
// This ensures DraftId is correctly set for both Wino-created and externally-created drafts.
await MapDraftIdsAsync(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
@@ -1271,6 +1276,11 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
foreach (var package in packages)
{
// When downloaded with Raw format, Payload.Headers is not populated by Gmail API.
// Enrich the MailCopy fields (Subject, From, MessageId, etc.) from the parsed MIME.
if (downloadRawMime && mimeMessage != null)
EnrichMailCopyFromMime(package.Copy, mimeMessage);
// Create the mail copy with the MIME (if downloaded)
var packageWithMime = downloadRawMime && mimeMessage != null
? new NewMailItemPackage(package.Copy, mimeMessage, package.AssignedRemoteFolderId)
@@ -1699,13 +1709,65 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
ItemType = itemType
};
// Set DraftId if this is a draft
if (copy.IsDraft)
copy.DraftId = copy.ThreadId;
// Note: DraftId is NOT set here. Gmail's Draft resource ID is separate from ThreadId
// and can only be obtained from the Drafts API (not Messages API).
// DraftId is populated by:
// - MapLocalDraftAsync (for Wino-created drafts, from CreateDraft response)
// - MapDraftIdsAsync (for all drafts, from Drafts.List API)
return Task.FromResult(copy);
}
/// <summary>
/// Enriches a MailCopy with fields extracted from a parsed MimeMessage.
/// This is needed when messages are downloaded with Raw format (delta sync),
/// because the Gmail API does not populate Payload.Headers in Raw format.
/// Fields already populated (non-null/non-empty) are NOT overwritten.
/// </summary>
private static void EnrichMailCopyFromMime(MailCopy copy, MimeMessage mime)
{
if (copy == null || mime == null) return;
if (string.IsNullOrEmpty(copy.Subject))
copy.Subject = mime.Subject ?? string.Empty;
if (string.IsNullOrEmpty(copy.FromName))
{
var from = mime.From.Mailboxes.FirstOrDefault();
if (from != null)
copy.FromName = from.Name ?? string.Empty;
}
if (string.IsNullOrEmpty(copy.FromAddress))
{
var from = mime.From.Mailboxes.FirstOrDefault();
if (from != null)
copy.FromAddress = from.Address ?? string.Empty;
}
if (string.IsNullOrEmpty(copy.MessageId))
copy.MessageId = mime.MessageId;
if (string.IsNullOrEmpty(copy.InReplyTo))
copy.InReplyTo = mime.InReplyTo;
if (string.IsNullOrEmpty(copy.References) && mime.References?.Count > 0)
copy.References = string.Join(";", mime.References);
if (!copy.HasAttachments && mime.Attachments.Any())
copy.HasAttachments = true;
if (copy.Importance == MailImportance.Normal)
{
copy.Importance = mime.Importance switch
{
MessageImportance.High => MailImportance.High,
MessageImportance.Low => MailImportance.Low,
_ => MailImportance.Normal
};
}
}
/// <summary>
/// Determines MailItemType based on Gmail message headers.
/// Gmail doesn't have EventMessage type like Outlook, but calendar invitations can be detected
@@ -1788,21 +1850,20 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
// Create base MailCopy from metadata only - NO MIME download
var baseMailCopy = await CreateMinimalMailCopyAsync(message, assignedFolder, cancellationToken);
// Check for local draft mapping using X-Wino-Draft-Id header from metadata
// Check for local draft mapping using X-Wino-Draft-Id header from metadata.
// If this is a Wino-created draft, the local copy was already mapped by the CreateDraft response handler
// with the correct Gmail Draft ID. We must NOT call MapLocalDraftAsync here because
// baseMailCopy.DraftId is derived from CreateMinimalMailCopyAsync (not the real Draft resource ID),
// which would overwrite the correctly mapped DraftId and break SendDraft.
if (baseMailCopy.IsDraft)
{
var draftIdHeader = message.Payload?.Headers?.FirstOrDefault(h => h.Name.Equals(Domain.Constants.WinoLocalDraftHeader, StringComparison.OrdinalIgnoreCase))?.Value;
if (!string.IsNullOrEmpty(draftIdHeader) && Guid.TryParse(draftIdHeader, out Guid localDraftCopyUniqueId))
if (!string.IsNullOrEmpty(draftIdHeader) && Guid.TryParse(draftIdHeader, out _))
{
// This message belongs to existing local draft copy.
// We don't need to create a new mail copy for this message, just update the existing one.
bool isMappingSuccesfull = await _gmailChangeProcessor.MapLocalDraftAsync(Account.Id, localDraftCopyUniqueId, baseMailCopy.Id, baseMailCopy.DraftId, baseMailCopy.ThreadId);
if (isMappingSuccesfull) return null;
// Local copy doesn't exists. Continue execution to insert mail copy.
// This message belongs to an existing local draft copy.
// Skip creating a new mail copy - the local copy was already mapped by the response handler.
return null;
}
}
@@ -1825,7 +1886,7 @@ public class GmailSynchronizer : WinoSynchronizer<IClientServiceRequest, Message
// Create a new MailCopy instance for each label to avoid shared reference issues
var mailCopyForLabel = await CreateMinimalMailCopyAsync(message, assignedFolder, cancellationToken);
// Ensure all copies share the same Id and FileId
mailCopyForLabel.Id = sharedId;
mailCopyForLabel.FileId = sharedFileId;
+2 -2
View File
@@ -457,7 +457,7 @@ public partial class ComposePageViewModel : MailBaseViewModel
primaryAlias = aliases.Find(a => a.AliasAddress == CurrentMailDraftItem.FromAddress);
}
primaryAlias ??= await _accountService.GetPrimaryAccountAliasAsync(ComposingAccount.Id).ConfigureAwait(false);
primaryAlias ??= await _accountService.GetPrimaryAccountAliasAsync(composingAccount.Id).ConfigureAwait(false);
await ExecuteUIThread(() =>
{
@@ -477,7 +477,7 @@ public partial class ComposePageViewModel : MailBaseViewModel
if (!isComposerInitialized) return;
retry:
retry:
// Replying existing message.
MimeMessageInformation mimeMessageInformation = null;