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. # Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL - name: Initialize CodeQL
uses: github/codeql-action/init@5d4e8d1aca955e8d8589aabd499c5cae939e33c7 # v4.31.9 uses: github/codeql-action/init@cdefb33c0f6224e58673d9004f47f7cb3e328b89 # v4.31.10
with: with:
languages: ${{ matrix.language }} languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file. # 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). # 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) # If this step fails, then you should remove it and run the build manually (see below)
- name: Autobuild - 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. # Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl # 📚 https://git.io/JvXDl
@@ -72,4 +72,4 @@ jobs:
# make release # make release
- name: Perform CodeQL Analysis - 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 egress-policy: audit
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- uses: actions/setup-go@4dc6199c7b1a012772edbd06daecab0f50c9053c # v6.1.0 - uses: actions/setup-go@7a3fe6cf4cb3a834922a1244abfce67bcef6a0c5 # v6.2.0
with: with:
go-version: 'stable' go-version: 'stable'

View File

@@ -3,6 +3,12 @@ name: Release Go Binaries
on: on:
release: release:
types: [created] types: [created]
workflow_dispatch:
inputs:
release_tag:
description: 'Tag name to build (v1.3.1)'
required: false
default: ''
# Declare default permissions as read only. # Declare default permissions as read only.
permissions: read-all permissions: read-all
@@ -25,10 +31,25 @@ jobs:
with: with:
egress-policy: audit 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 - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
with:
ref: ${{ env.REF }}
- name: Set APP_VERSION env - 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 - name: Set BUILD_TIME env
run: echo BUILD_TIME=$(date) >> ${GITHUB_ENV} run: echo BUILD_TIME=$(date) >> ${GITHUB_ENV}
@@ -39,3 +60,4 @@ jobs:
goarch: ${{ matrix.goarch }} goarch: ${{ matrix.goarch }}
extra_files: LICENSE README.md smtprelay.ini extra_files: LICENSE README.md smtprelay.ini
ldflags: -s -w -X "main.appVersion=${{ env.APP_VERSION }}" -X "main.buildTime=${{ env.BUILD_TIME }}" 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. # Upload the results to GitHub's code scanning dashboard.
- name: "Upload to code-scanning" - 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: with:
sarif_file: results.sarif sarif_file: results.sarif

View File

@@ -46,6 +46,8 @@ var (
command = flagset.String("command", "", "Path to pipe command") command = flagset.String("command", "", "Path to pipe command")
remotesStr = flagset.String("remotes", "", "Outgoing SMTP servers") remotesStr = flagset.String("remotes", "", "Outgoing SMTP servers")
strictSender = flagset.Bool("strict_sender", false, "Use only SMTP servers with Sender matches to From") 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 // additional flags
_ = flagset.String("config", "", "Path to config file (ini format)") _ = flagset.String("config", "", "Path to config file (ini format)")
@@ -67,6 +69,36 @@ func localAuthRequired() bool {
return *allowedUsers != "" 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() { func setupAliases() {
if *aliasFile != "" { if *aliasFile != "" {
aliases, err := AliasLoadFile(*aliasFile) aliases, err := AliasLoadFile(*aliasFile)
@@ -137,6 +169,11 @@ func setupRemotes() {
logger.Fatal().Msg(fmt.Sprintf("error parsing url: '%s': %v", remoteURL, err)) 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) remotes = append(remotes, r)
} }
} }
@@ -253,6 +290,13 @@ func ConfigLoad() {
log.Warn().Msg("no remotes or command set; mail will not be forwarded!") 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() setupAllowedNetworks()
setupAllowedPatterns() setupAllowedPatterns()
setupAliases() setupAliases()

4
go.mod
View File

@@ -8,7 +8,7 @@ require (
github.com/peterbourgon/ff/v3 v3.4.0 github.com/peterbourgon/ff/v3 v3.4.0
github.com/rs/zerolog v1.34.0 github.com/rs/zerolog v1.34.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.46.0 golang.org/x/crypto v0.47.0
) )
require ( require (
@@ -17,7 +17,7 @@ require (
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/pmezard/go-difflib v1.0.0 // 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 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/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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= 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.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.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 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 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
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/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

View File

@@ -7,13 +7,15 @@ import (
) )
type Remote struct { type Remote struct {
SkipVerify bool SkipVerify bool
Auth smtp.Auth Auth smtp.Auth
Scheme string Scheme string
Hostname string Hostname string
Port string Port string
Addr string Addr string
Sender string Sender string
ClientCertPath string
ClientKeyPath string
} }
// ParseRemote creates a remote from a given url in the following format: // 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, ServerName: r.Hostname,
InsecureSkipVerify: r.SkipVerify, 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) conn, err := tls.Dial("tcp", r.Addr, config)
if err != nil { if err != nil {
return err return err
@@ -366,6 +374,14 @@ func SendMail(r *Remote, from string, to []string, msg []byte) error {
ServerName: c.serverName, ServerName: c.serverName,
InsecureSkipVerify: r.SkipVerify, 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 { if testHookStartTLS != nil {
testHookStartTLS(config) testHookStartTLS(config)
} }

View File

@@ -119,6 +119,10 @@
; Mailjet.com ; Mailjet.com
;remotes = starttls://user:pass@in-v3.mailjet.com:587 ;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 ; Ignore remote host certificates
;remotes = starttls://user:pass@server:587?skipVerify ;remotes = starttls://user:pass@server:587?skipVerify
@@ -131,5 +135,11 @@
; Multiple remotes, space delimited ; Multiple remotes, space delimited
;remotes = smtp://127.0.0.1:1025 starttls://user:pass@smtp.mailgun.org:587 ;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 ; Pipe messages to external command
;command = /usr/local/bin/script ;command = /usr/local/bin/script