diff --git a/Makefile b/Makefile
index 8eaa794ee..bb5840b22 100644
--- a/Makefile
+++ b/Makefile
@@ -111,6 +111,10 @@ test.rbac: $(GOTEST)
$(GOTEST) -covermode count -coverprofile .cover.out -v ./internal/rbac/...
$(GO) tool cover -func=.cover.out | grep --color "^\|[^0-9]0.0%"
+test.mail: $(GOTEST)
+ $(GOTEST) -covermode count -coverprofile .cover.out -v ./internal/mail/...
+ $(GO) tool cover -func=.cover.out | grep --color "^\|[^0-9]0.0%"
+
test.rbac.resources: $(GOTEST)
$(GOTEST) -run Resources -covermode count -coverprofile .cover.out -v ./internal/rbac/...
$(GO) tool cover -func=.cover.out | grep --color "^\|[^0-9]0.0%"
diff --git a/internal/config/smtp.go b/internal/config/smtp.go
new file mode 100644
index 000000000..039972c9f
--- /dev/null
+++ b/internal/config/smtp.go
@@ -0,0 +1,54 @@
+package config
+
+import (
+ "strconv"
+ "strings"
+
+ "github.com/namsral/flag"
+ "github.com/pkg/errors"
+)
+
+type (
+ SMTP struct {
+ Host string
+ Port int
+ User string
+ Pass string
+
+ From string
+ }
+)
+
+var smtp *SMTP
+
+func (c *SMTP) Validate() error {
+ if c.Host == "" {
+ return errors.New("No hostname provided for SMTP")
+ }
+ // split host:port from host into .Host and .Port
+ if strings.Contains(c.Host, ":") {
+ parts := strings.SplitN(c.Host, ":", 2)
+ c.Port, _ = strconv.Atoi(parts[1])
+ c.Host = parts[0]
+ }
+ if c.Port == 0 {
+ return errors.New("No port provided for SMTP")
+ }
+ if c.From == "" {
+ return errors.New("Sender for SMTP is not set")
+ }
+ return nil
+}
+
+func (*SMTP) Init(prefix ...string) *SMTP {
+ if smtp != nil {
+ return smtp
+ }
+ smtp = new(SMTP)
+ flag.StringVar(&smtp.Host, "smtp-host", "", "SMTP hostname (may be host:port)")
+ flag.IntVar(&smtp.Port, "smtp-port", 0, "SMTP port number")
+ flag.StringVar(&smtp.User, "smtp-user", "", "SMTP server username")
+ flag.StringVar(&smtp.Pass, "smtp-pass", "", "SMTP server password")
+ flag.StringVar(&smtp.From, "smtp-from", "", "SMTP sender header")
+ return smtp
+}
diff --git a/internal/mail/flags.go b/internal/mail/flags.go
new file mode 100644
index 000000000..ec460015a
--- /dev/null
+++ b/internal/mail/flags.go
@@ -0,0 +1,17 @@
+package mail
+
+import (
+ "fmt"
+
+ "github.com/crusttech/crust/internal/config"
+)
+
+var flags *config.SMTP
+
+func Flags(prefix ...string) {
+ flags = new(config.SMTP).Init(prefix...)
+}
+
+func Debug() {
+ fmt.Printf("Debug SMTP flags: %#v", *flags)
+}
diff --git a/internal/mail/mail.go b/internal/mail/mail.go
new file mode 100644
index 000000000..88605defd
--- /dev/null
+++ b/internal/mail/mail.go
@@ -0,0 +1,22 @@
+package mail
+
+import (
+ "github.com/pkg/errors"
+ gomail "gopkg.in/mail.v2"
+)
+
+func New() *gomail.Message {
+ message := gomail.NewMessage()
+ message.SetHeader("From", flags.From)
+ return message
+}
+
+func Send(message *gomail.Message) error {
+ dialer := gomail.NewDialer(
+ flags.Host,
+ flags.Port,
+ flags.User,
+ flags.Pass,
+ )
+ return errors.WithStack(dialer.DialAndSend(message))
+}
diff --git a/internal/mail/mail_test.go b/internal/mail/mail_test.go
new file mode 100644
index 000000000..ff85b81ac
--- /dev/null
+++ b/internal/mail/mail_test.go
@@ -0,0 +1,28 @@
+package mail
+
+import (
+ "testing"
+
+ "github.com/joho/godotenv"
+ "github.com/namsral/flag"
+)
+
+func TestMailSend(t *testing.T) {
+ if !testing.Short() {
+ godotenv.Load("../../.env")
+
+ Flags()
+ flag.Parse()
+ if err := flags.Validate(); err != nil {
+ t.Fatalf("Missing SMTP flags: %+v", err)
+ }
+
+ message := New()
+ message.SetHeader("To", "black@scene-si.org")
+ message.SetHeader("Subject", "Hello from Crust tests!")
+ message.SetBody("text/html", "Lorem ipsum dolor sit amet!")
+ if err := Send(message); err != nil {
+ t.Fatalf("E-mail failed to send: %+v", err)
+ }
+ }
+}