├── LICENSE ├── README.md ├── builder.go ├── builder_test.go ├── cmd ├── amp.html ├── message.html ├── message.txt └── sendemail.go ├── connect.go ├── delimiter.go ├── delimiter_test.go ├── email.go ├── email_test.go ├── go.mod ├── go.sum ├── lock.json ├── manifest.json ├── sender.go ├── sender_test.go └── testdata ├── knwoman.png └── prwoman.png /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alexey Agafonov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # smtpSender 2 | 3 | [![Go Report Card](https://goreportcard.com/badge/github.com/Supme/smtpSender)](https://goreportcard.com/report/github.com/Supme/smtpSender) 4 | 5 | See cmd/sendemail.go file for example 6 | ``` 7 | go build -o sendemail sendemail.go 8 | sendemail -h 9 | sendemail -f from@domain.tld -t to@domain.tld -s "Hello subject!" -m "Hello, world!" 10 | sendemail -f from@domain.tld -t to@domain.tld -s "Hello subject!" -html ./message.html -amp ./amp.html -txt ./message.txt 11 | ``` 12 | 13 | Send email 14 | ``` 15 | bldr := &smtpSender.Builder{ 16 | From: "Sender ", 17 | To: "Me ", 18 | Subject: "Test subject", 19 | } 20 | bldr.SetDKIM("domain.tld", "test", myPrivateKey) 21 | bldr.AddHeader("Content-Language: ru", "Message-ID: ", "Precedence: bulk") 22 | bldr.AddTextPart("textPlain") 23 | bldr.AddHTMLPart("

textHTML

", "./image.gif") 24 | bldr.AddAttachment("./file.zip", "./music.mp3") 25 | email := bldr.Email("Id-123", func(result smtpSender.Result){ 26 | fmt.Printf("Result for email id '%s' duration: %f sec result: %v\n", result.ID, result.Duration.Seconds(), result.Err) 27 | }) 28 | 29 | conn := new(smtpSender.Connect) 30 | conn.SetHostName("sender.domain.tld") 31 | conn.SetMapIP("192.168.0.10", "31.32.33.34") 32 | 33 | email.Send(conn, nil) 34 | 35 | or 36 | 37 | server = &smtpSender.SMTPserver{ 38 | Host: "smtp.server.tld", 39 | Port: 587, 40 | Username: "sender@domain.tld", 41 | Password: "password", 42 | } 43 | email.Send(conn, server) 44 | ``` 45 | 46 | 47 | Best way send email from pool 48 | ``` 49 | pipe := smtpSender.NewPipe( 50 | smtpSender.Config{ 51 | Iface: "31.32.33.34", 52 | Stream: 5, 53 | }, 54 | smtpSender.Config{ 55 | Iface: "socks5://222.222.222.222:7080", 56 | Stream: 2, 57 | }) 58 | pipe.Start() 59 | 60 | for i := 1; i <= 50; i++ { 61 | bldr := new(smtpSender.Builder) 62 | bldr.SetFrom("Sender", "sender@domain.tld") 63 | bldr.SetTo("Me", "me+test@mail.tld") 64 | bldr.SetSubject("Test subject " + id) 65 | bldr.AddHTMLPart("

textHTML

", "./image.gif") 66 | email := bldr.Email(id, func(result smtpSender.Result) { 67 | fmt.Printf("Result for email id '%s' duration: %f sec result: %v\n", result.ID, result.Duration.Seconds(), result.Err) 68 | wg.Done() 69 | }) 70 | err := pipe.Send(email) 71 | if err != nil { 72 | fmt.Printf("Send email id '%d' error %+v\n", i, err) 73 | break 74 | } 75 | if i == 35 { 76 | pipe.Stop() 77 | } 78 | } 79 | ``` 80 | 81 | 82 | Use template for email 83 | ``` 84 | import ( 85 | ... 86 | tmplHTML "html/template" 87 | tmplText "text/template" 88 | ) 89 | ... 90 | subj := tmplText.New("Subject") 91 | subj.Parse("{{.Name}} this template subject text.") 92 | html := tmplHTML.New("HTML") 93 | html.Parse(`

This 'HTML' template.

Hello {{.Name}}!

`) 94 | text := tmplText.New("Text") 95 | text.Parse("This 'Text' template. Hello {{.Name}}!") 96 | data := map[string]string{"Name": "Вася"} 97 | bldr.AddSubjectFunc(func(w io.Writer) error { 98 | return subj.Execute(w, &data) 99 | }) 100 | bldr.AddTextFunc(func(w io.Writer) error { 101 | return text.Execute(w, &data) 102 | }) 103 | bldr.AddHTMLFunc(func(w io.Writer) error { 104 | return html.Execute(w, &data) 105 | }, "./image.gif") 106 | ... 107 | ``` 108 | 109 | 110 | One more method send email from pool (Depricated) 111 | ``` 112 | emailPipe := smtpSender.NewEmailPipe( 113 | smtpSender.Config{ 114 | Iface: "31.32.33.34", 115 | Stream: 5, 116 | }, 117 | smtpSender.Config{ 118 | Iface: "socks5://222.222.222.222:7080", 119 | Stream: 2, 120 | }) 121 | 122 | start := time.Now() 123 | wg := &sync.WaitGroup{} 124 | for i := 1; i <= 15; i++ { 125 | id := "Id-" + strconv.Itoa(i) 126 | bldr := new(smtpSender.Builder) 127 | bldr.SetFrom("Sender", "sender@domain.tld") 128 | bldr.SetTo("Me", "me+test@mail.tld") 129 | bldr.SetSubject("Test subject " + id) 130 | bldr.SetDKIM("domain.tld", "test", myPrivateKey) 131 | bldr.AddHeader("Content-Language: ru", "Message-ID: ", "Precedence: bulk") 132 | bldr.AddTextPart("textPlain") 133 | bldr.AddHTMLPart("

textHTML

", "./image.gif") 134 | bldr.AddAttachment("./file.zip", "./music.mp3") 135 | wg.Add(1) 136 | email := bldr.Email(id, func(result smtpSender.Result) { 137 | fmt.Printf("Result for email id '%s' duration: %f sec result: %v\n", result.ID, result.Duration.Seconds(), result.Err) 138 | wg.Done() 139 | }) 140 | emailPipe <- email 141 | } 142 | wg.Wait() 143 | 144 | fmt.Printf("Stream send duration: %s\r\n", time.Now().Sub(start).String()) 145 | ``` -------------------------------------------------------------------------------- /builder.go: -------------------------------------------------------------------------------- 1 | package smtpSender 2 | 3 | import ( 4 | "bytes" 5 | "crypto" 6 | "crypto/x509" 7 | "encoding/base64" 8 | "encoding/pem" 9 | "errors" 10 | "fmt" 11 | "github.com/emersion/go-msgauth/dkim" 12 | "golang.org/x/crypto/ed25519" 13 | "io" 14 | "mime" 15 | "mime/quotedprintable" 16 | "net/http" 17 | "net/mail" 18 | "net/textproto" 19 | "os" 20 | "path/filepath" 21 | "strings" 22 | "time" 23 | ) 24 | 25 | const ( 26 | boundaryMixed = "===============_MIXED==" 27 | boundaryMixedBegin = "--" + boundaryMixed + "\r\n" 28 | boundaryMixedEnd = "--" + boundaryMixed + "--\r\n" 29 | boundaryHTMLRelated = "===============_HTML_RELATED==" 30 | boundaryHTMLRelatedBegin = "--" + boundaryHTMLRelated + "\r\n" 31 | boundaryHTMLRelatedEnd = "--" + boundaryHTMLRelated + "--\r\n" 32 | boundaryAlternative = "===============_ALTERNATIVE==" 33 | boundaryAlternativeBegin = "--" + boundaryAlternative + "\r\n" 34 | boundaryAlternativeEnd = "--" + boundaryAlternative + "--\r\n" 35 | ) 36 | 37 | // Builder helper for create email 38 | type Builder struct { 39 | From string 40 | To string 41 | Subject string 42 | subjectFunc func(io.Writer) error 43 | replyTo string 44 | headers []string 45 | mimeHeader textproto.MIMEHeader 46 | htmlPart []byte 47 | textPart []byte 48 | ampPart []byte 49 | htmlFunc func(io.Writer) error 50 | textFunc func(io.Writer) error 51 | ampFunc func(io.Writer) error 52 | htmlRelatedFiles []*os.File 53 | attachments []*os.File 54 | dkim builderDKIM 55 | } 56 | 57 | type builderDKIM struct { 58 | domain string 59 | selector string 60 | privateKey []byte 61 | dkimSignMethod int 62 | } 63 | 64 | // NewBuilder return new Builder 65 | func NewBuilder() *Builder { 66 | return &Builder{} 67 | } 68 | 69 | const ( 70 | DKIMSignMethodDoubleWrite = iota 71 | DKIMSignMethodBufferWrite 72 | ) 73 | 74 | func (b *Builder) SetDKIMSignMethod(signMethod int) *Builder { 75 | b.dkim.dkimSignMethod = signMethod 76 | return b 77 | } 78 | 79 | // SetDKIM sign DKIM parameters 80 | func (b *Builder) SetDKIM(domain, selector string, privateKey []byte) *Builder { 81 | b.dkim.domain = domain 82 | b.dkim.selector = selector 83 | b.dkim.privateKey = privateKey 84 | return b 85 | } 86 | 87 | // SetFrom email sender 88 | func (b *Builder) SetFrom(name, email string) *Builder { 89 | from := mail.Address{Name: name, Address: email} 90 | b.From = from.String() 91 | return b 92 | } 93 | 94 | // SetTo email recipient 95 | func (b *Builder) SetTo(name, email string) *Builder { 96 | to := mail.Address{Name: name, Address: email} 97 | b.To = to.String() 98 | return b 99 | } 100 | 101 | // SetSubject set email subject 102 | func (b *Builder) SetSubject(subject string) *Builder { 103 | b.Subject = subject 104 | return b 105 | } 106 | 107 | // AddSubjectFunc add writer function for subject 108 | func (b *Builder) AddSubjectFunc(f func(io.Writer) error) *Builder { 109 | b.subjectFunc = f 110 | return b 111 | } 112 | 113 | // AddReplyTo add Reply-To header 114 | func (b *Builder) AddReplyTo(name, email string) *Builder { 115 | b.replyTo = email 116 | return b 117 | } 118 | 119 | // AddHTMLFunc add writer function for HTML 120 | func (b *Builder) AddHTMLFunc(f func(io.Writer) error, file ...string) error { 121 | for i := range file { 122 | file, err := os.Open(file[i]) 123 | if err != nil { 124 | return err 125 | } 126 | b.htmlRelatedFiles = append(b.htmlRelatedFiles, file) 127 | } 128 | b.htmlFunc = f 129 | return nil 130 | } 131 | 132 | // AddTextFunc add writer function for plain text 133 | func (b *Builder) AddTextFunc(f func(io.Writer) error) *Builder { 134 | b.textFunc = f 135 | return b 136 | } 137 | 138 | // AddAMPFunc add writer function for AMP HTML 139 | func (b *Builder) AddAMPFunc(f func(io.Writer) error) *Builder { 140 | b.ampFunc = f 141 | return b 142 | } 143 | 144 | // AddHeader add extra header to email 145 | func (b *Builder) AddHeader(headers ...string) *Builder { 146 | for i := range headers { 147 | b.headers = append(b.headers, headers[i]+"\r\n") 148 | } 149 | return b 150 | } 151 | 152 | // AddMIMEHeader add extra mime header to email 153 | func (b *Builder) AddMIMEHeader(mimeHeader textproto.MIMEHeader) *Builder { 154 | b.mimeHeader = mimeHeader 155 | return b 156 | } 157 | 158 | // AddHTMLPart add text/html content with related file. 159 | // 160 | // Example use related file in html 161 | // AddHTMLPart( 162 | // `... My image ...`, 163 | // "/path/to/attach/myImage.jpg", 164 | // ) 165 | func (b *Builder) AddHTMLPart(html []byte, file ...string) (err error) { 166 | for i := range file { 167 | file, err := os.Open(file[i]) 168 | if err != nil { 169 | return err 170 | } 171 | b.htmlRelatedFiles = append(b.htmlRelatedFiles, file) 172 | } 173 | b.htmlPart = html 174 | return nil 175 | } 176 | 177 | // AddTextHTML 178 | // Deprecated: use AddHTMLPart 179 | func (b *Builder) AddTextHTML(html []byte, file ...string) (err error) { 180 | return b.AddHTMLPart(html, file...) 181 | } 182 | 183 | // AddTextPart add plain text 184 | func (b *Builder) AddTextPart(text []byte) *Builder { 185 | b.textPart = text 186 | return b 187 | } 188 | 189 | // AddTextPlain add plain text 190 | // Deprecated: use AddTextPart 191 | func (b *Builder) AddTextPlain(text []byte) { 192 | b.AddTextPart(text) 193 | } 194 | 195 | // AddAMPPart add text/x-amp-html content with related file. 196 | func (b *Builder) AddAMPPart(amp []byte) *Builder { 197 | b.ampPart = amp 198 | return b 199 | } 200 | 201 | // AddAttachment add attachment files to email 202 | func (b *Builder) AddAttachment(file ...string) error { 203 | for i := range file { 204 | file, err := os.Open(file[i]) 205 | if err != nil { 206 | return err 207 | } 208 | b.attachments = append(b.attachments, file) 209 | } 210 | return nil 211 | } 212 | 213 | // Email return Email struct with render function 214 | func (b *Builder) Email(id string, resultFunc func(Result)) *Email { 215 | email := new(Email) 216 | email.ID = id 217 | email.From = b.From 218 | email.To = b.To 219 | email.ResultFunc = resultFunc 220 | email.WriteCloser = b.emailWriteCloser 221 | return email 222 | } 223 | 224 | func (b Builder) emailWriteCloser(w io.WriteCloser) error { 225 | var err error 226 | defer w.Close() 227 | 228 | if b.dkim.domain == "" { 229 | err = b.headersBuilder(w) 230 | if err != nil { 231 | return err 232 | } 233 | err = b.bodyBuilder(w) 234 | return err 235 | } 236 | 237 | block, _ := pem.Decode(b.dkim.privateKey) 238 | if block == nil { 239 | return errors.New("dkim: cannot decode key") 240 | } 241 | var privateKey crypto.Signer 242 | switch strings.ToUpper(block.Type) { 243 | case "RSA PRIVATE KEY": 244 | privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes) 245 | if err != nil { 246 | return fmt.Errorf("error RSA private key: '%s'", err) 247 | } 248 | case "EDDSA PRIVATE KEY": 249 | if len(block.Bytes) != ed25519.PrivateKeySize { 250 | return fmt.Errorf("invalid Ed25519 private key size") 251 | } 252 | privateKey = ed25519.PrivateKey(block.Bytes) 253 | default: 254 | return fmt.Errorf("unknown private key type: '%v'", block.Type) 255 | } 256 | options := dkim.SignOptions{ 257 | Domain: b.dkim.domain, 258 | Selector: b.dkim.selector, 259 | HeaderKeys: []string{ 260 | "From", 261 | "Subject", 262 | "To", 263 | }, 264 | Signer: privateKey, 265 | } 266 | 267 | // dkimEmailDoubleWriteCloser 268 | // BenchmarkBuilderDKIM-2 1000 1689315 ns/op 52760 B/op 1130 allocs/op 269 | // BenchmarkBuilderAttachmentDKIM-2 1 1020528457 ns/op 9910984 B/op 1214748 allocs/op 270 | // 271 | // dkimEmailBufferWriteCloser 272 | // BenchmarkBuilderDKIM-2 500 2303593 ns/op 52805 B/op 1107 allocs/op 273 | // BenchmarkBuilderAttachmentDKIM-2 1 1396323806 ns/op 11647864 B/op 1214690 allocs/op 274 | switch b.dkim.dkimSignMethod { 275 | case DKIMSignMethodDoubleWrite: 276 | return b.dkimEmailDoubleWriteCloser(w, &options) 277 | case DKIMSignMethodBufferWrite: 278 | return b.dkimEmailBufferWriteCloser(w, &options) 279 | } 280 | return errors.New("unknown sign method") 281 | } 282 | 283 | func (b Builder) dkimEmailDoubleWriteCloser(w io.WriteCloser, options *dkim.SignOptions) error { 284 | signer, err := dkim.NewSigner(options) 285 | if err != nil { 286 | return err 287 | } 288 | if err := b.headersBuilder(signer); err != nil { 289 | return err 290 | } 291 | if err := b.bodyBuilder(signer); err != nil { 292 | return err 293 | } 294 | if err := signer.Close(); err != nil { 295 | return err 296 | } 297 | 298 | if _, err := w.Write([]byte(signer.Signature())); err != nil { 299 | return err 300 | } 301 | 302 | if err := b.headersBuilder(w); err != nil { 303 | return err 304 | } 305 | return b.bodyBuilder(w) 306 | } 307 | 308 | func (b *Builder) dkimEmailBufferWriteCloser(w io.WriteCloser, options *dkim.SignOptions) error { 309 | s, err := dkim.NewSigner(options) 310 | if err != nil { 311 | return err 312 | } 313 | defer s.Close() 314 | 315 | var buf bytes.Buffer 316 | mw := io.MultiWriter(&buf, s) 317 | 318 | if err := b.headersBuilder(mw); err != nil { 319 | return err 320 | } 321 | if err := b.bodyBuilder(mw); err != nil { 322 | return err 323 | } 324 | if err := s.Close(); err != nil { 325 | return err 326 | } 327 | 328 | if _, err := io.WriteString(w, s.Signature()); err != nil { 329 | return err 330 | } 331 | _, err = io.Copy(w, &buf) 332 | return err 333 | } 334 | 335 | func (b Builder) headersBuilder(w io.Writer) error { 336 | err := b.writeHeaders(w) 337 | if err != nil { 338 | return err 339 | } 340 | 341 | switch { 342 | case b.isMultipart(): 343 | err = b.writeMultipartHeader(w) 344 | case b.hasAlternative(): 345 | err = b.writeAlternativeHeader(w) 346 | case b.hasText(): 347 | err = b.writeTextPartHeader(w) 348 | case b.hasAMP(): 349 | err = b.writeAMPPartHeader(w) 350 | case b.hasHTML(): 351 | err = b.writeHTMLPartHeader(w) 352 | case b.hasAttachment(): 353 | err = b.writeMultipartHeader(w) 354 | } 355 | 356 | return err 357 | } 358 | 359 | func (b Builder) bodyBuilder(w io.Writer) error { 360 | switch { 361 | case b.isMultipart() || b.hasAttachment(): 362 | return b.multipartBuilder(w) 363 | case b.hasAlternative(): 364 | return b.alternativeBuilder(w) 365 | case b.hasText(): 366 | return b.writeTextPart(w) 367 | case b.hasAMP(): 368 | return b.writeAMPPart(w) 369 | case b.hasHTML(): 370 | return b.writeHTMLPart(w) 371 | case b.hasAMP(): 372 | return b.writeAMPPart(w) 373 | } 374 | return nil 375 | } 376 | 377 | func (b Builder) multipartBuilder(w io.Writer) error { 378 | switch { 379 | case b.hasAlternative(): 380 | if _, err := w.Write([]byte(boundaryMixedBegin)); err != nil { 381 | return err 382 | } 383 | if err := b.writeAlternativeHeader(w); err != nil { 384 | return err 385 | } 386 | if err := b.alternativeBuilder(w); err != nil { 387 | return err 388 | } 389 | case b.hasText(): 390 | if _, err := w.Write([]byte(boundaryMixedBegin)); err != nil { 391 | return err 392 | } 393 | if err := b.writeTextPartHeader(w); err != nil { 394 | return err 395 | } 396 | 397 | if err := b.writeTextPart(w); err != nil { 398 | return err 399 | } 400 | case b.hasAMP(): 401 | if _, err := w.Write([]byte(boundaryMixedBegin)); err != nil { 402 | return err 403 | } 404 | if err := b.writeAMPPartHeader(w); err != nil { 405 | return err 406 | } 407 | 408 | if err := b.writeAMPPart(w); err != nil { 409 | return err 410 | } 411 | case b.hasHTML(): 412 | if _, err := w.Write([]byte(boundaryMixedBegin)); err != nil { 413 | return err 414 | } 415 | if err := b.writeHTMLPartHeader(w); err != nil { 416 | return err 417 | } 418 | 419 | if err := b.writeHTMLPart(w); err != nil { 420 | return err 421 | } 422 | } 423 | 424 | // Attachments 425 | if err := b.writeAttachment(w); err != nil { 426 | return err 427 | } 428 | 429 | if _, err := w.Write([]byte(boundaryMixedEnd)); err != nil { 430 | return err 431 | } 432 | 433 | return nil 434 | } 435 | 436 | func (b Builder) alternativeBuilder(w io.Writer) error { 437 | if b.hasText() { 438 | if _, err := w.Write([]byte(boundaryAlternativeBegin)); err != nil { 439 | return err 440 | } 441 | if err := b.writeTextPartHeader(w); err != nil { 442 | return err 443 | } 444 | if err := b.writeTextPart(w); err != nil { 445 | return err 446 | } 447 | } 448 | 449 | if b.hasAMP() { 450 | if _, err := w.Write([]byte(boundaryAlternativeBegin)); err != nil { 451 | return err 452 | } 453 | if err := b.writeAMPPartHeader(w); err != nil { 454 | return err 455 | } 456 | if err := b.writeAMPPart(w); err != nil { 457 | return err 458 | } 459 | } 460 | 461 | if b.hasHTML() { 462 | if _, err := w.Write([]byte(boundaryAlternativeBegin)); err != nil { 463 | return err 464 | } 465 | if err := b.writeHTMLPartHeader(w); err != nil { 466 | return err 467 | } 468 | if err := b.writeHTMLPart(w); err != nil { 469 | return err 470 | } 471 | } 472 | 473 | if _, err := w.Write([]byte(boundaryAlternativeEnd)); err != nil { 474 | return err 475 | } 476 | 477 | return nil 478 | } 479 | 480 | func (b Builder) writeMultipartHeader(w io.Writer) error { 481 | _, err := w.Write([]byte("Content-Type: multipart/mixed; boundary=\"" + boundaryMixed + "\"\r\n\r\n")) 482 | return err 483 | } 484 | 485 | func (b Builder) writeAlternativeHeader(w io.Writer) error { 486 | _, err := w.Write([]byte("Content-Type: multipart/alternative; boundary=\"" + boundaryAlternative + "\"\r\n\r\n")) 487 | return err 488 | } 489 | 490 | func (b Builder) writeTextPartHeader(w io.Writer) error { 491 | _, err := w.Write([]byte("Content-Type: text/plain; charset=\"utf-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n")) 492 | return err 493 | } 494 | 495 | func (b Builder) writeAMPPartHeader(w io.Writer) error { 496 | _, err := w.Write([]byte("Content-Type: text/x-amp-html; charset=\"utf-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n")) 497 | return err 498 | } 499 | 500 | func (b Builder) writeHTMLPartHeader(w io.Writer) error { 501 | if b.hasHTMLRelated() { 502 | if _, err := w.Write([]byte("Content-Type: multipart/related; boundary=\"" + boundaryHTMLRelated + "\"\r\n\r\n")); err != nil { 503 | return err 504 | } 505 | if _, err := w.Write([]byte(boundaryHTMLRelatedBegin)); err != nil { 506 | return err 507 | } 508 | } 509 | _, err := w.Write([]byte("Content-Type: text/html; charset=\"utf-8\"\r\nContent-Transfer-Encoding: quoted-printable\r\n\r\n")) 510 | return err 511 | } 512 | 513 | func (b Builder) writeHeaders(w io.Writer) error { 514 | if _, err := w.Write([]byte("From: " + b.From + "\r\n")); err != nil { 515 | return err 516 | } 517 | if _, err := w.Write([]byte("To: " + b.To + "\r\n")); err != nil { 518 | return err 519 | } 520 | if b.replyTo != "" { 521 | if _, err := w.Write([]byte("Reply-To: <" + b.replyTo + ">\r\n")); err != nil { 522 | return err 523 | } 524 | } 525 | if _, err := w.Write([]byte("Date: " + time.Now().Format(time.RFC1123Z) + "\r\n")); err != nil { 526 | return err 527 | } 528 | if _, err := w.Write([]byte("MIME-Version: 1.0\r\n")); err != nil { 529 | return err 530 | } 531 | for i := range b.headers { 532 | if _, err := w.Write([]byte(b.headers[i])); err != nil { 533 | return err 534 | } 535 | } 536 | for k, v := range b.mimeHeader { 537 | if _, err := w.Write([]byte(k + ": ")); err != nil { 538 | return err 539 | } 540 | for i := range v { 541 | if len(v) == (i + 1) { 542 | if _, err := w.Write([]byte(" " + v[i])); err != nil { 543 | return err 544 | } 545 | } else { 546 | if _, err := w.Write([]byte(" " + v[i] + ";\r\n\t")); err != nil { 547 | return err 548 | } 549 | } 550 | } 551 | if _, err := w.Write([]byte("\r\n")); err != nil { 552 | return err 553 | } 554 | } 555 | 556 | if _, err := w.Write([]byte("Subject: ")); err != nil { 557 | return err 558 | } 559 | subj, err := b.makeSubject() 560 | if err != nil { 561 | return err 562 | } 563 | _, err = w.Write(subj) 564 | if err != nil { 565 | return err 566 | } 567 | _, err = w.Write([]byte("\r\n")) 568 | return err 569 | } 570 | 571 | func (b Builder) makeSubject() ([]byte, error) { 572 | var err error 573 | subj := bytes.NewBufferString(b.Subject) 574 | if b.subjectFunc != nil { 575 | err = b.subjectFunc(subj) 576 | if err != nil { 577 | return nil, err 578 | } 579 | } 580 | return []byte(mime.QEncoding.Encode("utf-8", subj.String())), nil 581 | } 582 | 583 | // Text part 584 | func (b Builder) writeTextPart(w io.Writer) error { 585 | q := quotedprintable.NewWriter(w) 586 | 587 | if _, err := q.Write(b.textPart); err != nil { 588 | return err 589 | } 590 | 591 | if b.textFunc != nil { 592 | if err := b.textFunc(q); err != nil { 593 | return err 594 | } 595 | } 596 | 597 | if err := q.Close(); err != nil { 598 | return err 599 | } 600 | 601 | if b.hasAlternative() || b.isMultipart() { 602 | if _, err := w.Write([]byte("\r\n")); err != nil { 603 | return err 604 | } 605 | } 606 | 607 | return nil 608 | } 609 | 610 | // AMP part 611 | func (b Builder) writeAMPPart(w io.Writer) error { 612 | q := quotedprintable.NewWriter(w) 613 | if _, err := q.Write(b.ampPart); err != nil { 614 | return err 615 | } 616 | 617 | if b.ampFunc != nil { 618 | if err := b.ampFunc(q); err != nil { 619 | return err 620 | } 621 | } 622 | 623 | if err := q.Close(); err != nil { 624 | return err 625 | } 626 | 627 | if b.hasAlternative() || b.isMultipart() { 628 | if _, err := w.Write([]byte("\r\n")); err != nil { 629 | return err 630 | } 631 | } 632 | 633 | return nil 634 | } 635 | 636 | // HTML part 637 | func (b Builder) writeHTMLPart(w io.Writer) error { 638 | q := quotedprintable.NewWriter(w) 639 | if _, err := q.Write(b.htmlPart); err != nil { 640 | return err 641 | } 642 | 643 | if b.htmlFunc != nil { 644 | if err := b.htmlFunc(q); err != nil { 645 | return err 646 | } 647 | } 648 | 649 | if err := q.Close(); err != nil { 650 | return err 651 | } 652 | 653 | if (b.hasAlternative() || b.isMultipart()) && len(b.htmlRelatedFiles) == 0 { 654 | if _, err := w.Write([]byte("\r\n")); err != nil { 655 | return err 656 | } 657 | } 658 | 659 | // related files 660 | for i := range b.htmlRelatedFiles { 661 | if _, err := w.Write([]byte("\r\n")); err != nil { 662 | return err 663 | } 664 | if _, err := w.Write([]byte(boundaryHTMLRelatedBegin)); err != nil { 665 | return err 666 | } 667 | 668 | if err := fileWriter(w, b.htmlRelatedFiles[i], "inline"); err != nil { 669 | return err 670 | } 671 | if _, err := w.Write([]byte("\r\n")); err != nil { 672 | return err 673 | } 674 | } 675 | if b.hasHTMLRelated() { 676 | if _, err := w.Write([]byte(boundaryHTMLRelatedEnd)); err != nil { 677 | return err 678 | } 679 | } 680 | 681 | return nil 682 | } 683 | 684 | func (b Builder) writeAttachment(w io.Writer) error { 685 | for i := range b.attachments { 686 | if _, err := w.Write([]byte(boundaryMixedBegin)); err != nil { 687 | return err 688 | } 689 | if err := fileWriter(w, b.attachments[i], "attachment"); err != nil { 690 | return err 691 | } 692 | if _, err := w.Write([]byte("\r\n")); err != nil { 693 | return err 694 | } 695 | } 696 | return nil 697 | } 698 | 699 | func (b Builder) hasText() bool { 700 | return len(b.textPart) != 0 || b.textFunc != nil 701 | } 702 | 703 | func (b Builder) hasHTML() bool { 704 | return len(b.htmlPart) != 0 || b.htmlFunc != nil 705 | } 706 | 707 | func (b Builder) hasAMP() bool { 708 | return len(b.ampPart) != 0 || b.ampFunc != nil 709 | } 710 | 711 | func (b Builder) hasAlternative() bool { 712 | var c = 0 713 | if b.hasText() { 714 | c++ 715 | } 716 | if b.hasHTML() { 717 | c++ 718 | } 719 | if b.hasAMP() { 720 | c++ 721 | } 722 | return c > 1 723 | } 724 | 725 | func (b Builder) hasHTMLRelated() bool { 726 | return b.hasHTML() && (len(b.htmlRelatedFiles) > 0) 727 | } 728 | 729 | func (b Builder) hasAttachment() bool { 730 | return len(b.attachments) > 0 731 | } 732 | 733 | func (b Builder) isMultipart() bool { 734 | var c = 0 735 | if b.hasText() || b.hasAMP() || b.hasHTML() { 736 | c++ 737 | } 738 | 739 | c = c + len(b.attachments) 740 | return c > 1 741 | } 742 | 743 | func fileWriter(w io.Writer, f *os.File, disposition string) error { 744 | var err error 745 | var info os.FileInfo 746 | name := filepath.Base(f.Name()) 747 | info, err = f.Stat() 748 | if err != nil { 749 | return err 750 | } 751 | size := info.Size() 752 | buf := make([]byte, 512) 753 | _, err = f.Read(buf) 754 | if err != nil && err != io.EOF { 755 | return err 756 | } 757 | content := http.DetectContentType(buf) 758 | _, err = f.Seek(0, 0) 759 | if err != nil { 760 | return err 761 | } 762 | var contentID string 763 | if disposition == "inline" { 764 | contentID = "Content-ID: <" + name + ">\r\n" 765 | } 766 | _, err = w.Write([]byte(fmt.Sprintf( 767 | "Content-Type: %s; name=\"%s\"\r\nContent-Transfer-Encoding: base64\r\n%sContent-Disposition: %s; filename=\"%s\"; size=%d;\r\n\r\n", 768 | content, 769 | name, 770 | contentID, 771 | disposition, 772 | name, 773 | size))) 774 | if err != nil { 775 | return err 776 | } 777 | 778 | dwr := NewDelimitWriter(w, []byte{0x0d, 0x0a}, 76) // 76 from RFC 779 | b64Enc := base64.NewEncoder(base64.StdEncoding, dwr) 780 | _, err = io.Copy(b64Enc, f) 781 | if err != nil { 782 | return err 783 | } 784 | return b64Enc.Close() 785 | } 786 | -------------------------------------------------------------------------------- /builder_test.go: -------------------------------------------------------------------------------- 1 | package smtpSender_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | tmplHTML "html/template" 7 | "io" 8 | "io/ioutil" 9 | "net/textproto" 10 | "testing" 11 | tmplText "text/template" 12 | 13 | "github.com/Supme/smtpSender" 14 | ) 15 | 16 | var ( 17 | pkey = []byte(`-----BEGIN RSA PRIVATE KEY----- 18 | MIICXQIBAAKBgQCvis6cltd3R1Y2xJm1mUpR9aBnpH2x8qux8kg1Pvsk9reeXN+n 19 | wGbIISC/yM/hvQx9lCQki/OETKQYdTERjndtaJv1AgRhMccJNocXheWJD9dMUMYd 20 | PX2PMLkFTguUT47bPt9rR9wXCDyf1tBIDnFzy8aaReLIupuDhV0al7mWOQIDAQAB 21 | AoGAOXcJN/2xP1zc/kTRxL8Ps1DjV8pjU3OLfU9BEB0z/d++MFta5AF6JB2kKORG 22 | GTHX+uwaANTHvRGRzmfezk6DDYUD0I0v8kdfp1EHX1klMHHu8jQFA7mkoBbUwjca 23 | oqwizZNUkXRNV2V6E5U933+TBFm0J0ejF0vnUDD1dpvbF+ECQQDb+D67wI39KzVb 24 | VOs49RDEXYVLihmXsWZopbuoMaS5ZQ8q7cZ+qDIJimxTvObGQhO57EgTe2M9IZKh 25 | v0RsV15lAkEAzEuhpKUnJZkxtY154SiU2QzbGm+W70YQovqfZYBHFyLVAHfmHjNN 26 | LgbWeHA63ata28rkfe6m9sBFIR7teEfBRQJAZLkKOLyWB7wWRYjf4IfOsqvEEm/d 27 | AinYI8jn4b9BlybgSB7yiiKILvg0XC+eWF//Wl4ILuuL6H0MAIZtVVK4RQJBAIyw 28 | GOUVhtvxn7Xzc9eG5tqCa/DMoBivG43hIhv4NvzL0/u6lhJ+KcxkkRXn0+ILu0pZ 29 | cvj2fKy4w+KHNen7IDECQQCv4zeGpyO2AZnEBeHqHs9PCySglqIiHc56l9fSZu0u 30 | 6nAfGefm766gqBoYTC/1upkMYJpxizyH7U/7WessATfA 31 | -----END RSA PRIVATE KEY-----`) 32 | textPart = []byte("Привет, буфет\r\nЗдорова, колбаса!\r\nКак твои дела?\r\n0123456789\r\nabcdefgh\r\n") 33 | htmlPart = []byte("

