news 2026/10/23 5:08:37

C#操作Exchange邮箱:EWS Managed API入门与实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C#操作Exchange邮箱:EWS Managed API入门与实战指南

简介:这是一份面向C#开发者的英文技术文档,系统讲解如何借助微软EWS Managed API访问与操作Exchange邮箱,解决邮件收发、日历管理、联系人同步等场景的二次开发需求。文档从引用Microsoft.Exchange.WebServices.dll与相应命名空间开始,逐步说明测试服务器自签名证书的验证回调处理、ExchangeService对象的构建与凭据设置,以及手动指定URL或使用Autodiscover自动发现服务端点的两种连接方式,并配有查找收件箱邮件、发送邮件、操作日历任务等实际代码示例。资源包以单个doc文档呈现,共1个文件,压缩包大小147KB,内容聚焦核心API用法,兼顾证书排错与端点选择思路,适合具备基础C#知识、希望快速上手Exchange集成的开发者参考。该资源目前已有681人学习下载,具有较强的实践参考价值。

1. 直接用C#操作Exchange邮箱:一套EWS Managed API就够

当内部系统需要自动读收件箱、按模板回复邮件、把日历忙闲同步到业务页面时,拿Exchange EWS来对接比走SMTP/IMAP更顺手,因为邮件、日历、联系人、任务全在同一个对象模型里。这份英文版入门指南把EWS Managed API的用法拆成了可以直接编译的C#片段:从引用一个dll开始,到创建ExchangeService连接,再到发信、建周期约会、查忙闲,每一步都有对应的代码。适合正在给公司Exchange做自动化的.NET开发者,也适合想快速评估EWS方案可行性的同学。

2. 把EWS Managed API接入C#项目:引用程序集与三种连接配置

2.1 程序集引用与命名空间:两个文件就够

整个EWS Managed API实际只有一个核心程序集Microsoft.Exchange.WebServices.dll,外加一个提供给Visual Studio做智能提示的配套XML文件。这两个文件放哪、怎么引用,决定了项目能不能顺利编译。

我习惯在解决方案里建一个lib目录,把dll和xml按版本号分文件夹放好,然后在项目里通过浏览方式添加引用。这样做比直接拖进bin目录清爽,以后升级版本时只需要改引用路径,不用动源码。引用成功后,在代码文件顶部写入命名空间:

using Microsoft.Exchange.WebServices.Data;

这行using要放在所有需要操作EWS的.cs文件顶部。dll和xml必须放在同一目录下,否则Visual Studio的智能感知会失效,代码还是能编译,但写属性时没有自动补全,效率会明显下降。xml文件只在设计期起作用,发布上线时甚至可以不用拷贝到服务器。

2.2 创建ExchangeService:凭据、URL与Autodiscover的取舍

ExchangeService是EWS所有操作的入口,相当于一个封装好的HTTP客户端。创建它很简单,但连接参数怎么配,直接决定后面能不能连通。

ExchangeService service = new ExchangeService(); // 显式传域账号,适用于服务账号运行或非域内机器 service.Credentials = new WebCredentials("username", "password", "domain"); // WebCredentials只是NetworkCredential的封装,两种写法等价 service.Credentials = new NetworkCredential("username", "password", "domain");

在域内一台已登录的开发机上,不设置凭据时EWS会沿用当前Windows身份,调试阶段很省事。但后台服务跑的任务往往使用独立账号,显式传凭据才是稳妥做法。WebCredentials是EWS包自带的类型,内部就是对NetworkCredential的包装,二者可以互换,我统一用WebCredentials,少引一个命名空间。

URL的指定方式有两种,取舍要看你的运行环境:

// 方式一:手动指定EWS端点地址 service.Url = new Uri("https://mail.example.com/EWS/Exchange.asmx"); // 方式二:通过Autodiscover自动发现 service.AutodiscoverUrl("someone@example.com");

