Convert from BoltDB to Maildir storage for emails
This commit is contained in:
233
internal/imap/uidlist/uidlist.go
Normal file
233
internal/imap/uidlist/uidlist.go
Normal file
@@ -0,0 +1,233 @@
|
||||
// package uidlist implements Dovecot's `dovecot-uidlist` file format for
|
||||
// Maildir directories
|
||||
//
|
||||
// Reference: https://wiki.dovecot.org/MailboxFormat/Maildir
|
||||
package uidlist
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultFilename = "dovecot-uidlist"
|
||||
)
|
||||
|
||||
type List struct {
|
||||
filename string
|
||||
header Header
|
||||
entries []Entry
|
||||
|
||||
mutex *sync.Mutex
|
||||
}
|
||||
|
||||
func Create(dir string) (*List, error) {
|
||||
return CreateFile(filepath.Join(dir, DefaultFilename))
|
||||
}
|
||||
|
||||
// FIXME: on startup, we need to scan `cur/` in case any problems arose
|
||||
func Open(dir string) (*List, error) {
|
||||
return OpenFile(filepath.Join(dir, DefaultFilename))
|
||||
}
|
||||
|
||||
func CreateFile(filename string) (*List, error) {
|
||||
list, err := handleFile(filename, os.O_RDWR|os.O_CREATE|os.O_TRUNC)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
hdr, err := GenerateHeader()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
list.header = hdr
|
||||
|
||||
// TODO: allow the dovecot-uidlist to be populated from scratch
|
||||
// FIXME: in particular, NextUID and entries are wrong here
|
||||
|
||||
if err := list.save(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func OpenFile(filename string) (*List, error) {
|
||||
list, err := handleFile(filename, os.O_RDWR|os.O_CREATE)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := list.parse(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return list, nil
|
||||
}
|
||||
|
||||
func handleFile(filename string, flags int) (*List, error) {
|
||||
file, err := os.OpenFile(filename, flags, 0600)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
file.Close()
|
||||
|
||||
return &List{
|
||||
filename: filename,
|
||||
mutex: &sync.Mutex{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (l *List) UIDValidity() uint64 {
|
||||
return l.header.UIDValidity
|
||||
}
|
||||
|
||||
func (l *List) GUID() string {
|
||||
return l.header.GUID
|
||||
}
|
||||
|
||||
func (l *List) Entries() []Entry {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
// FIXME: use copy() ?
|
||||
return l.entries
|
||||
}
|
||||
|
||||
func (l *List) EntriesInRange(start, end uint64) []Entry {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
// FIXME: this shouldn't need a full scan
|
||||
var out []Entry
|
||||
|
||||
log.Printf("Checking range %d to %d in %#v", start, end, l.entries)
|
||||
|
||||
for _, e := range l.entries {
|
||||
if e.UID >= start && (end == 0 || e.UID <= end) {
|
||||
out = append(out, e)
|
||||
}
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
func (l *List) Entry(key string) *Entry {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *List) Append(filenames ...string) error {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
file, err := os.OpenFile(l.filename, os.O_APPEND|os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
for _, filename := range filenames {
|
||||
entry := Entry{
|
||||
UID: l.header.NextUID,
|
||||
LastKnownFilename: filename,
|
||||
}
|
||||
log.Printf("Adding entry: %#v", entry)
|
||||
|
||||
l.entries = append(l.entries, entry)
|
||||
l.header.NextUID = l.header.NextUID + 1
|
||||
|
||||
// Try to write them all, at least
|
||||
// FIXME: we don't bubble up errors when we should
|
||||
if _, err := fmt.Fprintln(file, entry.String()); err != nil {
|
||||
log.Printf("failed to write UID %d to %s: %v", entry.UID, l.filename, err)
|
||||
}
|
||||
}
|
||||
log.Printf("Appended %d entries", len(filenames))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *List) save() (err error) {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
// FIXME: When rewriting the file, we're meant to:
|
||||
// * Take the dovecot-uidlist.lock file
|
||||
// * write to a temporary file then atomically rename onto -uidlist
|
||||
// Unsure if the temporary file and the lock file are the same thing or not
|
||||
|
||||
file, err := os.OpenFile(l.filename, os.O_TRUNC|os.O_RDWR, 0600)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
defer func() { err = file.Close() }()
|
||||
|
||||
_, err = fmt.Fprintln(file, l.header.String())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, e := range l.entries {
|
||||
_, err = fmt.Fprintln(file, e.String())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (l *List) parse() error {
|
||||
l.mutex.Lock()
|
||||
defer l.mutex.Unlock()
|
||||
|
||||
file, err := os.Open(l.filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer file.Close()
|
||||
|
||||
r := bufio.NewScanner(file)
|
||||
|
||||
// Get the header if present, which is the first line
|
||||
if !r.Scan() {
|
||||
return r.Err()
|
||||
}
|
||||
|
||||
header, err := ParseHeader(r.Text())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now parse each additional line
|
||||
entries := []Entry{}
|
||||
for r.Scan() {
|
||||
entry, err := ParseEntry(r.Text())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entries = append(entries, entry)
|
||||
|
||||
// If NextUID is out of date in the file, ignore it. We never want to
|
||||
// re-use a seen UID
|
||||
if entry.UID >= l.header.NextUID {
|
||||
l.header.NextUID++
|
||||
}
|
||||
}
|
||||
|
||||
l.header = header
|
||||
l.entries = entries
|
||||
|
||||
return nil
|
||||
}
|
Reference in New Issue
Block a user