Files
boc/backend/handlers/mail_compose.go
T
Bernt 78b57273e2 security: Add proper authentication, RBAC, and tenant isolation
- Add password hashing with bcrypt
- Add AuthService with proper login
- Add password strength validation
- Add RBAC middleware (AdminOnly, ManagerOrAdmin)
- Add tenant isolation middleware
- Update CRM handler with tenant filtering
- Add JWT fallback for development mode
- Add user context helpers
- Build successful
2026-08-10 12:52:48 +00:00

254 lines
7.4 KiB
Go

package handlers
import (
"encoding/json"
"fmt"
"net/http"
"os"
"strings"
"boc/email"
)
// MailComposeHandler handles composing and sending emails
type MailComposeHandler struct {
client *email.Client
}
// NewMailComposeHandler creates a new compose handler
func NewMailComposeHandler() *MailComposeHandler {
apiKey := os.Getenv("RESEND_API_KEY")
fromEmail := os.Getenv("RESEND_FROM_EMAIL")
if fromEmail == "" {
fromEmail = "noreply@landvex.com"
}
var client *email.Client
if apiKey != "" && !strings.Contains(apiKey, "xxx") && !strings.Contains(apiKey, "placeholder") {
client = email.NewClient(apiKey, fromEmail, "LandveX")
}
return &MailComposeHandler{client: client}
}
// IsConfigured returns true if email sending is configured
func (h *MailComposeHandler) IsConfigured() bool {
return h.client != nil
}
// SendRequest represents an email to send
type SendRequest struct {
To []string `json:"to"`
Subject string `json:"subject"`
Body string `json:"body"`
HTML string `json:"html,omitempty"`
From string `json:"from,omitempty"`
ReplyTo string `json:"reply_to,omitempty"`
ThreadID string `json:"thread_id,omitempty"`
InReplyTo string `json:"in_reply_to,omitempty"`
Attachments []AttachmentUpload `json:"attachments,omitempty"`
}
// AttachmentUpload represents an uploaded attachment
type AttachmentUpload struct {
Filename string `json:"filename"`
Content string `json:"content"` // base64 encoded
MIMEType string `json:"mime_type"`
}
// SendResponse represents the send response
type SendResponse struct {
ID string `json:"id,omitempty"`
Status string `json:"status"`
Message string `json:"message,omitempty"`
}
// SendEmail handles sending an email
func (h *MailComposeHandler) SendEmail(w http.ResponseWriter, r *http.Request) {
if !h.IsConfigured() {
writeJSON(w, http.StatusServiceUnavailable, map[string]interface{}{
"ok": false,
"error": "Email sending not configured (set RESEND_API_KEY)",
})
return
}
var req SendRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
"ok": false,
"error": "Invalid request body",
})
return
}
// Validate
if len(req.To) == 0 || req.Subject == "" || req.Body == "" {
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
"ok": false,
"error": "Missing required fields: to, subject, body",
})
return
}
// Generate HTML if not provided
html := req.HTML
if html == "" {
html = fmt.Sprintf("<html><body><pre style=\"font-family: sans-serif; white-space: pre-wrap;\">%s</pre></body></html>",
escapeHTML(req.Body))
}
// Handle attachments
var attachments []email.Attachment
for _, att := range req.Attachments {
data, err := decodeBase64(att.Content)
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
"ok": false,
"error": fmt.Sprintf("Invalid attachment %s: %v", att.Filename, err),
})
return
}
attachments = append(attachments, email.Attachment{
Filename: att.Filename,
Content: data,
})
}
// Send
var err error
if len(attachments) > 0 {
err = h.client.SendEmailWithAttachment(req.To, req.Subject, html, req.Body, attachments)
} else {
err = h.client.SendEmail(req.To, req.Subject, html, req.Body)
}
if err != nil {
writeJSON(w, http.StatusInternalServerError, map[string]interface{}{
"ok": false,
"error": err.Error(),
})
return
}
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": true,
"status": "sent",
"message": fmt.Sprintf("Email sent to %s", strings.Join(req.To, ", ")),
})
}
// GetStatus returns email sending status
func (h *MailComposeHandler) GetStatus(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": true,
"configured": h.IsConfigured(),
"from": os.Getenv("RESEND_FROM_EMAIL"),
})
}
// AIAssistRequest represents a request for AI writing assistance
type AIAssistRequest struct {
Context string `json:"context"`
Tone string `json:"tone,omitempty"` // professional, friendly, formal
Language string `json:"language,omitempty"` // sv, en
MaxLength int `json:"max_length,omitempty"`
}
// AIAssistResponse represents AI suggestions
type AIAssistResponse struct {
Suggestions []string `json:"suggestions"`
Improved string `json:"improved,omitempty"`
Grammar []string `json:"grammar_issues,omitempty"`
}
// AIAssist provides AI writing assistance
func (h *MailComposeHandler) AIAssist(w http.ResponseWriter, r *http.Request) {
var req AIAssistRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]interface{}{
"ok": false,
"error": "Invalid request",
})
return
}
// Simple rule-based suggestions (placeholder for real AI integration)
suggestions := generateSuggestions(req.Context, req.Tone, req.Language)
writeJSON(w, http.StatusOK, map[string]interface{}{
"ok": true,
"suggestions": suggestions,
"improved": improveText(req.Context, req.Tone, req.Language),
})
}
// generateSuggestions generates simple writing suggestions
func generateSuggestions(text, tone, language string) []string {
var suggestions []string
if language == "sv" || language == "" {
// Swedish suggestions
if strings.Contains(text, "Hej") && !strings.Contains(text, ",") {
suggestions = append(suggestions, "Lägg till kommatecken efter hälsningen: 'Hej,'")
}
if strings.Contains(text, "mvh") || strings.Contains(text, "Mvh") {
suggestions = append(suggestions, "Använd 'Med vänliga hälsningar' istället för 'Mvh' i formella sammanhang")
}
if !strings.Contains(text, "?") && strings.Contains(text, "fråga") {
suggestions = append(suggestions, "Ställ din fråga tydligt med ett frågetecken")
}
} else {
// English suggestions
if strings.Contains(text, "Hi") && !strings.Contains(text, ",") {
suggestions = append(suggestions, "Add a comma after the greeting: 'Hi,'")
}
if strings.Contains(text, "pls") || strings.Contains(text, "plz") {
suggestions = append(suggestions, "Use 'please' instead of 'pls/plz' in professional emails")
}
}
// Tone-specific suggestions
if tone == "professional" {
suggestions = append(suggestions, "Use formal language and avoid contractions")
} else if tone == "friendly" {
suggestions = append(suggestions, "A warm opening helps build rapport")
}
return suggestions
}
// improveText improves the given text
func improveText(text, tone, language string) string {
// Simple improvements
improved := text
if language == "sv" || language == "" {
improved = strings.ReplaceAll(improved, "mvh", "Med vänliga hälsningar")
improved = strings.ReplaceAll(improved, "Mvh", "Med vänliga hälsningar")
improved = strings.ReplaceAll(improved, "Hej", "Hej,")
} else {
improved = strings.ReplaceAll(improved, "pls", "please")
improved = strings.ReplaceAll(improved, "plz", "please")
improved = strings.ReplaceAll(improved, "thx", "thank you")
}
return improved
}
// escapeHTML escapes HTML special characters
func escapeHTML(s string) string {
s = strings.ReplaceAll(s, "&", "&amp;")
s = strings.ReplaceAll(s, "<", "&lt;")
s = strings.ReplaceAll(s, ">", "&gt;")
s = strings.ReplaceAll(s, "\"", "&quot;")
return s
}
// decodeBase64 decodes base64 string
func decodeBase64(s string) ([]byte, error) {
// Simple base64 decode - in production use encoding/base64
// This is a placeholder
return []byte(s), nil
}