init: pristine aerc 0.20.0 source
This commit is contained in:
@@ -0,0 +1,397 @@
|
||||
package imap
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"time"
|
||||
|
||||
"github.com/emersion/go-imap"
|
||||
sortthread "github.com/emersion/go-imap-sortthread"
|
||||
"github.com/emersion/go-imap/client"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
|
||||
"git.sr.ht/~rjarry/aerc/lib"
|
||||
"git.sr.ht/~rjarry/aerc/models"
|
||||
"git.sr.ht/~rjarry/aerc/worker/handlers"
|
||||
"git.sr.ht/~rjarry/aerc/worker/imap/extensions"
|
||||
"git.sr.ht/~rjarry/aerc/worker/middleware"
|
||||
"git.sr.ht/~rjarry/aerc/worker/types"
|
||||
)
|
||||
|
||||
func init() {
|
||||
handlers.RegisterWorkerFactory("imap", NewIMAPWorker)
|
||||
handlers.RegisterWorkerFactory("imaps", NewIMAPWorker)
|
||||
}
|
||||
|
||||
var (
|
||||
errUnsupported = fmt.Errorf("unsupported command")
|
||||
errClientNotReady = fmt.Errorf("client not ready")
|
||||
errNotConnected = fmt.Errorf("not connected")
|
||||
errAlreadyConnected = fmt.Errorf("already connected")
|
||||
)
|
||||
|
||||
type imapClient struct {
|
||||
*client.Client
|
||||
thread *sortthread.ThreadClient
|
||||
sort *sortthread.SortClient
|
||||
liststatus *extensions.ListStatusClient
|
||||
}
|
||||
|
||||
type imapConfig struct {
|
||||
name string
|
||||
scheme string
|
||||
insecure bool
|
||||
addr string
|
||||
user *url.Userinfo
|
||||
headers []string
|
||||
headersExclude []string
|
||||
folders []string
|
||||
oauthBearer lib.OAuthBearer
|
||||
xoauth2 lib.Xoauth2
|
||||
idle_timeout time.Duration
|
||||
idle_debounce time.Duration
|
||||
reconnect_maxwait time.Duration
|
||||
// tcp connection parameters
|
||||
connection_timeout time.Duration
|
||||
keepalive_period time.Duration
|
||||
keepalive_probes int
|
||||
keepalive_interval int
|
||||
cacheEnabled bool
|
||||
cacheMaxAge time.Duration
|
||||
useXGMEXT bool
|
||||
}
|
||||
|
||||
type IMAPWorker struct {
|
||||
config imapConfig
|
||||
|
||||
client *imapClient
|
||||
selected *imap.MailboxStatus
|
||||
updates chan client.Update
|
||||
worker types.WorkerInteractor
|
||||
seqMap SeqMap
|
||||
delimiter string
|
||||
|
||||
idler *idler
|
||||
observer *observer
|
||||
cache *leveldb.DB
|
||||
|
||||
caps *models.Capabilities
|
||||
|
||||
threadAlgorithm sortthread.ThreadAlgorithm
|
||||
liststatus bool
|
||||
|
||||
executeIdle chan struct{}
|
||||
}
|
||||
|
||||
func NewIMAPWorker(worker *types.Worker) (types.Backend, error) {
|
||||
return &IMAPWorker{
|
||||
updates: make(chan client.Update, 50),
|
||||
worker: worker,
|
||||
selected: &imap.MailboxStatus{},
|
||||
idler: nil, // will be set in configure()
|
||||
observer: nil, // will be set in configure()
|
||||
caps: &models.Capabilities{},
|
||||
executeIdle: make(chan struct{}),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *IMAPWorker) newClient(c *client.Client) {
|
||||
c.Updates = nil
|
||||
w.client = &imapClient{
|
||||
c,
|
||||
sortthread.NewThreadClient(c),
|
||||
sortthread.NewSortClient(c),
|
||||
extensions.NewListStatusClient(c),
|
||||
}
|
||||
if w.idler != nil {
|
||||
w.idler.SetClient(w.client)
|
||||
c.Updates = w.updates
|
||||
}
|
||||
if w.observer != nil {
|
||||
w.observer.SetClient(w.client)
|
||||
}
|
||||
sort, err := w.client.sort.SupportSort()
|
||||
if err == nil && sort {
|
||||
w.caps.Sort = true
|
||||
w.worker.Debugf("Server Capability found: Sort")
|
||||
}
|
||||
for _, alg := range []sortthread.ThreadAlgorithm{sortthread.References, sortthread.OrderedSubject} {
|
||||
ok, err := w.client.Support(fmt.Sprintf("THREAD=%s", string(alg)))
|
||||
if err == nil && ok {
|
||||
w.threadAlgorithm = alg
|
||||
w.caps.Thread = true
|
||||
w.worker.Debugf("Server Capability found: Thread (algorithm: %s)", string(alg))
|
||||
break
|
||||
}
|
||||
}
|
||||
lStatus, err := w.client.liststatus.SupportListStatus()
|
||||
if err == nil && lStatus {
|
||||
w.liststatus = true
|
||||
w.caps.Extensions = append(w.caps.Extensions, "LIST-STATUS")
|
||||
w.worker.Debugf("Server Capability found: LIST-STATUS")
|
||||
}
|
||||
xgmext, err := w.client.Support("X-GM-EXT-1")
|
||||
if err == nil && xgmext && w.config.useXGMEXT {
|
||||
w.caps.Extensions = append(w.caps.Extensions, "X-GM-EXT-1")
|
||||
w.worker.Debugf("Server Capability found: X-GM-EXT-1")
|
||||
w.worker = middleware.NewGmailWorker(w.worker, w.client.Client)
|
||||
}
|
||||
if err == nil && !xgmext && w.config.useXGMEXT {
|
||||
w.worker.Infof("X-GM-EXT-1 requested, but it is not supported")
|
||||
}
|
||||
}
|
||||
|
||||
func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error {
|
||||
var reterr error // will be returned at the end, needed to support idle
|
||||
|
||||
// when client is nil allow only certain messages to be handled
|
||||
if w.client == nil {
|
||||
switch msg.(type) {
|
||||
case *types.Connect, *types.Reconnect, *types.Disconnect, *types.Configure:
|
||||
default:
|
||||
return errClientNotReady
|
||||
}
|
||||
}
|
||||
|
||||
// set connection timeout for calls to imap server
|
||||
if w.client != nil {
|
||||
w.client.Timeout = w.config.connection_timeout
|
||||
}
|
||||
|
||||
switch msg := msg.(type) {
|
||||
case *types.Unsupported:
|
||||
// No-op
|
||||
case *types.Configure:
|
||||
reterr = w.handleConfigure(msg)
|
||||
case *types.Connect:
|
||||
if w.client != nil && w.client.State() == imap.SelectedState {
|
||||
if !w.observer.AutoReconnect() {
|
||||
w.observer.SetAutoReconnect(true)
|
||||
w.observer.EmitIfNotConnected()
|
||||
}
|
||||
reterr = errAlreadyConnected
|
||||
break
|
||||
}
|
||||
|
||||
w.observer.SetAutoReconnect(true)
|
||||
c, err := w.connect()
|
||||
if err != nil {
|
||||
w.observer.EmitIfNotConnected()
|
||||
reterr = err
|
||||
break
|
||||
}
|
||||
|
||||
w.newClient(c)
|
||||
|
||||
w.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil)
|
||||
case *types.Reconnect:
|
||||
if !w.observer.AutoReconnect() {
|
||||
reterr = fmt.Errorf("auto-reconnect is disabled; run connect to enable it")
|
||||
break
|
||||
}
|
||||
c, err := w.connect()
|
||||
if err != nil {
|
||||
errReconnect := w.observer.DelayedReconnect()
|
||||
reterr = errors.Wrap(errReconnect, err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
w.newClient(c)
|
||||
|
||||
w.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil)
|
||||
case *types.Disconnect:
|
||||
w.observer.SetAutoReconnect(false)
|
||||
w.observer.Stop()
|
||||
|
||||
if w.client == nil || (w.client != nil && w.client.State() != imap.SelectedState) {
|
||||
reterr = errNotConnected
|
||||
break
|
||||
}
|
||||
|
||||
if err := w.client.Logout(); err != nil {
|
||||
w.terminate()
|
||||
reterr = err
|
||||
break
|
||||
}
|
||||
w.worker.PostMessage(&types.Done{Message: types.RespondTo(msg)}, nil)
|
||||
case *types.ListDirectories:
|
||||
w.handleListDirectories(msg)
|
||||
case *types.OpenDirectory:
|
||||
w.handleOpenDirectory(msg)
|
||||
case *types.FetchDirectoryContents:
|
||||
w.handleFetchDirectoryContents(msg)
|
||||
case *types.FetchDirectoryThreaded:
|
||||
w.handleDirectoryThreaded(msg)
|
||||
case *types.CreateDirectory:
|
||||
w.handleCreateDirectory(msg)
|
||||
case *types.RemoveDirectory:
|
||||
w.handleRemoveDirectory(msg)
|
||||
case *types.FetchMessageHeaders:
|
||||
w.handleFetchMessageHeaders(msg)
|
||||
case *types.FetchMessageBodyPart:
|
||||
w.handleFetchMessageBodyPart(msg)
|
||||
case *types.FetchFullMessages:
|
||||
w.handleFetchFullMessages(msg)
|
||||
case *types.FetchMessageFlags:
|
||||
w.handleFetchMessageFlags(msg)
|
||||
case *types.DeleteMessages:
|
||||
w.handleDeleteMessages(msg)
|
||||
case *types.FlagMessages:
|
||||
w.handleFlagMessages(msg)
|
||||
case *types.AnsweredMessages:
|
||||
w.handleAnsweredMessages(msg)
|
||||
case *types.CopyMessages:
|
||||
w.handleCopyMessages(msg)
|
||||
case *types.MoveMessages:
|
||||
w.handleMoveMessages(msg)
|
||||
case *types.AppendMessage:
|
||||
w.handleAppendMessage(msg)
|
||||
case *types.SearchDirectory:
|
||||
w.handleSearchDirectory(msg)
|
||||
case *types.CheckMail:
|
||||
w.handleCheckMailMessage(msg)
|
||||
default:
|
||||
reterr = errUnsupported
|
||||
}
|
||||
|
||||
// we don't want idle to timeout, so set timeout to zero
|
||||
if w.client != nil {
|
||||
w.client.Timeout = 0
|
||||
}
|
||||
|
||||
return reterr
|
||||
}
|
||||
|
||||
func (w *IMAPWorker) handleImapUpdate(update client.Update) {
|
||||
w.worker.Tracef("(= %T", update)
|
||||
switch update := update.(type) {
|
||||
case *client.MailboxUpdate:
|
||||
w.worker.PostAction(&types.CheckMail{
|
||||
Directories: []string{update.Mailbox.Name},
|
||||
}, nil)
|
||||
case *client.MessageUpdate:
|
||||
msg := update.Message
|
||||
if msg.Uid == 0 {
|
||||
if uid, found := w.seqMap.Get(msg.SeqNum); !found {
|
||||
w.worker.Errorf("MessageUpdate unknown seqnum: %d", msg.SeqNum)
|
||||
return
|
||||
} else {
|
||||
msg.Uid = uid
|
||||
}
|
||||
}
|
||||
if int(msg.SeqNum) > w.seqMap.Size() {
|
||||
w.seqMap.Put(msg.Uid)
|
||||
}
|
||||
w.worker.PostMessage(&types.MessageInfo{
|
||||
Info: &models.MessageInfo{
|
||||
BodyStructure: translateBodyStructure(msg.BodyStructure),
|
||||
Envelope: translateEnvelope(msg.Envelope),
|
||||
Flags: translateImapFlags(msg.Flags),
|
||||
InternalDate: msg.InternalDate,
|
||||
Uid: models.Uint32ToUid(msg.Uid),
|
||||
},
|
||||
}, nil)
|
||||
case *client.ExpungeUpdate:
|
||||
if uid, found := w.seqMap.Pop(update.SeqNum); !found {
|
||||
w.worker.Errorf("ExpungeUpdate unknown seqnum: %d", update.SeqNum)
|
||||
} else {
|
||||
w.worker.PostMessage(&types.MessagesDeleted{
|
||||
Uids: []models.UID{models.Uint32ToUid(uid)},
|
||||
}, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *IMAPWorker) terminate() {
|
||||
if w.observer != nil {
|
||||
w.observer.Stop()
|
||||
w.observer.SetClient(nil)
|
||||
}
|
||||
|
||||
if w.client != nil {
|
||||
w.client.Updates = nil
|
||||
if err := w.client.Terminate(); err != nil {
|
||||
w.worker.Errorf("could not terminate connection: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
w.client = nil
|
||||
w.selected = &imap.MailboxStatus{}
|
||||
|
||||
if w.idler != nil {
|
||||
w.idler.SetClient(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *IMAPWorker) stopIdler() error {
|
||||
if w.idler == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := w.idler.Stop(); err != nil {
|
||||
w.terminate()
|
||||
w.observer.EmitIfNotConnected()
|
||||
w.worker.Errorf("idler stopped with error:%v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w *IMAPWorker) startIdler() {
|
||||
if w.idler == nil {
|
||||
return
|
||||
}
|
||||
|
||||
w.idler.Start()
|
||||
}
|
||||
|
||||
func (w *IMAPWorker) Run() {
|
||||
for {
|
||||
select {
|
||||
case msg := <-w.worker.Actions():
|
||||
|
||||
if err := w.stopIdler(); err != nil {
|
||||
w.worker.PostMessage(&types.Error{
|
||||
Message: types.RespondTo(msg),
|
||||
Error: err,
|
||||
}, nil)
|
||||
break
|
||||
}
|
||||
w.worker.Tracef("ready to handle %T", msg)
|
||||
|
||||
msg = w.worker.ProcessAction(msg)
|
||||
|
||||
if err := w.handleMessage(msg); errors.Is(err, errUnsupported) {
|
||||
w.worker.PostMessage(&types.Unsupported{
|
||||
Message: types.RespondTo(msg),
|
||||
}, nil)
|
||||
} else if err != nil {
|
||||
w.worker.PostMessage(&types.Error{
|
||||
Message: types.RespondTo(msg),
|
||||
Error: err,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
w.startIdler()
|
||||
|
||||
case update := <-w.updates:
|
||||
w.handleImapUpdate(update)
|
||||
|
||||
case <-w.executeIdle:
|
||||
w.idler.Execute()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (w *IMAPWorker) Capabilities() *models.Capabilities {
|
||||
return w.caps
|
||||
}
|
||||
|
||||
func (w *IMAPWorker) PathSeparator() string {
|
||||
if w.delimiter == "" {
|
||||
return "/"
|
||||
}
|
||||
return w.delimiter
|
||||
}
|
||||
Reference in New Issue
Block a user