Compare commits

..

4 Commits

Author SHA1 Message Date
929d2e9c81 docs(tools): include Tailscale link to saved YouTube notes
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 05:45:15 +00:00
000b406c8d feat(newsletter): scan forward for multiple issues and re-enable cron
Loop through consecutive newsletter numbers until one is missing, so
backlog gets delivered in a single run. Use httpx for 404 check and
point to absolute claude binary path for cron. Enable job in config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 05:45:10 +00:00
9314d63aa0 feat(email): add digest and forward commands to WhatsApp
Digest summarizes unread emails via Claude CLI; forward sends raw
content (split to 4096 chars). Wired as /email digest and
/email forward slash commands, plus instant per-guild sync on ready.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 05:45:02 +00:00
9d447b9ff1 fix(newsletter): use follow_redirects=False to avoid false positive on 404 redirect
Beehiiv redirects non-existent newsletters to /?404=... with HTTP 302.
With follow_redirects=True, the final 200 was misread as "newsletter exists".
Fix: disable redirect following so only a direct HTTP 200 = newsletter real.
Also reset state back to last_sent=13 (real).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 17:06:00 +00:00
10 changed files with 482 additions and 81 deletions

View File

@@ -49,7 +49,7 @@
} }
}, },
"newsletter_cercetasi": { "newsletter_cercetasi": {
"enabled": false, "enabled": true,
"cron": "0 17 * * 4,5,1", "cron": "0 17 * * 4,5,1",
"channel": "echo-core" "channel": "echo-core"
}, },

View File

@@ -1,5 +1,5 @@
{ {
"last_sent": 13, "last_sent": 14,
"year": 2026, "year": 2026,
"last_sent_at": "2026-04-02T18:59:37.878273+00:00" "last_sent_at": "2026-04-09T14:23:55.586085+00:00"
} }

View File

@@ -57,6 +57,7 @@
- **Flux complet după procesare:** - **Flux complet după procesare:**
1. Salvează nota în `memory/kb/youtube/YYYY-MM-DD_slug.md` 1. Salvează nota în `memory/kb/youtube/YYYY-MM-DD_slug.md`
2. Rulează `python3 tools/update_notes_index.py` → actualizează notes.html 2. Rulează `python3 tools/update_notes_index.py` → actualizează notes.html
3. Include în răspuns link-ul Tailscale direct la notă: `https://moltbot.tailf7372d.ts.net/echo/notes.html#notes-data/youtube/YYYY-MM-DD_slug`
### Whisper ### Whisper
- **Venv:** ~/echo-core/.venv/ | **Model:** base - **Venv:** ~/echo-core/.venv/ | **Model:** base

View File

@@ -464,6 +464,18 @@ def create_bot(config: Config) -> discord.Client:
result = await asyncio.to_thread(fast_dispatch, "email", ["save"]) result = await asyncio.to_thread(fast_dispatch, "email", ["save"])
await interaction.followup.send(result) await interaction.followup.send(result)
@email_group.command(name="digest", description="Procesează emailuri necitite și trimite rezumate pe WhatsApp")
async def email_digest(interaction: discord.Interaction) -> None:
await interaction.response.defer()
result = await asyncio.to_thread(fast_dispatch, "email", ["digest"])
await interaction.followup.send(result)
@email_group.command(name="forward", description="Forwardează emailuri necitite direct pe WhatsApp fără rezumat")
async def email_forward(interaction: discord.Interaction) -> None:
await interaction.response.defer()
result = await asyncio.to_thread(fast_dispatch, "email", ["forward"])
await interaction.followup.send(result)
tree.add_command(email_group) tree.add_command(email_group)
# --- Calendar commands --- # --- Calendar commands ---
@@ -878,6 +890,10 @@ def create_bot(config: Config) -> discord.Client:
@client.event @client.event
async def on_ready() -> None: async def on_ready() -> None:
# Sync to each guild instantly, then global (global can take up to 1h)
for guild in client.guilds:
tree.copy_global_to(guild=guild)
await tree.sync(guild=guild)
await tree.sync() await tree.sync()
scheduler = getattr(client, "scheduler", None) scheduler = getattr(client, "scheduler", None)
if scheduler is not None: if scheduler is not None:

View File

