|
|
|
|
| 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.")
|
|
|