Skip to content
無料の100クレジットで検証を始める
Qualisend
すべての記事
エンジニアリング / 2026年6月13日

C#でメールアドレスを検証する方法

約10分で読めます

Qualisend team
C#でメールアドレスを検証する方法

C#でメールアドレスを検証するとき、多くの開発者はまずSystem.Net.Mail.MailAddressを使い、それで終わりにしてしまいます。この型はフレームワークに組み込まれており、自前の正規表現を書かなくても不正な文字列を弾けるため、最初の一手としては妥当です。ただし、実際のメールアドレス検証が問う3つの質問のうち、答えられるのは最初の1つだけです。アドレスの形式は正しいか。ドメインはメールを受信できるか。メールボックスは本当に存在するか。.NETの基本クラスライブラリは1つ目にきれいに答え、2つ目には定番のNuGetパッケージが1つ必要で、3つ目はネットワークの問題として外部に任せるのが得策です。このガイドでは、C#のメールアドレス検証を3つのレイヤーに分け、それぞれを慣用的なasync/awaitのコードで組み立てます。あわせて、MailAddressがどこで限界を迎えるかも示します。

結論#

構文チェックにはMailAddress.TryCreate、MXルックアップにはDnsClient.NETパッケージ、SMTPによるメールボックスの確認には検証APIを使います。コストの低いものから順に実行し、いずれかのレイヤーで結論が出た時点で打ち切ります。アプリからSMTPのソケットを直接開いてメールボックスを調べるのは避けてください。ほとんどのホスティング環境でアウトバウンドのポート25がブロックされており、結果は送信元IPのレピュテーションやグレーリスティングに左右されるため、自前で再現するのは割に合いません。各レイヤーは、後のレイヤーより低いコストでアドレスを除外できます。アドレスを有効と判断できるのは最後のレイヤーだけです。1件のアドレスの判定がいますぐ欲しいだけなら、無料メールチェッカーに貼り付ければコードは不要です。以降の内容は、チェックをアプリに組み込む方向けです。各段階の背景にある考え方は、メールアドレス検証とはをご覧ください。

レイヤー1:MailAddress.TryCreateによる構文チェック#

レイヤー1にはMailAddressが適しています。.NET 5で追加されたTryCreateオーバーロードを使いましょう。不正な入力でもFormatExceptionをスローせずboolを返すため、呼び出しごとにtry/catchで囲まなくても判定に使えます。

using System.Net.Mail;

public static bool IsValidSyntax(string email)
{
    return !string.IsNullOrWhiteSpace(email)
        && email.Length <= 320
        && MailAddress.TryCreate(email, out var parsed)
        && parsed.Address == email;
}
IsValidSyntax("jane@example.com");           // true
IsValidSyntax("not-an-email");               // false
IsValidSyntax("Jane <jane@example.com>");    // false

最後の比較が重要です。MailAddressは厳密なバリデーターではなくメールのパーサーであり、Jane <jane@example.com>のような表示名付きの形式も受け付け、アドレス部分をparsed.Addressとして返します。parsed.Addressを元の入力と比較すれば、そうした形式や余分な空白を弾けます。ユーザー入力に対する可否判定の構文チェックとして、これが望ましい動作です。320文字の上限は念のための保険です。それより長い文字列が実在のアドレスになることはなく、巨大な文字列を後続の処理に渡すより、早い段階で弾くほうが低コストです。

.NET Frameworkや.NET 5より前のターゲットにはTryCreateがないため、コンストラクターを使って例外をキャッチします。

try { _ = new MailAddress(email); /* syntax ok */ }
catch (FormatException) { /* reject */ }

モデルですでにデータアノテーションを使っているなら、System.ComponentModel.DataAnnotations.EmailAddressAttribute([EmailAddress])でも宣言的に同じ役割を果たせます。ASP.NET Coreのモデル検証の中で使うには十分な選択です。どちらを選ぶ場合でも、通過した結果は「きちんと調べる価値がある」という意味にとどめ、「有効」とは扱わないでください。構文チェックはDNSもメールボックスも知りません。これは正規表現によるメールアドレス検証が失敗するのと同じ理由です。形式と到達性は別の問いであり、前者は文字列についての事実、後者はインターネットについての事実です。

レイヤー2:ドメインはメールを受信できるか#

