Improve mailto links handling (#310)

* Refactor draft creation

* try scoped namespace

* Refactor mailto protocol and revert namespaces

* Remove useless account query

* Fix typo and CC/BCC in replies

* Replace convert with existing extension

* Small fixes

* Fix CC/Bcc in replies to automatically show if needed.

* Fixed body parameter position from mailto parameters

* Fixed issue with ReplyAll self not removed
This commit is contained in:
Tiktack
2024-08-10 14:33:02 +02:00
committed by GitHub
parent 8763bf11ab
commit f408f59beb
20 changed files with 360 additions and 377 deletions

View File

@@ -1,10 +1,16 @@
using System.Collections.Specialized; using Wino.Core.Domain.Models.Launch;
namespace Wino.Core.Domain.Interfaces namespace Wino.Core.Domain.Interfaces;
public interface ILaunchProtocolService
{ {
public interface ILaunchProtocolService /// <summary>
{ /// Used to handle toasts.
object LaunchParameter { get; set; } /// </summary>
NameValueCollection MailtoParameters { get; set; } object LaunchParameter { get; set; }
}
/// <summary>
/// Used to handle mailto links.
/// </summary>
MailToUri MailToUri { get; set; }
} }

View File

@@ -11,7 +11,6 @@ namespace Wino.Core.Domain.Interfaces
{ {
Task<MailCopy> GetSingleMailItemAsync(string mailCopyId, string remoteFolderId); Task<MailCopy> GetSingleMailItemAsync(string mailCopyId, string remoteFolderId);
Task<MailCopy> GetSingleMailItemAsync(Guid uniqueMailId); Task<MailCopy> GetSingleMailItemAsync(Guid uniqueMailId);
Task<MailCopy> CreateDraftAsync(MailAccount composerAccount, string generatedReplyMimeMessageBase64, MimeMessage replyingMimeMessage = null, IMailItem replyingMailItem = null);
Task<List<IMailItem>> FetchMailsAsync(MailListInitializationOptions options); Task<List<IMailItem>> FetchMailsAsync(MailListInitializationOptions options);
/// <summary> /// <summary>
@@ -44,23 +43,12 @@ namespace Wino.Core.Domain.Interfaces
/// <summary> /// <summary>
/// Maps new mail item with the existing local draft copy. /// Maps new mail item with the existing local draft copy.
///
/// </summary> /// </summary>
/// <param name="newMailCopyId"></param> /// <param name="newMailCopyId"></param>
/// <param name="newDraftId"></param> /// <param name="newDraftId"></param>
/// <param name="newThreadId"></param> /// <param name="newThreadId"></param>
Task MapLocalDraftAsync(string newMailCopyId, string newDraftId, string newThreadId); Task MapLocalDraftAsync(string newMailCopyId, string newDraftId, string newThreadId);
/// <summary>
/// Creates a draft message with the given options.
/// </summary>
/// <param name="accountId">Account to create draft for.</param>
/// <param name="options">Draft creation options.</param>
/// <returns>
/// Base64 encoded string of MimeMessage object.
/// This is mainly for serialization purposes.
/// </returns>
Task<string> CreateDraftMimeBase64Async(Guid accountId, DraftCreationOptions options);
Task UpdateMailAsync(MailCopy mailCopy); Task UpdateMailAsync(MailCopy mailCopy);
/// <summary> /// <summary>
@@ -106,9 +94,18 @@ namespace Wino.Core.Domain.Interfaces
/// Checks whether the mail exists in the folder. /// Checks whether the mail exists in the folder.
/// When deciding Create or Update existing mail, we need to check if the mail exists in the folder. /// When deciding Create or Update existing mail, we need to check if the mail exists in the folder.
/// </summary> /// </summary>
/// <param name="messageId">Message id</param> /// <param name="mailCopyId">MailCopy id</param>
/// <param name="folderId">Folder's local id.</param> /// <param name="folderId">Folder's local id.</param>
/// <returns>Whether mail exists in the folder or not.</returns> /// <returns>Whether mail exists in the folder or not.</returns>
Task<bool> IsMailExistsAsync(string mailCopyId, Guid folderId); Task<bool> IsMailExistsAsync(string mailCopyId, Guid folderId);
/// <summary>
/// Creates a draft MailCopy and MimeMessage based on the given options.
/// For forward/reply it would include the referenced message.
/// </summary>
/// <param name="composerAccount">Account which should have new draft.</param>
/// <param name="draftCreationOptions">Options like new email/forward/draft.</param>
/// <returns>Draft MailCopy and Draft MimeMessage as base64.</returns>
Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(MailAccount composerAccount, DraftCreationOptions draftCreationOptions);
} }
} }

View File

@@ -16,7 +16,7 @@ namespace Wino.Core.Domain.Interfaces
/// Queues new draft creation request for synchronizer. /// Queues new draft creation request for synchronizer.
/// </summary> /// </summary>
/// <param name="draftPreperationRequest">A class that holds the parameters for creating a draft.</param> /// <param name="draftPreperationRequest">A class that holds the parameters for creating a draft.</param>
Task ExecuteAsync(DraftPreperationRequest draftPreperationRequest); Task ExecuteAsync(DraftPreparationRequest draftPreperationRequest);
/// <summary> /// <summary>
/// Queues a new request for synchronizer to send a draft. /// Queues a new request for synchronizer to send a draft.

View File

@@ -0,0 +1,76 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Wino.Core.Domain.Models.Launch;
public class MailToUri
{
public string Subject { get; private set; }
public string Body { get; private set; }
public List<string> To { get; } = [];
public List<string> Cc { get; } = [];
public List<string> Bcc { get; } = [];
public Dictionary<string, string> OtherParameters { get; } = [];
public MailToUri(string mailToUrl)
{
ParseMailToUrl(mailToUrl);
}
private void ParseMailToUrl(string mailToUrl)
{
if (string.IsNullOrWhiteSpace(mailToUrl))
throw new ArgumentException("mailtoUrl cannot be null or empty.", nameof(mailToUrl));
if (!mailToUrl.StartsWith("mailto:", StringComparison.OrdinalIgnoreCase))
throw new ArgumentException("URL must start with 'mailto:'.", nameof(mailToUrl));
var mailToWithoutScheme = mailToUrl.Substring(7); // Remove "mailto:"
var components = mailToWithoutScheme.Split('?');
if (!string.IsNullOrEmpty(components[0]))
{
To.AddRange(components[0].Split(',').Select(email => HttpUtility.UrlDecode(email).Trim()));
}
if (components.Length <= 1)
{
return;
}
var parameters = components[1].Split('&');
foreach (var parameter in parameters)
{
var keyValue = parameter.Split('=');
if (keyValue.Length != 2)
continue;
var key = keyValue[0].ToLowerInvariant();
var value = HttpUtility.UrlDecode(keyValue[1]);
switch (key)
{
case "to":
To.AddRange(value.Split(',').Select(email => email.Trim()));
break;
case "subject":
Subject = value;
break;
case "body":
Body = value;
break;
case "cc":
Cc.AddRange(value.Split(',').Select(email => email.Trim()));
break;
case "bcc":
Bcc.AddRange(value.Split(',').Select(email => email.Trim()));
break;
default:
OtherParameters[key] = value;
break;
}
}
}
}

View File

@@ -1,42 +1,27 @@
using System.Collections.Specialized; using MimeKit;
using System.Linq;
using System.Text.Json.Serialization;
using MimeKit;
using Wino.Core.Domain.Entities; using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums; using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Models.Launch;
namespace Wino.Core.Domain.Models.MailItem namespace Wino.Core.Domain.Models.MailItem;
public class DraftCreationOptions
{ {
public class DraftCreationOptions public DraftCreationReason Reason { get; set; }
{
[JsonIgnore]
public MimeMessage ReferenceMimeMessage { get; set; }
public MailCopy ReferenceMailCopy { get; set; }
public DraftCreationReason Reason { get; set; }
#region Mailto Protocol Related Stuff /// <summary>
/// Used for forward/reply
/// </summary>
public ReferencedMessage ReferencedMessage { get; set; }
public const string MailtoSubjectParameterKey = "subject"; /// <summary>
public const string MailtoBodyParameterKey = "body"; /// Used to create mails from Mailto links
public const string MailtoToParameterKey = "mailto"; /// </summary>
public const string MailtoCCParameterKey = "cc"; public MailToUri MailToUri { get; set; }
public const string MailtoBCCParameterKey = "bcc"; }
public NameValueCollection MailtoParameters { get; set; } public class ReferencedMessage
{
private bool IsMailtoParameterExists(string parameterKey) public MailCopy MailCopy { get; set; }
=> MailtoParameters != null public MimeMessage MimeMessage { get; set; }
&& MailtoParameters.AllKeys.Contains(parameterKey);
public bool TryGetMailtoValue(string key, out string value)
{
bool valueExists = IsMailtoParameterExists(key);
value = valueExists ? MailtoParameters[key] : string.Empty;
return valueExists;
}
#endregion
}
} }

View File

@@ -0,0 +1,48 @@
using System;
using System.Text.Json.Serialization;
using MimeKit;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Extensions;
namespace Wino.Core.Domain.Models.MailItem;
public class DraftPreparationRequest
{
public DraftPreparationRequest(MailAccount account, MailCopy createdLocalDraftCopy, string base64EncodedMimeMessage, MailCopy referenceMailCopy = null)
{
Account = account ?? throw new ArgumentNullException(nameof(account));
CreatedLocalDraftCopy = createdLocalDraftCopy ?? throw new ArgumentNullException(nameof(createdLocalDraftCopy));
ReferenceMailCopy = referenceMailCopy;
// MimeMessage is not serializable with System.Text.Json. Convert to base64 string.
// This is additional work when deserialization needed, but not much to do atm.
Base64LocalDraftMimeMessage = base64EncodedMimeMessage;
}
[JsonConstructor]
private DraftPreparationRequest() { }
public MailCopy CreatedLocalDraftCopy { get; set; }
public MailCopy ReferenceMailCopy { get; set; }
public string Base64LocalDraftMimeMessage { get; set; }
[JsonIgnore]
private MimeMessage createdLocalDraftMimeMessage;
[JsonIgnore]
public MimeMessage CreatedLocalDraftMimeMessage
{
get
{
createdLocalDraftMimeMessage ??= Base64LocalDraftMimeMessage.GetMimeMessageFromBase64();
return createdLocalDraftMimeMessage;
}
}
public MailAccount Account { get; }
}

View File

@@ -1,49 +0,0 @@
using System;
using System.Text.Json.Serialization;
using MimeKit;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Extensions;
namespace Wino.Core.Domain.Models.MailItem
{
public class DraftPreperationRequest : DraftCreationOptions
{
public DraftPreperationRequest(MailAccount account, MailCopy createdLocalDraftCopy, string base64EncodedMimeMessage)
{
Account = account ?? throw new ArgumentNullException(nameof(account));
CreatedLocalDraftCopy = createdLocalDraftCopy ?? throw new ArgumentNullException(nameof(createdLocalDraftCopy));
// MimeMessage is not serializable with System.Text.Json. Convert to base64 string.
// This is additional work when deserialization needed, but not much to do atm.
Base64LocalDraftMimeMessage = base64EncodedMimeMessage;
}
[JsonConstructor]
private DraftPreperationRequest() { }
public MailCopy CreatedLocalDraftCopy { get; set; }
public string Base64LocalDraftMimeMessage { get; set; }
[JsonIgnore]
private MimeMessage createdLocalDraftMimeMessage;
[JsonIgnore]
public MimeMessage CreatedLocalDraftMimeMessage
{
get
{
if (createdLocalDraftMimeMessage == null)
{
createdLocalDraftMimeMessage = Base64LocalDraftMimeMessage.GetMimeMessageFromBase64();
}
return createdLocalDraftMimeMessage;
}
}
public MailAccount Account { get; }
}
}

View File

@@ -11,7 +11,7 @@ using Wino.Messaging.UI;
namespace Wino.Core.Requests namespace Wino.Core.Requests
{ {
public record CreateDraftRequest(DraftPreperationRequest DraftPreperationRequest) public record CreateDraftRequest(DraftPreparationRequest DraftPreperationRequest)
: RequestBase<BatchCreateDraftRequest>(DraftPreperationRequest.CreatedLocalDraftCopy, MailSynchronizerOperation.CreateDraft), : RequestBase<BatchCreateDraftRequest>(DraftPreperationRequest.CreatedLocalDraftCopy, MailSynchronizerOperation.CreateDraft),
ICustomFolderSynchronizationRequest ICustomFolderSynchronizationRequest
{ {
@@ -36,7 +36,7 @@ namespace Wino.Core.Requests
} }
[EditorBrowsable(EditorBrowsableState.Never)] [EditorBrowsable(EditorBrowsableState.Never)]
public record class BatchCreateDraftRequest(IEnumerable<IRequest> Items, DraftPreperationRequest DraftPreperationRequest) public record class BatchCreateDraftRequest(IEnumerable<IRequest> Items, DraftPreparationRequest DraftPreperationRequest)
: BatchRequestBase(Items, MailSynchronizerOperation.CreateDraft) : BatchRequestBase(Items, MailSynchronizerOperation.CreateDraft)
{ {
public override void ApplyUIChanges() public override void ApplyUIChanges()

View File

@@ -0,0 +1,10 @@
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Launch;
namespace Wino.Core.Services;
public class LaunchProtocolService : ILaunchProtocolService
{
public object LaunchParameter { get; set; }
public MailToUri MailToUri { get; set; }
}

View File

@@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.Kiota.Abstractions.Extensions; using Microsoft.Kiota.Abstractions.Extensions;
@@ -32,7 +31,6 @@ namespace Wino.Core.Services
private readonly IMimeFileService _mimeFileService; private readonly IMimeFileService _mimeFileService;
private readonly IPreferencesService _preferencesService; private readonly IPreferencesService _preferencesService;
private readonly ILogger _logger = Log.ForContext<MailService>(); private readonly ILogger _logger = Log.ForContext<MailService>();
public MailService(IDatabaseService databaseService, public MailService(IDatabaseService databaseService,
@@ -53,18 +51,9 @@ namespace Wino.Core.Services
_preferencesService = preferencesService; _preferencesService = preferencesService;
} }
public async Task<MailCopy> CreateDraftAsync(MailAccount composerAccount, public async Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(MailAccount composerAccount, DraftCreationOptions draftCreationOptions)
string generatedReplyMimeMessageBase64,
MimeMessage replyingMimeMessage = null,
IMailItem replyingMailItem = null)
{ {
var createdDraftMimeMessage = generatedReplyMimeMessageBase64.GetMimeMessageFromBase64(); var createdDraftMimeMessage = await CreateDraftMimeAsync(composerAccount, draftCreationOptions);
bool isImapAccount = composerAccount.ServerInformation != null;
string fromName;
fromName = composerAccount.SenderName;
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(composerAccount.Id, SpecialFolderType.Draft); var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(composerAccount.Id, SpecialFolderType.Draft);
@@ -78,7 +67,7 @@ namespace Wino.Core.Services
Id = Guid.NewGuid().ToString(), // This will be replaced after network call with the remote draft id. Id = Guid.NewGuid().ToString(), // This will be replaced after network call with the remote draft id.
CreationDate = DateTime.UtcNow, CreationDate = DateTime.UtcNow,
FromAddress = composerAccount.Address, FromAddress = composerAccount.Address,
FromName = fromName, FromName = composerAccount.SenderName,
HasAttachments = false, HasAttachments = false,
Importance = MailImportance.Normal, Importance = MailImportance.Normal,
Subject = createdDraftMimeMessage.Subject, Subject = createdDraftMimeMessage.Subject,
@@ -93,28 +82,25 @@ namespace Wino.Core.Services
}; };
// If replying, add In-Reply-To, ThreadId and References. // If replying, add In-Reply-To, ThreadId and References.
bool isReplying = replyingMimeMessage != null; if (draftCreationOptions.ReferencedMessage != null)
if (isReplying)
{ {
if (replyingMimeMessage.References != null) if (draftCreationOptions.ReferencedMessage.MimeMessage.References != null)
copy.References = string.Join(",", replyingMimeMessage.References); copy.References = string.Join(",", draftCreationOptions.ReferencedMessage.MimeMessage.References);
if (!string.IsNullOrEmpty(replyingMimeMessage.MessageId)) if (!string.IsNullOrEmpty(draftCreationOptions.ReferencedMessage.MimeMessage.MessageId))
copy.InReplyTo = replyingMimeMessage.MessageId; copy.InReplyTo = draftCreationOptions.ReferencedMessage.MimeMessage.MessageId;
if (!string.IsNullOrEmpty(replyingMailItem?.ThreadId)) if (!string.IsNullOrEmpty(draftCreationOptions.ReferencedMessage.MailCopy?.ThreadId))
copy.ThreadId = replyingMailItem.ThreadId; copy.ThreadId = draftCreationOptions.ReferencedMessage.MailCopy.ThreadId;
} }
await Connection.InsertAsync(copy); await Connection.InsertAsync(copy);
await _mimeFileService.SaveMimeMessageAsync(copy.FileId, createdDraftMimeMessage, composerAccount.Id); await _mimeFileService.SaveMimeMessageAsync(copy.FileId, createdDraftMimeMessage, composerAccount.Id);
ReportUIChange(new DraftCreated(copy, composerAccount)); ReportUIChange(new DraftCreated(copy, composerAccount));
return copy; return (copy, createdDraftMimeMessage.GetBase64MimeMessage());
} }
public async Task<List<MailCopy>> GetMailsByFolderIdAsync(Guid folderId) public async Task<List<MailCopy>> GetMailsByFolderIdAsync(Guid folderId)
@@ -629,85 +615,45 @@ namespace Wino.Core.Services
} }
} }
public async Task<string> CreateDraftMimeBase64Async(Guid accountId, DraftCreationOptions draftCreationOptions) private async Task<MimeMessage> CreateDraftMimeAsync(MailAccount account, DraftCreationOptions draftCreationOptions)
{ {
// This unique id is stored in mime headers for Wino to identify remote message with local copy. // This unique id is stored in mime headers for Wino to identify remote message with local copy.
// Same unique id will be used for the local copy as well. // Same unique id will be used for the local copy as well.
// Synchronizer will map this unique id to the local draft copy after synchronization. // Synchronizer will map this unique id to the local draft copy after synchronization.
var messageUniqueId = Guid.NewGuid();
var message = new MimeMessage() var message = new MimeMessage()
{ {
Headers = { { Constants.WinoLocalDraftHeader, messageUniqueId.ToString() } } Headers = { { Constants.WinoLocalDraftHeader, Guid.NewGuid().ToString() } },
From = { new MailboxAddress(account.SenderName, account.Address) }
}; };
var builder = new BodyBuilder(); var builder = new BodyBuilder();
var account = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false); var signature = await GetSignature(account, draftCreationOptions.Reason);
if (account == null) _ = draftCreationOptions.Reason switch
{ {
_logger.Warning("Can't create draft mime message because account {AccountId} does not exist.", accountId); DraftCreationReason.Empty => CreateEmptyDraft(builder, message, draftCreationOptions, signature),
_ => CreateReferencedDraft(builder, message, draftCreationOptions, account, signature),
};
return null; if (!string.IsNullOrEmpty(builder.HtmlBody))
{
builder.TextBody = HtmlAgilityPackExtensions.GetPreviewText(builder.HtmlBody);
} }
var reason = draftCreationOptions.Reason; message.Body = builder.ToMessageBody();
var referenceMessage = draftCreationOptions.ReferenceMimeMessage;
message.From.Add(new MailboxAddress(account.SenderName, account.Address)); return message;
}
// It contains empty blocks with inlined font, to make sure when users starts typing,it will follow selected font. private string CreateHtmlGap()
var gapHtml = CreateHtmlGap(); {
var template = $"""<div style="font-family: '{_preferencesService.ComposerFont}', Arial, sans-serif; font-size: {_preferencesService.ComposerFontSize}px"><br></div>""";
return string.Concat(Enumerable.Repeat(template, 2));
}
// Manage "To" private async Task<string> GetSignature(MailAccount account, DraftCreationReason reason)
if (reason == DraftCreationReason.Reply || reason == DraftCreationReason.ReplyAll) {
{
// Reply to the sender of the message
if (referenceMessage.ReplyTo.Count > 0)
message.To.AddRange(referenceMessage.ReplyTo);
else if (referenceMessage.From.Count > 0)
message.To.AddRange(referenceMessage.From);
else if (referenceMessage.Sender != null)
message.To.Add(referenceMessage.Sender);
if (reason == DraftCreationReason.ReplyAll)
{
// Include all of the other original recipients
message.To.AddRange(referenceMessage.To);
// Find self and remove
var self = message.To.FirstOrDefault(a => a is MailboxAddress mailboxAddress && mailboxAddress.Address == account.Address);
if (self != null)
message.To.Remove(self);
message.Cc.AddRange(referenceMessage.Cc);
}
// Manage "ThreadId-ConversationId"
if (!string.IsNullOrEmpty(referenceMessage.MessageId))
{
message.InReplyTo = referenceMessage.MessageId;
message.References.AddRange(referenceMessage.References);
message.References.Add(referenceMessage.MessageId);
}
message.Headers.Add("Thread-Topic", referenceMessage.Subject);
builder.HtmlBody = CreateHtmlForReferencingMessage(referenceMessage);
}
if (reason == DraftCreationReason.Forward)
{
builder.HtmlBody = CreateHtmlForReferencingMessage(referenceMessage);
}
// Append signatures if needed.
if (account.Preferences.IsSignatureEnabled) if (account.Preferences.IsSignatureEnabled)
{ {
var signatureId = reason == DraftCreationReason.Empty ? var signatureId = reason == DraftCreationReason.Empty ?
@@ -718,26 +664,88 @@ namespace Wino.Core.Services
{ {
var signature = await _signatureService.GetSignatureAsync(signatureId.Value); var signature = await _signatureService.GetSignatureAsync(signatureId.Value);
if (string.IsNullOrWhiteSpace(builder.HtmlBody)) return signature.HtmlBody;
{
builder.HtmlBody = $"{gapHtml}{signature.HtmlBody}";
}
else
{
builder.HtmlBody = $"{gapHtml}{signature.HtmlBody}{gapHtml}{builder.HtmlBody}";
}
} }
} }
else
return null;
}
private MimeMessage CreateEmptyDraft(BodyBuilder builder, MimeMessage message, DraftCreationOptions draftCreationOptions, string signature)
{
builder.HtmlBody = CreateHtmlGap();
if (draftCreationOptions.MailToUri != null)
{ {
builder.HtmlBody = $"{gapHtml}{builder.HtmlBody}"; if (draftCreationOptions.MailToUri.Subject != null)
message.Subject = draftCreationOptions.MailToUri.Subject;
if (draftCreationOptions.MailToUri.Body != null)
{
builder.HtmlBody = $"""<div style="font-family: '{_preferencesService.ComposerFont}', Arial, sans-serif; font-size: {_preferencesService.ComposerFontSize}px">{draftCreationOptions.MailToUri.Body}</div>""" + builder.HtmlBody;
}
if (draftCreationOptions.MailToUri.To.Any())
message.To.AddRange(draftCreationOptions.MailToUri.To.Select(x => new MailboxAddress(x, x)));
if (draftCreationOptions.MailToUri.Cc.Any())
message.Cc.AddRange(draftCreationOptions.MailToUri.Cc.Select(x => new MailboxAddress(x, x)));
if (draftCreationOptions.MailToUri.Bcc.Any())
message.Bcc.AddRange(draftCreationOptions.MailToUri.Bcc.Select(x => new MailboxAddress(x, x)));
}
if (signature != null)
builder.HtmlBody += signature;
return message;
}
private MimeMessage CreateReferencedDraft(BodyBuilder builder, MimeMessage message, DraftCreationOptions draftCreationOptions, MailAccount account, string signature)
{
var reason = draftCreationOptions.Reason;
var referenceMessage = draftCreationOptions.ReferencedMessage.MimeMessage;
var gap = CreateHtmlGap();
builder.HtmlBody = gap + CreateHtmlForReferencingMessage(referenceMessage);
if (signature != null)
{
builder.HtmlBody = gap + signature + builder.HtmlBody;
}
// Manage "To"
if (reason == DraftCreationReason.Reply || reason == DraftCreationReason.ReplyAll)
{
// Reply to the sender of the message
if (referenceMessage.ReplyTo.Count > 0)
message.To.AddRange(referenceMessage.ReplyTo);
else if (referenceMessage.From.Count > 0)
message.To.AddRange(referenceMessage.From);
else if (referenceMessage.Sender != null)
message.To.Add(referenceMessage.Sender);
if (reason == DraftCreationReason.ReplyAll)
{
// Include all of the other original recipients
message.To.AddRange(referenceMessage.To.Where(x => x is MailboxAddress mailboxAddress && !mailboxAddress.Address.Equals(account.Address, StringComparison.OrdinalIgnoreCase)));
message.Cc.AddRange(referenceMessage.Cc.Where(x => x is MailboxAddress mailboxAddress && !mailboxAddress.Address.Equals(account.Address, StringComparison.OrdinalIgnoreCase)));
}
// Manage "ThreadId-ConversationId"
if (!string.IsNullOrEmpty(referenceMessage.MessageId))
{
message.InReplyTo = referenceMessage.MessageId;
message.References.AddRange(referenceMessage.References);
message.References.Add(referenceMessage.MessageId);
}
message.Headers.Add("Thread-Topic", referenceMessage.Subject);
} }
// Manage Subject // Manage Subject
if (reason == DraftCreationReason.Forward && !referenceMessage.Subject.StartsWith("FW: ", StringComparison.OrdinalIgnoreCase)) if (reason == DraftCreationReason.Forward && !referenceMessage.Subject.StartsWith("FW: ", StringComparison.OrdinalIgnoreCase))
message.Subject = $"FW: {referenceMessage.Subject}"; message.Subject = $"FW: {referenceMessage.Subject}";
else if ((reason == DraftCreationReason.Reply || reason == DraftCreationReason.ReplyAll) && else if ((reason == DraftCreationReason.Reply || reason == DraftCreationReason.ReplyAll) && !referenceMessage.Subject.StartsWith("RE: ", StringComparison.OrdinalIgnoreCase))
!referenceMessage.Subject.StartsWith("RE: ", StringComparison.OrdinalIgnoreCase))
message.Subject = $"RE: {referenceMessage.Subject}"; message.Subject = $"RE: {referenceMessage.Subject}";
else if (referenceMessage != null) else if (referenceMessage != null)
message.Subject = referenceMessage.Subject; message.Subject = referenceMessage.Subject;
@@ -751,63 +759,7 @@ namespace Wino.Core.Services
} }
} }
if (!string.IsNullOrEmpty(builder.HtmlBody)) return message;
{
builder.TextBody = HtmlAgilityPackExtensions.GetPreviewText(builder.HtmlBody);
}
message.Body = builder.ToMessageBody();
// Apply mail-to protocol parameters if exists.
if (draftCreationOptions.MailtoParameters != null)
{
if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoSubjectParameterKey, out string subjectParameter))
message.Subject = subjectParameter;
if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoBodyParameterKey, out string bodyParameter))
{
builder.TextBody = bodyParameter;
builder.HtmlBody = bodyParameter;
message.Body = builder.ToMessageBody();
}
static InternetAddressList ExtractRecipients(string parameterValue)
{
var list = new InternetAddressList();
var splittedRecipients = parameterValue.Split(',');
foreach (var recipient in splittedRecipients)
list.Add(new MailboxAddress(recipient, recipient));
return list;
}
if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoToParameterKey, out string toParameter))
message.To.AddRange(ExtractRecipients(toParameter));
if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoCCParameterKey, out string ccParameter))
message.Cc.AddRange(ExtractRecipients(ccParameter));
if (draftCreationOptions.TryGetMailtoValue(DraftCreationOptions.MailtoBCCParameterKey, out string bccParameter))
message.Bcc.AddRange(ExtractRecipients(bccParameter));
}
else
{
// Update TextBody from existing HtmlBody if exists.
}
using MemoryStream memoryStream = new();
message.WriteTo(FormatOptions.Default, memoryStream);
byte[] buffer = memoryStream.GetBuffer();
int count = (int)memoryStream.Length;
return Convert.ToBase64String(buffer);
// return message;
// Generates html representation of To/Cc/From/Time and so on from referenced message. // Generates html representation of To/Cc/From/Time and so on from referenced message.
string CreateHtmlForReferencingMessage(MimeMessage referenceMessage) string CreateHtmlForReferencingMessage(MimeMessage referenceMessage)
@@ -820,28 +772,22 @@ namespace Wino.Core.Services
visitor.Visit(referenceMessage); visitor.Visit(referenceMessage);
htmlMimeInfo += $""" htmlMimeInfo += $"""
<div id="divRplyFwdMsg" dir="ltr"> <div id="divRplyFwdMsg" dir="ltr">
<font face="Calibri, sans-serif" style="font-size: 11pt;" color="#000000"> <font face="Calibri, sans-serif" style="font-size: 11pt;" color="#000000">
<b>From:</b> {ParticipantsToHtml(referenceMessage.From)}<br> <b>From:</b> {ParticipantsToHtml(referenceMessage.From)}<br>
<b>Sent:</b> {referenceMessage.Date.ToLocalTime()}<br> <b>Sent:</b> {referenceMessage.Date.ToLocalTime()}<br>
<b>To:</b> {ParticipantsToHtml(referenceMessage.To)}<br> <b>To:</b> {ParticipantsToHtml(referenceMessage.To)}<br>
{(referenceMessage.Cc.Count > 0 ? $"<b>Cc:</b> {ParticipantsToHtml(referenceMessage.Cc)}<br>" : string.Empty)} {(referenceMessage.Cc.Count > 0 ? $"<b>Cc:</b> {ParticipantsToHtml(referenceMessage.Cc)}<br>" : string.Empty)}
<b>Subject:</b> {referenceMessage.Subject} <b>Subject:</b> {referenceMessage.Subject}
</font> </font>
<div>&nbsp;</div> <div>&nbsp;</div>
{visitor.HtmlBody} {visitor.HtmlBody}
</div> </div>
"""; """;
return htmlMimeInfo; return htmlMimeInfo;
} }
string CreateHtmlGap()
{
var template = $"""<div style="font-family: '{_preferencesService.ComposerFont}', Arial, sans-serif; font-size: {_preferencesService.ComposerFontSize}px"><br></div>""";
return string.Concat(Enumerable.Repeat(template, 5));
}
static string ParticipantsToHtml(InternetAddressList internetAddresses) => static string ParticipantsToHtml(InternetAddressList internetAddresses) =>
string.Join("; ", internetAddresses.Mailboxes string.Join("; ", internetAddresses.Mailboxes
.Select(x => $"{x.Name ?? Translator.UnknownSender} &lt;<a href=\"mailto:{x.Address ?? Translator.UnknownAddress}\">{x.Address ?? Translator.UnknownAddress}</a>&gt;")); .Select(x => $"{x.Name ?? Translator.UnknownSender} &lt;<a href=\"mailto:{x.Address ?? Translator.UnknownAddress}\">{x.Address ?? Translator.UnknownAddress}</a>&gt;"));

