Skip to content

Commit

Permalink
Implement subscription via email
Browse files Browse the repository at this point in the history
  • Loading branch information
karashiiro committed May 7, 2022
1 parent 639f24e commit bf5690b
Show file tree
Hide file tree
Showing 11 changed files with 264 additions and 37 deletions.
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ FROM alpine:latest
RUN apk --no-cache add ca-certificates
COPY --from=builder /go/bin/app /app
COPY ./sql ./sql
COPY email-template.gohtml .
COPY ./templates ./templates
ENTRYPOINT /app
LABEL Name=operator Version=1.0.0
15 changes: 12 additions & 3 deletions cmd/operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"github.com/jackc/pgx"
"github.com/karashiiro/operator/pkg/db"
"github.com/karashiiro/operator/pkg/jobs"
"github.com/microcosm-cc/bluemonday"
"github.com/reugn/go-quartz/quartz"
)

Expand Down Expand Up @@ -64,9 +65,17 @@ func main() {
sched.Start()

// Schedule the report job
trigger := quartz.NewSimpleTrigger(2 * time.Minute)
job := jobs.ReportJob{Pool: pool}
sched.ScheduleJob(&job, trigger)
reportTrigger := quartz.NewSimpleTrigger(2 * time.Minute)
reportJob := jobs.ReportJob{Pool: pool}
sched.ScheduleJob(&reportJob, reportTrigger)

// Schedule the email-checking job
receiveTrigger := quartz.NewSimpleTrigger(time.Minute)
receiveJob := jobs.ReceiveEmailsJob{
Pool: pool,
Policy: bluemonday.UGCPolicy(),
}
sched.ScheduleJob(&receiveJob, receiveTrigger)

// Block until SIGINT or SIGTERM is received
sigs := make(chan os.Signal, 1)
Expand Down
2 changes: 2 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ services:
OPERATOR_EMAIL: ${OPERATOR_EMAIL}
OPERATOR_PASSWORD: ${OPERATOR_PASSWORD}
OPERATOR_SMTP_SERVER: ${OPERATOR_SMTP_SERVER}
OPERATOR_IMAP_SERVER: ${OPERATOR_IMAP_SERVER}
OPERATOR_INBOX: ${OPERATOR_INBOX}
OPERATOR_POSTGRES: postgres
depends_on:
- postgres
27 changes: 21 additions & 6 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,30 @@ module github.com/karashiiro/operator

go 1.18

require github.com/reugn/go-quartz v0.3.9
replace github.com/mxk/go-imap => github.com/glennzw/go-imap v0.0.0-20200213170711-35ad56e460d4

require (
github.com/jackc/pgx v3.6.2+incompatible // indirect
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible // indirect
github.com/jackc/pgx v3.6.2+incompatible
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible
github.com/jprobinson/eazye v0.0.0-20200316195029-00167c745a93
github.com/microcosm-cc/bluemonday v1.0.18
github.com/reugn/go-quartz v0.3.9
)

require (
github.com/aymerick/douceur v0.2.0 // indirect
github.com/cockroachdb/apd v1.1.0 // indirect
github.com/gofrs/uuid v4.2.0+incompatible // indirect
github.com/gorilla/css v1.0.0 // indirect
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 // indirect
github.com/lib/pq v1.10.5 // indirect
github.com/mxk/go-imap v0.0.0-20150429134902-531c36c3f12d // indirect
github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c // indirect
github.com/pkg/errors v0.9.1 // indirect
golang.org/x/text v0.3.3 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df // indirect
github.com/shopspring/decimal v1.3.1 // indirect
github.com/sloonz/go-qprintable v0.0.0-20210417175225-715103f9e6eb // indirect
golang.org/x/net v0.0.0-20210614182718-04defd469f4e // indirect
golang.org/x/text v0.3.6 // indirect
)

require (
Expand Down
39 changes: 33 additions & 6 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,24 +1,51 @@
github.com/aymerick/douceur v0.2.0 h1:Mv+mAeH1Q+n9Fr+oyamOlAkUNPWPlA8PPGR0QAaYuPk=
github.com/aymerick/douceur v0.2.0/go.mod h1:wlT5vV2O3h55X9m7iVYN0TBM0NH/MmbLnd30/FjWUq4=
github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I=
github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ=
github.com/glennzw/go-imap v0.0.0-20200213170711-35ad56e460d4 h1:pQAqfr44gthAWySJTn3ND5+Ho3yJjlFeUIXaNBQCW/8=
github.com/glennzw/go-imap v0.0.0-20200213170711-35ad56e460d4/go.mod h1:PmwW3hrodDWXh1ZW9S/NsHYFI0USpChBw9yZ/CBeWwA=
github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0=
github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg=
github.com/google/go-github/v44 v44.0.1-0.20220502191311-417479672f91 h1:07IRiIaXrkBvpln6reaLXjWorQTQ2PvwiSNgW4YwJeA=
github.com/google/go-github/v44 v44.0.1-0.20220502191311-417479672f91/go.mod h1:iWn00mWcP6PRWHhXm0zuFJ8wbEjE5AGO5D5HXYM4zgw=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/gorilla/css v1.0.0 h1:BQqNyPTi50JCFMTw/b67hByjMVXZRwGha6wxVGkeihY=
github.com/gorilla/css v1.0.0/go.mod h1:Dn721qIggHpt4+EFCcTLTU/vk5ySda2ReITrtgBl60c=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733 h1:vr3AYkKovP8uR8AvSGGUK1IDqRa5lAAvEkZG1LKaCRc=
github.com/jackc/fake v0.0.0-20150926172116-812a484cc733/go.mod h1:WrMFNQdiFJ80sQsxDoMokWK1W5TQtxBFNpzWTD84ibQ=
github.com/jackc/pgx v3.6.2+incompatible h1:2zP5OD7kiyR3xzRYMhOcXVvkDZsImVXfj+yIyTQf3/o=
github.com/jackc/pgx v3.6.2+incompatible/go.mod h1:0ZGrqGqkRlliWnWB4zKnWtjbSWbGkVEFm4TeybAXq+I=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible h1:jdpOPRN1zP63Td1hDQbZW73xKmzDvZHzVdNYxhnTMDA=
github.com/jordan-wright/email v4.0.1-0.20210109023952-943e75fe5223+incompatible/go.mod h1:1c7szIrayyPPB/987hsnvNzLushdWf4o/79s3P08L8A=
github.com/jprobinson/eazye v0.0.0-20200316195029-00167c745a93 h1:o/YCOvH76r/so3yha6jmtDDbI0nDizdwnZXNeQ+4hME=
github.com/jprobinson/eazye v0.0.0-20200316195029-00167c745a93/go.mod h1:PrbzpJpOHK6YA5SptY8xl6RxWRZ2YGTDvVFzP/hjN4I=
github.com/lib/pq v1.10.5 h1:J+gdV2cUmX7ZqL2B0lFcW0m+egaHC2V3lpO8nWxyYiQ=
github.com/lib/pq v1.10.5/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
github.com/microcosm-cc/bluemonday v1.0.18 h1:6HcxvXDAi3ARt3slx6nTesbvorIc3QeTzBNRvWktHBo=
github.com/microcosm-cc/bluemonday v1.0.18/go.mod h1:Z0r70sCuXHig8YpBzCc5eGHAap2K7e/u082ZUpDRRqM=
github.com/mxk/go-imap v0.0.0-20150429134902-531c36c3f12d h1:+DgqA2tuWi/8VU+gVgBAa7+WZrnFbPKhQWbKBB54cVs=
github.com/mxk/go-imap v0.0.0-20150429134902-531c36c3f12d/go.mod h1:xacC5qXZnL/ooiitVoe3BtI1OotFTqi5zICBs9J5Fyk=
github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c h1:P6XGcuPTigoHf4TSu+3D/7QOQ1MbL6alNwrGhcW7sKw=
github.com/paulrosania/go-charset v0.0.0-20190326053356-55c9d7a5834c/go.mod h1:YnNlZP7l4MhyGQ4CBRwv6ohZTPrUJJZtEv4ZgADkbs4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/reugn/go-quartz v0.3.9 h1:0peG19P1obG+Yx4bFrHTRUWLO9BOkg24W97soIaZ9zo=
github.com/reugn/go-quartz v0.3.9/go.mod h1:Tf7ynScQD/H5V02OKi0sux5d95tZJQmdJWwdDsVOx1M=
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
github.com/sloonz/go-qprintable v0.0.0-20210417175225-715103f9e6eb h1:T+USeSgAg9MysHPeOQ2W3KAuBQHVZzG0XMHyfHN88Yg=
github.com/sloonz/go-qprintable v0.0.0-20210417175225-715103f9e6eb/go.mod h1:WKd1iQMtoZdaS9rlKDPprxWJoan2hkQA9BcGt+oxezs=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5 h1:HWj/xjIHfjYU5nVXpTM0s39J9CbLn7Cc5a7IC5rwsMQ=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc h1:2gGKlE2+asNV9m7xrywl36YYNnBG5ZQ0r/BOOxqPpmk=
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc/go.mod h1:m7x9LTH6d71AHyAX77c9yqWCCa3UKHcVEj9y7hAtKDk=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df h1:n7WqCuqOuCbNr617RXOY0AWRXxgwEyPp2z+p0+hgMuE=
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df/go.mod h1:LRQQ+SO6ZHR7tOkpBDuZnXENFzX8qRjMDMyPD6BRkCw=
165 changes: 165 additions & 0 deletions pkg/jobs/receive-emails.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
package jobs

import (
"bytes"
"fmt"
"hash/fnv"
"io"
"log"
"os"
"regexp"
"strings"
"text/template"
"time"

"github.com/jackc/pgx"
"github.com/jprobinson/eazye"
"github.com/karashiiro/operator/pkg/outlook"
"github.com/microcosm-cc/bluemonday"
)

var githubPattern = regexp.MustCompile(`(?i)github:\s*(?P<github>\S*)`)
var intervalPattern = regexp.MustCompile(`(?i)interval:\s*(?P<interval>\S*)`)

type ReceiveEmailsJob struct {
Pool *pgx.ConnPool
Policy *bluemonday.Policy
}

type newReader struct {
Email string
GitHub string
ReportInterval time.Duration
}

func (j *ReceiveEmailsJob) Execute() {
auth := eazye.MailboxInfo{
Host: os.Getenv("OPERATOR_IMAP_SERVER"),
TLS: true,
User: os.Getenv("OPERATOR_EMAIL"),
Pwd: os.Getenv("OPERATOR_PASSWORD"),
Folder: os.Getenv("OPERATOR_INBOX"),
}

emails, err := eazye.GetUnread(auth, true, false)
if err != nil {
log.Printf("Failed to get incoming emails: %v\n", err)
}

newReaders := make([]*newReader, 0)
for _, email := range emails {
// Parse out the email information
subjectCleaned := strings.TrimSpace(email.Subject)
if strings.HasPrefix(subjectCleaned, "[op] subscribe") {
r, err := j.subscribe(email)
if err != nil {
log.Printf("Failed to parse subscription email: %v\n", err)
}

newReaders = append(newReaders, r)
}
}

// Save new readers to the database
if len(newReaders) > 0 {
readerConn, err := j.Pool.Acquire()
if err != nil {
log.Printf("Failed to acquire database connection: %v\n", err)
return
}
defer j.Pool.Release(readerConn)

for _, r := range newReaders {
_, err := storeReader(readerConn, r)
if err != nil {
log.Printf("Failed to add new reader: %v\n", err)
}

log.Printf("Sending subscription confirmation email to %s\n", r.Email)

var subscribeMessage bytes.Buffer
buildSubscriptionTemplate(&subscribeMessage, r.ReportInterval)

err = outlook.SendEmail(r.Email, "Subscription confirmed", subscribeMessage.String())
if err != nil {
log.Printf("Unable to send mail: %v\n", err)
continue
}

log.Printf("Added reader %s\n", r.Email)
}
}
}

func (j *ReceiveEmailsJob) Description() string {
return "ReceiveEmailsJob"
}

func (j *ReceiveEmailsJob) Key() int {
h := fnv.New32a()
h.Write([]byte(j.Description()))
return int(h.Sum32())
}

func (j *ReceiveEmailsJob) subscribe(email eazye.Email) (*newReader, error) {
r := &newReader{
Email: j.Policy.Sanitize(email.From.Address),
}

bodyLines := strings.Split(string(email.Text), "\n")
for _, line := range bodyLines {
lineCleaned := strings.TrimSpace(line)

// Parse their optionally-provided GitHub username
githubMatches := githubPattern.FindStringSubmatch(lineCleaned)
if len(githubMatches) != 0 {
github := githubMatches[githubPattern.SubexpIndex("github")]
r.GitHub = j.Policy.Sanitize(github)
continue
}

// Parse their requested reporting interval
intervalMatches := intervalPattern.FindStringSubmatch(lineCleaned)
if len(intervalMatches) != 0 {
interval, err := time.ParseDuration(intervalMatches[intervalPattern.SubexpIndex("interval")])
if err == nil {
r.ReportInterval = interval
continue
}
}
}

// Validate reporting interval
if r.ReportInterval.Minutes() <= 0 {
return nil, fmt.Errorf("user attempted to set a reporting interval of 0 or less")
}

return r, nil
}

func buildSubscriptionTemplate(w io.Writer, interval time.Duration) error {
t, err := template.ParseFiles("./templates/subscribe-template.gohtml")
if err != nil {
return err
}

err = t.Execute(w, struct {
Interval time.Duration
}{
Interval: interval,
})
if err != nil {
return err
}

return nil
}

func storeReader(conn *pgx.Conn, r *newReader) (int64, error) {
t, err := conn.Exec(`
INSERT INTO Reader (email, github, report_interval, active)
VALUES
($1, $2, $3, TRUE)
`, r.Email, r.GitHub, r.ReportInterval)
return t.RowsAffected(), err
}
21 changes: 1 addition & 20 deletions pkg/jobs/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,12 @@ package jobs
import (
"bytes"
"context"
"fmt"
"hash/fnv"
"log"
"os"
"time"

"github.com/google/go-github/v44/github"
"github.com/jackc/pgx"
"github.com/jordan-wright/email"
"github.com/karashiiro/operator/pkg/outlook"
"github.com/karashiiro/operator/pkg/pretty"
)
Expand Down Expand Up @@ -133,7 +130,7 @@ func (j *ReportJob) Execute() {
}

log.Printf("Sending email to %s\n", readerEmail)
err = sendEmail(readerEmail, "Updated Dalamud Plugin Pull Requests", readerMessage.String())
err = outlook.SendEmail(readerEmail, "Updated Dalamud Plugin Pull Requests", readerMessage.String())
if err != nil {
log.Printf("Unable to send mail: %v\n", err)
continue
Expand Down Expand Up @@ -192,19 +189,3 @@ func storeReportLogSkipped(conn *pgx.Conn, readerId int) (int64, error) {
`, readerId)
return tag.RowsAffected(), err
}

func sendEmail(to, subject, body string) error {
auth := outlook.LoginAuth(os.Getenv("OPERATOR_EMAIL"), os.Getenv("OPERATOR_PASSWORD"))
e := email.NewEmail()
e.To = []string{to}
e.From = fmt.Sprintf("Caprine Operator <%s>", os.Getenv("OPERATOR_EMAIL"))
e.Subject = subject
e.HTML = []byte(body)

err := e.Send(os.Getenv("OPERATOR_SMTP_SERVER"), auth)
if err != nil {
return err
}

return nil
}
24 changes: 24 additions & 0 deletions pkg/outlook/send-email.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package outlook

import (
"fmt"
"os"

"github.com/jordan-wright/email"
)

func SendEmail(to, subject, body string) error {
auth := LoginAuth(os.Getenv("OPERATOR_EMAIL"), os.Getenv("OPERATOR_PASSWORD"))
e := email.NewEmail()
e.To = []string{to}
e.From = fmt.Sprintf("Caprine Operator <%s>", os.Getenv("OPERATOR_EMAIL"))
e.Subject = subject
e.HTML = []byte(body)

err := e.Send(os.Getenv("OPERATOR_SMTP_SERVER"), auth)
if err != nil {
return err
}

return nil
}
2 changes: 1 addition & 1 deletion pkg/pretty/pretty.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ type Plogon struct {
}

func BuildTemplate(w io.Writer, plogons []*Plogon) error {
t, err := template.ParseFiles("email-template.gohtml")
t, err := template.ParseFiles("./templates/email-template.gohtml")
if err != nil {
return err
}
Expand Down
File renamed without changes.
4 changes: 4 additions & 0 deletions templates/subscribe-template.gohtml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<p>
You are subscribed to operator updates! If any updates have occurred, you will be emailed
within {{.Interval}}.
</p>

0 comments on commit bf5690b

Please sign in to comment.