1336 lines
27 KiB
Markdown
1336 lines
27 KiB
Markdown
# Email and SMTP Issues
|
|
|
|
This guide covers email sending, SMTP configuration, and template-related problems in Changemaker Lite V2.
|
|
|
|
## Overview
|
|
|
|
### Email System Architecture
|
|
|
|
Changemaker Lite V2 has **dual email systems**:
|
|
|
|
1. **Transactional Emails** (BullMQ + Nodemailer)
|
|
- Campaign advocacy emails
|
|
- Shift confirmation emails
|
|
- Response verification emails
|
|
- System notifications
|
|
|
|
2. **Newsletter Emails** (Listmonk)
|
|
- Marketing campaigns
|
|
- Newsletter broadcasts
|
|
- Subscriber management
|
|
|
|
### Email Flow
|
|
|
|
```
|
|
User Action → Email Service → BullMQ Queue → Worker → SMTP Server → Recipient
|
|
```
|
|
|
|
### Key Components
|
|
|
|
- **BullMQ** - Job queue for async email sending
|
|
- **Nodemailer** - SMTP client library
|
|
- **Redis** - Queue backend
|
|
- **MailHog** - Development email capture (test mode)
|
|
- **Listmonk** - Newsletter platform (optional)
|
|
|
|
---
|
|
|
|
## SMTP Configuration
|
|
|
|
### Connection Refused
|
|
|
|
**Severity:** 🔴 Critical
|
|
|
|
#### Symptoms
|
|
|
|
API logs:
|
|
```
|
|
Error: Connection timeout
|
|
Error: connect ECONNREFUSED smtp.gmail.com:587
|
|
Error: Invalid login: 535-5.7.8 Username and Password not accepted
|
|
```
|
|
|
|
Emails not sending.
|
|
|
|
#### Common Causes
|
|
|
|
1. **Wrong SMTP host** - Incorrect hostname
|
|
2. **Port blocked** - Firewall blocking port 587/465
|
|
3. **Wrong credentials** - Invalid username/password
|
|
4. **TLS/SSL mismatch** - Wrong secure setting
|
|
|
|
#### Solutions
|
|
|
|
**Solution 1: Test SMTP connection**
|
|
|
|
```bash
|
|
# Test with telnet
|
|
telnet smtp.gmail.com 587
|
|
|
|
# Should show:
|
|
# 220 smtp.gmail.com ESMTP ...
|
|
|
|
# Or test with openssl (for SSL)
|
|
openssl s_client -connect smtp.gmail.com:465
|
|
```
|
|
|
|
**Solution 2: Verify SMTP configuration**
|
|
|
|
In `.env`:
|
|
|
|
```bash
|
|
# Gmail example (requires app password)
|
|
SMTP_HOST=smtp.gmail.com
|
|
SMTP_PORT=587
|
|
SMTP_SECURE=false # false for STARTTLS on 587, true for SSL on 465
|
|
SMTP_USER=your-email@gmail.com
|
|
SMTP_PASS=your-app-password # NOT regular password
|
|
SMTP_FROM=your-email@gmail.com
|
|
|
|
# Office365 example
|
|
SMTP_HOST=smtp.office365.com
|
|
SMTP_PORT=587
|
|
SMTP_SECURE=false
|
|
SMTP_USER=your-email@outlook.com
|
|
SMTP_PASS=your-password
|
|
SMTP_FROM=your-email@outlook.com
|
|
|
|
# SendGrid example
|
|
SMTP_HOST=smtp.sendgrid.net
|
|
SMTP_PORT=587
|
|
SMTP_SECURE=false
|
|
SMTP_USER=apikey # Literally "apikey"
|
|
SMTP_PASS=your-sendgrid-api-key
|
|
SMTP_FROM=your-verified-sender@example.com
|
|
```
|
|
|
|
**Solution 3: Use test mode**
|
|
|
|
```bash
|
|
# In .env
|
|
EMAIL_TEST_MODE=true
|
|
|
|
# Restart API
|
|
docker compose restart api
|
|
|
|
# All emails now sent to MailHog
|
|
# View at http://localhost:8025
|
|
```
|
|
|
|
**Solution 4: Test email sending**
|
|
|
|
```bash
|
|
# Send test email via API
|
|
curl -X POST http://localhost:4000/api/test-email \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
-d '{
|
|
"to": "test@example.com",
|
|
"subject": "Test Email",
|
|
"text": "This is a test email from Changemaker Lite"
|
|
}'
|
|
|
|
# Check API logs
|
|
docker compose logs api | grep -i "email\|smtp"
|
|
```
|
|
|
|
**Solution 5: Gmail app password**
|
|
|
|
For Gmail (required if 2FA enabled):
|
|
|
|
1. Go to https://myaccount.google.com/apppasswords
|
|
2. Select app: Mail
|
|
3. Select device: Other (Changemaker Lite)
|
|
4. Click Generate
|
|
5. Copy 16-character password
|
|
6. Use in SMTP_PASS (no spaces)
|
|
|
|
#### Prevention
|
|
|
|
- **Test mode for dev** - Use MailHog locally
|
|
- **Secure credentials** - Use app passwords, not real passwords
|
|
- **Environment-specific** - Different SMTP per environment
|
|
- **Health checks** - Test SMTP on API startup
|
|
|
|
---
|
|
|
|
### Authentication Failed
|
|
|
|
**Severity:** 🔴 Critical
|
|
|
|
#### Symptoms
|
|
|
|
```
|
|
Error: Invalid login: 535-5.7.8 Username and Password not accepted
|
|
Error: 535 Authentication failed
|
|
```
|
|
|
|
#### Common Causes
|
|
|
|
1. **Wrong password** - Incorrect password
|
|
2. **2FA enabled** - Need app password
|
|
3. **Less secure apps** - Gmail blocking
|
|
4. **Account locked** - Too many failed attempts
|
|
|
|
#### Solutions
|
|
|
|
**Solution 1: Verify credentials**
|
|
|
|
```bash
|
|
# Check .env
|
|
cat .env | grep SMTP_
|
|
|
|
# Test login manually (if possible)
|
|
# Gmail doesn't allow this, but some SMTP servers do
|
|
```
|
|
|
|
**Solution 2: Enable less secure apps (Gmail)**
|
|
|
|
⚠️ Not recommended. Use app password instead.
|
|
|
|
1. Go to https://myaccount.google.com/lesssecureapps
|
|
2. Turn on "Allow less secure apps"
|
|
|
|
**Solution 3: Check account status**
|
|
|
|
1. Try logging into email account via web
|
|
2. Check for security alerts
|
|
3. Verify account not locked
|
|
|
|
**Solution 4: Use OAuth2 (advanced)**
|
|
|
|
For production Gmail:
|
|
|
|
```typescript
|
|
// In email.service.ts
|
|
const transporter = nodemailer.createTransporter({
|
|
service: 'gmail',
|
|
auth: {
|
|
type: 'OAuth2',
|
|
user: process.env.SMTP_USER,
|
|
clientId: process.env.GMAIL_CLIENT_ID,
|
|
clientSecret: process.env.GMAIL_CLIENT_SECRET,
|
|
refreshToken: process.env.GMAIL_REFRESH_TOKEN
|
|
}
|
|
});
|
|
```
|
|
|
|
#### Prevention
|
|
|
|
- **App passwords** - Always use app-specific passwords
|
|
- **Test credentials** - Verify before deploying
|
|
- **Monitor failures** - Alert on auth failures
|
|
- **Backup SMTP** - Configure fallback SMTP server
|
|
|
|
---
|
|
|
|
### Invalid Credentials
|
|
|
|
**Severity:** 🔴 Critical
|
|
|
|
#### Symptoms
|
|
|
|
```
|
|
Error: Invalid SMTP credentials
|
|
Error: Username and Password not accepted
|
|
```
|
|
|
|
#### Solutions
|
|
|
|
See "Authentication Failed" section above.
|
|
|
|
---
|
|
|
|
### Port Blocked
|
|
|
|
**Severity:** 🟠 High
|
|
|
|
#### Symptoms
|
|
|
|
```
|
|
Error: connect ETIMEDOUT smtp.gmail.com:587
|
|
Error: Connection timeout
|
|
```
|
|
|
|
Connection attempt hangs, then times out after 30+ seconds.
|
|
|
|
#### Common Causes
|
|
|
|
1. **Firewall blocking** - Network firewall blocking port
|
|
2. **ISP blocking** - ISP blocks port 25/587
|
|
3. **Docker network** - Container can't reach external SMTP
|
|
|
|
#### Solutions
|
|
|
|
**Solution 1: Test port access**
|
|
|
|
```bash
|
|
# From API container
|
|
docker compose exec api telnet smtp.gmail.com 587
|
|
|
|
# If timeout, port is blocked
|
|
```
|
|
|
|
**Solution 2: Try alternative port**
|
|
|
|
```bash
|
|
# Try port 465 (SSL) instead of 587 (STARTTLS)
|
|
SMTP_PORT=465
|
|
SMTP_SECURE=true
|
|
|
|
# Or try port 2525 (some providers)
|
|
SMTP_PORT=2525
|
|
SMTP_SECURE=false
|
|
```
|
|
|
|
**Solution 3: Check Docker network**
|
|
|
|
```bash
|
|
# Test external connectivity
|
|
docker compose exec api ping -c 3 smtp.gmail.com
|
|
|
|
# Test DNS resolution
|
|
docker compose exec api nslookup smtp.gmail.com
|
|
|
|
# If fails, Docker network issue
|
|
```
|
|
|
|
**Solution 4: Use SMTP relay**
|
|
|
|
If ISP blocks SMTP, use relay service:
|
|
- SendGrid
|
|
- Mailgun
|
|
- Amazon SES
|
|
- Postmark
|
|
|
|
**Solution 5: VPN or proxy**
|
|
|
|
As last resort, route SMTP through VPN/proxy.
|
|
|
|
#### Prevention
|
|
|
|
- **Use relay services** - More reliable than direct SMTP
|
|
- **Multiple ports** - Try 587, 465, 2525
|
|
- **Test on deploy** - Verify SMTP works in production
|
|
- **Documentation** - Document network requirements
|
|
|
|
---
|
|
|
|
## Template Issues
|
|
|
|
### Template Not Found
|
|
|
|
**Severity:** 🟠 High
|
|
|
|
#### Symptoms
|
|
|
|
API logs:
|
|
```
|
|
Error: Email template not found: campaign-email
|
|
Error: ENOENT: no such file or directory, open 'templates/campaign-email.html'
|
|
```
|
|
|
|
#### Common Causes
|
|
|
|
1. **Template file missing** - File doesn't exist
|
|
2. **Wrong template name** - Typo in name
|
|
3. **Wrong directory** - Looking in wrong path
|
|
4. **Deleted template** - Template was removed
|
|
|
|
#### Solutions
|
|
|
|
**Solution 1: List available templates**
|
|
|
|
```bash
|
|
# List template files
|
|
docker compose exec api ls -la templates/
|
|
|
|
# Should show:
|
|
# campaign-email.html
|
|
# shift-confirmation.html
|
|
# verification-email.html
|
|
# response-verification.html
|
|
```
|
|
|
|
**Solution 2: Create missing template**
|
|
|
|
```bash
|
|
# Create template file
|
|
docker compose exec api sh -c 'cat > templates/my-template.html << EOF
|
|
<!DOCTYPE html>
|
|
<html>
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<title>{{title}}</title>
|
|
</head>
|
|
<body>
|
|
<h1>Hello {{name}}</h1>
|
|
<p>{{message}}</p>
|
|
</body>
|
|
</html>
|
|
EOF'
|
|
```
|
|
|
|
**Solution 3: Use email template system**
|
|
|
|
Navigate to `/app/email-templates`:
|
|
|
|
1. Click "Create Template"
|
|
2. Fill in details
|
|
3. Design template
|
|
4. Save (creates file + DB record)
|
|
|
|
**Solution 4: Check template name**
|
|
|
|
```typescript
|
|
// In code, template name must match filename (without .html)
|
|
await emailService.sendEmail({
|
|
to: email,
|
|
subject: 'Campaign Email',
|
|
template: 'campaign-email', // Looks for templates/campaign-email.html
|
|
variables: { ... }
|
|
});
|
|
```
|
|
|
|
**Solution 5: Verify template path**
|
|
|
|
In `api/src/services/email.service.ts`:
|
|
|
|
```typescript
|
|
const templatePath = path.join(__dirname, '../../templates', `${template}.html`);
|
|
// Resolves to: api/templates/campaign-email.html
|
|
```
|
|
|
|
#### Prevention
|
|
|
|
- **Seed templates** - Include default templates in seed
|
|
- **Template management** - Use admin UI to manage
|
|
- **Version control** - Keep templates in git
|
|
- **Validation** - Check template exists before sending
|
|
|
|
---
|
|
|
|
### Variable Not Replaced
|
|
|
|
**Severity:** 🟡 Medium
|
|
|
|
#### Symptoms
|
|
|
|
Email received with unreplaced placeholders:
|
|
|
|
```
|
|
Hello {{name}},
|
|
|
|
Your campaign {{campaignName}} is ready.
|
|
```
|
|
|
|
#### Common Causes
|
|
|
|
1. **Variable not provided** - Missing from variables object
|
|
2. **Typo in variable name** - Mismatch between template and code
|
|
3. **Wrong delimiter** - Using ${} instead of {{}}
|
|
4. **Escaping issue** - HTML entities interfering
|
|
|
|
#### Solutions
|
|
|
|
**Solution 1: List template variables**
|
|
|
|
```bash
|
|
# Find all variables in template
|
|
docker compose exec api grep -o '{{[^}]*}}' templates/campaign-email.html
|
|
|
|
# Shows:
|
|
# {{name}}
|
|
# {{campaignName}}
|
|
# {{campaignUrl}}
|
|
```
|
|
|
|
**Solution 2: Provide all variables**
|
|
|
|
```typescript
|
|
await emailService.sendEmail({
|
|
to: email,
|
|
subject: 'Campaign Ready',
|
|
template: 'campaign-email',
|
|
variables: {
|
|
name: user.name, // Must provide ALL variables in template
|
|
campaignName: campaign.name,
|
|
campaignUrl: `${process.env.PUBLIC_URL}/campaigns/${campaign.id}`
|
|
}
|
|
});
|
|
```
|
|
|
|
**Solution 3: Check variable delimiter**
|
|
|
|
```html
|
|
<!-- Correct (Handlebars-style) -->
|
|
<h1>Hello {{name}}</h1>
|
|
<p>Your campaign {{campaignName}} is ready.</p>
|
|
|
|
<!-- Wrong -->
|
|
<h1>Hello ${name}</h1> <!-- JavaScript template literal -->
|
|
<p>Your campaign {campaignName} is ready.</p> <!-- Single braces -->
|
|
```
|
|
|
|
**Solution 4: Test template rendering**
|
|
|
|
```bash
|
|
# Test template rendering
|
|
curl -X POST http://localhost:4000/api/test-template \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
-d '{
|
|
"template": "campaign-email",
|
|
"variables": {
|
|
"name": "John",
|
|
"campaignName": "Save the Planet",
|
|
"campaignUrl": "https://example.com/campaigns/123"
|
|
}
|
|
}'
|
|
|
|
# Returns rendered HTML
|
|
```
|
|
|
|
**Solution 5: Use default values**
|
|
|
|
```html
|
|
<!-- In template, provide fallback -->
|
|
<h1>Hello {{name || "Friend"}}</h1>
|
|
```
|
|
|
|
Or in code:
|
|
|
|
```typescript
|
|
const variables = {
|
|
name: user.name || 'Friend',
|
|
campaignName: campaign.name || 'Campaign',
|
|
campaignUrl: campaignUrl || '#'
|
|
};
|
|
```
|
|
|
|
#### Prevention
|
|
|
|
- **Template validation** - Check all variables exist
|
|
- **TypeScript types** - Type template variables
|
|
- **Default values** - Always provide defaults
|
|
- **Testing** - Test all templates with sample data
|
|
|
|
---
|
|
|
|
### Syntax Errors
|
|
|
|
**Severity:** 🟠 High
|
|
|
|
#### Symptoms
|
|
|
|
```
|
|
Error: Parse error in template at line 15
|
|
Error: Unexpected token in template
|
|
```
|
|
|
|
Email fails to send.
|
|
|
|
#### Common Causes
|
|
|
|
1. **Invalid HTML** - Malformed HTML
|
|
2. **Unclosed tags** - Missing closing tags
|
|
3. **Special characters** - Unescaped < > &
|
|
4. **Handlebars syntax** - Invalid {{}} usage
|
|
|
|
#### Solutions
|
|
|
|
**Solution 1: Validate HTML**
|
|
|
|
```bash
|
|
# Use HTML validator
|
|
# Copy template content to https://validator.w3.org/nu/
|
|
|
|
# Or validate locally
|
|
docker compose exec api npx html-validate templates/campaign-email.html
|
|
```
|
|
|
|
**Solution 2: Check common errors**
|
|
|
|
```html
|
|
<!-- Unclosed tag -->
|
|
<div>Content here
|
|
<!-- Should be: -->
|
|
<div>Content here</div>
|
|
|
|
<!-- Unescaped characters -->
|
|
Price: $50 < $100
|
|
<!-- Should be: -->
|
|
Price: $50 < $100
|
|
|
|
<!-- Invalid Handlebars -->
|
|
{{if name}} <!-- No "if" helper by default -->
|
|
<!-- Should be: -->
|
|
{{#if name}}...{{/if}} <!-- Or don't use if -->
|
|
```
|
|
|
|
**Solution 3: Escape HTML**
|
|
|
|
```typescript
|
|
// In email.service.ts
|
|
import handlebars from 'handlebars';
|
|
|
|
// Register escape helper
|
|
handlebars.registerHelper('escape', (str) => {
|
|
return handlebars.escapeExpression(str);
|
|
});
|
|
|
|
// In template
|
|
<p>Message: {{escape message}}</p>
|
|
```
|
|
|
|
**Solution 4: Test template compilation**
|
|
|
|
```typescript
|
|
// Test if template compiles
|
|
import handlebars from 'handlebars';
|
|
import fs from 'fs';
|
|
|
|
const templateSource = fs.readFileSync('templates/campaign-email.html', 'utf8');
|
|
try {
|
|
const template = handlebars.compile(templateSource);
|
|
console.log('Template compiles successfully');
|
|
} catch (error) {
|
|
console.error('Template error:', error.message);
|
|
}
|
|
```
|
|
|
|
#### Prevention
|
|
|
|
- **HTML validation** - Validate before saving
|
|
- **Linting** - Use HTML linter in editor
|
|
- **Simple templates** - Keep templates simple
|
|
- **Testing** - Test rendering before deploying
|
|
|
|
---
|
|
|
|
## Queue Issues
|
|
|
|
### Queue Stuck
|
|
|
|
**Severity:** 🟠 High
|
|
|
|
#### Symptoms
|
|
|
|
Emails queued but not sending. Queue shows jobs but no progress.
|
|
|
|
#### Solutions
|
|
|
|
**Solution 1: Check queue status**
|
|
|
|
```bash
|
|
# View queue stats
|
|
curl http://localhost:4000/api/influence/email-queue/stats \
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|
|
|
# Shows:
|
|
# {
|
|
# "waiting": 50,
|
|
# "active": 0, # Should be > 0 if processing
|
|
# "completed": 1000,
|
|
# "failed": 5
|
|
# }
|
|
```
|
|
|
|
**Solution 2: Check worker is running**
|
|
|
|
```bash
|
|
# Worker should log processing
|
|
docker compose logs api | grep -i "email worker\|processing email"
|
|
|
|
# Should show:
|
|
# Email worker started
|
|
# Processing email job for campaign: abc-123
|
|
```
|
|
|
|
**Solution 3: Restart worker**
|
|
|
|
```bash
|
|
# Restart API (restarts worker)
|
|
docker compose restart api
|
|
|
|
# Check worker started
|
|
docker compose logs api | grep "Email worker started"
|
|
```
|
|
|
|
**Solution 4: Check Redis**
|
|
|
|
```bash
|
|
# Test Redis connection
|
|
docker compose exec redis redis-cli -a YOUR_REDIS_PASSWORD ping
|
|
|
|
# Check queue keys
|
|
docker compose exec redis redis-cli -a YOUR_REDIS_PASSWORD keys "bull:email-queue:*"
|
|
```
|
|
|
|
**Solution 5: Process stuck jobs**
|
|
|
|
```bash
|
|
# Retry failed jobs
|
|
curl -X POST http://localhost:4000/api/influence/email-queue/retry-failed \
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|
|
|
# Clean old jobs
|
|
curl -X POST http://localhost:4000/api/influence/email-queue/clean \
|
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
-d '{"status": "completed", "grace": 86400000}' # Clean completed > 1 day
|
|
```
|
|
|
|
#### Prevention
|
|
|
|
- **Health checks** - Monitor worker health
|
|
- **Auto-restart** - Restart worker if stuck
|
|
- **Alerting** - Alert if queue backed up
|
|
- **Dead letter queue** - Move repeatedly failed jobs
|
|
|
|
---
|
|
|
|
### Jobs Failing
|
|
|
|
**Severity:** 🟠 High
|
|
|
|
#### Symptoms
|
|
|
|
High failed job count. Emails not reaching recipients.
|
|
|
|
#### Solutions
|
|
|
|
**Solution 1: View failed jobs**
|
|
|
|
```bash
|
|
# Get failed job details
|
|
curl http://localhost:4000/api/influence/email-queue/failed \
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|
|
|
# Shows:
|
|
# [
|
|
# {
|
|
# "id": "123",
|
|
# "data": { "to": "user@example.com", "subject": "..." },
|
|
# "failedReason": "SMTP connection failed",
|
|
# "attemptsMade": 3
|
|
# }
|
|
# ]
|
|
```
|
|
|
|
**Solution 2: Check error patterns**
|
|
|
|
```bash
|
|
# Common failure reasons
|
|
docker compose logs api | grep "Email failed" | sort | uniq -c
|
|
|
|
# Example output:
|
|
# 25 Email failed: Invalid email address
|
|
# 10 Email failed: SMTP connection refused
|
|
# 3 Email failed: Recipient mailbox full
|
|
```
|
|
|
|
**Solution 3: Retry with fixes**
|
|
|
|
```bash
|
|
# Fix SMTP config if needed
|
|
# Then retry failed jobs
|
|
curl -X POST http://localhost:4000/api/influence/email-queue/retry-failed \
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|
```
|
|
|
|
**Solution 4: Manual intervention**
|
|
|
|
For repeatedly failing emails:
|
|
|
|
1. Check email address validity
|
|
2. Verify SMTP configuration
|
|
3. Test with different recipient
|
|
4. Check if recipient's mailbox full
|
|
|
|
#### Prevention
|
|
|
|
- **Retry logic** - Auto-retry with exponential backoff
|
|
- **Email validation** - Validate before queuing
|
|
- **Error categorization** - Permanent vs transient failures
|
|
- **Bounce handling** - Handle bounce notifications
|
|
|
|
---
|
|
|
|
## Delivery Issues
|
|
|
|
### Emails Not Arriving
|
|
|
|
**Severity:** 🔴 Critical
|
|
|
|
#### Symptoms
|
|
|
|
Emails sent successfully (no errors) but not received.
|
|
|
|
#### Common Causes
|
|
|
|
1. **Spam folder** - Filtered to spam
|
|
2. **Email delay** - Taking long to deliver
|
|
3. **Email blocking** - Recipient server blocking
|
|
4. **Wrong address** - Typo in email address
|
|
|
|
#### Solutions
|
|
|
|
**Solution 1: Check spam folder**
|
|
|
|
1. Check spam/junk folder
|
|
2. Check promotions tab (Gmail)
|
|
3. Mark as "Not Spam" to whitelist
|
|
|
|
**Solution 2: Check email logs**
|
|
|
|
```bash
|
|
# Verify email was sent
|
|
docker compose logs api | grep "Email sent"
|
|
|
|
# Should show:
|
|
# Email sent to user@example.com: Campaign Email
|
|
```
|
|
|
|
**Solution 3: Use MailHog to test**
|
|
|
|
```bash
|
|
# In .env
|
|
EMAIL_TEST_MODE=true
|
|
|
|
# Restart API
|
|
docker compose restart api
|
|
|
|
# Send test email
|
|
curl -X POST http://localhost:4000/api/test-email \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
-d '{"to": "test@example.com", "subject": "Test", "text": "Test"}'
|
|
|
|
# Check MailHog
|
|
# http://localhost:8025
|
|
|
|
# If appears in MailHog, SMTP working
|
|
# If not appearing in real inbox, delivery issue
|
|
```
|
|
|
|
**Solution 4: Check email headers**
|
|
|
|
In MailHog or received email:
|
|
1. View full headers
|
|
2. Check "Received" path
|
|
3. Look for spam scores
|
|
4. Check SPF/DKIM/DMARC status
|
|
|
|
**Solution 5: Test with different address**
|
|
|
|
```bash
|
|
# Try sending to different email provider
|
|
# Gmail vs Outlook vs Yahoo
|
|
# If some work and others don't, specific provider blocking
|
|
```
|
|
|
|
#### Prevention
|
|
|
|
- **Email authentication** - SPF, DKIM, DMARC
|
|
- **Reputation management** - Maintain good sender reputation
|
|
- **Bounce handling** - Monitor bounces
|
|
- **Testing** - Regular delivery tests
|
|
|
|
---
|
|
|
|
### Marked as Spam
|
|
|
|
**Severity:** 🟠 High
|
|
|
|
#### Symptoms
|
|
|
|
Emails consistently go to spam folder.
|
|
|
|
#### Solutions
|
|
|
|
**Solution 1: Configure SPF**
|
|
|
|
Add TXT record to DNS:
|
|
|
|
```
|
|
v=spf1 include:_spf.google.com ~all
|
|
```
|
|
|
|
Or for SendGrid:
|
|
|
|
```
|
|
v=spf1 include:sendgrid.net ~all
|
|
```
|
|
|
|
**Solution 2: Configure DKIM**
|
|
|
|
1. Generate DKIM keys (via email provider)
|
|
2. Add DKIM TXT record to DNS
|
|
3. Enable DKIM signing in SMTP settings
|
|
|
|
**Solution 3: Configure DMARC**
|
|
|
|
Add TXT record to DNS:
|
|
|
|
```
|
|
v=DMARC1; p=quarantine; rua=mailto:dmarc@yourdomain.com
|
|
```
|
|
|
|
**Solution 4: Improve email content**
|
|
|
|
- Use plain text version alongside HTML
|
|
- Avoid spam trigger words ("FREE", "CLICK HERE", "ACT NOW")
|
|
- Proper from/reply-to addresses
|
|
- Unsubscribe link
|
|
- Physical address in footer
|
|
|
|
**Solution 5: Warm up IP**
|
|
|
|
If using dedicated IP:
|
|
1. Start with low volume
|
|
2. Gradually increase over weeks
|
|
3. Monitor reputation scores
|
|
|
|
#### Prevention
|
|
|
|
- **Email authentication** - SPF, DKIM, DMARC mandatory
|
|
- **Content quality** - Professional, non-spammy content
|
|
- **Reputation monitoring** - Monitor sender scores
|
|
- **Engagement** - High engagement = good reputation
|
|
|
|
---
|
|
|
|
### Bounce Errors
|
|
|
|
**Severity:** 🟡 Medium
|
|
|
|
#### Symptoms
|
|
|
|
```
|
|
Email bounced: user@example.com
|
|
554 Recipient address rejected: User unknown
|
|
```
|
|
|
|
#### Common Causes
|
|
|
|
1. **Invalid address** - Email doesn't exist
|
|
2. **Full mailbox** - Recipient mailbox full
|
|
3. **Temporary failure** - Server temporarily unavailable
|
|
4. **Blocked sender** - Your domain/IP blocked
|
|
|
|
#### Solutions
|
|
|
|
**Solution 1: Categorize bounces**
|
|
|
|
Hard bounces (permanent):
|
|
- User unknown
|
|
- Domain doesn't exist
|
|
- Invalid address format
|
|
|
|
Soft bounces (temporary):
|
|
- Mailbox full
|
|
- Server temporarily unavailable
|
|
- Message too large
|
|
|
|
**Solution 2: Handle hard bounces**
|
|
|
|
```bash
|
|
# Remove hard bounce addresses
|
|
docker compose exec v2-postgres psql -U changemaker -d changemaker_v2 \
|
|
-c "UPDATE \"User\" SET \"emailBounced\" = true
|
|
WHERE email = 'bounced@example.com';"
|
|
|
|
# Don't send to bounced addresses
|
|
```
|
|
|
|
**Solution 3: Retry soft bounces**
|
|
|
|
```bash
|
|
# Retry soft bounces after delay
|
|
curl -X POST http://localhost:4000/api/influence/email-queue/retry-failed \
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|
```
|
|
|
|
**Solution 4: Validate emails before sending**
|
|
|
|
```typescript
|
|
import validator from 'validator';
|
|
|
|
const isValidEmail = validator.isEmail(email);
|
|
if (!isValidEmail) {
|
|
throw new Error('Invalid email address');
|
|
}
|
|
```
|
|
|
|
#### Prevention
|
|
|
|
- **Email validation** - Validate before saving
|
|
- **Bounce tracking** - Track bounces per address
|
|
- **Automatic removal** - Don't send to bounced addresses
|
|
- **Double opt-in** - Confirm email addresses work
|
|
|
|
---
|
|
|
|
## Listmonk Integration
|
|
|
|
### API Connection Failed
|
|
|
|
**Severity:** 🟠 High
|
|
|
|
#### Symptoms
|
|
|
|
```
|
|
Error: Failed to connect to Listmonk API
|
|
Error: ECONNREFUSED localhost:9001
|
|
```
|
|
|
|
#### Solutions
|
|
|
|
**Solution 1: Check Listmonk is running**
|
|
|
|
```bash
|
|
docker compose ps listmonk
|
|
|
|
# Should show "Up"
|
|
# If not:
|
|
docker compose up -d listmonk
|
|
```
|
|
|
|
**Solution 2: Verify API credentials**
|
|
|
|
```bash
|
|
# Check .env
|
|
cat .env | grep LISTMONK_
|
|
|
|
# Required:
|
|
LISTMONK_URL=http://listmonk:9001
|
|
LISTMONK_ADMIN_USER=admin
|
|
LISTMONK_ADMIN_PASSWORD=password
|
|
```
|
|
|
|
**Solution 3: Test API connection**
|
|
|
|
```bash
|
|
# From API container
|
|
docker compose exec api curl -u admin:password http://listmonk:9001/api/health
|
|
|
|
# Should return:
|
|
# {"data": "OK"}
|
|
```
|
|
|
|
**Solution 4: Check Docker network**
|
|
|
|
```bash
|
|
# Both on same network?
|
|
docker inspect changemaker-lite-api-1 | grep NetworkMode
|
|
docker inspect changemaker-lite-listmonk-1 | grep NetworkMode
|
|
|
|
# Should both show "changemaker-lite"
|
|
```
|
|
|
|
#### Prevention
|
|
|
|
- **Health checks** - Verify Listmonk health on API startup
|
|
- **Proper credentials** - Use API user (not web admin)
|
|
- **Network config** - Ensure same Docker network
|
|
- **Error handling** - Graceful degradation if Listmonk down
|
|
|
|
---
|
|
|
|
### Sync Errors
|
|
|
|
**Severity:** 🟡 Medium
|
|
|
|
#### Symptoms
|
|
|
|
```
|
|
Error: Failed to sync subscribers to Listmonk
|
|
Error: 400 Bad Request: Invalid email format
|
|
```
|
|
|
|
#### Solutions
|
|
|
|
**Solution 1: Check sync status**
|
|
|
|
Navigate to `/app/listmonk`:
|
|
|
|
- View sync statistics
|
|
- See last sync time
|
|
- Check error count
|
|
|
|
**Solution 2: View sync logs**
|
|
|
|
```bash
|
|
docker compose logs api | grep -i "listmonk\|sync"
|
|
|
|
# Shows:
|
|
# Syncing 150 participants to Listmonk
|
|
# Created list: Campaign Participants
|
|
# Added 145 subscribers, 5 failed
|
|
```
|
|
|
|
**Solution 3: Manual sync**
|
|
|
|
```bash
|
|
# Trigger manual sync
|
|
curl -X POST http://localhost:4000/api/listmonk/sync \
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|
```
|
|
|
|
**Solution 4: Check subscriber data**
|
|
|
|
```bash
|
|
# View failed subscribers
|
|
docker compose logs api | grep "Failed to add subscriber"
|
|
|
|
# Common issues:
|
|
# - Invalid email format
|
|
# - Email already exists
|
|
# - Missing required fields
|
|
```
|
|
|
|
#### Prevention
|
|
|
|
- **Data validation** - Validate before sync
|
|
- **Duplicate handling** - Handle existing subscribers
|
|
- **Error logging** - Log sync errors
|
|
- **Regular syncs** - Automated periodic syncs
|
|
|
|
---
|
|
|
|
## Performance Issues
|
|
|
|
### Slow Email Sending
|
|
|
|
**Severity:** 🟡 Medium
|
|
|
|
#### Symptoms
|
|
|
|
Sending emails takes several seconds each. Bulk sends very slow.
|
|
|
|
#### Solutions
|
|
|
|
**Solution 1: Use queue system**
|
|
|
|
```bash
|
|
# Don't send synchronously
|
|
# Queue emails instead
|
|
curl -X POST http://localhost:4000/api/influence/campaigns/CAMPAIGN_ID/send-bulk \
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|
|
|
# Processes in background via queue
|
|
```
|
|
|
|
**Solution 2: Increase worker concurrency**
|
|
|
|
In `api/src/services/email-queue.service.ts`:
|
|
|
|
```typescript
|
|
const worker = new Worker('email-queue', processor, {
|
|
concurrency: 5, // Process 5 emails at a time (default: 1)
|
|
limiter: {
|
|
max: 50, // Max 50 emails per second
|
|
duration: 1000
|
|
}
|
|
});
|
|
```
|
|
|
|
**Solution 3: Use batch sending**
|
|
|
|
For transactional email services:
|
|
|
|
```typescript
|
|
// Some SMTP services support batch sending
|
|
// Send 100 emails in single API call instead of 100 separate calls
|
|
```
|
|
|
|
**Solution 4: Check SMTP performance**
|
|
|
|
```bash
|
|
# Test SMTP connection speed
|
|
time curl -X POST http://localhost:4000/api/test-email \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
-d '{"to": "test@example.com", "subject": "Test", "text": "Test"}'
|
|
|
|
# Should complete in < 2 seconds
|
|
# If > 5 seconds, SMTP server slow
|
|
```
|
|
|
|
**Solution 5: Use email service**
|
|
|
|
For high volume, use transactional email service:
|
|
- SendGrid
|
|
- Mailgun
|
|
- Amazon SES
|
|
- Postmark
|
|
|
|
Faster and more reliable than SMTP.
|
|
|
|
#### Prevention
|
|
|
|
- **Queue system** - Never send synchronously
|
|
- **Worker concurrency** - Process multiple at once
|
|
- **Email service** - Use dedicated email service
|
|
- **Rate limiting** - Respect provider limits
|
|
|
|
---
|
|
|
|
### Queue Backlog
|
|
|
|
**Severity:** 🟡 Medium
|
|
|
|
#### Symptoms
|
|
|
|
Thousands of emails waiting in queue. Taking hours to process.
|
|
|
|
#### Solutions
|
|
|
|
**Solution 1: Increase worker count**
|
|
|
|
Start multiple API instances:
|
|
|
|
```yaml
|
|
# In docker-compose.yml
|
|
api:
|
|
deploy:
|
|
replicas: 3 # 3 API instances
|
|
```
|
|
|
|
Each instance runs its own worker.
|
|
|
|
**Solution 2: Increase concurrency**
|
|
|
|
See "Slow Email Sending" section above.
|
|
|
|
**Solution 3: Pause new emails**
|
|
|
|
```bash
|
|
# Pause queue
|
|
curl -X POST http://localhost:4000/api/influence/email-queue/pause \
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|
|
|
# Process backlog
|
|
# Resume when caught up
|
|
curl -X POST http://localhost:4000/api/influence/email-queue/resume \
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|
```
|
|
|
|
**Solution 4: Clean old jobs**
|
|
|
|
```bash
|
|
# Remove completed jobs
|
|
curl -X POST http://localhost:4000/api/influence/email-queue/clean \
|
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
-d '{"status": "completed", "grace": 3600000}' # Older than 1 hour
|
|
```
|
|
|
|
#### Prevention
|
|
|
|
- **Monitor queue size** - Alert when > 1000 waiting
|
|
- **Rate limiting** - Don't queue faster than can process
|
|
- **Capacity planning** - Size workers for expected load
|
|
- **Cleanup jobs** - Regular cleanup of old jobs
|
|
|
|
---
|
|
|
|
## Useful Commands
|
|
|
|
### Testing Email
|
|
|
|
```bash
|
|
# Send test email
|
|
curl -X POST http://localhost:4000/api/test-email \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
-d '{
|
|
"to": "test@example.com",
|
|
"subject": "Test Email",
|
|
"text": "This is a test email",
|
|
"html": "<h1>Test Email</h1><p>This is a test email</p>"
|
|
}'
|
|
|
|
# Test with template
|
|
curl -X POST http://localhost:4000/api/test-template-email \
|
|
-H "Content-Type: application/json" \
|
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
-d '{
|
|
"to": "test@example.com",
|
|
"subject": "Test Template",
|
|
"template": "campaign-email",
|
|
"variables": {
|
|
"name": "Test User",
|
|
"campaignName": "Test Campaign"
|
|
}
|
|
}'
|
|
```
|
|
|
|
### Queue Management
|
|
|
|
```bash
|
|
# Get queue stats
|
|
curl http://localhost:4000/api/influence/email-queue/stats \
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|
|
|
# Pause queue
|
|
curl -X POST http://localhost:4000/api/influence/email-queue/pause \
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|
|
|
# Resume queue
|
|
curl -X POST http://localhost:4000/api/influence/email-queue/resume \
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|
|
|
# Retry failed
|
|
curl -X POST http://localhost:4000/api/influence/email-queue/retry-failed \
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|
|
|
# Clean completed
|
|
curl -X POST http://localhost:4000/api/influence/email-queue/clean \
|
|
-H "Authorization: Bearer YOUR_TOKEN" \
|
|
-d '{"status": "completed", "grace": 86400000}'
|
|
```
|
|
|
|
### Listmonk Operations
|
|
|
|
```bash
|
|
# Test Listmonk connection
|
|
curl -u admin:password http://localhost:9001/api/health
|
|
|
|
# Get lists
|
|
curl -u admin:password http://localhost:9001/api/lists
|
|
|
|
# Sync subscribers
|
|
curl -X POST http://localhost:4000/api/listmonk/sync \
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|
|
|
# Get sync status
|
|
curl http://localhost:4000/api/listmonk/status \
|
|
-H "Authorization: Bearer YOUR_TOKEN"
|
|
```
|
|
|
|
---
|
|
|
|
## Related Documentation
|
|
|
|
### Email Documentation
|
|
- [Email Issues](email-issues.md) - This guide
|
|
- [Email Templates Feature](../features/influence/email-templates.md) - Template management
|
|
- [Email Queue](../features/influence/email-queue.md) - Queue monitoring
|
|
|
|
### Other Troubleshooting
|
|
- [Common Errors](common-errors.md) - General errors
|
|
- [Performance Optimization](performance-optimization.md) - Email performance
|
|
|
|
### External Resources
|
|
- [Nodemailer Documentation](https://nodemailer.com/)
|
|
- [BullMQ Documentation](https://docs.bullmq.io/)
|
|
- [Listmonk Documentation](https://listmonk.app/docs/)
|
|
- [Gmail SMTP Settings](https://support.google.com/a/answer/176600)
|
|
- [SPF/DKIM/DMARC Guide](https://www.dmarcanalyzer.com/)
|
|
|
|
---
|
|
|
|
**Last Updated:** February 2026
|
|
**Version:** V2.0
|
|
**Status:** Complete
|