💾 Archived View for thrig.me › blog › 2024 › 04 › 23 › syncmail.go captured on 2024-05-10 at 12:05:48.

View Raw

More Information

-=-=-=-=-=-=-

// syncmail - transfer between maildir directories by SFTP. there are
// things to TWEAK

package main

import (
	"encoding/base64"
	"fmt"
	"io"
	"log"
	"net"
	"os"
	"os/signal"
	"path/filepath"
	"syscall"

	"github.com/pkg/sftp"
	"golang.org/x/crypto/ssh"
	"suah.dev/protect"
)

func keyString(k ssh.PublicKey) string {
	return k.Type() + " " + base64.StdEncoding.EncodeToString(k.Marshal()) // e.
}

func hostkey_callback(trustedKey string) ssh.HostKeyCallback {
	if trustedKey == "" {
		return func(_ string, _ net.Addr, k ssh.PublicKey) error {
			return fmt.Errorf("SSH no key verify: %q", keyString(k))
		}
	}
	return func(_ string, _ net.Addr, k ssh.PublicKey) error {
		ks := keyString(k)
		if trustedKey != ks {
			return fmt.Errorf("SSH verify: expect %q got %q", trustedKey, ks)
		}
		return nil
	}
}

func main() {
	sigc := make(chan os.Signal, 1)
	signal.Notify(sigc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT)
	go func() {
		<-sigc
	}()

	protect.Pledge("cpath inet rpath stdio wpath unveil")

	// TWEAK where the remote files are copied to
	home, _ := os.UserHomeDir()
	tmp_path := filepath.Join(home, "mail/inbox/tmp")
	new_path := filepath.Join(home, "mail/inbox/new")
	herr := os.Chdir(tmp_path)
	if herr != nil {
		log.Fatal(herr)
	}

	sshkey := filepath.Join(home, ".ssh/TWEAK_your_public_key_file_here")

	protect.Unveil(sshkey, "r")
	protect.Unveil(tmp_path, "cw")
	protect.Unveil(new_path, "cw")
	protect.UnveilBlock()

	key, err := os.ReadFile(sshkey)
	if err != nil {
		log.Fatalf("unable to read private key: %v", err)
	}
	signer, err := ssh.ParsePrivateKey(key)
	if err != nil {
		log.Fatalf("unable to parse private key: %v", err)
	}
	config := &ssh.ClientConfig{
		User: "TWEAK_username",
		Auth: []ssh.AuthMethod{
			ssh.PublicKeys(signer),
		},
		HostKeyCallback: hostkey_callback("ssh-ed25519 TWEAK_your_host_key_here"),
	}

	// NOTE this requires a port, does not assume :22
	ssh, err := ssh.Dial("tcp", "TWEAK_hostname_or_ip:22", config)
	if err != nil {
		log.Fatalf("unable to connect: %v", err)
	}
	defer ssh.Close()

	protect.Pledge("cpath stdio wpath")

	sftp, err := sftp.NewClient(ssh)
	if err != nil {
		log.Fatal(err)
	}
	defer sftp.Close()

	files, err := sftp.ReadDir("mail/inbox/new")
	if err != nil {
		log.Fatal(err)
	}
	copied := 0
	for _, dirent := range files {
		if !dirent.Mode().IsRegular() {
			// TWEAK you could log unexpected file types here
			continue
		}
		remote_file := sftp.Join("mail/inbox/new", dirent.Name())

		sfh, err := sftp.Open(remote_file)
		if err != nil {
			log.Fatal(err)
		}
		defer sfh.Close()

		dfh, err := os.OpenFile(dirent.Name(), os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0755)
		if err != nil {
			log.Fatal(err)
		}
		defer dfh.Close()

		_, cerr := io.Copy(dfh, sfh)
		if cerr != nil {
			log.Fatal(cerr)
		}

		derr := dfh.Sync()
		if derr != nil {
			log.Fatal(derr)
		}

		rnerr := os.Rename(dirent.Name(), filepath.Join("../new", dirent.Name()))
		if rnerr != nil {
			// TWEAK you may want to instead keep this file
			// around, especially if the files can be large
			// and network transfers expensive, but then
			// you'd need something to detect the error or
			// review for stale files in the "tmp"
			// directory, and to manually rename the file
			// or whatever
			os.Remove(dirent.Name())
			log.Fatal(rnerr)
		}

		rerr := sftp.Remove(remote_file)
		if rerr != nil {
			log.Fatal(rerr)
		}

		// TWEAK log what got copied
		//log.Println(remote_file)

		copied++
	}

	if copied > 0 {
		os.Exit(0)
	} else {
		os.Exit(2)
	}
}