aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--config/config.go4
-rw-r--r--config/types.go8
-rw-r--r--config/validation.go87
-rw-r--r--database/database.go2
-rw-r--r--database/migration.go22
-rw-r--r--dove/main.go5
-rw-r--r--example.config.toml101
-rw-r--r--go.mod2
-rw-r--r--go.sum4
-rw-r--r--messages/config.go2
-rw-r--r--messages/database.go1
-rw-r--r--messages/smtp.go15
-rw-r--r--middleware/constants.go5
-rw-r--r--middleware/logging.go69
-rw-r--r--middleware/middleware.go1
-rw-r--r--models/alias.go10
-rw-r--r--models/attachment.go14
-rw-r--r--models/email.go25
-rw-r--r--models/mailbox.go12
-rw-r--r--models/tag.go9
-rw-r--r--models/user.go10
-rw-r--r--utils/meta/request.go40
-rw-r--r--utils/meta/types.go3
-rw-r--r--utils/meta/value.go23
-rw-r--r--utils/smtp/constants.go5
-rw-r--r--utils/smtp/server.go56
-rw-r--r--utils/smtp/session.go61
-rw-r--r--utils/smtp/storage.go22
-rw-r--r--utils/smtp/types.go13
29 files changed, 534 insertions, 97 deletions
diff --git a/config/config.go b/config/config.go
index 8a481c7..7465da7 100644
--- a/config/config.go
+++ b/config/config.go
@@ -42,5 +42,9 @@ func init() {
AuthEnabled = isAuthEnabled()
+ if portError := ValidatePorts(); portError != nil {
+ logger.Fatalf(LOG_PREFIX, messages.ConfigPortValidFailed, portError)
+ }
+
logger.Successf(LOG_PREFIX, messages.ConfigLoaded)
}
diff --git a/config/types.go b/config/types.go
index ab5a569..33d37f7 100644
--- a/config/types.go
+++ b/config/types.go
@@ -15,7 +15,10 @@ type smtp struct {
Port int `toml:"port" default:"5025"`
SMTPSPort int `toml:"smtps_port" default:"5465"`
StartTLSPort int `toml:"starttls_port" default:"5587"`
+ Domain string `toml:"domain" default:"localhost"`
MaxMessageSize int `toml:"max_message_size" default:"26214400"`
+ ReadTimeout int `toml:"read_timeout" default:"30"`
+ WriteTimeout int `toml:"write_timeout" default:"30"`
AuthRequired bool `toml:"auth_required" default:"false"`
Username string `toml:"username"`
Password string `toml:"password"`
@@ -58,3 +61,8 @@ type mailbox struct {
Mode enums.MailboxMode `toml:"mode" default:"registered"`
}
+type portAssignment struct {
+ service string
+ host string
+ port int
+}
diff --git a/config/validation.go b/config/validation.go
new file mode 100644
index 0000000..46b85ac
--- /dev/null
+++ b/config/validation.go
@@ -0,0 +1,87 @@
+package config
+
+import (
+ "fmt"
+
+ "dove/messages"
+ "dove/utils/errors"
+)
+
+func ValidatePorts() error {
+ portAssignments := collectPortAssignments()
+ occupiedPorts := make(map[string]string)
+
+ for _, assignment := range portAssignments {
+ portKey := fmt.Sprintf("%s:%d", assignment.host, assignment.port)
+
+ if existingService, occupied := occupiedPorts[portKey]; occupied {
+ return errors.Error(messages.ConfigPortCollision, assignment.service, existingService, portKey)
+ }
+
+ occupiedPorts[portKey] = assignment.service
+ }
+
+ return nil
+}
+
+func collectPortAssignments() []portAssignment {
+ assignments := []portAssignment{
+ {
+ service: "HTTP",
+ host: Server.Host,
+ port: Server.Port,
+ },
+ {
+ service: "SMTP",
+ host: SMTP.Host,
+ port: SMTP.Port,
+ },
+ {
+ service: "IMAP",
+ host: IMAP.Host,
+ port: IMAP.Port,
+ },
+ {
+ service: "POP3",
+ host: POP3.Host,
+ port: POP3.Port,
+ },
+ }
+
+ if SMTP.TLSEnabled {
+ assignments = append(assignments,
+ portAssignment{
+ service: "SMTPS",
+ host: SMTP.Host,
+ port: SMTP.SMTPSPort,
+ },
+ portAssignment{
+ service: "SMTP STARTTLS",
+ host: SMTP.Host,
+ port: SMTP.StartTLSPort,
+ },
+ )
+ }
+
+ if IMAP.TLSEnabled {
+ assignments = append(assignments,
+ portAssignment{
+ service: "IMAPS",
+ host: IMAP.Host,
+ port: IMAP.IMAPSPort,
+ },
+ )
+ }
+
+ if POP3.TLSEnabled {
+ assignments = append(assignments,
+ portAssignment{
+ service: "POP3S",
+ host: POP3.Host,
+ port: POP3.POP3SPort,
+ },
+ )
+ }
+
+ return assignments
+}
diff --git a/database/database.go b/database/database.go
index 27b99eb..ac06c98 100644
--- a/database/database.go
+++ b/database/database.go
@@ -33,4 +33,6 @@ func init() {
sqlDB.SetConnMaxLifetime(MAX_CONNECTION_LIFETIME)
logger.Successf(LOG_PREFIX, messages.DatabaseConnected, databaseDSN)
+
+ migrate()
}
diff --git a/database/migration.go b/database/migration.go
new file mode 100644
index 0000000..4d8363c
--- /dev/null
+++ b/database/migration.go
@@ -0,0 +1,22 @@
+package database
+
+import (
+ "dove/messages"
+ "dove/models"
+ "dove/utils/logger"
+)
+
+func migrate() {
+ migrationError := DB.AutoMigrate(
+ &models.User{},
+ &models.Mailbox{},
+ &models.Alias{},
+ &models.Email{},
+ &models.Tag{},
+ &models.Attachment{},
+ )
+
+ if migrationError != nil {
+ logger.Fatalf(LOG_PREFIX, messages.DatabaseMigrationFailed, migrationError)
+ }
+}
diff --git a/dove/main.go b/dove/main.go
index e3da2ce..4de7ef8 100644
--- a/dove/main.go
+++ b/dove/main.go
@@ -12,6 +12,7 @@ import (
"dove/router"
"dove/tags"
"dove/utils/logger"
+ "dove/utils/smtp"
"github.com/gofiber/fiber/v2"
"github.com/gofiber/fiber/v2/middleware/recover"
@@ -50,6 +51,8 @@ func serve(command *cobra.Command, arguments []string) error {
middleware.Initialize(application)
router.Initialize(application)
+ smtp.Start()
+
shutdownSignal := make(chan os.Signal, 1)
signal.Notify(shutdownSignal, syscall.SIGINT, syscall.SIGTERM)
@@ -65,6 +68,8 @@ func serve(command *cobra.Command, arguments []string) error {
<-shutdownSignal
logger.Infof(LOG_PREFIX, messages.ServerShuttingDown)
+ smtp.Shutdown()
+
if shutdownError := application.Shutdown(); shutdownError != nil {
logger.Errorf(LOG_PREFIX, messages.ServerShutdownFailed, shutdownError)
}
diff --git a/example.config.toml b/example.config.toml
index 8748a3b..be3cdaf 100644
--- a/example.config.toml
+++ b/example.config.toml
@@ -1,22 +1,77 @@
+# =============================================================================
+# Dove - Local SMTP Email Testing Tool
+# =============================================================================
+# Copy this file to config.toml and adjust values as needed.
+# All values shown below are defaults and can be omitted if unchanged.
+
+# -----------------------------------------------------------------------------
+# HTTP Server
+# -----------------------------------------------------------------------------
+# The web dashboard for viewing and managing captured emails.
+
[server]
+# Network interface to bind the HTTP server to.
+# Use "0.0.0.0" to listen on all interfaces, or "127.0.0.1" for local only.
host = "0.0.0.0"
+
+# Port for the web dashboard.
port = 8080
+
+# Enable verbose debug logging for HTTP requests.
debug = false
+
+# Optional credentials to protect the web dashboard.
+# Leave commented out to disable dashboard authentication.
# username = ""
# password = ""
+# -----------------------------------------------------------------------------
+# SMTP Server
+# -----------------------------------------------------------------------------
+# Receives incoming emails from mail clients and applications.
+
[smtp]
+# Network interface to bind the SMTP server to.
host = "0.0.0.0"
+
+# Port for plain SMTP connections.
port = 5025
+
+# Port for implicit TLS (SMTPS) connections. Requires tls_enabled = true.
smtps_port = 5465
+
+# Port for STARTTLS connections. Requires tls_enabled = true.
starttls_port = 5587
+
+# The domain name announced in SMTP EHLO/HELO greetings.
+# This identifies the mail server to connecting clients.
+domain = "localhost"
+
+# Maximum allowed email size in bytes (default: 25 MB).
max_message_size = 26214400
+
+# Timeout in seconds for reading data from SMTP clients.
+read_timeout = 30
+
+# Timeout in seconds for writing data to SMTP clients.
+write_timeout = 30
+
+# Require SMTP authentication before accepting messages.
auth_required = false
+
+# Credentials for SMTP authentication. Only used when auth_required = true.
# username = ""
# password = ""
+
+# Enable TLS support for SMTPS and STARTTLS listeners.
tls_enabled = false
+
+# Paths to TLS certificate and private key files. Required when tls_enabled = true.
# tls_cert = ""
# tls_key = ""
+
+# Relay captured emails to an upstream SMTP server.
+# Useful for forwarding test emails to a real mail server.
relay_enabled = false
# relay_host = ""
relay_port = 587
@@ -24,27 +79,71 @@ relay_port = 587
# relay_password = ""
relay_starttls = true
+# -----------------------------------------------------------------------------
+# IMAP Server
+# -----------------------------------------------------------------------------
+# Allows mail clients to retrieve captured emails via the IMAP protocol.
+
[imap]
+# Network interface to bind the IMAP server to.
host = "0.0.0.0"
+
+# Port for plain IMAP connections.
port = 5143
+
+# Port for implicit TLS (IMAPS) connections. Requires tls_enabled = true.
imaps_port = 5993
+
+# Require IMAP authentication before granting access.
auth_required = false
+
+# Credentials for IMAP authentication. Only used when auth_required = true.
# username = ""
# password = ""
+
+# Enable TLS support for the IMAPS listener.
tls_enabled = false
+
+# Paths to TLS certificate and private key files. Required when tls_enabled = true.
# tls_cert = ""
# tls_key = ""
+# -----------------------------------------------------------------------------
+# POP3 Server
+# -----------------------------------------------------------------------------
+# Allows mail clients to retrieve captured emails via the POP3 protocol.
+
[pop3]
+# Network interface to bind the POP3 server to.
host = "0.0.0.0"
+
+# Port for plain POP3 connections.
port = 5110
+
+# Port for implicit TLS (POP3S) connections. Requires tls_enabled = true.
pop3s_port = 5995
+
+# Require POP3 authentication before granting access.
auth_required = false
+
+# Credentials for POP3 authentication. Only used when auth_required = true.
# username = ""
# password = ""
+
+# Enable TLS support for the POP3S listener.
tls_enabled = false
+
+# Paths to TLS certificate and private key files. Required when tls_enabled = true.
# tls_cert = ""
# tls_key = ""
+# -----------------------------------------------------------------------------
+# Mailbox
+# -----------------------------------------------------------------------------
+# Controls how incoming emails are routed to mailboxes.
+
[mailbox]
-mode = "registered" \ No newline at end of file
+# Mailbox routing mode:
+# "registered" - Only accept emails for registered mailbox addresses.
+# "open" - Automatically create mailboxes for any recipient address.
+mode = "registered"
diff --git a/go.mod b/go.mod
index 7b1ea17..9995e0d 100644
--- a/go.mod
+++ b/go.mod
@@ -14,6 +14,8 @@ require (
require (
github.com/andybalholm/brotli v1.1.0 // indirect
+ github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 // indirect
+ github.com/emersion/go-smtp v0.24.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
diff --git a/go.sum b/go.sum
index 9e0be7a..b586b87 100644
--- a/go.sum
+++ b/go.sum
@@ -3,6 +3,10 @@ github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer5
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/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6 h1:oP4q0fw+fOSWn3DfFi4EXdT+B+gTtzx8GC9xsc26Znk=
+github.com/emersion/go-sasl v0.0.0-20241020182733-b788ff22d5a6/go.mod h1:iL2twTeMvZnrg54ZoPDNfJaJaqy0xIQFuBdrLsmspwQ=
+github.com/emersion/go-smtp v0.24.0 h1:g6AfoF140mvW0vLNPD/LuCBLEAdlxOjIXqbIkJIS6Wk=
+github.com/emersion/go-smtp v0.24.0/go.mod h1:ZtRRkbTyp2XTHCA+BmyTFTrj8xY4I+b4McvHxCU2gsQ=
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=
diff --git a/messages/config.go b/messages/config.go
index 10c85db..43b40a6 100644
--- a/messages/config.go
+++ b/messages/config.go
@@ -6,6 +6,8 @@ const (
ConfigFileLoadFailed = "Failed to load config: %v"
ConfigFileReadFailed = "Failed to read config file %s: %s."
ConfigLoaded = "Configuration loaded successfully."
+ ConfigPortCollision = "Port collision: %s and %s both configured on %s."
+ ConfigPortValidFailed = "Port validation failed: %v"
ConfigSectionInvalid = "Config section '%s' has invalid data."
ConfigSectionParseFailed = "Failed to parse config section: %v"
)
diff --git a/messages/database.go b/messages/database.go
index 57bc27f..36ba275 100644
--- a/messages/database.go
+++ b/messages/database.go
@@ -3,5 +3,6 @@ package messages
const (
DatabaseConnected = "Connected to %s."
DatabaseConnectionFailed = "Failed to connect to database: %v"
+ DatabaseMigrationFailed = "Failed to run database migrations: %v"
DatabasePoolConfigFailed = "Failed to configure connection pool: %v"
)
diff --git a/messages/smtp.go b/messages/smtp.go
new file mode 100644
index 0000000..eef071c
--- /dev/null
+++ b/messages/smtp.go
@@ -0,0 +1,15 @@
+package messages
+
+const (
+ SMTPListenFailed = "Failed to start %s listener: %v"
+ SMTPSessionStarted = "New session from %s."
+ SMTPMailFrom = "Mail from: %s"
+ SMTPRecipient = "Recipient: %s"
+ SMTPMessageReceived = "Message received (%d bytes)."
+ SMTPMessageStoreFailed = "Failed to store message: %v"
+ SMTPServerStarting = "%s listener started on %s."
+ SMTPShutdownFailed = "Failed to shutdown %s listener: %v"
+ SMTPShutdownComplete = "All listeners stopped."
+ SMTPAuthFailed = "Authentication failed for user: %s"
+ SMTPInvalidCredentials = "Invalid credentials."
+)
diff --git a/middleware/constants.go b/middleware/constants.go
deleted file mode 100644
index dc6505b..0000000
--- a/middleware/constants.go
+++ /dev/null
@@ -1,5 +0,0 @@
-package middleware
-
-const (
- LOG_PREFIX = "HTTP"
-)
diff --git a/middleware/logging.go b/middleware/logging.go
deleted file mode 100644
index a8c3bd6..0000000
--- a/middleware/logging.go
+++ /dev/null
@@ -1,69 +0,0 @@
-package middleware
-
-import (
- "fmt"
- "strconv"
- "strings"
- "time"
-
- "dove/utils/logger"
-
- "github.com/gofiber/fiber/v2"
-)
-
-func httpLogger(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 1a4f1ed..1e73145 100644
--- a/middleware/middleware.go
+++ b/middleware/middleware.go
@@ -3,7 +3,6 @@ package middleware
import "github.com/gofiber/fiber/v2"
func Initialize(application *fiber.App) {
- application.Use(httpLogger)
application.Use(requestBuilder)
application.Use(globals)
}
diff --git a/models/alias.go b/models/alias.go
new file mode 100644
index 0000000..5b0dcb3
--- /dev/null
+++ b/models/alias.go
@@ -0,0 +1,10 @@
+package models
+
+import "gorm.io/gorm"
+
+type Alias struct {
+ gorm.Model
+ SourceAddress string `gorm:"uniqueIndex;not null" json:"source_address"`
+ MailboxID uint `gorm:"not null" json:"mailbox_id"`
+ Mailbox Mailbox `gorm:"foreignKey:MailboxID" json:"mailbox"`
+}
diff --git a/models/attachment.go b/models/attachment.go
new file mode 100644
index 0000000..cd1a741
--- /dev/null
+++ b/models/attachment.go
@@ -0,0 +1,14 @@
+package models
+
+import "gorm.io/gorm"
+
+type Attachment struct {
+ gorm.Model
+ EmailID uint `gorm:"not null;index" json:"email_id"`
+ Email Email `gorm:"foreignKey:EmailID" json:"email"`
+ Filename string `gorm:"not null" json:"filename"`
+ ContentType string `gorm:"not null" json:"content_type"`
+ ContentID string `json:"content_id"`
+ Size int64 `json:"size"`
+ IsInline bool `gorm:"default:false" json:"is_inline"`
+}
diff --git a/models/email.go b/models/email.go
new file mode 100644
index 0000000..12b3ad1
--- /dev/null
+++ b/models/email.go
@@ -0,0 +1,25 @@
+package models
+
+import "gorm.io/gorm"
+
+type Email struct {
+ gorm.Model
+ MailboxID uint `gorm:"not null;index" json:"mailbox_id"`
+ Mailbox Mailbox `gorm:"foreignKey:MailboxID" json:"mailbox"`
+ MessageID string `gorm:"index" json:"message_id"`
+ Filename string `gorm:"uniqueIndex;not null" json:"filename"`
+ FromAddress string `gorm:"not null" json:"from_address"`
+ FromName string `json:"from_name"`
+ ToAddresses string `gorm:"not null" json:"to_addresses"`
+ CcAddresses string `json:"cc_addresses"`
+ BccAddresses string `json:"bcc_addresses"`
+ ReplyToAddress string `json:"reply_to_address"`
+ ReturnPath string `json:"return_path"`
+ Subject string `json:"subject"`
+ Snippet string `json:"snippet"`
+ Size int64 `json:"size"`
+ IsRead bool `gorm:"default:false;index" json:"is_read"`
+ AttachmentCount int `gorm:"default:0" json:"attachment_count"`
+ InlineCount int `gorm:"default:0" json:"inline_count"`
+ Tags []Tag `gorm:"many2many:email_tags" json:"tags"`
+}
diff --git a/models/mailbox.go b/models/mailbox.go
new file mode 100644
index 0000000..da4114f
--- /dev/null
+++ b/models/mailbox.go
@@ -0,0 +1,12 @@
+package models
+
+import "gorm.io/gorm"
+
+type Mailbox struct {
+ gorm.Model
+ Address string `gorm:"uniqueIndex;not null" json:"address"`
+ UserID uint `gorm:"not null" json:"user_id"`
+ User User `gorm:"foreignKey:UserID" json:"user"`
+ Aliases []Alias `gorm:"foreignKey:MailboxID" json:"aliases"`
+ Emails []Email `gorm:"foreignKey:MailboxID" json:"emails"`
+}
diff --git a/models/tag.go b/models/tag.go
new file mode 100644
index 0000000..79fa65f
--- /dev/null
+++ b/models/tag.go
@@ -0,0 +1,9 @@
+package models
+
+import "gorm.io/gorm"
+
+type Tag struct {
+ gorm.Model
+ Name string `gorm:"uniqueIndex;not null" json:"name"`
+ Emails []Email `gorm:"many2many:email_tags" json:"emails"`
+}
diff --git a/models/user.go b/models/user.go
new file mode 100644
index 0000000..6bb6edf
--- /dev/null
+++ b/models/user.go
@@ -0,0 +1,10 @@
+package models
+
+import "gorm.io/gorm"
+
+type User struct {
+ gorm.Model
+ Username string `gorm:"uniqueIndex;not null" json:"username"`
+ DisplayName string `json:"display_name"`
+ Mailboxes []Mailbox `gorm:"foreignKey:UserID" json:"mailboxes"`
+}
diff --git a/utils/meta/request.go b/utils/meta/request.go
index 3098970..34a659a 100644
--- a/utils/meta/request.go
+++ b/utils/meta/request.go
@@ -8,36 +8,56 @@ import (
"github.com/gofiber/fiber/v2"
)
-func Request(context *fiber.Ctx) request {
+func Request(context *fiber.Ctx) *request {
data, ok := context.Locals(REQUEST_KEY).(types.Request)
if !ok {
logger.Errorf(LOG_PREFIX, messages.MetaRequestContextMissing)
- return request{}
+ return nil
}
- return request{
+ return &request{
Request: data,
context: context,
}
}
-func (self request) Param(key string) value {
+func (self *request) Param(key string) *value {
+ if self == nil {
+ return nil
+ }
+
if self.context != nil {
result := self.context.Params(key)
if result != "" {
- return value{data: result, found: true}
+ return &value{data: result}
}
}
- return value{}
+ return nil
}
-func (self request) Query(key string) value {
+func (self *request) Query(key string) *value {
+ if self == nil {
+ return nil
+ }
+
result, found := findParam(self.Request.Query, key)
- return value{data: result, found: found}
+ if found {
+ return &value{data: result}
+ }
+
+ return nil
}
-func (self request) Header(key string) value {
+func (self *request) Header(key string) *value {
+ if self == nil {
+ return nil
+ }
+
result, found := findParam(self.Request.Headers, key)
- return value{data: result, found: found}
+ if found {
+ return &value{data: result}
+ }
+
+ return nil
}
diff --git a/utils/meta/types.go b/utils/meta/types.go
index 8aa710f..3bc8a0e 100644
--- a/utils/meta/types.go
+++ b/utils/meta/types.go
@@ -12,6 +12,5 @@ type request struct {
}
type value struct {
- data string
- found bool
+ data string
}
diff --git a/utils/meta/value.go b/utils/meta/value.go
index fb90500..889ba0e 100644
--- a/utils/meta/value.go
+++ b/utils/meta/value.go
@@ -5,25 +5,30 @@ import (
"dove/utils/logger"
)
-func (self value) String() string {
+func (self *value) String() string {
+ if self == nil {
+ return ""
+ }
+
return self.data
}
-func (self value) Exists() bool {
- return self.found
+func (self *value) Exists() bool {
+ return self != nil
}
-func (self value) Or(fallback string) string {
- if self.found {
- return self.data
+func (self *value) Or(fallback string) string {
+ if self == nil {
+ return fallback
}
- return fallback
+ return self.data
}
-func (self value) Required() string {
- if !self.found {
+func (self *value) Required() string {
+ if self == nil {
logger.Errorf(LOG_PREFIX, messages.MetaRequiredValueMissing)
+ return ""
}
return self.data
diff --git a/utils/smtp/constants.go b/utils/smtp/constants.go
new file mode 100644
index 0000000..cc6d173
--- /dev/null
+++ b/utils/smtp/constants.go
@@ -0,0 +1,5 @@
+package smtp
+
+const (
+ LOG_PREFIX = "SMTP"
+)
diff --git a/utils/smtp/server.go b/utils/smtp/server.go
new file mode 100644
index 0000000..0092c16
--- /dev/null
+++ b/utils/smtp/server.go
@@ -0,0 +1,56 @@
+package smtp
+
+import (
+ "dove/config"
+ "dove/messages"
+ "dove/utils/logger"
+ "fmt"
+ "time"
+
+ gosmtp "github.com/emersion/go-smtp"
+)
+
+var activeServers []serverInstance
+
+func Start() {
+ plainAddress := fmt.Sprintf("%s:%d", config.SMTP.Host, config.SMTP.Port)
+ plainServer := createServer(plainAddress)
+
+ activeServers = append(activeServers, serverInstance{server: plainServer, label: LOG_PREFIX})
+
+ go startListener(plainServer, LOG_PREFIX, plainAddress)
+}
+
+func Shutdown() {
+ for _, instance := range activeServers {
+ if shutdownError := instance.server.Close(); shutdownError != nil {
+ logger.Errorf(LOG_PREFIX, messages.SMTPShutdownFailed, instance.label, shutdownError)
+ }
+ }
+
+ 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
new file mode 100644
index 0000000..029c257
--- /dev/null
+++ b/utils/smtp/session.go
@@ -0,0 +1,61 @@
+package smtp
+
+import (
+ "dove/config"
+ "dove/messages"
+ "dove/utils/logger"
+ "dove/utils/errors"
+ "io"
+
+ gosmtp "github.com/emersion/go-smtp"
+)
+
+func (self *session) AuthPlain(username string, password string) error {
+ if !config.SMTP.AuthRequired {
+ return nil
+ }
+
+ if username != config.SMTP.Username || password != config.SMTP.Password {
+ logger.Warnf(LOG_PREFIX, messages.SMTPAuthFailed, username)
+ return errors.Error(messages.SMTPInvalidCredentials)
+ }
+
+ return nil
+}
+
+func (self *session) Mail(senderAddress string, mailOptions *gosmtp.MailOptions) error {
+ logger.Debugf(LOG_PREFIX, messages.SMTPMailFrom, senderAddress)
+ self.fromAddress = senderAddress
+ return nil
+}
+
+func (self *session) Rcpt(recipientAddress string, recipientOptions *gosmtp.RcptOptions) error {
+ logger.Debugf(LOG_PREFIX, messages.SMTPRecipient, recipientAddress)
+ self.toAddresses = append(self.toAddresses, recipientAddress)
+ return nil
+}
+
+func (self *session) Data(messageReader io.Reader) error {
+ rawMessage, readError := io.ReadAll(messageReader)
+ if readError != nil {
+ return readError
+ }
+
+ 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
+ }
+
+ return nil
+}
+
+func (self *session) Reset() {
+ self.fromAddress = ""
+ self.toAddresses = nil
+}
+
+func (self *session) Logout() error {
+ return nil
+}
diff --git a/utils/smtp/storage.go b/utils/smtp/storage.go
new file mode 100644
index 0000000..b580d67
--- /dev/null
+++ b/utils/smtp/storage.go
@@ -0,0 +1,22 @@
+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
new file mode 100644
index 0000000..38c6504
--- /dev/null
+++ b/utils/smtp/types.go
@@ -0,0 +1,13 @@
+package smtp
+
+import gosmtp "github.com/emersion/go-smtp"
+
+type session struct {
+ fromAddress string
+ toAddresses []string
+}
+
+type serverInstance struct {
+ server *gosmtp.Server
+ label string
+}