aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile2
-rw-r--r--config/config.go12
-rw-r--r--controllers/auth/auth.go2
-rw-r--r--controllers/dns/records.go23
-rw-r--r--controllers/domain/domain.go21
-rw-r--r--database/migration.go2
-rw-r--r--database/resolve.go2
-rw-r--r--dove/main.go10
-rw-r--r--pages/domain/domain.go17
-rw-r--r--pages/mail/mailboxes.go2
-rw-r--r--pages/mail/users.go2
-rw-r--r--repositories/dns/a.go4
-rw-r--r--repositories/dns/aaaa.go4
-rw-r--r--repositories/dns/cname.go4
-rw-r--r--repositories/dns/mx.go4
-rw-r--r--repositories/dns/srv.go4
-rw-r--r--repositories/dns/txt.go4
-rw-r--r--router/domain.go5
-rw-r--r--services/auth/auth.go2
-rw-r--r--services/dns/messages.go2
-rw-r--r--services/dns/records.go129
-rw-r--r--services/domain/domain.go67
-rw-r--r--services/domain/messages.go35
-rw-r--r--services/mail/aliases.go2
-rw-r--r--services/mail/mailboxes.go1
-rw-r--r--services/mail/messages.go44
-rw-r--r--services/mail/users.go2
-rw-r--r--tags/defaults.go2
-rw-r--r--tags/messages.go2
-rw-r--r--tags/tags.go2
-rw-r--r--tags/url.go2
-rw-r--r--templates/domains/htmx/detail.htmx.django228
-rw-r--r--templates/domains/htmx/domains.htmx.django2
-rw-r--r--templates/partials/sidebar.django6
-rw-r--r--utils/email/defaults.go2
-rw-r--r--utils/smtp/messages.go1
-rw-r--r--utils/smtp/session.go25
37 files changed, 456 insertions, 224 deletions
diff --git a/Makefile b/Makefile
index 1e64aa9..e82d330 100644
--- a/Makefile
+++ b/Makefile
@@ -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
+}