Насколько сложно построить полноценный сервис email-маркетинга? Что для этого нужно предусмотреть? Какие подводные камни могут встретиться на пути пытливых умов разработчиков?
Давайте попробуем разобраться вместе. В рамках нескольких статей я расскажу о том, как я за год сделал свой собственный сервис email-рассылок, какие уроки для себя извлек и что планирую со всем этим делать дальше.
Сразу оговорюсь, что в статье рассмотрена только техническая сторона вопроса.
Кратко о себе
Я пишу на Python вот уже 5 лет, в основном использую Django, PostgreSQL, умею готовить JavaScript на уровне jQuery + KnockoutJS. В свободное от основной работы время занимаюсь фрилансом и собственными интернет-проектами, об одном из которых сейчас и планирую рассказать. Занимаюсь я этим проектом уже около года.
Цель проекта
В самом начале мною была поставлена достаточно простая цель — создать работающее решение для отправки транзакционных писем и email-рассылок с функциями отслеживания открытий, переходов, невозможности доставки письма, жалоб на спам. Использовать я это решение планировал в других своих проектах, поскольку Яндекс ПДД (почта для домена), которую я использовал до этого, такими функциями не обладала, а они были нужны.
Тогда еще не шла речь о том, чтобы дать это решение в виде SaaS всем пользователям в Интернете.
Задачи
- Понять, как работает отслеживание событий в email-рассылках, разобраться с трекингом.
- Придумать решение, которое будет работать под средними нагрузками (2-3 миллиона писем в месяц). Почему именно 2-3 миллиона? Я считаю, что такой объем необходим, чтобы окупать такой проект (затрачиваемое время + материальные ресурсы типа серверов).
- Реализовать удобный интерфейс для аналитики массовых и транзакционных рассылок.
Далее я постараюсь более-менее подробно остановиться на том, как я выполнил каждую из этих задач.
Технологии
Использовать я решил те технологии, которые знаю — Python, Django, PostgreSQL, KnockoutJS, LESS, py.test.
Дополнительно в процессе работы над проектом я неплохо разобрался в Celery и микросервисной архитектуре.
На этом я предлагаю закончить вступительную часть и перейти к самому интересному — практике.
Как работает трекинг email-сообщений?
Когда вы отправляете почту, вы наверняка хотите знать, дошли ли ваши письма адресатам, прочитали ли они их вообще, интересны ли они им, перешли ли они по ссылкам в письме, что после этого они делали на сайте, совершили ли целевое действие — покупку, заказ, звонок и так далее.
Получить ответы на эти вопросы вы можете только с помощью трекинга или систем типа Яндекс.Метрики (ну или спросив своих получателей лично).
Отслеживание открытий писем
Сегодня стандартным подходом для отслеживания открытий писем является внедрение специального пикселя в письмо — вы можете увидеть этот пиксель в большей части рекламных писем в вашем почтовом ящике, если посмотрите исходники письма. Он может выглядеть примерно так:
<img src="http://api.mailhandler.ru/message/track/<UNIQUE_EMAIL_ID>/OPENED/" width="1px" height="1px" border="0"/>
Понятно, что при запросе на URL, указанный в атрибуте src изображения, должно происходить добавление события, означающего, что письмо с id равным UNIQUE_EMAIL_ID было открыто.
Однако не всё так просто. Очень часто в src изображения указывают URL, ведущий на какой-либо php-скрипт и не думают о том, что почтовый сервис очень хочет получить в ответ валидные для изображения заголовки, а так же само изображение. Если почтовый сервис по этой причине разочаровывается в вашем пикселе, он просто вырежет его из письма и вы не узнаете о том, открыл ваш адресат письмо или нет.
Для того, чтобы этого не произошло, следует добавить корректные заголовки ответа и отдать валидное изображение клиенту. Реализация на Django Rest Framework может выглядеть примерно так:
class TrackMessageView(APIView):
renderer_classes = [JPEGRenderer]
@property
def pixel(self):
return open(os.path.join(settings.STATIC_ROOT, 'site/img/pixel.jpg'), 'rb')
def get(self, request, *args, **kwargs):
manager = BaseManager()
message = manager.get_message_by_unique_id(self.kwargs['unique_id'])
if message:
manager.track_message(message)
return Response(self.pixel.read(), status=201)
return Response(status=404)
Отслеживание переходов по ссылкам в письме
Думаю, у пытливого читателя не должно возникнуть проблем с реализацией такого типа трекинга. В общем и целом — каждая ссылка в письме заменяется на ссылку через специальный сервис перенаправления, который создает событие типа «Переход по ссылке». Дополнительно можно к каждой ссылке добавлять уникальный идентификатор — тогда вы сможете реализовать «тепловую карту» письма. Это очень полезная функция, например, для А/Б тестирования.
Реализация на Python выглядит достаточно просто:
REDIRECT_URL_TEMPLATE = '%s/message/redirect/%s/'
HREF_REGEXP = r'(?<=href=("|'))(http|https)([^"']+)(?=("|'))'
...
def replace_links(message):
redirect_url = REDIRECT_URL_TEMPLATE % (settings.API_URL, message.unique_id)
message.html_body = re.sub(HREF_REGEXP, r'%s?next=23' % redirect_url, message.html_body)
...
Отслеживание невозможности доставки писем
А вот с этим все намного интереснее.
Каждый раз, когда почтовый сервер не может доставить ваше письмо, в ответ на адрес отправителя уходит отчет о невозможности доставки с описанием причины (иногда подробным, иногда — так себе). Для обработки этих входящих писем я использовал подход, который заключается в пробросе входящего письма на Python скрипт обработчика через /etc/aliases.
Пример кусочка письма для анализа:
Final-Recipient: rfc822; ****@****.ru
Original-Recipient: rfc822; ****@****.ru
Action: failed
Status: 4.4.1
Diagnostic-Code: X-Postfix; connect to ****.ru[xx.xx.xxx.xxx]:25: Connection refused
Сам скрипт пытается более-менее интеллектуально понять причину невозможности доставки письма и создает событие Soft-Bounce (письмо в данный момент не может быть доставлено, но вы сможете попробовать еще разок) или Hard-Bounce (письмо не будет доставлено никогда, например потому что ящик не существует).
Тут важно сделать небольшую ремарку о том, как собственно нужно реагировать на такие события согласно правилам почтовых сервисов типа Mail.ru, Yandex и др.
сервисы, осуществляющие рассылки на основе подписки, должны безусловно удалять из базы подписчиков или принимать меры по приостановке рассылок на адреса, которые генерируют ошибку протокола SMTP: 550 user not found (отслеживание валидности базы получателей — необходимое условие для поддержания положительной репутации рассыльщика);
Таким образом, мне необходимо было предусмотреть «выключение» подписчиков, на адреса которых невозможно доставить почту. В итоге я пришел к тому, что выключаю подписчика из всех списков подписчиков на сервисе.
Ну вот, с трекингом вроде бы разобрались.
Немного статистики
В данный момент через мой сервис отправляется около 150 000 писем в месяц. Много это, или мало? Наверное мало, учитывая объемы, которые я себе задал в рамках обозначенных задач.
Из них:
- 20% — открыты (это достаточно большой процент, на самом деле, спасибо транзакционной почте)
- 13% — переходы по ссылкам
- 9% — Hard/Soft bounce
P.S.
В следующих статьях я расскажу о том, как и чем я обрабатываю эти данные, расскажу о тонкостях использования celery в подобных проектах, а так же остановлюсь на том, что я планирую делать с этим сервисом дальше.
Спасибо за внимание!
Автор: astrikovd