Add Technical Kiwi website with Go, templ, and HTMX.

Single-page site with gallery by album and event, contact form over SMTP,
Docker dev/prod setup, and on-server image derivatives. Gallery photos stay
local (app/images/ is gitignored).

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-05-25 23:57:59 +12:00
parent c21be097b0
commit 509e7ccb43
33 changed files with 2635 additions and 1 deletions

163
app/internal/mail/send.go Normal file
View File

@@ -0,0 +1,163 @@
package mail
import (
"bytes"
"crypto/tls"
"fmt"
"net"
"net/smtp"
"strings"
)
// ContactEmail sends a contact form message via the configured SMTP relay.
func (c Config) ContactEmail(name, replyTo, message string) error {
subject := fmt.Sprintf("Contact form: %s", name)
var body bytes.Buffer
body.WriteString(fmt.Sprintf("Name: %s\n", name))
body.WriteString(fmt.Sprintf("Email: %s\n\n", replyTo))
body.WriteString(message)
return c.send(subject, body.String(), replyTo)
}
func (c Config) send(subject, body, replyTo string) error {
addr := fmt.Sprintf("%s:%d", c.Host, c.Port)
var msg bytes.Buffer
msg.WriteString(fmt.Sprintf("From: %s\r\n", c.From))
msg.WriteString(fmt.Sprintf("To: %s\r\n", c.To))
if replyTo != "" {
msg.WriteString(fmt.Sprintf("Reply-To: %s\r\n", replyTo))
}
msg.WriteString(fmt.Sprintf("Subject: %s\r\n", subject))
msg.WriteString("MIME-Version: 1.0\r\n")
msg.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
msg.WriteString("\r\n")
msg.WriteString(body)
raw := msg.Bytes()
from := extractAddr(c.From)
var auth smtp.Auth
if c.User != "" {
auth = smtp.PlainAuth("", c.User, c.Password, c.Host)
}
switch c.tlsMode() {
case "plain":
return smtp.SendMail(addr, auth, from, []string{c.To}, raw)
case "tls":
return c.sendTLS(addr, auth, from, raw)
default:
return c.sendStartTLS(addr, auth, from, raw)
}
}
func (c Config) tlsMode() string {
switch strings.ToLower(c.TLS) {
case "plain", "tls":
return strings.ToLower(c.TLS)
}
if c.Port == 465 {
return "tls"
}
if c.Port == 25 {
return "plain"
}
return "starttls"
}
func (c Config) sendStartTLS(addr string, auth smtp.Auth, from string, raw []byte) error {
conn, err := net.Dial("tcp", addr)
if err != nil {
return err
}
defer conn.Close()
client, err := smtp.NewClient(conn, c.Host)
if err != nil {
return err
}
defer client.Close()
if ok, _ := client.Extension("STARTTLS"); ok {
if err := client.StartTLS(&tls.Config{ServerName: c.Host}); err != nil {
return err
}
}
if auth != nil {
if ok, _ := client.Extension("AUTH"); ok {
if err := client.Auth(auth); err != nil {
return err
}
}
}
if err := client.Mail(from); err != nil {
return err
}
if err := client.Rcpt(c.To); err != nil {
return err
}
w, err := client.Data()
if err != nil {
return err
}
if _, err := w.Write(raw); err != nil {
return err
}
if err := w.Close(); err != nil {
return err
}
return client.Quit()
}
func (c Config) sendTLS(addr string, auth smtp.Auth, from string, raw []byte) error {
conn, err := tls.Dial("tcp", addr, &tls.Config{ServerName: c.Host})
if err != nil {
return err
}
defer conn.Close()
client, err := smtp.NewClient(conn, c.Host)
if err != nil {
return err
}
defer client.Close()
if auth != nil {
if ok, _ := client.Extension("AUTH"); ok {
if err := client.Auth(auth); err != nil {
return err
}
}
}
if err := client.Mail(from); err != nil {
return err
}
if err := client.Rcpt(c.To); err != nil {
return err
}
w, err := client.Data()
if err != nil {
return err
}
if _, err := w.Write(raw); err != nil {
return err
}
if err := w.Close(); err != nil {
return err
}
return client.Quit()
}
func extractAddr(from string) string {
if i := strings.Index(from, "<"); i >= 0 {
if j := strings.Index(from[i:], ">"); j > 0 {
return strings.TrimSpace(from[i+1 : i+j])
}
}
return strings.TrimSpace(from)
}