Overview
Plunk is a simple email service for sending transactional emails. The integration provides a straightforward way to send emails from your Go application without managing SMTP servers.
This integration is optional. Only configure Plunk if you need to send emails from your application.
Prerequisites
- A Plunk account - Sign up at useplunk.com
- API key from your Plunk dashboard
- Verified sender email domain (required by Plunk)
Configuration
Add your Plunk API key to your .env file:
USE_PLUNK=your_plunk_api_key_here
The API key is accessed via the configs package:
configs.GetPlunkKey() // Returns USE_PLUNK value
Available Functions
Send Email
func SendEmailWithPlunk(payload, recipient, title, fromEmail string) error
Sends an email using the Plunk API.
Parameters:
payload - Email body (plain text or HTML)
recipient - Recipient email address
title - Email subject line
fromEmail - Reply-to email address
Returns:
error - nil if successful, error if failed
Example:
import "backend/integrations"
err := integrations.SendEmailWithPlunk(
"<h1>Welcome!</h1><p>Thanks for signing up.</p>",
"[email protected]",
"Welcome to Our App",
"[email protected]",
)
if err != nil {
log.Printf("Failed to send email: %v", err)
return err
}
Use Cases
1. Welcome Emails
Send onboarding emails to new users:
func RegisterUser(c echo.Context) error {
// ... user registration logic ...
// Send welcome email
emailBody := fmt.Sprintf(`
<h1>Welcome to Our App, %s!</h1>
<p>Thanks for joining us. Here are some next steps:</p>
<ul>
<li>Complete your profile</li>
<li>Explore our features</li>
<li>Connect with others</li>
</ul>
<p>If you have questions, just reply to this email.</p>
`, user.Name)
err := integrations.SendEmailWithPlunk(
emailBody,
user.Email,
"Welcome to Our App!",
"[email protected]",
)
if err != nil {
log.Printf("Failed to send welcome email: %v", err)
// Don't fail registration if email fails
}
return c.JSON(http.StatusCreated, user)
}
2. Password Reset
Send password reset links:
func RequestPasswordReset(c echo.Context) error {
email := c.FormValue("email")
// Generate reset token
resetToken := generateResetToken(email)
resetLink := fmt.Sprintf("https://yourdomain.com/reset-password?token=%s", resetToken)
// Send reset email
emailBody := fmt.Sprintf(`
<h2>Password Reset Request</h2>
<p>Click the link below to reset your password:</p>
<p><a href="%s">Reset Password</a></p>
<p>This link expires in 1 hour.</p>
<p>If you didn't request this, please ignore this email.</p>
`, resetLink)
err := integrations.SendEmailWithPlunk(
emailBody,
email,
"Reset Your Password",
"[email protected]",
)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to send reset email",
})
}
return c.JSON(http.StatusOK, map[string]string{
"message": "Reset email sent",
})
}
3. Email Verification
Send verification codes:
func SendVerificationEmail(userEmail, userName, verificationCode string) error {
emailBody := fmt.Sprintf(`
<h1>Verify Your Email</h1>
<p>Hi %s,</p>
<p>Your verification code is:</p>
<h2 style="background: #f4f4f4; padding: 20px; text-align: center; font-size: 32px; letter-spacing: 5px;">%s</h2>
<p>Enter this code to verify your email address.</p>
<p>This code expires in 15 minutes.</p>
`, userName, verificationCode)
return integrations.SendEmailWithPlunk(
emailBody,
userEmail,
"Verify Your Email Address",
"[email protected]",
)
}
Built-in OTP Verification Helper
The scaffold includes a pre-built helper in utils/emails.go for sending OTP verification emails:
// From utils/emails.go
func SendOtpVerificationEmail(code, email, userId string) error {
payload := fmt.Sprintf(
"Hello, \n\nYour OTP code is %s. \n\nPlease use this code to verify your email address. \n\nThank you.",
code,
)
err := integrations.SendEmailWithPlunk(
payload,
email,
"Welcome to My Project",
"[email protected]",
)
return err
}
Usage:
import "backend/utils"
func SendVerificationCode(c echo.Context) error {
email := c.FormValue("email")
userId := c.FormValue("user_id")
// Generate OTP code
code := generateOTP() // e.g., "123456"
// Send via helper
err := utils.SendOtpVerificationEmail(code, email, userId)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to send verification email",
})
}
return c.JSON(http.StatusOK, map[string]string{
"message": "Verification code sent",
})
}
The helper uses plain text format. For HTML emails with better styling, use the HTML example above or customize the helper function.
4. Transaction Receipts
Send payment confirmations:
func SendPaymentReceipt(order Order, user User) error {
emailBody := fmt.Sprintf(`
<h1>Payment Received</h1>
<p>Hi %s,</p>
<p>We've received your payment of <strong>%d RWF</strong>.</p>
<h3>Order Details:</h3>
<ul>
<li>Order ID: %s</li>
<li>Amount: %d RWF</li>
<li>Date: %s</li>
<li>Status: Completed</li>
</ul>
<p>Thank you for your business!</p>
`, user.Name, order.Amount, order.ID, order.Amount, order.CreatedAt.Format("Jan 2, 2006"))
return integrations.SendEmailWithPlunk(
emailBody,
user.Email,
fmt.Sprintf("Receipt for Order %s", order.ID),
"[email protected]",
)
}
5. Notification Emails
Send alerts and updates:
func NotifyAdminOfError(errorMsg string, context string) {
emailBody := fmt.Sprintf(`
<h2>⚠️ Application Error</h2>
<p><strong>Context:</strong> %s</p>
<p><strong>Error:</strong></p>
<pre style="background: #f4f4f4; padding: 15px; border-radius: 5px;">%s</pre>
<p><strong>Time:</strong> %s</p>
`, context, errorMsg, time.Now().Format(time.RFC3339))
err := integrations.SendEmailWithPlunk(
emailBody,
"[email protected]",
"Application Error Alert",
"[email protected]",
)
if err != nil {
log.Printf("Failed to send error notification: %v", err)
}
}
HTML Email Templates
Basic Template Structure
const emailTemplate = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #4F46E5; color: white; padding: 20px; text-align: center; }
.content { padding: 20px; background: #f9f9f9; }
.button { display: inline-block; padding: 12px 30px; background: #4F46E5; color: white; text-decoration: none; border-radius: 5px; }
.footer { text-align: center; padding: 20px; font-size: 12px; color: #666; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>%s</h1>
</div>
<div class="content">
%s
</div>
<div class="footer">
<p>© 2026 Your Company. All rights reserved.</p>
<p>You're receiving this email because you signed up for our service.</p>
</div>
</div>
</body>
</html>
`
func SendStyledEmail(recipient, subject, heading, body string) error {
htmlContent := fmt.Sprintf(emailTemplate, heading, body)
return integrations.SendEmailWithPlunk(
htmlContent,
recipient,
subject,
"[email protected]",
)
}
API Request Details
The function sends a POST request to Plunk’s API:
Endpoint: https://api.useplunk.com/v1/send
Headers:
Content-Type: application/json
Accept: application/json
Authorization: Bearer YOUR_API_KEY
Request Body:
Error Handling
The function returns errors that should be handled appropriately:
err := integrations.SendEmailWithPlunk(body, recipient, subject, from)
if err != nil {
// Log the error
log.Printf("Email send failed: %v", err)
// Decide how to handle
// Option 1: Fail the request
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to send email",
})
// Option 2: Continue anyway (for non-critical emails)
log.Printf("Continuing despite email failure")
}
Common error scenarios:
- Invalid API key: Check your
.env configuration
- Invalid recipient: Validate email addresses before sending
- Rate limiting: Plunk has sending limits based on your plan
- Network errors: API unreachable
Email Queue (Recommended)
For production applications, consider using a queue for emails:
type EmailJob struct {
Body string
Recipient string
Subject string
From string
}
var emailQueue = make(chan EmailJob, 100)
func StartEmailWorker() {
go func() {
for job := range emailQueue {
err := integrations.SendEmailWithPlunk(
job.Body,
job.Recipient,
job.Subject,
job.From,
)
if err != nil {
log.Printf("Failed to send email to %s: %v", job.Recipient, err)
// Could retry or move to dead letter queue
} else {
log.Printf("Email sent to %s", job.Recipient)
}
// Rate limiting: wait between sends
time.Sleep(100 * time.Millisecond)
}
}()
}
func QueueEmail(body, recipient, subject, from string) {
emailQueue <- EmailJob{
Body: body,
Recipient: recipient,
Subject: subject,
From: from,
}
}
Usage:
func main() {
// Start email worker
StartEmailWorker()
// ... rest of app setup ...
}
func SendWelcomeEmail(user User) {
// Queue the email instead of sending directly
QueueEmail(
"<h1>Welcome!</h1>",
user.Email,
"Welcome to Our App",
"[email protected]",
)
}
Testing
Test your email integration:
func TestEmailIntegration(c echo.Context) error {
testEmail := c.QueryParam("email")
if testEmail == "" {
testEmail = "[email protected]"
}
err := integrations.SendEmailWithPlunk(
"<h1>Test Email</h1><p>This is a test from Go React Scaffold.</p>",
testEmail,
"Test Email",
"[email protected]",
)
if err != nil {
return c.JSON(http.StatusInternalServerError, map[string]string{
"error": "Failed to send test email",
"details": err.Error(),
})
}
return c.JSON(http.StatusOK, map[string]string{
"message": "Test email sent successfully",
"to": testEmail,
})
}
Best Practices
- Validate email addresses: Use a validation library before sending
- Use templates: Create reusable email templates
- Handle failures gracefully: Don’t fail user actions if email fails
- Queue emails: Don’t send emails synchronously in request handlers
- Test your emails: Send test emails to yourself first
- Respect unsubscribes: Maintain an unsubscribe list
- Monitor deliverability: Track bounces and complaints
- Use descriptive subjects: Clear subject lines improve open rates
- Include plain text: Some email clients prefer plain text
- Add unsubscribe links: Required for marketing emails
HTML Email Best Practices
- Use inline CSS (not all email clients support
<style> tags)
- Keep width under 600px
- Use tables for layout (more reliable than divs)
- Test in multiple email clients
- Include alt text for images
- Use web-safe fonts
- Provide a plain-text fallback
Rate Limits
Plunk has rate limits based on your plan:
- Free tier: 3,000 emails/month
- Paid plans: Higher limits
Monitor your usage in the Plunk dashboard.
Security Considerations
Never include sensitive data like passwords or full credit card numbers in emails.
- Store API key securely in environment variables
- Never commit
.env file
- Validate and sanitize email content to prevent injection
- Use HTTPS for any links in emails
- Don’t expose user data in email URLs
- Implement rate limiting to prevent abuse
Troubleshooting
Emails not sending
- Check API key is correct in
.env
- Verify sender domain is verified in Plunk
- Check Plunk dashboard for errors
- Look for errors in application logs
- Test with curl:
curl -X POST https://api.useplunk.com/v1/send \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"to": "[email protected]",
"subject": "Test",
"body": "Test message"
}'
Emails going to spam
- Verify your domain with Plunk
- Add SPF and DKIM records to your DNS
- Avoid spam trigger words
- Include an unsubscribe link
- Start with small volumes and increase gradually
Production Checklist
Alternatives
If you need more features:
- SendGrid: More features, higher volume
- Postmark: Focus on deliverability
- AWS SES: Cheaper for high volume
- Resend: Developer-friendly, React email support
- Mailgun: Enterprise features
Support