Communication illustrations by Storyset
こんにちは、開発チームでエンジニアをしている和田です。
最近はいろんな開発ツールに AI が導入され、馴染みあるツールの進化に驚かされる毎日です。
話は変わりますが、ついこの前に自分が担当しているプロダクトで、ユーザーが SendGrid 経由で送信したメールの文章が文字化けするという現象に遭遇しました。 Mac と Windows 端末でファイルのやり取りをしていたりすると遭遇しやすい文字化け...。
今回は文字化けさせないように処理を追加するタスクを担当したので、その作業記録として記事を書かせていただきます。
背景・目的
SendGrid のリクエストに含まれていたメール本文の文字化け対応の方法解説。
対象読者
Node.js(Express)と SendGrid API を使用した環境における文字化けの変換方法を知りたい開発者。
メールの文章が文字化けした状況
自身が担当しているプロダクトでは、メールの送受信に SendGrid を使用しています。 SendGrid からのリクエストに含まれるメール本文が文字化けしており、メール本文が判読できない状態になっていました。 発生頻度としては月に数回程度、こちらの現象が確認できました。
文字化けの対策方針
実際にメール本文を処理しているコードを確認したところ、字数制限などに対するバリデーション処理はありましたが、文字コードに関する処理がなかったため、SendGrid からのリクエストを受け取った直後に文字コードの変換を行います。
SendGrid のリクエストについて
SendGrid からのリクエストには、下記のように送信元・受信元・メールタイトル・メール本文および各項目それぞれに対して扱われた文字コードが含まれています。
{ "to": "test@test.test", "subject": "メールタイトル", "from": "test@test.test", "text": "メール本文", "charsets": { "to": "UTF-8", "subject": "UTF-8", "from": "UTF-8", "text": "iso-2022-jp" } }
今回はこちらの値を使って文字コードの変換を行っていきます。
SendGrid のメール本文が文字化していた場合の対処方法
1. ログの確認・文字化けの再現
まずはログの確認を行って、実際にどの種類の文字コードとして送信されたのか確認します。
先程、示した SendGrid のリクエストを確認すると、req.body.charsets.text: "iso-2022-jp"
として送信されており、メール本文のみが文字コード ISO-2022-JP
として送信されており文字化けが発生していました。
メール本文が文字化けすることを確認するため、メーラーで送信時の文字コードを変更して再現してみます。
こちらの記事を参考にメール送信時に文字コードを変更したところ、ログの通りにメール本文が文字化けしました 👍
2. 文字コード変換関数の作成
ログを確認して再現ができたところで、実際に文字コードを変換する処理を記述していきます。
今回、発生していたのは ISO-2022-JP
による文字化けでした。
Node.js であれば TextDecoder().decode()
を使えば ISO-2022-JP
の文字化けを解消することができますが、今回は日本語のメールでよく使用される Shift_JIS, EUC-JP
にも対応したいのでこれらの文字コード変換ができるライブラリを使用して関数を作成します。
有名どころとしては、iconv-lite ですが、こちらは ISO-2022-JP
に対応していなかったので encoding.js を使用することにしました。
では、文字コード変換関数を作成します。
const Encoding = require("encoding-japanese"); /** * 文字列のエンコーディングを変換する * @param { string } str 変換前の文字列 * @param { string } fromEncoding 変換前のエンコーディング * @param { string } toEncoding 変換後のエンコーディング * @return { string } 変換後の文字列 */ const convertEncoding = (str, fromEncoding, toEncoding = "unicode") => { // 変換前と変換後のエンコーディングが同じ場合は受け取った文字列をそのまま返す if (fromEncoding === toEncoding) return str; // 変更前後のエンコーディングを encoding.js で扱える文字列表記に変換する関数 const convertToDefinedEncoding = (encoding) => { const enc = encoding.toLowerCase(); if (enc === "utf-8" || enc === "utf8") return "UTF8"; if (enc === "iso-2022-jp") return "JIS"; if (enc === "shift-jis" || enc === "shift_jis") return "SJIS"; if (enc === "euc-jp") return "EUCJP"; return "UNICODE"; }; // 変換前の文字列を Buffer に変換する // 実際は Buffer ではなく Buffer を模した配列に変換される const buffer = Encoding.stringToCode(str); const to = convertToDefinedEncoding(toEncoding); const from = convertToDefinedEncoding(fromEncoding); // Buffer と変更前後のエンコーディングを渡して文字列として出力する return Encoding.convert(buffer, { to, from, type: "string" }); };
encoding.js で文字コードの指定をするには UTF8
, JIS
, SJIS
のように全て大文字の表記にする必要があるので、SendGrid のリクエストの charsets
の文字コードを渡して変換する convertToDefinedEncoding
を通して、encoding.js convert
関数に渡して文字化けしたテキストを読める文字として返します。
3. 文字コード変換関数のテスト作成
convertEncoding
関数が実際に期待どおりの動作をするか確認します。
テストには Jest を使用しているので下記のコードでテストを行います。
it("Test convertEncoding", () => { // UTF8, ISO-2022-JP, Shift_JIS, EUC-JP の4つのテストケースを用意 const testCases = ["utf-8", "iso-2022-jp", "shift-jis", "euc-jp"]; // 各文字コードを encoding.js で扱える形式に変換 const convertEncTypeName = (enc) => { if (enc === "utf-8") return "UTF8"; if (enc === "iso-2022-jp") return "JIS"; if (enc === "shift-jis") return "SJIS"; if (enc === "euc-jp") return "EUCJP"; return "UTF8"; }; // テストケースに対して forEach でテストを実行する testCases.forEach((testCase) => { // 日本語を含む文字列を encoding.js で変換して文字化けさせる const sourceText = `${testCase}エンコーディング`; const encodingText = Encoding.convert(sourceText, { to: convertEncTypeName(testCase), type: "string", }); // convertEncoding に文字化けした文字列を渡して出力が期待値通りか確認する const convertedText = convertEncoding(encodingText, testCase); expect(convertedText).toBe(sourceText); }); });
ポイントとしては、検証するテキスト(sourceText
)には日本語の文字列を必ず含めることです。半角英数字のみだと文字化けは生じず、テストの意味がなくなってしまいます。
無事にテストが通過したので、リクエスト処理部分にこちらの関数を通すように書き換えました。
まとめ
最後まで読んでくださりありがとうございました 🙇♂️
今回は文字コードの Node.js, SendGrid API を使用した環境での文字コードの変換について解説させていただきました。
SendGrid のリクエストから文字コードを取り出して処理を行ったため、encoding.js で扱えるようにする変換処理を各部に入れる必要がありましたが、encoding.js の convert
関数は変換元の文字コードを自動判定できるようです。
場合によっては、もっとシンプルに実装できたかもしれませんね。