Pular para o conteúdo

Envie o e-mail do seu próprio app

Um e-mail transacional é o que o seu app deve a uma pessoa: um recibo, um link de acesso, uma redefinição de senha. Ele sai de uma caixa reservada para isso, ou de um endereço de remetente que envia pelos nossos servidores de e-mail, exatamente como você o escreveu. Comece lendo o que o seu app já enviou. Isto não envia nada.

import { Colding } from "colding";

const colding = new Colding("cd_your_key");
const page = await colding.sends.list({ kind: "transactional" });
for (const send of page.items) console.log(send.to, send.status);

Troque a chave pela sua. kind: "transactional" deixa de fora todos os outros tipos de e-mail. A lista fica vazia até o seu app enviar o primeiro.

Exemplo de saída, para um e-mail que foi enviado
[email protected] sent

Veja o que você precisa para enviar um

O que você precisa antes

Use uma chave do primeiro guia. Cada permissão cobre um tipo de ação, e uma não inclui a outra.

  • A permissão transactional na chave. Uma chave só com send não envia esses e-mails, e uma chave só com transactional não envia campanhas. Para acompanhar um e-mail depois, a chave também precisa de read.
  • De onde ele sai: uma caixa reservada para os e-mails do seu app, com purpose: "transactional" (a próxima seção mostra como definir), ou um endereço de remetente (a seção Endereços de remetente).
  • Um plano com e-mail transacional. O plano gratuito não tem, e a chamada responde 402 PLAN_GATE.
  • O dono do workspace precisa ter aceitado a política de uso aceitável. Até lá, o envio responde 403 AUP_REQUIRED.
  • Um endereço confirmado para enviar. Um login que funciona prova a senha, não que o endereço é seu. Por isso um dono confirma o endereço de uma caixa no console. Até lá, a chamada responde 409. Um endereço de remetente não precisa disso: os registros do domínio dele já provam.

Veja como as caixas são conectadas

Um plano pago deixa uma caixa enviar de 200 a 1.000 desses e-mails por dia, conforme o plano. A contagem recomeça à meia-noite, no fuso do workspace.

Veja os limites de cada plano

Reserve uma caixa para isso

Toda caixa tem uma só finalidade por vez, o purpose: cold para campanhas e mensagens, ou transactional para os e-mails do seu app. Uma caixa transacional não envia campanhas, e a outra não envia os e-mails do seu app. Se você tentar enviar o tipo errado por uma caixa, a chamada responde 409.

Defina a finalidade com mailboxes.update. A chave precisa de operate e transactional, e o plano precisa incluir e-mail transacional:

const mailbox = await colding.mailboxes.update({
  mailboxId,
  purpose: "transactional",
});
console.log(mailbox.purpose);

Ele imprime transactional. Voltar para cold é a mesma chamada, e também pede transactional. Uma caixa transacional continua recebendo e-mails, e você ainda pode responder por ela.

Uma caixa de onde saem campanhas, ou que tem e-mails agendados, não pode virar transacional. A chamada responde 409, e details.campaigns lista essas campanhas com ID e nome. Tire a caixa delas, deixe os e-mails agendados saírem ou cancele-os, e chame de novo.

Uma caixa só de envio não tem caixa de entrada. Ela é o login SMTP de um serviço de envio e é sempre transacional. Um dono a conecta no console. Um agente pede essa conexão com um handoff.

Veja como uma caixa só de envio é conectada

Deixe os e-mails do seu app num subdomínio próprio, como mail.example.com, separado do domínio das suas campanhas. Assim, os bounces e as denúncias de spam de uma campanha têm menos chance de chegar aos seus recibos. O console sugere isso quando os dois tipos dividem um domínio. É um conselho, não uma regra.

Endereços de remetente

Os endereços de remetente ainda não estão em nenhum plano à venda. Até a nossa equipe liberar o envio pelos nossos servidores de e-mail para o seu workspace, criar um responde 402 PLAN_GATE com o messageKey serverErrors.hostedTransactionalClosed. A gente libera para uma empresa de cada vez: escreva para [email protected].

Um endereço de remetente é o endereço From do e-mail do seu app, como [email protected]. Ele não tem senha nem login, e fica em um domínio de e-mail seu, preparado para e-mail transacional. Ele não recebe e-mails.

Uma chave não cria endereços de remetente. O dono cria no console, em Domínios, quando o domínio está Ativo. Um agente pede a um dono com um handoff manage_sender_addresses. Por trás do console há três operações que só o dono pode chamar: create_sender_address, update_sender_address e delete_sender_address. Uma chave recebe 403 nelas.

Todo endereço de remetente tem um replyTo, o endereço para onde as respostas vão. Nós não recebemos os e-mails do domínio, então uma resposta ao próprio endereço de remetente não chegaria a ninguém. Indique um endereço que alguém leia, como o do seu suporte. Ele não pode ser o próprio endereço de remetente.

Uma chave lê endereços de remetente com senderAddresses.list e senderAddresses.get. Troque a chave pela sua.

const page = await colding.senderAddresses.list();
for (const sender of page.items) console.log(sender.id, sender.address, sender.replyTo);

Cada um tem id, address, domainId, displayName, replyTo, createdAt e createdBy. O id é o mailboxId que um envio cita. Restrinja a lista com domainId. A lista é paginada: devolva nextCursor como cursor. Um endereço de remetente excluído não aparece na lista, e buscá-lo responde 404.

Para pedir um, crie um handoff, entregue o link ao dono e leia o resultado. Quando o dono cria o endereço, o handoff fica completed e resultMailboxId é o ID do novo endereço de remetente.

const request = await colding.handoffs.create({
  kind: "manage_sender_addresses",
  note: "Add [email protected]. Replies go to [email protected].",
});
console.log(request.url);

// Later, after the owner has opened the link:
const done = await colding.handoffs.get({ handoffId: request.id });
if (done.resultMailboxId) {
  const sender = await colding.senderAddresses.get({ senderAddressId: done.resultMailboxId });
  console.log(sender.address, sender.replyTo);
}

Um endereço pelo qual o seu app já envia, como caixa de e-mail conectada ou como caixa só de envio, não pode ser também um endereço de remetente. Para mudar, desconecte a caixa primeiro. Isso exclui as mensagens guardadas aqui para ela (os e-mails no seu provedor continuam lá), e o endereço de remetente recebe um ID novo, que o seu app precisa usar nos envios. Até você trocar o ID na configuração do app, um envio que cite o ID antigo responde 404.

Para enviar de um endereço de remetente, passe o id dele como mailboxId.

Veja como o envio funciona pelos nossos servidores

Envie um e-mail

transactional.send envia um e-mail para uma pessoa, de uma caixa ou de um endereço de remetente:

const send = await colding.transactional.send({
  mailboxId: "66666666-6666-4666-8666-666666666666",
  to: "[email protected]",
  subject: "Your sign-in link",
  bodyText: "Open this link to sign in: https://app.example.com/in/abc123",
  intentKey: "sign-in-link-user-42-2030-01-15",
  expiresAt: "2030-01-15T10:15:00Z",
});
console.log(send.id, send.status);

A chamada devolve o e-mail com status reserved, que aparece como Prestes a sair no console. Ele já conta nos envios de hoje, mas ainda não saiu. O worker, nosso processo de envio em segundo plano, cuida do envio.

Envie bodyHtml, bodyText ou os dois. O e-mail sai exatamente como você escreveu: sem assinatura, sem linha de descadastro, sem rastreamento de abertura ou de clique, sem espera entre e-mails e sem janela para desfazer. O toName define o nome na linha Para, e o replyTo diz para onde vão as respostas.

Defina expiresAt para um código ou link que perde a validade, com data, hora e fuso. Se o e-mail não tiver começado a sair até lá, ele é cancelado com o errorCode expired e nunca é enviado. Um horário que já passou responde 400.

Você pode cancelar um e-mail que ainda não começou a sair com sends.cancel. Ele pede a permissão transactional. Depois que o e-mail começa a sair, não dá mais para cancelá-lo, e a chamada responde 409.

Dê um nome a cada e-mail

O intentKey é obrigatório. É o seu nome para um e-mail, com até 255 caracteres, como receipt-order-1042. Use algo que o seu app já tem, como o número do pedido.

Envie a mesma requisição de novo e você recebe o mesmo e-mail de volta. Nada mais é enviado. Isso vale para sempre, não só por 24 horas.

