Dev Life

Otimização do envio de e-mail em massa com o gerenciamento de filas personalizadas usando a API do Mailgun

Enviar e-mails em massa não é tão fácil quanto clicar em “enviar”. Neste guia, explicamos como o gerenciamento de filas personalizadas com a API do Mailgun ajuda a controlar o tempo, manter a entregabilidade e evitar armadilhas que podem prejudicar a reputação do remetente.
Imagem para Otimização do envio de e-mail em massa com o gerenciamento de filas personalizadas usando a API do Mailgun

O envio de e-mails em massa exige bastante sutileza e cuidado. Vários fatores aumentam a complexidade, incluindo o throttling e os limites de taxa definidos pelos provedores de caixa de correio para controlar o fluxo de e-mails, as leis de proteção de dados que devem ser seguidas e os problemas de entregabilidade causados por endereços inválidos ou duplicados. Além disso, você precisa gerenciar os limites de conexão SMTP, as restrições impostas pelos provedores e os atrasos inesperados.

Felizmente, é possível superar esses desafios. Este guia mostrará como usar a API do Mailgun como sua batuta de maestro para otimizar o envio de e-mail em massa e evitar armadilhas comuns. Mas antes, vamos explorar por que esses problemas acontecem e por que eles podem arruinar suas campanhas de e-mail em massa.

Os desafios de enviar e-mails em massa

Os limites de taxa e o throttling definidos por provedores de serviços de e-mail (ESPs) costumam ser os principais culpados pelos problemas de entregabilidade de e-mails em massa. Além dos limites impostos pelo provedor, a sua pontuação de reputação do remetente também desempenha uma função importante. Lançar uma campanha de e-mail em massa sem verificar sua pontuação e entender os limites específicos do ESP pode degradar rapidamente sua reputação.

A queda na pontuação do remetente cria um ciclo de feedback negativo, em que cada devolução ou posicionamento na pasta de spam diminui sua pontuação e prejudica ainda mais a entregabilidade. E, à medida que sua pontuação diminui, é mais provável que os ESPs sinalizem ou coloquem seu(s) domínio(s) ou IP em uma lista de bloqueio.

Embora isso não seja uma preocupação para sistemas internos de notificação, os atrasos na entregabilidade (e como você lida com eles) podem atrapalhar quase qualquer caso de uso de e-mail em massa. O congestionamento da rede, os filtros de spam agressivos e os problemas de servidor podem atrasar a entrega, o que é especialmente problemático para alertas urgentes. Esses atrasos também podem gerar e-mails duplicados quando os sistemas reenviam automaticamente as mensagens não entregues, aumentando o risco de posicionamento na pasta de spam e prejudicando ainda mais a reputação do remetente.

Felizmente, você pode resolver muitos desses problemas exercendo mais controle sobre como e quando suas mensagens de e-mail em massa são enviadas. Em vez de disparar mensagens aleatoriamente, você precisa de uma abordagem mais inteligente que considere variáveis importantes. É aí que as filas entram.

Como as filas podem ajudar

Se você se preocupa com a reputação do remetente, as filas não a corrigem de forma direta ou imediata, mas ajudam a melhorá-la.

As filas são estruturas de dados que permitem classificar e categorizar suas mensagens de forma programática para, em seguida, processá-las em sucessão. Isso permite coordenar e controlar as taxas de envio das suas campanhas de e-mail em massa.

Por exemplo, você pode dividir e agrupar seus e-mails em massa por provedor de caixa de correio, estabelecer uma fila separada para cada grupo e, em seguida, definir regras para o processamento das suas filas. Isso pode ser útil quando é necessário agendar e-mails em horários de menor movimento para aumentar a entregabilidade.

As filas também auxiliam no balanceamento de carga, pois permitem implementar intervalos curtos ou pausas entre o processamento de cada grupo de e-mails. Você também pode armazenar temporariamente as mensagens de saída, o que dá tempo ao servidor para processar cada carga.

Sistemas de fila também tornam o tratamento de e-mails não entregues mais eficiente. Você pode configurar estratégias inteligentes de reenvio para tentar enviar novamente as mensagens que falharam devido a erros de rede ou de servidor, ignorando aquelas com endereços inválidos ou caixas de entrada cheias. Também é possível usar as filas para criar uma lista de devoluções e filtrar endereços de destinatários não confiáveis.

Além disso, as filas ajudam a otimizar as campanhas de e-mail em massa simplificando o monitoramento de desempenho. Como permitem controlar o fluxo de saída dos seus e-mails, elas facilitam a identificação de gargalos, o refinamento da sua estratégia e a solução de problemas.

Otimização do envio de e-mail em massa com filas e o Mailgun

Neste guia, vamos imaginar que você está criando um serviço de notificação para a força de trabalho de uma grande empresa global, que possui funcionários e filiais em várias regiões pelo mundo. Algumas mensagens serão para toda a empresa, enquanto outras serão específicas para um local ou departamento.

Selecione a sua solução de fila

A primeira coisa que você precisa fazer é descobrir as ferramentas e os componentes necessários para este projeto. Você tem as seguintes opções para o gerenciamento de filas:

  • RabbitMQ é um intermediário de mensagens de código aberto com roteamento flexível e vários protocolos. Graças a uma comunidade ativa, ele conta com bibliotecas de clientes e ferramentas de desenvolvedor para Java, .NET, Erlang, Python, PHP, JavaScript e Go.
  • Amazon SQS é uma ótima opção se você já usa o ecossistema AWS ou quer uma solução totalmente gerenciada. Seus maiores atrativos são o suporte para sistemas de filas distribuídas e sua escalabilidade.
  • Apache Kafka é uma plataforma multifuncional de código aberto para streaming e processamento de dados distribuídos que pode ser usada como intermediário de mensagens. Como foi criado para lidar com grandes volumes de dados em tempo real, ele oferece uma vazão superior à maioria dos intermediários de mensagens ou sistemas de fila tradicionais. No entanto, configurar e gerenciar o Kafka pode ser um desafio devido à sua complexidade.
  • Redpanda é uma alternativa simplificada e que consome menos recursos em comparação com o Kafka. Ele é compatível com as APIs do Kafka e oferece suporte para muitas das mesmas implementações, incluindo a intermediação de mensagens.

Como este pode ser um projeto bem grande, uma solução gerenciada com dimensionamento automático (como o SQS) é preferível. Entretanto, se você tiver a experiência e os recursos para gerenciar por conta própria, uma solução de código aberto pode cortar custos e oferecer mais personalização.

Mas em vez de gerenciar os e-mails por conta própria, uma solução mais inteligente é o Sinch Mailgun. Ele não apenas simplifica a entrega de e-mails, mas também permite criar filas personalizadas, garantindo controle total sem o trabalho de gerenciar a infraestrutura.

Visão geral da arquitetura

As próximas seções mostrarão como criar filas personalizadas programaticamente para gerenciar suas campanhas de e-mail em massa. Este exemplo usa o RabbitMQ para a criação e o gerenciamento de filas e o SDK do Sinch Mailgun para Java para enviar mensagens de e-mail de forma assíncrona. Como o RabbitMQ e o Sinch Mailgun oferecem suporte a uma grande variedade de linguagens de programação e frameworks, você pode adaptar este exemplo para a linguagem de sua preferência.

Junto com a API HTTP e o Java SDK, o Sinch Mailgun oferece kits de desenvolvimento de software específicos de linguagem para Ir, Node.js, PHP e Ruby.

Use o diagrama de arquitetura a seguir (e sua respectiva explicação) para entender melhor a estrutura e o funcionamento do sistema de fila e envio de e-mails em massa:

Simple mailing queue system using Mailgun diagram

Este sistema usa uma API do Sinch Mailgun (MailgunMailingListApi) para buscar todas as listas de e-mails conectadas à sua conta. As listas de e-mail podem ser criadas e preenchidas de forma programática a partir de um cliente ou aplicativo de cadastro de usuários ou, de forma manual, pelo painel do Sinch Mailgun. As listas de e-mail permitem que você organize e gerencie os destinatários dos seus e-mails de maneira estruturada e funcionam como a fonte de dados do seu sistema.

