diff --git a/config.go b/config.go index be0ea2b..2fa28fd 100644 --- a/config.go +++ b/config.go @@ -71,6 +71,7 @@ type configBlog struct { RandomPost *configRandomPost `mapstructure:"randomPost"` Comments *configComments `mapstructure:"comments"` Map *configGeoMap `mapstructure:"map"` + Contact *configContact `mapstructure:"contact"` } type configSection struct { @@ -151,6 +152,19 @@ type configGeoMap struct { Path string `mapstructure:"path"` } +type configContact struct { + Enabled bool `mapstructure:"enabled"` + Path string `mapstructure:"path"` + Title string `mapstructure:"title"` + Description string `mapstructure:"description"` + SMTPHost string `mapstructure:"smtpHost"` + SMTPPort int `mapstructure:"smtpPort"` + SMTPUser string `mapstructure:"smtpUser"` + SMTPPassword string `mapstructure:"smtpPassword"` + EmailFrom string `mapstructure:"emailFrom"` + EmailTo string `mapstructure:"emailTo"` +} + type configUser struct { Nick string `mapstructure:"nick"` Name string `mapstructure:"name"` diff --git a/contact.go b/contact.go new file mode 100644 index 0000000..0b2d494 --- /dev/null +++ b/contact.go @@ -0,0 +1,104 @@ +package main + +import ( + "bytes" + "fmt" + "log" + "net/http" + "net/smtp" + "strconv" + "strings" + "time" + + "github.com/microcosm-cc/bluemonday" +) + +const defaultContactPath = "/contact" + +func (a *goBlog) serveContactForm(w http.ResponseWriter, r *http.Request) { + blog := r.Context().Value(blogContextKey).(string) + cc := a.cfg.Blogs[blog].Contact + a.render(w, r, templateContact, &renderData{ + BlogString: blog, + Data: map[string]interface{}{ + "title": cc.Title, + "description": cc.Description, + }, + }) +} + +func (a *goBlog) sendContactSubmission(w http.ResponseWriter, r *http.Request) { + // Get form values + strict := bluemonday.StrictPolicy() + // Name + formName := strings.TrimSpace(strict.Sanitize(r.FormValue("name"))) + // Email + formEmail := strings.TrimSpace(strict.Sanitize(r.FormValue("email"))) + // Website + formWebsite := strings.TrimSpace(strict.Sanitize(r.FormValue("website"))) + // Message + formMessage := strings.TrimSpace(strict.Sanitize(r.FormValue("message"))) + if formMessage == "" { + a.serveError(w, r, "Message is empty", http.StatusBadRequest) + return + } + // Build message + var message bytes.Buffer + if formName != "" { + _, _ = fmt.Fprintf(&message, "Name: %s", formName) + _, _ = fmt.Fprintln(&message) + } + if formEmail != "" { + _, _ = fmt.Fprintf(&message, "Email: %s", formEmail) + _, _ = fmt.Fprintln(&message) + } + if formWebsite != "" { + _, _ = fmt.Fprintf(&message, "Website: %s", formWebsite) + _, _ = fmt.Fprintln(&message) + } + if message.Len() > 0 { + _, _ = fmt.Fprintln(&message) + } + _, _ = message.WriteString(formMessage) + // Send submission + blog := r.Context().Value(blogContextKey).(string) + if cc := a.cfg.Blogs[blog].Contact; cc != nil && cc.SMTPHost != "" && cc.EmailFrom != "" && cc.EmailTo != "" { + // Build email + var email bytes.Buffer + if ef := cc.EmailFrom; ef != "" { + _, _ = fmt.Fprintf(&email, "From: %s <%s>", defaultIfEmpty(a.cfg.Blogs[blog].Title, "GoBlog"), cc.EmailFrom) + _, _ = fmt.Fprintln(&email) + } + _, _ = fmt.Fprintf(&email, "To: %s", cc.EmailTo) + _, _ = fmt.Fprintln(&email) + if formEmail != "" { + _, _ = fmt.Fprintf(&email, "Reply-To: %s", formEmail) + _, _ = fmt.Fprintln(&email) + } + _, _ = fmt.Fprintf(&email, "Date: %s", time.Now().UTC().Format(time.RFC1123Z)) + _, _ = fmt.Fprintln(&email) + _, _ = fmt.Fprintln(&email, "Subject: New message") + _, _ = fmt.Fprintln(&email) + _, _ = fmt.Fprintln(&email, message.String()) + // Send email + auth := smtp.PlainAuth("", cc.SMTPUser, cc.SMTPPassword, cc.SMTPHost) + port := cc.SMTPPort + if port == 0 { + port = 587 + } + if err := smtp.SendMail(cc.SMTPHost+":"+strconv.Itoa(port), auth, cc.EmailFrom, []string{cc.EmailTo}, email.Bytes()); err != nil { + log.Println("Failed to send mail:", err.Error()) + } + } else { + log.Println("New contact submission not send as email, config missing") + } + // Send notification + a.sendNotification(message.String()) + // Give feedback + a.render(w, r, templateContact, &renderData{ + BlogString: blog, + Data: map[string]interface{}{ + "sent": true, + }, + }) +} diff --git a/example-config.yml b/example-config.yml index 101ba41..d1e6d5e 100644 --- a/example-config.yml +++ b/example-config.yml @@ -216,4 +216,16 @@ blogs: # Map map: enabled: true # Enable the map feature (shows a map with all post locations) - path: /map # (Optional) Set a custom path (relative to blog path) \ No newline at end of file + path: /map # (Optional) Set a custom path (relative to blog path), default is /map + # Contact form + contact: + enabled: true # Enable a contact form + title: "Contact me!" # (Optional) Title to show above the form + description: "Feel free to send me a message" # (Optional) Description to show above the form + path: /contact # (Optional) Set a custom path (relative to blog path), default is /contact + smtpHost: smtp.example.com # SMTP host + smtpPort: 587 # (Optional) SMTP port, default is 587 + smtpUser: mail@example.com # SMTP user + smtpPassword: secret # SMTP password + emailFrom: blog@example.com # Email sender + emailTo: mail@example.com # Email recipient \ No newline at end of file diff --git a/geoMap.go b/geoMap.go index 556988e..9230853 100644 --- a/geoMap.go +++ b/geoMap.go @@ -91,13 +91,13 @@ func (a *goBlog) serveLeaflet(basePath string) http.HandlerFunc { switch path.Ext(fileName) { case ".js": w.Header().Set(contentType, contenttype.JS) - a.min.Write(w, contenttype.JSUTF8, fb) + _, _ = a.min.Write(w, contenttype.JSUTF8, fb) case ".css": w.Header().Set(contentType, contenttype.CSS) - a.min.Write(w, contenttype.CSSUTF8, fb) + _, _ = a.min.Write(w, contenttype.CSSUTF8, fb) default: w.Header().Set(contentType, http.DetectContentType(fb)) - w.Write(fb) + _, _ = w.Write(fb) } } } @@ -137,7 +137,7 @@ func (a *goBlog) proxyTiles(basePath string) http.HandlerFunc { w.Header().Set(k, res.Header.Get(k)) } w.WriteHeader(res.StatusCode) - io.Copy(w, res.Body) + _, _ = io.Copy(w, res.Body) _ = res.Body.Close() } } diff --git a/http.go b/http.go index 4ffc412..409fc98 100644 --- a/http.go +++ b/http.go @@ -444,6 +444,17 @@ func (a *goBlog) buildRouter() (*chi.Mux, error) { }) } + // Contact + if cc := blogConfig.Contact; cc != nil && cc.Enabled { + contactPath := blogConfig.getRelativePath(defaultIfEmpty(cc.Path, defaultContactPath)) + r.Route(contactPath, func(r chi.Router) { + r.Use(privateModeHandler...) + r.Use(a.cache.cacheMiddleware, sbm) + r.Get("/", a.serveContactForm) + r.With(a.captchaMiddleware).Post("/", a.sendContactSubmission) + }) + } + } // Sitemap diff --git a/original-assets/styles/styles.scss b/original-assets/styles/styles.scss index ca283b3..4dfb1d3 100644 --- a/original-assets/styles/styles.scss +++ b/original-assets/styles/styles.scss @@ -115,7 +115,7 @@ form { } } -.fw-form { +form.fw { @extend .fw; input:not([type]), input[type="submit"], input[type="button"], input[type="text"], input[type="email"], input[type="url"], input[type="password"], input[type="file"], textarea, select { diff --git a/render.go b/render.go index 9d5fd5b..5b90096 100644 --- a/render.go +++ b/render.go @@ -40,6 +40,7 @@ const ( templateWebmentionAdmin = "webmentionadmin" templateBlogroll = "blogroll" templateGeoMap = "geomap" + templateContact = "contact" ) func (a *goBlog) initRendering() error { diff --git a/templates/assets/css/styles.css b/templates/assets/css/styles.css index 636c378..dd75205 100644 --- a/templates/assets/css/styles.css +++ b/templates/assets/css/styles.css @@ -159,7 +159,7 @@ footer * { display: inline; } -.fw, img, audio, .fw-form, .fw-form input:not([type]), .fw-form input[type=submit], .fw-form input[type=button], .fw-form input[type=text], .fw-form input[type=email], .fw-form input[type=url], .fw-form input[type=password], .fw-form input[type=file], .fw-form textarea, .fw-form select { +.fw, img, audio, form.fw, form.fw input:not([type]), form.fw input[type=submit], form.fw input[type=button], form.fw input[type=text], form.fw input[type=email], form.fw input[type=url], form.fw input[type=password], form.fw input[type=file], form.fw textarea, form.fw select { width: 100%; } diff --git a/templates/captcha.gohtml b/templates/captcha.gohtml index badb30e..b743786 100644 --- a/templates/captcha.gohtml +++ b/templates/captcha.gohtml @@ -6,14 +6,14 @@

{{ string .Blog.Lang "captcha" }}

-
+ - +
{{ end }} diff --git a/templates/contact.gohtml b/templates/contact.gohtml new file mode 100644 index 0000000..b08f506 --- /dev/null +++ b/templates/contact.gohtml @@ -0,0 +1,25 @@ +{{ define "title" }} + {{ .Data.Name }} +{{ end }} + +{{ define "main" }} +
+ {{ if .Data.sent }} +

{{ string .Blog.Lang "messagesent" }}

+ {{ else }} + {{ with .Data.title }}

{{ . }}

{{ end }} + {{ with .Data.description }}{{ md . }}{{ end }} +
+ + + + + +
+ {{ end }} +
+{{ end }} + +{{ define "contact" }} + {{ template "base" . }} +{{ end }} \ No newline at end of file diff --git a/templates/editor.gohtml b/templates/editor.gohtml index 6710e01..d81d0fc 100644 --- a/templates/editor.gohtml +++ b/templates/editor.gohtml @@ -6,7 +6,7 @@

{{ string .Blog.Lang "editor" }}

{{ string .Blog.Lang "create" }}

-
+ - +
{{ end }} \ No newline at end of file diff --git a/templates/login.gohtml b/templates/login.gohtml index 5831bef..89c43e4 100644 --- a/templates/login.gohtml +++ b/templates/login.gohtml @@ -5,7 +5,7 @@ {{ define "main" }}

{{ string .Blog.Lang "login" }}

-
+ @@ -15,7 +15,7 @@ {{ if .Data.totp }} {{ end }} - +
{{ include "author" . }}
diff --git a/templates/search.gohtml b/templates/search.gohtml index ed1e411..8ccf6aa 100644 --- a/templates/search.gohtml +++ b/templates/search.gohtml @@ -9,9 +9,9 @@ {{ if (or .Blog.Search.Title .Blog.Search.Description) }}
{{ end }} -
+ - +
{{ end }} diff --git a/templates/strings/de.yaml b/templates/strings/de.yaml index 5b6d2d2..69182eb 100644 --- a/templates/strings/de.yaml +++ b/templates/strings/de.yaml @@ -5,12 +5,14 @@ comments: "Kommentare" confirmdelete: "Löschen bestätigen" connectedviator: "Verbunden über Tor." connectviator: "Über Tor verbinden." +contactsend: "Senden" create: "Erstellen" delete: "Löschen" docomment: "Kommentieren" download: "Herunterladen" drafts: "Entwürfe" editor: "Editor" +emailopt: "E-Mail (optional)" fileuses: "Datei-Verwendungen" interactions: "Interaktionen & Kommentare" interactionslabel: "Hast du eine Antwort hierzu veröffentlicht? Füge hier die URL ein." @@ -21,6 +23,8 @@ locationfailed: "Abfragen des Standorts fehlgeschlagen" locationget: "Standort abfragen" locationnotsupported: "Die Standort-API wird von diesem Browser nicht unterstützt" mediafiles: "Medien-Dateien" +message: "Nachricht" +messagesent: "Nachricht gesendet" next: "Weiter" nofiles: "Keine Dateien" nolocations: "Keine Posts mit Standorten" diff --git a/templates/strings/default.yaml b/templates/strings/default.yaml index d5cb1cf..0aa9959 100644 --- a/templates/strings/default.yaml +++ b/templates/strings/default.yaml @@ -10,12 +10,14 @@ comments: "Comments" confirmdelete: "Confirm deletion" connectedviator: "Connected via Tor." connectviator: "Connect via Tor." +contactsend: "Send" create: "Create" delete: "Delete" docomment: "Comment" download: "Download" drafts: "Drafts" editor: "Editor" +emailopt: "Email (optional)" feed: "Feed" fileuses: "file uses" indieauth: "IndieAuth" @@ -30,6 +32,8 @@ locationnotsupported: "The location API is not supported by this browser" login: "Login" logout: "Logout" mediafiles: "Media files" +message: "Message" +messagesent: "Message sent" nameopt: "Name (optional)" next: "Next" nofiles: "No files"