#!/usr/bin/perl -w
#------------------------------------------------------------------------------
# Location mail configuration CGI
#
# Copyright (c) BAYBITS LLC. All rights reserved.
#------------------------------------------------------------------------------

use strict;
use utf8;
use CGI;
use HTML::Template;
use TF::CommonUtils;
use TF::SingleLock;

binmode(STDOUT, ":utf8");

my $LOCATION      = $ENV{'TF_LOCATION'} || '';
my $BRAND         = $ENV{'TF_BRANDNAME'} || '';
my $USERTYPE      = $ENV{'TF_USERTYPE'} || 'normal';
my $LANGPARAM     = TF::CommonUtils::getLangPriority();
my $TF_TOKEN      = $ENV{'TF_TOKEN'} || undef;
my $TEMPLATE_NAME = 'mailconf.tmpl.' . $LANGPARAM;
my $BACKURI       = $LOCATION . "/.cgi-bin/admin/mailconf";
my $SETTINGSURI   = $LOCATION . "/.cgi-bin/admin/settings";
my $CONFIG_ROOT   = $ENV{'TF_CONFIG_ROOT'} || '/usr/local/teamfile/www/conf/conf.d';
my $LOCATION_NAME = $LOCATION;
$LOCATION_NAME =~ s/\///g;

my $OVERRIDE_DIR  = $CONFIG_ROOT . "/location-overrides/" . $LOCATION_NAME;
my $OVERRIDE_FILE = $OVERRIDE_DIR . "/mail.conf";
my $BASE_FILE     = $CONFIG_ROOT . "/new_" . $LOCATION_NAME . ".conf";
my $LOCK_FILE     = $OVERRIDE_DIR . "/.mailconf.lock";

my @DIRECTIVES = (
	{ key => 'TfMail',                label_ja => 'メール機能',             label_en => 'Mail function',             desc_ja => 'TeamFile のメール送信機能を有効または無効にします。', desc_en => 'Enables or disables TeamFile mail delivery.', type => 'flag', default => 'on' },
	{ key => 'TfMlHostname',          label_ja => 'SMTP サーバ名',          label_en => 'SMTP hostname',             desc_ja => 'メール送信に使用する SMTP サーバのホスト名を指定します。', desc_en => 'SMTP server hostname used for mail delivery.', type => 'text', default => 'localhost' },
	{ key => 'TfMlHostport',          label_ja => 'SMTP ポート番号',        label_en => 'SMTP port',                 desc_ja => 'SMTP サーバへ接続するポート番号を指定します。', desc_en => 'Port number used to connect to the SMTP server.', type => 'number', default => '25' },
	{ key => 'TfMlUsername',          label_ja => 'SMTP ユーザ名',          label_en => 'SMTP username',             desc_ja => 'SMTP 認証で使用するユーザ名を指定します。不要な場合は空欄にします。', desc_en => 'Username for SMTP authentication. Leave blank when authentication is not required.', type => 'text', default => '' },
	{ key => 'TfMlPassword',          label_ja => 'SMTP パスワード',        label_en => 'SMTP password',             desc_ja => 'SMTP 認証で使用するパスワードを指定します。不要な場合は空欄にします。', desc_en => 'Password for SMTP authentication. Leave blank when authentication is not required.', type => 'text', default => '' },
	{ key => 'TfMlAdminaddr',         label_ja => '管理者メールアドレス',   label_en => 'Administrator mail address', desc_ja => '通知メールの管理者アドレスとして使用します。', desc_en => 'Administrator address used in notification mail.', type => 'mail', default => 'root@localhost' },
	{ key => 'TfMlEncoding',          label_ja => 'メール文字コード',       label_en => 'Mail encoding',             desc_ja => 'メール本文とヘッダで使用する文字コードを指定します。', desc_en => 'Character encoding used for mail headers and body.', type => 'encoding', default => 'UTF-8' },
	{ key => 'TfMlServerSend',        label_ja => 'サーバ送信モード',       label_en => 'Server send mode',          desc_ja => 'on はサーバ送信、off はクライアント主体、middle は監視メールなどを含む中間モードです。', desc_en => 'on sends from the server, off uses client-side sending, and middle is the hybrid mode used for watched mail.', type => 'mode', default => 'middle' },
	{ key => 'TfMlTemplateRoot',      label_ja => 'メールテンプレート場所', label_en => 'Mail template root',        desc_ja => 'メールテンプレートファイルを格納するディレクトリを指定します。', desc_en => 'Directory containing mail template files.', type => 'text', default => '/usr/local/teamfile/www/conf/conf.d/mailtemplate' },
	{ key => 'TfMlNotifyAction',      label_ja => 'ユーザ通知メール',       label_en => 'Notify action mail',        desc_ja => 'ユーザ作成や更新などの通知メールを送信するかを指定します。', desc_en => 'Controls whether user action notification mail is sent.', type => 'flag', default => 'on' },
	{ key => 'TfMlEnableOneClickURL', label_ja => 'ワンクリック URL',       label_en => 'One click URL',             desc_ja => '通知メールにワンクリックで開ける URL を含めるかを指定します。', desc_en => 'Controls whether notification mail includes one-click URLs.', type => 'flag', default => 'on' },
	{ key => 'TfMlEdit',              label_ja => 'メールテンプレート編集', label_en => 'Mail template edit',        desc_ja => '管理者によるメールテンプレート編集機能を有効または無効にします。', desc_en => 'Enables or disables administrator mail template editing.', type => 'flag', default => 'on' },
	{ key => 'TfMlRepFromAddr',       label_ja => '返信元メールアドレス',   label_en => 'Reply-To address',          desc_ja => '指定した場合、送信者として管理者メールアドレスではなくこのアドレスを使用します。', desc_en => 'When set, this address is used as the sender instead of the administrator address.', type => 'mail', default => '' },
	{ key => 'TfMlSendInterval',      label_ja => '送信間隔',               label_en => 'Send interval',             desc_ja => '複数宛先へ送信する際の RCPT コマンド間隔をミリ秒で指定します。空欄または 0 は待機なしです。', desc_en => 'Interval in milliseconds between RCPT commands when sending to multiple recipients. Blank or 0 means no delay.', type => 'number', default => '' },
);

