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
6 changes: 3 additions & 3 deletions .github/workflows/gommon.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,16 @@ on:

env:
# run static analysis only with the latest Go version
LATEST_GO_VERSION: "1.21"
LATEST_GO_VERSION: "1.26"

jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
# Each major Go release is supported until there are two newer major releases. https://golang.org/doc/devel/release.html#policy
# Echo tests with last four major releases
go: ["1.18","1.19","1.20","1.21"]
# Matches the go directive floor in go.mod.
go: ["1.23","1.24","1.25","1.26"]
name: ${{ matrix.os }} @ Go ${{ matrix.go }}
runs-on: ${{ matrix.os }}
steps:
Expand Down
19 changes: 15 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
# Gommon [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/labstack/gommon) [![Coverage Status](http://img.shields.io/coveralls/labstack/gommon.svg?style=flat-square)](https://coveralls.io/r/labstack/gommon)

Common packages for Go
- [Bytes](https://github.com/labstack/gommon/tree/master/bytes) - Format/parse bytes.
- [Color](https://github.com/labstack/gommon/tree/master/color) - Style terminal text.
- [Log](https://github.com/labstack/gommon/tree/master/log) - Simple logging.
Common packages for Go.

Requires Go 1.23 or later.

- [Bytes](https://github.com/labstack/gommon/tree/master/bytes) — format/parse byte sizes (binary IEC and decimal SI).
- [Color](https://github.com/labstack/gommon/tree/master/color) — style terminal text.
- [Email](https://github.com/labstack/gommon/tree/master/email) — send email over SMTP; supports STARTTLS and implicit TLS (SMTPS, port 465).
- [Log](https://github.com/labstack/gommon/tree/master/log) — simple leveled logger with text and JSON output.
- [Random](https://github.com/labstack/gommon/tree/master/random) — cryptographically secure random strings over configurable charsets.

## Install

```sh
go get github.com/labstack/gommon
```
20 changes: 11 additions & 9 deletions bytes/bytes_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -246,24 +246,26 @@ func TestBytesParse(t *testing.T) {
assert.Equal(t, int64(10133099161583616), b)
}

// EiB
b, err = Parse("8EiB")
// EiB — 7EiB stays within int64; 8EiB == 2^63 overflows and the
// float-to-int conversion of out-of-range values is
// implementation-dependent per the Go spec.
b, err = Parse("7EiB")
if assert.NoError(t, err) {
assert.True(t, math.MaxInt64 == b-1)
assert.Equal(t, int64(8070450532247928832), b)
}
b, err = Parse("8Ei")
b, err = Parse("7Ei")
if assert.NoError(t, err) {
assert.True(t, math.MaxInt64 == b-1)
assert.Equal(t, int64(8070450532247928832), b)
}

// EiB with spaces
b, err = Parse("8 EiB")
b, err = Parse("7 EiB")
if assert.NoError(t, err) {
assert.True(t, math.MaxInt64 == b-1)
assert.Equal(t, int64(8070450532247928832), b)
}
b, err = Parse("8 Ei")
b, err = Parse("7 Ei")
if assert.NoError(t, err) {
assert.True(t, math.MaxInt64 == b-1)
assert.Equal(t, int64(8070450532247928832), b)
}

// KB
Expand Down
90 changes: 76 additions & 14 deletions email/email.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,19 @@ import (

type (
Email struct {
Auth smtp.Auth
Header map[string]string
Template *template.Template
Auth smtp.Auth
Header map[string]string
Template *template.Template
// TLSConfig, when non-nil, is used for both implicit TLS (SMTPS)
// and STARTTLS. Callers that need a custom root pool or a
// specific ServerName should set this. The config is cloned per
// dial, so callers can reuse a single value across sends; they
// must not mutate it concurrently with an in-flight Send.
TLSConfig *tls.Config
// DialTimeout caps the TCP (and, for SMTPS, TLS) connect phase.
// It does not bound the full SMTP conversation after the client
// is returned. Zero means no caller-imposed timeout.
DialTimeout time.Duration
smtpAddress string
}

Expand Down Expand Up @@ -124,22 +134,17 @@ func (e *Email) Send(m *Message) (err error) {
m.buffer.WriteString(m.boundary)
m.buffer.WriteString("--")

// Dial
c, err := smtp.Dial(e.smtpAddress)
// Dial. Port 465 is SMTPS (implicit TLS) per IANA and always uses
// TLS. Other ports connect plaintext and opportunistically upgrade
// to STARTTLS only if the server advertises it — if the server
// doesn't, the connection stays in the clear. Operators that
// require TLS must use port 465.
c, err := e.dial()
if err != nil {
return
}
defer c.Quit()

// Check if TLS is required
if ok, _ := c.Extension("STARTTLS"); ok {
host, _, _ := net.SplitHostPort(e.smtpAddress)
config := &tls.Config{ServerName: host}
if err = c.StartTLS(config); err != nil {
return err
}
}

// Authenticate
if e.Auth != nil {
if err = c.Auth(e.Auth); err != nil {
Expand Down Expand Up @@ -172,3 +177,60 @@ func (e *Email) Send(m *Message) (err error) {
_, err = m.buffer.WriteTo(wc)
return
}

func (e *Email) dial() (*smtp.Client, error) {
host, port, err := net.SplitHostPort(e.smtpAddress)
if err != nil {
return nil, err
}

// Always clone so we never mutate the caller's TLSConfig.
var tlsConfig *tls.Config
if e.TLSConfig == nil {
tlsConfig = &tls.Config{ServerName: host}
} else {
tlsConfig = e.TLSConfig.Clone()
if tlsConfig.ServerName == "" {
tlsConfig.ServerName = host
}
}

dialer := &net.Dialer{Timeout: e.DialTimeout}

if port == "465" {
conn, err := tls.DialWithDialer(dialer, "tcp", e.smtpAddress, tlsConfig)
if err != nil {
return nil, err
}
c, err := smtp.NewClient(conn, host)
if err != nil {
conn.Close()
return nil, err
}
return c, nil
}

conn, err := dialer.Dial("tcp", e.smtpAddress)
if err != nil {
return nil, err
}
c, err := smtp.NewClient(conn, host)
if err != nil {
conn.Close()
return nil, err
}
// Drive EHLO explicitly so we can surface its error. (*Client).Extension
// triggers a lazy hello() and swallows its error, which would silently
// treat a failed EHLO as "STARTTLS not advertised" and stay cleartext.
if err := c.Hello("localhost"); err != nil {
c.Close()
return nil, err
}
if ok, _ := c.Extension("STARTTLS"); ok {
if err := c.StartTLS(tlsConfig); err != nil {
c.Close()
return nil, err
}
}
return c, nil
}
92 changes: 92 additions & 0 deletions email/email_test.go
Original file line number Diff line number Diff line change
@@ -1 +1,93 @@
package email

import (
"bufio"
"net"
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

// TestDial_PlainAccepts verifies non-465 ports dial plaintext and
// complete the SMTP handshake against a minimal fake server.
func TestDial_PlainAccepts(t *testing.T) {
ln, err := net.Listen("tcp", "127.0.0.1:0")
assert.NoError(t, err)
defer ln.Close()

done := runFakeSMTP(ln)

e := New(ln.Addr().String())
e.DialTimeout = 2 * time.Second

c, err := e.dial()
assert.NoError(t, err)
if c != nil {
// Our fake server never advertises STARTTLS, so dial must have
// stayed plaintext — document that intent.
ok, _ := c.Extension("STARTTLS")
assert.False(t, ok, "fake server should not advertise STARTTLS")
_ = c.Quit()
}
<-done
}

// TestDial_Timeout verifies DialTimeout caps dial wait on unreachable
// hosts. Uses 203.0.113.1 (TEST-NET-3, RFC 5737) which is unroutable.
// Timeout is 500ms to absorb CI scheduler jitter; the upper bound is
// still well under a round-trip expectation.
func TestDial_Timeout(t *testing.T) {
e := New("203.0.113.1:465")
e.DialTimeout = 500 * time.Millisecond

start := time.Now()
_, err := e.dial()
elapsed := time.Since(start)

assert.Error(t, err)
assert.Less(t, elapsed, 3*time.Second)
}

// TestDial_InvalidAddress verifies malformed addresses fail fast.
func TestDial_InvalidAddress(t *testing.T) {
e := New("not-a-valid-address")
_, err := e.dial()
assert.Error(t, err)
}

// runFakeSMTP accepts one connection and drives a minimal SMTP dialog.
// Returns a channel that closes when the connection is done.
func runFakeSMTP(ln net.Listener) <-chan struct{} {
done := make(chan struct{})
go func() {
defer close(done)
conn, err := ln.Accept()
if err != nil {
return
}
defer conn.Close()
conn.SetDeadline(time.Now().Add(5 * time.Second))

_, _ = conn.Write([]byte("220 fake.local ESMTP ready\r\n"))
r := bufio.NewReader(conn)
for {
line, err := r.ReadString('\n')
if err != nil {
return
}
upper := strings.ToUpper(strings.TrimSpace(line))
switch {
case strings.HasPrefix(upper, "EHLO"), strings.HasPrefix(upper, "HELO"):
_, _ = conn.Write([]byte("250-fake.local\r\n250 OK\r\n"))
case strings.HasPrefix(upper, "QUIT"):
_, _ = conn.Write([]byte("221 bye\r\n"))
return
default:
_, _ = conn.Write([]byte("502 not implemented\r\n"))
}
}
}()
return done
}
10 changes: 5 additions & 5 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
module github.com/labstack/gommon

go 1.18
go 1.23.0

require (
github.com/mattn/go-colorable v0.1.13
github.com/mattn/go-isatty v0.0.20
github.com/stretchr/testify v1.8.4
github.com/mattn/go-colorable v0.1.14
github.com/mattn/go-isatty v0.0.21
github.com/stretchr/testify v1.11.1
github.com/valyala/fasttemplate v1.2.2
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
golang.org/x/sys v0.15.0 // indirect
golang.org/x/sys v0.29.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
19 changes: 8 additions & 11 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,22 +1,19 @@
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/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
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/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU=
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
Expand Down
Loading
Loading