/*
Maddy Mail Server - Composable all-in-one email server.
Copyright © 2019-2020 Max Mazurov <fox.cpp@disroot.org>, Maddy Mail Server contributors

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.
*/

package remote

import (
	"context"
	"crypto/tls"
	"errors"
	"os"
	"runtime/debug"
	"time"

	"github.com/foxcpp/go-mtasts"
	"github.com/foxcpp/maddy/framework/config"
	"github.com/foxcpp/maddy/framework/container"
	"github.com/foxcpp/maddy/framework/dns"
	"github.com/foxcpp/maddy/framework/exterrors"
	"github.com/foxcpp/maddy/framework/future"
	"github.com/foxcpp/maddy/framework/log"
	"github.com/foxcpp/maddy/framework/module"
	"github.com/foxcpp/maddy/framework/module/modules"
	"github.com/foxcpp/maddy/internal/target"
)

type (
	mtastsPolicy struct {
		cache       *mtasts.Cache
		mtastsGet   func(context.Context, string) (*mtasts.Policy, error)
		updaterStop chan struct{}
		log         *log.Logger
		instName    string
	}
	mtastsDelivery struct {
		c         *mtastsPolicy
		domain    string
		policyFut *future.Future
		log       *log.Logger
	}
)

func NewMTASTSPolicy(c *container.C, modName, instName string) (module.Module, error) {
	return &mtastsPolicy{
		instName: instName,
		log:      c.DefaultLogger.Sublogger(modName),
	}, nil
}

func (c *mtastsPolicy) Name() string {
	return c.log.Name
}

func (c *mtastsPolicy) InstanceName() string {
	return c.instName
}

func (c *mtastsPolicy) Weight() int {
	return 10
}

func (c *mtastsPolicy) Configure(inlineArgs []string, cfg *config.Map) error {
	var (
		storeType string
		storeDir  string
	)
	cfg.Enum("cache", false, false, []string{"ram", "fs"}, "fs", &storeType)
	cfg.String("fs_dir", false, false, "mtasts_cache", &storeDir)
	if _, err := cfg.Process(); err != nil {
		return err
	}

	switch storeType {
	case "fs":
		if err := os.MkdirAll(storeDir, os.ModePerm); err != nil {
			return err
		}
		c.cache = mtasts.NewFSCache(storeDir)
	case "ram":
		c.cache = mtasts.NewRAMCache()
	default:
		panic("mtasts policy init: unknown cache type")
	}
	c.cache.Resolver = dns.DefaultResolver()
	c.mtastsGet = c.cache.Get

	return nil
}

func (c *mtastsPolicy) Start() error {
	c.StartUpdater()
	return nil
}

// StartUpdater starts a goroutine to update MTA-STS cache periodically until
// Close is called.
//
// It can be called only once per mtastsPolicy instance.
func (c *mtastsPolicy) StartUpdater() {
	c.updaterStop = make(chan struct{})
	go c.updater()
}

func (c *mtastsPolicy) Stop() error {
	if c.updaterStop != nil {
		c.updaterStop <- struct{}{}
		<-c.updaterStop
		c.updaterStop = nil
	}
	return nil
}

func (c *mtastsPolicy) updater() {
	defer func() {
		if err := recover(); err != nil {
			stack := debug.Stack()
			log.Printf("panic during MTA-STS update: %v\n%s", err, stack)
			log.Printf("MTA-STS cache refresh disabled due to critical error")
			c.updaterStop = nil
		}
	}()

	// Always update cache on start-up since we may have been down for some
	// time.
	c.log.Debugln("updating MTA-STS cache...")
	if err := c.cache.Refresh(); err != nil {
		c.log.Error("MTA-STS cache update error", err)
	}
	c.log.Debugln("updating MTA-STS cache... done!")

	t := time.NewTicker(12 * time.Hour)
	for {
		select {
		case <-t.C:
			c.log.Debugln("updating MTA-STS cache...")
			if err := c.cache.Refresh(); err != nil {
				c.log.Error("MTA-STS cache opdate error", err)
			}
			c.log.Debugln("updating MTA-STS cache... done!")
		case <-c.updaterStop:
			c.updaterStop <- struct{}{}
			return
		}
	}
}