@@ -106,7 +106,7 @@ def cmd_test(args: list[str]) -> str:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def cmd_email(args: list[str]) -> str: def cmd_email(args: list[str]) -> str:
"""Dispatch: /email, /email send ..., /email save.""" """Dispatch: /email, /email send ..., /email save, /email digest."""
if not args: if not args:
return _email_check() return _email_check()
sub = args[0].lower() sub = args[0].lower()
@@ -114,7 +114,11 @@ def cmd_email(args: list[str]) -> str:
return _email_send(args[1:]) return _email_send(args[1:])
if sub == "save": if sub == "save":
return _email_save() return _email_save()
return f"Unknown email sub-command: {sub}. Use: /email, /email send, /email save" if sub == "digest":
return _email_digest()
if sub == "forward":
return _email_forward()
return f"Unknown email sub-command: {sub}. Use: /email, /email send, /email save, /email digest, /email forward"
def _email_check() -> str: def _email_check() -> str:
@@ -185,6 +189,62 @@ def _email_send(args: list[str]) -> str:
return f"Email send error: {e}" return f"Email send error: {e}"
def _email_digest() -> str:
"""Run email digest: save unread emails, generate summaries, send on WhatsApp."""
script = TOOLS_DIR / "email_digest.py"
if not script.exists():
return "email_digest.py not found."
import sys as _sys
def _run():
try:
result = subprocess.run(
[_sys.executable, str(script)],
timeout=120,
cwd=str(PROJECT_ROOT),
capture_output=True,
text=True,
)
if result.returncode != 0:
log.error("Email digest failed: %s", result.stderr.strip()[:300])
else:
log.info("Email digest: %s", result.stdout.strip()[:300])
except Exception as e:
log.error("Email digest error: %s", e)
threading.Thread(target=_run, daemon=True).start()
return "Procesez emailurile... rezumatele vin pe WhatsApp."
def _email_forward() -> str:
"""Forward unread emails directly to WhatsApp without summarizing."""
script = TOOLS_DIR / "email_forward.py"
if not script.exists():
return "email_forward.py not found."
import sys as _sys
def _run():
try:
result = subprocess.run(
[_sys.executable, str(script)],
timeout=60,
cwd=str(PROJECT_ROOT),
capture_output=True,
text=True,
)
if result.returncode != 0:
log.error("Email forward failed: %s", result.stderr.strip()[:300])
else:
log.info("Email forward: %s", result.stdout.strip()[:300])
except Exception as e:
log.error("Email forward error: %s", e)
threading.Thread(target=_run, daemon=True).start()
return "Forwardez emailurile pe WhatsApp..."
def _email_save() -> str: def _email_save() -> str:
"""Save unread emails as KB notes.""" """Save unread emails as KB notes."""
script = TOOLS_DIR / "email_process.py" script = TOOLS_DIR / "email_process.py"
@@ -593,6 +653,8 @@ Email:
/email — Check unread emails /email — Check unread emails
/email send <to> <subject> :: <body> — Send email /email send <to> <subject> :: <body> — Send email
/email save — Save unread emails to KB /email save — Save unread emails to KB
/email digest — Rezumate emailuri necitite → WhatsApp
/email forward — Forward emailuri necitite direct → WhatsApp (fără rezumat)
Calendar: Calendar:
/calendar — Today + tomorrow events /calendar — Today + tomorrow events

View File

@@ -35,10 +35,14 @@ def _write_state(state: dict) -> None:
async def _newsletter_exists(n: int, year: int) -> bool: async def _newsletter_exists(n: int, year: int) -> bool:
"""Return True if newsletter #{n}/{year} returns HTTP 200.""" """Return True if newsletter #{n}/{year} exists (HTTP 200, no redirect to 404 page).
Beehiiv redirects non-existent newsletters: /p/newsletter-N-din-YEAR → /?404=... (302)
A real newsletter returns 200 directly without redirect.
"""
url = NEWSLETTER_BASE_URL.format(n=n, year=year) url = NEWSLETTER_BASE_URL.format(n=n, year=year)
try: try:
async with httpx.AsyncClient(follow_redirects=True) as client: async with httpx.AsyncClient(follow_redirects=False) as client:
resp = await client.get(url, timeout=10) resp = await client.get(url, timeout=10)
return resp.status_code == 200 return resp.status_code == 200
except Exception as e: except Exception as e:

View File