const request = {
  mailboxId: "66666666-6666-4666-8666-666666666666",
  to: "[email protected]",
  subject: "Receipt for order 1042",
  bodyText: "Thanks for your order. The receipt is in your account.",
  intentKey: "receipt-order-1042",
};
const first = await colding.transactional.send(request);
const again = await colding.transactional.send(request);
console.log(first.id === again.id);
Exemplo de saída: a segunda chamada devolveu o primeiro e-mail
true

A requisição é a caixa, o endereço, o nome, o assunto, os dois corpos, o expiresAt e o replyTo. Monte-a uma vez e envie essa mesma cópia em cada nova tentativa. Guarde o intentKey antes de enviar, para que uma queda não o perca.

O mesmo intentKey com outra requisição é recusado com 422 IDEMPOTENCY_KEY_REUSED, e nada é enviado. Para enviar um segundo e-mail de propósito, como um recibo de novo, use um intentKey novo.

Uma nova tentativa é respondida antes de o plano, o pagamento e o expiresAt serem conferidos. Então uma nova tentativa que chega tarde ainda recebe o e-mail de volta, mesmo que o pagamento tenha falhado nesse meio-tempo.

Nomes que começam com draft:, warmup: ou campaign: são nossos e são recusados.

Pelos nossos servidores

Passe o id de um endereço de remetente como mailboxId. Nada mais muda na chamada: o mesmo intentKey, o mesmo corpo, o mesmo expiresAt. O e-mail sai dos nossos servidores de e-mail, então o seu app não precisa de uma caixa própria para enviá-lo.

const send = await colding.transactional.send({
  mailboxId: "77777777-7777-4777-8777-777777777777",
  to: "[email protected]",
  subject: "Your sign-in link",
  bodyText: "Open this link to sign in: https://app.example.com/in/abc123",
  intentKey: "sign-in-link-user-42-2030-01-16",
});
console.log(send.status, send.viaOurServers);
Exemplo de saída: o status e depois se saiu pelos nossos servidores
reserved true

A chamada responde como qualquer envio, com status reserved. O viaOurServers é true quando o e-mail sai pelos nossos servidores e false quando sai por uma caixa que você conectou.

Aqui, sent quer dizer que o nosso servidor de e-mail recebeu o e-mail e está repassando para o servidor da pessoa. Não quer dizer que chegou na caixa de entrada. Quando o servidor da pessoa responde, o send.delivered avisa que ele aceitou, ou o send.bounced avisa que recusou de vez. O bounce vem depois do sent, e o status continua sent.

Há dois limites. O expiresAt só impede um e-mail que o nosso servidor ainda não recebeu. Depois que ele recebe, continua tentando até o servidor da pessoa responder, então um código pode chegar depois de vencido. E, em casos raros, o servidor da pessoa aceita o e-mail mas a resposta se perde no caminho, e o nosso servidor envia de novo: a pessoa recebe duas vezes.

Quando os nossos servidores recusam

Um endereço de remetente pode ser recusado por mais motivos do que uma caixa. Nenhum deles reserva nada, então a mesma requisição pode ser enviada de novo. O resetAt diz quando.

402 hostedTransactionalClosed
O seu workspace ainda não está liberado para enviar e-mails pelos nossos servidores. A nossa equipe libera para uma empresa de cada vez: escreva para [email protected].
429 hostedTransactionalDay
O seu workspace já enviou hoje todos os e-mails que pode enviar pelos nossos servidores. Esse número é só do seu workspace. Espere até o resetAt, meia-noite no fuso do workspace.
429 domainRamp
Um domínio novo nos nossos servidores começa com poucos e-mails por dia e pode enviar um pouco mais a cada dia em que envia. O details.limit é o número de hoje. Ele recomeça no resetAt, à meia-noite UTC.
429 ipCeiling429 ipShare
Esses são limites dos nossos servidores, não do seu plano. Outras empresas enviam pelos mesmos servidores, e cada uma tem um espaço garantido para os próprios e-mails. Eles recomeçam no resetAt, à meia-noite UTC.
429 receiverPace
O provedor de e-mail da pessoa pediu para o nosso servidor ir mais devagar, ou a nossa equipe pausou por um tempo os e-mails para esse provedor. O details.receiver diz qual. Envie de novo depois do resetAt.
409 senderIpNotReady409 mailIpHeld
O problema é do nosso lado: ainda não há servidor nosso liberado para e-mails de app, ou o servidor por onde o seu e-mail sai está em espera. Nada foi enviado. Envie a mesma requisição de novo mais tarde.
409 mailDomainNotSigning
Os registros DNS do seu domínio não passaram na nossa verificação, então não conseguimos assinar os e-mails dele. Nada sai por ele até os registros voltarem a passar. Corrija no console, em Domínios.
409 senderDomainUnderReview
A nossa equipe colocou em espera o domínio do endereço de remetente porque e-mails demais dele deram bounce ou foram marcados como spam. Não há resetAt: nada sai pelo domínio até a nossa equipe liberar. O details.caseNumber é o número a informar. Mais abaixo nesta página, você vê quando colocamos um domínio em espera e como a espera acaba.

