package main import ( "bytes" "context" "encoding/json" "fmt" "html/template" "io" "log" "net/http" "net/url" "os" "os/signal" "strings" "time" "gopkg.in/ini.v1" ) // ------------- Api configs ------------- type DefaultSectionConfig struct { Live bool TestEmail string SenderName string } type HMCConfig struct { ApiSecret string ApiUrl string City string } type PostmarkConfig struct { ServerToken string ApiUrl string TemplateId int SenderEmail string MessageStream string } type ApiConfig struct { Default DefaultSectionConfig Hmc HMCConfig Postmark PostmarkConfig } // ------------- HMC Api Responses ------------- type EmailsResonse struct { Emails []string `json:"emails"` } // Load the HMCsend-email app.ini and store the values in a structure, some of the keys in here // we probably don't actualy care about for this usage. func loadConfig() ApiConfig { // Read the config file and get the HMC api settings config, err := ini.Load("app.ini") if err != nil { panic(err) } defSection := config.Section("DEFAULT") def := DefaultSectionConfig{ Live: defSection.Key("LIVE").MustBool(), TestEmail: defSection.Key("TEST_EMAIL").String(), SenderName: defSection.Key("SENDER_NAME").String(), } hmcSection := config.Section("hmc") hmc := HMCConfig{ ApiSecret: hmcSection.Key("SHARED_SECRET").String(), ApiUrl: hmcSection.Key("API_URL").String(), City: hmcSection.Key("CITY").String(), } postmarkSection := config.Section("postmark") postmark := PostmarkConfig{ ServerToken: postmarkSection.Key("SERVER_TOKEN").String(), ApiUrl: postmarkSection.Key("API_URL").String(), TemplateId: postmarkSection.Key("TEMPLATE_ID").MustInt(), SenderEmail: postmarkSection.Key("SENDER_EMAIL").String(), MessageStream: postmarkSection.Key("MESSAGE_STREAM").String(), } return ApiConfig{ Default: def, Hmc: hmc, Postmark: postmark, } } func main() { apiConfig := loadConfig() postmarkTemplate := getPostmarkTemplate(apiConfig.Postmark) // Prepare the go html templates and static file server templates := template.Must(template.ParseGlob("templates/*.html")) static_files := http.FileServer(http.Dir("static/")) mux := http.NewServeMux() mux.Handle("/static/", http.StripPrefix("/static/", static_files)) // The base endpoint for the web-app simply renders the index tempalte mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { log.Printf("Hello '%v %v' handler", r.Method, r.URL) templates.ExecuteTemplate(w, "index", nil) }) // Endpoint called when editing the text area and posts to the postmark api to generate the email preview mux.HandleFunc("/mail-content", func(w http.ResponseWriter, r *http.Request) { log.Printf("Hello '%v %v' handler", r.Method, r.URL) body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return } defer r.Body.Close() textarea_content := string(body) decoded_textarea, err := url.QueryUnescape(textarea_content) if err != nil { panic(err) } email_content, _ := strings.CutPrefix(decoded_textarea, "email_input=") // Return a response to HTMX model := PostmarkTemplateModel{ Name: apiConfig.Default.SenderName, Email: apiConfig.Postmark.SenderEmail, Body: email_content, Subject: "This is a test subject", // TODO(jack): Get from user input on page } w.Write([]byte(renderPostmarkTemplate(apiConfig.Postmark, postmarkTemplate, model))) }) // Endpoint called on load or button press by htmx to retrieve and populate the mailing list mux.HandleFunc("/mailing_list", func(w http.ResponseWriter, r *http.Request) { body := []byte(fmt.Sprintf("{\"city\": \"%s\"}", apiConfig.Hmc.City)) req, err := http.NewRequest("POST", apiConfig.Hmc.ApiUrl, bytes.NewBuffer(body)) if err != nil { panic(err) } req.Header.Add("Content-Type", "application/json") req.Header.Add("Authorization", apiConfig.Hmc.ApiSecret) client := &http.Client{} res, err := client.Do(req) if err != nil { panic(err) } defer res.Body.Close() response := &EmailsResonse{} decode_error := json.NewDecoder(res.Body).Decode(&response) if decode_error != nil { panic(decode_error) } templates.ExecuteTemplate(w, "mailing_list", response) }) srv := &http.Server{ Addr: "0.0.0.0:80", // Good practice to set timeouts to avoid Slowloris attacks. WriteTimeout: time.Second * 15, ReadTimeout: time.Second * 15, IdleTimeout: time.Second * 60, Handler: mux, } go func() { if err := srv.ListenAndServe(); err != nil { log.Println(err) } }() ////////////////////////////// Cleanup ////////////////////////////// c := make(chan os.Signal, 1) // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught. signal.Notify(c, os.Interrupt) log.Println("Waiting for interrupt") // Block until we receive our signal. <-c // Create a deadline to wait for. ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second) // Defer calling the canclefunction returned from context.WithTimeout defer cancel() // Doesn't block if no connections, but will otherwise wait // until the timeout deadline. srv.Shutdown(ctx) // Optionally, you could run srv.Shutdown in a goroutine and block on // <-ctx.Done() if your application should wait for other services // to finalize based on context cancellation. log.Println("shutting down") os.Exit(0) }