Refactor to github.com/mailhog/{MailHog-Server,MailHog-UI}

This commit is contained in:
Ian Kent 2014-12-24 16:52:53 +00:00
parent 09c9701511
commit ebbae16589
21 changed files with 10 additions and 2620 deletions

View file

@ -1,252 +0,0 @@
package api
import (
"encoding/json"
"net/smtp"
"strconv"
"github.com/ian-kent/Go-MailHog/MailHog-Server/config"
"github.com/ian-kent/Go-MailHog/data"
"github.com/ian-kent/Go-MailHog/storage"
"github.com/ian-kent/go-log/log"
gotcha "github.com/ian-kent/gotcha/app"
"github.com/ian-kent/gotcha/http"
"github.com/ian-kent/goose"
)
type APIv1 struct {
config *config.Config
app *gotcha.App
}
var stream *goose.EventStream
type ReleaseConfig struct {
Email string
Host string
Port string
}
func CreateAPIv1(conf *config.Config, app *gotcha.App) *APIv1 {
log.Println("Creating API v1")
apiv1 := &APIv1{
config: conf,
app: app,
}
stream = goose.NewEventStream()
r := app.Router
r.Get("/api/v1/messages/?", apiv1.messages)
r.Delete("/api/v1/messages/?", apiv1.delete_all)
r.Get("/api/v1/messages/(?P<id>[^/]+)/?", apiv1.message)
r.Delete("/api/v1/messages/(?P<id>[^/]+)/?", apiv1.delete_one)
r.Get("/api/v1/messages/(?P<id>[^/]+)/download/?", apiv1.download)
r.Get("/api/v1/messages/(?P<id>[^/]+)/mime/part/(\\d+)/download/?", apiv1.download_part)
r.Post("/api/v1/messages/(?P<id>[^/]+)/release/?", apiv1.release_one)
r.Get("/api/v1/events/?", apiv1.eventstream)
go func() {
for {
select {
case msg := <-apiv1.config.MessageChan:
log.Println("Got message in APIv1 event stream")
bytes, _ := json.MarshalIndent(msg, "", " ")
json := string(bytes)
log.Printf("Sending content: %s\n", json)
apiv1.broadcast(json)
}
}
}()
return apiv1
}
func (apiv1 *APIv1) broadcast(json string) {
log.Println("[APIv1] BROADCAST /api/v1/events")
b := []byte(json)
stream.Notify("data", b)
}
func (apiv1 *APIv1) eventstream(session *http.Session) {
log.Println("[APIv1] GET /api/v1/events")
stream.AddReceiver(session.Response.GetWriter())
}
func (apiv1 *APIv1) messages(session *http.Session) {
log.Println("[APIv1] GET /api/v1/messages")
// TODO start, limit
switch apiv1.config.Storage.(type) {
case *storage.MongoDB:
messages, _ := apiv1.config.Storage.(*storage.MongoDB).List(0, 1000)
bytes, _ := json.Marshal(messages)
session.Response.Headers.Add("Content-Type", "text/json")
session.Response.Write(bytes)
case *storage.InMemory:
messages, _ := apiv1.config.Storage.(*storage.InMemory).List(0, 1000)
bytes, _ := json.Marshal(messages)
session.Response.Headers.Add("Content-Type", "text/json")
session.Response.Write(bytes)
default:
session.Response.Status = 500
}
}
func (apiv1 *APIv1) message(session *http.Session) {
id := session.Stash["id"].(string)
log.Printf("[APIv1] GET /api/v1/messages/%s\n", id)
switch apiv1.config.Storage.(type) {
case *storage.MongoDB:
message, _ := apiv1.config.Storage.(*storage.MongoDB).Load(id)
bytes, _ := json.Marshal(message)
session.Response.Headers.Add("Content-Type", "text/json")
session.Response.Write(bytes)
case *storage.InMemory:
message, _ := apiv1.config.Storage.(*storage.InMemory).Load(id)
bytes, _ := json.Marshal(message)
session.Response.Headers.Add("Content-Type", "text/json")
session.Response.Write(bytes)
default:
session.Response.Status = 500
}
}
func (apiv1 *APIv1) download(session *http.Session) {
id := session.Stash["id"].(string)
log.Printf("[APIv1] GET /api/v1/messages/%s\n", id)
session.Response.Headers.Add("Content-Type", "message/rfc822")
session.Response.Headers.Add("Content-Disposition", "attachment; filename=\""+id+".eml\"")
switch apiv1.config.Storage.(type) {
case *storage.MongoDB:
message, _ := apiv1.config.Storage.(*storage.MongoDB).Load(id)
for h, l := range message.Content.Headers {
for _, v := range l {
session.Response.Write([]byte(h + ": " + v + "\r\n"))
}
}
session.Response.Write([]byte("\r\n" + message.Content.Body))
case *storage.InMemory:
message, _ := apiv1.config.Storage.(*storage.InMemory).Load(id)
for h, l := range message.Content.Headers {
for _, v := range l {
session.Response.Write([]byte(h + ": " + v + "\r\n"))
}
}
session.Response.Write([]byte("\r\n" + message.Content.Body))
default:
session.Response.Status = 500
}
}
func (apiv1 *APIv1) download_part(session *http.Session) {
id := session.Stash["id"].(string)
part, _ := strconv.Atoi(session.Stash["part"].(string))
log.Printf("[APIv1] GET /api/v1/messages/%s/mime/part/%d/download\n", id, part)
// TODO extension from content-type?
session.Response.Headers.Add("Content-Disposition", "attachment; filename=\""+id+"-part-"+strconv.Itoa(part)+"\"")
switch apiv1.config.Storage.(type) {
case *storage.MongoDB:
message, _ := apiv1.config.Storage.(*storage.MongoDB).Load(id)
for h, l := range message.MIME.Parts[part].Headers {
for _, v := range l {
session.Response.Headers.Add(h, v)
}
}
session.Response.Write([]byte("\r\n" + message.MIME.Parts[part].Body))
case *storage.InMemory:
message, _ := apiv1.config.Storage.(*storage.InMemory).Load(id)
for h, l := range message.MIME.Parts[part].Headers {
for _, v := range l {
session.Response.Headers.Add(h, v)
}
}
session.Response.Write([]byte("\r\n" + message.MIME.Parts[part].Body))
default:
session.Response.Status = 500
}
}
func (apiv1 *APIv1) delete_all(session *http.Session) {
log.Println("[APIv1] POST /api/v1/messages")
session.Response.Headers.Add("Content-Type", "text/json")
switch apiv1.config.Storage.(type) {
case *storage.MongoDB:
apiv1.config.Storage.(*storage.MongoDB).DeleteAll()
case *storage.InMemory:
apiv1.config.Storage.(*storage.InMemory).DeleteAll()
default:
session.Response.Status = 500
return
}
}
func (apiv1 *APIv1) release_one(session *http.Session) {
id := session.Stash["id"].(string)
log.Printf("[APIv1] POST /api/v1/messages/%s/release\n", id)
session.Response.Headers.Add("Content-Type", "text/json")
var msg = &data.Message{}
switch apiv1.config.Storage.(type) {
case *storage.MongoDB:
msg, _ = apiv1.config.Storage.(*storage.MongoDB).Load(id)
case *storage.InMemory:
msg, _ = apiv1.config.Storage.(*storage.InMemory).Load(id)
default:
session.Response.Status = 500
return
}
decoder := json.NewDecoder(session.Request.Body())
var cfg ReleaseConfig
err := decoder.Decode(&cfg)
if err != nil {
log.Printf("Error decoding request body: %s", err)
session.Response.Status = 500
session.Response.Write([]byte("Error decoding request body"))
return
}
log.Printf("Releasing to %s (via %s:%s)", cfg.Email, cfg.Host, cfg.Port)
log.Printf("Got message: %s", msg.ID)
bytes := make([]byte, 0)
for h, l := range msg.Content.Headers {
for _, v := range l {
bytes = append(bytes, []byte(h+": "+v+"\r\n")...)
}
}
bytes = append(bytes, []byte("\r\n"+msg.Content.Body)...)
err = smtp.SendMail(cfg.Host+":"+cfg.Port, nil, "nobody@"+apiv1.config.Hostname, []string{cfg.Email}, bytes)
if err != nil {
log.Printf("Failed to release message: %s", err)
session.Response.Status = 500
return
}
log.Printf("Message released successfully")
}
func (apiv1 *APIv1) delete_one(session *http.Session) {
id := session.Stash["id"].(string)
log.Printf("[APIv1] POST /api/v1/messages/%s/delete\n", id)
session.Response.Headers.Add("Content-Type", "text/json")
switch apiv1.config.Storage.(type) {
case *storage.MongoDB:
apiv1.config.Storage.(*storage.MongoDB).DeleteOne(id)
case *storage.InMemory:
apiv1.config.Storage.(*storage.InMemory).DeleteOne(id)
default:
session.Response.Status = 500
}
}

View file

@ -1,83 +0,0 @@
package config
import (
"flag"
"log"
"github.com/ian-kent/Go-MailHog/MailHog-Server/monkey"
"github.com/ian-kent/Go-MailHog/data"
"github.com/ian-kent/Go-MailHog/storage"
"github.com/ian-kent/envconf"
)
func DefaultConfig() *Config {
return &Config{
SMTPBindAddr: "0.0.0.0:1025",
HTTPBindAddr: "0.0.0.0:8025",
Hostname: "mailhog.example",
MongoUri: "127.0.0.1:27017",
MongoDb: "mailhog",
MongoColl: "messages",
StorageType: "memory",
MessageChan: make(chan *data.Message),
}
}
type Config struct {
SMTPBindAddr string
HTTPBindAddr string
Hostname string
MongoUri string
MongoDb string
MongoColl string
StorageType string
InviteJim bool
Storage storage.Storage
MessageChan chan *data.Message
Assets func(asset string) ([]byte, error)
Monkey monkey.ChaosMonkey
}
var cfg = DefaultConfig()
var jim = &monkey.Jim{}
func Configure() *Config {
switch cfg.StorageType {
case "memory":
log.Println("Using in-memory storage")
cfg.Storage = storage.CreateInMemory()
case "mongodb":
log.Println("Using MongoDB message storage")
s := storage.CreateMongoDB(cfg.MongoUri, cfg.MongoDb, cfg.MongoColl)
if s == nil {
log.Println("MongoDB storage unavailable, reverting to in-memory storage")
cfg.Storage = storage.CreateInMemory()
} else {
log.Println("Connected to MongoDB")
cfg.Storage = s
}
default:
log.Fatalf("Invalid storage type %s", cfg.StorageType)
}
if cfg.InviteJim {
jim.Configure(func(message string, args ...interface{}) {
log.Printf(message, args...)
})
cfg.Monkey = jim
}
return cfg
}
func RegisterFlags() {
flag.StringVar(&cfg.SMTPBindAddr, "smtpbindaddr", envconf.FromEnvP("MH_SMTP_BIND_ADDR", "0.0.0.0:1025").(string), "SMTP bind interface and port, e.g. 0.0.0.0:1025 or just :1025")
flag.StringVar(&cfg.HTTPBindAddr, "httpbindaddr", envconf.FromEnvP("MH_HTTP_BIND_ADDR", "0.0.0.0:8025").(string), "HTTP bind interface and port, e.g. 0.0.0.0:8025 or just :8025")
flag.StringVar(&cfg.Hostname, "hostname", envconf.FromEnvP("MH_HOSTNAME", "mailhog.example").(string), "Hostname for EHLO/HELO response, e.g. mailhog.example")
flag.StringVar(&cfg.StorageType, "storage", envconf.FromEnvP("MH_STORAGE", "memory").(string), "Message storage: memory (default) or mongodb")
flag.StringVar(&cfg.MongoUri, "mongouri", envconf.FromEnvP("MH_MONGO_URI", "127.0.0.1:27017").(string), "MongoDB URI, e.g. 127.0.0.1:27017")
flag.StringVar(&cfg.MongoDb, "mongodb", envconf.FromEnvP("MH_MONGO_DB", "mailhog").(string), "MongoDB database, e.g. mailhog")
flag.StringVar(&cfg.MongoColl, "mongocoll", envconf.FromEnvP("MH_MONGO_COLLECTION", "messages").(string), "MongoDB collection, e.g. messages")
flag.BoolVar(&cfg.InviteJim, "invite-jim", envconf.FromEnvP("MH_INVITE_JIM", false).(bool), "Decide whether to invite Jim (beware, he causes trouble)")
jim.RegisterFlags()
}

