From 8cce66d021802ab0483d59210f4539f4ccc58a9d Mon Sep 17 00:00:00 2001 From: Dominik Menke Date: Fri, 6 Apr 2018 13:39:26 +0200 Subject: [PATCH 01/15] Rework path to sendmail binary It turns out, a global variable is not a great idea, so we move it into the Mail object itself. This is also introduces a new "chaninable" API: `m.SetSendmail()` returns `m`, which allows chaining of further options (see following commits). --- README.md | 3 +-- options.go | 7 +++++++ options_test.go | 14 ++++++++++++++ sendmail.go | 32 ++++++++++++++++++++++---------- sendmail_test.go | 6 ++---- 5 files changed, 46 insertions(+), 16 deletions(-) create mode 100644 options.go create mode 100644 options_test.go diff --git a/README.md b/README.md index 58a7353..cb8661a 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,8 @@ or [sSMTP](https://wiki.debian.org/sSMTP), which provide compatibility interface * encodes UTF-8 headers like `Subject`, `From`, `To` * makes it easy to use [text/template](https://golang.org/pkg/text/template) * doesn't require any SMTP configuration, -* just uses `/usr/sbin/sendmail` command which is present on most of the systems, - * if not, just update `sendmail.Binary` * outputs emails to _stdout_ when environment variable `DEBUG` is set. +* by default, it just uses `/usr/sbin/sendmail` (but can be changed if need be) Installation ------------ diff --git a/options.go b/options.go new file mode 100644 index 0000000..9d5e3c2 --- /dev/null +++ b/options.go @@ -0,0 +1,7 @@ +package sendmail + +// SetSendmail modifies the path to the sendmail binary. +func (m *Mail) SetSendmail(path string) *Mail { + m.sendmail = path + return m +} diff --git a/options_test.go b/options_test.go new file mode 100644 index 0000000..1c581ee --- /dev/null +++ b/options_test.go @@ -0,0 +1,14 @@ +package sendmail + +import ( + "testing" +) + +func TestChaningOptions(t *testing.T) { + m := &Mail{} + m.SetSendmail("/bin/true") + + if m.sendmail != "/bin/true" { + t.Errorf("Expected sendmail to be %q, got %q", "/bin/true", m.sendmail) + } +} diff --git a/sendmail.go b/sendmail.go index 5f814ea..9998ec0 100644 --- a/sendmail.go +++ b/sendmail.go @@ -16,12 +16,10 @@ import ( "strings" ) -var ( - _, debug = os.LookupEnv("DEBUG") +var _, debug = os.LookupEnv("DEBUG") - // Binary points to the sendmail binary. - Binary = "/usr/sbin/sendmail" -) +// SendmailDefault points to the default sendmail binary location. +const SendmailDefault = "/usr/sbin/sendmail" // Mail defines basic mail structure and headers type Mail struct { @@ -31,6 +29,8 @@ type Mail struct { Header http.Header Text bytes.Buffer HTML bytes.Buffer + + sendmail string } // Send sends an email, or prints it on stderr, @@ -48,6 +48,7 @@ func (m *Mail) Send() error { m.Header.Set("Content-Type", "text/plain; charset=UTF-8") m.Header.Set("Subject", mime.QEncoding.Encode("utf-8", m.Subject)) m.Header.Set("From", m.From.String()) + to := make([]string, len(m.To)) arg := make([]string, len(m.To)) for i, t := range m.To { @@ -62,16 +63,27 @@ func (m *Mail) Send() error { fmt.Println(delimiter) return nil } - sendmail := exec.Command(Binary, arg...) - stdin, err := sendmail.StdinPipe() + + return m.exec(arg...) +} + +// exec handles sendmail command invokation. +func (m *Mail) exec(arg ...string) error { + bin := SendmailDefault + if m.sendmail != "" { + bin = m.sendmail + } + cmd := exec.Command(bin, arg...) + + stdin, err := cmd.StdinPipe() if err != nil { return err } - stderr, err := sendmail.StderrPipe() + stderr, err := cmd.StderrPipe() if err != nil { return err } - if err = sendmail.Start(); err != nil { + if err = cmd.Start(); err != nil { return err } if err = m.WriteTo(stdin); err != nil { @@ -87,7 +99,7 @@ func (m *Mail) Send() error { if len(out) != 0 { return errors.New(string(out)) } - return sendmail.Wait() + return cmd.Wait() } // WriteTo writes headers and content of the email to io.Writer diff --git a/sendmail_test.go b/sendmail_test.go index f4efa54..1eee865 100644 --- a/sendmail_test.go +++ b/sendmail_test.go @@ -13,10 +13,6 @@ func maddr(name, address string) *mail.Address { return &mail.Address{Name: name, Address: address + domain} } -func init() { - Binary = "/bin/true" -} - func TestSend(tc *testing.T) { tc.Run("debug:true", func(t *testing.T) { testSend(t, true) @@ -39,6 +35,8 @@ func testSend(t *testing.T, withDebug bool) { maddr("Ktoś2", "info2@"), }, } + sm.SetSendmail("/bin/true") + io.WriteString(&sm.Text, ":)\r\n") if err := sm.Send(); err != nil { t.Errorf("(debug=%v) %v", withDebug, err) From de370e3ad854f76cdac97338cf80651bfd35a067 Mon Sep 17 00:00:00 2001 From: Dominik Menke Date: Fri, 6 Apr 2018 13:43:34 +0200 Subject: [PATCH 02/15] Update README [ci skip] --- README.md | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index cb8661a..300dc8c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,17 @@ -Go sendmail [![Build Status](https://travis-ci.org/meehow/sendmail.svg?branch=master)](https://travis-ci.org/meehow/sendmail) -=========== +# Go sendmail -This package implements classic, well known from PHP, method of sending emails. -It's stupid simple and it works not only with Sendmail, -but also with other MTAs, like [Postfix](http://www.postfix.org/sendmail.1.html) -or [sSMTP](https://wiki.debian.org/sSMTP), which provide compatibility interface. +[![GoDoc](https://godoc.org/github.com/meehow/sendmail?status.svg)](https://godoc.org/github.com/meehow/sendmail) +[![Build Status](https://travis-ci.org/meehow/sendmail.svg?branch=master)](https://travis-ci.org/meehow/sendmail) + + +This package implements the classic method of sending emails, well known +from PHP. It's stupid simple and it works not only with Sendmail, but also +with other MTAs, like [Postfix][], [sSMTP][], or [mhsendmail][], which +provide a compatible interface. + +[Postfix]: http://www.postfix.org/sendmail.1.html +[sSMTP]: https://wiki.debian.org/sSMTP +[mhsendmail]: https://github.com/mailhog/mhsendmail * it separates email headers from email body, * encodes UTF-8 headers like `Subject`, `From`, `To` @@ -13,14 +20,15 @@ or [sSMTP](https://wiki.debian.org/sSMTP), which provide compatibility interface * outputs emails to _stdout_ when environment variable `DEBUG` is set. * by default, it just uses `/usr/sbin/sendmail` (but can be changed if need be) -Installation ------------- + +## Installation + ``` go get -u github.com/meehow/sendmail ``` -Usage ------ +## Usage + ```go package main @@ -58,7 +66,6 @@ tpl.ExecuteTemplate(&sm.Text, "email", &struct{ Name string }{"Dominik"}) ``` -ToDo ----- +## ToDo * HTML emails From d16b88f4835cb231b396e026a2497f5e9bfb255f Mon Sep 17 00:00:00 2001 From: Dominik Menke Date: Fri, 6 Apr 2018 13:55:04 +0200 Subject: [PATCH 03/15] Rework debug output Don't depend on environment variable for debug output, instead let the consumer decide where to print to. --- README.md | 2 +- options.go | 23 +++++++++++++++++++++++ options_test.go | 15 ++++++++++++++- sendmail.go | 11 ++++++----- sendmail_test.go | 8 +++----- 5 files changed, 47 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 300dc8c..dbcbceb 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ provide a compatible interface. * encodes UTF-8 headers like `Subject`, `From`, `To` * makes it easy to use [text/template](https://golang.org/pkg/text/template) * doesn't require any SMTP configuration, -* outputs emails to _stdout_ when environment variable `DEBUG` is set. +* can write email body to a custom `io.Writer` to simplify testing * by default, it just uses `/usr/sbin/sendmail` (but can be changed if need be) diff --git a/options.go b/options.go index 9d5e3c2..c61be04 100644 --- a/options.go +++ b/options.go @@ -1,7 +1,30 @@ package sendmail +import ( + "io" + "os" +) + // SetSendmail modifies the path to the sendmail binary. func (m *Mail) SetSendmail(path string) *Mail { m.sendmail = path return m } + +// SetDebug sets the debug output to stderr if active is true, else it +// removes the debug output. Use SetDebugOutput to set it to something else. +func (m *Mail) SetDebug(active bool) *Mail { + var out io.Writer + if active { + out = os.Stderr + } + m.debugOut = out + return m +} + +// SetDebugOutput sets the debug output to the given writer. If w is +// nil, this is equivalent to SetDebug(false). +func (m *Mail) SetDebugOutput(w io.Writer) *Mail { + m.debugOut = w + return m +} diff --git a/options_test.go b/options_test.go index 1c581ee..8d0c7c0 100644 --- a/options_test.go +++ b/options_test.go @@ -1,14 +1,27 @@ package sendmail import ( + "bytes" "testing" ) func TestChaningOptions(t *testing.T) { + var buf bytes.Buffer m := &Mail{} - m.SetSendmail("/bin/true") + + if m.sendmail != "" { + t.Errorf("Expected initial sendmail to be empty, got %q", m.sendmail) + } + if m.debugOut != nil { + t.Errorf("Expected initial debugOut to be nil, got %T", m.debugOut) + } + + m.SetSendmail("/bin/true").SetDebugOutput(&buf) if m.sendmail != "/bin/true" { t.Errorf("Expected sendmail to be %q, got %q", "/bin/true", m.sendmail) } + if m.debugOut != &buf { + t.Errorf("Expected debugOut to be %T (buf), got %T", &buf, m.debugOut) + } } diff --git a/sendmail.go b/sendmail.go index 9998ec0..2b13145 100644 --- a/sendmail.go +++ b/sendmail.go @@ -20,6 +20,7 @@ var _, debug = os.LookupEnv("DEBUG") // SendmailDefault points to the default sendmail binary location. const SendmailDefault = "/usr/sbin/sendmail" +const debugDelimiter = "\n----------------------------------------------------------------------" // Mail defines basic mail structure and headers type Mail struct { @@ -31,6 +32,7 @@ type Mail struct { HTML bytes.Buffer sendmail string + debugOut io.Writer } // Send sends an email, or prints it on stderr, @@ -56,11 +58,10 @@ func (m *Mail) Send() error { arg[i] = t.Address } m.Header.Set("To", strings.Join(to, ", ")) - if debug { - delimiter := "\n" + strings.Repeat("-", 70) - fmt.Println(delimiter) - m.WriteTo(os.Stdout) - fmt.Println(delimiter) + if m.debugOut != nil { + fmt.Println(debugDelimiter) + m.WriteTo(m.debugOut) + fmt.Println(debugDelimiter) return nil } diff --git a/sendmail_test.go b/sendmail_test.go index 1eee865..e04b943 100644 --- a/sendmail_test.go +++ b/sendmail_test.go @@ -15,18 +15,16 @@ func maddr(name, address string) *mail.Address { func TestSend(tc *testing.T) { tc.Run("debug:true", func(t *testing.T) { + t.Parallel() testSend(t, true) }) tc.Run("debug:false", func(t *testing.T) { + t.Parallel() testSend(t, false) }) } func testSend(t *testing.T, withDebug bool) { - oldDebug := debug - debug = withDebug - defer func() { debug = oldDebug }() - sm := Mail{ Subject: "Cześć", From: maddr("Michał", "me@"), @@ -35,7 +33,7 @@ func testSend(t *testing.T, withDebug bool) { maddr("Ktoś2", "info2@"), }, } - sm.SetSendmail("/bin/true") + sm.SetSendmail("/bin/true").SetDebug(withDebug) io.WriteString(&sm.Text, ":)\r\n") if err := sm.Send(); err != nil { From 455a1497a28f49edaff71179fcbbd97ea9dda32f Mon Sep 17 00:00:00 2001 From: Dominik Menke Date: Fri, 6 Apr 2018 14:01:50 +0200 Subject: [PATCH 04/15] New chainable options: SetSubject, AppendTo, SetFrom AppendTo might be a bit misleading, as it doesn't append the mail to something, but appends the mail's To field. It is literally a `To = append(To, ...)` construct. --- options.go | 22 ++++++++++++++++++++++ options_test.go | 33 ++++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/options.go b/options.go index c61be04..cf0791c 100644 --- a/options.go +++ b/options.go @@ -2,6 +2,7 @@ package sendmail import ( "io" + "net/mail" "os" ) @@ -28,3 +29,24 @@ func (m *Mail) SetDebugOutput(w io.Writer) *Mail { m.debugOut = w return m } + +// AppendTo adds a recipient to the Mail. The name argument is the +// "proper name" of the recipient and may be empty. The address must be +// in the form "user@domain". +func (m *Mail) AppendTo(name, address string) *Mail { + m.To = append(m.To, &mail.Address{Name: name, Address: address}) + return m +} + +// SetFrom updates the sender's address. Like AppendTo(), name may be +// empty, and address must be in the form "user@domain". +func (m *Mail) SetFrom(name, address string) *Mail { + m.From = &mail.Address{Name: name, Address: address} + return m +} + +// SetSubject sets the mail subject. +func (m *Mail) SetSubject(subject string) *Mail { + m.Subject = subject + return m +} diff --git a/options_test.go b/options_test.go index 8d0c7c0..199329e 100644 --- a/options_test.go +++ b/options_test.go @@ -2,13 +2,26 @@ package sendmail import ( "bytes" + "net/mail" "testing" ) func TestChaningOptions(t *testing.T) { var buf bytes.Buffer - m := &Mail{} - + m := &Mail{ + To: []*mail.Address{ + &mail.Address{Name: "Michał", Address: "me@example.com"}, + }, + } + if m.Subject != "" { + t.Errorf("Expected subject to be empty, got %q", m.Subject) + } + if len(m.To) != 1 { + t.Errorf("Expected len(To) to be 1, got %d: %+v", len(m.To), m.To) + } + if m.From != nil { + t.Errorf("Expected From address to be nil, got %s", m.From) + } if m.sendmail != "" { t.Errorf("Expected initial sendmail to be empty, got %q", m.sendmail) } @@ -16,8 +29,22 @@ func TestChaningOptions(t *testing.T) { t.Errorf("Expected initial debugOut to be nil, got %T", m.debugOut) } - m.SetSendmail("/bin/true").SetDebugOutput(&buf) + m.SetSubject("Test subject"). + SetFrom("Dominik", "dominik@example.org"). + AppendTo("Dominik2", "dominik2@example.org"). + SetDebugOutput(&buf). + SetSendmail("/bin/true") + if m.Subject != "Test subject" { + t.Errorf("Expected subject to be %q, got %q", "Test subject", m.Subject) + } + if len(m.To) != 2 { + t.Errorf("Expected len(To) to be 2, got %d: %+v", len(m.To), m.To) + } + if m.From == nil || m.From.Address != "dominik@example.org" { + expected := mail.Address{Name: "Dominik", Address: "dominik@example.org"} + t.Errorf("Expected From address to be %s, got %s", expected, m.From) + } if m.sendmail != "/bin/true" { t.Errorf("Expected sendmail to be %q, got %q", "/bin/true", m.sendmail) } From 71a0339ae0e9ecaae09518619de514f7aaff05db Mon Sep 17 00:00:00 2001 From: Dominik Menke Date: Fri, 6 Apr 2018 14:17:49 +0200 Subject: [PATCH 05/15] Additional Mail constructor Accepts chainable Options as arguments. --- options.go | 44 +++++++++++++++++++++++++++++++++ options_test.go | 64 ++++++++++++++++++++++++++++++++++++++++++++++++ sendmail.go | 9 +++++++ sendmail_test.go | 29 ++++++++++++++++++++++ 4 files changed, 146 insertions(+) diff --git a/options.go b/options.go index cf0791c..28bfd32 100644 --- a/options.go +++ b/options.go @@ -50,3 +50,47 @@ func (m *Mail) SetSubject(subject string) *Mail { m.Subject = subject return m } + +// Option is used in the Mail constructor. +type Option interface { + execute(*Mail) +} + +type optionFunc func(*Mail) + +func (o optionFunc) execute(m *Mail) { o(m) } + +// Sendmail modifies the path to the sendmail binary. +func Sendmail(path string) Option { + return optionFunc(func(m *Mail) { m.SetSendmail(path) }) +} + +// Debug sets the debug output to stderr if active is true, else it +// removes the debug output. Use SetDebugOutput to set it to something else. +func Debug(active bool) Option { + return optionFunc(func(m *Mail) { m.SetDebug(active) }) +} + +// DebugOutput sets the debug output to the given writer. If w is nil, +// this is equivalent to SetDebug(false). +func DebugOutput(w io.Writer) Option { + return optionFunc(func(m *Mail) { m.SetDebugOutput(w) }) +} + +// To adds a recipient to the Mail. The name argument is the "proper name" +// of the recipient and may be empty. The address must be in the form +// "user@domain". +func To(name, address string) Option { + return optionFunc(func(m *Mail) { m.AppendTo(name, address) }) +} + +// From updates the sender's address. Like To(), name may be empty, and +// address must be in the form "user@domain". +func From(name, address string) Option { + return optionFunc(func(m *Mail) { m.SetFrom(name, address) }) +} + +// Subject sets the mail subject. +func Subject(subject string) Option { + return optionFunc(func(m *Mail) { m.SetSubject(subject) }) +} diff --git a/options_test.go b/options_test.go index 199329e..d282457 100644 --- a/options_test.go +++ b/options_test.go @@ -3,6 +3,7 @@ package sendmail import ( "bytes" "net/mail" + "os" "testing" ) @@ -52,3 +53,66 @@ func TestChaningOptions(t *testing.T) { t.Errorf("Expected debugOut to be %T (buf), got %T", &buf, m.debugOut) } } + +func TestOptions(t *testing.T) { + m := &Mail{} + var o Option + + o = Sendmail("/foo/bar") + if o.execute(m); m.sendmail != "/foo/bar" { + t.Errorf("Expected sendmail to be %q, got %q", "/foo/bar", m.sendmail) + } + + o = Debug(true) + if o.execute(m); m.debugOut != os.Stderr { + t.Errorf("Expected debugOut to be %T (stderr), got %T", os.Stderr, m.debugOut) + } + + o = Debug(false) + if o.execute(m); m.debugOut != nil { + t.Errorf("Expected debugOut to be nil, got %T", m.debugOut) + } + + var buf bytes.Buffer + o = DebugOutput(&buf) + if o.execute(m); m.debugOut != &buf { + t.Errorf("Expected debugOut to be %T (buf), got %T", &buf, m.debugOut) + } + + o = DebugOutput(nil) + if o.execute(m); m.debugOut != nil { + t.Errorf("Expected debugOut to be nil, got %T", m.debugOut) + } + + // To() appends list + o = To("Ktoś", "info@example.com") + if o.execute(m); len(m.To) != 1 { + t.Errorf("Expected len(To) to be 1, got %d: %+v", len(m.To), m.To) + } + o = To("Ktoś2", "info2@example.com") + if o.execute(m); len(m.To) != 2 { + t.Errorf("Expected len(To) to be 2, got %d: %+v", len(m.To), m.To) + } + + // From() updates current sender + o = From("Michał", "me@example.com") + if o.execute(m); m.From == nil || m.From.Address != "me@example.com" { + expected := mail.Address{Name: "Michał", Address: "me@example.com"} + t.Errorf("Expected From address to be %s, got %s", expected, m.From) + } + o = From("Michał", "me@example.com") + if o.execute(m); m.From == nil || m.From.Address != "me@example.com" { + expected := mail.Address{Name: "Michał", Address: "me@example.com"} + t.Errorf("Expected From address to be %s, got %s", expected, m.From) + } + + // Subject() updates current subject + o = Subject("Cześć") + if o.execute(m); m.Subject != "Cześć" { + t.Errorf("Expected Subject to be %q, got %q", "Cześć", m.Subject) + } + o = Subject("Test") + if o.execute(m); m.Subject != "Test" { + t.Errorf("Expected Subject to be %q, got %q", "Test", m.Subject) + } +} diff --git a/sendmail.go b/sendmail.go index 2b13145..3dbd1ba 100644 --- a/sendmail.go +++ b/sendmail.go @@ -35,6 +35,15 @@ type Mail struct { debugOut io.Writer } +// New creates a new Mail instance with the given options. +func New(options ...Option) (m *Mail) { + m = &Mail{sendmail: SendmailDefault} + for _, option := range options { + option.execute(m) + } + return +} + // Send sends an email, or prints it on stderr, // when environment variable `DEBUG` is set. func (m *Mail) Send() error { diff --git a/sendmail_test.go b/sendmail_test.go index e04b943..bb7979f 100644 --- a/sendmail_test.go +++ b/sendmail_test.go @@ -1,6 +1,7 @@ package sendmail import ( + "bytes" "fmt" "io" "net/mail" @@ -66,3 +67,31 @@ func TestToError(t *testing.T) { t.Errorf("Expected an error because of missing `To` addresses") } } + +func TestNew(t *testing.T) { + var buf bytes.Buffer + m := New( + Subject("Test subject"), + From("Dominik", "dominik@example.org"), + To("Dominik2", "dominik2@example.org"), + DebugOutput(&buf), + Sendmail("/bin/true"), + ) + + if m.Subject != "Test subject" { + t.Errorf("Expected subject to be %q, got %q", "Test subject", m.Subject) + } + if len(m.To) != 1 { + t.Errorf("Expected len(To) to be 1, got %d: %+v", len(m.To), m.To) + } + if m.From == nil || m.From.Address != "dominik@example.org" { + expected := mail.Address{Name: "Dominik", Address: "dominik@example.org"} + t.Errorf("Expected From address to be %s, got %s", expected, m.From) + } + if m.sendmail != "/bin/true" { + t.Errorf("Expected sendmail to be %q, got %q", "/bin/true", m.sendmail) + } + if m.debugOut != &buf { + t.Errorf("Expected debugOut to be %T (buf), got %T", &buf, m.debugOut) + } +} From 344d404ed98575a22ac1583edb7fe96fa7f872de Mon Sep 17 00:00:00 2001 From: Dominik Menke Date: Fri, 6 Apr 2018 16:50:42 +0200 Subject: [PATCH 06/15] Remove debug delimiter Makes testing unneccessarily hard. --- sendmail.go | 3 --- sendmail_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/sendmail.go b/sendmail.go index 3dbd1ba..2cceb29 100644 --- a/sendmail.go +++ b/sendmail.go @@ -20,7 +20,6 @@ var _, debug = os.LookupEnv("DEBUG") // SendmailDefault points to the default sendmail binary location. const SendmailDefault = "/usr/sbin/sendmail" -const debugDelimiter = "\n----------------------------------------------------------------------" // Mail defines basic mail structure and headers type Mail struct { @@ -68,9 +67,7 @@ func (m *Mail) Send() error { } m.Header.Set("To", strings.Join(to, ", ")) if m.debugOut != nil { - fmt.Println(debugDelimiter) m.WriteTo(m.debugOut) - fmt.Println(debugDelimiter) return nil } diff --git a/sendmail_test.go b/sendmail_test.go index bb7979f..b72e05f 100644 --- a/sendmail_test.go +++ b/sendmail_test.go @@ -5,6 +5,8 @@ import ( "fmt" "io" "net/mail" + "os" + "strings" "testing" ) @@ -50,6 +52,36 @@ func testSend(t *testing.T, withDebug bool) { } } +func TestTextMail(t *testing.T) { + var buf bytes.Buffer + sm := New( + Subject("Cześć"), + From("Michał", "me@"+domain), + To("Ktoś", "info@"+domain), + To("Ktoś2", "info2@"+domain), + DebugOutput(&buf), + ) + io.WriteString(&sm.Text, ":)\r\n") + + expected := strings.Join([]string{ + "Content-Type: text/plain; charset=UTF-8", + "From: =?utf-8?q?Micha=C5=82?= ", + "Subject: =?utf-8?q?Cze=C5=9B=C4=87?=", + "To: =?utf-8?q?Kto=C5=9B?= , =?utf-8?q?Kto=C5=9B2?= ", + "", + ":)", + "", + }, "\r\n") + + if err := sm.Send(); err != nil { + t.Errorf("Error writing to buffer: %v", err) + } + if actual := buf.String(); actual != expected { + fmt.Fprintln(os.Stderr, actual) + t.Errorf("Unexpected mail content") + } +} + func TestFromError(t *testing.T) { sm := Mail{ To: []*mail.Address{maddr("Ktoś", "info@")}, From a66ed9855481e4c8f9976ffa70199a58ad707887 Mon Sep 17 00:00:00 2001 From: Dominik Menke Date: Fri, 6 Apr 2018 16:52:49 +0200 Subject: [PATCH 07/15] Allow either HTML or Text bodies Place multipart mails (HTML+Text) on the roadmap. --- README.md | 5 ++++- sendmail.go | 27 ++++++++++++++++++++++++--- sendmail_test.go | 30 ++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index dbcbceb..267f09a 100644 --- a/README.md +++ b/README.md @@ -68,4 +68,7 @@ tpl.ExecuteTemplate(&sm.Text, "email", &struct{ Name string }{"Dominik"}) ## ToDo -* HTML emails +- [x] HTML emails +- [ ] multipart emails (HTML + Text) +- [ ] attachments +- [ ] inline attachments diff --git a/sendmail.go b/sendmail.go index 2cceb29..b94f8d9 100644 --- a/sendmail.go +++ b/sendmail.go @@ -55,7 +55,6 @@ func (m *Mail) Send() error { if m.Header == nil { m.Header = make(http.Header) } - m.Header.Set("Content-Type", "text/plain; charset=UTF-8") m.Header.Set("Subject", mime.QEncoding.Encode("utf-8", m.Subject)) m.Header.Set("From", m.From.String()) @@ -111,14 +110,36 @@ func (m *Mail) exec(arg ...string) error { // WriteTo writes headers and content of the email to io.Writer func (m *Mail) WriteTo(wr io.Writer) error { + isText := m.Text.Len() > 0 + isHTML := m.HTML.Len() > 0 + + if isText && isHTML { + return fmt.Errorf("Multipart mails are not supported yet") + } else if isHTML { + m.Header.Set("Content-Type", "text/html; charset=UTF-8") + } else { + // also for mails without body + m.Header.Set("Content-Type", "text/plain; charset=UTF-8") + } + + // write header if err := m.Header.Write(wr); err != nil { return err } if _, err := wr.Write([]byte("\r\n")); err != nil { return err } - if _, err := m.Text.WriteTo(wr); err != nil { - return err + + if isText && isHTML { + // TODO + } else if isHTML { + if _, err := m.HTML.WriteTo(wr); err != nil { + return err + } + } else if isText { + if _, err := m.Text.WriteTo(wr); err != nil { + return err + } } return nil } diff --git a/sendmail_test.go b/sendmail_test.go index b72e05f..2ff140c 100644 --- a/sendmail_test.go +++ b/sendmail_test.go @@ -82,6 +82,36 @@ func TestTextMail(t *testing.T) { } } +func TestHTMLMail(t *testing.T) { + var buf bytes.Buffer + sm := New( + Subject("Cześć"), + From("Michał", "me@"+domain), + To("Ktoś", "info@"+domain), + To("Ktoś2", "info2@"+domain), + DebugOutput(&buf), + ) + io.WriteString(&sm.HTML, "

:)

