Add basic theme support
References: https://todo.sr.ht/~sircmpwn/koushin/1
This commit is contained in:
parent
4ab5fb7f65
commit
e94b1311de
5 changed files with 75 additions and 25 deletions
10
README.md
10
README.md
|
@ -4,6 +4,16 @@
|
||||||
|
|
||||||
go run ./cmd/koushin imaps://mail.example.org:993 smtps://mail.example.org:465
|
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
|
## License
|
||||||
|
|
||||||
MIT
|
MIT
|
||||||
|
|
|
@ -1,28 +1,41 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
|
||||||
|
|
||||||
"git.sr.ht/~emersion/koushin"
|
"git.sr.ht/~emersion/koushin"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
"github.com/labstack/echo/v4/middleware"
|
"github.com/labstack/echo/v4/middleware"
|
||||||
|
"github.com/labstack/gommon/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
if len(os.Args) != 2 && len(os.Args) != 3 {
|
var options koushin.Options
|
||||||
fmt.Println("usage: koushin <IMAP URL> [SMTP URL]")
|
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()
|
||||||
|
}
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
if flag.NArg() < 1 || flag.NArg() > 2 {
|
||||||
|
flag.Usage()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
imapURL := os.Args[1]
|
options.IMAPURL = flag.Arg(0)
|
||||||
|
options.SMTPURL = flag.Arg(1)
|
||||||
|
|
||||||
var smtpURL string
|
e := echo.New()
|
||||||
if len(os.Args) == 3 {
|
if l, ok := e.Logger.(*log.Logger); ok {
|
||||||
smtpURL = os.Args[2]
|
l.SetHeader("${time_rfc3339} ${level}")
|
||||||
|
}
|
||||||
|
if err := koushin.New(e, &options); err != nil {
|
||||||
|
e.Logger.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
e := koushin.New(imapURL, smtpURL)
|
|
||||||
e.Use(middleware.Logger())
|
|
||||||
e.Use(middleware.Recover())
|
e.Use(middleware.Recover())
|
||||||
e.Logger.Fatal(e.Start(":1323"))
|
e.Logger.Fatal(e.Start(":1323"))
|
||||||
}
|
}
|
||||||
|
|
1
go.mod
1
go.mod
|
@ -8,6 +8,7 @@ require (
|
||||||
github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e
|
github.com/emersion/go-sasl v0.0.0-20190817083125-240c8404624e
|
||||||
github.com/emersion/go-smtp v0.12.0
|
github.com/emersion/go-smtp v0.12.0
|
||||||
github.com/labstack/echo/v4 v4.1.11
|
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-colorable v0.1.4 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.10 // indirect
|
github.com/mattn/go-isatty v0.0.10 // indirect
|
||||||
github.com/valyala/fasttemplate v1.1.0 // indirect
|
github.com/valyala/fasttemplate v1.1.0 // indirect
|
||||||
|
|
33
server.go
33
server.go
|
@ -79,7 +79,7 @@ func (s *Server) parseSMTPURL(smtpURL string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(imapURL, smtpURL string) (*Server, error) {
|
func newServer(imapURL, smtpURL string) (*Server, error) {
|
||||||
s := &Server{}
|
s := &Server{}
|
||||||
|
|
||||||
if err := s.parseIMAPURL(imapURL); err != nil {
|
if err := s.parseIMAPURL(imapURL); err != nil {
|
||||||
|
@ -310,12 +310,25 @@ func handleCompose(ectx echo.Context) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(imapURL, smtpURL string) *echo.Echo {
|
func isPublic(path string) bool {
|
||||||
e := echo.New()
|
return path == "/login" || strings.HasPrefix(path, "/assets/") ||
|
||||||
|
strings.HasPrefix(path, "/themes/")
|
||||||
|
}
|
||||||
|
|
||||||
s, err := NewServer(imapURL, smtpURL)
|
type Options struct {
|
||||||
|
IMAPURL, SMTPURL string
|
||||||
|
Theme string
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(e *echo.Echo, options *Options) error {
|
||||||
|
s, err := newServer(options.IMAPURL, options.SMTPURL)
|
||||||
if err != nil {
|
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) {
|
e.HTTPErrorHandler = func(err error, c echo.Context) {
|
||||||
|
@ -336,7 +349,7 @@ func New(imapURL, smtpURL string) *echo.Echo {
|
||||||
cookie, err := ctx.Cookie(cookieName)
|
cookie, err := ctx.Cookie(cookieName)
|
||||||
if err == http.ErrNoCookie {
|
if err == http.ErrNoCookie {
|
||||||
// Require auth for all pages except /login
|
// Require auth for all pages except /login
|
||||||
if ctx.Path() == "/login" || strings.HasPrefix(ctx.Path(), "/assets/") {
|
if isPublic(ctx.Path()) {
|
||||||
return next(ctx)
|
return next(ctx)
|
||||||
} else {
|
} else {
|
||||||
return ctx.Redirect(http.StatusFound, "/login")
|
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 {
|
e.GET("/mailbox/:mbox", func(ectx echo.Context) error {
|
||||||
ctx := ectx.(*context)
|
ctx := ectx.(*context)
|
||||||
|
|
||||||
|
@ -446,6 +454,7 @@ func New(imapURL, smtpURL string) *echo.Echo {
|
||||||
e.POST("/message/:mbox/:uid/reply", handleCompose)
|
e.POST("/message/:mbox/:uid/reply", handleCompose)
|
||||||
|
|
||||||
e.Static("/assets", "public/assets")
|
e.Static("/assets", "public/assets")
|
||||||
|
e.Static("/themes", "public/themes")
|
||||||
|
|
||||||
return e
|
return nil
|
||||||
}
|
}
|
||||||
|
|
23
template.go
23
template.go
|
@ -9,6 +9,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
type tmpl struct {
|
type tmpl struct {
|
||||||
|
// TODO: add support for multiple themes
|
||||||
t *template.Template
|
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)
|
return t.t.ExecuteTemplate(w, name, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadTemplates() (*tmpl, error) {
|
func loadTemplates(logger echo.Logger, themeName string) (*tmpl, error) {
|
||||||
t, err := template.New("drmdb").Funcs(template.FuncMap{
|
base, err := template.New("").Funcs(template.FuncMap{
|
||||||
"tuple": func(values ...interface{}) []interface{} {
|
"tuple": func(values ...interface{}) []interface{} {
|
||||||
return values
|
return values
|
||||||
},
|
},
|
||||||
|
@ -25,5 +26,21 @@ func loadTemplates() (*tmpl, error) {
|
||||||
return url.PathEscape(s)
|
return url.PathEscape(s)
|
||||||
},
|
},
|
||||||
}).ParseGlob("public/*.html")
|
}).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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue