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

ASP.NET Coreでメールアドレスを検証する方法

約10分で読めます

Qualisend team
ASP.NET Coreでメールアドレスを検証する方法

ASP.NET Coreでメールアドレスを検証するとき、ほとんどのアプリは組み込みの[EmailAddress]データ注釈を使い、モデルバインディングに実行させ、ModelState.IsValidを確認します。これは確かな第一歩です。この属性はフレームワークに付属しており、自前の正規表現なしで不正な値を弾けます。ただし、実際の検証で問うべき3つの質問のうち、答えられるのは最初の1つだけです。アドレスの形式は正しいか。ドメインはメールを受信できるか。メールボックスは実在するか。ASP.NET Coreは最初の質問にはそのまま答え、2番目には有名なNuGetパッケージが1つ必要で、3番目はネットワークの問題なので外部に任せる価値があります。このガイドでは、フレームワーク標準の検証パイプラインに組み込む3層のチェックを構築し、[EmailAddress]がどこまでしか役に立たないのかを具体的に示します。リクエストパイプラインの外側で同じ基本要素を扱うC#メールアドレス検証ガイドの、Webフレームワーク版にあたります。

結論#

構文には[EmailAddress](またはFluentValidationの.EmailAddress())、MXルックアップにはDnsClient.NET、SMTPによるメールボックスの確認には検証APIを使います。コストの低い順に実行し、結果が確定した時点で処理を打ち切ります。DNSとAPIの層は非同期I/Oのため、同期的なValidationAttributeには収まりません。これらを置く場所として定石なのは、DIでサービスを注入したFluentValidationのバリデーターです。メールボックスを調べるために、アプリから生のSMTPソケットを開くのはやめてください。送信用のポート25はほとんどのホストでブロックされており、結果は送信元IPのレピュテーションとグレーリスティングに左右されます。これらを自前で再実装するべきではありません。各層は、次の層よりも低コストでアドレスを除外しますが、アドレスを有効と判断できるのは最後の層だけです。パイプラインに不慣れな場合は、先にメールアドレス検証とはで用語を確認してください。

第1層:DataAnnotationsによる形式の検証#

組み込みの第1層のチェックは、System.ComponentModel.DataAnnotationsの[EmailAddress]属性です。リクエストモデルの[Required]の隣に付けると、モデルバインディングのたびに実行されます。

using System.ComponentModel.DataAnnotations;

public class SignupRequest
{
    [Required]
    [EmailAddress]
    public string Email { get; init; } = string.Empty;
}

[ApiController]が付いたMVCまたはAPIコントローラーでは、属性による検証に失敗すると、アクション本体が実行される前に400のValidationProblemDetailsレスポンスが返ります。ModelStateを手作業で調べる必要はありません。

using Microsoft.AspNetCore.Mvc;

[ApiController]
[Route("signup")]
public class SignupController : ControllerBase
{
    [HttpPost]
    public IActionResult Register(SignupRequest request)
    {
        // With [ApiController], an invalid [EmailAddress] already returned 400.
        // Reaching here means the string is well-formed.
        return Ok();
    }
}

この検証で得られるものを正確に把握しておきましょう。EmailAddressAttributeが確認するのは形式だけです。実際には、@が1つあり、その前後に何かがあることしか求めません。DNSの解決もソケットの接続も行わず、ドメインが存在するかどうかもわかりません。definitely-fake@gmail.comは通ります。info@company-that-folded.comも通ります。typo@gmial.comも通ります。3つとも配信不能であり、文字列を読むだけの属性では、それを知るすべがありません。正規表現によるメール検証が失敗するのと同じ理由です。形式と到達性は別の問題であり、前者は文字列に関する事実、後者はインターネットに関する事実だからです。

次の2層を属性にできない理由#

このガイドの残りを方向づける、重要な注意点があります。DataAnnotationsの検証は同期処理です。ValidationAttribute.IsValidが返すのはTaskではなくValidationResultなので、属性の中でDNSクエリやHTTP呼び出しをawaitする自然な方法がありません。非同期呼び出しを同期の属性に押し込むために.Resultや.GetAwaiter().GetResult()でブロックすると、負荷時にスレッドプールの枯渇やデッドロックを招きます。登録処理で最も避けたい事態です。

これが、非同期の層に対するASP.NET Coreの定石がFluentValidationである理由です。MustAsyncルールはTaskを返すバリデーターとして標準で用意されており、アプリの他の部分と同じDIコンテナーで依存関係を解決します。そのため、バリデーターのコンストラクターでHttpClientやDNSサービスを受け取れます。第1層には[EmailAddress]を残し、第2層と第3層にFluentValidationを追加する方法もあります。あるいは、以下のようにFluentValidationの.EmailAddress()で構文もカバーし、3層すべてを1か所にまとめる方法もあります。

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

メール経路のないドメインは、誰宛てのメールも受け付けられません。そのため、ルックアップ1回で、存在しないドメイン、会社名のスペルミス、架空のTLDを除外できます。注意点があります。ASP.NET Coreには組み込みのMXリゾルバーがありません。System.Net.DnsはAレコードとAAAAレコードを解決しますが、MXレコードは解決しません。そのため、標準的な方法は、広く使われているNuGetパッケージのDnsClient.NETです。小さなインターフェースで包み、バリデーターがライブラリではなく抽象に依存するようにします。

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

public interface IMailRouteChecker
{
    Task<bool> HasMailRouteAsync(string email, CancellationToken ct = default);
}

public sealed class DnsMailRouteChecker : IMailRouteChecker
{
    private readonly ILookupClient _dns;

    public DnsMailRouteChecker(ILookupClient dns) => _dns = dns;

    public async Task<bool> HasMailRouteAsync(string email, CancellationToken ct = default)
    {
        var domain = email[(email.LastIndexOf('@') + 1)..];
        try
        {
            var response = await _dns.QueryAsync(domain, QueryType.MX, cancellationToken: ct);
            return response.Answers.MxRecords().Any();
        }
        catch (DnsResponseException)
        {
            // No reachable resolver, SERVFAIL, and similar — treat as "check later".
            return false;
        }
    }
}

ドメインは、範囲式を使って最後の@で切り出します。正当な形で@を複数含むアドレスを誤って解析しないようにするためです。LookupClientはシングルトンとして1つだけ登録してください。スレッドセーフで、レスポンスをキャッシュするため、リクエストごとにインスタンスを作ると、そのキャッシュを捨ててしまいます。MXレコードがなく、Aレコードでメールを受け付けるドメインもあります。そのケースにも対応したい場合は、MXの結果が空のときにQueryType.Aにフォールバックしてください。

第3層:メールボックスは実在するか#

第1層と第2層でできるのは、アドレスを除外することだけです。ドメインが完全なMXレコードを公開していても、手元のアドレスにメールボックスが存在しないことはあります。noreply-9f2x@gmail.comは、有効なメール経路上の有効な構文ですが、作成されたことのないメールボックスです。特定のメールボックスを確認するには、SMTPの配信のやり取りが必要です。メールホストに接続し、RCPT TOを発行して応答を読み、何も送信しないうちに切断します。キャッチオールドメインを含むパイプライン全体は、メールアドレス検証の仕組みで説明しています。

原理上は、C#でTcpClientと生のSMTPコマンドを使って自作できます。しかし実際には、アプリサーバーから実行すべきではありません。ほとんどのクラウドプロバイダーは送信用のポート25をブロックしており、結果は接続元のIPのレピュテーションに左右されます。しかも、受信サーバーは見慣れない送信者をグレーリスティングしたりレート制限したりします。この層は、そのために作られたインフラに任せる価値があります。

Qualisendの検証エンドポイントは、レピュテーションを管理したインフラでパイプライン全体を実行し、リアルタイムで結果を返します。型付きHttpClientとしてモデル化すると、ベースURL、タイムアウト、キーを1か所にまとめられます。レスポンスのエンベロープはSystem.Text.Jsonでデシリアライズします。

