1
0
mirror of https://github.com/axllent/mailpit.git synced 2024-12-28 23:06:43 +02:00

Merge branch 'feature/unread' into develop

This commit is contained in:
Ralph Slooten 2022-07-30 19:58:42 +12:00
commit ce23e0616e
7 changed files with 158 additions and 50 deletions

View File

@ -12,7 +12,7 @@ Mailpit is inspired by [MailHog](#why-rewrite-mailhog), but much, much faster.
- Runs completely on a single binary
- SMTP server (default `0.0.0.0:1025`)
- Web UI to view emails (HTML format, text, source and MIME attachments, default `0.0.0.0:8025`)
- Real-time web UI updates using websockets for new mail
- Real-time web UI updates using web sockets for new mail
- Email storage in either memory or disk (using [CloverDB](https://github.com/ostafen/clover)) - note that in-memory has a physical limit of 1MB per email
- Configurable automatic email pruning (default keeps the most recent 500 emails)
- Fast SMTP processing & storing - approximately 300-600 emails per second depending on CPU, network speed & email size

View File

@ -16,3 +16,9 @@ type WebsocketNotification struct {
Type string
Data interface{}
}
// MailboxStats struct for quick mailbox total/read lookups
type MailboxStats struct {
Total int
Unread int
}

View File

@ -12,10 +12,11 @@ import (
)
type messagesResult struct {
Total int `json:"total"`
Count int `json:"count"`
Start int `json:"start"`
Items []data.Summary `json:"items"`
Total int `json:"total"`
Unread int `json:"unread"`
Count int `json:"count"`
Start int `json:"start"`
Items []data.Summary `json:"items"`
}
// Return a list of available mailboxes
@ -49,18 +50,15 @@ func apiListMailbox(w http.ResponseWriter, r *http.Request) {
return
}
total, err := storage.Count(mailbox)
if err != nil {
httpError(w, err.Error())
return
}
stats := storage.StatsGet(mailbox)
var res messagesResult
res.Start = start
res.Items = messages
res.Count = len(res.Items)
res.Total = total
res.Total = stats.Total
res.Unread = stats.Unread
bytes, _ := json.Marshal(res)
w.Header().Add("Content-Type", "application/json")
@ -92,24 +90,15 @@ func apiSearchMailbox(w http.ResponseWriter, r *http.Request) {
return
}
total, err := storage.Count(mailbox)
if err != nil {
httpError(w, err.Error())
return
}
// total := limit
// count := len(messages)
// if total > count {
// total = count
// }
stats := storage.StatsGet(mailbox)
var res messagesResult
res.Start = start
res.Items = messages
res.Count = len(messages)
res.Total = total
res.Total = stats.Total
res.Unread = stats.Unread
bytes, _ := json.Marshal(res)
w.Header().Add("Content-Type", "application/json")

View File

@ -15,6 +15,7 @@ export default {
items: [],
limit: 50,
total: 0,
unread: 0,
start: 0,
search: "",
searching: false,
@ -71,6 +72,7 @@ export default {
self.get(uri, params, function(response){
self.total = response.data.total;
self.unread = response.data.unread;
self.count = response.data.count;
self.start = response.data.start;
self.items = response.data.items;
@ -119,7 +121,10 @@ export default {
self.get(uri, params, function(response) {
for (let i in self.items) {
if (self.items[i].ID == self.currentPath) {
self.items[i].Read = true;
if (!self.items[i].Read) {
self.items[i].Read = true;
self.unread--;
}
}
}
let d = response.data;
@ -208,6 +213,7 @@ export default {
}
}
self.total++;
self.unread++;
} else if (response.Type == "prune") {
// messages have been deleted, reload messages to adjust
self.scrollInPlace = true;
@ -323,8 +329,8 @@ export default {
<i class="bi bi-envelope me-1" v-if="isConnected"></i>
<i class="bi bi-arrow-clockwise me-1" v-else></i>
Inbox
<span class="position-absolute mt-2 ms-4 start-100 translate-middle badge rounded-pill text-bg-secondary" v-if="total">
{{ formatNumber(total) }}
<span class="position-absolute mt-2 ms-4 start-100 translate-middle badge rounded-pill text-bg-secondary" title="Unread messages" v-if="unread">
{{ formatNumber(unread) }}
</span>
</a>
</li>

View File

@ -8,6 +8,7 @@ import (
"os"
"os/signal"
"regexp"
"strings"
"syscall"
"time"
@ -99,24 +100,20 @@ func ListMailboxes() ([]data.MailboxSummary, error) {
results := []data.MailboxSummary{}
for _, m := range mailboxes {
total, err := Count(m)
if err != nil {
return nil, err
// ignore *_data collections
if strings.HasSuffix(m, "_data") {
continue
}
unread, err := CountUnread(m)
if err != nil {
return nil, err
}
stats := StatsGet(m)
mb := data.MailboxSummary{}
mb.Name = m
mb.Slug = m
mb.Total = total
mb.Unread = unread
mb.Total = stats.Total
mb.Unread = stats.Unread
if total > 0 {
if mb.Total > 0 {
q, err := db.FindFirst(
clover.NewQuery(m).Sort(clover.SortOption{Field: "Created", Direction: -1}),
)
@ -172,7 +169,7 @@ func CreateMailbox(name string) error {
}
}
return nil
return statsRefresh(name)
}
// Store will store a message in the database and return the unique ID
@ -223,6 +220,8 @@ func Store(mailbox string, b []byte) (string, error) {
return "", err
}
statsAddNewMessage(mailbox)
count++
if count%100 == 0 {
logger.Log().Infof("%d messages added (%s per 100)", count, time.Since(per100start))
@ -441,11 +440,16 @@ func GetMessage(mailbox, id string) (*data.Message, error) {
obj.HTML = html
updates := make(map[string]interface{})
updates["Read"] = true
msg, err := db.FindById(mailbox, id)
if err == nil && !msg.Get("Read").(bool) {
updates := make(map[string]interface{})
updates["Read"] = true
if err := db.UpdateById(mailbox, id, updates); err != nil {
return nil, err
if err := db.UpdateById(mailbox, id, updates); err != nil {
return nil, err
}
statsReadOneMessage(mailbox)
}
return &obj, nil
@ -507,6 +511,8 @@ func UnreadMessage(mailbox, id string) error {
updates := make(map[string]interface{})
updates["Read"] = false
statsUnreadOneMessage(mailbox)
return db.UpdateById(mailbox, id, updates)
}
@ -516,6 +522,8 @@ func DeleteOneMessage(mailbox, id string) error {
return err
}
statsDeleteOneMessage(mailbox)
return db.DeleteById(mailbox+"_data", id)
}
@ -545,13 +553,8 @@ func DeleteAllMessages(mailbox string) error {
}
}
// if err := db.Delete(clover.NewQuery(mailbox)); err != nil {
// return err
// }
// if err := db.Delete(clover.NewQuery(mailbox + "_data")); err != nil {
// return err
// }
// resets stats for mailbox
statsRefresh(mailbox)
elapsed := time.Since(totalStart)
logger.Log().Infof("Deleted %d messages from %s in %s", totalMessages, mailbox, elapsed)

103
storage/stats.go Normal file
View File

@ -0,0 +1,103 @@
package storage
import (
"sync"
"github.com/axllent/mailpit/data"
"github.com/axllent/mailpit/logger"
"github.com/ostafen/clover/v2"
)
var (
mailboxStats = map[string]data.MailboxStats{}
statsLock = sync.RWMutex{}
)
// StatsGet returns the total/unread statistics for a mailbox
func StatsGet(mailbox string) data.MailboxStats {
statsLock.Lock()
defer statsLock.Unlock()
s, ok := mailboxStats[mailbox]
if !ok {
return data.MailboxStats{
Total: 0,
Unread: 0,
}
}
return s
}
// Refresh will completely refresh the existing stats for a given mailbox
func statsRefresh(mailbox string) error {
logger.Log().Debugf("[stats] refreshing stats for %s", mailbox)
total, err := db.Count(clover.NewQuery(mailbox))
if err != nil {
return err
}
unread, err := db.Count(clover.NewQuery(mailbox).Where(clover.Field("Read").IsFalse()))
if err != nil {
return nil
}
statsLock.Lock()
mailboxStats[mailbox] = data.MailboxStats{
Total: total,
Unread: unread,
}
statsLock.Unlock()
return nil
}
func statsAddNewMessage(mailbox string) {
statsLock.Lock()
s, ok := mailboxStats[mailbox]
if ok {
mailboxStats[mailbox] = data.MailboxStats{
Total: s.Total + 1,
Unread: s.Unread + 1,
}
}
statsLock.Unlock()
}
// Deleting one will always mean it was read
func statsDeleteOneMessage(mailbox string) {
statsLock.Lock()
s, ok := mailboxStats[mailbox]
if ok {
mailboxStats[mailbox] = data.MailboxStats{
Total: s.Total - 1,
Unread: s.Unread,
}
}
statsLock.Unlock()
}
// Mark one message as read
func statsReadOneMessage(mailbox string) {
statsLock.Lock()
s, ok := mailboxStats[mailbox]
if ok {
mailboxStats[mailbox] = data.MailboxStats{
Total: s.Total,
Unread: s.Unread - 1,
}
}
statsLock.Unlock()
}
// Mark one message as unread
func statsUnreadOneMessage(mailbox string) {
statsLock.Lock()
s, ok := mailboxStats[mailbox]
if ok {
mailboxStats[mailbox] = data.MailboxStats{
Total: s.Total,
Unread: s.Unread + 1,
}
}
statsLock.Unlock()
}

View File

@ -76,11 +76,12 @@ func pruneCron() {
if err := db.Delete(clover.NewQuery(m).
Sort(clover.SortOption{Field: "Created", Direction: 1}).
Limit(limit)); err != nil {
logger.Log().Warnf("Error pruning: %s", err.Error())
logger.Log().Warnf("Error pruning %s: %s", m, err.Error())
continue
}
elapsed := time.Since(start)
logger.Log().Infof("Pruned %d messages from %s in %s", limit, m, elapsed)
statsRefresh(m)
if !strings.HasSuffix(m, "_data") {
websockets.Broadcast("prune", nil)
}