From b99cae7a2e217f0cdcacb8fb941c9a9f7dc72e71 Mon Sep 17 00:00:00 2001 From: "Stephen McQuay (smcquay)" Date: Thu, 26 May 2016 21:55:45 -0700 Subject: [PATCH] add support for sending emails. Change-Id: Iaff151dc1711b9b3ea9353070baa9202b1cc2a8c --- api_test.go | 20 +++++++------- cmd/vaind/main.go | 18 +++++++++++-- mail.go | 54 ++++++++++++++++++++++++++++++++++++++ server.go | 66 ++++++++++++++++++++++++++++++++--------------- 4 files changed, 125 insertions(+), 33 deletions(-) create mode 100644 mail.go diff --git a/api_test.go b/api_test.go index 31cf3e6..f25b8ac 100644 --- a/api_test.go +++ b/api_test.go @@ -23,7 +23,7 @@ func TestAdd(t *testing.T) { defer done() sm := http.NewServeMux() - NewServer(sm, db, "", window) + NewServer(sm, db, nil, "", window, false) ts := httptest.NewServer(sm) tok, err := db.addUser("sm@example.org") if err != nil { @@ -169,7 +169,7 @@ func TestInvalidPath(t *testing.T) { defer done() sm := http.NewServeMux() - NewServer(sm, db, "", window) + NewServer(sm, db, nil, "", window, false) ts := httptest.NewServer(sm) tok, err := db.addUser("sm@example.org") if err != nil { @@ -201,7 +201,7 @@ func TestCannotDuplicateExistingPath(t *testing.T) { defer done() sm := http.NewServeMux() - NewServer(sm, db, "", window) + NewServer(sm, db, nil, "", window, false) ts := httptest.NewServer(sm) tok, err := db.addUser("sm@example.org") @@ -247,7 +247,7 @@ func TestCannotAddExistingSubPath(t *testing.T) { defer done() sm := http.NewServeMux() - NewServer(sm, db, "", window) + NewServer(sm, db, nil, "", window, false) ts := httptest.NewServer(sm) tok, err := db.addUser("sm@example.org") @@ -295,7 +295,7 @@ func TestMissingRepo(t *testing.T) { defer done() sm := http.NewServeMux() - NewServer(sm, db, "", window) + NewServer(sm, db, nil, "", window, false) ts := httptest.NewServer(sm) tok, err := db.addUser("sm@example.org") @@ -328,7 +328,7 @@ func TestBadJson(t *testing.T) { defer done() sm := http.NewServeMux() - NewServer(sm, db, "", window) + NewServer(sm, db, nil, "", window, false) ts := httptest.NewServer(sm) tok, err := db.addUser("sm@example.org") @@ -361,7 +361,7 @@ func TestNoAuth(t *testing.T) { defer done() sm := http.NewServeMux() - NewServer(sm, db, "", window) + NewServer(sm, db, nil, "", window, false) ts := httptest.NewServer(sm) u := fmt.Sprintf("%s/foo", ts.URL) @@ -390,7 +390,7 @@ func TestBadVcs(t *testing.T) { defer done() sm := http.NewServeMux() - NewServer(sm, db, "", window) + NewServer(sm, db, nil, "", window, false) ts := httptest.NewServer(sm) tok, err := db.addUser("sm@example.org") @@ -421,7 +421,7 @@ func TestUnsupportedMethod(t *testing.T) { defer done() sm := http.NewServeMux() - NewServer(sm, db, "", window) + NewServer(sm, db, nil, "", window, false) ts := httptest.NewServer(sm) tok, err := db.addUser("sm@example.org") @@ -453,7 +453,7 @@ func TestDelete(t *testing.T) { defer done() sm := http.NewServeMux() - NewServer(sm, db, "", window) + NewServer(sm, db, nil, "", window, false) ts := httptest.NewServer(sm) tok, err := db.addUser("sm@example.org") diff --git a/cmd/vaind/main.go b/cmd/vaind/main.go index 9a2d942..1e3fb96 100644 --- a/cmd/vaind/main.go +++ b/cmd/vaind/main.go @@ -55,7 +55,8 @@ import ( const usage = "vaind [init] " type config struct { - Port int + Port int + Insecure bool Cert string Key string @@ -63,6 +64,11 @@ type config struct { Static string EmailTimeout time.Duration `envconfig:"email_timeout"` + + SMTPHost string `envconfig:"smtp_host"` + SMTPPort int `envconfig:"smtp_port"` + + From string } func main() { @@ -101,6 +107,7 @@ func main() { c := &config{ Port: 4040, EmailTimeout: 5 * time.Minute, + SMTPPort: 25, } if err := envconfig.Process("vain", c); err != nil { fmt.Fprintf(os.Stderr, "problem processing environment: %v", err) @@ -114,6 +121,13 @@ func main() { os.Exit(0) } } + + m, err := vain.NewEmail(c.From, c.SMTPHost, c.SMTPPort) + if err != nil { + fmt.Fprintf(os.Stderr, "problem initializing mailer: %v", err) + os.Exit(1) + } + hostname := "localhost" if hn, err := os.Hostname(); err != nil { log.Printf("problem getting hostname: %v", err) @@ -122,7 +136,7 @@ func main() { } log.Printf("serving at: http://%s:%d/", hostname, c.Port) sm := http.NewServeMux() - vain.NewServer(sm, db, c.Static, c.EmailTimeout) + vain.NewServer(sm, db, m, c.Static, c.EmailTimeout, c.Insecure) addr := fmt.Sprintf(":%d", c.Port) if c.Cert == "" || c.Key == "" { diff --git a/mail.go b/mail.go new file mode 100644 index 0000000..b410d6d --- /dev/null +++ b/mail.go @@ -0,0 +1,54 @@ +package vain + +import ( + "bytes" + "fmt" + "net/mail" + "net/smtp" +) + +type Mailer interface { + Send(to mail.Address, msg string) error +} + +func NewEmail(from, host string, port int) (*Email, error) { + if _, err := mail.ParseAddress(from); err != nil { + return nil, fmt.Errorf("can't parse an email address for 'from': %v", err) + } + r := &Email{ + host: host, + port: port, + from: from, + } + return r, nil +} + +type Email struct { + host string + port int + from string +} + +func (e Email) Send(to mail.Address, msg string) error { + c, err := smtp.Dial(fmt.Sprintf("%s:%d", e.host, e.port)) + if err != nil { + return fmt.Errorf("couldn't dial mail server: %v", err) + } + defer c.Close() + if err := c.Mail(e.from); err != nil { + return err + } + if err := c.Rcpt(to.String()); err != nil { + return err + } + wc, err := c.Data() + if err != nil { + return fmt.Errorf("problem sending mail: %v", err) + } + buf := bytes.NewBufferString("Subject: your api key\n\n" + msg) + buf.WriteTo(wc) + if err := c.Quit(); err != nil { + return nil + } + return err +} diff --git a/server.go b/server.go index b211521..086f1e6 100644 --- a/server.go +++ b/server.go @@ -3,7 +3,6 @@ package vain import ( "encoding/json" "fmt" - "log" "net/http" "net/mail" "strings" @@ -28,22 +27,26 @@ func init() { } } -// NewServer populates a server, adds the routes, and returns it for use. -func NewServer(sm *http.ServeMux, store *DB, static string, emailTimeout time.Duration) *Server { - s := &Server{ - db: store, - static: static, - emailTimeout: emailTimeout, - } - addRoutes(sm, s) - return s -} - // Server serves up the http. type Server struct { db *DB static string emailTimeout time.Duration + mail Mailer + insecure bool +} + +// NewServer populates a server, adds the routes, and returns it for use. +func NewServer(sm *http.ServeMux, store *DB, m Mailer, static string, emailTimeout time.Duration, insecure bool) *Server { + s := &Server{ + db: store, + static: static, + emailTimeout: emailTimeout, + mail: m, + insecure: insecure, + } + addRoutes(sm, s) + return s } func (s *Server) ServeHTTP(w http.ResponseWriter, req *http.Request) { @@ -138,27 +141,36 @@ func (s *Server) register(w http.ResponseWriter, req *http.Request) { return } - addr := email[0] - if _, err := mail.ParseAddress(addr); err != nil { + addr, err := mail.ParseAddress(email[0]) + if err != nil { http.Error(w, fmt.Sprintf("invalid email detected: %v", err), http.StatusBadRequest) return } - tok, err := s.db.Register(addr) + tok, err := s.db.Register(addr.Address) if err := verrors.ToHTTP(err); err != nil { http.Error(w, err.Message, err.Code) return } + proto := "https" - if req.TLS == nil { + if s.insecure { proto = "http" } - log.Printf("%s://%s/api/v0/confirm/%+v", proto, req.Host, tok) resp := struct { Msg string `json:"msg"` }{ Msg: "please check your email\n", } + + err = s.mail.Send( + *addr, + fmt.Sprintf("%s://%s/api/v0/confirm/%+v", proto, req.Host, tok), + ) + if err != nil { + resp.Msg = fmt.Sprintf("problem sending email: %v", err) + w.WriteHeader(http.StatusInternalServerError) + } w.Header().Set("Content-type", "application/json") json.NewEncoder(w).Encode(resp) } @@ -186,23 +198,35 @@ func (s *Server) forgot(w http.ResponseWriter, req *http.Request) { return } - addr := email[0] - if _, err := mail.ParseAddress(addr); err != nil { + addr, err := mail.ParseAddress(email[0]) + if err != nil { http.Error(w, fmt.Sprintf("invalid email detected: %v", err), http.StatusBadRequest) return } - tok, err := s.db.forgot(addr, s.emailTimeout) + tok, err := s.db.forgot(addr.Address, s.emailTimeout) if err := verrors.ToHTTP(err); err != nil { http.Error(w, err.Message, err.Code) return } - log.Printf("http://%s/api/v0/confirm/%+v", req.Host, tok) + proto := "https" + if s.insecure { + proto = "http" + } resp := struct { Msg string `json:"msg"` }{ Msg: "please check your email\n", } + + err = s.mail.Send( + *addr, + fmt.Sprintf("%s://%s/api/v0/confirm/%+v", proto, req.Host, tok), + ) + if err != nil { + resp.Msg = fmt.Sprintf("problem sending email: %v", err) + w.WriteHeader(http.StatusInternalServerError) + } w.Header().Set("Content-type", "application/json") json.NewEncoder(w).Encode(resp) }