The OpenNET Project / Index page

[ новости /+++ | форум | теги | ]

Каталог документации / Раздел "Электронная почта" (Архив | Для печати)

Почтовый сервер на основе реляционной СУБД.

Оцените преимущества!

Евгений Прокопьев

Опубликовано во 1 номере журнала "Системный администратор" за 2006 год.
Подписку можно оформить в любом почтовом отделении связи ( Роспечать - индексы 20780, 81655, Пресса России - 87836) или через альтернативные способы подписки.

Большинство почтовых систем в качестве хранилища сообщений до сих пор используют различные текстовые форматы. Сегодня мы построим почтовый сервер на основе реляционной СУБД и посмотрим, какие преимущества нам это даст.

Одним из малоизвестных, но очень эффективных инструментов для построения сложных и масштабируемых почтовых систем является DBMail - комплекс программ для UNIX-подобных систем, позволяющий использовать реляционные СУБД в качестве серверного хранилища почтовых сообщений вместо традиционных mbox, Maildir и прочих текстовых хранилищ. Используя DBMail, можно добиться:

n Более высокой производительности по сравнению с файловыми хранилищами - реляционные СУБД гораздо лучше приспособлены к обработке больших объемов структурированных данных, чем любой парсер текстовых файлов.

n Большей гибкости при обработке и анализе корреспонденции - возможности SQL опять-таки несоизмеримы с тем, что можно сделать, обрабатывая mbox или Maildir различными самописными скриптами на Perl/Python/Ruby для извлечения какой-либо статистики.

 

При этом администратор почтовой системы может задействовать различные механизмы, предоставляемые СУБД (при условии, что выбранная им СУБД это позволяет), например:

n Кластеризация - механизм, с помощью которого можно разместить СУБД на нескольких компьютерах (узлах кластера) с доступом к единой БД.

n Репликация - возможность автоматического переноса сообщений из одной БД в другую, создания консолидированной БД из разных источников и т. д.

n Фрагментирование - разделение одной логической таблицы БД на несколько физических для упрощения обслуживания, повышения производительности, сжатия архивных данных и т. д.

n Отказоустойчивость и балансировка нагрузки с автоматическим переключением на резервный сервер при отказе основного.

n Резервное копирование - горячее (без необходимости останавливать почтовый сервер, чтобы не получить поврежденное хранилище сообщений) и инкрементальное (копирование изменений вместо копирования всего хранилища).

n Дополнительная логика обработки почты, построенную на триггерах, представлениях и т. д.

Похоже на голубую мечту сисадмина. Конечно, не все так замечательно: в DBMail пока отсутствуют некоторые возможности более распространенных и развитых почтовых систем (например, аутентификация с помощью SASL, встроенная поддержка SSL/TLS, механизм фильтрации почтовых сообщений, аналогичный Sieve - это первое, что приходит в голову), но реализовать их проще (и работы над реализацией многих недостающих возможностей ведутся), чем добавить описанные выше возможности к традиционным почтовым системам.

DBMail - продукт нидерландской компании IC&S, в котором также есть вклад большого числа независимых разработчиков из разных стран. Он распространяется по лицензии GPL, кроме того, IC&S предлагает платную поддержку.

Архитектура

В дальнейшем я предполагаю, что вы знакомы с общими принципами построения почтовых систем, и термины MTA, MDA и MUA вас не пугают. Если это не так, то всю необходимую информацию вы можете найти в моей статье "Круговорот почты в сети, или Архитектура современных почтовых систем", опубликованной в предыдущем номере журнала. Принципиальная схема работы DBMail, которую нарисовал Wolfram A. Kraushaar, вдохновленный примером Postfix big picture, выглядит так (см. рис. 1).

Рисунок 1. DBMail big picture

 

Теперь небольшой комментарий к изображению. В основе DBMail лежит реляционная БД - хранилище учетных записей пользователей, почтовых ящиков, сообщений и прочей вспомогательной информации. Также в состав DBMail входят две категории MDA:

n Предназначенные для доставки сообщений от MTA в хранилище.

n Предназначенные для доставки MUA из хранилища.

 

К первым относятся:

n dbmail-smtp - исполняемый файл, использующий pipe-интерфейс: читает почтовое сообщение от MTA со стандарного входа (stdin) и сохраняет его в БД. Таким образом, на каждое входящее сообщение создается отдельный процесс UNIX и подключение к СУБД.

n dbmail-lmtpd - UNIX-демон, принимающий клиентские подключения через UNIX-сокет или TCP-сокет. Для приема почтовых сообщений используется протокол LMTP. На каждое входящее сообщение MTA создает только клиентский сокет, необходимое количество процессов и подключений к БД создается заранее. Таким образом, этот вариант обеспечивает лучшую производительность при высокой нагрузке, но при низкой он потребляет больше системных ресурсов, чем необходимо.

 

Ко вторым относятся:

n dbmail-pop3d - демон для доступа по протоколу POP3.

n dbmail-imapd - демон для доступа по протоколу IMAP.

 

Кроме того, в состав DBMail входят следующие вспомогательные утилиты:

n dbmail-users - инструмент для управления пользователями и их псевдонимами (возможно, многим из вас будет привычнее термин alias).

n dbmail-util - инструмент для очистки, оптимизации и проверки корректности БД.

Общая часть функциональности всех рассматриваемых исполняемых файлов реализована в разделяемых библиотеках: libdbmail, libauthdbmail, libsortdbmail и lib[rdms-name]dbmail. В настоящее время тип поддерживаемой СУБД жестко определяется на этапе ./configure, причем указать можно только одну СУБД. В будущем планируется реализовать динамическую подгрузку модулей для работы с различными СУБД, описываемую в конфигурационном файле, - это должно упростить создание и поддержку пакетов DBMail для различных дистрибутивов Linux.

В качестве СУБД в настоящее время поддерживаются только PostgreSQL и MySQL, кроме того, в текущей нестабильной версии появилась поддержка SQLite. Однако реализованный в DBMail уровень абстракции для доступа к БД позволяет добавлять поддержку других СУБД без внесения каких-либо изменений в основной код, отвечающий за логику обработки почтовых сообщений.

Установка и настройка

Последней стабильной версией DBMail является 2.0.7, а нестабильной - 2.1.3. Обе можно загрузить с http://www.dbmail.org, кроме того, самый свежий вариант можно выгрузить из CVN. Для FreeBSD, Debian Linux и Gentoo Linux существуют готовые порты/пакеты DBMail, для прочих систем предлагается следующая стандартная процедура установки:

# ./configure --with-pgsql (либо --with-mysql)

# make

# make install

Я использую ALT Linux и считаю этот способ приемлемым только на тестовых машинах, а для использования на рабочих серверах предпочитаю собирать любое ПО, отсутствующее в дистрибутиве, в пакеты и помещать в собственный репозитарий. Соответственно, установка DBMail в этом случае сведется к:

# apt-get install dbmail

Бинарный RPM-пакет и пакет с исходниками DBMail для ALT Linux желающие могут загрузить из коллекции бэкпортов для ALT Linux 2.4 Master (http://backports.altlinux.ru). Пользователям прочих дистрибутивов Linux или других UNIX-систем предлагается на выбор установить DBMail стандартным образом или собрать пакет для своей системы.

Мы будем настраивать DBMail в связке с Postfix (в качестве MTA) и PostgreSQL (в качестве СУБД - c поддержкой PostgreSQL собран пакет для ALT Linux) - предполагается, что они уже установлены, настроены и работают. Для начала сначала создадим БД и пользователя (имеется в виду не системный пользователь, а пользователь СУБД PostgreSQL)- владельца этой БД:

# createuser -s -U postgres dbmail

# createdb -U dbmail dbmail

# psql -U dbmail dbmail < /usr/share/doc/dbmail-2.0.7/sql/postgresql/create_tables.pgsql

Затем создадим конфигурационный файл /etc/dbmail.conf со следующим содержимым (или слегка отредактируем уже существующий):

[DBMAIL]

host=localhost

sqlport=5432

sqlsocket=

user=dbmail

pass=dbmailpwd

db=dbmail

[email protected]

trace_level=2

 

[SMTP]

sendmail=/usr/sbin/sendmail

auto_notify=no

auto_reply=no

trace_level=2

 

[LMTP]

effective_user=mail

effective_group=mail

bindip=*

port=24

nchildren=10

maxchildren=10

minsparechildren=2

maxsparechildren=4

maxconnects=10000

timeout=300

resolve_ip=yes

trace_level=2

max_errors=500

 

[POP]

effective_user=mail

effective_group=mail

bindip=*

port=110

nchildren=10

maxchildren=10

minsparechildren=2

maxsparechildren=4

maxconnects=10000

timeout=300

resolve_ip=yes

pop_before_smtp=no

trace_level=2

 

[IMAP]

effective_user=mail

effective_group=mail

bindip=*

port=143

nchildren=10

maxchildren=10

minsparechildren=2

maxsparechildren=4

maxconnects=10000

timeout=4000

resolve_ip=yes

imap_before_smtp=no

trace_level=2

 

Небольшой комментарий к содержимому конфигурационного файла. Он состоит из следующих секций:

n DBMAIL - в ней описываются глобальные настройки, применимые ко всем сервисам DBMail (в основном это параметры подключения к БД).

n SMTP - здесь описываются параметры взаимодействия с MTA, которому DBMail передает почтовые сообщения для дальнейшей обработки.

n LMTP, POP3, IMAP - в этих секциях описываются настройки соответствующих демонов: на каких адресах и портах принимать подключения, под какой учетной записью работать, сколько дочерних процессов породить и т. д.

Пример конфигурационного файла, поставляемого в комплекте с DBMail, содержит поясняющие комментарии для большинства настраиваемых параметров.

Теперь необходимо настроить механизм взаимодействия с Postfix. Как уже было сказано, таких механизмов два: pipe-интерфейс и LMTP.

В первом случае потребуется в файле /etc/postfix/master.cf описать следующий транспорт:

dbmail-smtp unix - n n - - pipe

flags= user=mail:mail

argv=/usr/sbin/dbmail-smtp -d ${recipient}

-r ${sender}

А затем задействовать этот транспорт в /etc/postfix/main.cf:

mailbox_transport = dbmail-smtp:

Описание транспорта LMTP будет выглядеть так:

dbmail-lmtp unix - - n - - lmtp

Или так (если требуется большая детализация в логах):

dbmail-lmtp unix - - n - - lmtp -v

Или так (если имена некоторых доменов назначения входящих почтовых сообщений не описаны в DNS):

dbmail-lmtp unix - - n - - lmtp

-o disable_dns_lookups=yes

Во всех этих случаях новый транспорт нужно будет задействовать следующим образом:

mailbox_transport = dbmail-lmtp:localhost:24

Разумеется, можно задействовать доставку почтовых сообщений в хранилище DBMail только для определенных доменов, но это уже не имеет никакого отношения к DBMail и настраивается исключительно средствами Postfix.

Кроме того, необходимо поставить Postfix в известность о тех почтовых ящиках, для которых он должен принимать сообщения. Создадим для этого файл /etc/postfix/dbmail-mailboxes.cf со следующим содержимым:

user = dbmail

password = dbmailpwd

hosts = localhost

dbname = dbmail

table = dbmail_aliases

select_field = alias

where_field = alias

И задействуем его в /etc/postfix/main.cf:

local_recipient_maps = pgsql:/etc/postfix/dbmail-mailboxes.cf $alias_maps

Для того чтобы это работало, Postfix должен быть собран с поддержкой PostgreSQL. В Debian и ALT Linux (возможно, не только в них) поддержка PostgreSQL в Postfix вынесена в пакет postfix-pgsql, который необходимо доустановить, если он еще не установлен.

Пользователи прочих дистрибутивов Linux или других UNIX-систем, в которых Postfix собран без поддержки PostgreSQL, либо вообще отсутствует, могут перед компиляцией включить его самостоятельно, указав ключ -DHAS_PGSQL и пути к соответствующим заголовкам и библиотекам:

# make tidy

# make -f Makefile.init makefiles \

'CCARGS=-DHAS_PGSQL -I/usr/local/include/pgsql' \

'AUXLIBS=-L/usr/local/lib -lpq'

# make

Наконец, последний штрих - ежедневные плановые работы по очистке БД от удаленных записей и проверке корректности БД. Их будет выполнять файл /etc/cron.daily/dbmail-clean со следующим содержимым:

#!/bin/sh

 

/usr/sbin/dbmail-util -cturpd -l 24h -qq

Теперь можно запустить демоны dbmail-lmtpd (в том случае, если для доставки мы планируем использовать LMTP), dbmail-imapd и dbmail-pop3d (можно запустить только тот, который будет использоваться для извлечения почты из хранилища, если он будет один). В пакет для ALT Linux включены соответствующие стартовые скрипты, которые могут быть вызваны вручную с помощью service или настроены на автоматическое выполнение при загрузке с помощью chkconfig/ntsysv. Если для вашей ОС или вашего дистрибутива Linux таких скриптов нет, вам придется настроить автозапуск вручную.

На этом настройку связки Postfix + DBMail + PostgreSQL можно считать законченной.

Создадим тестового пользователя почтовой системы следующим образом:

# dbmail-users -a test -w testpwd -s [email protected]

Opening connection to database...

Opening connection to authentication...

Ok. Connected

Adding user test with password type ,0 bytes mailbox limit and clientid 0... Ok, user added id [3]

Adding INBOX for new user

Ok. added

[[email protected]]

Done

В этом примере с помощью параметра -a указывается имя пользователя, с помощью -w - пароль, а с помощью s - почтовый ящик, принадлежащий пользователю (у одного пользователя их может быть несколько).

Отправим созданному пользователю тестовое письмо:

# echo hello | mail -s "Test Message" [email protected]

В логах мы должны увидеть нечто вроде:

postfix/qmgr[29149]: 432B01774F: from=<[email protected]>, size=301, nrcpt=1 (queue active)

dbmail/lmtpd[29359]: serverchild.c,PerformChildTask: incoming connection from [127.0.0.1 (localhost.localdomain)]

dbmail/lmtpd[29359]: db.c, db_get_mailbox_from_filters: default mailbox [(null)]

dbmail/lmtpd[29359]: sort.c, sort_and_deliver: message id=4, size=333 is inserted

postfix/lmtp[29710]: 432B01774F: to=<[email protected]>, orig_to=<test>, relay=localhost[127.0.0.1], delay=1341, status=sent (215 Recipient <[email protected]> OK)

postfix/qmgr[29149]: 432B01774F: removed

Это значит, что письмо успешно доставлено.

Попытаемся получить его по протоколу POP3, чтобы удостовериться в том, что это действительно так:

# telnet localhost 110

Trying 127.0.0.1...

Connected to localhost.

Escape character is '^]'.

+OK DBMAIL pop3 server ready to rock <[email protected]>

user test

+OK Password required for test

pass testpwd

+OK test has 1 messages (355 octets)

stat

+OK 1 355

list

+OK 1 messages (355 octets)

1 355

.

retr 1

+OK 355 octets

Return-Path: [email protected]

Received: by mydomain.com (Postfix, from userid 502)

id 7E8E617762; Sun, 20 Nov 2005 15:03:04 +0300 (MSK)

To: [email protected]

Subject: Test Message

Message-Id: <[email protected]>

Date: Sun, 20 Nov 2005 15:03:04 +0300 (MSK)

From: [email protected]

 

hello

 

.

dele 1

+OK message 1 deleted

quit

+OK see ya later

Connection closed by foreign host.

Итак, DBMail настроен и в первом приближении работает именно так, как нам нужно.

Внутреннее устройство хранилища DBMail

После удачного запуска DBMail имеет смысл взглянуть на устройство его хранилища более внимательно. Уже знакомый нам по "DBMail big picture" Wolfram A. Kraushaar нарисовал ER (Entity-Relationship) модель БД DBMail, смотрите рис. 2.

Рисунок 2. DBMail Entity-Relationship Model

Теперь краткое пояснение. Центральной таблицей является dbmail_users, в которой хранится вся информация о пользователях: их имена, пароли, текущий и максимальный размер всех почтовых ящиков, а также дата и время последнего подключения. Каждому пользователю могут соответствовать один или несколько почтовых адресов из таблицы dbmail_aliases. Впрочем, с помощью алиасов в DBMail можно также пересылать сообщения другим адресатам или передавать внешним программам на стандартный вход (stdin) для дальнейшей обработки. Каждому пользователю принадлежит один или несколько почтовых ящиков (каталогов IMAP) из таблицы dbmail_mailboxes, по умолчанию доставка осуществляется в ящик по имени INBOX. Кроме владельца, с ящиком могут работать и другие пользователи (такие ящики называются shared folders) - они описаны в таблице dbmail_subscription, соответствующие права доступа описаны в таблице dbmail_acl. В каждом ящике хранятся сообщения, IMAP-флаги сообщений находятся в таблице dbmail_messages, а содержимое сообщений разбивается на блоки не больше 512 Кб и располагается в таблице dbmail_messageblks - каждый блок в отдельной записи. Для увеличения производительности размер и дата получения сообщения вынесены в отдельную таблицу dbmail_physmessage. Таблицы dbmail_auto_replies и dbmail_auto_notifications используются для автоматической отправки ответов и уведомлений, а таблица dbmail_pbsp - для POP/IMAP-before-SMTP (механизма, аналогичного SMTP-авторизации, но менее распространенного и менее надежного).

Довольно неудобным обстоятельством является то, что все заголовки каждого сообщения целиком хранятся в одной строке таблицы dbmail_messageblks - у таких строк значение поля is_header = 1. В нестабильной версии DBMail заголовки вынесены в отдельную таблицу, и для каждого выделена собственная запись. Однако вместо того чтобы ждать выхода новой ветки DBMail или использовать нестабильную, мы можем реализовать эту функциональность самостоятельно средствами PostgreSQL.

Воспользуемся тем, что PostgreSQL позволяет создавать функции на языке PL/Python и богатыми возможностями Python в области обработки почтовых сообщений (разумеется, этот выбор субъективен: желающие могут реализовать аналогичную функциональность на PL/PgSQL, PL/Perl, PL/Tcl или даже на С). Сначала включим поддержку PL/Python для выбранной БД:

# createlang -U dbmail plpythonu dbmail

Для того чтобы эта команда выполнилась, PostgreSQL необходимо собрать с поддержкой PL/Python. В ALT Linux поддержка PL/Python вынесена в пакет postgresql-python, который необходимо доустановить, если он еще не установлен. Кроме того, в ALT Linux PostgreSQL выполняется в chroot-окружении, поэтому нужно проследить за тем, чтобы все модули Python, которыми мы собираемся воспользоваться, находились там же. Проще всего будет полностью перенести каталог /usr/lib/python2.3/ в /var/lib/pgsql-root/usr/lib, а правильнее будет делать это не регулярно при необходимости установить или удалить некоторые модули Python, а один раз описать необходимую последовательность действий в скрипте /etc/chroot.d/postgresql.lib.

Теперь создадим и протестируем функцию, которая будет извлекать нужный нам заголовок почтового сообщения:

# psql -U dbmail dbmail

 

dbmail=# create function mailheader(varchar, varchar) returns varchar as $$

dbmail$# import email.Parser

dbmail$# parser = email.Parser.Parser()

dbmail$# message = parser.parsestr(args[0])

dbmail$# return message.get(args[1])

dbmail$# $$ language plpythonu;

CREATE FUNCTION

dbmail=# select mailheader(messageblk, 'To') from dbmail_messageblks where is_header=1;

mailheader

---------------------------------------------------

[email protected]

(записей: 1)

Для того чтобы оценить возможности этого механизма, у нас слишком маленькая БД. Конечно, можно написать скрипт, генерирующий множество почтовых сообщений, но мы поступим иначе. Допустим, что у нас уже есть архив списка рассылки DBMail-Users в формате Maildir. С помощью MUA создадим IMAP-каталог, а затем с помощью скрипта mailbox2dbmail импортируем в него все сообщения из архива:

# /usr/share/doc/dbmail-2.0.7/contrib/mailbox2dbmail/mailbox2dbmail -u test -t maildir -m maillist/ -b INBOX/DBMail-Users -p /usr/sbin/dbmail-smtp

Processed Message 1

...

Processed Message 558

All Done!

Теперь можно провести анализ содержимого IMAP-каталога:

# psql -U dbmail dbmail

 

dbmail=# select user_idnr, userid from dbmail_users;

user_idnr | userid

-----------+--------------------------------

1 | __@!internal_delivery_user!@__

2 | anyone

3 | test

(записей: 3)

dbmail=# select mailbox_idnr, owner_idnr, name from dbmail_mailboxes where owner_idnr = 3;

mailbox_idnr | owner_idnr | name

--------------+------------+--------------------

1 | 3 | INBOX

3 | 3 | Trash

4 | 3 | INBOX/DBMail-Users

(записей: 3)

 

dbmail=# select mailheader(messageblk, 'User-Agent'), count(*)

dbmail=# from dbmail_messages inner join dbmail_messageblks

dbmail-# on dbmail_messages.physmessage_id = dbmail_messageblks.physmessage_id

dbmail-# where mailbox_idnr = 4 and is_header = 1

dbmail-# group by 1

dbmail-# order by 2 desc;

mailheader | count

---------------------------------------------------------------------+------

| 194

Debian Thunderbird 1.0.6 (X11/20050802) | 104

Mozilla Thunderbird 1.0.6 (Windows/20050716) | 49

Mozilla Thunderbird 1.0.2 (Windows/20050317) | 32

Debian Thunderbird 1.0.2 (X11/20050602) | 30

Mozilla/5.0 (X11; U; Linux i686; ru-RU; rv:1.7.2) Gecko/20040808 | 15

Mozilla Thunderbird 1.0.6 (X11/20050716) | 12

Mozilla Thunderbird 1.0 (X11/20041206) | 11

Mozilla Thunderbird 1.0.7 (X11/20051014) | 9

Mozilla Thunderbird 1.0.7 (Windows/20050923) | 9

Debian Thunderbird 1.0.2 (X11/20050331) | 8

Mozilla Thunderbird 1.0 (Windows/20041206) | 8

SquirrelMail/1.4.4 | 7

Mozilla Thunderbird 1.0.6 (Macintosh/20050716) | 7

Debian Thunderbird 1.0.7 (X11/20051017) | 6

Opera M2/8.50 (Win32, build 7700) | 6

Mozilla Thunderbird 1.0.7 (X11/20051010) | 5

KMail/1.8.2 | 4

Thunderbird 1.4 (Windows/20050908) | 4

Mozilla Thunderbird 1.0.5 (Windows/20050711) | 3

Mozilla Thunderbird 1.0.7 (X11/20051013) | 2

Thunderbird 1.5 (Windows/20051025) | 2

SquirrelMail/1.5.1 [CVS] | 2

SquirrelMail/1.4.5 | 2

Pan/0.14.2.91 (As She Crawled Across the Table (Debian GNU/Linux)) | 2

Mozilla Thunderbird 1.0.7 (X11/20051031) | 2

Mozilla Thunderbird 1.0.6 (X11/20050727) | 2

Mozilla Thunderbird 1.0.6 (X11/20050826) | 2

SquirrelMail/1.4.3a | 1

SquirrelMail/1.4.3a-12.EL4.centos4 | 1

Mozilla Thunderbird 1.0.6 (X11/20050721) | 1

Mozilla/5.0 (X11; U; SunOS i86pc; en-US; rv:1.7) Gecko/20041221 | 1

Mozilla Thunderbird 1.0.7 (Macintosh/20050923) | 1

Microsoft-Entourage/11.1.0.040913 | 1

Mozilla Thunderbird 1.0.7 (X11/20051017) | 1

Mozilla Thunderbird 1.0.6-1.4.1.centos4 (X11/20050721) | 1

Mozilla Thunderbird 1.0.7 (X11/20050923) | 1

Mozilla Thunderbird 1.0 (X11/20041207) | 1

Microsoft-Entourage/11.2.1.051004 | 1

(записей: 39)

Итак, мы получили распределение сообщений в списке рассылки по MUA (точнее, по заголовку User-Agent). Запрос выполнился не слишком быстро, что и не удивительно: для каждой строки пришлось вызывать функцию разбора содержимого. Чтобы уменьшить время выполнения, можно создать триггер на вставку записей с сообщениями и сохранять результаты mailheader в отдельную проиндексированную таблицу. Тем самым за счет уменьшения времени анализа сообщений мы получим увеличение времени, затрачиваемого на добавление сообщений, - что важнее, решать администратору почтовой системы. Впрочем, здесь из области администрирования почтового сервера мы переходим в область администрирования баз данных вообще и администрирования PostgreSQL в частности - последнему посвящен цикл статей Сергея Супрунова, уже публиковавшийся в нашем журнале.

Shared folders

Трудно представить себе полноценный IMAP-сервер без поддержки механизма shared folders, обеспечивающего совместный доступ различных пользователей к общим каталогам с сообщениями. В DBMail поддерживаются:

n Системные общие каталоги в пространстве имен #Public - настраиваются администратором почтового сервера.

n Личные общие каталоги в пространстве имен #Users - настраиваются как администратором, так и владельцами каталогов средствами MUA (при условии, что MUA поддерживают эту функциональность).

Как мы уже выяснили, универсальный инструмент администратора DBMail - это SQL-консоль выбранной им СУБД. Создадим системный общий каталог News и предоставим доступ к нему пользователю test с помощью этого инструмента. Сначала необходимо создать пользователя с именем __public__. Мы уже создавали пользователя test с помощью dbmail-users, сделаем теперь то же самое средствами SQL:

# psql -U dbmail dbmail

 

dbmail=# insert into dbmail_users (userid, passwd)

dbmail-# values ('__public__', 'abracadabra');

INSERT 0 1

dbmail=# select user_idnr from dbmail_users where userid='__public__';

user_idnr

-----------

5

(1 запись)

Теперь создадим каталог News, владельцем которого будет пользователь __public__:

# psql -U dbmail dbmail

 

dbmail=# insert into dbmail_mailboxes

dbmail=# (owner_idnr, name, seen_flag, answered_flag, deleted_flag, flagged_flag,

dbmail=# recent_flag, draft_flag, no_inferiors, no_select, permission)

dbmail-# values (5, 'News', 1, 1, 1, 1, 1, 1, 0, 0, 2);

INSERT 0 1

dbmail=# select mailbox_idnr from dbmail_mailboxes where name='News';

mailbox_idnr

--------------

7

(1 запись)

А теперь опишем права доступа для пользователя test к этому каталогу:

# psql -U dbmail dbmail

 

dbmail=# select user_idnr from dbmail_users where userid='test';

user_idnr

-----------

3

 

(1 запись)

dbmail=# insert into dbmail_acl

dbmail=# (user_id, mailbox_id, lookup_flag, read_flag, seen_flag, write_flag,

dbmail=# insert_flag, post_flag, create_flag, delete_flag, administer_flag)

dbmail-# values (3, 7, 1, 1, 1, 1, 1, 1, 1, 1, 1);

INSERT 0 1

Все, пользователю test осталось только подписаться на каталог #Public/News средствами своего MUA. Впрочем, и это тоже можно выполнить средствами SQL:

# psql -U dbmail dbmail

 

dbmail=# select user_idnr from dbmail_users where userid='test';

user_idnr

-----------

3

(1 запись)

dbmail=# select mailbox_idnr from dbmail_mailboxes where name='News';

mailbox_idnr

--------------

7

(1 запись)

dbmail=# insert into dbmail_subscription values (3, 7);

INSERT 0 1

Возможности перенаправления почты

Мы не станем рассматривать эти возможности так же детально, как и shared folders, т.к. общий принцип администрирования DBMail уже понятен. Упомянем только таблицы, принимающие участие в перенаправлении почты:

n dbmail_aliases - в этой таблице каждому почтовому адресу (или нескольким адресам) можно поставить в соответствие один или несколько идентификаторов пользователей, уже заведенных в DBMail, произвольных адресов и внешних программ, которые получат сообщение на stdin. Перед именем программы должен быть символ "|" (в этом случае программа получит сообщение в необработанном виде) или "!" (к сообщению будет добавлен mbox-заголовок). Вместо непосредственного редактирования таблицы можно также использовать утилиту dbmail-users (Примечание: примерно то же самое можно сделать средствами MTA, но не всегда. Postfix, например, обрабатывает собственную таблицу алиасов только в том случае, если в доставке принимает участие его собственный MDA local. Это не самый эффективный способ доставки, и при настройке DBMail во многих случаях предпочтительнее использовать LMTP).

n dbmail_auto_notifications - в этой таблице для каждого пользователя DBMail можно указать несколько адресов, на которые придут уведомления о получении пользователем сообщения. По умолчанию такая функциональность отключена, чтобы ее включить, необходимо в файле /etc/dbmail.conf в секции SMTP исправить значение параметра auto_notify с no на yes.

n dbmail_auto_replies - в этой таблице для каждого пользователя DBMail можно указать текст сообщения для автоматического ответа на все приходящие сообщения. По умолчанию такая функциональность также отключена, и чтобы ее включить, необходимо в файле /etc/dbmail.conf в секции SMTP исправить значение параметра auto_reply с no на yes.

Использование описанных механизмов требует аккуратного подхода, иначе очень легко получить зацикливание почты. Так я по невнимательности указал для почтового пользователя в качестве адреса для уведомлений один из его же алиасов. После этого вместо одного письма пользователь получил около 500 сообщений в течение нескольких секунд на машине с довольно скромной аппаратной конфигурацией. Прекращать это безобразие пришлось вручную с помощью инструмента postsuper из комплекта Postfix.

Прочие возможности DBMail

К числу прочих возможностей почтового сервера, часто востребованных администраторами, относятся:

n Квоты на размер почтового ящика - поддерживаются в DBMail непосредственно: максимальный размер ящика можно указать как при вызове утилиты dbmail-users ключом -m, так и изменив значение поля maxmail_size в таблице dbmail_users.

n SMTP-авторизация - вообще-то это задача MTA, но поскольку в DBMail уже хранится соответствие имен пользователей и их паролей, есть смысл настроить SASL на использование этой информации.

n POP/IMAP before SMTP - это альтернатива SMTP-авторизации, которая опирается на использование таблицы dbmail_pbsp и должна быть включена в /etc/dbmail.conf параметрами pop_before-smtp/imap_before_smtp.

n SSL/TLS - DBMail не поддерживает этот механизм непосредственно, но есть возможность организовать прозрачный с точки зрения клиентов SSL-доступ с помощью внешнего ПО, например, stunnel - работа с этим инструментом подробно рассматривалась в цикле статей Андрея Бешкова.

n Фильтрация почтовых сообщений на основании их содержимого - эту задачу и способы ее решения мы обсудим позже.

Кроме того, DBMail не обошелся без GUI-конфигуратора DbMail Administrator (DBMA), написанного на Perl и ориентированного на веб. Он доступен на http://library.mobrien.com/dbmailadministrator и выглядит так (см. рис. 3).

Рисунок 3. DbMail Administrator

Есть еще один веб-интерфейс к DBMail - swiftMail (http://www.obfuscatedcode.com/swiftmail/trac), написанный на PHP. Его можно было бы отнести к классу webmail-приложений, если бы не одна особенность: для чтения почты используется не POP3/IMAP, а прямой доступ к БД DBMail. В настоящее время доступна альфа-версия swiftMail, поддерживающая только MySQL. Поддержка PostgreSQL обещается в ближайшем будущем.

Фильтрация почтовых сообщений средствами DBMail

Общеизвестным преимуществом IMAP является то, что почтовый пользователь может работать со своей и общей корреспонденцией с разных рабочих мест с помощью совершенно различных MUA: как традиционных, так и ориентированных на веб. Это означает, что правила, в соответствии с которыми сообщения распределяются по каталогам IMAP, должны определяться не на уровне MUA, а на уровне MDA.

В настоящее время в DBMail отсутствуют штатные способы решения этой задачи. В будущих версиях планируется использование специализированного языка программирования для фильтрации почтовых сообщений Sieve (описан в RFC 3028, активно используется в Cyrus IMAP), код поддержки Sieve даже есть в текущей стабильной версии, но он не завершен и недостаточно протестирован, поэтому при компиляции по умолчанию поддержка Sieve отключена. Кроме того, для большинства случаев Sieve является слишком мощным инструментом.

Сформулируем, что нам требуется от простейшего механизма фильтрации почтовых сообщений:

n Каждый пользователь может задать собственный список фильтров и назначить их приоритеты.

n Фильтр представляет из себя соответствие названия заголовка письма и его значения IMAP-каталогу, в который должно попасть письмо.

n Письмо попадает в каталог, если значение заголовка в письме содержит в себе значение заголовка в фильтре.

 

А теперь перечислим те способы решения задачи, которые пришли мне в голову:

n Использование традиционных MDA (procmail, maildrop) и dbmail-smtp.

n Использование фильтров MTA и патча для поддержки RFC 3589 (Sieve Email Filtering - Subaddress Extension: user+folder@server) в dbmail-lmtpd. Патч можно взять здесь - http://www.dbmail.org/mantis/bug_view_advanced_page.php?bug_id=0000057.

n Использование средств СУБД (триггеров) для модификации записей, соответствующих сообщению, в БД.

n Написание собственного патча.

Сравним их (см. таблицу 1). После недолгих раздумий выбор был сделан в пользу последнего варианта. Самым трудным оказалось не реализовать нужную функциональность, а найти место в коде, куда ее необходимо вставить, не нарушив при этом работоспособности всего остального. Решение задачи сильно упростило то, что в DBMail хорошо организовано ведение логов. Для каждого демона в конфигурационном файле можно задать уровень отладки (параметр trace_level), определяющий степень детализации выводимых отладочных сообщений.

Таблица 1. Способы фильтрации почтовых сообщений средствами DBMail

 

Способ

Преимущества

Недостатки

Использование традиционных MDA

Простота, привычность

Теряются все преимущества DBMail в производительности. Кроме того, все пользователи в DBMail - виртуальные, а не системные, т.е. появляются проблемы с разделением доступа к фильтрам

Использование фильтров MTA + RFC3589

Приемлемая производительность

Необходимо отслеживать побочные эффекты при написании фильтров MTA, писать фильтры труднее, чем правила для procmail и maildrop. Аналогичные проблемы с разделением доступа к фильтрам

Использование триггеров

Высокая производительность, возможность хранить фильтры в таблице БД и легко разделить доступ к ним с помощью представлений

Во всех СУБД поддержка триггеров существенно отличается - придется либо ограничиться поддержкой одной СУБД, либо поддерживать несколько реализаций одной и той же функциональности для каждой СУБД. В текущей стабильной версии DBMail на каждый заголовок письма не выделяется отдельной записи, т.е. заголовки не очень удобно анализировать - хотя в случае использования PostgreSQL эту проблему мы уже решили

Написание собственного патча

Переносимость между различными СУБД, высокая производительность, возможность хранить фильтры в таблице БД и разделить доступ к ним, простота анализа заголовков (это стало ясно только после изучения исходного кода)

Необходимо изучение исходного кода DBMail, возможно внесение ошибок в код

При максимальном уровне отладки (trace_level=5 в секции LMTP) во время доставки сообщения в логах можно увидеть очень много сообщений, но наиболее интересным является следующий фрагмент:

dbmail/lmtpd[5327]: pipe.c, insert_messages: temporary msgidnr is [11]

dbmail/lmtpd[5327]: pipe.c, insert_messages: calling sort_and_deliver for useridnr [7]

dbmail/lmtpd[5327]: dbpgsql.c,db_query: executing query [SELECT mailbox_idnr FROM dbmail_mailboxes WHERE name='INBOX' AND owner_idnr='7']

dbmail/lmtpd[5327]: db.c, db_find_create_mailbox: mailbox [INBOX] found

dbmail/lmtpd[5327]: dbpgsql.c,db_query: executing query [SELECT pm.messagesize FROM dbmail_physmessage pm, dbmail_messages msg WHERE

pm.id = msg.physmessage_id AND message_idnr = '11']

dbmail/lmtpd[5327]: dbpgsql.c,db_query: executing query [SELECT 1 FROM dbmail_users WHERE user_idnr = '7' AND (maxmail_size > 0) AND

(curmail_size + '363' > maxmail_size)]

dbmail/lmtpd[5327]: misc.c,create_unique_id: created: 62ee468abbec0e64995295a02f0dcd5d

dbmail/lmtpd[5327]: dbpgsql.c,db_query: executing query [INSERT INTO dbmail_messages (mailbox_idnr,physmessage_id, seen_flag, answered_flag,

deleted_flag, flagged_flag, recent_flag, draft_flag, unique_id, status) SELECT '9', physmessage_id, seen_flag, answered_flag,

deleted_flag, flagged_flag, recent_flag, draft_flag, '62ee468abbec0e64995295a02f0dcd5d', status FROM dbmail_messages WHERE

message_idnr = '11']

dbmail/lmtpd[5327]: dbpgsql.c,db_query: executing query [SELECT currval('dbmail_message_idnr_seq')]

dbmail/lmtpd[5327]: db.c,db_add_quotum_used: adding 363 to mailsize

dbmail/lmtpd[5327]: db.c.user_idnr_is_delivery_user_idnr: no need to look up user_idnr for __@!internal_delivery_user!@__

dbmail/lmtpd[5327]: dbpgsql.c,db_query: executing query [UPDATE dbmail_users SET curmail_size = curmail_size + '363' WHERE user_idnr = '7']

dbmail/lmtpd[5327]: sort.c, sort_and_deliver: message id=12, size=363 is inserted

dbmail/lmtpd[5327]: pipe.c, insert_messages: successful sort_and_deliver for useridnr [7]

Похоже, что именно здесь и происходит сохранение сообщения. Код, выводящий это сообщение, выглядит так:

trace(TRACE_DEBUG,

"%s, %s: calling sort_and_deliver for useridnr [%llu]",

__FILE__, __func__, useridnr);

dsn_result = sort_and_deliver(tmpmsgidnr, msgsize, useridnr, delivery->mailbox);

Можно заглянуть в реализацию sort_and_deliver и найти там подтверждение догадки.

В delivery->mailbox по умолчанию передается [null]. Таким образом, задача сводится к тому, чтобы, проанализировав заголовки сообщения, передать в delivery->mailbox имя каталога IMAP. Заголовки сообщения функция insert_messages получает в виде структуры headerfields.

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

Вызов функции будет выглядеть так:

trace(TRACE_DEBUG,

"%s, %s: calling sort_and_deliver for useridnr [%llu]",

__FILE__, __func__, useridnr);

 

dsn_result = sort_and_deliver(tmpmsgidnr, msgsize, useridnr, db_get_mailbox_from_filters(useridnr, headerfields, delivery->mailbox));

Прототип функции, который мы поместим в db.h:

char *db_get_mailbox_from_filters(u64_t useridnr, struct list *headerfields, const char *mailbox);

При реализации функции необходимо использовать следующую таблицу:

CREATE TABLE dbmail_filters (

user_id INT8 REFERENCES dbmail_users(user_idnr) ON DELETE CASCADE ON UPDATE CASCADE,

filter_id INT8,

filter_field varchar(128) NOT NULL,

filter_value varchar(255) NOT NULL,

mailbox varchar(100) NOT NULL,

PRIMARY KEY (user_id, filter_id)

);

CREATE INDEX dbmail_user_id_idx ON dbmail_filters(user_id);

CREATE INDEX dbmail_filter_id_idx ON dbmail_filters(filter_id);

В качестве образца чтения таблицы из БД можно использовать функцию db_get_users_from_clientid из db.c. В результате реализация функции db_get_mailbox_from_filters в файле db.c будет выглядеть так:

char *db_get_mailbox_from_filters(u64_t useridnr, struct list *headerfields, const char *mailbox)

{

trace(TRACE_MESSAGE, "%s, %s: default mailbox [%s]", __FILE__, __func__, mailbox);

 

if (mailbox == NULL)

{

unsigned i = 0;

unsigned num_filters = 0;

 

snprintf(query, DEF_QUERYSIZE,

"SELECT filter_field, filter_value, mailbox FROM dbmail_filters WHERE user_id = '%llu' ORDER BY filter_id",

useridnr);

 

if (db_query(query) == -1) {

trace(TRACE_ERROR, "%s,%s: error gettings

filters for "

"user_id [%llu]", __FILE__, __func__,

useridnr);

return NULL;

}

 

num_filters = db_num_rows();

for (i = 0; i < num_filters; i++) {

struct element *el = list_getstart(headerfields);

char *filter_field = db_get_result(i, 0);

char *filter_value = db_get_result(i, 1);

char *mailbox = db_get_result(i, 2);

 

trace(TRACE_MESSAGE,

"%s, %s: processing filter [%s : \"%s\" => %s]",

__FILE__, __func__, filter_field, filter_value, mailbox);

 

while (el) {

struct mime_record *record = (struct mime_record *) el->data;

 

trace(TRACE_MESSAGE,

"%s, %s: processing header [%s : \"%s\"]",

__FILE__, __func__, record->field, record->value);

 

if (!strcmp(record->field,

filter_field) && strstr(record->value, filter_value)) {

trace(TRACE_MESSAGE,

"%s, %s: header [%s : \"%s\"] accept filter [%s : \"%s\" => %s]",

__FILE__, __func__, record->field, record->value, filter_field, filter_value, mailbox);

 

return mailbox;

}

 

el = el->nextnode;

}

 

trace(TRACE_MESSAGE,

"%s, %s: no header accept filter [%s : \"%s\" => %s]",

__FILE__, __func__, filter_field, filter_value, mailbox);

}

 

db_free_result();

 

return NULL;

}

else

{

return mailbox;

}

}

После внесения изменений в код, перекомпиляции, создания таблицы фильтров в БД и ее заполнения новый механизм фильтрации был тщательно протестирован. Ошибок выявлено не было. Фрагмент лога, в котором виден процесс анализа заголовков почтового сообщения, теперь выглядит так:

dbmail/lmtpd[5329]: pipe.c, insert_messages: temporary msgidnr is [13]

dbmail/lmtpd[5329]: pipe.c, insert_messages: calling sort_and_deliver for useridnr [7]

dbmail/lmtpd[5329]: db.c, db_get_mailbox_from_filters: default mailbox [(null)]

dbmail/lmtpd[5329]: dbpgsql.c,db_query: executing query [SELECT filter_field, filter_value, mailbox FROM dbmail_filters WHERE user_id = '7' ORDER BY filter_id]

dbmail/lmtpd[5329]: db.c, db_get_mailbox_from_filters: processing filter [Subject : "Test" => MyFolder]

dbmail/lmtpd[5329]: db.c, db_get_mailbox_from_filters: processing header [From : "[email protected]"]

dbmail/lmtpd[5329]: db.c, db_get_mailbox_from_filters: processing header [Date : " Mon, 24 Oct 2005 10:42:25 +0400 (MSD)"]

dbmail/lmtpd[5329]: db.c, db_get_mailbox_from_filters: processing header [Message-Id : "<[email protected]>"]

dbmail/lmtpd[5329]: db.c, db_get_mailbox_from_filters: processing header [Subject : "Test Message"]

dbmail/lmtpd[5329]: db.c, db_get_mailbox_from_filters: header [Subject : "Test Message"] accept filter [Subject : "Test" => MyFolder]

dbmail/lmtpd[5329]: dbpgsql.c,db_query: executing query [SELECT mailbox_idnr FROM dbmail_mailboxes WHERE name='MyFolder' AND owner_idnr='7']

dbmail/lmtpd[5329]: dbpgsql.c,db_query: previous result set is possibly not freed.

dbmail/lmtpd[5329]: dbpgsql.c,db_query: executing query [INSERT INTO dbmail_mailboxes (name, owner_idnr,seen_flag, answered_flag, deleted_flag, flagged_flag,

recent_flag, draft_flag, permission) VALUES ('MyFolder', '7', 1, 1, 1, 1, 1, 1, 2)]

dbmail/lmtpd[5329]: dbpgsql.c,db_query: executing query [SELECT currval('dbmail_mailbox_idnr_seq')]

dbmail/lmtpd[5329]: db.c, db_find_create_mailbox: mailbox [MyFolder] created on the fly

dbmail/lmtpd[5329]: db.c, db_find_create_mailbox: mailbox [MyFolder] found

dbmail/lmtpd[5329]: dbpgsql.c,db_query: executing query [SELECT pm.messagesize FROM dbmail_physmessage pm, dbmail_messages msg WHERE

pm.id = msg.physmessage_id AND message_idnr = '13']

dbmail/lmtpd[5329]: dbpgsql.c,db_query: executing query [SELECT 1 FROM dbmail_users WHERE user_idnr = '7' AND (maxmail_size > 0) AND

(curmail_size + '363' > maxmail_size)]

dbmail/lmtpd[5329]: misc.c,create_unique_id: created: 701c6fdeea62fbc2ed575ff730561c80

dbmail/lmtpd[5329]: dbpgsql.c,db_query: executing query [INSERT INTO dbmail_messages (mailbox_idnr,physmessage_id, seen_flag, answered_flag, deleted_flag,

flagged_flag, recent_flag, draft_flag, unique_id, status) SELECT '10', physmessage_id, seen_flag, answered_flag, deleted_flag, flagged_flag,

recent_flag, draft_flag, '701c6fdeea62fbc2ed575ff730561c80', status FROM dbmail_messages WHERE message_idnr = '13']

dbmail/lmtpd[5329]: dbpgsql.c,db_query: executing query [SELECT currval('dbmail_message_idnr_seq')]

dbmail/lmtpd[5329]: db.c,db_add_quotum_used: adding 363 to mailsize

dbmail/lmtpd[5329]: db.c.user_idnr_is_delivery_user_idnr: no need to look up user_idnr for __@!internal_delivery_user!@__

dbmail/lmtpd[5329]: dbpgsql.c,db_query: executing query [UPDATE dbmail_users SET curmail_size = curmail_size + '363' WHERE user_idnr = '7']

dbmail/lmtpd[5329]: sort.c, sort_and_deliver: message id=14, size=363 is inserted

dbmail/lmtpd[5329]: pipe.c, insert_messages: successful sort_and_deliver for useridnr [7]

При этом содержимое таблицы фильтров таково:

# psql -U dbmail dbmail

 

dbmail=# select * from dbmail_filters;

user_id | filter_id | filter_field | filter_value | mailbox

---------+-----------+--------------+--------------+----------

6 | 0 | Subject | Test | MyFolder

(1 запись)

Изменения были оформлены как .diff (с помощью утилиты gendiff - после этого они могут быть использованы утилитой patch для автоматического внесения изменений в оригинальный исходный код) и отправлены в списки рассылки пользователей и разработчиков DBMail. По согласованию с лидером проекта эти изменения не внесены в основную стабильную ветку, но патч включен в архив с кодом, и пакет DBMail для Debian Linux собран с этим патчем (т.к. лидер проекта по совместительству еще и мантейнер этого пакета в Debian). Пакет DBMail для ALT Linux также собран с этим патчем. Пользователи прочих дистрибутивов Linux или других UNIX-систем могут собрать DBMail с ним самостоятельно.

Разумеется, патч не идеален, и в будущем, может быть, придется усовершенствовать его, добавив:

n использование регулярных выражений POSIX вместо простого поиска подстроки;

n кэширование содержимого таблицы фильтров, чтобы не выполнять SQL-запрос при анализе каждого сообщения;

n использование нескольких критериев, объединенных с помощью операций AND и OR, в одном фильтре;

n дополнительные функции обработки сообщений (например, обработка сообщений внешними программами по pipe-интерфейсу).

Итоги

Проекту DBMail придется пройти еще долгий путь, чтобы заслужить признание и получить такую же популярность, как, например, Courier IMAP или Cyrus IMAP. Но уже сейчас видно, что его потенциальные возможности очень широки.

В будущих версиях планируется реализовать следующее (часть уже реализована в версии 2.1.3):

n Использование LDAP для аутентификации - это уже поддерживается ведущими POP3/IMAP-серверами и очень удобно для ведения единой БД пользователей, единой адресной книги и т. д.

n Организация пользователей в группы - поможет упростить управление пользователями и раздавать полномочия не отдельным пользователям, а целым группам.

n Хранение разных заголовков и вложений отдельно - позволит значительно упростить анализ корреспонденции посредством написания SQL-запросов. Примерно то же самое мы сегодня реализовали самостоятельно, но только для PostgreSQL.

n Sieve - как средство фильтрации почтовых сообщений, очень примитивный аналог которого мы также сегодня реализовали.

n Индивидуальные черные списки и фильтры для пользователей - позволят пользователям самостоятельно указывать, письма от кого им не хотелось бы читать.

n Графические средства администрирования - пока многие возможности DBMail можно задействовать только путем внесения исправлений в БД- с помощью написания SQL-инструкций (хотя DbMail Administrator решает большинство проблем такого рода).

n Средства администрирования по протоколам XML-RPC/SOAP - основные настройки DBMail хранятся не в БД, а в файле /etc/dbmail.conf (например, там указывается имя БД и логин/пароль для доступа к ней). К этим настройкам, а также к настройкам, хранящимся в БД, было бы очень удобно иметь унифицированный доступ.

Посмотрим, что ожидает DBMail в будущем.

Литература, ссылки:

1. http://dbmail.org/dokuwiki/doku.php.

2. Бешков А. Защита сетевых сервисов с помощью stunnel (1-3 части). - Журанал "Системный администратор", N12, 2004 г., N1, 2005 г., N3, 2005 г.

3. Супрунов С. PostgreSQL: первые шаги. Журнал "Системный администратор", N7, 2004 г. - 26-33 с.

4. Супрунов С. PostgreSQL: функции и триггеры. - Журнал "Системный администратор", N10, 2004 г. - 42-47 с.

5. Супрунов С. PostgreSQL 8.0: новые возможности. - Журнал "Системный администратор", N3, 2005 г. - 7 с.




Партнёры:
PostgresPro
Inferno Solutions
Hosting by Hoster.ru
Хостинг:

Закладки на сайте
Проследить за страницей
Created 1996-2024 by Maxim Chirkov
Добавить, Поддержать, Вебмастеру