Рецепты PostgreSQL: отправка писем с отчётом о доставке

от автора

Для приготовления отправки электронных писем с отчётом о доставке (а точнее с подтверждением о принятии или отклонении писем почтовым сервером получаетеля) нам понадобится сам postgres и его плагины планировщик асинхронных задач и cURL, а также локальный почтовый сервер, gawk и его плагин. Можно также воспользоваться docker-compose.

Итак, для начала, компилируем и устанавливаем плагины планировщик асинхронных задач и cURL.

CREATE EXTENSION IF NOT EXISTS pg_curl;

Далее,

CREATE TABLE email ( -- создаём таблицу для писем     id bigserial NOT NULL PRIMARY KEY, -- первичный ключ     timestamp timestamp without time zone NOT NULL DEFAULT now(), -- дата-время создания письмя     subject text NOT NULL, -- тема письма     sender text NOT NULL, -- отправитель письма     recipient text[] NOT NULL, -- массив получателей письма     body text NOT NULL, -- тело письма     message_id integer, -- идентификатор письма (из локального почтового сервера)     result text[] NOT NULL -- массив результатов (статусов) письма (для каждого получателя) );

а также

CREATE FUNCTION email_trigger() RETURNS trigger -- создаём триггерную функцию     LANGUAGE plpgsql -- на языке plpgsql     AS $_$ <<local>> declare -- с локальными переменными     input text; -- текст асинхронной задачи     recipient text; -- получатель письма begin     if tg_when = 'BEFORE' then -- перед     -- вставкой или обновлением (когда массив получателей обновился)         if tg_op = 'INSERT' or (tg_op = 'UPDATE' and new.recipient is distinct from old.recipient) then         -- заполняем/обнуляем массив результатов в соответствии с массивом получателей             new.result = array_fill('new'::text, array[array_length(new.recipient, 1)]);         end if;     elsif tg_when = 'AFTER' then -- после         if tg_op = 'INSERT' then -- вставки         -- генерируем текст задачи             local.input = format($format$select send(%1$L)$format$, new.id);             -- и добавляем новую задачу с указанным текстом             -- и датой-временем из письма (что позволяет сделать отложенную отправку),             insert into task (input, plan) values (local.input, new.timestamp);         end if;     end if;     -- и завершаем триггер     return case when tg_op = 'DELETE' then old else new end; end;$_$; -- затем создаем триггер перед CREATE TRIGGER email_after_trigger AFTER INSERT OR UPDATE OR DELETE ON email FOR EACH ROW EXECUTE PROCEDURE email_trigger(); -- и после CREATE TRIGGER email_before_trigger BEFORE INSERT OR UPDATE OR DELETE ON email FOR EACH ROW EXECUTE PROCEDURE email_trigger();

и конечно

CREATE FUNCTION send(email_id bigint) RETURNS text -- создаём функцию отправки писем     LANGUAGE plpgsql -- на языке plpgsql     AS $$ <<local>> declare -- с локальными переменными     email email; -- письмо     headers text; -- заголовки     recipient text; -- получатель begin -- задаём адрес нашего локального почтового сервера     perform curl_easy_setopt_url('smtp://smtp:25');     -- загружаем письмо в локальную переменную     select * from email where id = send.email_id into local.email;     -- для всех получателей письма     foreach local.recipient in array local.email.recipient loop     -- задаём получаетеля         perform curl_recipient_append(local.recipient);         -- и ещё раз         perform curl_header_append('To', local.recipient);     end loop;     -- задаём тему письма     perform curl_header_append('Subject', local.email.subject);     -- задаём отправителя письма     perform curl_header_append('From', local.email.sender);     -- тут ещё раз можно задать отправителя письма, но в этом случае     -- отправителю будет приходить уведомление о недоставке     --perform curl_easy_setopt_mail_from(local.email.sender);     perform curl_mime_data(local.email.body, type:='text/plain; charset=utf-8', code:='base64');     -- задаём таймаут     perform curl_easy_setopt_timeout(10);     -- устанавливаем массив статусов в соответствии с массивом получателей     update email as e set result = array_fill('sent'::text, array[array_length(e.recipient, 1)]) where id = send.email_id;     -- выполняем отправку     perform curl_easy_perform();     -- получаем заголовки отправки     local.headers = curl_easy_getinfo_header_in();     begin     -- вычисляем идентификатор письма         update email set message_id = ('x'||(regexp_match(local.headers, E'250 2.0.0 (\\w+) Message accepted for delivery'))[1])::bit(28)::int where id = send.email_id;         -- при ошибке - предупреждаем об этом         exception when others then raise warning 'ERROR: % - %', sqlstate, sqlerrm;     end;     -- возвращаем заголовки     return local.headers; end;$$;

Теперь перейдём к настройке нашего локального почтового сервера. Файл конфигурации smtpd.conf

# задаём процедуру proc "smtpd.awk" "/usr/bin/gawk -l /usr/local/lib/gawk/pgsql -f /etc/smtpd/smtpd.awk" user smtpd group smtpd # при выполнении фильтра filter "smtpd.awk" proc "smtpd.awk" # слушаем везде с заданным выше фильтром listen on 0.0.0.0 filter "smtpd.awk" # и действием action "relay" relay filter "smtpd.awk" # для всеговыполняем заданное выше действие match from any for any action "relay"

и собсвенно сама процедура smtpd.awk

function connect() { # функция соединения с базой # создаём подключение     conn = pg_connect("application_name=smtp target_session_attrs=read-write")     if (!conn) { # если не получилось         print("!pg_connect") > "/dev/stderr" # сообщаем об этом         exit 1 # и выходим     }     # создаём подготовленный оператор для обновления массива результатов с заданными параметрами     rcpt = pg_prepare(conn, "UPDATE email SET result[array_position(recipient, $3)] = $2 WHERE message_id = ('x'||$1)::bit(28)::int")     if (!rcpt) { # если не получилось         print(pg_errormessage(conn)) > "/dev/stderr" # сообщаем об этом         exit 1 # и выходим     }     # а также для обновления неудачей     rollback = pg_prepare(conn, "UPDATE email SET result = array_fill('permfail'::text, array[array_length(recipient, 1)]) WHERE array_length(recipient, 1) = 1 and message_id = ('x'||$1)::bit(28)::int")     if (!rollback) { # если не получилось         print(pg_errormessage(conn)) > "/dev/stderr" # сообщаем об этом         exit 1 # и выходим     } } BEGIN { # вначале     FS = "|" # задаём разделитель     OFS = FS # и ещё раз     _ = FS # и ещё     connect() # и подключаемся к базе } "config|subsystem|smtp-out" == $0 { # по команде настройки # регистрируем колбэк на получателя     print("register|report|smtp-out|tx-rcpt")     # и колбэк на ошибку     print("register|report|smtp-out|tx-rollback")     next # переходим к следующей команде } "config|ready" == $0 { # по команде готовности     print("register|ready") # сообщаем о готовности     fflush() # сбрасываем буферы     next # и переходим к следующей команде } "report|smtp-out|tx-rcpt" == $1_$4_$5 { # по команде получателя # создаём массив агрументов для подготовленного выше оператора     val[1] = $7 # идентификатор письма     val[2] = $8 # результат получателя     val[3] = $9 # получатель письма     res = pg_execprepared(conn, rcpt, 3, val) # выполняем подготовленный выше оператор     if (res == "ERROR BADCONN PGRES_FATAL_ERROR") { # при ошибке (отключения от базы)         connect() # ещё раз подключаемся к базе         res = pg_execprepared(conn, rcpt, 3, val) # и снова выполняем     }     if (res != "OK 1") { # если неудачно         print(pg_errormessage(conn)) > "/dev/stderr" # сообщаем об этом     }     pg_clear(res) # освобождаем рузультат     delete val # и аргументы     next # затем переходим к следующей команде } "report|smtp-out|tx-rollback" == $1_$4_$5 { # по команде ошибки # создаём массив агрументов для второго подготовленного выше оператора     val[1] = $7 # идентификатор письма     res = pg_execprepared(conn, rollback, 1, val) # выполняем подготовленный выше оператор     if (res == "ERROR BADCONN PGRES_FATAL_ERROR") { # при ошибке (отключения от базы)         connect() # ещё раз подключаемся к базе         res = pg_execprepared(conn, rollback, 1, val) # и снова выполняем     }     if (res != "OK 1") { # если неудачно         print(pg_errormessage(conn)) > "/dev/stderr" # сообщаем об этом     }     pg_clear(res) # освобождаем рузультат     delete val # и аргументы     next # затем переходим к следующей команде } END { # в конце     pg_disconnect(conn) # отключаемся от базы }

Ну а собственно для отправки письма используем команду

insert into email (subject, sender, recipient, body) values ('subject', 'sender@mail.com', '{recipient1@mail.com,recipient2@mail.com}', 'body');


ссылка на оригинал статьи https://habr.com/ru/post/650059/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *