LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發文檔 其他文檔  
 
網站管理員

C# 調用郵箱應用發送帶附件的郵件

freeflydom
2025年8月1日 9:5 本文熱度 749

郵件的幾大要素

  • 發件人 From
  • 收件人(主要收件人 To,抄送 CC,密送 BCC)
  • 主題 Subject
  • 正文 Body
  • 附件 Attachments

SmtpClient 和 MailKit

如果有郵箱服務器并且已知發件人郵箱和密碼,可以通過 C# 自帶的 SmtpClient 或者使用開源庫 MailKit

調用第三方郵箱應用

C# 自帶的 MailMessage 類中的 Attachments 會直接打開文件流,且沒有屬性可以獲取文件路徑

我們可以創建一個簡單的郵件信息類,調用第三方郵箱客戶端一般不需要發件人,可去掉發件人屬性

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>();
}

mailto 協議

mailto 是全平臺支持的協議,支持多個收件人,抄送和密送,但不支持添加附件

mailto 關聯應用

在 Windows 上使用 mailto 會調用其關聯應用,未設置關聯應用時,會彈出打開方式對話框詢問使用什么應用打開

關聯注冊表位置
HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\Shell\Associations\UrlAssociations\mailto\UserChoice

// 常見的郵箱應用 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);
}

mailto 標準

語法:mailto:sAddress[sHeaders]
示例:mailto:example@to.com?subject=Test%20Subject

主要收件人寫在 sAddress,抄送、密送、主題和正文都放在 sHeaders 里面,需要對所有 URL 保留字符進行編碼轉義

大部分郵箱應用都使用較新的 RFC 6068 標準(收件人、抄送、密送使用逗號分隔),且部分應用同時兼容分號和逗號

但是  Microsoft Outlook 還在使用著比較舊的 RFC 2368 標準(收件人、抄送、密送使用分號分隔)

故當關聯應用為 Outlook 時,包括 Classic 版本和 UWP 新版,都無法正確解析逗號連接的多個收件人、抄送、密送

Classic 版本支持的 COM Interop 方式中也是使用的分號分隔

另外 PDF 表單 JavaScript 動作中 mailDocmailForm 等發送郵件的方法也是使用的分號分隔符

因此我們可以給上文中的 MailInfo 類添加幾個獲取指定分隔符連接的收件人地址字符串的方法

/// <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());
}

調用 mailto 關聯郵箱

/// <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;
    }
}

Win32 MAPI

Windows 定義了 MAPI 接口供第三方郵箱應用實現集成,例如 Outlook(Classic)、eM ClientThunderbird

C# 中可以使用 MAPISendMail 進行調用,需要注意不一定成功,可能會遇到未知的MAPI_E_FAILURE錯誤

另外 MAPI 方式支持設置是否顯示 UI (MAPI_DIALOGMAPI_DIALOG_MODELESSMAPI_LOGON_UI)

可以為上文中的 MailInfo 類添加一個是否顯示 UI 的屬性

/// <summary>是否不顯示UI自動發送, 至少需要一名收件人</summary>
public bool WithoutUI { get; set; }

MAPI 關聯應用

支持 MAPI 的郵箱應用一般會在{HKLM|HKCU}\SOFTWARE\Clients\Mail下寫入子項

通過修改 Mail 項默認鍵值修改默認 MAPI 郵箱,HKCU 優先,鍵值需要與 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;
}

調用 MAPI 關聯郵箱

文件系統對象右鍵菜單的發送到子菜單中的就是調用的 MAPI 關聯郵箱

未設置 MAPI 關聯郵箱時調用會彈窗提示,如果 Mail 項中PreFirstRun鍵值不為空,則彈窗優先顯示其內容,*分隔內容和標題

但此彈窗內容會誤導用戶,因為控制面板默認程序中只能設置 mailto 關聯郵箱而不能設置 MAPI 關聯郵箱,兩者無關


另外建議異步調用,否則外部出錯可能會卡死進程

比如同時安裝了 Outlook(Classic) 和 Outlook(New) 且啟用 New 時,Outlook(Classic)后臺啟動后會調起 Outlook(New)并結束自身,提前關閉了 Outlook(New) 主窗口, 或設置了收件人、抄送、密送

而且下文中的 Outlook COM 和命令行方式也都是只支持 Classic 不支持 New,所以我們需要一個判斷是否啟用了 Outlook(New) 的方法

這里我們可以使用 AssocQueryString 根據 ProgID 獲取其友好名稱來判斷是否安裝了新版 Outlook,上文代碼中也提到了 Win10 以上系統可以用 AssocQueryString 直接查詢 mailto 關聯的 ProgID,而下文中也會用其根據 ProgID 獲取關聯可執行文件路徑

/// <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 默認預裝)的情況下,無法通過 MAPI 方式調起,如若可獲知 Outlook(Classic) 是如何啟動 Outlook(New) 即可有方法單獨啟動 Outlook(New)。現今未找到只安裝了 Outlook(New) 創建帶附件郵件的方法

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);
    }
}

用到的本機方法、結構體、枚舉
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>獲取結構體數組指針</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>釋放結構體數組指針</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 可設 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>發生一個或多個未指定錯誤</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,
}
[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
}

調用其他 MAPI 郵箱

已知第三方郵箱應用包含 MAPI 相關導出函數的 dll 位置時,可通過 GetProcAddress 來調用

[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;
}

上述方法需要調用程序和 dll 位數相同,故為了兼容不同郵箱應用可能需要分別編譯 x64 和 x86 的程序

這里提供一種兼容不同位數郵箱應用的方法:臨時將目標郵箱設為 MAPI 關聯郵箱,調用 MAPISendMail 后還原

/// <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;
        }
    }
}

Outlook(Classic)

Outlook(Classic) 還支持 COM 互操作和命令行的方式創建帶附件的郵件,Outlook(New) 兩種方式都不支持

/// <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));
}

Outlook COM

通過引用 Microsoft.Office.Interop.Outlook 互操作庫可用 COM 對象來創建帶附件的郵件,支持添加多個附件

當同時安裝了 Classic 和 New 且啟用 New 時此方式無效:會卡在創建 app 對象

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);
        }
    }
}

Outlook 命令行

命令行方式只能添加一個附件

當同時安裝了 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 命令行方式發送郵件</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;
}

其他郵箱應用

針對下方已知 ProgID 且支持命令行方式的郵箱應用,利用 AssocQueryString 可以快速獲取可執行文件路徑

/// <summary>通過關聯字符串查找可執行文件位置</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 發送郵件</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;
}

eM Client

New mail with multiple attachments with command line

eM Client 命令行方式是通過創建 .eml 文件并打開的方式創建郵件

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;
}

網易郵箱大師

命令行來自于文件系統對象發送到子菜單中的快捷方式

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;
}

調用默認郵箱

綜上,Windows 上默認郵箱有 mailto 關聯郵箱和 MAPI 關聯郵箱,但不懂注冊表的普通用戶可能只會在控制面板更改 mailto 關聯郵箱,為提高兼容性,我們可以用以下步驟一一嘗試調用默認郵箱:

  1. 當 MAPI 關聯郵箱存在時(避免系統彈窗提示無關聯郵箱),直接調用 MAPI 關聯郵箱

  2. 讀取 mailto 關聯郵箱 ProgID,并嘗試在 MAPI Mail 注冊表子項下找到對應的項,臨時設為 MAPI 關聯郵箱調用

  3. MAPI 方式失敗后,嘗試使用 COM 或命令行方式

  4. 以上支持添加附件的方式都失敗后,最后使用 mailto 方式

/// <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>>
            {
                [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;
}

轉自https://www.cnblogs.com/BluePointLilac/p/19010985


該文章在 2025/8/1 9:05:18 編輯過
關鍵字查詢
相關文章
正在查詢...
點晴ERP是一款針對中小制造業的專業生產管理軟件系統,系統成熟度和易用性得到了國內大量中小企業的青睞。
點晴PMS碼頭管理系統主要針對港口碼頭集裝箱與散貨日常運作、調度、堆場、車隊、財務費用、相關報表等業務管理,結合碼頭的業務特點,圍繞調度、堆場作業而開發的。集技術的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業的高效ERP管理信息系統。
點晴WMS倉儲管理系統提供了貨物產品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質期管理,貨位管理,庫位管理,生產管理,WMS管理系統,標簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務都免費,不限功能、不限時間、不限用戶的免費OA協同辦公管理系統。
Copyright 2010-2025 ClickSun All Rights Reserved

黄频国产免费高清视频,久久不卡精品中文字幕一区,激情五月天AV电影在线观看,欧美国产韩国日本一区二区
在线亚洲欧美日韩 | 欧美v日韩v国产 | 亚洲国产激情电影综合在线观看 | 好吊妞在线免费观看 | 婷婷色婷婷开心五月 | 亚洲中文字幕久久久 |