O número diário do seu workspace recomeça à meia-noite no fuso do workspace. O limite diário de um domínio novo e os limites dos nossos servidores recomeçam à meia-noite UTC.

Com domainRamp, ipCeiling ou ipShare, se o app já tem outro endereço confirmado e conectado para e-mail transacional, envie o mesmo conteúdo por ele, com um intentKey próprio. O destinatário verá esse outro endereço como remetente. Se não tiver, envie a mesma requisição de novo depois do resetAt. Com hostedTransactionalDay, espere até o resetAt: esse número é só do seu workspace.

Configure outra forma de enviar antes de precisar dela

Ainda não dá para anexar arquivos a um envio.

Configure outra forma de enviar antes de precisar dela

  • Antes de precisar, conecte outro endereço para e-mail transacional: uma caixa reservada para isso ou o login do seu serviço de envio. Confirme o endereço do remetente. Não pode ser o endereço de remetente que você usa pelos nossos servidores.
  • Guarde o mailboxId dele na configuração do app, como segundo remetente.
  • Use só depois de um 429 que não reservou nada, com details.scope igual a domainRamp, ipCeiling ou ipShare. Envie o mesmo conteúdo por esse mailboxId, com um intentKey próprio. Um e-mail diferente com a mesma chave é recusado. Se esse envio for aceito, não tente enviar também pela primeira forma. Nunca mande uma segunda cópia de um e-mail que já está sent ou unknown.
  • O destinatário verá o segundo endereço como remetente, não o endereço de remetente dos nossos servidores.
  • O hostedTransactionalDay é o número diário só do seu workspace. Nesse caso, espere até o resetAt.

Acompanhe até o fim

sends.get responde por um e-mail. sends.list responde por vários, do mais novo ao mais antigo, e kind: "transactional" deixa só estes. Os dois pedem read. O status só anda para a frente. Esses e-mails não podem ser agendados, então scheduled nunca aparece:

reserved
Prestes a sair. Já conta nos envios de hoje e espera a vez.
sending
Saindo agora.
sent
O servidor de e-mail por onde ele saiu o aceitou: o do provedor da caixa que você conectou ou, num endereço de remetente, o nosso. Isso não quer dizer que chegou na caixa de entrada.
failed
Não saiu. O errorCode diz o motivo.
unknown
A conexão caiu durante a saída, então não sabemos se o servidor recebeu. Conta como enviado e nunca é enviado de novo.
cancelled
Parou antes de sair: uma pessoa ou uma chave cancelou, ou uma verificação recusou na hora de enviar. O errorCode diz o motivo.

Para saber como cada e-mail terminou sem perguntar, leia os eventos. Todo envio termina em send.sent, send.failed, send.unknown, send.cancelled ou send.bounced, e os e-mails de campanha também, então o exemplo confere o kind:

const page = await colding.events.list({
  types: ["send.sent", "send.failed", "send.unknown", "send.cancelled", "send.bounced"],
});
for (const event of page.items) {
  if ("kind" in event.data && event.data.kind === "transactional")
    console.log(event.type, event.subjectId);
}

O subjectId é o ID do e-mail. Esses e-mails não têm send.opened nem send.clicked, porque não levam rastreamento.

sent quer dizer que o servidor de e-mail por onde ele saiu o aceitou: o do seu provedor, numa caixa que você conectou, ou o nosso, num endereço de remetente. Nenhum dos dois diz que ele chegou na caixa de entrada. Um bounce pode chegar depois. Aí o send.bounced dispara, e o bouncedAt diz quando.

As respostas chegam na caixa de entrada, ou no replyTo quando você define um. Uma caixa só de envio não tem caixa de entrada, e um endereço de remetente também não: as respostas vão para o replyTo ou, se você não definir, para o endereço de resposta com que o endereço de remetente foi criado.

Leia as respostas no guia da caixa de entrada

Quando ele é recusado ou barrado

Uma chamada recusada não envia nada. Estas são as respostas que um envio pode receber:

400 VALIDATION_ERROR
A requisição está errada: sem corpo, um endereço inválido ou um expiresAt que já passou.
402 PLAN_GATE
O plano não tem e-mail transacional. Trocar de plano resolve, e o details.upgradeTo diz qual plano tem. Num endereço de remetente, também pode ser que a nossa equipe ainda não liberou os nossos servidores para o workspace.
402 PAYMENT_REQUIRED
O último pagamento falhou. Pagar resolve.
403 FORBIDDEN
A chave não tem transactional, ou um dono a limitou: a algumas caixas, a um tipo de e-mail ou a um total de e-mails. O details.scope diz qual.
403 AUP_REQUIRED
O dono ainda não aceitou a política de uso aceitável.
404 NOT_FOUND
Não existe essa caixa nem esse endereço de remetente neste workspace. Um de outro workspace responde a mesma coisa.
409 CONFLICT
Algo na caixa, no endereço ou no workspace bloqueia este envio. A mensagem diz o quê: a caixa não é transacional, o endereço dela não foi confirmado, o destinatário parou os e-mails do seu app ou, num endereço de remetente, um servidor nosso não está pronto ou a nossa equipe colocou o domínio dele em espera.
422 IDEMPOTENCY_KEY_REUSED
Este intentKey já nomeia outro e-mail.
429 QUOTA_EXCEEDED
A caixa já enviou todos os e-mails do app de hoje, ou a chave chegou ao limite diário. Num endereço de remetente, também pode ser o espaço dos nossos servidores: a seção Pelos nossos servidores lista cada caso. O details.scope diz qual, e o resetAt diz quando o envio volta.
429 RATE_LIMIT_EXCEEDED
O provedor de e-mail da pessoa pediu para o nosso servidor ir mais devagar. Isso só acontece num endereço de remetente. O details.scope é receiverPace, e a seção Pelos nossos servidores explica.

O primeiro guia lista todos os códigos de erro e o que fazer com cada um.

Barrado antes de sair

Uma chamada pode dar certo e o e-mail mesmo assim não sair. Pouco antes de entregar, o worker olha mais uma vez. Se algo mudou, ele cancela o e-mail com um errorCode e não envia nada:

expired
O expiresAt passou antes de o e-mail começar a sair.
recipient_suppressed
O endereço entrou na lista de supressão dos e-mails do app depois da sua chamada.
authority_revoked
A chave ou a pessoa que enviou não pode mais enviar e-mails do app.
key_stream_scopekey_mailbox_scope
Um dono limitou a chave depois da sua chamada, e ela não pode mais enviar esse tipo de e-mail nem enviar dessa caixa.
payment_failed
Um pagamento falhou depois da sua chamada.
workspace_posture
Limitamos o workspace antes de o e-mail sair, ou a conta que enviou não pode mais enviar.
workspace_closing
O dono pediu para excluir o workspace.
sender_unconfirmed
O endereço da caixa não foi confirmado como do workspace.
mailbox_disconnected
A caixa foi desconectada, ou o endereço de remetente foi excluído, depois da sua chamada.
mail_ip_held
O servidor nosso por onde o e-mail sairia foi colocado em espera depois da sua chamada.
domain_not_signing
O domínio do endereço de remetente não passou na nossa verificação depois da sua chamada, então não conseguimos assinar os e-mails dele.
mailbox_paused
A caixa foi pausada, ou a nossa equipe colocou em espera o domínio do endereço de remetente, depois da sua chamada.

failed é diferente: o e-mail foi tentado, e o servidor ou a caixa o recusou. O errorCode diz o motivo. Para enviá-lo de novo, use um intentKey novo, porque o antigo ainda nomeia o e-mail que falhou.