Привет, буфет


\r\n

Здорова, колбаса!


\r\n

Как твои дела?


\r\n0123456789\r\nabcdefgh\r\n") 34 | ampPart = []byte(`\r\n\r\n\r\nHello World\r\n\r\n\r\n\r\n\r\n\r\n\r\n

Hello World

\r\n\r\n \r\n \r\n\r\n\r\n`) 35 | discard io.WriteCloser = devNull{} 36 | ) 37 | 38 | type devNull struct{} 39 | 40 | func (devNull) Write(p []byte) (int, error) { return len(p), nil } 41 | func (devNull) Close() error { return nil } 42 | 43 | func TestBuilder(t *testing.T) { 44 | bldr := new(smtpSender.Builder) 45 | bldr.SetSubject("Test subject") 46 | bldr.SetFrom("Вася", "vasya@mail.tld") 47 | bldr.SetTo("Петя", "petya@mail.tld") 48 | 49 | bldr.AddHeader("Message-ID: ") 50 | mimeHeader := textproto.MIMEHeader{} 51 | mimeHeader.Add("Content-Language", "ru") 52 | mimeHeader.Add("Precedence", " bulk") 53 | bldr.AddMIMEHeader(mimeHeader) 54 | 55 | bldr.AddTextPart(textPart) 56 | bldr.AddAMPPart(ampPart) 57 | if err := bldr.AddHTMLPart(htmlPart, "./testdata/prwoman.png"); err != nil { 58 | t.Error(err) 59 | } 60 | if err := bldr.AddAttachment("./testdata/knwoman.png"); err != nil { 61 | t.Error(err) 62 | } 63 | 64 | //_ = bldr.Email("Id-123", func(Result) {}) 65 | email := bldr.Email("Id-123", func(smtpSender.Result) {}) 66 | err := email.WriteCloser(discard) 67 | if err != nil { 68 | t.Error(err) 69 | } 70 | } 71 | 72 | func TestBuilderTemplate(t *testing.T) { 73 | bldr := new(smtpSender.Builder) 74 | data := map[string]string{"Name": "Вася"} 75 | 76 | subj := tmplText.New("Text") 77 | if _, err := subj.Parse("Test subject for {{.Name}}"); err != nil { 78 | t.Error(err) 79 | } 80 | bldr.AddSubjectFunc(func(w io.Writer) error { 81 | return subj.Execute(w, data) 82 | }) 83 | 84 | bldr.SetFrom("Вася", "vasya@mail.tld") 85 | bldr.SetTo("Петя", "petya@mail.tld") 86 | 87 | bldr.AddHeader("Content-Language: ru", "Message-ID: ", "Precedence: bulk") 88 | 89 | html := tmplHTML.New("HTML") 90 | if _, err := html.Parse(`

This 'HTML' template.

Hello {{.Name}}

`); err != nil { 91 | t.Error(err) 92 | } 93 | text := tmplText.New("Text") 94 | if _, err := text.Parse("This 'Text' template. Hello {{.Name}}"); err != nil { 95 | t.Error(err) 96 | } 97 | 98 | bldr.AddTextFunc(func(w io.Writer) error { 99 | return text.Execute(w, data) 100 | }) 101 | if err := bldr.AddHTMLFunc(func(w io.Writer) error { 102 | return html.Execute(w, data) 103 | }); err != nil { 104 | t.Error(err) 105 | } 106 | 107 | email := bldr.Email("Id-123", func(smtpSender.Result) {}) 108 | err := email.WriteCloser(discard) 109 | if err != nil { 110 | t.Error(err) 111 | } 112 | } 113 | 114 | func BenchmarkBuilder(b *testing.B) { 115 | bldr := new(smtpSender.Builder) 116 | bldr.SetSubject("Test subject") 117 | bldr.SetFrom("Вася", "vasya@mail.tld") 118 | bldr.SetTo("Петя", "petya@mail.tld") 119 | bldr.AddHeader("Content-Language: ru", "Message-ID: ", "Precedence: bulk") 120 | bldr.AddTextPart(textPart) 121 | if err := bldr.AddHTMLPart(htmlPart); err != nil { 122 | b.Error(err) 123 | } 124 | var err error 125 | for n := 0; n < b.N; n++ { 126 | email := bldr.Email("Id-123", func(smtpSender.Result) {}) 127 | err = email.WriteCloser(discard) 128 | if err != nil { 129 | b.Error(err) 130 | } 131 | } 132 | } 133 | 134 | func BenchmarkBuilderTemplate(b *testing.B) { 135 | bldr := new(smtpSender.Builder) 136 | data := map[string]string{"Name": "Вася"} 137 | 138 | subj := tmplText.New("Text") 139 | if _, err := subj.Parse("Test subject for {{.Name}}"); err != nil { 140 | b.Error(err) 141 | } 142 | bldr.AddSubjectFunc(func(w io.Writer) error { 143 | return subj.Execute(w, data) 144 | }) 145 | 146 | bldr.SetFrom("Вася", "vasya@mail.tld") 147 | bldr.SetTo("Петя", "petya@mail.tld") 148 | 149 | bldr.AddHeader("Content-Language: ru", "Message-ID: ", "Precedence: bulk") 150 | 151 | html := tmplHTML.New("HTML") 152 | if _, err := html.Parse(`

This 'HTML' template.

Hello {{.Name}}

