127 lines
4.2 KiB
Go
127 lines
4.2 KiB
Go
package backend
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"fmt"
|
|
"log"
|
|
"net/smtp"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"github.com/emersion/go-ical"
|
|
"nxcaldav/internal/config"
|
|
)
|
|
|
|
// 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 }
|
|
}
|
|
}
|
|
|
|
smtpPassword, err := config.ResolvePassword(b.smtp.Password, b.smtp.PasswordCmd)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to resolve SMTP password: %v", err)
|
|
}
|
|
|
|
if b.smtp.User != "" && smtpPassword != "" {
|
|
auth := smtp.PlainAuth("", b.smtp.User, smtpPassword, 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()
|
|
}
|