diff --git a/README.md b/README.md index ec90d82..2e3f193 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,12 @@ ## Usage +Assuming SRV DNS records are properly set up (see [RFC 6186]): + + go run example.org + +To manually specify upstream servers: + go run ./cmd/koushin imaps://mail.example.org:993 smtps://mail.example.org:465 See `-h` for more information. @@ -55,6 +61,7 @@ Send patches on the [mailing list], report bugs on the [issue tracker]. MIT +[RFC 6186]: https://tools.ietf.org/html/rfc6186 [Go plugin helpers]: https://godoc.org/git.sr.ht/~emersion/koushin#GoPlugin [mailing list]: https://lists.sr.ht/~sircmpwn/koushin [issue tracker]: https://todo.sr.ht/~sircmpwn/koushin diff --git a/discover.go b/discover.go new file mode 100644 index 0000000..08d4aa1 --- /dev/null +++ b/discover.go @@ -0,0 +1,66 @@ +package koushin + +import ( + "fmt" + "net" + "net/url" + "strings" +) + +func discoverTCP(service, name string) (string, error) { + _, addrs, err := net.LookupSRV(service, "tcp", name) + if dnsErr, ok := err.(*net.DNSError); ok { + if dnsErr.IsTemporary { + return "", err + } + } else if err != nil { + return "", err + } + + if len(addrs) == 0 { + return "", nil + } + addr := addrs[0] + + target := strings.TrimSuffix(addr.Target, ".") + if target == "" { + return "", nil + } + + return fmt.Sprintf("%v:%v", target, addr.Port), nil +} + +// discoverIMAP performs a DNS-based IMAP service discovery, as defined in +// RFC 6186 section 3.2. +func discoverIMAP(domain string) (*url.URL, error) { + imapsHost, err := discoverTCP("imaps", domain) + if err != nil { + return nil, err + } + if imapsHost != "" { + return &url.URL{Scheme: "imaps", Host: imapsHost}, nil + } + + imapHost, err := discoverTCP("imap", domain) + if err != nil { + return nil, err + } + if imapHost != "" { + return &url.URL{Scheme: "imap", Host: imapHost}, nil + } + + return nil, fmt.Errorf("IMAP service discovery not configured for domain %q", domain) +} + +// discoverSMTP performs a DNS-based SMTP submission service discovery, as +// defined in RFC 6186 section 3.1. +func discoverSMTP(domain string) (*url.URL, error) { + host, err := discoverTCP("submission", domain) + if err != nil { + return nil, err + } + if host == "" { + return nil, fmt.Errorf("SMTP service discovery not configured for domain %q", domain) + } + return &url.URL{Scheme: "smtp", Host: host}, nil +} diff --git a/server.go b/server.go index 71f40bf..11b55f9 100644 --- a/server.go +++ b/server.go @@ -108,12 +108,18 @@ func (s *Server) parseIMAPUpstream() error { return fmt.Errorf("failed to parse upstream IMAP server: %v", err) } + if u.Scheme == "" { + u, err = discoverIMAP(u.Host) + if err != nil { + return fmt.Errorf("failed to discover IMAP server: %v", err) + } + } + s.imap.host = u.Host switch u.Scheme { case "imap": // This space is intentionally left blank - case "imaps", "": - // TODO: auto-discovery for empty scheme + case "imaps": s.imap.tls = true case "imap+insecure": s.imap.insecure = true @@ -133,12 +139,19 @@ func (s *Server) parseSMTPUpstream() error { return fmt.Errorf("failed to parse upstream SMTP server: %v", err) } + if u.Scheme == "" { + u, err = discoverSMTP(u.Host) + if err != nil { + s.e.Logger.Printf("Failed to discover SMTP server: %v", err) + return nil + } + } + s.smtp.host = u.Host switch u.Scheme { case "smtp": // This space is intentionally left blank - case "smtps", "": - // TODO: auto-discovery for empty scheme + case "smtps": s.smtp.tls = true case "smtp+insecure": s.smtp.insecure = true