【Codeigniter】 メールクラス 文字化け

# 紹介

初めまして、日本でPHPエンジニアとして働いてる韓国人です。
よろしくお願いいたします。:relaxed:
恥ずかしながら日本語が未熟なので、ご指摘ほどよろしくお願いいたします。
CodeIgniterのメールクラスを使用する中、
エンコーディングとデコーディングを間違ってないのにも関わらず
文字化けがある場合参考すれば良いと思います。

# 環境情報

この記事はCodeIgniter4基準で作成させていただきました。
ただ、Emailクラスに対してはCodeIgniter3も大きい差はないですので、
ご参考のほどよろしくお願いいたします。

# CodeIgniter メールクラスとは？

メールクラスに詳しいではない方や詳細は下記のリンクをご参考ください。
公式文章
https://codeigniter.com/user_guide/libraries/email.html

CodeigniterにはSMTP設定が用意すれば、メールを簡単に送信できるクラスがあります。
ただ、あくまでも英語を基づいたメールなので日本語を使用する場合、文字化けが起きる場合があります。

# 文字化け原因

```shell
/vendor/codeigniter4/framework/system/Email/Email.php
```
上記のEmailクラスの中、改行を処理してくれる ```wordWrap```関数があります。
（Codeigniter3ではword_wrapです。）

```php
 public function wordWrap($str, $charlim = null)
    {
        if (empty($charlim)) {
            $charlim = empty($this->wrapChars) ? 76 : $this->wrapChars;
        }

        if (strpos($str, "\r") !== false) {
            $str = str_replace(["\r\n", "\r"], "\n", $str);
        }

        $str = preg_replace('| +\n|', "\n", $str);

        $unwrap = [];

        if (preg_match_all('|\{unwrap\}(.+?)\{/unwrap\}|s', $str, $matches)) {
            for ($i = 0, $c = count($matches[0]); $i < $c; $i++) {
                $unwrap[] = $matches[1][$i];
                $str      = str_replace($matches[0][$i], '{{unwrapped' . $i . '}}', $str);
            }
        }

        // Use PHP's native function to do the initial wordwrap.
        // We set the cut flag to FALSE so that any individual words that are
        // too long get left alone. In the next step we'll deal with them.
        $str = wordwrap($str, $charlim, "\n", false);

        // Split the string into individual lines of text and cycle through them
        $output = '';

        foreach (explode("\n", $str) as $line) {
            if (static::strlen($line) <= $charlim) {
                $output .= $line . $this->newline;

                continue;
            }

            $temp = '';

            do {
                if (preg_match('!\[url.+\]|://|www\.!', $line)) {
                    break;
                }

                $temp .= static::substr($line, 0, $charlim - 1);
                $line = static::substr($line, $charlim - 1);
            } while (static::strlen($line) > $charlim);

            if ($temp !== '') {
                $output .= $temp . $this->newline;
            }

            $output .= $line . $this->newline;
        }

        if ($unwrap) {
            foreach ($unwrap as $key => $val) {
                $output = str_replace('{{unwrapped' . $key . '}}', $val, $output);
            }
        }

        return $output;
    }
```
この関数はご覧の通り76byteになると自動で改行処理する関数です。
文字が1byte単位なら問題ないですが、日本語は2byteですので文字化けが出ます。

それで下記のようにコードをコメント処理していただくと解決できます。

```php
 public function wordWrap($str, $charlim = null)
    {
        if (empty($charlim)) {
            $charlim = empty($this->wrapChars) ? 76 : $this->wrapChars;
        }

        if (strpos($str, "\r") !== false) {
            $str = str_replace(["\r\n", "\r"], "\n", $str);
        }

        $str = preg_replace('| +\n|', "\n", $str);

        $unwrap = [];

        if (preg_match_all('|\{unwrap\}(.+?)\{/unwrap\}|s', $str, $matches)) {
            for ($i = 0, $c = count($matches[0]); $i < $c; $i++) {
                $unwrap[] = $matches[1][$i];
                $str      = str_replace($matches[0][$i], '{{unwrapped' . $i . '}}', $str);
            }
        }

        // Use PHP's native function to do the initial wordwrap.
        // We set the cut flag to FALSE so that any individual words that are
        // too long get left alone. In the next step we'll deal with them.

        // $str = wordwrap($str, $charlim, "\n", false);

        // Split the string into individual lines of text and cycle through them
        $output = '';

        foreach (explode("\n", $str) as $line) {
            if (static::strlen($line) <= $charlim) {
                $output .= $line . $this->newline;

                continue;
            }

            // $temp = '';

            // do {
            //     if (preg_match('!\[url.+\]|://|www\.!', $line)) {
            //         break;
            //     }

            //     $temp .= static::substr($line, 0, $charlim - 1);
            //     $line = static::substr($line, $charlim - 1);
            // } while (static::strlen($line) > $charlim);

            // if ($temp !== '') {
                // $output .= $temp . $this->newline;
            // }

            $output .= $line . $this->newline;
        }

        if ($unwrap) {
            foreach ($unwrap as $key => $val) {
                $output = str_replace('{{unwrapped' . $key . '}}', $val, $output);
            }
        }

        return $output;
    }
```

ただ、今後のためにvendor中のコードを修正するよりは
Emailクラスを継承して子供クラスを作成してwordWrap関数をオーバーライドした方が良いと思います。

```php

<?php

/**
 * 文字化け防止のメールクラス
 *
 */

namespace App\Libraries;

class EmailJP extends \CodeIgniter\Email\Email
{
    public function __construct($config)
    {
        parent::__construct($config);
    }

    /**
     * 
     * 文字化け関数 override
     * 
     *  */ 
    public function wordWrap($str, $charlim = null)
    {
        if (empty($charlim)) {
            $charlim = empty($this->wrapChars) ? 76 : $this->wrapChars;
        }

        if (strpos($str, "\r") !== false) {
            $str = str_replace(["\r\n", "\r"], "\n", $str);
        }

        $str = preg_replace('| +\n|', "\n", $str);

        $unwrap = [];

        if (preg_match_all('|\{unwrap\}(.+?)\{/unwrap\}|s', $str, $matches)) {
            for ($i = 0, $c = count($matches[0]); $i < $c; $i++) {
                $unwrap[] = $matches[1][$i];
                $str      = str_replace($matches[0][$i], '{{unwrapped' . $i . '}}', $str);
            }
        }

        $output = '';

        foreach (explode("\n", $str) as $line) {
            if (static::strlen($line) <= $charlim) {
                $output .= $line . $this->newline;

                continue;
            }

            $output .= $line . $this->newline;
        }

        if ($unwrap) {
            foreach ($unwrap as $key => $val) {
                $output = str_replace('{{unwrapped' . $key . '}}', $val, $output);
            }
        }

        return $output;
    }
}

```

自分はappのLibrariesへ作成させていただきました。
ただ、親クラスとは違くserviceへ登録してるわけではないですので、
constructの引数へ ```config/email.php```の値を渡さなきゃメール送信はできません。
(config/email.phpのはconfigサービスから得られます。)

使用例
```php
<?php

namespace App\Libraries\EmailJP;

class ExampleController extends BaseController
{
    ...
    public function sendMail($body)
    {
        $email = new EmailJP(config('Email'));

        // $email = \Config\Services::email();

        $email->setFrom('mailto:your@example.com', 'Your Name');
        $email->setTo('mailto:someone@example.com');
        $email->setCC('mailto:another@another-example.com');
        $email->setBCC('mailto:them@their-example.com');
        $email->setSubject('Email Test');
        $email->setMessage($body);
        $email->send();
    }
   ...
}

```

# 結論

上の問題を解決するために幾つか方法を考えてみました。
１。メール内容のタイプをHTMLへ変更、改行を```'\n'```を```<br>```に変更する。
- 受信側でHTMLメールが困る場合がありますので、やめました。

２。改行アルゴリズムを修正する。
- byte単位ではなく、文字単位で改行する(substr()→mb_substr())とか試してみましたが、
結局ユーザーが改行した本文、そのまま送信するのが一番だと結論を出しました。

色々足りない記事ですので、ご指摘ほどよろしくお願いいたします。

よろしくお願いいたします。
ありがとうございました。