package main

import (
	"bytes"
	"crypto"
	"crypto/x509"
	"encoding/pem"
	"flag"
	"fmt"
	"io"
	"io/ioutil"
	"log"
	"net"
	"net/mail"
	"net/textproto"
	"os"
	"os/signal"
	"path"
	"strings"
	"syscall"

	"github.com/emersion/go-milter"
	"github.com/emersion/go-msgauth/authres"
	"github.com/emersion/go-msgauth/dkim"
	"golang.org/x/crypto/ed25519"
)

var (
	signDomains    stringSliceFlag
	identity       string
	listenURI      string
	privateKeyPath string
	selector       string
	verbose        bool
)

var privateKey crypto.Signer

var signHeaderKeys = []string{
	"From",
	"Reply-To",
	"Subject",
	"Date",
	"To",
	"Cc",
	"Resent-Date",
	"Resent-From",
	"Resent-To",
	"Resent-Cc",
	"In-Reply-To",
	"References",
	"List-Id",
	"List-Help",
	"List-Unsubscribe",
	"List-Subscribe",
	"List-Post",
	"List-Owner",
	"List-Archive",
}

const maxVerifications = 5

func init() {
	flag.Var(&signDomains, "d", "Domain(s) whose mail should be signed (matched using path.Match)")
	flag.StringVar(&identity, "i", "", "Server identity (defaults to hostname)")
	flag.StringVar(&listenURI, "l", "unix:///tmp/dkim-milter.sock", "Listen URI")
	flag.StringVar(&privateKeyPath, "k", "", "Private key (PEM-formatted)")
	flag.StringVar(&selector, "s", "", "Selector")
	flag.BoolVar(&verbose, "v", false, "Enable verbose logging")
}

type stringSliceFlag []string

func (f *stringSliceFlag) String() string {
	return strings.Join(*f, ", ")
}

func (f *stringSliceFlag) Set(value string) error {
	*f = append(*f, value)
	return nil
}

type session struct {
	milter.NoOpMilter

	authResDelete []int
	headerBuf     bytes.Buffer

	signDomain     string
	signHeaderKeys []string

	done   <-chan error
	pw     *io.PipeWriter
	verifs []*dkim.Verification // only valid after done is closed
	signer *dkim.Signer
	mw     io.Writer
}

func parseAddressDomain(s string) (string, error) {
	addr, err := mail.ParseAddress(s)
	if err != nil {
		return "", err
	}

	parts := strings.SplitN(addr.Address, "@", 2)
	if len(parts) != 2 {
		return "", fmt.Errorf("dkim-milter: malformed address: missing '@'")
	}

	return parts[1], nil
}

func (s *session) Header(name string, value string, m *milter.Modifier) (milter.Response, error) {
	if strings.EqualFold(name, "From") || strings.EqualFold(name, "Sender") {
		domain, err := parseAddressDomain(value)
		if err != nil {
			return nil, fmt.Errorf("dkim-milter: failed to parse header field %q: %v", name, err)
		}
		domain = strings.ToLower(domain)

		for _, pattern := range signDomains {
			if ok, err := path.Match(pattern, domain); err != nil {
				return nil, fmt.Errorf("dkim-milter: failed to match domain %q: %v", domain, err)
			} else if ok {
				s.signDomain = domain
				break
			}
		}
	}

	for _, k := range signHeaderKeys {
		if strings.EqualFold(name, k) {
			s.signHeaderKeys = append(s.signHeaderKeys, name)
		}
	}

	field := name + ": " + value + "\r\n"
	_, err := s.headerBuf.WriteString(field)
	return milter.RespContinue, err
}

func getIdentity(authRes string) string {
	parts := strings.SplitN(authRes, ";", 2)
	return strings.TrimSpace(parts[0])
}

func shouldDeleteAuthRes(field string) bool {
	id, results, err := authres.Parse(field)
	if err != nil {
		// Delete fields we can't parse, because other implementations might
		// accept malformed fields
		return true
	}

	if !strings.EqualFold(id, identity) {
		// Not our Authentication-Results, ignore the field
		return false
	}

	for _, res := range results {
		if _, ok := res.(*authres.DKIMResult); ok {
			// Delete existing DKIM Authentication-Results fields
			return true
		}
	}

	// This is our Authentication-Results field, but it isn't about DKIM. Maybe
	// a previous milter has generated it (e.g. SPF), so keep it.
	return false
}

func (s *session) Headers(h textproto.MIMEHeader, m *milter.Modifier) (milter.Response, error) {
	// Write final CRLF to begin message body
	if _, err := s.headerBuf.WriteString("\r\n"); err != nil {
		return nil, err
	}

	// Delete any existing Authentication-Results header field with our identity
	fields := h["Authentication-Results"]
	for i, field := range fields {
		if shouldDeleteAuthRes(field) {
			s.authResDelete = append(s.authResDelete, i)
		}
	}

	// Sign if necessary
	if s.signDomain != "" {
		opts := dkim.SignOptions{
			Domain:       s.signDomain,
			Selector:     selector,
			Signer:       privateKey,
			HeaderKeys:   s.signHeaderKeys,
			QueryMethods: []dkim.QueryMethod{dkim.QueryMethodDNSTXT},
		}

		var err error
		s.signer, err = dkim.NewSigner(&opts)
		if err != nil {
			return nil, err
		}
	}

	// Verify existing signatures
	done := make(chan error, 1)
	pr, pw := io.Pipe()

	s.done = done
	s.pw = pw

	// TODO: limit max. number of signatures
	go func() {
		options := dkim.VerifyOptions{MaxVerifications: maxVerifications}

		var err error
		s.verifs, err = dkim.VerifyWithOptions(pr, &options)
		io.Copy(ioutil.Discard, pr)
		pr.Close()
		done <- err
		close(done)
	}()

	// Process header
	return s.BodyChunk(s.headerBuf.Bytes(), m)
}

func (s *session) BodyChunk(chunk []byte, m *milter.Modifier) (milter.Response, error) {
	if _, err := s.pw.Write(chunk); err != nil {
		return nil, err
	}
	if s.signer != nil {
		if _, err := s.signer.Write(chunk); err != nil {
			return nil, err
		}
	}
	return milter.RespContinue, nil
}

func (s *session) Body(m *milter.Modifier) (milter.Response, error) {
	if err := s.pw.Close(); err != nil {
		return nil, err
	}

	for _, index := range s.authResDelete {
		if err := m.ChangeHeader(index, "Authentication-Results", ""); err != nil {
			return nil, err
		}
	}

	if err := <-s.done; err == dkim.ErrTooManySignatures {
		if verbose {
			log.Printf("Too many signatures in message: %v", err)
		}
		// Ignore the error
	} else if err != nil {
		if verbose {
			log.Printf("DKIM verification failed: %v", err)
		}
		return nil, err
	}

	if s.signer != nil {
		if err := s.signer.Close(); err != nil {
			if verbose {
				log.Printf("DKIM signature failed: %v", err)
			}
			return nil, err
		}

		kv := s.signer.Signature()
		parts := strings.SplitN(kv, ": ", 2)
		if len(parts) != 2 {
			panic("dkim-milter: malformed DKIM-Signature header field")
		}
		k, v := parts[0], strings.TrimSuffix(parts[1], "\r\n")

		if err := m.InsertHeader(0, k, v); err != nil {
			return nil, err
		}
	}

	results := make([]authres.Result, 0, len(s.verifs))

	if len(s.verifs) == 0 && s.signer == nil {
		results = append(results, &authres.DKIMResult{
			Value: authres.ResultNone,
		})
	}

	for _, verif := range s.verifs {
		if verbose {
			if verif.Err != nil {
				log.Printf("DKIM verification failed for %v: %v", verif.Domain, verif.Err)
			} else {
				log.Printf("DKIM verification succeded for %v", verif.Domain)
			}
		}

		var val authres.ResultValue
		if verif.Err == nil {
			val = authres.ResultPass
		} else if dkim.IsPermFail(verif.Err) {
			val = authres.ResultPermError
		} else if dkim.IsTempFail(verif.Err) {
			val = authres.ResultTempError
		} else {
			val = authres.ResultFail
		}

		results = append(results, &authres.DKIMResult{
			Value:      val,
			Domain:     verif.Domain,
			Identifier: verif.Identifier,
		})
	}

	if len(s.verifs) > 0 || s.signer == nil {
		v := authres.Format(identity, results)
		if err := m.InsertHeader(0, "Authentication-Results", v); err != nil {
			return nil, err
		}
	}

	return milter.RespAccept, nil
}

func loadPrivateKey(path string) (crypto.Signer, error) {
	b, err := ioutil.ReadFile(privateKeyPath)
	if err != nil {
		return nil, err
	}

	block, _ := pem.Decode(b)
	if block == nil {
		return nil, fmt.Errorf("no PEM data found")
	}

	switch strings.ToUpper(block.Type) {
	case "PRIVATE KEY":
		k, err := x509.ParsePKCS8PrivateKey(block.Bytes)
		if err != nil {
			return nil, err
		}
		return k.(crypto.Signer), nil
	case "RSA PRIVATE KEY":
		return x509.ParsePKCS1PrivateKey(block.Bytes)
	case "EDDSA PRIVATE KEY":
		if len(block.Bytes) != ed25519.PrivateKeySize {
			return nil, fmt.Errorf("invalid Ed25519 private key size")
		}
		return ed25519.PrivateKey(block.Bytes), nil
	default:
		return nil, fmt.Errorf("unknown private key type: '%v'", block.Type)
	}
}

func main() {
	flag.Parse()

	if identity == "" {
		var err error
		identity, err = os.Hostname()
		if err != nil {
			log.Fatal("Failed to read hostname: ", err)
		}
	}

	if (len(signDomains) > 0 || privateKeyPath != "" || selector != "") && !(len(signDomains) > 0 && privateKeyPath != "" && selector != "") {
		log.Fatal("Domain(s) (-d) and private key (-k) must be both specified")
	}

	for i, pattern := range signDomains {
		if _, err := path.Match(pattern, ""); err != nil {
			log.Fatalf("Malformed domain pattern %q: %v", pattern, err)
		}
		signDomains[i] = strings.ToLower(pattern)
	}

	if privateKeyPath != "" {
		var err error
		privateKey, err = loadPrivateKey(privateKeyPath)
		if err != nil {
			log.Fatalf("Failed to load private key from '%v': %v", privateKeyPath, err)
		}
	}

	parts := strings.SplitN(listenURI, "://", 2)
	if len(parts) != 2 {
		log.Fatal("Invalid listen URI")
	}
	listenNetwork, listenAddr := parts[0], parts[1]

	s := milter.Server{
		NewMilter: func() milter.Milter {
			return &session{}
		},
		Actions:  milter.OptAddHeader | milter.OptChangeHeader,
		Protocol: milter.OptNoConnect | milter.OptNoHelo | milter.OptNoMailFrom | milter.OptNoRcptTo,
	}

	ln, err := net.Listen(listenNetwork, listenAddr)
	if err != nil {
		log.Fatal("Failed to setup listener: ", err)
	}

	// Closing the listener will unlink the unix socket, if any
	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		<-sigs
		if err := s.Close(); err != nil {
			log.Fatal("Failed to close server: ", err)
		}
	}()

	log.Println("Milter listening at", listenURI)
	if err := s.Serve(ln); err != nil && err != milter.ErrServerClosed {
		log.Fatal("Failed to serve: ", err)
	}
}
