💾 Archived View for thrig.me › blog › 2024 › 04 › 23 › syncmail.go captured on 2024-05-10 at 12:05:48.
-=-=-=-=-=-=-
// 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) } }