02 March 2013

Roundcube Webmail 0.8.5のセットアップと日本語化

Roundcube webmailをさくらインターネットの共用サーバにセットアップし、日本語環境での不具合を取り除くちょっとした改良など。

■ 検証環境

・さくらインターネット 共用サーバ (FreeBSD 7.1)
・PHP 5.3.21 (CGI版)
・MySQL 5.5

■ Roundcubeのインストール

Roundcube公式サイトより、現在の最新版”Complete: 0.8.5”(roundcubemail-0.8.5.tar.gz)をダウンロードし、サーバの適当なディレクトリに展開する。

$ wget http://downloads.sourceforge.net/project/roundcubemail/roundcubemail/0.8.5/roundcubemail-0.8.5.tar.gz $ mv roundcubemail-0.8.5 roundcube

次に、MySQLデータベースを作成する。さくらインターネットのサーバコントロールパネルで、”データベースの設定” → ”データベースの新規作成” を行う。

・MySQLのバージョン : 5.5
・作成するDB名 : user_roundcube
・文字コード : UTF-8

20130302-mysql-dbcreate.jpg

その後、データベースにテーブルを作成する。

$ mysql -h [SQLサーバ名] -u [ユーザ名] --password=[パスワード] user_roundcube < roundcube/SQL/mysql.initial.sql

さらに、.htaccessファイルを削除する。

$ rm roundcube/.htaccess

以上でWebセットアップを行うための下準備は全て完了。

■ Roundcubeの設定ファイル作成(Webセットアップを使う場合)

ブラウザよりroundcube/installer/index.phpを表示して、セットアップに入る。
先ほど設定したMySQLデータベースの項目を設定する以外は、初期値で放っておいても構わない。(ここで設定できない値もたくさんあるため、あとでroundcube/config/main.inc.phpをテキストエディタで編集すれば良い)

20130302-installer01.jpg

20130302-installer02.jpg

main.inc.phpdb.inc.phpが画面に表示されるので、ダウンロードしてサーバのroundcube/configに格納する。

■ Roundcubeの設定ファイル作成 (手作業で作成する場合)

MySQLデータベースの設定

roundcube/config/db.inc.php
$rcmail_config['db_dsnw'] = 'mysql://ユーザ名:パスワード@DBサーバ名/DB名';
roundcube/config/main.inc.php
// ---------------------------------- // IMAP // ---------------------------------- // the mail host chosen to perform the log-in // leave blank to show a textbox at login, give a list of hosts // to display a pulldown menu or set one host as string. $rcmail_config['default_host'] = 'ssl://localhost:993'; // TCP port used for IMAP connections $rcmail_config['default_port'] = 993; // ---------------------------------- // SYSTEM // ---------------------------------- // send plaintext messages as format=flowed $rcmail_config['send_format_flowed'] = false;

ローカルホスト以外に、GMailも使いたい場合は次のように設定すればよい。

$rcmail_config['default_host'] = array('ssl://localhost:993', 'ssl://imap.gmail.com:993');

■ 不必要なディレクトリをWebからアクセス不能にする

$ chmod 700 SQL $ chmod 700 bin $ chmod 700 config $ chmod 700 logs $ mv installer installer-disable $ chmod 700 installer-disable

■ Roundcube Webmailの起動

ここまでの設定でRoundcube Webmailを”とりあえず”利用できるようになる。一通りメールの送受信が出来るか確認してから、次の日本語化を行なってゆく。

20130302-roundcube-mailread.jpg
Roundcube メール受信画面の例

■ 日本語化

日本語メニューや、日本語でのメール送受信・表示はRoundcube公式配布パッケージ自体がそれなりに対応している。この状態で文句なしだという場合は、以下の記事は読むに値しない。

(1) 「①②ⅠⅡ㈱」などのWindows-31J(Microsoftコードページ932)のデコード
(2) 送信メッセージの文字エンコードをメール送信画面で切り替え
(3) 日本語メッセージの自動折り返し処理

これらの対応を”日本語化”と無理やりこじつけて、やってみることにする。

taka2.info 工作の日々 RoundCube 0.8.1 の日本語化から(1)と(2)の対応を行う

(1)Windows-31J(Microsoftコードページ932)のデコード

