Compare commits

..

117 Commits

Author SHA1 Message Date
Jonathon Reinhart 190c615029 Add SystemD unit file 2021-04-01 23:44:42 -04:00
Jonathon Reinhart 42abf27c1f Merge pull request #22 from decke/smtpd-shutdown
Handle signals and gracefully shut down, waiting for clients
2021-04-01 01:11:48 -04:00
Jonathon Reinhart 1b7b551f65 Handle signals and gracefully shut down server 2021-04-01 01:07:30 -04:00
Bernhard Fröhlich 2cd636c082 Merge pull request #19 from decke/allow-any-sender-recipient
Move remaining config option parsing to ConfigLoad()
2021-04-01 06:05:52 +02:00
Jonathon Reinhart 3debf4127d Adjust remote auth disabled check syntax 2021-03-31 17:18:13 -04:00
Bernhard Fröhlich 03b8b78f53 Merge pull request #24 from decke/dependabot/go_modules/github.com/chrj/smtpd-0.3.0
Bump github.com/chrj/smtpd from 0.2.0 to 0.3.0
2021-03-29 22:26:38 +02:00
dependabot[bot] 49c6880175 Bump github.com/chrj/smtpd from 0.2.0 to 0.3.0
Bumps [github.com/chrj/smtpd](https://github.com/chrj/smtpd) from 0.2.0 to 0.3.0.
- [Release notes](https://github.com/chrj/smtpd/releases)
- [Commits](https://github.com/chrj/smtpd/compare/v0.2.0...v0.3.0)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-29 20:24:57 +00:00
Bernhard Fröhlich 5470132251 Merge pull request #23 from decke/dependabot/github_actions/wangyoucao577/go-release-action-v1.15
Bump wangyoucao577/go-release-action from v1.14 to v1.15
2021-03-18 15:41:11 +01:00
dependabot[bot] 898f8e44cf Bump wangyoucao577/go-release-action from v1.14 to v1.15
Bumps [wangyoucao577/go-release-action](https://github.com/wangyoucao577/go-release-action) from v1.14 to v1.15.
- [Release notes](https://github.com/wangyoucao577/go-release-action/releases)
- [Commits](https://github.com/wangyoucao577/go-release-action/compare/v1.14...1caa775ade7bd6692dd752d506f106792e76843d)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-18 07:16:46 +00:00
Jonathon Reinhart 22ef0c2ee6 Move SMTP auth setup to ConfigLoad()
This has several benefits:
- Configuration errors are caught at startup rather than upon a connection
- mailHandler() has less work to do for each connection

Rather than relying on remote_user and remote_pass to control whether
authentication is used, introduce an explicit "none" type for
remote_auth, and make that the default. (This is effectively the same
default behavior since remote_user and remote_pass default to empty.)

Also, we are in a better position to more thoroughly check for
configuration errors or mismatches:
- If remote_auth is given, remote_user and remote_pass are required.
- If remote_auth is given, remote_host is also required (because it
  makes no sense to say we're going to authenticate if we have no server
  to which to authenticate.)
- If remote_user or remote_pass are given, remote_auth cannot be "none".
2021-03-14 18:41:54 -04:00
Bernhard Fröhlich 8eea677a3d Merge pull request #20 from decke/dependabot/go_modules/github.com/sirupsen/logrus-1.8.1
Bump github.com/sirupsen/logrus from 1.7.0 to 1.8.1
2021-03-14 21:27:11 +01:00
dependabot[bot] 9f2497d948 Bump github.com/sirupsen/logrus from 1.7.0 to 1.8.1
Bumps [github.com/sirupsen/logrus](https://github.com/sirupsen/logrus) from 1.7.0 to 1.8.1.
- [Release notes](https://github.com/sirupsen/logrus/releases)
- [Changelog](https://github.com/sirupsen/logrus/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sirupsen/logrus/compare/v1.7.0...v1.8.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-03-14 20:23:53 +00:00
Jonathon Reinhart 76ef135d33 Clarify allowed_sender/allowed_recipient empty string behavior 2021-03-14 12:36:34 -04:00
Jonathon Reinhart 7c0ba34025 Move compilation of allowed_recipients to ConfigLoad()
This has several benefits:
- Configuration errors are caught at startup rather than upon a connection
- recipientChecker() has less work to do for each connection
2021-03-14 12:31:38 -04:00
Jonathon Reinhart a896ab2847 Move compilation of allowed_sender to ConfigLoad()
This has several benefits:
- Configuration errors are caught at startup rather than upon a connection
- senderChecker() has less work to do for each connection
2021-03-14 12:31:38 -04:00
Jonathon Reinhart c9b55b833b Merge pull request #18 from decke/allow-any-net
Allow any network and related enhancements
2021-03-13 20:47:09 -05:00
Jonathon Reinhart 918df65a3a Require that networks in allowed_nets are networks and not hosts 2021-03-13 20:40:25 -05:00
Jonathon Reinhart 0503c12ccd Allow "allowed_nets" to be empty, meaning any network is allowed 2021-03-13 20:34:48 -05:00
Jonathon Reinhart ef3f9c8ea0 Move parsing of "allowed_nets" out to ConfigLoad()
This has several benefits:
- Configuration errors are caught at startup rather than upon a connection
- connectionChecker() has less work to do for each connection
2021-03-13 20:34:48 -05:00
Jonathon Reinhart 4036213dd5 Simplify peerIP determination in connectionChecker()
peerIP = net.ParseIP(addr.IP.String())

can be simplified to just:

    peerIP = addr.IP

but we can also skip the safe cast since we know the net.Addr will always
be net.TCPAddr because we only have TCP listeners.
2021-03-13 20:19:07 -05:00
Bernhard Fröhlich 2475cadbad Merge pull request #16 from decke/discard
Discard mail if remote_host is not set
2021-03-13 19:06:08 +01:00
Jonathon Reinhart 20ca816160 Discard mail if remote_host is not set
This is useful for test environments.
2021-03-13 09:28:04 -05:00
Bernhard Fröhlich d1933a2e35 Merge pull request #15 from decke/structured-logging
Add structured logging via logrus
2021-03-13 11:02:19 +01:00
Jonathon Reinhart 9921b38046 Explicitly configure default logfile for stderr 2021-03-13 03:25:57 -05:00
Jonathon Reinhart 095fba119a Change default logfile to empty string (meaning stderr)
The causes logrus to write to stderr.
2021-03-13 03:25:57 -05:00
Jonathon Reinhart 34cb47c364 Implement structured logs using logrus
This was based loosely on an earlier implementation by
Danny Kopping <danny.kopping@grafana.com>
2021-03-13 03:25:57 -05:00
Bernhard Froehlich b36ed8eddb net/smtp: adds support for the SMTPUTF8 extension
Obtained from:	https://github.com/golang/go/issues/19860
2021-02-25 21:15:16 +00:00
Bernhard Froehlich 822dbbce7d Bump Go to 1.15 for CI builds 2021-02-17 19:31:36 +00:00
Bernhard Fröhlich 42f5c68f0b Merge pull request #12 from decke/dependabot/github_actions/actions/setup-go-v2.1.3
Bump actions/setup-go from v1 to v2.1.3
2021-02-17 20:30:36 +01:00
dependabot[bot] cd2dab8f8f Bump actions/setup-go from v1 to v2.1.3
Bumps [actions/setup-go](https://github.com/actions/setup-go) from v1 to v2.1.3.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v1...37335c7bb261b353407cff977110895fa0b4f7d8)

Signed-off-by: dependabot[bot] <support@github.com>
2021-02-17 19:29:13 +00:00
Bernhard Froehlich f2af99dc52 Add github-actions to dependabot config 2021-02-17 19:28:08 +00:00
Bernhard Froehlich 23e10bb03e Add dependabot.yml for automatic Go dependency checking 2021-02-17 19:23:20 +00:00
Bernhard Froehlich 5ba64c5c6e Add new Release Workflow using Github Actions and wangyoucao577/go-release-action 2021-02-17 12:49:24 +00:00
Bernhard Froehlich 7f34fcbc99 gofmt: Fix formatting 2021-02-16 15:57:50 +00:00
Bernhard Froehlich 97943c87e7 Update go dependencies 2021-02-16 15:33:12 +00:00
Bernhard Froehlich fefeccec39 Remove weak CBC cipher suites and bump minimum TLS version to TLS 1.2 2021-02-16 15:31:53 +00:00
Bernhard Fröhlich c781938999 Merge pull request #11 from JonathonReinhart/minor-cleanup-fixes
Minor cleanup and fixes
2021-02-16 15:36:52 +01:00
Jonathon Reinhart 009ae8f73a hasher: Check number of arguments
This makes for a better user experience than a go segfault.
2021-02-15 00:18:15 -05:00
Jonathon Reinhart 70dfe6b128 Only call AuthLoadFile() once at startup 2021-02-15 00:08:37 -05:00
Jonathon Reinhart 7fa0eebf95 Simplify range code for setting up listeners 2021-02-15 00:00:38 -05:00
Jonathon Reinhart ecf830865c Add helpful log messages for various error cases 2021-02-14 23:49:17 -05:00
Jonathon Reinhart 4fd6bb1004 Refactor common code in listener setup 2021-02-14 23:30:31 -05:00
Jonathon Reinhart fd3f513b18 Don't run ListenAndServe in a goroutine
Any errors returned in ListenAndServe() (e.g. port already in use) will be
swallowed and not evident to the user.
2021-02-14 23:24:25 -05:00
Jonathon Reinhart b202a2209e Refactor out getTLSConfig() 2021-02-14 23:21:42 -05:00
Jonathon Reinhart 0e8986ca79 Expand allowedUsers email field to support comma-separated and domains (#9)
* Expand allowedUsers email field to support comma-separated and domains

Closes #8

* Refactor AuthFetch() to return AuthUser struct

Also, this breaks out a parseLine() function which can be easily tested.

* Ignore empty addrs after splitting commas

This ignores a trailing comma

* Add tests for auth parseLine()

* Update documentation in smtprelay.ini

* Fix bug where addrAllowed() was incorrectly case-sensitive

* Update allowedUsers allowed domain format to require leading @

This disambiguates a local user ('john.smith') from a domain ('example.com')
2021-02-14 22:16:18 +01:00
Bernhard Fröhlich 5c2e28ac36 Merge pull request #7 from JonathonReinhart/allow-empty-auth-email
Allow email field to be empty in allowedUsers file to accept any sending address
2021-02-09 14:18:06 +01:00
Jonathon Reinhart f33105f83c Allow email field to be empty in allowedUsers file
In this case, it is not checked.
2021-02-09 00:00:36 -05:00
Bernhard Fröhlich 9040a456cf Create codeql-analysis.yml 2020-09-28 21:26:31 +02:00
Bernhard Fröhlich d5c5e25d03 Merge pull request #4 from simon04/addresses
Fix typo "addresses"
2020-09-04 15:14:35 +02:00
Simon Legner 999cfea307 Fix typo "addresses" 2020-09-03 14:06:20 +02:00
Bernhard Froehlich f166c13350 Bump Version string 2020-06-14 18:51:16 +00:00
Bernhard Froehlich ed1c3a9888 Merge branch 'master' of github.com:decke/smtprelay 2020-06-07 17:19:52 +00:00
Bernhard Froehlich 6f3bd16988 The check if authentication was properly done is redundant now as of smtpd v0.2.0
See:	https://github.com/chrj/smtpd/commit/32be721d714c49abba11b0a3ba006ed6d8d50d8c
2020-06-07 17:17:28 +00:00
Bernhard Froehlich 4e0bf0908d Update dependencies 2020-06-07 17:15:57 +00:00
Bernhard Fröhlich 6662fb7155 Merge pull request #3 from nwillems/add-hasher-tool
Add helper to do hashing
2020-06-06 07:58:51 +02:00
Nicolai Willems 076fd65dea Use default cost for bcrypt 2020-06-05 22:48:33 +02:00
Bernhard Froehlich 880c3c365c Update dependencies and chrj/smtpd to latest master which contains a fix for us 2020-06-04 08:41:19 +00:00
Nicolai Willems 36673ae3f0 Add helper to do hashing 2020-05-24 21:07:29 +02:00
Bernhard Froehlich b42ad6ddc9 Add release script as requested in #2 2020-05-20 18:33:40 +00:00
Bernhard Froehlich 999ccab778 Fix spelling 2020-05-16 10:44:04 +00:00
Bernhard Froehlich 53c2c27647 Support LOGIN authentication on outgoing SMTP server
PR:		#1
Obtained from:	https://gist.github.com/andelf/5118732
2020-05-15 21:08:17 +00:00
Bernhard Froehlich 2afbe67407 Add Go Report Card badge 2020-05-11 13:56:24 +00:00
Bernhard Froehlich 00b96161b3 Remove duplication of TLS cipher suites for tls:// and startssl:// 2020-05-11 13:52:25 +00:00
Bernhard Froehlich e10cbcdbb0 Update crypto dependency 2020-05-11 13:33:33 +00:00
Bernhard Froehlich 0e643f7230 Update CI to go 1.14 2020-03-02 10:46:59 +00:00
Bernhard Froehlich 324585c63c Update list of cipher suites and add ciphers for TLS 1.3 2020-03-02 10:45:02 +00:00
Bernhard Froehlich 92cb02e46b Update crypto dependency 2020-03-02 10:44:35 +00:00
Bernhard Froehlich 62fcc11f61 Update crypto dependency 2019-12-16 19:20:48 +00:00
Bernhard Froehlich bfe8d39bb3 Merge branch 'master' of github.com:decke/smtprelay 2019-10-30 20:58:57 +00:00
Bernhard Froehlich cbb6f523c5 Update crypto dependency 2019-10-30 20:57:38 +00:00
Bernhard Fröhlich eb4a6b9eb6 Remove Travis CI 2019-10-20 13:24:42 +02:00
Bernhard Fröhlich 45db4ef786 Play with GitHub Actions 2019-10-20 13:21:39 +02:00
Bernhard Froehlich 946effcbcf Use Go 1.13 for CI 2019-10-14 09:55:05 +00:00
Bernhard Froehlich 118d1b88c2 Add Travis CI support and remove drone.yml 2019-10-14 09:51:09 +00:00
Bernhard Froehlich 5fd6aad9b1 Update project URL from code.bluelife.at to GitHub 2019-10-14 09:29:49 +00:00
Bernhard Froehlich 21b597f351 Bump version to 1.3.0 2019-09-07 11:31:28 +00:00
Bernhard Froehlich 5f82b4736c Update dependencies 2019-09-07 11:31:05 +00:00
Bernhard Fröhlich de430286b3 Merge branch 'remote-sender' of beppler/smtprelay into master 2019-09-07 06:20:28 +00:00
Carlos Alberto Costa Beppler 769193ea4d Adjust the description of remote_sender parameter.
It represents the e-mail address used while sending message to the outgoing SMTP server.
2019-09-06 21:00:35 -03:00
Carlos Alberto Costa Beppler 0b65e904d8 Allows specify the sender used on SMTP conversation with outgoing server. 2019-09-06 17:07:37 -03:00
Bernhard Froehlich 2c9645ac68 Add drone config 2019-05-09 08:17:45 +00:00
Bernhard Froehlich 770e819e2b Improve error checking 2019-02-21 08:29:49 +00:00
Bernhard Froehlich d11f8d81ea Fix formatting 2019-02-21 08:27:12 +00:00
Bernhard Froehlich 6270d75571 Improve TLS Config to prefer server ciphers, remove 3DES ciphers and require TLS 1.1 or higher 2019-01-08 15:09:29 +00:00
Bernhard Froehlich 92be537b25 Bump version to 1.2.0 2019-01-07 12:03:27 +00:00
Bernhard Froehlich b9d1663a18 Fixes for new authentication code 2019-01-07 11:52:25 +00:00
Bernhard Froehlich 3a96014c70 Revert package renaming 2018-12-29 13:08:10 +00:00
Bernhard Froehlich ade7cbca36 Rename to smtprelay 2018-12-29 12:39:56 +00:00
Bernhard Froehlich ab341c697d Code refactoring and rename package 2018-12-29 12:24:32 +00:00
Bernhard Froehlich ab850e8765 Use proper hostname instead of "localhost" for outgoing mails 2018-12-28 15:40:43 +00:00
Bernhard Froehlich a82b0faf96 Check sender email against auth file when user is authenticated 2018-12-28 15:30:55 +00:00
Bernhard Froehlich 76a04a2001 Authentication checker converted to store passwords as bcrypt hashes 2018-12-28 15:18:50 +00:00
Bernhard Froehlich 0df376e54a Bump to 1.1.1-dev 2018-12-26 21:20:51 +00:00
Bernhard Froehlich c05e5779a8 Bump version to 1.1.0 2018-12-26 21:07:01 +00:00
Bernhard Froehlich 3c1d683edc Update documentation for SMTPS support (outgoing) 2018-12-26 21:05:59 +00:00
Bernhard Froehlich 83b558239a Implement SMTPS support 2018-12-26 20:58:13 +00:00
Bernhard Froehlich 571e9ea942 Fork net/smtp package 2018-12-26 20:10:19 +00:00
Bernhard Froehlich 38aa14ddbf Ensure that authentication was successfull before we relay mails 2018-12-26 19:35:38 +00:00
Bernhard Froehlich 72dad9fb70 Handle error cases for user supplied regexp 2018-12-21 14:10:34 +00:00
Bernhard Froehlich e01895f25a Update README 2018-12-21 10:16:26 +00:00
Bernhard Froehlich a0e357c0de Add log message for successful delivery 2018-12-21 09:09:41 +00:00
Bernhard Froehlich 3d648a2ce7 Adjust SMTP error codes and messages to be aligned with the RFCs 2018-12-21 09:06:22 +00:00
Bernhard Froehlich 66fb86be7a Improve logging for received emails 2018-12-20 14:16:39 +00:00
Bernhard Froehlich 1d1c617161 Remove debug logging from smtpd 2018-12-20 13:53:06 +00:00
Bernhard Froehlich e87b1e7168 Implement authentication checker against a plaintext file 2018-12-20 13:52:19 +00:00
Bernhard Froehlich 926c681bdf Implement Sender and Recipient checker against regular expressions 2018-12-20 11:36:26 +00:00
Bernhard Froehlich dbe4c3bc50 More formatting fixes 2018-12-20 11:13:05 +00:00
Bernhard Froehlich 6427f0ec23 Fix whitespace 2018-12-20 11:11:28 +00:00
Bernhard Froehlich a5f3c2bd3f Implement connection checker to restrict which networks are allowed to send mails to us 2018-12-20 11:09:06 +00:00
Bernhard Froehlich b03ecbb02d Introduce VERSION to be able to identify ourselves 2018-12-20 09:58:04 +00:00
Bernhard Froehlich 3b643b3927 Improve logging and make sure smtpd is also logging 2018-12-20 09:44:41 +00:00
Bernhard Froehlich 88d85458dc Implement logfile and print on stdout and the logfile per default 2018-12-20 09:17:40 +00:00
Bernhard Froehlich 58d124b20d Switch dependency back to chrj/smtpd since my fixes were merged 2018-12-20 08:45:12 +00:00
Bernhard Froehlich 646103d157 Update smtpd dependency to latest commit 2018-12-14 13:37:22 +00:00
Bernhard Froehlich 5004734404 Temporary switch to my own fork of smtpd which contains a few fixes 2018-12-14 12:48:23 +00:00
Bernhard Froehlich 72fe85dea9 Add Received line to comply with RFC 5321 2018-12-14 11:26:48 +00:00
Bernhard Froehlich b55f5569b9 Repair totally broken STARTTLS and TLS listeners 2018-12-13 13:11:55 +00:00
19 changed files with 1620 additions and 99 deletions
+11
View File
@@ -0,0 +1,11 @@
version: 2
updates:
- package-ecosystem: "gomod"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "daily"
+66
View File
@@ -0,0 +1,66 @@
name: "CodeQL"
on:
push:
branches: [master]
pull_request:
# The branches below must be a subset of the branches above
branches: [master]
schedule:
- cron: '0 15 * * 5'
jobs:
analyze:
name: Analyze
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Override automatic language detection by changing the below list
# Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python']
language: ['go']
# Learn more...
# https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection
steps:
- name: Checkout repository
uses: actions/checkout@v2
with:
# We must fetch at least the immediate parents so that if this is
# a pull request then we can checkout the head.
fetch-depth: 2
# If this run was triggered by a pull request event, then checkout
# the head of the pull request instead of the merge commit.
- run: git checkout HEAD^2
if: ${{ github.event_name == 'pull_request' }}
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v1
with:
languages: ${{ matrix.language }}
# If you wish to specify custom queries, you can do so here or in a config file.
# By default, queries listed here will override any specified in a config file.
# Prefix the list here with "+" to use these queries and those in the config file.
# queries: ./path/to/local/query, your-org/your-repo/queries@main
# 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@v1
# ️ Command-line programs to run using the OS shell.
# 📚 https://git.io/JvXDl
# ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
# and modify them (or add more) to build your code if your project
# uses a compiled language
#- run: |
# make bootstrap
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
+24
View File
@@ -0,0 +1,24 @@
name: Go
on: [push]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.15
uses: actions/setup-go@v2.1.3
with:
go-version: 1.15
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v1
- name: Get dependencies
run: |
go get -v -t -d ./...
- name: Build
run: go build -v .
+30
View File
@@ -0,0 +1,30 @@
name: Release Go Binaries
on:
release:
types: [created]
jobs:
releases-matrix:
name: Release Go Binary
runs-on: ubuntu-latest
strategy:
matrix:
goos: [freebsd, linux, windows]
goarch: ["386", amd64]
steps:
- uses: actions/checkout@v2
- name: Set APP_VERSION env
run: echo APP_VERSION=$(echo ${GITHUB_REF} | rev | cut -d'/' -f 1 | rev ) >> ${GITHUB_ENV}
- name: Set BUILD_TIME env
run: echo BUILD_TIME=$(date) >> ${GITHUB_ENV}
- uses: wangyoucao577/go-release-action@v1.15
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
goversion: "https://golang.org/dl/go1.15.8.linux-amd64.tar.gz"
extra_files: LICENSE README.md smtprelay.ini
ldflags: -s -w -X "main.appVersion=${{ env.APP_VERSION }}" -X "main.buildTime=${{ env.BUILD_TIME }}"
+26 -5
View File
@@ -1,10 +1,31 @@
# smtp-proxy
# smtprelay
[![Go Report Card](https://goreportcard.com/badge/github.com/decke/smtprelay)](https://goreportcard.com/report/github.com/decke/smtprelay)
Simple Golang based SMTP relay/proxy server that accepts mail via SMTP
and forwards it directly to another SMTP server.
## Why another SMTP server?
Outgoing mails are usually send via SMTP to an MTA (Mail Transfer Agent)
which is one of Postfix, Exim, Sendmail or OpenSMTPD on UNIX/Linux in most
cases. You really don't want to setup and maintain any of those full blown
kitchensinks yourself because they are complex, fragile and hard to
configure.
My use case is simple. I need to send automatically generated mails from
cron via msmtp/sSMTP/dma, mails from various services and network printers
to GMail without giving away my GMail credentials to each device which
produces mail.
Simple Go SMTP relay/proxy server that accepts mails via SMTP
and forwards directly to another SMTP server.
## Main features
* STARTTLS/TLS support
* Supports SMTPS/TLS (465), STARTTLS (587) and unencrypted SMTP (25)
* Checks for sender, receiver, client IP
* Authentication support with file (LOGIN, PLAIN)
* Enforce encryption for authentication
* Forwards all mail to a smarthost (GMail, MailGun or any other SMTP server)
* Small codebase
* IPv6 support
* Forward to GMail, MailGun or any other SMTP server
+100
View File
@@ -0,0 +1,100 @@
package main
import (
"bufio"
"errors"
"os"
"strings"
"golang.org/x/crypto/bcrypt"
)
var (
filename string
)
type AuthUser struct {
username string
passwordHash string
allowedAddresses []string
}
func AuthLoadFile(file string) error {
f, err := os.Open(file)
if err != nil {
return err
}
f.Close()
filename = file
return nil
}
func AuthReady() bool {
return (filename != "")
}
// Split a string and ignore empty results
// https://stackoverflow.com/a/46798310/119527
func splitstr(s string, sep rune) []string {
return strings.FieldsFunc(s, func(c rune) bool { return c == sep })
}
func parseLine(line string) *AuthUser {
parts := strings.Fields(line)
if len(parts) < 2 || len(parts) > 3 {
return nil
}
user := AuthUser{
username: parts[0],
passwordHash: parts[1],
allowedAddresses: nil,
}
if len(parts) >= 3 {
user.allowedAddresses = splitstr(parts[2], ',')
}
return &user
}
func AuthFetch(username string) (*AuthUser, error) {
if !AuthReady() {
return nil, errors.New("Authentication file not specified. Call LoadFile() first")
}
file, err := os.Open(filename)
if err != nil {
return nil, err
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
user := parseLine(scanner.Text())
if user == nil {
continue
}
if strings.ToLower(username) != strings.ToLower(user.username) {
continue
}
return user, nil
}
return nil, errors.New("User not found")
}
func AuthCheckPassword(username string, secret string) error {
user, err := AuthFetch(username)
if err != nil {
return err
}
if bcrypt.CompareHashAndPassword([]byte(user.passwordHash), []byte(secret)) == nil {
return nil
}
return errors.New("Password invalid")
}
+89
View File
@@ -0,0 +1,89 @@
package main
import (
"testing"
)
func stringsEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
func TestParseLine(t *testing.T) {
var tests = []struct {
name string
expectFail bool
line string
username string
addrs []string
}{
{
name: "Empty line",
expectFail: true,
line: "",
},
{
name: "Too few fields",
expectFail: true,
line: "joe",
},
{
name: "Too many fields",
expectFail: true,
line: "joe xxx joe@example.com whatsthis",
},
{
name: "Normal case",
line: "joe xxx joe@example.com",
username: "joe",
addrs: []string{"joe@example.com"},
},
{
name: "No allowed addrs given",
line: "joe xxx",
username: "joe",
addrs: []string{},
},
{
name: "Trailing comma",
line: "joe xxx joe@example.com,",
username: "joe",
addrs: []string{"joe@example.com"},
},
{
name: "Multiple allowed addrs",
line: "joe xxx joe@example.com,@foo.example.com",
username: "joe",
addrs: []string{"joe@example.com", "@foo.example.com"},
},
}
for i, test := range tests {
t.Run(test.name, func(t *testing.T) {
user := parseLine(test.line)
if user == nil {
if !test.expectFail {
t.Errorf("parseLine() returned nil unexpectedly")
}
return
}
if user.username != test.username {
t.Errorf("Testcase %d: Incorrect username: expected %v, got %v",
i, test.username, user.username)
}
if !stringsEqual(user.allowedAddresses, test.addrs) {
t.Errorf("Testcase %d: Incorrect addresses: expected %v, got %v",
i, test.addrs, user.allowedAddresses)
}
})
}
}
+6
View File
@@ -0,0 +1,6 @@
To run the hasher, do like this
```bash
$ go run hasher.go hunter2
```
+22
View File
@@ -0,0 +1,22 @@
package main
import (
"fmt"
"os"
"golang.org/x/crypto/bcrypt"
)
func main() {
if len(os.Args) != 2 {
fmt.Fprintln(os.Stderr, "Usage: hasher PASSWORD")
os.Exit(1)
}
password := os.Args[1]
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
if err != nil {
fmt.Fprintln(os.Stderr, "Error generating hash: %s", err)
}
fmt.Println(string(hash))
}
+146
View File
@@ -0,0 +1,146 @@
package main
import (
"flag"
"net"
"regexp"
"net/smtp"
"github.com/vharitonsky/iniflags"
"github.com/sirupsen/logrus"
)
var (
appVersion = "unknown"
buildTime = "unknown"
)
var (
logFile = flag.String("logfile", "", "Path to logfile")
logFormat = flag.String("log_format", "default", "Log output format")
logLevel = flag.String("log_level", "info", "Minimum log level to output")
hostName = flag.String("hostname", "localhost.localdomain", "Server hostname")
welcomeMsg = flag.String("welcome_msg", "", "Welcome message for SMTP session")
listen = flag.String("listen", "127.0.0.1:25 [::1]:25", "Address and port to listen for incoming SMTP")
localCert = flag.String("local_cert", "", "SSL certificate for STARTTLS/TLS")
localKey = flag.String("local_key", "", "SSL private key for STARTTLS/TLS")
localForceTLS = flag.Bool("local_forcetls", false, "Force STARTTLS (needs local_cert and local_key)")
allowedNetsStr = flag.String("allowed_nets", "127.0.0.0/8 ::1/128", "Networks allowed to send mails")
allowedNets = []*net.IPNet{}
allowedSenderStr = flag.String("allowed_sender", "", "Regular expression for valid FROM EMail addresses")
allowedSender *regexp.Regexp
allowedRecipStr = flag.String("allowed_recipients", "", "Regular expression for valid TO EMail addresses")
allowedRecipients *regexp.Regexp
allowedUsers = flag.String("allowed_users", "", "Path to file with valid users/passwords")
remoteHost = flag.String("remote_host", "", "Outgoing SMTP server")
remoteUser = flag.String("remote_user", "", "Username for authentication on outgoing SMTP server")
remotePass = flag.String("remote_pass", "", "Password for authentication on outgoing SMTP server")
remoteAuthStr = flag.String("remote_auth", "none", "Auth method on outgoing SMTP server (none, plain, login)")
remoteAuth smtp.Auth
remoteSender = flag.String("remote_sender", "", "Sender e-mail address on outgoing SMTP server")
versionInfo = flag.Bool("version", false, "Show version information")
)
func setupAllowedNetworks() {
for _, netstr := range splitstr(*allowedNetsStr, ' ') {
baseIP, allowedNet, err := net.ParseCIDR(netstr)
if err != nil {
log.WithField("netstr", netstr).
WithError(err).
Fatal("Invalid CIDR notation in allowed_nets")
}
// Reject any network specification where any host bits are set,
// meaning the address refers to a host and not a network.
if !allowedNet.IP.Equal(baseIP) {
log.WithFields(logrus.Fields{
"given_net": netstr,
"proper_net": allowedNet,
}).Fatal("Invalid network in allowed_nets (host bits set)")
}
allowedNets = append(allowedNets, allowedNet)
}
}
func setupAllowedPatterns() {
var err error
if (*allowedSenderStr != "") {
allowedSender, err = regexp.Compile(*allowedSenderStr)
if err != nil {
log.WithField("allowed_sender", *allowedSenderStr).
WithError(err).
Fatal("allowed_sender pattern invalid")
}
}
if (*allowedRecipStr != "") {
allowedRecipients, err = regexp.Compile(*allowedRecipStr)
if err != nil {
log.WithField("allowed_recipients", *allowedRecipStr).
WithError(err).
Fatal("allowed_recipients pattern invalid")
}
}
}
func setupRemoteAuth() {
logger := log.WithField("remote_auth", *remoteAuthStr)
// Remote auth disabled?
if *remoteAuthStr == "" || *remoteAuthStr == "none" {
if *remoteUser != "" {
logger.Fatal("remote_user given but not used")
}
if *remotePass != "" {
logger.Fatal("remote_pass given but not used")
}
// No auth; use empty default
return
}
// We need a username, password, and remote host
if *remoteUser == "" {
logger.Fatal("remote_user required but empty")
}
if *remotePass == "" {
logger.Fatal("remote_pass required but empty")
}
if *remoteHost == "" {
logger.Fatal("remote_auth without remote_host is pointless")
}
host, _, err := net.SplitHostPort(*remoteHost)
if err != nil {
logger.WithField("remote_host", *remoteHost).
Fatal("Invalid remote_host")
}
switch *remoteAuthStr {
case "plain":
remoteAuth = smtp.PlainAuth("", *remoteUser, *remotePass, host)
case "login":
remoteAuth = LoginAuth(*remoteUser, *remotePass)
default:
logger.Fatal("Invalid remote_auth type")
}
}
func ConfigLoad() {
iniflags.Parse()
// Set up logging as soon as possible
setupLogger()
if (*remoteHost == "") {
log.Warn("remote_host not set; mail will not be forwarded!")
}
setupAllowedNetworks()
setupAllowedPatterns()
setupRemoteAuth()
}
+7 -2
View File
@@ -1,6 +1,11 @@
module code.bluelife.at/decke/smtp-proxy
module github.com/decke/smtprelay
require (
github.com/chrj/smtpd v0.1.1
github.com/chrj/smtpd v0.3.0
github.com/google/uuid v1.2.0
github.com/sirupsen/logrus v1.8.1
github.com/vharitonsky/iniflags v0.0.0-20180513140207-a33cd0b5f3de
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad
)
go 1.13
+21 -2
View File
@@ -1,5 +1,24 @@
github.com/chrj/smtpd v0.1.1 h1:Jk9ZimOh4njDpMbDLPmdQX+x9AUYGrfE68EyjuQDPjk=
github.com/chrj/smtpd v0.1.1/go.mod h1:jt4ydELuZmqhn9hn3YpEPV1dY00aOB+Q1nWXnBDFKeY=
github.com/chrj/smtpd v0.3.0 h1:cw1LSHDOz7N3XbkcZSF/bue9dh7ATKk5ZksfBztV6b0=
github.com/chrj/smtpd v0.3.0/go.mod h1:1hmG9KbrE10JG1SmvG79Krh4F6713oUrw2+gRp1oSYk=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/eaigner/dkim v0.0.0-20150301120808-6fe4a7ee9cfb/go.mod h1:FSCIHbrqk7D01Mj8y/jW+NS1uoCerr+ad+IckTHTFf4=
github.com/google/uuid v1.2.0 h1:qJYtXnJRWmpe7m/3XlyhrsLrEURqHRM2kxzoxXqyUDs=
github.com/google/uuid v1.2.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/vharitonsky/iniflags v0.0.0-20180513140207-a33cd0b5f3de h1:fkw+7JkxF3U1GzQoX9h69Wvtvxajo5Rbzy6+YMMzPIg=
github.com/vharitonsky/iniflags v0.0.0-20180513140207-a33cd0b5f3de/go.mod h1:irMhzlTz8+fVFj6CH2AN2i+WI5S6wWFtK3MBCIxIpyI=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+60
View File
@@ -0,0 +1,60 @@
package main
import (
"fmt"
"os"
"time"
"github.com/sirupsen/logrus"
)
var (
log *logrus.Logger
)
func setupLogger() {
log = logrus.New()
// Handle logfile
if (*logFile == "") {
log.SetOutput(os.Stderr)
} else {
writer, err := os.OpenFile(*logFile, os.O_CREATE|os.O_RDWR|os.O_APPEND, 0600)
if err != nil {
fmt.Printf("cannot open log file: %s\n", err)
os.Exit(1)
}
log.SetOutput(writer)
}
// Handle log_format
switch *logFormat {
case "json":
log.SetFormatter(&logrus.JSONFormatter{
TimestampFormat: time.RFC3339Nano,
DisableHTMLEscape: true,
})
case "plain":
log.SetFormatter(&logrus.TextFormatter{
DisableTimestamp: true,
})
case "", "default":
log.SetFormatter(&logrus.TextFormatter{
FullTimestamp: true,
})
default:
fmt.Fprintf(os.Stderr, "Invalid log_format: %s\n", *logFormat)
os.Exit(1)
}
// Handle log_level
level, err := logrus.ParseLevel(*logLevel)
if err != nil {
level = logrus.InfoLevel
log.WithField("given_level", *logLevel).
Warn("could not parse log level, defaulting to 'info'")
}
log.SetLevel(level)
}
+343 -53
View File
@@ -2,91 +2,381 @@ package main
import (
"crypto/tls"
"flag"
"log"
"fmt"
"net"
"net/smtp"
"net/textproto"
"os"
"os/signal"
"strings"
"time"
"syscall"
"github.com/chrj/smtpd"
"github.com/vharitonsky/iniflags"
"github.com/google/uuid"
"github.com/sirupsen/logrus"
)
var (
hostName = flag.String("hostname", "localhost.localdomain", "Server hostname")
welcomeMsg = flag.String("welcome_msg", "", "Welcome message for SMTP session")
listen = flag.String("listen", "127.0.0.1:25 [::1]:25", "Address and port to listen for incoming SMTP")
localCert = flag.String("local_cert", "", "SSL certificate for STARTTLS/TLS")
localKey = flag.String("local_key", "", "SSL private key for STARTTLS/TLS")
localForceTLS = flag.Bool("local_forcetls", false, "Force STARTTLS (needs local_cert and local_key)")
remoteHost = flag.String("remote_host", "smtp.gmail.com:587", "Outgoing SMTP server")
remoteUser = flag.String("remote_user", "", "Username for authentication on outgoing SMTP server")
remotePass = flag.String("remote_pass", "", "Password for authentication on outgoing SMTP server")
)
func connectionChecker(peer smtpd.Peer) error {
// This can't panic because we only have TCP listeners
peerIP := peer.Addr.(*net.TCPAddr).IP
func handler(peer smtpd.Peer, env smtpd.Envelope) error {
var auth smtp.Auth
host, _, _ := net.SplitHostPort(*remoteHost)
if *remoteUser != "" && *remotePass != "" {
auth = smtp.PlainAuth("", *remoteUser, *remotePass, host)
if len(allowedNets) == 0 {
// Special case: empty string means allow everything
return nil
}
return smtp.SendMail(
for _, allowedNet := range allowedNets {
if allowedNet.Contains(peerIP) {
return nil
}
}
log.WithFields(logrus.Fields{
"ip": peerIP,
}).Warn("Connection refused from address outside of allowed_nets")
return smtpd.Error{Code: 421, Message: "Denied"}
}
func addrAllowed(addr string, allowedAddrs []string) bool {
if allowedAddrs == nil {
// If absent, all addresses are allowed
return true
}
addr = strings.ToLower(addr)
// Extract optional domain part
domain := ""
if idx := strings.LastIndex(addr, "@"); idx != -1 {
domain = strings.ToLower(addr[idx+1:])
}
// Test each address from allowedUsers file
for _, allowedAddr := range allowedAddrs {
allowedAddr = strings.ToLower(allowedAddr)
// Three cases for allowedAddr format:
if idx := strings.Index(allowedAddr, "@"); idx == -1 {
// 1. local address (no @) -- must match exactly
if allowedAddr == addr {
return true
}
} else {
if idx != 0 {
// 2. email address (user@domain.com) -- must match exactly
if allowedAddr == addr {
return true
}
} else {
// 3. domain (@domain.com) -- must match addr domain
allowedDomain := allowedAddr[idx+1:]
if allowedDomain == domain {
return true
}
}
}
}
return false
}
func senderChecker(peer smtpd.Peer, addr string) error {
// check sender address from auth file if user is authenticated
if *allowedUsers != "" && peer.Username != "" {
user, err := AuthFetch(peer.Username)
if err != nil {
// Shouldn't happen: authChecker already validated username+password
log.WithFields(logrus.Fields{
"peer": peer.Addr,
"username": peer.Username,
}).WithError(err).Warn("could not fetch auth user")
return smtpd.Error{Code: 451, Message: "Bad sender address"}
}
if !addrAllowed(addr, user.allowedAddresses) {
log.WithFields(logrus.Fields{
"peer": peer.Addr,
"username": peer.Username,
"sender_address": addr,
}).Warn("sender address not allowed for authenticated user")
return smtpd.Error{Code: 451, Message: "Bad sender address"}
}
}
if allowedSender == nil {
// Any sender is permitted
return nil
}
if allowedSender.MatchString(addr) {
// Permitted by regex
return nil
}
log.WithFields(logrus.Fields{
"sender_address": addr,
"peer": peer.Addr,
}).Warn("sender address not allowed by allowed_sender pattern")
return smtpd.Error{Code: 451, Message: "Bad sender address"}
}
func recipientChecker(peer smtpd.Peer, addr string) error {
if allowedRecipients == nil {
// Any recipient is permitted
return nil
}
if allowedRecipients.MatchString(addr) {
// Permitted by regex
return nil
}
log.WithFields(logrus.Fields{
"peer": peer.Addr,
"recipient_address": addr,
}).Warn("recipient address not allowed by allowed_recipients pattern")
return smtpd.Error{Code: 451, Message: "Bad recipient address"}
}
func authChecker(peer smtpd.Peer, username string, password string) error {
err := AuthCheckPassword(username, password)
if err != nil {
log.WithFields(logrus.Fields{
"peer": peer.Addr,
"username": username,
}).WithError(err).Warn("auth error")
return smtpd.Error{Code: 535, Message: "Authentication credentials invalid"}
}
return nil
}
func mailHandler(peer smtpd.Peer, env smtpd.Envelope) error {
peerIP := ""
if addr, ok := peer.Addr.(*net.TCPAddr); ok {
peerIP = addr.IP.String()
}
logger := log.WithFields(logrus.Fields{
"from": env.Sender,
"to": env.Recipients,
"peer": peerIP,
"host": *remoteHost,
"uuid": generateUUID(),
})
if (*remoteHost == "") {
logger.Warning("remote_host not set; discarding mail")
return nil
}
logger.Info("delivering mail from peer using smarthost")
env.AddReceivedLine(peer)
var sender string
if *remoteSender == "" {
sender = env.Sender
} else {
sender = *remoteSender
}
err := SendMail(
*remoteHost,
auth,
env.Sender,
remoteAuth,
sender,
env.Recipients,
env.Data,
)
if err != nil {
var smtpError smtpd.Error
switch err.(type) {
case *textproto.Error:
err := err.(*textproto.Error)
smtpError = smtpd.Error{Code: err.Code, Message: err.Msg}
logger.WithFields(logrus.Fields{
"err_code": err.Code,
"err_msg": err.Msg,
}).Error("delivery failed")
default:
smtpError = smtpd.Error{Code: 554, Message: "Forwarding failed"}
logger.WithError(err).
Error("delivery failed")
}
return smtpError
}
logger.Debug("delivery successful")
return nil
}
func generateUUID() string {
uniqueID, err := uuid.NewRandom()
if err != nil {
log.WithError(err).
Error("could not generate UUIDv4")
return ""
}
return uniqueID.String()
}
func getTLSConfig() *tls.Config {
// Ciphersuites as defined in stock Go but without 3DES and RC4
// https://golang.org/src/crypto/tls/cipher_suites.go
var tlsCipherSuites = []uint16{
tls.TLS_AES_128_GCM_SHA256,
tls.TLS_AES_256_GCM_SHA384,
tls.TLS_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
tls.TLS_RSA_WITH_AES_128_GCM_SHA256, // does not provide PFS
tls.TLS_RSA_WITH_AES_256_GCM_SHA384, // does not provide PFS
}
if *localCert == "" || *localKey == "" {
log.WithFields(logrus.Fields{
"cert_file": *localCert,
"key_file": *localKey,
}).Fatal("TLS certificate/key file not defined in config")
}
cert, err := tls.LoadX509KeyPair(*localCert, *localKey)
if err != nil {
log.WithField("error", err).
Fatal("cannot load X509 keypair")
}
return &tls.Config{
PreferServerCipherSuites: true,
MinVersion: tls.VersionTLS12,
CipherSuites: tlsCipherSuites,
Certificates: []tls.Certificate{cert},
}
}
func main() {
ConfigLoad()
iniflags.Parse()
if *versionInfo {
fmt.Printf("smtprelay/%s (%s)\n", appVersion, buildTime)
os.Exit(0)
}
listeners := strings.Split(*listen, " ")
log.WithField("version", appVersion).
Debug("starting smtprelay")
for i := range(listeners) {
listener := listeners[i]
// Load allowed users file
if *allowedUsers != "" {
err := AuthLoadFile(*allowedUsers)
if err != nil {
log.WithField("file", *allowedUsers).
WithError(err).
Fatal("cannot load allowed users file")
}
}
var servers []*smtpd.Server
// Create a server for each desired listen address
for _, listenAddr := range strings.Split(*listen, " ") {
server := &smtpd.Server{
Hostname: *hostName,
WelcomeMessage: *welcomeMsg,
Handler: handler,
Hostname: *hostName,
WelcomeMessage: *welcomeMsg,
ConnectionChecker: connectionChecker,
SenderChecker: senderChecker,
RecipientChecker: recipientChecker,
Handler: mailHandler,
}
if strings.Index(listeners[i], "://") == -1 {
;
} else if strings.HasPrefix(listeners[i], "tls://") || strings.HasPrefix(listeners[i], "starttls://") {
if *allowedUsers != "" {
server.Authenticator = authChecker
}
listener = strings.TrimPrefix(listener, "tls://")
listener = strings.TrimPrefix(listener, "starttls://")
var lsnr net.Listener
var err error
if *localCert == "" || *localKey == "" {
log.Fatal("TLS certificate/key not defined in config")
}
if strings.Index(listenAddr, "://") == -1 {
log.WithField("address", listenAddr).
Info("listening on address")
cert, err := tls.LoadX509KeyPair(*localCert, *localKey)
if err != nil {
log.Fatal(err)
}
lsnr, err = net.Listen("tcp", listenAddr)
} else if strings.HasPrefix(listenAddr, "starttls://") {
listenAddr = strings.TrimPrefix(listenAddr, "starttls://")
server.TLSConfig = getTLSConfig()
server.ForceTLS = *localForceTLS
server.TLSConfig = &tls.Config {
Certificates: [] tls.Certificate{cert},
}
log.WithField("address", listenAddr).
Info("listening on address (STARTTLS)")
lsnr, err = net.Listen("tcp", listenAddr)
} else if strings.HasPrefix(listenAddr, "tls://") {
listenAddr = strings.TrimPrefix(listenAddr, "tls://")
server.TLSConfig = getTLSConfig()
log.WithField("address", listenAddr).
Info("listening on address (TLS)")
lsnr, err = tls.Listen("tcp", listenAddr, server.TLSConfig)
} else {
log.Fatal("Unknown protocol in listener ", listener)
log.WithField("address", listenAddr).
Fatal("unknown protocol in listen address")
}
log.Printf("Listen on %s ...\n", listener)
go server.ListenAndServe(listener)
if err != nil {
log.WithFields(logrus.Fields{
"address": listenAddr,
}).WithError(err).Fatal("error starting listener")
}
servers = append(servers, server)
go func() {
server.Serve(lsnr)
}()
}
for true {
time.Sleep(time.Minute)
handleSignals()
// First close the listeners
for _, server := range servers {
logger := log.WithField("address", server.Address())
logger.Debug("Shutting down server")
err := server.Shutdown(false)
if err != nil {
logger.WithError(err).
Warning("Shutdown failed")
}
}
// Then wait for the clients to exit
for _, server := range servers {
logger := log.WithField("address", server.Address())
logger.Debug("Waiting for server")
err := server.Wait()
if err != nil {
logger.WithError(err).
Warning("Wait failed")
}
}
log.Debug("done")
}
func handleSignals() {
// Wait for SIGINT, SIGQUIT, or SIGTERM
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGQUIT, syscall.SIGTERM)
sig := <-sigs
log.WithField("signal", sig).
Info("shutting down in response to received signal")
}
+94
View File
@@ -0,0 +1,94 @@
package main
import (
"testing"
)
func TestAddrAllowedNoDomain(t *testing.T) {
allowedAddrs := []string{"joe@abc.com"}
if addrAllowed("bob.com", allowedAddrs) {
t.FailNow()
}
}
func TestAddrAllowedSingle(t *testing.T) {
allowedAddrs := []string{"joe@abc.com"}
if !addrAllowed("joe@abc.com", allowedAddrs) {
t.FailNow()
}
if addrAllowed("bob@abc.com", allowedAddrs) {
t.FailNow()
}
}
func TestAddrAllowedDifferentCase(t *testing.T) {
allowedAddrs := []string{"joe@abc.com"}
testAddrs := []string{
"joe@ABC.com",
"Joe@abc.com",
"JOE@abc.com",
"JOE@ABC.COM",
}
for _, addr := range testAddrs {
if !addrAllowed(addr, allowedAddrs) {
t.Errorf("Address %v not allowed, but should be", addr)
}
}
}
func TestAddrAllowedLocal(t *testing.T) {
allowedAddrs := []string{"joe"}
if !addrAllowed("joe", allowedAddrs) {
t.FailNow()
}
if addrAllowed("bob", allowedAddrs) {
t.FailNow()
}
}
func TestAddrAllowedMulti(t *testing.T) {
allowedAddrs := []string{"joe@abc.com", "bob@def.com"}
if !addrAllowed("joe@abc.com", allowedAddrs) {
t.FailNow()
}
if !addrAllowed("bob@def.com", allowedAddrs) {
t.FailNow()
}
if addrAllowed("bob@abc.com", allowedAddrs) {
t.FailNow()
}
}
func TestAddrAllowedSingleDomain(t *testing.T) {
allowedAddrs := []string{"@abc.com"}
if !addrAllowed("joe@abc.com", allowedAddrs) {
t.FailNow()
}
if addrAllowed("joe@def.com", allowedAddrs) {
t.FailNow()
}
}
func TestAddrAllowedMixed(t *testing.T) {
allowedAddrs := []string{"app", "app@example.com", "@appsrv.example.com"}
if !addrAllowed("app", allowedAddrs) {
t.FailNow()
}
if !addrAllowed("app@example.com", allowedAddrs) {
t.FailNow()
}
if addrAllowed("ceo@example.com", allowedAddrs) {
t.FailNow()
}
if !addrAllowed("root@appsrv.example.com", allowedAddrs) {
t.FailNow()
}
if !addrAllowed("dev@appsrv.example.com", allowedAddrs) {
t.FailNow()
}
if addrAllowed("appsrv@example.com", allowedAddrs) {
t.FailNow()
}
}
+11
View File
@@ -0,0 +1,11 @@
[Unit]
Description=SMTP Relay
After=network-online.target
Wants=network-online.target
[Service]
Type=simple
ExecStart=/opt/smtprelay/bin/smtprelay -config /etc/smtprelay/smtprelay.ini
[Install]
WantedBy=multi-user.target
-37
View File
@@ -1,37 +0,0 @@
; smtp-proxy configuration
; Hostname for this SMTP server
;hostname = "localhost.localdomain"
; Welcome message for clients
;welcome_msg = "<hostname> ESMTP ready."
; Listen on the following addresses for incoming
; unencrypted connections.
;listen = 127.0.0.1:25 [::1]:25
; STARTTLS and TLS are also supported but need a
; SSL certificate and key.
;listen = tls://127.0.0.1:465 tls://[::1]:465
;listen = starttls://127.0.0.1:587 starttls://[::1]:587
;local_cert = smtpd.pem
;local_key = smtpd.key
; Enforce encrypted connection on STARTTLS ports before
; accepting mails from client.
;local_forcetls = false
; Relay all mails to this SMTP server
; GMail
;remote_host = smtp.gmail.com:587
; Mailgun.org
;remote_host = smtp.mailgun.org:587
; Mailjet.com
;remote_host = in-v3.mailjet.com:587
; Authentication credentials on outgoing SMTP server
;remote_user =
;remote_pass =
+484
View File
@@ -0,0 +1,484 @@
// Copyright 2010 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package smtp implements the Simple Mail Transfer Protocol as defined in RFC 5321.
// It also implements the following extensions:
// 8BITMIME RFC 1652
// AUTH RFC 2554
// STARTTLS RFC 3207
// Additional extensions may be handled by clients.
//
// The smtp package is frozen and is not accepting new features.
// Some external packages provide more functionality. See:
//
// https://godoc.org/?q=smtp
package main
import (
"crypto/tls"
"encoding/base64"
"errors"
"fmt"
"io"
"net"
"net/smtp"
"net/textproto"
"strings"
)
// A Client represents a client connection to an SMTP server.
type Client struct {
// Text is the textproto.Conn used by the Client. It is exported to allow for
// clients to add extensions.
Text *textproto.Conn
// keep a reference to the connection so it can be used to create a TLS
// connection later
conn net.Conn
// whether the Client is using TLS
tls bool
serverName string
// map of supported extensions
ext map[string]string
// supported auth mechanisms
auth []string
localName string // the name to use in HELO/EHLO
didHello bool // whether we've said HELO/EHLO
helloError error // the error from the hello
}
// Dial returns a new Client connected to an SMTP server at addr.
// The addr must include a port, as in "mail.example.com:smtp".
func Dial(addr string) (*Client, error) {
conn, err := net.Dial("tcp", addr)
if err != nil {
return nil, err
}
host, _, _ := net.SplitHostPort(addr)
return NewClient(conn, host)
}
// NewClient returns a new Client using an existing connection and host as a
// server name to be used when authenticating.
func NewClient(conn net.Conn, host string) (*Client, error) {
text := textproto.NewConn(conn)
_, _, err := text.ReadResponse(220)
if err != nil {
text.Close()
return nil, err
}
c := &Client{Text: text, conn: conn, serverName: host, localName: *hostName}
_, c.tls = conn.(*tls.Conn)
return c, nil
}
// Close closes the connection.
func (c *Client) Close() error {
return c.Text.Close()
}
// hello runs a hello exchange if needed.
func (c *Client) hello() error {
if !c.didHello {
c.didHello = true
err := c.ehlo()
if err != nil {
c.helloError = c.helo()
}
}
return c.helloError
}
// Hello sends a HELO or EHLO to the server as the given host name.
// Calling this method is only necessary if the client needs control
// over the host name used. The client will introduce itself as "localhost"
// automatically otherwise. If Hello is called, it must be called before
// any of the other methods.
func (c *Client) Hello(localName string) error {
if err := validateLine(localName); err != nil {
return err
}
if c.didHello {
return errors.New("smtp: Hello called after other methods")
}
c.localName = localName
return c.hello()
}
// cmd is a convenience function that sends a command and returns the response
func (c *Client) cmd(expectCode int, format string, args ...interface{}) (int, string, error) {
id, err := c.Text.Cmd(format, args...)
if err != nil {
return 0, "", err
}
c.Text.StartResponse(id)
defer c.Text.EndResponse(id)
code, msg, err := c.Text.ReadResponse(expectCode)
return code, msg, err
}
// helo sends the HELO greeting to the server. It should be used only when the
// server does not support ehlo.
func (c *Client) helo() error {
c.ext = nil
_, _, err := c.cmd(250, "HELO %s", c.localName)
return err
}
// ehlo sends the EHLO (extended hello) greeting to the server. It
// should be the preferred greeting for servers that support it.
func (c *Client) ehlo() error {
_, msg, err := c.cmd(250, "EHLO %s", c.localName)
if err != nil {
return err
}
ext := make(map[string]string)
extList := strings.Split(msg, "\n")
if len(extList) > 1 {
extList = extList[1:]
for _, line := range extList {
args := strings.SplitN(line, " ", 2)
if len(args) > 1 {
ext[args[0]] = args[1]
} else {
ext[args[0]] = ""
}
}
}
if mechs, ok := ext["AUTH"]; ok {
c.auth = strings.Split(mechs, " ")
}
c.ext = ext
return err
}
// StartTLS sends the STARTTLS command and encrypts all further communication.
// Only servers that advertise the STARTTLS extension support this function.
func (c *Client) StartTLS(config *tls.Config) error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(220, "STARTTLS")
if err != nil {
return err
}
c.conn = tls.Client(c.conn, config)
c.Text = textproto.NewConn(c.conn)
c.tls = true
return c.ehlo()
}
// TLSConnectionState returns the client's TLS connection state.
// The return values are their zero values if StartTLS did
// not succeed.
func (c *Client) TLSConnectionState() (state tls.ConnectionState, ok bool) {
tc, ok := c.conn.(*tls.Conn)
if !ok {
return
}
return tc.ConnectionState(), true
}
// Verify checks the validity of an email address on the server.
// If Verify returns nil, the address is valid. A non-nil return
// does not necessarily indicate an invalid address. Many servers
// will not verify addresses for security reasons.
func (c *Client) Verify(addr string) error {
if err := validateLine(addr); err != nil {
return err
}
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(250, "VRFY %s", addr)
return err
}
// Auth authenticates a client using the provided authentication mechanism.
// A failed authentication closes the connection.
// Only servers that advertise the AUTH extension support this function.
func (c *Client) Auth(a smtp.Auth) error {
if err := c.hello(); err != nil {
return err
}
encoding := base64.StdEncoding
mech, resp, err := a.Start(&smtp.ServerInfo{c.serverName, c.tls, c.auth})
if err != nil {
c.Quit()
return err
}
resp64 := make([]byte, encoding.EncodedLen(len(resp)))
encoding.Encode(resp64, resp)
code, msg64, err := c.cmd(0, strings.TrimSpace(fmt.Sprintf("AUTH %s %s", mech, resp64)))
for err == nil {
var msg []byte
switch code {
case 334:
msg, err = encoding.DecodeString(msg64)
case 235:
// the last message isn't base64 because it isn't a challenge
msg = []byte(msg64)
default:
err = &textproto.Error{Code: code, Msg: msg64}
}
if err == nil {
resp, err = a.Next(msg, code == 334)
}
if err != nil {
// abort the AUTH
c.cmd(501, "*")
c.Quit()
break
}
if resp == nil {
break
}
resp64 = make([]byte, encoding.EncodedLen(len(resp)))
encoding.Encode(resp64, resp)
code, msg64, err = c.cmd(0, string(resp64))
}
return err
}
// Mail issues a MAIL command to the server using the provided email address.
// If the server supports the 8BITMIME extension, Mail adds the BODY=8BITMIME
// parameter. If the server supports the SMTPUTF8 extension, Mail adds the
// SMTPUTF8 parameter.
// This initiates a mail transaction and is followed by one or more Rcpt calls.
func (c *Client) Mail(from string) error {
if err := validateLine(from); err != nil {
return err
}
if err := c.hello(); err != nil {
return err
}
cmdStr := "MAIL FROM:<%s>"
if c.ext != nil {
if _, ok := c.ext["8BITMIME"]; ok {
cmdStr += " BODY=8BITMIME"
}
if _, ok := c.ext["SMTPUTF8"]; ok {
cmdStr += " SMTPUTF8"
}
}
_, _, err := c.cmd(250, cmdStr, from)
return err
}
// Rcpt issues a RCPT command to the server using the provided email address.
// A call to Rcpt must be preceded by a call to Mail and may be followed by
// a Data call or another Rcpt call.
func (c *Client) Rcpt(to string) error {
if err := validateLine(to); err != nil {
return err
}
_, _, err := c.cmd(25, "RCPT TO:<%s>", to)
return err
}
type dataCloser struct {
c *Client
io.WriteCloser
}
func (d *dataCloser) Close() error {
d.WriteCloser.Close()
_, _, err := d.c.Text.ReadResponse(250)
return err
}
// 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
// close the writer before calling any more methods on c. A call to
// Data must be preceded by one or more calls to Rcpt.
func (c *Client) Data() (io.WriteCloser, error) {
_, _, err := c.cmd(354, "DATA")
if err != nil {
return nil, err
}
return &dataCloser{c, c.Text.DotWriter()}, nil
}
var testHookStartTLS func(*tls.Config) // nil, except for tests
// SendMail connects to the server at addr with TLS when port 465 or
// smtps is specified or unencrypted otherwise and switches to TLS if
// possible, authenticates with the optional mechanism a if possible,
// and then sends an email from address from, to addresses to, with
// message msg.
// The addr must include a port, as in "mail.example.com:smtp".
//
// The addresses in the to parameter are the SMTP RCPT addresses.
//
// The msg parameter should be an RFC 822-style email with headers
// first, a blank line, and then the message body. The lines of msg
// should be CRLF terminated. The msg headers should usually include
// fields such as "From", "To", "Subject", and "Cc". Sending "Bcc"
// messages is accomplished by including an email address in the to
// parameter but not including it in the msg headers.
//
// The SendMail function and the net/smtp package are low-level
// mechanisms and provide no support for DKIM signing, MIME
// attachments (see the mime/multipart package), or other mail
// functionality. Higher-level packages exist outside of the standard
// library.
func SendMail(addr string, a smtp.Auth, from string, to []string, msg []byte) error {
if err := validateLine(from); err != nil {
return err
}
for _, recp := range to {
if err := validateLine(recp); err != nil {
return err
}
}
host, port, err := net.SplitHostPort(addr)
if err != nil {
return err
}
var c *Client
if port == "465" || port == "smtps" {
config := &tls.Config{ServerName: host}
conn, err := tls.Dial("tcp", addr, config)
if err != nil {
return err
}
defer conn.Close()
c, err = NewClient(conn, host)
if err != nil {
return err
}
if err = c.hello(); err != nil {
return err
}
} else {
c, err = Dial(addr)
if err != nil {
return err
}
defer c.Close()
if err = c.hello(); err != nil {
return err
}
if ok, _ := c.Extension("STARTTLS"); ok {
config := &tls.Config{ServerName: c.serverName}
if testHookStartTLS != nil {
testHookStartTLS(config)
}
if err = c.StartTLS(config); err != nil {
return err
}
}
}
if a != nil && c.ext != nil {
if _, ok := c.ext["AUTH"]; !ok {
return errors.New("smtp: server doesn't support AUTH")
}
if err = c.Auth(a); err != nil {
return err
}
}
if err = c.Mail(from); err != nil {
return err
}
for _, addr := range to {
if err = c.Rcpt(addr); err != nil {
return err
}
}
w, err := c.Data()
if err != nil {
return err
}
_, err = w.Write(msg)
if err != nil {
return err
}
err = w.Close()
if err != nil {
return err
}
return c.Quit()
}
// Extension reports whether an extension is support by the server.
// The extension name is case-insensitive. If the extension is supported,
// Extension also returns a string that contains any parameters the
// server specifies for the extension.
func (c *Client) Extension(ext string) (bool, string) {
if err := c.hello(); err != nil {
return false, ""
}
if c.ext == nil {
return false, ""
}
ext = strings.ToUpper(ext)
param, ok := c.ext[ext]
return ok, param
}
// Reset sends the RSET command to the server, aborting the current mail
// transaction.
func (c *Client) Reset() error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(250, "RSET")
return err
}
// Noop sends the NOOP command to the server. It does nothing but check
// that the connection to the server is okay.
func (c *Client) Noop() error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(250, "NOOP")
return err
}
// Quit sends the QUIT command and closes the connection to the server.
func (c *Client) Quit() error {
if err := c.hello(); err != nil {
return err
}
_, _, err := c.cmd(221, "QUIT")
if err != nil {
return err
}
return c.Text.Close()
}
// validateLine checks to see if a line has CR or LF as per RFC 5321
func validateLine(line string) error {
if strings.ContainsAny(line, "\n\r") {
return errors.New("smtp: A line must not contain CR or LF")
}
return nil
}
// LOGIN authentication
type loginAuth struct {
username, password string
}
func LoginAuth(username, password string) smtp.Auth {
return &loginAuth{username, password}
}
func (a *loginAuth) Start(server *smtp.ServerInfo) (string, []byte, error) {
return "LOGIN", []byte{}, nil
}
func (a *loginAuth) Next(fromServer []byte, more bool) ([]byte, error) {
if more {
switch string(fromServer) {
case "Username:":
return []byte(a.username), nil
case "Password:":
return []byte(a.password), nil
default:
return nil, errors.New("Unknown fromServer")
}
}
return nil, nil
}
+80
View File
@@ -0,0 +1,80 @@
; smtprelay configuration
; Logfile (blank/default is stderr)
;logfile =
; Log format: default, plain (no timestamp), json
;log_format = "default"
; Log level: panic, fatal, error, warn, info, debug, trace
;log_level = "info"
; Hostname for this SMTP server
;hostname = "localhost.localdomain"
; Welcome message for clients
;welcome_msg = "<hostname> ESMTP ready."
; Listen on the following addresses for incoming
; unencrypted connections.
;listen = 127.0.0.1:25 [::1]:25
; STARTTLS and TLS are also supported but need a
; SSL certificate and key.
;listen = tls://127.0.0.1:465 tls://[::1]:465
;listen = starttls://127.0.0.1:587 starttls://[::1]:587
;local_cert = smtpd.pem
;local_key = smtpd.key
; Enforce encrypted connection on STARTTLS ports before
; accepting mails from client.
;local_forcetls = false
; Networks that are allowed to send mails to us
; Defaults to localhost. If set to "", then any address is allowed.
;allowed_nets = 127.0.0.0/8 ::1/128
; Regular expression for valid FROM EMail addresses
; If set to "", then any sender is permitted.
; Example: ^(.*)@localhost.localdomain$
;allowed_sender =
; Regular expression for valid TO EMail addresses
; If set to "", then any recipient is permitted.
; Example: ^(.*)@localhost.localdomain$
;allowed_recipients =
; File which contains username and password used for
; authentication before they can send mail.
; File format: username bcrypt-hash [email[,email[,...]]]
; username: The SMTP auth username
; bcrypt-hash: The bcrypt hash of the pasword (generate with "./hasher password")
; email: Comma-separated list of allowed "from" addresses:
; - If omitted, user can send from any address
; - If @domain.com is given, user can send from any address @domain.com
; - Otherwise, email address must match exactly (case-insensitive)
; E.g. "app@example.com,@appsrv.example.com"
;allowed_users =
; Relay all mails to this SMTP server.
; If not set, mails are discarded.
; GMail
;remote_host = smtp.gmail.com:587
; Mailgun.org
;remote_host = smtp.mailgun.org:587
; Mailjet.com
;remote_host = in-v3.mailjet.com:587
; Authentication credentials on outgoing SMTP server
;remote_user =
;remote_pass =
; Authentication method on outgoing SMTP server
; (none, plain, login)
;remote_auth = none
; Sender e-mail address on outgoing SMTP server
;remote_sender =