271 lines
6.4 KiB
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
|
|
}
|