From dcc5d240cb6e3c7832849d8b67c0d26a66273d14 Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Wed, 9 Oct 2013 16:20:13 -0700 Subject: [PATCH 1/6] Add listener configuration details to status page --- themes/integral/templates/root/status.html | 18 ++++++++++++++++++ web/root_controller.go | 12 +++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) 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/root_controller.go b/web/root_controller.go index bcd83a7..f5df0de 100644 --- a/web/root_controller.go +++ b/web/root_controller.go @@ -1,6 +1,7 @@ package web import ( + "fmt" "github.com/jhillyerd/inbucket/config" "net/http" ) @@ -13,8 +14,17 @@ func RootIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err erro 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, }) } From 97a2016177edd18968efabce4602eb3b49bfb3ff Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Thu, 10 Oct 2013 14:04:22 -0700 Subject: [PATCH 2/6] Add customizable greeting to index page Allow an organization to copy the greeting.html file out of the themes directory, customize it, then point the greeting.file config option at it so that it will survive Inbucket upgrades. --- config/config.go | 10 +++++++++- etc/devel.conf | 4 ++++ etc/inbucket.conf | 4 ++++ etc/unix-sample.conf | 4 ++++ etc/win-sample.conf | 4 ++++ themes/greeting.html | 9 +++++++++ themes/integral/templates/root/index.html | 9 +-------- web/root_controller.go | 8 ++++++++ 8 files changed, 43 insertions(+), 9 deletions(-) create mode 100644 themes/greeting.html 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/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/web/root_controller.go b/web/root_controller.go index f5df0de..aa410a1 100644 --- a/web/root_controller.go +++ b/web/root_controller.go @@ -3,12 +3,20 @@ 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)), }) } From 27015b3c581794b062110b400e0f9764f2a21ca8 Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Thu, 10 Oct 2013 15:26:33 -0700 Subject: [PATCH 3/6] Update github link in integral templates --- .goxc.json | 2 +- themes/integral/templates/_base.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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/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 From a12147930caeeace35d89a6abacd2708a7cdcea1 Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Fri, 11 Oct 2013 14:42:00 -0700 Subject: [PATCH 4/6] Can now list mailbox contents via REST --- bin/mailbox-list.sh | 1 + web/context.go | 20 ++++++++++++++++++++ web/mailbox_controller.go | 26 +++++++++++++++++++++----- web/rest.go | 13 +++++++++++++ 4 files changed, 55 insertions(+), 5 deletions(-) create mode 100755 bin/mailbox-list.sh create mode 100644 web/rest.go diff --git a/bin/mailbox-list.sh b/bin/mailbox-list.sh new file mode 100755 index 0000000..384560f --- /dev/null +++ b/bin/mailbox-list.sh @@ -0,0 +1 @@ +curl -i -H "Accept: application/json" --noproxy localhost http://localhost:9000/mailbox/list/$1 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..018a18b 100644 --- a/web/mailbox_controller.go +++ b/web/mailbox_controller.go @@ -9,6 +9,10 @@ import ( "strconv" ) +type JsonMessageHeader struct { + From, Subject, Date string +} + func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) { name := req.FormValue("name") if len(name) == 0 { @@ -37,11 +41,23 @@ func MailboxList(w http.ResponseWriter, req *http.Request, ctx *Context) (err er } log.LogTrace("Got %v messsages", len(messages)) - return RenderPartial("mailbox/_list.html", w, map[string]interface{}{ - "ctx": ctx, - "name": name, - "messages": messages, - }) + if ctx.IsJson { + jmessages := make([]*JsonMessageHeader, len(messages)) + for i, msg := range messages { + jmessages[i] = &JsonMessageHeader{ + From: msg.From(), + Subject: msg.Subject(), + Date: msg.Date().String(), + } + } + return RenderJson(w, jmessages) + } else { + return RenderPartial("mailbox/_list.html", w, map[string]interface{}{ + "ctx": ctx, + "name": name, + "messages": messages, + }) + } } func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) { 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) +} From d8ac1d78b058c8f2fe9fd7d7bec6d2bec4e719a6 Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Fri, 11 Oct 2013 16:40:06 -0700 Subject: [PATCH 5/6] Change format of json msg header --- bin/mailbox-source.sh | 1 + web/mailbox_controller.go | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100755 bin/mailbox-source.sh diff --git a/bin/mailbox-source.sh b/bin/mailbox-source.sh new file mode 100755 index 0000000..cbff7e6 --- /dev/null +++ b/bin/mailbox-source.sh @@ -0,0 +1 @@ +curl -i -H "Accept: application/json" --noproxy localhost http://localhost:9000/mailbox/source/$1/$2 diff --git a/web/mailbox_controller.go b/web/mailbox_controller.go index 018a18b..ecff8ae 100644 --- a/web/mailbox_controller.go +++ b/web/mailbox_controller.go @@ -7,10 +7,12 @@ import ( "io" "net/http" "strconv" + "time" ) type JsonMessageHeader struct { - From, Subject, Date string + Mailbox, Id, From, Subject string + Date time.Time } func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) { @@ -45,9 +47,11 @@ func MailboxList(w http.ResponseWriter, req *http.Request, ctx *Context) (err er 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().String(), + Date: msg.Date(), } } return RenderJson(w, jmessages) From 7b91a667a10ca69c170d48afdbd4e23ab5ac1b3f Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Fri, 11 Oct 2013 17:16:23 -0700 Subject: [PATCH 6/6] Reorganize URI routes to be more RESTful --- bin/mailbox-list.sh | 2 +- bin/message-delete.sh | 1 + bin/{mailbox-source.sh => message-source.sh} | 2 +- themes/integral/templates/mailbox/index.html | 12 ++++++------ web/mailbox_controller.go | 17 +++++++++++------ web/server.go | 10 +++++----- 6 files changed, 25 insertions(+), 19 deletions(-) create mode 100755 bin/message-delete.sh rename bin/{mailbox-source.sh => message-source.sh} (63%) diff --git a/bin/mailbox-list.sh b/bin/mailbox-list.sh index 384560f..da5ca4c 100755 --- a/bin/mailbox-list.sh +++ b/bin/mailbox-list.sh @@ -1 +1 @@ -curl -i -H "Accept: application/json" --noproxy localhost http://localhost:9000/mailbox/list/$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/mailbox-source.sh b/bin/message-source.sh similarity index 63% rename from bin/mailbox-source.sh rename to bin/message-source.sh index cbff7e6..21714cc 100755 --- a/bin/mailbox-source.sh +++ b/bin/message-source.sh @@ -1 +1 @@ -curl -i -H "Accept: application/json" --noproxy localhost http://localhost:9000/mailbox/source/$1/$2 +curl -i -H "Accept: application/json" --noproxy localhost http://localhost:9000/mailbox/$1/$2/source 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/web/mailbox_controller.go b/web/mailbox_controller.go index ecff8ae..1d4711b 100644 --- a/web/mailbox_controller.go +++ b/web/mailbox_controller.go @@ -55,13 +55,13 @@ func MailboxList(w http.ResponseWriter, req *http.Request, ctx *Context) (err er } } return RenderJson(w, jmessages) - } else { - return RenderPartial("mailbox/_list.html", w, map[string]interface{}{ - "ctx": ctx, - "name": name, - "messages": messages, - }) } + + return RenderPartial("mailbox/_list.html", w, map[string]interface{}{ + "ctx": ctx, + "name": name, + "messages": messages, + }) } func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) { @@ -234,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/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")