Envio de mensagens personalizadas em massa no Microsoft Teams (sem bots) — passo a passo com Graph, Power Automate e scripts

Precisa de enviar dezenas de mensagens personalizadas como chats 1:1 no Microsoft Teams a partir de um Excel — mantendo o histórico e sem copiar/colar? Este guia mostra, passo a passo, as opções reais para fazer isso de forma segura, suportada e com o mínimo de esforço manual.

Índice

Cenário prático

Uma utilizadora tem um ficheiro Excel com Nome, E‑mail e Mensagem (diferente por destinatário) e quer disparar cada mensagem como chat individual no Teams. Já tentou:

  • Power Automate: com bot funcionou, mas o diálogo “fica no bot” e perde-se visibilidade/continuidade no chat do utilizador.
  • Copilot Studio: poderoso, porém complexo e caro para um envio pontual.

O objetivo: mensagens que apareçam no chat normal do destinatário, preservem histórico e permitam resposta direta — com o menor trabalho possível.

Resumo das alternativas

OpçãoComo funcionaVantagensLimitações / Observações
Microsoft Graph APICriar um script (PowerShell, Python, Node.js) que: 1) lê o Excel; 2) cria ou reutiliza um chat 1:1 (/chats) para cada utilizador; 3) envia a mensagem (/chats/{id}/messages).100% automatizado; mensagens aparecem no chat normal; preserva histórico e permite resposta direta.Exige registo de app no Entra ID (Azure AD) e permissões adequadas (ex.: Chat.ReadWrite); requer conhecimentos básicos de scripting.
Power Automate + conector GraphFluxo com “Percorrer cada linha” do Excel → chamada HTTP Graph POST /chats/{id}/messages (ou ação nativa “Post message in a chat”, quando disponível).Interface low-code; usa o mesmo Excel como origem.Limites de ações e, em muitos casos, necessidade de conector premium para chamadas HTTP personalizadas.
Mail Merge no Outlook (paliativo)Combinar correspondência com fonte Excel; envia e‑mails personalizados.Sem programação; familiar para utilizadores Office.Não envia via Teams; a conversa não fica em chat.
Bot Framework / Copilot StudioConstruir um bot proativo que lê o Excel e dispara mensagens.Escalável e extensível.Sobrecarga de arquitetura, custos e manutenção para um cenário pontual.

Recomendação prática (em duas linhas)

  1. Precisa do chat dentro do Teams e tem apoio de TI? Use Microsoft Graph (script curto em PowerShell ou Python) ou um fluxo Power Automate que chame o Graph. É o caminho suportado e preserva o histórico.
  2. Sem permissões de developer ou licenças premium? Use Mail Merge como paliativo ou peça ativação temporária do conector premium. Evite Copilot Studio para tarefa pontual.

Como o Teams trata mensagens 1:1 via Graph

Por baixo dos panos, um chat 1:1 é uma entidade chat do tipo oneOnOne. A automação normalmente segue três passos:

  1. Resolver utilizador a partir do e‑mail → obter o objectId do utilizador no diretório (/users/{userPrincipalName}).
  2. Obter ou criar o chat 1:1 com esse utilizador (GET /me/chats + ver membros; ou POST /chats com dois membros).
  3. Enviar a mensagem no chat por POST /chats/{chat-id}/messages (conteúdo em text ou html).

Com permissões delegadas (o script corre sob a sua sessão), a mensagem aparece como sua no Teams do destinatário e mantém o histórico normal do chat.

Preparar o Excel

Organize a folha com cabeçalhos exatamente como abaixo (pode ter mais colunas; estas três são o mínimo):

NomeE-mailMensagem
Ana Silvaana.silva@contoso.comOlá {{Nome}}, segue o documento da semana. Se precisar, responda a este chat.
Bruno Costabruno.costa@contoso.comBruno, atualização importante sobre o projeto X. Podemos falar hoje?

Dica: use {{Nome}} (ou outro marcador) dentro da mensagem para personalizações dinâmicas. No script, substitua os marcadores pelos valores das colunas.

