Added support for one click unsubscribe with confirmation dialog.
This commit is contained in:
@@ -11,10 +11,9 @@ namespace Wino.Core.Domain.Models.Reader
|
|||||||
{
|
{
|
||||||
public string RenderHtml { get; }
|
public string RenderHtml { get; }
|
||||||
public MailRenderingOptions MailRenderingOptions { get; }
|
public MailRenderingOptions MailRenderingOptions { get; }
|
||||||
public List<MimePart> Attachments { get; set; } = new List<MimePart>();
|
public List<MimePart> Attachments { get; set; } = [];
|
||||||
|
|
||||||
public string UnsubscribeLink { get; set; }
|
public UnsubscribeInfo UnsubscribeInfo { get; set; }
|
||||||
public bool CanUnsubscribe => !string.IsNullOrEmpty(UnsubscribeLink);
|
|
||||||
|
|
||||||
public MailRenderModel(string renderHtml, MailRenderingOptions mailRenderingOptions = null)
|
public MailRenderModel(string renderHtml, MailRenderingOptions mailRenderingOptions = null)
|
||||||
{
|
{
|
||||||
@@ -22,4 +21,12 @@ namespace Wino.Core.Domain.Models.Reader
|
|||||||
MailRenderingOptions = mailRenderingOptions;
|
MailRenderingOptions = mailRenderingOptions;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public class UnsubscribeInfo
|
||||||
|
{
|
||||||
|
public string HttpLink { get; set; }
|
||||||
|
public string MailToLink { get; set; }
|
||||||
|
public bool IsOneClick { get; set; }
|
||||||
|
public bool CanUnsubscribe => HttpLink != null || MailToLink != null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -76,6 +76,11 @@
|
|||||||
"DialogMessage_UnlinkAccountsConfirmationTitle": "Unlink Accounts",
|
"DialogMessage_UnlinkAccountsConfirmationTitle": "Unlink Accounts",
|
||||||
"DialogMessage_EmptySubjectConfirmation": "Missin Subject",
|
"DialogMessage_EmptySubjectConfirmation": "Missin Subject",
|
||||||
"DialogMessage_EmptySubjectConfirmationMessage": "Message has no subject. Do you want to continue?",
|
"DialogMessage_EmptySubjectConfirmationMessage": "Message has no subject. Do you want to continue?",
|
||||||
|
"DialogMessage_UnsubscribeConfirmationTitle": "Unsubscribe",
|
||||||
|
"DialogMessage_UnsubscribeConfirmationOneClickMessage": "Do you want to stop getting messages from {0}?",
|
||||||
|
"DialogMessage_UnsubscribeConfirmationGoToWebsiteMessage": "To stop getting messages from {0}, go to their website to unsubscribe.",
|
||||||
|
"DialogMessage_UnsubscribeConfirmationGoToWebsiteConfirmButton": "Go to website",
|
||||||
|
"DialogMessage_UnsubscribeConfirmationMailtoMessage": "Do you want to stop getting messages from {0}? WinoMail will unsubscribe for you by sending an email from your email account to {1}.",
|
||||||
"Dialog_DontAskAgain": "Don't ask again",
|
"Dialog_DontAskAgain": "Don't ask again",
|
||||||
"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.",
|
"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",
|
"DiscordChannelDisclaimerTitle": "Important Discord Information",
|
||||||
@@ -230,6 +235,8 @@
|
|||||||
"Info_UnsupportedFunctionalityTitle": "Unsupported",
|
"Info_UnsupportedFunctionalityTitle": "Unsupported",
|
||||||
"Info_UnsubscribeLinkInvalidTitle": "Invalid Unsubscribe Uri",
|
"Info_UnsubscribeLinkInvalidTitle": "Invalid Unsubscribe Uri",
|
||||||
"Info_UnsubscribeLinkInvalidMessage": "This unsubscribe link is invalid. Failed to unsubscribe from the list.",
|
"Info_UnsubscribeLinkInvalidMessage": "This unsubscribe link is invalid. Failed to unsubscribe from the list.",
|
||||||
|
"Info_UnsubscribeSuccessMessage": "Successfully unsubscribed from {0}.",
|
||||||
|
"Info_UnsubscribeErrorMessage": "Failed to unsubscribe",
|
||||||
"ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method",
|
"ImapAdvancedSetupDialog_AuthenticationMethod": "Authentication method",
|
||||||
"ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security",
|
"ImapAdvancedSetupDialog_ConnectionSecurity": "Connection security",
|
||||||
"ImapAuthenticationMethod_Auto": "Auto",
|
"ImapAuthenticationMethod_Auto": "Auto",
|
||||||
|
|||||||
35
Wino.Core.Domain/Translator.Designer.cs
generated
35
Wino.Core.Domain/Translator.Designer.cs
generated
@@ -403,6 +403,31 @@ namespace Wino.Core.Domain
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static string DialogMessage_EmptySubjectConfirmationMessage => Resources.GetTranslatedString(@"DialogMessage_EmptySubjectConfirmationMessage");
|
public static string DialogMessage_EmptySubjectConfirmationMessage => Resources.GetTranslatedString(@"DialogMessage_EmptySubjectConfirmationMessage");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Unsubscribe
|
||||||
|
/// </summary>
|
||||||
|
public static string DialogMessage_UnsubscribeConfirmationTitle => Resources.GetTranslatedString(@"DialogMessage_UnsubscribeConfirmationTitle");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Do you want to stop getting messages from {0}?
|
||||||
|
/// </summary>
|
||||||
|
public static string DialogMessage_UnsubscribeConfirmationOneClickMessage => Resources.GetTranslatedString(@"DialogMessage_UnsubscribeConfirmationOneClickMessage");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// To stop getting messages from {0}, go to their website to unsubscribe.
|
||||||
|
/// </summary>
|
||||||
|
public static string DialogMessage_UnsubscribeConfirmationGoToWebsiteMessage => Resources.GetTranslatedString(@"DialogMessage_UnsubscribeConfirmationGoToWebsiteMessage");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Go to website
|
||||||
|
/// </summary>
|
||||||
|
public static string DialogMessage_UnsubscribeConfirmationGoToWebsiteConfirmButton => Resources.GetTranslatedString(@"DialogMessage_UnsubscribeConfirmationGoToWebsiteConfirmButton");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Do you want to stop getting messages from {0}? WinoMail will unsubscribe for you by sending an email from your email account to {1}.
|
||||||
|
/// </summary>
|
||||||
|
public static string DialogMessage_UnsubscribeConfirmationMailtoMessage => Resources.GetTranslatedString(@"DialogMessage_UnsubscribeConfirmationMailtoMessage");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Don't ask again
|
/// Don't ask again
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -1173,6 +1198,16 @@ namespace Wino.Core.Domain
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static string Info_UnsubscribeLinkInvalidMessage => Resources.GetTranslatedString(@"Info_UnsubscribeLinkInvalidMessage");
|
public static string Info_UnsubscribeLinkInvalidMessage => Resources.GetTranslatedString(@"Info_UnsubscribeLinkInvalidMessage");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Successfully unsubscribed from {0}.
|
||||||
|
/// </summary>
|
||||||
|
public static string Info_UnsubscribeSuccessMessage => Resources.GetTranslatedString(@"Info_UnsubscribeSuccessMessage");
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Failed to unsubscribe
|
||||||
|
/// </summary>
|
||||||
|
public static string Info_UnsubscribeErrorMessage => Resources.GetTranslatedString(@"Info_UnsubscribeErrorMessage");
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Authentication method
|
/// Authentication method
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using MimeKit;
|
using MimeKit;
|
||||||
@@ -221,22 +222,24 @@ namespace Wino.Core.Services
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for List-Unsubscribe link if possible.
|
|
||||||
|
|
||||||
if (message.Headers.Contains(HeaderId.ListUnsubscribe))
|
if (message.Headers.Contains(HeaderId.ListUnsubscribe))
|
||||||
{
|
{
|
||||||
renderingModel.UnsubscribeLink = message.Headers[HeaderId.ListUnsubscribe].Normalize();
|
var unsubscribeLinks = message.Headers[HeaderId.ListUnsubscribe]
|
||||||
|
.Normalize()
|
||||||
|
.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries)
|
||||||
|
.Select(x => x.Trim([' ', '<', '>']));
|
||||||
|
|
||||||
// Sometimes this link is wrapped with < >, remove them.
|
// Only two types of unsubscribe links are possible.
|
||||||
if (renderingModel.UnsubscribeLink.StartsWith("<"))
|
// So each has it's own property to simplify the usage.
|
||||||
|
renderingModel.UnsubscribeInfo = new UnsubscribeInfo()
|
||||||
{
|
{
|
||||||
renderingModel.UnsubscribeLink = renderingModel.UnsubscribeLink.Substring(1, renderingModel.UnsubscribeLink.Length - 2);
|
HttpLink = unsubscribeLinks.FirstOrDefault(x => x.StartsWith("http", StringComparison.OrdinalIgnoreCase)),
|
||||||
}
|
MailToLink = unsubscribeLinks.FirstOrDefault(x => x.StartsWith("mailto", StringComparison.OrdinalIgnoreCase)),
|
||||||
|
IsOneClick = message.Headers.Contains(HeaderId.ListUnsubscribePost)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return renderingModel;
|
return renderingModel;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
@@ -47,7 +49,7 @@ namespace Wino.Mail.ViewModels
|
|||||||
#region Properties
|
#region Properties
|
||||||
|
|
||||||
public bool ShouldDisplayDownloadProgress => IsIndetermineProgress || (CurrentDownloadPercentage > 0 && CurrentDownloadPercentage <= 100);
|
public bool ShouldDisplayDownloadProgress => IsIndetermineProgress || (CurrentDownloadPercentage > 0 && CurrentDownloadPercentage <= 100);
|
||||||
public bool CanUnsubscribe => CurrentRenderModel?.CanUnsubscribe ?? false;
|
public bool CanUnsubscribe => CurrentRenderModel?.UnsubscribeInfo?.CanUnsubscribe ?? false;
|
||||||
public bool IsJunkMail => initializedMailItemViewModel?.AssignedFolder != null && initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk;
|
public bool IsJunkMail => initializedMailItemViewModel?.AssignedFolder != null && initializedMailItemViewModel.AssignedFolder.SpecialFolderType == SpecialFolderType.Junk;
|
||||||
|
|
||||||
public bool IsImageRenderingDisabled
|
public bool IsImageRenderingDisabled
|
||||||
@@ -172,18 +174,60 @@ namespace Wino.Mail.ViewModels
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task UnsubscribeAsync()
|
private async Task UnsubscribeAsync()
|
||||||
{
|
{
|
||||||
if (!CurrentRenderModel?.CanUnsubscribe ?? false) return;
|
if (!(CurrentRenderModel?.UnsubscribeInfo?.CanUnsubscribe ?? false)) return;
|
||||||
|
|
||||||
// TODO: Support for List-Unsubscribe-Post header. It can be done without launching browser.
|
bool confirmed;
|
||||||
// https://certified-senders.org/wp-content/uploads/2017/07/CSA_one-click_list-unsubscribe.pdf
|
|
||||||
|
|
||||||
// TODO: Sometimes unsubscribe link can be a mailto: link.
|
// Try to unsubscribe by http first.
|
||||||
// or sometimes with mailto AND http link. We need to handle this.
|
if (CurrentRenderModel.UnsubscribeInfo.HttpLink is not null)
|
||||||
|
{
|
||||||
if (Uri.IsWellFormedUriString(CurrentRenderModel.UnsubscribeLink, UriKind.RelativeOrAbsolute))
|
if (!Uri.IsWellFormedUriString(CurrentRenderModel.UnsubscribeInfo.HttpLink, UriKind.RelativeOrAbsolute))
|
||||||
await NativeAppService.LaunchUriAsync(new Uri((CurrentRenderModel.UnsubscribeLink)));
|
{
|
||||||
else
|
|
||||||
DialogService.InfoBarMessage(Translator.Info_UnsubscribeLinkInvalidTitle, Translator.Info_UnsubscribeLinkInvalidMessage, InfoBarMessageType.Error);
|
DialogService.InfoBarMessage(Translator.Info_UnsubscribeLinkInvalidTitle, Translator.Info_UnsubscribeLinkInvalidMessage, InfoBarMessageType.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Support for List-Unsubscribe-Post header. It can be done without launching browser.
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc8058
|
||||||
|
if (CurrentRenderModel.UnsubscribeInfo.IsOneClick)
|
||||||
|
{
|
||||||
|
confirmed = await DialogService.ShowConfirmationDialogAsync(string.Format(Translator.DialogMessage_UnsubscribeConfirmationOneClickMessage, FromName), Translator.DialogMessage_UnsubscribeConfirmationTitle, Translator.Unsubscribe);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
using var httpClient = new HttpClient();
|
||||||
|
|
||||||
|
var unsubscribeRequest = new HttpRequestMessage(HttpMethod.Post, CurrentRenderModel.UnsubscribeInfo.HttpLink)
|
||||||
|
{
|
||||||
|
Content = new StringContent("List-Unsubscribe=One-Click", Encoding.UTF8, "application/x-www-form-urlencoded")
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = await httpClient.SendAsync(unsubscribeRequest);
|
||||||
|
if (result.IsSuccessStatusCode)
|
||||||
|
{
|
||||||
|
DialogService.InfoBarMessage(Translator.Unsubscribe, string.Format(Translator.Info_UnsubscribeSuccessMessage, FromName), InfoBarMessageType.Success);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DialogService.InfoBarMessage(Translator.GeneralTitle_Error, Translator.Info_UnsubscribeErrorMessage, InfoBarMessageType.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
confirmed = await DialogService.ShowConfirmationDialogAsync(string.Format(Translator.DialogMessage_UnsubscribeConfirmationGoToWebsiteMessage, FromName), Translator.DialogMessage_UnsubscribeConfirmationTitle, Translator.DialogMessage_UnsubscribeConfirmationGoToWebsiteConfirmButton);
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
await NativeAppService.LaunchUriAsync(new Uri(CurrentRenderModel.UnsubscribeInfo.HttpLink));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (CurrentRenderModel.UnsubscribeInfo.MailToLink is not null)
|
||||||
|
{
|
||||||
|
confirmed = await DialogService.ShowConfirmationDialogAsync(string.Format(Translator.DialogMessage_UnsubscribeConfirmationMailtoMessage, FromName, new string(CurrentRenderModel.UnsubscribeInfo.MailToLink.Skip(7).ToArray())), Translator.DialogMessage_UnsubscribeConfirmationTitle, Translator.Unsubscribe);
|
||||||
|
|
||||||
|
if (!confirmed) return;
|
||||||
|
|
||||||
|
// TODO:Implement mailto link support.
|
||||||
|
DialogService.InfoBarMessage(Translator.GeneralTitle_Error, "Mailto unsubscribe is not supported yet.", InfoBarMessageType.Error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|||||||
Reference in New Issue
Block a user