View File

@@ -111,7 +111,7 @@ namespace Wino.Core.Services
QueueSynchronization(accountId); QueueSynchronization(accountId);
} }
public async Task ExecuteAsync(DraftPreperationRequest draftPreperationRequest) public async Task ExecuteAsync(DraftPreparationRequest draftPreperationRequest)
{ {
var request = new CreateDraftRequest(draftPreperationRequest); var request = new CreateDraftRequest(draftPreperationRequest);

View File

@@ -302,7 +302,7 @@ namespace Wino.Mail.ViewModels
} }
else else
{ {
bool hasMailtoActivation = _launchProtocolService.MailtoParameters != null; bool hasMailtoActivation = _launchProtocolService.MailToUri != null;
if (hasMailtoActivation) if (hasMailtoActivation)
{ {
@@ -774,16 +774,13 @@ namespace Wino.Mail.ViewModels
var draftOptions = new DraftCreationOptions var draftOptions = new DraftCreationOptions
{ {
Reason = DraftCreationReason.Empty, Reason = DraftCreationReason.Empty,
MailToUri = _launchProtocolService.MailToUri
// Include mail to parameters for parsing mailto if any.
MailtoParameters = _launchProtocolService.MailtoParameters
}; };
var createdBase64EncodedMimeMessage = await _mailService.CreateDraftMimeBase64Async(account.Id, draftOptions).ConfigureAwait(false); var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(account, draftOptions).ConfigureAwait(false);
var createdDraftMailMessage = await _mailService.CreateDraftAsync(account, createdBase64EncodedMimeMessage).ConfigureAwait(false);
var draftPreperationRequest = new DraftPreperationRequest(account, createdDraftMailMessage, createdBase64EncodedMimeMessage); var draftPreparationRequest = new DraftPreparationRequest(account, draftMailCopy, draftBase64MimeMessage);
await _winoRequestDelegator.ExecuteAsync(draftPreperationRequest); await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest);
} }
protected override async void OnAccountUpdated(MailAccount updatedAccount) protected override async void OnAccountUpdated(MailAccount updatedAccount)

View File

@@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@@ -58,7 +57,7 @@ namespace Wino.Mail.ViewModels
private MessageImportance selectedMessageImportance; private MessageImportance selectedMessageImportance;
[ObservableProperty] [ObservableProperty]
private bool isCCBCCVisible = true; private bool isCCBCCVisible;
[ObservableProperty] [ObservableProperty]
private string subject; private string subject;
@@ -77,21 +76,20 @@ namespace Wino.Mail.ViewModels
[ObservableProperty] [ObservableProperty]
private bool isDraggingOverImagesDropZone; private bool isDraggingOverImagesDropZone;
public ObservableCollection<MailAttachmentViewModel> IncludedAttachments { get; set; } = new ObservableCollection<MailAttachmentViewModel>(); public ObservableCollection<MailAttachmentViewModel> IncludedAttachments { get; set; } = [];
public ObservableCollection<MailAccount> Accounts { get; set; } = [];
public ObservableCollection<MailAccount> Accounts { get; set; } = new ObservableCollection<MailAccount>(); public ObservableCollection<AddressInformation> ToItems { get; set; } = [];
public ObservableCollection<AddressInformation> ToItems { get; set; } = new ObservableCollection<AddressInformation>(); public ObservableCollection<AddressInformation> CCItems { get; set; } = [];
public ObservableCollection<AddressInformation> CCItemsItems { get; set; } = new ObservableCollection<AddressInformation>(); public ObservableCollection<AddressInformation> BCCItems { get; set; } = [];
public ObservableCollection<AddressInformation> BCCItems { get; set; } = new ObservableCollection<AddressInformation>();
public List<EditorToolbarSection> ToolbarSections { get; set; } = new List<EditorToolbarSection>() public List<EditorToolbarSection> ToolbarSections { get; set; } =
{ [
new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Format }, new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Format },
new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Insert }, new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Insert },
new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Draw }, new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Draw },
new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Options } new EditorToolbarSection(){ SectionType = EditorToolbarSectionType.Options }
}; ];
private EditorToolbarSection selectedToolbarSection; private EditorToolbarSection selectedToolbarSection;
@@ -190,7 +188,7 @@ namespace Wino.Mail.ViewModels
// Save recipients. // Save recipients.
SaveAddressInfo(ToItems, CurrentMimeMessage.To); SaveAddressInfo(ToItems, CurrentMimeMessage.To);
SaveAddressInfo(CCItemsItems, CurrentMimeMessage.Cc); SaveAddressInfo(CCItems, CurrentMimeMessage.Cc);
SaveAddressInfo(BCCItems, CurrentMimeMessage.Bcc); SaveAddressInfo(BCCItems, CurrentMimeMessage.Bcc);
SaveImportance(); SaveImportance();
@@ -239,12 +237,7 @@ namespace Wino.Mail.ViewModels
{ {
if (GetHTMLBodyFunction != null) if (GetHTMLBodyFunction != null)
{ {
var htmlBody = await GetHTMLBodyFunction(); bodyBuilder.HtmlBody = await GetHTMLBodyFunction();
if (!string.IsNullOrEmpty(htmlBody))
{
bodyBuilder.HtmlBody = Regex.Unescape(htmlBody);
}
} }
if (!string.IsNullOrEmpty(bodyBuilder.HtmlBody)) if (!string.IsNullOrEmpty(bodyBuilder.HtmlBody))
@@ -309,7 +302,7 @@ namespace Wino.Mail.ViewModels
// Check if there is any delivering mail address from protocol launch. // Check if there is any delivering mail address from protocol launch.
if (_launchProtocolService.MailtoParameters != null) if (_launchProtocolService.MailToUri != null)
{ {
// TODO // TODO
//var requestedMailContact = await GetAddressInformationAsync(_launchProtocolService.MailtoParameters, ToItems); //var requestedMailContact = await GetAddressInformationAsync(_launchProtocolService.MailtoParameters, ToItems);
@@ -322,7 +315,7 @@ namespace Wino.Mail.ViewModels
// DialogService.InfoBarMessage("Invalid Address", "Address is not a valid e-mail address.", InfoBarMessageType.Warning); // DialogService.InfoBarMessage("Invalid Address", "Address is not a valid e-mail address.", InfoBarMessageType.Warning);
// Clear the address. // Clear the address.
_launchProtocolService.MailtoParameters = null; _launchProtocolService.MailToUri = null;
} }
} }
@@ -427,15 +420,18 @@ namespace Wino.Mail.ViewModels
// Extract information // Extract information
ToItems.Clear(); ToItems.Clear();
CCItemsItems.Clear(); CCItems.Clear();
BCCItems.Clear(); BCCItems.Clear();
LoadAddressInfo(replyingMime.To, ToItems); LoadAddressInfo(replyingMime.To, ToItems);
LoadAddressInfo(replyingMime.Cc, CCItemsItems); LoadAddressInfo(replyingMime.Cc, CCItems);
LoadAddressInfo(replyingMime.Bcc, BCCItems); LoadAddressInfo(replyingMime.Bcc, BCCItems);
LoadAttachments(replyingMime.Attachments); LoadAttachments(replyingMime.Attachments);
if (replyingMime.Cc.Any() || replyingMime.Bcc.Any())
IsCCBCCVisible = true;
Subject = replyingMime.Subject; Subject = replyingMime.Subject;
CurrentMimeMessage = replyingMime; CurrentMimeMessage = replyingMime;

