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