diff options
| author | Bobby <[email protected]> | 2026-03-08 18:17:23 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2026-03-08 18:17:23 +0530 |
| commit | 1136af49815be77a0aca151f3b8ec7348bf4b4c8 (patch) | |
| tree | ca4d94f981be59c51fa7d160e32be978a8d4b4fb | |
| parent | f48054e9bc5e4fb36b9aba9126c6ace9c5b1f470 (diff) | |
| download | dove-1136af49815be77a0aca151f3b8ec7348bf4b4c8.tar.xz dove-1136af49815be77a0aca151f3b8ec7348bf4b4c8.zip | |
feat(dns): add update functionality for DNS records (MX, SRV, TXT)
- Implemented UpdateMXRecord, UpdateSRVRecord, and UpdateTXTRecord functions in their respective repositories.
- Added UpdateRecord method in dns service to handle updates for various DNS record types.
- Updated router to include a new route for updating DNS records.
- Enhanced error messages for record updates in messages.go.
- Modified the frontend forms to support editing DNS records with improved UI components.
- Refactored existing domain management code to remove unused update functionality.
- Improved email handling by adding MX record validation during email delivery.
37 files changed, 456 insertions, 224 deletions
@@ -17,6 +17,8 @@ clean: @echo "Cleaning up..." @rm -rf bin @rm -rf tmp + @rm -rf *.db + @rm -rf data/ @echo "Cleanup complete." tidy: diff --git a/config/config.go b/config/config.go index b90256d..d5e0d37 100644 --- a/config/config.go +++ b/config/config.go @@ -78,12 +78,12 @@ type PortAssignment struct { } var ( - HTTP Http - DNS Dns - SMTP Smtp - IMAP Imap - POP3 Pop3 - S3 Storage + HTTP Http + DNS Dns + SMTP Smtp + IMAP Imap + POP3 Pop3 + S3 Storage ) var ( diff --git a/controllers/auth/auth.go b/controllers/auth/auth.go index 8140ddd..f700716 100644 --- a/controllers/auth/auth.go +++ b/controllers/auth/auth.go @@ -29,4 +29,4 @@ func Logout(context *fiber.Ctx) error { } return shortcuts.Redirect(context, "home") -}
\ No newline at end of file +} diff --git a/controllers/dns/records.go b/controllers/dns/records.go index d16a6b3..2f7036f 100644 --- a/controllers/dns/records.go +++ b/controllers/dns/records.go @@ -25,6 +25,29 @@ func CreateRecord(context *fiber.Ctx) error { return shortcuts.RedirectToPath(context, fmt.Sprintf("/domains/manage/%d", body.DomainID)) } +func UpdateRecord(context *fiber.Ctx) error { + recordID, parseError := strconv.ParseUint(meta.Request(context).Param("id"), 10, 64) + if parseError != nil { + return shortcuts.BadRequestError(context, parseError) + } + + recordType := meta.Request(context).Param("type") + + body, bodyError := meta.Body[dnsService.UpdateRecordRequest](context) + if bodyError != nil { + return shortcuts.BadRequestError(context, bodyError) + } + + serviceError := dnsService.UpdateRecord(recordType, uint(recordID), body) + if serviceError != nil { + return shortcuts.HandleError(context, serviceError) + } + + domainID := meta.Request(context).Query("domain_id") + + return shortcuts.RedirectToPath(context, fmt.Sprintf("/domains/manage/%s", domainID)) +} + func DeleteRecord(context *fiber.Ctx) error { recordID, parseError := strconv.ParseUint(meta.Request(context).Param("id"), 10, 64) if parseError != nil { diff --git a/controllers/domain/domain.go b/controllers/domain/domain.go index ac61c94..ef450ac 100644 --- a/controllers/domain/domain.go +++ b/controllers/domain/domain.go @@ -24,25 +24,6 @@ func CreateDomain(context *fiber.Ctx) error { return shortcuts.Redirect(context, "domains.manage") } -func UpdateDomain(context *fiber.Ctx) error { - domainID, parseError := strconv.ParseUint(meta.Request(context).Param("id"), 10, 64) - if parseError != nil { - return shortcuts.BadRequestError(context, parseError) - } - - body, bodyError := meta.Body[domainService.UpdateDomainRequest](context) - if bodyError != nil { - return shortcuts.BadRequestError(context, bodyError) - } - - serviceError := domainService.UpdateDomain(uint(domainID), body) - if serviceError != nil { - return shortcuts.HandleError(context, serviceError) - } - - return shortcuts.Redirect(context, "domains.manage") -} - func DeleteDomain(context *fiber.Ctx) error { domainID, parseError := strconv.ParseUint(meta.Request(context).Param("id"), 10, 64) if parseError != nil { @@ -97,4 +78,4 @@ func DeleteTLD(context *fiber.Ctx) error { } return shortcuts.Redirect(context, "domains.tlds") -}
\ No newline at end of file +} diff --git a/database/migration.go b/database/migration.go index 9df1bfc..56a0115 100644 --- a/database/migration.go +++ b/database/migration.go @@ -29,4 +29,4 @@ func migrate() { if migrationError != nil { logger.Fatalf(LogPrefix, MigrationFailed, migrationError) } -}
\ No newline at end of file +} diff --git a/database/resolve.go b/database/resolve.go index db90dc5..66ede18 100644 --- a/database/resolve.go +++ b/database/resolve.go @@ -19,4 +19,4 @@ func resolveGORMLogLevel() logger.Interface { default: return logger.Default.LogMode(logger.Silent) } -}
\ No newline at end of file +} diff --git a/dove/main.go b/dove/main.go index 28c759a..9a6c16a 100644 --- a/dove/main.go +++ b/dove/main.go @@ -24,10 +24,10 @@ import ( const LogPrefix = "Server" const ( - ServerStarting = "Server started on %s." - ServerListenFailed = "Failed to start server: %v" - ServerShuttingDown = "Shutting down gracefully..." - ServerShutdownFailed = "Error during server shutdown: %v" + ServerStarting = "Server started on %s." + ServerListenFailed = "Failed to start server: %v" + ServerShuttingDown = "Shutting down gracefully..." + ServerShutdownFailed = "Error during server shutdown: %v" ServerShutdownComplete = "Shutdown complete." ) @@ -74,4 +74,4 @@ func main() { } logger.Successf(LogPrefix, ServerShutdownComplete) -}
\ No newline at end of file +} diff --git a/pages/domain/domain.go b/pages/domain/domain.go index 94068f2..8407efb 100644 --- a/pages/domain/domain.go +++ b/pages/domain/domain.go @@ -46,21 +46,6 @@ func NewDomain(context *fiber.Ctx) error { return shortcuts.Render(context, "domains/newdomain", domainService.DomainFormData()) } -func EditDomain(context *fiber.Ctx) error { - domainID, parseError := strconv.ParseUint(meta.Request(context).Param("id"), 10, 64) - if parseError != nil { - return shortcuts.BadRequestError(context, parseError) - } - - formData, serviceError := domainService.EditDomainFormData(uint(domainID)) - if serviceError != nil { - return shortcuts.HandleError(context, serviceError) - } - - meta.SetPageTitle(context, "Edit Domain") - return shortcuts.Render(context, "domains/editdomain", formData) -} - func NewTLD(context *fiber.Ctx) error { meta.SetPageTitle(context, "New TLD") return shortcuts.Render(context, "domains/newtld", nil) @@ -79,4 +64,4 @@ func EditTLD(context *fiber.Ctx) error { meta.SetPageTitle(context, "Edit TLD") return shortcuts.Render(context, "domains/edittld", formData) -}
\ No newline at end of file +} diff --git a/pages/mail/mailboxes.go b/pages/mail/mailboxes.go index f679839..58089b9 100644 --- a/pages/mail/mailboxes.go +++ b/pages/mail/mailboxes.go @@ -38,4 +38,4 @@ func Mailboxes(context *fiber.Ctx) error { search := context.Query("search") return shortcuts.Render(context, "mail/mailboxes", mailService.ListMailboxes(pagination, sorting, search)) -}
\ No newline at end of file +} diff --git a/pages/mail/users.go b/pages/mail/users.go index 6fe057d..6d3aa16 100644 --- a/pages/mail/users.go +++ b/pages/mail/users.go @@ -38,4 +38,4 @@ func Users(context *fiber.Ctx) error { search := context.Query("search") return shortcuts.Render(context, "mail/users", mailService.ListUsers(pagination, sorting, search)) -}
\ No newline at end of file +} diff --git a/repositories/dns/a.go b/repositories/dns/a.go index 88b3304..38e6ecf 100644 --- a/repositories/dns/a.go +++ b/repositories/dns/a.go @@ -30,6 +30,10 @@ func CreateARecord(record *dns.ARecord) error { return database.DB.Create(record).Error } +func UpdateARecord(record *dns.ARecord) error { + return database.DB.Save(record).Error +} + func DeleteARecord(record *dns.ARecord) error { return database.DB.Delete(record).Error } diff --git a/repositories/dns/aaaa.go b/repositories/dns/aaaa.go index 4fb1584..ddca575 100644 --- a/repositories/dns/aaaa.go +++ b/repositories/dns/aaaa.go @@ -30,6 +30,10 @@ func CreateAAAARecord(record *dns.AAAARecord) error { return database.DB.Create(record).Error } +func UpdateAAAARecord(record *dns.AAAARecord) error { + return database.DB.Save(record).Error +} + func DeleteAAAARecord(record *dns.AAAARecord) error { return database.DB.Delete(record).Error } diff --git a/repositories/dns/cname.go b/repositories/dns/cname.go index 16c5913..905fa93 100644 --- a/repositories/dns/cname.go +++ b/repositories/dns/cname.go @@ -30,6 +30,10 @@ func CreateCNAMERecord(record *dns.CNAMERecord) error { return database.DB.Create(record).Error } +func UpdateCNAMERecord(record *dns.CNAMERecord) error { + return database.DB.Save(record).Error +} + func DeleteCNAMERecord(record *dns.CNAMERecord) error { return database.DB.Delete(record).Error } diff --git a/repositories/dns/mx.go b/repositories/dns/mx.go index abb46a7..06f2d6c 100644 --- a/repositories/dns/mx.go +++ b/repositories/dns/mx.go @@ -39,6 +39,10 @@ func CreateMXRecord(record *dns.MXRecord) error { return database.DB.Create(record).Error } +func UpdateMXRecord(record *dns.MXRecord) error { + return database.DB.Save(record).Error +} + func DeleteMXRecord(record *dns.MXRecord) error { return database.DB.Delete(record).Error } diff --git a/repositories/dns/srv.go b/repositories/dns/srv.go index da29dea..0d20b00 100644 --- a/repositories/dns/srv.go +++ b/repositories/dns/srv.go @@ -30,6 +30,10 @@ func CreateSRVRecord(record *dns.SRVRecord) error { return database.DB.Create(record).Error } +func UpdateSRVRecord(record *dns.SRVRecord) error { + return database.DB.Save(record).Error +} + func DeleteSRVRecord(record *dns.SRVRecord) error { return database.DB.Delete(record).Error } diff --git a/repositories/dns/txt.go b/repositories/dns/txt.go index 0273af6..c704cce 100644 --- a/repositories/dns/txt.go +++ b/repositories/dns/txt.go @@ -30,6 +30,10 @@ func CreateTXTRecord(record *dns.TXTRecord) error { return database.DB.Create(record).Error } +func UpdateTXTRecord(record *dns.TXTRecord) error { + return database.DB.Save(record).Error +} + func DeleteTXTRecord(record *dns.TXTRecord) error { return database.DB.Delete(record).Error } diff --git a/router/domain.go b/router/domain.go index 482cbd0..a3a2164 100644 --- a/router/domain.go +++ b/router/domain.go @@ -21,10 +21,9 @@ func init() { urls.Path(urls.Get, "/manage", auth.RequireAuthentication(domainPage.Domains), "manage") urls.Path(urls.Get, "/manage/new", auth.RequireAuthentication(domainPage.NewDomain), "manage.new") urls.Path(urls.Post, "/manage", auth.RequireAuthentication(domainController.CreateDomain), "manage.create") - urls.Path(urls.Get, "/manage/:id/edit", auth.RequireAuthentication(domainPage.EditDomain), "manage.edit") - urls.Path(urls.Put, "/manage/:id", auth.RequireAuthentication(domainController.UpdateDomain), "manage.update") urls.Path(urls.Delete, "/manage/:id", auth.RequireAuthentication(domainController.DeleteDomain), "manage.delete") urls.Path(urls.Get, "/manage/:id", auth.RequireAuthentication(domainPage.DomainDetail), "manage.detail") urls.Path(urls.Post, "/records", auth.RequireAuthentication(dnsController.CreateRecord), "records.create") + urls.Path(urls.Put, "/records/:type/:id", auth.RequireAuthentication(dnsController.UpdateRecord), "records.update") urls.Path(urls.Delete, "/records/:type/:id", auth.RequireAuthentication(dnsController.DeleteRecord), "records.delete") -}
\ No newline at end of file +} diff --git a/services/auth/auth.go b/services/auth/auth.go index b8a0e13..6c6589b 100644 --- a/services/auth/auth.go +++ b/services/auth/auth.go @@ -40,4 +40,4 @@ func Deauthenticate(context *fiber.Ctx) (*MessageResponse, *shortcuts.Error) { return &MessageResponse{ Message: LoggedOut, }, nil -}
\ No newline at end of file +} diff --git a/services/dns/messages.go b/services/dns/messages.go index 723661a..2e24add 100644 --- a/services/dns/messages.go +++ b/services/dns/messages.go @@ -11,6 +11,8 @@ const ( 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 56ece1f..782b943 100644 --- a/services/dns/records.go +++ b/services/dns/records.go @@ -27,6 +27,13 @@ type DomainRecordsResponse struct { Records []RecordEntry `json:"records"` } +type UpdateRecordRequest struct { + Name string `form:"name"` + Value string `form:"value"` + TTL uint32 `form:"ttl"` + Priority uint16 `form:"priority"` +} + type CreateRecordRequest struct { DomainID uint `form:"domain_id"` RecordType string `form:"record_type"` @@ -85,6 +92,127 @@ func CreateRecord(request CreateRecordRequest) *shortcuts.Error { } } +func UpdateRecord(recordType string, recordID uint, request UpdateRecordRequest) *shortcuts.Error { + name := strings.TrimSpace(request.Name) + value := strings.TrimSpace(request.Value) + ttl := request.TTL + if ttl == 0 { + ttl = 1 + } + + switch recordType { + case "A": + record := dnsRepo.FindARecordByID(recordID) + if record == nil { + return shortcuts.ServiceError(shortcuts.NotFound, RecordNotFound) + } + if name != "" { + record.Name = name + } + if value != "" { + record.Address = value + } + record.TTL = ttl + if updateError := dnsRepo.UpdateARecord(record); updateError != nil { + return shortcuts.ServiceError(shortcuts.Internal, RecordUpdateFailed) + } + + case "AAAA": + record := dnsRepo.FindAAAARecordByID(recordID) + if record == nil { + return shortcuts.ServiceError(shortcuts.NotFound, RecordNotFound) + } + if name != "" { + record.Name = name + } + if value != "" { + record.Address = value + } + record.TTL = ttl + if updateError := dnsRepo.UpdateAAAARecord(record); updateError != nil { + return shortcuts.ServiceError(shortcuts.Internal, RecordUpdateFailed) + } + + case "CNAME": + record := dnsRepo.FindCNAMERecordByID(recordID) + if record == nil { + return shortcuts.ServiceError(shortcuts.NotFound, RecordNotFound) + } + if name != "" { + record.Name = name + } + if value != "" { + record.Target = value + } + record.TTL = ttl + if updateError := dnsRepo.UpdateCNAMERecord(record); updateError != nil { + return shortcuts.ServiceError(shortcuts.Internal, RecordUpdateFailed) + } + + case "MX": + record := dnsRepo.FindMXRecordByID(recordID) + if record == nil { + return shortcuts.ServiceError(shortcuts.NotFound, RecordNotFound) + } + if record.IsManaged { + return shortcuts.ServiceError(shortcuts.Forbidden, ManagedRecordUpdate) + } + if name != "" { + record.Name = name + } + if value != "" { + record.Target = value + } + if request.Priority > 0 { + record.Priority = request.Priority + } + record.TTL = ttl + if updateError := dnsRepo.UpdateMXRecord(record); updateError != nil { + return shortcuts.ServiceError(shortcuts.Internal, RecordUpdateFailed) + } + + case "TXT": + record := dnsRepo.FindTXTRecordByID(recordID) + if record == nil { + return shortcuts.ServiceError(shortcuts.NotFound, RecordNotFound) + } + if name != "" { + record.Name = name + } + if value != "" { + record.Content = value + } + record.TTL = ttl + if updateError := dnsRepo.UpdateTXTRecord(record); updateError != nil { + return shortcuts.ServiceError(shortcuts.Internal, RecordUpdateFailed) + } + + case "SRV": + record := dnsRepo.FindSRVRecordByID(recordID) + if record == nil { + return shortcuts.ServiceError(shortcuts.NotFound, RecordNotFound) + } + if name != "" { + record.Name = name + } + if value != "" { + record.Target = value + } + if request.Priority > 0 { + record.Priority = request.Priority + } + record.TTL = ttl + if updateError := dnsRepo.UpdateSRVRecord(record); updateError != nil { + return shortcuts.ServiceError(shortcuts.Internal, RecordUpdateFailed) + } + + default: + return shortcuts.ServiceError(shortcuts.BadRequest, TypeInvalid) + } + + return nil +} + func DeleteRecord(recordType string, recordID uint) *shortcuts.Error { switch recordType { case "A": @@ -389,4 +517,3 @@ func createSRVRecord(domainID uint, name string, target string, ttl uint32, prio return nil } - diff --git a/services/domain/domain.go b/services/domain/domain.go index ff3efa5..35a6578 100644 --- a/services/domain/domain.go +++ b/services/domain/domain.go @@ -17,16 +17,6 @@ type CreateDomainRequest struct { TLDName string `form:"tld_name"` } -type UpdateDomainRequest struct { - Name string `form:"name"` - TLDName string `form:"tld_name"` -} - -type EditDomainFormResponse struct { - Domain domainModel.Domain `json:"domain"` - TLDs []domainModel.TLD `json:"tlds"` -} - type DomainListResponse struct { Domains []domainModel.Domain `json:"domains"` } @@ -101,63 +91,6 @@ func seedDefaultARecord(domainID uint) { }) } -func EditDomainFormData(domainID uint) (*EditDomainFormResponse, *shortcuts.Error) { - foundDomain := domainRepo.FindDomainByID(domainID) - if foundDomain == nil { - return nil, shortcuts.ServiceError(shortcuts.NotFound, DomainNotFound) - } - - return &EditDomainFormResponse{ - Domain: *foundDomain, - TLDs: domainRepo.AllTLDs(), - }, nil -} - -func UpdateDomain(domainID uint, request UpdateDomainRequest) *shortcuts.Error { - foundDomain := domainRepo.FindDomainByID(domainID) - if foundDomain == nil { - return shortcuts.ServiceError(shortcuts.NotFound, DomainNotFound) - } - - name := strings.TrimSpace(strings.ToLower(request.Name)) - tldName := strings.TrimSpace(strings.ToLower(request.TLDName)) - - switch { - case name == "": - return shortcuts.ServiceError(shortcuts.BadRequest, DomainNameRequired) - case !validate.DNSLabel(name): - return shortcuts.ServiceError(shortcuts.BadRequest, DomainNameInvalid) - case tldName == "": - return shortcuts.ServiceError(shortcuts.BadRequest, DomainTLDRequired) - } - - tld := domainRepo.FindTLDByName(tldName) - if tld == nil { - return shortcuts.ServiceError(shortcuts.Unprocessable, TLDNotFound) - } - - nameChanged := name != foundDomain.Name || tld.ID != foundDomain.TLDID - - if nameChanged { - if domainRepo.FindDomainByFullName(name, tldName) != nil { - return shortcuts.ServiceError(shortcuts.Unprocessable, DomainAlreadyExists) - } - } - - foundDomain.Name = name - foundDomain.TLDID = tld.ID - - if updateError := domainRepo.UpdateDomain(foundDomain); updateError != nil { - return shortcuts.ServiceError(shortcuts.Internal, DomainUpdateFailed) - } - - if nameChanged { - mailRepo.RebuildMailboxAddressesByDomainID(foundDomain.ID) - } - - return nil -} - func DeleteDomain(domainID uint) *shortcuts.Error { foundDomain := domainRepo.FindDomainByID(domainID) if foundDomain == nil { diff --git a/services/domain/messages.go b/services/domain/messages.go index 583fab0..e8b0ae3 100644 --- a/services/domain/messages.go +++ b/services/domain/messages.go @@ -1,22 +1,21 @@ package domain const ( - DomainAlreadyExists = "A domain with this name already exists under this TLD." - DomainCreationFailed = "Failed to create domain." - DomainDeletionFailed = "Failed to delete domain." - DomainHasMailboxes = "Cannot delete a domain that has mailboxes. Remove all mailboxes first." - DomainNameInvalid = "Domain name must contain only lowercase letters, numbers, and hyphens." - DomainNameRequired = "Domain name is required." - DomainNotFound = "Domain not found." - DomainUpdateFailed = "Failed to update domain." - DomainTLDRequired = "A TLD must be selected for the domain." - TLDAlreadyExists = "A TLD with this name already exists." - TLDCreationFailed = "Failed to create TLD." - TLDDeletionFailed = "Failed to delete TLD." - TLDHasDomains = "Cannot delete a TLD that has registered domains. Remove all domains first." - TLDNameInvalid = "TLD name must contain only lowercase letters, numbers, and hyphens." - TLDNameRequired = "TLD name is required." - TLDNotFound = "TLD not found." - TLDProtected = "Default TLDs cannot be deleted." - TLDUpdateFailed = "Failed to update TLD." + DomainAlreadyExists = "A domain with this name already exists under this TLD." + DomainCreationFailed = "Failed to create domain." + DomainDeletionFailed = "Failed to delete domain." + DomainHasMailboxes = "Cannot delete a domain that has mailboxes. Remove all mailboxes first." + DomainNameInvalid = "Domain name must contain only lowercase letters, numbers, and hyphens." + DomainNameRequired = "Domain name is required." + DomainNotFound = "Domain not found." + DomainTLDRequired = "A TLD must be selected for the domain." + TLDAlreadyExists = "A TLD with this name already exists." + TLDCreationFailed = "Failed to create TLD." + TLDDeletionFailed = "Failed to delete TLD." + TLDHasDomains = "Cannot delete a TLD that has registered domains. Remove all domains first." + TLDNameInvalid = "TLD name must contain only lowercase letters, numbers, and hyphens." + TLDNameRequired = "TLD name is required." + TLDNotFound = "TLD not found." + TLDProtected = "Default TLDs cannot be deleted." + TLDUpdateFailed = "Failed to update TLD." ) diff --git a/services/mail/aliases.go b/services/mail/aliases.go index 2999681..be10eae 100644 --- a/services/mail/aliases.go +++ b/services/mail/aliases.go @@ -3,8 +3,8 @@ package mail import ( "strings" - domainRepo "dove/repositories/domain" mailModel "dove/models/mail" + domainRepo "dove/repositories/domain" mailRepo "dove/repositories/mail" "dove/utils/shortcuts" "dove/utils/validate" diff --git a/services/mail/mailboxes.go b/services/mail/mailboxes.go index 0f076ad..d6c17d7 100644 --- a/services/mail/mailboxes.go +++ b/services/mail/mailboxes.go @@ -108,6 +108,7 @@ func seedMXRecordForDomain(targetDomain *domainModel.Domain) { Name: "@", Target: fullDomainName, Priority: 10, + TTL: 1, IsManaged: true, }) } diff --git a/services/mail/messages.go b/services/mail/messages.go index 530ea45..87917fb 100644 --- a/services/mail/messages.go +++ b/services/mail/messages.go @@ -8,17 +8,17 @@ const ( AliasInvalid = "Alias address must be a valid email format ([email protected])." AliasNotFound = "Alias not found." - LocalPartInvalid = "Local part must contain only lowercase letters, numbers, dots, hyphens, and underscores." - LocalPartRequired = "The local part of the address is required." - DomainRequired = "A domain must be selected for the mailbox." - DomainNotFound = "The selected domain does not exist." - AlreadyExists = "A mailbox with this address already exists." - CreationFailed = "Failed to create mailbox." - DeletionFailed = "Failed to delete mailbox." - MailboxNotFound = "Mailbox not found." + LocalPartInvalid = "Local part must contain only lowercase letters, numbers, dots, hyphens, and underscores." + LocalPartRequired = "The local part of the address is required." + DomainRequired = "A domain must be selected for the mailbox." + DomainNotFound = "The selected domain does not exist." + AlreadyExists = "A mailbox with this address already exists." + CreationFailed = "Failed to create mailbox." + DeletionFailed = "Failed to delete mailbox." + MailboxNotFound = "Mailbox not found." MailboxUpdateFailed = "Failed to update mailbox." - UserNotFound = "The selected user does not exist." - UserRequired = "A user must be selected for the mailbox." + UserNotFound = "The selected user does not exist." + UserRequired = "A user must be selected for the mailbox." EmailNotFound = "Email not found." EmailMoveFailed = "Failed to move email." @@ -31,19 +31,19 @@ const ( LogPrefix = "mail" - FolderCreationFailed = "Failed to create folder." - FolderDeletionFailed = "Failed to delete folder." - FolderNameRequired = "Folder name is required." - FolderNotFound = "Folder not found." - FolderSeedFailed = "Failed to create default folders." - FolderIsSystem = "System folders cannot be deleted." - FolderHasEmails = "Cannot delete a folder that contains emails. Move or delete the emails first." + FolderCreationFailed = "Failed to create folder." + FolderDeletionFailed = "Failed to delete folder." + FolderNameRequired = "Folder name is required." + FolderNotFound = "Folder not found." + FolderSeedFailed = "Failed to create default folders." + FolderIsSystem = "System folders cannot be deleted." + FolderHasEmails = "Cannot delete a folder that contains emails. Move or delete the emails first." - NoMailboxesExist = "No mailboxes exist. Create a mailbox first." - RecipientRequired = "A recipient address is required." - RecipientNotFound = "The recipient mailbox does not exist." - SenderRequired = "A sender mailbox must be selected." - SubjectRequired = "Subject is required." + NoMailboxesExist = "No mailboxes exist. Create a mailbox first." + RecipientRequired = "A recipient address is required." + RecipientNotFound = "The recipient mailbox does not exist." + SenderRequired = "A sender mailbox must be selected." + SubjectRequired = "Subject is required." DisplayNameRequired = "Display name is required." UserAlreadyExists = "A user with this username already exists." diff --git a/services/mail/users.go b/services/mail/users.go index 025c411..3d77fde 100644 --- a/services/mail/users.go +++ b/services/mail/users.go @@ -103,4 +103,4 @@ func DeleteUser(userID uint) *shortcuts.Error { func AllUsers() []mailModel.User { return mailRepo.AllUsers() -}
\ No newline at end of file +} diff --git a/tags/defaults.go b/tags/defaults.go index 4a87ff9..4f6eb44 100644 --- a/tags/defaults.go +++ b/tags/defaults.go @@ -2,4 +2,4 @@ package tags const ( LogPrefix = "Tags" -)
\ No newline at end of file +) diff --git a/tags/messages.go b/tags/messages.go index 99be613..cb5cd12 100644 --- a/tags/messages.go +++ b/tags/messages.go @@ -8,4 +8,4 @@ const ( RegistrationFailed = "Failed to register tag: %s." RouteNotFound = "Route not found: %s." TemplateWriteFailed = "Failed to write template output." -)
\ No newline at end of file +) diff --git a/tags/tags.go b/tags/tags.go index 4854ea2..aa8a79b 100644 --- a/tags/tags.go +++ b/tags/tags.go @@ -21,4 +21,4 @@ func Initialize() { logger.Errorf(LogPrefix, RegistrationFailed, tag.Name) } } -}
\ No newline at end of file +} diff --git a/tags/url.go b/tags/url.go index a087ab7..2956876 100644 --- a/tags/url.go +++ b/tags/url.go @@ -95,4 +95,4 @@ func (self *UrlNode) Execute(executionContext *pongo2.ExecutionContext, writer p } return nil -}
\ No newline at end of file +} diff --git a/templates/domains/htmx/detail.htmx.django b/templates/domains/htmx/detail.htmx.django index 77a8615..eaa3806 100644 --- a/templates/domains/htmx/detail.htmx.django +++ b/templates/domains/htmx/detail.htmx.django @@ -23,36 +23,61 @@ {% url "domains.records.create" as create_record_path %} <form hx-post="{{ create_record_path }}" hx-swap="none" class="px-5 py-4 space-y-3"> <input type="hidden" name="domain_id" value="{{ domain.ID }}"> - <div class="grid grid-cols-6 gap-3"> + <div class="grid grid-cols-5 gap-3"> <div> <label class="block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5">Type</label> - <select name="record_type" class="input-field text-xs" data-record-type-select> - <option value="A">A</option> - <option value="AAAA">AAAA</option> - <option value="CNAME">CNAME</option> - <option value="MX">MX</option> - <option value="TXT">TXT</option> - <option value="SRV">SRV</option> - </select> + <div class="dropdown" data-dropdown data-record-type-dropdown> + <input type="hidden" name="record_type" value="A" data-dropdown-value> + <button type="button" data-dropdown-trigger class="input-field text-xs text-left flex items-center justify-between"> + <span class="truncate" data-dropdown-label>A</span> + <svg class="w-3.5 h-3.5 text-zinc-500 shrink-0 ml-1 transition-transform duration-150" data-dropdown-chevron fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> + <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" /> + </svg> + </button> + <div class="dropdown-menu" data-dropdown-menu> + <div class="dropdown-options" data-dropdown-options> + <button type="button" class="dropdown-option" data-dropdown-option data-value="A" data-label="A"><p class="text-sm text-zinc-200">A</p><p class="text-xs text-zinc-500">IPv4 address</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="AAAA" data-label="AAAA"><p class="text-sm text-zinc-200">AAAA</p><p class="text-xs text-zinc-500">IPv6 address</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="CNAME" data-label="CNAME"><p class="text-sm text-zinc-200">CNAME</p><p class="text-xs text-zinc-500">Canonical name</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="MX" data-label="MX"><p class="text-sm text-zinc-200">MX</p><p class="text-xs text-zinc-500">Mail exchange</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="TXT" data-label="TXT"><p class="text-sm text-zinc-200">TXT</p><p class="text-xs text-zinc-500">Text record</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="SRV" data-label="SRV"><p class="text-sm text-zinc-200">SRV</p><p class="text-xs text-zinc-500">Service locator</p></button> + </div> + </div> + </div> </div> <div> <label class="block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5">Name</label> <input type="text" name="name" autocomplete="off" placeholder="@" class="input-field text-xs"> </div> <div class="col-span-2"> - <label class="block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5" data-value-label>Address</label> + <label class="block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5" data-value-label>Address (IPv4)</label> <input type="text" name="value" required autocomplete="off" placeholder="127.0.0.1" class="input-field text-xs" data-value-input> </div> <div> <label class="block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5">TTL</label> - <input type="number" name="ttl" value="300" min="1" class="input-field text-xs"> - </div> - <div class="flex items-end gap-2"> - <button type="submit" class="btn-small bg-accent-500/20 text-accent-400 border-accent-500/20 hover:bg-accent-500/30">Save</button> - <button type="button" class="btn-small" data-cancel-new-record>Cancel</button> + <div class="dropdown" data-dropdown> + <input type="hidden" name="ttl" value="1" data-dropdown-value> + <button type="button" data-dropdown-trigger class="input-field text-xs text-left flex items-center justify-between"> + <span class="truncate" data-dropdown-label>Immediate</span> + <svg class="w-3.5 h-3.5 text-zinc-500 shrink-0 ml-1 transition-transform duration-150" data-dropdown-chevron fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> + <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" /> + </svg> + </button> + <div class="dropdown-menu" data-dropdown-menu> + <div class="dropdown-options" data-dropdown-options> + <button type="button" class="dropdown-option" data-dropdown-option data-value="1" data-label="Immediate"><p class="text-sm text-zinc-200">Immediate</p><p class="text-xs text-zinc-500">1 second</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="300" data-label="5 minutes"><p class="text-sm text-zinc-200">5 minutes</p><p class="text-xs text-zinc-500">300 seconds</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="900" data-label="15 minutes"><p class="text-sm text-zinc-200">15 minutes</p><p class="text-xs text-zinc-500">900 seconds</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="3600" data-label="1 hour"><p class="text-sm text-zinc-200">1 hour</p><p class="text-xs text-zinc-500">3600 seconds</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="14400" data-label="4 hours"><p class="text-sm text-zinc-200">4 hours</p><p class="text-xs text-zinc-500">14400 seconds</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="86400" data-label="1 day"><p class="text-sm text-zinc-200">1 day</p><p class="text-xs text-zinc-500">86400 seconds</p></button> + </div> + </div> + </div> </div> </div> - <div class="grid grid-cols-6 gap-3" data-extra-fields> + <div class="grid grid-cols-5 gap-3" data-extra-fields> <div data-priority-field style="display: none;"> <label class="block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5">Priority</label> <input type="number" name="priority" value="10" min="0" class="input-field text-xs"> @@ -71,42 +96,41 @@ </div> <div data-srv-protocol style="display: none;"> <label class="block text-[10px] font-medium text-zinc-500 mb-1 ml-0.5">Protocol</label> - <select name="protocol" class="input-field text-xs"> - <option value="tcp">TCP</option> - <option value="udp">UDP</option> - </select> + <div class="dropdown" data-dropdown> + <input type="hidden" name="protocol" value="tcp" data-dropdown-value> + <button type="button" data-dropdown-trigger class="input-field text-xs text-left flex items-center justify-between"> + <span class="truncate" data-dropdown-label>TCP</span> + <svg class="w-3.5 h-3.5 text-zinc-500 shrink-0 ml-1 transition-transform duration-150" data-dropdown-chevron fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> + <path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" /> + </svg> + </button> + <div class="dropdown-menu" data-dropdown-menu> + <div class="dropdown-options" data-dropdown-options> + <button type="button" class="dropdown-option" data-dropdown-option data-value="tcp" data-label="TCP"><p class="text-sm text-zinc-200">TCP</p></button> + <button type="button" class="dropdown-option" data-dropdown-option data-value="udp" data-label="UDP"><p class="text-sm text-zinc-200">UDP</p></button> + </div> + </div> + </div> </div> </div> + <div class="flex items-center gap-3 pt-1"> + <button type="submit" class="btn-small">Save Record</button> + <a href="javascript:void(0)" class="text-xs text-zinc-500 hover:text-zinc-300 transition-colors duration-150" data-cancel-new-record>Cancel</a> + </div> </form> </div> {% if records %} <div class="divide-y divide-white/[0.04]"> {% for record in records %} - <div class="flex items-center justify-between px-5 py-3"> - <div class="flex items-center gap-3 min-w-0"> + <div class="flex items-center justify-between px-5 py-3 group" data-record-row="{{ record.Type }}-{{ record.ID }}"> + <div class="flex items-center gap-3 min-w-0" data-record-display> {% if record.IsManaged %} - <div class="flex items-center justify-center w-8 h-8 rounded-lg bg-amber-500/10 shrink-0"> - <svg class="w-4 h-4 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> + <div class="flex items-center justify-center w-6 h-6 rounded bg-amber-500/10 shrink-0"> + <svg class="w-3.5 h-3.5 text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" /> </svg> </div> - {% else %} - <div class="flex items-center justify-center w-8 h-8 rounded-lg bg-surface-800 shrink-0"> - {% if record.Type == "A" %} - <span class="text-xs font-bold text-blue-400">A</span> - {% elif record.Type == "AAAA" %} - <span class="text-[10px] font-bold text-blue-300">AAAA</span> - {% elif record.Type == "CNAME" %} - <span class="text-[10px] font-bold text-purple-400">CN</span> - {% elif record.Type == "MX" %} - <span class="text-xs font-bold text-orange-400">MX</span> - {% elif record.Type == "TXT" %} - <span class="text-[10px] font-bold text-green-400">TXT</span> - {% elif record.Type == "SRV" %} - <span class="text-[10px] font-bold text-pink-400">SRV</span> - {% endif %} - </div> {% endif %} <div class="flex items-center gap-2 min-w-0"> <span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0 @@ -128,11 +152,12 @@ <span class="text-xs text-zinc-700 shrink-0">{{ record.TTL }}s</span> </div> </div> - <div class="flex items-center gap-3 shrink-0 ml-4"> + <div class="flex items-center gap-3 shrink-0 ml-4" data-record-actions> {% if record.IsManaged %} <span class="text-xs text-amber-500/60">System managed</span> {% else %} - <button data-confirm-trigger data-confirm-title="Delete {{ record.Type }} record" data-confirm-message="This DNS record will be permanently removed. This action cannot be undone." data-confirm-action="/domains/records/{{ record.Type }}/{{ record.ID }}?domain_id={{ domain.ID }}" class="text-xs text-zinc-600 hover:text-red-400 transition-colors duration-150">Delete</button> + <button type="button" class="text-xs text-zinc-600 hover:text-zinc-300 transition-colors duration-150 opacity-0 group-hover:opacity-100" data-edit-record data-record-type="{{ record.Type }}" data-record-id="{{ record.ID }}" data-record-name="{{ record.Name }}" data-record-value="{{ record.Value }}" data-record-ttl="{{ record.TTL }}" data-record-priority="{{ record.Priority }}">Edit</button> + <button data-confirm-trigger data-confirm-title="Delete {{ record.Type }} record" data-confirm-message="This DNS record will be permanently removed. This action cannot be undone." data-confirm-action="/domains/records/{{ record.Type }}/{{ record.ID }}?domain_id={{ domain.ID }}" class="text-xs text-zinc-600 hover:text-red-400 transition-colors duration-150 opacity-0 group-hover:opacity-100">Delete</button> {% endif %} </div> </div> @@ -178,7 +203,8 @@ var toggleButton = document.querySelector('[data-toggle-new-record]'); var cancelButton = document.querySelector('[data-cancel-new-record]'); var formContainer = document.querySelector('[data-new-record-form]'); - var typeSelect = document.querySelector('[data-record-type-select]'); + var typeDropdown = document.querySelector('[data-record-type-dropdown]'); + var typeHiddenInput = typeDropdown.querySelector('[data-dropdown-value]'); var priorityField = document.querySelector('[data-priority-field]'); var portMapField = document.querySelector('[data-port-map-field]'); var srvWeight = document.querySelector('[data-srv-weight]'); @@ -205,7 +231,8 @@ }; function updateFields() { - var config = fieldConfig[typeSelect.value]; + var selectedType = typeHiddenInput.value; + var config = fieldConfig[selectedType]; if (!config) return; valueLabel.textContent = config.label; @@ -217,7 +244,118 @@ srvProtocol.style.display = config.srv ? '' : 'none'; } - typeSelect.addEventListener('change', updateFields); + var typeOptions = typeDropdown.querySelectorAll('[data-dropdown-option]'); + typeOptions.forEach(function(option) { + option.addEventListener('click', function() { + setTimeout(updateFields, 10); + }); + }); + updateFields(); + + var ttlOptions = [['1', 'Immediate'], ['300', '5 min'], ['900', '15 min'], ['3600', '1 hour'], ['14400', '4 hours'], ['86400', '1 day']]; + + function createEditForm(recordType, recordId, recordName, recordValue, recordTtl, recordPriority) { + var form = document.createElement('form'); + form.setAttribute('hx-put', '/domains/records/' + recordType + '/' + recordId + '?domain_id={{ domain.ID }}'); + form.setAttribute('hx-swap', 'none'); + form.setAttribute('data-inline-edit', ''); + form.className = 'flex items-center gap-3 w-full'; + + var typeBadge = document.createElement('span'); + typeBadge.className = 'inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium shrink-0 bg-surface-800 text-zinc-400'; + typeBadge.textContent = recordType; + form.appendChild(typeBadge); + + var nameInput = document.createElement('input'); + nameInput.type = 'text'; + nameInput.name = 'name'; + nameInput.value = recordName; + nameInput.className = 'input-field text-xs w-20'; + nameInput.placeholder = '@'; + form.appendChild(nameInput); + + var valueInput = document.createElement('input'); + valueInput.type = 'text'; + valueInput.name = 'value'; + valueInput.value = recordValue; + valueInput.required = true; + valueInput.className = 'input-field text-xs flex-1'; + valueInput.placeholder = 'Value'; + form.appendChild(valueInput); + + var hasPriority = parseInt(recordPriority, 10) > 0; + if (hasPriority) { + var priorityInput = document.createElement('input'); + priorityInput.type = 'number'; + priorityInput.name = 'priority'; + priorityInput.value = recordPriority; + priorityInput.min = '0'; + priorityInput.className = 'input-field text-xs w-16'; + priorityInput.placeholder = 'Pri'; + form.appendChild(priorityInput); + } + + var ttlSelect = document.createElement('select'); + ttlSelect.name = 'ttl'; + ttlSelect.className = 'input-field text-xs w-24'; + for (var i = 0; i < ttlOptions.length; i++) { + var opt = document.createElement('option'); + opt.value = ttlOptions[i][0]; + opt.textContent = ttlOptions[i][1]; + if (recordTtl === ttlOptions[i][0]) opt.selected = true; + ttlSelect.appendChild(opt); + } + form.appendChild(ttlSelect); + + var saveButton = document.createElement('button'); + saveButton.type = 'submit'; + saveButton.className = 'btn-small'; + saveButton.textContent = 'Save'; + form.appendChild(saveButton); + + var cancelLink = document.createElement('a'); + cancelLink.href = 'javascript:void(0)'; + cancelLink.className = 'text-xs text-zinc-500 hover:text-zinc-300 transition-colors duration-150 shrink-0'; + cancelLink.textContent = 'Cancel'; + cancelLink.setAttribute('data-cancel-edit', ''); + form.appendChild(cancelLink); + + return form; + } + + function handleEditClick(event) { + var button = event.target.closest('[data-edit-record]'); + if (!button) return; + + var recordType = button.dataset.recordType; + var recordId = button.dataset.recordId; + var recordName = button.dataset.recordName; + var recordValue = button.dataset.recordValue; + var recordTtl = button.dataset.recordTtl; + var recordPriority = button.dataset.recordPriority; + + var row = document.querySelector('[data-record-row="' + recordType + '-' + recordId + '"]'); + if (!row || row.querySelector('[data-inline-edit]')) return; + + var originalContent = row.innerHTML; + + while (row.firstChild) row.removeChild(row.firstChild); + + var form = createEditForm(recordType, recordId, recordName, recordValue, recordTtl, recordPriority); + row.appendChild(form); + + form.querySelector('[data-cancel-edit]').addEventListener('click', function() { + row.innerHTML = originalContent; + htmx.process(row); + }); + + htmx.process(row); + } + + var recordsList = document.querySelector('.divide-y'); + if (recordsList) { + recordsList.addEventListener('click', handleEditClick); + } })(); </script>
\ No newline at end of file diff --git a/templates/domains/htmx/domains.htmx.django b/templates/domains/htmx/domains.htmx.django index 4c3e37f..86128f6 100644 --- a/templates/domains/htmx/domains.htmx.django +++ b/templates/domains/htmx/domains.htmx.django @@ -24,8 +24,6 @@ <div class="flex items-center gap-4"> {% url "domains.manage.detail" id=domain.ID as detail_domain_path %} <a href="{{ detail_domain_path }}" hx-get="{{ detail_domain_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="text-xs text-zinc-600 hover:text-zinc-300 transition-colors duration-150">DNS Records</a> - {% url "domains.manage.edit" id=domain.ID as edit_domain_path %} - <a href="{{ edit_domain_path }}" hx-get="{{ edit_domain_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="text-xs text-zinc-600 hover:text-zinc-300 transition-colors duration-150">Edit</a> {% url "domains.manage.delete" id=domain.ID as delete_domain_path %} <button data-confirm-trigger data-confirm-title="Delete {{ domain.Name }}.{{ domain.TLD.Name }}" data-confirm-message="This domain and all associated data will be permanently removed. This action cannot be undone." data-confirm-action="{{ delete_domain_path }}" class="text-xs text-zinc-600 hover:text-red-400 transition-colors duration-150">Delete</button> </div> diff --git a/templates/partials/sidebar.django b/templates/partials/sidebar.django index cb33ae5..01dd602 100644 --- a/templates/partials/sidebar.django +++ b/templates/partials/sidebar.django @@ -50,12 +50,6 @@ </svg> Domains </a> - <a href="{{ manage_path }}" hx-get="{{ manage_path }}" class="nav-link flex items-center gap-3 px-3 py-2 rounded-lg text-sm transition-colors duration-150"> - <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5"> - <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 0 0-3.375-3.375h-1.5A1.125 1.125 0 0 1 13.5 7.125v-1.5a3.375 3.375 0 0 0-3.375-3.375H8.25m0 12.75h7.5m-7.5 3H12M10.5 2.25H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 0 0-9-9Z" /> - </svg> - DNS Records - </a> </div> </div> diff --git a/utils/email/defaults.go b/utils/email/defaults.go index 56bee21..5aa11de 100644 --- a/utils/email/defaults.go +++ b/utils/email/defaults.go @@ -4,4 +4,4 @@ const ( AddressJoiner = ", " LogPrefix = "Email" SnippetLength = 200 -)
\ No newline at end of file +) diff --git a/utils/smtp/messages.go b/utils/smtp/messages.go index 1c82ab6..0fd280b 100644 --- a/utils/smtp/messages.go +++ b/utils/smtp/messages.go @@ -7,6 +7,7 @@ const ( InvalidCredentials = "Invalid credentials." ListenFailed = "Failed to start %s listener: %v" MailFrom = "Mail from: %s" + MXLookupFailed = "MX lookup failed for %s: %v" MessageParseFailed = "Failed to parse incoming message: %v" MessageReceived = "Message received (%d bytes) from %s to %v." MessageStoreFailed = "Failed to store message: %v" diff --git a/utils/smtp/session.go b/utils/smtp/session.go index ef5ddc7..e9efbd9 100644 --- a/utils/smtp/session.go +++ b/utils/smtp/session.go @@ -10,6 +10,7 @@ import ( "dove/utils/storage" "fmt" "io" + "net" "strings" "time" @@ -63,6 +64,12 @@ func (self *Session) Data(messageReader io.Reader) error { deliveredToLocal := false for _, recipientAddress := range self.toAddresses { + recipientDomain := extractDomain(recipientAddress) + if recipientDomain == "" || !domainHasMXRecords(recipientDomain) { + logger.Warnf(LogPrefix, UnknownRecipientDomain, recipientDomain) + continue + } + recipientMailbox := mailRepo.FindMailboxByAddress(recipientAddress) if recipientMailbox == nil { aliasMailbox := mailRepo.FindMailboxByAlias(recipientAddress) @@ -164,3 +171,21 @@ func deliverToLocalMailbox(mailbox *mailModel.Mailbox, parsedEmail *email.Parsed return nil } + +func extractDomain(emailAddress string) string { + atIndex := strings.LastIndex(emailAddress, "@") + if atIndex < 0 || atIndex >= len(emailAddress)-1 { + return "" + } + return emailAddress[atIndex+1:] +} + +func domainHasMXRecords(domainName string) bool { + mxRecords, lookupError := net.LookupMX(domainName) + if lookupError != nil { + logger.Debugf(LogPrefix, MXLookupFailed, domainName, lookupError) + return false + } + + return len(mxRecords) > 0 +} |
