郵件的幾大要素
- 發(fā)件人 From
- 收件人(主要收件人 To,抄送 CC,密送 BCC)
- 主題 Subject
- 正文 Body
- 附件 Attachments
SmtpClient 和 MailKit
如果有郵箱服務(wù)器并且已知發(fā)件人郵箱和密碼,可以通過 C# 自帶的 SmtpClient 或者使用開源庫 MailKit
調(diào)用第三方郵箱應(yīng)用
C# 自帶的 MailMessage 類中的 Attachments 會(huì)直接打開文件流,且沒有屬性可以獲取文件路徑
我們可以創(chuàng)建一個(gè)簡單的郵件信息類,調(diào)用第三方郵箱客戶端一般不需要發(fā)件人,可去掉發(fā)件人屬性
using System.Collections.Generic;
using System.Net.Mail;
public sealed class MailInfo
{
// /// <summary>發(fā)件人</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>();
}
mailto 協(xié)議
mailto 是全平臺(tái)支持的協(xié)議,支持多個(gè)收件人,抄送和密送,但不支持添加附件
mailto 關(guān)聯(lián)應(yīng)用
在 Windows 上使用 mailto 會(huì)調(diào)用其關(guān)聯(lián)應(yīng)用,未設(shè)置關(guān)聯(lián)應(yīng)用時(shí),會(huì)彈出打開方式對(duì)話框詢問使用什么應(yīng)用打開

關(guān)聯(lián)注冊(cè)表位置
HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\Shell\Associations\UrlAssociations\mailto\UserChoice
// 常見的郵箱應(yīng)用 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"; // 網(wǎng)易郵箱大師
/// <summary>查找 mailto 協(xié)議關(guān)聯(lián)的郵箱應(yīng)用 ProgID</summary>
private static string FindMailToClientProgID()
{
// Win10 以上支持 AssocQueryString 查找 ProgID, 為兼容低版本使用注冊(cè)表查詢
// 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 關(guān)聯(lián)的 ProgID</summary>
private static bool IsOutlookProgID(string progID)
{
var st = StringComparison.OrdinalIgnoreCase;
return progID.IndexOf("Outlook", st) >= 0 // Outlook(Classic) 版本相關(guān),如 Outlook.URL.mailto.15
|| progID.Equals(OutlookNewProgID, st);
}
mailto 標(biāo)準(zhǔn)
語法:mailto:sAddress[sHeaders]
示例:mailto:example@to.com?subject=Test%20Subject
主要收件人寫在 sAddress,抄送、密送、主題和正文都放在 sHeaders 里面,需要對(duì)所有 URL 保留字符進(jìn)行編碼轉(zhuǎn)義
大部分郵箱應(yīng)用都使用較新的 RFC 6068 標(biāo)準(zhǔn)(收件人、抄送、密送使用逗號(hào)分隔),且部分應(yīng)用同時(shí)兼容分號(hào)和逗號(hào)
但是 Microsoft Outlook 還在使用著比較舊的 RFC 2368 標(biāo)準(zhǔn)(收件人、抄送、密送使用分號(hào)分隔)
故當(dāng)關(guān)聯(lián)應(yīng)用為 Outlook 時(shí),包括 Classic 版本和 UWP 新版,都無法正確解析逗號(hào)連接的多個(gè)收件人、抄送、密送
Classic 版本支持的 COM Interop 方式中也是使用的分號(hào)分隔
另外 PDF 表單 JavaScript 動(dòng)作中 mailDoc、mailForm 等發(fā)送郵件的方法也是使用的分號(hào)分隔符
因此我們可以給上文中的 MailInfo 類添加幾個(gè)獲取指定分隔符連接的收件人地址字符串的方法
/// <summary>獲取指定分隔符連接的收件地址</summary>
/// <param name="separator">遵循 mailto RFC 6068 規(guī)范默認(rèn)為逗號(hào),部分郵箱客戶端支持逗號(hào)和分號(hào),
/// <para>但 Outlook 僅支持分號(hào); PDF 表單 JavaScript 動(dòng)作中使用分號(hào)</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());
}
調(diào)用 mailto 關(guān)聯(lián)郵箱
/// <summary>通過 mailto 協(xié)議調(diào)用默認(rèn)郵箱客戶端發(fā)送郵件</summary>
/// <remarks>不支持附件, 支持 Outlook(New)</remarks>
public static bool SendByProtocol(MailInfo info)
{
bool isOutlook = IsOutlookProgID(FindMailToClientProgID());
string separator = isOutlook ? ";" : ","; // Outlook 僅支持分號(hào), 其他客戶端支持標(biāo)準(zhǔn)的逗號(hào)
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;
}
}
Win32 MAPI
Windows 定義了 MAPI 接口供第三方郵箱應(yīng)用實(shí)現(xiàn)集成,例如 Outlook(Classic)、eM Client、Thunderbird
C# 中可以使用 MAPISendMail 進(jìn)行調(diào)用,需要注意不一定成功,可能會(huì)遇到未知的MAPI_E_FAILURE
錯(cuò)誤
另外 MAPI 方式支持設(shè)置是否顯示 UI (MAPI_DIALOG
、MAPI_DIALOG_MODELESS
、MAPI_LOGON_UI
)
可以為上文中的 MailInfo 類添加一個(gè)是否顯示 UI 的屬性
/// <summary>是否不顯示UI自動(dòng)發(fā)送, 至少需要一名收件人</summary>
public bool WithoutUI { get; set; }
MAPI 關(guān)聯(lián)應(yīng)用
支持 MAPI 的郵箱應(yīng)用一般會(huì)在{HKLM|HKCU}\SOFTWARE\Clients\Mail
下寫入子項(xiàng)
通過修改 Mail 項(xiàng)默認(rèn)鍵值修改默認(rèn) MAPI 郵箱,HKCU 優(yōu)先,鍵值需要與 Mail 子項(xiàng)名稱一致
/// <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[0];
var lmKeyNames = lmKey?.GetSubKeyNames() ?? new string[0];
// 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;
}
調(diào)用 MAPI 關(guān)聯(lián)郵箱
文件系統(tǒng)對(duì)象右鍵菜單的發(fā)送到子菜單中的就是調(diào)用的 MAPI 關(guān)聯(lián)郵箱
未設(shè)置 MAPI 關(guān)聯(lián)郵箱時(shí)調(diào)用會(huì)彈窗提示,如果 Mail 項(xiàng)中PreFirstRun
鍵值不為空,則彈窗優(yōu)先顯示其內(nèi)容,*分隔內(nèi)容和標(biāo)題
但此彈窗內(nèi)容會(huì)誤導(dǎo)用戶,因?yàn)榭刂泼姘迥J(rèn)程序中只能設(shè)置 mailto 關(guān)聯(lián)郵箱而不能設(shè)置 MAPI 關(guān)聯(lián)郵箱,兩者無關(guān)


