245 lines
4.8 KiB
Go
245 lines
4.8 KiB
Go
// 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)
|
|
|
|
log.Printf("NextUID: %v", l.header.NextUID)
|
|
l.header.NextUID++
|
|
log.Printf("NextUID: %v", l.header.NextUID)
|
|
|
|
// 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()
|
|
|
|
log.Printf("Parsing %v", l.filename)
|
|
defer func() { log.Printf("Finished parsing %v", l.filename) }()
|
|
|
|
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
|
|
log.Printf("l.header.NextUID: %v", l.header.NextUID)
|
|
if entry.UID >= l.header.NextUID {
|
|
log.Printf("Incrementing as entry with UID %v is greater", entry.UID)
|
|
l.header.NextUID = entry.UID + 1
|
|
}
|
|
log.Printf("l.header.NextUID: %v", l.header.NextUID)
|
|
}
|
|
|
|
l.header = header
|
|
l.entries = entries
|
|
log.Printf("list: %#v", l)
|
|
|
|
return nil
|
|
}
|