1// Copyright (c) 2012 - Cloud Instruments Co., Ltd.
2//
3// All rights reserved.
4//
5// Redistribution and use in source and binary forms, with or without
6// modification, are permitted provided that the following conditions are met:
7//
8// 1. Redistributions of source code must retain the above copyright notice, this
9//    list of conditions and the following disclaimer.
10// 2. Redistributions in binary form must reproduce the above copyright notice,
11//    this list of conditions and the following disclaimer in the documentation
12//    and/or other materials provided with the distribution.
13//
14// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
15// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
16// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
17// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
18// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
19// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
20// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
21// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
22// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
23// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
24
25package seelog
26
27import (
28	"crypto/tls"
29	"crypto/x509"
30	"errors"
31	"fmt"
32	"io/ioutil"
33	"net/smtp"
34	"path/filepath"
35	"strings"
36)
37
38const (
39	// Default subject phrase for sending emails.
40	DefaultSubjectPhrase = "Diagnostic message from server: "
41
42	// Message subject pattern composed according to RFC 5321.
43	rfc5321SubjectPattern = "From: %s <%s>\nSubject: %s\n\n"
44)
45
46// smtpWriter is used to send emails via given SMTP-server.
47type smtpWriter struct {
48	auth               smtp.Auth
49	hostName           string
50	hostPort           string
51	hostNameWithPort   string
52	senderAddress      string
53	senderName         string
54	recipientAddresses []string
55	caCertDirPaths     []string
56	mailHeaders        []string
57	subject            string
58}
59
60// NewSMTPWriter returns a new SMTP-writer.
61func NewSMTPWriter(sa, sn string, ras []string, hn, hp, un, pwd string, cacdps []string, subj string, headers []string) *smtpWriter {
62	return &smtpWriter{
63		auth:               smtp.PlainAuth("", un, pwd, hn),
64		hostName:           hn,
65		hostPort:           hp,
66		hostNameWithPort:   fmt.Sprintf("%s:%s", hn, hp),
67		senderAddress:      sa,
68		senderName:         sn,
69		recipientAddresses: ras,
70		caCertDirPaths:     cacdps,
71		subject:            subj,
72		mailHeaders:        headers,
73	}
74}
75
76func prepareMessage(senderAddr, senderName, subject string, body []byte, headers []string) []byte {
77	headerLines := fmt.Sprintf(rfc5321SubjectPattern, senderName, senderAddr, subject)
78	// Build header lines if configured.
79	if headers != nil && len(headers) > 0 {
80		headerLines += strings.Join(headers, "\n")
81		headerLines += "\n"
82	}
83	return append([]byte(headerLines), body...)
84}
85
86// getTLSConfig gets paths of PEM files with certificates,
87// host server name and tries to create an appropriate TLS.Config.
88func getTLSConfig(pemFileDirPaths []string, hostName string) (config *tls.Config, err error) {
89	if pemFileDirPaths == nil || len(pemFileDirPaths) == 0 {
90		err = errors.New("invalid PEM file paths")
91		return
92	}
93	pemEncodedContent := []byte{}
94	var (
95		e     error
96		bytes []byte
97	)
98	// Create a file-filter-by-extension, set aside non-pem files.
99	pemFilePathFilter := func(fp string) bool {
100		if filepath.Ext(fp) == ".pem" {
101			return true
102		}
103		return false
104	}
105	for _, pemFileDirPath := range pemFileDirPaths {
106		pemFilePaths, err := getDirFilePaths(pemFileDirPath, pemFilePathFilter, false)
107		if err != nil {
108			return nil, err
109		}
110
111		// Put together all the PEM files to decode them as a whole byte slice.
112		for _, pfp := range pemFilePaths {
113			if bytes, e = ioutil.ReadFile(pfp); e == nil {
114				pemEncodedContent = append(pemEncodedContent, bytes...)
115			} else {
116				return nil, fmt.Errorf("cannot read file: %s: %s", pfp, e.Error())
117			}
118		}
119	}
120	config = &tls.Config{RootCAs: x509.NewCertPool(), ServerName: hostName}
121	isAppended := config.RootCAs.AppendCertsFromPEM(pemEncodedContent)
122	if !isAppended {
123		// Extract this into a separate error.
124		err = errors.New("invalid PEM content")
125		return
126	}
127	return
128}
129
130// SendMail accepts TLS configuration, connects to the server at addr,
131// switches to TLS if possible, authenticates with mechanism a if possible,
132// and then sends an email from address from, to addresses to, with message msg.
133func sendMailWithTLSConfig(config *tls.Config, addr string, a smtp.Auth, from string, to []string, msg []byte) error {
134	c, err := smtp.Dial(addr)
135	if err != nil {
136		return err
137	}
138	// Check if the server supports STARTTLS extension.
139	if ok, _ := c.Extension("STARTTLS"); ok {
140		if err = c.StartTLS(config); err != nil {
141			return err
142		}
143	}
144	// Check if the server supports AUTH extension and use given smtp.Auth.
145	if a != nil {
146		if isSupported, _ := c.Extension("AUTH"); isSupported {
147			if err = c.Auth(a); err != nil {
148				return err
149			}
150		}
151	}
152	// Portion of code from the official smtp.SendMail function,
153	// see http://golang.org/src/pkg/net/smtp/smtp.go.
154	if err = c.Mail(from); err != nil {
155		return err
156	}
157	for _, addr := range to {
158		if err = c.Rcpt(addr); err != nil {
159			return err
160		}
161	}
162	w, err := c.Data()
163	if err != nil {
164		return err
165	}
166	_, err = w.Write(msg)
167	if err != nil {
168		return err
169	}
170	err = w.Close()
171	if err != nil {
172		return err
173	}
174	return c.Quit()
175}
176
177// Write pushes a text message properly composed according to RFC 5321
178// to a post server, which sends it to the recipients.
179func (smtpw *smtpWriter) Write(data []byte) (int, error) {
180	var err error
181
182	if smtpw.caCertDirPaths == nil {
183		err = smtp.SendMail(
184			smtpw.hostNameWithPort,
185			smtpw.auth,
186			smtpw.senderAddress,
187			smtpw.recipientAddresses,
188			prepareMessage(smtpw.senderAddress, smtpw.senderName, smtpw.subject, data, smtpw.mailHeaders),
189		)
190	} else {
191		config, e := getTLSConfig(smtpw.caCertDirPaths, smtpw.hostName)
192		if e != nil {
193			return 0, e
194		}
195		err = sendMailWithTLSConfig(
196			config,
197			smtpw.hostNameWithPort,
198			smtpw.auth,
199			smtpw.senderAddress,
200			smtpw.recipientAddresses,
201			prepareMessage(smtpw.senderAddress, smtpw.senderName, smtpw.subject, data, smtpw.mailHeaders),
202		)
203	}
204	if err != nil {
205		return 0, err
206	}
207	return len(data), nil
208}
209
210// Close closes down SMTP-connection.
211func (smtpw *smtpWriter) Close() error {
212	// Do nothing as Write method opens and closes connection automatically.
213	return nil
214}
215