@@ -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/<name>/`. | |||
Templates in `public/themes/<name>/*.html` override default templates in | |||
`public/*.html`. Assets in `public/themes/<name>/assets/*` are served by the | |||
HTTP server at `themes/<name>/assets/*`. | |||
## License | |||
MIT |
@@ -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 <IMAP URL> [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...] <IMAP URL> [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")) | |||
} |
@@ -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 | |||
@@ -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 | |||
} |
@@ -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 | |||
} |