Integração Resend Email

Configure notificações por email com Resend para suas tarefas agendadas

Nesta Página

Integração Resend Email

O Resend é o serviço de email transacional usado pelo Tarefa AI para enviar notificações, relatórios e alertas automaticamente quando suas tarefas são executadas.

Visão Geral

A integração com Resend permite:

  • Enviar notificações quando tarefas são concluídas
  • Enviar relatórios por email em horários agendados
  • Alertar sobre erros ou falhas
  • Enviar resultados de processamento de IA
  • Emails transacionais (boas-vindas, redefinição de senha)

Pré-requisitos

  • Conta no Resend
  • Domínio próprio verificado (ou usar domínio de teste)
  • Variáveis de ambiente configuradas

Configuração Passo a Passo

1. Criar Conta no Resend

  1. Acesse resend.com
  2. Clique em Get Started ou Sign Up
  3. Faça login com GitHub, Google ou email
  4. Confirme seu email de cadastro
  5. Complete o onboarding inicial

2. Obter API Key

  1. No dashboard, clique em API Keys no menu lateral
  2. Clique em Create API Key
  3. Dê um nome descritivo (ex: "Tarefa AI Production")
  4. Selecione as permissões:
    • Sending access (obrigatório)
    • Full access (não recomendado para produção)
  5. Clique em Add e copie a chave

3. Configurar Domínio

Opção A: Usar Domínio de Teste (Desenvolvimento)

O Resend fornece onboarding@resend.dev para testes:

# .env.local
RESEND_API_KEY=re_sua_chave_aqui
RESEND_FROM_EMAIL=onboarding@resend.dev

Opção B: Configurar Domínio Próprio (Produção)

  1. No dashboard, clique em Domains
  2. Clique em Add Domain
  3. Digite seu domínio (ex: tarefaai.com)
  4. Adicione os registros DNS fornecidos:
# Adicione estes registros no seu provedor DNS (Cloudflare, Vercel, etc)
 
# SPF (obrigatório)
Type: TXT
Name: @
Value: v=spf1 include:_spf.resend.com ~all
 
# DKIM (obrigatório)
Type: TXT
Name: resend._domainkey
Value: [valor fornecido pelo Resend]
 
# DMARC (recomendado)
Type: TXT
Name: _dmarc
Value: v=DMARC1; p=none; rua=mailto:dmarc@seudominio.com
  1. Aguarde verificação (5-30 minutos)
  2. Status mudará para ✅ Verified
# Acesse: cloudflare.com → DNS → Records
# Adicione os 3 registros TXT acima
# Acesse: vercel.com → Settings → Domains → DNS
# Adicione os registros manualmente
# Advanced DNS → Add New Record
# Tipo: TXT Record

4. Configurar Variáveis de Ambiente

Desenvolvimento Local (.env.local)

# Resend Configuration
RESEND_API_KEY=re_sua_chave_aqui
RESEND_FROM_EMAIL=noreply@seudominio.com
RESEND_FROM_NAME=Tarefa AI
 
# Optional: Reply-to
RESEND_REPLY_TO=contato@seudominio.com

Produção (Vercel/Railway/Render)

Vercel:

# Via CLI
vercel env add RESEND_API_KEY
vercel env add RESEND_FROM_EMAIL
 
# Ou no dashboard
# Settings → Environment Variables
# Adicione as variáveis e faça redeploy

Railway:

railway variables set RESEND_API_KEY=re_sua_chave
railway variables set RESEND_FROM_EMAIL=noreply@seudominio.com

Docker:

# docker-compose.yml
services:
  app:
    environment:
      - RESEND_API_KEY=${RESEND_API_KEY}
      - RESEND_FROM_EMAIL=${RESEND_FROM_EMAIL}

5. Verificar Instalação

Envie um email de teste:

// scripts/test-resend.ts
import { Resend } from 'resend';
 
const resend = new Resend(process.env.RESEND_API_KEY);
 
