package notifications import ( "bytes" "context" "encoding/json" "fmt" "log" "net/http" "time" ) // EmailNotifier sends alert notifications via email (Resend). type EmailNotifier struct { APIKey string From string To string } // Send implements Notifier for email. func (e *EmailNotifier) Send(_ context.Context, message string, meta AlertMetadata) error { _, err := SendEmailNotification(e.APIKey, e.From, e.To, message, meta) return err } const emailHTTPTimeoutSeconds = 10 var emailHTTPClient = &http.Client{ //nolint:gochecknoglobals Timeout: emailHTTPTimeoutSeconds * time.Second, } type resendPayload struct { From string `json:"from"` To string `json:"to"` Subject string `json:"subject"` HTML string `json:"html"` } func SendEmailNotification(apiKey, fromAddress, toAddress, message string, meta AlertMetadata) (bool, error) { if apiKey == "" { log.Printf("Skipping email notification: RESEND_API_KEY not configured") return false, nil } subject := fmt.Sprintf("Koin Ping Alert: %s", alertTypeLabel(meta.AlertType)) txLink := "" if meta.TxHash != "" { txLink = fmt.Sprintf( `

View on Etherscan

`, meta.TxHash, ) } html := fmt.Sprintf(`

Koin Ping Alert

%s

Address %s
Blockchain %s
%s

Sent by Koin Ping

`, message, meta.AddressLabel, meta.Address, txLink) payload := resendPayload{ From: fromAddress, To: toAddress, Subject: subject, HTML: html, } body, err := json.Marshal(payload) if err != nil { return false, fmt.Errorf("marshal email payload: %w", err) } req, err := http.NewRequest(http.MethodPost, "https://api.resend.com/emails", bytes.NewReader(body)) if err != nil { return false, fmt.Errorf("create email request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+apiKey) resp, err := emailHTTPClient.Do(req) if err != nil { log.Printf("Failed to send email notification: %v", err) return false, err } defer resp.Body.Close() if resp.StatusCode < 200 || resp.StatusCode >= 300 { log.Printf("Resend API failed: HTTP %d", resp.StatusCode) return false, fmt.Errorf("resend API failed: HTTP %d", resp.StatusCode) } return true, nil } func TestEmailNotification(apiKey, fromAddress, toAddress string) (bool, error) { if apiKey == "" { return false, fmt.Errorf("email not configured: RESEND_API_KEY not set") //nolint:err113 } payload := resendPayload{ From: fromAddress, To: toAddress, Subject: "Koin Ping — Test Notification", HTML: `

Your email alerts are configured correctly!

Sent by Koin Ping

`, } body, err := json.Marshal(payload) if err != nil { return false, err } req, err := http.NewRequest(http.MethodPost, "https://api.resend.com/emails", bytes.NewReader(body)) if err != nil { return false, err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", "Bearer "+apiKey) resp, err := emailHTTPClient.Do(req) if err != nil { log.Printf("Email test failed: %v", err) return false, err } defer resp.Body.Close() return resp.StatusCode >= 200 && resp.StatusCode < 300, nil } func alertTypeLabel(alertType string) string { switch alertType { case "incoming_tx": return "Incoming Transaction" case "outgoing_tx": return "Outgoing Transaction" case "large_transfer": return "Large Transfer" case "balance_below": return "Balance Below Threshold" default: return "Alert" } }