Серверная архитектура DebugMail

debugmail-logo

На днях мы запустили DebugMail - сервис, который помогает упростить тестирование email-рассылок на этапе разработки сайта/приложения/etc. Сервис позволяет сразу после регистрации получить свой виртуальный smtp-сервер, просматривать отправляемые письма, совместно работать над рассылками вместе с коллегами. В этом посте я расскажу про то, как разрабатывался backend сервиса, какие технологии использовались и какие проблемы приходилось решать. Думаю пост будет интересен прежде всего тем, кто работает с Node.js.

Основные используемые технологии:

Backend сервиса состоит 3 основных компонентов:

Для того, чтобы сервис было легче масштабировать, каждый из описанных выше компонентов разрабатывался с учетом того, что он может быть запущен отдельно, в том числе на другом сервере. Взаимодействие между компонентами в основном происходит через базу данных и систему обмена сообщениями Faye (для real-time обновлений).

debugmail-design

SMTP-сервер

Необходимо было разработать SMTP-север, который бы не осуществлял доставку письма на целевой хост, а только сохранял его в базе данных. В этом нам отлично помогли модули simplesmtp и mailparser, которые покрывали все базовые функции необходимые для работы smtp-сервера и парсинга писем. Оставалось только прописать нужные обработчики событий и другой специфичный для задачи код.

В сервисе предполагается, что все письма относятся к определенному проекту. При создании проекта пользователь получает параметры подключения к smtp-серверу среди которых есть пара login-password. На основе этих данных smtp-сервер определяет к какому проекту относится то или иное полученное письмо.

REST API

Для взаимодействия клиентского приложения (см. скоро пост про клиентскую часть) с backend было разработано REST API на базе фреймворка express. Выбран был именно этот фреймворк из-за его простоты, скорости работы и гибкости. При помощи поддержки middleware, которые пришли из connect, можно очень просто встраиваться в процесс обработки запросов. Несколько примеров разработанных middleware:

Content negotiation

Для того, чтобы максимально распараллелить работу с front-end разработчиком, было принято решение реализовывать сервис в виде SPA (single page application) и REST API. Далее возник вопрос, как различать запросы, которые отправляет клиентское приложение, от запросов, которые генерируются браузером автоматически (при загрузке страниц) при том, то URL-адреса REST API и клиентского приложения пересекаются. Самым корректным решением нам показалось использование content negotiation. При поступлении запроса срабатывает middleware, который проверяет какой формат ответа является предпочтительным для клиента, на основании значения из заголовка запроса Accept. Клиентское приложение отправляет значение данного заголовка как application/json, что сообщает серверу о том, что нужно обрабатывать запрос, как запрос к REST API и вернуть результат в виде JSON-документа. Во всех остальных случаях предполагается, что запрос приходит от браузера и происходит рендеринг основного шаблона с подключенным клиентским приложением.

Маршрутизация

Изначально было принято решение использовать модуль express-resource, но он оказался недостаточно гибким. Например, нельзя задавать кастомные middleware для определенных маршрутов. Pull Request для решения описанной проблемы висит уже больше года, в итоге от express-resource пришлось отказаться в пользу стандартного способа задания маршрутов в express.

Real-time updates

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

Мы остановились на Faye, т.к. он:

Аутентификация пользователей

Основной сложностью было реализовать аутентификацию клиентов, т.к. сессии, которые используются в основном приложении и REST API, здесь не доступны. Для решения этой проблемы была введена аутентификация при подписке на обновления проекта. Предпологается, что при инициализации подписки на уведомления клиент передает user id, session id и token. token является результатом выполнения хэш-функции к параметрам user id, session id и некоторого закрытого ключа. Чтобы реализовать такой механизм отлично подходят, т.н. extensions, которые позволяют перехватывать сообщения и выполнять дополнительные действия (проверки, дополнительную обработку данных и тд.). В итоге получается примерно такой код:

client.addExtension({
  outgoing: function(message, callback) {
    if (message.channel !== '/meta/subscribe')
      return callback(message);

    message.ext = message.ext || {};
    message.ext.uid = USER_ID;
    message.ext.sid = SESSION_ID;
    message.ext.token = PRIVATE_TOKEN;
    callback(message);
  }
});

Аналогичная система расширений на стороне сервера позволяет выполнять проверку и разрешать подписываться на обновления по проектам только тем пользователям, которые действительно имеют к ним доступ.

Автоматическое тестирование

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

Писать тесты на node.js - одно удовольствие. В основном это так благодаря большому количеству удобных инструментов и модулей в npm. Также помогают особенностям javascript и node.js, которые предоставляют все необходимые возможности для удобного создания моков. Рассмотрим, какие модули использовались и с какой целью:

request(app)
  .get('/user')
  .set('Accept', 'application/json')
  .expect(200)
  .end(function(err, res){
    if (err) return done(err);

    // ... check something here ...

    done()
  });

Стоит отметить, что тесты выполняются очень быстро. На данный момент 320 тестов выполняются примерно за 17с (в один поток).

Некоторые используемые модули

Список некоторых полезных модулей, о которых не упоминал выше: