10 Commits
v0.3 ... v0.8

Author SHA1 Message Date
Bel LaPointe
09f14ec44c chase payments a go, update password to app password 2021-07-31 11:35:15 -06:00
bel
3077343c89 add anti-patterns, print acc for ledger, fix chase 2021 from body.read double 2021-07-30 06:28:21 -06:00
bel
24b66d1c46 filter accounts by pattern 2021-07-30 00:07:34 -06:00
bel
a6c5121d54 check ledger output a little bit 2021-07-29 23:42:16 -06:00
bel
1ebaf9eac8 test new and old scrape for chase 2021-07-29 23:38:27 -06:00
bel
cea7a30884 chagne account to vendor 2021-07-29 22:58:01 -06:00
bel
c5c77a2b9b write banks diff for ledger, filter by bank 2021-07-29 22:50:17 -06:00
bel
bbd51ea9c5 support ledger file append 2021-07-29 22:24:30 -06:00
Bel LaPointe
f32fb5aad1 Impl uccu 2020-07-13 14:55:20 -06:00
Bel LaPointe
4006975a21 Update chase scrape 2020-07-01 05:26:20 -06:00
12 changed files with 1291 additions and 67 deletions

View File

@@ -5,6 +5,7 @@ type Bank int
const (
Chase Bank = iota + 1
Citi Bank = iota + 1
UCCU Bank = iota + 1
)
func (b Bank) String() string {
@@ -13,6 +14,8 @@ func (b Bank) String() string {
return "Chase"
case Citi:
return "Citi"
case UCCU:
return "UCCU"
}
return "?"
}

View File

@@ -11,15 +11,31 @@ import (
"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 +44,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", "uccu,citi,chase")
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 +69,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 +81,36 @@ 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{
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())),
},
}
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
View File

@@ -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 {

187
scrape.go
View File

@@ -7,6 +7,7 @@ import (
"io/ioutil"
"net/mail"
"regexp"
"strconv"
"strings"
)
@@ -16,27 +17,31 @@ type scraper interface {
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, "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)
}
@@ -44,6 +49,10 @@ func newChaseScraper() scraper {
return &chaseScraper{}
}
func newUCCUScraper() scraper {
return &uccuScraper{}
}
func newCitiScraper() scraper {
return &citiScraper{}
}
@@ -58,67 +67,173 @@ 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)), "$")
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)
}
}
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
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
}

110
scrape_test.go Normal file
View File

@@ -0,0 +1,110 @@
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)
}

10
testdata/chase.2020.txt vendored Normal file
View 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
View 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 &zwnj;&nbsp; after hidden preview tex=
t -->
<div style=3D"display: none; max-height: 0px; ove=
rflow: hidden;"> &nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwn=
j;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&=
nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwn=
j;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&=
nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbs=
p;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&=
zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwn=
j;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&=
nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbs=
p;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&=
zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&nbsp;&zwnj;&nbs=
p;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&=
zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwn=
j;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&=
nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbs=
p;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&=
zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwn=
j;&nbsp;</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&nbsp;Mobile<span style=3D"font-size:70%; line-height:0; v=
ertical-align:3px; text-decoration: none;">&reg;</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&nbsp;Mobile<span style=3D"font-size:70%=
; line-height:0; vertical-align:3px;">&reg;</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&nbsp;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&nbsp;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 />
&copy; 2021 JPMorgan Chase &amp; 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
View 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 &zwnj;&nbsp; after hidden preview tex=
t -->
<div style=3D"display: none; max-height: 0px; ove=
rflow: hidden;"> &nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwn=
j;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&=
nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwn=
j;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&=
nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbs=
p;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&=
zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwn=
j;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&=
nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbs=
p;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&=
zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&nbsp;&zwnj;&nbs=
p;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&=
zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwn=
j;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&=
nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbs=
p;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&=
zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwnj;&nbsp;&zwn=
j;&nbsp;</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&nbsp;Mobile<span style=3D"font-size:70%; line-height:0; v=
ertical-align:3px; text-decoration: none;">&reg;</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&nbsp;Mobile<span style=3D"font-size:70%=
; line-height:0; vertical-align:3px;">&reg;</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&nbsp;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&nbsp;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 />
&copy; 2021 JPMorgan Chase &amp; 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>

View File

@@ -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)
}
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
View 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())
}

View File

@@ -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
View 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)
}
})
}
}