\r\n") + + expected := strings.Join([]string{ + "Content-Type: text/html; charset=UTF-8", + "From: =?utf-8?q?Micha=C5=82?= ", + "Subject: =?utf-8?q?Cze=C5=9B=C4=87?=", + "To: =?utf-8?q?Kto=C5=9B?= , =?utf-8?q?Kto=C5=9B2?= ", + "", + "

:)

", + "", + }, "\r\n") + + if err := sm.Send(); err != nil { + t.Errorf("Error writing to buffer: %v", err) + } + if actual := buf.String(); actual != expected { + fmt.Fprintln(os.Stderr, actual) + t.Errorf("Unexpected mail content") + } +} + func TestFromError(t *testing.T) { sm := Mail{ To: []*mail.Address{maddr("Ktoś", "info@")}, From 33f74ca436f939d275ae03c154f3e0407596537e Mon Sep 17 00:00:00 2001 From: Dominik Menke Date: Fri, 6 Apr 2018 17:17:12 +0200 Subject: [PATCH 08/15] Travis: use gometalinter to perform linting --- .travis.yml | 8 ++++++++ options_test.go | 3 +-- sendmail.go | 8 ++------ sendmail_test.go | 3 +-- validate_test.go | 4 ++-- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9424da4..ffd8ca5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,3 +4,11 @@ go: - "1.8.x" - "1.9.x" - "1.10.x" + +before_install: + - go get -u gopkg.in/alecthomas/gometalinter.v2 + - gometalinter.v2 --install + +script: + - gometalinter.v2 --disable=gas --disable=gocyclo ./... + - go test -v ./... diff --git a/options_test.go b/options_test.go index d282457..9361bb5 100644 --- a/options_test.go +++ b/options_test.go @@ -56,9 +56,8 @@ func TestChaningOptions(t *testing.T) { func TestOptions(t *testing.T) { m := &Mail{} - var o Option - o = Sendmail("/foo/bar") + o := Sendmail("/foo/bar") if o.execute(m); m.sendmail != "/foo/bar" { t.Errorf("Expected sendmail to be %q, got %q", "/foo/bar", m.sendmail) } diff --git a/sendmail.go b/sendmail.go index b94f8d9..d6b8f62 100644 --- a/sendmail.go +++ b/sendmail.go @@ -11,13 +11,10 @@ import ( "mime" "net/http" "net/mail" - "os" "os/exec" "strings" ) -var _, debug = os.LookupEnv("DEBUG") - // SendmailDefault points to the default sendmail binary location. const SendmailDefault = "/usr/sbin/sendmail" @@ -66,8 +63,7 @@ func (m *Mail) Send() error { } m.Header.Set("To", strings.Join(to, ", ")) if m.debugOut != nil { - m.WriteTo(m.debugOut) - return nil + return m.WriteTo(m.debugOut) } return m.exec(arg...) @@ -109,7 +105,7 @@ func (m *Mail) exec(arg ...string) error { } // WriteTo writes headers and content of the email to io.Writer -func (m *Mail) WriteTo(wr io.Writer) error { +func (m *Mail) WriteTo(wr io.Writer) error { //nolint: vet isText := m.Text.Len() > 0 isHTML := m.HTML.Len() > 0 diff --git a/sendmail_test.go b/sendmail_test.go index 2ff140c..4b5fe90 100644 --- a/sendmail_test.go +++ b/sendmail_test.go @@ -77,8 +77,7 @@ func TestTextMail(t *testing.T) { t.Errorf("Error writing to buffer: %v", err) } if actual := buf.String(); actual != expected { - fmt.Fprintln(os.Stderr, actual) - t.Errorf("Unexpected mail content") + t.Error("Unexpected mail content", actual) } } diff --git a/validate_test.go b/validate_test.go index 11ab083..b663edf 100644 --- a/validate_test.go +++ b/validate_test.go @@ -22,9 +22,9 @@ func TestValidate(tc *testing.T) { e := email t.Parallel() err := Validate(e.Address) - if err == nil && e.IsValid == false { + if err == nil && !e.IsValid { t.Errorf("Email `%s` is valid, but should be invalid", e.Address) - } else if err != nil && e.IsValid == true { + } else if err != nil && e.IsValid { t.Errorf("Email `%s` is invalid, but should be valid", e.Address) } }) From 4c2b75ddb6a15b9268e9d6f79b277d91760aaa12 Mon Sep 17 00:00:00 2001 From: Dominik Menke Date: Fri, 6 Apr 2018 17:51:15 +0200 Subject: [PATCH 09/15] Allow additional arguments to be passed to the sendmail program --- options.go | 12 +++++++----- options_test.go | 17 ++++++++++------- sendmail.go | 16 +++++++++------- sendmail_test.go | 4 ++-- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/options.go b/options.go index 28bfd32..68b3db9 100644 --- a/options.go +++ b/options.go @@ -6,9 +6,11 @@ import ( "os" ) -// SetSendmail modifies the path to the sendmail binary. -func (m *Mail) SetSendmail(path string) *Mail { - m.sendmail = path +// SetSendmail modifies the path to the sendmail binary. You can pass +// additional arguments, if you need to. +func (m *Mail) SetSendmail(path string, args ...string) *Mail { + m.sendmailPath = path + m.sendmailArgs = args return m } @@ -61,8 +63,8 @@ type optionFunc func(*Mail) func (o optionFunc) execute(m *Mail) { o(m) } // Sendmail modifies the path to the sendmail binary. -func Sendmail(path string) Option { - return optionFunc(func(m *Mail) { m.SetSendmail(path) }) +func Sendmail(path string, args ...string) Option { + return optionFunc(func(m *Mail) { m.SetSendmail(path, args...) }) } // Debug sets the debug output to stderr if active is true, else it diff --git a/options_test.go b/options_test.go index 9361bb5..f736587 100644 --- a/options_test.go +++ b/options_test.go @@ -23,8 +23,8 @@ func TestChaningOptions(t *testing.T) { if m.From != nil { t.Errorf("Expected From address to be nil, got %s", m.From) } - if m.sendmail != "" { - t.Errorf("Expected initial sendmail to be empty, got %q", m.sendmail) + if m.sendmailPath != "" { + t.Errorf("Expected initial sendmail to be empty, got %q", m.sendmailPath) } if m.debugOut != nil { t.Errorf("Expected initial debugOut to be nil, got %T", m.debugOut) @@ -46,8 +46,8 @@ func TestChaningOptions(t *testing.T) { expected := mail.Address{Name: "Dominik", Address: "dominik@example.org"} t.Errorf("Expected From address to be %s, got %s", expected, m.From) } - if m.sendmail != "/bin/true" { - t.Errorf("Expected sendmail to be %q, got %q", "/bin/true", m.sendmail) + if m.sendmailPath != "/bin/true" { + t.Errorf("Expected sendmail to be %q, got %q", "/bin/true", m.sendmailPath) } if m.debugOut != &buf { t.Errorf("Expected debugOut to be %T (buf), got %T", &buf, m.debugOut) @@ -57,9 +57,12 @@ func TestChaningOptions(t *testing.T) { func TestOptions(t *testing.T) { m := &Mail{} - o := Sendmail("/foo/bar") - if o.execute(m); m.sendmail != "/foo/bar" { - t.Errorf("Expected sendmail to be %q, got %q", "/foo/bar", m.sendmail) + o := Sendmail("/foo/bar", "--verbose") + if o.execute(m); m.sendmailPath != "/foo/bar" { + t.Errorf("Expected sendmail to be %q, got %q", "/foo/bar", m.sendmailPath) + } + if len(m.sendmailArgs) != 1 || m.sendmailArgs[0] != "--verbose" { + t.Errorf("Expected sendmail args to be %q, got %v", "--verbose", m.sendmailArgs) } o = Debug(true) diff --git a/sendmail.go b/sendmail.go index d6b8f62..20965c3 100644 --- a/sendmail.go +++ b/sendmail.go @@ -27,13 +27,14 @@ type Mail struct { Text bytes.Buffer HTML bytes.Buffer - sendmail string - debugOut io.Writer + sendmailPath string + sendmailArgs []string + debugOut io.Writer } // New creates a new Mail instance with the given options. func New(options ...Option) (m *Mail) { - m = &Mail{sendmail: SendmailDefault} + m = &Mail{sendmailPath: SendmailDefault} for _, option := range options { option.execute(m) } @@ -72,10 +73,11 @@ func (m *Mail) Send() error { // exec handles sendmail command invokation. func (m *Mail) exec(arg ...string) error { bin := SendmailDefault - if m.sendmail != "" { - bin = m.sendmail + if m.sendmailPath != "" { + bin = m.sendmailPath } - cmd := exec.Command(bin, arg...) + args := append(append([]string{}, m.sendmailArgs...), arg...) + cmd := exec.Command(bin, args...) stdin, err := cmd.StdinPipe() if err != nil { @@ -105,7 +107,7 @@ func (m *Mail) exec(arg ...string) error { } // WriteTo writes headers and content of the email to io.Writer -func (m *Mail) WriteTo(wr io.Writer) error { //nolint: vet +func (m *Mail) WriteTo(wr io.Writer) error { // nolint: vet isText := m.Text.Len() > 0 isHTML := m.HTML.Len() > 0 diff --git a/sendmail_test.go b/sendmail_test.go index 4b5fe90..323ab42 100644 --- a/sendmail_test.go +++ b/sendmail_test.go @@ -149,8 +149,8 @@ func TestNew(t *testing.T) { expected := mail.Address{Name: "Dominik", Address: "dominik@example.org"} t.Errorf("Expected From address to be %s, got %s", expected, m.From) } - if m.sendmail != "/bin/true" { - t.Errorf("Expected sendmail to be %q, got %q", "/bin/true", m.sendmail) + if m.sendmailPath != "/bin/true" { + t.Errorf("Expected sendmail to be %q, got %q", "/bin/true", m.sendmailPath) } if m.debugOut != &buf { t.Errorf("Expected debugOut to be %T (buf), got %T", &buf, m.debugOut) From e2cabaf0d93fd037e857603a8120b2011b87933f Mon Sep 17 00:00:00 2001 From: Dominik Menke Date: Mon, 9 Apr 2018 12:22:23 +0200 Subject: [PATCH 10/15] Breaking: Satisfy govet and change Mail.WriteTo signature WriteTo now returns (int64, error); however the returned number of bytes written is always 0, since we don't get the actual number easily. --- sendmail.go | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/sendmail.go b/sendmail.go index 20965c3..13c2d76 100644 --- a/sendmail.go +++ b/sendmail.go @@ -64,7 +64,8 @@ func (m *Mail) Send() error { } m.Header.Set("To", strings.Join(to, ", ")) if m.debugOut != nil { - return m.WriteTo(m.debugOut) + _, err := m.WriteTo(m.debugOut) + return err } return m.exec(arg...) @@ -90,7 +91,7 @@ func (m *Mail) exec(arg ...string) error { if err = cmd.Start(); err != nil { return err } - if err = m.WriteTo(stdin); err != nil { + if _, err = m.WriteTo(stdin); err != nil { return err } if err = stdin.Close(); err != nil { @@ -107,12 +108,12 @@ func (m *Mail) exec(arg ...string) error { } // WriteTo writes headers and content of the email to io.Writer -func (m *Mail) WriteTo(wr io.Writer) error { // nolint: vet +func (m *Mail) WriteTo(wr io.Writer) (int64, error) { isText := m.Text.Len() > 0 isHTML := m.HTML.Len() > 0 if isText && isHTML { - return fmt.Errorf("Multipart mails are not supported yet") + return 0, fmt.Errorf("Multipart mails are not supported yet") } else if isHTML { m.Header.Set("Content-Type", "text/html; charset=UTF-8") } else { @@ -122,22 +123,22 @@ func (m *Mail) WriteTo(wr io.Writer) error { // nolint: vet // write header if err := m.Header.Write(wr); err != nil { - return err + return 0, err } if _, err := wr.Write([]byte("\r\n")); err != nil { - return err + return 0, err } if isText && isHTML { // TODO } else if isHTML { if _, err := m.HTML.WriteTo(wr); err != nil { - return err + return 0, err } - } else if isText { + } else { if _, err := m.Text.WriteTo(wr); err != nil { - return err + return 0, err } } - return nil + return 0, nil } From 012d27dba313839c073d6bf4d67bb144a48fc30b Mon Sep 17 00:00:00 2001 From: Dominik Menke Date: Mon, 9 Apr 2018 12:30:05 +0200 Subject: [PATCH 11/15] Use *mail.Address as argument for {Append,}To and {Set,}From --- options.go | 30 ++++++++++++------------------ options_test.go | 12 ++++++------ sendmail_test.go | 20 ++++++++++---------- 3 files changed, 28 insertions(+), 34 deletions(-) diff --git a/options.go b/options.go index 68b3db9..7bd9500 100644 --- a/options.go +++ b/options.go @@ -32,18 +32,15 @@ func (m *Mail) SetDebugOutput(w io.Writer) *Mail { return m } -// AppendTo adds a recipient to the Mail. The name argument is the -// "proper name" of the recipient and may be empty. The address must be -// in the form "user@domain". -func (m *Mail) AppendTo(name, address string) *Mail { - m.To = append(m.To, &mail.Address{Name: name, Address: address}) +// AppendTo adds a recipient to the Mail. +func (m *Mail) AppendTo(toAddress *mail.Address) *Mail { + m.To = append(m.To, toAddress) return m } -// SetFrom updates the sender's address. Like AppendTo(), name may be -// empty, and address must be in the form "user@domain". -func (m *Mail) SetFrom(name, address string) *Mail { - m.From = &mail.Address{Name: name, Address: address} +// SetFrom updates (replaces) the sender's address. +func (m *Mail) SetFrom(fromAddress *mail.Address) *Mail { + m.From = fromAddress return m } @@ -79,17 +76,14 @@ func DebugOutput(w io.Writer) Option { return optionFunc(func(m *Mail) { m.SetDebugOutput(w) }) } -// To adds a recipient to the Mail. The name argument is the "proper name" -// of the recipient and may be empty. The address must be in the form -// "user@domain". -func To(name, address string) Option { - return optionFunc(func(m *Mail) { m.AppendTo(name, address) }) +// To adds a recipient to the Mail. +func To(address *mail.Address) Option { + return optionFunc(func(m *Mail) { m.AppendTo(address) }) } -// From updates the sender's address. Like To(), name may be empty, and -// address must be in the form "user@domain". -func From(name, address string) Option { - return optionFunc(func(m *Mail) { m.SetFrom(name, address) }) +// From sets the sender's address. +func From(fromAddress *mail.Address) Option { + return optionFunc(func(m *Mail) { m.SetFrom(fromAddress) }) } // Subject sets the mail subject. diff --git a/options_test.go b/options_test.go index f736587..1e4fbe0 100644 --- a/options_test.go +++ b/options_test.go @@ -31,8 +31,8 @@ func TestChaningOptions(t *testing.T) { } m.SetSubject("Test subject"). - SetFrom("Dominik", "dominik@example.org"). - AppendTo("Dominik2", "dominik2@example.org"). + SetFrom(&mail.Address{Name: "Dominik", Address: "dominik@example.org"}). + AppendTo(&mail.Address{Name: "Dominik2", Address: "dominik2@example.org"}). SetDebugOutput(&buf). SetSendmail("/bin/true") @@ -87,22 +87,22 @@ func TestOptions(t *testing.T) { } // To() appends list - o = To("Ktoś", "info@example.com") + o = To(&mail.Address{Name: "Ktoś", Address: "info@example.com"}) if o.execute(m); len(m.To) != 1 { t.Errorf("Expected len(To) to be 1, got %d: %+v", len(m.To), m.To) } - o = To("Ktoś2", "info2@example.com") + o = To(&mail.Address{Name: "Ktoś2", Address: "info2@example.com"}) if o.execute(m); len(m.To) != 2 { t.Errorf("Expected len(To) to be 2, got %d: %+v", len(m.To), m.To) } // From() updates current sender - o = From("Michał", "me@example.com") + o = From(&mail.Address{Name: "Michał", Address: "me@example.com"}) if o.execute(m); m.From == nil || m.From.Address != "me@example.com" { expected := mail.Address{Name: "Michał", Address: "me@example.com"} t.Errorf("Expected From address to be %s, got %s", expected, m.From) } - o = From("Michał", "me@example.com") + o = From(&mail.Address{Name: "Michał", Address: "me@example.com"}) if o.execute(m); m.From == nil || m.From.Address != "me@example.com" { expected := mail.Address{Name: "Michał", Address: "me@example.com"} t.Errorf("Expected From address to be %s, got %s", expected, m.From) diff --git a/sendmail_test.go b/sendmail_test.go index 323ab42..af13d08 100644 --- a/sendmail_test.go +++ b/sendmail_test.go @@ -56,9 +56,9 @@ func TestTextMail(t *testing.T) { var buf bytes.Buffer sm := New( Subject("Cześć"), - From("Michał", "me@"+domain), - To("Ktoś", "info@"+domain), - To("Ktoś2", "info2@"+domain), + From(maddr("Michał", "me@")), + To(maddr("Ktoś", "info@")), + To(maddr("Ktoś2", "info2@")), DebugOutput(&buf), ) io.WriteString(&sm.Text, ":)\r\n") @@ -85,9 +85,9 @@ func TestHTMLMail(t *testing.T) { var buf bytes.Buffer sm := New( Subject("Cześć"), - From("Michał", "me@"+domain), - To("Ktoś", "info@"+domain), - To("Ktoś2", "info2@"+domain), + From(maddr("Michał", "me@")), + To(maddr("Ktoś", "info@")), + To(maddr("Ktoś2", "info2@")), DebugOutput(&buf), ) io.WriteString(&sm.HTML, "

:)

\r\n") @@ -133,8 +133,8 @@ func TestNew(t *testing.T) { var buf bytes.Buffer m := New( Subject("Test subject"), - From("Dominik", "dominik@example.org"), - To("Dominik2", "dominik2@example.org"), + From(maddr("Dominik", "dominik@")), + To(maddr("Dominik2", "dominik2@")), DebugOutput(&buf), Sendmail("/bin/true"), ) @@ -145,8 +145,8 @@ func TestNew(t *testing.T) { if len(m.To) != 1 { t.Errorf("Expected len(To) to be 1, got %d: %+v", len(m.To), m.To) } - if m.From == nil || m.From.Address != "dominik@example.org" { - expected := mail.Address{Name: "Dominik", Address: "dominik@example.org"} + if m.From == nil || m.From.Address != "dominik@example.com" { + expected := mail.Address{Name: "Dominik", Address: "dominik@example.com"} t.Errorf("Expected From address to be %s, got %s", expected, m.From) } if m.sendmailPath != "/bin/true" { From 98795e582487d4bf0326934b2e8e18ebba35a927 Mon Sep 17 00:00:00 2001 From: Dominik Menke Date: Mon, 9 Apr 2018 12:52:01 +0200 Subject: [PATCH 12/15] Fix: Mail.WriteTo now returns the number of bytes written --- sendmail.go | 43 ++++++++++++++++++++++++++++++++----------- 1 file changed, 32 insertions(+), 11 deletions(-) diff --git a/sendmail.go b/sendmail.go index 13c2d76..d117063 100644 --- a/sendmail.go +++ b/sendmail.go @@ -108,12 +108,13 @@ func (m *Mail) exec(arg ...string) error { } // WriteTo writes headers and content of the email to io.Writer -func (m *Mail) WriteTo(wr io.Writer) (int64, error) { +func (m *Mail) WriteTo(wr io.Writer) (n int64, err error) { isText := m.Text.Len() > 0 isHTML := m.HTML.Len() > 0 if isText && isHTML { - return 0, fmt.Errorf("Multipart mails are not supported yet") + err = fmt.Errorf("Multipart mails are not supported yet") + return } else if isHTML { m.Header.Set("Content-Type", "text/html; charset=UTF-8") } else { @@ -121,24 +122,44 @@ func (m *Mail) WriteTo(wr io.Writer) (int64, error) { m.Header.Set("Content-Type", "text/plain; charset=UTF-8") } + w := &writeCounter{w: wr} + // write header - if err := m.Header.Write(wr); err != nil { - return 0, err + if err = m.Header.Write(w); err != nil { + return } - if _, err := wr.Write([]byte("\r\n")); err != nil { - return 0, err + if _, err = w.Write([]byte("\r\n")); err != nil { + return } if isText && isHTML { // TODO } else if isHTML { - if _, err := m.HTML.WriteTo(wr); err != nil { - return 0, err + if _, err = m.HTML.WriteTo(w); err != nil { + return } } else { - if _, err := m.Text.WriteTo(wr); err != nil { - return 0, err + if _, err = m.Text.WriteTo(w); err != nil { + return } } - return 0, nil + return w.n, nil +} + +// writeCounter is an internal type wrapping an io.Writer to work around +// the issue of net.http.Header.Write() not returning the number of bytes +// written. +type writeCounter struct { + n int64 + w io.Writer +} + +// Write satisfies the io.Writer interface. It updates an internal cache +// for the number of bytes written. +func (wc *writeCounter) Write(p []byte) (n int, err error) { + n, err = wc.w.Write(p) + if err == nil { + wc.n += int64(n) + } + return } From 586204b02cc203a184515d4c41d2ff8cc7ce847a Mon Sep 17 00:00:00 2001 From: Dominik Menke Date: Mon, 9 Apr 2018 12:52:29 +0200 Subject: [PATCH 13/15] Tests for Mail.WriteTo --- sendmail.go | 5 ++++- sendmail_test.go | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/sendmail.go b/sendmail.go index d117063..83f94dc 100644 --- a/sendmail.go +++ b/sendmail.go @@ -34,7 +34,10 @@ type Mail struct { // New creates a new Mail instance with the given options. func New(options ...Option) (m *Mail) { - m = &Mail{sendmailPath: SendmailDefault} + m = &Mail{ + Header: make(http.Header), + sendmailPath: SendmailDefault, + } for _, option := range options { option.execute(m) } diff --git a/sendmail_test.go b/sendmail_test.go index af13d08..1ebf6c2 100644 --- a/sendmail_test.go +++ b/sendmail_test.go @@ -111,6 +111,26 @@ func TestHTMLMail(t *testing.T) { } } +func TestWriteTo(t *testing.T) { + var buf bytes.Buffer + sm := New( + Subject("Cześć"), + From(maddr("Michał", "me@")), + To(maddr("Ktoś", "info@")), + To(maddr("Ktoś2", "info2@")), + DebugOutput(&buf), + ) + io.WriteString(&sm.Text, ":)\r\n") + + actual, err := sm.WriteTo(&buf) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + if expected := int64(buf.Len()); actual != expected { + t.Errorf("expeted to have written %d bytes, got %d", expected, actual) + } +} + func TestFromError(t *testing.T) { sm := Mail{ To: []*mail.Address{maddr("Ktoś", "info@")}, From 67a6581efceaa67aed3f998ce4765aad0a08928d Mon Sep 17 00:00:00 2001 From: Dominik Menke Date: Thu, 2 Aug 2018 15:27:34 +0200 Subject: [PATCH 14/15] AppendCC and AppendBCC --- options.go | 12 ++++++++++++ sendmail.go | 18 ++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/options.go b/options.go index 7bd9500..2288fed 100644 --- a/options.go +++ b/options.go @@ -38,6 +38,18 @@ func (m *Mail) AppendTo(toAddress *mail.Address) *Mail { return m } +// AppendCC adds a carbon-copy recipient to the Mail. +func (m *Mail) AppendCC(ccAddress *mail.Address) *Mail { + m.CC = append(m.CC, ccAddress) + return m +} + +// AppendBCC adds a blind carbon-copy recipient to the Mail. +func (m *Mail) AppendBCC(bccAddress *mail.Address) *Mail { + m.BCC = append(m.BCC, bccAddress) + return m +} + // SetFrom updates (replaces) the sender's address. func (m *Mail) SetFrom(fromAddress *mail.Address) *Mail { m.From = fromAddress diff --git a/sendmail.go b/sendmail.go index 83f94dc..aab19fc 100644 --- a/sendmail.go +++ b/sendmail.go @@ -23,6 +23,8 @@ type Mail struct { Subject string From *mail.Address To []*mail.Address + CC []*mail.Address + BCC []*mail.Address Header http.Header Text bytes.Buffer HTML bytes.Buffer @@ -66,6 +68,14 @@ func (m *Mail) Send() error { arg[i] = t.Address } m.Header.Set("To", strings.Join(to, ", ")) + + if cc := concatAddresses(m.CC); cc != "" { + m.Header.Set("CC", cc) + } + if bcc := concatAddresses(m.BCC); bcc != "" { + m.Header.Set("BCC", bcc) + } + if m.debugOut != nil { _, err := m.WriteTo(m.debugOut) return err @@ -74,6 +84,14 @@ func (m *Mail) Send() error { return m.exec(arg...) } +func concatAddresses(list []*mail.Address) string { + buf := make([]string, 0, len(list)) + for _, addr := range list { + buf = append(buf, addr.String()) + } + return strings.Join(buf, ", ") +} + // exec handles sendmail command invokation. func (m *Mail) exec(arg ...string) error { bin := SendmailDefault From 8710b05febb8aa64386ffed07eec6b41674e1dd7 Mon Sep 17 00:00:00 2001 From: Dominik Menke Date: Thu, 2 Aug 2018 15:28:26 +0200 Subject: [PATCH 15/15] Allow Append{To,CC,BCC} to receive multiple addresses --- options.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/options.go b/options.go index 2288fed..6bc30a1 100644 --- a/options.go +++ b/options.go @@ -33,20 +33,20 @@ func (m *Mail) SetDebugOutput(w io.Writer) *Mail { } // AppendTo adds a recipient to the Mail. -func (m *Mail) AppendTo(toAddress *mail.Address) *Mail { - m.To = append(m.To, toAddress) +func (m *Mail) AppendTo(toAddress ...*mail.Address) *Mail { + m.To = append(m.To, toAddress...) return m } // AppendCC adds a carbon-copy recipient to the Mail. -func (m *Mail) AppendCC(ccAddress *mail.Address) *Mail { - m.CC = append(m.CC, ccAddress) +func (m *Mail) AppendCC(ccAddress ...*mail.Address) *Mail { + m.CC = append(m.CC, ccAddress...) return m } // AppendBCC adds a blind carbon-copy recipient to the Mail. -func (m *Mail) AppendBCC(bccAddress *mail.Address) *Mail { - m.BCC = append(m.BCC, bccAddress) +func (m *Mail) AppendBCC(bccAddress ...*mail.Address) *Mail { + m.BCC = append(m.BCC, bccAddress...) return m }