hmn/src/email/email.go

164 lines
4.6 KiB
Go
Raw Normal View History

2021-08-08 20:05:52 +00:00
package email
import (
"bytes"
"fmt"
"mime"
"mime/quotedprintable"
"net/smtp"
"regexp"
"strings"
"time"
"git.handmade.network/hmn/hmn/src/config"
"git.handmade.network/hmn/hmn/src/hmnurl"
"git.handmade.network/hmn/hmn/src/oops"
"git.handmade.network/hmn/hmn/src/perf"
"git.handmade.network/hmn/hmn/src/templates"
)
2021-08-17 18:48:54 +00:00
// TODO(asaf): Adjust this once we test on the server
2021-09-04 22:42:43 +00:00
const ExpectedEmailSendDuration = time.Millisecond * 1500
2021-08-17 18:48:54 +00:00
2021-08-08 20:05:52 +00:00
type RegistrationEmailData struct {
Name string
HomepageUrl string
CompleteRegistrationUrl string
}
2022-08-13 19:15:00 +00:00
func SendRegistrationEmail(
toAddress string,
toName string,
username string,
completionToken string,
destination string,
perf *perf.RequestPerf,
) error {
2021-08-08 20:05:52 +00:00
perf.StartBlock("EMAIL", "Registration email")
perf.StartBlock("EMAIL", "Rendering template")
contents, err := renderTemplate("email_registration.html", RegistrationEmailData{
Name: toName,
HomepageUrl: hmnurl.BuildHomepage(),
2022-08-13 19:15:00 +00:00
CompleteRegistrationUrl: hmnurl.BuildEmailConfirmation(username, completionToken, destination),
2021-08-08 20:05:52 +00:00
})
if err != nil {
return err
}
perf.EndBlock()
perf.StartBlock("EMAIL", "Sending email")
err = sendMail(toAddress, toName, "[handmade.network] Registration confirmation", contents)
if err != nil {
return oops.New(err, "Failed to send email")
}
perf.EndBlock()
perf.EndBlock()
return nil
}
2021-08-17 05:18:04 +00:00
type PasswordResetEmailData struct {
Name string
DoPasswordResetUrl string
Expiration time.Time
}
func SendPasswordReset(toAddress string, toName string, username string, resetToken string, expiration time.Time, perf *perf.RequestPerf) error {
perf.StartBlock("EMAIL", "Password reset email")
perf.StartBlock("EMAIL", "Rendering template")
contents, err := renderTemplate("email_password_reset.html", PasswordResetEmailData{
Name: toName,
DoPasswordResetUrl: hmnurl.BuildDoPasswordReset(username, resetToken),
Expiration: expiration,
})
if err != nil {
return err
}
perf.EndBlock()
perf.StartBlock("EMAIL", "Sending email")
err = sendMail(toAddress, toName, "[handmade.network] Your password reset request", contents)
if err != nil {
return oops.New(err, "Failed to send email")
}
perf.EndBlock()
perf.EndBlock()
return nil
}
2021-08-08 20:05:52 +00:00
var EmailRegex = regexp.MustCompile(`^[^:\p{Cc} ]+@[^:\p{Cc} ]+\.[^:\p{Cc} ]+$`)
func IsEmail(address string) bool {
return EmailRegex.Match([]byte(address))
}
func renderTemplate(name string, data interface{}) (string, error) {
var buffer bytes.Buffer
template, hasTemplate := templates.Templates[name]
if !hasTemplate {
return "", oops.New(nil, "Template not found: %s", name)
}
err := template.Execute(&buffer, data)
if err != nil {
return "", oops.New(err, "Failed to render template for email")
}
contentString := string(buffer.Bytes())
contentString = strings.ReplaceAll(contentString, "\n", "\r\n")
return contentString, nil
}
func sendMail(toAddress, toName, subject, contentHtml string) error {
if config.Config.Email.ForceToAddress != "" {
toAddress = config.Config.Email.ForceToAddress
2021-08-08 20:05:52 +00:00
}
contents := prepMailContents(
makeHeaderAddress(toAddress, toName),
makeHeaderAddress(config.Config.Email.FromAddress, config.Config.Email.FromName),
subject,
contentHtml,
)
return smtp.SendMail(
fmt.Sprintf("%s:%d", config.Config.Email.ServerAddress, config.Config.Email.ServerPort),
smtp.PlainAuth("", config.Config.Email.FromAddress, config.Config.Email.FromAddressPassword, config.Config.Email.ServerAddress),
config.Config.Email.FromAddress,
[]string{toAddress},
contents,
)
}
func makeHeaderAddress(email, fullname string) string {
if fullname != "" {
encoded := mime.BEncoding.Encode("utf-8", fullname)
if encoded == fullname {
encoded = strings.ReplaceAll(encoded, `"`, `\"`)
encoded = fmt.Sprintf("\"%s\"", encoded)
}
return fmt.Sprintf("%s <%s>", encoded, email)
} else {
return email
}
}
func prepMailContents(toLine string, fromLine string, subject string, contentHtml string) []byte {
var builder strings.Builder
builder.WriteString(fmt.Sprintf("To: %s\r\n", toLine))
builder.WriteString(fmt.Sprintf("From: %s\r\n", fromLine))
builder.WriteString(fmt.Sprintf("Date: %s\r\n", time.Now().UTC().Format(time.RFC1123Z)))
builder.WriteString(fmt.Sprintf("Subject: %s\r\n", mime.QEncoding.Encode("utf-8", subject)))
builder.WriteString("Content-Type: text/html; charset=UTF-8\r\n")
builder.WriteString("Content-Transfer-Encoding: quoted-printable\r\n")
builder.WriteString("\r\n")
writer := quotedprintable.NewWriter(&builder)
writer.Write([]byte(contentHtml))
writer.Close()
builder.WriteString("\r\n")
return []byte(builder.String())
}