package connect import ( "context" "fmt" "strings" "time" "github.com/emersion/go-imap/v2/imapclient" "github.com/emersion/go-sasl" gosmtp "github.com/emersion/go-smtp" "github.com/ultisuite/ulti-backend/internal/mail/credentials" ) type ServerConfig struct { IMAPHost string IMAPPort int IMAPTLS bool SMTPHost string SMTPPort int SMTPTLS bool } type TestResult struct { OK bool `json:"ok"` IMAP bool `json:"imap_ok"` IMAPError string `json:"imap_error,omitempty"` SMTP bool `json:"smtp_ok"` SMTPError string `json:"smtp_error,omitempty"` } func Test(ctx context.Context, cfg ServerConfig, cred credentials.Credential) TestResult { result := TestResult{} if cfg.IMAPHost != "" { if err := testIMAP(ctx, cfg, cred); err != nil { result.IMAPError = err.Error() } else { result.IMAP = true } } if cfg.SMTPHost != "" { if err := testSMTP(ctx, cfg, cred); err != nil { result.SMTPError = err.Error() } else { result.SMTP = true } } result.OK = (cfg.IMAPHost == "" || result.IMAP) && (cfg.SMTPHost == "" || result.SMTP) return result } func testIMAP(ctx context.Context, cfg ServerConfig, cred credentials.Credential) error { ctx, cancel := context.WithTimeout(ctx, 12*time.Second) defer cancel() addr := fmt.Sprintf("%s:%d", cfg.IMAPHost, cfg.IMAPPort) var client *imapclient.Client var err error if cfg.IMAPTLS { client, err = imapclient.DialTLS(addr, &imapclient.Options{}) } else { client, err = imapclient.DialStartTLS(addr, &imapclient.Options{}) } if err != nil { return fmt.Errorf("connexion IMAP: %w", err) } defer client.Close() if err := authenticateIMAP(client, cred); err != nil { return fmt.Errorf("authentification IMAP: %w", err) } if err := client.Noop().Wait(); err != nil { return fmt.Errorf("IMAP NOOP: %w", err) } return nil } func authenticateIMAP(client *imapclient.Client, cred credentials.Credential) error { if cred.IsOAuth() { saslClient := sasl.NewOAuthBearerClient(&sasl.OAuthBearerOptions{ Username: cred.Username, Token: cred.AccessToken, }) if err := client.Authenticate(saslClient); err != nil { return err } return nil } return client.Login(cred.Username, cred.Password).Wait() } func testSMTP(ctx context.Context, cfg ServerConfig, cred credentials.Credential) error { ctx, cancel := context.WithTimeout(ctx, 12*time.Second) defer cancel() addr := fmt.Sprintf("%s:%d", cfg.SMTPHost, cfg.SMTPPort) auth, err := smtpAuth(cred) if err != nil { return err } done := make(chan error, 1) go func() { var c *gosmtp.Client var dialErr error if cfg.SMTPTLS { c, dialErr = gosmtp.DialTLS(addr, nil) } else { c, dialErr = gosmtp.Dial(addr) } if dialErr != nil { done <- dialErr return } defer c.Close() if auth != nil { if err := c.Auth(auth); err != nil { done <- err return } } done <- c.Noop() }() select { case <-ctx.Done(): return fmt.Errorf("connexion SMTP: délai dépassé") case err := <-done: if err != nil { return fmt.Errorf("connexion SMTP: %w", err) } return nil } } func smtpAuth(cred credentials.Credential) (sasl.Client, error) { if cred.IsOAuth() { return sasl.NewOAuthBearerClient(&sasl.OAuthBearerOptions{ Username: cred.Username, Token: cred.AccessToken, }), nil } if cred.Username == "" || cred.Password == "" { return nil, fmt.Errorf("identifiants incomplets") } return sasl.NewPlainClient("", cred.Username, cred.Password), nil } // SanitizeError shortens provider errors for API responses. func SanitizeError(msg string) string { msg = strings.TrimSpace(msg) if len(msg) > 240 { return msg[:240] + "…" } return msg }