Permissões e segurança (sem dor de cabeça)

  • Registo de aplicação: peça ao administrador para criar uma app no Entra ID (Azure AD) ou, caso permitido, registe-a você mesma.
  • Permissões mínimas para um envio delegado típico: Chat.ReadWrite e User.ReadBasic.All. Se precisar ler perfis completos, User.Read.All.
  • Consentimento: conceda consentimento do utilizador (ou de administrador, conforme a política).
  • Governança: respeite comunicações internas, proteção de dados e etiqueta. Evite anúncios “em massa” sem contexto.

Exemplo completo em PowerShell (recomendado para quem usa Microsoft 365 no dia a dia)

Este script usa o módulo oficial Microsoft Graph para autenticação e chamadas REST com Invoke-MgGraphRequest. Lê o Excel (via módulo ImportExcel ou um CSV) e envia as mensagens 1:1 com controlo de throttling.

# Pré-requisitos (execute uma vez, se necessário):
Install-Module Microsoft.Graph -Scope CurrentUser
Install-Module ImportExcel -Scope CurrentUser   # opcional, para ler .xlsx sem converter

Import-Module Microsoft.Graph

1) Autenticação com permissões delegadas
try {
    Connect-MgGraph -Scopes @("Chat.ReadWrite","User.ReadBasic.All") -NoWelcome
} catch {
    Write-Error "Falha na autenticação. $_"
    exit 1
}
Select-MgProfile -Name "v1.0"

2) Ler dados do Excel (ou CSV)
$excelPath = "C:\dados\mensagens.xlsx"  # mude para o seu caminho
$worksheet = "Mensagens"                # nome da folha
$usarCsv   = $false                     # se true, muda para CSV

if ($usarCsv) {
    $linhas = Import-Csv -Path $excelPath
} else {
    if (-not (Get-Module -ListAvailable -Name ImportExcel)) {
        Write-Host "Módulo ImportExcel não encontrado. Converta para CSV ou instale com: Install-Module ImportExcel"
        exit 1
    }
    Import-Module ImportExcel
    $linhas = Import-Excel -Path $excelPath -WorksheetName $worksheet
}

3) Utilitário: obter o seu próprio ID (para criar chat 1:1)
$me = Invoke-MgGraphRequest -Method GET -Uri "/v1.0/me"
$meId = $me.id

4) Cache de chatIds para evitar chamadas repetidas
$chatCache = @{}

function Resolve-UserId([string]$email) {
    $encoded = [uri]::EscapeDataString($email)
    $u = Invoke-MgGraphRequest -Method GET -Uri "/v1.0/users/$encoded"
    return $u.id
}

function Get-OrCreate-ChatId([string]$targetUserId) {
    if ($chatCache.ContainsKey($targetUserId)) { return $chatCache[$targetUserId] }

    # 4.1 Tentar encontrar um chat 1:1 existente
    $next = "/v1.0/me/chats?`$filter=chatType eq 'oneOnOne'&`$top=50"
    while ($next) {
        $page = Invoke-MgGraphRequest -Method GET -Uri $next
        foreach ($chat in $page.value) {
            $members = Invoke-MgGraphRequest -Method GET -Uri "/v1.0/chats/$($chat.id)/members"
            if ($members.value | Where-Object { $_.userId -eq $targetUserId }) {
                $chatCache[$targetUserId] = $chat.id
                return $chat.id
            }
        }
        $next = $page.'@odata.nextLink'
    }

    # 4.2 Criar um novo chat 1:1
    $body = @{
        chatType = "oneOnOne"
        members  = @(
            @{
                "@odata.type"     = "#microsoft.graph.aadUserConversationMember"
                roles             = @()
                "user@odata.bind" = "https://graph.microsoft.com/v1.0/users/$meId"
            },
            @{
                "@odata.type"     = "#microsoft.graph.aadUserConversationMember"
                roles             = @()
                "user@odata.bind" = "https://graph.microsoft.com/v1.0/users/$targetUserId"
            }
        )
    } | ConvertTo-Json -Depth 6

    $newChat = Invoke-MgGraphRequest -Method POST -Uri "/v1.0/chats" -Body $body -ContentType "application/json"
    $chatCache[$targetUserId] = $newChat.id
    return $newChat.id
}

