SMTP Provider¶
The SMTP provider offers a comprehensive email client implementation with support for various authentication methods,
TLS encryption and attachments. Built on the powerful go-mail library, it provides a secure and feature-rich solution
for sending emails.
Features¶
- Multiple Authentication Methods: Support for Plain, Login, CRAMMD5, XOAUTH2, SCRAM-SHA-1, SCRAM-SHA-256, and custom auth
- TLS/SSL Support: Optional TLS encryption with certificate validation
- Secure Credential Management: Password encryption and secure storage
- Rich Message Composition: HTML and plain text bodies with attachments
- Email Validation: Built-in email address validation
- BCC Support: Automatic BCC functionality for compliance
- Message Options: Flexible message building with functional options
- Connection Management: Automatic connection handling and cleanup
Installation¶
Configuration¶
Basic Configuration¶
package main
import (
"github.com/oddbit-project/blueprint/provider/smtp"
)
func main() {
config := smtp.NewConfig()
config.Host = "smtp.gmail.com"
config.Port = 587
config.Username = "your-email@gmail.com"
config.Password = "your-app-password" // Embedded field, accessed directly
config.AuthType = "plain"
config.From = "your-email@gmail.com"
config.TLSEnable = true // Embedded field, accessed directly
mailer, err := smtp.NewMailer(config)
if err != nil {
panic(err)
}
}
JSON Configuration¶
{
"smtp": {
"host": "smtp.gmail.com",
"port": 587,
"username": "your-email@gmail.com",
"password": "your-app-password",
"authType": "plain",
"from": "your-email@gmail.com",
"bcc": "compliance@company.com",
"tlsEnable": true,
"tlsInsecureSkipVerify": false,
"tlsPolicy": "mandatory",
"sslOnConnect": false,
"timeout": 15
}
}
Configuration with Secure Credentials¶
config := smtp.NewConfig()
config.Host = "smtp.example.com"
config.Port = 587
config.Username = "mailer@example.com"
config.DefaultCredentialConfig = secure.DefaultCredentialConfig{
PasswordEnvVar: "SMTP_PASSWORD", // Read from environment
PasswordFile: "/secrets/smtp_pwd", // Or read from file
}
config.AuthType = "plain"
config.From = "noreply@example.com"
Advanced TLS Configuration¶
config := smtp.NewConfig()
config.Host = "secure-smtp.example.com"
config.Port = 465
config.ClientConfig = tls.ClientConfig{
TLSEnable: true,
TLSCA: "/path/to/ca-cert.pem",
TLSCert: "/path/to/client-cert.pem",
TLSKey: "/path/to/client-key.pem",
TLSInsecureSkipVerify: false,
}
config.SSLOnConnect = true // implicit TLS on port 465
TLSEnable controls whether the certificate settings above are used; when it is false, go-mail's own defaults apply
(server name taken from the host, TLS 1.2 minimum, system trust store), and configuring any of TLSCA, TLSCert,
TLSKey, TLSInsecureSkipVerify or a key password without it is rejected with ErrTLSNotEnabled rather than
silently ignored. With TLSEnable, TLSCert and TLSKey must be set together, and a key password requires both:
otherwise NewMailer fails with tls.ErrTLSIncompleteKeyPair, instead of connecting without the client certificate.
Note that TLSEnable does not, on its own, decide whether the connection is encrypted — that is the STARTTLS policy
below, which requires TLS by default. The two are configured separately:
// Self-signed or otherwise untrusted server certificate
config.TLSEnable = true
config.TLSInsecureSkipVerify = true
// Private CA instead of skipping verification
config.TLSEnable = true
config.TLSCA = "/path/to/ca-cert.pem"
// Plaintext server, e.g. a local mailhog instance
config.TLSPolicy = smtp.TLSPolicyNone
TLS Policy¶
| Value | Behaviour |
|---|---|
"mandatory" |
STARTTLS is required; the connection fails if the server lacks it (default) |
"opportunistic" |
STARTTLS is used when advertised, otherwise the connection stays plaintext |
"none" |
STARTTLS is never attempted |
The policy applies to STARTTLS only; with SSLOnConnect the connection is encrypted from the start and the policy is
not used.
Note: with
SSLOnConnectthe provider performs the TLS dial itself, in order to apply the timeout to the whole conversation.go-mailtherefore does not see the connection as encrypted when auto-discovering the authentication mechanism, and restricts its choice to SCRAM-SHA-256, SCRAM-SHA-1 and CRAM-MD5. Set an explicitAuthTypeinstead of"auto"when using implicit TLS with a server that only offers PLAIN or LOGIN.
Configuration Options¶
| Field | Type | Default | Description |
|---|---|---|---|
Host |
string |
"127.0.0.1" |
SMTP server hostname |
Port |
int |
1025 |
SMTP server port |
Username |
string |
"" |
SMTP authentication username |
Password |
string |
"" |
SMTP authentication password (embedded field) |
PasswordEnvVar |
string |
"" |
Environment variable for password |
PasswordFile |
string |
"" |
File path containing password |
AuthType |
string |
"noauth" |
Authentication method (plain, login, crammd5, etc.) |
From |
string |
"no-reply@acme.co" |
Sender address, applied to every message |
Bcc |
string |
"" |
Comma-separated BCC addresses, added to every message |
TLSEnable |
bool |
false |
Apply the TLS certificate settings (embedded field) |
TLSCA |
string |
"" |
CA bundle used to verify the server certificate |
TLSCert |
string |
"" |
Client certificate |
TLSKey |
string |
"" |
Client certificate key |
TLSInsecureSkipVerify |
bool |
false |
Skip TLS certificate verification (self-signed) |
TLSPolicy |
string |
"mandatory" |
STARTTLS policy: mandatory, opportunistic, none |
SSLOnConnect |
bool |
false |
Implicit TLS (SMTPS, usually port 465) |
Timeout |
duration.Seconds |
15 |
Timeout for the SMTP conversation, in seconds |
Timeout uses the types/duration package, which serializes as a plain integer number of seconds:
Authentication Types¶
The SMTP provider supports multiple authentication methods:
"plain"- Plain text authentication (most common)"login"- LOGIN authentication"crammd5"- CRAM-MD5 authentication (aliases:"cram-md5","cram")"xoauth2"- XOAUTH2 for OAuth2 authentication (alias:"oauth2")"scram-sha-1"/"scram-sha-256"- SCRAM authentication"scram-sha-1-plus"/"scram-sha-256-plus"- SCRAM with channel binding (requires TLS)"auto"- Pick the strongest mechanism the server advertises (aliases:"autodiscover","autodiscovery")"custom"- Custom authentication (requires custom auth option)"noauth"- No authentication (for development/testing); also assumed whenAuthTypeis empty (aliases:"none","no")
"plain-noenc" and "login-noenc" are also accepted; they send the credentials over an unencrypted connection, which
"plain" and "login" refuse to do. Do not use them outside a trusted network.
Because go-mail performs no authentication at all for noauth, a configuration that sets Username without an
authentication method would send unauthenticated mail with the credentials unused. NewMailer rejects that
combination with ErrCredentialsUnused.
Usage Examples¶
Basic Email Sending¶
package main
import (
"log"
"github.com/oddbit-project/blueprint/provider/smtp"
)
func main() {
// Create configuration
config := smtp.NewConfig()
config.Host = "smtp.gmail.com"
config.Port = 587
config.Username = "your-email@gmail.com"
config.Password = "your-app-password"
config.AuthType = "plain"
config.From = "your-email@gmail.com"
config.TLSEnable = true
// Create mailer
mailer, err := smtp.NewMailer(config)
if err != nil {
log.Fatal(err)
}
// Create and send message
recipients := []string{"recipient@example.com"}
msg, err := mailer.NewMessage(recipients, "Hello from Blueprint!",
smtp.WithBody("Hello World!", "<h1>Hello World!</h1>"),
)
if err != nil {
log.Fatal(err)
}
if err := mailer.Send(msg); err != nil {
log.Fatal(err)
}
log.Println("Email sent successfully!")
}
HTML Email with Plain Text Alternative¶
plainText := `
Hello John,
Thank you for signing up for our service!
Best regards,
The Team
`
htmlBody := `
<html>
<head><title>Welcome</title></head>
<body>
<h1>Hello John,</h1>
<p>Thank you for signing up for our service!</p>
<p><strong>Best regards,</strong><br/>The Team</p>
</body>
</html>
`
msg, err := mailer.NewMessage(
[]string{"john@example.com"},
"Welcome to our service!",
smtp.WithBody(plainText, htmlBody),
)
if err != nil {
log.Fatal(err)
}
err = mailer.Send(msg)
if err != nil {
log.Fatal(err)
}
Email with Attachments¶
msg, err := mailer.NewMessage(
[]string{"recipient@example.com"},
"Document Attached",
smtp.WithBody("Please find the attached document.", ""),
smtp.WithAttachment("/path/to/document.pdf"),
smtp.WithAttachment("/path/to/image.png"),
)
if err != nil {
log.Fatal(err)
}
err = mailer.Send(msg)
if err != nil {
log.Fatal(err)
}
Custom From Address¶
msg, err := mailer.NewMessage(
[]string{"customer@example.com"},
"Support Ticket Update",
smtp.WithFrom("support@company.com"),
smtp.WithBody("Your ticket has been updated.", ""),
)
if err != nil {
log.Fatal(err)
}
err = mailer.Send(msg)
Batch Email Sending¶
var messages []*gomail.Msg
// Create multiple messages
for _, recipient := range recipients {
msg, err := mailer.NewMessage(
[]string{recipient.Email},
"Newsletter",
smtp.WithBody("Newsletter content", "<h1>Newsletter</h1>"),
)
if err != nil {
log.Printf("Failed to create message for %s: %v", recipient.Email, err)
continue
}
messages = append(messages, msg)
}
// Send all messages in batch, over a single connection
if err := mailer.Send(messages...); err != nil {
log.Fatal("Failed to send batch emails:", err)
}
// Or with a caller-supplied context bounding the connection attempt
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := mailer.SendWithContext(ctx, messages...); err != nil {
log.Fatal("Failed to send batch emails:", err)
}
Send opens one connection for the whole batch and closes it when done. Connection failures — dial, TLS handshake,
STARTTLS negotiation and authentication, and a connection lost part-way through a batch — are reported as
ErrSMTPServer, and abort the remaining messages; failures while transferring a message are reported as ErrMessage,
and the rest of the batch is still attempted. Both wrap the underlying go-mail error, so errors.Is/errors.As reach it.
The Timeout setting is applied as a deadline over the whole conversation: it bounds the connection attempt, the
server greeting, STARTTLS and authentication, and is renewed before each message is sent.
BCC Support¶
// Configure automatic BCC
config.Bcc = "compliance@company.com,audit@company.com"
// All messages created by the mailer include these BCC recipients
msg, err := mailer.NewMessage(
[]string{"customer@example.com"},
"Important Notice",
smtp.WithBody("This email will be BCC'd to compliance.", ""),
)
The configured recipients are added after the message options are applied, so they do not replace any BCC address set on the message itself.
Message Options¶
The SMTP provider uses functional options for flexible message composition:
WithFrom(address)¶
Override the default sender address:
WithBody(plainText, htmlBody)¶
Set message body content:
WithAttachment(path)¶
Add file attachments:
Multiple options can be combined:
msg, err := mailer.NewMessage(
[]string{"user@example.com"},
"Multi-part Message",
smtp.WithFrom("sales@company.com"),
smtp.WithBody("Plain text", "<h1>HTML content</h1>"),
smtp.WithAttachment("/path/to/brochure.pdf"),
smtp.WithAttachment("/path/to/image.jpg"),
)
Security Considerations¶
Secure Password Management¶
// Use environment variables
config.PasswordEnvVar = "SMTP_PASSWORD"
// Use secure file storage
config.PasswordFile = "/run/secrets/smtp_password"
TLS Configuration¶
// Enable TLS with certificate verification
config.TLSEnable = true
config.TLSInsecureSkipVerify = false
// Use custom CA certificates
config.TLSCA = "/path/to/ca-bundle.crt"
// Client certificate authentication
config.TLSCert = "/path/to/client.crt"
config.TLSKey = "/path/to/client.key"
Email Validation¶
The provider automatically validates email addresses:
// Invalid emails will return ErrInvalidTo
invalidRecipients := []string{"not-an-email", "invalid@"}
_, err := mailer.NewMessage(invalidRecipients, "Subject", ...)
// err == smtp.ErrInvalidTo
Integration Examples¶
With HTTP Server for Contact Forms¶
import (
"fmt"
"github.com/gin-gonic/gin"
"github.com/oddbit-project/blueprint/provider/httpserver"
"github.com/oddbit-project/blueprint/provider/smtp"
)
type ContactForm struct {
Name string `json:"name" binding:"required"`
Email string `json:"email" binding:"required,email"`
Message string `json:"message" binding:"required"`
}
func setupContactHandler(mailer *smtp.Mailer) gin.HandlerFunc {
return func(c *gin.Context) {
var form ContactForm
if err := c.ShouldBindJSON(&form); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
// Send notification email
msg, err := mailer.NewMessage(
[]string{"contact@company.com"},
"New Contact Form Submission",
smtp.WithBody(
fmt.Sprintf("Name: %s\nEmail: %s\nMessage: %s",
form.Name, form.Email, form.Message),
fmt.Sprintf("<h3>New Contact</h3><p><strong>Name:</strong> %s<br/><strong>Email:</strong> %s<br/><strong>Message:</strong> %s</p>",
form.Name, form.Email, form.Message),
),
)
if err != nil {
c.JSON(500, gin.H{"error": "Failed to create email"})
return
}
if err := mailer.Send(msg); err != nil {
c.JSON(500, gin.H{"error": "Failed to send email"})
return
}
c.JSON(200, gin.H{"message": "Contact form submitted successfully"})
}
}
With Configuration Provider¶
import (
"github.com/oddbit-project/blueprint/config/provider"
"github.com/oddbit-project/blueprint/provider/smtp"
)
func setupMailerFromConfig(configFile string) (*smtp.Mailer, error) {
// Load configuration
cfg, err := provider.NewJsonProvider(configFile)
if err != nil {
return nil, err
}
// Create SMTP config
smtpConfig := smtp.NewConfig()
if err := cfg.GetKey("smtp", smtpConfig); err != nil {
return nil, err
}
// Create mailer
return smtp.NewMailer(smtpConfig)
}
Email Templates¶
import "html/template"
type EmailTemplate struct {
Subject string
Plain string
HTML string
}
func (t *EmailTemplate) Render(data interface{}) (string, string, error) {
plainTmpl, err := template.New("plain").Parse(t.Plain)
if err != nil {
return "", "", err
}
htmlTmpl, err := template.New("html").Parse(t.HTML)
if err != nil {
return "", "", err
}
var plainBuf, htmlBuf strings.Builder
if err := plainTmpl.Execute(&plainBuf, data); err != nil {
return "", "", err
}
if err := htmlTmpl.Execute(&htmlBuf, data); err != nil {
return "", "", err
}
return plainBuf.String(), htmlBuf.String(), nil
}
// Usage
welcomeTemplate := &EmailTemplate{
Subject: "Welcome {{.Name}}!",
Plain: "Hello {{.Name}}, welcome to our service!",
HTML: "<h1>Hello {{.Name}}</h1><p>Welcome to our service!</p>",
}
userData := map[string]string{"Name": "John"}
plain, html, err := welcomeTemplate.Render(userData)
if err != nil {
log.Fatal(err)
}
msg, err := mailer.NewMessage(
[]string{"john@example.com"},
"Welcome John!",
smtp.WithBody(plain, html),
)
Error Handling¶
The SMTP provider defines specific error types for different failure scenarios:
_, err := mailer.NewMessage([]string{"invalid-email"}, "Subject")
if err != nil {
switch err {
case smtp.ErrInvalidTo:
log.Println("Invalid recipient email address")
case smtp.ErrInvalidFrom:
log.Println("Invalid sender email address")
case smtp.ErrInvalidBcc:
log.Println("Invalid BCC email address")
default:
log.Printf("Unexpected error: %v", err)
}
}
Sending errors wrap the underlying go-mail error, so they must be matched with errors.Is rather than compared
directly:
if err := mailer.Send(msg); err != nil {
switch {
case errors.Is(err, smtp.ErrSMTPServer):
log.Println("Failed to connect to SMTP server:", err)
case errors.Is(err, smtp.ErrMessage):
log.Println("Failed to send email message:", err)
default:
log.Printf("Send error: %v", err)
}
}
NewMailer reports invalid configuration before any connection is attempted:
| Error | Cause |
|---|---|
ErrInvalidConfig |
Nil configuration |
ErrMissingHost |
Empty Host |
ErrMissingPort |
Port below 1 |
ErrInvalidTLSPolicy |
TLSPolicy is not mandatory, opportunistic or none |
ErrInvalidTimeout |
Negative Timeout |
ErrTLSNotEnabled |
Certificate settings configured while TLSEnable is false |
tls.ErrTLSIncompleteKeyPair |
With TLSEnable, TLSCert without TLSKey (or the reverse), or a key password without both |
ErrInvalidFrom |
From is not a valid address |
ErrInvalidBcc |
Bcc contains an invalid address |
ErrInvalidAuthType |
AuthType is not recognised, or custom without a custom auth option |
ErrInvalidTLSConfig |
The CA, certificate or key could not be loaded (wraps the underlying error) |
ErrInvalidPassword |
The configured password could not be read |
ErrCreatingClient |
go-mail rejected the client options (wraps the underlying error) |
Best Practices¶
- Credential Security: Always use environment variables or secure files for passwords
- TLS Configuration: Enable TLS for production deployments
- Email Validation: Validate email addresses before sending
- Error Handling: Implement proper error handling and retry logic
- Rate Limiting: Respect SMTP server rate limits
- Message Composition: Provide both plain text and HTML versions
- Testing: Use development SMTP servers (like MailHog) for testing
Testing with Development SMTP Server¶
For development and testing, you can use MailHog or similar tools:
Configuration for development:
config := smtp.NewConfig()
config.Host = "127.0.0.1"
config.Port = 1025
config.AuthType = "noauth" // No authentication needed for MailHog
config.From = "test@example.com"
config.TLSPolicy = smtp.TLSPolicyNone // MailHog does not offer STARTTLS
Performance Considerations¶
- Connection Reuse: Each
Sendcall uses a single connection for all the messages it is given - Batch Sending: Use batch sending for multiple emails
- Authentication: The password is resolved once, when the mailer is created
- TLS Overhead: Consider TLS overhead for high-volume sending
- Message Size: Be mindful of attachment sizes and message limits
Common SMTP Server Settings¶
Gmail¶
config.Host = "smtp.gmail.com"
config.Port = 587
config.AuthType = "plain"
config.TLSEnable = true
// Requires App Password, not regular password
Outlook/Hotmail¶
config.Host = "smtp-mail.outlook.com"
config.Port = 587
config.AuthType = "plain"
config.TLSEnable = true
Amazon SES¶
config.Host = "email-smtp.us-east-1.amazonaws.com"
config.Port = 587
config.AuthType = "plain"
config.TLSEnable = true
// Requires SES SMTP credentials
SendGrid¶
config.Host = "smtp.sendgrid.net"
config.Port = 587
config.Username = "apikey"
config.Password = "your-sendgrid-api-key"
config.AuthType = "plain"
config.TLSEnable = true
Implicit TLS (SMTPS)¶
config.Host = "smtp.example.com"
config.Port = 465
config.AuthType = "plain"
config.TLSEnable = true
config.SSLOnConnect = true
Troubleshooting¶
Connection Issues¶
// Test SMTP connection
config := smtp.NewConfig()
config.TLSPolicy = smtp.TLSPolicyNone // local development server without STARTTLS
mailer, err := smtp.NewMailer(config)
if err != nil {
log.Printf("Configuration error: %v", err)
return
}
// Try sending a test message
msg, _ := mailer.NewMessage([]string{"test@example.com"}, "Test")
if err := mailer.Send(msg); err != nil {
log.Printf("Send failed: %v", err)
// Check network, credentials, server settings
}
Common Issues¶
- Authentication Failure: Verify username/password and auth type
- TLS Handshake: Check TLS configuration and certificate validation
- Port Blocked: Ensure SMTP port is not blocked by firewall
- Rate Limiting: Implement delays between batch sends if needed
- Message Rejected: Check email format, size limits, and content filters
See Also¶
- Secure Credentials - For secure password management
- TLS Configuration - For TLS setup and certificates
- HTTP Server - For web application integration
- Configuration Management - For application configuration