2
0
forked from drew/smtprelay

196 Commits

Author SHA1 Message Date
Bernhard Froehlich
c21c13cc7a Remove i386 from release builds 2022-05-01 11:23:56 +00:00
Bernhard Fröhlich
1617155f1e Remove service names from readme 2022-04-30 08:10:21 +02:00
Bernhard Fröhlich
81ea0c7944 Merge pull request #69 from decke/dependabot/github_actions/github/codeql-action-2
Bump github/codeql-action from 1 to 2
2022-04-26 14:33:39 +02:00
Bernhard Fröhlich
d9a09a20da Merge pull request #70 from decke/dependabot/github_actions/wangyoucao577/go-release-action-1.28
Bump wangyoucao577/go-release-action from 1.26 to 1.28
2022-04-26 14:33:04 +02:00
dependabot[bot]
d5d4e7a821 Bump wangyoucao577/go-release-action from 1.26 to 1.28
Bumps [wangyoucao577/go-release-action](https://github.com/wangyoucao577/go-release-action) from 1.26 to 1.28.
- [Release notes](https://github.com/wangyoucao577/go-release-action/releases)
- [Commits](https://github.com/wangyoucao577/go-release-action/compare/v1.26...v1.28)

---
updated-dependencies:
- dependency-name: wangyoucao577/go-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-26 10:17:49 +00:00
dependabot[bot]
77ab4485b2 Bump github/codeql-action from 1 to 2
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 1 to 2.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v1...v2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-26 10:17:46 +00:00
Bernhard Froehlich
ce97293e33 Update Go crypto and sys dependencies 2022-04-25 15:06:32 +00:00
Bernhard Froehlich
5cd81cd7e4 smtp: gofmt main repo
Merge from net/smtp/smtp.go

Obtained from:	19309779ac5e2f5a2fd3cbb34421dafb2855ac21
2022-04-25 15:00:57 +00:00
Bernhard Froehlich
62bb2becbc smtp: gofmt -w -r 'interface{}->any' src
Merge from net/smtp/smtp.go

Obtained from:	2580d0e08d5e9f979b943758d3c49877fb2324cb
2022-04-25 14:58:09 +00:00
Bernhard Froehlich
a2826e949e Regenerate go.mod for go 1.18 2022-04-25 14:42:23 +00:00
Bernhard Froehlich
64872c0bea Bump Go version to 1.18 for release and CI jobs 2022-04-25 14:37:23 +00:00
Bernhard Froehlich
cdc4e572db smtp: use bytes.Cut, strings.Cut
Merge from net/smtp/smtp.go

Obtained from:		4d8db00641cc9ff4f44de7df9b8c4f4a4f9416ee
2022-04-25 14:30:43 +00:00
Bernhard Froehlich
5e78bbe643 Improve remotes documentation in config 2022-04-25 14:19:31 +00:00
Javi
b134e426d7 Propagate the error to the client if the external command fails (#40)
This change makes the external command code return a generic 554 error to the SMTP client if the external command fails, so it knows sending failed and can retry.
2022-04-25 15:52:12 +02:00
Bernhard Fröhlich
9c230182da Merge pull request #68 from decke/dependabot/go_modules/github.com/stretchr/testify-1.7.1
Bump github.com/stretchr/testify from 1.7.0 to 1.7.1
2022-04-25 15:42:34 +02:00
dependabot[bot]
39b8e01226 Bump github.com/stretchr/testify from 1.7.0 to 1.7.1
Bumps [github.com/stretchr/testify](https://github.com/stretchr/testify) from 1.7.0 to 1.7.1.
- [Release notes](https://github.com/stretchr/testify/releases)
- [Commits](https://github.com/stretchr/testify/compare/v1.7.0...v1.7.1)

---
updated-dependencies:
- dependency-name: github.com/stretchr/testify
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-25 10:09:05 +00:00
Bernhard Fröhlich
bf8c222ac1 Merge pull request #63 from markgardner/master
Allow config to have multiple remotes.
2022-04-24 19:45:34 +02:00
Mark Gardner
875264837e add test coverage for scheme validation 2022-04-21 11:39:41 -05:00
Mark Gardner
32d0206af0 throw an error if the remote is configured for starttls and the server does not have the extension 2022-04-21 11:29:15 -05:00
Mark Gardner
b7f3701502 Add missing default port for starttls and validate scheme to ensure support 2022-04-21 11:28:48 -05:00
Mark Gardner
cf927508dd Improve sample config to be more accurate 2022-04-21 11:28:16 -05:00
Mark Gardner
4221919689 Allow config to have multiple remotes.
This will enable development teams to test emails from
a system like mailcatcher while also having the email
delivered to the indented mailbox.
2022-04-21 10:52:03 -05:00
Bernhard Fröhlich
4f1148d77b Merge pull request #67 from decke/dependabot/github_actions/wangyoucao577/go-release-action-1.26
Bump wangyoucao577/go-release-action from 1.25 to 1.26
2022-04-16 19:23:31 +02:00
dependabot[bot]
32c43efd0c Bump wangyoucao577/go-release-action from 1.25 to 1.26
Bumps [wangyoucao577/go-release-action](https://github.com/wangyoucao577/go-release-action) from 1.25 to 1.26.
- [Release notes](https://github.com/wangyoucao577/go-release-action/releases)
- [Commits](https://github.com/wangyoucao577/go-release-action/compare/v1.25...v1.26)

---
updated-dependencies:
- dependency-name: wangyoucao577/go-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-04-15 10:05:45 +00:00
Bernhard Fröhlich
362a64dd8a Merge pull request #65 from decke/dependabot/github_actions/actions/checkout-3
Bump actions/checkout from 2.4.0 to 3
2022-03-02 21:47:04 +01:00
dependabot[bot]
bf58b2b4c8 Bump actions/checkout from 2.4.0 to 3
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.4.0 to 3.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2.4.0...v3)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-02 10:07:48 +00:00
Bernhard Fröhlich
ed13816dcd Merge pull request #64 from decke/dependabot/github_actions/actions/setup-go-3
Bump actions/setup-go from 2.2.0 to 3
2022-03-01 14:36:05 +01:00
dependabot[bot]
dd7905e60e Bump actions/setup-go from 2.2.0 to 3
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2.2.0 to 3.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2.2.0...v3)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-03-01 10:07:35 +00:00
Bernhard Fröhlich
5076988ddf Merge pull request #59 from decke/dependabot/github_actions/wangyoucao577/go-release-action-1.25
Bump wangyoucao577/go-release-action from 1.24 to 1.25
2022-02-23 12:11:56 +01:00
dependabot[bot]
dafbc327aa Bump wangyoucao577/go-release-action from 1.24 to 1.25
Bumps [wangyoucao577/go-release-action](https://github.com/wangyoucao577/go-release-action) from 1.24 to 1.25.
- [Release notes](https://github.com/wangyoucao577/go-release-action/releases)
- [Commits](https://github.com/wangyoucao577/go-release-action/compare/v1.24...v1.25)

---
updated-dependencies:
- dependency-name: wangyoucao577/go-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-23 10:06:40 +00:00
Bernhard Fröhlich
48926964bf Merge pull request #58 from decke/dependabot/github_actions/actions/setup-go-2.2.0
Bump actions/setup-go from 2.1.5 to 2.2.0
2022-02-10 13:49:46 +01:00
dependabot[bot]
5757db3a37 Bump actions/setup-go from 2.1.5 to 2.2.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2.1.5 to 2.2.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2.1.5...v2.2.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2022-02-10 10:39:30 +00:00
Bernhard Fröhlich
a527a147be Merge pull request #56 from decke/dependabot/github_actions/wangyoucao577/go-release-action-1.24
Bump wangyoucao577/go-release-action from 1.23 to 1.24
2022-01-31 13:46:04 +01:00
dependabot[bot]
8c0a6aab20 Bump wangyoucao577/go-release-action from 1.23 to 1.24
Bumps [wangyoucao577/go-release-action](https://github.com/wangyoucao577/go-release-action) from 1.23 to 1.24.
- [Release notes](https://github.com/wangyoucao577/go-release-action/releases)
- [Commits](https://github.com/wangyoucao577/go-release-action/compare/v1.23...v1.24)

---
updated-dependencies:
- dependency-name: wangyoucao577/go-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-31 10:07:26 +00:00
Bernhard Fröhlich
5be2f3aa4b Merge pull request #55 from decke/dependabot/github_actions/wangyoucao577/go-release-action-1.23
Bump wangyoucao577/go-release-action from 1.22 to 1.23
2022-01-26 13:19:08 +01:00
dependabot[bot]
2b046297ea Bump wangyoucao577/go-release-action from 1.22 to 1.23
Bumps [wangyoucao577/go-release-action](https://github.com/wangyoucao577/go-release-action) from 1.22 to 1.23.
- [Release notes](https://github.com/wangyoucao577/go-release-action/releases)
- [Commits](https://github.com/wangyoucao577/go-release-action/compare/v1.22...v1.23)

---
updated-dependencies:
- dependency-name: wangyoucao577/go-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2022-01-26 10:06:33 +00:00
Bernhard Froehlich
f0d39401d4 Allow go build workflow for pull requests 2022-01-11 12:55:39 +00:00
Bernhard Fröhlich
b5fcf8e0bb Merge pull request #52 from Coronon/master
add: basic smtpd configuration options
2022-01-11 13:55:02 +01:00
Rubin Raithel
016ef762fb add: basic smtpd configuration options 2022-01-10 20:39:55 +01:00
Bernhard Fröhlich
6afc87968c Merge pull request #51 from decke/dependabot/github_actions/actions/setup-go-2.1.5
Bump actions/setup-go from 2.1.4 to 2.1.5
2021-12-22 11:43:16 +01:00
dependabot[bot]
b32b5690bf Bump actions/setup-go from 2.1.4 to 2.1.5
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2.1.4 to 2.1.5.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2.1.4...v2.1.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-12-22 10:08:04 +00:00
Bernhard Fröhlich
38aa05c9f4 Merge pull request #49 from NoUseFreak/add-skip-verify
Allow ignoring an insecure tls connection
2021-12-15 21:31:54 +01:00
Dries De Peuter
03d109ff8b feat: Add support for ignoring certs 2021-12-09 21:38:38 +01:00
Bernhard Fröhlich
6c691f3cea Merge pull request #48 from decke/dependabot/github_actions/wangyoucao577/go-release-action-1.22
Bump wangyoucao577/go-release-action from 1.21 to 1.22
2021-11-24 13:07:20 +01:00
dependabot[bot]
c6b5e244eb Bump wangyoucao577/go-release-action from 1.21 to 1.22
Bumps [wangyoucao577/go-release-action](https://github.com/wangyoucao577/go-release-action) from 1.21 to 1.22.
- [Release notes](https://github.com/wangyoucao577/go-release-action/releases)
- [Commits](https://github.com/wangyoucao577/go-release-action/compare/v1.21...v1.22)

---
updated-dependencies:
- dependency-name: wangyoucao577/go-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-24 10:06:54 +00:00
Bernhard Froehlich
0ee982ea31 Bump used Go versions to 1.17 2021-11-22 15:38:17 +00:00
Bernhard Fröhlich
287395ad91 Merge pull request #47 from alex1989hu/bump/chrj-smtpd-v031
Bump chrj/smtpd from 0.3.0 to 0.3.1
2021-11-22 11:39:33 +01:00
Alex Szakaly
3aecd3c6d6 Bump chrj/smtpd from 0.3.0 to 0.3.1
Fixes #44

Signed-off-by: Alex Szakaly <alex.szakaly@gmail.com>
2021-11-22 07:53:43 +01:00
Bernhard Fröhlich
f8960053e8 Merge pull request #46 from decke/dependabot/github_actions/wangyoucao577/go-release-action-1.21
Bump wangyoucao577/go-release-action from 1.20 to 1.21
2021-11-17 11:59:35 +01:00
dependabot[bot]
db5512d47b Bump wangyoucao577/go-release-action from 1.20 to 1.21
Bumps [wangyoucao577/go-release-action](https://github.com/wangyoucao577/go-release-action) from 1.20 to 1.21.
- [Release notes](https://github.com/wangyoucao577/go-release-action/releases)
- [Commits](https://github.com/wangyoucao577/go-release-action/compare/v1.20...v1.21)

---
updated-dependencies:
- dependency-name: wangyoucao577/go-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-16 10:07:49 +00:00
Bernhard Fröhlich
fd063ad879 Merge pull request #45 from decke/dependabot/github_actions/actions/checkout-2.4.0
Bump actions/checkout from 2.3.5 to 2.4.0
2021-11-03 11:49:27 +01:00
dependabot[bot]
85bdd060e3 Bump actions/checkout from 2.3.5 to 2.4.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.5 to 2.4.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2.3.5...v2.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-11-03 10:05:55 +00:00
Bernhard Fröhlich
5be6165865 Merge pull request #43 from decke/dependabot/github_actions/actions/checkout-2.3.5
Bump actions/checkout from 2.3.4 to 2.3.5
2021-10-18 16:00:48 +02:00
dependabot[bot]
b64a34becf Bump actions/checkout from 2.3.4 to 2.3.5
Bumps [actions/checkout](https://github.com/actions/checkout) from 2.3.4 to 2.3.5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2.3.4...v2.3.5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-10-18 10:08:18 +00:00
Bernhard Fröhlich
a2ea5ab49e Merge pull request #42 from decke/dependabot/github_actions/wangyoucao577/go-release-action-1.20
Bump wangyoucao577/go-release-action from 1.19 to 1.20
2021-08-30 14:46:38 +02:00
dependabot[bot]
94957d944f Bump wangyoucao577/go-release-action from 1.19 to 1.20
Bumps [wangyoucao577/go-release-action](https://github.com/wangyoucao577/go-release-action) from 1.19 to 1.20.
- [Release notes](https://github.com/wangyoucao577/go-release-action/releases)
- [Commits](https://github.com/wangyoucao577/go-release-action/compare/v1.19...v1.20)

---
updated-dependencies:
- dependency-name: wangyoucao577/go-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-30 10:06:39 +00:00
Bernhard Fröhlich
a5db5e1ff5 Merge pull request #41 from decke/dependabot/github_actions/actions/setup-go-2.1.4
Bump actions/setup-go from 2.1.3 to 2.1.4
2021-08-26 13:14:35 +02:00
dependabot[bot]
94776b27d9 Bump actions/setup-go from 2.1.3 to 2.1.4
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 2.1.3 to 2.1.4.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v2.1.3...v2.1.4)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-26 10:06:19 +00:00
Bernhard Fröhlich
81bc7addc7 Merge pull request #39 from decke/dependabot/github_actions/wangyoucao577/go-release-action-1.19
Bump wangyoucao577/go-release-action from 1.18 to 1.19
2021-07-28 22:15:07 +02:00
dependabot[bot]
e9bfe53f18 Bump wangyoucao577/go-release-action from 1.18 to 1.19
Bumps [wangyoucao577/go-release-action](https://github.com/wangyoucao577/go-release-action) from 1.18 to 1.19.
- [Release notes](https://github.com/wangyoucao577/go-release-action/releases)
- [Commits](https://github.com/wangyoucao577/go-release-action/compare/v1.18...v1.19)

---
updated-dependencies:
- dependency-name: wangyoucao577/go-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-21 10:06:34 +00:00
Bernhard Fröhlich
32032c297c Merge pull request #38 from decke/dependabot/go_modules/github.com/google/uuid-1.3.0
Bump github.com/google/uuid from 1.2.0 to 1.3.0
2021-07-13 13:10:23 +02:00
dependabot[bot]
53e52de279 Bump github.com/google/uuid from 1.2.0 to 1.3.0
Bumps [github.com/google/uuid](https://github.com/google/uuid) from 1.2.0 to 1.3.0.
- [Release notes](https://github.com/google/uuid/releases)
- [Commits](https://github.com/google/uuid/compare/v1.2.0...v1.3.0)

---
updated-dependencies:
- dependency-name: github.com/google/uuid
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-13 10:06:56 +00:00
Bernhard Fröhlich
02810c0a50 Merge pull request #36 from decke/dependabot/github_actions/wangyoucao577/go-release-action-1.18
Bump wangyoucao577/go-release-action from 1.17 to 1.18
2021-05-26 14:36:30 +02:00
dependabot[bot]
6b21f52037 Bump wangyoucao577/go-release-action from 1.17 to 1.18
Bumps [wangyoucao577/go-release-action](https://github.com/wangyoucao577/go-release-action) from 1.17 to 1.18.
- [Release notes](https://github.com/wangyoucao577/go-release-action/releases)
- [Commits](https://github.com/wangyoucao577/go-release-action/compare/v1.17...v1.18)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-26 05:59:28 +00:00
Bernhard Fröhlich
544bd081ff Merge pull request #34 from decke/dependabot/github_actions/actions/checkout-2.3.4
Bump actions/checkout from 2 to 2.3.4
2021-05-12 08:41:49 +02:00
dependabot[bot]
6a28f939de Bump actions/checkout from 2 to 2.3.4
Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 2.3.4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v2...v2.3.4)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-12 05:54:25 +00:00
Bernhard Fröhlich
f0392bdf09 Merge pull request #33 from benubois/command
Add external command support
2021-05-10 10:18:56 +02:00
Ben Ubois
3f627d3281 Move AddReceivedLine before further processing. 2021-05-08 11:56:28 -07:00
Ben Ubois
d8860fc917 Added external command support. 2021-05-07 14:08:10 -07:00
Bernhard Fröhlich
ebb53ea1b6 Merge pull request #30 from decke/dependabot/github_actions/wangyoucao577/go-release-action-v1.17
Bump wangyoucao577/go-release-action from v1.16 to v1.17
2021-04-13 20:13:18 +02:00
dependabot[bot]
a5ee525825 Bump wangyoucao577/go-release-action from v1.16 to v1.17
Bumps [wangyoucao577/go-release-action](https://github.com/wangyoucao577/go-release-action) from v1.16 to v1.17.
- [Release notes](https://github.com/wangyoucao577/go-release-action/releases)
- [Commits](https://github.com/wangyoucao577/go-release-action/compare/v1.16...1a87d97ce87db435508d150622a9ab5472db15ac)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-13 07:18:35 +00:00
Bernhard Froehlich
441a53cfd9 Fix code formatting with gofmt -s 2021-04-03 19:00:38 +00:00
Bernhard Froehlich
7b199cc6ae Bump Go version for CI builds to 1.16 2021-04-02 18:51:23 +00:00
Bernhard Froehlich
184d5f8611 Bump Go version for builds to 1.16.3 2021-04-02 18:49:53 +00:00
Bernhard Fröhlich
9e8b5517a6 Merge pull request #27 from decke/26-tls-reqd-for-auth
Don't allow configuration requiring authentication with non-TLS listener
2021-04-02 20:41:38 +02:00
Bernhard Fröhlich
65f0cfcfbd Merge pull request #28 from decke/dependabot/github_actions/wangyoucao577/go-release-action-v1.16
Bump wangyoucao577/go-release-action from v1.15 to v1.16
2021-04-02 20:29:02 +02:00
dependabot[bot]
410c70d86f Bump wangyoucao577/go-release-action from v1.15 to v1.16
Bumps [wangyoucao577/go-release-action](https://github.com/wangyoucao577/go-release-action) from v1.15 to v1.16.
- [Release notes](https://github.com/wangyoucao577/go-release-action/releases)
- [Commits](https://github.com/wangyoucao577/go-release-action/compare/v1.15...d00bb7360b2dd6a39f1a7ca198b8289582eee2df)

Signed-off-by: dependabot[bot] <support@github.com>
2021-04-01 07:35:56 +00:00
Jonathon Reinhart
247d187576 Don't allow a configuration requiring auth with a non-TLS listener
This fixes #26
2021-04-01 01:12:34 -04:00
Jonathon Reinhart
45a676e274 Add localAuthRequired() helper function
This just makes the configuration state a little more obvious.
2021-04-01 01:12:34 -04:00
Jonathon Reinhart
ca1ccd85e3 Refactor parsing of -listen string out into separate config function
This makes the "for each listen address" loop in main() look even cleaner.
2021-04-01 01:12:34 -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:	32be721d71
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
21 changed files with 1988 additions and 106 deletions

11
.github/dependabot.yml vendored Normal file
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
.github/workflows/codeql-analysis.yml vendored Normal file
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@v3
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@v2
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@v2
# 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@v2

24
.github/workflows/go.yml vendored Normal file
View File

@@ -0,0 +1,24 @@
name: Go
on: [push, pull_request]
jobs:
build:
name: Build
runs-on: ubuntu-latest
steps:
- name: Set up Go 1.18
uses: actions/setup-go@v3
with:
go-version: 1.18
id: go
- name: Check out code into the Go module directory
uses: actions/checkout@v3
- name: Get dependencies
run: |
go get -v -t -d ./...
- name: Build
run: go build -v .

30
.github/workflows/release.yaml vendored Normal file
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: [amd64, arm64]
steps:
- uses: actions/checkout@v3
- 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.28
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
goos: ${{ matrix.goos }}
goarch: ${{ matrix.goarch }}
goversion: 1.18
extra_files: LICENSE README.md smtprelay.ini
ldflags: -s -w -X "main.appVersion=${{ env.APP_VERSION }}" -X "main.buildTime=${{ env.BUILD_TIME }}"

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
via a remote SMTP server without giving away my mail 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 ## 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 (any SMTP server)
* Small codebase
* IPv6 support * IPv6 support
* Forward to GMail, MailGun or any other SMTP server

100
auth.go Normal file
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
auth_test.go Normal file
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
cmd/README.md Normal file
View File

@@ -0,0 +1,6 @@
To run the hasher, do like this
```bash
$ go run hasher.go hunter2
```

22
cmd/hasher.go Normal file
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))
}

200
config.go Normal file
View File

@@ -0,0 +1,200 @@
package main
import (
"flag"
"fmt"
"net"
"regexp"
"strings"
"time"
"github.com/sirupsen/logrus"
"github.com/vharitonsky/iniflags"
)
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")
listenStr = flag.String("listen", "127.0.0.1:25 [::1]:25", "Address and port to listen for incoming SMTP")
listenAddrs = []protoAddr{}
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)")
readTimeoutStr = flag.String("read_timeout", "60s", "Socket timeout for read operations")
readTimeout time.Duration
writeTimeoutStr = flag.String("write_timeout", "60s", "Socket timeout for write operations")
writeTimeout time.Duration
dataTimeoutStr = flag.String("data_timeout", "5m", "Socket timeout for DATA command")
dataTimeout time.Duration
maxConnections = flag.Int("max_connections", 100, "Max concurrent connections, use -1 to disable")
maxMessageSize = flag.Int("max_message_size", 10240000, "Max message size in bytes")
maxRecipients = flag.Int("max_recipients", 100, "Max RCPT TO calls for each envelope")
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")
command = flag.String("command", "", "Path to pipe command")
remotesStr = flag.String("remotes", "", "Outgoing SMTP servers")
remotes = []*Remote{}
versionInfo = flag.Bool("version", false, "Show version information")
)
func localAuthRequired() bool {
return *allowedUsers != ""
}
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 setupRemotes() {
logger := log.WithField("remotes", *remotesStr)
if *remotesStr != "" {
for _, remoteURL := range strings.Split(*remotesStr, " ") {
r, err := ParseRemote(remoteURL)
if err != nil {
logger.Fatal(fmt.Sprintf("error parsing url: '%s': %v", remoteURL, err))
}
remotes = append(remotes, r)
}
}
}
type protoAddr struct {
protocol string
address string
}
func splitProto(s string) protoAddr {
idx := strings.Index(s, "://")
if idx == -1 {
return protoAddr{
address: s,
}
}
return protoAddr{
protocol: s[0:idx],
address: s[idx+3:],
}
}
func setupListeners() {
for _, listenAddr := range strings.Split(*listenStr, " ") {
pa := splitProto(listenAddr)
if localAuthRequired() && pa.protocol == "" {
log.WithField("address", pa.address).
Fatal("Local authentication (via allowed_users file) " +
"not allowed with non-TLS listener")
}
listenAddrs = append(listenAddrs, pa)
}
}
func setupTimeouts() {
var err error
readTimeout, err = time.ParseDuration(*readTimeoutStr)
if err != nil {
log.WithField("read_timeout", *readTimeoutStr).
WithError(err).
Fatal("read_timeout duration string invalid")
}
if readTimeout.Seconds() < 1 {
log.WithField("read_timeout", *readTimeoutStr).
Fatal("read_timeout less than one second")
}
writeTimeout, err = time.ParseDuration(*writeTimeoutStr)
if err != nil {
log.WithField("write_timeout", *writeTimeoutStr).
WithError(err).
Fatal("write_timeout duration string invalid")
}
if writeTimeout.Seconds() < 1 {
log.WithField("write_timeout", *writeTimeoutStr).
Fatal("write_timeout less than one second")
}
dataTimeout, err = time.ParseDuration(*dataTimeoutStr)
if err != nil {
log.WithField("data_timeout", *dataTimeoutStr).
WithError(err).
Fatal("data_timeout duration string invalid")
}
if dataTimeout.Seconds() < 1 {
log.WithField("data_timeout", *dataTimeoutStr).
Fatal("data_timeout less than one second")
}
}
func ConfigLoad() {
iniflags.Parse()
// Set up logging as soon as possible
setupLogger()
if *remotesStr == "" && *command == "" {
log.Warn("no remotes or command set; mail will not be forwarded!")
}
setupAllowedNetworks()
setupAllowedPatterns()
setupRemotes()
setupListeners()
setupTimeouts()
}

44
config_test.go Normal file
View File

@@ -0,0 +1,44 @@
package main
import (
"testing"
)
func TestSplitProto(t *testing.T) {
var tests = []struct {
input string
proto string
addr string
}{
{
input: "localhost",
proto: "",
addr: "localhost",
},
{
input: "tls://my.local.domain",
proto: "tls",
addr: "my.local.domain",
},
{
input: "starttls://my.local.domain",
proto: "starttls",
addr: "my.local.domain",
},
}
for i, test := range tests {
testName := test.input
t.Run(testName, func(t *testing.T) {
pa := splitProto(test.input)
if pa.protocol != test.proto {
t.Errorf("Testcase %d: Incorrect proto: expected %v, got %v",
i, test.proto, pa.protocol)
}
if pa.address != test.addr {
t.Errorf("Testcase %d: Incorrect addr: expected %v, got %v",
i, test.addr, pa.address)
}
})
}
}

17
go.mod
View File

@@ -1,6 +1,19 @@
module code.bluelife.at/decke/smtp-proxy module github.com/decke/smtprelay
require ( require (
github.com/chrj/smtpd v0.1.1 github.com/chrj/smtpd v0.3.1
github.com/google/uuid v1.3.0
github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.7.1
github.com/vharitonsky/iniflags v0.0.0-20180513140207-a33cd0b5f3de github.com/vharitonsky/iniflags v0.0.0-20180513140207-a33cd0b5f3de
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4
) )
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 // indirect
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c // indirect
)
go 1.18

27
go.sum
View File

@@ -1,5 +1,26 @@
github.com/chrj/smtpd v0.1.1 h1:Jk9ZimOh4njDpMbDLPmdQX+x9AUYGrfE68EyjuQDPjk= github.com/chrj/smtpd v0.3.1 h1:kogHFkbFdKaoH3bgZkqNC9uVtKYOFfM3uV3rroBdooE=
github.com/chrj/smtpd v0.1.1/go.mod h1:jt4ydELuZmqhn9hn3YpEPV1dY00aOB+Q1nWXnBDFKeY= github.com/chrj/smtpd v0.3.1/go.mod h1:JtABvV/LzvLmEIzy0NyDnrfMGOMd8wy5frAokwf6J9Q=
github.com/eaigner/dkim v0.0.0-20150301120808-6fe4a7ee9cfb/go.mod h1:FSCIHbrqk7D01Mj8y/jW+NS1uoCerr+ad+IckTHTFf4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/vharitonsky/iniflags v0.0.0-20180513140207-a33cd0b5f3de h1:fkw+7JkxF3U1GzQoX9h69Wvtvxajo5Rbzy6+YMMzPIg= 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= github.com/vharitonsky/iniflags v0.0.0-20180513140207-a33cd0b5f3de/go.mod h1:irMhzlTz8+fVFj6CH2AN2i+WI5S6wWFtK3MBCIxIpyI=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4 h1:kUhD7nTDoI3fVd9G4ORWrbV5NY0liEs/Jg2pv5f+bBA=
golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150 h1:xHms4gcpe1YE7A3yIllJXP16CMAGuqwO2lX1mTyyRRc=
golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

60
logger.go Normal file
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)
}

425
main.go
View File

@@ -1,92 +1,399 @@
package main package main
import ( import (
"bytes"
"crypto/tls" "crypto/tls"
"flag" "fmt"
"log"
"net" "net"
"net/smtp" "net/textproto"
"os"
"os/exec"
"os/signal"
"strings" "strings"
"time" "syscall"
"github.com/chrj/smtpd" "github.com/chrj/smtpd"
"github.com/vharitonsky/iniflags" "github.com/google/uuid"
"github.com/sirupsen/logrus"
) )
var ( func connectionChecker(peer smtpd.Peer) error {
hostName = flag.String("hostname", "localhost.localdomain", "Server hostname") // This can't panic because we only have TCP listeners
welcomeMsg = flag.String("welcome_msg", "", "Welcome message for SMTP session") peerIP := peer.Addr.(*net.TCPAddr).IP
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 handler(peer smtpd.Peer, env smtpd.Envelope) error { if len(allowedNets) == 0 {
// Special case: empty string means allow everything
var auth smtp.Auth return nil
host, _, _ := net.SplitHostPort(*remoteHost)
if *remoteUser != "" && *remotePass != "" {
auth = smtp.PlainAuth("", *remoteUser, *remotePass, host)
} }
return smtp.SendMail( for _, allowedNet := range allowedNets {
*remoteHost, if allowedNet.Contains(peerIP) {
auth, return nil
env.Sender, }
env.Recipients, }
env.Data,
) 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 localAuthRequired() && 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,
"uuid": generateUUID(),
})
if *remotesStr == "" && *command == "" {
logger.Warning("no remote_host or command set; discarding mail")
return nil
}
env.AddReceivedLine(peer)
if *command != "" {
cmdLogger := logger.WithField("command", *command)
var stdout bytes.Buffer
var stderr bytes.Buffer
cmd := exec.Command(*command)
cmd.Stdin = bytes.NewReader(env.Data)
cmd.Stdout = &stdout
cmd.Stderr = &stderr
err := cmd.Run()
if err != nil {
cmdLogger.WithError(err).Error(stderr.String())
return smtpd.Error{Code: 554, Message: "External command failed"}
}
cmdLogger.Info("pipe command successful: " + stdout.String())
}
for _, remote := range remotes {
logger = logger.WithField("host", remote.Addr)
logger.Info("delivering mail from peer using smarthost")
err := SendMail(
remote,
env.Sender,
env.Recipients,
env.Data,
)
if err != nil {
var smtpError smtpd.Error
switch err := err.(type) {
case *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() { 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) { // Load allowed users file
listener := listeners[i] if localAuthRequired() {
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 _, listen := range listenAddrs {
logger := log.WithField("address", listen.address)
server := &smtpd.Server{ server := &smtpd.Server{
Hostname: *hostName, Hostname: *hostName,
WelcomeMessage: *welcomeMsg, WelcomeMessage: *welcomeMsg,
Handler: handler, ReadTimeout: readTimeout,
WriteTimeout: writeTimeout,
DataTimeout: dataTimeout,
MaxConnections: *maxConnections,
MaxMessageSize: *maxMessageSize,
MaxRecipients: *maxRecipients,
ConnectionChecker: connectionChecker,
SenderChecker: senderChecker,
RecipientChecker: recipientChecker,
Handler: mailHandler,
} }
if strings.Index(listeners[i], "://") == -1 { if localAuthRequired() {
; server.Authenticator = authChecker
} else if strings.HasPrefix(listeners[i], "tls://") || strings.HasPrefix(listeners[i], "starttls://") { }
listener = strings.TrimPrefix(listener, "tls://") var lsnr net.Listener
listener = strings.TrimPrefix(listener, "starttls://") var err error
if *localCert == "" || *localKey == "" { switch listen.protocol {
log.Fatal("TLS certificate/key not defined in config") case "":
} logger.Info("listening on address")
lsnr, err = net.Listen("tcp", listen.address)
cert, err := tls.LoadX509KeyPair(*localCert, *localKey)
if err != nil {
log.Fatal(err)
}
case "starttls":
server.TLSConfig = getTLSConfig()
server.ForceTLS = *localForceTLS server.ForceTLS = *localForceTLS
server.TLSConfig = &tls.Config {
Certificates: [] tls.Certificate{cert}, logger.Info("listening on address (STARTTLS)")
} lsnr, err = net.Listen("tcp", listen.address)
} else {
log.Fatal("Unknown protocol in listener ", listener) case "tls":
server.TLSConfig = getTLSConfig()
logger.Info("listening on address (TLS)")
lsnr, err = tls.Listen("tcp", listen.address, server.TLSConfig)
default:
logger.WithField("protocol", listen.protocol).
Fatal("unknown protocol in listen address")
} }
log.Printf("Listen on %s ...\n", listener) if err != nil {
go server.ListenAndServe(listener) logger.WithError(err).Fatal("error starting listener")
}
servers = append(servers, server)
go func() {
server.Serve(lsnr)
}()
} }
for true { handleSignals()
time.Sleep(time.Minute)
// 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
main_test.go Normal file
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()
}
}

84
remotes.go Normal file
View File

@@ -0,0 +1,84 @@
package main
import (
"fmt"
"net/smtp"
"net/url"
)
type Remote struct {
SkipVerify bool
Auth smtp.Auth
Scheme string
Hostname string
Port string
Addr string
Sender string
}
// ParseRemote creates a remote from a given url in the following format:
//
// smtp://[user[:password]@][netloc][:port][/remote_sender][?param1=value1&...]
// smtps://[user[:password]@][netloc][:port][/remote_sender][?param1=value1&...]
// starttls://[user[:password]@][netloc][:port][/remote_sender][?param1=value1&...]
//
// Supported Params:
// - skipVerify: can be "true" or empty to prevent ssl verification of remote server's certificate.
// - auth: can be "login" to trigger "LOGIN" auth instead of "PLAIN" auth
//
func ParseRemote(remoteURL string) (*Remote, error) {
u, err := url.Parse(remoteURL)
if err != nil {
return nil, err
}
if u.Scheme != "smtp" && u.Scheme != "smtps" && u.Scheme != "starttls" {
return nil, fmt.Errorf("'%s' is not a supported relay scheme", u.Scheme)
}
hostname, port := u.Hostname(), u.Port()
if port == "" {
switch u.Scheme {
case "smtp":
port = "25"
case "smtps":
port = "465"
case "starttls":
port = "587"
}
}
q := u.Query()
r := &Remote{
Scheme: u.Scheme,
Hostname: hostname,
Port: port,
Addr: fmt.Sprintf("%s:%s", hostname, port),
}
if u.User != nil {
pass, _ := u.User.Password()
user := u.User.Username()
if hasAuth, authVal := q.Has("auth"), q.Get("auth"); hasAuth {
if authVal != "login" {
return nil, fmt.Errorf("Auth must be login or not present, received '%s'", authVal)
}
r.Auth = LoginAuth(user, pass)
} else {
r.Auth = smtp.PlainAuth("", user, pass, u.Hostname())
}
}
if hasVal, skipVerify := q.Has("skipVerify"), q.Get("skipVerify"); hasVal && skipVerify != "false" {
r.SkipVerify = true
}
if u.Path != "" {
r.Sender = u.Path[1:]
}
return r, nil
}

114
remotes_test.go Normal file
View File

@@ -0,0 +1,114 @@
package main
import (
"net/smtp"
"testing"
"github.com/stretchr/testify/assert"
)
func AssertRemoteUrlEquals(t *testing.T, expected *Remote, remotUrl string) {
actual, err := ParseRemote(remotUrl)
assert.Nil(t, err)
assert.NotNil(t, actual)
assert.Equal(t, expected.Scheme, actual.Scheme, "Scheme %s", remotUrl)
assert.Equal(t, expected.Addr, actual.Addr, "Addr %s", remotUrl)
assert.Equal(t, expected.Hostname, actual.Hostname, "Hostname %s", remotUrl)
assert.Equal(t, expected.Port, actual.Port, "Port %s", remotUrl)
assert.Equal(t, expected.Sender, actual.Sender, "Sender %s", remotUrl)
assert.Equal(t, expected.SkipVerify, actual.SkipVerify, "SkipVerify %s", remotUrl)
if expected.Auth != nil || actual.Auth != nil {
assert.NotNil(t, expected, "Auth %s", remotUrl)
assert.NotNil(t, actual, "Auth %s", remotUrl)
assert.IsType(t, expected.Auth, actual.Auth)
}
}
func TestValidRemoteUrls(t *testing.T) {
AssertRemoteUrlEquals(t, &Remote{
Scheme: "smtp",
SkipVerify: false,
Auth: nil,
Hostname: "email.com",
Port: "25",
Addr: "email.com:25",
Sender: "",
}, "smtp://email.com")
AssertRemoteUrlEquals(t, &Remote{
Scheme: "smtp",
SkipVerify: true,
Auth: nil,
Hostname: "email.com",
Port: "25",
Addr: "email.com:25",
Sender: "",
}, "smtp://email.com?skipVerify")
AssertRemoteUrlEquals(t, &Remote{
Scheme: "smtp",
SkipVerify: false,
Auth: smtp.PlainAuth("", "user", "pass", ""),
Hostname: "email.com",
Port: "25",
Addr: "email.com:25",
Sender: "",
}, "smtp://user:pass@email.com")
AssertRemoteUrlEquals(t, &Remote{
Scheme: "smtp",
SkipVerify: false,
Auth: LoginAuth("user", "pass"),
Hostname: "email.com",
Port: "25",
Addr: "email.com:25",
Sender: "",
}, "smtp://user:pass@email.com?auth=login")
AssertRemoteUrlEquals(t, &Remote{
Scheme: "smtp",
SkipVerify: false,
Auth: LoginAuth("user", "pass"),
Hostname: "email.com",
Port: "25",
Addr: "email.com:25",
Sender: "sender@website.com",
}, "smtp://user:pass@email.com/sender@website.com?auth=login")
AssertRemoteUrlEquals(t, &Remote{
Scheme: "smtps",
SkipVerify: false,
Auth: LoginAuth("user", "pass"),
Hostname: "email.com",
Port: "465",
Addr: "email.com:465",
Sender: "sender@website.com",
}, "smtps://user:pass@email.com/sender@website.com?auth=login")
AssertRemoteUrlEquals(t, &Remote{
Scheme: "smtps",
SkipVerify: true,
Auth: LoginAuth("user", "pass"),
Hostname: "email.com",
Port: "8425",
Addr: "email.com:8425",
Sender: "sender@website.com",
}, "smtps://user:pass@email.com:8425/sender@website.com?auth=login&skipVerify")
AssertRemoteUrlEquals(t, &Remote{
Scheme: "starttls",
SkipVerify: true,
Auth: LoginAuth("user", "pass"),
Hostname: "email.com",
Port: "8425",
Addr: "email.com:8425",
Sender: "sender@website.com",
}, "starttls://user:pass@email.com:8425/sender@website.com?auth=login&skipVerify")
}
func TestMissingScheme(t *testing.T) {
_, err := ParseRemote("http://user:pass@email.com:8425/sender@website.com")
assert.NotNil(t, err, "Err must be present")
assert.Equal(t, err.Error(), "'http' is not a supported relay scheme")
}

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 =

491
smtp.go Normal file
View File

@@ -0,0 +1,491 @@
// 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 ...any) (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 {
k, v, _ := strings.Cut(line, " ")
ext[k] = v
}
}
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(r *Remote, from string, to []string, msg []byte) error {
if r.Sender != "" {
from = r.Sender
}
if err := validateLine(from); err != nil {
return err
}
for _, recp := range to {
if err := validateLine(recp); err != nil {
return err
}
}
var c *Client
var err error
if r.Scheme == "smtps" {
config := &tls.Config{
ServerName: r.Hostname,
InsecureSkipVerify: r.SkipVerify,
}
conn, err := tls.Dial("tcp", r.Addr, config)
if err != nil {
return err
}
defer conn.Close()
c, err = NewClient(conn, r.Hostname)
if err != nil {
return err
}
if err = c.hello(); err != nil {
return err
}
} else {
c, err = Dial(r.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,
InsecureSkipVerify: r.SkipVerify,
}
if testHookStartTLS != nil {
testHookStartTLS(config)
}
if err = c.StartTLS(config); err != nil {
return err
}
} else if r.Scheme == "starttls" {
return errors.New("starttls: server does not support extension, check remote scheme")
}
}
if r.Auth != nil && c.ext != nil {
if _, ok := c.ext["AUTH"]; !ok {
return errors.New("smtp: server doesn't support AUTH")
}
if err = c.Auth(r.Auth); 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
}

122
smtprelay.ini Normal file
View File

@@ -0,0 +1,122 @@
; 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
; Socket timeout for read operations
; Duration string as sequence of decimal numbers,
; each with optional fraction and a unit suffix.
; Valid time units are "ns", "us", "ms", "s", "m", "h".
;read_timeout = 60s
; Socket timeout for write operations
; Duration string as sequence of decimal numbers,
; each with optional fraction and a unit suffix.
; Valid time units are "ns", "us", "ms", "s", "m", "h".
;write_timeout = 60s
; Socket timeout for DATA command
; Duration string as sequence of decimal numbers,
; each with optional fraction and a unit suffix.
; Valid time units are "ns", "us", "ms", "s", "m", "h".
;data_timeout = 5m
; Max concurrent connections, use -1 to disable
;max_connections = 100
; Max message size in bytes
;max_message_size = 10240000
; Max RCPT TO calls for each envelope
;max_recipients = 100
; 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 servers.
; If not set, mails are discarded.
;
; Format:
; protocol://[user[:password]@][netloc][:port][/remote_sender][?param1=value1&...]
;
; protocol: smtp (unencrypted), smtps (TLS), starttls (STARTTLS)
; user: Username for authentication
; password: Password for authentication
; remote_sender: Email address to use as FROM
; params:
; skipVerify: "true" or empty to prevent ssl verification of remote server's certificate
; auth: "login" to use LOGIN authentication
; GMail
;remotes = starttls://user:pass@smtp.gmail.com:587
; Mailgun.org
;remotes = starttls://user:pass@smtp.mailgun.org:587
; Mailjet.com
;remotes = starttls://user:pass@in-v3.mailjet.com:587
; Ignore remote host certificates
;remotes = starttls://user:pass@server:587?skipVerify
; Login Authentication method on outgoing SMTP server
;remotes = smtp://user:pass@server:2525?auth=login
; Sender e-mail address on outgoing SMTP server
;remotes = smtp://user:pass@server:2525/overridden@email.com?auth=login
; Multiple remotes, space delimited
;remotes = smtp://127.0.0.1:1025 starttls://user:pass@smtp.mailgun.org:587
; Pipe messages to external command
;command = /usr/local/bin/script