├── design └── PackageAssets │ ├── ERMail.psd │ └── ERMail.Large.psd ├── docs └── assets │ ├── 2018-04-15-19-15-57.png │ └── 2018-04-15-19-18-15.png ├── src ├── ERMail.Universal │ ├── Assets │ │ ├── StoreLogo.png │ │ ├── LockScreenLogo.scale-200.png │ │ ├── SplashScreen.scale-200.png │ │ ├── Square44x44Logo.scale-200.png │ │ ├── Wide310x150Logo.scale-200.png │ │ ├── Square150x150Logo.scale-200.png │ │ └── Square44x44Logo.targetsize-24_altform-unplated.png │ ├── Views │ │ ├── ClassificationPage.xaml │ │ ├── ClassificationPage.xaml.cs │ │ ├── OldMainPage.xaml.cs │ │ ├── ConfigMailBoxDialog.xaml.cs │ │ ├── MailPage.xaml.cs │ │ ├── MainPage.xaml.cs │ │ ├── OldMainPage.xaml │ │ ├── MainPage.xaml │ │ └── MailPage.xaml │ ├── Utils │ │ └── PasswordManager.cs │ ├── Properties │ │ ├── AssemblyInfo.cs │ │ └── Default.rd.xml │ ├── Package.appxmanifest │ ├── App.xaml.cs │ ├── ERMail.Universal.csproj │ └── App.xaml ├── ERMail.Desktop │ ├── Assets │ │ └── avalonia-logo.ico │ ├── nuget.config │ ├── App.xaml.cs │ ├── Views │ │ ├── MainWindow.xaml.cs │ │ ├── MailPage.xaml.cs │ │ ├── MainWindow.xaml │ │ └── MailPage.xaml │ ├── Program.cs │ ├── App.xaml │ ├── ViewLocator.cs │ └── ERMail.Desktop.csproj ├── ERMail.Core │ ├── ViewModels │ │ ├── ViewModelBase.cs │ │ ├── MainViewModel.cs │ │ ├── MailBoxViewModel.cs │ │ ├── MailGroupViewModel.cs │ │ └── MailBoxFolderViewModel.cs │ ├── OAuth │ │ ├── IOAuthInfo.cs │ │ ├── Tenant.cs │ │ ├── ERMailMicrosoftOAuth.cs │ │ └── Scope.cs │ ├── Models │ │ ├── MailBoxConfiguration.cs │ │ ├── MailBoxFolder.cs │ │ ├── MailContentCache.cs │ │ ├── MailSummary.cs │ │ └── MailBoxConnectionInfo.cs │ ├── Utils │ │ ├── ILogger.cs │ │ ├── IPasswordManager.cs │ │ └── FileSerializor.cs │ ├── ERMail.Core.csproj │ ├── Classification │ │ └── NaiveBayesClassifier.cs │ └── Mailing │ │ ├── IncomingMailClient.cs │ │ └── MailBoxCache.cs ├── ERMail.CLI │ ├── ERMail.CLI.csproj.DotSettings │ ├── ERMail.CLI.csproj │ ├── MailBoxConfiguration.cs │ ├── PasswordManager.cs │ └── Program.cs ├── AssembleMailing.Universal │ ├── Properties │ │ └── AssemblyInfo.cs │ ├── Package.appxmanifest │ └── Views │ │ └── MainPage.xaml └── AssembleMailing.Desktop │ └── Views │ └── MainWindow.xaml ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── ERMail.sln.DotSettings ├── README.zh-chs.md ├── README.zh-cht.md ├── README.jp.md ├── GitVersion.config ├── LICENSE ├── .gitattributes ├── README.md ├── .gitignore └── ERMail.sln /design/PackageAssets/ERMail.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterlv/ERMail/HEAD/design/PackageAssets/ERMail.psd -------------------------------------------------------------------------------- /docs/assets/2018-04-15-19-15-57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterlv/ERMail/HEAD/docs/assets/2018-04-15-19-15-57.png -------------------------------------------------------------------------------- /docs/assets/2018-04-15-19-18-15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterlv/ERMail/HEAD/docs/assets/2018-04-15-19-18-15.png -------------------------------------------------------------------------------- /design/PackageAssets/ERMail.Large.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterlv/ERMail/HEAD/design/PackageAssets/ERMail.Large.psd -------------------------------------------------------------------------------- /src/ERMail.Universal/Assets/StoreLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterlv/ERMail/HEAD/src/ERMail.Universal/Assets/StoreLogo.png -------------------------------------------------------------------------------- /src/ERMail.Desktop/Assets/avalonia-logo.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterlv/ERMail/HEAD/src/ERMail.Desktop/Assets/avalonia-logo.ico -------------------------------------------------------------------------------- /src/ERMail.Universal/Assets/LockScreenLogo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterlv/ERMail/HEAD/src/ERMail.Universal/Assets/LockScreenLogo.scale-200.png -------------------------------------------------------------------------------- /src/ERMail.Universal/Assets/SplashScreen.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterlv/ERMail/HEAD/src/ERMail.Universal/Assets/SplashScreen.scale-200.png -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "pasteImage.path": "${projectRoot}/docs/assets", 3 | "pasteImage.basePath": "${projectRoot}", 4 | "pasteImage.prefix": "/" 5 | } -------------------------------------------------------------------------------- /src/ERMail.Universal/Assets/Square44x44Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterlv/ERMail/HEAD/src/ERMail.Universal/Assets/Square44x44Logo.scale-200.png -------------------------------------------------------------------------------- /src/ERMail.Universal/Assets/Wide310x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterlv/ERMail/HEAD/src/ERMail.Universal/Assets/Wide310x150Logo.scale-200.png -------------------------------------------------------------------------------- /src/ERMail.Universal/Assets/Square150x150Logo.scale-200.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterlv/ERMail/HEAD/src/ERMail.Universal/Assets/Square150x150Logo.scale-200.png -------------------------------------------------------------------------------- /src/ERMail.Core/ViewModels/ViewModelBase.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | 3 | namespace Walterlv.ERMail.ViewModels 4 | { 5 | public class ViewModelBase : ReactiveObject 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/ERMail.Core/OAuth/IOAuthInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Walterlv.ERMail.OAuth 4 | { 5 | public interface IOAuthInfo 6 | { 7 | Uri MakeUrl(); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/ERMail.Universal/Assets/Square44x44Logo.targetsize-24_altform-unplated.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/walterlv/ERMail/HEAD/src/ERMail.Universal/Assets/Square44x44Logo.targetsize-24_altform-unplated.png -------------------------------------------------------------------------------- /src/ERMail.Desktop/nuget.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/ERMail.Desktop/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Markup.Xaml; 3 | 4 | namespace Walterlv.ERMail 5 | { 6 | public class App : Application 7 | { 8 | public override void Initialize() 9 | { 10 | AvaloniaXamlLoader.Load(this); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/ERMail.Desktop/Views/MainWindow.xaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace Walterlv.ERMail.Views 6 | { 7 | public class MainWindow : Window 8 | { 9 | public MainWindow() 10 | { 11 | AvaloniaXamlLoader.Load(this); 12 | #if DEBUG 13 | this.AttachDevTools(); 14 | #endif 15 | } 16 | 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "command": "dotnet", 7 | "type": "process", 8 | "args": [ 9 | "build", 10 | "${workspaceFolder}/src/ERMail.Desktop/ERMail.Desktop.csproj" 11 | ], 12 | "problemMatcher": "$msCompile" 13 | } 14 | ] 15 | } -------------------------------------------------------------------------------- /ERMail.sln.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | ER -------------------------------------------------------------------------------- /src/ERMail.CLI/ERMail.CLI.csproj.DotSettings: -------------------------------------------------------------------------------- 1 | 2 | CSharp71 -------------------------------------------------------------------------------- /src/ERMail.Desktop/Views/MailPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Controls; 3 | using Avalonia.Markup.Xaml; 4 | 5 | namespace Walterlv.ERMail.Views 6 | { 7 | public class MailPage : UserControl 8 | { 9 | public MailPage() 10 | { 11 | this.InitializeComponent(); 12 | } 13 | 14 | private void InitializeComponent() 15 | { 16 | AvaloniaXamlLoader.Load(this); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /README.zh-chs.md: -------------------------------------------------------------------------------- 1 | [English][en]|[日本語][jp]|[简体中文][zh-chs]|[繁體中文][zh-cht] 2 | -|-|-|- 3 | 4 | [en]: /README.md 5 | [jp]: /README.jp.md 6 | [zh-chs]: /README.zh-chs.md 7 | [zh-cht]: /README.zh-cht.md 8 | 9 | # 呀!邮箱 10 | 11 | 是否对杂七杂八的系统自动发送的邮件感到厌烦?呀邮箱是一款将这些系统自动发出的邮件进行聚合显示的邮箱客户端。在它的帮助下,你将能用统一的方式呈现最关键邮件信息,提高工作效率。 12 | 13 | ![UWP Client](/docs/assets/2018-04-15-19-15-57.png) 14 | ▲ UWP Client 15 | 16 | ![Avalonia Client](/docs/assets/2018-04-15-19-18-15.png) 17 | ▲ Avalonia Client 18 | -------------------------------------------------------------------------------- /README.zh-cht.md: -------------------------------------------------------------------------------- 1 | [English][en]|[日本語][jp]|[简体中文][zh-chs]|[繁體中文][zh-cht] 2 | -|-|-|- 3 | 4 | [en]: /README.md 5 | [jp]: /README.jp.md 6 | [zh-chs]: /README.zh-chs.md 7 | [zh-cht]: /README.zh-cht.md 8 | 9 | # 呀!郵箱 10 | 11 | 是否對雜七雜八的系統自動發送的郵件感到厭煩?呀郵箱是一款將這些系統自動發出的郵件進行聚合顯示的郵箱客戶端。在它的幫助下,你將能用統一的方式呈現最關鍵郵件信息,提高工作效率。 12 | 13 | ![UWP Client](/docs/assets/2018-04-15-19-15-57.png) 14 | ▲ UWP Client 15 | 16 | ![Avalonia Client](/docs/assets/2018-04-15-19-18-15.png) 17 | ▲ Avalonia Client 18 | -------------------------------------------------------------------------------- /README.jp.md: -------------------------------------------------------------------------------- 1 | [English][en]|[日本語][jp]|[简体中文][zh-chs]|[繁體中文][zh-cht] 2 | -|-|-|- 3 | 4 | [en]: /README.md 5 | [jp]: /README.jp.md 6 | [zh-chs]: /README.zh-chs.md 7 | [zh-cht]: /README.zh-cht.md 8 | 9 | # 集約された電子メール 10 | 11 | その他のシステムに電子メールを自動的に送信するのは面倒ですか? コンパイル済みメールは、これらのシステムから送信された電子メールを自動的に集約する電子メールクライアントです。 その助けを借りて、最も重要な電子メールメッセージを統一された方法で提示し、生産性を向上させることができます。 12 | 13 | ![UWP Client](/docs/assets/2018-04-15-19-15-57.png) 14 | ▲ UWP Client 15 | 16 | ![Avalonia Client](/docs/assets/2018-04-15-19-18-15.png) 17 | ▲ Avalonia Client 18 | -------------------------------------------------------------------------------- /src/ERMail.CLI/ERMail.CLI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | Walterlv.ERMail 7 | ERMail 8 | win10-x64;osx.10.11-x64 9 | latest 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/ERMail.Core/Models/MailBoxConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Walterlv.ERMail.Models 4 | { 5 | /// 6 | /// Stores connection information of all mail boxes. 7 | /// This is a model that will be serialized into a file. 8 | /// 9 | public class MailBoxConfiguration 10 | { 11 | /// 12 | /// Gets or sets all connection information of all mail boxes. 13 | /// 14 | public IList Connections { get; set; } = new List(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/ERMail.Core/Utils/ILogger.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | namespace Walterlv.ERMail.Utils 4 | { 5 | /// 6 | /// If a class need a Logger, it should arrange an interface in it's constructor parameter list. 7 | /// 8 | public interface ILogger 9 | { 10 | /// 11 | /// Log a new message. 12 | /// 13 | /// 14 | /// 15 | void Log(string message, [CallerMemberName] string callerName = null); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/ERMail.Desktop/Program.cs: -------------------------------------------------------------------------------- 1 | using Avalonia; 2 | using Avalonia.Logging.Serilog; 3 | using Walterlv.ERMail.ViewModels; 4 | using Walterlv.ERMail.Views; 5 | 6 | namespace Walterlv.ERMail 7 | { 8 | class Program 9 | { 10 | static void Main(string[] args) 11 | { 12 | BuildAvaloniaApp().Start(() => new MainViewModel()); 13 | } 14 | 15 | public static AppBuilder BuildAvaloniaApp() 16 | => AppBuilder.Configure() 17 | .UsePlatformDetect() 18 | .UseReactiveUI() 19 | .LogToDebug(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/ERMail.Desktop/App.xaml: -------------------------------------------------------------------------------- 1 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/ERMail.Core/Models/MailBoxFolder.cs: -------------------------------------------------------------------------------- 1 | namespace Walterlv.ERMail.Models 2 | { 3 | /// 4 | /// Stores a mail box folder info so that we could match the remote folder from the mail server. 5 | /// This is a model that will be serialized into a file. 6 | /// 7 | public class MailBoxFolder 8 | { 9 | /// 10 | /// Gets or sets the folder name. 11 | /// 12 | public string Name { get; set; } 13 | 14 | /// 15 | /// Gets or sets the full folder name. Use this to match the remote folder of the mail server. 16 | /// 17 | public string FullName { get; set; } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/ERMail.Core/ERMail.Core.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netstandard2.0;netcoreapp2.1;net471;uap10.0.16299 4 | Walterlv.ERMail 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/ERMail.Universal/Views/ClassificationPage.xaml: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /GitVersion.config: -------------------------------------------------------------------------------- 1 | branches: 2 | master: 3 | regex: master 4 | mode: ContinuousDelivery 5 | tag: '' 6 | increment: Patch 7 | prevent-increment-of-merged-branch-version: true 8 | track-merge-target: false 9 | tracks-release-branches: false 10 | is-release-branch: true 11 | release: 12 | regex: r(elease)?(s)?[/-] 13 | mode: ContinuousDelivery 14 | tag: beta 15 | increment: Patch 16 | prevent-increment-of-merged-branch-version: true 17 | track-merge-target: false 18 | tracks-release-branches: false 19 | is-release-branch: true 20 | feature: 21 | regex: f(eature)?(s)?[/-] 22 | mode: ContinuousDelivery 23 | tag: useBranchName 24 | increment: Inherit 25 | prevent-increment-of-merged-branch-version: false 26 | track-merge-target: false 27 | tracks-release-branches: false 28 | is-release-branch: false 29 | -------------------------------------------------------------------------------- /src/ERMail.Universal/Utils/PasswordManager.cs: -------------------------------------------------------------------------------- 1 | using Windows.Security.Credentials; 2 | using Walterlv.ERMail.Mailing; 3 | 4 | namespace Walterlv.ERMail.Utils 5 | { 6 | internal class PasswordManager : IPasswordManager 7 | { 8 | private const string MailVaultResourceName = "Walterlv.ERMail"; 9 | 10 | internal static IPasswordManager Current = new PasswordManager(); 11 | 12 | string IPasswordManager.Retrieve(string key) 13 | { 14 | var vault = new PasswordVault(); 15 | var credential = vault.Retrieve(MailVaultResourceName, key); 16 | return credential.Password; 17 | } 18 | 19 | void IPasswordManager.Add(string key, string password) 20 | { 21 | var vault = new PasswordVault(); 22 | vault.Add(new PasswordCredential(MailVaultResourceName, key, password)); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/ERMail.Desktop/ViewLocator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Avalonia.Controls; 3 | using Avalonia.Controls.Templates; 4 | using Walterlv.ERMail.ViewModels; 5 | 6 | namespace Walterlv.ERMail 7 | { 8 | public class ViewLocator : IDataTemplate 9 | { 10 | public bool SupportsRecycling => false; 11 | 12 | public IControl Build(object data) 13 | { 14 | var name = data.GetType().FullName.Replace("ViewModel", "View"); 15 | var type = Type.GetType(name); 16 | 17 | if (type != null) 18 | { 19 | return (Control)Activator.CreateInstance(type); 20 | } 21 | else 22 | { 23 | return new TextBlock { Text = "Not Found: " + name }; 24 | } 25 | } 26 | 27 | public bool Match(object data) 28 | { 29 | return data is ViewModelBase; 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/ERMail.Core/ViewModels/MainViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Diagnostics.Contracts; 3 | using ReactiveUI; 4 | 5 | namespace Walterlv.ERMail.ViewModels 6 | { 7 | public class MainViewModel : ViewModelBase 8 | { 9 | public string Greeting => "Hello World!"; 10 | 11 | public ObservableCollection MailBoxes { get; } = new ObservableCollection 12 | { 13 | new MailBoxViewModel {DisplayName = "Outlook"}, 14 | new MailBoxViewModel {DisplayName = "Gmail"}, 15 | new MailBoxViewModel {DisplayName = "iCloud"}, 16 | }; 17 | 18 | public MailBoxViewModel CurrentMailBox 19 | { 20 | get => _currentMailBox ?? (_currentMailBox = MailBoxes[1]); 21 | set => this.RaiseAndSetIfChanged(ref _currentMailBox, value); 22 | } 23 | 24 | [ContractPublicPropertyName(nameof(CurrentMailBox))] 25 | private MailBoxViewModel _currentMailBox; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/ERMail.Core/Utils/IPasswordManager.cs: -------------------------------------------------------------------------------- 1 | namespace Walterlv.ERMail.Utils 2 | { 3 | /// 4 | /// Provide methods to get or set user's password. 5 | /// We should not store the user's password directly, but we can use platform-specified method to store them. 6 | /// So there must be a password manager interface so that different platform can have it's own security solution. 7 | /// 8 | public interface IPasswordManager 9 | { 10 | /// 11 | /// Retrieve a user's password by a key. The key is commonly the users account id of mail address. 12 | /// 13 | /// 14 | /// 15 | string Retrieve(string key); 16 | 17 | /// 18 | /// Add to store a new password in a secure method. The key is commonly the users account id of mail address. 19 | /// 20 | /// 21 | /// 22 | void Add(string key, string password); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/ERMail.Core/Models/MailContentCache.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Walterlv.ERMail.Models 4 | { 5 | /// 6 | /// Stores the fetched mail content of a mail. 7 | /// This is a model that will be serialized into a file. 8 | /// 9 | public class MailContentCache 10 | { 11 | /// 12 | /// Gets or sets the topic of the mail. 13 | /// 14 | public string Topic { get; set; } 15 | 16 | /// 17 | /// Gets or sets the mail plain text content. 18 | /// 19 | public string Content { get; set; } 20 | 21 | /// 22 | /// Gets or sets the mail html file name. 23 | /// It is recommended to store the file name in relative path. 24 | /// 25 | public string HtmlFileName { get; set; } 26 | 27 | /// 28 | /// Gets or sets the attachment file names. 29 | /// 30 | public List AttachmentFileNames { get; set; } = new List(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 walterlv 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/ERMail.Core/Models/MailSummary.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace Walterlv.ERMail.Models 4 | { 5 | /// 6 | /// Stores the mail summary. 7 | /// 8 | public class MailSummary 9 | { 10 | /// 11 | /// Gets or sets the title of a mail. 12 | /// In this app, it is the name of the Envelope.From. 13 | /// TODO **It's improper!** 14 | /// 15 | public string Title { get; set; } 16 | 17 | /// 18 | /// Gets or sets the topic of a mail. 19 | /// 20 | public string Topic { get; set; } 21 | 22 | /// 23 | /// Gets or sets the excerpt of a mail. 24 | /// 25 | public string Excerpt { get; set; } 26 | 27 | /// 28 | /// Gets or sets the mail ids of this mail group summary. 29 | /// TODO It's improper because we should stores the origin data so that we could change our Views in an easier way. 30 | /// 31 | public IList MailIds { get; set; } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/ERMail.Desktop/ERMail.Desktop.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | Exe 4 | netcoreapp2.0 5 | Walterlv.ERMail 6 | ERMail 7 | win10-x64;osx.10.11-x64 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/ERMail.Universal/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ERMail")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("walterlv")] 12 | [assembly: AssemblyProduct("ER Mail")] 13 | [assembly: AssemblyCopyright("Copyright © walterlv 2017-2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Version information for an assembly consists of the following four values: 18 | // 19 | // Major Version 20 | // Minor Version 21 | // Build Number 22 | // Revision 23 | // 24 | // You can specify all the values or you can default the Build and Revision Numbers 25 | // by using the '*' as shown below: 26 | // [assembly: AssemblyVersion("1.0.*")] 27 | [assembly: AssemblyVersion("1.0.0.0")] 28 | [assembly: AssemblyFileVersion("1.0.0.0")] 29 | [assembly: ComVisible(false)] -------------------------------------------------------------------------------- /src/AssembleMailing.Universal/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | // General Information about an assembly is controlled through the following 6 | // set of attributes. Change these attribute values to modify the information 7 | // associated with an assembly. 8 | [assembly: AssemblyTitle("ERMail")] 9 | [assembly: AssemblyDescription("")] 10 | [assembly: AssemblyConfiguration("")] 11 | [assembly: AssemblyCompany("walterlv")] 12 | [assembly: AssemblyProduct("ER Mail")] 13 | [assembly: AssemblyCopyright("Copyright © walterlv 2017-2018")] 14 | [assembly: AssemblyTrademark("")] 15 | [assembly: AssemblyCulture("")] 16 | 17 | // Version information for an assembly consists of the following four values: 18 | // 19 | // Major Version 20 | // Minor Version 21 | // Build Number 22 | // Revision 23 | // 24 | // You can specify all the values or you can default the Build and Revision Numbers 25 | // by using the '*' as shown below: 26 | // [assembly: AssemblyVersion("1.0.*")] 27 | [assembly: AssemblyVersion("1.0.0.0")] 28 | [assembly: AssemblyFileVersion("1.0.0.0")] 29 | [assembly: ComVisible(false)] -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceFolder}/src/ERMail.Desktop/bin/Debug/netcoreapp2.0/ERMail.dll", 14 | "args": [], 15 | "cwd": "${workspaceFolder}/src/ERMail.Desktop", 16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 17 | "console": "internalConsole", 18 | "stopAtEntry": false, 19 | "internalConsoleOptions": "openOnSessionStart" 20 | }, 21 | { 22 | "name": ".NET Core Attach", 23 | "type": "coreclr", 24 | "request": "attach", 25 | "processId": "${command:pickProcess}" 26 | } 27 | ,] 28 | } -------------------------------------------------------------------------------- /src/ERMail.Universal/Properties/Default.rd.xml: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/AssembleMailing.Universal/Package.appxmanifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ERMail.Universal 7 | lvyi 8 | Assets\StoreLogo.png 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/ERMail.Desktop/Views/MainWindow.xaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/AssembleMailing.Desktop/Views/MainWindow.xaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 18 | 20 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/ERMail.Core/ViewModels/MailBoxViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Diagnostics.Contracts; 3 | using ReactiveUI; 4 | using Walterlv.ERMail.Models; 5 | 6 | namespace Walterlv.ERMail.ViewModels 7 | { 8 | public sealed class MailBoxViewModel : ViewModelBase 9 | { 10 | public string DisplayName 11 | { 12 | get => _displayName; 13 | set => this.RaiseAndSetIfChanged(ref _displayName, value); 14 | } 15 | 16 | public string MailAddress 17 | { 18 | get => _mailAddress; 19 | set => this.RaiseAndSetIfChanged(ref _mailAddress, value); 20 | } 21 | 22 | public MailBoxConnectionInfo ConnectionInfo 23 | { 24 | get => _connectionInfo; 25 | set => this.RaiseAndSetIfChanged(ref _connectionInfo, value); 26 | } 27 | 28 | public ObservableCollection Folders { get; } 29 | = new ObservableCollection(); 30 | 31 | public MailBoxFolderViewModel CurrentFolder 32 | { 33 | get => _currentFolder; 34 | set => this.RaiseAndSetIfChanged(ref _currentFolder, value); 35 | } 36 | 37 | [ContractPublicPropertyName(nameof(DisplayName))] 38 | private string _displayName; 39 | 40 | [ContractPublicPropertyName(nameof(MailAddress))] 41 | private string _mailAddress; 42 | 43 | [ContractPublicPropertyName(nameof(CurrentFolder))] 44 | private MailBoxFolderViewModel _currentFolder; 45 | 46 | [ContractPublicPropertyName(nameof(ConnectionInfo))] 47 | private MailBoxConnectionInfo _connectionInfo; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ERMail.Desktop/Views/MailPage.xaml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 22 | 23 | 24 | 25 | 27 | 28 | 29 | 30 | 32 | 34 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/ERMail.Core/Classification/NaiveBayesClassifier.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Async; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Walterlv.ERMail.Utils; 7 | 8 | namespace Walterlv.ERMail.Classification 9 | { 10 | public class NaiveBayesClassifier 11 | { 12 | private readonly ILogger _logger; 13 | 14 | public NaiveBayesClassifier(ILogger logger) 15 | { 16 | _logger = logger; 17 | } 18 | 19 | public async Task RunAsync(string current, IAsyncEnumerable all) 20 | { 21 | var currentWords = CreateWordsDictionary(current); 22 | _logger.Log($"{string.Join(", ", currentWords.SkipWhile(x => x.Value <= 1).Select(pair => $"{pair.Key}({pair.Value})"))}"); 23 | 24 | await all.Skip(1).ForEachAsync(async mail => 25 | { 26 | var words = CreateWordsDictionary(mail); 27 | var allWords = CreateWordsDictionary(mail, currentWords); 28 | _logger.Log($"{string.Join(", ", words.SkipWhile(x => x.Value <= 1).Select(pair => $"{pair.Key}({pair.Value})"))}"); 29 | _logger.Log($"{string.Join(", ", allWords.SkipWhile(x => x.Value <= 1).Select(pair => $"{pair.Key}({pair.Value})"))}"); 30 | }); 31 | } 32 | 33 | private Dictionary CreateWordsDictionary(string content, Dictionary source = null) 34 | { 35 | var result = source ?? new Dictionary(); 36 | var currentWords = content.Split(new[] {' ', '\r', '\n'}, StringSplitOptions.RemoveEmptyEntries); 37 | foreach (var word in currentWords) 38 | { 39 | if (!result.TryGetValue(word, out var count)) 40 | { 41 | count = 0; 42 | } 43 | result[word] = count + 1; 44 | } 45 | 46 | return result.OrderByDescending(x => x.Value).ToDictionary(x => x.Key, x => x.Value); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ERMail.Universal/Package.appxmanifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ER Mail 7 | walterlv 8 | Assets\StoreLogo.png 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/ERMail.Core/ViewModels/MailGroupViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Diagnostics.Contracts; 3 | using ReactiveUI; 4 | using Walterlv.ERMail.Models; 5 | 6 | namespace Walterlv.ERMail.ViewModels 7 | { 8 | public class MailGroupViewModel : ViewModelBase 9 | { 10 | public string Title 11 | { 12 | get => _title; 13 | set => this.RaiseAndSetIfChanged(ref _title, value); 14 | } 15 | 16 | public string Topic 17 | { 18 | get => _topic; 19 | set => this.RaiseAndSetIfChanged(ref _topic, value); 20 | } 21 | 22 | public string Excerpt 23 | { 24 | get => _excerpt; 25 | set => this.RaiseAndSetIfChanged(ref _excerpt, value); 26 | } 27 | 28 | public ObservableCollection MailIds { get; } = new ObservableCollection(); 29 | 30 | public static implicit operator MailGroupViewModel(MailSummary source) 31 | { 32 | var target = new MailGroupViewModel 33 | { 34 | Title = source.Title, 35 | Topic = source.Topic, 36 | Excerpt = source.Excerpt, 37 | }; 38 | foreach (var mailId in source.MailIds) 39 | { 40 | target.MailIds.Add(mailId); 41 | } 42 | 43 | return target; 44 | } 45 | 46 | public static implicit operator MailSummary(MailGroupViewModel source) 47 | { 48 | var target = new MailSummary 49 | { 50 | Title = source.Title, 51 | Topic = source.Topic, 52 | Excerpt = source.Excerpt, 53 | }; 54 | foreach (var mailId in source.MailIds) 55 | { 56 | target.MailIds.Add(mailId); 57 | } 58 | 59 | return target; 60 | } 61 | 62 | [ContractPublicPropertyName(nameof(Title))] 63 | private string _title; 64 | 65 | [ContractPublicPropertyName(nameof(Topic))] 66 | private string _topic; 67 | 68 | [ContractPublicPropertyName(nameof(Excerpt))] 69 | private string _excerpt; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/ERMail.Core/OAuth/Tenant.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using JetBrains.Annotations; 3 | 4 | namespace Walterlv.ERMail.OAuth 5 | { 6 | public class Tenant : IEquatable 7 | { 8 | /// 9 | /// Allows users with both personal Microsoft accounts and work/school accounts from Azure Active Directory to sign into the application. 10 | /// 11 | public static Tenant Common = new Tenant("common"); 12 | 13 | /// 14 | /// Allows only users with work/school accounts from Azure Active Directory to sign into the application. 15 | /// 16 | public static Tenant Organizations = new Tenant("organizations"); 17 | 18 | /// 19 | /// Allows only users with personal Microsoft accounts (MSA) to sign into the application. 20 | /// 21 | public static Tenant Consumers = new Tenant("consumers"); 22 | 23 | /// 24 | /// Initialize a new instance of . 25 | /// 26 | /// 27 | public Tenant([NotNull] string tenant) 28 | { 29 | _value = tenant ?? throw new ArgumentNullException(nameof(tenant)); 30 | } 31 | 32 | /// 33 | /// Gets the tenant value. 34 | /// 35 | private readonly string _value; 36 | 37 | /// 38 | public bool Equals(Tenant other) 39 | { 40 | if (other is null) return false; 41 | if (ReferenceEquals(this, other)) return true; 42 | return string.Equals(_value, other._value); 43 | } 44 | 45 | /// 46 | public override bool Equals(object obj) 47 | { 48 | if (obj is null) return false; 49 | if (ReferenceEquals(this, obj)) return true; 50 | if (obj.GetType() != GetType()) return false; 51 | return Equals((Tenant) obj); 52 | } 53 | 54 | /// 55 | public override int GetHashCode() 56 | { 57 | return _value.GetHashCode(); 58 | } 59 | 60 | /// 61 | public override string ToString() 62 | { 63 | return _value; 64 | } 65 | 66 | public static implicit operator string(Tenant tenant) 67 | { 68 | return tenant._value; 69 | } 70 | 71 | public static implicit operator Tenant(string tenant) 72 | { 73 | return new Tenant(tenant); 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/ERMail.Universal/Views/ClassificationPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Async; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using Windows.Storage; 6 | using Windows.UI.Core; 7 | using Windows.UI.Xaml.Controls; 8 | using Windows.UI.Xaml.Navigation; 9 | using Walterlv.ERMail.Classification; 10 | using Walterlv.ERMail.Mailing; 11 | using Walterlv.ERMail.Models; 12 | using Walterlv.ERMail.Utils; 13 | using Walterlv.ERMail.ViewModels; 14 | 15 | namespace Walterlv.ERMail.Views 16 | { 17 | public sealed partial class ClassificationPage : Page, ILogger 18 | { 19 | public ClassificationPage() 20 | { 21 | InitializeComponent(); 22 | } 23 | 24 | protected override async void OnNavigatedTo(NavigationEventArgs e) 25 | { 26 | base.OnNavigatedTo(e); 27 | 28 | //SystemNavigationManager.GetForCurrentView().BackRequested += OnBackRequested; 29 | 30 | //SystemNavigationManager.GetForCurrentView().AppViewBackButtonVisibility = 31 | // Frame.CanGoBack ? 32 | // AppViewBackButtonVisibility.Visible : 33 | // AppViewBackButtonVisibility.Collapsed; 34 | 35 | //void OnBackRequested(object sender, BackRequestedEventArgs e) 36 | //{ 37 | // Frame.GoBack(new SlideNavigationTransitionInfo()); 38 | //} 39 | 40 | if (e.Parameter is ValueTuple tuple) 41 | { 42 | var (info, folder, group) = tuple; 43 | 44 | var localFolder = ApplicationData.Current.LocalFolder.Path; 45 | var mailCache = MailBoxCache.Get(localFolder, info, PasswordManager.Current); 46 | 47 | var current = await mailCache.LoadMailAsync(folder, group.MailIds.First()); 48 | var mails = mailCache.EnumerateMailsAsync(folder); 49 | 50 | await new NaiveBayesClassifier(this).RunAsync($"{current.Topic}{Environment.NewLine}{current.Content}", 51 | mails.Select(x => $"{x.Topic}{Environment.NewLine}{x.Content}")); 52 | } 53 | } 54 | 55 | private ObservableCollection Logs { get; } = new ObservableCollection(); 56 | 57 | async void ILogger.Log(string message, string callerName) 58 | { 59 | await Dispatcher.RunAsync(CoreDispatcherPriority.Low, () => 60 | { 61 | message = $"[{DateTimeOffset.Now:T}] {message}"; 62 | Logs.Add(message); 63 | }); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /src/ERMail.Core/OAuth/ERMailMicrosoftOAuth.cs: -------------------------------------------------------------------------------- 1 | //using System; 2 | //using System.Linq; 3 | //using System.Threading.Tasks; 4 | //using Microsoft.Identity.Client; 5 | 6 | //namespace Walterlv.ERMail.OAuth 7 | //{ 8 | // /// 9 | // /// Stores OAuth info of outlook.com. 10 | // /// See https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-v2-scopes fore more details. 11 | // /// 12 | // public class ERMailMicrosoftOAuth : IOAuthInfo 13 | // { 14 | // public string ClientId { get; } = "00000000482269A3"; 15 | 16 | // public Tenant Tenant { get; } = "common"; 17 | 18 | // public Scope Scope { get; } = "openid profile offline_access email"; 19 | 20 | // public string ResponseType { get; } = "code"; 21 | 22 | // public Uri MakeUrl() 23 | // { 24 | // var uri = new Uri($@"https://login.microsoftonline.com/{Tenant}/oauth2/v2.0/authorize 25 | //?client_id={ClientId} 26 | //&&response_type={ResponseType} 27 | //&scope={Scope}"); 28 | // return uri; 29 | // } 30 | 31 | // public async Task AcquireTokenAsync() 32 | // { 33 | // var publicClientApp = new PublicClientApplication(ClientId); 34 | // AuthenticationResult authResult; 35 | 36 | // try 37 | // { 38 | // authResult = 39 | // await publicClientApp.AcquireTokenSilentAsync(Scope, publicClientApp.Users.FirstOrDefault()); 40 | // } 41 | // catch (MsalUiRequiredException ex) 42 | // { 43 | // // A MsalUiRequiredException happened on AcquireTokenSilentAsync. This indicates you need to call AcquireTokenAsync to acquire a token 44 | // System.Diagnostics.Debug.WriteLine($"MsalUiRequiredException: {ex.Message}"); 45 | 46 | // try 47 | // { 48 | // authResult = await publicClientApp.AcquireTokenAsync(Scope); 49 | // } 50 | // catch (MsalException msalex) 51 | // { 52 | // // ResultText.Text = $"Error Acquiring Token:{System.Environment.NewLine}{msalex}"; 53 | // throw; 54 | // } 55 | // } 56 | // catch (Exception ex) 57 | // { 58 | // // ResultText.Text = $"Error Acquiring Token Silently:{System.Environment.NewLine}{ex}"; 59 | // throw; 60 | // } 61 | 62 | // return authResult.AccessToken; 63 | // //ResultText.Text = await GetHttpContentWithToken(_graphAPIEndpoint, authResult.AccessToken); 64 | // //DisplayBasicTokenInfo(authResult); 65 | // //this.SignOutButton.Visibility = Visibility.Visible; 66 | // } 67 | // } 68 | //} 69 | -------------------------------------------------------------------------------- /src/ERMail.Core/ViewModels/MailBoxFolderViewModel.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | using System.Diagnostics.Contracts; 3 | using ReactiveUI; 4 | using Walterlv.ERMail.Models; 5 | 6 | namespace Walterlv.ERMail.ViewModels 7 | { 8 | public class MailBoxFolderViewModel : ViewModelBase 9 | { 10 | public string Name 11 | { 12 | get => _name; 13 | set => this.RaiseAndSetIfChanged(ref _name, value); 14 | } 15 | 16 | public string FullName 17 | { 18 | get => _fullName; 19 | set => this.RaiseAndSetIfChanged(ref _fullName, value); 20 | } 21 | 22 | public char Separator 23 | { 24 | get => _separator; 25 | set => this.RaiseAndSetIfChanged(ref _separator, value); 26 | } 27 | 28 | public ObservableCollection Mails { get; } = new ObservableCollection(); 29 | 30 | public static implicit operator MailBoxFolderViewModel(MailBoxFolder source) 31 | { 32 | return new MailBoxFolderViewModel 33 | { 34 | Name = source.Name, 35 | FullName = source.FullName, 36 | }; 37 | } 38 | 39 | public static implicit operator MailBoxFolder(MailBoxFolderViewModel source) 40 | { 41 | return new MailBoxFolder 42 | { 43 | Name = source.Name, 44 | FullName = source.FullName, 45 | }; 46 | } 47 | 48 | [ContractPublicPropertyName(nameof(Name))] 49 | private string _name; 50 | 51 | [ContractPublicPropertyName(nameof(FullName))] 52 | private string _fullName; 53 | 54 | [ContractPublicPropertyName(nameof(Separator))] 55 | private char _separator; 56 | 57 | public override bool Equals(object obj) 58 | { 59 | if (ReferenceEquals(null, obj)) 60 | { 61 | return false; 62 | } 63 | 64 | if (ReferenceEquals(this, obj)) 65 | { 66 | return true; 67 | } 68 | 69 | if (obj.GetType() != this.GetType()) 70 | { 71 | return false; 72 | } 73 | 74 | return Equals((MailBoxFolderViewModel) obj); 75 | } 76 | 77 | protected bool Equals(MailBoxFolderViewModel other) 78 | { 79 | return string.Equals(_fullName, other._fullName); 80 | } 81 | 82 | public override int GetHashCode() 83 | { 84 | return (_fullName != null ? _fullName.GetHashCode() : 0); 85 | } 86 | 87 | public static bool operator ==(MailBoxFolderViewModel left, MailBoxFolderViewModel right) 88 | { 89 | return Equals(left, right); 90 | } 91 | 92 | public static bool operator !=(MailBoxFolderViewModel left, MailBoxFolderViewModel right) 93 | { 94 | return !Equals(left, right); 95 | } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/ERMail.Core/Utils/FileSerializor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading.Tasks; 4 | using Newtonsoft.Json; 5 | 6 | namespace Walterlv.ERMail.Utils 7 | { 8 | /// 9 | /// Serialize or deserialize a class instance using json format. 10 | /// 11 | /// The type which is preparing to serialize. It can be am IList. 12 | public class FileSerializor where T : new() 13 | { 14 | /// 15 | /// Gets the serialization file name (full path). 16 | /// 17 | public string FileName { get; } 18 | 19 | /// 20 | /// Initialize a new instance of . 21 | /// 22 | /// 23 | public FileSerializor(string fileName) 24 | { 25 | if (fileName == null) 26 | { 27 | throw new ArgumentNullException(nameof(fileName)); 28 | } 29 | 30 | if (string.IsNullOrWhiteSpace(fileName)) 31 | { 32 | throw new ArgumentException("Configuration file name should not be empty.", nameof(fileName)); 33 | } 34 | 35 | FileName = fileName; 36 | } 37 | 38 | /// 39 | /// Read an instance from file. If the file does not exist, it will return a new one (with all fields default). 40 | /// 41 | /// 42 | public async Task ReadAsync() 43 | { 44 | if (!File.Exists(FileName)) 45 | { 46 | return new T(); 47 | } 48 | 49 | return await Task.Run(() => Read()).ConfigureAwait(false); 50 | } 51 | 52 | private T Read() 53 | { 54 | var json = JsonSerializer.Create(); 55 | using (var file = new FileStream(FileName, FileMode.Open, FileAccess.Read, FileShare.Write)) 56 | using (TextReader reader = new StreamReader(file)) 57 | { 58 | return json.Deserialize(new JsonTextReader(reader)); 59 | } 60 | } 61 | 62 | /// 63 | /// Save the target object into a file. If the file does not exists, a new one will be created. 64 | /// 65 | /// 66 | /// 67 | public Task SaveAsync(T target) 68 | { 69 | return Task.Run(() => Save(target)); 70 | } 71 | 72 | private void Save(T target) 73 | { 74 | var json = JsonSerializer.Create(); 75 | var directory = new FileInfo(FileName).Directory; 76 | if (directory?.Exists is false) 77 | { 78 | directory.Create(); 79 | } 80 | using (var file = new FileStream(FileName, FileMode.OpenOrCreate, FileAccess.Write, FileShare.None)) 81 | using (TextWriter writer = new StreamWriter(file)) 82 | { 83 | json.Serialize(writer, target); 84 | } 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/ERMail.Universal/Views/OldMainPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Windows.Storage; 6 | using Windows.UI.Xaml; 7 | using Windows.UI.Xaml.Controls; 8 | using Windows.UI.Xaml.Navigation; 9 | using Walterlv.ERMail.Models; 10 | using Walterlv.ERMail.Utils; 11 | using Walterlv.ERMail.ViewModels; 12 | 13 | namespace Walterlv.ERMail.Views 14 | { 15 | public sealed partial class OldMainPage : Page 16 | { 17 | public OldMainPage() 18 | { 19 | InitializeComponent(); 20 | 21 | var localFolder = ApplicationData.Current.LocalFolder; 22 | _configurationFile = new FileSerializor( 23 | Path.Combine(localFolder.Path, "MailBoxConfiguration.json")); 24 | } 25 | 26 | private readonly FileSerializor _configurationFile; 27 | 28 | private MainViewModel ViewModel => (MainViewModel) DataContext; 29 | 30 | protected override async void OnNavigatedTo(NavigationEventArgs e) 31 | { 32 | base.OnNavigatedTo(e); 33 | 34 | var configuration = await _configurationFile.ReadAsync(); 35 | var storedInfo = configuration.Connections.FirstOrDefault(); 36 | storedInfo = storedInfo ?? await ConfigConnectionInfo(); 37 | var mailBox = new MailBoxViewModel 38 | { 39 | DisplayName = storedInfo.AccountName, 40 | MailAddress = storedInfo.Address, 41 | ConnectionInfo = storedInfo, 42 | }; 43 | ViewModel.MailBoxes.Insert(0, mailBox); 44 | MailBoxListView.SelectedIndex = 0; 45 | ViewModel.CurrentMailBox = mailBox; 46 | 47 | DetailFrame.Navigate(typeof(MailPage)); 48 | } 49 | 50 | private async void ConfigButton_Click(object sender, RoutedEventArgs e) 51 | { 52 | var configuration = await _configurationFile.ReadAsync(); 53 | var address = configuration.Connections.Select(x => x.Address).FirstOrDefault(); 54 | var info = await ConfigConnectionInfo(address); 55 | if (info != null) 56 | { 57 | ViewModel.MailBoxes[0].ConnectionInfo = info; 58 | ViewModel.MailBoxes[0].DisplayName = info.AccountName; 59 | } 60 | } 61 | 62 | private async Task ConfigConnectionInfo(string address = null) 63 | { 64 | var configuration = await _configurationFile.ReadAsync(); 65 | var connections = configuration.Connections; 66 | var connectionInfo = connections.FirstOrDefault(x => x.Address == address) ?? new MailBoxConnectionInfo(); 67 | if (!string.IsNullOrWhiteSpace(connectionInfo.Address)) 68 | { 69 | connectionInfo.Password = PasswordManager.Current.Retrieve(connectionInfo.Address); 70 | } 71 | 72 | var config = new ConfigMailBoxDialog(connectionInfo); 73 | var result = await config.ShowAsync(); 74 | if (result == ContentDialogResult.Secondary) 75 | { 76 | PasswordManager.Current.Add(connectionInfo.Address, connectionInfo.Password); 77 | 78 | connections.Clear(); 79 | connections.Add(connectionInfo); 80 | await _configurationFile.SaveAsync(configuration); 81 | return connectionInfo; 82 | } 83 | 84 | return null; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/ERMail.Universal/Views/ConfigMailBoxDialog.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Windows.Security.Authentication.Web; 4 | using Windows.UI.Popups; 5 | using Windows.UI.Xaml; 6 | using Windows.UI.Xaml.Controls; 7 | using Windows.UI.Xaml.Data; 8 | using MailKit.Security; 9 | using Walterlv.ERMail.Mailing; 10 | using Walterlv.ERMail.Models; 11 | using Walterlv.ERMail.OAuth; 12 | 13 | namespace Walterlv.ERMail.Views 14 | { 15 | public sealed partial class ConfigMailBoxDialog : ContentDialog 16 | { 17 | public ConfigMailBoxDialog(MailBoxConnectionInfo connectionInfo) 18 | { 19 | InitializeComponent(); 20 | ConnectionInfo = connectionInfo; 21 | } 22 | 23 | public MailBoxConnectionInfo ConnectionInfo 24 | { 25 | get => (MailBoxConnectionInfo) DataContext; 26 | set => DataContext = value; 27 | } 28 | 29 | private string ErrorTip { get; set; } 30 | 31 | private void ContentDialog_PrimaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs e) 32 | { 33 | } 34 | 35 | private async void ContentDialog_SecondaryButtonClick(ContentDialog sender, ContentDialogButtonClickEventArgs e) 36 | { 37 | var info = ConnectionInfo; 38 | if (!info.Validate()) return; 39 | var deferral = e.GetDeferral(); 40 | 41 | var client = new IncomingMailClient(info); 42 | 43 | try 44 | { 45 | await client.TestConnectionAsync(); 46 | deferral.Complete(); 47 | } 48 | catch (AuthenticationException ex) 49 | { 50 | e.Cancel = true; 51 | ErrorTip = ex.Message; 52 | } 53 | catch (Exception ex) 54 | { 55 | e.Cancel = true; 56 | ErrorTip = ex.Message; 57 | } 58 | } 59 | 60 | private async void AddressTextBox_LostFocus(object sender, RoutedEventArgs e) 61 | { 62 | var parts = ConnectionInfo.Address?.Split(new[] {'@'}, StringSplitOptions.RemoveEmptyEntries); 63 | if (!(parts?.Length is 2)) return; 64 | 65 | var mailHost = $"@{parts[1]}"; 66 | 67 | if (!OAuthDictionary.TryGetValue(mailHost, out var oauth)) return; 68 | 69 | var authenticationResult = await WebAuthenticationBroker.AuthenticateAsync( 70 | WebAuthenticationOptions.UseCorporateNetwork, oauth.MakeUrl()); 71 | await new MessageDialog(authenticationResult.ResponseData).ShowAsync(); 72 | 73 | //WebView.Visibility = Visibility.Visible; 74 | //WebView.Navigate(oauth.MakeUrl()); 75 | } 76 | 77 | private static readonly Dictionary OAuthDictionary = new Dictionary 78 | { 79 | //{"@outlook.com", new ERMailMicrosoftOAuth()}, 80 | }; 81 | } 82 | 83 | internal class VisibilityReverseConverter : IValueConverter 84 | { 85 | public object Convert(object value, Type targetType, object parameter, string language) 86 | { 87 | if (value is Visibility.Visible) 88 | { 89 | return Visibility.Collapsed; 90 | } 91 | 92 | return Visibility.Visible; 93 | } 94 | 95 | public object ConvertBack(object value, Type targetType, object parameter, string language) 96 | { 97 | throw new NotSupportedException(); 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [English][en]|[日本語][jp]|[简体中文][zh-chs]|[繁體中文][zh-cht] 2 | -|-|-|- 3 | 4 | [en]: /README.md 5 | [jp]: /README.jp.md 6 | [zh-chs]: /README.zh-chs.md 7 | [zh-cht]: /README.zh-cht.md 8 | 9 | # ER Mail 10 | 11 | ER Mail is an e-mail client that can help you to assemble related emails into an aggregated view so that there is no need for you to click open them one by one. 12 | 13 | ## Getting started 14 | 15 | ### Run the program (UWP) 16 | 17 | 1. Run 18 | 1. Enter your mail info and then click "Sign in". *We'll not store user's password directly, it is managed by the operating system.* 19 | 1. Select a mail folder (INBOX is selected by default). 20 | 1. Select a mail from the mail list. 21 | 1. Wait and view your mail content, then you can click the Assemble Button on the app's bottom-right corner. 22 | 1. Then our classification algorithm will run with full-screen logs. *The algorithm is under developing so that it will result in nothing.* 23 | 24 | ## How to contribute 25 | 26 | ### Requirement 27 | 28 | - Visual Studio 2017 (with version 15.3 or later) 29 | - .NET Standard 2.0 30 | - .NET Core 2.0 31 | - C# 7.2 32 | - NuGet (4.3 or later) 33 | - UWP (C#) Windows SDK 10.0.16299.0 34 | - for building ERMail.Universal 35 | - [Avalonia](https://github.com/AvaloniaUI/Avalonia) [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) 36 | - for designing ERMail.Desktop 37 | 38 | ### Build and run 39 | 40 | 1. Ensure that your Windows device has developer mode enabled. 41 | - Goto Settings -> Update & Security -> For developers -> Developer mode 42 | 1. Switch your startup project from `ERMail.Desktop` to `ERMail.Universal` 43 | - It's not necessary if you'll contribute to Avalonia version. 44 | 1. Make sure your target device is `Local Machine` not a `Simulator`. 45 | - Goto project properties -> Debug -> Local Machine. 46 | 47 | ### Project structure 48 | 49 | + **.vscode** *If you debug this project using VSCode, this folder contains the build info and debug info.* 50 | + **docs** *Stores documentation of this project.* 51 | - **assets** *Images or other assets that is used by the documentation.* 52 | + **src** *The main source code.* 53 | - **ERMail.Core** *The main logic of this project. All the code here is cross-platform.* 54 | - **ERMail.Desktop** *The startup project targeting Avalonia UI Framework so that it could be cross-platform.* 55 | - **ERMail.Universal** *Windows 10 Specified startup project (UWP).* 56 | - You can add your own UI Framework here, but it should support .NET Standard 2.0. 57 | 58 | ### Roadmap 59 | 60 | This is a project mostly for studying and experimenting new technology we're learning. But we'll also publish the product with these new technologies to increase efficiency and create value for each user. 61 | 62 | 1. [x] Basic UI 63 | - [x] for UWP 64 | - [ ] for Avalonia 65 | - *for Xamarin* 66 | 1. [x] Fetch mails 67 | - [x] from the remote server 68 | - [x] using local cache 69 | 1. [ ] Classify all emails via Machine Learning 70 | - [ ] NaiveBayesClassifier 71 | - [ ] other available machine learning technology 72 | 1. [ ] Word segmentation algorithm 73 | - *for English* 74 | - [ ] for 中文 75 | - *for other languages* 76 | 1. [ ] Publish 77 | - [ ] prepare UI assets for UWP 78 | - [ ] prepare UI assets for Avalonia 79 | - [ ] improve stability 80 | 1. *Introduce some other black technology* 81 | 1. *Analysis feedback data and improve them* 82 | 83 | ## Preview of the UI 84 | 85 | ![UWP Client](/docs/assets/2018-04-15-19-15-57.png) 86 | ▲ UWP Client 87 | 88 | ![Avalonia Client](/docs/assets/2018-04-15-19-18-15.png) 89 | ▲ Avalonia Client 90 | -------------------------------------------------------------------------------- /src/ERMail.CLI/MailBoxConfiguration.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.InteropServices; 3 | using System.Security; 4 | using System.Threading.Tasks; 5 | using MailKit.Security; 6 | using Walterlv.ERMail.Mailing; 7 | using Walterlv.ERMail.Models; 8 | 9 | namespace Walterlv.ERMail 10 | { 11 | public class MailBoxCliConfiguration 12 | { 13 | public async Task Load() 14 | { 15 | Console.WriteLine("No user found, create a new one."); 16 | 17 | while (true) 18 | { 19 | Console.Write("Email address: "); 20 | var emailAddress = Console.ReadLine(); 21 | Console.Write("Password: "); 22 | var password = ReadPassword(); 23 | Console.Write("Incoming(IMAP) and outgoing(SMTP) mail server: "); 24 | var server = Console.ReadLine(); 25 | 26 | var connectionInfo = new MailBoxConnectionInfo 27 | { 28 | Address = emailAddress, 29 | UserName = emailAddress, 30 | Password = SecureStringToString(password), 31 | IncomingServer = server, 32 | OutgoingServer = server, 33 | }; 34 | 35 | var client = new IncomingMailClient(connectionInfo); 36 | try 37 | { 38 | await client.TestConnectionAsync(); 39 | return connectionInfo; 40 | } 41 | catch (AuthenticationException ex) 42 | { 43 | var foregroundColor = Console.ForegroundColor; 44 | Console.ForegroundColor = ConsoleColor.Red; 45 | Console.WriteLine(ex.Message); 46 | Console.ForegroundColor = foregroundColor; 47 | } 48 | catch (Exception ex) 49 | { 50 | var foregroundColor = Console.ForegroundColor; 51 | Console.ForegroundColor = ConsoleColor.Red; 52 | Console.WriteLine(ex.Message); 53 | Console.ForegroundColor = foregroundColor; 54 | } 55 | } 56 | } 57 | 58 | public SecureString ReadPassword() 59 | { 60 | var pwd = new SecureString(); 61 | while (true) 62 | { 63 | var i = Console.ReadKey(true); 64 | if (i.Key == ConsoleKey.Enter) 65 | { 66 | Console.WriteLine(); 67 | break; 68 | } 69 | 70 | if (i.Key == ConsoleKey.Backspace) 71 | { 72 | if (pwd.Length > 0) 73 | { 74 | pwd.RemoveAt(pwd.Length - 1); 75 | Console.Write("\b \b"); 76 | } 77 | } 78 | else 79 | { 80 | pwd.AppendChar(i.KeyChar); 81 | Console.Write("*"); 82 | } 83 | } 84 | return pwd; 85 | } 86 | 87 | private static string SecureStringToString(SecureString value) 88 | { 89 | var valuePtr = IntPtr.Zero; 90 | try 91 | { 92 | valuePtr = Marshal.SecureStringToGlobalAllocUnicode(value); 93 | return Marshal.PtrToStringUni(valuePtr); 94 | } 95 | finally 96 | { 97 | Marshal.ZeroFreeGlobalAllocUnicode(valuePtr); 98 | } 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/ERMail.Universal/Views/MailPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Windows.Storage; 4 | using Windows.UI.Xaml; 5 | using Windows.UI.Xaml.Controls; 6 | using Windows.UI.Xaml.Media.Animation; 7 | using Walterlv.ERMail.Mailing; 8 | using Walterlv.ERMail.Models; 9 | using Walterlv.ERMail.Utils; 10 | using Walterlv.ERMail.ViewModels; 11 | 12 | namespace Walterlv.ERMail.Views 13 | { 14 | public sealed partial class MailPage : Page 15 | { 16 | public MailPage() 17 | { 18 | InitializeComponent(); 19 | } 20 | 21 | private MailBoxCache _mailCache; 22 | 23 | private MailBoxCache MailCache 24 | { 25 | get => _mailCache; 26 | set 27 | { 28 | if (Equals(_mailCache, value)) return; 29 | 30 | if (_mailCache is MailBoxCache oldValue) 31 | { 32 | // Unregister cache events. 33 | } 34 | 35 | _mailCache = value; 36 | 37 | if (value != null) 38 | { 39 | // Register cache events. 40 | } 41 | } 42 | } 43 | 44 | public MailBoxViewModel ViewModel => (MailBoxViewModel) DataContext; 45 | 46 | private async void OnDataContextChanged(FrameworkElement sender, DataContextChangedEventArgs e) 47 | { 48 | if (e.NewValue is MailBoxViewModel vm && vm.ConnectionInfo is MailBoxConnectionInfo info) 49 | { 50 | var localFolder = ApplicationData.Current.LocalFolder.Path; 51 | MailCache = MailBoxCache.Get(localFolder, info, PasswordManager.Current); 52 | var folders = await MailCache.LoadMailFoldersAsync(); 53 | ViewModel.Folders.Clear(); 54 | foreach (var folder in folders) 55 | { 56 | ViewModel.Folders.Add(folder); 57 | } 58 | 59 | var inbox = ViewModel.Folders.FirstOrDefault(x=>x.FullName == "INBOX"); 60 | MailFolderComboBox.SelectedItem = inbox; 61 | } 62 | } 63 | 64 | private async void MailFolderComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) 65 | { 66 | if (ViewModel.ConnectionInfo is null) return; 67 | if (!(e.AddedItems.FirstOrDefault() is MailBoxFolderViewModel vm)) return; 68 | 69 | MailListView.DataContext = vm; 70 | ViewModel.CurrentFolder = vm; 71 | var summaries = await MailCache.LoadMailsAsync(vm); 72 | vm.Mails.Clear(); 73 | foreach (var summary in summaries) 74 | { 75 | vm.Mails.Add(summary); 76 | } 77 | } 78 | 79 | private async void MailGroupListView_SelectionChanged(object sender, SelectionChangedEventArgs e) 80 | { 81 | if (ViewModel.ConnectionInfo is null) return; 82 | if (!(e.AddedItems.FirstOrDefault() is MailGroupViewModel vm)) return; 83 | 84 | var mailCache = await MailCache.LoadMailAsync(ViewModel.CurrentFolder, vm.MailIds.First()); 85 | var file = await StorageFile.GetFileFromPathAsync(mailCache.HtmlFileName); 86 | var text = await FileIO.ReadTextAsync(file); 87 | WebView.NavigateToString(text); 88 | } 89 | 90 | private void AssembleButton_Click(object sender, RoutedEventArgs e) 91 | { 92 | if (!(MailListView.SelectedItems.FirstOrDefault() is MailGroupViewModel vm)) return; 93 | Frame.Navigate(typeof(ClassificationPage), 94 | (ViewModel.ConnectionInfo, ViewModel.CurrentFolder, vm), 95 | new SlideNavigationTransitionInfo()); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/ERMail.Core/Mailing/IncomingMailClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using JetBrains.Annotations; 4 | using MailKit.Net.Imap; 5 | using Walterlv.ERMail.Models; 6 | 7 | namespace Walterlv.ERMail.Mailing 8 | { 9 | /// 10 | /// A mail client that can receive mails. 11 | /// 12 | public class IncomingMailClient 13 | { 14 | /// 15 | /// Initialize a new instance of . 16 | /// 17 | /// 18 | /// 19 | /// 20 | /// Zero to use default, non-zero to specify one. 21 | public IncomingMailClient([NotNull] string userName, [NotNull] string password, 22 | [NotNull] string host, int port = 0) 23 | { 24 | UserName = userName ?? throw new ArgumentNullException(nameof(userName)); 25 | _password = password ?? throw new ArgumentNullException(nameof(password)); 26 | Host = host ?? throw new ArgumentNullException(nameof(host)); 27 | if (port < 0) 28 | { 29 | throw new ArgumentException( 30 | "IncomingServerPort should be larger than 0 (or by default 0).", nameof(port)); 31 | } 32 | 33 | Port = port > 0 ? port : 993; 34 | } 35 | 36 | /// 37 | /// Initialize a new instance of using the specified connection info. 38 | /// 39 | /// 40 | public IncomingMailClient([NotNull] MailBoxConnectionInfo info) 41 | // ReSharper disable once ConstantConditionalAccessQualifier 42 | : this(info?.UserName ?? throw new ArgumentNullException(nameof(info)), 43 | info.Password, info.IncomingServerHost, info.IncomingServerPort) 44 | { 45 | } 46 | 47 | /// 48 | /// Get the incoming host. Like `example.com`. 49 | /// 50 | [NotNull] 51 | public string Host { get; } 52 | 53 | /// 54 | /// Get the incoming port. 55 | /// 56 | public int Port { get; } 57 | 58 | /// 59 | /// Get the mail user name. 60 | /// 61 | [NotNull] 62 | public string UserName { get; } 63 | 64 | /// 65 | /// Get the user password. 66 | /// 67 | [NotNull] private readonly string _password; 68 | 69 | /// 70 | /// Test connection. Just connect and then disconnect the mail server. 71 | /// If test failed, an exception will throw. 72 | /// 73 | /// 74 | public async Task TestConnectionAsync() 75 | { 76 | using (var client = new ImapClient()) 77 | { 78 | // client.ServerCertificateValidationCallback = (s, c, h, e) => true; 79 | await client.ConnectAsync(Host, Port, true); 80 | await client.AuthenticateAsync(UserName, _password); 81 | client.Disconnect(true); 82 | } 83 | } 84 | 85 | /// 86 | /// Connect the mail server to receive mails. 87 | /// Please call this method using the `using` grammar. 88 | /// 89 | /// 90 | /// using(var client = await mailClient.ConnectAsync()) 91 | /// 92 | /// 93 | /// 94 | public async Task ConnectAsync() 95 | { 96 | var client = new ImapClient(); 97 | // client.ServerCertificateValidationCallback = (s, c, h, e) => true; 98 | await client.ConnectAsync(Host, Port, true); 99 | await client.AuthenticateAsync(UserName, _password); 100 | return client; 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/ERMail.Universal/App.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Windows.ApplicationModel; 3 | using Windows.ApplicationModel.Activation; 4 | using Windows.ApplicationModel.Core; 5 | using Windows.UI; 6 | using Windows.UI.ViewManagement; 7 | using Windows.UI.Xaml; 8 | using Windows.UI.Xaml.Controls; 9 | using Windows.UI.Xaml.Navigation; 10 | using Walterlv.ERMail.Views; 11 | using Walterlv.ERMail.ViewModels; 12 | 13 | namespace Walterlv.ERMail 14 | { 15 | /// 16 | /// Provides application-specific behavior to supplement the default Application class. 17 | /// 18 | sealed partial class App : Application 19 | { 20 | /// 21 | /// Initializes the singleton application object. This is the first line of authored code 22 | /// executed, and as such is the logical equivalent of main() or WinMain(). 23 | /// 24 | public App() 25 | { 26 | InitializeComponent(); 27 | Suspending += OnSuspending; 28 | } 29 | 30 | /// 31 | /// Invoked when the application is launched normally by the end user. Other entry points 32 | /// will be used such as when the application is launched to open a specific file. 33 | /// 34 | /// Details about the launch request and process. 35 | protected override void OnLaunched(LaunchActivatedEventArgs e) 36 | { 37 | Frame rootFrame = Window.Current.Content as Frame; 38 | 39 | // Do not repeat app initialization when the Window already has content, 40 | // just ensure that the window is active 41 | if (rootFrame == null) 42 | { 43 | // Create a Frame to act as the navigation context and navigate to the first page 44 | rootFrame = new Frame(); 45 | 46 | rootFrame.NavigationFailed += OnNavigationFailed; 47 | 48 | if (e.PreviousExecutionState == ApplicationExecutionState.Terminated) 49 | { 50 | //TODO: Load state from previously suspended application 51 | } 52 | 53 | // Place the frame in the current Window 54 | Window.Current.Content = rootFrame; 55 | } 56 | 57 | if (e.PrelaunchActivated == false) 58 | { 59 | if (rootFrame.Content == null) 60 | { 61 | // When the navigation stack isn't restored navigate to the first page, 62 | // configuring the new page by passing required information as a navigation 63 | // parameter 64 | rootFrame.Navigate(typeof(MainPage), e.Arguments); 65 | if (rootFrame.Content is FrameworkElement page) 66 | { 67 | page.DataContext = new MainViewModel(); 68 | } 69 | } 70 | // Ensure the current window is active 71 | Window.Current.Activate(); 72 | 73 | // Extend Acrylic into TitleBar 74 | CoreApplication.GetCurrentView().TitleBar.ExtendViewIntoTitleBar = true; 75 | var titleBar = ApplicationView.GetForCurrentView().TitleBar; 76 | titleBar.ButtonBackgroundColor = Colors.Transparent; 77 | titleBar.ButtonInactiveBackgroundColor = Colors.Transparent; 78 | } 79 | } 80 | 81 | /// 82 | /// Invoked when Navigation to a certain page fails 83 | /// 84 | /// The Frame which failed navigation 85 | /// Details about the navigation failure 86 | void OnNavigationFailed(object sender, NavigationFailedEventArgs e) 87 | { 88 | throw new Exception("Failed to load Page " + e.SourcePageType.FullName); 89 | } 90 | 91 | /// 92 | /// Invoked when application execution is being suspended. Application state is saved 93 | /// without knowing whether the application will be terminated or resumed with the contents 94 | /// of memory still intact. 95 | /// 96 | /// The source of the suspend request. 97 | /// Details about the suspend request. 98 | private void OnSuspending(object sender, SuspendingEventArgs e) 99 | { 100 | var deferral = e.SuspendingOperation.GetDeferral(); 101 | //TODO: Save application state and stop any background activity 102 | deferral.Complete(); 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/ERMail.Core/Models/MailBoxConnectionInfo.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.Contracts; 3 | using System.Globalization; 4 | using Newtonsoft.Json; 5 | 6 | namespace Walterlv.ERMail.Models 7 | { 8 | /// 9 | /// Stores connection information of a specified mail box. 10 | /// This is a model that will be serialized into a file. 11 | /// 12 | public class MailBoxConnectionInfo 13 | { 14 | public string Address 15 | { 16 | get => _address; 17 | set => SetAndValidate(ref _address, value); 18 | } 19 | 20 | public string UserName 21 | { 22 | get => _userName; 23 | set => SetAndValidate(ref _userName, value); 24 | } 25 | 26 | [JsonIgnore] 27 | public string Password 28 | { 29 | get => _password; 30 | set => SetAndValidate(ref _password, value); 31 | } 32 | 33 | public string AccountName 34 | { 35 | get => _accountName; 36 | set => SetAndValidate(ref _accountName, value); 37 | } 38 | 39 | public string EnvelopeName 40 | { 41 | get => _envelopeName; 42 | set => SetAndValidate(ref _envelopeName, value); 43 | } 44 | 45 | public string IncomingServer 46 | { 47 | get => _incomingServerPort > 0 ? $"{_incomingServerHost}:{_incomingServerPort}" : _incomingServerHost; 48 | set => SetAndValidate(out _incomingServerHost, out _incomingServerPort, value); 49 | } 50 | 51 | public string OutgoingServer 52 | { 53 | get => _outgoingServerPort > 0 ? $"{_outgoingServerHost}:{_outgoingServerPort}" : _outgoingServerHost; 54 | set => SetAndValidate(out _outgoingServerHost, out _outgoingServerPort, value); 55 | } 56 | 57 | [JsonIgnore] public string IncomingServerHost => _incomingServerHost; 58 | [JsonIgnore] public int IncomingServerPort => _incomingServerPort; 59 | [JsonIgnore] public string OutgoingServerHost => _outgoingServerHost; 60 | [JsonIgnore] public int OutgoingServerPort => _outgoingServerPort; 61 | 62 | private static void SetHostPort(out string hostField, out int portField, string value) 63 | { 64 | var parts = value.Split(new[] {':'}, StringSplitOptions.RemoveEmptyEntries); 65 | if (parts.Length == 1) 66 | { 67 | hostField = parts[0].Trim(); 68 | portField = 0; 69 | } 70 | else if (parts.Length == 2 && 71 | int.TryParse(parts[1], NumberStyles.Integer, CultureInfo.InvariantCulture, out var port)) 72 | { 73 | hostField = parts[0].Trim(); 74 | portField = port; 75 | } 76 | else 77 | { 78 | hostField = null; 79 | portField = 0; 80 | } 81 | } 82 | 83 | private void SetAndValidate(out string hostField, out int portField, string value) 84 | { 85 | SetHostPort(out hostField, out portField, value); 86 | Validate(); 87 | } 88 | 89 | private void SetAndValidate(ref T field, T value) 90 | { 91 | if (Equals(field, value)) return; 92 | 93 | field = value; 94 | Validate(); 95 | } 96 | 97 | public bool Validate() 98 | { 99 | var invalid = string.IsNullOrWhiteSpace(Address) 100 | || string.IsNullOrWhiteSpace(UserName) 101 | || string.IsNullOrEmpty(Password) 102 | || string.IsNullOrWhiteSpace(AccountName) 103 | || string.IsNullOrWhiteSpace(EnvelopeName) 104 | || string.IsNullOrWhiteSpace(_incomingServerHost) 105 | || string.IsNullOrWhiteSpace(_outgoingServerHost); 106 | return !invalid; 107 | } 108 | 109 | [ContractPublicPropertyName(nameof(Address))] 110 | private string _address; 111 | 112 | [ContractPublicPropertyName(nameof(UserName))] 113 | private string _userName; 114 | 115 | [ContractPublicPropertyName(nameof(Password))] 116 | private string _password; 117 | 118 | [ContractPublicPropertyName(nameof(AccountName))] 119 | private string _accountName; 120 | 121 | [ContractPublicPropertyName(nameof(EnvelopeName))] 122 | private string _envelopeName; 123 | 124 | [ContractPublicPropertyName(nameof(IncomingServerHost))] 125 | private string _incomingServerHost; 126 | 127 | [ContractPublicPropertyName(nameof(IncomingServerPort))] 128 | private int _incomingServerPort; 129 | 130 | [ContractPublicPropertyName(nameof(OutgoingServerHost))] 131 | private string _outgoingServerHost; 132 | 133 | [ContractPublicPropertyName(nameof(OutgoingServerPort))] 134 | private int _outgoingServerPort; 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | 198 | # Since there are multiple workflows, uncomment next line to ignore bower_components 199 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 200 | #bower_components/ 201 | 202 | # RIA/Silverlight projects 203 | Generated_Code/ 204 | 205 | # Backup & report files from converting an old project file 206 | # to a newer Visual Studio version. Backup files are not needed, 207 | # because we have git ;-) 208 | _UpgradeReport_Files/ 209 | Backup*/ 210 | UpgradeLog*.XML 211 | UpgradeLog*.htm 212 | 213 | # SQL Server files 214 | *.mdf 215 | *.ldf 216 | 217 | # Business Intelligence projects 218 | *.rdl.data 219 | *.bim.layout 220 | *.bim_*.settings 221 | 222 | # Microsoft Fakes 223 | FakesAssemblies/ 224 | 225 | # GhostDoc plugin setting file 226 | *.GhostDoc.xml 227 | 228 | # Node.js Tools for Visual Studio 229 | .ntvs_analysis.dat 230 | 231 | # Visual Studio 6 build log 232 | *.plg 233 | 234 | # Visual Studio 6 workspace options file 235 | *.opt 236 | 237 | # Visual Studio LightSwitch build output 238 | **/*.HTMLClient/GeneratedArtifacts 239 | **/*.DesktopClient/GeneratedArtifacts 240 | **/*.DesktopClient/ModelManifest.xml 241 | **/*.Server/GeneratedArtifacts 242 | **/*.Server/ModelManifest.xml 243 | _Pvt_Extensions 244 | 245 | # Paket dependency manager 246 | .paket/paket.exe 247 | paket-files/ 248 | 249 | # FAKE - F# Make 250 | .fake/ 251 | 252 | # JetBrains Rider 253 | .idea/ 254 | *.sln.iml 255 | 256 | # CodeRush 257 | .cr/ 258 | 259 | # Python Tools for Visual Studio (PTVS) 260 | __pycache__/ 261 | *.pyc -------------------------------------------------------------------------------- /src/ERMail.CLI/PasswordManager.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Security.Cryptography; 4 | using Walterlv.ERMail.Utils; 5 | 6 | namespace Walterlv.ERMail 7 | { 8 | internal class PasswordManager : IPasswordManager 9 | { 10 | internal static readonly IPasswordManager Current = new PasswordManager(); 11 | 12 | private static readonly string LocalFolder = 13 | Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".ermail"); 14 | 15 | public string Retrieve(string key) 16 | { 17 | var keyFile = Path.Combine(LocalFolder, "key"); 18 | var keyBytes = ReadOrGenerateRandomKeyFile(keyFile); 19 | 20 | var passwordFile = Path.Combine(LocalFolder, key, "token"); 21 | var encrypted = File.ReadAllBytes(passwordFile); 22 | 23 | var password = DecryptStringFromBytes(encrypted, keyBytes); 24 | return password; 25 | } 26 | 27 | public void Add(string key, string password) 28 | { 29 | 30 | 31 | 32 | var keyFile = Path.Combine(LocalFolder, "key"); 33 | var keyBytes = ReadOrGenerateRandomKeyFile(keyFile); 34 | var encrypted = EncryptStringToBytes(password, keyBytes); 35 | var passwordFile = Path.Combine(LocalFolder, key, "token"); 36 | if (!Directory.Exists(Path.GetDirectoryName(passwordFile))) 37 | { 38 | Directory.CreateDirectory(Path.GetDirectoryName(passwordFile)); 39 | } 40 | File.WriteAllBytes(passwordFile, encrypted); 41 | 42 | var retrieved = Retrieve(key); 43 | } 44 | 45 | private byte[] ReadOrGenerateRandomKeyFile(string keyFile) 46 | { 47 | if (!File.Exists(keyFile)) 48 | { 49 | using (var random = new RNGCryptoServiceProvider()) 50 | { 51 | var keyBytes = new byte[16]; 52 | random.GetBytes(keyBytes); 53 | File.WriteAllBytes(keyFile, keyBytes); 54 | return keyBytes; 55 | } 56 | } 57 | 58 | return File.ReadAllBytes(keyFile); 59 | } 60 | 61 | static byte[] EncryptStringToBytes(string plainText, byte[] Key) 62 | { 63 | byte[] encrypted; 64 | byte[] iv; 65 | 66 | using (var aesAlg = Aes.Create()) 67 | { 68 | aesAlg.Key = Key; 69 | 70 | aesAlg.GenerateIV(); 71 | iv = aesAlg.IV; 72 | 73 | aesAlg.Mode = CipherMode.CBC; 74 | 75 | var encryptor = aesAlg.CreateEncryptor(aesAlg.Key, aesAlg.IV); 76 | 77 | // Create the streams used for encryption. 78 | using (var msEncrypt = new MemoryStream()) 79 | { 80 | using (var csEncrypt = new CryptoStream(msEncrypt, encryptor, CryptoStreamMode.Write)) 81 | { 82 | using (var swEncrypt = new StreamWriter(csEncrypt)) 83 | { 84 | //Write all data to the stream. 85 | swEncrypt.Write(plainText); 86 | } 87 | encrypted = msEncrypt.ToArray(); 88 | } 89 | } 90 | } 91 | 92 | var combinedIvCt = new byte[iv.Length + encrypted.Length]; 93 | Array.Copy(iv, 0, combinedIvCt, 0, iv.Length); 94 | Array.Copy(encrypted, 0, combinedIvCt, iv.Length, encrypted.Length); 95 | 96 | // Return the encrypted bytes from the memory stream. 97 | return combinedIvCt; 98 | 99 | } 100 | 101 | static string DecryptStringFromBytes(byte[] cipherTextCombined, byte[] key) 102 | { 103 | // Declare the string used to hold 104 | // the decrypted text. 105 | string plaintext = null; 106 | 107 | // Create an Aes object 108 | // with the specified key and IV. 109 | using (var aesAlg = Aes.Create()) 110 | { 111 | aesAlg.Key = key; 112 | 113 | var iv = new byte[aesAlg.BlockSize / 8]; 114 | var cipherText = new byte[cipherTextCombined.Length - iv.Length]; 115 | 116 | Array.Copy(cipherTextCombined, iv, iv.Length); 117 | Array.Copy(cipherTextCombined, iv.Length, cipherText, 0, cipherText.Length); 118 | 119 | aesAlg.IV = iv; 120 | 121 | aesAlg.Mode = CipherMode.CBC; 122 | 123 | // Create a decrytor to perform the stream transform. 124 | var decryptor = aesAlg.CreateDecryptor(aesAlg.Key, aesAlg.IV); 125 | 126 | // Create the streams used for decryption. 127 | using (var msDecrypt = new MemoryStream(cipherText)) 128 | { 129 | using (var csDecrypt = new CryptoStream(msDecrypt, decryptor, CryptoStreamMode.Read)) 130 | { 131 | using (var srDecrypt = new StreamReader(csDecrypt)) 132 | { 133 | 134 | // Read the decrypted bytes from the decrypting stream 135 | // and place them in a string. 136 | plaintext = srDecrypt.ReadToEnd(); 137 | } 138 | } 139 | } 140 | 141 | } 142 | 143 | return plaintext; 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/ERMail.CLI/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Async; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Walterlv.ERMail.Mailing; 7 | using Walterlv.ERMail.Models; 8 | using Walterlv.ERMail.Utils; 9 | 10 | namespace Walterlv.ERMail 11 | { 12 | class Program 13 | { 14 | static async Task Main(string[] args) 15 | { 16 | // Starting. 17 | Console.WriteLine("ERMail | version 0.1.0"); 18 | Console.WriteLine("Copyright (C) 2018 Walterlv. All rightes reserved."); 19 | Console.WriteLine("--------------------------------------------------"); 20 | 21 | try 22 | { 23 | await RunAsync(); 24 | return 0; 25 | } 26 | catch (Exception ex) 27 | { 28 | Console.ForegroundColor = ConsoleColor.Red; 29 | Console.WriteLine(ex); 30 | return -1; 31 | } 32 | } 33 | 34 | private static async Task RunAsync() 35 | { 36 | // Prepare the configuration folder. 37 | var localFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".ermail"); 38 | 39 | // Load or input connection info. 40 | var connectionInfo = await ReadConnectionInfo(localFolder); 41 | 42 | var cache = MailBoxCache.Get(localFolder, connectionInfo, PasswordManager.Current); 43 | 44 | // Load and select folders. 45 | var currentFolderConfigurationFile = 46 | Path.Combine(localFolder, connectionInfo.Address, "CurrentFolder.json"); 47 | var folder = await SelectFolder(currentFolderConfigurationFile, cache); 48 | 49 | // Download mails. 50 | var mails = cache.EnumerateMailDetailsAsync(folder); 51 | Console.WriteLine("Start downloading mails."); 52 | await HandleMails(mails, Path.Combine(localFolder, "Attachments", "{topic}{ext}")); 53 | Console.WriteLine("All mails are downloaded."); 54 | } 55 | 56 | private static async Task HandleMails(IAsyncEnumerable mails, string fileFormat) 57 | { 58 | await mails.ForEachAsync(async mail => 59 | { 60 | if (mail.AttachmentFileNames.Any()) 61 | { 62 | foreach (var attachment in mail.AttachmentFileNames) 63 | { 64 | var fileName = fileFormat.Replace("{topic}", mail.Topic, StringComparison.OrdinalIgnoreCase); 65 | fileName = fileName.Replace("{ext}", Path.GetExtension(attachment), StringComparison.OrdinalIgnoreCase); 66 | var directory = Path.GetDirectoryName(fileName); 67 | if (!Directory.Exists(directory)) 68 | { 69 | Directory.CreateDirectory(directory); 70 | } 71 | File.Copy(attachment, fileName, true); 72 | } 73 | Console.WriteLine($"Downloaded: [{mail.Topic}]"); 74 | Console.Write("Downloading..."); 75 | Console.CursorLeft = 0; 76 | } 77 | else 78 | { 79 | Console.Write($"No attachment found: [{string.Join("", mail.Topic.Take(20))}]"); 80 | Console.CursorLeft = 0; 81 | } 82 | }); 83 | } 84 | 85 | private static async Task ReadConnectionInfo(string localFolder) 86 | { 87 | var configurationFile = new FileSerializor( 88 | Path.Combine(localFolder, "MailBoxConfiguration.json")); 89 | var mailBoxConfiguration = await configurationFile.ReadAsync(); 90 | if (!mailBoxConfiguration.Connections.Any()) 91 | { 92 | var mailBoxCliConfiguration = new MailBoxCliConfiguration(); 93 | var info = await mailBoxCliConfiguration.Load(); 94 | mailBoxConfiguration.Connections.Add(info); 95 | PasswordManager.Current.Add(info.Address, info.Password); 96 | await configurationFile.SaveAsync(mailBoxConfiguration); 97 | } 98 | 99 | var connectionInfo = mailBoxConfiguration.Connections.First(); 100 | return connectionInfo; 101 | } 102 | 103 | private static async Task SelectFolder(string configurationFile, MailBoxCache cache) 104 | { 105 | var currentFolderFile = new FileSerializor(configurationFile); 106 | var currentFolderInfo = await currentFolderFile.ReadAsync(); 107 | 108 | var folders = await cache.LoadMailFoldersAsync(); 109 | 110 | var storedFolder = folders.FirstOrDefault(x=>x.FullName == currentFolderInfo.CurrentFolder); 111 | if (storedFolder != null) 112 | { 113 | Console.WriteLine($"Your current folder is {storedFolder.FullName}."); 114 | return storedFolder; 115 | } 116 | 117 | Console.WriteLine("Find these folders in your mailbox:"); 118 | for (var i = 0; i < folders.Count; i++) 119 | { 120 | var folder = folders[i]; 121 | Console.WriteLine($"[{(i + 1).ToString().PadLeft(2, ' ')}] {folder.FullName}"); 122 | } 123 | 124 | var selection = 0; 125 | while (selection <= 0 || selection > folders.Count) 126 | { 127 | Console.Write($"Please select the folder you want in [1~{folders.Count}]: "); 128 | int.TryParse(Console.ReadLine(), out selection); 129 | } 130 | 131 | var mailBoxFolder = folders[selection - 1]; 132 | currentFolderInfo.CurrentFolder = mailBoxFolder.FullName; 133 | await currentFolderFile.SaveAsync(currentFolderInfo); 134 | return mailBoxFolder; 135 | } 136 | } 137 | 138 | public class FolderInfo 139 | { 140 | public string CurrentFolder { get; set; } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/ERMail.Universal/Views/MainPage.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Windows.Storage; 6 | using Windows.UI; 7 | using Windows.UI.Composition; 8 | using Windows.UI.Xaml.Controls; 9 | using Windows.UI.Xaml.Navigation; 10 | using Walterlv.ERMail.Mailing; 11 | using Walterlv.ERMail.Models; 12 | using Walterlv.ERMail.Utils; 13 | using Walterlv.ERMail.ViewModels; 14 | using MUXC = Microsoft.UI.Xaml.Controls; 15 | 16 | namespace Walterlv.ERMail.Views 17 | { 18 | public sealed partial class MainPage : Page 19 | { 20 | public MainPage() 21 | { 22 | InitializeComponent(); 23 | Setup(); 24 | 25 | var localFolder = ApplicationData.Current.LocalFolder; 26 | _configurationFile = new FileSerializor( 27 | Path.Combine(localFolder.Path, "MailBoxConfiguration.json")); 28 | } 29 | 30 | private void Setup() 31 | { 32 | var compositor = new Compositor(); 33 | var spriteVisual = compositor.CreateSpriteVisual(); 34 | spriteVisual.Brush = compositor.CreateColorBrush(Color.FromArgb(0xFF, 0xFF, 0xFF, 0xFF)); 35 | } 36 | 37 | private readonly FileSerializor _configurationFile; 38 | 39 | private MainViewModel Main => (MainViewModel) DataContext; 40 | 41 | protected override async void OnNavigatedTo(NavigationEventArgs e) 42 | { 43 | base.OnNavigatedTo(e); 44 | 45 | var configuration = await _configurationFile.ReadAsync(); 46 | var storedInfo = configuration.Connections.FirstOrDefault(); 47 | storedInfo = storedInfo ?? await ConfigConnectionInfo(); 48 | var mailBox = new MailBoxViewModel 49 | { 50 | DisplayName = storedInfo.AccountName, 51 | MailAddress = storedInfo.Address, 52 | ConnectionInfo = storedInfo, 53 | }; 54 | Main.MailBoxes.Insert(0, mailBox); 55 | MailBoxComboBox.SelectedIndex = 0; 56 | Main.CurrentMailBox = mailBox; 57 | } 58 | 59 | private async Task ConfigConnectionInfo(string address = null) 60 | { 61 | var configuration = await _configurationFile.ReadAsync(); 62 | var connections = configuration.Connections; 63 | var connectionInfo = connections.FirstOrDefault(x => x.Address == address) ?? new MailBoxConnectionInfo(); 64 | if (!string.IsNullOrWhiteSpace(connectionInfo.Address)) 65 | { 66 | connectionInfo.Password = PasswordManager.Current.Retrieve(connectionInfo.Address); 67 | } 68 | 69 | var config = new ConfigMailBoxDialog(connectionInfo); 70 | var result = await config.ShowAsync(); 71 | if (result == ContentDialogResult.Secondary) 72 | { 73 | PasswordManager.Current.Add(connectionInfo.Address, connectionInfo.Password); 74 | 75 | connections.Clear(); 76 | connections.Add(connectionInfo); 77 | await _configurationFile.SaveAsync(configuration); 78 | return connectionInfo; 79 | } 80 | 81 | return null; 82 | } 83 | 84 | private MailBoxCache MailCache 85 | { 86 | get => _mailCache; 87 | set 88 | { 89 | if (Equals(_mailCache, value)) return; 90 | 91 | if (_mailCache is MailBoxCache oldValue) 92 | { 93 | // Unregister cache events. 94 | } 95 | 96 | _mailCache = value; 97 | 98 | if (value != null) 99 | { 100 | // Register cache events. 101 | } 102 | } 103 | } 104 | 105 | private async void MailBoxComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e) 106 | { 107 | var selected = e.AddedItems.FirstOrDefault(); 108 | if (selected is MailBoxViewModel vm && vm.ConnectionInfo is MailBoxConnectionInfo info) 109 | { 110 | var localFolder = ApplicationData.Current.LocalFolder.Path; 111 | MailCache = MailBoxCache.Get(localFolder, info, PasswordManager.Current); 112 | var folders = await MailCache.LoadMailFoldersAsync(); 113 | Main.CurrentMailBox.Folders.Clear(); 114 | foreach (var folder in folders) 115 | { 116 | Main.CurrentMailBox.Folders.Add(folder); 117 | } 118 | 119 | var inbox = Main.CurrentMailBox.Folders.FirstOrDefault(x => x.FullName == "INBOX"); 120 | MainFolderListView.SelectedItem = inbox; 121 | } 122 | } 123 | 124 | private async void MainFolderListView_SelectionChanged(object sender, SelectionChangedEventArgs e) 125 | { 126 | if (Main.CurrentMailBox.ConnectionInfo is null) return; 127 | if (!(e.AddedItems.FirstOrDefault() is MailBoxFolderViewModel vm)) return; 128 | 129 | Main.CurrentMailBox.CurrentFolder = vm; 130 | var summaries = await MailCache.LoadMailsAsync(vm); 131 | vm.Mails.Clear(); 132 | foreach (var summary in summaries) 133 | { 134 | vm.Mails.Add(summary); 135 | } 136 | } 137 | 138 | private async void MailGroupListView_SelectionChanged(object sender, SelectionChangedEventArgs e) 139 | { 140 | WebView.Navigate(new Uri("about:blank")); 141 | if (Main.CurrentMailBox.ConnectionInfo is null) return; 142 | if (!(e.AddedItems.FirstOrDefault() is MailGroupViewModel vm)) return; 143 | 144 | var mailCache = await MailCache.LoadMailAsync(Main.CurrentMailBox.CurrentFolder, vm.MailIds.First()); 145 | var file = await StorageFile.GetFileFromPathAsync(mailCache.HtmlFileName); 146 | var text = await FileIO.ReadTextAsync(file); 147 | WebView.NavigateToString(text); 148 | } 149 | 150 | private MailBoxCache _mailCache; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/ERMail.Universal/Views/OldMainPage.xaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 98 | 99 | 100 | 105 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /src/AssembleMailing.Universal/Views/MainPage.xaml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | 13 | 14 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 98 | 99 | 100 | 105 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | -------------------------------------------------------------------------------- /src/ERMail.Core/OAuth/Scope.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using JetBrains.Annotations; 6 | 7 | namespace Walterlv.ERMail.OAuth 8 | { 9 | /// 10 | /// Define some types of permissions that OAuth called Scope. 11 | /// 12 | public class Scope : IEquatable, IEnumerable 13 | { 14 | /// 15 | /// Indicate that this is an invalid scope. 16 | /// 17 | public static Scope Invalid { get; } = new Scope(); 18 | 19 | /// 20 | /// Gets a value that indicates whether this scope is invalid. 21 | /// 22 | public bool IsInvalid => !_scopes.Any(); 23 | 24 | /// 25 | /// Initialize a new invalid instance of . 26 | /// 27 | private Scope() 28 | { 29 | } 30 | 31 | private Scope([NotNull] IEnumerable scopes) 32 | { 33 | if (scopes == null) throw new ArgumentNullException(nameof(scopes)); 34 | _scopes.AddRange(scopes.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct()); 35 | } 36 | 37 | /// 38 | /// Initialize a new instance of with some specified scope types. 39 | /// 40 | /// 41 | /// 42 | public Scope([NotNull] string scope, params string[] otherScopes) 43 | : this(scope, otherScopes ?? Enumerable.Empty()) 44 | { 45 | } 46 | 47 | /// 48 | /// Initialize a new instance of with some specified scope types. 49 | /// 50 | /// 51 | /// 52 | private Scope([NotNull] string scope, [NotNull] IEnumerable otherScopes) 53 | { 54 | if (scope == null) throw new ArgumentNullException(nameof(scope)); 55 | if (otherScopes == null) throw new ArgumentNullException(nameof(otherScopes)); 56 | 57 | var parts = scope.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries); 58 | if (parts.Length <= 0) 59 | { 60 | throw new ArgumentException("Specified scope string doesnot contains any scope values.", nameof(scope)); 61 | } 62 | 63 | _scopes.AddRange(parts.Distinct()); 64 | _scopes.AddRange(otherScopes 65 | .Where(x => !string.IsNullOrWhiteSpace(x)) 66 | .SelectMany(x => x.Split(new[] {' '}, StringSplitOptions.RemoveEmptyEntries)) 67 | .Distinct()); 68 | } 69 | 70 | /// 71 | /// Initialize a new instance of that merge two scope collections. 72 | /// 73 | /// 74 | /// 75 | private Scope(IEnumerable scopes, IEnumerable otherScopes) 76 | { 77 | _scopes.AddRange(scopes.Where(x => !string.IsNullOrWhiteSpace(x)).Distinct()); 78 | foreach (var scope in otherScopes.Where(x => !string.IsNullOrWhiteSpace(x)) 79 | .Where(x => !_scopes.Contains(x))) 80 | { 81 | _scopes.Add(scope); 82 | } 83 | } 84 | 85 | private readonly List _scopes = new List(); 86 | 87 | /// 88 | public bool Equals(Scope other) 89 | { 90 | if (other is null) return false; 91 | if (ReferenceEquals(this, other)) return true; 92 | if (_scopes.Count != other._scopes.Count) return false; 93 | if (_scopes.Except(other._scopes).Any()) return false; 94 | return true; 95 | } 96 | 97 | public IEnumerator GetEnumerator() 98 | { 99 | foreach (var scope in _scopes) 100 | { 101 | yield return scope; 102 | } 103 | } 104 | 105 | /// 106 | public override bool Equals(object obj) 107 | { 108 | if (obj is null) return false; 109 | if (ReferenceEquals(this, obj)) return true; 110 | if (obj.GetType() != GetType()) return false; 111 | return Equals((Scope) obj); 112 | } 113 | 114 | /// 115 | public override int GetHashCode() 116 | { 117 | var result = 0; 118 | 119 | foreach (var scope in _scopes) 120 | { 121 | result ^= scope.GetHashCode(); 122 | } 123 | 124 | return result; 125 | } 126 | 127 | public override string ToString() 128 | { 129 | return string.Join(" ", _scopes.OrderBy(x => x)); 130 | } 131 | 132 | IEnumerator IEnumerable.GetEnumerator() 133 | { 134 | return GetEnumerator(); 135 | } 136 | 137 | public static bool operator ==(Scope scope1, Scope scope2) 138 | { 139 | return Equals(scope1, scope2); 140 | } 141 | 142 | public static bool operator !=(Scope scope1, Scope scope2) 143 | { 144 | return !Equals(scope1, scope2); 145 | } 146 | 147 | /// 148 | /// Merge two scopes and returns a new scope with all their scopes. 149 | /// 150 | public static Scope operator |(Scope scope1, Scope scope2) 151 | { 152 | return new Scope(scope1._scopes, scope2._scopes); 153 | } 154 | 155 | /// 156 | /// Merge two scopes and returns a new scope with all their scopes. 157 | /// 158 | public static Scope operator |(string scope1, Scope scope2) 159 | { 160 | return new Scope(scope1, scope2._scopes); 161 | } 162 | 163 | /// 164 | /// Merge two scopes and returns a new scope with all their scopes. 165 | /// 166 | public static Scope operator |(Scope scope1, string scope2) 167 | { 168 | return new Scope(scope2, scope1._scopes); 169 | } 170 | 171 | /// 172 | /// Findout the common scopes between two scopes and returns a new to indicate it. 173 | /// If all their permissions are different, it will returns an invalid one. 174 | /// 175 | public static Scope operator &(Scope scope1, Scope scope2) 176 | { 177 | return new Scope(scope1._scopes.Intersect(scope2._scopes)); 178 | } 179 | 180 | /// 181 | /// Findout the common scopes between two scopes and returns a new to indicate it. 182 | /// If all their permissions are different, it will returns an invalid one. 183 | /// 184 | public static Scope operator &(string scope1, Scope scope2) 185 | { 186 | if (scope2._scopes.Contains(scope1)) 187 | { 188 | return new Scope(scope1); 189 | } 190 | 191 | return Invalid; 192 | } 193 | 194 | /// 195 | /// Findout the common scopes between two scopes and returns a new to indicate it. 196 | /// If all their permissions are different, it will returns an invalid one. 197 | /// 198 | public static Scope operator &(Scope scope1, string scope2) 199 | { 200 | if (scope1._scopes.Contains(scope2)) 201 | { 202 | return new Scope(scope2); 203 | } 204 | 205 | return Invalid; 206 | } 207 | 208 | public static implicit operator Scope(string scope) 209 | { 210 | return new Scope(scope); 211 | } 212 | 213 | public static implicit operator Scope(string[] scope) 214 | { 215 | return new Scope(scope); 216 | } 217 | 218 | public static implicit operator string[](Scope scope) 219 | { 220 | return scope._scopes.ToArray(); 221 | } 222 | } 223 | } 224 | -------------------------------------------------------------------------------- /ERMail.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27428.2037 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ERMail.Desktop", "src\ERMail.Desktop\ERMail.Desktop.csproj", "{DA212E0A-06A6-4CA0-995C-010F3D2CC4BF}" 7 | EndProject 8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ERMail.Universal", "src\ERMail.Universal\ERMail.Universal.csproj", "{4D1DA296-843D-41FD-B2A1-8EA4FACF2254}" 9 | EndProject 10 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{FF18DB4D-A38B-4A55-9549-7E084A48FF2B}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1472BF6A-97F5-47ED-A987-F4CF07A61AA9}" 13 | ProjectSection(SolutionItems) = preProject 14 | LICENSE = LICENSE 15 | EndProjectSection 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ERMail.Core", "src\ERMail.Core\ERMail.Core.csproj", "{A410373E-F9B5-4129-8F8E-97C2FCD3870D}" 18 | EndProject 19 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "README", "README", "{1DEFDD2D-23F3-4121-860E-4347A7EF5E7F}" 20 | ProjectSection(SolutionItems) = preProject 21 | README.jp.md = README.jp.md 22 | README.md = README.md 23 | README.zh-chs.md = README.zh-chs.md 24 | README.zh-cht.md = README.zh-cht.md 25 | EndProjectSection 26 | EndProject 27 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Git", "Git", "{C9D0FAE1-9DA2-4EE5-A308-9A0911DF71C4}" 28 | ProjectSection(SolutionItems) = preProject 29 | .gitattributes = .gitattributes 30 | .gitignore = .gitignore 31 | GitVersion.config = GitVersion.config 32 | EndProjectSection 33 | EndProject 34 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ERMail.CLI", "src\ERMail.CLI\ERMail.CLI.csproj", "{A4CDAA86-D2A4-44C9-AD41-18F9B9758282}" 35 | EndProject 36 | Global 37 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 38 | Debug|Any CPU = Debug|Any CPU 39 | Debug|ARM = Debug|ARM 40 | Debug|x64 = Debug|x64 41 | Debug|x86 = Debug|x86 42 | Release|Any CPU = Release|Any CPU 43 | Release|ARM = Release|ARM 44 | Release|x64 = Release|x64 45 | Release|x86 = Release|x86 46 | EndGlobalSection 47 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 48 | {DA212E0A-06A6-4CA0-995C-010F3D2CC4BF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {DA212E0A-06A6-4CA0-995C-010F3D2CC4BF}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {DA212E0A-06A6-4CA0-995C-010F3D2CC4BF}.Debug|ARM.ActiveCfg = Debug|Any CPU 51 | {DA212E0A-06A6-4CA0-995C-010F3D2CC4BF}.Debug|ARM.Build.0 = Debug|Any CPU 52 | {DA212E0A-06A6-4CA0-995C-010F3D2CC4BF}.Debug|x64.ActiveCfg = Debug|Any CPU 53 | {DA212E0A-06A6-4CA0-995C-010F3D2CC4BF}.Debug|x64.Build.0 = Debug|Any CPU 54 | {DA212E0A-06A6-4CA0-995C-010F3D2CC4BF}.Debug|x86.ActiveCfg = Debug|Any CPU 55 | {DA212E0A-06A6-4CA0-995C-010F3D2CC4BF}.Debug|x86.Build.0 = Debug|Any CPU 56 | {DA212E0A-06A6-4CA0-995C-010F3D2CC4BF}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {DA212E0A-06A6-4CA0-995C-010F3D2CC4BF}.Release|Any CPU.Build.0 = Release|Any CPU 58 | {DA212E0A-06A6-4CA0-995C-010F3D2CC4BF}.Release|ARM.ActiveCfg = Release|Any CPU 59 | {DA212E0A-06A6-4CA0-995C-010F3D2CC4BF}.Release|ARM.Build.0 = Release|Any CPU 60 | {DA212E0A-06A6-4CA0-995C-010F3D2CC4BF}.Release|x64.ActiveCfg = Release|Any CPU 61 | {DA212E0A-06A6-4CA0-995C-010F3D2CC4BF}.Release|x64.Build.0 = Release|Any CPU 62 | {DA212E0A-06A6-4CA0-995C-010F3D2CC4BF}.Release|x86.ActiveCfg = Release|Any CPU 63 | {DA212E0A-06A6-4CA0-995C-010F3D2CC4BF}.Release|x86.Build.0 = Release|Any CPU 64 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Debug|Any CPU.ActiveCfg = Debug|x86 65 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Debug|Any CPU.Build.0 = Debug|x86 66 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Debug|Any CPU.Deploy.0 = Debug|x86 67 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Debug|ARM.ActiveCfg = Debug|ARM 68 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Debug|ARM.Build.0 = Debug|ARM 69 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Debug|ARM.Deploy.0 = Debug|ARM 70 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Debug|x64.ActiveCfg = Debug|x64 71 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Debug|x64.Build.0 = Debug|x64 72 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Debug|x64.Deploy.0 = Debug|x64 73 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Debug|x86.ActiveCfg = Debug|x86 74 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Debug|x86.Build.0 = Debug|x86 75 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Debug|x86.Deploy.0 = Debug|x86 76 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Release|Any CPU.ActiveCfg = Release|x86 77 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Release|ARM.ActiveCfg = Release|ARM 78 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Release|ARM.Build.0 = Release|ARM 79 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Release|ARM.Deploy.0 = Release|ARM 80 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Release|x64.ActiveCfg = Release|x64 81 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Release|x64.Build.0 = Release|x64 82 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Release|x64.Deploy.0 = Release|x64 83 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Release|x86.ActiveCfg = Release|x86 84 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Release|x86.Build.0 = Release|x86 85 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254}.Release|x86.Deploy.0 = Release|x86 86 | {A410373E-F9B5-4129-8F8E-97C2FCD3870D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 87 | {A410373E-F9B5-4129-8F8E-97C2FCD3870D}.Debug|Any CPU.Build.0 = Debug|Any CPU 88 | {A410373E-F9B5-4129-8F8E-97C2FCD3870D}.Debug|ARM.ActiveCfg = Debug|Any CPU 89 | {A410373E-F9B5-4129-8F8E-97C2FCD3870D}.Debug|ARM.Build.0 = Debug|Any CPU 90 | {A410373E-F9B5-4129-8F8E-97C2FCD3870D}.Debug|x64.ActiveCfg = Debug|Any CPU 91 | {A410373E-F9B5-4129-8F8E-97C2FCD3870D}.Debug|x64.Build.0 = Debug|Any CPU 92 | {A410373E-F9B5-4129-8F8E-97C2FCD3870D}.Debug|x86.ActiveCfg = Debug|Any CPU 93 | {A410373E-F9B5-4129-8F8E-97C2FCD3870D}.Debug|x86.Build.0 = Debug|Any CPU 94 | {A410373E-F9B5-4129-8F8E-97C2FCD3870D}.Release|Any CPU.ActiveCfg = Release|Any CPU 95 | {A410373E-F9B5-4129-8F8E-97C2FCD3870D}.Release|Any CPU.Build.0 = Release|Any CPU 96 | {A410373E-F9B5-4129-8F8E-97C2FCD3870D}.Release|ARM.ActiveCfg = Release|Any CPU 97 | {A410373E-F9B5-4129-8F8E-97C2FCD3870D}.Release|ARM.Build.0 = Release|Any CPU 98 | {A410373E-F9B5-4129-8F8E-97C2FCD3870D}.Release|x64.ActiveCfg = Release|Any CPU 99 | {A410373E-F9B5-4129-8F8E-97C2FCD3870D}.Release|x64.Build.0 = Release|Any CPU 100 | {A410373E-F9B5-4129-8F8E-97C2FCD3870D}.Release|x86.ActiveCfg = Release|Any CPU 101 | {A410373E-F9B5-4129-8F8E-97C2FCD3870D}.Release|x86.Build.0 = Release|Any CPU 102 | {A4CDAA86-D2A4-44C9-AD41-18F9B9758282}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 103 | {A4CDAA86-D2A4-44C9-AD41-18F9B9758282}.Debug|Any CPU.Build.0 = Debug|Any CPU 104 | {A4CDAA86-D2A4-44C9-AD41-18F9B9758282}.Debug|ARM.ActiveCfg = Debug|Any CPU 105 | {A4CDAA86-D2A4-44C9-AD41-18F9B9758282}.Debug|ARM.Build.0 = Debug|Any CPU 106 | {A4CDAA86-D2A4-44C9-AD41-18F9B9758282}.Debug|x64.ActiveCfg = Debug|Any CPU 107 | {A4CDAA86-D2A4-44C9-AD41-18F9B9758282}.Debug|x64.Build.0 = Debug|Any CPU 108 | {A4CDAA86-D2A4-44C9-AD41-18F9B9758282}.Debug|x86.ActiveCfg = Debug|Any CPU 109 | {A4CDAA86-D2A4-44C9-AD41-18F9B9758282}.Debug|x86.Build.0 = Debug|Any CPU 110 | {A4CDAA86-D2A4-44C9-AD41-18F9B9758282}.Release|Any CPU.ActiveCfg = Release|Any CPU 111 | {A4CDAA86-D2A4-44C9-AD41-18F9B9758282}.Release|Any CPU.Build.0 = Release|Any CPU 112 | {A4CDAA86-D2A4-44C9-AD41-18F9B9758282}.Release|ARM.ActiveCfg = Release|Any CPU 113 | {A4CDAA86-D2A4-44C9-AD41-18F9B9758282}.Release|ARM.Build.0 = Release|Any CPU 114 | {A4CDAA86-D2A4-44C9-AD41-18F9B9758282}.Release|x64.ActiveCfg = Release|Any CPU 115 | {A4CDAA86-D2A4-44C9-AD41-18F9B9758282}.Release|x64.Build.0 = Release|Any CPU 116 | {A4CDAA86-D2A4-44C9-AD41-18F9B9758282}.Release|x86.ActiveCfg = Release|Any CPU 117 | {A4CDAA86-D2A4-44C9-AD41-18F9B9758282}.Release|x86.Build.0 = Release|Any CPU 118 | EndGlobalSection 119 | GlobalSection(SolutionProperties) = preSolution 120 | HideSolutionNode = FALSE 121 | EndGlobalSection 122 | GlobalSection(NestedProjects) = preSolution 123 | {DA212E0A-06A6-4CA0-995C-010F3D2CC4BF} = {FF18DB4D-A38B-4A55-9549-7E084A48FF2B} 124 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254} = {FF18DB4D-A38B-4A55-9549-7E084A48FF2B} 125 | {A410373E-F9B5-4129-8F8E-97C2FCD3870D} = {FF18DB4D-A38B-4A55-9549-7E084A48FF2B} 126 | {1DEFDD2D-23F3-4121-860E-4347A7EF5E7F} = {1472BF6A-97F5-47ED-A987-F4CF07A61AA9} 127 | {C9D0FAE1-9DA2-4EE5-A308-9A0911DF71C4} = {1472BF6A-97F5-47ED-A987-F4CF07A61AA9} 128 | {A4CDAA86-D2A4-44C9-AD41-18F9B9758282} = {FF18DB4D-A38B-4A55-9549-7E084A48FF2B} 129 | EndGlobalSection 130 | GlobalSection(ExtensibilityGlobals) = postSolution 131 | SolutionGuid = {F5E30582-D01A-42FB-BEC3-5B930DDA8E13} 132 | EndGlobalSection 133 | EndGlobal 134 | -------------------------------------------------------------------------------- /src/ERMail.Universal/Views/MainPage.xaml: -------------------------------------------------------------------------------- 1 | 12 | 13 | 16 | 19 | 20 | 21 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 37 | 42 | 43 | 45 | 46 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 113 | 116 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 135 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /src/ERMail.Universal/ERMail.Universal.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Debug 6 | x86 7 | {4D1DA296-843D-41FD-B2A1-8EA4FACF2254} 8 | AppContainerExe 9 | Properties 10 | Walterlv.ERMail 11 | ERMail 12 | en-US 13 | UAP 14 | 10.0.17134.0 15 | 10.0.16299.0 16 | 14 17 | 512 18 | {A5A43C5B-DE2A-4C0C-9213-0A381AF9435A};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 19 | true 20 | ERMail.Universal_StoreKey.pfx 21 | AF58E65184F2D3151130111CC5B9531FD6F79046 22 | 23 | 24 | true 25 | bin\x86\Debug\ 26 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP 27 | ;2008 28 | full 29 | x86 30 | false 31 | prompt 32 | true 33 | latest 34 | 35 | 36 | bin\x86\Release\ 37 | TRACE;NETFX_CORE;WINDOWS_UWP 38 | true 39 | ;2008 40 | pdbonly 41 | x86 42 | false 43 | prompt 44 | true 45 | true 46 | 47 | 48 | true 49 | bin\ARM\Debug\ 50 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP 51 | ;2008 52 | full 53 | ARM 54 | false 55 | prompt 56 | true 57 | 58 | 59 | bin\ARM\Release\ 60 | TRACE;NETFX_CORE;WINDOWS_UWP 61 | true 62 | ;2008 63 | pdbonly 64 | ARM 65 | false 66 | prompt 67 | true 68 | true 69 | 70 | 71 | true 72 | bin\x64\Debug\ 73 | DEBUG;TRACE;NETFX_CORE;WINDOWS_UWP 74 | ;2008 75 | full 76 | x64 77 | false 78 | prompt 79 | true 80 | 81 | 82 | bin\x64\Release\ 83 | TRACE;NETFX_CORE;WINDOWS_UWP 84 | true 85 | ;2008 86 | pdbonly 87 | x64 88 | false 89 | prompt 90 | true 91 | true 92 | 93 | 94 | PackageReference 95 | 96 | 97 | 98 | App.xaml 99 | 100 | 101 | 102 | ClassificationPage.xaml 103 | 104 | 105 | ConfigMailBoxDialog.xaml 106 | 107 | 108 | MailPage.xaml 109 | 110 | 111 | MainPage.xaml 112 | 113 | 114 | OldMainPage.xaml 115 | 116 | 117 | 118 | 119 | 120 | Designer 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | MSBuild:Compile 137 | Designer 138 | 139 | 140 | MSBuild:Compile 141 | Designer 142 | 143 | 144 | MSBuild:Compile 145 | Designer 146 | 147 | 148 | MSBuild:Compile 149 | Designer 150 | 151 | 152 | MSBuild:Compile 153 | Designer 154 | 155 | 156 | MSBuild:Compile 157 | Designer 158 | 159 | 160 | 161 | 162 | 6.0.8 163 | 164 | 165 | 3.0.0 166 | 167 | 168 | 3.0.0 169 | 170 | 171 | 2.0.180717002-prerelease 172 | 173 | 174 | 8.3.1 175 | 176 | 177 | 178 | 179 | {a410373e-f9b5-4129-8f8e-97c2fcd3870d} 180 | ERMail.Core 181 | 182 | 183 | 184 | 14.0 185 | 186 | 187 | 194 | -------------------------------------------------------------------------------- /src/ERMail.Universal/Views/MailPage.xaml: -------------------------------------------------------------------------------- 1 | 11 | 12 | 13 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 112 | 113 | 116 | 117 | 118 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 137 | 140 | 143 | 144 | 145 | 146 | 147 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | -------------------------------------------------------------------------------- /src/ERMail.Universal/App.xaml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 80 | 160 | 161 | 162 | 163 | 164 | -------------------------------------------------------------------------------- /src/ERMail.Core/Mailing/MailBoxCache.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Async; 3 | using System.Collections.Concurrent; 4 | using System.Collections.Generic; 5 | using System.Diagnostics; 6 | using System.IO; 7 | using System.Linq; 8 | using System.Threading.Tasks; 9 | using MailKit; 10 | using MimeKit; 11 | using Walterlv.ERMail.Models; 12 | using Walterlv.ERMail.Utils; 13 | 14 | namespace Walterlv.ERMail.Mailing 15 | { 16 | /// 17 | /// Manage the cache of all mail data. 18 | /// It also fetch data from the mail server, so that it can determine when to update the cache. 19 | /// 20 | public class MailBoxCache 21 | { 22 | /// 23 | /// Stores all cache manager indexed by mail address. 24 | /// 25 | private static readonly ConcurrentDictionary AllCache 26 | = new ConcurrentDictionary(); 27 | 28 | /// 29 | /// Gets a instance that is auto managed. 30 | /// Returns value will never be null. 31 | /// 32 | /// 33 | /// 34 | /// 35 | /// 36 | public static MailBoxCache Get(string directory, MailBoxConnectionInfo info, IPasswordManager passwordManager) 37 | { 38 | if (!AllCache.TryGetValue(info.Address, out var cache)) 39 | { 40 | cache = new MailBoxCache(Path.Combine(directory, info.Address), info, passwordManager); 41 | AllCache[info.Address] = cache; 42 | } 43 | 44 | return cache; 45 | } 46 | 47 | /// 48 | /// Initialize a new instance of . 49 | /// 50 | /// 51 | /// 52 | /// 53 | public MailBoxCache(string directory, MailBoxConnectionInfo info, IPasswordManager passwordManager) 54 | { 55 | Directory = directory; 56 | _connectionInfo = info; 57 | _passwordManager = passwordManager; 58 | } 59 | 60 | /// 61 | /// Gets the directory where this cache manager should store files in. 62 | /// If it is auto managed, the directory is named like user@example.com. 63 | /// 64 | public string Directory { get; } 65 | 66 | /// 67 | /// Load all folders of current mail cache. 68 | /// If any cache exists, it will return the cache and then **TODO** fetch them from the mail server later. 69 | /// If the cache does not exists, it will **TODO** return an empty result and then fetch all from the mail server. 70 | /// 71 | /// 72 | public async Task> LoadMailFoldersAsync() 73 | { 74 | var folderCache = new FileSerializor>(Path.Combine(Directory, "folders.json")); 75 | var cachedFolder = await folderCache.ReadAsync(); 76 | if (cachedFolder.Any()) 77 | { 78 | return cachedFolder; 79 | } 80 | 81 | FillPassword(_connectionInfo); 82 | var result = new List(); 83 | using (var client = await new IncomingMailClient(_connectionInfo).ConnectAsync()) 84 | { 85 | var inbox = client.Inbox; 86 | inbox.Open(FolderAccess.ReadOnly); 87 | 88 | var folders = await client.GetFoldersAsync(client.PersonalNamespaces[0]); 89 | foreach (var folder in new[] {inbox}.Union(folders)) 90 | { 91 | result.Add(new MailBoxFolder 92 | { 93 | Name = folder.Name, 94 | FullName = folder.FullName, 95 | }); 96 | } 97 | } 98 | 99 | await folderCache.SaveAsync(result); 100 | 101 | return result; 102 | } 103 | 104 | /// 105 | /// Load a range of mail summaries from a specified mail folder cache. 106 | /// If any cache exists, it will return the cache and then **TODO** fetch them from the mail server later. 107 | /// If the cache does not exists, it will **TODO** return an empty result and then fetch all from the mail server. 108 | /// 109 | /// 110 | /// 111 | /// 112 | /// 113 | public async Task> LoadMailsAsync(MailBoxFolder folder, int start = 0, int length = 20) 114 | { 115 | var cache = new FileSerializor>( 116 | Path.Combine(Directory, "Folders", folder.FullName, "summaries.json")); 117 | if (start == 0) 118 | { 119 | // Temporarily load cache only for first 100. 120 | var cachedSummary = await cache.ReadAsync(); 121 | if (cachedSummary.Any()) 122 | { 123 | return cachedSummary; 124 | } 125 | } 126 | 127 | FillPassword(_connectionInfo); 128 | var result = new List(); 129 | using (var client = await new IncomingMailClient(_connectionInfo).ConnectAsync()) 130 | { 131 | var mailFolder = await client.GetFolderAsync(folder.FullName); 132 | mailFolder.Open(FolderAccess.ReadOnly); 133 | 134 | var fetchingCount = mailFolder.Count < start + length ? mailFolder.Count : start + length; 135 | if (fetchingCount > 0) 136 | { 137 | var messageSummaries = await mailFolder.FetchAsync(mailFolder.Count - fetchingCount, 138 | mailFolder.Count - 1 - start, 139 | MessageSummaryItems.UniqueId | MessageSummaryItems.Full); 140 | foreach (var summary in messageSummaries.Reverse()) 141 | { 142 | TextPart body; 143 | try 144 | { 145 | body = (TextPart) await mailFolder.GetBodyPartAsync(summary.UniqueId, summary.TextBody); 146 | } 147 | catch (Exception ex) 148 | { 149 | // Temporarily catch all exceptions, and it will be handled correctly after main project is about to finish. 150 | body = null; 151 | } 152 | 153 | var mailGroup = new MailSummary 154 | { 155 | Title = summary.Envelope.From.Select(x => x.Name).FirstOrDefault() ?? "(Anonymous)", 156 | Topic = summary.Envelope.Subject, 157 | Excerpt = body?.Text?.Replace(Environment.NewLine, " "), 158 | MailIds = new List {summary.UniqueId.Id}, 159 | }; 160 | result.Add(mailGroup); 161 | } 162 | } 163 | } 164 | 165 | //await cache.SaveAsync(result); 166 | 167 | return result; 168 | } 169 | 170 | /// 171 | /// Load a mail content from a specified mail folder cache. 172 | /// If any cache exists, it will return the cache. 173 | /// But if it does not exists, it will fetch one from the mail server (and then cache it). 174 | /// 175 | /// 176 | /// 177 | /// 178 | public async Task LoadMailAsync(MailBoxFolder folder, uint id) 179 | { 180 | var htmlFileName = Path.Combine(Directory, "Mails", $"{id}.html"); 181 | var contentfileName = Path.Combine(Directory, "Mails", $"{id}.json"); 182 | var cache = new FileSerializor(contentfileName); 183 | 184 | var contentCache = await cache.ReadAsync(); 185 | if (contentCache.Content != null && File.Exists(contentCache.HtmlFileName)) 186 | { 187 | return contentCache; 188 | } 189 | 190 | 191 | FillPassword(_connectionInfo); 192 | using (var client = await new IncomingMailClient(_connectionInfo).ConnectAsync()) 193 | { 194 | var mailFolder = await client.GetFolderAsync(folder.FullName); 195 | mailFolder.Open(FolderAccess.ReadOnly); 196 | 197 | var message = await mailFolder.GetMessageAsync(new UniqueId(id)); 198 | var htmlBody = message.HtmlBody; 199 | 200 | var content = new MailContentCache 201 | { 202 | Topic = message.Subject, 203 | Content = message.TextBody ?? htmlBody, 204 | HtmlFileName = htmlFileName, 205 | }; 206 | await cache.SaveAsync(content); 207 | File.WriteAllText(htmlFileName, htmlBody); 208 | 209 | return content; 210 | } 211 | } 212 | 213 | /// 214 | /// Enumerate all mail contents asynchronously from the specified folder via the cache strategy. 215 | /// 216 | /// 217 | /// 218 | public IAsyncEnumerable EnumerateMailsAsync(MailBoxFolder folder) 219 | { 220 | return new AsyncEnumerable(async yield => 221 | { 222 | try 223 | { 224 | var startIndex = 0; 225 | while (true) 226 | { 227 | var summaries = await LoadMailsAsync(folder, startIndex, startIndex + 20); 228 | var finished = true; 229 | foreach (var summary in summaries) 230 | { 231 | finished = false; 232 | var contentCache = await LoadMailAsync(folder, summary.MailIds.First()); 233 | await yield.ReturnAsync(contentCache); 234 | } 235 | 236 | startIndex += 20; 237 | 238 | if (finished) 239 | { 240 | break; 241 | } 242 | } 243 | yield.Break(); 244 | } 245 | catch (Exception ex) 246 | { 247 | yield.Break(); 248 | if (Debugger.IsAttached) 249 | { 250 | Debugger.Break(); 251 | } 252 | } 253 | }); 254 | } 255 | 256 | 257 | public async Task DownloadMailAsync(MailBoxFolder folder, uint id) 258 | { 259 | var contentfileName = Path.Combine(Directory, "Mails", $"{id}.json"); 260 | var htmlFileName = Path.Combine(Directory, "Mails", $"{id}.html"); 261 | var attachmentDirectory = Path.Combine(Directory, "Mails", $"{id}.attachments"); 262 | var cache = new FileSerializor(contentfileName); 263 | var contentCache = await cache.ReadAsync(); 264 | if (contentCache.Content != null && File.Exists(contentCache.HtmlFileName)) 265 | { 266 | return contentCache; 267 | } 268 | 269 | FillPassword(_connectionInfo); 270 | using (var client = await new IncomingMailClient(_connectionInfo).ConnectAsync()) 271 | { 272 | var mailFolder = await client.GetFolderAsync(folder.FullName); 273 | mailFolder.Open(FolderAccess.ReadOnly); 274 | 275 | var message = await mailFolder.GetMessageAsync(new UniqueId(id)); 276 | var htmlBody = message.HtmlBody; 277 | var attachments = message.Attachments.Select(x => (x.ContentDisposition.FileName, x)); 278 | var attachmentFiles = new List(); 279 | foreach (var (fileName, attachment) in attachments) 280 | { 281 | if (!System.IO.Directory.Exists(attachmentDirectory)) 282 | { 283 | System.IO.Directory.CreateDirectory(attachmentDirectory); 284 | } 285 | var tempFileName = $"{fileName}.downloading"; 286 | var tempFile = Path.Combine(attachmentDirectory, tempFileName); 287 | var file = Path.Combine(attachmentDirectory, fileName); 288 | await attachment.WriteToAsync(tempFile); 289 | 290 | File.Move(tempFile, file); 291 | attachmentFiles.Add(file); 292 | } 293 | 294 | var content = new MailContentCache 295 | { 296 | Topic = message.Subject, 297 | Content = message.TextBody ?? htmlBody, 298 | HtmlFileName = htmlFileName, 299 | AttachmentFileNames = attachmentFiles, 300 | }; 301 | await cache.SaveAsync(content); 302 | File.WriteAllText(htmlFileName, htmlBody); 303 | 304 | return content; 305 | } 306 | } 307 | 308 | public IAsyncEnumerable EnumerateMailDetailsAsync(MailBoxFolder folder) 309 | { 310 | return new AsyncEnumerable(async yield => 311 | { 312 | try 313 | { 314 | var startIndex = 0; 315 | while (true) 316 | { 317 | var summaries = await LoadMailsAsync(folder, startIndex, startIndex + 20); 318 | var finished = true; 319 | foreach (var summary in summaries) 320 | { 321 | finished = false; 322 | var contentCache = await DownloadMailAsync(folder, summary.MailIds.First()); 323 | await yield.ReturnAsync(contentCache); 324 | } 325 | 326 | startIndex += 20; 327 | 328 | if (finished) 329 | { 330 | break; 331 | } 332 | } 333 | yield.Break(); 334 | } 335 | catch (Exception ex) 336 | { 337 | yield.Break(); 338 | if (Debugger.IsAttached) 339 | { 340 | Debugger.Break(); 341 | } 342 | } 343 | }); 344 | } 345 | 346 | private void FillPassword(MailBoxConnectionInfo info) 347 | { 348 | if (!string.IsNullOrWhiteSpace(info.Address) && string.IsNullOrWhiteSpace(info.Password)) 349 | { 350 | info.Password = _passwordManager.Retrieve(info.Address); 351 | } 352 | } 353 | 354 | private readonly IPasswordManager _passwordManager; 355 | private readonly MailBoxConnectionInfo _connectionInfo; 356 | } 357 | } 358 | --------------------------------------------------------------------------------