mirror of
https://github.com/harness-community/drone-email.git
synced 2026-06-04 18:24:18 +08:00
5b0a93b192
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.
315 lines
6.7 KiB
Go
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
|
|
}
|