ranranrunforit commited on
Commit
c2ae7d7
·
verified ·
1 Parent(s): e27bf9f

Upload 18 files

Browse files
Files changed (1) hide show
  1. emailer.py +84 -27
emailer.py CHANGED
@@ -13,9 +13,12 @@ store the App Password as a Space *secret* rather than in code).
13
  from __future__ import annotations
14
 
15
  import os
 
16
  import html
17
  import logging
18
  import smtplib
 
 
19
  import re
20
  from datetime import datetime
21
  from email.mime.text import MIMEText
@@ -25,9 +28,18 @@ from email.utils import formataddr
25
 
26
  logger = logging.getLogger("chan_emailer")
27
 
28
- # ── sender account (override via Space secrets if you prefer) ──
29
- EMAIL_SENDER = os.environ.get("CHAN_EMAIL_SENDER")
30
- EMAIL_PASSWORD = os.environ.get("CHAN_EMAIL_PASSWORD")
 
 
 
 
 
 
 
 
 
31
 
32
  SMTP_CONFIGS = {
33
  "gmail.com": {"server": "smtp.gmail.com", "port": 465, "ssl": True},
@@ -82,47 +94,92 @@ def _close(server):
82
  pass
83
 
84
 
85
- def send_result(content: str, recipients_raw: str, subject_tag: str) -> str:
86
- """Send `content` to the address(es) in `recipients_raw`. Returns a status
87
- string for the UI. Plain English throughout."""
88
- if not content or not content.strip():
89
- return "⚠️ Nothing to send yet — generate a result first."
90
- if content.strip().startswith(("", "🤖 _", "Run ", "Enter ", "Select ")):
91
- return "⚠️ Wait for the AI result to finish, then send."
92
- recipients = _parse_recipients(recipients_raw)
93
- if not recipients:
94
- return "⚠️ Enter a valid email address (e.g. name@example.com)."
95
- if not EMAIL_SENDER or not EMAIL_PASSWORD:
96
- return "⚠️ Sender email is not configured on the server."
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
- subject = (f"Chan Compass · {subject_tag} · "
99
- f"{datetime.now().strftime('%Y-%m-%d %H:%M')}")
 
 
 
100
  msg = MIMEMultipart("alternative")
101
  msg["Subject"] = Header(subject, "utf-8")
102
  msg["From"] = _sender_address(EMAIL_SENDER)
103
  msg["To"] = ", ".join(recipients)
104
  msg.attach(MIMEText(content, "plain", "utf-8"))
105
  msg.attach(MIMEText(_md_to_html(content), "html", "utf-8"))
106
-
107
  domain = EMAIL_SENDER.split("@")[-1].lower()
108
  sc = SMTP_CONFIGS.get(domain, {"server": f"smtp.{domain}", "port": 465, "ssl": True})
109
  server = None
110
  try:
111
  if sc["ssl"]:
112
- server = smtplib.SMTP_SSL(sc["server"], sc["port"], timeout=30)
113
  else:
114
- server = smtplib.SMTP(sc["server"], sc["port"], timeout=30)
115
  server.starttls()
116
  server.login(EMAIL_SENDER, EMAIL_PASSWORD)
117
  server.send_message(msg)
118
- logger.info("email sent to %s", recipients)
119
- return f"✅ Sent to {', '.join(recipients)}."
120
  except smtplib.SMTPAuthenticationError:
121
- return (" Authentication failed check the sender account / App "
122
- "Password (Gmail needs a 16-char App Password).")
123
- except smtplib.SMTPConnectError as e:
124
- return f"❌ Could not connect to the mail server: {e}"
125
  except Exception as e:
126
- return f" Send failed: {e}"
127
  finally:
128
  _close(server)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  from __future__ import annotations
14
 
15
  import os
16
+ import json
17
  import html
18
  import logging
19
  import smtplib
20
+ import urllib.request
21
+ import urllib.error
22
  import re
23
  from datetime import datetime
24
  from email.mime.text import MIMEText
 
28
 
29
  logger = logging.getLogger("chan_emailer")
30
 
31
+ # ── Transport 1 (preferred on HF): Resend HTTPS API on port 443 ──
32
+ # HF Spaces block outbound SMTP ports (465/587) → SMTP fails with
33
+ # "Network is unreachable". The Resend REST API uses plain HTTPS, which Spaces
34
+ # allow. Set a Space secret RESEND_API_KEY to enable it. Free tier ≈ 100/day.
35
+ # Until you verify your own domain, Resend only lets you send FROM
36
+ # "onboarding@resend.dev" — that's the default sender below.
37
+ RESEND_API_KEY = os.environ.get("RESEND_API_KEY", "")
38
+ RESEND_FROM = os.environ.get("RESEND_FROM", "Chan Compass <onboarding@resend.dev>")
39
+
40
+ # ── Transport 2 (fallback, works off-HF): classic SMTP ──
41
+ EMAIL_SENDER = os.environ.get("CHAN_EMAIL_SENDER", "cz78illinoisedu@gmail.com")
42
+ EMAIL_PASSWORD = os.environ.get("CHAN_EMAIL_PASSWORD", "tknl qpmb gzye zlva")
43
 
44
  SMTP_CONFIGS = {
45
  "gmail.com": {"server": "smtp.gmail.com", "port": 465, "ssl": True},
 
94
  pass
95
 
96
 
97
+ def _send_via_resend(recipients: list, subject: str, content: str) -> str:
98
+ """HTTPS POST to Resend works on HF (port 443). Returns '' on success."""
99
+ payload = json.dumps({
100
+ "from": RESEND_FROM,
101
+ "to": recipients,
102
+ "subject": subject,
103
+ "text": content,
104
+ "html": _md_to_html(content),
105
+ }).encode("utf-8")
106
+ req = urllib.request.Request(
107
+ "https://api.resend.com/emails", data=payload, method="POST",
108
+ headers={"Authorization": f"Bearer {RESEND_API_KEY}",
109
+ "Content-Type": "application/json"})
110
+ try:
111
+ with urllib.request.urlopen(req, timeout=30) as resp:
112
+ body = resp.read().decode("utf-8", "ignore")
113
+ if '"id"' in body:
114
+ return ""
115
+ return f"Resend API responded without an id: {body[:200]}"
116
+ except urllib.error.HTTPError as e:
117
+ detail = e.read().decode("utf-8", "ignore")[:200]
118
+ return f"Resend HTTP {e.code}: {detail}"
119
+ except Exception as e:
120
+ return f"Resend request failed: {e}"
121
 
122
+
123
+ def _send_via_smtp(recipients: list, subject: str, content: str) -> str:
124
+ """Classic SMTP — works locally / off-HF. Returns '' on success."""
125
+ if not EMAIL_SENDER or not EMAIL_PASSWORD:
126
+ return "SMTP sender not configured."
127
  msg = MIMEMultipart("alternative")
128
  msg["Subject"] = Header(subject, "utf-8")
129
  msg["From"] = _sender_address(EMAIL_SENDER)
130
  msg["To"] = ", ".join(recipients)
131
  msg.attach(MIMEText(content, "plain", "utf-8"))
132
  msg.attach(MIMEText(_md_to_html(content), "html", "utf-8"))
 
133
  domain = EMAIL_SENDER.split("@")[-1].lower()
134
  sc = SMTP_CONFIGS.get(domain, {"server": f"smtp.{domain}", "port": 465, "ssl": True})
135
  server = None
136
  try:
137
  if sc["ssl"]:
138
+ server = smtplib.SMTP_SSL(sc["server"], sc["port"], timeout=20)
139
  else:
140
+ server = smtplib.SMTP(sc["server"], sc["port"], timeout=20)
141
  server.starttls()
142
  server.login(EMAIL_SENDER, EMAIL_PASSWORD)
143
  server.send_message(msg)
144
+ return ""
 
145
  except smtplib.SMTPAuthenticationError:
146
+ return "SMTP authentication failed (check sender / App Password)."
147
+ except OSError as e:
148
+ return f"SMTP network error: {e}"
 
149
  except Exception as e:
150
+ return f"SMTP failed: {e}"
151
  finally:
152
  _close(server)
153
+
154
+
155
+ def send_result(content: str, recipients_raw: str, subject_tag: str) -> str:
156
+ """Send `content` to the address(es). Prefers the Resend HTTPS API (works on
157
+ HF Spaces); falls back to SMTP. English status string for the UI."""
158
+ if not content or not content.strip():
159
+ return "⚠️ Nothing to send yet — generate a result first."
160
+ if content.strip().startswith(("⏳", "🤖 _", "Run ", "Enter ", "Select ")):
161
+ return "⚠️ Wait for the AI result to finish, then send."
162
+ recipients = _parse_recipients(recipients_raw)
163
+ if not recipients:
164
+ return "⚠️ Enter a valid email address (e.g. name@example.com)."
165
+
166
+ subject = (f"Chan Compass · {subject_tag} · "
167
+ f"{datetime.now().strftime('%Y-%m-%d %H:%M')}")
168
+
169
+ errors = []
170
+ if RESEND_API_KEY:
171
+ err = _send_via_resend(recipients, subject, content)
172
+ if not err:
173
+ return f"✅ Sent to {', '.join(recipients)} (via Resend API)."
174
+ errors.append(err)
175
+ smtp_err = _send_via_smtp(recipients, subject, content)
176
+ if not smtp_err:
177
+ return f"✅ Sent to {', '.join(recipients)} (via SMTP)."
178
+ errors.append(smtp_err)
179
+
180
+ if not RESEND_API_KEY:
181
+ return ("❌ SMTP is blocked on Hugging Face Spaces (outbound mail ports "
182
+ "are closed). Fix: create a free key at resend.com and add it as "
183
+ "a Space secret named **RESEND_API_KEY** (then it sends over "
184
+ f"HTTPS). Detail: {smtp_err}")
185
+ return "❌ Send failed. " + " | ".join(errors)