diff --git a/.chglog/CHANGELOG.tpl.md b/.chglog/CHANGELOG.tpl.md new file mode 100644 index 0000000..afc339c --- /dev/null +++ b/.chglog/CHANGELOG.tpl.md @@ -0,0 +1,36 @@ +{{ range .Versions }} + + +## {{ if .Tag.Previous }}[{{ .Tag.Name }}]({{ $.Info.RepositoryURL }}/compare/{{ .Tag.Previous.Name }}...{{ .Tag.Name }}){{ else }}{{ .Tag.Name }}{{ end }} ({{ datetime "2006-01-02" .Tag.Date }}) + +{{ range .CommitGroups -}} + +### {{ .Title }} + +{{ range .Commits -}} + +- {{ if .Scope }}**{{ .Scope }}:** {{ end }}{{ .Subject }} + {{ end }} + {{ end -}} + +{{- if .RevertCommits -}} + +### Reverts + +{{ range .RevertCommits -}} + +- {{ .Revert.Header }} + {{ end }} + {{ end -}} + +{{- if .NoteGroups -}} +{{ range .NoteGroups -}} + +### {{ .Title }} + +{{ range .Notes }} +{{ .Body }} +{{ end }} +{{ end -}} +{{ end -}} +{{ end -}} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..1fd59f1 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,2 @@ +# These owners will be the default owners for everything in the repo. +* @go-vela/admins \ No newline at end of file diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..1571ff9 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to make participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +nationality, personal appearance, race, religion, or sexual identity and +orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +- Using welcoming and inclusive language +- Being respectful of differing viewpoints and experiences +- Gracefully accepting constructive criticism +- Focusing on what is best for the community +- Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +- The use of sexualized language or imagery and unwelcome sexual attention or + advances +- Trolling, insulting/derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or electronic + address, without explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at +[opensource@target.com](mailto:opensource@target.com). All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md new file mode 100644 index 0000000..14083c4 --- /dev/null +++ b/.github/CONTRIBUTING.md @@ -0,0 +1,142 @@ +# Contributing + +We'd love to accept your contributions to this project! There are just a few guidelines you need to follow. + +## Bugs + +Bug reports should be opened up as [issues](https://help.github.com/en/github/managing-your-work-on-github/about-issues) on the [go-vela/community](https://github.com/go-vela/community) repository! + +## Feature Requests + +Feature Requests should be opened up as [issues](https://help.github.com/en/github/managing-your-work-on-github/about-issues) on the [go-vela/community](https://github.com/go-vela/community) repository! + +## Pull Requests + +**NOTE: We recommend you start by opening a new issue describing the bug or feature you're intending to fix. Even if you think it's relatively minor, it's helpful to know what people are working on.** + +We are always open to new PRs! You can follow the below guide for learning how you can contribute to the project! + +## Getting Started + +### Prerequisites + +- [Review the commit guide we follow](https://chris.beams.io/posts/git-commit/#seven-rules) - ensure your commits follow our standards +- [Docker](https://docs.docker.com/install/) - building block for local development +- [Docker Compose](https://docs.docker.com/compose/install/) - start up local development +- [Golang](https://golang.org/dl/) - for source code and [dependency management](https://github.com/golang/go/wiki/Modules) +- [Make](https://www.gnu.org/software/make/) - start up local development + +### Setup + +- [Fork](/fork) this repository + +- Clone this repository to your workstation: + +```bash +# Clone the project +git clone git@github.com:go-vela/vela-email.git $HOME/go-vela/vela-email +``` + +- Navigate to the repository code: + +```bash +# Change into the project directory +cd $HOME/go-vela/vela-email +``` + +- Point the original code at your fork: + +```bash +# Add a remote branch pointing to your fork +git remote add fork https://github.com/your_fork/vela-email +``` + +### Development + +**Please review the [local development documentation](../DOCS.md) for more information.** + +- Navigate to the repository code: + +```bash +# change into the cloned project directory +cd $HOME/go-vela/vela-email +``` + +- Write your code and tests to implement the changes you desire. + + - Please be sure to [follow our commit rules](https://chris.beams.io/posts/git-commit/#seven-rules) + - Please address linter warnings appropriately. If you are intentionally violating a rule that triggers a linter, please annotate the respective code with `nolint` declarations [[docs](https://golangci-lint.run/usage/false-positives/)]. we are using the following format for `nolint` declarations: + + ```go + // nolint: // + ``` + + Example: + + ```go + // nolint:gocyclo // legacy function is complex, needs simplification + func superComplexFunction() error { + // .. + } + ``` + + Check the [documentation for more examples](https://golangci-lint.run/usage/false-positives/). + +- Build the repository code: + +```bash +# execute the `build` target with `make` +make build +``` + +- Run the repository code (ensures your changes perform as you desire): + +```bash +# execute the `run` target with `make` +make run +``` + +- Test the repository code (ensures your changes don't break existing functionality): + +```bash +# execute the `test` target with `make` +make test +``` + +- Clean the repository code (ensures your code meets the project standards): + +```bash +# execute the `clean` target with `make` +make clean +``` + +- Push to your fork: + +```bash +# push your code up to your fork +git push fork main +``` + +- Open a pull request! + + - For the title of the pull request, please use the following format for the title: + + ```text + feat(wobble): add hat wobble + ^--^^------^ ^------------^ + | | | + | | +---> Summary in present tense. + | +---> Scope: a noun describing a section of the codebase (optional) + +---> Type: chore, docs, feat, fix, refactor, or test. + ``` + + - feat: adds a new feature (equivalent to a MINOR in Semantic Versioning) + - fix: fixes a bug (equivalent to a PATCH in Semantic Versioning) + - docs: changes to the documentation + - refactor: refactors production code, eg. renaming a variable; doesn't change public API + - test: adds missing tests, refactors tests; no production code change + - chore: updates something without impacting the user (ex: bump a dependency in package.json or go.mod); no production code change + + If a code change introduces a breaking change, place ! suffix after type, ie. feat(change)!: adds breaking change. correlates with MAJOR in semantic versioning. + +Thank you for your contribution! diff --git a/.github/README.md b/.github/README.md new file mode 100644 index 0000000..2fee41a --- /dev/null +++ b/.github/README.md @@ -0,0 +1,29 @@ +# vela-email + +[![license](https://img.shields.io/crates/l/gl.svg)](../LICENSE) + +Vela plugin designed for sending data to email. + +## Documentation + +For installation and usage, please [visit our docs](https://go-vela.github.io/docs). + +## Contributing + +We are always welcome to new pull requests! + +Please see our [contributing](CONTRIBUTING.md) docs for further instructions. + +## Support + +We are always here to help! + +Please see our [support](SUPPORT.md) documentation for further instructions. + +## Copyright and License + +``` +Copyright (c) 2022 Target Brands, Inc. +``` + +[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0) diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 0000000..75826d5 --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,15 @@ +# Support + +Welcome to Vela! To get help with a specific aspect of Vela, we've provided the below information. + +## Bugs or Feature Requests + +We use GitHub for tracking bugs and feature requests. + +Please see our [contributing](CONTRIBUTING.md) documentation for further instructions. + +## Questions + +We use Slack for supporting questions not already covered in the above documents. + +Please join the [#vela](https://gophers.slack.com/app_redirect?channel=CNRRKE8KY) channel. diff --git a/.github/renovate.json b/.github/renovate.json new file mode 100644 index 0000000..df3d2ef --- /dev/null +++ b/.github/renovate.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": ["local>go-vela/renovate-config"] +} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..bfceca3 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,21 @@ +# name of the action +name: build + +# trigger on pull_request or push events +on: + pull_request: + push: + +# pipeline to execute +jobs: + build: + runs-on: ubuntu-latest + container: + image: golang:1.17 + steps: + - name: clone + uses: actions/checkout@v2 + + - name: build + run: | + make build diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..e7f488d --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,67 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +# +# ******** NOTE ******** +# We have attempted to detect the languages in your repository. Please check +# the `language` matrix defined below to confirm you have the correct set of +# supported CodeQL languages. +# +name: "CodeQL" + +on: + push: + branches: [ main ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ main ] + schedule: + - cron: '32 7 * * 6' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + language: [ 'go' ] + # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] + # Learn more: + # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/.github/workflows/prerelease.yml b/.github/workflows/prerelease.yml new file mode 100644 index 0000000..e068955 --- /dev/null +++ b/.github/workflows/prerelease.yml @@ -0,0 +1,42 @@ +# name of the action +name: prerelease + +# trigger on push events with `v*` in tag +on: + push: + tags: + - "v*" + +# pipeline to execute +jobs: + prerelease: + runs-on: ubuntu-latest + container: + image: golang:1.17 + steps: + - name: clone + uses: actions/checkout@v2 + with: + # ensures we fetch tag history for the repository + fetch-depth: 0 + + - name: setup + run: | + # setup git tag in Actions environment + echo "GITHUB_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + + - name: build + env: + GOOS: linux + CGO_ENABLED: "0" + run: | + make build-static-ci + + - name: publish + uses: elgohr/Publish-Docker-Github-Action@master + with: + name: target/vela-email + cache: true + tag_names: true + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..bd8171e --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,35 @@ +# name of the action +name: publish + +# trigger on push events with branch main +on: + push: + branches: [ main ] + +# pipeline to execute +jobs: + publish: + runs-on: ubuntu-latest + container: + image: golang:1.17 + steps: + - name: clone + uses: actions/checkout@v2 + with: + # ensures we fetch tag history for the repository + fetch-depth: 0 + + - name: build + env: + GOOS: linux + CGO_ENABLED: '0' + run: | + make build-static-ci + + - name: publish + uses: elgohr/Publish-Docker-Github-Action@master + with: + name: target/vela-email + cache: true + username: ${{ secrets.DOCKER_USERNAME }} + password: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..bfc2b4b --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,53 @@ +# name of the action +name: release + +# trigger on push events with `v*` in tag +# ignore push events with `v*-rc*` in tag +on: + push: + tags: + - "v*" + - "!v*-rc*" + +# pipeline to execute +jobs: + release: + runs-on: ubuntu-latest + container: + image: golang:1.17 + steps: + - name: clone + uses: actions/checkout@v2 + + - name: tags + run: | + git fetch --tags + + - name: version + id: version + run: | + echo ::set-output name=VERSION::${GITHUB_REF#refs/tags/} + + - name: install + run: | + go get github.com/git-chglog/git-chglog/cmd/git-chglog + go get github.com/github-release/github-release + + - name: changelog + run: | + # https://github.com/git-chglog/git-chglog#git-chglog + $(go env GOPATH)/bin/git-chglog \ + -o $GITHUB_WORKSPACE/CHANGELOG.md \ + ${{ steps.version.outputs.VERSION }} + + - name: release + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # https://github.com/github-release/github-release#how-to-use + $(go env GOPATH)/bin/github-release edit \ + --user go-vela \ + --repo vela-email \ + --tag ${{ steps.version.outputs.VERSION }} \ + --name ${{ steps.version.outputs.VERSION }} \ + --description "$(cat $GITHUB_WORKSPACE/CHANGELOG.md)" diff --git a/.github/workflows/reviewdog.yml b/.github/workflows/reviewdog.yml new file mode 100644 index 0000000..94d21fc --- /dev/null +++ b/.github/workflows/reviewdog.yml @@ -0,0 +1,41 @@ +# name of the action +name: reviewdog + +# trigger on pull_request events +on: + pull_request: + +# pipeline to execute +jobs: + diff-review: + runs-on: ubuntu-latest + container: + image: golang:1.17 + steps: + - name: clone + uses: actions/checkout@v2 + + - name: golangci-lint + uses: reviewdog/action-golangci-lint@v2 + with: + github_token: ${{ secrets.github_token }} + golangci_lint_flags: "--config=.golangci.yml" + fail_on_error: true + filter_mode: diff_context + reporter: github-pr-review + + full-review: + runs-on: ubuntu-latest + container: + image: golang:1.17 + steps: + - name: clone + uses: actions/checkout@v2 + + - name: golangci-lint + uses: reviewdog/action-golangci-lint@v2 + with: + github_token: ${{ secrets.github_token }} + golangci_lint_flags: "--config=.golangci.yml" + fail_on_error: false + filter_mode: nofilter diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..b2f98b5 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,27 @@ +# name of the action +name: test + +# trigger on pull_request or push events +on: + pull_request: + push: + +# pipeline to execute +jobs: + test: + runs-on: ubuntu-latest + container: + image: golang:1.17 + steps: + - name: clone + uses: actions/checkout@v2 + + - name: test + run: | + go test -race -covermode=atomic -coverprofile=coverage.out ./... + + - name: coverage + uses: codecov/codecov-action@v2 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: coverage.out diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..7ace2e2 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,28 @@ +# name of the action +name: validate + +# trigger on pull_request or push events +on: + pull_request: + push: + +# pipeline to execute +jobs: + validate: + runs-on: ubuntu-latest + container: + image: golang:1.17 + steps: + - name: clone + uses: actions/checkout@v2 + + - name: validate + run: | + # Check that go mod tidy produces a zero diff; clean up any changes afterwards. + go mod tidy && git diff --exit-code; code=$?; git checkout -- .; (exit $code) + # Check that go vet ./... produces a zero diff; clean up any changes afterwards. + go vet ./... && git diff --exit-code; code=$?; git checkout -- .; (exit $code) + # Check that go fmt ./... produces a zero diff; clean up any changes afterwards. + go fmt ./... && git diff --exit-code; code=$?; git checkout -- .; (exit $code) + # Check that go fix ./... produces a zero diff; clean up any changes afterwards. + go fix ./... && git diff --exit-code; code=$?; git checkout -- .; (exit $code) diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..90ccd03 --- /dev/null +++ b/.gitignore @@ -0,0 +1,30 @@ + + +# Created by https://www.gitignore.io/api/go + +### Go ### +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +### Go Patch ### +/vendor/ +/Godeps/ + + +# End of https://www.gitignore.io/api/go + +# Binary release folder +release/ + +# IntelliJ project folder +.idea/ \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 0000000..665b1f0 --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,137 @@ +# This is a manually created golangci.com yaml configuration with +# some defaults explicitly provided. There is a large number of +# linters we've enabled that are usually disabled by default. +# +# https://golangci-lint.run/usage/configuration/#config-file + +# This section provides the configuration for how golangci +# outputs it results from the linters it executes. +output: + # colored-line-number|line-number|json|tab|checkstyle|code-climate, default is "colored-line-number" + format: colored-line-number + + # print lines of code with issue, default is true + print-issued-lines: true + + # print linter name in the end of issue text, default is true + print-linter-name: true + + # make issues output unique by line, default is true + uniq-by-line: true + +# This section provides the configuration for each linter +# we've instructed golangci to execute. +linters-settings: + # https://github.com/mibk/dupl + dupl: + threshold: 100 + + # https://github.com/ultraware/funlen + funlen: + lines: 100 + statements: 50 + + # https://github.com/golang/lint + golint: + min-confidence: 0 + + # https://github.com/tommy-muehle/go-mnd + gomnd: + settings: + mnd: + # don't include the "operation" and "assign" + checks: argument,case,condition,return + + # https://github.com/walle/lll + lll: + line-length: 100 + + # https://github.com/mdempsky/maligned + maligned: + suggest-new: true + + # https://github.com/client9/misspell + misspell: + locale: US + + # https://github.com/golangci/golangci-lint/blob/master/pkg/golinters/nolintlint + nolintlint: + allow-leading-space: true # don't require machine-readable nolint directives (i.e. with no leading space) + allow-unused: false # report any unused nolint directives + require-explanation: false # don't require an explanation for nolint directives + require-specific: false # don't require nolint directives to be specific about which linter is being skipped + +# This section provides the configuration for which linters +# golangci will execute. Several of them were disabled by +# default but we've opted to enable them. +linters: + # disable all linters as new linters might be added to golangci + disable-all: true + + # enable a specific set of linters to run + enable: + - bodyclose + - deadcode # enabled by default + - dupl + - errcheck # enabled by default + - funlen + - goconst + - gocyclo + - godot + - gofmt + - goimports + - golint + - gomnd + - goprintffuncname + - gosec + - gosimple # enabled by default + - govet # enabled by default + - ineffassign # enabled by default + - lll + - maligned + - misspell + - nakedret + - nolintlint + - staticcheck # enabled by default + - structcheck # enabled by default + - stylecheck + - typecheck # enabled by default + - unconvert + - unparam + - unused # enabled by default + - varcheck # enabled by default + - whitespace + + # static list of linters we know golangci can run but we've + # chosen to leave disabled for now + # - asciicheck + # - depguard + # - dogsled + # - exhaustive + # - gochecknoinits + # - gochecknoglobals + # - gocognit + # - gocritic + # - godox + # - goerr113 + # - interfacer + # - nestif + # - noctx + # - prealloc + # - rowserrcheck + # - scopelint + # - testpackage + # - wsl + +# This section provides the configuration for how golangci +# will report the issues it finds. +issues: + # Excluding configuration per-path, per-linter, per-text and per-source + exclude-rules: + # prevent linters from running on *_test.go files + - path: _test\.go + linters: + - funlen + - goconst + - gocyclo + - gomnd diff --git a/DOCS.md b/DOCS.md new file mode 100644 index 0000000..cedf995 --- /dev/null +++ b/DOCS.md @@ -0,0 +1,370 @@ +## Description + +This plugin enables you to send data to an email. + +Source Code: https://github.com/go-vela/vela-email + +Registry: https://hub.docker.com/r/target/vela-email + +## Usage + +> **NOTE:** +> +> Users should refrain from using latest as the tag for the Docker image. +> +> It is recommended to use a semantically versioned tag instead. + +### Sample for .vela.yml + +```yaml +secrets: + - name: username + engine: native + key: vela/noreply/username + type: shared + + - name: password + engine: native + key: vela/noreply/password + type: shared + +steps: + - name: email on success + image: target/vela-email:latest + pull: not_present + secrets: [username, password] + ruleset: + status: success + parameters: + from: vela-noreply@fakemail.com + to: [one@email.com, two@email.com] + sendtype: StartTLS + auth: LoginAuth + host: smtp.youremailserver.com + port: 587 + subject: "{{ .VELA_BUILD_COMMIT }}" + text: "Author: {{ .VELA_BUILD_AUTHOR }} - Branch: {{ .VELA_BUILD_BRANCH }}" + + - name: email on failure + image: target/vela-email:latest + pull: not_present + secrets: [username, password] + ruleset: + status: failure + parameters: + from: vela-noreply@fakemail.com + to: [one@email.com, two@email.com] + sendtype: StartTLS + auth: LoginAuth + host: smtp.youremailserver.com + port: 587 + subject: "{{ .VELA_BUILD_COMMIT }}" + text: "Author: {{ .VELA_BUILD_AUTHOR }} - Branch: {{ .VELA_BUILD_BRANCH }}" +``` + +### Sample for .vela.yml with attachment + +```yaml +secrets: + - name: username + engine: native + key: vela/noreply/username + type: shared + + - name: password + engine: native + key: vela/noreply/password + type: shared + +steps: + - name: email on success + image: target/vela-email:latest + pull: not_present + secrets: [username, password] + ruleset: + status: success + parameters: + filename: + sendtype: StartTLS + auth: LoginAuth + host: smtp.youremailserver.com + port: 587 + + - name: email on failure + image: target/vela-email:latest + pull: not_present + secrets: [username, password] + ruleset: + status: failure + parameters: + filename: + sendtype: StartTLS + auth: LoginAuth + host: smtp.youremailserver.com + port: 587 +``` + +### Sample for email attachment + +```text +From: vela-noreply@fakemail.com +To: emailone@email.com, emailtwo@email.com +Subject: Vela Pipeline for {{ .VELA_REPO_FULL_NAME }} {{ .VELA_BUILD_BRANCH }} +Content-Type: text/plain + +BuildAuthor: {{ .VELA_BUILD_AUTHOR }} +BuildAuthorEmail: {{ .VELA_BUILD_AUTHOR_EMAIL }} +BuildBranch: {{ .VELA_BUILD_BRANCH }} +BuildCommit: {{ .VELA_BUILD_COMMIT }} +BuildCreated: {{ .BuildCreated }} +BuildMessage: {{ .VELA_BUILD_MESSAGE }} +BuildNumber: {{ .VELA_BUILD_NUMBER }} +RepositoryFullName: {{ .VELA_REPO_FULL_NAME }} +RepositoryLink: {{ .VELA_REPO_LINK }} +``` + +## Secrets + +> **NOTE:** Users should refrain from configuring sensitive information in your pipeline in plain text. + +### Internal + +The plugin accepts the following `parameters` for authentication: + +| Parameter | Environment Variable Configuration | +| ---------- | ---------------------------------- | +| `username` | `PARAMETER_USERNAME`, `USERNAME` | +| `password` | `PARAMETER_PASSWORD`, `PASSWORD` | + +Users can use [Vela internal secrets](https://go-vela.github.io/docs/tour/secrets/) to substitute these sensitive values at runtime: + +```diff +steps: + - name: email + image: target/vela-email:latest + pull: always ++ secrets: [ username, password ] + parameters: +- username: usernameexample@email.com +``` + +> This example will add the secret to the `email` step as environment variables: +> +> - `USERNAME=` + +### External + +The plugin accepts the following files for authentication: + +| Parameter | Volume Configuration | +| ---------- | ----------------------------------------------------------------- | +| `username` | `/vela/parameters/email/username`, `/vela/secrets/email/username` | +| `password` | `/vela/parameters/email/password`, `/vela/secrets/email/password` | + +Users can use [Vela external secrets](https://go-vela.github.io/docs/concepts/pipeline/secrets/origin/) to substitute these sensitive values at runtime: + +```diff +steps: + - name: email + image: target/vela-email:latest + pull: always + parameters: +- username: usernameexample@email.com +``` + +> This example will read the secret value in the volume stored at `/vela/secrets/` + +## Parameters + +> **NOTE:** +> +> Any values set from a file take precedence over values set from the environment. +> +> VELA environments can be found at [VELA Environments](https://go-vela.github.io/docs/reference/environment/) +> +> All environment variables are wrapped in sprig template. More information on sprig can be found at [sprig docs](http://masterminds.github.io/sprig/) + +The following parameters are used to configure the image: + +### Logging + +| Parameter | Description | Required | Default | Environment Variables | +| ----------- | -------------------------------------------------------------------------- | -------- | ------- | ------------------------------------------ | +| `log_level` | set the log level for the plugin (valid options: `info`, `debug`, `trace`) | `true` | `info` | `PARAMETER_LOG_LEVEL`
`EMAIL_LOG_LEVEL` | + +### Email + +| Parameter | Description | Required | Default | Environment Variables | +| ------------- | ----------------------------------------------------------------- | -------- | ----------------- | ---------------------------------------------- | +| `from` | who the email is being sent from | true | N/A | `PARAMETER_FROM`
`EMAIL_FROM` | +| `to` | who the email is being sent to | true | N/A | `PARAMETER_TO`
`EMAIL_TO` | +| `cc` | carbon copy of the email to be sent to | false | N/A | `PARAMETER_CC`
`EMAIL_CC` | +| `bcc` | blind carbon copy of the email to be sent to | false | N/A | `PARAMETER_BCC`
`EMAIL_BCC` | +| `sender` | who the email being sent from (will overwrite from) | false | N/A | `PARAMETER_SENDER`
`EMAIL_SENDER` | +| `replyto` | email address that will be used for replies | false | N/A | `PARAMETER_REPLYTO`
`EMAIL_REPLYTO` | +| `subject` | subject of the email | false | default subject | `PARAMETER_SUBJECT`
`EMAIL_SUBJECT` | +| `text` | body of the email in plain text format (HTML will overwrite TEXT) | false | N/A | `PARAMETER_TEXT`
`EMAIL_TEXT` | +| `html` | body of the email in hmtl format (HTML will overwrite TEXT) | false | default html body | `PARAMETER_HTML`
`EMAIL_HTML` | +| `readreceipt` | delivery confirmation | false | N/A | `PARAMETER_READRECEIPT`
`EMAIL_READRECEIPT` | + +> **NOTE:** +> +> The parameters To, CC, BCC and ReplyTo accepts an array of emails in the format of: +> +> - [ one@email.com, two@email.com ] or +> +> - [ firstname lastname \, firstname lastname \ ] +> +> Subject, Text body, and HTML body will accept VELA environments with the use of {{ }} such as: +> +> - `{{ .VELA_REPO_FULL_NAME }}` + +### Attachment + +| Parameter | Description | Required | Default | Environment Variables | +| ---------- | --------------------------------------------------------- | -------- | ------- | ---------------------------------------- | +| `filename` | data in attached file will be used to populate the email. | false | N/A | `PARAMETER_FILENAME`
`EMAIL_FILENAME` | + +### SMTP + +| Parameter | Description | Required | Default | Environment Variables | +| ---------- | ------------- | -------- | ------- | ---------------------------------------- | +| `host` | SMTP host | true | N/A | `PARAMETER_HOST`
`EMAIL_HOST` | +| `port` | SMTP port | true | N/A | `PARAMETER_PORT`
`EMAIL_PORT` | +| `username` | SMTP username | true | N/A | `PARAMETER_USERNAME`
`EMAIL_USERNAME` | +| `password` | SMTP password | true | N/A | `PARAMETER_PASSWORD`
`EMAIL_PASSWORD` | + +### TLS + +| Parameter | Description | Required | Default | Environment Variables | +| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | --------- | -------------------------------------------- | +| `servername` | default is set to host address of SMTP server | false | SMTP host | `PARAMETER_SERVERNAME`
`EMAIL_SERVERNAME` | +| `insecureskipverify` | verification of the server's certificate chain and host name. Only use true for testing purposes as this makes TLS susceptible to man-in-middle attacks. | false | false | `PARAMETER_SKIPVERIFY`
`EMAIL_SKIPVERIFY` | + +### Encryption + +| Parameter | Description | Required | Default | Environment Variables | +| ---------- | ----------------------------------------------------------------- | -------- | -------- | ---------------------------------------- | +| `sendtype` | security to send email (valid option: `Plain`, `StartTLS`, `TLS`) | true | StartTLS | `PARAMETER_SENDTYPE`
`EMAIL_SENDTYPE` | + +### Authentication + +| Parameter | Description | Required | Default | Environment Variables | +| --------- | ------------------------------------------------------------- | -------- | --------- | -------------------------------- | +| `auth` | login authentication (valid option: `PlainAuth`, `LoginAuth`) | true | LoginAuth | `PARAMETER_AUTH`
`EMAIL_AUTH` | + +> **NOTE:** +> +> PlainAuth using smtp/auth login for SMTP server. +> +> LoginAuth using a custom login for Office 365/Exchange SMTP server. + +## Variables + +The Plugin provides the following User friendly timestamp variables that can be used in a subject, text, HTML template. +These timestamps are converted to Unix timestamps in UTC. + +- BuildCreated +- BuildEnqueued +- BuildFinished +- BuildStarted + +## Defaults + +> **NOTE:** +> Context-Type options are as follow: +> +> - text/html for HTML based body of message. +> - text/plain for TEXT based body of message. + +### Default subject + +```text +{{ .VELA_REPO_FULL_NAME }} {{ .VELA_BUILD_BRANCH }} - {{ .VELA_BUILD_COMMIT }} +``` + +### Default body in HTML + +```html + + + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Build Number: + + {{ .VELA_BUILD_NUMBER }} + +
Repo:{{ .VELA_REPO_FULL_NAME }}
Author: + {{ .VELA_BUILD_AUTHOR }} ({{ .VELA_BUILD_AUTHOR_EMAIL + }}) +
Branch:{{ .VELA_BUILD_BRANCH }}
Commit:{{ .VELA_BUILD_COMMIT }}
Started at:{{ .BuildCreated }}
+
+ + + + + + +
{{ .VELA_BUILD_MESSAGE }}
+
+
+
+``` + +## Template + +COMING SOON! + +## Troubleshooting + +You can start troubleshooting this plugin by tuning the level of logs being displayed: + +```diff +steps: + - name: send email + image: target/vela-email:latest + pull: always + parameters: ++ log_level: trace + ... +``` + +Below are a list of common problems and how to solve them: diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..2a9c26b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ + +# Copyright (c) 2022 Target Brands, Inc. All rights reserved. +# +# Use of this source code is governed by the LICENSE file in this repository. + +######################################################################## +## docker build --no-cache --target certs -t vela-email:certs . ## +######################################################################## + +FROM alpine as certs + +RUN apk add --update --no-cache ca-certificates + +######################################################### +## docker build --no-cache -t vela-email:local . ## +######################################################### + +FROM scratch + +COPY --from=certs /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt + +COPY release/vela-email /bin/vela-email + +ENTRYPOINT [ "/bin/vela-email" ] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..2954d18 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright (c) 2022 Target Brands, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..eb294e5 --- /dev/null +++ b/Makefile @@ -0,0 +1,258 @@ + +# Copyright (c) 2022 Target Brands, Inc. All rights reserved. +# +# Use of this source code is governed by the LICENSE file in this repository. + +# capture the current date we build the application from +BUILD_DATE = $(shell date +%Y-%m-%dT%H:%M:%SZ) + +# check if a git commit sha is already set +ifndef GITHUB_SHA + # capture the current git commit sha we build the application from + GITHUB_SHA = $(shell git rev-parse HEAD) +endif + +# check if a git tag is already set +ifndef GITHUB_TAG + # capture the current git tag we build the application from + GITHUB_TAG = $(shell git describe --tag --abbrev=0) +endif + +# check if a go version is already set +ifndef GOLANG_VERSION + # capture the current go version we build the application from + GOLANG_VERSION = $(shell go version | awk '{ print $$3 }') +endif + +# create a list of linker flags for building the golang application +LD_FLAGS = -X github.com/go-vela/vela-email/version.Commit=${GITHUB_SHA} -X github.com/go-vela/vela-email/version.Date=${BUILD_DATE} -X github.com/go-vela/vela-email/version.Go=${GOLANG_VERSION} -X github.com/go-vela/vela-email/version.Tag=${GITHUB_TAG} + +# The `clean` target is intended to clean the workspace +# and prepare the local changes for submission. +# +# Usage: `make clean` +.PHONY: clean +clean: tidy vet fmt fix + +# The `run` target is intended to build and +# execute the Docker image for the plugin. +# +# Usage: `make run` +.PHONY: run +run: build docker-build docker-run + +# The `tidy` target is intended to clean up +# the Go module files (go.mod & go.sum). +# +# Usage: `make tidy` +.PHONY: tidy +tidy: + @echo + @echo "### Tidying Go module" + @go mod tidy + +# The `vet` target is intended to inspect the +# Go source code for potential issues. +# +# Usage: `make vet` +.PHONY: vet +vet: + @echo + @echo "### Vetting Go code" + @go vet ./... + +# The `fmt` target is intended to format the +# Go source code to meet the language standards. +# +# Usage: `make fmt` +.PHONY: fmt +fmt: + @echo + @echo "### Formatting Go Code" + @go fmt ./... + +# The `fix` target is intended to rewrite the +# Go source code using old APIs. +# +# Usage: `make fix` +.PHONY: fix +fix: + @echo + @echo "### Fixing Go Code" + @go fix ./... + +# The `test` target is intended to run +# the tests for the Go source code. +# +# Usage: `make test` +.PHONY: test +test: + @echo + @echo "### Testing Go Code" + @go test -race ./... + +# The `test-cover` target is intended to run +# the tests for the Go source code and then +# open the test coverage report. +# +# Usage: `make test-cover` +.PHONY: test-cover +test-cover: + @echo + @echo "### Creating test coverage report" + @go test -race -covermode=atomic -coverprofile=coverage.out ./... + @echo + @echo "### Opening test coverage report" + @go tool cover -html=coverage.out + +# The `build` target is intended to compile +# the Go source code into a binary. +# +# Usage: `make build` +.PHONY: build +build: + @echo + @echo "### Building release/vela-email binary" + GOOS=linux CGO_ENABLED=0 \ + go build -a \ + -ldflags '${LD_FLAGS}' \ + -o release/vela-email \ + github.com/go-vela/vela-email/cmd/vela-email + +# The `build-static` target is intended to compile +# the Go source code into a statically linked binary. +# +# Usage: `make build-static` +.PHONY: build-static +build-static: + @echo + @echo "### Building static release/vela-email binary" + GOOS=linux CGO_ENABLED=0 \ + go build -a \ + -ldflags '-s -w -extldflags "-static" ${LD_FLAGS}' \ + -o release/vela-email \ + github.com/go-vela/vela-email/cmd/vela-email + +# The `build-static-ci` target is intended to compile +# the Go source code into a statically linked binary +# when used within a CI environment. +# +# Usage: `make build-static-ci` +.PHONY: build-static-ci +build-static-ci: + @echo + @echo "### Building CI static release/vela-email binary" + @go build -a \ + -ldflags '-s -w -extldflags "-static" ${LD_FLAGS}' \ + -o release/vela-email \ + github.com/go-vela/vela-email/cmd/vela-email + +# The `check` target is intended to output all +# dependencies from the Go module that need updates. +# +# Usage: `make check` +.PHONY: check +check: check-install + @echo + @echo "### Checking dependencies for updates" + @go list -u -m -json all | go-mod-outdated -update + +# The `check-direct` target is intended to output direct +# dependencies from the Go module that need updates. +# +# Usage: `make check-direct` +.PHONY: check-direct +check-direct: check-install + @echo + @echo "### Checking direct dependencies for updates" + @go list -u -m -json all | go-mod-outdated -direct + +# The `check-full` target is intended to output +# all dependencies from the Go module. +# +# Usage: `make check-full` +.PHONY: check-full +check-full: check-install + @echo + @echo "### Checking all dependencies for updates" + @go list -u -m -json all | go-mod-outdated + +# The `check-install` target is intended to download +# the tool used to check dependencies from the Go module. +# +# Usage: `make check-install` +.PHONY: check-install +check-install: + @echo + @echo "### Installing psampaz/go-mod-outdated" + @go get -u github.com/psampaz/go-mod-outdated + +# The `bump-deps` target is intended to upgrade +# non-test dependencies for the Go module. +# +# Usage: `make bump-deps` +.PHONY: bump-deps +bump-deps: check + @echo + @echo "### Upgrading dependencies" + @go get -u ./... + +# The `bump-deps-full` target is intended to upgrade +# all dependencies for the Go module. +# +# Usage: `make bump-deps-full` +.PHONY: bump-deps-full +bump-deps-full: check + @echo + @echo "### Upgrading all dependencies" + @go get -t -u ./... + +# The `docker-build` target is intended to build +# the Docker image for the plugin. +# +# Usage: `make docker-build` +.PHONY: docker-build +docker-build: + @echo + @echo "### Building vela-email:local image" + @docker build --no-cache -t vela-email:local . + +# The `docker-test` target is intended to execute +# the Docker image for the plugin with test variables. +# +# Usage: `make docker-test` +.PHONY: docker-test +docker-test: + @echo + @echo "### Testing vela-email:local image" + @docker run --rm \ + -e PARAMETER_TO="" \ + -e PARAMETER_FROM="" \ + -e PARAMETER_HOST="" \ + -e PARAMETER_PORT="" \ + -e PARAMETER_USERNAME="" \ + -e PARAMETER_PASSWORD="" \ + -e PARAMETER_AUTH="loginauth" \ + -e PARAMETER_SKIPVERIFY="true" \ + -e PARAMETER_SENDTYPE="starttls" \ + vela-email:local + +# The `docker-run` target is intended to execute +# the Docker image for the plugin. +# +# Usage: `make docker-run` +# .PHONY: docker-run +docker-run: + @echo + @echo "### Executing vela-email:local image" + @docker run --rm \ + -e PARAMETER_TO \ + -e PARAMETER_FROM \ + -e PARAMETER_HOST \ + -e PARAMETER_PORT \ + -e PARAMETER_USERNAME \ + -e PARAMETER_PASSWORD \ + -e PARAMETER_AUTH \ + -e PARAMETER_SENDTYPE \ + vela-email:local + \ No newline at end of file diff --git a/cmd/vela-email/defaults.go b/cmd/vela-email/defaults.go new file mode 100644 index 0000000..1778810 --- /dev/null +++ b/cmd/vela-email/defaults.go @@ -0,0 +1,72 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package main + +// the default subject returns the full repository name (org/repo), the branch and the build commit. +const DefaultSubject = ` +{{ .VELA_REPO_FULL_NAME }} {{ .VELA_BUILD_BRANCH }} - {{ .VELA_BUILD_COMMIT }} +` + +// default html body returns the build link and build number, +// full repository name (org/repo), build author and email, +// branch, build commit, build start time, and build commit message. +const DefaultHTMLBody = ` + + + + + + +
+
+ + + + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Build Number: + {{ .VELA_BUILD_NUMBER }}
Repo:{{ .VELA_REPO_FULL_NAME }}
Author:{{ .VELA_BUILD_AUTHOR }} + ({{ .VELA_BUILD_AUTHOR_EMAIL }})
Branch:{{ .VELA_BUILD_BRANCH }}
Commit:{{ .VELA_BUILD_COMMIT }}
Started at:{{ .BuildCreated }}
+
+ + + + + + +
{{ .VELA_BUILD_MESSAGE }}
+
+
+
+` diff --git a/cmd/vela-email/loginauth.go b/cmd/vela-email/loginauth.go new file mode 100644 index 0000000..b2b1629 --- /dev/null +++ b/cmd/vela-email/loginauth.go @@ -0,0 +1,53 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +// AUTH LOGIN is not built into go std lib smtp. +// This helps with smtp hosts such as O365 and Exchange that uses AUTH LOGIN +package main + +import ( + "errors" + "net/smtp" +) + +// loginAuth struct contains username and password for login authentication. +type loginAuth struct { + username, password string +} + +// LoginAuth returns an Auth that implements Auth Login authentication mechanism. +func LoginAuth(username, password string) smtp.Auth { + return &loginAuth{username, password} +} + +// Start begins an authentication with a server. +// It returns the name of the authentication protocol +// and optionally data to include in the initial AUTH message +// sent to the server. It can return proto == "" to indicate +// that the authentication should be skipped. +// If it returns a non-nil error, the SMTP client aborts +// the authentication attempt and closes the connection. +func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) { + return "LOGIN", []byte{}, nil +} + +// Next continues the authentication. The server has just sent +// the fromServer data. If more is true, the server expects a +// response, which Next should return as toServer; otherwise +// Next should return toServer == nil. +// If Next returns a non-nil error, the SMTP client aborts +// the authentication attempt and closes the connection. +func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) { + if more { + switch string(fromServer) { + case "Username:": + return []byte(a.username), nil + case "Password:": + return []byte(a.password), nil + default: + return nil, errors.New("unknown fromServer") + } + } + return nil, nil +} diff --git a/cmd/vela-email/main.go b/cmd/vela-email/main.go new file mode 100644 index 0000000..2251f17 --- /dev/null +++ b/cmd/vela-email/main.go @@ -0,0 +1,281 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package main + +import ( + "crypto/tls" + "encoding/json" + "fmt" + "os" + "time" + + "github.com/go-vela/vela-email/version" + "github.com/jordan-wright/email" + "github.com/sirupsen/logrus" + "github.com/urfave/cli/v2" +) + +//nolint:funlen // ignore false positive +func main() { + // capture application version information. + pluginVersion := version.New() + + // serialize the version information as pretty JSON + bytes, err := json.MarshalIndent(pluginVersion, "", " ") + if err != nil { + logrus.Fatal(err) + } + + // output the version information to stdout + fmt.Fprintf(os.Stdout, "%s\n", string(bytes)) + + // create new CLI application + app := &cli.App{ + Name: "vela-email", + HelpName: "vela-email", + Usage: "Vela Email plugin for sending Vela build information to a user's email.", + Copyright: "Copyright (c) 2022 Target Brands, Inc. All rights reserved.", + Authors: []*cli.Author{ + { + Name: "Vela Admins", + Email: "vela@target.com", + }, + }, + Action: run, + Compiled: time.Now(), + Version: pluginVersion.Semantic(), + Flags: []cli.Flag{ + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_LOG_LEVEL", "EMAIL_LOG_LEVEL"}, + FilePath: "/vela/parameters/email/log_level,/vela/secrets/email/log_level", + Name: "log.level", + Usage: "set log level - options: (trace|debug|info|warn|error|fatal|panic)", + Value: "info", + }, + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_FROM", "EMAIL_FROM"}, + Name: "from", + Usage: "from address", + }, + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_SENDER", "EMAIL_SENDER"}, + Name: "sender", + Usage: "from address (overwrites from)", + }, + &cli.StringSliceFlag{ + EnvVars: []string{"PARAMETER_REPLYTO", "EMAIL_REPLYTO"}, + Name: "replyto", + Usage: "address to reply to", + }, + &cli.StringSliceFlag{ + EnvVars: []string{"PARAMETER_TO", "EMAIL_TO"}, + Name: "to", + Usage: "to addresses (supports more than one addresses)", + }, + &cli.StringSliceFlag{ + EnvVars: []string{"PARAMETER_BCC", "EMAIL_CC"}, + Name: "bcc", + Usage: "blind carbon copy to addresses (supports more than one addresses)", + }, + &cli.StringSliceFlag{ + EnvVars: []string{"PARAMETER_CC", "EMAIL_CC"}, + Name: "cc", + Usage: "carbon copy to addresses (supports more than one addresses", + }, + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_SUBJECT", "EMAIL_SUBJECT"}, + Name: "subject", + Usage: "subject of email", + }, + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_TEXT", "EMAIL_TEXT"}, + Name: "text", + Usage: "body of message in text format", + }, + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_HTML", "EMAIL_HTML"}, + Name: "html", + Usage: "body of message in html format", + }, + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_READRECEIPT", "EMAIL_READRECEIPT"}, + Name: "readreceipt", + Usage: "request read receipts and delivery notifications", + }, + // SmtpHost flags + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_HOST", "EMAIL_HOST"}, + Name: "host", + Usage: "smtp host", + Required: true, + }, + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_PORT", "EMAIL_PORT"}, + Name: "port", + Usage: "smtp port", + Required: true, + }, + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_USERNAME", "USERNAME"}, + FilePath: "/vela/parameters/email/username,/vela/secrets/email/username", + Name: "username", + Usage: "smtp host username", + }, + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_PASSWORD", "PASSWORD"}, + FilePath: "/vela/parameters/email/username,/vela/secrets/email/username", + Name: "password", + Usage: "smtp host password", + }, + // Attachment flag + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_FILENAME", "EMAIL_FILENAME"}, + Name: "filename", + Usage: "file to attach to email", + }, + // TLSConfig flag + &cli.BoolFlag{ + EnvVars: []string{"PARAMETER_SKIPVERIFY", "EMAIL_SKIPVERIFY"}, + Name: "skipverify", + Usage: "skip tls verify", + }, + // SendType flag + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_SENDTYPE", "EMAIL_SENDTYPE"}, + Name: "sendtype", + Usage: "send type options: (Plain|StartTLS|TLS) default is set to StartTLS", + Value: "StartTLS", + }, + // Auth flag + &cli.StringFlag{ + EnvVars: []string{"PARAMETER_AUTH", "EMAIL_AUTH"}, + Name: "auth", + Usage: "authentication for login type (PlainAuth|LoginAuth) default is set to nil", + }, + // Build Flags + &cli.IntFlag{ + EnvVars: []string{"VELA_BUILD_CREATED", "BUILD_CREATED"}, + Name: "build-created", + Usage: "environment variable reference for reading in build created", + }, + &cli.IntFlag{ + EnvVars: []string{"VELA_BUILD_ENQUEUED", "BUILD_ENQUEUED"}, + Name: "build-enqueued", + Usage: "environment variable reference for reading in build enqueued", + }, + &cli.IntFlag{ + EnvVars: []string{"VELA_BUILD_FINISHED", "BUILD_FINISHED"}, + Name: "build-finished", + Usage: "environment variable reference for reading in build finished", + }, + &cli.IntFlag{ + EnvVars: []string{"VELA_BUILD_STARTED", "BUILD_STARTED"}, + Name: "build-started", + Usage: "environment variable reference for reading in build started", + }, + }, + } + + if err := app.Run(os.Args); err != nil { + logrus.Fatal(err) + } +} + +func run(c *cli.Context) error { + switch c.String("log.level") { + case "t", "trace": + logrus.SetLevel(logrus.TraceLevel) + case "d", "debug": + logrus.SetLevel(logrus.DebugLevel) + case "w", "warn": + logrus.SetLevel(logrus.WarnLevel) + case "e", "error": + logrus.SetLevel(logrus.ErrorLevel) + case "f", "fatal": + logrus.SetLevel(logrus.FatalLevel) + case "p", "panic": + logrus.SetLevel(logrus.PanicLevel) + case "i", "info": + fallthrough + default: + logrus.SetLevel(logrus.InfoLevel) + } + + if c.IsSet("ci") { + logrus.SetFormatter(&logrus.TextFormatter{ + DisableColors: true, + FullTimestamp: true, + }) + } else { + logrus.SetFormatter(&logrus.TextFormatter{ + ForceColors: true, + FullTimestamp: false, + PadLevelText: true, + }) + } + + logrus.WithFields(logrus.Fields{ + "code": "https://github.com/go-vela/vela-email", + "docs": "https://go-vela.github.io/docs/plugins/registry/email/", + "registry": "https://hub.docker.com/r/target/vela-email", + }).Info("Vela Email Plugin") + + // create the plugin + p := &Plugin{ + // sendType configuration + SendType: c.String("sendtype"), + // auth configuration + Auth: c.String("auth"), + + // attachment configuration + Attachment: &email.Attachment{ + Filename: c.String("filename"), + }, + + // email configuration + Email: &email.Email{ + ReplyTo: c.StringSlice("replyto"), + From: c.String("from"), + To: c.StringSlice("to"), + Bcc: c.StringSlice("bcc"), + Cc: c.StringSlice("cc"), + Subject: c.String("subject"), + Text: []byte(c.String("text")), + HTML: []byte(c.String("html")), + Sender: c.String("sender"), + ReadReceipt: c.StringSlice("readreceipt"), + }, + + // smtp configuration + SMTPHost: &SMTPHost{ + Host: c.String("host"), + Port: c.String("port"), + Username: c.String("username"), + Password: c.String("password"), + }, + + // tls configuration + TLSConfig: &tls.Config{ + ServerName: c.String("host"), + InsecureSkipVerify: c.Bool("skipverify"), //nolint:gosec // ignore false positive + }, + + // User Friendly Build configuration + BuildEnv: &BuildEnv{ + BuildCreated: time.Unix(int64(c.Int("build-created")), 0).UTC().String(), + BuildEnqueued: time.Unix(int64(c.Int("build-enqueued")), 0).UTC().String(), + BuildFinished: time.Unix(int64(c.Int("build-finished")), 0).UTC().String(), + BuildStarted: time.Unix(int64(c.Int("build-started")), 0).UTC().String(), + }, + } + + // validates the plugin + if err := p.Validate(); err != nil { + return err + } + + // execute the plugin + return p.Exec() +} diff --git a/cmd/vela-email/plugin.go b/cmd/vela-email/plugin.go new file mode 100644 index 0000000..5b11f4e --- /dev/null +++ b/cmd/vela-email/plugin.go @@ -0,0 +1,263 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package main + +import ( + "bytes" + "crypto/tls" + "errors" + "fmt" + "html/template" + "net/smtp" + "os" + "strings" + + "github.com/aymerick/douceur/inliner" + "github.com/jordan-wright/email" + "github.com/sirupsen/logrus" +) + +var ( + // ErrorMissingEmailToParam is returned when the plugin is missing the To email parameter. + ErrorMissingEmailToParam = errors.New("missing email parameter: To") + + // ErrorMissingEmailFromParam is returned when the plugin is missing the From email parameter. + ErrorMissingEmailFromParam = errors.New("missing email parameter: From") + + // ErrorEmptyAttach is returned when the plugin finds the provided attachment to be empty. + ErrorEmptyAttach = errors.New("attachment provided is empty") + + // ErrorMissingSMTPParam is returned when the plugin is missing a smtp host or port parameter. + ErrorMissingSMTPParam = errors.New("missing smtp parameter (host/port)") +) + +// Plugin represents the configuration loaded for the plugin. +type ( + Plugin struct { + // Email arguments loaded for the plugin + Email *email.Email + // Attachment arguments loaded for the plugin + Attachment *email.Attachment + // SmtpHost arguments loaded for the plugin + SMTPHost *SMTPHost + // TLSConfig arguments loaded for the plugin + TLSConfig *tls.Config + // SendType arguments loaded for the plugin + SendType string + // Auth arguments loaded for the plugin + Auth string + // Readable build time environment variables + BuildEnv *BuildEnv + } + + // SMTPHost struct. + SMTPHost struct { + Host string + Port string + Username string + Password string + } + + // User friendly readable Build Environment Variables. + BuildEnv struct { + BuildCreated string + BuildEnqueued string + BuildFinished string + BuildStarted string + } +) + +// Validate checks the plugin parameters needed from the +// user are provided. If the email subject or HTML/text are +// not provided, defaults are set. +func (p *Plugin) Validate() error { + logrus.Trace("entered plugin.Validate") + defer logrus.Trace("exited plugin.Validate") + + logrus.Info("Validating Parameters...") + if len(p.Attachment.Filename) != 0 { + fileInfo, err := os.Stat(p.Attachment.Filename) + if errors.Is(err, os.ErrNotExist) { + return os.ErrNotExist + } + + if fileInfo.Size() == 0 { + return ErrorEmptyAttach + } + + file, err := os.Open(p.Attachment.Filename) + if err != nil { + return err + } + + p.Email, err = email.NewEmailFromReader(file) + if err != nil { + return err + } + + if len(p.Email.To) > 0 { + p.Email.To = stringToSlice(p.Email.To) + } + + if len(p.Email.Cc) > 0 { + p.Email.Cc = stringToSlice(p.Email.Cc) + } + + if len(p.Email.Bcc) > 0 { + p.Email.Bcc = stringToSlice(p.Email.Bcc) + } + } + + if len(p.Email.To) == 0 { + return ErrorMissingEmailToParam + } + + if len(p.Email.From) == 0 { + return ErrorMissingEmailFromParam + } + + if len(p.SMTPHost.Host) == 0 || len(p.SMTPHost.Port) == 0 { + return ErrorMissingSMTPParam + } + + // set defaults + if len(p.Email.Subject) == 0 { + p.Email.Subject = DefaultSubject + } + + if len(p.Email.HTML) == 0 && len(p.Email.Text) == 0 { + p.Email.HTML = []byte(DefaultHTMLBody) + } + + return nil +} + +// Creates an environment map for the plugin to use and adds +// any environment variables in the os environment as well as +// some user friendly build timestamps. +func (p *Plugin) Environment() map[string]string { + logrus.Trace("entered plugin.Environment") + defer logrus.Trace("exited plugin.Environment") + + logrus.Info("Setting up Environment...") + + envMap := map[string]string{} + + for _, v := range os.Environ() { + splitV := strings.Split(v, "=") + if strings.HasPrefix(splitV[0], "VELA_") { + envMap[splitV[0]] = strings.Join(splitV[1:], "=") + } + } + + envMap["BuildCreated"] = p.BuildEnv.BuildCreated + envMap["BuildEnqueued"] = p.BuildEnv.BuildEnqueued + envMap["BuildFinished"] = p.BuildEnv.BuildFinished + envMap["BuildStarted"] = p.BuildEnv.BuildStarted + + return envMap +} + +// Parses subject and body of email to inject environment +// variables. Uses provided authentication type and send type +// to send the email. +func (p *Plugin) Exec() error { + logrus.Trace("entered plugin.Execute") + defer logrus.Trace("exited plugin.Execute") + + logrus.Debug("Parsing Subject...") + subject, err := p.injectEnv(p.Email.Subject) + if err != nil { + return err + } + p.Email.Subject = subject + + if len(p.Email.HTML) > 0 { + logrus.Debug("Parsing HTML...") + body, err := p.injectEnv(string(p.Email.HTML)) + if err != nil { + return err + } + logrus.Debug("Parsing CSS...") + body, err = inliner.Inline(body) + if err != nil { + return err + } + p.Email.HTML = []byte(body) + } else { + logrus.Debug("Parsing Text...") + body, err := p.injectEnv(string(p.Email.Text)) + if err != nil { + return err + } + p.Email.Text = []byte(body) + } + + var auth smtp.Auth + switch strings.ToLower(p.Auth) { + case "plainauth": + logrus.Info("Using login authentication from smtp/PlainAuth...") + auth = smtp.PlainAuth("", p.SMTPHost.Username, p.SMTPHost.Password, p.SMTPHost.Host) + case "loginauth": + logrus.Info("Using login authentication from loginauth/LoginAuth...") + auth = LoginAuth(p.SMTPHost.Username, p.SMTPHost.Password) + default: + logrus.Info("Using no login authentication...") + auth = nil + } + + host := p.SMTPHost.Host + ":" + p.SMTPHost.Port + switch strings.ToLower(p.SendType) { + case "starttls": + logrus.Info("Sending email with StartTLS...") + if err := p.Email.SendWithStartTLS(host, auth, p.TLSConfig); err != nil { + return fmt.Errorf("error sending with StartTLS: %w", err) + } + case "tls": + logrus.Info("Sending email with TLS...") + if err := p.Email.SendWithTLS(host, auth, p.TLSConfig); err != nil { + return fmt.Errorf("error sending with TLS: %w", err) + } + case "plain": + fallthrough + default: + logrus.Info("Sending email with Plain...") + if err := p.Email.Send(host, auth); err != nil { + return fmt.Errorf("error sending with Plain: %w", err) + } + } + + logrus.Info("Plugin finished") + return nil +} + +// Injects environment variables into email template. +func (p *Plugin) injectEnv(str string) (string, error) { + logrus.Trace("entered plugin.InjectEnv") + defer logrus.Trace("exited plugin.InjectEnv") + + // Inject to subject + buffer := new(bytes.Buffer) + + // parse string to template + t := template.Must(template.New("input").Parse(str)) + + err := t.Execute(buffer, p.Environment()) + + return buffer.String(), err +} + +// splits a string of emails and returns them as a slice. +func stringToSlice(s []string) []string { + var slice []string + + for _, e := range s { + if len(e) > 0 { + temp := strings.Split(e, ", ") + slice = append(slice, temp...) + } + } + return slice +} diff --git a/cmd/vela-email/plugin_test.go b/cmd/vela-email/plugin_test.go new file mode 100644 index 0000000..7fc69a4 --- /dev/null +++ b/cmd/vela-email/plugin_test.go @@ -0,0 +1,348 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. +package main + +import ( + "io" + "os" + "strings" + "testing" + "time" + + "github.com/jordan-wright/email" +) + +var ( + mockEmail = &email.Email{ + To: []string{"fakemail1@example.com"}, + From: "fakemail2@example.com", + } + + mockSMTPHost = &SMTPHost{ + Host: "smtphost.com", + Port: "587", + Username: "username", + Password: "password", + } + noAttachment = &email.Attachment{ + Filename: "", + } + + mockBuildEnv = &BuildEnv{ + BuildCreated: time.Unix(int64(1556720958), 0).UTC().String(), + BuildEnqueued: time.Unix(int64(1556720958), 0).UTC().String(), + BuildFinished: time.Unix(int64(1556720958), 0).UTC().String(), + BuildStarted: time.Unix(int64(1556720958), 0).UTC().String(), + } + + mockPlugin = &Plugin{ + Email: mockEmail, + SMTPHost: mockSMTPHost, + Attachment: noAttachment, + BuildEnv: mockBuildEnv, + } +) + +func createMockEnv() { + os.Setenv("VELA_BUILD_CREATED", "1556720958") + os.Setenv("VELA_BUILD_ENQUEUED", "1556720958") + os.Setenv("VELA_BUILD_FINISHED", "1556720958") + os.Setenv("VELA_BUILD_STARTED", "1556720958") + os.Setenv("VELA_BUILD_AUTHOR", "octocat") + os.Setenv("VELA_BUILD_AUTHOR_EMAIL", "octocat@github.com") + os.Setenv("VELA_BUILD_BRANCH", "main") + os.Setenv("VELA_BUILD_COMMIT", "7fd1a60b01f91b314f59955a4e4d4e80d8edf11d") + os.Setenv("VELA_BUILD_LINK", "https://vela-server.localhost/octocat/hello-world/1") + os.Setenv("VELA_BUILD_MESSAGE", "Merge pull request #6 from octocat/patch-1") + os.Setenv("VELA_BUILD_NUMBER", "1") + os.Setenv("VELA_REPO_FULL_NAME", "octocat/hello-world") +} + +func TestValidateSuccess(t *testing.T) { + tests := []struct { + name string + parameters Plugin + }{ + { + name: "return no errors: single To email", + parameters: Plugin{ + Email: mockEmail, + SMTPHost: mockSMTPHost, + Attachment: noAttachment, + }, + }, + { + name: "return no errors: multiple To emails", + parameters: Plugin{ + Email: &email.Email{ + To: []string{"fakemail1@example.com", "fakemail2@example.com"}, + From: "fakemail3@example.com", + }, + SMTPHost: mockSMTPHost, + Attachment: noAttachment, + }, + }, + { + name: "return no errors: no username or password", + parameters: Plugin{ + Email: &email.Email{ + To: []string{"fakemail1@example.com", "fakemail2@example.com"}, + From: "fakemail3@example.com", + }, + SMTPHost: &SMTPHost{ + Host: "smtphost.com", + Port: "587", + }, + Attachment: noAttachment, + }, + }, + { + name: "return no errors: extra email parameters", + parameters: Plugin{ + Email: &email.Email{ + To: []string{"fakemail1@example.com", "fakemail2@example.com"}, + From: "fakemail3@example.com", + ReplyTo: []string{"fakemail@example.com"}, + Bcc: []string{"fakemail@example.com"}, + Cc: []string{"fakemail@example.com"}, + Subject: "subject", + Text: []byte(""), + HTML: []byte(""), + Sender: "sender", + ReadReceipt: []string{"idk"}, + }, + SMTPHost: mockSMTPHost, + Attachment: noAttachment, + }, + }, + { + name: "return no errors: parameters from attachment", + parameters: Plugin{ + Email: &email.Email{ + To: []string{""}, + From: "", + }, + SMTPHost: mockSMTPHost, + Attachment: &email.Attachment{ + Filename: "testdata/example1.txt", + }, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err := test.parameters.Validate(); err != nil { + t.Errorf("Validate() should not have raised an error %s", err) + } + }) + } +} + +func TestValidateErrors(t *testing.T) { + tests := []struct { + name string + parameters Plugin + wantErr error + }{ + { + name: "To missing", + parameters: Plugin{ + Email: &email.Email{ + From: "fakemail@example.com", + }, + Attachment: noAttachment, + }, + wantErr: ErrorMissingEmailToParam, + }, + { + name: "From missing", + parameters: Plugin{ + Email: &email.Email{ + To: []string{"fakemail@example.com"}, + }, + Attachment: noAttachment, + }, + wantErr: ErrorMissingEmailFromParam, + }, + { + name: "Email parameters missing from attachment", + parameters: Plugin{ + Email: &email.Email{ + To: []string{""}, + From: "", + }, + SMTPHost: mockSMTPHost, + Attachment: &email.Attachment{ + Filename: "testdata/badattachment.txt", + }, + }, + wantErr: io.EOF, + }, + { + name: "Email attachment missing", + parameters: Plugin{ + Attachment: &email.Attachment{ + Filename: "testdata/doesnotexist.txt", + }, + }, + wantErr: os.ErrNotExist, + }, + { + name: "Email attachment empty", + parameters: Plugin{ + Attachment: &email.Attachment{ + Filename: "testdata/empty.txt", + }, + }, + wantErr: ErrorEmptyAttach, + }, + { + name: "SMTP host missing", + parameters: Plugin{ + Email: mockEmail, + SMTPHost: &SMTPHost{ + Port: "1902", + }, + Attachment: noAttachment, + }, + wantErr: ErrorMissingSMTPParam, + }, + { + name: "SMTP port missing", + parameters: Plugin{ + Email: mockEmail, + SMTPHost: &SMTPHost{ + Host: "smtphost.com", + }, + Attachment: noAttachment, + }, + wantErr: ErrorMissingSMTPParam, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err := test.parameters.Validate(); err == nil { + t.Errorf("Validate() should have raised an error") + } else if err != test.wantErr { + t.Errorf("Validate() error = %v, wantErr = %v", err, test.wantErr) + } + }) + } +} + +func TestInjectEnvSuccess(t *testing.T) { + tests := []struct { + name string + parameters Plugin + }{ + { + name: "email using empty subject and html", + parameters: *mockPlugin, + }, + { + name: "email using default subject and user text", + parameters: Plugin{ + Email: &email.Email{ + To: []string{"fakemail1@example.com", "fakemail2@example.com"}, + From: "fakemail3@example.com", + Subject: DefaultSubject, + Text: []byte("This is some text for repo: {{ .VELA_REPO_FULL_NAME }}"), + }, + SMTPHost: mockSMTPHost, + Attachment: noAttachment, + BuildEnv: mockBuildEnv, + }, + }, + { + name: "email using user subject and html", + parameters: Plugin{ + Email: &email.Email{ + To: []string{"fakemail1@example.com", "fakemail2@example.com"}, + From: "fakemail3@example.com", + Subject: "Commit failure on vela build: {{ .VELA_BUILD_NUMBER }}", + Text: []byte("This is some text for repo: {{ .VELA_REPO_FULL_NAME }}"), + }, + SMTPHost: mockSMTPHost, + Attachment: noAttachment, + BuildEnv: mockBuildEnv, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err := test.parameters.Validate(); err != nil { + t.Errorf("Validate() should not have raised an error: %s", err) + t.FailNow() + } + createMockEnv() + subject, err := test.parameters.injectEnv(test.parameters.Email.Subject) + if err != nil { + t.Errorf("InjectEnv(subject) should not have raised an error %s", err) + t.FailNow() + } + if strings.Contains(subject, "") { + t.Errorf("InjectEnv(subject) failed to inject all environment variables %s", subject) + } + + var body string + if len(test.parameters.Email.HTML) == 0 { + body, err = test.parameters.injectEnv(string(test.parameters.Email.HTML)) + } else { + body, err = test.parameters.injectEnv(string(test.parameters.Email.Text)) + } + if err != nil { + t.Errorf("InjectEnv(body) should not have raised an error %s", err) + t.FailNow() + } + if strings.Contains(body, "") { + t.Errorf("InjectEnv(body) failed to inject all environment variables %s", body) + } + }) + } +} + +func TestInjectEnvBadVar(t *testing.T) { + tests := []struct { + name string + parameters Plugin + }{ + { + name: "error: using environment variable that doesnt exist", + parameters: Plugin{ + Email: &email.Email{ + To: []string{"fakemail1@example.com", "fakemail2@example.com"}, + From: "fakemail3@example.com", + Subject: "This is a bad subject {{ .SOME_OTHER_VARIABLE }}", + }, + SMTPHost: mockSMTPHost, + Attachment: noAttachment, + BuildEnv: mockBuildEnv, + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if err := test.parameters.Validate(); err != nil { + t.Errorf("Validate() should not have raised an error: %s", err) + t.FailNow() + } + createMockEnv() + os.Setenv("SOME_OTHER_VARIABLE", "check") + subject, err := test.parameters.injectEnv(test.parameters.Email.Subject) + + if err != nil { + t.Errorf("InjectEnv(subject) should not have raised an error %s", err) + t.FailNow() + } + + if strings.Contains(subject, "check") { + t.Errorf("InjectEnv(subject) shouldn't have injected variable: SOME_OTHER_VARIABLE, but did") + } + }) + } +} diff --git a/cmd/vela-email/testdata/badattachment.txt b/cmd/vela-email/testdata/badattachment.txt new file mode 100644 index 0000000..bfd2325 --- /dev/null +++ b/cmd/vela-email/testdata/badattachment.txt @@ -0,0 +1,3 @@ +To: +From: +Subject: Vela Pipeline for {{ .VELA_REPO_FULL_NAME }} {{ .VELA_BUILD_BRANCH }} \ No newline at end of file diff --git a/README.md b/cmd/vela-email/testdata/empty.txt similarity index 100% rename from README.md rename to cmd/vela-email/testdata/empty.txt diff --git a/cmd/vela-email/testdata/example1.txt b/cmd/vela-email/testdata/example1.txt new file mode 100644 index 0000000..917b47a --- /dev/null +++ b/cmd/vela-email/testdata/example1.txt @@ -0,0 +1,6 @@ +From: vela-noreply@fakemail.com +To: fakemail1@example.com, fakemail2@example.com +Subject: Vela Pipeline for {{ .VELA_REPO_FULL_NAME }} {{ .VELA_BUILD_BRANCH }} +Content-Type: text/plain + +BuildAuthor: {{ .VELA_BUILD_AUTHOR }} diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 0000000..cf9fb4a --- /dev/null +++ b/codecov.yml @@ -0,0 +1,52 @@ +# The is the default codecov.io yaml configuration all projects use +# that do not have their own codecov.yml already in the project. +# +# https://docs.codecov.io/docs/codecov-yaml#section-default-yaml + +# This section provides the generic configuration for codecov. +# +# https://docs.codecov.io/docs/codecovyml-reference#section-codecov +codecov: + require_ci_to_pass: yes + +# This section provides the configuration for the +# coverage report codecov analyzes for results. +# +# https://docs.codecov.io/docs/codecovyml-reference#section-coverage +coverage: + precision: 2 + round: down + range: "70...100" + + # https://docs.codecov.io/docs/commit-status + status: + # set rules for the project status report + project: + default: + target: 90% + threshold: 0% + # disable the patch status report + patch: off + # disable the changes status report + changes: off + +# This section provides the configuration for the +# parsers codecov uses for the coverage report. +# +# https://docs.codecov.io/docs/codecovyml-reference#section-parsers +parsers: + gcov: + branch_detection: + conditional: yes + loop: yes + method: no + macro: no + +# This section provides the configuration for the +# comments codecov makes to open pull requests. +# +# https://docs.codecov.io/docs/codecovyml-reference#section-comment +comment: + layout: "reach, diff, flags, files" + behavior: default + require_changes: no diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..7595a8b --- /dev/null +++ b/go.mod @@ -0,0 +1,15 @@ +module github.com/go-vela/vela-email + +go 1.14 + +require ( + github.com/Masterminds/semver/v3 v3.1.1 + github.com/PuerkitoBio/goquery v1.8.0 // indirect + github.com/aymerick/douceur v0.2.0 + github.com/go-vela/types v0.11.0 + github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible + github.com/sirupsen/logrus v1.8.1 + github.com/urfave/cli/v2 v2.3.0 + golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect + golang.org/x/sys v0.0.0-20211013075003-97ac67df715c // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..6c89070 --- /dev/null +++ b/go.sum @@ -0,0 +1,56 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= +github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= +github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= +github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= +github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk= +github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4= +github.com/buildkite/yaml v0.0.0-20181016232759-0caa5f0796e3/go.mod h1:5hCug3EZaHXU3FdCA3gJm0YTNi+V+ooA2qNTiVpky4A= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/drone/envsubst v1.0.3/go.mod h1:N2jZmlMufstn1KEqvbHjw40h1KyTmnVzHcSc9bFiJ2g= +github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/go-vela/types v0.11.0 h1:b7QMak0SLAfQjgUOPDhrS/XPPEswhI+27VZzn/axuJA= +github.com/go-vela/types v0.11.0/go.mod h1:8Oml/G1ATFTJsKdsIsstUuHVLsUv7pl6+EiIyOaUqH0= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY= +github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA= +github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A= +github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/lib/pq v1.10.3/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/microcosm-cc/bluemonday v1.0.16/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE= +github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M= +github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI= +golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c h1:taxlMj0D/1sOAuv/CbSD+MMDof2vbyPTqz5FNYKpXt8= +golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/version/version.go b/version/version.go new file mode 100644 index 0000000..b9a3fdf --- /dev/null +++ b/version/version.go @@ -0,0 +1,65 @@ +// Copyright (c) 2022 Target Brands, Inc. All rights reserved. +// +// Use of this source code is governed by the LICENSE file in this repository. + +package version + +import ( + "fmt" + "runtime" + + "github.com/go-vela/types/version" + + "github.com/Masterminds/semver/v3" + + "github.com/sirupsen/logrus" +) + +var ( + // Arch represents the architecture information for the package. + Arch = runtime.GOARCH + // Commit represents the git commit information for the package. + Commit string + // Compiler represents the compiler information for the package. + Compiler = runtime.Compiler + // Date represents the build date information for the package. + Date string + // Go represents the golang version information for the package. + Go = runtime.Version() + // OS represents the operating system information for the package. + OS = runtime.GOOS + // Tag represents the git tag information for the package. + Tag string +) + +// New creates a new version object for Vela that is used throughout the application. +func New() *version.Version { + // check if a semantic tag was provided + if len(Tag) == 0 { + logrus.Warning("no semantic tag provided - defaulting to v0.0.0") + + // set a fallback default for the tag + Tag = "v0.0.0" + } + + v, err := semver.NewVersion(Tag) + if err != nil { + fmt.Println(fmt.Errorf("unable to parse semantic version for %s: %v", Tag, err)) + } + + return &version.Version{ + Canonical: Tag, + Major: v.Major(), + Minor: v.Minor(), + Patch: v.Patch(), + PreRelease: v.Prerelease(), + Metadata: version.Metadata{ + Architecture: Arch, + BuildDate: Date, + Compiler: Compiler, + GitCommit: Commit, + GoVersion: Go, + OperatingSystem: OS, + }, + } +}