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

Дополнительные поля

Подключение дополнительных полей реализовано через дополнительную таблицу AdditionFields. Это компромиссное решение, которое позволяет упростить проектирование и расширять систему по мере ее необходимости.

Недостатком данного решения является то, что таблица AdditionFields может весьма сильно разрастаться и потенциально, это может создавать проблемы с быстродействием. С другой стороны, огромным плюсом данного решения является возможность легко делать выборки из любой таблицы по значению объектов в дополнительных полях ( конечно это обходится нам в один join). Альтернативно, дополнительные поля можно складывать в поле типа json или сериализованное поле. Однако при этом в mysql выборки будут значительно сложнее.

NB: Если выбрать в качестве базы данных postgresql, то в нем можно будет и складывать в json, и делать выборки по этому полю, и даже построить индексы.

Статусы подписчиков

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

Кроме того, через статусы подписчиков отмечается источник, откуда добавился подписчик в систему. Это можно было сделать через дополнительное поле источник и упрощение системы статусов. Однако использование статусов позволяет менять поведение в зависимости от источника данных (например не слать рекламную информацию тем, кто давал свои контакты на личных встречах) и при этом за поведение системы c подписчиком ответственно только одно поле.

Очередь сообщений

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

Каждая запись в таблице Queue соответствует или одному письму, которое нужно отправить, или одному списку подписчиков конкретной рассылки. В основном все задачи на отправку хранятся данные в списках (это позволяет уменьшить объем БД). Наиболее часто встречается задача разослать письмо по всем, кто находится в определенном списке. Поэтому мы можем:

  • отсортировать всех подписчиков в списке по дате добавления
  • выбрать 100 первых подписчиков и отправить группу писем
  • пометить, что 100 писем из списка были отправлены
  • выбрать еще 100 подписчиков, пропустив первые 100, и отправить еще 100 писем
  • пометить, что 200 писем были отправлены и т.д.

Значит для того, чтобы верно обработать рассылку, нам нужно всего 3 вещи:

  1. шаблон письма, который мы рассылаем
  2. номер списка подписчиков, по которому мы будем рассылать
  3. количество писем из этого списка, которое мы уже отправили.

Таким способом система дойдет до конца списка и разошлет все письма. Если во время рассылки будет добавлен новый подписчик, то он попадет в конец списка и так же получит письмо. Окончанием рассылки является момент, когда во всех очередях отосланы все письма. То есть если запросив из списка очередные 100 подписчиков, система получает только 60, то это значит, что отправка по списку завершена. Тогда система помечает список, отправленным и проверяет все остальные списки связанные с этой рассылкой. Если статус всех очередей «Завершено», то рассылке целиком ставится статус «Завершено».

Сбор статистики

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

Для сбора данных об открытии письма используется прозрачная картинка размером 1х1 пиксель, чей адрес так же проксируется. На самом деле данная реализация для большинства писем является избыточной. Практически в каждое письмо добавляется изображение (например логотип) и, проксируя это изображение, можно получать данные об открытии письма на основе этих данных.

Для сбора данных об недоставленных писем есть как минимум два способа:

  • Периодически открывать почтовый ящик, с которого вы пишете, скриптом и читать оттуда список недоставленных писем, которые вернулись
  • Если вы шлете через почтовый сервер, то парсить логи почтового сервера и брать из них список недоставленных писем

Так как в системе используется postfix, то парсились логи. При работе с логами возникает вопрос, как гарантировано понять письмо из какой рассылки не ушло. В логах вы будете видеть email, но вам нужно еще совместить его с нужной рассылкой. В принципе как один из вариантов «в лоб», если рассылки идут не часто, совмещать по времени. Кроме этого скорее всего, если не дошло одно письмо, то и остальные не дойдут, и можно помечать все письма как «не доставленные».

Есть другой способ, немного более сложный, но значительно более точный. У каждого письма есть заголовок "Message-Id". В этом заголовке содержится уникальный идентификатор письма. Это заголовок postfix, по умолчанию, пишет в свои логи. Если вы подмените заголовок на что-то такое «[email protected]», то при получении списка не доставленных писем получите не только письмо, но и еще рассылку, для которой письмо отправлялось. При этом если сделать заголовок не соответствующий формату, то postfix отбросит его и заменит на нормальный. Таким образом у получателей системный заголовок не будет отображаться.

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