aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorBobby <[email protected]>2026-03-07 18:42:40 +0530
committerBobby <[email protected]>2026-03-07 18:42:40 +0530
commit96c136f046d78c51210927e61483a36a220fedcb (patch)
tree8f5baf294d92ec3bc503bd02f4a08871874f048d
parent6bc2ef7a8972547f10a5a8f50269b0e6b487a580 (diff)
downloaddove-96c136f046d78c51210927e61483a36a220fedcb.tar.xz
dove-96c136f046d78c51210927e61483a36a220fedcb.zip
Refactor dashboard and mailboxes pages to integrate services for data retrieval
- Updated `Dashboard` function to use `services.Overview()` for rendering overview data. - Enhanced `Mailboxes` function to include pagination, sorting, and search functionality using `services.ListMailboxes()`. - Modified `Users` function to implement pagination, sorting, and search with `services.ListUsers()`. Revamped templates for mailboxes and users - Updated `mailboxes.htmx.django` to display mailbox items dynamically with total count. - Enhanced `users.htmx.django` to show user details and total count, with improved layout for user information. Introduced new response types and constants - Added `PaginatedResponse` type in `types/response.go` for consistent pagination responses. - Introduced constants for pagination in `utils/meta/constants.go`. Implemented email processing and storage services - Created `services/email.go` for processing incoming emails and storing them in the database. - Added email parsing utilities in `utils/email` for handling email content and attachments. Established repository functions for mailboxes, users, and emails - Created repository functions in `repositories` for managing mailboxes, users, and emails, including listing and searching capabilities. Refactored SMTP server functions - Updated SMTP server handling in `utils/smtp` to streamline session management and message processing. - Removed obsolete storage functions and integrated email processing directly into the SMTP session. Added new message constants for better logging and error handling - Introduced message constants in `messages/email.go` and `messages/mailbox.go` for improved clarity in logs.
-rw-r--r--.gitignore2
-rw-r--r--go.mod10
-rw-r--r--go.sum19
-rw-r--r--messages/email.go10
-rw-r--r--messages/mailbox.go6
-rw-r--r--pages/dashboard.go3
-rw-r--r--pages/mailboxes.go8
-rw-r--r--pages/users.go8
-rw-r--r--repositories/alias.go18
-rw-r--r--repositories/constants.go5
-rw-r--r--repositories/email.go50
-rw-r--r--repositories/mailbox.go46
-rw-r--r--repositories/user.go46
-rw-r--r--services/constants.go5
-rw-r--r--services/email.go31
-rw-r--r--services/functions.go137
-rw-r--r--services/mailbox.go26
-rw-r--r--services/overview.go16
-rw-r--r--services/user.go12
-rw-r--r--templates/dashboard/htmx/mailboxes.htmx.django25
-rw-r--r--templates/dashboard/htmx/overview.htmx.django6
-rw-r--r--templates/dashboard/htmx/users.htmx.django34
-rw-r--r--types/overview.go7
-rw-r--r--types/response.go8
-rw-r--r--utils/email/constants.go7
-rw-r--r--utils/email/functions.go79
-rw-r--r--utils/email/parse.go29
-rw-r--r--utils/email/types.go24
-rw-r--r--utils/meta/constants.go7
-rw-r--r--utils/meta/pagination.go84
-rw-r--r--utils/meta/types.go10
-rw-r--r--utils/smtp/functions.go34
-rw-r--r--utils/smtp/server.go27
-rw-r--r--utils/smtp/session.go15
-rw-r--r--utils/smtp/storage.go22
-rw-r--r--utils/smtp/types.go4
36 files changed, 799 insertions, 81 deletions
diff --git a/.gitignore b/.gitignore
index f9dd798..5b5c6a2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -46,3 +46,5 @@ static/js/htmx.min.js
config/example.config.toml
config.toml
+# Database files
+*.db
diff --git a/go.mod b/go.mod
index a14ec58..7edcbae 100644
--- a/go.mod
+++ b/go.mod
@@ -16,11 +16,15 @@ require (
require (
github.com/andybalholm/brotli v1.1.0 // indirect
+ github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a // indirect
github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
github.com/gofiber/template v1.8.3 // indirect
github.com/gofiber/utils v1.1.0 // indirect
+ github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
+ github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 // indirect
+ github.com/jhillyerd/enmime v1.3.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
@@ -28,12 +32,16 @@ require (
github.com/mattn/go-isatty v0.0.20 // indirect
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/olekukonko/tablewriter v0.0.5 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/rivo/uniseg v0.4.4 // indirect
github.com/spf13/pflag v1.0.9 // indirect
+ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // 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
go.uber.org/multierr v1.10.0 // indirect
+ golang.org/x/net v0.23.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.20.0 // indirect
)
diff --git a/go.sum b/go.sum
index b586b87..09318cd 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
+github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a h1:MISbI8sU/PSK/ztvmWKFcI7UGb5/HQT7B+i3a2myKgI=
+github.com/cention-sany/utf7 v0.0.0-20170124080048-26cad61bd60a/go.mod h1:2GxOXOlEPAMFPfp014mK1SWq8G8BN8o7/dfYqJrVGn8=
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=
@@ -17,10 +19,16 @@ github.com/gofiber/template/django/v3 v3.1.14 h1:SvTvs+u5vTZuu1Y2pMUD2NhaGIjBj9F
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/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f h1:3BSP1Tbs2djlpprl7wCLuiqMaUh5SJkkzI2gDs+FgLs=
+github.com/gogs/chardet v0.0.0-20211120154057-b7413eaefb8f/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
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/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 h1:iCHtR9CQyktQ5+f3dMVZfwD2KWJUgm7M0gdL9NGr8KA=
+github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
+github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99rw=
+github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs=
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=
@@ -36,21 +44,30 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
github.com/mattn/go-sqlite3 v1.14.22 h1:2gZY6PC6kBnID23Tichd1K+Z0oS6nE/XwU+Vz/5o4kU=
github.com/mattn/go-sqlite3 v1.14.22/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
+github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec=
+github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
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/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
+github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
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/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cmavspvIl9nulOYwdy6IFRRo=
+github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
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=
@@ -66,6 +83,8 @@ go.uber.org/multierr v1.10.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
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/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
+golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
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=
diff --git a/messages/email.go b/messages/email.go
new file mode 100644
index 0000000..28d6795
--- /dev/null
+++ b/messages/email.go
@@ -0,0 +1,10 @@
+package messages
+
+const (
+ EmailDirectoryCreateFailed = "Failed to create email directory: %v"
+ EmailFileSaveFailed = "Failed to save email file: %v"
+ EmailIndexFailed = "Failed to index email: %v"
+ EmailParseFailed = "Failed to parse email: %v"
+ EmailProcessed = "Email processed for %d recipient(s)."
+ EmailStoreFailed = "Failed to store email: %v"
+)
diff --git a/messages/mailbox.go b/messages/mailbox.go
new file mode 100644
index 0000000..e193bf7
--- /dev/null
+++ b/messages/mailbox.go
@@ -0,0 +1,6 @@
+package messages
+
+const (
+ MailboxAutoCreated = "Auto-created mailbox for %s."
+ MailboxNotRegistered = "No registered mailbox or alias for address: %s"
+)
diff --git a/pages/dashboard.go b/pages/dashboard.go
index 0640192..3fb16fb 100644
--- a/pages/dashboard.go
+++ b/pages/dashboard.go
@@ -1,6 +1,7 @@
package pages
import (
+ "dove/services"
"dove/utils/meta"
"dove/utils/shortcuts"
@@ -9,5 +10,5 @@ import (
func Dashboard(context *fiber.Ctx) error {
meta.SetPageTitle(context, "Overview")
- return shortcuts.Render(context, "dashboard/overview", nil)
+ return shortcuts.Render(context, "dashboard/overview", services.Overview())
}
diff --git a/pages/mailboxes.go b/pages/mailboxes.go
index c70574e..317d9c9 100644
--- a/pages/mailboxes.go
+++ b/pages/mailboxes.go
@@ -1,6 +1,7 @@
package pages
import (
+ "dove/services"
"dove/utils/meta"
"dove/utils/shortcuts"
@@ -9,5 +10,10 @@ import (
func Mailboxes(context *fiber.Ctx) error {
meta.SetPageTitle(context, "Mailboxes")
- return shortcuts.Render(context, "dashboard/mailboxes", nil)
+
+ pagination := meta.Paginate(context)
+ sorting := meta.Sort(context, []string{"address", "created_at"}, "created_at")
+ search := context.Query("search")
+
+ return shortcuts.Render(context, "dashboard/mailboxes", services.ListMailboxes(pagination, sorting, search))
}
diff --git a/pages/users.go b/pages/users.go
index ed25cd0..723b544 100644
--- a/pages/users.go
+++ b/pages/users.go
@@ -1,6 +1,7 @@
package pages
import (
+ "dove/services"
"dove/utils/meta"
"dove/utils/shortcuts"
@@ -9,5 +10,10 @@ import (
func Users(context *fiber.Ctx) error {
meta.SetPageTitle(context, "Users")
- return shortcuts.Render(context, "dashboard/users", nil)
+
+ pagination := meta.Paginate(context)
+ sorting := meta.Sort(context, []string{"username", "display_name", "created_at"}, "created_at")
+ search := context.Query("search")
+
+ return shortcuts.Render(context, "dashboard/users", services.ListUsers(pagination, sorting, search))
}
diff --git a/repositories/alias.go b/repositories/alias.go
new file mode 100644
index 0000000..ee038b2
--- /dev/null
+++ b/repositories/alias.go
@@ -0,0 +1,18 @@
+package repositories
+
+import (
+ "dove/database"
+ "dove/models"
+
+ "gorm.io/gorm"
+)
+
+func FindAliasByAddress(address string) *models.Alias {
+ var alias models.Alias
+ result := database.DB.Preload("Mailbox").Where("source_address = ?", address).First(&alias)
+ if result.Error == gorm.ErrRecordNotFound {
+ return nil
+ }
+
+ return &alias
+}
diff --git a/repositories/constants.go b/repositories/constants.go
new file mode 100644
index 0000000..29ce495
--- /dev/null
+++ b/repositories/constants.go
@@ -0,0 +1,5 @@
+package repositories
+
+const (
+ LOG_PREFIX = "Repositories"
+)
diff --git a/repositories/email.go b/repositories/email.go
new file mode 100644
index 0000000..26ce620
--- /dev/null
+++ b/repositories/email.go
@@ -0,0 +1,50 @@
+package repositories
+
+import (
+ "dove/database"
+ "dove/models"
+ "dove/utils/meta"
+)
+
+func CreateEmail(email *models.Email) error {
+ return database.DB.Create(email).Error
+}
+
+func CreateAttachment(attachment *models.Attachment) error {
+ return database.DB.Create(attachment).Error
+}
+
+func ListEmails(pagination meta.Pagination, sorting meta.Sorting, search string) ([]models.Email, int64) {
+ var emails []models.Email
+ var total int64
+
+ query := database.DB.Model(&models.Email{})
+
+ if search != "" {
+ like := "%" + search + "%"
+ query = query.Where("from_address LIKE ? OR to_addresses LIKE ? OR subject LIKE ?", like, like, like)
+ }
+
+ query.Count(&total)
+ pagination.Apply(sorting.Apply(query)).Preload("Tags").Find(&emails)
+
+ return emails, total
+}
+
+func CountEmails() int64 {
+ var count int64
+ database.DB.Model(&models.Email{}).Count(&count)
+ return count
+}
+
+func ListEmailsByMailbox(mailboxID uint, pagination meta.Pagination, sorting meta.Sorting) ([]models.Email, int64) {
+ var emails []models.Email
+ var total int64
+
+ query := database.DB.Model(&models.Email{}).Where("mailbox_id = ?", mailboxID)
+
+ query.Count(&total)
+ pagination.Apply(sorting.Apply(query)).Preload("Tags").Find(&emails)
+
+ return emails, total
+}
diff --git a/repositories/mailbox.go b/repositories/mailbox.go
new file mode 100644
index 0000000..56fb7b1
--- /dev/null
+++ b/repositories/mailbox.go
@@ -0,0 +1,46 @@
+package repositories
+
+import (
+ "dove/database"
+ "dove/models"
+ "dove/utils/meta"
+
+ "gorm.io/gorm"
+)
+
+func FindMailboxByAddress(address string) *models.Mailbox {
+ var mailbox models.Mailbox
+ result := database.DB.Where("address = ?", address).First(&mailbox)
+ if result.Error == gorm.ErrRecordNotFound {
+ return nil
+ }
+
+ return &mailbox
+}
+
+func CreateMailbox(mailbox *models.Mailbox) error {
+ return database.DB.Create(mailbox).Error
+}
+
+func ListMailboxes(pagination meta.Pagination, sorting meta.Sorting, search string) ([]models.Mailbox, int64) {
+ var mailboxes []models.Mailbox
+ var total int64
+
+ query := database.DB.Model(&models.Mailbox{})
+
+ if search != "" {
+ like := "%" + search + "%"
+ query = query.Where("address LIKE ?", like)
+ }
+
+ query.Count(&total)
+ pagination.Apply(sorting.Apply(query)).Preload("User").Find(&mailboxes)
+
+ return mailboxes, total
+}
+
+func CountMailboxes() int64 {
+ var count int64
+ database.DB.Model(&models.Mailbox{}).Count(&count)
+ return count
+}
diff --git a/repositories/user.go b/repositories/user.go
new file mode 100644
index 0000000..0147f55
--- /dev/null
+++ b/repositories/user.go
@@ -0,0 +1,46 @@
+package repositories
+
+import (
+ "dove/database"
+ "dove/models"
+ "dove/utils/meta"
+
+ "gorm.io/gorm"
+)
+
+func FindUserByUsername(username string) *models.User {
+ var user models.User
+ result := database.DB.Where("username = ?", username).First(&user)
+ if result.Error == gorm.ErrRecordNotFound {
+ return nil
+ }
+
+ return &user
+}
+
+func CreateUser(user *models.User) error {
+ return database.DB.Create(user).Error
+}
+
+func ListUsers(pagination meta.Pagination, sorting meta.Sorting, search string) ([]models.User, int64) {
+ var users []models.User
+ var total int64
+
+ query := database.DB.Model(&models.User{})
+
+ if search != "" {
+ like := "%" + search + "%"
+ query = query.Where("username LIKE ? OR display_name LIKE ?", like, like)
+ }
+
+ query.Count(&total)
+ pagination.Apply(sorting.Apply(query)).Preload("Mailboxes").Find(&users)
+
+ return users, total
+}
+
+func CountUsers() int64 {
+ var count int64
+ database.DB.Model(&models.User{}).Count(&count)
+ return count
+}
diff --git a/services/constants.go b/services/constants.go
new file mode 100644
index 0000000..bba619b
--- /dev/null
+++ b/services/constants.go
@@ -0,0 +1,5 @@
+package services
+
+const (
+ LOG_PREFIX = "Services"
+)
diff --git a/services/email.go b/services/email.go
new file mode 100644
index 0000000..36ae5f6
--- /dev/null
+++ b/services/email.go
@@ -0,0 +1,31 @@
+package services
+
+import (
+ "dove/messages"
+ "dove/utils/email"
+ "dove/utils/logger"
+)
+
+func ProcessEmail(rawMessage []byte, recipientAddresses []string) error {
+ parsedEmail, parseError := email.Parse(rawMessage)
+ if parseError != nil {
+ logger.Errorf(LOG_PREFIX, messages.EmailParseFailed, parseError)
+ return parseError
+ }
+
+ mailboxes := ResolveMailboxes(recipientAddresses)
+ if len(mailboxes) == 0 {
+ return nil
+ }
+
+ for _, mailbox := range mailboxes {
+ if storeError := storeEmailForMailbox(rawMessage, parsedEmail, mailbox); storeError != nil {
+ logger.Errorf(LOG_PREFIX, messages.EmailStoreFailed, storeError)
+ return storeError
+ }
+ }
+
+ logger.Infof(LOG_PREFIX, messages.EmailProcessed, len(mailboxes))
+
+ return nil
+}
diff --git a/services/functions.go b/services/functions.go
new file mode 100644
index 0000000..ffba7f4
--- /dev/null
+++ b/services/functions.go
@@ -0,0 +1,137 @@
+package services
+
+import (
+ "dove/config"
+ "dove/enums"
+ "dove/messages"
+ "dove/models"
+ "dove/repositories"
+ "dove/utils/email"
+ "dove/utils/logger"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+)
+
+func resolveMailbox(address string) *models.Mailbox {
+ mailbox := repositories.FindMailboxByAddress(address)
+ if mailbox != nil {
+ return mailbox
+ }
+
+ alias := repositories.FindAliasByAddress(address)
+ if alias != nil {
+ return &alias.Mailbox
+ }
+
+ switch config.Mailbox.Mode {
+ case enums.Catchall:
+ return createMailboxForAddress(address)
+ default:
+ logger.Warnf(LOG_PREFIX, messages.MailboxNotRegistered, address)
+ return nil
+ }
+}
+
+func createMailboxForAddress(address string) *models.Mailbox {
+ user := repositories.FindUserByUsername(address)
+ if user == nil {
+ user = &models.User{
+ Username: address,
+ DisplayName: address,
+ }
+ repositories.CreateUser(user)
+ }
+
+ mailbox := &models.Mailbox{
+ Address: address,
+ UserID: user.ID,
+ }
+
+ repositories.CreateMailbox(mailbox)
+ logger.Infof(LOG_PREFIX, messages.MailboxAutoCreated, address)
+
+ return mailbox
+}
+
+func storeEmailForMailbox(rawMessage []byte, parsedEmail *email.ParsedEmail, mailbox models.Mailbox) error {
+ filename, saveError := saveEmailFile(rawMessage, mailbox.ID)
+ if saveError != nil {
+ return saveError
+ }
+
+ attachmentCount, inlineCount := countAttachments(parsedEmail.Attachments)
+
+ emailRecord := &models.Email{
+ MailboxID: mailbox.ID,
+ MessageID: parsedEmail.MessageID,
+ Filename: filename,
+ FromAddress: parsedEmail.FromAddress,
+ FromName: parsedEmail.FromName,
+ ToAddresses: strings.Join(parsedEmail.ToAddresses, ", "),
+ CcAddresses: strings.Join(parsedEmail.CcAddresses, ", "),
+ BccAddresses: strings.Join(parsedEmail.BccAddresses, ", "),
+ ReplyToAddress: parsedEmail.ReplyToAddress,
+ ReturnPath: parsedEmail.ReturnPath,
+ Subject: parsedEmail.Subject,
+ Snippet: parsedEmail.Snippet,
+ Size: parsedEmail.Size,
+ AttachmentCount: attachmentCount,
+ InlineCount: inlineCount,
+ }
+
+ if indexError := repositories.CreateEmail(emailRecord); indexError != nil {
+ logger.Errorf(LOG_PREFIX, messages.EmailIndexFailed, indexError)
+ return indexError
+ }
+
+ for _, parsedAttachment := range parsedEmail.Attachments {
+ attachmentRecord := &models.Attachment{
+ EmailID: emailRecord.ID,
+ Filename: parsedAttachment.Filename,
+ ContentType: parsedAttachment.ContentType,
+ ContentID: parsedAttachment.ContentID,
+ Size: parsedAttachment.Size,
+ IsInline: parsedAttachment.IsInline,
+ }
+
+ repositories.CreateAttachment(attachmentRecord)
+ }
+
+ return nil
+}
+
+func saveEmailFile(rawMessage []byte, mailboxID uint) (string, error) {
+ mailboxDirectory := filepath.Join(config.DataDir, "emails", fmt.Sprintf("%d", mailboxID))
+
+ if directoryError := os.MkdirAll(mailboxDirectory, 0750); directoryError != nil {
+ return "", directoryError
+ }
+
+ filename := fmt.Sprintf("%d.eml", time.Now().UnixNano())
+ filePath := filepath.Join(mailboxDirectory, filename)
+
+ if writeError := os.WriteFile(filePath, rawMessage, 0640); writeError != nil {
+ return "", writeError
+ }
+
+ return filename, nil
+}
+
+func countAttachments(attachments []email.ParsedAttachment) (int, int) {
+ attachmentCount := 0
+ inlineCount := 0
+
+ for _, attachment := range attachments {
+ switch attachment.IsInline {
+ case true:
+ inlineCount++
+ default:
+ attachmentCount++
+ }
+ }
+
+ return attachmentCount, inlineCount
+}
diff --git a/services/mailbox.go b/services/mailbox.go
new file mode 100644
index 0000000..b84e19b
--- /dev/null
+++ b/services/mailbox.go
@@ -0,0 +1,26 @@
+package services
+
+import (
+ "dove/models"
+ "dove/repositories"
+ "dove/types"
+ "dove/utils/meta"
+)
+
+func ListMailboxes(pagination meta.Pagination, sorting meta.Sorting, search string) types.PaginatedResponse {
+ mailboxes, total := repositories.ListMailboxes(pagination, sorting, search)
+ return pagination.Response(mailboxes, total)
+}
+
+func ResolveMailboxes(recipientAddresses []string) []models.Mailbox {
+ var resolvedMailboxes []models.Mailbox
+
+ for _, address := range recipientAddresses {
+ mailbox := resolveMailbox(address)
+ if mailbox != nil {
+ resolvedMailboxes = append(resolvedMailboxes, *mailbox)
+ }
+ }
+
+ return resolvedMailboxes
+}
diff --git a/services/overview.go b/services/overview.go
new file mode 100644
index 0000000..d0d3f60
--- /dev/null
+++ b/services/overview.go
@@ -0,0 +1,16 @@
+package services
+
+import (
+ "dove/config"
+ "dove/repositories"
+ "dove/types"
+ "fmt"
+)
+
+func Overview() types.Overview {
+ return types.Overview{
+ MailboxCount: repositories.CountMailboxes(),
+ EmailCount: repositories.CountEmails(),
+ SMTPAddress: fmt.Sprintf(":%d", config.SMTP.Port),
+ }
+}
diff --git a/services/user.go b/services/user.go
new file mode 100644
index 0000000..392aaa1
--- /dev/null
+++ b/services/user.go
@@ -0,0 +1,12 @@
+package services
+
+import (
+ "dove/repositories"
+ "dove/types"
+ "dove/utils/meta"
+)
+
+func ListUsers(pagination meta.Pagination, sorting meta.Sorting, search string) types.PaginatedResponse {
+ users, total := repositories.ListUsers(pagination, sorting, search)
+ return pagination.Response(users, total)
+}
diff --git a/templates/dashboard/htmx/mailboxes.htmx.django b/templates/dashboard/htmx/mailboxes.htmx.django
index 57fdf04..9ccedb1 100644
--- a/templates/dashboard/htmx/mailboxes.htmx.django
+++ b/templates/dashboard/htmx/mailboxes.htmx.django
@@ -3,8 +3,28 @@
<div class="glass rounded-xl glow-border">
<div class="flex items-center justify-between px-5 py-4 border-b border-white/[0.04]">
<h2 class="text-sm font-medium text-zinc-200">All Mailboxes</h2>
- <span class="text-xs text-zinc-600">Mailboxes are created automatically when emails are received</span>
+ <span class="text-xs text-zinc-600">{{ total }} total</span>
</div>
+ {% if items %}
+ <div class="divide-y divide-white/[0.04]">
+ {% for mailbox in items %}
+ {% url "dashboard.mailbox" address=mailbox.Address as mailbox_path %}
+ <a href="{{ mailbox_path }}" hx-get="{{ mailbox_path }}" hx-target="#content" hx-swap="innerHTML" hx-push-url="true" class="flex items-center justify-between px-5 py-3 hover:bg-white/[0.02] transition-colors duration-150">
+ <div class="flex items-center gap-3">
+ <div class="flex items-center justify-center w-8 h-8 rounded-lg bg-accent-500/10">
+ <svg class="w-4 h-4 text-accent-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M2.25 13.5h3.86a2.25 2.25 0 0 1 2.012 1.244l.256.512a2.25 2.25 0 0 0 2.013 1.244h2.21a2.25 2.25 0 0 0 2.013-1.244l.256-.512a2.25 2.25 0 0 1 2.013-1.244h3.859m-19.5.338V18a2.25 2.25 0 0 0 2.25 2.25h15A2.25 2.25 0 0 0 21.75 18v-4.162c0-.224-.034-.447-.1-.661L19.24 5.338a2.25 2.25 0 0 0-2.15-1.588H6.911a2.25 2.25 0 0 0-2.15 1.588L2.35 13.177a2.25 2.25 0 0 0-.1.661Z" />
+ </svg>
+ </div>
+ <div>
+ <p class="text-sm text-zinc-200">{{ mailbox.Address }}</p>
+ <p class="text-xs text-zinc-600">{{ mailbox.User.DisplayName }}</p>
+ </div>
+ </div>
+ </a>
+ {% endfor %}
+ </div>
+ {% else %}
<div class="flex flex-col items-center justify-center py-16 text-center">
<div class="flex items-center justify-center w-12 h-12 rounded-2xl bg-surface-800 mb-4">
<svg class="w-6 h-6 text-zinc-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
@@ -14,5 +34,6 @@
<p class="text-sm text-zinc-400">No mailboxes yet</p>
<p class="mt-1 text-xs text-zinc-600">Send an email to start receiving mail</p>
</div>
+ {% endif %}
</div>
-</div> \ No newline at end of file
+</div>
diff --git a/templates/dashboard/htmx/overview.htmx.django b/templates/dashboard/htmx/overview.htmx.django
index 8d9e88a..c5a7b4d 100644
--- a/templates/dashboard/htmx/overview.htmx.django
+++ b/templates/dashboard/htmx/overview.htmx.django
@@ -10,7 +10,7 @@
</div>
<span class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Mailboxes</span>
</div>
- <p class="text-3xl font-bold text-zinc-100 tracking-tight">0</p>
+ <p class="text-3xl font-bold text-zinc-100 tracking-tight">{{ MailboxCount }}</p>
<p class="mt-1 text-xs text-zinc-600">Active inboxes</p>
</div>
@@ -23,7 +23,7 @@
</div>
<span class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Emails</span>
</div>
- <p class="text-3xl font-bold text-zinc-100 tracking-tight">0</p>
+ <p class="text-3xl font-bold text-zinc-100 tracking-tight">{{ EmailCount }}</p>
<p class="mt-1 text-xs text-zinc-600">Total received</p>
</div>
@@ -37,7 +37,7 @@
<span class="text-xs font-medium text-zinc-500 uppercase tracking-wider">Server</span>
</div>
<p class="text-3xl font-bold text-emerald-400 tracking-tight">Online</p>
- <p class="mt-1 text-xs text-zinc-600">SMTP listening on :1025</p>
+ <p class="mt-1 text-xs text-zinc-600">SMTP listening on {{ SMTPAddress }}</p>
</div>
</div>
diff --git a/templates/dashboard/htmx/users.htmx.django b/templates/dashboard/htmx/users.htmx.django
index 7a36fe8..6578ba2 100644
--- a/templates/dashboard/htmx/users.htmx.django
+++ b/templates/dashboard/htmx/users.htmx.django
@@ -3,27 +3,37 @@
<div class="glass rounded-xl glow-border">
<div class="flex items-center justify-between px-5 py-4 border-b border-white/[0.04]">
<h2 class="text-sm font-medium text-zinc-200">All Users</h2>
+ <span class="text-xs text-zinc-600">{{ total }} total</span>
</div>
- {% if AuthEnabled %}
- <div class="flex flex-col items-center justify-center py-16 text-center">
- <div class="flex items-center justify-center w-12 h-12 rounded-2xl bg-surface-800 mb-4">
- <svg class="w-6 h-6 text-zinc-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
- <path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" />
- </svg>
+ {% if items %}
+ <div class="divide-y divide-white/[0.04]">
+ {% for user in items %}
+ <div class="flex items-center justify-between px-5 py-3">
+ <div class="flex items-center gap-3">
+ <div class="flex items-center justify-center w-8 h-8 rounded-lg bg-accent-500/10">
+ <svg class="w-4 h-4 text-accent-400" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15.75 6a3.75 3.75 0 1 1-7.5 0 3.75 3.75 0 0 1 7.5 0ZM4.501 20.118a7.5 7.5 0 0 1 14.998 0A17.933 17.933 0 0 1 12 21.75c-2.676 0-5.216-.584-7.499-1.632Z" />
+ </svg>
+ </div>
+ <div>
+ <p class="text-sm text-zinc-200">{{ user.DisplayName }}</p>
+ <p class="text-xs text-zinc-600">{{ user.Username }}</p>
+ </div>
+ </div>
+ <span class="text-xs text-zinc-600">{{ user.Mailboxes|length }} mailbox{{ user.Mailboxes|length|pluralize:"es" }}</span>
</div>
- <p class="text-sm text-zinc-400">No additional users</p>
- <p class="mt-1 text-xs text-zinc-600">Users are configured in config.toml</p>
+ {% endfor %}
</div>
{% else %}
<div class="flex flex-col items-center justify-center py-16 text-center">
<div class="flex items-center justify-center w-12 h-12 rounded-2xl bg-surface-800 mb-4">
<svg class="w-6 h-6 text-zinc-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="1.5">
- <path stroke-linecap="round" stroke-linejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 0 0 2.625.372 9.337 9.337 0 0 0 4.121-.952 4.125 4.125 0 0 0-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 0 1 8.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0 1 11.964-3.07M12 6.375a3.375 3.375 0 1 1-6.75 0 3.375 3.375 0 0 1 6.75 0Zm8.25 2.25a2.625 2.625 0 1 1-5.25 0 2.625 2.625 0 0 1 5.25 0Z" />
</svg>
</div>
- <p class="text-sm text-zinc-400">Authentication is disabled</p>
- <p class="mt-1 text-xs text-zinc-600">Enable authentication in config.toml to manage users</p>
+ <p class="text-sm text-zinc-400">No users yet</p>
+ <p class="mt-1 text-xs text-zinc-600">Users are created when emails are received</p>
</div>
{% endif %}
</div>
-</div> \ No newline at end of file
+</div>
diff --git a/types/overview.go b/types/overview.go
new file mode 100644
index 0000000..ac983a8
--- /dev/null
+++ b/types/overview.go
@@ -0,0 +1,7 @@
+package types
+
+type Overview struct {
+ MailboxCount int64 `json:"MailboxCount"`
+ EmailCount int64 `json:"EmailCount"`
+ SMTPAddress string `json:"SMTPAddress"`
+}
diff --git a/types/response.go b/types/response.go
index d38051d..e576142 100644
--- a/types/response.go
+++ b/types/response.go
@@ -3,3 +3,11 @@ package types
type MessageResponse struct {
Message string
}
+
+type PaginatedResponse struct {
+ Items any `json:"items"`
+ Total int64 `json:"total"`
+ Page int `json:"page"`
+ PerPage int `json:"per_page"`
+ TotalPages int `json:"total_pages"`
+}
diff --git a/utils/email/constants.go b/utils/email/constants.go
new file mode 100644
index 0000000..4f3009d
--- /dev/null
+++ b/utils/email/constants.go
@@ -0,0 +1,7 @@
+package email
+
+const (
+ LOG_PREFIX = "Email"
+ SNIPPET_LENGTH = 200
+ ADDRESS_JOINER = ", "
+)
diff --git a/utils/email/functions.go b/utils/email/functions.go
new file mode 100644
index 0000000..b65a98c
--- /dev/null
+++ b/utils/email/functions.go
@@ -0,0 +1,79 @@
+package email
+
+import (
+ "net/mail"
+ "strings"
+
+ "github.com/jhillyerd/enmime"
+)
+
+func extractAddress(rawHeader string) string {
+ parsed, parseError := mail.ParseAddress(rawHeader)
+ if parseError != nil {
+ return rawHeader
+ }
+
+ return parsed.Address
+}
+
+func extractName(rawHeader string) string {
+ parsed, parseError := mail.ParseAddress(rawHeader)
+ if parseError != nil {
+ return ""
+ }
+
+ return parsed.Name
+}
+
+func extractAddressList(rawHeader string) []string {
+ if rawHeader == "" {
+ return nil
+ }
+
+ addresses, parseError := mail.ParseAddressList(rawHeader)
+ if parseError != nil {
+ return []string{rawHeader}
+ }
+
+ extracted := make([]string, len(addresses))
+ for index, address := range addresses {
+ extracted[index] = address.Address
+ }
+
+ return extracted
+}
+
+func generateSnippet(textBody string) string {
+ trimmed := strings.TrimSpace(textBody)
+ if len(trimmed) <= SNIPPET_LENGTH {
+ return trimmed
+ }
+
+ return trimmed[:SNIPPET_LENGTH]
+}
+
+func extractAttachments(envelope *enmime.Envelope) []ParsedAttachment {
+ var attachments []ParsedAttachment
+
+ for _, attachment := range envelope.Attachments {
+ attachments = append(attachments, ParsedAttachment{
+ Filename: attachment.FileName,
+ ContentType: attachment.ContentType,
+ ContentID: attachment.ContentID,
+ Size: int64(len(attachment.Content)),
+ IsInline: false,
+ })
+ }
+
+ for _, inline := range envelope.Inlines {
+ attachments = append(attachments, ParsedAttachment{
+ Filename: inline.FileName,
+ ContentType: inline.ContentType,
+ ContentID: inline.ContentID,
+ Size: int64(len(inline.Content)),
+ IsInline: true,
+ })
+ }
+
+ return attachments
+}
diff --git a/utils/email/parse.go b/utils/email/parse.go
new file mode 100644
index 0000000..549ac2f
--- /dev/null
+++ b/utils/email/parse.go
@@ -0,0 +1,29 @@
+package email
+
+import (
+ "bytes"
+
+ "github.com/jhillyerd/enmime"
+)
+
+func Parse(rawMessage []byte) (*ParsedEmail, error) {
+ envelope, parseError := enmime.ReadEnvelope(bytes.NewReader(rawMessage))
+ if parseError != nil {
+ return nil, parseError
+ }
+
+ return &ParsedEmail{
+ MessageID: envelope.GetHeader("Message-ID"),
+ FromAddress: extractAddress(envelope.GetHeader("From")),
+ FromName: extractName(envelope.GetHeader("From")),
+ ToAddresses: extractAddressList(envelope.GetHeader("To")),
+ CcAddresses: extractAddressList(envelope.GetHeader("Cc")),
+ BccAddresses: extractAddressList(envelope.GetHeader("Bcc")),
+ ReplyToAddress: envelope.GetHeader("Reply-To"),
+ ReturnPath: envelope.GetHeader("Return-Path"),
+ Subject: envelope.GetHeader("Subject"),
+ Snippet: generateSnippet(envelope.Text),
+ Size: int64(len(rawMessage)),
+ Attachments: extractAttachments(envelope),
+ }, nil
+} \ No newline at end of file
diff --git a/utils/email/types.go b/utils/email/types.go
new file mode 100644
index 0000000..497fad7
--- /dev/null
+++ b/utils/email/types.go
@@ -0,0 +1,24 @@
+package email
+
+type ParsedEmail struct {
+ MessageID string
+ FromAddress string
+ FromName string
+ ToAddresses []string
+ CcAddresses []string
+ BccAddresses []string
+ ReplyToAddress string
+ ReturnPath string
+ Subject string
+ Snippet string
+ Size int64
+ Attachments []ParsedAttachment
+}
+
+type ParsedAttachment struct {
+ Filename string
+ ContentType string
+ ContentID string
+ Size int64
+ IsInline bool
+} \ No newline at end of file
diff --git a/utils/meta/constants.go b/utils/meta/constants.go
index 0138884..13a2fde 100644
--- a/utils/meta/constants.go
+++ b/utils/meta/constants.go
@@ -1,6 +1,9 @@
package meta
const (
- LOG_PREFIX = "Meta"
- REQUEST_KEY = "Request"
+ DEFAULT_PAGE = 1
+ DEFAULT_PER_PAGE = 20
+ LOG_PREFIX = "Meta"
+ MAX_PER_PAGE = 50
+ REQUEST_KEY = "Request"
)
diff --git a/utils/meta/pagination.go b/utils/meta/pagination.go
new file mode 100644
index 0000000..9244294
--- /dev/null
+++ b/utils/meta/pagination.go
@@ -0,0 +1,84 @@
+package meta
+
+import (
+ "dove/types"
+ "strconv"
+
+ "github.com/gofiber/fiber/v2"
+ "gorm.io/gorm"
+)
+
+func Paginate(context *fiber.Ctx) Pagination {
+ requestData := Request(context)
+
+ page := DEFAULT_PAGE
+ perPage := DEFAULT_PER_PAGE
+
+ if requestData != nil {
+ if pageValue := requestData.Query("page"); pageValue != nil {
+ parsed, _ := strconv.Atoi(pageValue.String())
+ if parsed >= DEFAULT_PAGE {
+ page = parsed
+ }
+ }
+
+ if perPageValue := requestData.Query("per_page"); perPageValue != nil {
+ parsed, _ := strconv.Atoi(perPageValue.String())
+ if parsed >= DEFAULT_PAGE && parsed <= MAX_PER_PAGE {
+ perPage = parsed
+ }
+ }
+ }
+
+ return Pagination{Page: page, PerPage: perPage}
+}
+
+func Sort(context *fiber.Ctx, allowedFields []string, fallbackField string) Sorting {
+ requestData := Request(context)
+
+ field := fallbackField
+ direction := "desc"
+
+ if requestData != nil {
+ if sortValue := requestData.Query("sort"); sortValue != nil {
+ for _, allowedField := range allowedFields {
+ if sortValue.String() == allowedField {
+ field = sortValue.String()
+ break
+ }
+ }
+ }
+
+ if orderValue := requestData.Query("order"); orderValue != nil {
+ switch orderValue.String() {
+ case "asc", "desc":
+ direction = orderValue.String()
+ }
+ }
+ }
+
+ return Sorting{Field: field, Direction: direction}
+}
+
+func (self Pagination) Apply(query *gorm.DB) *gorm.DB {
+ return query.Offset((self.Page - 1) * self.PerPage).Limit(self.PerPage)
+}
+
+func (self Sorting) Apply(query *gorm.DB) *gorm.DB {
+ return query.Order(self.Field + " " + self.Direction)
+}
+
+func (self Pagination) Response(items any, total int64) types.PaginatedResponse {
+ totalPages := int(total) / self.PerPage
+ if int(total)%self.PerPage > 0 {
+ totalPages++
+ }
+
+ return types.PaginatedResponse{
+ Items: items,
+ Total: total,
+ Page: self.Page,
+ PerPage: self.PerPage,
+ TotalPages: totalPages,
+ }
+}
diff --git a/utils/meta/types.go b/utils/meta/types.go
index 3bc8a0e..c7b8e4d 100644
--- a/utils/meta/types.go
+++ b/utils/meta/types.go
@@ -14,3 +14,13 @@ type request struct {
type value struct {
data string
}
+
+type Pagination struct {
+ Page int
+ PerPage int
+}
+
+type Sorting struct {
+ Field string
+ Direction string
+}
diff --git a/utils/smtp/functions.go b/utils/smtp/functions.go
new file mode 100644
index 0000000..ae9aeae
--- /dev/null
+++ b/utils/smtp/functions.go
@@ -0,0 +1,34 @@
+package smtp
+
+import (
+ "dove/config"
+ "dove/messages"
+ "dove/utils/logger"
+ "time"
+
+ "github.com/emersion/go-smtp"
+)
+
+func createServer(address string) *smtp.Server {
+ smtpServer := smtp.NewServer(smtp.BackendFunc(func(connection *smtp.Conn) (smtp.Session, error) {
+ logger.Debugf(LOG_PREFIX, messages.SMTPSessionStarted, connection.Hostname())
+ return &session{}, nil
+ }))
+
+ smtpServer.Addr = address
+ smtpServer.Domain = config.SMTP.Domain
+ smtpServer.ReadTimeout = time.Duration(config.SMTP.ReadTimeout) * time.Second
+ smtpServer.WriteTimeout = time.Duration(config.SMTP.WriteTimeout) * time.Second
+ smtpServer.MaxMessageBytes = int64(config.SMTP.MaxMessageSize)
+ smtpServer.AllowInsecureAuth = true
+
+ return smtpServer
+}
+
+func startListener(smtpServer *smtp.Server, label string, address string) {
+ logger.Successf(LOG_PREFIX, messages.SMTPServerStarting, label, address)
+
+ if listenError := smtpServer.ListenAndServe(); listenError != nil {
+ logger.Fatalf(LOG_PREFIX, messages.SMTPListenFailed, label, listenError)
+ }
+} \ No newline at end of file
diff --git a/utils/smtp/server.go b/utils/smtp/server.go
index 0092c16..778dd3b 100644
--- a/utils/smtp/server.go
+++ b/utils/smtp/server.go
@@ -5,9 +5,6 @@ import (
"dove/messages"
"dove/utils/logger"
"fmt"
- "time"
-
- gosmtp "github.com/emersion/go-smtp"
)
var activeServers []serverInstance
@@ -30,27 +27,3 @@ func Shutdown() {
logger.Infof(LOG_PREFIX, messages.SMTPShutdownComplete)
}
-
-func createServer(address string) *gosmtp.Server {
- smtpServer := gosmtp.NewServer(gosmtp.BackendFunc(func(connection *gosmtp.Conn) (gosmtp.Session, error) {
- logger.Debugf(LOG_PREFIX, messages.SMTPSessionStarted, connection.Hostname())
- return &session{}, nil
- }))
-
- smtpServer.Addr = address
- smtpServer.Domain = config.SMTP.Domain
- smtpServer.ReadTimeout = time.Duration(config.SMTP.ReadTimeout) * time.Second
- smtpServer.WriteTimeout = time.Duration(config.SMTP.WriteTimeout) * time.Second
- smtpServer.MaxMessageBytes = int64(config.SMTP.MaxMessageSize)
- smtpServer.AllowInsecureAuth = true
-
- return smtpServer
-}
-
-func startListener(smtpServer *gosmtp.Server, label string, address string) {
- logger.Successf(LOG_PREFIX, messages.SMTPServerStarting, label, address)
-
- if listenError := smtpServer.ListenAndServe(); listenError != nil {
- logger.Fatalf(LOG_PREFIX, messages.SMTPListenFailed, label, listenError)
- }
-}
diff --git a/utils/smtp/session.go b/utils/smtp/session.go
index 029c257..fe1f5c4 100644
--- a/utils/smtp/session.go
+++ b/utils/smtp/session.go
@@ -3,11 +3,12 @@ package smtp
import (
"dove/config"
"dove/messages"
- "dove/utils/logger"
+ "dove/services"
"dove/utils/errors"
+ "dove/utils/logger"
"io"
- gosmtp "github.com/emersion/go-smtp"
+ "github.com/emersion/go-smtp"
)
func (self *session) AuthPlain(username string, password string) error {
@@ -23,13 +24,13 @@ func (self *session) AuthPlain(username string, password string) error {
return nil
}
-func (self *session) Mail(senderAddress string, mailOptions *gosmtp.MailOptions) error {
+func (self *session) Mail(senderAddress string, _ *smtp.MailOptions) error {
logger.Debugf(LOG_PREFIX, messages.SMTPMailFrom, senderAddress)
self.fromAddress = senderAddress
return nil
}
-func (self *session) Rcpt(recipientAddress string, recipientOptions *gosmtp.RcptOptions) error {
+func (self *session) Rcpt(recipientAddress string, _ *smtp.RcptOptions) error {
logger.Debugf(LOG_PREFIX, messages.SMTPRecipient, recipientAddress)
self.toAddresses = append(self.toAddresses, recipientAddress)
return nil
@@ -43,9 +44,9 @@ func (self *session) Data(messageReader io.Reader) error {
logger.Infof(LOG_PREFIX, messages.SMTPMessageReceived, len(rawMessage))
- if storeError := storeMessage(self.fromAddress, self.toAddresses, rawMessage); storeError != nil {
- logger.Errorf(LOG_PREFIX, messages.SMTPMessageStoreFailed, storeError)
- return storeError
+ if processError := services.ProcessEmail(rawMessage, self.toAddresses); processError != nil {
+ logger.Errorf(LOG_PREFIX, messages.SMTPMessageStoreFailed, processError)
+ return processError
}
return nil
diff --git a/utils/smtp/storage.go b/utils/smtp/storage.go
deleted file mode 100644
index b580d67..0000000
--- a/utils/smtp/storage.go
+++ /dev/null
@@ -1,22 +0,0 @@
-package smtp
-
-import (
- "dove/config"
- "fmt"
- "os"
- "path/filepath"
- "time"
-)
-
-func storeMessage(fromAddress string, toAddresses []string, rawMessage []byte) error {
- emailDirectory := filepath.Join(config.DataDir, "emails")
-
- if directoryError := os.MkdirAll(emailDirectory, 0750); directoryError != nil {
- return directoryError
- }
-
- filename := fmt.Sprintf("%d.eml", time.Now().UnixNano())
- filePath := filepath.Join(emailDirectory, filename)
-
- return os.WriteFile(filePath, rawMessage, 0640)
-} \ No newline at end of file
diff --git a/utils/smtp/types.go b/utils/smtp/types.go
index 38c6504..aced45a 100644
--- a/utils/smtp/types.go
+++ b/utils/smtp/types.go
@@ -1,6 +1,6 @@
package smtp
-import gosmtp "github.com/emersion/go-smtp"
+import "github.com/emersion/go-smtp"
type session struct {
fromAddress string
@@ -8,6 +8,6 @@ type session struct {
}
type serverInstance struct {
- server *gosmtp.Server
+ server *smtp.Server
label string
}