另外建議異步調(diào)用,否則外部出錯(cuò)可能會(huì)卡死進(jìn)程
比如同時(shí)安裝了 Outlook(Classic) 和 Outlook(New) 且啟用 New 時(shí),Outlook(Classic)后臺(tái)啟動(dòng)后會(huì)調(diào)起 Outlook(New)并結(jié)束自身,提前關(guān)閉了 Outlook(New) 主窗口, 或設(shè)置了收件人、抄送、密送

而且下文中的 Outlook COM 和命令行方式也都是只支持 Classic 不支持 New,所以我們需要一個(gè)判斷是否啟用了 Outlook(New) 的方法
這里我們可以使用 AssocQueryString 根據(jù) ProgID 獲取其友好名稱來判斷是否安裝了新版 Outlook,上文代碼中也提到了 Win10 以上系統(tǒng)可以用 AssocQueryString 直接查詢 mailto 關(guān)聯(lián)的 ProgID,而下文中也會(huì)用其根據(jù) ProgID 獲取關(guān)聯(lián)可執(zhí)行文件路徑
/// <summary>是否同時(shí)安裝了 Outlook Classic 和 New 兩個(gè)版本,且啟用 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;
}
另外如果只安裝了 Outlook(New)(Win11 默認(rèn)預(yù)裝)的情況下,無法通過 MAPI 方式調(diào)起,如若可獲知 Outlook(Classic) 是如何啟動(dòng) Outlook(New) 即可有方法單獨(dú)啟動(dòng) Outlook(New)。現(xiàn)今未找到只安裝了 Outlook(New) 創(chuàng)建帶附件郵件的方法
const string OutlookClientName = "Microsoft Outlook";
/// <summary>通過 Win32 MAPI 發(fā)送郵件</summary>
/// <remarks>?: 調(diào)用 MAPI 方式是同步執(zhí)行,外部出錯(cuò)可能會(huì)卡死進(jìn)程,
/// <para>比如同時(shí)安裝了 Outlook(Classic) 和 Outlook(New) 且啟用 New 時(shí),</para>
/// <para>提前關(guān)閉了 Outlook(New) 主窗口, 或設(shè)置了收件人、抄送、密送</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)
{
// 測(cè)試設(shè)置了收件人、抄送、密送 Outlook(New) 會(huì)卡住
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 時(shí)移除標(biāo)志
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);
}
}
用到的本機(jī)方法、結(jié)構(gòu)體、枚舉
static class NativeMethods
{
[DllImport("mapi32.dll", CharSet = CharSet.Auto)]
public static extern MapiError MAPISendMail(IntPtr session, IntPtr hWndParent, MapiMessage message, MapiFlags flags, int reserved);
[DllImport("shlwapi.dll", CharSet = CharSet.Auto)]
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>獲取結(jié)構(gòu)體數(shù)組指針</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[i], ptr, false);
copiedCount++;
}
}
catch
{
FreeStructArrayPointer<T>(hglobal, copiedCount);
throw;
}
return hglobal;
}
/// <summary>釋放結(jié)構(gòu)體數(shù)組指針</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);
}
}
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
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;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
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 可設(shè) address 或 name
result.name = result.address;
}
return result;
}
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
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, // 不指示附件位置
};
}
}
[Flags]
enum MapiFlags
{
LogonUI = 0x1,
NewSession = 0x2,
Dialog = 0x8,
DialogModeless = 0x4 | Dialog,
ForceUnicode = 0x40000,
}
enum MapiError
{
/// <summary>成功</summary>
Success = 0,
/// <summary>用戶中止</summary>
UserAbort = 1,
/// <summary>發(fā)生一個(gè)或多個(gè)未指定錯(cuò)誤</summary>
Failure = 2,
/// <summary>登錄失敗</summary>
LoginFailure = 3,
/// <summary>內(nèi)存不足</summary>
InsufficientMemory = 5,
/// <summary>文件附件太多</summary>
TooManyFiles = 9,
/// <summary>收件人太多</summary>
TooManyRecipients = 10,
/// <summary>找不到附件</summary>
AttachmentNotFound = 11,
/// <summary>無法打開附件</summary>
AttachmentOpenFailure = 12,
/// <summary>收件人未顯示在地址列表中</summary>
UnknownRecipient = 14,
/// <summary>收件人類型錯(cuò)誤</summary>
BadRecipient = 15,
/// <summary>消息中文本太大</summary>
TextTooLarge = 18,
/// <summary>收件人與多個(gè)收件人描述符結(jié)構(gòu)匹配,且未設(shè)置 MAPI_DIALOG</summary>
AmbiguousRecipient = 21,
/// <summary>一個(gè)或多個(gè)收件人無效</summary>
InvalidRecips = 25,
/// <summary>指定了 MAPI_FORCE_UNICODE 標(biāo)志,但不支持 Unicode</summary>
UnicodeNotSupported = 27,
/// <summary>附件太大</summary>
AttachmentTooLarge = 28,
}
[Flags]
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
}
調(diào)用其他 MAPI 郵箱
已知第三方郵箱應(yīng)用包含 MAPI 相關(guān)導(dǎo)出函數(shù)的 dll 位置時(shí),可通過 GetProcAddress 來調(diào)用
[DllImport("kernel32.dll")]
extern static IntPtr LoadLibrary(string lpLibFileName);
[DllImport("kernel32.dll")]
extern static IntPtr GetProcAddress(IntPtr hModule, string lpProcName);
[DllImport("kernel32.dll")]
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;
}
上述方法需要調(diào)用程序和 dll 位數(shù)相同,故為了兼容不同郵箱應(yīng)用可能需要分別編譯 x64 和 x86 的程序
這里提供一種兼容不同位數(shù)郵箱應(yīng)用的方法:臨時(shí)將目標(biāo)郵箱設(shè)為 MAPI 關(guān)聯(lián)郵箱,調(diào)用 MAPISendMail 后還原
/// <summary>通過 Win32 MAPI 發(fā)送郵件</summary>
/// <remarks>Microsoft Outlook、eM Client、Mozilla Thunderbird 支持, 其他待發(fā)現(xiàn)</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;
}
}
}
Outlook(Classic)
Outlook(Classic) 還支持 COM 互操作和命令行的方式創(chuàng)建帶附件的郵件,Outlook(New) 兩種方式都不支持
/// <summary>通過 Outlook 發(fā)送郵件</summary>
public static bool SendByOutlook(MailInfo info)
{
return SendByOutlookMAPI(info) || SendByOutlookWithoutMAPI(info);
}
/// <summary>通過 Outlook MAPI 發(fā)送郵件</summary>
public static bool SendByOutlookMAPI(MailInfo info)
{
return SendByMAPI(info, OutlookClientName);
}
/// <summary>通過 Outlook COM 或進(jìn)程方式發(fā)送郵件</summary>
public static bool SendByOutlookWithoutMAPI(MailInfo info)
{
return !IsUseNewOutlook() && (SendByOutlookCOM(info) || SendByOutlookProcess(info));
}
Outlook COM
通過引用 Microsoft.Office.Interop.Outlook 互操作庫可用 COM 對(duì)象來創(chuàng)建帶附件的郵件,支持添加多個(gè)附件
當(dāng)同時(shí)安裝了 Classic 和 New 且啟用 New 時(shí)此方式無效:會(huì)卡在創(chuàng)建 app 對(duì)象
using System.Runtime.InteropServices;
using Microsoft.Office.Interop.Outlook;
/// <summary>通過 Outlook COM 對(duì)象發(fā)送郵件</summary>
/// <remarks>?: 當(dāng)同時(shí)安裝了 Outlook(Classic) 和 Outlook(New) 且啟用 New 時(shí)會(huì)卡在創(chuàng)建 app 對(duì)象</remarks>
public static bool SendByOutlookCOM(MailInfo info)
{
Application app = null;
MailItem mail = null;
Attachments attachments = null;
try
{
app = (Application)Marshal.GetActiveObject("Outlook.Application");
}
catch
{
// 未找到活動(dòng)的 Outlook 實(shí)例
}
bool isRunning = app != null; // Outlook 同時(shí)只允許一個(gè)實(shí)例進(jìn)程
try
{
if (!isRunning)
{
app = new Application(); // 同時(shí)安裝 Classic 和 New 且啟用 New 時(shí)會(huì)卡在這里
}
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(); // 之前未運(yùn)行時(shí),啟動(dòng)的新實(shí)例遇到錯(cuò)誤時(shí)關(guān)閉程序
}
return false;
}
finally
{
if (attachments != null)
{
Marshal.ReleaseComObject(attachments);
}
if (mail != null)
{
Marshal.ReleaseComObject(mail);
}
if (app != null)
{
Marshal.ReleaseComObject(app);
}
}
}
Outlook 命令行
命令行方式只能添加一個(gè)附件
當(dāng)同時(shí)安裝了 Outlook(Classic) 和 Outlook(New) 且啟用 New 時(shí)此方式無效
示例:outlook.exe /c ipm.note /m example@to.com?subject=Test%20Subject /a C:\dir\file
/// <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 命令行方式發(fā)送郵件</summary>
/// <remarks>僅支持添加一個(gè)附件
/// <para>?: 當(dāng)同時(shí)安裝了 Outlook(Classic) 和 Outlook(New) 且啟用 New 時(shí)命令行方式無效</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}\""); // 僅支持添加一個(gè)附件
}
Process.Start(fileName, args.ToString());
return true;
}
return false;
}
其他郵箱應(yīng)用
針對(duì)下方已知 ProgID 且支持命令行方式的郵箱應(yīng)用,利用 AssocQueryString 可以快速獲取可執(zhí)行文件路徑
/// <summary>通過關(guān)聯(lián)字符串查找可執(zhí)行文件位置</summary>
private static string GetExecutePath(string assocString)
{
return NativeMethods.AssocQueryString(AssocStr.Executable, assocString);
}
Mozilla Thunderbird
Command line arguments - Thunderbird - MozillaZine Knowledge Base
const string ThunderbirdClientName = "Mozilla Thunderbird";
/// <summary>獲取 Mozilla Thunderbird 程序文件位置</summary>
public static string GetThunderbirdPath()
{
return GetExecutePath(ThunderbirdProgID);
}
/// <summary>通過 Mozilla Thunderbird 發(fā)送郵件</summary>
public static bool SendByThunderbird(MailInfo info)
{
return SendByMAPI(info, ThunderbirdClientName) || SendByThunderbirdProcess(info);
}
/// <summary>通過 Mozilla Thunderbird 程序發(fā)送郵件</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("',", "' ,"); // ',截?cái)鄷?huì)導(dǎo)致參數(shù)解析錯(cuò)誤
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;
}
eM Client
New mail with multiple attachments with command line
eM Client 命令行方式是通過創(chuàng)建 .eml 文件并打開的方式創(chuàng)建郵件
const string EMClientClientName = "eM Client";
/// <summary>獲取 eM Client 程序文件位置</summary>
public static string GetEmClientPath()
{
return GetExecutePath(EMClientProgID);
}
/// <summary>通過 eM Client 發(fā)送郵件</summary>
public static bool SendByEmClient(MailInfo info)
{
return SendByMAPI(info, EMClientClientName) || SendByEmClientProcess(info);
}
/// <summary>通過 eM Client 程序發(fā)送郵件</summary>
/// <remarks>通過創(chuàng)建 .eml 臨時(shí)文件的方式發(fā)送</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); // 必須設(shè)置發(fā)件人地址, 否則會(huì)報(bào)錯(cuò)
if (info.Recipients.Count == 0)
{
to = "to@exmple.com";
mail.To.Add(to); // 至少有一個(gè)收件人, 否則會(huì)報(bào)錯(cuò)
}
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"); // 標(biāo)記為未發(fā)送
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;
}
網(wǎng)易郵箱大師
命令行來自于文件系統(tǒng)對(duì)象發(fā)送到子菜單中的快捷方式
const string MailMasterProgID = "MailMaster";
/// <summary>獲取網(wǎng)易郵箱大師程序文件位置</summary>
public static string GetMailMasterPath()
{
return GetExecutePath(MailMasterProgID);
}
/// <summary>通過網(wǎng)易郵箱大師發(fā)送郵件</summary>
/// <remarks>命令來自于"發(fā)送到"菜單目錄快捷方式</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;
}
調(diào)用默認(rèn)郵箱
綜上,Windows 上默認(rèn)郵箱有 mailto 關(guān)聯(lián)郵箱和 MAPI 關(guān)聯(lián)郵箱,但不懂注冊(cè)表的普通用戶可能只會(huì)在控制面板更改 mailto 關(guān)聯(lián)郵箱,為提高兼容性,我們可以用以下步驟一一嘗試調(diào)用默認(rèn)郵箱:
當(dāng) MAPI 關(guān)聯(lián)郵箱存在時(shí)(避免系統(tǒng)彈窗提示無關(guān)聯(lián)郵箱),直接調(diào)用 MAPI 關(guān)聯(lián)郵箱
讀取 mailto 關(guān)聯(lián)郵箱 ProgID,并嘗試在 MAPI Mail 注冊(cè)表子項(xiàng)下找到對(duì)應(yīng)的項(xiàng),臨時(shí)設(shè)為 MAPI 關(guān)聯(lián)郵箱調(diào)用
MAPI 方式失敗后,嘗試使用 COM 或命令行方式
以上支持添加附件的方式都失敗后,最后使用 mailto 方式
/// <summary>通過默認(rèn)的郵箱客戶端發(fā)送郵件</summary>
public static bool SendByDefault(MailInfo info)
{
string progID = null;
string clientName = FindMAPIClientName();
if (clientName == null)
{
// 未設(shè)置 MAPI 客戶端時(shí), 嘗試查找 mailto 協(xié)議關(guān)聯(lián)的客戶端是否支持 MAPI
progID = FindMailToClientProgID();
clientName = FindMAPIClientName(progID);
}
// 優(yōu)先使用 MAPI 發(fā)送郵件
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 協(xié)議
success = SendByProtocol(info);
}
}
return success;
}
/// <summary>根據(jù) 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 子項(xiàng)
if (lmKeyNames.Contains(name, StringComparer.OrdinalIgnoreCase)
|| cuKeyNames.Contains(name, StringComparer.OrdinalIgnoreCase))
{
return name;
}
}
else
{
var dic = new Dictionary<RegistryKey, List<string>>
{
[lmKey] = lmKeyNames,
[cuKey] = 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;
}
轉(zhuǎn)自https://www.cnblogs.com/BluePointLilac/p/19010985
該文章在 2025/8/1 9:05:18 編輯過