It does something
This commit is contained in:
parent
cf0e6f61d2
commit
41c37f29ea
@ -1,16 +1,388 @@
|
|||||||
from argparse import ArgumentParser
|
from __future__ import annotations
|
||||||
from typing import Any
|
|
||||||
|
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, \
|
||||||
|
MailboxError
|
||||||
|
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.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 pymap.health import HealthStatus
|
||||||
|
from pymap.interfaces.token import TokensInterface
|
||||||
|
from pymap.token import AllTokens
|
||||||
|
|
||||||
|
|
||||||
|
__all__ = ['MinifluxBackend', 'Config']
|
||||||
|
|
||||||
|
from pysasl import AuthenticationCredentials
|
||||||
|
|
||||||
|
|
||||||
class MinifluxBackend(BackendInterface):
|
class MinifluxBackend(BackendInterface):
|
||||||
|
|
||||||
|
def __init__(self, login: Login, config: Config) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self._login = login
|
||||||
|
self._config = config
|
||||||
|
self._status = HealthStatus(True)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def login(self) -> Login:
|
||||||
|
return self._login
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self) -> Config:
|
||||||
|
return self._config
|
||||||
|
|
||||||
|
@property
|
||||||
|
def status(self) -> HealthStatus:
|
||||||
|
return self._status
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_subparser(cls, name: str, subparsers: Any) -> ArgumentParser:
|
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
|
return parser
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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(),
|
||||||
|
**extra)
|
||||||
|
self.server = server
|
||||||
|
|
||||||
|
@property
|
||||||
|
def backend_capability(self) -> BackendCapability:
|
||||||
|
return BackendCapability(idle=False, multi_append=False, object_id=False)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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:
|
||||||
|
super().__init__()
|
||||||
|
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()
|
||||||
|
|
||||||
|
@property
|
||||||
|
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:
|
||||||
|
print(resp.status)
|
||||||
|
profile = await resp.json()
|
||||||
|
print(profile)
|
||||||
|
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
|
||||||
|
|
||||||
|
@property
|
||||||
|
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] = (self.name, token_key)
|
||||||
|
return self.login.tokens.get_login_token(
|
||||||
|
token_id, token_key, expiration=expiration)
|
||||||
|
|
||||||
|
@asynccontextmanager
|
||||||
|
async def new_session(self) -> AsyncIterator[Session]:
|
||||||
|
identity = self.name
|
||||||
|
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)
|
||||||
|
self.session.close()
|
||||||
|
|
||||||
|
|
||||||
|
class Session(BaseSession):
|
||||||
|
"""The session implementation for the dict backend."""
|
||||||
|
|
||||||
|
def __init__(self, owner: str, session: aiohttp.ClientSession, config: Config) -> None:
|
||||||
|
super().__init__(owner)
|
||||||
|
self._config = config
|
||||||
|
self._session = session
|
||||||
|
self._mailbox_set = MailboxSet(session, config)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def config(self) -> Config:
|
||||||
|
return self._config
|
||||||
|
|
||||||
|
@property
|
||||||
|
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,
|
||||||
|
thread_id=thread_id)
|
||||||
|
self._uid = uid
|
||||||
|
self._expunged = expunged
|
||||||
|
self._internal_date = internal_date
|
||||||
|
self._recent = recent
|
||||||
|
self._content = content
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uid(self) -> int:
|
||||||
|
return self._uid
|
||||||
|
|
||||||
|
@uid.setter
|
||||||
|
def uid(self, value):
|
||||||
|
self._uid = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def expunged(self) -> bool:
|
||||||
|
return self._expunged
|
||||||
|
|
||||||
|
@expunged.setter
|
||||||
|
def expunged(self, value):
|
||||||
|
self._expunged = value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def internal_date(self) -> datetime:
|
||||||
|
return self._internal_date
|
||||||
|
|
||||||
|
@internal_date.setter
|
||||||
|
def internal_date(self, value):
|
||||||
|
self._internal_date = value
|
||||||
|
|
||||||
|
async def load_content(self, requirement: FetchRequirement) -> LoadedMessageInterface:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def mailbox_id(self) -> ObjectId:
|
||||||
|
return self._mailbox_id
|
||||||
|
|
||||||
|
@property
|
||||||
|
def readonly(self) -> bool:
|
||||||
|
return self._readonly
|
||||||
|
|
||||||
|
@property
|
||||||
|
def uid_validity(self) -> int:
|
||||||
|
return self._uid_validity
|
||||||
|
|
||||||
|
@property
|
||||||
|
def messages_lock(self) -> ReadWriteLock:
|
||||||
|
return self._messages_lock
|
||||||
|
|
||||||
|
@property
|
||||||
|
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():
|
||||||
|
messages.append(Message(
|
||||||
|
uid=id,
|
||||||
|
internal_date=datetime.fromisoformat(entry['published_at']),
|
||||||
|
permanent_flags=[],
|
||||||
|
))
|
||||||
|
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:
|
||||||
|
pass
|
||||||
|
|
||||||
|
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:
|
||||||
|
print("snapshot")
|
||||||
|
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:
|
||||||
|
super().__init__()
|
||||||
|
self.session = session
|
||||||
|
self.config = config
|
||||||
|
self._feeds = {}
|
||||||
|
|
||||||
|
@property
|
||||||
|
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
|
||||||
|
list_tree.update(name)
|
||||||
|
self._feeds = feeds
|
||||||
|
|
||||||
|
#print(list(list_tree.list()))
|
||||||
|
#list_tree.update('INBOX')
|
||||||
|
return list_tree
|
||||||
|
|
||||||
|
async def get_mailbox(self, name: str) -> MailboxData:
|
||||||
|
try:
|
||||||
|
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)
|
backends.add("miniflux", MinifluxBackend)
|
||||||
|
Loading…
Reference in New Issue
Block a user