48 Commits

Author SHA1 Message Date
Burak Kaan Köse
d623129d56 Merge pull request #325 from bkaankose/features/mail-list-splitter
Mail List splitter
2024-08-19 21:21:28 +02:00
Aleh Khantsevich
9cc4c33bb1 Fix multiselect and hide sizer 2024-08-19 21:16:58 +02:00
Burak Kaan Köse
c087b40d4a Renaming event handlers and fixing the search box margin issue. 2024-08-19 21:07:21 +02:00
Aleh Khantsevich
a82e074bd4 Fix merge conflicts 2024-08-19 20:50:14 +02:00
Burak Kaan Köse
3365c099bb Missing comments. 2024-08-19 20:43:26 +02:00
Aleh Khantsevich
d8705de26f Replaced setting with reset button 2024-08-19 20:41:55 +02:00
Burak Kaan Köse
3af181e736 Merge pull request #324 from bkaankose/feature/Aliases
E-mail Aliases Support
2024-08-19 20:37:58 +02:00
Burak Kaan Köse
ba6c01b7c6 Missing comments. 2024-08-19 19:16:54 +02:00
Burak Kaan Köse
7a7cdcb041 Remove json.net dependency to STJson 2024-08-19 19:16:47 +02:00
Burak Kaan Köse
09e52bf199 Remove not implemented comment. 2024-08-19 19:05:54 +02:00
Burak Kaan Köse
a8c39a1587 Fixing send draft issue with OutlookSynchronizer. 2024-08-19 19:02:33 +02:00
Aleh Khantsevich
68536d6c34 Fix padding in narrow state 2024-08-19 18:49:35 +02:00
Aleh Khantsevich
f57c27e755 Fix multiple items selected 2024-08-19 17:15:59 +02:00
Aleh Khantsevich
9a97a27c8a Init 2024-08-19 16:26:15 +02:00
Burak Kaan Köse
07bb90dda9 Refactoring outlook draft creation and sending. 2024-08-19 03:44:16 +02:00
Burak Kaan Köse
3bb156f4da Handling of OutlookSynchronizer alias. 2024-08-18 22:45:23 +02:00
Burak Kaan Köse
e13e0efcc6 Bump some nugets. 2024-08-18 22:44:55 +02:00
Burak Kaan Köse
3ae0a94159 Remove folder init progress bar in mail list page. 2024-08-18 22:27:31 +02:00
Burak Kaan Köse
eec67ec7dc Fixed an issue where re-loading messages with attachments break the included attachment encodings. 2024-08-18 22:25:29 +02:00
Burak Kaan Köse
cf51853eec Removed non-mandatory reply-to validation. 2024-08-18 01:11:23 +02:00
Burak Kaan Köse
67838b28a4 Syntactic sugar. 2024-08-18 01:06:21 +02:00
Burak Kaan Köse
bf68e3b7d5 Fix sending draft issue. 2024-08-18 01:05:43 +02:00
Burak Kaan Köse
91ed0bb8bd Ability to select alias in composer page. 2024-08-17 22:55:58 +02:00
Burak Kaan Köse
55fe791c2a Handling of missing mime downloads and mail processed messages. 2024-08-17 20:19:01 +02:00
Burak Kaan Köse
747efac2ec Reworked aliases. 2024-08-17 19:54:52 +02:00
Burak Kaan Köse
a87df2e9f6 Fixed an issue where deleting account navigates back to mail list of the next account. 2024-08-17 19:54:44 +02:00
Burak Kaan Köse
2e4a664744 More detailed ImapImplementation for ID extension. 2024-08-17 19:53:50 +02:00
Tiktack
579a22ea45 Remove self from reply all when no other recepients (#319) 2024-08-17 15:00:25 +02:00
Burak Kaan Köse
abff850427 Managing account aliases and profile synchronization for outlook and gmail. 2024-08-17 03:43:37 +02:00
Burak Kaan Köse
f1154058ba Fix ascending download for messages. QQ server issue will be handled later. 2024-08-17 00:03:45 +02:00
Burak Kaan Köse
cf9f308b7f Updating aliases during profile sync for Gmail. 2024-08-16 01:29:31 +02:00
Burak Kaan Köse
1791df236c Remove unused extension class. 2024-08-16 01:03:00 +02:00
Burak Kaan Köse
7211f94f08 Try - catch for outlook profile sync. 2024-08-16 00:40:10 +02:00
Burak Kaan Köse
7b0343c87f Added sender name comment for gmail. 2024-08-16 00:37:50 +02:00
Burak Kaan Köse
b80f0276b4 Sender Name and Profile Picture synchronization for Outlook 2024-08-16 00:37:38 +02:00
Burak Kaan Köse
8f66fcbb00 Activated contact service for Gmail to retrieve profile picture and sender name. 2024-08-15 23:57:45 +02:00
Burak Kaan Köse
fe449ee1f3 Comments for alias entity. 2024-08-15 16:13:18 +02:00
Burak Kaan Köse
34d6d95186 Including ReplyToAddress for alias. 2024-08-15 16:11:12 +02:00
Burak Kaan Köse
05ddc0660a Creating MailAccountAlias entity. 2024-08-15 16:02:02 +02:00
Burak Kaan Köse
c6047a8428 Version bump 2024-08-13 23:40:09 +02:00
Burak Kaan Köse
bc4838578e Handling null client connection while sending server response. 2024-08-13 22:57:36 +02:00
Burak Kaan Köse
548996405a Fix incorrect accounts' mails are going to different accounts. 2024-08-13 22:54:36 +02:00
Burak Kaan Köse
a9a5f0bd14 Ascending downloading of mails since some servers require it. 2024-08-13 22:54:14 +02:00
Burak Kaan Köse
ec05ff6123 Optional splash screen. 2024-08-13 19:26:24 +02:00
Burak Kaan Köse
10c7ab421b Setting exception on connection failure. 2024-08-13 16:14:25 +02:00
Burak Kaan Köse
a8a5cc53ea Merge branch 'main' of https://github.com/bkaankose/Wino-Mail 2024-08-13 16:13:25 +02:00
Burak Kaan Köse
8fe48ca438 Fixed an issue where reconnecting doesn't await the handle in the second attempt. 2024-08-13 16:12:34 +02:00
Tiktack
cbd5a515a9 Fix account signature preferences during draft creation (#314)
* Pass account ID instead of account to draft creation method, since account object can be stale.

* Configure await
2024-08-12 00:56:26 +02:00
86 changed files with 2242 additions and 884 deletions

View File

@@ -44,6 +44,11 @@ namespace Wino.Core.Domain.Entities
/// </summary>
public string AccountColorHex { get; set; }
/// <summary>
/// Base64 encoded profile picture of the account.
/// </summary>
public string Base64ProfilePictureData { get; set; }
/// <summary>
/// Gets or sets the listing order of the account in the accounts list.
/// </summary>
@@ -78,5 +83,15 @@ namespace Wino.Core.Domain.Entities
/// </summary>
[Ignore]
public MailAccountPreferences Preferences { get; set; }
/// <summary>
/// Gets whether the account can perform ProfileInformation sync type.
/// </summary>
public bool IsProfileInfoSyncSupported => ProviderType == MailProviderType.Outlook || ProviderType == MailProviderType.Office365 || ProviderType == MailProviderType.Gmail;
/// <summary>
/// Gets whether the account can perform AliasInformation sync type.
/// </summary>
public bool IsAliasSyncSupported => ProviderType == MailProviderType.Gmail;
}
}

View File

@@ -0,0 +1,56 @@
using System;
using SQLite;
namespace Wino.Core.Domain.Entities
{
public class RemoteAccountAlias
{
/// <summary>
/// Display address of the alias.
/// </summary>
public string AliasAddress { get; set; }
/// <summary>
/// Address to be included in Reply-To header when alias is used for sending messages.
/// </summary>
public string ReplyToAddress { get; set; }
/// <summary>
/// Whether this alias is the primary alias for the account.
/// </summary>
public bool IsPrimary { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool IsVerified { get; set; }
/// <summary>
/// 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.
/// </summary>
public bool IsRootAlias { get; set; }
}
public class MailAccountAlias : RemoteAccountAlias
{
/// <summary>
/// Unique Id for the alias.
/// </summary>
[PrimaryKey]
public Guid Id { get; set; }
/// <summary>
/// Account id that this alias is attached to.
/// </summary>
public Guid AccountId { get; set; }
/// <summary>
/// Root aliases can't be deleted.
/// </summary>
public bool CanDelete => !IsRootAlias;
}
}

View File

@@ -141,7 +141,7 @@ namespace Wino.Core.Domain.Entities
/// </summary>
[Ignore]
public MailAccount AssignedAccount { get; set; }
public IEnumerable<Guid> GetContainingIds() => new[] { UniqueId };
public IEnumerable<Guid> GetContainingIds() => [UniqueId];
public override string ToString() => $"{Subject} <-> {Id}";
}
}

View File

@@ -9,6 +9,7 @@
ManuelSetupWaiting,
TestingConnection,
AutoDiscoverySetup,
AutoDiscoveryInProgress
AutoDiscoveryInProgress,
FetchingProfileInformation
}
}

View File

@@ -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
}
}

View File

@@ -23,5 +23,6 @@
LanguageTimePage,
AppPreferencesPage,
SettingOptionsPage,
AliasManagementPage
}
}

View File

@@ -0,0 +1,7 @@
namespace Wino.Core.Domain.Exceptions
{
public class MissingAliasException : System.Exception
{
public MissingAliasException() : base(Translator.Exception_MissingAlias) { }
}
}

View File

@@ -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
/// </summary>
/// <param name="accountIdOrderPair">AccountId-OrderNumber pair for all accounts.</param>
Task UpdateAccountOrdersAsync(Dictionary<Guid, int> accountIdOrderPair);
/// <summary>
/// Returns the account aliases.
/// </summary>
/// <param name="accountId">Account id.</param>
/// <returns>A list of MailAccountAlias that has e-mail aliases.</returns>
Task<List<MailAccountAlias>> GetAccountAliasesAsync(Guid accountId);
/// <summary>
/// Updated account's aliases.
/// </summary>
/// <param name="accountId">Account id to update aliases for.</param>
/// <param name="aliases">Full list of updated aliases.</param>
/// <returns></returns>
Task UpdateAccountAliasesAsync(Guid accountId, List<MailAccountAlias> aliases);
/// <summary>
/// Delete account alias.
/// </summary>
/// <param name="aliasId">Alias to remove.</param>
Task DeleteAccountAliasAsync(Guid aliasId);
/// <summary>
/// Updated profile information of the account.
/// </summary>
/// <param name="accountId">Account id to update info for.</param>
/// <param name="profileInformation">Info data.</param>
/// <returns></returns>
Task UpdateProfileInformationAsync(Guid accountId, ProfileInformation profileInformation);
/// <summary>
/// Creates a root + primary alias for the account.
/// This is only called when the account is created.
/// </summary>
/// <param name="accountId">Account id.</param>
/// <param name="address">Address to create root primary alias from.</param>
Task CreateRootAliasAsync(Guid accountId, string address);
/// <summary>
/// Will compare local-remote aliases and update the local ones or add/delete new ones.
/// </summary>
/// <param name="remoteAccountAliases">Remotely fetched basic alias info from synchronizer.</param>
/// <param name="account">Account to update remote aliases for..</param>
Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> remoteAccountAliases);
/// <summary>
/// Gets the primary account alias for the given account id.
/// Used when creating draft messages.
/// </summary>
/// <param name="accountId">Account id.</param>
/// <returns>Primary alias for the account.</returns>
Task<MailAccountAlias> GetPrimaryAccountAliasAsync(Guid accountId);
}
}

View File

@@ -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
/// <returns>Result summary of synchronization.</returns>
Task<SynchronizationResult> SynchronizeAsync(SynchronizationOptions options, CancellationToken cancellationToken = default);
/// <summary>
/// Synchronizes profile information with the server.
/// Sender name and Profile picture are updated.
/// </summary>
/// <returns>Profile information model that holds the values.</returns>
Task<ProfileInformation> GetProfileInformationAsync();
/// <summary>
/// Downloads a single MIME message from the server and saves it to disk.
/// </summary>

View File

@@ -0,0 +1,9 @@
using Wino.Core.Domain.Entities;
namespace Wino.Core.Domain.Interfaces
{
public interface ICreateAccountAliasDialog
{
public MailAccountAlias CreatedAccountAlias { get; set; }
}
}

