pyactivesync 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pyactivesync/models.py ADDED
@@ -0,0 +1,112 @@
1
+ """Public data types returned by :class:`pyactivesync.client.Client`."""
2
+ from __future__ import annotations
3
+
4
+ from dataclasses import dataclass, field
5
+ from enum import IntEnum
6
+
7
+
8
+ class FolderType(IntEnum):
9
+ """MS-ASCMD ``FolderHierarchy:Type`` values."""
10
+
11
+ GENERIC = 1
12
+ INBOX = 2
13
+ DRAFTS = 3
14
+ DELETED_ITEMS = 4
15
+ SENT_ITEMS = 5
16
+ OUTBOX = 6
17
+ TASKS = 7
18
+ CALENDAR = 8
19
+ CONTACTS = 9
20
+ NOTES = 10
21
+ JOURNAL = 11
22
+ USER_GENERIC = 12
23
+ USER_MAIL = 13
24
+ USER_CALENDAR = 14
25
+ USER_CONTACTS = 15
26
+ USER_TASKS = 16
27
+ USER_JOURNAL = 17
28
+ USER_NOTES = 18
29
+ UNKNOWN = 19
30
+ RECIPIENT_CACHE = 20
31
+
32
+
33
+ class BodyType(IntEnum):
34
+ """MS-ASAIRSYNCBASE ``BodyPreference:Type`` / ``Body:Type`` values."""
35
+
36
+ PLAIN_TEXT = 1
37
+ HTML = 2
38
+ RTF = 3
39
+ MIME = 4
40
+
41
+
42
+ @dataclass(frozen=True, slots=True)
43
+ class Folder:
44
+ id: str
45
+ parent_id: str
46
+ type: FolderType
47
+ name: str
48
+
49
+
50
+ @dataclass(frozen=True, slots=True)
51
+ class SyncItem:
52
+ """One ``Add`` or ``Change`` entry from a ``Sync`` response.
53
+
54
+ ``fields`` is a flat ``{tag_name: text}`` dump of every leaf element
55
+ under ``ApplicationData`` (e.g. ``"Email.Subject"``), so it works
56
+ across Email/Contacts/Calendar item classes without a per-class field
57
+ table.
58
+ """
59
+
60
+ server_id: str
61
+ fields: dict[str, str] = field(default_factory=dict)
62
+
63
+
64
+ @dataclass(frozen=True, slots=True)
65
+ class SyncResult:
66
+ sync_key: str
67
+ added: list[SyncItem] = field(default_factory=list)
68
+ changed: list[SyncItem] = field(default_factory=list)
69
+ deleted: list[str] = field(default_factory=list)
70
+ more_available: bool = False
71
+
72
+
73
+ @dataclass(frozen=True, slots=True)
74
+ class AttachmentInfo:
75
+ """Attachment metadata as found in a fetched item's properties.
76
+
77
+ Use ``file_reference`` with ``Client.fetch_attachment()`` to download
78
+ the actual bytes.
79
+ """
80
+
81
+ display_name: str
82
+ file_reference: str
83
+ content_type: str | None = None
84
+ estimated_data_size: int | None = None
85
+
86
+
87
+ @dataclass(frozen=True, slots=True)
88
+ class GalEntry:
89
+ display_name: str | None = None
90
+ email_address: str | None = None
91
+ phone: str | None = None
92
+ office: str | None = None
93
+ title: str | None = None
94
+ company: str | None = None
95
+ alias: str | None = None
96
+ first_name: str | None = None
97
+ last_name: str | None = None
98
+ home_phone: str | None = None
99
+ mobile_phone: str | None = None
100
+
101
+
102
+ @dataclass(frozen=True, slots=True)
103
+ class Recipient:
104
+ type: str | None
105
+ display_name: str | None
106
+ email_address: str | None
107
+
108
+
109
+ @dataclass(frozen=True, slots=True)
110
+ class PingResult:
111
+ status: str
112
+ changed_folder_ids: list[str] = field(default_factory=list)
@@ -0,0 +1,142 @@
1
+ Metadata-Version: 2.5
2
+ Name: pyactivesync
3
+ Version: 0.2.0
4
+ Summary: A Python client for Exchange ActiveSync (EAS / MS-ASCMD / MS-ASWBXML)
5
+ Project-URL: Homepage, https://github.com/monperrus/pyactivesync
6
+ Project-URL: Repository, https://github.com/monperrus/pyactivesync
7
+ Project-URL: Issues, https://github.com/monperrus/pyactivesync/issues
8
+ Author: Martin Monperrus
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Topic :: Communications :: Email
19
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.10
22
+ Requires-Dist: requests>=2.28
23
+ Provides-Extra: dev
24
+ Requires-Dist: mypy>=1.8; extra == 'dev'
25
+ Requires-Dist: pytest-cov>=4.0; extra == 'dev'
26
+ Requires-Dist: pytest>=7.0; extra == 'dev'
27
+ Requires-Dist: ruff>=0.4; extra == 'dev'
28
+ Requires-Dist: types-requests; extra == 'dev'
29
+ Description-Content-Type: text/markdown
30
+
31
+ # pyactivesync
32
+
33
+ A Python client library for Exchange ActiveSync (EAS), implementing
34
+ enough of [MS-ASCMD] (the command protocol) and [MS-ASWBXML] (the binary
35
+ XML encoding) to talk to a real Exchange server: folder listing, mail
36
+ sync, item/attachment fetch, sending mail, folder management, moving
37
+ items, directory search, and push notifications via `Ping`.
38
+
39
+ [MS-ASCMD]: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-ascmd/
40
+ [MS-ASWBXML]: https://learn.microsoft.com/en-us/openspecs/exchange_server_protocols/ms-aswbxml/
41
+
42
+ ## Install
43
+
44
+ ```
45
+ pip install pyactivesync
46
+ ```
47
+
48
+ ## Usage
49
+
50
+ ```python
51
+ from email.message import EmailMessage
52
+ from pyactivesync import Client, FolderType, BodyType
53
+
54
+ with Client(
55
+ server="mail.example.com",
56
+ username=r"CORP\jdoe", # NTLM-style domain\user, or a plain email -- both work
57
+ password="...",
58
+ device_id="MyApp01", # caller-provided; persist it yourself for a stable
59
+ # device identity across runs -- pyactivesync doesn't
60
+ # persist anything to disk on its own
61
+ ) as client:
62
+ folders = client.list_folders()
63
+ inbox = next(f for f in folders if f.type == FolderType.INBOX)
64
+
65
+ result = client.sync_folder(inbox.id) # bootstrap
66
+ result = client.sync_folder(inbox.id, sync_key=result.sync_key) # Add/Change/Delete
67
+
68
+ for item in result.added:
69
+ print(item.fields.get("Email.Subject"))
70
+ body = client.fetch_item(inbox.id, item.server_id, body_type=BodyType.HTML)
71
+
72
+ msg = EmailMessage()
73
+ msg["To"] = "someone@example.com"
74
+ msg["Subject"] = "hello from pyactivesync"
75
+ msg.set_content("plain text body")
76
+ client.send_mail(msg)
77
+ ```
78
+
79
+ `Client` is a context manager wrapping one `requests.Session` -- EAS is
80
+ stateless HTTP (an auth header plus a `PolicyKey` header), so unlike an
81
+ IMAP connection there's no server-side session to tear down; `__exit__`
82
+ just closes the HTTP session. `provision()` (the device policy handshake)
83
+ is called lazily on first use if you don't call it explicitly.
84
+
85
+ Folder and item ids are plain strings (`"9"`, `"9:1"`), matching EAS's
86
+ own `ServerId` format exactly -- there's no synthetic id layer to keep in
87
+ sync with a local cache.
88
+
89
+ ## Command coverage
90
+
91
+ | Command | Client method |
92
+ |---|---|
93
+ | `Provision` | `Client.provision()` (also called lazily) |
94
+ | `FolderSync` | `Client.list_folders()` |
95
+ | `Sync` | `Client.sync_folder()` |
96
+ | `GetItemEstimate` | `Client.get_item_estimate()` |
97
+ | `ItemOperations` Fetch (body) | `Client.fetch_item()` |
98
+ | `ItemOperations` Fetch (attachment) | `Client.fetch_attachment()` |
99
+ | `SendMail` | `Client.send_mail()` |
100
+ | `FolderCreate`/`FolderUpdate`/`FolderDelete` | `Client.create_folder()`/`update_folder()`/`delete_folder()` |
101
+ | `MoveItems` | `Client.move_item()` |
102
+ | `Ping` | `Client.ping()` |
103
+ | `ResolveRecipients` | `Client.resolve_recipients()` |
104
+ | `Search` (GAL) | `Client.search_gal()` |
105
+ | `Search` (Mailbox, structured) | `Client.search_mailbox()` |
106
+
107
+ **Not implemented**: `MeetingResponse`, `ValidateCert`, `SmartForward`/`SmartReply`.
108
+ Documented as unimplemented, not silently missing.
109
+
110
+ **Non-goals**: an EAS *server*; Autodiscover (pass a server hostname
111
+ directly); Calendar/Contacts *write* operations (read via `Sync` is
112
+ supported); NTLM auth (Basic auth only -- `requests` doesn't do NTLM
113
+ without an extra dependency); credential storage of any kind (that's the
114
+ caller's business).
115
+
116
+ `search_mailbox()` deliberately has no `free_text=` parameter: full-text
117
+ `Search` conditions are known to fail against real Exchange servers with
118
+ `Store.Status=110`, a server-side bug in EAS's `Search` handling rather
119
+ than a WBXML encoding issue (confirmed by cross-checking the same
120
+ mailbox's content index through an unrelated protocol, which returns
121
+ correct results for the same query). Shipping that parameter would just
122
+ reproduce the failure for every caller.
123
+
124
+ ## Development
125
+
126
+ ```
127
+ pip install -e '.[dev]'
128
+ pytest
129
+ ruff check .
130
+ mypy pyactivesync tests
131
+ ```
132
+
133
+ Unit tests (WBXML codec against golden byte fixtures, codepage table
134
+ sanity checks) require no network and run in CI on every push. Live
135
+ integration tests in `tests/test_client_live.py` are skipped unless
136
+ `PYACTIVESYNC_TEST_SERVER`, `PYACTIVESYNC_TEST_USER`, and `PYACTIVESYNC_TEST_PASSWORD` are
137
+ set, and only ever create/rename/delete objects they create themselves --
138
+ pre-existing folders and items are never touched.
139
+
140
+ ## License
141
+
142
+ MIT
@@ -0,0 +1,14 @@
1
+ pyactivesync/__init__.py,sha256=4Hm02ufV_3KNeMvPJOs_MYMUDBYH-rr4-9WjqvL0ZuE,696
2
+ pyactivesync/_codepages.py,sha256=vLlgi229vAi7NdMFVQ3AafiS3-lZmwmksImMlNZxc_U,6170
3
+ pyactivesync/_http.py,sha256=X4HUb0xSM-b86KghebL-11p-a2Z8yG_X2dQsYgMikyA,2409
4
+ pyactivesync/_mime.py,sha256=_580HlTMBe75Ndu2S7tvrLHkyMJzbBtWDrv2iv3No3w,757
5
+ pyactivesync/_wbxml.py,sha256=ZBNjjGNlS_PjyCG92u2TW7H6L1DqNiaQ1l85Ftf0u64,6226
6
+ pyactivesync/cli.py,sha256=_ddoSg0vq3iKz2cS2ouX2wSAjUWR6gP7ts_oLM9nWVQ,5219
7
+ pyactivesync/client.py,sha256=R2CRy_CDfYo0dUihHtGhoZcJ5fnYE2agFmvBLuj5BPA,25803
8
+ pyactivesync/exceptions.py,sha256=k8IZm4qRaTqQOOUXif7AO2ufdLO6t64e2vBHSMvjoIg,944
9
+ pyactivesync/models.py,sha256=amQFJMo9Ij_Ipg2JbMfHAPtuXfU0CgnEXyWTD99EkLw,2656
10
+ pyactivesync-0.2.0.dist-info/METADATA,sha256=vYSy4OBX4lNizcMZLbDyLweTyENxy9iZkX-bjwXnix8,5690
11
+ pyactivesync-0.2.0.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
12
+ pyactivesync-0.2.0.dist-info/entry_points.txt,sha256=TqWjGn_lGoXbNjRoflxW12KnUHVywFeSUy9r33_9mJg,55
13
+ pyactivesync-0.2.0.dist-info/licenses/LICENSE,sha256=cZcVt0H9cnb7yEUhuaOjEY44tOenoE73-XlnbE3fTG8,1073
14
+ pyactivesync-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pyactivesync = pyactivesync.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Martin Monperrus
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.