简介:面向C#开发者的英文技术文档,讲解如何通过微软Exchange Web Services(EWS)管理API访问Exchange邮箱,覆盖邮件收发、日历、任务和联系人等对象的编程操作,适合需要使用Exchange做二次开发的.NET工程师学习。内容以EWS Managed API为主线,从引用Microsoft.Exchange.WebServices.dll程序集与命名空间开始,逐步说明测试服务器自签名证书的验证回调——通过ServicePointManager.ServerCertificateValidationCallback处理并不应一律返回true,避免测试环境常见的信任报错;随后介绍ExchangeService实例创建、凭据设置,以及服务URL的手动配置与Autodiscover自动发现两种方式。文中附有收件箱邮件查询、发送邮件及保存副本、日历管理等典型代码示例,可帮助读者快速理解EWS的核心调用流程并直接迁移到自身项目中。资源包共1个doc文档,大小仅147KB,内容精炼。目前已有679人学习/下载,对接触Exchange EWS的C#开发者具有较高参考价值。
1. 为什么这份 EWS 英文文档值得落地:先给自己一个“能跑起来”的起点
拿 C# 走 Exchange EWS 这条路,最劝退的不是 API 难懂,而是客户端一启动就被证书、Autodiscover 和凭据三件事卡住。这份英文版入门资料把 EWS Managed API 的初始化、绑定、搜索、扩展属性和忙闲查询都串了一遍,照着复现,等于把“连接测试服务器 → 收发邮件 → 查日历 → 读扩展字段”这条主链路完整踩通。适合刚接到企业邮箱集成任务、想快速做邮件客户端或日程同步模块的 C# 开发者;即使你已经会查官方文档,后半部分的避坑记录也能帮你少走弯路。
2. 连接与初始化:把“测试服务器能连上”放在第一优先级
2.1 程序集引用与命名空间:DLL 和 XML 要配对存放
EWS Managed API 实际交付物只有一个程序集:Microsoft.Exchange.WebServices.dll,边上再配一个Microsoft.Exchange.WebServices.xml,后者不是给运行用的,是给 Visual Studio 的 IntelliSense 提供注释提示。很多新手只把 DLL 拉进项目,XML 不拷,结果代码提示残缺,排查问题时连参数含义都要来回翻文档。
我一般会在解决方案里单独建一个Libs目录,把这两个文件整体放进去,再在项目文件里加引用。这样升级 EWS 版本时只需换目录内容,不用动引用路径。使用 VS 图形界面时,在“添加引用”里浏览选中 DLL 即可;如果你喜欢直接改工程文件,引用块长这样:
<Reference Include="Microsoft.Exchange.WebServices"> <HintPath>Libs\Microsoft.Exchange.WebServices.dll</HintPath> </Reference>这个HintPath是相对于工程文件所在目录的路径,Include里的名字是程序集名,不一定要和文件名完全一致,但为了少踩坑,我建议保持默认,别手动重命名文件。引用完成后,代码文件顶部加using Microsoft.Exchange.WebServices.Data;,后续所有类型都从这个命名空间进来。
2.2 ExchangeService 实例与凭据:域账号到底怎么填
访问 EWS 的入口是ExchangeService实例。原文档里直接new ExchangeService(),没有指定版本,实际项目里我会建议把版本参数显式写出来,因为 Exchange 服务端的版本不同,返回的架构会有细微差异,缺省值有时会落到比较老的兼容模式:
using Microsoft.Exchange.WebServices.Data; ExchangeService 服务 = new ExchangeService(ExchangeVersion.Exchange2013_SP1); 服务.Credentials = new WebCredentials("zhangsan", "口令", "example.com");WebCredentials本质上是NetworkCredential的封装,构造参数顺序是“用户名、密码、域”。如果账号不是域账号,可以省略第三个参数,或者在用户名里写成example.com\zhangsan的格式。两者效果接近,但WebCredentials在 EWS 场景下更省事,它会自动处理协议内部的 Basic/Digest 认证切换。
有一点要提醒:凭据对象不要用字符串拼接去构造,更不要把密码硬编码在业务代码里。生产环境一般用 Windows 集成认证跑服务账号,或者从配置中心读取加密后的口令。你要是刚上手,先本地写死跑通没问题,但提交代码前记得把敏感值抽出去。
2.3 手动 URL 还是 Autodiscover:两者只能选一个
EWS 端点地址有两个来源:一个是管理员给的固定 URL,另一个是 Autodiscover 自动发现。原文档说得非常直接:两种方式不要同时用。如果既设置了Url,又调用AutodiscoverUrl,后者通常不会覆盖前者,请求最终会一直打到旧地址上去,配置变更后很难排查。
ExchangeService 服务 = new ExchangeService(ExchangeVersion.Exchange2013_SP1); 服务.Credentials = new WebCredentials("zhangsan", "口令", "example.com"); // 方式一:固定 EWS 地址 服务.Url = new Uri("https://mail.example.com/EWS/Exchange.asmx"); // 方式二:Autodiscover 自动发现 // 服务.AutodiscoverUrl("zhangsan@example.com");用 Autodiscover 的好处是它会给当前用户选择离 Mailbox Server 最近的端点,而管理员在后端迁移 Client Access 服务后,EWS URL 可能变化。所以线上客户端我一般先把 Autodiscover 返回的 URL 缓存到本地,每隔一段时间或连接失败时再重新发现一次。
还要注意,原文强调AutodiscoverUrl默认不会跟随重定向。因为自动发现过程可能涉及 DNS 重定向,存在被劫持到不安全地址的风险。如果公司网络环境确实有跳转,必须提供一个重定向校验回调:
private static bool 校验重定向地址(string 目标地址) { return 目标地址.StartsWith("https://", StringComparison.OrdinalIgnoreCase); }回调里不要无条件return true,至少校验协议必须是 HTTPS,最好再比对域名白名单。我见过有人图省事直接放行,结果 Autodiscover 被中间人指到一个伪造地址,邮件凭据跟着被套走——这个风险不是理论上的。
2.4 测试服务器自签名证书:别让“先跑通”变成“裸奔”
测试环境通常用自签名证书,而 .NET Framework 默认不信任它,于是第一条 EWS 请求就会抛AuthenticationException或者 SSL 证书错误。EWS 文档里给的方案是挂ServicePointManager.ServerCertificateValidationCallback:
System.Net.ServicePointManager.ServerCertificateValidationCallback = delegate (object 请求对象, X509Certificate 服务端证书, X509Chain 证书链, SslPolicyErrors 验证错误) { if (验证错误 == SslPolicyErrors.None) { return true; } // 测试阶段固定指纹,只放行目标证书 string 指纹 = 服务端证书?.GetCertHashString(); return string.Equals(指纹, "AB:CD:EF:12:34:56:78:90:AB:CD:EF:12:34:56:78:90", StringComparison.OrdinalIgnoreCase); };这个回调是进程级的全局静态委托,一旦设置,会影响当前进程所有 SSL 请求。文档里那句“不要总是返回 true”绝不是客气话:如果无脑放行所有证书,攻击者用一个任意有效证书就能伪装成你的 Exchange 服务器,账号密码等于交给别人。正确做法是测试环境固定证书指纹,或者只对.local后缀的测试域名放行;生产环境保持默认校验,不要挂这个回调。
这段写完,连接初始化链路就完整了。接下来进入真正的业务操作。
3. 邮件、日历和文件夹操作:把文档里的类层次落到业务代码
3.1 绑定已有对象:ItemId、FolderId 与 WellKnownFolderName
EWS 把邮件、日历约会、联系人、任务都统称为 Item,每个 Item 在服务器上有一个唯一 ID;文件夹同理。拿到一个 ID 后,不需要手动解析 XML,直接用Bind方法把对象加载出来:
// 用已知 ID 绑定一封邮件 EmailMessage 那封邮件 = EmailMessage.Bind(服务, new ItemId("AQMkAGJhZGRl...AAAA==")); // 绑定已知 ID 的文件夹 Folder 某个文件夹 = Folder.Bind(服务, new FolderId("AQMkAGJhZGRl...AAA=")); // 绑定系统预定义文件夹,不需要 ID Folder 收件箱 = Folder.Bind(服务, WellKnownFolderName.Inbox);Bind的第一个参数是已初始化的ExchangeService实例,第二个参数是 ID 或文件夹名。WellKnownFolderName是一个枚举,覆盖 Inbox、Calendar、Tasks、Contacts 等常用位置。如果不确定某个 ID 指向的是邮件还是约会,可以用基类Item.Bind,再用is判断类型,避免强转出错。
3.2 发送、回复与转发:SendAndSaveCopy 和 CreateReply 的界限
发新邮件这段很直观:构造EmailMessage,填主题、正文、收件人,然后SendAndSaveCopy()会发送并把邮件副本存进已发送文件夹。Send()也能发送但不保存副本,两者差异一定要根据业务选对,别把该留底的邮件发完就找不到了。
EmailMessage 新邮件 = new EmailMessage(服务); 新邮件.Subject = "本周评审改期"; 新邮件.Body = "原定周三上午的评审,改到周四上午 10 点。"; 新邮件.ToRecipients.Add("someone@example.com"); 新邮件.SendAndSaveCopy(); // 对已有邮件做回复 EmailMessage 原邮件 = EmailMessage.Bind(服务, new ItemId("AQMkAGJhZGRl...AAAA==")); ResponseMessage 回复对象 = 原邮件.CreateReply(true); // true 表示答复全部 回复对象.BodyPrefix = "收到,我按新时间参加。"; 回复对象.CcRecipients.Add("manager@example.com"); 回复对象.SendAndSaveCopy();CreateReply(true)会生成一个答复全部收件人的响应对象,BodyPrefix是加在原文上方的前缀,不会改动原邮件本身。转发则用CreateForward(),参数更少,但同样需要调用SendAndSaveCopy()才真正离开发件箱。原文档里提供的是Reply快捷方法,它内部创建响应并立即发送;如果需要改收件人、加抄送,就走CreateReply路线。
3.3 搜索与分页:ItemView、SearchFilter、PropertySet 三件套
FindItems是最常用的读取入口。ItemView控制一次返回多少条,SearchFilter组合查询条件,PropertySet控制加载哪些属性。下面这段是先取收件箱中主题包含 “EWS” 或 “API” 的前 10 封邮件,按接收时间升序排:
ItemView 视图 = new ItemView(10); 视图.OrderBy.Add(ItemSchema.DateTimeReceived, SortDirection.Ascending); 视图.PropertySet = new PropertySet( BasePropertySet.IdOnly, ItemSchema.Subject, ItemSchema.DateTimeReceived); SearchFilter.SearchFilterCollection 过滤器 = new SearchFilter.SearchFilterCollection( LogicalOperator.Or, new SearchFilter.ContainsSubstring(ItemSchema.Subject, "EWS"), new SearchFilter.ContainsSubstring(ItemSchema.Subject, "API")); FindItemsResults<Item> 结果 = 服务.FindItems( WellKnownFolderName.Inbox, 过滤器, 视图); Console.WriteLine("命中数量: " + 结果.TotalCount); foreach (Item 邮件项 in 结果.Items) { Console.WriteLine(邮件项.Subject); }BasePropertySet.IdOnly表示只加载 ID,这样响应体积最小;如果你还想读主题和接收时间,必须显式放进PropertySet。很多新手拿到FindItems结果后直接访问Body或Attachments,发现全是空,就是因为没把这些属性加进PropertySet。搜索条件里ContainsSubstring默认做子串匹配,大小写不敏感,业务上做严格过滤时要注意。
3.4 扩展属性:GUID、MapiPropertyType 与 PropertySet 配合
Exchange 允许为邮件追加自定义字段,EWS 里叫 Extended Properties。实现分三步:定义属性、写入、读取。属性定义里的 GUID 要固定下来并全局唯一,不要每次运行都临时生成,否则同一套代码部署到多台机器后,各写各的 GUID,互相读不到。
// 整个项目统一使用这个 GUID,不要变 Guid 属性集标识 = new Guid("01638372-9F96-43b2-A403-B504ED14A910"); ExtendedPropertyDefinition 自定义编号 = new ExtendedPropertyDefinition( 属性集标识, "项目编号", MapiPropertyType.String); EmailMessage 带标记邮件 = new EmailMessage(服务); 带标记邮件.Subject = "带扩展属性的邮件"; 带标记邮件.SetExtendedProperty(自定义编号, "PRJ-2025-001"); 带标记邮件.Save(); // 读取时要把同一属性定义放进 PropertySet PropertySet 读取属性集 = new PropertySet( BasePropertySet.FirstClassProperties, 自定义编号); EmailMessage 读回邮件 = EmailMessage.Bind(服务, new ItemId("AQMkAGJhZGRl...AAAA=="), 读取属性集); foreach (ExtendedProperty 属性项 in 读回邮件.ExtendedProperties) { Console.WriteLine(属性项.Value); }写入用的SetExtendedProperty既能在新建邮件时调用,也能在 Bind 之后修改并Update。读取时如果PropertySet里没带上对应的ExtendedPropertyDefinition,服务端不会返回该字段。另外MapiPropertyType决定了底层字段类型:字符串用String,整数用Integer,日期用SystemTime,类型不匹配会在服务端报转换错误。
4. 避坑与排查:连不上的五张多米诺骨牌
4.1 证书回调“全返回 true”,测试一过生产就翻车
现象:测试环境用自签名证书,回调写成return true后一切正常;上线后换了正式证书,发现有时能通、有时突然断,抓包看到证书链不完整。
原因:全局回调放行了所有证书,包括无效或过期证书,服务端做证书轮换时,带外错误被吞掉了。
解决:测试环境固定证书指纹或使用测试专用 CA;生产环境移除ServerCertificateValidationCallback或只在回调里严格比对SslPolicyErrors.None。我习惯把证书校验逻辑单独抽成方法,用配置开关区分环境,避免代码里到处散布裸的return true。
4.2 同时设置 Url 和调用 AutodiscoverUrl,请求始终打到旧端点
现象:管理员通知 EWS 地址已迁移,代码里两项都写了,服务还是连旧地址,日志里反复超时。
原因:ExchangeService的Url被手动赋值后,AutodiscoverUrl不会自动覆盖这个值,后续请求优先使用已设置的 URL。
解决:代码里只保留一种地址来源。推荐把 Autodiscover 得到的 URL 先缓存,再赋值给Url,下次启动时优先用缓存,连接失败再重新 Autodiscover。这样既有自动发现的灵活性,又不会每次启动都增加 DNS 开销。
4.3 Autodiscover 直接抛异常,日志里只有 302
现象:调用AutodiscoverUrl抛AutodiscoverLocalException,打开网络跟踪看到 HTTP 302 响应。
原因:EWS Managed API 出于安全考虑,默认不允许跟随重定向,遇到需要跳转的 Autodiscover 响应就终止。
解决:提供重定向校验回调,只放行 HTTPS 白名单地址。回调里至少要检查url.StartsWith("https://"),公司内部环境再加域名后缀匹配。不要把回调写成无条件返回 true,否则 DNS 劫持可以直接把你带到仿冒服务器。
4.4 用 Offset 翻页拉完整收件箱,拉到中间漏邮件
现象:第一次查 50 条,第二次把Offset设为 50,第三次设 100,结果发现某些邮件永远查不到,而另一些重复出现。
原因:邮件在持续进入,页间数据发生变化,Offset 分页基于一个不稳定的集合,新增邮件会挤占后继页的“槽位”。
解决:优先用ItemView分页时按DateTimeReceived或Subject排序,并且把当前书签定位到“最后一条邮件的时间戳 + 时间偏移量”,而不是纯数字 Offset。EWS 还支持基于SyncFolderItems的同步,适合做长期增量拉取。
4.5 FindItems 返回结果里 Body 是空的
现象:用FindItems查到了邮件,Subject正常,Body却是 null,绑定单个邮件后又能读到正文。
原因:FindItems默认只返回摘要属性,不会把大字段(Body、Attachments)一起带回来。
解决:读邮件详情时用EmailMessage.Bind,并在PropertySet里显式加上ItemSchema.Body。如果只是做列表展示,建议只取Subject、DateTimeReceived、Size,需要正文时再逐封 Bind,避免一次性把几百封大邮件全量拉下来。
这些坑写完,连接和操作的基线都清楚了。最后一章说一个能直接提升代码可靠性的进阶点。
5. 再进一步:可用性查询与翻页兜底的连接自检
5.1 用 GetUserAvailability 查询忙闲与会议建议
EWS 的 Availability 服务可以查一批人在某个时间窗口内的忙闲状态,并给出建议时间段。这个能力用在会议室预定、跨部门约会上很实用。代码非常直接:
List<AttendeeInfo> 参会人列表 = new List<AttendeeInfo> { new AttendeeInfo("zhangsan@example.com"), new AttendeeInfo("lisi@example.com") }; GetUserAvailabilityResults 忙闲结果 = 服务.GetUserAvailability( 参会人列表, new TimeWindow(DateTime.Now, DateTime.Now.AddHours(12)), AvailabilityData.FreeBusyAndSuggestions);第三个参数AvailabilityData.FreeBusyAndSuggestions表示既返回忙闲数据,也计算建议时间。如果只关心某个人有没有空,用FreeBusy就够了,响应更快。返回的AttendeesAvailability按传入顺序对应,逐个看ErrorCode是否为NoError,再遍历CalendarEvents。
5.2 给翻页循环加一个业务出口,而不是等它自己翻完
文档里的do...while翻页是标准写法,但我会在循环里加一个最大页数保护:
ItemView 视图 = new ItemView(50); FindItemsResults<Item> 分页结果; int 已拉取页数 = 0; do { 分页结果 = 服务.FindItems(WellKnownFolderName.Inbox, 视图); foreach (Item 邮件项 in 分页结果.Items) { // 在这里做业务处理,比如写入日志或落库 Console.WriteLine(邮件项.Subject); } 视图.Offset += 50; 已拉取页数++; } while (分页结果.MoreAvailable && 已拉取页数 < 100);这个保护看起来简单,但能避免因为某条数据反复出现在页边界导致死循环。真正的生产代码里,每一批处理完都应该做一次连接可用性判断:如果连续出错超过三次,就停止翻页,记录书签,等下次任务续跑。从那以后我每次写 EWS 集成,都会强制把“连接自检”放在最前面:先建ExchangeService,设置凭据,再发一条最小查询命令,用FindItems查收件箱前一条,确认 URL、凭据、证书三层全部通过,然后才进入邮件发送或日历读取逻辑。这个二十行的检查步骤,能省掉后面一小时的排查时间。希望帮到你。
本文还有配套的精品资源,点击获取