mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-18 10:07:02 +00:00
Merge branch 'feature/cleanup' into develop #133
This commit is contained in:
@@ -30,9 +30,7 @@ variables it supports:
|
|||||||
INBUCKET_WEB_ADDR 0.0.0.0:9000 Web server IP4 host:port
|
INBUCKET_WEB_ADDR 0.0.0.0:9000 Web server IP4 host:port
|
||||||
INBUCKET_WEB_UIDIR ui User interface dir
|
INBUCKET_WEB_UIDIR ui User interface dir
|
||||||
INBUCKET_WEB_GREETINGFILE ui/greeting.html Home page greeting HTML
|
INBUCKET_WEB_GREETINGFILE ui/greeting.html Home page greeting HTML
|
||||||
INBUCKET_WEB_TEMPLATECACHE true Cache templates after first use?
|
|
||||||
INBUCKET_WEB_MAILBOXPROMPT @inbucket Prompt next to mailbox input
|
INBUCKET_WEB_MAILBOXPROMPT @inbucket Prompt next to mailbox input
|
||||||
INBUCKET_WEB_COOKIEAUTHKEY Session cipher key (text)
|
|
||||||
INBUCKET_WEB_MONITORVISIBLE true Show monitor tab in UI?
|
INBUCKET_WEB_MONITORVISIBLE true Show monitor tab in UI?
|
||||||
INBUCKET_WEB_MONITORHISTORY 30 Monitor remembered messages
|
INBUCKET_WEB_MONITORHISTORY 30 Monitor remembered messages
|
||||||
INBUCKET_WEB_PPROF false Expose profiling tools on /debug/pprof
|
INBUCKET_WEB_PPROF false Expose profiling tools on /debug/pprof
|
||||||
@@ -311,16 +309,6 @@ Inbucket installation, as well as link to REST documentation, etc.
|
|||||||
|
|
||||||
- Default: `ui/greeting.html`
|
- Default: `ui/greeting.html`
|
||||||
|
|
||||||
### Template Caching
|
|
||||||
|
|
||||||
`INBUCKET_WEB_TEMPLATECACHE`
|
|
||||||
|
|
||||||
Tells Inbucket to cache parsed template files. This should be left as default
|
|
||||||
unless you are a developer working on the Inbucket web interface.
|
|
||||||
|
|
||||||
- Default: `true`
|
|
||||||
- Values: `true` or `false`
|
|
||||||
|
|
||||||
### Mailbox Prompt
|
### Mailbox Prompt
|
||||||
|
|
||||||
`INBUCKET_WEB_MAILBOXPROMPT`
|
`INBUCKET_WEB_MAILBOXPROMPT`
|
||||||
@@ -333,17 +321,6 @@ Set to an empty string to hide the prompt.
|
|||||||
|
|
||||||
- Default: `@inbucket`
|
- Default: `@inbucket`
|
||||||
|
|
||||||
### Cookie Authentication Key
|
|
||||||
|
|
||||||
`INBUCKET_WEB_COOKIEAUTHKEY`
|
|
||||||
|
|
||||||
Inbucket stores session information in an encrypted browser cookie. Unless
|
|
||||||
specified, Inbucket generates a random key at startup. The only notable data
|
|
||||||
stored in a user session is the list of recently accessed mailboxes.
|
|
||||||
|
|
||||||
- Default: None
|
|
||||||
- Value: Text string, no particular format required
|
|
||||||
|
|
||||||
### Monitor Visible
|
### Monitor Visible
|
||||||
|
|
||||||
`INBUCKET_WEB_MONITORVISIBLE`
|
`INBUCKET_WEB_MONITORVISIBLE`
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -3,10 +3,9 @@ module github.com/jhillyerd/inbucket
|
|||||||
require (
|
require (
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/google/subcommands v0.0.0-20181012225330-46f0354f6315
|
github.com/google/subcommands v0.0.0-20181012225330-46f0354f6315
|
||||||
|
github.com/gorilla/context v1.1.1 // indirect
|
||||||
github.com/gorilla/css v1.0.0
|
github.com/gorilla/css v1.0.0
|
||||||
github.com/gorilla/mux v1.6.2
|
github.com/gorilla/mux v1.6.2
|
||||||
github.com/gorilla/securecookie v1.1.1
|
|
||||||
github.com/gorilla/sessions v1.1.3
|
|
||||||
github.com/gorilla/websocket v1.4.0
|
github.com/gorilla/websocket v1.4.0
|
||||||
github.com/jhillyerd/enmime v0.4.0
|
github.com/jhillyerd/enmime v0.4.0
|
||||||
github.com/jhillyerd/goldiff v0.1.0
|
github.com/jhillyerd/goldiff v0.1.0
|
||||||
|
|||||||
4
go.sum
4
go.sum
@@ -12,10 +12,6 @@ github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
|
|||||||
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
|
||||||
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
|
github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
|
||||||
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
|
||||||
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
|
|
||||||
github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4=
|
|
||||||
github.com/gorilla/sessions v1.1.3 h1:uXoZdcdA5XdXF3QzuSlheVRUvjl+1rKY7zBXL68L9RU=
|
|
||||||
github.com/gorilla/sessions v1.1.3/go.mod h1:8KCfur6+4Mqcc6S0FEfKuN15Vl5MgXW92AE8ovaJD0w=
|
|
||||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||||
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ=
|
github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ=
|
||||||
|
|||||||
@@ -95,9 +95,7 @@ type Web struct {
|
|||||||
Addr string `required:"true" default:"0.0.0.0:9000" desc:"Web server IP4 host:port"`
|
Addr string `required:"true" default:"0.0.0.0:9000" desc:"Web server IP4 host:port"`
|
||||||
UIDir string `required:"true" default:"ui" desc:"User interface dir"`
|
UIDir string `required:"true" default:"ui" desc:"User interface dir"`
|
||||||
GreetingFile string `required:"true" default:"ui/greeting.html" desc:"Home page greeting HTML"`
|
GreetingFile string `required:"true" default:"ui/greeting.html" desc:"Home page greeting HTML"`
|
||||||
TemplateCache bool `required:"true" default:"true" desc:"Cache templates after first use?"`
|
|
||||||
MailboxPrompt string `required:"true" default:"@inbucket" desc:"Prompt next to mailbox input"`
|
MailboxPrompt string `required:"true" default:"@inbucket" desc:"Prompt next to mailbox input"`
|
||||||
CookieAuthKey string `desc:"Session cipher key (text)"`
|
|
||||||
MonitorVisible bool `required:"true" default:"true" desc:"Show monitor tab in UI?"`
|
MonitorVisible bool `required:"true" default:"true" desc:"Show monitor tab in UI?"`
|
||||||
MonitorHistory int `required:"true" default:"30" desc:"Monitor remembered messages"`
|
MonitorHistory int `required:"true" default:"30" desc:"Monitor remembered messages"`
|
||||||
PProf bool `required:"true" default:"false" desc:"Expose profiling tools on /debug/pprof"`
|
PProf bool `required:"true" default:"false" desc:"Expose profiling tools on /debug/pprof"`
|
||||||
|
|||||||
@@ -159,16 +159,16 @@ func ValidateDomainPart(domain string) bool {
|
|||||||
// domain part is optional and not validated.
|
// domain part is optional and not validated.
|
||||||
func parseEmailAddress(address string) (local string, domain string, err error) {
|
func parseEmailAddress(address string) (local string, domain string, err error) {
|
||||||
if address == "" {
|
if address == "" {
|
||||||
return "", "", fmt.Errorf("Empty address")
|
return "", "", fmt.Errorf("empty address")
|
||||||
}
|
}
|
||||||
if len(address) > 320 {
|
if len(address) > 320 {
|
||||||
return "", "", fmt.Errorf("Address exceeds 320 characters")
|
return "", "", fmt.Errorf("address exceeds 320 characters")
|
||||||
}
|
}
|
||||||
if address[0] == '@' {
|
if address[0] == '@' {
|
||||||
return "", "", fmt.Errorf("Address cannot start with @ symbol")
|
return "", "", fmt.Errorf("address cannot start with @ symbol")
|
||||||
}
|
}
|
||||||
if address[0] == '.' {
|
if address[0] == '.' {
|
||||||
return "", "", fmt.Errorf("Address cannot start with a period")
|
return "", "", fmt.Errorf("address cannot start with a period")
|
||||||
}
|
}
|
||||||
// Loop over address parsing out local part.
|
// Loop over address parsing out local part.
|
||||||
buf := new(bytes.Buffer)
|
buf := new(bytes.Buffer)
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
"github.com/jhillyerd/inbucket/pkg/config"
|
"github.com/jhillyerd/inbucket/pkg/config"
|
||||||
"github.com/jhillyerd/inbucket/pkg/message"
|
"github.com/jhillyerd/inbucket/pkg/message"
|
||||||
"github.com/jhillyerd/inbucket/pkg/msghub"
|
"github.com/jhillyerd/inbucket/pkg/msghub"
|
||||||
@@ -15,7 +14,6 @@ import (
|
|||||||
// TODO remove redundant web config
|
// TODO remove redundant web config
|
||||||
type Context struct {
|
type Context struct {
|
||||||
Vars map[string]string
|
Vars map[string]string
|
||||||
Session *sessions.Session
|
|
||||||
MsgHub *msghub.Hub
|
MsgHub *msghub.Hub
|
||||||
Manager message.Manager
|
Manager message.Manager
|
||||||
RootConfig *config.Root
|
RootConfig *config.Root
|
||||||
@@ -48,24 +46,13 @@ func headerMatch(req *http.Request, name string, value string) bool {
|
|||||||
// NewContext returns a Context for the given HTTP Request
|
// NewContext returns a Context for the given HTTP Request
|
||||||
func NewContext(req *http.Request) (*Context, error) {
|
func NewContext(req *http.Request) (*Context, error) {
|
||||||
vars := mux.Vars(req)
|
vars := mux.Vars(req)
|
||||||
sess, err := sessionStore.Get(req, "inbucket")
|
|
||||||
if err != nil {
|
|
||||||
if sess == nil {
|
|
||||||
// No session, must fail
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
// The session cookie was probably signed by an old key, ignore it
|
|
||||||
// gorilla created an empty session for us
|
|
||||||
err = nil
|
|
||||||
}
|
|
||||||
ctx := &Context{
|
ctx := &Context{
|
||||||
Vars: vars,
|
Vars: vars,
|
||||||
Session: sess,
|
|
||||||
MsgHub: msgHub,
|
MsgHub: msgHub,
|
||||||
Manager: manager,
|
Manager: manager,
|
||||||
RootConfig: rootConfig,
|
RootConfig: rootConfig,
|
||||||
WebConfig: rootConfig.Web,
|
WebConfig: rootConfig.Web,
|
||||||
IsJSON: headerMatch(req, "Accept", "application/json"),
|
IsJSON: headerMatch(req, "Accept", "application/json"),
|
||||||
}
|
}
|
||||||
return ctx, err
|
return ctx, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,55 +3,13 @@ package web
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"html/template"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/jhillyerd/inbucket/pkg/stringutil"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// TemplateFuncs declares functions made available to all templates (including partials)
|
|
||||||
var TemplateFuncs = template.FuncMap{
|
|
||||||
"address": stringutil.StringAddress,
|
|
||||||
"friendlyTime": FriendlyTime,
|
|
||||||
"reverse": Reverse,
|
|
||||||
"stringsJoin": strings.Join,
|
|
||||||
"textToHtml": TextToHTML,
|
|
||||||
}
|
|
||||||
|
|
||||||
// From http://daringfireball.net/2010/07/improved_regex_for_matching_urls
|
// From http://daringfireball.net/2010/07/improved_regex_for_matching_urls
|
||||||
var urlRE = regexp.MustCompile("(?i)\\b((?:[a-z][\\w-]+:(?:/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}/)(?:[^\\s()<>]+|\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\))+(?:\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\)|[^\\s`!()\\[\\]{};:'\".,<>?«»“”‘’]))")
|
var urlRE = regexp.MustCompile("(?i)\\b((?:[a-z][\\w-]+:(?:/{1,3}|[a-z0-9%])|www\\d{0,3}[.]|[a-z0-9.\\-]+[.][a-z]{2,4}/)(?:[^\\s()<>]+|\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\))+(?:\\(([^\\s()<>]+|(\\([^\\s()<>]+\\)))*\\)|[^\\s`!()\\[\\]{};:'\".,<>?«»“”‘’]))")
|
||||||
|
|
||||||
// FriendlyTime renders a timestamp in a friendly fashion: 03:04:05 PM if same day,
|
|
||||||
// otherwise Mon Jan 2, 2006
|
|
||||||
func FriendlyTime(t time.Time) template.HTML {
|
|
||||||
ty, tm, td := t.Date()
|
|
||||||
ny, nm, nd := time.Now().Date()
|
|
||||||
if (ty == ny) && (tm == nm) && (td == nd) {
|
|
||||||
return template.HTML(t.Format("03:04:05 PM"))
|
|
||||||
}
|
|
||||||
return template.HTML(t.Format("Mon Jan 2, 2006"))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reverse routing function (shared with templates)
|
|
||||||
func Reverse(name string, things ...interface{}) string {
|
|
||||||
// Convert the things to strings
|
|
||||||
strs := make([]string, len(things))
|
|
||||||
for i, th := range things {
|
|
||||||
strs[i] = fmt.Sprint(th)
|
|
||||||
}
|
|
||||||
// Grab the route
|
|
||||||
u, err := Router.Get(name).URL(strs...)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Str("module", "web").Str("name", name).Err(err).
|
|
||||||
Msg("Failed to reverse route")
|
|
||||||
return "/ROUTE-ERROR"
|
|
||||||
}
|
|
||||||
return u.Path
|
|
||||||
}
|
|
||||||
|
|
||||||
// TextToHTML takes plain text, escapes it and tries to pretty it up for
|
// TextToHTML takes plain text, escapes it and tries to pretty it up for
|
||||||
// HTML display
|
// HTML display
|
||||||
func TextToHTML(text string) string {
|
func TextToHTML(text string) string {
|
||||||
|
|||||||
@@ -11,19 +11,12 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
"github.com/gorilla/securecookie"
|
|
||||||
"github.com/gorilla/sessions"
|
|
||||||
"github.com/jhillyerd/inbucket/pkg/config"
|
"github.com/jhillyerd/inbucket/pkg/config"
|
||||||
"github.com/jhillyerd/inbucket/pkg/message"
|
"github.com/jhillyerd/inbucket/pkg/message"
|
||||||
"github.com/jhillyerd/inbucket/pkg/msghub"
|
"github.com/jhillyerd/inbucket/pkg/msghub"
|
||||||
"github.com/rs/zerolog/log"
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
staticDir = "static"
|
|
||||||
templateDir = "templates"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// msgHub holds a reference to the message pub/sub system
|
// msgHub holds a reference to the message pub/sub system
|
||||||
msgHub *msghub.Hub
|
msgHub *msghub.Hub
|
||||||
@@ -36,7 +29,6 @@ var (
|
|||||||
rootConfig *config.Root
|
rootConfig *config.Root
|
||||||
server *http.Server
|
server *http.Server
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
sessionStore sessions.Store
|
|
||||||
globalShutdown chan bool
|
globalShutdown chan bool
|
||||||
|
|
||||||
// ExpWebSocketConnectsCurrent tracks the number of open WebSockets
|
// ExpWebSocketConnectsCurrent tracks the number of open WebSockets
|
||||||
@@ -94,17 +86,6 @@ func Initialize(
|
|||||||
http.StatusNotFound, "No route matches URI path")
|
http.StatusNotFound, "No route matches URI path")
|
||||||
Router.MethodNotAllowedHandler = noMatchHandler(
|
Router.MethodNotAllowedHandler = noMatchHandler(
|
||||||
http.StatusMethodNotAllowed, "Method not allowed for URI path")
|
http.StatusMethodNotAllowed, "Method not allowed for URI path")
|
||||||
|
|
||||||
// Session cookie setup.
|
|
||||||
if conf.Web.CookieAuthKey == "" {
|
|
||||||
log.Info().Str("module", "web").Str("phase", "startup").
|
|
||||||
Msg("Generating random cookie.auth.key")
|
|
||||||
sessionStore = sessions.NewCookieStore(securecookie.GenerateRandomKey(64))
|
|
||||||
} else {
|
|
||||||
log.Info().Str("module", "web").Str("phase", "startup").
|
|
||||||
Msg("Using configured cookie.auth.key")
|
|
||||||
sessionStore = sessions.NewCookieStore([]byte(conf.Web.CookieAuthKey))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start begins listening for HTTP requests
|
// Start begins listening for HTTP requests
|
||||||
|
|||||||
@@ -1,84 +0,0 @@
|
|||||||
package web
|
|
||||||
|
|
||||||
import (
|
|
||||||
"html/template"
|
|
||||||
"net/http"
|
|
||||||
"path"
|
|
||||||
"path/filepath"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
var cachedMutex sync.Mutex
|
|
||||||
var cachedTemplates = map[string]*template.Template{}
|
|
||||||
var cachedPartials = map[string]*template.Template{}
|
|
||||||
|
|
||||||
// RenderTemplate fetches the named template and renders it to the provided
|
|
||||||
// ResponseWriter.
|
|
||||||
func RenderTemplate(name string, w http.ResponseWriter, data interface{}) error {
|
|
||||||
t, err := ParseTemplate(name, false)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Str("module", "web").Str("path", name).Err(err).
|
|
||||||
Msg("Error in template")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
w.Header().Set("Expires", "-1")
|
|
||||||
return t.Execute(w, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RenderPartial fetches the named template and renders it to the provided
|
|
||||||
// ResponseWriter.
|
|
||||||
func RenderPartial(name string, w http.ResponseWriter, data interface{}) error {
|
|
||||||
t, err := ParseTemplate(name, true)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Str("module", "web").Str("path", name).Err(err).
|
|
||||||
Msg("Error in template")
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
w.Header().Set("Expires", "-1")
|
|
||||||
return t.Execute(w, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ParseTemplate loads the requested template along with _base.html, caching
|
|
||||||
// the result (if configured to do so)
|
|
||||||
func ParseTemplate(name string, partial bool) (*template.Template, error) {
|
|
||||||
cachedMutex.Lock()
|
|
||||||
defer cachedMutex.Unlock()
|
|
||||||
|
|
||||||
if t, ok := cachedTemplates[name]; ok {
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
tempFile := filepath.Join(rootConfig.Web.UIDir, templateDir, filepath.FromSlash(name))
|
|
||||||
log.Debug().Str("module", "web").Str("path", name).Msg("Parsing template")
|
|
||||||
|
|
||||||
var err error
|
|
||||||
var t *template.Template
|
|
||||||
if partial {
|
|
||||||
// Need to get basename of file to make it root template w/ funcs
|
|
||||||
base := path.Base(name)
|
|
||||||
t = template.New(base).Funcs(TemplateFuncs)
|
|
||||||
t, err = t.ParseFiles(tempFile)
|
|
||||||
} else {
|
|
||||||
t = template.New("_base.html").Funcs(TemplateFuncs)
|
|
||||||
t, err = t.ParseFiles(
|
|
||||||
filepath.Join(rootConfig.Web.UIDir, templateDir, "_base.html"), tempFile)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Allows us to disable caching for theme development
|
|
||||||
if rootConfig.Web.TemplateCache {
|
|
||||||
if partial {
|
|
||||||
log.Debug().Str("module", "web").Str("path", name).Msg("Caching partial")
|
|
||||||
cachedTemplates[name] = t
|
|
||||||
} else {
|
|
||||||
log.Debug().Str("module", "web").Str("path", name).Msg("Caching template")
|
|
||||||
cachedTemplates[name] = t
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return t, nil
|
|
||||||
}
|
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
package webui
|
package webui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"html/template"
|
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -109,14 +109,10 @@ func MailboxHTML(w http.ResponseWriter, req *http.Request, ctx *web.Context) (er
|
|||||||
// This doesn't indicate empty, likely an IO error
|
// This doesn't indicate empty, likely an IO error
|
||||||
return fmt.Errorf("GetMessage(%q) failed: %v", id, err)
|
return fmt.Errorf("GetMessage(%q) failed: %v", id, err)
|
||||||
}
|
}
|
||||||
// Render partial template
|
// Render HTML
|
||||||
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
|
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
|
||||||
return web.RenderPartial("mailbox/_html.html", w, map[string]interface{}{
|
_, err = w.Write([]byte(msg.HTML()))
|
||||||
"ctx": ctx,
|
return err
|
||||||
"name": name,
|
|
||||||
"message": msg,
|
|
||||||
"body": template.HTML(msg.HTML()),
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MailboxSource displays the raw source of a message, including headers. Renders text/plain
|
// MailboxSource displays the raw source of a message, including headers. Renders text/plain
|
||||||
@@ -147,19 +143,13 @@ func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *web.Contex
|
|||||||
// Don't have to validate these aren't empty, Gorilla returns 404
|
// Don't have to validate these aren't empty, Gorilla returns 404
|
||||||
name, err := ctx.Manager.MailboxForAddress(ctx.Vars["name"])
|
name, err := ctx.Manager.MailboxForAddress(ctx.Vars["name"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Session.AddFlash(err.Error(), "errors")
|
return err
|
||||||
_ = ctx.Session.Save(req, w)
|
|
||||||
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
id := ctx.Vars["id"]
|
id := ctx.Vars["id"]
|
||||||
numStr := ctx.Vars["num"]
|
numStr := ctx.Vars["num"]
|
||||||
num, err := strconv.ParseUint(numStr, 10, 32)
|
num, err := strconv.ParseUint(numStr, 10, 32)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Session.AddFlash("Attachment number must be unsigned numeric", "errors")
|
return err
|
||||||
_ = ctx.Session.Save(req, w)
|
|
||||||
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
msg, err := ctx.Manager.GetMessage(name, id)
|
msg, err := ctx.Manager.GetMessage(name, id)
|
||||||
if err == storage.ErrNotExist {
|
if err == storage.ErrNotExist {
|
||||||
@@ -171,10 +161,7 @@ func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *web.Contex
|
|||||||
return fmt.Errorf("GetMessage(%q) failed: %v", id, err)
|
return fmt.Errorf("GetMessage(%q) failed: %v", id, err)
|
||||||
}
|
}
|
||||||
if int(num) >= len(msg.Attachments()) {
|
if int(num) >= len(msg.Attachments()) {
|
||||||
ctx.Session.AddFlash("Attachment number too high", "errors")
|
return errors.New("requested attachment number does not exist")
|
||||||
_ = ctx.Session.Save(req, w)
|
|
||||||
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
// Output attachment
|
// Output attachment
|
||||||
part := msg.Attachments()[num]
|
part := msg.Attachments()[num]
|
||||||
|
|||||||
@@ -1,35 +0,0 @@
|
|||||||
package webui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/jhillyerd/inbucket/pkg/server/web"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
// maximum mailboxes to remember
|
|
||||||
maxRemembered = 8
|
|
||||||
// session value key; referenced in templates, do not change
|
|
||||||
mailboxKey = "recentMailboxes"
|
|
||||||
)
|
|
||||||
|
|
||||||
// RememberMailbox manages the list of recently accessed mailboxes stored in the session
|
|
||||||
func RememberMailbox(ctx *web.Context, mailbox string) {
|
|
||||||
recent := RecentMailboxes(ctx)
|
|
||||||
newRecent := make([]string, 1, maxRemembered)
|
|
||||||
newRecent[0] = mailbox
|
|
||||||
|
|
||||||
for _, recBox := range recent {
|
|
||||||
// Insert until newRecent is full, but don't repeat the new mailbox
|
|
||||||
if len(newRecent) < maxRemembered && mailbox != recBox {
|
|
||||||
newRecent = append(newRecent, recBox)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx.Session.Values[mailboxKey] = newRecent
|
|
||||||
}
|
|
||||||
|
|
||||||
// RecentMailboxes returns a slice of the most recently accessed mailboxes
|
|
||||||
func RecentMailboxes(ctx *web.Context) []string {
|
|
||||||
val := ctx.Session.Values[mailboxKey]
|
|
||||||
recent, _ := val.([]string)
|
|
||||||
return recent
|
|
||||||
}
|
|
||||||
@@ -21,55 +21,7 @@ func RootGreeting(w http.ResponseWriter, req *http.Request, ctx *web.Context) (e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// RootMonitor serves the Inbucket monitor page
|
// RootStatus renders portions of the server configuration as JSON.
|
||||||
func RootMonitor(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
|
|
||||||
if !ctx.RootConfig.Web.MonitorVisible {
|
|
||||||
ctx.Session.AddFlash("Monitor is disabled in configuration", "errors")
|
|
||||||
_ = ctx.Session.Save(req, w)
|
|
||||||
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Get flash messages, save session
|
|
||||||
errorFlash := ctx.Session.Flashes("errors")
|
|
||||||
if err = ctx.Session.Save(req, w); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Render template
|
|
||||||
return web.RenderTemplate("root/monitor.html", w, map[string]interface{}{
|
|
||||||
"ctx": ctx,
|
|
||||||
"errorFlash": errorFlash,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// RootMonitorMailbox serves the Inbucket monitor page for a particular mailbox
|
|
||||||
func RootMonitorMailbox(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
|
|
||||||
if !ctx.RootConfig.Web.MonitorVisible {
|
|
||||||
ctx.Session.AddFlash("Monitor is disabled in configuration", "errors")
|
|
||||||
_ = ctx.Session.Save(req, w)
|
|
||||||
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
name, err := ctx.Manager.MailboxForAddress(ctx.Vars["name"])
|
|
||||||
if err != nil {
|
|
||||||
ctx.Session.AddFlash(err.Error(), "errors")
|
|
||||||
_ = ctx.Session.Save(req, w)
|
|
||||||
http.Redirect(w, req, web.Reverse("RootIndex"), http.StatusSeeOther)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
// Get flash messages, save session
|
|
||||||
errorFlash := ctx.Session.Flashes("errors")
|
|
||||||
if err = ctx.Session.Save(req, w); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
// Render template
|
|
||||||
return web.RenderTemplate("root/monitor.html", w, map[string]interface{}{
|
|
||||||
"ctx": ctx,
|
|
||||||
"errorFlash": errorFlash,
|
|
||||||
"name": name,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// RootStatus serves the Inbucket status page
|
|
||||||
func RootStatus(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
|
func RootStatus(w http.ResponseWriter, req *http.Request, ctx *web.Context) (err error) {
|
||||||
root := ctx.RootConfig
|
root := ctx.RootConfig
|
||||||
retPeriod := ""
|
retPeriod := ""
|
||||||
|
|||||||
@@ -10,16 +10,12 @@ import (
|
|||||||
func SetupRoutes(r *mux.Router) {
|
func SetupRoutes(r *mux.Router) {
|
||||||
r.Path("/greeting").Handler(
|
r.Path("/greeting").Handler(
|
||||||
web.Handler(RootGreeting)).Name("RootGreeting").Methods("GET")
|
web.Handler(RootGreeting)).Name("RootGreeting").Methods("GET")
|
||||||
r.Path("/monitor").Handler(
|
|
||||||
web.Handler(RootMonitor)).Name("RootMonitor").Methods("GET")
|
|
||||||
r.Path("/monitor/{name}").Handler(
|
|
||||||
web.Handler(RootMonitorMailbox)).Name("RootMonitorMailbox").Methods("GET")
|
|
||||||
r.Path("/status").Handler(
|
r.Path("/status").Handler(
|
||||||
web.Handler(RootStatus)).Name("RootStatus").Methods("GET")
|
web.Handler(RootStatus)).Name("RootStatus").Methods("GET")
|
||||||
r.Path("/m/{name}/{id}").Handler(
|
r.Path("/m/{name}/{id}").Handler(
|
||||||
web.Handler(MailboxMessage)).Name("MailboxMessage").Methods("GET")
|
web.Handler(MailboxMessage)).Name("MailboxMessage").Methods("GET")
|
||||||
r.Path("/m/{name}/{id}/html").Handler(
|
r.Path("/m/{name}/{id}/html").Handler(
|
||||||
web.Handler(MailboxHTML)).Name("MailboxHtml").Methods("GET")
|
web.Handler(MailboxHTML)).Name("MailboxHTML").Methods("GET")
|
||||||
r.Path("/m/{name}/{id}/source").Handler(
|
r.Path("/m/{name}/{id}/source").Handler(
|
||||||
web.Handler(MailboxSource)).Name("MailboxSource").Methods("GET")
|
web.Handler(MailboxSource)).Name("MailboxSource").Methods("GET")
|
||||||
r.Path("/m/attach/{name}/{id}/{num}/{file}").Handler(
|
r.Path("/m/attach/{name}/{id}/{num}/{file}").Handler(
|
||||||
|
|||||||
Reference in New Issue
Block a user