1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-22 20:17:02 +00:00
This commit is contained in:
James Hillyerd
2012-11-06 09:33:36 -08:00
8 changed files with 152 additions and 59 deletions

View File

@@ -12,32 +12,37 @@ no password is required to browse the content of the mailboxes.
Inbucket has a built-in SMTP server and stores incoming mail as flat files on Inbucket has a built-in SMTP server and stores incoming mail as flat files on
disk - no external SMTP or database daemons required. disk - no external SMTP or database daemons required.
Features
--------
* Receive and store E/SMTP messages
* List messages in a mailbox
* Displays:
- Text content of a particular message
- Source of a message (headers + body text)
- HTML version of a message (in a new window)
* List MIME attachments with buttons to display or download
* Delete a message
* Purge messages after a configurable amount of time
* Optional load test mode; messages are never written to disk
It does not yet:
* Display inline attachments within HTML email
Screenshots Screenshots
----------- -----------
![An Email](http://cloud.github.com/downloads/jhillyerd/inbucket/inbucket-ss1.png) ![An Email](http://cloud.github.com/downloads/jhillyerd/inbucket/inbucket-ss1.png)
*Viewing an email in Inbucket.* *Viewing an email in Inbucket.*
![Metrics](http://cloud.github.com/downloads/jhillyerd/inbucket/inbucket-ss2.png) ![Metrics](http://cloud.github.com/downloads/jhillyerd/inbucket/inbucket-ss2.png)
*Watching metrics while Inbucket handles over 4,000 messages per minute.* *Watching metrics while Inbucket recieves and stores over 4,000 messages per minute.*
Development Status Development Status
------------------ ------------------
Inbucket is currently alpha quality: it works but is not well tested. Inbucket is currently beta quality: it works but is not well tested.
It can: Please check the [issues list](https://github.com/jhillyerd/inbucket/issues?state=open)
for more details.
* Receive SMTP and ESMTP messages and store them to disk
* List subject, sender and date of messages for a particular mailbox
* Parse MIME multipart emails
* Display the content of a particular message
* Display the source of a message (headers + body text)
* Display the HTML version of a message (in a new window)
* Delete a message
* Purge messages after a configurable amount of time
It does not yet:
* Display or download attachments
Installation Installation
------------ ------------
@@ -55,6 +60,8 @@ Unix and OS X machines as is. Launch the daemon:
By default the SMTP server will be listening on localhost port 2500 and By default the SMTP server will be listening on localhost port 2500 and
the web interface will be available at [localhost:9000](http://localhost:9000/). the web interface will be available at [localhost:9000](http://localhost:9000/).
There are RedHat EL6 init, logrotate and httpd proxy configs provided.
About About
----- -----
Inbucket is written in [Google Go][1]. Inbucket is written in [Google Go][1].

View File

@@ -10,6 +10,13 @@ label="$1"
# Bail on error # Bail on error
set -e set -e
# For OS X
if [ -x /usr/bin/gnutar ]; then
tar=/usr/bin/gnutar
else
tar=tar
fi
# Work directory # Work directory
tmpdir=/tmp/inbucket-dist.$$ tmpdir=/tmp/inbucket-dist.$$
mkdir -p $tmpdir mkdir -p $tmpdir
@@ -32,7 +39,7 @@ cp -r themes $distdir/themes
echo "Tarballing..." echo "Tarballing..."
tarball="$HOME/$distname.tbz2" tarball="$HOME/$distname.tbz2"
cd $tmpdir cd $tmpdir
tar --owner=root --group=root -cjvf $tarball $distname $tar --owner=root --group=0 -cjvf $tarball $distname
echo "Cleaning up..." echo "Cleaning up..."
if [ "$tmpdir" != "/" ]; then if [ "$tmpdir" != "/" ]; then

View File

@@ -34,7 +34,7 @@ max.recipients=100
max.idle.seconds=30 max.idle.seconds=30
# Maximum allowable size of message body in bytes (including attachments) # Maximum allowable size of message body in bytes (including attachments)
max.message.bytes=2048000 max.message.bytes=20480000
# Should we place messages into the datastore, or just throw them away # Should we place messages into the datastore, or just throw them away
# (for load testing): true or false # (for load testing): true or false

View File

@@ -10,4 +10,4 @@ swaks --h-Subject: "Swaks Plain Text" --body text.txt
swaks --h-Subject: "Swaks HTML" --data html.raw swaks --h-Subject: "Swaks HTML" --data html.raw
# Attachment test # Attachment test
swaks --h-Subject: "Swaks Attachment" --attach-type image/png --attach favicon.png swaks --h-Subject: "Swaks Attachment" --attach-type image/png --attach favicon.png --body text.txt

View File

@@ -327,3 +327,28 @@ table.metrics {
.metrics td.sparkline { .metrics td.sparkline {
width: 170px; width: 170px;
} }
#emailAttachments {
border-collapse: collapse;
}
#emailAttachments th, #emailAttachments td {
text-align: left;
padding: 0 3px 3px 0;
}
#emailAttachments .fileName:before {
content: '\203A\00A0';
}
#emailAttachments a {
background: #8ac6dc;
color: #fff;
text-decoration: none;
padding: 0 5px;
}
#emailAttachments a:hover {
background: #becf74;
}

View File

@@ -1,3 +1,5 @@
{{$name := .name}}
{{$id := .message.Id}}
<div id="emailActions"> <div id="emailActions">
<a href="javascript:deleteMessage('{{.message.Id}}');">Delete</a> <a href="javascript:deleteMessage('{{.message.Id}}');">Delete</a>
<a href="javascript:messageSource('{{.message.Id}}');">Source</a> <a href="javascript:messageSource('{{.message.Id}}');">Source</a>
@@ -15,6 +17,21 @@
<td>{{.message.Date}}</td> <td>{{.message.Date}}</td>
</tr> </tr>
<table> <table>
{{with .attachments}}
<table id="emailAttachments">
<tr><th colspan="4">Attachments:</th></tr>
{{range $i, $e := .}}
<tr>
<td class="fileName">{{$e.FileName}}</td>
<td>({{$e.ContentType}})</td>
<td><a href="/mailbox/vattach/{{$name}}/{{$id}}/{{$i}}/{{$e.FileName}}" target="_blank">View</a></td>
<td><a href="/mailbox/dattach/{{$name}}/{{$id}}/{{$i}}/{{$e.FileName}}">Download</a></td>
</tr>
{{end}}
</table>
{{end}}
<div id="emailSubject"><h3>{{.message.Subject}}</h3></div> <div id="emailSubject"><h3>{{.message.Subject}}</h3></div>
<div id="emailBody">{{.body}}</div> <div id="emailBody">{{.body}}</div>

View File

