node-cronとnodemailerを利用して
- DBに登録されたメール送信リクエストを定期的にチェック
- 未送信のものを5件ずつメールサーバーを介して送信
- 送信済みフラグを立てる
というシステムを作ってゆく
また、Dockerを用いた開発環境も紹介する
全体像
-
DB
- テーブル(mail_queue)に以下の列がある想定
- to_address(送信先アドレス)
- subject (件名)
- body (本文)
- from_address(送信元アドレス)
- smtp_host(SMTPサーバー、固定)
- smtp_id(SMTP認証ユーザー名、固定)
- smtp_pw(SMTP認証パスワード、固定)
- smtp_port(SMTPサーバーポート、固定)
- sent_flag(送信済みフラグ:0=未送信、1=送信済み)
- テーブル(mail_queue)に以下の列がある想定
-
Node.jsアプリケーション
- node-cronを使って、一定時間ごとにmail_queueテーブルをチェック
- nodemailerを使って5件ずつメールを送信
- 送信成功したらsent_flagを1に更新
-
Docker構成
- Node.jsアプリ(コンテナ)
- DB(コンテナ)
- 必要に応じてメールサーバー(SMTPサーバー)を立てる
ディレクトリ構成
my-mail-system/
├─ docker-compose.yml
├─ backend/
│ ├─ Dockerfile
│ ├─ package.json
│ ├─ package-lock.json
│ └─ src/
│ ├─ index.js
│ └─ db.js (DB接続設定など)
└─ .env
Node.jsアプリの実装例
1.パッケージインストール
bash
# backend ディレクトリで
npm init -y
npm install node-cron nodemailer pg # PostgreSQLの場合
2.DB接続設定(db.jsと.envファイル)
環境変数を読み込むためにdotenvパッケージをインストールします
bash
npm install dotenv
.envファイル
DB_HOST=db
DB_USER=user
DB_PASSWORD=pass
DB_NAME=maildb
DB_PORT=5432
プロジェクトのルートディレクトリに.envファイルを作成して、接続情報を記述します。
db.js
const { Pool } = require('pg');
require('dotenv').config(); // .envファイルの読み込み
const pool = new Pool({
host: process.env.DB_HOST, // ホスト名
user: process.env.DB_USER, // ユーザー名
password: process.env.DB_PASSWORD, // パスワード
database: process.env.DB_NAME, // データベース名
port: process.env.DB_PORT, // ポート番号
});
module.exports = pool;
注意点
- .envをGitで管理しない
.envファイルには機密情報が含まれるため、必ず.gitignoreに追加して下さい - 環境変数をDocker Composeで定義する場合
Docker環境を使用している場合、docker-compose.ymlで環境変数を定義できますdocker-compose.ymlservices: db: image: postgres:15 environment: POSTGRES_DB: maildb POSTGRES_USER: user POSTGRES_PASSWORD: pass ports: - "5432:5432"
- 本番環境では安全な方法で管理
本番環境では、.envファイルではなく、AWS Secrets ManagerやGoogle Cloud Secret Managerなどを使って管理するのがおすすめです。
3.メインロジック(index.js)
index.js
const cron = require('node-cron');
const nodemailer = require('nodemailer');
const pool = require('./db'); // PostgreSQL接続設定
const main = async () => {
console.log('Worker started');
// 毎分実行 (例: "* * * * *" -> 1分ごと)
cron.schedule('* * * * *', async () => {
console.log('=== Cron job triggered ===');
try {
// 1) 未送信のレコードを最大5件取得する
const res = await pool.query(
'SELECT id, to_address, subject, body, from_address, smtp_host, smtp_id, smtp_pw, smtp_port FROM mail_queue WHERE sent_flag = $1 LIMIT 5',
[0]
);
const rows = res.rows; // PostgreSQLでは`rows`にデータが格納される
if (!rows.length) {
console.log('No unsent mails.');
return;
}
// 2) 取得したレコード分だけメール送信
for (const mail of rows) {
const transporter = nodemailer.createTransport({
host: mail.smtp_host,
port: mail.smtp_port,
secure: false, // 587などTLSを使うなら false
auth: {
user: mail.smtp_id,
pass: mail.smtp_pw,
},
});
const mailOptions = {
from: mail.from_address,
to: mail.to_address,
subject: mail.subject,
text: mail.body,
};
try {
await transporter.sendMail(mailOptions);
console.log(`Mail sent to: ${mail.to_address}`);
// 3) 送信成功したらフラグを立てる
await pool.query(
'UPDATE mail_queue SET sent_flag = $1 WHERE id = $2',
[1, mail.id]
);
} catch (err) {
console.error('Error sending mail:', err);
}
}
} catch (err) {
console.error('Error in cron job:', err);
}
});
};
main();
上記は簡易的な例です。実際にはエラーハンドリングやリトライ制御、HTMLメールや添付ファイル対応などが必要に応じて追加されます。
Docker構成
1.docker-compose.yml
docker-compose.yml
version: '3.8'
services:
db:
image: postgres:15
container_name: my-mail-db
environment:
- POSTGRES_DB=maildb
- POSTGRES_USER=user
- POSTGRES_PASSWORD=pass
- DB_HOST=db
ports:
- "5432:5432"
volumes:
- db_data:/var/lib/postgresql/data
backend:
build: ./backend
container_name: my-mail-backend
depends_on:
- db
volumes:
- ./backend/src:/usr/src/app/src
ports:
- "3000:3000"
env_file:
- .env
volumes:
db_data:
docker-compose.yml で .env を明示的に読み込むようにしないとDocker環境では.envが読み込まれないようです
- dbサービス
- postgreSQL 15.0 イメージを使用
- POSTGRES_DB=maildbなど環境変数で初期DBを作成
- backendサービス
- ./backendディレクトリにあるDockerfileからビルド
- depends_onでdbが起動してから起動するようにしている
- volumesでローカルのソースコードをコンテナ内にマウント(ホットリロードできるように設定)
- commandでNode.jsアプリ(index.js)を起動
2.Dockerfileの例(backend用)
dockerfile
# backend/Dockerfile
FROM node:18
WORKDIR /usr/src/app
# package.json と package-lock.json をコピー
COPY package*.json ./
# npm install 実行
RUN npm install
# ソースコードをコピー
COPY . .
# ホットリロード(開発用)でなければエントリポイントはCMDでもOK
# CMD ["node", "src/index.js"]
CMD がコメントアウトされているため、コンテナ起動後、起動時に常駐するプロセスが実行されないため何も実行されずに exit 0(正常終了)でコンテナが停止します。開発時はコメントアウトをはずし忘れないこと
3.コンテナ起動
bash
docker-compose up -d --build
- --buildオプションでDockerfuleに従って再ビルド
- 立ち上がったら、backendコンテナ内でnode src/index.jsが実行され、node-cronが動き始める
4.DBテーブル作成(PostgrSQL)
sql
CREATE TABLE mail_queue (
id SERIAL PRIMARY KEY,
to_address VARCHAR(255) NOT NULL,
subject VARCHAR(255) NOT NULL,
body TEXT NOT NULL,
from_address VARCHAR(255) NOT NULL,
smtp_host VARCHAR(255) NOT NULL,
smtp_id VARCHAR(255) NOT NULL,
smtp_pw VARCHAR(255) NOT NULL,
smtp_port INTEGER NOT NULL,
sent_flag INTEGER NOT NULL DEFAULT 0
);
docker-compose で起動後、PostgreSQLコンテナに入って上記クエリを実行したり、環境変数等で初回起動時にマイグレーションを走らせる方法もあります
実行イメージ
- docker-compose up -d --buildでコンテナ起動
- dbコンテナ(postageSQL)の初期化後、SQLでテーブル作成
- mail_queueテーブルに(to_address, subject, body, from_address, smtp_host, smtp_id, smtp_pw, smtp_port, sent_flag=0) をINSERT
- backendコンテナ内のnode-cronが毎分未送信レコードをチェック→nodemailerで送信→成功したらsent_flag=1に更新
- ログはdocker-compose logs -f backendなどで確認可能
補足(SMTPサーバー)
- 実際に運用する場合は、信頼できるSMTPサーバーを利用します
- テスト環境用に一時的にmailhogのようなテスト用SMTPサーバーをdocker-composeで起動して受信確認するケースもあります
例:docker-compose.ymlにMailhogを追加する場合
yaml
mailhog:
image: mailhog/mailhog
container_name: my-mailhog
ports:
- "8025:8025" # Web UI
- Mailhogを立ち上げると、SMTP:1025,Web UI: 8025が提供され、ブラウザから http://localhost:8025 で受信メールを簡単に確認できます
- Node.js側のSMTP設定をhost=mailhog,port=1025にすると、実際のメールの代わりにMailhogのUIで受信を確認できます
まとめ
- node-cron で一定間隔ごとにDBをチェックして、未送信メールがあればnodemailerで送信→送信済みフラグを更新
- Docker + docker-composeでNode.jsとDBをコンテナ化し、必要に応じてテスト用SMTPサーバーも起動する