ms-graph-toolbox 0.1.0__py3-none-any.whl
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/__init__.py +0 -0
- ms_graph_toolbox/graph_email.py +173 -0
- ms_graph_toolbox/graph_sharepoint.py +109 -0
- ms_graph_toolbox/graph_users.py +109 -0
- ms_graph_toolbox/ms_graph_toolbox.py +57 -0
- ms_graph_toolbox-0.1.0.dist-info/METADATA +181 -0
- ms_graph_toolbox-0.1.0.dist-info/RECORD +10 -0
- ms_graph_toolbox-0.1.0.dist-info/WHEEL +5 -0
- ms_graph_toolbox-0.1.0.dist-info/licenses/LICENSE +21 -0
- ms_graph_toolbox-0.1.0.dist-info/top_level.txt +1 -0
|
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
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def get_users(gph_object,
|
|
5
|
+
select_data: str | None = None,
|
|
6
|
+
search_name: str | None = None,
|
|
7
|
+
search_title: str | None = None,
|
|
8
|
+
search_email: str | None = None,
|
|
9
|
+
search_alias: str | None = None,
|
|
10
|
+
search_company: str | None = None
|
|
11
|
+
) -> list[dict] | None:
|
|
12
|
+
"""
|
|
13
|
+
Search for users in Microsoft Graph based on provided criteria.
|
|
14
|
+
|
|
15
|
+
Notes:
|
|
16
|
+
- companyName is not reliably supported in server-side $filter for all tenants/APIs,
|
|
17
|
+
so company filtering is applied client-side to avoid "unsupported filter" errors.
|
|
18
|
+
- This function uses `startswith(...)` for server-side partial matching where possible
|
|
19
|
+
and escapes single quotes in filter values.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
gph_object: An initialized ms_graph_toolbox object with valid access_token and logger.
|
|
23
|
+
select_data: Comma-separated string or a list of properties to return (e.g. "displayName,mail,jobTitle").
|
|
24
|
+
search_name: Filter users by displayName (partial, startswith).
|
|
25
|
+
search_title: Filter users by jobTitle (partial, startswith).
|
|
26
|
+
search_email: Filter users by mail (partial, startswith).
|
|
27
|
+
search_alias: Filter users by alias (mailNickname / userPrincipalName / proxyAddresses) (exact or startswith).
|
|
28
|
+
search_company: Filter users by companyName (partial, case-insensitive). Applied client-side.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
List of user dicts matching criteria, or None on error.
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
def esc(val: str) -> str:
|
|
35
|
+
return val.replace("'", "''")
|
|
36
|
+
|
|
37
|
+
if not any([search_name, search_title, search_email, search_alias, search_company]):
|
|
38
|
+
gph_object.logger.warning("No filters provided; retrieving all users may be slow in large organizations.")
|
|
39
|
+
|
|
40
|
+
# Build server-side filters (exclude companyName to avoid unsupported-filter errors)
|
|
41
|
+
server_filters = []
|
|
42
|
+
if search_name:
|
|
43
|
+
server_filters.append(f"startswith(displayName,'{esc(search_name)}')")
|
|
44
|
+
if search_title:
|
|
45
|
+
server_filters.append(f"startswith(jobTitle,'{esc(search_title)}')")
|
|
46
|
+
if search_email:
|
|
47
|
+
server_filters.append(f"startswith(mail,'{esc(search_email)}')")
|
|
48
|
+
if search_alias:
|
|
49
|
+
# Try matching common alias forms
|
|
50
|
+
alias_escaped = esc(search_alias)
|
|
51
|
+
server_filters.append(
|
|
52
|
+
f"(startswith(userPrincipalName,'{alias_escaped}') or startswith(mailNickname,'{alias_escaped}') "
|
|
53
|
+
f"or proxyAddresses/any(x:x eq 'smtp:{alias_escaped}') or proxyAddresses/any(x:x eq 'SMTP:{alias_escaped}'))"
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
endpoint = "https://graph.microsoft.com/v1.0/users"
|
|
57
|
+
params = {}
|
|
58
|
+
if server_filters:
|
|
59
|
+
params["$filter"] = " and ".join(server_filters)
|
|
60
|
+
if select_data:
|
|
61
|
+
if isinstance(select_data, list):
|
|
62
|
+
params["$select"] = ",".join(select_data)
|
|
63
|
+
else:
|
|
64
|
+
params["$select"] = select_data
|
|
65
|
+
# Include count if desired (note: some endpoints require ConsistencyLevel header for $count)
|
|
66
|
+
params["$count"] = "true"
|
|
67
|
+
|
|
68
|
+
headers = {"Authorization": f"Bearer {gph_object.access_token}",
|
|
69
|
+
"ConsistencyLevel": "eventual"}
|
|
70
|
+
users_list = []
|
|
71
|
+
|
|
72
|
+
# Use params on first request; if @odata.nextLink is returned, follow it directly (it already contains params)
|
|
73
|
+
url = endpoint
|
|
74
|
+
first = True
|
|
75
|
+
while True:
|
|
76
|
+
if first:
|
|
77
|
+
resp = requests.get(url, headers=headers, params=params)
|
|
78
|
+
first = False
|
|
79
|
+
else:
|
|
80
|
+
resp = requests.get(url, headers=headers) # nextLink already has query
|
|
81
|
+
if resp.status_code != 200:
|
|
82
|
+
gph_object.logger.error(f"Failed to retrieve users: {resp.status_code} - {resp.text}")
|
|
83
|
+
return None
|
|
84
|
+
|
|
85
|
+
data = resp.json()
|
|
86
|
+
users = data.get("value", [])
|
|
87
|
+
users_list.extend(users)
|
|
88
|
+
|
|
89
|
+
next_link = data.get("@odata.nextLink")
|
|
90
|
+
if not next_link:
|
|
91
|
+
break
|
|
92
|
+
url = next_link
|
|
93
|
+
|
|
94
|
+
# Apply company filter client-side (case-insensitive, partial match) if requested
|
|
95
|
+
if search_company:
|
|
96
|
+
sc = search_company.lower()
|
|
97
|
+
users_list = [u for u in users_list if sc in (u.get("companyName") or "").lower()]
|
|
98
|
+
|
|
99
|
+
gph_object.logger.debug(f"Retrieved {len(users_list)} users")
|
|
100
|
+
return users_list
|
|
101
|
+
|
|
102
|
+
except Exception as e:
|
|
103
|
+
# Log exception details
|
|
104
|
+
try:
|
|
105
|
+
gph_object.logger.error(f"Exception occurred while searching for users: {e}")
|
|
106
|
+
except Exception:
|
|
107
|
+
# Fallback if logger not available
|
|
108
|
+
pass
|
|
109
|
+
return None
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Basic code to obtain an application token via MSAL to be used with Microsoft Graph.
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
import msal
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class ms_graph_toolbox:
|
|
9
|
+
"""
|
|
10
|
+
Wrapper class to handle app-only authentication and sending email via Microsoft Graph.
|
|
11
|
+
|
|
12
|
+
Attributes:
|
|
13
|
+
logger: Logger with .debug/.info/.warning/.error methods for logging.
|
|
14
|
+
sender: The user (email) that will be used as the "From" / mailbox to send from.
|
|
15
|
+
client_id: Azure AD Application (client) ID used for the OAuth2 client credentials flow.
|
|
16
|
+
client_secret: Confidential value (application secret) for the Azure AD app used to authenticate the app.
|
|
17
|
+
tenant_id: Azure AD tenant identifier (GUID) or tenant domain used to build the authority URL
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self,
|
|
21
|
+
client_id,
|
|
22
|
+
client_secret,
|
|
23
|
+
tenant_id,
|
|
24
|
+
logger):
|
|
25
|
+
|
|
26
|
+
# Set logger and access token placeholder
|
|
27
|
+
self.logger = logger
|
|
28
|
+
self.access_token = None
|
|
29
|
+
|
|
30
|
+
try:
|
|
31
|
+
# Build authority URL for tenant
|
|
32
|
+
authority = f"https://login.microsoftonline.com/{tenant_id}"
|
|
33
|
+
# Use .default scope for client credentials to get app-level permissions
|
|
34
|
+
scopes = ["https://graph.microsoft.com/.default"]
|
|
35
|
+
|
|
36
|
+
# Create MSAL confidential client app using client credentials
|
|
37
|
+
app = msal.ConfidentialClientApplication(
|
|
38
|
+
client_id,
|
|
39
|
+
authority=authority,
|
|
40
|
+
client_credential=client_secret
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
# Acquire token for client (app-only)
|
|
44
|
+
result = app.acquire_token_for_client(scopes)
|
|
45
|
+
|
|
46
|
+
# If token obtained, store it. Otherwise log the error.
|
|
47
|
+
if "access_token" in result:
|
|
48
|
+
self.access_token = result["access_token"]
|
|
49
|
+
logger.debug("Successfully obtained Graph API token.")
|
|
50
|
+
else:
|
|
51
|
+
# error_description may contain helpful details about why token request failed
|
|
52
|
+
error_msg = result.get("error_description", str(result))
|
|
53
|
+
logger.error(f"Failed to get token: {error_msg}")
|
|
54
|
+
|
|
55
|
+
except Exception as e:
|
|
56
|
+
# Catch-all to ensure initialization failure is logged
|
|
57
|
+
logger.error(f"graph_emailer initialization failed: {e}")
|
|
@@ -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,10 @@
|
|
|
1
|
+
ms_graph_toolbox/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
+
ms_graph_toolbox/graph_email.py,sha256=SBCKsGf_rzqYgD9H5_N7B3sCrhGei6liO626S3H4k-0,7521
|
|
3
|
+
ms_graph_toolbox/graph_sharepoint.py,sha256=j4XKcvFEb3ithrQMv4KF1Tu03R6HXx7NUq3ImhyCJnM,4345
|
|
4
|
+
ms_graph_toolbox/graph_users.py,sha256=tuwLaBXqgsm0vCZDS6ByK_G7izCjWumvuhQtaFVZHpw,4861
|
|
5
|
+
ms_graph_toolbox/ms_graph_toolbox.py,sha256=5y9Ft1JLCntcPcATCfEf7NDzMoQaRaBYAlUBstNhpmU,2361
|
|
6
|
+
ms_graph_toolbox-0.1.0.dist-info/licenses/LICENSE,sha256=D-rqtNjpHqJK4pXKCW9x_aHi0JsjhpbpmzL4uSpgEsg,1087
|
|
7
|
+
ms_graph_toolbox-0.1.0.dist-info/METADATA,sha256=u3mRjhEdOdEri6sUYufLBHAma1vOHjdpl4yFREIPfaU,4660
|
|
8
|
+
ms_graph_toolbox-0.1.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
9
|
+
ms_graph_toolbox-0.1.0.dist-info/top_level.txt,sha256=DEG-OQ73sCMidsbyVye0y05oZVXeFOUVWhkzmlrhZAw,17
|
|
10
|
+
ms_graph_toolbox-0.1.0.dist-info/RECORD,,
|
|
@@ -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 @@
|
|
|
1
|
+
ms_graph_toolbox
|