async def do_create(self, cmd: CreateCommand) -> _CommandRet: if cmd.mailbox == 'INBOX': return ResponseNo(cmd.tag, b'Cannot create INBOX.'), None mailbox_id, updates = await self.session.create_mailbox( cmd.mailbox, selected=self._selected) return ResponseOk(cmd.tag, cmd.command + b' completed.', MailboxId(mailbox_id)), updates
async def do_append(self, cmd: AppendCommand) -> _CommandRet: if len(cmd.messages) > 1 and b'MULTIAPPEND' not in self.capability: raise NotSupportedError('MULTIAPPEND is disabled.') if cmd.cancelled: return ResponseNo(cmd.tag, b'APPEND cancelled.'), None append_uid, updates = await self.session.append_messages( cmd.mailbox, cmd.messages, selected=self._selected) resp = ResponseOk(cmd.tag, cmd.command + b' completed.', append_uid) return resp, updates
async def do_authenticate(self, cmd: _AuthCommands, creds: Optional[AuthenticationCredentials]) \ -> CommandResponse: if not creds: return ResponseNo(cmd.tag, b'Invalid authentication mechanism.') self._session = await self._login(creds) self._capability.extend(self.config.login_capability) return ResponseOk(cmd.tag, b'Authentication successful.', self.capability)
async def do_command(self, cmd: Command) -> CommandResponse: if isinstance(cmd, InvalidCommand): return ResponseBad(cmd.tag, cmd.message) elif self._session and isinstance(cmd, CommandNonAuth): msg = cmd.command + b': Already authenticated.' return ResponseBad(cmd.tag, msg) elif not self._session and isinstance(cmd, CommandAuth): msg = cmd.command + b': Must authenticate first.' return ResponseBad(cmd.tag, msg) elif not self._selected and isinstance(cmd, CommandSelect): msg = cmd.command + b': Must select a mailbox first.' return ResponseBad(cmd.tag, msg) func_name = self._get_func_name(cmd) try: func: _CommandFunc = getattr(self, func_name) except AttributeError: return ResponseNo(cmd.tag, cmd.command + b': Not Implemented') response, selected = await func(cmd) if selected is not None: self._selected, untagged = selected.fork(cmd) response.add_untagged(*untagged) return response
async def do_rename(self, cmd: RenameCommand) -> _CommandRet: if cmd.to_mailbox == 'INBOX': return ResponseNo(cmd.tag, b'Cannot rename to INBOX.'), None updates = await self.session.rename_mailbox( cmd.from_mailbox, cmd.to_mailbox, selected=self._selected) return ResponseOk(cmd.tag, cmd.command + b' completed.'), updates
async def do_delete(self, cmd: DeleteCommand) -> _CommandRet: if cmd.mailbox == 'INBOX': return ResponseNo(cmd.tag, b'Cannot delete INBOX.'), None updates = await self.session.delete_mailbox( cmd.mailbox, selected=self._selected) return ResponseOk(cmd.tag, cmd.command + b' completed.'), updates
async def run(self, state: ConnectionState) -> None: """Start the socket communication with the IMAP greeting, and then enter the command/response cycle. Args: state: Defines the interaction with the backend plugin. """ self._print('%d +++| %s', bytes(socket_info.get())) bad_commands = 0 try: greeting = await self._exec(state.do_greeting()) except ResponseError as exc: resp = exc.get_response(b'*') resp.condition = ResponseBye.condition await self.write_response(resp) return else: await self.write_response(greeting) while True: try: cmd = await self.read_command(state) except (ConnectionError, EOFError): break except CancelledError: await self.send_error_disconnect() break except Exception: await self.send_error_disconnect() raise else: prev_cmd = current_command.set(cmd) try: if isinstance(cmd, AuthenticateCommand): creds = await self.authenticate(state, cmd.mech_name) response = await self._exec( state.do_authenticate(cmd, creds)) elif isinstance(cmd, IdleCommand): response = await self.idle(state, cmd) else: response = await self._exec(state.do_command(cmd)) except ResponseError as exc: resp = exc.get_response(cmd.tag) await self.write_response(resp) if resp.is_terminal: break except AuthenticationError as exc: msg = bytes(str(exc), 'utf-8', 'surrogateescape') resp = ResponseBad(cmd.tag, msg) await self.write_response(resp) except TimeoutError: resp = ResponseNo(cmd.tag, b'Operation timed out.', ResponseCode.of(b'TIMEOUT')) await self.write_response(resp) except CancelledError: await self.send_error_disconnect() break except Exception: await self.send_error_disconnect() raise else: await self.write_response(response) if response.is_bad: bad_commands += 1 if self.bad_command_limit \ and bad_commands >= self.bad_command_limit: msg = b'Too many errors, disconnecting.' response.add_untagged(ResponseBye(msg)) else: bad_commands = 0 if response.is_terminal: break if isinstance(cmd, StartTLSCommand) and state.ssl_context \ and isinstance(response, ResponseOk): await self.start_tls(state.ssl_context) finally: await state.do_cleanup() current_command.reset(prev_cmd) self._print('%d ---| %s', b'<disconnected>')
def test_bytes(self): resp1 = ResponseNo(b'tag', b'invalid response') self.assertEqual(b'tag NO invalid response\r\n', bytes(resp1)) resp2 = ResponseNo(b'tag', b'invalid response', _alert) self.assertEqual(b'tag NO [ALERT] invalid response\r\n', bytes(resp2))