aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-03-07 15:09:42 +0530
committerBobby <[email protected]>2026-03-07 15:09:42 +0530
commit8f8d41413ff775b6d721059783a0e2de4f90f50c (patch)
tree40923d049ee7df8b0bc90d74373c94cdd4d8b230
parent41926c10ea2e8496ce4b528262f5047ccbe6f155 (diff)
downloaddove-8f8d41413ff775b6d721059783a0e2de4f90f50c.tar.xz
dove-8f8d41413ff775b6d721059783a0e2de4f90f50c.zip
feat: add configuration management and server setup
- Implemented configuration file creation and loading in config.go. - Added default configuration content embedded in embed.go. - Introduced logging middleware for HTTP requests. - Created Makefile for build and setup automation. - Integrated Tailwind CSS and HTMX for frontend styling and interactivity. - Developed basic authentication flow with login and dashboard pages. - Enhanced error handling and user feedback in templates. - Updated dependencies in go.mod and go.sum.
-rw-r--r--.gitignore14
-rw-r--r--Makefile56
-rw-r--r--config/config.go5
-rw-r--r--config/constants.go14
-rw-r--r--config/embed.go6
-rw-r--r--config/functions.go13
-rw-r--r--dove/main.go71
-rw-r--r--example.config.toml50
-rw-r--r--go.mod7
-rw-r--r--go.sum28
-rw-r--r--messages/config.go2
-rw-r--r--messages/logger.go2
-rw-r--r--messages/server.go9
-rw-r--r--middleware/constants.go5
-rw-r--r--middleware/globals.go12
-rw-r--r--middleware/logging.go71
-rw-r--r--middleware/middleware.go3
-rw-r--r--pages/dashboard.go2
-rw-r--r--pages/home.go4
-rw-r--r--scripts/htmx.setup.sh32
-rw-r--r--scripts/tailwind.setup.sh78
-rw-r--r--static/css/tailwind.css1
-rw-r--r--tailwind.config.js9
-rw-r--r--templates/auth/login.django45
-rw-r--r--templates/dashboard.django30
-rw-r--r--templates/error.django11
-rw-r--r--templates/layouts/base.django15
-rw-r--r--utils/auth/auth.go8
-rw-r--r--utils/logger/logger.go2
-rw-r--r--utils/meta/title.go7
30 files changed, 596 insertions, 16 deletions
diff --git a/.gitignore b/.gitignore
index f20fd5a..f9dd798 100644
--- a/.gitignore
+++ b/.gitignore
@@ -32,5 +32,17 @@ go.work.sum
.vscode/
.claude/
-# Binaries
+# Binaries
bin/
+
+# Toolchain
+toolchain/
+
+# Built assets
+static/css/style.css
+static/js/htmx.min.js
+
+# Embedded copies
+config/example.config.toml
+config.toml
+
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..1e64aa9
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,56 @@
+BINARY_NAME = dove
+BUILD_PATH = bin/$(BINARY_NAME)
+MAIN_PATH = $(BINARY_NAME)/main.go
+TAILWIND = toolchain/tailwind
+
+.PHONY: setup clean tidy embed build run dev css watch all
+
+setup:
+ @echo "Setting up environment..."
+ @go mod download
+ @go mod tidy
+ @./scripts/tailwind.setup.sh
+ @./scripts/htmx.setup.sh
+ @echo "Environment setup complete."
+
+clean:
+ @echo "Cleaning up..."
+ @rm -rf bin
+ @rm -rf tmp
+ @echo "Cleanup complete."
+
+tidy:
+ @echo "Tidying modules..."
+ @go mod tidy
+ @echo "Modules tidied."
+
+embed:
+ @cp example.config.toml config/example.config.toml
+
+build: css embed
+ @echo "Building..."
+ @go build -o $(BUILD_PATH) $(MAIN_PATH)
+ @echo "Build complete."
+
+run:
+ @if [ ! -f $(BUILD_PATH) ]; then echo "Binary not found. Building..."; $(MAKE) -s build; fi
+ @echo "Running..."
+ @$(BUILD_PATH)
+
+dev: css embed
+ @echo "Running in development mode..."
+ @go run $(MAIN_PATH)
+
+css:
+ @if [ ! -f $(TAILWIND) ]; then echo "Tailwind not found. Installing..."; ./scripts/tailwind.setup.sh; fi
+ @echo "Building CSS..."
+ @$(TAILWIND) -i static/css/tailwind.css -o static/css/style.css --minify
+
+watch:
+ @if [ ! -f $(TAILWIND) ]; then echo "Tailwind not found. Installing..."; ./scripts/tailwind.setup.sh; fi
+ @echo "Watching CSS..."
+ @$(TAILWIND) -i static/css/tailwind.css -o static/css/style.css --watch
+
+all: setup clean build run
+
+.SILENT:
diff --git a/config/config.go b/config/config.go
index ed9dab3..8a481c7 100644
--- a/config/config.go
+++ b/config/config.go
@@ -25,6 +25,11 @@ func init() {
DataDir = resolveDataDirectory(DevMode, osConfigDirectory)
configFilePath := resolveConfigFilePath(DevMode, osConfigDirectory)
+
+ if createError := ensureConfigFileExists(configFilePath); createError != nil {
+ logger.Fatalf(LOG_PREFIX, messages.ConfigCreateFailed, createError)
+ }
+
if loadError := loadConfigFile(configFilePath); loadError != nil {
logger.Fatalf(LOG_PREFIX, messages.ConfigFileLoadFailed, loadError)
}
diff --git a/config/constants.go b/config/constants.go
index 14b6276..dae1811 100644
--- a/config/constants.go
+++ b/config/constants.go
@@ -1,10 +1,12 @@
package config
const (
- APPLICATION_DIRECTORY = "dove"
- CONFIG_FILE_NAME = "config.toml"
- CURRENT_DIRECTORY = "."
- GO_BUILD_ALT_INDICATOR = "go_build"
- GO_BUILD_PATH_INDICATOR = "go-build"
- LOG_PREFIX = "Config"
+ APPLICATION_DIRECTORY = "dove"
+ CONFIG_DIRECTORY_PERMISSIONS = 0755
+ CONFIG_FILE_NAME = "config.toml"
+ CONFIG_FILE_PERMISSIONS = 0644
+ CURRENT_DIRECTORY = "."
+ GO_BUILD_ALT_INDICATOR = "go_build"
+ GO_BUILD_PATH_INDICATOR = "go-build"
+ LOG_PREFIX = "Config"
)
diff --git a/config/embed.go b/config/embed.go
new file mode 100644
index 0000000..696ab8e
--- /dev/null
+++ b/config/embed.go
@@ -0,0 +1,6 @@
+package config
+
+import _ "embed"
+
+//go:embed example.config.toml
+var defaultConfigContent []byte
diff --git a/config/functions.go b/config/functions.go
index 1a49226..c6ce601 100644
--- a/config/functions.go
+++ b/config/functions.go
@@ -45,6 +45,19 @@ func resolveDataDirectory(developmentMode bool, osConfigDirectory string) string
}
}
+func ensureConfigFileExists(configFilePath string) error {
+ if _, statError := os.Stat(configFilePath); statError == nil {
+ return nil
+ }
+
+ configDirectory := filepath.Dir(configFilePath)
+ if mkdirError := os.MkdirAll(configDirectory, CONFIG_DIRECTORY_PERMISSIONS); mkdirError != nil {
+ return mkdirError
+ }
+
+ return os.WriteFile(configFilePath, defaultConfigContent, CONFIG_FILE_PERMISSIONS)
+}
+
func loadConfigFile(configFilePath string) error {
if _, statError := os.Stat(configFilePath); statError != nil {
return nil
diff --git a/dove/main.go b/dove/main.go
new file mode 100644
index 0000000..207565e
--- /dev/null
+++ b/dove/main.go
@@ -0,0 +1,71 @@
+package main
+
+import (
+ "fmt"
+ "os"
+ "os/signal"
+ "syscall"
+
+ "dove/config"
+ "dove/messages"
+ "dove/middleware"
+ "dove/router"
+ "dove/utils/logger"
+
+ "github.com/gofiber/fiber/v2"
+ "github.com/gofiber/fiber/v2/middleware/recover"
+ "github.com/gofiber/template/django/v3"
+ "github.com/spf13/cobra"
+)
+
+const LOG_PREFIX = "Server"
+
+var rootCommand = &cobra.Command{
+ Use: "dove",
+ Short: "Local SMTP email testing tool.",
+ RunE: serve,
+}
+
+func main() {
+ if executeError := rootCommand.Execute(); executeError != nil {
+ logger.Fatalf(LOG_PREFIX, "%v", executeError)
+ }
+}
+
+func serve(command *cobra.Command, arguments []string) error {
+ engine := django.New("./templates", ".django")
+ engine.Reload(config.DevMode)
+
+ application := fiber.New(fiber.Config{
+ DisableStartupMessage: true,
+ Views: engine,
+ ErrorHandler: router.ErrorHandler,
+ })
+
+ application.Use(recover.New())
+
+ middleware.Initialize(application)
+ router.Initialize(application)
+
+ shutdownSignal := make(chan os.Signal, 1)
+ signal.Notify(shutdownSignal, syscall.SIGINT, syscall.SIGTERM)
+
+ go func() {
+ address := fmt.Sprintf("%s:%d", config.Server.Host, config.Server.Port)
+ logger.Successf(LOG_PREFIX, messages.ServerStarting, address)
+
+ if listenError := application.Listen(address); listenError != nil {
+ logger.Fatalf(LOG_PREFIX, messages.ServerListenFailed, listenError)
+ }
+ }()
+
+ <-shutdownSignal
+ logger.Infof(LOG_PREFIX, messages.ServerShuttingDown)
+
+ if shutdownError := application.Shutdown(); shutdownError != nil {
+ logger.Errorf(LOG_PREFIX, messages.ServerShutdownFailed, shutdownError)
+ }
+
+ logger.Successf(LOG_PREFIX, messages.ServerShutdownComplete)
+ return nil
+}
diff --git a/example.config.toml b/example.config.toml
new file mode 100644
index 0000000..8748a3b
--- /dev/null
+++ b/example.config.toml
@@ -0,0 +1,50 @@
+[server]
+host = "0.0.0.0"
+port = 8080
+debug = false
+# username = ""
+# password = ""
+
+[smtp]
+host = "0.0.0.0"
+port = 5025
+smtps_port = 5465
+starttls_port = 5587
+max_message_size = 26214400
+auth_required = false
+# username = ""
+# password = ""
+tls_enabled = false
+# tls_cert = ""
+# tls_key = ""
+relay_enabled = false
+# relay_host = ""
+relay_port = 587
+# relay_username = ""
+# relay_password = ""
+relay_starttls = true
+
+[imap]
+host = "0.0.0.0"
+port = 5143
+imaps_port = 5993
+auth_required = false
+# username = ""
+# password = ""
+tls_enabled = false
+# tls_cert = ""
+# tls_key = ""
+
+[pop3]
+host = "0.0.0.0"
+port = 5110
+pop3s_port = 5995
+auth_required = false
+# username = ""
+# password = ""
+tls_enabled = false
+# tls_cert = ""
+# tls_key = ""
+
+[mailbox]
+mode = "registered" \ No newline at end of file
diff --git a/go.mod b/go.mod
index d8956de..7b1ea17 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,9 @@ go 1.25.0
require (
github.com/gofiber/fiber/v2 v2.52.12
+ github.com/gofiber/template/django/v3 v3.1.14
github.com/pelletier/go-toml/v2 v2.2.4
+ github.com/spf13/cobra v1.10.2
go.uber.org/zap v1.27.1
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
@@ -12,7 +14,11 @@ require (
require (
github.com/andybalholm/brotli v1.1.0 // indirect
+ github.com/flosch/pongo2/v6 v6.0.0 // indirect
+ github.com/gofiber/template v1.8.3 // indirect
+ github.com/gofiber/utils v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
+ github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/klauspost/compress v1.17.9 // indirect
@@ -21,6 +27,7 @@ require (
github.com/mattn/go-runewidth v0.0.16 // indirect
github.com/mattn/go-sqlite3 v1.14.22 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
+ github.com/spf13/pflag v1.0.9 // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fasthttp v1.51.0 // indirect
github.com/valyala/tcplisten v1.0.0 // indirect
diff --git a/go.sum b/go.sum
index f00df89..9e0be7a 100644
--- a/go.sum
+++ b/go.sum
@@ -1,17 +1,32 @@
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/flosch/pongo2/v6 v6.0.0 h1:lsGru8IAzHgIAw6H2m4PCyleO58I40ow6apih0WprMU=
+github.com/flosch/pongo2/v6 v6.0.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU=
github.com/gofiber/fiber/v2 v2.52.12 h1:0LdToKclcPOj8PktUdIKo9BUohjjwfnQl42Dhw8/WUw=
github.com/gofiber/fiber/v2 v2.52.12/go.mod h1:YEcBbO/FB+5M1IZNBP9FO3J9281zgPAreiI1oqg8nDw=
+github.com/gofiber/template v1.8.3 h1:hzHdvMwMo/T2kouz2pPCA0zGiLCeMnoGsQZBTSYgZxc=
+github.com/gofiber/template v1.8.3/go.mod h1:bs/2n0pSNPOkRa5VJ8zTIvedcI/lEYxzV3+YPXdBvq8=
+github.com/gofiber/template/django/v3 v3.1.14 h1:SvTvs+u5vTZuu1Y2pMUD2NhaGIjBj9FmDA3XD50QBvw=
+github.com/gofiber/template/django/v3 v3.1.14/go.mod h1:gP4vH+T1ajZw7yaejqG1dZVdHQkMC/jPoQbmlG812I0=
+github.com/gofiber/utils v1.1.0 h1:vdEBpn7AzIUJRhe+CiTOJdUcTg4Q9RK+pEa0KPbLdrM=
+github.com/gofiber/utils v1.1.0/go.mod h1:poZpsnhBykfnY1Mc0KeEa6mSHrS3dV0+oBWyeQmb2e0=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
+github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
+github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI=
+github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -27,8 +42,13 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
-github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
-github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
+github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU=
+github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4=
+github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY=
+github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
+github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
@@ -41,12 +61,16 @@ go.uber.org/multierr v1.10.0 h1:S0h4aNzvfcFsC3dRF1jLoaov7oRaKqRGC/pUEJ2yvPQ=
go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.20.0 h1:gK/Kv2otX8gz+wn7Rmb3vT96ZwuoxnQlY+HlJVj7Qug=
golang.org/x/text v0.20.0/go.mod h1:D4IsuqiFMhST5bX19pQ9ikHC2GsaKyk/oF+pn3ducp4=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
diff --git a/messages/config.go b/messages/config.go
index 43c9d7f..10c85db 100644
--- a/messages/config.go
+++ b/messages/config.go
@@ -1,6 +1,8 @@
package messages
const (
+ ConfigCreated = "Created default config at %s."
+ ConfigCreateFailed = "Failed to create config file: %v"
ConfigFileLoadFailed = "Failed to load config: %v"
ConfigFileReadFailed = "Failed to read config file %s: %s."
ConfigLoaded = "Configuration loaded successfully."
diff --git a/messages/logger.go b/messages/logger.go
index 755e259..7f6e99b 100644
--- a/messages/logger.go
+++ b/messages/logger.go
@@ -1,5 +1,5 @@
package messages
const (
- LoggerNotInitialized = "logger.Init() was not called."
+ LoggerNotInitialized = "Logger was not initialized."
)
diff --git a/messages/server.go b/messages/server.go
new file mode 100644
index 0000000..9f2b086
--- /dev/null
+++ b/messages/server.go
@@ -0,0 +1,9 @@
+package messages
+
+const (
+ ServerListenFailed = "Failed to start server: %v"
+ ServerShutdownComplete = "Shutdown complete."
+ ServerShutdownFailed = "Error during server shutdown: %v"
+ ServerShuttingDown = "Shutting down gracefully..."
+ ServerStarting = "Server started on %s."
+)
diff --git a/middleware/constants.go b/middleware/constants.go
new file mode 100644
index 0000000..dc6505b
--- /dev/null
+++ b/middleware/constants.go
@@ -0,0 +1,5 @@
+package middleware
+
+const (
+ LOG_PREFIX = "HTTP"
+)
diff --git a/middleware/globals.go b/middleware/globals.go
new file mode 100644
index 0000000..8a186bb
--- /dev/null
+++ b/middleware/globals.go
@@ -0,0 +1,12 @@
+package middleware
+
+import (
+ "dove/config"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func globals(context *fiber.Ctx) error {
+ context.Locals("AuthEnabled", config.AuthEnabled)
+ return context.Next()
+}
diff --git a/middleware/logging.go b/middleware/logging.go
new file mode 100644
index 0000000..15c82b6
--- /dev/null
+++ b/middleware/logging.go
@@ -0,0 +1,71 @@
+package middleware
+
+import (
+ "fmt"
+ "strconv"
+ "strings"
+ "time"
+
+ "dove/utils/logger"
+
+ "github.com/gofiber/fiber/v2"
+)
+
+func httpLogger() fiber.Handler {
+ return func(context *fiber.Ctx) error {
+ startTime := time.Now()
+
+ responseError := context.Next()
+
+ duration := time.Since(startTime)
+ statusCode := context.Response().StatusCode()
+ method := context.Method()
+ path := context.Path()
+ ipAddress := context.IP()
+
+ paddedMethod := method
+ if len(method) < 7 {
+ paddedMethod = method + strings.Repeat(" ", 7-len(method))
+ }
+
+ message := fmt.Sprintf(
+ "%s %-3d %-15s %-10s %s",
+ paddedMethod, statusCode, "IP: "+ipAddress, "TTR: "+formatDuration(duration), "Path: "+path,
+ )
+
+ logByStatus(statusCode, LOG_PREFIX, message)
+
+ return responseError
+ }
+}
+
+func logByStatus(statusCode int, prefix string, message string) {
+ switch {
+ case statusCode >= fiber.StatusInternalServerError:
+ logger.Errorf(prefix, "%s", message)
+ case statusCode >= fiber.StatusBadRequest:
+ logger.Warnf(prefix, "%s", message)
+ case statusCode >= fiber.StatusMultipleChoices:
+ logger.Infof(prefix, "%s", message)
+ case statusCode >= fiber.StatusOK:
+ logger.Successf(prefix, "%s", message)
+ default:
+ logger.Infof(prefix, "%s", message)
+ }
+}
+
+func formatDuration(duration time.Duration) string {
+ if duration < time.Microsecond {
+ return strconv.FormatInt(duration.Nanoseconds(), 10) + "ns"
+ }
+
+ if duration < time.Millisecond {
+ return strconv.FormatInt(duration.Nanoseconds()/1_000, 10) + "µs"
+ }
+
+ if duration < time.Second {
+ return strconv.FormatFloat(float64(duration.Nanoseconds())/float64(time.Millisecond), 'f', 3, 64) + "ms"
+ }
+
+ return strconv.FormatFloat(float64(duration.Nanoseconds())/float64(time.Second), 'f', 3, 64) + "s"
+}
diff --git a/middleware/middleware.go b/middleware/middleware.go
index dffc109..88e0820 100644
--- a/middleware/middleware.go
+++ b/middleware/middleware.go
@@ -7,6 +7,9 @@ import (
)
func Initialize(application *fiber.App) {
+ application.Use(httpLogger)
+ application.Use(globals)
+
switch config.AuthEnabled {
case true:
application.Use(authentication)
diff --git a/pages/dashboard.go b/pages/dashboard.go
index 323cd1f..2fd1f1d 100644
--- a/pages/dashboard.go
+++ b/pages/dashboard.go
@@ -1,11 +1,13 @@
package pages
import (
+ "dove/utils/meta"
"dove/utils/shortcuts"
"github.com/gofiber/fiber/v2"
)
func Dashboard(context *fiber.Ctx) error {
+ meta.SetPageTitle(context, "Dashboard")
return shortcuts.Render(context, "dashboard", nil)
}
diff --git a/pages/home.go b/pages/home.go
index cbf5a5d..06f836e 100644
--- a/pages/home.go
+++ b/pages/home.go
@@ -2,6 +2,7 @@ package pages
import (
"dove/config"
+ "dove/utils/meta"
"dove/utils/shortcuts"
"github.com/gofiber/fiber/v2"
@@ -10,8 +11,9 @@ import (
func Home(context *fiber.Ctx) error {
switch config.AuthEnabled {
case true:
+ meta.SetPageTitle(context, "Login")
return shortcuts.Render(context, "auth/login", nil)
default:
- return shortcuts.Render(context, "dashboard", nil)
+ return shortcuts.Redirect(context, "dashboard.index")
}
}
diff --git a/scripts/htmx.setup.sh b/scripts/htmx.setup.sh
new file mode 100644
index 0000000..b8d6fe6
--- /dev/null
+++ b/scripts/htmx.setup.sh
@@ -0,0 +1,32 @@
+#!/usr/bin/env bash
+set -e
+
+STATIC_JS_DIR="static/js"
+HTMX_FILE="$STATIC_JS_DIR/htmx.min.js"
+
+echo "Fetching latest HTMX release..."
+LATEST_RELEASE=$(curl -s https://api.github.com/repos/bigskysoftware/htmx/releases/latest | grep '"tag_name":' | sed -E 's/.*"v([^"]+)".*/\1/')
+
+if [ -z "$LATEST_RELEASE" ]; then
+ echo "Failed to fetch latest release, using default version 2.0.8."
+ LATEST_RELEASE="2.0.8"
+fi
+
+echo "Latest version: $LATEST_RELEASE"
+DOWNLOAD_URL="https://unpkg.com/htmx.org@${LATEST_RELEASE}/dist/htmx.min.js"
+
+echo "Downloading from: $DOWNLOAD_URL"
+echo ""
+
+mkdir -p "$STATIC_JS_DIR"
+
+if curl -fSL "$DOWNLOAD_URL" -o "$HTMX_FILE"; then
+ echo ""
+ echo "HTMX installed successfully!"
+ echo "Location: $HTMX_FILE"
+ echo "Version: $LATEST_RELEASE"
+else
+ echo ""
+ echo "Failed to download HTMX."
+ exit 1
+fi
diff --git a/scripts/tailwind.setup.sh b/scripts/tailwind.setup.sh
new file mode 100644
index 0000000..c77f615
--- /dev/null
+++ b/scripts/tailwind.setup.sh
@@ -0,0 +1,78 @@
+#!/usr/bin/env bash
+set -e
+
+TOOLCHAIN_DIR="toolchain"
+TAILWIND_BIN="$TOOLCHAIN_DIR/tailwind"
+
+echo "Detecting platform..."
+
+OS=$(uname -s | tr '[:upper:]' '[:lower:]')
+ARCH=$(uname -m)
+
+case "$OS" in
+ linux*)
+ PLATFORM="linux"
+ ;;
+ darwin*)
+ PLATFORM="macos"
+ ;;
+ msys*|mingw*|cygwin*)
+ PLATFORM="windows"
+ ;;
+ *)
+ echo "Unsupported OS: $OS"
+ exit 1
+ ;;
+esac
+
+case "$ARCH" in
+ x86_64|amd64)
+ ARCHITECTURE="x64"
+ ;;
+ arm64|aarch64)
+ ARCHITECTURE="arm64"
+ ;;
+ armv7l)
+ ARCHITECTURE="armv7"
+ ;;
+ *)
+ echo "Unsupported architecture: $ARCH"
+ exit 1
+ ;;
+esac
+
+BINARY_NAME="tailwindcss-${PLATFORM}-${ARCHITECTURE}"
+
+echo "Platform: $PLATFORM"
+echo "Architecture: $ARCHITECTURE"
+echo "Binary: $BINARY_NAME"
+echo ""
+
+echo "Fetching latest Tailwind CSS release..."
+LATEST_RELEASE=$(curl -s https://api.github.com/repos/tailwindlabs/tailwindcss/releases/latest | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
+
+if [ -z "$LATEST_RELEASE" ]; then
+ echo "Failed to fetch latest release."
+ exit 1
+fi
+
+echo "Latest version: $LATEST_RELEASE"
+DOWNLOAD_URL="https://github.com/tailwindlabs/tailwindcss/releases/download/${LATEST_RELEASE}/${BINARY_NAME}"
+
+echo "Downloading from: $DOWNLOAD_URL"
+echo ""
+
+mkdir -p "$TOOLCHAIN_DIR"
+
+if curl -fSL "$DOWNLOAD_URL" -o "$TAILWIND_BIN"; then
+ chmod +x "$TAILWIND_BIN"
+ echo ""
+ echo "Tailwind CSS installed successfully!"
+ echo "Location: $TAILWIND_BIN"
+ echo "Version: $LATEST_RELEASE"
+else
+ echo ""
+ echo "Failed to download Tailwind CSS binary."
+ echo "URL: $DOWNLOAD_URL"
+ exit 1
+fi
diff --git a/static/css/tailwind.css b/static/css/tailwind.css
new file mode 100644
index 0000000..f1d8c73
--- /dev/null
+++ b/static/css/tailwind.css
@@ -0,0 +1 @@
+@import "tailwindcss";
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..967b8e1
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,9 @@
+export default {
+ content: [
+ "./templates/**/*.django",
+ "./static/js/**/*.js",
+ ],
+ theme: {
+ extend: {},
+ },
+};
diff --git a/templates/auth/login.django b/templates/auth/login.django
new file mode 100644
index 0000000..3981abd
--- /dev/null
+++ b/templates/auth/login.django
@@ -0,0 +1,45 @@
+{% extends "layouts/base.django" %}
+
+{% block content %}
+<div class="min-h-screen flex items-center justify-center bg-gray-950">
+ <div class="w-full max-w-sm space-y-8">
+ <div class="text-center">
+ <h1 class="text-4xl font-bold text-white tracking-tight">Dove</h1>
+ <p class="mt-2 text-sm text-gray-400">Local SMTP server for peaceful email testing</p>
+ </div>
+
+ {% if ErrorMessage %}
+ <div class="rounded-lg bg-red-500/10 border border-red-500/20 px-4 py-3 text-sm text-red-400">
+ {{ ErrorMessage }}
+ </div>
+ {% endif %}
+
+ <form method="POST" action="/auth/login" class="space-y-5">
+ <div>
+ <input
+ type="text"
+ name="username"
+ placeholder="Username"
+ required
+ class="w-full rounded-lg border border-gray-700 bg-gray-900 px-4 py-3 text-sm text-white placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 transition"
+ >
+ </div>
+ <div>
+ <input
+ type="password"
+ name="password"
+ placeholder="Password"
+ required
+ class="w-full rounded-lg border border-gray-700 bg-gray-900 px-4 py-3 text-sm text-white placeholder-gray-500 focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 transition"
+ >
+ </div>
+ <button
+ type="submit"
+ class="w-full rounded-lg bg-blue-600 px-4 py-3 text-sm font-medium text-white hover:bg-blue-500 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 focus:ring-offset-gray-950 transition"
+ >
+ Sign in
+ </button>
+ </form>
+ </div>
+</div>
+{% endblock %}
diff --git a/templates/dashboard.django b/templates/dashboard.django
new file mode 100644
index 0000000..35c1246
--- /dev/null
+++ b/templates/dashboard.django
@@ -0,0 +1,30 @@
+{% extends "layouts/base.django" %}
+
+{% block content %}
+<div class="min-h-screen bg-gray-950">
+ <nav class="border-b border-gray-800 bg-gray-900/50 backdrop-blur-sm">
+ <div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
+ <div class="flex h-14 items-center justify-between">
+ <div class="flex items-center space-x-3">
+ <span class="text-lg font-semibold text-white">Dove</span>
+ </div>
+ {% if AuthEnabled %}
+ <div class="flex items-center space-x-4">
+ <a href="/auth/logout" class="text-sm text-gray-400 hover:text-white transition">Logout</a>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+ </nav>
+
+ <main class="mx-auto max-w-7xl px-4 py-8 sm:px-6 lg:px-8">
+ <div class="flex items-center justify-between">
+ <h1 class="text-2xl font-bold text-white">Mailboxes</h1>
+ </div>
+
+ <div class="mt-6 rounded-lg border border-gray-800 bg-gray-900/50 p-12 text-center">
+ <p class="text-gray-400">No mailboxes yet. Emails will appear here when received.</p>
+ </div>
+ </main>
+</div>
+{% endblock %}
diff --git a/templates/error.django b/templates/error.django
new file mode 100644
index 0000000..8c1521d
--- /dev/null
+++ b/templates/error.django
@@ -0,0 +1,11 @@
+{% extends "layouts/base.django" %}
+
+{% block content %}
+<div class="min-h-screen flex items-center justify-center bg-gray-950">
+ <div class="text-center space-y-4">
+ <h1 class="text-6xl font-bold text-gray-600">Error</h1>
+ <p class="text-gray-400">{{ ErrorMessage }}</p>
+ <a href="/" class="inline-block mt-4 text-sm text-blue-400 hover:text-blue-300 transition">Go back home</a>
+ </div>
+</div>
+{% endblock %}
diff --git a/templates/layouts/base.django b/templates/layouts/base.django
new file mode 100644
index 0000000..567b81d
--- /dev/null
+++ b/templates/layouts/base.django
@@ -0,0 +1,15 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>{% if PageTitle %}{{ PageTitle }} - {% endif %}Dove</title>
+ <link rel="stylesheet" href="/static/css/style.css">
+ {% block head %}{% endblock %}
+</head>
+<body class="antialiased">
+ {% block content %}{% endblock %}
+ <script src="/static/js/htmx.min.js"></script>
+ {% block scripts %}{% endblock %}
+</body>
+</html>
diff --git a/utils/auth/auth.go b/utils/auth/auth.go
index 5abb496..b3d322c 100644
--- a/utils/auth/auth.go
+++ b/utils/auth/auth.go
@@ -1,6 +1,7 @@
package auth
import (
+ "dove/config"
"dove/session"
"github.com/gofiber/fiber/v2"
@@ -17,12 +18,11 @@ func IsAuthenticated(context *fiber.Ctx) bool {
func RequireAuthentication(handler fiber.Handler) fiber.Handler {
return func(context *fiber.Ctx) error {
- switch IsAuthenticated(context) {
- case true:
+ if !config.AuthEnabled || IsAuthenticated(context) {
return handler(context)
- default:
- return fiber.ErrUnauthorized
}
+
+ return fiber.ErrUnauthorized
}
}
diff --git a/utils/logger/logger.go b/utils/logger/logger.go
index b1d1809..a08b68b 100644
--- a/utils/logger/logger.go
+++ b/utils/logger/logger.go
@@ -15,7 +15,7 @@ var (
atomicLevel zap.AtomicLevel
)
-func Init() {
+func init() {
atomicLevel = zap.NewAtomicLevelAt(zapcore.InfoLevel)
encoderConfig := zapcore.EncoderConfig{
diff --git a/utils/meta/title.go b/utils/meta/title.go
new file mode 100644
index 0000000..f6514e7
--- /dev/null
+++ b/utils/meta/title.go
@@ -0,0 +1,7 @@
+package meta
+
+import "github.com/gofiber/fiber/v2"
+
+func SetPageTitle(context *fiber.Ctx, title string) {
+ context.Locals("PageTitle", title)
+}