ブログ一覧

Nodemailer の使い方と TypeScript の型定義(v9 対応)

更新: Web開発者向け

Node.js の定番メール送信ライブラリに Nodemailer があります。必要な機能が一通り揃っていて、Promise ベースで扱いやすく、TypeScript 環境にも問題なく導入できます。

この記事は 2021 年に書いたものです。Nodemailer はその後もメジャーバージョンを重ねていて、この記事を直している 2026 年 8 月時点の最新は 9.0.6 です。基本的な書き方は当時から変わっていないので、コードを v9 で動かし直して全体を整理しました。あわせて、@types/nodemailer で検索して来る方が多いことが分かったので、TypeScript の型定義の節を足しています。

インストール

npm install nodemailer

TypeScript 環境では型定義を別途インストールします。v9 になった今も型は Nodemailer 本体に同梱されていません。手元で確かめると、nodemailer のパッケージには .d.ts が 1 つも入っておらず、package.jsontypes の指定もありませんでした。

npm install -D @types/nodemailer

@types/nodemailer の最新は 8.0.1(2026 年 6 月)で、メジャーが 1 つ古く見えますが、Nodemailer 9.0.6 と組み合わせて strict の型チェックが通ることは確認しています。v8 から v9 の変更は挙動の話(後述)で、API の形は変わっていないためです。

設定

スマホやパソコンで独自ドメインのメールを使うときと同じ設定です。

  • ホスト名(Host Name)
  • メールアドレス(User Name)
  • パスワード(Password)

の 3 つの情報を使ってメールを送信します。これらの情報は、メールアドレスを設定したサービス(レンタルサーバーなど)の管理画面で確認できます。

import { createTransport } from "nodemailer";

const transporter = createTransport({
	host: "mail.example.com", // ホスト名
	port: 465,
	secure: true,
	auth: {
		user: "mail@example.com", // メールアドレス
		pass: "********", // パスワード
	},
});

注意点として、パスワードをこのままコードに書くと、GitHub などにそのままアップロードされてしまいます。セキュリティー上の理由から、以下のように環境変数を使ってください。プライベートなリポジトリだとしてもです。

import { createTransport } from "nodemailer";

const transporter = createTransport({
	host: "mail.example.com",
	port: 465,
	secure: true,
	auth: {
		user: process.env.MAIL_AUTH_USER, // 環境変数
		pass: process.env.MAIL_AUTH_PASS, // 環境変数
	},
});

TypeScript の型定義

@types/nodemailer を入れると、createTransport の引数と sendMail の引数・戻り値に型が付きます。使う型は 3 つ覚えておけば足ります。

  • Transporter<SMTPTransport.SentMessageInfo>: createTransport が返すもの。SMTP で送るときの sendMail の戻り値の型まで決まる
  • SendMailOptions: sendMail に渡すオブジェクト(from / to / subject / text など)
  • SMTPTransport.Options: createTransport に渡す SMTP の設定

SMTPTransportnodemailer/lib/smtp-transport から type import します。

import { createTransport, type Transporter, type SendMailOptions } from "nodemailer";
import type SMTPTransport from "nodemailer/lib/smtp-transport";

const options: SMTPTransport.Options = {
	host: "mail.example.com",
	port: 465,
	secure: true,
	auth: {
		user: process.env.MAIL_AUTH_USER,
		pass: process.env.MAIL_AUTH_PASS,
	},
};

const transporter: Transporter<SMTPTransport.SentMessageInfo> = createTransport(options);

const message: SendMailOptions = {
	from: '"Shinobi Works" <no-reply@example.com>',
	to: "administer@example.com",
	subject: "問い合わせがありました",
	text: "サイトにお問い合わせがありました...(略)",
};

const info = await transporter.sendMail(message);
console.log(info.messageId); // SentMessageInfo なので messageId / accepted / rejected に型が付く

これは tsc --strict で通ることを確認したコードです。

設定オブジェクトを SMTPTransport.Options で注釈しているのには理由があります。createTransport は SMTP 以外(JSON 出力やストリーム、独自トランスポート)にも対応していて、@types/nodemailer ではオーバーロードが 7 本あります。注釈なしで port: "2525" のように型を間違えると、エラーは出るのですが、内容は No overload matches this call. で、どのプロパティが悪いのかは読み取れません。SMTPTransport.Options を付けておくと、同じ間違いが Type 'string' is not assignable to type 'number'. と、場所つきで出ます。

メールを送信する

送信は sendMail 関数で、Promise が返ってきます。成功と失敗で処理を分けたいときは try/catch が使えます。

sendMail に最低限必要なオプションは以下の通りです。

  • from(送信元)
  • to(送信先)
  • subject(件名)
  • text(メール本文)

その他のオプションには、返信先を指定する replyTo、ファイルを添付できる attachments、HTML 形式の本文を送れる html などがあります。添付については NodemailerでBase64の画像を添付する に分けて書いています。

try {
	const info = await transporter.sendMail({
		from: '"Shinobi Works" <no-reply@example.com>',
		to: "administer@example.com",
		subject: "問い合わせがありました",
		text: "サイトにお問い合わせがありました...(略)",
	});
	console.log(info.accepted, info.rejected, info.response);
} catch (error) {
	console.log("メールを送信できませんでした");
	throw error;
}

「成功」が意味するもの

try/catch で拾えるのは、ホスト名やパスワードの不備などで送信処理そのものが失敗した場合だけです。to(送信先メールアドレス)が間違っていたとしても送信は「成功」になります。

これは手元で確かめられます。ローカルに SMTP サーバー(smtp-server パッケージ)を立てて、存在しない宛先に送ったときの戻り値です。

{
	"messageId": "<40917cdc-7d73-2308-15cb-cb280ad4a334@example.com>",
	"accepted": ["nobody-does-not-exist@example.invalid"],
	"rejected": [],
	"response": "250 OK: message queued"
}

accepted に宛先が入り、response250 OK です。sendMail の成功は「SMTP サーバーがメッセージを受け取った」ことで、相手に届いたことではありません。PHP の mail 関数と同じで、送信先に届くかどうかは別問題です。届かなかった場合はサーバーから後でバウンスメールが返ってくるので、そちらで気付くことになります。

一方、認証に失敗したときは例外になります。同じローカルの SMTP サーバーに間違ったパスワードで送ると、サーバーが 535 を返し、sendMail は次の内容で reject されました。

code: EAUTH
responseCode: 535
command: AUTH PLAIN
message: Invalid login: 535 Invalid username or password

パスワードを空にした場合も code は同じ EAUTH ですが、こちらはサーバーに送る前に Nodemailer 側で止まるので responseCode が付かず、message は Missing credentials for "PLAIN" になります。接続先のポートに何もいないときは ESOCKETconnect ECONNREFUSED)でした。

エラーの code は v8 で整理されていて(NoAuthENOAUTH になるなど)、v9 のソースでは EAUTH / ECONNECTION / ESOCKET / ETIMEDOUT / EENVELOPE / EMESSAGE のような接頭辞 E の定数が使われています。code で分岐するなら、EAUTH は資格情報、ESOCKET / ECONNECTION / ETIMEDOUT は接続先、EENVELOPE は宛先や送信元の指定を疑う、という切り分けができます。

from にも同じことが言えて、認証に使ったメールアドレスと from のアドレスが一致していなくても送信できます。たとえば administer@example.com の認証情報で送信しつつ、from には no-reply@example.com を指定する、といった使い方ができます。システムの自動返信メールに役立ちます。

メールが送れない場合

国外 IP からのアクセスを制限しているレンタルサーバーがあるため、Netlify のような海外のサーバーから実行する場合は、制限を解除しないと送信できないことがあります。私が試したときは 502 エラーが返ってきました。

また、Cloudflare Workers や Pages Functions のような環境では、そもそも Nodemailer が前提とする SMTP 接続が使えないことがあります。この話は Cloudflare Pages Functionsでメールが送信できない場合の対処法 に書きました。

v8 と v9 で変わったこと

この記事を v8 で見直したあと、2026 年 6 月に v9 が出ています。CHANGELOG の破壊的変更はそれぞれ 1 点で、普通の SMTP 送信のコードには影響しません。

  • v8.0.0(2026 年 2 月): エラーコード NoAuthENOAUTH に改名。エラーコードで分岐しているコードだけ影響する
  • v9.0.0(2026 年 6 月): 添付ファイルを URL(href / path)で指定したときや OAuth2 のトークン取得など、Nodemailer が外部へ HTTPS で取りに行く処理で、TLS 証明書を既定で検証するようになった。自己署名証明書のサーバーから添付を取っていた場合は失敗するようになる。従来どおりにするなら tls.rejectUnauthorized: false

対応 Node.js は >=6.0.0 のままです。

全体コード

import { createTransport, type Transporter, type SendMailOptions } from "nodemailer";
import type SMTPTransport from "nodemailer/lib/smtp-transport";

const options: SMTPTransport.Options = {
	host: "mail.example.com",
	port: 465,
	secure: true,
	auth: {
		user: process.env.MAIL_AUTH_USER,
		pass: process.env.MAIL_AUTH_PASS,
	},
};

const transporter: Transporter<SMTPTransport.SentMessageInfo> = createTransport(options);

const message: SendMailOptions = {
	from: '"Shinobi Works" <no-reply@example.com>',
	to: "administer@example.com",
	subject: "問い合わせがありました",
	text: "サイトにお問い合わせがありました...(略)",
};

try {
	const info = await transporter.sendMail(message);
	console.log(info.accepted, info.rejected, info.response);
} catch (error) {
	console.log("メールを送信できませんでした");
	throw error;
}

この構成で気をつけること

  • 型は本体に同梱されていない。@types/nodemailer を入れる。メジャーが 1 つ古くても v9 で使える
  • createTransport の設定は SMTPTransport.Options で注釈する。オーバーロードが多く、注釈なしだとエラーの場所が分からない
  • sendMail の成功はサーバーが受け取ったことで、届いたことではない。宛先違いは例外にならない
  • 認証やホスト名の失敗は code で分岐できる。EAUTH なら資格情報、ESOCKET / ECONNECTION / ETIMEDOUT なら接続先を疑う
  • 認証情報は環境変数から読む。プライベートリポジトリでも直書きしない

送信そのものは 2021 年から変わらず数行で済みます。変わったのは周辺で、型の付け方と「成功」の読み方さえ押さえておけば、v9 でも同じコードが動きます。

関連

この記事をシェア