SPF and DKIM checks
This commit is contained in:
@@ -113,6 +113,8 @@ If you want to change them - check available options in the help message (`!pm h
|
|||||||
---
|
---
|
||||||
|
|
||||||
* **!pm spamcheck:mx** - only accept email from servers which seem prepared to receive it (those having valid MX records) (`true` - enable, `false` - disable)
|
* **!pm spamcheck:mx** - only accept email from servers which seem prepared to receive it (those having valid MX records) (`true` - enable, `false` - disable)
|
||||||
|
* **!pm spamcheck:spf** - only accept email from senders which authorized to send it (those matching SPF records) (`true` - enable, `false` - disable)
|
||||||
|
* **!pm spamcheck:dkim** - only accept correctly authorized emails (without DKIM signature at all or with valid DKIM signature) (`true` - enable, `false` - disable)
|
||||||
* **!pm spamcheck:smtp** - only accept email from servers which seem prepared to receive it (those listening on an SMTP port) (`true` - enable, `false` - disable)
|
* **!pm spamcheck:smtp** - only accept email from servers which seem prepared to receive it (those listening on an SMTP port) (`true` - enable, `false` - disable)
|
||||||
* **!pm spamlist** - Get or set `spamlist` of the room (comma-separated list), eg: `spammer@example.com,*@spammer.org,noreply@*`
|
* **!pm spamlist** - Get or set `spamlist` of the room (comma-separated list), eg: `spammer@example.com,*@spammer.org,noreply@*`
|
||||||
|
|
||||||
|
|||||||
@@ -173,6 +173,18 @@ func (b *Bot) initCommands() commandList {
|
|||||||
sanitizer: utils.SanitizeBoolString,
|
sanitizer: utils.SanitizeBoolString,
|
||||||
allowed: b.allowOwner,
|
allowed: b.allowOwner,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: roomOptionSpamcheckSPF,
|
||||||
|
description: "only accept email from senders which authorized to send it (those matching SPF records) (`true` - enable, `false` - disable)",
|
||||||
|
sanitizer: utils.SanitizeBoolString,
|
||||||
|
allowed: b.allowOwner,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: roomOptionSpamcheckDKIM,
|
||||||
|
description: "only accept correctly authorized emails (without DKIM signature at all or with valid DKIM signature) (`true` - enable, `false` - disable)",
|
||||||
|
sanitizer: utils.SanitizeBoolString,
|
||||||
|
allowed: b.allowOwner,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
key: roomOptionSpamcheckSMTP,
|
key: roomOptionSpamcheckSMTP,
|
||||||
description: "only accept email from servers which seem prepared to receive it (those listening on an SMTP port) (`true` - enable, `false` - disable)",
|
description: "only accept email from servers which seem prepared to receive it (those listening on an SMTP port) (`true` - enable, `false` - disable)",
|
||||||
|
|||||||
@@ -27,7 +27,9 @@ const (
|
|||||||
roomOptionNoThreads = "nothreads"
|
roomOptionNoThreads = "nothreads"
|
||||||
roomOptionNoFiles = "nofiles"
|
roomOptionNoFiles = "nofiles"
|
||||||
roomOptionPassword = "password"
|
roomOptionPassword = "password"
|
||||||
|
roomOptionSpamcheckDKIM = "spamcheck:dkim"
|
||||||
roomOptionSpamcheckSMTP = "spamcheck:smtp"
|
roomOptionSpamcheckSMTP = "spamcheck:smtp"
|
||||||
|
roomOptionSpamcheckSPF = "spamcheck:spf"
|
||||||
roomOptionSpamcheckMX = "spamcheck:mx"
|
roomOptionSpamcheckMX = "spamcheck:mx"
|
||||||
roomOptionSpamlist = "spamlist"
|
roomOptionSpamlist = "spamlist"
|
||||||
)
|
)
|
||||||
@@ -96,10 +98,18 @@ func (s roomSettings) NoFiles() bool {
|
|||||||
return utils.Bool(s.Get(roomOptionNoFiles))
|
return utils.Bool(s.Get(roomOptionNoFiles))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s roomSettings) SpamcheckDKIM() bool {
|
||||||
|
return utils.Bool(s.Get(roomOptionSpamcheckDKIM))
|
||||||
|
}
|
||||||
|
|
||||||
func (s roomSettings) SpamcheckSMTP() bool {
|
func (s roomSettings) SpamcheckSMTP() bool {
|
||||||
return utils.Bool(s.Get(roomOptionSpamcheckSMTP))
|
return utils.Bool(s.Get(roomOptionSpamcheckSMTP))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s roomSettings) SpamcheckSPF() bool {
|
||||||
|
return utils.Bool(s.Get(roomOptionSpamcheckSPF))
|
||||||
|
}
|
||||||
|
|
||||||
func (s roomSettings) SpamcheckMX() bool {
|
func (s roomSettings) SpamcheckMX() bool {
|
||||||
return utils.Bool(s.Get(roomOptionSpamcheckMX))
|
return utils.Bool(s.Get(roomOptionSpamcheckMX))
|
||||||
}
|
}
|
||||||
|
|||||||
42
docs/tricks.md
Normal file
42
docs/tricks.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# tricks
|
||||||
|
|
||||||
|
<!-- vim-markdown-toc GitLab -->
|
||||||
|
|
||||||
|
* [Logs](#logs)
|
||||||
|
* [get most active hosts](#get-most-active-hosts)
|
||||||
|
|
||||||
|
<!-- vim-markdown-toc -->
|
||||||
|
|
||||||
|
## Logs
|
||||||
|
|
||||||
|
### get most active hosts
|
||||||
|
|
||||||
|
Even if you use postmoogle as an internal mail server and contact "outside internet" quite rarely,
|
||||||
|
you will see lots of connections to your SMTP servers from random hosts over internet that do... nothing?
|
||||||
|
They don't send any valid emails or do something meaningful, thus you can safely assume they are spammers.
|
||||||
|
|
||||||
|
To get top X (in example: top 10) hosts with biggest count of attempts to connect to your postmoogle instance, follow the steps:
|
||||||
|
|
||||||
|
1. enable debug log: `export POSTMOOGLE_LOGLEVEL=debug`
|
||||||
|
2. restart postmoogle and wait some time to get stats
|
||||||
|
3. run the following bash one-liner to show top 10 hosts by connections count:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
journalctl -o cat -u postmoogle | grep "smtp.DEBUG accepted connection from " | grep -oE "[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}.[0-9]{1,3}" | sort | uniq -ci | sort -rn | head -n 10
|
||||||
|
253 111.111.111.111
|
||||||
|
183 222.222.222.222
|
||||||
|
39 333.333.333.333
|
||||||
|
38 444.444.444.444
|
||||||
|
18 555.555.555.555
|
||||||
|
16 666.666.666.666
|
||||||
|
8 777.777.777.777
|
||||||
|
5 888.888.888.888
|
||||||
|
5 999.999.999.999
|
||||||
|
4 010.010.010.010
|
||||||
|
```
|
||||||
|
|
||||||
|
of course, IP addresses above are crafted just to visualize their place in that top, according to the number of connections done.
|
||||||
|
In reality, you will see real IP addresses here. Usually, only hosts with hundreds or thousands of connections for the last 7 days worth checking.
|
||||||
|
|
||||||
|
What's next?
|
||||||
|
Do **not** ban them right away. Check WHOIS info for each host and only after that decide if you really want to ban that host or not.
|
||||||
@@ -2,7 +2,9 @@ package email
|
|||||||
|
|
||||||
// IncomingFilteringOptions for incoming mail
|
// IncomingFilteringOptions for incoming mail
|
||||||
type IncomingFilteringOptions interface {
|
type IncomingFilteringOptions interface {
|
||||||
|
SpamcheckDKIM() bool
|
||||||
SpamcheckSMTP() bool
|
SpamcheckSMTP() bool
|
||||||
|
SpamcheckSPF() bool
|
||||||
SpamcheckMX() bool
|
SpamcheckMX() bool
|
||||||
Spamlist() []string
|
Spamlist() []string
|
||||||
}
|
}
|
||||||
|
|||||||
3
go.mod
3
go.mod
@@ -20,12 +20,13 @@ require (
|
|||||||
gitlab.com/etke.cc/go/mxidwc v1.0.0
|
gitlab.com/etke.cc/go/mxidwc v1.0.0
|
||||||
gitlab.com/etke.cc/go/secgen v1.1.1
|
gitlab.com/etke.cc/go/secgen v1.1.1
|
||||||
gitlab.com/etke.cc/go/trysmtp v1.0.0
|
gitlab.com/etke.cc/go/trysmtp v1.0.0
|
||||||
gitlab.com/etke.cc/go/validator v1.0.4
|
gitlab.com/etke.cc/go/validator v1.0.5
|
||||||
gitlab.com/etke.cc/linkpearl v0.0.0-20221116205701-65547c5608e6
|
gitlab.com/etke.cc/linkpearl v0.0.0-20221116205701-65547c5608e6
|
||||||
maunium.net/go/mautrix v0.12.3
|
maunium.net/go/mautrix v0.12.3
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
|
blitiri.com.ar/go/spf v1.5.1 // indirect
|
||||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
|
||||||
github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 // indirect
|
github.com/gogs/chardet v0.0.0-20191104214054-4b6791f73a28 // indirect
|
||||||
github.com/google/go-cmp v0.5.8 // indirect
|
github.com/google/go-cmp v0.5.8 // indirect
|
||||||
|
|||||||
6
go.sum
6
go.sum
@@ -1,3 +1,5 @@
|
|||||||
|
blitiri.com.ar/go/spf v1.5.1 h1:CWUEasc44OrANJD8CzceRnRn1Jv0LttY68cYym2/pbE=
|
||||||
|
blitiri.com.ar/go/spf v1.5.1/go.mod h1:E71N92TfL4+Yyd5lpKuE9CAF2pd4JrUq1xQfkTxoNdk=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
|
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
|
||||||
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
|
github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
|
||||||
@@ -99,8 +101,8 @@ gitlab.com/etke.cc/go/secgen v1.1.1 h1:RmKOki725HIhWJHzPtAc9X4YvBneczndchpMgoDkE
|
|||||||
gitlab.com/etke.cc/go/secgen v1.1.1/go.mod h1:3pJqRGeWApzx7qXjABqz2o2SMCNpKSZao/gXVdasqE8=
|
gitlab.com/etke.cc/go/secgen v1.1.1/go.mod h1:3pJqRGeWApzx7qXjABqz2o2SMCNpKSZao/gXVdasqE8=
|
||||||
gitlab.com/etke.cc/go/trysmtp v1.0.0 h1:f/7gSmzohKniVeLSLevI+ZsySYcPUGkT9cRlOTwjOr8=
|
gitlab.com/etke.cc/go/trysmtp v1.0.0 h1:f/7gSmzohKniVeLSLevI+ZsySYcPUGkT9cRlOTwjOr8=
|
||||||
gitlab.com/etke.cc/go/trysmtp v1.0.0/go.mod h1:KqRuIB2IPElEEbAxXmFyKtm7S5YiuEb4lxwWthccqyE=
|
gitlab.com/etke.cc/go/trysmtp v1.0.0/go.mod h1:KqRuIB2IPElEEbAxXmFyKtm7S5YiuEb4lxwWthccqyE=
|
||||||
gitlab.com/etke.cc/go/validator v1.0.4 h1:2HIBP12f/RZr/7KTNH5/PgPTzl1vi7Co3lmhNTWB31A=
|
gitlab.com/etke.cc/go/validator v1.0.5 h1:YJs50yOHY3tKp/2NVP5U6Pbtvxof/YtGRpNOVjkGf+s=
|
||||||
gitlab.com/etke.cc/go/validator v1.0.4/go.mod h1:3vdssRG4LwgdTr9IHz9MjGSEO+3/FO9hXPGMuSeweJ8=
|
gitlab.com/etke.cc/go/validator v1.0.5/go.mod h1:Id0SxRj0J3IPhiKlj0w1plxVLZfHlkwipn7HfRZsDts=
|
||||||
gitlab.com/etke.cc/linkpearl v0.0.0-20221116205701-65547c5608e6 h1:+HDT2/bx3Hug++aeDE/PaoRRcnKdYzEm6i2RlOAzPXo=
|
gitlab.com/etke.cc/linkpearl v0.0.0-20221116205701-65547c5608e6 h1:+HDT2/bx3Hug++aeDE/PaoRRcnKdYzEm6i2RlOAzPXo=
|
||||||
gitlab.com/etke.cc/linkpearl v0.0.0-20221116205701-65547c5608e6/go.mod h1:Dgtu0qvymNjjky4Bu5WC8+iSohcb5xZ9CtkD3ezDqIA=
|
gitlab.com/etke.cc/linkpearl v0.0.0-20221116205701-65547c5608e6/go.mod h1:Dgtu0qvymNjjky4Bu5WC8+iSohcb5xZ9CtkD3ezDqIA=
|
||||||
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
package smtp
|
package smtp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
|
|
||||||
|
"github.com/emersion/go-msgauth/dkim"
|
||||||
"github.com/emersion/go-smtp"
|
"github.com/emersion/go-smtp"
|
||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
"github.com/jhillyerd/enmime"
|
"github.com/jhillyerd/enmime"
|
||||||
@@ -26,6 +28,7 @@ type incomingSession struct {
|
|||||||
greylisted func(net.Addr) bool
|
greylisted func(net.Addr) bool
|
||||||
ban func(net.Addr)
|
ban func(net.Addr)
|
||||||
domains []string
|
domains []string
|
||||||
|
enforceDKIM bool
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
addr net.Addr
|
addr net.Addr
|
||||||
@@ -68,7 +71,8 @@ func (s *incomingSession) Rcpt(to string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
validations := s.getFilters(roomID)
|
validations := s.getFilters(roomID)
|
||||||
if !validateEmail(s.from, to, s.log, validations) {
|
s.enforceDKIM = validations.SpamcheckDKIM()
|
||||||
|
if !validateIncoming(s.from, to, s.addr, s.log, validations) {
|
||||||
s.ban(s.addr)
|
s.ban(s.addr)
|
||||||
return ErrBanned
|
return ErrBanned
|
||||||
}
|
}
|
||||||
@@ -85,8 +89,28 @@ func (s *incomingSession) Data(r io.Reader) error {
|
|||||||
Message: "You have been greylisted, try again a bit later.",
|
Message: "You have been greylisted, try again a bit later.",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
data, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
s.log.Error("cannot read DATA: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
if s.enforceDKIM {
|
||||||
|
results, verr := dkim.Verify(reader)
|
||||||
|
if verr != nil {
|
||||||
|
s.log.Error("cannot verify DKIM: %v", verr)
|
||||||
|
return verr
|
||||||
|
}
|
||||||
|
for _, result := range results {
|
||||||
|
if result.Err != nil {
|
||||||
|
s.log.Info("DKIM verification of %q failed: %v", result.Domain, result.Err)
|
||||||
|
return result.Err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reader.Seek(0, io.SeekStart) //nolint:errcheck
|
||||||
|
}
|
||||||
parser := enmime.NewParser()
|
parser := enmime.NewParser()
|
||||||
envelope, err := parser.ReadEnvelope(r)
|
envelope, err := parser.ReadEnvelope(reader)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -176,13 +200,23 @@ func (s *outgoingSession) Data(r io.Reader) error {
|
|||||||
func (s *outgoingSession) Reset() {}
|
func (s *outgoingSession) Reset() {}
|
||||||
func (s *outgoingSession) Logout() error { return nil }
|
func (s *outgoingSession) Logout() error { return nil }
|
||||||
|
|
||||||
func validateEmail(from, to string, log *logger.Logger, options email.IncomingFilteringOptions) bool {
|
func validateIncoming(from, to string, senderAddr net.Addr, log *logger.Logger, options email.IncomingFilteringOptions) bool {
|
||||||
|
var sender net.IP
|
||||||
|
switch netaddr := senderAddr.(type) {
|
||||||
|
case *net.TCPAddr:
|
||||||
|
sender = netaddr.IP
|
||||||
|
default:
|
||||||
|
host, _, _ := net.SplitHostPort(senderAddr.String()) // nolint:errcheck
|
||||||
|
sender = net.ParseIP(host)
|
||||||
|
}
|
||||||
|
|
||||||
enforce := validator.Enforce{
|
enforce := validator.Enforce{
|
||||||
Email: true,
|
Email: true,
|
||||||
MX: options.SpamcheckMX(),
|
MX: options.SpamcheckMX(),
|
||||||
|
SPF: options.SpamcheckSPF(),
|
||||||
SMTP: options.SpamcheckSMTP(),
|
SMTP: options.SpamcheckSMTP(),
|
||||||
}
|
}
|
||||||
v := validator.New(options.Spamlist(), enforce, to, log)
|
v := validator.New(options.Spamlist(), enforce, to, log)
|
||||||
|
|
||||||
return v.Email(from)
|
return v.Email(from, sender)
|
||||||
}
|
}
|
||||||
|
|||||||
10
vendor/blitiri.com.ar/go/spf/.gitignore
generated
vendored
Normal file
10
vendor/blitiri.com.ar/go/spf/.gitignore
generated
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
# Ignore anything beginning with a dot: these are usually temporary or
|
||||||
|
# unimportant.
|
||||||
|
.*
|
||||||
|
|
||||||
|
# Exceptions to the rule above: files we care about that would otherwise be
|
||||||
|
# excluded.
|
||||||
|
!.gitignore
|
||||||
|
|
||||||
|
# go-fuzz build artifacts.
|
||||||
|
*-fuzz.zip
|
||||||
27
vendor/blitiri.com.ar/go/spf/LICENSE
generated
vendored
Normal file
27
vendor/blitiri.com.ar/go/spf/LICENSE
generated
vendored
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
Licensed under the MIT licence, which is reproduced below (from
|
||||||
|
https://opensource.org/licenses/MIT).
|
||||||
|
|
||||||
|
-----
|
||||||
|
|
||||||
|
Copyright (c) 2016
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
|
||||||
49
vendor/blitiri.com.ar/go/spf/README.md
generated
vendored
Normal file
49
vendor/blitiri.com.ar/go/spf/README.md
generated
vendored
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
|
||||||
|
# blitiri.com.ar/go/spf
|
||||||
|
|
||||||
|
[](https://pkg.go.dev/blitiri.com.ar/go/spf)
|
||||||
|
[](https://gitlab.com/albertito/spf/-/pipelines)
|
||||||
|
[](https://goreportcard.com/report/github.com/albertito/spf)
|
||||||
|
[](https://coveralls.io/github/albertito/spf)
|
||||||
|
|
||||||
|
[spf](https://godoc.org/blitiri.com.ar/go/spf) is an open source
|
||||||
|
implementation of the [Sender Policy Framework
|
||||||
|
(SPF)](https://en.wikipedia.org/wiki/Sender_Policy_Framework) in Go.
|
||||||
|
|
||||||
|
It is used by the [chasquid](https://blitiri.com.ar/p/chasquid/) and
|
||||||
|
[maddy](https://maddy.email) SMTP servers.
|
||||||
|
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Check if `sender` is authorized to send from the given `ip`. The `domain`
|
||||||
|
// is used if the sender doesn't have one.
|
||||||
|
result, err := spf.CheckHostWithSender(ip, domain, sender)
|
||||||
|
if result == spf.Fail {
|
||||||
|
// Not authorized to send.
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
See the [package documentation](https://pkg.go.dev/blitiri.com.ar/go/spf) for
|
||||||
|
more details.
|
||||||
|
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
All SPF mechanisms, modifiers, and macros are supported.
|
||||||
|
|
||||||
|
The API should be considered stable. Major version changes will be announced
|
||||||
|
to the mailing list (details below).
|
||||||
|
|
||||||
|
|
||||||
|
## Contact
|
||||||
|
|
||||||
|
If you have any questions, comments or patches please send them to the mailing
|
||||||
|
list, `chasquid@googlegroups.com`.
|
||||||
|
|
||||||
|
To subscribe, send an email to `chasquid+subscribe@googlegroups.com`.
|
||||||
|
|
||||||
|
You can also browse the
|
||||||
|
[archives](https://groups.google.com/forum/#!forum/chasquid).
|
||||||
|
|
||||||
58
vendor/blitiri.com.ar/go/spf/fuzz.go
generated
vendored
Normal file
58
vendor/blitiri.com.ar/go/spf/fuzz.go
generated
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// Fuzz testing for package spf.
|
||||||
|
//
|
||||||
|
// Run it with:
|
||||||
|
//
|
||||||
|
// go-fuzz-build blitiri.com.ar/go/spf
|
||||||
|
// go-fuzz -bin=./spf-fuzz.zip -workdir=testdata/fuzz
|
||||||
|
//
|
||||||
|
|
||||||
|
//go:build gofuzz
|
||||||
|
// +build gofuzz
|
||||||
|
|
||||||
|
package spf
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
|
||||||
|
"blitiri.com.ar/go/spf/internal/dnstest"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parsed IP addresses, for convenience.
|
||||||
|
var (
|
||||||
|
ip1110 = net.ParseIP("1.1.1.0")
|
||||||
|
ip1111 = net.ParseIP("1.1.1.1")
|
||||||
|
ip6666 = net.ParseIP("2001:db8::68")
|
||||||
|
ip6660 = net.ParseIP("2001:db8::0")
|
||||||
|
)
|
||||||
|
|
||||||
|
// DNS resolver to use. Will be initialized once with the expected fixtures,
|
||||||
|
// and then reused on each fuzz run.
|
||||||
|
var dns = dnstest.NewResolver()
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dns.Ip["d1111"] = []net.IP{ip1111}
|
||||||
|
dns.Ip["d1110"] = []net.IP{ip1110}
|
||||||
|
dns.Mx["d1110"] = []*net.MX{{"d1110", 5}, {"nothing", 10}}
|
||||||
|
dns.Ip["d6666"] = []net.IP{ip6666}
|
||||||
|
dns.Ip["d6660"] = []net.IP{ip6660}
|
||||||
|
dns.Mx["d6660"] = []*net.MX{{"d6660", 5}, {"nothing", 10}}
|
||||||
|
dns.Addr["2001:db8::68"] = []string{"sonlas6.", "domain.", "d6666."}
|
||||||
|
dns.Addr["1.1.1.1"] = []string{"lalala.", "domain.", "d1111."}
|
||||||
|
}
|
||||||
|
|
||||||
|
func Fuzz(data []byte) int {
|
||||||
|
// The domain's TXT record comes from the fuzzer.
|
||||||
|
dns.Txt["domain"] = []string{string(data)}
|
||||||
|
|
||||||
|
v4result, _ := CheckHostWithSender(
|
||||||
|
ip1111, "helo", "domain", WithResolver(dns))
|
||||||
|
v6result, _ := CheckHostWithSender(
|
||||||
|
ip6666, "helo", "domain", WithResolver(dns))
|
||||||
|
|
||||||
|
// Raise priority if any of the results was something other than
|
||||||
|
// PermError, as it means the data was better formed.
|
||||||
|
if v4result != PermError || v6result != PermError {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
111
vendor/blitiri.com.ar/go/spf/internal/dnstest/dns.go
generated
vendored
Normal file
111
vendor/blitiri.com.ar/go/spf/internal/dnstest/dns.go
generated
vendored
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
// DNS resolver for testing purposes.
|
||||||
|
//
|
||||||
|
// In the future, when go fuzz can make use of _test.go files, we can rename
|
||||||
|
// this file dns_test.go and remove this extra package entirely.
|
||||||
|
// Until then, unfortunately this is the most reasonable way to share these
|
||||||
|
// helpers between go and fuzz tests.
|
||||||
|
package dnstest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Testing DNS resolver.
|
||||||
|
//
|
||||||
|
// Not exported since this is not part of the public API and only used
|
||||||
|
// internally on tests.
|
||||||
|
//
|
||||||
|
type TestResolver struct {
|
||||||
|
Txt map[string][]string
|
||||||
|
Mx map[string][]*net.MX
|
||||||
|
Ip map[string][]net.IP
|
||||||
|
Addr map[string][]string
|
||||||
|
Cname map[string]string
|
||||||
|
Errors map[string]error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResolver() *TestResolver {
|
||||||
|
return &TestResolver{
|
||||||
|
Txt: map[string][]string{},
|
||||||
|
Mx: map[string][]*net.MX{},
|
||||||
|
Ip: map[string][]net.IP{},
|
||||||
|
Addr: map[string][]string{},
|
||||||
|
Cname: map[string]string{},
|
||||||
|
Errors: map[string]error{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var nxDomainErr = &net.DNSError{
|
||||||
|
Err: "domain not found (for testing)",
|
||||||
|
IsNotFound: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TestResolver) LookupTXT(ctx context.Context, domain string) (txts []string, err error) {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
domain = strings.ToLower(domain)
|
||||||
|
domain = strings.TrimRight(domain, ".")
|
||||||
|
if cname, ok := r.Cname[domain]; ok {
|
||||||
|
return r.LookupTXT(ctx, cname)
|
||||||
|
}
|
||||||
|
if _, ok := r.Txt[domain]; !ok && r.Errors[domain] == nil {
|
||||||
|
return nil, nxDomainErr
|
||||||
|
}
|
||||||
|
return r.Txt[domain], r.Errors[domain]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TestResolver) LookupMX(ctx context.Context, domain string) (mxs []*net.MX, err error) {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
domain = strings.ToLower(domain)
|
||||||
|
domain = strings.TrimRight(domain, ".")
|
||||||
|
if cname, ok := r.Cname[domain]; ok {
|
||||||
|
return r.LookupMX(ctx, cname)
|
||||||
|
}
|
||||||
|
if _, ok := r.Mx[domain]; !ok && r.Errors[domain] == nil {
|
||||||
|
return nil, nxDomainErr
|
||||||
|
}
|
||||||
|
return r.Mx[domain], r.Errors[domain]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TestResolver) LookupIPAddr(ctx context.Context, host string) (as []net.IPAddr, err error) {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
host = strings.ToLower(host)
|
||||||
|
host = strings.TrimRight(host, ".")
|
||||||
|
if cname, ok := r.Cname[host]; ok {
|
||||||
|
return r.LookupIPAddr(ctx, cname)
|
||||||
|
}
|
||||||
|
if _, ok := r.Ip[host]; !ok && r.Errors[host] == nil {
|
||||||
|
return nil, nxDomainErr
|
||||||
|
}
|
||||||
|
return ipsToAddrs(r.Ip[host]), r.Errors[host]
|
||||||
|
}
|
||||||
|
|
||||||
|
func ipsToAddrs(ips []net.IP) []net.IPAddr {
|
||||||
|
as := []net.IPAddr{}
|
||||||
|
for _, ip := range ips {
|
||||||
|
as = append(as, net.IPAddr{IP: ip, Zone: ""})
|
||||||
|
}
|
||||||
|
return as
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *TestResolver) LookupAddr(ctx context.Context, host string) (addrs []string, err error) {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return nil, ctx.Err()
|
||||||
|
}
|
||||||
|
host = strings.ToLower(host)
|
||||||
|
host = strings.TrimRight(host, ".")
|
||||||
|
if cname, ok := r.Cname[host]; ok {
|
||||||
|
return r.LookupAddr(ctx, cname)
|
||||||
|
}
|
||||||
|
if _, ok := r.Addr[host]; !ok && r.Errors[host] == nil {
|
||||||
|
return nil, nxDomainErr
|
||||||
|
}
|
||||||
|
return r.Addr[host], r.Errors[host]
|
||||||
|
}
|
||||||
1044
vendor/blitiri.com.ar/go/spf/spf.go
generated
vendored
Normal file
1044
vendor/blitiri.com.ar/go/spf/spf.go
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
20
vendor/gitlab.com/etke.cc/go/validator/emails.go
generated
vendored
20
vendor/gitlab.com/etke.cc/go/validator/emails.go
generated
vendored
@@ -1,14 +1,21 @@
|
|||||||
package validator
|
package validator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"blitiri.com.ar/go/spf"
|
||||||
"gitlab.com/etke.cc/go/trysmtp"
|
"gitlab.com/etke.cc/go/trysmtp"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Email checks if email is valid
|
// Email checks if email is valid
|
||||||
func (v *V) Email(email string) bool {
|
func (v *V) Email(email string, optionalSenderIP ...net.IP) bool {
|
||||||
|
var senderIP net.IP
|
||||||
|
if len(optionalSenderIP) > 0 {
|
||||||
|
senderIP = optionalSenderIP[0]
|
||||||
|
}
|
||||||
|
|
||||||
// edge case: email may be optional
|
// edge case: email may be optional
|
||||||
if email == "" {
|
if email == "" {
|
||||||
return !v.enforce.Email
|
return !v.enforce.Email
|
||||||
@@ -41,6 +48,12 @@ func (v *V) Email(email string) bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if v.enforce.SPF {
|
||||||
|
if v.emailNoSPF(email, senderIP) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if v.enforce.SMTP {
|
if v.enforce.SMTP {
|
||||||
if v.emailNoSMTP(email) {
|
if v.emailNoSMTP(email) {
|
||||||
return false
|
return false
|
||||||
@@ -78,3 +91,8 @@ func (v *V) emailNoSMTP(email string) bool {
|
|||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *V) emailNoSPF(email string, senderIP net.IP) bool {
|
||||||
|
result, _ := spf.CheckHostWithSender(senderIP, "", email, spf.WithTraceFunc(v.log.Info)) //nolint:errcheck // not a error
|
||||||
|
return result == spf.Fail
|
||||||
|
}
|
||||||
|
|||||||
2
vendor/gitlab.com/etke.cc/go/validator/validator.go
generated
vendored
2
vendor/gitlab.com/etke.cc/go/validator/validator.go
generated
vendored
@@ -21,6 +21,8 @@ type Enforce struct {
|
|||||||
Domain bool
|
Domain bool
|
||||||
// SMTP enforces SMTP check (email actually exists on mail server) and rejects non-existing emails
|
// SMTP enforces SMTP check (email actually exists on mail server) and rejects non-existing emails
|
||||||
SMTP bool
|
SMTP bool
|
||||||
|
// SPF enforces SPF record check (sender allowed to use that email and send emails) and rejects unathorized emails
|
||||||
|
SPF bool
|
||||||
// MX enforces MX records check on email's mail server
|
// MX enforces MX records check on email's mail server
|
||||||
MX bool
|
MX bool
|
||||||
}
|
}
|
||||||
|
|||||||
6
vendor/modules.txt
vendored
6
vendor/modules.txt
vendored
@@ -1,3 +1,7 @@
|
|||||||
|
# blitiri.com.ar/go/spf v1.5.1
|
||||||
|
## explicit; go 1.15
|
||||||
|
blitiri.com.ar/go/spf
|
||||||
|
blitiri.com.ar/go/spf/internal/dnstest
|
||||||
# github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a
|
# github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a
|
||||||
## explicit
|
## explicit
|
||||||
github.com/cention-sany/utf7
|
github.com/cention-sany/utf7
|
||||||
@@ -127,7 +131,7 @@ gitlab.com/etke.cc/go/secgen
|
|||||||
# gitlab.com/etke.cc/go/trysmtp v1.0.0
|
# gitlab.com/etke.cc/go/trysmtp v1.0.0
|
||||||
## explicit; go 1.18
|
## explicit; go 1.18
|
||||||
gitlab.com/etke.cc/go/trysmtp
|
gitlab.com/etke.cc/go/trysmtp
|
||||||
# gitlab.com/etke.cc/go/validator v1.0.4
|
# gitlab.com/etke.cc/go/validator v1.0.5
|
||||||
## explicit; go 1.18
|
## explicit; go 1.18
|
||||||
gitlab.com/etke.cc/go/validator
|
gitlab.com/etke.cc/go/validator
|
||||||
# gitlab.com/etke.cc/linkpearl v0.0.0-20221116205701-65547c5608e6
|
# gitlab.com/etke.cc/linkpearl v0.0.0-20221116205701-65547c5608e6
|
||||||
|
|||||||
Reference in New Issue
Block a user