func (c *mtastsPolicy) StartDelivery(msgMeta *module.MsgMetadata) module.DeliveryMXAuthPolicy {
	return &mtastsDelivery{
		c:   c,
		log: target.DeliveryLogger(c.log, msgMeta),
	}
}

func (c *mtastsDelivery) PrepareDomain(ctx context.Context, domain string) {
	c.policyFut = future.New()
	go func() {
		c.policyFut.Set(c.c.mtastsGet(ctx, domain))
	}()
}

func (c *mtastsDelivery) PrepareConn(ctx context.Context, mx string) {}

func (c *mtastsDelivery) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) {
	policyI, err := c.policyFut.GetContext(ctx)
	if err != nil {
		c.log.DebugMsg("MTA-STS error", "err", err)
		return module.MXNone, nil
	}
	policy := policyI.(*mtasts.Policy)

	if !policy.Match(mx) {
		if policy.Mode == mtasts.ModeEnforce {
			return module.MXNone, &exterrors.SMTPError{
				Code:         550,
				EnhancedCode: exterrors.EnhancedCode{5, 7, 0},
				Message:      "Failed to establish the MX record authenticity (MTA-STS)",
			}
		}
		c.log.Msg("MX does not match published non-enforced MTA-STS policy", "mx", mx, "domain", c.domain)
		return module.MXNone, nil
	}
	return module.MX_MTASTS, nil
}

func (c *mtastsDelivery) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) {
	policyI, err := c.policyFut.GetContext(ctx)
	if err != nil {
		c.c.log.DebugMsg("MTA-STS error", "err", err)
		return module.TLSNone, nil
	}
	policy := policyI.(*mtasts.Policy)

	if policy.Mode != mtasts.ModeEnforce {
		return module.TLSNone, nil
	}

	if !tlsState.HandshakeComplete {
		return module.TLSNone, &exterrors.SMTPError{
			Code:         451,
			EnhancedCode: exterrors.EnhancedCode{4, 7, 1},
			Message:      "TLS is required but unavailable or failed (MTA-STS)",
		}
	}

	if tlsState.VerifiedChains == nil {
		return module.TLSNone, &exterrors.SMTPError{
			Code:         451,
			EnhancedCode: exterrors.EnhancedCode{4, 7, 1},
			Message: "Recipient server TLS certificate is not trusted but " +
				"authentication is required by MTA-STS",
			Misc: map[string]interface{}{
				"tls_level": tlsLevel,
			},
		}
	}

	return module.TLSNone, nil
}

func (c *mtastsDelivery) Reset(msgMeta *module.MsgMetadata) {
	c.policyFut = nil
	if msgMeta != nil {
		c.log = target.DeliveryLogger(c.c.log, msgMeta)
	}
}

// Stub that will be removed in 0.5.
type stsPreloadPolicy struct {
	log      *log.Logger
	instName string
}

func NewSTSPreload(c *container.C, modName, instName string) (module.Module, error) {
	return &stsPreloadPolicy{
		instName: instName,
		log:      c.DefaultLogger.Sublogger(modName),
	}, nil
}

func (c *stsPreloadPolicy) Name() string {
	return c.log.Name
}

func (c *stsPreloadPolicy) InstanceName() string {
	return c.instName
}

func (c *stsPreloadPolicy) Weight() int {
	return 30 // after MTA-STS
}