@@ -6,6 +6,7 @@ import (
"html/template" "html/template"
"io" "io"
"net/http" "net/http"
"strconv"
) )
func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) { func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
@@ -23,12 +24,8 @@ func MailboxIndex(w http.ResponseWriter, req *http.Request, ctx *Context) (err e
} }
func MailboxList(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) { func MailboxList(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
name := ctx.Vars["name"] name := ctx.Vars["name"]
if len(name) == 0 {
ctx.Session.AddFlash("Account name is required", "errors")
http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
return nil
}
mb, err := ctx.DataStore.MailboxFor(name) mb, err := ctx.DataStore.MailboxFor(name)
if err != nil { if err != nil {
@@ -48,18 +45,9 @@ func MailboxList(w http.ResponseWriter, req *http.Request, ctx *Context) (err er
} }
func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) { func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
name := ctx.Vars["name"] name := ctx.Vars["name"]
id := ctx.Vars["id"] id := ctx.Vars["id"]
if len(name) == 0 {
ctx.Session.AddFlash("Account name is required", "errors")
http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
return nil
}
if len(id) == 0 {
ctx.Session.AddFlash("Message ID is required", "errors")
http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
return nil
}
mb, err := ctx.DataStore.MailboxFor(name) mb, err := ctx.DataStore.MailboxFor(name)
if err != nil { if err != nil {
@@ -82,22 +70,14 @@ func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *Context) (err er
"message": message, "message": message,
"body": body, "body": body,
"htmlAvailable": htmlAvailable, "htmlAvailable": htmlAvailable,
"attachments": mime.Attachments,
}) })
} }
func MailboxHtml(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) { func MailboxHtml(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
name := ctx.Vars["name"] name := ctx.Vars["name"]
id := ctx.Vars["id"] id := ctx.Vars["id"]
if len(name) == 0 {
ctx.Session.AddFlash("Account name is required", "errors")
http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
return nil
}
if len(id) == 0 {
ctx.Session.AddFlash("Message ID is required", "errors")
http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
return nil
}
mb, err := ctx.DataStore.MailboxFor(name) mb, err := ctx.DataStore.MailboxFor(name)
if err != nil { if err != nil {
@@ -122,18 +102,9 @@ func MailboxHtml(w http.ResponseWriter, req *http.Request, ctx *Context) (err er
} }
func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) { func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
name := ctx.Vars["name"] name := ctx.Vars["name"]
id := ctx.Vars["id"] id := ctx.Vars["id"]
if len(name) == 0 {
ctx.Session.AddFlash("Account name is required", "errors")
http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
return nil
}
if len(id) == 0 {
ctx.Session.AddFlash("Message ID is required", "errors")
http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
return nil
}
mb, err := ctx.DataStore.MailboxFor(name) mb, err := ctx.DataStore.MailboxFor(name)
if err != nil { if err != nil {
@@ -153,19 +124,83 @@ func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *Context) (err
return nil return nil
} }
func MailboxDelete(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) { func MailboxDownloadAttach(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
name := ctx.Vars["name"] name := ctx.Vars["name"]
id := ctx.Vars["id"] id := ctx.Vars["id"]
if len(name) == 0 { numStr := ctx.Vars["num"]
ctx.Session.AddFlash("Account name is required", "errors") num, err := strconv.ParseUint(numStr, 10, 32)
if err != nil {
ctx.Session.AddFlash("Attachment number must be unsigned numeric", "errors")
http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther) http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
return nil return nil
} }
if len(id) == 0 {
ctx.Session.AddFlash("Message ID is required", "errors") mb, err := ctx.DataStore.MailboxFor(name)
if err != nil {
return err
}
message, err := mb.GetMessage(id)
if err != nil {
return err
}
_, body, err := message.ReadBody()
if err != nil {
return err
}
if int(num) >= len(body.Attachments) {
ctx.Session.AddFlash("Attachment number too high", "errors")
http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther) http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
return nil return nil
} }
part := body.Attachments[num]
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Content-Disposition", "attachment")
w.Write(part.Content())
return nil
}
func MailboxViewAttach(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
name := ctx.Vars["name"]
id := ctx.Vars["id"]
numStr := ctx.Vars["num"]
num, err := strconv.ParseUint(numStr, 10, 32)
if err != nil {
ctx.Session.AddFlash("Attachment number must be unsigned numeric", "errors")
http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
return nil
}
mb, err := ctx.DataStore.MailboxFor(name)
if err != nil {
return err
}
message, err := mb.GetMessage(id)
if err != nil {
return err
}
_, body, err := message.ReadBody()
if err != nil {
return err
}
if int(num) >= len(body.Attachments) {
ctx.Session.AddFlash("Attachment number too high", "errors")
http.Redirect(w, req, reverse("RootIndex"), http.StatusSeeOther)
return nil
}
part := body.Attachments[num]
w.Header().Set("Content-Type", part.ContentType())
w.Write(part.Content())
return nil
}
func MailboxDelete(w http.ResponseWriter, req *http.Request, ctx *Context) (err error) {
// Don't have to validate these aren't empty, Gorilla returns 404
name := ctx.Vars["name"]
id := ctx.Vars["id"]
mb, err := ctx.DataStore.MailboxFor(name) mb, err := ctx.DataStore.MailboxFor(name)
if err != nil { if err != nil {

View File

@@ -38,6 +38,8 @@ func setupRoutes(cfg config.WebConfig) {
r.Path("/mailbox/html/{name}/{id}").Handler(handler(MailboxHtml)).Name("MailboxHtml").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/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/delete/{name}/{id}").Handler(handler(MailboxDelete)).Name("MailboxDelete").Methods("POST")
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")
// Register w/ HTTP // Register w/ HTTP
Router = r Router = r