Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
name: Bug report
title: "[BUG] "
labels: bug
about: Create a report to help us improve mailer
---

**Describe the bug**
A clear and concise description of what the bug is.

**To Reproduce**
Steps to reproduce the behavior.

**Expected behavior**
A clear and concise description of what you expected to happen.

**Environment**

- Go version:
- OS:
- Package version:

**Additional context**
Add any other context about the problem here.
8 changes: 8 additions & 0 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Security report
url: https://github.com/slashdevops/mailer/security/policy
about: Please report security vulnerabilities privately.
- name: Questions and support
url: https://github.com/slashdevops/mailer/discussions
about: Ask questions and discuss usage with the community.
59 changes: 59 additions & 0 deletions .golangci.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
version: "2"
linters:
# https://golangci-lint.run/usage/linters/#enabled-by-default
enable:
- errcheck
- ineffassign
- staticcheck
- unused

# https://golangci-lint.run/usage/linters/#disabled-by-default
disable:
- govet
- godot
- wsl
- testpackage
- whitespace
- tagalign
- nosprintfhostport
- nlreturn
- nestif
- mnd
- misspell
- lll
- godox
- funlen
- gochecknoinits
- depguard
- goconst
- dupword
- cyclop
- gocognit
- maintidx
- gocyclo
- dupl

settings:
errcheck:
check-type-assertions: false
check-blank: false
disable-default-exclusions: true
exclude-functions:
- (*os.File).Close
- (io.Closer).Close
- (net.Conn).Close
- (*net/smtp.Client).Close
- io/ioutil.ReadFile
- io.Copy(*bytes.Buffer)
- io.Copy(os.Stdout)
- (io.Writer).Write
- (*bufio.Writer).Write
- (*encoding/json.Encoder).Encode
- os.Setenv
- os.Unsetenv
- fmt.Printf
- fmt.Print
- fmt.Println
- fmt.Fprint
- fmt.Fprintf
- (*strings.Builder).WriteString
9 changes: 9 additions & 0 deletions .testcoverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
threshold:
file: 70
package: 70
total: 70

# The example program is runnable documentation, not covered by unit tests.
exclude:
paths:
- ^example/
42 changes: 42 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Changelog

All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- Exported read accessors on `MailContent` (`FromName`, `FromAddress`, `ToName`,
`ToAddress`, `MimeType`, `Subject`, `Body`) so external `MailerService`
implementations can read message fields.
- TLS support in the SMTP transport: implicit TLS/SMTPS (`ImplicitTLS`, implied
on port 465) and opportunistic/required STARTTLS (`RequireTLS`).
- Configurable SMTP `DialTimeout`, `LocalName` (EHLO), and custom `TLSConfig`.
- `MailServiceConfig.QueueSize` to size the buffered queue independently of the
worker count.
- Per-message delivery deadline via `MailServiceConfig.Timeout`.
- Header-injection protection: the builder rejects CR/LF/NUL in header fields.
- Error wrapping via `MailerError.Err`/`Unwrap` for `errors.Is`/`errors.As`.
- In-process SMTP test server covering plain, STARTTLS, and implicit TLS paths;
concurrency tests run under `-race`.
- Project docs (`docs/`), `Makefile`, `.golangci.yaml`, `DEVELOPMENT_GUIDELINES.md`.

### Changed

- **Fixed a deadlock**: `Enqueue` no longer holds an exclusive lock during a
blocking channel send, so a full queue can no longer block `Start`/`Stop`.
Concurrency now uses an `RWMutex` plus an atomic `started` flag.
- SMTP messages are rendered with CRLF line endings and a UTF-8 `Content-Type`.
- `SMTPPort` accepts the full valid range (1–65535) instead of a fixed allowlist.
- Address length validation raised to RFC-compatible bounds (up to 254 chars);
body limit raised to 256 KiB.
- SMTP transport errors now wrap their underlying cause.

### Security

- STARTTLS/implicit TLS support with `RequireTLS` prevents sending credentials
or content over unencrypted connections.
- SMTP header/command injection is prevented at message-build time.
21 changes: 21 additions & 0 deletions CODE_OF_CONDUCT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# Code of Conduct

This project follows the Contributor Covenant Code of Conduct.

## Our pledge

We pledge to make participation in our community a harassment-free experience for everyone.

## Our standards

Examples of behavior that contributes to a positive environment include:

- being respectful and constructive
- accepting feedback gracefully
- focusing on what is best for the community

Examples of unacceptable behavior include:

- harassment or intimidation
- insulting or derogatory comments
- publishing others' private information without permission
26 changes: 26 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Contributing

Thank you for helping improve `mailer`.

## Development workflow

1. Fork the repository and create a topic branch (`feat/...`, `fix/...`, `docs/...`).
2. Make focused changes and add or update tests and documentation.
3. Run the local verification suite:

