From c84b29037d1d63a9babad960a776616d5640c88f Mon Sep 17 00:00:00 2001 From: "Zed A. Shaw" Date: Thu, 5 Mar 2026 23:52:50 -0500 Subject: [PATCH] Worked out the docs for the email sending feature. --- README.md | 97 ++++++++++++++++++++++++++++++++++++++++-- common/email/api.go | 4 +- common/email/data.go | 1 - emails/signup.md | 8 +++- features/init.go | 2 - tools/cmd/qmgr/main.go | 9 ++++ 6 files changed, 112 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 6b264b6..266ef28 100644 --- a/README.md +++ b/README.md @@ -474,14 +474,94 @@ When you start with the UI can you can prototype all of the interactions, figure aim for only what you need. Keep doing this and you'll slowly build a nice data model with less cruft for the application you're creating. - ## Additional Features -### Sending Emails +## Sending Emails -Coming soon... +> __WARNING__: The email sending feature is currently very simple. Don't think you can send 200 +> million spam messages with it. + +The email system uses a [Redis]() queue to schedule messages, and an email router to actually +deliver them. The idea is that you don't want to wait for email crafting and delivery in your web +application most of the time. Ideally what you want to do is: + +1. In an `api.go` you receive a form or action from the user that needs an email. Say a password + reset. +2. You use the `common/email` module to craft a simple message and send it to the queue. Your + message at this stage is usually a very simple `struct` with the information your email router + needs to build the email. +3. In your `api.go` file _immediately return rather than wait for email delivery._ +4. Your email router then sits there and processes the email requests. When a new message comes in + it takes the data, queries your database, configures the templates, and finally sends it to the + email server. + +Since email is asynchronous there's no point in your web application waiting around for all of #4 to +be done. Just toss out a message on a queue and move on. + +### Running the `mailer` + +There's a simple router implemented in `tools/cmd/mailer` that you can run to get going. You can +use this to send simple emails but you should probably write a new one that handles your more +sophisticated emails like password resets and receipts. + +You can run it from the command line with: + +```shell +./bin/mailer +``` + +It will require that you install `redis` for it to work. + +### Testing Your Email Locally + +Running an email server locally is kind of a pain, and setting up everything so you can view sent +emails is even harder. That's way the awesome and fabulous [MailHog](https://github.com/mailhog/MailHog) exists. + +You can install it easily (it's written in Go) and then run it: + +```shell +MailHog +``` + +By default it opens an SMTP port on `1025` and a web server at 127.0.0.1:8025. If you point your +browser at 127.0.0.:8025 you'll get a fake email inbox. This inbox will show you both the HTML and +TEXT versions of your email, plus other useful things. + +### How To Send -### Receiving Payments +Here's an example of sending an email from a form submission in `features/email/api.go`. + +```go +package features_email + +import ( + email "MY/webapp/common/email" + "github.com/gofiber/fiber/v2" +) + +func PostApiEmailSend(c *fiber.Ctx) error { + go email.OneShotSend(email.EmailMessage{ + To: c.FormValue("To"), + From: c.FormValue("From"), + Subject: c.FormValue("Subject"), + Template: c.FormValue("Template"), + }) + + return c.Redirect("/email/") +} + +func SetupApi(app *fiber.App) { + app.Post("/api/email/send", PostApiEmailSend) +} +``` + +### Email Templates + +The templates use the same default `text/template` engine that the rest of the system uses, but you +should use Markdown to write them. This automatically gives you both a nice text format (Markdown +already is like an email) and an HTML output. Look at `emails/signup.md` for a simple example. + +## Receiving Payments Coming soon... @@ -489,6 +569,15 @@ Coming soon... Here's some questions I get when people see this. +### What if My Email Templates Need HTML? + +You should be able to work with the code in `common/email` to make it do what you want. I don't +think it's specifically looking for any extension, but I haven't tested it. Try just dropping the +`.html` you want into the `emails/` directory and sending it. + +You should also know that really complex HTML usually get filtered, so it's best to keep it simple +HTML and Markdown will help you stick to that. + ### Isn't Fiber Banned?! First off, nobody has the right to "ban" a project. If you're hanging out in the Go discord and diff --git a/common/email/api.go b/common/email/api.go index 77f5119..a6defcb 100644 --- a/common/email/api.go +++ b/common/email/api.go @@ -98,7 +98,7 @@ func (router *Router) DeliverEmail(msg EmailMessage) error { } log.Println("template is", msg.Template) - text, html, err := router.Render(msg.Template, nil) + text, html, err := router.Render(msg.Template, msg) if err != nil { return err } email_msg.SetBodyString(mail.TypeTextHTML, html) @@ -150,6 +150,8 @@ func (sender *Sender) QueueEmail(msg EmailMessage) error { return nil } +// Intended to be fired off on a goroutine and +// forgotten about. func OneShotSend(msg EmailMessage) { ctx := context.Background() diff --git a/common/email/data.go b/common/email/data.go index 2886b3e..058ff20 100644 --- a/common/email/data.go +++ b/common/email/data.go @@ -24,6 +24,5 @@ type Router struct { redis_client *redis.Client smtp_client *mail.Client ctx context.Context - } diff --git a/emails/signup.md b/emails/signup.md index e493761..aa2e1f9 100644 --- a/emails/signup.md +++ b/emails/signup.md @@ -1,4 +1,10 @@ Welcome ======= -Welcome to my website! +Welcome to my website! Here's what you sent: + +* {{ .From }} +* {{ .To }} +* {{ .Data.Name }} +* {{ .Data.Pet }} + diff --git a/features/init.go b/features/init.go index ac71ee2..a283162 100644 --- a/features/init.go +++ b/features/init.go @@ -6,7 +6,6 @@ import ( "MY/webapp/features/paypal" "MY/webapp/features/shopping" "MY/webapp/features/fakepay" - "MY/webapp/features/myfeature" ) func Setup(app *fiber.App) { @@ -14,5 +13,4 @@ func Setup(app *fiber.App) { features_paypal.Setup(app) features_shopping.Setup(app) features_fakepay.Setup(app) - features_myfeature.Setup(app) } diff --git a/tools/cmd/qmgr/main.go b/tools/cmd/qmgr/main.go index d849815..893bfce 100644 --- a/tools/cmd/qmgr/main.go +++ b/tools/cmd/qmgr/main.go @@ -7,6 +7,11 @@ import ( "fmt" ) +type Person struct { + Name string + Pet string +} + func main() { config.Load("config.json") @@ -17,6 +22,10 @@ func main() { From: "toni.sender@example.com", Subject: "This is my first mail.", Template: "signup.md", + Data: Person{ + Name: "Zed", + Pet: "None", + }, } fmt.Println("sending", msg)