View file

@ -1,42 +0,0 @@
package main
import (
"flag"
"os"
"github.com/ian-kent/Go-MailHog/MailHog-Server/api"
"github.com/ian-kent/Go-MailHog/MailHog-Server/config"
"github.com/ian-kent/Go-MailHog/MailHog-Server/smtp"
"github.com/ian-kent/Go-MailHog/MailHog-UI/assets"
"github.com/ian-kent/Go-MailHog/http"
"github.com/ian-kent/go-log/log"
gotcha "github.com/ian-kent/gotcha/app"
)
var conf *config.Config
var exitCh chan int
func configure() {
config.RegisterFlags()
flag.Parse()
conf = config.Configure()
}
func main() {
configure()
exitCh = make(chan int)
cb := func(app *gotcha.App) {
api.CreateAPIv1(conf, app)
}
go http.Listen(conf, assets.Asset, exitCh, cb)
go smtp.Listen(conf, exitCh)
for {
select {
case <-exitCh:
log.Printf("Received exit signal")
os.Exit(0)
}
}
}

View file

@ -1,105 +0,0 @@
package monkey
import (
"flag"
"math/rand"
"net"
"time"
"github.com/ian-kent/linkio"
)
// Jim is a chaos monkey
type Jim struct {
disconnectChance float64
acceptChance float64
linkSpeedAffect float64
linkSpeedMin float64
linkSpeedMax float64
rejectSenderChance float64
rejectRecipientChance float64
rejectAuthChance float64
logf func(message string, args ...interface{})
}
// RegisterFlags implements ChaosMonkey.RegisterFlags
func (j *Jim) RegisterFlags() {
flag.Float64Var(&j.disconnectChance, "jim-disconnect", 0.005, "Chance of disconnect")
flag.Float64Var(&j.acceptChance, "jim-accept", 0.99, "Chance of accept")
flag.Float64Var(&j.linkSpeedAffect, "jim-linkspeed-affect", 0.1, "Chance of affecting link speed")
flag.Float64Var(&j.linkSpeedMin, "jim-linkspeed-min", 1024, "Minimum link speed (in bytes per second)")
flag.Float64Var(&j.linkSpeedMax, "jim-linkspeed-max", 10240, "Maximum link speed (in bytes per second)")
flag.Float64Var(&j.rejectSenderChance, "jim-reject-sender", 0.05, "Chance of rejecting a sender (MAIL FROM)")
flag.Float64Var(&j.rejectRecipientChance, "jim-reject-recipient", 0.05, "Chance of rejecting a recipient (RCPT TO)")
flag.Float64Var(&j.rejectAuthChance, "jim-reject-auth", 0.05, "Chance of rejecting authentication (AUTH)")
}
// Configure implements ChaosMonkey.Configure
func (j *Jim) Configure(logf func(string, ...interface{})) {
j.logf = logf
rand.Seed(time.Now().Unix())
}
// Accept implements ChaosMonkey.Accept
func (j *Jim) Accept(conn net.Conn) bool {
if rand.Float64() > j.acceptChance {
j.logf("Jim: Rejecting connection\n")
return false
}
j.logf("Jim: Allowing connection\n")
return true
}
// LinkSpeed implements ChaosMonkey.LinkSpeed
func (j *Jim) LinkSpeed() *linkio.Throughput {
rand.Seed(time.Now().Unix())
if rand.Float64() < j.linkSpeedAffect {
lsDiff := j.linkSpeedMax - j.linkSpeedMin
lsAffect := j.linkSpeedMin + (lsDiff * rand.Float64())
f := linkio.Throughput(lsAffect) * linkio.BytePerSecond
j.logf("Jim: Restricting throughput to %s\n", f)
return &f
}
j.logf("Jim: Allowing unrestricted throughput")
return nil
}
// ValidRCPT implements ChaosMonkey.ValidRCPT
func (j *Jim) ValidRCPT(rcpt string) bool {
if rand.Float64() < j.rejectRecipientChance {
j.logf("Jim: Rejecting recipient %s\n", rcpt)
return false
}
j.logf("Jim: Allowing recipient%s\n", rcpt)
return true
}
// ValidMAIL implements ChaosMonkey.ValidMAIL
func (j *Jim) ValidMAIL(mail string) bool {
if rand.Float64() < j.rejectSenderChance {
j.logf("Jim: Rejecting sender %s\n", mail)
return false
}
j.logf("Jim: Allowing sender %s\n", mail)
return true
}
// ValidAUTH implements ChaosMonkey.ValidAUTH
func (j *Jim) ValidAUTH(mechanism string, args ...string) bool {
if rand.Float64() < j.rejectAuthChance {
j.logf("Jim: Rejecting authentication %s: %s\n", mechanism, args)
return false
}
j.logf("Jim: Allowing authentication %s: %s\n", mechanism, args)
return true
}
// Disconnect implements ChaosMonkey.Disconnect
func (j *Jim) Disconnect() bool {
if rand.Float64() < j.disconnectChance {
j.logf("Jim: Being nasty, kicking them off\n")
return true
}
j.logf("Jim: Being nice, letting them stay\n")
return false
}

View file

@ -1,28 +0,0 @@
package monkey
import (
"net"
"github.com/ian-kent/linkio"
)
// ChaosMonkey should be implemented by chaos monkeys!
type ChaosMonkey interface {
RegisterFlags()
Configure(func(string, ...interface{}))
// Accept is called for each incoming connection. Returning false closes the connection.
Accept(conn net.Conn) bool
// LinkSpeed sets the maximum connection throughput (in one direction)
LinkSpeed() *linkio.Throughput
// ValidRCPT is called for the RCPT command. Returning false signals an invalid recipient.
ValidRCPT(rcpt string) bool
// ValidMAIL is called for the MAIL command. Returning false signals an invalid sender.
ValidMAIL(mail string) bool
// ValidAUTH is called after authentication. Returning false signals invalid authentication.
ValidAUTH(mechanism string, args ...string) bool
// Disconnect is called after every read. Returning true will close the connection.
Disconnect() bool
}

View file

@ -1,161 +0,0 @@
package smtp
// http://www.rfc-editor.org/rfc/rfc5321.txt
import (
"io"
"log"
"strings"
"github.com/ian-kent/Go-MailHog/MailHog-Server/monkey"
"github.com/ian-kent/Go-MailHog/data"
"github.com/ian-kent/Go-MailHog/storage"
"github.com/ian-kent/linkio"
"github.com/mailhog/smtp"
)
// Session represents a SMTP session using net.TCPConn
type Session struct {
conn io.ReadWriteCloser
proto *smtp.Protocol
storage storage.Storage
messageChan chan *data.Message
remoteAddress string
isTLS bool
line string
link *linkio.Link
reader io.Reader
writer io.Writer
monkey monkey.ChaosMonkey
}
// Accept starts a new SMTP session using io.ReadWriteCloser
func Accept(remoteAddress string, conn io.ReadWriteCloser, storage storage.Storage, messageChan chan *data.Message, hostname string, monkey monkey.ChaosMonkey) {
defer conn.Close()
proto := smtp.NewProtocol()
proto.Hostname = hostname
var link *linkio.Link
reader := io.Reader(conn)
writer := io.Writer(conn)
if monkey != nil {
linkSpeed := monkey.LinkSpeed()
if linkSpeed != nil {
link = linkio.NewLink(*linkSpeed * linkio.BytePerSecond)
reader = link.NewLinkReader(io.Reader(conn))
writer = link.NewLinkWriter(io.Writer(conn))
}
}
session := &Session{conn, proto, storage, messageChan, remoteAddress, false, "", link, reader, writer, monkey}
proto.LogHandler = session.logf
proto.MessageReceivedHandler = session.acceptMessage
proto.ValidateSenderHandler = session.validateSender
proto.ValidateRecipientHandler = session.validateRecipient
proto.ValidateAuthenticationHandler = session.validateAuthentication
session.logf("Starting session")
session.Write(proto.Start())
for session.Read() == true {
if monkey != nil && monkey.Disconnect != nil && monkey.Disconnect() {
session.conn.Close()
break
}
}
session.logf("Session ended")
}
func (c *Session) validateAuthentication(mechanism string, args ...string) (errorReply *smtp.Reply, ok bool) {
if c.monkey != nil {
ok := c.monkey.ValidAUTH(mechanism, args...)
if !ok {
// FIXME better error?
return smtp.ReplyUnrecognisedCommand(), false
}
}
return nil, true
}
func (c *Session) validateRecipient(to string) bool {
if c.monkey != nil {
ok := c.monkey.ValidRCPT(to)
if !ok {
return false
}
}
return true
}
func (c *Session) validateSender(from string) bool {
if c.monkey != nil {
ok := c.monkey.ValidMAIL(from)
if !ok {
return false
}
}
return true
}
func (c *Session) acceptMessage(msg *data.Message) (id string, err error) {
c.logf("Storing message %s", msg.ID)
id, err = c.storage.Store(msg)
c.messageChan <- msg
return
}
func (c *Session) logf(message string, args ...interface{}) {
message = strings.Join([]string{"[SMTP %s]", message}, " ")
args = append([]interface{}{c.remoteAddress}, args...)
log.Printf(message, args...)
}
// Read reads from the underlying net.TCPConn
func (c *Session) Read() bool {
buf := make([]byte, 1024)
n, err := c.reader.Read(buf)
if n == 0 {
c.logf("Connection closed by remote host\n")
io.Closer(c.conn).Close() // not sure this is necessary?
return false
}
if err != nil {
c.logf("Error reading from socket: %s\n", err)
return false
}
text := string(buf[0:n])
logText := strings.Replace(text, "\n", "\\n", -1)
logText = strings.Replace(logText, "\r", "\\r", -1)
c.logf("Received %d bytes: '%s'\n", n, logText)
c.line += text
for strings.Contains(c.line, "\n") {
line, reply := c.proto.Parse(c.line)
c.line = line
if reply != nil {
c.Write(reply)
if reply.Status == 221 {
io.Closer(c.conn).Close()
return false
}
}
}
return true
}
// Write writes a reply to the underlying net.TCPConn
func (c *Session) Write(reply *smtp.Reply) {
lines := reply.Lines()
for _, l := range lines {
logText := strings.Replace(l, "\n", "\\n", -1)
logText = strings.Replace(logText, "\r", "\\r", -1)
c.logf("Sent %d bytes: '%s'", len(l), logText)
c.writer.Write([]byte(l))
}
}

View file

@ -1,141 +0,0 @@
package smtp
import (
"errors"
"sync"
"testing"
. "github.com/smartystreets/goconvey/convey"
"github.com/ian-kent/Go-MailHog/data"
"github.com/ian-kent/Go-MailHog/storage"
)
type fakeRw struct {
_read func(p []byte) (n int, err error)
_write func(p []byte) (n int, err error)
_close func() error
}
func (rw *fakeRw) Read(p []byte) (n int, err error) {
if rw._read != nil {
return rw._read(p)
}
return 0, nil
}
func (rw *fakeRw) Close() error {
if rw._close != nil {
return rw._close()
}
return nil
}
func (rw *fakeRw) Write(p []byte) (n int, err error) {
if rw._write != nil {
return rw._write(p)
}
return len(p), nil
}
func TestAccept(t *testing.T) {
Convey("Accept should handle a connection", t, func() {
frw := &fakeRw{}
mChan := make(chan *data.Message)
Accept("1.1.1.1:11111", frw, storage.CreateInMemory(), mChan, "localhost", nil)
})
}
func TestSocketError(t *testing.T) {
Convey("Socket errors should return from Accept", t, func() {
frw := &fakeRw{
_read: func(p []byte) (n int, err error) {
return -1, errors.New("OINK")
},
}
mChan := make(chan *data.Message)
Accept("1.1.1.1:11111", frw, storage.CreateInMemory(), mChan, "localhost", nil)
})
}
func TestAcceptMessage(t *testing.T) {
Convey("acceptMessage should be called", t, func() {
mbuf := "EHLO localhost\nMAIL FROM:<test>\nRCPT TO:<test>\nDATA\nHi.\r\n.\r\nQUIT\n"
var rbuf []byte
frw := &fakeRw{
_read: func(p []byte) (n int, err error) {
if len(p) >= len(mbuf) {
ba := []byte(mbuf)
mbuf = ""
for i, b := range ba {
p[i] = b
}
return len(ba), nil
}
ba := []byte(mbuf[0:len(p)])
mbuf = mbuf[len(p):]
for i, b := range ba {
p[i] = b
}
return len(ba), nil
},
_write: func(p []byte) (n int, err error) {
rbuf = append(rbuf, p...)
return len(p), nil
},
_close: func() error {
return nil
},
}
mChan := make(chan *data.Message)
var wg sync.WaitGroup
wg.Add(1)
handlerCalled := false
go func() {
handlerCalled = true
<-mChan
//FIXME breaks some tests (in drone.io)
//m := <-mChan
//So(m, ShouldNotBeNil)
wg.Done()
}()
Accept("1.1.1.1:11111", frw, storage.CreateInMemory(), mChan, "localhost", nil)
wg.Wait()
So(handlerCalled, ShouldBeTrue)
})
}
func TestValidateAuthentication(t *testing.T) {
Convey("validateAuthentication is always successful", t, func() {
c := &Session{}
err, ok := c.validateAuthentication("OINK")
So(err, ShouldBeNil)
So(ok, ShouldBeTrue)
err, ok = c.validateAuthentication("OINK", "arg1")
So(err, ShouldBeNil)
So(ok, ShouldBeTrue)
err, ok = c.validateAuthentication("OINK", "arg1", "arg2")
So(err, ShouldBeNil)
So(ok, ShouldBeTrue)
})
}
func TestValidateRecipient(t *testing.T) {
Convey("validateRecipient is always successful", t, func() {
c := &Session{}
So(c.validateRecipient("OINK"), ShouldBeTrue)
So(c.validateRecipient("foo@bar.mailhog"), ShouldBeTrue)
})
}
func TestValidateSender(t *testing.T) {
Convey("validateSender is always successful", t, func() {
c := &Session{}
So(c.validateSender("OINK"), ShouldBeTrue)
So(c.validateSender("foo@bar.mailhog"), ShouldBeTrue)
})
}

View file

@ -1,43 +0,0 @@
package smtp
import (
"io"
"log"
"net"
"github.com/ian-kent/Go-MailHog/MailHog-Server/config"
)
func Listen(cfg *config.Config, exitCh chan int) *net.TCPListener {
log.Printf("[SMTP] Binding to address: %s\n", cfg.SMTPBindAddr)
ln, err := net.Listen("tcp", cfg.SMTPBindAddr)
if err != nil {
log.Fatalf("[SMTP] Error listening on socket: %s\n", err)
}
defer ln.Close()
for {
conn, err := ln.Accept()
if err != nil {
log.Printf("[SMTP] Error accepting connection: %s\n", err)
continue
}
if cfg.Monkey != nil {
ok := cfg.Monkey.Accept(conn)
if !ok {
conn.Close()
continue
}
}
go Accept(
conn.(*net.TCPConn).RemoteAddr().String(),
io.ReadWriteCloser(conn),
cfg.Storage,
cfg.MessageChan,
cfg.Hostname,
cfg.Monkey,
)
}
}

View file

@ -1 +0,0 @@
assets.go

Binary file not shown.

Before

Width:  |  Height:  |  Size: 673 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -1,291 +0,0 @@
var mailhogApp = angular.module('mailhogApp', []);
function guid() {
function s4() {
return Math.floor((1 + Math.random()) * 0x10000)
.toString(16)
.substring(1);
}
return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
s4() + '-' + s4() + s4() + s4();
}
mailhogApp.controller('MailCtrl', function ($scope, $http, $sce, $timeout) {
$scope.cache = {};
$scope.previewAllHeaders = false;
$scope.eventsPending = {};
$scope.eventCount = 0;
$scope.eventDone = 0;
$scope.eventFailed = 0;
$scope.hasEventSource = false;
$scope.source = null;
$(function() {
$scope.openStream();
});
$scope.toggleStream = function() {
$scope.source == null ? $scope.openStream() : $scope.closeStream();
}
$scope.openStream = function() {
$scope.source = new EventSource('/api/v1/events');
$scope.source.addEventListener('message', function(e) {
$scope.$apply(function() {
$scope.messages.push(JSON.parse(e.data));
});
}, false);
$scope.source.addEventListener('open', function(e) {
$scope.$apply(function() {
$scope.hasEventSource = true;
});
}, false);
$scope.source.addEventListener('error', function(e) {
//if(e.readyState == EventSource.CLOSED) {
$scope.$apply(function() {
$scope.hasEventSource = false;
});
//}
}, false);
}
$scope.closeStream = function() {
$scope.source.close();
$scope.source = null;
$scope.hasEventSource = false;
}
$scope.tryDecodeMime = function(str) {
return unescapeFromMime(str)
}
$scope.startEvent = function(name, args, glyphicon) {
var eID = guid();
//console.log("Starting event '" + name + "' with id '" + eID + "'")
var e = {
id: eID,
name: name,
started: new Date(),
complete: false,
failed: false,
args: args,
glyphicon: glyphicon,
getClass: function() {
// FIXME bit nasty
if(this.failed) {
return "bg-danger"
}
if(this.complete) {
return "bg-success"
}
return "bg-warning"; // pending
},
done: function() {
//delete $scope.eventsPending[eID]
var e = this;
e.complete = true;
$scope.eventDone++;
if(this.failed) {
// console.log("Failed event '" + e.name + "' with id '" + eID + "'")
} else {
// console.log("Completed event '" + e.name + "' with id '" + eID + "'")
$timeout(function() {
e.remove();
}, 10000);
}
},
fail: function() {
$scope.eventFailed++;
this.failed = true;
this.done();
},
remove: function() {
// console.log("Deleted event '" + e.name + "' with id '" + eID + "'")
if(e.failed) {
$scope.eventFailed--;
}
delete $scope.eventsPending[eID];
$scope.eventDone--;
$scope.eventCount--;
return false;
}
};
$scope.eventsPending[eID] = e;
$scope.eventCount++;
return e;
}
$scope.messagesDisplayed = function() {
return $('#messages-container table tbody tr').length
}
$scope.refresh = function() {
var e = $scope.startEvent("Loading messages", null, "glyphicon-download");
$http.get('/api/v1/messages').success(function(data) {
$scope.messages = data;
e.done();
});
}
$scope.refresh();
$scope.selectMessage = function(message) {
if($scope.cache[message.ID]) {
$scope.preview = $scope.cache[message.ID];
reflow();
} else {
$scope.preview = message;
var e = $scope.startEvent("Loading message", message.ID, "glyphicon-download-alt");
$http.get('/api/v1/messages/' + message.ID).success(function(data) {
$scope.cache[message.ID] = data;
data.previewHTML = $sce.trustAsHtml($scope.getMessageHTML(data));
$scope.preview = data;
preview = $scope.cache[message.ID];
reflow();
e.done();
});
}
}
$scope.toggleHeaders = function(val) {
$scope.previewAllHeaders = val;
var t = window.setInterval(function() {
if(val) {
if($('#hide-headers').length) {
window.clearInterval(t);
reflow();
}
} else {
if($('#show-headers').length) {
window.clearInterval(t);
reflow();
}
}
}, 10);
}
$scope.tryDecodeContent = function(message, content) {
var charset = "UTF-8"
if(message.Content.Headers["Content-Type"][0]) {
// TODO
}
if(message.Content.Headers["Content-Transfer-Encoding"][0]) {
if(message.Content.Headers["Content-Transfer-Encoding"][0] == "quoted-printable") {
content = unescapeFromQuotedPrintable(content, charset)
}
}
return content
}
$scope.getMessagePlain = function(message) {
var l = $scope.findMatchingMIME(message, "text/plain");
if(l != null && l !== "undefined") {
return $scope.tryDecode(l);
}
return message.Content.Body;
}
$scope.findMatchingMIME = function(part, mime) {
// TODO cache results
if(part.MIME) {
for(var p in part.MIME.Parts) {
if("Content-Type" in part.MIME.Parts[p].Headers) {
if(part.MIME.Parts[p].Headers["Content-Type"].length > 0) {
if(part.MIME.Parts[p].Headers["Content-Type"][0].match(mime + ";?.*")) {
return part.MIME.Parts[p];
} else if (part.MIME.Parts[p].Headers["Content-Type"][0].match(/multipart\/.*/)) {
var f = $scope.findMatchingMIME(part.MIME.Parts[p], mime);
if(f != null) {
return f;
}
}
}
}
}
}
return null;
}
$scope.hasHTML = function(message) {
// TODO cache this
var l = $scope.findMatchingMIME(message, "text/html");
if(l != null && l !== "undefined") {
return true
}
return false;
}
$scope.getMessageHTML = function(message) {
var l = $scope.findMatchingMIME(message, "text/html");
if(l != null && l !== "undefined") {
return $scope.tryDecode(l);
}
return "<HTML not found>";
}
$scope.tryDecode = function(l){
if(l.Headers && l.Headers["Content-Type"] && l.Headers["Content-Transfer-Encoding"]){
return $scope.tryDecodeContent({Content:l},l.Body.replace(/=[\r\n]+/gm,""));
}else{
return l.Body;
}
};
$scope.date = function(timestamp) {
return (new Date(timestamp)).toString();
};
$scope.deleteAll = function() {
$('#confirm-delete-all').modal('show');
}
$scope.releaseOne = function(message) {
$scope.releasing = message;
$('#release-one').modal('show');
}
$scope.confirmReleaseMessage = function() {
$('#release-one').modal('hide');
var message = $scope.releasing;
$scope.releasing = null;
var e = $scope.startEvent("Releasing message", message.ID, "glyphicon-share");
$http.post('/api/v1/messages/' + message.ID + '/release', {
email: $('#release-message-email').val(),
host: $('#release-message-smtp-host').val(),
port: $('#release-message-smtp-port').val(),
}).success(function() {
e.done();
}).error(function(err) {
e.fail();
e.error = err;
});
}
$scope.getSource = function(message) {
var source = "";
$.each(message.Content.Headers, function(k, v) {
source += k + ": " + v + "\n";
});
source += "\n";
source += message.Content.Body;
return source;
}
$scope.deleteAllConfirm = function() {
$('#confirm-delete-all').modal('hide');
var e = $scope.startEvent("Deleting all messages", null, "glyphicon-remove-circle");
$http.delete('/api/v1/messages').success(function() {
$scope.refresh();
$scope.preview = null;
e.done()
});
}
$scope.deleteOne = function(message) {
var e = $scope.startEvent("Deleting message", message.ID, "glyphicon-remove");
$http.delete('/api/v1/messages/' + message.ID).success(function() {
if($scope.preview._id == message._id) $scope.preview = null;
$scope.refresh();
e.done();
});
}
});

