67a69ab073
- Go backend API with full CRUD for all modules - Rust analytics service with parallel processing - C runtime with POSIX shared memory IPC - PostgreSQL schema with 30+ tables - Redis cache, Kafka event streaming - WebSocket hub, automation engine - PDF generation, Resend email integration - JWT auth, multi-tenant - Docker Compose deployment - Nginx reverse proxy Refs: BOC-001
221 lines
5.9 KiB
Go
221 lines
5.9 KiB
Go
package email
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
const resendAPIURL = "https://api.resend.com/emails"
|
|
|
|
// Client handles email sending via Resend
|
|
type Client struct {
|
|
apiKey string
|
|
fromEmail string
|
|
fromName string
|
|
httpClient *http.Client
|
|
}
|
|
|
|
// NewClient creates a new Resend email client
|
|
func NewClient(apiKey, fromEmail, fromName string) *Client {
|
|
return &Client{
|
|
apiKey: apiKey,
|
|
fromEmail: fromEmail,
|
|
fromName: fromName,
|
|
httpClient: &http.Client{
|
|
Timeout: 30 * time.Second,
|
|
},
|
|
}
|
|
}
|
|
|
|
// Email represents an email to be sent
|
|
type Email struct {
|
|
To []string `json:"to"`
|
|
From string `json:"from"`
|
|
Subject string `json:"subject"`
|
|
HTML string `json:"html,omitempty"`
|
|
Text string `json:"text,omitempty"`
|
|
Attachments []Attachment `json:"attachments,omitempty"`
|
|
Headers map[string]string `json:"headers,omitempty"`
|
|
}
|
|
|
|
// Attachment represents an email attachment
|
|
type Attachment struct {
|
|
Filename string `json:"filename"`
|
|
Content []byte `json:"content"`
|
|
}
|
|
|
|
// SendEmail sends an email via Resend
|
|
func (c *Client) SendEmail(to []string, subject, htmlBody, textBody string) error {
|
|
from := c.fromEmail
|
|
if c.fromName != "" {
|
|
from = fmt.Sprintf("%s <%s>", c.fromName, c.fromEmail)
|
|
}
|
|
|
|
email := Email{
|
|
To: to,
|
|
From: from,
|
|
Subject: subject,
|
|
HTML: htmlBody,
|
|
Text: textBody,
|
|
}
|
|
|
|
return c.send(email)
|
|
}
|
|
|
|
// SendEmailWithAttachment sends an email with attachments
|
|
func (c *Client) SendEmailWithAttachment(to []string, subject, htmlBody, textBody string, attachments []Attachment) error {
|
|
from := c.fromEmail
|
|
if c.fromName != "" {
|
|
from = fmt.Sprintf("%s <%s>", c.fromName, c.fromEmail)
|
|
}
|
|
|
|
email := Email{
|
|
To: to,
|
|
From: from,
|
|
Subject: subject,
|
|
HTML: htmlBody,
|
|
Text: textBody,
|
|
Attachments: attachments,
|
|
}
|
|
|
|
return c.send(email)
|
|
}
|
|
|
|
// SendInvoice sends an invoice email with PDF attachment
|
|
func (c *Client) SendInvoice(to []string, invoiceNumber string, pdfData []byte, htmlBody string) error {
|
|
if htmlBody == "" {
|
|
htmlBody = fmt.Sprintf(`
|
|
<h2>Faktura %s</h2>
|
|
<p>Bifogat finner du din faktura.</p>
|
|
<p>Vid frågor, kontakta oss.</p>
|
|
`, invoiceNumber)
|
|
}
|
|
|
|
attachments := []Attachment{
|
|
{
|
|
Filename: fmt.Sprintf("faktura-%s.pdf", invoiceNumber),
|
|
Content: pdfData,
|
|
},
|
|
}
|
|
|
|
return c.SendEmailWithAttachment(
|
|
to,
|
|
fmt.Sprintf("Faktura %s", invoiceNumber),
|
|
htmlBody,
|
|
fmt.Sprintf("Faktura %s bifogad.", invoiceNumber),
|
|
attachments,
|
|
)
|
|
}
|
|
|
|
// SendQuote sends a quote email with PDF attachment
|
|
func (c *Client) SendQuote(to []string, quoteNumber string, pdfData []byte, htmlBody string) error {
|
|
if htmlBody == "" {
|
|
htmlBody = fmt.Sprintf(`
|
|
<h2>Offert %s</h2>
|
|
<p>Bifogat finner du din offert.</p>
|
|
<p>Offerten är giltig i 30 dagar.</p>
|
|
`, quoteNumber)
|
|
}
|
|
|
|
attachments := []Attachment{
|
|
{
|
|
Filename: fmt.Sprintf("offert-%s.pdf", quoteNumber),
|
|
Content: pdfData,
|
|
},
|
|
}
|
|
|
|
return c.SendEmailWithAttachment(
|
|
to,
|
|
fmt.Sprintf("Offert %s", quoteNumber),
|
|
htmlBody,
|
|
fmt.Sprintf("Offert %s bifogad.", quoteNumber),
|
|
attachments,
|
|
)
|
|
}
|
|
|
|
// SendWelcome sends a welcome email to a new customer
|
|
func (c *Client) SendWelcome(to []string, customerName string) error {
|
|
htmlBody := fmt.Sprintf(`
|
|
<h2>Välkommen %s!</h2>
|
|
<p>Tack för att du valde oss. Vi ser fram emot ett gott samarbete.</p>
|
|
<p>Logga in på din dashboard för att se dina uppgifter och hantera dina ärenden.</p>
|
|
`, customerName)
|
|
|
|
return c.SendEmail(
|
|
to,
|
|
"Välkommen!",
|
|
htmlBody,
|
|
fmt.Sprintf("Välkommen %s! Tack för att du valde oss.", customerName),
|
|
)
|
|
}
|
|
|
|
// SendPaymentReminder sends a payment reminder
|
|
func (c *Client) SendPaymentReminder(to []string, invoiceNumber string, amount float64, currency string, dueDate time.Time) error {
|
|
htmlBody := fmt.Sprintf(`
|
|
<h2>Påminnelse: Faktura %s</h2>
|
|
<p>Detta är en påminnelse om att faktura %s på <strong>%.2f %s</strong> förfaller %s.</p>
|
|
<p>Vänligen betala i tid för att undvika påminnelseavgifter.</p>
|
|
`, invoiceNumber, invoiceNumber, amount, currency, dueDate.Format("2006-01-02"))
|
|
|
|
return c.SendEmail(
|
|
to,
|
|
fmt.Sprintf("Påminnelse: Faktura %s", invoiceNumber),
|
|
htmlBody,
|
|
fmt.Sprintf("Påminnelse: Faktura %s på %.2f %s förfaller %s.", invoiceNumber, amount, currency, dueDate.Format("2006-01-02")),
|
|
)
|
|
}
|
|
|
|
// SendPasswordReset sends a password reset email
|
|
func (c *Client) SendPasswordReset(to []string, resetToken string, resetURL string) error {
|
|
htmlBody := fmt.Sprintf(`
|
|
<h2>Återställ ditt lösenord</h2>
|
|
<p>Du har begärt att återställa ditt lösenord.</p>
|
|
<p><a href="%s" style="background:#C96A3A;color:white;padding:12px 24px;text-decoration:none;border-radius:6px;">Återställ lösenord</a></p>
|
|
<p>Om du inte begärt detta, ignorera detta meddelande.</p>
|
|
`, resetURL+"?token="+resetToken)
|
|
|
|
return c.SendEmail(
|
|
to,
|
|
"Återställ ditt lösenord",
|
|
htmlBody,
|
|
"Klicka på länken för att återställa ditt lösenord: "+resetURL+"?token="+resetToken,
|
|
)
|
|
}
|
|
|
|
func (c *Client) send(email Email) error {
|
|
payload, err := json.Marshal(email)
|
|
if err != nil {
|
|
return fmt.Errorf("marshal email: %w", err)
|
|
}
|
|
|
|
req, err := http.NewRequest("POST", resendAPIURL, bytes.NewReader(payload))
|
|
if err != nil {
|
|
return fmt.Errorf("create request: %w", err)
|
|
}
|
|
|
|
req.Header.Set("Authorization", "Bearer "+c.apiKey)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
|
|
resp, err := c.httpClient.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("send request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
|
|
var errResp struct {
|
|
Error string `json:"error"`
|
|
Status int `json:"status"`
|
|
}
|
|
if err := json.NewDecoder(resp.Body).Decode(&errResp); err != nil {
|
|
return fmt.Errorf("resend API error (status %d)", resp.StatusCode)
|
|
}
|
|
return fmt.Errorf("resend API error: %s (status %d)", errResp.Error, resp.StatusCode)
|
|
}
|
|
|
|
return nil
|
|
}
|