relay_refused é problema nosso: o nosso servidor de e-mail recusou um e-mail de um endereço de remetente. Não é bounce, ninguém entra na lista de supressão e o e-mail não conta no número de hoje. Envie de novo mais tarde, com um intentKey novo.

Quando colocamos um domínio de e-mail em espera

Acompanhamos o e-mail do seu app que sai pelos nossos servidores, um domínio de e-mail por vez. Se e-mails demais derem bounce ou forem marcados como spam, colocamos o domínio em espera. Todos os endereços de remetente dele param de enviar até a nossa equipe liberar. Os outros domínios de e-mail e as caixas que você conectou continuam enviando.

Contamos os últimos 7 dias. Um bounce conta quando o servidor de e-mail da pessoa recusou o endereço. Um bounce sem motivo não conta, e um atraso do nosso lado também não. Um e-mail que a pessoa marca como spam conta uma vez.

Colocamos um domínio em espera quando 0,5% dos e-mails foram marcados como spam ou 10% deram bounce. Com menos de 50 e-mails nesses dias, nenhuma taxa conta. No caso do spam, também são necessárias pelo menos 3 pessoas diferentes que marcaram um e-mail, para que uma só pessoa não consiga colocar o seu domínio em espera sozinha. No caso do bounce, também são necessários pelo menos 10 bounces para 3 ou mais domínios de destinatário diferentes (a parte depois do @), para que o servidor de uma só empresa não consiga colocar o seu domínio em espera sozinho. Esses são os números de hoje, e eles podem mudar.

Um servidor de destino também pode recusar o seu e-mail como indesejado. Só colocamos o domínio em espera por isso quando outros remetentes dos nossos servidores não recebem a mesma recusa desse servidor.

Enquanto a espera dura, um envio por qualquer endereço de remetente do domínio responde 409 com senderDomainUnderReview. O details.caseNumber é o número do nosso caso. O details.ground diz qual regra colocou o domínio em espera: complaint_rate, bounce_rate, policy_bounce_rate ou policy_abuse_burst. Um e-mail que já estava esperando para sair é cancelado com mailbox_paused. Também não dá para adicionar um endereço de remetente no domínio.

Só a nossa equipe libera os envios de novo, e a contagem recomeça do zero quando isso acontece. Escreva para [email protected] e informe o número do caso.

O workspace recebe um alerta mail_domain_problem quando a espera começa (under_review) e outro quando ela termina (review_ended), e cada endereço de remetente dispara mailbox.review_changed. A página do domínio no console mostra os últimos 7 dias e a espera.

Também lemos o e-mail do seu app como um grupo à parte quando avaliamos o workspace inteiro. As taxas são as mesmas, mas só contam a partir de 200 e-mails nesses dias. As marcações de spam continuam exigindo 3 pessoas diferentes, e os bounces continuam exigindo 3 ou mais domínios de destinatário. Se o seu e-mail passar delas, ou se um domínio ficar em espera em vários dias diferentes, podemos limitar o workspace. O posture.reasonCode passa a trazer transactional_bounce_rate, transactional_complaint_rate ou sending_pauses.

Veja os alertas no guia do workspace

Quem para de receber

Os e-mails do seu app têm a própria lista de supressão. Um endereço pode parar campanhas e mensagens (cold na API), os e-mails do seu app (transactional) ou os dois. O que para o quê depende do que voltou e de qual e-mail se trata:

O endereço teve bounce, em qualquer e-mail
Os dois
Uma pessoa denunciou como spam um e-mail de campanha ou mensagem
Só campanhas e mensagens
Uma pessoa denunciou como spam um e-mail do seu app
Os dois
Uma pessoa respondeu pedindo para parar, num e-mail de campanha ou mensagem
Só campanhas e mensagens
Uma pessoa respondeu pedindo para parar, num e-mail do seu app
Os dois

Um descadastro das suas campanhas nunca para os e-mails do seu app.

Nenhuma verificação de endereço vem antes, porque a pessoa pediu esse e-mail. Só a lista de supressão é lida, duas vezes: quando você faz a chamada e de novo pouco antes de o e-mail sair.

Veja o que parou os e-mails do seu app:

const page = await colding.suppressions.list({ stream: "transactional" });
for (const row of page.items) console.log(row.value, row.source, row.twinSource);

O source diz por que cada linha está lá. O twinSource diz por que o mesmo endereço está na outra lista, ou é null quando não está.

suppressions.add adiciona um endereço com stream: "transactional" e pede operate. Só um dono ou admin que entrou no console pode tirar um da lista. Nenhuma chave pode.

O suppression.added dispara a cada linha nova, com o stream e o source dela.

Relatórios de um serviço de envio

Uma caixa só de envio não tem caixa de entrada, então um bounce não volta como e-mail. O serviço de envio avisa de outro jeito: um relatório enviado a um endereço nosso, o webhook de relatórios da caixa. Lemos Mailgun, Postmark, Resend, SendGrid e Amazon SES. Um relatório só conta se o serviço provar que é dele e se citar um e-mail enviado por esta caixa.

O dono liga o webhook no console e copia o endereço dele para o serviço. Uma chave não pode, porque o webhook guarda ou mostra um segredo. A chave lê o resto:

const hook = await colding.mailboxes.reportHook({ mailboxId });
console.log(hook.service, hook.on, hook.lastReportAt);

const reports = await colding.mailboxes.reports({ mailboxId });
for (const report of reports) console.log(report.kind, report.outcome);

O reportHook diz se lemos esse serviço (supported) e se o webhook está ligado (on). Também traz a hora do último relatório (lastReportAt) e quantos chegaram nos últimos 30 dias, por resultado (counts). Ele nunca mostra o segredo.

A Amazon SES não tem segredo. O dono nos dá o tópico SNS. O webhook diz quando a assinatura da AWS foi confirmada e qual foi a última mensagem que recusamos, com o motivo (refusal).

O reports lista o que chegou, do mais novo ao mais antigo: 20 por padrão, até 100. Cada um diz se foi um bounce ou uma denúncia e o que aconteceu com ele. Não traz endereço nem as palavras do serviço. Os relatórios ficam guardados por 30 dias.

O resultado diz o que fizemos:

suppressed
O relatório disse que o endereço não existe mais, ou uma pessoa denunciou. O destinatário daquele e-mail está na lista de supressão.
stamped
Uma falha permanente que não diz que o endereço não existe mais, como uma recusa sobre a mensagem ou sobre nós. O e-mail fica marcado com bounce, e ninguém entra na lista.
no_send
Nenhum e-mail desta caixa corresponde ao relatório. Nada mudou.
other_recipient
Cita alguém para quem o e-mail não foi. Nada mudou.
not_final
Um atraso, ou uma falha que o serviço vai tentar de novo. Nada mudou.

Só o destinatário do e-mail a que o relatório se refere entra na lista. Um endereço citado dentro de um relatório nunca entra.

O código que lê os relatórios de cada serviço foi escrito a partir da documentação dele e ainda não rodou numa conta de verdade. Confira o primeiro relatório com reports antes de confiar nele.

Veja como uma caixa só de envio é conectada

Três proteções contra envio duplicado

Um timeout não diz se o e-mail saiu. Três proteções separadas impedem um segundo. Cada uma age num ponto diferente, e nenhuma substitui as outras.

Uma resposta salva: a chave da requisição HTTP

Toda escrita do SDK leva uma Idempotency-Key. Por 24 horas, o mesmo workspace, operação, chave e dados recebem a resposta salva em vez de uma segunda execução. Se salvar a resposta falhar, ou se a primeira tentativa ficar 15 minutos sem resposta, a chamada pode rodar de novo. Só essa proteção não garante um único e-mail.

A intenção de envio: um nome para um e-mail

É por isso que o intentKey é obrigatório aqui: é a proteção que vale para sempre. A mesma chave com a mesma requisição devolve sempre o mesmo e-mail, muito depois de a resposta salva sumir.

O worker toma o e-mail para si

Antes de entregar, o worker marca o registro do e-mail como seu, uma única vez, então dois workers não conseguem enviá-lo juntos. Se a conexão cair no fim do envio, o e-mail fica como unknown, conta como enviado e nunca é enviado de novo.

Nenhuma das três prova que o e-mail chegou. sent quer dizer que o servidor de e-mail por onde ele saiu o aceitou: o do seu provedor ou, num endereço de remetente, o nosso.

O guia de envio mostra as mesmas três proteções para rascunhos