mirror of https://github.com/jlelse/GoBlog
Contact form feature
This commit is contained in:
parent
2cee446dff
commit
bdbaf9b915
14
config.go
14
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"`
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
})
|
||||
}
|
|
@ -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)
|
||||
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
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
11
http.go
11
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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -40,6 +40,7 @@ const (
|
|||
templateWebmentionAdmin = "webmentionadmin"
|
||||
templateBlogroll = "blogroll"
|
||||
templateGeoMap = "geomap"
|
||||
templateContact = "contact"
|
||||
)
|
||||
|
||||
func (a *goBlog) initRendering() error {
|
||||
|
|
|
@ -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%;
|
||||
}
|
||||
|
||||
|
|
|
@ -6,14 +6,14 @@
|
|||
<main>
|
||||
<h1>{{ string .Blog.Lang "captcha" }}</h1>
|
||||
<img src="/captcha/{{ .Data.captchaid }}.png" class="captchaimg">
|
||||
<form class="fw-form p" method="post">
|
||||
<form class="fw p" method="post">
|
||||
<input type="hidden" name="captchaaction" value="captcha">
|
||||
<input type="hidden" name="captchamethod" value="{{ .Data.captchamethod }}">
|
||||
<input type="hidden" name="captchaheaders" value="{{ .Data.captchaheaders }}">
|
||||
<input type="hidden" name="captchabody" value="{{ .Data.captchabody }}">
|
||||
<input type="hidden" name="captchaid" value="{{ .Data.captchaid }}">
|
||||
<input type="text" name="digits" placeholder="{{ string .Blog.Lang "captchainstructions" }}" required>
|
||||
<input class="fw" type="submit" value="{{ string .Blog.Lang "submit" }}">
|
||||
<input type="submit" value="{{ string .Blog.Lang "submit" }}">
|
||||
</form>
|
||||
</main>
|
||||
{{ end }}
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
{{ define "title" }}
|
||||
<title>{{ .Data.Name }}</title>
|
||||
{{ end }}
|
||||
|
||||
{{ define "main" }}
|
||||
<main>
|
||||
{{ if .Data.sent }}
|
||||
<p>{{ string .Blog.Lang "messagesent" }}</p>
|
||||
{{ else }}
|
||||
{{ with .Data.title }}<h1>{{ . }}</h1>{{ end }}
|
||||
{{ with .Data.description }}{{ md . }}{{ end }}
|
||||
<form class="fw p" method="post">
|
||||
<input type="text" name="name" placeholder="{{ string .Blog.Lang "nameopt" }}">
|
||||
<input type="url" name="website" placeholder="{{ string .Blog.Lang "websiteopt" }}">
|
||||
<input type="email" name="email" placeholder="{{ string .Blog.Lang "emailopt" }}">
|
||||
<textarea name="message" required placeholder="{{ string .Blog.Lang "message" }}"></textarea>
|
||||
<input type="submit" value="{{ string .Blog.Lang "contactsend" }}">
|
||||
</form>
|
||||
{{ end }}
|
||||
</main>
|
||||
{{ end }}
|
||||
|
||||
{{ define "contact" }}
|
||||
{{ template "base" . }}
|
||||
{{ end }}
|
|
@ -6,7 +6,7 @@
|
|||
<main>
|
||||
<h1>{{ string .Blog.Lang "editor" }}</h1>
|
||||
<h2>{{ string .Blog.Lang "create" }}</h2>
|
||||
<form class="fw-form p" method="post">
|
||||
<form class="fw p" method="post">
|
||||
<input type="hidden" name="h" value="entry">
|
||||
<textarea name="content" class="monospace h400p formcache" id="create-input">---
|
||||
status: draft
|
||||
|
@ -21,7 +21,7 @@ tags:
|
|||
<input type="submit" value="{{ string .Blog.Lang "create" }}">
|
||||
</form>
|
||||
<h2 id="update">{{ string .Blog.Lang "update" }}</h2>
|
||||
<form class="fw-form p" method="post" action="#update">
|
||||
<form class="fw p" method="post" action="#update">
|
||||
{{ if .Data.UpdatePostURL }}
|
||||
<input type="hidden" name="editoraction" value="updatepost">
|
||||
<input type="hidden" name="url" value="{{ .Data.UpdatePostURL }}">
|
||||
|
@ -33,7 +33,7 @@ tags:
|
|||
<input type="submit" value="{{ string .Blog.Lang "update" }}">
|
||||
</form>
|
||||
<h2 id="delete">{{ string .Blog.Lang "delete" }}</h2>
|
||||
<form class="fw-form p" method="post" action="#delete">
|
||||
<form class="fw p" method="post" action="#delete">
|
||||
{{ if .Data.DeleteURL }}
|
||||
<input type="hidden" name="action" value="delete">
|
||||
<input type="url" name="url" placeholder="URL" value="{{ .Data.DeleteURL }}">
|
||||
|
@ -49,14 +49,14 @@ tags:
|
|||
<p><a href="{{ .Blog.RelativePath "/editor/private" }}">{{ string .Blog.Lang "privateposts" }}</a></p>
|
||||
<p><a href="{{ .Blog.RelativePath "/editor/unlisted" }}">{{ string .Blog.Lang "unlistedposts" }}</a></p>
|
||||
<h2>{{ string .Blog.Lang "upload" }}</h2>
|
||||
<form class="fw-form p" method="post" enctype="multipart/form-data">
|
||||
<form class="fw p" method="post" enctype="multipart/form-data">
|
||||
<input type="hidden" name="editoraction" value="upload">
|
||||
<input type="file" name="file">
|
||||
<input type="submit" value="{{ string .Blog.Lang "upload" }}">
|
||||
</form>
|
||||
<p><a href="{{ .Blog.RelativePath "/editor/files" }}">{{ string .Blog.Lang "mediafiles" }}</a></p>
|
||||
<h2>{{ string .Blog.Lang "location" }}</h2>
|
||||
<form class="fw-form p">
|
||||
<form class="fw p">
|
||||
<input id="geobtn" type="button" value="{{ string .Blog.Lang "locationget" }}" data-failed="{{ string .Blog.Lang "locationfailed" }}" data-notsupported="{{ string .Blog.Lang "locationnotsupported" }}">
|
||||
<input id="geostatus" type="text" class="hide" readonly>
|
||||
</form>
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
{{ $blog := .Blog }}
|
||||
<h2>{{ string .Blog.Lang "mediafiles" }}</h2>
|
||||
{{ if .Data.Files }}
|
||||
<form class="fw-form p" method="post">
|
||||
<form class="fw p" method="post">
|
||||
<select name="filename">
|
||||
{{ $uses := .Data.Uses }}
|
||||
{{ range $i, $file := .Data.Files }}
|
||||
|
|
|
@ -14,18 +14,18 @@
|
|||
{{ end }}
|
||||
</ul>
|
||||
{{ end }}
|
||||
<form class="fw-form p" method="post" action="/webmention">
|
||||
<form class="fw p" method="post" action="/webmention">
|
||||
<label for="wm-source" class="p">{{ string .Blog.Lang "interactionslabel" }}</label>
|
||||
<input id="wm-source" type="url" name="source" placeholder="URL" required>
|
||||
<input type="hidden" name="target" value="{{ .Canonical }}">
|
||||
<input class="fw" type="submit" value="{{ string .Blog.Lang "send" }}">
|
||||
<input type="submit" value="{{ string .Blog.Lang "send" }}">
|
||||
</form>
|
||||
<form class="fw-form p" method="post" action="{{ .Blog.RelativePath "/comment" }}">
|
||||
<form class="fw p" method="post" action="{{ .Blog.RelativePath "/comment" }}">
|
||||
<input type="hidden" name="target" value="{{ .Canonical }}">
|
||||
<input type="text" name="name" placeholder="{{ string .Blog.Lang "nameopt" }}">
|
||||
<input type="url" name="website" placeholder="{{ string .Blog.Lang "websiteopt" }}">
|
||||
<textarea name="comment" required placeholder="{{ string .Blog.Lang "comment" }}"></textarea>
|
||||
<input class="fw" type="submit" value="{{ string .Blog.Lang "docomment" }}">
|
||||
<input type="submit" value="{{ string .Blog.Lang "docomment" }}">
|
||||
</form>
|
||||
</details>
|
||||
{{ end }}
|
|
@ -5,7 +5,7 @@
|
|||
{{ define "main" }}
|
||||
<main>
|
||||
<h1>{{ string .Blog.Lang "login" }}</h1>
|
||||
<form class="fw-form p" method="post">
|
||||
<form class="fw p" method="post">
|
||||
<input type="hidden" name="loginaction" value="login">
|
||||
<input type="hidden" name="loginmethod" value="{{ .Data.loginmethod }}">
|
||||
<input type="hidden" name="loginheaders" value="{{ .Data.loginheaders }}">
|
||||
|
@ -15,7 +15,7 @@
|
|||
{{ if .Data.totp }}
|
||||
<input type="text" name="token" inputmode="numeric" pattern="[0-9]*" autocomplete="one-time-code" placeholder="{{ string .Blog.Lang "totp" }}">
|
||||
{{ end }}
|
||||
<input class="fw" type="submit" value="{{ string .Blog.Lang "login" }}">
|
||||
<input type="submit" value="{{ string .Blog.Lang "login" }}">
|
||||
</form>
|
||||
{{ include "author" . }}
|
||||
</main>
|
||||
|
|
|
@ -9,9 +9,9 @@
|
|||
{{ if (or .Blog.Search.Title .Blog.Search.Description) }}
|
||||
<hr>
|
||||
{{ end }}
|
||||
<form class="fw-form p" method="post">
|
||||
<form class="fw p" method="post">
|
||||
<input type="text" name="q" {{ with .Blog.Search.Placeholder }}placeholder="{{ . }}"{{ end }}>
|
||||
<input class="fw" type="submit" value="🔍 {{ string .Blog.Lang "search" }}">
|
||||
<input type="submit" value="🔍 {{ string .Blog.Lang "search" }}">
|
||||
</form>
|
||||
</main>
|
||||
{{ end }}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue