diff --git a/.goxc.json b/.goxc.json index 918a9bb..296a88e 100644 --- a/.goxc.json +++ b/.goxc.json @@ -6,7 +6,7 @@ "Resources": { "Include": "README*,LICENSE*,bin,etc,themes" }, - "PackageVersion": "20131001", + "PackageVersion": "20131010", "PrereleaseInfo": "snapshot", "FormatVersion": "0.8" } diff --git a/bin/mailbox-list.sh b/bin/mailbox-list.sh new file mode 100755 index 0000000..da5ca4c --- /dev/null +++ b/bin/mailbox-list.sh @@ -0,0 +1 @@ +curl -i -H "Accept: application/json" --noproxy localhost http://localhost:9000/mailbox/$1 diff --git a/bin/message-delete.sh b/bin/message-delete.sh new file mode 100755 index 0000000..c3c8b86 --- /dev/null +++ b/bin/message-delete.sh @@ -0,0 +1 @@ +curl -i -H "Accept: application/json" --noproxy localhost -X DELETE http://localhost:9000/mailbox/$1/$2 diff --git a/bin/message-source.sh b/bin/message-source.sh new file mode 100755 index 0000000..21714cc --- /dev/null +++ b/bin/message-source.sh @@ -0,0 +1 @@ +curl -i -H "Accept: application/json" --noproxy localhost http://localhost:9000/mailbox/$1/$2/source diff --git a/config/config.go b/config/config.go index bfa1e5f..25b6030 100644 --- a/config/config.go +++ b/config/config.go @@ -25,7 +25,7 @@ type SmtpConfig struct { type Pop3Config struct { Ip4address net.IP Ip4port int - Domain string + Domain string MaxIdleSeconds int } @@ -35,6 +35,7 @@ type WebConfig struct { TemplateDir string TemplateCache bool PublicDir string + GreetingFile string } type DataStoreConfig struct { @@ -328,6 +329,13 @@ func parseWebConfig() error { } webConfig.PublicDir = str + option = "greeting.file" + str, err = Config.String(section, option) + if err != nil { + return fmt.Errorf("Failed to parse [%v]%v: '%v'", section, option, err) + } + webConfig.GreetingFile = str + return nil } diff --git a/etc/devel.conf b/etc/devel.conf index 2ce5c4b..56b5c04 100644 --- a/etc/devel.conf +++ b/etc/devel.conf @@ -81,6 +81,10 @@ template.cache=false # Path to the selected themes public (static) files public.dir=%(install.dir)s/themes/%(theme)s/public +# Path to the greeting HTML displayed on front page, can +# be moved out of installation dir for customization +greeting.file=%(install.dir)s/themes/greeting.html + ############################################################################# [datastore] diff --git a/etc/inbucket.conf b/etc/inbucket.conf index a6c9c81..2236f1c 100644 --- a/etc/inbucket.conf +++ b/etc/inbucket.conf @@ -81,6 +81,10 @@ template.cache=true # Path to the selected themes public (static) files public.dir=%(install.dir)s/themes/%(theme)s/public +# Path to the greeting HTML displayed on front page, can +# be moved out of installation dir for customization +greeting.file=%(install.dir)s/themes/greeting.html + ############################################################################# [datastore] diff --git a/etc/unix-sample.conf b/etc/unix-sample.conf index 17b7b9d..00061f8 100644 --- a/etc/unix-sample.conf +++ b/etc/unix-sample.conf @@ -81,6 +81,10 @@ template.cache=true # Path to the selected themes public (static) files public.dir=%(install.dir)s/themes/%(theme)s/public +# Path to the greeting HTML displayed on front page, can +# be moved out of installation dir for customization +greeting.file=%(install.dir)s/themes/greeting.html + ############################################################################# [datastore] diff --git a/etc/win-sample.conf b/etc/win-sample.conf index b0fcdcf..507336c 100644 --- a/etc/win-sample.conf +++ b/etc/win-sample.conf @@ -81,6 +81,10 @@ template.cache=true # Path to the selected themes public (static) files public.dir=%(install.dir)s\themes\%(theme)s\public +# Path to the greeting HTML displayed on front page, can +# be moved out of installation dir for customization +greeting.file=%(install.dir)s\themes\greeting.html + ############################################################################# [datastore] diff --git a/themes/greeting.html b/themes/greeting.html new file mode 100644 index 0000000..67f4794 --- /dev/null +++ b/themes/greeting.html @@ -0,0 +1,9 @@ +

Inbucket is an email testing service; it will accept email for any email +address and make it available to view without a password.

+ +

To view email for a particular address, enter the username portion +of the address into the box on the upper right and click go.

+ +

This message can be customized by editing greeting.html. Change the +configuration option greeting.file if you'd like to move it +outside of the Inbucket installation directory.

