package smtp2shoutrrr import ( "fmt" "io" "log/slog" "net/mail" "net/url" "slices" "strings" "github.com/containrrr/shoutrrr" "github.com/emersion/go-sasl" "github.com/emersion/go-smtp" ) type Backend struct { config *Config } func (bkd *Backend) sendNotification(recipient ConfigRecipient, email ReceivedEmail) error { if recipient.Target == "" { slog.Warn("no target provided for recipient", slog.String("recipient", strings.Join(recipient.Addresses, ","))) return nil } urlParams := url.Values{ "title": {email.Msg.Header.Get("Subject")}, } destinationURL := recipient.GetTargetURL() destinationURL.RawQuery = urlParams.Encode() body, err := email.Body() if err != nil { slog.Error("Error getting email body", slog.String("err", err.Error())) return fmt.Errorf("failed to get email body: %w", err) } if err := shoutrrr.Send(destinationURL.String(), body); err != nil { slog.Error("Error sending message", slog.String("err", err.Error())) return fmt.Errorf("failed to send notification: %w", err) } return nil } func (bkd *Backend) NewSession(c *smtp.Conn) (smtp.Session, error) { return &Session{ forwarderFunc: bkd.forwardEmail, config: bkd.config, }, nil } func (bkd *Backend) forwardEmail(email ReceivedEmail) error { slog.Info("forwading message", slog.String("to", strings.Join(email.Recipients, ","))) // Try to match configured recipients first matched := false for _, r := range bkd.config.Recipients { for _, a := range email.Recipients { if slices.Contains(r.Addresses, a) { if err := bkd.sendNotification(r, email); err != nil { return err } matched = true break } } if matched { break } } // If no recipient matched and catch-all is configured, use it if !matched && bkd.config.CatchAll != nil { slog.Info("using catch-all recipient for unmatched email") if err := bkd.sendNotification(*bkd.config.CatchAll, email); err != nil { return err } } return nil } type Session struct { addresses []string config *Config forwarderFunc func(ReceivedEmail) error } func (s *Session) AuthMechanisms() []string { return []string{sasl.Plain} } func (s *Session) Auth(mech string) (sasl.Server, error) { return sasl.NewPlainServer(func(identity, username, password string) error { if username != s.config.Username && password != s.config.Password { return fmt.Errorf("invalid credentials") } return nil }), nil } func (s *Session) Mail(from string, opts *smtp.MailOptions) error { slog.Debug("Mail from", slog.String("from", from)) return nil } func (s *Session) Rcpt(to string, opts *smtp.RcptOptions) error { slog.Debug("Rcpt to", slog.String("to", to)) s.addresses = append(s.addresses, to) return nil } func (s *Session) Data(r io.Reader) error { msg, err := mail.ReadMessage(r) if err != nil { slog.Error("Error reading data", slog.String("err", err.Error())) return fmt.Errorf("Error reading data: %w", err) } slog.Info("Received email", slog.String("destination", strings.Join(s.addresses, ","))) if err := s.forwarderFunc(ReceivedEmail{ Recipients: s.addresses, Msg: msg, }); err != nil { slog.Error("Error forwarding email", slog.String("err", err.Error())) } return nil } func (s *Session) Reset() {} func (s *Session) Logout() error { return nil }