381 lines
7.8 KiB
Go
381 lines
7.8 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"fmt"
|
|
"github.com/google/uuid"
|
|
"io"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/emersion/go-imap/v2"
|
|
"github.com/emersion/go-imap/v2/imapserver"
|
|
gomessage "github.com/emersion/go-message"
|
|
"github.com/emersion/go-message/mail"
|
|
"github.com/emersion/go-message/textproto"
|
|
)
|
|
|
|
type MessageBuffer struct {
|
|
underlyingBuf io.ReadSeekCloser
|
|
closed bool
|
|
parent *message
|
|
}
|
|
|
|
func (b *MessageBuffer) Read(p []byte) (n int, err error) {
|
|
if b.closed {
|
|
return 0, io.ErrClosedPipe
|
|
}
|
|
return b.underlyingBuf.Read(p)
|
|
}
|
|
|
|
func (b *MessageBuffer) Seek(offset int64, whence int) (int64, error) {
|
|
if b.closed {
|
|
return 0, io.ErrClosedPipe
|
|
}
|
|
return b.underlyingBuf.Seek(offset, whence)
|
|
}
|
|
|
|
// Close releases the lock on the parent message. It does not close the underlying buffer.
|
|
// It is safe to call Close multiple times.
|
|
func (b *MessageBuffer) Close() {
|
|
if b.closed {
|
|
return
|
|
}
|
|
b.closed = true
|
|
b.parent.bufMutex.Unlock()
|
|
}
|
|
|
|
// Reset resets the buffer to the initial state, once again reading from the beginning of the underlying buffer.
|
|
func (b *MessageBuffer) Reset() error {
|
|
if b.closed {
|
|
return io.ErrClosedPipe
|
|
}
|
|
_, err := b.underlyingBuf.Seek(0, io.SeekStart)
|
|
return err
|
|
}
|
|
|
|
type message struct {
|
|
// immutable
|
|
uid imap.UID
|
|
underlyingBuf io.ReadSeekCloser
|
|
bufMutex sync.Mutex
|
|
len int64
|
|
t time.Time
|
|
id uuid.UUID
|
|
|
|
// mutable, protected by Mailbox.mutex
|
|
flags map[imap.Flag]struct{}
|
|
}
|
|
|
|
func (msg *message) buf() (*MessageBuffer, error) {
|
|
msg.bufMutex.Lock()
|
|
buf := &MessageBuffer{msg.underlyingBuf, false, msg}
|
|
err := buf.Reset()
|
|
if err != nil {
|
|
msg.bufMutex.Unlock()
|
|
return nil, err
|
|
}
|
|
return buf, nil
|
|
}
|
|
|
|
func (msg *message) fetch(w *imapserver.FetchResponseWriter, options *imap.FetchOptions) error {
|
|
w.WriteUID(msg.uid)
|
|
|
|
if options.Flags {
|
|
w.WriteFlags(msg.flagList())
|
|
}
|
|
if options.InternalDate {
|
|
w.WriteInternalDate(msg.t)
|
|
}
|
|
if options.RFC822Size {
|
|
w.WriteRFC822Size(msg.len)
|
|
}
|
|
if options.Envelope {
|
|
w.WriteEnvelope(msg.envelope())
|
|
}
|
|
if options.BodyStructure != nil {
|
|
buf, err := msg.buf()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
w.WriteBodyStructure(imapserver.ExtractBodyStructure(buf))
|
|
buf.Close()
|
|
}
|
|
|
|
for _, bs := range options.BodySection {
|
|
msgBuf, err := msg.buf()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
buf := imapserver.ExtractBodySection(msgBuf, bs)
|
|
wc := w.WriteBodySection(bs, int64(len(buf)))
|
|
_, writeErr := wc.Write(buf)
|
|
closeErr := wc.Close()
|
|
msgBuf.Close()
|
|
if writeErr != nil {
|
|
return writeErr
|
|
}
|
|
if closeErr != nil {
|
|
return closeErr
|
|
}
|
|
}
|
|
|
|
for _, bs := range options.BinarySection {
|
|
msgBuf, err := msg.buf()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
buf := imapserver.ExtractBinarySection(msgBuf, bs)
|
|
wc := w.WriteBinarySection(bs, int64(len(buf)))
|
|
_, writeErr := wc.Write(buf)
|
|
closeErr := wc.Close()
|
|
msgBuf.Close()
|
|
if writeErr != nil {
|
|
return writeErr
|
|
}
|
|
if closeErr != nil {
|
|
return closeErr
|
|
}
|
|
}
|
|
|
|
for _, bss := range options.BinarySectionSize {
|
|
buf, err := msg.buf()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
n := imapserver.ExtractBinarySectionSize(buf, bss)
|
|
w.WriteBinarySectionSize(bss, n)
|
|
buf.Close()
|
|
}
|
|
|
|
return w.Close()
|
|
}
|
|
|
|
func (msg *message) envelope() *imap.Envelope {
|
|
buf, err := msg.buf()
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
br := bufio.NewReader(buf)
|
|
header, err := textproto.ReadHeader(br)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
buf.Close()
|
|
return imapserver.ExtractEnvelope(header)
|
|
}
|
|
|
|
func (msg *message) flagList() []imap.Flag {
|
|
var flags []imap.Flag
|
|
for flag := range msg.flags {
|
|
flags = append(flags, flag)
|
|
}
|
|
return flags
|
|
}
|
|
|
|
func (msg *message) store(store *imap.StoreFlags) {
|
|
switch store.Op {
|
|
case imap.StoreFlagsSet:
|
|
msg.flags = make(map[imap.Flag]struct{})
|
|
fallthrough
|
|
case imap.StoreFlagsAdd:
|
|
for _, flag := range store.Flags {
|
|
msg.flags[canonicalFlag(flag)] = struct{}{}
|
|
}
|
|
case imap.StoreFlagsDel:
|
|
for _, flag := range store.Flags {
|
|
delete(msg.flags, canonicalFlag(flag))
|
|
}
|
|
default:
|
|
panic(fmt.Errorf("unknown STORE flag operation: %v", store.Op))
|
|
}
|
|
}
|
|
|
|
func (msg *message) reader() (*gomessage.Entity, error) {
|
|
buf, err := msg.buf()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
r, _ := gomessage.Read(buf)
|
|
buf.Close()
|
|
if r == nil {
|
|
r, _ = gomessage.New(gomessage.Header{}, bytes.NewReader(nil))
|
|
}
|
|
return r, nil
|
|
}
|
|
|
|
func (msg *message) search(seqNum uint32, criteria *imap.SearchCriteria) (bool, error) {
|
|
for _, seqSet := range criteria.SeqNum {
|
|
if seqNum == 0 || !seqSet.Contains(seqNum) {
|
|
return false, nil
|
|
}
|
|
}
|
|
for _, uidSet := range criteria.UID {
|
|
if !uidSet.Contains(msg.uid) {
|
|
return false, nil
|
|
}
|
|
}
|
|
if !matchDate(msg.t, criteria.Since, criteria.Before) {
|
|
return false, nil
|
|
}
|
|
|
|
for _, flag := range criteria.Flag {
|
|
if _, ok := msg.flags[canonicalFlag(flag)]; !ok {
|
|
return false, nil
|
|
}
|
|
}
|
|
for _, flag := range criteria.NotFlag {
|
|
if _, ok := msg.flags[canonicalFlag(flag)]; ok {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
if criteria.Larger != 0 && msg.len <= criteria.Larger {
|
|
return false, nil
|
|
}
|
|
if criteria.Smaller != 0 && msg.len >= criteria.Smaller {
|
|
return false, nil
|
|
}
|
|
|
|
msgReader, err := msg.reader()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
|
|
header := mail.Header{Header: msgReader.Header}
|
|
|
|
for _, fieldCriteria := range criteria.Header {
|
|
if !matchHeaderFields(header.FieldsByKey(fieldCriteria.Key), fieldCriteria.Value) {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
if !criteria.SentSince.IsZero() || !criteria.SentBefore.IsZero() {
|
|
t, err := header.Date()
|
|
if err != nil {
|
|
return false, err
|
|
} else if !matchDate(t, criteria.SentSince, criteria.SentBefore) {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
for _, text := range criteria.Text {
|
|
msgReader, err := msg.reader()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !matchEntity(msgReader, text, true) {
|
|
return false, nil
|
|
}
|
|
}
|
|
for _, body := range criteria.Body {
|
|
msgReader, err := msg.reader()
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if !matchEntity(msgReader, body, false) {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
for _, not := range criteria.Not {
|
|
search, err := msg.search(seqNum, ¬)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if search {
|
|
return false, nil
|
|
}
|
|
}
|
|
for _, or := range criteria.Or {
|
|
search, err := msg.search(seqNum, &or[0])
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
search2, err := msg.search(seqNum, &or[1])
|
|
if !search && !search2 {
|
|
return false, nil
|
|
}
|
|
}
|
|
|
|
return true, nil
|
|
}
|
|
|
|
func matchDate(t, since, before time.Time) bool {
|
|
// We discard time zone information by setting it to UTC.
|
|
// RFC 3501 explicitly requires zone unaware date comparison.
|
|
t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.UTC)
|
|
|
|
if !since.IsZero() && t.Before(since) {
|
|
return false
|
|
}
|
|
if !before.IsZero() && !t.Before(before) {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func matchHeaderFields(fields gomessage.HeaderFields, pattern string) bool {
|
|
if pattern == "" {
|
|
return fields.Len() > 0
|
|
}
|
|
|
|
pattern = strings.ToLower(pattern)
|
|
for fields.Next() {
|
|
v, _ := fields.Text()
|
|
if strings.Contains(strings.ToLower(v), pattern) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func matchEntity(e *gomessage.Entity, pattern string, includeHeader bool) bool {
|
|
if pattern == "" {
|
|
return true
|
|
}
|
|
|
|
if includeHeader && matchHeaderFields(e.Header.Fields(), pattern) {
|
|
return true
|
|
}
|
|
|
|
if mr := e.MultipartReader(); mr != nil {
|
|
for {
|
|
part, err := mr.NextPart()
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return false
|
|
}
|
|
|
|
if matchEntity(part, pattern, includeHeader) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
} else {
|
|
t, _, err := e.Header.ContentType()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
if !strings.HasPrefix(t, "text/") && !strings.HasPrefix(t, "message/") {
|
|
return false
|
|
}
|
|
|
|
buf, err := io.ReadAll(e.Body)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
|
|
return bytes.Contains(bytes.ToLower(buf), bytes.ToLower([]byte(pattern)))
|
|
}
|
|
}
|
|
|
|
func canonicalFlag(flag imap.Flag) imap.Flag {
|
|
return imap.Flag(strings.ToLower(string(flag)))
|
|
}
|