带完整 MIME 支持的 POP3 电子邮件客户端(.NET 2.0)






4.88/5 (104投票s)
一个 C# 类,用于从 POP3 服务器读取 ASCII 电子邮件,并使用 MIME 将它们转换为 System.Net.Mail.MailMessage 派生类以进行进一步处理。提供了完整的代码(纯 C# 2.0,仅使用 .NET 框架 DLL)。如果可能,它将 MIME 多部分匹配到 MailM 的正文、附件等。
引言
这是我关于使用 POP3 接收电子邮件和 MIME 处理的文章的第 2 部分。我的第一篇文章 POP3 电子邮件客户端(.NET 2.0)涵盖了从 POP3 服务器可靠下载电子邮件,这给我们留下了电子邮件正文的纯 ASCII 表示。这是比较容易的部分。
在本文中,我提供了将原始 ASCII 电子邮件拆分为正文、附件、备用视图等的代码。这要困难得多,因为虽然 POP3 规范很简单,并且在一个 RFC 中直接规定,但有几个与 MIME 相关的 RFC,它们提供了多种可能性,说明如何发送像电子邮件的实际文本这样的简单内容。MIME 规范提供了极大的灵活性,但微软,作为微软,当然只支持一个子集(例如,MIME 部分内的 MIME 部分不递归)。提供的代码完全支持这两种情况,并为程序员提供了根据需要访问有关接收到的电子邮件信息的灵活性。
如果您想知道为什么我撰写了这篇文章,尽管 CodeProject 上有许多关于 MIME 支持的文章,但这里是一些遇到的缺点
- 一些代码不是托管代码
- 使用没有 .NET 源代码的 DLL
- 功能过于有限
- 未与
System.Net.Mail.MailMessage
集成 无错误报告
- 无 XML 文档等
我的代码基于以下工作
背景
简单电子邮件的结构
纯 ASCII 的简单电子邮件可能如下所示
Date: Sat, 2 Sep 2006 17:25:15 +0200
From: Sender@NoSpam.com
To: Receiver@NoSpam.com
Subject: simple plain text mail
Just a plain text email
.
前 4 行称为电子邮件的标头,它们与正文通过空行分隔。电子邮件的末尾用只包含一个“.”(句号)的行标记。当你查看真实的电子邮件时,会有更多标头行,一些是 RFC 标准的,还有一些,比如来自 GMail 的这一行
X-Gmail-Received: f105c784e77f8b689759558db72ccd07f60387ba
MIME 介绍
最初,只有 RFC 2822 中定义的纯 ASCII 电子邮件。然而,纯 ASCII 很快就不够了,因此创建了多用途互联网邮件扩展(MIME)规范,以支持非 US-ASCII 文本、多部分邮件正文、富文本(HTML)、图像、声音和附件。该规范试图提供极大的灵活性,并满足各种可能性。结果是产生了大量的 RFC(2045、2046、2047、2049、2231、2387、4288、4289 等)。正如在大型团体中经常发生的那样,整个事情变得相当复杂,更糟糕的是,将正文文本等的具体实现留给了实现者。为了帮助您从基于 MIME 的电子邮件中提取信息,我将向您解释 MIME 的基本原理。首先,让我们看看一封完整的 MIME 电子邮件。它可能有点令人困惑,但它很好地概述了各种 MIME 元素,我将逐一解释。这封电子邮件有一个电子邮件标头,后面是电子邮件正文和一张 .GIF 图片。注意“--0-494165446-1157210079=:74253
”行,它分隔了电子邮件的各个部分,称为 MIME 实体。
Date: Sat, 2 Sep 2006 17:25:15 +0200
From: Sender@NoSpam.com
To: Receiver@NoSpam.com
Subject: simple gmail mail
MIME-Version: 1.0
Content-Type: multipart/mixed; boundary="0-494165446-1157210079=:74253"
Content-Transfer-Encoding: 8bit
--0-494165446-1157210079=:74253
Content-Type: text/plain; charset=iso-8859-1
Content-Transfer-Encoding: 8bit
Content-Disposition: inline
This is the email body
This email has a smallPic.gif attachment
--0-494165446-1157210079=:74253
Content-Type: image/gif; name="SmallPic.GIF"
Content-Transfer-Encoding: base64
Content-Description: 437081412-SmallPic.GIF
Content-Disposition: inline; filename="SmallPic.GIF"
R0lGODlhQQBBAPcAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/
NZWZfpnCck/OeTUXvUdXxdi9/SbDPFS4t+/fwIMLH068uPHjyJMrX868ufPn
0KNLn069uvWVAQEAOw==
--0-494165446-1157210079=:74253--
.
电子邮件标头字段的结构
根据 RFC 2822 定义的电子邮件标头字段具有以下结构field-name ":" [ field-body ] CRLF
Example:
MIME-Version: 1.0
"MIME-Version" 是 field-name
,"1.0" 是 field-body
。MIME-Version 标头字段是每个 MIME 电子邮件的强制字段。所有其他 MIME 标头字段都以 "Content-...
" 开头。
Content-Type
最强大的 MIME 头部字段是 Content-Type
,它在 RFC 2046 中定义。它可能看起来像这样
Content-Type: text/plain;
Content-Type: text/plain; charset=ISO-8859-1
Content-Type: text/plain; charset=us-ascii
Content-Type: text/plain; charset=utf-8
Content-Type: text/html;
Content-Type: text/html; charset=ISO-8859-1
Content-Type: text/css
Content-Type: image/gif; name=image004.gif
Content-Type: image/jpeg; name="image005.jpg"
Content-Type: message/delivery-status
Content-Type: message/rfc822
Content-Type: audio/x-mpeg
Content-Type: video/mpeg-2
Content-Type: application/msword
Content-Type: application/mspowerpoint
Content-Type: application/zip
Content-Type: multipart/mixed;
boundary="----=_Part_3431_12384933.1139387792352"
Content-Type: multipart/alternative;
boundary="----=_Part_4088_29304219.1115463798628"
Content-Type: multipart/related;
boundary="----=_Part_2067_9241611.1139322711488"
Content-Type: multipart/digest;
boundary="----=Next message 15543233913938263541"
Content-Type: multipart/report; report-type=delivery-status;
boundary="k04G6HJ9025016.1136391237/carbon.singnet.com.sg"
Content-Type: multipart/parallel
Content-Type
字段用于通过指示媒体类型和子类型标识符以及提供某些媒体类型可能需要的辅助信息来指定 MIME 实体正文中数据的性质。一些媒体类型是
- 文本
- 图像
- message
- 音频
- 应用
- 多部分
每种媒体类型都定义了自己的一组子类型,其后可以跟一组参数,每个参数都以 attribute=value 对的形式指定。例如
Content-Type: text/plain; charset=ISO-8859-1; format=flowed
媒体类型
是“text
”,子类型
是“plain
”,属性
是“charset
”,属性值
是“ISO-8859-1
”。可能还有更多的 attribute=value 对,例如 "format=flowed"
。
内容类型多部分
媒体类型“multipart”提供了将电子邮件拆分为多个部分的灵活性,例如纯文本、HTML 文本和附件文件。multipart 有不同的版本(子类型),但都具有相同的属性“boundary”。其值是一个在整个电子邮件中唯一的字符串,用于标记各个部分的边界分隔符行。让我们再次查看前面的示例,这次只包含 Content-Type
信息行
Headerlines
Content-Type: multipart/mixed; boundary="0-494165446-1157210079=:74253"
Headerlines
--0-494165446-1157210079=:74253
Content-Type: text/plain; charset=iso-8859-1
Other MIME part header lines
The plain text email body
--0-494165446-1157210079=:74253
Content-Type: image/gif; name="SmallPic.GIF"
Other MIME part header lines
The attachment coded in Base64
--0-494165446-1157210079=:74253--
.
前 3 行是电子邮件标题的一部分。标题的末尾由一个空行标记。所有其他行都是电子邮件正文的一部分,正文以只包含一个“.
”(句号)的行结束。边界分隔符行将正文本身分成电子邮件文本和文件附件。此行始终以“--
”开头,后跟边界字符串。最后一个边界分隔符行后跟附加的“--
”。
每个 MIME 实体都有一个实体头和一个实体正文,由一个空行分隔。由于电子邮件和 MIME 实体使用相同的结构和相同类型的头行,因此整个电子邮件可以成为 MIME 实体,这对于邮件系统非常有用(Content-Type: message
)。但当然,一封邮件中包含另一封邮件再包含另一封邮件会导致许多复杂问题,因此大多数邮件程序使用不同的解决方案来转发电子邮件也就不足为奇了,它们只是将其与电子邮件正文文本合并。这样做的好处是,即使不支持 MIME 的邮件客户端也能处理转发。同样,即使 MIME 规范是递归的,微软的 System.Net.Mail.MailMessage
也不是!稍后会详细介绍。
Content-Type: multipart/mixed
通常,最顶层的多部分子类型是“mixed
”。它表示电子邮件由多个 MIME 实体组成,但没有指定更多关于实体类型的信息。如果电子邮件客户端未识别实际的子类型,“multipart/mixed
”将用作默认值。
内容类型: multipart/alternative
如果同一封电子邮件以纯文本和 HTML 格式发送,则使用子类型“alternative
”。两者内容相同,但编码方式不同。电子邮件客户端应向用户显示客户端理解的最后一个替代部分。如果一封电子邮件由一个纯文本实体和一个 HTML 实体组成,则电子邮件客户端应显示 HTML 文本,即使它也知道如何显示纯文本,因为 HTML 版本在最后。包含纯文本和 HTML 的电子邮件可能如下所示
Some header lines
MIME-Version: 1.0
Content-Type: multipart/mixed;
boundary="----_=_NextPart_001_01C6CEA2.EF9BECF8"
------_=_NextPart_001_01C6CEA2.EF9BECF8
Content-Type: multipart/alternative;
boundary="----_=_NextPart_002_01C6CEA2.EF9BECF8"
------_=_NextPart_002_01C6CEA2.EF9BECF8
Content-Type: text/plain; charset="iso-8859-1"
HTML sample email with bold text and attachment.
------_=_NextPart_002_01C6CEA2.EF9BECF8
Content-Type: text/html; charset="iso-8859-1"
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<HTML>
<HEAD>
<STYLE>
DIV { FONT-SIZE: 10pt;
FONT-FAMILY: Verdana, Arial, Helvetica, sans-serif }
</STYLE>
</HEAD>
<BODY>
<DIV>
HTML sample email with <STRONG>bold</STRONG> text and attachment.
</DIV>
</BODY>
</HTML>
------_=_NextPart_002_01C6CEA2.EF9BECF8--
------_=_NextPart_001_01C6CEA2.EF9BECF8
Content-Type: image/gif; name="SmallPic.GIF"
R0lGODlhQQBBAPcAAAAAAIAAAACAAICAAAAAgIAAgACAgICAgMDAwP8AAAD/
NZWZfpnCck/OeTUXvUdXxdi9/SbDPFS4t+/fwIMLH068uPHjyJMrX868ufPn
0KNLn069uvWVAQEAOw==
------_=_NextPart_001_01C6CEA2.EF9BECF8--
.
这封电子邮件的结构是
multipart/mixed
| multipart/alternative
| | text/plain; format=flowed; charset=ISO-8859-1
| | text/html; charset=ISO-8859-1
| image/gif; name=SmallPic.GIF
请注意,图片是 multipart/mixed
的一部分,而不是 multipart/alternative
。
内容类型: multipart/related
Multipart-related
可用于在同一封电子邮件中发送 HTML 文本和图形或其他相关材料。本文不打算详细解释其他多部分媒体类型的细节。
内容传输编码
POP3 定义电子邮件的正文是 7 位 US ASCII 码。由于向用户显示的文本可以是任何 Unicode 字符,并且文件附件通常是字节数组,因此电子邮件发送方必须将此内容编码为 ASCII,而我们,电子邮件接收方,需要对其进行解码。如果值为“7bit”,则未进行编码。“8bit”或“binary”具有相同的含义,但 .NET 框架不支持。我将“8bit”视为“7bit”,即按原样处理内容,而“binary”在 POP3 中是非法的,因为某些字符序列(如 CRLF "." CRLF)在 POP3 中具有特殊含义,但可能出现在随机二进制中。
内容传输编码:quoted-printable
如果 MIME 实体主要由 US ASCII 字符组成,则只需编码一些特殊字符和 US ASCII 字符集未涵盖的所有字节。“quoted-printable”通过发送“=
”和字节的十六进制值作为 ASCII 字符来实现这一点。回车符(十六进制:0D
)变为:“=0D
”。有许多规则处理特殊情况。我无法在 .NET 中找到 quoted-printable 的解码器,因此我从 ASP Emporium 复制了 Bill Gearhart 的 QuotedPrintable 类源代码。
Content-Transfer-Encoding: base64
Base64
使用有限的字符集(“A”-“Z”、“a”-“z”、“0”-“9”、“+”、“/”)来表示 6 位值。任何 3 个字节都可以用 4 个编码字符表示。例如,让我们以示例电子邮件中图形文件的前 4 个 ASCII 字符“R0lG
”为例
R 0 l G
001001 110100 100101 000110
Resulting 3 bytes:
00100111 01001001 01000110
详细信息可在 RFC 1421,4.3.2.4 步 4:可打印编码中找到
使用代码
理解一个库的最佳方法是使用它。可下载代码中的 Main
函数就是这样做的。它连接到 POP3 服务器(不要忘记提供正确的服务器名称、用户名和密码),并最多下载 5 封电子邮件。代码不会从服务器删除电子邮件,但服务器可能会根据其设置删除它们。5 封电子邮件的结构将显示在控制台上。“Program.cs”还包含 SendTestmail()
方法,用于生成一些示例电子邮件。
电子邮件由继承自 Pop3MailClient
的 Pop3MimeClient
接收,该类在 Peter Huber 的 POP3 电子邮件客户端(.NET 2.0)中描述,并提供了与 POP3 服务器交互的所有功能。Pop3MimeClient
添加了 GetEmail
方法,该方法从 POP3 服务器获取特定电子邮件并将其解码为 RxMailMessage
返回。
MIME 到 System.Net.Mail.MailMessage 的映射
System.Net.Mail.MailMessage
类由 System.Net.Mail.SmtpClient
用于通过 SMTP 发送电子邮件。MailMessage
仅包含发送电子邮件所需的信息。接收电子邮件会创建一些额外信息。因此,一个新的类 RxMailMessage
继承自 MailMessage
,并添加了 DeliveryDate
或 DeliveredTo
等属性。
SmtpClient
将 MailMessage
转换为符合 MIME 的电子邮件,但 MailMessage
几乎不提供任何 MIME 相关功能的访问。接收电子邮件时,我们希望存储完整的信息。Pop3MimeClient
按 MIME 实体接收第一个 MIME 实体,并将其作为 MIME 实体树存储在 RxMailMessage
的新 Entities
集合属性中。如果可能,信息也会复制到从 MailMessage
继承的属性中。这让用户可以自由选择是使用 MIME 形式的完整电子邮件进行进一步处理,还是只使用 MailMessage
定义的更简单但可能不完整的 Body
、AlternateViews
或 Attachments
。decodeEntity
方法可以作为如何遍历电子邮件中所有 MIME 实体的示例。
历史
-
2006 年 10 月 11 日 改进 构造函数,处理 ContentDisposition==null
- 在
Pop3MimeClient
构造函数中正确处理useSSL
- 避免
ContentDisposition
为null
时引发异常 -
2006 年 10 月 8 日 改进附件处理
- 检测 content-disposition 头部字段,如果看起来像“
C-Disp: attachment
”则创建附件 - 只有来自
multipart/alternative
父级的 MIME 实体才会成为备用视图 - 在
RxMailMessage.MailStructure()
中为多部分添加了结束标记 -
2006 年 9 月 17 日 首次发布
- 我不太确定如何将各种多部分实体映射到电子邮件正文等。我分析了可能数千封收到的电子邮件,并酌情填充了
RxMailMessage
属性,但您很可能会收到格式不同的电子邮件。如果您收到此类邮件或发现任何错误,请在此处提供一些反馈。