function Send-ChatMessage([string]$chatId, [string]$messageHtml) {
    $payload = @{
        body = @{
            contentType = "html"   # pode ser "text"
            content     = $messageHtml
        }
    } | ConvertTo-Json -Depth 4

    $maxRetries = 5
    for ($attempt = 1; $attempt -le $maxRetries; $attempt++) {
        try {
            $r = Invoke-MgGraphRequest -Method POST -Uri "/v1.0/chats/$chatId/messages" -Body $payload -ContentType "application/json"
            return $r.id
        } catch {
            $status = $.Exception.Response.StatusCode.value_
            $retryAfter = $_.Exception.Response.Headers['Retry-After']
            if ($status -eq 429 -or $status -eq 503) {
                $wait = [int]($retryAfter | Select-Object -First 1)
                if (-not $wait) { $wait = [math]::Min(60, [int][math]::Pow(2, $attempt)) }
                Start-Sleep -Seconds $wait
            } else {
                throw
            }
        }
    }
    throw "Falha ao enviar mensagem após $maxRetries tentativas."
}

function Expand-Tokens([string]$template, [hashtable]$data) {
    return ([regex]::Replace($template, "\{\{(\w+)\}\}", {
        param($m)
        $k = $m.Groups[1].Value
        if ($data.ContainsKey($k) -and $data[$k]) { $data[$k] } else { "" }
    }))
}

5) Enviar todas as mensagens
foreach ($row in $linhas) {
    $nome     = $row.Nome
    $email    = $row.'E-mail'
    $mensagem = $row.Mensagem

    if (-not $email -or -not $mensagem) {
        Write-Warning "Linha com e-mail ou mensagem vazios. A ignorar."
        continue
    }

    $userId = Resolve-UserId $email
    $chatId = Get-OrCreate-ChatId $userId

    # Personalização e formatação básica
    $html = Expand-Tokens $mensagem @{ Nome = $nome; Email = $email }
    $html = $html -replace "\r?\n", "<br/>"

    $msgId = Send-ChatMessage $chatId $html
    Write-Host "✔ Enviado para $nome ($email) | Chat: $chatId | Msg: $msgId"
    Start-Sleep -Milliseconds 300    # evita throttling
}

Porque este roteiro funciona bem? Faz somente o necessário: autentica, resolve o utilizador pelo e‑mail, reaproveita o chat se existir (para manter o histórico), cria se não existir e envia a mensagem. Inclui retry automático para lidar com limites temporários.

Variações úteis

  • CSV em vez de Excel: defina $usarCsv = $true e exporte a folha para .csv. O resto não muda.
  • Texto simples: mude contentType para text se não precisar de HTML.
  • Marcadores adicionais: adicione colunas (ex.: Cargo, Projeto) e inclua {{Cargo}}, {{Projeto}} na mensagem.

Exemplo em Python (pandas + MSAL)

Se preferir Python, o fluxo é idêntico. Este exemplo usa Device Code Flow, lê o Excel com pandas e chama os mesmos endpoints do Graph.

# Pré-requisitos:
pip install msal pandas openpyxl requests

import time, json, requests, msal
import pandas as pd

TENANT_ID = "seu-tenant-id"
CLIENT_ID = "id-da-sua-app"
SCOPES    = ["Chat.ReadWrite", "User.ReadBasic.All", "offline_access"]

AUTHORITY = f"https://login.microsoftonline.com/{TENANT_ID}"
GRAPH     = "https://graph.microsoft.com/v1.0"

app = msal.PublicClientApplication(CLIENT_ID, authority=AUTHORITY)
flow = app.initiatedeviceflow(scopes=[f"{GRAPH}/.default"] if ".default" in SCOPES else SCOPES)
print(flow["message"])
result = app.acquiretokenbydeviceflow(flow)

if "access_token" not in result:
    raise SystemExit(result.get("error_description"))

token = result["access_token"]
headers = {"Authorization": f"Bearer {token}", "Content-Type": "application/json"}

