From ff4f709486a69bc1650db73a003255e58cae0532 Mon Sep 17 00:00:00 2001 From: René 'Necoro' Neumann Date: Sat, 2 May 2020 02:06:56 +0200 Subject: Embedding images in mail --- internal/feed/feed.go | 17 ++++ internal/feed/mail.go | 184 +++++++++++++++++++++++++++++++++---- internal/feed/template/feed.tpl.go | 10 +- 3 files changed, 184 insertions(+), 27 deletions(-) (limited to 'internal') diff --git a/internal/feed/feed.go b/internal/feed/feed.go index ddc91ed..1a6ef97 100644 --- a/internal/feed/feed.go +++ b/internal/feed/feed.go @@ -23,11 +23,18 @@ type feedDescriptor struct { Url string } +type feedImage struct { + image []byte + mime string +} + type feeditem struct { *gofeed.Feed *gofeed.Item + Body string updateOnly bool reasons []string + images []feedImage } // Creator returns the name of the creating author. @@ -45,6 +52,16 @@ func (item *feeditem) addReason(reason string) { } } +func (item *feeditem) addImage(img []byte, mime string) int { + i := feedImage{img, mime} + item.images = append(item.images, i) + return len(item.images) +} + +func (item *feeditem) clearImages() { + item.images = []feedImage{} +} + func (feed *Feed) descriptor() feedDescriptor { return feedDescriptor{ Name: feed.Name, diff --git a/internal/feed/mail.go b/internal/feed/mail.go index 620b444..5f543e4 100644 --- a/internal/feed/mail.go +++ b/internal/feed/mail.go @@ -2,15 +2,22 @@ package feed import ( "bytes" + "encoding/base64" "fmt" "io" + "io/ioutil" + "mime" + "path" "strings" "time" + "github.com/PuerkitoBio/goquery" + "github.com/emersion/go-message" "github.com/emersion/go-message/mail" "github.com/Necoro/feed2imap-go/internal/feed/template" "github.com/Necoro/feed2imap-go/pkg/config" + "github.com/Necoro/feed2imap-go/pkg/log" ) func address(name, address string) []*mail.Address { @@ -36,8 +43,9 @@ func writeHtml(writer io.Writer, item feeditem) error { return template.Feed.Execute(writer, item) } -func writeToBuffer(b *bytes.Buffer, feed *Feed, item feeditem, cfg *config.Config) error { +func buildHeader(feed *Feed, item feeditem, cfg *config.Config) message.Header { var h mail.Header + h.SetContentType("multipart/alternative", nil) h.SetAddressList("From", fromAdress(feed, item, cfg)) h.SetAddressList("To", address(feed.Name, cfg.DefaultEmail)) h.Add("X-Feed2Imap-Version", config.Version()) @@ -62,39 +70,84 @@ func writeToBuffer(b *bytes.Buffer, feed *Feed, item feeditem, cfg *config.Confi h.SetSubject(subject) } - tw, err := mail.CreateInlineWriter(b, h) + return h.Header +} + +func writeHtmlPart(w *message.Writer, item feeditem) error { + var ih message.Header + ih.SetContentType("text/html", map[string]string{"charset": "utf-8"}) + ih.SetContentDisposition("inline", nil) + ih.Set("Content-Transfer-Encoding", "8bit") + + partW, err := w.CreatePart(ih) if err != nil { return err } - defer tw.Close() + defer partW.Close() - if false /* cfg.WithPartText() */ { - var th mail.InlineHeader - th.SetContentType("text/plain", map[string]string{"charset": "utf-8", "format": "flowed"}) + if err = writeHtml(w, item); err != nil { + return fmt.Errorf("writing html part: %w", err) + } - w, err := tw.CreatePart(th) - if err != nil { - return err - } - defer w.Close() + return nil +} + +func writeImagePart(w *message.Writer, img feedImage, cid string) error { + var ih message.Header + ih.SetContentType(img.mime, nil) + ih.SetContentDisposition("inline", nil) + ih.Set("Content-Transfer-Encoding", "base64") + ih.SetText("Content-ID", fmt.Sprintf("<%s>", cid)) + + imgW, err := w.CreatePart(ih) + if err != nil { + return err + } + defer imgW.Close() - _, _ = io.WriteString(w, "Who are you?") + if _, err = imgW.Write(img.image); err != nil { + return err } + return nil +} + +func writeToBuffer(b *bytes.Buffer, feed *Feed, item feeditem, cfg *config.Config) error { + h := buildHeader(feed, item, cfg) + + writer, err := message.CreateWriter(b, h) + if err != nil { + return err + } + defer writer.Close() + if cfg.WithPartHtml() { - var th mail.InlineHeader - th.SetContentType("text/html", map[string]string{"charset": "utf-8"}) + feed.buildBody(&item) - w, err := tw.CreatePart(th) - if err != nil { + var relWriter *message.Writer + if len(item.images) > 0 { + var rh message.Header + rh.SetContentType("multipart/related", map[string]string{"type": "text/html"}) + if relWriter, err = writer.CreatePart(rh); err != nil { + return err + } + defer relWriter.Close() + } else { + relWriter = writer + } + + if err = writeHtmlPart(relWriter, item); err != nil { return err } - if err = writeHtml(w, item); err != nil { - return fmt.Errorf("writing html part: %w", err) + for idx, img := range item.images { + cid := cidNr(idx + 1) + if err = writeImagePart(relWriter, img, cid); err != nil { + return err + } } - w.Close() + item.clearImages() // safe memory } return nil @@ -122,3 +175,96 @@ func (feed *Feed) ToMails(cfg *config.Config) ([]string, error) { } return mails, nil } + +func getImage(src string) ([]byte, string) { + resp, err := stdHTTPClient.Get(src) + if err != nil { + log.Errorf("Error fetching from '%s': %s", src, err) + return nil, "" + } + defer resp.Body.Close() + + img, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Errorf("Error reading body from '%s': %s", src, err) + return nil, "" + } + + ext := path.Ext(src) + if ext == "" { + log.Warnf("Cannot determine extension from '%s', skipping.", src) + return nil, "" + } + + mime := mime.TypeByExtension(ext) + return img, mime +} + +func cidNr(idx int) string { + return fmt.Sprintf("cid_%d", idx) +} + +func (feed *Feed) buildBody(item *feeditem) { + var body string + var comment string + + if item.Item.Content != "" { + comment = "\n" + body = item.Item.Content + } else if item.Item.Description != "" { + comment = "\n" + body = item.Item.Description + } + + if !feed.InclImages { + item.Body = comment + body + return + } + + doc, err := goquery.NewDocumentFromReader(strings.NewReader(body)) + if err != nil { + log.Debugf("Feed %s: Error while parsing html content: %s", feed.Name, err) + if body != "" { + item.Body = "
" + comment + body + } + return + } + + doneAnything := true + nodes := doc.Find("img") + nodes.Each(func(i int, selection *goquery.Selection) { + const attr = "src" + + src, ok := selection.Attr(attr) + if !ok { + return + } + + img, mime := getImage(src) + if img == nil { + return + } + + if feed.EmbedImages { + imgStr := "data:" + mime + ";base64," + base64.StdEncoding.EncodeToString(img) + selection.SetAttr(attr, imgStr) + } else { + idx := item.addImage(img, mime) + cid := "cid:" + cidNr(idx) + selection.SetAttr(attr, cid) + } + doneAnything = true + }) + + if doneAnything { + html, err := doc.Find("body").Html() + if err != nil { + item.clearImages() + log.Errorf("Error during rendering HTML, skipping.") + } else { + body = html + } + } + + item.Body = comment + body +} diff --git a/internal/feed/template/feed.tpl.go b/internal/feed/template/feed.tpl.go index 8aab9d7..0e09180 100644 --- a/internal/feed/template/feed.tpl.go +++ b/internal/feed/template/feed.tpl.go @@ -40,14 +40,8 @@ const feedTpl = `{{- /*gotype:github.com/Necoro/feed2imap-go/internal/feed.feedi -{{with .Item.Content}} -
+{{with .Body}} {{html .}} -{{else}} -{{with .Item.Description}} -
- {{html .}} -{{end}} {{end}} {{with .Item.Enclosures}} @@ -59,7 +53,7 @@ const feedTpl = `{{- /*gotype:github.com/Necoro/feed2imap-go/internal/feed.feedi {{end}} -- cgit v1.2.3-54-g00ecf
    - {{.URL | lastUrlPart}} ({{.Length | byteCount}}, {{.Type}}) + {{.URL | lastUrlPart}} ({{with .Length}}{{. | byteCount}}, {{end}}{{.Type}})