Как доставлять e-mail уведомления клиентам в условиях невозможности прописать обратную DNS зону

в 7:25, , рубрики: 1С-Битрикс, elinks, gmail, mail, php, pyexpect, smtp

Proof of Concept

Из такого заголовка довольно сложно понять, «а кому оно вообще нужно?», а потому для начала краткое предисловие.

Ни для кого не секрет, что интернет-провайдеры весьма агрессивно смотрят на малый бизнес. Условия обслуживания физических и юридических лиц примерно те же, а вот цена весьма существенно разнится. Ситуация монополии в какой-то мере исправилась с приходом таких стандартов, как LTE и 4G, но условия обслуживания и сейчас остаются весьма далекими от гуманности. Итак, это статья посвящается тем, кто по тем или иным условиям вынужден взаимодействовать с провайдером, который предоставляет внешний IP адрес, но не дает возможности редактировать соответствующие обратные DNS записи.

Наверняка многим известно, что в качестве требования к почтовым серверам, помимо DKIM записей и прочих проверок, предъявляется еще и обязательное наличие обратной DNS записи. В противном случае, письма либо не будут доходить вовсе, либо будут попадать в папку «Спам». С вышеозначенными фактами далее мы и будем разбираться.

Основная идея

Какие варианты остаются? Во-первых это сервисы прозрачного SMTP проксирования. Но в этом случае клиенту будут приходить письма со стороннего домена, что, вполне естественно, может вызвать недоверие клиента к вашему ресурсу. Во вторых это магия. В сторону последней и будем думать.

Идея в том, чтобы избавиться от вашего IP, как от начального релея. В этом нам помогут службы Gmail Hosted. Процедура настройки вашего домена для использования Gmail Hosted довольно проста и хорошо задокументирована. За всеми подробностями сюда: Google Apps для бизнеса.

Настроив свой домен для работы со службами Gmail Hosted и зарегистрировав соответствующий почтовый ящик для связи с клиентами (предположим это noreply@yourdomain.com), очень хочется автоматически из кода вашего ресурса оповещать своих клиентов о событиях. Но если же в такой конфигурации попытаться воспользоваться учетной записью noreply@yourdomain.com, как релеем, то получим то же, от чего уходили. Нанимать сотрудника, который бы входил в веб-интерфейс и отправлял оповещения — неразумно.

Много кода

В моем примере я использую OS FreeBSD, так что «as is» данный пример может быть использован только в ней.

Есть отличный консольный браузер elinks, который умеет очень много чего, но не JavaScript. Как бы документация Вас не уверяла в обратном, это не так, elinks интерпретирует JavaScript только в качестве языка для скриптования действий пользователя. Но, так или иначе, это нас не должно остановить, т.к. Gmail пока еще сохраняет совместимость с браузерами, в которых отсутствует поддержка JavaScript интерпретатора.

Как это будет работать:

  • Перехват встроенной в php функции mail()
  • Вывод аргументов ф-ции mail() во внешнюю среду исполнения
  • Запуск elinks, авторизация в системе Gmail
  • Переход на страницу создания письма, заполнение соответствующих полей письма
  • Отсылка формы с последующей отправкой письма

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

Нам понадобится:

  • PHP в связке с любым HTTP сервером
  • APD плагин для PHP (для перехвата базовой ф-ции mail())
  • Python с модулем pyexpect (для ведения диалога с процессом elinks)
  • Непосредственно, сам elinks с поддержкой скриптинга на языке Lua
  • Пакет sudo для того, чтобы дать процессу elinks права на создание необходимых для его работы socket-ов

Если будет необходимость в описании процесса установки всего необходимого, напишите в комментариях, но я беру на себя смелость предполагать, что тот, кто озаботился таковой проблемой уже знаком с процессом установки и настройки приложений.

Берем быка за рога

Перехват встроенной в php функции mail(), вывод аргументов ф-ции mail() во внешнюю среду исполнения

К любой постоянно включаемой области страницы вашего проекта, дописываем следующий код:

function my_mail($args) 
{

	$result = $args[0] . "n";
	$result .= $args[1] . "n";
	$result .= $args[2];
	$file = '/tmp/msg.exchange';
	$current = $result . "n";
	file_put_contents($file, $current);
	fclose($file);
	exec('/usr/local/bin/sudo /bin/csh /root/exec.sh > /dev/null &');
	return 1;
} 

