Compare commits
10 Commits
6ea1c86967
...
b65f60b163
Author | SHA1 | Date |
---|---|---|
lovezsh | b65f60b163 | |
Alexandre Cesaro | 81ebce5c23 | |
Alexandre Cesaro | e4bd87ad6e | |
Alexandre Cesaro | 92eaa13340 | |
slavikm | 4291610152 | |
Alexandre Cesaro | bd0e445b57 | |
Alexandre Cesaro | 84856b343c | |
Alexandre Cesaro | 060a5f4e98 | |
Alexandre Cesaro | afff51fd8c | |
Alexandre Cesaro | 5ceb8e6541 |
|
@ -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
52
auth.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
88
auth_test.go
88
auth_test.go
|
@ -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] {
|
||||||
|
|
|
@ -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>")
|
||||||
|
|
12
message.go
12
message.go
|
@ -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.
|
||||||
//
|
//
|
||||||
|
|
|
@ -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)
|
||||||
|
|
6
mime.go
6
mime.go
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
13
mime_go14.go
13
mime_go14.go
|
@ -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
11
send.go
|
@ -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
138
smtp.go
|
@ -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)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
109
smtp_test.go
109
smtp_test.go
|
@ -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}
|
||||||
|
|
74
writeto.go
74
writeto.go
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue