0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

日本語メールの文字化け対策:blastengine APIのencodeパラメータと UTF-8 / ISO-2022-JP の使い分け

0
Posted at

2026年になってもメールの文字化けは現役の問題です。特に日本語メールでは、エンコード設定をひとつ間違えるとキャリアメールやレガシーメーラーで本文が崩れます。

本記事では blastengine API の encode パラメータを軸に、UTF-8 と ISO-2022-JP の使い分けと、実際に詰まりがちな機種依存文字・添付ファイル名の扱いまで実装コード付きで整理します。

筆者環境:Node.js 20(標準の fetch を使用。外部HTTPライブラリ不要)。

TL;DR

// 迷ったらこれでOK(Webメール時代の標準)
{ encode: "UTF-8" }

// レガシー受信環境(古い携帯メール、社内システム等)が混じるなら
{ encode: "ISO-2022-JP" }

判断軸の早見表:

宛先・要件 推奨
Gmail / Outlook.com / Yahoo!メール 中心 UTF-8
絵文字・多言語・機種依存文字を含む UTF-8
ガラケー宛、レガシー社内システム宛 ISO-2022-JP
ASCII + 一般的な日本語のみ・最大互換性 ISO-2022-JP
添付ファイル名が日本語 UTF-8(RFC 2231 で安全)

blastengine の encode パラメータの基本

配信登録APIで encode を指定すると、blastengine 側で件名(subject)・本文(text_part / html_part)を自動的に変換してくれます。

指定できる値

内容
UTF-8 デフォルト。Web標準・Unicode 全体に対応
ISO-2022-JP JIS X 0208 の範囲。古いメーラー互換性に強い

どこに適用されるか

フィールド 適用
subject 適用される(MIMEエンコードされる)
text_part 適用される
html_part 適用される(<meta charset> と整合性を取る必要あり)
from.name 適用される
添付ファイル名 RFC 2231 / RFC 2047 のルールでヘッダ化される(後述)

リクエスト例

HTTP 呼び出しは Node.js 20 標準の fetch で行います(axios などの外部パッケージは不要)。本記事のサンプルで使い回す薄いラッパを用意しておきます。

const BASE_URL = "https://app.engn.jp/api/v1";

async function beFetch(method, path, { body, params } = {}) {
  const url = new URL(`${BASE_URL}${path}`);
  if (params) {
    for (const [key, value] of Object.entries(params)) {
      if (Array.isArray(value)) value.forEach((v) => url.searchParams.append(key, v));
      else url.searchParams.set(key, value);
    }
  }

  const res = await fetch(url, {
    method,
    headers: {
      Authorization: `Bearer ${process.env.BE_TOKEN}`,
      "Content-Type": "application/json",
    },
    body: body === undefined ? undefined : JSON.stringify(body),
  });

  const raw = await res.text();
  const data = raw ? JSON.parse(raw) : null;
  if (!res.ok) {
    throw Object.assign(new Error(`blastengine ${method} ${path} -> ${res.status}`), {
      status: res.status,
      data,
    });
  }
  return data;
}

const be = {
  get: (path, params) => beFetch("GET", path, { params }),
  post: (path, body) => beFetch("POST", path, { body }),
  put: (path, body) => beFetch("PUT", path, { body }),
  patch: (path, body) => beFetch("PATCH", path, { body }),
  delete: (path) => beFetch("DELETE", path),
};

このクライアントを使って送信します。

// UTF-8 で送る
await be.post("/deliveries/transaction", {
  from: { email: "noreply@example.com", name: "サポート" },
  to: "user@example.com",
  subject: "ご注文ありがとうございます🎉",
  encode: "UTF-8",
  text_part: "お買い上げありがとうございます。",
  html_part: "<p>お買い上げありがとうございます。</p>",
});

// ISO-2022-JP で送る
await be.post("/deliveries/transaction", {
  from: { email: "noreply@example.com", name: "サポート" },
  to: "user@example.com",
  subject: "ご注文ありがとうございます",   // 絵文字はNG
  encode: "ISO-2022-JP",
  text_part: "お買い上げありがとうございます。",
  html_part: '<meta charset="ISO-2022-JP"><p>お買い上げありがとうございます。</p>',
});

UTF-8 を選ぶべきケース

ケース1:Webメール中心の配信

Gmail / Outlook.com / Yahoo!メール の3大Webメールは UTF-8 をネイティブに扱います。受信箱でのプレビュー、検索、転送、どれも問題なし。

await be.post("/deliveries/transaction", {
  // ...
  encode: "UTF-8",
  subject: "🎁 6月限定キャンペーンのご案内",
  text_part: "♥ いつもありがとうございます ♥\n",
});

ケース2:絵文字・多言語を含む本文

絵文字(😀)、ハングル(한국어)、簡体字(你好)、特殊記号(✓、♥、★)など、JIS X 0208 の範囲外を含むなら UTF-8 一択。

ISO-2022-JP でこれらを送ろうとすると、blastengine API は HTTP 400 でリクエストを拒否します(受信側で化けるのではなく送信前に弾かれる)。

{
  "encode": "ISO-2022-JP",
  "subject": "🎉 キャンペーン",
  "text_part": "①②③ 髙﨑"
}

レスポンス:

{
  "error_messages": {
    "subject": ["the character encoding is not supported."],
    "text_part": ["the character encoding is not supported."],
    "html_part": ["the character encoding is not supported."]
  }
}

つまり ISO-2022-JP を選んだ時点で、本文・件名から JIS X 0208 範囲外の文字を完全に除去する責務がアプリ側にある ということです。後述のサニタイズ処理が必須になります。

ケース3:HTMLメール

HTML メールは <meta charset> を本文に書く慣習があるため、UTF-8 で統一しておく方が <meta> と整合性を取りやすい。

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width">
</head>
<body>
  <p>こんにちは ✨</p>
</body>
</html>

ISO-2022-JP を選ぶべきケース

ケース1:ガラケー・古い携帯メール宛が含まれる

docomo・au・SoftBankのフィーチャーフォン向けキャリアメール宛にメールを送る場合、UTF-8 だと一部端末で本文が読めなくなることがあります。ISO-2022-JP(≒ JIS)にすると互換性が広がります。

ただし2026年現在、フィーチャーフォン宛配信は実質ほぼ無くなっているので、明確に「ガラケー含む」と要件にある場合のみ。

ケース2:レガシー社内システム宛

社内グループウェア、古い受信ゲートウェイ、独自プロトコル変換のあるシステムなど、「ISO-2022-JP しか想定していない」受信側に送るケースでは ISO-2022-JP を選びます。

実例:B2B の請求書通知で「お客様の経理システムが ISO-2022-JP しか受け付けない」というパターンが今でもあります。

ケース3:絶対に化けさせたくない・JIS 範囲の文字のみ

ASCII + 第一水準・第二水準漢字のみの本文で、最大互換性が欲しい場合は ISO-2022-JP が安全。

機種依存文字でやらかすパターン

UTF-8 / ISO-2022-JP どちらでも詰まりやすいのが 機種依存文字

典型的な「ヤバい文字」リスト

const DANGEROUS_CHARS = [
  "", "", "",        // 丸数字
  "", "", "",       // 丸囲み記号
  "", "", "",        // 異体字
  "", "", "",        // ローマ数字
  "", "", "",        // 数学記号(一部)
  "", "", "",       // 元号
];

ISO-2022-JP では送信前に API が400拒否する

これらは JIS X 0208 の範囲外(または機種依存領域)なので、encode: "ISO-2022-JP" を指定するとblastengine API がリクエスト段階で the character encoding is not supported. を返します。

$ curl -X POST .../deliveries/transaction \
    -d '{"encode":"ISO-2022-JP","subject":"①注文確認", ...}'

HTTP/1.1 400 Bad Request
{"error_messages":{"subject":["the character encoding is not supported."]}}

受信側で化ける前に、API側で止まるのがポイント。ユーザー入力をそのまま件名・本文に流す実装で ISO-2022-JP を選ぶと、機種依存文字の混入で送信が失敗する事故が起きます。

UTF-8 でも受信側で化けるケース

UTF-8 なら API は受け付けますが、受信側のメーラーが古い/フォントが無い/表示エンジンが Unicode 拡張面に対応していないと、表示上の文字化けは起こり得ます。

対策:送信前にサニタイズする

// 機種依存文字を安全な文字に置換
const REPLACEMENTS = new Map([
  ["", "(1)"], ["", "(2)"], ["", "(3)"],
  ["", "(株)"], ["", "(有)"],
  ["", ""], ["", ""],
  ["", "I"], ["", "II"], ["", "III"],
]);

function sanitizeForMail(text) {
  let out = text;
  for (const [from, to] of REPLACEMENTS) {
    out = out.replaceAll(from, to);
  }
  // Unicode 正規化(NFKC)で互換文字を標準形に
  return out.normalize("NFKC");
}

// 使い方
const body = sanitizeForMail("ご注文ID ①〜③ をご確認ください(㈱ Example)");
// → "ご注文ID (1)〜(3) をご確認ください((株) Example)"

NFKC正規化の効果

"".normalize("NFKC")    // "(株)"
"".normalize("NFKC")    // "1"
"".normalize("NFKC")    // "平成"
"".normalize("NFKC")    // "III"
"".normalize("NFKC")    // "アール"

normalize("NFKC") をかけるだけで多くの機種依存文字が ASCII / 標準漢字に潰せます。送信前に1行入れるだけで化け事故が激減 するので、定型処理として組み込んでおくのがおすすめ。

HTML メール特有の注意点

<meta charset>encode を一致させる

// OK
{
  encode: "UTF-8",
  html_part: '<!DOCTYPE html><html><head><meta charset="UTF-8"></head>...',
}

// NG: 不整合
{
  encode: "ISO-2022-JP",
  html_part: '<!DOCTYPE html><html><head><meta charset="UTF-8"></head>...',
}

一部のメーラーは <meta> 宣言を優先するため、不整合があるとどちらかの解釈で化けます。

Outlook デスクトップ版の罠

Outlook(デスクトップ版)は HTML 解釈が独特で、UTF-8 / ISO-2022-JP の選択以前に CSS や HTML タグの解釈で見た目が崩れます。HTML メールを本格運用するなら、別途 Outlook テスト環境(または Litmus / Email on Acid のようなプレビューサービス)で確認するのが鉄則。

text_part も必ず入れる

HTML を表示しないメーラー(mutt 等)のために、text_part のプレーンテキスト版も必ず同梱します。HTMLのみだと迷惑メール判定スコアも上がりがち。

await be.post("/deliveries/transaction", {
  // ...
  encode: "UTF-8",
  text_part: "こんにちは。\n詳細はこちら: https://example.com/news",
  html_part: '<p>こんにちは。</p><p><a href="https://example.com/news">詳細はこちら</a></p>',
});

添付ファイル名の日本語問題

これも文字化け常連。添付ファイル名はメールヘッダ(Content-Disposition: attachment; filename=...)に乗るので、本文と同じエンコード議論が発生します。

RFC 2231 形式(推奨)

Content-Disposition: attachment;
  filename*=UTF-8''%E8%AB%8B%E6%B1%82%E6%9B%B8.pdf

UTF-8 を %XX 形式でエンコードした表記。モダンなメーラーはこれを正しく解釈します。

RFC 2047 形式(古い)

Content-Disposition: attachment;
  filename="=?UTF-8?B?6KuL5rGC5pu4LnBkZg==?="

Base64 でファイル名をエンコードする方式。古いメーラー向けの後方互換。

blastengine 側の挙動

添付ファイルを multipart/form-data でアップロードする際、ファイル名は API ライブラリが適切なエンコードでヘッダに乗せてくれます。安全のためファイル名はASCII(英数字)にしておくのが鉄板ですが、どうしても日本語が必要なら以下のように送ります。

import fs from "node:fs";

// FormData / Blob は Node.js 20 にグローバルで存在する(form-data パッケージ不要)
const form = new FormData();
// data パートは application/json を明示(未指定だと API が 415 を返す)
form.append(
  "data",
  new Blob(
    [JSON.stringify({
      from: { email: "noreply@example.com", name: "Example" },
      to: "user@example.com",
      subject: "請求書を送付します",
      encode: "UTF-8",
      text_part: "添付の請求書をご確認ください。",
    })],
    { type: "application/json" },
  ),
);

// ファイル名を ASCII に置き換え + 日本語ファイル名は本文に明記(第3引数がヘッダ上のファイル名)
form.append("file", await fs.openAsBlob("./invoice_202606.pdf", { type: "application/pdf" }), "invoice_202606.pdf");

await fetch("https://app.engn.jp/api/v1/deliveries/transaction", {
  method: "POST",
  headers: {
    Authorization: `Bearer ${process.env.BE_TOKEN}`,
    // Content-Type は付けない(multipart の boundary は fetch が自動設定する)
  },
  body: form,
});

どうしても日本語ファイル名が必須なら、filename オプションに UTF-8 文字列を渡します。ただし Content-Disposition ヘッダに非ASCIIファイル名がどう載るか(RFC 2231 の filename*= になるか、生のまま載るか)は、使う HTTP クライアント/ライブラリのバージョンと blastengine 側の解釈に依存します。公式ドキュメントに日本語ファイル名の明確な保証はないため、本番投入前に実際の受信メールのヘッダで必ず検証してください。 確実性を優先するならファイル名は ASCII にしておくのが鉄板です。

form.append("file", await fs.openAsBlob("./請求書.pdf", { type: "application/pdf" }), "請求書_202606.pdf"); // 受信側ヘッダで要検証

文字化けが起きたときの切り分け手順

「化けてる」と報告が来たら、以下の順でチェックします。

Step 1:送信時の本文を確認

配信詳細APIで、blastengine が受け取った本文をそのまま取得できます。

const data = await be.get(`/deliveries/${deliveryId}`);
console.log(data.subject);
console.log(data.text_part);
console.log(data.html_part);

ここで既に化けていれば、クライアント側で送信前に化けている=アプリのエンコード処理が原因。

Step 2:配信ログで宛先・ステータスを確認

const logs = await be.get("/logs/mails/results", { delivery_id: deliveryId, count: 100 });

for (const log of logs.data) {
  console.log({
    email: log.email,
    status: log.status,             // SENT / HARDERROR / SOFTERROR / DROP
    code: log.last_response_code,   // 250, 550 等
    message: log.last_response_message,
  });
}

特定の宛先だけ化ける → 受信側メーラーの問題の可能性が高い。

Step 3:受信側ヘッダを読む

受信メールの「メッセージのソースを表示」から以下のヘッダをチェック。

Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: base64
  • charsetencode の指定が一致しているか
  • Content-Transfer-Encodingbase64 / quoted-printable / 7bit のどれか
  • Subject: の MIME エンコード(=?UTF-8?B?...?= 等)が正しく解釈されているか

Step 4:別クライアントで再現確認

クライアント エンコード対応
Gmail (Web) UTF-8 / ISO-2022-JP どちらも安定
Apple Mail UTF-8 推奨。古いISO-2022-JPで稀に崩れる
Outlook (デスクトップ) 独特の解釈。HTML 崩れに注意
Outlook (Web) UTF-8 推奨
Thunderbird 両方OK
iPhone Mail UTF-8 推奨

複数クライアントで再現すれば送信側起因、特定クライアントだけなら受信側起因と切り分け可能。

実装サンプル:エンコード自動選択ヘルパー

宛先ドメインで自動的にエンコードを切り替える例。

const LEGACY_DOMAINS = new Set([
  "docomo.ne.jp",
  "ezweb.ne.jp",
  "softbank.ne.jp",
  "i.softbank.jp",
  "vodafone.ne.jp",
]);

function pickEncoding(email) {
  const domain = email.split("@")[1]?.toLowerCase();
  return LEGACY_DOMAINS.has(domain) ? "ISO-2022-JP" : "UTF-8";
}

export async function sendSafe({ to, subject, textPart, htmlPart }) {
  const encode = pickEncoding(to);

  // ISO-2022-JP の場合は subject / text_part / html_part すべて機種依存文字を除去する。
  // html_part をサニタイズし忘れると、本文中の範囲外文字で API が 400 を返す。
  const sanitize = (s) => (encode === "ISO-2022-JP" && s != null ? sanitizeForMail(s) : s);

  return be.post("/deliveries/transaction", {
    from: { email: "noreply@example.com", name: "Example" },
    to,
    encode,
    subject: sanitize(subject),
    text_part: sanitize(textPart),
    html_part: sanitize(htmlPart),
  });
}

ただし運用負荷を考えると、「全部 UTF-8 で送って、化けの報告が来てから対応」 が今の現実解。9割以上の宛先で UTF-8 で問題ありません。

まとめ

  • デフォルトは UTF-8。Webメール中心の宛先なら何も考えずこれで OK
  • ISO-2022-JP はレガシー宛が混じる、または最大互換性が欲しいときの選択肢
  • 機種依存文字は normalize("NFKC") + 置換テーブル で送信前にサニタイズ
  • HTMLメールは <meta charset>encode を一致させる
  • 添付ファイル名は ASCIIが最も安全、日本語必須なら RFC 2231 でエンコード
  • 化けたら 送信本文 → 配信ログ → 受信ヘッダ → 別クライアント の順で切り分け

文字化けは「起きる前に潰す」のがコスパ最強。送信前のサニタイズと、エンコードの明示指定を運用に組み込んでおきましょう。

公式リファレンス:blastengine API ドキュメント

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?