|
|
|
|
|
|
|
|
import streamlit as st
|
|
|
import pandas as pd
|
|
|
from datetime import datetime, timedelta, date
|
|
|
import io
|
|
|
import pythoncom
|
|
|
|
|
|
st.set_page_config(page_title="Relatório de E-mails • Outlook Desktop", layout="wide")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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_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",
|
|
|
)
|
|
|
|
|
|
|
|
|
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",
|
|
|
)
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
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()
|
|
|
outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
|
|
|
root_mailbox = outlook.Folders.Item(1)
|
|
|
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()
|
|
|
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)
|
|
|
dt_from = (datetime.now() - timedelta(days=dias)).strftime("%m/%d/%Y %H:%M %p")
|
|
|
try:
|
|
|
items = items.Restrict(f"[ReceivedTime] >= '{dt_from}'")
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
rows = []
|
|
|
for mail in items:
|
|
|
try:
|
|
|
if getattr(mail, "Class", None) != 43:
|
|
|
continue
|
|
|
try:
|
|
|
sender = mail.SenderEmailAddress or mail.Sender.Name
|
|
|
except Exception:
|
|
|
sender = getattr(mail, "SenderName", None)
|
|
|
|
|
|
|
|
|
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", "")),
|
|
|
"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()
|
|
|
outlook = win32com.client.Dispatch("Outlook.Application").GetNamespace("MAPI")
|
|
|
root = outlook.Folders.Item(1)
|
|
|
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()
|
|
|
except Exception:
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
|
|
|
todas_pastas = safe_list_all_folders()
|
|
|
|
|
|
|
|
|
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 = []
|
|
|
|
|
|
|
|
|
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 []
|
|
|
|
|
|
|
|
|
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"
|
|
|
)
|
|
|
|
|
|
|
|
|
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()]))
|
|
|
|
|
|
|
|
|
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.")
|
|
|
|