// Copyright Martin Dosch.
// Use of this source code is governed by the BSD-2-clause
// license that can be found in the LICENSE file.

package main

import (
	"bufio"
	"context"
	"crypto/tls"
	"fmt"
	"io"
	"log"
	"log/slog"
	"net"
	"os"
	"os/signal"
	osUser "os/user"
	"runtime"
	"strconv"
	"strings"
	"syscall"
	"time"

	gopenpgpConst "github.com/ProtonMail/gopenpgp/v3/constants" // MIT License
	"github.com/ProtonMail/gopenpgp/v3/crypto"                  // MIT License
	"github.com/pborman/getopt/v2"                              // BSD-3-Clause
	"github.com/xmppo/go-xmpp"                                  // BSD-3-Clause
	"golang.org/x/net/idna"                                     // BSD-3-Clause
	"salsa.debian.org/mdosch/xmppsrv"                           // BSD-2-Clause
)

type configuration struct {
	username   string
	jserver    string
	port       string
	password   string
	alias      string
	allowPLAIN bool
}

func closeAndExit(client *xmpp.Client, err error) {
	slog.Info("closing connection and exiting")
	_ = client.Close()
	if err != nil {
		log.Fatal(err)
	}
	os.Exit(0)
}

func readFileByLine(filePath string) (string, error) {
	var (
		output string
		err    error
	)

	// Check that the file is existing.
	_, err = os.Stat(filePath)
	if err != nil {
		return output, fmt.Errorf("read file by line: %w", err)
	}

	// Open message file.
	slog.Info("opening file", "file", filePath)
	// #nosec G304 -- file path is supplied by the user, it's no untrusted input from the network
	file, err := os.Open(filePath)
	if err != nil {
		return output, fmt.Errorf("read file by line: %w", err)
	}
	defer func() {
		derr := file.Close()
		if derr != nil {
			slog.Warn("closing file failed", "error", derr)
		}
	}()
	scanner := bufio.NewScanner(file)
	scanner.Split(bufio.ScanLines)
	for scanner.Scan() {
		if output == "" {
			output = scanner.Text()
		} else {
			output = output + "\n" + scanner.Text()
		}
	}

	if err = scanner.Err(); err != nil {
		if err != io.EOF {
			return "", fmt.Errorf("read file by line: %w", err)
		}
	}

	return output, nil
}

func dropPrivs() error {
	slog.Info("Trying to drop root privileges before connecting to the server")
	nobody, err := osUser.Lookup("nobody")
	if err != nil {
		slog.Info("failed to look up user nobody, will not drop privileges")
		return fmt.Errorf("failed to drop root privilege: %w", err)
	} else {
		gid, err := strconv.Atoi(nobody.Gid)
		if err != nil {
			return fmt.Errorf("failed to drop root privilege: %w", err)
		}
		uid, err := strconv.Atoi(nobody.Uid)
		if err != nil {
			return fmt.Errorf("failed to drop root privilege: %w", err)
		}
		err = syscall.Setgid(gid)
		if err != nil {
			slog.Info("failed to drop privileges to user nobody", "GID", gid)
			return fmt.Errorf("failed to drop root privilege: %w", err)
		}
		err = syscall.Setuid(uid)
		if err != nil {
			slog.Info("failed to drop privileges to user nobody", "UID", uid)
			return fmt.Errorf("failed to drop root privilege: %w", err)
		}
	}
	slog.Info("user identification", "UID", syscall.Getuid())
	slog.Info("user identification", "GID", syscall.Getgid())
	return nil
}

