Як доставляти 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, а як наслідок спам-фільтрація працюватиме виключно з текстом листа, а ті з формальними ознаками відправника. У моєму випадку, листи, відправлені таким методом приходять не просто в інбокс, а в інбокс з позначкою важливо.
 
Спасибі за увагу, товариші Хабровчане, успішних вам доставок.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.