送信メールの自動改行

送信メールの自動改行

- Tatsuya Shirai の投稿
返信数: 28

 最初に気になったのはフォーラム投稿の要約メールの折り返しでした.


LAB08 -> フォーラム -> 物品発注に関するフォーラム ->
SNCTエクスプローラ(シャフト)

Re: SNCTエクスプローラ(シャフト) 2008年 12月
9日(火曜日) 13:41 - 白井 達也 の投稿
---------------------------------------------------------------------
残念でした 2008年 12月 9日(火曜日) 15:50 -
白井 達也 の投稿
---------------------------------------------------------------------
Re: 残念でした 2008年 12月 9日(火曜日) 16:13 -
白井 達也 の投稿
---------------------------------------------------------------------
 
=====================================================================


 なんとなく折り返しが不自然です.投稿者の名前が行頭に来るように折り返しているわけではなく,日付の途中で折り返されているものもあります.
 しかし調べた限りでは要約メールを生成する箇所では自動折り返しを行なっていません.そこで単純にメッセージ機能で実験をして見ました.送信したのは以下の3行.


1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


 半角空白で区切ったa)半角数字,b)全角数字,c)半角空白で区切らない半角マイナスの記号の列.受け取ったメールでは,


1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
1 2 3 4 5 6 7 8
9 0 1 2 3 4 5 6
7 8 9 0 1 2 3 4
5 6 7 8 9 0 1 2
3 4 5 6 7 8 9 0
1 2 3 4 5 6 7 8
9 0
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------


 このようにa)半角では40ワード,b) 全角では8ワードで自動改行されています.c) 区切りが無い場合は自動改行されません.とてもバランスが悪い.総文字数でも無いし,ワード数でも無いし,その組み合わせとしても奇妙です.文字コードはISO-2022-JPです.

 ユーザにメールを送信するfunction email_to_user()関数(lib/moodlelib.php)では特に改行処理は見当たらず,引数で受け取ったUTF-8のメール本文を

            $mail->Body = $textlib->convert($mail->Body, 'utf-8', $mail->CharSet);         //Body

設定された電子メールの文字コードに変換して,

$mail->Send();

しているだけのように見えます.Send()の中で行なわれている処理なのでしょうか?

Tatsuya Shirai への返信

Re: 送信メールの自動改行

- Tatsuya Shirai の投稿

 当方の環境ですと,smtpを用いていますので,該当するのは辿りに辿って,

lib/phpmailer/class.smtp.phpのfunction Data()と思われます.

 関係しそうなのは2点.

 まずは改行コードの置換.

        $msg_data = str_replace("\r\n","\n",$msg_data);
        $msg_data = str_replace("\r","\n",$msg_data);
        $lines = explode("\n",$msg_data);

ISO-2022-JPの符号化法が分からないのですが,マルチバイトの場合に'\r'を含むということは無いですよね,さすがに.それに先に示した例では同じ文字の繰り返しですので,1個目に反応しないで2個目で改行,というのも妙です.

 もう一点は,

         $max_line_length = 998; # used below; set here for ease in change

この辺りに関わりがありそうです.この下に長々と,

            # ok we need to break this line up into several
            # smaller lines

という処理が続きます.この辺りでしょうか.

Tatsuya Shirai への返信

Re: 送信メールの自動改行

- Tatsuya Shirai の投稿

 送信するデータ($line: ISO-2022-JP)に手を加えて,

$line = strlen($line).':'.$line;

このようにしてみたところ,

79: 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
40: 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
71: 1 2 3 4 5 6 7 8
71: 9 0 1 2 3 4 5 6
71: 7 8 9 0 1 2 3 4
71: 5 6 7 8 9 0 1 2
71: 3 4 5 6 7 8 9 0
71: 1 2 3 4 5 6 7 8
18: 9 0

となりました.ISO-2022-JPは予想以上に多くのバイト数を要するようですね.最後の行は全角2文字と半角空白2文字(と改行文字?)なのに18byte.漢字イン,漢字アウトのようなエスケープ文字を入れているのか? だとしても全角1文字で4byte程度かと思うので,合計で11byte程度かと思うのですが.全角1文字と半角1文字の組み合わせが9byteであるならば,71byteの行は+9byteで80byteになるから改行しよう,というロジックが読み取れます.その割には,半角マイナス記号の羅列は201文字(イイカゲンな長さです)でも改行なし.単語の途中では折り返さない禁則処理の類でしょうか.

 ちなみにこの文字数(バイト数)のチェックは$max_line_lengthのチェックを行なう前の段階で調べましたので,自動改行処理はlib/phpmailer/class.smtp.phpのfunction Data()よりも前の段階で行なわれている可能性が出てきました.やはりMoodleかな?

