Initial Commit
All checks were successful
continuous-integration/drone/push Build is passing

This commit is contained in:
2022-06-27 21:42:59 +02:00
commit daf58bb4ca
30 changed files with 3040 additions and 0 deletions

7
pkg/domain/author.go Normal file
View File

@ -0,0 +1,7 @@
package domain
type Author struct {
Avatar string
Email string
Name string
}

33
pkg/domain/build.go Normal file
View File

@ -0,0 +1,33 @@
package domain
import "time"
type Build struct {
Created int64
Event string
Finished int64
Link string
Number int
Started int64
Status string
}
func (b *Build) CreatedToTimeFormat(format string) string {
return time.Unix(b.Created, 0).Format(format)
}
func (b *Build) FinishedToTimeFormat(format string) string {
return time.Unix(b.Finished, 0).Format(format)
}
func (b *Build) IsEvent(expectedEvent string) bool {
return expectedEvent == b.Event
}
func (b *Build) IsStatus(expectedStatus string) bool {
return expectedStatus == b.Status
}
func (b *Build) StartedToTimeFormat(format string) string {
return time.Unix(b.Started, 0).Format(format)
}

10
pkg/domain/commit.go Normal file
View File

@ -0,0 +1,10 @@
package domain
type Commit struct {
Author *Author
Branch string
Link string
Message string
Ref string
Sha string
}

8
pkg/domain/job.go Normal file
View File

@ -0,0 +1,8 @@
package domain
type Job struct {
ExitCode int
Finished int64
Started int64
Status string
}

6
pkg/domain/prev.go Normal file
View File

@ -0,0 +1,6 @@
package domain
type Prev struct {
Build *PrevBuild
Commit *PrevCommit
}

6
pkg/domain/prevBuild.go Normal file
View File

@ -0,0 +1,6 @@
package domain
type PrevBuild struct {
Number int
Status string
}

5
pkg/domain/prevCommit.go Normal file
View File

@ -0,0 +1,5 @@
package domain
type PrevCommit struct {
Sha string
}

5
pkg/domain/remote.go Normal file
View File

@ -0,0 +1,5 @@
package domain
type Remote struct {
URL string
}

13
pkg/domain/repo.go Normal file
View File

@ -0,0 +1,13 @@
package domain
type Repo struct {
Avatar string
Branch string
FullName string
Link string
Name string
Owner string
Private bool
SCM string
Trusted bool
}

13
pkg/domain/smtp.go Normal file
View File

@ -0,0 +1,13 @@
package domain
type SMTPSettings struct {
FromAddress string
FromName string
HELOName string
Host string
Password string
Port int
StartTLS bool
TLSInsecureSkipVerify bool
Username string
}

6
pkg/domain/yaml.go Normal file
View File

@ -0,0 +1,6 @@
package domain
type Yaml struct {
Signed bool
Verified bool
}

56
pkg/flags/flags.go Normal file
View File

