TGM Expert includes a comprehensive email system with multi-provider support, templating, and queue management for reliable email delivery.
Table of Contents¶
- Overview
- Multi-Tenancy Context
- Email Providers
- Configuration
- Email Templates
- Sending Emails
- Admin API
- Queue Management
- Best Practices
Overview¶
Key Features¶
- Multi-Provider Support - SendGrid, AWS SES, Mailgun, Postmark, SMTP, and more
- Automatic Failover - Falls back to secondary providers on failure
- Template Engine - FreeMarker-based email templates
- Queue System - Asynchronous email processing with retries
- Tracking - Email status tracking and delivery reports
- Rate Limiting - Provider-specific rate limit handling
- Multi-Tenant Support - Per-company email provider configuration
Multi-Tenancy Context¶
Email configuration operates within TGM's multi-tenant architecture:
┌─────────────────────────────────────────────────────────────────┐
│ Platform Level │
│ System Default Email Provider (fallback) │
└─────────────────────────────────────────────────────────────────┘
│
┌──────────────────────┼──────────────────────┐
▼ ▼ ▼
┌───────────────┐ ┌───────────────┐ ┌───────────────┐
│ Client A │ │ Client B │ │ Client C │
│ Database │ │ Database │ │ Database │
├───────────────┤ ├───────────────┤ ├───────────────┤
│ Company │ │ Company │ │ Company │
│ Email Config │ │ Email Config │ │ Email Config │
│ (SendGrid) │ │ (AWS SES) │ │ (default) │
└───────────────┘ └───────────────┘ └───────────────┘
Key Points¶
- Email providers are per-company - Each company within a client can have its own provider
- Fallback to system default - If company provider fails, system default is used
- Provider configs stored per-client - Email provider configurations are in each client's database
API Request Headers¶
curl -X POST "http://localhost:1337/api/admin/email-providers" \
-H "Authorization: Bearer $JWT" \
-H "Content-Type: application/json" \
-H "X-Client-ID: acme-corp" \
-d '{ ... }'
Architecture¶
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Service │────>│ Email │────>│ Provider │
│ (trigger) │ │ Queue │ │ (SendGrid) │
└─────────────┘ └─────────────┘ └─────────────┘
│ │
│ On failure │
▼ │
┌─────────────┐ │
│ Fallback │◀──────────┘
│ Provider │
└─────────────┘
Email Providers¶
Supported Providers¶
| Provider | Type | Features |
|---|---|---|
| SendGrid | API | High deliverability, analytics |
| AWS SES | API | Cost-effective at scale |
| Mailgun | API | Developer-friendly |
| Postmark | API | Transactional focused |
| SparkPost | API | Enterprise features |
| Mailjet | API | Marketing + transactional |
| Brevo | API | CRM integration |
| Resend | API | Modern API |
| SMTP | Protocol | Universal compatibility |
| Console | Debug | Development/testing only |
Provider Priority¶
- Primary Provider - Configured as default for a company
- System Default - Fallback when company provider fails
- Console (Dev) - Logs emails in development mode
Configuration¶
Provider Configuration via Admin API¶
curl -X POST https://api.tgm-expert.com/api/admin/email-providers \
-H "Authorization: Bearer $JWT_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "Company SendGrid",
"providerType": "SENDGRID",
"companyId": 1,
"apiKey": "SG.xxxxxxxxxxxx",
"fromEmail": "noreply@company.com",
"fromName": "TGM Expert",
"replyToEmail": "support@company.com",
"isActive": true
}'
Provider Types and Required Fields¶
SendGrid¶
{
"providerType": "SENDGRID",
"apiKey": "SG.xxxxxxxxxxxxx"
}
AWS SES¶
{
"providerType": "AWS_SES",
"awsAccessKeyId": "AKIAIOSFODNN7EXAMPLE",
"awsSecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY",
"awsRegion": "us-east-1"
}
Mailgun¶
{
"providerType": "MAILGUN",
"apiKey": "key-xxxxxxxxxxxxxxxx",
"domain": "mg.company.com",
"smtpHost": "smtp.mailgun.org"
}
SMTP (Generic)¶
{
"providerType": "SMTP",
"smtpHost": "smtp.gmail.com",
"smtpPort": 587,
"smtpUsername": "user@gmail.com",
"smtpPassword": "app-password",
"smtpStartTlsEnabled": true
}
Environment Variables¶
For system-wide defaults, configure via environment:
# Default email settings
app.email.default-from=noreply@tgm-expert.com
app.email.default-from-name=TGM Expert
app.email.enabled=true
# SendGrid (default provider)
spring.sendgrid.api-key=${SENDGRID_API_KEY}
# AWS SES (fallback)
aws.ses.access-key=${AWS_ACCESS_KEY}
aws.ses.secret-key=${AWS_SECRET_KEY}
aws.ses.region=us-east-1
Email Templates¶
Template Engine¶
TGM Expert uses FreeMarker for email templates. Templates are located in src/main/resources/templates/email/.
Base Layout¶
All emails use a consistent base layout:
<!-- base-layout.ftl -->
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>${subject}</title>
</head>
<body>
<div class="container">
<div class="header">
<img src="${logoUrl}" alt="TGM Expert">
</div>
<div class="content">
<#nested>
</div>
<div class="footer">
<p>The first intelligence platform built for hydro and energy assets</p>
<p>© ${.now?string('yyyy')} TGM Expert. All rights reserved.</p>
</div>
</div>
</body>
</html>
Available Templates¶
| Template | Purpose | Variables |
|---|---|---|
welcome.ftl |
New user welcome | user, loginUrl |
password-reset.ftl |
Password reset | user, resetUrl, expiresIn |
email-verification.ftl |
Verify email | user, verificationUrl |
password-changed.ftl |
Password change confirmation | user |
login-alert.ftl |
New device login | user, device, location, time |
two-factor-code.ftl |
2FA code | user, code, expiresIn |
invitation.ftl |
User invitation | inviter, invitationUrl |
inspection-assigned.ftl |
Inspection assignment | user, inspection |
inspection-reminder.ftl |
Inspection reminder | user, inspection, dueDate |
work-order-created.ftl |
Work order notification | user, workOrder |
alert-triggered.ftl |
Alert notification | user, alert |
maintenance-due.ftl |
Maintenance reminder | user, maintenancePlan |
Creating Custom Templates¶
<!-- templates/email/custom-notification.ftl -->
<#import "base-layout.ftl" as layout>
<@layout.base subject="Custom Notification">
<h1>Hello ${user.firstName}!</h1>
<p>This is a custom notification about ${entityType}.</p>
<div class="details">
<p><strong>Details:</strong></p>
<ul>
<#list details as detail>
<li>${detail}</li>
</#list>
</ul>
</div>
<a href="${actionUrl}" class="button">Take Action</a>
</@layout.base>
Template Variables¶
Common variables available in all templates:
| Variable | Type | Description |
|---|---|---|
user |
Object | Current user |
user.firstName |
String | User's first name |
user.lastName |
String | User's last name |
user.email |
String | User's email |
company |
Object | User's company |
appName |
String | Application name |
appUrl |
String | Application URL |
logoUrl |
String | Logo URL |
supportEmail |
String | Support email |
currentYear |
Number | Current year |
Sending Emails¶
Using EmailService (Programmatic)¶
@Service
@RequiredArgsConstructor
public class NotificationService {
private final EmailService emailService;
public void notifyUser(User user, Inspection inspection) {
Map<String, Object> variables = new HashMap<>();
variables.put("user", user);
variables.put("inspection", inspection);
emailService.sendTemplateEmail(
user.getEmail(),
"Inspection Assigned",
"inspection-assigned",
variables
);
}
}
Email Service Methods¶
// Simple text email
emailService.sendSimpleEmail(
"user@example.com",
"Subject",
"Plain text body"
);
// HTML email
emailService.sendHtmlEmail(
"user@example.com",
"Subject",
"<h1>HTML Content</h1>"
);
// Template email
emailService.sendTemplateEmail(
"user@example.com",
"Subject",
"template-name",
Map.of("var1", value1, "var2", value2)
);
// Email with attachments
emailService.sendEmailWithAttachment(
"user@example.com",
"Subject",
"Body",
attachment
);
// Bulk email
emailService.sendBulkEmail(
List.of("user1@example.com", "user2@example.com"),
"Subject",
"Body"
);
Company-Specific Sending¶
// Send using company's configured provider
emailService.sendForCompany(
companyId,
"user@example.com",
"Subject",
"Body"
);
Admin API¶
Provider Management¶
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/admin/email-providers/types |
List provider types |
| GET | /api/admin/email-providers/company/{id} |
List company configs |
| GET | /api/admin/email-providers/{id} |
Get config details |
| POST | /api/admin/email-providers |
Create config |
| PUT | /api/admin/email-providers/{id} |
Update config |
| DELETE | /api/admin/email-providers/{id} |
Delete config |
| POST | /api/admin/email-providers/{id}/activate |
Activate config |
| POST | /api/admin/email-providers/{id}/deactivate |
Deactivate config |
| POST | /api/admin/email-providers/{id}/test |
Test connection |
| POST | /api/admin/email-providers/{id}/test-email |
Send test email |
Testing Email Configuration¶
# Test provider connection
curl -X POST "https://api.tgm-expert.com/api/admin/email-providers/1/test" \
-H "Authorization: Bearer $JWT_TOKEN"
# Send test email
curl -X POST "https://api.tgm-expert.com/api/admin/email-providers/1/test-email?to=test@example.com" \
-H "Authorization: Bearer $JWT_TOKEN"
Email Administration¶
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/admin/email/templates |
List templates |
| GET | /api/admin/email/templates/{name}/preview |
Preview template |
| POST | /api/admin/email/send |
Send ad-hoc email |
| GET | /api/admin/email/stats |
Email statistics |
Queue Management¶
How the Queue Works¶
- Email is added to queue with
PENDINGstatus - Background worker picks up pending emails
- Email is sent via configured provider
- On success: marked as
SENT - On failure: retried with exponential backoff
- After max retries: marked as
FAILED
Queue Status¶
curl -X GET "https://api.tgm-expert.com/api/admin/queue/email/stats" \
-H "Authorization: Bearer $JWT_TOKEN"
Response:
{
"data": {
"pending": 5,
"processing": 2,
"sent": 1523,
"failed": 3,
"averageDeliveryTime": 1.2
}
}
Retry Configuration¶
| Setting | Default | Description |
|---|---|---|
| Max retries | 3 | Maximum retry attempts |
| Initial delay | 1 minute | First retry delay |
| Max delay | 1 hour | Maximum retry delay |
| Backoff multiplier | 2 | Exponential backoff factor |
Manual Retry¶
curl -X POST "https://api.tgm-expert.com/api/admin/queue/email/{id}/retry" \
-H "Authorization: Bearer $JWT_TOKEN"
Best Practices¶
1. Use Templates¶
Always use templates for consistent branding and easier maintenance.
2. Configure Fallback Providers¶
Set up at least two providers for redundancy.
Primary: SendGrid
Fallback: AWS SES
3. Monitor Delivery Rates¶
Regularly check email statistics and address bounces/complaints.
4. Validate Email Addresses¶
Validate email addresses before sending to reduce bounces.
5. Use Appropriate From Addresses¶
noreply@for automated notificationssupport@for support-related emails- Configure SPF, DKIM, and DMARC for deliverability
6. Handle Unsubscribes¶
Include unsubscribe links in marketing emails and honor opt-outs.
7. Rate Limit Bulk Sends¶
When sending bulk emails, respect provider rate limits:
| Provider | Rate Limit |
|---|---|
| SendGrid | 100/second |
| AWS SES | 14/second (default) |
| Mailgun | 300/minute |
Troubleshooting¶
Emails Not Sending¶
- Check if email is enabled:
app.email.enabled=true - Verify provider configuration:
GET /api/admin/email-providers/{id} - Test provider connection:
POST /api/admin/email-providers/{id}/test - Check queue status:
GET /api/admin/queue/email/stats - Review application logs
High Bounce Rate¶
- Verify email addresses before sending
- Check for typos in recipient addresses
- Remove invalid addresses from lists
- Monitor blacklist status
Emails Going to Spam¶
- Configure SPF record for your domain
- Set up DKIM signing
- Configure DMARC policy
- Use consistent from addresses
- Avoid spam trigger words
Provider Connection Errors¶
- Verify API keys/credentials
- Check IP whitelist requirements
- Ensure network connectivity
- Review provider status page
Email Templates Reference¶
Variable Types¶
| Type | Example | Access |
|---|---|---|
| String | ${user.firstName} |
Direct |
| Number | ${count} |
Direct |
| Date | ${date?string('yyyy-MM-dd')} |
Formatted |
| Boolean | <#if active>...</#if> |
Conditional |
| List | <#list items as item>...</#list> |
Loop |
| Object | ${user.company.name} |
Nested |
Common Directives¶
<!-- Conditionals -->
<#if condition>
...
<#elseif otherCondition>
...
<#else>
...
</#if>
<!-- Loops -->
<#list items as item>
${item.name}
</#list>
<!-- Default values -->
${variable!"default value"}
<!-- Null checks -->
<#if variable??>
${variable}
</#if>
<!-- Date formatting -->
${date?string('MMMM dd, yyyy')}
<!-- Number formatting -->
${number?string('#,##0.00')}