func (c *stsPreloadPolicy) Configure(inlineArgs []string, cfg *config.Map) error {
	c.log.Println("sts_preload module is deprecated and is no-op as the list is expired and unmaintained")

	var (
		sourcePath     string
		enforceTesting bool
	)
	cfg.String("source", false, false, "eff", &sourcePath)
	cfg.Bool("enforce_testing", false, true, &enforceTesting)
	if _, err := cfg.Process(); err != nil {
		return err
	}

	return nil
}

type preloadDelivery struct {
	*stsPreloadPolicy
}

func (p *stsPreloadPolicy) StartDelivery(*module.MsgMetadata) module.DeliveryMXAuthPolicy {
	return &preloadDelivery{stsPreloadPolicy: p}
}

func (p *preloadDelivery) Reset(*module.MsgMetadata)                        {}
func (p *preloadDelivery) PrepareDomain(ctx context.Context, domain string) {}
func (p *preloadDelivery) PrepareConn(ctx context.Context, mx string)       {}
func (p *preloadDelivery) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) {
	return mxLevel, nil
}

func (p *preloadDelivery) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) {
	return tlsLevel, nil
}

type dnssecPolicy struct {
	instName string
}

func NewDNSSECPolicy(_ *container.C, _, instName string) (module.Module, error) {
	return &dnssecPolicy{
		instName: instName,
	}, nil
}

func (dnssecPolicy) Name() string {
	return "mx_auth.dnssec"
}

func (c dnssecPolicy) InstanceName() string {
	return c.instName
}

func (dnssecPolicy) Weight() int {
	return 1
}

func (dnssecPolicy) Configure(inlineArgs []string, cfg *config.Map) error {
	_, err := cfg.Process() // will fail if there is any directive
	return err
}

func (dnssecPolicy) StartDelivery(*module.MsgMetadata) module.DeliveryMXAuthPolicy {
	return dnssecPolicy{}
}

func (dnssecPolicy) Reset(*module.MsgMetadata)                        {}
func (dnssecPolicy) PrepareDomain(ctx context.Context, domain string) {}
func (dnssecPolicy) PrepareConn(ctx context.Context, mx string)       {}

func (dnssecPolicy) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) {
	if dnssec {
		return module.MX_DNSSEC, nil
	}
	return module.MXNone, nil
}

func (dnssecPolicy) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) {
	return module.TLSNone, nil
}

type (
	danePolicy struct {
		extResolver *dns.ExtResolver
		log         *log.Logger
		instName    string
	}
	daneDelivery struct {
		c       *danePolicy
		tlsaFut *future.Future
	}
)

func NewDANEPolicy(c *container.C, modName, instName string) (module.Module, error) {
	return &danePolicy{
		instName: instName,
		log:      c.DefaultLogger.Sublogger(modName),
	}, nil
}

func (c *danePolicy) Name() string {
	return "mx_auth.dane"
}

func (c *danePolicy) InstanceName() string {
	return c.instName
}

func (c *danePolicy) Weight() int {
	return 10
}

func (c *danePolicy) Configure(inlineArgs []string, cfg *config.Map) error {
	var err error
	c.extResolver, err = dns.NewExtResolver()
	if err != nil {
		c.log.Error("DANE support is no-op: unable to init EDNS resolver", err)
	}

	cfg.Bool("debug", true, log.DefaultLogger.Debug, &c.log.Debug)

	_, err = cfg.Process()
	return err
}

func (c *danePolicy) StartDelivery(*module.MsgMetadata) module.DeliveryMXAuthPolicy {
	return &daneDelivery{c: c}
}

func (c *daneDelivery) PrepareDomain(ctx context.Context, domain string) {}

func (c *daneDelivery) discoverTLSA(ctx context.Context, mx string) ([]dns.TLSA, error) {
	adA, rname, err := c.c.extResolver.CheckCNAMEAD(ctx, mx)
	if err != nil {
		// This may indicate a bogus DNSSEC signature or other lookup issue
		// (including non-existing domain).
		// Per RFC 7672, any I/O errors (including SERVFAIL) should
		// cause delivery to be delayed.
		return nil, err
	}
	if rname == "" {
		// No A/AAAA records, short-circuit discovery instead of doing useless
		// queries.
		return nil, errors.New("no address associated with the host")
	}
	if !adA {
		// If A lookup is not DNSSEC-authenticated we assume the server cannot
		// have TLSA record and skip trying to actually lookup TLSA
		// to avoid hitting weird errors like SERVFAIL, NOTIMP
		// e.g. see https://github.com/foxcpp/maddy/issues/287
		if rname == mx {
			c.c.log.Debugln("skipping DANE for", mx, "due to non-authenticated A records")
			return nil, nil
		}

		// But if it is CNAME'd then we may not want to skip it and actually
		// consider initial name since it may be signed. To confirm the
		// initial name is signed, do CNAME lookup.
		cnameAD, _, err := c.c.extResolver.AuthLookupCNAME(ctx, mx)
		if err != nil {
			return nil, err
		}
		if !cnameAD {
			c.c.log.Debugln("skipping DANE for", mx, "due to non-authenticated CNAME record")
			return nil, nil
		}
	}

	// If there was a CNAME - try it first.
	if rname != mx {
		ad, recs, err := c.c.extResolver.AuthLookupTLSA(ctx, "25", "tcp", rname)
		if err != nil && !dns.IsNotFound(err) {
			return nil, err
		}
		if ad && len(recs) != 0 {
			// recs may be empty or contain only unusable records - this is
			// okay per RFC 7672, no fallback to initial name is done.
			c.c.log.Debugln("using", len(recs), "DANE records at", rname, "to authenticate", mx)
			return recs, nil
		}
		// Per RFC 7672 Section 2.2 we interpret a non-authenticated RRset just
		// like an empty RRset and fallback to trying original name.
		c.c.log.Debugln("ignoring non-authenticated TLSA records for", rname)
	}

	// If initial name is not a CNAME or final canonical name is not "secure"
	// - we consider TLSA under the initial name.
	ad, recs, err := c.c.extResolver.AuthLookupTLSA(ctx, "25", "tcp", mx)
	if err != nil && !dns.IsNotFound(err) {
		return nil, err
	}
	if !ad {
		c.c.log.Debugln("ignoring non-authenticated TLSA records for", mx)
		return nil, nil
	}

	c.c.log.Debugln("using", len(recs), "DANE records at original name to authenticate", mx)
	return recs, nil
}

func (c *daneDelivery) PrepareConn(ctx context.Context, mx string) {
	// No DNSSEC support.
	if c.c.extResolver == nil {
		return
	}

	c.tlsaFut = future.New()

	go func() {
		defer func() {
			if err := recover(); err != nil {
				stack := debug.Stack()
				log.Printf("panic during extended resolver lookup: %v\n%s", err, stack)
			}
		}()

		c.tlsaFut.Set(c.discoverTLSA(ctx, dns.FQDN(mx)))
	}()
}

func (c *daneDelivery) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) {
	return module.MXNone, nil
}

func (c *daneDelivery) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) {
	// No DNSSEC support.
	if c.c.extResolver == nil {
		return module.TLSNone, nil
	}

	recsI, err := c.tlsaFut.GetContext(ctx)
	if err != nil {
		// No records.
		if dns.IsNotFound(err) {
			return module.TLSNone, nil
		}

		// Lookup error here indicates a resolution failure or may also
		// indicate a bogus DNSSEC signature.
		// There is a big problem with differentiating these two.
		//
		// We assume DANE failure in both cases as a safety measure.
		// However, there is a possibility of a temporary error condition,
		// so we mark it as such.
		return module.TLSNone, exterrors.WithTemporary(err, true)
	}
	recs := recsI.([]dns.TLSA)

	overridePKIX, err := verifyDANE(recs, tlsState)
	if err != nil {
		return module.TLSNone, err
	}
	if overridePKIX {
		return module.TLSAuthenticated, nil
	}
	return module.TLSNone, nil
}

func (c *daneDelivery) Reset(*module.MsgMetadata) {}

type (
	localPolicy struct {
		instName    string
		minTLSLevel module.TLSLevel
		minMXLevel  module.MXLevel
	}
)

func NewLocalPolicy(_ *container.C, _, instName string) (module.Module, error) {
	return &localPolicy{
		instName: instName,
	}, nil
}

func (c *localPolicy) Name() string {
	return "mx_auth.local_policy"
}

func (c *localPolicy) InstanceName() string {
	return c.instName
}

func (c *localPolicy) Weight() int {
	return 1000
}

func (c *localPolicy) Configure(inlineArgs []string, cfg *config.Map) error {
	var (
		minTLSLevel string
		minMXLevel  string
	)

	cfg.Enum("min_tls_level", false, false,
		[]string{"none", "encrypted", "authenticated"}, "encrypted", &minTLSLevel)
	cfg.Enum("min_mx_level", false, false,
		[]string{"none", "mtasts", "dnssec"}, "none", &minMXLevel)
	if _, err := cfg.Process(); err != nil {
		return err
	}

	// Enum checks the value against allowed list, no 'default' necessary.
	switch minTLSLevel {
	case "none":
		c.minTLSLevel = module.TLSNone
	case "encrypted":
		c.minTLSLevel = module.TLSEncrypted
	case "authenticated":
		c.minTLSLevel = module.TLSAuthenticated
	}
	switch minMXLevel {
	case "none":
		c.minMXLevel = module.MXNone
	case "mtasts":
		c.minMXLevel = module.MX_MTASTS
	case "dnssec":
		c.minMXLevel = module.MX_DNSSEC
	}

	return nil
}

func (l *localPolicy) StartDelivery(msgMeta *module.MsgMetadata) module.DeliveryMXAuthPolicy {
	return l
}

func (l *localPolicy) Reset(*module.MsgMetadata)                        {}
func (l *localPolicy) PrepareDomain(ctx context.Context, domain string) {}
func (l *localPolicy) PrepareConn(ctx context.Context, mx string)       {}

func (l *localPolicy) CheckMX(ctx context.Context, mxLevel module.MXLevel, domain, mx string, dnssec bool) (module.MXLevel, error) {
	if mxLevel < l.minMXLevel {
		return module.MXNone, &exterrors.SMTPError{
			// Err on the side of caution if policy evaluation was messed up by
			// a temporary error (we can't know with the current design).
			Code:         451,
			EnhancedCode: exterrors.EnhancedCode{4, 7, 0},
			Message:      "Failed to establish the MX record authenticity",
			Misc: map[string]interface{}{
				"mx_level":          mxLevel,
				"required_mx_level": l.minMXLevel,
			},
		}
	}
	return module.MXNone, nil
}

func (l *localPolicy) CheckConn(ctx context.Context, mxLevel module.MXLevel, tlsLevel module.TLSLevel, domain, mx string, tlsState tls.ConnectionState) (module.TLSLevel, error) {
	if tlsLevel < l.minTLSLevel {
		return module.TLSNone, &exterrors.SMTPError{
			Code:         451,
			EnhancedCode: exterrors.EnhancedCode{4, 7, 1},
			Message:      "TLS it not available or unauthenticated but required",
			Misc: map[string]interface{}{
				"tls_level":          tlsLevel,
				"required_tls_level": l.minTLSLevel,
			},
		}
	}
	return module.TLSNone, nil
}

func init() {
	modules.Register("mx_auth.mtasts", NewMTASTSPolicy)
	modules.Register("mx_auth.sts_preload", NewSTSPreload)
	modules.Register("mx_auth.dnssec", NewDNSSECPolicy)
	modules.Register("mx_auth.dane", NewDANEPolicy)
	modules.Register("mx_auth.local_policy", NewLocalPolicy)
}
