mirror of
https://github.com/axllent/mailpit.git
synced 2025-12-07 23:23:27 +02:00
Feature: Option to use rqlite database storage (#254)
This commit is contained in:
4
.github/workflows/tests.yml
vendored
4
.github/workflows/tests.yml
vendored
@@ -26,8 +26,8 @@ jobs:
|
|||||||
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
${{ runner.os }}-go-
|
${{ runner.os }}-go-
|
||||||
- run: go test ./internal/storage ./server ./internal/tools ./internal/html2text -v
|
- run: go test -p 1 ./internal/storage ./server ./internal/tools ./internal/html2text -v
|
||||||
- run: go test ./internal/storage ./internal/html2text -bench=.
|
- run: go test -p 1 ./internal/storage ./internal/html2text -bench=.
|
||||||
|
|
||||||
# build the assets
|
# build the assets
|
||||||
- name: Build web UI
|
- name: Build web UI
|
||||||
|
|||||||
1
go.mod
1
go.mod
@@ -16,6 +16,7 @@ require (
|
|||||||
github.com/lithammer/shortuuid/v4 v4.0.0
|
github.com/lithammer/shortuuid/v4 v4.0.0
|
||||||
github.com/mhale/smtpd v0.8.2
|
github.com/mhale/smtpd v0.8.2
|
||||||
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
|
github.com/reiver/go-telnet v0.0.0-20180421082511-9ff0b2ab096e
|
||||||
|
github.com/rqlite/gorqlite v0.0.0-20240227123050-397b03f02418
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/spf13/cobra v1.8.0
|
github.com/spf13/cobra v1.8.0
|
||||||
github.com/spf13/pflag v1.0.5
|
github.com/spf13/pflag v1.0.5
|
||||||
|
|||||||
2
go.sum
2
go.sum
@@ -112,6 +112,8 @@ github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
|
||||||
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rqlite/gorqlite v0.0.0-20240227123050-397b03f02418 h1:gYUQqzapdN4PQF5j0zDFI9ANQVAVFoJivNp5bTZEZMo=
|
||||||
|
github.com/rqlite/gorqlite v0.0.0-20240227123050-397b03f02418/go.mod h1:xF/KoXmrRyahPfo5L7Szb5cAAUl53dMWBh9cMruGEZg=
|
||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
package stats
|
package stats
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -21,10 +20,10 @@ var (
|
|||||||
|
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
|
||||||
smtpAccepted int
|
smtpAccepted float64
|
||||||
smtpAcceptedSize int
|
smtpAcceptedSize float64
|
||||||
smtpRejected int
|
smtpRejected float64
|
||||||
smtpIgnored int
|
smtpIgnored float64
|
||||||
)
|
)
|
||||||
|
|
||||||
// AppInformation struct
|
// AppInformation struct
|
||||||
@@ -37,29 +36,29 @@ type AppInformation struct {
|
|||||||
// Database path
|
// Database path
|
||||||
Database string
|
Database string
|
||||||
// Database size in bytes
|
// Database size in bytes
|
||||||
DatabaseSize int64
|
DatabaseSize float64
|
||||||
// Total number of messages in the database
|
// Total number of messages in the database
|
||||||
Messages int
|
Messages float64
|
||||||
// Total number of messages in the database
|
// Total number of messages in the database
|
||||||
Unread int
|
Unread float64
|
||||||
// Tags and message totals per tag
|
// Tags and message totals per tag
|
||||||
Tags map[string]int64
|
Tags map[string]int64
|
||||||
// Runtime statistics
|
// Runtime statistics
|
||||||
RuntimeStats struct {
|
RuntimeStats struct {
|
||||||
// Mailpit server uptime in seconds
|
// Mailpit server uptime in seconds
|
||||||
Uptime int
|
Uptime float64
|
||||||
// Current memory usage in bytes
|
// Current memory usage in bytes
|
||||||
Memory uint64
|
Memory uint64
|
||||||
// Database runtime messages deleted
|
// Database runtime messages deleted
|
||||||
MessagesDeleted int
|
MessagesDeleted float64
|
||||||
// Accepted runtime SMTP messages
|
// Accepted runtime SMTP messages
|
||||||
SMTPAccepted int
|
SMTPAccepted float64
|
||||||
// Total runtime accepted messages size in bytes
|
// Total runtime accepted messages size in bytes
|
||||||
SMTPAcceptedSize int
|
SMTPAcceptedSize float64
|
||||||
// Rejected runtime SMTP messages
|
// Rejected runtime SMTP messages
|
||||||
SMTPRejected int
|
SMTPRejected float64
|
||||||
// Ignored runtime SMTP messages (when using --ignore-duplicate-ids)
|
// Ignored runtime SMTP messages (when using --ignore-duplicate-ids)
|
||||||
SMTPIgnored int
|
SMTPIgnored float64
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -72,8 +71,7 @@ func Load() AppInformation {
|
|||||||
runtime.ReadMemStats(&m)
|
runtime.ReadMemStats(&m)
|
||||||
|
|
||||||
info.RuntimeStats.Memory = m.Sys - m.HeapReleased
|
info.RuntimeStats.Memory = m.Sys - m.HeapReleased
|
||||||
|
info.RuntimeStats.Uptime = time.Since(startedAt).Seconds()
|
||||||
info.RuntimeStats.Uptime = int(time.Since(startedAt).Seconds())
|
|
||||||
info.RuntimeStats.MessagesDeleted = storage.StatsDeleted
|
info.RuntimeStats.MessagesDeleted = storage.StatsDeleted
|
||||||
info.RuntimeStats.SMTPAccepted = smtpAccepted
|
info.RuntimeStats.SMTPAccepted = smtpAccepted
|
||||||
info.RuntimeStats.SMTPAcceptedSize = smtpAcceptedSize
|
info.RuntimeStats.SMTPAcceptedSize = smtpAcceptedSize
|
||||||
@@ -97,15 +95,9 @@ func Load() AppInformation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
info.Database = config.DataFile
|
info.Database = config.DataFile
|
||||||
|
info.DatabaseSize = storage.DbSize()
|
||||||
db, err := os.Stat(info.Database)
|
|
||||||
if err == nil {
|
|
||||||
info.DatabaseSize = db.Size()
|
|
||||||
}
|
|
||||||
|
|
||||||
info.Messages = storage.CountTotal()
|
info.Messages = storage.CountTotal()
|
||||||
info.Unread = storage.CountUnread()
|
info.Unread = storage.CountUnread()
|
||||||
|
|
||||||
info.Tags = storage.GetAllTagsCount()
|
info.Tags = storage.GetAllTagsCount()
|
||||||
|
|
||||||
return info
|
return info
|
||||||
@@ -120,7 +112,7 @@ func Track() {
|
|||||||
func LogSMTPAccepted(size int) {
|
func LogSMTPAccepted(size int) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
smtpAccepted = smtpAccepted + 1
|
smtpAccepted = smtpAccepted + 1
|
||||||
smtpAcceptedSize = smtpAcceptedSize + size
|
smtpAcceptedSize = smtpAcceptedSize + float64(size)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ func dbCron() {
|
|||||||
|
|
||||||
if deletedSize > 0 {
|
if deletedSize > 0 {
|
||||||
total := totalMessagesSize()
|
total := totalMessagesSize()
|
||||||
var deletedPercent int64
|
var deletedPercent float64
|
||||||
if total == 0 {
|
if total == 0 {
|
||||||
deletedPercent = 100
|
deletedPercent = 100
|
||||||
} else {
|
} else {
|
||||||
@@ -35,7 +35,7 @@ func dbCron() {
|
|||||||
}
|
}
|
||||||
// only vacuum the DB if at least 1% of mail storage size has been deleted
|
// only vacuum the DB if at least 1% of mail storage size has been deleted
|
||||||
if deletedPercent >= 1 {
|
if deletedPercent >= 1 {
|
||||||
logger.Log().Debugf("[db] deleted messages is %d%% of total size, reclaim space", deletedPercent)
|
logger.Log().Debugf("[db] deleted messages is %f%% of total size, reclaim space", deletedPercent)
|
||||||
vacuumDb()
|
vacuumDb()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,8 +62,8 @@ func pruneMessages() {
|
|||||||
|
|
||||||
ids := []string{}
|
ids := []string{}
|
||||||
var prunedSize int64
|
var prunedSize int64
|
||||||
var size int
|
var size float64
|
||||||
if err := q.Query(context.TODO(), db, func(row *sql.Rows) {
|
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
||||||
var id string
|
var id string
|
||||||
|
|
||||||
if err := row.Scan(&id, &size); err != nil {
|
if err := row.Scan(&id, &size); err != nil {
|
||||||
@@ -93,19 +93,19 @@ func pruneMessages() {
|
|||||||
args[i] = id
|
args[i] = id
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.Query(`DELETE FROM mailbox WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
|
_, err = tx.Exec(`DELETE FROM mailbox_data WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Log().Errorf("[db] %s", err.Error())
|
logger.Log().Errorf("[db] %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.Query(`DELETE FROM mailbox_data WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
|
_, err = tx.Exec(`DELETE FROM message_tags WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Log().Errorf("[db] %s", err.Error())
|
logger.Log().Errorf("[db] %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.Query(`DELETE FROM message_tags WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
|
_, err = tx.Exec(`DELETE FROM mailbox WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Log().Errorf("[db] %s", err.Error())
|
logger.Log().Errorf("[db] %s", err.Error())
|
||||||
return
|
return
|
||||||
@@ -137,6 +137,11 @@ func pruneMessages() {
|
|||||||
|
|
||||||
// Vacuum the database to reclaim space from deleted messages
|
// Vacuum the database to reclaim space from deleted messages
|
||||||
func vacuumDb() {
|
func vacuumDb() {
|
||||||
|
if sqlDriver == "rqlite" {
|
||||||
|
// let rqlite handle vacuuming
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
// set WAL file checkpoint
|
// set WAL file checkpoint
|
||||||
@@ -147,7 +152,7 @@ func vacuumDb() {
|
|||||||
|
|
||||||
// vacuum database
|
// vacuum database
|
||||||
if _, err := db.Exec("VACUUM"); err != nil {
|
if _, err := db.Exec("VACUUM"); err != nil {
|
||||||
logger.Log().Errorf("[db] %s", err.Error())
|
logger.Log().Errorf("[db] VACUUM: %s", err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,5 +167,5 @@ func vacuumDb() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
logger.Log().Debugf("[db] vacuumed database in %s", elapsed)
|
logger.Log().Debugf("[db] vacuum completed in %s", elapsed)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"os/signal"
|
"os/signal"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -17,14 +18,18 @@ import (
|
|||||||
"github.com/klauspost/compress/zstd"
|
"github.com/klauspost/compress/zstd"
|
||||||
"github.com/leporo/sqlf"
|
"github.com/leporo/sqlf"
|
||||||
|
|
||||||
// sqlite (native) - https://gitlab.com/cznic/sqlite
|
// sqlite - https://gitlab.com/cznic/sqlite
|
||||||
_ "modernc.org/sqlite"
|
_ "modernc.org/sqlite"
|
||||||
|
|
||||||
|
// rqlite - https://github.com/rqlite/gorqlite | https://rqlite.io/
|
||||||
|
_ "github.com/rqlite/gorqlite/stdlib"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
db *sql.DB
|
db *sql.DB
|
||||||
dbFile string
|
dbFile string
|
||||||
dbIsTemp bool
|
dbIsTemp bool
|
||||||
|
sqlDriver string
|
||||||
dbLastAction time.Time
|
dbLastAction time.Time
|
||||||
|
|
||||||
// zstd compression encoder & decoder
|
// zstd compression encoder & decoder
|
||||||
@@ -35,38 +40,55 @@ var (
|
|||||||
// InitDB will initialise the database
|
// InitDB will initialise the database
|
||||||
func InitDB() error {
|
func InitDB() error {
|
||||||
p := config.DataFile
|
p := config.DataFile
|
||||||
|
var dsn string
|
||||||
|
|
||||||
if p == "" {
|
if p == "" {
|
||||||
// when no path is provided then we create a temporary file
|
// when no path is provided then we create a temporary file
|
||||||
// which will get deleted on Close(), SIGINT or SIGTERM
|
// which will get deleted on Close(), SIGINT or SIGTERM
|
||||||
p = fmt.Sprintf("%s-%d.db", path.Join(os.TempDir(), "mailpit"), time.Now().UnixNano())
|
p = fmt.Sprintf("%s-%d.db", path.Join(os.TempDir(), "mailpit"), time.Now().UnixNano())
|
||||||
dbIsTemp = true
|
dbIsTemp = true
|
||||||
|
sqlDriver = "sqlite"
|
||||||
logger.Log().Debugf("[db] using temporary database: %s", p)
|
logger.Log().Debugf("[db] using temporary database: %s", p)
|
||||||
|
} else if strings.HasPrefix(p, "http://") || strings.HasPrefix(p, "https://") {
|
||||||
|
sqlDriver = "rqlite"
|
||||||
|
dsn = p
|
||||||
|
logger.Log().Debugf("[db] opening rqlite database %s", p)
|
||||||
} else {
|
} else {
|
||||||
p = filepath.Clean(p)
|
p = filepath.Clean(p)
|
||||||
|
sqlDriver = "sqlite"
|
||||||
|
dsn = fmt.Sprintf("file:%s?cache=shared", p)
|
||||||
|
logger.Log().Debugf("[db] opening database %s", p)
|
||||||
}
|
}
|
||||||
|
|
||||||
config.DataFile = p
|
config.DataFile = p
|
||||||
|
|
||||||
logger.Log().Debugf("[db] opening database %s", p)
|
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
dsn := fmt.Sprintf("file:%s?cache=shared", p)
|
db, err = sql.Open(sqlDriver, dsn)
|
||||||
|
|
||||||
db, err = sql.Open("sqlite", dsn)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for i := 1; i < 6; i++ {
|
||||||
|
if err := Ping(); err != nil {
|
||||||
|
logger.Log().Errorf("[db] %s", err.Error())
|
||||||
|
logger.Log().Infof("[db] reconnecting in 5 seconds (%d/5)", i)
|
||||||
|
time.Sleep(5 * time.Second)
|
||||||
|
} else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// prevent "database locked" errors
|
// prevent "database locked" errors
|
||||||
// @see https://github.com/mattn/go-sqlite3#faq
|
// @see https://github.com/mattn/go-sqlite3#faq
|
||||||
db.SetMaxOpenConns(1)
|
db.SetMaxOpenConns(1)
|
||||||
|
|
||||||
// SQLite performance tuning (https://phiresky.github.io/blog/2020/sqlite-performance-tuning/)
|
if sqlDriver == "sqlite" {
|
||||||
_, err = db.Exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = normal;")
|
// SQLite performance tuning (https://phiresky.github.io/blog/2020/sqlite-performance-tuning/)
|
||||||
if err != nil {
|
_, err = db.Exec("PRAGMA journal_mode = WAL; PRAGMA synchronous = normal;")
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// create tables if necessary & apply migrations
|
// create tables if necessary & apply migrations
|
||||||
@@ -138,8 +160,8 @@ func StatsGet() MailboxStats {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CountTotal returns the number of emails in the database
|
// CountTotal returns the number of emails in the database
|
||||||
func CountTotal() int {
|
func CountTotal() float64 {
|
||||||
var total int
|
var total float64
|
||||||
|
|
||||||
_ = sqlf.From("mailbox").
|
_ = sqlf.From("mailbox").
|
||||||
Select("COUNT(*)").To(&total).
|
Select("COUNT(*)").To(&total).
|
||||||
@@ -149,8 +171,8 @@ func CountTotal() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CountUnread returns the number of emails in the database that are unread.
|
// CountUnread returns the number of emails in the database that are unread.
|
||||||
func CountUnread() int {
|
func CountUnread() float64 {
|
||||||
var total int
|
var total float64
|
||||||
|
|
||||||
_ = sqlf.From("mailbox").
|
_ = sqlf.From("mailbox").
|
||||||
Select("COUNT(*)").To(&total).
|
Select("COUNT(*)").To(&total).
|
||||||
@@ -161,8 +183,8 @@ func CountUnread() int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CountRead returns the number of emails in the database that are read.
|
// CountRead returns the number of emails in the database that are read.
|
||||||
func CountRead() int {
|
func CountRead() float64 {
|
||||||
var total int
|
var total float64
|
||||||
|
|
||||||
_ = sqlf.From("mailbox").
|
_ = sqlf.From("mailbox").
|
||||||
Select("COUNT(*)").To(&total).
|
Select("COUNT(*)").To(&total).
|
||||||
@@ -172,6 +194,20 @@ func CountRead() int {
|
|||||||
return total
|
return total
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DbSize returns the size of the SQLite database.
|
||||||
|
func DbSize() float64 {
|
||||||
|
var total sql.NullFloat64
|
||||||
|
|
||||||
|
err := db.QueryRow("SELECT page_count * page_size AS size FROM pragma_page_count(), pragma_page_size()").Scan(&total)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Log().Errorf("[db] %s", err.Error())
|
||||||
|
return total.Float64
|
||||||
|
}
|
||||||
|
|
||||||
|
return total.Float64
|
||||||
|
}
|
||||||
|
|
||||||
// IsUnread returns whether a message is unread or not.
|
// IsUnread returns whether a message is unread or not.
|
||||||
func IsUnread(id string) bool {
|
func IsUnread(id string) bool {
|
||||||
var unread int
|
var unread int
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -88,21 +90,22 @@ func Store(body *[]byte) (string, error) {
|
|||||||
defer tx.Rollback()
|
defer tx.Rollback()
|
||||||
|
|
||||||
subject := env.GetHeader("Subject")
|
subject := env.GetHeader("Subject")
|
||||||
size := len(*body)
|
size := float64(len(*body))
|
||||||
inline := len(env.Inlines)
|
inline := len(env.Inlines)
|
||||||
attachments := len(env.Attachments)
|
attachments := len(env.Attachments)
|
||||||
snippet := tools.CreateSnippet(env.Text, env.HTML)
|
snippet := tools.CreateSnippet(env.Text, env.HTML)
|
||||||
|
|
||||||
// insert mail summary data
|
// insert mail summary data
|
||||||
_, err = tx.Exec("INSERT INTO mailbox(Created, ID, MessageID, Subject, Metadata, Size, Inline, Attachments, SearchText, Read, Snippet) values(?,?,?,?,?,?,?,?,?,0,?)",
|
_, err = tx.Exec("INSERT INTO mailbox(Created, ID, MessageID, Subject, Metadata, Size, Inline, Attachments, SearchText, Read, Snippet) VALUES(?,?,?,?,?,?,?,?,?,0,?)",
|
||||||
created.UnixMilli(), id, messageID, subject, string(summaryJSON), size, inline, attachments, searchText, snippet)
|
created.UnixMilli(), id, messageID, subject, string(summaryJSON), size, inline, attachments, searchText, snippet)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
// insert compressed raw message
|
// insert compressed raw message
|
||||||
compressed := dbEncoder.EncodeAll(*body, make([]byte, 0, size))
|
encoded := dbEncoder.EncodeAll(*body, make([]byte, 0, int(size)))
|
||||||
_, err = tx.Exec("INSERT INTO mailbox_data(ID, Email) values(?,?)", id, string(compressed))
|
hexStr := hex.EncodeToString(encoded)
|
||||||
|
_, err = tx.Exec("INSERT INTO mailbox_data(ID, Email) VALUES(?, x'"+hexStr+"')", id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@@ -155,12 +158,12 @@ func List(start, limit int) ([]MessageSummary, error) {
|
|||||||
Offset(start)
|
Offset(start)
|
||||||
|
|
||||||
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
||||||
var created int64
|
var created float64
|
||||||
var id string
|
var id string
|
||||||
var messageID string
|
var messageID string
|
||||||
var subject string
|
var subject string
|
||||||
var metadata string
|
var metadata string
|
||||||
var size int
|
var size float64
|
||||||
var attachments int
|
var attachments int
|
||||||
var read int
|
var read int
|
||||||
var snippet string
|
var snippet string
|
||||||
@@ -176,7 +179,7 @@ func List(start, limit int) ([]MessageSummary, error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
em.Created = time.UnixMilli(created)
|
em.Created = time.UnixMilli(int64(created))
|
||||||
em.ID = id
|
em.ID = id
|
||||||
em.MessageID = messageID
|
em.MessageID = messageID
|
||||||
em.Subject = subject
|
em.Subject = subject
|
||||||
@@ -246,7 +249,7 @@ func GetMessage(id string) (*Message, error) {
|
|||||||
Where(`ID = ?`, id)
|
Where(`ID = ?`, id)
|
||||||
|
|
||||||
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
||||||
var created int64
|
var created float64
|
||||||
|
|
||||||
if err := row.Scan(&created); err != nil {
|
if err := row.Scan(&created); err != nil {
|
||||||
logger.Log().Errorf("[db] %s", err.Error())
|
logger.Log().Errorf("[db] %s", err.Error())
|
||||||
@@ -255,7 +258,7 @@ func GetMessage(id string) (*Message, error) {
|
|||||||
|
|
||||||
logger.Log().Debugf("[db] %s does not contain a date header, using received datetime", id)
|
logger.Log().Debugf("[db] %s does not contain a date header, using received datetime", id)
|
||||||
|
|
||||||
date = time.UnixMilli(created)
|
date = time.UnixMilli(int64(created))
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
logger.Log().Errorf("[db] %s", err.Error())
|
logger.Log().Errorf("[db] %s", err.Error())
|
||||||
}
|
}
|
||||||
@@ -273,7 +276,7 @@ func GetMessage(id string) (*Message, error) {
|
|||||||
ReturnPath: returnPath,
|
ReturnPath: returnPath,
|
||||||
Subject: env.GetHeader("Subject"),
|
Subject: env.GetHeader("Subject"),
|
||||||
Tags: getMessageTags(id),
|
Tags: getMessageTags(id),
|
||||||
Size: len(raw),
|
Size: float64(len(raw)),
|
||||||
Text: env.Text,
|
Text: env.Text,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +334,6 @@ func GetMessageRaw(id string) ([]byte, error) {
|
|||||||
Select(`ID`).To(&i).
|
Select(`ID`).To(&i).
|
||||||
Select(`Email`).To(&msg).
|
Select(`Email`).To(&msg).
|
||||||
Where(`ID = ?`, id)
|
Where(`ID = ?`, id)
|
||||||
|
|
||||||
err := q.QueryRowAndClose(context.Background(), db)
|
err := q.QueryRowAndClose(context.Background(), db)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -341,7 +343,17 @@ func GetMessageRaw(id string) ([]byte, error) {
|
|||||||
return nil, errors.New("message not found")
|
return nil, errors.New("message not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
raw, err := dbDecoder.DecodeAll([]byte(msg), nil)
|
var data []byte
|
||||||
|
if sqlDriver == "rqlite" {
|
||||||
|
data, err = base64.StdEncoding.DecodeString(msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error decoding base64 message: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
data = []byte(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
raw, err := dbDecoder.DecodeAll(data, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error decompressing message: %s", err.Error())
|
return nil, fmt.Errorf("error decompressing message: %s", err.Error())
|
||||||
}
|
}
|
||||||
@@ -451,7 +463,7 @@ func MarkAllRead() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
logger.Log().Debugf("[db] marked %d messages as read in %s", total, elapsed)
|
logger.Log().Debugf("[db] marked %v messages as read in %s", total, elapsed)
|
||||||
|
|
||||||
BroadcastMailboxStats()
|
BroadcastMailboxStats()
|
||||||
|
|
||||||
@@ -476,7 +488,7 @@ func MarkAllUnread() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
elapsed := time.Since(start)
|
elapsed := time.Since(start)
|
||||||
logger.Log().Debugf("[db] marked %d messages as unread in %s", total, elapsed)
|
logger.Log().Debugf("[db] marked %v messages as unread in %s", total, elapsed)
|
||||||
|
|
||||||
BroadcastMailboxStats()
|
BroadcastMailboxStats()
|
||||||
|
|
||||||
@@ -507,52 +519,92 @@ func MarkUnread(id string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteOneMessage will delete a single message from a mailbox
|
// DeleteMessages deletes one or more messages in bulk
|
||||||
func DeleteOneMessage(id string) error {
|
func DeleteMessages(ids []string) error {
|
||||||
m, err := GetMessageRaw(id)
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
start := time.Now()
|
||||||
|
|
||||||
|
args := make([]interface{}, len(ids))
|
||||||
|
for i, id := range ids {
|
||||||
|
args[i] = id
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := db.Query(`SELECT ID, Size FROM mailbox WHERE ID IN (?`+strings.Repeat(",?", len(args)-1)+`)`, args...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
toDelete := []string{}
|
||||||
|
var totalSize float64
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var id string
|
||||||
|
var size float64
|
||||||
|
if err := rows.Scan(&id, &size); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
toDelete = append(toDelete, id)
|
||||||
|
totalSize = totalSize + size
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = rows.Err(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(toDelete) == 0 {
|
||||||
|
return nil // nothing to delete
|
||||||
|
}
|
||||||
|
|
||||||
size := len(m)
|
|
||||||
// begin a transaction to ensure both the message
|
|
||||||
// and data are deleted successfully
|
|
||||||
tx, err := db.BeginTx(context.Background(), nil)
|
tx, err := db.BeginTx(context.Background(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// roll back if it fails
|
args = make([]interface{}, len(toDelete))
|
||||||
defer tx.Rollback()
|
for i, id := range toDelete {
|
||||||
|
args[i] = id
|
||||||
|
}
|
||||||
|
|
||||||
_, err = tx.Exec("DELETE FROM mailbox WHERE ID = ?", id)
|
_, err = tx.Exec(`DELETE FROM mailbox WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = tx.Exec("DELETE FROM mailbox_data WHERE ID = ?", id)
|
_, err = tx.Exec(`DELETE FROM mailbox_data WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = tx.Exec(`DELETE FROM message_tags WHERE ID IN (?`+strings.Repeat(",?", len(ids)-1)+`)`, args...) // #nosec
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = tx.Commit()
|
err = tx.Commit()
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
logger.Log().Debugf("[db] deleted message %s", id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := DeleteAllMessageTags(id); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
dbLastAction = time.Now()
|
dbLastAction = time.Now()
|
||||||
addDeletedSize(int64(size))
|
addDeletedSize(int64(totalSize))
|
||||||
|
|
||||||
logMessagesDeleted(1)
|
logMessagesDeleted(len(toDelete))
|
||||||
|
|
||||||
|
pruneUnusedTags()
|
||||||
|
|
||||||
|
elapsed := time.Since(start)
|
||||||
|
|
||||||
|
messages := "messages"
|
||||||
|
if len(toDelete) == 1 {
|
||||||
|
messages = "message"
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Log().Debugf("[db] deleted %d %s in %s", len(toDelete), messages, elapsed)
|
||||||
|
|
||||||
BroadcastMailboxStats()
|
BroadcastMailboxStats()
|
||||||
|
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteAllMessages will delete all messages from a mailbox
|
// DeleteAllMessages will delete all messages from a mailbox
|
||||||
|
|||||||
@@ -13,8 +13,6 @@ func TestTextEmailInserts(t *testing.T) {
|
|||||||
|
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
|
|
||||||
assertEqualStats(t, 0, 0)
|
|
||||||
|
|
||||||
for i := 0; i < testRuns; i++ {
|
for i := 0; i < testRuns; i++ {
|
||||||
if _, err := Store(&testTextEmail); err != nil {
|
if _, err := Store(&testTextEmail); err != nil {
|
||||||
t.Log("error ", err)
|
t.Log("error ", err)
|
||||||
@@ -22,19 +20,17 @@ func TestTextEmailInserts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEqual(t, CountTotal(), testRuns, "Incorrect number of text emails stored")
|
assertEqual(t, CountTotal(), float64(testRuns), "Incorrect number of text emails stored")
|
||||||
|
|
||||||
t.Logf("Inserted %d text emails in %s", testRuns, time.Since(start))
|
t.Logf("Inserted %d text emails in %s", testRuns, time.Since(start))
|
||||||
|
|
||||||
assertEqualStats(t, testRuns, testRuns)
|
|
||||||
|
|
||||||
delStart := time.Now()
|
delStart := time.Now()
|
||||||
if err := DeleteAllMessages(); err != nil {
|
if err := DeleteAllMessages(); err != nil {
|
||||||
t.Log("error ", err)
|
t.Log("error ", err)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEqual(t, CountTotal(), 0, "incorrect number of text emails deleted")
|
assertEqual(t, CountTotal(), float64(0), "incorrect number of text emails deleted")
|
||||||
|
|
||||||
t.Logf("deleted %d text emails in %s", testRuns, time.Since(delStart))
|
t.Logf("deleted %d text emails in %s", testRuns, time.Since(delStart))
|
||||||
|
|
||||||
@@ -56,19 +52,17 @@ func TestMimeEmailInserts(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEqual(t, CountTotal(), testRuns, "Incorrect number of mime emails stored")
|
assertEqual(t, CountTotal(), float64(testRuns), "Incorrect number of mime emails stored")
|
||||||
|
|
||||||
t.Logf("Inserted %d text emails in %s", testRuns, time.Since(start))
|
t.Logf("Inserted %d text emails in %s", testRuns, time.Since(start))
|
||||||
|
|
||||||
assertEqualStats(t, testRuns, testRuns)
|
|
||||||
|
|
||||||
delStart := time.Now()
|
delStart := time.Now()
|
||||||
if err := DeleteAllMessages(); err != nil {
|
if err := DeleteAllMessages(); err != nil {
|
||||||
t.Log("error ", err)
|
t.Log("error ", err)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEqual(t, CountTotal(), 0, "incorrect number of mime emails deleted")
|
assertEqual(t, CountTotal(), float64(0), "incorrect number of mime emails deleted")
|
||||||
|
|
||||||
t.Logf("Deleted %d mime emails in %s", testRuns, time.Since(delStart))
|
t.Logf("Deleted %d mime emails in %s", testRuns, time.Since(delStart))
|
||||||
}
|
}
|
||||||
@@ -107,14 +101,14 @@ func TestRetrieveMimeEmail(t *testing.T) {
|
|||||||
t.Log("error ", err)
|
t.Log("error ", err)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
assertEqual(t, len(attachmentData.Content), msg.Attachments[0].Size, "attachment size does not match")
|
assertEqual(t, float64(len(attachmentData.Content)), msg.Attachments[0].Size, "attachment size does not match")
|
||||||
|
|
||||||
inlineData, err := GetAttachmentPart(id, msg.Inline[0].PartID)
|
inlineData, err := GetAttachmentPart(id, msg.Inline[0].PartID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Log("error ", err)
|
t.Log("error ", err)
|
||||||
t.Fail()
|
t.Fail()
|
||||||
}
|
}
|
||||||
assertEqual(t, len(inlineData.Content), msg.Inline[0].Size, "inline attachment size does not match")
|
assertEqual(t, float64(len(inlineData.Content)), msg.Inline[0].Size, "inline attachment size does not match")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMessageSummary(t *testing.T) {
|
func TestMessageSummary(t *testing.T) {
|
||||||
|
|||||||
@@ -24,8 +24,8 @@ func BroadcastMailboxStats() {
|
|||||||
time.Sleep(250 * time.Millisecond)
|
time.Sleep(250 * time.Millisecond)
|
||||||
bcStatsDelay = false
|
bcStatsDelay = false
|
||||||
b := struct {
|
b := struct {
|
||||||
Total int
|
Total float64
|
||||||
Unread int
|
Unread float64
|
||||||
Version string
|
Version string
|
||||||
}{
|
}{
|
||||||
Total: CountTotal(),
|
Total: CountTotal(),
|
||||||
|
|||||||
@@ -30,12 +30,12 @@ func Search(search string, start, limit int) ([]MessageSummary, int, error) {
|
|||||||
var err error
|
var err error
|
||||||
|
|
||||||
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
||||||
var created int64
|
var created float64
|
||||||
var id string
|
var id string
|
||||||
var messageID string
|
var messageID string
|
||||||
var subject string
|
var subject string
|
||||||
var metadata string
|
var metadata string
|
||||||
var size int
|
var size float64
|
||||||
var attachments int
|
var attachments int
|
||||||
var snippet string
|
var snippet string
|
||||||
var read int
|
var read int
|
||||||
@@ -52,7 +52,7 @@ func Search(search string, start, limit int) ([]MessageSummary, int, error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
em.Created = time.UnixMilli(created)
|
em.Created = time.UnixMilli(int64(created))
|
||||||
em.ID = id
|
em.ID = id
|
||||||
em.MessageID = messageID
|
em.MessageID = messageID
|
||||||
em.Subject = subject
|
em.Subject = subject
|
||||||
@@ -99,17 +99,16 @@ func DeleteSearch(search string) error {
|
|||||||
q := searchQueryBuilder(search)
|
q := searchQueryBuilder(search)
|
||||||
|
|
||||||
ids := []string{}
|
ids := []string{}
|
||||||
deleteSize := 0
|
deleteSize := float64(0)
|
||||||
|
|
||||||
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
if err := q.QueryAndClose(context.TODO(), db, func(row *sql.Rows) {
|
||||||
var created int64
|
var created float64
|
||||||
var id string
|
var id string
|
||||||
var messageID string
|
var messageID string
|
||||||
var subject string
|
var subject string
|
||||||
var metadata string
|
var metadata string
|
||||||
var size int
|
var size float64
|
||||||
var attachments int
|
var attachments int
|
||||||
// var tags string
|
|
||||||
var read int
|
var read int
|
||||||
var snippet string
|
var snippet string
|
||||||
var ignore string
|
var ignore string
|
||||||
|
|||||||
@@ -35,8 +35,8 @@ func SettingPut(k, v string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The total deleted message size as an int64 value
|
// The total deleted message size as an int64 value
|
||||||
func getDeletedSize() int64 {
|
func getDeletedSize() float64 {
|
||||||
var result sql.NullInt64
|
var result sql.NullFloat64
|
||||||
err := sqlf.From("settings").
|
err := sqlf.From("settings").
|
||||||
Select("Value").To(&result).
|
Select("Value").To(&result).
|
||||||
Where("Key = ?", "DeletedSize").
|
Where("Key = ?", "DeletedSize").
|
||||||
@@ -47,12 +47,12 @@ func getDeletedSize() int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.Int64
|
return result.Float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// The total raw non-compressed messages size in bytes of all messages in the database
|
// The total raw non-compressed messages size in bytes of all messages in the database
|
||||||
func totalMessagesSize() int64 {
|
func totalMessagesSize() float64 {
|
||||||
var result sql.NullInt64
|
var result sql.NullFloat64
|
||||||
err := sqlf.From("mailbox").
|
err := sqlf.From("mailbox").
|
||||||
Select("SUM(Size)").To(&result).
|
Select("SUM(Size)").To(&result).
|
||||||
QueryAndClose(context.TODO(), db, func(row *sql.Rows) {})
|
QueryAndClose(context.TODO(), db, func(row *sql.Rows) {})
|
||||||
@@ -61,7 +61,7 @@ func totalMessagesSize() int64 {
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
return result.Int64
|
return result.Float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// AddDeletedSize will add the value to the DeletedSize setting
|
// AddDeletedSize will add the value to the DeletedSize setting
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ type Message struct {
|
|||||||
// Message body HTML
|
// Message body HTML
|
||||||
HTML string
|
HTML string
|
||||||
// Message size in bytes
|
// Message size in bytes
|
||||||
Size int
|
Size float64
|
||||||
// Inline message attachments
|
// Inline message attachments
|
||||||
Inline []Attachment
|
Inline []Attachment
|
||||||
// Message attachments
|
// Message attachments
|
||||||
@@ -61,7 +61,7 @@ type Attachment struct {
|
|||||||
// Content ID
|
// Content ID
|
||||||
ContentID string
|
ContentID string
|
||||||
// Size in bytes
|
// Size in bytes
|
||||||
Size int
|
Size float64
|
||||||
}
|
}
|
||||||
|
|
||||||
// MessageSummary struct for frontend messages
|
// MessageSummary struct for frontend messages
|
||||||
@@ -91,7 +91,7 @@ type MessageSummary struct {
|
|||||||
// Message tags
|
// Message tags
|
||||||
Tags []string
|
Tags []string
|
||||||
// Message size in bytes (total)
|
// Message size in bytes (total)
|
||||||
Size int
|
Size float64
|
||||||
// Whether the message has any attachments
|
// Whether the message has any attachments
|
||||||
Attachments int
|
Attachments int
|
||||||
// Message snippet includes up to 250 characters
|
// Message snippet includes up to 250 characters
|
||||||
@@ -100,8 +100,8 @@ type MessageSummary struct {
|
|||||||
|
|
||||||
// MailboxStats struct for quick mailbox total/read lookups
|
// MailboxStats struct for quick mailbox total/read lookups
|
||||||
type MailboxStats struct {
|
type MailboxStats struct {
|
||||||
Total int
|
Total float64
|
||||||
Unread int
|
Unread float64
|
||||||
Tags []string
|
Tags []string
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,7 +124,7 @@ func AttachmentSummary(a *enmime.Part) Attachment {
|
|||||||
}
|
}
|
||||||
o.ContentType = a.ContentType
|
o.ContentType = a.ContentType
|
||||||
o.ContentID = a.ContentID
|
o.ContentID = a.ContentID
|
||||||
o.Size = len(a.Content)
|
o.Size = float64(len(a.Content))
|
||||||
|
|
||||||
return o
|
return o
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,32 +92,13 @@ func AddMessageTag(id, name string) error {
|
|||||||
logger.Log().Debugf("[tags] adding tag \"%s\" to %s", name, id)
|
logger.Log().Debugf("[tags] adding tag \"%s\" to %s", name, id)
|
||||||
|
|
||||||
// tag dos not exist, add new one
|
// tag dos not exist, add new one
|
||||||
if err := sqlf.InsertInto("tags").
|
if _, err := sqlf.InsertInto("tags").
|
||||||
Set("Name", name).
|
Set("Name", name).
|
||||||
Returning("ID").To(&tagID).
|
|
||||||
QueryRowAndClose(context.TODO(), db); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// check message does not already have this tag
|
|
||||||
var count int
|
|
||||||
if _, err := sqlf.From("message_tags").
|
|
||||||
Select("COUNT(ID)").To(&count).
|
|
||||||
Where("ID = ?", id).
|
|
||||||
Where("TagID = ?", tagID).
|
|
||||||
ExecAndClose(context.TODO(), db); err != nil {
|
ExecAndClose(context.TODO(), db); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if count != 0 {
|
|
||||||
return nil // already exists
|
|
||||||
}
|
|
||||||
|
|
||||||
// add tag to message
|
return AddMessageTag(id, name)
|
||||||
_, err := sqlf.InsertInto("message_tags").
|
|
||||||
Set("ID", id).
|
|
||||||
Set("TagID", tagID).
|
|
||||||
ExecAndClose(context.TODO(), db)
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteMessageTag deleted a tag from a message
|
// DeleteMessageTag deleted a tag from a message
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ var (
|
|||||||
func setup() {
|
func setup() {
|
||||||
logger.NoLogging = true
|
logger.NoLogging = true
|
||||||
config.MaxMessages = 0
|
config.MaxMessages = 0
|
||||||
config.DataFile = ""
|
config.DataFile = os.Getenv("MP_DATA_FILE")
|
||||||
|
|
||||||
if err := InitDB(); err != nil {
|
if err := InitDB(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -27,6 +27,11 @@ func setup() {
|
|||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
|
||||||
|
// ensure DB is empty
|
||||||
|
if err := DeleteAllMessages(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
testTextEmail, err = os.ReadFile("testdata/plain-text.eml")
|
testTextEmail, err = os.ReadFile("testdata/plain-text.eml")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
@@ -53,11 +58,11 @@ func assertEqual(t *testing.T, a interface{}, b interface{}, message string) {
|
|||||||
|
|
||||||
func assertEqualStats(t *testing.T, total int, unread int) {
|
func assertEqualStats(t *testing.T, total int, unread int) {
|
||||||
s := StatsGet()
|
s := StatsGet()
|
||||||
if total != s.Total {
|
if float64(total) != s.Total {
|
||||||
t.Fatalf("Incorrect total mailbox stats: \"%d\" != \"%d\"", total, s.Total)
|
t.Fatalf("Incorrect total mailbox stats: \"%v\" != \"%v\"", total, s.Total)
|
||||||
}
|
}
|
||||||
|
|
||||||
if unread != s.Unread {
|
if float64(unread) != s.Unread {
|
||||||
t.Fatalf("Incorrect unread mailbox stats: \"%d\" != \"%d\"", unread, s.Unread)
|
t.Fatalf("Incorrect unread mailbox stats: \"%v\" != \"%v\"", unread, s.Unread)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ var (
|
|||||||
// for stats to prevent import cycle
|
// for stats to prevent import cycle
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
// StatsDeleted for counting the number of messages deleted
|
// StatsDeleted for counting the number of messages deleted
|
||||||
StatsDeleted int
|
StatsDeleted float64
|
||||||
)
|
)
|
||||||
|
|
||||||
// Return a header field as a []*mail.Address, or "null" is not found/empty
|
// Return a header field as a []*mail.Address, or "null" is not found/empty
|
||||||
@@ -73,7 +73,7 @@ func cleanString(str string) string {
|
|||||||
// LogMessagesDeleted logs the number of messages deleted
|
// LogMessagesDeleted logs the number of messages deleted
|
||||||
func logMessagesDeleted(n int) {
|
func logMessagesDeleted(n int) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
StatsDeleted = StatsDeleted + n
|
StatsDeleted = StatsDeleted + float64(n)
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ func GetMessages(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
res.Start = start
|
res.Start = start
|
||||||
res.Messages = messages
|
res.Messages = messages
|
||||||
res.Count = len(messages) // legacy - now undocumented in API specs
|
res.Count = float64(len(messages)) // legacy - now undocumented in API specs
|
||||||
res.Total = stats.Total
|
res.Total = stats.Total
|
||||||
res.Unread = stats.Unread
|
res.Unread = stats.Unread
|
||||||
res.Tags = stats.Tags
|
res.Tags = stats.Tags
|
||||||
@@ -133,9 +133,9 @@ func Search(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
res.Start = start
|
res.Start = start
|
||||||
res.Messages = messages
|
res.Messages = messages
|
||||||
res.Count = len(messages) // legacy - now undocumented in API specs
|
res.Count = float64(len(messages)) // legacy - now undocumented in API specs
|
||||||
res.Total = stats.Total // total messages in mailbox
|
res.Total = stats.Total // total messages in mailbox
|
||||||
res.MessagesCount = results
|
res.MessagesCount = float64(results)
|
||||||
res.Unread = stats.Unread
|
res.Unread = stats.Unread
|
||||||
res.Tags = stats.Tags
|
res.Tags = stats.Tags
|
||||||
|
|
||||||
@@ -337,7 +337,11 @@ func GetHeaders(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes, _ := json.Marshal(m.Header)
|
bytes, err := json.Marshal(m.Header)
|
||||||
|
if err != nil {
|
||||||
|
httpError(w, err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
w.Header().Add("Content-Type", "application/json")
|
w.Header().Add("Content-Type", "application/json")
|
||||||
_, _ = w.Write(bytes)
|
_, _ = w.Write(bytes)
|
||||||
@@ -428,11 +432,9 @@ func DeleteMessages(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for _, id := range data.IDs {
|
if err := storage.DeleteMessages(data.IDs); err != nil {
|
||||||
if err := storage.DeleteOneMessage(id); err != nil {
|
httpError(w, err.Error())
|
||||||
httpError(w, err.Error())
|
return
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,18 +10,18 @@ import (
|
|||||||
// MessagesSummary is a summary of a list of messages
|
// MessagesSummary is a summary of a list of messages
|
||||||
type MessagesSummary struct {
|
type MessagesSummary struct {
|
||||||
// Total number of messages in mailbox
|
// Total number of messages in mailbox
|
||||||
Total int `json:"total"`
|
Total float64 `json:"total"`
|
||||||
|
|
||||||
// Total number of unread messages in mailbox
|
// Total number of unread messages in mailbox
|
||||||
Unread int `json:"unread"`
|
Unread float64 `json:"unread"`
|
||||||
|
|
||||||
// Legacy - now undocumented in API specs but left for backwards compatibility.
|
// Legacy - now undocumented in API specs but left for backwards compatibility.
|
||||||
// Removed from API documentation 2023-07-12
|
// Removed from API documentation 2023-07-12
|
||||||
// swagger:ignore
|
// swagger:ignore
|
||||||
Count int `json:"count"`
|
Count float64 `json:"count"`
|
||||||
|
|
||||||
// Total number of messages matching current query
|
// Total number of messages matching current query
|
||||||
MessagesCount int `json:"messages_count"`
|
MessagesCount float64 `json:"messages_count"`
|
||||||
|
|
||||||
// Pagination offset
|
// Pagination offset
|
||||||
Start int `json:"start"`
|
Start int `json:"start"`
|
||||||
|
|||||||
@@ -78,11 +78,10 @@ func Run() {
|
|||||||
|
|
||||||
type message struct {
|
type message struct {
|
||||||
ID string
|
ID string
|
||||||
Size int
|
Size float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func handleClient(conn net.Conn) {
|
func handleClient(conn net.Conn) {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
user = ""
|
user = ""
|
||||||
state = 1
|
state = 1
|
||||||
@@ -92,7 +91,7 @@ func handleClient(conn net.Conn) {
|
|||||||
defer func() {
|
defer func() {
|
||||||
if state == UPDATE {
|
if state == UPDATE {
|
||||||
for _, id := range toDelete {
|
for _, id := range toDelete {
|
||||||
_ = storage.DeleteOneMessage(id)
|
_ = storage.DeleteMessages([]string{id})
|
||||||
}
|
}
|
||||||
if len(toDelete) > 0 {
|
if len(toDelete) > 0 {
|
||||||
// update web UI to remove deleted messages
|
// update web UI to remove deleted messages
|
||||||
@@ -178,19 +177,19 @@ func handleClient(conn net.Conn) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else if cmd == "STAT" && state == TRANSACTION {
|
} else if cmd == "STAT" && state == TRANSACTION {
|
||||||
totalSize := 0
|
totalSize := float64(0)
|
||||||
for _, m := range messages {
|
for _, m := range messages {
|
||||||
totalSize = totalSize + m.Size
|
totalSize = totalSize + m.Size
|
||||||
}
|
}
|
||||||
|
|
||||||
sendResponse(conn, fmt.Sprintf("+OK %d %d", len(messages), totalSize))
|
sendResponse(conn, fmt.Sprintf("+OK %d %d", len(messages), int64(totalSize)))
|
||||||
|
|
||||||
} else if cmd == "LIST" && state == TRANSACTION {
|
} else if cmd == "LIST" && state == TRANSACTION {
|
||||||
totalSize := 0
|
totalSize := float64(0)
|
||||||
for _, m := range messages {
|
for _, m := range messages {
|
||||||
totalSize = totalSize + m.Size
|
totalSize = totalSize + m.Size
|
||||||
}
|
}
|
||||||
sendData(conn, fmt.Sprintf("+OK %d messages (%d octets)", len(messages), totalSize))
|
sendData(conn, fmt.Sprintf("+OK %d messages (%d octets)", len(messages), int64(totalSize)))
|
||||||
|
|
||||||
// print all sizes
|
// print all sizes
|
||||||
for row, m := range messages {
|
for row, m := range messages {
|
||||||
@@ -200,7 +199,7 @@ func handleClient(conn net.Conn) {
|
|||||||
sendData(conn, ".")
|
sendData(conn, ".")
|
||||||
|
|
||||||
} else if cmd == "UIDL" && state == TRANSACTION {
|
} else if cmd == "UIDL" && state == TRANSACTION {
|
||||||
totalSize := 0
|
totalSize := float64(0)
|
||||||
for _, m := range messages {
|
for _, m := range messages {
|
||||||
totalSize = totalSize + m.Size
|
totalSize = totalSize + m.Size
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -204,11 +205,15 @@ func TestAPIv1Search(t *testing.T) {
|
|||||||
func setup() {
|
func setup() {
|
||||||
logger.NoLogging = true
|
logger.NoLogging = true
|
||||||
config.MaxMessages = 0
|
config.MaxMessages = 0
|
||||||
config.DataFile = ""
|
config.DataFile = os.Getenv("MP_DATA_FILE")
|
||||||
|
|
||||||
if err := storage.InitDB(); err != nil {
|
if err := storage.InitDB(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := storage.DeleteAllMessages(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertStatsEqual(t *testing.T, uri string, unread, total int) {
|
func assertStatsEqual(t *testing.T, uri string, unread, total int) {
|
||||||
@@ -225,8 +230,8 @@ func assertStatsEqual(t *testing.T, uri string, unread, total int) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEqual(t, unread, m.Unread, "wrong unread count")
|
assertEqual(t, float64(unread), m.Unread, "wrong unread count")
|
||||||
assertEqual(t, total, m.Total, "wrong total count")
|
assertEqual(t, float64(total), m.Total, "wrong total count")
|
||||||
}
|
}
|
||||||
|
|
||||||
func assertSearchEqual(t *testing.T, uri, query string, count int) {
|
func assertSearchEqual(t *testing.T, uri, query string, count int) {
|
||||||
@@ -246,7 +251,7 @@ func assertSearchEqual(t *testing.T, uri, query string, count int) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
assertEqual(t, count, m.MessagesCount, "wrong search results count")
|
assertEqual(t, float64(count), m.MessagesCount, "wrong search results count")
|
||||||
}
|
}
|
||||||
|
|
||||||
func insertEmailData(t *testing.T) {
|
func insertEmailData(t *testing.T) {
|
||||||
|
|||||||
@@ -773,8 +773,8 @@
|
|||||||
},
|
},
|
||||||
"DatabaseSize": {
|
"DatabaseSize": {
|
||||||
"description": "Database size in bytes",
|
"description": "Database size in bytes",
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"format": "int64"
|
"format": "double"
|
||||||
},
|
},
|
||||||
"LatestVersion": {
|
"LatestVersion": {
|
||||||
"description": "Latest Mailpit version",
|
"description": "Latest Mailpit version",
|
||||||
@@ -782,8 +782,8 @@
|
|||||||
},
|
},
|
||||||
"Messages": {
|
"Messages": {
|
||||||
"description": "Total number of messages in the database",
|
"description": "Total number of messages in the database",
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"format": "int64"
|
"format": "double"
|
||||||
},
|
},
|
||||||
"RuntimeStats": {
|
"RuntimeStats": {
|
||||||
"description": "Runtime statistics",
|
"description": "Runtime statistics",
|
||||||
@@ -796,33 +796,33 @@
|
|||||||
},
|
},
|
||||||
"MessagesDeleted": {
|
"MessagesDeleted": {
|
||||||
"description": "Database runtime messages deleted",
|
"description": "Database runtime messages deleted",
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"format": "int64"
|
"format": "double"
|
||||||
},
|
},
|
||||||
"SMTPAccepted": {
|
"SMTPAccepted": {
|
||||||
"description": "Accepted runtime SMTP messages",
|
"description": "Accepted runtime SMTP messages",
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"format": "int64"
|
"format": "double"
|
||||||
},
|
},
|
||||||
"SMTPAcceptedSize": {
|
"SMTPAcceptedSize": {
|
||||||
"description": "Total runtime accepted messages size in bytes",
|
"description": "Total runtime accepted messages size in bytes",
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"format": "int64"
|
"format": "double"
|
||||||
},
|
},
|
||||||
"SMTPIgnored": {
|
"SMTPIgnored": {
|
||||||
"description": "Ignored runtime SMTP messages (when using --ignore-duplicate-ids)",
|
"description": "Ignored runtime SMTP messages (when using --ignore-duplicate-ids)",
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"format": "int64"
|
"format": "double"
|
||||||
},
|
},
|
||||||
"SMTPRejected": {
|
"SMTPRejected": {
|
||||||
"description": "Rejected runtime SMTP messages",
|
"description": "Rejected runtime SMTP messages",
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"format": "int64"
|
"format": "double"
|
||||||
},
|
},
|
||||||
"Uptime": {
|
"Uptime": {
|
||||||
"description": "Mailpit server uptime in seconds",
|
"description": "Mailpit server uptime in seconds",
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"format": "int64"
|
"format": "double"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -836,8 +836,8 @@
|
|||||||
},
|
},
|
||||||
"Unread": {
|
"Unread": {
|
||||||
"description": "Total number of messages in the database",
|
"description": "Total number of messages in the database",
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"format": "int64"
|
"format": "double"
|
||||||
},
|
},
|
||||||
"Version": {
|
"Version": {
|
||||||
"description": "Current Mailpit version",
|
"description": "Current Mailpit version",
|
||||||
@@ -868,8 +868,8 @@
|
|||||||
},
|
},
|
||||||
"Size": {
|
"Size": {
|
||||||
"description": "Size in bytes",
|
"description": "Size in bytes",
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"format": "int64"
|
"format": "double"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"x-go-package": "github.com/axllent/mailpit/internal/storage"
|
"x-go-package": "github.com/axllent/mailpit/internal/storage"
|
||||||
@@ -1176,8 +1176,8 @@
|
|||||||
},
|
},
|
||||||
"Size": {
|
"Size": {
|
||||||
"description": "Message size in bytes",
|
"description": "Message size in bytes",
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"format": "int64"
|
"format": "double"
|
||||||
},
|
},
|
||||||
"Subject": {
|
"Subject": {
|
||||||
"description": "Message subject",
|
"description": "Message subject",
|
||||||
@@ -1268,8 +1268,8 @@
|
|||||||
},
|
},
|
||||||
"Size": {
|
"Size": {
|
||||||
"description": "Message size in bytes (total)",
|
"description": "Message size in bytes (total)",
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"format": "int64"
|
"format": "double"
|
||||||
},
|
},
|
||||||
"Snippet": {
|
"Snippet": {
|
||||||
"description": "Message snippet includes up to 250 characters",
|
"description": "Message snippet includes up to 250 characters",
|
||||||
@@ -1310,8 +1310,8 @@
|
|||||||
},
|
},
|
||||||
"messages_count": {
|
"messages_count": {
|
||||||
"description": "Total number of messages matching current query",
|
"description": "Total number of messages matching current query",
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"format": "int64",
|
"format": "double",
|
||||||
"x-go-name": "MessagesCount"
|
"x-go-name": "MessagesCount"
|
||||||
},
|
},
|
||||||
"start": {
|
"start": {
|
||||||
@@ -1330,14 +1330,14 @@
|
|||||||
},
|
},
|
||||||
"total": {
|
"total": {
|
||||||
"description": "Total number of messages in mailbox",
|
"description": "Total number of messages in mailbox",
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"format": "int64",
|
"format": "double",
|
||||||
"x-go-name": "Total"
|
"x-go-name": "Total"
|
||||||
},
|
},
|
||||||
"unread": {
|
"unread": {
|
||||||
"description": "Total number of unread messages in mailbox",
|
"description": "Total number of unread messages in mailbox",
|
||||||
"type": "integer",
|
"type": "number",
|
||||||
"format": "int64",
|
"format": "double",
|
||||||
"x-go-name": "Unread"
|
"x-go-name": "Unread"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user