Envio de E-mails e Integração com APIs de Comunicação

Envio de E-mails e Integração com APIs de Comunicação

E-mail, SMS, WhatsApp e Slack a partir do Python: smtplib com EmailMessage, templates Jinja2, SendGrid, Twilio e Slack atrás de um sistema único de notificações — e os erros que pesam aqui, como TLS sem verificação de certificado, remetente acentuado sem endereço e queda de rede que derruba o lote.
Python

• • 20 min de leitura

Comunicação programática é um pilar de qualquer sistema moderno — confirmações de cadastro, alertas de sistema, notificações de pagamento, relatórios automáticos, mensagens de suporte. Python tem suporte nativo a SMTP para e-mails e integração direta com as principais APIs de comunicação do mercado: SendGrid, Twilio, Slack e WhatsApp. Neste artigo veremos como implementar cada uma dessas integrações de forma profissional.

E-mail com smtplib: Protocolo Nativo

import mimetypes
import smtplib
import ssl
from email.message import EmailMessage
from email.utils import formataddr
from pathlib import Path


class ClienteEmail:
    """Cliente SMTP reutilizável com suporte a TLS."""

    def __init__(self, host: str, porta: int, usuario: str, senha: str, tls: bool = True):
        self.host    = host
        self.porta   = porta
        self.usuario = usuario
        self.senha   = senha
        self.tls     = tls

    def _criar_conexao(self) -> smtplib.SMTP:
        smtp = smtplib.SMTP(self.host, self.porta, timeout=30)
        if self.tls:
            # Sem context, o starttls() não verifica o certificado do servidor
            smtp.starttls(context=ssl.create_default_context())
        smtp.login(self.usuario, self.senha)
        return smtp

    def enviar(self, destinatarios: list, assunto: str, corpo_texto: str,
               corpo_html: str = None, anexos: list = None, cc: list = None,
               bcc: list = None, remetente_nome: str = None) -> bool:
        msg = EmailMessage()
        msg["From"]    = formataddr((remetente_nome, self.usuario)) if remetente_nome else self.usuario
        msg["To"]      = ", ".join(destinatarios)
        msg["Subject"] = assunto
        if cc:
            msg["Cc"] = ", ".join(cc)

        msg.set_content(corpo_texto)                        # texto puro (fallback)
        if corpo_html:
            msg.add_alternative(corpo_html, subtype="html")  # preferido pelos clientes

        for caminho_anexo in anexos or []:
            arquivo = Path(caminho_anexo)                    # ausente: FileNotFoundError,
            tipo, _ = mimetypes.guess_type(arquivo.name)     # em vez de sair sem o anexo
            principal, sub = (tipo or "application/octet-stream").split("/")
            msg.add_attachment(arquivo.read_bytes(), maintype=principal,
                               subtype=sub, filename=arquivo.name)

        try:
            with self._criar_conexao() as smtp:
                # send_message lê To e Cc do cabeçalho; o Bcc vai só no envelope
                smtp.send_message(msg, to_addrs=destinatarios + (cc or []) + (bcc or []))
            print(f"E-mail enviado para {len(destinatarios + (cc or []) + (bcc or []))} destinatário(s).")
            return True
        except (smtplib.SMTPException, OSError) as e:   # OSError: rede, DNS, timeout
            print(f"Erro ao enviar e-mail: {e}")
            return False


# Configurações por provedor
def gmail(usuario, senha):
    return ClienteEmail("smtp.gmail.com", 587, usuario, senha)

def outlook(usuario, senha):
    return ClienteEmail("smtp.office365.com", 587, usuario, senha)

def yahoo(usuario, senha):
    return ClienteEmail("smtp.mail.yahoo.com", 587, usuario, senha)


# Uso
cliente = gmail(
    os.getenv("EMAIL_USER"),
    os.getenv("EMAIL_PASS")   # use App Password no Gmail
)

cliente.enviar(
    destinatarios=["ana@email.com"],
    assunto="Boletim Escolar — Março 2024",
    corpo_texto="Segue em anexo seu boletim escolar.",
    corpo_html="""
        <h2>Olá, Ana!</h2>
        <p>Segue em anexo seu <strong>boletim escolar</strong> de Março 2024.</p>
        <p>Em caso de dúvidas, entre em contato com a secretaria.</p>
    """,
    anexos=["boletim_2024-03.pdf"],
    remetente_nome="Sistema Escolar"
)

Templates de E-mail com Jinja2

from jinja2 import Environment, FileSystemLoader
from pathlib import Path


def criar_ambiente_templates():
    Path("templates/email").mkdir(parents=True, exist_ok=True)
    return Environment(
        loader=FileSystemLoader("templates/email"),
        autoescape=True
    )


# templates/email/base.html
TEMPLATE_BASE = """
<!DOCTYPE html>
<html lang="pt-BR">
<head>
  <meta charset="UTF-8">
  <style>
    body        { font-family: Arial, sans-serif; background: #f4f4f4; margin: 0; }
    .container  { max-width: 600px; margin: 20px auto; background: white;
                  border-radius: 8px; overflow: hidden; }
    .header     { background: #1F4E79; color: white; padding: 30px;
                  text-align: center; }
    .header h1  { margin: 0; font-size: 22px; }
    .body       { padding: 30px; color: #333; line-height: 1.6; }
    .footer     { background: #f4f4f4; padding: 15px; text-align: center;
                  font-size: 12px; color: #888; }
    .btn        { display: inline-block; background: #2E75B6; color: white;
                  padding: 12px 24px; border-radius: 4px; text-decoration: none;
                  font-weight: bold; margin: 15px 0; }
    .alerta     { background: #FCE4D6; border-left: 4px solid #C00000;
                  padding: 12px; border-radius: 4px; }
    .sucesso    { background: #E2EFDA; border-left: 4px solid #375623;
                  padding: 12px; border-radius: 4px; }
    table       { width: 100%; border-collapse: collapse; }
    th          { background: #2E75B6; color: white; padding: 10px; }
    td          { padding: 8px; border-bottom: 1px solid #eee; }
    tr:nth-child(even) td { background: #f9f9f9; }
  </style>
</head>
<body>
  <div class="container">
    <div class="header">
      <h1>{% block titulo %}Sistema Escolar{% endblock %}</h1>
    </div>
    <div class="body">
      {% block conteudo %}{% endblock %}
    </div>
    <div class="footer">
      <p>© 2024 Sistema Escolar. Este é um e-mail automático.</p>
      <p><a href="{{ url_cancelar }}">Cancelar inscrição</a></p>
    </div>
  </div>
</body>
</html>
"""

# templates/email/boas_vindas.html
TEMPLATE_BOAS_VINDAS = """
{% extends "base.html" %}

{% block titulo %}Bem-vindo(a), {{ nome }}!{% endblock %}

{% block conteudo %}
<p>Olá, <strong>{{ nome }}</strong>!</p>
<p>Sua conta foi criada com sucesso no <strong>Sistema Escolar</strong>.</p>

<div class="sucesso">
  <strong>Seus dados de acesso:</strong><br>
  E-mail: {{ email }}<br>
  Senha temporária: {{ senha_temp }}
</div>

<p>Por segurança, recomendamos alterar sua senha no primeiro acesso.</p>

<a href="{{ url_acesso }}" class="btn">Acessar o Sistema</a>

<p>Em caso de problemas, entre em contato com a secretaria.</p>
{% endblock %}
"""


class ServicoEmail:
    def __init__(self, cliente: ClienteEmail):
        self.cliente = cliente
        self.env     = criar_ambiente_templates()

        # Salva templates em disco
        Path("templates/email/base.html").write_text(TEMPLATE_BASE, encoding="utf-8")   # o Jinja lê UTF-8
        Path("templates/email/boas_vindas.html").write_text(TEMPLATE_BOAS_VINDAS, encoding="utf-8")

    def _renderizar(self, template_nome: str, contexto: dict) -> str:
        template = self.env.get_template(template_nome)
        return template.render(**contexto, url_cancelar="#")

    def boas_vindas(self, nome: str, email: str, senha_temp: str):
        html = self._renderizar("boas_vindas.html", {
            "nome":       nome,
            "email":      email,
            "senha_temp": senha_temp,
            "url_acesso": "https://sistema.escolar.com/login"
        })
        texto = f"Olá, {nome}! Sua conta foi criada. Senha temporária: {senha_temp}"

        return self.cliente.enviar(
            destinatarios=[email],
            assunto=f"Bem-vindo(a) ao Sistema Escolar, {nome}!",
            corpo_texto=texto,
            corpo_html=html,
            remetente_nome="Sistema Escolar"
        )

    def boletim(self, aluno: dict, pdf_path: str):
        html = f"""
        <h2>Olá, {aluno['nome']}!</h2>
        <p>Seu boletim do mês está disponível em anexo.</p>
        <table>
          <tr><th>Disciplina</th><th>Nota</th><th>Status</th></tr>
          {''.join(
              f"<tr><td>{d['nome']}</td><td>{d['nota']}</td>"
              f"<td>{'✓' if d['nota'] >= 6 else '✗'}</td></tr>"
              for d in aluno.get('disciplinas', [])
          )}
        </table>
        """
        return self.cliente.enviar(
            destinatarios=[aluno["email"]],
            assunto="Seu Boletim Escolar está disponível",
            corpo_texto=f"Olá {aluno['nome']}, seu boletim está em anexo.",
            corpo_html=html,
            anexos=[pdf_path]
        )

SendGrid: API Profissional de E-mail

pip install sendgrid
import os
from sendgrid import SendGridAPIClient
from sendgrid.helpers.mail import (
    Mail, To, From, Subject,
    PlainTextContent, HtmlContent,
    Attachment, FileContent, FileName,
    FileType, Disposition
)
import base64
from pathlib import Path


class ClienteSendGrid:
    def __init__(self, api_key: str, remetente: str, nome_remetente: str = ""):
        self.sg         = SendGridAPIClient(api_key)
        self.remetente  = remetente
        self.nome       = nome_remetente

    def enviar(
        self,
        destinatario: str,
        assunto:      str,
        html:         str,
        texto:        str = None,
        anexos:       list = None
    ) -> bool:
        mensagem = Mail(
            from_email=From(self.remetente, self.nome),
            to_emails=To(destinatario),
            subject=Subject(assunto),
            plain_text_content=PlainTextContent(texto or ""),
            html_content=HtmlContent(html)
        )

        if anexos:
            for caminho in anexos:
                arquivo = Path(caminho)
                with open(arquivo, "rb") as f:
                    dados = base64.b64encode(f.read()).decode()

                anexo = Attachment(
                    file_content=FileContent(dados),
                    file_name=FileName(arquivo.name),
                    file_type=FileType("application/octet-stream"),
                    disposition=Disposition("attachment")
                )
                mensagem.attachment = anexo

        try:
            resposta = self.sg.send(mensagem)
            print(f"SendGrid: {resposta.status_code}")
            return resposta.status_code in [200, 202]
        except Exception as e:
            print(f"Erro SendGrid: {e}")
            return False


sg = ClienteSendGrid(
    api_key=os.getenv("SENDGRID_API_KEY"),
    remetente="noreply@escola.com",
    nome_remetente="Sistema Escolar"
)

sg.enviar(
    destinatario="aluno@email.com",
    assunto="Confirmação de Matrícula",
    html="<h1>Matrícula confirmada!</h1><p>Bem-vindo(a).</p>",
    texto="Matrícula confirmada! Bem-vindo(a)."
)

Twilio: SMS e WhatsApp

pip install twilio
import os
import json
from twilio.rest import Client
from twilio.base.exceptions import TwilioRestException


class ClienteTwilio:
    def __init__(self, account_sid: str, auth_token: str, numero: str):
        self.client = Client(account_sid, auth_token)
        self.numero = numero   # número Twilio: +15551234567

    def enviar_sms(self, destinatario: str, mensagem: str) -> bool:
        """Envia SMS. destinatario no formato +5511999999999"""
        try:
            msg = self.client.messages.create(
                body=mensagem,
                from_=self.numero,
                to=destinatario
            )
            print(f"SMS enviado: {msg.sid}")
            return True
        except (TwilioRestException, OSError) as e:   # OSError: rede fora, proxy, DNS
            print(f"Erro Twilio: {e}")
            return False

    def enviar_whatsapp(self, destinatario: str, mensagem: str) -> bool:
        """
        Envia WhatsApp via Twilio Sandbox.
        destinatario: whatsapp:+5511999999999
        """
        try:
            msg = self.client.messages.create(
                body=mensagem,
                from_=f"whatsapp:{self.numero}",
                to=f"whatsapp:{destinatario}"
            )
            print(f"WhatsApp enviado: {msg.sid}")
            return True
        except (TwilioRestException, OSError) as e:
            print(f"Erro WhatsApp: {e}")
            return False

    def enviar_whatsapp_template(
        self,
        destinatario: str,
        template_sid: str,
        variaveis:    dict
    ) -> bool:
        """Envia mensagem usando template aprovado pelo WhatsApp."""
        try:
            msg = self.client.messages.create(
                from_=f"whatsapp:{self.numero}",
                to=f"whatsapp:{destinatario}",
                content_sid=template_sid,
                content_variables=json.dumps(variaveis)   # str(dict) sai com aspas simples: não é JSON
            )
            return True
        except (TwilioRestException, OSError) as e:
            print(f"Erro template: {e}")
            return False


twilio = ClienteTwilio(
    account_sid=os.getenv("TWILIO_ACCOUNT_SID"),
    auth_token= os.getenv("TWILIO_AUTH_TOKEN"),
    numero=     os.getenv("TWILIO_NUMBER")
)

# SMS
twilio.enviar_sms(
    "+5511999999999",
    "Sua nota foi lançada: 8.5. Acesse o portal para ver o boletim."
)

# WhatsApp
twilio.enviar_whatsapp(
    "+5511999999999",
    "🎓 *Sistema Escolar*\nOlá, Ana! Sua nota de Matemática foi lançada: *8.5*"
)

Slack: Notificações para Equipes

pip install slack-sdk
import os
from slack_sdk import WebClient
from slack_sdk.errors import SlackApiError


class ClienteSlack:
    def __init__(self, token: str):
        self.client = WebClient(token=token)

    def enviar_mensagem(self, canal: str, texto: str) -> bool:
        try:
            self.client.chat_postMessage(channel=canal, text=texto)
            return True
        except (SlackApiError, OSError) as e:   # OSError: o Slack fora do ar
            print(f"Erro Slack: {e}")
            return False

    def enviar_bloco(self, canal: str, blocos: list, texto: str = "") -> bool:
        """Envia mensagem com Block Kit — layout rico."""
        try:
            self.client.chat_postMessage(
                channel=canal, blocks=blocos,
                text=texto,   # o que a notificação mostra; sem ele, o SDK avisa
            )
            return True
        except (SlackApiError, OSError) as e:
            print(f"Erro Slack: {e}")
            return False

    def notificar_alerta(self, canal: str, titulo: str, mensagem: str, urgente: bool = False):
        emoji = "🚨" if urgente else "ℹ️"

        blocos = [
            {
                "type": "header",
                "text": {"type": "plain_text", "text": f"{emoji} {titulo}"}
            },
            {
                "type": "section",
                "text": {"type": "mrkdwn", "text": mensagem}
            },
            {
                "type": "context",
                "elements": [{
                    "type": "mrkdwn",
                    "text": f"*Sistema Escolar* | {datetime.now().strftime('%d/%m/%Y %H:%M')}"
                }]
            }
        ]
        return self.enviar_bloco(canal, blocos, texto=f"{emoji} {titulo}")

    def notificar_boletim(self, canal: str, aluno: str, nota: float):
        status = "✅ Aprovado" if nota >= 6 else "❌ Reprovado"
        blocos = [
            {
                "type": "section",
                "text": {
                    "type": "mrkdwn",
                    "text": f"*Nova nota lançada*\n*Aluno:* {aluno}\n*Nota:* {nota:.1f}\n*Status:* {status}"
                }
            },
            {"type": "divider"},
            {
                "type": "actions",
                "elements": [{
                    "type": "button",
                    "text": {"type": "plain_text", "text": "Ver Boletim"},
                    "url":  "https://sistema.escolar.com/boletim"
                }]
            }
        ]
        return self.enviar_bloco(canal, blocos, texto=f"Nova nota: {aluno}")


from datetime import datetime

slack = ClienteSlack(token=os.getenv("SLACK_BOT_TOKEN"))

slack.notificar_alerta(
    canal="#alertas-sistema",
    titulo="Erro no processamento",
    mensagem="Falha ao gerar boletins da turma B. Verificar logs.",
    urgente=True
)

