Проверка рабочих процессов электронной почты с помощью API-интерфейса Serverless Inbox

Эта статья была впервые опубликована на bahr.dev. Подпишитесь, чтобы получать новые статьи прямо на почту!

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

Рабочий код готов для размещения на GitHub.

С помощью сервисов AWS Simple Email Service (SES) и API Gateway мы можем создать полностью автоматизированное решение. Его модель ценообразования подходит для большинства тестовых рабочих нагрузок уровня бесплатного пользования и может обрабатывать до 10 000 писем в месяц всего за 10 долларов. Не требует обслуживания или развития. Это также позволяет вам оставаться в песочнице ЕЭП.

Предпосылки

Для развертывания этого решения у вас должен быть аккаунт AWS и некоторый опыт работы с AWS CDK. Я буду использовать вариант TypeScript. В этой статье используется CDK версии 1.63.0. Дайте мне знать, если что-то сломается в новых версиях!

Для получения почты с SES нужен домен или субдомен. Вы можете зарегистрировать домен на Route53 или делегировать от другого провайдера. Вы также можете использовать поддомены, такие как mail-test.bahr.dev, для получения почты, если вы уже подключили свой домен вершины (например, bahr.dev) к другому почтовому серверу.

Общий обзор

Решение состоит из двух частей. Получатель электронной почты и API, который позволяет вам получить доступ к полученной почте. Первый пишет в БД, второй читает из нее.

Для получателя электронной почты мы используем SES с Правилами получения. Мы используем эти правила для хранения необработанной полезной нагрузки и вложений в корзине S3 и отправляем правильно сформированную полезную нагрузку в функцию Lambda, которая создает запись в таблице DynamoDB.

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

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

Подтвердить домен с помощью SES

Чтобы получать почту, вы должны управлять доменом, который вы можете зарегистрировать в SES. Это также может быть субдомен, например. если вы уже используете свой домен вершины (например, bahr.dev) для другой почтовой службы, такой как Office 365.

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

После того, как у вас есть размещенная зона для вашего домена, перейдите в Управление идентификацией домена в SES и подтвердите новый домен. Также есть короткое видео, где я верифицирую домен с помощью SES.

Модель данных

Мы будем использовать ключи разделов и сортировки DynamoDB, чтобы включить две основные функции: получение почты для многих псевдонимов и получение более одного письма для каждого псевдонима. Псевдоним — это front-part в [email protected].

partition_key: [email protected]
sort_key: timestamp#uuid
ttl: timestamp

Комбинируя временную метку и uuid, мы можем сортировать и фильтровать по временной метке, а также гарантировать, что никакие две записи не будут конфликтовать друг с другом. TTL помогает нам сохранить размер таблицы, позволяя DynamoDB удалять старые записи.

Я использую dynamodb-toolbox Джереми Дейли для моделирования объектов моей базы данных.

import { Table, Entity } from 'dynamodb-toolbox';
import { v4 as uuid } from 'uuid';
// Require AWS SDK and instantiate DocumentClient
import * as DynamoDB from 'aws-sdk/clients/dynamodb';
const DocumentClient = new DynamoDB.DocumentClient();
// Instantiate a table
export const MailTable = new Table({
  // Specify table name (used by DynamoDB)
  name: process.env.TABLE,
  // Define partition and sort keys
  partitionKey: 'pk',
  sortKey: 'sk',
  // Add the DocumentClient
  DocumentClient
});
export const Mail = new Entity({
    name: 'Mail',
  
    attributes: {
      id: { partitionKey: true }, // recipient address
      sk: { 
        hidden: true, 
        sortKey: true, 
        default: (data: any) => `${data.timestamp}#${uuid()}` 
      },
      timestamp: { type: 'string' },
      from: { type: 'string' },
      to: { type: 'string' },
      subject: { type: 'string' },
      ttl: { type: 'number' },
    },
  
    table: MailTable
  });

Получатель

SES позволяет нам настроить ReceiptRules, которые запускают действия при поступлении новой почты. Есть несколько действий на выбор, но нас больше всего интересуют действия Lambda и S3. Мы используем действие Lambda для хранения таких сведений, как получатель, отправитель и тема, в таблице DynamoDB. С помощью действия S3 мы получаем необработанное электронное письмо, доставляемое в виде файла в корзину. Это будет удобно в дальнейшем для поддержки других вариантов использования, таких как повторный возврат тела письма и вложений.

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

import * as cdk from '@aws-cdk/core';
import { Bucket } from '@aws-cdk/aws-s3';
import { Table } from '@aws-cdk/aws-dynamodb';
import { Function } from '@aws-cdk/aws-lambda';
import { ReceiptRuleSet } from '@aws-cdk/aws-ses';
import * as actions from '@aws-cdk/aws-ses-actions';
export class InboxApiStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    // your-domain.com
    const domain = process.env.INBOX_DOMAIN; 
    const rawMailBucket = new Bucket(this, 'RawMail');
    const table = new Table(this, 'TempMailMetadata', {
        ...
    });
    const postProcessFunction = new Function(this, 'PostProcessor', {
        ...
        environment: {
            'TABLE': table.tableName,
        }
    });
    table.grantWriteData(postProcessFunction);
    // after deploying the cdk stack you need to activate this ruleset
    new ReceiptRuleSet(this, 'ReceiverRuleSet', {
      rules: [
        {
          recipients: [domain],
          actions: [
            new actions.S3({
              bucket: rawMailBucket
            }),
            new actions.Lambda({
              function: postProcessFunction
            })
          ],
        }
      ]
    });
  }
}

Имея приведенный выше код CDK, давайте взглянем на функцию Lambda, которая запускается при поступлении новой почты.

import { SESHandler } from 'aws-lambda';
// the model uses dynamodb-toolbox
import { Mail } from './model';
export const handler: SESHandler = async(event) => {
    for (const record of event.Records) {
        const mail = record.ses.mail;
    
        const from = mail.source;
        const subject = mail.commonHeaders.subject;
        const timestamp = mail.timestamp;
        const now = new Date();
        // set the ttl as 7 days into the future and 
        // strip milliseconds (ddb expects seconds for the ttl)
        const ttl = now.setDate(now.getDate() + 7) / 1000;
        for (const to of mail.destination) {
            await Mail.put({
                id: to, timestamp,
                from, to,
                subject, ttl
            });
        }
    }
}

Приведенная выше функция сопоставляет событие SES с одной записью для каждого получателя и сохраняет их вместе с атрибутом TTL в базе данных. Вы можете найти полный исходный код на GitHub.

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

API чтения

API чтения состоит из шлюза API и функции Lambda с доступом для чтения к таблице DynamoDB. Если вы еще не создавали такой API, рекомендую вам посмотреть видео Марсии о том, как создавать бессерверные API.

Ниже вы можете увидеть сокращенный код CDK для настройки шлюза API и функции Lambda. Вы можете найти полный исходный код на GitHub.

import * as cdk from '@aws-cdk/core';
import { LambdaRestApi } from '@aws-cdk/aws-apigateway';
import { Table } from '@aws-cdk/aws-dynamodb';
export class InboxApiStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);
    const table = new Table(this, 'TempMailMetadata', {
        ...
    });
    const apiFunction = new Function(this, 'ApiLambda', {
        environment: {
            'TABLE': table.tableName,
        }
    });
    table.grantReadData(apiFunction);
    new LambdaRestApi(this, 'InboxApi', {
      handler: apiFunction,
    });
  }
}

API Gateway может напрямую интегрироваться с DynamoDB, но для продолжения использования модели базы данных, которую я построил с помощью dynamodb-toolbox, мне нужно пройти через функцию Lambda. Мне также удобнее писать на TypeScript, чем на шаблонах Apache Velocity.

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

import { APIGatewayProxyHandler } from 'aws-lambda';
// the model uses dynamodb-toolbox
import { Mail } from './model';
export const handler: APIGatewayProxyHandler = async(event) => {
    const queryParams = event.queryStringParameters;
    const recipient = queryParams?.recipient;
    if (!recipient) {
        return {
            statusCode: 400,
            body: 'Missing query parameter: recipient'
        }
    }
    const since = queryParams.since || '';
    const limit = +queryParams.limit || 1;
    const mails = (await Mail.query(
        recipient,
        {
            beginsWith: since,
            limit,
        }
    )).Items;
    return {
        statusCode: 200,
        body: JSON.stringify(mails)
    }
}

После развертывания API чтения вы можете запустить запрос GET, который включает почту получателя в качестве параметра запроса recipient. Вы можете дополнительно настроить свои вызовы, указав временную метку since или limit, которая лучше, чем 1 по умолчанию.

Например, если вы отправляете подтверждение заказа на [email protected], вам нужно выполнить запрос GET для https://YOUR_API_ENDPOINT/[email protected].

Ограничения и потенциальные улучшения

В то время как песочница SES ограничивает количество электронных писем, которые вы можете отправить, кажется, что нет ограничений на получение почты.

Наше решение еще не может предоставлять вложения или тело письма. Действие SES S3 уже хранит их в корзине, которую можно использовать для улучшенной функции чтения API.

Мы также могли бы отказаться от функции Lambda, которая связывает шлюз API и DynamoDB, заменив ее прямой интеграцией между двумя службами.

Попробуй сам

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

Дальнейшее чтение

Понравилась эта статья? Я публикую новую статью каждый месяц. Свяжитесь со мной в Twitter и подпишитесь на новые статьи!