// Copyright 2019 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT

package setting

import (
	"context"
	"net"
	"net/mail"
	"strings"
	"text/template"
	"time"

	"forgejo.org/modules/log"

	shellquote "github.com/kballard/go-shellquote"
)

// Mailer represents mail service.
type Mailer struct {
	// Mailer
	Name                 string              `ini:"NAME"`
	From                 string              `ini:"FROM"`
	EnvelopeFrom         string              `ini:"ENVELOPE_FROM"`
	OverrideEnvelopeFrom bool                `ini:"-"`
	FromName             string              `ini:"-"`
	FromEmail            string              `ini:"-"`
	SendAsPlainText      bool                `ini:"SEND_AS_PLAIN_TEXT"`
	SubjectPrefix        string              `ini:"SUBJECT_PREFIX"`
	OverrideHeader       map[string][]string `ini:"-"`

	// SMTP sender
	Protocol             string `ini:"PROTOCOL"`
	SMTPAddr             string `ini:"SMTP_ADDR"`
	SMTPPort             string `ini:"SMTP_PORT"`
	User                 string `ini:"USER"`
	Passwd               string `ini:"PASSWD"`
	EnableHelo           bool   `ini:"ENABLE_HELO"`
	HeloHostname         string `ini:"HELO_HOSTNAME"`
	ForceTrustServerCert bool   `ini:"FORCE_TRUST_SERVER_CERT"`
	UseClientCert        bool   `ini:"USE_CLIENT_CERT"`
	ClientCertFile       string `ini:"CLIENT_CERT_FILE"`
	ClientKeyFile        string `ini:"CLIENT_KEY_FILE"`

	// Sendmail sender
	SendmailPath        string        `ini:"SENDMAIL_PATH"`
	SendmailArgs        []string      `ini:"-"`
	SendmailTimeout     time.Duration `ini:"SENDMAIL_TIMEOUT"`
	SendmailConvertCRLF bool          `ini:"SENDMAIL_CONVERT_CRLF"`

	// Customization
	FromDisplayNameFormat         string             `ini:"FROM_DISPLAY_NAME_FORMAT"`
	FromDisplayNameFormatTemplate *template.Template `ini:"-"`
}

// MailService the global mailer
var MailService *Mailer

func loadMailsFrom(rootCfg ConfigProvider) {
	loadMailerFrom(rootCfg)
	loadRegisterMailFrom(rootCfg)
	loadNotifyMailFrom(rootCfg)
	loadIncomingEmailFrom(rootCfg)
}

