SPF and DKIM checks

This commit is contained in:
Aine
2022-11-23 21:30:13 +02:00
parent 0701f8c9c3
commit 3115373118
17 changed files with 1437 additions and 9 deletions

View File

@@ -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@*`

View File

@@ -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)",

View File

@@ -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
View 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.

View File

@@ -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
View File

@@ -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
View File

@@ -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=

View File

@@ -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
View 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
View 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
View File

@@ -0,0 +1,49 @@
# blitiri.com.ar/go/spf
[![GoDoc](https://godoc.org/blitiri.com.ar/go/spf?status.svg)](https://pkg.go.dev/blitiri.com.ar/go/spf)
[![Build Status](https://gitlab.com/albertito/spf/badges/master/pipeline.svg)](https://gitlab.com/albertito/spf/-/pipelines)
[![Go Report Card](https://goreportcard.com/badge/github.com/albertito/spf)](https://goreportcard.com/report/github.com/albertito/spf)
[![Coverage Status](https://coveralls.io/repos/github/albertito/spf/badge.svg?branch=next)](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
View 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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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
}

View File

@@ -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
View File

@@ -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