メールの経路がないドメインは、誰宛てのメールも受け付けられません。そのため、このルックアップ1回で、存在しないドメイン、会社名のつづり間違い、架空のTLDを、本格的にネットワークへ出る前に除外できます。ただし注意点があります。.NETにはMXを解決する標準機能がありません。System.Net.Dnsが解決できるのはAレコードとAAAAレコードまでで、MXは対象外です。そのため定番の答えはDnsClient.NETです。これは広く使われ、メンテナンスも行き届いたNuGetパッケージで、ホスト名の検索以上のDNS処理が必要な.NETプロジェクトの多くが採用しています。インストールして、MXレコードタイプを問い合わせます。

using System.Linq;
using DnsClient; // dotnet add package DnsClient

// LookupClient is thread-safe and caches responses — create one and reuse it.
private static readonly LookupClient Dns = new();

public static async Task<bool> HasMailRouteAsync(string domain)
{
    try
    {
        var response = await Dns.QueryAsync(domain, QueryType.MX);
        return response.Answers.MxRecords().Any();
    }
    catch (DnsResponseException)
    {
        // No reachable resolver, SERVFAIL, and similar — treat as "check later".
        return false;
    }
}
await HasMailRouteAsync("gmail.com");               // true
await HasMailRouteAsync("company-that-folded.com"); // false

真似しておきたい慣用的な書き方が2つあります。1つ目は、LookupClientを1回だけ作成して使い回すことです。スレッドセーフでレスポンスをキャッシュするため、リクエストごとにインスタンスを作るとキャッシュが無駄になります。2つ目は、範囲式を使って最後の@でアドレスからドメインを切り出すことです。@を複数含むアドレス(仕様上は有効です)を誤って分割せずに済みます。

var domain = email[(email.LastIndexOf('@') + 1)..];

ドメインの中には、MXがなくAレコードでメールを受け付けるもの(暗黙のMX)があります。この例外にも対応したい場合は、MXの結果が空のときにQueryType.Aへフォールバックしてください。ただし、実在するアドレスの圧倒的多数には、MXチェックが適切なフィルターです。サインアップ処理では一点注意が必要です。DNSクエリはリゾルバーの応答を待つ間ブロックされるため、一時的なDNSの不調で本物の顧客を即座に拒否してはいけません。ルックアップの失敗は「無効」ではなく「後で確認」として扱いましょう。

レイヤー3:メールボックスは本当に存在するか#

レイヤー1と2でできるのは、アドレスの除外だけです。ドメインに正しいMXレコードが公開されていても、手元のアドレスにメールボックスがあるとは限りません。noreply-9f2x@gmail.comは、メール経路のあるドメイン上の有効な構文ですが、作成されたことのないメールボックスです。特定のメールボックスの存在を確認するには、SMTPの配信のやり取りが必要です。メールホストに接続してRCPT TOを発行し、応答を読み取り、何も送信せずに切断します。メールアドレス検証の仕組みでは、この一連の流れを詳しく解説しています。すべてのアドレスを受け付けて単純なプローブを無効化するキャッチオールドメインも取り上げています。

原則として、TcpClientと生のSMTPコマンドを使えばC#でも実装できます。しかし実際には、アプリケーションサーバーから実行すべきではありません。ほとんどのクラウドプロバイダーはアウトバウンドのポート25をブロックしており、手元のノートPCで動くプローブが本番では黙って失敗します。結果は、確認対象のアドレスだけでなく、接続元のIPのレピュテーションにも左右されます。さらに、受信サーバーは見慣れない送信者をグレーリスティングやレート制限の対象にするため、大量にプローブすると遅延やブラックリスト入りを招きます。このレイヤーは、そのために作られたインフラに任せる価値があります。

C#から検証APIを呼び出す#

Qualisendの検証エンドポイントは、構文チェック、DNS、SMTPによるメールボックスのプローブという一連の処理を、レピュテーションを管理したインフラから実行し、判定結果をリアルタイムで返します。必要なのはHttpClientとSystem.Text.Jsonだけです。レスポンスのエンベロープをレコードとして定義し、JSONソースジェネレーターまたはリフレクションのデシリアライザーに処理させます。

using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Serialization;
using System.Threading.Channels;

public record VerifyResponse(
    [property: JsonPropertyName("job_id")] string JobId,
    [property: JsonPropertyName("result")] VerifyResult Result);

