Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10bc441e1e | ||
|
|
6f3bf1f6a4 | ||
|
|
09f14ec44c | ||
|
|
3077343c89 | ||
|
|
24b66d1c46 | ||
|
|
a6c5121d54 | ||
|
|
1ebaf9eac8 | ||
|
|
cea7a30884 | ||
|
|
c5c77a2b9b | ||
|
|
bbd51ea9c5 | ||
|
|
f32fb5aad1 | ||
|
|
4006975a21 |
10
bank.go
10
bank.go
@@ -3,16 +3,22 @@ package main
|
||||
type Bank int
|
||||
|
||||
const (
|
||||
Chase Bank = iota + 1
|
||||
Citi Bank = iota + 1
|
||||
Chase Bank = iota + 1
|
||||
Citi Bank = iota + 1
|
||||
UCCU Bank = iota + 1
|
||||
BankOfAmerica Bank = iota + 1
|
||||
)
|
||||
|
||||
func (b Bank) String() string {
|
||||
switch b {
|
||||
case BankOfAmerica:
|
||||
return "BankOfAmerica"
|
||||
case Chase:
|
||||
return "Chase"
|
||||
case Citi:
|
||||
return "Citi"
|
||||
case UCCU:
|
||||
return "UCCU"
|
||||
}
|
||||
return "?"
|
||||
}
|
||||
|
||||
93
config.go
93
config.go
@@ -7,19 +7,36 @@ import (
|
||||
"local/args"
|
||||
"local/oauth2"
|
||||
"local/storage"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Uploader int
|
||||
|
||||
const (
|
||||
UploaderTodo = Uploader(iota)
|
||||
UploaderLedger
|
||||
)
|
||||
|
||||
var uploaders = map[string]Uploader{
|
||||
"todo": UploaderTodo,
|
||||
"ledger": UploaderLedger,
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
EmailUser string
|
||||
EmailPass string
|
||||
EmailIMAP string
|
||||
TodoAddr string
|
||||
TodoToken string
|
||||
TodoList string
|
||||
TodoTag string
|
||||
Storage storage.DB
|
||||
EmailUser string
|
||||
EmailPass string
|
||||
EmailIMAP string
|
||||
TodoAddr string
|
||||
TodoToken string
|
||||
TodoList string
|
||||
TodoTag string
|
||||
Uploader Uploader
|
||||
Storage storage.DB
|
||||
Banks map[Bank]bool
|
||||
AccountsPattern string
|
||||
AccountsAntiPattern string
|
||||
}
|
||||
|
||||
var config Config
|
||||
@@ -28,14 +45,21 @@ func NewConfig() Config {
|
||||
as := args.NewArgSet()
|
||||
|
||||
as.Append(args.STRING, "emailuser", "email username", "breellocaldev@gmail.com")
|
||||
as.Append(args.STRING, "emailpass", "email password", "ML3WQRFSqe9rQ8qNkm")
|
||||
as.Append(args.STRING, "emailpass", "email password", "diblloewfncwssof")
|
||||
as.Append(args.STRING, "emailimap", "email imap", "imap.gmail.com:993")
|
||||
|
||||
as.Append(args.STRING, "uploader", "todo, ledger", "todo")
|
||||
|
||||
as.Append(args.STRING, "todoaddr", "todo addr", "https://todo-server.remote.blapointe.com")
|
||||
as.Append(args.STRING, "todopass", "todo pass", "gJtEXbbLHLf54yS9EdujtVN2n6Y")
|
||||
as.Append(args.STRING, "todotoken", "todo token", "")
|
||||
as.Append(args.STRING, "todolist", "todo list", "")
|
||||
as.Append(args.STRING, "todotag", "todo tag", "expense")
|
||||
|
||||
as.Append(args.STRING, "banks", "uccu,citi,chase,bankofamerica", "uccu,citi,chase,bankofamerica")
|
||||
as.Append(args.STRING, "accounts", "regex to include filter accounts", ".*")
|
||||
as.Append(args.STRING, "not-accounts", "regex to exclude filter accounts", "zzzzzz")
|
||||
|
||||
as.Append(args.STRING, "authaddr", "auth addr", "https://auth.remote.blapointe.com")
|
||||
as.Append(args.STRING, "store", "store type", "map")
|
||||
as.Append(args.STRING, "storeaddr", "store addr", "/tmp/store")
|
||||
@@ -46,14 +70,10 @@ func NewConfig() Config {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
token := as.GetString("todotoken")
|
||||
if len(token) == 0 {
|
||||
token = getToken(as)
|
||||
}
|
||||
|
||||
list := as.GetString("todolist")
|
||||
if len(list) == 0 {
|
||||
list = getList(as, token)
|
||||
uploader := as.GetString("uploader")
|
||||
ul, ok := uploaders[uploader]
|
||||
if !ok {
|
||||
panic("invalid uploader: " + uploader)
|
||||
}
|
||||
|
||||
storage, err := storage.New(storage.TypeFromString(as.GetString("store")), as.GetString("storeaddr"), as.GetString("storeuser"), as.GetString("storepass"))
|
||||
@@ -62,15 +82,38 @@ func NewConfig() Config {
|
||||
}
|
||||
|
||||
config = Config{
|
||||
EmailUser: as.GetString("emailuser"),
|
||||
EmailPass: as.GetString("emailpass"),
|
||||
EmailIMAP: as.GetString("emailimap"),
|
||||
TodoAddr: as.GetString("todoaddr"),
|
||||
TodoToken: token,
|
||||
TodoList: list,
|
||||
TodoTag: as.GetString("todotag"),
|
||||
Storage: storage,
|
||||
EmailUser: as.GetString("emailuser"),
|
||||
EmailPass: as.GetString("emailpass"),
|
||||
EmailIMAP: as.GetString("emailimap"),
|
||||
TodoAddr: as.GetString("todoaddr"),
|
||||
TodoTag: as.GetString("todotag"),
|
||||
AccountsPattern: as.GetString("accounts"),
|
||||
AccountsAntiPattern: as.GetString("not-accounts"),
|
||||
Storage: storage,
|
||||
Uploader: ul,
|
||||
Banks: map[Bank]bool{
|
||||
BankOfAmerica: strings.Contains(strings.ToLower(as.GetString("banks")), strings.ToLower(BankOfAmerica.String())),
|
||||
Chase: strings.Contains(strings.ToLower(as.GetString("banks")), strings.ToLower(Chase.String())),
|
||||
Citi: strings.Contains(strings.ToLower(as.GetString("banks")), strings.ToLower(Citi.String())),
|
||||
UCCU: strings.Contains(strings.ToLower(as.GetString("banks")), strings.ToLower(UCCU.String())),
|
||||
},
|
||||
}
|
||||
log.Printf("config: %+v", config)
|
||||
|
||||
if config.Uploader == UploaderTodo {
|
||||
token := as.GetString("todotoken")
|
||||
if len(token) == 0 {
|
||||
token = getToken(as)
|
||||
}
|
||||
|
||||
list := as.GetString("todolist")
|
||||
if len(list) == 0 {
|
||||
list = getList(as, token)
|
||||
}
|
||||
config.TodoToken = token
|
||||
config.TodoList = list
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
|
||||
13
main.go
13
main.go
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"local/sandbox/contact/contact"
|
||||
"log"
|
||||
"regexp"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -16,12 +17,22 @@ func main() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
patterns := regexp.MustCompile(config.AccountsPattern)
|
||||
antipatterns := regexp.MustCompile(config.AccountsAntiPattern)
|
||||
for email := range emails {
|
||||
transactions, err := Scrape(email)
|
||||
transactions, err := Scrape(email, config.Banks)
|
||||
if err != nil {
|
||||
log.Println("failed to scrape email:", err)
|
||||
}
|
||||
for _, transaction := range transactions {
|
||||
if !patterns.MatchString(transaction.Account) {
|
||||
log.Printf("skipping unmatching account pattern %q vs %q", config.AccountsPattern, transaction.Account)
|
||||
continue
|
||||
}
|
||||
if antipatterns.MatchString(transaction.Account) {
|
||||
log.Printf("skipping match account antipattern %q vs %q", config.AccountsAntiPattern, transaction.Account)
|
||||
continue
|
||||
}
|
||||
if _, err := config.Storage.Get(transaction.ID); err == nil {
|
||||
log.Println("skipping duplicate transaction:", transaction)
|
||||
} else {
|
||||
|
||||
227
scrape.go
227
scrape.go
@@ -7,6 +7,7 @@ import (
|
||||
"io/ioutil"
|
||||
"net/mail"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -14,36 +15,52 @@ type scraper interface {
|
||||
scrape(*mail.Message) ([]*Transaction, error)
|
||||
}
|
||||
|
||||
type bankOfAmericaScraper struct{}
|
||||
type chaseScraper struct{}
|
||||
type citiScraper struct{}
|
||||
type uccuScraper struct{}
|
||||
|
||||
func Scrape(m *mail.Message) ([]*Transaction, error) {
|
||||
scraper, err := buildScraper(m)
|
||||
func Scrape(m *mail.Message, banks map[Bank]bool) ([]*Transaction, error) {
|
||||
scraper, err := buildScraper(m, banks)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return scraper.scrape(m)
|
||||
}
|
||||
|
||||
func buildScraper(m *mail.Message) (scraper, error) {
|
||||
func buildScraper(m *mail.Message, banks map[Bank]bool) (scraper, error) {
|
||||
subject := fmt.Sprint(m.Header["Subject"])
|
||||
if !containsAny(subject, "transaction", "report", "Transaction") {
|
||||
if !containsAny(subject, "transaction", "report", "Transaction", "payment") {
|
||||
return nil, errors.New("cannot build scraper for subject " + subject)
|
||||
}
|
||||
from := fmt.Sprint(m.Header["From"])
|
||||
if strings.Contains(from, "Chase") {
|
||||
if strings.Contains(from, "Chase") && banks[Chase] {
|
||||
return newChaseScraper(), nil
|
||||
}
|
||||
if strings.Contains(from, "Citi") {
|
||||
if strings.Contains(from, "Bank of America") && banks[BankOfAmerica] {
|
||||
return newBankOfAmericaScraper(), nil
|
||||
}
|
||||
if strings.Contains(from, "Citi") && banks[Citi] {
|
||||
return newCitiScraper(), nil
|
||||
}
|
||||
if strings.Contains(from, "Notifications@uccu.com") && banks[UCCU] {
|
||||
return newUCCUScraper(), nil
|
||||
}
|
||||
return nil, errors.New("unknown sender: " + from)
|
||||
}
|
||||
|
||||
func newBankOfAmericaScraper() scraper {
|
||||
return &bankOfAmericaScraper{}
|
||||
}
|
||||
|
||||
func newChaseScraper() scraper {
|
||||
return &chaseScraper{}
|
||||
}
|
||||
|
||||
func newUCCUScraper() scraper {
|
||||
return &uccuScraper{}
|
||||
}
|
||||
|
||||
func newCitiScraper() scraper {
|
||||
return &citiScraper{}
|
||||
}
|
||||
@@ -58,67 +75,207 @@ func containsAny(a string, b ...string) bool {
|
||||
}
|
||||
|
||||
func (c *chaseScraper) scrape(m *mail.Message) ([]*Transaction, error) {
|
||||
transactions, err := c.scrape2021(m)
|
||||
if err == nil && len(transactions) > 0 {
|
||||
return transactions, err
|
||||
}
|
||||
return c.scrape2020(m)
|
||||
}
|
||||
|
||||
func (c *chaseScraper) scrape2021(m *mail.Message) ([]*Transaction, error) {
|
||||
if t, err := c.scrape2021Payment(m); err == nil {
|
||||
return t, err
|
||||
}
|
||||
return c.scrape2021Charge(m)
|
||||
}
|
||||
|
||||
func (c *chaseScraper) scrape2021Payment(m *mail.Message) ([]*Transaction, error) {
|
||||
re := regexp.MustCompile(`^We've received your .* payment$`)
|
||||
if !re.Match([]byte(m.Header["Subject"][0])) {
|
||||
return nil, errors.New("no match subject search")
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(m.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
regexp := regexp.MustCompile(`A charge of \([^)]*\) (?P<amount>[\d\.]+) at (?P<account>.*) has been authorized`)
|
||||
matches := regexp.FindSubmatch(b)
|
||||
|
||||
re = regexp.MustCompile(`\$[0-9]+\.[0-9]{2}`)
|
||||
amount := "-" + strings.TrimLeft(string(re.Find(b)), "$")
|
||||
amount = strings.TrimLeft(string(re.Find(b)), "$")
|
||||
|
||||
vendor := "Payment"
|
||||
|
||||
re = regexp.MustCompile(`\(\.\.\.[0-9]{4}\)`)
|
||||
match := re.Find(b)
|
||||
re = regexp.MustCompile(`[0-9]{4}`)
|
||||
account := string(re.Find(match))
|
||||
|
||||
return []*Transaction{NewTransaction(account, amount, vendor, fmt.Sprint(m.Header["Date"]), Chase)}, nil
|
||||
}
|
||||
|
||||
func (c *chaseScraper) scrape2021Charge(m *mail.Message) ([]*Transaction, error) {
|
||||
re := regexp.MustCompile(`^Your \$(?P<amount>[0-9\.]*) transaction with (?P<vendor>.*)$`)
|
||||
matches := re.FindSubmatch([]byte(m.Header["Subject"][0]))
|
||||
if len(matches) < 1 {
|
||||
return nil, errors.New("no match subject search")
|
||||
}
|
||||
amount := string(matches[1])
|
||||
vendor := string(matches[2])
|
||||
|
||||
b, _ := ioutil.ReadAll(m.Body)
|
||||
re = regexp.MustCompile(`\(\.\.\.[0-9]{4}\)`)
|
||||
match := re.Find(b)
|
||||
re = regexp.MustCompile(`[0-9]{4}`)
|
||||
account := string(re.Find(match))
|
||||
|
||||
return []*Transaction{NewTransaction(account, amount, vendor, fmt.Sprint(m.Header["Date"]), Chase)}, nil
|
||||
}
|
||||
|
||||
func (c *chaseScraper) scrape2020(m *mail.Message) ([]*Transaction, error) {
|
||||
b, err := ioutil.ReadAll(m.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
re := regexp.MustCompile(`A charge of \([^)]*\) (?P<amount>[\d\.]+) at (?P<vendor>.*) has been authorized`)
|
||||
matches := re.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() {
|
||||
for i, name := range re.SubexpNames() {
|
||||
if i != 0 && name != "" {
|
||||
results[name] = append(results[name], string(matches[i]))
|
||||
}
|
||||
}
|
||||
if len(results) != 2 || len(results["amount"]) != len(results["account"]) {
|
||||
if len(results) != 2 || len(results["amount"]) != len(results["vendor"]) {
|
||||
return nil, fmt.Errorf("unexpected matches found looking for transactions: %+v", results)
|
||||
}
|
||||
re = regexp.MustCompile(`account ending in (?P<account>[0-9]{4})\.`)
|
||||
match := re.Find(b)
|
||||
re = regexp.MustCompile(`[0-9]{4}`)
|
||||
account := string(re.Find(match))
|
||||
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)
|
||||
transactions[i] = NewTransaction(account, results["amount"][i], results["vendor"][i], fmt.Sprint(m.Header["Date"]), Chase)
|
||||
}
|
||||
return transactions, nil
|
||||
}
|
||||
|
||||
func (c *citiScraper) scrape(m *mail.Message) ([]*Transaction, error) {
|
||||
date := fmt.Sprint(m.Header["Date"])
|
||||
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")
|
||||
re := regexp.MustCompile(`Citi Alert: A \$[0-9][0-9]*\.[0-9][0-9] transaction was made at .* on card ending in`)
|
||||
match := re.Find(b)
|
||||
if len(match) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
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<amount>[0-9]+\.[0-9]*) at (?P<account>[^,]*)`)
|
||||
matches := regexp.FindSubmatch(b)
|
||||
if len(matches) < 2 {
|
||||
return nil, fmt.Errorf("no full matches found: %s", b)
|
||||
rePrice := regexp.MustCompile(`[0-9][0-9]*\.[0-9][0-9]`)
|
||||
price := rePrice.Find(match)
|
||||
|
||||
vendor := bytes.Split(bytes.Split(match, []byte(" on card ending in"))[0], []byte("transaction was made at "))[1]
|
||||
|
||||
transaction := NewTransaction(Citi.String(), string(price), string(vendor), date, Citi)
|
||||
|
||||
return []*Transaction{transaction}, nil
|
||||
//Citi Alert: A $598.14 transaction was made at REMIX MUSIC SPRINGDA on card ending in 3837
|
||||
/*
|
||||
b, err := ioutil.ReadAll(m.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for i, name := range regexp.SubexpNames() {
|
||||
if i != 0 && name != "" {
|
||||
if name == "account" {
|
||||
matches[i] = bytes.Split(matches[i], []byte(" on "))[0]
|
||||
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<amount>[0-9]+\.[0-9]*) at (?P<account>[^,]*)`)
|
||||
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]))
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
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(Citi.String(), results["amount"][i], results["account"][i], fmt.Sprint(m.Header["Date"]), Citi)
|
||||
}
|
||||
|
||||
return transactions, nil
|
||||
*/
|
||||
}
|
||||
|
||||
func (c *uccuScraper) scrape(m *mail.Message) ([]*Transaction, error) {
|
||||
b, err := ioutil.ReadAll(m.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
regexp := regexp.MustCompile(`\$([0-9]+,?)+\.[0-9][0-9]`)
|
||||
match := regexp.Find(b)
|
||||
if len(match) == 0 {
|
||||
return nil, fmt.Errorf("no matches found")
|
||||
}
|
||||
match = match[1:]
|
||||
match = bytes.ReplaceAll(match, []byte(","), []byte{})
|
||||
f, err := strconv.ParseFloat(string(match), 10)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !bytes.Contains(b, []byte("credit")) {
|
||||
f *= -1.0
|
||||
}
|
||||
transaction := NewTransaction(UCCU.String(), fmt.Sprintf("%.2f", f), "?", fmt.Sprint(m.Header["Date"]), UCCU)
|
||||
return []*Transaction{transaction}, nil
|
||||
}
|
||||
|
||||
func (c *bankOfAmericaScraper) scrape(m *mail.Message) ([]*Transaction, error) {
|
||||
subject := fmt.Sprint(m.Header["Subject"])
|
||||
if strings.Contains(subject, "Credit card transaction") {
|
||||
return c.scrapeCharge(m)
|
||||
}
|
||||
return nil, errors.New("not impl")
|
||||
}
|
||||
|
||||
func (c *bankOfAmericaScraper) scrapeCharge(m *mail.Message) ([]*Transaction, error) {
|
||||
b, err := ioutil.ReadAll(m.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
amount := ""
|
||||
acc := ""
|
||||
for _, line := range bytes.Split(b, []byte("\n")) {
|
||||
if amount == "" && bytes.HasPrefix(line, []byte("Amount: $")) {
|
||||
words := bytes.Split(bytes.TrimSpace(line), []byte(" "))
|
||||
lastword := words[len(words)-1][1:]
|
||||
escapedfloat := bytes.TrimPrefix(lastword, []byte("$"))
|
||||
fixEscape := bytes.ReplaceAll(escapedfloat, []byte("=2E"), []byte("."))
|
||||
amount = string(fixEscape)
|
||||
} else if acc == "" && bytes.HasPrefix(line, []byte("Where: ")) {
|
||||
acc = string(bytes.TrimSpace(bytes.TrimPrefix(line, []byte("Where: "))))
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
if amount == "" || acc == "" {
|
||||
return nil, errors.New("no amount/account found")
|
||||
}
|
||||
|
||||
return transactions, nil
|
||||
transaction := NewTransaction(BankOfAmerica.String(), amount, acc, fmt.Sprint(m.Header["Date"]), BankOfAmerica)
|
||||
return []*Transaction{transaction}, nil
|
||||
}
|
||||
|
||||
145
scrape_test.go
Normal file
145
scrape_test.go
Normal file
@@ -0,0 +1,145 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"net/mail"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScrapeChase2021Payment(t *testing.T) {
|
||||
b, err := ioutil.ReadFile("./testdata/chase.2021.payment.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
message := &mail.Message{
|
||||
Header: map[string][]string{
|
||||
"Subject": []string{"We've received your AARP from Chase payment"},
|
||||
},
|
||||
Body: bytes.NewReader(b),
|
||||
}
|
||||
|
||||
chase := &chaseScraper{}
|
||||
|
||||
gots, err := chase.scrape2021(message)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(gots) != 1 {
|
||||
t.Fatal(gots)
|
||||
}
|
||||
got := gots[0]
|
||||
|
||||
if got.Account != "8824" {
|
||||
t.Fatalf("bad account: %v: %+v", got.Account, got)
|
||||
}
|
||||
if got.Amount != "100.00" {
|
||||
t.Fatalf("bad amount: %v: %+v", got.Amount, got)
|
||||
}
|
||||
if got.Vendor != "Payment" {
|
||||
t.Fatalf("bad vendor: %v: %+v", got.Vendor, got)
|
||||
}
|
||||
t.Logf("%+v", got)
|
||||
}
|
||||
|
||||
func TestScrapeChase2021(t *testing.T) {
|
||||
b, err := ioutil.ReadFile("./testdata/chase.2021.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
message := &mail.Message{
|
||||
Header: map[string][]string{
|
||||
"Subject": []string{"Your $38.84 transaction with TARGET T-1754"},
|
||||
},
|
||||
Body: bytes.NewReader(b),
|
||||
}
|
||||
|
||||
chase := &chaseScraper{}
|
||||
|
||||
gots, err := chase.scrape2021(message)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(gots) != 1 {
|
||||
t.Fatal(gots)
|
||||
}
|
||||
got := gots[0]
|
||||
|
||||
if got.Account != "8824" {
|
||||
t.Fatalf("bad account: %v: %+v", got.Account, got)
|
||||
}
|
||||
if got.Amount != "38.84" {
|
||||
t.Fatalf("bad amount: %v: %+v", got.Amount, got)
|
||||
}
|
||||
if got.Vendor != "TARGET T-1754" {
|
||||
t.Fatalf("bad vendor: %v: %+v", got.Vendor, got)
|
||||
}
|
||||
t.Logf("%+v", got)
|
||||
}
|
||||
|
||||
func TestScrapeChase2020(t *testing.T) {
|
||||
b, err := ioutil.ReadFile("./testdata/chase.2020.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
message := &mail.Message{
|
||||
Body: bytes.NewReader(b),
|
||||
}
|
||||
|
||||
chase := &chaseScraper{}
|
||||
|
||||
gots, err := chase.scrape2020(message)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(gots) != 1 {
|
||||
t.Fatal(gots)
|
||||
}
|
||||
got := gots[0]
|
||||
|
||||
if got.Account != "8824" {
|
||||
t.Fatalf("bad account: %v: %+v", got.Account, got)
|
||||
}
|
||||
if got.Amount != "16.08" {
|
||||
t.Fatalf("bad amount: %v: %+v", got.Amount, got)
|
||||
}
|
||||
if got.Vendor != "PAYPAL *BLIZZARDENT" {
|
||||
t.Fatalf("bad vendor: %q: %+v", got.Vendor, got)
|
||||
}
|
||||
t.Logf("%+v", got)
|
||||
}
|
||||
|
||||
func TestScrapeBofACharge(t *testing.T) {
|
||||
b, err := ioutil.ReadFile("./testdata/bofa.charge.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
message := &mail.Message{
|
||||
Header: map[string][]string{
|
||||
"Subject": []string{"Credit card transaction exceeds alert limit you set"},
|
||||
},
|
||||
Body: bytes.NewReader(b),
|
||||
}
|
||||
bofa := &bankOfAmericaScraper{}
|
||||
|
||||
gots, err := bofa.scrape(message)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if len(gots) != 1 {
|
||||
t.Fatal(len(gots))
|
||||
}
|
||||
got := gots[0]
|
||||
want := Transaction{
|
||||
ID: got.ID,
|
||||
Bank: BankOfAmerica,
|
||||
Amount: "75.08",
|
||||
Vendor: "PAYPAL GIBBDOGENTE MA",
|
||||
Date: "[]",
|
||||
Account: BankOfAmerica.String(),
|
||||
}
|
||||
if *got != want {
|
||||
t.Fatalf("want:\n\t%+v, got\n\t%+v", want, *got)
|
||||
}
|
||||
}
|
||||
46
testdata/bofa.charge.txt
vendored
Normal file
46
testdata/bofa.charge.txt
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
|
||||
|
||||
Credit card transaction exceeds alert limit you set
|
||||
|
||||
National Education Association World Mas ending in 7522
|
||||
|
||||
Amount: $75=2E08
|
||||
Date: September 05, 2021
|
||||
Where: PAYPAL GIBBDOGENTE MA
|
||||
|
||||
View details by going to
|
||||
https://www=2Ebankofamerica=2Ecom/deeplink/redirect=2Ego?target=3Dbofasigni=
|
||||
n&screen=3DAccounts:Home&version=3D7=2E0=2E0
|
||||
|
||||
If you made this purchase or payment but don=27t recognize the amount,
|
||||
wait until the final purchase amount has posted before filing a dispute
|
||||
claim=2E
|
||||
|
||||
If you don=27t recognize this activity, please contact us at the number
|
||||
on the back of your card=2E
|
||||
|
||||
Did you know?
|
||||
You can choose how you get alerts from us including text messages and
|
||||
mobile notifications=2E Go to Alert Settings at
|
||||
https://www=2Ebankofamerica=2Ecom/deeplink/redirect=2Ego?target=3Dalerts_se=
|
||||
ttings&screen=3DAlerts:Home&gotoSetting=3Dtrue&version=3D7=2E1=2E0
|
||||
|
||||
|
||||
We'll never ask for your personal information such as SSN or ATM PIN in
|
||||
email messages=2E If you get an email that looks suspicious or you are not =
|
||||
|
||||
the intended recipient of this email, don't click on any links=2E Instead, =
|
||||
|
||||
forward to abuse@bankofamerica=2Ecom then delete it=2E
|
||||
|
||||
Please don't reply to this automatically generated service email=2E
|
||||
Read our Privacy Notice https://www=2Ebankofamerica=2Ecom/privacy/consumer-=
|
||||
privacy-notice=2Ego
|
||||
Equal Housing Lender: https://www=2Ebankofamerica=2Ecom/help/equalhousing=
|
||||
=2Ecfm
|
||||
Bank of America, N=2EA=2E Member FDIC
|
||||
(C) 2021 Bank of America Corporation
|
||||
|
||||
|
||||
|
||||
=20
|
||||
10
testdata/chase.2020.txt
vendored
Normal file
10
testdata/chase.2020.txt
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
This is an Alert to help you manage your credit card account ending in 8824.
|
||||
|
||||
As you requested, we are notifying you of any charges over the amount of ($USD) 0.00, as specified in your Alert settings.
|
||||
A charge of ($USD) 16.08 at PAYPAL *BLIZZARDENT has been authorized on Jul 6, 2021 at 6:21 PM ET.
|
||||
|
||||
Do not reply to this Alert.
|
||||
|
||||
If you have questions, please call the number on the back of your credit card, or send a secure message from your Inbox on www.chase.com.
|
||||
|
||||
To see all of the Alerts available to you, or to manage your Alert settings, please log on to www.chase.com.
|
||||
411
testdata/chase.2021.payment.txt
vendored
Normal file
411
testdata/chase.2021.payment.txt
vendored
Normal file
@@ -0,0 +1,411 @@
|
||||
|
||||
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.=
|
||||
w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns=3D"http://www.w3.org/1999/xhtml" lang=3D"en">
|
||||
<head>
|
||||
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3DUTF-8" />
|
||||
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=3D1.=
|
||||
0"/>
|
||||
<title>This payment has been applied to your account.</title>
|
||||
<style type=3D"text/css">
|
||||
* {
|
||||
=09line-height: normal !important;
|
||||
}
|
||||
strong {
|
||||
=09font-weight: bold !important;
|
||||
}
|
||||
em {
|
||||
=09font-style: italic !important;
|
||||
}
|
||||
body {
|
||||
=09background-color: #d7dbe0 !important;
|
||||
=09-webkit-text-size-adjust: none !important;
|
||||
}
|
||||
.ExternalClass * {
|
||||
=09line-height: 112%
|
||||
}
|
||||
.ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass =
|
||||
td {
|
||||
=09line-height: 112%
|
||||
}
|
||||
td {
|
||||
=09-webkit-text-size-adjust: none;
|
||||
}
|
||||
a[href^=3Dtel] {
|
||||
=09color: inherit;
|
||||
=09text-decoration: none;
|
||||
}
|
||||
.applelinksgray41 a {
|
||||
=09color: #414042 !important;
|
||||
=09text-decoration: none;
|
||||
}
|
||||
.applelinksgray a {
|
||||
=09color: #717171 !important;
|
||||
=09text-decoration: none;
|
||||
}
|
||||
.wordBreak {
|
||||
=09overflow-wrap: break-word;
|
||||
=09word-wrap: break-word;
|
||||
=09word-break: break-all;
|
||||
=09word-break: break-word;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.fullWidth {
|
||||
=09width: 100% !important;
|
||||
=09min-width: 100% !important;
|
||||
=09margin-left: auto !important;
|
||||
=09margin-right: auto !important;
|
||||
=09padding: 0px !important;
|
||||
=09text-align: center !important;
|
||||
}
|
||||
.hero {
|
||||
=09width: 100% !important;
|
||||
=09height: auto !important;
|
||||
}
|
||||
.moPad {
|
||||
=09padding-right: 20px !important;
|
||||
=09padding-left: 20px !important;
|
||||
}
|
||||
.zeroPad {
|
||||
=09padding-right: 0px !important;
|
||||
=09padding-left: 0px !important;
|
||||
}
|
||||
.font14 {
|
||||
=09font-size: 14px !important;
|
||||
}
|
||||
.font24 {
|
||||
=09font-size: 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media print and (max-width: 800px) {
|
||||
.fullWidth {
|
||||
=09width: 100% !important;
|
||||
=09min-width: 100% !important;
|
||||
=09margin-left: auto !important;
|
||||
=09margin-right: auto !important;
|
||||
=09padding: 0px !important;
|
||||
=09text-align: center !important;
|
||||
}
|
||||
.hero {
|
||||
=09width: 100% !important;
|
||||
=09height: auto !important;
|
||||
}
|
||||
.moPad {
|
||||
=09padding-right: 20px !important;
|
||||
=09padding-left: 20px !important;
|
||||
}
|
||||
.zeroPad {
|
||||
=09padding-right: 0px !important;
|
||||
=09padding-left: 0px !important;
|
||||
}
|
||||
.font14 {
|
||||
=09font-size: 14px !important;
|
||||
}
|
||||
.font24 {
|
||||
=09font-size: 24px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style=3D"padding: 0px;margin: 0px; background-color:#d7dbe0;">
|
||||
<table align=3D"center" width=3D"100%" border=3D"0" cellspacing=3D"0" cellp=
|
||||
adding=3D"0" style=3D"min-width:800px; background-color:#d7dbe0;" class=3D"=
|
||||
fullWidth">
|
||||
<tr>
|
||||
<td align=3D"center" style=3D"vertical-align:top; padding:0px 0px 20px =
|
||||
0px; min-width:800px; background-color:#d7dbe0;" class=3D"fullWidth"><table=
|
||||
align=3D"center" width=3D"800" cellpadding=3D"0" cellspacing=3D"0" border=
|
||||
=3D"0" class=3D"fullWidth" style=3D"background-color:#FFFFFF;">
|
||||
<!-- Start of Content -->
|
||||
<tr>
|
||||
<td align=3D"center" style=3D"vertical-align:top; padding: 23px 0=
|
||||
px 0px;background-color: #005EB8;"><table cellpadding=3D"0" cellspacing=3D"=
|
||||
0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"right" style=3D"vertical-align:bottom; padding=
|
||||
:0px 0px; width:12px;"><img src=3D"https://www.chase.com/content/dam/email/=
|
||||
images/blue-left.jpg" width=3D"12" height=3D"226" border=3D"0" style=3D"dis=
|
||||
play:block;" alt=3D""/></td>
|
||||
<td align=3D"center" style=3D"vertical-align:bottom; paddin=
|
||||
g: 0px 0px 0px;width:616px; background-color: #FFFFFF;"><table width=3D"100=
|
||||
%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top; paddi=
|
||||
ng: 0px 0px; background-color: #ffffff;"><table width=3D"100%" cellpadding=
|
||||
=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<!-- Start hidden preview text -->
|
||||
<div style=3D"display: none; max-height: 0px; ove=
|
||||
rflow: hidden;">This payment has been applied to your account.</div>
|
||||
|
||||
<!-- Insert ‌ after hidden preview tex=
|
||||
t -->
|
||||
|
||||
<div style=3D"display: none; max-height: 0px; ove=
|
||||
rflow: hidden;"> ‌ ‌ ‌ ‌ &zwn=
|
||||
j; ‌ ‌ ‌ ‌ ‌ ‌&=
|
||||
nbsp;‌ ‌ ‌ ‌ ‌ &zwn=
|
||||
j; ‌ ‌ ‌ ‌ ‌ ‌&=
|
||||
nbsp;‌ ‌ ‌ ‌ ‌ ‌&nbs=
|
||||
p;‌ ‌ ‌ ‌ ‌ ‌ &=
|
||||
zwnj; ‌ ‌ ‌ ‌ ‌ &zwn=
|
||||
j; ‌ ‌ ‌ ‌ ‌ ‌&=
|
||||
nbsp;‌ ‌ ‌ ‌ ‌ ‌&nbs=
|
||||
p;‌ ‌ ‌ ‌ ‌ ‌ &=
|
||||
zwnj; ‌ ‌ ‌ ‌ ‌&nbs=
|
||||
p;‌ ‌ ‌ ‌ ‌ ‌ &=
|
||||
zwnj; ‌ ‌ ‌ ‌ ‌ &zwn=
|
||||
j; ‌ ‌ ‌ ‌ ‌ ‌&=
|
||||
nbsp;‌ ‌ ‌ ‌ ‌ ‌&nbs=
|
||||
p;‌ ‌ ‌ ‌ ‌ ‌ &=
|
||||
zwnj; ‌ ‌ ‌ ‌ ‌ &zwn=
|
||||
j; </div>
|
||||
<!-- End hidden preview text -->
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top;=
|
||||
padding-left: 30px; background-color: #ffffff;" class=3D"moPad"><table widt=
|
||||
h=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-alig=
|
||||
n:bottom; padding:36px 0px 20px;"><img src=3D"https://www.chase.com/content=
|
||||
/dam/email/images/chase-logo-h-rgb.png" width=3D"104" height=3D"20" border=
|
||||
=3D"0" style=3D"display:block;" alt=3D"Chase Logo"/></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top;=
|
||||
padding: 20px 28px 0px;" class=3D"moPad"><table align=3D"left" cellpadding=
|
||||
=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-alig=
|
||||
n:top;"><table width=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D=
|
||||
"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertica=
|
||||
l-align:top; padding:5px 10px; font-family:Arial, Helvetica, sans-serif; fo=
|
||||
nt-size:12px; font-weight:bold; color:#000000; background-color:#24e16b; bo=
|
||||
rder-radius:20px; -moz-border-radius: 20px; -webkit-border-radius:20px; whi=
|
||||
te-space: nowrap;" class=3D"font14">Payment received</td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top;=
|
||||
background-color: #ffffff;"><table width=3D"100%" cellpadding=3D"0" cellsp=
|
||||
acing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-alig=
|
||||
n:top; padding: 20px 30px 28px;" class=3D"moPad"><table width=3D"100%" cell=
|
||||
padding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertica=
|
||||
l-align:top; padding: 0px 20px 0px 0px;"><img src=3D"https://static.chasecd=
|
||||
n.com/content/services/rendition/image.small.png/unified-assets/digital-car=
|
||||
ds/aarp/41473417018.png" width=3D"57" height=3D"auto" alt=3D"" border=3D"0=
|
||||
" style=3D"display:block;"/></td>
|
||||
<td align=3D"left" style=3D"vertica=
|
||||
l-align:top; padding:0px 50px 0px 0px; font-family:Arial, Helvetica, sans-s=
|
||||
erif; font-size:30px; font-weight: bold; color:#414042;" class=3D"zeroPad">=
|
||||
We've received your credit card payment</td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
<td align=3D"left" style=3D"vertical-align:bottom; padding:=
|
||||
0px 0px;width:12px; "><img src=3D"https://www.chase.com/content/dam/email/i=
|
||||
mages/blue-right.jpg " width=3D"12" height=3D"226" border=3D"0" style=3D"di=
|
||||
splay:block;" alt=3D""/></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"center" style=3D"vertical-align:top; padding: 0px 0p=
|
||||
x 0px; background-color: #FFFFFF;"><table cellpadding=3D"0" cellspacing=3D"=
|
||||
0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"right" style=3D"vertical-align:top; padding:0p=
|
||||
x 0px; width:12px;"><img src=3D"https://www.chase.com/content/dam/email/ima=
|
||||
ges/white-left.jpg" width=3D"12" height=3D"77" border=3D"0" style=3D"displa=
|
||||
y:block;" alt=3D""/></td>
|
||||
<td align=3D"center" style=3D"vertical-align:top; padding: =
|
||||
0px 0px 0px;width:616px;"><table width=3D"100%" cellpadding=3D"0" cellspaci=
|
||||
ng=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top; paddi=
|
||||
ng:0px 150px 20px 30px; font-family:Arial, Helvetica, sans-serif; font-size=
|
||||
:16px; color:#414042;" class=3D"moPad">This payment has been applied to you=
|
||||
r account.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top; paddi=
|
||||
ng: 0px 150px 0px 30px;" class=3D"moPad"><table width=3D"100%" cellpadding=
|
||||
=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top;=
|
||||
padding: 10px 0px;border-bottom: solid 1px #414042;"><table width=3D"100%"=
|
||||
cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-alig=
|
||||
n:top; padding:0px 0px 0px 0px; font-family:Arial, Helvetica, sans-serif; f=
|
||||
ont-size:16px; color:#414042;" class=3D"font14">Account</td>
|
||||
<td align=3D"right" style=3D"vertical-ali=
|
||||
gn:top; padding:0px 0px 0px 5px; font-family:Arial, Helvetica, sans-serif; =
|
||||
font-size:16px; font-weight:bold; color:#414042;" class=3D"font14">AARP fro=
|
||||
m Chase (...8824)</td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top;=
|
||||
padding: 10px 0px;border-bottom: solid 1px #414042;"><table width=3D"100%"=
|
||||
cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-alig=
|
||||
n:top; padding:0px 0px 0px 0px; font-family:Arial, Helvetica, sans-serif; f=
|
||||
ont-size:16px; color:#414042;" class=3D"font14">Posted date</td>
|
||||
<td align=3D"right" style=3D"vertical-ali=
|
||||
gn:top; padding:0px 0px 0px 5px; font-family:Arial, Helvetica, sans-serif; =
|
||||
font-size:16px; font-weight:bold; color:#414042;" class=3D"font14">Jul 30, =
|
||||
2021</td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top;=
|
||||
padding: 10px 0px;border-bottom: solid 1px #414042;"><table width=3D"100%"=
|
||||
cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-alig=
|
||||
n:top; padding:0px 0px 0px 0px; font-family:Arial, Helvetica, sans-serif; f=
|
||||
ont-size:16px; color:#414042;" class=3D"font14">Payment amount</td>
|
||||
<td align=3D"right" style=3D"vertical-ali=
|
||||
gn:top; padding:0px 0px 0px 5px; font-family:Arial, Helvetica, sans-serif; =
|
||||
font-size:16px; font-weight:bold; color:#414042;" class=3D"font14"><span cl=
|
||||
ass=3D"applelinksgray41"><a style=3D"color:#414042;text-decoration: none;">=
|
||||
$100.00</a></span></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top; paddi=
|
||||
ng:40px 150px 40px 30px; font-family:Arial, Helvetica, sans-serif; font-siz=
|
||||
e:16px; color:#414042;" class=3D"moPad">Find <a style=3D"text-decoration: u=
|
||||
nderline; color:#0060F0;" href=3D"https://www.chase.com/personal/credit-car=
|
||||
ds/login-epay" rel=3D"noopener noreferrer" target=3D"_blank">more informat=
|
||||
ion</a> about the credit card payments process.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top;"><tab=
|
||||
le width=3D"100%" align=3D"left" cellpadding=3D"0" cellspacing=3D"0" border=
|
||||
=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top;=
|
||||
"><table width=3D"100%" align=3D"left" cellpadding=3D"0" cellspacing=3D"0" =
|
||||
border=3D"0" class=3D"fullWidth">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"padding:0px; =
|
||||
vertical-align:top; padding: 0px 0px 30px 30px;" class=3D"moPad"><table ali=
|
||||
gn=3D"left" cellpadding=3D"0" cellspacing=3D"0" border=3D"0" style=3D"verti=
|
||||
cal-align:top;">
|
||||
<tr>
|
||||
<td role=3D"button" align=3D"center=
|
||||
" style=3D"background-color:#0060f0; color: #fffffe; font-size: 16px; font-=
|
||||
family: Arial, Helvetica, sans-serif; padding: 10px 0px; border: 1px solid =
|
||||
#0060f0; vertical-align:top; border-radius:4px; -moz-border-radius: 4px; -w=
|
||||
ebkit-border-radius:4px;width: 200px;"><a href=3D"https://www.chase.com/per=
|
||||
sonal/mobile-online-banking/payment-activity" target=3D"_blank" style=3D"co=
|
||||
lor: #fffffe; text-decoration:none;">See payment activity</a></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top; paddi=
|
||||
ng:0px 30px 20px; font-family:Arial, Helvetica, sans-serif; font-size:12px;=
|
||||
color:#717171;" class=3D"moPad font14">Securely access your accounts with =
|
||||
the <a style=3D"text-decoration: underline; color:#0060F0;" href=3D"https:/=
|
||||
/www.chase.com/digital/mobile-banking" rel=3D"noopener noreferrer" target=
|
||||
=3D"_blank">Chase Mobile<span style=3D"font-size:70%; line-height:0; v=
|
||||
ertical-align:3px; text-decoration: none;">®</span> app</a> or <a style=
|
||||
=3D"text-decoration: underline; color:#0060F0;" href=3D"https://secure.chas=
|
||||
e.com/web/auth/nav?navKey=3DrequestDashboard" rel=3D"noopener noreferrer" =
|
||||
target=3D"_blank">chase.com</a>. </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top; paddi=
|
||||
ng: 0px 0px; background-color: #F6F6F6;"><table width=3D"100%" cellpadding=
|
||||
=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top;=
|
||||
padding:20px 30px 60px; font-family:Arial, Helvetica, sans-serif; font-siz=
|
||||
e:12px; color:#717171;" class=3D"moPad font14"><span role=3D"heading" style=
|
||||
=3D"text-transform: uppercase; font-weight: bold;">About this message</span=
|
||||
><br />
|
||||
<br />
|
||||
Chase Mobile<span style=3D"font-size:70%=
|
||||
; line-height:0; vertical-align:3px;">®</span> app is available for se=
|
||||
lect mobile devices. Message and data rates may apply.<br />
|
||||
<br />
|
||||
This service email was sent based on your ale=
|
||||
rt settings. Use the Chase Mobile app or visit <a href=3D"https://www.=
|
||||
chase.com/personal/mobile-online-banking/login-alerts" target=3D"_blank" st=
|
||||
yle=3D"text-decoration: underline; color:#0060F0;" rel=3D"noopener noreferr=
|
||||
er">chase.com/alerts</a> to view or manage your settings.<br />
|
||||
<br />
|
||||
Chase cannot guarantee the delivery of alerts=
|
||||
and notifications. Wireless or internet service provider outages or other =
|
||||
circumstances could delay them. You can always check <span class=3D"appleli=
|
||||
nksgray"><a style=3D"color:#717171;text-decoration: none;">chase.com</a></s=
|
||||
pan> or the Chase Mobile app for the status of your accounts including=
|
||||
your latest account balances and transaction details.<br />
|
||||
<br />
|
||||
To protect your personal information, please =
|
||||
don't reply to this message. Chase won't ask for confidential information i=
|
||||
n an email. <br />
|
||||
<br />
|
||||
If you have concerns about the authenticity o=
|
||||
f this message or have questions about your account visit <a style=3D"text-=
|
||||
decoration: underline; color:#0060F0;" href=3D"https://www.chase.com/digita=
|
||||
l/customer-service" target=3D"_blank" rel=3D"noopener noreferrer">chase.com=
|
||||
/CustomerService</a> for ways to contact us.<br />
|
||||
<br />
|
||||
Your privacy is important to us. See our onli=
|
||||
ne <a style=3D"text-decoration: underline; color:#0060F0;" href=3D"https://=
|
||||
www.chase.com/digital/resources/privacy-security" target=3D"_blank" rel=3D"=
|
||||
noopener noreferrer">Security Center</a> to learn how to protect your infor=
|
||||
mation.<br />
|
||||
<br />
|
||||
© 2021 JPMorgan Chase & Co. </td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
<td align=3D"left" style=3D"vertical-align:top; padding:0px=
|
||||
0px; width:12px;"><img src=3D"https://www.chase.com/content/dam/email/imag=
|
||||
es/white-right.jpg" width=3D"12" height=3D"77" border=3D"0" style=3D"displa=
|
||||
y:block;" alt=3D""/></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
<!--End of Content -->
|
||||
|
||||
</table></td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
396
testdata/chase.2021.txt
vendored
Normal file
396
testdata/chase.2021.txt
vendored
Normal file
@@ -0,0 +1,396 @@
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.=
|
||||
w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<html xmlns=3D"http://www.w3.org/1999/xhtml" lang=3D"en">
|
||||
<head>
|
||||
<meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3DUTF-8" />
|
||||
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=3D1.=
|
||||
0"/>
|
||||
<title>This transaction is above the level you set, see more here.</title>
|
||||
<style type=3D"text/css">
|
||||
* {
|
||||
=09line-height: normal !important;
|
||||
}
|
||||
strong {
|
||||
=09font-weight: bold !important;
|
||||
}
|
||||
em {
|
||||
=09font-style: italic !important;
|
||||
}
|
||||
body {
|
||||
=09background-color: #d7dbe0 !important;
|
||||
=09-webkit-text-size-adjust: none !important;
|
||||
}
|
||||
.ExternalClass * {
|
||||
=09line-height: 112%
|
||||
}
|
||||
.ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass =
|
||||
td {
|
||||
=09line-height: 112%
|
||||
}
|
||||
td {
|
||||
=09-webkit-text-size-adjust: none;
|
||||
}
|
||||
a[href^=3Dtel] {
|
||||
=09color: inherit;
|
||||
=09text-decoration: none;
|
||||
}
|
||||
.applelinksgray41 a {
|
||||
=09color: #414042 !important;
|
||||
=09text-decoration: none;
|
||||
}
|
||||
.applelinksgray a {
|
||||
=09color: #717171 !important;
|
||||
=09text-decoration: none;
|
||||
}
|
||||
.wordBreak {
|
||||
=09overflow-wrap: break-word;
|
||||
=09word-wrap: break-word;
|
||||
=09word-break: break-all;
|
||||
=09word-break: break-word;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
.fullWidth {
|
||||
=09width: 100% !important;
|
||||
=09min-width: 100% !important;
|
||||
=09margin-left: auto !important;
|
||||
=09margin-right: auto !important;
|
||||
=09padding: 0px !important;
|
||||
=09text-align: center !important;
|
||||
}
|
||||
.moPad {
|
||||
=09padding-right: 20px !important;
|
||||
=09padding-left: 20px !important;
|
||||
}
|
||||
.zeroPad {
|
||||
=09padding-right: 0px !important;
|
||||
=09padding-left: 0px !important;
|
||||
}
|
||||
.font14 {
|
||||
=09font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
@media print and (max-width: 800px) {
|
||||
.fullWidth {
|
||||
=09width: 100% !important;
|
||||
=09min-width: 100% !important;
|
||||
=09margin-left: auto !important;
|
||||
=09margin-right: auto !important;
|
||||
=09padding: 0px !important;
|
||||
=09text-align: center !important;
|
||||
}
|
||||
.moPad {
|
||||
=09padding-right: 20px !important;
|
||||
=09padding-left: 20px !important;
|
||||
}
|
||||
.zeroPad {
|
||||
=09padding-right: 0px !important;
|
||||
=09padding-left: 0px !important;
|
||||
}
|
||||
.font14 {
|
||||
=09font-size: 14px !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style=3D"padding: 0px;margin: 0px; background-color:#d7dbe0;">
|
||||
<table role=3D"presentation" align=3D"center" width=3D"100%" border=3D"0" c=
|
||||
ellspacing=3D"0" cellpadding=3D"0" style=3D"min-width:800px; background-col=
|
||||
or:#d7dbe0;" class=3D"fullWidth">
|
||||
<tr>
|
||||
<td align=3D"center" style=3D"vertical-align:top; padding:0px 0px 20px =
|
||||
0px; min-width:800px; background-color:#d7dbe0;" class=3D"fullWidth"><table=
|
||||
role=3D"presentation" align=3D"center" width=3D"800" cellpadding=3D"0" cel=
|
||||
lspacing=3D"0" border=3D"0" class=3D"fullWidth" style=3D"background-color:#=
|
||||
FFFFFF;">
|
||||
<!-- Start of Content -->
|
||||
<tr>
|
||||
<td align=3D"center" style=3D"vertical-align:top; padding: 23px 0=
|
||||
px 0px;background-color: #005EB8;"><table role=3D"presentation" cellpadding=
|
||||
=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"right" style=3D"vertical-align:bottom; padding=
|
||||
:0px 0px; width:12px;"><img src=3D"https://www.chase.com/content/dam/email/=
|
||||
images/blue-left.jpg" width=3D"12" height=3D"226" border=3D"0" style=3D"dis=
|
||||
play:block;" alt=3D""/></td>
|
||||
<td align=3D"center" style=3D"vertical-align:bottom; paddin=
|
||||
g: 0px 0px 0px;width:616px; background-color: #FFFFFF;"><table role=3D"pres=
|
||||
entation" width=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top; paddi=
|
||||
ng: 0px 0px;"><table role=3D"presentation" width=3D"100%" cellpadding=3D"0"=
|
||||
cellspacing=3D"0" border=3D"0">
|
||||
<!-- Start hidden preview text -->
|
||||
<div style=3D"display: none; max-height: 0px; ove=
|
||||
rflow: hidden;">This transaction is above the level you set, see more here.=
|
||||
</div>
|
||||
|
||||
<!-- Insert ‌ after hidden preview tex=
|
||||
t -->
|
||||
|
||||
<div style=3D"display: none; max-height: 0px; ove=
|
||||
rflow: hidden;"> ‌ ‌ ‌ ‌ &zwn=
|
||||
j; ‌ ‌ ‌ ‌ ‌ ‌&=
|
||||
nbsp;‌ ‌ ‌ ‌ ‌ &zwn=
|
||||
j; ‌ ‌ ‌ ‌ ‌ ‌&=
|
||||
nbsp;‌ ‌ ‌ ‌ ‌ ‌&nbs=
|
||||
p;‌ ‌ ‌ ‌ ‌ ‌ &=
|
||||
zwnj; ‌ ‌ ‌ ‌ ‌ &zwn=
|
||||
j; ‌ ‌ ‌ ‌ ‌ ‌&=
|
||||
nbsp;‌ ‌ ‌ ‌ ‌ ‌&nbs=
|
||||
p;‌ ‌ ‌ ‌ ‌ ‌ &=
|
||||
zwnj; ‌ ‌ ‌ ‌ ‌&nbs=
|
||||
p;‌ ‌ ‌ ‌ ‌ ‌ &=
|
||||
zwnj; ‌ ‌ ‌ ‌ ‌ &zwn=
|
||||
j; ‌ ‌ ‌ ‌ ‌ ‌&=
|
||||
nbsp;‌ ‌ ‌ ‌ ‌ ‌&nbs=
|
||||
p;‌ ‌ ‌ ‌ ‌ ‌ &=
|
||||
zwnj; ‌ ‌ ‌ ‌ ‌ &zwn=
|
||||
j; </div>
|
||||
<!-- End hidden preview text -->
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top;=
|
||||
padding-left: 30px;" class=3D"moPad"><table role=3D"presentation" width=3D"=
|
||||
100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-alig=
|
||||
n:bottom; padding:36px 0px 20px;"><img src=3D"https://www.chase.com/content=
|
||||
/dam/email/images/chase-logo-h-rgb.png" width=3D"104" height=3D"20" border=
|
||||
=3D"0" style=3D"display:block;" alt=3D"Chase Logo"/></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top;=
|
||||
padding: 20px 28px 0px;" class=3D"moPad"><table role=3D"presentation" alig=
|
||||
n=3D"left" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-alig=
|
||||
n:top;"><table role=3D"presentation" width=3D"100%" cellpadding=3D"0" cells=
|
||||
pacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertica=
|
||||
l-align:top; padding:5px 10px; font-family:Arial, Helvetica, sans-serif; fo=
|
||||
nt-size:12px; font-weight:bold; color:#414042; background-color:#D7DBE0; bo=
|
||||
rder-radius:20px; -moz-border-radius: 20px; -webkit-border-radius:20px; whi=
|
||||
te-space: nowrap;" class=3D"font14">Transaction alert</td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top;=
|
||||
"><table role=3D"presentation" width=3D"100%" cellpadding=3D"0" cellspacing=
|
||||
=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-alig=
|
||||
n:top; padding: 20px 30px 28px;" class=3D"moPad"><table role=3D"presentatio=
|
||||
n" width=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertica=
|
||||
l-align:top; padding: 0px 20px 0px 0px;"><img src=3D"https://static.chasecd=
|
||||
n.com/content/services/rendition/image.small.png/unified-assets/digital-car=
|
||||
ds/aarp/41473417018.png" width=3D"57" height=3D"auto" alt=3D"" border=3D"0"=
|
||||
style=3D"display:block;"/></td>
|
||||
<td align=3D"left" style=3D"vertica=
|
||||
l-align:top; padding:0px 50px 0px 0px; font-family:Arial, Helvetica, sans-s=
|
||||
erif; font-size:30px; font-weight: bold; color:#414042;" class=3D"zeroPad">=
|
||||
You made a $38.84 transaction</td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
<td align=3D"left" style=3D"vertical-align:bottom; padding:=
|
||||
0px 0px;width:12px; "><img src=3D"https://www.chase.com/content/dam/email/i=
|
||||
mages/blue-right.jpg" width=3D"12" height=3D"226" border=3D"0" style=3D"dis=
|
||||
play:block;" alt=3D""/></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"center" style=3D"vertical-align:top; padding: 0px 0p=
|
||||
x 0px; background-color: #FFFFFF;"><table role=3D"presentation" cellpadding=
|
||||
=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"right" style=3D"vertical-align:top; padding:0p=
|
||||
x 0px; width:12px;"><img src=3D"https://www.chase.com/content/dam/email/ima=
|
||||
ges/white-left.jpg" width=3D"12" height=3D"77" border=3D"0" style=3D"displa=
|
||||
y:block;" alt=3D""/></td>
|
||||
<td align=3D"center" style=3D"vertical-align:top; padding: =
|
||||
0px 0px 0px;width:616px;"><table role=3D"presentation" width=3D"100%" cellp=
|
||||
adding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top; paddi=
|
||||
ng: 0px 150px 0px 30px;" class=3D"moPad"><table role=3D"presentation" width=
|
||||
=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top;=
|
||||
padding: 10px 0px;border-bottom: solid 1px #414042;"><table role=3D"presen=
|
||||
tation" width=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-alig=
|
||||
n:top; padding:0px 0px 0px 0px; font-family:Arial, Helvetica, sans-serif; f=
|
||||
ont-size:16px; color:#414042;" class=3D"font14">Account</td>
|
||||
<td align=3D"right" style=3D"vertical-ali=
|
||||
gn:top; padding:0px 0px 0px 5px; font-family:Arial, Helvetica, sans-serif; =
|
||||
font-size:16px; font-weight:bold; color:#414042;" class=3D"font14">AARP fro=
|
||||
m Chase (...8824)</td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top;=
|
||||
padding: 10px 0px;border-bottom: solid 1px #414042;"><table role=3D"presen=
|
||||
tation" width=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-alig=
|
||||
n:top; padding:0px 0px 0px 0px; font-family:Arial, Helvetica, sans-serif; f=
|
||||
ont-size:16px; color:#414042;" class=3D"font14">Date</td>
|
||||
<td align=3D"right" style=3D"vertical-ali=
|
||||
gn:top; padding:0px 0px 0px 5px; font-family:Arial, Helvetica, sans-serif; =
|
||||
font-size:16px; font-weight:bold; color:#414042;" class=3D"font14"><span cl=
|
||||
ass=3D"applelinksgray41"><a style=3D"color:#414042;text-decoration: none;">=
|
||||
Jul 23, 2021 at 3:06 PM ET</td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top;=
|
||||
padding: 10px 0px;border-bottom: solid 1px #414042;"><table role=3D"presen=
|
||||
tation" width=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-alig=
|
||||
n:top; padding:0px 0px 0px 0px; font-family:Arial, Helvetica, sans-serif; f=
|
||||
ont-size:16px; color:#414042;" class=3D"font14">Merchant</td>
|
||||
<td align=3D"right" style=3D"vertical-ali=
|
||||
gn:top; padding:0px 0px 0px 5px; font-family:Arial, Helvetica, sans-serif; =
|
||||
font-size:16px; font-weight:bold; color:#414042;" class=3D"font14">TARGET T=
|
||||
-1754</td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top;=
|
||||
padding: 10px 0px;border-bottom: solid 1px #414042;"><table role=3D"presen=
|
||||
tation" width=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-alig=
|
||||
n:top; padding:0px 0px 0px 0px; font-family:Arial, Helvetica, sans-serif; f=
|
||||
ont-size:16px; color:#414042;" class=3D"font14">Amount</td>
|
||||
<td align=3D"right" style=3D"vertical-ali=
|
||||
gn:top; padding:0px 0px 0px 5px; font-family:Arial, Helvetica, sans-serif; =
|
||||
font-size:16px; font-weight:bold; color:#414042;" class=3D"font14">$38.84</=
|
||||
td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top; pad=
|
||||
ding:40px 150px 40px 30px; font-family:Arial, Helvetica, sans-serif; font-s=
|
||||
ize:16px; color:#414042;" class=3D"moPad">You are receiving this alert beca=
|
||||
use your transaction was more than the $0.00 level you set. You can visit o=
|
||||
ur <a style=3D"text-decoration: underline; color:#0060F0;" href=3D"https://=
|
||||
www.chase.com/personal/credit-cards/card-resource-center" rel=3D"noopener n=
|
||||
oreferrer" target=3D"_blank">Resource Center</a> anytime to help answer yo=
|
||||
ur questions or manage your account.</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"padding:0px; vertical-ali=
|
||||
gn:top; padding: 0px 0px 30px 30px;" class=3D"moPad"><table role=3D"present=
|
||||
ation" align=3D"left" cellpadding=3D"0" cellspacing=3D"0" border=3D"0" styl=
|
||||
e=3D"vertical-align:top;">
|
||||
<tr>
|
||||
<td role=3D"button" align=3D"center" style=3D"b=
|
||||
ackground-color:#0060f0; color: #fffffe; font-size: 16px; font-family: Aria=
|
||||
l, Helvetica, sans-serif; padding: 10px 0px; border: 1px solid #0060f0; ver=
|
||||
tical-align:top; border-radius:4px; -moz-border-radius: 4px; -webkit-border=
|
||||
-radius:4px;width: 168px;"><a href=3D"https://secure.chase.com/web/auth/nav=
|
||||
?navKey=3DrequestDashboard" target=3D"_blank" style=3D"color: #fffffe; text=
|
||||
-decoration:none;">Review account</a></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top; paddi=
|
||||
ng:0px 30px 20px; font-family:Arial, Helvetica, sans-serif; font-size:12px;=
|
||||
color:#717171;" class=3D"moPad font14">Securely access your accounts with =
|
||||
the <a style=3D"text-decoration: underline; color:#0060F0;" href=3D"https:/=
|
||||
/www.chase.com/digital/mobile-banking" rel=3D"noopener noreferrer" target=
|
||||
=3D"_blank">Chase Mobile<span style=3D"font-size:70%; line-height:0; v=
|
||||
ertical-align:3px; text-decoration: none;">®</span> app</a> or <a style=
|
||||
=3D"text-decoration: underline; color:#0060F0;" href=3D"https://secure.chas=
|
||||
e.com/web/auth/nav?navKey=3DrequestDashboard" rel=3D"noopener noreferrer" =
|
||||
target=3D"_blank">chase.com</a>. </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top; paddi=
|
||||
ng: 0px 0px; background-color: #F6F6F6;"><table role=3D"presentation" width=
|
||||
=3D"100%" cellpadding=3D"0" cellspacing=3D"0" border=3D"0">
|
||||
<tr>
|
||||
<td align=3D"left" style=3D"vertical-align:top;=
|
||||
padding:20px 30px 60px; font-family:Arial, Helvetica, sans-serif; font-siz=
|
||||
e:12px; color:#717171;" class=3D"moPad font14"><span role=3D"heading" style=
|
||||
=3D"text-transform: uppercase; font-weight: bold;">About this message</span=
|
||||
><br />
|
||||
<br />
|
||||
Chase Mobile<span style=3D"font-size:70%=
|
||||
; line-height:0; vertical-align:3px;">®</span>=C2=A0app is available f=
|
||||
or select mobile devices. Message and data rates may apply.<br />
|
||||
<br />
|
||||
This service email was sent based on your ale=
|
||||
rt settings. Use the Chase Mobile app or visit <a href=3D"https://www.=
|
||||
chase.com/personal/mobile-online-banking/login-alerts" target=3D"_blank" st=
|
||||
yle=3D"text-decoration: underline; color:#0060F0;" rel=3D"noopener noreferr=
|
||||
er">chase.com/alerts</a> to view or manage your settings.<br />
|
||||
<br />
|
||||
Chase cannot guarantee the delivery of alerts=
|
||||
and notifications.=C2=A0Wireless or internet service provider outages or o=
|
||||
ther circumstances could delay them. You can always check <span class=3D"ap=
|
||||
plelinksgray"><a style=3D"color:#717171;text-decoration: none;">chase.com</=
|
||||
a></span> or the Chase Mobile=C2=A0app for the status of your accounts=
|
||||
including your latest account balances and transaction details.=C2=A0<br /=
|
||||
>
|
||||
<br />
|
||||
To protect your personal information, please =
|
||||
don't reply to this message. Chase won't ask for confidential information i=
|
||||
n an email. <br />
|
||||
<br />
|
||||
If you have concerns about the authenticity o=
|
||||
f this message or have questions about your account visit <a style=3D"text-=
|
||||
decoration: underline; color:#0060F0;" href=3D"https://www.chase.com/digita=
|
||||
l/customer-service" target=3D"_blank" rel=3D"noopener noreferrer">chase.com=
|
||||
/CustomerService</a> for ways to contact us.<br />
|
||||
<br />
|
||||
Your privacy is important to us. See our onli=
|
||||
ne <a style=3D"text-decoration: underline; color:#0060F0;" href=3D"https://=
|
||||
www.chase.com/digital/resources/privacy-security" target=3D"_blank" rel=3D"=
|
||||
noopener noreferrer">Security Center</a> to learn how to protect your infor=
|
||||
mation.<br />
|
||||
<br />
|
||||
© 2021 JPMorgan Chase & Co. </td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
<td align=3D"left" style=3D"vertical-align:top; padding:0px=
|
||||
0px; width:12px;"><img src=3D"https://www.chase.com/content/dam/email/imag=
|
||||
es/white-right.jpg" width=3D"12" height=3D"77" border=3D"0" style=3D"displa=
|
||||
y:block;" alt=3D""/></td>
|
||||
</tr>
|
||||
</table></td>
|
||||
</tr>
|
||||
<!--End of Content -->
|
||||
=20
|
||||
</table></td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@@ -12,23 +12,25 @@ type Transaction struct {
|
||||
ID string
|
||||
Bank Bank
|
||||
Amount string
|
||||
Account string
|
||||
Vendor string
|
||||
Date string
|
||||
Account string
|
||||
}
|
||||
|
||||
func (t *Transaction) Format() string {
|
||||
return fmt.Sprintf("(%s) %v: %s @ %s", cleanDate(t.Date), t.Bank, t.Amount, t.Account)
|
||||
return fmt.Sprintf("(%s) %v/%v: %s @ %s", cleanDate(t.Date), t.Account, t.Bank, t.Amount, t.Vendor)
|
||||
}
|
||||
|
||||
func (t *Transaction) String() string {
|
||||
return fmt.Sprint(*t)
|
||||
return fmt.Sprintf("%+v", *t)
|
||||
}
|
||||
|
||||
func NewTransaction(amount, account, date string, bank Bank) *Transaction {
|
||||
func NewTransaction(account, amount, vendor, date string, bank Bank) *Transaction {
|
||||
regexp := regexp.MustCompile(`\s\s+`)
|
||||
t := &Transaction{
|
||||
Account: account,
|
||||
Amount: regexp.ReplaceAllString(amount, " "),
|
||||
Account: regexp.ReplaceAllString(account, " "),
|
||||
Vendor: regexp.ReplaceAllString(vendor, " "),
|
||||
Bank: bank,
|
||||
Date: date,
|
||||
}
|
||||
|
||||
2
transaction_test.go
Normal file → Executable file
2
transaction_test.go
Normal file → Executable file
@@ -3,7 +3,7 @@ package main
|
||||
import "testing"
|
||||
|
||||
func TestTransactionFormat(t *testing.T) {
|
||||
x := NewTransaction("12.34", "Amazon", "[Wed, 1 Apr 2020 10:14:11 -0400 (EDT)]", Chase)
|
||||
x := NewTransaction("me", "12.34", "Amazon", "[Wed, 1 Apr 2020 10:14:11 -0400 (EDT)]", Chase)
|
||||
t.Logf("%s", x.String())
|
||||
t.Logf("%s", x.Format())
|
||||
}
|
||||
|
||||
51
upload.go
51
upload.go
@@ -1,15 +1,31 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"local/oauth2"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func Upload(config Config, transaction *Transaction) error {
|
||||
switch config.Uploader {
|
||||
case UploaderTodo:
|
||||
return uploadTodo(config, transaction)
|
||||
case UploaderLedger:
|
||||
return uploadLedger(config, transaction)
|
||||
default:
|
||||
return errors.New("not impl: uploader")
|
||||
}
|
||||
}
|
||||
|
||||
func uploadTodo(config Config, transaction *Transaction) error {
|
||||
params := url.Values{
|
||||
"list": {config.TodoList},
|
||||
"title": {transaction.Format()},
|
||||
@@ -32,3 +48,38 @@ func Upload(config Config, transaction *Transaction) error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func uploadLedger(config Config, transaction *Transaction) error {
|
||||
f, err := os.OpenFile(config.TodoAddr, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
amount, _ := strconv.ParseFloat(transaction.Amount, 32)
|
||||
amount *= -1
|
||||
remote := "Withdrawal:"
|
||||
if amount > 0 {
|
||||
remote = "Deposit:"
|
||||
}
|
||||
regexp := regexp.MustCompile(`[0-9a-zA-Z]`)
|
||||
for _, substr := range regexp.FindAllString(transaction.Vendor, -1) {
|
||||
remote += substr
|
||||
}
|
||||
fmt.Fprintf(f, "%-50s%-s\n", formatGMailDate(transaction.Date), transaction.Vendor)
|
||||
fmt.Fprintf(f, "%-50s%-50s$%.2f\n", "", "AssetAccount:"+transaction.Bank.String()+":"+transaction.Account, amount)
|
||||
fmt.Fprintf(f, "%-50s%-s\n", "", remote)
|
||||
return nil
|
||||
}
|
||||
|
||||
func formatGMailDate(s string) string {
|
||||
for _, format := range []string{
|
||||
"[Mon, 2 Jan 2006 15:04:05 -0700 (MST)]",
|
||||
"[Mon, 2 Jan 2006 15:04:05 -0700]",
|
||||
} {
|
||||
time, err := time.Parse(format, s)
|
||||
if err == nil {
|
||||
return time.Format("2006-01-02")
|
||||
}
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
75
upload_test.go
Normal file
75
upload_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"local/storage"
|
||||
"path"
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func TestUploadLedger(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
transaction Transaction
|
||||
}{
|
||||
"simple": {
|
||||
transaction: Transaction{
|
||||
ID: uuid.New().String(),
|
||||
Bank: Chase,
|
||||
Amount: "1.10",
|
||||
Vendor: "my's totally realistic!!! VENDOR 11",
|
||||
Date: "today",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for name, d := range cases {
|
||||
c := d
|
||||
t.Run(name, func(t *testing.T) {
|
||||
config := Config{
|
||||
TodoAddr: path.Join(t.TempDir(), name),
|
||||
Uploader: UploaderLedger,
|
||||
Storage: storage.NewMap(),
|
||||
}
|
||||
if err := uploadLedger(config, &c.transaction); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, err := ioutil.ReadFile(config.TodoAddr)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Contains(b, []byte("mystotallyrealisticVENDOR11")) {
|
||||
t.Fatal(string(b))
|
||||
}
|
||||
t.Logf("\n%s", b)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatGMailDate(t *testing.T) {
|
||||
cases := map[string]struct {
|
||||
input string
|
||||
want string
|
||||
}{
|
||||
"ok": {
|
||||
input: "[Tue, 20 Jul 2021 23:46:00 -0400 (EDT)]",
|
||||
want: "2021-07-20",
|
||||
},
|
||||
"bad": {
|
||||
input: "2021-07-20",
|
||||
want: "2021-07-20",
|
||||
},
|
||||
}
|
||||
|
||||
for name, d := range cases {
|
||||
c := d
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got := formatGMailDate(c.input)
|
||||
if got != c.want {
|
||||
t.Fatal(c.input, c.want, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user