my $MSGTABLE_ja = {
	invalidPrm => '値が不正です',
	dirNotWritable => '設定ディレクトリに書き込みできません',
	writeError => '設定の書き込みに失敗しました',
	serverBusy => 'サーバがビジーです。しばらく経ってからやり直してください',
};
my $MSGTABLE_en = {
	invalidPrm => 'Invalid parameter.',
	dirNotWritable => 'The settings directory is not writable.',
	writeError => 'Failed to write settings.',
	serverBusy => 'The server is busy. Please retry this operation after a while.',
};
my $MSGTABLE = ($LANGPARAM eq 'ja') ? $MSGTABLE_ja : $MSGTABLE_en;

if (!$LOCATION || !$USERTYPE || $USERTYPE ne 'admin') {
	TF::CommonUtils::showAccessDeny();
	die '';
}

main();

sub main {
	my $cgi = CGI->new;
	if (!$cgi || $cgi->cgi_error) {
		TF::CommonUtils::showErrorPage($MSGTABLE->{invalidPrm}, $LANGPARAM);
		return;
	}

	my $cmd = $cgi->param('hidCmd') || '';
	if ($cmd eq 'set') {
		storeSettings($cgi);
		return;
	}

	showResponse($cgi, '');
}

sub storeSettings {
	my $cgi = shift;

	if (!defined $TF_TOKEN || (($cgi->param('hidcsrftoken') || '') ne $TF_TOKEN)) {
		TF::CommonUtils::showErrorPage($MSGTABLE->{invalidPrm}, $LANGPARAM);
		return;
	}

	if (!-d $OVERRIDE_DIR && !mkdirParents($OVERRIDE_DIR)) {
		TF::CommonUtils::showErrorPage($MSGTABLE->{writeError}, $LANGPARAM);
		return;
	}
	if (!-w $OVERRIDE_DIR) {
		TF::CommonUtils::showErrorPage($MSGTABLE->{dirNotWritable}, $LANGPARAM);
		return;
	}

	my $lock = TF::SingleLock->new($LOCK_FILE);
	if (!$lock->waitForLocking(5)) {
		TF::CommonUtils::showErrorPage($MSGTABLE->{serverBusy}, $LANGPARAM);
		return;
	}

	my %params;
	foreach my $def (@DIRECTIVES) {
		my $key = $def->{key};
		my $value = TF::CommonUtils::decodeUTF8($cgi->param($key) || '');
		$value =~ s/^\s+//;
		$value =~ s/\s+$//;

		if (!validateValue($def, $value)) {
			$lock->release();
			showResponse($cgi, $MSGTABLE->{invalidPrm} . " ($key)");
			return;
		}
		$params{$key} = $value;
	}

	my $tmp = $OVERRIDE_FILE . ".$$.tmp";
	my $fh;
	if (!open($fh, ">", $tmp)) {
		$lock->release();
		TF::CommonUtils::showErrorPage($MSGTABLE->{writeError}, $LANGPARAM);
		return;
	}
	binmode($fh, ":utf8");
	print $fh "# This file is managed by TeamFile CGI.\n";
	print $fh "# Location: $LOCATION_NAME\n\n";
	foreach my $def (@DIRECTIVES) {
		my $key = $def->{key};
		my $value = $params{$key};
		next if $value eq '';
		print $fh sprintf("%-24s %s\n", $key, quoteApacheValue($value));
	}
	close($fh);

	if (!rename($tmp, $OVERRIDE_FILE)) {
		unlink($tmp);
		$lock->release();
		TF::CommonUtils::showErrorPage($MSGTABLE->{writeError}, $LANGPARAM);
		return;
	}

	$lock->release();
	TF::CommonUtils::redirectPage($BACKURI, $LANGPARAM);
}

