# aioimaplib : an IMAPrev4 lib using python asyncio # Copyright (C) 2016 Bruno Thomas # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation, either version 3 of the License, or # (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. import asyncio import email import email.mime.nonmultipart import logging import re import sys import uuid from collections import deque from copy import deepcopy from datetime import datetime, timedelta from email._policybase import Compat32 from email.header import Header from email.message import Message from functools import update_wrapper from math import ceil from operator import attrgetter from pytz import utc log = logging.getLogger(__name__) log.setLevel(logging.INFO) sh = logging.StreamHandler() sh.setLevel(logging.INFO) sh.setFormatter(logging.Formatter("%(asctime)s %(levelname)s " + "[%(module)s:%(lineno)d] %(message)s")) log.addHandler(sh) NONAUTH, AUTH, SELECTED, IDLE, LOGOUT = 'NONAUTH', 'AUTH', 'SELECTED', 'IDLE', 'LOGOUT' UID_RANGE_RE = re.compile(r'(?P<start>\d+):(?P<end>\d|\*)') CAPABILITIES = 'IDLE UIDPLUS MOVE ENABLE NAMESPACE' CRLF = b'\r\n' class InvalidUidSet(RuntimeError): def __init__(self, *args) -> None: super().__init__(*args) class ServerState(object): DEFAULT_MAILBOXES = ['INBOX', 'Trash', 'Sent', 'Drafts'] def __init__(self): self.mailboxes = dict() self.connections = dict() self.subcriptions = dict() def reset(self): self.mailboxes = dict() for connection in self.connections.values(): connection.transport.close() self.connections = dict() def add_mail(self, to, mail, mailbox='INBOX'): if to not in self.mailboxes: self.mailboxes[to] = dict() if mailbox not in self.mailboxes[to]: self.mailboxes[to][mailbox] = list() m = deepcopy(mail) m.id = len(self.mailboxes[to][mailbox]) + 1 m.uid = self.max_uid(to, mailbox) + 1 self.mailboxes[to][mailbox].append(m) return m.uid def max_uid(self, user, mailbox): if user not in self.mailboxes or mailbox not in self.mailboxes[user] \ or len(self.mailboxes[user][mailbox]) == 0: return 0 return max(self.mailboxes[user][mailbox], key=lambda msg: msg.uid).uid def max_id(self, user, mailbox): if user not in self.mailboxes or mailbox not in self.mailboxes[user]: return 0 return len(self.mailboxes[user][mailbox]) def login(self, user_login, protocol): if user_login not in self.mailboxes: self.mailboxes[user_login] = dict() for mb in self.DEFAULT_MAILBOXES: self.create_mailbox_if_not_exists(user_login, mb) if user_login not in self.connections: self.connections[user_login] = protocol if user_login not in self.subcriptions: self.subcriptions[user_login] = set() def create_mailbox_if_not_exists(self, user_login, user_mailbox): if user_mailbox not in self.mailboxes[user_login]: self.mailboxes[user_login][user_mailbox] = list() def get_mailbox_messages(self, user_login, user_mailbox): return self.mailboxes[user_login].get(user_mailbox) def imap_receive(self, user, mail, mailbox): uid = self.add_mail(user, mail, mailbox) log.debug('created mail with UID: %s' % uid) if user in self.connections: self.connections[user].notify_new_mail(uid) return uid def get_connection(self, user): return self.connections.get(user) def subscribe(self, user, mailbox): self.subcriptions[user].add(mailbox) def unsubscribe(self, user, mailbox): self.subcriptions[user].remove(mailbox) def lsub(self, user, mailbox_search): mb_re = re.compile(mailbox_search) return [mb for mb in self.subcriptions[user] if mb_re.match(mb)] def list(self, user, reference, mailbox_pattern): mb = self.mailboxes[user] for path_item in reference.split('/'): mb = self.mailboxes[user].get(path_item, self.mailboxes[user]) mb_re = re.compile(mailbox_pattern) return sorted([mb for mb in mb.keys() if mb_re.match(mb)]) def remove(self, message, user, mailbox): self.remove_byid(user, mailbox, message.id) def delete_mailbox(self, user, mailbox): if mailbox in self.mailboxes[user]: del self.mailboxes[user][mailbox] def rename_mailbox(self, user, old_mb, new_mb): if old_mb in self.mailboxes[user]: mb = self.mailboxes[user].pop(old_mb) self.mailboxes[user][new_mb] = mb def copy(self, user, src_mailbox, dest_mailbox, message_set): to_copy = [msg for msg in self.mailboxes[user][src_mailbox] if str(msg.id) in message_set] if dest_mailbox not in self.mailboxes[user]: self.mailboxes[user][dest_mailbox] = list() self.mailboxes[user][dest_mailbox] += to_copy def move(self, user, src_mailbox, dest_mailbox, id_range, msg_attribute): id_getter = attrgetter(msg_attribute) to_move = [msg for msg in self.mailboxes[user][src_mailbox] if id_getter(msg) in id_range] id_moved = [] for msg in to_move: self.remove(msg, user, src_mailbox) id_moved.append(self.add_mail(user, msg, dest_mailbox)) if len(id_moved) == 0: id_moved.append(0) return range(min(id_moved), max(id_moved) + 1) def remove_byid(self, user, mailbox, id): msg = self.mailboxes[user][mailbox].pop(id-1) self._reindex(user, mailbox) return msg def _reindex(self, user, mailbox): for idx, msg in enumerate(self.mailboxes[user][mailbox]): msg.id = idx + 1 def critical_section(next_state): @asyncio.coroutine def execute_section(self, state, critical_func, *args, **kwargs): with (yield from self.state_condition): critical_func(self, *args, **kwargs) self.state = state log.debug('state -> %s' % state) self.state_condition.notify_all() def decorator(func): def wrapper(self, *args, **kwargs): asyncio.ensure_future(execute_section(self, next_state, func, *args, **kwargs)) return update_wrapper(wrapper, func) return decorator command_re = re.compile(br'((DONE)|(?P<tag>\w+) (?P<cmd>[\w]+)([\w \.#@:\*"\(\)\{\}\[\]\+\-\\\%]+)?$)') FETCH_HEADERS_RE = re.compile(r'.*BODY.PEEK\[HEADER.FIELDS \((?P<headers>.+)\)\].*') class ImapProtocol(asyncio.Protocol): IDLE_STILL_HERE_PERIOD_SECONDS = 10 def __init__(self, server_state, fetch_chunk_size=0, capabilities=CAPABILITIES, loop=asyncio.get_event_loop()): self.uidvalidity = int(datetime.now().timestamp()) self.capabilities = capabilities self.state_to_send = list() self.delay_seconds = 0 self.loop = loop self.fetch_chunk_size = fetch_chunk_size self.transport = None self.server_state = server_state self.user_login = None self.user_mailbox = None self.idle_tag = None self.idle_task = None self.state = NONAUTH self.state_condition = asyncio.Condition() self.append_literal_command = None def connection_made(self, transport): self.transport = transport transport.write('* OK IMAP4rev1 MockIMAP Server ready\r\n'.encode()) def data_received(self, data): if self.append_literal_command is not None: self.append_literal(data) return for cmd_line in data.splitlines(): if command_re.match(cmd_line) is None: self.send_untagged_line('BAD Error in IMAP command : Unknown command (%r).' % cmd_line) else: command_array = cmd_line.decode().rstrip().split() if self.state is not IDLE: tag = command_array[0] self.exec_command(tag, command_array[1:]) else: self.exec_command(None, command_array) def connection_lost(self, error): if error: log.error(error) if self.idle_task is not None: self.idle_task.cancel() self.transport.close() def exec_command(self, tag, command_array): command = command_array[0].lower() parameters = command_array[1:] if command == 'uid': command = command_array[1].lower() parameters = ['uid'] + command_array[2:] if not hasattr(self, command): return self.error(tag, 'Command "%s" not implemented' % command) self.loop.call_later(self.delay_seconds, lambda: getattr(self, command)(tag, *parameters)) def send_untagged_line(self, response, encoding='utf-8', continuation=False, max_chunk_size=0): self.send_raw_untagged_line(response.encode(encoding=encoding), continuation, max_chunk_size) def send_raw_untagged_line(self, raw_response, continuation=False, max_chunk_size=0): prefix = b'+ ' if continuation else b'* ' raw_line = prefix + raw_response + CRLF if max_chunk_size: for nb_chunk in range(ceil(len(raw_line) / max_chunk_size)): chunk_start_index = nb_chunk * max_chunk_size self.send(raw_line[chunk_start_index:chunk_start_index + max_chunk_size]) else: self.send(raw_line) def send_tagged_line(self, tag, response): self.send('{tag} {response}\r\n'.format(tag=tag, response=response).encode()) def send(self, _bytes): log.debug("Sending %r", _bytes) self.transport.write(_bytes) @critical_section(next_state=AUTH) def login(self, tag, *args): self.user_login = args[0] self.server_state.login(self.user_login, self) self.send_untagged_line('CAPABILITY IMAP4rev1 %s' % self.capabilities) self.send_tagged_line(tag, 'OK LOGIN completed') @critical_section(next_state=LOGOUT) def logout(self, tag, *args): self.server_state.login(self.user_login, self) self.send_untagged_line('BYE Logging out') self.send_tagged_line(tag, 'OK LOGOUT completed') self.transport.close() @critical_section(next_state=SELECTED) def select(self, tag, *args): self.user_mailbox = args[0] self.examine(tag, *args) @critical_section(next_state=IDLE) def idle(self, tag, *args): log.debug("Entering idle for '%s'", self.user_login) self.idle_tag = tag def still_here(): self.send_untagged_line('OK Still here') self.idle_task = self.loop.call_later(self.IDLE_STILL_HERE_PERIOD_SECONDS, still_here) self.idle_task = self.loop.call_later(self.IDLE_STILL_HERE_PERIOD_SECONDS, still_here) self.send_untagged_line('idling', continuation=True) @critical_section(next_state=SELECTED) def done(self, _, *args): self.send_tagged_line(self.idle_tag, 'OK IDLE terminated') self.idle_task.cancel() self.idle_task = None self.idle_tag = None @critical_section(next_state=AUTH) def close(self, tag, *args): self.user_mailbox = None self.send_tagged_line(tag, 'OK CLOSE completed.') @asyncio.coroutine def wait(self, state): with (yield from self.state_condition): yield from self.state_condition.wait_for(lambda: self.state == state) def examine(self, tag, *args): mailbox_name = args[0] self.server_state.create_mailbox_if_not_exists(self.user_login, mailbox_name) mailbox = self.server_state.get_mailbox_messages(self.user_login, mailbox_name) self.send_untagged_line('FLAGS (\Answered \Flagged \Deleted \Seen \Draft)') self.send_untagged_line('OK [PERMANENTFLAGS (\Answered \Flagged \Deleted \Seen \Draft \*)] Flags permitted.') self.send_untagged_line('{nb_messages} EXISTS'.format(nb_messages=len(mailbox))) self.send_untagged_line('{nb_messages} RECENT'.format(nb_messages=0)) self.send_untagged_line('OK [UIDVALIDITY {uidvalidity}] UIDs valid'.format(uidvalidity=self.uidvalidity)) self.send_untagged_line('OK [UIDNEXT {next_uid}] Predicted next UID'.format( next_uid=self.server_state.max_uid(self.user_login, mailbox_name) + 1)) self.send_tagged_line(tag, 'OK [READ] Select completed (0.000 secs).') def search(self, tag, *args_param): args = list(args_param) by_uid = False args.reverse() if args[-1] == 'uid': args.pop() by_uid = True charset, keyword, unkeyword, older, younger, range_ = None, None, None, None, None, None if args and 'CHARSET' == args[-1].upper(): args.pop() charset = args.pop() if args and 'KEYWORD' == args[-1].upper(): args.pop() keyword = args.pop() if args and 'UNKEYWORD' == args[-1].upper(): args.pop() unkeyword = args.pop() if args and 'OLDER' == args[-1].upper(): args.pop() older = int(args.pop()) if args and 'YOUNGER' == args[-1].upper(): args.pop() younger = int(args.pop()) match_range = None if len(args) == 0 else UID_RANGE_RE.match(args[-1]) if match_range: args.pop() start = int(match_range.group('start')) if match_range.group('end') == '*': end = sys.maxsize else: end = int(match_range.group('end')) + 1 range_ = range(start, end) all = 'ALL' in args self.send_untagged_line( 'SEARCH {msg_uids}'.format(msg_uids=' '.join( self.memory_search(all, keyword, unkeyword, older, younger, by_uid=by_uid, range_=range_)))) self.send_tagged_line(tag, 'OK %sSEARCH completed' % ('UID ' if by_uid else '')) def memory_search(self, all, keyword, unkeyword, older, younger, by_uid=False, range_=None): def item_match(msg): return all or \ (keyword is not None and keyword in msg.flags) or \ (unkeyword is not None and unkeyword not in msg.flags) or \ (range_ is not None and msg.uid in range_) or \ (older is not None and datetime.now(tz=utc) - timedelta(seconds=older) > msg.date) or \ (younger is not None and datetime.now(tz=utc) - timedelta(seconds=younger) < msg.date) return [str(msg.uid if by_uid else msg.id) for msg in self.server_state.get_mailbox_messages(self.user_login, self.user_mailbox) if item_match(msg)] def store(self, tag, *args): arg_list = list(args) if arg_list[0] == 'uid': arg_list = list(args[1:]) uid = int(arg_list[0]) # args = ['12', '+FLAGS', '(FOO)'] flags = ' '.join(arg_list[2:]).strip('()').split() # only support one flag and do not handle replacement (without + sign) for message in self.server_state.get_mailbox_messages(self.user_login, self.user_mailbox): if message.uid == uid: message.flags.extend(flags) self.send_untagged_line('{uid} FETCH (UID {uid} FLAGS ({flags}))'.format( uid=uid, flags=' '.join(message.flags))) self.send_tagged_line(tag, 'OK Store completed.') def fetch(self, tag, *args): arg_list = list(args) by_uid = False if arg_list[0] == 'uid': by_uid = True arg_list = list(args[1:]) try: fetch_range = self._build_sequence_range(arg_list[0]) except InvalidUidSet: return self.error(tag, 'Error in IMAP command: Invalid uidset') parts = arg_list[1:] parts_str = ' '.join(parts) for message in self.server_state.get_mailbox_messages(self.user_login, self.user_mailbox): if (by_uid and message.uid in fetch_range) or (not by_uid and message.id in fetch_range): response = self._build_fetch_response(message, parts, by_uid=by_uid) if 'BODY.PEEK' not in parts_str and ('BODY[]' in parts_str or 'RFC822' in parts_str): message.flags.append('\Seen') self.send_raw_untagged_line(response) self.send_tagged_line(tag, 'OK FETCH completed.') def _build_sequence_range(self, uid_pattern): range_re = re.compile(r'(\d+):(\d+|\*)') match = range_re.match(uid_pattern) if match: start = int(match.group(1)) if start <= 0: raise InvalidUidSet() if match.group(2) == '*': return range(start, sys.maxsize) end = int(match.group(2)) if end <= 0 or end < start: raise InvalidUidSet() return range(start, end + 1) return [int(uid_pattern)] def _build_fetch_response(self, message, parts, by_uid=True): response = ('%d FETCH (UID %s' % (message.id, message.uid)).encode() if by_uid \ else ('%d FETCH (' % message.id).encode() for part in parts: if part.startswith('(') or part.endswith(')'): part = part.strip('()') if not response.endswith(b' ') and not response.endswith(b'('): response += b' ' if part == 'UID' and not by_uid: response += ('UID %s' % message.uid).encode() if part == 'BODY[]' or part == 'BODY.PEEK[]' or part == 'RFC822': response += ('%s {%s}\r\n' % (part, len(message.as_bytes()))).encode() + message.as_bytes() if part == 'BODY.PEEK[HEADER.FIELDS': fetch_header = FETCH_HEADERS_RE.match(' '.join(parts)) if fetch_header: headers = fetch_header.group('headers') message_headers = Message(policy=Compat32(linesep='\r\n')) for hk in headers.split(): message_headers[hk] = message.email.get(hk, '') response += ('BODY[HEADER.FIELDS (%s)] {%d}\r\n' % (headers, len(message_headers.as_bytes()))).encode() + message_headers.as_bytes() if part == 'FLAGS': response += ('FLAGS (%s)' % ' '.join(message.flags)).encode() response = response.strip(b' ') response += b')' return response def append(self, tag, *args): mailbox_name = args[0] size = args[-1].strip('{}') self.append_literal_command = (tag, mailbox_name, int(size)) self.send_untagged_line('Ready for literal data', continuation=True) def append_literal(self, data): tag, mailbox_name, size = self.append_literal_command if data == CRLF: if 'UIDPLUS' in self.capabilities: self.send_tagged_line(tag, 'OK [APPENDUID %s %s] APPEND completed.' % (self.uidvalidity, self.server_state.max_uid(self.user_login, mailbox_name))) else: self.send_tagged_line(tag, 'OK APPEND completed.') self.append_literal_command = None return literal_data, rest = data[:size], data[size:] if len(literal_data) < size: self.send_tagged_line(self.append_literal_command[0], 'BAD literal length : expected %s but was %s' % (size, len(literal_data))) self.append_literal_command = None elif rest and rest != CRLF: self.send_tagged_line(self.append_literal_command[0], 'BAD literal trailing data : expected CRLF but got %s' % (rest)) else: m = email.message_from_bytes(data) self.server_state.add_mail(self.user_login, Mail(m), mailbox_name) if rest: self.append_literal(rest) def expunge(self, tag, *args): expunge_range = range(0, sys.maxsize) uid_response = '' if args and args[0] == 'uid': uid_response = 'UID ' if len(args) > 1: try: expunge_range = self._build_sequence_range(args[1]) except InvalidUidSet: return self.error(tag, 'Error in IMAP command: Invalid uidset') for message in self.server_state.get_mailbox_messages(self.user_login, self.user_mailbox).copy(): if message.uid in expunge_range: self.server_state.remove(message, self.user_login, self.user_mailbox) self.send_untagged_line('{msg_uid} EXPUNGE'.format(msg_uid=message.uid)) self.send_tagged_line(tag, 'OK %sEXPUNGE completed.' % uid_response) def capability(self, tag, *args): self.send_untagged_line('CAPABILITY IMAP4rev1 YESAUTH') self.send_tagged_line(tag, 'OK Pre-login capabilities listed, post-login capabilities have more') def namespace(self, tag): self.send_untagged_line('NAMESPACE (("" "/")) NIL NIL') self.send_tagged_line(tag, 'OK NAMESPACE command completed') def enable(self, tag, *args): self.send_tagged_line(tag, 'OK %s enabled' % ' '.join(args)) def copy(self, tag, *args): message_set, mailbox = args[0:-1], args[-1] self.server_state.copy(self.user_login, self.user_mailbox, mailbox, message_set) self.send_tagged_line(tag, 'OK COPY completed.') def move(self, tag, *args): args_list = list(args) args_list.reverse() msg_attribute = 'id' if args[-1] == 'uid': msg_attribute = 'uid' mailbox, message_set = args_list[0:2] seq_range = self._build_sequence_range(message_set) seq_moved = self.server_state.move(self.user_login, self.user_mailbox, mailbox, seq_range, msg_attribute) if 'UIDPLUS' in self.capabilities: self.send_untagged_line( 'OK [COPYUID %d %d:%d %d:%d]' % (self.uidvalidity, seq_range.start, seq_range.stop-1, seq_moved.start, seq_moved.stop-1)) for msg_id in seq_moved: self.send_untagged_line('{msg_id} EXPUNGE'.format(msg_id=msg_id)) self.send_tagged_line(tag, 'OK Done') def id(self, tag, *args): self.send_untagged_line('NIL') self.send_tagged_line(tag, 'OK ID command completed') def noop(self, tag, *args): if len(self.state_to_send) > 0: for line in deque(self.state_to_send): self.send_untagged_line(line) self.send_tagged_line(tag, 'OK NOOP completed.') def check(self, tag, *args): self.send_tagged_line(tag, 'OK CHECK completed.') def status(self, tag, *args): mailbox_name = args[0] data_items = ' '.join(args[1:]) mailbox = self.server_state.get_mailbox_messages(self.user_login, mailbox_name) if mailbox is None: self.send_tagged_line(tag, 'NO STATUS completed.') return status_response = 'STATUS %s (' % mailbox_name if 'MESSAGES' in data_items: status_response += 'MESSAGES %s' % len(mailbox) if 'RECENT' in data_items: status_response += ' RECENT %s' % len([m for m in mailbox if 'RECENT' in m.flags]) if 'UIDNEXT' in data_items: status_response += ' UIDNEXT %s' % (self.server_state.max_uid(self.user_login, self.user_mailbox) + 1) if 'UIDVALIDITY' in data_items: status_response += ' UIDVALIDITY %s' % self.uidvalidity if 'UNSEEN' in data_items: status_response += ' UNSEEN %s' % len([m for m in mailbox if 'UNSEEN' in m.flags]) status_response += ')' self.send_untagged_line(status_response) self.send_tagged_line(tag, 'OK STATUS completed.') def subscribe(self, tag, *args): mailbox_name = args[0] self.server_state.subscribe(self.user_login, mailbox_name) self.send_tagged_line(tag, 'OK SUBSCRIBE completed.') def unsubscribe(self, tag, *args): mailbox_name = args[0] self.server_state.unsubscribe(self.user_login, mailbox_name) self.send_tagged_line(tag, 'OK UNSUBSCRIBE completed.') def lsub(self, tag, *args): reference_name, mailbox_name = args if not reference_name.endswith('.') and not mailbox_name.startswith('.'): mailbox_search = '%s.%s' % (reference_name, mailbox_name) else: mailbox_search = reference_name + mailbox_name for found_mb_name in self.server_state.lsub(self.user_login, mailbox_search): self.send_untagged_line('LSUB () "." %s' % found_mb_name) self.send_tagged_line(tag, 'OK LSUB completed.') def create(self, tag, *args): mailbox_name = args[0] self.server_state.create_mailbox_if_not_exists(self.user_login, mailbox_name) self.send_tagged_line(tag, 'OK CREATE completed.') def delete(self, tag, *args): mailbox_name = args[0] self.server_state.delete_mailbox(self.user_login, mailbox_name) self.send_tagged_line(tag, 'OK DELETE completed.') def rename(self, tag, *args): old_mb, new_mb = args self.server_state.rename_mailbox(self.user_login, old_mb, new_mb) self.send_tagged_line(tag, 'OK RENAME completed.') def list(self, tag, *args): reference = args[0] mailbox_pattern = args[1].replace('*', '.*').replace('%', '.*') for mb in self.server_state.list(self.user_login, reference, mailbox_pattern): self.send_untagged_line('LIST () "/" %s' % mb) self.send_tagged_line(tag, 'OK LIST completed.') def error(self, tag, msg): self.send_tagged_line(tag, 'BAD %s' % msg) def notify_new_mail(self, uid): if self.idle_tag: self.send_untagged_line('{uid} EXISTS'.format(uid=uid)) self.send_untagged_line('{uid} RECENT'.format(uid=uid)) else: self.state_to_send.append('{uid} EXISTS'.format(uid=uid)) self.state_to_send.append('{uid} RECENT'.format(uid=uid)) def delay(self, tag, *args): self.delay_seconds = int(args[0]) self.send_tagged_line(tag, 'OK DELAY completed.') class MockImapServer(object): def __init__(self, capabilities=CAPABILITIES, loop=None) -> None: self._server_state = ServerState() self._connections = list() self.capabilities = capabilities if loop is None: self.loop = asyncio.get_event_loop() else: self.loop = loop def receive(self, mail, imap_user=None, mailbox='INBOX'): """ :param imap_user: str :type mail: Mail :type mailbox: str :type to_list: list """ if imap_user is not None: return [self._server_state.imap_receive(imap_user, mail, mailbox)] else: uids = list() for to in mail.to: uids.append(self._server_state.imap_receive(to, mail, mailbox)) return uids @asyncio.coroutine def wait_state(self, state, user): user_connections = [connection for connection in self._connections if connection.user_login == user] if len(user_connections) == 0: other_users = list(map(lambda c: c.user_login, self._connections)) raise ValueError("wait_state didn't find a connection to user %s among %s" % (user, other_users)) if len(user_connections) > 1: raise ValueError("wait_state can't handle %d connections for user %s" % (len(user_connections), user)) yield from user_connections[0].wait(state) def get_connection(self, user): return self._server_state.get_connection(user) def run_server(self, host='127.0.0.1', port=1143, fetch_chunk_size=0, ssl_context=None): def create_protocol(): protocol = ImapProtocol(self._server_state, fetch_chunk_size, self.capabilities, self.loop) self._connections.append(protocol) return protocol server = self.loop.create_server(create_protocol, host, port, ssl=ssl_context) return self.loop.run_until_complete(server) def reset(self): self._server_state.reset() class Mail(object): def __init__(self, email, date=datetime.now()): self.date = date self.email = email self.uid = 0 self.id = 0 self.flags = [] def as_bytes(self): return self.email.as_bytes() def as_string(self): return self.email.as_string() @property def to(self): return self.email.get('To').split(', ') @staticmethod def create(to, mail_from='', subject='', content='', encoding='utf-8', date=None, in_reply_to=None, message_id=None, quoted_printable=False, cc=None, body_subtype='plain', references=None ): """ :param quoted_printable: boolean :type to: list :type cc: list :type mail_from: str :type subject: unicode :type content: unicode :type encoding: str :type date: datetime :param in_reply_to: str :param message_id: str :param body_subtype: str :param references: list """ charset = email.charset.Charset(encoding) msg = email.mime.nonmultipart.MIMENonMultipart('text', body_subtype, charset=encoding) if quoted_printable: charset.body_encoding = email.charset.QP msg.set_payload(content, charset=charset) date = date or datetime.now(tz=utc) msg['Return-Path'] = '<%s>' % mail_from msg['Delivered-To'] = '<%s>' % ', '.join(to) msg['Message-ID'] = '<%s>' % (message_id or '%s@mockimap' % str(uuid.uuid1())) msg['Date'] = date.strftime('%a, %d %b %Y %H:%M:%S %z') if '<' in mail_from and '>' in mail_from or mail_from == '': msg['From'] = mail_from else: msg['From'] = '<%s>' % mail_from msg['User-Agent'] = 'python3' msg['MIME-Version'] = '1.0' msg['To'] = ', '.join(to) msg['Subject'] = Header(subject, encoding) if in_reply_to is not None: msg['In-Reply-To'] = '<%s>' % in_reply_to if cc is not None: msg['Cc'] = ', '.join(cc) if references is not None: ' '.join(['<%s>' % ref for ref in references]) return Mail(msg, date=date) if __name__ == '__main__': loop = asyncio.get_event_loop() server = MockImapServer().run_server() loop.run_forever()