Напишите свой собственный почтовый сервер (на Rust!)

Пошаговое руководство по реализации и развертыванию собственного одноразового почтового сервера с нуля.

Напишите свой собственный почтовый сервер (на Rust!)

Как лояльный пользователь 10minutemail, я всегда задавался вопросом, насколько сложно будет внедрить и настроить свой собственный одноразовый сервер электронной почты — сервер, используемый исключительно для размещения вашего адреса электронной почты на одном из тех мы хотели бы узнать о вас больше. прежде чем мы позволим вам загрузить наше программное обеспечение с открытым исходным кодом», регистрация на пробные периоды и локальные тесты для других моих проектов выходного дня. Как оказалось, внедрение такого сервера — это на удивление гладкий и приятный опыт!

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

О SMTP

Простой протокол передачи почты (SMTP) старый. Я также был удивлен, обнаружив, что для такого проверенного и надежного протокола не так много ресурсов, чтобы узнать о его деталях, особенно если вы хотите реализовать его самостоятельно. Фактически, все руководства по созданию собственного SMTP в основном начинаются с установки Postfix — полноценного SMTP-сервера производственного уровня! Вместо этого давайте начнем с основ.

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

Пример диалога между клиентом и сервером может выглядеть так:

Server->Client: 220 edgemail server reporting for duty 🫡
Client->Server: HELO smtp.example.com
Server->Client: 250-smtp.example.com Hello user
Client->Server: MAIL FROM:<[email protected]>
Server->Client: 250 Ok
Client->Server: RCPT TO:<[email protected]>
Server->Client: 250 Ok
Client->Server: DATA
Server->Client: 354 End data with <CR><LF>.<CR><LF>
Client->Server: From: "Robert <[email protected]>
Client->Server: To: "Rosie <[email protected]>
Client->Server: Date: Wed, 19 Apr 2023, 12:30:34
Client->Server: Subject: Hi Rosie!
Client->Server: Hi Rosie! How are you today?
Client->Server: <CR><LF>.<CR><LF>
Server->Client: 250 Ok
Client->Server: QUIT
Server->Client: 221 Bye

Этого простого примера достаточно, чтобы начать набрасывать реализацию сервера, который будет довольно простым конечным автоматом, способным обрабатывать несколько команд: приветствие пользователя и получение почты. Отправка почты более сложна и не будет рассматриваться в этой статье. К счастью, нам не нужно когда-либо отправлять почту с нашего одноразового сервера — он будет использоваться только как одноразовый адрес, используемый для регистрации на нежелательные информационные бюллетени.

Выполнение

Название проекта — edgemail, что является данью уважения тому факту, что он обеспечивает низкую задержку для своих пользователей благодаря использованию собственной базы данных Edge. Об этом в нескольких абзацах!

Сервер будет реализован как 3 отдельных слоя:

  1. Конечный автомат SMTP, отвечающий за обработку связи SMTP.
  2. База данных для хранения почты
  3. Клиент для просмотра почты

Конечный автомат SMTP

SMTP — довольно сложный стандарт, но, к счастью, для одноразовой электронной почты мы можем игнорировать все расширения, аутентификацию, авторизацию и т. д., сосредоточившись только на возможности получать почту. На (слегка упрощенном) рисунке ниже показано, как будет работать наш сервер.

Как только соединение установлено, сервер представляет себя, а затем выполняет рукопожатие, принимая сообщение HELO или EHLO (расширенное приветствие). После выполнения рукопожатия сервер ожидает команду MAIL, содержащую информацию о том, кто отправил электронное письмо, за которой следуют команды RCPT с информацией обо всех получателях. Когда отправитель и получатели известны, сервер принимает команду DATA, которая позволяет ему получить тело письма. После этого выполняется процедура прощания, и транзакция выполняется.

Исходный код конечного автомата находится здесь.

TCP-сервер