Tatsuya Shirai への返信

場所は確定しました

- Tatsuya Shirai の投稿

 送信メールの自動改行を行なっている箇所は,lib/phpmailer/class.phpmailer.phpのfunction WrapText()でした.

 この関数をバイパスされたら折り返し(ワードラップ)されないことを確認しました.

 さて,今後はどうしましょう.日本語にも対応できるように改善するかどうか,ですね.

Tatsuya Shirai への返信

Re: 送信メールの自動改行

- Tatsuya Shirai の投稿

http://www.cam.hi-ho.ne.jp/mendoxi/rfc/rfc1554j.html

 文字集合を切り替えるエスケープ・シーケンスは3byteのようです.したがって,(日本語文字を表すエスケープ・シーケンス3byte)+’9’(2byte)+(ASCII文字を表すエスケープ・シーケンス3byte)+' '(1byte)=9byte ということで正しいようです.

Tatsuya Shirai への返信

Re: 送信メールの自動改行

- Tatsuya Shirai の投稿

 何文字でワードラップを行なうのか,をどこで指示しているのか.これは,電子メールを送信するための関数であるfunction email_to_user()(lib/moodlelib.php)の宣言にありました.

function email_to_user($user, $from, $subject, $messagetext, $messagehtml='', $attachment='', $attachname='', $usetrueaddress=true, $replyto='', $replytoname='', $wordwrapwidth=79) {

 この引数が,そのまま特に細工をされることなく,

    $mail->WordWrap = $wordwrapwidth;                   // set word wrap

このようにclass PHPMailerのインスタンスである$mailに代入されています.email_to_user()を利用している箇所を全ソースでgrepして調べて見ました.最大の11個目の引数まで設定してemail_to_user()を呼び出している箇所は一つもありませんので,全ての電子メールに送信で79文字を境にワードラップ処理を行っていることになります.

 問題は,このワードラップ処理というのが,”一行の最大文字数”を指定しているものでは無いことです.1行が半角空白で区切られた2単語以上で構成される場合に,79文字を超える位置に存在する単語を次の行へ送り込む処理です.つまり半角空白文字で区切られていない100文字長の単語で構成される行の場合は100文字のままです.分かち書きを行なわないで書かれた全角日本語の文章の場合はこれに相当します.
 ちなみに,では1行が1万文字でも良いのか? これはもっと下位レベルの(たとえばlib/phpmailer/class.smtp.phpの)function Data()で,$max_line_length = 998と設定して,
            while(strlen($line) > $max_line_length) {
                $pos = strrpos(substr($line,0,$max_line_length)," ");

                # Patch to fix DOS attack
                if(!$pos) {
                    $pos = $max_line_length - 1;
                }

                $lines_out[] = substr($line,0,$pos);
                $line = substr($line,$pos + 1);
おおよそこのような処理で,ワードラップ風に空白で切れるならば切る(既にワードラップで79文字程度に切ることが試みられた前提でしょう)し,ダメならば強制的にブッた切ります.安全策ですね.

 もう一つの問題点は,上では79文字と書きましたが,WrapText()関数を開発した人(コメントによるとphilippe氏)はそう期待していたのかも知れませんが,毎度毎度の話ですが,実際には79byteです.ISO-2022-JPですと,UTF-8よりも影響が大きいのは先の例に示した通りです.

 function WrapText()に手を加えて見ますか.たとえば一旦,UTF-8に戻した上でワードラップを行い,処理結果を再びシステムで設定したEメール用の文字コードの符号化(ISO-2022-JP)などへ戻す.UTF-8ならば日本語は3byteですので(本当は全角はASCII文字二文字分の幅なので2byteならば最高なのですが),いまよりはマシになりそうです.あるいはISO-2022-JPに特化した改造を行なって,ワードラップ時にエスケープシーケンスの文字列をカウントしないようにするか,です.

Tatsuya Shirai への返信

改善法方針

- Tatsuya Shirai の投稿

 やはり一旦,UTF-8に戻して処理をする方が楽そうです.

 というのは,ISO-2022-JPなどのエスケープ・シーケンスを用いて文字集合を切り替えるタイプの文字コード符号化法ですと,半角空白文字によるワードラップ処理でエスケープ・シーケンスが次の行に飛ばされるといった問題(支障は無い?)があるからです.それと非アスキー文字は1文字で2文字扱いとする,といった例外的なカウント法(strlen()に対応する関数を作成するなど)を取る際に,”いまASCII,それとも非ASCII?”の判断が難しくなります.

Tatsuya Shirai への返信

ステップ1(UTF-8に一旦戻す)

- Tatsuya Shirai の投稿

 function WrapText()に入ったところで,送信メール全文をISO-2022-JPからUTF-8に一旦戻して,オリジナルのワードラップ処理を行い,関数を抜ける直前でISO-2022-JPに戻したところ,これだけでも症状は少し改善しました.


1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
1 2 3 4 5 6 7 8 9 0


 それでも全角の方が短いのは,全角文字はUTF-8で3byteであるせいです.
 また,これで「まぁいいじゃん」と言うのはキケンで,実はオリジナルよりも悪い状態になっている可能性があります.上記例はASCII文字集合と非ASCII文字集合の切り替えが極端に頻繁に発生する例ですので,見た目の文字数よりもデータのバイト数が多いのですが,あまり文字コードの切り替わりが無い文字列の場合,UTF-8に変換するよりも,日本語文字1文字を2byteで表すISO-2022-JPのままで処理を行った方が正しい出力に近い出力を得られます.

 次のステップは,strlen()に相当するUTF-8専用の関数を作り,ASCII文字は1byte,非ASCII文字は2byte相当であると文字数をカウントする.

Tatsuya Shirai への返信

ステップ2(非ASCII文字は2byteとカウントする)

- Tatsuya Shirai の投稿

 考えてみたら,何も新しい関数を作成しないでも,mb_strwidth()を使用すれば良かったのですね.これで解決です.以下,実行結果です.


1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0
1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6
7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0


 Webブラウザ上で見ると等幅フォントではないのでおかしく見えますが,メールクライアント上で見るとピッタリです.たとえば赤い文字の行で数えると,全角26文字+半角26文字ですので,合わせて半角78文字相当.これにあと全角文字1文字(’7’)を追加しようとすると半角80文字相当で,ワードラップの制限値79byteを越えますので,OKですね.

Tatsuya Shirai への返信

Re: ステップ2(非ASCII文字は2byteとカウントする)

- Tatsuya Shirai の投稿

 修正箇所を示します.意外と小規模です.

 lib/phpmailer/class.phpmailer.php
 function WrapText()

(a) 冒頭

    function WrapText($message, $length, $qp_mode = false) {
// (Shirai104): ここから追加
        global $CFG;
        $message = mb_convert_encoding($message, 'utf-8', $CFG->sitemailcharset);
// (Shirai104): ここまで追加
        $soft_break = ($qp_mode) ? sprintf(" =%s", $this->LE) : $this->LE;

        $message = $this->FixEOL($message);
        if (substr($message, -1) == $this->LE)
            $message = substr($message, 0, -1);

(b) 後半

              }
              else
              {
                $buf_o = $buf;
                $buf .= ($e == 0) ? $word : (" " . $word);

// (Shirai104): ここからコメントアウト
//
              if (strlen($buf) > $length and $buf_o != "")
// (Shirai104): ここから追加
                if (mb_strwidth($buf, 'utf-8') > $length and $buf_o != "")
// (Shirai104): ここまで追加
                {
                    $message .= $buf_o . $soft_break;
                    $buf = $word;
                }
              }
          }
          $message .= $buf . $this->LE;
        }

// (Shirai104): ここから追加
        $message = mb_convert_encoding($message, $CFG->sitemailcharset, 'utf-8');
// (Shirai104): ここまで追加
        return $message;
    }

以上です.

 なお,function WrapText($message, $length, $qp_mode = false)の3個目の引数,$qp_modeが分かりません.どうやらMoodleでは$qp_modeを指定していないようですので,$qp_modeがtrueの時の処理にはmb_strwidth()を用いません.

 次のfs_moodle3.08.00にはこの機能を組み込みます.fsconfig.phpでこの機能を無効化するスイッチも設けます.
 

Tatsuya Shirai への返信

Re: ステップ2(非ASCII文字は2byteとカウントする)

- Tatsuya Shirai の投稿

Trackerに報告しました.

MDL-17587

#相変わらずムチャクチャな英語で恥ずかしいです^^;

Tatsuya Shirai への返信

Re: ステップ2(非ASCII文字は2byteとカウントする)

- Tatsuya Shirai の投稿

 動作確認を行なったところ,少し不安が出てきました.

 今朝,要約メールが無事に届くのを見届けてチェック完了とするつもりでしたが,要約メールが届きませんでした.Moodleのログを見てみたところ,"ERROR: The following From address failed: shirai@mech.suzuka-ct.ac.jp"というエラーがPHPmailerで発生し,その結果,"forum mail digest error"となっていました.From address failedが発生するのは,SMTPサーバとの接続ミスか,<MAIL FROM...>をSMTPサーバに送信した場合の返答が正しくなかった(リターンコード250以外)場合の二つです.これが本校のSMTPサーバの偶発的なエラーなのか,今回の改良が悪影響を与えたのか分かりません.

 要約メール送信時刻を10時に設定してリトライしたのですが,こちらは問題なく要約メールが送信されました.いまのところ確率1/2.

 現在,調査中です.結果が分かり次第,報告します.

