mirror of
https://codeberg.org/forgejo/forgejo
synced 2024-11-22 09:54:24 +01:00
feat: add setting to block disposable emails
- Add a new setting `EMAIL_DOMAIN_BLOCK_DISPOSABLE` that will append a list of domains that are known for being used by temporary or disposable email services. - Add a utility to automatically download and format the list of domains from the disposable-email-domains project on github. (https://github.com/disposable-email-domains/disposable-email-domains) license: CC0 1.0 Universal (CC0 1.0) [Public Domain] from README: """ This repo contains a list of disposable and temporary email address domains often used to register dummy users in order to spam or abuse some services. We cannot guarantee all of these can still be considered disposable but we do basic checking so chances are they were disposable at one point in time. """
This commit is contained in:
parent
ef9a0c8d3d
commit
735c2c8ada
109
build/generate-disposable-email.go
Normal file
109
build/generate-disposable-email.go
Normal file
|
@ -0,0 +1,109 @@
|
|||
// Copyright 2024 James Hatfield
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
//go:build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const disposableEmailListURL string = "https://raw.githubusercontent.com/disposable-email-domains/disposable-email-domains/%s/disposable_email_blocklist.conf"
|
||||
|
||||
var (
|
||||
gitRef *string = flag.String("r", "master", "Git reference of the domain list version")
|
||||
outPat *string = flag.String("o", "modules/setting/disposable_email_domain_data.go", "Output path")
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
|
||||
// generate the source code (array of domains)
|
||||
res, err := generate()
|
||||
if err != nil {
|
||||
log.Fatalf("Generation Error: %v", err)
|
||||
}
|
||||
|
||||
// write result to a file
|
||||
err = os.WriteFile(*outPat, res, 0o644)
|
||||
if err != nil {
|
||||
log.Fatalf("File Write Error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func generate() ([]byte, error) {
|
||||
var err error
|
||||
var url string = fmt.Sprintf(disposableEmailListURL, *gitRef)
|
||||
|
||||
// download the domain list
|
||||
res, err := http.Get(url)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// go through all entries (1 domain per line)
|
||||
scanner := bufio.NewScanner(bytes.NewReader(body))
|
||||
|
||||
var arrDomains []string
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
arrDomains = append(arrDomains, line)
|
||||
}
|
||||
|
||||
// build the string in a readable way
|
||||
var sb strings.Builder
|
||||
|
||||
_, err = sb.WriteString("[]string{\n")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, item := range arrDomains {
|
||||
_, err = sb.WriteString(fmt.Sprintf("\t%q,\n", item))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
_, err = sb.WriteString("}")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// insert the values into file
|
||||
final := fmt.Sprintf(hdr, url, sb.String())
|
||||
|
||||
return format.Source([]byte(final))
|
||||
}
|
||||
|
||||
const hdr = `
|
||||
// Copyright 2024 James Hatfield
|
||||
// SPDX-License-Identifier: MIT
|
||||
//
|
||||
// Code generated by build/generate-disposable-email.go. DO NOT EDIT
|
||||
// Sourced from %s
|
||||
package setting
|
||||
|
||||
import "sync"
|
||||
|
||||
var DisposableEmailDomains = sync.OnceValue(func() []string {
|
||||
return %s
|
||||
})
|
||||
`
|
3811
modules/setting/disposable_email_domain_data.go
Normal file
3811
modules/setting/disposable_email_domain_data.go
Normal file
File diff suppressed because it is too large
Load diff
|
@ -5,6 +5,7 @@ package setting
|
|||
|
||||
import (
|
||||
"regexp"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -37,6 +38,7 @@ var Service = struct {
|
|||
RegisterManualConfirm bool
|
||||
EmailDomainAllowList []glob.Glob
|
||||
EmailDomainBlockList []glob.Glob
|
||||
EmailDomainBlockDisposable bool
|
||||
DisableRegistration bool
|
||||
AllowOnlyInternalRegistration bool
|
||||
AllowOnlyExternalRegistration bool
|
||||
|
@ -156,6 +158,22 @@ func loadServiceFrom(rootCfg ConfigProvider) {
|
|||
}
|
||||
Service.EmailDomainAllowList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_WHITELIST", "EMAIL_DOMAIN_ALLOWLIST")
|
||||
Service.EmailDomainBlockList = CompileEmailGlobList(sec, "EMAIL_DOMAIN_BLOCKLIST")
|
||||
Service.EmailDomainBlockDisposable = sec.Key("EMAIL_DOMAIN_BLOCK_DISPOSABLE").MustBool(false)
|
||||
if Service.EmailDomainBlockDisposable {
|
||||
toAdd := make([]glob.Glob, 0, len(DisposableEmailDomains()))
|
||||
for _, domain := range DisposableEmailDomains() {
|
||||
domain = strings.ToLower(domain)
|
||||
// Only add domains that aren't blocked yet.
|
||||
if !slices.ContainsFunc(Service.EmailDomainBlockList, func(g glob.Glob) bool { return g.Match(domain) }) {
|
||||
if g, err := glob.Compile(domain); err != nil {
|
||||
log.Error("Error in disposable domain %s: %v", domain, err)
|
||||
} else {
|
||||
toAdd = append(toAdd, g)
|
||||
}
|
||||
}
|
||||
}
|
||||
Service.EmailDomainBlockList = append(Service.EmailDomainBlockList, toAdd...)
|
||||
}
|
||||
Service.ShowRegistrationButton = sec.Key("SHOW_REGISTRATION_BUTTON").MustBool(!(Service.DisableRegistration || Service.AllowOnlyExternalRegistration))
|
||||
Service.ShowMilestonesDashboardPage = sec.Key("SHOW_MILESTONES_DASHBOARD_PAGE").MustBool(true)
|
||||
Service.RequireSignInView = sec.Key("REQUIRE_SIGNIN_VIEW").MustBool()
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
package setting
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"code.gitea.io/gitea/modules/structs"
|
||||
|
@ -13,6 +14,15 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func match(globs []glob.Glob, s string) bool {
|
||||
for _, g := range globs {
|
||||
if g.Match(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func TestLoadServices(t *testing.T) {
|
||||
oldService := Service
|
||||
defer func() {
|
||||
|
@ -28,15 +38,6 @@ EMAIL_DOMAIN_BLOCKLIST = d3, *.b
|
|||
require.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
|
||||
match := func(globs []glob.Glob, s string) bool {
|
||||
for _, g := range globs {
|
||||
if g.Match(s) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
assert.True(t, match(Service.EmailDomainAllowList, "d1"))
|
||||
assert.True(t, match(Service.EmailDomainAllowList, "foo.w"))
|
||||
assert.True(t, match(Service.EmailDomainAllowList, "d2"))
|
||||
|
@ -48,6 +49,97 @@ EMAIL_DOMAIN_BLOCKLIST = d3, *.b
|
|||
assert.False(t, match(Service.EmailDomainBlockList, "d1"))
|
||||
}
|
||||
|
||||
func TestLoadServiceBlockDisposable(t *testing.T) {
|
||||
oldService := Service
|
||||
defer func() {
|
||||
Service = oldService
|
||||
}()
|
||||
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[service]
|
||||
EMAIL_DOMAIN_BLOCK_DISPOSABLE = true
|
||||
`)
|
||||
|
||||
require.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
|
||||
for _, domain := range DisposableEmailDomains() {
|
||||
require.True(t, match(Service.EmailDomainBlockList, domain))
|
||||
}
|
||||
|
||||
require.Len(t, Service.EmailDomainBlockList, len(DisposableEmailDomains()))
|
||||
|
||||
knownGood := [...]string{
|
||||
"aol.com",
|
||||
"gmx.com",
|
||||
"mail.com",
|
||||
"zoho.com",
|
||||
"proton.me",
|
||||
"gmail.com",
|
||||
"yahoo.com",
|
||||
"icloud.com",
|
||||
"outlook.com",
|
||||
"protonmail.com",
|
||||
}
|
||||
|
||||
for _, domain := range knownGood {
|
||||
require.False(t, match(Service.EmailDomainBlockList, domain))
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadServiceBlockDisposableWithExistingGlobs(t *testing.T) {
|
||||
oldService := Service
|
||||
defer func() {
|
||||
Service = oldService
|
||||
}()
|
||||
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[service]
|
||||
EMAIL_DOMAIN_BLOCKLIST = *.ru,*.org
|
||||
EMAIL_DOMAIN_BLOCK_DISPOSABLE = true
|
||||
`)
|
||||
|
||||
require.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
|
||||
for _, domain := range DisposableEmailDomains() {
|
||||
require.True(t, match(Service.EmailDomainBlockList, domain))
|
||||
}
|
||||
|
||||
redundant := 0
|
||||
for _, val := range DisposableEmailDomains() {
|
||||
if strings.HasSuffix(val, ".ru") || strings.HasSuffix(val, ".org") {
|
||||
redundant++
|
||||
}
|
||||
}
|
||||
|
||||
expected := len(DisposableEmailDomains()) - redundant + 2
|
||||
require.Len(t, Service.EmailDomainBlockList, expected)
|
||||
}
|
||||
|
||||
func TestLoadServiceBlockDisposableWithComplementGlobs(t *testing.T) {
|
||||
oldService := Service
|
||||
defer func() {
|
||||
Service = oldService
|
||||
}()
|
||||
|
||||
cfg, err := NewConfigProviderFromData(`
|
||||
[service]
|
||||
EMAIL_DOMAIN_BLOCKLIST = *.random
|
||||
EMAIL_DOMAIN_BLOCK_DISPOSABLE = true
|
||||
`)
|
||||
|
||||
require.NoError(t, err)
|
||||
loadServiceFrom(cfg)
|
||||
|
||||
for _, domain := range DisposableEmailDomains() {
|
||||
require.True(t, match(Service.EmailDomainBlockList, domain))
|
||||
}
|
||||
|
||||
expected := len(DisposableEmailDomains()) + 1
|
||||
require.Len(t, Service.EmailDomainBlockList, expected)
|
||||
}
|
||||
|
||||
func TestLoadServiceVisibilityModes(t *testing.T) {
|
||||
oldService := Service
|
||||
defer func() {
|
||||
|
|
Loading…
Reference in a new issue