async function testEmail() {
  try {
    const result = await resend.emails.send({
      from: process.env.RESEND_FROM_EMAIL!,
      to: 'seu-email@example.com',
      subject: 'Teste de Integração Resend',
      html: '<h1>Funcionou!</h1><p>O Resend está configurado corretamente.</p>',
    });
 
    console.log('Email enviado com sucesso!', result);
  } catch (error) {
    console.error('Erro ao enviar email:', error);
  }
}
 
testEmail();

Execute:

npm run test:resend
# ou
npx tsx scripts/test-resend.ts

Exemplos Práticos

Exemplo 1: Notificação de Tarefa Concluída

// src/lib/email/task-completed.tsx
import { Resend } from 'resend';
import { TaskCompletedEmail } from '@/emails/task-completed';
 
const resend = new Resend(process.env.RESEND_API_KEY);
 
export async function sendTaskCompletedEmail({
  taskName,
  result,
  userEmail,
}: {
  taskName: string;
  result: any;
  userEmail: string;
}) {
  try {
    const { data, error } = await resend.emails.send({
      from: `${process.env.RESEND_FROM_NAME} <${process.env.RESEND_FROM_EMAIL}>`,
      to: userEmail,
      subject: `Tarefa "${taskName}" concluída com sucesso`,
      react: TaskCompletedEmail({ taskName, result }),
    });
 
    if (error) {
      console.error('Erro ao enviar email:', error);
      return { success: false, error };
    }
 
    console.log('Email enviado:', data?.id);
    return { success: true, emailId: data?.id };
  } catch (error) {
    console.error('Exceção ao enviar email:', error);
    return { success: false, error };
  }
}

Exemplo 2: Template React Email

// emails/task-completed.tsx
import {
  Body,
  Container,
  Head,
  Heading,
  Html,
  Link,
  Preview,
  Section,
  Text,
  Button,
} from '@react-email/components';
 
interface TaskCompletedEmailProps {
  taskName: string;
  result: {
    output?: string;
    executedAt: string;
    duration: number;
  };
}
 
export function TaskCompletedEmail({ taskName, result }: TaskCompletedEmailProps) {
  return (
    <Html>
      <Head />
      <Preview>Sua tarefa "{taskName}" foi concluída com sucesso</Preview>
      <Body style={main}>
        <Container style={container}>
          <Heading style={h1}>Tarefa Concluída ✅</Heading>
 
          <Text style={text}>
            Sua tarefa <strong>{taskName}</strong> foi executada com sucesso!
          </Text>
 
          <Section style={resultBox}>
            <Text style={resultLabel}>Resultado:</Text>
            <Text style={resultText}>{result.output || 'Sem output'}</Text>
          </Section>
 
          <Section style={detailsBox}>
            <Text style={detailLine}>
              <strong>Executado em:</strong> {new Date(result.executedAt).toLocaleString('pt-BR')}
            </Text>
            <Text style={detailLine}>
              <strong>Duração:</strong> {result.duration}ms
            </Text>
          </Section>
 
          <Button
            href={`https://tarefaai.com/dashboard/tasks/${taskName}`}
            style={button}
          >
            Ver Detalhes
          </Button>
 
          <Text style={footer}>
            Tarefa AI - Automação Inteligente<br />
            <Link href="https://tarefaai.com">tarefaai.com</Link>
          </Text>
        </Container>
      </Body>
    </Html>
  );
}
 
// Estilos
const main = { backgroundColor: '#f6f9fc', fontFamily: 'sans-serif' };
const container = { backgroundColor: '#ffffff', margin: '0 auto', padding: '20px 0 48px', marginBottom: '64px' };
const h1 = { color: '#333', fontSize: '24px', fontWeight: 'bold', margin: '40px 0', padding: '0', textAlign: 'center' as const };
const text = { color: '#333', fontSize: '16px', lineHeight: '26px', margin: '16px 0' };
const resultBox = { backgroundColor: '#f4f4f5', borderRadius: '5px', padding: '24px', margin: '24px 0' };
const resultLabel = { color: '#71717a', fontSize: '14px', fontWeight: 'bold', margin: '0 0 8px 0' };
const resultText = { color: '#18181b', fontSize: '14px', margin: '0', whiteSpace: 'pre-wrap' as const };
const detailsBox = { margin: '24px 0' };
const detailLine = { color: '#52525b', fontSize: '14px', margin: '8px 0' };
const button = { backgroundColor: '#2563eb', borderRadius: '5px', color: '#fff', fontSize: '16px', fontWeight: 'bold', textDecoration: 'none', textAlign: 'center' as const, display: 'block', padding: '12px', margin: '24px 0' };
const footer = { color: '#8898aa', fontSize: '12px', lineHeight: '16px', textAlign: 'center' as const, margin: '32px 0' };

Exemplo 3: Relatório Diário Agendado

// src/lib/tasks/daily-report.ts
import { generateText } from 'ai';
import { createOpenRouter } from '@openrouter/ai-sdk-provider';
import { sendDailyReportEmail } from '@/lib/email/daily-report';
 
export async function sendDailyReport() {
  // 1. Coletar dados do dia
  const todayTasks = await prisma.taskExecution.findMany({
    where: {
      executedAt: {
        gte: new Date(new Date().setHours(0, 0, 0, 0)),
      },
    },
    include: {
      task: true,
    },
  });
 
  // 2. Gerar insights com IA
  const openrouter = createOpenRouter({
    apiKey: process.env.OPENROUTER_API_KEY,
  });
 
  const insights = await generateText({
    model: openrouter('anthropic/claude-3.5-sonnet'),
    prompt: `
      Analise as seguintes execuções de tarefas e gere insights:
 
      ${JSON.stringify(todayTasks, null, 2)}
 
      Forneça:
      1. Resumo do dia
      2. Tarefas mais executadas
      3. Taxa de sucesso
      4. Recomendações
    `,
  });
 
  // 3. Enviar email para todos os usuários
  const users = await prisma.user.findMany({
    where: {
      emailNotifications: true,
    },
  });
 
  const results = await Promise.all(
    users.map((user) =>
      sendDailyReportEmail({
        userEmail: user.email!,
        userName: user.name || 'Usuário',
        stats: {
          totalTasks: todayTasks.length,
          successRate: calculateSuccessRate(todayTasks),
          topTasks: getTopTasks(todayTasks),
        },
        insights: insights.text,
      })
    )
  );
 
  console.log(`Relatórios enviados: ${results.filter((r) => r.success).length}/${results.length}`);
}

Exemplo 4: Email de Erro com Stack Trace

// src/lib/email/error-notification.ts
export async function sendErrorNotification({
  taskName,
  error,
  adminEmail,
}: {
  taskName: string;
  error: Error;
  adminEmail: string;
}) {
  await resend.emails.send({
    from: `Alertas ${process.env.RESEND_FROM_EMAIL}`,
    to: adminEmail,
    subject: `Erro na tarefa: ${taskName}`,
    html: `
      <h2>Erro Detectado</h2>
      <p><strong>Tarefa:</strong> ${taskName}</p>
      <p><strong>Erro:</strong> ${error.message}</p>
 
      <h3>Stack Trace:</h3>
      <pre style="background: #f4f4f5; padding: 16px; border-radius: 8px; overflow-x: auto;">
${error.stack}
      </pre>
 
      <p><strong>Timestamp:</strong> ${new Date().toISOString()}</p>
    `,
  });
}

Exemplo 5: Email com Anexo

// src/lib/email/send-with-attachment.ts
export async function sendReportWithAttachment({
  userEmail,
  reportData,
}: {
  userEmail: string;
  reportData: any;
}) {
  // Gerar CSV
  const csv = generateCSV(reportData);
  const buffer = Buffer.from(csv);
 
  await resend.emails.send({
    from: process.env.RESEND_FROM_EMAIL!,
    to: userEmail,
    subject: 'Seu relatório mensal',
    html: '<p>Segue em anexo seu relatório mensal.</p>',
    attachments: [
      {
        filename: `relatorio-${new Date().toISOString().split('T')[0]}.csv`,
        content: buffer,
      },
    ],
  });
}

Templates Prontos

Template de Boas-vindas

// emails/welcome.tsx
export function WelcomeEmail({ userName }: { userName: string }) {
  return (
    <Html>
      <Head />
      <Preview>Bem-vindo ao Tarefa AI!</Preview>
      <Body style={main}>
        <Container style={container}>
          <Heading style={h1}>Bem-vindo, {userName}! 🎉</Heading>
 
          <Text style={text}>
            Obrigado por se juntar ao Tarefa AI. Estamos animados para ter você conosco!
          </Text>
 
          <Section style={featuresBox}>
            <Heading style={h2}>O que você pode fazer:</Heading>
            <ul>
              <li>Agendar tarefas com IA</li>
              <li>Automatizar workflows</li>
              <li>Receber notificações por email e WhatsApp</li>
              <li>Integrar com Notion e outras ferramentas</li>
            </ul>
          </Section>
 
          <Button href="https://tarefaai.com/dashboard" style={button}>
            Começar Agora
          </Button>
 
          <Text style={footer}>
            Precisa de ajuda? <Link href="https://tarefaai.com/docs">Consulte nossa documentação</Link>
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

Template de Redefinição de Senha

// emails/password-reset.tsx
export function PasswordResetEmail({ resetLink }: { resetLink: string }) {
  return (
    <Html>
      <Head />
      <Preview>Redefina sua senha - Tarefa AI</Preview>
      <Body style={main}>
        <Container style={container}>
          <Heading style={h1}>Redefinir Senha</Heading>
 
          <Text style={text}>
            Você solicitou a redefinição de senha. Clique no botão abaixo para criar uma nova senha:
          </Text>
 
          <Button href={resetLink} style={button}>
            Redefinir Senha
          </Button>
 
          <Text style={text}>
            Este link expira em 1 hora.
          </Text>
 
          <Text style={footer}>
            Se você não solicitou isso, ignore este email.
          </Text>
        </Container>
      </Body>
    </Html>
  );
}

Otimização e Boas Práticas

1. Rate Limiting

// src/lib/email/rate-limit.ts
import { Ratelimit } from '@upstash/ratelimit';
import { Redis } from '@upstash/redis';
 
const redis = new Redis({
  url: process.env.UPSTASH_REDIS_URL!,
  token: process.env.UPSTASH_REDIS_TOKEN!,
});
 
const ratelimit = new Ratelimit({
  redis,
  limiter: Ratelimit.slidingWindow(10, '1 h'), // 10 emails por hora por usuário
});
 
export async function sendEmailWithRateLimit(userId: string, emailData: any) {
  const { success } = await ratelimit.limit(userId);
 
  if (!success) {
    throw new Error('Limite de emails excedido. Aguarde 1 hora.');
  }
 
  return resend.emails.send(emailData);
}

2. Retry com Exponential Backoff

// src/lib/email/retry.ts
export async function sendEmailWithRetry(
  emailData: any,
  maxRetries = 3
) {
  for (let i = 0; i < maxRetries; i++) {
    try {
      const result = await resend.emails.send(emailData);
      return result;
    } catch (error) {
      if (i === maxRetries - 1) throw error;
 
      const delay = Math.pow(2, i) * 1000; // 1s, 2s, 4s
      console.log(`Tentativa ${i + 1} falhou. Aguardando ${delay}ms...`);
      await new Promise((resolve) => setTimeout(resolve, delay));
    }
  }
}

3. Batch Sending

// src/lib/email/batch.ts
export async function sendBatchEmails(emails: Array<any>) {
  const BATCH_SIZE = 100; // Resend recomenda max 100 por batch
 
  for (let i = 0; i < emails.length; i += BATCH_SIZE) {
    const batch = emails.slice(i, i + BATCH_SIZE);
 
    await resend.batch.send(batch);
 
    // Delay entre batches
    if (i + BATCH_SIZE < emails.length) {
      await new Promise((resolve) => setTimeout(resolve, 1000));
    }
  }
}

4. Tracking de Emails

// src/lib/email/tracking.ts
export async function sendEmailWithTracking(emailData: any) {
  const result = await resend.emails.send({
    ...emailData,
    tags: [
      { name: 'category', value: 'transactional' },
      { name: 'environment', value: process.env.NODE_ENV },
    ],
  });
 
  // Salvar no banco para tracking
  await prisma.emailLog.create({
    data: {
      emailId: result.data?.id,
      to: emailData.to,
      subject: emailData.subject,
      status: 'sent',
      sentAt: new Date(),
    },
  });
 
  return result;
}

Troubleshooting

Erro: "API key is invalid"

Causa: Chave API incorreta ou não configurada

Solução:

# Verificar variável de ambiente
echo $RESEND_API_KEY
 
# Deve começar com re_
# Se não estiver definida:
# Adicione ao .env.local ou variáveis do deploy

Erro: "Domain not verified"

Causa: Registros DNS não configurados ou ainda não propagados

Solução:

  1. Verifique os registros DNS no seu provedor
  2. Use ferramenta de verificação: mxtoolbox.com/SuperTool.aspx
  3. Aguarde até 48h para propagação DNS
  4. Para desenvolvimento, use onboarding@resend.dev

Emails Não Chegam

Possíveis causas e soluções:

  1. Na pasta de spam:

    • Configure SPF, DKIM e DMARC corretamente
    • Use domínio verificado
    • Evite palavras de spam no assunto
  2. Email inválido:

// Validar email antes de enviar
import { z } from 'zod';
 
const emailSchema = z.string().email();
 
if (!emailSchema.safeParse(userEmail).success) {
  throw new Error('Email inválido');
}
  1. Limite de envio atingido:
// Verificar limites no dashboard Resend
// Plano gratuito: 100/dia, 3.000/mês

Erro: "Rate limit exceeded"

Causa: Muitos emails enviados rapidamente

Solução:

// Implementar delay entre envios
async function sendEmailsWithDelay(emails: any[]) {
  for (const email of emails) {
    await resend.emails.send(email);
    await new Promise((resolve) => setTimeout(resolve, 100)); // 100ms delay
  }
}

Templates não Renderizam

Causa: Componente React Email incorreto

Solução:

# Instalar dependências
npm install @react-email/components
 
# Testar template localmente
npm run email:dev
// package.json
{
  "scripts": {
    "email:dev": "email dev"
  }
}

Monitoramento e Analytics

Dashboard do Resend

Acesse resend.com/emails para ver:

  • Emails enviados
  • Taxa de entrega
  • Opens e clicks (se configurado)
  • Bounces e reclamações

Webhooks para Eventos

Configure webhooks para receber eventos:

// src/app/api/webhooks/resend/route.ts
export async function POST(request: Request) {
  const event = await request.json();
 
  switch (event.type) {
    case 'email.delivered':
      await prisma.emailLog.update({
        where: { emailId: event.data.email_id },
        data: { status: 'delivered', deliveredAt: new Date() },
      });
      break;
 
    case 'email.bounced':
      await prisma.emailLog.update({
        where: { emailId: event.data.email_id },
        data: { status: 'bounced', bouncedAt: new Date() },
      });
      break;
 
    case 'email.complained':
      // Marcar usuário para não enviar mais emails
      await prisma.user.update({
        where: { email: event.data.recipient },
        data: { emailNotifications: false },
      });
      break;
  }
 
  return new Response('OK', { status: 200 });
}

Configure o webhook no Resend:

  1. Acesse Settings → Webhooks
  2. Adicione: https://seusite.com/api/webhooks/resend
  3. Selecione eventos desejados

Screenshot Placeholder

[INSERIR SCREENSHOT: Dashboard do Resend mostrando estatísticas de emails enviados]

Recursos Adicionais

Documentação Oficial

Comunidade

Suporte


Próximos Passos

Agora que você configurou o Resend, explore:

Precisa de ajuda?

Consulte nosso guia completo de troubleshooting

Learn more