zimbra-client 0.3.2__tar.gz
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.
- zimbra_client-0.3.2/LICENSE +21 -0
- zimbra_client-0.3.2/PKG-INFO +167 -0
- zimbra_client-0.3.2/README.md +144 -0
- zimbra_client-0.3.2/pyproject.toml +33 -0
- zimbra_client-0.3.2/setup.cfg +4 -0
- zimbra_client-0.3.2/tests/test_account.py +99 -0
- zimbra_client-0.3.2/tests/test_calendar.py +55 -0
- zimbra_client-0.3.2/tests/test_client.py +211 -0
- zimbra_client-0.3.2/tests/test_config.py +62 -0
- zimbra_client-0.3.2/tests/test_contacts.py +50 -0
- zimbra_client-0.3.2/tests/test_filters.py +77 -0
- zimbra_client-0.3.2/tests/test_live.py +27 -0
- zimbra_client-0.3.2/tests/test_live_mailbox.py +116 -0
- zimbra_client-0.3.2/tests/test_mail.py +357 -0
- zimbra_client-0.3.2/zimbra_client/__init__.py +92 -0
- zimbra_client-0.3.2/zimbra_client/account.py +225 -0
- zimbra_client-0.3.2/zimbra_client/calendar.py +339 -0
- zimbra_client-0.3.2/zimbra_client/client.py +824 -0
- zimbra_client-0.3.2/zimbra_client/config.py +119 -0
- zimbra_client-0.3.2/zimbra_client/contacts.py +120 -0
- zimbra_client-0.3.2/zimbra_client/errors.py +64 -0
- zimbra_client-0.3.2/zimbra_client/filters.py +191 -0
- zimbra_client-0.3.2/zimbra_client/mail.py +499 -0
- zimbra_client-0.3.2/zimbra_client/models.py +223 -0
- zimbra_client-0.3.2/zimbra_client.egg-info/PKG-INFO +167 -0
- zimbra_client-0.3.2/zimbra_client.egg-info/SOURCES.txt +26 -0
- zimbra_client-0.3.2/zimbra_client.egg-info/dependency_links.txt +1 -0
- zimbra_client-0.3.2/zimbra_client.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ben Chan
|
|
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.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zimbra-client
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: End-user Zimbra SOAP client for mail, contacts, calendar, and account settings
|
|
5
|
+
Author-email: Ben Chan <kpchanaf@connect.ust.hk>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: zimbra,email,soap,mailbox,calendar,contacts
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Topic :: Communications :: Email
|
|
18
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
19
|
+
Requires-Python: >=3.9
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
License-File: LICENSE
|
|
22
|
+
Dynamic: license-file
|
|
23
|
+
|
|
24
|
+
# zimbra-client
|
|
25
|
+
|
|
26
|
+
Python client for the Zimbra end-user SOAP API. Connect with an email account and work with mail, folders, drafts, contacts, calendar, tasks, signatures, preferences, and filter rules through typed methods.
|
|
27
|
+
|
|
28
|
+
## Install
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python -m pip install zimbra-client
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires Python 3.9+. The package uses the standard library only.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from zimbra_client import ZimbraClient, ZimbraConfig
|
|
40
|
+
|
|
41
|
+
config = ZimbraConfig(
|
|
42
|
+
host="https://mail.example.com",
|
|
43
|
+
email="user@example.com",
|
|
44
|
+
password="your-password",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
with ZimbraClient(config) as client:
|
|
48
|
+
inbox = client.search_messages(folder_id="2", limit=10)
|
|
49
|
+
for summary in inbox.messages:
|
|
50
|
+
message = client.get_message(summary.id)
|
|
51
|
+
print(message.subject, message.from_.email)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
You can also pass a mapping or keyword-style dict:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
client = ZimbraClient(
|
|
58
|
+
{
|
|
59
|
+
"host": "mail.example.com",
|
|
60
|
+
"email": "user@example.com",
|
|
61
|
+
"password": "your-password",
|
|
62
|
+
"verify_ssl": True,
|
|
63
|
+
}
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Configuration options
|
|
68
|
+
|
|
69
|
+
| Option | Description |
|
|
70
|
+
|--------|-------------|
|
|
71
|
+
| `host` | Zimbra server hostname or full `https://` URL |
|
|
72
|
+
| `email` | Account email address |
|
|
73
|
+
| `password` | Account password |
|
|
74
|
+
| `username` | Optional login name when it differs from `email` |
|
|
75
|
+
| `verify_ssl` | Validate TLS certificates (default: `False`) |
|
|
76
|
+
| `timeout` | Request timeout in seconds (default: `60`) |
|
|
77
|
+
|
|
78
|
+
## Mailbox
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
with ZimbraClient(config) as client:
|
|
82
|
+
results = client.search_messages(query="from:sender@example.com", limit=25)
|
|
83
|
+
|
|
84
|
+
message = client.get_message("12345")
|
|
85
|
+
print(message.body_text, message.body_html, message.headers)
|
|
86
|
+
|
|
87
|
+
sent = client.send_message(
|
|
88
|
+
to="recipient@example.com",
|
|
89
|
+
subject="Hello",
|
|
90
|
+
text="Plain text",
|
|
91
|
+
html="<p>HTML body</p>",
|
|
92
|
+
)
|
|
93
|
+
print(sent.message_id)
|
|
94
|
+
|
|
95
|
+
client.mark_read(message.id)
|
|
96
|
+
client.move_message(message.id, folder_id="256")
|
|
97
|
+
client.trash_message(message.id)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Drafts, attachments, folders, tags, and other mailbox actions are available on `ZimbraClient`.
|
|
101
|
+
|
|
102
|
+
## Account, contacts, calendar, and filters
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
from datetime import datetime, timedelta, timezone
|
|
106
|
+
|
|
107
|
+
from zimbra_client import FilterRule
|
|
108
|
+
from zimbra_client.filters import filter_file_into, filter_from_address, filter_stop
|
|
109
|
+
|
|
110
|
+
with ZimbraClient(config) as client:
|
|
111
|
+
signatures = client.list_signatures()
|
|
112
|
+
prefs = client.get_prefs("zimbraPrefLocale")
|
|
113
|
+
|
|
114
|
+
contact = client.create_contact(
|
|
115
|
+
{"firstName": "Jane", "lastName": "Doe", "email": "jane@example.com"}
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
start = datetime.now(tz=timezone.utc)
|
|
119
|
+
client.create_appointment(
|
|
120
|
+
"Team sync",
|
|
121
|
+
start,
|
|
122
|
+
start + timedelta(hours=1),
|
|
123
|
+
location="Room A",
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
task = client.create_task("Follow up", text="Send summary")
|
|
127
|
+
client.complete_task(task.id)
|
|
128
|
+
|
|
129
|
+
client.set_filter_rules(
|
|
130
|
+
(
|
|
131
|
+
FilterRule(
|
|
132
|
+
name="Archive reports",
|
|
133
|
+
tests=(filter_from_address("reports@example.com"),),
|
|
134
|
+
actions=(filter_file_into("/Archive"), filter_stop()),
|
|
135
|
+
),
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Advanced SOAP access
|
|
141
|
+
|
|
142
|
+
For API calls that do not have a convenience wrapper yet, use the generic request methods:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
import xml.etree.ElementTree as ET
|
|
146
|
+
|
|
147
|
+
from zimbra_client import ACCOUNT_NAMESPACE, ZimbraClient
|
|
148
|
+
|
|
149
|
+
request = ET.Element(f"{{{ACCOUNT_NAMESPACE}}}GetInfoRequest")
|
|
150
|
+
|
|
151
|
+
with ZimbraClient(config) as client:
|
|
152
|
+
response = client.request_account(request)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
- `request_account()` sends `urn:zimbraAccount` requests
|
|
156
|
+
- `request_mail()` sends `urn:zimbraMail` requests
|
|
157
|
+
- `request()` is an alias that works for either namespace
|
|
158
|
+
|
|
159
|
+
The client authenticates lazily, reuses the session token, retries once on session expiry, and clears credentials from error messages.
|
|
160
|
+
|
|
161
|
+
## Errors
|
|
162
|
+
|
|
163
|
+
The client raises typed exceptions such as `ZimbraAuthenticationError`, `ZimbraConnectionError`, `ZimbraNotFoundError`, `ZimbraLimitError`, and `ZimbraSOAPFault`.
|
|
164
|
+
|
|
165
|
+
## License
|
|
166
|
+
|
|
167
|
+
MIT
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# zimbra-client
|
|
2
|
+
|
|
3
|
+
Python client for the Zimbra end-user SOAP API. Connect with an email account and work with mail, folders, drafts, contacts, calendar, tasks, signatures, preferences, and filter rules through typed methods.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python -m pip install zimbra-client
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Requires Python 3.9+. The package uses the standard library only.
|
|
12
|
+
|
|
13
|
+
## Quick start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from zimbra_client import ZimbraClient, ZimbraConfig
|
|
17
|
+
|
|
18
|
+
config = ZimbraConfig(
|
|
19
|
+
host="https://mail.example.com",
|
|
20
|
+
email="user@example.com",
|
|
21
|
+
password="your-password",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
with ZimbraClient(config) as client:
|
|
25
|
+
inbox = client.search_messages(folder_id="2", limit=10)
|
|
26
|
+
for summary in inbox.messages:
|
|
27
|
+
message = client.get_message(summary.id)
|
|
28
|
+
print(message.subject, message.from_.email)
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
You can also pass a mapping or keyword-style dict:
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
client = ZimbraClient(
|
|
35
|
+
{
|
|
36
|
+
"host": "mail.example.com",
|
|
37
|
+
"email": "user@example.com",
|
|
38
|
+
"password": "your-password",
|
|
39
|
+
"verify_ssl": True,
|
|
40
|
+
}
|
|
41
|
+
)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
### Configuration options
|
|
45
|
+
|
|
46
|
+
| Option | Description |
|
|
47
|
+
|--------|-------------|
|
|
48
|
+
| `host` | Zimbra server hostname or full `https://` URL |
|
|
49
|
+
| `email` | Account email address |
|
|
50
|
+
| `password` | Account password |
|
|
51
|
+
| `username` | Optional login name when it differs from `email` |
|
|
52
|
+
| `verify_ssl` | Validate TLS certificates (default: `False`) |
|
|
53
|
+
| `timeout` | Request timeout in seconds (default: `60`) |
|
|
54
|
+
|
|
55
|
+
## Mailbox
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
with ZimbraClient(config) as client:
|
|
59
|
+
results = client.search_messages(query="from:sender@example.com", limit=25)
|
|
60
|
+
|
|
61
|
+
message = client.get_message("12345")
|
|
62
|
+
print(message.body_text, message.body_html, message.headers)
|
|
63
|
+
|
|
64
|
+
sent = client.send_message(
|
|
65
|
+
to="recipient@example.com",
|
|
66
|
+
subject="Hello",
|
|
67
|
+
text="Plain text",
|
|
68
|
+
html="<p>HTML body</p>",
|
|
69
|
+
)
|
|
70
|
+
print(sent.message_id)
|
|
71
|
+
|
|
72
|
+
client.mark_read(message.id)
|
|
73
|
+
client.move_message(message.id, folder_id="256")
|
|
74
|
+
client.trash_message(message.id)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Drafts, attachments, folders, tags, and other mailbox actions are available on `ZimbraClient`.
|
|
78
|
+
|
|
79
|
+
## Account, contacts, calendar, and filters
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from datetime import datetime, timedelta, timezone
|
|
83
|
+
|
|
84
|
+
from zimbra_client import FilterRule
|
|
85
|
+
from zimbra_client.filters import filter_file_into, filter_from_address, filter_stop
|
|
86
|
+
|
|
87
|
+
with ZimbraClient(config) as client:
|
|
88
|
+
signatures = client.list_signatures()
|
|
89
|
+
prefs = client.get_prefs("zimbraPrefLocale")
|
|
90
|
+
|
|
91
|
+
contact = client.create_contact(
|
|
92
|
+
{"firstName": "Jane", "lastName": "Doe", "email": "jane@example.com"}
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
start = datetime.now(tz=timezone.utc)
|
|
96
|
+
client.create_appointment(
|
|
97
|
+
"Team sync",
|
|
98
|
+
start,
|
|
99
|
+
start + timedelta(hours=1),
|
|
100
|
+
location="Room A",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
task = client.create_task("Follow up", text="Send summary")
|
|
104
|
+
client.complete_task(task.id)
|
|
105
|
+
|
|
106
|
+
client.set_filter_rules(
|
|
107
|
+
(
|
|
108
|
+
FilterRule(
|
|
109
|
+
name="Archive reports",
|
|
110
|
+
tests=(filter_from_address("reports@example.com"),),
|
|
111
|
+
actions=(filter_file_into("/Archive"), filter_stop()),
|
|
112
|
+
),
|
|
113
|
+
)
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Advanced SOAP access
|
|
118
|
+
|
|
119
|
+
For API calls that do not have a convenience wrapper yet, use the generic request methods:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
import xml.etree.ElementTree as ET
|
|
123
|
+
|
|
124
|
+
from zimbra_client import ACCOUNT_NAMESPACE, ZimbraClient
|
|
125
|
+
|
|
126
|
+
request = ET.Element(f"{{{ACCOUNT_NAMESPACE}}}GetInfoRequest")
|
|
127
|
+
|
|
128
|
+
with ZimbraClient(config) as client:
|
|
129
|
+
response = client.request_account(request)
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
- `request_account()` sends `urn:zimbraAccount` requests
|
|
133
|
+
- `request_mail()` sends `urn:zimbraMail` requests
|
|
134
|
+
- `request()` is an alias that works for either namespace
|
|
135
|
+
|
|
136
|
+
The client authenticates lazily, reuses the session token, retries once on session expiry, and clears credentials from error messages.
|
|
137
|
+
|
|
138
|
+
## Errors
|
|
139
|
+
|
|
140
|
+
The client raises typed exceptions such as `ZimbraAuthenticationError`, `ZimbraConnectionError`, `ZimbraNotFoundError`, `ZimbraLimitError`, and `ZimbraSOAPFault`.
|
|
141
|
+
|
|
142
|
+
## License
|
|
143
|
+
|
|
144
|
+
MIT
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "zimbra-client"
|
|
7
|
+
version = "0.3.2"
|
|
8
|
+
description = "End-user Zimbra SOAP client for mail, contacts, calendar, and account settings"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [
|
|
14
|
+
{ name = "Ben Chan", email = "kpchanaf@connect.ust.hk" },
|
|
15
|
+
]
|
|
16
|
+
keywords = ["zimbra", "email", "soap", "mailbox", "calendar", "contacts"]
|
|
17
|
+
classifiers = [
|
|
18
|
+
"Development Status :: 4 - Beta",
|
|
19
|
+
"Intended Audience :: Developers",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.9",
|
|
23
|
+
"Programming Language :: Python :: 3.10",
|
|
24
|
+
"Programming Language :: Python :: 3.11",
|
|
25
|
+
"Programming Language :: Python :: 3.12",
|
|
26
|
+
"Programming Language :: Python :: 3.13",
|
|
27
|
+
"Topic :: Communications :: Email",
|
|
28
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
29
|
+
]
|
|
30
|
+
dependencies = []
|
|
31
|
+
|
|
32
|
+
[tool.setuptools.packages.find]
|
|
33
|
+
include = ["zimbra_client*"]
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import unittest
|
|
3
|
+
import urllib.error
|
|
4
|
+
import xml.etree.ElementTree as ET
|
|
5
|
+
from unittest.mock import patch
|
|
6
|
+
|
|
7
|
+
from zimbra_client import ZimbraClient, ZimbraConfig, local_name
|
|
8
|
+
|
|
9
|
+
from tests.support.fake_zimbra import FakeZimbraDispatcher, request_name, soap_body_from_request
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class AccountTests(unittest.TestCase):
|
|
13
|
+
def setUp(self):
|
|
14
|
+
self.config = ZimbraConfig(
|
|
15
|
+
host="https://mail.example.com/",
|
|
16
|
+
email="user@example.com",
|
|
17
|
+
password="secret<&password",
|
|
18
|
+
)
|
|
19
|
+
self.client = ZimbraClient(self.config)
|
|
20
|
+
self.dispatcher = FakeZimbraDispatcher()
|
|
21
|
+
|
|
22
|
+
def test_list_and_create_signatures(self):
|
|
23
|
+
with patch("zimbra_client.client.urllib.request.urlopen", self.dispatcher):
|
|
24
|
+
signatures = self.client.list_signatures()
|
|
25
|
+
created = self.client.create_signature("Work", text="Hello & welcome")
|
|
26
|
+
|
|
27
|
+
self.assertEqual(signatures[0].name, "Default")
|
|
28
|
+
self.assertEqual(created.id, "11")
|
|
29
|
+
self.assertEqual(created.text_plain, "Hello & welcome")
|
|
30
|
+
|
|
31
|
+
def test_modify_and_delete_signature(self):
|
|
32
|
+
captured = []
|
|
33
|
+
|
|
34
|
+
def dispatch(request, timeout, context):
|
|
35
|
+
captured.append(soap_body_from_request(request))
|
|
36
|
+
return self.dispatcher(request, timeout, context)
|
|
37
|
+
|
|
38
|
+
with patch("zimbra_client.client.urllib.request.urlopen", dispatch):
|
|
39
|
+
updated = self.client.modify_signature("10", name="Updated")
|
|
40
|
+
self.client.delete_signature("10")
|
|
41
|
+
|
|
42
|
+
signature = next(elem for elem in captured[1] if local_name(elem.tag) == "signature")
|
|
43
|
+
self.assertEqual(updated.name, "Updated")
|
|
44
|
+
self.assertEqual(signature.get("id"), "10")
|
|
45
|
+
|
|
46
|
+
def test_identities_and_prefs(self):
|
|
47
|
+
with patch("zimbra_client.client.urllib.request.urlopen", self.dispatcher):
|
|
48
|
+
identities = self.client.list_identities()
|
|
49
|
+
created = self.client.create_identity(
|
|
50
|
+
"Work",
|
|
51
|
+
{"zimbraPrefFromDisplay": "Work User"},
|
|
52
|
+
)
|
|
53
|
+
modified = self.client.modify_identity("21", {"zimbraPrefFromDisplay": "Updated"})
|
|
54
|
+
self.client.delete_identity("21")
|
|
55
|
+
prefs = self.client.get_prefs("zimbraPrefLocale")
|
|
56
|
+
updated_prefs = self.client.set_prefs({"zimbraPrefLocale": "en_GB"})
|
|
57
|
+
|
|
58
|
+
self.assertEqual(identities[0].attrs["zimbraPrefFromAddress"], "user@example.com")
|
|
59
|
+
self.assertEqual(created.name, "Work")
|
|
60
|
+
self.assertEqual(modified.id, "21")
|
|
61
|
+
self.assertEqual(prefs[0].name, "zimbraPrefLocale")
|
|
62
|
+
self.assertEqual(updated_prefs[0].value, "en_GB")
|
|
63
|
+
|
|
64
|
+
def test_signature_request_xml_is_escaped(self):
|
|
65
|
+
captured = []
|
|
66
|
+
|
|
67
|
+
def dispatch(request, timeout, context):
|
|
68
|
+
captured.append(request.data)
|
|
69
|
+
return self.dispatcher(request, timeout, context)
|
|
70
|
+
|
|
71
|
+
with patch("zimbra_client.client.urllib.request.urlopen", dispatch):
|
|
72
|
+
self.client.create_signature("Sig", text="a & b")
|
|
73
|
+
|
|
74
|
+
self.assertIn(b"a & b", captured[1])
|
|
75
|
+
self.assertNotIn("secret<&password", captured[1].decode("utf-8"))
|
|
76
|
+
|
|
77
|
+
def test_auth_fault_does_not_leak_password(self):
|
|
78
|
+
fault = (
|
|
79
|
+
'<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">'
|
|
80
|
+
"<soap:Body><soap:Fault>"
|
|
81
|
+
"<soap:Reason><soap:Text>failed for secret<&password</soap:Text></soap:Reason>"
|
|
82
|
+
'<detail><Error xmlns="urn:zimbra"><Code>account.AUTH_FAILED</Code></Error></detail>'
|
|
83
|
+
"</soap:Fault></soap:Body></soap:Envelope>"
|
|
84
|
+
).encode("utf-8")
|
|
85
|
+
error = urllib.error.HTTPError(
|
|
86
|
+
"https://mail.example.com/service/soap",
|
|
87
|
+
500,
|
|
88
|
+
"Server Error",
|
|
89
|
+
None,
|
|
90
|
+
io.BytesIO(fault),
|
|
91
|
+
)
|
|
92
|
+
with patch("zimbra_client.client.urllib.request.urlopen", side_effect=error):
|
|
93
|
+
with self.assertRaises(Exception) as caught:
|
|
94
|
+
self.client.list_signatures()
|
|
95
|
+
self.assertNotIn("secret<&password", str(caught.exception))
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
if __name__ == "__main__":
|
|
99
|
+
unittest.main()
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from datetime import datetime, timezone
|
|
3
|
+
from unittest.mock import patch
|
|
4
|
+
|
|
5
|
+
from zimbra_client import ZimbraClient, ZimbraConfig
|
|
6
|
+
|
|
7
|
+
from tests.support.fake_zimbra import FakeZimbraDispatcher
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class CalendarTests(unittest.TestCase):
|
|
11
|
+
def setUp(self):
|
|
12
|
+
self.config = ZimbraConfig(
|
|
13
|
+
host="https://mail.example.com/",
|
|
14
|
+
email="user@example.com",
|
|
15
|
+
password="secret",
|
|
16
|
+
)
|
|
17
|
+
self.client = ZimbraClient(self.config)
|
|
18
|
+
self.dispatcher = FakeZimbraDispatcher()
|
|
19
|
+
self.start = datetime(2023, 11, 14, 22, 13, 20, tzinfo=timezone.utc)
|
|
20
|
+
self.end = datetime(2023, 11, 14, 23, 13, 20, tzinfo=timezone.utc)
|
|
21
|
+
|
|
22
|
+
def test_appointment_create_list_get_cancel(self):
|
|
23
|
+
with patch("zimbra_client.client.urllib.request.urlopen", self.dispatcher):
|
|
24
|
+
created = self.client.create_appointment(
|
|
25
|
+
"Meeting",
|
|
26
|
+
self.start,
|
|
27
|
+
self.end,
|
|
28
|
+
location="Room A",
|
|
29
|
+
attendees=["guest@example.com"],
|
|
30
|
+
)
|
|
31
|
+
listed = self.client.list_appointments(self.start, self.end)
|
|
32
|
+
fetched = self.client.get_appointment("400")
|
|
33
|
+
self.client.cancel_appointment("400")
|
|
34
|
+
|
|
35
|
+
self.assertEqual(created.id, "400")
|
|
36
|
+
self.assertEqual(created.name, "Meeting")
|
|
37
|
+
self.assertEqual(listed[0].location, "Room A")
|
|
38
|
+
self.assertEqual(fetched.uid, "uid-400")
|
|
39
|
+
|
|
40
|
+
def test_task_create_get_modify_complete_list(self):
|
|
41
|
+
due = datetime(2023, 11, 16, 10, 0, tzinfo=timezone.utc)
|
|
42
|
+
with patch("zimbra_client.client.urllib.request.urlopen", self.dispatcher):
|
|
43
|
+
created = self.client.create_task("Follow up", due=due, text="Details")
|
|
44
|
+
fetched = self.client.get_task("500")
|
|
45
|
+
completed = self.client.complete_task("500")
|
|
46
|
+
tasks = self.client.list_tasks(limit=10)
|
|
47
|
+
|
|
48
|
+
self.assertEqual(created.id, "500")
|
|
49
|
+
self.assertEqual(fetched.subject, "Follow up")
|
|
50
|
+
self.assertTrue(completed.completed)
|
|
51
|
+
self.assertEqual(tasks[0].id, "501")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
if __name__ == "__main__":
|
|
55
|
+
unittest.main()
|