@@ -20,7 +20,7 @@ PROJECT_ROOT = Path(__file__).resolve().parent.parent
STATE_FILE = PROJECT_ROOT / "cron" / "newsletter-cercetasi-state.json" STATE_FILE = PROJECT_ROOT / "cron" / "newsletter-cercetasi-state.json"
KB_PROMPT_FILE = PROJECT_ROOT / "memory" / "kb" / "projects" / "grup-sprijin" / "prompt-newsletter-cercetasi.md" KB_PROMPT_FILE = PROJECT_ROOT / "memory" / "kb" / "projects" / "grup-sprijin" / "prompt-newsletter-cercetasi.md"
CONFIG_FILE = PROJECT_ROOT / "config.json" CONFIG_FILE = PROJECT_ROOT / "config.json"
CLAUDE_BIN = "claude" CLAUDE_BIN = "/home/moltbot/.local/bin/claude"
CLAUDE_TIMEOUT = 300 CLAUDE_TIMEOUT = 300
NEWSLETTER_BASE_URL = "https://cercetaiis-newsletter.beehiiv.com/p/newsletter-{n}-din-{year}" NEWSLETTER_BASE_URL = "https://cercetaiis-newsletter.beehiiv.com/p/newsletter-{n}-din-{year}"
@@ -44,14 +44,9 @@ def write_state(state: dict):
def newsletter_exists(n: int, year: int) -> bool: def newsletter_exists(n: int, year: int) -> bool:
url = NEWSLETTER_BASE_URL.format(n=n, year=year) url = NEWSLETTER_BASE_URL.format(n=n, year=year)
try: try:
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) with httpx.Client(follow_redirects=False, timeout=10) as client:
with urllib.request.urlopen(req, timeout=10) as resp: resp = client.get(url, headers={"User-Agent": "Mozilla/5.0"})
return resp.status == 200 return resp.status_code == 200
except urllib.error.HTTPError as e:
if e.code == 404:
return False
log(f"HTTP check failed: {e}")
return False
except Exception as e: except Exception as e:
log(f"HTTP check failed: {e}") log(f"HTTP check failed: {e}")
return False return False
@@ -202,28 +197,39 @@ def main():
log(f"New year detected ({state['year']}{current_year}), resetting counter") log(f"New year detected ({state['year']}{current_year}), resetting counter")
state = {"last_sent": 0, "year": current_year} state = {"last_sent": 0, "year": current_year}
config = json.loads(CONFIG_FILE.read_text())
discord_token = get_discord_token()
channel_id = get_discord_channel_id()
telegram_token = get_telegram_token()
telegram_chat_id = config.get("newsletter_cercetasi", {}).get("telegram_chat_id", "5040014994")
bridge_url = config.get("whatsapp", {}).get("bridge_url", "http://127.0.0.1:8098")
owner_phone = config.get("whatsapp", {}).get("owner", "")
any_error = False
next_n = state["last_sent"] + 1 next_n = state["last_sent"] + 1
# Scan forward: process all available newsletters, not just the next one
while True:
log(f"Checking for newsletter #{next_n}/{current_year}...") log(f"Checking for newsletter #{next_n}/{current_year}...")
if not newsletter_exists(next_n, current_year): if not newsletter_exists(next_n, current_year):
log(f"Newsletter #{next_n}/{current_year} not yet available. Exiting.") log(f"Newsletter #{next_n}/{current_year} not yet available. Stopping.")
return break
log(f"Newsletter #{next_n}/{current_year} found! Generating summary...") log(f"Newsletter #{next_n}/{current_year} found! Generating summary...")
summary = generate_summary(next_n, current_year) summary = generate_summary(next_n, current_year)
if not summary: if not summary:
log("Summary generation failed. Exiting.") log(f"Summary generation failed for #{next_n}. Will retry next run.")
sys.exit(1) any_error = True
break
success = False success = False
# Send to Discord # Send to Discord
discord_token = get_discord_token()
channel_id = get_discord_channel_id()
if discord_token and channel_id: if discord_token and channel_id:
log(f"Sending {len(summary)} chars to Discord channel {channel_id}...") log(f"Sending #{next_n} to Discord channel {channel_id}...")
if send_discord(channel_id, discord_token, summary): if send_discord(channel_id, discord_token, summary):
log(f"Discord: sent successfully.") log("Discord: sent successfully.")
success = True success = True
else: else:
log("Discord send failed.") log("Discord send failed.")
@@ -231,13 +237,10 @@ def main():
log("Discord token or channel ID missing, skipping.") log("Discord token or channel ID missing, skipping.")
# Send to Telegram # Send to Telegram
telegram_token = get_telegram_token()
if telegram_token: if telegram_token:
config = json.loads(CONFIG_FILE.read_text()) log(f"Sending #{next_n} to Telegram chat {telegram_chat_id}...")
telegram_chat_id = config.get("newsletter_cercetasi", {}).get("telegram_chat_id", "5040014994")
log(f"Sending {len(summary)} chars to Telegram chat {telegram_chat_id}...")
if send_telegram(telegram_token, telegram_chat_id, summary): if send_telegram(telegram_token, telegram_chat_id, summary):
log(f"Telegram: sent successfully.") log("Telegram: sent successfully.")
success = True success = True
else: else:
log("Telegram send failed.") log("Telegram send failed.")
@@ -245,12 +248,9 @@ def main():
log("Telegram token missing, skipping.") log("Telegram token missing, skipping.")
# Send to WhatsApp # Send to WhatsApp
config = json.loads(CONFIG_FILE.read_text())
bridge_url = config.get("whatsapp", {}).get("bridge_url", "http://127.0.0.1:8098")
owner_phone = config.get("whatsapp", {}).get("owner", "")
if owner_phone: if owner_phone:
wa_to = f"{owner_phone}@s.whatsapp.net" wa_to = f"{owner_phone}@s.whatsapp.net"
log(f"Sending {len(summary)} chars to WhatsApp {owner_phone}...") log(f"Sending #{next_n} to WhatsApp {owner_phone}...")
try: try:
with httpx.Client(timeout=15) as client: with httpx.Client(timeout=15) as client:
resp = client.post(f"{bridge_url}/send", json={"to": wa_to, "text": summary}) resp = client.post(f"{bridge_url}/send", json={"to": wa_to, "text": summary})
@@ -269,9 +269,14 @@ def main():
state["year"] = current_year state["year"] = current_year
state["last_sent_at"] = datetime.now(timezone.utc).isoformat() state["last_sent_at"] = datetime.now(timezone.utc).isoformat()
write_state(state) write_state(state)
log(f"Newsletter #{next_n}/{current_year} done.") log(f"Newsletter #{next_n}/{current_year} done. State saved.")
next_n += 1
else: else:
log("All sends failed — will retry next run.") log(f"All sends failed for #{next_n} — will retry next run.")
any_error = True
break
if any_error:
sys.exit(1) sys.exit(1)

133
tools/email_digest.py Normal file
View File

@@ -0,0 +1,133 @@
#!/usr/bin/env python3
"""
Email digest: procesează emailuri necitite și trimite rezumate pe WhatsApp.
Usage:
python3 tools/email_digest.py # Run digest
python3 tools/email_digest.py --dry-run # Afișează rezumatele fără a trimite
"""
import sys
import subprocess
import requests
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from tools.email_process import save_unread_emails
from src.config import Config
BRIDGE_URL = "http://127.0.0.1:8098"
DRY_RUN = "--dry-run" in sys.argv
def get_owner_jid() -> str:
config = Config(PROJECT_ROOT / "config.json")
owner = config.get("whatsapp.owner", "")
return f"{owner}@s.whatsapp.net"
def generate_summary(filepath: str, subject: str, from_full: str, date: str) -> str:
"""Citește conținutul emailului și generează rezumat via Claude CLI."""
try:
email_content = Path(filepath).read_text(encoding="utf-8")
except Exception as e:
return f"[Eroare la citirea fișierului: {e}]"
prompt = f"""Mai jos este conținutul unui email. Generează un rezumat pentru WhatsApp.
EMAIL:
{email_content}
Format obligatoriu (plain text, fără markdown):
SUBIECT: {subject}
De la: {from_full}
Primit: {date}
---
[Rezumat compact dar complet — include TOATE detaliile importante:
- ce se întâmplă / despre ce e vorba
- cine organizează / cine trimite
- când (date, deadline-uri, perioade)
- unde (locație dacă există)
- ce trebuie să facă cititorul (acțiune clară)
- condiții de participare (vârstă, criterii etc.) dacă există
- ce este inclus/oferit dacă e relevant
Fiecare punct pe linie separată. Fără redundanță, fără filler.]
LINKURI:
[Listează TOATE linkurile acționabile: formulare, înscrieri, site-uri, documente, social media]
[Format: "Descriere scurtă: https://..." — câte un link per linie]
[Dacă nu există linkuri, omite secțiunea]
[Semnătura expeditorului din email pe ultima linie după —]
Fără emoji dacă nu sunt în original. Răspunde DOAR cu rezumatul, nimic altceva."""
result = subprocess.run(
["claude", "--print", prompt],
capture_output=True,
text=True,
timeout=60,
stdin=subprocess.DEVNULL,
)
if result.returncode != 0:
return f"[Eroare la generarea rezumatului pentru: {subject}]\n{result.stderr.strip()[:200]}"
return result.stdout.strip()
def send_whatsapp(to: str, text: str) -> bool:
"""Trimite mesaj pe WhatsApp prin bridge."""
try:
resp = requests.post(
f"{BRIDGE_URL}/send",
json={"to": to, "text": text},
timeout=15,
)
data = resp.json()
return data.get("ok", False)
except Exception as e:
print(f"[eroare send] {e}", file=sys.stderr)
return False
def run_digest():
print("📬 Verific emailuri necitite...")
saved = save_unread_emails()
if not saved:
print("Niciun email nou de procesat.")
return
owner_jid = get_owner_jid()
for result in saved:
if not result.get("ok"):
print(f"⚠️ Sărit: {result.get('error')}")
continue
filepath = result["file"]
subject = result["subject"]
from_full = result.get("from_full", result.get("from", ""))
date = result.get("date", "")
print(f"📧 Procesez: {subject}")
summary = generate_summary(filepath, subject, from_full, date)
if DRY_RUN:
print("\n--- REZUMAT (dry-run) ---")
print(summary)
print("------------------------\n")
else:
ok = send_whatsapp(owner_jid, summary)
if ok:
print(f"✅ Trimis pe WhatsApp: {subject}")
else:
print(f"❌ Trimitere eșuată: {subject}")
if __name__ == "__main__":
run_digest()

178
tools/email_forward.py Normal file
View File

@@ -0,0 +1,178 @@
#!/usr/bin/env python3
"""
Email forward: forwardează emailuri necitite direct pe WhatsApp fără rezumat.
Usage:
python3 tools/email_forward.py # Run forward
python3 tools/email_forward.py --dry-run # Afișează fără a trimite
"""
import sys
import re
import requests
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(PROJECT_ROOT))
from tools.email_process import (
IMAP_SERVER, IMAP_PORT, IMAP_USER, IMAP_PASS,
WHITELIST, decode_mime_header, extract_sender_email, get_email_body
)
from src.config import Config
import imaplib
import email as email_lib
BRIDGE_URL = "http://127.0.0.1:8098"
DRY_RUN = "--dry-run" in sys.argv
MAX_WA_LENGTH = 4096
def get_owner_jid() -> str:
config = Config(PROJECT_ROOT / "config.json")
owner = config.get("whatsapp.owner", "")
return f"{owner}@s.whatsapp.net"
def extract_original_sender(body: str, from_full: str) -> tuple[str, str]:
"""Dacă e email forwarded, extrage expeditorul și data originale."""
# Caută header-ul tipic de forward: "De la: X\nDate: Y" sau "From: X\nDate: Y"
match = re.search(
r'(?:De la|From):\s*(.+?)\n(?:Date|Data):\s*(.+?)(?:\n|$)',
body, re.IGNORECASE
)
if match:
return match.group(1).strip(), match.group(2).strip()
return from_full, ""
def format_for_whatsapp(subject: str, from_full: str, date: str, body: str) -> str:
"""Curăță corpul emailului și îl formatează pentru WhatsApp."""
# Extrage expeditorul original dacă e forward
original_from, original_date = extract_original_sender(body, from_full)
display_from = original_from
display_date = original_date or date
# Elimină header-ul markdown din fișierul KB (# Titlu, **De la:**, **Data:**, **Salvat:**)
body = re.sub(r'^#.+\n?', '', body, flags=re.MULTILINE)
body = re.sub(r'^\*\*(?:De la|Data|Salvat):\*\*.+\n?', '', body, flags=re.MULTILINE)
# Elimină header-ul forwarded (---------- Forwarded message ---------)
body = re.sub(r'-{5,}\s*Forwarded message\s*-{5,}.*?\n', '', body, flags=re.IGNORECASE)
# Elimină liniile De la/From/Date/Subject/To din header-ul forwarded
body = re.sub(r'^(?:De la|From|Date|Data|Subject|To):.+\n?', '', body, flags=re.IGNORECASE | re.MULTILINE)
# Elimină comentariile KB (<!-- Echo: ... -->)
body = re.sub(r'<!--.*?-->', '', body, flags=re.DOTALL)
# Elimină separatoarele --- rămase
body = re.sub(r'^\s*---\s*$', '', body, flags=re.MULTILINE)
# Elimină linii goale consecutive (mai mult de 2)
body = re.sub(r'\n{3,}', '\n\n', body)
# Elimină spații trailing
body = '\n'.join(line.rstrip() for line in body.splitlines())
body = body.strip()
header = f"*{subject}*\nDe la: {display_from}\nPrimit: {display_date}\n---\n"
full = header + body
if len(full) <= MAX_WA_LENGTH:
return [full]
# Împarte în bucăți la limita WhatsApp, tăind pe linie întreagă
parts = []
remaining = full
while remaining:
if len(remaining) <= MAX_WA_LENGTH:
parts.append(remaining)
break
cut = remaining[:MAX_WA_LENGTH].rfind('\n')
# Dacă nu găsește newline sau e prea aproape de început, taie la limită
if cut < MAX_WA_LENGTH // 2:
cut = MAX_WA_LENGTH
parts.append(remaining[:cut])
remaining = remaining[cut:].lstrip('\n')
total = len(parts)
if total > 1:
parts = [f"{p}\n\n[{i+1}/{total}]" for i, p in enumerate(parts)]
return parts
def send_whatsapp(to: str, text: str) -> bool:
try:
resp = requests.post(
f"{BRIDGE_URL}/send",
json={"to": to, "text": text},
timeout=15,
)
return resp.json().get("ok", False)
except Exception as e:
print(f"[eroare send] {e}", file=sys.stderr)
return False
def fetch_unread_emails():
"""Preia emailurile necitite din inbox fără a le salva sau marca ca citite."""
mail = imaplib.IMAP4_SSL(IMAP_SERVER, IMAP_PORT)
mail.login(IMAP_USER, IMAP_PASS)
mail.select('INBOX')
status, messages = mail.search(None, 'UNSEEN')
email_ids = messages[0].split() if messages[0] else []
results = []
for eid in email_ids:
# BODY.PEEK nu marchează ca citit
status, data = mail.fetch(eid, "(BODY.PEEK[])")
if status != "OK":
continue
msg = email_lib.message_from_bytes(data[0][1])
from_addr = decode_mime_header(msg['From'])
sender_email = extract_sender_email(from_addr)
if sender_email not in WHITELIST:
continue
results.append({
'subject': decode_mime_header(msg['Subject']),
'from_full': from_addr,
'date': msg['Date'],
'body': get_email_body(msg),
})
mail.logout()
return results
def run_forward():
print("Verific emailuri necitite...")
emails = fetch_unread_emails()
if not emails:
print("Niciun email nou de procesat.")
return
owner_jid = get_owner_jid()
for em in emails:
subject = em['subject']
print(f"Trimit: {subject}")
parts = format_for_whatsapp(subject, em['from_full'], em['date'], em['body'])
if DRY_RUN:
for i, part in enumerate(parts):
print(f"\n--- FORWARD {i+1}/{len(parts)} (dry-run) ---")
print(part)
print("------------------------\n")
else:
for part in parts:
ok = send_whatsapp(owner_jid, part)
if not ok:
print(f"Trimitere esuata: {subject}")
break
else:
print(f"Trimis pe WhatsApp ({len(parts)} mesaje): {subject}")
if __name__ == "__main__":
run_forward()

View File

@@ -193,7 +193,9 @@ def save_email_as_note(eid: str) -> dict:
'ok': True, 'ok': True,
'file': str(filepath), 'file': str(filepath),
'subject': subject, 'subject': subject,
'from': sender_email 'from': sender_email,
'from_full': from_addr,
'date': date_str,
} }
def save_unread_emails(): def save_unread_emails():