slack.notificar_boletim(
    canal="#notas",
    aluno="Ana Silva",
    nota=9.5
)

Exemplo Completo: Sistema Centralizado de Notificações

from enum import Enum
from dataclasses import dataclass
from typing import Optional
import os


class Canal(str, Enum):
    EMAIL    = "email"
    SMS      = "sms"
    WHATSAPP = "whatsapp"
    SLACK    = "slack"


@dataclass
class Notificacao:
    destinatario: str
    assunto:      str
    mensagem:     str
    mensagem_html: Optional[str] = None
    canal:        Canal = Canal.EMAIL
    urgente:      bool  = False
    anexos:       list  = None


class SistemaNotificacoes:
    def __init__(self):
        self._email   = ClienteEmail(
            host=os.getenv("SMTP_HOST", "smtp.gmail.com"),
            porta=587,
            usuario=os.getenv("EMAIL_USER"),
            senha=os.getenv("EMAIL_PASS")
        )
        self._twilio  = ClienteTwilio(
            os.getenv("TWILIO_ACCOUNT_SID"),
            os.getenv("TWILIO_AUTH_TOKEN"),
            os.getenv("TWILIO_NUMBER")
        )
        self._slack   = ClienteSlack(os.getenv("SLACK_BOT_TOKEN"))

    def enviar(self, notif: Notificacao) -> bool:
        print(f"[{notif.canal.value.upper()}] → {notif.destinatario}: {notif.assunto}")

        if notif.canal == Canal.EMAIL:
            return self._email.enviar(
                destinatarios=[notif.destinatario],
                assunto=notif.assunto,
                corpo_texto=notif.mensagem,
                corpo_html=notif.mensagem_html,
                anexos=notif.anexos or []
            )
        elif notif.canal == Canal.SMS:
            return self._twilio.enviar_sms(notif.destinatario, notif.mensagem)

        elif notif.canal == Canal.WHATSAPP:
            return self._twilio.enviar_whatsapp(notif.destinatario, notif.mensagem)

        elif notif.canal == Canal.SLACK:
            return self._slack.notificar_alerta(
                canal=notif.destinatario,
                titulo=notif.assunto,
                mensagem=notif.mensagem,
                urgente=notif.urgente
            )
        return False

    def enviar_multiplos(self, notificacoes: list) -> dict:
        resultados = {"sucesso": 0, "falha": 0}
        for notif in notificacoes:
            if self.enviar(notif):
                resultados["sucesso"] += 1
            else:
                resultados["falha"] += 1
        return resultados


# Uso
sistema = SistemaNotificacoes()

# Notificar resultado de prova por múltiplos canais
aluno = {
    "nome":      "Ana Silva",
    "email":     "ana@email.com",
    "telefone":  "+5511999999999",
    "nota":      9.5
}

notificacoes = [
    Notificacao(
        destinatario=aluno["email"],
        assunto="Sua nota foi lançada!",
        mensagem=f"Olá {aluno['nome']}, sua nota é {aluno['nota']}.",
        mensagem_html=f"<h2>Olá, {aluno['nome']}!</h2><p>Nota: <strong>{aluno['nota']}</strong></p>",
        canal=Canal.EMAIL
    ),
    Notificacao(
        destinatario=aluno["telefone"],
        assunto="Nota lançada",
        mensagem=f"Olá {aluno['nome']}! Sua nota: {aluno['nota']}. Aprovado(a)! 🎉",
        canal=Canal.SMS
    ),
    Notificacao(
        destinatario="#professores",
        assunto="Nota lançada",
        mensagem=f"*{aluno['nome']}* recebeu nota *{aluno['nota']}*",
        canal=Canal.SLACK
    ),
]

resultado = sistema.enviar_multiplos(notificacoes)
print(f"\nResultado: {resultado['sucesso']} enviados, {resultado['falha']} falhas")

Enviar uma mensagem por programa é sempre a mesma operação vestida de formas diferentes: montar o conteúdo, autenticar num serviço e entregar. Com smtplib e EmailMessage, o próprio Python monta um e-mail com texto, HTML e anexos e o entrega a qualquer servidor SMTP; o Jinja2 separa o layout do código; SendGrid, Twilio e Slack trocam o protocolo por uma API HTTP, com entregabilidade, SMS, WhatsApp e mensagens ricas para equipes. O sistema centralizado do fim põe todos atrás da mesma interface, e o passo seguinte natural é tirar o envio da requisição, com uma fila como a de Celery: Filas de Tarefas e Processamento Assíncrono, para que um provedor lento não atrase a resposta ao usuário.

Comunicação é a parte do sistema que fala com o mundo em nome da instituição, e por isso os erros daqui pesam mais. Um TLS sem verificação de certificado entrega a senha a quem estiver no meio do caminho; um remetente mal codificado vira spam ou some; um anexo que faltou sai como "segue em anexo" sem nada; e uma queda de rede num provedor não pode derrubar o envio pelos outros. Vale testar os envios contra um servidor SMTP local, que mostra a mensagem exatamente como sai, e tratar o retorno de cada canal como um dado que alguém vai conferir.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Uma auditoria de segurança aponta que o sistema de notificações da escola "aceita qualquer certificado" no envio de e-mails. O desenvolvedor responde que o código chama smtp.starttls(), "então a conexão é criptografada". Quem tem razão? Mostre o que o starttls() faz sem argumentos e qual é o risco concreto.

Ver resposta

✓ Resposta: Os dois, em parte, e o risco está na parte que o desenvolvedor não viu. A conexão é criptografada, mas o servidor não é autenticado. Sem o parâmetro context, o starttls() usa ssl._create_stdlib_context() — medido no código-fonte da 3.14 —, e esse contexto tem verify_mode igual a 0, CERT_NONE: qualquer certificado é aceito, inclusive um autoassinado de quem se colocou entre o servidor e o provedor, numa rede Wi-Fi de evento ou num proxy comprometido. Esse intermediário recebe o comando AUTH criptografado para ele mesmo e fica com o usuário e a senha da conta de envio. Com ssl.create_default_context(), o verify_mode é 2, CERT_REQUIRED, e o nome do servidor também é conferido. A correção é uma linha: smtp.starttls(context=ssl.create_default_context()), como no cliente corrigido do artigo. O mesmo vale para smtplib.SMTP_SSL, que recebe context no construtor. Criptografia sem autenticação só protege contra quem escuta sem interferir.

Exercício 2

Depois que o remetente passou a se chamar "Secretaria São José", as respostas dos pais pararam de chegar, e parte dos e-mails começou a cair no spam. Com "Secretaria Escolar" funcionava. O cabeçalho foi montado com msg["From"] = f"{remetente_nome} <{self.usuario}>" num MIMEMultipart. O que o acento mudou?

Ver resposta

✓ Resposta: Um cabeçalho de e-mail só pode ter ASCII; o que não é ASCII é codificado em "encoded-words", como =?utf-8?q?...?=. A API antiga do email.mime recebeu a string inteira e a codificou inteira — nome, sinal de menor, endereço e sinal de maior. Medido com um servidor SMTP local: o cabeçalho saiu como =?utf-8?q?Secretaria_S=C3=A3o_Jos=C3=A9_=3Csecretaria=40escola=2Eexample=3E?=, e o parser do destino entendeu tudo como o nome de uma caixa sem domínio: username='Secretaria São José <secretaria@escola.example>', domain=''. Sem endereço válido no From, o "Responder" não tem para onde ir, e os filtros tratam a mensagem como malformada. Com "Secretaria Escolar", tudo ASCII, nada era codificado e o defeito ficava invisível. A correção é montar o cabeçalho com email.utils.formataddr((nome, endereco)), que codifica só o nome, e usar a API moderna, EmailMessage: medido, o From passou a ter display_name='Secretaria São José' e o domínio escola.example. Todo teste de e-mail deveria incluir um nome acentuado.

Exercício 3

Na virada do mês, trezentos pais receberam "Segue em anexo o boletim de março" — sem anexo nenhum. O log registra "E-mail enviado para 1 destinatário(s)" trezentas vezes, e o enviar devolveu True em todas. O job que gera os PDFs tinha falhado antes, mas o de envio rodou no horário. Onde o cliente de e-mail do artigo contribuiu para o incidente?

Ver resposta

✓ Resposta: No if not arquivo.exists(): continue. O cliente tratava um anexo ausente como opcional: pulava o arquivo em silêncio e enviava o resto. Medido com um servidor SMTP local, pedindo dois anexos, um deles inexistente: a mensagem saiu com um só, e o enviar devolveu True. Para quem chama, não havia como saber. Um anexo pedido e não encontrado é erro, porque o texto do e-mail promete o arquivo. O cliente corrigido lê o arquivo com Path.read_bytes(), que levanta FileNotFoundError antes de qualquer conexão — medido. Havia ainda um segundo defeito, menos visível: o anexo era colocado dentro de uma parte multipart/alternative, que significa "versões diferentes do mesmo conteúdo", ao lado do texto e do HTML. A estrutura correta é multipart/mixed, com o alternative dentro e o anexo ao lado, que é o que o EmailMessage monta sozinho com add_alternative e add_attachment — medido. E, no desenho do processo, o job de envio deveria depender do de geração, e não de um horário.

Exercício 4

O enviar_multiplos recebe trinta notificações: e-mail, SMS e Slack. No dia em que a rede do escritório bloqueou o acesso ao Slack, o script terminou com um traceback de URLError na terceira notificação, e as outras vinte e sete nunca saíram — inclusive os e-mails, que funcionavam. Os métodos de envio tinham try/except. Por que não seguraram, e como corrigir?

Ver resposta

✓ Resposta: Porque cada except pegava só a exceção da própria biblioteca, e essas exceções representam respostas de erro do serviço: SlackApiError quando o Slack responde que o canal não existe, TwilioRestException quando a Twilio recusa o número, SMTPException quando o servidor recusa o login. Quando o serviço nem responde, o erro é de rede, e vem da camada de baixo. Medido: com o Slack inacessível, chat_postMessage levantou URLError; a Twilio atrás de um proxy fora do ar levantou ProxyError, do requests; e o SMTP numa porta fechada levantou ConnectionRefusedError. Nenhuma delas é subclasse da exceção da biblioteca, mas as três são OSError. A correção nos clientes é except (SlackApiError, OSError) e equivalentes — medido: os dois passaram a devolver False. Cuidado com o tratamento: o e.response['error'] do Slack não existe num OSError, e o próprio except levantaria AttributeError. Como defesa extra, o enviar_multiplos deve isolar cada envio com seu próprio try, porque um canal quebrado não pode impedir os outros.

Exercício 5

Um desenvolvedor passou a usar templates aprovados do WhatsApp com content_variables=str({"1": "Ana", "2": "9.5"}). Nos testes do Python, o valor "parece JSON" quando impresso, mas nenhuma mensagem sai com as variáveis preenchidas. Explique a diferença entre o que foi enviado e o que a API espera.

Ver resposta

✓ Resposta: str() de um dicionário é a representação do Python, não JSON. Medido: str({"1": "Ana", "2": "9.5"}) dá {'1': 'Ana', '2': '9.5'}, com aspas simples, e json.loads desse texto levanta JSONDecodeError, porque JSON só aceita aspas duplas. O parâmetro content_variables da Twilio é uma string com um objeto JSON, e o que chegou não é JSON válido. Com dados que por acaso não têm aspas nem acentos, a diferença é só essa troca de aspas, e passa despercebida numa inspeção visual; com um nome como "D'Ávila", a representação do Python passa a usar aspas duplas por fora e muda de forma de novo. A correção é json.dumps(variaveis), como no cliente corrigido. A regra geral vale para qualquer API: quando o parâmetro pede JSON em texto, gere com json.dumps, nunca com str ou f-string. Em testes, validar o parâmetro com json.loads antes de enviar pega o erro sem depender da resposta do serviço.

Comentários

Mais em Python

Classes e Objetos: os fundamentos da Orientação a Objetos
Classes e Objetos: os fundamentos da Orientação a Objetos

Classes, objetos, métodos dunder e encapsulamento em Python. Com duas…

Análise de Dados com Pandas Avançado e Plotly
Análise de Dados com Pandas Avançado e Plotly

Merge, pivot, melt, janelas e groupby no pandas 3, e gráficos interativos com…

Banco de Dados com SQLite e SQLAlchemy
Banco de Dados com SQLite e SQLAlchemy

SQLite com sqlite3 e SQLAlchemy 2.0 em Python, do CRUD ao repositório e às…