package mail import ( "errors" "net/http" "strings" "github.com/go-chi/chi/v5" "github.com/ultisuite/ulti-backend/internal/api/apiresponse" "github.com/ultisuite/ulti-backend/internal/api/apivalidate" "github.com/ultisuite/ulti-backend/internal/api/middleware" "github.com/ultisuite/ulti-backend/internal/mail/connect" mailoauth "github.com/ultisuite/ulti-backend/internal/mail/oauth" ) func (h *Handler) TestAccountConnection(w http.ResponseWriter, r *http.Request) { h.testAccountConnection(w, r, "") } func (h *Handler) TestStoredAccountConnection(w http.ResponseWriter, r *http.Request) { accountID := chi.URLParam(r, "accountID") if d := validateAccountUUID(accountID); d != nil { apivalidate.WriteNotFound(w, r, "not found") return } h.testAccountConnection(w, r, accountID) } func (h *Handler) testAccountConnection(w http.ResponseWriter, r *http.Request, accountIDFromURL string) { claims := middleware.ClaimsFromContext(r.Context()) var req testAccountRequest if err := apivalidate.DecodeJSON(w, r, maxAccountRequestBody, &req); err != nil { return } if accountIDFromURL != "" { req.AccountID = accountIDFromURL } if verr := validateTestAccount(&req); verr != nil { apivalidate.WriteValidationError(w, r, verr) return } cred, err := h.svc.CredentialForConnectionTest(r.Context(), claims.Sub, &req) if err != nil { if errors.Is(err, ErrNotFound) { apivalidate.WriteNotFound(w, r, "not found") return } if errors.Is(err, ErrInvalidAccountCredentials) { apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, "missing credentials for connection test", nil) return } if errors.Is(err, ErrCredentialsUnavailable) { apiresponse.WriteError(w, r, http.StatusInternalServerError, apiresponse.CodeInternal, "credentials encryption unavailable", nil) return } h.logger.Error("resolve test credentials", "error", err) apivalidate.WriteInternal(w, r) return } result := connect.Test(r.Context(), connect.ServerConfig{ IMAPHost: req.IMAPHost, IMAPPort: req.IMAPPort, IMAPTLS: req.IMAPTLS, SMTPHost: req.SMTPHost, SMTPPort: req.SMTPPort, SMTPTLS: req.SMTPTLS, }, cred) if result.IMAPError != "" { result.IMAPError = connect.SanitizeError(result.IMAPError) } if result.SMTPError != "" { result.SMTPError = connect.SanitizeError(result.SMTPError) } apiresponse.WriteJSON(w, http.StatusOK, result) } func (h *Handler) ListOAuthProviders(w http.ResponseWriter, r *http.Request) { if h.oauth == nil { apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"providers": []string{}}) return } apiresponse.WriteJSON(w, http.StatusOK, map[string]any{ "providers": h.oauth.EnabledProviders(), }) } func (h *Handler) StartOAuthAccount(w http.ResponseWriter, r *http.Request) { if h.oauth == nil { apiresponse.WriteError(w, r, http.StatusServiceUnavailable, apiresponse.CodeInternal, "mail oauth unavailable", nil) return } claims := middleware.ClaimsFromContext(r.Context()) var req oauthStartRequest if err := apivalidate.DecodeJSON(w, r, maxAccountRequestBody, &req); err != nil { return } if verr := validateOAuthStart(&req); verr != nil { apivalidate.WriteValidationError(w, r, verr) return } pending := h.oauthPendingFromRequest(&req) authURL, state, err := h.oauth.Start(r.Context(), claims.Sub, mailoauth.Provider(req.Provider), pending) if err != nil { h.logger.Error("oauth start", "error", err, "provider", req.Provider) apiresponse.WriteError(w, r, http.StatusBadRequest, apiresponse.CodeInvalidRequest, err.Error(), nil) return } apiresponse.WriteJSON(w, http.StatusOK, map[string]string{ "authorization_url": authURL, "state": state, }) } func (h *Handler) OAuthCallback(w http.ResponseWriter, r *http.Request) { if h.oauth == nil || h.svc == nil { http.Redirect(w, r, h.oauthReturnURL("error", "oauth_unavailable"), http.StatusFound) return } state := strings.TrimSpace(r.URL.Query().Get("state")) code := strings.TrimSpace(r.URL.Query().Get("code")) if errParam := r.URL.Query().Get("error"); errParam != "" { http.Redirect(w, r, h.oauthReturnURL("error", errParam), http.StatusFound) return } if state == "" || code == "" { http.Redirect(w, r, h.oauthReturnURL("error", "missing_code"), http.StatusFound) return } pending, token, err := h.oauth.Exchange(r.Context(), state, code) if err != nil { h.logger.Error("oauth exchange", "error", err) http.Redirect(w, r, h.oauthReturnURL("error", "exchange_failed"), http.StatusFound) return } cred := mailoauth.CredentialFromToken(pending.Email, pending.Provider, token) _, err = h.svc.CreateAccountWithCredential(r.Context(), pending.UserExternalID, &createAccountRequest{ Name: pending.Name, Email: pending.Email, Provider: pending.ProviderID, IMAPHost: pending.IMAPHost, IMAPPort: pending.IMAPPort, IMAPTLS: pending.IMAPTLS, SMTPHost: pending.SMTPHost, SMTPPort: pending.SMTPPort, SMTPTLS: pending.SMTPTLS, }, cred) if err != nil { h.logger.Error("oauth create account", "error", err) http.Redirect(w, r, h.oauthReturnURL("error", "create_failed"), http.StatusFound) return } http.Redirect(w, r, h.oauthReturnURL("success", ""), http.StatusFound) } func (h *Handler) oauthPendingFromRequest(req *oauthStartRequest) mailoauth.PendingAccount { name := req.Name if name == "" { name = req.Email } return mailoauth.PendingAccount{ Email: req.Email, Name: name, ProviderID: req.ProviderID, IMAPHost: req.IMAPHost, IMAPPort: req.IMAPPort, IMAPTLS: req.IMAPTLS, SMTPHost: req.SMTPHost, SMTPPort: req.SMTPPort, SMTPTLS: req.SMTPTLS, } } func (h *Handler) oauthReturnURL(status, code string) string { base := strings.TrimRight(h.appURL, "/") if base == "" { base = "http://localhost:3004" } u := base + "/mail/settings/accounts?oauth=" + status if code != "" { u += "&code=" + code } return u }