From 71937c1079a77e0b38dae00662ab2041c3e28f77 Mon Sep 17 00:00:00 2001 From: Thomas Voss Date: Mon, 12 Aug 2024 23:58:46 +0200 Subject: Add email support --- Makefile | 2 +- lib/email/email.go | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++++ main.go | 57 ++++++++++++++++++++++++++++++++++----- template/error.templ | 24 +++++++++++++++++ 4 files changed, 151 insertions(+), 7 deletions(-) create mode 100644 lib/email/email.go create mode 100644 template/error.templ diff --git a/Makefile b/Makefile index 85ba3d9..8893611 100644 --- a/Makefile +++ b/Makefile @@ -9,6 +9,6 @@ all-i18n: go build watch: - ls euro-cash.eu | entr -r ./euro-cash.eu -port $${PORT:-8080} + ls euro-cash.eu | entr -r ./euro-cash.eu -no-email -port $${PORT:-8080} .PHONY: watch diff --git a/lib/email/email.go b/lib/email/email.go new file mode 100644 index 0000000..f6d6673 --- /dev/null +++ b/lib/email/email.go @@ -0,0 +1,75 @@ +package email + +import ( + "crypto/tls" + "fmt" + "math/rand" + "net/smtp" + "strconv" + "time" +) + +var Config struct { + Host string + Port int + ToAddr, FromAddr string + Password string +} + +const emailTemplate = `From: %s +To: %s +Subject: %s +Date: %s +Content-Type: text/plain; charset=UTF-8 +MIME-Version: 1.0 +Message-ID: <%s> + +%s` + +func ServerError(fault error) error { + r := rand.New(rand.NewSource(time.Now().UnixNano())) + msgid := strconv.FormatInt(r.Int63(), 10) + "@" + Config.Host + msg := fmt.Sprintf(emailTemplate, Config.FromAddr, Config.ToAddr, + "Error Report", time.Now().Format(time.RFC1123Z), msgid, fault) + + tlsConfig := &tls.Config{ + InsecureSkipVerify: false, + ServerName: Config.Host, + } + + hostWithPort := Config.Host + ":" + strconv.Itoa(Config.Port) + conn, err := tls.Dial("tcp", hostWithPort, tlsConfig) + if err != nil { + return err + } + + client, err := smtp.NewClient(conn, Config.Host) + if err != nil { + return err + } + defer client.Close() + + auth := smtp.PlainAuth("", Config.FromAddr, Config.Password, Config.Host) + if err := client.Auth(auth); err != nil { + return err + } + + if err := client.Mail(Config.FromAddr); err != nil { + return err + } + + if err := client.Rcpt(Config.ToAddr); err != nil { + return err + } + + wc, err := client.Data() + if err != nil { + return err + } + defer wc.Close() + + if _, err = wc.Write([]byte(msg)); err != nil { + return err + } + return nil +} diff --git a/main.go b/main.go index 86966b5..138f3cd 100644 --- a/main.go +++ b/main.go @@ -11,15 +11,19 @@ import ( "net/http" "os" "path/filepath" + "slices" "strconv" "strings" "git.thomasvoss.com/euro-cash.eu/lib" + "git.thomasvoss.com/euro-cash.eu/lib/email" "git.thomasvoss.com/euro-cash.eu/lib/mintage" "git.thomasvoss.com/euro-cash.eu/template" "github.com/a-h/templ" ) +var emailDisabled bool + var components = map[string]templ.Component{ "/": template.Root(), "/about": template.About(), @@ -34,6 +38,18 @@ func main() { lib.InitPrinters() port := flag.Int("port", 8080, "port number") + flag.BoolVar(&emailDisabled, "no-email", false, + "disables email support") + flag.StringVar(&email.Config.Host, "smtp-host", "smtp.migadu.com", + "SMTP server hostname") + flag.IntVar(&email.Config.Port, "smtp-port", 465, + "SMTP server port number") + flag.StringVar(&email.Config.ToAddr, "email-to", "bugs@euro-cash.eu", + "address to send error messages to") + flag.StringVar(&email.Config.FromAddr, "email-from", "noreply@euro-cash.eu", + "address to send error messages from") + flag.StringVar(&email.Config.Password, "email-password", "", + "password to authenticate the email client") flag.Parse() fs := http.FileServer(http.Dir("static")) @@ -106,20 +122,35 @@ func mintageHandler(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { countries := lib.SortedCountries( r.Context().Value("printer").(lib.Printer)) - code := strings.ToLower(cmp.Or(r.FormValue("code"), countries[0].Code)) - ctype := strings.ToLower(r.FormValue("type")) - path := filepath.Join("data", "mintages", code) - f, _ := os.Open(path) // TODO: Handle error - defer f.Close() - data, _ := mintage.Parse(f, path) // TODO: Handle error + code := strings.ToLower(r.FormValue("code")) + if !slices.ContainsFunc(countries, func(c lib.Country) bool { + return c.Code == code + }) { + code = countries[0].Code + } + ctype := strings.ToLower(r.FormValue("type")) switch ctype { case "circ", "nifc", "proof": default: ctype = "circ" } + path := filepath.Join("data", "mintages", code) + f, err := os.Open(path) + if err != nil { + throwError(http.StatusInternalServerError, err, w, r) + return + } + defer f.Close() + + data, err := mintage.Parse(f, path) + if err != nil { + throwError(http.StatusInternalServerError, err, w, r) + return + } + ctx := context.WithValue(r.Context(), "code", code) ctx = context.WithValue(ctx, "type", ctype) ctx = context.WithValue(ctx, "mintages", data) @@ -152,3 +183,17 @@ func setUserLanguage(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, c.Value, http.StatusFound) } } + +func throwError(status int, err error, w http.ResponseWriter, r *http.Request) { + w.WriteHeader(status) + if emailDisabled { + log.Print(err) + } else { + go func() { + if err := email.ServerError(err); err != nil { + log.Print(err) + } + }() + } + template.Base(template.Error(status)).Render(r.Context(), w) +} diff --git a/template/error.templ b/template/error.templ new file mode 100644 index 0000000..d2fed83 --- /dev/null +++ b/template/error.templ @@ -0,0 +1,24 @@ +package template + +import ( + "net/http" + "strconv" + + "git.thomasvoss.com/euro-cash.eu/lib" +) + +templ Error(status int) { + {{ p := ctx.Value("printer").(lib.Printer) }} +
+ @navbar() +

{ strconv.Itoa(status) } { http.StatusText(status) }

+
+
+

+ { p.T("If you’re seeing this page, it means that something went wrong on our end that we need to fix. Our team has been notified of this error, and we apologise for the inconvenience.") } +

+

+ @templ.Raw(p.T("If this issue persists, don’t hesitate to contact @onetruemangoman on Discord or to email us at %s.", contactEmail)) +

+
+} -- cgit v1.2.3