diff options
| author | Bobby <[email protected]> | 2026-03-08 17:00:49 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2026-03-08 17:00:49 +0530 |
| commit | 2d5fb5e2078e92e7ec19582c3954409dd93f89fd (patch) | |
| tree | 932f96385d56c94596cb2bb073f0f72b13d3eee4 /utils | |
| parent | 0f254730178c9b0d9b171fef49993071a4b6a0f1 (diff) | |
| download | dove-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.go | 7 | ||||
| -rw-r--r-- | utils/dns/messages.go | 11 | ||||
| -rw-r--r-- | utils/dns/server.go | 153 | ||||
| -rw-r--r-- | utils/dns/upstream.go | 153 | ||||
| -rw-r--r-- | utils/smtp/messages.go | 32 | ||||
| -rw-r--r-- | utils/smtp/server.go | 41 | ||||
| -rw-r--r-- | utils/smtp/session.go | 108 |
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 +} |
