diff --git a/cmd/operator/main.go b/cmd/operator/main.go index e2bd351..ce3fca8 100644 --- a/cmd/operator/main.go +++ b/cmd/operator/main.go @@ -72,7 +72,7 @@ func main() { sched.ScheduleJob(&reportJob, reportTrigger) // Schedule the email-checking job - receiveTrigger := quartz.NewSimpleTrigger(time.Minute) + receiveTrigger := quartz.NewSimpleTrigger(5 * time.Second) receiveJob := inbox.ReceiveEmailsJob{ Pool: pool, Policy: bluemonday.UGCPolicy(), diff --git a/pkg/inbox/parse-body.go b/pkg/inbox/parse-body.go new file mode 100644 index 0000000..e03e05a --- /dev/null +++ b/pkg/inbox/parse-body.go @@ -0,0 +1,48 @@ +package inbox + +import ( + "strings" + "time" + + "github.com/jprobinson/eazye" + "github.com/microcosm-cc/bluemonday" +) + +type ReaderInfo struct { + Email string + GitHub string + GitHubSet bool + ReportInterval time.Duration +} + +func ParseBody(email eazye.Email, policy bluemonday.Policy) (*ReaderInfo, error) { + r := &ReaderInfo{ + Email: 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 = policy.Sanitize(github) + r.GitHubSet = true + 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 + } + } + } + + return r, nil +} diff --git a/pkg/inbox/patterns.go b/pkg/inbox/patterns.go new file mode 100644 index 0000000..bd94731 --- /dev/null +++ b/pkg/inbox/patterns.go @@ -0,0 +1,6 @@ +package inbox + +import "regexp" + +var githubPattern = regexp.MustCompile(`(?i)github:\s*(?P\S*)`) +var intervalPattern = regexp.MustCompile(`(?i)interval:\s*(?P\S*)`) diff --git a/pkg/inbox/receive-emails.go b/pkg/inbox/receive-emails.go index 670ec5b..6dfe45b 100644 --- a/pkg/inbox/receive-emails.go +++ b/pkg/inbox/receive-emails.go @@ -5,7 +5,6 @@ import ( "log" "os" "strings" - "time" "github.com/jackc/pgx" "github.com/jprobinson/eazye" @@ -17,12 +16,6 @@ type ReceiveEmailsJob struct { 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"), @@ -37,25 +30,41 @@ func (j *ReceiveEmailsJob) Execute() { log.Printf("Failed to get incoming emails: %v\n", err) } - newReaders := make([]*newReader, 0) + newReaders := make([]*ReaderInfo, 0) + updatedReaders := make([]*ReaderInfo, 0) unsubscribers := make([]string, 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) + r, err := ParseBody(email, *j.Policy) if err != nil { log.Printf("Failed to parse subscription email: %v\n", err) + continue + } + + // Validate reporting interval + if r.ReportInterval.Minutes() <= 0 { + log.Println("User attempted to set a reporting interval of 0 or less") + continue } newReaders = append(newReaders, r) + } else if strings.HasPrefix(subjectCleaned, "[op] update") { + r, err := ParseBody(email, *j.Policy) + if err != nil { + log.Printf("Failed to parse update email: %v\n", err) + continue + } + + updatedReaders = append(updatedReaders, r) } else if strings.HasPrefix(subjectCleaned, "[op] unsubscribe") { unsubscribers = append(unsubscribers, email.From.Address) } } - if len(newReaders) == 0 && len(unsubscribers) == 0 { + if len(newReaders) == 0 && len(updatedReaders) == 0 && len(unsubscribers) == 0 { return } @@ -71,6 +80,11 @@ func (j *ReceiveEmailsJob) Execute() { saveSubscribers(readerConn, newReaders) } + // Persist reader updates to the database + if len(updatedReaders) > 0 { + saveUpdatedInfo(readerConn, updatedReaders) + } + // Delete unsubscribing readers from the database if len(unsubscribers) > 0 { deleteUnsubscribers(readerConn, unsubscribers) diff --git a/pkg/inbox/subscribe.go b/pkg/inbox/subscribe.go index 15fd357..28c6e50 100644 --- a/pkg/inbox/subscribe.go +++ b/pkg/inbox/subscribe.go @@ -2,60 +2,17 @@ package inbox import ( "bytes" - "fmt" "io" "log" - "regexp" - "strings" "text/template" "time" "github.com/jackc/pgx" - "github.com/jprobinson/eazye" "github.com/karashiiro/operator/pkg/html" "github.com/karashiiro/operator/pkg/outlook" ) -var githubPattern = regexp.MustCompile(`(?i)github:\s*(?P\S*)`) -var intervalPattern = regexp.MustCompile(`(?i)interval:\s*(?P\S*)`) - -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 saveSubscribers(conn *pgx.Conn, readers []*newReader) { +func saveSubscribers(conn *pgx.Conn, readers []*ReaderInfo) { for _, r := range readers { _, err := storeReader(conn, r) if err != nil { @@ -99,7 +56,7 @@ func buildSubscribeTemplate(w io.Writer, interval time.Duration) error { return nil } -func storeReader(conn *pgx.Conn, r *newReader) (int64, error) { +func storeReader(conn *pgx.Conn, r *ReaderInfo) (int64, error) { t, err := conn.Exec(` INSERT INTO Reader (email, github, report_interval, active) VALUES diff --git a/pkg/inbox/update.go b/pkg/inbox/update.go new file mode 100644 index 0000000..f9a9a16 --- /dev/null +++ b/pkg/inbox/update.go @@ -0,0 +1,94 @@ +package inbox + +import ( + "bytes" + "io" + "log" + "text/template" + "time" + + "github.com/jackc/pgx" + "github.com/karashiiro/operator/pkg/html" + "github.com/karashiiro/operator/pkg/outlook" +) + +func saveUpdatedInfo(conn *pgx.Conn, readers []*ReaderInfo) { + for _, r := range readers { + if r.GitHubSet { + _, err := updateGitHub(conn, r.GitHub) + if err != nil { + log.Printf("Failed to update reader GitHub: %v\n", err) + continue + } + } + + if r.ReportInterval.Minutes() > 0 { + _, err := updateReportInterval(conn, r.ReportInterval) + if err != nil { + log.Printf("Failed to update reader report interval: %v\n", err) + continue + } + } + + log.Printf("Sending update confirmation email to %s\n", r.Email) + + var updateMessage bytes.Buffer + err := buildUpdateTemplate(&updateMessage, r.ReportInterval) + if err != nil { + log.Printf("Failed to build update template: %v\n", err) + } + + err = outlook.SendEmail(r.Email, "Information updated", updateMessage.String()) + if err != nil { + log.Printf("Unable to send mail: %v\n", err) + continue + } + + log.Printf("Updated reader %s\n", r.Email) + } +} + +func buildUpdateTemplate(w io.Writer, interval time.Duration) error { + t, err := template.ParseFS(html.Files, "confirm-update.gohtml") + if err != nil { + return err + } + + err = t.Execute(w, struct { + Interval time.Duration + }{ + Interval: interval, + }) + if err != nil { + return err + } + + return nil +} + +func updateGitHub(conn *pgx.Conn, gh string) (int64, error) { + var github *string + if gh != "" { + github = &gh + } + + t, err := conn.Exec(` + UPDATE Reader SET github = $1; + `, github) + if err != nil { + return 0, err + } + + return t.RowsAffected(), nil +} + +func updateReportInterval(conn *pgx.Conn, interval time.Duration) (int64, error) { + t, err := conn.Exec(` + UPDATE Reader SET report_interval = $1; + `, interval) + if err != nil { + return 0, err + } + + return t.RowsAffected(), nil +}