6 Commits

Author SHA1 Message Date
Bert Proesmans
0e352a9bb6 Client certificate for relay 2026-01-14 20:20:08 +01:00
dependabot[bot]
42d1721751 build(deps): Bump github/codeql-action from 4.31.9 to 4.31.10
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 4.31.9 to 4.31.10.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](5d4e8d1aca...cdefb33c0f)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: 4.31.10
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-14 19:59:43 +01:00
dependabot[bot]
e20797a4df build(deps): Bump actions/setup-go from 6.1.0 to 6.2.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 6.1.0 to 6.2.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](4dc6199c7b...7a3fe6cf4c)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-version: 6.2.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-14 19:58:56 +01:00
dependabot[bot]
54b224c8c6 build(deps): Bump golang.org/x/crypto from 0.46.0 to 0.47.0
Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.46.0 to 0.47.0.
- [Commits](https://github.com/golang/crypto/compare/v0.46.0...v0.47.0)

---
updated-dependencies:
- dependency-name: golang.org/x/crypto
  dependency-version: 0.47.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-01-14 19:57:14 +01:00
Bernhard Froehlich
8d7554031d CI: Add specific release_tag for manually triggered builds 2026-01-07 16:57:53 +00:00
Bernhard Froehlich
9ad25e290f CI: Improve release action to allow manual triggering 2026-01-07 16:36:36 +00:00
10 changed files with 114 additions and 20 deletions

View File

@@ -47,7 +47,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
@@ -58,7 +58,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild
uses: github/codeql-action/autobuild@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
uses: github/codeql-action/autobuild@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
# Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
@@ -72,4 +72,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
uses: github/codeql-action/analyze@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10

View File

@@ -15,7 +15,7 @@ jobs:
egress-policy: audit
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0
- uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with:
go-version: 'stable'

View File

@@ -3,6 +3,12 @@ name: Release Go Binaries
on:
release:
types: [created]
workflow_dispatch:
inputs:
release_tag:
description: 'Tag name to build (v1.3.1)'
required: false
default: ''
# Declare default permissions as read only.
permissions: read-all
@@ -25,10 +31,25 @@ jobs:
with:
egress-policy: audit
- name: Determine ref to checkout
run: |
# If manually invoked with a release_tag input, use refs/tags/<release_tag>.
if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ github.event.inputs.release_tag }}" ]; then
echo "REF=refs/tags/${{ github.event.inputs.release_tag }}" >> $GITHUB_ENV
else
# For release events GITHUB_REF is already refs/tags/<tag>; otherwise fall back to the incoming ref.
echo "REF=${GITHUB_REF}" >> $GITHUB_ENV
fi
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ env.REF }}
- name: Set APP_VERSION env
run: echo APP_VERSION=$(echo ${GITHUB_REF} | rev | cut -d'/' -f 1 | rev ) >> ${GITHUB_ENV}
run: |
# basename strips refs/... and yields the tag or branch name
echo "APP_VERSION=$(basename ${REF})" >> $GITHUB_ENV
- name: Set BUILD_TIME env
run: echo BUILD_TIME=$(date) >> ${GITHUB_ENV}
@@ -39,3 +60,4 @@ jobs:
goarch: ${{ matrix.goarch }}
extra_files: LICENSE README.md smtprelay.ini
ldflags: -s -w -X "main.appVersion=${{ env.APP_VERSION }}" -X "main.buildTime=${{ env.BUILD_TIME }}"
release_tag: ${{ env.APP_VERSION }}

View File

@@ -76,6 +76,6 @@ jobs:
# Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning"
uses: github/codeql-action/upload-sarif@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9
uses: github/codeql-action/upload-sarif@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
with:
sarif_file: results.sarif

View File