def graph_get(path):
    r = requests.get(GRAPH + path, headers=headers)
    r.raiseforstatus()
    return r.json()

def graph_post(path, payload):
    max_retry = 5
    for i in range(max_retry):
        r = requests.post(GRAPH + path, headers=headers, data=json.dumps(payload))
        if r.status_code in (429, 503):
            wait = int(r.headers.get("Retry-After", min(60, 2i)))
            time.sleep(wait)
            continue
        r.raiseforstatus()
        return r.json()
    raise RuntimeError("Falha após retries")

me = graph_get("/me")
me_id = me["id"]

def resolveuserid(email):
    from urllib.parse import quote
    u = graph_get(f"/users/{quote(email)}")
    return u["id"]

def getorcreatechatid(targetuserid):
    # procurar chat 1:1 existente
    url = "/me/chats?$filter=chatType eq 'oneOnOne'&$top=50"
    while url:
        page = graph_get(url.replace(GRAPH, ""))
        for chat in page.get("value", []):
            members = graph_get(f"/chats/{chat['id']}/members")
            if any(m.get("userId") == targetuserid for m in members.get("value", [])):
                return chat["id"]
        url = page.get("@odata.nextLink")
    # criar novo chat
    payload = {
        "chatType": "oneOnOne",
        "members": [
            {"@odata.type": "#microsoft.graph.aadUserConversationMember",
             "roles": [], "user@odata.bind": f"{GRAPH}/users/{me_id}"},
            {"@odata.type": "#microsoft.graph.aadUserConversationMember",
             "roles": [], "user@odata.bind": f"{GRAPH}/users/{targetuserid}"},
        ]
    }
    created = graph_post("/chats", payload)
    return created["id"]

def sendmessage(chatid, html):
    payload = {"body": {"contentType": "html", "content": html}}
    created = graphpost(f"/chats/{chatid}/messages", payload)
    return created.get("id")

df = pd.readexcel("C:/dados/mensagens.xlsx", sheetname="Mensagens")
for _, row in df.iterrows():
    email = row["E-mail"]
    nome  = row["Nome"]
    msg   = str(row["Mensagem"]).replace("\n", "<br/>")
    if not email or not msg: 
        continue
    uid   = resolveuserid(email)
    chat  = getorcreatechatid(uid)
    msgid = send_message(chat, msg.replace("{{Nome}}", nome if pd.notna(nome) else ""))
    print(f"Enviado para {nome} ({email}) | Chat {chat} | Msg {msgid}")
    time.sleep(0.3)

Automação no Power Automate (low-code)

Para quem prefere cliques a código, este desenho de fluxo funciona bem:

  1. Disparo: botão manual (Instant cloud flow) ou ficheiro adicionado a uma pasta no OneDrive/SharePoint.
  2. Excel Online (Business) — “Listar linhas presentes numa tabela”: a partir da tabela com Nome, E‑mail, Mensagem.
  3. Aplicar a cada linha → dentro do laço:
    • HTTP (com Azure AD): GET /v1.0/users/{email} → guarda o id.
    • HTTP: tenta POST /v1.0/chats com os dois membros (você e o destinatário). Opcionalmente, antes, liste /me/chats e verifique os membros para reaproveitar um chat existente.
    • HTTP: POST /v1.0/chats/{chat-id}/messages com o corpo da mensagem (HTML ou texto).

Observação: em muitas instâncias, a ação HTTP com Azure AD é premium. Algumas regiões já têm ação nativa “Post message in a chat” (Teams) — se disponível, prefira-a. Controle o ritmo com Delay de 0,2–0,5 s entre envios.

Boas práticas para evitar bloqueios e más experiências

  • Introduza pequena pausa entre chamadas (200–500 ms). Se receber 429 Too Many Requests, respeite o cabeçalho Retry-After.
  • Teste com um grupo pequeno antes de executar para toda a lista.
  • Guarde o chatId por destinatário após a primeira execução, para não criar múltiplos chats com a mesma pessoa.
  • Evite mensagens longas e encadeadas; prefira uma mensagem clara com call-to-action.
  • Privacidade: envie apenas para pessoas que precisam saber; informações sensíveis devem seguir a classificação/proteção de dados da organização.

