email kinda working

This commit is contained in:
Lennart J. Kurzweg (Nx2)
2026-03-30 22:18:58 +02:00
parent 057ba02865
commit ce78c6e07f
6 changed files with 375 additions and 3 deletions

119
internal/backend/email.go Normal file
View File

@@ -0,0 +1,119 @@
package backend
import (
"bytes"
"crypto/tls"
"fmt"
"log"
"net/smtp"
"net/url"
"strings"
"github.com/emersion/go-ical"
)
// sendInvitation sends an iMIP (RFC 6047) invitation email.
// It includes a plain-text fallback and a METHOD:REQUEST iCalendar attachment
// that calendar clients (Thunderbird, Apple, etc.) will recognize.
func (b *DBBackend) sendInvitation(senderName, recipientEmail, summary, description, start, end, objectPath, originalICS string) error {
fromAddr := fmt.Sprintf("%s@%s", senderName, b.emailDomain)
if b.smtp.User != "" {
fromAddr = b.smtp.User
}
fromHeader := fmt.Sprintf("%s <%s>", senderName, fromAddr)
baseURL := strings.TrimSuffix(b.publicURL, "/")
acceptURL := fmt.Sprintf("%s/respond?path=%s&attendee=%s&status=ACCEPTED", baseURL, url.QueryEscape(objectPath), url.QueryEscape(recipientEmail))
declineURL := fmt.Sprintf("%s/respond?path=%s&attendee=%s&status=DECLINED", baseURL, url.QueryEscape(objectPath), url.QueryEscape(recipientEmail))
// 1. Prepare plain-text fallback - with prominent links
textPart := "PLEASE RESPOND TO THIS INVITATION:\r\n"
textPart += fmt.Sprintf("✅ ACCEPT: %s\r\n", acceptURL)
textPart += fmt.Sprintf("❌ DECLINE: %s\r\n", declineURL)
textPart += "\r\n------------------------------------------\r\n\r\n"
textPart += fmt.Sprintf("You have been invited to an event by %s.\r\n\r\n", senderName)
textPart += fmt.Sprintf("Event: %s\r\n", summary)
// 2. Prepare iCalendar part with METHOD:REQUEST
var icsContent string
calendar, err := ical.NewDecoder(strings.NewReader(originalICS)).Decode()
if err == nil {
calendar.Props.SetText("METHOD", "REQUEST")
// Discourage clients from sending their own response emails
for _, event := range calendar.Events() {
for _, attendee := range event.Props["ATTENDEE"] {
attendee.Params.Set("RSVP", "FALSE")
}
}
var buf bytes.Buffer
if err := ical.NewEncoder(&buf).Encode(calendar); err == nil {
icsContent = buf.String()
}
}
if icsContent == "" {
icsContent = originalICS // Fallback to raw if decoding failed
}
// 3. Construct Multipart MIME Email
boundary := "nxcaldav_invite_boundary"
subject := fmt.Sprintf("Invitation: %s", summary)
header := fmt.Sprintf("Subject: %s\r\n", subject)
header += fmt.Sprintf("From: %s\r\n", fromHeader)
header += fmt.Sprintf("To: %s\r\n", recipientEmail)
header += "MIME-Version: 1.0\r\n"
header += fmt.Sprintf("Content-Type: multipart/mixed; boundary=\"%s\"\r\n", boundary)
header += "\r\n"
body := fmt.Sprintf("--%s\r\n", boundary)
body += "Content-Type: text/plain; charset=UTF-8\r\n"
body += "Content-Transfer-Encoding: 7bit\r\n"
body += "\r\n"
body += textPart + "\r\n"
body += fmt.Sprintf("--%s\r\n", boundary)
body += "Content-Type: text/calendar; method=REQUEST; charset=UTF-8\r\n"
body += "Content-Transfer-Encoding: 7bit\r\n"
body += "\r\n"
body += icsContent + "\r\n"
body += fmt.Sprintf("--%s--\r\n", boundary)
// 4. Send the mail
addr := fmt.Sprintf("%s:%d", b.smtp.Host, b.smtp.Port)
tlsConfig := &tls.Config{InsecureSkipVerify: true, ServerName: b.smtp.Host}
var c *smtp.Client
if b.smtp.Port == 465 {
conn, err := tls.Dial("tcp", addr, tlsConfig)
if err != nil { return err }
c, err = smtp.NewClient(conn, b.smtp.Host)
} else {
c, err = smtp.Dial(addr)
}
if err != nil { return err }
defer c.Close()
if err = c.Hello("localhost"); err != nil { return err }
if b.smtp.Port != 465 {
if ok, _ := c.Extension("STARTTLS"); ok {
if err = c.StartTLS(tlsConfig); err != nil { return err }
}
}
if b.smtp.User != "" && b.smtp.Password != "" {
auth := smtp.PlainAuth("", b.smtp.User, b.smtp.Password, b.smtp.Host)
if err = c.Auth(auth); err != nil { return err }
}
if err = c.Mail(fromAddr); err != nil { return err }
if err = c.Rcpt(recipientEmail); err != nil { return err }
w, err := c.Data()
if err != nil { return err }
_, err = w.Write([]byte(header + body))
if err != nil { return err }
err = w.Close()
if err != nil { return err }
log.Printf("[email] Successfully sent iMIP invitation to %s", recipientEmail)
return c.Quit()
}