View File

@@ -117,7 +117,7 @@ namespace Wino.Mail.ViewModels
#endregion #endregion
public INativeAppService NativeAppService { get; } public INativeAppService NativeAppService { get; }
public IStatePersistanceService StatePersistanceService { get; } public IStatePersistanceService StatePersistenceService { get; }
public IPreferencesService PreferencesService { get; } public IPreferencesService PreferencesService { get; }
public MailRenderingPageViewModel(IDialogService dialogService, public MailRenderingPageViewModel(IDialogService dialogService,
@@ -127,14 +127,14 @@ namespace Wino.Mail.ViewModels
Core.Domain.Interfaces.IMailService mailService, Core.Domain.Interfaces.IMailService mailService,
IFileService fileService, IFileService fileService,
IWinoRequestDelegator requestDelegator, IWinoRequestDelegator requestDelegator,
IStatePersistanceService statePersistanceService, IStatePersistanceService statePersistenceService,
IClipboardService clipboardService, IClipboardService clipboardService,
IUnsubscriptionService unsubscriptionService, IUnsubscriptionService unsubscriptionService,
IPreferencesService preferencesService, IPreferencesService preferencesService,
IWinoServerConnectionManager winoServerConnectionManager) : base(dialogService) IWinoServerConnectionManager winoServerConnectionManager) : base(dialogService)
{ {
NativeAppService = nativeAppService; NativeAppService = nativeAppService;
StatePersistanceService = statePersistanceService; StatePersistenceService = statePersistenceService;
PreferencesService = preferencesService; PreferencesService = preferencesService;
_winoServerConnectionManager = winoServerConnectionManager; _winoServerConnectionManager = winoServerConnectionManager;
_clipboardService = clipboardService; _clipboardService = clipboardService;
@@ -255,37 +255,27 @@ namespace Wino.Mail.ViewModels
if (initializedMailItemViewModel == null) return; if (initializedMailItemViewModel == null) return;
// Create new draft. // Create new draft.
var draftOptions = new DraftCreationOptions(); var draftOptions = new DraftCreationOptions()
if (operation == MailOperation.Reply)
draftOptions.Reason = DraftCreationReason.Reply;
else if (operation == MailOperation.ReplyAll)
draftOptions.Reason = DraftCreationReason.ReplyAll;
else if (operation == MailOperation.Forward)
draftOptions.Reason = DraftCreationReason.Forward;
// TODO: Separate mailto related stuff out of DraftCreationOptions and provide better
// model for draft preperation request. Right now it's a mess.
draftOptions.ReferenceMailCopy = initializedMailItemViewModel.MailCopy;
draftOptions.ReferenceMimeMessage = initializedMimeMessageInformation.MimeMessage;
var createdMimeMessage = await _mailService.CreateDraftMimeBase64Async(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false);
var createdDraftMailMessage = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount,
createdMimeMessage,
initializedMimeMessageInformation.MimeMessage,
initializedMailItemViewModel).ConfigureAwait(false);
var draftPreperationRequest = new DraftPreperationRequest(initializedMailItemViewModel.AssignedAccount,
createdDraftMailMessage,
createdMimeMessage)
{ {
ReferenceMimeMessage = initializedMimeMessageInformation.MimeMessage, Reason = operation switch
ReferenceMailCopy = initializedMailItemViewModel.MailCopy {
MailOperation.Reply => DraftCreationReason.Reply,
MailOperation.ReplyAll => DraftCreationReason.ReplyAll,
MailOperation.Forward => DraftCreationReason.Forward,
_ => DraftCreationReason.Empty
},
ReferencedMessage = new ReferencedMessage()
{
MimeMessage = initializedMimeMessageInformation.MimeMessage,
MailCopy = initializedMailItemViewModel.MailCopy
}
}; };
await _requestDelegator.ExecuteAsync(draftPreperationRequest); var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount, draftOptions).ConfigureAwait(false);
var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.AssignedAccount, draftMailCopy, draftBase64MimeMessage, initializedMailItemViewModel.MailCopy);
await _requestDelegator.ExecuteAsync(draftPreparationRequest);
} }
else if (initializedMailItemViewModel != null) else if (initializedMailItemViewModel != null)
@@ -453,7 +443,7 @@ namespace Wino.Mail.ViewModels
OnPropertyChanged(nameof(IsImageRenderingDisabled)); OnPropertyChanged(nameof(IsImageRenderingDisabled));
StatePersistanceService.IsReadingMail = true; StatePersistenceService.IsReadingMail = true;
}); });
} }
@@ -477,7 +467,7 @@ namespace Wino.Mail.ViewModels
Attachments.Clear(); Attachments.Clear();
MenuItems.Clear(); MenuItems.Clear();
StatePersistanceService.IsReadingMail = false; StatePersistenceService.IsReadingMail = false;
} }
private void LoadAddressInfo(InternetAddressList list, ObservableCollection<AddressInformation> collection) private void LoadAddressInfo(InternetAddressList list, ObservableCollection<AddressInformation> collection)

