Browse Source

It does something

master
Arti Zirk 4 months ago
parent
commit
41c37f29ea
  1. 380
      miniflux2imap/__init__.py

380
miniflux2imap/__init__.py

@ -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, \
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.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):
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
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
@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)
Loading…
Cancel
Save