update deps

This commit is contained in:
Aine 2025-06-24 10:56:57 +03:00
parent 301201c8d2
commit 3ef819de2d
No known key found for this signature in database
GPG key ID: 34969C908CCA2804
311 changed files with 143330 additions and 69305 deletions

View file

@ -18,9 +18,9 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
- uses: extractions/setup-just@v1 - uses: extractions/setup-just@e33e0265a09d6d736e2ee1e0eb685ef1de4669ff # v3
- name: lint - name: lint
uses: golangci/golangci-lint-action@v6 uses: golangci/golangci-lint-action@4afd733a84b1f43292c63897423277bb7f4313a9 # v8.0.0
with: with:
version: latest version: latest
- name: test - name: test
@ -31,16 +31,16 @@ jobs:
runs-on: self-hosted runs-on: self-hosted
steps: steps:
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v1 uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
- name: Login to ghcr.io - name: Login to ghcr.io
uses: docker/login-action@v3 uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker - name: Extract metadata (tags, labels) for Docker
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0
with: with:
images: | images: |
ghcr.io/${{ github.repository }} ghcr.io/${{ github.repository }}
@ -49,7 +49,7 @@ jobs:
type=raw,value=latest,enable=${{ github.ref_name == 'main' }} type=raw,value=latest,enable=${{ github.ref_name == 'main' }}
type=semver,pattern={{raw}} type=semver,pattern={{raw}}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with: with:
platforms: linux/amd64,linux/arm64 platforms: linux/amd64,linux/arm64
push: true push: true
@ -66,7 +66,7 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- uses: actions/setup-go@v5 - uses: actions/setup-go@v5
- name: Release - name: Release
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0
with: with:
version: latest version: latest
args: release --clean args: release --clean

View file

@ -1,19 +1,52 @@
version: "2"
run: run:
concurrency: 4 concurrency: 4
timeout: 30m modules-download-mode: readonly
issues-exit-code: 1 issues-exit-code: 1
tests: true tests: true
build-tags: []
modules-download-mode: readonly
output: output:
formats: formats:
- format: colored-line-number text:
print-issued-lines: true path: stdout
print-linter-name: true print-linter-name: true
sort-results: true print-issued-lines: true
linters:
linters-settings: enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- containedctx
- copyloopvar
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- errchkjson
- errname
- errorlint
- exhaustive
- forcetypeassert
- gocognit
- gocritic
- gocyclo
- gosec
- gosmopolitan
- makezero
- mirror
- misspell
- nestif
- nolintlint
- prealloc
- predeclared
- revive
- sqlclosecheck
- unconvert
- unparam
- usestdlibvars
- wastedassign
settings:
decorder: decorder:
dec-order: dec-order:
- const - const
@ -34,28 +67,21 @@ linters-settings:
default-signifies-exhaustive: true default-signifies-exhaustive: true
gocognit: gocognit:
min-complexity: 15 min-complexity: 15
nestif:
min-complexity: 5
gocritic: gocritic:
enabled-tags: enabled-tags:
- diagnostic - diagnostic
- style - style
- performance - performance
gofmt:
simplify: true
rewrite-rules:
- pattern: 'interface{}'
replacement: 'any'
- pattern: 'a[b:len(a)]'
replacement: 'a[b:]'
gofumpt:
extra-rules: true
grouper: grouper:
const-require-single-const: true const-require-single-const: true
import-require-single-import: true import-require-single-import: true
var-require-single-var: true var-require-single-var: true
misspell: misspell:
locale: US locale: US
nestif:
min-complexity: 5
unparam:
check-exported: true
usestdlibvars: usestdlibvars:
time-month: true time-month: true
time-layout: true time-layout: true
@ -64,78 +90,58 @@ linters-settings:
sql-isolation-level: true sql-isolation-level: true
tls-signature-scheme: true tls-signature-scheme: true
constant-kind: true constant-kind: true
unparam: exclusions:
check-exported: true generated: lax
linters: presets:
disable-all: false - comments
enable: - common-false-positives
- asasalint - legacy
- asciicheck - std-error-handling
- bidichk rules:
- bodyclose - linters:
- containedctx
- copyloopvar
- decorder
- dogsled
- dupl - dupl
- dupword
- durationcheck
- errcheck - errcheck
- errchkjson
- errname
- errorlint
- exhaustive
- forcetypeassert
- gocognit - gocognit
- gocritic
- gocyclo - gocyclo
- gofmt
- gofumpt
- goimports
- gosec
- gosimple
- gosmopolitan
- govet
- ineffassign
- makezero
- mirror
- misspell
- nestif
- nolintlint
- prealloc
- predeclared
- revive
- sqlclosecheck
- staticcheck
- unconvert
- unparam
- unused
- usestdlibvars
- wastedassign
fast: false
issues:
exclude-dirs-use-default: true
exclude-dirs:
- mocks
exclude-rules:
- path: _test\.go
linters:
- gocyclo
- gocognit
- errcheck
- dupl
- gosec - gosec
path: _test\.go
- linters: - linters:
- staticcheck - staticcheck
text: "SA9003:" text: 'SA9003:'
- linters: - linters:
- lll - lll
source: "^//go:generate " source: '^//go:generate '
- linters: - linters:
- revive - revive
text: "returns unexported type" text: returns unexported type
paths:
- mocks
- third_party$
- builtin$
- examples$
issues:
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0
new: false new: false
formatters:
enable:
- gofmt
- gofumpt
- goimports
settings:
gofmt:
simplify: true
rewrite-rules:
- pattern: interface{}
replacement: any
- pattern: a[b:len(a)]
replacement: a[b:]
gofumpt:
extra-rules: true
exclusions:
generated: lax
paths:
- mocks
- third_party$
- builtin$
- examples$

47
go.mod
View file

@ -1,26 +1,24 @@
module github.com/etkecc/postmoogle module github.com/etkecc/postmoogle
go 1.23.0 go 1.24.3
toolchain go1.23.5
require ( require (
github.com/archdx/zerolog-sentry v1.8.5 github.com/archdx/zerolog-sentry v1.8.5
github.com/emersion/go-msgauth v0.7.0 github.com/emersion/go-msgauth v0.7.0
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
github.com/emersion/go-smtp v0.21.3 github.com/emersion/go-smtp v0.22.0
github.com/etkecc/go-env v1.2.1 github.com/etkecc/go-env v1.2.1
github.com/etkecc/go-fswatcher v1.0.1 github.com/etkecc/go-fswatcher v1.0.1
github.com/etkecc/go-healthchecks/v2 v2.2.2 github.com/etkecc/go-healthchecks/v2 v2.2.2
github.com/etkecc/go-kit v1.6.0 github.com/etkecc/go-kit v1.7.0
github.com/etkecc/go-linkpearl v0.0.0-20250416104026-a65bda9f5f37 github.com/etkecc/go-linkpearl v0.0.0-20250617213914-419fd498ee39
github.com/etkecc/go-mxidwc v1.0.1 github.com/etkecc/go-mxidwc v1.0.1
github.com/etkecc/go-secgen v1.4.0 github.com/etkecc/go-secgen v1.4.0
github.com/etkecc/go-validator/v2 v2.2.4 github.com/etkecc/go-validator/v2 v2.2.4
github.com/fsnotify/fsnotify v1.9.0 github.com/fsnotify/fsnotify v1.9.0
github.com/gabriel-vasile/mimetype v1.4.9 github.com/gabriel-vasile/mimetype v1.4.9
github.com/getsentry/sentry-go v0.32.0 github.com/getsentry/sentry-go v0.34.0
github.com/jhillyerd/enmime v1.3.0 github.com/jhillyerd/enmime/v2 v2.2.0
github.com/kvannotten/mailstrip v0.0.0-20200711213611-0002f5c0467e github.com/kvannotten/mailstrip v0.0.0-20200711213611-0002f5c0467e
github.com/lib/pq v1.10.9 github.com/lib/pq v1.10.9
github.com/mcnijman/go-emailaddress v1.1.1 github.com/mcnijman/go-emailaddress v1.1.1
@ -28,9 +26,9 @@ require (
github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39 github.com/raja/argon2pw v1.0.2-0.20210910183755-a391af63bd39
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
github.com/swaggo/swag v1.16.3 github.com/swaggo/swag v1.16.3
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b
maunium.net/go/mautrix v0.23.3 maunium.net/go/mautrix v0.24.1
modernc.org/sqlite v1.37.0 modernc.org/sqlite v1.38.0
) )
require ( require (
@ -43,6 +41,7 @@ require (
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/etkecc/go-trysmtp v1.1.5 // indirect github.com/etkecc/go-trysmtp v1.1.5 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/jsonreference v0.19.6 // indirect github.com/go-openapi/jsonreference v0.19.6 // indirect
github.com/go-openapi/spec v0.20.4 // indirect github.com/go-openapi/spec v0.20.4 // indirect
@ -50,7 +49,7 @@ require (
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect github.com/inbucket/html2text v1.0.0 // indirect
github.com/josharian/intern v1.0.0 // indirect github.com/josharian/intern v1.0.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
@ -59,8 +58,10 @@ require (
github.com/mattn/go-sqlite3 v1.14.28 // indirect github.com/mattn/go-sqlite3 v1.14.28 // indirect
github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect github.com/mikesmitty/edkey v0.0.0-20170222072505-3356ea4e686a // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/olekukonko/tablewriter v0.0.5 // indirect github.com/olekukonko/errors v1.1.0 // indirect
github.com/petermattis/goid v0.0.0-20250319124200-ccd6737f222a // indirect github.com/olekukonko/ll v0.0.9 // indirect
github.com/olekukonko/tablewriter v1.0.7 // indirect
github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.7 // indirect github.com/rivo/uniseg v0.4.7 // indirect
@ -69,15 +70,15 @@ require (
github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
github.com/yuin/goldmark v1.7.10 // indirect github.com/yuin/goldmark v1.7.12 // indirect
go.mau.fi/util v0.8.6 // indirect go.mau.fi/util v0.8.8 // indirect
golang.org/x/crypto v0.37.0 // indirect golang.org/x/crypto v0.39.0 // indirect
golang.org/x/net v0.39.0 // indirect golang.org/x/net v0.41.0 // indirect
golang.org/x/sys v0.32.0 // indirect golang.org/x/sys v0.33.0 // indirect
golang.org/x/text v0.24.0 // indirect golang.org/x/text v0.26.0 // indirect
golang.org/x/tools v0.32.0 // indirect golang.org/x/tools v0.34.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
modernc.org/libc v1.63.0 // indirect modernc.org/libc v1.66.0 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.10.0 // indirect modernc.org/memory v1.11.0 // indirect
) )

118
go.sum
View file

@ -25,21 +25,20 @@ github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkp
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/emersion/go-msgauth v0.7.0 h1:vj2hMn6KhFtW41kshIBTXvp6KgYSqpA/ZN9Pv4g1INc= github.com/emersion/go-msgauth v0.7.0 h1:vj2hMn6KhFtW41kshIBTXvp6KgYSqpA/ZN9Pv4g1INc=
github.com/emersion/go-msgauth v0.7.0/go.mod h1:mmS9I6HkSovrNgq0HNXTeu8l3sRAAuQ9RMvbM4KU7Ck= github.com/emersion/go-msgauth v0.7.0/go.mod h1:mmS9I6HkSovrNgq0HNXTeu8l3sRAAuQ9RMvbM4KU7Ck=
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ= github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
github.com/emersion/go-smtp v0.21.3 h1:7uVwagE8iPYE48WhNsng3RRpCUpFvNl39JGNSIyGVMY= github.com/emersion/go-smtp v0.22.0 h1:/d3HWxkZZ4riB+0kzfoODh9X+xyCrLEezMnAAa1LEMU=
github.com/emersion/go-smtp v0.21.3/go.mod h1:qm27SGYgoIPRot6ubfQ/GpiPy/g3PaZAVRxiO/sDUgQ= github.com/emersion/go-smtp v0.22.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
github.com/etkecc/go-env v1.2.1 h1:b/mIa8D1d9hc3rI8h5bEtBHsnKkBZ6UbmYPog3QIPTU= github.com/etkecc/go-env v1.2.1 h1:b/mIa8D1d9hc3rI8h5bEtBHsnKkBZ6UbmYPog3QIPTU=
github.com/etkecc/go-env v1.2.1/go.mod h1:yTs1DWEsllAZYA417r4V+OmMuYinGXtBzKkLGNvCv5c= github.com/etkecc/go-env v1.2.1/go.mod h1:yTs1DWEsllAZYA417r4V+OmMuYinGXtBzKkLGNvCv5c=
github.com/etkecc/go-fswatcher v1.0.1 h1:n9hqtjzTS3ETb9hcZe1EqYA1lkkhlzZlztu3hXwFDQc= github.com/etkecc/go-fswatcher v1.0.1 h1:n9hqtjzTS3ETb9hcZe1EqYA1lkkhlzZlztu3hXwFDQc=
github.com/etkecc/go-fswatcher v1.0.1/go.mod h1:O5TODJ9z6Qb7X+snqHbB+F0Pah6G497Wdg0SFE/UYpE= github.com/etkecc/go-fswatcher v1.0.1/go.mod h1:O5TODJ9z6Qb7X+snqHbB+F0Pah6G497Wdg0SFE/UYpE=
github.com/etkecc/go-healthchecks/v2 v2.2.2 h1:YV7e+Ga8JY3aZX4Qf6Q1Ca+DnqbT5Drjli3PufB4J1g= github.com/etkecc/go-healthchecks/v2 v2.2.2 h1:YV7e+Ga8JY3aZX4Qf6Q1Ca+DnqbT5Drjli3PufB4J1g=
github.com/etkecc/go-healthchecks/v2 v2.2.2/go.mod h1:IowWGN4F6By6z0eh63+639OscylgtdvT9ITjfN6hnZA= github.com/etkecc/go-healthchecks/v2 v2.2.2/go.mod h1:IowWGN4F6By6z0eh63+639OscylgtdvT9ITjfN6hnZA=
github.com/etkecc/go-kit v1.6.0 h1:pNbIONvOuRaWuP0F4JLq2ASBxnsCEq5CFY3KeKdhaVY= github.com/etkecc/go-kit v1.7.0 h1:jxJ+tWfWwYqgNgfdbZ2va/M0AHyDdKwc8lYGXrFuWsc=
github.com/etkecc/go-kit v1.6.0/go.mod h1:yikghi8YaYbTjRXNtx82g0LFv90YqZi2vLf5Chw0ysg= github.com/etkecc/go-kit v1.7.0/go.mod h1:yikghi8YaYbTjRXNtx82g0LFv90YqZi2vLf5Chw0ysg=
github.com/etkecc/go-linkpearl v0.0.0-20250416104026-a65bda9f5f37 h1:ZWUy35kMjyTXTU7cwHyyV1ttrOIBZlg/+z8zmbevj98= github.com/etkecc/go-linkpearl v0.0.0-20250617213914-419fd498ee39 h1:wZ+Iv47qoBy3jzzVgzS52KkQWcddUuDsO5s59J2q+1g=
github.com/etkecc/go-linkpearl v0.0.0-20250416104026-a65bda9f5f37/go.mod h1:iXvKPwSFgTG9DyEMN9UMuUNeZrZFYlGoBsZZNbiTMdQ= github.com/etkecc/go-linkpearl v0.0.0-20250617213914-419fd498ee39/go.mod h1:v4QqBXgDZ4Hz235SqyaHde1FL+dNpdnjX4tTR4kxcaI=
github.com/etkecc/go-mxidwc v1.0.1 h1:t4Kq3FxSlQjt1i7RpzE5q3cOWjJ0vrTzzGZRSpgh8mg= github.com/etkecc/go-mxidwc v1.0.1 h1:t4Kq3FxSlQjt1i7RpzE5q3cOWjJ0vrTzzGZRSpgh8mg=
github.com/etkecc/go-mxidwc v1.0.1/go.mod h1:WFlntcH4mdual/gNi6X7a6rSJERNuLjdrwM3K/tucQA= github.com/etkecc/go-mxidwc v1.0.1/go.mod h1:WFlntcH4mdual/gNi6X7a6rSJERNuLjdrwM3K/tucQA=
github.com/etkecc/go-secgen v1.4.0 h1:PapLpen3aIqG2LDu+U6KWDb4SMW2UGuvetpJjd86yUc= github.com/etkecc/go-secgen v1.4.0 h1:PapLpen3aIqG2LDu+U6KWDb4SMW2UGuvetpJjd86yUc=
@ -48,12 +47,14 @@ github.com/etkecc/go-trysmtp v1.1.5 h1:MQ4l4bE9nWUqRKoJOalGHGLkb/hRmzbcBMlClufEO
github.com/etkecc/go-trysmtp v1.1.5/go.mod h1:J4zQu+kgM37xQY148wUz0Dm6y+YhtzMghAv/AaXt0sQ= github.com/etkecc/go-trysmtp v1.1.5/go.mod h1:J4zQu+kgM37xQY148wUz0Dm6y+YhtzMghAv/AaXt0sQ=
github.com/etkecc/go-validator/v2 v2.2.4 h1:E4sE4FpYPVP6VRvnndni+8qUXbeSuHxkniFsi6uCRFQ= github.com/etkecc/go-validator/v2 v2.2.4 h1:E4sE4FpYPVP6VRvnndni+8qUXbeSuHxkniFsi6uCRFQ=
github.com/etkecc/go-validator/v2 v2.2.4/go.mod h1:5VX2VICgMALoFBiGc02p/6RjQ8ifRxZ0KOuLAH5fTe0= github.com/etkecc/go-validator/v2 v2.2.4/go.mod h1:5VX2VICgMALoFBiGc02p/6RjQ8ifRxZ0KOuLAH5fTe0=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY= github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok= github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
github.com/getsentry/sentry-go v0.32.0 h1:YKs+//QmwE3DcYtfKRH8/KyOOF/I6Qnx7qYGNHCGmCY= github.com/getsentry/sentry-go v0.34.0 h1:1FCHBVp8TfSc8L10zqSwXUZNiOSF+10qw4czjarTiY4=
github.com/getsentry/sentry-go v0.32.0/go.mod h1:CYNcMMz73YigoHljQRG+qPF+eMq8gG72XcGN/p71BAY= github.com/getsentry/sentry-go v0.34.0/go.mod h1:C55omcY9ChRQIUcVcGcs+Zdy4ZpQGvNJ7JYHIoSWOtE=
github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA=
github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og=
github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= github.com/go-openapi/jsonpointer v0.19.3/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
@ -66,8 +67,8 @@ github.com/go-openapi/spec v0.20.4/go.mod h1:faYFR1CvsJZ0mNsmsphTMSoRrNV3TEDoAM7
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM=
github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ= github.com/go-openapi/swag v0.19.15/go.mod h1:QYRuS/SOXUCsnplDa677K7+DxSOj6IPNl/eQntq43wQ=
github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs= github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14= github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
@ -79,10 +80,10 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA= github.com/inbucket/html2text v1.0.0 h1:N5kza++4uBBDJ2Z3KUnTRyPNoBcW+YfOgNiNmNB+sgs=
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk= github.com/inbucket/html2text v1.0.0/go.mod h1:5TrhXQKGU+LXurODaSm55Y9eXoPBRnYiOz4x2XfUoJU=
github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99rw= github.com/jhillyerd/enmime/v2 v2.2.0 h1:Pe35MB96eZK5Q0XjlvPftOgWypQpd1gcbfJKAt7rsB8=
github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs= github.com/jhillyerd/enmime/v2 v2.2.0/go.mod h1:SOBXlCemjhiV2DvHhAKnJiWrtJGS/Ffuw4Iy7NjBTaI=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@ -108,7 +109,6 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 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-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A= github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
@ -122,10 +122,14 @@ github.com/mileusna/crontab v1.2.0/go.mod h1:dbns64w/u3tUnGZGf8pAa76ZqOfeBX4olW4
github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4= github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls= github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/errors v1.1.0 h1:RNuGIh15QdDenh+hNvKrJkmxxjV4hcS50Db478Ou5sM=
github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/olekukonko/errors v1.1.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y=
github.com/petermattis/goid v0.0.0-20250319124200-ccd6737f222a h1:S+AGcmAESQ0pXCUNnRH7V+bOUIgkSX5qVt2cNKCrm0Q= github.com/olekukonko/ll v0.0.9 h1:Y+1YqDfVkqMWuEQMclsF9HUR5+a82+dxJuL1HHSRpxI=
github.com/petermattis/goid v0.0.0-20250319124200-ccd6737f222a/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/olekukonko/ll v0.0.9/go.mod h1:En+sEW0JNETl26+K8eZ6/W4UQ7CYSrrgg/EdIYT2H8g=
github.com/olekukonko/tablewriter v1.0.7 h1:HCC2e3MM+2g72M81ZcJU11uciw6z/p82aEnm4/ySDGw=
github.com/olekukonko/tablewriter v1.0.7/go.mod h1:H428M+HzoUXC6JU2Abj9IT9ooRmdq9CxuDmKMtrOCMs=
github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb h1:3PrKuO92dUTMrQ9dx0YNejC6U/Si6jqKmyQ9vWjwqR4=
github.com/petermattis/goid v0.0.0-20250508124226-395b08cebbdb/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4= github.com/pingcap/errors v0.11.4 h1:lFuQV/oaUMGcD2tqt+01ROSmJs75VG1ToEOkZIZ4nE4=
github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8= github.com/pingcap/errors v0.11.4/go.mod h1:Oi8TUi2kEtXXLMJk9l1cGmz20kV3TaQ0usTwv5KuLY8=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@ -163,41 +167,41 @@ github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/yuin/goldmark v1.7.10 h1:S+LrtBjRmqMac2UdtB6yyCEJm+UILZ2fefI4p7o0QpI= github.com/yuin/goldmark v1.7.12 h1:YwGP/rrea2/CnCtUHgjuolG/PnMxdQtPMO5PvaE2/nY=
github.com/yuin/goldmark v1.7.10/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.7.12/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/util v0.8.6 h1:AEK13rfgtiZJL2YsNK+W4ihhYCuukcRom8WPP/w/L54= go.mau.fi/util v0.8.8 h1:OnuEEc/sIJFhnq4kFggiImUpcmnmL/xpvQMRu5Fiy5c=
go.mau.fi/util v0.8.6/go.mod h1:uNB3UTXFbkpp7xL1M/WvQks90B/L4gvbLpbS0603KOE= go.mau.fi/util v0.8.8/go.mod h1:Y/kS3loxTEhy8Vill513EtPXr+CRDdae+Xj2BXXMy/c=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM=
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b h1:M2rDM6z3Fhozi9O7NWsxAkg/yqS/lQJ6PmkyIV3YP+o=
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8= golang.org/x/exp v0.0.0-20250620022241-b7579e27df2b/go.mod h1:3//PLf8L/X+8b4vuAfHzxeRUl04Adcb341+IGKfnqS8=
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU= golang.org/x/mod v0.25.0 h1:n7a+ZbQKQA/Ysbyb0/6IbB1H/X41mKgbhfv7AfG/44w=
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww= golang.org/x/mod v0.25.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180911220305-26e67e76b6c3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM= golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw=
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA=
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8=
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M=
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.32.0 h1:Q7N1vhpkQv7ybVzLFtTjvQya2ewbwNDZzUgfXGqtMWU= golang.org/x/tools v0.34.0 h1:qIpSLOxeCYGg9TrcJokLBG4KFA6d795g0xkBkiESGlo=
golang.org/x/tools v0.32.0/go.mod h1:ZxrU41P/wAbZD8EDa6dDCa6XfpkhJ7HFMjHJXfBDu8s= golang.org/x/tools v0.34.0/go.mod h1:pAP9OwEaY1CAW3HOmg3hLZC5Z0CCmzjAF2UQMSqNARg=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -210,28 +214,30 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mautrix v0.23.3 h1:U+fzdcLhFKLUm5gf2+Q0hEUqWkwDMRfvE+paUH9ogSk= maunium.net/go/mautrix v0.24.1 h1:09/xi4qTeA03g1n/DPmmqAlT8Cx4QrgwiPlmLVzA9AU=
maunium.net/go/mautrix v0.23.3/go.mod h1:LX+3evXVKSvh/b43BVC3rkvN2qV7b0bkIV4fY7Snn/4= maunium.net/go/mautrix v0.24.1/go.mod h1:Xy6o+pXmbqmgWsUWh15EQ1eozjC+k/VT/7kloByv9PI=
modernc.org/cc/v4 v4.26.0 h1:QMYvbVduUGH0rrO+5mqF/PSPPRZNpRtg2CLELy7vUpA= modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
modernc.org/cc/v4 v4.26.0/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0= modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
modernc.org/ccgo/v4 v4.26.0 h1:gVzXaDzGeBYJ2uXTOpR8FR7OlksDOe9jxnjhIKCsiTc= modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
modernc.org/ccgo/v4 v4.26.0/go.mod h1:Sem8f7TFUtVXkG2fiaChQtyyfkqhJBg/zjEJBkmuAVY= modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.3 h1:3qaU+7f7xxTUmvU1pJTZiDLAIoJVdUSSauJNHg9yXoA=
modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/fileutil v1.3.3/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI= modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito= modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
modernc.org/libc v1.63.0 h1:wKzb61wOGCzgahQBORb1b0dZonh8Ufzl/7r4Yf1D5YA= modernc.org/goabi0 v0.0.3 h1:y81b9r3asCh6Xtse6Nz85aYGB0cG3M3U6222yap1KWI=
modernc.org/libc v1.63.0/go.mod h1:wDzH1mgz1wUIEwottFt++POjGRO9sgyQKrpXaz3x89E= modernc.org/goabi0 v0.0.3/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
modernc.org/libc v1.66.0 h1:eoFuDb1ozurUY5WSWlgvxHp0FuL+AncMwNjFqGYMJPQ=
modernc.org/libc v1.66.0/go.mod h1:AiZxInURfEJx516LqEaFcrC+X38rt9G7+8ojIXQKHbo=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU= modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg= modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.10.0 h1:fzumd51yQ1DxcOxSO+S6X7+QTuVU+n8/Aj7swYjFfC4= modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
modernc.org/memory v1.10.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw= modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8= modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns= modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w= modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE= modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
modernc.org/sqlite v1.37.0 h1:s1TMe7T3Q3ovQiK2Ouz4Jwh7dw4ZDqbebSDTlSJdfjI= modernc.org/sqlite v1.38.0 h1:+4OrfPQ8pxHKuWG4md1JpR/EYAh3Md7TdejuuzE7EUI=
modernc.org/sqlite v1.37.0/go.mod h1:5YiWv+YviqGMuGw4V+PNplcyaJ5v+vQd7TQOgkACoJM= modernc.org/sqlite v1.38.0/go.mod h1:1Bj+yES4SVvBZ4cBOpVZ6QgesMCKpJZDq0nxYzOpmNE=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0= modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A= modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=

View file

@ -8,7 +8,7 @@ import (
"github.com/emersion/go-msgauth/dkim" "github.com/emersion/go-msgauth/dkim"
"github.com/etkecc/go-linkpearl" "github.com/etkecc/go-linkpearl"
"github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime/v2"
"github.com/kvannotten/mailstrip" "github.com/kvannotten/mailstrip"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"

View file

@ -15,7 +15,7 @@ import (
"github.com/etkecc/go-validator/v2" "github.com/etkecc/go-validator/v2"
"github.com/etkecc/postmoogle/internal/email" "github.com/etkecc/postmoogle/internal/email"
"github.com/etkecc/postmoogle/internal/utils" "github.com/etkecc/postmoogle/internal/utils"
"github.com/jhillyerd/enmime" "github.com/jhillyerd/enmime/v2"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
) )

View file

@ -1,7 +1,6 @@
# go-smtp # go-smtp
[![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-smtp.svg)](https://pkg.go.dev/github.com/emersion/go-smtp) [![Go Reference](https://pkg.go.dev/badge/github.com/emersion/go-smtp.svg)](https://pkg.go.dev/github.com/emersion/go-smtp)
[![builds.sr.ht status](https://builds.sr.ht/~emersion/go-smtp/commits.svg)](https://builds.sr.ht/~emersion/go-smtp/commits?)
An ESMTP client and server library written in Go. An ESMTP client and server library written in Go.

View file

@ -12,6 +12,7 @@ import (
"io" "io"
"net" "net"
"net/textproto" "net/textproto"
"sort"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@ -81,7 +82,7 @@ func DialTLS(addr string, tlsConfig *tls.Config) (*Client, error) {
return client, nil return client, nil
} }
// DialStartTLS retruns a new Client connected to an SMTP server via STARTTLS // DialStartTLS returns a new Client connected to an SMTP server via STARTTLS
// at addr. The addr must include a port, as in "mail.example.com:smtp". // at addr. The addr must include a port, as in "mail.example.com:smtp".
// //
// A nil tlsConfig is equivalent to a zero tls.Config. // A nil tlsConfig is equivalent to a zero tls.Config.
@ -530,6 +531,9 @@ func (c *Client) Rcpt(to string, opts *RcptOptions) error {
fmt.Fprintf(&sb, " ORCPT=%s;%s", string(opts.OriginalRecipientType), enc) fmt.Fprintf(&sb, " ORCPT=%s;%s", string(opts.OriginalRecipientType), enc)
} }
} }
if _, ok := c.ext["RRVS"]; ok && opts != nil && !opts.RequireRecipientValidSince.IsZero() {
sb.WriteString(fmt.Sprintf(" RRVS=%s", opts.RequireRecipientValidSince.Format(time.RFC3339)))
}
if _, _, err := c.cmd(25, "%s", sb.String()); err != nil { if _, _, err := c.cmd(25, "%s", sb.String()); err != nil {
return err return err
} }
@ -537,85 +541,152 @@ func (c *Client) Rcpt(to string, opts *RcptOptions) error {
return nil return nil
} }
type dataCloser struct { // DataCommand is a pending DATA command. DataCommand is an io.WriteCloser.
c *Client // See Client.Data.
io.WriteCloser type DataCommand struct {
statusCb func(rcpt string, status *SMTPError) client *Client
closed bool wc io.WriteCloser
closeErr error
} }
func (d *dataCloser) Close() error { var _ io.WriteCloser = (*DataCommand)(nil)
if d.closed {
return fmt.Errorf("smtp: data writer closed twice") // Write implements io.Writer.
func (cmd *DataCommand) Write(b []byte) (int, error) {
return cmd.wc.Write(b)
} }
if err := d.WriteCloser.Close(); err != nil { // Close implements io.Closer.
func (cmd *DataCommand) Close() error {
var err error
if cmd.client.lmtp {
_, err = cmd.CloseWithLMTPResponse()
} else {
_, err = cmd.CloseWithResponse()
}
return err return err
} }
d.c.conn.SetDeadline(time.Now().Add(d.c.SubmissionTimeout)) // CloseWithResponse is equivalent to Close, but also returns the server
defer d.c.conn.SetDeadline(time.Time{}) // response. It cannot be called when the LMTP protocol is used.
//
// If server returns an error, it will be of type *SMTPError.
func (cmd *DataCommand) CloseWithResponse() (*DataResponse, error) {
if cmd.client.lmtp {
return nil, errors.New("smtp: CloseWithResponse used with an LMTP client")
}
expectedResponses := len(d.c.rcpts) if err := cmd.close(); err != nil {
if d.c.lmtp { return nil, err
for expectedResponses > 0 {
rcpt := d.c.rcpts[len(d.c.rcpts)-expectedResponses]
if _, _, err := d.c.readResponse(250); err != nil {
if smtpErr, ok := err.(*SMTPError); ok {
if d.statusCb != nil {
d.statusCb(rcpt, smtpErr)
} }
} else {
return err cmd.client.conn.SetDeadline(time.Now().Add(cmd.client.SubmissionTimeout))
} defer cmd.client.conn.SetDeadline(time.Time{})
} else if d.statusCb != nil {
d.statusCb(rcpt, nil) _, msg, err := cmd.client.readResponse(250)
}
expectedResponses--
}
} else {
_, _, err := d.c.readResponse(250)
if err != nil { if err != nil {
return err cmd.closeErr = err
return nil, err
}
return &DataResponse{StatusText: msg}, nil
}
// CloseWithLMTPResponse is equivalent to Close, but also returns per-recipient
// server responses. It can only be called when the LMTP protocol is used.
//
// If server returns an error, it will be of type LMTPDataError.
func (cmd *DataCommand) CloseWithLMTPResponse() (map[string]*DataResponse, error) {
if !cmd.client.lmtp {
return nil, errors.New("smtp: CloseWithLMTPResponse used without an LMTP client")
}
if err := cmd.close(); err != nil {
return nil, err
}
cmd.client.conn.SetDeadline(time.Now().Add(cmd.client.SubmissionTimeout))
defer cmd.client.conn.SetDeadline(time.Time{})
resp := make(map[string]*DataResponse, len(cmd.client.rcpts))
lmtpErr := make(LMTPDataError, len(cmd.client.rcpts))
for i := 0; i < len(cmd.client.rcpts); i++ {
rcpt := cmd.client.rcpts[i]
_, msg, err := cmd.client.readResponse(250)
if err != nil {
if smtpErr, ok := err.(*SMTPError); ok {
lmtpErr[rcpt] = smtpErr
} else {
if len(lmtpErr) > 0 {
return resp, errors.Join(err, lmtpErr)
}
return resp, err
}
} else {
resp[rcpt] = &DataResponse{StatusText: msg}
} }
} }
d.closed = true if len(lmtpErr) > 0 {
return resp, lmtpErr
}
return resp, nil
}
func (cmd *DataCommand) close() error {
if cmd.closeErr != nil {
return cmd.closeErr
}
if err := cmd.wc.Close(); err != nil {
cmd.closeErr = err
return err
}
cmd.closeErr = errors.New("smtp: data writer closed twice")
return nil return nil
} }
// DataResponse is the response returned by a DATA command. See
// DataCommand.CloseWithResponse.
type DataResponse struct {
// StatusText is the status text returned by the server. It may contain
// tracking information.
StatusText string
}
// LMTPDataError is a collection of errors returned by an LMTP server for a
// DATA command. It holds per-recipient errors.
type LMTPDataError map[string]*SMTPError
// Error implements error.
func (lmtpErr LMTPDataError) Error() string {
return errors.Join(lmtpErr.Unwrap()...).Error()
}
// Unwrap returns all per-recipient errors returned by the server.
func (lmtpErr LMTPDataError) Unwrap() []error {
l := make([]error, 0, len(lmtpErr))
for rcpt, smtpErr := range lmtpErr {
l = append(l, fmt.Errorf("<%v>: %w", rcpt, smtpErr))
}
sort.Slice(l, func(i, j int) bool {
return l[i].Error() < l[j].Error()
})
return l
}
// Data issues a DATA command to the server and returns a writer that // Data issues a DATA command to the server and returns a writer that
// can be used to write the mail headers and body. The caller should // can be used to write the mail headers and body. The caller should
// close the writer before calling any more methods on c. A call to // close the writer before calling any more methods on c. A call to
// Data must be preceded by one or more calls to Rcpt. // Data must be preceded by one or more calls to Rcpt.
// func (c *Client) Data() (*DataCommand, error) {
// If server returns an error, it will be of type *SMTPError.
func (c *Client) Data() (io.WriteCloser, error) {
_, _, err := c.cmd(354, "DATA") _, _, err := c.cmd(354, "DATA")
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &dataCloser{c: c, WriteCloser: c.text.DotWriter()}, nil return &DataCommand{client: c, wc: c.text.DotWriter()}, nil
}
// LMTPData is the LMTP-specific version of the Data method. It accepts a callback
// that will be called for each status response received from the server.
//
// Status callback will receive a SMTPError argument for each negative server
// reply and nil for each positive reply. I/O errors will not be reported using
// callback and instead will be returned by the Close method of io.WriteCloser.
// Callback will be called for each successfull Rcpt call done before in the
// same order.
func (c *Client) LMTPData(statusCb func(rcpt string, status *SMTPError)) (io.WriteCloser, error) {
if !c.lmtp {
return nil, errors.New("smtp: not a LMTP client")
}
_, _, err := c.cmd(354, "DATA")
if err != nil {
return nil, err
}
return &dataCloser{c: c, WriteCloser: c.text.DotWriter(), statusCb: statusCb}, nil
} }
// SendMail will use an existing connection to send an email from // SendMail will use an existing connection to send an email from
@ -784,6 +855,11 @@ func (c *Client) Reset() error {
if _, _, err := c.cmd(250, "RSET"); err != nil { if _, _, err := c.cmd(250, "RSET"); err != nil {
return err return err
} }
// allow custom HELLO again
c.didHello = false
c.helloError = nil
c.rcpts = nil c.rcpts = nil
return nil return nil
} }

View file

@ -229,6 +229,12 @@ func (c *Conn) handleGreet(enhanced bool, arg string) {
// NewSession can access it via Conn.Hostname. // NewSession can access it via Conn.Hostname.
c.helo = domain c.helo = domain
// RFC 5321: "An EHLO command MAY be issued by a client later in the session"
if c.session != nil {
// RFC 5321: "... the SMTP server MUST clear all buffers
// and reset the state exactly as if a RSET command has been issued."
c.reset()
} else {
sess, err := c.server.Backend.NewSession(c) sess, err := c.server.Backend.NewSession(c)
if err != nil { if err != nil {
c.helo = "" c.helo = ""
@ -237,6 +243,7 @@ func (c *Conn) handleGreet(enhanced bool, arg string) {
} }
c.setSession(sess) c.setSession(sess)
}
if !enhanced { if !enhanced {
c.writeResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("Hello %s", domain)) c.writeResponse(250, EnhancedCode{2, 0, 0}, fmt.Sprintf("Hello %s", domain))
@ -284,6 +291,9 @@ func (c *Conn) handleGreet(enhanced bool, arg string) {
if c.server.MaxRecipients > 0 { if c.server.MaxRecipients > 0 {
caps = append(caps, fmt.Sprintf("LIMITS RCPTMAX=%v", c.server.MaxRecipients)) caps = append(caps, fmt.Sprintf("LIMITS RCPTMAX=%v", c.server.MaxRecipients))
} }
if c.server.EnableRRVS {
caps = append(caps, "RRVS")
}
args := []string{"Hello " + domain} args := []string{"Hello " + domain}
args = append(args, caps...) args = append(args, caps...)
@ -709,6 +719,18 @@ func (c *Conn) handleRcpt(arg string) {
} }
opts.OriginalRecipientType = aType opts.OriginalRecipientType = aType
opts.OriginalRecipient = aAddr opts.OriginalRecipient = aAddr
case "RRVS":
if !c.server.EnableRRVS {
c.writeResponse(504, EnhancedCode{5, 5, 4}, "RRVS is not implemented")
return
}
value, _, _ = strings.Cut(value, ";") // discard the no-support action
rrvsTime, err := time.Parse(time.RFC3339, value)
if err != nil {
c.writeResponse(501, EnhancedCode{5, 5, 4}, "Malformed RRVS parameter value")
return
}
opts.RequireRecipientValidSince = rrvsTime
default: default:
c.writeResponse(500, EnhancedCode{5, 5, 4}, "Unknown RCPT TO argument") c.writeResponse(500, EnhancedCode{5, 5, 4}, "Unknown RCPT TO argument")
return return
@ -1229,13 +1251,17 @@ func (c *Conn) writeResponse(code int, enhCode EnhancedCode, text ...string) {
} }
} }
for i := 0; i < len(text)-1; i++ { // transform each single line with \n, into separate lines
text = strings.Split(strings.Join(text, "\n"), "\n")
lastLineIndex := len(text) - 1
for i := 0; i < lastLineIndex; i++ {
c.text.PrintfLine("%d-%v", code, text[i]) c.text.PrintfLine("%d-%v", code, text[i])
} }
if enhCode == NoEnhancedCode { if enhCode == NoEnhancedCode {
c.text.PrintfLine("%d %v", code, text[len(text)-1]) c.text.PrintfLine("%d %v", code, text[lastLineIndex])
} else { } else {
c.text.PrintfLine("%d %v.%v.%v %v", code, enhCode[0], enhCode[1], enhCode[2], text[len(text)-1]) c.text.PrintfLine("%d %v.%v.%v %v", code, enhCode[0], enhCode[1], enhCode[2], text[lastLineIndex])
} }
} }

