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() }