手动指定适合已经知道端点地址、且Exchange拓扑不频繁变化的环境;Autodiscover会根据邮箱地址自动解析出最合适的EWS端点,管理员调整了前端服务器后URL变了也不用改代码。我一般把Autodiscover返回的URL缓存到配置文件里,定期间隔重新探测一次,兼顾启动速度和端点时效性。特别要注意,手动设置Url和调用AutodiscoverUrl是互斥的,两个都来会导致行为不确定,代码里只保留一个分支。

提示:Autodiscover默认不跟随HTTP重定向,一旦检测到重定向就会抛异常。需要跟随重定向时,必须传入校验回调,并且只允许https的地址。

private static bool ValidateRedirectionUrlCallback(string url) { // 只放行https重定向,http的一律拒绝 return url.StartsWith("https://"); } service.AutodiscoverUrl("someone@example.com", ValidateRedirectionUrlCallback);

这个回调的返回值直接决定本次重定向是否被跟随。无条件return true是危险写法,DNS劫持场景下,Autodiscover请求可能被导到伪造的服务器,凭据直接泄露。我在回调里还会加一层域名白名单,URL同时满足https和内部域后缀才放行。

ExchangeService上还有容易忽略的UserAgent属性。有些Exchange网关会按UserAgent做流量治理或日志分组,默认的EWS标识有时会被误伤。项目初始化时顺手设置,出问题后查服务端日志也方便定位。

service.UserAgent = "InternalTools/1.0";

这个值只影响请求头,不影响认证逻辑。不设置的话,多个程序共用同一个Exchange时,服务端日志里很难区分请求来自哪个客户端。

2.3 测试服务器自签名证书:证书回调的正确写法

测试环境通常不装正式CA证书,而是用自签名证书部署Exchange前端。.NET默认的SSL校验不信任自签名证书,直接连接会报“基础连接已经关闭:无法为SSL/TLS安全通道建立信任关系”,连Autodiscover阶段都过不去。

解决思路是挂一个进程级的证书校验回调,让自签名证书能完成握手。

using System.Net.Security; using System.Security.Cryptography.X509Certificates; System.Net.ServicePointManager.ServerCertificateValidationCallback = (obj, certificate, chain, errors) => { // 测试环境临时放行,生产环境不要无条件返回true return true; };

这段代码挂的是进程级回调,影响范围是整个应用程序里的所有HTTPS请求,不只是EWS。我会把它收敛在一个初始化方法里,用配置项控制只在测试环境生效。更严谨的做法是:在回调里拿certificate对象对比测试服务器的证书指纹,只有指纹匹配才放行。

如果项目跑在.NET Framework 4.7.2之前的版本上,还需要显式提升TLS版本,否则新版Exchange会直接拒绝TLS 1.0的握手。

System.Net.ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

这一行加在初始化最前面,能避免一类“连接被服务器重置”的诡异报错。如果加了之后日志里仍是握手失败,优先查服务端的TLS策略配置,确认实际开放的协议版本。

3. 操作邮件、日历与通讯组:从Binding到发送的实战代码

3.1 Item与Folder的类层次:先搞懂对象模型再写代码

EWS Managed API把Exchange里的每一种数据都映射成了C#类。EmailMessage代表邮件,Appointment代表约会和会议,Task、Contact、ContactGroup、PostItem各有对应。这些类都继承自Item,Item再往上继承自ServiceObject。会议相关的MeetingRequest、MeetingResponse、MeetingCancellation则都是EmailMessage的子类。

这个继承关系决定了三个事情:Bind方法能返回什么类型、FindItems返回集合时能用哪些属性、以及什么时候该做类型判断。处理混合类型的文件夹时用Item统一接收,再按实际类型分支;处理明确类型时直接用EmailMessage或Appointment声明变量,代码更简洁。我先讲清楚这一层,后面写起来会顺很多。

3.2 绑定已知ID的邮件或文件夹

拿到一条邮件的唯一标识后,想读取它的完整内容,Bind是最直接的方式。

// 已知消息ID,绑定为EmailMessage EmailMessage message = EmailMessage.Bind(service, new ItemId("uniqueMessageId")); Console.WriteLine(message.Sender.Name);

Bind会向服务器发起一次请求,把所有第一类属性一次性加载到内存。之后直接访问Subject、Body、Sender这些属性就不会再产生网络调用。如果不确定这个ID对应的对象类型,就用Item.Bind再逐个判断。

Item item = Item.Bind(service, new ItemId("uniqueMessageId")); if (item is EmailMessage email) { Console.WriteLine(email.Sender.Name); } else if (item is Appointment appointment) { Console.WriteLine(appointment.Start); } else { // 其他类型按业务需要处理 }

这种多类型共存的写法在共享收件箱场景里很实用,比如一个公共邮箱里既有普通邮件又有会议请求。文件夹的绑定方式几乎一样。

// 绑定Inbox系统文件夹 Folder inbox = Folder.Bind(service, WellKnownFolderName.Inbox); // 绑定已知ID的文件夹 Folder customFolder = Folder.Bind(service, new FolderId("uniqueFolderId"));

WellKnownFolderName枚举覆盖Inbox、Calendar、Tasks、Contacts这些系统文件夹,不需要知道ID就能直接取到。业务自己创建的文件夹通常先查ID再绑定。

3.3 发送、回复、转发与删除邮件

发送一封新邮件,是这套API里最简单也最典型的操作。

EmailMessage message = new EmailMessage(service); message.Subject = "Hello world!"; message.Body = "Sent using the EWS Managed API."; message.ToRecipients.Add("someone@example.com"); message.SendAndSaveCopy();

SendAndSaveCopy会发送邮件,同时在发件箱保存一份副本;如果不留底,改成调用Send()。ToRecipients是RecipientCollection类型,除了Add邮箱地址,还可以Add带显示名的重载,这样收件箱里能看到姓名而不只是地址。

回复邮件的标准做法是先用CreateReply,再补内容。

ResponseMessage responseMessage = messageToReplyTo.CreateReply(true); responseMessage.BodyPrefix = "根据你的问题,我的回复如下:\n"; responseMessage.CcRecipients.Add("someone@example.com"); responseMessage.SendAndSaveCopy();

CreateReply里的bool参数为true表示回复所有人,false只回复发件人。BodyPrefix会追加到原邮件正文的上方,原邮件正文保留在下方,这是Exchange客户端回复时的常见格式。转发用Forward()或CreateForward(),写法和回复一致,区别只是收件人需要自己指定。

删除操作只有一个Delete方法,但模式要选对。

message.Delete(DeleteMode.HardDelete);

DeleteMode有三个值:HardDelete直接从Exchange数据库中物理清除,不可恢复;SoftDelete进可恢复项,管理员还能捞回来;MoveToDeletedItems就是普通删除,进回收站。业务代码里我默认用MoveToDeletedItems,只有明确要求不留痕迹的场景才用HardDelete。

3.4 创建周期约会与邀请与会者

日历场景的难点不在创建单次约会,而在周期规则。

Appointment appointment = new Appointment(service); appointment.Subject = "每周技术评审"; appointment.Body = "例行评审,请提前准备"; appointment.Start = new DateTime(2024, 1, 1, 18, 0, 0); appointment.End = appointment.Start.AddHours(2); // 每两周的周二和周四重复 appointment.Recurrence = new Recurrence.WeeklyPattern( new DateTime(2024, 1, 1), 2, DayOfWeek.Tuesday, DayOfWeek.Thursday); appointment.Save();

Recurrence.WeeklyPattern第一个参数是模式开始日期,第二个是间隔周数,后面是变长的星期几参数,传一个或多个DayOfWeek。模式开始日期最好与Start保持一致,如果两边不一致,Exchange会按模式开始日期推算后续发生实例,结果可能和你预期的首次开会时间对不上。

到这里约会还只是日历项,没有参与者。加上与会者并更新一次,它才变成会议。

appointment.RequiredAttendees.Add("someone@example.com"); appointment.Update(ConflictResolutionMode.AutoResolve);

RequiredAttendees中的地址会收到会议邀请。Update里的ConflictResolutionMode指定与会者时间冲突时如何处理,AutoResolve让Exchange自动解决,符合多数内部场景。想创建时就带参会者,也可以在第一次Save前就Add进去,一次保存完成。

3.5 创建个人通讯组列表与文件夹

个人通讯组列表存在当前用户的联系人文件夹里,适合工具内部引用一组固定收件人。

DistributionList distributionList = new DistributionList(service); distributionList.DisplayName = "项目团队"; distributionList.Members.Add(new DistributionListMember("测试用户A", "user.a@example.com")); distributionList.Save();

它和Exchange全局通讯组是两回事,创建、修改都只影响当前邮箱,不产生AD对象。成员用DistributionListMember表示,构造函数里可以同时填显示名和SMTP地址。

新建文件夹的操作更简单,指定父文件夹后保存。

Folder folder = new Folder(service); folder.DisplayName = "自动归档"; folder.Save(WellKnownFolderName.Inbox);

保存后folder.Id会被赋上服务器返回的标识,后续改名、移动、删除都靠这个ID。如果要在自定义文件夹下再建子文件夹,把Save的参数换成该文件夹的FolderId即可。

4. 搜索与分页:从Inbox前十条到复杂筛选

4.1 FindItems基础:ItemView与分页

搜索是EWS使用频率最高的功能。最简单的场景是取收件箱前十条。

FindItemsResults<Item> findResults = service.FindItems( WellKnownFolderName.Inbox, new ItemView(10)); foreach (Item item in findResults.Items) { Console.WriteLine(item.Subject); }

ItemView控制单次返回条数,上面这个写法默认从第0条开始取。FindItems返回的集合里是Item基类,Subject、DateTimeReceived这类公共属性可以直接访问;需要读收件人这类邮件专属属性,仍要类型判断后转成EmailMessage,后面避坑章节会展开说。

邮箱条目多了以后,分页取全量是个绕不开的问题。

ItemView view = new ItemView(50); FindItemsResults<Item> findResults; do { findResults = service.FindItems(WellKnownFolderName.Inbox, view); foreach (Item item in findResults.Items) { // 逐条处理,比如写入本地数据库 } view.Offset += 50; } while (findResults.MoreAvailable);

退出循环的唯一依据是MoreAvailable,它由服务端告诉客户端还有没有下一页。Offset的含义是已跳过的条数,每取完一批就加一个页面大小,语义上和页码翻页等价但更好理解。不要拿TotalCount当循环条件,它表示满足条件的总条数,集合变化时用它做边界容易多取或漏取。

4.2 SearchFilter组合:多条件OR、排序与属性裁剪

真正提效的是筛选、排序、裁剪一起上。

ItemView view = new ItemView(10); view.OrderBy.Add(ItemSchema.DateTimeReceived, SortDirection.Ascending); // 只取Id、主题和接收时间,避免把正文和附件拉到本地 view.PropertySet = new PropertySet( BasePropertySet.IdOnly, ItemSchema.Subject, ItemSchema.DateTimeReceived); FindItemsResults<Item> findResults = service.FindItems( WellKnownFolderName.Inbox, new SearchFilter.SearchFilterCollection( LogicalOperator.Or, new SearchFilter.ContainsSubstring(ItemSchema.Subject, "EWS"), new SearchFilter.ContainsSubstring(ItemSchema.Subject, "API")), view); Console.WriteLine("Total number of items found: " + findResults.TotalCount);

SearchFilterCollection是树形结构,LogicalOperator.Or表示下面几个条件满足其一即命中,改成And则需要同时满足。ContainsSubstring做的是子串匹配,大小写不敏感,不是全文搜索,只在指定的属性内匹配。这个例子等价于SQL里的subject like '%EWS%' or subject like '%API%'。

PropertySet裁剪很关键。默认的BasePropertySet.FirstClassProperties会把正文、附件元数据都拉回来,一次查询几千条时带宽和内存都会被占满。只需要主题和时间时,显式指定IdOnly加两个字段,响应体明显变小,速度也更快。排序字段加在view.OrderBy中,支持多字段组合,先按主字段排,相同再按次字段排。

4.3 日历搜索与模糊名称解析

文件夹搜索和日历搜索各有专属API。

FindFoldersResults folderResults = service.FindFolders( WellKnownFolderName.Inbox, new FolderView(int.MaxValue)); foreach (Folder folder in folderResults.Folders) { Console.WriteLine(folder.DisplayName); }

FolderView传int.MaxValue是一次性取完全部子文件夹,适合脚本类管理工具。生产环境里文件夹数量同样可能很多,稳妥做法是像ItemView一样分页,别总想着一次load全量。

日历搜索要查周期约会的具体发生实例,情况有些特殊。

CalendarFolder calendar = CalendarFolder.Bind(service, WellKnownFolderName.Calendar); FindItemsResults<Appointment> appointments = calendar.FindAppointments( new CalendarView(DateTime.Now, DateTime.Now.AddDays(7)));

CalendarView只接受起止时间,传两个DateTime就能拿到时间窗内的所有约会,包括周期约会被展开后的每一次occurrence。普通FindItems搜日历只会返回周期主项,不会展开每次发生;这里必须用CalendarFolder的FindAppointments。另外,CalendarView是按时间窗口展开的,天然适合查区间,不需要手动翻页,它也没有Offset属性可以设。

按人名找邮箱用ResolveName,适合做“输入姓氏自动补全收件人”的功能。

NameResolutionCollection nameResolutions = service.ResolveName( "张", NameResolutionSearchScope.ActiveDirectoryContacts, true); foreach (NameResolution nameResolution in nameResolutions) { Console.WriteLine(nameResolution.Mailbox.Name); Console.WriteLine(nameResolution.Mailbox.Address); }

NameResolutionSearchScope.ActiveDirectoryContacts表示同时查AD和联系人文件夹,顺序是先AD后联系人。第三个bool控制是否返回完整的联系人详情,只要邮箱和显示名时传false,能省一次详细查询的开销。

5. 避坑指南:EWS接入最常见的五个坑

接入EWS的过程中,文档里不会写但实际总会撞上的坑,按出现频率排序,集中在下面五条。

5.1 Autodiscover遇到重定向直接抛异常

现象:调用service.AutodiscoverUrl("someone@example.com")时邮箱地址确认无误,程序却抛出与重定向相关的异常,错误信息里通常带着“redirection response was not accepted”字样。

原因:EWS Managed API出于防DNS劫持的设计,默认不跟随任何重定向。测试环境的Autodiscover往往从http跳到https,于是这个保护机制被误触发,把正常跳转也拦了下来。

解决:改用带校验回调的重载,回调里只放行https开头的地址,再做一层域名白名单,不在白名单里的URL直接返回false。千万不要为了省事在回调里写return true。另一个连带问题是,有些代码把AutodiscoverUrl放在try-catch里吞掉异常,结果service.Url一直为空,后续请求的报错信息更隐蔽,排查方向直接跑偏。

5.2 时区不对,日历约会整体偏差数小时

现象:在服务器上创建一条18点的约会,Exchange日历里显示的是凌晨或隔天某个时间,每次都是整小时偏移。

原因:代码运行所在服务器时区和目标邮箱时区不一致时,new DateTime(2024, 1, 1, 18, 0, 0)会被服务器解释成自己的本地时间,Exchange再按邮箱时区做一次转换,于是出现固定时差。EWS本身不做隐式时区换算,它按你给的DateTime原样传输。

解决:创建约会前先确认服务器时区。常见做法是用TimeZoneInfo.ConvertTime把目标时区时间转成服务器本地时间后再赋值给Start和End;如果所有机器都在同一时区,最简单是让服务器时区与Exchange时区保持一致。DateTime.Kind也是一个变体,Kind为Utc和Unspecified的DateTime传到Exchange后的处理逻辑不同,赋值前可以先打印DateTime.Kind确认。

5.3 证书回调无条件return true,把安全防线拆了

现象:测试环境用ServerCertificateValidationCallback临时绕过了证书校验,代码上线时忘了删,生产环境里EWS连接被重定向到一个伪造的https站点,程序照样握手成功。

原因:这个回调是进程级的,return true等于告诉整个进程信任所有证书。它覆盖的不只是EWS连接,还包括程序里所有HTTPS请求。测试时看着解决了问题,上线后等于把证书链校验整个关掉。

解决:把证书回调放进只在测试环境执行的初始化方法,用配置文件的开关控制。回调内部不要直接返回true,而是校验certificate对象的指纹或颁发者,匹配测试服务器的证书才放行。生产环境保持默认校验逻辑。另一种常见写法是把回调直接挂在Main函数开头,上线时注释掉还好,漏删就是隐患;我习惯把证书策略单独抽成一个方法并用环境开关管理。

5.4 FindItems返回的Item基类,直接强转EmailMessage失败

现象:在foreach (Item item in findResults.Items)里写((EmailMessage)item).ToRecipients,运行时抛InvalidCastException,而且不是每次都抛,取决于收件箱里有没有非邮件类型的条目。

原因:FindItems不知道返回项的静态类型,统一按Item基类返回。收件箱里除了普通邮件,还可能有会议请求、投递回执、通知消息等类型,它们挂在EmailMessage的继承树下,但直接转基类以外的类型会失败。

解决:不要无条件强转。先判断item is EmailMessage再进分支,或者只读取Item基类上的公共属性。想确认实际类型,可以在异常发生前先打印item.GetType().FullName,同一封邮件在不同服务器上可能被识别成不同派生类型,打印出来最直观。

5.5 分页只取固定次数,邮件超过容量就漏数据

现象:用ItemView(50)写固定取两页的循环,测试收件箱只有80封邮件时看起来没问题,上线后收件箱超过100封,后面的邮件完全没被处理。

原因:把分页写成了“固定取前N批”,没根据服务端返回结果决定是否继续。ItemView只是单次数量的上限,不等于整个查询的结束条件。

解决:用do-while结构,每取完一批就更新view.Offset,循环条件用findResults.MoreAvailable。反面写法是while(pageCount < 3)这类固定轮数循环,测试环境数据量小看不出问题,线上邮件一多就漏。循环轮数永远由服务端的MoreAvailable来决定,不要自己拍脑袋。

6. 进阶:扩展属性与Availability服务,两个值得深挖的能力

基础功能跑通之后,有两个能力对内部系统价值很高:自定义扩展属性和Availability服务。前者让邮件对象能带上业务系统的字段,后者让会议安排不再靠人工来回对时间。

6.1 扩展属性:给邮件打自定义标签

扩展属性允许在标准属性之外给邮件加自定义字段,业务系统可以把单据号、流程状态直接存在邮件上。

Guid propertySetId = new Guid("{01638372-9F96-43b2-A403-B504ED14A910}"); ExtendedPropertyDefinition extendedPropertyDefinition = new ExtendedPropertyDefinition( propertySetId, "MyProperty", MapiPropertyType.String); EmailMessage message = new EmailMessage(service); message.SetExtendedProperty(extendedPropertyDefinition, "MyValue"); message.Save();

propertySetId建议按应用为单位生成一个专用GUID并复用,不要每个字段都新造一个,避免和其他应用写入的扩展属性冲突。读取时要把同一个definition带进PropertySet,否则绑定结果里不会加载它:

PropertySet propertySet = new PropertySet( BasePropertySet.FirstClassProperties, extendedPropertyDefinition); EmailMessage boundMessage = EmailMessage.Bind(service, new ItemId("uniqueMessageId"), propertySet); foreach (ExtendedProperty extendedProperty in boundMessage.ExtendedProperties) { Console.WriteLine(extendedProperty.Value); }

绑定后通过ExtendedProperties集合取回值。如果PropertySet里漏掉了extendedPropertyDefinition,这个集合就是空的,查不出任何自定义字段。

6.2 Availability服务:查忙闲与会议时间建议

Availability服务可以查询一组用户在某时间窗内的忙闲状态并给出会议时间建议,不需要对目标邮箱有访问权限。

List<AttendeeInfo> attendees = new List<AttendeeInfo>(); attendees.Add("someone@example.com"); attendees.Add("someoneelse@example.com"); GetUserAvailabilityResults results = service.GetUserAvailability( attendees, new TimeWindow(DateTime.Now, DateTime.Now.AddHours(12)), AvailabilityData.FreeBusyAndSuggestions); foreach (AttendeeAvailability availability in results.AttendeesAvailability) { if (availability.ErrorCode == ServiceError.NoError) { foreach (CalendarEvent calendarEvent in availability.CalendarEvents) { Console.WriteLine("Start: " + calendarEvent.StartTime); Console.WriteLine("End: " + calendarEvent.EndTime); } } }

GetUserAvailability第一个参数是与会者列表,AttendeeInfo支持从字符串隐式转换,直接Add邮箱地址即可。TimeWindow界定查询区间,第三个参数决定返回忙闲数据、时间建议还是两者都要。每个与会者对应一个AttendeeAvailability,先判断ErrorCode再读CalendarEvents,因为不是所有人都有权限返回忙闲。

会议建议直接读Suggestions集合,按天分组:

foreach (Suggestion suggestion in results.Suggestions) { Console.WriteLine("Suggested day: " + suggestion.Date); foreach (TimeSuggestion timeSuggestion in suggestion.TimeSuggestions) { Console.WriteLine("Suggested time: " + timeSuggestion.MeetingTime); } }

Suggestion按天给整体质量评分,TimeSuggestion里是具体建议时段。实际项目里我拿这套接口做会议室预定辅助,把参会人的忙闲和会议室日历放进同一个Availability查询,一次性算出可预订时段,比让用户来回对时间高效得多。

以前我把扩展属性只当个标签用,后来发现它还能存外部系统的单据号,邮件系统和业务系统就靠这一个字段对账。从那以后,我每次接EWS项目都会先花几分钟想清楚:哪些数据要落进Exchange,哪些只用它当通道。这套英文版指南加示例代码,按第2章到第6章的顺序过一遍,收信、发信、查忙闲三个场景基本就能搭起来,剩下细节边用边补。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 5:07:31

VisionPro中文版教程:机器视觉从选型到落地全攻略

简介&#xff1a;康耐视VisionPro v5.01中文版操作教程是一份系统讲解机器视觉应用开发的完整版PDF资料&#xff0c;面向自动化设备调试、产线视觉检测等相关技术人员&#xff0c;零基础新手可通过其搭建入门框架&#xff0c;有经验者也适合快速查找工具使用场景。教程以QuickB…

作者头像 李华
网站建设 2026/10/23 5:05:18

具身智能进阶:让 MicroDuck 自己学会荡秋千

前置知识&#xff1a;已阅读 HelloWorld 解读。本文深入解读摆动旋转&#xff08;Swing&#xff09;任务——MicroDuck 坐在两根软绳悬挂的摆座上&#xff0c;从静止出发&#xff0c;只靠自己的身体动作把摆幅越荡越大。 一、整体流程概览 这个 Notebook 演示了**摆动旋转&…

作者头像 李华
网站建设 2026/10/23 5:01:58

面对 AI 大模型智能涌现,如何从技术层面保护好隐私与数据安全?

评分口径&#xff1a;公开权威机构数据核对 六类典型任务实测&#xff08;多表合并、异常值排查、口径清洗、周期报表、可编辑汇报材料、处理过程可控性&#xff09;&#xff0c;满分 5 分一、评测背景与口径说明大模型带来的智能涌现&#xff0c;让"一句话把一张乱表变成…

作者头像 李华
网站建设 2026/10/23 5:00:40

没收手机老人偷塞、孩子半夜氪金?这套数字戒断法让全家停止内耗

我从来没想到&#xff0c;管一部手机&#xff0c;会比管一个公司还累。事情是从儿子拿到第一部iPhone开始的。起初只是说“查资料”“交作业”&#xff0c;后来变成“再玩五分钟”&#xff0c;再后来&#xff0c;我发现他半夜被窝里有光。我以为只要把手机收走就万事大吉&#…

作者头像 李华