Fix archive reindex and add target dedup
This commit is contained in:
parent
e612896e06
commit
c83225f5ad
9 changed files with 796 additions and 6 deletions
|
|
@ -114,6 +114,11 @@ func mboxRecord(m RawMessage) []byte {
|
|||
|
||||
func mboxAppendInfoFromMessage(m RawMessage, record []byte) MboxAppendInfo {
|
||||
info := MboxAppendInfo{InnerLen: int64(len(record))}
|
||||
parts := splitMboxRecordsWithOffsets(record)
|
||||
if len(parts) > 0 {
|
||||
info.InnerOffset = parts[0].Offset
|
||||
info.InnerLen = int64(len(parts[0].Message))
|
||||
}
|
||||
msg, err := mail.ReadMessage(bytes.NewReader(m.Body))
|
||||
if err != nil {
|
||||
return info
|
||||
|
|
@ -137,6 +142,9 @@ type MboxEntry struct {
|
|||
// ReadMboxList parst die Kopfzeilen aller Mails einer mbox-Datei (fuer die
|
||||
// Nachrichtenliste). ReadMboxMessage liefert eine einzelne Mail als Rohtext.
|
||||
func ReadMboxList(path string) ([]MboxEntry, error) {
|
||||
if err := EnsureMboxIndexed(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if entries, ok := readMboxListFromIndex(path); ok {
|
||||
return entries, nil
|
||||
}
|
||||
|
|
@ -162,6 +170,9 @@ func ReadMboxList(path string) ([]MboxEntry, error) {
|
|||
}
|
||||
|
||||
func ReadMboxMessage(path string, index int) ([]byte, error) {
|
||||
if err := EnsureMboxIndexed(path); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if raw, ok, err := readMboxMessageFromIndex(path, index); ok || err != nil {
|
||||
return raw, err
|
||||
}
|
||||
|
|
@ -194,6 +205,229 @@ func readMboxMessages(path string) ([][]byte, error) {
|
|||
return readMboxMessagesBytes(b), nil
|
||||
}
|
||||
|
||||
func EnsureMboxIndexed(path string) error {
|
||||
accountID, folder, ok := mboxIndexContext(path)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
indexed, err := MboxIndexedBytes(accountID, folder)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if indexed == info.Size() {
|
||||
return nil
|
||||
}
|
||||
return ReindexMboxFile(accountID, folder, path)
|
||||
}
|
||||
|
||||
func ReindexMboxFile(accountID int64, folder, path string) error {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var entries []MboxIndexEntry
|
||||
if strings.HasSuffix(strings.ToLower(path), ".zst") {
|
||||
entries, err = reindexZstdMbox(path)
|
||||
} else {
|
||||
entries, err = reindexPlainMbox(path)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ReplaceMboxIndex(accountID, folder, entries, info.Size())
|
||||
}
|
||||
|
||||
func ReindexArchives(name string) error {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return fmt.Errorf("reindex ziel fehlt")
|
||||
}
|
||||
accounts, err := ListAccounts()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
matched := 0
|
||||
for _, account := range accounts {
|
||||
archive := accountMboxDir(account)
|
||||
if archive == "" {
|
||||
continue
|
||||
}
|
||||
if name != "all" && !strings.EqualFold(name, account.Name) && !strings.EqualFold(name, archive) {
|
||||
continue
|
||||
}
|
||||
if err := reindexAccountArchive(account); err != nil {
|
||||
return err
|
||||
}
|
||||
matched++
|
||||
}
|
||||
if matched == 0 {
|
||||
return fmt.Errorf("kein Konto/Archiv fuer reindex %q gefunden", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func reindexAccountArchive(account Account) error {
|
||||
dir := filepath.Join(Cfg.MboxRoot, accountMboxDir(account))
|
||||
for _, path := range archiveMboxPaths(dir) {
|
||||
folder := folderNameFromMboxPath(path)
|
||||
if folder == "" {
|
||||
continue
|
||||
}
|
||||
if err := ReindexMboxFile(account.ID, folder, path); err != nil {
|
||||
return fmt.Errorf("%s %s: %w", account.Name, folder, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func reindexPlainMbox(path string) ([]MboxIndexEntry, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []MboxIndexEntry
|
||||
for _, part := range splitMboxRecordsWithOffsets(b) {
|
||||
entry := indexEntryFromMboxRecord(part.Message, part.Offset, int64(part.Length), 0, int64(part.Length))
|
||||
if entry.MessageID != "" {
|
||||
out = append(out, entry)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func reindexZstdMbox(path string) ([]MboxIndexEntry, error) {
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dec, err := zstd.NewReader(nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer dec.Close()
|
||||
var out []MboxIndexEntry
|
||||
for _, frame := range splitZstdFrames(b) {
|
||||
record, err := dec.DecodeAll(frame.Data, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, part := range splitMboxRecordsWithOffsets(record) {
|
||||
entry := indexEntryFromMboxRecord(part.Message, frame.Offset, int64(frame.Length), part.Offset, int64(len(part.Message)))
|
||||
if entry.MessageID != "" {
|
||||
out = append(out, entry)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
type mboxRecordPart struct {
|
||||
Offset int64
|
||||
Length int
|
||||
Message []byte
|
||||
}
|
||||
|
||||
func splitMboxRecordsWithOffsets(b []byte) []mboxRecordPart {
|
||||
normalized := bytes.ReplaceAll(b, []byte("\r\n"), []byte("\n"))
|
||||
lines := bytes.SplitAfter(normalized, []byte("\n"))
|
||||
var out []mboxRecordPart
|
||||
var cur bytes.Buffer
|
||||
var curStart int64
|
||||
var pos int64
|
||||
inMsg := false
|
||||
for _, lineWithNL := range lines {
|
||||
line := bytes.TrimSuffix(lineWithNL, []byte("\n"))
|
||||
if bytes.HasPrefix(line, []byte("From ")) {
|
||||
if inMsg && cur.Len() > 0 {
|
||||
msg := bytes.TrimRight(cur.Bytes(), "\n")
|
||||
out = append(out, mboxRecordPart{Offset: curStart, Length: int(pos - curStart), Message: append([]byte(nil), msg...)})
|
||||
cur.Reset()
|
||||
}
|
||||
inMsg = true
|
||||
pos += int64(len(lineWithNL))
|
||||
curStart = pos
|
||||
continue
|
||||
}
|
||||
if inMsg {
|
||||
if bytes.HasPrefix(line, []byte(">From ")) {
|
||||
line = line[1:]
|
||||
}
|
||||
_, _ = cur.Write(line)
|
||||
_ = cur.WriteByte('\n')
|
||||
}
|
||||
pos += int64(len(lineWithNL))
|
||||
}
|
||||
if inMsg && cur.Len() > 0 {
|
||||
msg := bytes.TrimRight(cur.Bytes(), "\n")
|
||||
out = append(out, mboxRecordPart{Offset: curStart, Length: int(pos - curStart), Message: append([]byte(nil), msg...)})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
type zstdFramePart struct {
|
||||
Offset int64
|
||||
Length int
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func splitZstdFrames(b []byte) []zstdFramePart {
|
||||
magic := []byte{0x28, 0xb5, 0x2f, 0xfd}
|
||||
var starts []int
|
||||
for i := 0; i <= len(b)-len(magic); i++ {
|
||||
if bytes.Equal(b[i:i+len(magic)], magic) {
|
||||
starts = append(starts, i)
|
||||
}
|
||||
}
|
||||
if len(starts) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]zstdFramePart, 0, len(starts))
|
||||
for i, start := range starts {
|
||||
end := len(b)
|
||||
if i+1 < len(starts) {
|
||||
end = starts[i+1]
|
||||
}
|
||||
out = append(out, zstdFramePart{Offset: int64(start), Length: end - start, Data: b[start:end]})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func indexEntryFromMboxRecord(raw []byte, fileOffset, frameLen, innerOffset, innerLen int64) MboxIndexEntry {
|
||||
entry := MboxIndexEntry{FileOffset: fileOffset, FrameLen: frameLen, InnerOffset: innerOffset, InnerLen: innerLen}
|
||||
msg, err := mail.ReadMessage(bytes.NewReader(raw))
|
||||
if err != nil {
|
||||
return entry
|
||||
}
|
||||
entry.MessageID = normalizeMessageID(msg.Header.Get("Message-ID"))
|
||||
if entry.MessageID == "" {
|
||||
entry.MessageID = messageID(raw)
|
||||
}
|
||||
entry.Subject = decodeHeader(msg.Header.Get("Subject"))
|
||||
entry.From = decodeHeader(msg.Header.Get("From"))
|
||||
entry.Date = decodeHeader(msg.Header.Get("Date"))
|
||||
return entry
|
||||
}
|
||||
|
||||
func unescapeMboxMessage(raw []byte) []byte {
|
||||
raw = bytes.ReplaceAll(raw, []byte("\r\n"), []byte("\n"))
|
||||
lines := bytes.Split(raw, []byte("\n"))
|
||||
var b bytes.Buffer
|
||||
for i, line := range lines {
|
||||
if i > 0 {
|
||||
_ = b.WriteByte('\n')
|
||||
}
|
||||
if bytes.HasPrefix(line, []byte(">From ")) {
|
||||
line = line[1:]
|
||||
}
|
||||
_, _ = b.Write(line)
|
||||
}
|
||||
return bytes.TrimRight(b.Bytes(), "\n")
|
||||
}
|
||||
|
||||
func readMboxMessagesBytes(b []byte) [][]byte {
|
||||
b = bytes.ReplaceAll(b, []byte("\r\n"), []byte("\n"))
|
||||
lines := bytes.Split(b, []byte("\n"))
|
||||
|
|
@ -278,11 +512,12 @@ func readMboxMessageFromIndex(path string, index int) ([]byte, bool, error) {
|
|||
if err != nil {
|
||||
return nil, true, err
|
||||
}
|
||||
msgs := readMboxMessagesBytes(record)
|
||||
if len(msgs) == 0 {
|
||||
start := entry.InnerOffset
|
||||
end := entry.InnerOffset + entry.InnerLen
|
||||
if start < 0 || end > int64(len(record)) || start >= end {
|
||||
return nil, true, fmt.Errorf("message index out of range")
|
||||
}
|
||||
return msgs[0], true, nil
|
||||
return unescapeMboxMessage(record[start:end]), true, nil
|
||||
}
|
||||
|
||||
func mboxIndexContext(path string) (int64, string, bool) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue