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.
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ *.egg-info/
4
+ build/
5
+ dist/
6
+ .venv/
7
+ venv/
8
+ .env
@@ -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,6 @@
1
+ class GWEasySoapError(Exception):
2
+ """Raised when a GroupWise SOAP request fails or returns a bad status."""
3
+
4
+ def __init__(self, message, code=None):
5
+ super().__init__(message)
6
+ self.code = code
@@ -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"