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:
commit
ce23e0616e
@ -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
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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")
|
||||
|
@ -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>
|
||||
|
@ -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
103
storage/stats.go
Normal 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()
|
||||
}
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user