2 Commits

Author SHA1 Message Date
Abner Coimbre
320bb5c34a Add progress indicator/animation 2025-10-22 00:58:20 -07:00
abnercoimbre
b35ac045f3 run go fmt 2024-10-19 14:15:56 -07:00

114
main.go
View File

@@ -1,6 +1,6 @@
// Meetup Invite 2000
// @author: Abner Coimbre <abner@handmadecities.com>
// Copyright (c) 2024 Handmade Cities
// Copyright (c) Handmade Cities LLC
// Based off News Blaster 2000 from the Handmade Network: https://git.handmade.network/hmn/newsblaster2000
@@ -12,7 +12,7 @@
// Jack Punter (@TarriestPython)
//
// Emotional Support
// Cucui Ganon Rosario
// Cucui Ganon Rosario: https://www.youtube.com/watch?v=bvCsTca0uBc
// =========================================================================
package main
@@ -113,7 +113,7 @@ func retrieveSubscribers(cfg *Config) ([]string, error) {
return nil, errors.New("response format error: 'emails' field not found")
}
/* De-duplicate emails using a map */
/* De-duplicate emails using a map */
/* *** */
emailMap := make(map[string]bool)
for _, email := range emails {
@@ -282,12 +282,12 @@ func blastMail(cfg *Config, logFile string, trackingFile string, audience []stri
}
}
existingReaderCount := len(sentToAddresses)
newReaderCount := 0
existingReaderCount := len(sentToAddresses)
newReaderCount := 0
if existingReaderCount > 0 {
fmt.Println("Same email file specified: Sending only to new subscribers...")
}
if existingReaderCount > 0 {
fmt.Println("Same email file specified: Sending only to new subscribers...")
}
var group []string
for _, a := range audience {
@@ -306,15 +306,19 @@ func blastMail(cfg *Config, logFile string, trackingFile string, audience []stri
if len(group) == cfg.BatchSize {
results, err := sendMail(cfg, group, subject, body)
if err != nil {
fmt.Printf("Error while sending mail: %v\n", err)
return
}
sentToAddresses = append(sentToAddresses, group...)
os.WriteFile(trackingFile, []byte(strings.Join(sentToAddresses, "\n")), 0666)
var failed []string
for i, res := range results {
log.WriteString(fmt.Sprintf("%s: %s\n", group[i], res.Message))
if res.ErrorCode != 0 {
failed = append(failed, group[i])
}
}
newReaderCount += len(group)
printBatchSummary(group, failed)
newReaderCount += len(group)
group = group[0:0]
}
}
@@ -322,25 +326,29 @@ func blastMail(cfg *Config, logFile string, trackingFile string, audience []stri
if len(group) > 0 {
results, err := sendMail(cfg, group, subject, body)
if err != nil {
fmt.Printf("Error while sending mail: %v\n", err)
return
}
sentToAddresses = append(sentToAddresses, group...)
os.WriteFile(trackingFile, []byte(strings.Join(sentToAddresses, "\n")), 0666)
var failed []string
for i, res := range results {
log.WriteString(fmt.Sprintf("%s: %s\n", group[i], res.Message))
if res.ErrorCode != 0 {
failed = append(failed, group[i])
}
}
newReaderCount += len(group)
printBatchSummary(group, failed)
newReaderCount += len(group)
}
if newReaderCount > 0 {
newSubscribers := existingReaderCount > 0
if newSubscribers {
fmt.Printf("Sent to %d new subscribers!\n", newReaderCount)
}
} else {
fmt.Printf("Not sent (no new subscribers)\n")
}
if newReaderCount > 0 {
newSubscribers := existingReaderCount > 0
if newSubscribers {
fmt.Printf("Sent to %d new subscribers!\n", newReaderCount)
}
} else {
fmt.Printf("Not sent (no new subscribers)\n")
}
}
var postmarkClient = http.Client{}
@@ -369,8 +377,47 @@ type PostmarkBatchResult struct {
Message string `json:"Message"`
}
func printBatchSummary(sent, failed []string) {
if len(failed) == 0 {
fmt.Printf("\033[32m✅ Sent %d messages\033[0m\n", len(sent))
} else {
successCount := len(sent) - len(failed)
if successCount < 0 {
successCount = 0
}
fmt.Printf("\033[31m❌ Sent %d, failed %d\033[0m\n", successCount, len(failed))
for _, f := range failed {
fmt.Printf(" - %s\n", f)
}
}
}
func sendMail(cfg *Config, recipients []string, subject, html string) ([]PostmarkBatchResult, error) {
fmt.Printf("Sending batch [%d recipients]...", len(recipients))
/* Start a tiny spinner while the HTTP call runs.
* Uses \r to update the same line. Stops as soon as the network call returns.
*/
stop := make(chan struct{})
done := make(chan struct{})
go func() {
frames := []rune{'|', '/', '-', '\\'}
i := 0
for {
select {
case <-stop:
close(done)
return
default:
fmt.Printf("\rSending batch [%d recipients]... %c", len(recipients), frames[i%len(frames)])
i++
time.Sleep(120 * time.Millisecond)
}
}
}()
/*
* Prepare and make the network call
*/
from := cfg.Postmark.SenderEmail
if cfg.Postmark.SenderName != "" {
@@ -400,13 +447,21 @@ func sendMail(cfg *Config, recipients []string, subject, html string) ([]Postmar
req, err := http.NewRequest(http.MethodPost, "https://api.postmarkapp.com/email/batchWithTemplates", bytes.NewReader(reqBody))
if err != nil {
// stop spinner before returning
close(stop)
<-done
fmt.Printf("\r")
return nil, err
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Content-Type", "application/json")
req.Header.Set("X-Postmark-Server-Token", cfg.Postmark.ServerToken)
res, err := postmarkClient.Do(req)
if err != nil {
close(stop)
<-done
fmt.Printf("\r\033[31m❌ Sending batch [%d recipients]... error: %v\033[0m\n", len(recipients), err)
return nil, err
}
@@ -414,17 +469,30 @@ func sendMail(cfg *Config, recipients []string, subject, html string) ([]Postmar
resBody, err := io.ReadAll(res.Body)
res.Body.Close()
if err != nil {
close(stop)
<-done
fmt.Printf("\r")
return nil, err
}
if res.StatusCode != 200 {
close(stop)
<-done
fmt.Printf("\r\033[31m❌ Bad response from postmark: %d\033[0m\n", res.StatusCode)
return nil, fmt.Errorf("Bad response from postmark: %d", res.StatusCode)
}
err = json.Unmarshal(resBody, &results)
if err != nil {
fmt.Printf("Batch sent successfully, but failed to parse response from postmark.\n")
close(stop)
<-done
fmt.Printf("\r\033[33m⚠ Batch sent but failed to parse Postmark response\033[0m\n")
return nil, nil
}
fmt.Printf("Done.\n")
close(stop)
<-done
fmt.Printf("\r\033[32m✅ Emailed batch of %d recipients\033[0m\n", len(recipients))
return results, nil
}