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, } }