View File

@@ -53,5 +53,6 @@ namespace Wino.Core.Domain.Interfaces
/// </summary>
/// <returns>Signature information. Null if canceled.</returns>
Task<AccountSignature> ShowSignatureEditorDialog(AccountSignature signatureModel = null);
Task<ICreateAccountAliasDialog> ShowCreateAccountAliasDialogAsync();
}
}

View File

@@ -103,9 +103,9 @@ namespace Wino.Core.Domain.Interfaces
/// Creates a draft MailCopy and MimeMessage based on the given options.
/// For forward/reply it would include the referenced message.
/// </summary>
/// <param name="composerAccount">Account which should have new draft.</param>
/// <param name="accountId">AccountId which should have new draft.</param>
/// <param name="draftCreationOptions">Options like new email/forward/draft.</param>
/// <returns>Draft MailCopy and Draft MimeMessage as base64.</returns>
Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(MailAccount composerAccount, DraftCreationOptions draftCreationOptions);
Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions);
}
}

View File

@@ -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 = "");
}

View File

@@ -0,0 +1,9 @@
namespace Wino.Core.Domain.Models.Accounts
{
/// <summary>
/// Encapsulates the profile information of an account.
/// </summary>
/// <param name="SenderName">Display sender name for the account.</param>
/// <param name="Base64ProfilePictureData">Base 64 encoded profile picture data of the account. Thumbnail size.</param>
public record ProfileInformation(string SenderName, string Base64ProfilePictureData);
}

View File

@@ -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)
{

View File

@@ -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,

View File

@@ -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; }
}

View File

@@ -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();
}
}

View File

@@ -1,4 +0,0 @@
namespace Wino.Core.Domain.Models.Personalization
{
public record MailListPaneLengthPreferences(string Title, double Length);
}

View File

@@ -3,14 +3,11 @@ using Wino.Core.Domain.Interfaces;
namespace Wino.Core.Domain.Models.Requests
{
/// <summary>
/// Encapsulates request to queue and account for synchronizer.
/// </summary>
/// <param name="AccountId"><inheritdoc/></param>
/// <param name="Request"></param>
/// <param name="AccountId">Which account to execute this request for.</param>
/// <param name="Request">Prepared request for the server.</param>
/// <param name="AccountId">Whihc account to execute this request for.</param>
public record ServerRequestPackage(Guid AccountId, IRequestBase Request) : IClientMessage
{
public override string ToString() => $"Server Package: {Request.GetType().Name}";

View File

@@ -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.
/// </summary>
[JsonIgnore]
public IEnumerable<IMailItem> DownloadedMessages { get; set; } = new List<IMailItem>();
public IEnumerable<IMailItem> 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<IMailItem> downloadedMessages)
=> new() { DownloadedMessages = downloadedMessages, CompletedState = SynchronizationCompletedState.Success };
public static SynchronizationResult Completed(IEnumerable<IMailItem> 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 };
}
}

View File

@@ -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",
@@ -42,6 +45,7 @@
"Buttons_SignIn": "Sign In",
"Buttons_TryAgain": "Try Again",
"Buttons_Yes": "Yes",
"Buttons_Reset": "Reset",
"Center": "Center",
"ComingSoon": "Coming soon...",
"ComposerFrom": "From: ",
@@ -64,6 +68,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 +106,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 +127,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 +135,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 +278,8 @@
"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",
"Info_MailListSizeResetSuccessMessage": "The Mail List size has been reset.",
"ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method",
"ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security",
"ImapAuthenticationMethod_Auto": "Auto",
@@ -396,6 +423,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 +435,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.",
@@ -448,8 +482,8 @@
"SettingsNoAccountSetupMessage": "You didn't setup any accounts yet.",
"SettingsNotifications_Description": "Turn on or off notifications for this account.",
"SettingsNotifications_Title": "Notifications",
"SettingsPaneLength_Description": "Change the width of the mail list.",
"SettingsPaneLength_Title": "Mail List Pane Length",
"SettingsPaneLengthReset_Description": "Reset the size of the mail list to original if you have issues with it.",
"SettingsPaneLengthReset_Title": "Reset Mail List Size",
"SettingsPaypal_Description": "Show much more love ❤️ All donations are appreciated.",
"SettingsPaypal_Title": "Donate via PayPal",
"SettingsPersonalizationMailDisplayCompactMode": "Compact Mode",

View File

@@ -38,6 +38,11 @@ namespace Wino.Core.Domain
/// </summary>
public static string AccountCreationDialog_SigninIn => Resources.GetTranslatedString(@"AccountCreationDialog_SigninIn");
/// <summary>
/// Fetching profile details.
/// </summary>
public static string AccountCreationDialog_FetchingProfileInformation => Resources.GetTranslatedString(@"AccountCreationDialog_FetchingProfileInformation");
/// <summary>
/// Account Name
/// </summary>
@@ -128,6 +133,16 @@ namespace Wino.Core.Domain
/// </summary>
public static string Buttons_AddAccount => Resources.GetTranslatedString(@"Buttons_AddAccount");
/// <summary>
/// Add New Alias
/// </summary>
public static string Buttons_AddNewAlias => Resources.GetTranslatedString(@"Buttons_AddNewAlias");
/// <summary>
/// Synchronize Aliases
/// </summary>
public static string Buttons_SyncAliases => Resources.GetTranslatedString(@"Buttons_SyncAliases");
/// <summary>
/// Apply Theme
/// </summary>
@@ -233,6 +248,11 @@ namespace Wino.Core.Domain
/// </summary>
public static string Buttons_Yes => Resources.GetTranslatedString(@"Buttons_Yes");
/// <summary>
/// Reset
/// </summary>
public static string Buttons_Reset => Resources.GetTranslatedString(@"Buttons_Reset");
/// <summary>
/// Center
/// </summary>
@@ -343,6 +363,56 @@ namespace Wino.Core.Domain
/// </summary>
public static string DialogMessage_AccountLimitTitle => Resources.GetTranslatedString(@"DialogMessage_AccountLimitTitle");
/// <summary>
/// Missing Alias
/// </summary>
public static string DialogMessage_AliasNotSelectedTitle => Resources.GetTranslatedString(@"DialogMessage_AliasNotSelectedTitle");
/// <summary>
/// You must select an alias before sending a message.
/// </summary>
public static string DialogMessage_AliasNotSelectedMessage => Resources.GetTranslatedString(@"DialogMessage_AliasNotSelectedMessage");
/// <summary>
/// Existing Alias
/// </summary>
public static string DialogMessage_AliasExistsTitle => Resources.GetTranslatedString(@"DialogMessage_AliasExistsTitle");
/// <summary>
/// This alias is already in use.
/// </summary>
public static string DialogMessage_AliasExistsMessage => Resources.GetTranslatedString(@"DialogMessage_AliasExistsMessage");
/// <summary>
/// Invalid Alias
/// </summary>
public static string DialogMessage_InvalidAliasTitle => Resources.GetTranslatedString(@"DialogMessage_InvalidAliasTitle");
/// <summary>
/// This alias is not valid. Make sure all addresses of the alias are valid e-mail addresses.
/// </summary>
public static string DialogMessage_InvalidAliasMessage => Resources.GetTranslatedString(@"DialogMessage_InvalidAliasMessage");
/// <summary>
/// Can't Delete Alias
/// </summary>
public static string DialogMessage_CantDeleteRootAliasTitle => Resources.GetTranslatedString(@"DialogMessage_CantDeleteRootAliasTitle");
/// <summary>
/// Root alias can't be deleted. This is your main identity associated with your account setup.
/// </summary>
public static string DialogMessage_CantDeleteRootAliasMessage => Resources.GetTranslatedString(@"DialogMessage_CantDeleteRootAliasMessage");
/// <summary>
/// Created New Alias
/// </summary>
public static string DialogMessage_AliasCreatedTitle => Resources.GetTranslatedString(@"DialogMessage_AliasCreatedTitle");
/// <summary>
/// New alias is succesfully created.
/// </summary>
public static string DialogMessage_AliasCreatedMessage => Resources.GetTranslatedString(@"DialogMessage_AliasCreatedMessage");
/// <summary>
/// Do you want to permanently delete all the mails in this folder?
/// </summary>
@@ -434,7 +504,7 @@ namespace Wino.Core.Domain
public static string DialogMessage_UnlinkAccountsConfirmationTitle => Resources.GetTranslatedString(@"DialogMessage_UnlinkAccountsConfirmationTitle");
/// <summary>
/// Missin Subject
/// Missing Subject
/// </summary>
public static string DialogMessage_EmptySubjectConfirmation => Resources.GetTranslatedString(@"DialogMessage_EmptySubjectConfirmation");
@@ -483,6 +553,36 @@ namespace Wino.Core.Domain
/// </summary>
public static string Dialog_DontAskAgain => Resources.GetTranslatedString(@"Dialog_DontAskAgain");
/// <summary>
/// Create Account Alias
/// </summary>
public static string CreateAccountAliasDialog_Title => Resources.GetTranslatedString(@"CreateAccountAliasDialog_Title");
/// <summary>
/// Make sure your outgoing server allows sending mails from this alias.
/// </summary>
public static string CreateAccountAliasDialog_Description => Resources.GetTranslatedString(@"CreateAccountAliasDialog_Description");
/// <summary>
/// Address
/// </summary>
public static string CreateAccountAliasDialog_AliasAddress => Resources.GetTranslatedString(@"CreateAccountAliasDialog_AliasAddress");
/// <summary>
/// eg. support@mydomain.com
/// </summary>
public static string CreateAccountAliasDialog_AliasAddressPlaceholder => Resources.GetTranslatedString(@"CreateAccountAliasDialog_AliasAddressPlaceholder");
/// <summary>
/// Reply-To Address
/// </summary>
public static string CreateAccountAliasDialog_ReplyToAddress => Resources.GetTranslatedString(@"CreateAccountAliasDialog_ReplyToAddress");
/// <summary>
/// admin@mydomain.com
/// </summary>
public static string CreateAccountAliasDialog_ReplyToAddressPlaceholder => Resources.GetTranslatedString(@"CreateAccountAliasDialog_ReplyToAddressPlaceholder");
/// <summary>
/// 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.
/// </summary>
@@ -498,6 +598,11 @@ namespace Wino.Core.Domain
/// </summary>
public static string Draft => Resources.GetTranslatedString(@"Draft");
/// <summary>
/// Busy
/// </summary>
public static string Busy => Resources.GetTranslatedString(@"Busy");
/// <summary>
/// Draw
/// </summary>
@@ -553,6 +658,11 @@ namespace Wino.Core.Domain
/// </summary>
public static string Exception_WinoServerException => Resources.GetTranslatedString(@"Exception_WinoServerException");
/// <summary>
/// This mail is still being processed. Please try again after few seconds.
/// </summary>
public static string Exception_MailProcessing => Resources.GetTranslatedString(@"Exception_MailProcessing");
/// <summary>
/// Couldn't find mailbox settings.
/// </summary>
@@ -588,6 +698,21 @@ namespace Wino.Core.Domain
/// </summary>
public static string Exception_FailedToSynchronizeFolders => Resources.GetTranslatedString(@"Exception_FailedToSynchronizeFolders");
/// <summary>
/// Failed to synchronize aliases
/// </summary>
public static string Exception_FailedToSynchronizeAliases => Resources.GetTranslatedString(@"Exception_FailedToSynchronizeAliases");
/// <summary>
/// Primary alias does not exist for this account. Creating draft failed.
/// </summary>
public static string Exception_MissingAlias => Resources.GetTranslatedString(@"Exception_MissingAlias");
/// <summary>
/// Failed to synchronize profile information
/// </summary>
public static string Exception_FailedToSynchronizeProfileInformation => Resources.GetTranslatedString(@"Exception_FailedToSynchronizeProfileInformation");
/// <summary>
/// Callback uri is null on activation.
/// </summary>
@@ -1288,6 +1413,16 @@ namespace Wino.Core.Domain
/// </summary>
public static string Info_UnsubscribeErrorMessage => Resources.GetTranslatedString(@"Info_UnsubscribeErrorMessage");
/// <summary>
/// Primary alias can't be deleted. Please change your alias before deleting this one
/// </summary>
public static string Info_CantDeletePrimaryAliasMessage => Resources.GetTranslatedString(@"Info_CantDeletePrimaryAliasMessage");
/// <summary>
/// The Mail List size has been reset.
/// </summary>
public static string Info_MailListSizeResetSuccessMessage => Resources.GetTranslatedString(@"Info_MailListSizeResetSuccessMessage");
/// <summary>
/// Authentication method
/// </summary>
@@ -2003,6 +2138,16 @@ namespace Wino.Core.Domain
/// </summary>
public static string SettingsFolderOptions_Description => Resources.GetTranslatedString(@"SettingsFolderOptions_Description");
/// <summary>
/// Aliases
/// </summary>
public static string SettingsManageAliases_Title => Resources.GetTranslatedString(@"SettingsManageAliases_Title");
/// <summary>
/// See e-mail aliases assigned for this account, update or delete them.
/// </summary>
public static string SettingsManageAliases_Description => Resources.GetTranslatedString(@"SettingsManageAliases_Description");
/// <summary>
/// Center Action
/// </summary>
@@ -2053,6 +2198,31 @@ namespace Wino.Core.Domain
/// </summary>
public static string CategoriesFolderNameOverride => Resources.GetTranslatedString(@"CategoriesFolderNameOverride");
/// <summary>
/// Verified
/// </summary>
public static string AccountAlias_Column_Verified => Resources.GetTranslatedString(@"AccountAlias_Column_Verified");
/// <summary>
/// Alias
/// </summary>
public static string AccountAlias_Column_Alias => Resources.GetTranslatedString(@"AccountAlias_Column_Alias");
/// <summary>
/// Primary
/// </summary>
public static string AccountAlias_Column_IsPrimaryAlias => Resources.GetTranslatedString(@"AccountAlias_Column_IsPrimaryAlias");
/// <summary>
/// Wino can only import aliases for your Gmail accounts.
/// </summary>
public static string AccountAlias_Disclaimer_FirstLine => Resources.GetTranslatedString(@"AccountAlias_Disclaimer_FirstLine");
/// <summary>
/// If you want to use aliases for your Outlook or IMAP account, please add them yourself.
/// </summary>
public static string AccountAlias_Disclaimer_SecondLine => Resources.GetTranslatedString(@"AccountAlias_Disclaimer_SecondLine");
/// <summary>
/// More
/// </summary>
@@ -2264,14 +2434,14 @@ namespace Wino.Core.Domain
public static string SettingsNotifications_Title => Resources.GetTranslatedString(@"SettingsNotifications_Title");
/// <summary>
/// Change the width of the mail list.
/// Reset the size of the mail list to original if you have issues with it.
/// </summary>
public static string SettingsPaneLength_Description => Resources.GetTranslatedString(@"SettingsPaneLength_Description");
public static string SettingsPaneLengthReset_Description => Resources.GetTranslatedString(@"SettingsPaneLengthReset_Description");
/// <summary>
/// Mail List Pane Length
/// Reset Mail List Size
/// </summary>
public static string SettingsPaneLength_Title => Resources.GetTranslatedString(@"SettingsPaneLength_Title");
public static string SettingsPaneLengthReset_Title => Resources.GetTranslatedString(@"SettingsPaneLengthReset_Title");
/// <summary>
/// Show much more love ❤️ All donations are appreciated.

