ms-graph-toolbox 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.
- ms_graph_toolbox-0.1.0/LICENSE +21 -0
- ms_graph_toolbox-0.1.0/PKG-INFO +181 -0
- ms_graph_toolbox-0.1.0/README.md +165 -0
- ms_graph_toolbox-0.1.0/ms_graph_toolbox/__init__.py +0 -0
- ms_graph_toolbox-0.1.0/ms_graph_toolbox/graph_email.py +173 -0
- ms_graph_toolbox-0.1.0/ms_graph_toolbox/graph_sharepoint.py +109 -0
- ms_graph_toolbox-0.1.0/ms_graph_toolbox/graph_users.py +109 -0
- ms_graph_toolbox-0.1.0/ms_graph_toolbox/ms_graph_toolbox.py +57 -0
- ms_graph_toolbox-0.1.0/ms_graph_toolbox.egg-info/PKG-INFO +181 -0
- ms_graph_toolbox-0.1.0/ms_graph_toolbox.egg-info/SOURCES.txt +17 -0
- ms_graph_toolbox-0.1.0/ms_graph_toolbox.egg-info/dependency_links.txt +1 -0
- ms_graph_toolbox-0.1.0/ms_graph_toolbox.egg-info/requires.txt +2 -0
- ms_graph_toolbox-0.1.0/ms_graph_toolbox.egg-info/top_level.txt +1 -0
- ms_graph_toolbox-0.1.0/pyproject.toml +34 -0
- ms_graph_toolbox-0.1.0/setup.cfg +4 -0
- ms_graph_toolbox-0.1.0/tests/test_graph_email.py +144 -0
- ms_graph_toolbox-0.1.0/tests/test_graph_sharepoint.py +176 -0
- ms_graph_toolbox-0.1.0/tests/test_graph_users.py +145 -0
- ms_graph_toolbox-0.1.0/tests/test_imports.py +11 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 runway28R
|
|
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,181 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ms-graph-toolbox
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A lightweight Python wrapper around Microsoft Graph for email, users, and SharePoint operations.
|
|
5
|
+
Author: runway28R
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/runway28R/ms-graph-toolbox
|
|
8
|
+
Project-URL: Source, https://github.com/runway28R/ms-graph-toolbox
|
|
9
|
+
Project-URL: Issues, https://github.com/runway28R/ms-graph-toolbox/issues
|
|
10
|
+
Requires-Python: >=3.12
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Requires-Dist: requests>=2.31.0
|
|
14
|
+
Requires-Dist: msal>=1.27.0
|
|
15
|
+
Dynamic: license-file
|
|
16
|
+
|
|
17
|
+
# Microsoft Graph Toolbox
|
|
18
|
+
|
|
19
|
+
A lightweight Python wrapper around Microsoft Graph for sending email, retrieving users, and working with SharePoint files.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
|
|
23
|
+
- Authenticate with Microsoft Graph using Microsoft Entra ID application credentials
|
|
24
|
+
- Send email messages
|
|
25
|
+
- Retrieve users from Microsoft Graph
|
|
26
|
+
- List SharePoint sites and document libraries
|
|
27
|
+
- Browse folders and upload files to SharePoint
|
|
28
|
+
|
|
29
|
+
## Requirements
|
|
30
|
+
|
|
31
|
+
- Python 3.12 or newer
|
|
32
|
+
- A Microsoft Entra ID application registration
|
|
33
|
+
- Microsoft Graph API permissions
|
|
34
|
+
- A Microsoft 365 account with access to the required resources
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
Install the package from PyPI:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install ms-graph-toolbox
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Authentication
|
|
45
|
+
|
|
46
|
+
The package uses application credentials:
|
|
47
|
+
|
|
48
|
+
- Client ID
|
|
49
|
+
- Client secret
|
|
50
|
+
- Tenant ID
|
|
51
|
+
|
|
52
|
+
Make sure the application has the required Microsoft Graph permissions and that administrator consent has been granted where necessary.
|
|
53
|
+
|
|
54
|
+
## Basic Usage
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import logging
|
|
58
|
+
import os
|
|
59
|
+
|
|
60
|
+
from ms_graph_toolbox.ms_graph_toolbox import ms_graph_toolbox
|
|
61
|
+
|
|
62
|
+
logger = logging.getLogger(__name__)
|
|
63
|
+
|
|
64
|
+
graph = ms_graph_toolbox(
|
|
65
|
+
client_id=os.environ["MS_GRAPH_CLIENT_ID"],
|
|
66
|
+
client_secret=os.environ["MS_GRAPH_CLIENT_SECRET"],
|
|
67
|
+
tenant_id=os.environ["MS_GRAPH_TENANT_ID"],
|
|
68
|
+
logger=logger,
|
|
69
|
+
)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The object automatically obtains an access token that is used by the other package functions.
|
|
73
|
+
|
|
74
|
+
## Sending Email
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
from ms_graph_toolbox.graph_email import send_email
|
|
78
|
+
|
|
79
|
+
send_email(
|
|
80
|
+
gph_object=graph,
|
|
81
|
+
subject="Test message",
|
|
82
|
+
content_type="Text",
|
|
83
|
+
body="This message was sent using Microsoft Graph.",
|
|
84
|
+
sender="sender@example.com",
|
|
85
|
+
to_field="recipient@example.com",
|
|
86
|
+
)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Additional options include carbon-copy recipients, blind-carbon-copy recipients, message priority, and file attachments.
|
|
90
|
+
|
|
91
|
+
Multiple recipients can be provided as a comma-separated string:
|
|
92
|
+
|
|
93
|
+
```python
|
|
94
|
+
send_email(
|
|
95
|
+
gph_object=graph,
|
|
96
|
+
subject="Team update",
|
|
97
|
+
content_type="Text",
|
|
98
|
+
body="This message was sent to multiple recipients.",
|
|
99
|
+
sender="sender@example.com",
|
|
100
|
+
to_field="first@example.com, second@example.com",
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Retrieving Users
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from ms_graph_toolbox.graph_users import get_users
|
|
108
|
+
|
|
109
|
+
users = get_users(graph)
|
|
110
|
+
|
|
111
|
+
for user in users:
|
|
112
|
+
print(user)
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
You can also filter the results by name, title, email address, alias, company, or selected fields:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
users = get_users(
|
|
119
|
+
graph,
|
|
120
|
+
select_data=["displayName", "mail", "jobTitle"],
|
|
121
|
+
search_name="Alex",
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## SharePoint Operations
|
|
126
|
+
|
|
127
|
+
SharePoint functionality is available through the `graph_sharepoint` class:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from ms_graph_toolbox.graph_sharepoint import graph_sharepoint
|
|
131
|
+
|
|
132
|
+
sharepoint = graph_sharepoint(
|
|
133
|
+
access_token=graph.access_token,
|
|
134
|
+
logger=logger,
|
|
135
|
+
)
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The SharePoint tools support:
|
|
139
|
+
|
|
140
|
+
- Finding a SharePoint site
|
|
141
|
+
- Listing document libraries
|
|
142
|
+
- Browsing folder contents
|
|
143
|
+
- Uploading files
|
|
144
|
+
|
|
145
|
+
See the [examples](https://github.com/runway28R/ms-graph-toolbox/tree/main/examples) directory for complete SharePoint usage examples.
|
|
146
|
+
|
|
147
|
+
## Examples
|
|
148
|
+
|
|
149
|
+
The repository contains complete examples for:
|
|
150
|
+
|
|
151
|
+
- Sending email
|
|
152
|
+
- Retrieving users
|
|
153
|
+
- Uploading files to SharePoint
|
|
154
|
+
|
|
155
|
+
The examples are available in the [examples](https://github.com/runway28R/ms-graph-toolbox/tree/main/examples) directory.
|
|
156
|
+
|
|
157
|
+
## Configuration
|
|
158
|
+
|
|
159
|
+
For local development, set these environment variables before running the examples.
|
|
160
|
+
|
|
161
|
+
### PowerShell
|
|
162
|
+
|
|
163
|
+
```powershell
|
|
164
|
+
$env:MS_GRAPH_CLIENT_ID = "your-client-id"
|
|
165
|
+
$env:MS_GRAPH_CLIENT_SECRET = "your-client-secret"
|
|
166
|
+
$env:MS_GRAPH_TENANT_ID = "your-tenant-id"
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
### Bash
|
|
170
|
+
|
|
171
|
+
```bash
|
|
172
|
+
export MS_GRAPH_CLIENT_ID="your-client-id"
|
|
173
|
+
export MS_GRAPH_CLIENT_SECRET="your-client-secret"
|
|
174
|
+
export MS_GRAPH_TENANT_ID="your-tenant-id"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Do not commit client secrets or other credentials to the repository.
|
|
178
|
+
|
|
179
|
+
## License
|
|
180
|
+
|
|
181
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# Microsoft Graph Toolbox
|
|
2
|
+
|
|
3
|
+
A lightweight Python wrapper around Microsoft Graph for sending email, retrieving users, and working with SharePoint files.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Authenticate with Microsoft Graph using Microsoft Entra ID application credentials
|
|
8
|
+
- Send email messages
|
|
9
|
+
- Retrieve users from Microsoft Graph
|
|
10
|
+
- List SharePoint sites and document libraries
|
|
11
|
+
- Browse folders and upload files to SharePoint
|
|
12
|
+
|
|
13
|
+
## Requirements
|
|
14
|
+
|
|
15
|
+
- Python 3.12 or newer
|
|
16
|
+
- A Microsoft Entra ID application registration
|
|
17
|
+
- Microsoft Graph API permissions
|
|
18
|
+
- A Microsoft 365 account with access to the required resources
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
|
|
22
|
+
Install the package from PyPI:
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install ms-graph-toolbox
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Authentication
|
|
29
|
+
|
|
30
|
+
The package uses application credentials:
|
|
31
|
+
|
|
32
|
+
- Client ID
|
|
33
|
+
- Client secret
|
|
34
|
+
- Tenant ID
|
|
35
|
+
|
|
36
|
+
Make sure the application has the required Microsoft Graph permissions and that administrator consent has been granted where necessary.
|
|
37
|
+
|
|
38
|
+
## Basic Usage
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import logging
|
|
42
|
+
import os
|
|
43
|
+
|
|
44
|
+
from ms_graph_toolbox.ms_graph_toolbox import ms_graph_toolbox
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger(__name__)
|
|
47
|
+
|
|
48
|
+
graph = ms_graph_toolbox(
|
|
49
|
+
client_id=os.environ["MS_GRAPH_CLIENT_ID"],
|
|
50
|
+
client_secret=os.environ["MS_GRAPH_CLIENT_SECRET"],
|
|
51
|
+
tenant_id=os.environ["MS_GRAPH_TENANT_ID"],
|
|
52
|
+
logger=logger,
|
|
53
|
+
)
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The object automatically obtains an access token that is used by the other package functions.
|
|
57
|
+
|
|
58
|
+
## Sending Email
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from ms_graph_toolbox.graph_email import send_email
|
|
62
|
+
|
|
63
|
+
send_email(
|
|
64
|
+
gph_object=graph,
|
|
65
|
+
subject="Test message",
|
|
66
|
+
content_type="Text",
|
|
67
|
+
body="This message was sent using Microsoft Graph.",
|
|
68
|
+
sender="sender@example.com",
|
|
69
|
+
to_field="recipient@example.com",
|
|
70
|
+
)
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Additional options include carbon-copy recipients, blind-carbon-copy recipients, message priority, and file attachments.
|
|
74
|
+
|
|
75
|
+
Multiple recipients can be provided as a comma-separated string:
|
|
76
|
+
|
|
77
|
+
```python
|
|
78
|
+
send_email(
|
|
79
|
+
gph_object=graph,
|
|
80
|
+
subject="Team update",
|
|
81
|
+
content_type="Text",
|
|
82
|
+
body="This message was sent to multiple recipients.",
|
|
83
|
+
sender="sender@example.com",
|
|
84
|
+
to_field="first@example.com, second@example.com",
|
|
85
|
+
)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## Retrieving Users
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from ms_graph_toolbox.graph_users import get_users
|
|
92
|
+
|
|
93
|
+
users = get_users(graph)
|
|
94
|
+
|
|
95
|
+
for user in users:
|
|
96
|
+
print(user)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
You can also filter the results by name, title, email address, alias, company, or selected fields:
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
users = get_users(
|
|
103
|
+
graph,
|
|
104
|
+
select_data=["displayName", "mail", "jobTitle"],
|
|
105
|
+
search_name="Alex",
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## SharePoint Operations
|
|
110
|
+
|
|
111
|
+
SharePoint functionality is available through the `graph_sharepoint` class:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from ms_graph_toolbox.graph_sharepoint import graph_sharepoint
|
|
115
|
+
|
|
116
|
+
sharepoint = graph_sharepoint(
|
|
117
|
+
access_token=graph.access_token,
|
|
118
|
+
logger=logger,
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The SharePoint tools support:
|
|
123
|
+
|
|
124
|
+
- Finding a SharePoint site
|
|
125
|
+
- Listing document libraries
|
|
126
|
+
- Browsing folder contents
|
|
127
|
+
- Uploading files
|
|
128
|
+
|
|
129
|
+
See the [examples](https://github.com/runway28R/ms-graph-toolbox/tree/main/examples) directory for complete SharePoint usage examples.
|
|
130
|
+
|
|
131
|
+
## Examples
|
|
132
|
+
|
|
133
|
+
The repository contains complete examples for:
|
|
134
|
+
|
|
135
|
+
- Sending email
|
|
136
|
+
- Retrieving users
|
|
137
|
+
- Uploading files to SharePoint
|
|
138
|
+
|
|
139
|
+
The examples are available in the [examples](https://github.com/runway28R/ms-graph-toolbox/tree/main/examples) directory.
|
|
140
|
+
|
|
141
|
+
## Configuration
|
|
142
|
+
|
|
143
|
+
For local development, set these environment variables before running the examples.
|
|
144
|
+
|
|
145
|
+
### PowerShell
|
|
146
|
+
|
|
147
|
+
```powershell
|
|
148
|
+
$env:MS_GRAPH_CLIENT_ID = "your-client-id"
|
|
149
|
+
$env:MS_GRAPH_CLIENT_SECRET = "your-client-secret"
|
|
150
|
+
$env:MS_GRAPH_TENANT_ID = "your-tenant-id"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Bash
|
|
154
|
+
|
|
155
|
+
```bash
|
|
156
|
+
export MS_GRAPH_CLIENT_ID="your-client-id"
|
|
157
|
+
export MS_GRAPH_CLIENT_SECRET="your-client-secret"
|
|
158
|
+
export MS_GRAPH_TENANT_ID="your-tenant-id"
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Do not commit client secrets or other credentials to the repository.
|
|
162
|
+
|
|
163
|
+
## License
|
|
164
|
+
|
|
165
|
+
This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
|
|
File without changes
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import base64
|
|
3
|
+
import os
|
|
4
|
+
import mimetypes
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def send_email(gph_object,
|
|
8
|
+
subject,
|
|
9
|
+
content_type,
|
|
10
|
+
body,
|
|
11
|
+
sender,
|
|
12
|
+
to_field,
|
|
13
|
+
cc_field=None,
|
|
14
|
+
bcc_field=None,
|
|
15
|
+
priority="Normal",
|
|
16
|
+
attachments=None):
|
|
17
|
+
"""
|
|
18
|
+
Send an email using Microsoft Graph on behalf of `sender`.
|
|
19
|
+
|
|
20
|
+
Parameters:
|
|
21
|
+
logger: logger instance to record actions (debug/error).
|
|
22
|
+
subject: email subject string.
|
|
23
|
+
content_type: one of 'text', 'html', 'text/plain', 'text/html', etc.
|
|
24
|
+
body: email body string.
|
|
25
|
+
to_field, cc_field, bcc_field: comma-separated recipient strings (e.g. "a@x.com, b@y.com").
|
|
26
|
+
priority: message importance, e.g. "Low", "Normal", "High".
|
|
27
|
+
attachments: optional list of attachment descriptors. Each descriptor may be:
|
|
28
|
+
- {'path': 'C:\\full\\path\\file.pdf', 'name': 'file.pdf', 'content_type': 'application/pdf', 'inline': False}
|
|
29
|
+
- {'content_bytes': b'...', 'name': 'image.png', 'content_type': 'image/png', 'inline': True, 'content_id': 'img1'}
|
|
30
|
+
For inline images set 'inline': True and reference them in an HTML body as <img src="cid:content_id">.
|
|
31
|
+
If content_type not provided it will be guessed from the filename.
|
|
32
|
+
The code will base64-encode file contents as required by Graph.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
0 on success (202 response), 1 on exception, 2 if no access token, 3 on non-202 HTTP response.
|
|
36
|
+
"""
|
|
37
|
+
try:
|
|
38
|
+
# Ensure we have an access token before attempting to send
|
|
39
|
+
if not gph_object.access_token:
|
|
40
|
+
gph_object.logger.error("Invalid Access Token, email cannot be sent!")
|
|
41
|
+
return 2
|
|
42
|
+
|
|
43
|
+
# Parse recipient fields into Graph-friendly lists
|
|
44
|
+
to_recipients = parse_recipients(to_field)
|
|
45
|
+
cc_recipients = parse_recipients(cc_field)
|
|
46
|
+
bcc_recipients = parse_recipients(bcc_field)
|
|
47
|
+
|
|
48
|
+
# Normalize content type to one of Graph's expected values ("Text" or "HTML"), use "Text" as default
|
|
49
|
+
valid_types = {"text": "Text", "plain": "Text", "html": "HTML", "text/plain": "Text", "text/html": "HTML"}
|
|
50
|
+
content_type_normalized = valid_types.get(content_type.strip().lower(), "Text")
|
|
51
|
+
|
|
52
|
+
# Process attachments and if any contain inline items but the body is not HTML, log a warning
|
|
53
|
+
inline_found = False
|
|
54
|
+
attachments_payload = []
|
|
55
|
+
if attachments:
|
|
56
|
+
# Process all attachments in one pass
|
|
57
|
+
attachments_payload = [
|
|
58
|
+
att for desc in attachments
|
|
59
|
+
if (att := build_attachment(descriptor=desc, logger=gph_object.logger)) is not None
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
# Check for inline attachments
|
|
63
|
+
inline_found = any(att.get("isInline") for att in attachments_payload)
|
|
64
|
+
|
|
65
|
+
# Log warning if inline attachments found with non-HTML content
|
|
66
|
+
if inline_found and content_type_normalized != "HTML":
|
|
67
|
+
gph_object.logger.warning("Inline attachments present but body is not HTML. Inline images require HTML body and <img src=\"cid:contentId\"> references.")
|
|
68
|
+
|
|
69
|
+
# Build sendMail endpoint for the configured sender user
|
|
70
|
+
endpoint = f"https://graph.microsoft.com/v1.0/users/{sender}/sendMail"
|
|
71
|
+
|
|
72
|
+
# Compose message payload according to Graph sendMail schema
|
|
73
|
+
email_msg = {
|
|
74
|
+
"message": {
|
|
75
|
+
"subject": subject,
|
|
76
|
+
"body": {
|
|
77
|
+
"contentType": content_type_normalized,
|
|
78
|
+
"content": body
|
|
79
|
+
},
|
|
80
|
+
"toRecipients": to_recipients,
|
|
81
|
+
"importance": priority
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
# Attach cc/bcc only if provided
|
|
86
|
+
if cc_recipients:
|
|
87
|
+
email_msg["message"]["ccRecipients"] = cc_recipients
|
|
88
|
+
if bcc_recipients:
|
|
89
|
+
email_msg["message"]["bccRecipients"] = bcc_recipients
|
|
90
|
+
|
|
91
|
+
# Attach attachments if any
|
|
92
|
+
if attachments_payload:
|
|
93
|
+
email_msg["message"]["attachments"] = attachments_payload
|
|
94
|
+
|
|
95
|
+
# Set authorization header with Bearer token
|
|
96
|
+
headers = {"Authorization": f"Bearer {gph_object.access_token}", "Content-Type": "application/json"}
|
|
97
|
+
gph_object.logger.debug(f"Sending email using MS Graph from {sender}")
|
|
98
|
+
response = requests.post(endpoint, headers=headers, json=email_msg)
|
|
99
|
+
|
|
100
|
+
# 202 Accepted indicates Graph accepted the send request
|
|
101
|
+
if response.status_code == 202:
|
|
102
|
+
gph_object.logger.debug("Sending successful!")
|
|
103
|
+
return 0
|
|
104
|
+
else:
|
|
105
|
+
# Log status and response body for debugging failures
|
|
106
|
+
gph_object.logger.error(f"Sending Failed: {response.status_code}, {response.text}")
|
|
107
|
+
return 3
|
|
108
|
+
except Exception as e:
|
|
109
|
+
# Any unexpected exception during send is logged
|
|
110
|
+
gph_object.logger.error(f"Sending Failed: {e}")
|
|
111
|
+
return 1
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
# Convert a comma-separated string of addresses into the Graph recipient JSON format.
|
|
115
|
+
def parse_recipients(field):
|
|
116
|
+
if not field:
|
|
117
|
+
return []
|
|
118
|
+
return [{"emailAddress": {"address": addr.strip()}} for addr in field.split(",") if addr.strip()]
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# Build Graph attachment payloads from descriptors
|
|
122
|
+
def build_attachment(descriptor, logger):
|
|
123
|
+
"""
|
|
124
|
+
Accepts either:
|
|
125
|
+
- {'path': 'C:\\file', ...}
|
|
126
|
+
- {'content_bytes': b'...', ...}
|
|
127
|
+
Returns a dict suitable for Graph message attachments.
|
|
128
|
+
"""
|
|
129
|
+
try:
|
|
130
|
+
# Basic check
|
|
131
|
+
if "path" not in descriptor and "content_bytes" not in descriptor:
|
|
132
|
+
logger.error(f"build_attachment failed: Descriptor missing 'path' or 'content_bytes': {descriptor}")
|
|
133
|
+
return None
|
|
134
|
+
# Determine name
|
|
135
|
+
name = descriptor.get("name")
|
|
136
|
+
content_type_guess = None
|
|
137
|
+
|
|
138
|
+
# Load bytes from file path if provided
|
|
139
|
+
if "path" in descriptor and descriptor["path"]:
|
|
140
|
+
path = descriptor["path"]
|
|
141
|
+
with open(path, "rb") as f:
|
|
142
|
+
data = f.read()
|
|
143
|
+
if not name:
|
|
144
|
+
name = os.path.basename(path)
|
|
145
|
+
content_type_guess = mimetypes.guess_type(path)[0]
|
|
146
|
+
elif "content_bytes" in descriptor and descriptor["content_bytes"] is not None:
|
|
147
|
+
data = descriptor["content_bytes"]
|
|
148
|
+
if isinstance(data, str):
|
|
149
|
+
data = data.encode("utf-8")
|
|
150
|
+
|
|
151
|
+
# Determine contentType
|
|
152
|
+
content_type = descriptor.get("content_type") or content_type_guess or "application/octet-stream"
|
|
153
|
+
|
|
154
|
+
# Base64 encode content
|
|
155
|
+
content_bytes_b64 = base64.b64encode(data).decode("utf-8")
|
|
156
|
+
|
|
157
|
+
# Build attachment object (fileAttachment)
|
|
158
|
+
attachment = {
|
|
159
|
+
"@odata.type": "#microsoft.graph.fileAttachment",
|
|
160
|
+
"name": name or "attachment",
|
|
161
|
+
"contentType": content_type,
|
|
162
|
+
"contentBytes": content_bytes_b64
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
# Inline settings for images
|
|
166
|
+
if descriptor.get("inline"):
|
|
167
|
+
attachment["isInline"] = True
|
|
168
|
+
# contentId is used as cid reference in HTML body: <img src="cid:contentId">
|
|
169
|
+
attachment["contentId"] = descriptor.get("content_id") or (name or "inline")
|
|
170
|
+
return attachment
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.error(f"build_attachment: failed for descriptor {descriptor}: {e}")
|
|
173
|
+
return None
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
from urllib.parse import quote
|
|
2
|
+
import pathlib as pl
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class graph_sharepoint:
|
|
7
|
+
def __init__(self, access_token:str, logger):
|
|
8
|
+
self.access_token = access_token
|
|
9
|
+
self.logger = logger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def get_site_id(self, site_url:str):
|
|
13
|
+
# Request site ID
|
|
14
|
+
try:
|
|
15
|
+
full_url = f'https://graph.microsoft.com/v1.0/sites/{site_url}'
|
|
16
|
+
response = requests.get(full_url,
|
|
17
|
+
headers={'Authorization': f'Bearer {self.access_token}'})
|
|
18
|
+
self.logger.debug(f"get_site_id response: {response.status_code} - {response.text}")
|
|
19
|
+
return response.json().get('id') # Return the site ID
|
|
20
|
+
except Exception as e:
|
|
21
|
+
self.logger.error(f"get_site_id failed: {e}")
|
|
22
|
+
return None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def get_document_libraries(self, site_id:str):
|
|
26
|
+
# Retrieve drive IDs and names associated with a site
|
|
27
|
+
try:
|
|
28
|
+
drives_url = f'https://graph.microsoft.com/v1.0/sites/{site_id}/drives'
|
|
29
|
+
response = requests.get(drives_url, headers={'Authorization': f'Bearer {self.access_token}'})
|
|
30
|
+
drives = response.json().get('value', [])
|
|
31
|
+
return [(drive['id'], drive['name']) for drive in drives]
|
|
32
|
+
except Exception as e:
|
|
33
|
+
self.logger.error(f"get_document_libraries failed: {e}")
|
|
34
|
+
return None
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_folder_content(self, site_id:str, drive_id:str):
|
|
38
|
+
# Get the contents of a folder
|
|
39
|
+
try:
|
|
40
|
+
folder_url = f'https://graph.microsoft.com/v1.0/sites/{site_id}/drives/{drive_id}/root/children'
|
|
41
|
+
response = requests.get(folder_url, headers={'Authorization': f'Bearer {self.access_token}'})
|
|
42
|
+
return response.json().get('value', [])
|
|
43
|
+
except Exception as e:
|
|
44
|
+
self.logger.error(f"get_folder_content failed: {e}")
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
def print_folder_content(self, folder_content):
|
|
48
|
+
# Display the contents of a SharePoint folder
|
|
49
|
+
folders = []
|
|
50
|
+
files = []
|
|
51
|
+
|
|
52
|
+
for item in folder_content:
|
|
53
|
+
if "folder" in item:
|
|
54
|
+
folders.append(item["name"])
|
|
55
|
+
elif "file" in item:
|
|
56
|
+
files.append(item["name"])
|
|
57
|
+
|
|
58
|
+
print(f"Folders: {len(folders)}")
|
|
59
|
+
for f in sorted(folders):
|
|
60
|
+
print(f)
|
|
61
|
+
|
|
62
|
+
print(f"Files: {len(files)}")
|
|
63
|
+
for f in sorted(files):
|
|
64
|
+
print(f)
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def upload_file_graph(self, site_id, drive_id:str, folder_path:str, local_file_path:str):
|
|
68
|
+
"""
|
|
69
|
+
Uploads a file to a SharePoint folder using Microsoft Graph API.
|
|
70
|
+
|
|
71
|
+
:param site_id: The SharePoint site ID
|
|
72
|
+
:param drive_id: The drive ID for the desired folder
|
|
73
|
+
:param folder_path: Path inside taht folder
|
|
74
|
+
:param local_file_path: Local path of the file to be uploaded
|
|
75
|
+
:return: (file_url, success_file_count)
|
|
76
|
+
"""
|
|
77
|
+
try:
|
|
78
|
+
file_path = pl.Path(local_file_path)
|
|
79
|
+
if not file_path.is_file():
|
|
80
|
+
raise FileNotFoundError(f"File not found: {file_path}")
|
|
81
|
+
|
|
82
|
+
with open(file_path, "rb") as file:
|
|
83
|
+
file_content = file.read()
|
|
84
|
+
|
|
85
|
+
# Build the upload URL
|
|
86
|
+
folder_path_encoded = quote(folder_path)
|
|
87
|
+
upload_url = (
|
|
88
|
+
f"https://graph.microsoft.com/v1.0/sites/{site_id}/drives/{drive_id}/root:/{folder_path_encoded}/{file_path.name}:/content"
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
headers = {
|
|
92
|
+
"Authorization": f"Bearer {self.access_token}",
|
|
93
|
+
"Content-Type": "application/octet-stream"
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
response = requests.put(upload_url, headers=headers, data=file_content)
|
|
97
|
+
if response.status_code in [200, 201]:
|
|
98
|
+
file_info = response.json()
|
|
99
|
+
file_url = file_info.get("webUrl", "")
|
|
100
|
+
self.logger.debug(f"File uploaded to: {file_url}")
|
|
101
|
+
return file_url, 1
|
|
102
|
+
else:
|
|
103
|
+
self.logger.error(f"Upload to {upload_url} failed: {response.text}")
|
|
104
|
+
return response.text, 0
|
|
105
|
+
except FileNotFoundError as e:
|
|
106
|
+
return str(e), 0
|
|
107
|
+
except Exception as e:
|
|
108
|
+
self.logger.error(f"upload_file_graph failed: {e}")
|
|
109
|
+
return str(e), 0
|