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.
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ção | Como funciona | Vantagens | Limitações / Observações |
---|---|---|---|
Microsoft Graph API | Criar 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 Graph | Fluxo 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 Studio | Construir 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)
- 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.
- 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:
- Resolver utilizador a partir do e‑mail → obter o objectId do utilizador no diretório (
/users/{userPrincipalName}
). - Obter ou criar o chat 1:1 com esse utilizador (
GET /me/chats
+ ver membros; ouPOST /chats
com dois membros). - Enviar a mensagem no chat por
POST /chats/{chat-id}/messages
(conteúdo emtext
ouhtml
).
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):
Nome | Mensagem | |
---|---|---|
Ana Silva | ana.silva@contoso.com | Olá {{Nome}}, segue o documento da semana. Se precisar, responda a este chat. |
Bruno Costa | bruno.costa@contoso.com | Bruno, 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
eUser.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
paratext
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:
- Disparo: botão manual (Instant cloud flow) ou ficheiro adicionado a uma pasta no OneDrive/SharePoint.
- Excel Online (Business) — “Listar linhas presentes numa tabela”: a partir da tabela com Nome, E‑mail, Mensagem.
- Aplicar a cada linha → dentro do laço:
- HTTP (com Azure AD): GET
/v1.0/users/{email}
→ guarda oid
. - 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).
- HTTP (com Azure AD): GET
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çalhoRetry-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
Sintoma | Causa provável | Como resolver |
---|---|---|
401/403 Unauthorized | Permissões insuficientes ou consentimento em falta. | Revise as scopes (Chat.ReadWrite , User.ReadBasic.All ) e obtenha o consentimento adequado. |
404 Not Found ao resolver utilizador | E‑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 Requests | Limites 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 recebe | Polí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.