diff options
-rw-r--r-- | Makefile | 2 | ||||
-rw-r--r-- | lib/email/email.go | 75 | ||||
-rw-r--r-- | main.go | 57 | ||||
-rw-r--r-- | template/error.templ | 24 |
4 files changed, 151 insertions, 7 deletions
@@ -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 +} @@ -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) }} + <header> + @navbar() + <h1>{ strconv.Itoa(status) } { http.StatusText(status) }</h1> + </header> + <main> + <p> + { 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.") } + </p> + <p> + @templ.Raw(p.T("If this issue persists, don’t hesitate to contact @onetruemangoman on Discord or to email us at %s.", contactEmail)) + </p> + </main> +} |