View File

@@ -109,12 +109,12 @@ namespace Wino.Core.UWP.Services
{
try
{
ConnectingHandle ??= new TaskCompletionSource<bool>();
var connectionCancellationToken = new CancellationTokenSource(TimeSpan.FromMilliseconds(ServerConnectionTimeoutMs));
ConnectingHandle = new TaskCompletionSource<bool>();
Status = WinoServerConnectionStatus.Connecting;
var connectionCancellationToken = new CancellationTokenSource(TimeSpan.FromMilliseconds(ServerConnectionTimeoutMs));
await FullTrustProcessLauncher.LaunchFullTrustProcessForCurrentAppAsync();
// Connection establishment handler is in App.xaml.cs OnBackgroundActivated.
@@ -125,10 +125,21 @@ namespace Wino.Core.UWP.Services
Log.Information("Server connection established successfully.");
}
catch (OperationCanceledException canceledException)
{
Log.Error(canceledException, $"Server process did not start in {ServerConnectionTimeoutMs} ms. Operation is canceled.");
ConnectingHandle?.TrySetException(canceledException);
Status = WinoServerConnectionStatus.Failed;
return false;
}
catch (Exception ex)
{
Log.Error(ex, "Failed to connect to the server.");
ConnectingHandle?.TrySetException(ex);
Status = WinoServerConnectionStatus.Failed;
return false;
}

View File

@@ -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;

View File

@@ -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<IMailItemFolder, FolderMenuItem> GetMenuItemByFolderRecursive(IMailItemFolder structure, AccountMenuItem parentAccountMenuItem, IMenuItem parentFolderItem)
{
MenuItemBase<IMailItemFolder, FolderMenuItem> 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;
}
}
}

View File

@@ -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<MailCopy, MimeMessage, IEnumerable<string>> GetMailDetails(this Message message)
public static List<RemoteAccountAlias> 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, IEnumerable<string>>(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();
}
}
}

View File

@@ -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<Recipient> 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<InternetMessageHeader> 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<InternetMessageHeader>();
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
}
}

View File

@@ -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;

View File

@@ -19,6 +19,7 @@ namespace Wino.Core.Integration.Processors
/// </summary>
public interface IDefaultChangeProcessor
{
Task UpdateAccountAsync(MailAccount account);
Task<string> 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
/// <returns>All folders.</returns>
Task<List<MailItemFolder>> GetLocalFoldersAsync(Guid accountId);
Task<List<MailItemFolder>> GetSynchronizationFoldersAsync(SynchronizationOptions options);
Task<bool> MapLocalDraftAsync(Guid accountId, Guid localDraftCopyUniqueId, string newMailCopyId, string newDraftId, string newThreadId);
Task UpdateFolderLastSyncDateAsync(Guid folderId);
Task<List<MailItemFolder>> GetExistingFoldersAsync(Guid accountId);
Task UpdateRemoteAliasInformationAsync(MailAccount account, List<RemoteAccountAlias> 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<RemoteAccountAlias> remoteAccountAliases)
=> AccountService.UpdateRemoteAliasInformationAsync(account, remoteAccountAliases);
}
}

View File

@@ -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<MailAccount> HoldingAccounts => new List<MailAccount> { 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;

View File

@@ -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<List<MailAccountAlias>> GetAccountAliasesAsync(Guid accountId)
{
var query = new Query(nameof(MailAccountAlias))
.Where(nameof(MailAccountAlias.AccountId), accountId)
.OrderByDesc(nameof(MailAccountAlias.IsRootAlias));
return await Connection.QueryAsync<MailAccountAlias>(query.GetRawQuery()).ConfigureAwait(false);
}
private Task<MergedInbox> GetMergedInboxInformationAsync(Guid mergedInboxId)
=> Connection.Table<MergedInbox>().FirstOrDefaultAsync(a => a.Id == mergedInboxId);
@@ -245,6 +272,7 @@ namespace Wino.Core.Services
await Connection.Table<TokenInformation>().Where(a => a.AccountId == account.Id).DeleteAsync();
await Connection.Table<MailItemFolder>().DeleteAsync(a => a.MailAccountId == account.Id);
await Connection.Table<AccountSignature>().DeleteAsync(a => a.MailAccountId == account.Id);
await Connection.Table<MailAccountAlias>().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<MailAccount> GetAccountAsync(Guid accountId)
{
var account = await Connection.Table<MailAccount>().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<MailAccountAlias> aliases)
{
// Delete existing ones.
await Connection.Table<MailAccountAlias>().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<RemoteAccountAlias> 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<MailAccountAlias> GetPrimaryAccountAliasAsync(Guid accountId)
{
var aliases = await GetAccountAliasesAsync(accountId);
if (aliases == null || aliases.Count == 0) return null;
return aliases.FirstOrDefault(a => a.IsPrimary) ?? aliases.First();
}
}
}

View File

@@ -61,7 +61,8 @@ namespace Wino.Core.Services
typeof(CustomServerInformation),
typeof(AccountSignature),
typeof(MergedInbox),
typeof(MailAccountPreferences)
typeof(MailAccountPreferences),
typeof(MailAccountAlias)
);
}
}

View File

@@ -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;
@@ -51,8 +52,9 @@ namespace Wino.Core.Services
_preferencesService = preferencesService;
}
public async Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(MailAccount composerAccount, DraftCreationOptions draftCreationOptions)
public async Task<(MailCopy draftMailCopy, string draftBase64MimeMessage)> CreateDraftAsync(Guid accountId, DraftCreationOptions draftCreationOptions)
{
var composerAccount = await _accountService.GetAccountAsync(accountId).ConfigureAwait(false);
var createdDraftMimeMessage = await CreateDraftMimeAsync(composerAccount, draftCreationOptions);
var draftFolder = await _folderService.GetSpecialFolderByAccountIdAsync(composerAccount.Id, SpecialFolderType.Draft);
@@ -61,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,
@@ -620,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);
@@ -728,6 +737,14 @@ namespace Wino.Core.Services
message.Cc.AddRange(referenceMessage.Cc.Where(x => x is MailboxAddress mailboxAddress && !mailboxAddress.Address.Equals(account.Address, StringComparison.OrdinalIgnoreCase)));
}
// Self email can be present at this step, when replying to own message. It should be removed only in case there no other recipients.
if (message.To.Count > 1)
{
var self = message.To.FirstOrDefault(x => x is MailboxAddress mailboxAddress && mailboxAddress.Address.Equals(account.Address, StringComparison.OrdinalIgnoreCase));
if (self != null)
message.To.Remove(self);
}
// Manage "ThreadId-ConversationId"
if (!string.IsNullOrEmpty(referenceMessage.MessageId))
{

View File

@@ -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;
}

View File

@@ -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
/// <param name="cancellationToken">Cancellation token</param>
public abstract Task ExecuteNativeRequestsAsync(IEnumerable<IRequestBundle<TBaseRequest>> batchedRequests, CancellationToken cancellationToken = default);
public abstract Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default);
/// <summary>
/// Refreshes remote mail account profile if possible.
/// Profile picture, sender name and mailbox settings (todo) will be handled in this step.
/// </summary>
public virtual Task<ProfileInformation> GetProfileInformationAsync() => default;
/// <summary>
/// Refreshes the aliases of the account.
/// Only available for Gmail right now.
/// </summary>
protected virtual Task SynchronizeAliasesAsync() => Task.CompletedTask;
/// <summary>
/// Returns the base64 encoded profile picture of the account from the given URL.
/// </summary>
/// <param name="url">URL to retrieve picture from.</param>
/// <returns>base64 encoded profile picture</returns>
protected async Task<string> 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);
}
/// <summary>
/// Internally synchronizes the account with the given options.
/// Not exposed and overriden for each synchronizer.
/// </summary>
/// <param name="options">Synchronization options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Synchronization result that contains summary of the sync.</returns>
protected abstract Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default);
/// <summary>
/// Safely updates account's profile information.
/// Database changes are reflected after this call.
/// </summary>
private async Task<ProfileInformation> SynchronizeProfileInformationInternalAsync()
{
var profileInformation = await GetProfileInformationAsync();
if (profileInformation != null)
{
Account.SenderName = profileInformation.SenderName;
Account.Base64ProfilePictureData = profileInformation.Base64ProfilePictureData;
}
return profileInformation;
}
/// <summary>
/// Batches network requests, executes them, and does the needed synchronization after the batch request execution.
/// </summary>
/// <param name="options">Synchronization options.</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>Synchronization result that contains summary of the sync.</returns>
public async Task<SynchronizationResult> 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));
/// <summary>
/// Sends a message to the shell to update the synchronization progress.
/// </summary>
/// <param name="progress">Percentage of the progress.</param>
public void PublishSynchronizationProgress(double progress)
=> WeakReferenceMessenger.Default.Send(new AccountSynchronizationProgressUpdatedMessage(Account.Id, progress));

View File

@@ -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<GmailSynchronizer>();
@@ -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<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
public override async Task<ProfileInformation> 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<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{
_logger.Information("Internal synchronization started for {Name}", Account.Name);

View File

@@ -405,7 +405,7 @@ namespace Wino.Core.Synchronizers
];
}
public override async Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
protected override async Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{
var downloadedMessageIds = new List<string>();
@@ -922,7 +922,7 @@ namespace Wino.Core.Synchronizers
}
// In case of the high input, we'll batch them by 50 to reflect changes quickly.
var batchedMissingMailIds = missingMailIds.Batch(50).Select(a => new UniqueIdSet(a, SortOrder.Descending));
var batchedMissingMailIds = missingMailIds.Batch(50).Select(a => new UniqueIdSet(a, SortOrder.Ascending));
foreach (var batchMissingMailIds in batchedMissingMailIds)
{

View File

@@ -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<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
protected override async Task<SynchronizationResult> SynchronizeInternalAsync(SynchronizationOptions options, CancellationToken cancellationToken = default)
{
var downloadedMessageIds = new List<string>();
@@ -473,6 +473,40 @@ namespace Wino.Core.Synchronizers
}
}
/// <summary>
/// Get the user's profile picture
/// </summary>
/// <returns>Base64 encoded profile picture.</returns>
private async Task<string> 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);
}
/// <summary>
/// Get the user's display name.
/// </summary>
/// <returns>Display name of the user.</returns>
private async Task<string> GetSenderNameAsync()
{
var userInfo = await _graphClient.Users["me"].GetAsync();
return userInfo.DisplayName;
}
public override async Task<ProfileInformation> 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 +606,37 @@ 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);
var requestInformation = _graphClient.Me.Messages.ToPostRequestInformation(new Message());
requestInformation.Headers.Clear();// replace the json content header
requestInformation.Headers.Add("Content-Type", "text/plain");
requestInformation.SetStreamContent(new MemoryStream(Encoding.UTF8.GetBytes(base64Encoded)), "text/plain");
return requestInformation;
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
});
}
}
return default;
throw new Exception("Invalid create draft request type.");
});
}
@@ -602,50 +651,43 @@ namespace Wino.Core.Synchronizers
var mailCopyId = sendDraftPreparationRequest.MailItem.Id;
var mimeMessage = sendDraftPreparationRequest.Mime;
var batchDeleteRequest = new BatchDeleteRequest(new List<IRequest>()
// 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<RequestInformation>(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<RequestInformation>(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<RequestInformation>(sendRequest, request);
return [deleteBundle, sendMailRequest];
return [patchDraftRequestBundle, sendDraftRequestBundle];
}
public override IEnumerable<IRequestBundle<RequestInformation>> 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 +739,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<string>();
foreach (var bundleId in bundleIds)
{
@@ -727,45 +784,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<RequestInformation> 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<RequestInformation, Message> messageBundle)
{
var outlookMessage = await messageBundle.DeserializeBundleAsync(httpResponseMessage, cancellationToken);
if (outlookMessage == null) return;
// TODO: Handle new message added or updated.
}
else if (bundle is HttpRequestBundle<RequestInformation, Microsoft.Graph.Models.MailFolder> folderBundle)
{
var outlookFolder = await folderBundle.DeserializeBundleAsync(httpResponseMessage, cancellationToken);
if (outlookFolder == null) return;
// TODO: Handle new folder added or updated.
}
else if (bundle is HttpRequestBundle<RequestInformation, MimeMessage> mimeBundle)
{
// TODO: Handle mime retrieve message.
}
}
private async Task<MimeMessage> DownloadMimeMessageAsync(string messageId, CancellationToken cancellationToken = default)
{

View File

@@ -17,7 +17,8 @@
<PackageReference Include="CommunityToolkit.Diagnostics" Version="8.2.2" />
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="Google.Apis.Gmail.v1" Version="1.68.0.3427" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.59" />
<PackageReference Include="Google.Apis.PeopleService.v1" Version="1.68.0.3359" />
<PackageReference Include="HtmlAgilityPack" Version="1.11.63" />
<PackageReference Include="HtmlKit" Version="1.1.0" />
<PackageReference Include="IsExternalInit" Version="1.0.3">
<PrivateAssets>all</PrivateAssets>
@@ -26,9 +27,9 @@
<PackageReference Include="MailKit" Version="4.7.1.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
<PackageReference Include="Microsoft.Graph" Version="5.56.0" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.62.0" />
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.62.0" />
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.62.0" />
<PackageReference Include="Microsoft.Identity.Client" Version="4.63.0" />
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.63.0" />
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.63.0" />
<PackageReference Include="MimeKit" Version="4.7.1" />
<PackageReference Include="morelinq" Version="4.1.0" />
<PackageReference Include="Nito.AsyncEx.Tasks" Version="5.1.2" />

View File

@@ -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);

View File

@@ -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,83 @@ namespace Wino.Mail.ViewModels
await _accountService.CreateAccountAsync(createdAccount, tokenInformation, customServerInformation);
// Local account has been created.
// Create new synchronizer and start synchronization.
// Sync profile information if supported.
if (createdAccount.IsProfileInfoSyncSupported)
{
// 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<SynchronizationResult, NewSynchronizationRequested>(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<SynchronizationResult, NewSynchronizationRequested>(new NewSynchronizationRequested(options, SynchronizationSource.Client));
var folderSynchronizationResponse = await _winoServerConnectionManager.GetResponseAsync<SynchronizationResult, NewSynchronizationRequested>(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);
// Sync aliases if supported.
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<SynchronizationResult, NewSynchronizationRequested>(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));

View File

@@ -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<MailAccountAlias> 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<SynchronizationResult, NewSynchronizationRequested>(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();
}
}
}

View File

@@ -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));
}
}
}
@@ -777,9 +786,9 @@ namespace Wino.Mail.ViewModels
MailToUri = _launchProtocolService.MailToUri
};
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(account, draftOptions).ConfigureAwait(false);
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);
}
}

View File

@@ -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<MailAccountAlias> 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<MimePart>();
var multiparts = new List<Multipart>();
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<AddressInformation> 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<bool> 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<bool, DownloadMissingMessageRequested>(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<MimeEntity> 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<AddressInformation> addresses, InternetAddressList list)
{
list.Clear();

View File

@@ -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;

View File

@@ -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;
}
}
}

View File

@@ -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
/// <summary>
/// Gets or sets whether attachment is busy with opening or saving etc.
/// </summary>
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);

View File

@@ -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<Guid> GetContainingIds() => new[] { UniqueId };

View File

