Browse Source

Generalize upstream server URLs

koushin now takes a list of upstream URLs instead of an IMAP and SMTP
URL. This allows to specify upstream server URLs for plugins. In the
future, this will allow for auto-discovering upstream servers based on a
single domain name.

References: https://todo.sr.ht/~sircmpwn/koushin/49
master
Simon Ser 4 years ago
parent
commit
db328bf7c3
No known key found for this signature in database GPG Key ID: FDE7BE0E88F5E48
2 changed files with 89 additions and 36 deletions
  1. +3
    -5
      cmd/koushin/main.go
  2. +86
    -31
      server.go

+ 3
- 5
cmd/koushin/main.go View File

@@ -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 {


+ 86
- 31
server.go View File

@@ -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.


Loading…
Cancel
Save