@@ -0,0 +1,5 @@ | |||||
*~ | |||||
*.swp | |||||
*.log | |||||
run.sh | |||||
burner |
@@ -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)) | |||||
} |
@@ -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 | |||||
} |
@@ -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) | |||||
} |
@@ -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 | |||||
} | |||||
} |
@@ -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 | |||||
} |