トップページへ

S.A.G.A佐賀!!出身者のプログラミングブログ

ExpressとNodemailerを使ってメールを送信してみる

ExpressとNodemailerを使ってメールを送信してみる

本文書では言わずと知れたNode.jsフレームワークのExpressとNodemailerライブラリを使用してGmailでのメール送信を行なってみたいと思います。 今回はメールの送信のみの実装なのでさくっとViteでプロジェクトを作りたいと思います。 自分のWebサイトを作ったけどお問い合わせフォームの実装ができない方の参考になればと幸いです。

プロジェクトの作成

メール送信機能のプロジェクト作成しますが、動作させるにはNode.jsが必要です。 まずはNode.jsをインストールしましょう。

Node.jsバージョン

今回の開発に必要なNode.jsのバージョンは下記です。Node.jsのバージョン管理のツールに私はVoltaを使用しておりますが、お好みのものを使用してください。

  • Node.js 20.10.0
  • npm 10.2.3

最終的にRender.comにデプロイしますがデフォルトのバージョンが20.9.0なのでRenderの設定画面からバージョンを指定するか、ルートに.node-versionファイルを置く必要があるので.node-versionで管理するツールが良いのかもしれません。 ターミナルで下記コマンドを流してバージョンを確認します。

node -v

20.10.0
npm -v

10.2.3

Viteでプロジェクトを作成

プロジェクトを作成するディレクトリでターミナルを開き以下のコマンドでViteプロジェクトを作成します。

npm create vite@latest

プロジェクト作成時にいくつか質問されますが、今回私はsample-express-nodemailerというプロジェクト名で作成します。

Project name: ... sample-express-nodemailer
Select a framework: » Vanilla
Select a variant: » TypeScript

Viteのバージョン

今回vite-plugin-nodeパッケージを使用して動作させますが最新のVite5系に対応していない(2023年12月3日現在)ためViteを4系の最新バージョンに落とします。

npm i -D vite@4

必要なパッケージのインストール

プロジェクトを作成しましたが、まだExpressをインストールしていません。他にも必要なパッケージがあるのでまとめてインストールします。

npm i cors dotenv express nodemailer

今回はTypeScriptを使用するのでtypesファイルと、ViteでNode.jsを動かすためのパッケージもインストールします。

npm i -D @types/cors @types/express @types/node @types/nodemailer vite-plugin-node

不要なファイルの削除

今回は使用しないファイルを最初に削除しておきます。 以下のものを削除してください。

  • srcディレクトリの中身全て
  • publicディレクトリ
  • index.html

vite.config.tsの作成

Viteの設定ファイルであるvite.config.tsファイルをルートディレクトリに作成します。 設定は以下のようにします。

vite.config.ts

import { defineConfig } from 'vite';
import { VitePluginNode } from 'vite-plugin-node';

export default defineConfig({
  server: {
    port: 3002,
  },
  plugins: [
    ...VitePluginNode({
      adapter: 'express',
      appPath: './src/app.ts',
      exportName: 'viteNodeApp',
      tsCompiler: 'esbuild'
    }),
  ],
  build: {
    minify: true,
  }
});

メール送信機能の実装

ここまでで実装の準備ができました。 これからメールの送信機能を実装していきますがディレクトリを後から分けると長くなってしまうので ディレクトリを分けている状態で進めていきます。 またエントリーファイルから進めたいのですがエラーが出てしまうため最後にエントリーファイルを作成するのをご承知おきください。

.envの作成

環境変数を扱う.envファイルをルートに作成します。

.env

MAILER_PORT=465
MAILER_HOST=smtp.gmail.com
MAILER_USER=sample@gmail.com #Gmailのメールアドレス
MAILER_PASS=gmailapplicationpass #アプリパスワードを入力

MAILER_USER、MAILER_PASSはご自身のものを使用してください。 Googleアカウントを持っていない方やアプリパスワードを設定していない方は アカウントの作成とアプリパスワードを設定してから進んでください。

環境変数を読み込む

使用する環境変数を.envに記述しました。この環境変数を使えるようにしてみましょう。 srcディレクトリ配下にconfigsディレクトリを作成し、そこにcontactConfig.tsというファイルを作り記述します。 PORT番号を設定しない場合はデフォルトで3002番ポートを使用するようにします。

src/configs/contactConfig.ts

import { config } from 'dotenv';

config();

export const serverPort = Number(process.env.PORT) || 3002;

export const mailer = {
  port: process.env.MAILER_PORT,
  host: process.env.MAILER_HOST,
  user: process.env.MAILER_USER,
  pass: process.env.MAILER_PASS,
};

型を定義する

ExpressのRequestの型が以下のように定義されています。

interface Request<
  P = core.ParamsDictionary,
  ResBody = any,
  ReqBody = any,
  ReqQuery = core.Query,
  Locals extends Record<string, any> = Record<string, any>,
> extends core.Request<P, ResBody, ReqBody, ReqQuery, Locals> {}

このまま使用すると型の絞り込みが難しかったため(筆者のTS力不足でしょうな...)専用の型を定義します。 また定義する型は複数ファイルで読み込む必要のある型のため、定義するファイルを作成します。 srcディレクトリ配下にtypesディレクトリを作成し、そこにcontactTypes.tsファイルを作成します。

src/types/contactTypes.ts

import type { Request } from 'express';

export interface RequestBody {
  name: string;
  message: string;
}

export type ContactRequestBody = Request<unknown, unknown, unknown, RequestBody>;

メール送信の処理

ここでNodemailerを使ってメール送信の処理の記述をします。 srcディレクトリの中にcontrollersディレクトリを作成します。 そこにcontactController.tsファイルを作成し以下を記述します。

src/controllers/contactController.ts

import nodemailer from 'nodemailer';

import { mailer } from '../configs/contactConfig';
import type { ContactRequestBody, RequestBody } from '../types/contactTypes';

const transporter = nodemailer.createTransport({
  port: Number(mailer.port),
  host: mailer.host,
  auth: {
    user: mailer.user,
    pass: mailer.pass,
  },
  secure: true,
});

function isContactRequestBody(body: unknown): body is RequestBody {
  if (typeof body !== 'object' || body === null) return false;
  const { name, message } = body as Record<string, unknown>;

  return typeof name === 'string' && typeof message === 'string';
}

export async function sendmail(req: ContactRequestBody): Promise<void> {
  await new Promise((resolve, reject) => {
    if (!isContactRequestBody(req.body)) {
      reject(new Error('Invalid request body'));

      return;
    }

    const { name, message } = req.body;

    const textContent = `
      フォームからお問い合わせがありました。
      ======================
      【名前】${name}
      【お問い合わせ内容】${message}
      ======================
    `;

    const toAdminMail = {
      from: mailer.user,
      to: mailer.user,
      subject: `【お問い合わせ】${name}様より`,
      text: textContent,
    };

    transporter.sendMail(toAdminMail, function (err, info) {
      if (err) {
        reject(err);
      } else {
        resolve(info);
      }
    });
  });
}

ルーターとエンドポイントの設定

Nodemailerでメールの送信処理は実装しました。 今度はルーターの設定とエンドポイントを設定します。 srcディレクトリにroutesディレクトリを作成しそこにcontactRoute.tsファイルを作成します。 メソッドをpostで/contactをエンドポイントとします。

src/routes/contactRoute.ts

import express, { Response } from 'express';

import { sendmail } from '../controllers/contactController';
import type { ContactRequestBody } from '../types/contactTypes';

const router = express.Router();

router.post('/contact', (req: ContactRequestBody, res: Response) => {
  sendmail(req)
    .then(() => {
      res.status(201).send('ok');
    })
    .catch(() => {
      res.status(500).send('Internal Server Error');
    });
});

export default router;

エントリーファイルの作成

エントリーファイルとなるapp.tsをsrcディレクトリ直下に作成します。

src/app.ts

import cors from 'cors';
import express, { type Application } from 'express';

import { serverPort } from './configs/contactConfig';
import router from './routes/contactRoute';

const app: Application = express();
app.use(cors({}));

//POSTのパラメータを取得
app.use(express.urlencoded({ extended: true }));
app.use(express.json({}));

const port = serverPort;
app.listen(port, () => console.log(`Server is running on port ${port}`));
app.use(router);

export const viteNodeApp = app;

これでメール送信の準備ができましたので実際に送信してみます。 後ほどこのメール機能をRenderにデプロイしフォームはNetlifyにデプロイするため、フォームはこちらと分けて作成します。

メールフォームの作成

sample-express-nodemailerのプロジェクトと別のところ(私はデスクトップ上に作成します)に index.htmlファイルを作成し以下のHTMLを記述します。 今回はメール送信機能に重きを置いているためフォームに関してはスタイリングなどは一切しません。 またJavaScriptもindex.htmlに直接記述します。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>メールの送信</title>
</head>
<body>
  <form>
    <div>
      <label for="name">名前</label>
      <input type="text" name="name" id="name" />
    </div>
    <div>
      <label for="message">お問い合わせ内容</label>
      <textarea name="message" id="message"></textarea>
    </div>
    <button id="submit" type="button">送信する</button>
  </form>

  <script>
    const buttonSubmit = document.querySelector('#submit');
    const inputName = document.querySelector('#name');
    const inputMessage = document.querySelector('#message');

    buttonSubmit.addEventListener('click', () => {
      const formData = {
        name: inputName.value,
        message: inputMessage.value,
      };

      fetch(new Request('http://localhost:3002/contact'), {
        method: 'POST',
        body: JSON.stringify(formData),
        mode: 'cors',
        headers: { 'Content-Type': 'application/json' },
      })
        .then((response) => {
          window.alert('送信できました。');
        })
        .catch((error) => {
          console.error(error);
        });
    });
  </script>
</body>
</html>

