package util
import (
"crypto/tls"
"encoding/base64"
"fmt"
"net/smtp"
"strings"
"time"
"go.uber.org/zap"
)
// AlertType defines the severity level of alerts
type AlertType string
const (
INFO AlertType = "INFO"
WARNING AlertType = "WARNING"
ERROR AlertType = "ERROR"
CRITICAL AlertType = "CRITICAL"
)
// SMTPConfig holds SMTP server configuration
type SMTPConfig struct {
Server string
Port int
UseTLS bool // Use TLS direct connection (port 465)
Username string
Password string
}
// EmailAlert is the main email alert service
type EmailAlert struct {
config SMTPConfig
enable bool
logger *zap.Logger
}
// NewEmailAlert creates a new email alert service
func NewEmailAlert(alertEmail, password, provider string, enable bool) *EmailAlert {
var config SMTPConfig
switch strings.ToLower(provider) {
case "gmail", "google":
config = SMTPConfig{
Server: "smtp.gmail.com",
Port: 587,
UseTLS: false, // Gmail uses STARTTLS on port 587
Username: alertEmail,
Password: password,
}
case "qq":
config = SMTPConfig{
Server: "smtp.qq.com",
Port: 465,
UseTLS: true, // QQ mail uses direct TLS
Username: alertEmail,
Password: password,
}
default:
// Default to Tencent Corporate Email
config = SMTPConfig{
Server: "smtp.exmail.qq.com",
Port: 465,
UseTLS: true,
Username: alertEmail,
Password: password,
}
}
logger, _ := zap.NewProduction()
return &EmailAlert{
config: config,
enable: enable,
logger: logger.Named("EmailAlert"),
}
}
// SendAlert sends an alert email with specified type
func (e *EmailAlert) SendAlert(recipients []string, subject, body string, alertType AlertType) error {
if !e.enable {
e.logger.Info("Email alerts are disabled, skipping...")
return nil
}
message := e.buildMessage(recipients, subject, body, alertType)
addr := fmt.Sprintf("%s:%d", e.config.Server, e.config.Port)
var err error
var client *smtp.Client
// Establish connection based on TLS settings
if e.config.UseTLS {
// Direct TLS connection (port 465)
tlsConfig := &tls.Config{
InsecureSkipVerify: false,
ServerName: e.config.Server,
}
conn, err := tls.Dial("tcp", addr, tlsConfig)
if err != nil {
return fmt.Errorf("TLS connection failed: %v", err)
}
client, err = smtp.NewClient(conn, e.config.Server)
if err != nil {
return fmt.Errorf("failed to create SMTP client: %v", err)
}
} else {
// Plain connection with STARTTLS (port 587)
client, err = smtp.Dial(addr)
if err != nil {
return fmt.Errorf("failed to connect to SMTP server: %v", err)
}
// Upgrade to TLS using STARTTLS for Gmail
if e.config.Server == "smtp.gmail.com" {
tlsConfig := &tls.Config{
InsecureSkipVerify: false,
ServerName: e.config.Server,
}
if err = client.StartTLS(tlsConfig); err != nil {
client.Close()
return fmt.Errorf("STARTTLS failed: %v", err)
}
}
}
defer client.Close()
// Authenticate
auth := smtp.PlainAuth("", e.config.Username, e.config.Password, e.config.Server)
if err = client.Auth(auth); err != nil {
return fmt.Errorf("SMTP authentication failed: %v", err)
}
// Set sender
if err = client.Mail(e.config.Username); err != nil {
return fmt.Errorf("failed to set sender: %v", err)
}
// Set recipients
for _, recipient := range recipients {
if err = client.Rcpt(recipient); err != nil {
return fmt.Errorf("failed to set recipient %s: %v", recipient, err)
}
}
// Send email content
writer, err := client.Data()
if err != nil {
return fmt.Errorf("failed to send email content: %v", err)
}
_, err = writer.Write([]byte(message))
if err != nil {
return fmt.Errorf("failed to write message content: %v", err)
}
err = writer.Close()
if err != nil {
return fmt.Errorf("failed to complete email delivery: %v", err)
}
e.logger.Info("Alert email sent successfully",
zap.String("type", string(alertType)),
zap.Any("recipients", recipients))
return nil
}
// encodeRFC2047 encodes a string using RFC 2047 for email headers
func (e *EmailAlert) encodeRFC2047(s string) string {
// RFC 2047 format: =?UTF-8?B?<base64-encoded-text>?=
return fmt.Sprintf("=?UTF-8?B?%s?=", base64.StdEncoding.EncodeToString([]byte(s)))
}
// buildMessage constructs the email message with HTML formatting
func (e *EmailAlert) buildMessage(recipients []string, subject, body string, alertType AlertType) string {
timestamp := time.Now().Format("2006-01-02 15:04:05")
// Add emoji prefix based on alert type
var prefix string
switch alertType {
case INFO:
prefix = "ℹ️ [INFO]"
case WARNING:
prefix = "⚠️ [WARNING]"
case ERROR:
prefix = "❌ [ERROR]"
case CRITICAL:
prefix = "🚨 [CRITICAL]"
default:
prefix = "📧 [NOTIFY]"
}
fullSubject := fmt.Sprintf("%s %s", prefix, subject)
// Build HTML email body
htmlBody := fmt.Sprintf(`
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.container {
max-width: 600px;
margin: 0 auto;
background-color: white;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.header {
background-color: %s;
color: white;
padding: 20px;
text-align: center;
}
.header h2 {
margin: 0;
font-size: 24px;
}
.content {
padding: 30px;
border-left: 4px solid %s;
background-color: #fafafa;
margin: 20px;
border-radius: 4px;
}
.info-row {
margin: 10px 0;
padding: 8px 0;
border-bottom: 1px solid #eee;
}
.info-row:last-child {
border-bottom: none;
}
.label {
font-weight: bold;
color: #555;
display: inline-block;
min-width: 100px;
}
.value {
color: #333;
}
.timestamp {
font-weight: bold;
color: #333;
}
.message-content {
margin-top: 20px;
padding: 15px;
background-color: white;
border-radius: 4px;
line-height: 1.6;
}
.footer {
margin-top: 20px;
padding: 20px;
font-size: 12px;
color: #666;
text-align: center;
background-color: #f9f9f9;
}
.footer p {
margin: 5px 0;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h2>%s</h2>
</div>
<div class="content">
<div class="info-row">
<span class="label">Alert Time:</span>
<span class="value timestamp">%s</span>
</div>
<div class="info-row">
<span class="label">Alert Level:</span>
<span class="value">%s</span>
</div>
<div class="message-content">
%s
</div>
</div>
<div class="footer">
<p>⚙️ This is an automated system email, please do not reply directly</p>
<p>If you have any questions, please contact your system administrator</p>
</div>
</div>
</body>
</html>`,
e.getAlertColor(alertType),
e.getAlertColor(alertType),
fullSubject,
timestamp,
string(alertType),
strings.ReplaceAll(body, "\n", "<br>"))
// Build complete email message with required RFC5322 headers
// Generate RFC5322 compliant date
rfcDate := time.Now().Format(time.RFC1123Z)
// Encode subject according to RFC2047
encodedSubject := e.encodeRFC2047(fullSubject)
message := fmt.Sprintf("From: %s\r\n"+
"To: %s\r\n"+
"Subject: %s\r\n"+
"Date: %s\r\n"+
"MIME-Version: 1.0\r\n"+
"Content-Type: text/html; charset=UTF-8\r\n"+
"\r\n"+
"%s",
e.config.Username,
strings.Join(recipients, ", "),
encodedSubject,
rfcDate,
htmlBody)
return message
}
// getAlertColor returns the color code for each alert type
func (e *EmailAlert) getAlertColor(alertType AlertType) string {
colors := map[AlertType]string{
INFO: "#17a2b8", // Light blue
WARNING: "#ffc107", // Yellow
ERROR: "#dc3545", // Red
CRITICAL: "#6f42c1", // Purple
}
if color, exists := colors[alertType]; exists {
return color
}
return "#6c757d" // Gray
}
// SendSystemAlert sends a system error alert
func (e *EmailAlert) SendSystemAlert(recipients []string, systemName, errorMsg string) error {
if !e.enable {
return nil
}
subject := fmt.Sprintf("System Alert - %s", systemName)
body := fmt.Sprintf(`
<strong>System:</strong> %s<br>
<strong>Time:</strong> %s<br>
<strong>Error Message:</strong><br>
<pre style="background-color: #f4f4f4; padding: 10px; border-radius: 4px;">%s</pre>
`,
systemName,
time.Now().Format("2006-01-02 15:04:05"),
errorMsg)
return e.SendAlert(recipients, subject, body, ERROR)
}
// SendServiceDownAlert sends a service down alert
func (e *EmailAlert) SendServiceDownAlert(recipients []string, serviceName string) error {
if !e.enable {
return nil
}
subject := fmt.Sprintf("Service Down Alert - %s", serviceName)
body := fmt.Sprintf(`
<strong>Service Name:</strong> %s<br>
<strong>Status:</strong> <span style="color: red;">Service Unavailable</span><br>
<strong>Time:</strong> %s<br>
<br>
<p style="color: red; font-weight: bold;">⚠️ Please check the service status immediately!</p>
`,
serviceName,
time.Now().Format("2006-01-02 15:04:05"))
return e.SendAlert(recipients, subject, body, CRITICAL)
}