C# 调用邮箱应用发送带附件的邮件
<h1 id="c调用邮箱应用发送带附件的邮件">C# 调用邮箱应用发送带附件的邮件</h1><h2 id="邮件的几大要素">邮件的几大要素</h2>
<ul>
<li>发件人 From</li>
<li>收件人(主要收件人 To,抄送 CC,密送 BCC)</li>
<li>主题 Subject</li>
<li>正文 Body</li>
<li>附件 Attachments</li>
</ul>
<h2 id="smtpclient和mailkit">SmtpClient 和 MailKit</h2>
<p>如果有邮箱服务器并且已知发件人邮箱和密码,可以通过 C# 自带的 SmtpClient 或者使用开源库 MailKit</p>
<h2 id="调用第三方邮箱应用">调用第三方邮箱应用</h2>
<p>C# 自带的 MailMessage 类中的 Attachments 会直接打开文件流,且没有属性可以获取文件路径</p>
<p>我们可以创建一个简单的邮件信息类,调用第三方邮箱客户端一般不需要发件人,可去掉发件人属性</p>
<pre><code class="language-csharp">using System.Collections.Generic;
using System.Net.Mail;
public sealed class MailInfo
{
// /// <summary>发件人</summary>
// public MailAddress From { get; set; }
/// <summary>主要收件人</summary>
public List<MailAddress> Recipients { get; } = new List<MailAddress>();
/// <summary>抄送收件人</summary>
public List<MailAddress> CcRecipients { get; } = new List<MailAddress>();
/// <summary>密送收件人</summary>
public List<MailAddress> BccRecipients { get; } = new List<MailAddress>();
/// <summary>主题</summary>
public string Subject { get; set; }
/// <summary>正文</summary>
public string Body { get; set; }
/// <summary>附件文件列表</summary>
/// <remarks>Key 为显示文件名, Value 为文件路径</remarks>
public Dictionary<string, string> Attachments { get; } = new Dictionary<string, string>();
}
</code></pre>
<h3 id="mailto协议">mailto 协议</h3>
<p>mailto 是全平台支持的协议,支持多个收件人,抄送和密送,但不支持添加附件</p>
<h4 id="mailto关联应用">mailto 关联应用</h4>
<p>在 Windows 上使用 mailto 会调用其关联应用,未设置关联应用时,会弹出打开方式对话框询问使用什么应用打开</p>
<p><img src="https://img2024.cnblogs.com/blog/1823651/202507/1823651-20250729155550674-133004506.png" alt="image" loading="lazy"></p>
<p>关联注册表位置<br>
<code>HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\Shell\Associations\UrlAssociations\mailto\UserChoice</code></p>
<pre><code class="language-csharp">// 常见的邮箱应用 mailto ProgID
const string OutlookNewProgID = "AppXbx2ce4vcxjdhff3d1ms66qqzk12zn827"; // Outlook(New)
const string EMClientProgID = "eM Client.Url.mailto"; // eM Client
const string ThunderbirdProgID = "Thunderbird.Url.mailto"; // Mozilla Thunderbird
const string MailMasterProgID = "MailMaster"; // 网易邮箱大师
/// <summary>查找 mailto 协议关联的邮箱应用 ProgID</summary>
private static string FindMailToClientProgID()
{
// Win10 以上支持 AssocQueryString 查找 ProgID, 为兼容低版本使用注册表查询
// return NativeMethods.AssocQueryString(AssocStr.ProgID, "mailto");
const string keyPath = @"HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\Shell\Associations\UrlAssociations\mailto\UserChoice";
return Registry.GetValue(keyPath, "ProgId", null)?.ToString();
}
/// <summary>判断是否是 Outlook 关联的 ProgID</summary>
private static bool IsOutlookProgID(string progID)
{
var st = StringComparison.OrdinalIgnoreCase;
return progID.IndexOf("Outlook", st) >= 0 // Outlook(Classic) 版本相关,如 Outlook.URL.mailto.15
|| progID.Equals(OutlookNewProgID, st);
}
</code></pre>
<h4 id="mailto标准">mailto 标准</h4>
<p>语法:<code>mailto:sAddress</code><br>
示例:<code>mailto:example@to.com?subject=Test%20Subject</code></p>
<p>主要收件人写在 sAddress,抄送、密送、主题和正文都放在 sHeaders 里面,需要对所有 URL 保留字符进行编码转义</p>
<p>大部分邮箱应用都使用较新的 RFC 6068 标准(收件人、抄送、密送使用逗号分隔),且部分应用同时兼容分号和逗号</p>
<p>但是 Microsoft Outlook 还在使用着比较旧的 RFC 2368 标准(收件人、抄送、密送使用分号分隔)</p>
<p>故当关联应用为 Outlook 时,包括 Classic 版本和 UWP 新版,都无法正确解析逗号连接的多个收件人、抄送、密送</p>
<p>Classic 版本支持的 COM Interop 方式中也是使用的分号分隔</p>
<p>另外 PDF 表单 JavaScript 动作中 mailDoc、mailForm 等发送邮件的方法也是使用的分号分隔符</p>
<p>因此我们可以给上文中的 MailInfo 类添加几个获取指定分隔符连接的收件人地址字符串的方法</p>
<pre><code class="language-csharp">/// <summary>获取指定分隔符连接的收件地址</summary>
/// <param name="separator">遵循 mailto RFC 6068 规范默认为逗号,部分邮箱客户端支持逗号和分号,
/// <para>但 Outlook 仅支持分号; PDF 表单 JavaScript 动作中使用分号</para></param>
public string GetTO(string separator = ",")
{
return string.Join(separator, Recipients.ToArray());
}
/// <summary>获取指定分隔符连接的抄送地址</summary>
public string GetCC(string separator = ",")
{
return string.Join(separator, CcRecipients.ToArray());
}
/// <summary>获取指定分隔符连接的密送地址</summary>
public string GetBCC(string separator = ",")
{
return string.Join(separator, BccRecipients.ToArray());
}
</code></pre>
<h4 id="调用mailto关联邮箱">调用 mailto 关联邮箱</h4>
<pre><code class="language-csharp">/// <summary>通过 mailto 协议调用默认邮箱客户端发送邮件</summary>
/// <remarks>不支持附件, 支持 Outlook(New)</remarks>
public static bool SendByProtocol(MailInfo info)
{
bool isOutlook = IsOutlookProgID(FindMailToClientProgID());
string separator = isOutlook ? ";" : ","; // Outlook 仅支持分号, 其他客户端支持标准的逗号
var url = new StringBuilder("mailto:");
url.Append(info.GetTO(separator));
url.Append("?");
string cc = info.GetCC(separator);
string bcc = info.GetBCC(separator);
if (!string.IsNullOrEmpty(cc))
{
url.Append($"cc={Uri.EscapeDataString(cc)}&");
}
if (!string.IsNullOrEmpty(bcc))
{
url.Append($"bcc={Uri.EscapeDataString(bcc)}&");
}
if (!string.IsNullOrEmpty(info.Subject))
{
url.Append($"subject={Uri.EscapeDataString(info.Subject)}&");
}
if (!string.IsNullOrEmpty(info.Body))
{
url.Append($"body={Uri.EscapeDataString(info.Body)}&");
}
url.Remove(url.Length - 1, 1);
var startInfo = new ProcessStartInfo
{
FileName = url.ToString(),
UseShellExecute = true,
};
try
{
Process.Start(startInfo);
return true;
}
catch
{
return false;
}
}
</code></pre>
<h3 id="win32mapi">Win32 MAPI</h3>
<p>Windows 定义了 MAPI 接口供第三方邮箱应用实现集成,例如 Outlook(Classic)、eM Client、Thunderbird</p>
<p>C# 中可以使用 MAPISendMail 进行调用,需要注意不一定成功,可能会遇到未知的<code>MAPI_E_FAILURE</code>错误</p>
<p>另外 MAPI 方式支持设置是否显示 UI (<code>MAPI_DIALOG</code>、<code>MAPI_DIALOG_MODELESS</code>、<code>MAPI_LOGON_UI</code>)</p>
<p>可以为上文中的 MailInfo 类添加一个是否显示 UI 的属性</p>
<pre><code class="language-csharp">/// <summary>是否不显示UI自动发送, 至少需要一名收件人</summary>
public bool WithoutUI { get; set; }
</code></pre>
<h4 id="mapi关联应用">MAPI 关联应用</h4>
<p>支持 MAPI 的邮箱应用一般会在<code>{HKLM|HKCU}\SOFTWARE\Clients\Mail</code>下写入子项</p>
<p>通过修改 Mail 项默认键值修改默认 MAPI 邮箱,HKCU 优先,键值需要与 Mail 子项名称一致</p>
<pre><code class="language-csharp">/// <summary>查找 MAPI 邮箱客户端</summary>
private static string FindMAPIClientName()
{
const string MapiKeyPath = @"Software\Clients\Mail";
using (var cuKey = Registry.CurrentUser.OpenSubKey(MapiKeyPath))
using (var lmKey = Registry.LocalMachine.OpenSubKey(MapiKeyPath))
{
var cuKeyNames = cuKey?.GetSubKeyNames() ?? new string;
var lmKeyNames = lmKey?.GetSubKeyNames() ?? new string;
// HKCU 可获取 HKLM 子健, HKLM 不可反向获取 HKCU 子健
cuKeyNames = cuKeyNames.Concat(lmKeyNames).ToArray();
string cuValue = cuKey?.GetValue(null)?.ToString();
if (cuKeyNames.Contains(cuValue, StringComparer.OrdinalIgnoreCase))
{
return cuValue;
}
string lmValue = lmKey?.GetValue(null)?.ToString();
if (lmKeyNames.Contains(lmValue, StringComparer.OrdinalIgnoreCase))
{
return lmValue;
}
}
return null;
}
</code></pre>
<h4 id="调用mapi关联邮箱">调用 MAPI 关联邮箱</h4>
<p>文件系统对象右键菜单的发送到子菜单中的就是调用的 MAPI 关联邮箱</p>
<p>未设置 MAPI 关联邮箱时调用会弹窗提示,如果 Mail 项中<code>PreFirstRun</code>键值不为空,则弹窗优先显示其内容,*分隔内容和标题</p>
<p>但此弹窗内容会误导用户,因为控制面板默认程序中只能设置 mailto 关联邮箱而不能设置 MAPI 关联邮箱,两者无关</p>
<p><img src="https://img2024.cnblogs.com/blog/1823651/202507/1823651-20250729155644306-1628775417.png" alt="image" loading="lazy"><br>
<img src="https://img2024.cnblogs.com/blog/1823651/202507/1823651-20250729155650521-667082313.png" alt="image" loading="lazy"></p>
<p>另外建议异步调用,否则外部出错可能会卡死进程</p>
<p>比如同时安装了 Outlook(Classic) 和 Outlook(New) 且启用 New 时,Outlook(Classic)后台启动后会调起 Outlook(New)并结束自身,提前关闭了 Outlook(New) 主窗口, 或设置了收件人、抄送、密送</p>
<p><img src="https://img2024.cnblogs.com/blog/1823651/202507/1823651-20250729155708841-1617718329.png" alt="image" loading="lazy"></p>
<p>而且下文中的 Outlook COM 和命令行方式也都是只支持 Classic 不支持 New,所以我们需要一个判断是否启用了 Outlook(New) 的方法</p>
<p>这里我们可以使用 AssocQueryString 根据 ProgID 获取其友好名称来判断是否安装了新版 Outlook,上文代码中也提到了 Win10 以上系统可以用 AssocQueryString 直接查询 mailto 关联的 ProgID,而下文中也会用其根据 ProgID 获取关联可执行文件路径</p>
<pre><code class="language-csharp">/// <summary>是否同时安装了 Outlook Classic 和 New 两个版本,且启用 New</summary>
public static bool IsUseNewOutlook()
{
string name = NativeMethods.AssocQueryString(AssocStr.FriendlyAppName, OutlookNewProgID);
bool existsNew = name.Equals("Outlook", StringComparison.OrdinalIgnoreCase);
if (existsNew)
{
string regPath = @"HKEY_CURRENT_USER\SOFTWARE\Microsoft\Office\16.0\Outlook\Preferences";
bool useNew = Convert.ToInt32(Registry.GetValue(regPath, "UseNewOutlook", 0)) == 1;
if (useNew)
{
return true;
}
}
return false;
}
</code></pre>
<p>另外如果只安装了 Outlook(New)(Win11 默认预装)的情况下,无法通过 MAPI 方式调起,如若可获知 Outlook(Classic) 是如何启动 Outlook(New) 即可有方法单独启动 Outlook(New)。现今未找到只安装了 Outlook(New) 创建带附件邮件的方法</p>
<pre><code class="language-csharp">const string OutlookClientName = "Microsoft Outlook";
/// <summary>通过 Win32 MAPI 发送邮件</summary>
/// <remarks>⚠: 调用 MAPI 方式是同步执行,外部出错可能会卡死进程,
/// <para>比如同时安装了 Outlook(Classic) 和 Outlook(New) 且启用 New 时,</para>
/// <para>提前关闭了 Outlook(New) 主窗口, 或设置了收件人、抄送、密送</para></remarks>
public static bool SendByMAPI(MailInfo info)
{
var msg = new MapiMessage
{
subject = info.Subject,
noteText = info.Body,
};
var recipients =
info.Recipients.Select(x => MapiRecipDesc.Create(x, RecipClass.TO)).Concat(
info.CcRecipients.Select(x => MapiRecipDesc.Create(x, RecipClass.CC))).Concat(
info.BccRecipients.Select(x => MapiRecipDesc.Create(x, RecipClass.BCC))).ToArray();
if (recipients.Length > 0)
{
// 测试设置了收件人、抄送、密送 Outlook(New) 会卡住
if (OutlookClientName.Equals(FindMAPIClientName(), StringComparison.OrdinalIgnoreCase) && IsUseNewOutlook())
return false;
IntPtr pRecips = NativeMethods.GetStructArrayPointer(recipients);
if (pRecips != IntPtr.Zero)
{
msg.recips = pRecips;
msg.recipCount = recipients.Length;
}
}
var attachments = info.Attachments.Select(x => MapiFileDesc.Create(x.Value, x.Key)).ToArray();
if (attachments.Length > 0)
{
IntPtr pFiles = NativeMethods.GetStructArrayPointer(attachments);
if (pFiles != IntPtr.Zero)
{
msg.files = pFiles;
msg.fileCount = attachments.Length;
}
}
var flags = MapiFlags.ForceUnicode;
if (!(info.WithoutUI && info.Recipients.Count > 0))
{
flags |= MapiFlags.DialogModeless | MapiFlags.LogonUI;
}
try
{
var error = NativeMethods.MAPISendMail(IntPtr.Zero, IntPtr.Zero, msg, flags, 0);
if (error == MapiError.UnicodeNotSupported)
{
flags &= ~MapiFlags.ForceUnicode; // 不支持 Unicode 时移除标志
error = NativeMethods.MAPISendMail(IntPtr.Zero, IntPtr.Zero, msg, flags, 0);
}
return error == MapiError.Success || error == MapiError.UserAbort;
}
finally
{
NativeMethods.FreeStructArrayPointer<MapiRecipDesc>(msg.recips, recipients.Length);
NativeMethods.FreeStructArrayPointer<MapiFileDesc>(msg.files, attachments.Length);
}
}
</code></pre>
<details open="">
<summary>用到的本机方法、结构体、枚举</summary>
<pre><code class="language-csharp">static class NativeMethods
{
public static extern MapiError MAPISendMail(IntPtr session, IntPtr hWndParent, MapiMessage message, MapiFlags flags, int reserved);
public static extern int AssocQueryString(AssocFlags assocFlag, AssocStr assocStr, string pszAssoc, string pszExtra, StringBuilder pszOut, ref int pcchOut);
public static string AssocQueryString(AssocStr type, string assocStr, AssocFlags flags = AssocFlags.None)
{
int length = 0;
AssocQueryString(flags, type, assocStr, null, null, ref length); // 获取长度
var sb = new StringBuilder(length);
AssocQueryString(flags, type, assocStr, null, sb, ref length);
return sb.ToString();
}
/// <summary>获取结构体数组指针</summary>
public static IntPtr GetStructArrayPointer<T>(T[] array) where T : struct
{
IntPtr hglobal = IntPtr.Zero;
int copiedCount = 0;
try
{
int size = Marshal.SizeOf(typeof(T));
hglobal = Marshal.AllocHGlobal(size * array.Length);
for (int i = 0; i < array.Length; i++)
{
IntPtr ptr = new IntPtr(hglobal.ToInt64() + i * size);
Marshal.StructureToPtr(array, ptr, false);
copiedCount++;
}
}
catch
{
FreeStructArrayPointer<T>(hglobal, copiedCount);
throw;
}
return hglobal;
}
/// <summary>释放结构体数组指针</summary>
public static void FreeStructArrayPointer<T>(IntPtr ptr, int count) where T : struct
{
if (ptr != IntPtr.Zero && count > 0)
{
int size = Marshal.SizeOf(typeof(T));
for (int i = 0; i < count; i++)
{
IntPtr itemPtr = new IntPtr(ptr.ToInt64() + i * size);
Marshal.DestroyStructure(itemPtr, typeof(T));
}
Marshal.FreeHGlobal(ptr);
}
}
}
struct MapiMessage
{
public int reserved;
public string subject;
public string noteText;
public string messageType;
public string dateReceived;
public string conversationID;
public int flags;
public IntPtr originator;
public int recipCount;
public IntPtr recips;
public int fileCount;
public IntPtr files;
}
struct MapiRecipDesc
{
public int reserved;
public RecipClass recipClass;
public string name;
public string address;
public int eIDSize;
public IntPtr entryID;
public static MapiRecipDesc Create(MailAddress address, RecipClass recipClass = RecipClass.TO)
{
var result = new MapiRecipDesc
{
name = address.DisplayName,
address = address.Address,
recipClass = recipClass,
};
if (string.IsNullOrEmpty(result.name))
{
// Outlook name 不可为空, em Client 可设 address 或 name
result.name = result.address;
}
return result;
}
}
struct MapiFileDesc
{
public int reserved;
public int flags;
public int position;
public string pathName;
public string fileName;
public IntPtr fileType; // MapiFileTagExt
public static MapiFileDesc Create(string filePath, string fileName = null)
{
return new MapiFileDesc
{
pathName = filePath,
fileName = fileName ?? Path.GetFileName(filePath),
position = -1, // 不指示附件位置
};
}
}
enum MapiFlags
{
LogonUI = 0x1,
NewSession = 0x2,
Dialog = 0x8,
DialogModeless = 0x4 | Dialog,
ForceUnicode = 0x40000,
}
enum MapiError
{
/// <summary>成功</summary>
Success = 0,
/// <summary>用户中止</summary>
UserAbort = 1,
/// <summary>发生一个或多个未指定错误</summary>
Failure = 2,
/// <summary>登录失败</summary>
LoginFailure = 3,
/// <summary>内存不足</summary>
InsufficientMemory = 5,
/// <summary>文件附件太多</summary>
TooManyFiles = 9,
/// <summary>收件人太多</summary>
TooManyRecipients = 10,
/// <summary>找不到附件</summary>
AttachmentNotFound = 11,
/// <summary>无法打开附件</summary>
AttachmentOpenFailure = 12,
/// <summary>收件人未显示在地址列表中</summary>
UnknownRecipient = 14,
/// <summary>收件人类型错误</summary>
BadRecipient = 15,
/// <summary>消息中文本太大</summary>
TextTooLarge = 18,
/// <summary>收件人与多个收件人描述符结构匹配,且未设置 MAPI_DIALOG</summary>
AmbiguousRecipient = 21,
/// <summary>一个或多个收件人无效</summary>
InvalidRecips = 25,
/// <summary>指定了 MAPI_FORCE_UNICODE 标志,但不支持 Unicode</summary>
UnicodeNotSupported = 27,
/// <summary>附件太大</summary>
AttachmentTooLarge = 28,
}
enum AssocFlags
{
None = 0,
InitNoreMapClsid = 0x1,
InitByExeName = 0x2,
InitDefaultToStar = 0x4,
InitDefaultToFolder = 0x8,
NoUserSettings = 0x10,
NotRunCate = 0x20,
Verify = 0x40,
RemapRunDll = 0x80,
NoFixups = 0x100,
IgnoreBaseClass = 0x200,
InitIgnoreUnknown = 0x400,
InitFixedProgID = 0x800,
IsProtocol = 0x1000,
InitForFile = 0x2000
}
enum AssocStr
{
Command = 1,
Executable,
FriendlyDocName,
FriendlyAppName,
NoOpen,
ShellNewValue,
DDECommand,
DDEIfExec,
DDEApplication,
DDEToPIC,
InfoTip,
QuickTip,
TileInfo,
ContentType,
DefaultIcon,
ShellExtension,
DropTarget,
DelegateExecute,
SupportedURIProtocols,
ProgID,
AppID,
AppPublisher,
AppIconReference
}
</code></pre>
</details>
<h4 id="调用其他mapi邮箱">调用其他 MAPI 邮箱</h4>
<p>已知第三方邮箱应用包含 MAPI 相关导出函数的 dll 位置时,可通过 GetProcAddress 来调用</p>
<pre><code class="language-csharp">
extern static IntPtr LoadLibrary(string lpLibFileName);
extern static IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
extern static bool FreeLibrary(IntPtr hLibModule);
// 定义与 MAPISendMail 方法相同签名的委托
delegate MapiError MAPISendMailDelegate(IntPtr session, IntPtr hWndParent, MapiMessage message, MapiFlags flags, int reserved);
public static bool SendMail(MapiMessage msg, MapiFlags flags, string dllPath)
{
IntPtr hLib = LoadLibrary(dllPath);
if(hLib != IntPtr.Zero)
{
try
{
IntPtr hProc = GetProcAddress(hLib, "MAPISendMail");
if(hProc != IntPtr.Zero)
{
var func = Marshal.GetDelegateForFunctionPointer(hProc, typeof(MAPISendMailDelegate) as MAPISendMailDelegate);
var error = func?.Invoke(IntPtr.Zero, IntPtr.Zero, msg, flags, 0);
return error == MapiError.Success || error == MapiError.UserAbort;
}
}
finally
{
FreeLibrary(hLib);
}
}
return false;
}
</code></pre>
<p>上述方法需要调用程序和 dll 位数相同,故为了兼容不同邮箱应用可能需要分别编译 x64 和 x86 的程序</p>
<p>这里提供一种兼容不同位数邮箱应用的方法:临时将目标邮箱设为 MAPI 关联邮箱,调用 MAPISendMail 后还原</p>
<pre><code class="language-csharp">/// <summary>通过 Win32 MAPI 发送邮件</summary>
/// <remarks>Microsoft Outlook、eM Client、Mozilla Thunderbird 支持, 其他待发现</remarks>
private static bool SendByMAPI(MailInfo info, string clientName)
{
if (string.IsNullOrEmpty(clientName))
{
return false;
}
if (FindMAPIClientName() == clientName)
{
return SendByMAPI(info);
}
else
{
try
{
using (var key = Registry.CurrentUser.OpenSubKey(MapiKeyPath, true)
?? Registry.CurrentUser.CreateSubKey(MapiKeyPath))
{
string currentValue = key.GetValue(null)?.ToString();
key.SetValue(null, clientName);
bool success = SendByMAPI(info);
if (currentValue != null)
{
key.SetValue(null, currentValue);
}
else
{
key.DeleteValue(null, false);
}
return success;
}
}
catch
{
return false;
}
}
}
</code></pre>
<h3 id="outlookclassic">Outlook(Classic)</h3>
<p>Outlook(Classic) 还支持 COM 互操作和命令行的方式创建带附件的邮件,Outlook(New) 两种方式都不支持</p>
<pre><code class="language-csharp">/// <summary>通过 Outlook 发送邮件</summary>
public static bool SendByOutlook(MailInfo info)
{
return SendByOutlookMAPI(info) || SendByOutlookWithoutMAPI(info);
}
/// <summary>通过 Outlook MAPI 发送邮件</summary>
public static bool SendByOutlookMAPI(MailInfo info)
{
return SendByMAPI(info, OutlookClientName);
}
/// <summary>通过 Outlook COM 或进程方式发送邮件</summary>
public static bool SendByOutlookWithoutMAPI(MailInfo info)
{
return !IsUseNewOutlook() && (SendByOutlookCOM(info) || SendByOutlookProcess(info));
}
</code></pre>
<h4 id="outlookcom">Outlook COM</h4>
<p>通过引用 Microsoft.Office.Interop.Outlook 互操作库可用 COM 对象来创建带附件的邮件,支持添加多个附件</p>
<p>当同时安装了 Classic 和 New 且启用 New 时此方式无效:会卡在创建 app 对象</p>
<pre><code class="language-csharp">using System.Runtime.InteropServices;
using Microsoft.Office.Interop.Outlook;
/// <summary>通过 Outlook COM 对象发送邮件</summary>
/// <remarks>⚠: 当同时安装了 Outlook(Classic) 和 Outlook(New) 且启用 New 时会卡在创建 app 对象</remarks>
public static bool SendByOutlookCOM(MailInfo info)
{
Application app = null;
MailItem mail = null;
Attachments attachments = null;
try
{
app = (Application)Marshal.GetActiveObject("Outlook.Application");
}
catch
{
// 未找到活动的 Outlook 实例
}
bool isRunning = app != null; // Outlook 同时只允许一个实例进程
try
{
if (!isRunning)
{
app = new Application(); // 同时安装 Classic 和 New 且启用 New 时会卡在这里
}
mail = app.CreateItem(OlItemType.olMailItem) as MailItem;
mail.Subject = info.Subject;
mail.Body = info.Body;
mail.To = info.GetTO(";");
mail.CC = info.GetCC(";");
mail.BCC = info.GetBCC(";");
if (info.Attachments != null)
{
attachments = mail.Attachments;
foreach (var file in info.Attachments.Values)
{
attachments.Add(file);
}
}
if (info.WithoutUI && info.Recipients.Count > 0)
{
mail.Send();
}
else
{
mail.Display(false);
}
return true;
}
catch
{
if (!isRunning)
{
app?.Quit(); // 之前未运行时,启动的新实例遇到错误时关闭程序
}
return false;
}
finally
{
if (attachments != null)
{
Marshal.ReleaseComObject(attachments);
}
if (mail != null)
{
Marshal.ReleaseComObject(mail);
}
if (app != null)
{
Marshal.ReleaseComObject(app);
}
}
}
</code></pre>
<h4 id="outlook命令行">Outlook 命令行</h4>
<p>命令行方式只能添加一个附件</p>
<p>当同时安装了 Outlook(Classic) 和 Outlook(New) 且启用 New 时此方式无效</p>
<p>示例:<code>outlook.exe /c ipm.note /m example@to.com?subject=Test%20Subject /a C:\dir\file</code></p>
<pre><code class="language-csharp">/// <summary>获取 Outlook 程序文件位置</summary>
public static string GetOutlookPath()
{
// 此 CLSID 为固定值,与 Microsoft.Office.Interop.Outlook.ApplicationClass 的 GUID 值相同
string regPath = @"HKEY_CLASSES_ROOT\CLSID\{0006F03A-0000-0000-C000-000000000046}\LocalServer32";
string filePath = Registry.GetValue(regPath, null, null)?.ToString();
return filePath;
}
/// <summary>通过 Outlook 命令行方式发送邮件</summary>
/// <remarks>仅支持添加一个附件
/// <para>⚠: 当同时安装了 Outlook(Classic) 和 Outlook(New) 且启用 New 时命令行方式无效</para></remarks>
public static bool SendByOutlookProcess(MailInfo info)
{
string fileName = GetOutlookPath();
if (File.Exists(fileName))
{
var args = new StringBuilder($"/c ipm.note");
bool hasTO = info.Recipients.Count > 0;
bool hasCC = info.CcRecipients.Count > 0;
bool hasBCC = info.BccRecipients.Count > 0;
bool hasSubject = !string.IsNullOrEmpty(info.Subject);
bool hasBody = !string.IsNullOrEmpty(info.Body);
if (hasTO || hasSubject || hasBody)
{
args.Append(" /m ");
if (hasTO)
{
args.Append($"{Uri.EscapeDataString(info.GetTO(";"))}");
}
args.Append("?");
if (hasCC)
{
args.Append($"cc={Uri.EscapeDataString(info.GetCC(";"))}&");
}
if (hasBCC)
{
args.Append($"bcc={Uri.EscapeDataString(info.GetBCC(";"))}&");
}
if (hasSubject)
{
args.Append($"subject={Uri.EscapeDataString(info.Subject)}&");
}
if (hasBody)
{
args.Append($"body={Uri.EscapeDataString(info.Body)}&");
}
args.Remove(args.Length - 1, 1);
}
if (info.Attachments.Count > 0)
{
args.Append($" /a \"{info.Attachments.First().Value}\""); // 仅支持添加一个附件
}
Process.Start(fileName, args.ToString());
return true;
}
return false;
}
</code></pre>
<h3 id="其他邮箱应用">其他邮箱应用</h3>
<p>针对下方已知 ProgID 且支持命令行方式的邮箱应用,利用 AssocQueryString 可以快速获取可执行文件路径</p>
<pre><code class="language-csharp">/// <summary>通过关联字符串查找可执行文件位置</summary>
private static string GetExecutePath(string assocString)
{
return NativeMethods.AssocQueryString(AssocStr.Executable, assocString);
}
</code></pre>
<h4 id="mozillathunderbird">Mozilla Thunderbird</h4>
<p>Command line arguments - Thunderbird - MozillaZine Knowledge Base</p>
<pre><code class="language-csharp">const string ThunderbirdClientName = "Mozilla Thunderbird";
/// <summary>获取 Mozilla Thunderbird 程序文件位置</summary>
public static string GetThunderbirdPath()
{
return GetExecutePath(ThunderbirdProgID);
}
/// <summary>通过 Mozilla Thunderbird 发送邮件</summary>
public static bool SendByThunderbird(MailInfo info)
{
return SendByMAPI(info, ThunderbirdClientName) || SendByThunderbirdProcess(info);
}
/// <summary>通过 Mozilla Thunderbird 程序发送邮件</summary>
public static bool SendByThunderbirdProcess(MailInfo info)
{
string exePath = GetThunderbirdPath();
if (File.Exists(exePath))
{
var options = new List<string>();
if (info.Recipients.Count > 0)
{
options.Add($"to='{info.GetTO()}'");
}
if (info.CcRecipients.Count > 0)
{
options.Add($"cc='{info.GetCC()}'");
}
if (info.BccRecipients.Count > 0)
{
options.Add($"bcc='{info.GetBCC()}'");
}
if (!string.IsNullOrEmpty(info.Subject))
{
string subject = info.Subject.Replace("',", "' ,"); // ',截断会导致参数解析错误
options.Add($"subject='{subject}'");
}
if (!string.IsNullOrEmpty(info.Body))
{
string body = info.Body.Replace("',", "' ,"); // 同上
options.Add($"body='{body}'");
}
if (info.Attachments.Count > 0)
{
var files = info.Attachments.Values.Select(x => new Uri(x).AbsoluteUri).ToArray();
options.Add($"attachment='{string.Join("','", files)}'");
}
string args = "-compose";
if (options.Count > 0)
{
args += " \"" + string.Join(",", options.ToArray()) + "\"";
}
Process.Start(exePath, args);
return true;
}
return false;
}
</code></pre>
<h4 id="emclient">eM Client</h4>
<p>New mail with multiple attachments with command line</p>
<p>eM Client 命令行方式是通过创建 .eml 文件并打开的方式创建邮件</p>
<pre><code class="language-csharp">const string EMClientClientName = "eM Client";
/// <summary>获取 eM Client 程序文件位置</summary>
public static string GetEmClientPath()
{
return GetExecutePath(EMClientProgID);
}
/// <summary>通过 eM Client 发送邮件</summary>
public static bool SendByEmClient(MailInfo info)
{
return SendByMAPI(info, EMClientClientName) || SendByEmClientProcess(info);
}
/// <summary>通过 eM Client 程序发送邮件</summary>
/// <remarks>通过创建 .eml 临时文件的方式发送</remarks>
public static bool SendByEmClientProcess(MailInfo info)
{
string exePath = GetEmClientPath();
if (File.Exists(exePath))
{
using (var mail = new MailMessage())
{
mail.Subject = info.Subject;
mail.Body = info.Body;
info.Recipients.ForEach(mail.To.Add);
info.CcRecipients.ForEach(mail.CC.Add);
info.BccRecipients.ForEach(mail.Bcc.Add);
foreach (var file in info.Attachments.Values)
{
mail.Attachments.Add(new System.Net.Mail.Attachment(file));
}
string from = "from@exmple.com";
string to = null;
mail.From = new MailAddress(from); // 必须设置发件人地址, 否则会报错
if (info.Recipients.Count == 0)
{
to = "to@exmple.com";
mail.To.Add(to); // 至少有一个收件人, 否则会报错
}
var client = new SmtpClient();
client.DeliveryMethod = SmtpDeliveryMethod.SpecifiedPickupDirectory;
string tempDir = Path.Combine(Path.GetTempPath(), "tempMail");
try
{
Directory.Delete(tempDir, true);
}
catch
{
// ignore
}
Directory.CreateDirectory(tempDir);
client.PickupDirectoryLocation = tempDir;
client.Send(mail);
var emlFile = new DirectoryInfo(tempDir).GetFiles("*.eml").OrderByDescending(x => x.LastWriteTime).FirstOrDefault();
if (emlFile != null)
{
string emlPath = emlFile.FullName;
var lines = File.ReadAllLines(emlPath, Encoding.UTF8).ToList();
lines.Remove($"X-Sender: {from}");
lines.Remove($"From: {from}");
if (to != null)
{
lines.Remove($"X-Receiver: {to}");
lines.Remove($"To: {to}");
}
lines.Insert(0, "X-Unsent: 1"); // 标记为未发送
File.WriteAllLines(emlPath, lines.ToArray(), Encoding.UTF8);
var process = Process.Start(exePath, $"/open \"{emlPath}\"");
process.EnableRaisingEvents = true;
process.Exited += (s, e) =>
{
try
{
Directory.Delete(tempDir, true);
}
catch
{
// ignore
}
};
return true;
}
}
}
return false;
}
</code></pre>
<h4 id="网易邮箱大师">网易邮箱大师</h4>
<p>命令行来自于文件系统对象发送到子菜单中的快捷方式</p>
<pre><code class="language-csharp">const string MailMasterProgID = "MailMaster";
/// <summary>获取网易邮箱大师程序文件位置</summary>
public static string GetMailMasterPath()
{
return GetExecutePath(MailMasterProgID);
}
/// <summary>通过网易邮箱大师发送邮件</summary>
/// <remarks>命令来自于"发送到"菜单目录快捷方式</remarks>
public static bool SendByMailMaster(MailInfo info)
{
string exePath = GetMailMasterPath();
if (File.Exists(exePath))
{
var args = new StringBuilder();
if (info.Attachments.Count > 0)
{
args.Append($"--send-as-attachment \"{info.Attachments.First().Value}\"");
}
Process.Start(exePath, args.ToString());
return true;
}
return false;
}
</code></pre>
<h3 id="调用默认邮箱">调用默认邮箱</h3>
<p>综上,Windows 上默认邮箱有 mailto 关联邮箱和 MAPI 关联邮箱,但不懂注册表的普通用户可能只会在控制面板更改 mailto 关联邮箱,为提高兼容性,我们可以用以下步骤一一尝试调用默认邮箱:</p>
<ol>
<li>
<p>当 MAPI 关联邮箱存在时(避免系统弹窗提示无关联邮箱),直接调用 MAPI 关联邮箱</p>
</li>
<li>
<p>读取 mailto 关联邮箱 ProgID,并尝试在 MAPI Mail 注册表子项下找到对应的项,临时设为 MAPI 关联邮箱调用</p>
</li>
<li>
<p>MAPI 方式失败后,尝试使用 COM 或命令行方式</p>
</li>
<li>
<p>以上支持添加附件的方式都失败后,最后使用 mailto 方式</p>
</li>
</ol>
<pre><code class="language-csharp">/// <summary>通过默认的邮箱客户端发送邮件</summary>
public static bool SendByDefault(MailInfo info)
{
string progID = null;
string clientName = FindMAPIClientName();
if (clientName == null)
{
// 未设置 MAPI 客户端时, 尝试查找 mailto 协议关联的客户端是否支持 MAPI
progID = FindMailToClientProgID();
clientName = FindMAPIClientName(progID);
}
// 优先使用 MAPI 发送邮件
bool success = SendByMAPI(info, clientName);
if (!success)
{
progID = progID ?? FindMailToClientProgID();
var st = StringComparison.OrdinalIgnoreCase;
if (IsOutlookProgID(progID))
{
success = SendByOutlookWithoutMAPI(info);
}
else if (progID.Equals(EMClientProgID, st))
{
success = SendByEmClientProcess(info);
}
else if (progID.Equals(ThunderbirdProgID, st))
{
success = SendByThunderbirdProcess(info);
}
else if (progID.Equals(MailMasterProgID, st))
{
success = SendByMailMaster(info);
}
if (!success)
{
// 如果以上方式都失败了最后尝试 mailto 协议
success = SendByProtocol(info);
}
}
return success;
}
/// <summary>根据 ProgID 查找 MAPI 邮箱客户端名称</summary>
private static string FindMAPIClientName(string progID)
{
if (string.IsNullOrEmpty(progID))
{
return null;
}
using (var cuKey = Registry.CurrentUser.OpenSubKey(MapiKeyPath))
using (var lmKey = Registry.LocalMachine.OpenSubKey(MapiKeyPath))
{
var cuKeyNames = cuKey?.GetSubKeyNames().ToList() ?? new List<string>();
var lmKeyNames = lmKey?.GetSubKeyNames().ToList() ?? new List<string>();
if (IsOutlookProgID(progID))
{
string name = OutlookClientName; // Microsoft Outlook 没有 Capabilities\URLAssociations 子项
if (lmKeyNames.Contains(name, StringComparer.OrdinalIgnoreCase)
|| cuKeyNames.Contains(name, StringComparer.OrdinalIgnoreCase))
{
return name;
}
}
else
{
var dic = new Dictionary<RegistryKey, List<string>>
{
= lmKeyNames,
= cuKeyNames
};
foreach (var item in dic)
{
foreach (var keyName in item.Value)
{
using (var key = item.Key.OpenSubKey($@"{keyName}\Capabilities\URLAssociations"))
{
string value = key?.GetValue("mailto")?.ToString();
if (progID.Equals(value, StringComparison.OrdinalIgnoreCase))
{
return keyName;
}
}
}
}
}
}
return null;
}
</code></pre><br><br>
来源:https://www.cnblogs.com/BluePointLilac/p/19010985
頁:
[1]