coderuday21 commited on
Commit
2d92257
·
1 Parent(s): 1fc9452

Use Resend HTTPS API for email (HF Spaces); SMTP fallback for local

Browse files
Files changed (3) hide show
  1. README.md +6 -0
  2. app/notifier.py +54 -18
  3. requirements.txt +1 -0
README.md CHANGED
@@ -55,6 +55,12 @@ Standalone web application for satellite image change detection with **user acco
55
 
56
  - **Database**: set `DATABASE_URL` (e.g. `postgresql://user:pass@host/db`) to use another DB; otherwise SQLite under `data/satellite_app.db` is used.
57
  - **JWT**: set `SECRET_KEY` in `app/auth.py` (or via env) in production.
 
 
 
 
 
 
58
 
59
  ## Project layout
60
 
 
55
 
56
  - **Database**: set `DATABASE_URL` (e.g. `postgresql://user:pass@host/db`) to use another DB; otherwise SQLite under `data/satellite_app.db` is used.
57
  - **JWT**: set `SECRET_KEY` in `app/auth.py` (or via env) in production.
58
+ - **Email (Hugging Face Spaces)**: Outbound SMTP is blocked on Spaces. Use [Resend](https://resend.com) (HTTPS API):
59
+ 1. Sign up at [resend.com](https://resend.com), create an API key.
60
+ 2. In your Space → **Settings** → **Repository secrets**, add:
61
+ - `RESEND_API_KEY` = your Resend API key (e.g. `re_...`).
62
+ - (Optional) `RESEND_FROM` = sender string, e.g. `AI Change Detection <onboarding@resend.dev>` or your verified domain.
63
+ 3. Emails will be sent via Resend; no SMTP or Gmail app password needed on the server.
64
 
65
  ## Project layout
66
 
app/notifier.py CHANGED
@@ -1,8 +1,8 @@
1
  """
2
  Email notification module.
3
- Sends HTML-formatted detection reports via Gmail SMTP.
4
- Credentials are read from environment variables and loaded at send time so a
5
- server restart or secret update is picked up reliably.
6
  """
7
  import logging
8
  import os
@@ -20,6 +20,15 @@ EMAIL_RE = re.compile(r"^[^\s@]+@[^\s@]+\.[^\s@]+$")
20
 
21
  TEMPLATE_PATH = Path(__file__).resolve().parent.parent / "templates" / "ChangeDetection.html"
22
 
 
 
 
 
 
 
 
 
 
23
 
24
  def _smtp_settings():
25
  return {
@@ -94,7 +103,6 @@ def build_email_body(
94
  for key, val in replacements.items():
95
  html = html.replace(key, val)
96
 
97
- # Handle conditional regions block
98
  if regions:
99
  html = html.replace("{{#regions}}", "").replace("{{/regions}}", "")
100
  else:
@@ -103,18 +111,40 @@ def build_email_body(
103
  return html
104
 
105
 
106
- def _send_html_email(recipient: str, subject: str, html_body: str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  settings = _smtp_settings()
108
  smtp_user = settings["user"]
109
  smtp_pass = settings["password"]
110
  smtp_host = settings["host"]
111
  smtp_port = settings["port"]
112
 
113
- if not _valid_email(recipient):
114
- return False, "Enter a valid recipient email address."
115
  if not smtp_user or not smtp_pass:
116
- logger.warning("SMTP_USER or SMTP_PASS not set — skipping email notification")
117
- return False, "SMTP credentials are not configured on the server."
118
 
119
  msg = MIMEMultipart("alternative")
120
  msg["Subject"] = subject
@@ -130,21 +160,27 @@ def _send_html_email(recipient: str, subject: str, html_body: str):
130
  server.ehlo()
131
  server.login(smtp_user, smtp_pass)
132
  server.sendmail(smtp_user, recipient, msg.as_string())
133
- logger.info("Notification email sent to %s", recipient)
134
  return True, None
135
  except smtplib.SMTPAuthenticationError:
136
  logger.exception("SMTP authentication failed")
137
  return False, "SMTP authentication failed. Check the Gmail app password."
138
  except Exception as exc:
139
- logger.error(
140
- "Failed to send notification email to %s: %s: %s",
141
- recipient,
142
- type(exc).__name__,
143
- exc,
144
- )
145
  return False, f"{type(exc).__name__}: {exc}"
146
 
147
 
 
 
 
 
 
 
 
 
 
 
 
148
  def send_notification(
149
  recipient: str,
150
  title: str,
@@ -165,14 +201,14 @@ def send_notification(
165
 
166
 
167
  def send_test_email(recipient: str):
168
- """Send a small test email so SMTP configuration can be verified quickly."""
169
  now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
170
  html_body = f"""
171
  <html>
172
  <body style="font-family: Arial, sans-serif; color: #222;">
173
  <h2>AI Change Detection Email Test</h2>
174
  <p>This is a test email from the AI Change Detection application.</p>
175
- <p>If you received this, the SMTP configuration is working.</p>
176
  <p><strong>Timestamp:</strong> {now}</p>
177
  </body>
178
  </html>
 
1
  """
2
  Email notification module.
3
+ Sends HTML-formatted emails via Resend HTTPS API (works on HF Spaces)
4
+ or fallback to SMTP for local development.
5
+ Credentials from environment variables only.
6
  """
7
  import logging
8
  import os
 
20
 
21
  TEMPLATE_PATH = Path(__file__).resolve().parent.parent / "templates" / "ChangeDetection.html"
22
 
23
+ # Resend: preferred on HF Spaces (HTTPS, no outbound SMTP)
24
+ RESEND_API_KEY = os.environ.get("RESEND_API_KEY", "")
25
+ RESEND_FROM = os.environ.get("RESEND_FROM", "AI Change Detection <onboarding@resend.dev>")
26
+
27
+ if RESEND_API_KEY:
28
+ logger.info("Email: using Resend HTTPS API (RESEND_API_KEY set)")
29
+ else:
30
+ logger.info("Email: using SMTP fallback (set RESEND_API_KEY for Hugging Face Spaces)")
31
+
32
 
33
  def _smtp_settings():
34
  return {
 
103
  for key, val in replacements.items():
104
  html = html.replace(key, val)
105
 
 
106
  if regions:
107
  html = html.replace("{{#regions}}", "").replace("{{/regions}}", "")
108
  else:
 
111
  return html
112
 
113
 
114
+ def _send_via_resend(recipient: str, subject: str, html_body: str):
115
+ """Send email using Resend HTTPS API. Works on HF Spaces."""
116
+ if not RESEND_API_KEY:
117
+ return False, "RESEND_API_KEY is not set."
118
+ try:
119
+ import resend
120
+ resend.api_key = RESEND_API_KEY
121
+ r = resend.Emails.send({
122
+ "from": RESEND_FROM,
123
+ "to": [recipient],
124
+ "subject": subject,
125
+ "html": html_body,
126
+ })
127
+ # SDK may return object with .id or dict with "id"
128
+ ok = (getattr(r, "id", None) or (isinstance(r, dict) and r.get("id")) or r is not None)
129
+ if ok:
130
+ logger.info("Resend email sent to %s", recipient)
131
+ return True, None
132
+ return False, "Resend API returned no id."
133
+ except Exception as exc:
134
+ logger.exception("Resend send failed: %s", exc)
135
+ return False, f"{type(exc).__name__}: {exc}"
136
+
137
+
138
+ def _send_via_smtp(recipient: str, subject: str, html_body: str):
139
+ """Send email via SMTP (for local dev; often blocked on HF)."""
140
  settings = _smtp_settings()
141
  smtp_user = settings["user"]
142
  smtp_pass = settings["password"]
143
  smtp_host = settings["host"]
144
  smtp_port = settings["port"]
145
 
 
 
146
  if not smtp_user or not smtp_pass:
147
+ return False, "SMTP credentials are not configured."
 
148
 
149
  msg = MIMEMultipart("alternative")
150
  msg["Subject"] = subject
 
160
  server.ehlo()
161
  server.login(smtp_user, smtp_pass)
162
  server.sendmail(smtp_user, recipient, msg.as_string())
163
+ logger.info("SMTP email sent to %s", recipient)
164
  return True, None
165
  except smtplib.SMTPAuthenticationError:
166
  logger.exception("SMTP authentication failed")
167
  return False, "SMTP authentication failed. Check the Gmail app password."
168
  except Exception as exc:
169
+ logger.error("SMTP failed to %s: %s: %s", recipient, type(exc).__name__, exc)
 
 
 
 
 
170
  return False, f"{type(exc).__name__}: {exc}"
171
 
172
 
173
+ def _send_html_email(recipient: str, subject: str, html_body: str):
174
+ """Send email: use Resend if API key set, else SMTP."""
175
+ if not _valid_email(recipient):
176
+ return False, "Enter a valid recipient email address."
177
+
178
+ if RESEND_API_KEY:
179
+ return _send_via_resend(recipient, subject, html_body)
180
+ # Fallback to SMTP (e.g. local dev)
181
+ return _send_via_smtp(recipient, subject, html_body)
182
+
183
+
184
  def send_notification(
185
  recipient: str,
186
  title: str,
 
201
 
202
 
203
  def send_test_email(recipient: str):
204
+ """Send a small test email to verify delivery."""
205
  now = datetime.now(timezone.utc).strftime("%Y-%m-%d %H:%M UTC")
206
  html_body = f"""
207
  <html>
208
  <body style="font-family: Arial, sans-serif; color: #222;">
209
  <h2>AI Change Detection Email Test</h2>
210
  <p>This is a test email from the AI Change Detection application.</p>
211
+ <p>If you received this, the email configuration is working.</p>
212
  <p><strong>Timestamp:</strong> {now}</p>
213
  </body>
214
  </html>
requirements.txt CHANGED
@@ -9,3 +9,4 @@ pillow>=10.0.0
9
  numpy>=1.24.0
10
  opencv-python-headless>=4.8.0
11
  scikit-learn>=1.3.0
 
 
9
  numpy>=1.24.0
10
  opencv-python-headless>=4.8.0
11
  scikit-learn>=1.3.0
12
+ resend>=2.0.0