commit 8795da59d0da9e206ec7898e52fc6099a4993663 Author: Matt Baer Date: Tue Feb 2 13:43:57 2016 -0500 Add basic mail / auth server diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..905ce86 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +*~ +*.swp +*.log +run.sh +burner diff --git a/auth/server.go b/auth/server.go new file mode 100644 index 0000000..41c7924 --- /dev/null +++ b/auth/server.go @@ -0,0 +1,66 @@ +package auth + +import ( + "errors" + "fmt" + "log" + "net/http" + "os" + "regexp" + + "github.com/thebaer/burner/validate" +) + +// Serve starts an HTTP server that handles auth requests from nginx. +func Serve(port int) error { + if port <= 0 { + return errors.New("auth server: Invalid port number.") + } + serverPort = port + + mailInfo := log.New(os.Stdout, "", log.Ldate|log.Ltime) + mailInfo.Printf("Starting mail auth server on :%d", serverPort) + + http.HandleFunc("/auth", authHandler) + http.ListenAndServe(fmt.Sprintf("127.0.0.1:%d", serverPort), nil) + + return nil +} + +var ( + // Port that the auth server will run on. + serverPort int + + // Regular expression for matching / finding a valid To address. + smtpEmailReg = regexp.MustCompile("<(.+)>") +) + +// authHandler works with nginx to determine whether or not a receipient email +// address is valid. If it is, running mail server's information is passed +// back. +func authHandler(w http.ResponseWriter, r *http.Request) { + toHeader := r.Header.Get("Auth-SMTP-To") + if toHeader == "" { + w.Header().Set("Auth-Status", "Unrecognized receipient.") + w.Header().Set("Auth-Error-Code", "550") + return + } + + to := smtpEmailReg.FindStringSubmatch(toHeader)[1] + if to == "" { + w.Header().Set("Auth-Status", "Unrecognized receipient.") + w.Header().Set("Auth-Error-Code", "550") + return + } + if err := validate.Email(to); err != nil { + // Email address validation failed + w.Header().Set("Auth-Status", err.Error()) + w.Header().Set("Auth-Error-Code", "550") + return + } + + // Email passed validation, send back mail server information + w.Header().Set("Auth-Status", "OK") + w.Header().Set("Auth-Server", "127.0.0.1") + w.Header().Set("Auth-Port", fmt.Sprintf("%d", serverPort)) +} diff --git a/database/database.go b/database/database.go new file mode 100644 index 0000000..2568219 --- /dev/null +++ b/database/database.go @@ -0,0 +1,45 @@ +package database + +import ( + "database/sql" + "errors" + "fmt" + "os" + + _ "github.com/go-sql-driver/mysql" +) + +type ( + DB struct { + *sql.DB + } + Tx struct { + *sql.Tx + } +) + +func Open() (*DB, error) { + // Get database configuration + dbUser := os.Getenv("DB_USER") + dbPassword := os.Getenv("DB_PASSWORD") + dbName := os.Getenv("DB_DB") + dbHost := os.Getenv("DB_HOST") + + if dbUser == "" || dbPassword == "" { + return nil, errors.New("Database user or password not set.") + } + if dbHost == "" { + dbHost = "localhost" + } + if dbName == "" { + dbName = "burnermail" + } + + db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@tcp(%s:3306)/%s?charset=utf8mb4&parseTime=true", dbUser, dbPassword, dbHost, dbName)) + if err != nil { + return nil, err + } + db.SetMaxOpenConns(250) + + return &DB{db}, nil +} diff --git a/mail.go b/mail.go new file mode 100644 index 0000000..69bf847 --- /dev/null +++ b/mail.go @@ -0,0 +1,34 @@ +package main + +import ( + "flag" + "log" + + "github.com/thebaer/burner/auth" + "github.com/thebaer/burner/database" + "github.com/thebaer/burner/mail" +) + +var host = flag.String("h", "example.com", "Domain this service lives on.") + +func main() { + // Parse configuration flags and validate + flag.Parse() + if *host == "example.com" { + log.Printf("WARNING: Default hostname (example.com) unchanged. Use -h flag to set correct host.") + } + + // Connect to database + db, err := database.Open() + if err != nil { + panic(err) + } + defer db.Close() + + // TODO: make port numbers configurable + go func() { + auth.Serve(8080) + }() + + mail.Serve(*host, 2525) +} diff --git a/mail/mail.go b/mail/mail.go new file mode 100644 index 0000000..0842479 --- /dev/null +++ b/mail/mail.go @@ -0,0 +1,106 @@ +package mail + +import ( + "bytes" + "fmt" + "io" + "io/ioutil" + "log" + "mime" + "mime/multipart" + "net" + "net/mail" + "os" + "strings" + + "github.com/mhale/smtpd" + "github.com/thebaer/burner/validate" +) + +var ( + mailInfo *log.Logger + mailError *log.Logger +) + +type MailConfig struct { + Port int + Host string +} + +var mailCfg *MailConfig + +func Serve(host string, port int) error { + mailCfg = &MailConfig{ + Port: port, + Host: host, + } + + // Set up variables + mailInfo = log.New(os.Stdout, "", log.Ldate|log.Ltime) + mailError = log.New(os.Stderr, "ERROR: ", log.Ldate|log.Ltime|log.Lshortfile) + + // Start mail server + mailInfo.Printf("Starting %s mail server on :%d", mailCfg.Host, mailCfg.Port) + err := smtpd.ListenAndServe(fmt.Sprintf(":%d", mailCfg.Port), mailHandler, mailCfg.Host, mailCfg.Host) + if err != nil { + mailError.Printf("Couldn't start mail server: %v", err) + return err + } + + return nil +} + +func mailHandler(origin net.Addr, from string, to []string, data []byte) { + msg, err := mail.ReadMessage(bytes.NewReader(data)) + if err != nil { + mailError.Printf("Couldn't read email message: %v", err) + return + } + + var content []byte + subject := msg.Header.Get("Subject") + contentType, params, err := mime.ParseMediaType(msg.Header.Get("Content-Type")) + if err != nil { + mailError.Printf("Couldn't parse Content-Type: %v", err) + } + if strings.HasPrefix(contentType, "multipart/") { + mr := multipart.NewReader(msg.Body, params["boundary"]) + for { + p, err := mr.NextPart() + if err == io.EOF { + break + } + if err != nil { + mailError.Printf("Error getting NextPart(): %v", err) + continue + } + if !strings.HasPrefix(p.Header.Get("Content-Type"), "text/plain") { + continue + } + + // We correctly read the text/plain section of email + content, err = ioutil.ReadAll(p) + if err != nil { + mailError.Printf("Error in ReadAll on part: %v", err) + } + break + } + } else { + var err error + content, err = ioutil.ReadAll(msg.Body) + if err != nil { + mailError.Printf("Couldn't read email body: %v", err) + return + } + } + + mailInfo.Printf("Received mail from %s for %s with subject %s", from, to[0], subject) + + createPostFromEmail(to[0], subject, from, content) +} + +func createPostFromEmail(to, subject, from string, content []byte) { + if err := validate.Email(to); err != nil { + return + } +} diff --git a/validate/mail.go b/validate/mail.go new file mode 100644 index 0000000..ed3d58c --- /dev/null +++ b/validate/mail.go @@ -0,0 +1,33 @@ +package validate + +import ( + "errors" + "strings" +) + +// Friendly user-facing errors +var ( + errBadEmail = errors.New("User doesn't exist.") +) + +// Email does basic validation on the intended receiving address. It returns a +// friendly error message that can be served directly to connecting clients if +// validation fails. +func Email(to string) error { + host := to[strings.IndexRune(to, '@')+1:] + // TODO: use given configurable host (don't hardcode) + if host != "writ.es" { + return errBadEmail + } + + toName := to[:strings.IndexRune(to, '@')] + if toName == "anyone" { + return nil + } + + if len(toName) != 32 { + return errBadEmail + } + + return nil +}