added IMAP, LetsEncrypt, update layout
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
package mailstore
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ErrQuotaExceeded is returned by StoreMessage when storing raw would push the
|
||||
// mailbox over its quota. No row, file, or used_bytes change occurs in that case.
|
||||
var ErrQuotaExceeded = errors.New("mailstore: mailbox quota exceeded")
|
||||
|
||||
// StoreMessage encrypts raw with the mailbox's own data encryption key and persists
|
||||
// it to disk, then indexes it in esrv_mailbox_messages and updates the mailbox's
|
||||
// cached used_bytes. from/subject are cached in the DB in plain text by design (see
|
||||
// schema.go) so IMAP LIST/basic SEARCH don't need to decrypt every message.
|
||||
func (s *Store) StoreMessage(mailboxID int64, folder string, raw []byte, messageIDHeader, from, subject string) (uid int64, err error) {
|
||||
mbox, err := s.DB.GetMailboxByID(mailboxID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if mbox == nil {
|
||||
return 0, fmt.Errorf("mailstore: mailbox %d not found", mailboxID)
|
||||
}
|
||||
if mbox.UsedBytes+int64(len(raw)) > mbox.QuotaBytes {
|
||||
return 0, ErrQuotaExceeded
|
||||
}
|
||||
|
||||
dek, err := s.UnwrapDEK(mbox.DEKWrapped, mbox.DEKNonce)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
ciphertext, nonce, err := sealAESGCM(dek, raw)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
dir := filepath.Join(s.BasePath, sanitizePathSegment(mbox.Email), folder, now.Format("2006-02-Jan"))
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
name := make([]byte, 8)
|
||||
rand.Read(name)
|
||||
storagePath := filepath.Join(dir, hex.EncodeToString(name)+".eml.enc")
|
||||
if err := os.WriteFile(storagePath, ciphertext, 0o600); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
uid, err = s.DB.InsertMessage(mailboxID, folder, messageIDHeader, "", now, int64(len(raw)), storagePath, nonce, from, subject)
|
||||
if err != nil {
|
||||
os.Remove(storagePath)
|
||||
return 0, err
|
||||
}
|
||||
if err := s.DB.AddMailboxUsedBytes(mailboxID, int64(len(raw))); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return uid, nil
|
||||
}
|
||||
|
||||
// FetchMessage decrypts a stored message on demand. Plaintext is never written to disk
|
||||
// or cached — only returned to the caller.
|
||||
func (s *Store) FetchMessage(mailboxID, uid int64) ([]byte, error) {
|
||||
msg, err := s.DB.GetMessageByUID(mailboxID, uid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if msg == nil {
|
||||
return nil, fmt.Errorf("mailstore: message %d not found in mailbox %d", uid, mailboxID)
|
||||
}
|
||||
mbox, err := s.DB.GetMailboxByID(mailboxID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if mbox == nil {
|
||||
return nil, fmt.Errorf("mailstore: mailbox %d not found", mailboxID)
|
||||
}
|
||||
dek, err := s.UnwrapDEK(mbox.DEKWrapped, mbox.DEKNonce)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ciphertext, err := os.ReadFile(msg.StoragePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return openAESGCM(dek, ciphertext, msg.Nonce)
|
||||
}
|
||||
|
||||
// DeleteMessage removes the on-disk ciphertext, the index row, and frees the quota.
|
||||
func (s *Store) DeleteMessage(mailboxID, uid int64) error {
|
||||
msg, err := s.DB.GetMessageByUID(mailboxID, uid)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if msg == nil {
|
||||
return nil
|
||||
}
|
||||
if err := os.Remove(msg.StoragePath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if err := s.DB.DeleteMessage(mailboxID, uid); err != nil {
|
||||
return err
|
||||
}
|
||||
return s.DB.AddMailboxUsedBytes(mailboxID, -msg.SizeBytes)
|
||||
}
|
||||
Reference in New Issue
Block a user