Compare commits

...

10 Commits

Author SHA1 Message Date
lovezsh b65f60b163 feat: go mod support 2024-03-19 20:05:52 +08:00
Alexandre Cesaro 81ebce5c23 Fixed a typo 2016-04-11 23:29:32 +02:00
Alexandre Cesaro e4bd87ad6e Set a 10 seconds timeout in Dial 2016-04-11 23:24:34 +02:00
Alexandre Cesaro 92eaa13340 Made the error clearer when an address is invalid 2016-04-11 23:18:11 +02:00
slavikm 4291610152 Added an option to manually set filename of attachments
Fixes #55
Fixes #56
2016-04-01 10:05:10 +02:00
Alexandre Cesaro bd0e445b57 Do not insert a newline as the first character of a header
It is closer to Gmail behavior for example.
See #53
2016-03-30 19:05:19 +02:00
Alexandre Cesaro 84856b343c Fixed a bug when using an empty name in SetAddressHeader
Fixes #54
2016-03-20 17:20:50 +01:00
Alexandre Cesaro 060a5f4e98 Added automatic folding of long header lines
Fixes #53
2016-03-16 23:42:32 +01:00
Alexandre Cesaro afff51fd8c smtpSender.Send() now automatically redials in case of a timeout 2016-03-14 13:43:57 +01:00
Alexandre Cesaro 5ceb8e6541 Dialer.Dial() now automatically uses CRAM-MD5 when it's available
Also deprecated NewPlainDialer() in favor of NewDialer().

Fixes #52
2016-03-06 19:17:01 +01:00
13 changed files with 353 additions and 262 deletions

View File

