Проверка квоты пользователя на этапе RCPT

На одном из почтовых серверов появилась необходимость отшибать письма (до приема содержимого письма) для пользователей, у которых исчерпан отведенный им лимит дискового пространства. В качестве MTA на сервере используется Exim. Как известно стандартными средствами данного MTA такое не сделать, но в силу гибкости Exim,а можно выйти из положения написав скрипт, который будет проверять лимиты пользователей и посредством сокетов взаимодействовать с ним (см. в доках readsocket). Что и было реализовано в скрипте, приведенном немного ниже. Скрипт запускается в фоне и обслуживает запросы Exim,а на проверку превышения квоты пользователем. На входе скрипт имеет полный почтовый адрес пользователя, а на выход выдает одну из заранее определенных констант:

  • QUOTA_OK — квота не исчерпана;
  • QUOTA_EXCEEDED — квота исчерпана;
  • QUOTA_UNKNOWN — по какой-либо причине не удалось установить состояние квоты;
  • QUOTA_ERROR — произошла ошибка на стороне скрипта.

Вот собственно код скрипта на Perl:

  1. #!/usr/bin/env perl
  2.  
  3. #
  4. # Для работы требуется модуль DBD::Pg
  5. #
  6.  
  7. use strict;
  8. use warnings;
  9. use Socket;
  10. use POSIX;
  11. use DBI;
  12.  
  13. my $work_dir = "/path/to/workdir";
  14. my $sock_name = "/tmp/ckmq.sock";
  15. my $log_file = "/path/to/log/ckmq_daemon.log";
  16. my $pid_file = "/var/run/ckmq.pid";
  17.  
  18. my $SOCK_FD = -1;
  19. my $srv_status = 1;
  20. my $sock_addr = sockaddr_un($sock_name);
  21. my $max_forks = 15;
  22. my $fork_count = 0;
  23. my $pid = -1;
  24. my $dbh = -1;
  25.  
  26. my $db_host = '127.0.0.1';
  27. my $db_user = 'dbuser';
  28. my $db_pass = 'dbpass';
  29. my $db_name = 'dbname';
  30.  
  31. #--------------------------------------------------------------------
  32.  
  33. # Переключаемся в режим демона
  34. defined($pid = fork())          || die "can't fork: $!\n";
  35. exit(0) if $pid;
  36. (setsid() != -1)                || die "can't start new session: $!\n";
  37.  
  38. open(PID_FILE, '>', $pid_file)  || die "cat't write pid: $!\n";
  39. print(PID_FILE $$);
  40. close(PID_FILE);
  41.  
  42. open(LOG_FILE, '>>', $log_file) || die "can't write $log_file: $!\n";
  43. open(STDIN, '<', '/dev/null')   || die "can't read /dev/null: $!\n";
  44. open(STDOUT, '>', '/dev/null')  || die "can't write /dev/null: $!\n";
  45. open(STDERR, '>', '/dev/null')  || die "can't write /dev/null: $!\n";
  46.  
  47. $SIG{INT} = \&sig_int;
  48. $SIG{TERM} = \&sig_int;
  49. $SIG{CHLD} = \&sig_chld;
  50.  
  51. socket(SOCK_FD, AF_UNIX, SOCK_STREAM, 0) || &error_log("$!");
  52. bind(SOCK_FD, $sock_addr)                || &error_log("$!");
  53. listen(SOCK_FD, SOMAXCONN)               || &error_log("$!");
  54. chmod(0777, $sock_name)                  || &error_log("can't chmod: $!");
  55. chdir($work_dir)                         || &error_log("can't chdir: $!");
  56.  
  57. &notice_log("check quota daemon started (pid: $$)");
  58. while($srv_status){
  59.   my $CLIENT_FD = -1;
  60.  
  61.   if (!accept(CLIENT_FD, SOCK_FD)){ next; }
  62.  
  63.   if ($fork_count >= $max_forks){
  64.     send(CLIENT_FD, "QUOTA_ERROR", 0);
  65.     shutdown(CLIENT_FD, 2);
  66.     close(CLIENT_FD);
  67.     &notice_log('fork_max exceeded');
  68.     next;
  69.   }
  70.  
  71.   $fork_count++;
  72.   if (fork() == 0){
  73.     close(SOCK_FD);
  74.  
  75.     $SIG{INT} = 'IGNORE';
  76.     $SIG{TERM} = 'DEFAULT';
  77.     $SIG{CHLD} = 'IGNORE';
  78.     setsockopt(CLIENT_FD, SOL_SOCKET, SO_RCVTIMEO, pack('L!L!', 5, 0));
  79.     &client_work($CLIENT_FD);
  80.  
  81.     shutdown(CLIENT_FD, 2);
  82.     close(CLIENT_FD);
  83.  
  84.     exit(0);
  85.   }
  86.   close(CLIENT_FD);
  87. }
  88.  
  89. if ($SOCK_FD != -1){
  90.   shutdown(SOCK_FD, 2);
  91.   close(SOCK_FD);
  92. }
  93. unlink($sock_name);
  94.  
  95. &notice_log('check quota daemon stopped');
  96.  
  97. exit(0);
  98.  
  99. #--------------------------------------------------------------------
  100.  
  101. sub error_log {
  102.   my $message = shift;
  103.   my ($sec, $min, $hour, $mday, $mon) = localtime();
  104.   print(LOG_FILE "$mon $mday $hour:$min:$sec error: $message\n");
  105.  
  106.   if ($SOCK_FD != -1){
  107.     shutdown(SOCK_FD, 2);
  108.     close(SOCK_FD);
  109.   }
  110.  
  111.   exit(0);
  112. }
  113.  
  114. sub notice_log {
  115.   my $message = shift;
  116.   my ($sec, $min, $hour, $mday, $mon) = localtime();
  117.   print(LOG_FILE "$mon $mday $hour:$min:$sec notice: $message\n");
  118. }
  119.  
  120. sub sig_int {
  121.   $srv_status = 0;
  122.   shutdown(SOCK_FD, 2);
  123.   close(SOCK_FD);
  124.   $SOCK_FD = -1;
  125. }
  126.  
  127. sub sig_chld {
  128.   my $pid = -1;
  129.  
  130.   while (($pid = waitpid(-1, WNOHANG)) > 0){
  131.     $fork_count--;
  132.   }
  133. }
  134.  
  135. sub client_work {
  136.   my $CLIENT_FD = shift;
  137.   my $buffer = '';
  138.   my $user_name = '';
  139.   my @email = ();
  140.   my ($quota_size, $mail_root) = (0,0);
  141.  
  142.   if (!defined(recv(CLIENT_FD, $buffer, 255, 0))){ return; }
  143.   if (!$buffer){ return; }
  144.  
  145.   &db_connect();
  146.  
  147.   chomp($buffer);
  148.   @email = split(/@/, $buffer);
  149.   #print("Name: ", $email[0], "\nDomain: ", $email[1], "\n");
  150.   if (defined($buffer = &db_get_user_from_alias($email[0], $email[1]))){
  151.     @email = split(/@/, $buffer);
  152.   }
  153.  
  154.   ($quota_size, $mail_root) = &db_get_user_quota($email[0], $email[1]);
  155.   $quota_size = $quota_size * 1024;
  156.  
  157.   $buffer = "$mail_root/$email[1]/$email[0]/Maildir/maildirsize";
  158.   if (open(MDSF, "<", $buffer)){
  159.     my $total_size = 0;
  160.  
  161.     $buffer = <MDSF>;
  162.     while (<MDSF>){
  163.         my ($tmp) = split(/ /);
  164.         $total_size += $tmp;
  165.     }
  166.     #&notice_log("Quota size: $quota_size");
  167.     #&notice_log("Total maildir size: $total_size");
  168.     send(CLIENT_FD, ($quota_size == 0) || ($total_size < $quota_size) ? 'QUOTA_OK' : 'QUOTA_EXCEEDED', 0);
  169.  
  170.     close(MDSF);
  171.   } else {
  172.     send(CLIENT_FD, 'QUOTA_UNKNOWN', 0);
  173.   }
  174.  
  175.   &db_disconnect();
  176.  
  177.   undef;
  178. }
  179.  
  180.  
  181. #--------------------------------------------------------------------
  182. sub db_connect {
  183.   $dbh = DBI->connect("dbi:Pg:host=$db_host;dbname=$db_name", $db_user, $db_pass, {AutoCommit => 0});
  184. }
  185.  
  186. sub db_disconnect {
  187.   $dbh->disconnect();
  188. }
  189.  
  190. # Функция возвращает массив из двух элементов: путь до почтового ящика и размер квоты
  191. sub db_get_user_data {
  192.   my ($uname, $dname) = @_;
  193.   my $stm = -1;
  194.   my @row = ();
  195.  
  196.   @row = $dbh->selectrow_array('SELECT "users_tb"."quota", "users_tb"."homedir" FROM "users_tb"
  197.     INNER JOIN "domains_tb" ON ("users_tb"."domain_id" = "domains_tb"."id")
  198.     WHERE "username" = $$' . $uname . '$$ AND
  199.       "domains_tb"."domainname" = $$' . $dname . '$$ LIMIT 1;');
  200.  
  201.   return ($#row == 2) ? @row : -1;
  202. }
  203.  
  204. # Функция преобразовывает алиас в реальное имя пользователя
  205. sub db_get_user_from_alias {
  206.   my ($uname, $dname) = @_;
  207.   my $stm = -1;
  208.   my @row = ();
  209.  
  210.   @row = $dbh->selectrow_array('SELECT "aliases_tb"."mailaddr" FROM "aliases_tb"
  211.       INNER JOIN "domains_tb" ON ("aliases_tb"."domain_id" = "domains_tb"."id")
  212.       WHERE "aliases_tb"."aliasname" = $$'.$uname.'$$ AND
  213.         "domains_tb"."domainname" = $$'.$dname.'$$ AND
  214.         "aliases_tb"."active" = $$true$$ AND "domains_tb"."active" = $$true$$');
  215.  
  216.   return $row[0];
  217. }

Данный скрипт написан для почтовой системы, описанной в данной статье. Думаю, переделать под какую-либо другую конфигурацию не составит особого труда. Для автоматического запуска во время старта системы можно добавить его в автозагрузку, например, создать файл в /usr/local/etc/rc.d с таким содержимым:
  1. #!/bin/sh
  2.  
  3. # PROVIDE: ckmq
  4. # BEFORE: exim
  5. # KEYWORD: shutdown
  6. #
  7. # Add the following lines to /etc/rc.conf to enable Check Mail Quota Daemon:
  8. # ckmq_enable (bool):    Set to "NO" by default.
  9. #                       Set it to "YES" to enable ckmq.
  10. #
  11.  
  12. . /etc/rc.subr
  13.  
  14. name="ckmq"
  15. rcvar=ckmq_enable
  16.  
  17. load_rc_config ${name}
  18.  
  19. : ${ckmq_enable="NO"}
  20.  
  21. pidfile="/var/run/${name}.pid"
  22. command="/path/to/script"
  23.  
  24. run_rc_command "$1"

Правило для Exim,а будет выглядить так:
  1. deny message = Quota size exceeded
  2.        domains = +local_domains
  3.        condition = ${if eq{QUOTA_EXCEEDED}{${readsocket{/tmp/ckmq.sock}{$local_part@$domain}{3s}{}{false}}}{yes}{no}}

Поскольку скрипт не может узнать размер принимаемого сообщения, то в MTA или MDA необходимо отключить проверку квот, чтобы допустить переполнение почтового ящика. В остальном скрипт довольно прост для понимания, поэтому я не буду подробно расписывать каждую строчку. Если есть вопросы по работе скрипта, то задавайте их в комменты или в форум.

Добавить комментарий

Filtered text

CAPTCHA
Этот вопрос предназначен для предотвращения автоматической отправки форм спам-ботами.
Fill in the blank.