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

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

約9分で読めます

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

Goでメールアドレスを検証する作業は、実は3つの仕事が1つの名前で呼ばれているだけです。正規表現を示して終わるチュートリアルが解決しているのは、そのうち1つにすぎず、しかも最も役に立たないものです。実際の検証は、低コストの構文チェック、DNSルックアップ、SMTPによるメールボックスの確認という、コストの低い順の層構造で行います。Goの標準ライブラリなら、最初の2つはnet/mailとnetだけで、依存パッケージなしにすっきり実装できます。3つ目はネットワークの問題であり、外部に任せる価値があります。このガイドでは、各層を実際に動くGoらしいコードで組み立て、検証APIにどこから任せるかを具体的に示します。

結論#

構文にはnet/mail.ParseAddress、MXルックアップにはnet.LookupMX、SMTPによるメールボックスの確認には検証APIを使います。この順序で、コストの低いものから実行し、どれかの層で結論が出た時点で処理を打ち切ります。メールボックスのプローブのために、自社サーバーからnet/smtpを使うのはやめてください。アウトバウンドのポート25はほとんどのホストでブロックされています。ブロックされていない環境でも、結果は送信元IPのレピュテーションやグレーリスティングの挙動に左右されるため、自前で再現するのは得策ではありません。各層は、前の層より低いコストでアドレスを除外できますが、アドレスを有効と確定できるのは最後の層だけです。

レイヤー1:net/mailによる構文チェック#

ここで手書きの正規表現は必要ありません。標準ライブラリのnet/mailパッケージはRFC 5322のアドレスを解析し、ParseAddressは不正な入力に対してnilではないエラーを返します。これは構文チェックの関門に必要なyes/noの判定そのものです。

package main

import "net/mail"

func isValidSyntax(email string) bool {
	if len(email) > 320 {
		return false
	}
	addr, err := mail.ParseAddress(email)
	return err == nil && addr.Address == email
}

addr.Address == emailの比較は、多くのコード例が見落とす部分です。ParseAddressはメールボックス行全体を読み取る設計のため、表示名もそのまま受け入れます。mail.ParseAddress("Jane Doe <jane@example.com>")はエラーを返さず、addr.Addressにはjane@example.comが入ります。サインアップのフィールドで必要なのは、素のアドレスだけです。そこで、解析したAddressを入力と照合し、表示名、山括弧、末尾の空白が付いたものは拒否します。

isValidSyntax("jane@example.com")            // true
isValidSyntax("Jane Doe <jane@example.com>") // false — not a bare address
isValidSyntax("not-an-email")                // false — ParseAddress errors
isValidSyntax("a@@b.com")                    // false

ここで成功しても、意味は「確認する価値がある」であり、「有効」ではありません。文字列がアドレスの形をしていることが分かるだけで、メールが届くかどうかは何も分かりません。正規表現によるメールチェックが突き当たるのと同じ壁があり、net/mailもその壁の手前にあります。これはパーサーであって、到達可否を判定するものではありません。以降の層はすべて、構文が正常であることを前提にしています。

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

ここでは標準ライブラリが、依存パッケージなしで力を発揮します。MXレコードを公開していないドメインには、メールの届け先がありません。そのため、ルックアップを1回行うだけで、存在しないドメイン、社名のスペルミス、架空のTLDを除外できます。net.LookupMXでMXレコードを解決します。

package main

import "net"

func hasMailRoute(domain string) bool {
	mxs, err := net.LookupMX(domain)
	if err != nil {
		return false
	}
	return len(mxs) > 0
}
hasMailRoute("gmail.com")               // true
hasMailRoute("company-that-folded.com") // false

LookupMXは、優先度順に並んだ*net.MXのスライスを返します。ドメインが解決できない場合は*net.DNSErrorを返します。バリデーターとしては、エラーや空のスライスを「メール経路なし」と扱うのが安全な解釈です。明示的なMXがなくAレコードでメールを受け付けるドメイン(いわゆる暗黙のMX)も有効としたい場合は、MXのスライスが空だったときにnet.LookupHost(domain)にフォールバックします。ただし、実在するアドレスの大多数については、MXチェックが適切なフィルターです。

運用上の注意点があります。LookupMXはDNSリゾルバーの応答を待ってブロックするため、ネームサーバーが遅い、または到達できない場合はゴルーチンが停止します。リクエストの処理経路では、コンテキストに対応したリゾルバー(&net.Resolver{}).LookupMX(ctx, domain)をデッドライン付きで使ってください。タイムアウトは即時の拒否ではなく「後で再確認」として扱います。DNSの一時的な不調で、実在する顧客を締め出さないためです。

レイヤー3:メールボックスは実在するか#

レイヤー1と2でできるのは、アドレスを除外することだけです。ドメインに正しいMXレコードがあっても、手元のアドレスにメールボックスがあるとは限りません。noreply-9f2x@gmail.comは構文が正しく、メール経路も有効ですが、実在するメールボックスだったことはありません。特定のメールボックスを確認するには、SMTPの配信時のやり取りが必要です。メールホストに接続し、RCPT TOを発行して応答を読み取り、何も送信せずに切断します。

Goにはそのための道具が揃っています。net/smtpにはsmtp.Dialと、Mail、Rcpt、Closeの各メソッドを持つClientがあり、原理的にはこのハンドシェイクを自分で実行できます。しかし実際には、アプリケーションサーバーから実行すべきではありません。ほとんどのクラウドプロバイダーはアウトバウンドのポート25を完全にブロックしているため、手元のノートPCでは動いても、本番ではダイヤルがタイムアウトするだけです。ポートが開いている環境でも、結果は接続元IPのレピュテーションに左右されます。さらに、受信サーバーは見慣れない送信者に対してグレーリスティングやレート制限を行うため、単純なプローブは保留されたり、スロットリングされたり、ブラックリストに載ったりします。1回のやり取りでは済まない事情もあります。キャッチオールドメインはすべてのアドレスを受け付けるため、1回のRCPT TOでは見分けられません。全体のパイプラインについては、メールアドレス検証の仕組みで解説しています。ここは外部に任せる価値のある層です。

APIで完全なチェックを行う#

QualisendのリアルタイムAPIは、構文、DNS、SMTPによるメールボックスの確認というパイプライン全体を、そのために構築されたレピュテーション管理済みのインフラから実行し、判定結果を返します。単純なJSONのPOSTなので、標準ライブラリのnet/httpとencoding/jsonだけで足ります。resultのエンベロープは、構造体へそのままデコードします。

package main

import (
	"bytes"
	"context"
	"encoding/json"
	"fmt"
	"net/http"
	"os"
)

type VerifyResult struct {
	Status   string          `json:"status"` // deliverable | risky | undeliverable | unknown
	Score    int             `json:"score"`
	Reason   string          `json:"reason"`
	SubFlags map[string]bool `json:"sub_flags"`
	SMTP     string          `json:"smtp"` // pending | cached | skipped
	JobID    string          `json:"-"`    // from the top-level job_id
}

type verifyResponse struct {
	JobID  string       `json:"job_id"`
	Result VerifyResult `json:"result"`
}

func verify(ctx context.Context, email string) (VerifyResult, error) {
	body, err := json.Marshal(map[string]string{"email": email})
	if err != nil {
		return VerifyResult{}, err
	}

	req, err := http.NewRequestWithContext(ctx, http.MethodPost,
		"https://app.qualisend.com/api/v1/verify", bytes.NewReader(body))
	if err != nil {
		return VerifyResult{}, err
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("QUALISEND_API_KEY"))
	req.Header.Set("Content-Type", "application/json")

	res, err := http.DefaultClient.Do(req)
	if err != nil {
		return VerifyResult{}, err
	}
	defer res.Body.Close()

	if res.StatusCode != http.StatusOK {
		return VerifyResult{}, fmt.Errorf("qualisend: unexpected status %s", res.Status)
	}

	var payload verifyResponse
	if err := json.NewDecoder(res.Body).Decode(&payload); err != nil {
		return VerifyResult{}, fmt.Errorf("qualisend: decode response: %w", err)
	}
	payload.Result.JobID = payload.JobID
	return payload.Result, nil
}

スコープを限定したキーは、ハードコードせず環境変数QUALISEND_API_KEYから読み込みます。リクエストとレスポンスの正確な形式は、開発者ページのAPIリファレンスで確認してください。resultオブジェクトには、deliverable、risky、undeliverable、unknownのいずれかであるstatus、数値のscore、reason、そしてsub_flagsのマップ(役割アドレス、使い捨て、フリーメールのプロバイダー、キャッチオールなど)が含まれます。実際に分岐に使うのは、これらのフィールドです。

レスポンスはメールボックスのプローブの完了を待たないため、SMTPを見れば、どちらの種類の回答かが分かります。cachedまたはskippedなら、結果はすでに確定しています。pendingは、statusがローカルチェックの結果を反映したもので、reasonはまだ空であり、SMTPの判定はジョブに届くことを意味します。取得先はGET https://app.qualisend.com/api/v1/jobs/{job_id}?include=resultsで、IDはJobIDに入っています。ジョブがcompleted、canceled、failedのいずれかになるまで、バックグラウンドのワーカーから数秒おきにポーリングしてください。サインアップのリクエストの中では行わず、同じアドレスを更新するために/verifyを再度呼び出すのもやめてください。

verifyはcontext.Contextを受け取るため、タイムアウトとキャンセルは呼び出し側が管理します。検証が遅れてもサインアップのリクエストが止まらないよう、デッドラインを渡します。

ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()

result, err := verify(ctx, "jane@example.com")
if err != nil {
	log.Printf("verify failed: %v", err)
}

層を組み合わせてGoでメールアドレスを検証する#

コストの低いものから実行し、答えが出た時点で止めます。ローカルの2つの層はコストがかからず、不正なアドレスの大半を即座に弾きます。APIの呼び出しは、それらを通過し、ネットワークの往復に見合うアドレスにだけ実行されます。

package main

import (
	"context"
	"strings"
)

func validateEmail(ctx context.Context, email string) (VerifyResult, error) {
	if !isValidSyntax(email) {
		return VerifyResult{Status: "undeliverable", Reason: "invalid_email"}, nil
	}

	domain := email[strings.LastIndex(email, "@")+1:]
	if !hasMailRoute(domain) {
		return VerifyResult{Status: "undeliverable", Reason: "invalid_domain"}, nil
	}

	return verify(ctx, email)
}

あとは、プロダクトの要件に合わせてstatusで分岐します。配信不能は拒否し、配信可能は通し、曖昧な判定についてはどう扱うかを機能ごとに決めます。

result, err := validateEmail(ctx, "jane@example.com")
if err != nil {
	// network/API failure — fail open or retry, don't block a real user
}

switch result.Status {
case "deliverable":
	// accept
case "undeliverable":
	// reject at the form
default:
	// risky | unknown — flag for review, or allow with a soft warning
}

この順序こそが肝心な点です。このガイドのNode.js版やPython版でも、同じ構成になっています。検証を確実にするのは、言語ではなく層構造です。各段階がその位置にある理由は、メールアドレス検証の仕組みを参照してください。

よくある質問

01net/mail.ParseAddressだけでGoのメールアドレス検証は足りますか?

構文チェックとしては十分です。net/mail.ParseAddress はレイヤー1のチェックとして適切で、RFC 5322に沿って解析し、不正な入力にはエラーを返すため、手書きの正規表現よりはるかに確実です。ただし、検証するのは到達可否ではなく形式です。DNSの解決もメールサーバーへの接続も行わないため、エラーがnilであることは「アドレスの形をしている」という意味であり、「配信できる」という意味ではありません。表示名を拒否するには addr.Address を入力と照合し、そのうえでMXルックアップとSMTPによるメールボックスのプローブを組み合わせてください。

02GoでMXレコードを調べるにはどうすればよいですか?

標準ライブラリの net.LookupMX(domain) を使います。優先度順に並んだ *net.MX レコードのスライスを返し、ドメインが解決できない場合は *net.DNSError を返します。エラーや空のスライスは「メール経路なし」として扱ってください。リクエストの処理経路では、コンテキストに対応した (*net.Resolver).LookupMX(ctx, domain) を使うと、ネームサーバーが遅くてもゴルーチンがデッドラインを超えて停止することを防げます。

03外部サービスなしで、Goでメールボックスを検証できますか?

一部は可能です。net.LookupMX なら、標準ライブラリだけでドメインがメールを受け付けることを確認でき、存在しないドメインを無料で除外できます。メールボックス自体の確認にはSMTPのやり取りが必要です。net/smtp でスクリプト化はできますが、アプリケーションサーバーからは実行しないでください。ポート25は広くブロックされており、結果は送信元IPのレピュテーションに左右されます。この層を担うために存在するのが検証サービスです。違いは、無料メールチェッカーでどのアドレスでも試せます。

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

両方で行いますが、深さが異なります。構文チェックとMXルックアップは、サインアップ時に同期的に実行します。リクエストをブロックしても問題ないほど高速で、その場でフィードバックを返せます。APIから即座に返る判定結果もそこで使います。SMTPで確認する、より深い処理は、リストのクリーニングや管理業務のジョブに回してください。リアルタイムのパターンは、サーバーレスのサインアップガイドで最初から最後まで確認できます。


GoのサービスにSMTPの層を追加する準備はできましたか。QualisendのAPIリファレンスには、スコープ付きキーとコピー&ペーストで使えるコード例を備えた/verifyエンドポイントがあります。コードを書く前に、無料メールチェッカーで、構文が完璧なアドレスがundeliverableと判定される様子も確認できます。

送信者の信頼を、守る。

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

今すぐ始める