package lib import ( "bytes" "fmt" "time" "git.iamthefij.com/iamthefij/slog" "github.com/emersion/go-imap" imapClient "github.com/emersion/go-imap/client" ) const ( maxChannels = 10 baseNotesFolder = "Notes" ) type ContentType string const ( ContentTypePlainText ContentType = "text/plain" ContentTypeHtml ContentType = `text/html; charset="UTF-8"` ) // BuildEmail generates a buffer with the email contents func BuildEmail(envelope imap.Envelope, body string, contentType ContentType) bytes.Buffer { if contentType == "" { contentType = ContentTypePlainText } var messageBuffer bytes.Buffer if envelope.From != nil && len(envelope.From) > 0 { messageBuffer.WriteString("From: " + envelope.From[0].Address() + "\r\n") } messageBuffer.WriteString("Date: " + envelope.Date.Format(time.RFC1123Z) + "\r\n") messageBuffer.WriteString("X-Mail-Created-Date: " + envelope.Date.Format(time.RFC1123Z) + "\r\n") messageBuffer.WriteString("Subject: " + envelope.Subject + "\r\n") messageBuffer.WriteString("Content-Type: " + string(contentType) + "\r\n") messageBuffer.WriteString("X-Uniform-Type-Identifier: com.apple.mail-note\r\n") messageBuffer.WriteString("\r\n") messageBuffer.WriteString(body) return messageBuffer } type Client struct { BaseClient *imapClient.Client } func (c *Client) Logout() error { if err := c.BaseClient.Logout(); err != nil { return fmt.Errorf("failed to logout: %w", err) } return nil } func (c *Client) SelectRW(folderName string) (*imap.MailboxStatus, error) { readOnly := false mbox, err := c.BaseClient.Select(folderName, readOnly) if err != nil { err = fmt.Errorf("failed slecting folder %s as read write: %w", folderName, err) } return mbox, err } func (c *Client) Select(folderName string) (*imap.MailboxStatus, error) { readOnly := true mbox, err := c.BaseClient.Select(folderName, readOnly) if err != nil { err = fmt.Errorf("failed slecting folder %s as read only: %w", folderName, err) } return mbox, err } func (c *Client) StoreNote(folderName string, envelope imap.Envelope, body string, contentType ContentType) error { b := BuildEmail(envelope, body, contentType) if err := c.BaseClient.Append(folderName, []string{}, time.Now(), &b); err != nil { return fmt.Errorf("failed to append new note to folder %s: %w", folderName, err) } return nil } func (c *Client) ListNoteFolders() ([]*NoteFolder, error) { mailboxes := make(chan *imap.MailboxInfo, maxChannels) done := make(chan error, 1) go func() { done <- c.BaseClient.List(baseNotesFolder, "*", mailboxes) }() results := []*NoteFolder{} for m := range mailboxes { results = append(results, NewNoteFolder(c, m.Name)) } if err := <-done; err != nil { return results, err } return results, nil } func (c *Client) GetNoteFolder(folderName string) (*NoteFolder, error) { mbox, err := c.Select(folderName) if err != nil { return nil, err } return NewNoteFolder(c, mbox.Name), nil } func (c Client) getMessages(seqset *imap.SeqSet) ([]*imap.Message, error) { messages := make(chan *imap.Message, maxChannels) done := make(chan error, 1) go func() { done <- c.BaseClient.Fetch( seqset, []imap.FetchItem{ imap.FetchEnvelope, imap.FetchUid, imap.FetchRFC822, }, messages, ) }() results := []*imap.Message{} for msg := range messages { results = append(results, msg) } err := <-done if err != nil { err = fmt.Errorf("failed to retrieve messages with seq %v from server: %w", seqset, err) } return results, err } // ListNotesInFolder returns a list of Notes in the provided folder func (c Client) ListNotesInFolder(folder *NoteFolder) ([]*Note, error) { mbox, err := folder.Select() if err != nil { return nil, err } slog.Debugf("mailbox %s has %d messages", folder.Name, mbox.Messages) seqset := new(imap.SeqSet) seqset.AddRange(1, mbox.Messages) slog.Debugf("Fetching notes %v from %s", seqset, folder.Name) messages, err := c.getMessages(seqset) if err != nil { return nil, fmt.Errorf("failed to list messages: %w", err) } results := []*Note{} for _, message := range messages { results = append(results, NewNote(folder, message)) } return results, nil } func (c Client) SearchNotes(folder *NoteFolder, criteria *imap.SearchCriteria) ([]*Note, error) { if _, err := folder.Select(); err != nil { return nil, err } ids, err := c.BaseClient.Search(criteria) if err != nil { return nil, fmt.Errorf("search on mail server failed: %w", err) } slog.Debugf("search result ids: %v", ids) if len(ids) == 0 { return []*Note{}, nil } seqset := new(imap.SeqSet) seqset.AddNum(ids...) messages, err := c.getMessages(seqset) if err != nil { return nil, fmt.Errorf("failed to get messages after search: %w", err) } results := []*Note{} for _, message := range messages { results = append(results, NewNote(folder, message)) } return results, nil } func (c Client) SetFlagForNote(note Note, flags []interface{}) error { if _, err := note.Folder.SelectRW(); err != nil { return err } seqset := new(imap.SeqSet) seqset.AddNum(note.message.Uid) slog.Debugf("seqset to set flags on message: %v to %v\n", seqset, flags) silent := true item := imap.FormatFlagsOp(imap.AddFlags, silent) messages := make(chan *imap.Message) if err := c.BaseClient.UidStore(seqset, item, flags, messages); err != nil { return fmt.Errorf("failed to set note flags: %w", err) } for message := range messages { slog.Debugf("Set flag on message: %v\n", message) } return nil } func (c Client) DeleteNote(note Note) error { flags := []interface{}{imap.DeletedFlag} err := c.SetFlagForNote(note, flags) if err != nil { return fmt.Errorf("failed flag note as deleted: %w", err) } // deletedItems := make(chan uint32) if err := c.BaseClient.Expunge(nil); err != nil { return fmt.Errorf("expunge deleted messages: %w", err) } /* Don't know why this doesn't return anything on the channel * for item := range deletedItems { * fmt.Println(item) * } */ return nil } func ConnectImap(hostname string, username string, password string) (*Client, error) { slog.Debugf("Connecting to server...") client, err := imapClient.DialTLS(hostname, nil) if err != nil { return nil, fmt.Errorf("failed to connect to IMAP server: %w", err) } slog.Debugf("Logging in...") err = client.Login(username, password) if err != nil { return nil, fmt.Errorf("failed to login: %w", err) } slog.Debugf("Logged in!") return &Client{client}, nil }