diff --git a/README.md b/README.md index 874ac56..33288c8 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,16 @@ go run ./cmd/koushin imaps://mail.example.org:993 smtps://mail.example.org:465 +See `-h` for more information. + +## Themes + +They should be put in `public/themes//`. + +Templates in `public/themes//*.html` override default templates in +`public/*.html`. Assets in `public/themes//assets/*` are served by the +HTTP server at `themes//assets/*`. + ## License MIT diff --git a/cmd/koushin/main.go b/cmd/koushin/main.go index d30cb85..e644884 100644 --- a/cmd/koushin/main.go +++ b/cmd/koushin/main.go @@ -1,28 +1,41 @@ package main import ( + "flag" "fmt" - "os" "git.sr.ht/~emersion/koushin" + "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" + "github.com/labstack/gommon/log" ) func main() { - if len(os.Args) != 2 && len(os.Args) != 3 { - fmt.Println("usage: koushin [SMTP URL]") - return + var options koushin.Options + flag.StringVar(&options.Theme, "theme", "", "default theme") + + flag.Usage = func() { + fmt.Fprintf(flag.CommandLine.Output(), "usage: koushin [options...] [SMTP URL]\n") + flag.PrintDefaults() } - imapURL := os.Args[1] + flag.Parse() - var smtpURL string - if len(os.Args) == 3 { - smtpURL = os.Args[2] + if flag.NArg() < 1 || flag.NArg() > 2 { + flag.Usage() + return } - e := koushin.New(imapURL, smtpURL) - e.Use(middleware.Logger()) + options.IMAPURL = flag.Arg(0) + options.SMTPURL = flag.Arg(1) + + e := echo.New() + if l, ok := e.Logger.(*log.Logger); ok { + l.SetHeader("${time_rfc3339} ${level}") + } + if err := koushin.New(e, &options); err != nil { + e.Logger.Fatal(err) + } e.Use(middleware.Recover()) e.Logger.Fatal(e.Start(":1323")) } diff --git a/go.mod b/go.mod index be3095a..1301e14 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e github.com/emersion/go-smtp v0.12.0 github.com/labstack/echo/v4 v4.1.11 + github.com/labstack/gommon v0.3.0 github.com/mattn/go-colorable v0.1.4 // indirect github.com/mattn/go-isatty v0.0.10 // indirect github.com/valyala/fasttemplate v1.1.0 // indirect diff --git a/server.go b/server.go index 021ecff..d9542b5 100644 --- a/server.go +++ b/server.go @@ -79,7 +79,7 @@ func (s *Server) parseSMTPURL(smtpURL string) error { return nil } -func NewServer(imapURL, smtpURL string) (*Server, error) { +func newServer(imapURL, smtpURL string) (*Server, error) { s := &Server{} if err := s.parseIMAPURL(imapURL); err != nil { @@ -310,12 +310,25 @@ func handleCompose(ectx echo.Context) error { }) } -func New(imapURL, smtpURL string) *echo.Echo { - e := echo.New() +func isPublic(path string) bool { + return path == "/login" || strings.HasPrefix(path, "/assets/") || + strings.HasPrefix(path, "/themes/") +} + +type Options struct { + IMAPURL, SMTPURL string + Theme string +} - s, err := NewServer(imapURL, smtpURL) +func New(e *echo.Echo, options *Options) error { + s, err := newServer(options.IMAPURL, options.SMTPURL) if err != nil { - e.Logger.Fatal(err) + return err + } + + e.Renderer, err = loadTemplates(e.Logger, options.Theme) + if err != nil { + return fmt.Errorf("failed to load templates: %v", err) } e.HTTPErrorHandler = func(err error, c echo.Context) { @@ -336,7 +349,7 @@ func New(imapURL, smtpURL string) *echo.Echo { cookie, err := ctx.Cookie(cookieName) if err == http.ErrNoCookie { // Require auth for all pages except /login - if ctx.Path() == "/login" || strings.HasPrefix(ctx.Path(), "/assets/") { + if isPublic(ctx.Path()) { return next(ctx) } else { return ctx.Redirect(http.StatusFound, "/login") @@ -357,11 +370,6 @@ func New(imapURL, smtpURL string) *echo.Echo { } }) - e.Renderer, err = loadTemplates() - if err != nil { - e.Logger.Fatal("Failed to load templates:", err) - } - e.GET("/mailbox/:mbox", func(ectx echo.Context) error { ctx := ectx.(*context) @@ -446,6 +454,7 @@ func New(imapURL, smtpURL string) *echo.Echo { e.POST("/message/:mbox/:uid/reply", handleCompose) e.Static("/assets", "public/assets") + e.Static("/themes", "public/themes") - return e + return nil } diff --git a/template.go b/template.go index 2581da0..f12e2ec 100644 --- a/template.go +++ b/template.go @@ -9,6 +9,7 @@ import ( ) type tmpl struct { + // TODO: add support for multiple themes t *template.Template } @@ -16,8 +17,8 @@ func (t *tmpl) Render(w io.Writer, name string, data interface{}, c echo.Context return t.t.ExecuteTemplate(w, name, data) } -func loadTemplates() (*tmpl, error) { - t, err := template.New("drmdb").Funcs(template.FuncMap{ +func loadTemplates(logger echo.Logger, themeName string) (*tmpl, error) { + base, err := template.New("").Funcs(template.FuncMap{ "tuple": func(values ...interface{}) []interface{} { return values }, @@ -25,5 +26,21 @@ func loadTemplates() (*tmpl, error) { return url.PathEscape(s) }, }).ParseGlob("public/*.html") - return &tmpl{t}, err + if err != nil { + return nil, err + } + + theme, err := base.Clone() + if err != nil { + return nil, err + } + + if themeName != "" { + logger.Printf("Loading theme \"%s\"", themeName) + if _, err := theme.ParseGlob("public/themes/" + themeName + "/*.html"); err != nil { + return nil, err + } + } + + return &tmpl{theme}, err }