Skip to content

Commit fd3b26a

Browse files
author
Vadim
committed
Add Teams and Outlook examples: tags, clone, shared channels, archive lifecycle, message lifecycle, folder management, contacts, meeting finder
1 parent 495da28 commit fd3b26a

11 files changed

Lines changed: 865 additions & 0 deletions

File tree

examples/outlook/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ Microsoft Graph.
1616
| `Mail.Read` (delegated) | Read mail tips and message properties | [Mail permissions](https://learn.microsoft.com/en-us/graph/permissions-reference#mail-permissions) |
1717
| `Reports.Read.All` (delegated) | Access email and mailbox usage reports | [Reports permissions](https://learn.microsoft.com/en-us/graph/permissions-reference#reports-permissions) |
1818
| `User.Read.All` (delegated or app) | Read user properties for mailbox audit, shared mailboxes, forwarding detection | [User permissions](https://learn.microsoft.com/en-us/graph/permissions-reference#user-permissions) |
19+
| `Contacts.ReadWrite` (delegated) | Create, read, update, delete personal contacts | [Contacts permissions](https://learn.microsoft.com/en-us/graph/permissions-reference#contacts-permissions) |
20+
| `Place.Read.All` (delegated) | List rooms and room lists | [Places permissions](https://learn.microsoft.com/en-us/graph/permissions-reference#places-permissions) |
1921

2022
---
2123

@@ -61,6 +63,10 @@ flowchart LR
6163
| **Mailbox audit** — auto-replies, mailbox settings report | [`mailboxes/report.py`](./mailboxes/report.py) | Bulk audit of user mailbox configurations |
6264
| **Shared mailboxes** — list and check configuration | [`shared_mailboxes/report.py`](./shared_mailboxes/report.py) | Discover and validate shared mailbox setup |
6365
| **Mail flow audit** — detect external forwarding | [`mail_flow/forwarding_report.py`](./mail_flow/forwarding_report.py) | Security audit — identify users forwarding mail externally |
66+
| **Message lifecycle** — reply, forward, move, copy in one flow | [`messages/lifecycle.py`](./messages/lifecycle.py) | Common post-receive operations: create reply draft → edit → send, forward to someone else, move and copy across folders |
67+
| **Folder management** — create, empty, mark-all-read, permanent delete | [`messages/folders.py`](./messages/folders.py) | Mailbox cleanup and compliance: create folder tree, empty content (including subfolders), bulk mark read, hard delete |
68+
| **Contacts** — create, update, delete with contact folders | [`contacts/manage.py`](./contacts/manage.py) | Full personal contact CRUD with contact folder organization, multiple email addresses, and lifecycle management |
69+
| **Meeting finder + rooms** — suggest time slots, list rooms, check availability | [`calendars/meeting_finder.py`](./calendars/meeting_finder.py) | Scheduling assistant pattern: find meeting times with attendees and rooms, list places/room lists, room capacity info |
6470

6571
---
6672

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""
2+
Meeting finder: suggest optimal time slots and resolve rooms.
3+
4+
Brings together three related patterns:
5+
1. Find available meeting times that work for all attendees.
6+
2. List room lists and rooms in the tenant.
7+
3. Get free/busy schedule for a room across a time window.
8+
9+
Useful for scheduling assistants, booking flows, and room
10+
management tooling.
11+
12+
Requires delegated permissions:
13+
Calendars.ReadWrite Create and manage events
14+
MailboxSettings.Read Read user availability
15+
Place.Read.All List rooms and room lists
16+
17+
https://learn.microsoft.com/en-us/graph/api/user-findmeetingtimes
18+
https://learn.microsoft.com/en-us/graph/api/resources/place
19+
"""
20+
21+
from datetime import datetime, timedelta, timezone
22+
23+
from office365.graph_client import GraphClient
24+
from tests import test_client_id, test_password, test_tenant, test_username
25+
26+
27+
def main():
28+
client = GraphClient(tenant=test_tenant).with_username_and_password(test_client_id, test_username, test_password)
29+
30+
# -- Step 1: list room lists and rooms in the tenant --
31+
print("Places (room lists and rooms):")
32+
all_places = client.places.get().execute_query()
33+
for p in all_places:
34+
print(f" {p.display_name:45s} type={p.entity_type_name}")
35+
print()
36+
37+
# Try to get room lists specifically
38+
room_lists = client.places.room_lists.get().execute_query()
39+
if room_lists:
40+
print("Room lists:")
41+
for rl in room_lists:
42+
print(f" {rl.display_name:45s} {rl.email_address or ''}")
43+
# Get rooms in this list
44+
try:
45+
rooms = rl.rooms.get().execute_query()
46+
for room in rooms:
47+
print(f" ↳ {room.display_name:40s} {room.email_address or ''} ({room.capacity or '?'} seats)")
48+
except Exception:
49+
pass
50+
else:
51+
print("(No room lists found — room list management may need Place.Read.All)")
52+
53+
# -- Step 2: find meeting times for next week --
54+
now = datetime.now(timezone.utc)
55+
start_time = now + timedelta(days=1)
56+
end_time = now + timedelta(days=7)
57+
58+
# Collect room email addresses if available
59+
attendee_smtp = ["meganb@contoso.onmicrosoft.com"]
60+
if room_lists:
61+
for rl in room_lists[:1]: # first room list
62+
try:
63+
rooms = rl.rooms.get().execute_query()
64+
attendee_smtp.extend(r.email_address for r in rooms[:2] if r.email_address)
65+
except Exception:
66+
pass
67+
68+
print(f"\nFinding meeting times for: {', '.join(attendee_smtp)}")
69+
print(f" Window: {start_time.strftime('%Y-%m-%d %H:%M')}{end_time.strftime('%Y-%m-%d %H:%M')}")
70+
71+
suggestions = client.me.find_meeting_times(
72+
attendees=attendee_smtp,
73+
meeting_duration=timedelta(hours=1),
74+
max_candidates=5,
75+
).execute_query()
76+
77+
if suggestions and hasattr(suggestions, "value") and suggestions.value:
78+
print(f"\nSuggested times ({len(suggestions.value)} candidate(s)):\n")
79+
# The value is a MeetingTimeSuggestionsResult
80+
result = suggestions.value
81+
if hasattr(result, "meeting_time_suggestions") and result.meeting_time_suggestions:
82+
for s in result.meeting_time_suggestions:
83+
slot = s.meeting_time_slot
84+
print(f" {slot.start.strftime('%Y-%m-%d %H:%M')}{slot.end.strftime('%H:%M')} (confidence: {s.confidence})")
85+
if hasattr(s, "locations") and s.locations:
86+
for loc in s.locations:
87+
print(f" Location: {loc.display_name}")
88+
else:
89+
print(" (no detailed suggestions — check permissions)")
90+
else:
91+
print(" No meeting suggestions returned.")
92+
93+
94+
if __name__ == "__main__":
95+
main()
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
"""
2+
Contact management: create, read, update, and delete personal contacts.
3+
4+
Covers the full CRUD lifecycle for Outlook contacts, including
5+
contact folders for organization.
6+
7+
Requires delegated permission ``Contacts.ReadWrite``.
8+
9+
https://learn.microsoft.com/en-us/graph/api/resources/contact
10+
"""
11+
12+
import sys
13+
14+
from office365.graph_client import GraphClient
15+
from tests import test_client_id, test_password, test_tenant, test_username
16+
17+
18+
def main():
19+
client = GraphClient(tenant=test_tenant).with_username_and_password(test_client_id, test_username, test_password)
20+
21+
# -- Step 1: create a contact folder --
22+
folder_name = "SDK Demo Contacts"
23+
folders = client.me.contact_folders.get().execute_query()
24+
folder = next((f for f in folders if f.display_name == folder_name), None)
25+
26+
if folder is None:
27+
folder = client.me.contact_folders.add(folder_name).execute_query()
28+
print(f"Created contact folder: '{folder.display_name}' (id: {folder.id})")
29+
else:
30+
print(f"Using existing folder: '{folder.display_name}'")
31+
32+
# -- Step 2: create a contact --
33+
contact = (
34+
folder.contacts.add(
35+
given_name="Maria",
36+
surname="Andersen",
37+
email_address="mariaa@contoso.onmicrosoft.com",
38+
business_phone="+1 555 123 4567",
39+
)
40+
.execute_query()
41+
)
42+
print(f"Created contact: {contact.display_name} ({contact.primary_email_address.address})")
43+
44+
# -- Step 3: update fields --
45+
contact.set_property("mobilePhone", "+1 555 987 6543")
46+
contact.set_property("jobTitle", "Senior Engineer")
47+
contact.update().execute_query()
48+
print(f"Updated contact: mobile & job title set.")
49+
50+
# -- Step 4: add a second email address --
51+
# The email_addresses property is a collection — append to it
52+
contact.email_addresses.add("maria.andersen@personal.com")
53+
contact.update().execute_query()
54+
print(f"Added secondary email.")
55+
56+
# -- Step 5: list all contacts in the folder --
57+
contacts = folder.contacts.get().execute_query()
58+
print(f"\nContacts in '{folder_name}' ({len(contacts)}):")
59+
for c in contacts:
60+
print(f" {c.display_name:25s} {c.primary_email_address.address or '':35s} {c.mobile_phone or '':15s}")
61+
62+
# -- Step 6: delete the contact --
63+
contact.delete_object().execute_query()
64+
print(f"\n✓ Deleted contact: {contact.display_name}")
65+
66+
67+
if __name__ == "__main__":
68+
main()
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
"""
2+
Mail folder management: create, navigate, empty, mark all read, and
3+
permanent-delete folders.
4+
5+
Mailbox cleanup and compliance workflows often need bulk operations
6+
on folders — purge old content, reset unread counts, or remove
7+
stale folder trees.
8+
9+
Requires delegated permission ``Mail.ReadWrite`` and
10+
``MailboxSettings.ReadWrite``.
11+
12+
https://learn.microsoft.com/en-us/graph/api/resources/mailfolder
13+
"""
14+
15+
from office365.graph_client import GraphClient
16+
from tests import test_client_id, test_password, test_tenant, test_username
17+
18+
19+
def walk_folders(client: GraphClient, parent_path="inbox", indent=0):
20+
"""Recursively list folders and subfolders with item counts."""
21+
parent = client.me.mail_folders[parent_path]
22+
parent.select(["displayName", "totalItemCount", "unreadItemCount"]).expand(["childFolders"]).execute_query()
23+
24+
prefix = " " * indent
25+
print(
26+
f"{prefix}{parent.display_name} "
27+
f"({parent.total_item_count or 0} items, "
28+
f"{parent.unread_item_count or 0} unread)"
29+
)
30+
31+
if parent.child_folders:
32+
for child in parent.child_folders:
33+
walk_folders(client, child.id, indent + 1)
34+
35+
36+
def main():
37+
client = GraphClient(tenant=test_tenant).with_username_and_password(test_client_id, test_username, test_password)
38+
39+
# -- Step 1: create a folder --
40+
inbox = client.me.mail_folders["inbox"]
41+
new_folder = inbox.child_folders.add("Cleanup Test").execute_query()
42+
print(f"Created folder: '{new_folder.display_name}' (id: {new_folder.id})")
43+
44+
# -- Step 2: create a child subfolder --
45+
sub_folder = new_folder.child_folders.add("Subfolder").execute_query()
46+
print(f"Created subfolder: '{sub_folder.display_name}'")
47+
48+
# -- Step 3: navigate the tree --
49+
print("\nMailbox folder tree:")
50+
walk_folders(client)
51+
print()
52+
53+
# -- Step 4: mark all items as read in a folder --
54+
# Move one message into the folder first so we have something
55+
latest = client.me.mail_folders["inbox"].messages.top(1).get().execute_query()
56+
if latest:
57+
latest[0].move(new_folder.id).execute_query()
58+
print("Moved 1 message into folder (to simulate unread).")
59+
60+
new_folder.mark_all_items_as_read().execute_query()
61+
print(f"✓ Marked all items as read in '{new_folder.display_name}'.")
62+
63+
# -- Step 5: empty the folder (delete all items) --
64+
new_folder.empty(delete_sub_folders=True).execute_query()
65+
print(f"✓ Emptied '{new_folder.display_name}' (including subfolders).")
66+
67+
# -- Step 6: permanent delete removes the folder itself --
68+
# Re-fetch the folder reference
69+
target = client.me.mail_folders[new_folder.id]
70+
target.permanent_delete().execute_query()
71+
print("✓ Permanent-deleted the folder.")
72+
73+
# Verify it's gone
74+
try:
75+
client.me.mail_folders[new_folder.id].get().execute_query()
76+
except Exception:
77+
print(" Confirmed: folder no longer exists.")
78+
79+
80+
if __name__ == "__main__":
81+
main()
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
"""
2+
Message lifecycle: reply, forward, move, and copy messages.
3+
4+
Covers the most common post-receive operations:
5+
- Reply to sender (with modified body)
6+
- Forward to another recipient
7+
- Move to a different folder
8+
- Copy to a different folder
9+
10+
The pattern: create a draft → optionally edit → save or send.
11+
12+
Requires delegated permission ``Mail.ReadWrite`` and ``Mail.Send``.
13+
14+
https://learn.microsoft.com/en-us/graph/api/message-createreply
15+
https://learn.microsoft.com/en-us/graph/api/message-forward
16+
https://learn.microsoft.com/en-us/graph/api/message-move
17+
https://learn.microsoft.com/en-us/graph/api/message-copy
18+
"""
19+
20+
import sys
21+
22+
from office365.graph_client import GraphClient
23+
from tests import test_client_id, test_password, test_tenant, test_username
24+
25+
26+
def get_latest_inbox_message(client: GraphClient):
27+
"""Return the most recent message in the inbox, or None."""
28+
messages = client.me.mail_folders["inbox"].messages.top(1).get().execute_query()
29+
return messages[0] if len(messages) > 0 else None
30+
31+
32+
def main():
33+
client = GraphClient(tenant=test_tenant).with_username_and_password(test_client_id, test_username, test_password)
34+
35+
# -- Step 1: pick a message to work with --
36+
message = get_latest_inbox_message(client)
37+
if message is None:
38+
sys.exit("No messages in inbox.")
39+
40+
# Load subject and body for display
41+
message.select(["subject", "body"]).execute_query()
42+
print(f"Source message: \"{message.subject}\"\n")
43+
44+
# -- Step 2: reply --
45+
draft = message.create_reply().execute_query()
46+
# Draft body is auto-populated; append a line
47+
body = draft.body.get_property("content", "")
48+
draft.set_property("body", {"contentType": "Text", "content": body + "\n\n---\nSent via Microsoft Graph SDK"})
49+
draft.update().execute_query()
50+
draft.send().execute_query()
51+
print("✓ Reply sent (with appended signature).")
52+
53+
# -- Step 3: forward --
54+
forward_to = "meganb@contoso.onmicrosoft.com"
55+
message.forward(to_recipients=[forward_to], comment="FYI — see below.").execute_query()
56+
print(f"✓ Forwarded to {forward_to}.")
57+
58+
# -- Step 4: move to a subfolder --
59+
# Ensure a subfolder exists
60+
inbox = client.me.mail_folders["inbox"]
61+
child_folders = inbox.child_folders.get().execute_query()
62+
archive_folder = next((f for f in child_folders if f.display_name == "Archive"), None)
63+
64+
if archive_folder is None:
65+
archive_folder = inbox.child_folders.add("Archive").execute_query()
66+
print(f" (Created 'Archive' folder)")
67+
68+
moved = message.move(archive_folder.id).execute_query()
69+
print(f"✓ Moved to '{archive_folder.display_name}' (new id: {moved.id})")
70+
71+
# -- Step 5: copy back to inbox --
72+
inbox = client.me.mail_folders["inbox"]
73+
# Refresh the message reference from its new location
74+
copied = client.me.messages[moved.id].copy(inbox.id).execute_query()
75+
print(f"✓ Copied back to Inbox.")
76+
77+
78+
if __name__ == "__main__":
79+
main()

examples/teams/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ patterns.
1717
| `Chat.ReadWrite` (delegated) | Create and message in chats | [Microsoft Graph permissions](https://learn.microsoft.com/en-us/graph/permissions-reference#teams-permissions) |
1818
| `TeamSettings.ReadWrite.All` (application) | Audit and remediate team settings | [Microsoft Graph permissions](https://learn.microsoft.com/en-us/graph/permissions-reference#teams-permissions) |
1919
| `Directory.Read.All` (application) | Enumerate tenant-wide groups and deleted items | [Microsoft Graph permissions](https://learn.microsoft.com/en-us/graph/permissions-reference#directory-permissions) |
20+
| `TeamworkTag.ReadWrite.All` (delegated) | Create and manage tags | [Microsoft Graph permissions](https://learn.microsoft.com/en-us/graph/permissions-reference#teams-permissions) |
21+
| `ChannelMember.ReadWrite.All` (delegated/application) | Manage shared channel members | [Microsoft Graph permissions](https://learn.microsoft.com/en-us/graph/permissions-reference#teams-permissions) |
22+
| `TeamworkTag.Read.All` (application) | Read tags across all teams | [Microsoft Graph permissions](https://learn.microsoft.com/en-us/graph/permissions-reference#teams-permissions) |
2023

2124
Admin consent is required for all permissions above.
2225

@@ -68,6 +71,11 @@ conversations not tied to any team/channel.
6871
| **Usage report** — team counts and user activity over D7/D30/D90 | [`reports/usage.py`](./reports/usage.py) | Adoption tracking, chargeback, and inactivity detection across multiple time windows |
6972
| **Inactive channels** — channels with no recent messages across all teams | [`find_inactive_channels.py`](./find_inactive_channels.py) | Identify channel sprawl and candidates for archiving |
7073
| **App inventory** — report installed apps across all teams | [`apps/report.py`](./apps/report.py) | Detect shadow IT and track app adoption across the tenant |
74+
| **Create tag** — create a tag and assign members in a team | [`tags/create_and_assign.py`](./tags/create_and_assign.py) | Provision @mention groups for routing and alerts. Resolves users by email then creates tag with members |
75+
| **Tag inventory** — report all tags across every team with member count | [`tags/report.py`](./tags/report.py) | Shows which teams use tags, identifies untagged teams and stale tags across the tenant |
76+
| **Clone team** — clone channels, apps, tabs, settings, and/or members | [`clone_team.py`](./clone_team.py) | Bootstrap new projects from a template team. Uses `ClonableTeamParts` bitset + async polling with progress |
77+
| **Shared channel** — create shared channel, share with other team, manage access | [`channels/shared.py`](./channels/shared.py) | Cross-team collaboration via shared channels. Covers creation, sharing, allowed members, and access check |
78+
| **Archive lifecycle** — find inactive teams, archive with audit mode, unarchive | [`archive_lifecycle.py`](./archive_lifecycle.py) | Compliance workflow: scan teams for inactivity, archive candidates, with ready-to-paste unarchive snippet |
7179

7280
---
7381
---

0 commit comments

Comments
 (0)