`); err != nil { 153 | b.Error(err) 154 | } 155 | text := tmplText.New("Text") 156 | if _, err := text.Parse("This 'Text' template. Hello {{.Name}}"); err != nil { 157 | b.Error(err) 158 | } 159 | 160 | bldr.AddTextFunc(func(w io.Writer) error { 161 | return text.Execute(w, data) 162 | }) 163 | if err := bldr.AddHTMLFunc(func(w io.Writer) error { 164 | return html.Execute(w, data) 165 | }); err != nil { 166 | b.Error(err) 167 | } 168 | 169 | var err error 170 | for n := 0; n < b.N; n++ { 171 | //_ = bldr.Email("Id-123", func(Result) {}) 172 | email := bldr.Email("Id-123", func(smtpSender.Result) {}) 173 | err = email.WriteCloser(discard) 174 | if err != nil { 175 | b.Error(err) 176 | } 177 | } 178 | } 179 | 180 | func BenchmarkBuilderAttachment(b *testing.B) { 181 | bldr := new(smtpSender.Builder) 182 | bldr.SetSubject("Test subject") 183 | bldr.SetFrom("Вася", "vasya@mail.tld") 184 | bldr.SetTo("Петя", "petya@mail.tld") 185 | bldr.AddHeader("Content-Language: ru", "Message-ID: ", "Precedence: bulk") 186 | bldr.AddTextPart(textPart) 187 | if err := bldr.AddHTMLPart(htmlPart, "./testdata/prwoman.png"); err != nil { 188 | b.Error(err) 189 | } 190 | if err := bldr.AddAttachment("./testdata/knwoman.png"); err != nil { 191 | b.Error(err) 192 | } 193 | var err error 194 | for n := 0; n < b.N; n++ { 195 | //_ = bldr.Email("Id-123", func(Result) {}) 196 | email := bldr.Email("Id-123", func(smtpSender.Result) {}) 197 | err = email.WriteCloser(discard) 198 | if err != nil { 199 | b.Error(err) 200 | } 201 | } 202 | } 203 | 204 | func BenchmarkBuilderDKIM(b *testing.B) { 205 | bldr := new(smtpSender.Builder) 206 | bldr.SetDKIM("mail.ru", "test", pkey) 207 | bldr.SetSubject("Test subject") 208 | bldr.SetFrom("Вася", "vasya@mail.tld") 209 | bldr.SetTo("Петя", "petya@mail.tld") 210 | bldr.AddHeader("Content-Language: ru", "Message-ID: ", "Precedence: bulk") 211 | bldr.AddTextPart(textPart) 212 | if err := bldr.AddHTMLPart(htmlPart); err != nil { 213 | b.Error(err) 214 | } 215 | var err error 216 | for n := 0; n < b.N; n++ { 217 | //_ = bldr.Email("Id-123", func(Result) {}) 218 | email := bldr.Email("Id-123", func(smtpSender.Result) {}) 219 | err = email.WriteCloser(discard) 220 | if err != nil { 221 | b.Error(err) 222 | } 223 | } 224 | } 225 | 226 | func BenchmarkBuilderAttachmentDKIM(b *testing.B) { 227 | bldr := new(smtpSender.Builder) 228 | bldr.SetDKIM("mail.ru", "test", pkey) 229 | bldr.SetSubject("Test subject") 230 | bldr.SetFrom("Вася", "vasya@mail.tld") 231 | bldr.SetTo("Петя", "petya@mail.tld") 232 | bldr.AddHeader("Content-Language: ru", "Message-ID: ", "Precedence: bulk") 233 | bldr.AddTextPart(textPart) 234 | if err := bldr.AddHTMLPart(htmlPart, "./testdata/prwoman.png"); err != nil { 235 | b.Error(err) 236 | } 237 | if err := bldr.AddAttachment("./testdata/knwoman.png"); err != nil { 238 | b.Error(err) 239 | } 240 | var err error 241 | for n := 0; n < b.N; n++ { 242 | //_ = bldr.Email("Id-123", func(smtpSender.Result) {}) 243 | email := bldr.Email("Id-123", func(smtpSender.Result) {}) 244 | err = email.WriteCloser(discard) 245 | if err != nil { 246 | b.Error(err) 247 | } 248 | } 249 | } 250 | 251 | func TestDelimitWriter(t *testing.T) { 252 | m := []byte(htmlPart) 253 | w := &bytes.Buffer{} 254 | dwr := smtpSender.NewDelimitWriter(w, []byte{0x0d, 0x0a}, 16) 255 | encoder := base64.NewEncoder(base64.StdEncoding, dwr) 256 | _, err := encoder.Write(m) 257 | if err != nil { 258 | t.Error(err) 259 | } 260 | err = encoder.Close() 261 | if err != nil { 262 | t.Error(err) 263 | } 264 | 265 | d, _ := base64.StdEncoding.DecodeString(w.String()) 266 | if c := bytes.Compare(m, d); c != 0 { 267 | t.Error("Base64 encode/decode not equivalent") 268 | } 269 | } 270 | 271 | func BenchmarkBase64DelimitWriter(b *testing.B) { 272 | m := []byte("

Hello, буфет


\r\n

Здорова, колбаса!


\r\n

Как твои дела?


\r\n0123456789\r\nabcdefgh\r\n") 273 | w := ioutil.Discard 274 | dwr := smtpSender.NewDelimitWriter(w, []byte{0x0d, 0x0a}, 8) 275 | encoder := base64.NewEncoder(base64.StdEncoding, dwr) 276 | for n := 0; n < b.N; n++ { 277 | _, err := encoder.Write(m) 278 | if err != nil { 279 | b.Error(err) 280 | } 281 | err = encoder.Close() 282 | if err != nil { 283 | b.Error(err) 284 | } 285 | } 286 | } 287 | 288 | func BenchmarkDelimitWriter(b *testing.B) { 289 | m := []byte("

Hello, буфет


\r\n

Здорова, колбаса!


\r\n

Как твои дела?


\r\n0123456789\r\nabcdefgh\r\n") 290 | w := ioutil.Discard 291 | dwr := smtpSender.NewDelimitWriter(w, []byte{0x0d, 0x0a}, 8) 292 | for n := 0; n < b.N; n++ { 293 | _, err := dwr.Write(m) 294 | if err != nil { 295 | b.Error(err) 296 | } 297 | } 298 | } 299 | -------------------------------------------------------------------------------- /cmd/amp.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello, {{.Name}}! 5 | 6 | 7 | 8 | 9 | 10 | 11 |

Hello, world!

12 |

This is AMP part.

13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /cmd/message.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hello message 6 | 7 | 8 |

Hello, world!

9 |

This is HTML part

10 | 11 | -------------------------------------------------------------------------------- /cmd/message.txt: -------------------------------------------------------------------------------- 1 | Hello, world! This is text part. -------------------------------------------------------------------------------- /cmd/sendemail.go: -------------------------------------------------------------------------------- 1 | // Example use: 2 | // sendemail -V -f from@domain.tld -t to@domain.tld -s "Hello subject!" -m "Hello, world!" 3 | // sendemail -f from@domain.tld -t to@domain.tld -s "Hello subject!" -html ./message.html -amp ./amp.html -txt ./message.txt 4 | package main 5 | 6 | import ( 7 | "bytes" 8 | "flag" 9 | "fmt" 10 | "github.com/Supme/smtpSender" 11 | "io" 12 | "io/ioutil" 13 | "log" 14 | "net" 15 | "os" 16 | "strconv" 17 | "strings" 18 | "sync" 19 | ) 20 | 21 | var ( 22 | fromEmail string 23 | fromName string 24 | toEmail string 25 | toName string 26 | subject string 27 | textPartFile string 28 | ampPartFile string 29 | htmlPartFile string 30 | htmlRelatedFiles string 31 | attachmentFiles string 32 | message string 33 | hostname string 34 | smtpServer string 35 | smtpUser string 36 | smtpPassword string 37 | dkimDomain string 38 | dkimSelector string 39 | dkimKeyFile string 40 | ) 41 | 42 | type buffer struct { 43 | bytes.Buffer 44 | } 45 | 46 | func (b *buffer) Close() error { 47 | return nil 48 | } 49 | 50 | func main() { 51 | flag.StringVar(&fromEmail, "f", "", "From email") 52 | flag.StringVar(&fromName, "fn", "", "From name") 53 | flag.StringVar(&toEmail, "t", "", "To email") 54 | flag.StringVar(&toName, "tn", "", "To name") 55 | flag.StringVar(&message, "m", "", "Text email message") 56 | flag.StringVar(&subject, "s", "", "Email subject") 57 | flag.StringVar(&textPartFile, "txt", "", "Text part file") 58 | flag.StringVar(&PartFile, "amp", "", "AMP part file") 59 | flag.StringVar(&htmlPartFile, "html", "", "HTML part file") 60 | flag.StringVar(&htmlRelatedFiles, "htmlrfs", "", "HTML related file split comma (',') separator") 61 | flag.StringVar(&attachmentFiles, "att", "", "Attachment files split comma (',') separator") 62 | flag.StringVar(&hostname, "h", "", "Hostname for direct send (if blank, use resolved IP)") 63 | flag.StringVar(&smtpServer, "S", "", "SMTP server, if not set, use direct send") 64 | flag.StringVar(&smtpUser, "u", "", "SMTP user") 65 | flag.StringVar(&smtpPassword, "p", "", "SMTP password") 66 | flag.StringVar(&dkimDomain, "dd", "", "DKIM domain") 67 | flag.StringVar(&dkimSelector, "ds", "", "DKIM selector") 68 | flag.StringVar(&dkimKeyFile, "df", "", "DKIM private key file") 69 | verbose := flag.Bool("V", false, "Verbose message") 70 | notSend := flag.Bool("N", false, "Not send to server") 71 | 72 | version := flag.Bool("v", false, "Prints version") 73 | flag.Parse() 74 | if *version { 75 | fmt.Printf("Sendemail version: v%s\r\n\r\n", smtpSender.Version) 76 | os.Exit(0) 77 | } 78 | 79 | if *verbose { 80 | fmt.Printf( 81 | "Use parameters:\r\n\tfromEmail: %s\r\n\tfromName: %s\r\n\ttoEmail: %s\r\n\ttoName: %s\r\n\tsubject: %s\r\n\ttextPartFile: %s\r\n\tampPartFile: %s\r\n\thtmlPartFile: %s\r\n\thtmlRelatedFiles: %s\r\n\tattachmentFiles: %s\r\n\tmessage: %s\r\n\thostname: %s\r\n\tsmtpServer: %s\r\n\tsmtpUser: %s\r\n\tsmtpPassword: %s\r\n\tdkimDomain: %s\r\n\tdkimSelector: %s\r\n\tdkimKeyFile: %s\r\n", 82 | fromEmail, fromName, toEmail, toName, subject, textPartFile, ampPartFile, htmlPartFile, htmlRelatedFiles, attachmentFiles, message, hostname, smtpServer, smtpUser, smtpPassword, dkimDomain, dkimSelector, dkimKeyFile, 83 | ) 84 | } 85 | 86 | bldr := smtpSender.NewBuilder() 87 | bldr.SetFrom(fromName, fromEmail).SetTo(toName, toEmail).SetSubject(subject).AddTextPart([]byte(message)) 88 | if textPartFile != "" { 89 | bldr.AddTextFunc(func(w io.Writer) error { 90 | f, err := os.Open(textPartFile) 91 | if err != nil { 92 | return fmt.Errorf("open text part file: %s", err) 93 | } 94 | _, err = io.Copy(w, f) 95 | if err != nil { 96 | return fmt.Errorf("send text part file: %s", err) 97 | } 98 | return nil 99 | }) 100 | } 101 | 102 | if ampPartFile != "" { 103 | bldr.AddAMPFunc(func(w io.Writer) error { 104 | f, err := os.Open(ampPartFile) 105 | if err != nil { 106 | return fmt.Errorf("open amp part file: %s", err) 107 | } 108 | _, err = io.Copy(w, f) 109 | if err != nil { 110 | return fmt.Errorf("send amp part file: %s", err) 111 | } 112 | return nil 113 | }) 114 | } 115 | 116 | if htmlPartFile != "" { 117 | var related []string 118 | if htmlRelatedFiles != "" { 119 | related = strings.Split(htmlRelatedFiles, ",") 120 | } 121 | 122 | err := bldr.AddHTMLFunc(func(w io.Writer) error { 123 | f, err := os.Open(htmlPartFile) 124 | if err != nil { 125 | return fmt.Errorf("open html part file: %s", err) 126 | } 127 | _, err = io.Copy(w, f) 128 | if err != nil { 129 | return fmt.Errorf("send html part file: %s", err) 130 | } 131 | return nil 132 | }, related...) 133 | if err != nil { 134 | log.Fatalf("add html part: %s", err) 135 | } 136 | } 137 | 138 | if attachmentFiles != "" { 139 | attachments := strings.Split(attachmentFiles, ",") 140 | err := bldr.AddAttachment(attachments...) 141 | if err != nil { 142 | log.Fatalf("add attachment files: %s", err) 143 | } 144 | } 145 | 146 | if dkimDomain != "" && dkimSelector != "" && dkimKeyFile != "" { 147 | privateKey, err := ioutil.ReadFile(dkimKeyFile) 148 | if err != nil { 149 | log.Fatalf("read DKIM private key file: %s", err) 150 | } 151 | bldr.SetDKIM(dkimDomain, dkimSelector, privateKey) 152 | } 153 | 154 | wg := &sync.WaitGroup{} 155 | 156 | email := bldr.Email("", func(result smtpSender.Result) { 157 | fmt.Printf("Result for email duration: %f sec result: %v\n", result.Duration.Seconds(), result.Err) 158 | wg.Done() 159 | }) 160 | 161 | if *verbose { 162 | buf := &buffer{} 163 | err := email.WriteCloser(buf) 164 | if err != nil { 165 | log.Fatalf("write email to buffer: %s", err) 166 | } 167 | fmt.Printf("\r\n--- Message body ---\r\n%s--- End message body ---\r\n\r\n", buf.String()) 168 | } 169 | 170 | if !*notSend { 171 | fmt.Println("Send and wait result...") 172 | wg.Add(1) 173 | conn := new(smtpSender.Connect) 174 | conn.SetHostName(hostname) 175 | if smtpServer != "" && smtpUser != "" && smtpPassword != "" { 176 | host, portStr, err := net.SplitHostPort(smtpServer) 177 | if err != nil { 178 | log.Fatalf("split host port SMTP server: %s", err) 179 | } 180 | port, err := strconv.Atoi(portStr) 181 | if err != nil { 182 | log.Fatalf("parse SMTP server port: %s", err) 183 | } 184 | server := &smtpSender.SMTPserver{ 185 | Host: host, 186 | Port: port, 187 | Username: smtpUser, 188 | Password: smtpPassword, 189 | } 190 | email.Send(conn, server) 191 | } else { 192 | email.Send(conn, nil) 193 | } 194 | 195 | wg.Wait() 196 | fmt.Println("Done") 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /connect.go: -------------------------------------------------------------------------------- 1 | package smtpSender 2 | 3 | import ( 4 | "fmt" 5 | "golang.org/x/net/proxy" 6 | "net" 7 | "net/smtp" 8 | "net/url" 9 | "os" 10 | "strconv" 11 | "strings" 12 | "sync" 13 | "time" 14 | ) 15 | 16 | const ( 17 | dialTimeout = 60 * time.Second 18 | dialTries = 3 19 | connTimeout = 5 * time.Minute // SMTP RFC 5 min 20 | connTries = 5 21 | ) 22 | 23 | // Connect to smtp server from configured interface 24 | type Connect struct { 25 | iface string 26 | hostname string 27 | portSMTP int 28 | mapIP map[string]string 29 | } 30 | 31 | // SetMapIP if use NAT set global IP address 32 | func (c *Connect) SetMapIP(localIP, globalIP string) { 33 | if c.mapIP == nil { 34 | c.mapIP = map[string]string{} 35 | } 36 | c.mapIP[localIP] = globalIP 37 | } 38 | 39 | // SetSMTPport set SMTP server port. Default 25 40 | // use for translate local Iface to global if NAT 41 | // if use Socks server translate Iface SOCKS server to real Iface 42 | func (c *Connect) SetSMTPport(port int) { 43 | c.portSMTP = port 44 | } 45 | 46 | // SetIface use this Iface for send. Default use default interface. 47 | // Example: 48 | // IP "1.2.3.4" 49 | // Socks5 proxy "socks5://user:password@1.2.3.4" or "socks5://1.2.3.4" 50 | func (c *Connect) SetIface(iface string) { 51 | c.iface = iface 52 | } 53 | 54 | // SetHostName set server hostname for HELO. If left blanc then use resolv name. 55 | func (c *Connect) SetHostName(name string) { 56 | c.hostname = name 57 | } 58 | 59 | func (c *Connect) newClient(domain string, lookupMX bool) (client *smtp.Client, err error) { 60 | var ( 61 | dialer func(network, address string) (net.Conn, error) 62 | mxs []*net.MX 63 | ) 64 | 65 | if c.portSMTP == 0 { 66 | c.portSMTP = 25 67 | } 68 | 69 | if dialer, err = dialFunction(c.iface); err != nil { 70 | return nil, err 71 | } 72 | 73 | if lookupMX { 74 | for tries := 0; tries < dialTries; tries++ { 75 | mxs, err = net.LookupMX(domain) 76 | if err == nil { 77 | break 78 | } 79 | time.Sleep(time.Second) 80 | } 81 | } else { 82 | mxs = append(mxs, &net.MX{Host: domain, Pref: 10}) 83 | } 84 | 85 | if len(mxs) == 0 { 86 | return nil, fmt.Errorf("max MX lookup tries reached") 87 | } 88 | 89 | for i := range mxs { 90 | var conn net.Conn 91 | server := strings.TrimSpace(mxs[i].Host) 92 | for tries := 1; tries <= connTries; tries++ { 93 | conn, err = dialer("tcp", net.JoinHostPort(server, strconv.Itoa(c.portSMTP))) 94 | if err == nil { 95 | e := conn.SetDeadline(time.Now().Add(connTimeout)) 96 | if e != nil { 97 | return nil, e 98 | } 99 | e = conn.SetReadDeadline(time.Now().Add(connTimeout)) 100 | if e != nil { 101 | return nil, e 102 | } 103 | e = conn.SetWriteDeadline(time.Now().Add(connTimeout)) 104 | if e != nil { 105 | return nil, e 106 | } 107 | break 108 | } 109 | if tries != connTries { 110 | time.Sleep(5 * time.Second) 111 | } 112 | } 113 | if err != nil { 114 | return 115 | } 116 | 117 | var ip string 118 | if c.iface == "" { 119 | ip, _, err = net.SplitHostPort(conn.LocalAddr().String()) 120 | if err != nil { 121 | return nil, err 122 | } 123 | } else { 124 | var u *url.URL 125 | u, err = url.Parse(c.iface) 126 | if err != nil { 127 | return nil, err 128 | } 129 | if strings.ToLower(u.Scheme) == "socks" || strings.ToLower(u.Scheme) == "socks5" { 130 | ip, _, err = net.SplitHostPort(u.Host) 131 | if err != nil { 132 | return nil, err 133 | } 134 | } 135 | } 136 | if myGlobalIP, ok := c.mapIP[ip]; ok { 137 | ip = myGlobalIP 138 | } 139 | if c.hostname == "" { 140 | name, err := lookup(ip) 141 | if err != nil { 142 | return nil, err 143 | } 144 | c.hostname = name 145 | } 146 | 147 | client, err = smtp.NewClient(conn, server) 148 | if err != nil { 149 | continue 150 | } 151 | err = client.Hello(strings.TrimRight(c.hostname, ".")) 152 | if err == nil { 153 | break 154 | } 155 | } 156 | 157 | if err != nil { 158 | return 159 | } 160 | 161 | return 162 | } 163 | 164 | var resolvedHosts struct { 165 | host map[string]string 166 | sync.Mutex 167 | } 168 | 169 | func lookup(ip string) (string, error) { 170 | resolvedHosts.Lock() 171 | defer resolvedHosts.Unlock() 172 | if resolvedHosts.host == nil { 173 | resolvedHosts.host = map[string]string{} 174 | } 175 | if name, ok := resolvedHosts.host[ip]; ok { 176 | return name, nil 177 | } 178 | names, err := net.LookupAddr(ip) 179 | if err != nil { 180 | return "", err 181 | } 182 | if len(names) != 0 { 183 | resolvedHosts.host[ip] = names[0] 184 | } else { 185 | resolvedHosts.host[ip], err = os.Hostname() 186 | if err != nil { 187 | return "", err 188 | } 189 | } 190 | return resolvedHosts.host[ip], nil 191 | } 192 | 193 | func dialFunction(iface string) (dialFunc func(network, address string) (net.Conn, error), err error) { 194 | if iface == "" { 195 | iface := net.Dialer{ 196 | Timeout: dialTimeout, 197 | } 198 | dialFunc = iface.Dial 199 | } else { 200 | var u *url.URL 201 | u, err = url.Parse(iface) 202 | if strings.ToLower(u.Scheme) == "socks" || strings.ToLower(u.Scheme) == "socks5" { 203 | if err != nil { 204 | return nil, fmt.Errorf("error parse socks: %s", err.Error()) 205 | } 206 | var iface proxy.Dialer 207 | if u.User != nil { 208 | auth := proxy.Auth{} 209 | auth.User = u.User.Username() 210 | auth.Password, _ = u.User.Password() 211 | iface, err = proxy.SOCKS5("tcp", u.Host, &auth, proxy.FromEnvironment()) 212 | if err != nil { 213 | return 214 | } 215 | } else { 216 | iface, err = proxy.SOCKS5("tcp", u.Host, nil, proxy.FromEnvironment()) 217 | if err != nil { 218 | return 219 | } 220 | } 221 | dialFunc = iface.Dial 222 | } else { 223 | iface := net.Dialer{ 224 | LocalAddr: &net.TCPAddr{IP: net.ParseIP(iface)}, 225 | Timeout: dialTimeout, 226 | } 227 | dialFunc = iface.Dial 228 | } 229 | } 230 | return 231 | } 232 | -------------------------------------------------------------------------------- /delimiter.go: -------------------------------------------------------------------------------- 1 | package smtpSender 2 | 3 | import "io" 4 | 5 | // DelimitWriter Writer with delimiter bytes 6 | type DelimitWriter struct { 7 | n int 8 | cnt int 9 | dr []byte 10 | writer io.Writer 11 | } 12 | 13 | // NewDelimitWriter get writer, delimit bytes and count through which to add a delimit bytes. Return DelimitWriter 14 | func NewDelimitWriter(writer io.Writer, delimiter []byte, cnt int) *DelimitWriter { 15 | return &DelimitWriter{n: 0, cnt: cnt, dr: delimiter, writer: writer} 16 | } 17 | 18 | // Write write delimiter function 19 | func (w *DelimitWriter) Write(p []byte) (int, error) { 20 | var err error 21 | for i := range p { 22 | _, err = w.writer.Write(p[i : i+1]) 23 | if err != nil { 24 | break 25 | } 26 | if w.n++; w.n%w.cnt == 0 { 27 | _, err = w.writer.Write(w.dr) 28 | if err != nil { 29 | break 30 | } 31 | } 32 | } 33 | return w.n, err 34 | } 35 | -------------------------------------------------------------------------------- /delimiter_test.go: -------------------------------------------------------------------------------- 1 | package smtpSender_test 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "fmt" 7 | "io" 8 | "io/ioutil" 9 | "os" 10 | "path/filepath" 11 | "testing" 12 | 13 | "github.com/Supme/smtpSender" 14 | ) 15 | 16 | const testfolder = "testdata" 17 | 18 | func TestDelimitWriter_Write(t *testing.T) { 19 | compare := func(r1, r2 io.Reader) error { 20 | d1, err := ioutil.ReadAll(r1) 21 | if err != nil { 22 | return fmt.Errorf("r1: %s", err) 23 | } 24 | d2, err := ioutil.ReadAll(r2) 25 | if err != nil { 26 | return fmt.Errorf("r2: %s", err) 27 | } 28 | if len(d1) > len(d2) { 29 | return fmt.Errorf("r1 size is bigger than r2 %d > %d", len(d1), len(d2)) 30 | } 31 | if len(d1) < len(d2) { 32 | return fmt.Errorf("r1 size is smaller than %d < %d", len(d1), len(d2)) 33 | } 34 | for i := range d1 { 35 | if d1[i] != d2[i] { 36 | return fmt.Errorf("at %d bytes are not equal %x != %x", i, d1[i], d2[i]) 37 | } 38 | } 39 | return nil 40 | } 41 | 42 | files, err := ioutil.ReadDir(testfolder) 43 | if err != nil { 44 | t.Fatal(err) 45 | } 46 | for _, file := range files { 47 | if file.IsDir() { 48 | continue 49 | } 50 | f, err := os.Open(filepath.Join(testfolder, file.Name())) 51 | if err != nil { 52 | t.Fatal(err) 53 | } 54 | 55 | b64buf := &bytes.Buffer{} 56 | dwr := smtpSender.NewDelimitWriter(b64buf, []byte{0x0d, 0x0a}, 76) 57 | b64Enc := base64.NewEncoder(base64.StdEncoding, dwr) 58 | _, err = io.Copy(b64Enc, f) 59 | if err != nil { 60 | t.Fatal(err) 61 | } 62 | err = b64Enc.Close() 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | 67 | dec := base64.NewDecoder(base64.StdEncoding, b64buf) 68 | 69 | _, err = f.Seek(0, 0) 70 | if err != nil { 71 | t.Fatal(err) 72 | } 73 | err = compare(f, dec) 74 | if err != nil { 75 | t.Fatalf("compare %s: %s", f.Name(), err) 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /email.go: -------------------------------------------------------------------------------- 1 | package smtpSender 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "io" 7 | "net/smtp" 8 | "regexp" 9 | "strings" 10 | "time" 11 | ) 12 | 13 | // Email struct 14 | type Email struct { 15 | // ID is id for return result 16 | ID string 17 | // From emailField has format 18 | // example 19 | // "Name " 20 | // "" 21 | // "emailField@domain.tld" 22 | From string 23 | fromName, fromEmail, fromDomain string 24 | // To emailField has format as From 25 | To string 26 | toName, toEmail, toDomain string 27 | // ResultFunc exec after send emil 28 | ResultFunc func(Result) 29 | // WriteCloser email body data writer function 30 | WriteCloser func(io.WriteCloser) error 31 | // DontUseTLS STARTTLS off 32 | DontUseTLS bool 33 | } 34 | 35 | // Result struct for return send emailField result 36 | type Result struct { 37 | ID string 38 | Duration time.Duration 39 | Err error 40 | } 41 | 42 | // SMTPserver use for send email from server 43 | type SMTPserver struct { 44 | Host string 45 | Port int 46 | Username string 47 | Password string 48 | } 49 | 50 | // Send sending this email 51 | func (e *Email) Send(connect *Connect, server *SMTPserver) { 52 | if connect == nil { 53 | connect = &Connect{} 54 | } 55 | 56 | var ( 57 | client *smtp.Client 58 | auth smtp.Auth 59 | err error 60 | ) 61 | start := time.Now() 62 | err = e.parseEmail() 63 | if err != nil { 64 | if e.ResultFunc != nil { 65 | e.ResultFunc(Result{ID: e.ID, Err: fmt.Errorf("513 %v", err), Duration: time.Since(start)}) 66 | } 67 | return 68 | } 69 | if server == nil { 70 | client, err = connect.newClient(e.toDomain, true) 71 | } else { 72 | auth = smtp.PlainAuth( 73 | "", 74 | server.Username, 75 | server.Password, 76 | server.Host, 77 | ) 78 | connect.SetSMTPport(server.Port) 79 | client, err = connect.newClient(server.Host, false) 80 | } 81 | if err != nil { 82 | e.ResultFunc(Result{ID: e.ID, Err: fmt.Errorf("421 %v", err), Duration: time.Since(start)}) 83 | return 84 | } 85 | 86 | err = e.send(auth, client) 87 | e.ResultFunc(Result{ID: e.ID, Err: err, Duration: time.Since(start)}) 88 | } 89 | 90 | func (e *Email) send(auth smtp.Auth, client *smtp.Client) error { 91 | var ( 92 | err error 93 | ) 94 | if ok, _ := client.Extension("STARTTLS"); ok && !e.DontUseTLS { 95 | config := &tls.Config{ServerName: e.toDomain, InsecureSkipVerify: true} 96 | if err = client.StartTLS(config); err != nil { 97 | return err 98 | } 99 | } 100 | 101 | if auth != nil { 102 | if err = client.Auth(auth); err != nil { 103 | return err 104 | } 105 | } 106 | 107 | if err := client.Mail(e.from()); err != nil { 108 | return err 109 | } 110 | 111 | if err := client.Rcpt(e.to()); err != nil { 112 | return err 113 | } 114 | 115 | defer func() { 116 | _ = client.Quit() 117 | _ = client.Close() 118 | }() 119 | 120 | w, err := client.Data() 121 | if err != nil { 122 | return err 123 | } 124 | 125 | return e.WriteCloser(w) 126 | } 127 | 128 | func (e *Email) from() string { 129 | return e.fromEmail + "@" + e.fromDomain 130 | } 131 | 132 | func (e *Email) to() string { 133 | return e.toEmail + "@" + e.toDomain 134 | } 135 | 136 | func (e *Email) parseEmail() (err error) { 137 | e.fromName, e.fromEmail, e.fromDomain, err = splitEmail(e.From) 138 | if err != nil { 139 | return fmt.Errorf("Field From has %s", err) 140 | } 141 | e.toName, e.toEmail, e.toDomain, err = splitEmail(e.To) 142 | if err != nil { 143 | return fmt.Errorf("Field To has %s", err) 144 | } 145 | return 146 | } 147 | 148 | var ( 149 | splitEmailFullStringRe = regexp.MustCompile(`(.+)<(.+)@(.+\..{2,12})>`) 150 | splitEmailOnlyStringRe = regexp.MustCompile(`<(.+)@(.+\..{2,12})>`) 151 | splitEmailRe = regexp.MustCompile(`(.+)@(.+\..{2,12})`) 152 | ) 153 | 154 | func splitEmail(e string) (name, email, domain string, err error) { 155 | //addr, err := mail.ParseAddress(e) 156 | //if err != nil { 157 | // return "", "", "", err 158 | //} 159 | //name = addr.Name 160 | //split := strings.Split(addr.Address, "@") 161 | //if len(split) != 2 { 162 | // return name, "", "", fmt.Errorf("bad email format") 163 | //} 164 | //email = strings.TrimSpace(split[0]) 165 | //domain = strings.TrimRight(strings.ToLower(strings.TrimSpace(split[1])), ".") 166 | 167 | s := strings.TrimSpace(e) 168 | if m := splitEmailFullStringRe.FindStringSubmatch(s); len(m) == 4 { 169 | name = strings.TrimSpace(m[1]) 170 | email = strings.ToLower(strings.TrimSpace(m[2])) 171 | domain = strings.TrimRight(strings.ToLower(strings.TrimSpace(m[3])), ".") 172 | } else if m := splitEmailOnlyStringRe.FindStringSubmatch(s); len(m) == 3 { 173 | email = strings.ToLower(strings.TrimSpace(m[1])) 174 | domain = strings.TrimRight(strings.ToLower(strings.TrimSpace(m[2])), ".") 175 | } else if m := splitEmailRe.FindStringSubmatch(s); len(m) == 3 { 176 | email = strings.ToLower(strings.TrimSpace(m[1])) 177 | domain = strings.TrimRight(strings.ToLower(strings.TrimSpace(m[2])), ".") 178 | } else { 179 | err = fmt.Errorf("bad email format") 180 | } 181 | return 182 | } 183 | -------------------------------------------------------------------------------- /email_test.go: -------------------------------------------------------------------------------- 1 | package smtpSender 2 | 3 | import "testing" 4 | 5 | type emailField struct { 6 | input, name, email, domain string 7 | } 8 | 9 | var ( 10 | rightEmail []emailField 11 | badEmail []emailField 12 | ) 13 | 14 | func init() { 15 | rightEmail = append(rightEmail, emailField{" My name < my+email@domain.tld. > ", "My name", "my+email", "domain.tld"}) 16 | rightEmail = append(rightEmail, emailField{" < My+Email@doMain.tld. > ", "", "my+email", "domain.tld"}) 17 | rightEmail = append(rightEmail, emailField{" mY+eMail@Domain.Tld. ", "", "my+email", "domain.tld"}) 18 | rightEmail = append(rightEmail, emailField{"recipient@linklocal.supme.ru", "", "recipient", "linklocal.supme.ru"}) 19 | rightEmail = append(rightEmail, emailField{"=?utf-8?q?=D0=9E=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D1=82=D0=B5=D0=BB?= =?utf-8?q?=D1=8C?= ", "=?utf-8?q?=D0=9E=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D1=82=D0=B5=D0=BB?= =?utf-8?q?=D1=8C?=", "sender", "localhost.localdomain"}) 20 | 21 | badEmail = append(badEmail, emailField{input: "my+email@domain.t"}) 22 | badEmail = append(badEmail, emailField{input: "< my+email[at]domain.tld>"}) 23 | //badEmail = append(badEmail, emailField{input: " 24 | 25 | 26 | 27 | Test content 28 | 29 | 30 |

Test HTML message

31 | ` 32 | testAmp = ` 33 | 34 | 35 | 36 | 37 | Test content 38 | 39 | 40 |

Test AMP message

41 | ` 42 | ) 43 | 44 | func TestEmail_Send(t *testing.T) { 45 | // start test smtp server 46 | received := make(chan receiveMail, 1) 47 | addr, closer := runsslserver( 48 | t, 49 | &smtpd.Server{ 50 | //ProtocolLogger: log.New(os.Stdout, "smtpdLog: ", log.Lshortfile), 51 | }, 52 | received) 53 | defer closer() 54 | //t.Logf("Listen: %s", addr) 55 | 56 | testEmails := map[string]testEmail{ 57 | "all parts": { 58 | heloName: "localtest", 59 | senderName: "Sender Отправитель", 60 | senderEmail: "sender@localhost.localdomain", 61 | recipientName: "Recipient Получатель", 62 | recipientEmail: "recipient@linklocal.supme.ru", 63 | subject: "🚀 Test message 🚀", 64 | text: testText, 65 | html: testHtml, 66 | htmlRelated: []string{"testdata/knwoman.png", "testdata/prwoman.png"}, 67 | amp: testAmp, 68 | attachments: []string{"testdata/knwoman.png", "testdata/prwoman.png"}, 69 | }, 70 | "all parts without html related": { 71 | heloName: "localtest", 72 | senderName: "Sender Отправитель", 73 | senderEmail: "sender@localhost.localdomain", 74 | recipientName: "Recipient Получатель", 75 | recipientEmail: "recipient@linklocal.supme.ru", 76 | subject: "Test message", 77 | text: testText, 78 | html: testHtml, 79 | htmlRelated: []string{"testdata/knwoman.png", "testdata/prwoman.png"}, 80 | amp: testAmp, 81 | attachments: []string{"testdata/knwoman.png", "testdata/prwoman.png"}, 82 | }, 83 | 84 | "text, html and amp parts": { 85 | heloName: "localtest", 86 | senderName: "Sender Отправитель", 87 | senderEmail: "sender@localhost.localdomain", 88 | recipientName: "Recipient Получатель", 89 | recipientEmail: "recipient@linklocal.supme.ru", 90 | subject: "Test message", 91 | text: testText, 92 | html: testHtml, 93 | amp: testAmp, 94 | }, 95 | 96 | "text and html parts": { 97 | heloName: "localtest", 98 | senderName: "Sender Отправитель", 99 | senderEmail: "sender@localhost.localdomain", 100 | recipientName: "Recipient Получатель", 101 | recipientEmail: "recipient@linklocal.supme.ru", 102 | subject: "Test message", 103 | text: testText, 104 | html: testHtml, 105 | }, 106 | 107 | "only text part": { 108 | heloName: "localtest", 109 | senderName: "Sender Отправитель", 110 | senderEmail: "sender@localhost.localdomain", 111 | recipientName: "Recipient Получатель", 112 | recipientEmail: "recipient@linklocal.supme.ru", 113 | subject: "Тестовое сообщение", 114 | // one part always add new line 115 | text: testText + "\n", 116 | }, 117 | 118 | "only html part": { 119 | heloName: "localtest", 120 | senderName: "Sender Отправитель", 121 | senderEmail: "sender@localhost.localdomain", 122 | recipientName: "Recipient Получатель", 123 | recipientEmail: "recipient@linklocal.supme.ru", 124 | subject: "Test message", 125 | // one part always add new line 126 | html: testHtml + "\n", 127 | }, 128 | 129 | "html part with related": { 130 | heloName: "localtest", 131 | senderName: "Sender Отправитель", 132 | senderEmail: "sender@localhost.localdomain", 133 | recipientName: "Recipient Получатель", 134 | recipientEmail: "recipient@linklocal.supme.ru", 135 | subject: "Test message", 136 | // one part always add new line 137 | html: testHtml + "\n", 138 | htmlRelated: []string{"testdata/knwoman.png", "testdata/prwoman.png"}, 139 | }, 140 | 141 | // this unreal but... need add AMP support to github.com/jhillyerd/enmime 142 | //"only amp part": { 143 | // heloName: "localtest", 144 | // senderName: "Sender Отправитель", 145 | // senderEmail: "sender@localhost.localdomain", 146 | // recipientName: "Recipient Получатель", 147 | // recipientEmail: "recipient@linklocal.supme.ru", 148 | // subject: "Test message", 149 | // // one part always add new line 150 | // amp: testAmp+"\n", 151 | //}, 152 | 153 | "html and two attachments parts": { 154 | heloName: "localtest", 155 | senderName: "Sender Отправитель", 156 | senderEmail: "sender@localhost.localdomain", 157 | recipientName: "Recipient Получатель", 158 | recipientEmail: "recipient@linklocal.supme.ru", 159 | subject: "Test message", 160 | html: testHtml, 161 | attachments: []string{"testdata/knwoman.png", "testdata/prwoman.png"}, 162 | }, 163 | 164 | "html and one attachments parts": { 165 | heloName: "localtest", 166 | senderName: "Sender Отправитель", 167 | senderEmail: "sender@localhost.localdomain", 168 | recipientName: "Recipient Получатель", 169 | recipientEmail: "recipient@linklocal.supme.ru", 170 | subject: "Test message", 171 | html: testHtml, 172 | attachments: []string{"testdata/knwoman.png"}, 173 | }, 174 | 175 | "amp and one attachments parts": { 176 | heloName: "localtest", 177 | senderName: "Sender Отправитель", 178 | senderEmail: "sender@localhost.localdomain", 179 | recipientName: "Recipient Получатель", 180 | recipientEmail: "recipient@linklocal.supme.ru", 181 | subject: "Test message", 182 | amp: testAmp, 183 | attachments: []string{"testdata/knwoman.png"}, 184 | }, 185 | 186 | "text and one attachments parts": { 187 | heloName: "localtest", 188 | senderName: "Sender Отправитель", 189 | senderEmail: "sender@localhost.localdomain", 190 | recipientName: "Recipient Получатель", 191 | recipientEmail: "recipient@linklocal.supme.ru", 192 | subject: "Test message", 193 | text: testText, 194 | attachments: []string{"testdata/knwoman.png"}, 195 | }, 196 | 197 | "only attachments parts": { 198 | heloName: "localtest", 199 | senderName: "Sender Отправитель", 200 | senderEmail: "sender@localhost.localdomain", 201 | recipientName: "Recipient Получатель", 202 | recipientEmail: "recipient@linklocal.supme.ru", 203 | subject: "Test message", 204 | attachments: []string{"testdata/knwoman.png", "testdata/prwoman.png"}, 205 | }, 206 | 207 | "one attachment part": { 208 | heloName: "localtest", 209 | senderName: "Sender Отправитель", 210 | senderEmail: "sender@localhost.localdomain", 211 | recipientName: "Recipient Получатель", 212 | recipientEmail: "recipient@linklocal.supme.ru", 213 | subject: "Test message", 214 | attachments: []string{"testdata/prwoman.png"}, 215 | }, 216 | } 217 | 218 | for id := range testEmails { 219 | testEmails[id].start(t, addr, received, id) 220 | } 221 | } 222 | 223 | type testEmail struct { 224 | heloName string 225 | senderName, senderEmail string 226 | recipientName, recipientEmail string 227 | subject string 228 | text string 229 | html string 230 | htmlRelated []string 231 | amp string 232 | attachments []string 233 | } 234 | 235 | func (te testEmail) start(t *testing.T, addr string, received chan receiveMail, id string) { 236 | // build email 237 | b := smtpSender.NewBuilder(). 238 | SetFrom(te.senderName, te.senderEmail). 239 | SetTo(te.recipientName, te.recipientEmail). 240 | SetSubject(te.subject) 241 | _ = b.AddTextPart([]byte(te.text)) 242 | _ = b.AddHTMLPart([]byte(te.html), te.htmlRelated...) 243 | _ = b.AddAMPPart([]byte(te.amp)) 244 | for i := range te.attachments { 245 | err := b.AddAttachment(te.attachments[i]) 246 | if err != nil { 247 | t.Errorf("add attachment to email with id '%s': %s", id, err) 248 | } 249 | } 250 | 251 | // send email 252 | e := b.Email(id, 253 | func(result smtpSender.Result) { 254 | //t.Logf("%+v", result) 255 | if result.Err != nil { 256 | t.Error(result.Err) 257 | } 258 | }) 259 | 260 | conn := new(smtpSender.Connect) 261 | conn.SetHostName(te.heloName) 262 | _, port, err := net.SplitHostPort(addr) 263 | if err != nil { 264 | t.Errorf("split hostport: %v", err) 265 | } 266 | p, _ := strconv.Atoi(port) 267 | conn.SetSMTPport(p) 268 | 269 | e.Send(conn, nil) 270 | 271 | // receive email 272 | timeout := time.NewTimer(time.Second) 273 | select { 274 | case <-timeout.C: 275 | t.Errorf("timeout receive email id '%s'", id) 276 | case r := <-received: 277 | //t.Logf("receive email with id '%s'", id) 278 | 279 | // check helo name 280 | //t.Logf("HeloName: '%s'\r", r.HeloName) 281 | if r.HeloName != te.heloName { 282 | t.Errorf("compare helo name '%s' fail", te.heloName) 283 | } 284 | 285 | // check sender 286 | //t.Logf("Sender: '%s'", r.Sender) 287 | if r.Sender != te.senderEmail { 288 | t.Errorf("compare sender email '%s' fail", te.senderEmail) 289 | } 290 | 291 | // check recipient 292 | //t.Logf("Recipient: '%s'", r.Recipients) 293 | if len(r.Recipients) != 1 { 294 | t.Errorf("recipients count '%d' in email id '%s' fail", len(r.Recipients), id) 295 | } else if r.Recipients[0] != te.recipientEmail { 296 | t.Errorf("compare recipient '%s' email id '%s' fail", te.recipientEmail, id) 297 | } 298 | 299 | //t.Logf("Data:\r\n%s", r.Data) 300 | buf := bytes.NewBuffer(r.Data) 301 | env, err := enmime.ReadEnvelope(buf) 302 | if err != nil { 303 | t.Errorf("parse MIME body error: %s", err) 304 | } 305 | 306 | // check subject 307 | //t.Logf("Subject: '%s'", env.GetHeader("Subject")) 308 | if env.GetHeader("Subject") != te.subject { 309 | t.Errorf("compare subject '%s' fail", te.subject) 310 | } 311 | 312 | // check text part 313 | if te.text != "" { 314 | //t.Logf("Text:\r\n'%s'", env.Text) 315 | if env.Text != te.text { 316 | t.Errorf("compare text part '%s' fail", env.Text) 317 | } 318 | } 319 | 320 | // check html part 321 | if te.html != "" { 322 | //t.Logf("HTML:\r\n'%s'", env.HTML) 323 | if env.HTML != te.html { 324 | t.Errorf("compare HTML part '%s' fail", env.HTML) 325 | } 326 | } 327 | 328 | // check amp part 329 | if te.amp != "" { 330 | var amp *enmime.Part 331 | for _, p := range env.OtherParts { 332 | if p.ContentType == "text/x-amp-html" { 333 | amp = p 334 | break 335 | } 336 | } 337 | if amp != nil { 338 | //t.Logf("AMP:\r\n'%s", string(amp.Content)) 339 | if string(amp.Content) != te.amp { 340 | t.Errorf("compare AMP part '%s' fail", string(amp.Content)) 341 | } 342 | } else { 343 | t.Errorf("amp part not found in email with id '%s'", id) 344 | } 345 | } 346 | 347 | // check html related attachments 348 | for i := range te.htmlRelated { 349 | var inline *enmime.Part 350 | for _, p := range env.Inlines { 351 | //t.Logf("html related '%s' file name '%v'", filepath.Base(te.htmlRelated[i]), p.FileName) 352 | if p.FileName == filepath.Base(te.htmlRelated[i]) { 353 | inline = p 354 | break 355 | } 356 | } 357 | if inline != nil { 358 | b, err := ioutil.ReadFile(te.htmlRelated[i]) 359 | if err != nil { 360 | fmt.Print(err) 361 | } 362 | //t.Logf("compare '%s' type '%s'", inline.FileName, inline.ContentType) 363 | if bytes.Compare(b, inline.Content) != 0 { 364 | t.Errorf("compare attachment '%s' in mail with id '%s' fail", te.attachments[i], id) 365 | } 366 | } else { 367 | t.Errorf("attachment '%s' part not found in email with id '%s'", te.attachments[i], id) 368 | } 369 | } 370 | 371 | // check attachments 372 | for i := range te.attachments { 373 | var attach *enmime.Part 374 | for _, p := range env.Attachments { 375 | //t.Logf("attachment '%s' file name '%v'", filepath.Base(te.attachments[i]), p.FileName) 376 | if p.FileName == filepath.Base(te.attachments[i]) { 377 | attach = p 378 | break 379 | } 380 | } 381 | if attach != nil { 382 | b, err := ioutil.ReadFile(te.attachments[i]) 383 | if err != nil { 384 | fmt.Print(err) 385 | } 386 | //t.Logf("compare '%s' type '%s'", attach.FileName, attach.ContentType) 387 | if bytes.Compare(b, attach.Content) != 0 { 388 | t.Errorf("compare attachment '%s' in mail with id '%s' fail", te.attachments[i], id) 389 | } 390 | } else { 391 | t.Errorf("attachment '%s' part not found in email with id '%s'", te.attachments[i], id) 392 | } 393 | } 394 | } 395 | } 396 | 397 | var localhostCert = []byte(`-----BEGIN CERTIFICATE----- 398 | MIIFkzCCA3ugAwIBAgIUQvhoyGmvPHq8q6BHrygu4dPp0CkwDQYJKoZIhvcNAQEL 399 | BQAwWTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM 400 | GEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MB4X 401 | DTIwMDUyMTE2MzI1NVoXDTMwMDUxOTE2MzI1NVowWTELMAkGA1UEBhMCQVUxEzAR 402 | BgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMgUHR5 403 | IEx0ZDESMBAGA1UEAwwJbG9jYWxob3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8A 404 | MIICCgKCAgEAk773plyfK4u2uIIZ6H7vEnTb5qJT6R/KCY9yniRvCFV+jCrISAs9 405 | 0pgU+/P8iePnZRGbRCGGt1B+1/JAVLIYFZuawILHNs4yWKAwh0uNpR1Pec8v7vpq 406 | NpdUzXKQKIqFynSkcLA8c2DOZwuhwVc8rZw50yY3r4i4Vxf0AARGXapnBfy6WerR 407 | /6xT7y/OcK8+8aOirDQ9P6WlvZ0ynZKi5q2o1eEVypT2us9r+HsCYosKEEAnjzjJ 408 | wP5rvredxUqb7OupIkgA4Nq80+4tqGGQfWetmoi3zXRhKpijKjgxBOYEqSUWm9ws 409 | /aC91Iy5RawyTB0W064z75OgfuI5GwFUbyLD0YVN4DLSAI79GUfvc8NeLEXpQvYq 410 | +f8P+O1Hbv2AQ28IdbyQrNefB+/WgjeTvXLploNlUihVhpmLpptqnauw/DY5Ix51 411 | w60lHIZ6esNOmMQB+/z/IY5gpmuo66yH8aSCPSYBFxQebB7NMqYGOS9nXx62/Bn1 412 | OUVXtdtrhfbbdQW6zMZjka0t8m83fnGw3ISyBK2NNnSzOgycu0ChsW6sk7lKyeWa 413 | 85eJGsQWIhkOeF9v9GAIH/qsrgVpToVC9Krbk+/gqYIYF330tHQrzp6M6LiG5OY1 414 | P7grUBovN2ZFt10B97HxWKa2f/8t9sfHZuKbfLSFbDsyI2JyNDh+Vk0CAwEAAaNT 415 | MFEwHQYDVR0OBBYEFOLdIQUr3gDQF5YBor75mlnCdKngMB8GA1UdIwQYMBaAFOLd 416 | IQUr3gDQF5YBor75mlnCdKngMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL 417 | BQADggIBAGddhQMVMZ14TY7bU8CMuc9IrXUwxp59QfqpcXCA2pHc2VOWkylv2dH7 418 | ta6KooPMKwJ61d+coYPK1zMUvNHHJCYVpVK0r+IGzs8mzg91JJpX2gV5moJqNXvd 419 | Fy6heQJuAvzbb0Tfsv8KN7U8zg/ovpS7MbY+8mRJTQINn2pCzt2y2C7EftLK36x0 420 | KeBWqyXofBJoMy03VfCRqQlWK7VPqxluAbkH+bzji1g/BTkoCKzOitAbjS5lT3sk 421 | oCrF9N6AcjpFOH2ZZmTO4cZ6TSWfrb/9OWFXl0TNR9+x5c/bUEKoGeSMV1YT1SlK 422 | TNFMUlq0sPRgaITotRdcptc045M6KF777QVbrYm/VH1T3pwPGYu2kUdYHcteyX9P 423 | 8aRG4xsPGQ6DD7YjBFsif2fxlR3nQ+J/l/+eXHO4C+eRbxi15Z2NjwVjYpxZlUOq 424 | HD96v516JkMJ63awbY+HkYdEUBKqR55tzcvNWnnfiboVmIecjAjoV4zStwDIti9u 425 | 14IgdqqAbnx0ALbUWnvfFloLdCzPPQhgLHpTeRSEDPljJWX8rmy8iQtRb0FWYQ3z 426 | A2wsUyutzK19nt4hjVrTX0At9ku3gMmViXFlbvyA1Y4TuhdUYqJauMBrWKl2ybDW 427 | yhdKg/V3yTwgBUtb3QO4m1khNQjQLuPFVxULGEA38Y5dXSONsYnt 428 | -----END CERTIFICATE-----`) 429 | 430 | var localhostKey = []byte(`-----BEGIN PRIVATE KEY----- 431 | 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 480 | hBgHM6A0WJC9MO3aAKRBcp48y6DXNA== 481 | -----END PRIVATE KEY-----`) 482 | 483 | type receiveMail struct { 484 | HeloName string 485 | Sender string 486 | Recipients []string 487 | Data []byte 488 | } 489 | 490 | func runserver(t *testing.T, server *smtpd.Server, received chan receiveMail) (addr string, closer func()) { 491 | ln, err := net.Listen("tcp", "127.0.1.10:0") 492 | if err != nil { 493 | t.Fatalf("Listen failed: %v", err) 494 | } 495 | go func() { 496 | server.Handler = func(peer smtpd.Peer, env smtpd.Envelope) error { 497 | m := receiveMail{ 498 | HeloName: peer.HeloName, 499 | Sender: env.Sender, 500 | Recipients: env.Recipients, 501 | Data: env.Data, 502 | } 503 | received <- m 504 | return nil 505 | } 506 | server.Serve(ln) 507 | }() 508 | 509 | done := make(chan bool) 510 | 511 | go func() { 512 | <-done 513 | ln.Close() 514 | }() 515 | 516 | return ln.Addr().String(), func() { 517 | done <- true 518 | } 519 | 520 | } 521 | 522 | func runsslserver(t *testing.T, server *smtpd.Server, received chan receiveMail) (addr string, closer func()) { 523 | cert, err := tls.X509KeyPair(localhostCert, localhostKey) 524 | if err != nil { 525 | t.Fatalf("Cert load failed: %v", err) 526 | } 527 | 528 | server.TLSConfig = &tls.Config{ 529 | Certificates: []tls.Certificate{cert}, 530 | } 531 | 532 | return runserver(t, server, received) 533 | } 534 | -------------------------------------------------------------------------------- /testdata/knwoman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Supme/smtpSender/582914e8f4327d6521b9a2a086902e8509f16ab5/testdata/knwoman.png -------------------------------------------------------------------------------- /testdata/prwoman.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Supme/smtpSender/582914e8f4327d6521b9a2a086902e8509f16ab5/testdata/prwoman.png --------------------------------------------------------------------------------