View file

@ -1,980 +0,0 @@
// -*- coding: utf-8 -*-
// GO-MAILHOG: This file borrowed from http://0xcc.net/jsescape/strutil.js
// Utility functions for strings.
//
// Copyright (C) 2007 Satoru Takabayashi <satoru 0xcc.net>
// All rights reserved. This is free software with ABSOLUTELY NO WARRANTY.
// You can redistribute it and/or modify it under the terms of
// the GNU General Public License version 2.
// NOTES:
//
// Surrogate pairs:
//
// 1st 0xD800 - 0xDBFF (high surrogate)
// 2nd 0xDC00 - 0xDFFF (low surrogate)
//
// UTF-8 sequences:
//
// 0xxxxxxx
// 110xxxxx 10xxxxxx
// 1110xxxx 10xxxxxx 10xxxxxx
// 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
var EQUAL_SIGN = 0x3D;
var QUESTION_MARK = 0x3F;
// "あい" => [ 0x3042, 0x3044 ]
function convertStringToUnicodeCodePoints(str) {
var surrogate_1st = 0;
var unicode_codes = [];
for (var i = 0; i < str.length; ++i) {
var utf16_code = str.charCodeAt(i);
if (surrogate_1st != 0) {
if (utf16_code >= 0xDC00 && utf16_code <= 0xDFFF) {
var surrogate_2nd = utf16_code;
var unicode_code = (surrogate_1st - 0xD800) * (1 << 10) + (1 << 16) +
(surrogate_2nd - 0xDC00);
unicode_codes.push(unicode_code);
} else {
// Malformed surrogate pair ignored.
}
surrogate_1st = 0;
} else if (utf16_code >= 0xD800 && utf16_code <= 0xDBFF) {
surrogate_1st = utf16_code;
} else {
unicode_codes.push(utf16_code);
}
}
return unicode_codes;
}
// [ 0xE3, 0x81, 0x82, 0xE3, 0x81, 0x84 ] => [ 0x3042, 0x3044 ]
function convertUtf8BytesToUnicodeCodePoints(utf8_bytes) {
var unicode_codes = [];
var unicode_code = 0;
var num_followed = 0;
for (var i = 0; i < utf8_bytes.length; ++i) {
var utf8_byte = utf8_bytes[i];
if (utf8_byte >= 0x100) {
// Malformed utf8 byte ignored.
} else if ((utf8_byte & 0xC0) == 0x80) {
if (num_followed > 0) {
unicode_code = (unicode_code << 6) | (utf8_byte & 0x3f);
num_followed -= 1;
} else {
// Malformed UTF-8 sequence ignored.
}
} else {
if (num_followed == 0) {
unicode_codes.push(unicode_code);
} else {
// Malformed UTF-8 sequence ignored.
}
if (utf8_byte < 0x80){ // 1-byte
unicode_code = utf8_byte;
num_followed = 0;
} else if ((utf8_byte & 0xE0) == 0xC0) { // 2-byte
unicode_code = utf8_byte & 0x1f;
num_followed = 1;
} else if ((utf8_byte & 0xF0) == 0xE0) { // 3-byte
unicode_code = utf8_byte & 0x0f;
num_followed = 2;
} else if ((utf8_byte & 0xF8) == 0xF0) { // 4-byte
unicode_code = utf8_byte & 0x07;
num_followed = 3;
} else {
// Malformed UTF-8 sequence ignored.
}
}
}
if (num_followed == 0) {
unicode_codes.push(unicode_code);
} else {
// Malformed UTF-8 sequence ignored.
}
unicode_codes.shift(); // Trim the first element.
return unicode_codes;
}
// Helper function.
function convertEscapedCodesToCodes(str, prefix, base, num_bits) {
var parts = str.split(prefix);
parts.shift(); // Trim the first element.
var codes = [];
var max = Math.pow(2, num_bits);
for (var i = 0; i < parts.length; ++i) {
var code = parseInt(parts[i], base);
if (code >= 0 && code < max) {
codes.push(code);
} else {
// Malformed code ignored.
}
}
return codes;
}
// r'\u3042\u3044' => [ 0x3042, 0x3044 ]
// Note that the r '...' notation is borrowed from Python.
function convertEscapedUtf16CodesToUtf16Codes(str) {
return convertEscapedCodesToCodes(str, "\\u", 16, 16);
}
// r'\U00003042\U00003044' => [ 0x3042, 0x3044 ]
function convertEscapedUtf32CodesToUnicodeCodePoints(str) {
return convertEscapedCodesToCodes(str, "\\U", 16, 32);
}
// r'\xE3\x81\x82\xE3\x81\x84' => [ 0xE3, 0x81, 0x82, 0xE3, 0x81, 0x84 ]
// r'\343\201\202\343\201\204' => [ 0343, 0201, 0202, 0343, 0201, 0204 ]
function convertEscapedBytesToBytes(str, base) {
var prefix = (base == 16 ? "\\x" : "\\");
return convertEscapedCodesToCodes(str, prefix, base, 8);
}
// "&amp;#12354;&amp;#12356;" => [ 0x3042, 0x3044 ]
// "&amp;#x3042;&amp;#x3044;" => [ 0x3042, 0x3044 ]
function convertNumRefToUnicodeCodePoints(str, base) {
var num_refs = str.split(";");
num_refs.pop(); // Trim the last element.
var unicode_codes = [];
for (var i = 0; i < num_refs.length; ++i) {
var decimal_str = num_refs[i].replace(/^&#x?/, "");
var unicode_code = parseInt(decimal_str, base);
unicode_codes.push(unicode_code);
}
return unicode_codes;
}
// [ 0x3042, 0x3044 ] => [ 0x3042, 0x3044 ]
// [ 0xD840, 0xDC0B ] => [ 0x2000B ] // A surrogate pair.
function convertUnicodeCodePointsToUtf16Codes(unicode_codes) {
var utf16_codes = [];
for (var i = 0; i < unicode_codes.length; ++i) {
var unicode_code = unicode_codes[i];
if (unicode_code < (1 << 16)) {
utf16_codes.push(unicode_code);
} else {
var first = ((unicode_code - (1 << 16)) / (1 << 10)) + 0xD800;
var second = (unicode_code % (1 << 10)) + 0xDC00;
utf16_codes.push(first)
utf16_codes.push(second)
}
}
return utf16_codes;
}
// 0x3042 => [ 0xE3, 0x81, 0x82 ]
function convertUnicodeCodePointToUtf8Bytes(unicode_code, base) {
var utf8_bytes = [];
if (unicode_code < 0x80) { // 1-byte
utf8_bytes.push(unicode_code);
} else if (unicode_code < (1 << 11)) { // 2-byte
utf8_bytes.push((unicode_code >>> 6) | 0xC0);
utf8_bytes.push((unicode_code & 0x3F) | 0x80);
} else if (unicode_code < (1 << 16)) { // 3-byte
utf8_bytes.push((unicode_code >>> 12) | 0xE0);
utf8_bytes.push(((unicode_code >> 6) & 0x3f) | 0x80);
utf8_bytes.push((unicode_code & 0x3F) | 0x80);
} else if (unicode_code < (1 << 21)) { // 4-byte
utf8_bytes.push((unicode_code >>> 18) | 0xF0);
utf8_bytes.push(((unicode_code >> 12) & 0x3F) | 0x80);
utf8_bytes.push(((unicode_code >> 6) & 0x3F) | 0x80);
utf8_bytes.push((unicode_code & 0x3F) | 0x80);
}
return utf8_bytes;
}
// [ 0x3042, 0x3044 ] => [ 0xE3, 0x81, 0x82, 0xE3, 0x81, 0x84 ]
function convertUnicodeCodePointsToUtf8Bytes(unicode_codes) {
var utf8_bytes = [];
for (var i = 0; i < unicode_codes.length; ++i) {
var bytes = convertUnicodeCodePointToUtf8Bytes(unicode_codes[i]);
utf8_bytes = utf8_bytes.concat(bytes);
}
return utf8_bytes;
}
// 0xff => "ff"
// 0xff => "377"
function formatNumber(number, base, num_digits) {
var str = number.toString(base).toUpperCase();
for (var i = str.length; i < num_digits; ++i) {
str = "0" + str;
}
return str;
}
var BASE64 =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
function encodeBase64Helper(data) {
var encoded = [];
if (data.length == 1) {
encoded.push(BASE64.charAt(data[0] >> 2));
encoded.push(BASE64.charAt(((data[0] & 3) << 4)));
encoded.push('=');
encoded.push('=');
} else if (data.length == 2) {
encoded.push(BASE64.charAt(data[0] >> 2));
encoded.push(BASE64.charAt(((data[0] & 3) << 4) |
(data[1] >> 4)));
encoded.push(BASE64.charAt(((data[1] & 0xF) << 2)));
encoded.push('=');
} else if (data.length == 3) {
encoded.push(BASE64.charAt(data[0] >> 2));
encoded.push(BASE64.charAt(((data[0] & 3) << 4) |
(data[1] >> 4)));
encoded.push(BASE64.charAt(((data[1] & 0xF) << 2) |
(data[2] >> 6)));
encoded.push(BASE64.charAt(data[2] & 0x3f));
}
return encoded.join('');
}
// "44GC44GE" => [ 0xE3, 0x81, 0x82, 0xE3, 0x81, 0x84 ]
function decodeBase64(encoded) {
var decoded_bytes = [];
var data_bytes = [];
for (var i = 0; i < encoded.length; i += 4) {
data_bytes.length = 0;
for (var j = i; j < i + 4; ++j) {
var letter = encoded.charAt(j);
if (letter == "=" || letter == "") {
break;
}
var data_byte = BASE64.indexOf(letter);
if (data_byte >= 64) { // Malformed base64 data.
break;
}
data_bytes.push(data_byte);
}
if (data_bytes.length == 1) {
// Malformed base64 data.
} else if (data_bytes.length == 2) { // 12-bit.
decoded_bytes.push((data_bytes[0] << 2) | (data_bytes[1] >> 4));
} else if (data_bytes.length == 3) { // 18-bit.
decoded_bytes.push((data_bytes[0] << 2) | (data_bytes[1] >> 4));
decoded_bytes.push(((data_bytes[1] & 0xF) << 4) | (data_bytes[2] >> 2));
} else if (data_bytes.length == 4) { // 24-bit.
decoded_bytes.push((data_bytes[0] << 2) | (data_bytes[1] >> 4));
decoded_bytes.push(((data_bytes[1] & 0xF) << 4) | (data_bytes[2] >> 2));
decoded_bytes.push(((data_bytes[2] & 0x3) << 6) | (data_bytes[3]));
}
}
return decoded_bytes;
}
// [ 0xE3, 0x81, 0x82, 0xE3, 0x81, 0x84 ] => "44GC44GE"
function encodeBase64(data_bytes) {
var encoded = '';
for (var i = 0; i < data_bytes.length; i += 3) {
var at_most_three_bytes = data_bytes.slice(i, i + 3);
encoded += encodeBase64Helper(at_most_three_bytes);
}
return encoded;
}
function decodeQuotedPrintableHelper(str, prefix) {
var decoded_bytes = [];
for (var i = 0; i < str.length;) {
if (str.charAt(i) == prefix) {
decoded_bytes.push(parseInt(str.substr(i + 1, 2), 16));
i += 3;
} else {
decoded_bytes.push(str.charCodeAt(i));
++i;
}
}
return decoded_bytes;
}
// "=E3=81=82=E3=81=84" => [ 0xE3, 0x81, 0x82, 0xE3, 0x81, 0x84 ]
function decodeQuotedPrintable(str) {
str = str.replace(/_/g, " ") // RFC 2047.
return decodeQuotedPrintableHelper(str, "=");
}
// "%E3%81%82%E3%81%84" => [ 0xE3, 0x81, 0x82, 0xE3, 0x81, 0x84 ]
function decodeUrl(str) {
return decodeQuotedPrintableHelper(str, "%");
}
function encodeQuotedPrintableHelper(data_bytes, prefix, should_escape) {
var encoded = '';
var prefix_code = prefix.charCodeAt(0);
for (var i = 0; i < data_bytes.length; ++i) {
var data_byte = data_bytes[i];
if (should_escape(data_byte)) {
encoded += prefix + formatNumber(data_bytes[i], 16, 2);
} else {
encoded += String.fromCharCode(data_byte);
}
}
return encoded;
}
// [ 0xE3, 0x81, 0x82, 0xE3, 0x81, 0x84 ] => "=E3=81=82=E3=81=84"
function encodeQuotedPrintable(data_bytes) {
var should_escape = function(b) {
return b < 32 || b > 126 || b == EQUAL_SIGN || b == QUESTION_MARK;
};
return encodeQuotedPrintableHelper(data_bytes, '=', should_escape);
}
var URL_SAFE =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-";
// [ 0xE3, 0x81, 0x82, 0xE3, 0x81, 0x84 ] => "%E3%81%82%E3%81%84"
function encodeUrl(data_bytes) {
var should_escape = function(b) {
return URL_SAFE.indexOf(String.fromCharCode(b)) == -1;
};
return encodeQuotedPrintableHelper(data_bytes, '%', should_escape);
}
// [ 0x3042, 0x3044 ] => "あい"
function convertUtf16CodesToString(utf16_codes) {
var unescaped = '';
for (var i = 0; i < utf16_codes.length; ++i) {
unescaped += String.fromCharCode(utf16_codes[i]);
}
return unescaped;
}
// [ 0x3042, 0x3044 ] => "あい"
function convertUnicodeCodePointsToString(unicode_codes) {
var utf16_codes = convertUnicodeCodePointsToUtf16Codes(unicode_codes);
return convertUtf16CodesToString(utf16_codes);
}
function maybeInitMaps(encoded_maps, to_unicode_map, from_unicode_map) {
if (to_unicode_map.is_initialized) {
return;
}
var data_types = [ 'ROUNDTRIP', 'INPUT_ONLY', 'OUTPUT_ONLY' ];
for (var i = 0; i < data_types.length; ++i) {
var data_type = data_types[i];
var encoded_data = encoded_maps[data_type];
var data_bytes = decodeBase64(encoded_data);
for (var j = 0; j < data_bytes.length; j += 4) {
var local_code = (data_bytes[j] << 8) | data_bytes[j + 1];
var unicode_code = (data_bytes[j + 2] << 8) | data_bytes[j + 3];
if (i == 0 || i == 1) { // ROUNDTRIP or INPUT_ONLY
to_unicode_map[local_code] = unicode_code;
}
if (i == 0 || i == 2) { // ROUNDTRIP or OUTPUT_ONLY
from_unicode_map[unicode_code] = local_code;
}
}
}
to_unicode_map.is_initialized = true;
}
var SJIS_TO_UNICODE = {}
var UNICODE_TO_SJIS = {}
// Requires: sjis_map.js should be loaded.
function maybeInitSjisMaps() {
maybeInitMaps(SJIS_MAP_ENCODED, SJIS_TO_UNICODE, UNICODE_TO_SJIS);
}
var ISO88591_TO_UNICODE = {}
var UNICODE_TO_ISO88591 = {}
// Requires: iso88591_map.js should be loaded.
function maybeInitIso88591Maps() {
maybeInitMaps(ISO88591_MAP_ENCODED, ISO88591_TO_UNICODE,
UNICODE_TO_ISO88591);
}
function lookupMapWithDefault(map, key, default_value) {
var value = map[key];
if (!value) {
value = default_value;
}
return value;
}
// [ 0x3042, 0x3044 ] => [ 0x82, 0xA0, 0x82, 0xA2 ]
function convertUnicodeCodePointsToSjisBytes(unicode_codes) {
maybeInitSjisMaps();
var sjis_bytes = [];
for (var i = 0; i < unicode_codes.length; ++i) {
var unicode_code = unicode_codes[i];
var sjis_code = lookupMapWithDefault(UNICODE_TO_SJIS,
unicode_code, QUESTION_MARK);
if (sjis_code <= 0xFF) { // 1-byte character.
sjis_bytes.push(sjis_code);
} else {
sjis_bytes.push(sjis_code >> 8);
sjis_bytes.push(sjis_code & 0xFF);
}
}
return sjis_bytes;
}
// [ 0x3042, 0x3044 ] => [ 0xA4, 0xA2, 0xA4, 0xA4 ]
function convertUnicodeCodePointsToEucJpBytes(unicode_codes) {
maybeInitSjisMaps();
var eucjp_bytes = [];
for (var i = 0; i < unicode_codes.length; ++i) {
var unicode_code = unicode_codes[i];
var sjis_code = lookupMapWithDefault(UNICODE_TO_SJIS, unicode_code,
QUESTION_MARK);
if (sjis_code > 0xFF) { // Double byte character.
var jis_code = convertSjisCodeToJisX208Code(sjis_code);
var eucjp_code = jis_code | 0x8080;
eucjp_bytes.push(eucjp_code >> 8);
eucjp_bytes.push(eucjp_code & 0xFF);
} else if (sjis_code >= 0x80) { // 8-bit character.
eucjp_bytes.push(0x8E);
eucjp_bytes.push(sjis_code);
} else { // 7-bit character.
eucjp_bytes.push(sjis_code);
}
}
return eucjp_bytes;
}
function convertUnicodeCodePointsToIso88591Bytes(unicode_codes) {
maybeInitIso88591Maps();
var latin_bytes = [];
for (var i = 0; i < unicode_codes.length; ++i) {
var unicode_code = unicode_codes[i];
var latin_code = lookupMapWithDefault(UNICODE_TO_ISO88591,
unicode_code, QUESTION_MARK);
latin_bytes.push(latin_code);
}
return latin_bytes;
}
// [ 0x82, 0xA0, 0x82, 0xA2 ] => [ 0x3042, 0x3044 ]
function convertSjisBytesToUnicodeCodePoints(sjis_bytes) {
maybeInitSjisMaps();
var unicode_codes = [];
for (var i = 0; i < sjis_bytes.length;) {
var sjis_code = -1;
var sjis_byte = sjis_bytes[i];
if ((sjis_byte >= 0x81 && sjis_byte <= 0x9F) ||
(sjis_byte >= 0xE0 && sjis_byte <= 0xFC)) {
++i;
var sjis_byte2 = sjis_bytes[i];
if ((sjis_byte2 >= 0x40 && sjis_byte2 <= 0x7E) ||
(sjis_byte2 >= 0x80 && sjis_byte2 <= 0xFC)) {
sjis_code = (sjis_byte << 8) | sjis_byte2;
++i;
}
} else {
sjis_code = sjis_byte;
++i;
}
var unicode_code = lookupMapWithDefault(SJIS_TO_UNICODE,
sjis_code, QUESTION_MARK);
unicode_codes.push(unicode_code);
}
return unicode_codes;
}
function convertIso88591BytesToUnicodeCodePoints(latin_bytes) {
maybeInitIso88591Maps();
var unicode_codes = [];
for (var i = 0; i < latin_bytes.length; ++i) {
var latin_code = latin_bytes[i];
var unicode_code = lookupMapWithDefault(ISO88591_TO_UNICODE,
latin_code, QUESTION_MARK);
unicode_codes.push(unicode_code);
}
return unicode_codes;
}
// 0x2422 => 0x82a0
function convertJisX208CodeToSjisCode(jis_code) {
var j1 = jis_code >> 8;
var j2 = jis_code & 0xFF;
// http://people.debian.org/~kubota/unicode-symbols-map2.html.ja
var s1 = ((j1 - 1) >> 1) + ((j1 <= 0x5E) ? 0x71 : 0xB1);
var s2 = j2 + ((j1 & 1) ? ((j2 < 0x60) ? 0x1F : 0x20) : 0x7E);
return (s1 << 8) | s2;
}
// 0x82a0 => 0x2422
function convertSjisCodeToJisX208Code(sjis_code) {
var s1 = sjis_code >> 8;
var s2 = sjis_code & 0xFF;
// http://people.debian.org/~kubota/unicode-symbols-map2.html.ja
var j1 = (s1 << 1) - (s1 <= 0x9f ? 0xe0 : 0x160) - (s2 < 0x9f ? 1 : 0);
var j2 = s2 - 0x1f - (s2 >= 0x7f ? 1 : 0) - (s2 >= 0x9f ? 0x5e : 0);
return (j1 << 8) | j2;
}
// [ 0x24, 0x22, 0x24, 0x24 ] => [ 0x82, 0xA0, 0x82, 0xA2 ]
function convertJisX208BytesToSjisBytes(jis_bytes) {
var sjis_bytes = [];
for (var i = 0; i < jis_bytes.length; i += 2) {
var jis_code = (jis_bytes[i] << 8) | jis_bytes[i + 1];
var sjis_code = convertJisX208CodeToSjisCode(jis_code);
sjis_bytes.push(sjis_code >> 8);
sjis_bytes.push(sjis_code & 0xFF);
}
return sjis_bytes;
}
// [ 0x82, 0xA0, 0x82, 0xA2 ] => [ 0x24, 0x22, 0x24, 0x24 ]
function convertSjisBytesToJisX208Bytes(sjis_bytes) {
var jis_bytes = [];
for (var i = 0; i < sjis_bytes.length; i += 2) {
var sjis_code = (sjis_bytes[i] << 8) | sjis_bytes[i + 1];
var jis_code = convertSjisCodeToJisX208Code(sjis_code);
jis_bytes.push(jis_code >> 8);
jis_bytes.push(jis_code & 0xFF);
}
return jis_bytes;
}
// Constants used in convertJisBytesToUnicodeCodePoints().
var ASCII = 0;
var JISX201 = 1;
var JISX208 = 2;
// Map used in convertIso2022JpBytesToUnicodeCodePoints().
var ESCAPE_SEQUENCE_TO_MODE = {
"(B": ASCII,
"(J": JISX201,
"$B": JISX208,
"$@": JISX208
};
// Map used in convertUnicodeCodePointsToIso2022JpBytes().
var MODE_TO_ESCAPE_SEQUENCE = {}
MODE_TO_ESCAPE_SEQUENCE[ASCII] = "(B";
MODE_TO_ESCAPE_SEQUENCE[JISX201] = "(J";
MODE_TO_ESCAPE_SEQUENCE[JISX208] = "$B";
// [ 0x1B, 0x24, 0x42, 0x24, 0x22, 0x1B, 0x28, 0x42, ] => [ 0x3042 ]
function convertIso2022JpBytesToUnicodeCodePoints(iso2022jp_bytes) {
maybeInitSjisMaps();
var flush = function(mode, data_bytes, output) {
var unicode_codes = [];
if (mode == ASCII) {
unicode_codes = data_bytes;
} else if (mode == JISX201) { // Might have half-width Katakana?
unicode_codes = convertSjisBytesToUnicodeCodePoints(data_bytes);
} else if (mode == JISX208) {
var sjis_bytes = convertJisX208BytesToSjisBytes(data_bytes);
unicode_codes = convertSjisBytesToUnicodeCodePoints(sjis_bytes);
} else { // Unknown mode
}
for (var i = 0; i < unicode_codes.length; ++i) {
output.push(unicode_codes[i]);
}
data_bytes.length = 0; // Clear.
}
var unicode_codes = [];
var mode = ASCII;
var current_data_bytes = [];
for (var i = 0; i < iso2022jp_bytes.length;) {
if (iso2022jp_bytes[i] == 0x1B) { // Mode is changed.
flush(mode, current_data_bytes, unicode_codes);
++i;
var code = String.fromCharCode(iso2022jp_bytes[i],
iso2022jp_bytes[i + 1]);
mode = ESCAPE_SEQUENCE_TO_MODE[code];
if (!mode) { // Unknown mode.
mode = ASCII;
}
i += 2;
} else {
current_data_bytes.push(iso2022jp_bytes[i]);
++i;
}
}
flush(mode, current_data_bytes, unicode_codes);
return unicode_codes;
}
// [ 0xA4, 0xA2, 0xA4, 0xA4 ] => [ 0x3042, 0x3044 ]
function convertEucJpBytesToUnicodeCodePoints(eucjp_bytes) {
maybeInitSjisMaps();
var unicode_codes = [];
for (var i = 0; i < eucjp_bytes.length;) {
if (eucjp_bytes[i] >= 0x80 && (i + 1) < eucjp_bytes.length &&
eucjp_bytes[i + 1] >= 0x80) {
var eucjp_code = (eucjp_bytes[i] << 8) | eucjp_bytes[i + 1];
var jis_code = eucjp_code & 0x7F7F;
var sjis_code = convertJisX208CodeToSjisCode(jis_code);
var unicode_code = lookupMapWithDefault(SJIS_TO_UNICODE,
sjis_code, QUESTION_MARK);
unicode_codes.push(unicode_code);
i += 2;
} else {
if (eucjp_bytes[i] < 0x80) {
unicode_codes.push(eucjp_bytes[i]);
} else {
// Ignore singleton 8-bit byte.
}
++i;
}
}
return unicode_codes;
}
// [ 0x3042 ] => [ 0x1B, 0x24, 0x42, 0x24, 0x22, 0x1B, 0x28, 0x42, ]
function convertUnicodeCodePointsToIso2022JpBytes(unicode_codes) {
maybeInitSjisMaps();
var mode = ASCII;
var maybeChangeMode = function(new_mode) {
if (mode != new_mode) {
mode = new_mode;
var esc_as_string = MODE_TO_ESCAPE_SEQUENCE[mode];
var esc_as_code_points = convertStringToUnicodeCodePoints(esc_as_string);
iso2022jp_bytes.push(0x1B); // ESC code.
iso2022jp_bytes = iso2022jp_bytes.concat(esc_as_code_points);
}
}
var iso2022jp_bytes = [];
for (var i = 0; i < unicode_codes.length; ++i) {
var unicode_code = unicode_codes[i];
var sjis_code = lookupMapWithDefault(UNICODE_TO_SJIS, unicode_code,
QUESTION_MARK);
if (sjis_code > 0xFF) { // Double byte character.
var jis_code = convertSjisCodeToJisX208Code(sjis_code);
maybeChangeMode(JISX208);
iso2022jp_bytes.push(jis_code >> 8);
iso2022jp_bytes.push(jis_code & 0xFF);
} else if (sjis_code >= 0x80) { // 8-bit character.
maybeChangeMode(JISX201);
iso2022jp_bytes.push(sjis_code);
} else { // 7-bit character.
maybeChangeMode(ASCII);
iso2022jp_bytes.push(sjis_code);
}
}
maybeChangeMode(ASCII);
return iso2022jp_bytes;
}
var MIME_FULL_MATCH = /^=\?([^?]+)\?([BQ])\?([^?]+)\?=$/;
var MIME_PARTIAL_MATCH = /^=\?([^?]+)\?([BQ])\?([^?]+)\?=/;
// "=?UTF-8?B?44GC?=" => true
// "foobar" => false
function isMimeEncodedString(str) {
return str.match(MIME_FULL_MATCH) != null;
}
// "=?UTF-8?B?44GC?=" => ["UTF-8", [0xE3, 0x81, 0x82]]
// "=?UTF-8?Q?=E3=81=82?=" => ["UTF-8", [0xE3, 0x81, 0x82]]
// "INVALID" => []
function decodeMime(str) {
var m = str.match(MIME_FULL_MATCH);
if (m) {
var char_encoding = m[1];
// We don't need the language information preceded by '*'.
char_encoding = char_encoding.replace(/\*.*$/, "")
var mime_encoding = m[2];
var mime_str = m[3];
var decoded_bytes;
if (mime_encoding == "B") {
decoded_bytes = decodeBase64(mime_str);
} else if (mime_encoding == "Q") {
decoded_bytes = decodeQuotedPrintable(mime_str);
}
if (char_encoding != "" && decoded_bytes) {
return [char_encoding, decoded_bytes]
}
}
return [];
}
var OUTPUT_CONVERTERS = {
'ISO2022JP': convertUnicodeCodePointsToIso2022JpBytes,
'ISO88591': convertUnicodeCodePointsToIso88591Bytes,
'SHIFTJIS': convertUnicodeCodePointsToSjisBytes,
'EUCJP': convertUnicodeCodePointsToEucJpBytes,
'UTF8': convertUnicodeCodePointsToUtf8Bytes
}
var INPUT_CONVERTERS = {
'ISO2022JP': convertIso2022JpBytesToUnicodeCodePoints,
'ISO88591': convertIso88591BytesToUnicodeCodePoints,
'SHIFTJIS': convertSjisBytesToUnicodeCodePoints,
'EUCJP': convertEucJpBytesToUnicodeCodePoints,
'UTF8': convertUtf8BytesToUnicodeCodePoints
}
function convertUnicodeCodePointsToBytes(unicode_codes, encoding) {
var normalized_encoding = normalizeEncodingName(encoding);
var convert_function = OUTPUT_CONVERTERS[normalized_encoding];
if (convert_function) {
return convert_function(unicode_codes);
}
return [];
}
function convertBytesToUnicodeCodePoints(data_bytes, encoding) {
var normalized_encoding = normalizeEncodingName(encoding);
var convert_function = INPUT_CONVERTERS[normalized_encoding];
if (convert_function) {
return convert_function(data_bytes);
}
return [];
}
// 'あい' => r'\u3042\u3044'
function escapeToUtf16(str) {
var escaped = ''
for (var i = 0; i < str.length; ++i) {
var hex = str.charCodeAt(i).toString(16).toUpperCase();
escaped += "\\u" + "0000".substr(hex.length) + hex;
}
return escaped;
}
// 'あい' => r'\U00003042\U00003044'
function escapeToUtf32(str) {
var escaped = ''
var unicode_codes = convertStringToUnicodeCodePoints(str);
for (var i = 0; i <unicode_codes.length; ++i) {
var hex = unicode_codes[i].toString(16).toUpperCase();
escaped += "\\U" + "00000000".substr(hex.length) + hex;
}
return escaped;
}
// "あい" => "&#12354;&#12356;"
// "あい" => "&#x3042;&#x3044;"
function escapeToNumRef(str, base) {
var unicode_codes = convertStringToUnicodeCodePoints(str);
var escaped = ''
var prefix = base == 10 ? '' : 'x';
for (var i = 0; i < unicode_codes.length; ++i) {
var code = unicode_codes[i].toString(base).toUpperCase();
var num_ref = "&#" + prefix + code + ";"
escaped += num_ref;
}
return escaped;
}
// "あい" => "l8je"
function escapeToPunyCode(str) {
var unicode_codes = convertStringToPunyCodes(str);
return convertUnicodeCodePointsToString(unicode_codes);
}
// [ 0xE3, 0x81, 0x82, 0xE3, 0x81, 0x84 ] => '\xE3\x81\x82\xE3\x81\x84'
// [ 0343, 0201, 0202, 0343, 0201, 0204 ] => '\343\201\202\343\201\204'
function convertBytesToEscapedString(data_bytes, base) {
var escaped = '';
for (var i = 0; i < data_bytes.length; ++i) {
var prefix = (base == 16 ? "\\x" : "\\");
var num_digits = base == 16 ? 2 : 3;
var escaped_byte = prefix + formatNumber(data_bytes[i], base, num_digits)
escaped += escaped_byte;
}
return escaped;
}
// "あい" => [0x6C, 0x38, 0x6A, 0x65] // "l8je"
// Requires: punycode.js should be loaded.
function convertStringToPunyCodes(str) {
var unicode_codes = convertStringToUnicodeCodePoints(str);
var puny_codes = [];
var result = "";
if (PunyCode.encode(unicode_codes, puny_codes)) {
return puny_codes;
}
return unicode_codes;
}
// [ 0x6C, 0x38, 0x6A, 0x65 ] => "あい"
// Requires: punycode.js should be loaded.
function convertPunyCodesToString(puny_codes) {
var unicode_codes = [];
if (PunyCode.decode(puny_codes, unicode_codes)) {
return convertUnicodeCodePointsToString(unicode_codes);
}
return convertUnicodeCodePointsToString(puny_codes);
}
// "あい" => r'\xE3\x81\x82\xE3\x81\x84' // UTF-8
// "あい" => r'\343\201\202\343\201\204' // UTF-8
function escapeToEscapedBytes(str, base, encoding) {
var unicode_codes = convertStringToUnicodeCodePoints(str);
var data_bytes = convertUnicodeCodePointsToBytes(unicode_codes, encoding);
return convertBytesToEscapedString(data_bytes, base);
}
// "あい" => "44GC44GE" // UTF-8
function escapeToBase64(str, encoding) {
var unicode_codes = convertStringToUnicodeCodePoints(str);
var data_bytes = convertUnicodeCodePointsToBytes(unicode_codes, encoding);
return encodeBase64(data_bytes);
}
// "あい" => "=E3=81=82=E3=81=84" // UTF-8
function escapeToQuotedPrintable(str, encoding) {
var unicode_codes = convertStringToUnicodeCodePoints(str);
var data_bytes = convertUnicodeCodePointsToBytes(unicode_codes, encoding);
return encodeQuotedPrintable(data_bytes);
}
// "あい" => "%E3%81%82%E3%81%84"
function escapeToUrl(str, encoding) {
var unicode_codes = convertStringToUnicodeCodePoints(str);
var data_bytes = convertUnicodeCodePointsToBytes(unicode_codes, encoding);
return encodeUrl(data_bytes);
}
// "あい" => "=?UTF-8?B?44GC44GE?="
// "あい" => "=?UTF-8?Q?=E3=81=82=E3=81=84?="
function escapeToMime(str, mime_encoding, char_encoding) {
var unicode_codes = convertStringToUnicodeCodePoints(str);
var data_bytes = convertUnicodeCodePointsToBytes(unicode_codes,
char_encoding);
if (str == "") {
return "";
}
var escaped = "=?" + char_encoding + "?";
if (mime_encoding == 'base64') {
escaped += "B?";
escaped += encodeBase64(data_bytes);
} else {
escaped += "Q?";
escaped += encodeQuotedPrintable(data_bytes);
}
escaped += '?=';
return escaped;
}
// r'\u3042\u3044 => "あい"
function unescapeFromUtf16(str) {
var utf16_codes = convertEscapedUtf16CodesToUtf16Codes(str);
return convertUtf16CodesToString(utf16_codes);
}
// r'\U00003042\U00003044 => "あい"
function unescapeFromUtf32(str) {
var unicode_codes = convertEscapedUtf32CodesToUnicodeCodePoints(str);
var utf16_codes = convertUnicodeCodePointsToUtf16Codes(unicode_codes);
return convertUtf16CodesToString(utf16_codes);
}
// r'\xE3\x81\x82\xE3\x81\x84' => "あい"
// r'\343\201\202\343\201\204' => "あい"
function unescapeFromEscapedBytes(str, base, encoding) {
var data_bytes = convertEscapedBytesToBytes(str, base);
var unicode_codes = convertBytesToUnicodeCodePoints(data_bytes, encoding);
return convertUnicodeCodePointsToString(unicode_codes);
}
// "&#12354;&#12356;" => "あい"
// "&#x3042;&#x3044;" => "あい"
function unescapeFromNumRef(str, base) {
var unicode_codes = convertNumRefToUnicodeCodePoints(str, base);
return convertUnicodeCodePointsToString(unicode_codes);
}
// "l8je" => "あい"
function unescapeFromPunyCode(str) {
var unicode_codes = convertStringToUnicodeCodePoints(str);
return convertPunyCodesToString(unicode_codes);
}
// "44GC44GE" => "あい"
function unescapeFromBase64(str, encoding) {
var decoded_bytes = decodeBase64(str);
var unicode_codes = convertBytesToUnicodeCodePoints(decoded_bytes, encoding);
return convertUnicodeCodePointsToString(unicode_codes);
}
// "=E3=81=82=E3=81=84" => "あい"
function unescapeFromQuotedPrintable(str, encoding) {
var decoded_bytes = decodeQuotedPrintable(str);
var unicode_bytes = convertBytesToUnicodeCodePoints(decoded_bytes, encoding);
return convertUnicodeCodePointsToString(unicode_bytes);
}
// "%E3%81%82%E3%81%84" => "あい"
function unescapeFromUrl(str, encoding) {
var decoded_bytes = decodeUrl(str);
var unicode_bytes = convertBytesToUnicodeCodePoints(decoded_bytes, encoding);
return convertUnicodeCodePointsToString(unicode_bytes);
}
// " " => true
// " \n" => true
function isEmptyOrSequenceOfWhiteSpaces(str) {
for (var i = 0; i < str.length; ++i) {
var code = str.charCodeAt(i);
if (!(code == 0x09 || // TAB
code == 0x0A || // LF
code == 0x0D || // CR
code == 0x20)) { // SPACE
return false;
}
}
return true;
}
// "=?UTF-8?B?*?= =?UTF-8?B?*?=" => ["=?UTF-8?B?*?=", "=?UTF-8?B?*?="]
// "=?UTF-8?B?*?=FOO" => ["=?UTF-8?B?*?=", "FOO"]
function splitMimeString(str) {
var parts = [];
var current = "";
while (str != "") {
var m = str.match(MIME_PARTIAL_MATCH)
if (m) {
if (!isEmptyOrSequenceOfWhiteSpaces(current)) {
parts.push(current);
}
current = "";
parts.push(m[0]);
str = str.substr(m[0].length);
} else {
current += str.charAt(0);
str = str.substr(1);
}
}
if (!isEmptyOrSequenceOfWhiteSpaces(current)) {
parts.push(current);
}
return parts;
}
// "UTF-8" => "UTF8"
// "Shift_JIS" => "SHIFTJIS"
function normalizeEncodingName(encoding) {
return encoding.toUpperCase().replace(/[_-]/g, "");
}
// "=?UTF-8?B?44GC44GE?=" => "あい"
// "=?Shift_JIS?B?gqCCog==?=" => "あい"
// "=?ISO-2022-JP?B?GyRCJCIkJBsoQg==?=" => "あい"
// "=?UTF-8?Q?=E3=81=82=E3=81=84?=" => "あい"
// "=?Shift_JIS?Q?=82=A0=82=A2?=" => "あい"
// "=?ISO-2022-JP?Q?=1B$B$"$$=1B(B?=" => "あい"
function unescapeFromMime(str) {
var parts = splitMimeString(str);
var unescaped = "";
for (var i = 0; i < parts.length; ++i) {
if (isMimeEncodedString(parts[i])) {
var pair = decodeMime(parts[i]);
if (pair.length == 0) { // Malformed MIME string. Skip it.
continue;
}
var encoding = normalizeEncodingName(pair[0]);
var data_bytes = pair[1];
var unicode_codes = convertBytesToUnicodeCodePoints(data_bytes,
encoding);
unescaped += convertUnicodeCodePointsToString(unicode_codes);
} else {
unescaped += parts[i];
}
}
return unescaped;
}

View file

@ -1,244 +0,0 @@
<style>
.messages {
padding-top: 51px;
height: 350px;
border-bottom: 1px solid #999;
overflow: hidden;
}
#messages-container {
height: 100%;
}
.preview #headers {
border-bottom: 1px solid #DDDDDD;
}
.selected {
background: #0066CC;
color: #fff;
}
table td, table th {
padding: 2px 4px 2px 8px !important;
}
#messages-container table tbody {
overflow: auto;
height: 260px;
}
#messages-container table tbody td {
overflow-x: hidden;
white-space: nowrap;
}
table thead {
background: #eee;
}
#messages-container > table > thead > tr, #messages-container > table > tbody {
display: block;
}
table th:not(:last-child) {
border-right: 2px solid #DDD;
}
table#headers {
margin-bottom: 1px;
background: #eee;
}
#content .nav>li>a {
padding: 5px 8px;
}
.preview #headers tbody td {
width: 100%;
}
.preview #headers tbody th {
white-space: nowrap;
padding-right: 10px !important;
padding-left: 10px !important;
text-align: right;
color: #666;
font-weight: normal;
}
#preview-plain, #preview-source {
white-space: pre;
font-family: Courier New, Courier, System, fixed-width;
}
.preview .tab-content {
padding: 0;
overflow-y: scroll;
}
.mime-part {
padding: 10px;
}
.ui-resizable-handle.ui-resizable-s {
background: #aaa;
border-bottom: 1px solid #ccc;
}
</style>
<script>
var columns = [15,15,40,20,10];
var reflow = function() {
var remaining = 0;
if($('.preview').length > 0) {
remaining = $(window).height() - $('.preview .nav-tabs').offset().top
} else {
remaining = $('#messages-container').offset().top + $('#messages-container').height();
}
remaining -= $('.resize_bar').height();
$('.preview .tab-content').height(remaining - 32)
$('#messages-container table').height($('#messages-container').height())
$('#messages-container table tbody').height($('#messages-container').height() - $('#messages-container table thead').height())
var $table = $('#messages-container table');
var colWidth = [];
for(var i in columns) {
colWidth[i] = $table.innerWidth() / 100 * columns[i];
}
$table.find('thead tr').children().each(function(i, v) { $(v).width(colWidth[i]); });
$table.find('tbody tr:first').children().each(function(i, v) { $(v).width(colWidth[i]); });
}
$(function() {
$(".messages").resizable({"handles":"s"});
reflow();
$(window).resize(function() {
reflow();
}).resize();
})
</script>
<div class="modal fade" id="confirm-delete-all">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Delete all messages?</h4>
</div>
<div class="modal-body">
<p>Are you sure you want to delete all messages?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" ng-click="deleteAllConfirm()">Delete all messages</button>
</div>
</div>
</div>
</div>
<div class="modal fade" id="release-one">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title">Release message</h4>
</div>
<div class="modal-body">
<p>To release this message, enter a recipient and SMTP server address:</p>
<form role="form">
<div class="form-group">
<label for="release-message-email">Email address</label>
<input type="email" autofocus class="form-control" id="release-message-email" placeholder="someone@example.com">
</div>
<div class="form-group">
<label for="release-message-smtp-host">SMTP server</label>
<input type="text" class="form-control" id="release-message-smtp-host" placeholder="mail.example.com">
</div>
<div class="form-group">
<label for="release-message-smtp-port">SMTP port</label>
<input type="number" class="form-control" id="release-message-smtp-port" value="25">
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger" ng-click="confirmReleaseMessage()">Release message</button>
</div>
</div>
</div>
</div>
<div class="messages">
<div id="messages-container">
<table class="table">
<thead>
<tr>
<th>From</th>
<th>To</th>
<th>Subject</th>
<th>Received</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="message in messages | filter:searchText" ng-click="selectMessage(message)" ng-class="{ selected: message.ID == preview.ID }">
<td>
{{ message.From.Mailbox }}@{{ message.From.Domain }}
</td>
<td>
<span ng-repeat="to in message.To">
{{ to.Mailbox }}@{{ to.Domain }}
</span>
</td>
<td>
{{ tryDecodeMime(message.Content.Headers["Subject"][0]) }}
</td>
<td>
{{ date(message.Created) }}
</td>
<td>
<button class="btn btn-xs btn-default" title="Delete" ng-click="deleteOne(message)"><span class="glyphicon glyphicon-remove"></span></button>
<a class="btn btn-xs btn-default" title="Download" href="/api/v1/messages/{{ message.Id }}/download"><span class="glyphicon glyphicon-save"></span></a>
<button class="btn btn-xs btn-default" title="Release" ng-click="releaseOne(message)"><span class="glyphicon glyphicon-share"></span></button>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="preview" ng-if="preview">
<table class="table" id="headers">
<tbody ng-if="!previewAllHeaders">
<tr>
<th>From</th>
<td>{{ tryDecodeMime(preview.Content.Headers["From"][0]) }}</td>
</tr>
<tr>
<th>Subject</th>
<td><strong>{{ tryDecodeMime(preview.Content.Headers["Subject"][0]) }}</strong></td>
</tr>
<tr>
<th>To</th>
<td>
<button id="show-headers" ng-click="toggleHeaders(true)" type="button" class="btn btn-default pull-right btn-xs">Show headers <span class="glyphicon glyphicon-chevron-down"></span></button>
{{ tryDecodeMime(preview.Content.Headers["To"].join(', ')) }}
</td>
</tr>
</tbody>
<tbody ng-if="previewAllHeaders">
<tr ng-repeat="(header, value) in preview.Content.Headers">
<th>
{{ tryDecodeMime(header) }}
</th>
<td>
<button id="hide-headers" ng-if="$last" ng-click="toggleHeaders(false)" type="button" class="btn btn-default pull-right btn-xs">Hide headers <span class="glyphicon glyphicon-chevron-up"></span></button>
<div ng-repeat="v in value">{{ v }}</div>
</td>
</tr>
</tbody>
</table>
<div id="content">
<ul class="nav nav-tabs">
<li ng-if="hasHTML(preview)" ng-class="{ active: hasHTML(preview) }"><a href="#preview-html" data-toggle="tab">HTML</a></li>
<li ng-class="{ active: !hasHTML(preview) }"><a href="#preview-plain" data-toggle="tab">Plain text</a></li>
<li><a href="#preview-source" data-toggle="tab">Source</a></li>
<li ng-if="preview.MIME"><a href="#preview-mime" data-toggle="tab">MIME</a></li>
</ul>
<div class="tab-content">
<div ng-if="hasHTML(preview)" ng-class="{ active: hasHTML(preview) }" class="tab-pane" id="preview-html" ng-bind-html="preview.previewHTML"></div>
<div class="tab-pane" ng-class="{ active: !hasHTML(preview) }" id="preview-plain">{{ getMessagePlain(preview) }}</div>
<div class="tab-pane" id="preview-source">{{ getSource(preview) }}</div>
<div class="tab-pane" id="preview-mime">
<div ng-repeat="part in preview.MIME.Parts" class="mime-part">
<a href="/api/v1/messages/{{ preview.Id }}/mime/part/{{ $index }}/download" type="button" class="btn btn-default btn-sm">
<span class="glyphicon glyphicon-save"></span>
Download
</a>
{{ part.Headers["Content-Type"][0] || "Unknown type" }} ({{ part.Size }} bytes)
</div>
</div>
</div>
</div>
</div>

