package main import ( "bytes" "errors" "fmt" "io/ioutil" "net/mail" "regexp" "strings" ) type scraper interface { scrape(*mail.Message) ([]*Transaction, error) } type chaseScraper struct{} type citiScraper struct{} func Scrape(m *mail.Message) ([]*Transaction, error) { scraper, err := buildScraper(m) if err != nil { return nil, err } return scraper.scrape(m) } func buildScraper(m *mail.Message) (scraper, error) { subject := fmt.Sprint(m.Header["Subject"]) if !containsAny(subject, "transaction", "report", "Transaction") { return nil, errors.New("cannot build scraper for subject " + subject) } from := fmt.Sprint(m.Header["From"]) if strings.Contains(from, "Chase") { return newChaseScraper(), nil } if strings.Contains(from, "Citi") { return newCitiScraper(), nil } return nil, errors.New("unknown sender: " + from) } func newChaseScraper() scraper { return &chaseScraper{} } func newCitiScraper() scraper { return &citiScraper{} } func containsAny(a string, b ...string) bool { for i := range b { if strings.Contains(a, b[i]) { return true } } return false } func (c *chaseScraper) scrape(m *mail.Message) ([]*Transaction, error) { b, err := ioutil.ReadAll(m.Body) if err != nil { return nil, err } regexp := regexp.MustCompile(`A charge of \([^)]*\) (?P[\d\.]+) at (?P.*) has been authorized`) matches := regexp.FindSubmatch(b) if len(matches) < 2 { return nil, fmt.Errorf("no full matches found") } results := make(map[string][]string) for i, name := range regexp.SubexpNames() { if i != 0 && name != "" { results[name] = append(results[name], string(matches[i])) } } if len(results) != 2 || len(results["amount"]) != len(results["account"]) { return nil, fmt.Errorf("unexpected matches found looking for transactions: %+v", results) } transactions := make([]*Transaction, len(results["amount"])) for i := range results["amount"] { transactions[i] = NewTransaction(results["amount"][i], results["account"][i], fmt.Sprint(m.Header["Date"]), Chase) } return transactions, nil } func (c *citiScraper) scrape(m *mail.Message) ([]*Transaction, error) { b, err := ioutil.ReadAll(m.Body) if err != nil { return nil, err } targetLineRegexp := regexp.MustCompile(`Account #: XXXX[0-9]{4} .*`) targetMatches := targetLineRegexp.FindAll(b, -1) if len(targetMatches) == 0 { return nil, errors.New("no lines with transactions found") } results := make(map[string][]string) for _, b := range targetMatches { // Account #: XXXX3837 $137.87 at AMZN Mktp US Amzn.com/bill WA on 04/03/2020, 09:05 PM ET regexp := regexp.MustCompile(`Account #: XXXX[0-9]{4} \$(?P[0-9]+\.[0-9]*) at (?P[^,]*)`) matches := regexp.FindSubmatch(b) if len(matches) < 2 { return nil, fmt.Errorf("no full matches found: %s", b) } for i, name := range regexp.SubexpNames() { if i != 0 && name != "" { if name == "account" { matches[i] = bytes.Split(matches[i], []byte(" on "))[0] } results[name] = append(results[name], string(matches[i])) } } if len(results) != 2 || len(results["amount"]) != len(results["account"]) { return nil, fmt.Errorf("unexpected matches found looking for transactions: %+v", results) } } transactions := make([]*Transaction, len(results["amount"])) for i := range results["amount"] { transactions[i] = NewTransaction(results["amount"][i], results["account"][i], fmt.Sprint(m.Header["Date"]), Citi) } return transactions, nil }