diff --git a/Wino.Core/Extensions/OutlookIntegratorExtensions.cs b/Wino.Core/Extensions/OutlookIntegratorExtensions.cs
index 894b1bce..42223fcb 100644
--- a/Wino.Core/Extensions/OutlookIntegratorExtensions.cs
+++ b/Wino.Core/Extensions/OutlookIntegratorExtensions.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.IO;
using System.Linq;
using System.Text;
using Microsoft.Graph.Models;
@@ -138,37 +139,38 @@ public static class OutlookIntegratorExtensions
var bccAddresses = GetRecipients(mime.Bcc).ToList();
var replyToAddresses = GetRecipients(mime.ReplyTo).ToList();
+ // Prefer HTML body, fall back to plain text.
+ var (bodyContent, bodyType) = mime.HtmlBody != null
+ ? (mime.HtmlBody, BodyType.Html)
+ : (mime.TextBody ?? string.Empty, BodyType.Text);
+
var message = new Message()
{
Subject = mime.Subject,
Importance = GetImportance(mime.Importance),
- Body = new ItemBody() { ContentType = BodyType.Html, Content = mime.HtmlBody },
+ Body = new ItemBody() { ContentType = bodyType, Content = bodyContent },
IsDraft = false,
- IsRead = true, // Sent messages are always read.
+ IsRead = true,
ToRecipients = toAddresses,
CcRecipients = ccAddresses,
BccRecipients = bccAddresses,
From = fromAddress,
InternetMessageId = mime.MessageId,
ReplyTo = replyToAddresses,
- Attachments = []
};
- // Set ConversationId if provided to maintain threading
if (!string.IsNullOrEmpty(conversationId))
{
message.ConversationId = conversationId;
}
// Headers are only included when creating the draft.
- // When sending, they are not included. Graph will throw an error.
-
+ // Graph API throws an error if headers are included in send/patch operations.
if (includeInternetHeaders)
{
message.InternetMessageHeaders = GetHeaderList(mime);
}
-
return message;
}
@@ -367,6 +369,40 @@ public static class OutlookIntegratorExtensions
#region Mime to Outlook Message Helpers
+ ///
+ /// Extracts all attachments (inline and regular) from a MimeMessage
+ /// and returns them as Graph SDK FileAttachment objects.
+ ///
+ public static List ExtractAttachments(this MimeMessage mime)
+ {
+ var attachments = new List();
+
+ foreach (var part in mime.BodyParts)
+ {
+ bool isInline = part.ContentDisposition?.Disposition == "inline";
+
+ if (!part.IsAttachment && !isInline)
+ continue;
+
+ if (part is not MimePart mimePart || mimePart.Content == null)
+ continue;
+
+ using var memory = new MemoryStream();
+ mimePart.Content.DecodeTo(memory);
+
+ attachments.Add(new FileAttachment()
+ {
+ Name = part.ContentDisposition?.FileName ?? part.ContentType.Name,
+ ContentBytes = memory.ToArray(),
+ ContentType = part.ContentType.MimeType,
+ ContentId = part.ContentId,
+ IsInline = isInline
+ });
+ }
+
+ return attachments;
+ }
+
private static IEnumerable GetRecipients(this InternetAddressList internetAddresses)
{
foreach (var address in internetAddresses)
@@ -399,46 +435,46 @@ public static class OutlookIntegratorExtensions
private static List GetHeaderList(this MimeMessage mime)
{
// Graph API only allows max of 5 headers.
- // Here we'll try to ignore some headers that are not neccessary.
- // Outlook API will generate them automatically.
-
- // Some headers also require to start with X- or x-.
+ // Prioritize threading headers to keep reply grouping intact.
+ const int headerLimit = 5;
string[] headersToIgnore = ["Date", "To", "Cc", "Bcc", "MIME-Version", "From", "Subject", "Message-Id"];
- string[] headersToModify = ["In-Reply-To", "Reply-To", "References", "Thread-Topic"];
var headers = new List();
- // PRIORITY: Always include WinoLocalDraftHeader first if it exists
- // This is critical for draft mapping functionality
- var winoDraftHeader = mime.Headers.FirstOrDefault(h => h.Field == Domain.Constants.WinoLocalDraftHeader);
- int includedHeaderCount = 0;
-
- if (winoDraftHeader != null)
+ void AddHeader(string name, string value)
{
- var headerValue = winoDraftHeader.Value.Length >= 995 ? winoDraftHeader.Value.Substring(0, 995) : winoDraftHeader.Value;
- headers.Add(new InternetMessageHeader() { Name = winoDraftHeader.Field, Value = headerValue });
- includedHeaderCount++;
+ if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(value)) return;
+ if (headers.Count >= headerLimit) return;
+ if (headers.Any(h => string.Equals(h.Name, name, StringComparison.OrdinalIgnoreCase))) return;
+
+ // No header value should exceed 995 characters.
+ var headerValue = value.Length >= 995 ? value.Substring(0, 995) : value;
+ headers.Add(new InternetMessageHeader() { Name = name, Value = headerValue });
}
- // Include other headers up to the limit (excluding the already added WinoLocalDraftHeader)
+ // PRIORITY: Always include WinoLocalDraftHeader first if it exists.
+ var winoDraftHeader = mime.Headers.FirstOrDefault(h => h.Field == Domain.Constants.WinoLocalDraftHeader);
+ if (winoDraftHeader != null)
+ AddHeader(winoDraftHeader.Field, winoDraftHeader.Value);
+
+ // Threading headers must be preserved with their real RFC names.
+ AddHeader("In-Reply-To", mime.Headers[HeaderId.InReplyTo]);
+ AddHeader("References", mime.Headers[HeaderId.References]);
+
+ // Fill remaining slots with custom headers only (avoid Graph restrictions).
foreach (var header in mime.Headers)
{
- if (header.Field == Domain.Constants.WinoLocalDraftHeader)
- continue; // Already processed above
+ if (headers.Count >= headerLimit) break;
+ if (header.Field == Domain.Constants.WinoLocalDraftHeader) continue;
+ if (headersToIgnore.Contains(header.Field)) continue;
+ if (string.Equals(header.Field, "In-Reply-To", StringComparison.OrdinalIgnoreCase)) continue;
+ if (string.Equals(header.Field, "References", StringComparison.OrdinalIgnoreCase)) continue;
- if (!headersToIgnore.Contains(header.Field))
- {
- var headerName = headersToModify.Contains(header.Field) ? $"X-{header.Field}" : header.Field;
+ // Only include custom headers beyond the core threading ones.
+ if (!header.Field.StartsWith("X-", StringComparison.OrdinalIgnoreCase)) continue;
- // No header value should exceed 995 characters.
- var headerValue = header.Value.Length >= 995 ? header.Value.Substring(0, 995) : header.Value;
-
- headers.Add(new InternetMessageHeader() { Name = headerName, Value = headerValue });
- includedHeaderCount++;
- }
-
- if (includedHeaderCount >= 5) break;
+ AddHeader(header.Field, header.Value);
}
return headers;
diff --git a/Wino.Core/Misc/OutlookFileAttachment.cs b/Wino.Core/Misc/OutlookFileAttachment.cs
deleted file mode 100644
index 0839657c..00000000
--- a/Wino.Core/Misc/OutlookFileAttachment.cs
+++ /dev/null
@@ -1,24 +0,0 @@
-using System.Text.Json.Serialization;
-
-namespace Wino.Core.Misc;
-
-public class OutlookFileAttachment
-{
- [JsonPropertyName("@odata.type")]
- public string OdataType { get; } = "#microsoft.graph.fileAttachment";
-
- [JsonPropertyName("name")]
- public string FileName { get; set; }
-
- [JsonPropertyName("contentBytes")]
- public string Base64EncodedContentBytes { get; set; }
-
- [JsonPropertyName("contentType")]
- public string ContentType { get; set; }
-
- [JsonPropertyName("contentId")]
- public string ContentId { get; set; }
-
- [JsonPropertyName("isInline")]
- public bool IsInline { get; set; }
-}
diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs
index 1fac6c5a..c1b73098 100644
--- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs
+++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs
@@ -36,7 +36,6 @@ using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Extensions;
using Wino.Core.Http;
using Wino.Core.Integration.Processors;
-using Wino.Core.Misc;
using Wino.Core.Requests.Bundles;
using Wino.Core.Requests.Calendar;
using Wino.Core.Requests.Folder;
@@ -45,7 +44,6 @@ using Wino.Core.Requests.Mail;
namespace Wino.Core.Synchronizers.Mail;
[JsonSerializable(typeof(Microsoft.Graph.Me.Messages.Item.Move.MovePostRequestBody))]
-[JsonSerializable(typeof(OutlookFileAttachment))]
public partial class OutlookSynchronizerJsonContext : JsonSerializerContext;
///
@@ -1382,96 +1380,43 @@ public class OutlookSynchronizer : WinoSynchronizer> SendDraft(SendDraftRequest request)
{
var sendDraftPreparationRequest = request.Request;
-
- // 1. Delete draft
- // 2. Create new Message with new MIME.
- // 3. Make sure that conversation id is tagged correctly for replies.
-
var mailCopyId = sendDraftPreparationRequest.MailItem.Id;
var mimeMessage = sendDraftPreparationRequest.Mime;
- // Convert mime message to Outlook message.
- // Outlook synchronizer does not send MIME messages directly anymore.
- // Alias support is lacking with direct MIMEs.
- // Therefore we convert the MIME message to Outlook message and use proper APIs.
-
- // Pass the ConversationId (ThreadId) to maintain threading for replies/forwards
+ // Graph API ignores the From header in direct MIME uploads, so we must convert
+ // to a JSON Message object to properly support sending from aliases.
var conversationId = sendDraftPreparationRequest.MailItem.ThreadId;
var outlookMessage = mimeMessage.AsOutlookMessage(false, conversationId);
- // Create attachment requests.
- // TODO: We need to support large file attachments with sessioned upload at some point.
-
- var attachmentRequestList = CreateAttachmentUploadBundles(mimeMessage, mailCopyId, request).ToList();
-
- // Update draft.
+ // Build the request sequence: upload attachments -> patch draft -> send.
+ // These execute serially via batch DependsOn (see ConfigureSerialExecution).
+ var attachmentBundles = CreateAttachmentUploadBundles(mimeMessage, mailCopyId);
var patchDraftRequest = _graphClient.Me.Messages[mailCopyId].ToPatchRequestInformation(outlookMessage);
- var patchDraftRequestBundle = new HttpRequestBundle(patchDraftRequest, request);
+ var patchDraftBundle = new HttpRequestBundle(patchDraftRequest, request);
- // Send draft.
+ var sendRequest = PreparePostRequestInformation(_graphClient.Me.Messages[mailCopyId].Send.ToPostRequestInformation());
+ var sendBundle = new HttpRequestBundle(sendRequest, request);
- var sendDraftRequest = PreparePostRequestInformation(_graphClient.Me.Messages[mailCopyId].Send.ToPostRequestInformation());
- var sendDraftRequestBundle = new HttpRequestBundle(sendDraftRequest, request);
-
- return [.. attachmentRequestList, patchDraftRequestBundle, sendDraftRequestBundle];
+ return [.. attachmentBundles, patchDraftBundle, sendBundle];
}
- private List> CreateAttachmentUploadBundles(MimeMessage mime, string mailCopyId, IRequestBase sourceRequest)
+ ///
+ /// Extracts attachments from the MIME message and creates individual
+ /// Graph API upload requests using the SDK's FileAttachment type.
+ ///
+ private List> CreateAttachmentUploadBundles(MimeMessage mime, string mailCopyId)
{
- var allAttachments = new List();
+ var attachments = mime.ExtractAttachments();
+ var bundles = new List>(attachments.Count);
- foreach (var part in mime.BodyParts)
+ foreach (var attachment in attachments)
{
- var isAttachmentOrInline = part.IsAttachment ? true : part.ContentDisposition?.Disposition == "inline";
-
- if (!isAttachmentOrInline) continue;
-
- using var memory = new MemoryStream();
- ((MimePart)part).Content.DecodeTo(memory);
-
- var base64String = Convert.ToBase64String(memory.ToArray());
-
- var attachment = new OutlookFileAttachment()
- {
- Base64EncodedContentBytes = base64String,
- FileName = part.ContentDisposition?.FileName ?? part.ContentType.Name,
- ContentId = part.ContentId,
- ContentType = part.ContentType.MimeType,
- IsInline = part.ContentDisposition?.Disposition == "inline"
- };
-
- allAttachments.Add(attachment);
+ var uploadRequest = _graphClient.Me.Messages[mailCopyId].Attachments.ToPostRequestInformation(attachment);
+ bundles.Add(new HttpRequestBundle(uploadRequest, null));
}
- static RequestInformation PrepareUploadAttachmentRequest(RequestInformation requestInformation, OutlookFileAttachment outlookFileAttachment)
- {
- requestInformation.Headers.Clear();
-
- string contentJson = JsonSerializer.Serialize(outlookFileAttachment, OutlookSynchronizerJsonContext.Default.OutlookFileAttachment);
-
- requestInformation.Content = new MemoryStream(Encoding.UTF8.GetBytes(contentJson));
- requestInformation.HttpMethod = Method.POST;
- requestInformation.Headers.Add("Content-Type", "application/json");
-
- return requestInformation;
- }
-
- var retList = new List>();
-
- // Prepare attachment upload requests.
-
- foreach (var attachment in allAttachments)
- {
- var emptyPostRequest = _graphClient.Me.Messages[mailCopyId].Attachments.ToPostRequestInformation(new Attachment());
- var modifiedAttachmentUploadRequest = PrepareUploadAttachmentRequest(emptyPostRequest, attachment);
-
- var bundle = new HttpRequestBundle(modifiedAttachmentUploadRequest, null);
-
- retList.Add(bundle);
- }
-
- return retList;
+ return bundles;
}
public override List> Archive(BatchArchiveRequest request)
diff --git a/Wino.Mail.ViewModels/ComposePageViewModel.cs b/Wino.Mail.ViewModels/ComposePageViewModel.cs
index 17d665f2..4e9eadfb 100644
--- a/Wino.Mail.ViewModels/ComposePageViewModel.cs
+++ b/Wino.Mail.ViewModels/ComposePageViewModel.cs
@@ -21,11 +21,13 @@ using Wino.Core.Domain.Models.Navigation;
using Wino.Core.Extensions;
using Wino.Core.Services;
using Wino.Mail.ViewModels.Data;
+using Wino.Mail.ViewModels.Messages;
using Wino.Messaging.Client.Mails;
namespace Wino.Mail.ViewModels;
-public partial class ComposePageViewModel : MailBaseViewModel
+public partial class ComposePageViewModel : MailBaseViewModel,
+ IRecipient
{
public Func> GetHTMLBodyFunction;
@@ -432,6 +434,35 @@ public partial class ComposePageViewModel : MailBaseViewModel
}
}
+ public async void Receive(NewComposeDraftItemRequestedEvent message)
+ {
+ // Save current draft before switching.
+ await UpdateMimeChangesAsync();
+
+ // Reset state for the new draft.
+ isUpdatingMimeBlocked = false;
+ ComposingAccount = null;
+ IncludedAttachments.Clear();
+
+ // Set the new draft item and prepare it.
+ CurrentMailDraftItem = message.MailItemViewModel;
+ await TryPrepareComposeAsync(true);
+ }
+
+ protected override void RegisterRecipients()
+ {
+ base.RegisterRecipients();
+
+ Messenger.Register(this);
+ }
+
+ protected override void UnregisterRecipients()
+ {
+ base.UnregisterRecipients();
+
+ Messenger.Unregister(this);
+ }
+
private async Task InitializeComposerAccountAsync()
{
if (CurrentMailDraftItem == null) return false;
@@ -550,6 +581,8 @@ public partial class ComposePageViewModel : MailBaseViewModel
{
if (CurrentMimeMessage == null) return;
+ IncludedAttachments.Clear();
+
foreach (var attachment in CurrentMimeMessage.Attachments)
{
if (attachment.IsAttachment && attachment is MimePart attachmentPart)
diff --git a/Wino.Mail.ViewModels/MailListPageViewModel.cs b/Wino.Mail.ViewModels/MailListPageViewModel.cs
index d49d3a56..38597738 100644
--- a/Wino.Mail.ViewModels/MailListPageViewModel.cs
+++ b/Wino.Mail.ViewModels/MailListPageViewModel.cs
@@ -635,6 +635,56 @@ public partial class MailListPageViewModel : MailBaseViewModel,
if (isFromDraftOrSentFolder)
{
+ // Fix for draft duplication: When a draft is created for reply/forward, it's first added as local draft.
+ // Then the server sync fetches it back. We should skip adding remote drafts if a local draft already exists
+ // with the same ThreadId. The mapping system (DraftMapped) will handle updating the existing local draft.
+ if (addedMail.IsDraft && !addedMail.IsLocalDraft && !string.IsNullOrEmpty(addedMail.ThreadId))
+ {
+ // Check if collection already has a local draft with the same ThreadId in the same folder
+ bool hasLocalDraftInSameThread = false;
+
+ foreach (var group in MailCollection.MailItems)
+ {
+ foreach (var item in group)
+ {
+ if (item is MailItemViewModel mailItem)
+ {
+ if (mailItem.IsDraft &&
+ mailItem.MailCopy.IsLocalDraft &&
+ mailItem.MailCopy.ThreadId == addedMail.ThreadId &&
+ mailItem.MailCopy.FolderId == addedMail.FolderId)
+ {
+ hasLocalDraftInSameThread = true;
+ break;
+ }
+ }
+ else if (item is ThreadMailItemViewModel threadItem)
+ {
+ foreach (var threadEmail in threadItem.ThreadEmails)
+ {
+ if (threadEmail.IsDraft &&
+ threadEmail.MailCopy.IsLocalDraft &&
+ threadEmail.MailCopy.ThreadId == addedMail.ThreadId &&
+ threadEmail.MailCopy.FolderId == addedMail.FolderId)
+ {
+ hasLocalDraftInSameThread = true;
+ break;
+ }
+ }
+ if (hasLocalDraftInSameThread) break;
+ }
+ }
+ if (hasLocalDraftInSameThread) break;
+ }
+
+ if (hasLocalDraftInSameThread)
+ {
+ // Local draft exists in the same thread - skip adding remote duplicate
+ // The mapping system will update the local draft with remote IDs when DraftMapped message is received
+ return;
+ }
+ }
+
// Only add if the ThreadId exists in the collection (can be threaded with existing items)
if (!ThreadIdExistsInCollection(addedMail)) return;
}
@@ -757,6 +807,17 @@ public partial class MailListPageViewModel : MailBaseViewModel,
}
}
+ protected override void OnDraftMapped(string localDraftCopyId, string remoteDraftCopyId)
+ {
+ base.OnDraftMapped(localDraftCopyId, remoteDraftCopyId);
+
+ // When a draft is mapped from local to remote, the database has been updated
+ // but the UI collection still references the MailCopy object with old IDs.
+ // The MailCollection.AddAsync method checks UniqueId (which doesn't change during mapping)
+ // so if mapping worked correctly, no duplicate should appear.
+ // This method is here for future enhancements if additional UI updates are needed.
+ }
+
private async Task> PrepareMailViewModelsAsync(IEnumerable mailItems, CancellationToken cancellationToken = default)
{
// Run ViewModel creation on background thread to avoid blocking UI
diff --git a/Wino.Mail.ViewModels/Messages/NewComposeDraftItemRequestedEvent.cs b/Wino.Mail.ViewModels/Messages/NewComposeDraftItemRequestedEvent.cs
new file mode 100644
index 00000000..1b94c725
--- /dev/null
+++ b/Wino.Mail.ViewModels/Messages/NewComposeDraftItemRequestedEvent.cs
@@ -0,0 +1,10 @@
+using Wino.Mail.ViewModels.Data;
+
+namespace Wino.Mail.ViewModels.Messages;
+
+///
+/// When the compose page is already active, but a different draft item is selected.
+/// To not trigger navigation again and re-use existing WebView2 editor.
+///
+/// The new draft mail item to compose.
+public record NewComposeDraftItemRequestedEvent(MailItemViewModel MailItemViewModel);
diff --git a/Wino.Mail.WinUI/Services/NavigationService.cs b/Wino.Mail.WinUI/Services/NavigationService.cs
index 5d22203e..6915420f 100644
--- a/Wino.Mail.WinUI/Services/NavigationService.cs
+++ b/Wino.Mail.WinUI/Services/NavigationService.cs
@@ -185,6 +185,15 @@ public class NavigationService : NavigationServiceBase, INavigationService
{
WeakReferenceMessenger.Default.Send(new NewMailItemRenderingRequestedEvent(mailItemViewModel));
}
+ else if (listingFrame.Content != null
+ && listingFrame.Content.GetType() == GetPageType(WinoPage.ComposePage)
+ && page == WinoPage.ComposePage
+ && parameter is MailItemViewModel composeDraftViewModel)
+ {
+ // ComposePage is already active and we're switching to another draft.
+ // Reuse existing ComposePage and WebView2 instead of navigating.
+ WeakReferenceMessenger.Default.Send(new NewComposeDraftItemRequestedEvent(composeDraftViewModel));
+ }
else if (listingFrame.Content != null
&& listingFrame.Content.GetType() == GetPageType(WinoPage.IdlePage)
&& pageType == typeof(IdlePage))
diff --git a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs
index a3b99c69..2e78f3be 100644
--- a/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs
+++ b/Wino.Mail.WinUI/Views/Mail/ComposePage.xaml.cs
@@ -21,6 +21,7 @@ using Wino.Core.Domain;
using Wino.Core.Domain.Entities.Shared;
using Wino.Core.Domain.Models.Reader;
using Wino.Mail.ViewModels.Data;
+using Wino.Mail.ViewModels.Messages;
using Wino.Mail.WinUI.Extensions;
using Wino.Messaging.Client.Mails;
using Wino.Messaging.Client.Shell;
@@ -30,7 +31,8 @@ namespace Wino.Views.Mail;
public sealed partial class ComposePage : ComposePageAbstract,
IRecipient,
- IRecipient
+ IRecipient,
+ IRecipient
{
public WebView2 GetWebView() => WebViewEditor.GetUnderlyingWebView();
@@ -300,6 +302,12 @@ public sealed partial class ComposePage : ComposePageAbstract,
WebViewEditor.IsEditorDarkMode = message.IsUnderlyingThemeDark;
}
+ void IRecipient.Receive(NewComposeDraftItemRequestedEvent message)
+ {
+ // Reset the initial focus flag so ToBox gets focus for the new draft.
+ isInitialFocusHandled = false;
+ }
+
private void ImportanceClicked(object sender, RoutedEventArgs e)
{
ImportanceFlyout.Hide();
@@ -415,6 +423,7 @@ public sealed partial class ComposePage : ComposePageAbstract,
WeakReferenceMessenger.Default.Register(this);
WeakReferenceMessenger.Default.Register(this);
+ WeakReferenceMessenger.Default.Register(this);
}
protected override void UnregisterRecipients()
@@ -423,6 +432,7 @@ public sealed partial class ComposePage : ComposePageAbstract,
WeakReferenceMessenger.Default.Unregister(this);
WeakReferenceMessenger.Default.Unregister(this);
+ WeakReferenceMessenger.Default.Unregister(this);
}
// TODO: Save mime on closing the app.
diff --git a/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml.cs b/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml.cs
index f2cb5458..d05785e1 100644
--- a/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml.cs
+++ b/Wino.Mail.WinUI/Views/Mail/MailListPage.xaml.cs
@@ -262,8 +262,9 @@ public sealed partial class MailListPage : MailListPageAbstract,
}
else if (IsComposingPageActive())
{
- // Composer is already active. Prepare composer WebView2 animation.
- PrepareComposePageWebViewTransition();
+ // Composer is already active. Skip connected animation since the page
+ // will be reused in-place (no navigation occurs).
+ // NavigationService will send NewComposeDraftItemRequestedEvent instead.
}
else
composerPageTransition = NavigationTransitionType.DrillIn;
diff --git a/Wino.Services/MailService.cs b/Wino.Services/MailService.cs
index e5323775..c571eb2d 100644
--- a/Wino.Services/MailService.cs
+++ b/Wino.Services/MailService.cs
@@ -1056,12 +1056,14 @@ public class MailService : BaseDatabaseService, IMailService
bool isIdChanging = localDraftCopy.Id != newMailCopyId;
localDraftCopy.Id = newMailCopyId;
- localDraftCopy.DraftId = newDraftId;
- localDraftCopy.ThreadId = newThreadId;
+ if (!string.IsNullOrEmpty(newDraftId))
+ localDraftCopy.DraftId = newDraftId;
+ if (!string.IsNullOrEmpty(newThreadId))
+ localDraftCopy.ThreadId = newThreadId;
await UpdateMailAsync(localDraftCopy).ConfigureAwait(false);
- ReportUIChange(new DraftMapped(oldLocalDraftId, newDraftId));
+ ReportUIChange(new DraftMapped(oldLocalDraftId, localDraftCopy.DraftId));
return true;
}
@@ -1070,14 +1072,20 @@ public class MailService : BaseDatabaseService, IMailService
{
return UpdateAllMailCopiesAsync(mailCopyId, (item) =>
{
- if (item.ThreadId != newThreadId || item.DraftId != newDraftId)
+ var shouldUpdateThreadId = !string.IsNullOrEmpty(newThreadId);
+ var shouldUpdateDraftId = !string.IsNullOrEmpty(newDraftId);
+
+ if ((shouldUpdateThreadId && item.ThreadId != newThreadId) ||
+ (shouldUpdateDraftId && item.DraftId != newDraftId))
{
var oldDraftId = item.DraftId;
- item.DraftId = newDraftId;
- item.ThreadId = newThreadId;
+ if (shouldUpdateDraftId)
+ item.DraftId = newDraftId;
+ if (shouldUpdateThreadId)
+ item.ThreadId = newThreadId;
- ReportUIChange(new DraftMapped(oldDraftId, newDraftId));
+ ReportUIChange(new DraftMapped(oldDraftId, item.DraftId));
return true;
}