.NET 基礎類別庫(System.Net.Dns)僅支援 A/AAAA 記錄查詢,無法直接取得郵件交換器(MX)記錄。本文示範如何透過 P/Invoke 呼叫 Windows dnsapi.dll 中的 DnsQuery_W,完整解析 MX 記錄並取得郵件伺服器主機名稱與優先順序。
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.ComponentModel;
public static class DnsMxResolver
{
// 查詢選項
private enum DnsQueryOptions : uint
{
Standard = 0,
BypassCache = 0x8,
NoRecursion = 0x4,
// ... 其他選項略
}
// 記錄類型
private enum DnsRecordType : ushort
{
MX = 15,
// A = 1, AAAA = 28, etc.
}
// MX 記錄結構體(對應 DNS_RECORD 聯集中的 MX 部分)
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct DnsMxRecord
{
public IntPtr pNext; // 下一筆記錄指標
public string pName; // 記錄名稱
public ushort wType; // 記錄類型
public ushort wDataLength; // 資料長度
public uint flags;
public uint dwTtl;
public uint dwReserved;
public IntPtr pNameExchange; // 郵件伺服器主機名稱指標
public ushort wPreference; // 優先順序(數值越小優先級越高)
public ushort Pad;
}
// 匯入 DnsQuery_W
[DllImport("dnsapi.dll", CharSet = CharSet.Unicode, SetLastError = true, ExactSpelling = true)]
private static extern int DnsQuery(
[MarshalAs(UnmanagedType.VBByRefStr)] ref string pszName,
DnsRecordType wType,
DnsQueryOptions options,
IntPtr aipServers, // IntPtr.Zero 使用系統預設 DNS
ref IntPtr ppQueryResults,
IntPtr pReserved);
// 釋放記錄鏈結記憶體
[DllImport("dnsapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern void DnsRecordListFree(IntPtr pRecordList, int freeType);
///
/// 查詢指定網域的 MX 記錄
///
/// 依優先順序排序的 (Preference, HostName) 元組列表
public static List<(ushort Preference, string Host)> QueryMx(string domain)
{
if (Environment.OSVersion.Platform != PlatformID.Win32NT)
throw new PlatformNotSupportedException("僅支援 Windows NT 核心系列作業系統。");
IntPtr ptr = IntPtr.Zero;
int result = DnsQuery(ref domain, DnsRecordType.MX, DnsQueryOptions.BypassCache, IntPtr.Zero, ref ptr, IntPtr.Zero);
if (result != 0)
throw new Win32Exception(result, $"DnsQuery 失敗,錯誤碼:{result}");
var list = new List<(ushort, string)>();
try
{
IntPtr current = ptr;
while (current != IntPtr.Zero)
{
var rec = Marshal.PtrToStructure(current);
if (rec.wType == (ushort)DnsRecordType.MX)
{
string host = Marshal.PtrToStringUni(rec.pNameExchange);
list.Add((rec.wPreference, host.TrimEnd('.'))); // 移除結尾點
}
current = rec.pNext;
}
}
finally
{
DnsRecordListFree(ptr, 0); // 0 = DnsFreeRecordList
}
// 依優先順序排序
list.Sort((a, b) => a.Item1.CompareTo(b.Item1));
return list;
}
}
class Program
{
static void Main()
{
try
{
var mxRecords = DnsMxResolver.QueryMx("microsoft.com");
Console.WriteLine($"{"Priority",-10} {"Mail Server"}");
Console.WriteLine(new string('-', 50));
foreach (var (pref, host) in mxRecords)
{
Console.WriteLine($"{pref,-10} {host}");
}
}
catch (Exception ex)
{
Console.Error.WriteLine($"查詢失敗:{ex.Message}");
}
}
}
Priority Mail Server
--------------------------------------------------
10 microsoft-com.mail.protection.outlook.com
20 microsoft-com.mail.protection.outlook.com
| 需求 | 方案 |
|---|---|
| 跨平台 | 使用 DnsClient.NET (NuGet) 純 Managed 實作,支援 Linux/macOS |
| 查詢其他記錄類型 | 擴充 DnsRecordType 並定義對應結構體(A、AAAA、TXT、CNAME、SRV 等) |
| 指定自訂 DNS 伺服器 | 將 aipServers 參數傳入 IP4_ARRAY 結構體指標 |
| 非同步查詢 | 包裝為 Task.Run(() => QueryMx(domain)) 或使用 DnsClient.NET 內建非同步 API |
finally 區塊呼叫 DnsRecordListFree。CharSet.Unicode + DnsQuery_W + Marshal.PtrToStringUni 三者必須一致。雖然 .NET Core/.NET 5+ 仍未內建 MX 查詢,但透過 dnsapi.dll 即可在 Windows 環境獲得原生效能與完整功能。跨平台專案建議改用 DnsClient.NET 等成熟套件,避免維護 P/Invoke 細節。