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

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("%s\n" % 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, а как следствие спам-фильтрация будет работать исключительно с текстом письма, а те с формальными признаками отправителя. В моем случае, письма, отправленные таким методом приходят не просто в инбокс, а в инбокс с пометкой важно.

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

Similar posts

AdBlock has stolen the banner, but banners are not teeth — they will be back

More
Ads

Comments 21

    +2
    Хороший ник :)

    1. Почему все не на python, раз уж знакомы
    developers.google.com/gmail/api/quickstart/quickstart-python
    2. Воспользоваться ssmtp? Там конфиг буквально из 5 строк
    3. Даже штатный sendmail умеет слать почту через gmail/бизнес аккаунт.

    Только что проверил с ip без обратной зоны. Все работает, в спам не попало.
      0
      1. Т.к., насколько я могу судить, доверие машинного обучения к API и веб-интерфейсу разное.
      2. Разумеется можно и ssmtp, но после пары отправленных писем с IP без обратной зоны все письма начинают попадать в спам.
      3. Конечно же и sendmail умеет. Да по сути любой почтовый сервер подойдет, но смысл не в этом.

      Все нежелательные эффекты от отсылки через штатные средства без заморочек начинаю проявляться через, примерно, месяц отсылки однотипных уведомлений.
        0
        1. Т.к., насколько я могу судить, доверие машинного обучения к API и веб-интерфейсу разное.

        У Вас же бизнес аккаунт. там уровень доверия впринципе к Вам высок, я не думаю что гугл станет блокировать письма. если вы про механизмы гугла.
          0
          Пару раз блокировал. Пришлось вот таким странным образом выкручиваться.
          0
          А эта система как давно работает?
          Может ответ проще? Например сами пользователи довабляют уведомления в спам или к примеру кто-то прошртрафился с того же ip?
          Наверное у меня не очень большой объем, 3-4 сотни писем в день. Но работает как часы уже польше трех лет. Правда все по делу.
            0
            Ну 3-4 сотни это весьма хороший объём, на мой взгляд. У меня объём ниже. А вариант с добавлением в черный список пытался прорабатывать, создал много почтовых ящиков на том же gmail, и добавлял в доверенных адресатов свой нотификатор, результата так и не было. А работает только пару месяцев. Может только каким-то образом дело в содержании письма, но никакого криминала не рассылалось, стандартные битриксовские уведомления о заказах.
        +3
        Как вариант — дешевый vps для рассылок, за 10-15$.
        С PTR, SPF, DKIM, bulk заголовками, ссылкой на отписку и прочими поэтессами.
        Плюс, очередь писем пусть на нем болтается, а не у вас.
        А gmail может вас заподозрить и заблочить в любой момент за те же однотипные действия.
          0
          Цитата: «Сразу оговорюсь, что механизмы, использованные мной не претендуют на изящество».
          За такие объёмы, которые через мой сервер ходят, вряд ли заблокируют, но тут все же надо будет посмотреть как большая статистика покажет.
          К тому же такое решение полностью бесплатно, возможно кому-то все же пригодится.
            0
            Я понял вас.
            А для чего нужны su, root права и файлы в /root/?
            Не нашел про это в посте.
              0
              Чтобы в случае, если что не так пойдёт, было проще отлаживать. Одни и те же настройки elinks, одни и те же переменные окружения, так куда проще. Если вдруг кто вздумает это пускать на серьезной основе, разумеется от подобных «костылей» лучше избавиться.
          0
          amazon SES?
            +1
            Спасибо, не слышал о таком решении, буду иметь ввиду если что, хорошие цены.
            0
            5 евро в месяц стоит атом в kimisufi. За сравнимые деньги можно иметь виртуалку с большей производительностью. Обычно у таких сервисов нет никаких проблем с ptr'ами, а «избавляться от первого хопа со своего IP» просто нет смысла — почтовые сервера требуют fqdn на этапе HELO, а не на этапе «просмотра заголовков».
              0
              В этой схеме как, такового этапа SMTP диалога нет. Он происходит между сервером гугл и конечным сервером получателя письма. Единственное, где фигурирует IP машины, с которой была осуществлена отправка, так это в поле X-Originating-IP, которое мало влияет на параметры фильтрации.
                0
                Я это понимаю. Я говорю, что нет смысла отдавать это на откуп гуглу (чтобы периодически получать непонятные потери и задержки почты), когда можно просто поднять smart relay в пять минут работы не очень крутого админа. Заметим, в этом случае недоставка почты может отлаживаться человеком по логам, а не по обиженным от пользователей «у меня почта иногда не проходит».
                  0
                  А к чему тогда вот эта фраза: «а «избавляться от первого хопа со своего IP» просто нет смысла — почтовые сервера требуют fqdn на этапе HELO, а не на этапе «просмотра заголовков»»?
                    0
                    Это ответ на фразу «Идея в том, чтобы избавиться от вашего IP, как от начального релея.».
                      0
                      А кого вообще, кроме алгоритмов эвристики, должен волновать первый хоп, болтающийся в заголовке письма? Да и у спам-фильтра обычно этот параметр имеет небольшой вес. Все прекрасно понимают, что UA может отослать письмо из любой помойки. Другое дело MTA. Для него как раз и рекомендуется наличие PTR. И это будет скорее всего первый параметр по которому его отфутболят. Но, опять же «рекомендовано не означает обязательно». Опять же, многие MTA вырезают всю цепочку релеев из заголовка по разным причинам, оставляя только последний — свой.
                      Вобщем, как говаривал Уэф: "Скрипач Первый хоп не нужен, родной"

                      По теме поста: уважаемый, YMA_HET, предложенная вами конструкция пугает меня практической сложностью выявления ошибок в конвейере обработки корреспонденции.
                      Поддержу amarao, традиционный smart relay на недорогом VPS/VDS проще и прозрачнее.
                      О безопасности: я не готов сходу провести аудит кода, но цепочка httpd-php-bash-python-sudo-tty вызывает у меня сильный приступ паранойи.
                      humor mode
                      Придется вечером сходить на собрание анонимных админов-паранойиков. А ведь я был в завязке уже полгода.

                      Уж лучше использовать postfix под непривилегированной учеткой.
                0
                Насколько я могу судить, сейчас за обратный ДНС «впрягается» только пара серверов из DNSBL.
                Все остальные вроде как вполне понимают, что если есть DKIM, SPF и прочие приблуды, значит всё хорошо с большой вероятностью (опыт 7000+ писем в неделю, очень похожих).

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

                P.S.
                Загляните в mandrillap, он умеет слать много, интеллектуально, а главное, логи и уведомления о
                1. Если пользователь нажал «В Спам»
                2. Если письмо не доставлено по какой-то причине
                и пр.
                  0
                  Некоторые особо олдскульные админы в крупных организациях таки просят fqdn/rdns на этапе helo/ehlo, и отфутболивают ваши замечательные письма с ptr/dkim/spf только в путь.
                  Так что rdns обязательно должен быть. Если у вас что-то серьезное, а не так, поиграться )
                    0
                    Наверное, вы поторопились записав PTR к «отфутболиваемым», это же как раз то, что нужно для rDNS.

                    В любом случае, местные банки (как по мне самые критичные ко всему стороннему субьекты) принимают наши емэйлы без проблем. Мы же не MTA, мы прячемся за глобальным игроком Exchange сервисов. Лишь несколько раз было, что DNSBL нас блокировал по причине отсутствия PTR к нашему домену, но это имхо глупость, у MTA свои айпишники, у нас свои, и почти каждый может побывать в X-Originating-IP.

                Only users with full accounts can post comments. Log in, please.