- Corrected frontend route references in README to reflect updated paths for account and settings. - Modified Nginx configuration comments to align with the new route structure, ensuring clarity for development and production setups. - Added new Nginx location blocks for handling account settings and redirecting old paths to the new structure.
197 lines
5.8 KiB
Go
197 lines
5.8 KiB
Go
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 + "/settings/accounts?oauth=" + status
|
|
if code != "" {
|
|
u += "&code=" + code
|
|
}
|
|
return u
|
|
}
|