worker.go (4529B)
1 package imap 2 3 import ( 4 "crypto/tls" 5 "fmt" 6 "net/url" 7 "strings" 8 9 "github.com/emersion/go-imap" 10 "github.com/emersion/go-imap-idle" 11 "github.com/emersion/go-imap/client" 12 13 "git.sr.ht/~sircmpwn/aerc/worker/types" 14 ) 15 16 var errUnsupported = fmt.Errorf("unsupported command") 17 18 type imapClient struct { 19 *client.Client 20 idle *idle.IdleClient 21 } 22 23 type IMAPWorker struct { 24 config struct { 25 scheme string 26 insecure bool 27 addr string 28 user *url.Userinfo 29 } 30 31 client *imapClient 32 idleStop chan struct{} 33 idleDone chan error 34 selected imap.MailboxStatus 35 updates chan client.Update 36 worker *types.Worker 37 // Map of sequence numbers to UIDs, index 0 is seq number 1 38 seqMap []uint32 39 } 40 41 func NewIMAPWorker(worker *types.Worker) *IMAPWorker { 42 return &IMAPWorker{ 43 idleDone: make(chan error), 44 updates: make(chan client.Update, 50), 45 worker: worker, 46 } 47 } 48 49 func (w *IMAPWorker) handleMessage(msg types.WorkerMessage) error { 50 if w.idleStop != nil { 51 close(w.idleStop) 52 if err := <-w.idleDone; err != nil { 53 w.worker.PostMessage(&types.Error{Error: err}, nil) 54 } 55 } 56 57 switch msg := msg.(type) { 58 case *types.Unsupported: 59 // No-op 60 case *types.Configure: 61 u, err := url.Parse(msg.Config.Source) 62 if err != nil { 63 return err 64 } 65 66 w.config.scheme = u.Scheme 67 if strings.HasSuffix(w.config.scheme, "+insecure") { 68 w.config.scheme = strings.TrimSuffix(w.config.scheme, "+insecure") 69 w.config.insecure = true 70 } 71 72 w.config.addr = u.Host 73 if !strings.ContainsRune(w.config.addr, ':') { 74 w.config.addr += ":" + w.config.scheme 75 } 76 77 w.config.user = u.User 78 case *types.Connect: 79 var ( 80 c *client.Client 81 err error 82 ) 83 switch w.config.scheme { 84 case "imap": 85 c, err = client.Dial(w.config.addr) 86 if err != nil { 87 return err 88 } 89 90 if !w.config.insecure { 91 if err := c.StartTLS(&tls.Config{}); err != nil { 92 return err 93 } 94 } 95 case "imaps": 96 c, err = client.DialTLS(w.config.addr, &tls.Config{}) 97 if err != nil { 98 return err 99 } 100 default: 101 return fmt.Errorf("Unknown IMAP scheme %s", w.config.scheme) 102 } 103 104 if w.config.user != nil { 105 username := w.config.user.Username() 106 password, hasPassword := w.config.user.Password() 107 if !hasPassword { 108 // TODO: ask password 109 } 110 if err := c.Login(username, password); err != nil { 111 return err 112 } 113 } 114 115 c.SetDebug(w.worker.Logger.Writer()) 116 117 if _, err := c.Select(imap.InboxName, false); err != nil { 118 return err 119 } 120 121 c.Updates = w.updates 122 w.client = &imapClient{c, idle.NewClient(c)} 123 w.worker.PostMessage(&types.Done{types.RespondTo(msg)}, nil) 124 case *types.ListDirectories: 125 w.handleListDirectories(msg) 126 case *types.OpenDirectory: 127 w.handleOpenDirectory(msg) 128 case *types.FetchDirectoryContents: 129 w.handleFetchDirectoryContents(msg) 130 case *types.FetchMessageHeaders: 131 w.handleFetchMessageHeaders(msg) 132 case *types.FetchMessageBodyPart: 133 w.handleFetchMessageBodyPart(msg) 134 case *types.FetchFullMessages: 135 w.handleFetchFullMessages(msg) 136 case *types.DeleteMessages: 137 w.handleDeleteMessages(msg) 138 case *types.CopyMessages: 139 w.handleCopyMessages(msg) 140 case *types.AppendMessage: 141 w.handleAppendMessage(msg) 142 default: 143 return errUnsupported 144 } 145 146 if w.idleStop != nil { 147 w.idleStop = make(chan struct{}) 148 go func() { 149 w.idleDone <- w.client.idle.IdleWithFallback(w.idleStop, 0) 150 }() 151 } 152 return nil 153 } 154 155 func (w *IMAPWorker) handleImapUpdate(update client.Update) { 156 w.worker.Logger.Printf("(= %T", update) 157 switch update := update.(type) { 158 case *client.MailboxUpdate: 159 status := update.Mailbox 160 if w.selected.Name == status.Name { 161 w.selected = *status 162 } 163 w.worker.PostMessage(&types.DirectoryInfo{ 164 Flags: status.Flags, 165 Name: status.Name, 166 ReadOnly: status.ReadOnly, 167 168 Exists: int(status.Messages), 169 Recent: int(status.Recent), 170 Unseen: int(status.Unseen), 171 }, nil) 172 case *client.ExpungeUpdate: 173 i := update.SeqNum - 1 174 uid := w.seqMap[i] 175 w.seqMap = append(w.seqMap[:i], w.seqMap[i+1:]...) 176 w.worker.PostMessage(&types.MessagesDeleted{ 177 Uids: []uint32{uid}, 178 }, nil) 179 } 180 } 181 182 func (w *IMAPWorker) Run() { 183 for { 184 select { 185 case msg := <-w.worker.Actions: 186 msg = w.worker.ProcessAction(msg) 187 if err := w.handleMessage(msg); err == errUnsupported { 188 w.worker.PostMessage(&types.Unsupported{ 189 Message: types.RespondTo(msg), 190 }, nil) 191 } else if err != nil { 192 w.worker.PostMessage(&types.Error{ 193 Message: types.RespondTo(msg), 194 Error: err, 195 }, nil) 196 } 197 case update := <-w.updates: 198 w.handleImapUpdate(update) 199 } 200 } 201 }