package main import ( "bytes" "crypto/tls" "encoding/json" "errors" "fmt" "io" "log" "net" "net/http" "net/smtp" "os" "path/filepath" "strings" "time" ) type Mailer struct { Host string `json:"host"` Port int `json:"port"` User string `json:"user"` Password string `json:"password"` From string `json:"from"` StartTLS bool `json:"starttls"` Insecure bool `json:"insecure"` API string `json:"api"` Key string `json:"key"` } func loadMailer(root string) *Mailer { b, err := os.ReadFile(filepath.Join(root, "smtp.json")) if err != nil { return nil } var m Mailer if json.Unmarshal(b, &m) != nil || (m.Host == "" && m.API == "") || m.From == "" { return nil } if m.Port == 0 { m.Port = 465 } return &m } func (m *Mailer) Send(to, subject, body string) error { if m == nil { return errors.New("mail is not configured") } if m.API != "" { return m.post(to, subject, body) } addr := net.JoinHostPort(m.Host, fmt.Sprint(m.Port)) tlsConfig := &tls.Config{ServerName: m.Host, InsecureSkipVerify: m.Insecure} var client *smtp.Client var err error if m.StartTLS { client, err = smtp.Dial(addr) if err == nil { err = client.StartTLS(tlsConfig) } } else if m.Insecure && m.Port != 465 { client, err = smtp.Dial(addr) } else { var conn net.Conn conn, err = tls.DialWithDialer(&net.Dialer{Timeout: 15 * time.Second}, "tcp", addr, tlsConfig) if err == nil { client, err = smtp.NewClient(conn, m.Host) } } if err != nil { return err } defer client.Close() if m.User != "" { if err := client.Auth(smtp.PlainAuth("", m.User, m.Password, m.Host)); err != nil { return err } } if err := client.Mail(m.From); err != nil { return err } if err := client.Rcpt(to); err != nil { return err } w, err := client.Data() if err != nil { return err } msg := fmt.Sprintf("From: %s\r\nTo: %s\r\nSubject: %s\r\nDate: %s\r\nMessage-ID: <%d.%s>\r\nMIME-Version: 1.0\r\nContent-Type: text/plain; charset=utf-8\r\n\r\n%s\r\n", m.From, to, headerText(subject), time.Now().Format(time.RFC1123Z), time.Now().UnixNano(), m.domain(), body) if _, err := w.Write([]byte(msg)); err != nil { return err } if err := w.Close(); err != nil { return err } return client.Quit() } func (s *Server) notify(to, subject, body string) { if to == "" || s.mailer == nil { return } go func() { if err := s.mailer.Send(to, subject, body); err != nil { log.Printf("mail to %s: %v", to, err) } }() } func (m *Mailer) domain() string { from := m.From if i := strings.LastIndex(from, "<"); i >= 0 { from = strings.TrimSuffix(from[i+1:], ">") } _, d, _ := strings.Cut(from, "@") return d } func (m *Mailer) post(to, subject, body string) error { payload, _ := json.Marshal(map[string]any{"from": m.From, "to": []string{to}, "subject": headerText(subject), "text": body}) req, err := http.NewRequest("POST", m.API, bytes.NewReader(payload)) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+m.Key) req.Header.Set("Content-Type", "application/json") resp, err := (&http.Client{Timeout: 20 * time.Second}).Do(req) if err != nil { return err } defer resp.Body.Close() if resp.StatusCode/100 != 2 { msg, _ := io.ReadAll(io.LimitReader(resp.Body, 512)) return fmt.Errorf("mail api: %s: %s", resp.Status, strings.TrimSpace(string(msg))) } return nil } func headerText(s string) string { return strings.Map(func(r rune) rune { if r < 0x20 || r == 0x7f { return ' ' } return r }, s) }