authelia-api/src/internal/smtp/client.go
cclohmar 38a69937d5 chore: add Go source code to src/ directory
- Source code for authelia-api (Go 1.22)
- cmd/server/main.go - entry point
- internal/api, auth, bootstrap, config, database, smtp, sync packages
- migrations/001_initial.sql - database schema
- go.mod with dependencies (x/crypto, yaml.v3, modernc/sqlite)
2026-07-15 14:27:40 +00:00

323 lines
8.2 KiB
Go

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(`<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { background: #f8f9fa; padding: 20px; text-align: center; border-radius: 5px; }
.content { padding: 20px; background: white; border-radius: 5px; margin-top: 20px; }
.info-box { background: #e9ecef; padding: 15px; border-radius: 5px; margin: 15px 0; font-family: monospace; }
.button { display: inline-block; background: #007bff; color: white; padding: 12px 24px; text-decoration: none; border-radius: 5px; margin: 10px 0; }
.footer { margin-top: 20px; padding-top: 20px; border-top: 1px solid #dee2e6; font-size: 12px; color: #6c757d; }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>Welcome to Authelia</h1>
</div>
<div class="content">
<p>Hello <strong>%s</strong>,</p>
<p>Your account has been created in our authentication system. To get started, you need to set your password.</p>
<h3>Next Steps:</h3>
<ol>
<li>Visit the Authelia portal at: <strong>https://auth.sechpoint.app</strong></li>
<li>Click on "Reset Password"</li>
<li>Enter your username: <strong>%s</strong></li>
<li>You will receive a password reset email</li>
<li>Follow the instructions in that email to set your password</li>
</ol>
<div class="info-box">
<strong>Important:</strong> Do not use the placeholder password below for login.<br>
It is only shown for verification purposes.
</div>
<p>If you have any issues, please contact your system administrator.</p>
<p>Best regards,<br>
The Authelia Team</p>
</div>
<div class="footer">
<p>This is an automated message. Please do not reply to this email.</p>
<p>If you did not request this account, please contact your administrator immediately.</p>
</div>
</div>
</body>
</html>`, 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
}