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