backup-utils

Backup utilities
git clone http://git.omkov.net/backup-utils
Log | Tree | Refs | Download

AuthorJakob Wakeling <[email protected]>
Date2024-01-28 08:13:23
Commitf28abe48a882b98ff776e696ce4d7a5838438de7

Add basic IMAP backup program

Diffstat

A .gitignore | 2 ++
A Makefile | 12 ++++++++++++
A src/backup-imap/go.mod | 14 ++++++++++++++
A src/backup-imap/go.sum | 26 ++++++++++++++++++++++++++
A src/backup-imap/main.go | 149 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

5 files changed, 203 insertions, 0 deletions

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2cc5ddb
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,2 @@
+/.vscode/
+/bin/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..290900b
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,12 @@
+.PHONY: all build test help
+all: help
+
+build: ## Build the project
+	@go build -C ./src/backup-imap -o ../../bin/backup-imap .
+
+test: ## Run unit tests
+	@go test ./...
+
+help: ## Display help information
+	@grep -E '^[a-zA-Z_-]+:.*?##.*$$' $(MAKEFILE_LIST) | \
+		awk 'BEGIN {FS = ":.*?## *"}; {printf "\033[36m%-6s\033[0m %s\n", $$1, $$2}'
diff --git a/src/backup-imap/go.mod b/src/backup-imap/go.mod
new file mode 100644
index 0000000..3499a01
--- /dev/null
+++ b/src/backup-imap/go.mod
@@ -0,0 +1,14 @@
+module backup-imap
+
+go 1.21.0
+
+require (
+	github.com/alexflint/go-arg v1.4.3
+	github.com/emersion/go-imap v1.2.1
+)
+
+require (
+	github.com/alexflint/go-scalar v1.1.0 // indirect
+	github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
+	golang.org/x/text v0.3.7 // indirect
+)
diff --git a/src/backup-imap/go.sum b/src/backup-imap/go.sum
new file mode 100644
index 0000000..e0cf39a
--- /dev/null
+++ b/src/backup-imap/go.sum
@@ -0,0 +1,26 @@
+github.com/alexflint/go-arg v1.4.3 h1:9rwwEBpMXfKQKceuZfYcwuc/7YY7tWJbFsgG5cAU/uo=
+github.com/alexflint/go-arg v1.4.3/go.mod h1:3PZ/wp/8HuqRZMUUgu7I+e1qcpUbvmS258mRXkFH4IA=
+github.com/alexflint/go-scalar v1.1.0 h1:aaAouLLzI9TChcPXotr6gUhq+Scr8rl0P9P4PnltbhM=
+github.com/alexflint/go-scalar v1.1.0/go.mod h1:LoFvNMqS1CPrMVltza4LvnGKhaSpc3oyLEBUZVhhS2o=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/emersion/go-imap v1.2.1 h1:+s9ZjMEjOB8NzZMVTM3cCenz2JrQIGGo5j1df19WjTA=
+github.com/emersion/go-imap v1.2.1/go.mod h1:Qlx1FSx2FTxjnjWpIlVNEuX+ylerZQNFE5NsmKFSejY=
+github.com/emersion/go-message v0.15.0/go.mod h1:wQUEfE+38+7EW8p8aZ96ptg6bAb1iwdgej19uXASlE4=
+github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 h1:OJyUGMJTzHTd1XQp98QTaHernxMYzRaOasRir9hUlFQ=
+github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
+github.com/emersion/go-textwrapper v0.0.0-20200911093747-65d896831594/go.mod h1:aqO8z8wPrjkscevZJFVE1wXJrLpC5LtJG7fqLOsPb2U=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/src/backup-imap/main.go b/src/backup-imap/main.go
new file mode 100644
index 0000000..bab3340
--- /dev/null
+++ b/src/backup-imap/main.go
@@ -0,0 +1,149 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
+
+	"github.com/alexflint/go-arg"
+	"github.com/emersion/go-imap"
+	"github.com/emersion/go-imap/client"
+)
+
+const Version = "0.0.1"
+
+func main() {
+	var args struct {
+		Address  string `arg:"positional,required"`
+		Username string `arg:"positional,required"`
+		Password string `arg:"positional,required"`
+		Output   string `arg:"-o" help:"Output directory" default:"mail"`
+	}
+
+	if err := arg.Parse(&args); err != nil {
+		switch err {
+		case arg.ErrHelp:
+			fmt.Print(help)
+			os.Exit(0)
+		case arg.ErrVersion:
+			fmt.Print(version)
+			os.Exit(0)
+		}
+
+		fmt.Println(err.Error())
+		os.Exit(-1)
+	}
+
+	c, err := client.DialTLS(args.Address, nil)
+	if err != nil {
+		log.Fatalln(err.Error())
+	}
+	defer c.Logout()
+
+	if err := c.Login(args.Username, args.Password); err != nil {
+		log.Fatalln(err.Error())
+	}
+
+	mailboxes, err := Mailboxes(c)
+	if err != nil {
+		log.Fatalln(err.Error())
+	}
+
+	for _, m := range mailboxes {
+		if m.Name == "Trash" {
+			log.Println("Skipping Trash")
+			continue
+		}
+
+		log.Println(m.Name)
+		if err := FetchMailbox(c, m, args.Output); err != nil {
+			log.Fatalln(err.Error())
+		}
+	}
+}
+
+/* Get list of Mailboxes. */
+func Mailboxes(c *client.Client) ([]*imap.MailboxInfo, error) {
+	ch := make(chan *imap.MailboxInfo, 6)
+	done := make(chan error, 1)
+
+	go func() { done <- c.List("", "*", ch) }()
+
+	var mailboxes []*imap.MailboxInfo
+	for m := range ch {
+		mailboxes = append(mailboxes, m)
+	}
+
+	if err := <-done; err != nil {
+		return nil, err
+	}
+
+	return mailboxes, nil
+}
+
+func FetchMailbox(c *client.Client, mailbox *imap.MailboxInfo, dir string) error {
+	mbox, err := c.Select(mailbox.Name, true)
+	if err != nil {
+		return err
+	}
+
+	if err := os.MkdirAll(filepath.Join(dir, mailbox.Name), 0o777); err != nil {
+		return err
+	}
+
+	if mbox.Messages == 0 {
+		return nil
+	}
+
+	seqset := new(imap.SeqSet)
+	seqset.AddRange(1, mbox.Messages)
+
+	section := &imap.BodySectionName{Peek: false}
+	items := []imap.FetchItem{imap.FetchAll, section.FetchItem()}
+
+	messages := make(chan *imap.Message, 10)
+	done := make(chan error, 1)
+
+	go func() {
+		done <- c.Fetch(seqset, items, messages)
+	}()
+
+	for msg := range messages {
+		body := msg.GetBody(section)
+		if body == nil {
+			return fmt.Errorf("message body is nil")
+		}
+
+		data := make([]byte, body.Len())
+		if _, err := body.Read(data); err != nil {
+			return err
+		}
+
+		file, err := os.Create(
+			filepath.Join(dir, mailbox.Name, fmt.Sprintf(
+				"%s_%d.eml", msg.InternalDate.UTC().Format("20060102T150405Z"), msg.SeqNum,
+			)),
+		)
+		if err != nil {
+			return err
+		}
+
+		if _, err := file.Write(data); err != nil {
+			file.Close()
+			return err
+		}
+
+		log.Println(mailbox.Name, "->", msg.Envelope.Subject)
+		file.Close()
+	}
+
+	if err := <-done; err != nil {
+		return err
+	}
+
+	return nil
+}
+
+const help = ``
+const version = "backup-imap, version " + Version + "\n"