From 94d5561e7cc39eb2909bdc36d4ef4972cd21e56d Mon Sep 17 00:00:00 2001 From: Bobby Date: Sun, 8 Mar 2026 23:38:54 +0530 Subject: Refactor DNS and SMTP configurations; add system DNS management - Updated DNS server address configuration to use BindAddress and DnsPort. - Enhanced email submission to utilize BindAddress for SMTP server address. - Improved error messages for unknown recipient domains. - Introduced a new OrderedMap structure for route management. - Added system DNS management functions for Linux, Darwin, and Windows platforms. - Created new dashboard services for DNS configuration and overview. - Updated UI to include Proxy Rules section and improved descriptions. - Added new utility functions for handling DNS configurations. --- services/dashboard/overview.go | 417 +++++++++++++++++++++++++++++++++++++++++ services/dns/messages.go | 2 - services/dns/records.go | 47 ++--- services/domain/domain.go | 65 ++++++- services/mail/mailboxes.go | 21 --- 5 files changed, 488 insertions(+), 64 deletions(-) create mode 100644 services/dashboard/overview.go (limited to 'services') diff --git a/services/dashboard/overview.go b/services/dashboard/overview.go new file mode 100644 index 0000000..e073bf8 --- /dev/null +++ b/services/dashboard/overview.go @@ -0,0 +1,417 @@ +package dashboard + +import ( + "dove/config" + "fmt" + "math" + "math/rand" + "strings" + + domainRepo "dove/repositories/domain" + mailRepo "dove/repositories/mail" + dnsSystem "dove/utils/dns" +) + +type ServiceStatus struct { + Name string `json:"name"` + Port string `json:"port"` + Protocol string `json:"protocol"` + Active bool `json:"active"` +} + +type SparklineData struct { + LinePath string `json:"line_path"` + FillPath string `json:"fill_path"` +} + +type EmailCard struct { + Sparkline24h SparklineData `json:"sparkline_24h"` + Sparkline7d SparklineData `json:"sparkline_7d"` + Sparkline30d SparklineData `json:"sparkline_30d"` + TotalMailboxes int64 `json:"total_mailboxes"` + TotalDelivered int `json:"total_delivered"` + TotalBounced int `json:"total_bounced"` + BounceRate string `json:"bounce_rate"` + TotalMessages int64 `json:"total_messages"` +} + +type DomainStatusEntry struct { + Name string `json:"name"` + StatusOk int `json:"status_ok"` + StatusErr int `json:"status_err"` + StatusWarn int `json:"status_warn"` + Total int `json:"total"` + PercentOk string `json:"percent_ok"` + PercentWarn string `json:"percent_warn"` + PercentErr string `json:"percent_err"` +} + +type DomainsCard struct { + Domains []DomainStatusEntry `json:"domains"` + Selected string `json:"selected"` +} + +type ProxyServiceEntry struct { + Name string `json:"name"` + Target string `json:"target"` + Up bool `json:"up"` +} + +type ReverseProxyCard struct { + Services []ProxyServiceEntry `json:"services"` +} + +type StorageBucketEntry struct { + Name string `json:"name"` + UsageBytes int64 `json:"usage_bytes"` + UsageLabel string `json:"usage_label"` + Percentage float64 `json:"percentage"` +} + +type ObjectStorageCard struct { + Buckets []StorageBucketEntry `json:"buckets"` + TotalUsage string `json:"total_usage"` +} + +type CronFailureEntry struct { + JobName string `json:"job_name"` + Domain string `json:"domain"` + FailedAt string `json:"failed_at"` + ErrorText string `json:"error_text"` +} + +type CronJobsCard struct { + Failures []CronFailureEntry `json:"failures"` + TotalJobs int `json:"total_jobs"` + TotalRuns int `json:"total_runs"` + FailCount int `json:"fail_count"` +} + +type DoveStatusEntry struct { + Route string `json:"route"` + Method string `json:"method"` + StatusCode int `json:"status_code"` + Count int `json:"count"` +} + +type DoveCard struct { + StatusGroups map[string]int `json:"status_groups"` + RecentRoutes []DoveStatusEntry `json:"recent_routes"` + TotalRoutes int `json:"total_routes"` +} + +type DnsCard struct { + Configured bool `json:"configured"` + Address string `json:"address"` + Platform string `json:"platform"` +} + +type OverviewResponse struct { + BindAddress string `json:"bind_address"` + DnsAddress string `json:"dns_address"` + Services []ServiceStatus `json:"services"` + Email EmailCard `json:"email"` + Domains DomainsCard `json:"domains"` + ReverseProxy ReverseProxyCard `json:"reverse_proxy"` + Storage ObjectStorageCard `json:"storage"` + CronJobs CronJobsCard `json:"cron_jobs"` + Dove DoveCard `json:"dove"` + Dns DnsCard `json:"dns"` +} + +func Overview() OverviewResponse { + return OverviewResponse{ + BindAddress: config.BindAddress, + DnsAddress: fmt.Sprintf("%s:%d", config.BindAddress, config.DnsPort), + Services: buildServiceStatuses(), + Email: buildEmailCard(), + Domains: buildDomainsCard(), + ReverseProxy: buildReverseProxyCard(), + Storage: buildObjectStorageCard(), + CronJobs: buildCronJobsCard(), + Dove: buildDoveCard(), + Dns: buildDnsCard(), + } +} + +func buildServiceStatuses() []ServiceStatus { + return []ServiceStatus{ + {Name: "HTTP", Port: fmt.Sprintf("%d", config.HttpPort), Protocol: "Plain", Active: true}, + {Name: "HTTPS", Port: "443", Protocol: "TLS", Active: false}, + {Name: "DNS", Port: fmt.Sprintf("%d", config.DnsPort), Protocol: "UDP/TCP", Active: true}, + {Name: "SMTP", Port: fmt.Sprintf("%d", config.SmtpPort), Protocol: "Plain/STARTTLS", Active: true}, + {Name: "SMTPS", Port: fmt.Sprintf("%d", config.SmtpsPort), Protocol: "TLS", Active: true}, + {Name: "SMTP (MSA)", Port: fmt.Sprintf("%d", config.SubmissionPort), Protocol: "Plain/STARTTLS", Active: true}, + {Name: "IMAP", Port: fmt.Sprintf("%d", config.ImapPort), Protocol: "Plain/STARTTLS", Active: false}, + {Name: "IMAPS", Port: fmt.Sprintf("%d", config.ImapsPort), Protocol: "TLS", Active: false}, + {Name: "POP3", Port: fmt.Sprintf("%d", config.Pop3Port), Protocol: "Plain/STARTTLS", Active: false}, + {Name: "POP3S", Port: fmt.Sprintf("%d", config.Pop3sPort), Protocol: "TLS", Active: false}, + {Name: "S3", Port: fmt.Sprintf("%d", config.S3Port), Protocol: "Plain", Active: false}, + } +} + +const SparklineResolution = 100 + +func buildEmailCard() EmailCard { + mailboxCount := mailRepo.CountMailboxes() + emailCount := mailRepo.CountEmails() + delivered := 150 + rand.Intn(500) + bounced := rand.Intn(20) + total := delivered + bounced + bounceRate := 0.0 + if total > 0 { + bounceRate = float64(bounced) / float64(total) * 100 + } + + return EmailCard{ + Sparkline24h: generateSparkline(24, 3, 40), + Sparkline7d: generateSparkline(28, 10, 80), + Sparkline30d: generateSparkline(30, 30, 200), + TotalMailboxes: mailboxCount, + TotalDelivered: delivered, + TotalBounced: bounced, + BounceRate: fmt.Sprintf("%.1f", math.Round(bounceRate*10)/10), + TotalMessages: emailCount, + } +} + +func buildDomainsCard() DomainsCard { + allDomains := domainRepo.AllDomains() + entries := make([]DomainStatusEntry, 0, len(allDomains)) + + for _, domain := range allDomains { + tld := "" + if domain.TLD.Name != "" { + tld = "." + domain.TLD.Name + } + fullName := domain.Name + tld + statusOk := 200 + rand.Intn(800) + statusWarn := rand.Intn(50) + statusErr := rand.Intn(30) + total := statusOk + statusWarn + statusErr + percentOk := 0.0 + percentWarn := 0.0 + percentErr := 0.0 + if total > 0 { + percentOk = math.Round(float64(statusOk)/float64(total)*1000) / 10 + percentWarn = math.Round(float64(statusWarn)/float64(total)*1000) / 10 + percentErr = math.Round(float64(statusErr)/float64(total)*1000) / 10 + } + entries = append(entries, DomainStatusEntry{ + Name: fullName, + StatusOk: statusOk, + StatusErr: statusErr, + StatusWarn: statusWarn, + Total: total, + PercentOk: fmt.Sprintf("%.1f", percentOk), + PercentWarn: fmt.Sprintf("%.1f", percentWarn), + PercentErr: fmt.Sprintf("%.1f", percentErr), + }) + } + + selected := "" + if len(entries) > 0 { + selected = entries[0].Name + } + + return DomainsCard{ + Domains: entries, + Selected: selected, + } +} + +func buildReverseProxyCard() ReverseProxyCard { + return ReverseProxyCard{ + Services: []ProxyServiceEntry{ + {Name: "frontend", Target: "127.0.0.1:3001", Up: true}, + {Name: "api", Target: "127.0.0.1:8080", Up: true}, + {Name: "worker", Target: "127.0.0.1:9090", Up: false}, + {Name: "docs", Target: "127.0.0.1:4000", Up: true}, + {Name: "websocket", Target: "127.0.0.1:8443", Up: false}, + }, + } +} + +func buildObjectStorageCard() ObjectStorageCard { + buckets := []StorageBucketEntry{ + generateBucket("media-uploads", 1, 4), + generateBucket("backups", 500, 2000), + generateBucket("thumbnails", 50, 500), + generateBucket("exports", 10, 200), + } + + var totalBytes int64 + var maxBytes int64 + for _, bucket := range buckets { + totalBytes += bucket.UsageBytes + if bucket.UsageBytes > maxBytes { + maxBytes = bucket.UsageBytes + } + } + + for index := range buckets { + buckets[index].Percentage = float64(buckets[index].UsageBytes) / float64(maxBytes) * 100 + } + + return ObjectStorageCard{ + Buckets: buckets, + TotalUsage: formatBytes(totalBytes), + } +} + +func buildCronJobsCard() CronJobsCard { + totalJobs := 5 + rand.Intn(8) + totalRuns := 20 + rand.Intn(80) + failures := []CronFailureEntry{ + {JobName: "cleanup-temp", Domain: "api.dove", FailedAt: "2m ago", ErrorText: "Connection refused on :8080"}, + {JobName: "sync-storage", Domain: "cdn.dove", FailedAt: "18m ago", ErrorText: "Bucket not found: staging"}, + {JobName: "health-ping", Domain: "monitor.dove", FailedAt: "1h ago", ErrorText: "Timeout after 30s"}, + } + + return CronJobsCard{ + Failures: failures, + TotalJobs: totalJobs, + TotalRuns: totalRuns, + FailCount: len(failures), + } +} + +func buildDoveCard() DoveCard { + statusGroups := map[string]int{ + "2xx": 800 + rand.Intn(2000), + "3xx": 50 + rand.Intn(200), + "4xx": 10 + rand.Intn(50), + "5xx": rand.Intn(10), + } + + recentRoutes := []DoveStatusEntry{ + {Route: "/dashboard", Method: "GET", StatusCode: 200, Count: 120 + rand.Intn(200)}, + {Route: "/domains", Method: "GET", StatusCode: 200, Count: 80 + rand.Intn(150)}, + {Route: "/mail/webmail", Method: "GET", StatusCode: 200, Count: 60 + rand.Intn(100)}, + {Route: "/domains/records", Method: "POST", StatusCode: 201, Count: 15 + rand.Intn(30)}, + {Route: "/auth/login", Method: "POST", StatusCode: 302, Count: 10 + rand.Intn(20)}, + {Route: "/mail/compose", Method: "POST", StatusCode: 500, Count: rand.Intn(5)}, + {Route: "/domains/manage", Method: "DELETE", StatusCode: 404, Count: rand.Intn(8)}, + } + + return DoveCard{ + StatusGroups: statusGroups, + RecentRoutes: recentRoutes, + TotalRoutes: len(recentRoutes), + } +} + +func generateBucket(name string, minMegabytes int, maxMegabytes int) StorageBucketEntry { + megabytes := int64(minMegabytes) + int64(rand.Intn(maxMegabytes-minMegabytes)) + bytes := megabytes * 1024 * 1024 + return StorageBucketEntry{ + Name: name, + UsageBytes: bytes, + UsageLabel: formatBytes(bytes), + } +} + +func generateSparkline(anchorCount int, minValue float64, maxValue float64) SparklineData { + viewboxWidth := 200.0 + viewboxHeight := 40.0 + padding := 2.0 + + anchorValues := make([]float64, anchorCount) + currentValue := minValue + (maxValue-minValue)*0.3 + rand.Float64()*(maxValue-minValue)*0.4 + + for index := range anchorValues { + delta := (rand.Float64() - 0.45) * (maxValue - minValue) * 0.25 + currentValue += delta + currentValue = math.Max(minValue, math.Min(maxValue, currentValue)) + anchorValues[index] = currentValue + } + + smoothValues := catmullRomResample(anchorValues, SparklineResolution) + + minPoint := smoothValues[0] + maxPoint := smoothValues[0] + for _, point := range smoothValues { + if point < minPoint { + minPoint = point + } + if point > maxPoint { + maxPoint = point + } + } + + pointRange := maxPoint - minPoint + if pointRange == 0 { + pointRange = 1 + } + + lineCoordinates := make([]string, SparklineResolution) + for index, point := range smoothValues { + horizontalPosition := padding + (float64(index)/float64(SparklineResolution-1))*(viewboxWidth-2*padding) + verticalPosition := padding + (1-(point-minPoint)/pointRange)*(viewboxHeight-2*padding) + lineCoordinates[index] = fmt.Sprintf("%.1f,%.1f", horizontalPosition, verticalPosition) + } + + linePath := strings.Join(lineCoordinates, " ") + + fillPath := fmt.Sprintf("M%.1f,%.1f L%s L%.1f,%.1f Z", + padding, viewboxHeight, + linePath, + viewboxWidth-padding, viewboxHeight, + ) + + return SparklineData{ + LinePath: linePath, + FillPath: fillPath, + } +} + +func catmullRomResample(inputValues []float64, outputCount int) []float64 { + inputCount := len(inputValues) + if inputCount < 2 { + return inputValues + } + + resampledValues := make([]float64, outputCount) + for outputIndex := 0; outputIndex < outputCount; outputIndex++ { + normalizedPosition := float64(outputIndex) / float64(outputCount-1) * float64(inputCount-1) + segmentIndex := int(normalizedPosition) + segmentFraction := normalizedPosition - float64(segmentIndex) + + point0 := inputValues[max(0, segmentIndex-1)] + point1 := inputValues[min(inputCount-1, segmentIndex)] + point2 := inputValues[min(inputCount-1, segmentIndex+1)] + point3 := inputValues[min(inputCount-1, segmentIndex+2)] + + fractionSquared := segmentFraction * segmentFraction + fractionCubed := fractionSquared * segmentFraction + + resampledValues[outputIndex] = 0.5 * ((2 * point1) + + (-point0+point2)*segmentFraction + + (2*point0-5*point1+4*point2-point3)*fractionSquared + + (-point0+3*point1-3*point2+point3)*fractionCubed) + } + + return resampledValues +} + +func buildDnsCard() DnsCard { + systemDnsStatus := dnsSystem.CheckSystemDns() + return DnsCard{ + Configured: systemDnsStatus.Configured, + Address: systemDnsStatus.Address, + Platform: systemDnsStatus.Platform, + } +} + +func formatBytes(bytes int64) string { + if bytes < 1024 { + return fmt.Sprintf("%d B", bytes) + } + if bytes < 1024*1024 { + return fmt.Sprintf("%.1f KB", float64(bytes)/1024) + } + if bytes < 1024*1024*1024 { + return fmt.Sprintf("%.1f MB", float64(bytes)/(1024*1024)) + } + return fmt.Sprintf("%.1f GB", float64(bytes)/(1024*1024*1024)) +} diff --git a/services/dns/messages.go b/services/dns/messages.go index 2e24add..6b46c58 100644 --- a/services/dns/messages.go +++ b/services/dns/messages.go @@ -6,13 +6,11 @@ const ( AddressRequired = "Address is required." ContentRequired = "TXT record content is required." DomainNotFound = "Domain not found." - ManagedRecordLocked = "System-managed records cannot be deleted." NameRequired = "Record name is required." PortRequired = "Port is required for SRV records." RecordCreationFailed = "Failed to create DNS record." RecordDeletionFailed = "Failed to delete DNS record." RecordUpdateFailed = "Failed to update DNS record." - ManagedRecordUpdate = "System-managed records cannot be modified." RecordNotFound = "DNS record not found." TargetRequired = "Target is required." TypeInvalid = "Invalid DNS record type." diff --git a/services/dns/records.go b/services/dns/records.go index 782b943..6a66ab7 100644 --- a/services/dns/records.go +++ b/services/dns/records.go @@ -1,7 +1,6 @@ package dns import ( - "fmt" "net" "strings" @@ -13,13 +12,12 @@ import ( ) type RecordEntry struct { - ID uint `json:"id"` - Type string `json:"type"` - Name string `json:"name"` - Value string `json:"value"` - TTL uint32 `json:"ttl"` - Priority uint16 `json:"priority"` - IsManaged bool `json:"is_managed"` + ID uint `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Value string `json:"value"` + TTL uint32 `json:"ttl"` + Priority uint16 `json:"priority"` } type DomainRecordsResponse struct { @@ -44,7 +42,6 @@ type CreateRecordRequest struct { Weight uint16 `form:"weight"` Port uint16 `form:"port"` Protocol string `form:"protocol"` - PortMap int `form:"port_map"` } func DomainRecords(domainID uint) (*DomainRecordsResponse, *shortcuts.Error) { @@ -71,12 +68,12 @@ func CreateRecord(request CreateRecordRequest) *shortcuts.Error { value := strings.TrimSpace(request.Value) ttl := request.TTL if ttl == 0 { - ttl = 300 + ttl = 1 } switch request.RecordType { case "A": - return createARecord(request.DomainID, name, value, ttl, request.PortMap) + return createARecord(request.DomainID, name, value, ttl) case "AAAA": return createAAAARecord(request.DomainID, name, value, ttl) case "CNAME": @@ -154,9 +151,6 @@ func UpdateRecord(recordType string, recordID uint, request UpdateRecordRequest) if record == nil { return shortcuts.ServiceError(shortcuts.NotFound, RecordNotFound) } - if record.IsManaged { - return shortcuts.ServiceError(shortcuts.Forbidden, ManagedRecordUpdate) - } if name != "" { record.Name = name } @@ -247,9 +241,6 @@ func DeleteRecord(recordType string, recordID uint) *shortcuts.Error { if record == nil { return shortcuts.ServiceError(shortcuts.NotFound, RecordNotFound) } - if record.IsManaged { - return shortcuts.ServiceError(shortcuts.Forbidden, ManagedRecordLocked) - } if deleteError := dnsRepo.DeleteMXRecord(record); deleteError != nil { return shortcuts.ServiceError(shortcuts.Internal, RecordDeletionFailed) } @@ -283,15 +274,11 @@ func collectAllRecords(domainID uint) []RecordEntry { var entries []RecordEntry for _, record := range dnsRepo.FindARecordsByDomainID(domainID) { - value := record.Address - if record.Port > 0 { - value = fmt.Sprintf("%s (port %d)", record.Address, record.Port) - } entries = append(entries, RecordEntry{ ID: record.ID, Type: "A", Name: record.Name, - Value: value, + Value: record.Address, TTL: record.TTL, }) } @@ -318,13 +305,12 @@ func collectAllRecords(domainID uint) []RecordEntry { for _, record := range dnsRepo.FindMXRecordsByDomainID(domainID) { entries = append(entries, RecordEntry{ - ID: record.ID, - Type: "MX", - Name: record.Name, - Value: record.Target, - TTL: record.TTL, - Priority: record.Priority, - IsManaged: record.IsManaged, + ID: record.ID, + Type: "MX", + Name: record.Name, + Value: record.Target, + TTL: record.TTL, + Priority: record.Priority, }) } @@ -352,7 +338,7 @@ func collectAllRecords(domainID uint) []RecordEntry { return entries } -func createARecord(domainID uint, name string, address string, ttl uint32, portMap int) *shortcuts.Error { +func createARecord(domainID uint, name string, address string, ttl uint32) *shortcuts.Error { if address == "" { return shortcuts.ServiceError(shortcuts.BadRequest, AddressRequired) } @@ -370,7 +356,6 @@ func createARecord(domainID uint, name string, address string, ttl uint32, portM DomainID: domainID, Name: name, Address: address, - Port: portMap, TTL: ttl, } diff --git a/services/domain/domain.go b/services/domain/domain.go index 35a6578..91ca1e9 100644 --- a/services/domain/domain.go +++ b/services/domain/domain.go @@ -78,19 +78,11 @@ func CreateDomain(request CreateDomainRequest) *shortcuts.Error { return shortcuts.ServiceError(shortcuts.Internal, DomainCreationFailed) } - seedDefaultARecord(newDomain.ID) + seedDefaultRecords(newDomain.ID, name, tldName) return nil } -func seedDefaultARecord(domainID uint) { - dnsRepo.CreateARecord(&dnsModel.ARecord{ - DomainID: domainID, - Name: "@", - Address: DefaultAddress, - }) -} - func DeleteDomain(domainID uint) *shortcuts.Error { foundDomain := domainRepo.FindDomainByID(domainID) if foundDomain == nil { @@ -110,6 +102,59 @@ func DeleteDomain(domainID uint) *shortcuts.Error { return nil } +func seedDefaultRecords(domainID uint, domainName string, tldName string) { + mailHostname := DefaultMXTarget + "." + domainName + "." + tldName + "." + + dnsRepo.CreateARecord(&dnsModel.ARecord{ + DomainID: domainID, + Name: "@", + Address: DefaultAddress, + }) + + dnsRepo.CreateARecord(&dnsModel.ARecord{ + DomainID: domainID, + Name: DefaultMXTarget, + Address: DefaultAddress, + }) + + dnsRepo.CreateMXRecord(&dnsModel.MXRecord{ + DomainID: domainID, + Name: "@", + Target: mailHostname, + Priority: 10, + }) + + dnsRepo.CreateSRVRecord(&dnsModel.SRVRecord{ + DomainID: domainID, + Name: "_submission._tcp", + Target: mailHostname, + Port: 587, + Priority: 0, + Weight: 0, + Protocol: "tcp", + }) + + dnsRepo.CreateSRVRecord(&dnsModel.SRVRecord{ + DomainID: domainID, + Name: "_imap._tcp", + Target: mailHostname, + Port: 143, + Priority: 0, + Weight: 0, + Protocol: "tcp", + }) + + dnsRepo.CreateSRVRecord(&dnsModel.SRVRecord{ + DomainID: domainID, + Name: "_pop3._tcp", + Target: mailHostname, + Port: 110, + Priority: 0, + Weight: 0, + Protocol: "tcp", + }) +} + func deleteAllDNSRecords(domainID uint) { dnsRepo.DeleteARecordsByDomainID(domainID) dnsRepo.DeleteAAAARecordsByDomainID(domainID) @@ -117,4 +162,4 @@ func deleteAllDNSRecords(domainID uint) { dnsRepo.DeleteMXRecordsByDomainID(domainID) dnsRepo.DeleteTXTRecordsByDomainID(domainID) dnsRepo.DeleteSRVRecordsByDomainID(domainID) -} +} \ No newline at end of file diff --git a/services/mail/mailboxes.go b/services/mail/mailboxes.go index d6c17d7..69c6ee7 100644 --- a/services/mail/mailboxes.go +++ b/services/mail/mailboxes.go @@ -3,10 +3,8 @@ package mail import ( "strings" - dnsModel "dove/models/dns" domainModel "dove/models/domain" mailModel "dove/models/mail" - dnsRepo "dove/repositories/dns" domainRepo "dove/repositories/domain" mailRepo "dove/repositories/mail" "dove/utils/meta" @@ -91,28 +89,9 @@ func CreateMailbox(request CreateMailboxRequest) *shortcuts.Error { return shortcuts.ServiceError(shortcuts.Internal, FolderSeedFailed) } - seedMXRecordForDomain(foundDomain) - return nil } -func seedMXRecordForDomain(targetDomain *domainModel.Domain) { - fullDomainName := targetDomain.Name + "." + targetDomain.TLD.Name - existingMX := dnsRepo.FindMXRecordByTarget(targetDomain.ID, fullDomainName) - if existingMX != nil { - return - } - - dnsRepo.CreateMXRecord(&dnsModel.MXRecord{ - DomainID: targetDomain.ID, - Name: "@", - Target: fullDomainName, - Priority: 10, - TTL: 1, - IsManaged: true, - }) -} - func EditMailboxFormData(mailboxID uint) (*EditMailboxFormResponse, *shortcuts.Error) { mailbox := mailRepo.FindMailboxByIDWithRelations(mailboxID) if mailbox == nil { -- cgit v1.2.3