Author | Jakob Wakeling <[email protected]> |
Date | 2024-01-28 08:13:23 |
Commit | f28abe48a882b98ff776e696ce4d7a5838438de7 |
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"