package mail import ( "errors" "net/http" "strings" "github.com/jackc/pgx/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/api/query" "github.com/ultisuite/ulti-backend/internal/mail/hosted" ) func (h *Handler) SetHostedService(svc *hosted.Service) { if s, ok := h.svc.(*Service); ok { s.SetHostedService(svc) } } func (h *Handler) SetHostedPlatformDomain(domain string) { h.hostedPlatformDomain = strings.ToLower(strings.TrimSpace(domain)) } func (h *Handler) CheckAddressAvailability(w http.ResponseWriter, r *http.Request) { svc := h.hostedService() if svc == nil { apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"available": true, "reason": "hosted_mail_disabled"}) return } local := strings.TrimSpace(r.URL.Query().Get("local")) domain := strings.TrimSpace(r.URL.Query().Get("domain")) if domain == "" { domain = strings.TrimSpace(r.URL.Query().Get("domain_name")) } if domain == "" { domain = h.hostedPlatformDomain } if local == "" || domain == "" { apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError(apivalidate.FieldDetail{ Field: "local", Message: "local and domain required", })) return } available, err := svc.IsAddressAvailable(r.Context(), domain, local) if err != nil { apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"available": false, "reason": err.Error()}) return } apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"available": available}) } func (h *Handler) GetHostedMailStatus(w http.ResponseWriter, r *http.Request) { svc := h.hostedService() if svc == nil || !svc.Available() { apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"enabled": false}) return } ctx := r.Context() domainName := h.hostedPlatformDomain domainStatus := "" if domain, err := svc.GetPlatformDomain(ctx); err == nil { domainName = domain.Name domainStatus = domain.Status } if domainName == "" { apiresponse.WriteJSON(w, http.StatusOK, map[string]any{"enabled": false}) return } resp := map[string]any{ "enabled": true, "platform_domain": domainName, "domain_status": domainStatus, "endpoints": svc.Endpoints(), } claims := middleware.ClaimsFromContext(ctx) if claims != nil { userID, err := h.svc.ResolveUserID(ctx, claims.Sub) if err == nil { if mailbox, err := svc.GetUserMailbox(ctx, userID); err == nil { resp["mailbox"] = mailbox } else if !errors.Is(err, pgx.ErrNoRows) { h.logger.Error("hosted mailbox lookup", "error", err) } accounts, listErr := h.svc.ListAccounts(ctx, claims.Sub, query.ListParams{}) if listErr == nil { for _, acct := range accounts.Accounts { provider, _ := acct["provider"].(string) if provider == "hosted" { resp["hosted_mail_account_id"] = acct["id"] resp["hosted_mail_account_email"] = acct["email"] break } } } } } apiresponse.WriteJSON(w, http.StatusOK, resp) } type setupHostedMailboxRequest struct { LocalPart string `json:"local_part"` Password string `json:"password"` DisplayName string `json:"display_name"` } func (h *Handler) SetupHostedMailbox(w http.ResponseWriter, r *http.Request) { svc := h.hostedService() if svc == nil || !svc.Available() { apiresponse.WriteError(w, r, http.StatusServiceUnavailable, "hosted_mail_disabled", "hosted mail is not configured", nil) return } var req setupHostedMailboxRequest if err := apivalidate.DecodeJSON(w, r, 16*1024, &req); err != nil { return } localPart := strings.TrimSpace(req.LocalPart) password := req.Password if localPart == "" { apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError( apivalidate.FieldDetail{Field: "local_part", Message: "local part required"}, )) return } if len(password) < 8 { apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError( apivalidate.FieldDetail{Field: "password", Message: "password must be at least 8 characters"}, )) return } domainName := h.hostedPlatformDomain if domain, err := svc.GetPlatformDomain(r.Context()); err == nil { domainName = domain.Name } if domainName == "" { apiresponse.WriteError(w, r, http.StatusServiceUnavailable, "hosted_mail_disabled", "platform mail domain not configured", nil) return } claims := middleware.ClaimsFromContext(r.Context()) userID, err := h.svc.ResolveUserID(r.Context(), claims.Sub) if err != nil { apiresponse.WriteError(w, r, http.StatusUnauthorized, apiresponse.CodeAuthUnauthorized, "user not provisioned", nil) return } displayName := strings.TrimSpace(req.DisplayName) if displayName == "" { displayName = localPart } email := strings.ToLower(localPart + "@" + domainName) result, err := svc.EnsureMailboxProvisioned(r.Context(), hosted.ProvisionMailboxInput{ UserID: userID, Email: email, DisplayName: displayName, Password: password, }) if err != nil { switch { case errors.Is(err, hosted.ErrAddressTaken): apiresponse.WriteError(w, r, http.StatusConflict, "address_taken", err.Error(), nil) case errors.Is(err, hosted.ErrInvalidLocalPart): apivalidate.WriteValidationError(w, r, apivalidate.NewValidationError( apivalidate.FieldDetail{Field: "local_part", Message: err.Error()}, )) case errors.Is(err, hosted.ErrDomainNotActive): apiresponse.WriteError(w, r, http.StatusConflict, "domain_not_active", err.Error(), nil) default: h.logger.Error("setup hosted mailbox", "error", err, "email", email) apiresponse.WriteError(w, r, http.StatusInternalServerError, apiresponse.CodeInternal, "failed to provision mailbox", nil) } return } if h.accountSync != nil && result.MailAccountID != "" { if syncErr := h.accountSync.SyncAccountForUser(r.Context(), claims.Sub, result.MailAccountID); syncErr != nil { h.logger.Warn("hosted mailbox initial sync", "error", syncErr, "account_id", result.MailAccountID) } } apiresponse.WriteJSON(w, http.StatusOK, map[string]any{ "mailbox": result.Mailbox, "mail_account_id": result.MailAccountID, "email": email, }) } func (h *Handler) hostedService() *hosted.Service { if s, ok := h.svc.(*Service); ok { return s.HostedService() } return nil }