@ -60,7 +60,7 @@ bypass the verification of the server's certificate chain and host name by using
) )
func main() { func main() {
d := gomail.NewPlainDialer("smtp.example.com", 587, "user", "123456") d := gomail.NewDialer("smtp.example.com", 587, "user", "123456")
d.TLSConfig = &tls.Config{InsecureSkipVerify: true} d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
// Send emails using d. // Send emails using d.

52
auth.go
View File

@ -7,51 +7,33 @@ import (
"net/smtp" "net/smtp"
) )
// plainAuth is an smtp.Auth that implements the PLAIN authentication mechanism. // loginAuth is an smtp.Auth that implements the LOGIN authentication mechanism.
// It fallbacks to the LOGIN mechanism if it is the only mechanism advertised type loginAuth struct {
// by the server.
type plainAuth struct {
username string username string
password string password string
host string host string
login bool
} }
func (a *plainAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
if !server.TLS {
advertised := false
for _, mechanism := range server.Auth {
if mechanism == "LOGIN" {
advertised = true
break
}
}
if !advertised {
return "", nil, errors.New("gomail: unencrypted connection")
}
}
if server.Name != a.host { if server.Name != a.host {
return "", nil, errors.New("gomail: wrong host name") return "", nil, errors.New("gomail: wrong host name")
} }
return "LOGIN", nil, nil
var plain, login bool
for _, a := range server.Auth {
switch a {
case "PLAIN":
plain = true
case "LOGIN":
login = true
}
}
if !server.TLS && !plain && !login {
return "", nil, errors.New("gomail: unencrypted connection")
}
if !plain && login {
a.login = true
return "LOGIN", nil, nil
}
return "PLAIN", []byte("\x00" + a.username + "\x00" + a.password), nil
} }
func (a *plainAuth) Next(fromServer []byte, more bool) ([]byte, error) { func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if !a.login {
if more {
return nil, errors.New("gomail: unexpected server challenge")
}
return nil, nil
}
if !more { if !more {
return nil, nil return nil, nil
} }

View File

@ -11,103 +11,51 @@ const (
testHost = "smtp.example.com" testHost = "smtp.example.com"
) )
var testAuth = &plainAuth{ type authTest struct {
username: testUser,
password: testPwd,
host: testHost,
}
type plainAuthTest struct {
auths []string auths []string
challenges []string challenges []string
tls bool tls bool
wantProto string
wantData []string wantData []string
wantError bool wantError bool
} }
func TestNoAdvertisement(t *testing.T) { func TestNoAdvertisement(t *testing.T) {
testPlainAuth(t, &plainAuthTest{ testLoginAuth(t, &authTest{
auths: []string{}, auths: []string{},
challenges: []string{"Username:", "Password:"}, tls: false,
tls: false, wantError: true,
wantProto: "PLAIN",
wantError: true,
}) })
} }
func TestNoAdvertisementTLS(t *testing.T) { func TestNoAdvertisementTLS(t *testing.T) {
testPlainAuth(t, &plainAuthTest{ testLoginAuth(t, &authTest{
auths: []string{}, auths: []string{},
challenges: []string{"Username:", "Password:"}, challenges: []string{"Username:", "Password:"},
tls: true, tls: true,
wantProto: "PLAIN", wantData: []string{"", testUser, testPwd},
wantData: []string{"\x00" + testUser + "\x00" + testPwd},
})
}
func TestPlain(t *testing.T) {
testPlainAuth(t, &plainAuthTest{
auths: []string{"PLAIN"},
challenges: []string{"Username:", "Password:"},
tls: false,
wantProto: "PLAIN",
wantData: []string{"\x00" + testUser + "\x00" + testPwd},
})
}
func TestPlainTLS(t *testing.T) {
testPlainAuth(t, &plainAuthTest{
auths: []string{"PLAIN"},
challenges: []string{"Username:", "Password:"},
tls: true,
wantProto: "PLAIN",
wantData: []string{"\x00" + testUser + "\x00" + testPwd},
})
}
func TestPlainAndLogin(t *testing.T) {
testPlainAuth(t, &plainAuthTest{
auths: []string{"PLAIN", "LOGIN"},
challenges: []string{"Username:", "Password:"},
tls: false,
wantProto: "PLAIN",
wantData: []string{"\x00" + testUser + "\x00" + testPwd},
})
}
func TestPlainAndLoginTLS(t *testing.T) {
testPlainAuth(t, &plainAuthTest{
auths: []string{"PLAIN", "LOGIN"},
challenges: []string{"Username:", "Password:"},
tls: true,
wantProto: "PLAIN",
wantData: []string{"\x00" + testUser + "\x00" + testPwd},
}) })
} }
func TestLogin(t *testing.T) { func TestLogin(t *testing.T) {
testPlainAuth(t, &plainAuthTest{ testLoginAuth(t, &authTest{
auths: []string{"LOGIN"}, auths: []string{"PLAIN", "LOGIN"},
challenges: []string{"Username:", "Password:"}, challenges: []string{"Username:", "Password:"},
tls: false, tls: false,
wantProto: "LOGIN",
wantData: []string{"", testUser, testPwd}, wantData: []string{"", testUser, testPwd},
}) })
} }
func TestLoginTLS(t *testing.T) { func TestLoginTLS(t *testing.T) {
testPlainAuth(t, &plainAuthTest{ testLoginAuth(t, &authTest{
auths: []string{"LOGIN"}, auths: []string{"LOGIN"},
challenges: []string{"Username:", "Password:"}, challenges: []string{"Username:", "Password:"},
tls: true, tls: true,
wantProto: "LOGIN",
wantData: []string{"", testUser, testPwd}, wantData: []string{"", testUser, testPwd},
}) })
} }
func testPlainAuth(t *testing.T, test *plainAuthTest) { func testLoginAuth(t *testing.T, test *authTest) {
auth := &plainAuth{ auth := &loginAuth{
username: testUser, username: testUser,
password: testPwd, password: testPwd,
host: testHost, host: testHost,
@ -119,13 +67,13 @@ func testPlainAuth(t *testing.T, test *plainAuthTest) {
} }
proto, toServer, err := auth.Start(server) proto, toServer, err := auth.Start(server)
if err != nil && !test.wantError { if err != nil && !test.wantError {
t.Fatalf("plainAuth.Start(): %v", err) t.Fatalf("loginAuth.Start(): %v", err)
} }
if err != nil && test.wantError { if err != nil && test.wantError {
return return
} }
if proto != test.wantProto { if proto != "LOGIN" {
t.Errorf("invalid protocol, got %q, want %q", proto, test.wantProto) t.Errorf("invalid protocol, got %q, want LOGIN", proto)
} }
i := 0 i := 0
@ -134,10 +82,6 @@ func testPlainAuth(t *testing.T, test *plainAuthTest) {
t.Errorf("Invalid response, got %q, want %q", got, test.wantData[i]) t.Errorf("Invalid response, got %q, want %q", got, test.wantData[i])
} }
if proto == "PLAIN" {
return
}
for _, challenge := range test.challenges { for _, challenge := range test.challenges {
i++ i++
if i >= len(test.wantData) { if i >= len(test.wantData) {
@ -146,7 +90,7 @@ func testPlainAuth(t *testing.T, test *plainAuthTest) {
toServer, err = auth.Next([]byte(challenge), true) toServer, err = auth.Next([]byte(challenge), true)
if err != nil { if err != nil {
t.Fatalf("plainAuth.Auth(): %v", err) t.Fatalf("loginAuth.Auth(): %v", err)
} }
got = string(toServer) got = string(toServer)
if got != test.wantData[i] { if got != test.wantData[i] {

View File

@ -5,7 +5,6 @@ import (
"html/template" "html/template"
"io" "io"
"log" "log"
"net/smtp"
"time" "time"
"gopkg.in/gomail.v2" "gopkg.in/gomail.v2"
@ -20,7 +19,7 @@ func Example() {
m.SetBody("text/html", "Hello <b>Bob</b> and <i>Cora</i>!") m.SetBody("text/html", "Hello <b>Bob</b> and <i>Cora</i>!")
m.Attach("/home/Alex/lolcat.jpg") m.Attach("/home/Alex/lolcat.jpg")
d := gomail.NewPlainDialer("smtp.example.com", 587, "user", "123456") d := gomail.NewDialer("smtp.example.com", 587, "user", "123456")
// Send the email to Bob, Cora and Dan. // Send the email to Bob, Cora and Dan.
if err := d.DialAndSend(m); err != nil { if err := d.DialAndSend(m); err != nil {
@ -33,7 +32,7 @@ func Example_daemon() {
ch := make(chan *gomail.Message) ch := make(chan *gomail.Message)
go func() { go func() {
d := gomail.NewPlainDialer("smtp.example.com", 587, "user", "123456") d := gomail.NewDialer("smtp.example.com", 587, "user", "123456")
var s gomail.SendCloser var s gomail.SendCloser
var err error var err error
@ -80,7 +79,7 @@ func Example_newsletter() {
Address string Address string
} }
d := gomail.NewPlainDialer("smtp.example.com", 587, "user", "123456") d := gomail.NewDialer("smtp.example.com", 587, "user", "123456")
s, err := d.Dial() s, err := d.Dial()
if err != nil { if err != nil {
panic(err) panic(err)
@ -114,24 +113,6 @@ func Example_noAuth() {
} }
} }
// Send an email using the CRAM-MD5 authentication mechanism.
func Example_cRAMMD5() {
m := gomail.NewMessage()
m.SetHeader("From", "from@example.com")
m.SetHeader("To", "to@example.com")
m.SetHeader("Subject", "Hello!")
m.SetBody("text/plain", "Hello!")
d := gomail.Dialer{
Host: "localhost",
Port: 587,
Auth: smtp.CRAMMD5Auth("username", "secret"),
}
if err := d.DialAndSend(m); err != nil {
panic(err)
}
}
// Send an email using an API or postfix. // Send an email using an API or postfix.
func Example_noSMTP() { func Example_noSMTP() {
m := gomail.NewMessage() m := gomail.NewMessage()
@ -170,6 +151,10 @@ func ExampleSetHeader() {
m.Attach("foo.jpg", gomail.SetHeader(h)) m.Attach("foo.jpg", gomail.SetHeader(h))
} }
func ExampleRename() {
m.Attach("/tmp/0000146.jpg", gomail.Rename("picture.jpg"))
}
func ExampleMessage_AddAlternative() { func ExampleMessage_AddAlternative() {
m.SetBody("text/plain", "Hello!") m.SetBody("text/plain", "Hello!")
m.AddAlternative("text/html", "<p>Hello!</p>") m.AddAlternative("text/html", "<p>Hello!</p>")

3
go.mod Normal file
View File

@ -0,0 +1,3 @@
module git.lovezsh.com/go-kit/gomail
go 1.21.3

View File

@ -127,6 +127,10 @@ func (m *Message) SetAddressHeader(field, address, name string) {
// FormatAddress formats an address and a name as a valid RFC 5322 address. // FormatAddress formats an address and a name as a valid RFC 5322 address.
func (m *Message) FormatAddress(address, name string) string { func (m *Message) FormatAddress(address, name string) string {
if name == "" {
return address
}
enc := m.encodeString(name) enc := m.encodeString(name)
if enc == name { if enc == name {
m.buf.WriteByte('"') m.buf.WriteByte('"')
@ -260,6 +264,14 @@ func SetHeader(h map[string][]string) FileSetting {
} }
} }
// Rename is a file setting to set the name of the attachment if the name is
// different than the filename on disk.
func Rename(name string) FileSetting {
return func(f *file) {
f.Name = name
}
}
// SetCopyFunc is a file setting to replace the function that runs when the // SetCopyFunc is a file setting to replace the function that runs when the
// message is sent. It should copy the content of the file to the io.Writer. // message is sent. It should copy the content of the file to the io.Writer.
// //

View File

@ -150,7 +150,8 @@ func TestAlternative(t *testing.T) {
to: []string{"to@example.com"}, to: []string{"to@example.com"},
content: "From: from@example.com\r\n" + content: "From: from@example.com\r\n" +
"To: to@example.com\r\n" + "To: to@example.com\r\n" +
"Content-Type: multipart/alternative; boundary=_BOUNDARY_1_\r\n" + "Content-Type: multipart/alternative;\r\n" +
" boundary=_BOUNDARY_1_\r\n" +
"\r\n" + "\r\n" +
"--_BOUNDARY_1_\r\n" + "--_BOUNDARY_1_\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" + "Content-Type: text/plain; charset=UTF-8\r\n" +
@ -180,7 +181,8 @@ func TestPartSetting(t *testing.T) {
to: []string{"to@example.com"}, to: []string{"to@example.com"},
content: "From: from@example.com\r\n" + content: "From: from@example.com\r\n" +
"To: to@example.com\r\n" + "To: to@example.com\r\n" +
"Content-Type: multipart/alternative; boundary=_BOUNDARY_1_\r\n" + "Content-Type: multipart/alternative;\r\n" +
" boundary=_BOUNDARY_1_\r\n" +
"\r\n" + "\r\n" +
"--_BOUNDARY_1_\r\n" + "--_BOUNDARY_1_\r\n" +
"Content-Type: text/plain; format=flowed; charset=UTF-8\r\n" + "Content-Type: text/plain; format=flowed; charset=UTF-8\r\n" +
@ -216,7 +218,8 @@ func TestBodyWriter(t *testing.T) {
to: []string{"to@example.com"}, to: []string{"to@example.com"},
content: "From: from@example.com\r\n" + content: "From: from@example.com\r\n" +
"To: to@example.com\r\n" + "To: to@example.com\r\n" +
"Content-Type: multipart/alternative; boundary=_BOUNDARY_1_\r\n" + "Content-Type: multipart/alternative;\r\n" +
" boundary=_BOUNDARY_1_\r\n" +
"\r\n" + "\r\n" +
"--_BOUNDARY_1_\r\n" + "--_BOUNDARY_1_\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" + "Content-Type: text/plain; charset=UTF-8\r\n" +
@ -267,7 +270,8 @@ func TestAttachment(t *testing.T) {
to: []string{"to@example.com"}, to: []string{"to@example.com"},
content: "From: from@example.com\r\n" + content: "From: from@example.com\r\n" +
"To: to@example.com\r\n" + "To: to@example.com\r\n" +
"Content-Type: multipart/mixed; boundary=_BOUNDARY_1_\r\n" + "Content-Type: multipart/mixed;\r\n" +
" boundary=_BOUNDARY_1_\r\n" +
"\r\n" + "\r\n" +
"--_BOUNDARY_1_\r\n" + "--_BOUNDARY_1_\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" + "Content-Type: text/plain; charset=UTF-8\r\n" +
@ -286,6 +290,40 @@ func TestAttachment(t *testing.T) {
testMessage(t, m, 1, want) testMessage(t, m, 1, want)
} }
func TestRename(t *testing.T) {
m := NewMessage()
m.SetHeader("From", "from@example.com")
m.SetHeader("To", "to@example.com")
m.SetBody("text/plain", "Test")
name, copy := mockCopyFile("/tmp/test.pdf")
rename := Rename("another.pdf")
m.Attach(name, copy, rename)
want := &message{
from: "from@example.com",
to: []string{"to@example.com"},
content: "From: from@example.com\r\n" +
"To: to@example.com\r\n" +
"Content-Type: multipart/mixed;\r\n" +
" boundary=_BOUNDARY_1_\r\n" +
"\r\n" +
"--_BOUNDARY_1_\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" +
"Content-Transfer-Encoding: quoted-printable\r\n" +
"\r\n" +
"Test\r\n" +
"--_BOUNDARY_1_\r\n" +
"Content-Type: application/pdf; name=\"another.pdf\"\r\n" +
"Content-Disposition: attachment; filename=\"another.pdf\"\r\n" +
"Content-Transfer-Encoding: base64\r\n" +
"\r\n" +
base64.StdEncoding.EncodeToString([]byte("Content of test.pdf")) + "\r\n" +
"--_BOUNDARY_1_--\r\n",
}
testMessage(t, m, 1, want)
}
func TestAttachmentsOnly(t *testing.T) { func TestAttachmentsOnly(t *testing.T) {
m := NewMessage() m := NewMessage()
m.SetHeader("From", "from@example.com") m.SetHeader("From", "from@example.com")
@ -298,7 +336,8 @@ func TestAttachmentsOnly(t *testing.T) {
to: []string{"to@example.com"}, to: []string{"to@example.com"},
content: "From: from@example.com\r\n" + content: "From: from@example.com\r\n" +
"To: to@example.com\r\n" + "To: to@example.com\r\n" +
"Content-Type: multipart/mixed; boundary=_BOUNDARY_1_\r\n" + "Content-Type: multipart/mixed;\r\n" +
" boundary=_BOUNDARY_1_\r\n" +
"\r\n" + "\r\n" +
"--_BOUNDARY_1_\r\n" + "--_BOUNDARY_1_\r\n" +
"Content-Type: application/pdf; name=\"test.pdf\"\r\n" + "Content-Type: application/pdf; name=\"test.pdf\"\r\n" +
@ -331,7 +370,8 @@ func TestAttachments(t *testing.T) {
to: []string{"to@example.com"}, to: []string{"to@example.com"},
content: "From: from@example.com\r\n" + content: "From: from@example.com\r\n" +
"To: to@example.com\r\n" + "To: to@example.com\r\n" +
"Content-Type: multipart/mixed; boundary=_BOUNDARY_1_\r\n" + "Content-Type: multipart/mixed;\r\n" +
" boundary=_BOUNDARY_1_\r\n" +
"\r\n" + "\r\n" +
"--_BOUNDARY_1_\r\n" + "--_BOUNDARY_1_\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" + "Content-Type: text/plain; charset=UTF-8\r\n" +
@ -369,7 +409,8 @@ func TestEmbedded(t *testing.T) {
to: []string{"to@example.com"}, to: []string{"to@example.com"},
content: "From: from@example.com\r\n" + content: "From: from@example.com\r\n" +
"To: to@example.com\r\n" + "To: to@example.com\r\n" +
"Content-Type: multipart/related; boundary=_BOUNDARY_1_\r\n" + "Content-Type: multipart/related;\r\n" +
" boundary=_BOUNDARY_1_\r\n" +
"\r\n" + "\r\n" +
"--_BOUNDARY_1_\r\n" + "--_BOUNDARY_1_\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" + "Content-Type: text/plain; charset=UTF-8\r\n" +
@ -410,13 +451,16 @@ func TestFullMessage(t *testing.T) {
to: []string{"to@example.com"}, to: []string{"to@example.com"},
content: "From: from@example.com\r\n" + content: "From: from@example.com\r\n" +
"To: to@example.com\r\n" + "To: to@example.com\r\n" +
"Content-Type: multipart/mixed; boundary=_BOUNDARY_1_\r\n" + "Content-Type: multipart/mixed;\r\n" +
" boundary=_BOUNDARY_1_\r\n" +
"\r\n" + "\r\n" +
"--_BOUNDARY_1_\r\n" + "--_BOUNDARY_1_\r\n" +
"Content-Type: multipart/related; boundary=_BOUNDARY_2_\r\n" + "Content-Type: multipart/related;\r\n" +
" boundary=_BOUNDARY_2_\r\n" +
"\r\n" + "\r\n" +
"--_BOUNDARY_2_\r\n" + "--_BOUNDARY_2_\r\n" +
"Content-Type: multipart/alternative; boundary=_BOUNDARY_3_\r\n" + "Content-Type: multipart/alternative;\r\n" +
" boundary=_BOUNDARY_3_\r\n" +
"\r\n" + "\r\n" +
"--_BOUNDARY_3_\r\n" + "--_BOUNDARY_3_\r\n" +
"Content-Type: text/plain; charset=UTF-8\r\n" + "Content-Type: text/plain; charset=UTF-8\r\n" +
@ -520,6 +564,18 @@ func TestBase64LineLength(t *testing.T) {
testMessage(t, m, 0, want) testMessage(t, m, 0, want)
} }
func TestEmptyName(t *testing.T) {
m := NewMessage()
m.SetAddressHeader("From", "from@example.com", "")
want := &message{
from: "from@example.com",
content: "From: from@example.com\r\n",
}
testMessage(t, m, 0, want)
}
func TestEmptyHeader(t *testing.T) { func TestEmptyHeader(t *testing.T) {
m := NewMessage() m := NewMessage()
m.SetHeaders(map[string][]string{ m.SetHeaders(map[string][]string{
@ -530,7 +586,7 @@ func TestEmptyHeader(t *testing.T) {
want := &message{ want := &message{
from: "from@example.com", from: "from@example.com",
content: "From: from@example.com\r\n" + content: "From: from@example.com\r\n" +
"X-Empty: \r\n", "X-Empty:\r\n",
} }
testMessage(t, m, 0, want) testMessage(t, m, 0, want)

View File

@ -5,6 +5,7 @@ package gomail
import ( import (
"mime" "mime"
"mime/quotedprintable" "mime/quotedprintable"
"strings"
) )
var newQPWriter = quotedprintable.NewWriter var newQPWriter = quotedprintable.NewWriter
@ -14,6 +15,7 @@ type mimeEncoder struct {
} }
var ( var (
bEncoding = mimeEncoder{mime.BEncoding} bEncoding = mimeEncoder{mime.BEncoding}
qEncoding = mimeEncoder{mime.QEncoding} qEncoding = mimeEncoder{mime.QEncoding}
lastIndexByte = strings.LastIndexByte
) )

View File

@ -11,6 +11,15 @@ type mimeEncoder struct {
} }
var ( var (
bEncoding = mimeEncoder{quotedprintable.BEncoding} bEncoding = mimeEncoder{quotedprintable.BEncoding}
qEncoding = mimeEncoder{quotedprintable.QEncoding} qEncoding = mimeEncoder{quotedprintable.QEncoding}
lastIndexByte = func(s string, c byte) int {
for i := len(s) - 1; i >= 0; i-- {
if s[i] == c {
return i
}
}
return -1
}
) )

11
send.go
View File

@ -20,7 +20,7 @@ type SendCloser interface {
Close() error Close() error
} }
// A SendFunc is a function that sends emails to the given adresses. // A SendFunc is a function that sends emails to the given addresses.
// //
// The SendFunc type is an adapter to allow the use of ordinary functions as // The SendFunc type is an adapter to allow the use of ordinary functions as
// email senders. If f is a function with the appropriate signature, SendFunc(f) // email senders. If f is a function with the appropriate signature, SendFunc(f)
@ -108,10 +108,9 @@ func addAddress(list []string, addr string) []string {
} }
func parseAddress(field string) (string, error) { func parseAddress(field string) (string, error) {
a, err := mail.ParseAddress(field) addr, err := mail.ParseAddress(field)
if a == nil { if err != nil {
return "", err return "", fmt.Errorf("gomail: invalid address %q: %v", field, err)
} }
return addr.Address, nil
return a.Address, err
} }

138
smtp.go
View File

@ -6,6 +6,8 @@ import (
"io" "io"
"net" "net"
"net/smtp" "net/smtp"
"strings"
"time"
) )
// A Dialer is a dialer to an SMTP server. // A Dialer is a dialer to an SMTP server.
@ -14,6 +16,10 @@ type Dialer struct {
Host string Host string
// Port represents the port of the SMTP server. // Port represents the port of the SMTP server.
Port int Port int
// Username is the username to use to authenticate to the SMTP server.
Username string
// Password is the password to use to authenticate to the SMTP server.
Password string
// Auth represents the authentication mechanism used to authenticate to the // Auth represents the authentication mechanism used to authenticate to the
// SMTP server. // SMTP server.
Auth smtp.Auth Auth smtp.Auth
@ -29,98 +35,89 @@ type Dialer struct {
LocalName string LocalName string
} }
// NewPlainDialer returns a Dialer. The given parameters are used to connect to // NewDialer returns a new SMTP Dialer. The given parameters are used to connect
// the SMTP server via a PLAIN authentication mechanism. // to the SMTP server.
// func NewDialer(host string, port int, username, password string) *Dialer {
// It fallbacks to the LOGIN mechanism if it is the only mechanism advertised by
// the server.
func NewPlainDialer(host string, port int, username, password string) *Dialer {
return &Dialer{ return &Dialer{
Host: host, Host: host,
Port: port, Port: port,
Auth: &plainAuth{ Username: username,
username: username, Password: password,
password: password, SSL: port == 465,
host: host,
},
SSL: port == 465,
} }
} }
// NewPlainDialer returns a new SMTP Dialer. The given parameters are used to
// connect to the SMTP server.
//
// Deprecated: Use NewDialer instead.
func NewPlainDialer(host string, port int, username, password string) *Dialer {
return NewDialer(host, port, username, password)
}
// Dial dials and authenticates to an SMTP server. The returned SendCloser // Dial dials and authenticates to an SMTP server. The returned SendCloser
// should be closed when done using it. // should be closed when done using it.
func (d *Dialer) Dial() (SendCloser, error) { func (d *Dialer) Dial() (SendCloser, error) {
c, err := d.dial() conn, err := netDialTimeout("tcp", addr(d.Host, d.Port), 10*time.Second)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if d.Auth != nil { if d.SSL {
if ok, _ := c.Extension("AUTH"); ok { conn = tlsClient(conn, d.tlsConfig())
if err = c.Auth(d.Auth); err != nil { }
c, err := smtpNewClient(conn, d.Host)
if err != nil {
return nil, err
}
if d.LocalName != "" {
if err := c.Hello(d.LocalName); err != nil {
return nil, err
}
}
if !d.SSL {
if ok, _ := c.Extension("STARTTLS"); ok {
if err := c.StartTLS(d.tlsConfig()); err != nil {
c.Close() c.Close()
return nil, err return nil, err
} }
} }
} }
return &smtpSender{c}, nil if d.Auth == nil && d.Username != "" {
} if ok, auths := c.Extension("AUTH"); ok {
if strings.Contains(auths, "CRAM-MD5") {
func (d *Dialer) dial() (smtpClient, error) { d.Auth = smtp.CRAMMD5Auth(d.Username, d.Password)
if d.SSL { } else if strings.Contains(auths, "LOGIN") &&
return d.sslDial() !strings.Contains(auths, "PLAIN") {
} d.Auth = &loginAuth{
return d.starttlsDial() username: d.Username,
} password: d.Password,
host: d.Host,
func (d *Dialer) starttlsDial() (smtpClient, error) { }
c, err := smtpDial(addr(d.Host, d.Port)) } else {
if err != nil { d.Auth = smtp.PlainAuth("", d.Username, d.Password, d.Host)
return nil, err }
}
if d.LocalName != "" {
if err := c.Hello(d.LocalName); err != nil {
return nil, err
} }
} }
if ok, _ := c.Extension("STARTTLS"); ok { if d.Auth != nil {
if err := c.StartTLS(d.tlsConfig()); err != nil { if err = c.Auth(d.Auth); err != nil {
c.Close() c.Close()
return nil, err return nil, err
} }
} }
return c, nil return &smtpSender{c, d}, nil
}
func (d *Dialer) sslDial() (smtpClient, error) {
conn, err := tlsDial("tcp", addr(d.Host, d.Port), d.tlsConfig())
if err != nil {
return nil, err
}
c, err := newClient(conn, d.Host)
if err != nil {
return nil, err
}
if d.LocalName != "" {
if err := c.Hello(d.LocalName); err != nil {
return nil, err
}
}
return c, nil
} }
func (d *Dialer) tlsConfig() *tls.Config { func (d *Dialer) tlsConfig() *tls.Config {
if d.TLSConfig == nil { if d.TLSConfig == nil {
return &tls.Config{ServerName: d.Host} return &tls.Config{ServerName: d.Host}
} }
return d.TLSConfig return d.TLSConfig
} }
@ -142,10 +139,21 @@ func (d *Dialer) DialAndSend(m ...*Message) error {
type smtpSender struct { type smtpSender struct {
smtpClient smtpClient
d *Dialer
} }
func (c *smtpSender) Send(from string, to []string, msg io.WriterTo) error { func (c *smtpSender) Send(from string, to []string, msg io.WriterTo) error {
if err := c.Mail(from); err != nil { if err := c.Mail(from); err != nil {
if err == io.EOF {
// This is probably due to a timeout, so reconnect and try again.
sc, derr := c.d.Dial()
if derr == nil {
if s, ok := sc.(*smtpSender); ok {
*c = *s
return c.Send(from, to, msg)
}
}
}
return err return err
} }
@ -174,11 +182,9 @@ func (c *smtpSender) Close() error {
// Stubbed out for tests. // Stubbed out for tests.
var ( var (
smtpDial = func(addr string) (smtpClient, error) { netDialTimeout = net.DialTimeout
return smtp.Dial(addr) tlsClient = tls.Client
} smtpNewClient = func(conn net.Conn, host string) (smtpClient, error) {
tlsDial = tls.Dial
newClient = func(conn net.Conn, host string) (smtpClient, error) {
return smtp.NewClient(conn, host) return smtp.NewClient(conn, host)
} }
) )

View File

@ -8,6 +8,7 @@ import (
"net/smtp" "net/smtp"
"reflect" "reflect"
"testing" "testing"
"time"
) )
const ( const (
@ -16,12 +17,14 @@ const (
) )
var ( var (
testConn = &net.TCPConn{}
testTLSConn = &tls.Conn{} testTLSConn = &tls.Conn{}
testConfig = &tls.Config{InsecureSkipVerify: true} testConfig = &tls.Config{InsecureSkipVerify: true}
testAuth = smtp.PlainAuth("", testUser, testPwd, testHost)
) )
func TestDialer(t *testing.T) { func TestDialer(t *testing.T) {
d := NewPlainDialer(testHost, testPort, "user", "pwd") d := NewDialer(testHost, testPort, "user", "pwd")
testSendMail(t, d, []string{ testSendMail(t, d, []string{
"Extension STARTTLS", "Extension STARTTLS",
"StartTLS", "StartTLS",
@ -39,7 +42,7 @@ func TestDialer(t *testing.T) {
} }
func TestDialerSSL(t *testing.T) { func TestDialerSSL(t *testing.T) {
d := NewPlainDialer(testHost, testSSLPort, "user", "pwd") d := NewDialer(testHost, testSSLPort, "user", "pwd")
testSendMail(t, d, []string{ testSendMail(t, d, []string{
"Extension AUTH", "Extension AUTH",
"Auth", "Auth",
@ -55,7 +58,7 @@ func TestDialerSSL(t *testing.T) {
} }
func TestDialerConfig(t *testing.T) { func TestDialerConfig(t *testing.T) {
d := NewPlainDialer(testHost, testPort, "user", "pwd") d := NewDialer(testHost, testPort, "user", "pwd")
d.LocalName = "test" d.LocalName = "test"
d.TLSConfig = testConfig d.TLSConfig = testConfig
testSendMail(t, d, []string{ testSendMail(t, d, []string{
@ -76,7 +79,7 @@ func TestDialerConfig(t *testing.T) {
} }
func TestDialerSSLConfig(t *testing.T) { func TestDialerSSLConfig(t *testing.T) {
d := NewPlainDialer(testHost, testSSLPort, "user", "pwd") d := NewDialer(testHost, testSSLPort, "user", "pwd")
d.LocalName = "test" d.LocalName = "test"
d.TLSConfig = testConfig d.TLSConfig = testConfig
testSendMail(t, d, []string{ testSendMail(t, d, []string{
@ -113,13 +116,35 @@ func TestDialerNoAuth(t *testing.T) {
}) })
} }
func TestDialerTimeout(t *testing.T) {
d := &Dialer{
Host: testHost,
Port: testPort,
}
testSendMailTimeout(t, d, []string{
"Extension STARTTLS",
"StartTLS",
"Mail " + testFrom,
"Extension STARTTLS",
"StartTLS",
"Mail " + testFrom,
"Rcpt " + testTo1,
"Rcpt " + testTo2,
"Data",
"Write message",
"Close writer",
"Quit",
"Close",
})
}
type mockClient struct { type mockClient struct {
t *testing.T t *testing.T
i int i int
want []string want []string
addr string addr string
auth smtp.Auth config *tls.Config
config *tls.Config timeout bool
} }
func (c *mockClient) Hello(localName string) error { func (c *mockClient) Hello(localName string) error {
@ -139,13 +164,19 @@ func (c *mockClient) StartTLS(config *tls.Config) error {
} }
func (c *mockClient) Auth(a smtp.Auth) error { func (c *mockClient) Auth(a smtp.Auth) error {
assertAuth(c.t, a, c.auth) if !reflect.DeepEqual(a, testAuth) {
c.t.Errorf("Invalid auth, got %#v, want %#v", a, testAuth)
}
c.do("Auth") c.do("Auth")
return nil return nil
} }
func (c *mockClient) Mail(from string) error { func (c *mockClient) Mail(from string) error {
c.do("Mail " + from) c.do("Mail " + from)
if c.timeout {
c.timeout = false
return io.EOF
}
return nil return nil
} }
@ -201,32 +232,42 @@ func (w *mockWriter) Close() error {
} }
func testSendMail(t *testing.T, d *Dialer, want []string) { func testSendMail(t *testing.T, d *Dialer, want []string) {
doTestSendMail(t, d, want, false)
}
func testSendMailTimeout(t *testing.T, d *Dialer, want []string) {
doTestSendMail(t, d, want, true)
}
func doTestSendMail(t *testing.T, d *Dialer, want []string, timeout bool) {
testClient := &mockClient{ testClient := &mockClient{
t: t, t: t,
want: want, want: want,
addr: addr(d.Host, d.Port), addr: addr(d.Host, d.Port),
auth: testAuth, config: d.TLSConfig,
config: d.TLSConfig, timeout: timeout,
} }
smtpDial = func(addr string) (smtpClient, error) { netDialTimeout = func(network, address string, d time.Duration) (net.Conn, error) {
assertAddr(t, addr, testClient.addr)
return testClient, nil
}
tlsDial = func(network, addr string, config *tls.Config) (*tls.Conn, error) {
if network != "tcp" { if network != "tcp" {
t.Errorf("Invalid network, got %q, want tcp", network) t.Errorf("Invalid network, got %q, want tcp", network)
} }
assertAddr(t, addr, testClient.addr) if address != testClient.addr {
assertConfig(t, config, testClient.config) t.Errorf("Invalid address, got %q, want %q",
return testTLSConn, nil address, testClient.addr)
}
return testConn, nil
} }
newClient = func(conn net.Conn, host string) (smtpClient, error) { tlsClient = func(conn net.Conn, config *tls.Config) *tls.Conn {
if conn != testTLSConn { if conn != testConn {
t.Error("Invalid TLS connection used") t.Errorf("Invalid conn, got %#v, want %#v", conn, testConn)
} }
assertConfig(t, config, testClient.config)
return testTLSConn
}
smtpNewClient = func(conn net.Conn, host string) (smtpClient, error) {
if host != testHost { if host != testHost {
t.Errorf("Invalid host, got %q, want %q", host, testHost) t.Errorf("Invalid host, got %q, want %q", host, testHost)
} }
@ -238,18 +279,6 @@ func testSendMail(t *testing.T, d *Dialer, want []string) {
} }
} }
func assertAuth(t *testing.T, got, want smtp.Auth) {
if !reflect.DeepEqual(got, want) {
t.Errorf("Invalid auth, got %#v, want %#v", got, want)
}
}
func assertAddr(t *testing.T, got, want string) {
if got != want {
t.Errorf("Invalid addr, got %q, want %q", got, want)
}
}
func assertConfig(t *testing.T, got, want *tls.Config) { func assertConfig(t *testing.T, got, want *tls.Config) {
if want == nil { if want == nil {
want = &tls.Config{ServerName: testHost} want = &tls.Config{ServerName: testHost}

View File

@ -7,6 +7,7 @@ import (
"mime" "mime"
"mime/multipart" "mime/multipart"
"path/filepath" "path/filepath"
"strings"
"time" "time"
) )
@ -78,7 +79,7 @@ type messageWriter struct {
func (w *messageWriter) openMultipart(mimeType string) { func (w *messageWriter) openMultipart(mimeType string) {
mw := multipart.NewWriter(w) mw := multipart.NewWriter(w)
contentType := "multipart/" + mimeType + "; boundary=" + mw.Boundary() contentType := "multipart/" + mimeType + ";\r\n boundary=" + mw.Boundary()
w.writers[w.depth] = mw w.writers[w.depth] = mw
if w.depth == 0 { if w.depth == 0 {
@ -163,17 +164,80 @@ func (w *messageWriter) writeString(s string) {
func (w *messageWriter) writeHeader(k string, v ...string) { func (w *messageWriter) writeHeader(k string, v ...string) {
w.writeString(k) w.writeString(k)
if len(v) == 0 {
w.writeString(":\r\n")
return
}
w.writeString(": ") w.writeString(": ")
if len(v) > 0 {
w.writeString(v[0]) // Max header line length is 78 characters in RFC 5322 and 76 characters
for _, s := range v[1:] { // in RFC 2047. So for the sake of simplicity we use the 76 characters
// limit.
charsLeft := 76 - len(k) - len(": ")
for i, s := range v {
// If the line is already too long, insert a newline right away.
if charsLeft < 1 {
if i == 0 {
w.writeString("\r\n ")
} else {
w.writeString(",\r\n ")
}
charsLeft = 75
} else if i != 0 {
w.writeString(", ") w.writeString(", ")
w.writeString(s) charsLeft -= 2
}
// While the header content is too long, fold it by inserting a newline.
for len(s) > charsLeft {
s = w.writeLine(s, charsLeft)
charsLeft = 75
}
w.writeString(s)
if i := lastIndexByte(s, '\n'); i != -1 {
charsLeft = 75 - (len(s) - i - 1)
} else {
charsLeft -= len(s)
} }
} }
w.writeString("\r\n") w.writeString("\r\n")
} }
func (w *messageWriter) writeLine(s string, charsLeft int) string {
// If there is already a newline before the limit. Write the line.
if i := strings.IndexByte(s, '\n'); i != -1 && i < charsLeft {
w.writeString(s[:i+1])
return s[i+1:]
}
for i := charsLeft - 1; i >= 0; i-- {
if s[i] == ' ' {
w.writeString(s[:i])
w.writeString("\r\n ")
return s[i+1:]
}
}
// We could not insert a newline cleanly so look for a space or a newline
// even if it is after the limit.
for i := 75; i < len(s); i++ {
if s[i] == ' ' {
w.writeString(s[:i])
w.writeString("\r\n ")
return s[i+1:]
}
if s[i] == '\n' {
w.writeString(s[:i+1])
return s[i+1:]
}
}
// Too bad, no space or newline in the whole string. Just write everything.
w.writeString(s)
return ""
}
func (w *messageWriter) writeHeaders(h map[string][]string) { func (w *messageWriter) writeHeaders(h map[string][]string) {
if w.depth == 0 { if w.depth == 0 {
for k, v := range h { for k, v := range h {