Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add hash for unsubscribe option #12

Closed
wants to merge 12 commits into from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
### Hugo ###
/public/
/resources/_gen/
scripts/mail-scripts/Mercurius
7 changes: 2 additions & 5 deletions scripts/mail-scripts/emails.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
{
"subscribers": [
"[email protected]",
"[email protected]"
]
}
"subscribers": ["[email protected]"]
}
4 changes: 2 additions & 2 deletions scripts/mail-scripts/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.15

require (
github.com/yuin/goldmark v1.3.5
github.com/yuin/goldmark-meta v1.0.0 // indirect
github.com/yuin/goldmark-meta v1.0.0
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
)
137 changes: 111 additions & 26 deletions scripts/mail-scripts/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ package main

import (
"bytes"
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"os"
Expand All @@ -12,64 +18,143 @@ import (
"gopkg.in/gomail.v2"
)

// Struct to store subscribers list array
type subscriberList struct {
Subscribers []string `json:"subscribers"`
}

// Domain of cloudfare serverless endpoint
var domain string

func main() {

log.SetFlags(log.LstdFlags | log.Lshortfile)

domain = os.Getenv("DOMAIN_NAME")

// Init goldmark configuration
markdown := goldmark.New(
goldmark.WithExtensions(
meta.Meta,
),
)

jsonFile, err := os.Open("emails.json")

// Open emails file for reading emails (Will be replaced in future)
emailFile, err := os.Open("emails.json")
if err != nil {
log.Print(err)
}
defer emailFile.Close()

defer jsonFile.Close()
// Read emails from the file into a buffer
byteValue, _ := ioutil.ReadAll(emailFile)

byteValue, _ := ioutil.ReadAll(jsonFile)
// Transferring email data to a var
var subsList subscriberList
json.Unmarshal(byteValue, &subsList)

var list subscriberList
// Reading content to be sent
// TODO: Replace this with logic to read file containing the latest post
content, _ := ioutil.ReadFile("../../content/post/example.md")

json.Unmarshal(byteValue, &list)
// Convert the markdown into a compatible HTML format
var htmlContent bytes.Buffer
if err := markdown.Convert(content, &htmlContent); err != nil {
log.Fatalln(err)
}
// Convert to string for using and tweaking it everywhere
contentString := htmlContent.String()

content, _ := ioutil.ReadFile("../../content/post/example.md")
// Iterate over emails list and start sending
for _, email := range subsList.Subscribers {

var buf bytes.Buffer
// Get the unsubscribe hash string
completeContent := addUnsubscribeLink(contentString, email)

if err := markdown.Convert(content, &buf); err != nil {
panic(err)
// TODO here: Add image to the top of content if needed

// Send the email to the recipient
err := send(completeContent, email)
if err != nil {
log.Println("Error while sending mail to subscriber", email, "\nError : ", err)
}
}

log.Print(string(buf.String()))
}

// Utility to get the unsubscribe hash
func addUnsubscribeLink(contentString string, email string) string {

send(string(buf.String()), list.Subscribers)
// Get the encrypted hash to be sent
unsubString := encryptUnsubscribeString(email)

// Prepare the HTML template of the unsubscribe option (raw as of now)
unSubscribeTemplate := fmt.Sprintf("<a href=\"%s/unsubscribe?uniqhash=%s\">UnSubscribe</a>", domain, unsubString)

// Append the hash to the email body
newContent := fmt.Sprintf("%s\n\n%s", contentString, unSubscribeTemplate)

return newContent
}

func send(body string, to []string) {
// Helper function to above to encrypt user email in order to find
// unsubscribe link hash
func encryptUnsubscribeString(email string) string {

// THE EMAIL_ENC_KEY is the base64 encoded string of a 32 byte character string (I hope its right!)
encKey := os.Getenv("EMAIL_ENC_KEY")

// Decode the key to bytes from base64
key, err := base64.StdEncoding.DecodeString(encKey)
if err != nil {
log.Fatalln("Error in decoding key :", err)
}

// convert the string to encrypt to bytes
byteSecret := []byte(email)
block, err := aes.NewCipher(key)
if err != nil {
panic(err.Error())
}

// Creating a new GCM
aesGCM, err := cipher.NewGCM(block)
if err != nil {
panic(err.Error())
}

// Creating a nonce. Nonce should be from GCM
nonce := make([]byte, aesGCM.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
panic(err.Error())
}

ciphertext := aesGCM.Seal(nonce, nonce, byteSecret, nil)
return base64.StdEncoding.EncodeToString(ciphertext)
}

// Utility to send emails using gomail
func send(body string, to string) error {
from := os.Getenv("MAIL_ID")
pass := os.Getenv("MAIL_PASSWORD")

d := gomail.NewDialer("smtp.gmail.com", 587, from, pass)
s, err := d.Dial()
dialer := gomail.NewDialer("smtp.gmail.com", 587, from, pass)
s, err := dialer.Dial()
if err != nil {
panic(err)
log.Println(err)
return err
}

m := gomail.NewMessage()
for _, r := range to {
m.SetHeader("From", from)
m.SetAddressHeader("To", r, r)
m.SetHeader("Subject", "Newsletter Test")
m.SetBody("text/html", body)

if err := gomail.Send(s, m); err != nil {
log.Printf("Could not send email to %q: %v", r, err)
}
m.Reset()
m.SetHeader("From", from)
m.SetAddressHeader("To", to, to)
m.SetHeader("Subject", "Newsletter Test")
m.SetBody("text/html", body)

if err := gomail.Send(s, m); err != nil {
log.Printf("Could not send email to %q: %v", body, err)
return err
}
m.Reset()
return nil
}
11 changes: 11 additions & 0 deletions scripts/workers/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/target
/dist
**/*.rs.bk
Cargo.lock
bin/
pkg/
wasm-pack.log
worker/
node_modules/
.cargo-ok
wrangler.toml
15 changes: 15 additions & 0 deletions scripts/workers/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# 👷 `worker-template` Hello World

A template for kick starting a Cloudflare worker project.

[`index.js`](https://github.com/cloudflare/worker-template/blob/master/index.js) is the content of the Workers script.

#### Wrangler

To generate using [wrangler](https://github.com/cloudflare/wrangler)

```
wrangler generate projectname https://github.com/cloudflare/worker-template
```

Further documentation for Wrangler can be found [here](https://developers.cloudflare.com/workers/tooling/wrangler).
32 changes: 32 additions & 0 deletions scripts/workers/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
addEventListener("fetch", (event) => {
event.respondWith(handleRequest(event.request));
});

async function handleRequest(request) {
let pathname = new URL(request.url).pathname;
if (pathname === "/subscribe") {
//TODO check HTTP Request type
try {
const body = await request.text();
let date = Date.now();
//TODO perform email validation
//TODO check if email already exists
//TODO send a confirmation email before actually adding to DB
await NL_EMAIL.put(body, date);
return new Response("Subscribed", { status: 201 });
} catch (error) {
return new Response(error || "error", { status: 400 });
}
} else if (pathname === "/unsubscribe") {
try {
const body = await request.text();
//TODO maybe use encoded request and decode it here
await NL_EMAIL.delete(body);
return new Response("Unubscribed", { status: 204 });
} catch (error) {
return new Response(error || "error", { status: 400 });
}
}
const value = await NL_EMAIL.list();
return new Response(JSON.stringify(value.keys));
}
12 changes: 12 additions & 0 deletions scripts/workers/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions scripts/workers/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"private": true,
"name": "workers",
"version": "1.0.0",
"description": "A template for kick starting a Cloudflare Workers project",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "daemon1024 <[email protected]>",
"license": "MIT"
}
5 changes: 5 additions & 0 deletions scripts/workers/wrangler.sample.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
name = "workers"
type = "javascript"
account_id = ""
workers_dev = true
kv_namespaces = [ { binding = "NL_EMAIL", preview_id = "", id = "" }]