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

關(guān)聯(lián)注冊表位置
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, 為兼容低版本使用注冊表查詢
// 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 里面,需要對所有 URL 保留字符進(jìn)行編碼轉(zhuǎn)義
大部分郵箱應(yīng)用都使用較新的 RFC 6068 標(biāo)準(zhǔn)(收件人、抄送、密送使用逗號分隔),且部分應(yīng)用同時兼容分號和逗號
但是 Microsoft Outlook 還在使用著比較舊的 RFC 2368 標(biāo)準(zhǔn)(收件人、抄送、密送使用分號分隔)
故當(dāng)關(guān)聯(lián)應(yīng)用為 Outlook 時,包括 Classic 版本和 UWP 新版,都無法正確解析逗號連接的多個收件人、抄送、密送
Classic 版本支持的 COM Interop 方式中也是使用的分號分隔
另外 PDF 表單 JavaScript 動作中 mailDoc、mailForm 等發(fā)送郵件的方法也是使用的分號分隔符
因此我們可以給上文中的 MailInfo 類添加幾個獲取指定分隔符連接的收件人地址字符串的方法
/// <summary>獲取指定分隔符連接的收件地址</summary>
/// <param name="separator">遵循 mailto RFC 6068 規(guī)范默認(rèn)為逗號,部分郵箱客戶端支持逗號和分號,
/// <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());
}
調(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 僅支持分號, 其他客戶端支持標(biāo)準(zhǔn)的逗號
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)用實現(xiàn)集成,例如 Outlook(Classic)、eM Client、Thunderbird
C# 中可以使用 MAPISendMail 進(jìn)行調(diào)用,需要注意不一定成功,可能會遇到未知的MAPI_E_FAILURE
錯誤
另外 MAPI 方式支持設(shè)置是否顯示 UI (MAPI_DIALOG
、MAPI_DIALOG_MODELESS
、MAPI_LOGON_UI
)
可以為上文中的 MailInfo 類添加一個是否顯示 UI 的屬性
/// <summary>是否不顯示UI自動發(fā)送, 至少需要一名收件人</summary>
public bool WithoutUI { get; set; }
MAPI 關(guān)聯(lián)應(yīng)用
支持 MAPI 的郵箱應(yīng)用一般會在{HKLM|HKCU}\SOFTWARE\Clients\Mail
下寫入子項
通過修改 Mail 項默認(rèn)鍵值修改默認(rèn) MAPI 郵箱,HKCU 優(yōu)先,鍵值需要與 Mail 子項名稱一致
/// <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)對象右鍵菜單的發(fā)送到子菜單中的就是調(diào)用的 MAPI 關(guān)聯(lián)郵箱
未設(shè)置 MAPI 關(guān)聯(lián)郵箱時調(diào)用會彈窗提示,如果 Mail 項中PreFirstRun
鍵值不為空,則彈窗優(yōu)先顯示其內(nèi)容,*分隔內(nèi)容和標(biāo)題
但此彈窗內(nèi)容會誤導(dǎo)用戶,因為控制面板默認(rèn)程序中只能設(shè)置 mailto 關(guān)聯(lián)郵箱而不能設(shè)置 MAPI 關(guān)聯(lián)郵箱,兩者無關(guān)


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

而且下文中的 Outlook COM 和命令行方式也都是只支持 Classic 不支持 New,所以我們需要一個判斷是否啟用了 Outlook(New) 的方法
這里我們可以使用 AssocQueryString 根據(jù) ProgID 獲取其友好名稱來判斷是否安裝了新版 Outlook,上文代碼中也提到了 Win10 以上系統(tǒng)可以用 AssocQueryString 直接查詢 mailto 關(guān)聯(lián)的 ProgID,而下文中也會用其根據(jù) ProgID 獲取關(guān)聯(lián)可執(zhí)行文件路徑
/// <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;
}
另外如果只安裝了 Outlook(New)(Win11 默認(rèn)預(yù)裝)的情況下,無法通過 MAPI 方式調(diào)起,如若可獲知 Outlook(Classic) 是如何啟動 Outlook(New) 即可有方法單獨啟動 Outlook(New)?,F(xiàn)今未找到只安裝了 Outlook(New) 創(chuàng)建帶附件郵件的方法
const string OutlookClientName = "Microsoft Outlook";
/// <summary>通過 Win32 MAPI 發(fā)送郵件</summary>
/// <remarks>?: 調(diào)用 MAPI 方式是同步執(zhí)行,外部出錯可能會卡死進(jìn)程,
/// <para>比如同時安裝了 Outlook(Classic) 和 Outlook(New) 且啟用 New 時,</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)
{
// 測試設(shè)置了收件人、抄送、密送 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 時移除標(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ā)生一個或多個未指定錯誤</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>收件人類型錯誤</summary>
BadRecipient = 15,
/// <summary>消息中文本太大</summary>
TextTooLarge = 18,
/// <summary>收件人與多個收件人描述符結(jié)構(gòu)匹配,且未設(shè)置 MAPI_DIALOG</summary>
AmbiguousRecipient = 21,
/// <summary>一個或多個收件人無效</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 位置時,可通過 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)用的方法:臨時將目標(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 對象來創(chuàng)建帶附件的郵件,支持添加多個附件
當(dāng)同時安裝了 Classic 和 New 且啟用 New 時此方式無效:會卡在創(chuàng)建 app 對象
using System.Runtime.InteropServices;
using Microsoft.Office.Interop.Outlook;
/// <summary>通過 Outlook COM 對象發(fā)送郵件</summary>
/// <remarks>?: 當(dāng)同時安裝了 Outlook(Classic) 和 Outlook(New) 且啟用 New 時會卡在創(chuàng)建 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 同時只允許一個實例進(jìn)程
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(); // 之前未運行時,啟動的新實例遇到錯誤時關(guān)閉程序
}
return false;
}
finally
{
if (attachments != null)
{
Marshal.ReleaseComObject(attachments);
}
if (mail != null)
{
Marshal.ReleaseComObject(mail);
}
if (app != null)
{
Marshal.ReleaseComObject(app);
}
}
}
Outlook 命令行
命令行方式只能添加一個附件
當(dāng)同時安裝了 Outlook(Classic) 和 Outlook(New) 且啟用 New 時此方式無效
示例: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>僅支持添加一個附件
/// <para>?: 當(dāng)同時安裝了 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;
}
其他郵箱應(yīng)用
針對下方已知 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("',", "' ,"); // ',截斷會導(dǎo)致參數(shù)解析錯誤
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 臨時文件的方式發(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ā)件人地址, 否則會報錯
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"); // 標(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)對象發(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)郵箱,但不懂注冊表的普通用戶可能只會在控制面板更改 mailto 關(guān)聯(lián)郵箱,為提高兼容性,我們可以用以下步驟一一嘗試調(diào)用默認(rèn)郵箱:
當(dāng) MAPI 關(guān)聯(lián)郵箱存在時(避免系統(tǒng)彈窗提示無關(guān)聯(lián)郵箱),直接調(diào)用 MAPI 關(guān)聯(lián)郵箱
讀取 mailto 關(guān)聯(lián)郵箱 ProgID,并嘗試在 MAPI Mail 注冊表子項下找到對應(yīng)的項,臨時設(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 客戶端時, 嘗試查找 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 子項
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 編輯過