From 5934036a0938953d60342d3619265b2474ee9fee Mon Sep 17 00:00:00 2001 From: Jack Punter Date: Fri, 19 Apr 2024 01:24:55 +0100 Subject: [PATCH] Add the ability to view a preview of the email entered into the textarea --- main.go | 99 ++++++++++++++++++++++++++----- postmark.go | 135 +++++++++++++++++++++++++++++++++++++++++++ templates/index.html | 2 +- 3 files changed, 222 insertions(+), 14 deletions(-) create mode 100644 postmark.go diff --git a/main.go b/main.go index af7cec2..1404284 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "io" "log" "net/http" + "net/url" "os" "os/signal" "strings" @@ -17,20 +18,80 @@ import ( "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"` } -func main() { +// 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) } - HMC_CFG := config.Section("hmc") - HMC_API_SECRET := HMC_CFG.Key("SHARED_SECRET").String() - HMC_API_URL := HMC_CFG.Key("API_URL").String() - HMC_CITY := HMC_CFG.Key("CITY").String() + + 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")) @@ -39,11 +100,13 @@ func main() { 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) @@ -52,24 +115,34 @@ func main() { return } defer r.Body.Close() - textarea_content := string(body) - email_content, _ := strings.CutPrefix(textarea_content, "email_input=") + 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 - response := fmt.Sprintf("

Received content: %s

", email_content) - w.Write([]byte(response)) + model := PostmarkTemplateModel{ + Name: apiConfig.Default.SenderName, + Email: apiConfig.Default.TestEmail, + 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\"}", HMC_CITY)) + body := []byte(fmt.Sprintf("{\"city\": \"%s\"}", apiConfig.Hmc.City)) - req, err := http.NewRequest("POST", HMC_API_URL, bytes.NewBuffer(body)) + 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", HMC_API_SECRET) + req.Header.Add("Authorization", apiConfig.Hmc.ApiSecret) client := &http.Client{} res, err := client.Do(req) @@ -79,7 +152,7 @@ func main() { defer res.Body.Close() response := &EmailsResonse{} - decode_error := json.NewDecoder(res.Body).Decode(response) + decode_error := json.NewDecoder(res.Body).Decode(&response) if decode_error != nil { panic(decode_error) } diff --git a/postmark.go b/postmark.go new file mode 100644 index 0000000..a934655 --- /dev/null +++ b/postmark.go @@ -0,0 +1,135 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" +) + +// The response from a get reqeuest to the postmark tempalte API. +type PostmarkTemplate struct { + TemplateId int + Name string + Subject string + HtmlBody string + TextBody string + AssociatedServerId int + Active bool + Alias string + TemplateType string + LayoutTemplate string +} + +// The model that we're using for the template that abner made +type PostmarkTemplateModel struct { + Name string `json:"name"` + Email string `json:"email"` + Body string `json:"body"` + Subject string `json:"subject"` +} + +// The request body for the postmark validate endpoint which renders a provided tempalte if its valid +type ValidateTemplateBody struct { + Subject string + HTMLBody string + TextBody string + + TestRenderModel PostmarkTemplateModel + // There are other body options but i think this is all we care about + // InlineCssForHtmlTestRender bool + // TemplateType string +} + +// The validation error types returned from the postmark template validation api +type ValidationError struct { + Message string + Line int + CharacterPosition int +} + +type ContentBodyValidationResponse struct { + ContentIsValid bool + ValidationErrors []ValidationError + RenderedContent string +} + +// The response from the postmark tempalte validation endpoint +type ValidationResponse struct { + AllContentIsValid bool + Subject ContentBodyValidationResponse + HtmlBody ContentBodyValidationResponse + TextBody ContentBodyValidationResponse + SuggestedTemplateModel interface{} +} + +// Gets the tempalte in the postmark config section of the app.ini file from teh postmark server so we can render it +func getPostmarkTemplate(cfg PostmarkConfig) PostmarkTemplate { + // Get Template + getTemplateURL := fmt.Sprintf("https://api.postmarkapp.com/templates/%v", cfg.TemplateId) + getReq, err := http.NewRequest(http.MethodGet, getTemplateURL, bytes.NewBuffer([]byte(""))) + if err != nil { + panic(err) + } + getReq.Header.Add("Accept", "application/json") + getReq.Header.Add("X-Postmark-Server-Token", cfg.ServerToken) + + client := &http.Client{} + getResp, err := client.Do(getReq) + if err != nil { + panic(err) + } + defer getReq.Body.Close() + + template := PostmarkTemplate{} + decode_error := json.NewDecoder(getResp.Body).Decode(&template) + + if decode_error != nil { + panic(decode_error) + } + return template +} + +// Given a tempalte and model use the postmark tempalte validation api to render the template +func renderPostmarkTemplate(cfg PostmarkConfig, template PostmarkTemplate, model PostmarkTemplateModel) string { + bodyObj := ValidateTemplateBody{ + Subject: template.Subject, + HTMLBody: template.HtmlBody, + TextBody: template.TextBody, + TestRenderModel: model, + } + + body, err := json.Marshal(bodyObj) + if err != nil { + panic(err) + } + + req, err := http.NewRequest("POST", "https://api.postmarkapp.com/templates/validate", bytes.NewBuffer(body)) + if err != nil { + panic(err) + } + + req.Header.Add("Content-Type", "application/json") + req.Header.Add("Accept", "application/json") + req.Header.Add("X-Postmark-Server-Token", cfg.ServerToken) + + client := &http.Client{} + resp, err := client.Do(req) + if err != nil { + panic(err) + } + defer resp.Body.Close() + + response := &ValidationResponse{} + rawBodyContent, err := io.ReadAll(resp.Body) + if err != nil { + panic(err) + } + decode_error := json.NewDecoder(bytes.NewBuffer(rawBodyContent)).Decode(response) + if decode_error != nil { + panic(decode_error) + } + + return response.HtmlBody.RenderedContent +} diff --git a/templates/index.html b/templates/index.html index f419149..7c2923c 100644 --- a/templates/index.html +++ b/templates/index.html @@ -18,7 +18,7 @@