override_function('mail', '', 'my_mail(func_get_args());'); 

Здесь такие аргументы ф-ции mail(), как «Кому», «Тема», «Тело» сохраняются в хранимый в памяти файл "/tmp/msg.exchange". Далее происходит вызов основного тела скрипта с перенаправлением потока stdout в null устройство, чтобы загрузка страницы происходила сразу, не дожидаясь окончания исполнения скрипта.

Сразу стоит отметить назначение прав в файле "/usr/local/etc/sudoers":

Defaults:www !requiretty
www ALL=(ALL) NOPASSWD: /usr/local/bin/elinks, /usr/local/bin/python, /usr/bin/su, /bin/csh

Внимание! строка «Defaults:www !requiretty» необходима, т. к. если ее не будет, вы будете получать ошибку в логе вашего веб-сервера о том, что запуск приложения в headless режиме без выделения отдельного tty устройства невозможен.

Запуск elinks, авторизация в системе Gmail

Для корректной работы elinks с русским языком, понадобится небольшая настройка:

mkdir ~/.elinks
printf 'set terminal.xterm.charset = "koi8-r"nset ui.language = "Russian"nset document.browse.search.regex = 0' >> ~/.elinks/elinks.conf

Как таковой авторизации при каждом запуске нам не понадобится. С этим справится сам elinks. Вполне достаточно один раз авторизоваться на странице «elinks mail.google.com/mail/u/0/?ui=html&zy=c», и далее эти данные будут использоваться во всех последующих сессиях.

Далее, поскольку у нас уже есть вызов sh скрипта, приведу его код:

/root/exec.sh

#!/bin/sh
su -l
setenv LANG ru_RU.KOI8-R
/usr/local/bin/python /root/headless.py

Непрямой вызов python скрипта связан с некоторыми особенностями работы механизмов смены прав и особенностями работы кодировок в headless режиме.

Переход на страницу создания письма, заполнение соответствующих полей письма, отсылка формы с последующей отправкой письма

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

Заполнение соответствующих полей письма

Скриптование пользовательских функций — весьма удобный механизм в elinks. Он позволяет выполнять некие макросы в ответ на возникновение некоторых событий. В частности, событие pre_format_html_hook возникает тогда, когда браузер уже загрузил содержимое страницы, но перед началом ее интерпретации из HTML вида в тот вид, в котором мы его увидим. Но, как сказано в документации к Lua части скриптования elinks, не все механизмы Lua вполне корректно работают в рамках взаимодействия Lua<->elinks. Например, при попытке вызовов встроенных в Lua функций чтений фалов, оба исполняемых файла ведут себя непредсказуемым образом, так что приходится обходить это ограничение, путем использования специально для этого написанной функции pipe_read, которая по сути есть простой приемник потока stdout от исполнения любого бинарного elf кода. Так же, стоит отметить момент с кодировками: по сути ничего сложного, но работать с полученными HTML строками приходится в той кодировке, в которой они пришли, потому строки поиска будут выглядеть совершенно нечитаемо.

Все пользовательские скрипты располагаются в папке ~/.elinks и именуются hooks.lang, где lang — это имя языка скриптования. В моем случае это ~/.elinks/hooks.lua, содержимое которого ниже и приводится:
/root/.elinks/hooks.lua

function pre_format_html_hook (url, html)
  toaddress = pipe_read("head -n 1 /tmp/msg.exchange")

  nstr = pipe_read("cat /tmp/msg.exchange | tail -n+2 | /usr/local/bin/iconv -f windows-1251 -t koi8-r | /usr/local/bin/iconv -f koi8-r -t utf-8")
  theme = pipe_read("head -n 2 /tmp/msg.exchange | tail -n+2 | /usr/local/bin/iconv -f windows-1251 -t koi8-r | /usr/local/bin/iconv -f koi8-r -t utf-8")

  html1 = string.gsub (html, 'aria%-labelledby=l%-to>', 'aria%-labelledby=l%-to>' .. toaddress)
  html1 = string.gsub (html1, 'input name=subject value=""', 'input name=subject value="' .. theme .. '"')
  html1 = string.gsub (html1, 'aria%-label="п╒п╣п╩п╬ п©п╦я│я▄п╪п╟">', 'aria%-label="п╒п╣п╩п╬ п©п╦я│я▄п╪п╟">' .. nstr)
  return html1
end

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

Переход на страницу создания письма, отсылка формы с последующей отправкой письма

Этот этап был, пожалуй, самым сложным. Перепробовав все средства в попытках отправить уже порожденному процессу спецсимволы, такие как Enter и нажатие клавиши «вправо», наткнулся на замечательную библиотеку для python, под названием pyexpect. С ней все моментально стало очень просто и понятно. Просто приведу код:
/root/headless.py

#!/usr/bin/python
# -*- coding: koi8-r -*-
from pexpect import spawn
import time
import datetime
import base64
import pickle


'''
fd = open('/tmp/msg.exchange', 'r+')

with fd as f:
    lines = f.read().splitlines()


theme = lines[1]
theme = theme.replace("=?windows-1251?B?", "")
theme = theme.replace("?=", "")

lines[1] = base64.b64decode(theme)

fd = open('/tmp/msg.exchange', 'r+')
fd.truncate()

for item in lines:
  fd.write("%sn" % item)
fd.close()
'''

#KEY_UP = 'x1b[A'
#KEY_DOWN = 'x1b[B'
#KEY_RIGHT = 'x1b[C'
#KEY_LEFT = 'x1b[D'
#KEY_ESCAPE = 'x1b'
#KEY_BACKSPACE = 'x7f'

child = spawn('/usr/local/bin/elinks  https://mail.google.com/mail/u/0/?ui=html&zy=c')

#child.logfile = open('/tmp/elinks.log', 'r+')

print 'waiting for gmail.com to load'

child.expect('ORGNAME')
time.sleep(0.5)
child.sendline('/аписать')
print 'search of "new message" string has been reached'
child.sendline('')
print 'the enter key after searching "new message" button has been emulated'
child.expect('тправить')
print 'weve got "send" string back from server'
child.sendline('/тправить')
print 'search of "send" string has been reached'
child.sendline('')
print 'emulated enter key for submitting completed form'
child.sendline('')
print 'emulated enter key for accepting dialog'
child.expect('тправить')
print 'got signal about sucssefull message sending'
#child.interact()
time.sleep(2)
child.sendline('q')
print 'sent quit key emulation'
child.sendline('')
print 'accepted quit with enter key'

Здесь я намеренно оставил много интересных закомментированных строк. Многострочный комментарий это код, специфичный для движка Bitrix. Дело в том, что битрикс с его системой почтовых шаблонов автоматически кодирует строку с темой письма в base64 кодировку. В этом куске кода происходит расшифровка из base64 в plain text, с последующей записью обратно в файл обмена. KEY_UP, KEY_DOWN, и т. д. — это коды спецсимволов для соответствующих клавиш, вверх, вниз и т. д. Строка «child.logfile = open('/tmp/elinks.log', 'r+')» весьма и весьма полезна для отладки скрипта в headless режиме. Строка «child.interact()» полезна при отладке скрипта в режиме обычного исполнения из консоли.

Для использования этого скрипта «as is», замените ORGNAME на название вашей организации в том виде, в котором оно отображается в шапке Gmail.

Касательно ограничений данного метода, да, их не мало. Во-первых скорость. На отправку каждого письма уходит весьма солидное количество времени и вычислительных ресурсов. Нет возможности отправки красиво отформатированных HTML писем. Скрипты требуют доработки для отправки приложений.

Но и плюсов достаточно много, например, данным метод может использоваться даже в том случае, если у Вас динамический IP адрес, работающий, скажем, в связке с No-IP. Но, пожалуй, главное преимущество данного метода, это полная имитация ручного ввода в веб-интерфейс Gmail, а как следствие спам-фильтрация будет работать исключительно с текстом письма, а те с формальными признаками отправителя. В моем случае, письма, отправленные таким методом приходят не просто в инбокс, а в инбокс с пометкой важно.

Спасибо за внимание, товарищи Хабровчане, успешных вам доставок.

Автор: YMA_HET

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js