diff --git a/Wino.Core.Domain/Entities/MailAccount.cs b/Wino.Core.Domain/Entities/MailAccount.cs
index dbeac65c..e668a6a3 100644
--- a/Wino.Core.Domain/Entities/MailAccount.cs
+++ b/Wino.Core.Domain/Entities/MailAccount.cs
@@ -44,6 +44,11 @@ namespace Wino.Core.Domain.Entities
///
public string AccountColorHex { get; set; }
+ ///
+ /// Base64 encoded profile picture of the account.
+ ///
+ public string Base64ProfilePictureData { get; set; }
+
///
/// Gets or sets the listing order of the account in the accounts list.
///
@@ -78,5 +83,15 @@ namespace Wino.Core.Domain.Entities
///
[Ignore]
public MailAccountPreferences Preferences { get; set; }
+
+ ///
+ /// Gets whether the account can perform ProfileInformation sync type.
+ ///
+ public bool IsProfileInfoSyncSupported => ProviderType == MailProviderType.Outlook || ProviderType == MailProviderType.Office365 || ProviderType == MailProviderType.Gmail;
+
+ ///
+ /// Gets whether the account can perform AliasInformation sync type.
+ ///
+ public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail;
}
}
diff --git a/Wino.Core.Domain/Entities/MailAccountAlias.cs b/Wino.Core.Domain/Entities/MailAccountAlias.cs
new file mode 100644
index 00000000..984d3460
--- /dev/null
+++ b/Wino.Core.Domain/Entities/MailAccountAlias.cs
@@ -0,0 +1,56 @@
+using System;
+using SQLite;
+
+namespace Wino.Core.Domain.Entities
+{
+ public class RemoteAccountAlias
+ {
+ ///
+ /// Display address of the alias.
+ ///
+ public string AliasAddress { get; set; }
+
+ ///
+ /// Address to be included in Reply-To header when alias is used for sending messages.
+ ///
+ public string ReplyToAddress { get; set; }
+
+ ///
+ /// Whether this alias is the primary alias for the account.
+ ///
+ public bool IsPrimary { get; set; }
+
+ ///
+ /// Whether the alias is verified by the server.
+ /// Only Gmail aliases are verified for now.
+ /// Non-verified alias messages might be rejected by SMTP server.
+ ///
+ public bool IsVerified { get; set; }
+
+ ///
+ /// Whether this alias is the root alias for the account.
+ /// Root alias means the first alias that was created for the account.
+ /// It can't be deleted or changed.
+ ///
+ public bool IsRootAlias { get; set; }
+ }
+
+ public class MailAccountAlias : RemoteAccountAlias
+ {
+ ///
+ /// Unique Id for the alias.
+ ///
+ [PrimaryKey]
+ public Guid Id { get; set; }
+
+ ///
+ /// Account id that this alias is attached to.
+ ///
+ public Guid AccountId { get; set; }
+
+ ///
+ /// Root aliases can't be deleted.
+ ///
+ public bool CanDelete => !IsRootAlias;
+ }
+}
diff --git a/Wino.Core.Domain/Entities/MailCopy.cs b/Wino.Core.Domain/Entities/MailCopy.cs
index 489bc916..eeb81ccd 100644
--- a/Wino.Core.Domain/Entities/MailCopy.cs
+++ b/Wino.Core.Domain/Entities/MailCopy.cs
@@ -141,7 +141,7 @@ namespace Wino.Core.Domain.Entities
///
[Ignore]
public MailAccount AssignedAccount { get; set; }
- public IEnumerable GetContainingIds() => new[] { UniqueId };
+ public IEnumerable GetContainingIds() => [UniqueId];
public override string ToString() => $"{Subject} <-> {Id}";
}
}
diff --git a/Wino.Core.Domain/Enums/AccountCreationDialogState.cs b/Wino.Core.Domain/Enums/AccountCreationDialogState.cs
index af724f47..947449d4 100644
--- a/Wino.Core.Domain/Enums/AccountCreationDialogState.cs
+++ b/Wino.Core.Domain/Enums/AccountCreationDialogState.cs
@@ -9,6 +9,7 @@
ManuelSetupWaiting,
TestingConnection,
AutoDiscoverySetup,
- AutoDiscoveryInProgress
+ AutoDiscoveryInProgress,
+ FetchingProfileInformation
}
}
diff --git a/Wino.Core.Domain/Enums/SynchronizationType.cs b/Wino.Core.Domain/Enums/SynchronizationType.cs
index c95b7d52..a8cfba1d 100644
--- a/Wino.Core.Domain/Enums/SynchronizationType.cs
+++ b/Wino.Core.Domain/Enums/SynchronizationType.cs
@@ -4,8 +4,10 @@
{
FoldersOnly, // Only synchronize folder metadata.
ExecuteRequests, // Run the queued requests, and then synchronize if needed.
- Inbox, // Only Inbox
+ Inbox, // Only Inbox, Sent and Draft folders.
Custom, // Only sync folders that are specified in the options.
- Full, // Synchronize everything
+ Full, // Synchronize all folders. This won't update profile or alias information.
+ UpdateProfile, // Only update profile information
+ Alias, // Only update alias information
}
}
diff --git a/Wino.Core.Domain/Enums/WinoPage.cs b/Wino.Core.Domain/Enums/WinoPage.cs
index 2a7264a2..cf3b5292 100644
--- a/Wino.Core.Domain/Enums/WinoPage.cs
+++ b/Wino.Core.Domain/Enums/WinoPage.cs
@@ -23,5 +23,6 @@
LanguageTimePage,
AppPreferencesPage,
SettingOptionsPage,
+ AliasManagementPage
}
}
diff --git a/Wino.Core.Domain/Exceptions/MissingAliasException.cs b/Wino.Core.Domain/Exceptions/MissingAliasException.cs
new file mode 100644
index 00000000..f81cb8c6
--- /dev/null
+++ b/Wino.Core.Domain/Exceptions/MissingAliasException.cs
@@ -0,0 +1,7 @@
+namespace Wino.Core.Domain.Exceptions
+{
+ public class MissingAliasException : System.Exception
+ {
+ public MissingAliasException() : base(Translator.Exception_MissingAlias) { }
+ }
+}
diff --git a/Wino.Core.Domain/Interfaces/IAccountService.cs b/Wino.Core.Domain/Interfaces/IAccountService.cs
index 78682cda..128dac82 100644
--- a/Wino.Core.Domain/Interfaces/IAccountService.cs
+++ b/Wino.Core.Domain/Interfaces/IAccountService.cs
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Wino.Core.Domain.Entities;
+using Wino.Core.Domain.Models.Accounts;
namespace Wino.Core.Domain.Interfaces
{
@@ -100,5 +101,59 @@ namespace Wino.Core.Domain.Interfaces
///
/// AccountId-OrderNumber pair for all accounts.
Task UpdateAccountOrdersAsync(Dictionary accountIdOrderPair);
+
+ ///
+ /// Returns the account aliases.
+ ///
+ /// Account id.
+ /// A list of MailAccountAlias that has e-mail aliases.
+ Task> GetAccountAliasesAsync(Guid accountId);
+
+ ///
+ /// Updated account's aliases.
+ ///
+ /// Account id to update aliases for.
+ /// Full list of updated aliases.
+ ///
+ Task UpdateAccountAliasesAsync(Guid accountId, List aliases);
+
+ ///
+ /// Delete account alias.
+ ///
+ /// Alias to remove.
+ Task DeleteAccountAliasAsync(Guid aliasId);
+
+ ///
+ /// Updated profile information of the account.
+ ///
+ /// Account id to update info for.
+ /// Info data.
+ ///
+ Task UpdateProfileInformationAsync(Guid accountId, ProfileInformation profileInformation);
+
+
+ ///
+ /// Creates a root + primary alias for the account.
+ /// This is only called when the account is created.
+ ///
+ /// Account id.
+ /// Address to create root primary alias from.
+ Task CreateRootAliasAsync(Guid accountId, string address);
+
+ ///
+ /// Will compare local-remote aliases and update the local ones or add/delete new ones.
+ ///
+ /// Remotely fetched basic alias info from synchronizer.
+ /// Account to update remote aliases for..
+ Task UpdateRemoteAliasInformationAsync(MailAccount account, List remoteAccountAliases);
+
+ ///
+ /// Gets the primary account alias for the given account id.
+ /// Used when creating draft messages.
+ ///
+ /// Account id.
+ /// Primary alias for the account.
+ Task GetPrimaryAccountAliasAsync(Guid accountId);
+
}
}
diff --git a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs
index 21ca1f8a..fe0044a5 100644
--- a/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs
+++ b/Wino.Core.Domain/Interfaces/IBaseSynchronizer.cs
@@ -3,6 +3,7 @@ using System.Threading.Tasks;
using MailKit;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
@@ -43,6 +44,13 @@ namespace Wino.Core.Domain.Interfaces
/// Result summary of synchronization.
Task SynchronizeAsync(SynchronizationOptions options, CancellationToken cancellationToken = default);
+ ///
+ /// Synchronizes profile information with the server.
+ /// Sender name and Profile picture are updated.
+ ///
+ /// Profile information model that holds the values.
+ Task GetProfileInformationAsync();
+
///
/// Downloads a single MIME message from the server and saves it to disk.
///
diff --git a/Wino.Core.Domain/Interfaces/ICreateAccountAliasDialog.cs b/Wino.Core.Domain/Interfaces/ICreateAccountAliasDialog.cs
new file mode 100644
index 00000000..d27a0229
--- /dev/null
+++ b/Wino.Core.Domain/Interfaces/ICreateAccountAliasDialog.cs
@@ -0,0 +1,9 @@
+using Wino.Core.Domain.Entities;
+
+namespace Wino.Core.Domain.Interfaces
+{
+ public interface ICreateAccountAliasDialog
+ {
+ public MailAccountAlias CreatedAccountAlias { get; set; }
+ }
+}
diff --git a/Wino.Core.Domain/Interfaces/AfterRequestExecutionSynchronizationInterfaces.cs b/Wino.Core.Domain/Interfaces/ICustomFolderSynchronizationRequest.cs
similarity index 100%
rename from Wino.Core.Domain/Interfaces/AfterRequestExecutionSynchronizationInterfaces.cs
rename to Wino.Core.Domain/Interfaces/ICustomFolderSynchronizationRequest.cs
diff --git a/Wino.Core.Domain/Interfaces/IDialogService.cs b/Wino.Core.Domain/Interfaces/IDialogService.cs
index a2986bd9..9d5fad1b 100644
--- a/Wino.Core.Domain/Interfaces/IDialogService.cs
+++ b/Wino.Core.Domain/Interfaces/IDialogService.cs
@@ -53,5 +53,6 @@ namespace Wino.Core.Domain.Interfaces
///
/// Signature information. Null if canceled.
Task ShowSignatureEditorDialog(AccountSignature signatureModel = null);
+ Task ShowCreateAccountAliasDialogAsync();
}
}
diff --git a/Wino.Core.Domain/Models/Accounts/AccountCreationDialogResult.cs b/Wino.Core.Domain/Models/Accounts/AccountCreationDialogResult.cs
index a1081b9e..428d1307 100644
--- a/Wino.Core.Domain/Models/Accounts/AccountCreationDialogResult.cs
+++ b/Wino.Core.Domain/Models/Accounts/AccountCreationDialogResult.cs
@@ -2,5 +2,5 @@
namespace Wino.Core.Domain.Models.Accounts
{
- public record AccountCreationDialogResult(MailProviderType ProviderType, string AccountName, string SenderName, string AccountColorHex = "");
+ public record AccountCreationDialogResult(MailProviderType ProviderType, string AccountName, string AccountColorHex = "");
}
diff --git a/Wino.Core.Domain/Models/Accounts/ProfileInformation.cs b/Wino.Core.Domain/Models/Accounts/ProfileInformation.cs
new file mode 100644
index 00000000..b9fb5677
--- /dev/null
+++ b/Wino.Core.Domain/Models/Accounts/ProfileInformation.cs
@@ -0,0 +1,9 @@
+namespace Wino.Core.Domain.Models.Accounts
+{
+ ///
+ /// Encapsulates the profile information of an account.
+ ///
+ /// Display sender name for the account.
+ /// Base 64 encoded profile picture data of the account. Thumbnail size.
+ public record ProfileInformation(string SenderName, string Base64ProfilePictureData);
+}
diff --git a/Wino.Core.Domain/Models/Accounts/ProviderDetail.cs b/Wino.Core.Domain/Models/Accounts/ProviderDetail.cs
index 4f794749..dc9a5824 100644
--- a/Wino.Core.Domain/Models/Accounts/ProviderDetail.cs
+++ b/Wino.Core.Domain/Models/Accounts/ProviderDetail.cs
@@ -14,7 +14,6 @@ namespace Wino.Core.Domain.Models.Accounts
public string ProviderImage => $"ms-appx:///Assets/Providers/{Type}.png";
public bool IsSupported => Type == MailProviderType.Outlook || Type == MailProviderType.Gmail || Type == MailProviderType.IMAP4;
- public bool RequireSenderNameOnCreationDialog => Type != MailProviderType.IMAP4;
public ProviderDetail(MailProviderType type)
{
diff --git a/Wino.Core.Domain/Models/Authorization/GoogleAuthorizationRequest.cs b/Wino.Core.Domain/Models/Authorization/GoogleAuthorizationRequest.cs
index 455fff47..bef11d59 100644
--- a/Wino.Core.Domain/Models/Authorization/GoogleAuthorizationRequest.cs
+++ b/Wino.Core.Domain/Models/Authorization/GoogleAuthorizationRequest.cs
@@ -33,7 +33,7 @@ namespace Wino.Core.Domain.Models.Authorization
ClientId = clientId;
// Creates the OAuth 2.0 authorization request.
- return string.Format("{0}?response_type=code&scope=https://mail.google.com/ https://www.googleapis.com/auth/gmail.labels&redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}",
+ return string.Format("{0}?response_type=code&scope=https://mail.google.com/ https://www.googleapis.com/auth/gmail.labels https://www.googleapis.com/auth/userinfo.profile&redirect_uri={1}&client_id={2}&state={3}&code_challenge={4}&code_challenge_method={5}",
authorizationEndpoint,
Uri.EscapeDataString(RedirectUri),
ClientId,
diff --git a/Wino.Core.Domain/Models/MailItem/DraftPreparationRequest.cs b/Wino.Core.Domain/Models/MailItem/DraftPreparationRequest.cs
index 8d7809b6..af8bf6b1 100644
--- a/Wino.Core.Domain/Models/MailItem/DraftPreparationRequest.cs
+++ b/Wino.Core.Domain/Models/MailItem/DraftPreparationRequest.cs
@@ -2,13 +2,18 @@
using System.Text.Json.Serialization;
using MimeKit;
using Wino.Core.Domain.Entities;
+using Wino.Core.Domain.Enums;
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)
+ public DraftPreparationRequest(MailAccount account,
+ MailCopy createdLocalDraftCopy,
+ string base64EncodedMimeMessage,
+ DraftCreationReason reason,
+ MailCopy referenceMailCopy = null)
{
Account = account ?? throw new ArgumentNullException(nameof(account));
@@ -19,6 +24,7 @@ public class DraftPreparationRequest
// This is additional work when deserialization needed, but not much to do atm.
Base64LocalDraftMimeMessage = base64EncodedMimeMessage;
+ Reason = reason;
}
[JsonConstructor]
@@ -29,6 +35,7 @@ public class DraftPreparationRequest
public MailCopy ReferenceMailCopy { get; set; }
public string Base64LocalDraftMimeMessage { get; set; }
+ public DraftCreationReason Reason { get; set; }
[JsonIgnore]
private MimeMessage createdLocalDraftMimeMessage;
@@ -44,5 +51,5 @@ public class DraftPreparationRequest
}
}
- public MailAccount Account { get; }
+ public MailAccount Account { get; set; }
}
diff --git a/Wino.Core.Domain/Models/MailItem/SendDraftPreparationRequest.cs b/Wino.Core.Domain/Models/MailItem/SendDraftPreparationRequest.cs
index 41f7a17b..7cabb4eb 100644
--- a/Wino.Core.Domain/Models/MailItem/SendDraftPreparationRequest.cs
+++ b/Wino.Core.Domain/Models/MailItem/SendDraftPreparationRequest.cs
@@ -5,45 +5,17 @@ using Wino.Core.Domain.Extensions;
namespace Wino.Core.Domain.Models.MailItem
{
- public class SendDraftPreparationRequest
+ public record SendDraftPreparationRequest(MailCopy MailItem,
+ MailAccountAlias SendingAlias,
+ MailItemFolder SentFolder,
+ MailItemFolder DraftFolder,
+ MailAccountPreferences AccountPreferences,
+ string Base64MimeMessage)
{
- public MailCopy MailItem { get; set; }
- public string Base64MimeMessage { get; set; }
- public MailItemFolder SentFolder { get; set; }
- public MailItemFolder DraftFolder { get; set; }
- public MailAccountPreferences AccountPreferences { get; set; }
-
- public SendDraftPreparationRequest(MailCopy mailItem,
- MailItemFolder sentFolder,
- MailItemFolder draftFolder,
- MailAccountPreferences accountPreferences,
- string base64MimeMessage)
- {
- MailItem = mailItem;
- SentFolder = sentFolder;
- DraftFolder = draftFolder;
- AccountPreferences = accountPreferences;
- Base64MimeMessage = base64MimeMessage;
- }
-
- [JsonConstructor]
- private SendDraftPreparationRequest() { }
-
[JsonIgnore]
private MimeMessage mime;
[JsonIgnore]
- public MimeMessage Mime
- {
- get
- {
- if (mime == null)
- {
- mime = Base64MimeMessage.GetMimeMessageFromBase64();
- }
-
- return mime;
- }
- }
+ public MimeMessage Mime => mime ??= Base64MimeMessage.GetMimeMessageFromBase64();
}
}
diff --git a/Wino.Core.Domain/Models/Requests/ServerRequestPackage.cs b/Wino.Core.Domain/Models/Requests/ServerRequestPackage.cs
index 98ca5792..31a408a2 100644
--- a/Wino.Core.Domain/Models/Requests/ServerRequestPackage.cs
+++ b/Wino.Core.Domain/Models/Requests/ServerRequestPackage.cs
@@ -3,14 +3,11 @@ using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Models.Requests
{
-
///
/// Encapsulates request to queue and account for synchronizer.
///
- ///
- ///
+ /// Which account to execute this request for.
/// Prepared request for the server.
- /// Whihc account to execute this request for.
public record ServerRequestPackage(Guid AccountId, IRequestBase Request) : IClientMessage
{
public override string ToString() => $"Server Package: {Request.GetType().Name}";
diff --git a/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs b/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs
index 19e45eb7..0e98844a 100644
--- a/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs
+++ b/Wino.Core.Domain/Models/Synchronization/SynchronizationResult.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.MailItem;
namespace Wino.Core.Domain.Models.Synchronization
@@ -15,14 +16,23 @@ namespace Wino.Core.Domain.Models.Synchronization
/// It's ignored in serialization. Client should not react to this.
///
[JsonIgnore]
- public IEnumerable DownloadedMessages { get; set; } = new List();
+ public IEnumerable DownloadedMessages { get; set; } = [];
+
+ public ProfileInformation ProfileInformation { get; set; }
+
public SynchronizationCompletedState CompletedState { get; set; }
public static SynchronizationResult Empty => new() { CompletedState = SynchronizationCompletedState.Success };
- public static SynchronizationResult Completed(IEnumerable downloadedMessages)
- => new() { DownloadedMessages = downloadedMessages, CompletedState = SynchronizationCompletedState.Success };
+ public static SynchronizationResult Completed(IEnumerable downloadedMessages, ProfileInformation profileInformation = null)
+ => new()
+ {
+ DownloadedMessages = downloadedMessages,
+ ProfileInformation = profileInformation,
+ CompletedState = SynchronizationCompletedState.Success
+ };
public static SynchronizationResult Canceled => new() { CompletedState = SynchronizationCompletedState.Canceled };
+ public static SynchronizationResult Failed => new() { CompletedState = SynchronizationCompletedState.Failed };
}
}
diff --git a/Wino.Core.Domain/Translations/en_US/resources.json b/Wino.Core.Domain/Translations/en_US/resources.json
index 2f6699c7..7f61605a 100644
--- a/Wino.Core.Domain/Translations/en_US/resources.json
+++ b/Wino.Core.Domain/Translations/en_US/resources.json
@@ -3,6 +3,7 @@
"AccountCreationDialog_Initializing": "initializing",
"AccountCreationDialog_PreparingFolders": "We are getting folder information at the moment.",
"AccountCreationDialog_SigninIn": "Account information is being saved.",
+ "AccountCreationDialog_FetchingProfileInformation": "Fetching profile details.",
"AccountEditDialog_Message": "Account Name",
"AccountEditDialog_Title": "Edit Account",
"AccountPickerDialog_Title": "Pick an account",
@@ -21,6 +22,8 @@
"BasicIMAPSetupDialog_Password": "Password",
"BasicIMAPSetupDialog_Title": "IMAP Account",
"Buttons_AddAccount": "Add Account",
+ "Buttons_AddNewAlias": "Add New Alias",
+ "Buttons_SyncAliases": "Synchronize Aliases",
"Buttons_ApplyTheme": "Apply Theme",
"Buttons_Browse": "Browse",
"Buttons_Cancel": "Cancel",
@@ -64,6 +67,16 @@
"CustomThemeBuilder_WallpaperTitle": "Set custom wallpaper",
"DialogMessage_AccountLimitMessage": "You have reached the account creation limit.\nWould you like to purchase 'Unlimited Account' add-on to continue?",
"DialogMessage_AccountLimitTitle": "Account Limit Reached",
+ "DialogMessage_AliasNotSelectedTitle": "Missing Alias",
+ "DialogMessage_AliasNotSelectedMessage": "You must select an alias before sending a message.",
+ "DialogMessage_AliasExistsTitle": "Existing Alias",
+ "DialogMessage_AliasExistsMessage": "This alias is already in use.",
+ "DialogMessage_InvalidAliasTitle": "Invalid Alias",
+ "DialogMessage_InvalidAliasMessage": "This alias is not valid. Make sure all addresses of the alias are valid e-mail addresses.",
+ "DialogMessage_CantDeleteRootAliasTitle": "Can't Delete Alias",
+ "DialogMessage_CantDeleteRootAliasMessage": "Root alias can't be deleted. This is your main identity associated with your account setup.",
+ "DialogMessage_AliasCreatedTitle": "Created New Alias",
+ "DialogMessage_AliasCreatedMessage": "New alias is succesfully created.",
"DialogMessage_CleanupFolderMessage": "Do you want to permanently delete all the mails in this folder?",
"DialogMessage_CleanupFolderTitle": "Cleanup Folder",
"DialogMessage_ComposerMissingRecipientMessage": "Message has no recipient.",
@@ -92,9 +105,16 @@
"DialogMessage_UnsubscribeConfirmationGoToWebsiteConfirmButton": "Go to website",
"DialogMessage_UnsubscribeConfirmationMailtoMessage": "Do you want to stop getting messages from {0}? Wino will unsubscribe for you by sending an email from your email account to {1}.",
"Dialog_DontAskAgain": "Don't ask again",
+ "CreateAccountAliasDialog_Title": "Create Account Alias",
+ "CreateAccountAliasDialog_Description": "Make sure your outgoing server allows sending mails from this alias.",
+ "CreateAccountAliasDialog_AliasAddress": "Address",
+ "CreateAccountAliasDialog_AliasAddressPlaceholder": "eg. support@mydomain.com",
+ "CreateAccountAliasDialog_ReplyToAddress": "Reply-To Address",
+ "CreateAccountAliasDialog_ReplyToAddressPlaceholder": "admin@mydomain.com",
"DiscordChannelDisclaimerMessage": "Wino doesn't have it's own Discord server, but special 'wino-mail' channel is hosted at 'Developer Sanctuary' server.\nTo get the updates about Wino please join Developer Sanctuary server and follow 'wino-mail' channel under 'Community Projects'\n\nYou will be directed to server URL since Discord doesn't support channel invites.",
"DiscordChannelDisclaimerTitle": "Important Discord Information",
"Draft": "Draft",
+ "Busy": "Busy",
"EditorToolbarOption_Draw": "Draw",
"EditorToolbarOption_Format": "Format",
"EditorToolbarOption_Insert": "Insert",
@@ -106,6 +126,7 @@
"ElementTheme_Light": "Light mode",
"Emoji": "Emoji",
"Exception_WinoServerException": "Wino server failed.",
+ "Exception_MailProcessing": "This mail is still being processed. Please try again after few seconds.",
"Exception_ImapAutoDiscoveryFailed": "Couldn't find mailbox settings.",
"Exception_ImapClientPoolFailed": "IMAP Client Pool failed.",
"Exception_AuthenticationCanceled": "Authentication canceled",
@@ -113,6 +134,9 @@
"Exception_CustomThemeMissingName": "You must provide a name.",
"Exception_CustomThemeMissingWallpaper": "You must provide a custom background image.",
"Exception_FailedToSynchronizeFolders": "Failed to synchronize folders",
+ "Exception_FailedToSynchronizeAliases": "Failed to synchronize aliases",
+ "Exception_MissingAlias": "Primary alias does not exist for this account. Creating draft failed.",
+ "Exception_FailedToSynchronizeProfileInformation": "Failed to synchronize profile information",
"Exception_GoogleAuthCallbackNull": "Callback uri is null on activation.",
"Exception_GoogleAuthCorruptedCode": "Corrupted authorization response.",
"Exception_GoogleAuthError": "OAuth authorization error: {0}",
@@ -253,6 +277,7 @@
"Info_UnsubscribeLinkInvalidMessage": "This unsubscribe link is invalid. Failed to unsubscribe from the list.",
"Info_UnsubscribeSuccessMessage": "Successfully unsubscribed from {0}.",
"Info_UnsubscribeErrorMessage": "Failed to unsubscribe",
+ "Info_CantDeletePrimaryAliasMessage": "Primary alias can't be deleted. Please change your alias before deleting this one",
"ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method",
"ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security",
"ImapAuthenticationMethod_Auto": "Auto",
@@ -396,6 +421,8 @@
"SettingsFolderSync_Title": "Folder Synchronization",
"SettingsFolderOptions_Title": "Folder Configuration",
"SettingsFolderOptions_Description": "Change individual folder settings like enable/disable sync or show/hide unread badge.",
+ "SettingsManageAliases_Title": "Aliases",
+ "SettingsManageAliases_Description": "See e-mail aliases assigned for this account, update or delete them.",
"SettingsHoverActionCenter": "Center Action",
"SettingsHoverActionLeft": "Left Action",
"SettingsHoverActionRight": "Right Action",
@@ -406,6 +433,11 @@
"SettingsLanguageTime_Title": "Language & Time",
"SettingsLanguageTime_Description": "Wino display language, preferred time format.",
"CategoriesFolderNameOverride": "Categories",
+ "AccountAlias_Column_Verified": "Verified",
+ "AccountAlias_Column_Alias": "Alias",
+ "AccountAlias_Column_IsPrimaryAlias": "Primary",
+ "AccountAlias_Disclaimer_FirstLine": "Wino can only import aliases for your Gmail accounts.",
+ "AccountAlias_Disclaimer_SecondLine": "If you want to use aliases for your Outlook or IMAP account, please add them yourself.",
"MoreFolderNameOverride": "More",
"SettingsOptions_Title": "Settings",
"SettingsLinkAccounts_Description": "Merge multiple accounts into one. See mails from one Inbox together.",
diff --git a/Wino.Core.Domain/Translator.Designer.cs b/Wino.Core.Domain/Translator.Designer.cs
index 6fc65a03..fa0c50f4 100644
--- a/Wino.Core.Domain/Translator.Designer.cs
+++ b/Wino.Core.Domain/Translator.Designer.cs
@@ -38,6 +38,11 @@ namespace Wino.Core.Domain
///
public static string AccountCreationDialog_SigninIn => Resources.GetTranslatedString(@"AccountCreationDialog_SigninIn");
+ ///
+ /// Fetching profile details.
+ ///
+ public static string AccountCreationDialog_FetchingProfileInformation => Resources.GetTranslatedString(@"AccountCreationDialog_FetchingProfileInformation");
+
///
/// Account Name
///
@@ -128,6 +133,16 @@ namespace Wino.Core.Domain
///
public static string Buttons_AddAccount => Resources.GetTranslatedString(@"Buttons_AddAccount");
+ ///
+ /// Add New Alias
+ ///
+ public static string Buttons_AddNewAlias => Resources.GetTranslatedString(@"Buttons_AddNewAlias");
+
+ ///
+ /// Synchronize Aliases
+ ///
+ public static string Buttons_SyncAliases => Resources.GetTranslatedString(@"Buttons_SyncAliases");
+
///
/// Apply Theme
///
@@ -343,6 +358,56 @@ namespace Wino.Core.Domain
///
public static string DialogMessage_AccountLimitTitle => Resources.GetTranslatedString(@"DialogMessage_AccountLimitTitle");
+ ///
+ /// Missing Alias
+ ///
+ public static string DialogMessage_AliasNotSelectedTitle => Resources.GetTranslatedString(@"DialogMessage_AliasNotSelectedTitle");
+
+ ///
+ /// You must select an alias before sending a message.
+ ///
+ public static string DialogMessage_AliasNotSelectedMessage => Resources.GetTranslatedString(@"DialogMessage_AliasNotSelectedMessage");
+
+ ///
+ /// Existing Alias
+ ///
+ public static string DialogMessage_AliasExistsTitle => Resources.GetTranslatedString(@"DialogMessage_AliasExistsTitle");
+
+ ///
+ /// This alias is already in use.
+ ///
+ public static string DialogMessage_AliasExistsMessage => Resources.GetTranslatedString(@"DialogMessage_AliasExistsMessage");
+
+ ///
+ /// Invalid Alias
+ ///
+ public static string DialogMessage_InvalidAliasTitle => Resources.GetTranslatedString(@"DialogMessage_InvalidAliasTitle");
+
+ ///
+ /// This alias is not valid. Make sure all addresses of the alias are valid e-mail addresses.
+ ///
+ public static string DialogMessage_InvalidAliasMessage => Resources.GetTranslatedString(@"DialogMessage_InvalidAliasMessage");
+
+ ///
+ /// Can't Delete Alias
+ ///
+ public static string DialogMessage_CantDeleteRootAliasTitle => Resources.GetTranslatedString(@"DialogMessage_CantDeleteRootAliasTitle");
+
+ ///
+ /// Root alias can't be deleted. This is your main identity associated with your account setup.
+ ///
+ public static string DialogMessage_CantDeleteRootAliasMessage => Resources.GetTranslatedString(@"DialogMessage_CantDeleteRootAliasMessage");
+
+ ///
+ /// Created New Alias
+ ///
+ public static string DialogMessage_AliasCreatedTitle => Resources.GetTranslatedString(@"DialogMessage_AliasCreatedTitle");
+
+ ///
+ /// New alias is succesfully created.
+ ///
+ public static string DialogMessage_AliasCreatedMessage => Resources.GetTranslatedString(@"DialogMessage_AliasCreatedMessage");
+
///
/// Do you want to permanently delete all the mails in this folder?
///
@@ -434,7 +499,7 @@ namespace Wino.Core.Domain
public static string DialogMessage_UnlinkAccountsConfirmationTitle => Resources.GetTranslatedString(@"DialogMessage_UnlinkAccountsConfirmationTitle");
///
- /// Missin Subject
+ /// Missing Subject
///
public static string DialogMessage_EmptySubjectConfirmation => Resources.GetTranslatedString(@"DialogMessage_EmptySubjectConfirmation");
@@ -483,6 +548,36 @@ namespace Wino.Core.Domain
///
public static string Dialog_DontAskAgain => Resources.GetTranslatedString(@"Dialog_DontAskAgain");
+ ///
+ /// Create Account Alias
+ ///
+ public static string CreateAccountAliasDialog_Title => Resources.GetTranslatedString(@"CreateAccountAliasDialog_Title");
+
+ ///
+ /// Make sure your outgoing server allows sending mails from this alias.
+ ///
+ public static string CreateAccountAliasDialog_Description => Resources.GetTranslatedString(@"CreateAccountAliasDialog_Description");
+
+ ///
+ /// Address
+ ///
+ public static string CreateAccountAliasDialog_AliasAddress => Resources.GetTranslatedString(@"CreateAccountAliasDialog_AliasAddress");
+
+ ///
+ /// eg. support@mydomain.com
+ ///
+ public static string CreateAccountAliasDialog_AliasAddressPlaceholder => Resources.GetTranslatedString(@"CreateAccountAliasDialog_AliasAddressPlaceholder");
+
+ ///
+ /// Reply-To Address
+ ///
+ public static string CreateAccountAliasDialog_ReplyToAddress => Resources.GetTranslatedString(@"CreateAccountAliasDialog_ReplyToAddress");
+
+ ///
+ /// admin@mydomain.com
+ ///
+ public static string CreateAccountAliasDialog_ReplyToAddressPlaceholder => Resources.GetTranslatedString(@"CreateAccountAliasDialog_ReplyToAddressPlaceholder");
+
///
/// Wino doesn't have it's own Discord server, but special 'wino-mail' channel is hosted at 'Developer Sanctuary' server. To get the updates about Wino please join Developer Sanctuary server and follow 'wino-mail' channel under 'Community Projects' You will be directed to server URL since Discord doesn't support channel invites.
///
@@ -498,6 +593,11 @@ namespace Wino.Core.Domain
///
public static string Draft => Resources.GetTranslatedString(@"Draft");
+ ///
+ /// Busy
+ ///
+ public static string Busy => Resources.GetTranslatedString(@"Busy");
+
///
/// Draw
///
@@ -553,6 +653,11 @@ namespace Wino.Core.Domain
///
public static string Exception_WinoServerException => Resources.GetTranslatedString(@"Exception_WinoServerException");
+ ///
+ /// This mail is still being processed. Please try again after few seconds.
+ ///
+ public static string Exception_MailProcessing => Resources.GetTranslatedString(@"Exception_MailProcessing");
+
///
/// Couldn't find mailbox settings.
///
@@ -588,6 +693,21 @@ namespace Wino.Core.Domain
///
public static string Exception_FailedToSynchronizeFolders => Resources.GetTranslatedString(@"Exception_FailedToSynchronizeFolders");
+ ///
+ /// Failed to synchronize aliases
+ ///
+ public static string Exception_FailedToSynchronizeAliases => Resources.GetTranslatedString(@"Exception_FailedToSynchronizeAliases");
+
+ ///
+ /// Primary alias does not exist for this account. Creating draft failed.
+ ///
+ public static string Exception_MissingAlias => Resources.GetTranslatedString(@"Exception_MissingAlias");
+
+ ///
+ /// Failed to synchronize profile information
+ ///
+ public static string Exception_FailedToSynchronizeProfileInformation => Resources.GetTranslatedString(@"Exception_FailedToSynchronizeProfileInformation");
+
///
/// Callback uri is null on activation.
///
@@ -1288,6 +1408,11 @@ namespace Wino.Core.Domain
///
public static string Info_UnsubscribeErrorMessage => Resources.GetTranslatedString(@"Info_UnsubscribeErrorMessage");
+ ///
+ /// Primary alias can't be deleted. Please change your alias before deleting this one
+ ///
+ public static string Info_CantDeletePrimaryAliasMessage => Resources.GetTranslatedString(@"Info_CantDeletePrimaryAliasMessage");
+
///
/// Authentication method
///
@@ -2003,6 +2128,16 @@ namespace Wino.Core.Domain
///
public static string SettingsFolderOptions_Description => Resources.GetTranslatedString(@"SettingsFolderOptions_Description");
+ ///
+ /// Aliases
+ ///
+ public static string SettingsManageAliases_Title => Resources.GetTranslatedString(@"SettingsManageAliases_Title");
+
+ ///
+ /// See e-mail aliases assigned for this account, update or delete them.
+ ///
+ public static string SettingsManageAliases_Description => Resources.GetTranslatedString(@"SettingsManageAliases_Description");
+
///
/// Center Action
///
@@ -2053,6 +2188,31 @@ namespace Wino.Core.Domain
///
public static string CategoriesFolderNameOverride => Resources.GetTranslatedString(@"CategoriesFolderNameOverride");
+ ///
+ /// Verified
+ ///
+ public static string AccountAlias_Column_Verified => Resources.GetTranslatedString(@"AccountAlias_Column_Verified");
+
+ ///
+ /// Alias
+ ///
+ public static string AccountAlias_Column_Alias => Resources.GetTranslatedString(@"AccountAlias_Column_Alias");
+
+ ///
+ /// Primary
+ ///
+ public static string AccountAlias_Column_IsPrimaryAlias => Resources.GetTranslatedString(@"AccountAlias_Column_IsPrimaryAlias");
+
+ ///
+ /// Wino can only import aliases for your Gmail accounts.
+ ///
+ public static string AccountAlias_Disclaimer_FirstLine => Resources.GetTranslatedString(@"AccountAlias_Disclaimer_FirstLine");
+
+ ///
+ /// If you want to use aliases for your Outlook or IMAP account, please add them yourself.
+ ///
+ public static string AccountAlias_Disclaimer_SecondLine => Resources.GetTranslatedString(@"AccountAlias_Disclaimer_SecondLine");
+
///
/// More
///
diff --git a/Wino.Core/Authenticators/OutlookAuthenticator.cs b/Wino.Core/Authenticators/OutlookAuthenticator.cs
index 52bd8654..4fa78378 100644
--- a/Wino.Core/Authenticators/OutlookAuthenticator.cs
+++ b/Wino.Core/Authenticators/OutlookAuthenticator.cs
@@ -28,7 +28,7 @@ namespace Wino.Core.Authenticators
public string ClientId { get; } = "b19c2035-d740-49ff-b297-de6ec561b208";
- private readonly string[] MailScope = ["email", "mail.readwrite", "offline_access", "mail.send"];
+ private readonly string[] MailScope = ["email", "mail.readwrite", "offline_access", "mail.send", "Mail.Send.Shared", "Mail.ReadWrite.Shared"];
public override MailProviderType ProviderType => MailProviderType.Outlook;
diff --git a/Wino.Core/Extensions/FolderTreeExtensions.cs b/Wino.Core/Extensions/FolderTreeExtensions.cs
deleted file mode 100644
index 18cf16ca..00000000
--- a/Wino.Core/Extensions/FolderTreeExtensions.cs
+++ /dev/null
@@ -1,31 +0,0 @@
-using Wino.Core.Domain.Interfaces;
-using Wino.Core.Domain.Models.Folders;
-using Wino.Core.MenuItems;
-
-namespace Wino.Core.Extensions
-{
- public static class FolderTreeExtensions
- {
- private static MenuItemBase GetMenuItemByFolderRecursive(IMailItemFolder structure, AccountMenuItem parentAccountMenuItem, IMenuItem parentFolderItem)
- {
- MenuItemBase parentMenuItem = new FolderMenuItem(structure, parentAccountMenuItem.Parameter, parentFolderItem);
-
- var childStructures = structure.ChildFolders;
-
- foreach (var childFolder in childStructures)
- {
- if (childFolder == null) continue;
-
- // Folder menu item.
- var subChildrenFolderTree = GetMenuItemByFolderRecursive(childFolder, parentAccountMenuItem, parentMenuItem);
-
- if (subChildrenFolderTree is FolderMenuItem folderItem)
- {
- parentMenuItem.SubMenuItems.Add(folderItem);
- }
- }
-
- return parentMenuItem;
- }
- }
-}
diff --git a/Wino.Core/Extensions/GoogleIntegratorExtensions.cs b/Wino.Core/Extensions/GoogleIntegratorExtensions.cs
index d03877b9..1aa319e3 100644
--- a/Wino.Core/Extensions/GoogleIntegratorExtensions.cs
+++ b/Wino.Core/Extensions/GoogleIntegratorExtensions.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
using System.Web;
using Google.Apis.Gmail.v1.Data;
@@ -205,44 +204,16 @@ namespace Wino.Core.Extensions
};
}
- public static Tuple> GetMailDetails(this Message message)
+ public static List GetRemoteAliases(this ListSendAsResponse response)
{
- MimeMessage mimeMessage = message.GetGmailMimeMessage();
-
- if (mimeMessage == null)
+ return response?.SendAs?.Select(a => new RemoteAccountAlias()
{
- // This should never happen.
- Debugger.Break();
-
- return default;
- }
-
- bool isUnread = message.GetIsUnread();
- bool isFocused = message.GetIsFocused();
- bool isFlagged = message.GetIsFlagged();
- bool isDraft = message.GetIsDraft();
-
- var mailCopy = new MailCopy()
- {
- CreationDate = mimeMessage.Date.UtcDateTime,
- Subject = HttpUtility.HtmlDecode(mimeMessage.Subject),
- FromName = MailkitClientExtensions.GetActualSenderName(mimeMessage),
- FromAddress = MailkitClientExtensions.GetActualSenderAddress(mimeMessage),
- PreviewText = HttpUtility.HtmlDecode(message.Snippet),
- ThreadId = message.ThreadId,
- Importance = (MailImportance)mimeMessage.Importance,
- Id = message.Id,
- IsDraft = isDraft,
- HasAttachments = mimeMessage.Attachments.Any(),
- IsRead = !isUnread,
- IsFlagged = isFlagged,
- IsFocused = isFocused,
- InReplyTo = mimeMessage.InReplyTo,
- MessageId = mimeMessage.MessageId,
- References = mimeMessage.References.GetReferences()
- };
-
- return new Tuple>(mailCopy, mimeMessage, message.LabelIds);
+ AliasAddress = a.SendAsEmail,
+ IsRootAlias = a.IsDefault.GetValueOrDefault(),
+ IsPrimary = a.IsPrimary.GetValueOrDefault(),
+ ReplyToAddress = a.ReplyToAddress,
+ IsVerified = a.VerificationStatus == "accepted" || a.IsDefault.GetValueOrDefault(),
+ }).ToList();
}
}
}
diff --git a/Wino.Core/Extensions/OutlookIntegratorExtensions.cs b/Wino.Core/Extensions/OutlookIntegratorExtensions.cs
index 2d542849..12e635c4 100644
--- a/Wino.Core/Extensions/OutlookIntegratorExtensions.cs
+++ b/Wino.Core/Extensions/OutlookIntegratorExtensions.cs
@@ -1,5 +1,9 @@
using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
using Microsoft.Graph.Models;
+using MimeKit;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
@@ -61,5 +65,160 @@ namespace Wino.Core.Extensions
return mailCopy;
}
+
+ public static Message AsOutlookMessage(this MimeMessage mime, bool includeInternetHeaders)
+ {
+ var fromAddress = GetRecipients(mime.From).ElementAt(0);
+ var toAddresses = GetRecipients(mime.To).ToList();
+ var ccAddresses = GetRecipients(mime.Cc).ToList();
+ var bccAddresses = GetRecipients(mime.Bcc).ToList();
+ var replyToAddresses = GetRecipients(mime.ReplyTo).ToList();
+
+ var message = new Message()
+ {
+ Subject = mime.Subject,
+ Importance = GetImportance(mime.Importance),
+ Body = new ItemBody() { ContentType = BodyType.Html, Content = mime.HtmlBody },
+ IsDraft = false,
+ IsRead = true, // Sent messages are always read.
+ ToRecipients = toAddresses,
+ CcRecipients = ccAddresses,
+ BccRecipients = bccAddresses,
+ From = fromAddress,
+ InternetMessageId = GetProperId(mime.MessageId),
+ ReplyTo = replyToAddresses,
+ Attachments = []
+ };
+
+ // Headers are only included when creating the draft.
+ // When sending, they are not included. Graph will throw an error.
+
+ if (includeInternetHeaders)
+ {
+ message.InternetMessageHeaders = GetHeaderList(mime);
+ }
+
+ foreach (var part in mime.BodyParts)
+ {
+ if (part.IsAttachment)
+ {
+ // File attachment.
+
+ using var memory = new MemoryStream();
+ ((MimePart)part).Content.DecodeTo(memory);
+
+ var bytes = memory.ToArray();
+
+ var fileAttachment = new FileAttachment()
+ {
+ ContentId = part.ContentId,
+ Name = part.ContentDisposition?.FileName ?? part.ContentType.Name,
+ ContentBytes = bytes,
+ };
+
+ message.Attachments.Add(fileAttachment);
+ }
+ else if (part.ContentDisposition != null && part.ContentDisposition.Disposition == "inline")
+ {
+ // Inline attachment.
+
+ using var memory = new MemoryStream();
+ ((MimePart)part).Content.DecodeTo(memory);
+
+ var bytes = memory.ToArray();
+ var inlineAttachment = new FileAttachment()
+ {
+ IsInline = true,
+ ContentId = part.ContentId,
+ Name = part.ContentDisposition?.FileName ?? part.ContentType.Name,
+ ContentBytes = bytes
+ };
+
+ message.Attachments.Add(inlineAttachment);
+ }
+ }
+
+ return message;
+ }
+
+ #region Mime to Outlook Message Helpers
+
+ private static IEnumerable GetRecipients(this InternetAddressList internetAddresses)
+ {
+ foreach (var address in internetAddresses)
+ {
+ if (address is MailboxAddress mailboxAddress)
+ yield return new Recipient() { EmailAddress = new EmailAddress() { Address = mailboxAddress.Address, Name = mailboxAddress.Name } };
+ else if (address is GroupAddress groupAddress)
+ {
+ // TODO: Group addresses are not directly supported.
+ // It'll be individually added.
+
+ foreach (var mailbox in groupAddress.Members)
+ if (mailbox is MailboxAddress groupMemberMailAddress)
+ yield return new Recipient() { EmailAddress = new EmailAddress() { Address = groupMemberMailAddress.Address, Name = groupMemberMailAddress.Name } };
+ }
+ }
+ }
+
+ private static Importance? GetImportance(MessageImportance importance)
+ {
+ return importance switch
+ {
+ MessageImportance.Low => Importance.Low,
+ MessageImportance.Normal => Importance.Normal,
+ MessageImportance.High => Importance.High,
+ _ => null
+ };
+ }
+
+ 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-.
+
+ string[] headersToIgnore = ["Date", "To", "MIME-Version", "From", "Subject", "Message-Id"];
+ string[] headersToModify = ["In-Reply-To", "Reply-To", "References", "Thread-Topic"];
+
+ var headers = new List();
+
+ int includedHeaderCount = 0;
+
+ foreach (var header in mime.Headers)
+ {
+ if (!headersToIgnore.Contains(header.Field))
+ {
+ if (headersToModify.Contains(header.Field))
+ {
+ headers.Add(new InternetMessageHeader() { Name = $"X-{header.Field}", Value = header.Value });
+ }
+ else
+ {
+ headers.Add(new InternetMessageHeader() { Name = header.Field, Value = header.Value });
+ }
+
+ includedHeaderCount++;
+ }
+
+ if (includedHeaderCount >= 5) break;
+ }
+
+ return headers;
+ }
+
+ private static string GetProperId(string id)
+ {
+ // Outlook requires some identifiers to start with "X-" or "x-".
+ if (string.IsNullOrEmpty(id)) return string.Empty;
+
+ if (!id.StartsWith("x-") || !id.StartsWith("X-"))
+ return $"X-{id}";
+
+ return id;
+ }
+ #endregion
}
}
diff --git a/Wino.Core/Integration/ImapClientPool.cs b/Wino.Core/Integration/ImapClientPool.cs
index b49556f7..1038a950 100644
--- a/Wino.Core/Integration/ImapClientPool.cs
+++ b/Wino.Core/Integration/ImapClientPool.cs
@@ -33,11 +33,13 @@ namespace Wino.Core.Integration
// Later on maybe we can make it configurable and leave it to the user with passing
// real implementation details.
- private readonly ImapImplementation _implementation = new ImapImplementation()
+ private readonly ImapImplementation _implementation = new()
{
- Version = "1.0",
+ Version = "1.8.0",
OS = "Windows",
- Vendor = "Wino"
+ Vendor = "Wino",
+ SupportUrl = "https://www.winomail.app",
+ Name = "Wino Mail User",
};
private readonly int MinimumPoolSize = 5;
diff --git a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs
index 14ebc1de..7c39fcb5 100644
--- a/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs
+++ b/Wino.Core/Integration/Processors/DefaultChangeProcessor.cs
@@ -19,6 +19,7 @@ namespace Wino.Core.Integration.Processors
///
public interface IDefaultChangeProcessor
{
+ Task UpdateAccountAsync(MailAccount account);
Task UpdateAccountDeltaSynchronizationIdentifierAsync(Guid accountId, string deltaSynchronizationIdentifier);
Task CreateAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
Task DeleteAssignmentAsync(Guid accountId, string mailCopyId, string remoteFolderId);
@@ -39,12 +40,14 @@ namespace Wino.Core.Integration.Processors
/// All folders.
Task> GetLocalFoldersAsync(Guid accountId);
+
Task> GetSynchronizationFoldersAsync(SynchronizationOptions options);
Task MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId);
Task UpdateFolderLastSyncDateAsync(Guid folderId);
Task> GetExistingFoldersAsync(Guid accountId);
+ Task UpdateRemoteAliasInformationAsync(MailAccount account, List remoteAccountAliases);
}
public interface IGmailChangeProcessor : IDefaultChangeProcessor
@@ -172,5 +175,11 @@ namespace Wino.Core.Integration.Processors
public Task UpdateFolderLastSyncDateAsync(Guid folderId)
=> FolderService.UpdateFolderLastSyncDateAsync(folderId);
+
+ public Task UpdateAccountAsync(MailAccount account)
+ => AccountService.UpdateAccountAsync(account);
+
+ public Task UpdateRemoteAliasInformationAsync(MailAccount account, List remoteAccountAliases)
+ => AccountService.UpdateRemoteAliasInformationAsync(account, remoteAccountAliases);
}
}
diff --git a/Wino.Core/MenuItems/AccountMenuItem.cs b/Wino.Core/MenuItems/AccountMenuItem.cs
index 045038f4..c06da2f5 100644
--- a/Wino.Core/MenuItems/AccountMenuItem.cs
+++ b/Wino.Core/MenuItems/AccountMenuItem.cs
@@ -47,6 +47,12 @@ namespace Wino.Core.MenuItems
set => SetProperty(Parameter.Name, value, Parameter, (u, n) => u.Name = n);
}
+ public string Base64ProfilePicture
+ {
+ get => Parameter.Name;
+ set => SetProperty(Parameter.Base64ProfilePictureData, value, Parameter, (u, n) => u.Base64ProfilePictureData = n);
+ }
+
public IEnumerable HoldingAccounts => new List { Parameter };
public AccountMenuItem(MailAccount account, IMenuItem parent = null) : base(account, account.Id, parent)
@@ -59,6 +65,7 @@ namespace Wino.Core.MenuItems
Parameter = account;
AccountName = account.Name;
AttentionReason = account.AttentionReason;
+ Base64ProfilePicture = account.Base64ProfilePictureData;
if (SubMenuItems == null) return;
diff --git a/Wino.Core/Services/AccountService.cs b/Wino.Core/Services/AccountService.cs
index 63e1055b..e4e504e8 100644
--- a/Wino.Core/Services/AccountService.cs
+++ b/Wino.Core/Services/AccountService.cs
@@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
-using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Diagnostics;
@@ -10,6 +9,7 @@ using SqlKata;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Extensions;
using Wino.Messaging.Client.Accounts;
using Wino.Messaging.UI;
@@ -233,6 +233,33 @@ namespace Wino.Core.Services
return accounts;
}
+ public async Task CreateRootAliasAsync(Guid accountId, string address)
+ {
+ var rootAlias = new MailAccountAlias()
+ {
+ AccountId = accountId,
+ AliasAddress = address,
+ IsPrimary = true,
+ IsRootAlias = true,
+ IsVerified = true,
+ ReplyToAddress = address,
+ Id = Guid.NewGuid()
+ };
+
+ await Connection.InsertAsync(rootAlias).ConfigureAwait(false);
+
+ Log.Information("Created root alias for the account {AccountId}", accountId);
+ }
+
+ public async Task> GetAccountAliasesAsync(Guid accountId)
+ {
+ var query = new Query(nameof(MailAccountAlias))
+ .Where(nameof(MailAccountAlias.AccountId), accountId)
+ .OrderByDesc(nameof(MailAccountAlias.IsRootAlias));
+
+ return await Connection.QueryAsync(query.GetRawQuery()).ConfigureAwait(false);
+ }
+
private Task GetMergedInboxInformationAsync(Guid mergedInboxId)
=> Connection.Table().FirstOrDefaultAsync(a => a.Id == mergedInboxId);
@@ -245,6 +272,7 @@ namespace Wino.Core.Services
await Connection.Table().Where(a => a.AccountId == account.Id).DeleteAsync();
await Connection.Table().DeleteAsync(a => a.MailAccountId == account.Id);
await Connection.Table().DeleteAsync(a => a.MailAccountId == account.Id);
+ await Connection.Table().DeleteAsync(a => a.AccountId == account.Id);
// Account belongs to a merged inbox.
// In case of there'll be a single account in the merged inbox, remove the merged inbox as well.
@@ -295,6 +323,19 @@ namespace Wino.Core.Services
ReportUIChange(new AccountRemovedMessage(account));
}
+ public async Task UpdateProfileInformationAsync(Guid accountId, ProfileInformation profileInformation)
+ {
+ var account = await GetAccountAsync(accountId).ConfigureAwait(false);
+
+ if (account != null)
+ {
+ account.SenderName = profileInformation.SenderName;
+ account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
+
+ await UpdateAccountAsync(account).ConfigureAwait(false);
+ }
+ }
+
public async Task GetAccountAsync(Guid accountId)
{
var account = await Connection.Table().FirstOrDefaultAsync(a => a.Id == accountId);
@@ -321,17 +362,98 @@ namespace Wino.Core.Services
public async Task UpdateAccountAsync(MailAccount account)
{
- if (account.Preferences == null)
- {
- Debugger.Break();
- }
-
- await Connection.UpdateAsync(account.Preferences);
- await Connection.UpdateAsync(account);
+ await Connection.UpdateAsync(account.Preferences).ConfigureAwait(false);
+ await Connection.UpdateAsync(account).ConfigureAwait(false);
ReportUIChange(new AccountUpdatedMessage(account));
}
+ public async Task UpdateAccountAliasesAsync(Guid accountId, List aliases)
+ {
+ // Delete existing ones.
+ await Connection.Table().DeleteAsync(a => a.AccountId == accountId).ConfigureAwait(false);
+
+ // Insert new ones.
+ foreach (var alias in aliases)
+ {
+ await Connection.InsertAsync(alias).ConfigureAwait(false);
+ }
+ }
+
+ public async Task UpdateRemoteAliasInformationAsync(MailAccount account, List remoteAccountAliases)
+ {
+ var localAliases = await GetAccountAliasesAsync(account.Id).ConfigureAwait(false);
+ var rootAlias = localAliases.Find(a => a.IsRootAlias);
+
+ foreach (var remoteAlias in remoteAccountAliases)
+ {
+ var existingAlias = localAliases.Find(a => a.AccountId == account.Id && a.AliasAddress == remoteAlias.AliasAddress);
+
+ if (existingAlias == null)
+ {
+ // Create new alias.
+ var newAlias = new MailAccountAlias()
+ {
+ AccountId = account.Id,
+ AliasAddress = remoteAlias.AliasAddress,
+ IsPrimary = remoteAlias.IsPrimary,
+ IsVerified = remoteAlias.IsVerified,
+ ReplyToAddress = remoteAlias.ReplyToAddress,
+ Id = Guid.NewGuid(),
+ IsRootAlias = remoteAlias.IsRootAlias
+ };
+
+ await Connection.InsertAsync(newAlias);
+ localAliases.Add(newAlias);
+ }
+ else
+ {
+ // Update existing alias.
+ existingAlias.IsPrimary = remoteAlias.IsPrimary;
+ existingAlias.IsVerified = remoteAlias.IsVerified;
+ existingAlias.ReplyToAddress = remoteAlias.ReplyToAddress;
+
+ await Connection.UpdateAsync(existingAlias);
+ }
+ }
+
+ // Make sure there is only 1 root alias and 1 primary alias selected.
+
+ bool shouldUpdatePrimary = localAliases.Count(a => a.IsPrimary) != 1;
+ bool shouldUpdateRoot = localAliases.Count(a => a.IsRootAlias) != 1;
+
+ if (shouldUpdatePrimary)
+ {
+ localAliases.ForEach(a => a.IsPrimary = false);
+
+ var idealPrimaryAlias = localAliases.Find(a => a.AliasAddress == account.Address) ?? localAliases.First();
+
+ idealPrimaryAlias.IsPrimary = true;
+ await Connection.UpdateAsync(idealPrimaryAlias).ConfigureAwait(false);
+ }
+
+ if (shouldUpdateRoot)
+ {
+ localAliases.ForEach(a => a.IsRootAlias = false);
+
+ var idealRootAlias = localAliases.Find(a => a.AliasAddress == account.Address) ?? localAliases.First();
+
+ idealRootAlias.IsRootAlias = true;
+ await Connection.UpdateAsync(idealRootAlias).ConfigureAwait(false);
+ }
+ }
+
+ public async Task DeleteAccountAliasAsync(Guid aliasId)
+ {
+ // Create query to delete alias.
+
+ var query = new Query("MailAccountAlias")
+ .Where("Id", aliasId)
+ .AsDelete();
+
+ await Connection.ExecuteAsync(query.GetRawQuery()).ConfigureAwait(false);
+ }
+
public async Task CreateAccountAsync(MailAccount account, TokenInformation tokenInformation, CustomServerInformation customServerInformation)
{
Guard.IsNotNull(account);
@@ -385,7 +507,7 @@ namespace Wino.Core.Services
// Outlook token cache is managed by MSAL.
// Don't save it to database.
- if (tokenInformation != null && account.ProviderType != MailProviderType.Outlook)
+ if (tokenInformation != null && (account.ProviderType != MailProviderType.Outlook || account.ProviderType == MailProviderType.Office365))
await Connection.InsertAsync(tokenInformation);
}
@@ -437,5 +559,14 @@ namespace Wino.Core.Services
Messenger.Send(new AccountMenuItemsReordered(accountIdOrderPair));
}
+
+ public async Task GetPrimaryAccountAliasAsync(Guid accountId)
+ {
+ var aliases = await GetAccountAliasesAsync(accountId);
+
+ if (aliases == null || aliases.Count == 0) return null;
+
+ return aliases.FirstOrDefault(a => a.IsPrimary) ?? aliases.First();
+ }
}
}
diff --git a/Wino.Core/Services/DatabaseService.cs b/Wino.Core/Services/DatabaseService.cs
index 246f9a63..e21a88eb 100644
--- a/Wino.Core/Services/DatabaseService.cs
+++ b/Wino.Core/Services/DatabaseService.cs
@@ -61,7 +61,8 @@ namespace Wino.Core.Services
typeof(CustomServerInformation),
typeof(AccountSignature),
typeof(MergedInbox),
- typeof(MailAccountPreferences)
+ typeof(MailAccountPreferences),
+ typeof(MailAccountAlias)
);
}
}
diff --git a/Wino.Core/Services/MailService.cs b/Wino.Core/Services/MailService.cs
index 16a18d6e..98e1101d 100644
--- a/Wino.Core/Services/MailService.cs
+++ b/Wino.Core/Services/MailService.cs
@@ -10,6 +10,7 @@ using SqlKata;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Extensions;
using Wino.Core.Domain.Interfaces;
using Wino.Core.Domain.Models.Comparers;
@@ -62,12 +63,14 @@ namespace Wino.Core.Services
// This header will be used to map the local draft copy with the remote draft copy.
var mimeUniqueId = createdDraftMimeMessage.Headers[Constants.WinoLocalDraftHeader];
+ var primaryAlias = await _accountService.GetPrimaryAccountAliasAsync(accountId).ConfigureAwait(false);
+
var copy = new MailCopy
{
UniqueId = Guid.Parse(mimeUniqueId),
Id = Guid.NewGuid().ToString(), // This will be replaced after network call with the remote draft id.
CreationDate = DateTime.UtcNow,
- FromAddress = composerAccount.Address,
+ FromAddress = primaryAlias?.AliasAddress ?? composerAccount.Address,
FromName = composerAccount.SenderName,
HasAttachments = false,
Importance = MailImportance.Normal,
@@ -621,12 +624,17 @@ namespace Wino.Core.Services
// 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.
// Synchronizer will map this unique id to the local draft copy after synchronization.
+
var message = new MimeMessage()
{
Headers = { { Constants.WinoLocalDraftHeader, Guid.NewGuid().ToString() } },
- From = { new MailboxAddress(account.SenderName, account.Address) }
};
+ var primaryAlias = await _accountService.GetPrimaryAccountAliasAsync(account.Id) ?? throw new MissingAliasException();
+
+ // Set FromName and FromAddress by alias.
+ message.From.Add(new MailboxAddress(account.SenderName, primaryAlias.AliasAddress));
+
var builder = new BodyBuilder();
var signature = await GetSignature(account, draftCreationOptions.Reason);
diff --git a/Wino.Core/Services/MimeFileService.cs b/Wino.Core/Services/MimeFileService.cs
index 02768349..42a93f65 100644
--- a/Wino.Core/Services/MimeFileService.cs
+++ b/Wino.Core/Services/MimeFileService.cs
@@ -109,12 +109,9 @@ namespace Wino.Core.Services
var resourcePath = await GetMimeResourcePathAsync(accountId, fileId).ConfigureAwait(false);
var completeFilePath = GetEMLPath(resourcePath);
- var fileStream = File.Create(completeFilePath);
+ using var fileStream = File.Open(completeFilePath, FileMode.OpenOrCreate);
- using (fileStream)
- {
- await mimeMessage.WriteToAsync(fileStream).ConfigureAwait(false);
- }
+ await mimeMessage.WriteToAsync(fileStream).ConfigureAwait(false);
return true;
}
diff --git a/Wino.Core/Synchronizers/BaseSynchronizer.cs b/Wino.Core/Synchronizers/BaseSynchronizer.cs
index acd7f213..39d53f66 100644
--- a/Wino.Core/Synchronizers/BaseSynchronizer.cs
+++ b/Wino.Core/Synchronizers/BaseSynchronizer.cs
@@ -3,6 +3,7 @@ using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.Messaging;
@@ -12,6 +13,7 @@ using Wino.Core.Domain;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Synchronization;
using Wino.Core.Integration;
@@ -69,8 +71,65 @@ namespace Wino.Core.Synchronizers
/// Cancellation token
public abstract Task ExecuteNativeRequestsAsync(IEnumerable> batchedRequests, CancellationToken cancellationToken = default);
- public abstract Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default);
+ ///
+ /// Refreshes remote mail account profile if possible.
+ /// Profile picture, sender name and mailbox settings (todo) will be handled in this step.
+ ///
+ public virtual Task GetProfileInformationAsync() => default;
+ ///
+ /// Refreshes the aliases of the account.
+ /// Only available for Gmail right now.
+ ///
+ protected virtual Task SynchronizeAliasesAsync() => Task.CompletedTask;
+
+ ///
+ /// Returns the base64 encoded profile picture of the account from the given URL.
+ ///
+ /// URL to retrieve picture from.
+ /// base64 encoded profile picture
+ protected async Task GetProfilePictureBase64EncodedAsync(string url)
+ {
+ using var client = new HttpClient();
+
+ var response = await client.GetAsync(url).ConfigureAwait(false);
+ var byteContent = await response.Content.ReadAsByteArrayAsync().ConfigureAwait(false);
+
+ return Convert.ToBase64String(byteContent);
+ }
+
+ ///
+ /// Internally synchronizes the account with the given options.
+ /// Not exposed and overriden for each synchronizer.
+ ///
+ /// Synchronization options.
+ /// Cancellation token.
+ /// Synchronization result that contains summary of the sync.
+ protected abstract Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default);
+
+ ///
+ /// Safely updates account's profile information.
+ /// Database changes are reflected after this call.
+ ///
+ private async Task SynchronizeProfileInformationInternalAsync()
+ {
+ var profileInformation = await GetProfileInformationAsync();
+
+ if (profileInformation != null)
+ {
+ Account.SenderName = profileInformation.SenderName;
+ Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
+ }
+
+ return profileInformation;
+ }
+
+ ///
+ /// Batches network requests, executes them, and does the needed synchronization after the batch request execution.
+ ///
+ /// Synchronization options.
+ /// Cancellation token.
+ /// Synchronization result that contains summary of the sync.
public async Task SynchronizeAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{
try
@@ -104,6 +163,48 @@ namespace Wino.Core.Synchronizers
await synchronizationSemaphore.WaitAsync(activeSynchronizationCancellationToken);
+ // Handle special synchronization types.
+
+ // Profile information sync.
+ if (options.Type == SynchronizationType.UpdateProfile)
+ {
+ if (!Account.IsProfileInfoSyncSupported) return SynchronizationResult.Empty;
+
+ ProfileInformation newProfileInformation = null;
+
+ try
+ {
+ newProfileInformation = await SynchronizeProfileInformationInternalAsync();
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Failed to update profile information for {Name}", Account.Name);
+
+ return SynchronizationResult.Failed;
+ }
+
+ return SynchronizationResult.Completed(null, newProfileInformation);
+ }
+
+ // Alias sync.
+ if (options.Type == SynchronizationType.Alias)
+ {
+ if (!Account.IsAliasSyncSupported) return SynchronizationResult.Empty;
+
+ try
+ {
+ await SynchronizeAliasesAsync();
+
+ return SynchronizationResult.Empty;
+ }
+ catch (Exception ex)
+ {
+ Log.Error(ex, "Failed to update aliases for {Name}", Account.Name);
+
+ return SynchronizationResult.Failed;
+ }
+ }
+
// Let servers to finish their job. Sometimes the servers doesn't respond immediately.
bool shouldDelayExecution = batches.Any(a => a.DelayExecution);
@@ -150,6 +251,10 @@ namespace Wino.Core.Synchronizers
private void PublishUnreadItemChanges()
=> WeakReferenceMessenger.Default.Send(new RefreshUnreadCountsMessage(Account.Id));
+ ///
+ /// Sends a message to the shell to update the synchronization progress.
+ ///
+ /// Percentage of the progress.
public void PublishSynchronizationProgress(double progress)
=> WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(Account.Id, progress));
diff --git a/Wino.Core/Synchronizers/GmailSynchronizer.cs b/Wino.Core/Synchronizers/GmailSynchronizer.cs
index 3ee7e044..d0cf7b2f 100644
--- a/Wino.Core/Synchronizers/GmailSynchronizer.cs
+++ b/Wino.Core/Synchronizers/GmailSynchronizer.cs
@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Google.Apis.Gmail.v1;
using Google.Apis.Gmail.v1.Data;
using Google.Apis.Http;
+using Google.Apis.PeopleService.v1;
using Google.Apis.Requests;
using Google.Apis.Services;
using MailKit;
@@ -18,6 +19,7 @@ using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Requests;
using Wino.Core.Domain.Models.Synchronization;
@@ -37,8 +39,10 @@ namespace Wino.Core.Synchronizers
// https://github.com/googleapis/google-api-dotnet-client/issues/2603
private const uint MaximumAllowedBatchRequestSize = 10;
- private readonly ConfigurableHttpClient _gmailHttpClient;
+ private readonly ConfigurableHttpClient _googleHttpClient;
private readonly GmailService _gmailService;
+ private readonly PeopleServiceService _peopleService;
+
private readonly IAuthenticator _authenticator;
private readonly IGmailChangeProcessor _gmailChangeProcessor;
private readonly ILogger _logger = Log.ForContext();
@@ -54,15 +58,48 @@ namespace Wino.Core.Synchronizers
HttpClientFactory = this
};
- _gmailHttpClient = new ConfigurableHttpClient(messageHandler);
+ _googleHttpClient = new ConfigurableHttpClient(messageHandler);
+
_gmailService = new GmailService(initializer);
+ _peopleService = new PeopleServiceService(initializer);
+
_authenticator = authenticator;
_gmailChangeProcessor = gmailChangeProcessor;
}
- public ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args) => _gmailHttpClient;
+ public ConfigurableHttpClient CreateHttpClient(CreateHttpClientArgs args) => _googleHttpClient;
- public override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
+ public override async Task GetProfileInformationAsync()
+ {
+ var profileRequest = _peopleService.People.Get("people/me");
+ profileRequest.PersonFields = "names,photos";
+
+ string senderName = string.Empty, base64ProfilePicture = string.Empty;
+
+ var userProfile = await profileRequest.ExecuteAsync();
+
+ senderName = userProfile.Names?.FirstOrDefault()?.DisplayName ?? Account.SenderName;
+
+ var profilePicture = userProfile.Photos?.FirstOrDefault()?.Url ?? string.Empty;
+
+ if (!string.IsNullOrEmpty(profilePicture))
+ {
+ base64ProfilePicture = await GetProfilePictureBase64EncodedAsync(profilePicture).ConfigureAwait(false);
+ }
+
+ return new ProfileInformation(senderName, base64ProfilePicture);
+ }
+
+ protected override async Task SynchronizeAliasesAsync()
+ {
+ var sendAsListRequest = _gmailService.Users.Settings.SendAs.List("me");
+ var sendAsListResponse = await sendAsListRequest.ExecuteAsync();
+ var remoteAliases = sendAsListResponse.GetRemoteAliases();
+
+ await _gmailChangeProcessor.UpdateRemoteAliasInformationAsync(Account, remoteAliases).ConfigureAwait(false);
+ }
+
+ protected override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{
_logger.Information("Internal synchronization started for {Name}", Account.Name);
diff --git a/Wino.Core/Synchronizers/ImapSynchronizer.cs b/Wino.Core/Synchronizers/ImapSynchronizer.cs
index 0b8cd2f6..e0ef6810 100644
--- a/Wino.Core/Synchronizers/ImapSynchronizer.cs
+++ b/Wino.Core/Synchronizers/ImapSynchronizer.cs
@@ -405,7 +405,7 @@ namespace Wino.Core.Synchronizers
];
}
- public override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
+ protected override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{
var downloadedMessageIds = new List();
diff --git a/Wino.Core/Synchronizers/OutlookSynchronizer.cs b/Wino.Core/Synchronizers/OutlookSynchronizer.cs
index 554e9dcf..7a4cde6e 100644
--- a/Wino.Core/Synchronizers/OutlookSynchronizer.cs
+++ b/Wino.Core/Synchronizers/OutlookSynchronizer.cs
@@ -4,8 +4,8 @@ using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
-using System.Net.Http;
using System.Text;
+using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
@@ -18,11 +18,11 @@ using Microsoft.Kiota.Http.HttpClientLibrary.Middleware.Options;
using MimeKit;
using MoreLinq.Extensions;
using Serilog;
-using Wino.Core.Domain;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
using Wino.Core.Domain.Exceptions;
using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Accounts;
using Wino.Core.Domain.Models.MailItem;
using Wino.Core.Domain.Models.Requests;
using Wino.Core.Domain.Models.Synchronization;
@@ -128,7 +128,7 @@ namespace Wino.Core.Synchronizers
#endregion
- public override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
+ protected override async Task SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{
var downloadedMessageIds = new List();
@@ -473,6 +473,36 @@ namespace Wino.Core.Synchronizers
}
}
+ ///
+ /// Get the user's profile picture
+ ///
+ /// Base64 encoded profile picture.
+ private async Task GetUserProfilePictureAsync()
+ {
+ var photoStream = await _graphClient.Me.Photos["48x48"].Content.GetAsync();
+
+ using var memoryStream = new MemoryStream();
+ await photoStream.CopyToAsync(memoryStream);
+ var byteArray = memoryStream.ToArray();
+
+ return Convert.ToBase64String(byteArray);
+ }
+
+ private async Task GetSenderNameAsync()
+ {
+ var userInfo = await _graphClient.Users["me"].GetAsync();
+
+ return userInfo.DisplayName;
+ }
+
+ public override async Task GetProfileInformationAsync()
+ {
+ var profilePictureData = await GetUserProfilePictureAsync().ConfigureAwait(false);
+ var senderName = await GetSenderNameAsync().ConfigureAwait(false);
+
+ return new ProfileInformation(senderName, profilePictureData);
+ }
+
#region Mail Integration
public override bool DelaySendOperationSynchronization() => true;
@@ -572,22 +602,50 @@ namespace Wino.Core.Synchronizers
{
if (item is CreateDraftRequest createDraftRequest)
{
- createDraftRequest.DraftPreperationRequest.CreatedLocalDraftMimeMessage.Prepare(EncodingConstraint.None);
+ var reason = createDraftRequest.DraftPreperationRequest.Reason;
+ var message = createDraftRequest.DraftPreperationRequest.CreatedLocalDraftMimeMessage.AsOutlookMessage(true);
- var plainTextBytes = Encoding.UTF8.GetBytes(createDraftRequest.DraftPreperationRequest.CreatedLocalDraftMimeMessage.ToString());
- var base64Encoded = Convert.ToBase64String(plainTextBytes);
+ if (reason == DraftCreationReason.Empty)
+ {
+ return _graphClient.Me.Messages.ToPostRequestInformation(message);
+ }
+ else if (reason == DraftCreationReason.Reply)
+ {
+ return _graphClient.Me.Messages[createDraftRequest.DraftPreperationRequest.ReferenceMailCopy.Id].CreateReply.ToPostRequestInformation(new Microsoft.Graph.Me.Messages.Item.CreateReply.CreateReplyPostRequestBody()
+ {
+ Message = message
+ });
+ }
+ else if (reason == DraftCreationReason.ReplyAll)
+ {
+ return _graphClient.Me.Messages[createDraftRequest.DraftPreperationRequest.ReferenceMailCopy.Id].CreateReplyAll.ToPostRequestInformation(new Microsoft.Graph.Me.Messages.Item.CreateReplyAll.CreateReplyAllPostRequestBody()
+ {
+ Message = message
+ });
+ }
+ else if (reason == DraftCreationReason.Forward)
+ {
+ return _graphClient.Me.Messages[createDraftRequest.DraftPreperationRequest.ReferenceMailCopy.Id].CreateForward.ToPostRequestInformation(new Microsoft.Graph.Me.Messages.Item.CreateForward.CreateForwardPostRequestBody()
+ {
+ Message = message
+ });
+ //createDraftRequest.DraftPreperationRequest.CreatedLocalDraftMimeMessage.Prepare(EncodingConstraint.None);
- var requestInformation = _graphClient.Me.Messages.ToPostRequestInformation(new Message());
+ //var plainTextBytes = Encoding.UTF8.GetBytes(createDraftRequest.DraftPreperationRequest.CreatedLocalDraftMimeMessage.ToString());
+ //var base64Encoded = Convert.ToBase64String(plainTextBytes);
- requestInformation.Headers.Clear();// replace the json content header
- requestInformation.Headers.Add("Content-Type", "text/plain");
+ //var requestInformation = _graphClient.Me.Messages.ToPostRequestInformation(new Message());
- requestInformation.SetStreamContent(new MemoryStream(Encoding.UTF8.GetBytes(base64Encoded)), "text/plain");
+ //requestInformation.Headers.Clear();// replace the json content header
+ //requestInformation.Headers.Add("Content-Type", "text/plain");
- return requestInformation;
+ //requestInformation.SetStreamContent(new MemoryStream(Encoding.UTF8.GetBytes(base64Encoded)), "text/plain");
+
+ //return requestInformation;
+ }
}
- return default;
+ throw new Exception("Invalid create draft request type.");
});
}
@@ -602,50 +660,43 @@ namespace Wino.Core.Synchronizers
var mailCopyId = sendDraftPreparationRequest.MailItem.Id;
var mimeMessage = sendDraftPreparationRequest.Mime;
- var batchDeleteRequest = new BatchDeleteRequest(new List()
+ // 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.
+
+ var outlookMessage = mimeMessage.AsOutlookMessage(false);
+
+ // Update draft.
+
+ var patchDraftRequest = _graphClient.Me.Messages[mailCopyId].ToPatchRequestInformation(outlookMessage);
+ var patchDraftRequestBundle = new HttpRequestBundle(patchDraftRequest, request);
+
+ // Send draft.
+
+ // POST requests are handled differently in batches in Graph SDK.
+ // Batch basically ignores the step's coontent-type and body.
+ // Manually create a POST request with empty body and send it.
+
+ var sendDraftRequest = _graphClient.Me.Messages[mailCopyId].Send.ToPostRequestInformation((config) =>
{
- new DeleteRequest(sendDraftPreparationRequest.MailItem)
+ config.Headers.Add("Content-Type", "application/json");
});
- var deleteBundle = Delete(batchDeleteRequest).ElementAt(0);
+ sendDraftRequest.Headers.Clear();
- mimeMessage.Prepare(EncodingConstraint.None);
+ sendDraftRequest.Content = new MemoryStream(Encoding.UTF8.GetBytes("{}"));
+ sendDraftRequest.HttpMethod = Method.POST;
+ sendDraftRequest.Headers.Add("Content-Type", "application/json");
- var plainTextBytes = Encoding.UTF8.GetBytes(mimeMessage.ToString());
- var base64Encoded = Convert.ToBase64String(plainTextBytes);
+ var sendDraftRequestBundle = new HttpRequestBundle(sendDraftRequest, request);
- var outlookMessage = new Message()
- {
- ConversationId = sendDraftPreparationRequest.MailItem.ThreadId
- };
-
- // Apply importance here as well just in case.
- if (mimeMessage.Importance != MessageImportance.Normal)
- outlookMessage.Importance = mimeMessage.Importance == MessageImportance.High ? Importance.High : Importance.Low;
-
- var body = new Microsoft.Graph.Me.SendMail.SendMailPostRequestBody()
- {
- Message = outlookMessage
- };
-
- var sendRequest = _graphClient.Me.SendMail.ToPostRequestInformation(body);
-
- sendRequest.Headers.Clear();
- sendRequest.Headers.Add("Content-Type", "text/plain");
-
- var stream = new MemoryStream(Encoding.UTF8.GetBytes(base64Encoded));
- sendRequest.SetStreamContent(stream, "text/plain");
-
- var sendMailRequest = new HttpRequestBundle(sendRequest, request);
-
- return [deleteBundle, sendMailRequest];
+ return [patchDraftRequestBundle, sendDraftRequestBundle];
}
public override IEnumerable> Archive(BatchArchiveRequest request)
=> Move(new BatchMoveRequest(request.Items, request.FromFolder, request.ToFolder));
-
-
public override async Task DownloadMissingMimeMessageAsync(IMailItem mailItem,
MailKit.ITransferProgress transferProgress = null,
CancellationToken cancellationToken = default)
@@ -697,26 +748,41 @@ namespace Wino.Core.Synchronizers
request.ApplyUIChanges();
- await batchContent.AddBatchRequestStepAsync(nativeRequest).ConfigureAwait(false);
+ var batchRequestId = await batchContent.AddBatchRequestStepAsync(nativeRequest).ConfigureAwait(false);
// Map BundleId to batch request step's key.
// This is how we can identify which step succeeded or failed in the bundle.
- bundle.BundleId = batchContent.BatchRequestSteps.ElementAt(i).Key;
+ bundle.BundleId = batchRequestId;
}
if (!batchContent.BatchRequestSteps.Any())
continue;
+ // Set execution type to serial instead of parallel if needed.
+ // Each step will depend on the previous one.
+
+ if (itemCount > 1)
+ {
+ for (int i = 1; i < itemCount; i++)
+ {
+ var currentStep = batchContent.BatchRequestSteps.ElementAt(i);
+ var previousStep = batchContent.BatchRequestSteps.ElementAt(i - 1);
+
+ currentStep.Value.DependsOn = [previousStep.Key];
+ }
+
+ }
+
// Execute batch. This will collect responses from network call for each batch step.
- var batchRequestResponse = await _graphClient.Batch.PostAsync(batchContent).ConfigureAwait(false);
+ var batchRequestResponse = await _graphClient.Batch.PostAsync(batchContent, cancellationToken).ConfigureAwait(false);
// Check responses for each bundle id.
// Each bundle id must return some HttpResponseMessage ideally.
var bundleIds = batchContent.BatchRequestSteps.Select(a => a.Key);
- // TODO: Handling responses. They used to work in v1 core, but not in v2.
+ var exceptionBag = new List();
foreach (var bundleId in bundleIds)
{
@@ -727,45 +793,31 @@ namespace Wino.Core.Synchronizers
var httpResponseMessage = await batchRequestResponse.GetResponseByIdAsync(bundleId);
+ if (httpResponseMessage == null)
+ continue;
+
using (httpResponseMessage)
{
- await ProcessSingleNativeRequestResponseAsync(bundle, httpResponseMessage, cancellationToken).ConfigureAwait(false);
+ if (!httpResponseMessage.IsSuccessStatusCode)
+ {
+ var content = await httpResponseMessage.Content.ReadAsStringAsync();
+ var errorJson = JsonObject.Parse(content);
+ var errorString = $"({httpResponseMessage.StatusCode}) {errorJson["error"]["code"]} - {errorJson["error"]["message"]}";
+
+ exceptionBag.Add(errorString);
+ }
}
}
+
+ if (exceptionBag.Any())
+ {
+ var formattedErrorString = string.Join("\n", exceptionBag.Select((item, index) => $"{index + 1}. {item}"));
+
+ throw new SynchronizerException(formattedErrorString);
+ }
}
}
- private async Task ProcessSingleNativeRequestResponseAsync(IRequestBundle bundle,
- HttpResponseMessage httpResponseMessage,
- CancellationToken cancellationToken = default)
- {
- if (httpResponseMessage == null) return;
-
- if (!httpResponseMessage.IsSuccessStatusCode)
- {
- throw new SynchronizerException(string.Format(Translator.Exception_SynchronizerFailureHTTP, httpResponseMessage.StatusCode));
- }
- else if (bundle is HttpRequestBundle messageBundle)
- {
- var outlookMessage = await messageBundle.DeserializeBundleAsync(httpResponseMessage, cancellationToken);
-
- if (outlookMessage == null) return;
-
- // TODO: Handle new message added or updated.
- }
- else if (bundle is HttpRequestBundle folderBundle)
- {
- var outlookFolder = await folderBundle.DeserializeBundleAsync(httpResponseMessage, cancellationToken);
-
- if (outlookFolder == null) return;
-
- // TODO: Handle new folder added or updated.
- }
- else if (bundle is HttpRequestBundle mimeBundle)
- {
- // TODO: Handle mime retrieve message.
- }
- }
private async Task DownloadMimeMessageAsync(string messageId, CancellationToken cancellationToken = default)
{
diff --git a/Wino.Core/Wino.Core.csproj b/Wino.Core/Wino.Core.csproj
index be8ca55c..edfaccdd 100644
--- a/Wino.Core/Wino.Core.csproj
+++ b/Wino.Core/Wino.Core.csproj
@@ -17,7 +17,8 @@
-
+
+
all
@@ -26,9 +27,9 @@
-
-
-
+
+
+
diff --git a/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs b/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs
index eb8e0fc0..306f3bff 100644
--- a/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs
+++ b/Wino.Mail.ViewModels/AccountDetailsPageViewModel.cs
@@ -56,7 +56,11 @@ namespace Wino.Mail.ViewModels
[RelayCommand]
private void EditSignature()
- => Messenger.Send(new BreadcrumbNavigationRequested("Signature", WinoPage.SignatureManagementPage, Account.Id));
+ => Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsSignature_Title, WinoPage.SignatureManagementPage, Account.Id));
+
+ [RelayCommand]
+ private void EditAliases()
+ => Messenger.Send(new BreadcrumbNavigationRequested(Translator.SettingsManageAliases_Title, WinoPage.AliasManagementPage, Account.Id));
public Task FolderSyncToggledAsync(IMailItemFolder folderStructure, bool isEnabled)
=> _folderService.ChangeFolderSynchronizationStateAsync(folderStructure.Id, isEnabled);
diff --git a/Wino.Mail.ViewModels/AccountManagementViewModel.cs b/Wino.Mail.ViewModels/AccountManagementViewModel.cs
index e503b99b..a1dee843 100644
--- a/Wino.Mail.ViewModels/AccountManagementViewModel.cs
+++ b/Wino.Mail.ViewModels/AccountManagementViewModel.cs
@@ -154,15 +154,12 @@ namespace Wino.Mail.ViewModels
{
creationDialog = _dialogService.GetAccountCreationDialog(accountCreationDialogResult.ProviderType);
- // _accountService.ExternalAuthenticationAuthenticator = _authenticationProvider.GetAuthenticator(accountCreationDialogResult.ProviderType);
-
CustomServerInformation customServerInformation = null;
createdAccount = new MailAccount()
{
ProviderType = accountCreationDialogResult.ProviderType,
Name = accountCreationDialogResult.AccountName,
- SenderName = accountCreationDialogResult.SenderName,
AccountColorHex = accountCreationDialogResult.AccountColorHex,
Id = Guid.NewGuid()
};
@@ -208,30 +205,81 @@ namespace Wino.Mail.ViewModels
await _accountService.CreateAccountAsync(createdAccount, tokenInformation, customServerInformation);
// Local account has been created.
- // Create new synchronizer and start synchronization.
+
+ if (createdAccount.ProviderType != MailProviderType.IMAP4)
+ {
+ // Start profile information synchronization.
+ // It's only available for Outlook and Gmail synchronizers.
+
+ var profileSyncOptions = new SynchronizationOptions()
+ {
+ AccountId = createdAccount.Id,
+ Type = SynchronizationType.UpdateProfile
+ };
+
+ var profileSynchronizationResponse = await _winoServerConnectionManager.GetResponseAsync(new NewSynchronizationRequested(profileSyncOptions, SynchronizationSource.Client));
+
+ var profileSynchronizationResult = profileSynchronizationResponse.Data;
+
+ if (profileSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
+ throw new Exception(Translator.Exception_FailedToSynchronizeProfileInformation);
+
+ createdAccount.SenderName = profileSynchronizationResult.ProfileInformation.SenderName;
+ createdAccount.Base64ProfilePictureData = profileSynchronizationResult.ProfileInformation.Base64ProfilePictureData;
+
+ await _accountService.UpdateProfileInformationAsync(createdAccount.Id, profileSynchronizationResult.ProfileInformation);
+ }
if (creationDialog is ICustomServerAccountCreationDialog customServerAccountCreationDialog)
customServerAccountCreationDialog.ShowPreparingFolders();
else
creationDialog.State = AccountCreationDialogState.PreparingFolders;
- var options = new SynchronizationOptions()
+ // Start synchronizing folders.
+ var folderSyncOptions = new SynchronizationOptions()
{
AccountId = createdAccount.Id,
Type = SynchronizationType.FoldersOnly
};
- var synchronizationResultResponse = await _winoServerConnectionManager.GetResponseAsync(new NewSynchronizationRequested(options, SynchronizationSource.Client));
+ var folderSynchronizationResponse = await _winoServerConnectionManager.GetResponseAsync(new NewSynchronizationRequested(folderSyncOptions, SynchronizationSource.Client));
- var synchronizationResult = synchronizationResultResponse.Data;
- if (synchronizationResult.CompletedState != SynchronizationCompletedState.Success)
+ var folderSynchronizationResult = folderSynchronizationResponse.Data;
+
+ if (folderSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
throw new Exception(Translator.Exception_FailedToSynchronizeFolders);
- // Check if Inbox folder is available for the account after synchronization.
- var isInboxAvailable = await _folderService.IsInboxAvailableForAccountAsync(createdAccount.Id);
+ if (createdAccount.IsAliasSyncSupported)
+ {
+ // Try to synchronize aliases for the account.
- if (!isInboxAvailable)
- throw new Exception(Translator.Exception_InboxNotAvailable);
+ var aliasSyncOptions = new SynchronizationOptions()
+ {
+ AccountId = createdAccount.Id,
+ Type = SynchronizationType.Alias
+ };
+
+ var aliasSyncResponse = await _winoServerConnectionManager.GetResponseAsync(new NewSynchronizationRequested(aliasSyncOptions, SynchronizationSource.Client));
+ var aliasSynchronizationResult = folderSynchronizationResponse.Data;
+
+ if (aliasSynchronizationResult.CompletedState != SynchronizationCompletedState.Success)
+ throw new Exception(Translator.Exception_FailedToSynchronizeAliases);
+ }
+ else
+ {
+ // Create root primary alias for the account.
+ // This is only available for accounts that do not support alias synchronization.
+
+ await _accountService.CreateRootAliasAsync(createdAccount.Id, createdAccount.Address);
+ }
+
+ // TODO: Temporary disabled. Is this even needed? Users can configure special folders manually later on if discovery fails.
+ // Check if Inbox folder is available for the account after synchronization.
+
+ //var isInboxAvailable = await _folderService.IsInboxAvailableForAccountAsync(createdAccount.Id);
+
+ //if (!isInboxAvailable)
+ // throw new Exception(Translator.Exception_InboxNotAvailable);
// Send changes to listeners.
ReportUIChange(new AccountCreatedMessage(createdAccount));
diff --git a/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs b/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs
new file mode 100644
index 00000000..c11b0daa
--- /dev/null
+++ b/Wino.Mail.ViewModels/AliasManagementPageViewModel.cs
@@ -0,0 +1,143 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using EmailValidation;
+using Wino.Core.Domain;
+using Wino.Core.Domain.Entities;
+using Wino.Core.Domain.Enums;
+using Wino.Core.Domain.Interfaces;
+using Wino.Core.Domain.Models.Navigation;
+using Wino.Core.Domain.Models.Synchronization;
+using Wino.Messaging.Server;
+
+namespace Wino.Mail.ViewModels
+{
+ public partial class AliasManagementPageViewModel : BaseViewModel
+ {
+ private readonly IAccountService _accountService;
+ private readonly IWinoServerConnectionManager _winoServerConnectionManager;
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(CanSynchronizeAliases))]
+ private MailAccount account;
+
+ [ObservableProperty]
+ private List accountAliases = [];
+
+ public bool CanSynchronizeAliases => Account?.IsAliasSyncSupported ?? false;
+
+ public AliasManagementPageViewModel(IDialogService dialogService,
+ IAccountService accountService,
+ IWinoServerConnectionManager winoServerConnectionManager) : base(dialogService)
+ {
+ _accountService = accountService;
+ _winoServerConnectionManager = winoServerConnectionManager;
+ }
+
+ public override async void OnNavigatedTo(NavigationMode mode, object parameters)
+ {
+ base.OnNavigatedTo(mode, parameters);
+
+ if (parameters is Guid accountId)
+ Account = await _accountService.GetAccountAsync(accountId);
+
+ if (Account == null) return;
+
+ await LoadAliasesAsync();
+ }
+
+ private async Task LoadAliasesAsync()
+ {
+ AccountAliases = await _accountService.GetAccountAliasesAsync(Account.Id);
+ }
+
+ [RelayCommand]
+ private async Task SetAliasPrimaryAsync(MailAccountAlias alias)
+ {
+ if (alias.IsPrimary) return;
+
+ AccountAliases.ForEach(a =>
+ {
+ a.IsPrimary = a == alias;
+ });
+
+ await _accountService.UpdateAccountAliasesAsync(Account.Id, AccountAliases);
+ await LoadAliasesAsync();
+ }
+
+ [RelayCommand]
+ private async Task SyncAliasesAsync()
+ {
+ if (!CanSynchronizeAliases) return;
+
+ var aliasSyncOptions = new SynchronizationOptions()
+ {
+ AccountId = Account.Id,
+ Type = SynchronizationType.Alias
+ };
+
+ var aliasSyncResponse = await _winoServerConnectionManager.GetResponseAsync(new NewSynchronizationRequested(aliasSyncOptions, SynchronizationSource.Client));
+
+ if (aliasSyncResponse.IsSuccess)
+ await LoadAliasesAsync();
+ else
+ DialogService.InfoBarMessage(Translator.GeneralTitle_Error, aliasSyncResponse.Message, InfoBarMessageType.Error);
+ }
+
+ [RelayCommand]
+ private async Task AddNewAliasAsync()
+ {
+ var createdAliasDialog = await DialogService.ShowCreateAccountAliasDialogAsync();
+
+ if (createdAliasDialog.CreatedAccountAlias == null) return;
+
+ var newAlias = createdAliasDialog.CreatedAccountAlias;
+
+ // Check existence.
+ if (AccountAliases.Any(a => a.AliasAddress == newAlias.AliasAddress))
+ {
+ await DialogService.ShowMessageAsync(Translator.DialogMessage_AliasExistsTitle, Translator.DialogMessage_AliasExistsMessage);
+ return;
+ }
+
+ // Validate all addresses.
+ if (!EmailValidator.Validate(newAlias.AliasAddress) || (!string.IsNullOrEmpty(newAlias.ReplyToAddress) && !EmailValidator.Validate(newAlias.ReplyToAddress)))
+ {
+ await DialogService.ShowMessageAsync(Translator.DialogMessage_InvalidAliasMessage, Translator.DialogMessage_InvalidAliasTitle);
+ return;
+ }
+
+ newAlias.AccountId = Account.Id;
+
+ AccountAliases.Add(newAlias);
+
+ await _accountService.UpdateAccountAliasesAsync(Account.Id, AccountAliases);
+ DialogService.InfoBarMessage(Translator.DialogMessage_AliasCreatedTitle, Translator.DialogMessage_AliasCreatedMessage, InfoBarMessageType.Success);
+
+ await LoadAliasesAsync();
+ }
+
+ [RelayCommand]
+ private async Task DeleteAliasAsync(MailAccountAlias alias)
+ {
+ // Primary aliases can't be deleted.
+ if (alias.IsPrimary)
+ {
+ await DialogService.ShowMessageAsync(Translator.Info_CantDeletePrimaryAliasMessage, Translator.GeneralTitle_Warning);
+ return;
+ }
+
+ // Root aliases can't be deleted.
+ if (alias.IsRootAlias)
+ {
+ await DialogService.ShowMessageAsync(Translator.DialogMessage_CantDeleteRootAliasTitle, Translator.DialogMessage_CantDeleteRootAliasMessage);
+ return;
+ }
+
+ await _accountService.DeleteAccountAliasAsync(alias.Id);
+ await LoadAliasesAsync();
+ }
+ }
+}
diff --git a/Wino.Mail.ViewModels/AppShellViewModel.cs b/Wino.Mail.ViewModels/AppShellViewModel.cs
index 5e7b0b53..0acc62da 100644
--- a/Wino.Mail.ViewModels/AppShellViewModel.cs
+++ b/Wino.Mail.ViewModels/AppShellViewModel.cs
@@ -729,17 +729,26 @@ namespace Wino.Mail.ViewModels
operationAccount = accounts.FirstOrDefault();
else
{
- // There are multiple accounts and there is no selection.
- // Don't list all accounts, but only accounts that belong to Merged Inbox.
-
if (latestSelectedAccountMenuItem is MergedAccountMenuItem selectedMergedAccountMenuItem)
{
+ // There are multiple accounts and there is no selection.
+ // Don't list all accounts, but only accounts that belong to Merged Inbox.
+
var mergedAccounts = accounts.Where(a => a.MergedInboxId == selectedMergedAccountMenuItem.EntityId);
if (!mergedAccounts.Any()) return;
Messenger.Send(new CreateNewMailWithMultipleAccountsRequested(mergedAccounts.ToList()));
}
+ else if (latestSelectedAccountMenuItem is AccountMenuItem selectedAccountMenuItem)
+ {
+ operationAccount = selectedAccountMenuItem.HoldingAccounts.ElementAt(0);
+ }
+ else
+ {
+ // User is at some other page. List all accounts.
+ Messenger.Send(new CreateNewMailWithMultipleAccountsRequested(accounts));
+ }
}
}
@@ -779,7 +788,7 @@ namespace Wino.Mail.ViewModels
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(account.Id, draftOptions).ConfigureAwait(false);
- var draftPreparationRequest = new DraftPreparationRequest(account, draftMailCopy, draftBase64MimeMessage);
+ var draftPreparationRequest = new DraftPreparationRequest(account, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason);
await _winoRequestDelegator.ExecuteAsync(draftPreparationRequest);
}
@@ -795,7 +804,7 @@ namespace Wino.Mail.ViewModels
}
protected override void OnAccountRemoved(MailAccount removedAccount)
- => Messenger.Send(new AccountsMenuRefreshRequested(true));
+ => Messenger.Send(new AccountsMenuRefreshRequested(false));
protected override async void OnAccountCreated(MailAccount createdAccount)
{
@@ -877,12 +886,9 @@ namespace Wino.Mail.ViewModels
{
await RecreateMenuItemsAsync();
- if (message.AutomaticallyNavigateFirstItem)
+ if (MenuItems.FirstOrDefault(a => a is IAccountMenuItem) is IAccountMenuItem firstAccount)
{
- if (MenuItems.FirstOrDefault(a => a is IAccountMenuItem) is IAccountMenuItem firstAccount)
- {
- await ChangeLoadedAccountAsync(firstAccount);
- }
+ await ChangeLoadedAccountAsync(firstAccount, message.AutomaticallyNavigateFirstItem);
}
}
diff --git a/Wino.Mail.ViewModels/ComposePageViewModel.cs b/Wino.Mail.ViewModels/ComposePageViewModel.cs
index d3424bf5..f91ca00a 100644
--- a/Wino.Mail.ViewModels/ComposePageViewModel.cs
+++ b/Wino.Mail.ViewModels/ComposePageViewModel.cs
@@ -8,7 +8,6 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using MimeKit;
-using MimeKit.Utils;
using Wino.Core.Domain;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Enums;
@@ -21,6 +20,7 @@ using Wino.Core.Extensions;
using Wino.Core.Services;
using Wino.Mail.ViewModels.Data;
using Wino.Messaging.Client.Mails;
+using Wino.Messaging.Server;
namespace Wino.Mail.ViewModels
{
@@ -68,6 +68,12 @@ namespace Wino.Mail.ViewModels
[NotifyCanExecuteChangedFor(nameof(SendCommand))]
private MailAccount composingAccount;
+ [ObservableProperty]
+ private List availableAliases;
+
+ [ObservableProperty]
+ private MailAccountAlias selectedAlias;
+
[ObservableProperty]
private bool isDraggingOverComposerGrid;
@@ -112,6 +118,7 @@ namespace Wino.Mail.ViewModels
private readonly IWinoRequestDelegator _worker;
public readonly IFontService FontService;
public readonly IPreferencesService PreferencesService;
+ private readonly IWinoServerConnectionManager _winoServerConnectionManager;
public readonly IContactService ContactService;
public ComposePageViewModel(IDialogService dialogService,
@@ -124,21 +131,23 @@ namespace Wino.Mail.ViewModels
IWinoRequestDelegator worker,
IContactService contactService,
IFontService fontService,
- IPreferencesService preferencesService) : base(dialogService)
+ IPreferencesService preferencesService,
+ IWinoServerConnectionManager winoServerConnectionManager) : base(dialogService)
{
NativeAppService = nativeAppService;
- _folderService = folderService;
ContactService = contactService;
FontService = fontService;
+ PreferencesService = preferencesService;
+ _folderService = folderService;
_mailService = mailService;
_launchProtocolService = launchProtocolService;
_mimeFileService = mimeFileService;
_accountService = accountService;
_worker = worker;
+ _winoServerConnectionManager = winoServerConnectionManager;
SelectedToolbarSection = ToolbarSections[0];
- PreferencesService = preferencesService;
}
[RelayCommand]
@@ -163,6 +172,12 @@ namespace Wino.Mail.ViewModels
if (!isConfirmed) return;
}
+ if (SelectedAlias == null)
+ {
+ DialogService.InfoBarMessage(Translator.DialogMessage_AliasNotSelectedTitle, Translator.DialogMessage_AliasNotSelectedMessage, InfoBarMessageType.Error);
+ return;
+ }
+
// Save mime changes before sending.
await UpdateMimeChangesAsync().ConfigureAwait(false);
@@ -177,11 +192,26 @@ namespace Wino.Mail.ViewModels
int count = (int)memoryStream.Length;
var base64EncodedMessage = Convert.ToBase64String(buffer);
- var draftSendPreparationRequest = new SendDraftPreparationRequest(CurrentMailDraftItem.MailCopy, sentFolder, CurrentMailDraftItem.AssignedFolder, CurrentMailDraftItem.AssignedAccount.Preferences, base64EncodedMessage);
+ var draftSendPreparationRequest = new SendDraftPreparationRequest(CurrentMailDraftItem.MailCopy,
+ SelectedAlias,
+ sentFolder,
+ CurrentMailDraftItem.AssignedFolder,
+ CurrentMailDraftItem.AssignedAccount.Preferences,
+ base64EncodedMessage);
await _worker.ExecuteAsync(draftSendPreparationRequest);
}
+ public async Task IncludeAttachmentAsync(MailAttachmentViewModel viewModel)
+ {
+ //if (bodyBuilder == null) return;
+
+ //bodyBuilder.Attachments.Add(viewModel.FileName, new MemoryStream(viewModel.Content));
+
+ //LoadAttachments();
+ IncludedAttachments.Add(viewModel);
+ }
+
private async Task UpdateMimeChangesAsync()
{
if (isUpdatingMimeBlocked || CurrentMimeMessage == null || ComposingAccount == null || CurrentMailDraftItem == null) return;
@@ -194,6 +224,8 @@ namespace Wino.Mail.ViewModels
SaveImportance();
SaveSubject();
+ SaveFromAddress();
+ SaveReplyToAddress();
await SaveAttachmentsAsync();
await SaveBodyAsync();
@@ -207,6 +239,8 @@ namespace Wino.Mail.ViewModels
{
CurrentMailDraftItem.Subject = CurrentMimeMessage.Subject;
CurrentMailDraftItem.PreviewText = CurrentMimeMessage.TextBody;
+ CurrentMailDraftItem.FromAddress = SelectedAlias.AliasAddress;
+ CurrentMailDraftItem.HasAttachments = CurrentMimeMessage.Attachments.Any();
// Update database.
await _mailService.UpdateMailAsync(CurrentMailDraftItem.MailCopy);
@@ -224,7 +258,10 @@ namespace Wino.Mail.ViewModels
}
}
- private void SaveImportance() { CurrentMimeMessage.Importance = IsImportanceSelected ? SelectedMessageImportance : MessageImportance.Normal; }
+ private void SaveImportance()
+ {
+ CurrentMimeMessage.Importance = IsImportanceSelected ? SelectedMessageImportance : MessageImportance.Normal;
+ }
private void SaveSubject()
{
@@ -234,6 +271,31 @@ namespace Wino.Mail.ViewModels
}
}
+ private void ClearCurrentMimeAttachments()
+ {
+ var attachments = new List();
+ var multiparts = new List();
+ var iter = new MimeIterator(CurrentMimeMessage);
+
+ // collect our list of attachments and their parent multiparts
+ while (iter.MoveNext())
+ {
+ var multipart = iter.Parent as Multipart;
+ var part = iter.Current as MimePart;
+
+ if (multipart != null && part != null && part.IsAttachment)
+ {
+ // keep track of each attachment's parent multipart
+ multiparts.Add(multipart);
+ attachments.Add(part);
+ }
+ }
+
+ // now remove each attachment from its parent multipart...
+ for (int i = 0; i < attachments.Count; i++)
+ multiparts[i].Remove(attachments[i]);
+ }
+
private async Task SaveBodyAsync()
{
if (GetHTMLBodyFunction != null)
@@ -241,8 +303,7 @@ namespace Wino.Mail.ViewModels
bodyBuilder.SetHtmlBody(await GetHTMLBodyFunction());
}
- if (bodyBuilder.HtmlBody != null && bodyBuilder.TextBody != null)
- CurrentMimeMessage.Body = bodyBuilder.ToMessageBody();
+ CurrentMimeMessage.Body = bodyBuilder.ToMessageBody();
}
[RelayCommand(CanExecute = nameof(canSendMail))]
@@ -280,6 +341,8 @@ namespace Wino.Mail.ViewModels
base.OnNavigatedFrom(mode, parameters);
await UpdateMimeChangesAsync().ConfigureAwait(false);
+
+ Messenger.Send(new KillChromiumRequested());
}
public override async void OnNavigatedTo(NavigationMode mode, object parameters)
@@ -288,92 +351,58 @@ namespace Wino.Mail.ViewModels
if (parameters != null && parameters is MailItemViewModel mailItem)
{
- await LoadAccountsAsync();
-
CurrentMailDraftItem = mailItem;
- _ = TryPrepareComposeAsync(true);
- }
-
- ToItems.CollectionChanged -= ContactListCollectionChanged;
- ToItems.CollectionChanged += ContactListCollectionChanged;
-
- // Check if there is any delivering mail address from protocol launch.
-
- if (_launchProtocolService.MailToUri != null)
- {
- // TODO
- //var requestedMailContact = await GetAddressInformationAsync(_launchProtocolService.MailtoParameters, ToItems);
-
- //if (requestedMailContact != null)
- //{
- // ToItems.Add(requestedMailContact);
- //}
- //else
- // DialogService.InfoBarMessage("Invalid Address", "Address is not a valid e-mail address.", InfoBarMessageType.Warning);
-
- // Clear the address.
- _launchProtocolService.MailToUri = null;
- }
- }
-
- private void ContactListCollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
- {
- if (e.Action == System.Collections.Specialized.NotifyCollectionChangedAction.Add)
- {
- // Prevent duplicates.
- if (!(sender is ObservableCollection list))
- return;
-
- foreach (var item in e.NewItems)
- {
- if (item is AddressInformation addedInfo && list.Count(a => a == addedInfo) > 1)
- {
- var addedIndex = list.IndexOf(addedInfo);
- list.RemoveAt(addedIndex);
- }
- }
- }
- }
-
- private async Task LoadAccountsAsync()
- {
- // Load accounts
-
- var accounts = await _accountService.GetAccountsAsync();
-
- foreach (var account in accounts)
- {
- Accounts.Add(account);
+ await TryPrepareComposeAsync(true);
}
}
private async Task InitializeComposerAccountAsync()
{
+ if (CurrentMailDraftItem == null) return false;
+
if (ComposingAccount != null) return true;
- if (CurrentMailDraftItem == null)
- return false;
+ var composingAccount = await _accountService.GetAccountAsync(CurrentMailDraftItem.AssignedAccount.Id).ConfigureAwait(false);
+ if (composingAccount == null) return false;
+
+ var aliases = await _accountService.GetAccountAliasesAsync(composingAccount.Id).ConfigureAwait(false);
+
+ if (aliases == null || !aliases.Any()) return false;
+
+ // MailAccountAlias primaryAlias = aliases.Find(a => a.IsPrimary) ?? aliases.First();
+
+ // Auto-select the correct alias from the message itself.
+ // If can't, fallback to primary alias.
+
+ MailAccountAlias primaryAlias = null;
+
+ if (!string.IsNullOrEmpty(CurrentMailDraftItem.FromAddress))
+ {
+ primaryAlias = aliases.Find(a => a.AliasAddress == CurrentMailDraftItem.FromAddress);
+ }
+
+ primaryAlias ??= await _accountService.GetPrimaryAccountAliasAsync(ComposingAccount.Id).ConfigureAwait(false);
await ExecuteUIThread(() =>
{
- ComposingAccount = Accounts.FirstOrDefault(a => a.Id == CurrentMailDraftItem.AssignedAccount.Id);
+ ComposingAccount = composingAccount;
+ AvailableAliases = aliases;
+ SelectedAlias = primaryAlias;
});
- return ComposingAccount != null;
+ return true;
}
private async Task TryPrepareComposeAsync(bool downloadIfNeeded)
{
- if (CurrentMailDraftItem == null)
- return;
+ if (CurrentMailDraftItem == null) return;
bool isComposerInitialized = await InitializeComposerAccountAsync();
- if (!isComposerInitialized)
- {
- return;
- }
+ if (!isComposerInitialized) return;
+
+ retry:
// Replying existing message.
MimeMessageInformation mimeMessageInformation = null;
@@ -386,18 +415,24 @@ namespace Wino.Mail.ViewModels
{
if (downloadIfNeeded)
{
- // TODO: Folder id needs to be passed.
- // TODO: Send mail retrieve request.
- // _worker.Queue(new FetchSingleItemRequest(ComposingAccount.Id, CurrentMailDraftItem.Id, string.Empty));
+ downloadIfNeeded = false;
+
+ var package = new DownloadMissingMessageRequested(CurrentMailDraftItem.AssignedAccount.Id, CurrentMailDraftItem.MailCopy);
+ var downloadResponse = await _winoServerConnectionManager.GetResponseAsync(package);
+
+ if (downloadResponse.IsSuccess)
+ {
+ goto retry;
+ }
}
- //else
- // DialogService.ShowMIMENotFoundMessage();
+ else
+ DialogService.InfoBarMessage(Translator.Info_ComposerMissingMIMETitle, Translator.Info_ComposerMissingMIMEMessage, InfoBarMessageType.Error);
return;
}
catch (IOException)
{
- DialogService.InfoBarMessage("Busy", "Mail is being processed. Please wait a moment and try again.", InfoBarMessageType.Warning);
+ DialogService.InfoBarMessage(Translator.Busy, Translator.Exception_MailProcessing, InfoBarMessageType.Warning);
}
catch (ComposerMimeNotFoundException)
{
@@ -416,6 +451,8 @@ namespace Wino.Mail.ViewModels
{
// Extract information
+ CurrentMimeMessage = replyingMime;
+
ToItems.Clear();
CCItems.Clear();
BCCItems.Clear();
@@ -424,22 +461,22 @@ namespace Wino.Mail.ViewModels
LoadAddressInfo(replyingMime.Cc, CCItems);
LoadAddressInfo(replyingMime.Bcc, BCCItems);
- LoadAttachments(replyingMime.Attachments);
+ LoadAttachments();
if (replyingMime.Cc.Any() || replyingMime.Bcc.Any())
IsCCBCCVisible = true;
Subject = replyingMime.Subject;
- CurrentMimeMessage = replyingMime;
-
Messenger.Send(new CreateNewComposeMailRequested(renderModel));
});
}
- private void LoadAttachments(IEnumerable mimeEntities)
+ private void LoadAttachments()
{
- foreach (var attachment in mimeEntities)
+ if (CurrentMimeMessage == null) return;
+
+ foreach (var attachment in CurrentMimeMessage.Attachments)
{
if (attachment.IsAttachment && attachment is MimePart attachmentPart)
{
@@ -459,6 +496,28 @@ namespace Wino.Mail.ViewModels
}
}
+ private void SaveFromAddress()
+ {
+ if (SelectedAlias == null) return;
+
+ CurrentMimeMessage.From.Clear();
+ CurrentMimeMessage.From.Add(new MailboxAddress(ComposingAccount.SenderName, SelectedAlias.AliasAddress));
+ }
+
+ private void SaveReplyToAddress()
+ {
+ if (SelectedAlias == null) return;
+
+ 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));
+ }
+ }
+ }
+
private void SaveAddressInfo(IEnumerable addresses, InternetAddressList list)
{
list.Clear();
diff --git a/Wino.Mail.ViewModels/Data/AccountProviderDetailViewModel.cs b/Wino.Mail.ViewModels/Data/AccountProviderDetailViewModel.cs
index 8cdacde7..3d1c5e3a 100644
--- a/Wino.Mail.ViewModels/Data/AccountProviderDetailViewModel.cs
+++ b/Wino.Mail.ViewModels/Data/AccountProviderDetailViewModel.cs
@@ -23,6 +23,8 @@ namespace Wino.Mail.ViewModels.Data
public int HoldingAccountCount => 1;
+ public bool HasProfilePicture => !string.IsNullOrEmpty(Account.Base64ProfilePictureData);
+
public AccountProviderDetailViewModel(IProviderDetail providerDetail, MailAccount account)
{
ProviderDetail = providerDetail;
diff --git a/Wino.Mail.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs b/Wino.Mail.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs
index c90b81aa..9ba7f782 100644
--- a/Wino.Mail.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs
+++ b/Wino.Mail.ViewModels/Data/BreadcrumbNavigationItemViewModel.cs
@@ -3,31 +3,21 @@ using Wino.Messaging.Client.Navigation;
namespace Wino.Mail.ViewModels.Data
{
- public class BreadcrumbNavigationItemViewModel : ObservableObject
+ public partial class BreadcrumbNavigationItemViewModel : ObservableObject
{
+ [ObservableProperty]
+ private string title;
+
+ [ObservableProperty]
+ private bool isActive;
+
public BreadcrumbNavigationRequested Request { get; set; }
public BreadcrumbNavigationItemViewModel(BreadcrumbNavigationRequested request, bool isActive)
{
Request = request;
Title = request.PageTitle;
-
- this.isActive = isActive;
- }
-
- private string title;
- public string Title
- {
- get => title;
- set => SetProperty(ref title, value);
- }
-
- private bool isActive;
-
- public bool IsActive
- {
- get => isActive;
- set => SetProperty(ref isActive, value);
+ IsActive = isActive;
}
}
}
diff --git a/Wino.Mail.ViewModels/Data/MailAttachmentViewModel.cs b/Wino.Mail.ViewModels/Data/MailAttachmentViewModel.cs
index 90387ce4..469b55ba 100644
--- a/Wino.Mail.ViewModels/Data/MailAttachmentViewModel.cs
+++ b/Wino.Mail.ViewModels/Data/MailAttachmentViewModel.cs
@@ -6,9 +6,8 @@ using Wino.Core.Extensions;
namespace Wino.Mail.ViewModels.Data
{
- public class MailAttachmentViewModel : ObservableObject
+ public partial class MailAttachmentViewModel : ObservableObject
{
- private bool isBusy;
private readonly MimePart _mimePart;
public MailAttachmentType AttachmentType { get; }
@@ -22,23 +21,21 @@ namespace Wino.Mail.ViewModels.Data
///
/// Gets or sets whether attachment is busy with opening or saving etc.
///
- public bool IsBusy
- {
- get => isBusy;
- set => SetProperty(ref isBusy, value);
- }
+ [ObservableProperty]
+ private bool isBusy;
public MailAttachmentViewModel(MimePart mimePart)
{
_mimePart = mimePart;
- var array = new byte[_mimePart.Content.Stream.Length];
- _mimePart.Content.Stream.Read(array, 0, (int)_mimePart.Content.Stream.Length);
+ var memoryStream = new MemoryStream();
- Content = array;
+ using (memoryStream) mimePart.Content.DecodeTo(memoryStream);
+
+ Content = memoryStream.ToArray();
FileName = mimePart.FileName;
- ReadableSize = mimePart.Content.Stream.Length.GetBytesReadable();
+ ReadableSize = ((long)Content.Length).GetBytesReadable();
var extension = Path.GetExtension(FileName);
AttachmentType = GetAttachmentType(extension);
diff --git a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs
index aa77ead0..d96e486e 100644
--- a/Wino.Mail.ViewModels/Data/MailItemViewModel.cs
+++ b/Wino.Mail.ViewModels/Data/MailItemViewModel.cs
@@ -18,8 +18,6 @@ namespace Wino.Mail.ViewModels.Data
public string MessageId => ((IMailItem)MailCopy).MessageId;
public string FromName => ((IMailItem)MailCopy).FromName ?? FromAddress;
public DateTime CreationDate => ((IMailItem)MailCopy).CreationDate;
- public string FromAddress => ((IMailItem)MailCopy).FromAddress;
- public bool HasAttachments => ((IMailItem)MailCopy).HasAttachments;
public string References => ((IMailItem)MailCopy).References;
public string InReplyTo => ((IMailItem)MailCopy).InReplyTo;
@@ -77,6 +75,18 @@ namespace Wino.Mail.ViewModels.Data
set => SetProperty(MailCopy.PreviewText, value, MailCopy, (u, n) => u.PreviewText = n);
}
+ public string FromAddress
+ {
+ get => MailCopy.FromAddress;
+ set => SetProperty(MailCopy.FromAddress, value, MailCopy, (u, n) => u.FromAddress = n);
+ }
+
+ public bool HasAttachments
+ {
+ get => MailCopy.HasAttachments;
+ set => SetProperty(MailCopy.HasAttachments, value, MailCopy, (u, n) => u.HasAttachments = n);
+ }
+
public MailItemFolder AssignedFolder => ((IMailItem)MailCopy).AssignedFolder;
public MailAccount AssignedAccount => ((IMailItem)MailCopy).AssignedAccount;
@@ -94,6 +104,8 @@ namespace Wino.Mail.ViewModels.Data
OnPropertyChanged(nameof(DraftId));
OnPropertyChanged(nameof(Subject));
OnPropertyChanged(nameof(PreviewText));
+ OnPropertyChanged(nameof(FromAddress));
+ OnPropertyChanged(nameof(HasAttachments));
}
public IEnumerable GetContainingIds() => new[] { UniqueId };
diff --git a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs
index 305a7ece..7fe96b2e 100644
--- a/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs
+++ b/Wino.Mail.ViewModels/MailRenderingPageViewModel.cs
@@ -273,7 +273,7 @@ namespace Wino.Mail.ViewModels
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount.Id, draftOptions).ConfigureAwait(false);
- var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.AssignedAccount, draftMailCopy, draftBase64MimeMessage, initializedMailItemViewModel.MailCopy);
+ var draftPreparationRequest = new DraftPreparationRequest(initializedMailItemViewModel.AssignedAccount, draftMailCopy, draftBase64MimeMessage, draftOptions.Reason, initializedMailItemViewModel.MailCopy);
await _requestDelegator.ExecuteAsync(draftPreparationRequest);
diff --git a/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj b/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj
index 946a3bf8..82c61360 100644
--- a/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj
+++ b/Wino.Mail.ViewModels/Wino.Mail.ViewModels.csproj
@@ -7,6 +7,7 @@
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/Wino.Mail/App.xaml b/Wino.Mail/App.xaml
index 64b6c59e..57cb3f20 100644
--- a/Wino.Mail/App.xaml
+++ b/Wino.Mail/App.xaml
@@ -1,9 +1,10 @@
-
+
@@ -48,6 +49,14 @@
+
+
+
-
+