Когда у нас есть конечный автомат SMTP, пришло время подключить его к TCP-серверу. Работа сервера будет предельно простой:

  1. Принять новое подключение
  2. Отправить SMTP-приветствие
  3. Получить SMTP-команду
  4. Обработайте команду с помощью нашего конечного автомата SMTP.
  5. Верните ответ обратно пользователю
  6. Если почта была получена, сохранить ее в базе данных

С крейтом Tokio реализация такого TCP-сервера станет легкой задачей.

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

Исходный код сервера находится здесь.

База данных

Для хранения всей почты мы будем использовать Turso — нативную базу данных SQL. Turso может получать запросы через HTTP, что позволяет выполнять запросы прямо из браузера, что делает ответы невероятно быстрыми, как вы увидите позже в разделе Производительность.

Кроме того, клиент Rust для Turso, libsql-client, также способен хранить все в базе данных libSQL, которая хранится в локальном файле, как и SQLite. И это просто идеально для тестов и быстрого прототипирования.

Чтобы создать новую базу данных, начните с установки нашего инструмента командной строки turso.

Когда вы закончите, создайте новую базу данных — назовем ее edgemaildb — с помощью:

$ turso db create edgemaildb

Схема хранения почты будет простой, вся почта будет храниться в одной таблице. Эта таблица будет автоматически создана при запуске сервера Edgemail, если она не существует:

CREATE TABLE IF NOT EXISTS mail (date text, sender text, recipients text, data text)

Для ускорения запросов к таблице mail можно создать следующие индексы. Они также будут автоматически созданы при запуске Edgemail, если их нет:

CREATE INDEX IF NOT EXISTS mail_date ON mail (date)
CREATE INDEX IF NOT EXISTS mail_recipients ON mail (recipients)

Подключение к базе данных из Rust

Turso совместим с драйвером libSQL Rust. Для работы водителю нужны только две части информации:

  1. URL-адрес базы данных.
  2. Маркер аутентификации, который требуется только при подключении к удаленному экземпляру Turso.

Они могут быть указаны как переменные среды. Следующая конфигурация будет подключаться к экземпляру Turso:

LIBSQL_CLIENT_URL=https://your-db-name-and-username.turso.io
LIBSQL_CLIENT_TOKEN=your-auth-token

… но вы также можете указать локальный файл для использования в целях разработки — не нужно ни на что подписываться!

LIBSQL_CLIENT_URL=file:///tmp/edgemail-test.db

В edgemail ситуация еще проще. Если URL-адрес не указан, сервер автоматически запустится с локальной базой данных, хранящейся в файле edgemail.db, помещенном во временный каталог вашей системы:

Когда вы перейдете к рабочей среде и сохраните электронные письма на периферии, достаточно указать переменную LIBSQL_CLIENT_URL на URL-адрес вашей базы данных Turso, а LIBSQL_CLIENT_TOKEN — на токен аутентификации.

Вы можете проверить URL-адрес вашей базы данных, выполнив следующую команду:

turso db show --url edgemaildb

Чтобы сгенерировать токен авторизации, выполните следующую команду:

turso db tokens create edgemaildb

Клиент

Поскольку Turso использует HTTP, нашим клиентом будет статическая веб-страница, которую можно бесплатно разместить во множестве мест, например на страницах GitHub. Если вы беспокоитесь об утечке ваших токенов доступа к базе данных после публикации на веб-странице — не беспокойтесь. С помощью Turso вы можете генерировать токены только для чтения для своей базы данных:

turso db tokens create edgemaildb --read-only

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

Клиент будет разделен на две страницы: лендинг для выбора имени пользователя и почтовый ящик.

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

Как только имя пользователя выбрано, пользователи переходят на страницу входящих сообщений, где и происходит все волшебство.

Страница «Входящие» извлекает всю почту, адресованную данному пользователю, непосредственно из базы данных, по сути, отправляя следующий SQL-запрос:

Этот запрос является операцией SELECT, поэтому для него требуется только доступ к базе данных только для чтения. Это позволяет нам использовать токен аутентификации только для чтения, который можно безопасно «слить» в исходный HTML-код веб-страницы.

Исходный код

Весь исходный код, включая сервер, клиент и тесты, открыт и доступен здесь. Наслаждаться! Чтобы запустить сервер, просто введите cargo run. Тесты можно запускать с помощью cargo test.

Развертывание

SMTP-сервер

Теперь, когда сервер реализован, пришло время сделать его общедоступным. Для этого вам понадобится любая машина с общедоступным открытым IPv4-адресом и портом 25, открытым для входящего трафика. Технически SMTP прекрасно работает и на IPv6, но на практике почтовые провайдеры часто отказываются работать с IPv6-серверами для защиты от спама. Адресное пространство IPv6 просто слишком широкое и слишком нерегулируемое, чтобы его можно было так же легко проверить, как старый добрый IPv4, поэтому лучше придерживаться этого. SMTP-серверы часто прослушивают соединения с другими номерами портов, включая 465 и даже 2525, но, поскольку 25 поддерживается всеми почтовыми провайдерами, этого достаточно.

Сервер Edgemail весит менее 5 МБ и отлично работает на моем старом Raspberry Pi 2 — пока он доступен через общедоступный IPv4-адрес на порту 25, все готово.

DNS-конфигурация

Чтобы стать поставщиком почтовых ящиков, вам нужен домен. К счастью, с таким количеством доступных доменов верхнего уровня вы можете получить их по действительно доступной цене — я зарегистрировал домен idont.date несколько лет назад в Porkbun менее чем за 2 доллара в год.

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

  1. Запись A, указывающая на IP-адрес вашего сервера, например
    [ A ][ smtp.idont.date ][ 3.143.135.0 ]
  2. Запись MX (обмен почтой), которая указывает на запись A выше, например,
    [ MX ][ idont.date ][ smtp.idont.date ]

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

Вот и все! После настройки сервера и DNS сервер готов к приему почты. Вы можете попробовать это, отправив электронное письмо на свой новый домен или подписавшись на рассылку новостей, которую вы всегда так ненавидели.

Производительность

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

С Turso вы можете создать базу данных в одном из нескольких мест, охватывающих почти все континенты (я хотел бы лично извиниться перед всеми жителями Антарктиды, но я уверен, что у вас есть более серьезные проблемы с вашим климатом). Как только база данных будет запущена, вы можете создать несколько реплик, чтобы переместить данные ближе к вашим конечным пользователям. Давайте посмотрим, как изменится воспринимаемая задержка, когда реплика появится ближе к вам.

В настоящее время я житель Варшавской агломерации в Польше. Сразу после создания базы данных в районе Ньюарк, штат Нью-Джерси (turso db create — location ewr mail_db), время загрузки моего почтового ящика [email protected] далеко не велико. Вот скриншот консоли разработчика в моем браузере, отслеживающий временную шкалу выполнения команды SQL через HTTP:

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

А вот как выглядит временная шкала после репликации базы данных в центр обработки данных Equinix в Варшаве (turso db replicate mail_db waw):

Это не опечатка, запрос был обработан за 5,87 миллисекунды. Возможно, это связано с тем, что мой ПК находится менее чем в 1 километре (а значит, и менее 1 мили, удобно для любителей всех систем измерения) от дата-центра Equinix, где обрабатывался запрос. Но в этом весь смысл передачи данных на край! Последняя миля имеет значение, и если вы разместите свои данные на периферии, реплицируя их во многие местоположения, вы улучшите работу для все большего числа пользователей.

Что дальше?

Попробуйте Турсо сами, я уверен, вы получите такое же удовольствие, как и я! У Turso есть всегда бесплатный уровень с ограничениями, которых вполне достаточно для одноразового почтового сервера. И не забудьте теперь всегда использовать https://sorry.idont.date для всех одноразовых писем!