@ -0,0 +1,56 @@
package flags
const (
DRONE_BUILD_CREATED string = "drone-build-created"
DRONE_BUILD_EVENT string = "drone-build-event"
DRONE_BUILD_FINISHED string = "drone-build-finished"
DRONE_BUILD_LINK string = "drone-build-link"
DRONE_BUILD_NUMBER string = "drone-build-number"
DRONE_BUILD_STARTED string = "drone-build-started"
DRONE_BUILD_STATUS string = "drone-build-status"
DRONE_COMMIT_AUTHOR_NAME string = "drone-commit-author-name"
DRONE_COMMIT_AUTHOR_AVATAR string = "drone-commit-author-avatar"
DRONE_COMMIT_AUTHOR_EMAIL string = "drone-commit-author-email"
DRONE_COMMIT_BRANCH string = "drone-commit-branch"
DRONE_COMMIT_LINK string = "drone-commit-link"
DRONE_COMMIT_MESSAGE string = "drone-commit-message"
DRONE_COMMIT_REF string = "drone-commit-ref"
DRONE_COMMIT_SHA string = "drone-commit-sha"
DRONE_DEPLOY_TO string = "drone-deploy-to"
DRONE_JOB_EXIT_CODE string = "drone-job-exit-code"
DRONE_JOB_FINISHED string = "drone-job-finished"
DRONE_JOB_NUMBER string = "drone-job-number"
DRONE_JOB_STARTED string = "drone-job-started"
DRONE_JOB_STATUS string = "drone-job-status"
DRONE_PREV_BUILD_NUMBER string = "drone-prev-build-number"
DRONE_PREV_BUILD_STATUS string = "drone-prev-build-status"
DRONE_PREV_COMMIT_SHA string = "drone-prev-commit-sha"
DRONE_PULL_REQUEST string = "drone-pull-request"
DRONE_REMOTE_URL string = "drone-remote-url"
DRONE_REPO string = "drone-repo"
DRONE_REPO_AVATAR string = "drone-repo-avatar"
DRONE_REPO_BRANCH string = "drone-repo-branch"
DRONE_REPO_LINK string = "drone-repo-link"
DRONE_REPO_NAME string = "drone-repo-name"
DRONE_REPO_OWNER string = "drone-repo-owner"
DRONE_REPO_PRIVATE string = "drone-repo-private"
DRONE_REPO_SCM string = "drone-repo-scm"
DRONE_REPO_TRUSTED string = "drone-repo-trusted"
DRONE_TAG string = "drone-tag"
DRONE_YAML_SIGNED string = "drone-yaml-signed"
DRONE_YAML_VERIFIED string = "drone-yaml-verified"
)
const (
SMTP_FROM_ADDRESS string = "smtp-from-address"
SMTP_FROM_NAME string = "smtp-from-name"
SMTP_HELO string = "smtp-helo"
SMTP_HOST string = "smtp-host"
SMTP_MAIL_SUBJECT string = "smtp-mail-subject"
SMTP_PASSWORD string = "smtp-password"
SMTP_PORT string = "smtp-port"
SMTP_START_TLS string = "smtp-no-start-tls"
SMTP_TLS_INSECURE_SKIP_VERIFY string = "smtp-tls-insecure"
SMTP_TO_ADDRESSES string = "smtp-to-addresses"
SMTP_USERNAME string = "smtp-username"
)

278
pkg/mail/assets/mail.txt Normal file
View File

