diff options
| author | Bobby <[email protected]> | 2026-03-07 18:42:40 +0530 |
|---|---|---|
| committer | Bobby <[email protected]> | 2026-03-07 18:42:40 +0530 |
| commit | 96c136f046d78c51210927e61483a36a220fedcb (patch) | |
| tree | 8f5baf294d92ec3bc503bd02f4a08871874f048d | |
| parent | 6bc2ef7a8972547f10a5a8f50269b0e6b487a580 (diff) | |
| download | dove-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.
36 files changed, 799 insertions, 81 deletions
@@ -46,3 +46,5 @@ static/js/htmx.min.js config/example.config.toml config.toml +# Database files +*.db @@ -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 ) @@ -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 } |
