Improve alias capability model and Outlook alias sync
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
using System.Security.Cryptography.X509Certificates;
|
||||||
using SQLite;
|
using SQLite;
|
||||||
|
using Wino.Core.Domain;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
namespace Wino.Core.Domain.Entities.Mail;
|
namespace Wino.Core.Domain.Entities.Mail;
|
||||||
|
|
||||||
@@ -42,6 +44,16 @@ public class RemoteAccountAlias
|
|||||||
/// Used for Gmail only.
|
/// Used for Gmail only.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string AliasSenderName { get; set; }
|
public string AliasSenderName { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the alias was entered by the user or discovered from the provider.
|
||||||
|
/// </summary>
|
||||||
|
public AliasSource Source { get; set; } = AliasSource.Manual;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents Wino's confidence that the alias can be used for sending.
|
||||||
|
/// </summary>
|
||||||
|
public AliasSendCapability SendCapability { get; set; } = AliasSendCapability.Unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
public class MailAccountAlias : RemoteAccountAlias
|
public class MailAccountAlias : RemoteAccountAlias
|
||||||
@@ -70,4 +82,28 @@ public class MailAccountAlias : RemoteAccountAlias
|
|||||||
|
|
||||||
[Ignore]
|
[Ignore]
|
||||||
public ObservableCollection<X509Certificate2> Certificates { get; set; } = [];
|
public ObservableCollection<X509Certificate2> Certificates { get; set; } = [];
|
||||||
|
|
||||||
|
[Ignore]
|
||||||
|
public bool IsCapabilityConfirmed => SendCapability == AliasSendCapability.Confirmed;
|
||||||
|
|
||||||
|
[Ignore]
|
||||||
|
public bool IsCapabilityUnknown => SendCapability == AliasSendCapability.Unknown;
|
||||||
|
|
||||||
|
[Ignore]
|
||||||
|
public bool IsCapabilityDenied => SendCapability == AliasSendCapability.Denied;
|
||||||
|
|
||||||
|
[Ignore]
|
||||||
|
public string CapabilityDisplayName => SendCapability switch
|
||||||
|
{
|
||||||
|
AliasSendCapability.Confirmed => Translator.AccountAlias_Status_Confirmed,
|
||||||
|
AliasSendCapability.Denied => Translator.AccountAlias_Status_Denied,
|
||||||
|
_ => Translator.AccountAlias_Status_Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
[Ignore]
|
||||||
|
public string SourceDisplayName => Source switch
|
||||||
|
{
|
||||||
|
AliasSource.ProviderDiscovered => Translator.AccountAlias_Source_ProviderDiscovered,
|
||||||
|
_ => Translator.AccountAlias_Source_Manual
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ public class MailAccount
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets whether the account can perform AliasInformation sync type.
|
/// Gets whether the account can perform AliasInformation sync type.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail;
|
public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail || ProviderType == MailProviderType.Outlook;
|
||||||
|
|
||||||
public override string ToString() => Name;
|
public override string ToString() => Name;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
namespace Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
|
public enum AliasSendCapability
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Confirmed = 1,
|
||||||
|
Denied = 2
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
namespace Wino.Core.Domain.Enums;
|
||||||
|
|
||||||
|
public enum AliasSource
|
||||||
|
{
|
||||||
|
Manual = 0,
|
||||||
|
ProviderDiscovered = 1
|
||||||
|
}
|
||||||
@@ -147,6 +147,7 @@ public interface IAccountService
|
|||||||
/// <param name="accountId">Account id.</param>
|
/// <param name="accountId">Account id.</param>
|
||||||
/// <returns>Primary alias for the account.</returns>
|
/// <returns>Primary alias for the account.</returns>
|
||||||
Task<MailAccountAlias> GetPrimaryAccountAliasAsync(Guid accountId);
|
Task<MailAccountAlias> GetPrimaryAccountAliasAsync(Guid accountId);
|
||||||
|
Task UpdateAliasSendCapabilityAsync(Guid accountId, string aliasAddress, AliasSendCapability capability);
|
||||||
Task<bool> IsAccountFocusedEnabledAsync(Guid accountId);
|
Task<bool> IsAccountFocusedEnabledAsync(Guid accountId);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -1,9 +1,14 @@
|
|||||||
{
|
{
|
||||||
"AccountAlias_Column_Alias": "Alias",
|
"AccountAlias_Column_Alias": "Alias",
|
||||||
"AccountAlias_Column_IsPrimaryAlias": "Primary",
|
"AccountAlias_Column_IsPrimaryAlias": "Primary",
|
||||||
"AccountAlias_Column_Verified": "Verified",
|
"AccountAlias_Column_Status": "Status",
|
||||||
"AccountAlias_Disclaimer_FirstLine": "Wino can only import aliases for your Gmail accounts.",
|
"AccountAlias_Disclaimer_FirstLine": "Wino can import Gmail aliases directly and can best-effort discover Outlook aliases from Microsoft Graph.",
|
||||||
"AccountAlias_Disclaimer_SecondLine": "If you want to use aliases for your Outlook or IMAP account, please add them yourself.",
|
"AccountAlias_Disclaimer_SecondLine": "Manual aliases are still available for Outlook or IMAP when the provider can't confirm sending capability ahead of time.",
|
||||||
|
"AccountAlias_Source_Manual": "Manual",
|
||||||
|
"AccountAlias_Source_ProviderDiscovered": "Provider discovered",
|
||||||
|
"AccountAlias_Status_Confirmed": "Ready to send",
|
||||||
|
"AccountAlias_Status_Unknown": "Capability unknown",
|
||||||
|
"AccountAlias_Status_Denied": "Send denied",
|
||||||
"AccountCacheReset_Title": "Account Cache Reset",
|
"AccountCacheReset_Title": "Account Cache Reset",
|
||||||
"AccountCacheReset_Message": "This account requires full re-sychronization to continue working. Please wait while Wino re-synchronizes your messages...",
|
"AccountCacheReset_Message": "This account requires full re-sychronization to continue working. Please wait while Wino re-synchronizes your messages...",
|
||||||
"AccountContactNameYou": "You",
|
"AccountContactNameYou": "You",
|
||||||
@@ -344,6 +349,7 @@
|
|||||||
"Exception_InvalidMultiAccountMoveTarget": "You can't move multiple items that belong to different accounts in linked account.",
|
"Exception_InvalidMultiAccountMoveTarget": "You can't move multiple items that belong to different accounts in linked account.",
|
||||||
"Exception_MailProcessing": "This mail is still being processed. Please try again after few seconds.",
|
"Exception_MailProcessing": "This mail is still being processed. Please try again after few seconds.",
|
||||||
"Exception_MissingAlias": "Primary alias does not exist for this account. Creating draft failed.",
|
"Exception_MissingAlias": "Primary alias does not exist for this account. Creating draft failed.",
|
||||||
|
"Exception_AliasSendDenied_Message": "You do not have permission to send from alias {0}. Pick another alias or update your mailbox permissions.",
|
||||||
"Exception_NullAssignedAccount": "Assigned account is null",
|
"Exception_NullAssignedAccount": "Assigned account is null",
|
||||||
"Exception_NullAssignedFolder": "Assigned folder is null",
|
"Exception_NullAssignedFolder": "Assigned folder is null",
|
||||||
"Exception_SynchronizerFailureHTTP": "Response handling failed with error HTTP code {0}",
|
"Exception_SynchronizerFailureHTTP": "Response handling failed with error HTTP code {0}",
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
using FluentAssertions;
|
||||||
|
using Moq;
|
||||||
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
|
using Wino.Core.Domain.Entities.Shared;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
using Wino.Core.Tests.Helpers;
|
||||||
|
using Wino.Services;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Wino.Core.Tests.Services;
|
||||||
|
|
||||||
|
public class AccountAliasCapabilityTests : IAsyncLifetime
|
||||||
|
{
|
||||||
|
private InMemoryDatabaseService _databaseService = null!;
|
||||||
|
private AccountService _accountService = null!;
|
||||||
|
|
||||||
|
public async Task InitializeAsync()
|
||||||
|
{
|
||||||
|
_databaseService = new InMemoryDatabaseService();
|
||||||
|
await _databaseService.InitializeAsync();
|
||||||
|
_accountService = CreateAccountService(_databaseService);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DisposeAsync() => await _databaseService.DisposeAsync();
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateRootAliasAsync_SetsManualConfirmedDefaults()
|
||||||
|
{
|
||||||
|
var accountId = Guid.NewGuid();
|
||||||
|
|
||||||
|
await _accountService.CreateRootAliasAsync(accountId, "root@example.com");
|
||||||
|
|
||||||
|
var aliases = await _accountService.GetAccountAliasesAsync(accountId);
|
||||||
|
var alias = aliases.Should().ContainSingle().Subject;
|
||||||
|
|
||||||
|
alias.Source.Should().Be(AliasSource.Manual);
|
||||||
|
alias.SendCapability.Should().Be(AliasSendCapability.Confirmed);
|
||||||
|
alias.IsPrimary.Should().BeTrue();
|
||||||
|
alias.IsRootAlias.Should().BeTrue();
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task UpdateRemoteAliasInformationAsync_PreservesManualAliasesWhileAddingProviderAliases()
|
||||||
|
{
|
||||||
|
var account = new MailAccount
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
Name = "Alias Test",
|
||||||
|
Address = "primary@example.com",
|
||||||
|
ProviderType = MailProviderType.Outlook
|
||||||
|
};
|
||||||
|
|
||||||
|
await _databaseService.Connection.InsertAsync(account, typeof(MailAccount));
|
||||||
|
|
||||||
|
await _accountService.UpdateAccountAliasesAsync(account.Id,
|
||||||
|
[
|
||||||
|
new MailAccountAlias
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
AccountId = account.Id,
|
||||||
|
AliasAddress = "primary@example.com",
|
||||||
|
ReplyToAddress = "primary@example.com",
|
||||||
|
IsPrimary = true,
|
||||||
|
IsRootAlias = true,
|
||||||
|
Source = AliasSource.Manual,
|
||||||
|
SendCapability = AliasSendCapability.Confirmed
|
||||||
|
},
|
||||||
|
new MailAccountAlias
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
AccountId = account.Id,
|
||||||
|
AliasAddress = "custom@example.com",
|
||||||
|
ReplyToAddress = "replies@example.com",
|
||||||
|
Source = AliasSource.Manual,
|
||||||
|
SendCapability = AliasSendCapability.Unknown
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
await _accountService.UpdateRemoteAliasInformationAsync(account,
|
||||||
|
[
|
||||||
|
new RemoteAccountAlias
|
||||||
|
{
|
||||||
|
AliasAddress = "primary@example.com",
|
||||||
|
ReplyToAddress = "primary@example.com",
|
||||||
|
IsPrimary = true,
|
||||||
|
IsRootAlias = true,
|
||||||
|
Source = AliasSource.ProviderDiscovered,
|
||||||
|
SendCapability = AliasSendCapability.Confirmed
|
||||||
|
},
|
||||||
|
new RemoteAccountAlias
|
||||||
|
{
|
||||||
|
AliasAddress = "sales@example.com",
|
||||||
|
ReplyToAddress = "sales@example.com",
|
||||||
|
Source = AliasSource.ProviderDiscovered,
|
||||||
|
SendCapability = AliasSendCapability.Unknown
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
var aliases = await _accountService.GetAccountAliasesAsync(account.Id);
|
||||||
|
|
||||||
|
aliases.Should().Contain(a => a.AliasAddress == "custom@example.com" && a.Source == AliasSource.Manual);
|
||||||
|
aliases.Should().Contain(a => a.AliasAddress == "sales@example.com" && a.Source == AliasSource.ProviderDiscovered);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static AccountService CreateAccountService(InMemoryDatabaseService databaseService)
|
||||||
|
{
|
||||||
|
var preferencesService = new Mock<IPreferencesService>();
|
||||||
|
var signatureService = new Mock<ISignatureService>();
|
||||||
|
var mimeFileService = new Mock<IMimeFileService>();
|
||||||
|
var contactPictureFileService = new Mock<IContactPictureFileService>();
|
||||||
|
|
||||||
|
return new AccountService(
|
||||||
|
databaseService,
|
||||||
|
signatureService.Object,
|
||||||
|
Mock.Of<IAuthenticationProvider>(),
|
||||||
|
mimeFileService.Object,
|
||||||
|
preferencesService.Object,
|
||||||
|
contactPictureFileService.Object);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -60,7 +60,9 @@ public class MailThreadingTests : IAsyncLifetime
|
|||||||
ReplyToAddress = _account.Address,
|
ReplyToAddress = _account.Address,
|
||||||
IsPrimary = true,
|
IsPrimary = true,
|
||||||
IsRootAlias = true,
|
IsRootAlias = true,
|
||||||
IsVerified = true
|
IsVerified = true,
|
||||||
|
Source = AliasSource.Manual,
|
||||||
|
SendCapability = AliasSendCapability.Confirmed
|
||||||
};
|
};
|
||||||
|
|
||||||
await _databaseService.Connection.InsertAsync(_account, typeof(MailAccount));
|
await _databaseService.Connection.InsertAsync(_account, typeof(MailAccount));
|
||||||
@@ -188,6 +190,45 @@ public class MailThreadingTests : IAsyncLifetime
|
|||||||
draftMailCopy.References.Should().Be($"{rootMessageId};{parentMessageId}");
|
draftMailCopy.References.Should().Be($"{rootMessageId};{parentMessageId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateDraftAsync_Reply_PicksAliasFromDeliveredToHeader()
|
||||||
|
{
|
||||||
|
var secondaryAlias = new MailAccountAlias
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid(),
|
||||||
|
AccountId = _account.Id,
|
||||||
|
AliasAddress = "support@test.local",
|
||||||
|
ReplyToAddress = "support@test.local",
|
||||||
|
IsPrimary = false,
|
||||||
|
IsRootAlias = false,
|
||||||
|
Source = AliasSource.Manual,
|
||||||
|
SendCapability = AliasSendCapability.Unknown
|
||||||
|
};
|
||||||
|
|
||||||
|
await _databaseService.Connection.InsertAsync(secondaryAlias, typeof(MailAccountAlias));
|
||||||
|
|
||||||
|
var referencedMimeMessage = CreateReferencedMimeMessage("Alias reply");
|
||||||
|
referencedMimeMessage.Headers.Add("Delivered-To", "<support@test.local>");
|
||||||
|
|
||||||
|
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(
|
||||||
|
_account.Id,
|
||||||
|
new DraftCreationOptions
|
||||||
|
{
|
||||||
|
Reason = DraftCreationReason.Reply,
|
||||||
|
ReferencedMessage = new ReferencedMessage
|
||||||
|
{
|
||||||
|
MimeMessage = referencedMimeMessage,
|
||||||
|
MailCopy = new MailCopy { UniqueId = Guid.NewGuid(), Id = Guid.NewGuid().ToString(), MessageId = "alias-parent@domain.com" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
var mimeMessage = draftBase64MimeMessage.GetMimeMessageFromBase64();
|
||||||
|
|
||||||
|
draftMailCopy.FromAddress.Should().Be("support@test.local");
|
||||||
|
mimeMessage.From.Mailboxes.Should().ContainSingle(m => m.Address == "support@test.local");
|
||||||
|
mimeMessage.ReplyTo.Mailboxes.Should().ContainSingle(m => m.Address == "support@test.local");
|
||||||
|
}
|
||||||
|
|
||||||
private static MimeMessage CreateReferencedMimeMessage(string subject, string? messageId = null)
|
private static MimeMessage CreateReferencedMimeMessage(string subject, string? messageId = null)
|
||||||
{
|
{
|
||||||
var message = new MimeMessage();
|
var message = new MimeMessage();
|
||||||
|
|||||||
@@ -128,6 +128,10 @@ public static class GoogleIntegratorExtensions
|
|||||||
ReplyToAddress = a.ReplyToAddress,
|
ReplyToAddress = a.ReplyToAddress,
|
||||||
AliasSenderName = a.DisplayName,
|
AliasSenderName = a.DisplayName,
|
||||||
IsVerified = a.VerificationStatus == "accepted" || a.IsDefault.GetValueOrDefault(),
|
IsVerified = a.VerificationStatus == "accepted" || a.IsDefault.GetValueOrDefault(),
|
||||||
|
Source = AliasSource.ProviderDiscovered,
|
||||||
|
SendCapability = a.VerificationStatus == "accepted" || a.IsDefault.GetValueOrDefault()
|
||||||
|
? AliasSendCapability.Confirmed
|
||||||
|
: AliasSendCapability.Unknown,
|
||||||
}).ToList();
|
}).ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ public interface IDefaultChangeProcessor
|
|||||||
Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId);
|
Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId);
|
||||||
Task UpdateFolderLastSyncDateAsync(Guid folderId);
|
Task UpdateFolderLastSyncDateAsync(Guid folderId);
|
||||||
Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases);
|
Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases);
|
||||||
|
Task UpdateAliasSendCapabilityAsync(Guid accountId, string aliasAddress, AliasSendCapability capability);
|
||||||
// Calendar
|
// Calendar
|
||||||
Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId);
|
Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId);
|
||||||
|
|
||||||
@@ -200,6 +201,9 @@ public class DefaultChangeProcessor(IDatabaseService databaseService,
|
|||||||
public Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases)
|
public Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases)
|
||||||
=> AccountService.UpdateRemoteAliasInformationAsync(account, remoteAccountAliases);
|
=> AccountService.UpdateRemoteAliasInformationAsync(account, remoteAccountAliases);
|
||||||
|
|
||||||
|
public Task UpdateAliasSendCapabilityAsync(Guid accountId, string aliasAddress, AliasSendCapability capability)
|
||||||
|
=> AccountService.UpdateAliasSendCapabilityAsync(accountId, aliasAddress, capability);
|
||||||
|
|
||||||
public Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId)
|
public Task<List<AccountCalendar>> GetAccountCalendarsAsync(Guid accountId)
|
||||||
=> CalendarService.GetAccountCalendarsAsync(accountId);
|
=> CalendarService.GetAccountCalendarsAsync(accountId);
|
||||||
|
|
||||||
|
|||||||
@@ -1405,6 +1405,69 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
return new ProfileInformation(displayNameAndAddress.Item1, profilePictureData, displayNameAndAddress.Item2);
|
return new ProfileInformation(displayNameAndAddress.Item1, profilePictureData, displayNameAndAddress.Item2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override async Task SynchronizeAliasesAsync()
|
||||||
|
{
|
||||||
|
var userInfo = await _graphClient.Me.GetAsync((config) =>
|
||||||
|
{
|
||||||
|
config.QueryParameters.Select = ["mail", "proxyAddresses"];
|
||||||
|
}).ConfigureAwait(false);
|
||||||
|
|
||||||
|
var remoteAliases = GetRemoteAliases(userInfo);
|
||||||
|
await _outlookChangeProcessor.UpdateRemoteAliasInformationAsync(Account, remoteAliases).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<RemoteAccountAlias> GetRemoteAliases(User userInfo)
|
||||||
|
{
|
||||||
|
var aliases = new Dictionary<string, RemoteAccountAlias>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
void AddAlias(string address)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(address))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var normalizedAddress = address.Trim();
|
||||||
|
var isAccountAddress = normalizedAddress.Equals(Account.Address, StringComparison.OrdinalIgnoreCase);
|
||||||
|
var capability = isAccountAddress ? AliasSendCapability.Confirmed : AliasSendCapability.Unknown;
|
||||||
|
|
||||||
|
if (aliases.TryGetValue(normalizedAddress, out var existingAlias))
|
||||||
|
{
|
||||||
|
existingAlias.IsPrimary |= isAccountAddress;
|
||||||
|
existingAlias.IsRootAlias |= isAccountAddress;
|
||||||
|
|
||||||
|
if (capability == AliasSendCapability.Confirmed)
|
||||||
|
existingAlias.SendCapability = AliasSendCapability.Confirmed;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
aliases[normalizedAddress] = new RemoteAccountAlias
|
||||||
|
{
|
||||||
|
AliasAddress = normalizedAddress,
|
||||||
|
ReplyToAddress = normalizedAddress,
|
||||||
|
IsPrimary = isAccountAddress,
|
||||||
|
IsRootAlias = isAccountAddress,
|
||||||
|
IsVerified = capability == AliasSendCapability.Confirmed,
|
||||||
|
Source = AliasSource.ProviderDiscovered,
|
||||||
|
SendCapability = capability
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
AddAlias(userInfo?.Mail);
|
||||||
|
|
||||||
|
foreach (var proxyAddress in userInfo?.ProxyAddresses ?? [])
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(proxyAddress) ||
|
||||||
|
!proxyAddress.StartsWith("smtp:", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
AddAlias(proxyAddress["smtp:".Length..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return aliases.Values.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// POST requests are handled differently in batches in Graph SDK.
|
/// POST requests are handled differently in batches in Graph SDK.
|
||||||
/// Batch basically ignores the step's coontent-type and body.
|
/// Batch basically ignores the step's coontent-type and body.
|
||||||
@@ -1959,6 +2022,16 @@ public class OutlookSynchronizer : WinoSynchronizer<RequestInformation, Message,
|
|||||||
var errorJson = JsonNode.Parse(content);
|
var errorJson = JsonNode.Parse(content);
|
||||||
var errorCode = errorJson["error"]["code"].GetValue<string>();
|
var errorCode = errorJson["error"]["code"].GetValue<string>();
|
||||||
var errorMessage = errorJson["error"]["message"].GetValue<string>();
|
var errorMessage = errorJson["error"]["message"].GetValue<string>();
|
||||||
|
|
||||||
|
if (response.StatusCode == HttpStatusCode.Forbidden &&
|
||||||
|
string.Equals(errorCode, "ErrorSendAsDenied", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
bundle?.UIChangeRequest is SendDraftRequest sendDraftRequest)
|
||||||
|
{
|
||||||
|
var sendingAlias = sendDraftRequest.Request.SendingAlias?.AliasAddress ?? Account.Address;
|
||||||
|
await _outlookChangeProcessor.UpdateAliasSendCapabilityAsync(Account.Id, sendingAlias, AliasSendCapability.Denied).ConfigureAwait(false);
|
||||||
|
errorMessage = string.Format(Translator.Exception_AliasSendDenied_Message, sendingAlias);
|
||||||
|
}
|
||||||
|
|
||||||
var errorString = $"[{response.StatusCode}] {errorCode} - {errorMessage}\n";
|
var errorString = $"[{response.StatusCode}] {errorCode} - {errorMessage}\n";
|
||||||
|
|
||||||
// Create error context
|
// Create error context
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ public partial class AliasManagementPageViewModel : MailBaseViewModel
|
|||||||
if (aliasSyncResult.CompletedState == SynchronizationCompletedState.Success)
|
if (aliasSyncResult.CompletedState == SynchronizationCompletedState.Success)
|
||||||
await LoadAliasesAsync();
|
await LoadAliasesAsync();
|
||||||
else
|
else
|
||||||
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, "Failed to synchronize aliases", InfoBarMessageType.Error);
|
_dialogService.InfoBarMessage(Translator.GeneralTitle_Error, Translator.Exception_FailedToSynchronizeAliases, InfoBarMessageType.Error);
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|||||||
@@ -813,17 +813,15 @@ public partial class ComposePageViewModel : MailBaseViewModel,
|
|||||||
|
|
||||||
private void SaveReplyToAddress()
|
private void SaveReplyToAddress()
|
||||||
{
|
{
|
||||||
if (SelectedAlias == null) return;
|
if (SelectedAlias == null || CurrentMimeMessage == null) return;
|
||||||
|
|
||||||
|
CurrentMimeMessage.ReplyTo.Clear();
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(SelectedAlias.ReplyToAddress))
|
if (!string.IsNullOrEmpty(SelectedAlias.ReplyToAddress))
|
||||||
{
|
{
|
||||||
if (!CurrentMimeMessage.ReplyTo.Any(a => a is MailboxAddress mailboxAddress && mailboxAddress.Address == SelectedAlias.ReplyToAddress))
|
|
||||||
{
|
|
||||||
CurrentMimeMessage.ReplyTo.Clear();
|
|
||||||
CurrentMimeMessage.ReplyTo.Add(new MailboxAddress(SelectedAlias.ReplyToAddress, SelectedAlias.ReplyToAddress));
|
CurrentMimeMessage.ReplyTo.Add(new MailboxAddress(SelectedAlias.ReplyToAddress, SelectedAlias.ReplyToAddress));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private void SaveReadReceiptRequest()
|
private void SaveReadReceiptRequest()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System;
|
using System;
|
||||||
using Microsoft.UI.Xaml.Controls;
|
using Microsoft.UI.Xaml.Controls;
|
||||||
using Wino.Core.Domain.Entities.Mail;
|
using Wino.Core.Domain.Entities.Mail;
|
||||||
|
using Wino.Core.Domain.Enums;
|
||||||
using Wino.Core.Domain.Interfaces;
|
using Wino.Core.Domain.Interfaces;
|
||||||
|
|
||||||
namespace Wino.Dialogs;
|
namespace Wino.Dialogs;
|
||||||
@@ -21,7 +22,9 @@ public sealed partial class CreateAccountAliasDialog : ContentDialog, ICreateAcc
|
|||||||
ReplyToAddress = ReplyToTextBox.Text.Trim(),
|
ReplyToAddress = ReplyToTextBox.Text.Trim(),
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
IsPrimary = false,
|
IsPrimary = false,
|
||||||
IsVerified = false
|
IsVerified = false,
|
||||||
|
Source = AliasSource.Manual,
|
||||||
|
SendCapability = AliasSendCapability.Unknown
|
||||||
};
|
};
|
||||||
|
|
||||||
Hide();
|
Hide();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
<SolidColorBrush x:Key="DiagnosticIdCopyBrush">#ff7675</SolidColorBrush>
|
<SolidColorBrush x:Key="DiagnosticIdCopyBrush">#ff7675</SolidColorBrush>
|
||||||
<SolidColorBrush x:Key="AliasUnverifiedBrush">#ff7675</SolidColorBrush>
|
<SolidColorBrush x:Key="AliasUnverifiedBrush">#ff7675</SolidColorBrush>
|
||||||
<SolidColorBrush x:Key="AliasVerifiedBrush">#1abc9c</SolidColorBrush>
|
<SolidColorBrush x:Key="AliasVerifiedBrush">#1abc9c</SolidColorBrush>
|
||||||
|
<SolidColorBrush x:Key="AliasUnknownBrush">#f39c12</SolidColorBrush>
|
||||||
|
|
||||||
<ResourceDictionary.ThemeDictionaries>
|
<ResourceDictionary.ThemeDictionaries>
|
||||||
<ResourceDictionary x:Name="Light">
|
<ResourceDictionary x:Name="Light">
|
||||||
|
|||||||
@@ -34,33 +34,42 @@
|
|||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
<RowDefinition Height="Auto" />
|
<RowDefinition Height="Auto" />
|
||||||
|
<RowDefinition Height="Auto" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<TextBlock FontWeight="SemiBold" Text="{x:Bind AliasAddress}" />
|
<TextBlock FontWeight="SemiBold" Text="{x:Bind AliasAddress}" />
|
||||||
<TextBlock Grid.Row="1" Style="{StaticResource CaptionTextBlockStyle}">
|
<TextBlock Grid.Row="1" Style="{StaticResource CaptionTextBlockStyle}">
|
||||||
|
<Run Text="{x:Bind SourceDisplayName}" />
|
||||||
|
<Run Text=" • " />
|
||||||
|
<Run Text="{x:Bind CapabilityDisplayName}" />
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Grid.Row="2" Style="{StaticResource CaptionTextBlockStyle}">
|
||||||
<Run Text="Reply-To:" /> <Run Text="{x:Bind ReplyToAddress}" />
|
<Run Text="Reply-To:" /> <Run Text="{x:Bind ReplyToAddress}" />
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<controls1:SwitchPresenter
|
<Grid
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Center"
|
||||||
TargetType="x:Boolean"
|
VerticalAlignment="Center">
|
||||||
Value="{x:Bind IsVerified}">
|
|
||||||
<controls1:Case Value="True">
|
|
||||||
<Ellipse
|
<Ellipse
|
||||||
Width="12"
|
Width="12"
|
||||||
Height="12"
|
Height="12"
|
||||||
Margin="0,0,12,0"
|
Margin="0,0,12,0"
|
||||||
Fill="{StaticResource AliasVerifiedBrush}" />
|
Fill="{StaticResource AliasVerifiedBrush}"
|
||||||
</controls1:Case>
|
Visibility="{x:Bind IsCapabilityConfirmed, Mode=OneWay}" />
|
||||||
<controls1:Case Value="False">
|
|
||||||
<Ellipse
|
<Ellipse
|
||||||
Width="12"
|
Width="12"
|
||||||
Height="12"
|
Height="12"
|
||||||
Margin="0,0,12,0"
|
Margin="0,0,12,0"
|
||||||
Fill="{StaticResource AliasUnverifiedBrush}" />
|
Fill="{StaticResource AliasUnknownBrush}"
|
||||||
</controls1:Case>
|
Visibility="{x:Bind IsCapabilityUnknown, Mode=OneWay}" />
|
||||||
</controls1:SwitchPresenter>
|
<Ellipse
|
||||||
|
Width="12"
|
||||||
|
Height="12"
|
||||||
|
Margin="0,0,12,0"
|
||||||
|
Fill="{StaticResource AliasUnverifiedBrush}"
|
||||||
|
Visibility="{x:Bind IsCapabilityDenied, Mode=OneWay}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
<RadioButton
|
<RadioButton
|
||||||
Grid.Column="2"
|
Grid.Column="2"
|
||||||
@@ -195,7 +204,7 @@
|
|||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
Text="{x:Bind domain:Translator.AccountAlias_Column_Verified}" />
|
Text="{x:Bind domain:Translator.AccountAlias_Column_Status}" />
|
||||||
|
|
||||||
<TextBlock
|
<TextBlock
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
|
|||||||
@@ -250,7 +250,9 @@ public class AccountService : BaseDatabaseService, IAccountService
|
|||||||
IsRootAlias = true,
|
IsRootAlias = true,
|
||||||
IsVerified = true,
|
IsVerified = true,
|
||||||
ReplyToAddress = address,
|
ReplyToAddress = address,
|
||||||
Id = Guid.NewGuid()
|
Id = Guid.NewGuid(),
|
||||||
|
Source = AliasSource.Manual,
|
||||||
|
SendCapability = AliasSendCapability.Confirmed
|
||||||
};
|
};
|
||||||
|
|
||||||
await Connection.InsertAsync(rootAlias, typeof(MailAccountAlias)).ConfigureAwait(false);
|
await Connection.InsertAsync(rootAlias, typeof(MailAccountAlias)).ConfigureAwait(false);
|
||||||
@@ -261,7 +263,7 @@ public class AccountService : BaseDatabaseService, IAccountService
|
|||||||
public async Task<List<MailAccountAlias>> GetAccountAliasesAsync(Guid accountId)
|
public async Task<List<MailAccountAlias>> GetAccountAliasesAsync(Guid accountId)
|
||||||
{
|
{
|
||||||
return await Connection.QueryAsync<MailAccountAlias>(
|
return await Connection.QueryAsync<MailAccountAlias>(
|
||||||
"SELECT * FROM MailAccountAlias WHERE AccountId = ? ORDER BY IsRootAlias DESC",
|
"SELECT * FROM MailAccountAlias WHERE AccountId = ? ORDER BY IsRootAlias DESC, IsPrimary DESC, AliasAddress ASC",
|
||||||
accountId).ConfigureAwait(false);
|
accountId).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -491,11 +493,16 @@ public class AccountService : BaseDatabaseService, IAccountService
|
|||||||
public async Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases)
|
public async Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases)
|
||||||
{
|
{
|
||||||
var localAliases = await GetAccountAliasesAsync(account.Id).ConfigureAwait(false);
|
var localAliases = await GetAccountAliasesAsync(account.Id).ConfigureAwait(false);
|
||||||
var rootAlias = localAliases.Find(a => a.IsRootAlias);
|
var normalizedRemoteAliases = remoteAccountAliases ?? [];
|
||||||
|
|
||||||
foreach (var remoteAlias in remoteAccountAliases)
|
foreach (var remoteAlias in normalizedRemoteAliases)
|
||||||
{
|
{
|
||||||
var existingAlias = localAliases.Find(a => a.AccountId == account.Id && a.AliasAddress == remoteAlias.AliasAddress);
|
if (string.IsNullOrWhiteSpace(remoteAlias?.AliasAddress))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
var existingAlias = localAliases.Find(a =>
|
||||||
|
a.AccountId == account.Id &&
|
||||||
|
a.AliasAddress.Equals(remoteAlias.AliasAddress, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
if (existingAlias == null)
|
if (existingAlias == null)
|
||||||
{
|
{
|
||||||
@@ -509,7 +516,9 @@ public class AccountService : BaseDatabaseService, IAccountService
|
|||||||
ReplyToAddress = remoteAlias.ReplyToAddress,
|
ReplyToAddress = remoteAlias.ReplyToAddress,
|
||||||
Id = Guid.NewGuid(),
|
Id = Guid.NewGuid(),
|
||||||
IsRootAlias = remoteAlias.IsRootAlias,
|
IsRootAlias = remoteAlias.IsRootAlias,
|
||||||
AliasSenderName = remoteAlias.AliasSenderName
|
AliasSenderName = remoteAlias.AliasSenderName,
|
||||||
|
Source = remoteAlias.Source,
|
||||||
|
SendCapability = remoteAlias.SendCapability
|
||||||
};
|
};
|
||||||
|
|
||||||
await Connection.InsertAsync(newAlias, typeof(MailAccountAlias));
|
await Connection.InsertAsync(newAlias, typeof(MailAccountAlias));
|
||||||
@@ -522,6 +531,8 @@ public class AccountService : BaseDatabaseService, IAccountService
|
|||||||
existingAlias.IsVerified = remoteAlias.IsVerified;
|
existingAlias.IsVerified = remoteAlias.IsVerified;
|
||||||
existingAlias.ReplyToAddress = remoteAlias.ReplyToAddress;
|
existingAlias.ReplyToAddress = remoteAlias.ReplyToAddress;
|
||||||
existingAlias.AliasSenderName = remoteAlias.AliasSenderName;
|
existingAlias.AliasSenderName = remoteAlias.AliasSenderName;
|
||||||
|
existingAlias.Source = remoteAlias.Source;
|
||||||
|
existingAlias.SendCapability = remoteAlias.SendCapability;
|
||||||
|
|
||||||
await Connection.UpdateAsync(existingAlias, typeof(MailAccountAlias));
|
await Connection.UpdateAsync(existingAlias, typeof(MailAccountAlias));
|
||||||
}
|
}
|
||||||
@@ -553,6 +564,21 @@ public class AccountService : BaseDatabaseService, IAccountService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAliasSendCapabilityAsync(Guid accountId, string aliasAddress, AliasSendCapability capability)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(aliasAddress))
|
||||||
|
return;
|
||||||
|
|
||||||
|
var aliases = await GetAccountAliasesAsync(accountId).ConfigureAwait(false);
|
||||||
|
var alias = aliases.FirstOrDefault(a => a.AliasAddress.Equals(aliasAddress, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (alias == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
alias.SendCapability = capability;
|
||||||
|
await Connection.UpdateAsync(alias, typeof(MailAccountAlias)).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
public async Task DeleteAccountAliasAsync(Guid aliasId)
|
public async Task DeleteAccountAliasAsync(Guid aliasId)
|
||||||
{
|
{
|
||||||
// Create query to delete alias.
|
// Create query to delete alias.
|
||||||
|
|||||||
@@ -56,7 +56,8 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
public async Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions)
|
public async Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions)
|
||||||
{
|
{
|
||||||
var composerAccount = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
|
var composerAccount = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
|
||||||
var createdDraftMimeMessage = await CreateDraftMimeAsync(composerAccount, draftCreationOptions);
|
var selectedAlias = await ResolveDraftAliasAsync(composerAccount, draftCreationOptions).ConfigureAwait(false);
|
||||||
|
var createdDraftMimeMessage = await CreateDraftMimeAsync(composerAccount, draftCreationOptions, selectedAlias).ConfigureAwait(false);
|
||||||
|
|
||||||
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(composerAccount.Id, SpecialFolderType.Draft);
|
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(composerAccount.Id, SpecialFolderType.Draft);
|
||||||
|
|
||||||
@@ -74,8 +75,8 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
UniqueId = Guid.Parse(mimeUniqueId),
|
UniqueId = Guid.Parse(mimeUniqueId),
|
||||||
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 = primaryAlias?.AliasAddress ?? composerAccount.Address,
|
FromAddress = selectedAlias?.AliasAddress ?? primaryAlias?.AliasAddress ?? composerAccount.Address,
|
||||||
FromName = composerAccount.SenderName,
|
FromName = selectedAlias?.AliasSenderName ?? composerAccount.SenderName,
|
||||||
HasAttachments = false,
|
HasAttachments = false,
|
||||||
Importance = MailImportance.Normal,
|
Importance = MailImportance.Normal,
|
||||||
Subject = createdDraftMimeMessage.Subject,
|
Subject = createdDraftMimeMessage.Subject,
|
||||||
@@ -1016,7 +1017,7 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
await _contactService.SaveAddressInformationAsync(contacts).ConfigureAwait(false);
|
await _contactService.SaveAddressInformationAsync(contacts).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<MimeMessage> CreateDraftMimeAsync(MailAccount account, DraftCreationOptions draftCreationOptions)
|
private async Task<MimeMessage> CreateDraftMimeAsync(MailAccount account, DraftCreationOptions draftCreationOptions, MailAccountAlias selectedAlias)
|
||||||
{
|
{
|
||||||
// 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.
|
||||||
@@ -1028,10 +1029,15 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
};
|
};
|
||||||
EnsureOutgoingMessageId(message);
|
EnsureOutgoingMessageId(message);
|
||||||
|
|
||||||
var primaryAlias = await _accountService.GetPrimaryAccountAliasAsync(account.Id) ?? throw new MissingAliasException();
|
selectedAlias ??= await _accountService.GetPrimaryAccountAliasAsync(account.Id) ?? throw new MissingAliasException();
|
||||||
|
|
||||||
// Set FromName and FromAddress by alias.
|
// Set FromName and FromAddress by alias.
|
||||||
message.From.Add(new MailboxAddress(account.SenderName, primaryAlias.AliasAddress));
|
message.From.Add(new MailboxAddress(selectedAlias.AliasSenderName ?? account.SenderName, selectedAlias.AliasAddress));
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(selectedAlias.ReplyToAddress))
|
||||||
|
{
|
||||||
|
message.ReplyTo.Add(new MailboxAddress(selectedAlias.ReplyToAddress, selectedAlias.ReplyToAddress));
|
||||||
|
}
|
||||||
|
|
||||||
var builder = new BodyBuilder();
|
var builder = new BodyBuilder();
|
||||||
|
|
||||||
@@ -1052,6 +1058,61 @@ public class MailService : BaseDatabaseService, IMailService
|
|||||||
return message;
|
return message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<MailAccountAlias> ResolveDraftAliasAsync(MailAccount account, DraftCreationOptions draftCreationOptions)
|
||||||
|
{
|
||||||
|
var aliases = await _accountService.GetAccountAliasesAsync(account.Id).ConfigureAwait(false);
|
||||||
|
var primaryAlias = aliases.FirstOrDefault(a => a.IsPrimary) ?? aliases.FirstOrDefault();
|
||||||
|
|
||||||
|
if (draftCreationOptions?.ReferencedMessage?.MimeMessage == null)
|
||||||
|
return primaryAlias;
|
||||||
|
|
||||||
|
var referencedMessage = draftCreationOptions.ReferencedMessage.MimeMessage;
|
||||||
|
|
||||||
|
MailAccountAlias FindAlias(string address)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(address))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return aliases.FirstOrDefault(a => a.AliasAddress.Equals(address.Trim(), StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
var deliveredToAlias = FindAlias(ExtractAddressFromHeader(referencedMessage.Headers["Delivered-To"]))
|
||||||
|
?? FindAlias(ExtractAddressFromHeader(referencedMessage.Headers["X-Original-To"]));
|
||||||
|
if (deliveredToAlias != null)
|
||||||
|
return deliveredToAlias;
|
||||||
|
|
||||||
|
foreach (var mailbox in referencedMessage.To.Mailboxes)
|
||||||
|
{
|
||||||
|
var matchedAlias = FindAlias(mailbox.Address);
|
||||||
|
if (matchedAlias != null)
|
||||||
|
return matchedAlias;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var mailbox in referencedMessage.Cc.Mailboxes)
|
||||||
|
{
|
||||||
|
var matchedAlias = FindAlias(mailbox.Address);
|
||||||
|
if (matchedAlias != null)
|
||||||
|
return matchedAlias;
|
||||||
|
}
|
||||||
|
|
||||||
|
return primaryAlias;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ExtractAddressFromHeader(string headerValue)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(headerValue))
|
||||||
|
return string.Empty;
|
||||||
|
|
||||||
|
var trimmed = headerValue.Trim();
|
||||||
|
var leftBracketIndex = trimmed.LastIndexOf('<');
|
||||||
|
var rightBracketIndex = trimmed.LastIndexOf('>');
|
||||||
|
|
||||||
|
if (leftBracketIndex >= 0 && rightBracketIndex > leftBracketIndex)
|
||||||
|
return trimmed[(leftBracketIndex + 1)..rightBracketIndex].Trim();
|
||||||
|
|
||||||
|
return trimmed.Trim().Trim('<', '>', '"', '\'');
|
||||||
|
}
|
||||||
|
|
||||||
private string CreateHtmlGap()
|
private string CreateHtmlGap()
|
||||||
{
|
{
|
||||||
var template = $"""<div style="font-family: '{_preferencesService.ComposerFont}', Arial, sans-serif; font-size: {_preferencesService.ComposerFontSize}px"><br></div>""";
|
var template = $"""<div style="font-family: '{_preferencesService.ComposerFont}', Arial, sans-serif; font-size: {_preferencesService.ComposerFontSize}px"><br></div>""";
|
||||||
|
|||||||
Reference in New Issue
Block a user