mirror of
https://gitlab.com/ric_harvey/MailHog.git
synced 2024-11-27 16:24:04 +00:00
Refactor to github.com/mailhog/{MailHog-Server,MailHog-UI}
This commit is contained in:
parent
09c9701511
commit
ebbae16589
21 changed files with 10 additions and 2620 deletions
|
@ -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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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()
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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
|
|
||||||
}
|
|
|
@ -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))
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
})
|
|
||||||
}
|
|
|
@ -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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
1
MailHog-UI/assets/.gitignore
vendored
1
MailHog-UI/assets/.gitignore
vendored
|
@ -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 |
|
@ -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();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
|
@ -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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// "&#12354;&#12356;" => [ 0x3042, 0x3044 ]
|
|
||||||
// "&#x3042;&#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;
|
|
||||||
}
|
|
||||||
|
|
||||||
// "あい" => "あい"
|
|
||||||
// "あい" => "あい"
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// "あい" => "あい"
|
|
||||||
// "あい" => "あい"
|
|
||||||
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;
|
|
||||||
}
|
|
|
@ -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">×</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">×</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>
|
|
|
@ -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>
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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")
|
|
||||||
}
|
|
|
@ -4,14 +4,14 @@ import (
|
||||||
"flag"
|
"flag"
|
||||||
"os"
|
"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-MailHog/http"
|
||||||
"github.com/ian-kent/go-log/log"
|
"github.com/ian-kent/go-log/log"
|
||||||
gotcha "github.com/ian-kent/gotcha/app"
|
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
|
var conf *config.Config
|
||||||
|
|
15
Makefile
15
Makefile
|
@ -1,19 +1,10 @@
|
||||||
DEPS = $(go list -f '{{range .TestImports}}{{.}} {{end}}' ./...)
|
DEPS = $(go list -f '{{range .TestImports}}{{.}} {{end}}' ./...)
|
||||||
|
|
||||||
all: deps bindata fmt combined
|
all: deps fmt combined
|
||||||
|
|
||||||
combined:
|
combined:
|
||||||
go install ./MailHog
|
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
|
release: release-deps
|
||||||
gox -output="build/{{.Dir}}_{{.OS}}_{{.Arch}}" ./MailHog
|
gox -output="build/{{.Dir}}_{{.OS}}_{{.Arch}}" ./MailHog
|
||||||
|
|
||||||
|
@ -21,6 +12,8 @@ fmt:
|
||||||
go fmt ./...
|
go fmt ./...
|
||||||
|
|
||||||
deps:
|
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/gotcha/gotcha
|
||||||
go get github.com/ian-kent/go-log/log
|
go get github.com/ian-kent/go-log/log
|
||||||
go get github.com/ian-kent/envconf
|
go get github.com/ian-kent/envconf
|
||||||
|
@ -38,4 +31,4 @@ test-deps:
|
||||||
release-deps:
|
release-deps:
|
||||||
go get github.com/mitchellh/gox
|
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
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package http
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/ian-kent/Go-MailHog/MailHog-Server/config"
|
|
||||||
"github.com/ian-kent/go-log/log"
|
"github.com/ian-kent/go-log/log"
|
||||||
gotcha "github.com/ian-kent/gotcha/app"
|
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)) {
|
func Listen(cfg *config.Config, Asset func(string) ([]byte, error), exitCh chan int, registerCallback func(*gotcha.App)) {
|
||||||
|
|
Loading…
Reference in a new issue