Tatsuya Shirai への返信

Re: ステップ2(非ASCII文字は2byteとカウントする)

- Tatsuya Shirai の投稿

 SMTPサーバの管理者の方にログを調べて貰ったのですが,アクセス拒否の記録すら残っていないそうです.SMTPサーバとの接続に失敗したという線が強いですが,だとして,ではなぜ接続に失敗したのか,を知ることはできないでしょう.

 現在,Moodleのfunction email_to_user()関数ではPHPmailerが出力するエラーメッセージを破棄して独自のエラーメッセージのみを出力しています.そこで,PHPmailerのエラーメッセージを取得し,かつemail_to_user()関数をコールされるたびに詳細なログを残す機能を試験的に組み込んで,しばらく様子を見てみます.

Tatsuya Shirai への返信

Re: 送信メールの自動改行

- Haruhiko Okumura の投稿
すいません,大幅に出遅れました。^^;

三重大学版はどうしているかというと,居直って,lib/moodlelib.php で

function email_to_user($user, $from, $subject, $messagetext, $messagehtml='', $attachment='', $attachname='', $usetrueaddress=true, $replyto='', $replytoname='', $wordwrapwidth=998) { // okumura: $wordwrapwidth=79

としてしまっています。つまり物理的な上限までワードラップをしない方針にしています。意図しないところで勝手に改行を挿入するよりも,書き手に任せたほうがいいだろうということです。今のメールクライアントは昔と違って,行が長くておかしくなるということはありえないですし。これに対して,998バイトというのは厳密に守らないと,SMTPサーバが勝手にマルチバイトの途中に改行を入れてしまいます。もっとも,998バイト以上になることはほとんどないと思います。
Haruhiko Okumura への返信

Re: 送信メールの自動改行

- Tatsuya Shirai の投稿

 さすがですね! この問題に気付いていましたか.私は2年近く気付いていませんでした^^;

 ちなみにPHPmailerはマルチバイトの途中だろうが998バイトになったら改行を入れます(笑).私もさすがにこちらは手を入れていません.

でも,さすがに80バイト相当はいまの世の中,短すぎますよねぇ.

Tatsuya Shirai への返信

Re: 送信メールの自動改行

- Tatsuya Shirai の投稿

 それとワードラップの問題点は,

(a) あわあわあわ...あわわ :合計1,000文字強

が,

(a)            ←ワードラップで改行
あわあわあわ...あ ←998文字の制限で改行
わわ           ←その残り

のように”半角空白で区切られていなければどんなに長くても一つの単語”と扱う点にもありますね.どうしてもワードラップしたいのならば,一つの単語の最大長を制限して,ブッツリと切るべきでしょうね.
http://detail.chiebukuro.yahoo.co.jp/qa/question_detail/q1113648291


ちなみに,上記ページを探している途中で見たページにあった,

Q 「最も長い英単語はなんですか?」
A 「longest」

には,

Q 「レーザーの”略”は何ですか?」
A 「レ」

なみに笑わせて貰いました満面の笑顔

Haruhiko Okumura への返信

Wordの文章等をCopy&Pasteすると...

- Tatsuya Shirai の投稿

 $wordwrapwidthを変更できるように改造してみました.最初はユーザごとに変更可能としようか,とも考えたのですが,メール送信のたびにユーザプロファイルフィールドをデータベースに読み出しにいかなくてはならないので煩雑です.ですのでサイト単位での設定としました.

 ところで998byteの制限ですが,意外と簡単に露見しました.Word等の日本語の文章をコピー&ペーストしてメール本文として送る場合です.手入力でしたらほどほどのところで改行してくれると思いますが,論文の文章など改行がほとんど無い場合,一行(句点ではなく改行文字まで)が簡単に998byteに達します.手入力だとしても私の場合,あまり改行を入れずにダラダラと長文を書きますので,もしかしたら998byteを越えるかもしれません.試してみたところ,予想通り,998byteで強制改行されるため,ISO-2022-JPは特にまずいのですが,文字化けしました.改行位置周辺だけではなく,そこから後,次の改行位置まで全て化けますね...

・・・する際に必要な力が少なくて済むことも実験により検証した.この機G
 =$rBN0L
Q49Jd=u5!G=$H8F$V!%!!FC$K%Y%C%II=LL$NFCDj$NNN0h$N9E$5$r%3%s%H%m!<%k2DG=$G$"$kE@$O=EMW$G$"$k!%$?$H$($P1&H>?H$NNN0h$r9E$/!$:8H>?H$NNN0h$r=@$i$+$/$9$k$3$H$G%Y%C%II=LL$K79<P$r@8$8$k!%$3$N%Y%C%II=LL$N79<P$rMxMQ$7$F!$%Y%C%I>e$K="?2$7$F$$$k>c32<T$r2p8n<T$,BN0LJQ49!J?2JV$j!K$9$k:]$KI,MW$JNO$,>/$J$/$F:Q$`$3$H$b<B83$K$h$j8!>Z$7$?!%$3$N5!G=

ワードラップよりも,実はこちらの方が(色々な使い方をユーザはすると思いますので)対策が必要かも知れません.


SMTPを利用する場合は,lib/phpmailer/class.smtp.php中のfunction Data()中で998byteでの強制改行を行なっています.この箇所でマルチバイト対応の正しい強制改行処理を行うと,SMTP以外の手段でメール送信するサイトの場合には意味がないので,やはりlib/moodlelib.phpのfunction email_to_user()の中で行なうべきでしょうか.

Tatsuya Shirai への返信

Re: Wordの文章等をCopy&Pasteすると...

- Tatsuya Shirai の投稿

 smtpで電子メールを送信する場合,先に述べたように,lib/phpmailer/class.smtp.phpのfunction Data()中で998byteを越える行を強制改行しているように見えます.

 その処理の始めに,$msg_data(多分,送信用文字コードに変換済み)を行単位に分割している処理があります.

        # normalize the line breaks so we know the explode works
        $msg_data = str_replace("\r\n","\n",$msg_data);
        $msg_data = str_replace("\r","\n",$msg_data);
        $lines = explode("\n",$msg_data);

これがUTF-8の状態で行なわれる処理ならば気にならないのですが,ISO-2022-JP等にエンコード後でも問題ないのでしょうか? \rと\nはマルチバイトの2バイト目に現れない,のであれば私の杞憂.


手元の日本語文章で試した感じでは大丈夫そうです.UTF-8からISO-2022-JPに変換してから上記処理を行い,implode("\r\n", $msg_data)で連結してからUTF-8に戻しても文字化け無しでした.

Tatsuya Shirai への返信

Re: Wordの文章等をCopy&Pasteすると...

- Haruhiko Okumura の投稿
> \rと\nはマルチバイトの2バイト目に現れない

はい,現れません。
Haruhiko Okumura への返信

Re: Wordの文章等をCopy&Pasteすると...

- Tatsuya Shirai の投稿

 ありがとうございます.少し安心しました.

 では,function email_to_user()でコードを変換する前に,メール送信用文字コードに変換後に998byteを越えないように強制改行する自前の関数で前処理を行なえばこの問題は回避できそうですね.

Tatsuya Shirai への返信

Re: Wordの文章等をCopy&Pasteすると...

- Tatsuya Shirai の投稿

 実験用に関数を作ってみました.うん,パフォーマンスに不安がありますが一応,動作します.

 autocut998()とautocut998_sub()の2つで,autocut998_sub()は再起呼び出しされます.

// $line : UTF-8
function autocut998_sub($line, $max_line_length)
{
    if (($linelength = mb_strwidth($line)) == 0) return '';
    for ($offset = 1; $offset <= $linelength; $offset++) {
        if (strlen(mb_convert_encoding(mb_substr($line, 0, $offset+1), 'ISO-2022-JP', 'UTF-8')) >= $max_line_length) {
            $line = mb_substr($line, 0, $offset)."\n".autocut998_sub(mb_substr($line, $offset), $max_line_length);
            break;
        }
    }
    return $line;
}

// $msg_data : UTF-8
function autocut998($msg_data)
{
    $max_line_length = 998; # used below; set here for ease in change
    $max_line_length -= 8;  // 自信が無いので安全のために

    # normalize the line breaks so we know the explode works
    $msg_data = str_replace("\r\n","\n",$msg_data);
    $msg_data = str_replace("\r","\n",$msg_data);
    $lines = explode("\n",$msg_data);
    foreach ($lines as $pos=>$line) {
        if (strlen(mb_convert_encoding($line, 'ISO-2022-JP', 'UTF-8')) > $max_line_length) {
            $lines[$pos] = autocut998_sub($line, $max_line_length);
        }
    }
    $msg_data = implode("\n", $lines);
    return $msg_data;
}


 動作確認用のソースファイル(上記関数と,サンプルの文字列と表示確認)を添付します.実行しますと,分割後の文字列が水平線で分割されて表示されます.改行文字は"\n"に正規化されています.行頭のカッコ内の数字はISO-2022-JPに変換した際のバイト数です.998byteから8byte(小心者なので)を引いた990byteを制限としてます.
 メール送信の文字コードはISO-2022-JPに決め打ちになっています.サイト標準文字コードは$CFG->sitemailcharsetですが,ユーザ単位でも設定可能ですね.$mail->CharSetに最終的には代入されているようですので,これを利用すれば良さそうです.

Tatsuya Shirai への返信

Re: Wordの文章等をCopy&Pasteすると...

- Haruhiko Okumura の投稿
あ,こういうことですか。

すごい,再帰呼び出しだ!
Haruhiko Okumura への返信

Re: Wordの文章等をCopy&Pasteすると...

- Tatsuya Shirai の投稿

 function autocut998()単体で済めば見た目はカッコいいのですが,改行コードの正規化処理は1回だけ行えば良いですし,既に1行に分解されているのにexplode()するのも無駄かな?と思ってfunction autocut998_sub()に1つの行を複数行に分割する処理を任せました.再起呼び出しの例題としては最適ですね^^.

 境目を探すのに長さ1文字から「変換しても大丈夫?」をキチキチと繰り返すのはかなり時間の無駄なのですが,ここは安全性重視です.OKな文字を一文字ずつバッファに追加していくと速いかな?と思ったのですが,ISO-2022-JPの場合は文字集合の変更のためのエスケープシーケンスが曲者ですので,対象となる文字列全体をエンコードし直さないといけないですね.

Tatsuya Shirai への返信

Re: Wordの文章等をCopy&Pasteすると...

- Haruhiko Okumura の投稿
えーと,よく理解していないのですが,ISO-2022-JPに直した後でということですか?

うまく文字の境界を見つけることと,改行の前にいったんASCIIの状態に戻してから改行しないといけないことに注意すればいいと思います。

世の中のメールソフトがみんなUTF-8対応になれば楽なのですが,さらにBase64エンコードされれば行の長さの制限も気にすることはなくなります。まだまだ先の話でしょうけれど。
Haruhiko Okumura への返信

Re: Wordの文章等をCopy&Pasteすると...

- Tatsuya Shirai の投稿

 いえいえ,これはMoodle限定で,lib/moodlelib.phpのfunction email_to_user()内において行なう処理と考えています.

 $textmessageはテキスト形式のメールであれば$mail->Bodyに代入されます.
 その後に,ReplyTO, Subject, to と共に$mail->Bodyと$mail->AltBody(HTMLメール用)がユーザの選んだメール送信用の文字コードに変換されます.この変換前に強制改行を行なってしまおう,という考えです.したがいまして,処理対象のメール本文はUTF-8の状態です.

 その後,$mail->Send()でメールを送信する処理に入ります.この先はメール送信手段によって呼ばれる関数が違うのですが,それぞれの関数の末端で行を送信するギリギリの直前で998byteを越えていないかチェックを行ない,越えているならば強制改行を行ないます.でも,既にUTF-8の状態でメール送信用の文字コードに変換しても998byteを越えないように処理してあるからヘッチャラ!というわけです.

Tatsuya Shirai への返信

Re: Wordの文章等をCopy&Pasteすると...

- Tatsuya Shirai の投稿

 Moodleに組み込んで見ました.

 先ほどの実験用のプログラムのエンコード部分を引数渡しに(最初からしておけば良かった)しただけです.

// 一行が998byteを超えるマルチバイト文字列を含む文章をメール送信すると強制改行により文字化けする問題
// 再帰呼び出しのサブルーチン: function autocut998()より呼び出される
// $line : UTF-8
// $encode   : メール送信時に文字コード ex) 'ISO-2022-JP'
function autocut998_sub($line, $max_line_length, $encode)
{
    if (($linelength = mb_strwidth($line)) == 0) return '';
    for ($offset = 1; $offset <= $linelength; $offset++) {
        if (strlen(mb_convert_encoding(mb_substr($line, 0, $offset+1), $encode, 'UTF-8')) >= $max_line_length) {
            $line = mb_substr($line, 0, $offset)."\n".autocut998_sub(mb_substr($line, $offset), $max_line_length, $encode);
            break;
        }
    }
    return $line;
}

// $msg_data : UTF-8
// $encode   : メール送信時に文字コード ex) 'ISO-2022-JP'
function autocut998($msg_data, $encode)
{
    $max_line_length = 998; # used below; set here for ease in change
    $max_line_length -= 8;  // 自信が無いので安全のために

    # normalize the line breaks so we know the explode works
    $msg_data = str_replace("\r\n","\n",$msg_data);
    $msg_data = str_replace("\r","\n",$msg_data);
    $lines = explode("\n",$msg_data);
    foreach ($lines as $pos=>$line) {
        if (strlen(mb_convert_encoding($line, $encode, 'UTF-8')) > $max_line_length) {
            $lines[$pos] = autocut998_sub($line, $max_line_length, $encode);
        }
    }
    $msg_data = implode("\n", $lines);
    return $msg_data;
}

これをたとえばlib/moodlelib.phpに追加し,lib/moodlelib.phpのfunction email_to_user()の以下の箇所を修正すれば,一行が猛烈に長い日本語文章をメールで送っても文字化けしませんでした.

        $charsets = get_list_of_charsets();
        unset($charsets['UTF-8']);
        if (in_array($charset, $charsets)) {
        /// Save the new mail charset
            $mail->CharSet = $charset;
        /// And convert some strings
            $mail->FromName = $textlib->convert($mail->FromName, 'utf-8', $mail->CharSet); //From Name
            foreach ($mail->ReplyTo as $key => $rt) {                                      //ReplyTo Names
                $mail->ReplyTo[$key][1] = $textlib->convert($rt[1], 'utf-8', $mail->CharSet);
            }
            $mail->Subject = $textlib->convert($mail->Subject, 'utf-8', $mail->CharSet);   //Subject
            foreach ($mail->to as $key => $to) {
                $mail->to[$key][1] = $textlib->convert($to[1], 'utf-8', $mail->CharSet);
            }
// ここから追加
            $mail->Body    = autocut998($mail->Body,    $mail->CharSet);
            $mail->AltBody = autocut998($mail->AltBody, $mail->CharSet);
// ここまで追加
            $mail->Body = $textlib->convert($mail->Body, 'utf-8', $mail->CharSet);         //Body
            $mail->AltBody = $textlib->convert($mail->AltBody, 'utf-8', $mail->CharSet);   //Subject
        }
    }
// ここから追加
    if (empty($charset) || (strtoupper($charset) === 'UTF-8')) {
        $mail->Body    = autocut998($mail->Body,    'UTF-8');
        $mail->AltBody = autocut998($mail->AltBody, 'UTF-8');
    }
// ここまで追加

    if ($mail->Send()) {

 上の赤いブロックは確認済みですが,”メール送信用の文字コードを指定していない(生で送信,つまりUTF-8)”かつ”ユーザに選択を許していない”場合と,”ユーザに選択を許している”かつ”ユーザがUTF-8を選択してる”場合は上のブロックは通りませんので,下の赤いブロックが必要なはずです.UTF-8の場合も改行コードを挿入されて途中で強制的に切られると文字化けするでしょう.autocut998()の内部ではUTF-8からUTF-8に変換する不毛な処理を行うことになりますが,場合分けすると余計に手間が増えそうなので特別な処理を行いません.

 なお,2箇所の[1]は,Trackerにも報告済みですがMoodleのバグです.もし欠けている場合は追加して下さい.

 

Tatsuya Shirai への返信

Re: Wordの文章等をCopy&Pasteすると...

- Tatsuya Shirai の投稿

 これでかなり「ヨッシャー!」なのですが,実はまだ気になるところがあります.

 Moodleのメッセージ機能を使ってメールを送ると,Subjectが物凄く長くなることがあります.

 普段はQuickMailを使用しているのですが,QuickMailブロックを追加していないコースに属する学生にメールを送るには,Moodleの”メッセージ”の機能(ディスカッション)を使っています.もし相手の学生がログオンしているならばチャット風にメッセージをやりとりできるのですが,不在の場合は電子メールで送信されます.

 この場合,画面下の入力ボックスに入力した”なんとなく1行目くらい”がメールのSubjectになります.そのことを知らなかった頃は,入力ボックスの1行目から長い文章を書いてバンバンとメールを送っていました.そして学生から「先生のメール,SPAMだと認識されてゴミ箱に入っていました」と言われました^^;.Subjectが長すぎたからなのか,不適切な用語が含まれていたからなのかは不明ですが...

 先ほど,1行目は短いタイトル風の文,2行目から猛烈に長い文章をCopy&Pasteしてメール送信した所,2行目の長い文章もSubjectに含まれて送信されました.1行目だけでいいのに.これもちょっとソースを見てみますか.


以下,サンプルです.1行目が「タイトル忘れた」というタイトル.その後に空行一行,その後に長い1行の3行が合せてSubjectになっています.

From: "白井 達也" <***@***.***.ac.jp*>
Subject: タイトル忘れた  近年,様々なヒューマノイドロボットが開発されてきたが,その大半は,ヒトのように跳んだり,走ったりといった衝撃力を伴うダイナミックな動作が苦手である.小型のヒューマノイドロボットやペットロボットの中にはダイナミックな動作を実現しているものも存在する.この違いはロボットのスケールに依存している.ロボットの多くは,電気モータを動力として用いている.ロボットのサイズがn倍になると重量はn3倍になるが,アクチュエータは体積がn倍になったとしても能力が単純にn3倍になるとは限らない.ロボットの重量を軽減し,かつロボットを動かすのに十分な大きさのトルクを得るには小型軽量のサーボモータに高減速比を持
Message-ID: <c96210c8cdc0d164042a2206945301e7@******.*****.*****.ac.jp>

Tatsuya Shirai への返信

Re: Wordの文章等をCopy&Pasteすると...

- Tatsuya Shirai の投稿

> 境目を探すのに長さ1文字から「変換しても大丈夫?」をキチキチと繰り返すのはかなり時間の無駄なのですが,ここは安全性重視です.

 やはりこれがどうも気になっていたので,function autocut998_sub()を二分法的なアルゴリズムに変えました.

function autocut998_sub($line, $max_line_length, $encode)
{
    if (($linelength = mb_strwidth($line)) == 0) return '';
    if (strlen(mb_convert_encoding($line, $encode, 'UTF-8')) <= $max_line_length) return $line;
    $offsetA = 0; $offsetB = $linelength;
    while (1) {
        $offsetC = (int)(($offsetA + $offsetB) / 2);
        if (($offsetC == $offsetA) || ($offsetC == $offsetB)) {
            $line = mb_substr($line, 0, $offsetC)."\n".autocut998_sub(mb_substr($line, $offsetC), $max_line_length, $encode);
            return $line;
        }
        $newlinelength = strlen(mb_convert_encoding(mb_substr($line, 0, $offsetC), $encode, 'UTF-8'));
        if ($newlinelength >= $max_line_length) $offsetB = $offsetC;
            else                                $offsetA = $offsetC;
    }
}

これで,サンプルの文字列データをmb_convert_encoding()する回数が,元のキチキチと頑張る方法では1971回も呼ばれていたのに対して,36回に激減しました.再帰呼び出しの回数は変わらないのでメモリの消費量は変化ありません.純粋に計算回数だけの減少ですが,かなり気持ちがスッキリしました.