ultisuite-backend/internal/mail/connect/test.go
2026-05-24 00:03:36 +02:00

158 lines
3.6 KiB
Go

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
}