VSTO Outlook 获取新/回/转发窗口To/Bc/Cc栏最新输入邮箱地址
VSTO Outlook 收件人字段失焦获取最新输入邮箱实现方案
核心实现思路
不需要硬找输入框控件句柄(兼容性极差,不同Outlook版本控件结构会变),通过Outlook对象模型自带的Inspector窗口事件+收件人集合快照对比的方式实现,覆盖新建邮件、回复、转发所有邮件编辑场景,兼容Outlook 2016及以上版本。
注:你提到的「Bc」字段是Outlook密送字段,对应对象模型里的
olBCC类型收件人,逻辑和To、Cc字段完全一致。
具体实现步骤
1. 全局注册邮件编辑窗口捕获事件
在VSTO外接程序的ThisAddIn_Startup方法中,注册Inspectors集合的NewInspector事件,所有打开的邮件编辑窗口(新建/回复/转发均会触发)都会被捕获,过滤出非MailItem的窗口(比如约会、任务编辑窗口)不做处理。
2. 给每个邮件编辑窗口绑定焦点相关事件
对捕获到的MailItem编辑窗口,做两个状态缓存:
- 缓存当前窗口To/Cc/Bcc三个字段的收件人快照(焦点进入字段时更新)
- 绑定Inspector的
Deactivate事件(编辑区整体失焦时触发,覆盖三个收件人字段失焦场景),同时可配合Word编辑器的SelectionChange事件辅助判断是否在收件人输入区,精准识别字段失焦时机。
3. 失焦时对比快照提取最新收件人
失焦触发时,Outlook会自动解析用户输入的未识别收件人文本,此时遍历对应字段的Recipients集合,和焦点进入时存的快照做差集,差集中按添加顺序取最后一个对象,就是用户刚刚输入的最新收件人,提取其SMTP邮箱地址即可。
可直接复用的核心代码
using Outlook = Microsoft.Office.Interop.Outlook; using System.Collections.Generic; using System.Runtime.InteropServices; using System; private Outlook.Inspectors _inspectors; // 缓存每个邮件窗口的收件人快照:key为MailItem条目ID,value为三个字段的邮箱列表快照 private Dictionary<string, (List<string> toMails, List<string> ccMails, List<string> bccMails)> _recipientSnapshot = new Dictionary<string, (List<string> toMails, List<string> ccMails, List<string> bccMails)>(); private void ThisAddIn_Startup(object sender, System.EventArgs e) { _inspectors = Application.Inspectors; _inspectors.NewInspector += Inspectors_NewInspector; } private void Inspectors_NewInspector(Outlook.Inspector Inspector) { // 只处理邮件编辑窗口 if (Inspector.CurrentItem is not Outlook.MailItem mailItem) return; string itemId = mailItem.EntryID; // 初始化快照 UpdateRecipientSnapshot(mailItem); // 绑定窗口失焦事件 Inspector.Deactivate += () => { // 失焦时获取各字段最新收件人 var latestTo = GetLatestRecipient(mailItem, Outlook.OlMailRecipientType.olTo); var latestCc = GetLatestRecipient(mailItem, Outlook.OlMailRecipientType.olCC); var latestBcc = GetLatestRecipient(mailItem, Outlook.OlMailRecipientType.olBCC); // 这里写自定义业务逻辑,处理拿到的最新邮箱地址 if (!string.IsNullOrEmpty(latestTo)) { // To字段最新输入邮箱的处理逻辑 } if (!string.IsNullOrEmpty(latestCc)) { // Cc字段最新输入邮箱的处理逻辑 } if (!string.IsNullOrEmpty(latestBcc)) { // Bcc字段最新输入邮箱的处理逻辑 } // 处理完更新快照,供下次对比 UpdateRecipientSnapshot(mailItem); }; // 窗口关闭时清理缓存和COM对象 ((Outlook.ItemEvents_10_Event)mailItem).Close += (ref bool Cancel) => { if (_recipientSnapshot.ContainsKey(itemId)) _recipientSnapshot.Remove(itemId); Marshal.ReleaseComObject(mailItem); }; } /// <summary> /// 更新指定邮件的收件人快照 /// </summary> private void UpdateRecipientSnapshot(Outlook.MailItem mailItem) { var toMails = new List<string>(); var ccMails = new List<string>(); var bccMails = new List<string>(); foreach (Outlook.Recipient recip in mailItem.Recipients) { string smtpAddress = GetSmtpAddress(recip); switch (recip.Type) { case (int)Outlook.OlMailRecipientType.olTo: toMails.Add(smtpAddress); break; case (int)Outlook.OlMailRecipientType.olCC: ccMails.Add(smtpAddress); break; case (int)Outlook.OlMailRecipientType.olBCC: bccMails.Add(smtpAddress); break; } Marshal.ReleaseComObject(recip); } _recipientSnapshot[mailItem.EntryID] = (toMails, ccMails, bccMails); } /// <summary> /// 获取指定字段最新输入的收件人邮箱 /// </summary> private string GetLatestRecipient(Outlook.MailItem mailItem, Outlook.OlMailRecipientType recipType) { if (!_recipientSnapshot.TryGetValue(mailItem.EntryID, out var snapshot)) return null; List<string> oldList = recipType switch { Outlook.OlMailRecipientType.olTo => snapshot.toMails, Outlook.OlMailRecipientType.olCC => snapshot.ccMails, Outlook.OlMailRecipientType.olBCC => snapshot.bccMails, _ => new List<string>() }; string latestAddr = null; foreach (Outlook.Recipient recip in mailItem.Recipients) { if (recip.Type != (int)recipType) { Marshal.ReleaseComObject(recip); continue; } string currentAddr = GetSmtpAddress(recip); if (!oldList.Contains(currentAddr)) { latestAddr = currentAddr; } Marshal.ReleaseComObject(recip); } return latestAddr; } /// <summary> /// 统一获取收件人SMTP地址,兼容Exchange地址类型 /// </summary> private string GetSmtpAddress(Outlook.Recipient recipient) { if (recipient.AddressEntry == null) return recipient.Name; // 未解析的收件人返回用户输入的原始文本 try { if (recipient.AddressEntry.Type == "EX") { var exUser = recipient.AddressEntry.GetExchangeUser(); string addr = exUser?.PrimarySmtpAddress; if (exUser != null) Marshal.ReleaseComObject(exUser); return addr ?? recipient.Address; } return recipient.Address; } finally { Marshal.ReleaseComObject(recipient.AddressEntry); } } private void ThisAddIn_Shutdown(object sender, System.EventArgs e) { // 释放COM对象 if (_inspectors != null) Marshal.ReleaseComObject(_inspectors); _recipientSnapshot.Clear(); }
注意事项
- 所有Outlook COM对象(Recipient、MailItem、AddressEntry等)用完必须手动释放,否则会导致Outlook内存泄漏、进程无法正常退出
- 如果需要更精准的单字段失焦判断(比如只在To字段失焦时触发,Cc/Bcc失焦不触发),可以通过Win32 API获取当前焦点控件的类名,判断焦点离开的是哪个收件人输入框再做对应处理,上述核心的快照对比、邮箱提取逻辑无需修改
- 若用户输入的收件人无法被Outlook解析(比如输入无效邮箱直接失焦),代码会返回用户输入的原始文本,不会抛异常
- 密送(Bcc)字段默认在新邮件窗口隐藏,用户手动调出该字段后逻辑会自动生效,不需要额外处理
内容的提问来源于stack exchange,提问作者Willy
相关产品推荐
相关产品推荐