View file

@ -57,6 +57,10 @@ type Server struct {
// Should be used only if backend supports it. // Should be used only if backend supports it.
EnableDSN bool EnableDSN bool
// Advertise RRVS (RFC 7293) capability.
// Should be used only if backend supports it.
EnableRRVS bool
// The server backend. // The server backend.
Backend Backend Backend Backend

View file

@ -17,6 +17,8 @@
// Additional extensions may be handled by other packages. // Additional extensions may be handled by other packages.
package smtp package smtp
import "time"
type BodyType string type BodyType string
const ( const (
@ -91,4 +93,8 @@ type RcptOptions struct {
// Original recipient set by client. // Original recipient set by client.
OriginalRecipientType DSNAddressType OriginalRecipientType DSNAddressType
OriginalRecipient string OriginalRecipient string
// Time value of the RRVS= argument
// Left as the zero time if unset.
RequireRecipientValidSince time.Time
} }

View file

@ -1,19 +1,52 @@
version: "2"
run: run:
concurrency: 4 concurrency: 4
timeout: 30m modules-download-mode: readonly
issues-exit-code: 1 issues-exit-code: 1
tests: true tests: true
build-tags: []
modules-download-mode: readonly
output: output:
formats: formats:
- format: colored-line-number text:
print-issued-lines: true path: stdout
print-linter-name: true print-linter-name: true
sort-results: true print-issued-lines: true
linters:
linters-settings: enable:
- asasalint
- asciicheck
- bidichk
- bodyclose
- containedctx
- copyloopvar
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- errchkjson
- errname
- errorlint
- exhaustive
- forcetypeassert
- gocognit
- gocritic
- gocyclo
- gosec
- gosmopolitan
- makezero
- mirror
- misspell
- nestif
- nolintlint
- prealloc
- predeclared
- revive
- sqlclosecheck
- unconvert
- unparam
- usestdlibvars
- wastedassign
settings:
decorder: decorder:
dec-order: dec-order:
- const - const
@ -34,28 +67,21 @@ linters-settings:
default-signifies-exhaustive: true default-signifies-exhaustive: true
gocognit: gocognit:
min-complexity: 15 min-complexity: 15
nestif:
min-complexity: 5
gocritic: gocritic:
enabled-tags: enabled-tags:
- diagnostic - diagnostic
- style - style
- performance - performance
gofmt:
simplify: true
rewrite-rules:
- pattern: 'interface{}'
replacement: 'any'
- pattern: 'a[b:len(a)]'
replacement: 'a[b:]'
gofumpt:
extra-rules: true
grouper: grouper:
const-require-single-const: true const-require-single-const: true
import-require-single-import: true import-require-single-import: true
var-require-single-var: true var-require-single-var: true
misspell: misspell:
locale: US locale: US
nestif:
min-complexity: 5
unparam:
check-exported: true
usestdlibvars: usestdlibvars:
time-month: true time-month: true
time-layout: true time-layout: true
@ -64,78 +90,58 @@ linters-settings:
sql-isolation-level: true sql-isolation-level: true
tls-signature-scheme: true tls-signature-scheme: true
constant-kind: true constant-kind: true
unparam: exclusions:
check-exported: true generated: lax
linters: presets:
disable-all: false - comments
enable: - common-false-positives
- asasalint - legacy
- asciicheck - std-error-handling
- bidichk rules:
- bodyclose - linters:
- containedctx
- copyloopvar
- decorder
- dogsled
- dupl - dupl
- dupword
- durationcheck
- errcheck - errcheck
- errchkjson
- errname
- errorlint
- exhaustive
- forcetypeassert
- gocognit - gocognit
- gocritic
- gocyclo - gocyclo
- gofmt
- gofumpt
- goimports
- gosec
- gosimple
- gosmopolitan
- govet
- ineffassign
- makezero
- mirror
- misspell
- nestif
- nolintlint
- prealloc
- predeclared
- revive
- sqlclosecheck
- staticcheck
- unconvert
- unparam
- unused
- usestdlibvars
- wastedassign
fast: false
issues:
exclude-dirs-use-default: true
exclude-dirs:
- mocks
exclude-rules:
- path: _test\.go
linters:
- gocyclo
- gocognit
- errcheck
- dupl
- gosec - gosec
path: _test\.go
- linters: - linters:
- staticcheck - staticcheck
text: "SA9003:" text: 'SA9003:'
- linters: - linters:
- lll - lll
source: "^//go:generate " source: '^//go:generate '
- linters: - linters:
- revive - revive
text: "returns unexported type" text: returns unexported type
paths:
- mocks
- third_party$
- builtin$
- examples$
issues:
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0
new: false new: false
formatters:
enable:
- gofmt
- gofumpt
- goimports
settings:
gofmt:
simplify: true
rewrite-rules:
- pattern: interface{}
replacement: any
- pattern: a[b:len(a)]
replacement: a[b:]
gofumpt:
extra-rules: true
exclusions:
generated: lax
paths:
- mocks
- third_party$
- builtin$
- examples$

34
vendor/github.com/etkecc/go-kit/ip.go generated vendored Normal file
View file

@ -0,0 +1,34 @@
package kit
import (
"net"
"strings"
)
// AnonymizeIP drops the last octet of the IPv4 and IPv6 address to anonymize it
func AnonymizeIP(ip string) string {
if ip == "" {
return ""
}
parsedIP := net.ParseIP(ip)
if parsedIP == nil {
return ip // not an ip
}
// IPv4
if parsedIP.To4() != nil {
ipParts := strings.Split(parsedIP.String(), ".")
if len(ipParts) == 4 {
ipParts[3] = "0"
return strings.Join(ipParts, ".")
}
}
// IPv6
ipParts := strings.Split(parsedIP.String(), ":")
if len(ipParts) > 0 {
ipParts[len(ipParts)-1] = "0"
return strings.Join(ipParts, ":")
}
return ip // not an ip
}

View file

@ -63,6 +63,11 @@ func StringToInt(value string, optionalDefaultValue ...int) int {
defaultValue = optionalDefaultValue[0] defaultValue = optionalDefaultValue[0]
} }
value = strings.TrimSpace(value)
if value == "" {
return defaultValue
}
vInt, err := strconv.Atoi(value) vInt, err := strconv.Atoi(value)
if err != nil { if err != nil {
return defaultValue return defaultValue
@ -80,7 +85,7 @@ func StringToSlice(value string, optionalDefaultValue ...string) []string {
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
if idx := strings.Index(value, ","); idx == -1 { if idx := strings.Index(value, ","); idx == -1 {
value = defaultValue return []string{defaultValue}
} }
parts := strings.Split(value, ",") parts := strings.Split(value, ",")
@ -93,6 +98,10 @@ func StringToSlice(value string, optionalDefaultValue ...string) []string {
// SliceToString converts slice of strings into single string (using strings.Join) with optional hook // SliceToString converts slice of strings into single string (using strings.Join) with optional hook
func SliceToString(slice []string, delimiter string, hook func(string) string) string { func SliceToString(slice []string, delimiter string, hook func(string) string) string {
if len(slice) == 0 {
return ""
}
adjusted := make([]string, 0, len(slice)) adjusted := make([]string, 0, len(slice))
for _, item := range slice { for _, item := range slice {
if hook != nil { if hook != nil {

View file

@ -159,7 +159,7 @@ func (l *Linkpearl) Start(ctx context.Context, optionalStatusMsg ...string) erro
err := l.api.SetPresence(ctx, mautrix.ReqPresence{Presence: event.PresenceOnline, StatusMsg: statusMsg}) err := l.api.SetPresence(ctx, mautrix.ReqPresence{Presence: event.PresenceOnline, StatusMsg: statusMsg})
if err != nil { if err != nil {
l.log.Error().Err(err).Msg("cannot set presence") l.log.Warn().Err(err).Msg("cannot set presence on start")
} }
defer l.Stop(ctx) defer l.Stop(ctx)
@ -171,7 +171,7 @@ func (l *Linkpearl) Start(ctx context.Context, optionalStatusMsg ...string) erro
func (l *Linkpearl) Stop(ctx context.Context) { func (l *Linkpearl) Stop(ctx context.Context) {
l.log.Debug().Msg("stopping the client") l.log.Debug().Msg("stopping the client")
if err := l.api.SetPresence(ctx, mautrix.ReqPresence{Presence: event.PresenceOffline}); err != nil { if err := l.api.SetPresence(ctx, mautrix.ReqPresence{Presence: event.PresenceOffline}); err != nil {
l.log.Error().Err(err).Msg("cannot set presence") l.log.Warn().Err(err).Msg("cannot set presence")
} }
l.api.StopSync() l.api.StopSync()
if err := l.ch.Close(); err != nil { if err := l.ch.Close(); err != nil {

20
vendor/github.com/fatih/color/LICENSE.md generated vendored Normal file
View file

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2013 Fatih Arslan
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

189
vendor/github.com/fatih/color/README.md generated vendored Normal file
View file

@ -0,0 +1,189 @@
# color [![](https://github.com/fatih/color/workflows/build/badge.svg)](https://github.com/fatih/color/actions) [![PkgGoDev](https://pkg.go.dev/badge/github.com/fatih/color)](https://pkg.go.dev/github.com/fatih/color)
Color lets you use colorized outputs in terms of [ANSI Escape
Codes](http://en.wikipedia.org/wiki/ANSI_escape_code#Colors) in Go (Golang). It
has support for Windows too! The API can be used in several ways, pick one that
suits you.
![Color](https://user-images.githubusercontent.com/438920/96832689-03b3e000-13f4-11eb-9803-46f4c4de3406.jpg)
## Install
```
go get github.com/fatih/color
```
## Examples
### Standard colors
```go
// Print with default helper functions
color.Cyan("Prints text in cyan.")
// A newline will be appended automatically
color.Blue("Prints %s in blue.", "text")
// These are using the default foreground colors
color.Red("We have red")
color.Magenta("And many others ..")
```
### RGB colors
If your terminal supports 24-bit colors, you can use RGB color codes.
```go
color.RGB(255, 128, 0).Println("foreground orange")
color.RGB(230, 42, 42).Println("foreground red")
color.BgRGB(255, 128, 0).Println("background orange")
color.BgRGB(230, 42, 42).Println("background red")
```
### Mix and reuse colors
```go
// Create a new color object
c := color.New(color.FgCyan).Add(color.Underline)
c.Println("Prints cyan text with an underline.")
// Or just add them to New()
d := color.New(color.FgCyan, color.Bold)
d.Printf("This prints bold cyan %s\n", "too!.")
// Mix up foreground and background colors, create new mixes!
red := color.New(color.FgRed)
boldRed := red.Add(color.Bold)
boldRed.Println("This will print text in bold red.")
whiteBackground := red.Add(color.BgWhite)
whiteBackground.Println("Red text with white background.")
// Mix with RGB color codes
color.RGB(255, 128, 0).AddBgRGB(0, 0, 0).Println("orange with black background")
color.BgRGB(255, 128, 0).AddRGB(255, 255, 255).Println("orange background with white foreground")
```
### Use your own output (io.Writer)
```go
// Use your own io.Writer output
color.New(color.FgBlue).Fprintln(myWriter, "blue color!")
blue := color.New(color.FgBlue)
blue.Fprint(writer, "This will print text in blue.")
```
### Custom print functions (PrintFunc)
```go
// Create a custom print function for convenience
red := color.New(color.FgRed).PrintfFunc()
red("Warning")
red("Error: %s", err)
// Mix up multiple attributes
notice := color.New(color.Bold, color.FgGreen).PrintlnFunc()
notice("Don't forget this...")
```
### Custom fprint functions (FprintFunc)
```go
blue := color.New(color.FgBlue).FprintfFunc()
blue(myWriter, "important notice: %s", stars)
// Mix up with multiple attributes
success := color.New(color.Bold, color.FgGreen).FprintlnFunc()
success(myWriter, "Don't forget this...")
```
### Insert into noncolor strings (SprintFunc)
```go
// Create SprintXxx functions to mix strings with other non-colorized strings:
yellow := color.New(color.FgYellow).SprintFunc()
red := color.New(color.FgRed).SprintFunc()
fmt.Printf("This is a %s and this is %s.\n", yellow("warning"), red("error"))
info := color.New(color.FgWhite, color.BgGreen).SprintFunc()
fmt.Printf("This %s rocks!\n", info("package"))
// Use helper functions
fmt.Println("This", color.RedString("warning"), "should be not neglected.")
fmt.Printf("%v %v\n", color.GreenString("Info:"), "an important message.")
// Windows supported too! Just don't forget to change the output to color.Output
fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS"))
```
### Plug into existing code
```go
// Use handy standard colors
color.Set(color.FgYellow)
fmt.Println("Existing text will now be in yellow")
fmt.Printf("This one %s\n", "too")
color.Unset() // Don't forget to unset
// You can mix up parameters
color.Set(color.FgMagenta, color.Bold)
defer color.Unset() // Use it in your function
fmt.Println("All text will now be bold magenta.")
```
### Disable/Enable color
There might be a case where you want to explicitly disable/enable color output. the
`go-isatty` package will automatically disable color output for non-tty output streams
(for example if the output were piped directly to `less`).
The `color` package also disables color output if the [`NO_COLOR`](https://no-color.org) environment
variable is set to a non-empty string.
`Color` has support to disable/enable colors programmatically both globally and
for single color definitions. For example suppose you have a CLI app and a
`-no-color` bool flag. You can easily disable the color output with:
```go
var flagNoColor = flag.Bool("no-color", false, "Disable color output")
if *flagNoColor {
color.NoColor = true // disables colorized output
}
```
It also has support for single color definitions (local). You can
disable/enable color output on the fly:
```go
c := color.New(color.FgCyan)
c.Println("Prints cyan text")
c.DisableColor()
c.Println("This is printed without any color")
c.EnableColor()
c.Println("This prints again cyan...")
```
## GitHub Actions
To output color in GitHub Actions (or other CI systems that support ANSI colors), make sure to set `color.NoColor = false` so that it bypasses the check for non-tty output streams.
## Credits
* [Fatih Arslan](https://github.com/fatih)
* Windows support via @mattn: [colorable](https://github.com/mattn/go-colorable)
## License
The MIT License (MIT) - see [`LICENSE.md`](https://github.com/fatih/color/blob/master/LICENSE.md) for more details

685
vendor/github.com/fatih/color/color.go generated vendored Normal file
View file

@ -0,0 +1,685 @@
package color
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"sync"
"github.com/mattn/go-colorable"
"github.com/mattn/go-isatty"
)
var (
// NoColor defines if the output is colorized or not. It's dynamically set to
// false or true based on the stdout's file descriptor referring to a terminal
// or not. It's also set to true if the NO_COLOR environment variable is
// set (regardless of its value). This is a global option and affects all
// colors. For more control over each color block use the methods
// DisableColor() individually.
NoColor = noColorIsSet() || os.Getenv("TERM") == "dumb" ||
(!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()))
// Output defines the standard output of the print functions. By default,
// os.Stdout is used.
Output = colorable.NewColorableStdout()
// Error defines a color supporting writer for os.Stderr.
Error = colorable.NewColorableStderr()
// colorsCache is used to reduce the count of created Color objects and
// allows to reuse already created objects with required Attribute.
colorsCache = make(map[Attribute]*Color)
colorsCacheMu sync.Mutex // protects colorsCache
)
// noColorIsSet returns true if the environment variable NO_COLOR is set to a non-empty string.
func noColorIsSet() bool {
return os.Getenv("NO_COLOR") != ""
}
// Color defines a custom color object which is defined by SGR parameters.
type Color struct {
params []Attribute
noColor *bool
}
// Attribute defines a single SGR Code
type Attribute int
const escape = "\x1b"
// Base attributes
const (
Reset Attribute = iota
Bold
Faint
Italic
Underline
BlinkSlow
BlinkRapid
ReverseVideo
Concealed
CrossedOut
)
const (
ResetBold Attribute = iota + 22
ResetItalic
ResetUnderline
ResetBlinking
_
ResetReversed
ResetConcealed
ResetCrossedOut
)
var mapResetAttributes map[Attribute]Attribute = map[Attribute]Attribute{
Bold: ResetBold,
Faint: ResetBold,
Italic: ResetItalic,
Underline: ResetUnderline,
BlinkSlow: ResetBlinking,
BlinkRapid: ResetBlinking,
ReverseVideo: ResetReversed,
Concealed: ResetConcealed,
CrossedOut: ResetCrossedOut,
}
// Foreground text colors
const (
FgBlack Attribute = iota + 30
FgRed
FgGreen
FgYellow
FgBlue
FgMagenta
FgCyan
FgWhite
// used internally for 256 and 24-bit coloring
foreground
)
// Foreground Hi-Intensity text colors
const (
FgHiBlack Attribute = iota + 90
FgHiRed
FgHiGreen
FgHiYellow
FgHiBlue
FgHiMagenta
FgHiCyan
FgHiWhite
)
// Background text colors
const (
BgBlack Attribute = iota + 40
BgRed
BgGreen
BgYellow
BgBlue
BgMagenta
BgCyan
BgWhite
// used internally for 256 and 24-bit coloring
background
)
// Background Hi-Intensity text colors
const (
BgHiBlack Attribute = iota + 100
BgHiRed
BgHiGreen
BgHiYellow
BgHiBlue
BgHiMagenta
BgHiCyan
BgHiWhite
)
// New returns a newly created color object.
func New(value ...Attribute) *Color {
c := &Color{
params: make([]Attribute, 0),
}
if noColorIsSet() {
c.noColor = boolPtr(true)
}
c.Add(value...)
return c
}
// RGB returns a new foreground color in 24-bit RGB.
func RGB(r, g, b int) *Color {
return New(foreground, 2, Attribute(r), Attribute(g), Attribute(b))
}
// BgRGB returns a new background color in 24-bit RGB.
func BgRGB(r, g, b int) *Color {
return New(background, 2, Attribute(r), Attribute(g), Attribute(b))
}
// AddRGB is used to chain foreground RGB SGR parameters. Use as many as parameters to combine
// and create custom color objects. Example: .Add(34, 0, 12).Add(255, 128, 0).
func (c *Color) AddRGB(r, g, b int) *Color {
c.params = append(c.params, foreground, 2, Attribute(r), Attribute(g), Attribute(b))
return c
}
// AddRGB is used to chain background RGB SGR parameters. Use as many as parameters to combine
// and create custom color objects. Example: .Add(34, 0, 12).Add(255, 128, 0).
func (c *Color) AddBgRGB(r, g, b int) *Color {
c.params = append(c.params, background, 2, Attribute(r), Attribute(g), Attribute(b))
return c
}
// Set sets the given parameters immediately. It will change the color of
// output with the given SGR parameters until color.Unset() is called.
func Set(p ...Attribute) *Color {
c := New(p...)
c.Set()
return c
}
// Unset resets all escape attributes and clears the output. Usually should
// be called after Set().
func Unset() {
if NoColor {
return
}
fmt.Fprintf(Output, "%s[%dm", escape, Reset)
}
// Set sets the SGR sequence.
func (c *Color) Set() *Color {
if c.isNoColorSet() {
return c
}
fmt.Fprint(Output, c.format())
return c
}
func (c *Color) unset() {
if c.isNoColorSet() {
return
}
Unset()
}
// SetWriter is used to set the SGR sequence with the given io.Writer. This is
// a low-level function, and users should use the higher-level functions, such
// as color.Fprint, color.Print, etc.
func (c *Color) SetWriter(w io.Writer) *Color {
if c.isNoColorSet() {
return c
}
fmt.Fprint(w, c.format())
return c
}
// UnsetWriter resets all escape attributes and clears the output with the give
// io.Writer. Usually should be called after SetWriter().
func (c *Color) UnsetWriter(w io.Writer) {
if c.isNoColorSet() {
return
}
if NoColor {
return
}
fmt.Fprintf(w, "%s[%dm", escape, Reset)
}
// Add is used to chain SGR parameters. Use as many as parameters to combine
// and create custom color objects. Example: Add(color.FgRed, color.Underline).
func (c *Color) Add(value ...Attribute) *Color {
c.params = append(c.params, value...)
return c
}
// Fprint formats using the default formats for its operands and writes to w.
// Spaces are added between operands when neither is a string.
// It returns the number of bytes written and any write error encountered.
// On Windows, users should wrap w with colorable.NewColorable() if w is of
// type *os.File.
func (c *Color) Fprint(w io.Writer, a ...interface{}) (n int, err error) {
c.SetWriter(w)
defer c.UnsetWriter(w)
return fmt.Fprint(w, a...)
}
// Print formats using the default formats for its operands and writes to
// standard output. Spaces are added between operands when neither is a
// string. It returns the number of bytes written and any write error
// encountered. This is the standard fmt.Print() method wrapped with the given
// color.
func (c *Color) Print(a ...interface{}) (n int, err error) {
c.Set()
defer c.unset()
return fmt.Fprint(Output, a...)
}
// Fprintf formats according to a format specifier and writes to w.
// It returns the number of bytes written and any write error encountered.
// On Windows, users should wrap w with colorable.NewColorable() if w is of
// type *os.File.
func (c *Color) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
c.SetWriter(w)
defer c.UnsetWriter(w)
return fmt.Fprintf(w, format, a...)
}
// Printf formats according to a format specifier and writes to standard output.
// It returns the number of bytes written and any write error encountered.
// This is the standard fmt.Printf() method wrapped with the given color.
func (c *Color) Printf(format string, a ...interface{}) (n int, err error) {
c.Set()
defer c.unset()
return fmt.Fprintf(Output, format, a...)
}
// Fprintln formats using the default formats for its operands and writes to w.
// Spaces are always added between operands and a newline is appended.
// On Windows, users should wrap w with colorable.NewColorable() if w is of
// type *os.File.
func (c *Color) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
return fmt.Fprintln(w, c.wrap(sprintln(a...)))
}
// Println formats using the default formats for its operands and writes to
// standard output. Spaces are always added between operands and a newline is
// appended. It returns the number of bytes written and any write error
// encountered. This is the standard fmt.Print() method wrapped with the given
// color.
func (c *Color) Println(a ...interface{}) (n int, err error) {
return fmt.Fprintln(Output, c.wrap(sprintln(a...)))
}
// Sprint is just like Print, but returns a string instead of printing it.
func (c *Color) Sprint(a ...interface{}) string {
return c.wrap(fmt.Sprint(a...))
}
// Sprintln is just like Println, but returns a string instead of printing it.
func (c *Color) Sprintln(a ...interface{}) string {
return c.wrap(sprintln(a...)) + "\n"
}
// Sprintf is just like Printf, but returns a string instead of printing it.
func (c *Color) Sprintf(format string, a ...interface{}) string {
return c.wrap(fmt.Sprintf(format, a...))
}
// FprintFunc returns a new function that prints the passed arguments as
// colorized with color.Fprint().
func (c *Color) FprintFunc() func(w io.Writer, a ...interface{}) {
return func(w io.Writer, a ...interface{}) {
c.Fprint(w, a...)
}
}
// PrintFunc returns a new function that prints the passed arguments as
// colorized with color.Print().
func (c *Color) PrintFunc() func(a ...interface{}) {
return func(a ...interface{}) {
c.Print(a...)
}
}
// FprintfFunc returns a new function that prints the passed arguments as
// colorized with color.Fprintf().
func (c *Color) FprintfFunc() func(w io.Writer, format string, a ...interface{}) {
return func(w io.Writer, format string, a ...interface{}) {
c.Fprintf(w, format, a...)
}
}
// PrintfFunc returns a new function that prints the passed arguments as
// colorized with color.Printf().
func (c *Color) PrintfFunc() func(format string, a ...interface{}) {
return func(format string, a ...interface{}) {
c.Printf(format, a...)
}
}
// FprintlnFunc returns a new function that prints the passed arguments as
// colorized with color.Fprintln().
func (c *Color) FprintlnFunc() func(w io.Writer, a ...interface{}) {
return func(w io.Writer, a ...interface{}) {
c.Fprintln(w, a...)
}
}
// PrintlnFunc returns a new function that prints the passed arguments as
// colorized with color.Println().
func (c *Color) PrintlnFunc() func(a ...interface{}) {
return func(a ...interface{}) {
c.Println(a...)
}
}
// SprintFunc returns a new function that returns colorized strings for the
// given arguments with fmt.Sprint(). Useful to put into or mix into other
// string. Windows users should use this in conjunction with color.Output, example:
//
// put := New(FgYellow).SprintFunc()
// fmt.Fprintf(color.Output, "This is a %s", put("warning"))
func (c *Color) SprintFunc() func(a ...interface{}) string {
return func(a ...interface{}) string {
return c.wrap(fmt.Sprint(a...))
}
}
// SprintfFunc returns a new function that returns colorized strings for the
// given arguments with fmt.Sprintf(). Useful to put into or mix into other
// string. Windows users should use this in conjunction with color.Output.
func (c *Color) SprintfFunc() func(format string, a ...interface{}) string {
return func(format string, a ...interface{}) string {
return c.wrap(fmt.Sprintf(format, a...))
}
}
// SprintlnFunc returns a new function that returns colorized strings for the
// given arguments with fmt.Sprintln(). Useful to put into or mix into other
// string. Windows users should use this in conjunction with color.Output.
func (c *Color) SprintlnFunc() func(a ...interface{}) string {
return func(a ...interface{}) string {
return c.wrap(sprintln(a...)) + "\n"
}
}
// sequence returns a formatted SGR sequence to be plugged into a "\x1b[...m"
// an example output might be: "1;36" -> bold cyan
func (c *Color) sequence() string {
format := make([]string, len(c.params))
for i, v := range c.params {
format[i] = strconv.Itoa(int(v))
}
return strings.Join(format, ";")
}
// wrap wraps the s string with the colors attributes. The string is ready to
// be printed.
func (c *Color) wrap(s string) string {
if c.isNoColorSet() {
return s
}
return c.format() + s + c.unformat()
}
func (c *Color) format() string {
return fmt.Sprintf("%s[%sm", escape, c.sequence())
}
func (c *Color) unformat() string {
//return fmt.Sprintf("%s[%dm", escape, Reset)
//for each element in sequence let's use the specific reset escape, or the generic one if not found
format := make([]string, len(c.params))
for i, v := range c.params {
format[i] = strconv.Itoa(int(Reset))
ra, ok := mapResetAttributes[v]
if ok {
format[i] = strconv.Itoa(int(ra))
}
}
return fmt.Sprintf("%s[%sm", escape, strings.Join(format, ";"))
}
// DisableColor disables the color output. Useful to not change any existing
// code and still being able to output. Can be used for flags like
// "--no-color". To enable back use EnableColor() method.
func (c *Color) DisableColor() {
c.noColor = boolPtr(true)
}
// EnableColor enables the color output. Use it in conjunction with
// DisableColor(). Otherwise, this method has no side effects.
func (c *Color) EnableColor() {
c.noColor = boolPtr(false)
}
func (c *Color) isNoColorSet() bool {
// check first if we have user set action
if c.noColor != nil {
return *c.noColor
}
// if not return the global option, which is disabled by default
return NoColor
}
// Equals returns a boolean value indicating whether two colors are equal.
func (c *Color) Equals(c2 *Color) bool {
if c == nil && c2 == nil {
return true
}
if c == nil || c2 == nil {
return false
}
if len(c.params) != len(c2.params) {
return false
}
for _, attr := range c.params {
if !c2.attrExists(attr) {
return false
}
}
return true
}
func (c *Color) attrExists(a Attribute) bool {
for _, attr := range c.params {
if attr == a {
return true
}
}
return false
}
func boolPtr(v bool) *bool {
return &v
}
func getCachedColor(p Attribute) *Color {
colorsCacheMu.Lock()
defer colorsCacheMu.Unlock()
c, ok := colorsCache[p]
if !ok {
c = New(p)
colorsCache[p] = c
}
return c
}
func colorPrint(format string, p Attribute, a ...interface{}) {
c := getCachedColor(p)
if !strings.HasSuffix(format, "\n") {
format += "\n"
}
if len(a) == 0 {
c.Print(format)
} else {
c.Printf(format, a...)
}
}
func colorString(format string, p Attribute, a ...interface{}) string {
c := getCachedColor(p)
if len(a) == 0 {
return c.SprintFunc()(format)
}
return c.SprintfFunc()(format, a...)
}
// Black is a convenient helper function to print with black foreground. A
// newline is appended to format by default.
func Black(format string, a ...interface{}) { colorPrint(format, FgBlack, a...) }
// Red is a convenient helper function to print with red foreground. A
// newline is appended to format by default.
func Red(format string, a ...interface{}) { colorPrint(format, FgRed, a...) }
// Green is a convenient helper function to print with green foreground. A
// newline is appended to format by default.
func Green(format string, a ...interface{}) { colorPrint(format, FgGreen, a...) }
// Yellow is a convenient helper function to print with yellow foreground.
// A newline is appended to format by default.
func Yellow(format string, a ...interface{}) { colorPrint(format, FgYellow, a...) }
// Blue is a convenient helper function to print with blue foreground. A
// newline is appended to format by default.
func Blue(format string, a ...interface{}) { colorPrint(format, FgBlue, a...) }
// Magenta is a convenient helper function to print with magenta foreground.
// A newline is appended to format by default.
func Magenta(format string, a ...interface{}) { colorPrint(format, FgMagenta, a...) }
// Cyan is a convenient helper function to print with cyan foreground. A
// newline is appended to format by default.
func Cyan(format string, a ...interface{}) { colorPrint(format, FgCyan, a...) }
// White is a convenient helper function to print with white foreground. A
// newline is appended to format by default.
func White(format string, a ...interface{}) { colorPrint(format, FgWhite, a...) }
// BlackString is a convenient helper function to return a string with black
// foreground.
func BlackString(format string, a ...interface{}) string { return colorString(format, FgBlack, a...) }
// RedString is a convenient helper function to return a string with red
// foreground.
func RedString(format string, a ...interface{}) string { return colorString(format, FgRed, a...) }
// GreenString is a convenient helper function to return a string with green
// foreground.
func GreenString(format string, a ...interface{}) string { return colorString(format, FgGreen, a...) }
// YellowString is a convenient helper function to return a string with yellow
// foreground.
func YellowString(format string, a ...interface{}) string { return colorString(format, FgYellow, a...) }
// BlueString is a convenient helper function to return a string with blue
// foreground.
func BlueString(format string, a ...interface{}) string { return colorString(format, FgBlue, a...) }
// MagentaString is a convenient helper function to return a string with magenta
// foreground.
func MagentaString(format string, a ...interface{}) string {
return colorString(format, FgMagenta, a...)
}
// CyanString is a convenient helper function to return a string with cyan
// foreground.
func CyanString(format string, a ...interface{}) string { return colorString(format, FgCyan, a...) }
// WhiteString is a convenient helper function to return a string with white
// foreground.
func WhiteString(format string, a ...interface{}) string { return colorString(format, FgWhite, a...) }
// HiBlack is a convenient helper function to print with hi-intensity black foreground. A
// newline is appended to format by default.
func HiBlack(format string, a ...interface{}) { colorPrint(format, FgHiBlack, a...) }
// HiRed is a convenient helper function to print with hi-intensity red foreground. A
// newline is appended to format by default.
func HiRed(format string, a ...interface{}) { colorPrint(format, FgHiRed, a...) }
// HiGreen is a convenient helper function to print with hi-intensity green foreground. A
// newline is appended to format by default.
func HiGreen(format string, a ...interface{}) { colorPrint(format, FgHiGreen, a...) }
// HiYellow is a convenient helper function to print with hi-intensity yellow foreground.
// A newline is appended to format by default.
func HiYellow(format string, a ...interface{}) { colorPrint(format, FgHiYellow, a...) }
// HiBlue is a convenient helper function to print with hi-intensity blue foreground. A
// newline is appended to format by default.
func HiBlue(format string, a ...interface{}) { colorPrint(format, FgHiBlue, a...) }
// HiMagenta is a convenient helper function to print with hi-intensity magenta foreground.
// A newline is appended to format by default.
func HiMagenta(format string, a ...interface{}) { colorPrint(format, FgHiMagenta, a...) }
// HiCyan is a convenient helper function to print with hi-intensity cyan foreground. A
// newline is appended to format by default.
func HiCyan(format string, a ...interface{}) { colorPrint(format, FgHiCyan, a...) }
// HiWhite is a convenient helper function to print with hi-intensity white foreground. A
// newline is appended to format by default.
func HiWhite(format string, a ...interface{}) { colorPrint(format, FgHiWhite, a...) }
// HiBlackString is a convenient helper function to return a string with hi-intensity black
// foreground.
func HiBlackString(format string, a ...interface{}) string {
return colorString(format, FgHiBlack, a...)
}
// HiRedString is a convenient helper function to return a string with hi-intensity red
// foreground.
func HiRedString(format string, a ...interface{}) string { return colorString(format, FgHiRed, a...) }
// HiGreenString is a convenient helper function to return a string with hi-intensity green
// foreground.
func HiGreenString(format string, a ...interface{}) string {
return colorString(format, FgHiGreen, a...)
}
// HiYellowString is a convenient helper function to return a string with hi-intensity yellow
// foreground.
func HiYellowString(format string, a ...interface{}) string {
return colorString(format, FgHiYellow, a...)
}
// HiBlueString is a convenient helper function to return a string with hi-intensity blue
// foreground.
func HiBlueString(format string, a ...interface{}) string { return colorString(format, FgHiBlue, a...) }
// HiMagentaString is a convenient helper function to return a string with hi-intensity magenta
// foreground.
func HiMagentaString(format string, a ...interface{}) string {
return colorString(format, FgHiMagenta, a...)
}
// HiCyanString is a convenient helper function to return a string with hi-intensity cyan
// foreground.
func HiCyanString(format string, a ...interface{}) string { return colorString(format, FgHiCyan, a...) }
// HiWhiteString is a convenient helper function to return a string with hi-intensity white
// foreground.
func HiWhiteString(format string, a ...interface{}) string {
return colorString(format, FgHiWhite, a...)
}
// sprintln is a helper function to format a string with fmt.Sprintln and trim the trailing newline.
func sprintln(a ...interface{}) string {
return strings.TrimSuffix(fmt.Sprintln(a...), "\n")
}

19
vendor/github.com/fatih/color/color_windows.go generated vendored Normal file
View file

@ -0,0 +1,19 @@
package color
import (
"os"
"golang.org/x/sys/windows"
)
func init() {
// Opt-in for ansi color support for current process.
// https://learn.microsoft.com/en-us/windows/console/console-virtual-terminal-sequences#output-sequences
var outMode uint32
out := windows.Handle(os.Stdout.Fd())
if err := windows.GetConsoleMode(out, &outMode); err != nil {
return
}
outMode |= windows.ENABLE_PROCESSED_OUTPUT | windows.ENABLE_VIRTUAL_TERMINAL_PROCESSING
_ = windows.SetConsoleMode(out, outMode)
}

134
vendor/github.com/fatih/color/doc.go generated vendored Normal file
View file

@ -0,0 +1,134 @@
/*
Package color is an ANSI color package to output colorized or SGR defined
output to the standard output. The API can be used in several way, pick one
that suits you.
Use simple and default helper functions with predefined foreground colors:
color.Cyan("Prints text in cyan.")
// a newline will be appended automatically
color.Blue("Prints %s in blue.", "text")
// More default foreground colors..
color.Red("We have red")
color.Yellow("Yellow color too!")
color.Magenta("And many others ..")
// Hi-intensity colors
color.HiGreen("Bright green color.")
color.HiBlack("Bright black means gray..")
color.HiWhite("Shiny white color!")
However, there are times when custom color mixes are required. Below are some
examples to create custom color objects and use the print functions of each
separate color object.
// Create a new color object
c := color.New(color.FgCyan).Add(color.Underline)
c.Println("Prints cyan text with an underline.")
// Or just add them to New()
d := color.New(color.FgCyan, color.Bold)
d.Printf("This prints bold cyan %s\n", "too!.")
// Mix up foreground and background colors, create new mixes!
red := color.New(color.FgRed)
boldRed := red.Add(color.Bold)
boldRed.Println("This will print text in bold red.")
whiteBackground := red.Add(color.BgWhite)
whiteBackground.Println("Red text with White background.")
// Use your own io.Writer output
color.New(color.FgBlue).Fprintln(myWriter, "blue color!")
blue := color.New(color.FgBlue)
blue.Fprint(myWriter, "This will print text in blue.")
You can create PrintXxx functions to simplify even more:
// Create a custom print function for convenient
red := color.New(color.FgRed).PrintfFunc()
red("warning")
red("error: %s", err)
// Mix up multiple attributes
notice := color.New(color.Bold, color.FgGreen).PrintlnFunc()
notice("don't forget this...")
You can also FprintXxx functions to pass your own io.Writer:
blue := color.New(FgBlue).FprintfFunc()
blue(myWriter, "important notice: %s", stars)
// Mix up with multiple attributes
success := color.New(color.Bold, color.FgGreen).FprintlnFunc()
success(myWriter, don't forget this...")
Or create SprintXxx functions to mix strings with other non-colorized strings:
yellow := New(FgYellow).SprintFunc()
red := New(FgRed).SprintFunc()
fmt.Printf("this is a %s and this is %s.\n", yellow("warning"), red("error"))
info := New(FgWhite, BgGreen).SprintFunc()
fmt.Printf("this %s rocks!\n", info("package"))
Windows support is enabled by default. All Print functions work as intended.
However, only for color.SprintXXX functions, user should use fmt.FprintXXX and
set the output to color.Output:
fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS"))
info := New(FgWhite, BgGreen).SprintFunc()
fmt.Fprintf(color.Output, "this %s rocks!\n", info("package"))
Using with existing code is possible. Just use the Set() method to set the
standard output to the given parameters. That way a rewrite of an existing
code is not required.
// Use handy standard colors.
color.Set(color.FgYellow)
fmt.Println("Existing text will be now in Yellow")
fmt.Printf("This one %s\n", "too")
color.Unset() // don't forget to unset
// You can mix up parameters
color.Set(color.FgMagenta, color.Bold)
defer color.Unset() // use it in your function
fmt.Println("All text will be now bold magenta.")
There might be a case where you want to disable color output (for example to
pipe the standard output of your app to somewhere else). `Color` has support to
disable colors both globally and for single color definition. For example
suppose you have a CLI app and a `--no-color` bool flag. You can easily disable
the color output with:
var flagNoColor = flag.Bool("no-color", false, "Disable color output")
if *flagNoColor {
color.NoColor = true // disables colorized output
}
You can also disable the color by setting the NO_COLOR environment variable to any value.
It also has support for single color definitions (local). You can
disable/enable color output on the fly:
c := color.New(color.FgCyan)
c.Println("Prints cyan text")
c.DisableColor()
c.Println("This is printed without any color")
c.EnableColor()
c.Println("This prints again cyan...")
*/
package color

View file

@ -11,3 +11,5 @@ coverage:
default: default:
# Do not fail the commit status if the coverage was reduced up to this value # Do not fail the commit status if the coverage was reduced up to this value
threshold: 0.5% threshold: 0.5%
ignore:
- "log_fallback.go"

View file

@ -1,20 +1,17 @@
version: "2"
linters: linters:
disable-all: true default: none
enable: enable:
- bodyclose - bodyclose
- dogsled - dogsled
- dupl - dupl
- errcheck - errcheck
- exportloopref
- gochecknoinits - gochecknoinits
- goconst - goconst
- gocritic - gocritic
- gocyclo - gocyclo
- godot - godot
- gofmt
- goimports
- gosec - gosec
- gosimple
- govet - govet
- ineffassign - ineffassign
- misspell - misspell
@ -22,25 +19,44 @@ linters:
- prealloc - prealloc
- revive - revive
- staticcheck - staticcheck
- typecheck
- unconvert - unconvert
- unparam - unparam
- unused - unused
- whitespace - whitespace
issues: exclusions:
exclude-rules: generated: lax
- path: _test\.go presets:
linters: - comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- goconst - goconst
- prealloc - prealloc
- path: _test\.go path: _test\.go
text: "G306:" - linters:
linters:
- gosec - gosec
- path: errors_test\.go path: _test\.go
linters: text: 'G306:'
- linters:
- unused - unused
- path: http/example_test\.go path: errors_test\.go
linters: - linters:
- errcheck
- bodyclose - bodyclose
- errcheck
path: http/example_test\.go
paths:
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View file

@ -1,5 +1,138 @@
# Changelog # Changelog
## 0.34.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.34.0.
### Breaking Changes
- Logrus structured logging support replaces the `sentrylogrus.Hook` signature from a `*Hook` to an interface.
```go
var hook *sentrylogrus.Hook
hook = sentrylogrus.New(
// ... your setup
)
// should change the definition to
var hook sentrylogrus.Hook
hook = sentrylogrus.New(
// ... your setup
)
```
### Features
- Structured logging support for [slog](https://pkg.go.dev/log/slog). ([#1033](https://github.com/getsentry/sentry-go/pull/1033))
```go
ctx := context.Background()
handler := sentryslog.Option{
EventLevel: []slog.Level{slog.LevelError, sentryslog.LevelFatal}, // Only Error and Fatal as events
LogLevel: []slog.Level{slog.LevelWarn, slog.LevelInfo}, // Only Warn and Info as logs
}.NewSentryHandler(ctx)
logger := slog.New(handler)
logger.Info("hello"))
```
- Structured logging support for [logrus](https://github.com/sirupsen/logrus). ([#1036](https://github.com/getsentry/sentry-go/pull/1036))
```go
logHook, _ := sentrylogrus.NewLogHook(
[]logrus.Level{logrus.InfoLevel, logrus.WarnLevel},
sentry.ClientOptions{
Dsn: "your-dsn",
EnableLogs: true, // Required for log entries
})
defer logHook.Flush(5 * time.Secod)
logrus.RegisterExitHandler(func() {
logHook.Flush(5 * time.Second)
})
logger := logrus.New()
logger.AddHook(logHook)
logger.Infof("hello")
```
- Add support for flushing events with context using `FlushWithContext()`. ([#935](https://github.com/getsentry/sentry-go/pull/935))
```go
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if !sentry.FlushWithContext(ctx) {
// Handle timeout or cancellation
}
```
- Add support for custom fingerprints in slog integration. ([#1039](https://github.com/getsentry/sentry-go/pull/1039))
### Deprecations
- Slog structured logging support replaces `Level` option with `EventLevel` and `LogLevel` options, for specifying fine-grained levels for capturing events and logs.
```go
handler := sentryslog.Option{
EventLevel: []slog.Level{slog.LevelWarn, slog.LevelError, sentryslog.LevelFatal},
LogLevel: []slog.Level{slog.LevelDebug, slog.LevelInfo, slog.LevelWarn, slog.LevelError, sentryslog.LevelFatal},
}.NewSentryHandler(ctx)
```
- Logrus structured logging support replaces `New` and `NewFromClient` functions to `NewEventHook`, `NewEventHookFromClient`, to match the newly added `NewLogHook` functions, and specify the hook type being created each time.
```go
logHook, err := sentrylogrus.NewLogHook(
[]logrus.Level{logrus.InfoLevel},
sentry.ClientOptions{})
eventHook, err := sentrylogrus.NewEventHook([]logrus.Level{
logrus.ErrorLevel,
logrus.FatalLevel,
logrus.PanicLevel,
}, sentry.ClientOptions{})
```
### Bug Fixes
- Fix issue where `ContinueTrace()` would panic when `sentry-trace` header does not exist. ([#1026](https://github.com/getsentry/sentry-go/pull/1026))
- Fix incorrect log level signature in structured logging. ([#1034](https://github.com/getsentry/sentry-go/pull/1034))
- Remove `sentry.origin` attribute from Sentry logger to prevent confusion in spans. ([#1038](https://github.com/getsentry/sentry-go/pull/1038))
- Don't gate user information behind `SendDefaultPII` flag for logs. ([#1032](https://github.com/getsentry/sentry-go/pull/1032))
### Misc
- Add more sensitive HTTP headers to the default list of headers that are scrubbed by default. ([#1008](https://github.com/getsentry/sentry-go/pull/1008))
## 0.33.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.33.0.
### Breaking Changes
- Rename the internal `Logger` to `DebugLogger`. This feature was only used when you set `Debug: True` in your `sentry.Init()` call. If you haven't used the Logger directly, no changes are necessary. ([#1012](https://github.com/getsentry/sentry-go/issues/1012))
### Features
- Add support for [Structured Logging](https://docs.sentry.io/product/explore/logs/). ([#1010](https://github.com/getsentry/sentry-go/issues/1010))
```go
logger := sentry.NewLogger(ctx)
logger.Info(ctx, "Hello, Logs!")
```
You can learn more about Sentry Logs on our [docs](https://docs.sentry.io/product/explore/logs/) and the [examples](https://github.com/getsentry/sentry-go/blob/master/_examples/logs/main.go).
- Add new attributes APIs, which are currently only exposed on logs. ([#1007](https://github.com/getsentry/sentry-go/issues/1007))
### Bug Fixes
- Do not push a new scope on `StartSpan`. ([#1013](https://github.com/getsentry/sentry-go/issues/1013))
- Fix an issue where the propagated smapling decision wasn't used. ([#995](https://github.com/getsentry/sentry-go/issues/995))
- [Otel] Prefer `httpRoute` over `httpTarget` for span descriptions. ([#1002](https://github.com/getsentry/sentry-go/issues/1002))
### Misc
- Update `github.com/stretchr/testify` to v1.8.4. ([#988](https://github.com/getsentry/sentry-go/issues/988))
## 0.32.0 ## 0.32.0
The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.32.0. The Sentry SDK team is happy to announce the immediate availability of Sentry Go SDK v0.32.0.

View file

@ -0,0 +1,36 @@
package attribute
type Builder struct {
Key string
Value Value
}
// String returns a Builder for a string value.
func String(key, value string) Builder {
return Builder{key, StringValue(value)}
}
// Int64 returns a Builder for an int64.
func Int64(key string, value int64) Builder {
return Builder{key, Int64Value(value)}
}
// Int returns a Builder for an int64.
func Int(key string, value int) Builder {
return Builder{key, IntValue(value)}
}
// Float64 returns a Builder for a float64.
func Float64(key string, v float64) Builder {
return Builder{key, Float64Value(v)}
}
// Bool returns a Builder for a boolean.
func Bool(key string, v bool) Builder {
return Builder{key, BoolValue(v)}
}
// Valid checks for valid key and type.
func (b *Builder) Valid() bool {
return len(b.Key) > 0 && b.Value.Type() != INVALID
}

View file

@ -0,0 +1,49 @@
// Copied from https://github.com/open-telemetry/opentelemetry-go/blob/cc43e01c27892252aac9a8f20da28cdde957a289/attribute/rawhelpers.go
// Copyright The OpenTelemetry Authors
//
// 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.
package attribute
import (
"math"
)
func boolToRaw(b bool) uint64 { // b is not a control flag.
if b {
return 1
}
return 0
}
func rawToBool(r uint64) bool {
return r != 0
}
func int64ToRaw(i int64) uint64 {
// Assumes original was a valid int64 (overflow not checked).
return uint64(i) // nolint: gosec
}
func rawToInt64(r uint64) int64 {
// Assumes original was a valid int64 (overflow not checked).
return int64(r) // nolint: gosec
}
func float64ToRaw(f float64) uint64 {
return math.Float64bits(f)
}
func rawToFloat64(r uint64) float64 {
return math.Float64frombits(r)
}

View file

@ -0,0 +1,170 @@
// Adapted from https://github.com/open-telemetry/opentelemetry-go/blob/cc43e01c27892252aac9a8f20da28cdde957a289/attribute/value.go
//
// Copyright The OpenTelemetry Authors
//
// 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.
package attribute
import (
"encoding/json"
"fmt"
"strconv"
)
// Type describes the type of the data Value holds.
type Type int // redefines builtin Type.
// Value represents the value part in key-value pairs.
type Value struct {
vtype Type
numeric uint64
stringly string
}
const (
// INVALID is used for a Value with no value set.
INVALID Type = iota
// BOOL is a boolean Type Value.
BOOL
// INT64 is a 64-bit signed integral Type Value.
INT64
// FLOAT64 is a 64-bit floating point Type Value.
FLOAT64
// STRING is a string Type Value.
STRING
)
// BoolValue creates a BOOL Value.
func BoolValue(v bool) Value {
return Value{
vtype: BOOL,
numeric: boolToRaw(v),
}
}
// IntValue creates an INT64 Value.
func IntValue(v int) Value {
return Int64Value(int64(v))
}
// Int64Value creates an INT64 Value.
func Int64Value(v int64) Value {
return Value{
vtype: INT64,
numeric: int64ToRaw(v),
}
}
// Float64Value creates a FLOAT64 Value.
func Float64Value(v float64) Value {
return Value{
vtype: FLOAT64,
numeric: float64ToRaw(v),
}
}
// StringValue creates a STRING Value.
func StringValue(v string) Value {
return Value{
vtype: STRING,
stringly: v,
}
}
// Type returns a type of the Value.
func (v Value) Type() Type {
return v.vtype
}
// AsBool returns the bool value. Make sure that the Value's type is
// BOOL.
func (v Value) AsBool() bool {
return rawToBool(v.numeric)
}
// AsInt64 returns the int64 value. Make sure that the Value's type is
// INT64.
func (v Value) AsInt64() int64 {
return rawToInt64(v.numeric)
}
// AsFloat64 returns the float64 value. Make sure that the Value's
// type is FLOAT64.
func (v Value) AsFloat64() float64 {
return rawToFloat64(v.numeric)
}
// AsString returns the string value. Make sure that the Value's type
// is STRING.
func (v Value) AsString() string {
return v.stringly
}
type unknownValueType struct{}
// AsInterface returns Value's data as interface{}.
func (v Value) AsInterface() interface{} {
switch v.Type() {
case BOOL:
return v.AsBool()
case INT64:
return v.AsInt64()
case FLOAT64:
return v.AsFloat64()
case STRING:
return v.stringly
}
return unknownValueType{}
}
// String returns a string representation of Value's data.
func (v Value) String() string {
switch v.Type() {
case BOOL:
return strconv.FormatBool(v.AsBool())
case INT64:
return strconv.FormatInt(v.AsInt64(), 10)
case FLOAT64:
return fmt.Sprint(v.AsFloat64())
case STRING:
return v.stringly
default:
return "unknown"
}
}
// MarshalJSON returns the JSON encoding of the Value.
func (v Value) MarshalJSON() ([]byte, error) {
var jsonVal struct {
Type string
Value interface{}
}
jsonVal.Type = v.Type().String()
jsonVal.Value = v.AsInterface()
return json.Marshal(jsonVal)
}
func (t Type) String() string {
switch t {
case BOOL:
return "bool"
case INT64:
return "int64"
case FLOAT64:
return "float64"
case STRING:
return "string"
}
return "invalid"
}

94
vendor/github.com/getsentry/sentry-go/batch_logger.go generated vendored Normal file
View file

@ -0,0 +1,94 @@
package sentry
import (
"context"
"sync"
"time"
)
const (
batchSize = 100
batchTimeout = 5 * time.Second
)
type BatchLogger struct {
client *Client
logCh chan Log
cancel context.CancelFunc
wg sync.WaitGroup
startOnce sync.Once
}
func NewBatchLogger(client *Client) *BatchLogger {
return &BatchLogger{
client: client,
logCh: make(chan Log, batchSize),
}
}
func (l *BatchLogger) Start() {
l.startOnce.Do(func() {
ctx, cancel := context.WithCancel(context.Background())
l.cancel = cancel
l.wg.Add(1)
go l.run(ctx)
})
}
func (l *BatchLogger) Flush() {
if l.cancel != nil {
l.cancel()
l.wg.Wait()
}
}
func (l *BatchLogger) run(ctx context.Context) {
defer l.wg.Done()
var logs []Log
timer := time.NewTimer(batchTimeout)
for {
select {
case log := <-l.logCh:
logs = append(logs, log)
if len(logs) >= batchSize {
l.processEvent(logs)
logs = nil
if !timer.Stop() {
<-timer.C
}
timer.Reset(batchTimeout)
}
case <-timer.C:
if len(logs) > 0 {
l.processEvent(logs)
logs = nil
}
timer.Reset(batchTimeout)
case <-ctx.Done():
// Drain remaining logs from channel
drain:
for {
select {
case log := <-l.logCh:
logs = append(logs, log)
default:
break drain
}
}
if len(logs) > 0 {
l.processEvent(logs)
}
return
}
}
}
func (l *BatchLogger) processEvent(logs []Log) {
event := NewEvent()
event.Timestamp = time.Now()
event.Type = logEvent.Type
event.Logs = logs
l.client.CaptureEvent(event, nil, nil)
}

View file

@ -77,9 +77,9 @@ type usageError struct {
error error
} }
// Logger is an instance of log.Logger that is use to provide debug information about running Sentry Client // DebugLogger is an instance of log.Logger that is used to provide debug information about running Sentry Client
// can be enabled by either using Logger.SetOutput directly or with Debug client option. // can be enabled by either using DebugLogger.SetOutput directly or with Debug client option.
var Logger = log.New(io.Discard, "[Sentry] ", log.LstdFlags) var DebugLogger = log.New(io.Discard, "[Sentry] ", log.LstdFlags)
// EventProcessor is a function that processes an event. // EventProcessor is a function that processes an event.
// Event processors are used to change an event before it is sent to Sentry. // Event processors are used to change an event before it is sent to Sentry.
@ -144,8 +144,11 @@ type ClientOptions struct {
// By default, no such data is sent. // By default, no such data is sent.
SendDefaultPII bool SendDefaultPII bool
// BeforeSend is called before error events are sent to Sentry. // BeforeSend is called before error events are sent to Sentry.
// Use it to mutate the event or return nil to discard the event. // You can use it to mutate the event or return nil to discard it.
BeforeSend func(event *Event, hint *EventHint) *Event BeforeSend func(event *Event, hint *EventHint) *Event
// BeforeSendLong is called before log events are sent to Sentry.
// You can use it to mutate the log event or return nil to discard it.
BeforeSendLog func(event *Log) *Log
// BeforeSendTransaction is called before transaction events are sent to Sentry. // BeforeSendTransaction is called before transaction events are sent to Sentry.
// Use it to mutate the transaction or return nil to discard the transaction. // Use it to mutate the transaction or return nil to discard the transaction.
BeforeSendTransaction func(event *Event, hint *EventHint) *Event BeforeSendTransaction func(event *Event, hint *EventHint) *Event
@ -223,6 +226,8 @@ type ClientOptions struct {
MaxErrorDepth int MaxErrorDepth int
// Default event tags. These are overridden by tags set on a scope. // Default event tags. These are overridden by tags set on a scope.
Tags map[string]string Tags map[string]string
// EnableLogs controls when logs should be emitted.
EnableLogs bool
} }
// Client is the underlying processor that is used by the main API and Hub // Client is the underlying processor that is used by the main API and Hub
@ -238,6 +243,7 @@ type Client struct {
// Transport is read-only. Replacing the transport of an existing client is // Transport is read-only. Replacing the transport of an existing client is
// not supported, create a new client instead. // not supported, create a new client instead.
Transport Transport Transport Transport
batchLogger *BatchLogger
} }
// NewClient creates and returns an instance of Client configured using // NewClient creates and returns an instance of Client configured using
@ -275,7 +281,7 @@ func NewClient(options ClientOptions) (*Client, error) {
if debugWriter == nil { if debugWriter == nil {
debugWriter = os.Stderr debugWriter = os.Stderr
} }
Logger.SetOutput(debugWriter) DebugLogger.SetOutput(debugWriter)
} }
if options.Dsn == "" { if options.Dsn == "" {
@ -337,6 +343,11 @@ func NewClient(options ClientOptions) (*Client, error) {
sdkVersion: SDKVersion, sdkVersion: SDKVersion,
} }
if options.EnableLogs {
client.batchLogger = NewBatchLogger(&client)
client.batchLogger.Start()
}
client.setupTransport() client.setupTransport()
client.setupIntegrations() client.setupIntegrations()
@ -351,15 +362,7 @@ func (client *Client) setupTransport() {
if opts.Dsn == "" { if opts.Dsn == "" {
transport = new(noopTransport) transport = new(noopTransport)
} else { } else {
httpTransport := NewHTTPTransport() transport = NewHTTPTransport()
// When tracing is enabled, use larger buffer to
// accommodate more concurrent events.
// TODO(tracing): consider using separate buffers per
// event type.
if opts.EnableTracing {
httpTransport.BufferSize = 1000
}
transport = httpTransport
} }
} }
@ -383,12 +386,12 @@ func (client *Client) setupIntegrations() {
for _, integration := range integrations { for _, integration := range integrations {
if client.integrationAlreadyInstalled(integration.Name()) { if client.integrationAlreadyInstalled(integration.Name()) {
Logger.Printf("Integration %s is already installed\n", integration.Name()) DebugLogger.Printf("Integration %s is already installed\n", integration.Name())
continue continue
} }
client.integrations = append(client.integrations, integration) client.integrations = append(client.integrations, integration)
integration.SetupOnce(client) integration.SetupOnce(client)
Logger.Printf("Integration installed: %s\n", integration.Name()) DebugLogger.Printf("Integration installed: %s\n", integration.Name())
} }
sort.Slice(client.integrations, func(i, j int) bool { sort.Slice(client.integrations, func(i, j int) bool {
@ -507,9 +510,31 @@ func (client *Client) RecoverWithContext(
// the network synchronously, configure it to use the HTTPSyncTransport in the // the network synchronously, configure it to use the HTTPSyncTransport in the
// call to Init. // call to Init.
func (client *Client) Flush(timeout time.Duration) bool { func (client *Client) Flush(timeout time.Duration) bool {
if client.batchLogger != nil {
client.batchLogger.Flush()
}
return client.Transport.Flush(timeout) return client.Transport.Flush(timeout)
} }
// FlushWithContext waits until the underlying Transport sends any buffered events
// to the Sentry server, blocking for at most the duration specified by the context.
// It returns false if the context is canceled before the events are sent. In such a case,
// some events may not be delivered.
//
// FlushWithContext should be called before terminating the program to ensure no
// events are unintentionally dropped.
//
// Avoid calling FlushWithContext indiscriminately after each call to CaptureEvent,
// CaptureException, or CaptureMessage. To send events synchronously over the network,
// configure the SDK to use HTTPSyncTransport during initialization with Init.
func (client *Client) FlushWithContext(ctx context.Context) bool {
if client.batchLogger != nil {
client.batchLogger.Flush()
}
return client.Transport.FlushWithContext(ctx)
}
// Close clean up underlying Transport resources. // Close clean up underlying Transport resources.
// //
// Close should be called after Flush and before terminating the program // Close should be called after Flush and before terminating the program
@ -605,7 +630,7 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
// options.TracesSampler when they are started. Other events // options.TracesSampler when they are started. Other events
// (errors, messages) are sampled here. Does not apply to check-ins. // (errors, messages) are sampled here. Does not apply to check-ins.
if event.Type != transactionType && event.Type != checkInType && !sample(client.options.SampleRate) { if event.Type != transactionType && event.Type != checkInType && !sample(client.options.SampleRate) {
Logger.Println("Event dropped due to SampleRate hit.") DebugLogger.Println("Event dropped due to SampleRate hit.")
return nil return nil
} }
@ -617,19 +642,23 @@ func (client *Client) processEvent(event *Event, hint *EventHint, scope EventMod
if hint == nil { if hint == nil {
hint = &EventHint{} hint = &EventHint{}
} }
if event.Type == transactionType && client.options.BeforeSendTransaction != nil { switch event.Type {
// Transaction events case transactionType:
if client.options.BeforeSendTransaction != nil {
if event = client.options.BeforeSendTransaction(event, hint); event == nil { if event = client.options.BeforeSendTransaction(event, hint); event == nil {
Logger.Println("Transaction dropped due to BeforeSendTransaction callback.") DebugLogger.Println("Transaction dropped due to BeforeSendTransaction callback.")
return nil return nil
} }
} else if event.Type != transactionType && event.Type != checkInType && client.options.BeforeSend != nil { }
// All other events case checkInType: // not a default case, since we shouldn't apply BeforeSend on check-in events
default:
if client.options.BeforeSend != nil {
if event = client.options.BeforeSend(event, hint); event == nil { if event = client.options.BeforeSend(event, hint); event == nil {
Logger.Println("Event dropped due to BeforeSend callback.") DebugLogger.Println("Event dropped due to BeforeSend callback.")
return nil return nil
} }
} }
}
client.Transport.SendEvent(event) client.Transport.SendEvent(event)
@ -692,7 +721,7 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod
id := event.EventID id := event.EventID
event = processor(event, hint) event = processor(event, hint)
if event == nil { if event == nil {
Logger.Printf("Event dropped by one of the Client EventProcessors: %s\n", id) DebugLogger.Printf("Event dropped by one of the Client EventProcessors: %s\n", id)
return nil return nil
} }
} }
@ -701,7 +730,7 @@ func (client *Client) prepareEvent(event *Event, hint *EventHint, scope EventMod
id := event.EventID id := event.EventID
event = processor(event, hint) event = processor(event, hint)
if event == nil { if event == nil {
Logger.Printf("Event dropped by one of the Global EventProcessors: %s\n", id) DebugLogger.Printf("Event dropped by one of the Global EventProcessors: %s\n", id)
return nil return nil
} }
} }

View file

@ -308,7 +308,7 @@ func (hub *Hub) AddBreadcrumb(breadcrumb *Breadcrumb, hint *BreadcrumbHint) {
hint = &BreadcrumbHint{} hint = &BreadcrumbHint{}
} }
if breadcrumb = client.options.BeforeBreadcrumb(breadcrumb, hint); breadcrumb == nil { if breadcrumb = client.options.BeforeBreadcrumb(breadcrumb, hint); breadcrumb == nil {
Logger.Println("breadcrumb dropped due to BeforeBreadcrumb callback.") DebugLogger.Println("breadcrumb dropped due to BeforeBreadcrumb callback.")
return return
} }
} }
@ -365,6 +365,28 @@ func (hub *Hub) Flush(timeout time.Duration) bool {
return client.Flush(timeout) return client.Flush(timeout)
} }
// FlushWithContext waits until the underlying Transport sends any buffered events
// to the Sentry server, blocking for at most the duration specified by the context.
// It returns false if the context is canceled before the events are sent. In such a case,
// some events may not be delivered.
//
// FlushWithContext should be called before terminating the program to ensure no
// events are unintentionally dropped.
//
// Avoid calling FlushWithContext indiscriminately after each call to CaptureEvent,
// CaptureException, or CaptureMessage. To send events synchronously over the network,
// configure the SDK to use HTTPSyncTransport during initialization with Init.
func (hub *Hub) FlushWithContext(ctx context.Context) bool {
client := hub.Client()
if client == nil {
return false
}
return client.FlushWithContext(ctx)
}
// GetTraceparent returns the current Sentry traceparent string, to be used as a HTTP header value // GetTraceparent returns the current Sentry traceparent string, to be used as a HTTP header value
// or HTML meta tag value. // or HTML meta tag value.
// This function is context aware, as in it either returns the traceparent based // This function is context aware, as in it either returns the traceparent based

View file

@ -32,7 +32,7 @@ func (mi *modulesIntegration) processor(event *Event, _ *EventHint) *Event {
mi.once.Do(func() { mi.once.Do(func() {
info, ok := debug.ReadBuildInfo() info, ok := debug.ReadBuildInfo()
if !ok { if !ok {
Logger.Print("The Modules integration is not available in binaries built without module support.") DebugLogger.Print("The Modules integration is not available in binaries built without module support.")
return return
} }
mi.modules = extractModules(info) mi.modules = extractModules(info)
@ -141,7 +141,7 @@ func (iei *ignoreErrorsIntegration) processor(event *Event, _ *EventHint) *Event
for _, suspect := range suspects { for _, suspect := range suspects {
for _, pattern := range iei.ignoreErrors { for _, pattern := range iei.ignoreErrors {
if pattern.Match([]byte(suspect)) || strings.Contains(suspect, pattern.String()) { if pattern.Match([]byte(suspect)) || strings.Contains(suspect, pattern.String()) {
Logger.Printf("Event dropped due to being matched by `IgnoreErrors` option."+ DebugLogger.Printf("Event dropped due to being matched by `IgnoreErrors` option."+
"| Value matched: %s | Filter used: %s", suspect, pattern) "| Value matched: %s | Filter used: %s", suspect, pattern)
return nil return nil
} }
@ -203,7 +203,7 @@ func (iei *ignoreTransactionsIntegration) processor(event *Event, _ *EventHint)
for _, pattern := range iei.ignoreTransactions { for _, pattern := range iei.ignoreTransactions {
if pattern.Match([]byte(suspect)) || strings.Contains(suspect, pattern.String()) { if pattern.Match([]byte(suspect)) || strings.Contains(suspect, pattern.String()) {
Logger.Printf("Transaction dropped due to being matched by `IgnoreTransactions` option."+ DebugLogger.Printf("Transaction dropped due to being matched by `IgnoreTransactions` option."+
"| Value matched: %s | Filter used: %s", suspect, pattern) "| Value matched: %s | Filter used: %s", suspect, pattern)
return nil return nil
} }

View file

@ -11,17 +11,22 @@ import (
"slices" "slices"
"strings" "strings"
"time" "time"
"github.com/getsentry/sentry-go/attribute"
) )
// eventType is the type of an error event.
const eventType = "event" const eventType = "event"
// transactionType is the type of a transaction event.
const transactionType = "transaction" const transactionType = "transaction"
// checkInType is the type of a check in event.
const checkInType = "check_in" const checkInType = "check_in"
var logEvent = struct {
Type string
ContentType string
}{
"log",
"application/vnd.sentry.items.log+json",
}
// Level marks the severity of the event. // Level marks the severity of the event.
type Level string type Level string
@ -34,7 +39,7 @@ const (
LevelFatal Level = "fatal" LevelFatal Level = "fatal"
) )
// SdkInfo contains all metadata about about the SDK being used. // SdkInfo contains all metadata about the SDK.
type SdkInfo struct { type SdkInfo struct {
Name string `json:"name,omitempty"` Name string `json:"name,omitempty"`
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
@ -96,6 +101,60 @@ func (b *Breadcrumb) MarshalJSON() ([]byte, error) {
return json.Marshal((*breadcrumb)(b)) return json.Marshal((*breadcrumb)(b))
} }
type Logger interface {
// Write implements the io.Writer interface. Currently, the [sentry.Hub] is
// context aware, in order to get the correct trace correlation. Using this
// might result in incorrect span association on logs. If you need to use
// Write it is recommended to create a NewLogger so that the associated context
// is passed correctly.
Write(p []byte) (n int, err error)
// Trace emits a [LogLevelTrace] log to Sentry.
// Arguments are handled in the manner of [fmt.Print].
Trace(ctx context.Context, v ...interface{})
// Debug emits a [LogLevelDebug] log to Sentry.
// Arguments are handled in the manner of [fmt.Print].
Debug(ctx context.Context, v ...interface{})
// Info emits a [LogLevelInfo] log to Sentry.
// Arguments are handled in the manner of [fmt.Print].
Info(ctx context.Context, v ...interface{})
// Warn emits a [LogLevelWarn] log to Sentry.
// Arguments are handled in the manner of [fmt.Print].
Warn(ctx context.Context, v ...interface{})
// Error emits a [LogLevelError] log to Sentry.
// Arguments are handled in the manner of [fmt.Print].
Error(ctx context.Context, v ...interface{})
// Fatal emits a [LogLevelFatal] log to Sentry followed by a call to [os.Exit](1).
// Arguments are handled in the manner of [fmt.Print].
Fatal(ctx context.Context, v ...interface{})
// Panic emits a [LogLevelFatal] log to Sentry followed by a call to panic().
// Arguments are handled in the manner of [fmt.Print].
Panic(ctx context.Context, v ...interface{})
// Tracef emits a [LogLevelTrace] log to Sentry.
// Arguments are handled in the manner of [fmt.Printf].
Tracef(ctx context.Context, format string, v ...interface{})
// Debugf emits a [LogLevelDebug] log to Sentry.
// Arguments are handled in the manner of [fmt.Printf].
Debugf(ctx context.Context, format string, v ...interface{})
// Infof emits a [LogLevelInfo] log to Sentry.
// Arguments are handled in the manner of [fmt.Printf].
Infof(ctx context.Context, format string, v ...interface{})
// Warnf emits a [LogLevelWarn] log to Sentry.
// Arguments are handled in the manner of [fmt.Printf].
Warnf(ctx context.Context, format string, v ...interface{})
// Errorf emits a [LogLevelError] log to Sentry.
// Arguments are handled in the manner of [fmt.Printf].
Errorf(ctx context.Context, format string, v ...interface{})
// Fatalf emits a [LogLevelFatal] log to Sentry followed by a call to [os.Exit](1).
// Arguments are handled in the manner of [fmt.Printf].
Fatalf(ctx context.Context, format string, v ...interface{})
// Panicf emits a [LogLevelFatal] log to Sentry followed by a call to panic().
// Arguments are handled in the manner of [fmt.Printf].
Panicf(ctx context.Context, format string, v ...interface{})
// SetAttributes allows attaching parameters to the log message using the attribute API.
SetAttributes(...attribute.Builder)
}
// Attachment allows associating files with your events to aid in investigation. // Attachment allows associating files with your events to aid in investigation.
// An event may contain one or more attachments. // An event may contain one or more attachments.
type Attachment struct { type Attachment struct {
@ -155,11 +214,36 @@ type Request struct {
} }
var sensitiveHeaders = map[string]struct{}{ var sensitiveHeaders = map[string]struct{}{
"_csrf": {},
"_csrf_token": {},
"_session": {},
"_xsrf": {},
"Api-Key": {},
"Apikey": {},
"Auth": {},
"Authorization": {}, "Authorization": {},
"Proxy-Authorization": {},
"Cookie": {}, "Cookie": {},
"Credentials": {},
"Csrf": {},
"Csrf-Token": {},
"Csrftoken": {},
"Ip-Address": {},
"Passwd": {},
"Password": {},
"Private-Key": {},
"Privatekey": {},
"Proxy-Authorization": {},
"Remote-Addr": {},
"Secret": {},
"Session": {},
"Sessionid": {},
"Token": {},
"User-Session": {},
"X-Api-Key": {},
"X-Csrftoken": {},
"X-Forwarded-For": {}, "X-Forwarded-For": {},
"X-Real-Ip": {}, "X-Real-Ip": {},
"XSRF-TOKEN": {},
} }
// NewRequest returns a new Sentry Request from the given http.Request. // NewRequest returns a new Sentry Request from the given http.Request.
@ -325,6 +409,9 @@ type Event struct {
CheckIn *CheckIn `json:"check_in,omitempty"` CheckIn *CheckIn `json:"check_in,omitempty"`
MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"` MonitorConfig *MonitorConfig `json:"monitor_config,omitempty"`
// The fields below are only relevant for logs
Logs []Log `json:"items,omitempty"`
// The fields below are not part of the final JSON payload. // The fields below are not part of the final JSON payload.
sdkMetaData SDKMetaData sdkMetaData SDKMetaData
@ -547,3 +634,17 @@ type EventHint struct {
Request *http.Request Request *http.Request
Response *http.Response Response *http.Response
} }
type Log struct {
Timestamp time.Time `json:"timestamp,omitempty"`
TraceID TraceID `json:"trace_id,omitempty"`
Level LogLevel `json:"level"`
Severity int `json:"severity_number,omitempty"`
Body string `json:"body,omitempty"`
Attributes map[string]Attribute `json:"attributes,omitempty"`
}
type Attribute struct {
Value any `json:"value"`
Type string `json:"type"`
}

230
vendor/github.com/getsentry/sentry-go/log.go generated vendored Normal file
View file

@ -0,0 +1,230 @@
package sentry
import (
"context"
"fmt"
"os"
"strings"
"time"
"github.com/getsentry/sentry-go/attribute"
)
type LogLevel string
const (
LogLevelTrace LogLevel = "trace"
LogLevelDebug LogLevel = "debug"
LogLevelInfo LogLevel = "info"
LogLevelWarn LogLevel = "warn"
LogLevelError LogLevel = "error"
LogLevelFatal LogLevel = "fatal"
)
const (
LogSeverityTrace int = 1
LogSeverityDebug int = 5
LogSeverityInfo int = 9
LogSeverityWarning int = 13
LogSeverityError int = 17
LogSeverityFatal int = 21
)
var mapTypesToStr = map[attribute.Type]string{
attribute.INVALID: "",
attribute.BOOL: "boolean",
attribute.INT64: "integer",
attribute.FLOAT64: "double",
attribute.STRING: "string",
}
type sentryLogger struct {
client *Client
attributes map[string]Attribute
}
// NewLogger returns a Logger that emits logs to Sentry. If logging is turned off, all logs get discarded.
func NewLogger(ctx context.Context) Logger {
var hub *Hub
hub = GetHubFromContext(ctx)
if hub == nil {
hub = CurrentHub()
}
client := hub.Client()
if client != nil && client.batchLogger != nil {
return &sentryLogger{client, make(map[string]Attribute)}
}
DebugLogger.Println("fallback to noopLogger: enableLogs disabled")
return &noopLogger{} // fallback: does nothing
}
func (l *sentryLogger) Write(p []byte) (int, error) {
// Avoid sending double newlines to Sentry
msg := strings.TrimRight(string(p), "\n")
l.log(context.Background(), LogLevelInfo, LogSeverityInfo, msg)
return len(p), nil
}
func (l *sentryLogger) log(ctx context.Context, level LogLevel, severity int, message string, args ...interface{}) {
if message == "" {
return
}
hub := GetHubFromContext(ctx)
if hub == nil {
hub = CurrentHub()
}
var traceID TraceID
var spanID SpanID
span := hub.Scope().span
if span != nil {
traceID = span.TraceID
spanID = span.SpanID
} else {
traceID = hub.Scope().propagationContext.TraceID
}
attrs := map[string]Attribute{}
if len(args) > 0 {
attrs["sentry.message.template"] = Attribute{
Value: message, Type: "string",
}
for i, p := range args {
attrs[fmt.Sprintf("sentry.message.parameters.%d", i)] = Attribute{
Value: fmt.Sprint(p), Type: "string",
}
}
}
// If `log` was called with SetAttributes, pass the attributes to attrs
if len(l.attributes) > 0 {
for k, v := range l.attributes {
attrs[k] = v
}
// flush attributes from logger after send
clear(l.attributes)
}
// Set default attributes
if release := l.client.options.Release; release != "" {
attrs["sentry.release"] = Attribute{Value: release, Type: "string"}
}
if environment := l.client.options.Environment; environment != "" {
attrs["sentry.environment"] = Attribute{Value: environment, Type: "string"}
}
if serverName := l.client.options.ServerName; serverName != "" {
attrs["sentry.server.address"] = Attribute{Value: serverName, Type: "string"}
} else if serverAddr, err := os.Hostname(); err == nil {
attrs["sentry.server.address"] = Attribute{Value: serverAddr, Type: "string"}
}
scope := hub.Scope()
if scope != nil {
user := scope.user
if !user.IsEmpty() {
if user.ID != "" {
attrs["user.id"] = Attribute{Value: user.ID, Type: "string"}
}
if user.Name != "" {
attrs["user.name"] = Attribute{Value: user.Name, Type: "string"}
}
if user.Email != "" {
attrs["user.email"] = Attribute{Value: user.Email, Type: "string"}
}
}
}
if spanID.String() != "0000000000000000" {
attrs["sentry.trace.parent_span_id"] = Attribute{Value: spanID.String(), Type: "string"}
}
if sdkIdentifier := l.client.sdkIdentifier; sdkIdentifier != "" {
attrs["sentry.sdk.name"] = Attribute{Value: sdkIdentifier, Type: "string"}
}
if sdkVersion := l.client.sdkVersion; sdkVersion != "" {
attrs["sentry.sdk.version"] = Attribute{Value: sdkVersion, Type: "string"}
}
log := &Log{
Timestamp: time.Now(),
TraceID: traceID,
Level: level,
Severity: severity,
Body: fmt.Sprintf(message, args...),
Attributes: attrs,
}
if l.client.options.BeforeSendLog != nil {
log = l.client.options.BeforeSendLog(log)
}
if log != nil {
l.client.batchLogger.logCh <- *log
}
if l.client.options.Debug {
DebugLogger.Printf(message, args...)
}
}
func (l *sentryLogger) SetAttributes(attrs ...attribute.Builder) {
for _, v := range attrs {
t, ok := mapTypesToStr[v.Value.Type()]
if !ok || t == "" {
DebugLogger.Printf("invalid attribute type set: %v", t)
continue
}
l.attributes[v.Key] = Attribute{
Value: v.Value.AsInterface(),
Type: t,
}
}
}
func (l *sentryLogger) Trace(ctx context.Context, v ...interface{}) {
l.log(ctx, LogLevelTrace, LogSeverityTrace, fmt.Sprint(v...))
}
func (l *sentryLogger) Debug(ctx context.Context, v ...interface{}) {
l.log(ctx, LogLevelDebug, LogSeverityDebug, fmt.Sprint(v...))
}
func (l *sentryLogger) Info(ctx context.Context, v ...interface{}) {
l.log(ctx, LogLevelInfo, LogSeverityInfo, fmt.Sprint(v...))
}
func (l *sentryLogger) Warn(ctx context.Context, v ...interface{}) {
l.log(ctx, LogLevelWarn, LogSeverityWarning, fmt.Sprint(v...))
}
func (l *sentryLogger) Error(ctx context.Context, v ...interface{}) {
l.log(ctx, LogLevelError, LogSeverityError, fmt.Sprint(v...))
}
func (l *sentryLogger) Fatal(ctx context.Context, v ...interface{}) {
l.log(ctx, LogLevelFatal, LogSeverityFatal, fmt.Sprint(v...))
os.Exit(1)
}
func (l *sentryLogger) Panic(ctx context.Context, v ...interface{}) {
l.log(ctx, LogLevelFatal, LogSeverityFatal, fmt.Sprint(v...))
panic(fmt.Sprint(v...))
}
func (l *sentryLogger) Tracef(ctx context.Context, format string, v ...interface{}) {
l.log(ctx, LogLevelTrace, LogSeverityTrace, format, v...)
}
func (l *sentryLogger) Debugf(ctx context.Context, format string, v ...interface{}) {
l.log(ctx, LogLevelDebug, LogSeverityDebug, format, v...)
}
func (l *sentryLogger) Infof(ctx context.Context, format string, v ...interface{}) {
l.log(ctx, LogLevelInfo, LogSeverityInfo, format, v...)
}
func (l *sentryLogger) Warnf(ctx context.Context, format string, v ...interface{}) {
l.log(ctx, LogLevelWarn, LogSeverityWarning, format, v...)
}
func (l *sentryLogger) Errorf(ctx context.Context, format string, v ...interface{}) {
l.log(ctx, LogLevelError, LogSeverityError, format, v...)
}
func (l *sentryLogger) Fatalf(ctx context.Context, format string, v ...interface{}) {
l.log(ctx, LogLevelFatal, LogSeverityFatal, format, v...)
os.Exit(1)
}
func (l *sentryLogger) Panicf(ctx context.Context, format string, v ...interface{}) {
l.log(ctx, LogLevelFatal, LogSeverityFatal, format, v...)
panic(fmt.Sprint(v...))
}

65
vendor/github.com/getsentry/sentry-go/log_fallback.go generated vendored Normal file
View file

@ -0,0 +1,65 @@
package sentry
import (
"context"
"fmt"
"os"
"github.com/getsentry/sentry-go/attribute"
)
// Fallback, no-op logger if logging is disabled.
type noopLogger struct{}
func (*noopLogger) Trace(_ context.Context, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelTrace)
}
func (*noopLogger) Debug(_ context.Context, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelDebug)
}
func (*noopLogger) Info(_ context.Context, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelInfo)
}
func (*noopLogger) Warn(_ context.Context, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelWarn)
}
func (*noopLogger) Error(_ context.Context, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelError)
}
func (*noopLogger) Fatal(_ context.Context, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal)
os.Exit(1)
}
func (*noopLogger) Panic(_ context.Context, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal)
panic(fmt.Sprintf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal))
}
func (*noopLogger) Tracef(_ context.Context, _ string, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelTrace)
}
func (*noopLogger) Debugf(_ context.Context, _ string, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelDebug)
}
func (*noopLogger) Infof(_ context.Context, _ string, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelInfo)
}
func (*noopLogger) Warnf(_ context.Context, _ string, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelWarn)
}
func (*noopLogger) Errorf(_ context.Context, _ string, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelError)
}
func (*noopLogger) Fatalf(_ context.Context, _ string, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal)
os.Exit(1)
}
func (*noopLogger) Panicf(_ context.Context, _ string, _ ...interface{}) {
DebugLogger.Printf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal)
panic(fmt.Sprintf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelFatal))
}
func (*noopLogger) SetAttributes(...attribute.Builder) {
DebugLogger.Printf("No attributes attached. Turn on logging via EnableLogs")
}
func (*noopLogger) Write(_ []byte) (n int, err error) {
return 0, fmt.Errorf("Log with level=[%v] is being dropped. Turn on logging via EnableLogs", LogLevelInfo)
}

View file

@ -1,6 +1,7 @@
package sentry package sentry
import ( import (
"context"
"sync" "sync"
"time" "time"
) )
@ -39,6 +40,7 @@ func (t *MockTransport) SendEvent(event *Event) {
func (t *MockTransport) Flush(_ time.Duration) bool { func (t *MockTransport) Flush(_ time.Duration) bool {
return true return true
} }
func (t *MockTransport) FlushWithContext(_ context.Context) bool { return true }
func (t *MockTransport) Events() []*Event { func (t *MockTransport) Events() []*Event {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()

View file

@ -302,6 +302,11 @@ func (scope *Scope) SetPropagationContext(propagationContext PropagationContext)
scope.propagationContext = propagationContext scope.propagationContext = propagationContext
} }
// GetSpan returns the span from the current scope.
func (scope *Scope) GetSpan() *Span {
return scope.span
}
// SetSpan sets a span for the current scope. // SetSpan sets a span for the current scope.
func (scope *Scope) SetSpan(span *Span) { func (scope *Scope) SetSpan(span *Span) {
scope.mu.Lock() scope.mu.Lock()
@ -464,7 +469,7 @@ func (scope *Scope) ApplyToEvent(event *Event, hint *EventHint, client *Client)
id := event.EventID id := event.EventID
event = processor(event, hint) event = processor(event, hint)
if event == nil { if event == nil {
Logger.Printf("Event dropped by one of the Scope EventProcessors: %s\n", id) DebugLogger.Printf("Event dropped by one of the Scope EventProcessors: %s\n", id)
return nil return nil
} }
} }

View file

@ -6,7 +6,7 @@ import (
) )
// The version of the SDK. // The version of the SDK.
const SDKVersion = "0.32.0" const SDKVersion = "0.34.0"
// apiVersion is the minimum version of the Sentry API compatible with the // apiVersion is the minimum version of the Sentry API compatible with the
// sentry-go SDK. // sentry-go SDK.
@ -125,6 +125,23 @@ func Flush(timeout time.Duration) bool {
return hub.Flush(timeout) return hub.Flush(timeout)
} }
// FlushWithContext waits until the underlying Transport sends any buffered events
// to the Sentry server, blocking for at most the duration specified by the context.
// It returns false if the context is canceled before the events are sent. In such a case,
// some events may not be delivered.
//
// FlushWithContext should be called before terminating the program to ensure no
// events are unintentionally dropped.
//
// Avoid calling FlushWithContext indiscriminately after each call to CaptureEvent,
// CaptureException, or CaptureMessage. To send events synchronously over the network,
// configure the SDK to use HTTPSyncTransport during initialization with Init.
func FlushWithContext(ctx context.Context) bool {
hub := CurrentHub()
return hub.FlushWithContext(ctx)
}
// LastEventID returns an ID of last captured event. // LastEventID returns an ID of last captured event.
func LastEventID() EventID { func LastEventID() EventID {
hub := CurrentHub() hub := CurrentHub()

View file

@ -24,7 +24,7 @@ func (r *spanRecorder) record(s *Span) {
if len(r.spans) >= maxSpans { if len(r.spans) >= maxSpans {
r.overflowOnce.Do(func() { r.overflowOnce.Do(func() {
root := r.spans[0] root := r.spans[0]
Logger.Printf("Too many spans: dropping spans from transaction with TraceID=%s SpanID=%s limit=%d", DebugLogger.Printf("Too many spans: dropping spans from transaction with TraceID=%s SpanID=%s limit=%d",
root.TraceID, root.SpanID, maxSpans) root.TraceID, root.SpanID, maxSpans)
}) })
// TODO(tracing): mark the transaction event in some way to // TODO(tracing): mark the transaction event in some way to

View file

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"net/http" "net/http"
"regexp" "regexp"
"strconv"
"strings" "strings"
"sync" "sync"
"time" "time"
@ -72,6 +73,8 @@ type Span struct { //nolint: maligned // prefer readability over optimal memory
contexts map[string]Context contexts map[string]Context
// a Once instance to make sure that Finish() is only called once. // a Once instance to make sure that Finish() is only called once.
finishOnce sync.Once finishOnce sync.Once
// explicitSampled is a flag for configuring sampling by using `WithSpanSampled` option.
explicitSampled Sampled
} }
// TraceParentContext describes the context of a (remote) parent span. // TraceParentContext describes the context of a (remote) parent span.
@ -195,10 +198,6 @@ func StartSpan(ctx context.Context, operation string, options ...SpanOption) *Sp
clientOptions := span.clientOptions() clientOptions := span.clientOptions()
if clientOptions.EnableTracing { if clientOptions.EnableTracing {
hub := hubFromContext(ctx) hub := hubFromContext(ctx)
if !span.IsTransaction() {
// Push a new scope to stack for non transaction span
hub.PushScope()
}
hub.Scope().SetSpan(&span) hub.Scope().SetSpan(&span)
} }
@ -302,7 +301,7 @@ func (s *Span) GetTransaction() *Span {
// func (s *Span) TransactionName() string // func (s *Span) TransactionName() string
// func (s *Span) SetTransactionName(name string) // func (s *Span) SetTransactionName(name string)
// ToSentryTrace returns the seralized TraceParentContext from a transaction/span. // ToSentryTrace returns the serialized TraceParentContext from a transaction/span.
// Use this function to propagate the TraceParentContext to a downstream SDK, // Use this function to propagate the TraceParentContext to a downstream SDK,
// either as the value of the "sentry-trace" HTTP header, or as an html "sentry-trace" meta tag. // either as the value of the "sentry-trace" HTTP header, or as an html "sentry-trace" meta tag.
func (s *Span) ToSentryTrace() string { func (s *Span) ToSentryTrace() string {
@ -356,8 +355,9 @@ func (s *Span) doFinish() {
hub := hubFromContext(s.ctx) hub := hubFromContext(s.ctx)
if !s.IsTransaction() { if !s.IsTransaction() {
// Referenced to StartSpan function that pushes current non-transaction span to scope stack if s.parent != nil {
defer hub.PopScope() hub.Scope().SetSpan(s.parent)
}
} }
if !s.Sampled.Bool() { if !s.Sampled.Bool() {
@ -449,21 +449,21 @@ func (s *Span) sample() Sampled {
// https://develop.sentry.dev/sdk/performance/#sampling // https://develop.sentry.dev/sdk/performance/#sampling
// #1 tracing is not enabled. // #1 tracing is not enabled.
if !clientOptions.EnableTracing { if !clientOptions.EnableTracing {
Logger.Printf("Dropping transaction: EnableTracing is set to %t", clientOptions.EnableTracing) DebugLogger.Printf("Dropping transaction: EnableTracing is set to %t", clientOptions.EnableTracing)
s.sampleRate = 0.0 s.sampleRate = 0.0
return SampledFalse return SampledFalse
} }
// #2 explicit sampling decision via StartSpan/StartTransaction options. // #2 explicit sampling decision via StartSpan/StartTransaction options.
if s.Sampled != SampledUndefined { if s.explicitSampled != SampledUndefined {
Logger.Printf("Using explicit sampling decision from StartSpan/StartTransaction: %v", s.Sampled) DebugLogger.Printf("Using explicit sampling decision from StartSpan/StartTransaction: %v", s.explicitSampled)
switch s.Sampled { switch s.explicitSampled {
case SampledTrue: case SampledTrue:
s.sampleRate = 1.0 s.sampleRate = 1.0
case SampledFalse: case SampledFalse:
s.sampleRate = 0.0 s.sampleRate = 0.0
} }
return s.Sampled return s.explicitSampled
} }
// Variant for non-transaction spans: they inherit the parent decision. // Variant for non-transaction spans: they inherit the parent decision.
@ -484,42 +484,52 @@ func (s *Span) sample() Sampled {
if sampler != nil { if sampler != nil {
tracesSamplerSampleRate := sampler.Sample(samplingContext) tracesSamplerSampleRate := sampler.Sample(samplingContext)
s.sampleRate = tracesSamplerSampleRate s.sampleRate = tracesSamplerSampleRate
// tracesSampler can update the sample_rate on frozen DSC
if s.dynamicSamplingContext.HasEntries() {
s.dynamicSamplingContext.Entries["sample_rate"] = strconv.FormatFloat(tracesSamplerSampleRate, 'f', -1, 64)
}
if tracesSamplerSampleRate < 0.0 || tracesSamplerSampleRate > 1.0 { if tracesSamplerSampleRate < 0.0 || tracesSamplerSampleRate > 1.0 {
Logger.Printf("Dropping transaction: Returned TracesSampler rate is out of range [0.0, 1.0]: %f", tracesSamplerSampleRate) DebugLogger.Printf("Dropping transaction: Returned TracesSampler rate is out of range [0.0, 1.0]: %f", tracesSamplerSampleRate)
return SampledFalse return SampledFalse
} }
if tracesSamplerSampleRate == 0 { if tracesSamplerSampleRate == 0.0 {
Logger.Printf("Dropping transaction: Returned TracesSampler rate is: %f", tracesSamplerSampleRate) DebugLogger.Printf("Dropping transaction: Returned TracesSampler rate is: %f", tracesSamplerSampleRate)
return SampledFalse return SampledFalse
} }
if rng.Float64() < tracesSamplerSampleRate { if rng.Float64() < tracesSamplerSampleRate {
return SampledTrue return SampledTrue
} }
Logger.Printf("Dropping transaction: TracesSampler returned rate: %f", tracesSamplerSampleRate) DebugLogger.Printf("Dropping transaction: TracesSampler returned rate: %f", tracesSamplerSampleRate)
return SampledFalse return SampledFalse
} }
// #4 inherit parent decision. // #4 inherit parent decision.
if s.parent != nil { if s.Sampled != SampledUndefined {
Logger.Printf("Using sampling decision from parent: %v", s.parent.Sampled) DebugLogger.Printf("Using sampling decision from parent: %v", s.Sampled)
switch s.parent.Sampled { switch s.Sampled {
case SampledTrue: case SampledTrue:
s.sampleRate = 1.0 s.sampleRate = 1.0
case SampledFalse: case SampledFalse:
s.sampleRate = 0.0 s.sampleRate = 0.0
} }
return s.parent.Sampled return s.Sampled
} }
// #5 use TracesSampleRate from ClientOptions. // #5 use TracesSampleRate from ClientOptions.
sampleRate := clientOptions.TracesSampleRate sampleRate := clientOptions.TracesSampleRate
s.sampleRate = sampleRate s.sampleRate = sampleRate
// tracesSampleRate can update the sample_rate on frozen DSC
if s.dynamicSamplingContext.HasEntries() {
s.dynamicSamplingContext.Entries["sample_rate"] = strconv.FormatFloat(sampleRate, 'f', -1, 64)
}
if sampleRate < 0.0 || sampleRate > 1.0 { if sampleRate < 0.0 || sampleRate > 1.0 {
Logger.Printf("Dropping transaction: TracesSamplerRate out of range [0.0, 1.0]: %f", sampleRate) DebugLogger.Printf("Dropping transaction: TracesSampleRate out of range [0.0, 1.0]: %f", sampleRate)
return SampledFalse return SampledFalse
} }
if sampleRate == 0.0 { if sampleRate == 0.0 {
Logger.Printf("Dropping transaction: TracesSampleRate rate is: %f", sampleRate) DebugLogger.Printf("Dropping transaction: TracesSampleRate rate is: %f", sampleRate)
return SampledFalse return SampledFalse
} }
@ -542,7 +552,7 @@ func (s *Span) toEvent() *Event {
finished := make([]*Span, 0, len(children)) finished := make([]*Span, 0, len(children))
for _, child := range children { for _, child := range children {
if child.EndTime.IsZero() { if child.EndTime.IsZero() {
Logger.Printf("Dropped unfinished span: Op=%q TraceID=%s SpanID=%s", child.Op, child.TraceID, child.SpanID) DebugLogger.Printf("Dropped unfinished span: Op=%q TraceID=%s SpanID=%s", child.Op, child.TraceID, child.SpanID)
continue continue
} }
finished = append(finished, child) finished = append(finished, child)
@ -892,7 +902,7 @@ func WithTransactionSource(source TransactionSource) SpanOption {
// WithSpanSampled updates the sampling flag for a given span. // WithSpanSampled updates the sampling flag for a given span.
func WithSpanSampled(sampled Sampled) SpanOption { func WithSpanSampled(sampled Sampled) SpanOption {
return func(s *Span) { return func(s *Span) {
s.Sampled = sampled s.explicitSampled = sampled
} }
} }
@ -903,7 +913,7 @@ func WithSpanOrigin(origin SpanOrigin) SpanOption {
} }
} }
// Continue a trace based on traceparent and bagge values. // ContinueTrace continues a trace based on traceparent and baggage values.
// If the SDK is configured with tracing enabled, // If the SDK is configured with tracing enabled,
// this function returns populated SpanOption. // this function returns populated SpanOption.
// In any other cases, it populates the propagation context on the scope. // In any other cases, it populates the propagation context on the scope.
@ -932,20 +942,21 @@ func ContinueFromHeaders(trace, baggage string) SpanOption {
return func(s *Span) { return func(s *Span) {
if trace != "" { if trace != "" {
s.updateFromSentryTrace([]byte(trace)) s.updateFromSentryTrace([]byte(trace))
}
if baggage != "" { if baggage != "" {
s.updateFromBaggage([]byte(baggage)) s.updateFromBaggage([]byte(baggage))
} }
// In case a sentry-trace header is present but there are no sentry-related // In case a sentry-trace header is present but there are no sentry-related
// values in the baggage, create an empty, frozen DynamicSamplingContext. // values in the baggage, create an empty, frozen DynamicSamplingContext.
if trace != "" && !s.dynamicSamplingContext.HasEntries() { if !s.dynamicSamplingContext.HasEntries() {
s.dynamicSamplingContext = DynamicSamplingContext{ s.dynamicSamplingContext = DynamicSamplingContext{
Frozen: true, Frozen: true,
} }
} }
} }
} }
}
// ContinueFromTrace returns a span option that updates the span to continue // ContinueFromTrace returns a span option that updates the span to continue
// an existing TraceID. // an existing TraceID.

View file

@ -16,8 +16,10 @@ import (
"github.com/getsentry/sentry-go/internal/ratelimit" "github.com/getsentry/sentry-go/internal/ratelimit"
) )
const defaultBufferSize = 30 const (
const defaultTimeout = time.Second * 30 defaultBufferSize = 1000
defaultTimeout = time.Second * 30
)
// maxDrainResponseBytes is the maximum number of bytes that transport // maxDrainResponseBytes is the maximum number of bytes that transport
// implementations will read from response bodies when draining them. // implementations will read from response bodies when draining them.
@ -33,6 +35,7 @@ const maxDrainResponseBytes = 16 << 10
// Transport is used by the Client to deliver events to remote server. // Transport is used by the Client to deliver events to remote server.
type Transport interface { type Transport interface {
Flush(timeout time.Duration) bool Flush(timeout time.Duration) bool
FlushWithContext(ctx context.Context) bool
Configure(options ClientOptions) Configure(options ClientOptions)
SendEvent(event *Event) SendEvent(event *Event)
Close() Close()
@ -84,14 +87,14 @@ func getRequestBodyFromEvent(event *Event) []byte {
} }
body, err = json.Marshal(event) body, err = json.Marshal(event)
if err == nil { if err == nil {
Logger.Println(msg) DebugLogger.Println(msg)
return body return body
} }
// This should _only_ happen when Event.Exception[0].Stacktrace.Frames[0].Vars is unserializable // This should _only_ happen when Event.Exception[0].Stacktrace.Frames[0].Vars is unserializable
// Which won't ever happen, as we don't use it now (although it's the part of public interface accepted by Sentry) // Which won't ever happen, as we don't use it now (although it's the part of public interface accepted by Sentry)
// Juuust in case something, somehow goes utterly wrong. // Juuust in case something, somehow goes utterly wrong.
Logger.Println("Event couldn't be marshaled, even with stripped contextual data. Skipping delivery. " + DebugLogger.Println("Event couldn't be marshaled, even with stripped contextual data. Skipping delivery. " +
"Please notify the SDK owners with possibly broken payload.") "Please notify the SDK owners with possibly broken payload.")
return nil return nil
} }
@ -144,6 +147,23 @@ func encodeEnvelopeItem(enc *json.Encoder, itemType string, body json.RawMessage
return err return err
} }
func encodeEnvelopeLogs(enc *json.Encoder, itemsLength int, body json.RawMessage) error {
err := enc.Encode(
struct {
Type string `json:"type"`
ItemCount int `json:"item_count"`
ContentType string `json:"content_type"`
}{
Type: logEvent.Type,
ItemCount: itemsLength,
ContentType: logEvent.ContentType,
})
if err == nil {
err = enc.Encode(body)
}
return err
}
func envelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMessage) (*bytes.Buffer, error) { func envelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMessage) (*bytes.Buffer, error) {
var b bytes.Buffer var b bytes.Buffer
enc := json.NewEncoder(&b) enc := json.NewEncoder(&b)
@ -180,6 +200,8 @@ func envelopeFromBody(event *Event, dsn *Dsn, sentAt time.Time, body json.RawMes
switch event.Type { switch event.Type {
case transactionType, checkInType: case transactionType, checkInType:
err = encodeEnvelopeItem(enc, event.Type, body) err = encodeEnvelopeItem(enc, event.Type, body)
case logEvent.Type:
err = encodeEnvelopeLogs(enc, len(event.Logs), body)
default: default:
err = encodeEnvelopeItem(enc, eventType, body) err = encodeEnvelopeItem(enc, eventType, body)
} }
@ -309,7 +331,7 @@ func NewHTTPTransport() *HTTPTransport {
func (t *HTTPTransport) Configure(options ClientOptions) { func (t *HTTPTransport) Configure(options ClientOptions) {
dsn, err := NewDsn(options.Dsn) dsn, err := NewDsn(options.Dsn)
if err != nil { if err != nil {
Logger.Printf("%v\n", err) DebugLogger.Printf("%v\n", err)
return return
} }
t.dsn = dsn t.dsn = dsn
@ -393,7 +415,7 @@ func (t *HTTPTransport) SendEventWithContext(ctx context.Context, event *Event)
} else { } else {
eventType = fmt.Sprintf("%s event", event.Level) eventType = fmt.Sprintf("%s event", event.Level)
} }
Logger.Printf( DebugLogger.Printf(
"Sending %s [%s] to %s project: %s", "Sending %s [%s] to %s project: %s",
eventType, eventType,
event.EventID, event.EventID,
@ -401,7 +423,7 @@ func (t *HTTPTransport) SendEventWithContext(ctx context.Context, event *Event)
t.dsn.projectID, t.dsn.projectID,
) )
default: default:
Logger.Println("Event dropped due to transport buffer being full.") DebugLogger.Println("Event dropped due to transport buffer being full.")
} }
t.buffer <- b t.buffer <- b
@ -418,8 +440,19 @@ func (t *HTTPTransport) SendEventWithContext(ctx context.Context, event *Event)
// have the SDK send events over the network synchronously, configure it to use // have the SDK send events over the network synchronously, configure it to use
// the HTTPSyncTransport in the call to Init. // the HTTPSyncTransport in the call to Init.
func (t *HTTPTransport) Flush(timeout time.Duration) bool { func (t *HTTPTransport) Flush(timeout time.Duration) bool {
toolate := time.After(timeout) timeoutCh := make(chan struct{})
time.AfterFunc(timeout, func() {
close(timeoutCh)
})
return t.flushInternal(timeoutCh)
}
// FlushWithContext works like Flush, but it accepts a context.Context instead of a timeout.
func (t *HTTPTransport) FlushWithContext(ctx context.Context) bool {
return t.flushInternal(ctx.Done())
}
func (t *HTTPTransport) flushInternal(timeout <-chan struct{}) bool {
// Wait until processing the current batch has started or the timeout. // Wait until processing the current batch has started or the timeout.
// //
// We must wait until the worker has seen the current batch, because it is // We must wait until the worker has seen the current batch, because it is
@ -427,6 +460,7 @@ func (t *HTTPTransport) Flush(timeout time.Duration) bool {
// possible execution flow in which b.done is never closed, and the only way // possible execution flow in which b.done is never closed, and the only way
// out of Flush would be waiting for the timeout, which is undesired. // out of Flush would be waiting for the timeout, which is undesired.
var b batch var b batch
for { for {
select { select {
case b = <-t.buffer: case b = <-t.buffer:
@ -436,7 +470,7 @@ func (t *HTTPTransport) Flush(timeout time.Duration) bool {
default: default:
t.buffer <- b t.buffer <- b
} }
case <-toolate: case <-timeout:
goto fail goto fail
} }
} }
@ -455,14 +489,14 @@ started:
// Wait until the current batch is done or the timeout. // Wait until the current batch is done or the timeout.
select { select {
case <-b.done: case <-b.done:
Logger.Println("Buffer flushed successfully.") DebugLogger.Println("Buffer flushed successfully.")
return true return true
case <-toolate: case <-timeout:
goto fail goto fail
} }
fail: fail:
Logger.Println("Buffer flushing reached the timeout.") DebugLogger.Println("Buffer flushing was canceled or timed out.")
return false return false
} }
@ -500,15 +534,15 @@ func (t *HTTPTransport) worker() {
response, err := t.client.Do(item.request) response, err := t.client.Do(item.request)
if err != nil { if err != nil {
Logger.Printf("There was an issue with sending an event: %v", err) DebugLogger.Printf("There was an issue with sending an event: %v", err)
continue continue
} }
if response.StatusCode >= 400 && response.StatusCode <= 599 { if response.StatusCode >= 400 && response.StatusCode <= 599 {
b, err := io.ReadAll(response.Body) b, err := io.ReadAll(response.Body)
if err != nil { if err != nil {
Logger.Printf("Error while reading response code: %v", err) DebugLogger.Printf("Error while reading response code: %v", err)
} }
Logger.Printf("Sending %s failed with the following error: %s", eventType, string(b)) DebugLogger.Printf("Sending %s failed with the following error: %s", eventType, string(b))
} }
t.mu.Lock() t.mu.Lock()
@ -535,7 +569,7 @@ func (t *HTTPTransport) disabled(c ratelimit.Category) bool {
defer t.mu.RUnlock() defer t.mu.RUnlock()
disabled := t.limits.IsRateLimited(c) disabled := t.limits.IsRateLimited(c)
if disabled { if disabled {
Logger.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c)) DebugLogger.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c))
} }
return disabled return disabled
} }
@ -581,7 +615,7 @@ func NewHTTPSyncTransport() *HTTPSyncTransport {
func (t *HTTPSyncTransport) Configure(options ClientOptions) { func (t *HTTPSyncTransport) Configure(options ClientOptions) {
dsn, err := NewDsn(options.Dsn) dsn, err := NewDsn(options.Dsn)
if err != nil { if err != nil {
Logger.Printf("%v\n", err) DebugLogger.Printf("%v\n", err)
return return
} }
t.dsn = dsn t.dsn = dsn
@ -627,16 +661,18 @@ func (t *HTTPSyncTransport) SendEventWithContext(ctx context.Context, event *Eve
return return
} }
var eventType string var eventIdentifier string
switch { switch event.Type {
case event.Type == transactionType: case transactionType:
eventType = "transaction" eventIdentifier = "transaction"
case logEvent.Type:
eventIdentifier = fmt.Sprintf("%v log events", len(event.Logs))
default: default:
eventType = fmt.Sprintf("%s event", event.Level) eventIdentifier = fmt.Sprintf("%s event", event.Level)
} }
Logger.Printf( DebugLogger.Printf(
"Sending %s [%s] to %s project: %s", "Sending %s [%s] to %s project: %s",
eventType, eventIdentifier,
event.EventID, event.EventID,
t.dsn.host, t.dsn.host,
t.dsn.projectID, t.dsn.projectID,
@ -644,15 +680,15 @@ func (t *HTTPSyncTransport) SendEventWithContext(ctx context.Context, event *Eve
response, err := t.client.Do(request) response, err := t.client.Do(request)
if err != nil { if err != nil {
Logger.Printf("There was an issue with sending an event: %v", err) DebugLogger.Printf("There was an issue with sending an event: %v", err)
return return
} }
if response.StatusCode >= 400 && response.StatusCode <= 599 { if response.StatusCode >= 400 && response.StatusCode <= 599 {
b, err := io.ReadAll(response.Body) b, err := io.ReadAll(response.Body)
if err != nil { if err != nil {
Logger.Printf("Error while reading response code: %v", err) DebugLogger.Printf("Error while reading response code: %v", err)
} }
Logger.Printf("Sending %s failed with the following error: %s", eventType, string(b)) DebugLogger.Printf("Sending %s failed with the following error: %s", eventIdentifier, string(b))
} }
t.mu.Lock() t.mu.Lock()
@ -674,12 +710,17 @@ func (t *HTTPSyncTransport) Flush(_ time.Duration) bool {
return true return true
} }
// FlushWithContext is a no-op for HTTPSyncTransport. It always returns true immediately.
func (t *HTTPSyncTransport) FlushWithContext(_ context.Context) bool {
return true
}
func (t *HTTPSyncTransport) disabled(c ratelimit.Category) bool { func (t *HTTPSyncTransport) disabled(c ratelimit.Category) bool {
t.mu.Lock() t.mu.Lock()
defer t.mu.Unlock() defer t.mu.Unlock()
disabled := t.limits.IsRateLimited(c) disabled := t.limits.IsRateLimited(c)
if disabled { if disabled {
Logger.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c)) DebugLogger.Printf("Too many requests for %q, backing off till: %v", c, t.limits.Deadline(c))
} }
return disabled return disabled
} }
@ -695,15 +736,19 @@ type noopTransport struct{}
var _ Transport = noopTransport{} var _ Transport = noopTransport{}
func (noopTransport) Configure(ClientOptions) { func (noopTransport) Configure(ClientOptions) {
Logger.Println("Sentry client initialized with an empty DSN. Using noopTransport. No events will be delivered.") DebugLogger.Println("Sentry client initialized with an empty DSN. Using noopTransport. No events will be delivered.")
} }
func (noopTransport) SendEvent(*Event) { func (noopTransport) SendEvent(*Event) {
Logger.Println("Event dropped due to noopTransport usage.") DebugLogger.Println("Event dropped due to noopTransport usage.")
} }
func (noopTransport) Flush(time.Duration) bool { func (noopTransport) Flush(time.Duration) bool {
return true return true
} }
func (noopTransport) FlushWithContext(context.Context) bool {
return true
}
func (noopTransport) Close() {} func (noopTransport) Close() {}

View file

@ -36,7 +36,7 @@ func monotonicTimeSince(start time.Time) (end time.Time) {
return start.Add(time.Since(start)) return start.Add(time.Since(start))
} }
// nolint: deadcode, unused // nolint: unused
func prettyPrint(data interface{}) { func prettyPrint(data interface{}) {
dbg, _ := json.MarshalIndent(data, "", " ") dbg, _ := json.MarshalIndent(data, "", " ")
fmt.Println(string(dbg)) fmt.Println(string(dbg))
@ -62,7 +62,7 @@ func defaultRelease() (release string) {
} }
for _, e := range envs { for _, e := range envs {
if release = os.Getenv(e); release != "" { if release = os.Getenv(e); release != "" {
Logger.Printf("Using release from environment variable %s: %s", e, release) DebugLogger.Printf("Using release from environment variable %s: %s", e, release)
return release return release
} }
} }
@ -89,23 +89,23 @@ func defaultRelease() (release string) {
if err, ok := err.(*exec.ExitError); ok && len(err.Stderr) > 0 { if err, ok := err.(*exec.ExitError); ok && len(err.Stderr) > 0 {
fmt.Fprintf(&s, ": %s", err.Stderr) fmt.Fprintf(&s, ": %s", err.Stderr)
} }
Logger.Print(s.String()) DebugLogger.Print(s.String())
} else { } else {
release = strings.TrimSpace(string(b)) release = strings.TrimSpace(string(b))
Logger.Printf("Using release from Git: %s", release) DebugLogger.Printf("Using release from Git: %s", release)
return release return release
} }
} }
Logger.Print("Some Sentry features will not be available. See https://docs.sentry.io/product/releases/.") DebugLogger.Print("Some Sentry features will not be available. See https://docs.sentry.io/product/releases/.")
Logger.Print("To stop seeing this message, pass a Release to sentry.Init or set the SENTRY_RELEASE environment variable.") DebugLogger.Print("To stop seeing this message, pass a Release to sentry.Init or set the SENTRY_RELEASE environment variable.")
return "" return ""
} }
func revisionFromBuildInfo(info *debug.BuildInfo) string { func revisionFromBuildInfo(info *debug.BuildInfo) string {
for _, setting := range info.Settings { for _, setting := range info.Settings {
if setting.Key == "vcs.revision" && setting.Value != "" { if setting.Key == "vcs.revision" && setting.Value != "" {
Logger.Printf("Using release from debug info: %s", setting.Value) DebugLogger.Printf("Using release from debug info: %s", setting.Value)
return setting.Value return setting.Value
} }
} }

View file

@ -22,3 +22,5 @@ _testmain.go
*.exe *.exe
*.test *.test
*.prof *.prof
.direnv
.envrc

View file

@ -1,11 +1,14 @@
# html2text # html2text
[![Documentation](https://godoc.org/github.com/jaytaylor/html2text?status.svg)](https://godoc.org/github.com/jaytaylor/html2text) [![Go Reference](https://pkg.go.dev/badge/github.com/inbucket/html2text.svg)](https://pkg.go.dev/github.com/inbucket/html2text)
[![Build Status](https://travis-ci.org/jaytaylor/html2text.svg?branch=master)](https://travis-ci.org/jaytaylor/html2text) [![Build and Test](https://github.com/inbucket/html2text/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/inbucket/html2text/actions/workflows/build-and-test.yml)
[![Report Card](https://goreportcard.com/badge/github.com/jaytaylor/html2text)](https://goreportcard.com/report/github.com/jaytaylor/html2text) [![Report Card](https://goreportcard.com/badge/github.com/inbucket/html2text)](https://goreportcard.com/report/github.com/inbucket/html2text)
### Converts HTML into text of the markdown-flavored variety ### Converts HTML into text of the markdown-flavored variety
This is a permanent fork of the original
[jaytaylor.com/html2text](https://github.com/jaytaylor/html2text) package.
## Introduction ## Introduction
Ensure your emails are readable by all! Ensure your emails are readable by all!
@ -21,7 +24,7 @@ It requires go 1.x or newer ;)
## Download the package ## Download the package
```bash ```bash
go get jaytaylor.com/html2text go get github.com/inbucket/html2text
``` ```
## Example usage ## Example usage
@ -34,7 +37,7 @@ package main
import ( import (
"fmt" "fmt"
"jaytaylor.com/html2text" "github.com/inbucket/html2text"
) )
func main() { func main() {
@ -127,17 +130,3 @@ go test
# License # License
Permissive MIT license. Permissive MIT license.
## Contact
You are more than welcome to open issues and send pull requests if you find a bug or want a new feature.
If you appreciate this library please feel free to drop me a line and tell me! It's always nice to hear from people who have benefitted from my work.
Email: jay at (my github username).com
Twitter: [@jtaylor](https://twitter.com/jtaylor)
# Alternatives
https://github.com/k3a/html2text - Lightweight

View file

@ -5,11 +5,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1701680307, "lastModified": 1731533236,
"narHash": "sha256-kAuep2h5ajznlPMD9rnQyffWG8EM/C73lejGofXvdM8=", "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "4022d587cbbfd70fe950c1e2083a02621806a725", "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -20,11 +20,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1702272962, "lastModified": 1749369410,
"narHash": "sha256-D+zHwkwPc6oYQ4G3A1HuadopqRwUY/JkMwHz1YF7j4Q=", "narHash": "sha256-P1eYm8bewiyWg3QaPCHrOp6iWg/7ESi/aGHT4yilyNo=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "e97b3e4186bcadf0ef1b6be22b8558eab1cdeb5d", "rev": "8207ad0d501dd3590e0e81a7c56b386a5c4342c9",
"type": "github" "type": "github"
}, },
"original": { "original": {

17
vendor/github.com/inbucket/html2text/flake.nix generated vendored Normal file
View file

@ -0,0 +1,17 @@
{
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { nixpkgs, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem
(system:
let
pkgs = nixpkgs.legacyPackages.${system};
in
{
devShell = pkgs.callPackage ./shell.nix { };
}
);
}

View file

@ -21,48 +21,6 @@ type Options struct {
TextOnly bool // Returns only plain text TextOnly bool // Returns only plain text
} }
// PrettyTablesOptions overrides tablewriter behaviors
type PrettyTablesOptions struct {
AutoFormatHeader bool
AutoWrapText bool
ReflowDuringAutoWrap bool
ColWidth int
ColumnSeparator string
RowSeparator string
CenterSeparator string
HeaderAlignment int
FooterAlignment int
Alignment int
ColumnAlignment []int
NewLine string
HeaderLine bool
RowLine bool
AutoMergeCells bool
Borders tablewriter.Border
}
// NewPrettyTablesOptions creates PrettyTablesOptions with default settings
func NewPrettyTablesOptions() *PrettyTablesOptions {
return &PrettyTablesOptions{
AutoFormatHeader: true,
AutoWrapText: true,
ReflowDuringAutoWrap: true,
ColWidth: tablewriter.MAX_ROW_WIDTH,
ColumnSeparator: tablewriter.COLUMN,
RowSeparator: tablewriter.ROW,
CenterSeparator: tablewriter.CENTER,
HeaderAlignment: tablewriter.ALIGN_DEFAULT,
FooterAlignment: tablewriter.ALIGN_DEFAULT,
Alignment: tablewriter.ALIGN_DEFAULT,
ColumnAlignment: []int{},
NewLine: tablewriter.NEWLINE,
HeaderLine: true,
RowLine: false,
AutoMergeCells: false,
Borders: tablewriter.Border{Left: true, Right: true, Bottom: true, Top: true},
}
}
// FromHTMLNode renders text output from a pre-parsed HTML document. // FromHTMLNode renders text output from a pre-parsed HTML document.
func FromHTMLNode(doc *html.Node, o ...Options) (string, error) { func FromHTMLNode(doc *html.Node, o ...Options) (string, error) {
var options Options var options Options
@ -70,6 +28,11 @@ func FromHTMLNode(doc *html.Node, o ...Options) (string, error) {
options = o[0] options = o[0]
} }
if options.PrettyTables && options.PrettyTablesOptions == nil {
// defaults need to make explicit as they are no longer identical with tablewriter
options.PrettyTablesOptions = NewPrettyTablesOptions()
}
ctx := textifyTraverseContext{ ctx := textifyTraverseContext{
buf: bytes.Buffer{}, buf: bytes.Buffer{},
options: options, options: options,
@ -333,31 +296,18 @@ func (ctx *textifyTraverseContext) handleTableElement(node *html.Node) error {
buf := &bytes.Buffer{} buf := &bytes.Buffer{}
table := tablewriter.NewWriter(buf) table := tablewriter.NewWriter(buf)
if ctx.options.PrettyTablesOptions != nil { ctx.options.PrettyTablesOptions.configureTable(table)
options := ctx.options.PrettyTablesOptions
table.SetAutoFormatHeaders(options.AutoFormatHeader) table.Header(ctx.tableCtx.header)
table.SetAutoWrapText(options.AutoWrapText) table.Footer(ctx.tableCtx.footer)
table.SetReflowDuringAutoWrap(options.ReflowDuringAutoWrap) if err := table.Bulk(ctx.tableCtx.body); err != nil {
table.SetColWidth(options.ColWidth) return err
table.SetColumnSeparator(options.ColumnSeparator)
table.SetRowSeparator(options.RowSeparator)
table.SetCenterSeparator(options.CenterSeparator)
table.SetHeaderAlignment(options.HeaderAlignment)
table.SetFooterAlignment(options.FooterAlignment)
table.SetAlignment(options.Alignment)
table.SetColumnAlignment(options.ColumnAlignment)
table.SetNewLine(options.NewLine)
table.SetHeaderLine(options.HeaderLine)
table.SetRowLine(options.RowLine)
table.SetAutoMergeCells(options.AutoMergeCells)
table.SetBorders(options.Borders)
} }
table.SetHeader(ctx.tableCtx.header)
table.SetFooter(ctx.tableCtx.footer)
table.AppendBulk(ctx.tableCtx.body)
// Render the table using ASCII. // Render the table using ASCII.
table.Render() if err := table.Render(); err != nil {
return err
}
if err := ctx.emit(buf.String()); err != nil { if err := ctx.emit(buf.String()); err != nil {
return err return err
} }

212
vendor/github.com/inbucket/html2text/prettytables.go generated vendored Normal file
View file

@ -0,0 +1,212 @@
package html2text
import (
"github.com/olekukonko/tablewriter"
"github.com/olekukonko/tablewriter/tw"
)
// PrettyTablesOptions overrides tablewriter behaviors
type PrettyTablesOptions struct {
AutoFormatHeader bool
AutoWrapText bool
// Deprecated. Tablewriter always assumes this to be `true`
ReflowDuringAutoWrap bool
ColWidth int
ColumnSeparator string
RowSeparator string
CenterSeparator string
HeaderAlignment tw.Align
FooterAlignment tw.Align
Alignment tw.Align
ColumnAlignment tw.Alignment
// Deprecated. Tablewriter always assumes this to be `\n`
NewLine string
HeaderLine bool
RowLine bool
AutoMergeCells bool
Borders Border
// Configuration allows to directly manipulate the `Table` with all what [tablewriter] offers.
// Setting this ignores all the rest of the settings of this struct.
Configuration func(table *tablewriter.Table)
}
// NewPrettyTablesOptions creates PrettyTablesOptions with default settings
func NewPrettyTablesOptions() *PrettyTablesOptions {
return &PrettyTablesOptions{
AutoFormatHeader: true,
AutoWrapText: true,
ColWidth: 32, // old tablewriter.MAX_ROW_WIDTH + borders now count into width
ColumnSeparator: defaultBorderStyle.ColumnSeparator,
RowSeparator: defaultBorderStyle.RowSeparator,
CenterSeparator: defaultBorderStyle.CenterSeparator,
HeaderAlignment: tw.AlignCenter,
FooterAlignment: tw.AlignCenter,
Alignment: tw.AlignDefault,
ColumnAlignment: make(tw.Alignment, 0),
HeaderLine: true,
RowLine: false,
AutoMergeCells: false,
Borders: Border{Left: true, Right: true, Bottom: true, Top: true},
}
}
func (p *PrettyTablesOptions) configureTable(table *tablewriter.Table) {
if p.Configuration != nil {
p.Configuration(table)
return
}
cfg := tablewriter.NewConfigBuilder()
cfg.WithHeaderAutoFormat(asState(p.AutoFormatHeader)).WithFooterAutoFormat(asState(p.AutoFormatHeader)).
WithRowAutoWrap(p.wrapMode()).WithHeaderAutoWrap(p.wrapMode()).WithFooterAutoWrap(p.wrapMode()).
WithRowMaxWidth(p.ColWidth).
WithHeaderAlignment(p.HeaderAlignment).
WithFooterAlignment(p.FooterAlignment).
WithRowAlignment(p.Alignment).
WithRowMergeMode(p.mergeMode())
if len(p.ColumnAlignment) > 0 {
cfg.Row().Alignment().WithPerColumn(p.ColumnAlignment)
}
rendition := tw.Rendition{
Borders: p.Borders.withStates(),
Symbols: p.borderStyle(),
Settings: p.renderSettings(),
}
table.Options(
tablewriter.WithConfig(cfg.Build()),
tablewriter.WithRendition(rendition))
}
func (p *PrettyTablesOptions) wrapMode() int {
if p.AutoWrapText {
return tw.WrapNormal
} else {
return tw.WrapNone
}
}
func (p *PrettyTablesOptions) mergeMode() int {
if p.AutoMergeCells {
return tw.MergeVertical
} else {
return tw.MergeNone
}
}
func (p *PrettyTablesOptions) renderSettings() tw.Settings {
return tw.Settings{
Lines: tw.Lines{
ShowHeaderLine: asState(p.HeaderLine),
},
Separators: tw.Separators{
BetweenRows: asState(p.RowLine),
},
}
}
// Border controls tablewriter borders. It uses simple bools instead of tablewriters `State`
type Border struct {
Left, Right, Bottom, Top bool
}
func (b Border) withStates() tw.Border {
return tw.Border{
Left: asState(b.Left),
Right: asState(b.Right),
Bottom: asState(b.Bottom),
Top: asState(b.Top),
}
}
type BorderStyle struct {
ColumnSeparator string
RowSeparator string
CenterSeparator string
}
func (b BorderStyle) Name() string {
return "html2text"
}
func (b BorderStyle) Center() string {
return b.CenterSeparator
}
func (b BorderStyle) Row() string {
return b.RowSeparator
}
func (b BorderStyle) Column() string {
return b.ColumnSeparator
}
func (b BorderStyle) TopLeft() string {
return b.CenterSeparator
}
func (b BorderStyle) TopMid() string {
return b.CenterSeparator
}
func (b BorderStyle) TopRight() string {
return b.CenterSeparator
}
func (b BorderStyle) MidLeft() string {
return b.CenterSeparator
}
func (b BorderStyle) MidRight() string {
return b.CenterSeparator
}
func (b BorderStyle) BottomLeft() string {
return b.CenterSeparator
}
func (b BorderStyle) BottomMid() string {
return b.CenterSeparator
}
func (b BorderStyle) BottomRight() string {
return b.CenterSeparator
}
func (b BorderStyle) HeaderLeft() string {
return b.CenterSeparator
}
func (b BorderStyle) HeaderMid() string {
return b.CenterSeparator
}
func (b BorderStyle) HeaderRight() string {
return b.CenterSeparator
}
var defaultBorderStyle = BorderStyle{
ColumnSeparator: "|",
RowSeparator: "-",
CenterSeparator: "+",
}
func (p *PrettyTablesOptions) borderStyle() BorderStyle {
return BorderStyle{
ColumnSeparator: p.ColumnSeparator,
RowSeparator: p.RowSeparator,
CenterSeparator: p.CenterSeparator,
}
}
func asState(b bool) tw.State {
// TableWriter does not provide this by default :(
if b {
return tw.On
} else {
return tw.Off
}
}

View file

@ -2,8 +2,8 @@
pkgs.mkShell { pkgs.mkShell {
buildInputs = with pkgs; [ buildInputs = with pkgs; [
delve delve
go_1_20 go
golint golangci-lint
gopls gopls
]; ];

View file

@ -1,13 +0,0 @@
language: go
go:
# n.b. For golang release history, see https://golang.org/doc/devel/release.html
- tip
- "1.13.8"
- "1.12.17"
- "1.11.13"
- "1.10.8"
- "1.9.7"
notifications:
email:
on_success: change
on_failure: always

View file

@ -1,5 +1,4 @@
run: version: "2"
timeout: 5m
linters: linters:
enable: enable:
- asasalint - asasalint
@ -8,32 +7,19 @@ linters:
- bodyclose - bodyclose
- containedctx - containedctx
- contextcheck - contextcheck
- copyloopvar
- decorder - decorder
# - dupl
# - dupword
- durationcheck - durationcheck
- errchkjson - errchkjson
- errname - errname
# - errorlint
- execinquery
# - exhaustive
- exportloopref
# - forcetypeassert
- ginkgolinter - ginkgolinter
- gocheckcompilerdirectives - gocheckcompilerdirectives
# - gochecknoinits
- gochecksumtype - gochecksumtype
- gocritic - gocritic
# - godot
# - goerr113
- gofmt
# - gofumpt
- goheader - goheader
- goimports
- gomoddirectives - gomoddirectives
- gomodguard - gomodguard
- goprintffuncname - goprintffuncname
# - gosec
- gosmopolitan - gosmopolitan
- grouper - grouper
- importas - importas
@ -45,8 +31,6 @@ linters:
- misspell - misspell
- musttag - musttag
- nilerr - nilerr
# - nilnil
# - nlreturn
- noctx - noctx
- nolintlint - nolintlint
- nosprintfhostport - nosprintfhostport
@ -56,20 +40,44 @@ linters:
- promlinter - promlinter
- protogetter - protogetter
- reassign - reassign
# - revive - revive
- rowserrcheck - rowserrcheck
- sloglint - sloglint
- stylecheck - staticcheck
- tagalign - tagalign
- tagliatelle - tagliatelle
- tenv
- testableexamples - testableexamples
- testifylint - testifylint
- thelper - thelper
- tparallel - tparallel
# - unconvert
- unparam - unparam
- usestdlibvars - usestdlibvars
- wastedassign - wastedassign
- whitespace - whitespace
- zerologlint - zerologlint
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
paths:
- third_party$
- builtin$
- examples$
rules:
# Exclude specific linters from test files.
- path: _test\.go
linters:
- gosmopolitan
formatters:
enable:
- gofmt
- goimports
exclusions:
generated: lax
paths:
- third_party$
- builtin$
- examples$

View file

@ -27,8 +27,6 @@ simplify:
lint: lint:
@test -z "$(shell gofmt -l . | tee /dev/stderr)" || echo "[WARN] Fix formatting issues with 'make fmt'" @test -z "$(shell gofmt -l . | tee /dev/stderr)" || echo "[WARN] Fix formatting issues with 'make fmt'"
@# TODO: lint disabled until `mediatype.MediaTypeParseOptions` renamed to remove repetition.
@#@golint -set_exit_status $(PKGS)
@go vet $(PKGS) @go vet $(PKGS)
reflex: reflex:

View file

@ -180,7 +180,7 @@ func (b *boundaryReader) Next() (bool, error) {
_, _ = io.Copy(io.Discard, b) _, _ = io.Copy(io.Discard, b)
} }
for { for {
var line []byte = nil var line []byte
var err error var err error
for { for {
// Read whole line, handle extra long lines in cycle // Read whole line, handle extra long lines in cycle

View file

@ -13,7 +13,7 @@ import (
"reflect" "reflect"
"time" "time"
"github.com/jhillyerd/enmime/internal/stringutil" "github.com/jhillyerd/enmime/v2/internal/stringutil"
) )
// MailBuilder facilitates the easy construction of a MIME message. Each manipulation method // MailBuilder facilitates the easy construction of a MIME message. Each manipulation method

View file

@ -3,7 +3,7 @@ package enmime
import ( import (
"strings" "strings"
inttp "github.com/jhillyerd/enmime/internal/textproto" inttp "github.com/jhillyerd/enmime/v2/internal/textproto"
) )
// detectMultipartMessage returns true if the message has a recognized multipart Content-Type header // detectMultipartMessage returns true if the message has a recognized multipart Content-Type header

View file

@ -8,10 +8,11 @@ import (
"mime/quotedprintable" "mime/quotedprintable"
"net/textproto" "net/textproto"
"sort" "sort"
"strings"
"time" "time"
"github.com/jhillyerd/enmime/internal/coding" "github.com/jhillyerd/enmime/v2/internal/coding"
"github.com/jhillyerd/enmime/internal/stringutil" "github.com/jhillyerd/enmime/v2/internal/stringutil"
) )
// b64Percent determines the percent of non-ASCII characters enmime will tolerate before switching // b64Percent determines the percent of non-ASCII characters enmime will tolerate before switching
@ -22,6 +23,7 @@ type transferEncoding byte
const ( const (
te7Bit transferEncoding = iota te7Bit transferEncoding = iota
te8Bit
teQuoted teQuoted
teBase64 teBase64
teRaw teRaw
@ -106,21 +108,33 @@ func (p *Part) setupMIMEHeaders() transferEncoding {
cte := te7Bit cte := te7Bit
if len(p.Content) > 0 { if len(p.Content) > 0 {
if strings.Index(strings.ToLower(p.ContentType), "message/") == 0 {
// RFC 1341: `message` types must have no encoding other than "7bit", "8bit", or
// "binary". The message header fields are always US-ASCII in any case, and data within
// the body can still be encoded, in which case the Content-Transfer-Encoding header
// field in the encapsulated message will reflect this.
cte = te8Bit
} else {
cte = teBase64 cte = teBase64
if p.TextContent() && p.ContentReader == nil { if p.TextContent() && p.ContentReader == nil {
cte = selectTransferEncoding(p.Content, false) cte = p.selectTransferEncoding(p.Content, false)
if p.Charset == "" { if p.Charset == "" {
p.Charset = utf8 p.Charset = utf8
} }
} }
}
// RFC 2045: 7bit is assumed if CTE header not present. // RFC 2045: 7bit is assumed if CTE header not present.
switch cte { switch cte {
case te8Bit:
p.Header.Set(hnContentEncoding, cte8Bit)
case teBase64: case teBase64:
p.Header.Set(hnContentEncoding, cteBase64) p.Header.Set(hnContentEncoding, cteBase64)
case teQuoted: case teQuoted:
p.Header.Set(hnContentEncoding, cteQuotedPrintable) p.Header.Set(hnContentEncoding, cteQuotedPrintable)
} }
} }
// Setup headers. // Setup headers.
if p.FirstChild != nil && p.Boundary == "" { if p.FirstChild != nil && p.Boundary == "" {
// Multipart, generate random boundary marker. // Multipart, generate random boundary marker.
@ -130,12 +144,13 @@ func (p *Part) setupMIMEHeaders() transferEncoding {
p.Header.Set(hnContentID, coding.ToIDHeader(p.ContentID)) p.Header.Set(hnContentID, coding.ToIDHeader(p.ContentID))
} }
fileName := p.FileName fileName := p.FileName
switch selectTransferEncoding([]byte(p.FileName), true) { switch p.selectTransferEncoding([]byte(p.FileName), true) {
case teBase64: case teBase64:
fileName = mime.BEncoding.Encode(utf8, p.FileName) fileName = mime.BEncoding.Encode(utf8, p.FileName)
case teQuoted: case teQuoted:
fileName = mime.QEncoding.Encode(utf8, p.FileName) fileName = mime.QEncoding.Encode(utf8, p.FileName)
} }
if p.ContentType != "" { if p.ContentType != "" {
// Build content type header. // Build content type header.
param := make(map[string]string) param := make(map[string]string)
@ -150,6 +165,7 @@ func (p *Part) setupMIMEHeaders() transferEncoding {
} }
p.Header.Set(hnContentType, p.ContentType) p.Header.Set(hnContentType, p.ContentType)
} }
if p.Disposition != "" { if p.Disposition != "" {
// Build disposition header. // Build disposition header.
param := make(map[string]string) param := make(map[string]string)
@ -162,6 +178,7 @@ func (p *Part) setupMIMEHeaders() transferEncoding {
} }
p.Header.Set(hnContentDisposition, p.Disposition) p.Header.Set(hnContentDisposition, p.Disposition)
} }
return cte return cte
} }
@ -178,7 +195,7 @@ func (p *Part) encodeHeader(b *bufio.Writer) error {
for _, v := range p.Header[k] { for _, v := range p.Header[k] {
encv := v encv := v
if !rawContent { if !rawContent {
switch selectTransferEncoding([]byte(v), true) { switch p.selectTransferEncoding([]byte(v), true) {
case teBase64: case teBase64:
encv = mime.BEncoding.Encode(utf8, v) encv = mime.BEncoding.Encode(utf8, v)
case teQuoted: case teQuoted:
@ -286,10 +303,15 @@ func (p *Part) encodeContentFromReader(b *bufio.Writer) error {
} }
// selectTransferEncoding scans content for non-ASCII characters and selects 'b' or 'q' encoding. // selectTransferEncoding scans content for non-ASCII characters and selects 'b' or 'q' encoding.
func selectTransferEncoding(content []byte, quoteLineBreaks bool) transferEncoding { func (p *Part) selectTransferEncoding(content []byte, quoteLineBreaks bool) transferEncoding {
if len(content) == 0 { if len(content) == 0 {
return te7Bit return te7Bit
} }
if p.encoder != nil && p.encoder.forceQuotedPrintableCteOption {
return teQuoted
}
// Binary chars remaining before we choose b64 encoding. // Binary chars remaining before we choose b64 encoding.
threshold := b64Percent * len(content) / 100 threshold := b64Percent * len(content) / 100
bincount := 0 bincount := 0

View file

@ -0,0 +1,33 @@
package enmime
type EncoderOption interface {
apply(p *Encoder)
}
// Encoder implements MIME part encoding options
type Encoder struct {
forceQuotedPrintableCteOption bool
}
// ForceQuotedPrintableCte forces "quoted-printable" transfer encoding when selecting Content Transfer Encoding, preventing the use of base64.
func ForceQuotedPrintableCte(b bool) EncoderOption {
return forceQuotedPrintableCteOption(b)
}
type forceQuotedPrintableCteOption bool
func (o forceQuotedPrintableCteOption) apply(p *Encoder) {
p.forceQuotedPrintableCteOption = bool(o)
}
func NewEncoder(ops ...EncoderOption) *Encoder {
e := Encoder{
forceQuotedPrintableCteOption: false,
}
for _, o := range ops {
o.apply(&e)
}
return &e
}

View file

@ -51,7 +51,7 @@
// multi-gigabyte attachments. // multi-gigabyte attachments.
// //
// enmime is open source software released under the MIT License. The latest version can be found // enmime is open source software released under the MIT License. The latest version can be found
// at https://github.com/jhillyerd/enmime // at https://github.com/jhillyerd/enmime/v2
// //
// [1]: https://github.com/jaytaylor/html2text // [1]: https://github.com/inbucket/html2text
package enmime // import "github.com/jhillyerd/enmime" package enmime // import "github.com/jhillyerd/enmime/v2"

View file

@ -9,9 +9,9 @@ import (
"strings" "strings"
"time" "time"
"github.com/jaytaylor/html2text" "github.com/inbucket/html2text"
"github.com/jhillyerd/enmime/internal/coding" "github.com/jhillyerd/enmime/v2/internal/coding"
inttp "github.com/jhillyerd/enmime/internal/textproto" inttp "github.com/jhillyerd/enmime/v2/internal/textproto"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -198,16 +198,14 @@ func (p Parser) EnvelopeFromPart(root *Part) (*Envelope, error) {
// Down-convert HTML to text if necessary // Down-convert HTML to text if necessary
if e.Text == "" && e.HTML != "" { if e.Text == "" && e.HTML != "" {
// We always warn when this happens // We always warn when this happens
e.Root.addWarning( e.Root.addWarning(ErrorPlainTextFromHTML, "Message did not contain a text/plain part")
ErrorPlainTextFromHTML,
"Message did not contain a text/plain part")
if !p.disableTextConversion { if !p.disableTextConversion {
var err error var err error
if e.Text, err = html2text.FromString(e.HTML); err != nil { if e.Text, err = html2text.FromString(e.HTML); err != nil {
e.Text = "" // Down-conversion shouldn't fail e.Text = "" // Down-conversion shouldn't fail
p := e.Root.BreadthMatchFirst(matchHTMLBodyPart) p := e.Root.BreadthMatchFirst(matchHTMLBodyPart)
p.addError(ErrorPlainTextFromHTML, "Failed to downconvert HTML: %v", err) p.addErrorf(ErrorPlainTextFromHTML, "Failed to downconvert HTML: %v", err)
} }
} }
} }

View file

@ -27,12 +27,6 @@ const (
ErrorMalformedChildPart = "Malformed child part" ErrorMalformedChildPart = "Malformed child part"
) )
// MaxPartErrors limits number of part parsing errors, errors after the limit are ignored.
// 0 means unlimited.
//
// Deprecated: This limit may be set via the `MaxStoredPartErrors` Parser option.
var MaxPartErrors = 0
// Error describes an error encountered while parsing. // Error describes an error encountered while parsing.
type Error struct { type Error struct {
Name string // The name or type of error encountered, from Error consts. Name string // The name or type of error encountered, from Error consts.
@ -49,13 +43,13 @@ func (e *Error) Error() string {
return fmt.Sprintf("[%s] %s: %s", sev, e.Name, e.Detail) return fmt.Sprintf("[%s] %s: %s", sev, e.Name, e.Detail)
} }
// String formats the enmime.Error as a string. DEPRECATED; use Error() instead. // addError builds a severe Error and appends to the Part error slice.
func (e *Error) String() string { func (p *Part) addError(name string, detail string) {
return e.Error() p.addProblem(&Error{name, detail, true})
} }
// addWarning builds a severe Error and appends to the Part error slice. // addErrorf builds a severe Error and appends to the Part error slice.
func (p *Part) addError(name string, detailFmt string, args ...interface{}) { func (p *Part) addErrorf(name string, detailFmt string, args ...interface{}) {
p.addProblem(&Error{ p.addProblem(&Error{
name, name,
fmt.Sprintf(detailFmt, args...), fmt.Sprintf(detailFmt, args...),
@ -64,7 +58,12 @@ func (p *Part) addError(name string, detailFmt string, args ...interface{}) {
} }
// addWarning builds a non-severe Error and appends to the Part error slice. // addWarning builds a non-severe Error and appends to the Part error slice.
func (p *Part) addWarning(name string, detailFmt string, args ...interface{}) { func (p *Part) addWarning(name string, detail string) {
p.addProblem(&Error{name, detail, false})
}
// addWarningf builds a non-severe Error and appends to the Part error slice.
func (p *Part) addWarningf(name string, detailFmt string, args ...interface{}) {
p.addProblem(&Error{ p.addProblem(&Error{
name, name,
fmt.Sprintf(detailFmt, args...), fmt.Sprintf(detailFmt, args...),
@ -74,10 +73,10 @@ func (p *Part) addWarning(name string, detailFmt string, args ...interface{}) {
// addProblem adds general *Error to the Part error slice. // addProblem adds general *Error to the Part error slice.
func (p *Part) addProblem(err *Error) { func (p *Part) addProblem(err *Error) {
maxErrors := MaxPartErrors maxErrors := 0
if p.parser != nil && p.parser.maxStoredPartErrors != nil { if p.parser != nil {
// Override global var. // Override global var.
maxErrors = *p.parser.maxStoredPartErrors maxErrors = p.parser.maxStoredPartErrors
} }
if (maxErrors == 0) || (len(p.Errors) < maxErrors) { if (maxErrors == 0) || (len(p.Errors) < maxErrors) {
@ -97,9 +96,9 @@ type partErrorCollector struct {
} }
func (p *partErrorCollector) AddError(name string, detailFmt string, args ...any) { func (p *partErrorCollector) AddError(name string, detailFmt string, args ...any) {
p.part.addError(name, detailFmt, args...) p.part.addErrorf(name, detailFmt, args...)
} }
func (p *partErrorCollector) AddWarning(name string, detailFmt string, args ...any) { func (p *partErrorCollector) AddWarning(name string, detailFmt string, args ...any) {
p.part.addWarning(name, detailFmt, args...) p.part.addWarningf(name, detailFmt, args...)
} }

61
vendor/github.com/jhillyerd/enmime/v2/flake.lock generated vendored Normal file
View file

@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1736657626,
"narHash": "sha256-FWlPMUzp0lkQBdhKlPqtQdqmp+/C+1MBiEytaYfrCTY=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2f9e2f85cb14a46410a1399aa9ea7ecf433e422e",
"type": "github"
},
"original": {
"owner": "nixos",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View file

@ -9,10 +9,9 @@ import (
"net/textproto" "net/textproto"
"strings" "strings"
"github.com/jhillyerd/enmime/internal/coding" "github.com/jhillyerd/enmime/v2/internal/coding"
"github.com/jhillyerd/enmime/internal/stringutil" "github.com/jhillyerd/enmime/v2/internal/stringutil"
inttp "github.com/jhillyerd/enmime/internal/textproto" inttp "github.com/jhillyerd/enmime/v2/internal/textproto"
"github.com/jhillyerd/enmime/mediatype"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -101,26 +100,6 @@ func ParseAddressList(list string) ([]*mail.Address, error) {
return ret, nil return ret, nil
} }
// Terminology from RFC 2047:
// encoded-word: the entire =?charset?encoding?encoded-text?= string
// charset: the character set portion of the encoded word
// encoding: the character encoding type used for the encoded-text
// encoded-text: the text we are decoding
// ParseMediaType is a more tolerant implementation of Go's mime.ParseMediaType function.
//
// Tolerances accounted for:
// - Missing ';' between content-type and media parameters
// - Repeating media parameters
// - Unquoted values in media parameters containing 'tspecials' characters
//
// Deprecated: Use mediaType.Parse instead
func ParseMediaType(ctype string) (mtype string, params map[string]string, invalidParams []string,
err error) {
// Export of internal function.
return mediatype.Parse(ctype)
}
// ReadHeader reads a block of SMTP or MIME headers and returns a // ReadHeader reads a block of SMTP or MIME headers and returns a
// textproto.MIMEHeader. Header parse warnings & errors will be added to // textproto.MIMEHeader. Header parse warnings & errors will be added to
// ErrorCollector, io errors will be returned directly. // ErrorCollector, io errors will be returned directly.

View file

@ -5,8 +5,8 @@ import (
"bytes" "bytes"
"io" "io"
"github.com/jhillyerd/enmime/internal/coding" "github.com/jhillyerd/enmime/v2/internal/coding"
"github.com/jhillyerd/enmime/internal/textproto" "github.com/jhillyerd/enmime/v2/internal/textproto"
"github.com/pkg/errors" "github.com/pkg/errors"
) )

View file

@ -34,6 +34,7 @@ func EnsureCommaDelimitedAddresses(s string) string {
inQuotes := false inQuotes := false
inDomain := false inDomain := false
escapeSequence := false escapeSequence := false
inAngles := false
sb := strings.Builder{} sb := strings.Builder{}
for i, r := range s { for i, r := range s {
if escapeSequence { if escapeSequence {
@ -46,6 +47,19 @@ func EnsureCommaDelimitedAddresses(s string) string {
sb.WriteRune(r) sb.WriteRune(r)
continue continue
} }
if r == '<' {
inAngles = true
sb.WriteRune(r)
continue
}
if r == '>' {
inAngles = false
sb.WriteRune(r)
continue
}
if inQuotes { if inQuotes {
if r == '\\' { if r == '\\' {
escapeSequence = true escapeSequence = true
@ -74,13 +88,17 @@ func EnsureCommaDelimitedAddresses(s string) string {
sb.WriteRune(r) sb.WriteRune(r)
continue continue
} }
if r == ' ' { if r == ' ' && !inAngles {
inDomain = false inDomain = false
sb.WriteRune(',') sb.WriteRune(',')
sb.WriteRune(r) sb.WriteRune(r)
continue continue
} }
} }
if inAngles && r == ' ' {
continue
}
} }
sb.WriteRune(r) sb.WriteRune(r)
} }

View file

@ -1,13 +1,13 @@
package stringutil package stringutil
// Wrap builds a byte slice from strs, wrapping on word boundaries before max chars // Wrap builds a byte slice from strs, wrapping on word boundaries before maxLen chars
func Wrap(max int, strs ...string) []byte { func Wrap(maxLen int, strs ...string) []byte {
input := make([]byte, 0) input := make([]byte, 0)
output := make([]byte, 0) output := make([]byte, 0)
for _, s := range strs { for _, s := range strs {
input = append(input, []byte(s)...) input = append(input, []byte(s)...)
} }
if len(input) < max { if len(input) < maxLen {
// Doesn't need to be wrapped // Doesn't need to be wrapped
return input return input
} }
@ -20,7 +20,7 @@ func Wrap(max int, strs ...string) []byte {
case ' ', '\t': case ' ', '\t':
ls = i ls = i
} }
if ll >= max { if ll >= maxLen {
if ls >= 0 { if ls >= 0 {
output = append(output, input[lw+1:ls]...) output = append(output, input[lw+1:ls]...)
output = append(output, '\r', '\n', ' ') output = append(output, '\r', '\n', ' ')

View file

@ -6,8 +6,8 @@ import (
"strings" "strings"
_utf8 "unicode/utf8" _utf8 "unicode/utf8"
"github.com/jhillyerd/enmime/internal/coding" "github.com/jhillyerd/enmime/v2/internal/coding"
"github.com/jhillyerd/enmime/internal/stringutil" "github.com/jhillyerd/enmime/v2/internal/stringutil"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
@ -30,8 +30,8 @@ const (
utf8 = "utf-8" utf8 = "utf-8"
) )
// MediaTypeParseOptions controls the parsing of content-type and media-type strings. // ParseOptions controls the parsing of content-type and media-type strings.
type MediaTypeParseOptions struct { type ParseOptions struct {
StripMediaTypeInvalidCharacters bool StripMediaTypeInvalidCharacters bool
} }
@ -43,11 +43,11 @@ type MediaTypeParseOptions struct {
// - Unquoted values in media parameters containing 'tspecials' characters // - Unquoted values in media parameters containing 'tspecials' characters
// - Newline characters // - Newline characters
func Parse(ctype string) (mtype string, params map[string]string, invalidParams []string, err error) { func Parse(ctype string) (mtype string, params map[string]string, invalidParams []string, err error) {
return ParseWithOptions(ctype, MediaTypeParseOptions{}) return ParseWithOptions(ctype, ParseOptions{})
} }
// ParseWithOptions parses media-type with additional options controlling the parsing behavior. // ParseWithOptions parses media-type with additional options controlling the parsing behavior.
func ParseWithOptions(ctype string, options MediaTypeParseOptions) (mtype string, params map[string]string, invalidParams []string, err error) { func ParseWithOptions(ctype string, options ParseOptions) (mtype string, params map[string]string, invalidParams []string, err error) {
mtype, params, err = mime.ParseMediaType( mtype, params, err = mime.ParseMediaType(
fixNewlines(fixUnescapedQuotes(fixUnquotedSpecials(fixMangledMediaType(removeTrailingHTMLTags(ctype), ';', options))))) fixNewlines(fixUnescapedQuotes(fixUnquotedSpecials(fixMangledMediaType(removeTrailingHTMLTags(ctype), ';', options)))))
if err != nil { if err != nil {
@ -73,7 +73,7 @@ func ParseWithOptions(ctype string, options MediaTypeParseOptions) (mtype string
// fixMangledMediaType is used to insert ; separators into media type strings that lack them, and // fixMangledMediaType is used to insert ; separators into media type strings that lack them, and
// remove repeated parameters. // remove repeated parameters.
func fixMangledMediaType(mtype string, sep rune, options MediaTypeParseOptions) string { func fixMangledMediaType(mtype string, sep rune, options ParseOptions) string {
strsep := string([]rune{sep}) strsep := string([]rune{sep})
if mtype == "" { if mtype == "" {
return "" return ""

View file

@ -48,8 +48,7 @@ func MaxStoredPartErrors(n int) Option {
type maxStoredPartErrorsOption int type maxStoredPartErrorsOption int
func (o maxStoredPartErrorsOption) apply(p *Parser) { func (o maxStoredPartErrorsOption) apply(p *Parser) {
max := int(o) p.maxStoredPartErrors = int(o)
p.maxStoredPartErrors = &max
} }
// RawContent if set to true will not try to decode the CTE and return the raw part content. // RawContent if set to true will not try to decode the CTE and return the raw part content.
@ -112,3 +111,16 @@ func (o disableCharacterDetectionOption) apply(p *Parser) {
func DisableCharacterDetection(disableCharacterDetection bool) Option { func DisableCharacterDetection(disableCharacterDetection bool) Option {
return disableCharacterDetectionOption(disableCharacterDetection) return disableCharacterDetectionOption(disableCharacterDetection)
} }
type minCharsetDetectRunesOption int
func (o minCharsetDetectRunesOption) apply(p *Parser) {
p.minCharsetDetectRunes = int(o)
}
// MinCharsetDetectRunes sets the minimum length of a MIME part before enmime will attempt to
// detect its character set. The shorter the text, the more likely an incorrect character set
// will be chosen. The default is 100.
func MinCharsetDetectRunes(minCharsetDetectRunes int) Option {
return minCharsetDetectRunesOption(minCharsetDetectRunes)
}

View file

@ -16,10 +16,9 @@ func AllowCorruptTextPartErrorPolicy(p *Part, err error) bool {
// CustomParseMediaType parses media type. See ParseMediaType for more details // CustomParseMediaType parses media type. See ParseMediaType for more details
type CustomParseMediaType func(ctype string) (mtype string, params map[string]string, invalidParams []string, err error) type CustomParseMediaType func(ctype string) (mtype string, params map[string]string, invalidParams []string, err error)
// Parser parses MIME. // Parser parses MIME. Create with NewParser to inherit recommended defaults.
// Default parser is a valid one.
type Parser struct { type Parser struct {
maxStoredPartErrors *int // TODO: Pointer until global var removed. maxStoredPartErrors int
multipartWOBoundaryAsSinglePart bool multipartWOBoundaryAsSinglePart bool
readPartErrorPolicy ReadPartErrorPolicy readPartErrorPolicy ReadPartErrorPolicy
skipMalformedParts bool skipMalformedParts bool
@ -28,14 +27,18 @@ type Parser struct {
stripMediaTypeInvalidCharacters bool stripMediaTypeInvalidCharacters bool
disableTextConversion bool disableTextConversion bool
disableCharacterDetection bool disableCharacterDetection bool
minCharsetDetectRunes int
} }
// defaultParser is a Parser with default configuration. // defaultParser is a Parser with default configuration.
var defaultParser = Parser{} var defaultParser = *NewParser()
// NewParser creates new parser with given options. // NewParser creates new parser with given options.
func NewParser(ops ...Option) *Parser { func NewParser(ops ...Option) *Parser {
p := Parser{} // Construct parser with default options.
p := Parser{
minCharsetDetectRunes: 100,
}
for _, o := range ops { for _, o := range ops {
o.apply(&p) o.apply(&p)

View file

@ -13,15 +13,14 @@ import (
"time" "time"
"github.com/gogs/chardet" "github.com/gogs/chardet"
"github.com/jhillyerd/enmime/internal/coding" "github.com/jhillyerd/enmime/v2/internal/coding"
inttp "github.com/jhillyerd/enmime/internal/textproto" inttp "github.com/jhillyerd/enmime/v2/internal/textproto"
"github.com/jhillyerd/enmime/mediatype" "github.com/jhillyerd/enmime/v2/mediatype"
"github.com/pkg/errors" "github.com/pkg/errors"
) )
const ( const (
minCharsetConfidence = 85 minCharsetConfidence = 85
minCharsetRuneLength = 100
) )
// Part represents a node in the MIME multipart tree. The Content-Type, Disposition and File Name // Part represents a node in the MIME multipart tree. The Content-Type, Disposition and File Name
@ -51,6 +50,8 @@ type Part struct {
parser *Parser // Provides access to parsing options. parser *Parser // Provides access to parsing options.
randSource rand.Source // optional rand for uuid boundary generation randSource rand.Source // optional rand for uuid boundary generation
encoder *Encoder // provides encoding options
} }
// NewPart creates a new Part object. // NewPart creates a new Part object.
@ -130,7 +131,7 @@ func (p *Part) setupHeaders(r *bufio.Reader, defaultContentType string) error {
return err return err
} }
for i := range minvalidParams { for i := range minvalidParams {
p.addWarning( p.addWarningf(
ErrorMalformedHeader, ErrorMalformedHeader,
"Content-Type header has malformed parameter %q", "Content-Type header has malformed parameter %q",
minvalidParams[i]) minvalidParams[i])
@ -172,7 +173,7 @@ func (p *Part) readPartContent(r io.Reader, readPartErrorPolicy ReadPartErrorPol
buf, err := io.ReadAll(r) buf, err := io.ReadAll(r)
if err != nil { if err != nil {
if readPartErrorPolicy != nil && readPartErrorPolicy(p, err) { if readPartErrorPolicy != nil && readPartErrorPolicy(p, err) {
p.addWarning(ErrorMalformedChildPart, "partial content: %s", err.Error()) p.addWarningf(ErrorMalformedChildPart, "partial content: %s", err.Error())
return buf, nil return buf, nil
} }
return nil, err return nil, err
@ -199,26 +200,30 @@ func (p *Part) convertFromDetectedCharset(r io.Reader, readPartErrorPolicy ReadP
return nil, errors.WithStack(err) return nil, errors.WithStack(err)
} }
// Restore r.
r = bytes.NewReader(buf)
if p.parser.disableCharacterDetection && p.Charset != "" {
// Charset detection is disabled, use declared charset.
return p.convertFromStatedCharset(r), nil
}
cs, err := cd.DetectBest(buf) cs, err := cd.DetectBest(buf)
switch err { switch err {
case nil: case nil:
// Carry on // Carry on
default: default:
p.addWarning(ErrorCharsetDeclaration, "charset could not be detected: %v", err) p.addWarningf(ErrorCharsetDeclaration, "charset could not be detected: %v", err)
} }
// Restore r. if cs == nil || cs.Confidence < minCharsetConfidence || len(bytes.Runes(buf)) < p.parser.minCharsetDetectRunes {
r = bytes.NewReader(buf)
if (p.parser.disableCharacterDetection && p.Charset != "") ||
(cs == nil || cs.Confidence < minCharsetConfidence || len(bytes.Runes(buf)) < minCharsetRuneLength) {
// Low confidence or not enough characters, use declared character set. // Low confidence or not enough characters, use declared character set.
return p.convertFromStatedCharset(r), nil return p.convertFromStatedCharset(r), nil
} }
// Confidence exceeded our threshold, use detected character set. // Confidence exceeded our threshold, use detected character set.
if p.Charset != "" && !strings.EqualFold(cs.Charset, p.Charset) { if p.Charset != "" && !strings.EqualFold(cs.Charset, p.Charset) {
p.addWarning(ErrorCharsetDeclaration, p.addWarningf(ErrorCharsetDeclaration,
"declared charset %q, detected %q, confidence %d", "declared charset %q, detected %q, confidence %d",
p.Charset, cs.Charset, cs.Confidence) p.Charset, cs.Charset, cs.Confidence)
} }
@ -244,7 +249,7 @@ func (p *Part) convertFromStatedCharset(r io.Reader) io.Reader {
reader, err := coding.NewCharsetReader(p.Charset, r) reader, err := coding.NewCharsetReader(p.Charset, r)
if err != nil { if err != nil {
// Failed to get a conversion reader. // Failed to get a conversion reader.
p.addWarning(ErrorCharsetConversion, "failed to get reader for charset %q: %v", p.Charset, err) p.addWarningf(ErrorCharsetConversion, "failed to get reader for charset %q: %v", p.Charset, err)
} else { } else {
return reader return reader
} }
@ -258,7 +263,7 @@ func (p *Part) convertFromStatedCharset(r io.Reader) io.Reader {
reader, err = coding.NewCharsetReader(p.Charset, r) reader, err = coding.NewCharsetReader(p.Charset, r)
if err != nil { if err != nil {
// Failed to get a conversion reader. // Failed to get a conversion reader.
p.addWarning(ErrorCharsetConversion, "failed to get reader for charset %q: %v", p.Charset, err) p.addWarningf(ErrorCharsetConversion, "failed to get reader for charset %q: %v", p.Charset, err)
} else { } else {
return reader return reader
} }
@ -294,7 +299,7 @@ func (p *Part) decodeContent(r io.Reader, readPartErrorPolicy ReadPartErrorPolic
default: default:
// Unknown encoding. // Unknown encoding.
validEncoding = false validEncoding = false
p.addWarning( p.addWarningf(
ErrorContentEncoding, ErrorContentEncoding,
"Unrecognized Content-Transfer-Encoding type %q", "Unrecognized Content-Transfer-Encoding type %q",
encoding) encoding)
@ -321,7 +326,7 @@ func (p *Part) decodeContent(r io.Reader, readPartErrorPolicy ReadPartErrorPolic
} }
// Set empty content-type error. // Set empty content-type error.
if p.ContentType == "" { if p.ContentType == "" {
p.addWarning( p.addWarningf(
ErrorMissingContentType, "content-type is empty for part id: %s", p.PartID) ErrorMissingContentType, "content-type is empty for part id: %s", p.PartID)
} }
return nil return nil
@ -330,7 +335,7 @@ func (p *Part) decodeContent(r io.Reader, readPartErrorPolicy ReadPartErrorPolic
// parses media type using custom or default media type parser // parses media type using custom or default media type parser
func (p *Part) parseMediaType(ctype string) (mtype string, params map[string]string, invalidParams []string, err error) { func (p *Part) parseMediaType(ctype string) (mtype string, params map[string]string, invalidParams []string, err error) {
if p.parser == nil || p.parser.customParseMediaType == nil { if p.parser == nil || p.parser.customParseMediaType == nil {
return mediatype.ParseWithOptions(ctype, mediatype.MediaTypeParseOptions{StripMediaTypeInvalidCharacters: p.parser.stripMediaTypeInvalidCharacters}) return mediatype.ParseWithOptions(ctype, mediatype.ParseOptions{StripMediaTypeInvalidCharacters: p.parser.stripMediaTypeInvalidCharacters})
} }
return p.parser.customParseMediaType(ctype) return p.parser.customParseMediaType(ctype)
@ -426,7 +431,7 @@ func parseParts(parent *Part, reader *bufio.Reader) error {
return err return err
} }
if br.unbounded { if br.unbounded {
parent.addWarning(ErrorMissingBoundary, "Boundary %q was not closed correctly", parent.addWarningf(ErrorMissingBoundary, "Boundary %q was not closed correctly",
parent.Boundary) parent.Boundary)
} }
if !next { if !next {
@ -445,7 +450,7 @@ func parseParts(parent *Part, reader *bufio.Reader) error {
bbr := bufio.NewReader(br) bbr := bufio.NewReader(br)
if err = p.setupHeaders(bbr, ""); err != nil { if err = p.setupHeaders(bbr, ""); err != nil {
if p.parser.skipMalformedParts { if p.parser.skipMalformedParts {
parent.addError(ErrorMalformedChildPart, "read header: %s", err.Error()) parent.addErrorf(ErrorMalformedChildPart, "read header: %s", err.Error())
continue continue
} }
@ -457,7 +462,7 @@ func parseParts(parent *Part, reader *bufio.Reader) error {
// Content is text or data, decode it. // Content is text or data, decode it.
if err = p.decodeContent(bbr, p.parser.readPartErrorPolicy); err != nil { if err = p.decodeContent(bbr, p.parser.readPartErrorPolicy); err != nil {
if p.parser.skipMalformedParts { if p.parser.skipMalformedParts {
parent.addError(ErrorMalformedChildPart, "decode content: %s", err.Error()) parent.addErrorf(ErrorMalformedChildPart, "decode content: %s", err.Error())
continue continue
} }
return err return err
@ -470,7 +475,7 @@ func parseParts(parent *Part, reader *bufio.Reader) error {
// Content is another multipart. // Content is another multipart.
if err = parseParts(p, bbr); err != nil { if err = parseParts(p, bbr); err != nil {
if p.parser.skipMalformedParts { if p.parser.skipMalformedParts {
parent.addError(ErrorMalformedChildPart, "parse parts: %s", err.Error()) parent.addErrorf(ErrorMalformedChildPart, "parse parts: %s", err.Error())
continue continue
} }
return err return err
@ -491,3 +496,10 @@ func parseParts(parent *Part, reader *bufio.Reader) error {
} }
return nil return nil
} }
func (p *Part) WithEncoder(e *Encoder) *Part {
if e != nil {
p.encoder = e
}
return p
}

11
vendor/github.com/jhillyerd/enmime/v2/shell.nix generated vendored Normal file
View file

@ -0,0 +1,11 @@
{ pkgs ? import <nixpkgs> { } }:
pkgs.mkShell {
buildInputs = with pkgs; [
delve
go
golangci-lint
gopls
];
hardeningDisable = [ "fortify" ];
}

3
vendor/github.com/olekukonko/errors/.gitignore generated vendored Normal file
View file

@ -0,0 +1,3 @@
# Created by .ignore support plugin (hsz.mobi)
.idea/
tmp/

21
vendor/github.com/olekukonko/errors/LICENSE generated vendored Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Oleku Konko
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

1565
vendor/github.com/olekukonko/errors/README.md generated vendored Normal file

File diff suppressed because it is too large Load diff

610
vendor/github.com/olekukonko/errors/chain.go generated vendored Normal file
View file

@ -0,0 +1,610 @@
package errors
import (
"context"
"fmt"
"log/slog" // Standard structured logging package
"reflect"
"strings"
"time"
)
// Chain executes functions sequentially with enhanced error handling.
// Logging is optional and configured via a slog.Handler.
type Chain struct {
steps []chainStep // List of steps to execute
errors []error // Accumulated errors during execution
config chainConfig // Chain-wide configuration
lastStep *chainStep // Pointer to the last added step for configuration
logHandler slog.Handler // Optional logging handler (nil means no logging)
cancel context.CancelFunc // Function to cancel the context
}
// chainStep represents a single step in the chain.
type chainStep struct {
execute func() error // Function to execute for this step
optional bool // If true, errors don't stop the chain
config stepConfig // Step-specific configuration
}
// chainConfig holds chain-wide settings.
type chainConfig struct {
timeout time.Duration // Maximum duration for the entire chain
maxErrors int // Maximum number of errors before stopping (-1 for unlimited)
autoWrap bool // Whether to automatically wrap errors with additional context
}
// stepConfig holds configuration for an individual step.
type stepConfig struct {
context map[string]interface{} // Arbitrary key-value pairs for context
category ErrorCategory // Category for error classification
code int // Numeric error code
retry *Retry // Retry policy for the step
logOnFail bool // Whether to log errors automatically
metricsLabel string // Label for metrics (not used in this code)
logAttrs []slog.Attr // Additional attributes for logging
}
// ChainOption defines a function that configures a Chain.
type ChainOption func(*Chain)
// NewChain creates a new Chain with the given options.
// Logging is disabled by default (logHandler is nil).
func NewChain(opts ...ChainOption) *Chain {
c := &Chain{
config: chainConfig{
autoWrap: true, // Enable error wrapping by default
maxErrors: -1, // No limit on errors by default
},
// logHandler is nil, meaning no logging unless explicitly configured
}
// Apply each configuration option
for _, opt := range opts {
opt(c)
}
return c
}
// ChainWithLogHandler sets a custom slog.Handler for logging.
// If handler is nil, logging is effectively disabled.
func ChainWithLogHandler(handler slog.Handler) ChainOption {
return func(c *Chain) {
c.logHandler = handler
}
}
// ChainWithTimeout sets a timeout for the entire chain.
func ChainWithTimeout(d time.Duration) ChainOption {
return func(c *Chain) {
c.config.timeout = d
}
}
// ChainWithMaxErrors sets the maximum number of errors allowed.
// A value <= 0 means no limit.
func ChainWithMaxErrors(max int) ChainOption {
return func(c *Chain) {
if max <= 0 {
c.config.maxErrors = -1 // No limit
} else {
c.config.maxErrors = max
}
}
}
// ChainWithAutoWrap enables or disables automatic error wrapping.
func ChainWithAutoWrap(auto bool) ChainOption {
return func(c *Chain) {
c.config.autoWrap = auto
}
}
// Step adds a new step to the chain with the provided function.
// The function must return an error or nil.
func (c *Chain) Step(fn func() error) *Chain {
if fn == nil {
// Panic to enforce valid input
panic("Chain.Step: provided function cannot be nil")
}
// Create a new step with default configuration
step := chainStep{execute: fn, config: stepConfig{}}
c.steps = append(c.steps, step)
// Update lastStep to point to the newly added step
c.lastStep = &c.steps[len(c.steps)-1]
return c
}
// Call adds a step by wrapping a function with arguments.
// It uses reflection to validate and invoke the function.
func (c *Chain) Call(fn interface{}, args ...interface{}) *Chain {
// Wrap the function and arguments into an executable step
wrappedFn, err := c.wrapCallable(fn, args...)
if err != nil {
// Panic on setup errors to catch them early
panic(fmt.Sprintf("Chain.Call setup error: %v", err))
}
// Add the wrapped function as a step
step := chainStep{execute: wrappedFn, config: stepConfig{}}
c.steps = append(c.steps, step)
c.lastStep = &c.steps[len(c.steps)-1]
return c
}
// Optional marks the last step as optional.
// Optional steps don't stop the chain on error.
func (c *Chain) Optional() *Chain {
if c.lastStep == nil {
// Panic if no step exists to mark as optional
panic("Chain.Optional: must call Step() or Call() before Optional()")
}
c.lastStep.optional = true
return c
}
// WithLog adds logging attributes to the last step.
func (c *Chain) WithLog(attrs ...slog.Attr) *Chain {
if c.lastStep == nil {
// Panic if no step exists to configure
panic("Chain.WithLog: must call Step() or Call() before WithLog()")
}
// Append attributes to the step's logging configuration
c.lastStep.config.logAttrs = append(c.lastStep.config.logAttrs, attrs...)
return c
}
// Timeout sets a timeout for the entire chain.
func (c *Chain) Timeout(d time.Duration) *Chain {
c.config.timeout = d
return c
}
// MaxErrors sets the maximum number of errors allowed.
func (c *Chain) MaxErrors(max int) *Chain {
if max <= 0 {
c.config.maxErrors = -1 // No limit
} else {
c.config.maxErrors = max
}
return c
}
// With adds a key-value pair to the last step's context.
func (c *Chain) With(key string, value interface{}) *Chain {
if c.lastStep == nil {
// Panic if no step exists to configure
panic("Chain.With: must call Step() or Call() before With()")
}
// Initialize context map if nil
if c.lastStep.config.context == nil {
c.lastStep.config.context = make(map[string]interface{})
}
// Add the key-value pair
c.lastStep.config.context[key] = value
return c
}
// Tag sets an error category for the last step.
func (c *Chain) Tag(category ErrorCategory) *Chain {
if c.lastStep == nil {
// Panic if no step exists to configure
panic("Chain.Tag: must call Step() or Call() before Tag()")
}
c.lastStep.config.category = category
return c
}
// Code sets a numeric error code for the last step.
func (c *Chain) Code(code int) *Chain {
if c.lastStep == nil {
// Panic if no step exists to configure
panic("Chain.Code: must call Step() or Call() before Code()")
}
c.lastStep.config.code = code
return c
}
// Retry configures retry behavior for the last step.
// Retry configures retry behavior for the last step.
func (c *Chain) Retry(maxAttempts int, delay time.Duration, opts ...RetryOption) *Chain {
if c.lastStep == nil {
panic("Chain.Retry: must call Step() or Call() before Retry()")
}
if maxAttempts < 1 {
maxAttempts = 1
}
// Define default retry options
retryOpts := []RetryOption{
WithMaxAttempts(maxAttempts),
WithDelay(delay),
WithRetryIf(func(err error) bool { return IsRetryable(err) }),
}
// Add logging for retry attempts if a handler is configured
if c.logHandler != nil {
step := c.lastStep
retryOpts = append(retryOpts, WithOnRetry(func(attempt int, err error) {
// Prepare logging attributes
logAttrs := []slog.Attr{
slog.Int("attempt", attempt),
slog.Int("max_attempts", maxAttempts),
}
// Enhance the error with step context
enhancedErr := c.enhanceError(err, step)
// Log the retry attempt
c.logError(enhancedErr, fmt.Sprintf("Retrying step (attempt %d/%d)", attempt, maxAttempts), step.config, logAttrs...)
}))
}
// Append any additional retry options
retryOpts = append(retryOpts, opts...)
// Create and assign the retry configuration
c.lastStep.config.retry = NewRetry(retryOpts...)
return c
}
// LogOnFail enables automatic logging of errors for the last step.
func (c *Chain) LogOnFail() *Chain {
if c.lastStep == nil {
// Panic if no step exists to configure
panic("Chain.LogOnFail: must call Step() or Call() before LogOnFail()")
}
c.lastStep.config.logOnFail = true
return c
}
// Run executes the chain, stopping on the first non-optional error.
// It returns the first error encountered or nil if all steps succeed.
func (c *Chain) Run() error {
// Create a context with timeout or cancellation
ctx, cancel := c.getContextAndCancel()
defer cancel()
c.cancel = cancel
// Clear any previous errors
c.errors = c.errors[:0]
// Execute each step in sequence
for i := range c.steps {
step := &c.steps[i]
// Check if the context has been canceled
select {
case <-ctx.Done():
err := ctx.Err()
// Enhance the error with step context
enhancedErr := c.enhanceError(err, step)
c.errors = append(c.errors, enhancedErr)
// Log the context error
c.logError(enhancedErr, "Chain stopped due to context error before step", step.config)
return enhancedErr
default:
}
// Execute the step
err := c.executeStep(ctx, step)
if err != nil {
// Enhance the error with step context
enhancedErr := c.enhanceError(err, step)
c.errors = append(c.errors, enhancedErr)
// Log the error if required
if step.config.logOnFail || !step.optional {
logMsg := "Chain stopped due to error in step"
if step.optional {
logMsg = "Optional step failed"
}
c.logError(enhancedErr, logMsg, step.config)
}
// Stop execution if the step is not optional
if !step.optional {
return enhancedErr
}
}
}
// Return nil if all steps completed successfully
return nil
}
// RunAll executes all steps, collecting errors without stopping.
// It returns a MultiError containing all errors or nil if none occurred.
func (c *Chain) RunAll() error {
ctx, cancel := c.getContextAndCancel()
defer cancel()
c.cancel = cancel
c.errors = c.errors[:0]
multi := NewMultiError()
for i := range c.steps {
step := &c.steps[i]
select {
case <-ctx.Done():
err := ctx.Err()
enhancedErr := c.enhanceError(err, step)
c.errors = append(c.errors, enhancedErr)
multi.Add(enhancedErr)
c.logError(enhancedErr, "Chain stopped due to context error before step (RunAll)", step.config)
goto endRunAll
default:
}
err := c.executeStep(ctx, step)
if err != nil {
enhancedErr := c.enhanceError(err, step)
c.errors = append(c.errors, enhancedErr)
multi.Add(enhancedErr)
if step.config.logOnFail && c.logHandler != nil {
c.logError(enhancedErr, "Step failed during RunAll", step.config)
}
if c.config.maxErrors > 0 && multi.Count() >= c.config.maxErrors {
if c.logHandler != nil {
// Create a logger to log the max errors condition
logger := slog.New(c.logHandler)
logger.LogAttrs(
context.Background(),
slog.LevelError,
fmt.Sprintf("Stopping RunAll after reaching max errors (%d)", c.config.maxErrors),
slog.Int("max_errors", c.config.maxErrors),
)
}
goto endRunAll
}
}
}
endRunAll:
return multi.Single()
}
// Errors returns a copy of the collected errors.
func (c *Chain) Errors() []error {
if len(c.errors) == 0 {
return nil
}
// Create a copy to prevent external modification
errs := make([]error, len(c.errors))
copy(errs, c.errors)
return errs
}
// Len returns the number of steps in the chain.
func (c *Chain) Len() int {
return len(c.steps)
}
// HasErrors checks if any errors were collected.
func (c *Chain) HasErrors() bool {
return len(c.errors) > 0
}
// LastError returns the most recent error or nil if none exist.
func (c *Chain) LastError() error {
if len(c.errors) > 0 {
return c.errors[len(c.errors)-1]
}
return nil
}
// Reset clears the chain's steps, errors, and context.
func (c *Chain) Reset() {
if c.cancel != nil {
// Cancel any active context
c.cancel()
c.cancel = nil
}
// Clear steps and errors
c.steps = c.steps[:0]
c.errors = c.errors[:0]
c.lastStep = nil
}
// Unwrap returns the collected errors (alias for Errors).
func (c *Chain) Unwrap() []error {
return c.errors
}
// getContextAndCancel creates a context based on the chain's timeout.
// It returns a context and its cancellation function.
func (c *Chain) getContextAndCancel() (context.Context, context.CancelFunc) {
parentCtx := context.Background()
if c.config.timeout > 0 {
// Create a context with a timeout
return context.WithTimeout(parentCtx, c.config.timeout)
}
// Create a cancellable context
return context.WithCancel(parentCtx)
}
// logError logs an error with step-specific context and attributes.
// It only logs if a handler is configured and the error is non-nil.
func (c *Chain) logError(err error, msg string, config stepConfig, additionalAttrs ...slog.Attr) {
// Skip logging if no handler is set or error is nil
if c == nil || c.logHandler == nil || err == nil {
return
}
// Create a logger on demand using the configured handler
logger := slog.New(c.logHandler)
// Initialize attributes with error and timestamp
allAttrs := make([]slog.Attr, 0, 5+len(config.logAttrs)+len(additionalAttrs))
allAttrs = append(allAttrs, slog.Any("error", err))
allAttrs = append(allAttrs, slog.Time("timestamp", time.Now()))
// Add step-specific metadata
if config.category != "" {
allAttrs = append(allAttrs, slog.String("category", string(config.category)))
}
if config.code != 0 {
allAttrs = append(allAttrs, slog.Int("code", config.code))
}
for k, v := range config.context {
allAttrs = append(allAttrs, slog.Any(k, v))
}
allAttrs = append(allAttrs, config.logAttrs...)
allAttrs = append(allAttrs, additionalAttrs...)
// Add stack trace and error name if the error is of type *Error
if e, ok := err.(*Error); ok {
if stack := e.Stack(); len(stack) > 0 {
// Format stack trace, truncating if too long
stackStr := "\n\t" + strings.Join(stack, "\n\t")
if len(stackStr) > 1000 {
stackStr = stackStr[:1000] + "..."
}
allAttrs = append(allAttrs, slog.String("stacktrace", stackStr))
}
if name := e.Name(); name != "" {
allAttrs = append(allAttrs, slog.String("error_name", name))
}
}
// Log the error at ERROR level with all attributes
// Use a defer to catch any panics during logging
defer func() {
if r := recover(); r != nil {
// Print to stdout to avoid infinite recursion
fmt.Printf("ERROR: Recovered from panic during logging: %v\nAttributes: %v\n", r, allAttrs)
}
}()
logger.LogAttrs(context.Background(), slog.LevelError, msg, allAttrs...)
}
// wrapCallable wraps a function and its arguments into an executable step.
// It uses reflection to validate the function and arguments.
func (c *Chain) wrapCallable(fn interface{}, args ...interface{}) (func() error, error) {
val := reflect.ValueOf(fn)
typ := val.Type()
// Ensure the provided value is a function
if typ.Kind() != reflect.Func {
return nil, fmt.Errorf("provided 'fn' is not a function (got %T)", fn)
}
// Check if the number of arguments matches the function's signature
if typ.NumIn() != len(args) {
return nil, fmt.Errorf("function expects %d arguments, but %d were provided", typ.NumIn(), len(args))
}
// Prepare argument values
argVals := make([]reflect.Value, len(args))
errorType := reflect.TypeOf((*error)(nil)).Elem()
for i, arg := range args {
expectedType := typ.In(i)
var providedVal reflect.Value
if arg != nil {
providedVal = reflect.ValueOf(arg)
// Check if the argument type is assignable to the expected type
if !providedVal.Type().AssignableTo(expectedType) {
// Special case for error interfaces
if expectedType.Kind() == reflect.Interface && expectedType.Implements(errorType) && providedVal.Type().Implements(errorType) {
// Allow error interface
} else {
return nil, fmt.Errorf("argument %d type mismatch: expected %s, got %s", i, expectedType, providedVal.Type())
}
}
} else {
// Handle nil arguments for nullable types
switch expectedType.Kind() {
case reflect.Chan, reflect.Func, reflect.Interface, reflect.Map, reflect.Pointer, reflect.Slice:
providedVal = reflect.Zero(expectedType)
default:
return nil, fmt.Errorf("argument %d is nil, but expected non-nillable type %s", i, expectedType)
}
}
argVals[i] = providedVal
}
// Validate the function's return type
if typ.NumOut() > 1 || (typ.NumOut() == 1 && !typ.Out(0).Implements(errorType)) {
return nil, fmt.Errorf("function must return either no values or a single error (got %d return values)", typ.NumOut())
}
// Return a wrapped function that calls the original with the provided arguments
return func() error {
results := val.Call(argVals)
if len(results) == 1 && results[0].Interface() != nil {
return results[0].Interface().(error)
}
return nil
}, nil
}
// executeStep runs a single step, applying retries if configured.
func (c *Chain) executeStep(ctx context.Context, step *chainStep) error {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
if step.config.retry != nil {
retry := step.config.retry.Transform(WithContext(ctx))
// Wrap step execution to respect context
wrappedFn := func() error {
type result struct {
err error
}
done := make(chan result, 1)
go func() {
done <- result{err: step.execute()}
}()
select {
case res := <-done:
return res.err
case <-ctx.Done():
return ctx.Err()
}
}
return retry.Execute(wrappedFn)
}
// Non-retry case also respects context
type result struct {
err error
}
done := make(chan result, 1)
go func() {
done <- result{err: step.execute()}
}()
select {
case res := <-done:
return res.err
case <-ctx.Done():
return ctx.Err()
}
}
// enhanceError wraps an error with additional context from the step.
func (c *Chain) enhanceError(err error, step *chainStep) error {
if err == nil || !c.config.autoWrap {
// Return the error unchanged if nil or autoWrap is disabled
return err
}
// Initialize the base error
var baseError *Error
if e, ok := err.(*Error); ok {
// Copy existing *Error to preserve its properties
baseError = e.Copy()
} else {
// Create a new *Error wrapping the original
baseError = New(err.Error()).Wrap(err).WithStack()
}
if step != nil {
// Add step-specific context to the error
if step.config.category != "" && baseError.Category() == "" {
baseError.WithCategory(step.config.category)
}
if step.config.code != 0 && baseError.Code() == 0 {
baseError.WithCode(step.config.code)
}
for k, v := range step.config.context {
baseError.With(k, v)
}
for _, attr := range step.config.logAttrs {
baseError.With(attr.Key, attr.Value.Any())
}
if step.config.retry != nil && !baseError.HasContextKey(ctxRetry) {
// Mark the error as retryable if retries are configured
baseError.WithRetryable()
}
}
return baseError
}

1496
vendor/github.com/olekukonko/errors/errors.go generated vendored Normal file

File diff suppressed because it is too large Load diff

432
vendor/github.com/olekukonko/errors/helper.go generated vendored Normal file
View file

@ -0,0 +1,432 @@
package errors
import (
"context"
"errors"
"fmt"
"strings"
"time"
)
// As wraps errors.As, using custom type assertion for *Error types.
// Falls back to standard errors.As for non-*Error types.
// Returns false if either err or target is nil.
func As(err error, target interface{}) bool {
if err == nil || target == nil {
return false
}
// First try our custom *Error handling
if e, ok := err.(*Error); ok {
return e.As(target)
}
// Fall back to standard errors.As
return errors.As(err, target)
}
// Code returns the status code of an error, if it is an *Error.
// Returns 500 as a default for non-*Error types to indicate an internal error.
func Code(err error) int {
if e, ok := err.(*Error); ok {
return e.Code()
}
return DefaultCode
}
// Context extracts the context map from an error, if it is an *Error.
// Returns nil for non-*Error types or if no context is present.
func Context(err error) map[string]interface{} {
if e, ok := err.(*Error); ok {
return e.Context()
}
return nil
}
// Convert transforms any error into an *Error, preserving its message and wrapping it if needed.
// Returns nil if the input is nil; returns the original if already an *Error.
// Uses multiple strategies: direct assertion, errors.As, manual unwrapping, and fallback creation.
func Convert(err error) *Error {
if err == nil {
return nil
}
// First try direct type assertion (fast path)
if e, ok := err.(*Error); ok {
return e
}
// Try using errors.As (more flexible)
var e *Error
if errors.As(err, &e) {
return e
}
// Manual unwrapping as fallback
visited := make(map[error]bool)
for unwrapped := err; unwrapped != nil; {
if visited[unwrapped] {
break // Cycle detected
}
visited[unwrapped] = true
if e, ok := unwrapped.(*Error); ok {
return e
}
unwrapped = errors.Unwrap(unwrapped)
}
// Final fallback: create new error with original message and wrap it
return New(err.Error()).Wrap(err)
}
// Count returns the occurrence count of an error, if it is an *Error.
// Returns 0 for non-*Error types.
func Count(err error) uint64 {
if e, ok := err.(*Error); ok {
return e.Count()
}
return 0
}
// Find searches the error chain for the first error matching pred.
// Returns nil if no match is found or pred is nil; traverses both Unwrap() and Cause() chains.
func Find(err error, pred func(error) bool) error {
for current := err; current != nil; {
if pred(current) {
return current
}
// Attempt to unwrap using Unwrap() or Cause()
switch v := current.(type) {
case interface{ Unwrap() error }:
current = v.Unwrap()
case interface{ Cause() error }:
current = v.Cause()
default:
return nil
}
}
return nil
}
// From transforms any error into an *Error, preserving its message and wrapping it if needed.
// Alias of Convert; returns nil if input is nil, original if already an *Error.
func From(err error) *Error {
return Convert(err)
}
// FromContext creates an *Error from a context and an existing error.
// Enhances the error with context info: timeout status, deadline, or cancellation.
// Returns nil if input error is nil; does not store context values directly.
func FromContext(ctx context.Context, err error) *Error {
if err == nil {
return nil
}
e := New(err.Error())
// Handle context errors
switch ctx.Err() {
case context.DeadlineExceeded:
e.WithTimeout()
if deadline, ok := ctx.Deadline(); ok {
e.With("deadline", deadline.Format(time.RFC3339))
}
case context.Canceled:
e.With("cancelled", true)
}
return e
}
// Category returns the category of an error, if it is an *Error.
// Returns an empty string for non-*Error types or unset categories.
func Category(err error) string {
if e, ok := err.(*Error); ok {
return e.category
}
return ""
}
// Has checks if an error contains meaningful content.
// Returns true for non-nil standard errors or *Error with content (msg, name, template, or cause).
func Has(err error) bool {
if e, ok := err.(*Error); ok {
return e.Has()
}
return err != nil
}
// HasContextKey checks if the error's context contains the specified key.
// Returns false for non-*Error types or if the key is not present in the context.
func HasContextKey(err error, key string) bool {
if e, ok := err.(*Error); ok {
ctx := e.Context()
if ctx != nil {
_, exists := ctx[key]
return exists
}
}
return false
}
// Is wraps errors.Is, using custom matching for *Error types.
// Falls back to standard errors.Is for non-*Error types; returns true if err equals target.
func Is(err, target error) bool {
if err == nil || target == nil {
return err == target
}
if e, ok := err.(*Error); ok {
return e.Is(target)
}
// Use standard errors.Is for non-Error types
return errors.Is(err, target)
}
// IsError checks if an error is an instance of *Error.
// Returns true only for this package's custom error type; false for nil or other types.
func IsError(err error) bool {
_, ok := err.(*Error)
return ok
}
// IsEmpty checks if an error has no meaningful content.
// Returns true for nil errors, empty *Error instances, or standard errors with whitespace-only messages.
func IsEmpty(err error) bool {
if err == nil {
return true
}
if e, ok := err.(*Error); ok {
return e.IsEmpty()
}
return strings.TrimSpace(err.Error()) == ""
}
// IsNull checks if an error is nil or represents a NULL value.
// Delegates to *Errors IsNull for custom errors; uses sqlNull for others.
func IsNull(err error) bool {
if err == nil {
return true
}
if e, ok := err.(*Error); ok {
return e.IsNull()
}
return sqlNull(err)
}
// IsRetryable checks if an error is retryable.
// For *Error, checks context for retry flag; for others, looks for "retry" or timeout in message.
// Returns false for nil errors; thread-safe for *Error types.
func IsRetryable(err error) bool {
if err == nil {
return false
}
if e, ok := err.(*Error); ok {
e.mu.RLock()
defer e.mu.RUnlock()
// Check smallContext directly if context map isnt populated
for i := int32(0); i < e.smallCount; i++ {
if e.smallContext[i].key == ctxRetry {
if val, ok := e.smallContext[i].value.(bool); ok {
return val
}
}
}
// Check regular context
if e.context != nil {
if val, ok := e.context[ctxRetry].(bool); ok {
return val
}
}
// Check cause recursively
if e.cause != nil {
return IsRetryable(e.cause)
}
}
lowerMsg := strings.ToLower(err.Error())
return IsTimeout(err) || strings.Contains(lowerMsg, "retry")
}
// IsTimeout checks if an error indicates a timeout.
// For *Error, checks context for timeout flag; for others, looks for "timeout" in message.
// Returns false for nil errors.
func IsTimeout(err error) bool {
if err == nil {
return false
}
if e, ok := err.(*Error); ok {
if val, ok := e.Context()[ctxTimeout].(bool); ok {
return val
}
}
return strings.Contains(strings.ToLower(err.Error()), "timeout")
}
// Merge combines multiple errors into a single *Error.
// Aggregates messages with "; " separator, merges contexts and stacks; returns nil if no errors provided.
func Merge(errs ...error) *Error {
if len(errs) == 0 {
return nil
}
var messages []string
combined := New("")
for _, err := range errs {
if err == nil {
continue
}
messages = append(messages, err.Error())
if e, ok := err.(*Error); ok {
if e.stack != nil && combined.stack == nil {
combined.WithStack() // Capture stack from first *Error with stack
}
if ctx := e.Context(); ctx != nil {
for k, v := range ctx {
combined.With(k, v)
}
}
if e.cause != nil {
combined.Wrap(e.cause)
}
} else {
combined.Wrap(err)
}
}
if len(messages) > 0 {
combined.msg = strings.Join(messages, "; ")
}
return combined
}
// Name returns the name of an error, if it is an *Error.
// Returns an empty string for non-*Error types or unset names.
func Name(err error) string {
if e, ok := err.(*Error); ok {
return e.name
}
return ""
}
// UnwrapAll returns a slice of all errors in the chain, including the root error.
// Traverses both Unwrap() and Cause() chains; returns nil if err is nil.
func UnwrapAll(err error) []error {
if err == nil {
return nil
}
if e, ok := err.(*Error); ok {
return e.UnwrapAll()
}
var result []error
Walk(err, func(e error) {
result = append(result, e)
})
return result
}
// Stack extracts the stack trace from an error, if it is an *Error.
// Returns nil for non-*Error types or if no stack is present.
func Stack(err error) []string {
if e, ok := err.(*Error); ok {
return e.Stack()
}
return nil
}
// Transform applies transformations to an error, returning a new *Error.
// Creates a new *Error from non-*Error types before applying fn; returns nil if err is nil.
func Transform(err error, fn func(*Error)) *Error {
if err == nil {
return nil
}
if e, ok := err.(*Error); ok {
newErr := e.Copy()
fn(newErr)
return newErr
}
// If not an *Error, create a new one and transform it
newErr := New(err.Error())
fn(newErr)
return newErr
}
// Unwrap returns the underlying cause of an error, if it implements Unwrap.
// For *Error, returns cause; for others, returns the error itself; nil if err is nil.
func Unwrap(err error) error {
for current := err; current != nil; {
if e, ok := current.(*Error); ok {
if e.cause == nil {
return current
}
current = e.cause
} else {
return current
}
}
return nil
}
// Walk traverses the error chain, applying fn to each error.
// Supports both Unwrap() and Cause() interfaces; stops at nil or non-unwrappable errors.
func Walk(err error, fn func(error)) {
for current := err; current != nil; {
fn(current)
// Attempt to unwrap using Unwrap() or Cause()
switch v := current.(type) {
case interface{ Unwrap() error }:
current = v.Unwrap()
case interface{ Cause() error }:
current = v.Cause()
default:
return
}
}
}
// With adds a key-value pair to an error's context, if it is an *Error.
// Returns the original error unchanged if not an *Error; no-op for non-*Error types.
func With(err error, key string, value interface{}) error {
if e, ok := err.(*Error); ok {
return e.With(key, value)
}
return err
}
// WithStack converts any error to an *Error and captures a stack trace.
// Returns nil if input is nil; adds stack to existing *Error or wraps non-*Error types.
func WithStack(err error) *Error {
if err == nil {
return nil
}
if e, ok := err.(*Error); ok {
return e.WithStack()
}
return New(err.Error()).WithStack().Wrap(err)
}
// Wrap creates a new *Error that wraps another error with additional context.
// Uses a copy of the provided wrapper *Error; returns nil if err is nil.
func Wrap(err error, wrapper *Error) *Error {
if err == nil {
return nil
}
if wrapper == nil {
wrapper = newError()
}
newErr := wrapper.Copy()
newErr.cause = err
return newErr
}
// Wrapf creates a new formatted *Error that wraps another error.
// Formats the message and sets the cause; returns nil if err is nil.
func Wrapf(err error, format string, args ...interface{}) *Error {
if err == nil {
return nil
}
e := newError()
e.msg = fmt.Sprintf(format, args...)
e.cause = err
return e
}

225
vendor/github.com/olekukonko/errors/inspect.go generated vendored Normal file
View file

@ -0,0 +1,225 @@
// File: inspect.go
// Updated to support both error and *Error with delegation for cleaner *Error handling
package errors
import (
stderrs "errors"
"fmt"
"strings"
"time"
)
// Inspect provides detailed examination of an error, handling both single errors and MultiError
func Inspect(err error) {
if err == nil {
fmt.Println("No error occurred")
return
}
fmt.Printf("\n=== Error Inspection ===\n")
fmt.Printf("Top-level error: %v\n", err)
fmt.Printf("Top-level error type: %T\n", err)
// Handle *Error directly
if e, ok := err.(*Error); ok {
InspectError(e)
return
}
// Handle MultiError
if multi, ok := err.(*MultiError); ok {
allErrors := multi.Errors()
fmt.Printf("\nContains %d errors:\n", len(allErrors))
for i, e := range allErrors {
fmt.Printf("\n--- Error %d ---\n", i+1)
inspectSingleError(e)
}
} else {
// Inspect single error if not MultiError or *Error
fmt.Println("\n--- Details ---")
inspectSingleError(err)
}
// Additional diagnostics
fmt.Println("\n--- Diagnostics ---")
if IsRetryable(err) {
fmt.Println("- Error chain contains retryable errors")
}
if IsTimeout(err) {
fmt.Println("- Error chain contains timeout errors")
}
if code := getErrorCode(err); code != 0 {
fmt.Printf("- Highest priority error code: %d\n", code)
}
fmt.Printf("========================\n\n")
}
// InspectError provides detailed inspection of a specific *Error instance
func InspectError(err *Error) {
if err == nil {
fmt.Println("No error occurred")
return
}
fmt.Printf("\n=== Error Inspection (*Error) ===\n")
fmt.Printf("Top-level error: %v\n", err)
fmt.Printf("Top-level error type: %T\n", err)
fmt.Println("\n--- Details ---")
inspectSingleError(err) // Delegate to handle unwrapping and details
// Additional diagnostics specific to *Error
fmt.Println("\n--- Diagnostics ---")
if IsRetryable(err) {
fmt.Println("- Error is retryable")
}
if IsTimeout(err) {
fmt.Println("- Error chain contains timeout errors")
}
if code := err.Code(); code != 0 {
fmt.Printf("- Error code: %d\n", code)
}
fmt.Printf("========================\n\n")
}
// inspectSingleError handles inspection of a single error (may be part of a chain)
func inspectSingleError(err error) {
if err == nil {
fmt.Println(" (nil error)")
return
}
fmt.Printf(" Error: %v\n", err)
fmt.Printf(" Type: %T\n", err)
// Handle wrapped errors, including *Error type
var currentErr error = err
depth := 0
for currentErr != nil {
prefix := strings.Repeat(" ", depth+1)
if depth > 0 {
fmt.Printf("%sWrapped Cause (%T): %v\n", prefix, currentErr, currentErr)
}
// Check if it's our specific *Error type
if e, ok := currentErr.(*Error); ok {
if name := e.Name(); name != "" {
fmt.Printf("%sName: %s\n", prefix, name)
}
if cat := e.Category(); cat != "" {
fmt.Printf("%sCategory: %s\n", prefix, cat)
}
if code := e.Code(); code != 0 {
fmt.Printf("%sCode: %d\n", prefix, code)
}
if ctx := e.Context(); len(ctx) > 0 {
fmt.Printf("%sContext:\n", prefix)
for k, v := range ctx {
fmt.Printf("%s %s: %v\n", prefix, k, v)
}
}
if stack := e.Stack(); len(stack) > 0 {
fmt.Printf("%sStack (Top 3):\n", prefix)
limit := 3
if len(stack) < limit {
limit = len(stack)
}
for i := 0; i < limit; i++ {
fmt.Printf("%s %s\n", prefix, stack[i])
}
if len(stack) > limit {
fmt.Printf("%s ... (%d more frames)\n", prefix, len(stack)-limit)
}
}
}
// Unwrap using standard errors.Unwrap and handle *Error Unwrap
var nextErr error
// Prioritize *Error's Unwrap if available AND it returns non-nil
if e, ok := currentErr.(*Error); ok {
unwrapped := e.Unwrap()
if unwrapped != nil {
nextErr = unwrapped
} else {
// If *Error.Unwrap returns nil, fall back to standard unwrap
// This handles cases where *Error might wrap a non-standard error
// or where its internal cause is deliberately nil.
nextErr = stderrs.Unwrap(currentErr)
}
} else {
nextErr = stderrs.Unwrap(currentErr) // Fall back to standard unwrap for non-*Error types
}
// Prevent infinite loops if Unwrap returns the same error, or stop if no more unwrapping
if nextErr == currentErr || nextErr == nil {
break
}
currentErr = nextErr
depth++
if depth > 10 { // Safety break for very deep or potentially cyclic chains
fmt.Printf("%s... (chain too deep or potential cycle)\n", strings.Repeat(" ", depth+1))
break
}
}
}
// getErrorCode traverses the error chain to find the highest priority code.
// It uses errors.As to find the first *Error in the chain.
func getErrorCode(err error) int {
var code int = 0 // Default code
var target *Error
if As(err, &target) { // Use the package's As helper
if target != nil { // Add nil check for safety
code = target.Code()
}
}
// If the top-level error is *Error and has a code, it might take precedence.
// This depends on desired logic. Let's keep it simple for now: first code found by As.
if code == 0 { // Only check top-level if As didn't find one with a code
if e, ok := err.(*Error); ok {
code = e.Code()
}
}
return code
}
// handleError demonstrates using Inspect with additional handling logic
func handleError(err error) {
fmt.Println("\n=== Processing Failure ===")
Inspect(err) // Use the primary Inspect function
// Additional handling based on inspection
code := getErrorCode(err) // Use the helper
switch {
case IsTimeout(err):
fmt.Println("\nAction: Check connectivity or increase timeout")
case code == 402: // Check code obtained via helper
fmt.Println("\nAction: Payment processing failed - notify billing")
default:
fmt.Println("\nAction: Generic failure handling")
}
}
// processOrder demonstrates Chain usage with Inspect
func processOrder() error {
validateInput := func() error { return nil }
processPayment := func() error { return stderrs.New("credit card declined") }
sendNotification := func() error { fmt.Println("Notification sent."); return nil }
logOrder := func() error { fmt.Println("Order logged."); return nil }
chain := NewChain(ChainWithTimeout(2*time.Second)).
Step(validateInput).Tag("validation").
Step(processPayment).Tag("billing").Code(402).Retry(3, 100*time.Millisecond, WithRetryIf(IsRetryable)).
Step(sendNotification).Optional().
Step(logOrder)
err := chain.Run()
if err != nil {
handleError(err) // Call the unified error handler
return err // Propagate the error if needed
}
fmt.Println("Order processed successfully!")
return nil
}

Some files were not shown because too many files have changed in this diff Show more