sub showResponse {
	my $cgi = shift;
	my $status = shift || '';
	my %params = readMailSettings();
	my @rows;

	foreach my $def (@DIRECTIVES) {
		my $key = $def->{key};
		my $value = defined $params{$key} ? $params{$key} : $def->{default};
		my $is_flag = ($def->{type} eq 'flag') ? 1 : 0;
		my $is_mode = ($def->{type} eq 'mode') ? 1 : 0;
		my $is_encoding = ($def->{type} eq 'encoding') ? 1 : 0;
		my $is_mail = ($def->{type} eq 'mail') ? 1 : 0;
		my $is_text = ($def->{type} ne 'flag' && $def->{type} ne 'mode' && $def->{type} ne 'encoding') ? 1 : 0;
		my $on_selected = (lc($value) eq 'on') ? 1 : 0;
		my $off_selected = (lc($value) eq 'off') ? 1 : 0;
		my $middle_selected = (lc($value) eq 'middle') ? 1 : 0;
		my $utf8_selected = ($value =~ /^utf-?8$/i) ? 1 : 0;
		my $iso2022jp_selected = (lc($value) eq 'iso-2022-jp') ? 1 : 0;
		my $escaped_value = TF::CommonUtils::escapeHTML($value, 0);
		my $is_password = ($key eq 'TfMlPassword') ? 1 : 0;

		push(@rows, {
			key => $key,
			label => localized($def, 'label'),
			desc => localized($def, 'desc'),
			value => $escaped_value,
			isFlag => $is_flag,
			isMode => $is_mode,
			isEncoding => $is_encoding,
			isMail => $is_mail,
			isText => $is_text,
			isPassword => $is_password,
			onSelected => $on_selected,
			offSelected => $off_selected,
			middleSelected => $middle_selected,
			utf8Selected => $utf8_selected,
			iso2022jpSelected => $iso2022jp_selected,
		});
	}

	my $template_file = findTemplateFile($TEMPLATE_NAME);
	if (open(my $tmplfh, "<", $template_file)) {
		binmode($tmplfh, ":utf8");
		my $htmltmpl = HTML::Template->new(
			filehandle => $tmplfh,
			case_sensitive => 1,
			die_on_bad_params => 0,
		);
		$htmltmpl->param(
			brandname => $BRAND,
			location => $LOCATION_NAME,
			settingsFile => $OVERRIDE_FILE,
			backUri => $SETTINGSURI,
			status => $status,
			settings => \@rows,
			token => $TF_TOKEN,
		);
		print $cgi->header(-charset => 'utf-8');
		print $htmltmpl->output;
		close($tmplfh);
	}
	else {
		TF::CommonUtils::showTemplateOpenErr($LANGPARAM);
	}
}