@ -0,0 +1,278 @@
Date: {{ .TimeNowFormat "Mon, 02 Jan 2006 15:04:05" }}
From: {{ .SMTPSettings.FromName }} <{{ .SMTPSettings.FromAddress }}>
To: {{ .Recipient }}
Subject: [{{ .CIVars.Build.Status }}] {{ .CIVars.Repo.Name }} ({{ .CIVars.Commit.Branch }} - {{ .CIVars.Commit.Sha }})
Content-Type: multipart/alternative;
boundary=3399d59dca7fb53c0236f440e2a402d670fc0abe57faa6f0233e85338b03
--3399d59dca7fb53c0236f440e2a402d670fc0abe57faa6f0233e85338b03
Content-Transfer-Encoding: quoted-printable
Content-Type: text/plain; charset=UTF-8
{{- if .CIVars.Build.IsStatus "success" -}}
Success build #{{ .CIVars.Build.Number }}
{{- else -}}
Failed build #{{ .CIVars.Build.Number }}
{{- end }}
Name: {{ .CIVars.Repo.Name }}
Author: {{ .CIVars.Commit.Author.Name }} <{{ .CIVars.Commit.Author.Email }}>
Branch: {{ .CIVars.Repo.Branch }}
Commit: {{ .CIVars.Commit.Sha }}
Started At: {{ .CIVars.Build.StartedToTimeFormat "2006-02-01 15:04:05" }}
Link: {{ .CIVars.Build.Link }}
--3399d59dca7fb53c0236f440e2a402d670fc0abe57faa6f0233e85338b03
Content-Transfer-Encoding: quoted-printable
Content-Type: text/html; charset=UTF-8
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta name="viewport" content="width=device-width" />
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<style>
* {
margin: 0;
padding: 0;
font-family: "Helvetica Neue", "Helvetica", Helvetica, Arial, sans-serif;
box-sizing: border-box;
font-size: 14px;
}
body {
-webkit-font-smoothing: antialiased;
-webkit-text-size-adjust: none;
width: 100% !important;
height: 100%;
line-height: 1.6;
background-color: #f6f6f6;
}
table td {
vertical-align: top;
}
.body-wrap {
background-color: #f6f6f6;
width: 100%;
}
.container {
display: block !important;
max-width: 600px !important;
margin: 0 auto !important;
/* makes it centered */
clear: both !important;
}
.content {
max-width: 600px;
margin: 0 auto;
display: block;
padding: 20px;
}
.main {
background: #fff;
border: 1px solid #e9e9e9;
border-radius: 3px;
}
.content-wrap {
padding: 20px;
}
.content-block {
padding: 0 0 20px;
}
.header {
width: 100%;
margin-bottom: 20px;
}
h1, h2, h3 {
font-family: "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
color: #000;
margin: 40px 0 0;
line-height: 1.2;
font-weight: 400;
}
h1 {
font-size: 32px;
font-weight: 500;
}
h2 {
font-size: 24px;
}
h3 {
font-size: 18px;
}
hr {
border: 1px solid #e9e9e9;
margin: 20px 0;
height: 1px;
padding: 0;
}
p,
ul,
ol {
margin-bottom: 10px;
font-weight: normal;
}
p li,
ul li,
ol li {
margin-left: 5px;
list-style-position: inside;
}
a {
color: #348eda;
text-decoration: underline;
}
.last {
margin-bottom: 0;
}
.first {
margin-top: 0;
}
.padding {
padding: 10px 0;
}
.aligncenter {
text-align: center;
}
.alignright {
text-align: right;
}
.alignleft {
text-align: left;
}
.clear {
clear: both;
}
.alert {
font-size: 16px;
color: #fff;
font-weight: 500;
padding: 20px;
text-align: center;
border-radius: 3px 3px 0 0;
}
.alert a {
color: #fff;
text-decoration: none;
font-weight: 500;
font-size: 16px;
}
.alert.alert-warning {
background: #ff9f00;
}
.alert.alert-bad {
background: #d0021b;
}
.alert.alert-good {
background: #68b90f;
}
@media only screen and (max-width: 640px) {
h1,
h2,
h3 {
font-weight: 600 !important;
margin: 20px 0 5px !important;
}
h1 {
font-size: 22px !important;
}
h2 {
font-size: 18px !important;
}
h3 {
font-size: 16px !important;
}
.container {
width: 100% !important;
}
.content,
.content-wrapper {
padding: 10px !important;
}
}
</style>
</head>
<body>
<table class="body-wrap">
<tr>
<td></td>
<td class="container" width="600">
<div class="content">
<table class="main" width="100%" cellpadding="0" cellspacing="0">
<tr>
{{ if .CIVars.Build.IsStatus "success" }}
<td class="alert alert-good">
<a href="{{ .CIVars.Build.Link }}">
Successful build #{{ .CIVars.Build.Number }}
</a>
</td>
{{ else }}
<td class="alert alert-bad">
<a href="{{ .CIVars.Build.Link }}">
Failed build #{{ .CIVars.Build.Number }}
</a>
</td>
{{ end }}
</tr>
<tr>
<td class="content-wrap">
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td>
Repo:
</td>
<td>
{{ .CIVars.Repo.Name }}
</td>
</tr>
<tr>
<td>
Author:
</td>
<td>
{{ .CIVars.Commit.Author.Name }} &lt{{ .CIVars.Commit.Author.Email }}&gt
</td>
</tr>
<tr>
<td>
Branch:
</td>
<td>
{{ .CIVars.Commit.Branch }}
</td>
</tr>
<tr>
<td>
Commit:
</td>
<td>
{{ .CIVars.Commit.Sha }}
</td>
</tr>
<tr>
<td>
Started at:
</td>
<td>
{{ .CIVars.Build.StartedToTimeFormat "2006-02-01 15:04:05" }}
</td>
</tr>
</table>
<hr>
<table width="100%" cellpadding="0" cellspacing="0">
<tr>
<td>
{{ .CIVars.Commit.Message }}
</td>
</tr>
</table>
</td>
</tr>
</table>
</div>
</td>
<td></td>
</tr>
</table>
</body>
</html>
--3399d59dca7fb53c0236f440e2a402d670fc0abe57faa6f0233e85338b03--

180
pkg/mail/mail.go Normal file
View File

@ -0,0 +1,180 @@
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)
}
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,
}
}