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
-
charsetとencodeの指定が一致しているか -
Content-Transfer-Encodingがbase64/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 ドキュメント