View file

@ -1,146 +0,0 @@
<!DOCTYPE html>
<html ng-app="mailhogApp">
<head>
<title>MailHog</title>
<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
<script src="//code.jquery.com/ui/1.10.4/jquery-ui.min.js"></script>
<link rel="stylesheet" href="//code.jquery.com/ui/1.10.4/themes/smoothness/jquery-ui.css">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
<link rel="stylesheet" href="//netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap-theme.min.css">
<script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.2.15/angular.js"></script>
<script src="/js/strutil.js"></script>
<script src="/js/controllers.js"></script>
<style>
body, html { height: 100%; overflow: hidden; }
.navbar {
margin-bottom: 0;
position: absolute;
top: 0;
right: 0;
width: 100%;
border-bottom: 1px solid #ccc;
}
.navbar-header img {
height: 35px;
margin: 8px 0 0 5px;
float: left;
}
.navbar-nav.navbar-right:last-child {
margin-right: 0; /* bootstrap fix?! */
}
.ajax-loader {
background: url('/images/ajax-loader.gif');
width: 16px;
height: 16px;
display: inline-block;
}
.ajax-event {
padding: 5px;
width: 225px;
}
.ajax-event h1 {
font-size: 1em;
padding: 2px;
margin: 0;
padding-left: 20px;
}
.ajax-event h2 {
font-size: 0.8em;
padding: 2px;
margin: 0;
padding-left: 20px;
}
.ajax-event .glyphicon {
float: left;
padding: 1px;
color: #666;
}
.dropdown-menu .glyphicon {
padding: 1px;
margin-right: 5px;
color: #666;
}
.dropdown-menu > li > a {
padding: 3px 10px;
}
/* http://stackoverflow.com/questions/18838964/add-bootstrap-glyphicon-to-input-box */
.left-inner-addon {
position: relative;
}
.left-inner-addon input {
padding-left: 30px;
}
.left-inner-addon i {
color: #aaa;
position: absolute;
padding: 9px 10px;
pointer-events: none;
}
.ev_good {
color: green;
}
.ev_bad {
color: red;
}
</style>
</head>
<body ng-controller="MailCtrl">
<nav class="navbar navbar-default navbar-static-top" role="navigation">
<div class="navbar-header">
<img src="/images/hog.png">
<a class="navbar-brand" href="#">MailHog</a>
</div>
<ul class="nav navbar-nav navbar-left navbar-subtext">
<li>
<a>[: .config.Hostname :]</a>
</li>
<li>
<a>{{ messagesDisplayed() }} {{ searchText ? "of " + messages.length : "" }} messages</a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li ng-if="eventCount > 0" class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">
<span ng-if="eventCount > eventDone" class="ajax-loader"></span>
{{ eventCount - eventDone }} pending<span ng-if="eventFailed > 0"> ({{ eventFailed}} failed)</span> <span ng-if="eventDone > 0"> ({{ eventDone}} complete)</span> <b class="caret"></b>
</a>
<ul class="dropdown-menu">
<li ng-class="e.getClass()" ng-repeat="(id, e) in eventsPending" class="ajax-event">
<span class="glyphicon {{ e.glyphicon }}"></span>
<button ng-if="e.failed" ng-click="e.remove()" class="btn btn-xs btn-danger pull-right"><span class="glyphicon glyphicon-remove"></span></button>
<h1>{{ e.name }}</h1>
<h2 ng-if="e.args">{{ e.args }}</h2>
<h2 ng-if="e.error">{{ e.error }}</h2>
</li>
</ul>
</li>
<li>
<a href="#" class="dropdown-toggle" data-toggle="dropdown" title="Event stream connected" ng-click="toggleStream()">
<span class="glyphicon glyphicon-asterisk {{ hasEventSource ? 'ev_good' : 'ev_bad' }}"></span>
</a>
</li>
<li>
<form class="navbar-form navbar-left" role="search">
<div class="form-group left-inner-addon">
<i class="glyphicon glyphicon-search"></i>
<input ng-model="searchText" type="text" class="form-control" placeholder="Search">
</div>
</form>
</li>
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Options <b class="caret"></b></a>
<ul class="dropdown-menu">
<li><a href="#" ng-click="refresh()"><span class="glyphicon glyphicon-download"></span> Refresh</a></li>
<li class="divider"></li>
<li><a href="#" ng-click="deleteAll()"><span class="glyphicon glyphicon-remove-circle"></span> Delete all messages</a></li>
</ul>
</li>
<li><a target="_blank" href="https://github.com/ian-kent/Go-MailHog"><img src="/images/github.png" style="width: 16px;" /> GitHub</a></li>
</ul>
</nav>
[: .Content :]
</body>
</html>

View file

@ -1,42 +0,0 @@
package main
import (
"flag"
"os"
"github.com/ian-kent/Go-MailHog/MailHog-Server/config"
"github.com/ian-kent/Go-MailHog/MailHog-UI/assets"
"github.com/ian-kent/Go-MailHog/MailHog-UI/web"
"github.com/ian-kent/Go-MailHog/http"
"github.com/ian-kent/go-log/log"
gotcha "github.com/ian-kent/gotcha/app"
)
var conf *config.Config
var exitCh chan int
func configure() {
config.RegisterFlags()
flag.Parse()
conf = config.Configure()
}
func main() {
configure()
// FIXME need to make API URL configurable
exitCh = make(chan int)
cb := func(app *gotcha.App) {
web.CreateWeb(conf, app)
}
go http.Listen(conf, assets.Asset, exitCh, cb)
for {
select {
case <-exitCh:
log.Printf("Received exit signal")
os.Exit(0)
}
}
}

View file

@ -1,44 +0,0 @@
package web
import (
"html/template"
"github.com/ian-kent/Go-MailHog/MailHog-Server/config"
gotcha "github.com/ian-kent/gotcha/app"
"github.com/ian-kent/gotcha/events"
"github.com/ian-kent/gotcha/http"
)
type Web struct {
config *config.Config
app *gotcha.App
}
func CreateWeb(cfg *config.Config, app *gotcha.App) *Web {
app.On(events.BeforeHandler, func(session *http.Session, next func()) {
session.Stash["config"] = cfg
next()
})
r := app.Router
r.Get("/images/(?P<file>.*)", r.Static("assets/images/{{file}}"))
r.Get("/js/(?P<file>.*)", r.Static("assets/js/{{file}}"))
r.Get("/", Index)
app.Config.LeftDelim = "[:"
app.Config.RightDelim = ":]"
return &Web{
config: cfg,
app: app,
}
}
func Index(session *http.Session) {
html, _ := session.RenderTemplate("index.html")
session.Stash["Page"] = "Browse"
session.Stash["Content"] = template.HTML(html)
session.Render("layout.html")
}

