- Added configuration options for Stalwart hosted mail in .env.example. - Updated Docker Compose to include Stalwart service with health checks. - Introduced new API endpoints for managing mail domains and migration projects. - Enhanced Authentik blueprints for user enrollment and post-migration security. - Updated OAuth handling for Google and Microsoft migration processes. - Improved error handling and response structures in the mail API. - Added integration tests for email claiming and migration workflows.
143 lines
3.5 KiB
Go
143 lines
3.5 KiB
Go
package migration
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
|
|
"github.com/ultisuite/ulti-backend/internal/mail/credentials"
|
|
)
|
|
|
|
type progressUpdater func(status string, cursor, stats map[string]any, jobErr string) error
|
|
|
|
type migrationUser struct {
|
|
Email string
|
|
ExternalID string
|
|
Name string
|
|
}
|
|
|
|
func resolveMigrationUser(ctx context.Context, db *pgxpool.Pool, userID string) (migrationUser, error) {
|
|
var u migrationUser
|
|
err := db.QueryRow(ctx, `
|
|
SELECT COALESCE(email, ''), COALESCE(external_id, ''), COALESCE(name, '')
|
|
FROM users WHERE id = $1::uuid
|
|
`, userID).Scan(&u.Email, &u.ExternalID, &u.Name)
|
|
if err != nil {
|
|
return migrationUser{}, fmt.Errorf("migration user not found")
|
|
}
|
|
if u.Email == "" {
|
|
return migrationUser{}, fmt.Errorf("migration user email missing")
|
|
}
|
|
return u, nil
|
|
}
|
|
|
|
func migrationHTTPClient() *http.Client {
|
|
return &http.Client{Timeout: 90 * time.Second}
|
|
}
|
|
|
|
func apiGet(ctx context.Context, client *http.Client, url, accessToken string) ([]byte, error) {
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
|
resp, err := migrationDo(ctx, client, req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
return io.ReadAll(resp.Body)
|
|
}
|
|
|
|
func alreadyImported(store *ImportedItemStore, id string) bool {
|
|
if store == nil {
|
|
return false
|
|
}
|
|
return store.Has(id)
|
|
}
|
|
|
|
func calendarSyncTokens(cursor map[string]any) map[string]string {
|
|
raw, _ := cursor["calendarSyncTokens"].(map[string]any)
|
|
out := make(map[string]string, len(raw))
|
|
for k, v := range raw {
|
|
if s, ok := v.(string); ok && s != "" {
|
|
out[k] = s
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func setCalendarSyncToken(cursor map[string]any, calID, token string) {
|
|
if calID == "" || token == "" {
|
|
return
|
|
}
|
|
raw, _ := cursor["calendarSyncTokens"].(map[string]any)
|
|
if raw == nil {
|
|
raw = map[string]any{}
|
|
cursor["calendarSyncTokens"] = raw
|
|
}
|
|
raw[calID] = token
|
|
}
|
|
|
|
func calendarDeltaLinks(cursor map[string]any) map[string]string {
|
|
raw, _ := cursor["calendarDeltaLinks"].(map[string]any)
|
|
out := make(map[string]string, len(raw))
|
|
for k, v := range raw {
|
|
if s, ok := v.(string); ok && s != "" {
|
|
out[k] = s
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func setCalendarDeltaLink(cursor map[string]any, calID, link string) {
|
|
if calID == "" || link == "" {
|
|
return
|
|
}
|
|
raw, _ := cursor["calendarDeltaLinks"].(map[string]any)
|
|
if raw == nil {
|
|
raw = map[string]any{}
|
|
cursor["calendarDeltaLinks"] = raw
|
|
}
|
|
raw[calID] = link
|
|
}
|
|
|
|
func migrationContactPath(bookPath, provider, sourceID string) string {
|
|
uid := sanitizeMigrationUID(provider, sourceID)
|
|
return bookPath + uid + ".vcf"
|
|
}
|
|
|
|
func migrationEventPath(calPath, provider, sourceID string) string {
|
|
uid := sanitizeMigrationUID(provider, sourceID)
|
|
return calPath + uid + ".ics"
|
|
}
|
|
|
|
func sanitizeMigrationUID(provider, sourceID string) string {
|
|
sourceID = strings.TrimSpace(sourceID)
|
|
sourceID = strings.ReplaceAll(sourceID, "/", "-")
|
|
return provider + "-" + sourceID + "@ultimail.migrated"
|
|
}
|
|
|
|
func applyOAuthToken(cred credentials.Credential, token *oauthToken) credentials.Credential {
|
|
cred.AuthType = credentials.AuthOAuth2
|
|
cred.AccessToken = token.AccessToken
|
|
if token.RefreshToken != "" {
|
|
cred.RefreshToken = token.RefreshToken
|
|
}
|
|
if !token.Expiry.IsZero() {
|
|
cred.Expiry = token.Expiry.UTC()
|
|
}
|
|
return cred
|
|
}
|
|
|
|
type oauthToken struct {
|
|
AccessToken string
|
|
RefreshToken string
|
|
Expiry time.Time
|
|
}
|