diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..280d268 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,37 @@ +name: Build and Test + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + +jobs: + + build: + name: Build + runs-on: ubuntu-latest + steps: + + - name: Set up Go 1.x + uses: actions/setup-go@v2 + with: + go-version: ^1.13 + id: go + + - name: Check out code into the Go module directory + uses: actions/checkout@v2 + + - name: Get dependencies + run: | + go get -v -t -d ./... + if [ -f Gopkg.toml ]; then + curl https://raw.githubusercontent.com/golang/dep/master/install.sh | sh + dep ensure + fi + + - name: Build + run: go build -v . + + - name: Test + run: go test -v . diff --git a/.gitignore b/.gitignore index 0026861..7de90cf 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,6 @@ _cgo_export.* _testmain.go *.exe + +# IDEs +.idea diff --git a/README.md b/README.md index 55d501a..47c15f7 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,12 @@ e := NewEmail() e.AttachFile("test.txt") ``` +#### Attaching a File with custom file name +```go +e := NewEmail() +e.AttachFileWithName("internalName.txt", "publicName.txt") +``` + #### A Pool of Reusable Connections ```go (var ch <-chan *email.Email) diff --git a/email.go b/email.go index a8069a3..d296fc1 100644 --- a/email.go +++ b/email.go @@ -95,18 +95,47 @@ func NewEmailFromReader(r io.Reader) (*Email, error) { switch { case h == "Subject": e.Subject = v[0] + subj, err := (&mime.WordDecoder{}).DecodeHeader(e.Subject) + if err == nil && len(subj) > 0 { + e.Subject = subj + } delete(hdrs, h) case h == "To": - e.To = v + for _, to := range v { + tt, err := (&mime.WordDecoder{}).DecodeHeader(to) + if err == nil { + e.To = append(e.To, tt) + } else { + e.To = append(e.To, to) + } + } delete(hdrs, h) case h == "Cc": - e.Cc = v + for _, cc := range v { + tcc, err := (&mime.WordDecoder{}).DecodeHeader(cc) + if err == nil { + e.Cc = append(e.Cc, tcc) + } else { + e.Cc = append(e.Cc, cc) + } + } delete(hdrs, h) case h == "Bcc": - e.Bcc = v + for _, bcc := range v { + tbcc, err := (&mime.WordDecoder{}).DecodeHeader(bcc) + if err == nil { + e.Bcc = append(e.Bcc, tbcc) + } else { + e.Bcc = append(e.Bcc, bcc) + } + } delete(hdrs, h) case h == "From": e.From = v[0] + fr, err := (&mime.WordDecoder{}).DecodeHeader(e.From) + if err == nil && len(fr) > 0 { + e.From = fr + } delete(hdrs, h) } } @@ -125,6 +154,20 @@ func NewEmailFromReader(r io.Reader) (*Email, error) { if err != nil { return e, err } + // Check if part is an attachment based on the existence of the Content-Disposition header with a value of "attachment". + if cd := p.header.Get("Content-Disposition"); cd != "" { + cd, params, err := mime.ParseMediaType(p.header.Get("Content-Disposition")) + if err != nil { + return e, err + } + if cd == "attachment" { + _, err = e.Attach(bytes.NewReader(p.body), params["filename"], ct) + if err != nil { + return e, err + } + continue + } + } switch { case ct == "text/plain": e.Text = p.body @@ -194,6 +237,10 @@ func parseMIMEParts(hs textproto.MIMEHeader, b io.Reader) ([]*part, error) { } } else { // If it is not a multipart email, parse the body content as a single "part" + if hs.Get("Content-Transfer-Encoding") == "quoted-printable" { + b = quotedprintable.NewReader(b) + + } var buf bytes.Buffer if _, err := io.Copy(&buf, b); err != nil { return ps, err @@ -216,11 +263,9 @@ func (e *Email) Attach(r io.Reader, filename string, c string) (a *Attachment, e Header: textproto.MIMEHeader{}, Content: buffer.Bytes(), } - // Get the Content-Type to be used in the MIMEHeader if c != "" { at.Header.Set("Content-Type", c) } else { - // If the Content-Type is blank, set the Content-Type to "application/octet-stream" at.Header.Set("Content-Type", "application/octet-stream") } at.Header.Set("Content-Disposition", fmt.Sprintf("attachment;\r\n filename=\"%s\"", filename)) @@ -246,6 +291,21 @@ func (e *Email) AttachFile(filename string) (a *Attachment, err error) { return e.Attach(f, basename, ct) } +// AttachFileWithName is used to attach content to the email. +// It attempts to open the file referenced by filePath and, if successful, creates an Attachment with the provided fileName. +// This Attachment is then appended to the slice of Email.Attachments. +// The function will then return the Attachment for reference, as well as nil for the error, if successful. +func (e *Email) AttachFileWithName(filePath, fileName string) (a *Attachment, err error) { + f, err := os.Open(filePath) + if err != nil { + return + } + defer f.Close() + + ct := mime.TypeByExtension(filepath.Ext(filePath)) + return e.Attach(f, fileName, ct) +} + // msgHeaders merges the Email's various fields and custom headers together in a // standards compliant way to create a MIMEHeader to be used in the resulting // message. It does not alter e.Headers. @@ -253,7 +313,7 @@ func (e *Email) AttachFile(filename string) (a *Attachment, err error) { // "e"'s fields To, Cc, From, Subject will be used unless they are present in // e.Headers. Unless set in e.Headers, "Date" will filled with the current time. func (e *Email) msgHeaders() (textproto.MIMEHeader, error) { - res := make(textproto.MIMEHeader, len(e.Headers)+4) + res := make(textproto.MIMEHeader, len(e.Headers)+6) if e.Headers != nil { for _, h := range []string{"Reply-To", "To", "Cc", "From", "Subject", "Date", "Message-Id", "MIME-Version"} { if v, ok := e.Headers[h]; ok { @@ -318,6 +378,17 @@ func writeMessage(buff io.Writer, msg []byte, multipart bool, mediaType string, return qp.Close() } +func (e *Email) categorizeAttachments() (htmlRelated, others []*Attachment) { + for _, a := range e.Attachments { + if a.HTMLRelated { + htmlRelated = append(htmlRelated, a) + } else { + others = append(others, a) + } + } + return +} + // Bytes converts the Email object to a []byte representation, including all needed MIMEHeaders, boundaries, etc. func (e *Email) Bytes() ([]byte, error) { // TODO: better guess buffer size @@ -328,8 +399,13 @@ func (e *Email) Bytes() ([]byte, error) { return nil, err } + htmlAttachments, otherAttachments := e.categorizeAttachments() + if len(e.HTML) == 0 && len(htmlAttachments) > 0 { + return nil, errors.New("there are HTML attachments, but no HTML body") + } + var ( - isMixed = len(e.Attachments) > 0 + isMixed = len(otherAttachments) > 0 isAlternative = len(e.Text) > 0 && len(e.HTML) > 0 ) @@ -379,10 +455,35 @@ func (e *Email) Bytes() ([]byte, error) { } } if len(e.HTML) > 0 { + messageWriter := subWriter + var relatedWriter *multipart.Writer + if len(htmlAttachments) > 0 { + relatedWriter = multipart.NewWriter(buff) + header := textproto.MIMEHeader{ + "Content-Type": {"multipart/related;\r\n boundary=" + relatedWriter.Boundary()}, + } + if _, err := subWriter.CreatePart(header); err != nil { + return nil, err + } + + messageWriter = relatedWriter + } // Write the HTML - if err := writeMessage(buff, e.HTML, isMixed || isAlternative, "text/html", subWriter); err != nil { + if err := writeMessage(buff, e.HTML, isMixed || isAlternative, "text/html", messageWriter); err != nil { return nil, err } + if len(htmlAttachments) > 0 { + for _, a := range htmlAttachments { + ap, err := relatedWriter.CreatePart(a.Header) + if err != nil { + return nil, err + } + // Write the base64Wrapped content to the part + base64Wrap(ap, a.Content) + } + + relatedWriter.Close() + } } if isMixed && isAlternative { if err := subWriter.Close(); err != nil { @@ -391,7 +492,7 @@ func (e *Email) Bytes() ([]byte, error) { } } // Create attachment part, if necessary - for _, a := range e.Attachments { + for _, a := range otherAttachments { ap, err := w.CreatePart(a.Header) if err != nil { return nil, err @@ -452,8 +553,9 @@ func (e *Email) parseSender() (string, error) { } } -// SendWithTLS sends an email with an optional TLS config. -// This is helpful if you need to connect to a host that is used an untrusted +// SendWithTLS sends an email over tls with an optional TLS config. +// +// The TLS Config is helpful if you need to connect to a host that is used an untrusted // certificate. func (e *Email) SendWithTLS(addr string, a smtp.Auth, t *tls.Config) error { // Merge the To, Cc, and Bcc fields @@ -492,6 +594,75 @@ func (e *Email) SendWithTLS(addr string, a smtp.Auth, t *tls.Config) error { if err = c.Hello("localhost"); err != nil { return err } + + if a != nil { + if ok, _ := c.Extension("AUTH"); ok { + if err = c.Auth(a); err != nil { + return err + } + } + } + if err = c.Mail(sender); err != nil { + return err + } + for _, addr := range to { + if err = c.Rcpt(addr); err != nil { + return err + } + } + w, err := c.Data() + if err != nil { + return err + } + _, err = w.Write(raw) + if err != nil { + return err + } + err = w.Close() + if err != nil { + return err + } + return c.Quit() +} + +// SendWithStartTLS sends an email over TLS using STARTTLS with an optional TLS config. +// +// The TLS Config is helpful if you need to connect to a host that is used an untrusted +// certificate. +func (e *Email) SendWithStartTLS(addr string, a smtp.Auth, t *tls.Config) error { + // Merge the To, Cc, and Bcc fields + to := make([]string, 0, len(e.To)+len(e.Cc)+len(e.Bcc)) + to = append(append(append(to, e.To...), e.Cc...), e.Bcc...) + for i := 0; i < len(to); i++ { + addr, err := mail.ParseAddress(to[i]) + if err != nil { + return err + } + to[i] = addr.Address + } + // Check to make sure there is at least one recipient and one "From" address + if e.From == "" || len(to) == 0 { + return errors.New("Must specify at least one From address and one To address") + } + sender, err := e.parseSender() + if err != nil { + return err + } + raw, err := e.Bytes() + if err != nil { + return err + } + + // Taken from the standard library + // https://github.com/golang/go/blob/master/src/net/smtp/smtp.go#L328 + c, err := smtp.Dial(addr) + if err != nil { + return err + } + defer c.Close() + if err = c.Hello("localhost"); err != nil { + return err + } // Use TLS if available if ok, _ := c.Extension("STARTTLS"); ok { if err = c.StartTLS(t); err != nil { @@ -532,9 +703,10 @@ func (e *Email) SendWithTLS(addr string, a smtp.Auth, t *tls.Config) error { // Attachment is a struct representing an email attachment. // Based on the mime/multipart.FileHeader struct, Attachment contains the name, MIMEHeader, and content of the attachment in question type Attachment struct { - Filename string - Header textproto.MIMEHeader - Content []byte + Filename string + Header textproto.MIMEHeader + Content []byte + HTMLRelated bool } // base64Wrap encodes the attachment content, and wraps it according to RFC 2045 standards (every 76 chars) @@ -572,6 +744,18 @@ func headerToBytes(buff io.Writer, header textproto.MIMEHeader) { switch { case field == "Content-Type" || field == "Content-Disposition": buff.Write([]byte(subval)) + case field == "From" || field == "To" || field == "Cc" || field == "Bcc": + participants := strings.Split(subval, ",") + for i, v := range participants { + addr, err := mail.ParseAddress(v) + if err != nil { + continue + } + if addr.Name != "" { + participants[i] = fmt.Sprintf("%s <%s>", mime.QEncoding.Encode("UTF-8", addr.Name), addr.Address) + } + } + buff.Write([]byte(strings.Join(participants, ", "))) default: buff.Write([]byte(mime.QEncoding.Encode("UTF-8", subval))) } diff --git a/email_test.go b/email_test.go index a95bf07..1e721bd 100644 --- a/email_test.go +++ b/email_test.go @@ -1,8 +1,11 @@ package email import ( + "fmt" + "strings" "testing" + "bufio" "bytes" "crypto/rand" "io" @@ -67,6 +70,74 @@ func TestEmailText(t *testing.T) { } } +func TestEmailWithHTMLAttachments(t *testing.T) { + e := prepareEmail() + + // Set plain text to exercise "mime/alternative" + e.Text = []byte("Text Body is, of course, supported!\n") + + e.HTML = []byte("This is a text.") + + // Set HTML attachment to exercise "mime/related". + attachment, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "image/png; charset=utf-8") + if err != nil { + t.Fatal("Could not add an attachment to the message: ", err) + } + attachment.HTMLRelated = true + + b, err := e.Bytes() + if err != nil { + t.Fatal("Could not serialize e-mail:", err) + } + + // Print the bytes for ocular validation and make sure no errors. + //fmt.Println(string(b)) + + // TODO: Verify the attachments. + s := trimReader{rd: bytes.NewBuffer(b)} + tp := textproto.NewReader(bufio.NewReader(s)) + // Parse the main headers + hdrs, err := tp.ReadMIMEHeader() + if err != nil { + t.Fatal("Could not parse the headers:", err) + } + // Recursively parse the MIME parts + ps, err := parseMIMEParts(hdrs, tp.R) + if err != nil { + t.Fatal("Could not parse the MIME parts recursively:", err) + } + + plainTextFound := false + htmlFound := false + imageFound := false + if expected, actual := 3, len(ps); actual != expected { + t.Error("Unexpected number of parts. Expected:", expected, "Was:", actual) + } + for _, part := range ps { + // part has "header" and "body []byte" + ct := part.header.Get("Content-Type") + if strings.Contains(ct, "image/png") { + imageFound = true + } + if strings.Contains(ct, "text/html") { + htmlFound = true + } + if strings.Contains(ct, "text/plain") { + plainTextFound = true + } + } + + if !plainTextFound { + t.Error("Did not find plain text part.") + } + if !htmlFound { + t.Error("Did not find HTML part.") + } + if !imageFound { + t.Error("Did not find image part.") + } +} + func TestEmailHTML(t *testing.T) { e := prepareEmail() e.HTML = []byte("

