5 Commits
v0.14 ... v0.15

Author SHA1 Message Date
Bel LaPointe
1e01058c7c add fidelity to config 2022-12-09 09:58:19 -07:00
Bel LaPointe
5c557ea713 impl fidel deposit 2022-12-09 09:52:22 -07:00
Bel LaPointe
8b226294a2 can read fidel withdrawal emails 2022-12-09 09:48:04 -07:00
Bel LaPointe
f6fc366dd4 stub wip fidelity 2022-12-09 08:52:36 -07:00
Bel LaPointe
1139fef0ab tidy 2022-12-09 08:44:30 -07:00
9 changed files with 234 additions and 24 deletions

View File

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

View File

@@ -30,6 +30,7 @@ type Config struct {
EmailUser string EmailUser string
EmailPass string EmailPass string
EmailIMAP string EmailIMAP string
EmailLimit int
TodoAddr string TodoAddr string
TodoToken string TodoToken string
TodoList string TodoList string
@@ -49,6 +50,7 @@ func NewConfig() Config {
as.Append(args.STRING, "emailuser", "email username", "breellocaldev@gmail.com") as.Append(args.STRING, "emailuser", "email username", "breellocaldev@gmail.com")
as.Append(args.STRING, "emailpass", "email password", "diblloewfncwssof") as.Append(args.STRING, "emailpass", "email password", "diblloewfncwssof")
as.Append(args.STRING, "emailimap", "email imap", "imap.gmail.com:993") as.Append(args.STRING, "emailimap", "email imap", "imap.gmail.com:993")
as.Append(args.INT, "emaillimit", "email limit", 0)
as.Append(args.STRING, "uploader", "todo, ledger, pttodo", "todo") as.Append(args.STRING, "uploader", "todo, ledger, pttodo", "todo")
@@ -58,7 +60,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,bankofamerica", "uccu,citi,chase,bankofamerica") as.Append(args.STRING, "banks", "uccu,citi,chase,bankofamerica,fidelity", "uccu,citi,chase,bankofamerica,fidelity")
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")
@@ -87,6 +89,7 @@ func NewConfig() Config {
EmailUser: as.GetString("emailuser"), EmailUser: as.GetString("emailuser"),
EmailPass: as.GetString("emailpass"), EmailPass: as.GetString("emailpass"),
EmailIMAP: as.GetString("emailimap"), EmailIMAP: as.GetString("emailimap"),
EmailLimit: as.GetInt("emaillimit"),
TodoAddr: as.GetString("todoaddr"), TodoAddr: as.GetString("todoaddr"),
TodoTag: as.GetString("todotag"), TodoTag: as.GetString("todotag"),
AccountsPattern: as.GetString("accounts"), AccountsPattern: as.GetString("accounts"),
@@ -98,6 +101,7 @@ func NewConfig() Config {
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())),
Fidelity: strings.Contains(strings.ToLower(as.GetString("banks")), strings.ToLower(Fidelity.String())),
}, },
} }
log.Printf("config: %+v", config) log.Printf("config: %+v", config)

7
go.mod
View File

