File size: 12,004 Bytes
0f0ef8d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 |
# -*- 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.")
|