diff --git a/themes/integral/templates/_base.html b/themes/integral/templates/_base.html index 138ced7..37656b6 100644 --- a/themes/integral/templates/_base.html +++ b/themes/integral/templates/_base.html @@ -47,7 +47,7 @@ Released for free under a Creative Commons Attribution 2.5 License diff --git a/themes/integral/templates/mailbox/index.html b/themes/integral/templates/mailbox/index.html index 66e078e..6581e18 100644 --- a/themes/integral/templates/mailbox/index.html +++ b/themes/integral/templates/mailbox/index.html @@ -15,14 +15,14 @@ function() { $('.listEntry').removeClass("listEntrySelected") $(this).addClass("listEntrySelected") - $('#emailContent').load('/mailbox/show/{{.name}}/' + this.id) + $('#emailContent').load('/mailbox/{{.name}}/' + this.id) } ) $("#messageList").slideDown() } function loadList() { - $('#messageList').load("/mailbox/list/{{.name}}", listLoaded) + $('#messageList').load("/mailbox/{{.name}}", listLoaded) } function reloadList() { @@ -38,20 +38,20 @@ function deleteMessage(id) { $('#emailContent').empty() $.ajax({ - type: 'POST', - url: '/mailbox/delete/{{.name}}/' + id, + type: 'DELETE', + url: '/mailbox/{{.name}}/' + id, success: reloadList }) } function htmlView(id) { - window.open('/mailbox/html/{{.name}}/' + id, '_blank', + window.open('/mailbox/{{.name}}/' + id + "/html", '_blank', 'width=800,height=600,' + 'menubar=yes,resizable=yes,scrollbars=yes,status=yes,toolbar=yes') } function messageSource(id) { - window.open('/mailbox/source/{{.name}}/' + id, '_blank', + window.open('/mailbox/{{.name}}/' + id + "/source", '_blank', 'width=800,height=600,' + 'menubar=no,resizable=yes,scrollbars=yes,status=no,toolbar=no') } diff --git a/themes/integral/templates/root/index.html b/themes/integral/templates/root/index.html index a8404c1..8a1899e 100644 --- a/themes/integral/templates/root/index.html +++ b/themes/integral/templates/root/index.html @@ -9,11 +9,4 @@ {{end}} -{{define "content"}} -

Inbucket is an email testing service; it will accept email for any email -address and make it available to view without a password.

- -

To view email for a particular address, enter the username portion -of the address into the box on the upper right and click go.

-{{end}} - +{{define "content"}}{{.greeting}}{{end}} diff --git a/themes/integral/templates/root/status.html b/themes/integral/templates/root/status.html index 5804178..fc1cb12 100644 --- a/themes/integral/templates/root/status.html +++ b/themes/integral/templates/root/status.html @@ -139,6 +139,24 @@

Metrics are polled every 10 seconds. Inbucket does not keep history for the 10 minute graphs, but your web browser will accumulate the data over time.

+
+

Configuration

+ + + + + + + + + + + + + +
SMTP Listener:{{.smtpListener}}
POP3 Listener:{{.pop3Listener}}
HTTP Listener:{{.webListener}}
+

 

+

General Metrics

diff --git a/web/context.go b/web/context.go index 7946cdc..5bf4400 100644 --- a/web/context.go +++ b/web/context.go @@ -5,18 +5,37 @@ import ( "github.com/gorilla/sessions" "github.com/jhillyerd/inbucket/smtpd" "net/http" + "strings" ) type Context struct { Vars map[string]string Session *sessions.Session DataStore smtpd.DataStore + IsJson bool } func (c *Context) Close() { // Do nothing } +// headerMatch returns true if the request header specified by name contains +// the specified value. Case is ignored. +func headerMatch(req *http.Request, name string, value string) bool { + name = http.CanonicalHeaderKey(name) + value = strings.ToLower(value) + + if header := req.Header[name]; header != nil { + for _, hv := range header { + if value == strings.ToLower(hv) { + return true + } + } + } + + return false +} + func NewContext(req *http.Request) (*Context, error) { vars := mux.Vars(req) sess, err := sessionStore.Get(req, "inbucket") @@ -25,6 +44,7 @@ func NewContext(req *http.Request) (*Context, error) { Vars: vars, Session: sess, DataStore: ds, + IsJson: headerMatch(req, "Accept", "application/json"), } if err != nil { return ctx, err diff --git a/web/mailbox_controller.go b/web/mailbox_controller.go index ef9866b..1d4711b 100644 --- a/web/mailbox_controller.go +++ b/web/mailbox_controller.go @@ -7,8 +7,14 @@ import ( "io" "net/http" "strconv" + "time" ) +type JsonMessageHeader struct { + Mailbox, Id, From, Subject string + Date time.Time +} + func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) { name := req.FormValue("name") if len(name) == 0 { @@ -37,6 +43,20 @@ func MailboxList(w http.ResponseWriter, req *http.Request, ctx *Context) (err er } log.LogTrace("Got %v messsages", len(messages)) + if ctx.IsJson { + jmessages := make([]*JsonMessageHeader, len(messages)) + for i, msg := range messages { + jmessages[i] = &JsonMessageHeader{ + Mailbox: name, + Id: msg.Id(), + From: msg.From(), + Subject: msg.Subject(), + Date: msg.Date(), + } + } + return RenderJson(w, jmessages) + } + return RenderPartial("mailbox/_list.html", w, map[string]interface{}{ "ctx": ctx, "name": name, @@ -214,6 +234,11 @@ func MailboxDelete(w http.ResponseWriter, req *http.Request, ctx *Context) (err if err != nil { return err } + + if ctx.IsJson { + return RenderJson(w, "OK") + } + w.Header().Set("Content-Type", "text/plain") io.WriteString(w, "OK") return nil diff --git a/web/rest.go b/web/rest.go new file mode 100644 index 0000000..be5238d --- /dev/null +++ b/web/rest.go @@ -0,0 +1,13 @@ +package web + +import ( + "encoding/json" + "net/http" +) + +func RenderJson(w http.ResponseWriter, data interface{}) error { + w.Header().Set("Content-Type", "application/json; charset=utf-8") + w.Header().Set("Expires", "-1") + enc := json.NewEncoder(w) + return enc.Encode(data) +} diff --git a/web/root_controller.go b/web/root_controller.go index bcd83a7..aa410a1 100644 --- a/web/root_controller.go +++ b/web/root_controller.go @@ -1,20 +1,38 @@ package web import ( + "fmt" "github.com/jhillyerd/inbucket/config" + "html/template" + "io/ioutil" "net/http" ) func RootIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) { + greeting, err := ioutil.ReadFile(config.GetWebConfig().GreetingFile) + if err != nil { + return fmt.Errorf("Failed to load greeting: %v", err) + } + return RenderTemplate("root/index.html", w, map[string]interface{}{ "ctx": ctx, + "greeting": template.HTML(string(greeting)), }) } func RootStatus(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) { retentionMinutes := config.GetDataStoreConfig().RetentionMinutes + smtpListener := fmt.Sprintf("%s:%d", config.GetSmtpConfig().Ip4address.String(), + config.GetSmtpConfig().Ip4port) + pop3Listener := fmt.Sprintf("%s:%d", config.GetPop3Config().Ip4address.String(), + config.GetPop3Config().Ip4port) + webListener := fmt.Sprintf("%s:%d", config.GetWebConfig().Ip4address.String(), + config.GetWebConfig().Ip4port) return RenderTemplate("root/status.html", w, map[string]interface{}{ - "ctx": ctx, + "ctx": ctx, "retentionMinutes": retentionMinutes, + "smtpListener": smtpListener, + "pop3Listener": pop3Listener, + "webListener": webListener, }) } diff --git a/web/server.go b/web/server.go index 120a9cd..fcffdce 100644 --- a/web/server.go +++ b/web/server.go @@ -35,11 +35,11 @@ func setupRoutes(cfg config.WebConfig) { r.Path("/").Handler(handler(RootIndex)).Name("RootIndex").Methods("GET") r.Path("/status").Handler(handler(RootStatus)).Name("RootStatus").Methods("GET") r.Path("/mailbox").Handler(handler(MailboxIndex)).Name("MailboxIndex").Methods("GET") - r.Path("/mailbox/list/{name}").Handler(handler(MailboxList)).Name("MailboxList").Methods("GET") - r.Path("/mailbox/show/{name}/{id}").Handler(handler(MailboxShow)).Name("MailboxShow").Methods("GET") - r.Path("/mailbox/html/{name}/{id}").Handler(handler(MailboxHtml)).Name("MailboxHtml").Methods("GET") - r.Path("/mailbox/source/{name}/{id}").Handler(handler(MailboxSource)).Name("MailboxSource").Methods("GET") - r.Path("/mailbox/delete/{name}/{id}").Handler(handler(MailboxDelete)).Name("MailboxDelete").Methods("POST") + r.Path("/mailbox/{name}").Handler(handler(MailboxList)).Name("MailboxList").Methods("GET") + r.Path("/mailbox/{name}/{id}").Handler(handler(MailboxShow)).Name("MailboxShow").Methods("GET") + r.Path("/mailbox/{name}/{id}/html").Handler(handler(MailboxHtml)).Name("MailboxHtml").Methods("GET") + r.Path("/mailbox/{name}/{id}/source").Handler(handler(MailboxSource)).Name("MailboxSource").Methods("GET") + r.Path("/mailbox/{name}/{id}").Handler(handler(MailboxDelete)).Name("MailboxDelete").Methods("DELETE") r.Path("/mailbox/dattach/{name}/{id}/{num}/{file}").Handler(handler(MailboxDownloadAttach)).Name("MailboxDownloadAttach").Methods("GET") r.Path("/mailbox/vattach/{name}/{id}/{num}/{file}").Handler(handler(MailboxViewAttach)).Name("MailboxViewAttach").Methods("GET")