@@ -4,6 +4,7 @@ go 1.17
require ( require (
github.com/google/uuid v1.3.0 github.com/google/uuid v1.3.0
gopkg.in/yaml.v2 v2.4.0
local/args v0.0.0-00010101000000-000000000000 local/args v0.0.0-00010101000000-000000000000
local/oauth2 v0.0.0-00010101000000-000000000000 local/oauth2 v0.0.0-00010101000000-000000000000
local/sandbox/contact/contact v0.0.0-00010101000000-000000000000 local/sandbox/contact/contact v0.0.0-00010101000000-000000000000
@@ -16,10 +17,7 @@ require (
github.com/abbot/go-http-auth v0.4.0 // indirect github.com/abbot/go-http-auth v0.4.0 // indirect
github.com/aws/aws-sdk-go v1.15.81 // indirect github.com/aws/aws-sdk-go v1.15.81 // indirect
github.com/boltdb/bolt v1.3.1 // indirect github.com/boltdb/bolt v1.3.1 // indirect
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b // indirect
github.com/buraksezer/consistent v0.9.0 // indirect
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e // indirect github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/emersion/go-imap v1.2.0 // indirect github.com/emersion/go-imap v1.2.0 // indirect
github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect github.com/emersion/go-sasl v0.0.0-20200509203442-7bfe0ed36a21 // indirect
github.com/go-stack/stack v1.8.0 // indirect github.com/go-stack/stack v1.8.0 // indirect
@@ -52,13 +50,12 @@ require (
golang.org/x/net v0.0.0-20190522155817-f3200d17e092 // indirect golang.org/x/net v0.0.0-20190522155817-f3200d17e092 // indirect
golang.org/x/oauth2 v0.0.0-20181120190819-8f65e3013eba // indirect golang.org/x/oauth2 v0.0.0-20181120190819-8f65e3013eba // indirect
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e // indirect
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 // indirect golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect
golang.org/x/text v0.3.7 // indirect golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect golang.org/x/time v0.0.0-20210723032227-1f47c861a9ac // indirect
google.golang.org/api v0.0.0-20181120235003-faade3cbb06a // indirect google.golang.org/api v0.0.0-20181120235003-faade3cbb06a // indirect
google.golang.org/appengine v1.3.0 // indirect google.golang.org/appengine v1.3.0 // indirect
gopkg.in/ini.v1 v1.42.0 // indirect gopkg.in/ini.v1 v1.42.0 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
local/logb v0.0.0-00010101000000-000000000000 // indirect local/logb v0.0.0-00010101000000-000000000000 // indirect
) )

13
go.sum
View File

@@ -5,8 +5,6 @@ github.com/Azure/azure-pipeline-go v0.1.8/go.mod h1:XA1kFWRVhSK+KNFiOhfv83Fv8L9a
github.com/Azure/azure-storage-blob-go v0.0.0-20181023070848-cf01652132cc/go.mod h1:oGfmITT1V6x//CswqY2gtAHND+xIP64/qL7a5QJix0Y= github.com/Azure/azure-storage-blob-go v0.0.0-20181023070848-cf01652132cc/go.mod h1:oGfmITT1V6x//CswqY2gtAHND+xIP64/qL7a5QJix0Y=
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/OneOfOne/xxhash v1.2.2 h1:KMrpdQIwFcEqXDklaen+P1axHaj9BSKzvpUUfnHldSE=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Unknwon/goconfig v0.0.0-20181105214110-56bd8ab18619 h1:6X8iB881g299aNEv6KXrcjL31iLOH7yA6NXoQX+MbDg= github.com/Unknwon/goconfig v0.0.0-20181105214110-56bd8ab18619 h1:6X8iB881g299aNEv6KXrcjL31iLOH7yA6NXoQX+MbDg=
github.com/Unknwon/goconfig v0.0.0-20181105214110-56bd8ab18619/go.mod h1:wngxua9XCNjvHjDiTiV26DaKDT+0c63QR6H5hjVUUxw= github.com/Unknwon/goconfig v0.0.0-20181105214110-56bd8ab18619/go.mod h1:wngxua9XCNjvHjDiTiV26DaKDT+0c63QR6H5hjVUUxw=
github.com/a8m/tree v0.0.0-20180321023834-3cf936ce15d6/go.mod h1:FSdwKX97koS5efgm8WevNf7XS3PqtyFkKDDXrz778cg= github.com/a8m/tree v0.0.0-20180321023834-3cf936ce15d6/go.mod h1:FSdwKX97koS5efgm8WevNf7XS3PqtyFkKDDXrz778cg=
@@ -18,14 +16,8 @@ github.com/aws/aws-sdk-go v1.15.81/go.mod h1:E3/ieXAlvM0XWO57iftYVDLLvQ824smPP3A
github.com/billziss-gh/cgofuse v1.1.0/go.mod h1:LJjoaUojlVjgo5GQoEJTcJNqZJeRU0nCR84CyxKt2YM= github.com/billziss-gh/cgofuse v1.1.0/go.mod h1:LJjoaUojlVjgo5GQoEJTcJNqZJeRU0nCR84CyxKt2YM=
github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b h1:L/QXpzIa3pOvUGt1D1lA5KjYhPBAN/3iWdP7xeFS9F0=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/buraksezer/consistent v0.9.0 h1:Zfs6bX62wbP3QlbPGKUhqDw7SmNkOzY5bHZIYXYpR5g=
github.com/buraksezer/consistent v0.9.0/go.mod h1:6BrVajWq7wbKZlTOUPs/XVfR8c0maujuPowduSpZqmw=
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e h1:mQTN05gz0rDZSABqKMzAPMb5ATWcvvdMljRzEh0LjBo= github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e h1:mQTN05gz0rDZSABqKMzAPMb5ATWcvvdMljRzEh0LjBo=
github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e/go.mod h1:alXX+s7a4cKaIprgjeEboqi4Tm7XR/HXEwUTxUV/ywU= github.com/bytbox/go-pop3 v0.0.0-20120201222208-3046caf0763e/go.mod h1:alXX+s7a4cKaIprgjeEboqi4Tm7XR/HXEwUTxUV/ywU=
github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko=
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
github.com/coreos/bbolt v0.0.0-20180318001526-af9db2027c98/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/bbolt v0.0.0-20180318001526-af9db2027c98/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
github.com/cpuguy83/go-md2man v1.0.8/go.mod h1:N6JayAiVKtlHSnuTCeuLSQVs75hb8q+dYQLjr7cDsKY= github.com/cpuguy83/go-md2man v1.0.8/go.mod h1:N6JayAiVKtlHSnuTCeuLSQVs75hb8q+dYQLjr7cDsKY=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -172,8 +164,6 @@ github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1
github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s= github.com/smartystreets/goconvey v0.0.0-20181108003508-044398e4856c/go.mod h1:XDJAKZRPZ1CvBcN2aX5YOUTYGHki24fSF0Iv48Ibg0s=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a h1:pa8hGb/2YqsZKovtsgrwcDH1RZhVbTKCjLp47XpqCDs=
github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/goconvey v0.0.0-20190330032615-68dc04aab96a/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 h1:qLC7fQah7D6K1B0ujays3HV9gkFtllcxhzImRR7ArPQ=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg= github.com/spf13/pflag v1.0.3 h1:zPAT6CGy6wXeQ7NtTnaTerfKOsV6V6F8agHXFiazDkg=
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
@@ -232,8 +222,9 @@ golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190419153524-e8e3143a4f4a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2 h1:T5DasATyLQfmbTpfEXx/IOL9vfjzW6up+ZDkmHvIf2s=
golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190531175056-4c3a928424d2/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60=
golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

View File

@@ -12,6 +12,7 @@ func main() {
IMAP: config.EmailIMAP, IMAP: config.EmailIMAP,
From: config.EmailUser, From: config.EmailUser,
Password: config.EmailPass, Password: config.EmailPass,
Limit: config.EmailLimit,
} }
emails, err := emailer.Read() emails, err := emailer.Read()
if err != nil { if err != nil {

View File

@@ -15,6 +15,7 @@ type scraper interface {
scrape(*mail.Message) ([]*Transaction, error) scrape(*mail.Message) ([]*Transaction, error)
} }
type fidelityScraper struct{}
type bankOfAmericaScraper struct{} type bankOfAmericaScraper struct{}
type chaseScraper struct{} type chaseScraper struct{}
type citiScraper struct{} type citiScraper struct{}
@@ -30,13 +31,16 @@ func Scrape(m *mail.Message, banks map[Bank]bool) ([]*Transaction, error) {
func buildScraper(m *mail.Message, banks map[Bank]bool) (scraper, error) { func buildScraper(m *mail.Message, banks map[Bank]bool) (scraper, error) {
subject := fmt.Sprint(m.Header["Subject"]) subject := fmt.Sprint(m.Header["Subject"])
if !containsAny(subject, "transaction", "report", "Transaction", "payment", "Payment") { if !containsAny(subject, "transaction", "report", "Transaction", "payment", "Payment", "Deposit", "Withdrawal") {
return nil, errors.New("cannot build scraper for subject " + subject) return nil, errors.New("cannot build scraper for subject " + subject)
} }
from := fmt.Sprint(m.Header["From"]) from := fmt.Sprint(m.Header["From"])
if strings.Contains(from, "Chase") && banks[Chase] { if strings.Contains(from, "Chase") && banks[Chase] {
return newChaseScraper(), nil return newChaseScraper(), nil
} }
if strings.Contains(from, "Fidelity") && banks[Fidelity] {
return newFidelityScraper(), nil
}
if strings.Contains(from, "Bank of America") && banks[BankOfAmerica] { if strings.Contains(from, "Bank of America") && banks[BankOfAmerica] {
return newBankOfAmericaScraper(), nil return newBankOfAmericaScraper(), nil
} }
@@ -49,6 +53,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 newFidelityScraper() scraper {
return &fidelityScraper{}
}
func newBankOfAmericaScraper() scraper { func newBankOfAmericaScraper() scraper {
return &bankOfAmericaScraper{} return &bankOfAmericaScraper{}
} }
@@ -210,6 +218,52 @@ func (c *uccuScraper) scrape(m *mail.Message) ([]*Transaction, error) {
return []*Transaction{transaction}, nil return []*Transaction{transaction}, nil
} }
func (c *fidelityScraper) scrape(m *mail.Message) ([]*Transaction, error) {
subject := fmt.Sprint(m.Header["Subject"])
if strings.Contains(subject, "Debit Withdrawal") {
return c.scrapeWithdrawal(m)
}
if strings.Contains(subject, "Deposit Received") {
return c.scrapeDeposit(m)
}
panic(nil)
}
func (c *fidelityScraper) scrapeDeposit(m *mail.Message) ([]*Transaction, error) {
b, err := ioutil.ReadAll(m.Body)
if err != nil {
return nil, err
}
fidelAcc, _ := findSubstringBetween(b, "Account: XXXXX", "\n")
transaction := NewTransaction(
fmt.Sprintf("%s-%s", Fidelity, fidelAcc),
"?.??",
"misc",
fmt.Sprint(m.Header["Date"]),
Fidelity,
)
return []*Transaction{transaction}, nil
}
func (c *fidelityScraper) scrapeWithdrawal(m *mail.Message) ([]*Transaction, error) {
b, err := ioutil.ReadAll(m.Body)
if err != nil {
return nil, err
}
amount, amountOk := findSubstringBetween(b, "in the amount of $", " ")
fidelAcc, fidelAccOk := findSubstringBetween(b, "For account ending in ", ":")
acc, accOk := findSubstringBetween(b, "in the amount of $"+amount+" by ", ".")
if amount == "" || acc == "" {
return nil, fmt.Errorf("no amount/account found: fidelAcc=%v,fidelAccOk=%v, acc=%v,accOk=%v, amount=%v,amountOk=%v", fidelAcc, fidelAccOk, acc, accOk, amount, amountOk)
}
transaction := NewTransaction(fmt.Sprintf("%s-%s", Fidelity, fidelAcc), amount, acc, fmt.Sprint(m.Header["Date"]), Fidelity)
return []*Transaction{transaction}, nil
}
func (c *bankOfAmericaScraper) scrape(m *mail.Message) ([]*Transaction, error) { func (c *bankOfAmericaScraper) scrape(m *mail.Message) ([]*Transaction, error) {
subject := fmt.Sprint(m.Header["Subject"]) subject := fmt.Sprint(m.Header["Subject"])
if strings.Contains(subject, "Credit card transaction") { if strings.Contains(subject, "Credit card transaction") {
@@ -227,8 +281,8 @@ func (c *bankOfAmericaScraper) scrapeCharge(m *mail.Message) ([]*Transaction, er
return nil, err return nil, err
} }
amount := c.findFloatAfter(b, "Amount: $") amount := findFloatAfter(b, "Amount: $")
acc := string(c.findLineAfter(b, "Where: ")) acc := string(findLineAfter(b, "Where: "))
if amount == "" || acc == "" { if amount == "" || acc == "" {
return nil, errors.New("no amount/account found") return nil, errors.New("no amount/account found")
@@ -242,7 +296,7 @@ func (c *bankOfAmericaScraper) scrapePayment(m *mail.Message) ([]*Transaction, e
if err != nil { if err != nil {
return nil, err return nil, err
} }
amount := "-" + c.findFloatAfter(b, "Payment: $") amount := "-" + findFloatAfter(b, "Payment: $")
acc := "Payment" acc := "Payment"
if amount == "" || acc == "" { if amount == "" || acc == "" {
return nil, errors.New("no amount/account found") return nil, errors.New("no amount/account found")
@@ -251,8 +305,20 @@ func (c *bankOfAmericaScraper) scrapePayment(m *mail.Message) ([]*Transaction, e
return []*Transaction{transaction}, nil return []*Transaction{transaction}, nil
} }
func (c *bankOfAmericaScraper) findFloatAfter(b []byte, prefix string) string { func findSubstringBetween(b []byte, prefix, suffix string) (string, bool) {
amount := string(c.findLineAfter(b, prefix)) byPre := bytes.Split(b, []byte(prefix))
if len(byPre) < 2 {
return "", false
}
bySuff := bytes.Split(byPre[1], []byte(suffix))
if len(bySuff) < 2 {
return "", false
}
return string(bySuff[0]), true
}
func findFloatAfter(b []byte, prefix string) string {
amount := string(findLineAfter(b, prefix))
words := strings.Split(amount, " ") words := strings.Split(amount, " ")
lastword := words[len(words)-1] lastword := words[len(words)-1]
escapedfloat := strings.TrimPrefix(lastword, "$") escapedfloat := strings.TrimPrefix(lastword, "$")
@@ -261,7 +327,7 @@ func (c *bankOfAmericaScraper) findFloatAfter(b []byte, prefix string) string {
return amount return amount
} }
func (c *bankOfAmericaScraper) findLineAfter(b []byte, prefix string) []byte { func findLineAfter(b []byte, prefix string) []byte {
for _, line := range bytes.Split(b, []byte("\n")) { for _, line := range bytes.Split(b, []byte("\n")) {
if bytes.HasPrefix(line, []byte(prefix)) { if bytes.HasPrefix(line, []byte(prefix)) {
return bytes.TrimSpace(bytes.TrimPrefix(line, []byte(prefix))) return bytes.TrimSpace(bytes.TrimPrefix(line, []byte(prefix)))

View File

@@ -179,6 +179,76 @@ func TestScrapeBofAPayment(t *testing.T) {
} }
} }
func TestScrapeFidelityDeposit(t *testing.T) {
b, err := ioutil.ReadFile("./testdata/fidelity.deposit.txt")
if err != nil {
t.Fatal(err)
}
message := &mail.Message{
Header: map[string][]string{
"Subject": []string{"Fidelity Alerts: Deposit Received"},
},
Body: bytes.NewReader(b),
}
fidelity := &fidelityScraper{}
gots, err := fidelity.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: Fidelity,
Amount: "?.??",
Vendor: "misc",
Date: "[]",
Account: Fidelity.String() + "-5576",
}
if *got != want {
t.Fatalf("want:\n\t%+v, got\n\t%+v", want, *got)
}
}
func TestScrapeFidelityWithdrawal(t *testing.T) {
b, err := ioutil.ReadFile("./testdata/fidelity.withdrawal.txt")
if err != nil {
t.Fatal(err)
}
message := &mail.Message{
Header: map[string][]string{
"Subject": []string{"Fidelity Alerts - Direct Debit Withdrawal"},
},
Body: bytes.NewReader(b),
}
fidelity := &fidelityScraper{}
gots, err := fidelity.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: Fidelity,
Amount: "1.00",
Vendor: "CHASE CREDIT CRD",
Date: "[]",
Account: Fidelity.String() + "-5576",
}
if *got != want {
t.Fatalf("want:\n\t%+v, got\n\t%+v", want, *got)
}
}
func TestScrapeBofACharge(t *testing.T) { func TestScrapeBofACharge(t *testing.T) {
b, err := ioutil.ReadFile("./testdata/bofa.charge.txt") b, err := ioutil.ReadFile("./testdata/bofa.charge.txt")
if err != nil { if err != nil {

29
testdata/fidelity.deposit.txt vendored Normal file
View File

@@ -0,0 +1,29 @@
Account: XXXXX5576
A deposit to your account was received on 12/08/2022.
Important information:
Fidelity automatically emails certain alerts to customers who have provided
an email address. You can see the terms which govern these alerts at
https://www.fidelity.com/customer-service/alerts-agreement. If you would
prefer to not receive these alerts, please change your preferences at
https://scs.fidelity.com/customeronly/alerts.shtml.
Review Fidelity's Terms of Use for Third Party Content and Research at
https://www.fidelity.com/terms-of-use#Third.
If your email has changed, please update your email address
at https://alertable.fidelity.com/ftgw/alerts/GetUserDeliveryDevices to continue to
receive your alerts.
Fidelity Brokerage Services LLC, Member NYSE, SIPC
EMAIL REF# 537048
Copyright 2022 FMR LLC
All rights reserved. Important Legal Information
at http://www.fidelity.com/terms-of-use.

49
testdata/fidelity.withdrawal.txt vendored Normal file
View File

@@ -0,0 +1,49 @@
For account ending in 5576:
Money was withdrawn from your account through a direct debit in the amount of $1.00 by CHASE CREDIT CRD.
If you authorized this transaction, no action is needed.
If you did not authorize this transaction, please contact us immediately at 800-343-3548.
Important information:
Fidelity automatically emails certain alerts to customers who have provided
an email address. You can see the terms which govern these alerts at
https://alertable.fidelity.com/alerts/help/agreement.html. If you would
prefer to not receive these alerts, please change your preferences at
https://scs.fidelity.com/customeronly/alerts.shtml.
This new alerts service will not affect delivery of paper communications
you are scheduled to receive.
To stop receipt of alerts, modify your preferences on the Existing Alerts page
at https://scs.fidelity.com/customeronly/fens.shtml, or temporarily
stop/restart alerts at
https://scs.fidelity.com/customeronly/fens_alertstatus.shtml.
Fidelity offers access to a broader range of third-party research at
http://personal.fidelity.com/research/stocks/content/stocksindex.shtml.
Fidelity is not recommending or endorsing any third-party research by making it
available to its customers or by notifying customers of its availability.
Review Fidelity's Terms of Use for Third Party Content and Research at
http://activequote.fidelity.com/rtrnews/terms.html.
If your email has changed, please update your email address
at https://scs.fidelity.com/customeronly/fens_profile.shtml to continue to
receive your alerts.
Read Fidelity's Commitment to Privacy
at http://personal.fidelity.com/global/search/content/privacy.html.tvsr.
Visit Fidelity's Home Page
http://www.fidelity.com/
Fidelity Brokerage Services LLC, Member NYSE, SIPC, 900 Salem Street, Smithfield, RI 02917
EMAIL REF# 537048
Copyright 2022 FMR LLC
All rights reserved. Important Legal Information
at http://personal.fidelity.com/misc/legal/legal.html.tvsr.