aboutsummaryrefslogtreecommitdiff
path: root/utils
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-03-08 17:00:49 +0530
committerBobby <[email protected]>2026-03-08 17:00:49 +0530
commit2d5fb5e2078e92e7ec19582c3954409dd93f89fd (patch)
tree932f96385d56c94596cb2bb073f0f72b13d3eee4 /utils
parent0f254730178c9b0d9b171fef49993071a4b6a0f1 (diff)
downloaddove-2d5fb5e2078e92e7ec19582c3954409dd93f89fd.tar.xz
dove-2d5fb5e2078e92e7ec19582c3954409dd93f89fd.zip
feat(dns): Implement DNS record management and query handling
- Added models for various DNS record types: A, AAAA, CNAME, MX, SRV, and TXT. - Created repository functions for CRUD operations on DNS records. - Developed DNS server functionality to handle incoming queries and forward them to upstream servers. - Implemented local resolution for DNS queries, including support for A, AAAA, CNAME, MX, TXT, and SRV records. - Enhanced SMTP server to support TLS and STARTTLS configurations. - Improved email session handling with local delivery and error logging. - Added new log messages for better traceability of DNS operations and SMTP actions.
Diffstat (limited to 'utils')
-rw-r--r--utils/dns/defaults.go7
-rw-r--r--utils/dns/messages.go11
-rw-r--r--utils/dns/server.go153
-rw-r--r--utils/dns/upstream.go153
-rw-r--r--utils/smtp/messages.go32
-rw-r--r--utils/smtp/server.go41
-rw-r--r--utils/smtp/session.go108
7 files changed, 490 insertions, 15 deletions
diff --git a/utils/dns/defaults.go b/utils/dns/defaults.go
new file mode 100644
index 0000000..dc9824a
--- /dev/null
+++ b/utils/dns/defaults.go
@@ -0,0 +1,7 @@
+package dns
+
+const (
+ LogPrefix = "DNS"
+ UpstreamTimeoutSeconds = 5
+ UpstreamResponseTTL = 60
+)
diff --git a/utils/dns/messages.go b/utils/dns/messages.go
new file mode 100644
index 0000000..a73c8de
--- /dev/null
+++ b/utils/dns/messages.go
@@ -0,0 +1,11 @@
+package dns
+
+const (
+ ForwardFailed = "Failed to forward query for %s to upstream: %v"
+ ForwardSuccess = "Forwarded query for %s to upstream."
+ ListenFailed = "Failed to start DNS listener: %v"
+ QueryReceived = "Query: %s %s"
+ ServerStarting = "DNS server started on %s (UDP)."
+ ShutdownComplete = "DNS server stopped."
+ ShutdownFailed = "Failed to shutdown DNS server: %v"
+)
diff --git a/utils/dns/server.go b/utils/dns/server.go
new file mode 100644
index 0000000..7ae8770
--- /dev/null
+++ b/utils/dns/server.go
@@ -0,0 +1,153 @@
+package dns
+
+import (
+ "fmt"
+ "net"
+
+ "dove/config"
+ dnsRepo "dove/repositories/dns"
+ "dove/utils/logger"
+
+ mdns "github.com/miekg/dns"
+)
+
+var activeServer *mdns.Server
+
+func Start() {
+ address := fmt.Sprintf("%s:%d", config.DNS.Host, config.DNS.Port)
+
+ mdns.HandleFunc(".", handleQuery)
+
+ activeServer = &mdns.Server{
+ Addr: address,
+ Net: "udp",
+ }
+
+ go func() {
+ logger.Successf(LogPrefix, ServerStarting, address)
+
+ if listenError := activeServer.ListenAndServe(); listenError != nil {
+ logger.Fatalf(LogPrefix, ListenFailed, listenError)
+ }
+ }()
+}
+
+func Shutdown() {
+ if activeServer == nil {
+ return
+ }
+
+ if shutdownError := activeServer.Shutdown(); shutdownError != nil {
+ logger.Errorf(LogPrefix, ShutdownFailed, shutdownError)
+ }
+
+ logger.Infof(LogPrefix, ShutdownComplete)
+}
+
+func handleQuery(writer mdns.ResponseWriter, request *mdns.Msg) {
+ if len(request.Question) == 0 {
+ return
+ }
+
+ question := request.Question[0]
+ queryName := question.Name
+ queryType := mdns.TypeToString[question.Qtype]
+
+ logger.Debugf(LogPrefix, QueryReceived, queryType, queryName)
+
+ if dnsRepo.IsLocalDomain(queryName) {
+ response := resolveLocal(request)
+ writer.WriteMsg(response)
+ return
+ }
+
+ upstreamResponse := forwardToUpstream(request)
+ if upstreamResponse != nil {
+ writer.WriteMsg(upstreamResponse)
+ return
+ }
+
+ refusedResponse := &mdns.Msg{}
+ refusedResponse.SetRcode(request, mdns.RcodeServerFailure)
+ writer.WriteMsg(refusedResponse)
+}
+
+func resolveLocal(request *mdns.Msg) *mdns.Msg {
+ question := request.Question[0]
+ queryName := question.Name
+
+ response := &mdns.Msg{}
+ response.SetReply(request)
+ response.Authoritative = true
+ response.RecursionAvailable = true
+
+ switch question.Qtype {
+ case mdns.TypeA:
+ for _, record := range dnsRepo.ResolveA(queryName) {
+ parsedIP := net.ParseIP(record.Address)
+ if parsedIP == nil || parsedIP.To4() == nil {
+ continue
+ }
+
+ response.Answer = append(response.Answer, &mdns.A{
+ Hdr: mdns.RR_Header{Name: queryName, Rrtype: mdns.TypeA, Class: mdns.ClassINET, Ttl: record.TTL},
+ A: parsedIP.To4(),
+ })
+ }
+
+ case mdns.TypeAAAA:
+ for _, record := range dnsRepo.ResolveAAAA(queryName) {
+ parsedIP := net.ParseIP(record.Address)
+ if parsedIP == nil || parsedIP.To16() == nil {
+ continue
+ }
+
+ response.Answer = append(response.Answer, &mdns.AAAA{
+ Hdr: mdns.RR_Header{Name: queryName, Rrtype: mdns.TypeAAAA, Class: mdns.ClassINET, Ttl: record.TTL},
+ AAAA: parsedIP.To16(),
+ })
+ }
+
+ case mdns.TypeCNAME:
+ for _, record := range dnsRepo.ResolveCNAME(queryName) {
+ response.Answer = append(response.Answer, &mdns.CNAME{
+ Hdr: mdns.RR_Header{Name: queryName, Rrtype: mdns.TypeCNAME, Class: mdns.ClassINET, Ttl: record.TTL},
+ Target: mdns.Fqdn(record.Target),
+ })
+ }
+
+ case mdns.TypeMX:
+ for _, record := range dnsRepo.ResolveMX(queryName) {
+ response.Answer = append(response.Answer, &mdns.MX{
+ Hdr: mdns.RR_Header{Name: queryName, Rrtype: mdns.TypeMX, Class: mdns.ClassINET, Ttl: record.TTL},
+ Preference: record.Priority,
+ Mx: mdns.Fqdn(record.Target),
+ })
+ }
+
+ case mdns.TypeTXT:
+ for _, record := range dnsRepo.ResolveTXT(queryName) {
+ response.Answer = append(response.Answer, &mdns.TXT{
+ Hdr: mdns.RR_Header{Name: queryName, Rrtype: mdns.TypeTXT, Class: mdns.ClassINET, Ttl: record.TTL},
+ Txt: []string{record.Content},
+ })
+ }
+
+ case mdns.TypeSRV:
+ for _, record := range dnsRepo.ResolveSRV(queryName) {
+ response.Answer = append(response.Answer, &mdns.SRV{
+ Hdr: mdns.RR_Header{Name: queryName, Rrtype: mdns.TypeSRV, Class: mdns.ClassINET, Ttl: record.TTL},
+ Priority: record.Priority,
+ Weight: record.Weight,
+ Port: record.Port,
+ Target: mdns.Fqdn(record.Target),
+ })
+ }
+ }
+
+ if len(response.Answer) == 0 {
+ response.Rcode = mdns.RcodeNameError
+ }
+
+ return response
+}
diff --git a/utils/dns/upstream.go b/utils/dns/upstream.go
new file mode 100644
index 0000000..e2af583
--- /dev/null
+++ b/utils/dns/upstream.go
@@ -0,0 +1,153 @@
+package dns
+
+import (
+ "context"
+ "net"
+ "time"
+
+ "dove/utils/logger"
+
+ mdns "github.com/miekg/dns"
+)
+
+func forwardToUpstream(request *mdns.Msg) *mdns.Msg {
+ if len(request.Question) == 0 {
+ return nil
+ }
+
+ question := request.Question[0]
+ queryName := question.Name
+
+ resolver := &net.Resolver{PreferGo: false}
+ ctx, cancel := context.WithTimeout(context.Background(), time.Duration(UpstreamTimeoutSeconds)*time.Second)
+ defer cancel()
+
+ response := &mdns.Msg{}
+ response.SetReply(request)
+ response.Authoritative = false
+ response.RecursionAvailable = true
+
+ switch question.Qtype {
+ case mdns.TypeA:
+ addresses, lookupError := resolver.LookupIPAddr(ctx, mdns.Fqdn(queryName))
+ if lookupError != nil {
+ logger.Debugf(LogPrefix, ForwardFailed, queryName, lookupError)
+ response.Rcode = mdns.RcodeNameError
+ return response
+ }
+
+ for _, address := range addresses {
+ if ipv4 := address.IP.To4(); ipv4 != nil {
+ response.Answer = append(response.Answer, &mdns.A{
+ Hdr: mdns.RR_Header{Name: queryName, Rrtype: mdns.TypeA, Class: mdns.ClassINET, Ttl: UpstreamResponseTTL},
+ A: ipv4,
+ })
+ }
+ }
+
+ case mdns.TypeAAAA:
+ addresses, lookupError := resolver.LookupIPAddr(ctx, mdns.Fqdn(queryName))
+ if lookupError != nil {
+ logger.Debugf(LogPrefix, ForwardFailed, queryName, lookupError)
+ response.Rcode = mdns.RcodeNameError
+ return response
+ }
+
+ for _, address := range addresses {
+ if address.IP.To4() == nil && address.IP.To16() != nil {
+ response.Answer = append(response.Answer, &mdns.AAAA{
+ Hdr: mdns.RR_Header{Name: queryName, Rrtype: mdns.TypeAAAA, Class: mdns.ClassINET, Ttl: UpstreamResponseTTL},
+ AAAA: address.IP.To16(),
+ })
+ }
+ }
+
+ case mdns.TypeCNAME:
+ canonicalName, lookupError := resolver.LookupCNAME(ctx, mdns.Fqdn(queryName))
+ if lookupError != nil {
+ logger.Debugf(LogPrefix, ForwardFailed, queryName, lookupError)
+ response.Rcode = mdns.RcodeNameError
+ return response
+ }
+
+ response.Answer = append(response.Answer, &mdns.CNAME{
+ Hdr: mdns.RR_Header{Name: queryName, Rrtype: mdns.TypeCNAME, Class: mdns.ClassINET, Ttl: UpstreamResponseTTL},
+ Target: mdns.Fqdn(canonicalName),
+ })
+
+ case mdns.TypeMX:
+ mxRecords, lookupError := resolver.LookupMX(ctx, mdns.Fqdn(queryName))
+ if lookupError != nil {
+ logger.Debugf(LogPrefix, ForwardFailed, queryName, lookupError)
+ response.Rcode = mdns.RcodeNameError
+ return response
+ }
+
+ for _, mxRecord := range mxRecords {
+ response.Answer = append(response.Answer, &mdns.MX{
+ Hdr: mdns.RR_Header{Name: queryName, Rrtype: mdns.TypeMX, Class: mdns.ClassINET, Ttl: UpstreamResponseTTL},
+ Preference: mxRecord.Pref,
+ Mx: mdns.Fqdn(mxRecord.Host),
+ })
+ }
+
+ case mdns.TypeTXT:
+ txtRecords, lookupError := resolver.LookupTXT(ctx, mdns.Fqdn(queryName))
+ if lookupError != nil {
+ logger.Debugf(LogPrefix, ForwardFailed, queryName, lookupError)
+ response.Rcode = mdns.RcodeNameError
+ return response
+ }
+
+ if len(txtRecords) > 0 {
+ response.Answer = append(response.Answer, &mdns.TXT{
+ Hdr: mdns.RR_Header{Name: queryName, Rrtype: mdns.TypeTXT, Class: mdns.ClassINET, Ttl: UpstreamResponseTTL},
+ Txt: txtRecords,
+ })
+ }
+
+ case mdns.TypeSRV:
+ _, srvRecords, lookupError := resolver.LookupSRV(ctx, "", "", mdns.Fqdn(queryName))
+ if lookupError != nil {
+ logger.Debugf(LogPrefix, ForwardFailed, queryName, lookupError)
+ response.Rcode = mdns.RcodeNameError
+ return response
+ }
+
+ for _, srvRecord := range srvRecords {
+ response.Answer = append(response.Answer, &mdns.SRV{
+ Hdr: mdns.RR_Header{Name: queryName, Rrtype: mdns.TypeSRV, Class: mdns.ClassINET, Ttl: UpstreamResponseTTL},
+ Priority: srvRecord.Priority,
+ Weight: srvRecord.Weight,
+ Port: srvRecord.Port,
+ Target: mdns.Fqdn(srvRecord.Target),
+ })
+ }
+
+ case mdns.TypeNS:
+ nsRecords, lookupError := resolver.LookupNS(ctx, mdns.Fqdn(queryName))
+ if lookupError != nil {
+ logger.Debugf(LogPrefix, ForwardFailed, queryName, lookupError)
+ response.Rcode = mdns.RcodeNameError
+ return response
+ }
+
+ for _, nsRecord := range nsRecords {
+ response.Answer = append(response.Answer, &mdns.NS{
+ Hdr: mdns.RR_Header{Name: queryName, Rrtype: mdns.TypeNS, Class: mdns.ClassINET, Ttl: UpstreamResponseTTL},
+ Ns: mdns.Fqdn(nsRecord.Host),
+ })
+ }
+
+ default:
+ response.Rcode = mdns.RcodeNotImplemented
+ return response
+ }
+
+ if len(response.Answer) == 0 {
+ response.Rcode = mdns.RcodeNameError
+ }
+
+ logger.Debugf(LogPrefix, ForwardSuccess, queryName)
+ return response
+}
diff --git a/utils/smtp/messages.go b/utils/smtp/messages.go
index 90568e0..6441fde 100644
--- a/utils/smtp/messages.go
+++ b/utils/smtp/messages.go
@@ -1,15 +1,25 @@
package smtp
const (
- AuthFailed = "Authentication failed for user: %s"
- InvalidCredentials = "Invalid credentials."
- ListenFailed = "Failed to start %s listener: %v"
- MailFrom = "Mail from: %s"
- MessageReceived = "Message received (%d bytes)."
- MessageStoreFailed = "Failed to store message: %v"
- Recipient = "Recipient: %s"
- ServerStarting = "%s listener started on %s."
- SessionStarted = "New session from %s."
- ShutdownComplete = "All listeners stopped."
- ShutdownFailed = "Failed to shutdown %s listener: %v"
+ AuthFailed = "Authentication failed for user: %s"
+ DeliveryFailed = "Failed to deliver to %s: %v"
+ DeliverySuccess = "Delivered to local mailbox: %s"
+ InvalidCredentials = "Invalid credentials."
+ ListenFailed = "Failed to start %s listener: %v"
+ MailFrom = "Mail from: %s"
+ MessageParseFailed = "Failed to parse incoming message: %v"
+ MessageReceived = "Message received (%d bytes) from %s to %v."
+ MessageStoreFailed = "Failed to store message: %v"
+ NoLocalRecipients = "No local recipients found for message from %s."
+ Recipient = "Recipient: %s"
+ RecipientNotLocal = "Recipient %s is not a local mailbox."
+ RelayDeliveryFailed = "Failed to relay message to %s: %v"
+ RelayDeliverySuccess = "Relayed message to %s via %s:%d."
+ RelayDisabled = "Relay disabled. External recipient %s will not receive the message."
+ ServerStarting = "%s listener started on %s."
+ SessionStarted = "New session from %s."
+ ShutdownComplete = "All listeners stopped."
+ ShutdownFailed = "Failed to shutdown %s listener: %v"
+ TLSCertLoadFailed = "Failed to load TLS certificate: %v"
+ UnknownRecipientDomain = "Recipient domain %s is not managed by this server."
)
diff --git a/utils/smtp/server.go b/utils/smtp/server.go
index 2773647..5260d20 100644
--- a/utils/smtp/server.go
+++ b/utils/smtp/server.go
@@ -1,6 +1,7 @@
package smtp
import (
+ "crypto/tls"
"dove/config"
"dove/utils/logger"
"fmt"
@@ -19,10 +20,26 @@ var activeServers []ServerInstance
func Start() {
plainAddress := fmt.Sprintf("%s:%d", config.SMTP.Host, config.SMTP.Port)
plainServer := createServer(plainAddress)
+ activeServers = append(activeServers, ServerInstance{Server: plainServer, Label: "SMTP"})
+ go startListener(plainServer, "SMTP", plainAddress)
- activeServers = append(activeServers, ServerInstance{Server: plainServer, Label: LogPrefix})
+ if config.SMTP.TLSEnabled {
+ tlsConfig := loadTLSConfig()
+ if tlsConfig != nil {
+ smtpsAddress := fmt.Sprintf("%s:%d", config.SMTP.Host, config.SMTP.SMTPSPort)
+ smtpsServer := createServer(smtpsAddress)
+ smtpsServer.TLSConfig = tlsConfig
+ activeServers = append(activeServers, ServerInstance{Server: smtpsServer, Label: "SMTPS"})
+ go startTLSListener(smtpsServer, "SMTPS", smtpsAddress)
- go startListener(plainServer, LogPrefix, plainAddress)
+ starttlsAddress := fmt.Sprintf("%s:%d", config.SMTP.Host, config.SMTP.StartTLSPort)
+ starttlsServer := createServer(starttlsAddress)
+ starttlsServer.TLSConfig = tlsConfig
+ starttlsServer.EnableSMTPUTF8 = true
+ activeServers = append(activeServers, ServerInstance{Server: starttlsServer, Label: "STARTTLS"})
+ go startListener(starttlsServer, "STARTTLS", starttlsAddress)
+ }
+ }
}
func Shutdown() {
@@ -51,6 +68,18 @@ func createServer(address string) *gosmtp.Server {
return smtpServer
}
+func loadTLSConfig() *tls.Config {
+ certificate, loadError := tls.LoadX509KeyPair(config.SMTP.TLSCertPath, config.SMTP.TLSKeyPath)
+ if loadError != nil {
+ logger.Errorf(LogPrefix, TLSCertLoadFailed, loadError)
+ return nil
+ }
+
+ return &tls.Config{
+ Certificates: []tls.Certificate{certificate},
+ }
+}
+
func startListener(smtpServer *gosmtp.Server, label string, address string) {
logger.Successf(LogPrefix, ServerStarting, label, address)
@@ -58,3 +87,11 @@ func startListener(smtpServer *gosmtp.Server, label string, address string) {
logger.Fatalf(LogPrefix, ListenFailed, label, listenError)
}
}
+
+func startTLSListener(smtpServer *gosmtp.Server, label string, address string) {
+ logger.Successf(LogPrefix, ServerStarting, label, address)
+
+ if listenError := smtpServer.ListenAndServeTLS(); listenError != nil {
+ logger.Fatalf(LogPrefix, ListenFailed, label, listenError)
+ }
+}
diff --git a/utils/smtp/session.go b/utils/smtp/session.go
index 73fc4e1..ef5ddc7 100644
--- a/utils/smtp/session.go
+++ b/utils/smtp/session.go
@@ -2,9 +2,16 @@ package smtp
import (
"dove/config"
+ mailModel "dove/models/mail"
+ mailRepo "dove/repositories/mail"
+ "dove/utils/email"
"dove/utils/errors"
"dove/utils/logger"
+ "dove/utils/storage"
+ "fmt"
"io"
+ "strings"
+ "time"
gosmtp "github.com/emersion/go-smtp"
)
@@ -45,9 +52,41 @@ func (self *Session) Data(messageReader io.Reader) error {
return readError
}
- logger.Infof(LogPrefix, MessageReceived, len(rawMessage))
+ logger.Infof(LogPrefix, MessageReceived, len(rawMessage), self.fromAddress, self.toAddresses)
- _ = rawMessage
+ parsedEmail, parseError := email.Parse(rawMessage)
+ if parseError != nil {
+ logger.Errorf(LogPrefix, MessageParseFailed, parseError)
+ return parseError
+ }
+
+ deliveredToLocal := false
+
+ for _, recipientAddress := range self.toAddresses {
+ recipientMailbox := mailRepo.FindMailboxByAddress(recipientAddress)
+ if recipientMailbox == nil {
+ aliasMailbox := mailRepo.FindMailboxByAlias(recipientAddress)
+ if aliasMailbox != nil {
+ recipientMailbox = aliasMailbox
+ }
+ }
+
+ if recipientMailbox != nil {
+ deliverError := deliverToLocalMailbox(recipientMailbox, parsedEmail, rawMessage)
+ if deliverError != nil {
+ logger.Errorf(LogPrefix, DeliveryFailed, recipientAddress, deliverError)
+ continue
+ }
+ logger.Infof(LogPrefix, DeliverySuccess, recipientAddress)
+ deliveredToLocal = true
+ } else {
+ logger.Debugf(LogPrefix, RecipientNotLocal, recipientAddress)
+ }
+ }
+
+ if !deliveredToLocal {
+ logger.Warnf(LogPrefix, NoLocalRecipients, self.fromAddress)
+ }
return nil
}
@@ -60,3 +99,68 @@ func (self *Session) Reset() {
func (self *Session) Logout() error {
return nil
}
+
+func deliverToLocalMailbox(mailbox *mailModel.Mailbox, parsedEmail *email.ParsedEmail, rawMessage []byte) error {
+ inboxFolder := mailRepo.FindFolderBySlug(mailbox.ID, "inbox")
+ if inboxFolder == nil {
+ return errors.Error("Inbox folder not found for mailbox %s.", mailbox.Address)
+ }
+
+ filename := fmt.Sprintf("%d", time.Now().UnixNano())
+
+ toAddressesJoined := strings.Join(parsedEmail.ToAddresses, ", ")
+ ccAddressesJoined := strings.Join(parsedEmail.CcAddresses, ", ")
+ bccAddressesJoined := strings.Join(parsedEmail.BccAddresses, ", ")
+
+ attachmentCount := 0
+ inlineCount := 0
+ for _, attachment := range parsedEmail.Attachments {
+ if attachment.IsInline {
+ inlineCount++
+ } else {
+ attachmentCount++
+ }
+ }
+
+ emailRecord := &mailModel.Email{
+ MailboxID: mailbox.ID,
+ FolderID: inboxFolder.ID,
+ MessageID: parsedEmail.MessageID,
+ Filename: filename,
+ FromAddress: parsedEmail.FromAddress,
+ FromName: parsedEmail.FromName,
+ ToAddresses: toAddressesJoined,
+ CcAddresses: ccAddressesJoined,
+ BccAddresses: bccAddressesJoined,
+ ReplyToAddress: parsedEmail.ReplyToAddress,
+ ReturnPath: parsedEmail.ReturnPath,
+ Subject: parsedEmail.Subject,
+ Snippet: parsedEmail.Snippet,
+ Size: parsedEmail.Size,
+ IsRead: false,
+ AttachmentCount: attachmentCount,
+ InlineCount: inlineCount,
+ }
+
+ if createError := mailRepo.CreateEmail(emailRecord); createError != nil {
+ return createError
+ }
+
+ for _, parsedAttachment := range parsedEmail.Attachments {
+ attachmentRecord := &mailModel.Attachment{
+ EmailID: emailRecord.ID,
+ Filename: parsedAttachment.Filename,
+ ContentType: parsedAttachment.ContentType,
+ ContentID: parsedAttachment.ContentID,
+ Size: parsedAttachment.Size,
+ IsInline: parsedAttachment.IsInline,
+ }
+ mailRepo.CreateAttachment(attachmentRecord)
+ }
+
+ if writeError := storage.WriteMailFile(mailbox.Address, "inbox", filename, rawMessage); writeError != nil {
+ logger.Warnf(LogPrefix, MessageStoreFailed, writeError)
+ }
+
+ return nil
+}