diff options
-rw-r--r-- | cmd/koushin/main.go | 8 | ||||
-rw-r--r-- | server.go | 117 |
2 files changed, 89 insertions, 36 deletions
diff --git a/cmd/koushin/main.go b/cmd/koushin/main.go index 472cd4a..d22b404 100644 --- a/cmd/koushin/main.go +++ b/cmd/koushin/main.go @@ -22,20 +22,18 @@ func main() { flag.StringVar(&addr, "addr", ":1323", "listening address") flag.Usage = func() { - fmt.Fprintf(flag.CommandLine.Output(), "usage: koushin [options...] <IMAP URL> [SMTP URL]\n") + fmt.Fprintf(flag.CommandLine.Output(), "usage: koushin [options...] <upstream server...>\n") flag.PrintDefaults() } flag.Parse() - if flag.NArg() < 1 || flag.NArg() > 2 { + options.Upstreams = flag.Args() + if len(options.Upstreams) == 0 { flag.Usage() return } - options.IMAPURL = flag.Arg(0) - options.SMTPURL = flag.Arg(1) - e := echo.New() e.HideBanner = true if l, ok := e.Logger.(*log.Logger); ok { @@ -22,6 +22,9 @@ type Server struct { plugins []Plugin luaPlugins []Plugin + // maps protocols to URLs (protocol can be empty for auto-discovery) + upstreams map[string]*url.URL + imap struct { host string tls bool @@ -35,45 +38,115 @@ type Server struct { defaultTheme string } -func (s *Server) parseIMAPURL(imapURL string) error { - u, err := url.Parse(imapURL) +func newServer(e *echo.Echo, options *Options) (*Server, error) { + s := &Server{e: e, defaultTheme: options.Theme} + + s.upstreams = make(map[string]*url.URL, len(options.Upstreams)) + for _, upstream := range options.Upstreams { + u, err := parseUpstream(upstream) + if err != nil { + return nil, fmt.Errorf("failed to parse upstream %q: %v", upstream, err) + } + if _, ok := s.upstreams[u.Scheme]; ok { + return nil, fmt.Errorf("found two upstream servers for scheme %q", u.Scheme) + } + s.upstreams[u.Scheme] = u + } + + if err := s.parseIMAPUpstream(); err != nil { + return nil, err + } + if err := s.parseSMTPUpstream(); err != nil { + return nil, err + } + + s.Sessions = newSessionManager(s.dialIMAP, s.dialSMTP) + + return s, nil +} + +func parseUpstream(s string) (*url.URL, error) { + if !strings.ContainsAny(s, ":/") { + // This is a raw domain name, make it an URL with an empty scheme + s = "//" + s + } + return url.Parse(s) +} + +type NoUpstreamError struct { + schemes []string +} + +func (err *NoUpstreamError) Error() string { + return fmt.Sprintf("no upstream server configured for schemes %v", err.schemes) +} + +// Upstream retrieves the configured upstream server URL for the provided +// schemes. If no configured upstream server matches, a *NoUpstreamError is +// returned. An empty URL.Scheme means that the caller needs to perform +// auto-discovery with URL.Host. +func (s *Server) Upstream(schemes... string) (*url.URL, error) { + var urls []*url.URL + for _, scheme := range append(schemes, "") { + u, ok := s.upstreams[scheme] + if ok { + urls = append(urls, u) + } + } + if len(urls) == 0 { + return nil, &NoUpstreamError{schemes} + } + if len(urls) > 1 { + return nil, fmt.Errorf("multiple upstream servers are configured for schemes %v", schemes) + } + return urls[0], nil +} + +func (s *Server) parseIMAPUpstream() error { + u, err := s.Upstream("imap", "imaps", "imap+insecure") if err != nil { - return fmt.Errorf("failed to parse IMAP server URL: %v", err) + return fmt.Errorf("failed to parse upstream IMAP server: %v", err) } s.imap.host = u.Host switch u.Scheme { case "imap": // This space is intentionally left blank - case "imaps": + case "imaps", "": + // TODO: auto-discovery for empty scheme s.imap.tls = true case "imap+insecure": s.imap.insecure = true default: - return fmt.Errorf("unrecognized IMAP URL scheme: %s", u.Scheme) + panic("unreachable") } + s.e.Logger.Printf("Configured upstream IMAP server: %v", u) return nil } -func (s *Server) parseSMTPURL(smtpURL string) error { - u, err := url.Parse(smtpURL) - if err != nil { - return fmt.Errorf("failed to parse SMTP server URL: %v", err) +func (s *Server) parseSMTPUpstream() error { + u, err := s.Upstream("smtp", "smtps", "smtp+insecure") + if _, ok := err.(*NoUpstreamError); ok { + return nil + } else if err != nil { + return fmt.Errorf("failed to parse upstream SMTP server: %v", err) } s.smtp.host = u.Host switch u.Scheme { case "smtp": // This space is intentionally left blank - case "smtps": + case "smtps", "": + // TODO: auto-discovery for empty scheme s.smtp.tls = true case "smtp+insecure": s.smtp.insecure = true default: - return fmt.Errorf("unrecognized SMTP URL scheme: %s", u.Scheme) + panic("unreachable") } + s.e.Logger.Printf("Configured upstream SMTP server: %v", u) return nil } @@ -123,24 +196,6 @@ func (s *Server) Reload() error { return s.load() } -func newServer(e *echo.Echo, options *Options) (*Server, error) { - s := &Server{e: e, defaultTheme: options.Theme} - - if err := s.parseIMAPURL(options.IMAPURL); err != nil { - return nil, err - } - - if options.SMTPURL != "" { - if err := s.parseSMTPURL(options.SMTPURL); err != nil { - return nil, err - } - } - - s.Sessions = newSessionManager(s.dialIMAP, s.dialSMTP) - - return s, nil -} - // Context is the context used by HTTP handlers. // // Use a type assertion to get it from a echo.Context: @@ -197,8 +252,8 @@ func handleUnauthenticated(next echo.HandlerFunc, ctx *Context) error { } type Options struct { - IMAPURL, SMTPURL string - Theme string + Upstreams []string + Theme string } // New creates a new server. |