実際にindex.htmlをブラウザで開き、フォームの入力をして送信してみましょう。 Gmailのアドレスにフォームからのメールが来ていれば成功です。

デプロイしてメールの送信を確認する

ローカルでの動作確認ができました。実際に運用することを想定し、 デプロイして動作するか確認してみましょう。

app.tsの修正

デプロイの前に現在は全てのCORS許可をしている状態なのでapp.tsを修正します。 私は許可するオリジンを《https://sample-express-nodemailer.netlify.app》とします。 (つまり《https://sample-express-nodemailer.netlify.app》にindex.htmlを置きます) こちらは適宜変更してください。

src/app.ts

import cors from 'cors';
import express, { type Application } from 'express';

import { serverPort } from './configs/contactConfig';
import router from './routes/contactRoute';

const app: Application = express();
app.use(
  cors({
    origin: 'https://sample-express-nodemailer.netlify.app', // アクセス許可するオリジン
    credentials: true, // レスポンスヘッダーにAccess-Control-Allow-Credentials追加
    optionsSuccessStatus: 200, // レスポンスstatusを200に設定
  }),
);

//POSTのパラメータを取得
app.use(express.urlencoded({ extended: true }));
app.use(express.json({}));

const port = serverPort;
app.listen(port, () => console.log(`Server is running on port ${port}`));
app.use(router);

export const viteNodeApp = app;

scriptsを追加する

Renderにデプロイした際に走らせるスタートコマンドを作成しておきます。

package.json

"scripts": {
  "dev": "vite",
  "build": "tsc && vite build",
  "preview": "vite preview",
  "start": "node dist/app.js" // 追加
}

Renderにデプロイする

今回はRender.comにデプロイしフリープランで使用します。GitHubのアカウントが必要なので作成し、プロジェクトをpushしておいてください。 Render.comにGitHubアカウントでサインインしプロジェクトの作成を行ないます。 Web Serviceを選択し《Build and deploy from a Git repository》を選択してください。 《Configure account》を押下するとGitHubに飛びます。《Only select repositories》を選択し該当リポジトリ選択し《save》してください。 デプロイ設定を変更する箇所は下記で残りはデフォルトのままで良いです。 またEnvironment Variablesですが.node-versionファイルを作成しバージョン指定している場合は設定不要です。

Name ... ご自身のわかりやすいものに変更
Build Command ... npm install && npm run build
Start Command ... npm start
Secret Files
  Filename ... .env
  File Contents ... .envの中身全て
Environment Variables
  key ... NODE_VERSION
  value ... 20.10.0

index.htmlを書き換える

API側ではアクセス許可するオリジンを設定しました。 index.htmlは現在リスエスト先がlocalhostになっているので変更します。 Renderへデプロイする際のNameが当てられているかと思いますが念のためRenderサイトから確認してください。 私の場合は《sample-express-nodemailer》ですので《https://sample-express-nodemailer.onrender.com》となっています。

index.html

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>メールの送信</title>
</head>
<body>
  <form>
    <div>
      <label for="name">名前</label>
      <input type="text" name="name" id="name" />
    </div>
    <div>
      <label for="message">お問い合わせ内容</label>
      <textarea name="message" id="message"></textarea>
    </div>
    <button id="submit" type="button">送信する</button>
  </form>

  <script>
    const buttonSubmit = document.querySelector('#submit');
    const inputName = document.querySelector('#name');
    const inputMessage = document.querySelector('#message');

    buttonSubmit.addEventListener('click', () => {
      const formData = {
        name: inputName.value,
        message: inputMessage.value,
      };

      fetch(new Request('https://sample-express-nodemailer.onrender.com/contact'), { // 変更
        method: 'POST',
        body: JSON.stringify(formData),
        mode: 'cors',
        headers: { 'Content-Type': 'application/json' },
      })
        .then((response) => {
          window.alert('送信できました。');
        })
        .catch((error) => {
          console.error(error);
        });
    });
  </script>
</body>
</html>

Netlifyにデプロイする

index.htmlの書き換えが済んだところでデプロイしてみましょう。 メール送信のAPIに重きを置いているためこちらも最低限の方法でデプロイします。 Netlifyにサインインし《Add new site》から《Deploy manually》を選択します。 ドロップゾーンが現れるので作成済のindex.htmlをドラッグアンドドロップでデプロイしてください。 左メニュー内の《Domain management》から《Edit site name》でアクセス許可したオリジンを設定してください。 (私の場合は《https://sample-express-nodemailer.netlify.app》)です。

送信してみる

実際にフォームのあるページにアクセスし入力後送信してみてください。 ここで送信できなかった場合はアクセス許可のオリジンを間違えていないか、 リクエストを送る先が間違えていないか確認してください。 またRenderのフリープランの注意点ですが非アクティブな状態が15分間続くとサービスがスリープします。 スリープ状態に新しいリクエストが来ると起動するようになっています。 対策はググると出てくるので実際にポートフォリオなどに実装・運用する際は調べてみてください。

この記事のサンプルコードはこちら