sub localized {
	my $def = shift || {};
	my $name = shift || '';
	my $key = $name . '_' . $LANGPARAM;
	return $def->{$key} || $def->{$name . '_en'} || '';
}

sub readMailSettings {
	my %params;
	readDirectiveFile($BASE_FILE, \%params);
	readDirectiveFile($OVERRIDE_FILE, \%params);
	return %params;
}

sub readDirectiveFile {
	my $path = shift || '';
	my $params_ref = shift;
	return if !$path || !-f $path;

	if (open(my $fh, "<", $path)) {
		while (my $line = <$fh>) {
			chomp($line);
			$line =~ s/\r$//;
			$line =~ s/^\s+//;
			next if $line eq '' || $line =~ /^#/;
			if ($line =~ /^([A-Za-z][A-Za-z0-9]*)\s+(.+?)\s*$/) {
				my ($key, $value) = ($1, $2);
				next if !isMailDirective($key);
				$value =~ s/\s+#.*$//;
				$value =~ s/^"(.*)"$/$1/;
				$value =~ s/^'(.*)'$/$1/;
				$params_ref->{$key} = $value;
			}
		}
		close($fh);
	}
}

sub isMailDirective {
	my $key = shift || '';
	foreach my $def (@DIRECTIVES) {
		return 1 if $def->{key} eq $key;
	}
	return 0;
}

sub validateValue {
	my $def = shift;
	my $value = shift;
	my $type = $def->{type};

	return 0 if $value =~ /[\r\n]/;
	return 1 if $value eq '' && ($def->{key} eq 'TfMlUsername' ||
								 $def->{key} eq 'TfMlPassword' ||
								 $def->{key} eq 'TfMlRepFromAddr' ||
								 $def->{key} eq 'TfMlSendInterval');

	if ($type eq 'flag') {
		return ($value =~ /^(on|off)$/i) ? 1 : 0;
	}
	if ($type eq 'mode') {
		return ($value =~ /^(on|off|middle)$/i) ? 1 : 0;
	}
	if ($type eq 'encoding') {
		return ($value =~ /^(UTF-8|ISO-2022-JP)$/i) ? 1 : 0;
	}
	if ($type eq 'number') {
		return ($value =~ /^[0-9]+$/) ? 1 : 0;
	}
	if ($type eq 'mail') {
		return TF::CommonUtils::isMailAddress($value);
	}
	return ($value =~ /^[^\r\n]+$/) ? 1 : 0;
}

sub quoteApacheValue {
	my $value = shift || '';
	if ($value =~ /[\s"#]/) {
		$value =~ s/\\/\\\\/g;
		$value =~ s/"/\\"/g;
		return '"' . $value . '"';
	}
	return $value;
}

sub mkdirParents {
	my $dir = shift || '';
	my @parts = split(/\//, $dir);
	my $path = '';

	foreach my $part (@parts) {
		next if $part eq '';
		$path .= '/' . $part;
		next if -d $path;
		return 0 if !mkdir($path);
		chmod(0777, $path);
	}
	return 1;
}

sub findTemplateFile {
	my $name = shift || '';
	my @candidates = ("./tmpl/$name", "../tmpl/$name");

	foreach my $path (@candidates) {
		return $path if -f $path;
	}
	return $candidates[0];
}
