На одном из почтовых серверов появилась необходимость отшибать письма (до приема содержимого письма) для пользователей, у которых исчерпан отведенный им лимит дискового пространства. В качестве MTA на сервере используется Exim. Как известно стандартными средствами данного MTA такое не сделать, но в силу гибкости Exim,а можно выйти из положения написав скрипт, который будет проверять лимиты пользователей и посредством сокетов взаимодействовать с ним (см. в доках readsocket). Что и было реализовано в скрипте, приведенном немного ниже. Скрипт запускается в фоне и обслуживает запросы Exim,а на проверку превышения квоты пользователем. На входе скрипт имеет полный почтовый адрес пользователя, а на выход выдает одну из заранее определенных констант:
- QUOTA_OK — квота не исчерпана;
- QUOTA_EXCEEDED — квота исчерпана;
- QUOTA_UNKNOWN — по какой-либо причине не удалось установить состояние квоты;
- QUOTA_ERROR — произошла ошибка на стороне скрипта.
Вот собственно код скрипта на Perl:
#!/usr/bin/env perl
#
# Для работы требуется модуль DBD::Pg
#
use strict;
use warnings;
use Socket;
use POSIX;
use DBI;
my $work_dir = "/path/to/workdir";
my $sock_name = "/tmp/ckmq.sock";
my $log_file = "/path/to/log/ckmq_daemon.log";
my $pid_file = "/var/run/ckmq.pid";
my $SOCK_FD = -1;
my $srv_status = 1;
my $sock_addr = sockaddr_un($sock_name);
my $max_forks = 15;
my $fork_count = 0;
my $pid = -1;
my $dbh = -1;
my $db_host = '127.0.0.1';
my $db_user = 'dbuser';
my $db_pass = 'dbpass';
my $db_name = 'dbname';
#--------------------------------------------------------------------
# Переключаемся в режим демона
defined($pid = fork()) || die "can't fork: $!\n";
exit(0) if $pid;
(setsid() != -1) || die "can't start new session: $!\n";
open(PID_FILE, '>', $pid_file) || die "cat't write pid: $!\n";
print(PID_FILE $$);
close(PID_FILE);
open(LOG_FILE, '>>', $log_file) || die "can't write $log_file: $!\n";
open(STDIN, '<', '/dev/null') || die "can't read /dev/null: $!\n";
open(STDOUT, '>', '/dev/null') || die "can't write /dev/null: $!\n";
open(STDERR, '>', '/dev/null') || die "can't write /dev/null: $!\n";
$SIG{INT} = \&sig_int;
$SIG{TERM} = \&sig_int;
$SIG{CHLD} = \&sig_chld;
socket(SOCK_FD, AF_UNIX, SOCK_STREAM, 0) || &error_log("$!");
bind(SOCK_FD, $sock_addr) || &error_log("$!");
listen(SOCK_FD, SOMAXCONN) || &error_log("$!");
chmod(0777, $sock_name) || &error_log("can't chmod: $!");
chdir($work_dir) || &error_log("can't chdir: $!");
¬ice_log("check quota daemon started (pid: $$)");
while($srv_status){
my $CLIENT_FD = -1;
if (!accept(CLIENT_FD, SOCK_FD)){ next; }
if ($fork_count >= $max_forks){
send(CLIENT_FD, "QUOTA_ERROR", 0);
shutdown(CLIENT_FD, 2);
close(CLIENT_FD);
¬ice_log('fork_max exceeded');
next;
}
$fork_count++;
if (fork() == 0){
close(SOCK_FD);
$SIG{INT} = 'IGNORE';
$SIG{TERM} = 'DEFAULT';
$SIG{CHLD} = 'IGNORE';
setsockopt(CLIENT_FD, SOL_SOCKET, SO_RCVTIMEO, pack('L!L!', 5, 0));
&client_work($CLIENT_FD);
shutdown(CLIENT_FD, 2);
close(CLIENT_FD);
exit(0);
}
close(CLIENT_FD);
}
if ($SOCK_FD != -1){
shutdown(SOCK_FD, 2);
close(SOCK_FD);
}
unlink($sock_name);
¬ice_log('check quota daemon stopped');
exit(0);
#--------------------------------------------------------------------
sub error_log {
my $message = shift;
my ($sec, $min, $hour, $mday, $mon) = localtime();
print(LOG_FILE "$mon $mday $hour:$min:$sec error: $message\n");
if ($SOCK_FD != -1){
shutdown(SOCK_FD, 2);
close(SOCK_FD);
}
exit(0);
}
sub notice_log {
my $message = shift;
my ($sec, $min, $hour, $mday, $mon) = localtime();
print(LOG_FILE "$mon $mday $hour:$min:$sec notice: $message\n");
}
sub sig_int {
$srv_status = 0;
shutdown(SOCK_FD, 2);
close(SOCK_FD);
$SOCK_FD = -1;
}
sub sig_chld {
my $pid = -1;
while (($pid = waitpid(-1, WNOHANG)) > 0){
$fork_count--;
}
}
sub client_work {
my $CLIENT_FD = shift;
my $buffer = '';
my $user_name = '';
my @email = ();
my ($quota_size, $mail_root) = (0,0);
if (!defined(recv(CLIENT_FD, $buffer, 255, 0))){ return; }
if (!$buffer){ return; }
&db_connect();
chomp($buffer);
@email = split(/@/, $buffer);
#print("Name: ", $email[0], "\nDomain: ", $email[1], "\n");
if (defined($buffer = &db_get_user_from_alias($email[0], $email[1]))){
@email = split(/@/, $buffer);
}
($quota_size, $mail_root) = &db_get_user_quota($email[0], $email[1]);
$quota_size = $quota_size * 1024;
$buffer = "$mail_root/$email[1]/$email[0]/Maildir/maildirsize";
if (open(MDSF, "<", $buffer)){
my $total_size = 0;
$buffer = <MDSF>;
while (<MDSF>){
my ($tmp) = split(/ /);
$total_size += $tmp;
}
#¬ice_log("Quota size: $quota_size");
#¬ice_log("Total maildir size: $total_size");
send(CLIENT_FD, ($quota_size == 0) || ($total_size < $quota_size) ? 'QUOTA_OK' : 'QUOTA_EXCEEDED', 0);
close(MDSF);
} else {
send(CLIENT_FD, 'QUOTA_UNKNOWN', 0);
}
&db_disconnect();
undef;
}
#--------------------------------------------------------------------
sub db_connect {
$dbh = DBI->connect("dbi:Pg:host=$db_host;dbname=$db_name", $db_user, $db_pass, {AutoCommit => 0});
}
sub db_disconnect {
$dbh->disconnect();
}
# Функция возвращает массив из двух элементов: путь до почтового ящика и размер квоты
sub db_get_user_data {
my ($uname, $dname) = @_;
my $stm = -1;
my @row = ();
@row = $dbh->selectrow_array('SELECT "users_tb"."quota", "users_tb"."homedir" FROM "users_tb"
INNER JOIN "domains_tb" ON ("users_tb"."domain_id" = "domains_tb"."id")
WHERE "username" = $$' . $uname . '$$ AND
"domains_tb"."domainname" = $$' . $dname . '$$ LIMIT 1;');
return ($#row == 2) ? @row : -1;
}
# Функция преобразовывает алиас в реальное имя пользователя
sub db_get_user_from_alias {
my ($uname, $dname) = @_;
my $stm = -1;
my @row = ();
@row = $dbh->selectrow_array('SELECT "aliases_tb"."mailaddr" FROM "aliases_tb"
INNER JOIN "domains_tb" ON ("aliases_tb"."domain_id" = "domains_tb"."id")
WHERE "aliases_tb"."aliasname" = $$'.$uname.'$$ AND
"domains_tb"."domainname" = $$'.$dname.'$$ AND
"aliases_tb"."active" = $$true$$ AND "domains_tb"."active" = $$true$$');
return $row[0];
}
Данный скрипт написан для почтовой системы, описанной в данной статье. Думаю, переделать под какую-либо другую конфигурацию не составит особого труда. Для автоматического запуска во время старта системы можно добавить его в автозагрузку, например, создать файл в /usr/local/etc/rc.d с таким содержимым:
#!/bin/sh
# PROVIDE: ckmq
# BEFORE: exim
# KEYWORD: shutdown
#
# Add the following lines to /etc/rc.conf to enable Check Mail Quota Daemon:
# ckmq_enable (bool): Set to "NO" by default.
# Set it to "YES" to enable ckmq.
#
. /etc/rc.subr
name="ckmq"
rcvar=ckmq_enable
load_rc_config ${name}
: ${ckmq_enable="NO"}
pidfile="/var/run/${name}.pid"
command="/path/to/script"
run_rc_command "$1"
Правило для Exim,а будет выглядить так:
deny message = Quota size exceeded
domains = +local_domains
condition = ${if eq{QUOTA_EXCEEDED}{${readsocket{/tmp/ckmq.sock}{$local_part@$domain}{3s}{}{false}}}{yes}{no}}
Поскольку скрипт не может узнать размер принимаемого сообщения, то в MTA или MDA необходимо отключить проверку квот, чтобы допустить переполнение почтового ящика. В остальном скрипт довольно прост для понимания, поэтому я не буду подробно расписывать каждую строчку. Если есть вопросы по работе скрипта, то задавайте их в комменты или в форум.
Добавить комментарий