diff --git a/Dockerfile b/Dockerfile
index 6eefa81b8..87a38cae4 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -124,6 +124,9 @@ RUN filetest -c $GOPATH/src/github.com/gobuffalo/buffalo/buffalo/cmd/filetests/g
RUN buffalo g resource person_event
RUN filetest -c $GOPATH/src/github.com/gobuffalo/buffalo/buffalo/cmd/filetests/generate_underscore.json
+RUN buffalo g mailer welcome_email
+RUN filetest -c $GOPATH/src/github.com/gobuffalo/buffalo/buffalo/cmd/filetests/generate_mailer.json
+
RUN rm -rf bin
RUN buffalo build -k -e
RUN filetest -c $GOPATH/src/github.com/gobuffalo/buffalo/buffalo/cmd/filetests/no_assets_build.json
diff --git a/buffalo/cmd/filetests/generate_mailer.json b/buffalo/cmd/filetests/generate_mailer.json
new file mode 100644
index 000000000..01acbd54c
--- /dev/null
+++ b/buffalo/cmd/filetests/generate_mailer.json
@@ -0,0 +1,30 @@
+[{
+ "path": "mailers/mailers.go",
+ "contains": [
+ "github.com/gobuffalo/buffalo/mail",
+ "smtp, err = mail.NewSMTPSender(host, port, user, password)"
+ ],
+ "!contains": [
+ "github.com/gobuffalo/x/mail"
+ ]
+ },
+ {
+ "path": "templates/mail/layout.html",
+ "contains": [
+ "
templates/mailers/layout.html
"
+ ]
+ },
+ {
+ "path": "mailers/welcome_email.go",
+ "contains": [
+ "err := m.AddBody(r.HTML(\"welcome_email.html\"), render.Data{})"
+ ]
+ },
+ {
+ "path": "templates/mail/welcome_email.html",
+ "contains": [
+ "Welcome Email
",
+ "../templates/mail/welcome_email.html
"
+ ]
+ }
+]
diff --git a/buffalo/cmd/generate.go b/buffalo/cmd/generate.go
index 0fea27a8c..b676aa572 100644
--- a/buffalo/cmd/generate.go
+++ b/buffalo/cmd/generate.go
@@ -17,6 +17,7 @@ func init() {
generateCmd.AddCommand(generate.ActionCmd)
generateCmd.AddCommand(generate.DockerCmd)
generateCmd.AddCommand(generate.TaskCmd)
+ generateCmd.AddCommand(generate.MailCmd)
decorate("generate", generateCmd)
RootCmd.AddCommand(generateCmd)
diff --git a/buffalo/cmd/generate/mailer.go b/buffalo/cmd/generate/mailer.go
new file mode 100644
index 000000000..1cc975d72
--- /dev/null
+++ b/buffalo/cmd/generate/mailer.go
@@ -0,0 +1,31 @@
+package generate
+
+import (
+ "github.com/gobuffalo/buffalo/generators/mail"
+ "github.com/gobuffalo/buffalo/meta"
+ "github.com/gobuffalo/makr"
+ "github.com/pkg/errors"
+ "github.com/spf13/cobra"
+)
+
+var mailer = mail.Generator{}
+
+// MailCmd for generating mailers
+var MailCmd = &cobra.Command{
+ Use: "mailer",
+ Short: "Generates a new mailer for Buffalo",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if len(args) == 0 {
+ return errors.New("you must supply a name for your mailer")
+ }
+ mailer.App = meta.New(".")
+ mailer.Name = meta.Name(args[0])
+ data := makr.Data{}
+ return mailer.Run(".", data)
+
+ },
+}
+
+func init() {
+ MailCmd.Flags().BoolVar(&mailer.SkipInit, "skip-init", false, "skip initializing mailers/")
+}
diff --git a/generators/mail/init/templates/mailers/mailers.go.tmpl b/generators/mail/init/templates/mailers/mailers.go.tmpl
new file mode 100644
index 000000000..4cc20fcb2
--- /dev/null
+++ b/generators/mail/init/templates/mailers/mailers.go.tmpl
@@ -0,0 +1,36 @@
+package mailers
+
+import (
+ "log"
+
+ "github.com/gobuffalo/buffalo/render"
+ "github.com/gobuffalo/envy"
+ "github.com/gobuffalo/packr"
+ "github.com/gobuffalo/buffalo/mail"
+ "github.com/pkg/errors"
+)
+
+var smtp mail.Sender
+var r *render.Engine
+
+func init() {
+
+ // Pulling config from the env.
+ port := envy.Get("SMTP_PORT", "1025")
+ host := envy.Get("SMTP_HOST", "localhost")
+ user := envy.Get("SMTP_USER", "")
+ password := envy.Get("SMTP_PASSWORD", "")
+
+ var err error
+ smtp, err = mail.NewSMTPSender(host, port, user, password)
+
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ r = render.New(render.Options{
+ HTMLLayout: "layout.html",
+ TemplatesBox: packr.NewBox("../templates/mail"),
+ Helpers: render.Helpers{},
+ })
+}
diff --git a/generators/mail/init/templates/templates/mail/layout.html.tmpl b/generators/mail/init/templates/templates/mail/layout.html.tmpl
new file mode 100644
index 000000000..7d88c8fe6
--- /dev/null
+++ b/generators/mail/init/templates/templates/mail/layout.html.tmpl
@@ -0,0 +1,3 @@
+templates/mailers/layout.html
+
+<%= yield %>
diff --git a/generators/mail/mail.go b/generators/mail/mail.go
new file mode 100644
index 000000000..78899bbd1
--- /dev/null
+++ b/generators/mail/mail.go
@@ -0,0 +1,84 @@
+package mail
+
+import (
+ "os"
+ "path/filepath"
+
+ "github.com/gobuffalo/buffalo/generators"
+ "github.com/gobuffalo/buffalo/meta"
+ "github.com/gobuffalo/makr"
+ "github.com/pkg/errors"
+)
+
+// Generator for creating new mailers
+type Generator struct {
+ App meta.App `json:"app"`
+ Name meta.Name `json:"name"`
+ SkipInit bool `json:"skip_init"`
+}
+
+// Run the new mailer generator. It will init the mailers directory
+// if it doesn't already exist
+func (d Generator) Run(root string, data makr.Data) error {
+ g := makr.New()
+ defer g.Fmt(root)
+ data["opts"] = d
+
+ if err := d.initGenerator(data); err != nil {
+ return errors.WithStack(err)
+ }
+
+ fn := d.Name.File()
+ g.Add(makr.NewFile(filepath.Join("mailers", fn+".go"), mailerTmpl))
+ g.Add(makr.NewFile(filepath.Join("templates", "mail", fn+".html"), mailTmpl))
+ return g.Run(root, data)
+}
+
+func (d Generator) initGenerator(data makr.Data) error {
+ files, err := generators.Find(filepath.Join(generators.TemplatesPath, "mail", "init"))
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ g := makr.New()
+ for _, f := range files {
+ g.Add(makr.NewFile(f.WritePath, f.Body))
+ }
+
+ g.Should = func(data makr.Data) bool {
+ if d.SkipInit {
+ return false
+ }
+ if _, err := os.Stat(filepath.Join("mailers", "mailers.go")); err == nil {
+ return false
+ }
+ return true
+ }
+ return g.Run(".", data)
+}
+
+const mailerTmpl = `package mailers
+
+import (
+ "github.com/gobuffalo/buffalo/render"
+ "github.com/gobuffalo/buffalo/mail"
+ "github.com/pkg/errors"
+)
+
+func Send{{.opts.Name.Model}}() error {
+ m := mail.NewMessage()
+
+ // fill in with your stuff:
+ m.Subject = "{{.opts.Name.Title}}"
+ m.From = ""
+ m.To = []string{}
+ err := m.AddBody(r.HTML("{{.opts.Name.File}}.html"), render.Data{})
+ if err != nil {
+ return errors.WithStack(err)
+ }
+ return smtp.Send(m)
+}
+`
+
+const mailTmpl = `{{.opts.Name.Title}}
+
+../templates/mail/{{.opts.Name.File}}.html
`
diff --git a/mail/README.md b/mail/README.md
new file mode 100644
index 000000000..e86b0c562
--- /dev/null
+++ b/mail/README.md
@@ -0,0 +1,125 @@
+# github.com/gobuffalo/buffalo/mail
+
+This package is intended to allow easy Email sending with Buffalo, it allows you to define your custom `mail.Sender` for the provider you would like to use.
+
+## Generator
+
+```bash
+$ buffalo generate mailer welcome_email
+```
+
+## Example Usage
+
+```go
+//actions/mail.go
+package x
+
+import (
+ "log"
+
+ "github.com/gobuffalo/buffalo/render"
+ "github.com/gobuffalo/envy"
+ "github.com/gobuffalo/packr"
+ "github.com/gobuffalo/plush"
+ "github.com/gobuffalo/buffalo/mail"
+ "github.com/pkg/errors"
+ "gitlab.com/wawandco/app/models"
+)
+
+var smtp mail.Sender
+var r *render.Engine
+
+func init() {
+
+ //Pulling config from the env.
+ port := envy.Get("SMTP_PORT", "1025")
+ host := envy.Get("SMTP_HOST", "localhost")
+ user := envy.Get("SMTP_USER", "")
+ password := envy.Get("SMTP_PASSWORD", "")
+
+ var err error
+ smtp, err = mail.NewSMTPSender(host, port, user, password)
+
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ //The rendering engine, this is usually generated inside actions/render.go in your buffalo app.
+ r = render.New(render.Options{
+ TemplatesBox: packr.NewBox("../templates"),
+ })
+}
+
+//SendContactMessage Sends contact message to contact@myapp.com
+func SendContactMessage(c *models.Contact) error {
+
+ //Creates a new message
+ m := mail.NewMessage()
+ m.From = "sender@myapp.com"
+ m.Subject = "New Contact"
+ m.To = []string{"contact@myapp.com"}
+
+ // Data that will be used inside the templates when rendering.
+ data := map[string]interface{}{
+ "contact": c,
+ }
+
+ // You can add multiple bodies to the message you're creating to have content-types alternatives.
+ err := m.AddBodies(data, r.HTML("mail/contact.html"), r.Plain("mail/contact.txt"))
+
+ if err != nil {
+ return errors.WithStack(err)
+ }
+
+ err = smtp.Send(m)
+ if err != nil {
+ return errors.WithStack(err)
+ }
+
+ return nil
+}
+
+```
+
+This `SendContactMessage` could be called by one of your actions, p.e. the action that handles your contact form submission.
+
+```go
+//actions/contact.go
+...
+
+func ContactFormHandler(c buffalo.Context) error {
+ contact := &models.Contact{}
+ c.Bind(contact)
+
+ //Calling to send the message
+ SendContactMessage(contact)
+ return c.Redirect(302, "contact/thanks")
+}
+...
+```
+
+If you're using Gmail or need to configure your SMTP connection you can use the Dialer property on the SMTPSender, p.e: (for Gmail)
+
+```go
+...
+var smtp mail.Sender
+
+func init() {
+ port := envy.Get("SMTP_PORT", "465")
+ // or 587 with TLS
+
+ host := envy.Get("SMTP_HOST", "smtp.gmail.com")
+ user := envy.Get("SMTP_USER", "your@email.com")
+ password := envy.Get("SMTP_PASSWORD", "yourp4ssw0rd")
+
+ var err error
+ sender, err := mail.NewSMTPSender(host, port, user, password)
+ sender.Dialer.SSL = true
+
+ //or if TLS
+ sender.Dialer.TLSConfig = &tls.Config{...}
+
+ smtp = sender
+}
+...
+```
diff --git a/mail/mail.go b/mail/mail.go
new file mode 100644
index 000000000..755ce3d16
--- /dev/null
+++ b/mail/mail.go
@@ -0,0 +1,6 @@
+package mail
+
+// Sender interface for any upcomming mailers.
+type Sender interface {
+ Send(Message) error
+}
diff --git a/mail/message.go b/mail/message.go
new file mode 100644
index 000000000..7c9295ea4
--- /dev/null
+++ b/mail/message.go
@@ -0,0 +1,82 @@
+package mail
+
+import (
+ "io"
+
+ "bytes"
+
+ "github.com/gobuffalo/buffalo/render"
+)
+
+//Message represents an Email message
+type Message struct {
+ From string
+ To []string
+ CC []string
+ Bcc []string
+ Subject string
+
+ Bodies []Body
+ Attachments []Attachment
+}
+
+// Body represents one of the bodies in the Message could be main or alternative
+type Body struct {
+ Content string
+ ContentType string
+}
+
+// Attachment are files added into a email message
+type Attachment struct {
+ Name string
+ Reader io.Reader
+ ContentType string
+}
+
+// AddBody the message by receiving a renderer and rendering data, first message will be
+// used as the main message Body rest of them will be passed as alternative bodies on the
+// email message
+func (m *Message) AddBody(r render.Renderer, data render.Data) error {
+ buf := bytes.NewBuffer([]byte{})
+ err := r.Render(buf, data)
+
+ if err != nil {
+ return err
+ }
+
+ m.Bodies = append(m.Bodies, Body{
+ Content: string(buf.Bytes()),
+ ContentType: r.ContentType(),
+ })
+
+ return nil
+}
+
+// AddBodies Allows to add multiple bodies to the message, it returns errors that
+// could happen in the rendering.
+func (m *Message) AddBodies(data render.Data, renderers ...render.Renderer) error {
+ for _, r := range renderers {
+ err := m.AddBody(r, data)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+//AddAttachment adds the attachment to the list of attachments the Message has.
+func (m *Message) AddAttachment(name, contentType string, r io.Reader) error {
+ m.Attachments = append(m.Attachments, Attachment{
+ Name: name,
+ ContentType: contentType,
+ Reader: r,
+ })
+
+ return nil
+}
+
+//NewMessage Builds a new message.
+func NewMessage() Message {
+ return Message{}
+}
diff --git a/mail/smtp_sender.go b/mail/smtp_sender.go
new file mode 100644
index 000000000..987a411a6
--- /dev/null
+++ b/mail/smtp_sender.go
@@ -0,0 +1,80 @@
+package mail
+
+import (
+ "io"
+ "strconv"
+
+ "github.com/pkg/errors"
+ gomail "gopkg.in/gomail.v2"
+)
+
+//SMTPSender allows to send Emails by connecting to a SMTP server.
+type SMTPSender struct {
+ Dialer *gomail.Dialer
+}
+
+//Send a message using SMTP configuration or returns an error if something goes wrong.
+func (sm SMTPSender) Send(message Message) error {
+ m := gomail.NewMessage()
+
+ m.SetHeader("From", message.From)
+ m.SetHeader("To", message.To...)
+ m.SetHeader("Subject", message.Subject)
+ m.SetHeader("Cc", message.CC...)
+ m.SetHeader("Bcc", message.Bcc...)
+
+ if len(message.Bodies) > 0 {
+ mainBody := message.Bodies[0]
+ m.SetBody(mainBody.ContentType, mainBody.Content, gomail.SetPartEncoding(gomail.Unencoded))
+ }
+
+ if len(message.Bodies) > 1 {
+ for i := 1; i < len(message.Bodies); i++ {
+ alt := message.Bodies[i]
+ m.AddAlternative(alt.ContentType, alt.Content, gomail.SetPartEncoding(gomail.Unencoded))
+ }
+ }
+
+ for _, at := range message.Attachments {
+ settings := gomail.SetCopyFunc(func(w io.Writer) error {
+ if _, err := io.Copy(w, at.Reader); err != nil {
+ return err
+ }
+
+ return nil
+ })
+
+ m.Attach(at.Name, settings)
+ }
+
+ err := sm.Dialer.DialAndSend(m)
+
+ if err != nil {
+ return errors.WithStack(err)
+ }
+
+ return nil
+}
+
+//NewSMTPSender builds a SMTP mail based in passed config.
+func NewSMTPSender(host string, port string, user string, password string) (SMTPSender, error) {
+ iport, err := strconv.Atoi(port)
+
+ if err != nil {
+ return SMTPSender{}, errors.New("invalid port for the SMTP mail")
+ }
+
+ dialer := &gomail.Dialer{
+ Host: host,
+ Port: iport,
+ }
+
+ if user != "" {
+ dialer.Username = user
+ dialer.Password = password
+ }
+
+ return SMTPSender{
+ Dialer: dialer,
+ }, nil
+}
diff --git a/mail/smtp_sender_test.go b/mail/smtp_sender_test.go
new file mode 100644
index 000000000..9521db906
--- /dev/null
+++ b/mail/smtp_sender_test.go
@@ -0,0 +1,58 @@
+package mail_test
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/gobuffalo/buffalo/render"
+ "github.com/gobuffalo/x/fakesmtp"
+ "github.com/gobuffalo/x/mail"
+ "github.com/stretchr/testify/require"
+)
+
+var sender mail.Sender
+var rend *render.Engine
+var smtpServer *fakesmtp.Server
+
+const smtpPort = "2002"
+
+func init() {
+ rend = render.New(render.Options{})
+ smtpServer, _ = fakesmtp.New(smtpPort)
+ sender, _ = mail.NewSMTPSender("127.0.0.1", smtpPort, "username", "password")
+
+ go smtpServer.Start(smtpPort)
+}
+
+func TestSendPlain(t *testing.T) {
+ smtpServer.Clear()
+ r := require.New(t)
+
+ m := mail.Message{
+ From: "mark@example.com",
+ To: []string{"something@something.com"},
+ Subject: "Cool Message",
+ CC: []string{"other@other.com", "my@other.com"},
+ Bcc: []string{"secret@other.com"},
+ }
+
+ m.AddAttachment("someFile.txt", "text/plain", bytes.NewBuffer([]byte("hello")))
+ m.AddBody(rend.String("Hello <%= Name %>"), render.Data{"Name": "Antonio"})
+ r.Equal(m.Bodies[0].Content, "Hello Antonio")
+
+ err := sender.Send(m)
+ r.Nil(err)
+
+ lastMessage := smtpServer.LastMessage()
+
+ r.Contains(lastMessage, "FROM:")
+ r.Contains(lastMessage, "RCPT TO:")
+ r.Contains(lastMessage, "RCPT TO:")
+ r.Contains(lastMessage, "RCPT TO:")
+ r.Contains(lastMessage, "Subject: Cool Message")
+ r.Contains(lastMessage, "Cc: other@other.com, my@other.com")
+ r.Contains(lastMessage, "Content-Type: text/plain")
+ r.Contains(lastMessage, "Hello Antonio")
+ r.Contains(lastMessage, "Content-Disposition: attachment; filename=\"someFile.txt\"")
+ r.Contains(lastMessage, "aGVsbG8=") //base64 of the file content
+}