repos / pico

pico services mono repo
git clone https://github.com/picosh/pico.git

commit
fffe43e
parent
7792e55
author
Eric Bower
date
2025-07-27 09:57:26 -0400 EDT
refactor(feeds): replace sendgrid with smtp
3 files changed,  +51, -43
M go.mod
M go.sum
M go.mod
+2, -2
 1@@ -30,6 +30,8 @@ require (
 2 	github.com/darkweak/souin v1.7.6
 3 	github.com/darkweak/souin/plugins/souin/storages v1.7.6
 4 	github.com/darkweak/storages/core v0.0.14
 5+	github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6
 6+	github.com/emersion/go-smtp v0.23.0
 7 	github.com/gkampitakis/go-snaps v0.5.7
 8 	github.com/google/go-cmp v0.7.0
 9 	github.com/google/renameio/v2 v2.0.0
10@@ -51,7 +53,6 @@ require (
11 	github.com/pkg/sftp v1.13.9
12 	github.com/prometheus/client_golang v1.21.1
13 	github.com/sabhiram/go-gitignore v0.0.0-20210923224102-525f6e181f06
14-	github.com/sendgrid/sendgrid-go v3.16.0+incompatible
15 	github.com/simplesurance/go-ip-anonymizer v0.0.0-20200429124537-35a880f8e87d
16 	github.com/x-way/crawlerdetect v0.2.28
17 	github.com/yuin/goldmark v1.7.8
18@@ -232,7 +233,6 @@ require (
19 	github.com/safchain/ethtool v0.5.10 // indirect
20 	github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 // indirect
21 	github.com/secure-io/sio-go v0.3.1 // indirect
22-	github.com/sendgrid/rest v2.6.9+incompatible // indirect
23 	github.com/shirou/gopsutil/v3 v3.24.5 // indirect
24 	github.com/shoenig/go-m1cpu v0.1.6 // indirect
25 	github.com/shopspring/decimal v1.4.0 // indirect
M go.sum
+4, -4
 1@@ -262,6 +262,10 @@ github.com/dsoprea/go-utility/v2 v2.0.0-20200717064901-2fccff4aa15e/go.mod h1:uA
 2 github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
 3 github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
 4 github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
 5+github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
 6+github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
 7+github.com/emersion/go-smtp v0.23.0 h1:ZiriTOTK7sKep7jbWqgB5kPsiBp5wnE5auEMnwRMnGc=
 8+github.com/emersion/go-smtp v0.23.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
 9 github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
10 github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
11 github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
12@@ -731,10 +735,6 @@ github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529 h1:nn5Wsu0esKSJiIVhscUt
13 github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
14 github.com/secure-io/sio-go v0.3.1 h1:dNvY9awjabXTYGsTF1PiCySl9Ltofk9GA3VdWlo7rRc=
15 github.com/secure-io/sio-go v0.3.1/go.mod h1:+xbkjDzPjwh4Axd07pRKSNriS9SCiYksWnZqdnfpQxs=
16-github.com/sendgrid/rest v2.6.9+incompatible h1:1EyIcsNdn9KIisLW50MKwmSRSK+ekueiEMJ7NEoxJo0=
17-github.com/sendgrid/rest v2.6.9+incompatible/go.mod h1:kXX7q3jZtJXK5c5qK83bSGMdV6tsOE70KbHoqJls4lE=
18-github.com/sendgrid/sendgrid-go v3.16.0+incompatible h1:i8eE6IMkiCy7vusSdacHHSBUpXyTcTXy/Rl9N9aZ/Qw=
19-github.com/sendgrid/sendgrid-go v3.16.0+incompatible/go.mod h1:QRQt+LX/NmgVEvmdRw0VT/QgUn499+iza2FnDca9fg8=
20 github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
21 github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
22 github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
M pkg/apps/feeds/cron.go
+45, -37
  1@@ -10,15 +10,16 @@ import (
  2 	"math"
  3 	"net/http"
  4 	"net/url"
  5+	"os"
  6 	"strings"
  7 	"text/template"
  8 	"time"
  9 
 10+	"github.com/emersion/go-sasl"
 11+	"github.com/emersion/go-smtp"
 12 	"github.com/mmcdole/gofeed"
 13 	"github.com/picosh/pico/pkg/db"
 14 	"github.com/picosh/pico/pkg/shared"
 15-	"github.com/sendgrid/sendgrid-go"
 16-	"github.com/sendgrid/sendgrid-go/helpers/mail"
 17 )
 18 
 19 var ErrNoRecentArticles = errors.New("no recent articles")
 20@@ -125,14 +126,19 @@ func isValidItem(logger *slog.Logger, item *gofeed.Item, feedItems []*db.FeedIte
 21 }
 22 
 23 type Fetcher struct {
 24-	cfg *shared.ConfigSite
 25-	db  db.DB
 26+	cfg  *shared.ConfigSite
 27+	db   db.DB
 28+	auth sasl.Client
 29 }
 30 
 31 func NewFetcher(dbpool db.DB, cfg *shared.ConfigSite) *Fetcher {
 32+	smtPass := os.Getenv("PICO_SMTP_PASS")
 33+	emailLogin := os.Getenv("PICO_SMTP_USER")
 34+	auth := sasl.NewPlainClient("", emailLogin, smtPass)
 35 	return &Fetcher{
 36-		db:  dbpool,
 37-		cfg: cfg,
 38+		db:   dbpool,
 39+		cfg:  cfg,
 40+		auth: auth,
 41 	}
 42 }
 43 
 44@@ -516,41 +522,43 @@ func (f *Fetcher) FetchAll(logger *slog.Logger, urls []string, inlineContent boo
 45 	}, nil
 46 }
 47 
 48-func (f *Fetcher) SendEmail(logger *slog.Logger, username, email string, subject string, msg *MsgBody) error {
 49+func (f *Fetcher) SendEmail(logger *slog.Logger, username, email, subject string, msg *MsgBody) error {
 50 	if email == "" {
 51 		return fmt.Errorf("(%s) does not have an email associated with their feed post", username)
 52 	}
 53-
 54-	from := mail.NewEmail("team pico", shared.DefaultEmail)
 55-	to := mail.NewEmail(username, email)
 56-
 57-	// f.logger.Infof("message body (%s)", plainTextContent)
 58-
 59-	message := mail.NewSingleEmail(from, subject, to, msg.Text, msg.Html)
 60-	client := sendgrid.NewSendClient(f.cfg.SendgridKey)
 61-
 62+	smtpAddr := "smtp.fastmail.com:587"
 63+	fromEmail := "hello@pico.sh"
 64+	to := []string{email}
 65+	headers := map[string]string{
 66+		"From": fromEmail,
 67+		"To": email,
 68+		"Subject": subject,
 69+		"MIME-Version": "1.0",
 70+		"Content-Type": `multipart/alternative; boundary="boundary123"`,
 71+	}
 72+	var content strings.Builder
 73+	for k, v := range headers {
 74+		content.WriteString(fmt.Sprintf("%s: %s\r\n", k, v))
 75+	}
 76+	content.WriteString("\r\n")
 77+	content.WriteString("\r\n--boundary123\r\n")
 78+	content.WriteString("Content-Type: text/plain; charset=\"utf-8\"\r\n")
 79+	content.WriteString("\r\n" + msg.Text + "\r\n")
 80+	content.WriteString("--boundary123\r\n")
 81+	content.WriteString("Content-Type: text/html; charset=\"utf-8\"\r\n")
 82+	content.WriteString("\r\n" + msg.Html + "\r\n")
 83+	content.WriteString("--boundary123--")
 84+
 85+	reader := strings.NewReader(content.String())
 86 	logger.Info("sending email digest")
 87-	response, err := client.Send(message)
 88-	if err != nil {
 89-		return err
 90-	}
 91-
 92-	// f.logger.Infof("(%s) email digest response: %v", username, response)
 93-
 94-	if len(response.Headers["X-Message-Id"]) > 0 {
 95-		logger.Info(
 96-			"successfully sent email digest",
 97-			"email", email,
 98-			"x-message-id", response.Headers["X-Message-Id"][0],
 99-		)
100-	} else {
101-		logger.Error(
102-			"could not find x-message-id, which means sending an email failed",
103-			"email", email,
104-		)
105-	}
106-
107-	return nil
108+	err := smtp.SendMail(
109+		smtpAddr,
110+		f.auth,
111+		fromEmail,
112+		to,
113+		reader,
114+	)
115+	return err
116 }
117 
118 func (f *Fetcher) Run(logger *slog.Logger) error {