Spaces:
Sleeping
Sleeping
Commit ·
b55c72c
1
Parent(s): 9dfb9fd
Add email verification and failed email detection with detailed error display
Browse files
backend/routers/email.py
CHANGED
|
@@ -41,10 +41,22 @@ async def get_email_config():
|
|
| 41 |
|
| 42 |
@router.get("/progress/{job_id}")
|
| 43 |
async def get_email_progress(job_id: str):
|
| 44 |
-
"""Return the current progress of an email sending job"""
|
| 45 |
if job_id not in email_progress:
|
| 46 |
raise HTTPException(status_code=404, detail="Job not found")
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
|
| 49 |
def send_email_task(
|
| 50 |
job_id,
|
|
@@ -134,21 +146,55 @@ def send_email_task(
|
|
| 134 |
print(f"Sending email to {email} via SendGrid...")
|
| 135 |
sg = SendGridAPIClient(api_key)
|
| 136 |
response = sg.send(message)
|
| 137 |
-
|
|
|
|
| 138 |
|
| 139 |
-
#
|
| 140 |
-
if
|
| 141 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 142 |
|
| 143 |
except Exception as e:
|
| 144 |
-
|
|
|
|
| 145 |
import traceback
|
| 146 |
traceback.print_exc()
|
| 147 |
|
| 148 |
-
# Update progress
|
| 149 |
if job_id in email_progress:
|
| 150 |
email_progress[job_id]["sent"] += 1
|
| 151 |
email_progress[job_id]["failed"] += 1
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 152 |
|
| 153 |
@router.post("/send")
|
| 154 |
async def send_emails(
|
|
|
|
| 41 |
|
| 42 |
@router.get("/progress/{job_id}")
|
| 43 |
async def get_email_progress(job_id: str):
|
| 44 |
+
"""Return the current progress of an email sending job with detailed failure info"""
|
| 45 |
if job_id not in email_progress:
|
| 46 |
raise HTTPException(status_code=404, detail="Job not found")
|
| 47 |
+
|
| 48 |
+
progress = email_progress[job_id]
|
| 49 |
+
|
| 50 |
+
# Return detailed progress including failed email details
|
| 51 |
+
return {
|
| 52 |
+
"total": progress["total"],
|
| 53 |
+
"sent": progress["sent"],
|
| 54 |
+
"failed": progress["failed"],
|
| 55 |
+
"status": progress["status"],
|
| 56 |
+
"failed_emails": progress.get("failed_emails", []), # List of {email, name, error}
|
| 57 |
+
"success_emails": progress.get("success_emails", []) # List of emails sent successfully
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
|
| 61 |
def send_email_task(
|
| 62 |
job_id,
|
|
|
|
| 146 |
print(f"Sending email to {email} via SendGrid...")
|
| 147 |
sg = SendGridAPIClient(api_key)
|
| 148 |
response = sg.send(message)
|
| 149 |
+
status_code = response.status_code
|
| 150 |
+
print(f"Sent to {email}: Status {status_code}")
|
| 151 |
|
| 152 |
+
# Check if email was actually sent successfully (2xx status codes)
|
| 153 |
+
if status_code >= 200 and status_code < 300:
|
| 154 |
+
# Update progress - successful
|
| 155 |
+
if job_id in email_progress:
|
| 156 |
+
email_progress[job_id]["sent"] += 1
|
| 157 |
+
# Track successful email
|
| 158 |
+
if "success_emails" not in email_progress[job_id]:
|
| 159 |
+
email_progress[job_id]["success_emails"] = []
|
| 160 |
+
email_progress[job_id]["success_emails"].append({
|
| 161 |
+
"email": email,
|
| 162 |
+
"name": name,
|
| 163 |
+
"status_code": status_code
|
| 164 |
+
})
|
| 165 |
+
else:
|
| 166 |
+
# Non-2xx status code - treat as failure
|
| 167 |
+
print(f"SendGrid returned non-success status {status_code} for {email}")
|
| 168 |
+
if job_id in email_progress:
|
| 169 |
+
email_progress[job_id]["sent"] += 1
|
| 170 |
+
email_progress[job_id]["failed"] += 1
|
| 171 |
+
if "failed_emails" not in email_progress[job_id]:
|
| 172 |
+
email_progress[job_id]["failed_emails"] = []
|
| 173 |
+
email_progress[job_id]["failed_emails"].append({
|
| 174 |
+
"email": email,
|
| 175 |
+
"name": name,
|
| 176 |
+
"error": f"SendGrid status code: {status_code}"
|
| 177 |
+
})
|
| 178 |
|
| 179 |
except Exception as e:
|
| 180 |
+
error_message = str(e)
|
| 181 |
+
print(f"Failed to send to {email}: {error_message}")
|
| 182 |
import traceback
|
| 183 |
traceback.print_exc()
|
| 184 |
|
| 185 |
+
# Update progress with detailed failure info
|
| 186 |
if job_id in email_progress:
|
| 187 |
email_progress[job_id]["sent"] += 1
|
| 188 |
email_progress[job_id]["failed"] += 1
|
| 189 |
+
# Track failed email with error details
|
| 190 |
+
if "failed_emails" not in email_progress[job_id]:
|
| 191 |
+
email_progress[job_id]["failed_emails"] = []
|
| 192 |
+
email_progress[job_id]["failed_emails"].append({
|
| 193 |
+
"email": email,
|
| 194 |
+
"name": name,
|
| 195 |
+
"error": error_message
|
| 196 |
+
})
|
| 197 |
+
|
| 198 |
|
| 199 |
@router.post("/send")
|
| 200 |
async def send_emails(
|
frontend/src/components/ReviewSendStep.jsx
CHANGED
|
@@ -14,6 +14,7 @@ const ReviewSendStep = ({ fileData, designParams, mappings, onBack }) => {
|
|
| 14 |
const [status, setStatus] = useState(null);
|
| 15 |
const [progress, setProgress] = useState(null);
|
| 16 |
const [jobId, setJobId] = useState(null);
|
|
|
|
| 17 |
|
| 18 |
// Fetch email configuration on mount
|
| 19 |
useEffect(() => {
|
|
@@ -67,7 +68,17 @@ const ReviewSendStep = ({ fileData, designParams, mappings, onBack }) => {
|
|
| 67 |
if (progressData.sent >= progressData.total) {
|
| 68 |
clearInterval(pollInterval);
|
| 69 |
setSending(false);
|
| 70 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
setProgress(null);
|
| 72 |
}
|
| 73 |
} catch (pollError) {
|
|
@@ -291,12 +302,40 @@ const ReviewSendStep = ({ fileData, designParams, mappings, onBack }) => {
|
|
| 291 |
animate={{ opacity: 1, y: 0 }}
|
| 292 |
className={`p-4 rounded-lg border text-center ${status.type === 'success'
|
| 293 |
? 'bg-green-50 border-green-200 text-green-700'
|
| 294 |
-
:
|
|
|
|
|
|
|
| 295 |
}`}
|
| 296 |
>
|
| 297 |
{status.msg}
|
| 298 |
</motion.div>
|
| 299 |
)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 300 |
</div>
|
| 301 |
</div>
|
| 302 |
|
|
|
|
| 14 |
const [status, setStatus] = useState(null);
|
| 15 |
const [progress, setProgress] = useState(null);
|
| 16 |
const [jobId, setJobId] = useState(null);
|
| 17 |
+
const [failedEmails, setFailedEmails] = useState([]); // Store failed email details
|
| 18 |
|
| 19 |
// Fetch email configuration on mount
|
| 20 |
useEffect(() => {
|
|
|
|
| 68 |
if (progressData.sent >= progressData.total) {
|
| 69 |
clearInterval(pollInterval);
|
| 70 |
setSending(false);
|
| 71 |
+
|
| 72 |
+
// Store failed emails for display
|
| 73 |
+
if (progressData.failed_emails && progressData.failed_emails.length > 0) {
|
| 74 |
+
setFailedEmails(progressData.failed_emails);
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
const successCount = progressData.sent - progressData.failed;
|
| 78 |
+
setStatus({
|
| 79 |
+
type: progressData.failed > 0 ? 'warning' : 'success',
|
| 80 |
+
msg: `Successfully sent ${successCount} emails!${progressData.failed > 0 ? ` (${progressData.failed} failed)` : ''}`
|
| 81 |
+
});
|
| 82 |
setProgress(null);
|
| 83 |
}
|
| 84 |
} catch (pollError) {
|
|
|
|
| 302 |
animate={{ opacity: 1, y: 0 }}
|
| 303 |
className={`p-4 rounded-lg border text-center ${status.type === 'success'
|
| 304 |
? 'bg-green-50 border-green-200 text-green-700'
|
| 305 |
+
: status.type === 'warning'
|
| 306 |
+
? 'bg-yellow-50 border-yellow-200 text-yellow-700'
|
| 307 |
+
: 'bg-red-50 border-red-200 text-red-700'
|
| 308 |
}`}
|
| 309 |
>
|
| 310 |
{status.msg}
|
| 311 |
</motion.div>
|
| 312 |
)}
|
| 313 |
+
|
| 314 |
+
{/* Failed Emails Details */}
|
| 315 |
+
{failedEmails.length > 0 && (
|
| 316 |
+
<motion.div
|
| 317 |
+
initial={{ opacity: 0, y: 10 }}
|
| 318 |
+
animate={{ opacity: 1, y: 0 }}
|
| 319 |
+
className="bg-red-50 border border-red-200 p-6 rounded-lg"
|
| 320 |
+
>
|
| 321 |
+
<h4 className="text-sm font-semibold text-red-800 mb-3 flex items-center">
|
| 322 |
+
⚠️ Failed Emails ({failedEmails.length})
|
| 323 |
+
</h4>
|
| 324 |
+
<div className="max-h-48 overflow-y-auto space-y-2">
|
| 325 |
+
{failedEmails.map((failed, index) => (
|
| 326 |
+
<div key={index} className="bg-white rounded p-3 border border-red-100">
|
| 327 |
+
<div className="flex justify-between items-start">
|
| 328 |
+
<div>
|
| 329 |
+
<p className="text-sm font-medium text-gray-900">{failed.name}</p>
|
| 330 |
+
<p className="text-xs text-gray-600">{failed.email}</p>
|
| 331 |
+
</div>
|
| 332 |
+
</div>
|
| 333 |
+
<p className="text-xs text-red-600 mt-1 font-mono">{failed.error}</p>
|
| 334 |
+
</div>
|
| 335 |
+
))}
|
| 336 |
+
</div>
|
| 337 |
+
</motion.div>
|
| 338 |
+
)}
|
| 339 |
</div>
|
| 340 |
</div>
|
| 341 |
|