aboutsummaryrefslogtreecommitdiffstats
path: root/lib/send/smtp.go
blob: b1679014441139ee1b5bdee2499a0d5d4bbdfb86 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
package send

import (
	"crypto/tls"
	"fmt"
	"io"
	"net/url"
	"strings"

	"github.com/emersion/go-message/mail"
	"github.com/emersion/go-smtp"
	"github.com/pkg/errors"
)

func connectSmtp(starttls bool, host string, domain string) (*smtp.Client, error) {
	serverName := host
	if !strings.ContainsRune(host, ':') {
		host += ":587" // Default to submission port
	} else {
		serverName = host[:strings.IndexRune(host, ':')]
	}
	var conn *smtp.Client
	var err error
	if starttls {
		conn, err = smtp.DialStartTLS(host, &tls.Config{ServerName: serverName})
	} else {
		conn, err = smtp.Dial(host)
	}
	if err != nil {
		return nil, errors.Wrap(err, "smtp.Dial")
	}
	if domain != "" {
		err := conn.Hello(domain)
		if err != nil {
			conn.Close()
			return nil, errors.Wrap(err, "Hello")
		}
	}
	return conn, nil
}

func connectSmtps(host string) (*smtp.Client, error) {
	serverName := host
	if !strings.ContainsRune(host, ':') {
		host += ":465" // Default to smtps port
	} else {
		serverName = host[:strings.IndexRune(host, ':')]
	}
	conn, err := smtp.DialTLS(host, &tls.Config{
		ServerName: serverName,
	})
	if err != nil {
		return nil, errors.Wrap(err, "smtp.DialTLS")
	}
	return conn, nil
}

type smtpSender struct {
	conn *smtp.Client
	w    io.WriteCloser
}

func (s *smtpSender) Write(p []byte) (int, error) {
	return s.w.Write(p)
}

func (s *smtpSender) Close() error {
	we := s.w.Close()
	ce := s.conn.Close()
	if we != nil {
		return we
	}
	return ce
}

func newSmtpSender(
	protocol string, auth string, uri *url.URL, domain string,
	from *mail.Address, rcpts []*mail.Address,
) (io.WriteCloser, error) {
	var err error
	var conn *smtp.Client
	switch protocol {
	case "smtp":
		conn, err = connectSmtp(true, uri.Host, domain)
	case "smtp+insecure":
		conn, err = connectSmtp(false, uri.Host, domain)
	case "smtps":
		conn, err = connectSmtps(uri.Host)
	default:
		return nil, fmt.Errorf("not a smtp protocol %s", protocol)
	}

	if err != nil {
		return nil, errors.Wrap(err, "Connection failed")
	}

	saslclient, err := newSaslClient(auth, uri)
	if err != nil {
		conn.Close()
		return nil, err
	}
	if saslclient != nil {
		if err := conn.Auth(saslclient); err != nil {
			conn.Close()
			return nil, errors.Wrap(err, "conn.Auth")
		}
	}
	s := &smtpSender{
		conn: conn,
	}
	if err := s.conn.Mail(from.Address, nil); err != nil {
		conn.Close()
		return nil, errors.Wrap(err, "conn.Mail")
	}
	for _, rcpt := range rcpts {
		if err := s.conn.Rcpt(rcpt.Address, nil); err != nil {
			conn.Close()
			return nil, errors.Wrap(err, "conn.Rcpt")
		}
	}
	s.w, err = s.conn.Data()
	if err != nil {
		conn.Close()
		return nil, errors.Wrap(err, "conn.Data")
	}
	return s.w, nil
}