Após o sistema buscar todas as listas de e-mail, é criada uma fila do RabbitMQ para cada uma. As filas são preenchidas com o conteúdo do e-mail, bem como o nome e o endereço de cada destinatário (membro) que receberá o e-mail.

Um módulo separado, contendo executores agendados para cada fila, busca novos itens nas filas em intervalos específicos. Se algum dos executores descobrir novos itens na fila, ele começará a consumi-la e processá-la. Na etapa final, o processador de e-mail do sistema usa a MailgunMessagesApi para enviar a mensagem.

Você pode aumentar a robustez do sistema adicionando uma fila ou um banco de dados para lidar com mensagens cujo envio falhou e para arquivar as que foram enviadas com sucesso.

Crie suas filas

Para criar uma fila, você precisa configurar e executar uma instância do RabbitMQ usando o Docker. Feito isso, você poderá criar um novo projeto em Java para a criação de filas. Não se esqueça de adicionar as seguintes bibliotecas à lista de dependências do seu projeto:

Você também precisará adicionar as seguintes importações à sua classe principal:

                                

                                    // Mailgun importsrnimport com.mailgun.api.v3.MailgunMailingListApi;rnimport com.mailgun.client.MailgunClient;rnimport com.mailgun.model.mailing.lists.MailingListData;rnimport com.mailgun.model.mailing.lists.MailingListMember;rnrn// RabbitMQ importsrnimport com.rabbitmq.client.AMQP;rnimport com.rabbitmq.client.ConnectionFactory;rnimport com.rabbitmq.client.Connection;rnimport com.rabbitmq.client.Channel;rnrn// Java importsrnimport java.io.IOException;rnimport java.util.HashMap;rnimport java.util.Map;rnimport java.util.concurrent.TimeoutException;rn
                                
                            

Seu método main será preenchido com o código para inicializar a API do Sinch Mailgun para listas de e-mail, estabelecer uma conexão com o RabbitMQ e criar uma fila para cada lista de e-mail:

                                

                                    public static void main(String[] args) throws IOException, TimeoutException{rn    // Initialize Mailing List APIrn    // Replace with your own Sinch Mailgun API keyrn    MailgunMailingListApi mailgunMailingListApi = MailgunClient.config("<SINCH_API_KEY>")rn        .createApi(MailgunMailingListApi.class);rnrn    // Initialize and create connection to RabbitMQ   rn    ConnectionFactory factory = new ConnectionFactory();rn    // Replace with your detailsrn    factory.setHost("<HOSTNAME>");                                           rn    factory.setUsername("<USERNAME>");rn    factory.setPassword("<PASSWORD>");rn    factory.setPort(<PORT>);rnrn    // Create queue channel - throws IOException and TimeoutExceptionrn    Connection connection = factory.newConnection();rn    Channel channel = connection.createChannel();    rnrn    // Loop through mailing listsrn    for(MailingListData mailingList : mailgunMailingListApi.getMailingList().getItems()) {rn        String queue_name = mailingList.getName();rnrn        // Create a simple queue using the mailing list name                rn        channel.queueDeclare(queue_name, false, false, false, null);rnrn        // Populate queue with mailing list member addresses and message contentrn        for(MailingListMember mailingListMember : mailgunMailingListApi.getMailingListMembers(mailingList.getAddress()).getItems()) {rn            // Create headers with email message detailsrn            Map<String, Object> headers = new HashMap<String, Object>();rn            headers.put("address", mailingListMember.getAddress());rn            headers.put("name", mailingListMember.getName());rn            headers.put("subject", "Alert");          rn            // Add a row to the queue  rn            // Replace with actual email contentrn            String emailMessage = "Email content";       rn            channel.basicPublish("", queue_name,rn                                 new AMQP.BasicProperties.Builder()rn                                 .headers(headers)rn                                 .build(),rn                                 emailMessage.getBytes());rnrn            // Add code to log each queued entryrn            System.out.println("Sent: " + emailMessage);rn        }    rnrn    }rnrn}
                                
                            
Você pode encontrar a chave de API do Sinch Mailgun na seção Segurança da API da sua conta.

Quando as conexões necessárias são estabelecidas, o código gera uma fila para cada lista de e-mail com base no nome de cada uma. Um loop for-each busca todos os membros (MailingListMember) da respectiva lista de e-mail. Em seguida, o código usa os detalhes de cada membro (endereço e nome), além dos detalhes da mensagem de e-mail (assunto e conteúdo da mensagem), para criar e publicar uma entrada na fila.

Nesse ponto, você pode adicionar um código para criar filas adicionais por tipos de assunto ou mensagem. Por exemplo, seria possível criar filas separadas para newsletters, notificações, alertas e outras atualizações importantes. Isso daria um controle mais granular sobre como suas mensagens são priorizadas e enviadas. Porém, para fins de simplicidade, você pode pular esta etapa.

Após o código ser executado com sucesso, você poderá visualizar e gerenciar as suas filas pelo painel do RabbitMQ:

Queues and Streams screen

Agora, você precisa de um cliente para consumir as filas e enviar suas mensagens.

Consuma suas filas e envie suas mensagens

Assim como o módulo anterior, esse módulo exige a adição das bibliotecas amqp-client do RabbitMQ, SLF4J e Mailgun como dependências. Você também precisará adicionar a versão mais recente da biblioteca Feign Core e as seguintes importações na classe principal do seu novo módulo:

                                

                                    // RabbitMQ importsrnimport com.rabbitmq.client.Channel;rnimport com.rabbitmq.client.ConnectionFactory;rnimport com.rabbitmq.client.Connection;rnimport com.rabbitmq.client.DeliverCallback;rnrn// Mailgun importsrnimport com.mailgun.api.v3.MailgunMessagesApi;rnimport com.mailgun.client.MailgunClient;rnimport com.mailgun.model.message.Message;rnimport com.mailgun.model.message.MessageResponse;rnrn// Feign Core importsrnimport feign.AsyncClient;rnimport feign.Client;rnrn// Java importsrnimport java.util.concurrent.ScheduledExecutorService;rnimport java.util.concurrent.TimeUnit;rnimport java.io.IOException;rnimport java.nio.charset.StandardCharsets;rnimport java.util.Map;rnimport java.util.concurrent.CompletableFuture;rnimport java.util.concurrent.ExecutorService;rnimport java.util.concurrent.Executors;rnimport java.util.concurrent.TimeoutException;rn
                                
                            

Depois, você deve avaliar como deseja processar as filas e estabelecer as regras necessárias. Por exemplo, se quiser que cada fila seja processada em intervalos de horas diferentes, será necessário configurar e criar um ScheduledExecutorService:

                                

                                        private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(3);rnrn    public void startProcessing() {rn        Runnable china_task = () -> {rn            try {rn                consumeEmails("China");rn            } catch (Exception e) {rn                e.printStackTrace();rn            }rn        };rnrn        Runnable britain_task = () -> {rn            try {rn                consumeEmails("Britain");rn            } catch (Exception e) {rn                e.printStackTrace();rn            }rn        };rnrn        Runnable america_task = () -> {rn            try {rn                consumeEmails("America");rn            } catch (Exception e) {rn                e.printStackTrace();rn            }rn        };rnrn        // Schedule each task to run at different hourly intervalsrn        scheduler.scheduleWithFixedDelay(china_task, 11, 11, TimeUnit.HOURS);rn        scheduler.scheduleWithFixedDelay(britain_task, 13, 13, TimeUnit.HOURS); rn        scheduler.scheduleWithFixedDelay(america_task, 17, 17, TimeUnit.HOURS); rn    }rnrn    public void stopProcessing() {rn        scheduler.shutdown();rn    }
                                
                            

Lembre-se de adicionar o código acima na classe main do seu novo módulo.

Como o aplicativo de exemplo tem três filas, será necessário instanciar um objeto ScheduledExecutorService com tamanho de três em seu pool principal de threads. Em seguida, é possível criar uma tarefa e definir um intervalo de atraso para cada fila.

Neste exemplo, cada tarefa executável leva o nome de uma fila (lista de e-mail), mas você pode nomear as tarefas como desejar. O método stopProcessing encerra o agendador quando você sai do aplicativo.

Agora, você pode criar um método para lidar com as suas filas:

                                

                                    public void consumeEmails(String queueName) throws IOException, TimeoutException {rnrn    ConnectionFactory factory = new ConnectionFactory();rn    // Replace with your detailsrn    factory.setHost("<HOST>");                                           rn    factory.setUsername("<USERNAME>");rn    factory.setPassword("<PASSWORD>");rn    factory.setPort(<PORT>);rnrn    Connection connection = factory.newConnection();rn    Channel channel = connection.createChannel();rnrn    channel.queueDeclare(queueName, false, false, false, null);rn    // Send the messagern    DeliverCallback deliverCallback = (consumerTag, delivery) -> {rn        String emailMessage = new String(delivery.getBody(), StandardCharsets.UTF_8);rn        Map <String, Object> headers = delivery.getProperties().getHeaders();rnrn        String recipient = headers.get("address").toString();rn        String subject = headers.get("subject").toString();rn        String name = headers.get("name").toString();rn        String body = "Dear " + name + ",
" + emailMessage;rnrn        sendEmail(recipient, subject, body);rn    };rn    channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });rn}
                                
                            

O método consumeEmails estabelece uma conexão com sua fila, analisa-a, busca os detalhes de cada entrada e, por fim, transmite essas informações a sendEmail. Assim que o e-mail é enviado com sucesso, ele é marcado como consumido no registro da fila.

Vamos analisar o método sendEmail em mais detalhes:

                                

                                    public void sendEmail(String recipient, String subject, String body) {rn    // Create async clientrn    ExecutorService executor = Executors.newFixedThreadPool(1);rn    AsyncClient.Default<Object> asyncClient = new AsyncClient.Default<>(rn        new Client.Default(null, null), executor);rnrn    MailgunMessagesApi mailgunMessagesApi = MailgunClient.config("<SINCH_MAILGUN_API_KEY>")rn        .client(asyncClient)rn        .createApi(MailgunMessagesApi.class);rnrn    Message message = Message.builder()rn        .from("<REPLY_INBOX>")rn        .to(recipient)rn        .subject(subject)rn        .text(body)rn        .build();rnrn    CompletableFuture<MessageResponse> messageResponse = mailgunMessagesApi.sendMessageAsync("<SINCH_MAILGUN_DOMAIN>", message);rn    // Log message responsern    System.out.println(messageResponse);rnrn}
                                
                            

Ao usar o método de envio padrão (mailgunMessagesApi.sendMessage("", message)), o Mailgun coloca sua mensagem em uma fila para processamento. Você pode contornar a fila ao enviar a mensagem de forma assíncrona. Isso exige a criação de um AsyncClient para vinculá-lo à API Messages do Mailgun.

Feito isso, o código cria a mensagem com os detalhes da fila do RabbitMQ e faz o envio usando a API Messages do Mailgun.

Para executar seu aplicativo, é necessário adicionar uma chamada ao método startProcessing() em seu método main e algum código para processar a finalização do aplicativo:

                                

                                        public static void main(String[] args) {                                   rn        // Your main class rn        SinchRabbitMQConsumer processor = new SinchRabbitMQConsumer();                              rn        processor.startProcessing();rnrn        // Add a shutdown hook to stop processing when the application is terminatedrn        Runtime.getRuntime().addShutdownHook(new Thread(processor::stopProcessing));rn    }
                                
                            

Conclusão

Neste artigo, você descobriu como as filas podem ajudar a gerenciar o fluxo de suas campanhas de e-mail em massa e proporcionar um controle granular sobre como seus e-mails em massa são enviados. Quando implementadas em um sistema de gerenciamento de filas eficiente, elas ajudam a atenuar problemas como limite de taxa, throttling e congestionamento de rede.

Independentemente de como você implementa e gerencia suas filas, um parceiro de envio confiável como o Mailgun pode ajudar.

Sendo uma plataforma desenvolvida com foco em API (API-first), o Mailgun oferece uma série de ferramentas para não apenas enviar e-mails, mas também dimensionar, monitorar e rastrear suas campanhas de forma programática. Confira a documentação oficial da Send API e não se esqueça de assinar a nossa newsletter para receber mais tutoriais como este.