Symfony Advent Calendar 2019 8日目の記事です。
昨日は@brtriverさんのSymfonyの歴史を振り返ってみるでした!
はじめに
こちらの記事は、symfony/mailerで試行錯誤したことの続きになります。
前回は、配信処理までの設定で四苦八苦しましたが、今回は送信するメールの文字コードやエンコードの仕方について試行錯誤していきたいと思います。
メールは、インターネット時代のコミュニケーション手段として、
-
utf-8、iso-2022-jp
などの文字コード -
7bit, 8bit, base64, quoted-printable
などのエンコーディング - 単純に
テキスト
のみ、テキストとHTMLの併用
など
様々な設定ができるようになっています。
そしてSPAM配信の増加による、迷惑メール対策技術の向上によって、
メールを迷惑メールとして判定させないために、いろいろと気をつける部分となります。
新規のシステムならよいのですが、
システムのリニューアルによって、メールが届かなくなるということはよくあります。
これは送信されるメールの文面が、以前のシステムと異なっていることも関係します。
例えば、夏休みに明けに、気になるあの子が茶髪ギャルになっていたら、違和感覚えますよね。
迷惑メール判定も、そんな感じです。
夏休み前 | 夏休み明け |
---|---|
ここまでの変化ではないにしても、ちょっと髪型変えたかな?ぐらいの変化、
メールの世界だと、ヘッダーにX-Mailler
が追加されただけでも、迷惑メールとして判定されたりします。
感覚的には、文字コード、エンコーディングが違うと大きな変化だったりします。
なので、前回のシステムと同様のメールを送信ができるように、Symfony/mailerも使いこなせる様になる必要があるのです。
テスト環境の構築
Dockerで開発環境を作って、SymfonyのWebスケルトンプロジェクトを作ります。
そこまで作ったのが以下になります。
一番かんたんな送信方法
「[Creating & Sending Messages](Creating & Sending Messages)」のサンプルコードを実行します。
class MailerController extends AbstractController
{
/**
* @Route("/mailer", name="mailer")
*/
public function index(MailerInterface $mailer)
{
$email = (new Email())
->from('hello@example.com')
->to('you@example.com')
//->cc('cc@example.com')
//->bcc('bcc@example.com')
//->replyTo('fabien@example.com')
//->priority(Email::PRIORITY_HIGH)
->subject('Time for Symfony Mailer!')
->text('Sending emails is fun again!')
->html('<p>See Twig integration for better HTML integration!</p>');
$mailer->send($email);
return $this->render('mailer/index.html.twig', [
'controller_name' => 'MailerController',
]);
}
}
http://localhost:8000/mailer を開くと以下の画面が表示されてメールが送信されました。
MailDevで確認すると、受信されています。
1つ目の画像はHTMLメールを表示した場合。2つ目の画像は、テキストの画面を表示した場合です。
メールのソースは、以下になります。
From: hello@example.com
To: you@example.com
Subject: Time for Symfony Mailer!
Message-ID: <f9549d7eac6696b8cd513cc398e5bcc9@example.com>
MIME-Version: 1.0
Date: Wed, 04 Dec 2019 14:51:44 +0900
Content-Type: multipart/alternative;
boundary="_=_symfony_1575438704_55dde3f773e700c11643dd0965c1db31_=_"
--_=_symfony_1575438704_55dde3f773e700c11643dd0965c1db31_=_
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
Sending emails is fun again!
--_=_symfony_1575438704_55dde3f773e700c11643dd0965c1db31_=_
Content-Type: text/html; charset=utf-8
Content-Transfer-Encoding: quoted-printable
<p>See Twig integration for better HTML integration!</p>
--_=_symfony_1575438704_55dde3f773e700c11643dd0965c1db31_=_--
日本語を試してみる
送信者名、受信者名、サブジェクト、本文に日本語を試してみましょう。
メールアドレスでの名前の指定は、Email Addressesに記載がありますように、Address.phpクラスを利用すると良さそうです。
サブジェクト、本文は、何も考えずに日本語を入れてみます。
また、HTMLは面倒なので、ここでは一旦、オミットしておきます。
以下のように修正しました。
class MailerController extends AbstractController
{
/**
* @Route("/mailer", name="mailer")
*/
public function index(MailerInterface $mailer)
{
$body = <<<EOL
https://symfony.com/why-use-a-framework
フレームワークを使用する必要があるのはなぜですか?
フレームワークは絶対に必要というわけではありません。それは、あなたがより良く、より速く開発するのを
~~~中略~~~
この点で、フレームワークはブラックボックスではありません!Symfonyの場合、それはまだPHPです...開発されるアプリケーションはSymfonyユニバースに限定されず、たとえば他のPHPライブラリとネイティブに相互運用できます。
EOL;
$email = (new Email())
->from(new Address('hello@example.com', '送信者名'))
->to(new Address('you@example.com', '受信者名'))
//->cc('cc@example.com')
//->bcc('bcc@example.com')
//->replyTo('fabien@example.com')
//->priority(Email::PRIORITY_HIGH)
->subject('日本語のサブジェクトになります。長くなると文字化けするという話もありますので、長く書いてみます。これぐらい長いとどうかな?')
->text($body)
// ->html('<p>See Twig integration for better HTML integration!</p>')
;
$mailer->send($email);
return $this->render('mailer/index.html.twig', [
'controller_name' => 'MailerController',
]);
}
}
MailDevではこのように受信ができました。
パッと見はよさそうです。
メールソースを見ると、quoted-printable でエンコーディングをされていました。
From: =?utf-8?Q?=E9=80=81=E4=BF=A1=E8=80=85=E5=90=8D?= <hello@example.com>
To: =?utf-8?Q?=E5=8F=97=E4=BF=A1=E8=80=85=E5=90=8D?= <you@example.com>
Subject: =?utf-8?Q?=E6=97=A5=E6=9C=AC=E8=AA=9E=E3=81=AE?=
=?utf-8?Q?=E3=82=B5=E3=83=96=E3=82=B8=E3=82=A7?=
=?utf-8?Q?=E3=82=AF=E3=83=88=E3=81=AB=E3=81=AA?=
=?utf-8?Q?=E3=82=8A=E3=81=BE=E3=81=99=E3=80=82?=
=?utf-8?Q?=E9=95=B7=E3=81=8F=E3=81=AA=E3=82=8B?=
=?utf-8?Q?=E3=81=A8=E6=96=87=E5=AD=97=E5=8C=96?=
=?utf-8?Q?=E3=81=91=E3=81=99=E3=82=8B=E3=81=A8?=
=?utf-8?Q?=E3=81=84=E3=81=86=E8=A9=B1=E3=82=82?=
=?utf-8?Q?=E3=81=82=E3=82=8A=E3=81=BE=E3=81=99?=
=?utf-8?Q?=E3=81=AE=E3=81=A7=E3=80=81=E9=95=B7?=
=?utf-8?Q?=E3=81=8F=E6=9B=B8=E3=81=84=E3=81=A6?=
=?utf-8?Q?=E3=81=BF=E3=81=BE=E3=81=99=E3=80=82?=
=?utf-8?Q?=E3=81=93=E3=82=8C=E3=81=90=E3=82=89?=
=?utf-8?Q?=E3=81=84=E9=95=B7=E3=81=84=E3=81=A8?=
=?utf-8?Q?=E3=81=A9=E3=81=86=E3=81=8B=E3=81=AA=EF=BC=9F?=
Message-ID: <964a02541e6dc9af98f5af3f31b3bf77@example.com>
MIME-Version: 1.0
Date: Wed, 04 Dec 2019 15:22:13 +0900
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: quoted-printable
https://symfony.com/why-use-a-framework
=E3=83=95=E3=83=AC=E3=83=BC=
=E3=83=A0=E3=83=AF=E3=83=BC=E3=82=AF=E3=82=92=E4=BD=BF=E7=94=A8=E3=81=99=
=E3=82=8B=E5=BF=85=E8=A6=81=E3=81=8C=E3=81=82=E3=82=8B=E3=81=AE=E3=81=AF=
=E3=81=AA=E3=81=9C=E3=81=A7=E3=81=99=E3=81=8B=EF=BC=9F
=E3=83=95=E3=83=
=AC=E3=83=BC=E3=83=A0=E3=83=AF=E3=83=BC=E3=82=AF=E3=81=AF=E7=B5=B6=E5=AF=
=BE=E3=81=AB=E5=BF=85=E8=A6=81=E3=81=A8=E3=81=84=E3=81=86=E3=82=8F=E3=81=
=91=E3=81=A7=E3=81=AF=E3=81=82=E3=82=8A=E3=81=BE=E3=81=9B=E3=82=93=E3=80=
以下省略
Base64で送信する
日本語は送信ができるようですが、エンコーディングがquoted-printable
なので、いろいろとデータ量が増えています。日本だとbase64が主流なのでbase64
に変更できるかトライしてみます。
Emailクラスでメールデータを作成しますので、Email.phpを見ていきます。
そうすると、EmailクラスがMimeコンポーネントに所属していることがわかりました!
メールデータをカスタマイズする
The Mime Componentドキュメントに目を通すと、Emailクラスは高レベルAPI、Messageクラスは低レベルAPIと記載があります。
そして、Creating Raw Email Messagesとして、直接メッセージを作成する方法が記載されていました。
use Symfony\Component\Mime\Header\Headers;
use Symfony\Component\Mime\Message;
use Symfony\Component\Mime\Part\Multipart\AlternativePart;
use Symfony\Component\Mime\Part\TextPart;
$headers = (new Headers())
->addMailboxListHeader('From', ['fabien@symfony.com'])
->addMailboxListHeader('To', ['foo@example.com'])
->addTextHeader('Subject', 'Important Notification')
;
$textContent = new TextPart('Lorem ipsum...');
$htmlContent = new TextPart('<h1>Lorem ipsum</h1> <p>...</p>', 'html');
$body = new AlternativePart($textContent, $htmlContent);
$email = new Message($headers, $body);
本文をBase64でエンコードする
サンプルを見ると、メール本文は、TextPartクラスが作成しています。
TextPartクラス
のコンストラクタを見ると、ビンゴです!
エンコーディングを引数に取っていました。
public function __construct($body, ?string $charset = 'utf-8', $subtype = 'plain', string $encoding = null)
{
parent::__construct();
~~~中略~~~
if (null === $encoding) {
$this->encoding = $this->chooseEncoding();
} else {
if ('quoted-printable' !== $encoding && 'base64' !== $encoding && '8bit' !== $encoding) {
throw new InvalidArgumentException(sprintf('The encoding must be one of "quoted-printable", "base64", or "8bit" ("%s" given).', $encoding));
}
$this->encoding = $encoding;
}
}
メールアドレスの名前部分をBase64でエンコードする
ヘッダーは、Headersクラスで定義していますが、エンコーディングについては、基底クラスのAbstractHeaderクラスで、quoted-printable
に固定されていました。
namespace Symfony\Component\Mime\Header;
use Symfony\Component\Mime\Encoder\QpMimeHeaderEncoder;
/**
* An abstract base MIME Header.
*
* @author Chris Corbyn
*/
abstract class AbstractHeader implements HeaderInterface
{
private static $encoder;
~~~中略~~~
/**
* Get a token as an encoded word for safe insertion into headers.
*/
protected function getTokenAsEncodedWord(string $token, int $firstLineOffset = 0): string
{
if (null === self::$encoder) {
ここで固定=> self::$encoder = new QpMimeHeaderEncoder();
}
~~~省略~~~
ただし、エンコーディングするのは条件があるようです。
MailboxListHeader.php(106)で、$this->createPhrase
で名前をエンコードしています。
public function getAddressStrings(): array
{
$strings = [];
foreach ($this->addresses as $address) {
$str = $address->getEncodedAddress();
if ($name = $address->getName()) {
$str = $this->createPhrase($this, $name, $this->getCharset(), !$strings).' <'.$str.'>';
}
$strings[] = $str;
}
return $strings;
}
これはAbstractHeader.php(88)で定義されていて、preg_match('/^'.self::PHRASE_PATTERN.'$/D', $phraseStr)
にマッチしなければ$phraseStr = $this->encodeWords($header, $string, $usedLength);
でエンコードするようです。
protected function createPhrase(HeaderInterface $header, string $string, string $charset, bool $shorten = false): string
{
// Treat token as exactly what was given
$phraseStr = $string;
// If it's not valid
if (!preg_match('/^'.self::PHRASE_PATTERN.'$/D', $phraseStr)) {
// .. but it is just ascii text, try escaping some characters
// and make it a quoted-string
if (preg_match('/^[\x00-\x08\x0B\x0C\x0E-\x7F]*$/D', $phraseStr)) {
foreach (['\\', '"'] as $char) {
$phraseStr = str_replace($char, '\\'.$char, $phraseStr);
}
$phraseStr = '"'.$phraseStr.'"';
} else {
// ... otherwise it needs encoding
// Determine space remaining on line if first line
if ($shorten) {
$usedLength = \strlen($header->getName().': ');
} else {
$usedLength = 0;
}
$phraseStr = $this->encodeWords($header, $string, $usedLength);
}
}
return $phraseStr;
}
PHRASE_PATTERN
がAbstractHeader.php(23)で定義されていますが、長くて意味がわかりません。
たぶん、エンコード済みならスキップする処理じゃないかな?と予想します。
そしたら、名前は、mb_encode_mimeheader
して渡せば良さそうです。
const PHRASE_PATTERN = '(?:(?:(?:(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?[a-zA-Z0-9!#\$%&\'\*\+\-\/=\?\^_`\{\}\|~]+(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?)|(?:(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?"((?:(?:[ \t]*(?:\r\n))?[ \t])?(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21\x23-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])))*(?:(?:[ \t]*(?:\r\n))?[ \t])?"(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))*(?:(?:(?:(?:[ \t]*(?:\r\n))?[ \t])?(\((?:(?:(?:[ \t]*(?:\r\n))?[ \t])|(?:(?:[\x01-\x08\x0B\x0C\x0E-\x19\x7F]|[\x21-\x27\x2A-\x5B\x5D-\x7E])|(?:\\[\x00-\x08\x0B\x0C\x0E-\x7F])|(?1)))*(?:(?:[ \t]*(?:\r\n))?[ \t])?\)))|(?:(?:[ \t]*(?:\r\n))?[ \t])))?))+?)';
サブジェクトをbase64でエンコードする
次にサブジェクトですが、Headers.php(112)からたどっていくと、AbstractHeaderクラス
を継承したUnstructuredHeaderクラス
に格納されていました。
/**
* @return $this
*/
public function addTextHeader(string $name, string $value): self
{
return $this->add(new UnstructuredHeader($name, $value));
}
最終的には、UnstructuredHeader.php(65)でエンコードして渡しています。
/**
* Get the value of this header prepared for rendering.
*/
public function getBodyAsString(): string
{
return $this->encodeWords($this, $this->value);
}
これはAbstractHeader
で定義されていて、$this->tokenNeedsEncoding
があります。
/**
* Encode needed word tokens within a string of input.
*/
protected function encodeWords(HeaderInterface $header, string $input, int $usedLength = -1): string
{
$value = '';
$tokens = $this->getEncodableWordTokens($input);
foreach ($tokens as $token) {
// See RFC 2822, Sect 2.2 (really 2.2 ??)
if ($this->tokenNeedsEncoding($token)) {
// Don't encode starting WSP
$firstChar = substr($token, 0, 1);
switch ($firstChar) {
case ' ':
case "\t":
$value .= $firstChar;
$token = substr($token, 1);
}
if (-1 == $usedLength) {
$usedLength = \strlen($header->getName().': ') + \strlen($value);
}
$value .= $this->getTokenAsEncodedWord($token, $usedLength);
} else {
$value .= $token;
}
}
return $value;
}
tokenNeedsEncoding()を確認すると、制御文字と改行が含まれてれば、エンコードが必要という判定をしています。
ですから、こちらもエンコード済みなら、この判定でFalseになって、そのまま使ってくれそうです。
protected function tokenNeedsEncoding(string $token): bool
{
return (bool) preg_match('~[\x00-\x08\x10-\x19\x7F-\xFF\r\n]~', $token);
}
サンプルを以下のように書き換えます。
class MailerController extends AbstractController
{
/**
* @Route("/mailer", name="mailer")
*/
public function index(MailerInterface $mailer)
{
$body = <<<EOL
https://symfony.com/why-use-a-framework
フレームワークを使用する必要があるのはなぜですか?
フレームワークは絶対に必要というわけではありません。それは、あなたがより良く、より速く開発するのを
~~~中略~~~
EOL;
$headers = (new Headers())
->addMailboxListHeader('From', [new Address('hello@example.com', mb_encode_mimeheader('送信者名'))])
->addMailboxListHeader('To', [new Address('you@example.com', mb_encode_mimeheader('受信者名'))])
->addTextHeader('Subject', mb_encode_mimeheader('日本語のサブジェクトになります。長くなると文字化けするという話もありますので、長く書いてみます。これぐらい長いとどうかな?'))
;
$textContent = new TextPart($body, 'utf-8', 'plain', 'base64');
$email = new Message($headers, $textContent);
$mailer->send($email);
return $this->render('mailer/index.html.twig', [
'controller_name' => 'MailerController',
]);
}
}
これでBase64のいい感じなメールがでるかと思いましたが、MailDevではサブジェクトが文字化けしてしまいました。。。
メールソースを確認すると、
本文は想定通りのUTF-8なのですが、
Subjectは、base64
でエンコードしたデータを渡しているのですが、quoted-printable
で更にエンコードがされていました。
また、From/Toはiso-2022-jp
が使われています。
From: =?ISO-2022-JP?B?GyRCQXc/LjxUTD4bKEI=?= <hello@example.com>
To: =?ISO-2022-JP?B?GyRCPHU/LjxUTD4bKEI=?= <you@example.com>
Subject: =?utf-8?Q?=3D=3FISO-2022-JP=3FB=3FGyRCRnxLXDhsJE4lNSVWJTglJyUv?=
=?utf-8?Q?JUgkSyRKJGokXiQ5ISNEOSQvGyhC=3F=3D?=
=?utf-8?Q?_=3D=3FISO-2022-JP=3FB=3FGyRCJEokayRISjg7ejI9JDEkOSRrJEgkJCQ?=
=?utf-8?Q?mT0MkYiQiJGokXiQ5GyhC=3F=3D?=
=?utf-8?Q?_=3D=3FISO-2022-JP=3FB=3FGy?=
=?utf-8?Q?RCJE4kRyEiRDkkLz1xJCQkRiRfJF4kOSEjJDMkbCQwJGkkJEQ5GyhC=3F=3D?=
=?utf-8?Q??= =?ISO-2022-JP?B?GyRCJCQkSCRJJCYkKyRKISkbKEI=?=
Message-ID: <d3e1f390c82ee075dbcf96b7565bccff@example.com>
MIME-Version: 1.0
Date: Wed, 04 Dec 2019 16:19:51 +0900
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64
aHR0cHM6Ly9zeW1mb255LmNvbS93aHktdXNlLWEtZnJhbWV3b3JrCgrjg5Xjg6zjg7zjg6Djg6/j
g7zjgq/jgpLkvb/nlKjjgZnjgovlv4XopoHjgYzjgYLjgovjga7jga/jgarjgZzjgafjgZnjgYvv
vJ8K44OV44Os44O844Og44Ov44O844Kv44Gv57W25a++44Gr5b+F6KaB44Go44GE44GG44KP44GR
44Gn44Gv44GC44KK44G+44Gb44KT44CC44Gd44KM44Gv44CB44GC44Gq44Gf44GM44KI44KK6Imv
44GP44CB44KI44KK6YCf44GP6ZaL55m644GZ44KL44Gu44KS5Yqp44GR44KL44Gf44KB44Gr5Yip
55So44Gn44GN44KL44OE44O844Or44Gu44CM44Gh44KH44GG44Gp44CNMeOBpOOBp+OBme+8gQoK
44OV44Os44O844Og44Ov44O844Kv44Gv44CB44OT44K444ON44K544Or44O844Or44Gr5a6M5YWo
44Gr5rqW5oug44GX44CB5qeL6YCg5YyW44GV44KM44CB44Oh44Oz44OG44OK44Oz44K544Go44Ki
44OD44OX44Kw44Os44O844OJ44Gu5Lih5pa544GM5Y+v6IO944Gq44Ki44OX44Oq44Kx44O844K3
iso-2022-jp
については、よくある話で、mb_language("uni")
を宣言すれば良さそうです。
mb_language("uni");
mb_internal_encoding("UTF-8");
サブジェクトの方でquoted-printable
になっていますので、
さきほどのAbstractHeader.php(120)のencodeWords()
を更に見ていきましょう。
/**
* Encode needed word tokens within a string of input.
*/
protected function encodeWords(HeaderInterface $header, string $input, int $usedLength = -1): string
{
$value = '';
$tokens = $this->getEncodableWordTokens($input);
foreach ($tokens as $token) {
// See RFC 2822, Sect 2.2 (really 2.2 ??)
if ($this->tokenNeedsEncoding($token)) {
// Don't encode starting WSP
$firstChar = substr($token, 0, 1);
switch ($firstChar) {
case ' ':
case "\t":
$value .= $firstChar;
$token = substr($token, 1);
}
if (-1 == $usedLength) {
$usedLength = \strlen($header->getName().': ') + \strlen($value);
}
$value .= $this->getTokenAsEncodedWord($token, $usedLength);
} else {
$value .= $token;
}
}
return $value;
}
$this->getEncodableWordTokens($input)
を見ていなかったので見てみます。
protected function getEncodableWordTokens(string $string): array
{
$tokens = [];
$encodedToken = '';
// Split at all whitespace boundaries
foreach (preg_split('~(?=[\t ])~', $string) as $token) {
if ($this->tokenNeedsEncoding($token)) {
$encodedToken .= $token;
} else {
if (\strlen($encodedToken) > 0) {
$tokens[] = $encodedToken;
$encodedToken = '';
}
$tokens[] = $token;
}
}
if (\strlen($encodedToken)) {
$tokens[] = $encodedToken;
}
return $tokens;
}
preg_split('~(?=[\t ])~', $string)
で、=?UTF-8?B?77yf?='
の最後の?=<タブ or スペース>
で分割しています。
それを$this->tokenNeedsEncoding
でエンコード済みかチェックしているのですが。。。
サブジェクトをmb_encode_mimeheader()
した結果は、以下になります。
=?UTF-8?B?5pel5pys6Kqe44Gu44K144OW44K444Kn44Kv44OI44Gr44Gq44KK44G+44GZ?=
=?UTF-8?B?44CC6ZW344GP44Gq44KL44Go5paH5a2X5YyW44GR44GZ44KL44Go44GE44GG?=
=?UTF-8?B?6Kmx44KC44GC44KK44G+44GZ44Gu44Gn44CB6ZW344GP5pu444GE44Gm44G/?=
=?UTF-8?B?44G+44GZ44CC44GT44KM44GQ44KJ44GE6ZW344GE44Go44Gp44GG44GL44Gq?=
=?UTF-8?B?77yf?='
さきほどの、preg_split('~(?=[\t ])~', $string)
で分割をすると、分割したデータの最後に改行がついてしまうみたいで、その後の$this->tokenNeedsEncoding($token)
でエンコード済みと判定されないようです。
それでは、mb_encode_mimeheader()
の結果から改行コードを外してみます。
サンプルコードを以下のように修正しました。
class MailerController extends AbstractController
{
/**
* @Route("/mailer", name="mailer")
*/
public function index(MailerInterface $mailer)
{
$body = <<<EOL
https://symfony.com/why-use-a-framework
フレームワークを使用する必要があるのはなぜですか?
フレームワークは絶対に必要というわけではありません。それは、あなたがより良く、より速く開発するのを
~~~中略~~~
EOL;
mb_language("uni");
mb_internal_encoding("UTF-8");
$subject = mb_encode_mimeheader('日本語のサブジェクトになります。長くなると文字化けするという話もありますので、長く書いてみます。これぐらい長いとどうかな?');
$subject = str_replace("\r\n", '', $subject);
$headers = (new Headers())
->addMailboxListHeader('From', [new Address('hello@example.com', mb_encode_mimeheader('送信者名'))])
->addMailboxListHeader('To', [new Address('you@example.com', mb_encode_mimeheader('受信者名'))])
->addTextHeader('Subject', $subject)
;
$textContent = new TextPart($body, 'utf-8', 'plain', 'base64');
$email = new Message($headers, $textContent);
$mailer->send($email);
return $this->render('mailer/index.html.twig', [
'controller_name' => 'MailerController',
]);
}
}
実行してみると、subjcetの文字化けが治りましたね。
メールソースを確認しましたが、想定どおりです。
From: =?UTF-8?B?6YCB5L+h6ICF5ZCN?= <hello@example.com>
To: =?UTF-8?B?5Y+X5L+h6ICF5ZCN?= <you@example.com>
Subject: =?UTF-8?B?5pel5pys6Kqe44Gu44K144OW44K444Kn44Kv44OI44Gr44Gq44KK44G+44GZ?=
=?UTF-8?B?44CC6ZW344GP44Gq44KL44Go5paH5a2X5YyW44GR44GZ44KL44Go44GE44GG?=
=?UTF-8?B?6Kmx44KC44GC44KK44G+44GZ44Gu44Gn44CB6ZW344GP5pu444GE44Gm44G/?=
=?UTF-8?B?44G+44GZ44CC44GT44KM44GQ44KJ44GE6ZW344GE44Go44Gp44GG44GL44Gq?=
=?UTF-8?B?77yf?=
Message-ID: <1916f37d03fc486c2babe15a606eda8f@example.com>
MIME-Version: 1.0
Date: Thu, 05 Dec 2019 11:19:48 +0900
Content-Type: text/plain; charset=utf-8
Content-Transfer-Encoding: base64
aHR0cHM6Ly9zeW1mb255LmNvbS93aHktdXNlLWEtZnJhbWV3b3JrCgrjg5Xjg6zjg7zjg6Djg6/j
g7zjgq/jgpLkvb/nlKjjgZnjgovlv4XopoHjgYzjgYLjgovjga7jga/jgarjgZzjgafjgZnjgYvv
vJ8K44OV44Os44O844Og44Ov44O844Kv44Gv57W25a++44Gr5b+F6KaB44Go44GE44GG44KP44GR
44Gn44Gv44GC44KK44G+44Gb44KT44CC44Gd44KM44Gv44CB44GC44Gq44Gf44GM44KI44KK6Imv
以下省略
base64のまとめ
ということで、サブジェクトのワークアラウンドが微妙ですが、UTF-8+base64での送信が実現できました。
パッチを送る感じになるのかな?
iso-2022-jpで送信する
それでは、文字コードがiso-2022-jp
で、エンコーディングが7bit
にチャレンジしてみます。
ヘッダーは、mb_encode_mimeheader()
で準備ができますし、本文が7bitが通れば行けそうです。
サンプルコードを以下のように修正します。
class MailerController extends AbstractController
{
/**
* @Route("/mailer", name="mailer")
*/
public function index(MailerInterface $mailer)
{
$body = <<<EOL
https://symfony.com/why-use-a-framework
フレームワークを使用する必要があるのはなぜですか?
フレームワークは絶対に必要というわけではありません。それは、あなたがより良く、より速く開発するのを
~~~中略~~~
EOL;
$subject = mb_encode_mimeheader('日本語のサブジェクトになります。長くなると文字化けするという話もありますので、長く書いてみます。これぐらい長いとどうかな?');
$subject = str_replace("\r\n", '', $subject);
$headers = (new Headers())
->addMailboxListHeader('From', [new Address('hello@example.com', mb_encode_mimeheader('送信者名'))])
->addMailboxListHeader('To', [new Address('you@example.com', mb_encode_mimeheader('受信者名'))])
->addTextHeader('Subject', $subject)
;
$textContent = new TextPart($body, 'iso-2022-jp', 'plain', '7bit');
$email = new Message($headers, $textContent);
$mailer->send($email);
return $this->render('mailer/index.html.twig', [
'controller_name' => 'MailerController',
]);
}
}
以下のようなエラーがでました。
7bit
のエンコーディングに対応していないようです。
しょうがないので、エンコーディングをbase64
に変更してトライしてみます。
class MailerController extends AbstractController
{
/**
* @Route("/mailer", name="mailer")
*/
public function index(MailerInterface $mailer)
{
$body = <<<EOL
https://symfony.com/why-use-a-framework
フレームワークを使用する必要があるのはなぜですか?
フレームワークは絶対に必要というわけではありません。それは、あなたがより良く、より速く開発するのを
~~~中略~~~
EOL;
$subject = mb_encode_mimeheader('日本語のサブジェクトになります。長くなると文字化けするという話もありますので、長く書いてみます。これぐらい長いとどうかな?');
$subject = str_replace("\r\n", '', $subject);
$headers = (new Headers())
->addMailboxListHeader('From', [new Address('hello@example.com', mb_encode_mimeheader('送信者名'))])
->addMailboxListHeader('To', [new Address('you@example.com', mb_encode_mimeheader('受信者名'))])
->addTextHeader('Subject', $subject)
;
$textContent = new TextPart($body, 'iso-2022-jp', 'plain', 'base64');
$email = new Message($headers, $textContent);
$mailer->send($email);
return $this->render('mailer/index.html.twig', [
'controller_name' => 'MailerController',
]);
}
}
本文が痛い感じになってしまいました。
こちらがメールソースになります。From/To/Subjectのヘッダーは、想定通りですし、Content-Type
も想定どおりです。
From: =?ISO-2022-JP?B?GyRCQXc/LjxUTD4bKEI=?= <hello@example.com>
To: =?ISO-2022-JP?B?GyRCPHU/LjxUTD4bKEI=?= <you@example.com>
Subject: =?ISO-2022-JP?B?GyRCRnxLXDhsJE4lNSVWJTglJyUvJUgkSyRKJGokXiQ5ISNEOSQvGyhC?=
=?ISO-2022-JP?B?GyRCJEokayRISjg7ejI9JDEkOSRrJEgkJCQmT0MkYiQiJGokXiQ5GyhC?=
=?ISO-2022-JP?B?GyRCJE4kRyEiRDkkLz1xJCQkRiRfJF4kOSEjJDMkbCQwJGkkJEQ5GyhC?=
=?ISO-2022-JP?B?GyRCJCQkSCRJJCYkKyRKISkbKEI=?=
Message-ID: <e2e57a249f7daeaff165d25eddc2a876@example.com>
MIME-Version: 1.0
Date: Thu, 05 Dec 2019 12:20:15 +0900
Content-Type: text/plain; charset=iso-2022-jp
Content-Transfer-Encoding: base64
aHR0cHM6Ly9zeW1mb255LmNvbS93aHktdXNlLWEtZnJhbWV3b3JrCgrjg5Xjg6zjg7zjg6Djg6/j
g7zjgq/jgpLkvb/nlKjjgZnjgovlv4XopoHjgYzjgYLjgovjga7jga/jgarjgZzjgafjgZnjgYvv
vJ8K44OV44Os44O844Og44Ov44O844Kv44Gv57W25a++44Gr5b+F6KaB44Go44GE44GG44KP44GR
44Gn44Gv44GC44KK44G+44Gb44KT44CC44Gd44KM44Gv44CB44GC44Gq44Gf44GM44KI44KK6Imv
44GP44CB44KI44KK6YCf44GP6ZaL55m644GZ44KL44Gu44KS5Yqp44GR44KL44Gf44KB44Gr5Yip
55So44Gn44GN44KL44OE44O844Or44Gu44CM44Gh44KH44GG44Gp44CNMeOBpOOBp+OBme+8gQoK
以下、省略
またまた、ソースコードを見てみると、TextPartクラス
で渡しているiso-2022-jp
というのは、Content-Type
の出力に使うだけで、文字コードの変換はしていませんでした。
一応、Base64Encoder.php(26)で$charset
として文字コードまでもらっているのですが、使っていません;;
class Base64Encoder implements EncoderInterface
{
public function encodeString(string $string, ?string $charset = 'utf-8', int $firstLineOffset = 0, int $maxLineLength = 0): string
{
if (0 >= $maxLineLength || 76 < $maxLineLength) {
$maxLineLength = 76;
}
$encodedString = base64_encode($string);
$firstLine = '';
if (0 !== $firstLineOffset) {
$firstLine = substr($encodedString, 0, $maxLineLength - $firstLineOffset)."\r\n";
$encodedString = substr($encodedString, $maxLineLength - $firstLineOffset);
}
return $firstLine.trim(chunk_split($encodedString, $maxLineLength, "\r\n"));
}
}
そしたら本文も自前で変換する必要がわかったので、以下のようにします。
class MailerController extends AbstractController
{
/**
* @Route("/mailer", name="mailer")
*/
public function index(MailerInterface $mailer)
{
$body = <<<EOL
https://symfony.com/why-use-a-framework
フレームワークを使用する必要があるのはなぜですか?
フレームワークは絶対に必要というわけではありません。それは、あなたがより良く、より速く開発するのを
~~~中略~~~
EOL;
$subject = mb_encode_mimeheader('日本語のサブジェクトになります。長くなると文字化けするという話もありますので、長く書いてみます。これぐらい長いとどうかな?');
$subject = str_replace("\r\n", '', $subject);
$headers = (new Headers())
->addMailboxListHeader('From', [new Address('hello@example.com', mb_encode_mimeheader('送信者名'))])
->addMailboxListHeader('To', [new Address('you@example.com', mb_encode_mimeheader('受信者名'))])
->addTextHeader('Subject', $subject)
;
$body = mb_convert_encoding($body, 'ISO-2022-JP-MS', 'UTF-8');
$textContent = new TextPart($body, 'iso-2022-jp', 'plain', 'base64');
$email = new Message($headers, $textContent);
$mailer->send($email);
return $this->render('mailer/index.html.twig', [
'controller_name' => 'MailerController',
]);
}
}
本文も問題なさそうです。
メールソースはこんな感じ。
From: =?ISO-2022-JP?B?GyRCQXc/LjxUTD4bKEI=?= <hello@example.com>
To: =?ISO-2022-JP?B?GyRCPHU/LjxUTD4bKEI=?= <you@example.com>
Subject: =?ISO-2022-JP?B?GyRCRnxLXDhsJE4lNSVWJTglJyUvJUgkSyRKJGokXiQ5ISNEOSQvGyhC?=
=?ISO-2022-JP?B?GyRCJEokayRISjg7ejI9JDEkOSRrJEgkJCQmT0MkYiQiJGokXiQ5GyhC?=
=?ISO-2022-JP?B?GyRCJE4kRyEiRDkkLz1xJCQkRiRfJF4kOSEjJDMkbCQwJGkkJEQ5GyhC?=
=?ISO-2022-JP?B?GyRCJCQkSCRJJCYkKyRKISkbKEI=?=
Message-ID: <e9e0eefd92fd68df3ec3a8bf6133bf9b@example.com>
MIME-Version: 1.0
Date: Thu, 05 Dec 2019 12:36:00 +0900
Content-Type: text/plain; charset=iso-2022-jp
Content-Transfer-Encoding: base64
aHR0cHM6Ly9zeW1mb255LmNvbS93aHktdXNlLWEtZnJhbWV3b3JrCgobJEIlVSVsITwlYCVvITwl
LyRyO0hNUSQ5JGtJLE1XJCwkIiRrJE4kTyRKJDwkRyQ5JCshKRsoQgobJEIlVSVsITwlYCVvITwl
LyRPQGRCUCRLSSxNVyRIJCQkJiRvJDEkRyRPJCIkaiReJDskcyEjJD0kbCRPISIkIiRKJD8kLCRo
JGpOSSQvISIkaCRqQi4kLzMrSC8kOSRrJE4kcj11JDEkayQ/JGEkS014TVEkRyQtJGslRCE8JWsk
TiFWJEEkZyQmJEkhVxsoQjEbJEIkRCRHJDkhKhsoQgoKGyRCJVUlbCE8JWAlbyE8JS8kTyEiJVMl
OCVNJTklayE8JWskSzQwQTQkSz1gNXIkNyEiOT1CJDI9JDUkbCEiJWElcyVGJUolcyU5JEglIiVD
JVclMCVsITwlSSROTj5KfSQsMkRHPSRKJSIlVyVqJTEhPCU3JWclcyRyMytILyQ3JEYkJCRrJEgk
JCQmM048QkAtJHJEczYhJDkkayQ/JGEkRyQ5ISMbKEIKChskQjMrSC88VCQsSEZNUSViJTglZSE8
JWskcjpGTXhNUSQ3JEZCPiROTk4waCRLPThDZiQ5JGskMyRIJEc7fjRWJHJAYUxzJEckLSRrJD8k
以下省略
iso-2022-jpのまとめ
メール本文に7bit
エンコーディングが使えなかった。
base64
でエンコーディングするなら、迷惑メール判定としては、大きく変更と感じる可能性があります。
つまり迷惑メールに初回は入る可能性があります。
それならISO-2022-jpを捨てて、utf-8にしても結局迷惑メールになりますので大差ありません。
UTF-8だと、顔文字など使える文字の種類が増えるメリットがありますから、UTF-8に乗り換えがよさそうです。
個人的なまとめ
symfony/mailerを使う場合は、システム刷新として使うなら、utf-8
の文字コード、base64
エンコーディングを使うのが良さそうです。
新規システムなら、エンコーディングがquoted-printable
になりますが、簡単に使えるのも魅力的だと思います。