func loadMailerFrom(rootCfg ConfigProvider) {
	sec := rootCfg.Section("mailer")
	// Check mailer setting.
	if !sec.Key("ENABLED").MustBool() {
		return
	}

	// Handle Deprecations and map on to new configuration
	// DEPRECATED should not be removed because users maybe upgrade from lower version to the latest version
	// if these are removed, the warning will not be shown
	deprecatedSetting(rootCfg, "mailer", "MAILER_TYPE", "mailer", "PROTOCOL", "v1.19.0")
	if sec.HasKey("MAILER_TYPE") && !sec.HasKey("PROTOCOL") {
		if sec.Key("MAILER_TYPE").String() == "sendmail" {
			sec.Key("PROTOCOL").MustString("sendmail")
		}
	}

	deprecatedSetting(rootCfg, "mailer", "HOST", "mailer", "SMTP_ADDR", "v1.19.0")
	if sec.HasKey("HOST") && !sec.HasKey("SMTP_ADDR") {
		givenHost := sec.Key("HOST").String()
		addr, port, err := net.SplitHostPort(givenHost)
		if err != nil && strings.Contains(err.Error(), "missing port in address") {
			addr = givenHost
		} else if err != nil {
			log.Fatal("Invalid mailer.HOST (%s): %v", givenHost, err)
		}
		if addr == "" {
			addr = "127.0.0.1"
		}
		sec.Key("SMTP_ADDR").MustString(addr)
		sec.Key("SMTP_PORT").MustString(port)
	}

	deprecatedSetting(rootCfg, "mailer", "IS_TLS_ENABLED", "mailer", "PROTOCOL", "v1.19.0")
	if sec.HasKey("IS_TLS_ENABLED") && !sec.HasKey("PROTOCOL") {
		if sec.Key("IS_TLS_ENABLED").MustBool() {
			sec.Key("PROTOCOL").MustString("smtps")
		} else {
			sec.Key("PROTOCOL").MustString("smtp+starttls")
		}
	}

	deprecatedSetting(rootCfg, "mailer", "DISABLE_HELO", "mailer", "ENABLE_HELO", "v1.19.0")
	if sec.HasKey("DISABLE_HELO") && !sec.HasKey("ENABLE_HELO") {
		sec.Key("ENABLE_HELO").MustBool(!sec.Key("DISABLE_HELO").MustBool())
	}

	deprecatedSetting(rootCfg, "mailer", "SKIP_VERIFY", "mailer", "FORCE_TRUST_SERVER_CERT", "v1.19.0")
	if sec.HasKey("SKIP_VERIFY") && !sec.HasKey("FORCE_TRUST_SERVER_CERT") {
		sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(sec.Key("SKIP_VERIFY").MustBool())
	}

	deprecatedSetting(rootCfg, "mailer", "USE_CERTIFICATE", "mailer", "USE_CLIENT_CERT", "v1.19.0")
	if sec.HasKey("USE_CERTIFICATE") && !sec.HasKey("USE_CLIENT_CERT") {
		sec.Key("USE_CLIENT_CERT").MustBool(sec.Key("USE_CERTIFICATE").MustBool())
	}

	deprecatedSetting(rootCfg, "mailer", "CERT_FILE", "mailer", "CLIENT_CERT_FILE", "v1.19.0")
	if sec.HasKey("CERT_FILE") && !sec.HasKey("CLIENT_CERT_FILE") {
		sec.Key("CERT_FILE").MustString(sec.Key("CERT_FILE").String())
	}

	deprecatedSetting(rootCfg, "mailer", "KEY_FILE", "mailer", "CLIENT_KEY_FILE", "v1.19.0")
	if sec.HasKey("KEY_FILE") && !sec.HasKey("CLIENT_KEY_FILE") {
		sec.Key("KEY_FILE").MustString(sec.Key("KEY_FILE").String())
	}

	deprecatedSetting(rootCfg, "mailer", "ENABLE_HTML_ALTERNATIVE", "mailer", "SEND_AS_PLAIN_TEXT", "v1.19.0")
	if sec.HasKey("ENABLE_HTML_ALTERNATIVE") && !sec.HasKey("SEND_AS_PLAIN_TEXT") {
		sec.Key("SEND_AS_PLAIN_TEXT").MustBool(!sec.Key("ENABLE_HTML_ALTERNATIVE").MustBool(false))
	}

	if sec.HasKey("PROTOCOL") && sec.Key("PROTOCOL").String() == "smtp+startls" {
		log.Error("Deprecated fallback `[mailer]` `PROTOCOL = smtp+startls` present. Use `[mailer]` `PROTOCOL = smtp+starttls`` instead. This fallback will be removed in v1.19.0")
		sec.Key("PROTOCOL").SetValue("smtp+starttls")
	}

	// Handle aliases
	if sec.HasKey("USERNAME") && !sec.HasKey("USER") {
		sec.Key("USER").SetValue(sec.Key("USERNAME").String())
	}
	if sec.HasKey("PASSWORD") && !sec.HasKey("PASSWD") {
		sec.Key("PASSWD").SetValue(sec.Key("PASSWORD").String())
	}
	if sec.HasKey("PASSWORD_URI") && !sec.HasKey("PASSWD_URI") {
		sec.Key("PASSWD_URI").SetValue(sec.Key("PASSWORD_URI").String())
	}
	sec.Key("PASSWD").SetValue(loadSecret(sec, "PASSWD_URI", "PASSWD"))

	// Set default values & validate
	sec.Key("NAME").MustString(AppName)
	sec.Key("PROTOCOL").In("", []string{"smtp", "smtps", "smtp+starttls", "smtp+unix", "sendmail", "dummy"})
	sec.Key("ENABLE_HELO").MustBool(true)
	sec.Key("FORCE_TRUST_SERVER_CERT").MustBool(false)
	sec.Key("USE_CLIENT_CERT").MustBool(false)
	sec.Key("SENDMAIL_PATH").MustString("sendmail")
	sec.Key("SENDMAIL_TIMEOUT").MustDuration(5 * time.Minute)
	sec.Key("SENDMAIL_CONVERT_CRLF").MustBool(true)
	sec.Key("FROM").MustString(sec.Key("USER").String())

	// Now map the values on to the MailService
	MailService = &Mailer{}
	if err := sec.MapTo(MailService); err != nil {
		log.Fatal("Unable to map [mailer] section on to MailService. Error: %v", err)
	}

	overrideHeader := rootCfg.Section("mailer.override_header").Keys()
	MailService.OverrideHeader = make(map[string][]string)
	for _, key := range overrideHeader {
		MailService.OverrideHeader[key.Name()] = key.Strings(",")
	}

	// Infer SMTPPort if not set
	if MailService.SMTPPort == "" {
		switch MailService.Protocol {
		case "smtp":
			MailService.SMTPPort = "25"
		case "smtps":
			MailService.SMTPPort = "465"
		case "smtp+starttls":
			MailService.SMTPPort = "587"
		}
	}

	// Infer Protocol
	if MailService.Protocol == "" {
		if strings.ContainsAny(MailService.SMTPAddr, "/\\") {
			MailService.Protocol = "smtp+unix"
		} else {
			switch MailService.SMTPPort {
			case "25":
				MailService.Protocol = "smtp"
			case "465":
				MailService.Protocol = "smtps"
			case "587":
				MailService.Protocol = "smtp+starttls"
			default:
				log.Error("unable to infer unspecified mailer.PROTOCOL from mailer.SMTP_PORT = %q, assume using smtps", MailService.SMTPPort)
				MailService.Protocol = "smtps"
				if MailService.SMTPPort == "" {
					MailService.SMTPPort = "465"
				}
			}
		}
	}

	// we want to warn if users use SMTP on a non-local IP;
	// we might as well take the opportunity to check that it has an IP at all
	// This check is not needed for sendmail
	switch MailService.Protocol {
	case "sendmail":
		var err error
		MailService.SendmailArgs, err = shellquote.Split(sec.Key("SENDMAIL_ARGS").String())
		if err != nil {
			log.Error("Failed to parse Sendmail args: '%s' with error %v", sec.Key("SENDMAIL_ARGS").String(), err)
		}

		if len(MailService.SendmailArgs) == 0 || MailService.SendmailArgs[len(MailService.SendmailArgs)-1] != "--" {
			log.Warn("SENDMAIL_ARGS setting does not end in \"--\", appending it to prevent argument injection")
			MailService.SendmailArgs = append(MailService.SendmailArgs, "--")
		}
	case "smtp", "smtps", "smtp+starttls", "smtp+unix":
		ips := tryResolveAddr(MailService.SMTPAddr)
		if MailService.Protocol == "smtp" {
			for _, ip := range ips {
				if !ip.IP.IsLoopback() {
					log.Warn("connecting over insecure SMTP protocol to non-local address is not recommended")
					break
				}
			}
		}
	case "dummy": // just mention and do nothing
	}

	if MailService.From != "" {
		parsed, err := mail.ParseAddress(MailService.From)
		if err != nil {
			log.Fatal("Invalid mailer.FROM (%s): %v", MailService.From, err)
		}
		MailService.FromName = parsed.Name
		MailService.FromEmail = parsed.Address
	} else {
		log.Error("no mailer.FROM provided, email system may not work.")
	}

	MailService.FromDisplayNameFormatTemplate, _ = template.New("mailFrom").Parse("{{ .DisplayName }}")
	if MailService.FromDisplayNameFormat != "" {
		template, err := template.New("mailFrom").Parse(MailService.FromDisplayNameFormat)
		if err != nil {
			log.Error("mailer.FROM_DISPLAY_NAME_FORMAT is no valid template: %v", err)
		} else {
			MailService.FromDisplayNameFormatTemplate = template
		}
	}

	switch MailService.EnvelopeFrom {
	case "":
		MailService.OverrideEnvelopeFrom = false
	case "<>":
		MailService.EnvelopeFrom = ""
		MailService.OverrideEnvelopeFrom = true
	default:
		parsed, err := mail.ParseAddress(MailService.EnvelopeFrom)
		if err != nil {
			log.Fatal("Invalid mailer.ENVELOPE_FROM (%s): %v", MailService.EnvelopeFrom, err)
		}
		MailService.OverrideEnvelopeFrom = true
		MailService.EnvelopeFrom = parsed.Address
	}
}

func loadRegisterMailFrom(rootCfg ConfigProvider) {
	if !rootCfg.Section("service").Key("REGISTER_EMAIL_CONFIRM").MustBool() {
		return
	} else if MailService == nil {
		log.Warn("Register Mail Service: Mail Service is not enabled")
		return
	}
	Service.RegisterEmailConfirm = true
}

func loadNotifyMailFrom(rootCfg ConfigProvider) {
	if !rootCfg.Section("service").Key("ENABLE_NOTIFY_MAIL").MustBool() {
		return
	} else if MailService == nil {
		log.Warn("Notify Mail Service: Mail Service is not enabled")
		return
	}
	Service.EnableNotifyMail = true
}

func tryResolveAddr(addr string) []net.IPAddr {
	if strings.HasPrefix(addr, "[") && strings.HasSuffix(addr, "]") {
		addr = addr[1 : len(addr)-1]
	}
	ip := net.ParseIP(addr)
	if ip != nil {
		return []net.IPAddr{{IP: ip}}
	}

	ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer cancel()
	ips, err := net.DefaultResolver.LookupIPAddr(ctx, addr)
	if err != nil {
		log.Warn("could not look up mailer.SMTP_ADDR: %v", err)
		return nil
	}
	return ips
}