func main() {
	type recipientsType struct {
		Jid       string
		OxKeyRing *crypto.KeyRing
	}

	var (
		err                                    error
		message, user, server, password, alias string
		allowPLAIN, isRoot                     bool
		oxPrivKey                              *crypto.Key
		recipients                             []recipientsType
		fast                                   xmpp.Fast
		// There are some errors that we ignore as we do not want to
		// stop the execution. Failure is used to track those to exit
		// with a non-success return value.
		failure error
	)

	log.SetFlags(0)
	log.SetOutput(new(prettyLogger))
	slog.SetLogLoggerLevel(slog.LevelWarn)
	// Define command line flags.
	flagHelp := getopt.BoolLong("help", 0, "Show help.")
	flagHTTPUpload := getopt.ListLong("http-upload", 'h', "Send a file via http-upload. Can be invoked several times to upload multiple files.")
	flagDebug := getopt.BoolLong("debug", 'd', "Show XMPP stanzas.")
	flagVerbose := getopt.BoolLong("verbose", 'v', "Show debug information.")
	flagServer := getopt.StringLong("jserver", 'j', "", "XMPP server address.")
	flagUser := getopt.StringLong("username", 'u', "", "Username for XMPP account.")
	flagPassword := getopt.StringLong("password", 'p', "", "Password for XMPP account.")
	flagChatroom := getopt.BoolLong("chatroom", 'c', "Send message to a chatroom.")
	flagDirectTLS := getopt.BoolLong("tls", 't', "Use direct TLS.")
	flagAlias := getopt.StringLong("alias", 'a', "", "Set alias/nickname"+
		"for chatrooms.")
	flagFile := getopt.StringLong("file", 'f', "", "Set configuration file. (Default: "+
		"~/.config/go-sendxmpp/sendxmpprc)")
	flagMessageFile := getopt.StringLong("message", 'm', "", "Set file including the message.")
	flagInteractive := getopt.BoolLong("interactive", 'i', "Interactive mode (for use with e.g. 'tail -f').")
	flagSkipVerify := getopt.BoolLong("no-tls-verify", 'n',
		"Skip verification of TLS certificates (not recommended).")
	flagRaw := getopt.BoolLong("raw", 0, "Send raw XML.")
	flagListen := getopt.BoolLong("listen", 'l', "Listen for messages and print them to stdout.")
	flagTimeout := getopt.IntLong("timeout", 0, defaultTimeout, "Connection timeout in seconds.")
	flagTLSMinVersion := getopt.IntLong("tls-version", 0, defaultTLSMinVersion,
		"Minimal TLS version. 10 (TLSv1.0), 11 (TLSv1.1), 12 (TLSv1.2) or 13 (TLSv1.3).")
	flagVersion := getopt.BoolLong("version", 0, "Show version information.")
	flagMUCPassword := getopt.StringLong("muc-password", 0, "", "Password for password protected MUCs.")
	flagOx := getopt.BoolLong("ox", 0, "Use \"OpenPGP for XMPP\" encryption (experimental).")
	flagOxGenPrivKeyRSA := getopt.BoolLong("ox-genprivkey-rsa", 0,
		"Generate a private OpenPGP key (RSA 4096 bit) for the given JID and publish the "+
			"corresponding public key.")
	flagOxGenPrivKeyX25519 := getopt.BoolLong("ox-genprivkey-x25519", 0,
		"Generate a private OpenPGP key (x25519) for the given JID and publish the "+
			"corresponding public key.")
	flagOxPassphrase := getopt.StringLong("ox-passphrase", 0, "",
		"Passphrase for locking and unlocking the private OpenPGP key.")
	flagOxImportPrivKey := getopt.StringLong("ox-import-privkey", 0, "",
		"Import an existing private OpenPGP key.")
	flagOxDeleteNodes := getopt.BoolLong("ox-delete-nodes", 0, "Delete existing OpenPGP nodes on the server.")
	flagOOBFile := getopt.StringLong("oob-file", 0, "", "URL to send a file as out of band data.")
	flagHeadline := getopt.BoolLong("headline", 0, "Send message as type headline.")
	flagSCRAMPinning := getopt.StringLong("scram-mech-pinning", 0, "", "Enforce the use of a certain SCRAM authentication mechanism.")
	flagSSDPOff := getopt.BoolLong("ssdp-off", 0, "Disable XEP-0474: SASL SCRAM Downgrade Protection.")
	flagSubject := getopt.StringLong("subject", 's', "", "Set message subject.")
	flagFastOff := getopt.BoolLong("fast-off", 0, "Disable XEP-0484: Fast Authentication Streamlining Tokens.")
	flagFastInvalidate := getopt.BoolLong("fast-invalidate", 0, "Invalidate XEP-0484: Fast Authentication Streamlining Tokens.")
	flagPLAINAllow := getopt.BoolLong("allow-plain", 0, "Allow PLAIN authentication.")
	flagNoRootWarning := getopt.BoolLong("suppress-root-warning", 0, "Suppress warning when run as root.")
	flagAnon := getopt.BoolLong("anonymous", 0, "Use anonymous authentication (specify the target anon service as username.")
	flagNoSASLUpgrade := getopt.BoolLong("no-sasl-upgrade", 0, "Disable XEP-0480: SASL Upgrade Tasks.")
	flagRecipientsFile := getopt.StringLong("recipients", 'r', "", "Read recipients from file.")
	flagRetryConnect := getopt.IntLong("retry-connect", 0, 0, "Time to retry to connect after failed connection in seconds. (0 = disabled)")
	flagRetryConnectMax := getopt.IntLong("retry-connect-max", 0, 0, "Number of maximum retries to perform for '--retry-connect'. (0 = unlimited)")
	flagLegacyPGP := getopt.BoolLong("legacy-pgp", 0, "Use \"Legacy PGP\" encryption using the Ox key infrastructure (experimental and not encouraged).")

	// Parse command line flags.
	getopt.Parse()

	switch {
	case *flagHelp:
		// If requested, show help and quit.
		getopt.PrintUsage(os.Stdout)
		os.Exit(0)
	case *flagVersion:
		// If requested, show version and quit.
		fmt.Println("Go-sendxmpp", version)
		fmt.Println("Go-xmpp library version:", xmpp.Version)
		fmt.Println("Xmppsrv library version:", xmppsrv.Version)
		fmt.Println("Gopenpgp library version:", gopenpgpConst.Version)
		system := runtime.GOOS + "/" + runtime.GOARCH
		fmt.Println("System:", system, runtime.Version())
		fmt.Println("License: BSD-2-clause")
		os.Exit(0)
		// Quit if Ox (OpenPGP for XMPP) is requested for unsupported operations like
		// groupchat, http-upload or listening.
	case *flagOx && len(*flagHTTPUpload) != 0:
		log.Fatal("No Ox support for http-upload available.")
	case (*flagOx || *flagLegacyPGP) && *flagChatroom:
		log.Fatal("No Ox or legacy PGP support for chat rooms available.")
	case len(*flagHTTPUpload) != 0 && *flagInteractive:
		log.Fatal("Interactive mode and http upload can't" +
			" be used at the same time.")
	case len(*flagHTTPUpload) != 0 && *flagMessageFile != "":
		log.Fatal("You can't send a message while using" +
			" http upload.")
	case *flagOx && *flagLegacyPGP:
		log.Fatal("Ox and legacy PGP can't be used simultaneously.")
	case (*flagOx || *flagLegacyPGP) && *flagOOBFile != "":
		log.Fatal("No encryption possible for OOB data.")
	case (*flagOx || *flagLegacyPGP) && *flagHeadline:
		log.Fatal("No Ox or legacy PGP support for headline messages.")
	case *flagLegacyPGP && *flagSubject != "":
		log.Fatal("No legacy PGP support for subject.")
	case *flagHeadline && *flagChatroom:
		log.Fatal("Can't use message type headline for groupchat messages.")
	case *flagAnon && *flagUser == "":
		log.Fatal("Specifying a username is required when using anonymous authentication.")
	}

	if *flagVerbose {
		slog.SetLogLoggerLevel(slog.LevelDebug)
	}

	// Print a warning if go-sendxmpp is run by the user root on non-windows systems.
	if runtime.GOOS != "windows" && !*flagNoRootWarning {
		// Get the current user.
		slog.Info("checking for current OS user")
		currUser, err := osUser.Current()
		if err != nil {
			log.Fatal("Failed to get current user: ", err)
		}
		slog.Info("checking if OS user is root")
		slog.Info("user identification", "UID", syscall.Getuid())
		slog.Info("user identification", "GID", syscall.Getgid())
		if currUser.Username == "root" {
			fmt.Println("WARNING: It seems you are running go-sendxmpp as root user.\n" +
				"This is not recommended as go-sendxmpp does not require root " +
				"privileges. Please consider using a less privileged user. For an " +
				"example how to do this with sudo please consult the manpage chapter " +
				"TIPS.")
			// Logging that we're running as root
			isRoot = true
			*flagFastOff = true
			slog.Info("disabling fast as we're running as root")
			*flagOx = false
			slog.Info("disabling OX as we're running as root")
		}
	}

	switch *flagSCRAMPinning {
	case "", "SCRAM-SHA-1", "SCRAM-SHA-1-PLUS", "SCRAM-SHA-256", "SCRAM-SHA-256-PLUS",
		"SCRAM-SHA-512", "SCRAM-SHA-512-PLUS":
		slog.Info("SCRAM pinning", "mechanism", *flagSCRAMPinning)
	default:
		log.Fatal("Unknown SCRAM mechanism: ", *flagSCRAMPinning)
	}

	// Read recipients from command line, if no recipients list is specified, and quit
	// if none are specified. For listening or sending raw XML it's not required to
	// specify a recipient except when sending raw messages to MUCs (go-sendxmpp will
	// join the MUC automatically).
	var recipientsList []string
	if *flagRecipientsFile == "" {
		slog.Info("getting recipients from command line")
		recipientsList = getopt.Args()
	} else {
		slog.Info("getting recipients from recipients file")
		rl, err := readFileByLine(*flagRecipientsFile)
		if err != nil {
			log.Fatal(err)
		}
		if strings.Contains(rl, " ") {
			recipientsList = strings.Split(rl, " ")
		} else {
			recipientsList = strings.Split(rl, "\n")
		}
	}
	slog.Info("determining", "recipients", recipientsList)

	if (len(recipientsList) == 0 && !*flagRaw && !*flagListen && !*flagOxGenPrivKeyX25519 &&
		!*flagOxGenPrivKeyRSA && *flagOxImportPrivKey == "" && !*flagFastInvalidate) &&
		!*flagOxDeleteNodes || (len(recipientsList) == 0 && *flagChatroom) {
		log.Fatal("No recipient specified.")
	}

	// Read configuration file if no user or password is specified and anonymous
	// authentication is not used.
	if (*flagUser == "" || *flagPassword == "") && !*flagAnon {
		// Read configuration from file.
		slog.Info("reading config from command line", "flag", *flagFile)
		config, err := parseConfig(*flagFile)
		if err != nil {
			log.Fatal("Error parsing ", *flagFile, ": ", err)
		}
		// Set connection options according to config.
		user = config.username
		slog.Info("config", "user", config.username)
		server = config.jserver
		slog.Info("config", "jserver", config.jserver)
		password = config.password
		slog.Info("config", "password", "REDACTED")
		alias = config.alias
		slog.Info("config", "alias", config.alias)
		if config.port != "" {
			slog.Info("config", "port", config.port)
			server = net.JoinHostPort(server, fmt.Sprint(config.port))
			slog.Info("setting", "server", server)
		}
		allowPLAIN = config.allowPLAIN
		slog.Info("config", "allow-plain", config.allowPLAIN)
	}

	// Overwrite user if specified via command line flag.
	if *flagUser != "" {
		user = *flagUser
		slog.Info("overwriting config value by command line flag", "user", *flagUser)
	}

	// Overwrite server if specified via command line flag.
	if *flagServer != "" {
		server = *flagServer
		slog.Info("overwriting config value by command line flag", "server", *flagServer)
	}

	// Overwrite password if specified via command line flag.
	if *flagPassword != "" && !*flagAnon {
		password = *flagPassword
		slog.Info("overwriting config value by command line flag", "password", "REDACTED")
	}
	// If no server part is specified in the username but a server is specified
	// just assume the server is identical to the server part and hope for the
	// best. This is for compatibility with the old perl sendxmpp config files.
	var serverpart string
	if !strings.Contains(user, "@") && server != "" && !*flagAnon {
		slog.Info("user seems to be no full JID")
		// Remove port if server contains it.
		if strings.Contains(server, ":") {
			serverpart, _, err = net.SplitHostPort(server)
			if err != nil {
				log.Fatal(err)
			}
		} else {
			serverpart = server
		}
		slog.Info("creating JID with", "userpart", user, "serverpart", serverpart)
		user = user + "@" + serverpart
	}

	switch {
	// Use "go-sendxmpp" if no nick is specified via config or command line flag.
	case *flagChatroom && alias == "" && *flagAlias == "":
		slog.Info("no alias defined, using go-sendxmpp")
		alias = "go-sendxmpp"
		// Overwrite configured alias if a nick is specified via command line flag.
	case *flagAlias != "":
		alias = *flagAlias
		slog.Info("overwriting config value by command line flag", "alias", *flagAlias)
	}

	// Timeout
	timeout := time.Duration(*flagTimeout) * time.Second
	slog.Info("setting", "timeout", timeout)

	slog.Info("creating client ID")
	clientID, err := getClientID(user)
	if err != nil {
		fmt.Println(err)
	}

	if !*flagFastOff {
		slog.Info("getting FAST data")
		fast, err = getFastData(user, password, clientID)
		if err != nil {
			slog.Info("no FAST data found")
		}
		// Reset FAST token and mechanism if expired or
		// FastInvalidate is set.
		if time.Now().After(fast.Expiry) || *flagFastInvalidate {
			slog.Info("invalidating fast data:", "token expired", time.Now().After(fast.Expiry),
				"flag FAST invalidate", *flagFastInvalidate)
			fast.Token = ""
			fast.Mechanism = ""
		}
	}
	if fast.Mechanism != "" && *flagSCRAMPinning != "" {
		log.Fatalf("FAST: %s is requested, but we have a token for %s.", *flagSCRAMPinning, fast.Mechanism)
	}

	var tlsConfig tls.Config
	jserver := user[strings.Index(user, "@")+1:]
	if !*flagAnon {
		tlsConfig.ServerName, err = idna.ToASCII(jserver)
		if err != nil {
			log.Fatal(err)
		}
		slog.Info("setting TLS config:", "ServerName", tlsConfig.ServerName)
	} else {
		tlsConfig.ServerName, err = idna.ToASCII(*flagUser)
		if err != nil {
			log.Fatal(err)
		}
		slog.Info("setting TLS config:", "ServerName", tlsConfig.ServerName)
		*flagSCRAMPinning = "ANONYMOUS"
		slog.Info("setting SCRAM", "mechanism", *flagSCRAMPinning)
	}
	// Use ALPN
	tlsConfig.NextProtos = append(tlsConfig.NextProtos, "xmpp-client")
	slog.Info("setting TLS config:", "NextProtos", tlsConfig.NextProtos)
	if *flagSkipVerify {
		// #nosec G402 -- user must explicitly enable it and a warning is logged
		tlsConfig.InsecureSkipVerify = *flagSkipVerify
		slog.Warn("TLS config INSECURE:", "InsecureSkipVerify", tlsConfig.InsecureSkipVerify)
	} else {
		slog.Info("setting TLS config:", "InsecureSkipVerify", tlsConfig.InsecureSkipVerify)
	}
	tlsConfig.Renegotiation = tls.RenegotiateNever
	slog.Info("setting TLS config:", "Renegotiation", tlsConfig.Renegotiation)
	switch *flagTLSMinVersion {
	case defaultTLS10:
		slog.Info("setting TLS config:", "MinVersion", defaultTLS10)
		tlsConfig.MinVersion = tls.VersionTLS10
	case defaultTLS11:
		slog.Info("setting TLS config:", "MinVersion", defaultTLS11)
		tlsConfig.MinVersion = tls.VersionTLS11
	case defaultTLS12:
		slog.Info("setting TLS config:", "MinVersion", defaultTLS12)
		tlsConfig.MinVersion = tls.VersionTLS12
	case defaultTLS13:
		slog.Info("setting TLS config:", "MinVersion", defaultTLS13)
		tlsConfig.MinVersion = tls.VersionTLS13
	default:
		fmt.Println("Unknown TLS version.")
		os.Exit(0)
	}

	resource := "go-sendxmpp." + getShortID()

	// Check whether PLAIN authentication is disabled.
	pinNoPLAIN, _ := parseAuthPinFile(user)
	noPLAIN := pinNoPLAIN || (!*flagPLAINAllow && !allowPLAIN)

	// Periodic server ping settings
	psp := true
	pspPeriod := 30000

	// Set XMPP connection options.
	options := xmpp.Options{
		Host:        server,
		User:        user,
		DialTimeout: timeout,
		Resource:    resource,
		Password:    password,
		// NoTLS doesn't mean that no TLS is used at all but that instead
		// of using an encrypted connection to the server (direct TLS)
		// an unencrypted connection is established. As StartTLS is
		// set when NoTLS is set go-sendxmpp won't use unencrypted
		// client-to-server connections.
		// See https://pkg.go.dev/github.com/xmppo/go-xmpp#Options
		NoTLS:                     !*flagDirectTLS,
		StartTLS:                  !*flagDirectTLS,
		Debug:                     *flagDebug,
		DebugWriter:               os.Stdout,
		TLSConfig:                 &tlsConfig,
		Mechanism:                 *flagSCRAMPinning,
		SSDP:                      !*flagSSDPOff,
		UserAgentSW:               resource,
		UserAgentID:               clientID,
		Fast:                      !*flagFastOff,
		FastToken:                 fast.Token,
		FastMechanism:             fast.Mechanism,
		FastInvalidate:            *flagFastInvalidate,
		NoPLAIN:                   noPLAIN,
		NoSASLUpgrade:             *flagNoSASLUpgrade,
		PeriodicServerPings:       psp,
		PeriodicServerPingsPeriod: pspPeriod,
		ReportSoftwareVersion:     true,
		SoftwareName:              "go-sendxmpp",
		SoftwareVersion:           version,
		ReportSoftwareOS:          true,
	}
	slog.Info("setting xmpp connection option:", "Host", server)
	slog.Info("setting xmpp connection option:", "User", user)
	slog.Info("setting xmpp connection option:", "DialTimeout", timeout)
	slog.Info("setting xmpp connection option:", "Resource", resource)
	slog.Info("setting xmpp connection option:", "Password", "REDACTED")
	slog.Info("setting xmpp connection option:", "NoTLS", !*flagDirectTLS)
	slog.Info("setting xmpp connection option:", "StartTLS", !*flagDirectTLS)
	slog.Info("setting xmpp connection option:", "Debug", *flagDebug)
	slog.Info("setting xmpp connection option:", "Mechanism", *flagSCRAMPinning)
	slog.Info("setting xmpp connection option:", "SSDP", !*flagSSDPOff)
	slog.Info("setting xmpp connection option:", "UserAgentSW", resource)
	slog.Info("setting xmpp connection option:", "UserAgentID", clientID)
	slog.Info("setting xmpp connection option:", "Fast", !*flagFastOff)
	slog.Info("setting xmpp connection option:", "FastToken", "REDACTED")
	slog.Info("setting xmpp connection option:", "FastMechanism", fast.Mechanism)
	slog.Info("setting xmpp connection option:", "FastInvalidate", *flagFastInvalidate)
	slog.Info("setting xmpp connection option:", "NoSASLUpgrade", *flagNoSASLUpgrade)
	slog.Info("setting xmpp connection option:", "NoPLAIN", noPLAIN)
	slog.Info("setting xmpp connection option:", "PeriodicServerPings", psp)
	slog.Info("setting xmpp connection option:", "PeriodicServerPingsPeriod", pspPeriod)

	// Read message from file.
	if *flagMessageFile != "" {
		slog.Info("reading message from message file")
		message, err = readFileByLine(*flagMessageFile)
		if err != nil {
			log.Fatal(err)
		}
	}

	// Skip reading message if '-i' or '--interactive' is set to work with e.g. 'tail -f'.
	// Also for listening mode and Ox key handling.
	if !*flagInteractive && !*flagListen && len(*flagHTTPUpload) == 0 &&
		!*flagOxDeleteNodes && *flagOxImportPrivKey == "" &&
		!*flagOxGenPrivKeyX25519 && !*flagOxGenPrivKeyRSA && *flagOOBFile == "" &&
		!*flagFastInvalidate && message == "" {
		slog.Info("reading message from stdin")
		scanner := bufio.NewScanner(os.Stdin)
		for scanner.Scan() {
			if message == "" {
				message = scanner.Text()
				slog.Info("reading message", "line", scanner.Text())
			} else {
				message = message + "\n" + scanner.Text()
				slog.Info("reading message", "line", scanner.Text())
			}
		}

		if err := scanner.Err(); err != nil {
			if err != io.EOF {
				log.Fatal(err)
			}
		}
	}

	// Remove invalid code points.
	slog.Info("removing invalid code points from message")
	message = validUTF8(message)
	// Exit if message is empty.
	if message == "" && !*flagInteractive && !*flagListen && !*flagOxGenPrivKeyRSA &&
		!*flagOxGenPrivKeyX25519 && *flagOxImportPrivKey == "" &&
		!*flagOxDeleteNodes && len(*flagHTTPUpload) == 0 && *flagOOBFile == "" &&
		!*flagFastInvalidate {
		slog.Info("exiting due to empty message")
		os.Exit(0)
	}

	if isRoot {
		_ = dropPrivs()
	}

	// Connect to server.
	client, err := connect(options, *flagDirectTLS, *flagAnon, *flagRetryConnect, *flagRetryConnectMax)
	if err != nil {
		if fast.Token != "" {
			slog.Info("failed connecting using fast", "mechanism", options.FastMechanism, "token", options.FastToken)
			options.FastToken = ""
			options.FastMechanism = ""
			client, err = connect(options, *flagDirectTLS, *flagAnon, *flagRetryConnect, *flagRetryConnectMax)
			if err != nil {
				log.Fatal(err)
			}
		} else {
			log.Fatal(err)
		}
	}
	slog.Info("connection successful")

	// Update fast token if a new one is received or expiry time is reduced.
	if (client.Fast.Token != "" && client.Fast.Token != fast.Token) ||
		(client.Fast.Expiry.Before(fast.Expiry) && !client.Fast.Expiry.IsZero()) {
		fast.Token = client.Fast.Token
		slog.Info("updating FAST:", "token", "REDACTED")
		fast.Mechanism = client.Fast.Mechanism
		slog.Info("updating FAST:", "mechanism", fast.Mechanism)
		fast.Expiry = client.Fast.Expiry
		slog.Info("updating FAST:", "expiry", fast.Expiry)
		slog.Info("writing FAST data to disk")
		err := writeFastData(user, password, clientID, fast)
		if err != nil {
			fmt.Println(err)
		}
	}
	// Delete stored fast data when FastInvalidate is set.
	if *flagFastInvalidate && *flagFastOff {
		slog.Info("deleting FAST data")
		err := deleteFastData(user)
		if err != nil {
			fmt.Println(err)
		}
	}
	// If authentication is not yet pinned to not use PLAIN and a SCRAM mechanism is
	// used, write the auth pin file.
	if !pinNoPLAIN && (strings.HasPrefix(client.Mechanism, "SCRAM") ||
		strings.HasPrefix(client.Mechanism, "HT")) {
		slog.Info("writing auth pin file")
		err = writeAuthPinFile(user)
		if err != nil {
			log.Println("could not write authentication mechanism pin file:", err)
		}
	}
	iqc := make(chan xmpp.IQ, defaultBufferSize)
	msgc := make(chan xmpp.Chat, defaultBufferSize)
	prsc := make(chan xmpp.Presence, defaultBufferSize)
	disc := make(chan xmpp.DiscoItems, defaultBufferSize)
	drc := make(chan xmpp.DiscoResult, defaultBufferSize)
	slc := make(chan xmpp.Slot, defaultBufferSize)
	ctx, cancel := context.WithCancel(context.Background())
	slog.Info("starting to receive stanzas")
	go rcvStanzas(client, ctx, iqc, msgc, prsc, disc, drc, slc)
	for _, r := range recipientsList {
		var re recipientsType
		re.Jid = r
		if *flagOx || *flagLegacyPGP {
			slog.Info("requesting OX key for", "JID", re.Jid)
			re.OxKeyRing, err = oxGetPublicKeyRing(client, iqc, r)
			if err != nil {
				re.OxKeyRing = nil
				fmt.Printf("ox: error fetching key for %s: %v\n", r, err)
				failure = err
			}
		}
		recipients = append(recipients, re)
	}

	// Check that all recipient JIDs are valid.
	for i, recipient := range recipients {
		slog.Info("checking validity", "JID", recipient.Jid)
		validatedJid, err := MarshalJID(recipient.Jid)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		recipients[i].Jid = validatedJid
	}

	switch {
	case *flagOxGenPrivKeyX25519:
		slog.Info("checking validity", "JID", user)
		validatedOwnJid, err := MarshalJID(user)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		slog.Info("generating X25519 private OX key")
		err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase,
			crypto.KeyGenerationCurve25519Legacy)
		// crypto.KeyGenerationCurve25519)
		// TODO: Clarify whether RFC9580 can also be used instead of RFC4880bis
		// for OX keys.
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		os.Exit(0)
	case *flagOxGenPrivKeyRSA:
		slog.Info("checking validity", "JID", user)
		validatedOwnJid, err := MarshalJID(user)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		slog.Info("generating RSA4096 private OX key")
		err = oxGenPrivKey(validatedOwnJid, client, iqc, *flagOxPassphrase, crypto.KeyGenerationRSA4096)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		os.Exit(0)
	case *flagOxImportPrivKey != "":
		slog.Info("checking validity", "JID", user)
		validatedOwnJid, err := MarshalJID(user)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		slog.Info("importing private OX key:", "file", *flagOxImportPrivKey)
		err = oxImportPrivKey(validatedOwnJid, *flagOxImportPrivKey,
			client, iqc)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		os.Exit(0)
	case *flagOxDeleteNodes:
		slog.Info("checking validity", "JID", user)
		validatedOwnJid, err := MarshalJID(user)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		slog.Info("delete OX pep nodes")
		err = oxDeleteNodes(validatedOwnJid, client, iqc)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		os.Exit(0)
	case *flagOx, *flagLegacyPGP:
		slog.Info("checking validity", "JID", user)
		validatedOwnJid, err := MarshalJID(user)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		slog.Info("reading private OX key")
		oxPrivKey, err = oxGetPrivKey(validatedOwnJid, *flagOxPassphrase)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
	}

	var uploadMessages []string
	if len(*flagHTTPUpload) != 0 && !*flagLegacyPGP {
		slog.Info("http-upload:", "file", *flagHTTPUpload)
		uploadMessages, err = httpUpload(client, iqc, disc, drc, slc,
			jserver, *flagHTTPUpload, timeout, nil, nil)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
	}

	if *flagOOBFile != "" {
		// Remove invalid UTF8 code points.
		message = validUTF8(*flagOOBFile)
		// Check if the URI is valid.
		uri, err := validURI(message)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
		message = uri.String()
		slog.Info("send OOB:", "uri", message)
	}

	var msgType string
	switch {
	case *flagHeadline:
		msgType = strHeadline
	case *flagChatroom:
		msgType = strGroupchat
	default:
		msgType = strChat
	}
	slog.Info("setting message", "type", msgType)
	if *flagChatroom {
		// Join the MUCs.
		for _, recipient := range recipients {
			var presence xmpp.Presence
			pc := make(chan xmpp.Presence, defaultBufferSize)
			go getPresence(recipient.Jid, pc, prsc)
			if *flagMUCPassword != "" {
				slog.Info("joining MUC using password", "MUC", recipient.Jid, "password", "REDACTED")
				dummyTime := time.Now()
				_, err = client.JoinProtectedMUC(recipient.Jid, alias,
					*flagMUCPassword, xmpp.NoHistory, 0, &dummyTime)
			} else {
				slog.Info("joining", "MUC", recipient.Jid)
				_, err = client.JoinMUCNoHistory(recipient.Jid, alias)
			}
			if err != nil {
				fmt.Printf("Could not join MUC %s: %v\n", recipient.Jid, err)
				failure = err
			}
			select {
			case presence = <-pc:
				if presence.Type == "error" {
					fmt.Printf("Could not join MUC %s\n", recipient.Jid)
					failure = fmt.Errorf("failed to join one or more MUCs")
				}
			case <-time.After(60 * time.Second):
				fmt.Printf("Could not join muc: didn't receive a presence from %s\n", recipient.Jid)
			}
		}
	}
	switch {
	case *flagRaw:
		if message == "" {
			slog.Info("not sending empty message")
			break
		}
		// Send raw XML
		slog.Info("sending raw message")
		_, err = client.SendOrg(message)
		if err != nil {
			cancel()
			closeAndExit(client, err)
		}
	case *flagInteractive:
		// Send in endless loop (for usage with e.g. "tail -f").
		slog.Info("reading in interactive mode from stdin")
		reader := bufio.NewReader(os.Stdin)
		// Quit if ^C is pressed.
		c := make(chan os.Signal, 1)
		signal.Notify(c, os.Interrupt)
		go func() {
			for range c {
				slog.Info("stopping interactive mode")
				cancel()
				if failure != nil {
					closeAndExit(client, failure)
				}
				closeAndExit(client, nil)
			}
		}()
		for {
			message, err = reader.ReadString('\n')
			if err != nil {
				select {
				case <-ctx.Done():
					return
				default:
					if err != nil {
						cancel()
						closeAndExit(client, fmt.Errorf("failed to read from stdin"))
					}
				}
			}
			message = strings.TrimSuffix(message, "\n")
			slog.Info("reading message", "line", message)

			// Remove invalid code points.
			slog.Info("removing invalid code points from message")
			message = validUTF8(message)
			if message == "" {
				continue
			}
			for _, recipient := range recipients {
				switch {
				case *flagOx, *flagLegacyPGP:
					var cryptMessage string
					if recipient.OxKeyRing == nil {
						slog.Info("skipping sending an OX encrypted message due to missing public key for", "JID", recipient.Jid)
						continue
					}
					switch {
					case *flagOx:
						slog.Info("encrypting message with OX for", "JID", recipient.Jid)
						cryptMessage, err = oxEncrypt(client, oxPrivKey,
							recipient.Jid, *recipient.OxKeyRing, message, *flagSubject)
						if err != nil {
							fmt.Printf("ox: couldn't encrypt to %s: %v\n",
								recipient.Jid, err)
							failure = err
							continue
						}
					case *flagLegacyPGP:
						slog.Info("encrypting message with legacy PGP for", "JID", recipient.Jid)
						cryptMessage, err = legacyPGPEncrypt(client, oxPrivKey,
							recipient.Jid, *recipient.OxKeyRing, message)
						if err != nil {
							fmt.Printf("legacy PGP: couldn't encrypt to %s: %v\n",
								recipient.Jid, err)
							failure = err
							continue
						}
					}
					slog.Info("sending OX encrypted message to", "JID", recipient.Jid)
					_, err = client.SendOrg(cryptMessage)
					if err != nil {
						cancel()
						closeAndExit(client, err)
					}
				default:
					slog.Info("sending message to", "JID", recipient.Jid)
					_, err = client.Send(xmpp.Chat{
						Remote: recipient.Jid,
						Type:   msgType, Text: message,
						Subject: *flagSubject,
					})
					if err != nil {
						cancel()
						closeAndExit(client, err)
					}
				}
			}
		}
	case *flagListen:
		slog.Info("listening for incoming messages")
		tz := time.Now().Location()
		// Quit if ^C is pressed.
		c := make(chan os.Signal, 1)
		signal.Notify(c, os.Interrupt)
		go func() {
			for range c {
				slog.Info("stopping listening for incoming messages")
				cancel()
				if failure != nil {
					closeAndExit(client, failure)
				}
				closeAndExit(client, nil)
			}
		}()
		for {
			v := <-msgc
			slog.Info("received message")
			isOx := isOxMsg(v)
			isLegacyPGP := isLegacyPGPMsg(v)
			switch {
			case isOx, isLegacyPGP:
				var msg, prefix string
				var t time.Time
				if isOx {
					slog.Info("message is OX encrypted")
					msg, t, err = oxDecrypt(v, client, iqc, user, oxPrivKey)
					if err != nil {
						log.Println(err)
						continue
					}
					if msg == "" {
						slog.Info("message is empty")
						continue
					}
					prefix = "[OX]"
				} else {
					msg, t, err = legacyPGPDecrypt(v, client, iqc, user, oxPrivKey)
					if err != nil {
						log.Println(err)
						continue
					}
					if msg == "" {
						slog.Info("message is empty")
						continue
					}
					prefix = "[LPGP]"
				}
				var bareFrom string
				switch v.Type {
				case strChat:
					bareFrom = strings.Split(v.Remote, "/")[0]
				case strGroupchat:
					bareFrom = v.Remote
				default:
					bareFrom = strings.Split(v.Remote, "/")[0]
				}
				// Print any messages if no recipients are specified
				if len(recipients) == 0 {
					fmt.Println(t.In(tz).Format(time.RFC3339), prefix,
						bareFrom+":", msg)
				} else {
					for _, recipient := range recipients {
						if strings.Split(v.Remote, "/")[0] ==
							strings.ToLower(recipient.Jid) {
							fmt.Println(t.In(tz).Format(time.RFC3339),
								prefix, bareFrom+":", msg)
						}
					}
				}
			default:
				var t time.Time
				if v.Text == "" {
					continue
				}
				if v.Stamp.IsZero() {
					t = time.Now()
				} else {
					t = v.Stamp
				}
				var bareFrom string
				switch v.Type {
				case strChat:
					bareFrom = strings.Split(v.Remote, "/")[0]
				case strGroupchat:
					bareFrom = v.Remote
				default:
					bareFrom = strings.Split(v.Remote, "/")[0]
				}
				// Print any messages if no recipients are specified
				if len(recipients) == 0 {
					fmt.Println(t.In(tz).Format(time.RFC3339), bareFrom+":", v.Text)
				} else {
					for _, recipient := range recipients {
						if strings.Split(v.Remote, "/")[0] ==
							strings.ToLower(recipient.Jid) {
							fmt.Println(t.In(tz).Format(time.RFC3339),
								bareFrom+":", v.Text)
						}
					}
				}
			}
		}
	default:
		for _, recipient := range recipients {
			if message == "" && len(*flagHTTPUpload) == 0 {
				slog.Info("skipping sending of empty message to", "JID", recipient.Jid)
				break
			}
			switch {
			case len(*flagHTTPUpload) != 0 && !*flagLegacyPGP:
				for _, message = range uploadMessages {
					slog.Info("sending http-upload message:", "file", message, "JID", recipient.Jid)
					_, err = client.SendOOB(xmpp.Chat{
						Remote: recipient.Jid,
						Type:   msgType, Oob: xmpp.Oob{Url: message}, Subject: *flagSubject,
					})
					if err != nil {
						fmt.Println("Couldn't send message to",
							recipient.Jid)
					}
				}
			case len(*flagHTTPUpload) != 0 && *flagLegacyPGP:
				uploadMessages, err = httpUpload(client, iqc, disc, drc, slc,
					jserver, *flagHTTPUpload, timeout, oxPrivKey, recipient.OxKeyRing)
				if err != nil {
					cancel()
					closeAndExit(client, err)
				}
				for _, message = range uploadMessages {
					slog.Info("sending legacy pgp http-upload message:", "file", message, "JID", recipient.Jid)
					_, err = client.SendOOB(xmpp.Chat{
						Remote: recipient.Jid,
						Type:   msgType, Oob: xmpp.Oob{Url: message}, Subject: *flagSubject,
					})
					if err != nil {
						fmt.Println("Couldn't send message to",
							recipient.Jid)
					}
				}

			// (Hopefully) temporary workaround due to go-xmpp choking on URL encoding.
			// Once this is fixed in the lib the http-upload case above can be reused.
			case *flagOOBFile != "":
				_, err = client.SendOOB(xmpp.Chat{
					Remote: recipient.Jid,
					Type:   msgType, Oob: xmpp.Oob{Url: message}, Subject: *flagSubject,
				})
				if err != nil {
					fmt.Println("Couldn't send message to",
						recipient.Jid)
				}
			case *flagOx, *flagLegacyPGP:
				if recipient.OxKeyRing == nil {
					slog.Info("skipping sending an OX encrypted message due to missing public key for", "JID", recipient.Jid)
					continue
				}
				var cryptMessage string
				switch {
				case *flagOx:
					slog.Info("encrypting message with OX for", "JID", recipient.Jid)
					cryptMessage, err = oxEncrypt(client, oxPrivKey,
						recipient.Jid, *recipient.OxKeyRing, message, *flagSubject)
					if err != nil {
						fmt.Printf("ox: couldn't encrypt to %s: %v\n",
							recipient.Jid, err)
						failure = err
						continue
					}
				case *flagLegacyPGP:
					slog.Info("encrypting message with legacy PGP for", "JID", recipient.Jid)
					cryptMessage, err = legacyPGPEncrypt(client, oxPrivKey,
						recipient.Jid, *recipient.OxKeyRing, message)
					if err != nil {
						fmt.Printf("legacy PGP: couldn't encrypt to %s: %v\n",
							recipient.Jid, err)
						failure = err
						continue
					}
				}
				_, err = client.SendOrg(cryptMessage)
				if err != nil {
					cancel()
					closeAndExit(client, err)
				}
			default:
				slog.Info("sending message to", "JID", recipient.Jid)
				_, err = client.Send(xmpp.Chat{
					Remote: recipient.Jid,
					Type:   msgType, Text: message,
					Subject: *flagSubject,
				})
				if err != nil {
					cancel()
					closeAndExit(client, err)
				}
			}
		}
	}
	cancel()
	if failure != nil {
		closeAndExit(client, failure)
	}
	closeAndExit(client, nil)
}
