- PVSM.RU - https://www.pvsm.ru -
Довелось мне работать админом в крупной компании, имеющей более 10-ти филиалов в разных городах, объединенных достаточно нестабильными и медленными каналами. Как и во многих других, основу обмена информацией в компании представляла электронная почта. Следует отметить что на каждом филиале, как и в головной конторе имеется свой почтовый сервер, управление почтовыми аккаунтами производится местным админом.
Задача основная — поддержка актуального справочника email адресов всех пользователей компании для внутреннего использования, второстепенная — список имеющихся email адресов для основного почтового сервера, дабы он проверял есть ли такой адрес в компании перед тем как переслать письмо на нужный филиал.
Каналы нестабильны, частенько «падают», скорость их работы вообще непредсказуема. Кому интересно как была решена задача приглашаю под кат.
Почтовая топология, если так можно назвать схему взаимоотношений почтовых серверов компании, звезда. Существует один транзитный сервер, доступный извне, через который происходит обмен почтой между филиалами компании, а также между пользователями компании и внешним миром. На сервере все проходящие сообщения проверяются на вирусы, входящие извне еще и на спам. Почтовых ящиков на сервере нет, это чисто SMTP relay.
Остальные сервера находятся физически в офисах филиалов и один в головном офисе. Выхода во внешний мир не имеют, получают входящие сообщения с главного сервера, исходящие отправляют через него же. Основная функция — обслуживание почтовых ящиков конечных пользователей.
Все сервера работают под управлением FreeBSD версий 7.x — 8.x, в качестве SMTP используется Postfix, в качестве POP3/IMAP Courier-IMAP.
Почтовые аккаунты пользователей хранятся в БД MySQL, управление ими производится админами на местах посредством web интерфейса, написанного на PHP. На филиалах простенькая моя разработка еще 2005г. на PHP 4, в головном офисе красивое приложение на Yii с использованием AJAX фич. Написано также мной в рамках изучения фреймворка.
Первоначально справочник хранился на сервере в виде HTML страницы, содержащей списки всех email-ов пофилиально. Со временем такое решение перестало меня удовлетворять по следующими причинам, в порядке убывания важности:
Долго думал как все сделать, перебрал варианты от репликации встроенными в MySQL средствами до написания собственных сервисов, слушающих сетевые порты. Ответ же все это время стоял рядом и ухмылялся. Это все связано с электронной почтой? Ну так вот вам и транспорт. Реалтайм с погрешностью в доли секунды нам не нужен, в пределах минуты же все будет работать при наличии канала. При отсутствии канала данные встанут в очередь и подождут, потом будут доставлены. В общем нам только отправить, остальное не наша забота.
Итак, решено, обмен данными будет происходить посредством email сообщений.
Обмен данными будет односторонний, почтовые сервера будут, по мере редактирования базы пользователей админом, отправлять данные по изменениям на центральный сервер, где и будет размещаться наш справочник.
Первым делом определимся с форматом команд. Команды будут передаваться в теле письма. Содержимое письма будет text/plain, вложений не будет, а значит MIME multipart с кодированием всего в base64 или quoted-printable не будет.
Тема письма будет содержать уникальный код подразделения (филиала) чтобы однозначно идентифицировать какие данные будем менять. Также это небольшая защита от «хулиганов» если таковые прочухают алгоритм работы нашей системы, не зная кода подразделения (довольно мудреного) никаких изменения нельзя будет произвести, письмо с неизвестным кодом будет просто проигнорировано. В теле письма может быть произвольное количество строк вида:
_cmd=insert:type=mbox:name=Иванов Иван Иванович:eml=ivanov@company.ru:vis=1:gor=88-02-93:mer=38-32-93:sot=:of=214:post=Начальник участка:sdiv=Производственное управление
_cmd=update:type=mbox:eml=petrov@company.ru:name=Петров Петр Петрович:act=1:vis=1:post=стажер:of=822:mer=27-71:gor=88-02-49:sot=:sdiv=Техническая дирекция.
_cmd=delete:type=mbox:eml=sidorov@company.ru
Как видно из этих строк однозначно идентифицировать запись у нас будет email адрес. Обычно в письме будет одна команда (строка) потому как для оперативности накапливать команды нет смысла. Однако изредка разными скриптами будет использоваться возможность использования одного письма для целого пакета команд.
Принимать команды будет скрипт, написанный на perl. Для передачи писем с командами во входной поток скрипта будет использоваться доступный только из внутренней сети почтовый ящик. Для реализации в моей системе (postfix во FreeBSD) я просто создал такой почтовый псевдоним в /etc/mail/aliases:
userdir: "|/home/admin/directory/userdir.pl"
Здесь я приведу сильно укороченный его листинг userdir.pl, дабы не перегружать и без того большую статью:
#!/usr/bin/perl -w
use Email::Simple;
use MIME::Base64 ();
use DBI;
...
# Хэш имен полей для конвертирования команд системы в SQL
my %fnames = (
name => 'name',
gor => 'gtel',
...
);
...
# Сообщение как есть поступает на входной поток нашего скрипта
# Записываем его целиком в переменную
while(<>)
{
$message .= $_;
}
# Парсим переменную, выделяя нужные нам данные
my $email = new Email::Simple($message);
my $from = $email->header("From");
my $to = $email->header("To");
my $subj = $email->header("Subject");
...
# По теме письма определяем идентификатор подразделения
my $sth = $dbh->prepare("select id from divs where code=?");
if ($sth->execute($subj))
{
if (defined($div_code = $sth->fetchrow())) { $div_id = $div_code; }
}
$sth->finish();
...
if ($div_id ne 'empty')
{
$cmd =~ s/n//;
#$cmd =~ tr/ //g;
my @lines = split(/;/, $cmd);
...
foreach $line (@lines)
{
# Тут непосредственно происходит разбор команды
chomp($line);
$line =~ s/n//;
if ($line =~ /^_cmd/)
{
my @fields = split(/:/, $line);
my $debug_str = '';
foreach $field (@fields)
{
my @prms = split(/=/, $field);
if ($#prms == 1) { $params{$prms[0]} = $prms[1]; }
$debug_str .= " $prms[0]=$params{$prms[0]};";
}
}
# Далее уже происходит обработка полученных команд
if ($params{'_cmd'} eq 'insert') ########################### INSERT cmd
{
my $qry = '';
my $qry_valid = 0;
if ($params{'type'} eq 'mbox') ######## Mailbox
{
$qry = "insert into persons(";
my $dfields = '';
my $fields_cnt = 0;
while (($key, $value) = each(%params))
{
if (defined($fnames{$key}) && $params{$key} ne '')
{
...
}
}
...
}
elsif ($params{'type'} eq 'alias')
...
}
...
# Далее подобным образом производится обработка остальных команд
# и запись данных в БД
}
}
Теперь только осталось сделать формирование команды при редактировании почтовых аккаунтов и ее отправку на описанный выше приемник. Поскольку web интерфейс я писал сам, сделать это не составило труда, вот например компонент для Yii Framework, который используется в afterSave модели:
/*
To set properties throught config file main.php you have to add this lines to file
'components'=>array(
…
'Userdir' => array(
'class' => 'Userdir',
'divId' => '0001',
'userdirAddr' => 'admin@company.ru',
),
…
),
*/
class Userdir extends CApplicationComponent
{
private $_divId;
private $_userdirAddr;
private $_isLoaded = false;
private $_transNames = array(
'name' => 'name',
'username' => 'eml',
'post' => 'post',
'phone_gor' => 'gor',
...
);
private $_cmdLine = '';
// Working with properties
public function getDivId()
{
return $this->_divId;
}
public function setDivId($value)
{
$this->_divId = $value;
}
public function getUserdirAddr()
{
return $this->_userdirAddr;
}
public function setUserdirAddr($value)
{
$this->_userdirAddr = $value;
}
// Public methods
public function load($attrs, $cmd, $type = 'mbox')
{
$this->_cmdLine = '_cmd='.$cmd.':type='.$type;
foreach ($attrs as $key => $value)
{
if (isset($this->_transNames[$key]))
{
$encodedValue = iconv('UTF-8', 'cp1251', $value);
$this->_cmdLine .= ':'.$this->_transNames[$key].'='.$encodedValue;
}
}
$this->_cmdLine .= "n";
$this->_isLoaded = true;
}
public function send($params = null)
{
if ($this->_isLoaded)
{
return mail($this->_userdirAddr, $this->_divId, $this->_cmdLine);
}
return true;
}
}
В afterSave пишем нечто вроде:
// Userdir replication
$userdir = Yii::app()->Userdir;
$userdir->setDivId($div_id = Domain::model()->find("domain='$this->domain'")->userdir_id);
$_userdirModes = array(
'phones' => 'update',
'update' => 'update',
'create' => 'insert',
'delete' => 'delete',
'active' => 'update',
);
if (isset($_userdirModes[$this->scenario]))
{
/* Edit */
$userdir->load($this->attributes, $_userdirModes[$this->scenario]);
}
if (!$retCode = $userdir->send()) return $retCode;
И теперь у нас все автоматически обновляется.
Система внедрена и успешно работает уже третий год. Я написал немного сумбурно, старался сократить, если кому интересно — отвечу на любые вопросы.
Автор: ischerbin
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/php-2/4679
Нажмите здесь для печати.