program/include/rcube_charset.php の180行目辺りより
/** * Convert a string from one charset to another. * Uses mbstring and iconv functions if possible * * @param string Input string * @param string Suspected charset of the input string * @param string Target charset to convert to; defaults to RCMAIL_CHARSET * * @return string Converted string */ public static function convert($str, $from, $to = null) { static $iconv_options = null; static $mbstring_loaded = null; static $mbstring_list = null; static $conv = null; $to = empty($to) ? strtoupper(RCMAIL_CHARSET) : self::parse($to); $from = self::parse($from); // It is a common case when UTF-16 charset is used with US-ASCII content (#1488654) // In that case we can just skip the conversion (use UTF-8) if ($from == 'UTF-16' && !preg_match('/[^\x00-\x7F]/', $str)) { $from = 'UTF-8'; } if ($from == $to || empty($str) || empty($from)) { return $str; } // 日本語コードの場合、Microsoft拡張文字セットを認識させるよう修正した // mbstring を iconv より優先して用いるため、順序を変更した if ($mbstring_loaded === null) { $mbstring_loaded = extension_loaded('mbstring'); } // convert charset using mbstring module if ($mbstring_loaded) { $aliases['WINDOWS-1257'] = 'ISO-8859-13'; $aliases['JIS'] = 'ISO-2022-JP-MS'; $aliases['ISO-2022-JP'] = 'ISO-2022-JP-MS'; $aliases['EUC-JP'] = 'EUCJP-WIN'; $aliases['SJIS'] = 'SJIS-WIN'; $aliases['SHIFT_JIS'] = 'SJIS-WIN'; if ($mbstring_list === null) { $mbstring_list = mb_list_encodings(); $mbstring_list = array_map('strtoupper', $mbstring_list); } $mb_from = $aliases[$from] ? $aliases[$from] : $from; $mb_to = $aliases[$to] ? $aliases[$to] : $to; // return if encoding found, string matches encoding and convert succeeded if (in_array($mb_from, $mbstring_list) && in_array($mb_to, $mbstring_list)) { if (mb_check_encoding($str, $mb_from) && ($out = mb_convert_encoding($str, $mb_to, $mb_from))) { return $out; } } } // convert charset using iconv module if (function_exists('iconv') && $from != 'UTF7-IMAP' && $to != 'UTF7-IMAP') { if ($iconv_options === null) { // ignore characters not available in output charset $iconv_options = '//IGNORE'; if (iconv('', $iconv_options, '') === false) { // iconv implementation does not support options $iconv_options = ''; } } // throw an exception if iconv reports an illegal character in input // it means that input string has been truncated set_error_handler(array('rcube_charset', 'error_handler'), E_NOTICE); try { $_iconv = iconv($from, $to . $iconv_options, $str); } catch (ErrorException $e) { $_iconv = false; } restore_error_handler(); if ($_iconv !== false) { return $_iconv; // ここのコードは少し上に移動した } }

(2) 送信メッセージの文字エンコードをメール送信画面で切り替え

config/main.inc.php.dist の770行目辺り
// When replying place cursor above original message (top posting) $rcmail_config['top_posting'] = false; // Default charset for sending message $rcmail_config['send_charset'] = 'ISO-8859-1'; // When replying strip original signature from message $rcmail_config['strip_existing_sig'] = true; // Show signature: // 0 - Never // 1 - Always // 2 - New messages only // 3 - Forwards and Replies only $rcmail_config['show_sig'] = 1; // When replying or forwarding place sender's signature above existing message $rcmail_config['sig_above'] = false; // Use MIME encoding (quoted-printable) for 8bit characters in message body $rcmail_config['force_7bit'] = false; // Use MIME B encoding (base64) for header $rcmail_config['head_encoding_base64'] = false;
program/include/rcube_mime.php の150行目辺り
foreach ($a as $val) { $j++; $address = trim($val['address']); $name = preg_replace(array('/^[\'"]/','/[\'"]$/'),'', trim($val['name']));
program/localization/en_US/labels.inc の420行目辺り
$labels['force7bit'] = 'Use MIME encoding for 8-bit characters'; $labels['encodingbase64'] = 'Use MIME B encoding for header'; $labels['advancedoptions'] = 'Advanced options';
program/localization/en_US/labels.inc の1500行目辺りに関数を追加
function rcmail_charset_selector($attrib) { global $RCMAIL; return $RCMAIL->output->charset_selector(array( 'name' => '_charset', 'selected' => $RCMAIL->config->get('send_charset') )); } function rcmail_check_sent_folder($folder, $create=false) { global $RCMAIL;
program/steps/mail/sendmail.inc には多数の変更
//80行目辺り // get identity record function rcmail_get_identity($id) { global $RCMAIL, $message_charset; if ($sql_arr = $RCMAIL->user->get_identity($id)) { $out = $sql_arr; $out['mailto'] = $sql_arr['email']; $name = rcube_charset_convert($sql_arr['name'], RCMAIL_CHARSET, $message_charset); if (function_exists('mb_encode_mimeheader')) { $head_encoding_mode = $RCMAIL->config->get('head_encoding_base64') ? 'B' : 'Q'; mb_internal_encoding($message_charset); $name = mb_encode_mimeheader($name, $message_charset, $head_encoding_mode, "\r\n", 8); mb_internal_encoding(RCMAIL_CHARSET); } $out['string'] = format_email_recipient($sql_arr['email'], $name); return $out; } return FALSE; } // 160行目辺りに追加 function rcmail_email_input_format($mailto, $count=false, $check=true) { global $RCMAIL, $EMAIL_FORMAT_ERROR, $RECIPIENT_COUNT; // convert UTF-8 to preserve \x2c(,) and \x3b(;) used in ISO-2022-JP; if (function_exists('mb_encode_mimeheader') && function_exists('mb_convert_encoding')) { global $message_charset; $mailto = mb_convert_encoding($mailto, 'UTF-8', $message_charset); } // simplified email regexp, supporting quoted local part $email_regexp = '(\S+|("[^"]+"))@\S+'; // 200行目辺り if ($name[0] == '"' && $name[count($name)-1] == '"') { $name = substr($name, 1, -1); } // $name = stripcslashes($name); $address = rcube_idn_to_ascii(trim($address, '<>')); // encode "name" field. if (function_exists('mb_encode_mimeheader') && function_exists('mb_convert_encoding')) { global $message_charset; $head_encoding_mode = $RCMAIL->config->get('head_encoding_base64') ? 'B' : 'Q'; $name = mb_convert_encoding($name, $message_charset, 'UTF-8'); mb_internal_encoding($message_charset); $name = preg_replace('/^"(.*)"$/', '$1', $name); $name = mb_encode_mimeheader($name, $message_charset, $head_encoding_mode, "\r\n", 8); mb_internal_encoding(RCMAIL_CHARSET); } else { $name = stripcslashes($name); } $result[] = format_email_recipient($address, $name); $item = $address; } else if (trim($item)) { continue; } // 240行目辺り (参考にしたWebサイトのコードからさらに改変) // set default charset $input_charset = $OUTPUT->get_charset(); $send_charset = $RCMAIL->config->get('send_charset'); global $message_charset; $message_charset = isset($_POST['_charset']) ? get_input_value('_charset', RCUBE_INPUT_POST) : ($send_charset != '' ? $send_charset : $input_charset); $EMAIL_FORMAT_ERROR = NULL; $RECIPIENT_COUNT = 0; // 330行目辺り $headers['Date'] = rcmail_user_date(); $headers['From'] = $from_string; $headers['To'] = $mailto; // 600行目辺り $transfer_encoding = '7bit'; $head_encoding = $RCMAIL->config->get('head_encoding_base64') ? 'base64' : 'quoted-printable'; // encoding settings for mail composing $MAIL_MIME->setParam('text_encoding', $transfer_encoding); $MAIL_MIME->setParam('html_encoding', 'quoted-printable'); $MAIL_MIME->setParam('head_encoding', $head_encoding); $MAIL_MIME->setParam('head_charset', $message_charset); $MAIL_MIME->setParam('html_charset', $message_charset); $MAIL_MIME->setParam('text_charset', $message_charset . ($flowed ? ";\r\n format=flowed" : '')); // encoding subject header with mb_encode provides better results with asian characters if (function_exists('mb_encode_mimeheader')) { $head_encoding_mode = $RCMAIL->config->get('head_encoding_base64') ? 'B' : 'Q'; mb_internal_encoding($message_charset); $headers['Subject'] = mb_encode_mimeheader($headers['Subject'], $message_charset, $head_encoding_mode, "\r\n", 8); mb_internal_encoding(RCMAIL_CHARSET); }
program/steps/settings/func.inc の520行目辺り
$blocks['main']['options']['force_7bit'] = array( 'title' => html::label($field_id, Q(rcube_label('force7bit'))), 'content' => $input_7bit->show($config['force_7bit']?1:0), ); } if (!isset($no_override['head_encoding_base64'])) { $field_id = 'rcmfd_head_encoding_base64'; $input_7bit = new html_checkbox(array('name' => '_head_encoding_base64', 'id' => $field_id, 'value' => 1)); $blocks['main']['options']['head_encoding_base64'] = array( 'title' => html::label($field_id, Q(rcube_label('encodingbase64'))), 'content' => $input_7bit->show($config['head_encoding_base64']?1:0), ); } if (!isset($no_override['mdn_default'])) { $field_id = 'rcmfd_mdn_default'; $input_mdn = new html_checkbox(array('name' => '_mdn_default', 'id' => $field_id, 'value' => 1)); // 570行目辺り $blocks['main']['options']['top_posting'] = array( 'title' => html::label($field_id, Q(rcube_label('whenreplying'))), 'content' => $select_replymode->show($config['top_posting']?1:0), ); } if (!isset($no_override['send_charset'])) { $field_id = 'rcmfd_send_charset'; $blocks['main']['options']['send_charset'] = array( 'title' => html::label($field_id, Q(rcube_label('defaultcharset'))), 'content' => $RCMAIL->output->charset_selector(array( 'name' => '_send_charset', 'selected' => $config['send_charset'] )) ); }
program/steps/settings/save_prefs.inc の80行目辺り
['_mime_param_folding']) : 0, 'force_7bit' => isset($_POST['_force_7bit']) ? TRUE : FALSE, 'head_encoding_base64' => isset($_POST['_head_encoding_base64']) ? TRUE : FALSE, 'mdn_default' => isset($_POST['_mdn_default']) ? TRUE : FALSE, 'dsn_default' => isset($_POST['_dsn_default']) ? TRUE : FALSE, 'strip_existing_sig' => isset($_POST['_strip_existing_sig']), 'sig_above' => !empty($_POST['_sig_above']) && !empty($_POST['_top_posting']), 'send_charset' => get_input_value('_send_charset', RCUBE_INPUT_POST), 'default_font' => get_input_value('_default_font', RCUBE_INPUT_POST), 'forward_attachment' => !empty($_POST['_forward_attachment']),

スキンファイルは、こちら側のみ例示

skins/larry/templates/compose.html の145行目辺り
<span class="composeoption"> <label><roundcube:label name="savesentmessagein" /> <roundcube:object name="storetarget" maxlength="30" style="max-width:12em" /></label> </span> <span class="composeoption"> <label><roundcube:label name="charset" /> <roundcube:object name="charsetSelector" id="rcmcomposecharset" /></label> </span> <roundcube:container name="composeoptions" id="composeoptions" />

(3) 日本語メッセージの自動折り返し処理

rc_wordwrap関数はASCIIの場合はスペースで分離する処理が働くが、日本語のようにスペースが入らない文章の場合は全く機能しない。

ASCII(文字列長と文字列の幅が一致する場合として判定)の場合はRoundcubeに実装されていた方法を、CJK文字が含まれる場合は(ASCIIでいうところの)単語の途中であってもぶった切る仕様を追加。

さらに、文字列先頭に「>」が含まれる場合はぶった切った後に全行にクオートを入れるようにした。

なお、rc_wordwrap関数でline wrap処理が行われるよう、設定ファイルconfig/main.inc.php.dist$rcmail_config['send_format_flowed'] = false;とすること。

program/include/rcube_shared.inc の170行目辺りから
/** * Wrapper function for wordwrap */ function rc_wordwrap($string, $width=75, $break="\n", $cut=false) { global $message_charset; // Step 1:original line-wrap process for ASCII string $para = mb_split("\n", $string); $string = ''; while (count($para)) { $line = array_shift($para); // skip, if newline only if(mb_strlen($line, $message_charset) == 0) { $string .= $break; continue; } // skip line-wrap, if $line is double-width character (CJK) string if(mb_strlen($line, $message_charset) != mb_strwidth($line, $message_charset)) { $string .= $line; $string .= $break; continue; } $in_quote = false; // quoted line detect if ($line[0] == '>') $in_quote = true; $list = explode(' ', $line); $len = 0; while (count($list)) { $line = array_shift($list); $l = mb_strlen($line, $message_charset); $newlen = $len + $l + ($len ? 1 : 0); if ($newlen <= $width) { $string .= ($len ? ' ' : '').$line; $len += (1 + $l); } else { if ($l > $width) { if ($cut) { $start = 0; while ($l) { $str = mb_substr($line, $start, $width, $message_charset); $strlen = mb_strlen($str, $message_charset); $string .= ($len ? $break.'>' : '').$str; $start += $strlen; $l -= $strlen; $len = $strlen; } } else { $string .= ($len ? $break.'>' : '').$line; if (count($list)) $string .= $break; $len = 0; } } else { if($in_quote) $string .= $break.'>'.$line; else $string .= $break.$line; $len = $l; } } } if (count($para)) $string .= $break; } // Step 2:line-wrap process for non ASCII double-width character (CJK) string $para = explode($break, $string); $string = ''; while (count($para)) { $line = array_shift($para); // skip, if newline only if(mb_strlen($line, $message_charset) == 0) { $string .= $break; continue; } // skip, if ASCII (single-width char) string elseif(mb_strlen($line, $message_charset) == mb_strwidth($line, $message_charset)) { $string .= $line; $string .= $break; continue; } $in_quote = false; // quoted line detect if ($line[0] == '>' || mb_substr($line,0,1,$message_charset) == '>') { // strip quote char '>' $line = mb_substr($line, 1, mb_strlen($line, $message_charset) - 1, $message_charset); $in_quote = true; } $line_part = ""; $len = 0; for($i=0; $i<mb_strlen($line, $message_charset); $i++) { $char= mb_substr($line, $i, 1, $message_charset); $line_part .= $char; if($char == "\n") { $len = 0; } $len += mb_strwidth($char, $message_charset); //==1?1:2; // 切り出された文字のバイト数 if($len >= $width) { $len=0; if($in_quote) $string .= '>'; $string .= $line_part.$break; $line_part = ''; } } if($in_quote) $string .= '>'; $string .= $line_part.$break; } return $string; }

※ その後、送信されたメールをバイナリで確認したところ、エンコードがISO-2022-JPの場合に文字を1つずつ結合するときに毎回KI/KOを入れるのでとんでもないことになっているのがわかった。

解決方法としてPHPの内部文字列UTF-8に一旦変換してline wrap処理を行い、完了後にメール用の文字エンコーディングに戻すと良いことがわかった。

program/include/rcube_shared.inc の170行目辺りから
/** * Wrapper function for wordwrap */ function rc_wordwrap($string, $width=75, $break="\n", $cut=false) { global $message_charset; // convert into utf8 (for optimize in iso-2022-jp KI/KO duplicate) $string = mb_convert_encoding($string, 'UTF-8', $message_charset); // Step 1:original line-wrap process for ASCII string $para = mb_split("\n", $string); $string = ''; while (count($para)) { $line = array_shift($para); // skip, if newline only if(mb_strlen($line, 'UTF-8') == 0) { $string .= $break; continue; } 〜 省略 〜 } if($in_quote) $string .= '>'; $string .= $line_part.$break; } // convert into mail-sending encoding $string = mb_convert_encoding($string, $message_charset, 'UTF-8'); return $string; }

■ 変更ファイル一式または、パッチファイルの配布

パッチファイルroundcube-0.8.5-ja.diffをダウンロードする

パッチの適用は、公式配布パッケージ(roundcubemail-0.8.5.tar.gz)を解凍(tar xvf roundcubemail-0.8.5.tar.gz)した後に、ディレクトリを移らずに次のように行う。

$ patch -p0 < patch.diff

あるいは、変更ファイルを含む全ファイルを圧縮したroundcubemail-0.8.5-ja.zipをダウンロードする。(diffでオリジナルとの変更点を確認してから使うこと。そうでなければ保証しない)

■ ブラウザを全画面で表示しない時用のスキン

larry-mod-skin.zipをダウンロードする

横800pxで表示するようカスタマイズしています。