// Copyright 2016 The Gogs Authors. All rights reserved.
// Copyright 2020 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package validation

import (
	"fmt"
	"net/mail"
	"regexp"
	"strings"

	"code.gitea.io/gitea/modules/setting"
	"code.gitea.io/gitea/modules/util"

	"github.com/gobwas/glob"
)

// ErrEmailNotActivated e-mail address has not been activated error
var ErrEmailNotActivated = util.NewInvalidArgumentErrorf("e-mail address has not been activated")

// ErrEmailCharIsNotSupported e-mail address contains unsupported character
type ErrEmailCharIsNotSupported struct {
	Email string
}

// IsErrEmailCharIsNotSupported checks if an error is an ErrEmailCharIsNotSupported
func IsErrEmailCharIsNotSupported(err error) bool {
	_, ok := err.(ErrEmailCharIsNotSupported)
	return ok
}

func (err ErrEmailCharIsNotSupported) Error() string {
	return fmt.Sprintf("e-mail address contains unsupported character [email: %s]", err.Email)
}

// ErrEmailInvalid represents an error where the email address does not comply with RFC 5322
// or has a leading '-' character
type ErrEmailInvalid struct {
	Email string
}

// IsErrEmailInvalid checks if an error is an ErrEmailInvalid
func IsErrEmailInvalid(err error) bool {
	_, ok := err.(ErrEmailInvalid)
	return ok
}

func (err ErrEmailInvalid) Error() string {
	return fmt.Sprintf("e-mail invalid [email: %s]", err.Email)
}

func (err ErrEmailInvalid) Unwrap() error {
	return util.ErrInvalidArgument
}

var emailRegexp = regexp.MustCompile("^[a-zA-Z0-9.!#$%&'*+-/=?^_`{|}~]*@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$")

// check if email is a valid address with allowed domain
func ValidateEmail(email string) error {
	if err := validateEmailBasic(email); err != nil {
		return err
	}
	return validateEmailDomain(email)
}

// check if email is a valid address when admins manually add or edit users
func ValidateEmailForAdmin(email string) error {
	return validateEmailBasic(email)
	// In this case we do not need to check the email domain
}

// validateEmailBasic checks whether the email complies with the rules
func validateEmailBasic(email string) error {
	if len(email) == 0 {
		return ErrEmailInvalid{email}
	}

	if !emailRegexp.MatchString(email) {
		return ErrEmailCharIsNotSupported{email}
	}

	if email[0] == '-' {
		return ErrEmailInvalid{email}
	}

	if _, err := mail.ParseAddress(email); err != nil {
		return ErrEmailInvalid{email}
	}

	return nil
}

func validateEmailDomain(email string) error {
	if !IsEmailDomainAllowed(email) {
		return ErrEmailInvalid{email}
	}

	return nil
}

func IsEmailDomainAllowed(email string) bool {
	if len(setting.Service.EmailDomainAllowList) == 0 {
		return !isEmailDomainListed(setting.Service.EmailDomainBlockList, email)
	}

	return isEmailDomainListed(setting.Service.EmailDomainAllowList, email)
}

// isEmailDomainListed checks whether the domain of an email address
// matches a list of domains
func isEmailDomainListed(globs []glob.Glob, email string) bool {
	if len(globs) == 0 {
		return false
	}

	n := strings.LastIndex(email, "@")
	if n <= 0 {
		return false
	}

	domain := strings.ToLower(email[n+1:])

	for _, g := range globs {
		if g.Match(domain) {
			return true
		}
	}

	return false
}
