4 Commits
v0.8 ... v0.10

Author SHA1 Message Date
bel
c6d88f6abe impl bofa payment 2021-09-10 09:12:26 -06:00
bel
0449e7bdaa shrink 2021-09-08 14:41:41 -06:00
Bel LaPointe
10bc441e1e bankofamerica impl charges 2021-09-07 15:46:01 -06:00
Bel LaPointe
6f3bf1f6a4 dont negate payment 2021-09-07 15:03:38 -06:00
8 changed files with 287 additions and 109 deletions

View File

@@ -6,10 +6,13 @@ const (
Chase Bank = iota + 1 Chase Bank = iota + 1
Citi Bank = iota + 1 Citi Bank = iota + 1
UCCU Bank = iota + 1 UCCU Bank = iota + 1
BankOfAmerica Bank = iota + 1
) )
func (b Bank) String() string { func (b Bank) String() string {
switch b { switch b {
case BankOfAmerica:
return "BankOfAmerica"
case Chase: case Chase:
return "Chase" return "Chase"
case Citi: case Citi:

View File

@@ -7,6 +7,7 @@ import (
"local/args" "local/args"
"local/oauth2" "local/oauth2"
"local/storage" "local/storage"
"log"
"net/http" "net/http"
"strings" "strings"
) )
@@ -55,7 +56,7 @@ func NewConfig() Config {
as.Append(args.STRING, "todolist", "todo list", "") as.Append(args.STRING, "todolist", "todo list", "")
as.Append(args.STRING, "todotag", "todo tag", "expense") as.Append(args.STRING, "todotag", "todo tag", "expense")
as.Append(args.STRING, "banks", "uccu,citi,chase", "uccu,citi,chase") 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, "accounts", "regex to include filter accounts", ".*")
as.Append(args.STRING, "not-accounts", "regex to exclude filter accounts", "zzzzzz") as.Append(args.STRING, "not-accounts", "regex to exclude filter accounts", "zzzzzz")
@@ -91,11 +92,13 @@ func NewConfig() Config {
Storage: storage, Storage: storage,
Uploader: ul, Uploader: ul,
Banks: map[Bank]bool{ 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())), Chase: strings.Contains(strings.ToLower(as.GetString("banks")), strings.ToLower(Chase.String())),
Citi: strings.Contains(strings.ToLower(as.GetString("banks")), strings.ToLower(Citi.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())), UCCU: strings.Contains(strings.ToLower(as.GetString("banks")), strings.ToLower(UCCU.String())),
}, },
} }
log.Printf("config: %+v", config)
if config.Uploader == UploaderTodo { if config.Uploader == UploaderTodo {
token := as.GetString("todotoken") token := as.GetString("todotoken")

View File

@@ -15,6 +15,7 @@ type scraper interface {
scrape(*mail.Message) ([]*Transaction, error) scrape(*mail.Message) ([]*Transaction, error)
} }
type bankOfAmericaScraper struct{}
type chaseScraper struct{} type chaseScraper struct{}
type citiScraper struct{} type citiScraper struct{}
type uccuScraper struct{} type uccuScraper struct{}
@@ -36,6 +37,9 @@ func buildScraper(m *mail.Message, banks map[Bank]bool) (scraper, error) {
if strings.Contains(from, "Chase") && banks[Chase] { if strings.Contains(from, "Chase") && banks[Chase] {
return newChaseScraper(), nil return newChaseScraper(), nil
} }
if strings.Contains(from, "Bank of America") && banks[BankOfAmerica] {
return newBankOfAmericaScraper(), nil
}
if strings.Contains(from, "Citi") && banks[Citi] { if strings.Contains(from, "Citi") && banks[Citi] {
return newCitiScraper(), nil return newCitiScraper(), nil
} }
@@ -45,6 +49,10 @@ func buildScraper(m *mail.Message, banks map[Bank]bool) (scraper, error) {
return nil, errors.New("unknown sender: " + from) return nil, errors.New("unknown sender: " + from)
} }
func newBankOfAmericaScraper() scraper {
return &bankOfAmericaScraper{}
}
func newChaseScraper() scraper { func newChaseScraper() scraper {
return &chaseScraper{} return &chaseScraper{}
} }
@@ -94,6 +102,7 @@ func (c *chaseScraper) scrape2021Payment(m *mail.Message) ([]*Transaction, error
re = regexp.MustCompile(`\$[0-9]+\.[0-9]{2}`) re = regexp.MustCompile(`\$[0-9]+\.[0-9]{2}`)
amount := "-" + strings.TrimLeft(string(re.Find(b)), "$") amount := "-" + strings.TrimLeft(string(re.Find(b)), "$")
amount = strings.TrimLeft(string(re.Find(b)), "$")
vendor := "Payment" vendor := "Payment"
@@ -237,3 +246,63 @@ func (c *uccuScraper) scrape(m *mail.Message) ([]*Transaction, error) {
transaction := NewTransaction(UCCU.String(), fmt.Sprintf("%.2f", f), "?", fmt.Sprint(m.Header["Date"]), UCCU) transaction := NewTransaction(UCCU.String(), fmt.Sprintf("%.2f", f), "?", fmt.Sprint(m.Header["Date"]), UCCU)
return []*Transaction{transaction}, nil 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)
}
if strings.Contains(subject, "Credit Card Payment") {
return c.scrapePayment(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 := c.findFloatAfter(b, "Amount: $")
acc := string(c.findLineAfter(b, "Where: "))
if amount == "" || acc == "" {
return nil, errors.New("no amount/account found")
}
transaction := NewTransaction(BankOfAmerica.String(), amount, acc, fmt.Sprint(m.Header["Date"]), BankOfAmerica)
return []*Transaction{transaction}, nil
}
func (c *bankOfAmericaScraper) scrapePayment(m *mail.Message) ([]*Transaction, error) {
b, err := ioutil.ReadAll(m.Body)
if err != nil {
return nil, err
}
amount := "-" + c.findFloatAfter(b, "Payment: $")
acc := "Payment"
if amount == "" || acc == "" {
return nil, errors.New("no amount/account found")
}
transaction := NewTransaction(BankOfAmerica.String(), amount, acc, fmt.Sprint(m.Header["Date"]), BankOfAmerica)
return []*Transaction{transaction}, nil
}
func (c *bankOfAmericaScraper) findFloatAfter(b []byte, prefix string) string {
amount := string(c.findLineAfter(b, prefix))
words := strings.Split(amount, " ")
lastword := words[len(words)-1]
escapedfloat := strings.TrimPrefix(lastword, "$")
fixEscape := strings.ReplaceAll(escapedfloat, "=2E", ".")
amount = fixEscape
return amount
}
func (c *bankOfAmericaScraper) findLineAfter(b []byte, prefix string) []byte {
for _, line := range bytes.Split(b, []byte("\n")) {
if bytes.HasPrefix(line, []byte(prefix)) {
return bytes.TrimSpace(bytes.TrimPrefix(line, []byte(prefix)))
}
}
return nil
}

View File

@@ -108,3 +108,73 @@ func TestScrapeChase2020(t *testing.T) {
} }
t.Logf("%+v", got) t.Logf("%+v", got)
} }
func TestScrapeBofAPayment(t *testing.T) {
b, err := ioutil.ReadFile("./testdata/bofa.payment.txt")
if err != nil {
t.Fatal(err)
}
message := &mail.Message{
Header: map[string][]string{
"Subject": []string{"Confirmation: Thanks for Your Credit Card Payment"},
},
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: "-251.74",
Vendor: "Payment",
Date: "[]",
Account: BankOfAmerica.String(),
}
if *got != want {
t.Fatalf("want:\n\t%+v, got\n\t%+v", want, *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
View 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

88
testdata/bofa.payment.txt vendored Normal file
View File

@@ -0,0 +1,88 @@
Delivered-To: breellocaldev@gmail.com
Received: by 2002:a4f:f556:0:0:0:0:0 with SMTP id s22csp88584ivo;
Fri, 10 Sep 2021 03:09:37 -0700 (PDT)
X-Google-Smtp-Source: ABdhPJy5KOCCQILLhifnSPNnjMikzSGgZX0rSLKqSzdRkpjWyZAZB7Ml4gWSWxuiMPuJMUFQZPnF
X-Received: by 2002:a05:620a:15e8:: with SMTP id p8mr6940748qkm.27.1631268577237;
Fri, 10 Sep 2021 03:09:37 -0700 (PDT)
ARC-Seal: i=1; a=rsa-sha256; t=1631268577; cv=none;
d=google.com; s=arc-20160816;
b=fRiwZLXmORGlNgDHdYZ3g7DbcggjP3zVkUX1gIVHo3z/c4SLgmwu1FVu4qiUr7M2+6
9Ez7xjq0rG3JCLUk77q4I2MJW9pWL5LZdcMtoP9bbu5KYoZ0JwLQldFuzUOFp1qyLICc
pegPsozU1lTG3WSr2fxAi4kGgvr1PQUGd5EaeztK+u7I9SNyyOdXsgavbx0Dr+XLFAyG
eGo1WzDGy7NG8TMstFxQu+cfZiWKKtEeTFUGEjcXAUxCm/jvqK8MT1fPTwac9c66cCls
7bvBpXlmoSEmTz6NseH0DblgWZsdmGgkYZhIUS2cJaqIhGJUFxNqbMQswEXT29LrmnbG
zz0w==
ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20160816;
h=content-transfer-encoding:feedback-id:message-id:list-id:reply-to
:mime-version:date:subject:to:from:dkim-signature;
bh=RNAxijcTyDxYmdzbEHMEKXkbCbc/Wnnsez0HenNHbUA=;
b=KtJdTZ9LFxfJTwq0gldceho7ktEybby+DLrKgjgjI2yUlaS4u0IJC2nDvkA21HjV1w
R2HMT4UITrQVoi9xa/fTsbdVIfEDjBl2rdbvO+gOthaonsCvxAsiQGFRPhmKHlbb1IiE
9GbgjYaf4qEZCO4nQUnMKTQPK+TalO1pX3UNPHf2/KTeAuXCUrySVKgierhZIxnkS3WQ
/GUsV4gDHMhmRKEQF8yxgLv3podfCm63iOBgOZ/CCITcKkQTFUByLQ2HdAmLo9TUXHNM
sJKv6pK7e05Dxp4ZeNKlm15c5xSo3OXoRqupvsXYCbzjvR2moBrVRSB7iwZHGL45zQXQ
OVug==
ARC-Authentication-Results: i=1; mx.google.com;
dkim=pass header.i=@ealerts.bankofamerica.com header.s=200608 header.b=niVgyX92;
spf=pass (google.com: domain of bounce-29_html-819616257-1667962-73720-2833596@bounce.ealerts.bankofamerica.com designates 68.232.194.2 as permitted sender) smtp.mailfrom=bounce-29_HTML-819616257-1667962-73720-2833596@bounce.ealerts.bankofamerica.com;
dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=bankofamerica.com
Return-Path: <bounce-29_HTML-819616257-1667962-73720-2833596@bounce.ealerts.bankofamerica.com>
Received: from mta5.ealerts.bankofamerica.com (mta5.ealerts.bankofamerica.com. [68.232.194.2])
by mx.google.com with ESMTPS id a7si3031949qtn.85.2021.09.10.03.09.36
for <breellocaldev@gmail.com>
(version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128);
Fri, 10 Sep 2021 03:09:37 -0700 (PDT)
Received-SPF: pass (google.com: domain of bounce-29_html-819616257-1667962-73720-2833596@bounce.ealerts.bankofamerica.com designates 68.232.194.2 as permitted sender) client-ip=68.232.194.2;
Authentication-Results: mx.google.com;
dkim=pass header.i=@ealerts.bankofamerica.com header.s=200608 header.b=niVgyX92;
spf=pass (google.com: domain of bounce-29_html-819616257-1667962-73720-2833596@bounce.ealerts.bankofamerica.com designates 68.232.194.2 as permitted sender) smtp.mailfrom=bounce-29_HTML-819616257-1667962-73720-2833596@bounce.ealerts.bankofamerica.com;
dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=bankofamerica.com
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; s=200608; d=ealerts.bankofamerica.com;
h=From:To:Subject:Date:MIME-Version:Reply-To:List-ID:X-CSA-Complaints:
Message-ID:Content-Type:Content-Transfer-Encoding;
i=onlinebanking@ealerts.bankofamerica.com;
bh=RNAxijcTyDxYmdzbEHMEKXkbCbc/Wnnsez0HenNHbUA=;
b=niVgyX923ETmQwhHEaUcs91DEv/nznIH0c7CyqIgwu0h5KtgJZIKbkIw3inZNwLL9hF+/7lfV57q
ZYXmHQVV1aXIqJLQDD5RlAq2YZvghgLdglRBbq5N9cCDTsKIA3VlrKicwN+sAwDq2JlfBv4I8rzw
Vcmfup5eqf0vJnn6k9c=
Received: by mta5.ealerts.bankofamerica.com id h7cne22fmd4j for <breellocaldev@gmail.com>; Fri, 10 Sep 2021 10:09:35 +0000 (envelope-from <bounce-29_HTML-819616257-1667962-73720-2833596@bounce.ealerts.bankofamerica.com>)
From: "Bank of America" <onlinebanking@ealerts.bankofamerica.com>
To: <breellocaldev@gmail.com>
Subject: Confirmation: Thanks for Your Credit Card Payment
Date: Fri, 10 Sep 2021 04:09:32 -0600
MIME-Version: 1.0
Reply-To: "Bank of America" <reply-fe8a157673630d7b77-29_HTML-819616257-73720-2833596@ealerts.bankofamerica.com>
List-ID: <71108.xt.local>
X-CSA-Complaints: whitelistcomplaints@eco.de
x-job: 73720_1667962
Message-ID: <3fb2377b-699b-411c-9ea5-a3b8817aa853@las1s04mta1081.xt.local>
Feedback-ID: 73720:1667962:68.232.194.2:sfmktgcld
Content-Type: text/plain;
charset="iso-8859-1"
Content-Transfer-Encoding: 7bit
Hi, BEL, we've received your credit card payment
Payment: $251.74
To: National Education Association World Mas ending in - 7522
Date posted: September 09, 2021
Sign in to bankofamerica.com to view your account details.
Thank you for being our customer.
We'll never ask for your personal information such as SSN or ATM PIN in
email messages. If you get an email that looks suspicious or you are
not the intended recipient of this email, don't click on any links.
Instead, forward to abuse@bankofamerica.com then delete it.
Please don't reply to this automatically generated service email.
Read our Privacy Notice https://www.bankofamerica.com/privacy/consumer-privacy-notice.go
Equal Housing Lender: https://www.bankofamerica.com/help/equalhousing.cfm
Bank of America, N.A. Member FDIC
(C) 2019 Bank of America Corporation

View File

@@ -8,107 +8,6 @@ w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=3D1.= <meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=3D1.=
0"/> 0"/>
<title>This payment has been applied to your account.</title> <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> </head>
<body style=3D"padding: 0px;margin: 0px; background-color:#d7dbe0;"> <body style=3D"padding: 0px;margin: 0px; background-color:#d7dbe0;">
<table align=3D"center" width=3D"100%" border=3D"0" cellspacing=3D"0" cellp= <table align=3D"center" width=3D"100%" border=3D"0" cellspacing=3D"0" cellp=

View File

@@ -22,7 +22,7 @@ func (t *Transaction) Format() string {
} }
func (t *Transaction) String() string { func (t *Transaction) String() string {
return fmt.Sprint(*t) return fmt.Sprintf("%+v", *t)
} }
func NewTransaction(account, amount, vendor, date string, bank Bank) *Transaction { func NewTransaction(account, amount, vendor, date string, bank Bank) *Transaction {