diff --git a/README.md b/README.md index f276995..1bbcfbe 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ Currently implemented: * `Mailbox/*` (`get`, `changes`, `query`, `queryChanges`, `set`) * `Thread/*` (`get`, `changes`) * Arbitrary methods via the `CustomMethod` class +* Fastmail-specific methods: + * [`MaskedEmail/*` (`get`, `set`)][fastmail-maskedemail] * Combined requests with support for result references * Basic JMAP method response error handling * EventSource event handling @@ -70,6 +72,7 @@ Created from [smkent/cookie-python][cookie-python] using [codecov]: https://codecov.io/gh/smkent/jmapc [cookie-python]: https://github.com/smkent/cookie-python [cookiecutter]: https://github.com/cookiecutter/cookiecutter +[fastmail-maskedemail]: https://www.fastmail.com/developer/maskedemail/ [gh-actions]: https://github.com/smkent/jmapc/actions?query=branch%3Amain [logo]: https://raw.github.com/smkent/jmapc/main/img/jmapc.png [jmapc-pypi]: https://pypi.org/project/jmapc/ diff --git a/jmapc/__init__.py b/jmapc/__init__.py index ba3ab9a..45dae45 100644 --- a/jmapc/__init__.py +++ b/jmapc/__init__.py @@ -1,4 +1,4 @@ -from . import auth, errors, methods, models +from . import auth, errors, fastmail, methods, models from .__version__ import __version__ as version from .client import Client, EventSourceConfig from .errors import Error @@ -82,6 +82,7 @@ "TypeState", "UndoStatus", "auth", + "fastmail", "errors", "log", "methods", diff --git a/jmapc/fastmail/__init__.py b/jmapc/fastmail/__init__.py new file mode 100644 index 0000000..2d39268 --- /dev/null +++ b/jmapc/fastmail/__init__.py @@ -0,0 +1,15 @@ +from .maskedemail_methods import ( + MaskedEmailGet, + MaskedEmailGetResponse, + MaskedEmailSet, + MaskedEmailSetResponse, +) +from .maskedemail_models import MaskedEmail + +__all__ = [ + "MaskedEmail", + "MaskedEmailGet", + "MaskedEmailGetResponse", + "MaskedEmailSet", + "MaskedEmailSetResponse", +] diff --git a/jmapc/fastmail/maskedemail_methods.py b/jmapc/fastmail/maskedemail_methods.py new file mode 100644 index 0000000..c26736b --- /dev/null +++ b/jmapc/fastmail/maskedemail_methods.py @@ -0,0 +1,37 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from typing import Dict, List, Optional + +from dataclasses_json import config + +from ..methods.base import Get, GetResponse, Set, SetResponse +from .maskedemail_models import MaskedEmail + +URN = "https://www.fastmail.com/dev/maskedemail" + + +class MaskedEmailBase: + method_namespace: Optional[str] = "MaskedEmail" + using = {URN} + + +@dataclass +class MaskedEmailGet(MaskedEmailBase, Get): + pass + + +@dataclass +class MaskedEmailGetResponse(MaskedEmailBase, GetResponse): + data: List[MaskedEmail] = field(metadata=config(field_name="list")) + + +@dataclass +class MaskedEmailSet(MaskedEmailBase, Set): + pass + + +@dataclass +class MaskedEmailSetResponse(MaskedEmailBase, SetResponse): + created: Optional[Dict[str, Optional[MaskedEmail]]] + updated: Optional[Dict[str, Optional[MaskedEmail]]] diff --git a/jmapc/fastmail/maskedemail_models.py b/jmapc/fastmail/maskedemail_models.py new file mode 100644 index 0000000..0b986ee --- /dev/null +++ b/jmapc/fastmail/maskedemail_models.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import datetime +from enum import Enum +from typing import Optional + +from dataclasses_json import config + +from ..serializer import Model, datetime_decode, datetime_encode + + +class MaskedEmailState(Enum): + PENDING = "pending" + ENABLED = "enabled" + DISABLED = "disabled" + DELETED = "deleted" + + +@dataclass +class MaskedEmail(Model): + id: Optional[str] = None + email: Optional[str] = None + for_domain: Optional[str] = None + description: Optional[str] = None + last_message_at: Optional[datetime] = field( + default=None, + metadata=config(encoder=datetime_encode, decoder=datetime_decode), + ) + created_at: Optional[datetime] = field( + default=None, + metadata=config(encoder=datetime_encode, decoder=datetime_decode), + ) + created_by: Optional[str] = None + url: Optional[str] = None + email_prefix: Optional[str] = None diff --git a/tests/methods/test_fastmail_maskedemail.py b/tests/methods/test_fastmail_maskedemail.py new file mode 100644 index 0000000..0c4944f --- /dev/null +++ b/tests/methods/test_fastmail_maskedemail.py @@ -0,0 +1,189 @@ +from datetime import datetime, timezone + +import responses + +from jmapc import Client +from jmapc.fastmail import ( + MaskedEmail, + MaskedEmailGet, + MaskedEmailGetResponse, + MaskedEmailSet, + MaskedEmailSetResponse, +) + +from ..utils import expect_jmap_call + + +def test_maskedemail_get( + client: Client, http_responses: responses.RequestsMock +) -> None: + expected_request = { + "methodCalls": [ + [ + "MaskedEmail/get", + {"accountId": "u1138", "ids": ["masked-1138"]}, + "single.MaskedEmail/get", + ] + ], + "using": [ + "https://www.fastmail.com/dev/maskedemail", + "urn:ietf:params:jmap:core", + ], + } + response = { + "methodResponses": [ + [ + "MaskedEmail/get", + { + "accountId": "u1138", + "list": [ + { + "id": "masked-1138", + "email": "pk.fire@ness.example.com", + "forDomain": "ness.example.com", + "description": ( + "Masked Email (pk.fire@ness.example.com)" + ), + "lastMessageAt": "1994-08-24T12:01:02Z", + "createdAt": "1994-08-24T12:01:02Z", + "createdBy": "ness", + "url": None, + }, + ], + "not_found": [], + "state": "2187", + }, + "single.MaskedEmail/get", + ] + ] + } + expect_jmap_call(http_responses, expected_request, response) + jmap_response = client.request(MaskedEmailGet(ids=["masked-1138"])) + assert jmap_response == MaskedEmailGetResponse( + account_id="u1138", + state="2187", + not_found=[], + data=[ + MaskedEmail( + id="masked-1138", + email="pk.fire@ness.example.com", + for_domain="ness.example.com", + description="Masked Email (pk.fire@ness.example.com)", + last_message_at=datetime( + 1994, 8, 24, 12, 1, 2, tzinfo=timezone.utc + ), + created_at=datetime( + 1994, 8, 24, 12, 1, 2, tzinfo=timezone.utc + ), + created_by="ness", + ), + ], + ) + + +def test_maskedemail_set( + client: Client, http_responses: responses.RequestsMock +) -> None: + expected_request = { + "methodCalls": [ + [ + "MaskedEmail/set", + { + "accountId": "u1138", + "create": { + "create": { + "email": "pk.fire@ness.example.com", + "forDomain": "ness.example.com", + "description": ( + "Masked Email (pk.fire@ness.example.com)" + ), + "lastMessageAt": "1994-08-24T12:01:02Z", + "createdAt": "1994-08-24T12:01:02Z", + "createdBy": "ness", + }, + }, + }, + "single.MaskedEmail/set", + ] + ], + "using": [ + "https://www.fastmail.com/dev/maskedemail", + "urn:ietf:params:jmap:core", + ], + } + response = { + "methodResponses": [ + [ + "MaskedEmail/set", + { + "accountId": "u1138", + "created": { + "create": { + "id": "masked-42", + "email": "pk.fire@ness.example.com", + "forDomain": "ness.example.com", + "description": ( + "Masked Email (pk.fire@ness.example.com)" + ), + "lastMessageAt": "1994-08-24T12:01:02Z", + "createdAt": "1994-08-24T12:01:02Z", + "createdBy": "ness", + } + }, + "destroyed": None, + "newState": "2", + "notCreated": None, + "notDestroyed": None, + "notUpdated": None, + "oldState": "1", + "updated": None, + }, + "single.MaskedEmail/set", + ] + ] + } + expect_jmap_call(http_responses, expected_request, response) + + assert client.request( + MaskedEmailSet( + create=dict( + create=MaskedEmail( + id=None, + email="pk.fire@ness.example.com", + for_domain="ness.example.com", + description="Masked Email (pk.fire@ness.example.com)", + last_message_at=datetime( + 1994, 8, 24, 12, 1, 2, tzinfo=timezone.utc + ), + created_at=datetime( + 1994, 8, 24, 12, 1, 2, tzinfo=timezone.utc + ), + created_by="ness", + ) + ) + ) + ) == MaskedEmailSetResponse( + account_id="u1138", + old_state="1", + new_state="2", + created=dict( + create=MaskedEmail( + id="masked-42", + email="pk.fire@ness.example.com", + for_domain="ness.example.com", + description="Masked Email (pk.fire@ness.example.com)", + last_message_at=datetime( + 1994, 8, 24, 12, 1, 2, tzinfo=timezone.utc + ), + created_at=datetime( + 1994, 8, 24, 12, 1, 2, tzinfo=timezone.utc + ), + created_by="ness", + ), + ), + updated=None, + destroyed=None, + not_created=None, + not_updated=None, + not_destroyed=None, + )