View File

@@ -1,8 +1,8 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web;
using CommunityToolkit.Mvvm.Messaging; using CommunityToolkit.Mvvm.Messaging;
using Windows.ApplicationModel.Activation; using Windows.ApplicationModel.Activation;
using Wino.Core.Domain.Interfaces; using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Launch;
using Wino.Messaging.Client.Authorization; using Wino.Messaging.Client.Authorization;
using Wino.Messaging.Client.Shell; using Wino.Messaging.Client.Shell;
@@ -36,11 +36,7 @@ namespace Wino.Activation
else if (protocolString.StartsWith(MailtoProtocolTag)) else if (protocolString.StartsWith(MailtoProtocolTag))
{ {
// mailto activation. Try to parse params. // mailto activation. Try to parse params.
_launchProtocolService.MailToUri = new MailToUri(protocolString);
var replaced = protocolString.Replace(MailtoProtocolTag, "mailto=");
replaced = Wino.Core.Extensions.StringExtensions.ReplaceFirst(replaced, "?", "&");
_launchProtocolService.MailtoParameters = HttpUtility.ParseQueryString(replaced);
if (_nativeAppService.IsAppRunning()) if (_nativeAppService.IsAppRunning())
{ {

View File

@@ -1,11 +0,0 @@
using System.Collections.Specialized;
using Wino.Core.Domain.Interfaces;
namespace Wino.Core.UWP.Services
{
public class LaunchProtocolService : ILaunchProtocolService
{
public object LaunchParameter { get; set; }
public NameValueCollection MailtoParameters { get; set; }
}
}

View File

@@ -493,7 +493,7 @@
VerticalAlignment="Center" VerticalAlignment="Center"
Click="ShowCCBCCClicked" Click="ShowCCBCCClicked"
GotFocus="CCBBCGotFocus" GotFocus="CCBBCGotFocus"
Visibility="{x:Bind ViewModel.IsCCBCCVisible, Mode=OneWay}"> Visibility="{x:Bind helpers:XamlHelpers.ReverseBoolToVisibilityConverter(ViewModel.IsCCBCCVisible), Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="6"> <StackPanel Orientation="Horizontal" Spacing="6">
<PathIcon <PathIcon
HorizontalAlignment="Center" HorizontalAlignment="Center"
@@ -512,13 +512,14 @@
HorizontalAlignment="Right" HorizontalAlignment="Right"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="Cc: " Text="Cc: "
Visibility="{x:Bind helpers:XamlHelpers.ReverseVisibilityConverter(CCBCCShowButton.Visibility), Mode=OneWay}" /> Visibility="{x:Bind ViewModel.IsCCBCCVisible, Mode=OneWay}" />
<controls1:TokenizingTextBox <controls1:TokenizingTextBox
x:Name="CCBox" x:Name="CCBox"
Grid.Row="2" Grid.Row="2"
Grid.Column="1" Grid.Column="1"
VerticalAlignment="Center" VerticalAlignment="Center"
ItemsSource="{x:Bind ViewModel.CCItems, Mode=OneTime}"
LostFocus="AddressBoxLostFocus" LostFocus="AddressBoxLostFocus"
PlaceholderText="{x:Bind domain:Translator.ComposerToPlaceholder}" PlaceholderText="{x:Bind domain:Translator.ComposerToPlaceholder}"
SuggestedItemTemplate="{StaticResource SuggestionBoxTemplate}" SuggestedItemTemplate="{StaticResource SuggestionBoxTemplate}"
@@ -526,7 +527,7 @@
TokenDelimiter=";" TokenDelimiter=";"
TokenItemAdding="TokenItemAdding" TokenItemAdding="TokenItemAdding"
TokenItemTemplate="{StaticResource TokenBoxTemplate}" TokenItemTemplate="{StaticResource TokenBoxTemplate}"
Visibility="{x:Bind helpers:XamlHelpers.ReverseVisibilityConverter(CCBCCShowButton.Visibility), Mode=OneWay}" /> Visibility="{x:Bind ViewModel.IsCCBCCVisible, Mode=OneWay}" />
<TextBlock <TextBlock
x:Name="BccTextBlock" x:Name="BccTextBlock"
@@ -534,13 +535,14 @@
HorizontalAlignment="Right" HorizontalAlignment="Right"
VerticalAlignment="Center" VerticalAlignment="Center"
Text="Bcc: " Text="Bcc: "
Visibility="{x:Bind helpers:XamlHelpers.ReverseVisibilityConverter(CCBCCShowButton.Visibility), Mode=OneWay}" /> Visibility="{x:Bind ViewModel.IsCCBCCVisible, Mode=OneWay}" />
<controls1:TokenizingTextBox <controls1:TokenizingTextBox
x:Name="BccBox" x:Name="BccBox"
Grid.Row="3" Grid.Row="3"
Grid.Column="1" Grid.Column="1"
VerticalAlignment="Center" VerticalAlignment="Center"
ItemsSource="{x:Bind ViewModel.BCCItems, Mode=OneTime}"
LostFocus="AddressBoxLostFocus" LostFocus="AddressBoxLostFocus"
PlaceholderText="{x:Bind domain:Translator.ComposerToPlaceholder}" PlaceholderText="{x:Bind domain:Translator.ComposerToPlaceholder}"
SuggestedItemTemplate="{StaticResource SuggestionBoxTemplate}" SuggestedItemTemplate="{StaticResource SuggestionBoxTemplate}"
@@ -548,7 +550,7 @@
TokenDelimiter=";" TokenDelimiter=";"
TokenItemAdding="TokenItemAdding" TokenItemAdding="TokenItemAdding"
TokenItemTemplate="{StaticResource TokenBoxTemplate}" TokenItemTemplate="{StaticResource TokenBoxTemplate}"
Visibility="{x:Bind helpers:XamlHelpers.ReverseVisibilityConverter(CCBCCShowButton.Visibility), Mode=OneWay}" /> Visibility="{x:Bind ViewModel.IsCCBCCVisible, Mode=OneWay}" />
<!-- Subject --> <!-- Subject -->
<TextBlock <TextBlock

View File

@@ -562,12 +562,7 @@ namespace Wino.Views
private void ShowCCBCCClicked(object sender, RoutedEventArgs e) private void ShowCCBCCClicked(object sender, RoutedEventArgs e)
{ {
CCBCCShowButton.Visibility = Visibility.Collapsed; ViewModel.IsCCBCCVisible = true;
CCTextBlock.Visibility = Visibility.Visible;
CCBox.Visibility = Visibility.Visible;
BccTextBlock.Visibility = Visibility.Visible;
BccBox.Visibility = Visibility.Visible;
} }
private async void TokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args) private async void TokenItemAdding(TokenizingTextBox sender, TokenItemAddingEventArgs args)
@@ -591,7 +586,7 @@ namespace Wino.Views
if (boxTag == "ToBox") if (boxTag == "ToBox")
addedItem = await ViewModel.GetAddressInformationAsync(args.TokenText, ViewModel.ToItems); addedItem = await ViewModel.GetAddressInformationAsync(args.TokenText, ViewModel.ToItems);
else if (boxTag == "CCBox") else if (boxTag == "CCBox")
addedItem = await ViewModel.GetAddressInformationAsync(args.TokenText, ViewModel.CCItemsItems); addedItem = await ViewModel.GetAddressInformationAsync(args.TokenText, ViewModel.CCItems);
else if (boxTag == "BCCBox") else if (boxTag == "BCCBox")
addedItem = await ViewModel.GetAddressInformationAsync(args.TokenText, ViewModel.BCCItems); addedItem = await ViewModel.GetAddressInformationAsync(args.TokenText, ViewModel.BCCItems);
@@ -660,7 +655,7 @@ namespace Wino.Views
if (boxTag == "ToBox") if (boxTag == "ToBox")
addressCollection = ViewModel.ToItems; addressCollection = ViewModel.ToItems;
else if (boxTag == "CCBox") else if (boxTag == "CCBox")
addressCollection = ViewModel.CCItemsItems; addressCollection = ViewModel.CCItems;
else if (boxTag == "BCCBox") else if (boxTag == "BCCBox")
addressCollection = ViewModel.BCCItems; addressCollection = ViewModel.BCCItems;

View File

@@ -174,7 +174,7 @@ namespace Wino.Views
// We don't have shell initialized here. It's only standalone EML viewing. // We don't have shell initialized here. It's only standalone EML viewing.
// Shift command bar from top to adjust the design. // Shift command bar from top to adjust the design.
if (ViewModel.StatePersistanceService.ShouldShiftMailRenderingDesign) if (ViewModel.StatePersistenceService.ShouldShiftMailRenderingDesign)
RendererGridFrame.Margin = new Thickness(0, 24, 0, 0); RendererGridFrame.Margin = new Thickness(0, 24, 0, 0);
else else
RendererGridFrame.Margin = new Thickness(0, 0, 0, 0); RendererGridFrame.Margin = new Thickness(0, 0, 0, 0);

View File

@@ -335,7 +335,6 @@
<Compile Include="Selectors\RendererCommandBarItemTemplateSelector.cs" /> <Compile Include="Selectors\RendererCommandBarItemTemplateSelector.cs" />
<Compile Include="Services\ApplicationResourceManager.cs" /> <Compile Include="Services\ApplicationResourceManager.cs" />
<Compile Include="Services\DialogService.cs" /> <Compile Include="Services\DialogService.cs" />
<Compile Include="Services\LaunchProtocolService.cs" />
<Compile Include="Services\WinoNavigationService.cs" /> <Compile Include="Services\WinoNavigationService.cs" />
<Compile Include="Styles\CommandBarItems.xaml.cs"> <Compile Include="Styles\CommandBarItems.xaml.cs">
<DependentUpon>CommandBarItems.xaml</DependentUpon> <DependentUpon>CommandBarItems.xaml</DependentUpon>
@@ -892,4 +891,4 @@
<Target Name="AfterBuild"> <Target Name="AfterBuild">
</Target> </Target>
--> -->
</Project> </Project>