Use header-only IMAP lists
This commit is contained in:
parent
a52963c380
commit
a17d2c8a2c
5 changed files with 456 additions and 184 deletions
|
|
@ -24,6 +24,17 @@ type RawMessage struct {
|
|||
InternalDate time.Time // Original-Zeit, per APPEND erhalten
|
||||
}
|
||||
|
||||
type MessageHeader struct {
|
||||
UID uint32
|
||||
MessageID string
|
||||
Subject string
|
||||
From string
|
||||
Date time.Time
|
||||
InternalDate time.Time
|
||||
Flags []string
|
||||
Size int64
|
||||
}
|
||||
|
||||
// Folder ist ein Ordner der Quelle MIT den Infos, die 11-folders.go fuer die
|
||||
// Rollen-Zuordnung braucht: dekodierter Name, SPECIAL-USE-Attribute und der
|
||||
// Hierarchie-Trenner.
|
||||
|
|
@ -36,7 +47,9 @@ type Folder struct {
|
|||
// SourceMailbox abstrahiert Quelle (IMAP primaer, POP3 Fallback), damit
|
||||
// 07-migrate.go gegen ein Interface arbeitet.
|
||||
type SourceMailbox interface {
|
||||
Folders() ([]Folder, error) // rekursiver Ordnerbaum
|
||||
Folders() ([]Folder, error) // rekursiver Ordnerbaum
|
||||
Headers(folder string, limit, offset int) ([]MessageHeader, error)
|
||||
FetchOne(folder string, uid uint32) (RawMessage, error)
|
||||
Fetch(folder string, fn func(RawMessage) error) error // BODY[] FLAGS INTERNALDATE
|
||||
Close() error
|
||||
}
|
||||
|
|
@ -79,6 +92,14 @@ func (s *imapSource) Fetch(folder string, fn func(RawMessage) error) error {
|
|||
return s.mailbox.fetchEach(folder, fn)
|
||||
}
|
||||
|
||||
func (s *imapSource) Headers(folder string, limit, offset int) ([]MessageHeader, error) {
|
||||
return s.mailbox.fetchHeaders(folder, limit, offset)
|
||||
}
|
||||
|
||||
func (s *imapSource) FetchOne(folder string, uid uint32) (RawMessage, error) {
|
||||
return s.mailbox.fetchOne(folder, uid)
|
||||
}
|
||||
|
||||
func (s *imapSource) Close() error {
|
||||
return s.mailbox.Close()
|
||||
}
|
||||
|
|
@ -154,6 +175,7 @@ func (m *imapClientMailbox) fetchEach(folder string, fn func(RawMessage) error)
|
|||
InternalDate: true,
|
||||
BodySection: []*imap.FetchItemBodySection{section},
|
||||
})
|
||||
defer cmd.Close()
|
||||
|
||||
for {
|
||||
data := cmd.Next()
|
||||
|
|
@ -162,38 +184,103 @@ func (m *imapClientMailbox) fetchEach(folder string, fn func(RawMessage) error)
|
|||
}
|
||||
buf, err := data.Collect()
|
||||
if err != nil {
|
||||
_ = cmd.Close()
|
||||
return err
|
||||
}
|
||||
body := buf.FindBodySection(section)
|
||||
if body == nil {
|
||||
body = []byte{}
|
||||
}
|
||||
if err := fn(RawMessage{
|
||||
MessageID: messageIDFromFetch(buf, body),
|
||||
Body: body,
|
||||
Flags: flagsToStrings(sanitizeIMAPFlags(buf.Flags)),
|
||||
InternalDate: buf.InternalDate,
|
||||
}); err != nil {
|
||||
_ = cmd.Close()
|
||||
if err := fn(rawMessageFromFetch(buf, section)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return cmd.Close()
|
||||
}
|
||||
|
||||
func (m *imapClientMailbox) fetchAll(folder string) ([]RawMessage, error) {
|
||||
var out []RawMessage
|
||||
err := m.fetchEach(folder, func(m RawMessage) error {
|
||||
out = append(out, m)
|
||||
return nil
|
||||
})
|
||||
func (m *imapClientMailbox) fetchHeaders(folder string, limit, offset int) ([]MessageHeader, error) {
|
||||
selected, err := m.c.Select(folder, &imap.SelectOptions{ReadOnly: true}).Wait()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if selected.NumMessages == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
if limit <= 0 || limit > 200 {
|
||||
limit = 200
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
stop := int(selected.NumMessages) - offset
|
||||
if stop <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
start := stop - limit + 1
|
||||
if start < 1 {
|
||||
start = 1
|
||||
}
|
||||
|
||||
seqSet := imap.SeqSet{}
|
||||
seqSet.AddRange(uint32(start), uint32(stop))
|
||||
cmd := m.c.Fetch(seqSet, &imap.FetchOptions{
|
||||
Envelope: true,
|
||||
Flags: true,
|
||||
InternalDate: true,
|
||||
RFC822Size: true,
|
||||
UID: true,
|
||||
})
|
||||
defer cmd.Close()
|
||||
|
||||
var out []MessageHeader
|
||||
for {
|
||||
data := cmd.Next()
|
||||
if data == nil {
|
||||
break
|
||||
}
|
||||
buf, err := data.Collect()
|
||||
if err != nil {
|
||||
return out, err
|
||||
}
|
||||
out = append(out, messageHeaderFromFetch(buf))
|
||||
}
|
||||
if err := cmd.Close(); err != nil {
|
||||
return out, err
|
||||
}
|
||||
reverseHeaders(out)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (m *imapClientMailbox) fetchOne(folder string, uid uint32) (RawMessage, error) {
|
||||
if uid == 0 {
|
||||
return RawMessage{}, fmt.Errorf("Nachricht ohne UID")
|
||||
}
|
||||
if _, err := m.c.Select(folder, &imap.SelectOptions{ReadOnly: true}).Wait(); err != nil {
|
||||
return RawMessage{}, err
|
||||
}
|
||||
|
||||
section := &imap.FetchItemBodySection{Peek: true}
|
||||
cmd := m.c.Fetch(imap.UIDSetNum(imap.UID(uid)), &imap.FetchOptions{
|
||||
Envelope: true,
|
||||
Flags: true,
|
||||
InternalDate: true,
|
||||
UID: true,
|
||||
BodySection: []*imap.FetchItemBodySection{section},
|
||||
})
|
||||
defer cmd.Close()
|
||||
data := cmd.Next()
|
||||
if data == nil {
|
||||
if err := cmd.Close(); err != nil {
|
||||
return RawMessage{}, err
|
||||
}
|
||||
return RawMessage{}, fmt.Errorf("Nachricht nicht gefunden")
|
||||
}
|
||||
buf, err := data.Collect()
|
||||
if err != nil {
|
||||
return RawMessage{}, err
|
||||
}
|
||||
msg := rawMessageFromFetch(buf, section)
|
||||
if err := cmd.Close(); err != nil {
|
||||
return RawMessage{}, err
|
||||
}
|
||||
return msg, nil
|
||||
}
|
||||
|
||||
func (m *imapClientMailbox) Close() error {
|
||||
if m == nil || m.c == nil {
|
||||
return nil
|
||||
|
|
@ -262,9 +349,68 @@ func stringsToFlags(flags []string) []imap.Flag {
|
|||
}())
|
||||
}
|
||||
|
||||
func rawMessageFromFetch(buf *imapclient.FetchMessageBuffer, section *imap.FetchItemBodySection) RawMessage {
|
||||
body := buf.FindBodySection(section)
|
||||
if body == nil {
|
||||
body = []byte{}
|
||||
}
|
||||
return RawMessage{
|
||||
MessageID: messageIDFromFetch(buf, body),
|
||||
Body: body,
|
||||
Flags: flagsToStrings(sanitizeIMAPFlags(buf.Flags)),
|
||||
InternalDate: buf.InternalDate,
|
||||
}
|
||||
}
|
||||
|
||||
func messageHeaderFromFetch(buf *imapclient.FetchMessageBuffer) MessageHeader {
|
||||
h := MessageHeader{
|
||||
UID: uint32(buf.UID),
|
||||
MessageID: messageIDFromFetch(buf, nil),
|
||||
InternalDate: buf.InternalDate,
|
||||
Flags: flagsToStrings(sanitizeIMAPFlags(buf.Flags)),
|
||||
Size: buf.RFC822Size,
|
||||
}
|
||||
if buf.Envelope != nil {
|
||||
h.Subject = strings.TrimSpace(buf.Envelope.Subject)
|
||||
h.From = envelopeFrom(buf.Envelope)
|
||||
h.Date = buf.Envelope.Date
|
||||
}
|
||||
if h.Date.IsZero() {
|
||||
h.Date = h.InternalDate
|
||||
}
|
||||
return h
|
||||
}
|
||||
|
||||
func envelopeFrom(env *imap.Envelope) string {
|
||||
if env == nil || len(env.From) == 0 {
|
||||
return ""
|
||||
}
|
||||
for _, addr := range env.From {
|
||||
email := strings.TrimSpace(addr.Addr())
|
||||
if email == "" {
|
||||
continue
|
||||
}
|
||||
name := strings.TrimSpace(addr.Name)
|
||||
if name != "" {
|
||||
return (&mail.Address{Name: name, Address: email}).String()
|
||||
}
|
||||
return email
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func reverseHeaders(headers []MessageHeader) {
|
||||
for i, j := 0, len(headers)-1; i < j; i, j = i+1, j-1 {
|
||||
headers[i], headers[j] = headers[j], headers[i]
|
||||
}
|
||||
}
|
||||
|
||||
func messageIDFromFetch(buf *imapclient.FetchMessageBuffer, body []byte) string {
|
||||
if buf != nil && buf.Envelope != nil && strings.TrimSpace(buf.Envelope.MessageID) != "" {
|
||||
return strings.TrimSpace(buf.Envelope.MessageID)
|
||||
return normalizeMessageID(buf.Envelope.MessageID)
|
||||
}
|
||||
if len(body) == 0 {
|
||||
return ""
|
||||
}
|
||||
return messageID(body)
|
||||
}
|
||||
|
|
@ -273,12 +419,16 @@ func messageID(body []byte) string {
|
|||
msg, err := mail.ReadMessage(strings.NewReader(string(body)))
|
||||
if err == nil {
|
||||
if id := strings.TrimSpace(msg.Header.Get("Message-ID")); id != "" {
|
||||
return id
|
||||
return normalizeMessageID(id)
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("sha256:%x", sha256.Sum256(body))
|
||||
}
|
||||
|
||||
func normalizeMessageID(id string) string {
|
||||
return strings.Trim(strings.TrimSpace(id), "<>")
|
||||
}
|
||||
|
||||
func isAlreadyExistsError(err error) bool {
|
||||
var imapErr *imap.Error
|
||||
if errors.As(err, &imapErr) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue