imap-notes/lib/client.go

271 lines
6.4 KiB
Go

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
}