Skip to content

Commit 091aef9

Browse files
authored
feat: Block specific outgoing mail servers (#1971)
## What kind of change does this PR introduce? Feature that gives configuration option to block an email address event if the mx server of the domain is on a blocklist ## What is the current behavior? Existing behavior only checks for syntax issues and single email addresses against a message stream. ## What is the new behavior? This is called on every sent email event, the mx server of the email addresses domain is queried and checked against a hard-coded blocklist ## Additional context Functionality to allow for the long term blocking of bot and spam behavior. Resolves SEC-245
1 parent ccf20d7 commit 091aef9

File tree

3 files changed

+62
-11
lines changed

3 files changed

+62
-11
lines changed

internal/conf/configuration.go

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,10 @@ type MailerConfiguration struct {
411411
EmailValidationExtended bool `json:"email_validation_extended" split_words:"true" default:"false"`
412412
EmailValidationServiceURL string `json:"email_validation_service_url" split_words:"true"`
413413
EmailValidationServiceHeaders string `json:"email_validation_service_headers" split_words:"true"`
414+
EmailValidationBlockedMX string `json:"email_validation_blocked_mx" split_words:"true"`
414415

415-
serviceHeaders map[string][]string `json:"-"`
416+
serviceHeaders map[string][]string `json:"-"`
417+
blockedMXRecords map[string]bool `json:"-"`
416418
}
417419

418420
func (c *MailerConfiguration) Validate() error {
@@ -428,13 +430,35 @@ func (c *MailerConfiguration) Validate() error {
428430
if len(headers) > 0 {
429431
c.serviceHeaders = headers
430432
}
433+
434+
// EmailValidationBlockedMX is a JSON array in the config string for brevity.
435+
var blockedMXRecords map[string]bool
436+
if c.EmailValidationBlockedMX != "" {
437+
var blockedMXArray []string
438+
err := json.Unmarshal([]byte(c.EmailValidationBlockedMX), &blockedMXArray)
439+
if err != nil {
440+
return fmt.Errorf("conf: email_validation_blocked_mx is not a valid JSON array: %w", err)
441+
}
442+
blockedMXRecords = make(map[string]bool, len(blockedMXArray)*2)
443+
for _, record := range blockedMXArray {
444+
blockedMXRecords[record] = true
445+
blockedMXRecords[record+"."] = true
446+
}
447+
}
448+
449+
c.blockedMXRecords = blockedMXRecords
450+
431451
return nil
432452
}
433453

434454
func (c *MailerConfiguration) GetEmailValidationServiceHeaders() map[string][]string {
435455
return c.serviceHeaders
436456
}
437457

458+
func (c *MailerConfiguration) GetEmailValidationBlockedMXRecords() map[string]bool {
459+
return c.blockedMXRecords
460+
}
461+
438462
type PhoneProviderConfiguration struct {
439463
Enabled bool `json:"enabled" default:"false"`
440464
}

internal/mailer/validate.go

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -74,19 +74,22 @@ var (
7474
ErrInvalidEmailAddress = errors.New("invalid_email_address")
7575
ErrInvalidEmailFormat = errors.New("invalid_email_format")
7676
ErrInvalidEmailDNS = errors.New("invalid_email_dns")
77+
ErrInvalidEmailMX = errors.New("invalid_email_mx")
7778
)
7879

7980
type EmailValidator struct {
80-
extended bool
81-
serviceURL string
82-
serviceHeaders map[string][]string
81+
extended bool
82+
serviceURL string
83+
serviceHeaders map[string][]string
84+
blockedMXRecords map[string]bool
8385
}
8486

8587
func newEmailValidator(mc conf.MailerConfiguration) *EmailValidator {
8688
return &EmailValidator{
87-
extended: mc.EmailValidationExtended,
88-
serviceURL: mc.EmailValidationServiceURL,
89-
serviceHeaders: mc.GetEmailValidationServiceHeaders(),
89+
extended: mc.EmailValidationExtended,
90+
serviceURL: mc.EmailValidationServiceURL,
91+
serviceHeaders: mc.GetEmailValidationServiceHeaders(),
92+
blockedMXRecords: mc.GetEmailValidationBlockedMXRecords(),
9093
}
9194
}
9295

@@ -253,20 +256,34 @@ func (ev *EmailValidator) validateProviders(name, host string) error {
253256
}
254257

255258
func (ev *EmailValidator) validateHost(ctx context.Context, host string) error {
256-
_, err := validateEmailResolver.LookupMX(ctx, host)
259+
mxs, err := validateEmailResolver.LookupMX(ctx, host)
257260
if !isHostNotFound(err) {
258-
return nil
261+
return ev.validateMXRecords(mxs, nil)
259262
}
260263

261-
_, err = validateEmailResolver.LookupHost(ctx, host)
264+
hosts, err := validateEmailResolver.LookupHost(ctx, host)
262265
if !isHostNotFound(err) {
263-
return nil
266+
return ev.validateMXRecords(nil, hosts)
264267
}
265268

266269
// No addrs or mx records were found
267270
return ErrInvalidEmailDNS
268271
}
269272

273+
func (ev *EmailValidator) validateMXRecords(mxs []*net.MX, hosts []string) error {
274+
for _, mx := range mxs {
275+
if ev.blockedMXRecords[mx.Host] {
276+
return ErrInvalidEmailMX
277+
}
278+
}
279+
for _, host := range hosts {
280+
if ev.blockedMXRecords[host] {
281+
return ErrInvalidEmailMX
282+
}
283+
}
284+
return nil
285+
}
286+
270287
func isHostNotFound(err error) bool {
271288
if err == nil {
272289
// We had no err, so we treat it as valid. We don't check the mx records

internal/mailer/validate_test.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,9 @@ func TestValidateEmailExtended(t *testing.T) {
242242
{email: "[email protected]", err: "invalid_email_dns"},
243243
{email: "[email protected]", err: "invalid_email_dns"},
244244

245+
// test blocked mx records
246+
{email: "[email protected]", err: "invalid_email_mx"},
247+
245248
// this low timeout should simulate a dns timeout, which should
246249
// not be treated as an invalid email.
247250
{email: "[email protected]",
@@ -255,7 +258,14 @@ func TestValidateEmailExtended(t *testing.T) {
255258
EmailValidationExtended: true,
256259
EmailValidationServiceURL: "",
257260
EmailValidationServiceHeaders: "",
261+
EmailValidationBlockedMX: `["hotmail-com.olc.protection.outlook.com"]`,
262+
}
263+
264+
// Ensure the BlockedMX transformation occurs by calling Validate
265+
if err := cfg.Validate(); err != nil {
266+
t.Fatalf("failed to validate MailerConfiguration: %v", err)
258267
}
268+
259269
ev := newEmailValidator(cfg)
260270

261271
for idx, tc := range cases {

0 commit comments

Comments
 (0)