Fancy Html is supported, too!

\n") @@ -143,6 +214,73 @@ func TestEmailTextAttachment(t *testing.T) { } } +func TestEmailTextAttachmentWithName(t *testing.T) { + var ( + file = "go.mod" + fileName = "rad2.mod" + ) + e := prepareEmail() + e.Text = []byte("Text Body is, of course, supported!\n") + _, err := e.AttachFileWithName(file, fileName) + if err != nil { + t.Fatal("Could not add an attachment to the message: ", err) + } + + msg := basicTests(t, e) + + // Were the right headers set? + ct := msg.Header.Get("Content-type") + mt, params, err := mime.ParseMediaType(ct) + if err != nil { + t.Fatal("Content-type header is invalid: ", ct) + } else if mt != "multipart/mixed" { + t.Fatalf("Content-type expected \"multipart/mixed\", not %v", mt) + } + b := params["boundary"] + if b == "" { + t.Fatalf("Invalid or missing boundary parameter: %#v", b) + } + if len(params) != 1 { + t.Fatal("Unexpected content-type parameters") + } + + // Is the generated message parsable? + mixed := multipart.NewReader(msg.Body, params["boundary"]) + + text, err := mixed.NextPart() + if err != nil { + t.Fatalf("Could not find text component of email: %s", err) + } + + // Does the text portion match what we expect? + mt, _, err = mime.ParseMediaType(text.Header.Get("Content-type")) + if err != nil { + t.Fatal("Could not parse message's Content-Type") + } else if mt != "text/plain" { + t.Fatal("Message missing text/plain") + } + plainText, err := ioutil.ReadAll(text) + if err != nil { + t.Fatal("Could not read plain text component of message: ", err) + } + if !bytes.Equal(plainText, []byte("Text Body is, of course, supported!\r\n")) { + t.Fatalf("Plain text is broken: %#q", plainText) + } + + // Check attachments. + part, err := mixed.NextPart() + if err != nil { + t.Fatalf("Could not find attachment component of email: %s", err) + } + if !strings.Contains(part.FileName(), fileName) { + t.Fatalf("Could not find the matched attachement name required %s find %s", fileName, part.FileName()) + } + + if _, err = mixed.NextPart(); err != io.EOF { + t.Error("Expected only text and one attachment!") + } +} + func TestEmailTextHtmlAttachment(t *testing.T) { e := prepareEmail() e.Text = []byte("Text Body is, of course, supported!\n") @@ -247,6 +385,52 @@ func TestEmailAttachment(t *testing.T) { } } +func TestHeaderEncoding(t *testing.T) { + cases := []struct { + field string + have string + want string + }{ + { + field: "From", + have: "Needs Encóding , Only ASCII ", + want: "=?UTF-8?q?Needs_Enc=C3=B3ding?= , Only ASCII \r\n", + }, + { + field: "To", + have: "Keith Moore , Keld Jørn Simonsen ", + want: "Keith Moore , =?UTF-8?q?Keld_J=C3=B8rn_Simonsen?= \r\n", + }, + { + field: "Cc", + have: "Needs Encóding ", + want: "=?UTF-8?q?Needs_Enc=C3=B3ding?= \r\n", + }, + { + field: "Subject", + have: "Subject with a 🐟", + want: "=?UTF-8?q?Subject_with_a_=F0=9F=90=9F?=\r\n", + }, + { + field: "Subject", + have: "Subject with only ASCII", + want: "Subject with only ASCII\r\n", + }, + } + buff := &bytes.Buffer{} + for _, c := range cases { + header := make(textproto.MIMEHeader) + header.Add(c.field, c.have) + buff.Reset() + headerToBytes(buff, header) + want := fmt.Sprintf("%s: %s", c.field, c.want) + got := buff.String() + if got != want { + t.Errorf("invalid utf-8 header encoding. \nwant:%#v\ngot: %#v", want, got) + } + } +} + func TestEmailFromReader(t *testing.T) { ex := &Email{ Subject: "Test Subject", @@ -296,6 +480,41 @@ d-printable decoding. } +func TestNonAsciiEmailFromReader(t *testing.T) { + ex := &Email{ + Subject: "Test Subject", + To: []string{"Anaïs "}, + Cc: []string{"Patrik Fältström "}, + From: "Mrs Valérie Dupont ", + Text: []byte("This is a test message!"), + } + raw := []byte(` + MIME-Version: 1.0 +Subject: =?UTF-8?Q?Test Subject?= +From: Mrs =?ISO-8859-1?Q?Val=C3=A9rie=20Dupont?= +To: =?utf-8?q?Ana=C3=AFs?= +Cc: =?ISO-8859-1?Q?Patrik_F=E4ltstr=F6m?= +Content-type: text/plain; charset=ISO-8859-1 + +This is a test message!`) + e, err := NewEmailFromReader(bytes.NewReader(raw)) + if err != nil { + t.Fatalf("Error creating email %s", err.Error()) + } + if e.Subject != ex.Subject { + t.Fatalf("Incorrect subject. %#q != %#q", e.Subject, ex.Subject) + } + if e.From != ex.From { + t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From) + } + if e.To[0] != ex.To[0] { + t.Fatalf("Incorrect \"To\": %#q != %#q", e.To, ex.To) + } + if e.Cc[0] != ex.Cc[0] { + t.Fatalf("Incorrect \"Cc\": %#q != %#q", e.Cc, ex.Cc) + } +} + func TestNonMultipartEmailFromReader(t *testing.T) { ex := &Email{ Text: []byte("This is a test message!"), @@ -376,6 +595,81 @@ d-printable decoding. } } +func TestAttachmentEmailFromReader(t *testing.T) { + ex := &Email{ + Subject: "Test Subject", + To: []string{"Jordan Wright "}, + From: "Jordan Wright ", + Text: []byte("Simple text body"), + HTML: []byte("
Simple HTML body
\n"), + } + a, err := ex.Attach(bytes.NewReader([]byte("Let's just pretend this is raw JPEG data.")), "cat.jpeg", "image/jpeg") + if err != nil { + t.Fatalf("Error attaching image %s", err.Error()) + } + raw := []byte(` +From: Jordan Wright +Date: Thu, 17 Oct 2019 08:55:37 +0100 +Mime-Version: 1.0 +Content-Type: multipart/mixed; + boundary=35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7 +To: Jordan Wright +Subject: Test Subject + +--35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7 +Content-Type: multipart/alternative; + boundary=b10ca5b1072908cceb667e8968d3af04503b7ab07d61c9f579c15b416d7c + +--b10ca5b1072908cceb667e8968d3af04503b7ab07d61c9f579c15b416d7c +Content-Transfer-Encoding: quoted-printable +Content-Type: text/plain; charset=UTF-8 + +Simple text body +--b10ca5b1072908cceb667e8968d3af04503b7ab07d61c9f579c15b416d7c +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset=UTF-8 + +
Simple HTML body
+ +--b10ca5b1072908cceb667e8968d3af04503b7ab07d61c9f579c15b416d7c-- + +--35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7 +Content-Disposition: attachment; + filename="cat.jpeg" +Content-Id: +Content-Transfer-Encoding: base64 +Content-Type: image/jpeg + +TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4= + +--35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7--`) + e, err := NewEmailFromReader(bytes.NewReader(raw)) + if err != nil { + t.Fatalf("Error creating email %s", err.Error()) + } + if e.Subject != ex.Subject { + t.Fatalf("Incorrect subject. %#q != %#q", e.Subject, ex.Subject) + } + if !bytes.Equal(e.Text, ex.Text) { + t.Fatalf("Incorrect text: %#q != %#q", e.Text, ex.Text) + } + if !bytes.Equal(e.HTML, ex.HTML) { + t.Fatalf("Incorrect HTML: %#q != %#q", e.HTML, ex.HTML) + } + if e.From != ex.From { + t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From) + } + if len(e.Attachments) != 1 { + t.Fatalf("Incorrect number of attachments %d != %d", len(e.Attachments), 1) + } + if e.Attachments[0].Filename != a.Filename { + t.Fatalf("Incorrect attachment filename %s != %s", e.Attachments[0].Filename, a.Filename) + } + if !bytes.Equal(e.Attachments[0].Content, a.Content) { + t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[0].Content, a.Content) + } +} + func ExampleGmail() { e := NewEmail() e.From = "Jordan Wright " @@ -393,6 +687,11 @@ func ExampleAttach() { e.AttachFile("test.txt") } +func ExampleAttachFileWithNamed() { + e := NewEmail() + e.AttachFileWithName("test.txt", "test2") +} + func Test_base64Wrap(t *testing.T) { file := "I'm a file long enough to force the function to wrap a\n" + "couple of lines, but I stop short of the end of one line and\n" + diff --git a/go.mod b/go.mod index 5f33738..4a588fb 100644 --- a/go.mod +++ b/go.mod @@ -1 +1,3 @@ module github.com/jordan-wright/email + +go 1.13 diff --git a/pool.go b/pool.go index b4be6a3..67f224a 100644 --- a/pool.go +++ b/pool.go @@ -14,16 +14,17 @@ import ( ) type Pool struct { - addr string - auth smtp.Auth - max int - created int - clients chan *client - rebuild chan struct{} - mut *sync.Mutex - lastBuildErr *timestampedErr - closing chan struct{} - tlsConfig *tls.Config + addr string + auth smtp.Auth + max int + created int + clients chan *client + rebuild chan struct{} + mut *sync.Mutex + lastBuildErr *timestampedErr + closing chan struct{} + tlsConfig *tls.Config + helloHostname string } type client struct { @@ -68,6 +69,14 @@ func (c *client) Close() error { return c.Text.Close() } +// SetHelloHostname optionally sets the hostname that the Go smtp.Client will +// use when doing a HELLO with the upstream SMTP server. By default, Go uses +// "localhost" which may not be accepted by certain SMTP servers that demand +// an FQDN. +func (p *Pool) SetHelloHostname(h string) { + p.helloHostname = h +} + func (p *Pool) get(timeout time.Duration) *client { select { case c := <-p.clients: @@ -200,6 +209,12 @@ func (p *Pool) build() (*client, error) { if err != nil { return nil, err } + + // Is there a custom hostname for doing a HELLO with the SMTP server? + if p.helloHostname != "" { + cl.Hello(p.helloHostname) + } + c := &client{cl, 0} if _, err := startTLS(c, p.tlsConfig); err != nil {