SendGrid нам друг, но истина дороже

    SendGrid logo
    Как уже я уже писал, не так давно мы начали работать с командой одного популярного freemium сервиса, являющегося системой управления проектами. Сервис генерирует довольно большое количество писем, исчисляемое в тысячах в день, это в основном различные уведомления пользователям о произошедших изменениях и ежедневные напоминания о приближении/истечении срока выполнения каких-либо работ. Пользователи также могут отвечать на пришедшие письма и таким образом обновлять данные. Поскольку количество пользователей увеличивается, мы заметили, что увеличиваются и наши счета SendGrid, которые, будучи freemium сервисом, нам хотелось бы минимизировать. Также нам хотелось понять насколько эффективно мы используем почту и не отправляем ли мы часть почты просто в никуда, платя за доставку SendGrid.

    Всё чаще стали возникать вопросы о том, что кто-то не получил какое-то уведомление или его ответ на письмо из сервиса не был им получен и опубликован. Таким образом, второй проблемой была невозможность эффективно заниматься поддержкой пользовательских проблем, связанных с доставкой почты, поскольку постоянно приходилось просматривать и собственную базу, и интерфейс SendGrid. При том, что это занимало уйму времени, это часто не давало никакого результата. После отправки письма по smtp у нас не оставалось никакой связи между записями в нашей базе данных и результатом на стороне SendGrid.

    Аналогичной была ситуация с входящей почтой. Для тех, кто еще не сталкивался, опишу как это работает в SendGrid: письма получаются SendGrid-ом, они парсятся и затем полученные данные отправляются POST-ом по указанному адресу, т.е. вам. Так вот, когда возникает ситуация, что чье-то письмо не было опубликовано в нашем сервисе, непонятно кто виноват — SendGrid не смог его пропарсить или наш скрипт не отработал. Вы поймёте нас, если вы попадали в ситуацию, когда правдивый ответ — «ну не знаю я, блин, почему письмо не пришло» пользователю не напишешь, и проходится только обещать в скором времени разобраться с его вопросом. Чтобы наши обещания не стали ложью, я взялся за дело.

    Итак — у нас возникла необходимость в полном контроле над процессом доставки и получения писем. Текущим положением дел было — отправка (по smtp) и получение почты через SendGrid, полное отсутствие связи наших и SendGrid данных, тысячи писем в день и их экспоненциальный рост, выливающийся во всё большие суммы и отсутствие эффективного механизма решения проблем пользователей. Задача — максимально связать наши данные с данными SendGrid, чтобы сократить расходы и повысить качество и скорость поддержки клиентов.

    Первым делом мы обратились за ответами на наши вопросы к их документации. Оказалось, что у SendGrid есть 2 хука, которые как раз и дают нужный нам контроль над состоянием писем. Первый хук, это Events — SendGrid отправляет на указанный урл уведомления о событиях, генерируемых в их системе. Для идентификации конкретного письма они предлагают использовать механизм уникальных аргументов и категорий, которые могут быть переданы в виде специального заголовка при отправке письма через SMTP. И в дальнейшем уведомления о событиях включают в себя эти аргументы и категории, что позволяет точно определить к какому именно письму относится данное событие. Что это дает? Во-первых, «если повар нам не врет», мы всегда точно знаем, доставлено ли письмо пользователю. Во-вторых, можно полностью или частично автоматизировать процесс повторной отправки писем, которые получили статус «Deferred» или попали в Bounces список. Кроме того, теперь мы быстро узнаем о занесении нового email-а в Bounces список и можем оперативно принимать меры. Есть и дополнительные бонусы, связанные с использованием категорий писем.

    Как было сказано выше, отправляемому письму могут быть назначены категории и согласно документации их может быть до 10. Данные категории используются затем при составлении отчетов в интерфейсе SendGrid, их можно сравнивать друг с другом на симпатичного вида графиках. Правда, к сожалению, у них почему-то только одноуровневая система категорий, а нам было бы удобнее использовать двухуровневый подход. Двухуровневый потому, что первый уровень у нас определяет приложение, отправившее письмо, а второй уровень — действие, которое вызывало его отправление. Например, приложение Todo отправляет письма при создании, редактировании и смене даты окончания записи. Использование второй категории дает нам более полное представление о том, кто и сколько генерирует писем внутри одного приложения. Однако подобные действия есть и в других приложениях, таким образом, сама по себе категория Add не столь интересна для статистики, сколько ее привязка к конкретному приложению. Впрочем, возможно, что кому-то и такой срез статистики будет полезен.

    Несколько слов о реализации: проект написан с использованием CodeIgniter и письма отправляются стандартной библиотекой Email. Однако она не позволяет создавать собственные заголовки при отправке писем по SMTP, следовательно, ее нужно расширить собственным классом. Взяв в качестве примера библиотеку github.com/leonbarrett/CodeIgniter-Sendgrid-API/blob/master/system/libraries/Email.php мы сделали собственную библиотеку, отбросив все ненужное и упростив оставшееся до одного метода :). На самом деле вышло 2 метода, поскольку метод _set_header() объявлен в классе CI_Email как private, что не позволяет его использовать в классах-наследниках.
    Вот полный код библиотеки:
        <?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');
        /**
         * CodeIgniter
         *
         * An open source application development framework for PHP 5.1.6 or newer
         *
         * @package      CodeIgniter
         * @author      Deep Shift Labs Dev Team
         * @copyright      Copyright (c) 2013, Deep Shift Labs
         * @license      http://codeigniter.com/user_guide/license.html
         * @link      http://codeigniter.com
         * @since      Version 1.0
         * @filesource
         */
    
        // ------------------------------------------------------------------------
    
        /**
         * CodeIgniter Email Class
         *
         * Permits email to be sent using Mail, Sendmail, or SMTP.
         *
         * @package   CodeIgniter
         * @subpackage   Libraries
         * @category   Libraries
         * @author   Deep Shift Labs Dev Team
         * @link   http://deepshiftlabs.com
         */
        class MY_Email extends CI_Email {
            /**
            * Add a Header Item
            *
            * @access  private
            * @param   string
            * @param   string
            * @return  void
            */
            private function _set_header($header, $value)
            {
                $this->_headers[$header] = $value;
            }
    
            /**
            * Add a Sendgrid header
            *
            * @access  public
            * @param   array
            * @return  void
            */
            public function addSendGridHeader($data) {
                $xsmtpapi = json_encode($data);
                // Add spaces so that the field can be foldd
                $xsmtpapi = preg_replace('/(["\]}])([,:])(["\[{])/', '$1$2 $3', $xsmtpapi);
    
                $this->_set_header('X-SMTPAPI', $xsmtpapi);
            }
        }
    


    Таким образом, добавление к письму уникальных аргументов и категорий происходит одним вызовом метода addSendGridHeader(), например так:

    $this->email->addSendGridHeader(array('unique_args'=>array('email_id'=>1), 'category'=>array('help', 'question')));
    


    Однако вернемся к хукам, второй хук, Inbound Parse, позволяет получать ответ от пользователей. После соответствующей настройки MX записи домена или поддомена Sendgrid начинает присылать на указанный адрес обработанное письмо с ответом. К нашему удивлению, оказалось, что для входящих писем нет никаких событий, т.е. получить информацию о том, в какой стадии обработки оно находится сейчас невозможно. Остается лишь надеяться, что этот процесс у них отлажен и не дает сбоев, т.е. все, что они получили передается нам, проверить-то все равно невозможно. Исходя из этого нам остается лишь зафиксировать те стадии обработки писем, которые происходят на нашей стороне. У нас получились следующие статусы входящих писем: 'received', 'hash_checked', 'duplicate', 'processed'. Наверняка вас заинтересовал второй статус 'hash_checked', во всяком случае я надеюсь, что это произошло, потому что сейчас я буду рассказывать о нем. Кроме того, что мы не получаем никаких событий о входящих письмах от SendGrid, мы также вообще ничего не получаем, что помогло бы идентифицировать начальное письмо. Ни уникальных аргументов, ни категорий, вообще ничего. Т.е. понять на какое письмо мы получили ответ просто так невозможно. А ведь здорово было бы, если бы SendGrid взял задачу привязки отправленных и полученных писем на себя — мы передаем уникальные заголовки с отправленным письмом и они же возвращаются нам вместе с ответом. Это часть того самого непаханного поля, о котором я писал в предыдущем посте. Но поскольку этого нет, приходится выкручиваться самостоятельно.

    Мы пошли путем использования специального хэша в адресе для заголовка «Reply-To», он выглядит как:
    "db051171af45b683c50eb3d66017ecf2+с@incoming.servicedomain.com"
    
    Этот хэш генерируется таким образом, что мы точно знаем какой записи в системе он соответствует. Это может быть запись Todo, дискуссия или еще что-нибудь. При получении письма мы определяем запись, к которой письмо относится, и если ее удалось определить, письму присваивается статус 'hash_checked'. Далее мы проверяем, не является ли письмо дубликатом, чтобы не создавать несколько одинаковых комментариев. Затем собственно создается новый комментарий и письму присваивается статус 'processed'.

    Одной из задач, которые мы хотели решить, была возможность повторной отправки писем, которые не были доставлены пользователям. Для этого все необходимые данные сохраняются в базу. Поскольку проект написан на CodeIgniter, то таковыми данными являются тело письма и сформированные перед отправкой заголовки, которые включают в себя сабжект, получателей, наши уникальные параметры и прочее. Само сохранение писем было реализовано буквально в несколько строк кода при переопределении метода send() стандартной библиотеки Email. Итак, письма сохраняются, однако нет особого смысла хранить исходные данные писем, которые были благополучно доставлены, верно? Во всяком случае, для нас его нет и потому мы внесли небольшие изменения в скрипт, который получает события от SendGrida — если пришло уведомление о событии 'delivered', то мы удаляем все соответствующие данному письму записи из таблицы. Здесь использовано множительное число не случайно, для ускорения процесса сохранения писем в базу мы не проверяем является ли это первой попыткой его отправить или нет, т.е. в случае нескольких попыток отправить одно и то же письмо в базе будет сохранено несколько его экземпляров. Логическим завершением было добавление действия для повторной отправки писем из базы, с этим сложностей не возникло, хотя и потребовались небольшие изменения упомянутой выше библиотеки для работы с письмами. Если будет интересно, пишите в комментариях и я расскажу какие именно.

    Вот в общих чертах и всё о том, как мы контролируем свою отправляемую и получаемую почту. Запустив эту новую подсистему и поднакопив данных, мы начнем её оптимизировать и автоматизировать те случаи, где решение понимаемо нами и программируется. Если вы уже решали подобную задачу и поделитесь своим опытом в комментариях — буды вам очень признателен.

    P.S. При релизе описанной выше системы автоматизации мы столкнулись с неприятной ситуацией. Наши Production и Pre-production версии системы используют разные аккаунты в SendGrid для того, чтобы тестовые письма не влияли на статистику реальной системы. 15 сентября мы попробовали запустить изменения на Production сервере и оказалось, что почему-то мы не можем получить данные из присылаемых уведомлений о событиях. Недолгое расследование выявило причину — 6 сентября SendGrid запустил третью версию Event webhook-a, в которой был кардинально изменен формат передаваемых данных. В версиях 1 и 2 это были обычные POST переменные, теперь же присылается JSON структура с массивом записей. Довольно странно, что компания SendGrid не предупредила своих платных клиентов о таких существенных изменениях. По закону подлости, приложение Event Notification в аккаунте для Pre-production было активировано до 6-го сентября и соответственно мы тестировали на версии 2 хука. Перед релизом 15-го числа мы активировали уведомления в аккаунте, используемом живой системой, и нас молча подключили уже к версии 3 хука. Таким образом, мы были вынуждены перенести релиз, чтобы изменить и протестировать парсер уведомлений.
    Метки:
    • +5
    • 15,6k
    • 7
    Поделиться публикацией
    Похожие публикации
    Реклама помогает поддерживать и развивать наши сервисы

    Подробнее
    Реклама
    Комментарии 7
    • +2
      Мы как-то тоже пользовались SendGrid, отправляли по 60-100к писем каждый день имея инвойс ближе к $600. Перенесли все за день на Amazon SES и опустили инвойс до $50.
      • 0
        Разве SES поддерживает inbound?
        • 0
          Нет, нам не требуется. Я в другом контексте свои мысли изложил: SendGrid дерет бешеное бабло, в то время как многие из задач можно решить альтернативными путями, дешевле и даже качественнее.
          Кстати, мы используем в том числе и SNS наряду с SES — полет нормальный, ошибки приходят и обрабатываются корректно. Более того, можно совместить SES с SQS, тогда необходимость в первом хуке в общем-то пропадет.
          • 0
            Посмотрел SES — inbound не увидел.
        • +1
          Пользуемся SendGrid уже довольно давно, а пару месяцев назад тоже решили прикрутить их EventAPI. Выяснилось, что у них были (и есть до сих пор) проблемы с нотификациями — SendGrid просто не запрашивает ваш URL в некоторых случаях. Проблема была воспроизведена нами ТРИЖДЫ: на наших серверах, с помощью рекомендуемого SendGrid сервиса KeenIO и с помощью чистого Nginx сервера, запущенного на EC2 на Amazon Web Services. Во всех трех случаях в логах SendGrid видно, что письмо отправилось, а в наших логах нет сведений о том, что информация об этом сообщении была передана нашему хуку (при этом информация о предыдущих/последующих события успешно получена).

          По итогу, соответствующий тикет открыт уже больше двух месяцев, поддержка SendGrid до сих пор ничего определенного сказать не может. Последние новости были 2 недели назад — они пытались получить какие-то логи со своих серверов. Потребовал подключить менеджера команды поддержки, он подключился (Ken Apple), но воз и ныне там.

          TL;DR: Не расчитывайте на
          а) то, что хук будет дергаться на все сообщения
          б) на качественный саппорт.
          • 0
            Не рекламы ради, а по собственному опыту: уже почти год юзаю сервис Mailgun: уровень саппорта и желания помочь сильно выше, чем Вы описываете. Да и хуки на ивенты это их основная функция, так что неожиданных изменений в API не будет.
            Цены вроде как тоже ниже, чем sendgrid.
            Кстати, их полгода назад купил Rackspace (в хорошем смысле этого слова).
            • 0
              Есть ли смысл собрать небольшую команду энтузиастов и создать open source проект, превосходящий коммерческие, как по возможностям так и по уровню поддержки? Мы готовы подключиться — пишите если вам интересно.

              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.