update
This commit is contained in:
@@ -6,8 +6,10 @@ package mailstore
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/mail"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -33,6 +35,27 @@ func New(root string, mk *crypto.MasterKey, database *db.DB) *Store {
|
||||
return &Store{root: root, mk: mk, db: database}
|
||||
}
|
||||
|
||||
// CachedHeader is the small subset of a message's headers worth caching
|
||||
// separately from the full body, so a folder listing can decrypt a few
|
||||
// hundred bytes instead of the whole message just to show a list of
|
||||
// subjects — see Deliver and DecryptHeaderCache.
|
||||
type CachedHeader struct {
|
||||
From, To, Subject, Date string
|
||||
}
|
||||
|
||||
func parseCachedHeader(raw []byte) CachedHeader {
|
||||
msg, err := mail.ReadMessage(strings.NewReader(string(raw)))
|
||||
if err != nil {
|
||||
return CachedHeader{}
|
||||
}
|
||||
return CachedHeader{
|
||||
From: msg.Header.Get("From"),
|
||||
To: msg.Header.Get("To"),
|
||||
Subject: msg.Header.Get("Subject"),
|
||||
Date: msg.Header.Get("Date"),
|
||||
}
|
||||
}
|
||||
|
||||
// Deliver writes a raw message into a user's mailbox, encrypting it at rest,
|
||||
// allocates the next IMAP UID, and records the mailbox_index row. Returns the
|
||||
// assigned UID.
|
||||
@@ -47,6 +70,18 @@ func (s *Store) Deliver(userID, userEmail, mailbox string, raw []byte) (uid int,
|
||||
return 0, fmt.Errorf("encrypt message: %w", err)
|
||||
}
|
||||
|
||||
// Encrypted separately under its own purpose so folder listing can
|
||||
// decrypt this small blob instead of the full message body — same
|
||||
// per-record HKDF scheme, so "everything encrypted at rest" still
|
||||
// holds. A failure here isn't fatal to delivery: ListMessages falls
|
||||
// back to a full read when header_enc is absent.
|
||||
var headerEnc []byte
|
||||
if hdrJSON, err := json.Marshal(parseCachedHeader(raw)); err == nil {
|
||||
if enc, err := crypto.Encrypt(s.mk, messageID, "message-header", hdrJSON); err == nil {
|
||||
headerEnc = enc
|
||||
}
|
||||
}
|
||||
|
||||
filename := maildirFilename(messageID)
|
||||
tmpPath := filepath.Join(s.mailboxDir(userEmail, mailbox), "tmp", filename)
|
||||
finalPath := filepath.Join(s.mailboxDir(userEmail, mailbox), "cur", filename)
|
||||
@@ -76,6 +111,7 @@ func (s *Store) Deliver(userID, userEmail, mailbox string, raw []byte) (uid int,
|
||||
SizeBytes: int64(len(raw)),
|
||||
ReceivedAt: time.Now().UTC(),
|
||||
InternalDate: time.Now().UTC(),
|
||||
HeaderEnc: headerEnc,
|
||||
}
|
||||
if err := s.db.InsertMailboxEntry(entry); err != nil {
|
||||
// Best-effort cleanup of the file we just wrote — DB is the source of
|
||||
@@ -104,6 +140,20 @@ func (s *Store) Read(path string) ([]byte, error) {
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
// DecryptHeaderCache decrypts a header_enc blob written by Deliver.
|
||||
// messageID must be the same ID used at Deliver time (mailbox_index.id).
|
||||
func (s *Store) DecryptHeaderCache(messageID string, headerEnc []byte) (CachedHeader, error) {
|
||||
plaintext, err := crypto.Decrypt(s.mk, messageID, "message-header", headerEnc)
|
||||
if err != nil {
|
||||
return CachedHeader{}, fmt.Errorf("decrypt header cache: %w", err)
|
||||
}
|
||||
var h CachedHeader
|
||||
if err := json.Unmarshal(plaintext, &h); err != nil {
|
||||
return CachedHeader{}, fmt.Errorf("unmarshal header cache: %w", err)
|
||||
}
|
||||
return h, nil
|
||||
}
|
||||
|
||||
func (s *Store) ensureMailboxDirs(userEmail, mailbox string) error {
|
||||
base := s.mailboxDir(userEmail, mailbox)
|
||||
for _, sub := range []string{"cur", "new", "tmp"} {
|
||||
|
||||
Reference in New Issue
Block a user