IOI-RUN / app_outlook.py
Roudrigus's picture
Upload 82 files
0f0ef8d verified
# -*- coding: utf-8 -*-
import streamlit as st
import pandas as pd
from datetime import datetime, timedelta, date
import io
import pythoncom # ✅ necessário para inicializar/finalizar COM em cada operação
st.set_page_config(page_title="Relatório de E-mails • Outlook Desktop", layout="wide")
# ==============================
# Utilitários de exportação/indicadores
# ==============================
def build_downloads(df: pd.DataFrame, base_name: str):
"""Cria botões de download (CSV, Excel e PDF) para o DataFrame."""
if df.empty:
st.warning("Nenhum dado para exportar.")
return
# CSV
csv_buf = io.StringIO()
df.to_csv(csv_buf, index=False, encoding="utf-8-sig")
st.download_button(
"⬇️ Baixar CSV",
data=csv_buf.getvalue(),
file_name=f"{base_name}.csv",
mime="text/csv",
)
# Excel
xlsx_buf = io.BytesIO()
with pd.ExcelWriter(xlsx_buf, engine="openpyxl") as writer:
df.to_excel(writer, index=False, sheet_name="Relatorio")
xlsx_buf.seek(0)
st.download_button(
"⬇️ Baixar Excel",
data=xlsx_buf,
file_name=f"{base_name}.xlsx",
mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
)
# PDF (resumo com até 100 linhas para leitura confortável)
try:
from reportlab.lib.pagesizes import A4, landscape
from reportlab.platypus import SimpleDocTemplate, Table, TableStyle, Paragraph, Spacer
from reportlab.lib import colors
from reportlab.lib.styles import getSampleStyleSheet
pdf_buf = io.BytesIO()
doc = SimpleDocTemplate(
pdf_buf,
pagesize=landscape(A4),
rightMargin=20, leftMargin=20, topMargin=20, bottomMargin=20
)
styles = getSampleStyleSheet()
story = []
title = Paragraph(f"Relatório de E-mails — {base_name}", styles["Title"])
story.append(title)
story.append(Spacer(1, 12))
# Limita tabela para evitar PDFs gigantes
df_show = df.copy().head(100)
data_table = [list(df_show.columns)] + df_show.astype(str).values.tolist()
table = Table(data_table, repeatRows=1)
table.setStyle(TableStyle([
("BACKGROUND", (0,0), (-1,0), colors.HexColor("#E9ECEF")),
("TEXTCOLOR", (0,0), (-1,0), colors.HexColor("#212529")),
("GRID", (0,0), (-1,-1), 0.25, colors.HexColor("#ADB5BD")),
("FONTNAME", (0,0), (-1,0), "Helvetica-Bold"),
("FONTNAME", (0,1), (-1,-1), "Helvetica"),
("FONTSIZE", (0,0), (-1,-1), 9),
("ALIGN", (0,0), (-1,-1), "LEFT"),
("VALIGN", (0,0), (-1,-1), "MIDDLE"),
]))
story.append(table)
doc.build(story)
pdf_buf.seek(0)
st.download_button(
"⬇️ Baixar PDF",
data=pdf_buf,
file_name=f"{base_name}.pdf",
mime="application/pdf",
)
except Exception as e:
st.info(f"PDF: não foi possível gerar o arquivo (ReportLab). Detalhe: {e}")
def render_indicators(df: pd.DataFrame, dt_col_name: str):
"""Exibe indicadores simples (top remetentes, distribuição por dia)."""
if df.empty:
return
st.subheader("📊 Indicadores")
col1, col2 = st.columns(2)
with col1:
st.write("**Top Remetentes (Top 10)**")
st.dataframe(
df["Remetente"].value_counts().head(10).rename("Qtd").to_frame(),
use_container_width=True,
)
with col2:
st.write("**Mensagens por Dia**")
if dt_col_name in df.columns:
_dt = pd.to_datetime(df[dt_col_name], errors="coerce")
por_dia = _dt.dt.date.value_counts().sort_index().rename("Qtd")
st.dataframe(por_dia.to_frame(), use_container_width=True)
# ==============================
# Outlook Desktop (Windows) — COM-safe helpers
# ==============================
def _list_folders_desktop(root_folder, prefix=""):
"""Recursão local (já com root_folder pronto) — retorna caminhos completos de subpastas."""
paths = []
try:
for i in range(1, root_folder.Folders.Count + 1):
f = root_folder.Folders.Item(i)
full_path = prefix + f.Name
paths.append(full_path)
# recursão
try:
paths.extend(_list_folders_desktop(f, prefix=full_path + "\\"))
except Exception:
pass
except Exception:
pass
return paths
def safe_list_all_folders():
"""
✅ Inicializa COM, conecta no Outlook e retorna TODOS os caminhos de pastas
da caixa padrão. Finaliza COM ao terminar. Evita 'CoInitialize não foi chamado'.
"""
try:
import win32com.client
pythoncom.CoInitialize() # inicializa COM
outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
root_mailbox = outlook.Folders.Item(1) # índice da caixa de correio padrão
return _list_folders_desktop(root_mailbox, prefix="")
except Exception as e:
st.sidebar.info(f"Não foi possível listar pastas automaticamente ({e}). Informe manualmente abaixo.")
return []
finally:
try:
pythoncom.CoUninitialize() # finaliza COM
except Exception:
pass
def _get_folder_by_path(root_folder, path: str):
parts = [p for p in path.split("\\") if p]
folder = root_folder.Folders.Item(parts[0])
for p in parts[1:]:
folder = folder.Folders.Item(p)
return folder
def _read_folder_items(folder, dias: int, filtro_remetente: str = "") -> pd.DataFrame:
"""Lê e-mails de uma pasta específica e retorna DataFrame."""
items = folder.Items
items.Sort("[ReceivedTime]", True) # decrescente
dt_from = (datetime.now() - timedelta(days=dias)).strftime("%m/%d/%Y %H:%M %p")
try:
items = items.Restrict(f"[ReceivedTime] >= '{dt_from}'")
except Exception:
# Alguns ambientes podem falhar no Restrict; segue sem filtro temporal
pass
rows = []
for mail in items:
try:
if getattr(mail, "Class", None) != 43: # 43 = MailItem
continue
try:
sender = mail.SenderEmailAddress or mail.Sender.Name
except Exception:
sender = getattr(mail, "SenderName", None)
# Filtro opcional por remetente
if filtro_remetente and sender:
if filtro_remetente.lower() not in str(sender).lower():
continue
anexos = mail.Attachments.Count if hasattr(mail, "Attachments") else 0
tamanho_kb = round(mail.Size / 1024, 1) if hasattr(mail, "Size") else None
rows.append({
"Pasta": folder.Name,
"Assunto": mail.Subject,
"Remetente": sender,
"RecebidoEm": mail.ReceivedTime.strftime("%Y-%m-%d %H:%M"),
"Anexos": anexos,
"TamanhoKB": tamanho_kb,
"Importancia": str(getattr(mail, "Importance", "")), # 0 baixa, 1 normal, 2 alta
"Categoria": getattr(mail, "Categories", "") or "",
"Lido": bool(getattr(mail, "UnRead", False) == False),
})
except Exception as e:
rows.append({
"Pasta": folder.Name, "Assunto": f"[ERRO] {e}", "Remetente": "",
"RecebidoEm": "", "Anexos": "", "TamanhoKB": "", "Importancia": "", "Categoria": "", "Lido": ""
})
return pd.DataFrame(rows)
def gerar_relatorio_outlook_desktop_multi(pastas: list[str], dias: int, filtro_remetente: str = "") -> pd.DataFrame:
"""
✅ Envolve toda operação COM: inicializa, lê e finaliza.
Evita o erro 'CoInitialize não foi chamado.'
"""
try:
import win32com.client
pythoncom.CoInitialize() # inicializa COM
outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
root = outlook.Folders.Item(1) # ajuste o índice se tiver múltiplas caixas
except Exception as e:
st.error(f"Falha ao conectar ao Outlook/pywin32: {e}")
return pd.DataFrame()
frames = []
try:
for path in pastas:
try:
folder = _get_folder_by_path(root, path)
df = _read_folder_items(folder, dias, filtro_remetente=filtro_remetente)
df["PastaPath"] = path
frames.append(df)
except Exception as e:
st.warning(f"Não foi possível ler a pasta '{path}': {e}")
return pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()
finally:
try:
pythoncom.CoUninitialize() # finaliza COM
except Exception:
pass
# ==============================
# UI — Streamlit (seleção de múltiplas pastas)
# ==============================
st.title("📧 Relatório de E-mails • Outlook Desktop (Windows)")
st.caption("Escolha **uma ou várias pastas** da sua Caixa de Entrada, defina o período, filtre por remetente (opcional) e gere o relatório.")
st.sidebar.header("Configurações")
dias = st.sidebar.slider("Período (últimos N dias)", min_value=1, max_value=365, value=30)
filtro_remetente = st.sidebar.text_input(
"Filtrar por remetente (opcional)",
value="",
placeholder='Ex.: "@fornecedor.com" ou "Fulano"'
)
apenas_inbox = st.sidebar.checkbox("Mostrar somente pastas sob Inbox", value=True)
# Tentar listar todas as pastas (COM-safe)
todas_pastas = safe_list_all_folders()
# Filtrar apenas pastas sob Inbox (Caixa de Entrada), se marcado
if todas_pastas:
if apenas_inbox:
opcoes_base = [p for p in todas_pastas if p.lower().startswith("inbox")]
else:
opcoes_base = todas_pastas
else:
opcoes_base = []
# Busca por nome
filtro_pasta = st.sidebar.text_input("Pesquisar pasta por nome:", value="")
if filtro_pasta and opcoes_base:
opcoes = [p for p in opcoes_base if filtro_pasta.lower() in p.lower()]
else:
opcoes = opcoes_base or []
# Multiselect de pastas
pastas_escolhidas = st.sidebar.multiselect(
"Selecione uma ou mais pastas:",
options=opcoes if opcoes else ["Inbox"],
default=(opcoes[:1] if opcoes else ["Inbox"]),
help="Use '\\' para subpastas. Ex.: Inbox\\Financeiro\\Notas"
)
# Campo manual adicional (para quem quer escrever um caminho específico não listado)
pasta_manual_extra = st.sidebar.text_input(
"Adicionar caminho manual (opcional)",
value="",
placeholder="Inbox\\Financeiro\\Notas"
)
if pasta_manual_extra.strip():
pastas_escolhidas = list(set(pastas_escolhidas + [pasta_manual_extra.strip()]))
# Botão gerar
if st.sidebar.button("🔍 Gerar relatório"):
if not pastas_escolhidas:
st.error("Selecione ao menos uma pasta.")
else:
with st.spinner("Lendo e-mails do Outlook..."):
df = gerar_relatorio_outlook_desktop_multi(
pastas_escolhidas,
dias,
filtro_remetente=filtro_remetente
)
st.success(f"Relatório gerado ({len(df)} registros) a partir de {len(pastas_escolhidas)} pasta(s).")
st.subheader("📄 Resultado")
st.dataframe(df, use_container_width=True)
render_indicators(df, dt_col_name="RecebidoEm")
base_name = f"relatorio_outlook_desktop_{date.today()}"
build_downloads(df, base_name=base_name)
st.markdown("---")
st.caption("Dica: se você tem várias caixas postais, troque o índice em `outlook.Folders.Item(1)` para a caixa correta.")