Nodemailer の使い方と TypeScript の型定義(v9 対応)
Node.js の定番メール送信ライブラリに Nodemailer があります。必要な機能が一通り揃っていて、Promise ベースで扱いやすく、TypeScript 環境にも問題なく導入できます。
この記事は 2021 年に書いたものです。Nodemailer はその後もメジャーバージョンを重ねていて、この記事を直している 2026 年 8 月時点の最新は 9.0.6 です。基本的な書き方は当時から変わっていないので、コードを v9 で動かし直して全体を整理しました。あわせて、@types/nodemailer で検索して来る方が多いことが分かったので、TypeScript の型定義の節を足しています。
インストール
npm install nodemailerTypeScript 環境では型定義を別途インストールします。v9 になった今も型は Nodemailer 本体に同梱されていません。手元で確かめると、nodemailer のパッケージには .d.ts が 1 つも入っておらず、package.json に types の指定もありませんでした。
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 の設定
SMTPTransport は nodemailer/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 に宛先が入り、response は 250 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" になります。接続先のポートに何もいないときは ESOCKET(connect ECONNREFUSED)でした。
エラーの code は v8 で整理されていて(NoAuth が ENOAUTH になるなど)、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 月): エラーコード
NoAuthをENOAUTHに改名。エラーコードで分岐しているコードだけ影響する - 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 でも同じコードが動きます。