Files
2025-11-21 22:43:05 +08:00

195 lines
3.8 KiB
Go

package storage
import (
"math/big"
"sort"
"strings"
"sync"
"time"
"github.com/google/uuid"
)
type Message struct {
ID string `json:"id"`
Address string `json:"address"`
From string `json:"from"`
Subject string `json:"subject"`
Snippet string `json:"snippet"`
CreatedAt time.Time `json:"createdAt"`
ExpiresAt time.Time `json:"expiresAt"`
// Raw MIME for full fetch
Raw []byte `json:"-"`
}
type Store interface {
CreateAddress(local string) string
AddressExists(local string) bool
Save(addr string, msg Message) (Message, error)
List(addr string) []Message
Get(addr, id string) (Message, bool)
PurgeExpired()
TTL() time.Duration
Close()
}
type MemoryStore struct {
mu sync.RWMutex
ttl time.Duration
messages map[string]map[string]Message // addr -> id -> message
stopCh chan struct{}
}
func NewMemoryStore(ttl time.Duration) *MemoryStore {
ms := &MemoryStore{
ttl: ttl,
messages: make(map[string]map[string]Message),
stopCh: make(chan struct{}),
}
go ms.gcLoop()
return ms
}
// uuidToBase36 converts a UUID to a base36 (lowercase) string with 8 characters
func uuidToBase36() string {
id := uuid.New()
// Convert UUID bytes to big.Int
var num big.Int
num.SetBytes(id[:])
// Base36 alphabet (0-9, a-z)
const base36Chars = "0123456789abcdefghijklmnopqrstuvwxyz"
base := big.NewInt(36)
// Encode to base36
var encoded strings.Builder
if num.Sign() == 0 {
return "00000000"
}
var digits []byte
temp := new(big.Int).Set(&num)
zero := big.NewInt(0)
mod := new(big.Int)
for temp.Cmp(zero) > 0 {
temp.DivMod(temp, base, mod)
digits = append(digits, base36Chars[mod.Int64()])
}
// Reverse the digits
for i := len(digits) - 1; i >= 0; i-- {
encoded.WriteByte(digits[i])
}
result := encoded.String()
// Truncate to 8 characters
if len(result) > 8 {
return result[:8]
}
// Pad with leading zeros if needed
for len(result) < 8 {
result = "0" + result
}
return result
}
func (m *MemoryStore) CreateAddress(local string) string {
if local == "" {
local = uuidToBase36()
}
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.messages[local]; !ok {
m.messages[local] = make(map[string]Message)
}
return local
}
func (m *MemoryStore) AddressExists(local string) bool {
m.mu.RLock()
defer m.mu.RUnlock()
_, exists := m.messages[local]
return exists
}
func (m *MemoryStore) Save(addr string, msg Message) (Message, error) {
m.mu.Lock()
defer m.mu.Unlock()
if _, ok := m.messages[addr]; !ok {
m.messages[addr] = make(map[string]Message)
}
if msg.ID == "" {
msg.ID = uuid.NewString()
}
now := time.Now()
msg.CreatedAt = now
msg.ExpiresAt = now.Add(m.ttl)
m.messages[addr][msg.ID] = msg
return msg, nil
}
func (m *MemoryStore) List(addr string) []Message {
m.mu.RLock()
defer m.mu.RUnlock()
var out []Message
for _, msg := range m.messages[addr] {
out = append(out, msg)
}
sort.Slice(out, func(i, j int) bool { return out[i].CreatedAt.After(out[j].CreatedAt) })
return out
}
func (m *MemoryStore) Get(addr, id string) (Message, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
msgs, ok := m.messages[addr]
if !ok {
return Message{}, false
}
msg, ok := msgs[id]
if !ok {
return Message{}, false
}
return msg, true
}
func (m *MemoryStore) PurgeExpired() {
m.mu.Lock()
defer m.mu.Unlock()
now := time.Now()
for addr, msgs := range m.messages {
for id, msg := range msgs {
if now.After(msg.ExpiresAt) {
delete(msgs, id)
}
}
if len(msgs) == 0 {
delete(m.messages, addr)
}
}
}
func (m *MemoryStore) TTL() time.Duration { return m.ttl }
func (m *MemoryStore) gcLoop() {
ticker := time.NewTicker(time.Minute)
defer ticker.Stop()
for {
select {
case <-ticker.C:
m.PurgeExpired()
case <-m.stopCh:
return
}
}
}
func (m *MemoryStore) Close() {
close(m.stopCh)
}