@@ -68,7 +68,7 @@ namespace Wino.Mail.ViewModels
private CancellationTokenSource listManipulationCancellationTokenSource = new CancellationTokenSource();
public IWinoNavigationService NavigationService { get; }
public IStatePersistanceService StatePersistanceService { get; }
public IStatePersistanceService StatePersistenceService { get; }
public IPreferencesService PreferencesService { get; }
private readonly IMailService _mailService;
@@ -120,6 +120,12 @@ namespace Wino.Mail.ViewModels
[ObservableProperty]
private string barMessage;
[ObservableProperty]
private double mailListLength = 420;
[ObservableProperty]
private double maxMailListLength = 1200;
[ObservableProperty]
private string barTitle;
@@ -141,7 +147,7 @@ namespace Wino.Mail.ViewModels
public MailListPageViewModel(IDialogService dialogService,
IWinoNavigationService navigationService,
IMailService mailService,
IStatePersistanceService statePersistanceService,
IStatePersistanceService statePersistenceService,
IFolderService folderService,
IThreadingStrategyProvider threadingStrategyProvider,
IContextMenuItemService contextMenuItemService,
@@ -152,7 +158,7 @@ namespace Wino.Mail.ViewModels
{
PreferencesService = preferencesService;
_winoServerConnectionManager = winoServerConnectionManager;
StatePersistanceService = statePersistanceService;
StatePersistenceService = statePersistenceService;
NavigationService = navigationService;
_mailService = mailService;
@@ -165,6 +171,8 @@ namespace Wino.Mail.ViewModels
SelectedFilterOption = FilterOptions[0];
SelectedSortingOption = SortingOptions[0];
mailListLength = statePersistenceService.MailListPaneLength;
selectionChangedObservable = Observable.FromEventPattern<NotifyCollectionChangedEventArgs>(SelectedItems, nameof(SelectedItems.CollectionChanged));
selectionChangedObservable
.Throttle(TimeSpan.FromMilliseconds(100))
@@ -257,7 +265,7 @@ namespace Wino.Mail.ViewModels
{
if (_activeMailItem == selectedMailItemViewModel) return;
// Don't update active mail item if Ctrl key is pressed or multi selection is ennabled.
// Don't update active mail item if Ctrl key is pressed or multi selection is enabled.
// User is probably trying to select multiple items.
// This is not the same behavior in Windows Mail,
// but it's a trash behavior.
@@ -266,7 +274,7 @@ namespace Wino.Mail.ViewModels
bool isMultiSelecting = isCtrlKeyPressed || IsMultiSelectionModeEnabled;
if (isMultiSelecting ? StatePersistanceService.IsReaderNarrowed : false)
if (isMultiSelecting && StatePersistenceService.IsReaderNarrowed)
{
// Don't change the active mail item if the reader is narrowed, but just update the shell.
Messenger.Send(new ShellStateUpdated());
@@ -610,6 +618,9 @@ namespace Wino.Mail.ViewModels
if (ActiveFolder == null) return;
// At least accounts must match.
if (ActiveFolder.HandlingFolders.Any(a => a.MailAccountId != addedMail.AssignedAccount.Id)) return;
// Messages coming to sent or draft folder must be inserted regardless of the filter.
bool shouldPreventIgnoringFilter = addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Draft ||
addedMail.AssignedFolder.SpecialFolderType == SpecialFolderType.Sent;
@@ -617,6 +628,7 @@ namespace Wino.Mail.ViewModels
// Item does not belong to this folder and doesn't have special type to be inserted.
if (!shouldPreventIgnoringFilter && !ActiveFolder.HandlingFolders.Any(a => a.Id == addedMail.AssignedFolder.Id)) return;
// Item should be prevented from being added to the list due to filter.
if (!shouldPreventIgnoringFilter && ShouldPreventItemAdd(addedMail)) return;
await MailCollection.AddAsync(addedMail);

View File

@@ -271,9 +271,9 @@ namespace Wino.Mail.ViewModels
}
};
var (draftMailCopy, draftBase64MimeMessage) = await _mailService.CreateDraftAsync(initializedMailItemViewModel.AssignedAccount, draftOptions).ConfigureAwait(false);
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);

View File

@@ -25,37 +25,24 @@ namespace Wino.Mail.ViewModels
public bool IsSelectedWindowsAccentColor => SelectedAppColor == Colors.LastOrDefault();
public ObservableCollection<AppColorViewModel> Colors { get; set; } = new ObservableCollection<AppColorViewModel>();
public ObservableCollection<AppColorViewModel> Colors { get; set; } = [];
public List<ElementThemeContainer> ElementThemes { get; set; } = new List<ElementThemeContainer>()
{
public List<ElementThemeContainer> ElementThemes { get; set; } =
[
new ElementThemeContainer(ApplicationElementTheme.Light, Translator.ElementTheme_Light),
new ElementThemeContainer(ApplicationElementTheme.Dark, Translator.ElementTheme_Dark),
new ElementThemeContainer(ApplicationElementTheme.Default, Translator.ElementTheme_Default),
};
];
public List<MailListPaneLengthPreferences> PaneLengths { get; set; } = new List<MailListPaneLengthPreferences>()
{
new MailListPaneLengthPreferences(Translator.PaneLengthOption_Micro, 300),
new MailListPaneLengthPreferences(Translator.PaneLengthOption_Small, 350),
new MailListPaneLengthPreferences(Translator.PaneLengthOption_Default, 420),
new MailListPaneLengthPreferences(Translator.PaneLengthOption_Medium, 700),
new MailListPaneLengthPreferences(Translator.PaneLengthOption_Large, 900),
new MailListPaneLengthPreferences(Translator.PaneLengthOption_ExtraLarge, 1200),
};
public List<MailListDisplayMode> InformationDisplayModes { get; set; } = new List<MailListDisplayMode>()
{
public List<MailListDisplayMode> InformationDisplayModes { get; set; } =
[
MailListDisplayMode.Compact,
MailListDisplayMode.Medium,
MailListDisplayMode.Spacious
};
];
public List<AppThemeBase> AppThemes { get; set; }
[ObservableProperty]
private MailListPaneLengthPreferences selectedMailListPaneLength;
[ObservableProperty]
private ElementThemeContainer selectedElementTheme;
@@ -123,6 +110,13 @@ namespace Wino.Mail.ViewModels
#endregion
[RelayCommand]
private void ResetMailListPaneLength()
{
StatePersistanceService.MailListPaneLength = 420;
DialogService.InfoBarMessage(Translator.GeneralTitle_Info, Translator.Info_MailListSizeResetSuccessMessage, InfoBarMessageType.Success);
}
public AsyncRelayCommand CreateCustomThemeCommand { get; set; }
public PersonalizationPageViewModel(IDialogService dialogService,
IStatePersistanceService statePersistanceService,
@@ -179,7 +173,6 @@ namespace Wino.Mail.ViewModels
{
SelectedElementTheme = ElementThemes.Find(a => a.NativeTheme == _themeService.RootTheme);
SelectedInfoDisplayMode = PreferencesService.MailItemDisplayMode;
SelectedMailListPaneLength = PaneLengths.Find(a => a.Length == StatePersistanceService.MailListPaneLength);
var currentAccentColor = _themeService.AccentColor;
@@ -289,8 +282,6 @@ namespace Wino.Mail.ViewModels
{
_themeService.CurrentApplicationThemeId = SelectedAppTheme.Id;
}
else if (e.PropertyName == nameof(SelectedMailListPaneLength) && SelectedMailListPaneLength != null)
StatePersistanceService.MailListPaneLength = SelectedMailListPaneLength.Length;
else
{
if (e.PropertyName == nameof(SelectedInfoDisplayMode))

View File

@@ -7,6 +7,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="EmailValidation" Version="1.2.0" />
<PackageReference Include="IsExternalInit" Version="1.0.3">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -1,9 +1,10 @@
<Application x:Class="Wino.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Wino.Controls"
xmlns:selectors="using:Wino.Selectors"
xmlns:wino="using:Wino">
<Application
x:Class="Wino.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="using:Wino.Controls"
xmlns:selectors="using:Wino.Selectors"
xmlns:wino="using:Wino">
<Application.Resources>
<ResourceDictionary>
@@ -48,6 +49,14 @@
<Setter Property="BorderThickness" Value="1" />
</Style>
<!-- Custom Grid style for info panels. -->
<Style TargetType="Grid" x:Key="InformationAreaGridStyle">
<Setter Property="Background" Value="{ThemeResource CardBackgroundFillColorDefaultBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Padding" Value="16" />
</Style>
<!-- Default StackPanel animation. -->
<Style TargetType="StackPanel">
<Setter Property="ChildrenTransitions">
@@ -60,9 +69,10 @@
</Style>
<!-- Default Style for ContentDialog -->
<Style x:Key="WinoDialogStyle"
BasedOn="{StaticResource DefaultContentDialogStyle}"
TargetType="ContentDialog" />
<Style
x:Key="WinoDialogStyle"
BasedOn="{StaticResource DefaultContentDialogStyle}"
TargetType="ContentDialog" />
<!-- Settings Menu Item Template -->
<Style TargetType="controls:SettingsMenuItemControl">
@@ -74,63 +84,71 @@
<Setter.Value>
<ControlTemplate TargetType="controls:SettingsMenuItemControl">
<Grid>
<Button Padding="0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Command="{TemplateBinding Command}"
CommandParameter="{TemplateBinding CommandParameter}"
IsEnabled="{TemplateBinding IsEnabled}"
IsHitTestVisible="{TemplateBinding IsClickable}">
<Grid Height="70"
Padding="0,6,12,6"
CornerRadius="4">
<Button
Padding="0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Command="{TemplateBinding Command}"
CommandParameter="{TemplateBinding CommandParameter}"
IsEnabled="{TemplateBinding IsEnabled}"
IsHitTestVisible="{TemplateBinding IsClickable}">
<Grid
Height="70"
Padding="0,6,12,6"
CornerRadius="4">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="50" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<ContentControl HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{TemplateBinding Icon}" />
<ContentControl
HorizontalAlignment="Center"
VerticalAlignment="Center"
Content="{TemplateBinding Icon}" />
<Grid Grid.Column="1"
Margin="4,0"
VerticalAlignment="Center"
RowSpacing="3">
<Grid
Grid.Column="1"
Margin="4,0"
VerticalAlignment="Center"
RowSpacing="3">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock VerticalAlignment="Center"
FontWeight="SemiBold"
Style="{StaticResource BodyTextBlockStyle}"
Text="{TemplateBinding Title}" />
<TextBlock Grid.Row="1"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{TemplateBinding Description}" />
<TextBlock
VerticalAlignment="Center"
FontWeight="SemiBold"
Style="{StaticResource BodyTextBlockStyle}"
Text="{TemplateBinding Title}" />
<TextBlock
Grid.Row="1"
VerticalAlignment="Center"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{TemplateBinding Description}" />
</Grid>
<Viewbox Grid.Column="0"
Grid.ColumnSpan="2"
Width="16"
Height="16"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsNavigateIconVisible}">
<Viewbox
Grid.Column="0"
Grid.ColumnSpan="2"
Width="16"
Height="16"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsNavigateIconVisible}">
<PathIcon Data="F1 M 5.029297 19.091797 L 14.111328 10 L 5.029297 0.908203 L 5.908203 0.029297 L 15.888672 10 L 5.908203 19.970703 Z " />
</Viewbox>
</Grid>
</Button>
<ContentControl Grid.RowSpan="2"
Margin="0,0,16,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{TemplateBinding SideContent}"
IsHitTestVisible="True"
Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsNavigateIconVisible, Converter={StaticResource ReverseBooleanToVisibilityConverter}}" />
<ContentControl
Grid.RowSpan="2"
Margin="0,0,16,0"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Content="{TemplateBinding SideContent}"
IsHitTestVisible="True"
Visibility="{Binding RelativeSource={RelativeSource Mode=TemplatedParent}, Path=IsNavigateIconVisible, Converter={StaticResource ReverseBooleanToVisibilityConverter}}" />
</Grid>
</ControlTemplate>
</Setter.Value>
@@ -188,16 +206,17 @@
<Image Source="/Assets/FileTypes/type_other.png" />
</DataTemplate>
<selectors:FileAttachmentTypeSelector x:Key="FileTypeIconSelector"
Archive="{StaticResource ArchiveTemplate}"
Executable="{StaticResource ExecutableTemplate}"
HTML="{StaticResource HTMLTemplate}"
Image="{StaticResource ImageTemplate}"
None="{StaticResource NoneTemplate}"
Other="{StaticResource OtherTemplate}"
PDF="{StaticResource PDFTemplate}"
RarArchive="{StaticResource RarTemplate}"
Video="{StaticResource VideoTemplate}" />
<selectors:FileAttachmentTypeSelector
x:Key="FileTypeIconSelector"
Archive="{StaticResource ArchiveTemplate}"
Executable="{StaticResource ExecutableTemplate}"
HTML="{StaticResource HTMLTemplate}"
Image="{StaticResource ImageTemplate}"
None="{StaticResource NoneTemplate}"
Other="{StaticResource OtherTemplate}"
PDF="{StaticResource PDFTemplate}"
RarArchive="{StaticResource RarTemplate}"
Video="{StaticResource VideoTemplate}" />
</ResourceDictionary>
<XamlControlsResources xmlns="using:Microsoft.UI.Xaml.Controls" />

View File

@@ -179,6 +179,7 @@ namespace Wino
services.AddTransient(typeof(MergedAccountDetailsPageViewModel));
services.AddTransient(typeof(LanguageTimePageViewModel));
services.AddTransient(typeof(AppPreferencesPageViewModel));
services.AddTransient(typeof(AliasManagementPageViewModel));
}
#endregion

View File

@@ -208,7 +208,6 @@ namespace Wino.Views
WeakReferenceMessenger.Default.Send(new ClearMailSelectionsRequested());
WeakReferenceMessenger.Default.Send(new DisposeRenderingFrameRequested());
WeakReferenceMessenger.Default.Send(new ShellStateUpdated());
}
private async void MenuItemContextRequested(UIElement sender, ContextRequestedEventArgs args)

View File

@@ -1,6 +1,7 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:xaml="using:Windows.UI.Xaml">
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:xaml="using:Windows.UI.Xaml">
<x:String x:Key="ThemeName">Mica</x:String>
<x:Boolean x:Key="UseMica">True</x:Boolean>

View File

@@ -0,0 +1,27 @@
using System;
using Windows.UI.Xaml.Data;
using Windows.UI.Xaml;
namespace Wino.Converters
{
public class GridLengthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if (value is double doubleValue)
{
return new GridLength(doubleValue);
}
return new GridLength(1, GridUnitType.Auto);
}
public object ConvertBack(object value, Type targetType, object parameter, string language)
{
if (value is GridLength gridLength)
{
return gridLength.Value;
}
return 0.0;
}
}
}

View File

@@ -47,6 +47,13 @@
</VisualState.Setters>
</VisualState>
<VisualState x:Name="FetchingProfileInformation">
<VisualState.Setters>
<Setter Target="StatusText.Text" Value="{x:Bind domain:Translator.AccountCreationDialog_FetchingProfileInformation}" />
<Setter Target="DialogIcon.Glyph" Value="F1 M 1.875 17.5 C 1.621094 17.5 1.380208 17.451172 1.152344 17.353516 C 0.924479 17.255859 0.724284 17.120768 0.551758 16.948242 C 0.379232 16.775717 0.244141 16.575521 0.146484 16.347656 C 0.048828 16.119791 0 15.878906 0 15.625 L 0 4.375 C 0 4.121094 0.048828 3.880209 0.146484 3.652344 C 0.244141 3.42448 0.379232 3.224285 0.551758 3.051758 C 0.724284 2.879232 0.924479 2.744141 1.152344 2.646484 C 1.380208 2.548828 1.621094 2.5 1.875 2.5 L 18.125 2.5 C 18.378906 2.5 18.619791 2.548828 18.847656 2.646484 C 19.07552 2.744141 19.275715 2.879232 19.448242 3.051758 C 19.620768 3.224285 19.755859 3.42448 19.853516 3.652344 C 19.951172 3.880209 20 4.121094 20 4.375 L 20 15.625 C 20 15.878906 19.951172 16.119791 19.853516 16.347656 C 19.755859 16.575521 19.620768 16.775717 19.448242 16.948242 C 19.275715 17.120768 19.07552 17.255859 18.847656 17.353516 C 18.619791 17.451172 18.378906 17.5 18.125 17.5 Z M 5 16.25 L 5 14.375 C 5 14.121094 5.048828 13.880209 5.146484 13.652344 C 5.244141 13.424479 5.379231 13.224284 5.551758 13.051758 C 5.724284 12.879232 5.924479 12.744141 6.152344 12.646484 C 6.380208 12.548828 6.621094 12.5 6.875 12.5 L 13.125 12.5 C 13.378905 12.5 13.619791 12.548828 13.847656 12.646484 C 14.075521 12.744141 14.275716 12.879232 14.448242 13.051758 C 14.620768 13.224284 14.755858 13.424479 14.853516 13.652344 C 14.951171 13.880209 14.999999 14.121094 15 14.375 L 15 16.25 L 18.125 16.25 C 18.29427 16.25 18.440754 16.188152 18.564453 16.064453 C 18.68815 15.940756 18.75 15.794271 18.75 15.625 L 18.75 4.375 C 18.75 4.20573 18.68815 4.059246 18.564453 3.935547 C 18.440754 3.81185 18.29427 3.75 18.125 3.75 L 1.875 3.75 C 1.705729 3.75 1.559245 3.81185 1.435547 3.935547 C 1.311849 4.059246 1.25 4.20573 1.25 4.375 L 1.25 15.625 C 1.25 15.794271 1.311849 15.940756 1.435547 16.064453 C 1.559245 16.188152 1.705729 16.25 1.875 16.25 Z M 6.875 8.056641 C 6.875 7.633464 6.959635 7.236328 7.128906 6.865234 C 7.298177 6.494141 7.526041 6.170248 7.8125 5.893555 C 8.098958 5.616862 8.430989 5.398764 8.808594 5.239258 C 9.186197 5.079754 9.583333 5.000001 10 5 C 10.436197 5.000001 10.843099 5.081381 11.220703 5.244141 C 11.598307 5.406901 11.928711 5.629883 12.211914 5.913086 C 12.495117 6.196289 12.718099 6.526693 12.880859 6.904297 C 13.043619 7.281901 13.124999 7.688803 13.125 8.125 C 13.124999 8.561198 13.043619 8.9681 12.880859 9.345703 C 12.718099 9.723308 12.495117 10.053711 12.211914 10.336914 C 11.928711 10.620117 11.598307 10.8431 11.220703 11.005859 C 10.843099 11.16862 10.436197 11.25 10 11.25 C 9.550781 11.25 9.135742 11.166992 8.754883 11.000977 C 8.374023 10.834961 8.043619 10.607097 7.763672 10.317383 C 7.483724 10.02767 7.265625 9.689128 7.109375 9.301758 C 6.953125 8.914389 6.875 8.49935 6.875 8.056641 Z M 11.875 8.125 C 11.875 7.871094 11.826172 7.630209 11.728516 7.402344 C 11.630859 7.174479 11.495768 6.974284 11.323242 6.801758 C 11.150716 6.629232 10.950521 6.494141 10.722656 6.396484 C 10.494791 6.298828 10.253906 6.25 10 6.25 C 9.746094 6.25 9.505208 6.298828 9.277344 6.396484 C 9.049479 6.494141 8.849283 6.629232 8.676758 6.801758 C 8.504231 6.974284 8.369141 7.174479 8.271484 7.402344 C 8.173828 7.630209 8.125 7.871094 8.125 8.125 C 8.125 8.378906 8.173828 8.619792 8.271484 8.847656 C 8.369141 9.075521 8.504231 9.275717 8.676758 9.448242 C 8.849283 9.620769 9.049479 9.755859 9.277344 9.853516 C 9.505208 9.951172 9.746094 10 10 10 C 10.253906 10 10.494791 9.951172 10.722656 9.853516 C 10.950521 9.755859 11.150716 9.620769 11.323242 9.448242 C 11.495768 9.275717 11.630859 9.075521 11.728516 8.847656 C 11.826172 8.619792 11.875 8.378906 11.875 8.125 Z M 6.25 16.25 L 13.75 16.25 L 13.75 14.375 C 13.75 14.205729 13.68815 14.059245 13.564453 13.935547 C 13.440755 13.81185 13.294271 13.75 13.125 13.75 L 6.875 13.75 C 6.705729 13.75 6.559244 13.81185 6.435547 13.935547 C 6.311849 14.059245 6.25 14.205729 6.25 14.375 Z " />
</VisualState.Setters>
</VisualState>
<VisualState x:Name="Completed">
<VisualState.Setters>
<Setter Target="StatusText.Text" Value="{x:Bind domain:Translator.AccountCreationDialog_Completed}" />

View File

@@ -0,0 +1,40 @@
<ContentDialog
x:Class="Wino.Dialogs.CreateAccountAliasDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Wino.Dialogs"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
xmlns:domain="using:Wino.Core.Domain"
PrimaryButtonText="{x:Bind domain:Translator.Buttons_Create}"
SecondaryButtonText="{x:Bind domain:Translator.Buttons_Cancel}"
DefaultButton="Primary"
PrimaryButtonClick="CreateClicked"
Title="{x:Bind domain:Translator.CreateAccountAliasDialog_Title}"
Style="{StaticResource WinoDialogStyle}">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Text="{x:Bind domain:Translator.CreateAccountAliasDialog_Description}" Style="{StaticResource CaptionTextBlockStyle}" />
<StackPanel
Grid.Row="1"
Margin="0,20"
Spacing="8">
<TextBox
x:Name="AliasTextBox"
PlaceholderText="{x:Bind domain:Translator.CreateAccountAliasDialog_AliasAddressPlaceholder}"
Header="{x:Bind domain:Translator.CreateAccountAliasDialog_AliasAddress}" />
<TextBox
x:Name="ReplyToTextBox"
PlaceholderText="{x:Bind domain:Translator.CreateAccountAliasDialog_ReplyToAddressPlaceholder}"
Header="{x:Bind domain:Translator.CreateAccountAliasDialog_ReplyToAddress}" />
</StackPanel>
</Grid>
</ContentDialog>

View File

@@ -0,0 +1,30 @@
using System;
using Windows.UI.Xaml.Controls;
using Wino.Core.Domain.Entities;
using Wino.Core.Domain.Interfaces;
namespace Wino.Dialogs
{
public sealed partial class CreateAccountAliasDialog : ContentDialog, ICreateAccountAliasDialog
{
public MailAccountAlias CreatedAccountAlias { get; set; }
public CreateAccountAliasDialog()
{
InitializeComponent();
}
private void CreateClicked(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
CreatedAccountAlias = new MailAccountAlias
{
AliasAddress = AliasTextBox.Text.Trim(),
ReplyToAddress = ReplyToTextBox.Text.Trim(),
Id = Guid.NewGuid(),
IsPrimary = false,
IsVerified = false
};
Hide();
}
}
}

View File

@@ -56,14 +56,6 @@
PlaceholderText="{x:Bind domain:Translator.NewAccountDialog_AccountNamePlaceholder}"
TextChanged="AccountNameChanged" />
<!-- Sender Name -->
<TextBox
x:Name="SenderNameTextbox"
Grid.Row="1"
Header="{x:Bind domain:Translator.AccountSettingsDialog_AccountName}"
PlaceholderText="{x:Bind domain:Translator.AccountSettingsDialog_AccountNamePlaceholder}"
TextChanged="SenderNameChanged" />
<!--
TODO: Move Name, Sender Name and Color Picker to another Frame.
Provider selection should be first, then account details.
@@ -79,7 +71,7 @@
<ListView
Grid.Row="2"
Margin="0,12"
Padding="0"
ItemTemplate="{StaticResource NewMailProviderTemplate}"
ItemsSource="{x:Bind Providers}"

View File

@@ -49,7 +49,7 @@ namespace Wino.Dialogs
if (IsSecondaryButtonEnabled)
{
Result = new AccountCreationDialogResult(SelectedMailProvider.Type, AccountNameTextbox.Text.Trim(), SenderNameTextbox.Text.Trim());
Result = new AccountCreationDialogResult(SelectedMailProvider.Type, AccountNameTextbox.Text.Trim());
Hide();
}
}
@@ -68,8 +68,7 @@ namespace Wino.Dialogs
{
bool shouldEnable = SelectedMailProvider != null
&& SelectedMailProvider.IsSupported
&& !string.IsNullOrEmpty(AccountNameTextbox.Text)
&& (SelectedMailProvider.RequireSenderNameOnCreationDialog ? !string.IsNullOrEmpty(SenderNameTextbox.Text) : true);
&& !string.IsNullOrEmpty(AccountNameTextbox.Text);
IsPrimaryButtonEnabled = shouldEnable;
}
@@ -77,7 +76,6 @@ namespace Wino.Dialogs
private void ValidateNames()
{
AccountNameTextbox.IsEnabled = SelectedMailProvider != null;
SenderNameTextbox.IsEnabled = SelectedMailProvider != null && SelectedMailProvider.Type != Core.Domain.Enums.MailProviderType.IMAP4;
}
private void DialogOpened(ContentDialog sender, ContentDialogOpenedEventArgs args) => Validate();

View File

@@ -50,7 +50,7 @@
Description="Mail client designed for Windows 11"
BackgroundColor="transparent">
<uap:DefaultTile Wide310x150Logo="Assets\Wide310x150Logo.png" Square71x71Logo="Assets\SmallTile.png" Square310x310Logo="Assets\LargeTile.png"/>
<uap:SplashScreen Image="Assets\SplashScreen.png" BackgroundColor="transparent"/>
<uap:SplashScreen Image="Assets\SplashScreen.png" BackgroundColor="transparent" uap5:Optional="true" />
<uap:LockScreen BadgeLogo="Assets\BadgeLogo.png" Notification="badgeAndTileText"/>
</uap:VisualElements>
<Extensions>

View File

@@ -216,6 +216,18 @@ namespace Wino.Services
return storeDialog;
}
public async Task<ICreateAccountAliasDialog> ShowCreateAccountAliasDialogAsync()
{
var createAccountAliasDialog = new CreateAccountAliasDialog()
{
RequestedTheme = _themeService.RootTheme.ToWindowsElementTheme()
};
await HandleDialogPresentationAsync(createAccountAliasDialog);
return createAccountAliasDialog;
}
public async Task HandleSystemFolderConfigurationDialogAsync(Guid accountId, IFolderService folderService)
{
try

View File

@@ -11,6 +11,7 @@ using Wino.Core.Domain.Models.Navigation;
using Wino.Helpers;
using Wino.Mail.ViewModels.Data;
using Wino.Mail.ViewModels.Messages;
using Wino.Messaging.Client.Mails;
using Wino.Views;
using Wino.Views.Account;
using Wino.Views.Settings;
@@ -88,6 +89,8 @@ namespace Wino.Services
return typeof(SettingOptionsPage);
case WinoPage.AppPreferencesPage:
return typeof(AppPreferencesPage);
case WinoPage.AliasManagementPage:
return typeof(AliasManagementPage);
default:
return null;
}
@@ -114,6 +117,7 @@ namespace Wino.Services
{
// No need for new navigation, just refresh the folder.
WeakReferenceMessenger.Default.Send(new ActiveMailFolderChangedEvent(folderNavigationArgs.BaseFolderMenuItem, folderNavigationArgs.FolderInitLoadAwaitTask));
WeakReferenceMessenger.Default.Send(new DisposeRenderingFrameRequested());
return true;
}
@@ -136,6 +140,13 @@ namespace Wino.Services
{
WeakReferenceMessenger.Default.Send(new NewMailItemRenderingRequestedEvent(mailItemViewModel));
}
else if (listingFrame.Content != null
&& listingFrame.Content.GetType() == GetPageType(WinoPage.IdlePage)
&& pageType == typeof(IdlePage))
{
// Idle -> Idle navigation. Ignore.
return true;
}
else
{
listingFrame.Navigate(pageType, parameter, transitionInfo);

View File

@@ -9,6 +9,9 @@
<SolidColorBrush x:Key="FolderSyncBrush">#1abc9c</SolidColorBrush>
<SolidColorBrush x:Key="AliasUnverifiedBrush">#ff7675</SolidColorBrush>
<SolidColorBrush x:Key="AliasVerifiedBrush">#1abc9c</SolidColorBrush>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Name="Light">
<SolidColorBrush x:Key="AttachmentBrush">#fdcb6e</SolidColorBrush>

View File

@@ -4,4 +4,5 @@
xmlns:converters="using:Wino.Converters">
<converters:ReverseBooleanToVisibilityConverter x:Key="ReverseBooleanToVisibilityConverter" />
<converters:ReverseBooleanConverter x:Key="ReverseBooleanConverter" />
<converters:GridLengthConverter x:Key="GridLengthConverter" />
</ResourceDictionary>

View File

@@ -0,0 +1,6 @@
using Wino.Mail.ViewModels;
namespace Wino.Views.Abstract
{
public abstract class AliasManagementPageAbstract : BasePage<AliasManagementPageViewModel> { }
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -443,17 +443,9 @@
x:Name="AccountsComboBox"
Grid.Column="1"
IsEditable="False"
IsEnabled="False"
ItemsSource="{x:Bind ViewModel.Accounts}"
SelectedItem="{x:Bind ViewModel.ComposingAccount, Mode=TwoWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="entities:MailAccount">
<TextBlock>
<Run Text="{x:Bind Name}" /><Run Text=" (" /><Run Text="{x:Bind Address}" /><Run Text=")" />
</TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
ItemsSource="{x:Bind ViewModel.AvailableAliases, Mode=OneWay}"
DisplayMemberPath="AliasAddress"
SelectedItem="{x:Bind ViewModel.SelectedAlias, Mode=TwoWay}" />
<!-- To -->
<TextBlock

View File

@@ -39,7 +39,8 @@ namespace Wino.Views
public sealed partial class ComposePage : ComposePageAbstract,
IRecipient<NavigationPaneModeChanged>,
IRecipient<CreateNewComposeMailRequested>,
IRecipient<ApplicationThemeChanged>
IRecipient<ApplicationThemeChanged>,
IRecipient<KillChromiumRequested>
{
public bool IsComposerDarkMode
{
@@ -237,12 +238,9 @@ namespace Wino.Views
// Convert files to MailAttachmentViewModel.
foreach (var file in files)
{
if (!ViewModel.IncludedAttachments.Any(a => a.FileName == file.Path))
{
var attachmentViewModel = await file.ToAttachmentViewModelAsync();
var attachmentViewModel = await file.ToAttachmentViewModelAsync();
ViewModel.IncludedAttachments.Add(attachmentViewModel);
}
await ViewModel.IncludeAttachmentAsync(attachmentViewModel);
}
}
@@ -417,13 +415,6 @@ namespace Wino.Views
return await ExecuteScriptFunctionAsync("initializeJodit", fonts, composerFont, composerFontSize, readerFont, readerFontSize);
}
protected override void OnNavigatingFrom(NavigatingCancelEventArgs e)
{
base.OnNavigatingFrom(e);
DisposeDisposables();
DisposeWebView2();
}
private void DisposeWebView2()
{
@@ -700,5 +691,11 @@ namespace Wino.Views
ToBox.Focus(FocusState.Programmatic);
}
}
public void Receive(KillChromiumRequested message)
{
DisposeDisposables();
DisposeWebView2();
}
}
}

View File

@@ -6,6 +6,7 @@
xmlns:collections="using:CommunityToolkit.Mvvm.Collections"
xmlns:controls="using:Wino.Controls"
xmlns:controls1="using:CommunityToolkit.WinUI.Controls"
xmlns:converters="using:Wino.Converters"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:domain="using:Wino.Core.Domain"
xmlns:enums="using:Wino.Core.Domain.Enums"
@@ -20,9 +21,8 @@
xmlns:ui="using:Microsoft.Toolkit.Uwp.UI"
xmlns:viewModelData="using:Wino.Mail.ViewModels.Data"
xmlns:wino="using:Wino"
xmlns:converters="using:Wino.Converters"
x:Name="root"
Loaded="MailListPageLoaded"
SizeChanged="PageSizeChanged"
mc:Ignorable="d">
<Page.Resources>
@@ -181,11 +181,11 @@
<DataTemplate x:DataType="viewModelData:ThreadMailItemViewModel">
<controls:WinoExpander
x:Name="ThreadExpander"
Padding="0"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
BackgroundSizing="InnerBorderEdge"
BorderThickness="0"
Padding="0"
IsExpanded="{x:Bind IsThreadExpanded, Mode=TwoWay}">
<muxc:Expander.Header>
<controls:MailItemDisplayInformationControl
@@ -276,9 +276,9 @@
<AutoSuggestBox
x:Name="SearchBar"
Margin="2,0,-2,0"
VerticalAlignment="Center"
BorderBrush="Transparent"
Margin="2,0,-2,0"
GotFocus="SearchBoxFocused"
LostFocus="SearchBarUnfocused"
PlaceholderText="{x:Bind domain:Translator.SearchBarPlaceholder}"
@@ -296,23 +296,20 @@
<Grid x:Name="RootGrid" Padding="0,0,0,7">
<Grid.ColumnDefinitions>
<ColumnDefinition
x:Name="ReaderColumn"
Width="*"
MaxWidth="{x:Bind ViewModel.StatePersistanceService.MailListPaneLength, Mode=OneWay}" />
<ColumnDefinition x:Name="MailListColumn" Width="{x:Bind ViewModel.MailListLength, Mode=OneWay, Converter={StaticResource GridLengthConverter}}" />
<ColumnDefinition x:Name="RendererColumn" Width="*" />
</Grid.ColumnDefinitions>
<!-- Mail Items -->
<Border
x:Name="ReaderGridContainer"
x:Name="MailListContainer"
Grid.Column="0"
Padding="5,0,0,0"
Background="{ThemeResource WinoContentZoneBackgroud}"
BorderBrush="{StaticResource CardStrokeColorDefaultBrush}"
BorderThickness="1"
CornerRadius="7">
<Grid x:Name="ReaderGrid">
<Grid x:Name="MailListGrid">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
@@ -335,11 +332,6 @@
Padding="2,0"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}"
CornerRadius="8">
<Grid.RowDefinitions>
<RowDefinition Height="48" />
<RowDefinition Height="2" />
</Grid.RowDefinitions>
<!-- Commands -->
<Grid>
<Grid.ColumnDefinitions>
@@ -481,12 +473,6 @@
</Button.Flyout>
</Button>
</Grid>
<muxc:ProgressBar
x:Name="LoadingProgressBar"
Grid.Row="1"
x:Load="{x:Bind ViewModel.IsInitializingFolder, Mode=OneWay}"
IsIndeterminate="True" />
</Grid>
<!-- Pivot + Sync + Multi Select -->
@@ -615,11 +601,11 @@
<SemanticZoom x:Name="SemanticZoomContainer" CanChangeViews="{x:Bind ViewModel.PreferencesService.IsSemanticZoomEnabled, Mode=OneWay}">
<SemanticZoom.ZoomedInView>
<listview:WinoListView
ui:ScrollViewerExtensions.VerticalScrollBarMargin="0"
x:Name="MailListView"
HorizontalContentAlignment="Stretch"
ui:ScrollViewerExtensions.EnableMiddleClickScrolling="True"
ui:ListViewExtensions.ItemContainerStretchDirection="Horizontal"
ui:ScrollViewerExtensions.EnableMiddleClickScrolling="True"
ui:ScrollViewerExtensions.VerticalScrollBarMargin="0"
ItemDeletedCommand="{x:Bind ViewModel.MailOperationCommand}"
ItemTemplateSelector="{StaticResource MailItemDisplaySelector}"
ItemsSource="{x:Bind MailCollectionViewSource.View, Mode=OneWay}"
@@ -695,74 +681,39 @@
</Grid>
</Border>
<controls1:PropertySizer
x:Name="MailListSizer"
Grid.Column="1"
Width="16"
HorizontalAlignment="Left"
Binding="{x:Bind ViewModel.MailListLength, Mode=TwoWay}"
Canvas.ZIndex="20"
ManipulationCompleted="MailListSizerManipulationCompleted"
Maximum="{x:Bind ViewModel.MaxMailListLength, Mode=OneWay}"
Minimum="270"
Opacity="0" />
<Grid Grid.Column="1" x:Name="RenderingGrid">
<Grid x:Name="RenderingGrid" Grid.Column="1">
<!-- Mail Rendering Frame -->
<Frame x:Name="RenderingFrame" IsNavigationStackEnabled="False" />
<!-- No Mail Selected Message -->
<StackPanel
x:Name="NoMailSelectedPanel"
Opacity="0.5"
Spacing="6"
HorizontalAlignment="Center"
VerticalAlignment="Center">
VerticalAlignment="Center"
Opacity="0.5"
Spacing="6">
<controls:WinoFontIcon Icon="Mail" FontSize="80" />
<controls:WinoFontIcon FontSize="80" Icon="Mail" />
<TextBlock
x:Name="CountTextBlock"
HorizontalAlignment="Center"
FontSize="31"
Text="{x:Bind ViewModel.SelectedMessageText, Mode=OneWay}"
Style="{StaticResource SubheaderTextBlockStyle}"
x:Name="CountTextBlock" />
Text="{x:Bind ViewModel.SelectedMessageText, Mode=OneWay}" />
</StackPanel>
</Grid>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="AdaptiveStates" CurrentStateChanged="AdaptivenessChanged">
<VisualState x:Name="NormalState">
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="{x:Bind helpers:XamlHelpers.MailListAdaptivityConverter(ViewModel.StatePersistanceService.MailListPaneLength), Mode=OneWay}" />
</VisualState.StateTriggers>
</VisualState>
<VisualState x:Name="NarrowState">
<VisualState.Setters>
<Setter Target="ReaderGrid.MaxWidth" Value="10000" />
</VisualState.Setters>
<VisualState.StateTriggers>
<AdaptiveTrigger MinWindowWidth="0" />
</VisualState.StateTriggers>
</VisualState>
</VisualStateGroup>
<!-- Pane open adjustments -->
<VisualStateGroup x:Name="PaneStates">
<VisualState x:Name="PaneOpened">
<VisualState.Setters>
<!--<Setter Target="RootGrid.Margin" Value="-6,0,0,0" />-->
<Setter Target="ReaderGrid.Margin" Value="-4,0,0,0" />
</VisualState.Setters>
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind PreferencesService.IsNavigationPaneOpened, Mode=OneWay}" />
</VisualState.StateTriggers>
</VisualState>
<VisualState x:Name="PaneClosed" />
</VisualStateGroup>
<!-- Mail reader states -->
<VisualStateGroup x:Name="ReaderStates">
<VisualState x:Name="ReaderActive">
<VisualState.Setters>
<Setter Target="ReaderGrid.CornerRadius" Value="0" />
</VisualState.Setters>
<VisualState.StateTriggers>
<StateTrigger IsActive="{x:Bind StatePersistanceService.IsReadingMail, Mode=OneWay}" />
</VisualState.StateTriggers>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
</Grid>
</abstract:MailListPageAbstract>

View File

