drone-email/pkg/mail/mail.go
Markus Pesch 953497a1fe
All checks were successful
continuous-integration/drone/push Build is passing
fix: close smtp client connection
2022-07-26 13:29:21 +02:00

189 lines
4.7 KiB
Go

package mail
import (
"bytes"
"context"
"crypto/tls"
"fmt"
"io"
"net"
"net/smtp"
"text/template"
"time"
"git.cryptic.systems/volker.raschek/drone-email-docker/pkg/domain"
_ "embed"
)
const (
DefaultSMTPFromAddress = "root@localhost"
DefaultSMTPFromName = "root"
DefaultSMTPHost = "localhost"
DefaultSMTPPort = 587
DefaultSMTPStartTLS = true
DefaultSMTPTLSInsecureSkipVerify = false
DefaultSMTPToAddress = "root@localhost"
)
//go:embed assets/mail.txt
var mailTemplate string
type CIVars struct {
Build *domain.Build
Commit *domain.Commit
DeployTo string
Job *domain.Job
Prev *domain.Prev
PullRequest int
Remote *domain.Remote
Repo *domain.Repo
Tag string
Yaml *domain.Yaml
}
type templateVars struct {
CIVars *CIVars
Recipient string
SMTPSettings *domain.SMTPSettings
}
func (t *templateVars) TimeNowFormat(layout string) string {
return time.Now().Format(layout)
}
type Plugin struct {
smtpSettings *domain.SMTPSettings
}
// Exec will send emails over SMTP
func (p *Plugin) Exec(ctx context.Context, recipients []string, ciVars *CIVars) error {
exists := false
for _, recipient := range recipients {
if recipient == ciVars.Commit.Author.Email {
exists = true
break
}
}
if !exists {
recipients = append(recipients, ciVars.Commit.Author.Email)
}
tpl, err := template.New("mail").Parse(mailTemplate)
if err != nil {
return fmt.Errorf("failed to parse template: %w", err)
}
buf := make([]byte, 0)
buffer := bytes.NewBuffer(buf)
for _, recipient := range recipients {
err = tpl.Execute(buffer, &templateVars{
CIVars: ciVars,
Recipient: recipient,
SMTPSettings: p.smtpSettings,
})
if err != nil {
return fmt.Errorf("failed to generate template: %w", err)
}
err := p.sendMail(recipient, buffer)
if err != nil {
return fmt.Errorf("failed to send mail: %w", err)
}
buffer.Reset()
}
return nil
}
func (p *Plugin) sendMail(recipient string, r io.Reader) error {
// log.Printf("FROM_ADDRESS: %s", p.smtpSettings.FromAddress)
// log.Printf("FROM_NAME: %s", p.smtpSettings.FromName)
// log.Printf("HELO: %s", p.smtpSettings.HELOName)
// log.Printf("HOST: %s", p.smtpSettings.Host)
// log.Printf("PASSWORD: %s", p.smtpSettings.Password)
// log.Printf("USERNAME: %s", p.smtpSettings.Username)
// log.Printf("PORT: %v", p.smtpSettings.Port)
// log.Printf("START_TLS: %v", p.smtpSettings.StartTLS)
// log.Printf("INSECURE: %v", p.smtpSettings.TLSInsecureSkipVerify)
address := fmt.Sprintf("%s:%d", p.smtpSettings.Host, p.smtpSettings.Port)
tcpConn, err := net.Dial("tcp", address)
if err != nil {
return fmt.Errorf("failed to dial a connection to %s: %w", address, err)
}
defer func() { _ = tcpConn.Close() }()
smtpClient, err := smtp.NewClient(tcpConn, p.smtpSettings.Host)
if err != nil {
return fmt.Errorf("failed to initialize a new smtp client: %w", err)
}
defer func() { _ = smtpClient.Close() }()
err = smtpClient.Hello(p.smtpSettings.HELOName)
if err != nil {
return fmt.Errorf("failed to send helo command: %w", err)
}
// #nosec G402
err = smtpClient.StartTLS(&tls.Config{
InsecureSkipVerify: p.smtpSettings.TLSInsecureSkipVerify,
MinVersion: tls.VersionTLS12,
ServerName: p.smtpSettings.Host,
})
if err != nil {
return fmt.Errorf("failed initialize starttls session: %w", err)
}
smtpAuth := smtp.PlainAuth(p.smtpSettings.FromAddress, p.smtpSettings.FromAddress, p.smtpSettings.Password, p.smtpSettings.Host)
err = smtpClient.Auth(smtpAuth)
if err != nil {
return fmt.Errorf("failed to authenticate client: %w", err)
}
err = smtpClient.Mail(p.smtpSettings.FromAddress)
if err != nil {
return fmt.Errorf("failed to sent mail command: %w", err)
}
err = smtpClient.Rcpt(recipient)
if err != nil {
return fmt.Errorf("failed to sent rcpt command for %s: %w", recipient, err)
}
wc, err := smtpClient.Data()
if err != nil {
return fmt.Errorf("failed to send data command: %w", err)
}
defer func() { _ = wc.Close() }()
_, err = io.Copy(wc, r)
if err != nil {
return fmt.Errorf("failed to copy input from passed reader to smtp writer: %w", err)
}
// close smtpClient before defer to avoid returning an error of
// smtpClient.Quit() like the following example:
// Error: failed to execute mail plugin: failed to send mail: failed to send quit command: 250 2.0.0 Ok: queued as C7F009B4ED
err = wc.Close()
if err != nil {
return fmt.Errorf("failed to close smtp client connection: %w", err)
}
err = smtpClient.Quit()
if err != nil {
return fmt.Errorf("failed to send quit command: %w", err)
}
return nil
}
func NewPlugin(config *domain.SMTPSettings) *Plugin {
return &Plugin{
smtpSettings: config,
}
}