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
|
|
|
|
}
|
|
|
|
|
2023-06-01 21:42:46 +00:00
|
|
|
type TimeMachineEmailData struct {
|
|
|
|
ProfileUrl string
|
|
|
|
Username string
|
|
|
|
UserEmail string
|
|
|
|
DiscordUsername string
|
|
|
|
MediaUrl string
|
|
|
|
DeviceInfo string
|
|
|
|
Description string
|
|
|
|
}
|
|
|
|
|
|
|
|
func SendTimeMachineEmail(profileUrl, username, userEmail, discordUsername, mediaUrl, deviceInfo, description string, perf *perf.RequestPerf) error {
|
|
|
|
perf.StartBlock("EMAIL", "Time machine email")
|
|
|
|
defer perf.EndBlock()
|
|
|
|
|
|
|
|
contents, err := renderTemplate("email_time_machine.html", TimeMachineEmailData{
|
|
|
|
ProfileUrl: profileUrl,
|
|
|
|
Username: username,
|
|
|
|
UserEmail: userEmail,
|
|
|
|
DiscordUsername: discordUsername,
|
|
|
|
MediaUrl: mediaUrl,
|
|
|
|
DeviceInfo: deviceInfo,
|
|
|
|
Description: description,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
err = sendMail("team@handmade.network", "HMN Team", "[time machine] New submission", contents)
|
|
|
|
if err != nil {
|
|
|
|
return oops.New(err, "Failed to send email")
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
2023-04-08 16:14:44 +00:00
|
|
|
template := templates.GetTemplate(name)
|
2021-08-08 20:05:52 +00:00
|
|
|
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 {
|
2022-05-25 22:39:57 +00:00
|
|
|
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),
|
2022-09-18 19:23:36 +00:00
|
|
|
smtp.PlainAuth("", config.Config.Email.MailerUsername, config.Config.Email.MailerPassword, config.Config.Email.ServerAddress),
|
2021-08-08 20:05:52 +00:00
|
|
|
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())
|
|
|
|
}
|