Add the ability to view a preview of the email entered into the textarea
parent
cf5c04759a
commit
5934036a09
99
main.go
99
main.go
|
@ -9,6 +9,7 @@ import (
|
||||||
"io"
|
"io"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"strings"
|
"strings"
|
||||||
|
@ -17,20 +18,80 @@ import (
|
||||||
"gopkg.in/ini.v1"
|
"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 {
|
type EmailsResonse struct {
|
||||||
Emails []string `json:"emails"`
|
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
|
// Read the config file and get the HMC api settings
|
||||||
config, err := ini.Load("app.ini")
|
config, err := ini.Load("app.ini")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
HMC_CFG := config.Section("hmc")
|
|
||||||
HMC_API_SECRET := HMC_CFG.Key("SHARED_SECRET").String()
|
defSection := config.Section("DEFAULT")
|
||||||
HMC_API_URL := HMC_CFG.Key("API_URL").String()
|
def := DefaultSectionConfig{
|
||||||
HMC_CITY := HMC_CFG.Key("CITY").String()
|
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
|
// Prepare the go html templates and static file server
|
||||||
templates := template.Must(template.ParseGlob("templates/*.html"))
|
templates := template.Must(template.ParseGlob("templates/*.html"))
|
||||||
|
@ -39,11 +100,13 @@ func main() {
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.Handle("/static/", http.StripPrefix("/static/", static_files))
|
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) {
|
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("Hello '%v %v' handler", r.Method, r.URL)
|
log.Printf("Hello '%v %v' handler", r.Method, r.URL)
|
||||||
templates.ExecuteTemplate(w, "index", nil)
|
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) {
|
mux.HandleFunc("/mail-content", func(w http.ResponseWriter, r *http.Request) {
|
||||||
log.Printf("Hello '%v %v' handler", r.Method, r.URL)
|
log.Printf("Hello '%v %v' handler", r.Method, r.URL)
|
||||||
body, err := io.ReadAll(r.Body)
|
body, err := io.ReadAll(r.Body)
|
||||||
|
@ -52,24 +115,34 @@ func main() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer r.Body.Close()
|
defer r.Body.Close()
|
||||||
|
|
||||||
textarea_content := string(body)
|
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
|
// Return a response to HTMX
|
||||||
response := fmt.Sprintf("<p>Received content: %s</p>", email_content)
|
model := PostmarkTemplateModel{
|
||||||
w.Write([]byte(response))
|
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) {
|
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 {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
req.Header.Add("Content-Type", "application/json")
|
req.Header.Add("Content-Type", "application/json")
|
||||||
req.Header.Add("Authorization", HMC_API_SECRET)
|
req.Header.Add("Authorization", apiConfig.Hmc.ApiSecret)
|
||||||
|
|
||||||
client := &http.Client{}
|
client := &http.Client{}
|
||||||
res, err := client.Do(req)
|
res, err := client.Do(req)
|
||||||
|
@ -79,7 +152,7 @@ func main() {
|
||||||
defer res.Body.Close()
|
defer res.Body.Close()
|
||||||
|
|
||||||
response := &EmailsResonse{}
|
response := &EmailsResonse{}
|
||||||
decode_error := json.NewDecoder(res.Body).Decode(response)
|
decode_error := json.NewDecoder(res.Body).Decode(&response)
|
||||||
if decode_error != nil {
|
if decode_error != nil {
|
||||||
panic(decode_error)
|
panic(decode_error)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
}
|
|
@ -18,7 +18,7 @@
|
||||||
<div class="row" style="display: flex;">
|
<div class="row" style="display: flex;">
|
||||||
<div class="column" style="flex: 50%;">
|
<div class="column" style="flex: 50%;">
|
||||||
<textarea name="email_input" id="typebox" rows="32" cols="64" style="resize: none;"
|
<textarea name="email_input" id="typebox" rows="32" cols="64" style="resize: none;"
|
||||||
hx-trigger="keyup changed delay:500ms" hx-post="/mail-content" hx-target="#email-preview"
|
hx-trigger="keyup changed delay:1000ms" hx-post="/mail-content" hx-target="#email-preview"
|
||||||
placeholder="Type out your email here..."></textarea>
|
placeholder="Type out your email here..."></textarea>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue