LaravelでISO-2022-JPのメールを送りたい

Laravel でメールを送信すると、デフォルトでは文字コードが UTF-8 になります。
そこで、ISO-2022-JP で送信する方法を考えてみました。

# 前提
- Laravel 6.12

# やり方1
まずは、 `Mailable` クラスで頑張るやり方です。

コンストラクタの中に処理を追加します。また、 `withSwiftMessage()` の中でも `setCharset()` と `setEncoder()` をコールしています。

```php:app/Mail/CustomerRegistered.php
namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Swift_DependencyContainer;
use Swift_Mime_ContentEncoder_PlainContentEncoder;
use Swift_Preferences;

class CustomerRegistered extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct()
    {
        /* 処理追加 ここから */
        Swift_DependencyContainer::getInstance()
            ->register('mime.qpheaderencoder')
            ->asAliasOf('mime.base64headerencoder');

        Swift_Preferences::getInstance()->setCharset('iso-2022-jp');
        /* 処理追加 ここまで */
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->from('admin@example.com')
                    ->subject('件名テスト')
                    ->text('mails.customer_registered')
                    ->withSwiftMessage(function ($message) {
                       /* 処理追加 ここから */
                       $message->setCharset('iso-2022-jp');
                       $message->setEncoder(new Swift_Mime_ContentEncoder_PlainContentEncoder('7bit'));
                       /* 処理追加 ここまで */
                    });
    }
```

このやり方の良いところは、メール（ `Mailable` クラス）ごとに文字コードを設定することが明確になることだと思います。[^1]

[^1]: そもそも、システム内で、送信するメールごとに文字コードを分けるようなことが現実にあるのか？とは思いますが...

`Swift_Preferences::getInstance()->setCharset('iso-2022-jp')` をコールすると、Laravel が送信する全ての文字コードが ISO-2022-JP に設定されます。[^2]

[^2]: 一方、 `withSwiftMessage()` の中でコールしている `setCharset()` は、メール本文に文字コードを設定するためのものです。なぜ、こちらの `setCharset()` は各メールごとに設定する前提なのに、`Swift_Preferences::setCharset()` は SwiftMailer 全体に設定する前提なのか。一貫性がなくて気持ち悪い。

そのため、UTF-8 で送信したい場合は、以下のように `setCharset()` に `utf-8` を指定する必要があります。

```php:app/Mail/CustomerChanged.php
namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Swift_Preferences;

class CustomerChanged extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct()
    {
        /* 処理追加 ここから */
        Swift_Preferences::getInstance()->setCharset('utf-8');
        /* 処理追加 ここまで */
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->from('admin@example.com')
            ->subject('件名テスト')
            ->text('mails.customer_changed');
    }
}
```

# やり方2
続いて、サービスプロバイダでがんばるやり方。

[Laravel 5.4でiso-2022-jpエンコードのメールを送信する](https://qiita.com/pekeq/items/784ca0e8c8eb9623d794) に記載されているものを、ほぼ引用させていただきました。

```php:app/Providers/AppServiceProvider.php
namespace App\Providers;

use Illuminate\Support\ServiceProvider;
use Swift;
use Swift_DependencyContainer;
use Swift_Preferences;

class AppServiceProvider extends ServiceProvider
{
    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        /* 処理追加 ここから */
        Swift::init(function () {
            Swift_DependencyContainer::getInstance()
                ->register('mime.qpheaderencoder')
                ->asAliasOf('mime.base64headerencoder');
            Swift_Preferences::getInstance()->setCharset('iso-2022-jp');
        });
        /* 処理追加 ここまで */
    }

    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        //
    }
}
```

```php:app/Mail/CustomerRegistered.php
namespace App\Mail;

use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;
use Swift_Mime_ContentEncoder_PlainContentEncoder;

class CustomerRegistered extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct()
    {
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->from('admin@example.com')
                    ->subject('件名テスト')
                    ->text('mails.customer_registered')
                    ->withSwiftMessage(function ($message) {
                       /* 処理追加 ここから */
                       $message->setCharset('iso-2022-jp');
                       $message->setEncoder(new Swift_Mime_ContentEncoder_PlainContentEncoder('7bit'));
                       /* 処理追加 ここまで */
                    });
    }
```

システム全体でメールを送るときは必ず ISO-2022-JP にするということであれば、このやり方が一番良いように思います。

あえて難点を挙げると、サービスプロバイダで設定するだけでなく、 `Mailable` クラスでも相変わらず設定しなければならないということでしょうか。たかが 2 行と思ったけど、 `setEncoder()` とかコールし忘れそうで...

# やり方3
最後は、SwiftMailer のプラグインでがんばるやり方です。

「もう Laravel じゃないじゃん！」というツッコミもあるかと思いますが、無視します（笑）

まず、以下のようなプラグインを作ります。 `beforeSendPerformed()` はメール送信前に自動的にコールされるようです。

```php:app/Common/JapaneseMailPlugin.php
namespace App\Common;

use Swift_Events_SendListener;
use Swift_Events_SendEvent;
use Swift_Mime_ContentEncoder_PlainContentEncoder;
use Swift_DependencyContainer;
use Swift_Preferences;

class JapaneseMailPlugin implements Swift_Events_SendListener
{
    public function __construct()
    {
        Swift_DependencyContainer::getInstance()
            ->register('mime.qpheaderencoder')
            ->asAliasOf('mime.base64headerencoder');

        Swift_Preferences::getInstance()->setCharset('iso-2022-jp');
    }

    /**
     * Invoked immediately before the Message is sent.
     *
     * @param Swift_Events_SendEvent $evt
     */
    public function beforeSendPerformed(Swift_Events_SendEvent $evt)
    {
        $message = $evt->getMessage();
        $message->setCharset('iso-2022-jp');
        $message->setEncoder(new Swift_Mime_ContentEncoder_PlainContentEncoder('7bit'));
    }

    /**
     * Invoked immediately after the Message is sent.
     *
     * @param Swift_Events_SendEvent $evt
     */
    public function sendPerformed(Swift_Events_SendEvent $evt)
    {
        // 特に何もしない
    }
}
```

一方、 `Mailable` クラスでは、コンストラクタで、プラグインの登録のみを行います。

```php
namespace App\Mail;

use App\Common\JapaneseMailPlugin;
use Illuminate\Bus\Queueable;
use Illuminate\Mail\Mailable;
use Illuminate\Queue\SerializesModels;

class CustomerRegistered extends Mailable
{
    use Queueable, SerializesModels;

    /**
     * Create a new message instance.
     *
     * @return void
     */
    public function __construct()
    {
        /* 処理追加 ここから */
        app()->make('swift.mailer')->registerPlugin(new JapaneseMailPlugin());
        /* 処理追加 ここまで */
    }

    /**
     * Build the message.
     *
     * @return $this
     */
    public function build()
    {
        return $this->from('admin@example.com')
                    ->subject('件名テスト')
                    ->text('mails.customer_registered');
    }
}
```

このやり方の良いところは、 `Mailable` クラスで1行だけ書けば良いということです。[^3] また、上記の例では `withSwiftMessage()` を消してしまいましたが、文字コード以外の設定を行うために `withSwiftMessage()` を記載しても問題ありません。

[^3]: 逆に言えば「1行はまだ書かないといけない」ということでもあります。 SwiftMailer はシングルトン結合されているので、 `registerPlugin()` は1回だけ行えば十分だとは思いますが、思いの外、良い方法が思いつかなかったので、一旦、そのままにしておきます。

難点は、Laravel で処理が完結せず、SwiftMailer まで理解する必要があるということでしょうか。保守性を考えると、あまり良くないかもしれません。

# まとめ的な感想
キレイさでいえば **やり方3** でしょうけど、ちょっとトリッキーかもしれません。
そう考えると、一番理解してもらえそうなのは、 **やり方2** でしょうか。

でも一番は **UTF-8 にすること** だと思いますけど（笑）

# 参考
- Laravel 公式ドキュメント
  - [メール](https://readouble.com/laravel/6.x/ja/mail.html)
- SwiftMailer 公式ドキュメント
  - [Using Swift Mailer for Japanese Emails](https://swiftmailer.symfony.com/docs/japanese.html)
  - [Plugins](https://swiftmailer.symfony.com/docs/plugins.html)
- [Laravelで文字コードISO-2022-JPのメールを送る](https://qiita.com/muramoto/items/ddf136a809af22e0e9d8)
- [[PHP]Swift Mailerで日本語(ISO-2022-JP)のメールを送信 ](https://php-archive.net/php/swift-mailer/)