Browse Source

It does something

Arti Zirk 4 months ago
  1. 380


@ -1,16 +1,388 @@
from argparse import ArgumentParser
from typing import Any
from __future__ import annotations
import asyncio
import uuid
from collections import OrderedDict
from contextlib import asynccontextmanager
from datetime import datetime
from secrets import token_bytes
from urllib.parse import urljoin
import aiohttp
from argparse import ArgumentParser, Namespace
from typing import Any, Tuple, Sequence, Awaitable, Dict, Mapping, Optional, AsyncIterator, FrozenSet, Iterable, \
AsyncIterable, Final
from pymap.backend.mailbox import MailboxSetInterface, MailboxDataInterface, MailboxDataT_co
from pymap.backend.session import BaseSession
from pymap.concurrent import ReadWriteLock, Event
from pymap.context import subsystem
from pymap.exceptions import AuthorizationFailure, UserNotFound, NotAllowedError, NotSupportedError, MailboxNotFound, \
from pymap.flags import FlagOp
from pymap.interfaces.message import CachedMessage, MessageT_co, LoadedMessageInterface
from pymap.listtree import ListTree
from pymap.mailbox import MailboxSnapshot
from pymap.message import BaseMessage
from pymap.mime import MessageContent
from pymap.parsing.message import AppendMessage
from pymap.parsing.specials import ObjectId, Flag, SequenceSet, FetchRequirement
from pymap.selected import SelectedSet, SelectedMailbox
from pymap.user import UserMetadata
from pysasl.hashing import Cleartext
from pymap.backend import backends
from pymap.interfaces.backend import BackendInterface
from pymap.interfaces.backend import BackendInterface, ServiceInterface
from pymap.interfaces.login import LoginInterface, IdentityInterface
from pymap.config import IMAPConfig, BackendCapability
from import HealthStatus
from pymap.interfaces.token import TokensInterface
from pymap.token import AllTokens
__all__ = ['MinifluxBackend', 'Config']
from pysasl import AuthenticationCredentials
class MinifluxBackend(BackendInterface):
def __init__(self, login: Login, config: Config) -> None:
self._login = login
self._config = config
self._status = HealthStatus(True)
def login(self) -> Login:
return self._login
def config(self) -> Config:
return self._config
def status(self) -> HealthStatus:
return self._status
def add_subparser(cls, name: str, subparsers: Any) -> ArgumentParser:
parser = subparsers.add_parser(name, help='Serve')
parser: ArgumentParser = subparsers.add_parser(name, help='Miniflux')
parser.add_argument("server", help="Miniflux API endpoint")
return parser
async def init(cls, args: Namespace, **overrides: Any) \
-> Tuple[MinifluxBackend, Config]:
config = Config.from_args(args, **overrides)
login = Login(config)
return cls(login, config), config
async def start(self, services: Sequence[ServiceInterface]) -> Awaitable:
tasks = [await service.start() for service in services]
return asyncio.gather(*tasks)
class Config(IMAPConfig):
def __init__(self, args: Namespace, *, server: str,
**extra: Any) -> None:
super().__init__(args, hash_context=Cleartext(), admin_key=token_bytes(),
self.server = server
def backend_capability(self) -> BackendCapability:
return BackendCapability(idle=False, multi_append=False, object_id=False)
def parse_args(cls, args: Namespace) -> Mapping[str, Any]:
return {**super().parse_args(args),
'server': args.server}
class Login(LoginInterface):
def __init__(self, config: Config) -> None:
self.config = config
# self.users_dict = {config.demo_user: UserMetadata(
# config, password=config.demo_password)}
self.tokens_dict: Dict[str, Tuple[str, bytes]] = {}
self._tokens = AllTokens()
def tokens(self) -> TokensInterface:
return self._tokens
async def authenticate(self, credentials: AuthenticationCredentials) -> IdentityInterface:
session = aiohttp.ClientSession(auth=aiohttp.BasicAuth(credentials.identity, credentials.secret))
async with session.get(urljoin(self.config.server, '/v1/me')) as resp:
profile = await resp.json()
if resp.status == 200:
return Identity(credentials.identity, session, profile, self)
raise AuthorizationFailure()
class Identity(IdentityInterface):
def __init__(self, username, session, profile, login):
self.username = username
self.session = session
self.profile = profile
self.login = login
self.config = login.config
def name(self):
return self.username
async def new_token(self, *, expiration: datetime = None) -> Optional[str]:
token_id = uuid.uuid4().hex
token_key = token_bytes()
self.login.tokens_dict[token_id] = (, token_key)
return self.login.tokens.get_login_token(
token_id, token_key, expiration=expiration)
async def new_session(self) -> AsyncIterator[Session]:
identity =
config = self.config
session = self.session
_ = await self.get()
yield Session(identity, session, config)
async def get(self) -> UserMetadata:
return UserMetadata(self.config)
async def set(self, data: UserMetadata) -> None:
return None
async def delete(self) -> None:
print("Deleting session", self.session)
class Session(BaseSession):
"""The session implementation for the dict backend."""
def __init__(self, owner: str, session: aiohttp.ClientSession, config: Config) -> None:
self._config = config
self._session = session
self._mailbox_set = MailboxSet(session, config)
def config(self) -> Config:
return self._config
def mailbox_set(self) -> MailboxSet:
return self._mailbox_set
class Message(BaseMessage):
def __init__(self, uid: int, internal_date: datetime,
permanent_flags: Iterable[Flag], *, expunged: bool = False,
email_id: ObjectId = None, thread_id: ObjectId = None,
recent: bool = False, content: MessageContent = None) -> None:
super().__init__(uid, internal_date, permanent_flags,
expunged=expunged, email_id=email_id,
self._uid = uid
self._expunged = expunged
self._internal_date = internal_date
self._recent = recent
self._content = content
def uid(self) -> int:
return self._uid
def uid(self, value):
self._uid = value
def expunged(self) -> bool:
return self._expunged
def expunged(self, value):
self._expunged = value
def internal_date(self) -> datetime:
return self._internal_date
def internal_date(self, value):
self._internal_date = value
async def load_content(self, requirement: FetchRequirement) -> LoadedMessageInterface:
class MailboxData(MailboxDataInterface[Message]):
def __init__(self, name, feed, session, config):
self._session: aiohttp.ClientSession = session
self._config = config
self._name = name
self._feed = feed
self._mailbox_id = ObjectId.random_mailbox_id()
self._readonly = False
self._updated = subsystem.get().new_event()
self._messages_lock = subsystem.get().new_rwlock()
self._selected_set = SelectedSet()
self._uid_validity = MailboxSnapshot.new_uid_validity()
#self._max_uid = 100
#self._mod_sequences = _ModSequenceMapping()
self._messages: Dict[int, Message] = OrderedDict()
def mailbox_id(self) -> ObjectId:
return self._mailbox_id
def readonly(self) -> bool:
return self._readonly
def uid_validity(self) -> int:
return self._uid_validity
def messages_lock(self) -> ReadWriteLock:
return self._messages_lock
def selected_set(self) -> SelectedSet:
return self._selected_set
async def update_selected(self, selected: SelectedMailbox, *,
wait_on: Event = None) -> SelectedMailbox:
print("update selected")
messages = []
for id, entry in self._messages.items():
selected.add_updates(messages, [])
return selected
async def get(self, uid: int, cached_msg: CachedMessage) -> Message:
print("get message", uid, cached_msg)
return cached_msg
#raise MailboxError(mailbox=self._name, message="Not implemented")
async def append(self, append_msg: AppendMessage, *,
recent: bool = False) -> Message:
raise NotSupportedError()
async def copy(self, uid: int, destination: MailboxData, *,
recent: bool = False) -> Optional[int]:
raise NotSupportedError()
async def move(self: MailboxData, uid: int, destination: MailboxData, *,
recent: bool = False) -> Optional[int]:
raise NotSupportedError()
async def update(self, uid: int, cached_msg: CachedMessage,
flag_set: FrozenSet[Flag], mode: FlagOp) -> Message:
raise NotSupportedError()
async def delete(self, uids: Iterable[int]) -> None:
raise NotSupportedError()
async def claim_recent(self, selected: SelectedMailbox) -> None:
print("claim recent")
async def cleanup(self) -> None:
async def messages(self):
url_fragmet = f'/v1/feeds/{self._feed["id"]}/entries'
async with self._session.get(urljoin(self._config.server, url_fragmet)) as resp:
if resp.status == 200:
data = await resp.json()
for entry in data["entries"]:
self._messages[entry["id"]] = entry
yield entry
async def snapshot(self) -> MailboxSnapshot:
exists = 0
recent = 0
unseen = 0
first_unseen: Optional[int] = None
async for msg in self.messages():
exists += 1
next_uid = exists + 1
return MailboxSnapshot(self.mailbox_id, self.readonly,
self.uid_validity, self.permanent_flags,
self.session_flags, exists, recent, unseen,
first_unseen, next_uid)
class MailboxSet(MailboxSetInterface[MailboxData]):
def __init__(self, session: aiohttp.ClientSession, config: Config) -> None:
self.session = session
self.config = config
self._feeds = {}
def delimiter(self):
return '/'
async def set_subscribed(self, name: str, subscribed: bool) -> None:
print("set_subscribed", name, subscribed)
raise NotSupportedError()
async def list_subscribed(self) -> ListTree:
"""Tell client that we are subscribed to all mailboxes"""
return await self.list_mailboxes()
async def list_mailboxes(self) -> ListTree:
list_tree = ListTree(delimiter=self.delimiter)
async with self.session.get(urljoin(self.config.server, '/v1/feeds')) as resp:
if resp.status == 200:
feeds = {}
data = await resp.json()
for feed in data:
name = feed['category']['title']+'/'+feed['title']
feeds[name] = feed
self._feeds = feeds
return list_tree
async def get_mailbox(self, name: str) -> MailboxData:
return MailboxData(name, self._feeds[name], self.session, self.config)
except KeyError:
raise MailboxNotFound(name)
async def add_mailbox(self, name: str) -> ObjectId:
raise NotSupportedError()
async def delete_mailbox(self, name: str) -> None:
raise NotSupportedError()
async def rename_mailbox(self, before: str, after: str) -> None:
raise NotSupportedError()
backends.add("miniflux", MinifluxBackend)