Files
plugin-drone-email/plugin.go
T
Ompragash Viswanathan 5b0a93b192 fix: use WithTLSPolicy instead of WithTLSPortPolicy to preserve user-configured SMTP port
WithTLSPortPolicy calls SetTLSPortPolicy, which checks `c.port == DefaultPort`
(25) to decide whether the port was explicitly set or left as default. When a
user configures port 25 — common for internal relay hosts that accept plaintext
SMTP — the guard condition evaluates true and the library silently overrides the
port to 587 (DefaultPortTLS) with a fallback to 25.

This causes a two-stage failure in environments where port 587 is unreachable:

  1. The primary dial to host:587 blocks for the full 15-second connection
     timeout (go-mail DefaultTimeout) waiting on a firewalled port.
  2. The fallback dial to host:25 fires with an already-expired context,
     producing a misleading "lookup <host>: i/o timeout" DNS error rather
     than the actual root cause (port 587 unreachable).

The previous mail library (gopkg.in/mail.v2) used the port value directly
without any policy-based override, so this regression surfaced only after
migrating to github.com/wneessen/go-mail.

WithTLSPolicy applies the same STARTTLS negotiation semantics (NoTLS or
TLSOpportunistic) without mutating the port, which is the correct choice
when the caller has already provided an explicit port via WithPort.
2026-02-17 19:30:07 +05:30

315 lines
6.7 KiB
Go

package main
import (
"bufio"
"context"
"crypto/tls"
"os"
"github.com/aymerick/douceur/inliner"
"github.com/drone/drone-template-lib/template"
"github.com/jaytaylor/html2text"
log "github.com/sirupsen/logrus"
mail "github.com/wneessen/go-mail"
)
type (
Repo struct {
FullName string
Owner string
Name string
SCM string
Link string
Avatar string
Branch string
Private bool
Trusted bool
}
Remote struct {
URL string
}
Author struct {
Name string
Email string
Avatar string
}
Commit struct {
Sha string
Ref string
Branch string
Link string
Message string
Author Author
}
Build struct {
Number int
Event string
Status string
Link string
Created float64
Started float64
Finished float64
}
PrevBuild struct {
Status string
Number int
}
PrevCommit struct {
Sha string
}
Prev struct {
Build PrevBuild
Commit PrevCommit
}
Job struct {
Status string
ExitCode int
Started float64
Finished float64
}
Yaml struct {
Signed bool
Verified bool
}
Config struct {
FromAddress string
FromName string
Host string
Port int
Username string
Password string
SkipVerify bool
NoStartTLS bool
Recipients []string
RecipientsFile string
RecipientsOnly bool
Subject string
Body string
Attachment string
Attachments []string
ClientHostname string
}
Plugin struct {
Repo Repo
Remote Remote
Commit Commit
Build Build
Prev Prev
Job Job
Yaml Yaml
Tag string
PullRequest int
DeployTo string
Config Config
}
)
// Exec will send emails over SMTP
func (p Plugin) Exec() error {
// Build recipient list
recipientsMap := make(map[string]struct{})
// Add recipients from the config
for _, recipient := range p.Config.Recipients {
if recipient == "" {
log.Warnf("Skipping empty recipient from config")
continue
}
recipientsMap[recipient] = struct{}{}
}
// Add commit author's email if not already present and RecipientsOnly is false
if !p.Config.RecipientsOnly {
if p.Commit.Author.Email != "" {
recipientsMap[p.Commit.Author.Email] = struct{}{}
} else {
log.Warn("Commit author email is empty")
}
}
// Add recipients from the recipients file
if p.Config.RecipientsFile != "" {
f, err := os.Open(p.Config.RecipientsFile)
if err == nil {
scanner := bufio.NewScanner(f)
for scanner.Scan() {
recipient := scanner.Text()
if recipient == "" {
log.Warnf("Skipping empty recipient from file %s", p.Config.RecipientsFile)
continue
}
recipientsMap[recipient] = struct{}{}
}
} else {
log.Errorf("Could not open RecipientsFile %s: %v", p.Config.RecipientsFile, err)
}
}
log.Infof("Recipients: %v", recipientsMap)
// Create mail client with options
options := []mail.Option{
mail.WithPort(p.Config.Port),
}
// Set HELO hostname if provided
if p.Config.ClientHostname != "" {
options = append(options, mail.WithHELO(p.Config.ClientHostname))
}
// Add authentication if provided
if p.Config.Username != "" && p.Config.Password != "" {
options = append(options,
mail.WithSMTPAuth(mail.SMTPAuthPlain),
mail.WithUsername(p.Config.Username),
mail.WithPassword(p.Config.Password),
)
}
// Handle TLS configuration
if p.Config.SkipVerify {
options = append(options, mail.WithTLSConfig(&tls.Config{
InsecureSkipVerify: true,
}))
}
// Handle STARTTLS policy
// Note: Use WithTLSPolicy (not WithTLSPortPolicy) to avoid overriding
// the user-configured port. WithTLSPortPolicy treats port 25 as "default/unset"
// and silently changes it to 587 for TLSOpportunistic/TLSMandatory.
if p.Config.NoStartTLS {
options = append(options, mail.WithTLSPolicy(mail.NoTLS))
} else {
options = append(options, mail.WithTLSPolicy(mail.TLSOpportunistic))
}
client, err := mail.NewClient(p.Config.Host, options...)
if err != nil {
log.Errorf("Error creating mail client: %v", err)
return err
}
// Prepare template context
type Context struct {
Repo Repo
Remote Remote
Commit Commit
Build Build
Prev Prev
Job Job
Yaml Yaml
Tag string
PullRequest int
DeployTo string
}
ctx := Context{
Repo: p.Repo,
Remote: p.Remote,
Commit: p.Commit,
Build: p.Build,
Prev: p.Prev,
Job: p.Job,
Yaml: p.Yaml,
Tag: p.Tag,
PullRequest: p.PullRequest,
DeployTo: p.DeployTo,
}
// Render body in HTML and plain text
renderedBody, err := template.RenderTrim(p.Config.Body, ctx)
if err != nil {
log.Errorf("Could not render body template: %v", err)
return err
}
html, err := inliner.Inline(renderedBody)
if err != nil {
log.Errorf("Could not inline rendered body: %v", err)
return err
}
plainBody, err := html2text.FromString(html)
if err != nil {
log.Errorf("Could not convert html to text: %v", err)
return err
}
// Render subject
subject, err := template.RenderTrim(p.Config.Subject, ctx)
if err != nil {
log.Errorf("Could not render subject template: %v", err)
return err
}
// Dial connection once and reuse for all recipients
if err := client.DialWithContext(context.Background()); err != nil {
log.Errorf("Error while dialing SMTP server: %v", err)
return err
}
defer client.Close()
// Send emails to each recipient
for recipient := range recipientsMap {
msg := mail.NewMsg()
// Set From header with optional name
if p.Config.FromName != "" {
if err := msg.FromFormat(p.Config.FromName, p.Config.FromAddress); err != nil {
log.Errorf("Could not set From header: %v", err)
return err
}
} else {
if err := msg.From(p.Config.FromAddress); err != nil {
log.Errorf("Could not set From header: %v", err)
return err
}
}
// Set To header
if err := msg.To(recipient); err != nil {
log.Errorf("Could not set To header: %v", err)
return err
}
// Set Subject
msg.Subject(subject)
// Set body with plain text and HTML alternatives
msg.SetBodyString(mail.TypeTextPlain, plainBody)
msg.AddAlternativeString(mail.TypeTextHTML, html)
// Add single attachment if specified
if p.Config.Attachment != "" {
if _, err := os.Stat(p.Config.Attachment); err == nil {
msg.AttachFile(p.Config.Attachment)
}
}
// Add multiple attachments
for _, attachment := range p.Config.Attachments {
if _, err := os.Stat(attachment); err == nil {
msg.AttachFile(attachment)
}
}
// Send using existing connection
if err := client.Send(msg); err != nil {
log.Errorf("Could not send email to %q: %v", recipient, err)
return err
}
}
return nil
}