使用 C# 构建自定义电子邮件客户端(WPF)






4.58/5 (6投票s)
在这篇文章中,我将讨论使用 WPF 框架和 ImapX 库构建 IMAP 客户端。
引言和背景
昨天,我还在自言自语,想写一篇关于 C# 编程的文章;我承认我现在已经沉迷于编程了。所以,我想写一篇博客文章,我发现我有一个“待写列表”上的主题。不仅仅是我写了那个主题,我还添加了一些额外的编程来支持另一项功能。所以,基本上我待写列表上的内容是用 C# 编程语言编写一个电子邮件客户端。在这篇博客文章中,我将分享使用 Windows Presentation Foundation 框架用 C# 开发电子邮件客户端的基础知识。源代码非常简短紧凑,构建自己的基本客户端几乎不需要时间。但是,添加一些额外功能需要一些时间。我将在文章中稍后指出这些功能,您以后可以添加它们。
您需要对 Windows Presentation Foundation 框架以及 C# 语言的使用有基本了解。通常,C# 可以用来创建任何类型的应用程序。然而,WPF 是一个可以无限扩展的框架,并且可以用来创建高性能、高度可扩展的图形应用程序。WPF 的强大功能将用于构建这个 SMTP + IMAP 应用程序,以开发一个简单的电子邮件客户端。
了解协议
在我深入挖掘并开始编写源代码和解释构建应用程序的方法之前。我将尝试解释将在该应用程序中使用的协议。它们是两个
- IMAP:Internet 消息访问协议。
- SMTP:简单邮件传输协议。
这些协议通常用于发送和接收电子邮件。除了 IMAP,POP 和 POP3 是用于接收电子邮件或将电子邮件保存在您的设备上的两个协议。但是我不会讨论它们。
.NET 框架为 SMTP 协议提供了原生支持,可以通过您的 .NET 应用程序(如 Console、WinForms 或 WPF 等)发送电子邮件。然而,.NET 框架不提供 IMAP 协议的任何实现。相反,它们为您提供了 TCP 客户端和监听器。由于 IMAP 和 SMTP 等都是在 TCP 协议上运行的协议;或者任何其他传输层协议(如 UDP),在 .NET 框架中实现 IMAP 等原生协议非常简单易行。
有关更多此类包和命名空间来管理应用程序的网络功能,请在 MSDN 上阅读System.Net 命名空间。
相反,我将使用一个库来立即开始。很久以前,我发现了一个库,“ImapX”,它是 C# 应用程序中实现 IMAP 协议的一个绝佳工具。您可以通过执行以下 NuGet 包管理器命令从 NuGet 图库获取 ImapX:
Install-Package Imapx
这将添加该程序包。请记住,Imapx 仅在选定的环境中运行,并非所有环境都支持。您应该在CodePlex 网站上进一步阅读有关它的信息。
IMAP 协议用于获取电子邮件,进行阅读,而不是下载整个邮箱并将其存储在您自己的设备上。它是一个非常灵活的协议,因为电子邮件仍然保留在服务器上,并且可以被您拥有的每台设备访问。网络延迟在这里不是一个坏因素,因为您不必完全下载电子邮件。您只需下载您现在需要的内容。
图 1:IMAP 协议在网络上的使用。
构建应用程序
现在我已经指出了将在该应用程序中使用的一些事物,我想是时候继续编程部分并开发应用程序本身了。首先,创建一个新的 WPF 框架应用程序。编写应用程序的关注点和不同部分始终是一个好方法。
我们的应用程序将包含以下模块
- 身份验证模块。
- 最初,我们将要求用户进行身份验证。
- 由于复杂性,目前我仅支持 Gmail。
- 因此,应用程序仅要求输入用户名和密码组合。
- 您绝对应该将大多数字段留给用户填写,以便他们可以连接到自己的 IMAP 服务;除了 Gmail。
- 文件夹视图
- 查看文件夹及其消息。
- 消息视图
- 查看消息及其详细信息。
- 创建新消息。
分离这些关注点将帮助我们更敏捷地构建应用程序,以便当我们更新或创建应用程序中的新功能时,所需的时间不会更长。但是,如果您将所有内容硬编码在同一页面上。那么事情会变得非常困难。在这篇文章中,我还会给您一些技巧,以确保事情不会比它们应有的更困难。
管理“MainWindow”
每个 WPF 应用程序都会包含一个 MainWindow 窗口,这是屏幕上默认渲染的窗口。但是,**我的建议**是,您只在该窗口中创建一个 **Frame** 对象。仅此而已。该 Frame 对象将用于根据用户和应用程序的交互导航到应用程序的多个页面和不同的视图。
<Grid> <Frame Name="mainFrame" NavigationUIVisibility="Hidden" /> </Grid>
这里要注意的一点是,Frame 有一个(**IMO**)非常令人讨厌的导航 UI。您应该将其隐藏。对 Frame 应用相同的设置,或在应用程序资源中使用此设置并将其应用于整个应用程序。
这有助于更改视图,而不是更改 Grid 和 StackPanel 对象的可视性。但是,在运行时交换时,这将不可用。要能够使用它,您必须将 Frame 实例存储在一个可以从外部对象访问的位置。我在类中创建了以下属性,以保存对该 Frame 的引用。
public static Frame MainFrame { get; set; }
静态字段可以在外部对象中访问,而无需实例。这将很有帮助,并且您会在下面的后续源代码示例中发现它很有帮助。该类本身如下所示
public partial class MainWindow : Window { public static Frame MainFrame { get; set; } public static bool LoggedIn { get; set; } public MainWindow() { InitializeComponent(); MainFrame = mainFrame; // Chance are, its not logged in. MainFrame.Content = new LoginPage(); // Initialize the Imap ImapService.Initialize(); } }
非常简单,开发了应用程序的构建块。这将允许我们为不同的关注点和功能拥有单独的 UI。主要内容将由我们将为应用程序创建的单独页面完成、渲染和处理。
在我开始渲染后续页面部分之前,我需要解释一下 IMAP 基础控制器,我开发了它来提供 IMAP 协议的基本功能和特性。
ImapService 对象
将事物分配在同一位置会更好,这样当我们想要进行更改时,我们可以直接从那里进行更改,而不必搜索“我写 API 的那部分在哪里?”。这就是为什么,这个服务对象将包含将在应用程序中使用的所有函数。
我们需要 4 个函数和一个事件处理程序(用于 IDLE 支持;稍后讨论)
- 初始化函数;这是设计的要求。
- 登录和注销函数。
- 获取所有文件夹的函数
- 获取函数邮件消息的函数。
事件用于在新消息到达时通知。
对象定义如下
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using ImapX; using ImapX.Collections; using System.Windows; namespace ImapPackage { class ImapService { private static ImapClient client { get; set; } public static void Initialize() { client = new ImapClient("imap.gmail.com", true); if(!client.Connect()) { throw new Exception("Error connecting to the client."); } } public static bool Login(string u, string p) { return client.Login(u, p); } public static void Logout() { // Remove the login value from the client. if(client.IsAuthenticated) { client.Logout(); } // Clear the logged in value. MainWindow.LoggedIn = false; } public static List<EmailFolder> GetFolders() { var folders = new List<EmailFolder>(); foreach (var folder in client.Folders) { folders.Add(new EmailFolder { Title = folder.Name }); } // Before returning start the idling client.Folders.Inbox.StartIdling(); // And continue to listen for more. client.Folders.Inbox.OnNewMessagesArrived += Inbox_OnNewMessagesArrived; return folders; } private static void Inbox_OnNewMessagesArrived(object sender, IdleEventArgs e) { // Show a dialog MessageBox.Show($"A new message was downloaded in {e.Folder.Name} folder."); } public static MessageCollection GetMessagesForFolder(string name) { client.Folders[name].Messages.Download(); // Start the download process; return client.Folders[name].Messages; } } }
这个基本类将用于整个应用程序加载资源和消息。我不会深入研究这个库,因为我只会解释几点以及如何使用基本函数和特性。但是,该库非常强大,为您提供了构建功能齐全的电子邮件客户端所需的所有工具和服务。
登录页面
第一步是让用户输入他们的用户名和密码来验证用户。通常,您的应用程序会缓存用户的身份验证详细信息;在大多数情况下是用户名,在某些情况下(如果用户允许)则缓存密码。
在实际应用程序中,您将拥有一个完整的身份验证页面,其中将提供输入用户名、端口、IMAP 主机和密码等的字段。但是,在我的应用程序中,我没有使用任何此类字段,因为我想让事情保持非常简单。我只是要求用户输入用户名和密码,并在应用程序的源代码中硬编码了其他设置。
页面的 XAML 如下所示
<StackPanel Width="500" Height="300"> <TextBlock Text="Login to continue" FontSize="25" /> <TextBlock Text="We support Google Mail at the moment, only." /> <Grid Height="150" Margin="0, 30, 0, 0"> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="3*" /> </Grid.ColumnDefinitions> <StackPanel Grid.Column="0"> <TextBlock Text="Username" Margin="0, 5, 0, 15" /> <TextBlock Text="Password" /> </StackPanel> <StackPanel Grid.Column="1"> <TextBox Name="username" Margin="0, 0, 0, 5" Padding="4" /> <PasswordBox Name="password" Padding="4" /> </StackPanel> </Grid> <Button Width="150" Name="loginBtn" Click="loginBtn_Click">Login</Button> <TextBlock Name="error" Margin="10" Foreground="Red" /> </StackPanel>
此内容将被加载,由于我们在 MainWindow 的构造函数中加载了此页面,因此我们将首先看到此页面。
图 2:应用程序的身份验证页面。
当然,您将提供额外的部分和字段供用户配置您的应用程序将如何连接到他们的 IMAP 服务提供商。但是,这样也有效。Mozilla 的 Thunderbird 就是这样做的,它们只要求用户名和密码,然后自己找到正确的设置和配置。但是,无论您如何选择,都可以以自己的方式编写应用程序。
我们只需要处理按钮的事件,
private void loginBtn_Click(object sender, RoutedEventArgs e) { MainWindow.LoggedIn = ImapService.Login(username.Text, password.Password); // Also navigate the user if(MainWindow.LoggedIn) { // Logged in MainWindow.MainFrame.Content = new HomePage(); } else { // Problem error.Text = "There was a problem logging you in to Google Mail."; } }
显然,这只会调用我们之前创建的服务。好处是我们只需要在这里添加验证和验证代码。我们不需要在这里写任何新东西,因为我们已经在之前设置的服务“ImapService”中完成了所有这些工作。最后,如果一切顺利,它将把用户导航到主页。
注意:应用程序中没有异步,因此它有时可能会“冻结”。要解决此问题,您可能需要重写整个服务,使用 async/await 来开发它。
主页
在此应用程序中,主要组件是主页,主页是加载文件夹和消息的地方。加载这些并不困难,因为我们已经创建了函数来执行以获取文件夹和这些文件夹中的消息(**再次阅读 ImapService 类**)。
这里使用的基本 XAML 控件如下
<ListView Name="foldersList" SelectionChanged="foldersList_SelectionChanged"> <ListView.ItemTemplate> <DataTemplate> <TextBlock Margin="10" Cursor="Hand" Text="{Binding Title}" Name="folderTitle" /> </DataTemplate> </ListView.ItemTemplate> </ListView> <Frame Grid.Column="1" Name="contentFrame" NavigationUIVisibility="Hidden" />
一个用于渲染文件夹的列表视图,以及另一个用于加载多个页面的 Frame。我使用了相同的方法来保存我的类对象中这个 Frame 的引用,并且我使用构造函数本身加载了它。
public HomePage() { InitializeComponent(); ContentFrame = contentFrame; foldersList.ItemsSource = ImapService.GetFolders(); ClearRoom(); }
仅仅使用相同的几个小步骤,我就创建了页面。然后它将具有自己的功能,并允许功能处理与用户的交互。
图 3:应用程序中列出的文件夹。
我们可以通过选择一个文件夹来加载文件夹中的消息。这将在右侧显示消息,然后我们可以阅读消息。代码包含在应用程序本身中。
private void foldersList_SelectionChanged(object sender, SelectionChangedEventArgs e) { var item = foldersList.SelectedItem as EmailFolder; if(item != null) { // Load the folder for its messages. loadFolder(item.Title); } } private void loadFolder(string name) { ContentFrame.Content = new FolderMessagesPage(name); }
通过这种方式,我们将文件夹加载到应用程序主页的内容 Frame 中。
发送电子邮件
我们已经谈了很多关于 IMAP 协议的内容,我想我也应该分享一些关于该应用程序的 SMTP 协议支持的要点。SMTP 协议支持由 .NET 框架原生提供。
我写了一篇关于 .NET 框架 SMTP 协议支持的文章,您可能会感兴趣,使用 C# 代码通过 .NET 框架发送电子邮件及常见问题。那篇文章讨论了很多关于这样做的内容,而且,我使用了那篇文章中的相同代码,并根据它编写了用于发送电子邮件的整个功能。
我创建了“发送电子邮件”表单,如下所示
<Grid> <Grid.ColumnDefinitions> <ColumnDefinition /> <ColumnDefinition Width="3*" /> </Grid.ColumnDefinitions> <StackPanel Grid.Column="0"> <TextBlock Text="To" Margin="0, 5" /> <TextBlock Text="Subject" Margin="0, 5" /> <TextBlock Text="Body" Margin="0, 5" /> </StackPanel> <StackPanel Grid.Column="1"> <TextBox Margin="0, 3" Padding="1" Name="to" /> <TextBox Margin="0, 3" Padding="1" Name="subject" /> <TextBox Margin="0, 3" Padding="1" Height="130" Name="body" AcceptsReturn="True" ScrollViewer.CanContentScroll="True" ScrollViewer.VerticalScrollBarVisibility="Auto" /> <Button Width="50" Name="sendBtn" Click="sendBtn_Click">Send</Button> <Button Width="50" Name="cancelBtn" Click="cancelBtn_Click" Margin="130, -20, 0, 0">Cancel</Button> </StackPanel> </Grid>
虽然,我明白这不是我能做出的最好的东西,但是,为了这篇文章,它(**应该!**)足够了。这将允许用户输入非常基础电子邮件的详细信息。
图 4:SMTP 协议测试,电子邮件表单。
点击发送按钮后,电子邮件将被发送。执行此操作的代码非常直接,并且(如前所述)在我撰写的关于在 C# 中发送电子邮件的文章中有。
奖励:文件夹的 IDLE 支持
在我结束之前,我想阐明 IDLE 支持的主题。在 IMAP 中,IDLE 支持意味着您的应用程序无需向服务器发送请求即可获取新消息。相反,服务器会在收到新电子邮件时自行将数据发送给客户端。
我所做的是,我编写了支持 ImapService 中的 IDLE 功能的代码,因为那是 IMAP 的服务而不是应用程序本身。我更新了文件夹获取代码并添加了以下语句(**或者由于我根本没有共享代码,代码如下所示**),
public static List<EmailFolder> GetFolders() { var folders = new List<EmailFolder>(); foreach (var folder in client.Folders) { folders.Add(new EmailFolder { Title = folder.Name }); } // Before returning start the idling client.Folders.Inbox.StartIdling(); // And continue to listen for more. client.Folders.Inbox.OnNewMessagesArrived += Inbox_OnNewMessagesArrived; return folders; } private static void Inbox_OnNewMessagesArrived(object sender, IdleEventArgs e) { // Show a dialog MessageBox.Show($"A new message was downloaded in {e.Folder.Name} folder."); }
然后,处理程序和事件将作为对应项工作,并为我们提供一个非常出色的服务,使我们的应用程序能够在收到消息时获取消息!
我们可以使用一个文件夹来启动 IDLE。在我的例子中,我使用了收件箱文件夹。收件箱文件夹将让我们知道收到的任何新电子邮件消息,然后我们可以使用事件参数的属性来找出哪个文件夹收到了哪条消息。
关注点
在 .NET 框架中,SMTP 协议得到原生支持,实现 IMAP 协议是一项艰巨的任务。为此,开发人员可以使用 ImapX,这是一个免费开源的包和库,适用于希望支持电子邮件消息的 C# 应用程序。
在这篇博客中,我解释了用于构建可以向客户端发送和接收 Internet 电子邮件消息的应用程序的方法。IMAP 协议是更受欢迎和喜爱的协议,因为它不需要您下载电子邮件,您只需下载您需要的内容。
如果我写了额外的内容,我会更新帖子或创建一个新的帖子。如果您发现有遗漏之处,请给我留言。