Markus Pesch
953497a1fe
All checks were successful
continuous-integration/drone/push Build is passing
189 lines
4.7 KiB
Go
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,
|
|
}
|
|
}
|