using System.Net.Http.Json;
using System.Text.Json.Serialization;

public interface IEmailVerifier
{
    Task<bool> IsDeliverableAsync(string email, CancellationToken ct = default);
}

public sealed class QualisendVerifier : IEmailVerifier
{
    private readonly HttpClient _http;

    public QualisendVerifier(HttpClient http) => _http = http;

    public async Task<bool> IsDeliverableAsync(string email, CancellationToken ct = default)
    {
        try
        {
            using var response = await _http.PostAsJsonAsync("verify", new { email }, ct);

            // On our own outage or a rate limit, don't block a real signup.
            if (!response.IsSuccessStatusCode)
                return true;

            var body = await response.Content.ReadFromJsonAsync<VerifyResponse>(ct);
            return body?.Result.Status != "undeliverable";
        }
        catch (HttpRequestException)
        {
            return true; // network failure: fail open too
        }
        catch (TaskCanceledException) when (!ct.IsCancellationRequested)
        {
            return true; // the 10-second client timeout fired: fail open too
        }
    }
}

public record VerifyResponse(
    [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);

レスポンスはresultエンベロープに入って返ります。

{
  "job_id": "6f1c2e0a-9b3d-4a1e-8c77-1a2b3c4d5e6f",
  "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のいずれかです。上のヘルパーはundeliverableだけを失敗として扱い、それ以外は通しますが、score、reason、sub_flags(role、disposable、free_mail、catch_all)も返されます。これらを使って独自のポリシーを適用できます。たとえば、riskyは保留して確認する、disposableは登録時にフラグを立てる、といった運用です。エンドポイントはPOST https://app.qualisend.com/api/v1/verifyです。スコープ付きキーの形式とすべてのフィールドは、/developersのAPIリファレンスで確認してください。上のサンプルからフィールドを推測せず、ドキュメントで確かめてください。

この最初のレスポンスは、即時の判定です。result.smtpが"pending"の間は、SMTPによるメールボックスのプローブがまだ実行中で、reasonはnullのままです。最終的な判定はGET https://app.qualisend.com/api/v1/jobs/{job_id}で取得できます。登録時には即時の判定に基づいて処理し、最終的な判定が必要な場合は、バックグラウンドタスクからそのジョブをポーリングしてください。リクエスト内でポーリングしたり、/verifyを再度呼び出したりしてはいけません。

3つの層を組み合わせてASP.NET Coreでメールアドレスを検証する#

3つの層すべてを、FluentValidationのバリデーターにまとめます。ルールはコストの低い順に並べ、CascadeMode.Stopを設定して、最初に失敗した層で打ち切ります。非同期のDNSルールとAPIルールは、構文を通過したアドレスに対してのみ実行されます。

using FluentValidation;

public class SignupRequestValidator : AbstractValidator<SignupRequest>
{
    public SignupRequestValidator(IMailRouteChecker dns, IEmailVerifier verifier)
    {
        RuleFor(x => x.Email)
            .Cascade(CascadeMode.Stop)   // bail at the first failing layer
            .NotEmpty()
            .EmailAddress()              // layer 1: syntax
            .MustAsync((email, ct) => dns.HasMailRouteAsync(email, ct))
                .WithMessage("Enter an email on a domain that can receive mail.")
            .MustAsync((email, ct) => verifier.IsDeliverableAsync(email, ct))
                .WithMessage("We couldn't confirm a mailbox at this address.");
    }
}

サービスはProgram.csで登録します。LookupClientはシングルトン、検証用クラスは設定からキーを読み取る型付きクライアント(本番では環境変数やユーザーシークレットを使い、ハードコードしない)、バリデーターはアセンブリのスキャンで登録します。

using DnsClient;
using FluentValidation;
using System.Net.Http.Headers;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddSingleton<ILookupClient>(new LookupClient());
builder.Services.AddScoped<IMailRouteChecker, DnsMailRouteChecker>();

builder.Services.AddHttpClient<IEmailVerifier, QualisendVerifier>(client =>
{
    client.BaseAddress = new Uri("https://app.qualisend.com/api/v1/");
    client.Timeout = TimeSpan.FromSeconds(10);
    client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue(
        "Bearer", builder.Configuration["Qualisend:ApiKey"]);
});

builder.Services.AddValidatorsFromAssemblyContaining<SignupRequestValidator>();

var app = builder.Build();

app.MapPost("/signup", async (
    SignupRequest request,
    IValidator<SignupRequest> validator,
    CancellationToken ct) =>
{
    var result = await validator.ValidateAsync(request, ct);
    if (!result.IsValid)
        return Results.ValidationProblem(result.ToDictionary());

    // Every layer passed — safe to persist.
    return Results.Ok();
});

app.Run();

エンドポイントでIValidator<SignupRequest>を注入してValidateAsyncを呼び出すのが、現在推奨されているパターンです。非同期ルールが正しく実行されます。従来のMVC自動統合では実行されませんでした。同じバリデーターは、MVCコントローラーにもそのまま組み込めます。コンストラクターで解決し、データベースに触れる前にValidateAsyncを呼び出してください。

この順序がすべての要です。構文のルールが不正な値をコストなしで弾き、ローカルのDNSチェックが存在しないドメインをコストなしで除外し、APIは両方を通過したアドレスにのみ呼び出されます。これはNode.jsとC#のガイドと同じ3層の構造です。検証を信頼できるものにするのはフレームワークではなく、層の組み立てです。この高速なパイプラインは登録時に同期的に実行し、より深いバッチ検証は、遅延が問題にならず徹底さが重視される既存リストのクリーニングに回してください。

よくある質問

01ASP.NET Coreでのメールアドレス検証は、EmailAddress属性だけで十分ですか?

構文については十分です。第1層のチェックとして適切で、自前の正規表現より確実です。ただし、EmailAddressAttributeが検証するのは形式(実際には、前後にテキストがある@が1つあるかどうか)だけで、DNSの解決もメールサーバーへの接続も行いません。そのため、ModelState.IsValidを通過しても、「メールアドレスらしい」という意味であり、「配信できる」という意味ではありません。アドレスを信頼する前に、MXルックアップとSMTPによるメールボックスの確認を組み合わせてください。

02ASP.NET Coreで非同期のメール検証を実行するにはどうすればよいですか?

DataAnnotationsの属性は同期処理のため、DNSクエリやAPI呼び出しを入れるべきではありません。非同期呼び出しをブロックすると、デッドロックを招くおそれがあります。代わりにFluentValidationを使い、MustAsyncルールを書き、DNSサービスと検証サービスをバリデーターに注入して、エンドポイントやコントローラーからawait validator.ValidateAsync(request)を呼び出します。従来のMVC自動統合は非同期バリデーターを実行しなかったため、これが現在推奨されている方法です。

03ASP.NET CoreにはMXレコードを調べる組み込みの方法はありますか?

ありません。System.Net.DnsはホストのAレコードとAAAAレコードを解決しますが、MXには対応していません。そのため、標準的な方法はNuGetパッケージのDnsClient.NETです。QueryType.MXでクエリし、LookupClientを1つだけシングルトンとして登録します。結果が空の場合やDnsResponseExceptionが発生した場合は、例外を投げずに「メール経路なし」として扱います。

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

両方で、深さを変えて行います。構文とMXルックアップは、リクエストをブロックしても問題ないほど高速でその場でフィードバックを返せるため、登録時に同期的に実行します。APIの即時の判定に基づく処理も、ここで行います。エンドツーエンドのパターンは、サーバーレスの登録時検証ガイドで紹介しています。より深いバッチ検証は、遅延が問題にならない既存リストのクリーニングに回してください。


SMTPの層を追加しますか?APIリファレンスに、/verifyの正確な形式、スコープ付きキー、コピーして使える例があります。あるいは、無料メールチェッカーに1つアドレスを入力して、[EmailAddress]を通過した文字列がundeliverableと判定される様子を確かめてください。

送信者の信頼を、守る。

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

今すぐ始める