gweasysoap 0.1.0__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.
- gweasysoap-0.1.0/.gitignore +8 -0
- gweasysoap-0.1.0/LICENSE +21 -0
- gweasysoap-0.1.0/PKG-INFO +92 -0
- gweasysoap-0.1.0/PYPI_UPLOAD.md +76 -0
- gweasysoap-0.1.0/README.md +73 -0
- gweasysoap-0.1.0/gweasysoap/__init__.py +8 -0
- gweasysoap-0.1.0/gweasysoap/client.py +460 -0
- gweasysoap-0.1.0/gweasysoap/exceptions.py +6 -0
- gweasysoap-0.1.0/issues.txt +28 -0
- gweasysoap-0.1.0/pyproject.toml +28 -0
- gweasysoap-0.1.0/tests/01_login_password.py +8 -0
- gweasysoap-0.1.0/tests/02_login_trusted_app.py +15 -0
- gweasysoap-0.1.0/tests/03_user_list.py +12 -0
- gweasysoap-0.1.0/tests/04_login_any_user.py +10 -0
- gweasysoap-0.1.0/tests/05_mailbox_items.py +18 -0
- gweasysoap-0.1.0/tests/06_calendar.py +14 -0
- gweasysoap-0.1.0/tests/07_address_book.py +16 -0
- gweasysoap-0.1.0/tests/08_filter_items.py +17 -0
- gweasysoap-0.1.0/tests/09_read_body.py +18 -0
- gweasysoap-0.1.0/tests/10_send_mail.py +16 -0
- gweasysoap-0.1.0/tests/11_create_appointment.py +27 -0
- gweasysoap-0.1.0/tests/README.md +40 -0
- gweasysoap-0.1.0/tests/config.py +10 -0
gweasysoap-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Raymond Hulha
|
|
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,92 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: gweasysoap
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A helper library for the GroupWise SOAP API (gwsoap).
|
|
5
|
+
Project-URL: Homepage, https://github.com/rhulha/GWEasySoap
|
|
6
|
+
Project-URL: Documentation, https://github.com/rhulha/GWEasySoap#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/rhulha/GWEasySoap
|
|
8
|
+
Project-URL: Bug Reports, https://github.com/rhulha/GWEasySoap/issues
|
|
9
|
+
License: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: groupwise,gwsoap,micro focus,novell,soap
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Topic :: Communications :: Email
|
|
16
|
+
Requires-Python: >=3.9
|
|
17
|
+
Requires-Dist: gwsoap>=1.0.3
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# gweasysoap
|
|
21
|
+
|
|
22
|
+
A small, pythonic helper library on top of the auto-generated
|
|
23
|
+
[`gwsoap`](https://pypi.org/project/gwsoap/) GroupWise SOAP client. It hides the
|
|
24
|
+
request/context boilerplate and gives you simple methods for logging in,
|
|
25
|
+
reading folders and items, sending mail, and creating appointments.
|
|
26
|
+
|
|
27
|
+
## Installation
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install gweasysoap
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This pulls in `gwsoap>=1.0.3`.
|
|
34
|
+
|
|
35
|
+
## Quick start
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
from gweasysoap import GWEasySoap
|
|
39
|
+
|
|
40
|
+
# Use it as a context manager so the session is always logged out.
|
|
41
|
+
with GWEasySoap.connect_trusted_app(
|
|
42
|
+
"https://gw.example.com:7191/soap", "jdoe", "MyApp", "trusted-key"
|
|
43
|
+
) as gw:
|
|
44
|
+
print("Logged in as", gw.user_name, gw.user_email)
|
|
45
|
+
|
|
46
|
+
mailbox = gw.get_mailbox()
|
|
47
|
+
for item in gw.get_folder_items(mailbox.id, count=20):
|
|
48
|
+
print(item.subject)
|
|
49
|
+
|
|
50
|
+
gw.send_mail(
|
|
51
|
+
subject="Hello",
|
|
52
|
+
body_text="Sent from gweasysoap",
|
|
53
|
+
recipients=[{"display_name": "Jane", "email": "jane@example.com"}],
|
|
54
|
+
)
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Logging in
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
# Username / password
|
|
61
|
+
gw = GWEasySoap.connect(endpoint, "jdoe", "secret")
|
|
62
|
+
|
|
63
|
+
# Trusted application key (impersonate a specific user)
|
|
64
|
+
gw = GWEasySoap.connect_trusted_app(endpoint, "jdoe", "MyApp", "trusted-key")
|
|
65
|
+
|
|
66
|
+
# Trusted application key, first usable mailbox on the post office
|
|
67
|
+
gw = GWEasySoap.connect_any_user(endpoint, "MyApp", "trusted-key")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Each `connect*` factory raises `GWEasySoapError` on failure. The matching
|
|
71
|
+
instance methods (`login`, `login_trusted_app`, `login_with_any_user`) return a
|
|
72
|
+
boolean instead, if you prefer to manage the instance yourself.
|
|
73
|
+
|
|
74
|
+
Constructor options: `application` (the name reported to GroupWise),
|
|
75
|
+
`debug`, and `verify_ssl`.
|
|
76
|
+
|
|
77
|
+
### What you can do
|
|
78
|
+
|
|
79
|
+
- **Users / mailboxes** – `get_user_list`, `get_user_mailboxes`
|
|
80
|
+
- **Address book** – `get_system_address_book`, `get_address_book_entries`
|
|
81
|
+
- **Folders** – `get_mailbox`, `get_trash_folder`, `get_calendar`,
|
|
82
|
+
`get_system_folder`, `get_folder_list`, `get_shared_folder_by_name`
|
|
83
|
+
- **Items** – `get_items`, `get_folder_items`, `get_calendar_items`,
|
|
84
|
+
`get_item`, `get_full_item`, `get_body`, `remove_item`
|
|
85
|
+
- **Mail** – `send_mail`
|
|
86
|
+
- **Appointments** – `create_appointment`, `create_appointment_in_folder`,
|
|
87
|
+
`add_category`
|
|
88
|
+
- **Session** – `whoami`, `user_name`, `user_email`, `logout`
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# PyPI Upload Instructions
|
|
2
|
+
|
|
3
|
+
## Prerequisites
|
|
4
|
+
|
|
5
|
+
1. Install build tools:
|
|
6
|
+
```bash
|
|
7
|
+
pip install --upgrade build twine
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
2. Create accounts on:
|
|
11
|
+
- PyPI: https://pypi.org/account/register/
|
|
12
|
+
- TestPyPI: https://test.pypi.org/account/register/
|
|
13
|
+
|
|
14
|
+
3. Create API tokens:
|
|
15
|
+
- PyPI: https://pypi.org/manage/account/token/
|
|
16
|
+
- TestPyPI: https://test.pypi.org/manage/account/token/
|
|
17
|
+
|
|
18
|
+
## Build the Package
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
python -m build
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
This creates:
|
|
25
|
+
- `dist/moonpie-X.Y.Z-py3-none-any.whl` (wheel)
|
|
26
|
+
- `dist/moonpie-X.Y.Z.tar.gz` (source distribution)
|
|
27
|
+
|
|
28
|
+
## Upload to TestPyPI (Recommended First)
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
python -m twine upload --repository testpypi dist/*
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Test installation:
|
|
35
|
+
```bash
|
|
36
|
+
pip install --index-url https://test.pypi.org/simple/ moonpie
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Upload to PyPI (Production)
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python -m twine upload dist/*
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or use API token:
|
|
46
|
+
```bash
|
|
47
|
+
python -m twine upload --username __token__ --password pypi-YOUR_TOKEN_HERE dist/*
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Test Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install moonpie
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Test the CLI:
|
|
57
|
+
```bash
|
|
58
|
+
moonpie
|
|
59
|
+
moonpie examples/examples.lua
|
|
60
|
+
python -m moonpie
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Update Version
|
|
64
|
+
|
|
65
|
+
Before uploading a new version:
|
|
66
|
+
1. Update version in `moonpie/__init__.py`
|
|
67
|
+
2. Clean old builds: `rm -rf build dist *.egg-info`
|
|
68
|
+
3. Rebuild: `python -m build`
|
|
69
|
+
4. Upload: `python -m twine upload dist/*`
|
|
70
|
+
|
|
71
|
+
## Notes
|
|
72
|
+
|
|
73
|
+
- Version numbers must be unique (can't overwrite)
|
|
74
|
+
- Use semantic versioning (MAJOR.MINOR.PATCH)
|
|
75
|
+
- TestPyPI is cleared regularly, use for testing only
|
|
76
|
+
- Keep your API tokens secure
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# gweasysoap
|
|
2
|
+
|
|
3
|
+
A small, pythonic helper library on top of the auto-generated
|
|
4
|
+
[`gwsoap`](https://pypi.org/project/gwsoap/) GroupWise SOAP client. It hides the
|
|
5
|
+
request/context boilerplate and gives you simple methods for logging in,
|
|
6
|
+
reading folders and items, sending mail, and creating appointments.
|
|
7
|
+
|
|
8
|
+
## Installation
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install gweasysoap
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
This pulls in `gwsoap>=1.0.3`.
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
from gweasysoap import GWEasySoap
|
|
20
|
+
|
|
21
|
+
# Use it as a context manager so the session is always logged out.
|
|
22
|
+
with GWEasySoap.connect_trusted_app(
|
|
23
|
+
"https://gw.example.com:7191/soap", "jdoe", "MyApp", "trusted-key"
|
|
24
|
+
) as gw:
|
|
25
|
+
print("Logged in as", gw.user_name, gw.user_email)
|
|
26
|
+
|
|
27
|
+
mailbox = gw.get_mailbox()
|
|
28
|
+
for item in gw.get_folder_items(mailbox.id, count=20):
|
|
29
|
+
print(item.subject)
|
|
30
|
+
|
|
31
|
+
gw.send_mail(
|
|
32
|
+
subject="Hello",
|
|
33
|
+
body_text="Sent from gweasysoap",
|
|
34
|
+
recipients=[{"display_name": "Jane", "email": "jane@example.com"}],
|
|
35
|
+
)
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Logging in
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
# Username / password
|
|
42
|
+
gw = GWEasySoap.connect(endpoint, "jdoe", "secret")
|
|
43
|
+
|
|
44
|
+
# Trusted application key (impersonate a specific user)
|
|
45
|
+
gw = GWEasySoap.connect_trusted_app(endpoint, "jdoe", "MyApp", "trusted-key")
|
|
46
|
+
|
|
47
|
+
# Trusted application key, first usable mailbox on the post office
|
|
48
|
+
gw = GWEasySoap.connect_any_user(endpoint, "MyApp", "trusted-key")
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Each `connect*` factory raises `GWEasySoapError` on failure. The matching
|
|
52
|
+
instance methods (`login`, `login_trusted_app`, `login_with_any_user`) return a
|
|
53
|
+
boolean instead, if you prefer to manage the instance yourself.
|
|
54
|
+
|
|
55
|
+
Constructor options: `application` (the name reported to GroupWise),
|
|
56
|
+
`debug`, and `verify_ssl`.
|
|
57
|
+
|
|
58
|
+
### What you can do
|
|
59
|
+
|
|
60
|
+
- **Users / mailboxes** – `get_user_list`, `get_user_mailboxes`
|
|
61
|
+
- **Address book** – `get_system_address_book`, `get_address_book_entries`
|
|
62
|
+
- **Folders** – `get_mailbox`, `get_trash_folder`, `get_calendar`,
|
|
63
|
+
`get_system_folder`, `get_folder_list`, `get_shared_folder_by_name`
|
|
64
|
+
- **Items** – `get_items`, `get_folder_items`, `get_calendar_items`,
|
|
65
|
+
`get_item`, `get_full_item`, `get_body`, `remove_item`
|
|
66
|
+
- **Mail** – `send_mail`
|
|
67
|
+
- **Appointments** – `create_appointment`, `create_appointment_in_folder`,
|
|
68
|
+
`add_category`
|
|
69
|
+
- **Session** – `whoami`, `user_name`, `user_email`, `logout`
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
MIT
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""gweasysoap - a pythonic helper library for the GroupWise SOAP API."""
|
|
2
|
+
|
|
3
|
+
from .client import GWEasySoap, GW_SYSTEM_ADDRESS_BOOK_NAME
|
|
4
|
+
from .exceptions import GWEasySoapError
|
|
5
|
+
|
|
6
|
+
__version__ = "0.1.0"
|
|
7
|
+
|
|
8
|
+
__all__ = ["GWEasySoap", "GWEasySoapError", "GW_SYSTEM_ADDRESS_BOOK_NAME"]
|
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
"""A pythonic helper around the auto-generated ``gwsoap`` GroupWise SOAP client."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Any, Mapping, Optional, Sequence, Union
|
|
6
|
+
|
|
7
|
+
from gwsoap.service.groupwise_client import GroupWiseClient
|
|
8
|
+
from gwsoap.soap.exceptions import SoapFaultException
|
|
9
|
+
from gwsoap.soap.request_context import RequestContext
|
|
10
|
+
|
|
11
|
+
from gwsoap.methods.LoginRequest import LoginRequest
|
|
12
|
+
from gwsoap.methods.LogoutRequest import LogoutRequest
|
|
13
|
+
from gwsoap.methods.GetUserListRequest import GetUserListRequest
|
|
14
|
+
from gwsoap.methods.GetAddressBookListRequest import GetAddressBookListRequest
|
|
15
|
+
from gwsoap.methods.GetItemsRequest import GetItemsRequest
|
|
16
|
+
from gwsoap.methods.GetItemRequest import GetItemRequest
|
|
17
|
+
from gwsoap.methods.RemoveItemRequest import RemoveItemRequest
|
|
18
|
+
from gwsoap.methods.SendItemRequest import SendItemRequest
|
|
19
|
+
from gwsoap.methods.CreateItemRequest import CreateItemRequest
|
|
20
|
+
from gwsoap.methods.ModifyItemRequest import ModifyItemRequest
|
|
21
|
+
from gwsoap.methods.GetFolderListRequest import GetFolderListRequest
|
|
22
|
+
|
|
23
|
+
from gwsoap.types.AddressBookItem import AddressBookItem
|
|
24
|
+
from gwsoap.types.Appointment import Appointment
|
|
25
|
+
from gwsoap.types.CategoryRefList import CategoryRefList
|
|
26
|
+
from gwsoap.types.ContainerRef import ContainerRef
|
|
27
|
+
from gwsoap.types.Custom import Custom
|
|
28
|
+
from gwsoap.types.CustomList import CustomList
|
|
29
|
+
from gwsoap.types.Distribution import Distribution
|
|
30
|
+
from gwsoap.types.DistributionType import DistributionType
|
|
31
|
+
from gwsoap.types.FolderType import FolderType
|
|
32
|
+
from gwsoap.types.From import From
|
|
33
|
+
from gwsoap.types.ItemChanges import ItemChanges
|
|
34
|
+
from gwsoap.types.Mail import Mail
|
|
35
|
+
from gwsoap.types.MessageBody import MessageBody
|
|
36
|
+
from gwsoap.types.MessagePart import MessagePart
|
|
37
|
+
from gwsoap.types.PlainText import PlainText
|
|
38
|
+
from gwsoap.types.Recipient import Recipient
|
|
39
|
+
from gwsoap.types.RecipientList import RecipientList
|
|
40
|
+
from gwsoap.types.RecipientType import RecipientType
|
|
41
|
+
from gwsoap.types.SharedFolder import SharedFolder
|
|
42
|
+
from gwsoap.types.SystemFolder import SystemFolder
|
|
43
|
+
from gwsoap.types.TrustedApplication import TrustedApplication
|
|
44
|
+
|
|
45
|
+
from .exceptions import GWEasySoapError
|
|
46
|
+
|
|
47
|
+
logger = logging.getLogger(__name__)
|
|
48
|
+
|
|
49
|
+
GW_SYSTEM_ADDRESS_BOOK_NAME = "GroupWiseSystemAddressBook"
|
|
50
|
+
|
|
51
|
+
RecipientLike = Union[Mapping[str, Any], Any]
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class GWEasySoap:
|
|
55
|
+
"""High-level, session-aware wrapper for the GroupWise SOAP API.
|
|
56
|
+
|
|
57
|
+
Create it directly and call one of the ``login*`` methods, use one of the
|
|
58
|
+
``connect*`` classmethod factories, or use it as a context manager so the
|
|
59
|
+
session is always logged out::
|
|
60
|
+
|
|
61
|
+
with GWEasySoap.connect_trusted_app(endpoint, user, app, key) as gw:
|
|
62
|
+
mailbox = gw.get_mailbox()
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
def __init__(self, application: str = "GWEasySoap", debug: bool = False,
|
|
66
|
+
verify_ssl: bool = True):
|
|
67
|
+
self.client: Optional[GroupWiseClient] = None
|
|
68
|
+
self.session: Optional[str] = None
|
|
69
|
+
self.login_resp = None
|
|
70
|
+
self.application = application
|
|
71
|
+
self.debug = debug
|
|
72
|
+
self.verify_ssl = verify_ssl
|
|
73
|
+
|
|
74
|
+
# -- factories ---------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
@classmethod
|
|
77
|
+
def connect(cls, endpoint: str, username: str, password: str, **kwargs) -> "GWEasySoap":
|
|
78
|
+
"""Log in with a username/password and return the connected instance."""
|
|
79
|
+
gw = cls(**kwargs)
|
|
80
|
+
if not gw.login(endpoint, username, password):
|
|
81
|
+
raise GWEasySoapError(f"Login failed for user {username!r}")
|
|
82
|
+
return gw
|
|
83
|
+
|
|
84
|
+
@classmethod
|
|
85
|
+
def connect_trusted_app(cls, endpoint: str, username: str, tapp_name: str,
|
|
86
|
+
tapp_key: str, **kwargs) -> "GWEasySoap":
|
|
87
|
+
"""Log in via a trusted application key and return the connected instance."""
|
|
88
|
+
gw = cls(**kwargs)
|
|
89
|
+
if not gw.login_trusted_app(endpoint, username, tapp_name, tapp_key):
|
|
90
|
+
raise GWEasySoapError(f"Trusted app login failed for user {username!r}")
|
|
91
|
+
return gw
|
|
92
|
+
|
|
93
|
+
@classmethod
|
|
94
|
+
def connect_any_user(cls, endpoint: str, tapp_name: str, tapp_key: str,
|
|
95
|
+
**kwargs) -> "GWEasySoap":
|
|
96
|
+
"""Log in as the first usable mailbox on the post office (trusted app)."""
|
|
97
|
+
gw = cls(**kwargs)
|
|
98
|
+
gw.login_with_any_user(endpoint, tapp_name, tapp_key)
|
|
99
|
+
return gw
|
|
100
|
+
|
|
101
|
+
# -- context manager ---------------------------------------------------
|
|
102
|
+
|
|
103
|
+
def __enter__(self) -> "GWEasySoap":
|
|
104
|
+
return self
|
|
105
|
+
|
|
106
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
107
|
+
self.logout()
|
|
108
|
+
|
|
109
|
+
# -- login -------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
def login(self, endpoint: str, username: str, password: str) -> bool:
|
|
112
|
+
"""Log in with a plain username and password."""
|
|
113
|
+
auth = PlainText(username=username, password=password)
|
|
114
|
+
return self._login_with_redirect(endpoint, auth)
|
|
115
|
+
|
|
116
|
+
def login_trusted_app(self, endpoint: str, username: str, tapp_name: str,
|
|
117
|
+
tapp_key: str) -> bool:
|
|
118
|
+
"""Log in on behalf of ``username`` using a trusted application key."""
|
|
119
|
+
auth = TrustedApplication(username=username, name=tapp_name, key=tapp_key)
|
|
120
|
+
return self._login_with_redirect(endpoint, auth)
|
|
121
|
+
|
|
122
|
+
def login_with_any_user(self, endpoint: str, tapp_name: str, tapp_key: str) -> bool:
|
|
123
|
+
"""Try the trusted-app login against post office users until one succeeds."""
|
|
124
|
+
last_fault = None
|
|
125
|
+
for tried, info in enumerate(self.get_user_list(endpoint, tapp_name, tapp_key)):
|
|
126
|
+
if info.recip_type != RecipientType.USER or not info.userid:
|
|
127
|
+
continue
|
|
128
|
+
if tried >= 10:
|
|
129
|
+
break
|
|
130
|
+
try:
|
|
131
|
+
if self.login_trusted_app(endpoint, info.userid, tapp_name, tapp_key):
|
|
132
|
+
return True
|
|
133
|
+
except SoapFaultException as e:
|
|
134
|
+
last_fault = e
|
|
135
|
+
if last_fault is not None:
|
|
136
|
+
raise last_fault
|
|
137
|
+
raise GWEasySoapError(
|
|
138
|
+
"No usable GroupWise mailbox found - does this post office have any?"
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def _login_with_redirect(self, endpoint: str, auth) -> bool:
|
|
142
|
+
login_resp = self._attempt_login(endpoint, auth)
|
|
143
|
+
if login_resp is not None and login_resp.redirect_to_host:
|
|
144
|
+
# The mailbox lives on another post office; its Host carries no
|
|
145
|
+
# scheme, so retry against that POA trying both http and https.
|
|
146
|
+
host = login_resp.redirect_to_host[0]
|
|
147
|
+
logger.info("Login redirected to host %s:%s", host.ip_address, host.port)
|
|
148
|
+
login_resp = self._attempt_redirect(endpoint, host, auth)
|
|
149
|
+
|
|
150
|
+
if login_resp is None or login_resp.status is None or login_resp.status.code != 0:
|
|
151
|
+
return False
|
|
152
|
+
self.login_resp = login_resp
|
|
153
|
+
self.session = login_resp.session
|
|
154
|
+
return True
|
|
155
|
+
|
|
156
|
+
def _attempt_login(self, endpoint: str, auth):
|
|
157
|
+
self.client = GroupWiseClient(endpoint, debug=self.debug, verify_ssl=self.verify_ssl)
|
|
158
|
+
login_req = LoginRequest(auth=auth, application=self.application, language="en")
|
|
159
|
+
return self.client.login(login_req)
|
|
160
|
+
|
|
161
|
+
def _attempt_redirect(self, endpoint: str, host, auth):
|
|
162
|
+
import http.client
|
|
163
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
164
|
+
|
|
165
|
+
parts = urlsplit(endpoint)
|
|
166
|
+
netloc = f"{host.ip_address}:{host.port}"
|
|
167
|
+
schemes = [parts.scheme, "https" if parts.scheme == "http" else "http"]
|
|
168
|
+
last_err = None
|
|
169
|
+
for scheme in schemes:
|
|
170
|
+
target = urlunsplit((scheme, netloc, parts.path, parts.query, parts.fragment))
|
|
171
|
+
try:
|
|
172
|
+
return self._attempt_login(target, auth)
|
|
173
|
+
except (OSError, http.client.HTTPException) as e:
|
|
174
|
+
last_err = e
|
|
175
|
+
if last_err is not None:
|
|
176
|
+
raise last_err
|
|
177
|
+
return None
|
|
178
|
+
|
|
179
|
+
# -- connected user ----------------------------------------------------
|
|
180
|
+
|
|
181
|
+
def whoami(self):
|
|
182
|
+
"""Return the ``userinfo`` of the logged-in user, or ``None``."""
|
|
183
|
+
return self.login_resp.userinfo if self.login_resp else None
|
|
184
|
+
|
|
185
|
+
@property
|
|
186
|
+
def user_name(self) -> str:
|
|
187
|
+
info = self.whoami()
|
|
188
|
+
return (info.name if info else "") or ""
|
|
189
|
+
|
|
190
|
+
@property
|
|
191
|
+
def user_email(self) -> str:
|
|
192
|
+
info = self.whoami()
|
|
193
|
+
return (getattr(info, "email", None) if info else "") or ""
|
|
194
|
+
|
|
195
|
+
# -- users (no session needed) ----------------------------------------
|
|
196
|
+
|
|
197
|
+
def get_user_list(self, endpoint: str, tapp_name: str, tapp_key: str) -> list:
|
|
198
|
+
"""Return the raw user records for the post office at ``endpoint``."""
|
|
199
|
+
client = GroupWiseClient(endpoint, debug=self.debug, verify_ssl=self.verify_ssl)
|
|
200
|
+
response = client.get_user_list(GetUserListRequest(name=tapp_name, key=tapp_key))
|
|
201
|
+
self._raise_on_bad_status(response, "get_user_list")
|
|
202
|
+
if response and response.users and response.users.user:
|
|
203
|
+
return response.users.user
|
|
204
|
+
return []
|
|
205
|
+
|
|
206
|
+
def get_user_mailboxes(self, endpoint: str, tapp_name: str, tapp_key: str) -> list:
|
|
207
|
+
"""Return the post office mailboxes as plain dicts."""
|
|
208
|
+
return [
|
|
209
|
+
{
|
|
210
|
+
"userid": u.userid,
|
|
211
|
+
"display_name": u.name or u.userid,
|
|
212
|
+
"email": u.email or u.userid,
|
|
213
|
+
"uuid": u.uuid,
|
|
214
|
+
}
|
|
215
|
+
for u in self.get_user_list(endpoint, tapp_name, tapp_key)
|
|
216
|
+
if u.recip_type == RecipientType.USER and u.userid
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
# -- address book ------------------------------------------------------
|
|
220
|
+
|
|
221
|
+
def get_system_address_book(self):
|
|
222
|
+
"""Return the GroupWise system address book, or ``None``."""
|
|
223
|
+
response = self.client.get_address_book_list(GetAddressBookListRequest(), self._ctx)
|
|
224
|
+
books = response.books
|
|
225
|
+
if not books or not books.book:
|
|
226
|
+
return None
|
|
227
|
+
for book in books.book:
|
|
228
|
+
if book and book.id and book.id.startswith(GW_SYSTEM_ADDRESS_BOOK_NAME + "@"):
|
|
229
|
+
return book
|
|
230
|
+
return None
|
|
231
|
+
|
|
232
|
+
def get_address_book_entries(self, book, count: Optional[int] = None) -> list:
|
|
233
|
+
"""Return the :class:`AddressBookItem` entries inside ``book``."""
|
|
234
|
+
response = self.client.get_items(
|
|
235
|
+
GetItemsRequest(container=book.id, count=count), self._ctx
|
|
236
|
+
)
|
|
237
|
+
items = response.items
|
|
238
|
+
if not items or not items.item:
|
|
239
|
+
return []
|
|
240
|
+
return [item for item in items.item if isinstance(item, AddressBookItem)]
|
|
241
|
+
|
|
242
|
+
# -- folders -----------------------------------------------------------
|
|
243
|
+
|
|
244
|
+
def get_system_folder(self, folder_type):
|
|
245
|
+
"""Return the system folder of the given :class:`FolderType`, or ``None``."""
|
|
246
|
+
request = GetFolderListRequest(parent="folders", recurse=False, imap=False, nntp=False)
|
|
247
|
+
response = self.client.get_folder_list(request, self._ctx)
|
|
248
|
+
folders = response.folders
|
|
249
|
+
if not folders or not folders.folder:
|
|
250
|
+
return None
|
|
251
|
+
for folder in folders.folder:
|
|
252
|
+
if isinstance(folder, SystemFolder) and folder.folder_type == folder_type:
|
|
253
|
+
return folder
|
|
254
|
+
return None
|
|
255
|
+
|
|
256
|
+
def get_mailbox(self):
|
|
257
|
+
return self.get_system_folder(FolderType.MAILBOX)
|
|
258
|
+
|
|
259
|
+
def get_trash_folder(self):
|
|
260
|
+
return self.get_system_folder(FolderType.TRASH)
|
|
261
|
+
|
|
262
|
+
def get_calendar(self):
|
|
263
|
+
return self.get_system_folder(FolderType.CALENDAR)
|
|
264
|
+
|
|
265
|
+
def get_folder_list(self) -> list:
|
|
266
|
+
"""Return every folder in the mailbox (recursive)."""
|
|
267
|
+
request = GetFolderListRequest(parent="folders", recurse=True, imap=False, nntp=False)
|
|
268
|
+
response = self.client.get_folder_list(request, self._ctx)
|
|
269
|
+
folders = response.folders
|
|
270
|
+
if not folders or not folders.folder:
|
|
271
|
+
return []
|
|
272
|
+
return folders.folder
|
|
273
|
+
|
|
274
|
+
def get_shared_folder_by_name(self, name: str):
|
|
275
|
+
"""Return the :class:`SharedFolder` with the given name, or ``None``."""
|
|
276
|
+
for folder in self.get_folder_list():
|
|
277
|
+
if isinstance(folder, SharedFolder) and folder.name == name:
|
|
278
|
+
return folder
|
|
279
|
+
return None
|
|
280
|
+
|
|
281
|
+
# -- items -------------------------------------------------------------
|
|
282
|
+
|
|
283
|
+
def get_items(self, container_id: str, filter: Optional[str] = None,
|
|
284
|
+
count: Optional[int] = None) -> list:
|
|
285
|
+
"""Return the items inside a container (folder or address book)."""
|
|
286
|
+
request = GetItemsRequest(container=container_id, filter=filter, count=count)
|
|
287
|
+
response = self.client.get_items(request, self._ctx)
|
|
288
|
+
items = response.items
|
|
289
|
+
if not items or not items.item:
|
|
290
|
+
return []
|
|
291
|
+
return items.item
|
|
292
|
+
|
|
293
|
+
def get_folder_items(self, folder_id: str, count: int = 200) -> list:
|
|
294
|
+
return self.get_items(folder_id, count=count)
|
|
295
|
+
|
|
296
|
+
def get_calendar_items(self, count: int = 200) -> list:
|
|
297
|
+
calendar = self.get_calendar()
|
|
298
|
+
return self.get_items(calendar.id, count=count) if calendar else []
|
|
299
|
+
|
|
300
|
+
def get_item(self, item_id: str):
|
|
301
|
+
"""Return a single item by id."""
|
|
302
|
+
response = self.client.get_item(GetItemRequest(id=item_id), self._ctx)
|
|
303
|
+
return response.item if response and response.item else None
|
|
304
|
+
|
|
305
|
+
def get_full_item(self, item_id: str):
|
|
306
|
+
"""Return a single item including its message body view."""
|
|
307
|
+
from gwsoap.types.View import View
|
|
308
|
+
|
|
309
|
+
request = GetItemRequest(id=item_id, view=View(value=["message"]))
|
|
310
|
+
response = self.client.get_item(request, self._ctx)
|
|
311
|
+
return response.item if response and response.item else None
|
|
312
|
+
|
|
313
|
+
def get_body(self, item) -> str:
|
|
314
|
+
"""Return the decoded plain-text body of ``item``."""
|
|
315
|
+
full = self.get_full_item(item.id)
|
|
316
|
+
message = getattr(full, "message", None)
|
|
317
|
+
if not message or not message.part:
|
|
318
|
+
return ""
|
|
319
|
+
value = message.part[0].value
|
|
320
|
+
return value.decode("utf-8", errors="replace") if value is not None else ""
|
|
321
|
+
|
|
322
|
+
def remove_item(self, container: str, item_id: str) -> None:
|
|
323
|
+
"""Remove an item from a container."""
|
|
324
|
+
response = self.client.remove_item(
|
|
325
|
+
RemoveItemRequest(container=container, id=item_id), self._ctx
|
|
326
|
+
)
|
|
327
|
+
self._raise_on_bad_status(response, "remove_item")
|
|
328
|
+
|
|
329
|
+
# -- mail --------------------------------------------------------------
|
|
330
|
+
|
|
331
|
+
def send_mail(self, subject: str, body_text: str,
|
|
332
|
+
recipients: Sequence[RecipientLike],
|
|
333
|
+
return_sent_items_id: bool = True) -> Optional[str]:
|
|
334
|
+
"""Send a plain-text mail.
|
|
335
|
+
|
|
336
|
+
``recipients`` may be dicts or objects carrying ``display_name``/``name``,
|
|
337
|
+
``email``/``userid`` and an optional ``uuid``.
|
|
338
|
+
"""
|
|
339
|
+
self._require_session()
|
|
340
|
+
recipient_list = RecipientList(
|
|
341
|
+
recipient=[
|
|
342
|
+
Recipient(
|
|
343
|
+
display_name=self._recipient_value(r, "display_name")
|
|
344
|
+
or self._recipient_value(r, "name")
|
|
345
|
+
or self._recipient_value(r, "userid"),
|
|
346
|
+
email=self._recipient_value(r, "email")
|
|
347
|
+
or self._recipient_value(r, "userid"),
|
|
348
|
+
uuid=self._recipient_value(r, "uuid"),
|
|
349
|
+
dist_type=DistributionType.TO,
|
|
350
|
+
recip_type=RecipientType.USER,
|
|
351
|
+
)
|
|
352
|
+
for r in recipients
|
|
353
|
+
]
|
|
354
|
+
)
|
|
355
|
+
request = SendItemRequest(
|
|
356
|
+
item=Mail(
|
|
357
|
+
subject=subject,
|
|
358
|
+
distribution=Distribution(recipients=recipient_list),
|
|
359
|
+
message=MessageBody(part=[MessagePart(value=body_text.encode("utf-8"))]),
|
|
360
|
+
return_sent_items_id=return_sent_items_id,
|
|
361
|
+
)
|
|
362
|
+
)
|
|
363
|
+
response = self.client.send_item(request, self._ctx)
|
|
364
|
+
self._raise_on_bad_status(response, "send_mail")
|
|
365
|
+
return response.id
|
|
366
|
+
|
|
367
|
+
# -- appointments ------------------------------------------------------
|
|
368
|
+
|
|
369
|
+
def create_appointment(self, subject: str, body: str, from_display_name: str,
|
|
370
|
+
from_email: str, start: datetime, end: datetime,
|
|
371
|
+
custom_field: str = "") -> None:
|
|
372
|
+
"""Create an appointment in the connected user's calendar."""
|
|
373
|
+
apt = Appointment(
|
|
374
|
+
subject=subject,
|
|
375
|
+
distribution=Distribution(
|
|
376
|
+
from_value=From(display_name=from_display_name, email=from_email),
|
|
377
|
+
recipients=RecipientList(recipient=[
|
|
378
|
+
Recipient(
|
|
379
|
+
display_name=self.user_name,
|
|
380
|
+
email=self.user_email,
|
|
381
|
+
dist_type=DistributionType.TO,
|
|
382
|
+
recip_type=RecipientType.USER,
|
|
383
|
+
)
|
|
384
|
+
]),
|
|
385
|
+
),
|
|
386
|
+
message=self._text_message(body),
|
|
387
|
+
start_date=start,
|
|
388
|
+
end_date=end,
|
|
389
|
+
customs=CustomList(custom=[Custom(field="etermin", value=custom_field)])
|
|
390
|
+
if custom_field else None,
|
|
391
|
+
)
|
|
392
|
+
self._raise_on_bad_status(
|
|
393
|
+
self.client.create_item(CreateItemRequest(item=apt), self._ctx), "create_appointment"
|
|
394
|
+
)
|
|
395
|
+
|
|
396
|
+
def create_appointment_in_folder(self, folder_id: str, subject: str, body: str,
|
|
397
|
+
start: datetime, end: datetime) -> None:
|
|
398
|
+
"""Create an appointment inside a specific (e.g. shared) folder."""
|
|
399
|
+
apt = Appointment(
|
|
400
|
+
subject=subject,
|
|
401
|
+
container=[ContainerRef(value=folder_id)],
|
|
402
|
+
message=self._text_message(body),
|
|
403
|
+
start_date=start,
|
|
404
|
+
end_date=end,
|
|
405
|
+
)
|
|
406
|
+
self._raise_on_bad_status(
|
|
407
|
+
self.client.create_item(CreateItemRequest(item=apt), self._ctx),
|
|
408
|
+
"create_appointment_in_folder",
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
def add_category(self, item_id: str, category_name: str) -> None:
|
|
412
|
+
"""Tag an existing item with a category."""
|
|
413
|
+
updates = ItemChanges(add=Appointment(categories=CategoryRefList(category=[category_name])))
|
|
414
|
+
self._raise_on_bad_status(
|
|
415
|
+
self.client.modify_item(ModifyItemRequest(id=item_id, updates=updates), self._ctx),
|
|
416
|
+
"add_category",
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
# -- lifecycle ---------------------------------------------------------
|
|
420
|
+
|
|
421
|
+
def logout(self) -> None:
|
|
422
|
+
"""Log out and clear the session (safe to call when not logged in)."""
|
|
423
|
+
if self.client and self.session:
|
|
424
|
+
try:
|
|
425
|
+
self.client.logout(LogoutRequest(), self._ctx)
|
|
426
|
+
except SoapFaultException as e:
|
|
427
|
+
logger.warning("Logout failed: %s", e.fault_string)
|
|
428
|
+
finally:
|
|
429
|
+
self.session = None
|
|
430
|
+
self.login_resp = None
|
|
431
|
+
|
|
432
|
+
# -- helpers -----------------------------------------------------------
|
|
433
|
+
|
|
434
|
+
@property
|
|
435
|
+
def _ctx(self) -> RequestContext:
|
|
436
|
+
self._require_session()
|
|
437
|
+
return RequestContext(session_id=self.session)
|
|
438
|
+
|
|
439
|
+
def _require_session(self) -> None:
|
|
440
|
+
if not self.client or not self.session:
|
|
441
|
+
raise GWEasySoapError("Not logged in")
|
|
442
|
+
|
|
443
|
+
@staticmethod
|
|
444
|
+
def _text_message(body: str):
|
|
445
|
+
return MessageBody(part=[MessagePart(value=body.encode("utf-8"))]) if body else None
|
|
446
|
+
|
|
447
|
+
@staticmethod
|
|
448
|
+
def _recipient_value(recipient: RecipientLike, name: str):
|
|
449
|
+
if isinstance(recipient, Mapping):
|
|
450
|
+
return recipient.get(name)
|
|
451
|
+
return getattr(recipient, name, None)
|
|
452
|
+
|
|
453
|
+
@staticmethod
|
|
454
|
+
def _raise_on_bad_status(response, action: str) -> None:
|
|
455
|
+
status = getattr(response, "status", None)
|
|
456
|
+
if status and status.code not in (None, 0):
|
|
457
|
+
raise GWEasySoapError(
|
|
458
|
+
f"{action} failed: {status.description} (code {status.code})",
|
|
459
|
+
code=status.code,
|
|
460
|
+
)
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
ISSUES
|
|
2
|
+
======
|
|
3
|
+
|
|
4
|
+
1. Non-ASCII strings print as garbage on the Windows console
|
|
5
|
+
----------------------------------------------------------------
|
|
6
|
+
Symptom:
|
|
7
|
+
Running tests/07_address_book.py prints an address book entry as:
|
|
8
|
+
Gyn�kologie
|
|
9
|
+
instead of:
|
|
10
|
+
Gynäkologie
|
|
11
|
+
|
|
12
|
+
Cause:
|
|
13
|
+
This is NOT a library bug. The data returned by gweasysoap is a correct
|
|
14
|
+
UTF-8 Python str. The problem is the default Windows console code page
|
|
15
|
+
(cp1252) used by print(); it cannot represent the 'ä' character and
|
|
16
|
+
substitutes the replacement glyph.
|
|
17
|
+
|
|
18
|
+
Workaround:
|
|
19
|
+
Force UTF-8 output before printing, e.g. at the top of a script:
|
|
20
|
+
import sys
|
|
21
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
22
|
+
or run Python with:
|
|
23
|
+
set PYTHONUTF8=1
|
|
24
|
+
or switch the console code page:
|
|
25
|
+
chcp 65001
|
|
26
|
+
|
|
27
|
+
Status:
|
|
28
|
+
Open. Cosmetic only - affects console display, not the library data.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "gweasysoap"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "A helper library for the GroupWise SOAP API (gwsoap)."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
keywords = ["groupwise", "soap", "gwsoap", "novell", "micro focus"]
|
|
13
|
+
classifiers = [
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"License :: OSI Approved :: MIT License",
|
|
16
|
+
"Operating System :: OS Independent",
|
|
17
|
+
"Topic :: Communications :: Email",
|
|
18
|
+
]
|
|
19
|
+
dependencies = ["gwsoap>=1.0.3"]
|
|
20
|
+
|
|
21
|
+
[project.urls]
|
|
22
|
+
Homepage = "https://github.com/rhulha/GWEasySoap"
|
|
23
|
+
Documentation = "https://github.com/rhulha/GWEasySoap#readme"
|
|
24
|
+
Repository = "https://github.com/rhulha/GWEasySoap"
|
|
25
|
+
"Bug Reports" = "https://github.com/rhulha/GWEasySoap/issues"
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.targets.wheel]
|
|
28
|
+
packages = ["gweasysoap"]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""1 - The simplest case: log in with a password and print who we are."""
|
|
2
|
+
|
|
3
|
+
from gweasysoap import GWEasySoap
|
|
4
|
+
from config import GW_SOAP_URL, GW_USER, GW_PASSWORD
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
with GWEasySoap.connect(GW_SOAP_URL, GW_USER, GW_PASSWORD) as gw:
|
|
8
|
+
print(f"Logged in as: {gw.user_name} <{gw.user_email}>")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""2 - Log in on behalf of a user with a trusted application key (no password)."""
|
|
2
|
+
|
|
3
|
+
from gweasysoap import GWEasySoap
|
|
4
|
+
from config import GW_SOAP_URL, GW_USER, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
with GWEasySoap.connect_trusted_app(
|
|
8
|
+
GW_SOAP_URL, GW_USER, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY
|
|
9
|
+
) as gw:
|
|
10
|
+
info = gw.whoami()
|
|
11
|
+
print("User info:")
|
|
12
|
+
for attr in ("display_name", "name", "email"):
|
|
13
|
+
value = getattr(info, attr, None)
|
|
14
|
+
if value:
|
|
15
|
+
print(f" {attr}: {value}")
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""3 - List the post office mailboxes (uses the trusted-app key, no session)."""
|
|
2
|
+
|
|
3
|
+
from gweasysoap import GWEasySoap
|
|
4
|
+
from config import GW_SOAP_URL, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
gw = GWEasySoap()
|
|
8
|
+
mailboxes = gw.get_user_mailboxes(GW_SOAP_URL, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY)
|
|
9
|
+
|
|
10
|
+
print(f"Found {len(mailboxes)} mailbox(es):")
|
|
11
|
+
for mb in mailboxes:
|
|
12
|
+
print(f" {mb['display_name']} <{mb['email']}> (userid={mb['userid']})")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""4 - Log in as the first usable mailbox on the post office."""
|
|
2
|
+
|
|
3
|
+
from gweasysoap import GWEasySoap
|
|
4
|
+
from config import GW_SOAP_URL, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
with GWEasySoap.connect_any_user(
|
|
8
|
+
GW_SOAP_URL, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY
|
|
9
|
+
) as gw:
|
|
10
|
+
print(f"Logged in as: {gw.user_name} <{gw.user_email}>")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""5 - Open the mailbox folder and list the items inside it."""
|
|
2
|
+
|
|
3
|
+
from gweasysoap import GWEasySoap
|
|
4
|
+
from config import GW_SOAP_URL, GW_USER, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
with GWEasySoap.connect_trusted_app(
|
|
8
|
+
GW_SOAP_URL, GW_USER, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY
|
|
9
|
+
) as gw:
|
|
10
|
+
mailbox = gw.get_mailbox()
|
|
11
|
+
if mailbox is None:
|
|
12
|
+
raise SystemExit("Mailbox not found")
|
|
13
|
+
|
|
14
|
+
print(f"Mailbox: {mailbox.name} (id={mailbox.id})")
|
|
15
|
+
items = gw.get_folder_items(mailbox.id, count=20)
|
|
16
|
+
print(f"{len(items)} item(s):")
|
|
17
|
+
for item in items:
|
|
18
|
+
print(f" {getattr(item, 'subject', None) or item.name}")
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""6 - List the appointments in the calendar."""
|
|
2
|
+
|
|
3
|
+
from gweasysoap import GWEasySoap
|
|
4
|
+
from config import GW_SOAP_URL, GW_USER, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
with GWEasySoap.connect_trusted_app(
|
|
8
|
+
GW_SOAP_URL, GW_USER, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY
|
|
9
|
+
) as gw:
|
|
10
|
+
items = gw.get_calendar_items(count=50)
|
|
11
|
+
print(f"{len(items)} calendar item(s):")
|
|
12
|
+
for item in items:
|
|
13
|
+
start = getattr(item, "start_date", None)
|
|
14
|
+
print(f" {start} {item.subject}")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""7 - Read the system address book and list its entries."""
|
|
2
|
+
|
|
3
|
+
from gweasysoap import GWEasySoap
|
|
4
|
+
from config import GW_SOAP_URL, GW_USER, GW_PASSWORD
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
with GWEasySoap.connect(GW_SOAP_URL, GW_USER, GW_PASSWORD) as gw:
|
|
8
|
+
book = gw.get_system_address_book()
|
|
9
|
+
if book is None:
|
|
10
|
+
raise SystemExit("System address book not found")
|
|
11
|
+
|
|
12
|
+
print(f"Address book: {book.name}")
|
|
13
|
+
entries = gw.get_address_book_entries(book)
|
|
14
|
+
print(f"{len(entries)} entry/entries:")
|
|
15
|
+
for entry in entries:
|
|
16
|
+
print(f" {entry.name}")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""8 - Use a server-side filter: find items whose subject begins with "P"."""
|
|
2
|
+
|
|
3
|
+
from gweasysoap import GWEasySoap
|
|
4
|
+
from config import GW_SOAP_URL, GW_USER, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY
|
|
5
|
+
from gwsoap.types.Filter import Filter
|
|
6
|
+
from gwsoap.types.FilterEntry import FilterEntry
|
|
7
|
+
from gwsoap.types.FilterOp import FilterOp
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
with GWEasySoap.connect_trusted_app(
|
|
11
|
+
GW_SOAP_URL, GW_USER, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY
|
|
12
|
+
) as gw:
|
|
13
|
+
f = Filter(element=FilterEntry(op=FilterOp.BEGINS, field="subject", value="P"))
|
|
14
|
+
items = gw.get_items(None, filter=f, count=10)
|
|
15
|
+
print(f"{len(items)} match(es):")
|
|
16
|
+
for item in items:
|
|
17
|
+
print(f" {item.subject}")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""9 - Drill into a single mailbox item and fetch its decoded body text."""
|
|
2
|
+
|
|
3
|
+
from gweasysoap import GWEasySoap
|
|
4
|
+
from config import GW_SOAP_URL, GW_USER, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
with GWEasySoap.connect_trusted_app(
|
|
8
|
+
GW_SOAP_URL, GW_USER, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY
|
|
9
|
+
) as gw:
|
|
10
|
+
mailbox = gw.get_mailbox()
|
|
11
|
+
items = gw.get_folder_items(mailbox.id, count=5) if mailbox else []
|
|
12
|
+
if not items:
|
|
13
|
+
raise SystemExit("Mailbox is empty - nothing to read")
|
|
14
|
+
|
|
15
|
+
first = items[0]
|
|
16
|
+
print(f"Subject: {getattr(first, 'subject', None)}")
|
|
17
|
+
print("Body:")
|
|
18
|
+
print(gw.get_body(first) or " (no text body)")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""10 - Send a plain-text mail to the connected user (a safe self-send)."""
|
|
2
|
+
|
|
3
|
+
from gweasysoap import GWEasySoap
|
|
4
|
+
from config import GW_SOAP_URL, GW_USER, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
with GWEasySoap.connect_trusted_app(
|
|
8
|
+
GW_SOAP_URL, GW_USER, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY
|
|
9
|
+
) as gw:
|
|
10
|
+
recipient = {"display_name": gw.user_name, "email": gw.user_email}
|
|
11
|
+
ids = gw.send_mail(
|
|
12
|
+
subject="Hello from gweasysoap",
|
|
13
|
+
body_text="This message was sent by the gweasysoap test suite.",
|
|
14
|
+
recipients=[recipient],
|
|
15
|
+
)
|
|
16
|
+
print("Sent mail; sent-items id(s):", ids or "(none returned)")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""11 - The hardest case: create a one-hour appointment in the calendar."""
|
|
2
|
+
|
|
3
|
+
from datetime import datetime, timedelta
|
|
4
|
+
|
|
5
|
+
from gweasysoap import GWEasySoap
|
|
6
|
+
from config import GW_SOAP_URL, GW_USER, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
with GWEasySoap.connect_trusted_app(
|
|
10
|
+
GW_SOAP_URL, GW_USER, GW_TRUSTED_APP_NAME, GW_TRUSTED_APP_KEY
|
|
11
|
+
) as gw:
|
|
12
|
+
start = datetime.now().replace(minute=0, second=0, microsecond=0) + timedelta(days=1)
|
|
13
|
+
end = start + timedelta(hours=1)
|
|
14
|
+
|
|
15
|
+
gw.create_appointment(
|
|
16
|
+
subject="gweasysoap test appointment",
|
|
17
|
+
body="Created by the gweasysoap test suite.",
|
|
18
|
+
from_display_name=gw.user_name,
|
|
19
|
+
from_email=gw.user_email,
|
|
20
|
+
start=start,
|
|
21
|
+
end=end,
|
|
22
|
+
)
|
|
23
|
+
print(f"Created appointment {start:%Y-%m-%d %H:%M} - {end:%H:%M}")
|
|
24
|
+
|
|
25
|
+
print("Calendar now contains:")
|
|
26
|
+
for item in gw.get_calendar_items(count=50):
|
|
27
|
+
print(f" {getattr(item, 'start_date', None)} {item.subject}")
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# gweasysoap test scripts
|
|
2
|
+
|
|
3
|
+
Manual, runnable scripts that exercise the library against a live GroupWise post
|
|
4
|
+
office. They are ordered from the simplest task to the hardest.
|
|
5
|
+
|
|
6
|
+
## Setup
|
|
7
|
+
|
|
8
|
+
1. Install the package (from the project root):
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
pip install -e .
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
2. Edit [config.py](config.py) so the URL, user and trusted-app key point at a
|
|
15
|
+
reachable POA.
|
|
16
|
+
|
|
17
|
+
3. Run a script from inside this folder (so `config.py` is importable):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
cd tests
|
|
21
|
+
python 01_login_password.py
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## The scripts
|
|
25
|
+
|
|
26
|
+
| # | Script | What it shows |
|
|
27
|
+
|---|--------|---------------|
|
|
28
|
+
| 01 | `01_login_password.py` | Log in with a password, print the connected user |
|
|
29
|
+
| 02 | `02_login_trusted_app.py` | Trusted-application login (no password) |
|
|
30
|
+
| 03 | `03_user_list.py` | List the post office mailboxes (no session) |
|
|
31
|
+
| 04 | `04_login_any_user.py` | Log in as the first usable mailbox |
|
|
32
|
+
| 05 | `05_mailbox_items.py` | Open the mailbox folder and list its items |
|
|
33
|
+
| 06 | `06_calendar.py` | List calendar appointments |
|
|
34
|
+
| 07 | `07_address_book.py` | Read the system address book |
|
|
35
|
+
| 08 | `08_filter_items.py` | Server-side filter (subject begins with "P") |
|
|
36
|
+
| 09 | `09_read_body.py` | Fetch and decode a single item's body |
|
|
37
|
+
| 10 | `10_send_mail.py` | **Sends** a mail (self-send) |
|
|
38
|
+
| 11 | `11_create_appointment.py` | **Creates** a calendar appointment |
|
|
39
|
+
|
|
40
|
+
Scripts 10 and 11 modify the mailbox; run them only against a test account.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""Connection settings for the test scripts.
|
|
2
|
+
|
|
3
|
+
Point these at a reachable GroupWise post office agent (POA) before running.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
GW_SOAP_URL = "http://172.30.15.95:7191/soap"
|
|
7
|
+
GW_USER = "testgw1"
|
|
8
|
+
GW_PASSWORD = "novell"
|
|
9
|
+
GW_TRUSTED_APP_NAME = "MyTapp"
|
|
10
|
+
GW_TRUSTED_APP_KEY = "E879BF5111D1123486257DE8AD4B4FF9E879BF5211D10000AB9FA533175F62F2"
|