```sh
make check # gofmt + go vet + go test -race + golangci-lint
```

Or individually: `make fmt`, `make vet`, `make test`, `make lint`, `make cover`.
4. Open a pull request with a clear summary and rationale.

## Pull request expectations

- Include tests for any behavior change; new concurrent code must pass under `-race`.
- Keep the coverage threshold in `.testcoverage.yml` satisfied (`make cover`).
- Keep changes focused and update `CHANGELOG.md`.
- Mention any breaking changes clearly.

See [DEVELOPMENT_GUIDELINES.md](DEVELOPMENT_GUIDELINES.md) for coding, concurrency,
and testing standards.
60 changes: 60 additions & 0 deletions DEVELOPMENT_GUIDELINES.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Development Guidelines

These guidelines keep `mailer` consistent, correct, and easy to maintain.

## Requirements

- Go **1.25+**.
- `golangci-lint` for linting (`make lint`).

## Project layout

The package is intentionally flat (a library, not an application):

| File | Responsibility |
| ---- | -------------- |
| `mailer.go` | `MailContent`, `MailContentBuilder`, validation, `MimeType`, `MailerError`. |
| `service.go` | `MailService` queue and worker pool, `MailQueueError`. |
| `smtp.go` | `MailerSMTP` transport (TLS/STARTTLS, auth, message rendering). |
| `doc.go` | Package-level documentation. |
| `*_test.go` | Unit and integration tests (including an in-process SMTP server). |
| `example/` | A runnable usage example (`package main`). |
| `docs/` | Guides and examples. |

## Coding standards

- Format with `gofmt` (`make fmt`); no unformatted code is merged.
- Keep the package **dependency-free** — standard library only.
- Every exported identifier has a doc comment starting with its name.
- Return typed errors (`*MailerError`, `*MailQueueError`) and wrap causes with
the `Err` field so `errors.Is`/`errors.As` work.
- Log through `log/slog` at appropriate levels; never log credentials or message bodies.
- Public API changes are additive where possible. `MailContent` stays immutable —
add accessors rather than exported fields.

## Concurrency rules

- `MailService` is safe for concurrent use. The `content` channel is closed
exactly once, under the write lock, in `Stop`; `Enqueue` holds the read lock
for the duration of a send so the channel can never be closed mid-send.
- Never perform a blocking channel send while holding an exclusive lock.
- All new concurrent behavior must be covered by tests that pass under `-race`.

## Testing

- Run `make test` (race detector + coverage) before opening a PR.
- Maintain the coverage threshold in `.testcoverage.yml` (`make cover`).
- Prefer the in-process `fakeSMTPServer` over network access for transport tests.
- Table-driven tests for validation and configuration.

## Commit and PR workflow

1. Branch from `main` (`feat/...`, `fix/...`, `docs/...`, `chore/...`).
2. Keep changes focused; add or update tests and docs.
3. Run `make check` locally.
4. Open a PR with a clear summary and rationale; call out breaking changes.

## Releases

Releases are tag-driven (`vX.Y.Z`). Pushing a matching tag triggers the release
workflow. Update `CHANGELOG.md` before tagging.
51 changes: 51 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
GO ?= go
GOLANGCI_LINT ?= golangci-lint
COVER_PROFILE ?= coverage.txt

.DEFAULT_GOAL := help

.PHONY: help
help: ## Show this help.
@grep -hE '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | \
awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-16s\033[0m %s\n", $$1, $$2}'

.PHONY: tidy
tidy: ## Tidy and verify go modules.
$(GO) mod tidy
$(GO) mod verify

.PHONY: fmt
fmt: ## Format all Go sources.
gofmt -w .

.PHONY: vet
vet: ## Run go vet.
$(GO) vet ./...

.PHONY: build
build: ## Build all packages.
$(GO) build ./...

.PHONY: test
test: ## Run tests with the race detector and coverage.
$(GO) test -race -covermode=atomic -coverprofile=$(COVER_PROFILE) ./...

.PHONY: cover
cover: test ## Enforce the coverage threshold from .testcoverage.yml.
$(GO) run github.com/vladopajic/go-test-coverage/v2@latest --config=./.testcoverage.yml --profile=$(COVER_PROFILE)

.PHONY: cover-html
cover-html: test ## Open the HTML coverage report.
$(GO) tool cover -html=$(COVER_PROFILE)

.PHONY: lint
lint: ## Run golangci-lint.
$(GOLANGCI_LINT) run ./...

.PHONY: check
check: fmt vet test lint ## Run the full local verification suite.

.PHONY: clean
clean: ## Remove build and coverage artifacts.
rm -f $(COVER_PROFILE)
$(GO) clean ./...
Loading
Loading