public record VerifyResult(
    [property: JsonPropertyName("status")] string Status,
    [property: JsonPropertyName("score")] int Score,
    [property: JsonPropertyName("reason")] string? Reason,
    [property: JsonPropertyName("sub_flags")] IReadOnlyDictionary<string, bool> SubFlags,
    [property: JsonPropertyName("smtp")] string? Smtp = null); // pending | cached | skipped
// One HttpClient for the whole app — don't new one up per request.
private static readonly HttpClient Http = new()
{
    BaseAddress = new Uri("https://app.qualisend.com/api/v1/"),
    Timeout = TimeSpan.FromSeconds(5), // fail fast: the default is 100 s
};

// Fail open: when the API can't answer, let the address through and re-check it later.
private static readonly VerifyResult FailOpen =
    new("unknown", 0, null, new Dictionary<string, bool>());

// (email, job id) pairs whose mailbox probe is still running. A background
// worker drains this channel; use a durable queue in production.
private static readonly Channel<(string Email, string JobId)> PendingProbes =
    Channel.CreateUnbounded<(string Email, string JobId)>();

public static async Task<VerifyResult> VerifyAsync(string email)
{
    using var request = new HttpRequestMessage(HttpMethod.Post, "verify")
    {
        Content = JsonContent.Create(new { email }),
    };
    request.Headers.Authorization = new AuthenticationHeaderValue(
        "Bearer", Environment.GetEnvironmentVariable("QUALISEND_API_KEY"));

    try
    {
        using var response = await Http.SendAsync(request);
        var code = (int)response.StatusCode;
        if (code == 402 || code == 429 || code >= 500)
            return FailOpen; // out of credits, rate limited, or a server error
        response.EnsureSuccessStatusCode(); // 400/401/403: fix the request or the key

        var body = await response.Content.ReadFromJsonAsync<VerifyResponse>();
        if (body!.Result.Smtp == "pending")
            PendingProbes.Writer.TryWrite((email, body.JobId)); // finished off the request path
        return body.Result; // status: deliverable | risky | undeliverable | unknown
    }
    catch (TaskCanceledException)
    {
        return FailOpen; // timed out
    }
}

このサンプルはPOST https://app.qualisend.com/api/v1/verifyを呼び出します。スコープ付きキーの形式とすべてのフィールドについては、/developersのAPIリファレンスをご覧ください。キーはYOUR_API_KEYのように直接書き込まず、環境変数から読み取ってください。また、ソケットの枯渇を避けるため、HttpClientは1つを使い回します。サインアップフォームでは、VerifyAsyncはフェイルオープンで動作します。402(クレジット切れ)、429(レート制限)、5xx、タイムアウトのいずれでもunknownを返すため、本物の顧客は通過でき、アドレスは後で再確認できます。レスポンスはresultエンベロープに入って返されます。

{
  "job_id": "3f9a6c1e-5b7d-4e2a-9c8f-1d2e3a4b5c6d",
  "result": {
    "status": "unknown",
    "score": 61,
    "reason": null,
    "smtp": "pending",
    "sub_flags": { "role": false, "disposable": false, "free_mail": false, "catch_all": false }
  }
}

statusは主となる判定結果で、deliverable、risky、undeliverable、unknownのいずれかです。scoreは確信度を表し、reasonは否定的な結果の理由を示します。sub_flagsはrole、disposable、free_mail、catch_allなどのシグナルを個別に示すため、自社のポリシーを適用できます(undeliverableは拒否、riskyは確認のため保留、disposableはサインアップ時にフラグを立てる、など)。

statusを最終結果として扱う前に、smtpを確認してください。cachedまたはskippedなら、判定はすでに確定しています。pendingの場合、メールボックスのプローブはまだ実行中です。statusは暫定のunknown、reasonはnullで、最終的な判定はGET /jobs/{job_id}に届きます。そのためVerifyAsyncは、保留中のジョブを待たずにキューへ入れます。バックグラウンドワーカー(たとえばホステッドサービス)がPendingProbesを読み取り、リクエスト処理とは別に数秒おきに各ジョブをポーリングします。同じアドレスで/verifyを再び呼び出すことはありません。

public record JobResponse(
    [property: JsonPropertyName("status")] string Status,
    [property: JsonPropertyName("results")] IReadOnlyList<VerifyResult>? Results);

// Runs in the background worker, never in the web request. Returns null while
// the probe is still running: call it again in a few seconds.
public static async Task<VerifyResult?> GetFinalVerdictAsync(string jobId)
{
    using var request = new HttpRequestMessage(HttpMethod.Get, $"jobs/{jobId}?include=results");
    request.Headers.Authorization = new AuthenticationHeaderValue(
        "Bearer", Environment.GetEnvironmentVariable("QUALISEND_API_KEY"));

    using var response = await Http.SendAsync(request);
    response.EnsureSuccessStatusCode();

    var job = await response.Content.ReadFromJsonAsync<JobResponse>();
    return job!.Status switch
    {
        "completed" => job.Results?.FirstOrDefault(), // the final, post-SMTP verdict
        "queued" or "processing" => null,             // not finished yet
        _ => throw new InvalidOperationException($"Job {jobId} {job.Status}"), // canceled | failed
    };
}

3つのレイヤーを組み合わせてC#でメールアドレスを検証する#

コストの低いものから順に実行し、答えが出た時点で止めます。

public static async Task<VerifyResult> ValidateEmailAsync(string email)
{
    if (!IsValidSyntax(email))
        return new VerifyResult("undeliverable", 0, "invalid_email",
            new Dictionary<string, bool>());

    var domain = email[(email.LastIndexOf('@') + 1)..];
    if (!await HasMailRouteAsync(domain))
        return new VerifyResult("undeliverable", 0, "invalid_domain",
            new Dictionary<string, bool>());

    return await VerifyAsync(email); // deliverable | risky | undeliverable | unknown
}

2つのローカルレイヤーはコストがかからず、不正なアドレスの大半を即座に除外できます。APIレイヤーは、ネットワークの往復に見合うアドレスにだけ実行します。この順序こそが肝です。Node.js版、Python版、PHP版のガイドも同じ構成です。検証を機能させるのは言語ではなく、レイヤーの重ね方だからです。

よくある質問

01C#でメールアドレスを検証するにはMailAddressやEmailAddressAttributeで十分ですか?

構文チェックであれば十分です。どちらもレイヤー1の確かなチェックで、自前の正規表現より安心でき、MailAddress.TryCreateなら例外をスローせずbool値が得られます。ただし検証できるのは形式であり、到達性ではありません。どちらもDNSを解決せず、メールサーバーにも接続しないため、通過しても「メールアドレスの形をしている」という意味で、「配信できる」とは限りません。アドレスを信頼する前に、MXルックアップとSMTPによるメールボックスの確認を組み合わせてください。

02.NETにはMXレコードを調べる標準機能がありますか?

ありません。System.Net.Dnsが解決できるのはAレコードとAAAAレコードのホスト情報までで、MXには対応していません。そのため標準的な方法はDnsClient.NETのNuGetパッケージです。QueryType.MXを問い合わせ、LookupClientは1つを使い回し、結果が空の場合やDnsResponseExceptionが発生した場合は、例外を投げずにメール経路なしとして扱ってください。

03APIを使わずにC#でメールボックスの存在を確認できますか?

部分的にしかできません。DnsClient.NETを使えばドメインがメールを受け付けるかを確認でき、存在しないドメインを低コストで除外できます。実際のメールボックスを確認するにはSMTPのやり取りが必要で、TcpClientで試すことはできますが、アプリサーバーから実行すべきではありません。アウトバウンドのポート25は広くブロックされており、結果は送信元IPのレピュテーションにも左右されるからです。この部分を引き受けるのが検証サービスです。自前で構築する前に、検証サービスを比較しておくことをおすすめします。

04メールアドレスの検証はサインアップ時とリストのクリーニング時のどちらで行うべきですか?

どちらでも行いますが、深さは変えます。サインアップ時は構文チェックとMXルックアップを同期的に実行します。リクエストをブロックしても問題ない速さで、すぐにフィードバックを返せるからです。APIの即時の判定結果にも、その場で対応してください。一連の流れはサーバーレスのサインアップガイドで紹介しています。より深いバッチ検証は、遅延が問題にならず、徹底さが重視される既存リストのクリーニングに取っておきます。


SMTPレイヤーを追加する準備はできましたか?APIリファレンスには、/verifyの正確な形式、スコープ付きキー、コピーして使えるサンプルがあります。無料メールチェッカーにアドレスを1件入力すれば、MailAddressが通した文字列がundeliverableで返ってくる様子も確認できます。

送信者の信頼を、守る。

最初のリストを数分でクリーニング。100クレジット無料、クレジットカード登録は不要です。

今すぐ始める