package smtp
import (
"crypto/tls"
"fmt"
"net"
"net/smtp"
"net/url"
"time"
)
// Client represents an SMTP client for sending emails
type Client struct {
host string
port string
username string
password string
sender string
tls bool
startTLS bool
}
// Config holds SMTP configuration
type Config struct {
Address string
Username string
Password string
Sender string
}
// NewClient creates a new SMTP client from Authelia configuration
func NewClient(cfg Config) (*Client, error) {
if cfg.Address == "" {
return nil, fmt.Errorf("SMTP address is required")
}
// Parse the address (format: "submission://host:port")
u, err := url.Parse(cfg.Address)
if err != nil {
return nil, fmt.Errorf("invalid SMTP address format: %w", err)
}
host := u.Hostname()
port := u.Port()
if port == "" {
// Default ports based on scheme
switch u.Scheme {
case "smtp":
port = "25"
case "smtps":
port = "465"
case "submission":
port = "587"
default:
port = "587"
}
}
// Determine TLS mode based on scheme
var useTLS, useStartTLS bool
switch u.Scheme {
case "smtp":
// Plain SMTP, no TLS
useTLS = false
useStartTLS = false
case "smtps":
// Implicit TLS (SMTPS)
useTLS = true
useStartTLS = false
case "submission":
// STARTTLS on port 587
useTLS = false
useStartTLS = true
default:
// Default to STARTTLS for unknown schemes
useTLS = false
useStartTLS = true
}
return &Client{
host: host,
port: port,
username: cfg.Username,
password: cfg.Password,
sender: cfg.Sender,
tls: useTLS,
startTLS: useStartTLS,
}, nil
}
// SendWelcomeEmail sends a welcome email to a new user
func (c *Client) SendWelcomeEmail(toEmail, username, placeholderPassword string) error {
subject := "Welcome to Authelia - Set Your Password"
body := c.generateWelcomeEmail(username, placeholderPassword)
// Prepare message
message := fmt.Sprintf(`From: %s
To: %s
Subject: %s
Content-Type: text/html; charset=UTF-8
%s`, c.sender, toEmail, subject, body)
// Connect to SMTP server
var auth smtp.Auth
if c.username != "" && c.password != "" {
auth = smtp.PlainAuth("", c.username, c.password, c.host)
}
addr := fmt.Sprintf("%s:%s", c.host, c.port)
// Send email based on TLS configuration
if c.tls {
// Implicit TLS (SMTPS)
tlsConfig := &tls.Config{
ServerName: c.host,
MinVersion: tls.VersionTLS12,
}
conn, err := tls.Dial("tcp", addr, tlsConfig)
if err != nil {
return fmt.Errorf("TLS dial failed: %w", err)
}
defer conn.Close()
client, err := smtp.NewClient(conn, c.host)
if err != nil {
return fmt.Errorf("SMTP client creation failed: %w", err)
}
defer client.Close()
// Authenticate if needed
if auth != nil {
if err := client.Auth(auth); err != nil {
return fmt.Errorf("SMTP authentication failed: %w", err)
}
}
// Send mail
if err := client.Mail(c.sender); err != nil {
return fmt.Errorf("SMTP MAIL failed: %w", err)
}
if err := client.Rcpt(toEmail); err != nil {
return fmt.Errorf("SMTP RCPT failed: %w", err)
}
w, err := client.Data()
if err != nil {
return fmt.Errorf("SMTP DATA failed: %w", err)
}
_, err = w.Write([]byte(message))
if err != nil {
return fmt.Errorf("SMTP write failed: %w", err)
}
err = w.Close()
if err != nil {
return fmt.Errorf("SMTP close failed: %w", err)
}
client.Quit()
} else if c.startTLS {
// STARTTLS (plain connection then upgrade)
client, err := smtp.Dial(addr)
if err != nil {
return fmt.Errorf("SMTP dial failed: %w", err)
}
defer client.Close()
// Start TLS
tlsConfig := &tls.Config{
ServerName: c.host,
MinVersion: tls.VersionTLS12,
}
if err := client.StartTLS(tlsConfig); err != nil {
return fmt.Errorf("STARTTLS failed: %w", err)
}
// Authenticate if needed
if auth != nil {
if err := client.Auth(auth); err != nil {
return fmt.Errorf("SMTP authentication failed: %w", err)
}
}
// Send mail
if err := client.Mail(c.sender); err != nil {
return fmt.Errorf("SMTP MAIL failed: %w", err)
}
if err := client.Rcpt(toEmail); err != nil {
return fmt.Errorf("SMTP RCPT failed: %w", err)
}
w, err := client.Data()
if err != nil {
return fmt.Errorf("SMTP DATA failed: %w", err)
}
_, err = w.Write([]byte(message))
if err != nil {
return fmt.Errorf("SMTP write failed: %w", err)
}
err = w.Close()
if err != nil {
return fmt.Errorf("SMTP close failed: %w", err)
}
client.Quit()
} else {
// Plain SMTP (not recommended)
err := smtp.SendMail(addr, auth, c.sender, []string{toEmail}, []byte(message))
if err != nil {
return fmt.Errorf("SMTP send failed: %w", err)
}
}
fmt.Printf("✓ Welcome email sent to %s\n", toEmail)
return nil
}
// generateWelcomeEmail generates the HTML email content
func (c *Client) generateWelcomeEmail(username, placeholderPassword string) string {
// Simple HTML email template
return fmt.Sprintf(`
Hello %s,
Your account has been created in our authentication system. To get started, you need to set your password.
Next Steps:
- Visit the Authelia portal at: https://auth.sechpoint.app
- Click on "Reset Password"
- Enter your username: %s
- You will receive a password reset email
- Follow the instructions in that email to set your password
Important: Do not use the placeholder password below for login.
It is only shown for verification purposes.
If you have any issues, please contact your system administrator.
Best regards,
The Authelia Team
`, username, username)
}
// TestConnection tests the SMTP connection with current credentials
func (c *Client) TestConnection() error {
addr := fmt.Sprintf("%s:%s", c.host, c.port)
// Test with timeout
conn, err := net.DialTimeout("tcp", addr, 10*time.Second)
if err != nil {
return fmt.Errorf("connection failed: %w", err)
}
conn.Close()
// Don't actually send, just test credentials
if c.tls {
// Implicit TLS test
tlsConfig := &tls.Config{
ServerName: c.host,
MinVersion: tls.VersionTLS12,
}
conn, err := tls.Dial("tcp", addr, tlsConfig)
if err != nil {
return fmt.Errorf("TLS test failed: %w", err)
}
conn.Close()
} else if c.startTLS {
// STARTTLS test
client, err := smtp.Dial(addr)
if err != nil {
return fmt.Errorf("SMTP dial failed: %w", err)
}
defer client.Close()
tlsConfig := &tls.Config{
ServerName: c.host,
MinVersion: tls.VersionTLS12,
}
if err := client.StartTLS(tlsConfig); err != nil {
return fmt.Errorf("STARTTLS test failed: %w", err)
}
}
fmt.Printf("✓ SMTP connection test passed: %s:%s\n", c.host, c.port)
return nil
}