View file

@ -4,14 +4,14 @@ import (
"flag"
"os"
"github.com/ian-kent/Go-MailHog/MailHog-Server/api"
"github.com/ian-kent/Go-MailHog/MailHog-Server/config"
"github.com/ian-kent/Go-MailHog/MailHog-Server/smtp"
"github.com/ian-kent/Go-MailHog/MailHog-UI/assets"
"github.com/ian-kent/Go-MailHog/MailHog-UI/web"
"github.com/ian-kent/Go-MailHog/http"
"github.com/ian-kent/go-log/log"
gotcha "github.com/ian-kent/gotcha/app"
"github.com/mailhog/MailHog-Server/api"
"github.com/mailhog/MailHog-Server/config"
"github.com/mailhog/MailHog-Server/smtp"
"github.com/mailhog/MailHog-UI/assets"
"github.com/mailhog/MailHog-UI/web"
)
var conf *config.Config

View file

@ -1,19 +1,10 @@
DEPS = $(go list -f '{{range .TestImports}}{{.}} {{end}}' ./...)
all: deps bindata fmt combined
all: deps fmt combined
combined:
go install ./MailHog
server:
go install ./MailHog-Server
ui:
go install ./MailHog-UI
bindata:
go-bindata -o MailHog-UI/assets/assets.go -pkg assets -prefix MailHog-UI/ MailHog-UI/assets/...
release: release-deps
gox -output="build/{{.Dir}}_{{.OS}}_{{.Arch}}" ./MailHog
@ -21,6 +12,8 @@ fmt:
go fmt ./...
deps:
go get github.com/mailhog/MailHog-Server
go get github.com/mailhog/MailHog-UI
go get github.com/ian-kent/gotcha/gotcha
go get github.com/ian-kent/go-log/log
go get github.com/ian-kent/envconf
@ -38,4 +31,4 @@ test-deps:
release-deps:
go get github.com/mitchellh/gox
.PNONY: all combined server ui bindata release fmt test-deps release-deps
.PNONY: all combined release fmt deps test-deps release-deps

View file

@ -1,9 +1,9 @@
package http
import (
"github.com/ian-kent/Go-MailHog/MailHog-Server/config"
"github.com/ian-kent/go-log/log"
gotcha "github.com/ian-kent/gotcha/app"
"github.com/mailhog/MailHog-Server/config"
)
func Listen(cfg *config.Config, Asset func(string) ([]byte, error), exitCh chan int, registerCallback func(*gotcha.App)) {