imap-notes/lib/notes.go

243 lines
5.3 KiB
Go

package lib
import (
"errors"
"fmt"
"io/ioutil"
"net/mail"
"strings"
"time"
md "github.com/JohannesKaufmann/html-to-markdown"
"github.com/emersion/go-imap"
"github.com/russross/blackfriday/v2"
)
var (
ErrNoMessageBody = errors.New("server did not provide a message body")
)
// Note represents an IMAP note.
type Note struct {
message *imap.Message
Folder *NoteFolder
deleted bool
}
// Deleted will indicate if a Note has been deleted
func (n Note) Deleted() bool {
return n.deleted
}
// ReadMarkdown will return the contents of a Note in Markdown format
func (n Note) ReadMarkdown() (string, error) {
body, err := n.ReadText()
if err != nil {
return "", err
}
// Convert to to markdown from html
urlDomain := ""
enableCommonmark := true
converter := md.NewConverter(urlDomain, enableCommonmark, nil)
body, err = converter.ConvertString(body)
if err != nil {
return "", fmt.Errorf("failed converting to markdown: %w", err)
}
return body, nil
}
// OpenMarkdownInEditor opens the Markdown converted contents of a Note in an editor
func (n Note) OpenMarkdownInEditor() (string, error) {
body, err := n.ReadMarkdown()
if err != nil {
return "", err
}
return OpenInEditor(body)
}
// ReadText reads text contents of note from IMAP server.
func (n Note) ReadText() (string, error) {
messages := make(chan *imap.Message, 1)
done := make(chan error, 1)
section := &imap.BodySectionName{}
seqset := new(imap.SeqSet)
seqset.AddNum(n.message.Uid)
go func() {
done <- n.Folder.client.BaseClient.UidFetch(
seqset,
[]imap.FetchItem{section.FetchItem(), imap.FetchUid},
messages,
)
}()
msg := <-messages
result := msg.GetBody(section)
if result == nil {
return "", ErrNoMessageBody
}
if err := <-done; err != nil {
return "", fmt.Errorf("failed getting message from server: %w", err)
}
m, err := mail.ReadMessage(result)
if err != nil {
return "", fmt.Errorf("failed reading message received from server: %w", err)
}
body, err := ioutil.ReadAll(m.Body)
if err != nil {
return "", fmt.Errorf("failed reading message body from server message: %w", err)
}
return string(body), nil
}
// OpenInEditor opens the note in an external editor.
func (n Note) OpenInEditor() (string, error) {
body, err := n.ReadText()
if err != nil {
return "", err
}
return OpenInEditor(body)
}
func (n Note) Delete() error {
return n.Folder.client.DeleteNote(n)
}
func (n Note) OpenPager() error {
body, err := n.ReadText()
if err != nil {
return err
}
return OpenInPager(body)
}
func (n Note) OpenMarkdownInPager() error {
body, err := n.ReadMarkdown()
if err != nil {
return err
}
return OpenInPager(body)
}
func (n Note) EditMarkdown() error {
// TODO: Handle an edit where nothing changes
body, err := n.OpenMarkdownInEditor()
if err != nil {
return err
}
// Get first line as subject
subject := strings.Split(body, "\n")[0]
subject = strings.TrimLeft(subject, "#")
subject = strings.Trim(subject, " ")
// Convert Markdown to HTML
body = string(blackfriday.Run([]byte(body)))
// Save updated content as a new note
err = n.Folder.StoreNote(subject, body, ContentTypeHtml)
if err != nil {
return err
}
// Delete existing (this) note
err = n.Delete()
if err != nil {
return fmt.Errorf("failed to delete note after edit: %w", err)
}
// Mark this as deleted so it is known to not reuse
n.deleted = true
return nil
}
func (n Note) Name() string {
return n.message.Envelope.Subject
}
/*
* func (n Note) Preview() string {
* m.GetBody()
* }
*/
// NewNote creates a new note from an imap message.
func NewNote(folder *NoteFolder, message *imap.Message) *Note {
// TODO: should this be private? See NewNoteFolder
return &Note{
message: message,
Folder: folder,
deleted: false,
}
}
// NoteFolder represents a particular IMAP folder holding notes.
type NoteFolder struct {
client *Client
Name string
}
// ListNotes returns all notes in the folder.
func (nf *NoteFolder) ListNotes() (notes []*Note, err error) {
// TODO: Accept a limit parameter
messages, err := nf.client.ListNotesInFolder(nf)
if err != nil {
return
}
notes = append(notes, messages...)
return
}
// Select selects this folder in Read Only mode.
func (nf *NoteFolder) Select() (*imap.MailboxStatus, error) {
return nf.client.Select(nf.Name)
}
// SelectRW selects this folder in Read Write mode.
func (nf *NoteFolder) SelectRW() (*imap.MailboxStatus, error) {
return nf.client.SelectRW(nf.Name)
}
// StoreNote stores a note with provided content to the folder.
func (nf NoteFolder) StoreNote(subject string, body string, contentType ContentType) error {
// TODO: Maybe refactor to accept only body and extract subject/title here
envelope := imap.Envelope{
Subject: subject,
Date: time.Now(),
}
return nf.client.StoreNote(nf.Name, envelope, body, contentType)
}
// FindNoteByName searches for a Note using it's name and returns that note
func (nf *NoteFolder) FindNotesByName(noteName string) ([]*Note, error) {
criteria := imap.NewSearchCriteria()
criteria.Header.Add("SUBJECT", noteName)
return nf.client.SearchNotes(nf, criteria)
}
// NewNoteFolder creates a NoteFolder for a given folder name.
func NewNoteFolder(client *Client, name string) *NoteFolder {
// TODO: Should this (or anything that requires passing something like client) be private?
return &NoteFolder{
client, name,
}
}