diff --git a/bank.go b/bank.go index 5841c76..a47c9c6 100755 --- a/bank.go +++ b/bank.go @@ -3,11 +3,12 @@ package main type Bank int const ( - Chase Bank = iota + 1 - Citi Bank = iota + 1 - UCCU Bank = iota + 1 - BankOfAmerica Bank = iota + 1 - Fidelity Bank = iota + 1 + Chase Bank = iota + 1 + Citi + UCCU + BankOfAmerica + Fidelity + Amex ) func (b Bank) String() string { @@ -22,6 +23,8 @@ func (b Bank) String() string { return "Citi" case UCCU: return "UCCU" + case Amex: + return "AmericanExpress" } return "?" } diff --git a/config.go b/config.go index 19d52c8..0cd6a92 100755 --- a/config.go +++ b/config.go @@ -11,13 +11,12 @@ import ( type Uploader int const ( - UploaderTodo = Uploader(iota) + DeprecatedUploaderTodo = Uploader(iota) UploaderLedger UploaderPTTodo ) var uploaders = map[string]Uploader{ - "todo": UploaderTodo, "ledger": UploaderLedger, "pttodo": UploaderPTTodo, } @@ -28,8 +27,6 @@ type Config struct { EmailIMAP string EmailLimit int TodoAddr string - TodoToken string - TodoList string TodoTag string Uploader Uploader Storage storage.DB @@ -48,9 +45,9 @@ func NewConfig() Config { 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", "ledger|pttodo", "ledger") - as.Append(args.STRING, "todoaddr", "todo addr", "https://todo-server.remote.blapointe.com") + as.Append(args.STRING, "todoaddr", "todo addr", "/tmp/email-xactions-to-todo.dat.txt") as.Append(args.STRING, "todopass", "todo pass", "gJtEXbbLHLf54yS9EdujtVN2n6Y") as.Append(args.STRING, "todotoken", "todo token", "") as.Append(args.STRING, "todolist", "todo list", "") @@ -102,20 +99,6 @@ func NewConfig() Config { } log.Printf("config: %+v", config) - if config.Uploader == UploaderTodo { - token := as.GetString("todotoken") - if len(token) == 0 { - token = getToken(as) - } - - list := as.GetString("todolist") - if len(list) == 0 { - list = getList(as, token) - } - config.TodoToken = token - config.TodoList = list - } - return config } diff --git a/go.mod b/go.mod index cddedf0..89bcddf 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module gitea.inhome.blapointe.com/local/email-xactions-to-todo -go 1.17 +go 1.23.0 require ( gitea.inhome.blapointe.com/local-sandbox/contact v0.0.2-0.20231109150121-14036702ee2a diff --git a/scrape.go b/scrape.go index 4d6eaa2..36d22d2 100755 --- a/scrape.go +++ b/scrape.go @@ -7,6 +7,7 @@ import ( "io/ioutil" "net/mail" "regexp" + "slices" "strconv" "strings" ) @@ -20,6 +21,7 @@ type bankOfAmericaScraper struct{} type chaseScraper struct{} type citiScraper struct{} type uccuScraper struct{} +type amexScraper struct{} func Scrape(m *mail.Message, banks map[Bank]bool) ([]*Transaction, error) { scraper, err := buildScraper(m, banks) @@ -50,6 +52,9 @@ func buildScraper(m *mail.Message, banks map[Bank]bool) (scraper, error) { if strings.Contains(from, "Notifications@uccu.com") && banks[UCCU] { return newUCCUScraper(), nil } + if strings.Contains(from, "American Express") && banks[Amex] { + return newAmexScraper(), nil + } return nil, errors.New("unknown sender: " + from) } @@ -73,6 +78,10 @@ func newCitiScraper() scraper { return &citiScraper{} } +func newAmexScraper() scraper { + return &amexScraper{} +} + func containsAny(a string, b ...string) bool { for i := range b { if strings.Contains(a, b[i]) { @@ -245,6 +254,53 @@ func (c *uccuScraper) scrape(m *mail.Message) ([]*Transaction, error) { return []*Transaction{transaction}, nil } +func (c *amexScraper) scrape(m *mail.Message) ([]*Transaction, error) { + b, err := ioutil.ReadAll(m.Body) + if err != nil { + return nil, err + } + b = bytes.ReplaceAll(b, []byte("=\n"), []byte("")) + + matches := regexp.MustCompile(`\$([0-9]+,?)+\.[0-9][0-9]`).FindAll(b, -1) + matches = slices.DeleteFunc(matches, func(match []byte) bool { + return string(match) == "$1.00" + }) + if len(matches) == 0 { + return nil, fmt.Errorf("no matches found") + } + match := matches[0] + match = match[1:] + match = bytes.ReplaceAll(match, []byte(","), []byte{}) + f, err := strconv.ParseFloat(string(match), 10) + if err != nil { + return nil, err + } + f *= -1.0 + + vendors := regexp.MustCompile(`>[A-Z][A-Z ]*<`).FindAll(b, -1) + vendors = slices.DeleteFunc(vendors, func(b []byte) bool { return string(b) == ">BREE A LAPOINTE<" }) + vendor := "*" + if len(vendors) > 0 { + vendor = string(vendors[0]) + } + vendor = strings.TrimSpace(strings.Trim(strings.Trim(vendor, ">"), "<")) + + accs := regexp.MustCompile(`Account Ending: [0-9]*([0-9]{4})[^0-9]`).FindSubmatch(b) + acc := "?" + if len(accs) > 1 { + acc = string(accs[1]) + } + + transaction := NewTransaction( + fmt.Sprintf("%s-%s", Amex.String(), acc), + fmt.Sprintf("%.2f", f), + vendor, + fmt.Sprint(m.Header["Date"]), + Amex, + ) + return []*Transaction{transaction}, nil +} + func (c *fidelityScraper) scrape(m *mail.Message) ([]*Transaction, error) { subject := fmt.Sprint(m.Header["Subject"]) if strings.Contains(subject, "Daily Balance") { diff --git a/scrape_test.go b/scrape_test.go index ad59c61..3896692 100644 --- a/scrape_test.go +++ b/scrape_test.go @@ -8,6 +8,39 @@ import ( "testing" ) +func TestScrapeAmex(t *testing.T) { + b, _ := os.ReadFile("testdata/amex.txt") + + message := &mail.Message{ + Header: map[string][]string{ + "Subject": []string{"Large Purchase Approved"}, + }, + Body: bytes.NewReader(b), + } + + amex := &amexScraper{} + + gots, err := amex.scrape(message) + if err != nil { + t.Fatal(err) + } + if len(gots) != 1 { + t.Fatal(gots) + } + got := gots[0] + + if got.Account != "AmericanExpress-2003" { + t.Fatalf("bad account: %v: %+v", got.Account, got) + } + if got.Amount != "-30.00" { + t.Fatalf("bad amount: %v: %+v", got.Amount, got) + } + if got.Vendor != "CRAWFORD LEISHMAN DENTAL" { + t.Fatalf("bad vendor: %v: %+v", got.Vendor, got) + } + t.Logf("%+v", got) +} + func TestScrapeFidelityBalance(t *testing.T) { b, _ := os.ReadFile("testdata/fidelity.balance.txt") diff --git a/testdata/amex.txt b/testdata/amex.txt new file mode 100644 index 0000000..b4176e9 --- /dev/null +++ b/testdata/amex.txt @@ -0,0 +1,1407 @@ +Delivered-To: breellocaldev@gmail.com +Received: by 2002:a05:6022:9201:b0:72:1e7d:3888 with SMTP id da1csp331127lab; + Thu, 5 Jun 2025 08:45:27 -0700 (PDT) +X-Forwarded-Encrypted: i=4; AJvYcCVUP+by/Wwgb76FkfPB6hm7y+cNjixvYyr6Fon5cqSBqLHGgKPVwoc6EWYLSec/M2qKkBSh2r/8cJC1qxH8@gmail.com +X-Received: by 2002:a05:6512:10cb:b0:553:375e:79bc with SMTP id 2adb3069b0e04-55356e04721mr2469532e87.51.1749138327442; + Thu, 05 Jun 2025 08:45:27 -0700 (PDT) +ARC-Seal: i=3; a=rsa-sha256; t=1749138327; cv=pass; + d=google.com; s=arc-20240605; + b=hB3zliLME6P7nORR2RQ8nrKWB+/CKxDy5g2g8meYnbahBO7HFWHxZ+2xfCOpFcPOm9 + 3yzAImzzkrpU+QQOi3k0rUjal7BU5LinwUXv+l/Plz2JrPY1P+Y47IKjtv3HN/KTEHEe + 4WFqn8lY8OCdQMhC/+6zKctpz7wcRS0QLvq9EnVMiyQQ3XPTwcwl+N81luHPULUcdwwS + A1S3PbkOiDMdhDZPkc9GgUVH8yXoZxKYnWeGCCxIhb3u2Y0LykOQEwqq85JpnQ/al5A7 + K/ykA+co3DZyqT3ta+B6qjJ35AKQzoeN1Fo27SSch2EJh3g7nPUOtYUjJTcXLvwTZyBN + Yh9g== +ARC-Message-Signature: i=3; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; + h=from:subject:reply-to:mime-version:date:message-id:to + :content-transfer-encoding:dkim-signature:delivered-to; + bh=NGWN0O3BYMk61jkPqS3+aURKLg2z1jXp365xwfbczyQ=; + fh=3dEoCJZ4r1gpEg7dbQH+WNtJmDEmGfHYWoJbKyO936A=; + b=VEoE5JpIUcDM6fh/cJLIin1lXdmnwdV4CnCs3PCVSk3xLvoCl54b+mi8cjMOA45t8j + LrpUoht5gjowryuafIJGw31sDDuNEmX6dipRQrWRWAq1yfjwJ7MpeTNBmf1NVtp3aqTh + 3Uqj7jU5vvdZy075uc+rF1PBipumJhKxn/8hdm5ZD1BVe2VehgmC5PMfcqjflb3+cpx2 + YdPFDY+7h/p9Kj44ie6AzUwZoLxh2GD5Kqw6Jhiuj0kc9T+w18MK7SEviX6oSPrawTsq + w5q4rV5tAtvUzOD2si4uIQ0gWDWCCa+4kpaFbPcaaezjTCHT9M21H2RI49holOqLBv35 + dGuw==; + dara=google.com +ARC-Authentication-Results: i=3; mx.google.com; + dkim=pass header.i=@welcome.americanexpress.com header.s=2000006650524 header.b=vAyBYC2M; + arc=pass (i=2 spf=pass spfdomain=welcome.americanexpress.com dkim=pass dkdomain=welcome.americanexpress.com dmarc=pass fromdomain=americanexpress.com); + spf=pass (google.com: domain of squeaky2x3+caf_=breellocaldev=gmail.com@gmail.com designates 209.85.220.41 as permitted sender) smtp.mailfrom="squeaky2x3+caf_=breellocaldev=gmail.com@gmail.com"; + dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=americanexpress.com; + dara=pass header.i=@gmail.com +Return-Path: +Received: from mail-sor-f41.google.com (mail-sor-f41.google.com. [209.85.220.41]) + by mx.google.com with SMTPS id 2adb3069b0e04-5533c277ebasor2836535e87.13.2025.06.05.08.45.27 + for + (Google Transport Security); + Thu, 05 Jun 2025 08:45:27 -0700 (PDT) +Received-SPF: pass (google.com: domain of squeaky2x3+caf_=breellocaldev=gmail.com@gmail.com designates 209.85.220.41 as permitted sender) client-ip=209.85.220.41; +Authentication-Results: mx.google.com; + dkim=pass header.i=@welcome.americanexpress.com header.s=2000006650524 header.b=vAyBYC2M; + arc=pass (i=2 spf=pass spfdomain=welcome.americanexpress.com dkim=pass dkdomain=welcome.americanexpress.com dmarc=pass fromdomain=americanexpress.com); + spf=pass (google.com: domain of squeaky2x3+caf_=breellocaldev=gmail.com@gmail.com designates 209.85.220.41 as permitted sender) smtp.mailfrom="squeaky2x3+caf_=breellocaldev=gmail.com@gmail.com"; + dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=americanexpress.com; + dara=pass header.i=@gmail.com +ARC-Seal: i=2; a=rsa-sha256; t=1749138327; cv=pass; + d=google.com; s=arc-20240605; + b=LDeNgSxJhyDxEow93TUP6CKfzD926idX1gcCxZPXG20LuRxjIoo0DV+9z9hLvT1NAE + Z9RSAO7ZTMhSGsfQlDchTJLiZr/hAQO5y/ySDxcT1jExLCkKuMi780uJAL4ZjiyKsoJF + g4R01cRu19LXkOoMpRBJU2esmXMwjL0GOmQ9mH6H80E8cGmexC7k3A/KF5gBtN7JatwO + 1hsKUwvm7qNd+UNWLbT0ca7eXVCOAZe/whNDDRxlQjNEh99kIQvB3sAncj9ijGtmpglp + T4lbOeihAl/Imh/85uXFOfie5elkspXtwLLbTX6pUk6bnXQTEyqDTiOGD1WDA1d7SR+n + hHyQ== +ARC-Message-Signature: i=2; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; + h=from:subject:reply-to:mime-version:date:message-id:to + :content-transfer-encoding:dkim-signature:delivered-to; + bh=NGWN0O3BYMk61jkPqS3+aURKLg2z1jXp365xwfbczyQ=; + fh=3dEoCJZ4r1gpEg7dbQH+WNtJmDEmGfHYWoJbKyO936A=; + b=BT5jO2GF7FbgcCao2HsMBc9LO1Z4llotdBNlC++n3FbpxBG9EfmPw9fO87ByEfdsq8 + 0S5b4Zc3Xz0D2EEYxNqItsQvNLVMcTbrLyEc3oufLMuRqs/bEB6VZCCHAcA0Bhb2pRVj + 4H3sByHUnaoKLUQ7xED5duzd75PSwVEy27jY4biedwm3XeMZHgcVnUJ6mWmIygBmyzKV + AeIXNQ+QM7hPpgvF1aP3nDeWIMzmnmNHra51gpIKsRTYsU7q7DtN4CO6qnViUGZv2Y6s + HvJFFiscsfQ5eP8PQOYBrFSjmi5hW7wpM668eNCRWtQZEPacDqMXufA7pZ+o9d9akKXt + M/AQ==; + dara=google.com +ARC-Authentication-Results: i=2; mx.google.com; + dkim=pass header.i=@welcome.americanexpress.com header.s=2000006650524 header.b=vAyBYC2M; + spf=pass (google.com: domain of r_02700000000890991722630600_1_x.americanexpress@welcome.americanexpress.com designates 148.173.91.84 as permitted sender) smtp.mailfrom=r_02700000000890991722630600_1_x.AmericanExpress@welcome.americanexpress.com; + dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=americanexpress.com +X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; + d=1e100.net; s=20230601; t=1749138327; x=1749743127; + h=from:subject:reply-to:mime-version:date:message-id:to + :content-transfer-encoding:dkim-signature:delivered-to + :x-forwarded-for:x-forwarded-to:x-gm-message-state:from:to:cc + :subject:date:message-id:reply-to; + bh=NGWN0O3BYMk61jkPqS3+aURKLg2z1jXp365xwfbczyQ=; + b=LkvIuP/7HIc23CY+zymNGhZF4tZ8tm8XeXaGfYB/tsfFu/Q8DNmhqFBB+4NWHjDm59 + CxAr8h23seRjFI/W2G0u/rKXfLQcZ9JUj1wDMaIl/H8lRIt5Aj1uLfdwIbZBOe1KFz4C + MLjG6S9af6q1yn8D37DBmd1pMIa71q1RNLXw5cOe61bZ0geTd/JBpbOvbdaTsVzFunbl + ImY0XDrIF6vTXpHR5/dzc/uo4Tcw+m8azaCFyI3Tg2+Nik2fnQKwwFI2ObGf3mrdkEFb + HIl1sNJWRyOEgfIJNhndmo/vzWffSc+3se5gh8SPZdUWx2D2RPjZ6mVpxcxoZ9HleuUJ + SUyQ== +X-Forwarded-Encrypted: i=2; AJvYcCWh8o0D3mKjTitGrBSuQ+lzSIv4kv2BVRdjbXt9Gi9zn7Mk2u/XagvRPIUq3mChGGb3Aqpl7kWwWhwG4B19@gmail.com +X-Gm-Message-State: AOJu0YyDKD4m6/K80wlFyi1r3ceobtxpj7ghnkjeYAWMaxgIZF36bgjX p/65HpiUSEfV4EbWitcFfCieq6ELZdfelkJMDFgZm9cr7vG3a0A1+uRvVrRfEsKkHRNNkXU4MRw RQOZdjG4w/QS24iyrDr55w5t6NOsL5KGBM7mB4DBFZkO25KzltpuGbt4ZF88jwA== +X-Received: by 2002:a05:6512:23c2:b0:553:659c:53fa with SMTP id 2adb3069b0e04-553659c5621mr273155e87.5.1749138326818; + Thu, 05 Jun 2025 08:45:26 -0700 (PDT) +X-Forwarded-To: breellocaldev@gmail.com +X-Forwarded-For: squeaky2x3@gmail.com breellocaldev@gmail.com +Delivered-To: squeaky2x3@gmail.com +Received: by 2002:ab3:1908:0:b0:2b2:ef9d:fd80 with SMTP id u8csp369751ltc; + Thu, 5 Jun 2025 08:45:24 -0700 (PDT) +X-Google-Smtp-Source: AGHT+IEeLvkPA8naOeU5rkLD9Ab76JIvrf6wJVOvuNET1DPCa0O7YnymN2RPbzvZ2/fVc7QvuHgS +X-Received: by 2002:a17:902:d2c6:b0:234:c5c1:9b5f with SMTP id d9443c01a7336-235e112499cmr99536985ad.16.1749138324475; + Thu, 05 Jun 2025 08:45:24 -0700 (PDT) +ARC-Seal: i=1; a=rsa-sha256; t=1749138324; cv=none; + d=google.com; s=arc-20240605; + b=Kg44GwuNI4m9Mcc3a/zr6jSXTS3P244woaIa71N+k5i5mzSieaNK/qMguo9AZhJuTu + rrnjaBk+K45ElpAU1NjQHkhdcke9LCimseMp8j91pzLmDjsAev1dBDhFPMcyu82KdF7Y + 87L4TK8u7CL535W9QoICbuJtTGBC4Kc2lyAziwXVDoQl/O4eqnYqKEcISJ44nw0E0Qem + 2kTilZuPNXJ7Uvv59WfqrMssoWKO1xF2MaCCjC/AWL8HeKZO4SoGVTpsBvpq5Zj4VsNO + xZfqdYlAsh1wlaqsmv39VcxKMUMiogeXlryB6LlNbhynxQFSAqRl4akYDfzqQP53Udm2 + wfyQ== +ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=google.com; s=arc-20240605; + h=from:subject:reply-to:mime-version:date:message-id:to + :content-transfer-encoding:dkim-signature; + bh=NGWN0O3BYMk61jkPqS3+aURKLg2z1jXp365xwfbczyQ=; + fh=azjU2qaVuDlsQbzujpw5Djd1IsoYJ9q+1l8SxmTlxk0=; + b=ZPBaeVB9g3xfPLAK5yvAE7CDdnOmYVwIlnbXfH6k9xR7haBc50GTw4uJytQ88vdMFt + nN5HwuQbzanPO6Jb07cUd1AmQUzEFKxrxXjYVJWKatiH+ELLUZlVy/3y1kzbUINTp22O + tL0gy4yidVD7t6vy2nInTzxpSC3RKywq3hqIh8q+m+KoEwryWdztCBlvlzLcgJiCCATp + MKwGPHTooa96usft8MuhDnqcMeL/mpbrAar3YfAmfQt9pJPfm/fQVgbNOcYDbEoWRWYq + l3EqV0ZiUBA7YGA1Enns1BXZpF3+qUW0+vNEs1m9KDPTz/wIeNrvuIn8mzDkyZm/EDHA + HpVw==; + dara=google.com +ARC-Authentication-Results: i=1; mx.google.com; + dkim=pass header.i=@welcome.americanexpress.com header.s=2000006650524 header.b=vAyBYC2M; + spf=pass (google.com: domain of r_02700000000890991722630600_1_x.americanexpress@welcome.americanexpress.com designates 148.173.91.84 as permitted sender) smtp.mailfrom=r_02700000000890991722630600_1_x.AmericanExpress@welcome.americanexpress.com; + dmarc=pass (p=REJECT sp=REJECT dis=NONE) header.from=americanexpress.com +Return-Path: +Received: from extmta2-ipc.americanexpress.com (extmta2-ipc.americanexpress.com. [148.173.91.84]) + by mx.google.com with ESMTPS id d9443c01a7336-23506c43609si213850995ad.413.2025.06.05.08.45.22 + for + (version=TLS1_2 cipher=ECDHE-ECDSA-AES128-GCM-SHA256 bits=128/128); + Thu, 05 Jun 2025 08:45:24 -0700 (PDT) +Received-SPF: pass (google.com: domain of r_02700000000890991722630600_1_x.americanexpress@welcome.americanexpress.com designates 148.173.91.84 as permitted sender) client-ip=148.173.91.84; +DKIM-Signature: v=1; a=rsa-sha256; d=welcome.americanexpress.com; s=2000006650524; c=relaxed/relaxed; q=dns/txt; i=@welcome.americanexpress.com; t=1749138318; h=From:Reply-To:Subject:Date:Message-ID:To:MIME-Version:Content-Type:Content-Transfer-Encoding; bh=NGWN0O3BYMk61jkPqS3+aURKLg2z1jXp365xwfbczyQ=; b=vAyBYC2M7zan1URDjXvF9Vu4VnFEUtQNa2JQOgyggwL2Qi2aI2iGn2V9T1BHkU+Q A9FKjJkiaJo8Qq10rnG9i9F4IMyX+cBjqHOW/5bXWG1zToydWi/LhDotYluzovHG HzUnVi8B3i4IG7GvIiu38SsoEMD6Tlsx6Do1oOzy+OYzl0U9YBpR0xzKkmlcupYl k14sMnCGZX/Zv8VO94OEf+y43EcMVfatjbiLzN1GJ04vJcxUDTUvQggVhMXQA16c 5CZN0UymqK/6OIFLP89P4AjcskZLTcgNCP2wWekoQKCaGKV85JCbcMxHT+Gl/dP7 7A+pg9KKoKr1bYyX/qlr4Q==; +X-MSFBL: vZPBnfgGVUeiXZpXM7kNtsez4SL3jgKdKJ6BwmTYJ6o=|eyJ0ZW5hbnRfaWQiOiJ rZXlzcGFjZV9kZWZhdWx0IiwiY3VzdG9tZXJfaWQiOiIxIiwicmNwdF9tZXRhIjp 7ICJhbWV4X1JldHJ5UnVsZSI6ICJBRVhQIiwgImFtZXhfTWVzc2FnZUlEIjogIjx yXzAyNzAwMDAwMDAwODkwOTkxNzIyNjMwNjAwXzFfeC4uQW1lcmljYW5FeHByZXN zQHdlbGNvbWUuYW1lcmljYW5leHByZXNzLmNvbT4iLCAiYW1leF9Db21tdW5TdWJ qVGV4dCI6ICI9P1VURi04P0I/VEdGeVoyVWdVSFZ5WTJoaGMyVWdRWEJ3Y205Mlp XUT0/PSIsICJhbWV4X01lc3NhZ2VDbGFzcyI6ICJ0cmFuc2FjdGlvbmFsIiB9LCJ yIjoic3F1ZWFreTJ4M0BnbWFpbC5jb20iLCJzbmlwcGV0X2NvdW50IjoiMCIsImI iOiJ0cmFuc2FjdGlvbmFsXzRYNDgiLCJ0ZW1wbGF0ZV9jb250ZW50X3R5cGUiOiJ pbmxpbmVfcGFydHNfd2l0aG91dF9zdWJzdGl0dXRpb25zIiwiZyI6InRyYW5zYWN 0aW9uYWwiLCJ0ZW1wbGF0ZV92ZXJzaW9uIjoiMCIsInRlbXBsYXRlX2lkIjoidGV tcGxhdGVfMTQ2NjI3Mzg0NDg4MjIwNDMiLCJtZXNzYWdlX2lkIjoiMDAwMDhlYmI 0MTY4YzRlMTNmMjkiLCJyY3B0X3RhZ3MiOlsgXSwidSI6IiV2Y3R4X21lc3N7YW1 leF9NZXNzYWdlSUR9IiwidHJhbnNtaXNzaW9uX2lkIjoiMTQ2NjI3Mzg0NDg4MjI wNDMiLCJzdWJhY2NvdW50X2lkIjoiMCJ9 +Content-Transfer-Encoding: quoted-printable +Content-Type: text/html; charset="UTF-8" +To: squeaky2x3@gmail.com +Message-ID: +Date: Thu, 05 Jun 2025 08:45:18 -0700 +MIME-Version: 1.0 +Reply-To: AmericanExpress@welcome.americanexpress.com +Subject: Large Purchase Approved +From: American Express + +
<= +div style=3D"background:#ffffff;background-color:#ffffff;margin:0px auto;ma= +x-width:620px;">
<= +/tr>

Se= +e the details about this purchase

<= +/td>
<= +td style=3D"direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0;pad= +ding-left:0;padding-right:0;padding-top:0;text-align:center;">

BREE A LAPOINTE

Accoun= +t Ending: 82003

= +
<= +div style=3D"margin:0px auto;max-width:620px;">
= +
3D"Information

There was a large purchas= +e on your Card

<= +/div>

Dear BREE A LAPOINTE,

As you requested, we're = +letting you know that this purchase was more than $1.00.

<= +/tr>

You can change the dollar amount= + of these large purchase notifications online.

<= +table align=3D"center" border=3D"0" cellpadding=3D"0" cellspacing=3D"0" rol= +e=3D"presentation" style=3D"width:100%;">

CRAWFOR= +D LEISHMAN DENTAL

= +

$30.00*

Thu, Jun 5, 2025

= +

*The amou= +nt above may not reflect the final amount as some merchants issue a pre-aut= +horization charge

<= +/tr>

You can track this spending charge online and b= +e notified when the final amount is posted to your account.

If you still = +have questions about this transaction, we suggest contacting the merchant d= +irectly.

= +
= +
3D"Don't
<= +td style=3D"direction:ltr;font-size:0px;padding:20px 0;padding-bottom:0;pad= +ding-left:40px;padding-right:40px;padding-top:40px;text-align:center;">
How do you feel about this fraud protection experience?
3D"Rating
3D"Rating
3D"Rating
3D"Rating
3D"Rating
<= +/tr>
<= +/tbody>

To stop this alert, simply click here.

Your account information is = +included above to help you recognize this as a customer care email from Ame= +rican Express. To learn more about email security or report a suspicious em= +ail, please visit us at amer= +icanexpress.com/phishing. We kindly ask you not to reply to this email = +but instead contact us via C= +ustomer Care.

=C2=A92025 American Express. All rights reserved.

=C2=A0

SAM0FYI568

3D""
+ + diff --git a/upload.go b/upload.go index 4dd25b4..169328e 100755 --- a/upload.go +++ b/upload.go @@ -13,7 +13,7 @@ import ( func Upload(config Config, transaction *Transaction) error { switch config.Uploader { - case UploaderTodo: + case DeprecatedUploaderTodo: panic("DEAD") case UploaderLedger: return uploadLedger(config, transaction)