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で表示するようカスタマイズしています。