@@ -46,6 +46,8 @@ var (
command = flagset.String("command", "", "Path to pipe command")
remotesStr = flagset.String("remotes", "", "Outgoing SMTP servers")
strictSender = flagset.Bool("strict_sender", false, "Use only SMTP servers with Sender matches to From")
remoteCert = flagset.String("remote_certificate", "", "Client SSL certificate for remote STARTTLS/TLS")
remoteKey = flagset.String("remote_key", "", "Client SSL private key for remote STARTTLS/TLS")
// additional flags
_ = flagset.String("config", "", "Path to config file (ini format)")
@@ -67,6 +69,36 @@ func localAuthRequired() bool {
return *allowedUsers != ""
}
func remoteCertAndKeyReadable() bool {
certSet := *remoteCert != ""
keySet := *remoteKey != ""
// Both must be set or both must be unset
if certSet != keySet {
return false
}
// If both are set, verify files exist and are accessible
if certSet && keySet {
if _, err := os.Stat(*remoteCert); err != nil {
log.Error().
Str("cert", *remoteCert).
Err(err).
Msg("cannot access remote client certificate file")
return false
}
if _, err := os.Stat(*remoteKey); err != nil {
log.Error().
Str("key", *remoteKey).
Err(err).
Msg("cannot access remote client key file")
return false
}
}
return true
}
func setupAliases() {
if *aliasFile != "" {
aliases, err := AliasLoadFile(*aliasFile)
@@ -137,6 +169,11 @@ func setupRemotes() {
logger.Fatal().Msg(fmt.Sprintf("error parsing url: '%s': %v", remoteURL, err))
}
if *remoteCert != "" && *remoteKey != "" && (r.Scheme == "smtps" || r.Scheme == "starttls") {
r.ClientCertPath = *remoteCert
r.ClientKeyPath = *remoteKey
}
remotes = append(remotes, r)
}
}
@@ -253,6 +290,13 @@ func ConfigLoad() {
log.Warn().Msg("no remotes or command set; mail will not be forwarded!")
}
if !remoteCertAndKeyReadable() {
log.Fatal().
Str("remote_certificate", *remoteCert).
Str("remote_key", *remoteKey).
Msg("remote_certificate and remote_key must both be set or both be empty")
}
setupAllowedNetworks()
setupAllowedPatterns()
setupAliases()

4
go.mod
View File

@@ -8,7 +8,7 @@ require (
github.com/peterbourgon/ff/v3 v3.4.0
github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.46.0
golang.org/x/crypto v0.47.0
)
require (
@@ -17,7 +17,7 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/sys v0.40.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

8
go.sum
View File

@@ -31,13 +31,13 @@ github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=
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.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -14,6 +14,8 @@ type Remote struct {
Port string
Addr string
Sender string
ClientCertPath string
ClientKeyPath string
}
// ParseRemote creates a remote from a given url in the following format:

16
smtp.go
View File

@@ -340,6 +340,14 @@ func SendMail(r *Remote, from string, to []string, msg []byte) error {
ServerName: r.Hostname,
InsecureSkipVerify: r.SkipVerify,
}
// Load client certificate on-demand, just before connection
if r.ClientCertPath != "" && r.ClientKeyPath != "" {
cert, err := tls.LoadX509KeyPair(r.ClientCertPath, r.ClientKeyPath)
if err != nil {
return err
}
config.Certificates = []tls.Certificate{cert}
}
conn, err := tls.Dial("tcp", r.Addr, config)
if err != nil {
return err
@@ -366,6 +374,14 @@ func SendMail(r *Remote, from string, to []string, msg []byte) error {
ServerName: c.serverName,
InsecureSkipVerify: r.SkipVerify,
}
// Load client certificate on-demand, just before use
if r.ClientCertPath != "" && r.ClientKeyPath != "" {
cert, err := tls.LoadX509KeyPair(r.ClientCertPath, r.ClientKeyPath)
if err != nil {
return err
}
config.Certificates = []tls.Certificate{cert}
}
if testHookStartTLS != nil {
testHookStartTLS(config)
}

View File

@@ -119,6 +119,10 @@
; Mailjet.com
;remotes = starttls://user:pass@in-v3.mailjet.com:587
; Exchange Online (O365) SMTP relay
; (Change netloc to your own Exchange MX endpoint)
; remotes = starttls://contoso-com.mail.protection.outlook.com:25
; Ignore remote host certificates
;remotes = starttls://user:pass@server:587?skipVerify
@@ -131,5 +135,11 @@
; Multiple remotes, space delimited
;remotes = smtp://127.0.0.1:1025 starttls://user:pass@smtp.mailgun.org:587
; Client SSL certificate for remote STARTTLS/TLS
; remote_certificate = /path/to/certificate-chain.pem
; Client SSL private key for remote STARTTLS/TLS
; remote_key = /path/to/private-key.pem
; Pipe messages to external command
;command = /usr/local/bin/script