// 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 }