Skip to content

Instantly share code, notes, and snippets.

@corpix
Created May 2, 2017 13:02
Show Gist options
  • Save corpix/fa9c53acf7d55f8188850c5b4239fea0 to your computer and use it in GitHub Desktop.
Save corpix/fa9c53acf7d55f8188850c5b4239fea0 to your computer and use it in GitHub Desktop.
package main
// XXX: Here will be a bunch of shitcoded solutions :)
import (
"crypto/sha1"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
"os"
"strings"
"github.com/PuerkitoBio/goquery"
"github.com/corpix/clistruct"
"github.com/davecgh/go-spew/spew"
"github.com/urfave/cli"
"gopkg.in/gomail.v2"
)
type Flags struct {
SMTP string `value:"smtp.gmail.com"`
SMTPPort int `name:"smtp-port" value:"587"`
Mime string `value:"text/html"`
Embed []string `usage:"A slice of attribute names with base64 encoded content to embed as inline attachments"`
From string
To string
Password string
Subject string
Body string
Attachment []string
Debug bool
}
var (
flags = &Flags{}
)
type embedding struct {
name string
id string
body gomail.FileSetting
headers gomail.FileSetting
}
func newEmbeddingFromByte(mime string, data []byte) (*embedding, error) {
var (
err error
)
h := sha1.New()
_, err = h.Write(data)
if err != nil {
return nil, err
}
id := hex.EncodeToString(h.Sum(nil))
t := strings.Split(mime, "/")
name := string(id) + `.` + t[len(t)-1]
return &embedding{
name: name,
id: id,
body: gomail.SetCopyFunc(
func(w io.Writer) error {
_, err := w.Write(data)
return err
},
),
headers: gomail.SetHeader(
map[string][]string{
"Content-Type": {mime + `; name="` + name + `"`},
"Content-ID": {id},
},
),
}, nil
}
func embedFromReader(message *gomail.Message, r io.Reader, attributes []string) ([]byte, error) {
d, err := goquery.NewDocumentFromReader(r)
if err != nil {
return nil, err
}
d.
Find("*").
Each(func(k int, s *goquery.Selection) {
var (
val string
exist bool
mimeEnd int
mime string
dataTypeEnd int
data []byte
embed *embedding
err error
)
for _, attribute := range attributes {
val, exist = s.Attr(attribute)
if !exist {
return
}
if !strings.HasPrefix(val, "data:") {
// Supports only `data:` atm.
fmt.Printf("Unsupported URI '%s', skipping\n", val)
return
}
val = val[5:]
mimeEnd = strings.IndexRune(val, ';')
if mimeEnd <= 0 {
panic(fmt.Errorf("Can not find mime type in '%s'", val))
}
mime = val[:mimeEnd]
val = val[mimeEnd+1:]
dataTypeEnd = strings.IndexRune(val, ',')
if dataTypeEnd <= 0 {
panic(fmt.Errorf("Can not find data type in '%s'", val))
}
if val[:dataTypeEnd] != "base64" {
panic(fmt.Errorf("Invalid data type '%s'", val[:dataTypeEnd]))
}
val = val[dataTypeEnd+1:]
data, err = base64.StdEncoding.DecodeString(val)
if err != nil {
panic(err)
}
embed, err = newEmbeddingFromByte(mime, data)
if err != nil {
panic(err)
}
message.Embed(embed.name, embed.body, embed.headers)
s.SetAttr(attribute, "cid:"+embed.id)
}
})
html, err := d.Html()
if err != nil {
return nil, err
}
return []byte(html), nil
}
func rootAction(flags *Flags, context *cli.Context) error {
var (
err error
body []byte
)
m := gomail.NewMessage()
m.SetHeader("From", flags.From)
m.SetHeader("To", flags.To)
m.SetHeader("Subject", flags.Subject)
r, err := os.Open(flags.Body)
if err != nil {
return err
}
defer r.Close()
body, err = embedFromReader(m, r, flags.Embed)
if err != nil {
return err
}
m.SetBody(flags.Mime, string(body))
for _, v := range flags.Attachment {
m.Attach(v)
}
d := gomail.NewDialer(
flags.SMTP,
flags.SMTPPort,
flags.From,
flags.Password,
)
err = d.DialAndSend(m)
if err != nil {
return err
}
fmt.Printf("Mail to '%s' has been sent\n", flags.To)
return nil
}
func before(flags *Flags, context *cli.Context) error {
if flags.Debug {
spew.Dump(flags)
}
return nil
}
func wrapWithFlags(fn func(*Flags, *cli.Context) error) func(*cli.Context) error {
return func(context *cli.Context) error {
flags := &Flags{}
err := clistruct.FlagsToStruct(context, flags)
if err != nil {
return err
}
return fn(flags, context)
}
}
func main() {
cliFlags, err := clistruct.FlagsFromStruct(flags)
if err != nil {
panic(err)
}
app := cli.NewApp()
app.Flags = cliFlags
app.Before = wrapWithFlags(before)
app.Action = wrapWithFlags(rootAction)
app.RunAndExitOnError()
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment