From c883470c2ef977b8675b12428591bb003694e235 Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Thu, 23 Apr 2020 20:48:17 +0200 Subject: Restructure imap pkg --- internal/imap/client.go | 202 +------------------------------------------- internal/imap/cmds.go | 27 ++++++ internal/imap/commando.go | 17 ---- internal/imap/connection.go | 137 ++++++++++++++++++++++++++++++ internal/imap/folder.go | 33 ++++++++ internal/imap/imap.go | 57 ++----------- internal/imap/mailboxes.go | 34 ++++++++ internal/imap/url.go | 76 +++++++++++++++++ 8 files changed, 316 insertions(+), 267 deletions(-) create mode 100644 internal/imap/cmds.go create mode 100644 internal/imap/connection.go create mode 100644 internal/imap/folder.go create mode 100644 internal/imap/mailboxes.go create mode 100644 internal/imap/url.go (limited to 'internal') diff --git a/internal/imap/client.go b/internal/imap/client.go index 5e2546b..404c03e 100644 --- a/internal/imap/client.go +++ b/internal/imap/client.go @@ -1,12 +1,6 @@ package imap import ( - "fmt" - "strings" - "sync" - "time" - - "github.com/emersion/go-imap" imapClient "github.com/emersion/go-imap/client" "github.com/Necoro/feed2imap-go/internal/log" @@ -20,75 +14,21 @@ type connConf struct { toplevel Folder } -type connection struct { - *connConf - mailboxes *mailboxes - c *imapClient.Client -} - -type mailboxes struct { - mb map[string]*imap.MailboxInfo - mu sync.RWMutex -} - type Client struct { connConf - mailboxes mailboxes + mailboxes *mailboxes commander *commander connections [numberConns]*connection nextFreeIndex int } -type Folder struct { - str string - delimiter string -} - -func (f Folder) String() string { - return f.str -} - -func (f Folder) Append(other Folder) Folder { - if f.delimiter != other.delimiter { - panic("Delimiters do not match") - } - return Folder{ - str: f.str + f.delimiter + other.str, - delimiter: f.delimiter, - } -} - -func (mbs *mailboxes) contains(elem Folder) bool { - mbs.mu.RLock() - defer mbs.mu.RUnlock() - - _, ok := mbs.mb[elem.str] - return ok -} - -func (mbs *mailboxes) add(elem *imap.MailboxInfo) { - mbs.mu.Lock() - defer mbs.mu.Unlock() - - mbs.mb[elem.Name] = elem -} - -func (conn *connection) Disconnect() bool { - if conn != nil { - connected := (conn.c.State() & imap.ConnectedState) != 0 - _ = conn.c.Logout() - return connected - } - return false -} - func (client *Client) Disconnect() { if client != nil { client.stopCommander() connected := false for _, conn := range client.connections { - connected = conn.Disconnect() || connected + connected = conn.disconnect() || connected } if connected { @@ -97,116 +37,6 @@ func (client *Client) Disconnect() { } } -func (client *Client) folderName(path []string) Folder { - return Folder{ - strings.Join(path, client.delimiter), - client.delimiter, - } -} - -func (client *Client) NewFolder(path []string) Folder { - return client.toplevel.Append(client.folderName(path)) -} - -func (conn *connection) createFolder(folder string) error { - err := conn.c.Create(folder) - if err != nil { - return fmt.Errorf("creating folder '%s': %w", folder, err) - } - - err = conn.c.Subscribe(folder) - if err != nil { - return fmt.Errorf("subscribing to folder '%s': %w", folder, err) - } - - log.Printf("Created folder '%s'", folder) - - return nil -} - -func (conn *connection) list(folder string) (*imap.MailboxInfo, int, error) { - mailboxes := make(chan *imap.MailboxInfo, 10) - done := make(chan error, 1) - go func() { - done <- conn.c.List("", folder, mailboxes) - }() - - found := 0 - var mbox *imap.MailboxInfo - for m := range mailboxes { - if found == 0 { - mbox = m - } - found++ - } - - if err := <-done; err != nil { - return nil, 0, fmt.Errorf("while listing '%s': %w", folder, err) - } - - return mbox, found, nil -} - -func (conn *connection) fetchDelimiter() (string, error) { - mbox, _, err := conn.list("") - if err != nil { - return "", err - } - - return mbox.Delimiter, nil -} - -func (conn *connection) ensureFolder(folder Folder) error { - if conn.mailboxes.contains(folder) { - return nil - } - - log.Printf("Checking for folder '%s'", folder) - - mbox, found, err := conn.list(folder.str) - if err != nil { - return err - } - - if mbox != nil && mbox.Delimiter != folder.delimiter { - panic("Delimiters do not match") - } - - switch found { - case 0: - return conn.createFolder(folder.str) - case 1: - conn.mailboxes.add(mbox) - return nil - default: - return fmt.Errorf("Found multiple folders matching '%s'.", folder) - } -} - -func (client *Client) EnsureFolder(folder Folder) error { - return client.commander.execute(ensureCommando{folder}) -} - -func (conn *connection) putMessages(folder Folder, messages []string) error { - if len(messages) == 0 { - return nil - } - - now := time.Now() - for _, msg := range messages { - reader := strings.NewReader(msg) - if err := conn.c.Append(folder.str, nil, now, reader); err != nil { - return fmt.Errorf("uploading message to %s: %w", folder, err) - } - } - - return nil -} - -func (client *Client) PutMessages(folder Folder, messages []string) error { - return client.commander.execute(addCommando{folder, messages}) -} - func (client *Client) createConnection(c *imapClient.Client) *connection{ if client.nextFreeIndex >= len(client.connections) { panic("Too many connections") @@ -214,7 +44,7 @@ func (client *Client) createConnection(c *imapClient.Client) *connection{ conn := &connection{ connConf: &client.connConf, - mailboxes: &client.mailboxes, + mailboxes: client.mailboxes, c: c, } @@ -224,30 +54,6 @@ func (client *Client) createConnection(c *imapClient.Client) *connection{ return conn } -func (conn *connection) startTls() error { - hasStartTls, err := conn.c.SupportStartTLS() - if err != nil { - return fmt.Errorf("checking for starttls for %s: %w", conn.host, err) - } - - if hasStartTls { - if err = conn.c.StartTLS(nil); err != nil { - return fmt.Errorf("enabling starttls for %s: %w", conn.host, err) - } - - log.Print("Connected to ", conn.host, " (STARTTLS)") - } else { - log.Print("Connected to ", conn.host, " (Plain)") - } - - return nil -} - func NewClient() *Client { - return &Client{ - mailboxes: mailboxes{ - mb: map[string]*imap.MailboxInfo{}, - mu: sync.RWMutex{}, - }, - } + return &Client{mailboxes: NewMailboxes()} } \ No newline at end of file diff --git a/internal/imap/cmds.go b/internal/imap/cmds.go new file mode 100644 index 0000000..499d6c0 --- /dev/null +++ b/internal/imap/cmds.go @@ -0,0 +1,27 @@ +package imap + +type ensureCommando struct { + folder Folder +} + +func (cmd ensureCommando) execute(conn *connection) error { + return conn.ensureFolder(cmd.folder) +} + +func (client *Client) EnsureFolder(folder Folder) error { + return client.commander.execute(ensureCommando{folder}) +} + + +type addCommando struct { + folder Folder + messages []string +} + +func (cmd addCommando) execute(conn *connection) error { + return conn.putMessages(cmd.folder, cmd.messages) +} + +func (client *Client) PutMessages(folder Folder, messages []string) error { + return client.commander.execute(addCommando{folder, messages}) +} diff --git a/internal/imap/commando.go b/internal/imap/commando.go index 171a507..1cf688e 100644 --- a/internal/imap/commando.go +++ b/internal/imap/commando.go @@ -17,23 +17,6 @@ type execution struct { done chan<- error } -type addCommando struct { - folder Folder - messages []string -} - -func (cmd addCommando) execute(conn *connection) error { - return conn.putMessages(cmd.folder, cmd.messages) -} - -type ensureCommando struct { - folder Folder -} - -func (cmd ensureCommando) execute(conn *connection) error { - return conn.ensureFolder(cmd.folder) -} - func (commander *commander) execute(command command) error { done := make(chan error) commander.pipe <- execution{command, done} diff --git a/internal/imap/connection.go b/internal/imap/connection.go new file mode 100644 index 0000000..e0360bd --- /dev/null +++ b/internal/imap/connection.go @@ -0,0 +1,137 @@ +package imap + +import ( + "fmt" + "strings" + "time" + + "github.com/emersion/go-imap" + imapClient "github.com/emersion/go-imap/client" + + "github.com/Necoro/feed2imap-go/internal/log" +) + +type connection struct { + *connConf + mailboxes *mailboxes + c *imapClient.Client +} + +func (conn *connection) startTls() error { + hasStartTls, err := conn.c.SupportStartTLS() + if err != nil { + return fmt.Errorf("checking for starttls for %s: %w", conn.host, err) + } + + if hasStartTls { + if err = conn.c.StartTLS(nil); err != nil { + return fmt.Errorf("enabling starttls for %s: %w", conn.host, err) + } + + log.Print("Connected to ", conn.host, " (STARTTLS)") + } else { + log.Print("Connected to ", conn.host, " (Plain)") + } + + return nil +} + +func (conn *connection) disconnect() bool { + if conn != nil { + connected := (conn.c.State() & imap.ConnectedState) != 0 + _ = conn.c.Logout() + return connected + } + return false +} + +func (conn *connection) createFolder(folder string) error { + err := conn.c.Create(folder) + if err != nil { + return fmt.Errorf("creating folder '%s': %w", folder, err) + } + + err = conn.c.Subscribe(folder) + if err != nil { + return fmt.Errorf("subscribing to folder '%s': %w", folder, err) + } + + log.Printf("Created folder '%s'", folder) + + return nil +} + +func (conn *connection) list(folder string) (*imap.MailboxInfo, int, error) { + mailboxes := make(chan *imap.MailboxInfo, 10) + done := make(chan error, 1) + go func() { + done <- conn.c.List("", folder, mailboxes) + }() + + found := 0 + var mbox *imap.MailboxInfo + for m := range mailboxes { + if found == 0 { + mbox = m + } + found++ + } + + if err := <-done; err != nil { + return nil, 0, fmt.Errorf("while listing '%s': %w", folder, err) + } + + return mbox, found, nil +} + +func (conn *connection) fetchDelimiter() (string, error) { + mbox, _, err := conn.list("") + if err != nil { + return "", err + } + + return mbox.Delimiter, nil +} + +func (conn *connection) ensureFolder(folder Folder) error { + if conn.mailboxes.contains(folder) { + return nil + } + + log.Printf("Checking for folder '%s'", folder) + + mbox, found, err := conn.list(folder.str) + if err != nil { + return err + } + + if mbox != nil && mbox.Delimiter != folder.delimiter { + panic("Delimiters do not match") + } + + switch found { + case 0: + return conn.createFolder(folder.str) + case 1: + conn.mailboxes.add(mbox) + return nil + default: + return fmt.Errorf("Found multiple folders matching '%s'.", folder) + } +} + +func (conn *connection) putMessages(folder Folder, messages []string) error { + if len(messages) == 0 { + return nil + } + + now := time.Now() + for _, msg := range messages { + reader := strings.NewReader(msg) + if err := conn.c.Append(folder.str, nil, now, reader); err != nil { + return fmt.Errorf("uploading message to %s: %w", folder, err) + } + } + + return nil +} diff --git a/internal/imap/folder.go b/internal/imap/folder.go new file mode 100644 index 0000000..1f4e0bf --- /dev/null +++ b/internal/imap/folder.go @@ -0,0 +1,33 @@ +package imap + +import "strings" + +type Folder struct { + str string + delimiter string +} + +func (f Folder) String() string { + return f.str +} + +func (f Folder) Append(other Folder) Folder { + if f.delimiter != other.delimiter { + panic("Delimiters do not match") + } + return Folder{ + str: f.str + f.delimiter + other.str, + delimiter: f.delimiter, + } +} + +func (client *Client) folderName(path []string) Folder { + return Folder{ + strings.Join(path, client.delimiter), + client.delimiter, + } +} + +func (client *Client) NewFolder(path []string) Folder { + return client.toplevel.Append(client.folderName(path)) +} diff --git a/internal/imap/imap.go b/internal/imap/imap.go index 52c61f0..f8dc7d7 100644 --- a/internal/imap/imap.go +++ b/internal/imap/imap.go @@ -10,53 +10,7 @@ import ( "github.com/Necoro/feed2imap-go/internal/log" ) -const ( - imapsPort = "993" - imapPort = "143" - imapsSchema = "imaps" - imapSchema = "imap" -) - -func forceTLS(url *url.URL) bool { - return url.Scheme == imapsSchema || url.Port() == imapsPort -} - -func setDefaultScheme(url *url.URL) { - switch url.Scheme { - case imapSchema, imapsSchema: - return - default: - oldScheme := url.Scheme - if url.Port() == imapsPort { - url.Scheme = imapsSchema - } else { - url.Scheme = imapSchema - } - - if oldScheme != "" { - log.Warnf("Unknown scheme '%s', defaulting to '%s'", oldScheme, url.Scheme) - } - } -} - -func setDefaultPort(url *url.URL) { - if url.Port() == "" { - var port string - if url.Scheme == imapsSchema { - port = imapsPort - } else { - port = imapPort - } - url.Host += ":" + port - } -} - -func sanitizeUrl(url *url.URL) { - setDefaultScheme(url) - setDefaultPort(url) -} - -func newImapClient(url *url.URL, forceTls bool) (*imapClient.Client,error) { +func newImapClient(url *URL, forceTls bool) (*imapClient.Client,error) { if forceTls { c, err := imapClient.DialTLS(url.Host, nil) if err != nil { @@ -73,7 +27,7 @@ func newImapClient(url *url.URL, forceTls bool) (*imapClient.Client,error) { } } -func (client *Client) connect(url *url.URL, forceTls bool) (*connection, error) { +func (client *Client) connect(url *URL, forceTls bool) (*connection, error) { c, err := newImapClient(url, forceTls) if err != nil { return nil, err @@ -95,11 +49,10 @@ func (client *Client) connect(url *url.URL, forceTls bool) (*connection, error) return conn, nil } -func Connect(url *url.URL) (*Client, error) { +func Connect(_url *url.URL) (*Client, error) { var err error - - sanitizeUrl(url) - forceTls := forceTLS(url) + url := NewUrl(_url) + forceTls := url.ForceTLS() client := NewClient() client.host = url.Host diff --git a/internal/imap/mailboxes.go b/internal/imap/mailboxes.go new file mode 100644 index 0000000..d0fdede --- /dev/null +++ b/internal/imap/mailboxes.go @@ -0,0 +1,34 @@ +package imap + +import ( + "sync" + + "github.com/emersion/go-imap" +) + +type mailboxes struct { + mb map[string]*imap.MailboxInfo + mu sync.RWMutex +} + +func (mbs *mailboxes) contains(elem Folder) bool { + mbs.mu.RLock() + defer mbs.mu.RUnlock() + + _, ok := mbs.mb[elem.str] + return ok +} + +func (mbs *mailboxes) add(elem *imap.MailboxInfo) { + mbs.mu.Lock() + defer mbs.mu.Unlock() + + mbs.mb[elem.Name] = elem +} + +func NewMailboxes() *mailboxes { + return &mailboxes{ + mb: map[string]*imap.MailboxInfo{}, + mu: sync.RWMutex{}, + } +} \ No newline at end of file diff --git a/internal/imap/url.go b/internal/imap/url.go new file mode 100644 index 0000000..6ffea72 --- /dev/null +++ b/internal/imap/url.go @@ -0,0 +1,76 @@ +package imap + +import ( + "net" + "net/url" + + "github.com/Necoro/feed2imap-go/internal/log" +) + +// Our own convenience wrapper +type URL struct { + *url.URL + // url.URL has no port field and splits it everytime from Host + port *string +} + +const ( + imapsPort = "993" + imapPort = "143" + imapsSchema = "imaps" + imapSchema = "imap" +) + +func (url *URL) Port() string { + if url.port == nil { + port := url.URL.Port() + url.port = &port + } + return *url.port +} + +func (url *URL) ForceTLS() bool { + return url.Scheme == imapsSchema || url.Port() == imapsPort +} + +func (url *URL) setDefaultScheme() { + switch url.Scheme { + case imapSchema, imapsSchema: + return + default: + oldScheme := url.Scheme + if url.Port() == imapsPort { + url.Scheme = imapsSchema + } else { + url.Scheme = imapSchema + } + + if oldScheme != "" { + log.Warnf("Unknown scheme '%s', defaulting to '%s'", oldScheme, url.Scheme) + } + } +} + +func (url *URL) setDefaultPort() { + if url.Port() == "" { + var port string + if url.Scheme == imapsSchema { + port = imapsPort + } else { + port = imapPort + } + url.port = &port + url.Host = net.JoinHostPort(url.Host, port) + } +} + +func (url *URL) sanitizeUrl() { + url.setDefaultScheme() + url.setDefaultPort() +} + +func NewUrl(url *url.URL) *URL { + u := URL{URL: url} + u.sanitizeUrl() + return &u +} \ No newline at end of file -- cgit v1.2.3-54-g00ecf