Erros comuns e como resolver

SintomaCausa provávelComo resolver
401/403 UnauthorizedPermissões insuficientes ou consentimento em falta.Revise as scopes (Chat.ReadWrite, User.ReadBasic.All) e obtenha o consentimento adequado.
404 Not Found ao resolver utilizadorE‑mail não corresponde ao userPrincipalName ou não existe no diretório.Confirme o UPN ou use uma consulta na diretiva (ex.: procurar por mail eq 'email@dominio' com endpoint de pesquisa apropriado).
429 Too Many RequestsLimites temporários do Graph.Leia Retry-After e espere; introduza backoff exponencial.
Mensagem não aparece como “sua”Execução com identidade de app (aplicação) ou bot.Use permissões delegadas (script executado por si) para que a mensagem saia em seu nome.
Destinatário externo/guest não recebePolíticas de chat externo/guests.Valide se o chat externo está permitido e se o utilizador é membro convidado no tenant, conforme as regras da sua organização.

Modelo de mensagem (copiar e adaptar)

Assunto: (opcional, apenas se usar e-mail)

Olá {{Nome}},

Temos uma atualização rápida sobre o projeto. Partilhei o ficheiro na equipa do Teams.
Se puder, responda a este chat com a sua disponibilidade para hoje ou amanhã.

Obrigado! 

Checklist antes de carregar no “Executar”

  • ✔ Excel limpo, sem linhas vazias ou e‑mails inválidos.
  • ✔ Permissões concedidas (delegadas).
  • ✔ Mensagem testada em 2–3 destinatários.
  • ✔ Pausa configurada entre envios (≥ 200 ms).
  • ✔ Plano de suporte caso alguém responda (quem vai acompanhar o chat?).

Quando faz sentido usar outras opções

  • Mail Merge: se o objetivo é meramente informativo e o e‑mail já resolve (por exemplo, boletins internos).
  • Copilot Studio / Bot: quando pretende conversas ricas, FAQs e integrações — e não apenas um disparo pontual.

Perguntas frequentes

Posso anexar ficheiros? É preferível partilhar a ligação a um ficheiro no SharePoint/OneDrive. Anexos nativos em mensagens de chat exigem passos adicionais (conteúdo hospedado). Para começar, mantenha o texto simples ou HTML.

É possível enviar cartões (Adaptive Cards)? Sim, enviando um attachment com o conteúdo do cartão em JSON. Para um primeiro envio, simplifique em HTML.

Quantas mensagens posso enviar? Depende das políticas e limites de serviço. Use pausas e backoff. Para centenas/dezenas, o roteiro apresentado é robusto.

Preciso de app registration? Para chamadas Graph via scripts ou Power Automate com HTTP, sim (ou um conector nativo que abstraia isso). O administrador pode criar e conceder as permissões mínimas.

Conclusão

Para enviar mensagens personalizadas em massa no Microsoft Teams de forma suportada e com histórico preservado, a rota mais simples é Microsoft Graph com um pequeno script (PowerShell/Python) ou um fluxo Power Automate que chame o Graph. Evite soluções baseadas em bots para este caso específico: são ótimas para experiências conversacionais, mas desnecessárias quando o objetivo é um disparo pontual e direto ao chat do colaborador. Ao seguir as práticas de permissões mínimas, pausas entre chamadas e testes graduais, a automação torna‑se segura, previsível e fácil de manter.

Resumo de decisão em uma frase

Se precisa do chat do Teams “de verdade”, com histórico e resposta direta, use Graph (PowerShell/Python) ou Power Automate com Graph; caso contrário, Mail Merge no Outlook resolve.

Dicas finais

  • Use linguagem clara e pessoal nas mensagens; evite jargões.
  • Inclua sempre um único pedido de ação (ex.: “Pode confirmar até às 17h?”).
  • Guarde um registo simples do que foi enviado (timestamp, e‑mail, estado) — um CSV log basta.
Índice