1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-22 12:07:04 +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
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
-----------
![An Email](http://cloud.github.com/downloads/jhillyerd/inbucket/inbucket-ss1.png)
*Viewing an email in Inbucket.*
![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
------------------
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:
* 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
Please check the [issues list](https://github.com/jhillyerd/inbucket/issues?state=open)
for more details.
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
the web interface will be available at [localhost:9000](http://localhost:9000/).
There are RedHat EL6 init, logrotate and httpd proxy configs provided.
About
-----
Inbucket is written in [Google Go][1].

View File

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

View File

@@ -34,7 +34,7 @@ max.recipients=100
max.idle.seconds=30
# 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
# (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
# 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 {
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">
<a href="javascript:deleteMessage('{{.message.Id}}');">Delete</a>
<a href="javascript:messageSource('{{.message.Id}}');">Source</a>
@@ -15,6 +17,21 @@
<td>{{.message.Date}}</td>
</tr>
<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="emailBody">{{.body}}</div>

View File

@@ -6,6 +6,7 @@ import (
"html/template"
"io"
"net/http"
"strconv"
)
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) {
// Don't have to validate these aren't empty, Gorilla returns 404
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)
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) {
// Don't have to validate these aren't empty, Gorilla returns 404
name := ctx.Vars["name"]
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)
if err != nil {
@@ -82,22 +70,14 @@ func MailboxShow(w http.ResponseWriter, req *http.Request, ctx *Context) (err er
"message": message,
"body": body,
"htmlAvailable": htmlAvailable,
"attachments": mime.Attachments,
})
}
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"]
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)
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) {
// Don't have to validate these aren't empty, Gorilla returns 404
name := ctx.Vars["name"]
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)
if err != nil {
@@ -153,19 +124,83 @@ func MailboxSource(w http.ResponseWriter, req *http.Request, ctx *Context) (err
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"]
id := ctx.Vars["id"]
if len(name) == 0 {
ctx.Session.AddFlash("Account name is required", "errors")
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
}
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)
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)
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/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/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
Router = r