@@ -40,11 +40,9 @@ namespace Wino.Views
IRecipient<ShellStateUpdated>,
IRecipient<DisposeRenderingFrameRequested>
{
private const string NarrowVisualStateKey = "NarrowState";
private const string AdaptivenessStatesKey = "AdaptiveStates";
private const double RENDERING_COLUMN_MIN_WIDTH = 300;
private IStatePersistanceService StatePersistanceService { get; } = App.Current.Services.GetService<IStatePersistanceService>();
private IPreferencesService PreferencesService { get; } = App.Current.Services.GetService<IPreferencesService>();
private IStatePersistanceService StatePersistenceService { get; } = App.Current.Services.GetService<IStatePersistanceService>();
private IKeyPressService KeyPressService { get; } = App.Current.Services.GetService<IKeyPressService>();
public MailListPage()
@@ -90,7 +88,7 @@ namespace Wino.Views
SelectAllCheckbox.Unchecked += SelectAllCheckboxUnchecked;
}
private void SelectionModeToggleChecked(object sender, Windows.UI.Xaml.RoutedEventArgs e)
private void SelectionModeToggleChecked(object sender, RoutedEventArgs e)
{
ChangeSelectionMode(ListViewSelectionMode.Multiple);
}
@@ -132,92 +130,21 @@ namespace Wino.Views
}
}
private void SelectionModeToggleUnchecked(object sender, Windows.UI.Xaml.RoutedEventArgs e)
private void SelectionModeToggleUnchecked(object sender, RoutedEventArgs e)
{
ChangeSelectionMode(ListViewSelectionMode.Extended);
}
private void SelectAllCheckboxChecked(object sender, Windows.UI.Xaml.RoutedEventArgs e)
private void SelectAllCheckboxChecked(object sender, RoutedEventArgs e)
{
MailListView.SelectAllWino();
}
private void SelectAllCheckboxUnchecked(object sender, Windows.UI.Xaml.RoutedEventArgs e)
private void SelectAllCheckboxUnchecked(object sender, RoutedEventArgs e)
{
MailListView.ClearSelections();
}
#region Mostly UI
private void UpdateAdaptiveness()
{
bool shouldDisplayNoMessagePanel, shouldDisplayMailingList, shouldDisplayRenderingFrame;
// This is the smallest state UI can get.
// Either mailing list or rendering grid is visible.
if (StatePersistanceService.IsReaderNarrowed)
{
// Start visibility checks by no message panel.
bool isMultiSelectionEnabled = ViewModel.IsMultiSelectionModeEnabled || KeyPressService.IsCtrlKeyPressed();
shouldDisplayMailingList = isMultiSelectionEnabled ? true : (!ViewModel.HasSelectedItems || ViewModel.HasMultipleItemSelections);
shouldDisplayNoMessagePanel = shouldDisplayMailingList ? false : !ViewModel.HasSelectedItems || ViewModel.HasMultipleItemSelections;
shouldDisplayRenderingFrame = shouldDisplayMailingList ? false : !shouldDisplayNoMessagePanel;
}
else
{
shouldDisplayMailingList = true;
shouldDisplayNoMessagePanel = !ViewModel.HasSelectedItems || ViewModel.HasMultipleItemSelections;
shouldDisplayRenderingFrame = !shouldDisplayNoMessagePanel;
}
ReaderGridContainer.Visibility = shouldDisplayMailingList ? Visibility.Visible : Visibility.Collapsed;
RenderingFrame.Visibility = shouldDisplayRenderingFrame ? Visibility.Visible : Visibility.Collapsed;
NoMailSelectedPanel.Visibility = shouldDisplayNoMessagePanel ? Visibility.Visible : Visibility.Collapsed;
if (StatePersistanceService.IsReaderNarrowed)
{
if (RenderingFrame.Visibility == Visibility.Visible && ReaderGridContainer.Visibility == Visibility.Collapsed)
{
// Extend rendering frame to full width.
Grid.SetColumn(RenderingGrid, 0);
Grid.SetColumnSpan(RenderingGrid, 2);
Grid.SetColumn(ReaderGrid, 0);
Grid.SetColumnSpan(ReaderGrid, 2);
}
else if (RenderingFrame.Visibility == Visibility.Collapsed && NoMailSelectedPanel.Visibility == Visibility.Collapsed)
{
// Only mail list is available.
// Extend the mailing list.
Grid.SetColumn(ReaderGridContainer, 0);
Grid.SetColumnSpan(ReaderGridContainer, 2);
}
}
else
{
// Mailing list is always visible on the first part.
Grid.SetColumn(ReaderGridContainer, 0);
Grid.SetColumnSpan(ReaderGridContainer, 1);
// Rendering grid should take the rest of the space.
Grid.SetColumn(RenderingGrid, 1);
Grid.SetColumnSpan(RenderingGrid, 1);
}
}
private void AdaptivenessChanged(object sender, VisualStateChangedEventArgs e)
{
StatePersistanceService.IsReaderNarrowed = e.NewState.Name == "NarrowState";
UpdateAdaptiveness();
}
#endregion
void IRecipient<ResetSingleMailItemSelectionEvent>.Receive(ResetSingleMailItemSelectionEvent message)
{
// Single item in thread selected.
@@ -446,21 +373,6 @@ namespace Wino.Views
SearchBar.PlaceholderText = Translator.SearchBarPlaceholder;
}
private void MailListPageLoaded(object sender, RoutedEventArgs e)
{
// App might open with narrowed state.
// VSM will not trigger in this case.
// Set values to force updating adaptiveness.
var groups = VisualStateManager.GetVisualStateGroups(RootGrid);
var adaptiveState = groups.FirstOrDefault(a => a.Name == AdaptivenessStatesKey);
if (adaptiveState == null) return;
// This should force UpdateAdaptiveness call.
StatePersistanceService.IsReaderNarrowed = adaptiveState.CurrentState.Name == NarrowVisualStateKey;
}
private void ProcessMailItemKeyboardAccelerator(UIElement sender, ProcessKeyboardAcceleratorEventArgs args)
{
if (args.Key == Windows.System.VirtualKey.Delete)
@@ -568,5 +480,89 @@ namespace Wino.Views
{
ViewModel.NavigationService.Navigate(WinoPage.IdlePage, null, NavigationReferenceFrame.RenderingFrame, NavigationTransitionType.DrillIn);
}
private void PageSizeChanged(object sender, SizeChangedEventArgs e)
{
ViewModel.MaxMailListLength = e.NewSize.Width - RENDERING_COLUMN_MIN_WIDTH;
StatePersistenceService.IsReaderNarrowed = e.NewSize.Width < StatePersistenceService.MailListPaneLength + RENDERING_COLUMN_MIN_WIDTH;
UpdateAdaptiveness();
}
private void MailListSizerManipulationCompleted(object sender, ManipulationCompletedRoutedEventArgs e)
{
StatePersistenceService.MailListPaneLength = ViewModel.MailListLength;
}
private void UpdateAdaptiveness()
{
bool shouldDisplayNoMessagePanel, shouldDisplayMailingList, shouldDisplayRenderingFrame;
bool isMultiSelectionEnabled = ViewModel.IsMultiSelectionModeEnabled || KeyPressService.IsCtrlKeyPressed();
// This is the smallest state UI can get.
// Either mailing list or rendering grid is visible.
if (StatePersistenceService.IsReaderNarrowed)
{
// Start visibility checks by no message panel.
shouldDisplayMailingList = isMultiSelectionEnabled ? true : (!ViewModel.HasSelectedItems || ViewModel.HasMultipleItemSelections);
shouldDisplayNoMessagePanel = shouldDisplayMailingList ? false : !ViewModel.HasSelectedItems || ViewModel.HasMultipleItemSelections;
shouldDisplayRenderingFrame = shouldDisplayMailingList ? false : !shouldDisplayNoMessagePanel;
}
else
{
shouldDisplayMailingList = true;
shouldDisplayNoMessagePanel = !ViewModel.HasSelectedItems || ViewModel.HasMultipleItemSelections;
shouldDisplayRenderingFrame = !shouldDisplayNoMessagePanel;
}
MailListContainer.Visibility = shouldDisplayMailingList ? Visibility.Visible : Visibility.Collapsed;
RenderingFrame.Visibility = shouldDisplayRenderingFrame ? Visibility.Visible : Visibility.Collapsed;
NoMailSelectedPanel.Visibility = shouldDisplayNoMessagePanel ? Visibility.Visible : Visibility.Collapsed;
if (StatePersistenceService.IsReaderNarrowed == true)
{
if (ViewModel.HasSingleItemSelection && !isMultiSelectionEnabled)
{
MailListColumn.Width = new GridLength(0);
RendererColumn.Width = new GridLength(1, GridUnitType.Star);
Grid.SetColumn(MailListContainer, 0);
Grid.SetColumnSpan(RenderingGrid, 2);
MailListContainer.Visibility = Visibility.Collapsed;
RenderingGrid.Visibility = Visibility.Visible;
}
else
{
MailListColumn.Width = new GridLength(1, GridUnitType.Star);
RendererColumn.Width = new GridLength(0);
Grid.SetColumnSpan(MailListContainer, 2);
MailListContainer.Margin = new Thickness(7, 0, 7, 0);
MailListContainer.Visibility = Visibility.Visible;
RenderingGrid.Visibility = Visibility.Collapsed;
SearchBar.Margin = new Thickness(8, 0, -2, 0);
MailListSizer.Visibility = Visibility.Collapsed;
}
}
else
{
MailListColumn.Width = new GridLength(StatePersistenceService.MailListPaneLength);
RendererColumn.Width = new GridLength(1, GridUnitType.Star);
MailListContainer.Margin = new Thickness(0, 0, 0, 0);
Grid.SetColumn(MailListContainer, 0);
Grid.SetColumn(RenderingGrid, 1);
Grid.SetColumnSpan(MailListContainer, 1);
Grid.SetColumnSpan(RenderingGrid, 1);
MailListContainer.Visibility = Visibility.Visible;
RenderingGrid.Visibility = Visibility.Visible;
SearchBar.Margin = new Thickness(2, 0, -2, 0);
MailListSizer.Visibility = Visibility.Visible;
}
}
}
}

View File

@@ -44,6 +44,7 @@ namespace Wino.Views
WinoPage.SignatureManagementPage => typeof(SignatureManagementPage),
WinoPage.AccountDetailsPage => typeof(AccountDetailsPage),
WinoPage.MergedAccountDetailsPage => typeof(MergedAccountDetailsPage),
WinoPage.AliasManagementPage => typeof(AliasManagementPage),
_ => null,
};
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,12 @@
using Wino.Views.Abstract;
namespace Wino.Views.Settings
{
public sealed partial class AliasManagementPage : AliasManagementPageAbstract
{
public AliasManagementPage()
{
this.InitializeComponent();
}
}
}

File diff suppressed because one or more lines are too long

View File

@@ -169,6 +169,9 @@
<PackageReference Include="CommunityToolkit.Uwp.Animations">
<Version>8.0.240109</Version>
</PackageReference>
<PackageReference Include="CommunityToolkit.Uwp.Behaviors">
<Version>8.0.240109</Version>
</PackageReference>
<PackageReference Include="CommunityToolkit.Uwp.Controls.SettingsControls">
<Version>8.0.240109</Version>
</PackageReference>
@@ -246,6 +249,7 @@
<Compile Include="Controls\WinoFontIconSource.cs" />
<Compile Include="Controls\WinoFontIcon.cs" />
<Compile Include="Controls\WinoSwipeControlItems.cs" />
<Compile Include="Converters\GridLengthConverter.cs" />
<Compile Include="Dialogs\AccountEditDialog.xaml.cs">
<DependentUpon>AccountEditDialog.xaml</DependentUpon>
</Compile>
@@ -268,6 +272,9 @@
<Compile Include="Dialogs\NewImapSetupDialog.xaml.cs">
<DependentUpon>NewImapSetupDialog.xaml</DependentUpon>
</Compile>
<Compile Include="Dialogs\CreateAccountAliasDialog.xaml.cs">
<DependentUpon>CreateAccountAliasDialog.xaml</DependentUpon>
</Compile>
<Compile Include="Dialogs\StoreRatingDialog.xaml.cs">
<DependentUpon>StoreRatingDialog.xaml</DependentUpon>
</Compile>
@@ -340,6 +347,7 @@
<Compile Include="Views\Abstract\AboutPageAbstract.cs" />
<Compile Include="Views\Abstract\AccountDetailsPageAbstract.cs" />
<Compile Include="Views\Abstract\AccountManagementPageAbstract.cs" />
<Compile Include="Views\Abstract\AliasManagementPageAbstract.cs" />
<Compile Include="Views\Abstract\AppPreferencesPageAbstract.cs" />
<Compile Include="Views\Abstract\AppShellAbstract.cs" />
<Compile Include="Views\Abstract\ComposePageAbstract.cs" />
@@ -423,6 +431,9 @@
<Compile Include="Views\Settings\SettingOptionsPage.xaml.cs">
<DependentUpon>SettingOptionsPage.xaml</DependentUpon>
</Compile>
<Compile Include="Views\Settings\AliasManagementPage.xaml.cs">
<DependentUpon>AliasManagementPage.xaml</DependentUpon>
</Compile>
<Compile Include="Views\Settings\SignatureManagementPage.xaml.cs">
<DependentUpon>SignatureManagementPage.xaml</DependentUpon>
</Compile>
@@ -509,6 +520,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Dialogs\CreateAccountAliasDialog.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Dialogs\StoreRatingDialog.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
@@ -657,6 +672,10 @@
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>
</Page>
<Page Include="Views\Settings\AliasManagementPage.xaml">
<Generator>MSBuild:Compile</Generator>
<SubType>Designer</SubType>
</Page>
<Page Include="Views\Settings\SignatureManagementPage.xaml">
<SubType>Designer</SubType>
<Generator>MSBuild:Compile</Generator>

View File

@@ -0,0 +1,7 @@
namespace Wino.Messaging.Client.Mails
{
/// <summary>
/// Terminates all chromum instances.
/// </summary>
public record KillChromiumRequested;
}

View File

@@ -11,7 +11,7 @@
<Identity
Name="58272BurakKSE.WinoMailPreview"
Publisher="CN=51FBDAF3-E212-4149-89A2-A2636B3BC911"
Version="1.8.0.0" />
Version="1.8.2.0" />
<Extensions>
<!-- Publisher Cache Folders -->
@@ -48,7 +48,7 @@
Square150x150Logo="Images\Square150x150Logo.png"
Square44x44Logo="Images\Square44x44Logo.png">
<uap:DefaultTile Wide310x150Logo="Images\Wide310x150Logo.png" Square71x71Logo="Images\SmallTile.png" Square310x310Logo="Images\LargeTile.png"/>
<uap:SplashScreen Image="Images\SplashScreen.png" />
<uap:SplashScreen Image="Images\SplashScreen.png" uap5:Optional="true" />
</uap:VisualElements>
<Extensions>

View File

@@ -52,7 +52,7 @@ namespace Wino.Server.MessageHandlers
{
var synchronizationResult = await synchronizer.SynchronizeAsync(message.Options, cancellationToken).ConfigureAwait(false);
if (synchronizationResult.DownloadedMessages.Any())
if (synchronizationResult.DownloadedMessages?.Any() ?? false || !synchronizer.Account.Preferences.IsNotificationsEnabled)
{
var accountInboxFolder = await _folderService.GetSpecialFolderByAccountIdAsync(message.Options.AccountId, SpecialFolderType.Inbox);

View File

@@ -219,7 +219,20 @@ namespace Wino.Server
{ MessageConstants.MessageDataTypeKey, message.GetType().Name }
};
await connection.SendMessageAsync(set);
try
{
await connection.SendMessageAsync(set);
}
catch (InvalidOperationException)
{
// Connection might've been disposed during the SendMessageAsync call.
// This is a safe way to handle the exception.
// We don't lock the connection since this request may take sometime to complete.
}
catch (Exception exception)
{
Log.Error(exception, "SendMessageAsync threw an exception");
}
}
private void OnConnectionClosed(AppServiceConnection sender, AppServiceClosedEventArgs args)

View File

@@ -31,8 +31,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.2" />
<PackageReference Include="H.NotifyIcon.Wpf" Version="2.1.0" />
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.0.3" />
<PackageReference Include="H.NotifyIcon.Wpf" Version="2.1.2" />
<PackageReference Include="CommunityToolkit.WinUI.Notifications" Version="7.1.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Wino.Core.Domain\Wino.Core.Domain.csproj" />