mailsuite 2.0.0__tar.gz → 2.0.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.
- {mailsuite-2.0.0 → mailsuite-2.0.2}/PKG-INFO +28 -1
- {mailsuite-2.0.0 → mailsuite-2.0.2}/README.md +27 -0
- {mailsuite-2.0.0 → mailsuite-2.0.2}/mailsuite/__init__.py +1 -1
- {mailsuite-2.0.0 → mailsuite-2.0.2}/mailsuite/imap.py +89 -8
- {mailsuite-2.0.0 → mailsuite-2.0.2}/mailsuite/mailbox/graph.py +49 -12
- {mailsuite-2.0.0 → mailsuite-2.0.2}/mailsuite/mailbox/imap.py +35 -1
- {mailsuite-2.0.0 → mailsuite-2.0.2}/.gitignore +0 -0
- {mailsuite-2.0.0 → mailsuite-2.0.2}/LICENSE +0 -0
- {mailsuite-2.0.0 → mailsuite-2.0.2}/mailsuite/dkim.py +0 -0
- {mailsuite-2.0.0 → mailsuite-2.0.2}/mailsuite/mailbox/__init__.py +0 -0
- {mailsuite-2.0.0 → mailsuite-2.0.2}/mailsuite/mailbox/base.py +0 -0
- {mailsuite-2.0.0 → mailsuite-2.0.2}/mailsuite/mailbox/gmail.py +0 -0
- {mailsuite-2.0.0 → mailsuite-2.0.2}/mailsuite/mailbox/maildir.py +0 -0
- {mailsuite-2.0.0 → mailsuite-2.0.2}/mailsuite/smtp.py +0 -0
- {mailsuite-2.0.0 → mailsuite-2.0.2}/mailsuite/utils.py +0 -0
- {mailsuite-2.0.0 → mailsuite-2.0.2}/pyproject.toml +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mailsuite
|
|
3
|
-
Version: 2.0.
|
|
3
|
+
Version: 2.0.2
|
|
4
4
|
Summary: A Python package for retrieving, parsing, and sending emails
|
|
5
5
|
Project-URL: Homepage, https://github.com/seanthegeek/mailsuite/
|
|
6
6
|
Project-URL: Documentation, https://seanthegeek.github.io/mailsuite/
|
|
@@ -119,3 +119,30 @@ different cache name can pass it through `token_cache_name=` so existing
|
|
|
119
119
|
cached `AuthenticationRecord`s and tokens continue to work — for
|
|
120
120
|
example, `token_cache_name="parsedmarc"` keeps users authenticated
|
|
121
121
|
across the migration.
|
|
122
|
+
|
|
123
|
+
### Microsoft Graph permissions
|
|
124
|
+
|
|
125
|
+
Grant the appropriate Microsoft Graph **API permissions** on the app
|
|
126
|
+
registration based on which `MSGraphConnection` operations you need.
|
|
127
|
+
Combine permissions across rows when you need multiple capabilities —
|
|
128
|
+
e.g., to both read and send mail in a delegated flow against your own
|
|
129
|
+
mailbox, grant `Mail.ReadWrite` and `Mail.Send`.
|
|
130
|
+
|
|
131
|
+
| Use case | Delegated (own mailbox) | Delegated (shared mailbox) | App-only |
|
|
132
|
+
| --- | --- | --- | --- |
|
|
133
|
+
| Read messages only (`fetch_message`, `fetch_messages`) | `Mail.Read` | `Mail.Read.Shared` | `Mail.Read` |
|
|
134
|
+
| Read + modify (mark read, delete, move, create folder) | `Mail.ReadWrite` | `Mail.ReadWrite.Shared` | `Mail.ReadWrite` |
|
|
135
|
+
| Send mail (`send_message`) | `Mail.Send` | `Mail.Send.Shared` | `Mail.Send` |
|
|
136
|
+
|
|
137
|
+
Delegated flows (`DeviceCode`, `UsernamePassword`) targeting a shared
|
|
138
|
+
mailbox — i.e. when the `mailbox` argument differs from `username` —
|
|
139
|
+
use the `.Shared` variants. App-only flows (`ClientSecret`,
|
|
140
|
+
`Certificate`) do not need the `.Shared` variants since application
|
|
141
|
+
permissions span every mailbox in the tenant (unless restricted by an
|
|
142
|
+
[Application Access Policy](https://learn.microsoft.com/en-us/graph/auth-limit-mailbox-access)).
|
|
143
|
+
|
|
144
|
+
For delegated flows, `MSGraphConnection` requests `Mail.ReadWrite` (or
|
|
145
|
+
`Mail.ReadWrite.Shared`) at authenticate time, so even read-only
|
|
146
|
+
callers must consent to at least `Mail.ReadWrite`. App-only flows
|
|
147
|
+
authenticate with `https://graph.microsoft.com/.default`, which grants
|
|
148
|
+
whichever permissions the app registration has consented.
|
|
@@ -77,3 +77,30 @@ different cache name can pass it through `token_cache_name=` so existing
|
|
|
77
77
|
cached `AuthenticationRecord`s and tokens continue to work — for
|
|
78
78
|
example, `token_cache_name="parsedmarc"` keeps users authenticated
|
|
79
79
|
across the migration.
|
|
80
|
+
|
|
81
|
+
### Microsoft Graph permissions
|
|
82
|
+
|
|
83
|
+
Grant the appropriate Microsoft Graph **API permissions** on the app
|
|
84
|
+
registration based on which `MSGraphConnection` operations you need.
|
|
85
|
+
Combine permissions across rows when you need multiple capabilities —
|
|
86
|
+
e.g., to both read and send mail in a delegated flow against your own
|
|
87
|
+
mailbox, grant `Mail.ReadWrite` and `Mail.Send`.
|
|
88
|
+
|
|
89
|
+
| Use case | Delegated (own mailbox) | Delegated (shared mailbox) | App-only |
|
|
90
|
+
| --- | --- | --- | --- |
|
|
91
|
+
| Read messages only (`fetch_message`, `fetch_messages`) | `Mail.Read` | `Mail.Read.Shared` | `Mail.Read` |
|
|
92
|
+
| Read + modify (mark read, delete, move, create folder) | `Mail.ReadWrite` | `Mail.ReadWrite.Shared` | `Mail.ReadWrite` |
|
|
93
|
+
| Send mail (`send_message`) | `Mail.Send` | `Mail.Send.Shared` | `Mail.Send` |
|
|
94
|
+
|
|
95
|
+
Delegated flows (`DeviceCode`, `UsernamePassword`) targeting a shared
|
|
96
|
+
mailbox — i.e. when the `mailbox` argument differs from `username` —
|
|
97
|
+
use the `.Shared` variants. App-only flows (`ClientSecret`,
|
|
98
|
+
`Certificate`) do not need the `.Shared` variants since application
|
|
99
|
+
permissions span every mailbox in the tenant (unless restricted by an
|
|
100
|
+
[Application Access Policy](https://learn.microsoft.com/en-us/graph/auth-limit-mailbox-access)).
|
|
101
|
+
|
|
102
|
+
For delegated flows, `MSGraphConnection` requests `Mail.ReadWrite` (or
|
|
103
|
+
`Mail.ReadWrite.Shared`) at authenticate time, so even read-only
|
|
104
|
+
callers must consent to at least `Mail.ReadWrite`. App-only flows
|
|
105
|
+
authenticate with `https://graph.microsoft.com/.default`, which grants
|
|
106
|
+
whichever permissions the app registration has consented.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Union, List, Dict, Optional, cast
|
|
2
|
+
from typing import Callable, Union, List, Dict, Optional, cast
|
|
3
3
|
import time
|
|
4
4
|
import socket
|
|
5
5
|
from ssl import (
|
|
@@ -58,6 +58,17 @@ class IMAPClient(imapclient.IMAPClient):
|
|
|
58
58
|
return str(result)
|
|
59
59
|
|
|
60
60
|
folder_name = folder_name.rstrip("/")
|
|
61
|
+
|
|
62
|
+
# If the path is already inside a non-personal namespace (other-users
|
|
63
|
+
# or shared per RFC 2342), pass it through without applying the
|
|
64
|
+
# personal-namespace prefix. Otherwise paths like "user/colleague/Inbox"
|
|
65
|
+
# would get rewritten to "INBOX/user/colleague/Inbox".
|
|
66
|
+
if any(folder_name.startswith(p) for p in self._other_namespace_prefixes):
|
|
67
|
+
result = imapclient.IMAPClient._normalise_folder(self, folder_name)
|
|
68
|
+
if isinstance(result, bytes):
|
|
69
|
+
return result.decode("utf-8", "replace")
|
|
70
|
+
return str(result)
|
|
71
|
+
|
|
61
72
|
folder_name = folder_name.replace(self._path_prefix, "")
|
|
62
73
|
if not self._hierarchy_separator == "/":
|
|
63
74
|
folder_name = folder_name.replace(self._hierarchy_separator, "")
|
|
@@ -126,9 +137,9 @@ class IMAPClient(imapclient.IMAPClient):
|
|
|
126
137
|
def __init__(
|
|
127
138
|
self,
|
|
128
139
|
host: str,
|
|
129
|
-
username: str,
|
|
130
|
-
password: str,
|
|
131
|
-
port: int =
|
|
140
|
+
username: Optional[str] = None,
|
|
141
|
+
password: Optional[str] = None,
|
|
142
|
+
port: int = 993,
|
|
132
143
|
ssl: bool = True,
|
|
133
144
|
ssl_context: Optional[SSLContext] = None,
|
|
134
145
|
verify: bool = True,
|
|
@@ -137,14 +148,18 @@ class IMAPClient(imapclient.IMAPClient):
|
|
|
137
148
|
initial_folder: str = "INBOX",
|
|
138
149
|
idle_callback=None,
|
|
139
150
|
idle_timeout: int = 30,
|
|
151
|
+
oauth2_token: Optional[str] = None,
|
|
152
|
+
oauth2_token_provider: Optional[Callable[[], str]] = None,
|
|
153
|
+
oauth2_mechanism: str = "XOAUTH2",
|
|
154
|
+
oauth2_vendor: Optional[str] = None,
|
|
140
155
|
):
|
|
141
156
|
"""
|
|
142
157
|
Connects to an IMAP server
|
|
143
158
|
|
|
144
159
|
Args:
|
|
145
160
|
host: The server hostname or IP address
|
|
146
|
-
username: The username
|
|
147
|
-
password: The password
|
|
161
|
+
username: The username (or OAuth2 identity / email address)
|
|
162
|
+
password: The password (omit when using OAuth2)
|
|
148
163
|
port: The port
|
|
149
164
|
ssl: Use SSL or TLS
|
|
150
165
|
ssl_context: For more advanced TLS options
|
|
@@ -155,6 +170,25 @@ class IMAPClient(imapclient.IMAPClient):
|
|
|
155
170
|
idle_callback: The function to call when new messages are detected
|
|
156
171
|
idle_timeout: Number of seconds to wait for an IDLE
|
|
157
172
|
response
|
|
173
|
+
oauth2_token: A static OAuth2 access token. For long-running
|
|
174
|
+
connections (IDLE, reconnects after timeouts) prefer
|
|
175
|
+
``oauth2_token_provider`` so a fresh token is fetched on
|
|
176
|
+
reconnect.
|
|
177
|
+
oauth2_token_provider: A zero-arg callable returning a current
|
|
178
|
+
OAuth2 access token. Invoked on every (re)connect, so the
|
|
179
|
+
callable is responsible for refreshing the token when
|
|
180
|
+
needed.
|
|
181
|
+
oauth2_mechanism: ``"XOAUTH2"`` (default — Gmail/Yahoo/M365) or
|
|
182
|
+
``"OAUTHBEARER"`` (Gmail's standards-track replacement).
|
|
183
|
+
oauth2_vendor: Optional vendor string required by Yahoo's
|
|
184
|
+
XOAUTH2 implementation.
|
|
185
|
+
|
|
186
|
+
For Google Workspace and Microsoft 365 the higher-level
|
|
187
|
+
:class:`mailsuite.mailbox.GmailConnection` and
|
|
188
|
+
:class:`mailsuite.mailbox.MSGraphConnection` backends are usually
|
|
189
|
+
a better fit — they speak the providers' native APIs and handle
|
|
190
|
+
token refresh end-to-end. Generic IMAP OAuth2 is intended for
|
|
191
|
+
other providers (Yahoo, self-hosted, etc.).
|
|
158
192
|
"""
|
|
159
193
|
|
|
160
194
|
if ssl_context is None:
|
|
@@ -162,6 +196,18 @@ class IMAPClient(imapclient.IMAPClient):
|
|
|
162
196
|
if verify is False:
|
|
163
197
|
ssl_context.check_hostname = False
|
|
164
198
|
ssl_context.verify_mode = CERT_NONE
|
|
199
|
+
using_oauth = (
|
|
200
|
+
oauth2_token is not None or oauth2_token_provider is not None
|
|
201
|
+
)
|
|
202
|
+
if using_oauth and not username:
|
|
203
|
+
raise ValueError(
|
|
204
|
+
"username is required when authenticating with OAuth2"
|
|
205
|
+
)
|
|
206
|
+
if not using_oauth and (username is None or password is None):
|
|
207
|
+
raise ValueError(
|
|
208
|
+
"either password or an OAuth2 token / token_provider must be "
|
|
209
|
+
"provided"
|
|
210
|
+
)
|
|
165
211
|
self._init_args = dict(
|
|
166
212
|
host=host,
|
|
167
213
|
username=username,
|
|
@@ -175,11 +221,16 @@ class IMAPClient(imapclient.IMAPClient):
|
|
|
175
221
|
initial_folder=initial_folder,
|
|
176
222
|
idle_callback=idle_callback,
|
|
177
223
|
idle_timeout=idle_timeout,
|
|
224
|
+
oauth2_token=oauth2_token,
|
|
225
|
+
oauth2_token_provider=oauth2_token_provider,
|
|
226
|
+
oauth2_mechanism=oauth2_mechanism,
|
|
227
|
+
oauth2_vendor=oauth2_vendor,
|
|
178
228
|
)
|
|
179
229
|
self.max_retries = max_retries
|
|
180
230
|
self.idle_callback = idle_callback
|
|
181
231
|
self.idle_timeout = idle_timeout
|
|
182
232
|
self._path_prefix = ""
|
|
233
|
+
self._other_namespace_prefixes: List[str] = []
|
|
183
234
|
self._hierarchy_separator = ""
|
|
184
235
|
if not ssl:
|
|
185
236
|
logger.info("Connecting to IMAP over plain text")
|
|
@@ -196,7 +247,23 @@ class IMAPClient(imapclient.IMAPClient):
|
|
|
196
247
|
if not ssl and b"STARTTLS" in self.capabilities():
|
|
197
248
|
logger.info("IMAP server supports STARTTLS ... activating now")
|
|
198
249
|
self.starttls(ssl_context=ssl_context)
|
|
199
|
-
|
|
250
|
+
if using_oauth:
|
|
251
|
+
token = (
|
|
252
|
+
oauth2_token_provider()
|
|
253
|
+
if oauth2_token_provider is not None
|
|
254
|
+
else oauth2_token
|
|
255
|
+
)
|
|
256
|
+
if oauth2_mechanism.upper() == "OAUTHBEARER":
|
|
257
|
+
self.oauthbearer_login(username, token)
|
|
258
|
+
else:
|
|
259
|
+
self.oauth2_login(
|
|
260
|
+
cast(str, username),
|
|
261
|
+
cast(str, token),
|
|
262
|
+
mech=oauth2_mechanism,
|
|
263
|
+
vendor=oauth2_vendor,
|
|
264
|
+
)
|
|
265
|
+
else:
|
|
266
|
+
self.login(cast(str, username), cast(str, password))
|
|
200
267
|
self.server_capabilities = self.capabilities()
|
|
201
268
|
self._move_supported = b"MOVE" in self.server_capabilities
|
|
202
269
|
self._idle_supported = b"IDLE" in self.server_capabilities
|
|
@@ -217,6 +284,16 @@ class IMAPClient(imapclient.IMAPClient):
|
|
|
217
284
|
self._path_prefix = personal_namespace[0][0]
|
|
218
285
|
if type(self._path_prefix) is bytes:
|
|
219
286
|
self._path_prefix = self._path_prefix.decode("utf-8")
|
|
287
|
+
# Track non-personal namespace prefixes (other-users, shared)
|
|
288
|
+
# so _normalise_folder can recognise paths that already live
|
|
289
|
+
# in a different namespace and leave them alone.
|
|
290
|
+
for ns in (self._namespace.other, self._namespace.shared):
|
|
291
|
+
for entry in ns or ():
|
|
292
|
+
prefix = entry[0]
|
|
293
|
+
if isinstance(prefix, bytes):
|
|
294
|
+
prefix = prefix.decode("utf-8")
|
|
295
|
+
if prefix:
|
|
296
|
+
self._other_namespace_prefixes.append(prefix)
|
|
220
297
|
else:
|
|
221
298
|
self._namespace = None
|
|
222
299
|
self.select_folder(initial_folder)
|
|
@@ -244,13 +321,17 @@ class IMAPClient(imapclient.IMAPClient):
|
|
|
244
321
|
self._init_args["password"], # pyright: ignore[reportArgumentType]
|
|
245
322
|
port=self._init_args["port"], # pyright: ignore[reportArgumentType]
|
|
246
323
|
ssl=self._init_args["ssl"], # pyright: ignore[reportArgumentType]
|
|
247
|
-
ssl_context=self._init_args["ssl_context"],
|
|
324
|
+
ssl_context=self._init_args["ssl_context"], # pyright: ignore[reportArgumentType]
|
|
248
325
|
verify=self._init_args["verify"], # pyright: ignore[reportArgumentType]
|
|
249
326
|
timeout=self._init_args["timeout"], # pyright: ignore[reportArgumentType]
|
|
250
327
|
max_retries=self._init_args["max_retries"], # pyright: ignore[reportArgumentType]
|
|
251
328
|
initial_folder=self._init_args["initial_folder"], # pyright: ignore[reportArgumentType]
|
|
252
329
|
idle_callback=self._init_args["idle_callback"],
|
|
253
330
|
idle_timeout=self._init_args["idle_timeout"], # pyright: ignore[reportArgumentType]
|
|
331
|
+
oauth2_token=self._init_args["oauth2_token"], # pyright: ignore[reportArgumentType]
|
|
332
|
+
oauth2_token_provider=self._init_args["oauth2_token_provider"], # pyright: ignore[reportArgumentType]
|
|
333
|
+
oauth2_mechanism=self._init_args["oauth2_mechanism"], # pyright: ignore[reportArgumentType]
|
|
334
|
+
oauth2_vendor=self._init_args["oauth2_vendor"], # pyright: ignore[reportArgumentType]
|
|
254
335
|
)
|
|
255
336
|
|
|
256
337
|
def fetch_message(
|
|
@@ -147,16 +147,35 @@ def _generate_credential(auth_method: str, token_path: Path, **kwargs):
|
|
|
147
147
|
raise RuntimeError(f"Auth method {auth_method} not found")
|
|
148
148
|
|
|
149
149
|
|
|
150
|
+
_persistent_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
151
|
+
|
|
152
|
+
|
|
150
153
|
def _run(coro):
|
|
151
|
-
"""Run a coroutine to completion
|
|
154
|
+
"""Run a coroutine to completion on a persistent event loop.
|
|
155
|
+
|
|
156
|
+
Refuses to nest in a running loop. We retain a single persistent
|
|
157
|
+
loop across calls because the Graph SDK's underlying
|
|
158
|
+
``httpx.AsyncClient`` keeps connection-pool resources bound to the
|
|
159
|
+
loop that issued the first request — closing the loop between calls
|
|
160
|
+
(as ``asyncio.run`` does) invalidates those resources and surfaces
|
|
161
|
+
on the next call as ``RuntimeError: Event loop is closed``. See
|
|
162
|
+
https://github.com/domainaware/parsedmarc/issues/742.
|
|
163
|
+
"""
|
|
164
|
+
global _persistent_loop
|
|
152
165
|
try:
|
|
153
166
|
asyncio.get_running_loop()
|
|
154
167
|
except RuntimeError:
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
168
|
+
pass
|
|
169
|
+
else:
|
|
170
|
+
raise RuntimeError(
|
|
171
|
+
"MSGraphConnection cannot be called from inside a running event loop. "
|
|
172
|
+
"Use msgraph.GraphServiceClient directly from async code."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
if _persistent_loop is None or _persistent_loop.is_closed():
|
|
176
|
+
_persistent_loop = asyncio.new_event_loop()
|
|
177
|
+
asyncio.set_event_loop(_persistent_loop)
|
|
178
|
+
return _persistent_loop.run_until_complete(coro)
|
|
160
179
|
|
|
161
180
|
|
|
162
181
|
class MSGraphConnection(MailboxConnection):
|
|
@@ -168,6 +187,25 @@ class MSGraphConnection(MailboxConnection):
|
|
|
168
187
|
``/users/{mailbox}/sendMail`` with a structured ``Message`` body
|
|
169
188
|
(Graph automatically saves a copy to Sent Items).
|
|
170
189
|
|
|
190
|
+
Required Microsoft Graph **API permissions** on the app registration
|
|
191
|
+
(combine as needed):
|
|
192
|
+
|
|
193
|
+
* Read-only (``fetch_message``, ``fetch_messages``): ``Mail.Read``
|
|
194
|
+
* Read + modify (mark read, delete, move, create folder):
|
|
195
|
+
``Mail.ReadWrite``
|
|
196
|
+
* Send mail (``send_message``): ``Mail.Send``
|
|
197
|
+
|
|
198
|
+
Delegated flows (``DeviceCode``, ``UsernamePassword``) targeting a
|
|
199
|
+
shared mailbox (i.e. ``mailbox != username``) use the ``.Shared``
|
|
200
|
+
variants — ``Mail.Read.Shared``, ``Mail.ReadWrite.Shared``,
|
|
201
|
+
``Mail.Send.Shared``. App-only flows (``ClientSecret``,
|
|
202
|
+
``Certificate``) do not need the ``.Shared`` variants. See the
|
|
203
|
+
README "Microsoft Graph permissions" section for the full mapping.
|
|
204
|
+
|
|
205
|
+
Note: delegated flows always request ``Mail.ReadWrite`` at
|
|
206
|
+
authenticate time, so even read-only callers must consent to at
|
|
207
|
+
least ``Mail.ReadWrite``.
|
|
208
|
+
|
|
171
209
|
Requires the ``msgraph`` extra::
|
|
172
210
|
|
|
173
211
|
pip install mailsuite[msgraph]
|
|
@@ -289,8 +327,7 @@ class MSGraphConnection(MailboxConnection):
|
|
|
289
327
|
)
|
|
290
328
|
logger.debug("Created folder %s", folder_name)
|
|
291
329
|
except Exception as e:
|
|
292
|
-
|
|
293
|
-
if "already exists" in str(e).lower() or "ErrorFolderExists" in str(e):
|
|
330
|
+
if getattr(e, "response_status_code", None) == 409:
|
|
294
331
|
logger.debug("Folder %s already exists, skipping", folder_name)
|
|
295
332
|
return
|
|
296
333
|
raise
|
|
@@ -347,8 +384,6 @@ class MSGraphConnection(MailboxConnection):
|
|
|
347
384
|
self.mark_message_read(str(message_id))
|
|
348
385
|
if raw is None:
|
|
349
386
|
return ""
|
|
350
|
-
if isinstance(raw, str):
|
|
351
|
-
return raw
|
|
352
387
|
return bytes(raw).decode("utf-8", errors="replace")
|
|
353
388
|
|
|
354
389
|
def mark_message_read(self, message_id: str) -> None:
|
|
@@ -503,9 +538,11 @@ class MSGraphConnection(MailboxConnection):
|
|
|
503
538
|
def _list_folders_filtered(
|
|
504
539
|
self, folder_name: str, parent_folder_id: Optional[str]
|
|
505
540
|
) -> List[MailFolder]:
|
|
541
|
+
# OData string-literal escape: single quotes are doubled.
|
|
542
|
+
escaped = folder_name.replace("'", "''")
|
|
506
543
|
if parent_folder_id is None:
|
|
507
544
|
query = MailFoldersRequestBuilder.MailFoldersRequestBuilderGetQueryParameters(
|
|
508
|
-
filter=f"displayName eq '{
|
|
545
|
+
filter=f"displayName eq '{escaped}'"
|
|
509
546
|
)
|
|
510
547
|
config = MailFoldersRequestBuilder.MailFoldersRequestBuilderGetRequestConfiguration(
|
|
511
548
|
query_parameters=query
|
|
@@ -517,7 +554,7 @@ class MSGraphConnection(MailboxConnection):
|
|
|
517
554
|
)
|
|
518
555
|
else:
|
|
519
556
|
child_query = ChildFoldersRequestBuilder.ChildFoldersRequestBuilderGetQueryParameters(
|
|
520
|
-
filter=f"displayName eq '{
|
|
557
|
+
filter=f"displayName eq '{escaped}'"
|
|
521
558
|
)
|
|
522
559
|
child_config = ChildFoldersRequestBuilder.ChildFoldersRequestBuilderGetRequestConfiguration(
|
|
523
560
|
query_parameters=child_query
|
|
@@ -32,16 +32,42 @@ class IMAPConnection(MailboxConnection):
|
|
|
32
32
|
self,
|
|
33
33
|
host: str,
|
|
34
34
|
user: str,
|
|
35
|
-
password: str,
|
|
35
|
+
password: Optional[str] = None,
|
|
36
36
|
port: int = 993,
|
|
37
37
|
ssl: bool = True,
|
|
38
38
|
verify: bool = True,
|
|
39
39
|
timeout: int = 30,
|
|
40
40
|
max_retries: int = 4,
|
|
41
|
+
oauth2_token: Optional[str] = None,
|
|
42
|
+
oauth2_token_provider: Optional[Callable[[], str]] = None,
|
|
43
|
+
oauth2_mechanism: str = "XOAUTH2",
|
|
44
|
+
oauth2_vendor: Optional[str] = None,
|
|
41
45
|
):
|
|
46
|
+
"""
|
|
47
|
+
Args:
|
|
48
|
+
host: IMAP server hostname
|
|
49
|
+
user: Username (or OAuth2 identity / email address)
|
|
50
|
+
password: Password (omit when using OAuth2)
|
|
51
|
+
port: Server port
|
|
52
|
+
ssl: Use SSL/TLS
|
|
53
|
+
verify: Verify the server's TLS certificate
|
|
54
|
+
timeout: Per-operation timeout in seconds
|
|
55
|
+
max_retries: Retries after a timeout
|
|
56
|
+
oauth2_token: Static OAuth2 access token
|
|
57
|
+
oauth2_token_provider: Zero-arg callable returning a current
|
|
58
|
+
token. Re-invoked on reconnect, so the callable is
|
|
59
|
+
responsible for refreshing the token. Prefer this over
|
|
60
|
+
``oauth2_token`` for long-running watch loops.
|
|
61
|
+
oauth2_mechanism: ``"XOAUTH2"`` (default) or ``"OAUTHBEARER"``
|
|
62
|
+
oauth2_vendor: Optional vendor string for Yahoo's XOAUTH2
|
|
63
|
+
"""
|
|
42
64
|
self._username = user
|
|
43
65
|
self._password = password
|
|
44
66
|
self._verify = verify
|
|
67
|
+
self._oauth2_token = oauth2_token
|
|
68
|
+
self._oauth2_token_provider = oauth2_token_provider
|
|
69
|
+
self._oauth2_mechanism = oauth2_mechanism
|
|
70
|
+
self._oauth2_vendor = oauth2_vendor
|
|
45
71
|
self._client = IMAPClient(
|
|
46
72
|
host,
|
|
47
73
|
user,
|
|
@@ -51,6 +77,10 @@ class IMAPConnection(MailboxConnection):
|
|
|
51
77
|
verify=verify,
|
|
52
78
|
timeout=timeout,
|
|
53
79
|
max_retries=max_retries,
|
|
80
|
+
oauth2_token=oauth2_token,
|
|
81
|
+
oauth2_token_provider=oauth2_token_provider,
|
|
82
|
+
oauth2_mechanism=oauth2_mechanism,
|
|
83
|
+
oauth2_vendor=oauth2_vendor,
|
|
54
84
|
)
|
|
55
85
|
|
|
56
86
|
def create_folder(self, folder_name: str) -> None:
|
|
@@ -121,6 +151,10 @@ class IMAPConnection(MailboxConnection):
|
|
|
121
151
|
verify=self._verify,
|
|
122
152
|
idle_callback=idle_callback_wrapper,
|
|
123
153
|
idle_timeout=check_timeout,
|
|
154
|
+
oauth2_token=self._oauth2_token,
|
|
155
|
+
oauth2_token_provider=self._oauth2_token_provider,
|
|
156
|
+
oauth2_mechanism=self._oauth2_mechanism,
|
|
157
|
+
oauth2_vendor=self._oauth2_vendor,
|
|
124
158
|
)
|
|
125
159
|
except (timeout, IMAPClientError):
|
|
126
160
|
logger.warning("IMAP connection timeout. Reconnecting...")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|