feat(microsoft-defender-o365): new O365 email-remediation integration
Microsoft Graph mail APIs, 7 commands: search mailbox messages, get message, list attachments, list folders, move message, hard-delete (purge) message. Azure AD OAuth2 client-credentials auth (Mail.ReadWrite), stdlib-only. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,97 @@
|
|||||||
|
id: microsoft_defender_o365
|
||||||
|
name: Microsoft Defender for Office 365
|
||||||
|
version: 1.0.0
|
||||||
|
description: "Microsoft Defender for Office 365 email remediation (Microsoft Graph mail APIs) — anti-phishing containment: search a mailbox for messages, read a message and its attachments, move a message to a folder, and hard-delete (purge) a phishing message. Azure AD OAuth2 client-credentials authentication; stdlib-only, no extra Python dependencies."
|
||||||
|
changelog: "1.0.0 — Initial release: search mailbox messages, get message, list attachments, list folders, move message, delete (purge) message."
|
||||||
|
category: email
|
||||||
|
|
||||||
|
# Per-instance configuration. Uses application (client-credentials) OAuth2.
|
||||||
|
# The app registration needs Graph application permission Mail.ReadWrite.
|
||||||
|
config_schema:
|
||||||
|
properties:
|
||||||
|
tenant_id:
|
||||||
|
type: string
|
||||||
|
description: "Azure AD tenant ID"
|
||||||
|
client_id:
|
||||||
|
type: string
|
||||||
|
description: "App registration (client) ID"
|
||||||
|
client_secret:
|
||||||
|
type: string
|
||||||
|
description: "App registration client secret"
|
||||||
|
x-soar-sensitive: true
|
||||||
|
required:
|
||||||
|
- tenant_id
|
||||||
|
- client_id
|
||||||
|
- client_secret
|
||||||
|
|
||||||
|
commands:
|
||||||
|
- id: search_messages
|
||||||
|
name: mdo-search-messages
|
||||||
|
description: "Search a mailbox for messages by sender, subject, or free-text search."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
mailbox: { type: string, description: "Target mailbox (user principal name or ID)" }
|
||||||
|
subject: { type: string, description: "Filter by exact subject" }
|
||||||
|
from_address: { type: string, description: "Filter by sender address" }
|
||||||
|
search: { type: string, description: "Free-text $search query (KQL) over the mailbox" }
|
||||||
|
limit: { type: number, description: "Max messages (default 25)" }
|
||||||
|
required: [mailbox]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: get_message
|
||||||
|
name: mdo-get-message
|
||||||
|
description: "Get a single message by ID."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
mailbox: { type: string, description: "Target mailbox (UPN or ID)" }
|
||||||
|
message_id: { type: string, description: "Message ID" }
|
||||||
|
required: [mailbox, message_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: list_attachments
|
||||||
|
name: mdo-list-attachments
|
||||||
|
description: "List a message's attachments."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
mailbox: { type: string, description: "Target mailbox (UPN or ID)" }
|
||||||
|
message_id: { type: string, description: "Message ID" }
|
||||||
|
required: [mailbox, message_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: list_folders
|
||||||
|
name: mdo-list-folders
|
||||||
|
description: "List the mail folders of a mailbox (to obtain destination IDs for move)."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
mailbox: { type: string, description: "Target mailbox (UPN or ID)" }
|
||||||
|
required: [mailbox]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: move_message
|
||||||
|
name: mdo-move-message
|
||||||
|
description: "Move a message to another folder (e.g. to Junk Email or a quarantine folder)."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
mailbox: { type: string, description: "Target mailbox (UPN or ID)" }
|
||||||
|
message_id: { type: string, description: "Message ID" }
|
||||||
|
destination_id: { type: string, description: "Destination folder ID or well-known name (e.g. junkemail, deleteditems)" }
|
||||||
|
required: [mailbox, message_id, destination_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
- id: delete_message
|
||||||
|
name: mdo-delete-message
|
||||||
|
description: "Hard-delete (purge) a message from the mailbox."
|
||||||
|
inputs_schema:
|
||||||
|
properties:
|
||||||
|
mailbox: { type: string, description: "Target mailbox (UPN or ID)" }
|
||||||
|
message_id: { type: string, description: "Message ID" }
|
||||||
|
required: [mailbox, message_id]
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
|
|
||||||
|
- id: test_connection
|
||||||
|
name: mdo-test-connection
|
||||||
|
description: "Verify connectivity and the app credentials (used by the Test button)."
|
||||||
|
risk: read
|
||||||
|
inputs_schema:
|
||||||
|
properties: {}
|
||||||
|
required: []
|
||||||
|
outputs_schema: { properties: {} }
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
GRAPH = "https://graph.microsoft.com/v1.0"
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _token(cfg):
|
||||||
|
tenant = str(cfg.get("tenant_id", ""))
|
||||||
|
url = "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token"
|
||||||
|
form = urllib.parse.urlencode({
|
||||||
|
"grant_type": "client_credentials",
|
||||||
|
"client_id": str(cfg.get("client_id", "")),
|
||||||
|
"client_secret": str(cfg.get("client_secret", "")),
|
||||||
|
"scope": "https://graph.microsoft.com/.default",
|
||||||
|
}).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
tok = json.loads(r.read())
|
||||||
|
if not tok.get("access_token"):
|
||||||
|
raise Exception("Token request failed: " + json.dumps(tok))
|
||||||
|
return tok["access_token"]
|
||||||
|
|
||||||
|
|
||||||
|
def graph(method, path, token, body=None, params=None):
|
||||||
|
url = GRAPH + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
token = _token(cfg)
|
||||||
|
print(json.dumps(fn(token, inputs)))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
|
||||||
|
|
||||||
|
def main(token, inputs):
|
||||||
|
mailbox = inputs.get("mailbox")
|
||||||
|
if not mailbox:
|
||||||
|
raise Exception("mailbox is required")
|
||||||
|
message_id = inputs.get("message_id")
|
||||||
|
if not message_id:
|
||||||
|
raise Exception("message_id is required")
|
||||||
|
|
||||||
|
graph("DELETE", "/users/" + q(mailbox) + "/messages/" + q(message_id), token)
|
||||||
|
return {"ok": True, "deleted": message_id}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
GRAPH = "https://graph.microsoft.com/v1.0"
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _token(cfg):
|
||||||
|
tenant = str(cfg.get("tenant_id", ""))
|
||||||
|
url = "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token"
|
||||||
|
form = urllib.parse.urlencode({
|
||||||
|
"grant_type": "client_credentials",
|
||||||
|
"client_id": str(cfg.get("client_id", "")),
|
||||||
|
"client_secret": str(cfg.get("client_secret", "")),
|
||||||
|
"scope": "https://graph.microsoft.com/.default",
|
||||||
|
}).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
tok = json.loads(r.read())
|
||||||
|
if not tok.get("access_token"):
|
||||||
|
raise Exception("Token request failed: " + json.dumps(tok))
|
||||||
|
return tok["access_token"]
|
||||||
|
|
||||||
|
|
||||||
|
def graph(method, path, token, body=None, params=None):
|
||||||
|
url = GRAPH + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
token = _token(cfg)
|
||||||
|
print(json.dumps(fn(token, inputs)))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
|
||||||
|
|
||||||
|
def main(token, inputs):
|
||||||
|
mailbox = inputs.get("mailbox")
|
||||||
|
if not mailbox:
|
||||||
|
raise Exception("mailbox is required")
|
||||||
|
message_id = inputs.get("message_id")
|
||||||
|
if not message_id:
|
||||||
|
raise Exception("message_id is required")
|
||||||
|
|
||||||
|
return graph("GET", "/users/" + q(mailbox) + "/messages/" + q(message_id), token)
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
GRAPH = "https://graph.microsoft.com/v1.0"
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _token(cfg):
|
||||||
|
tenant = str(cfg.get("tenant_id", ""))
|
||||||
|
url = "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token"
|
||||||
|
form = urllib.parse.urlencode({
|
||||||
|
"grant_type": "client_credentials",
|
||||||
|
"client_id": str(cfg.get("client_id", "")),
|
||||||
|
"client_secret": str(cfg.get("client_secret", "")),
|
||||||
|
"scope": "https://graph.microsoft.com/.default",
|
||||||
|
}).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
tok = json.loads(r.read())
|
||||||
|
if not tok.get("access_token"):
|
||||||
|
raise Exception("Token request failed: " + json.dumps(tok))
|
||||||
|
return tok["access_token"]
|
||||||
|
|
||||||
|
|
||||||
|
def graph(method, path, token, body=None, params=None):
|
||||||
|
url = GRAPH + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
token = _token(cfg)
|
||||||
|
print(json.dumps(fn(token, inputs)))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
|
||||||
|
|
||||||
|
def main(token, inputs):
|
||||||
|
mailbox = inputs.get("mailbox")
|
||||||
|
if not mailbox:
|
||||||
|
raise Exception("mailbox is required")
|
||||||
|
message_id = inputs.get("message_id")
|
||||||
|
if not message_id:
|
||||||
|
raise Exception("message_id is required")
|
||||||
|
|
||||||
|
return graph("GET", "/users/" + q(mailbox) + "/messages/" + q(message_id) + "/attachments", token)
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
GRAPH = "https://graph.microsoft.com/v1.0"
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _token(cfg):
|
||||||
|
tenant = str(cfg.get("tenant_id", ""))
|
||||||
|
url = "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token"
|
||||||
|
form = urllib.parse.urlencode({
|
||||||
|
"grant_type": "client_credentials",
|
||||||
|
"client_id": str(cfg.get("client_id", "")),
|
||||||
|
"client_secret": str(cfg.get("client_secret", "")),
|
||||||
|
"scope": "https://graph.microsoft.com/.default",
|
||||||
|
}).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
tok = json.loads(r.read())
|
||||||
|
if not tok.get("access_token"):
|
||||||
|
raise Exception("Token request failed: " + json.dumps(tok))
|
||||||
|
return tok["access_token"]
|
||||||
|
|
||||||
|
|
||||||
|
def graph(method, path, token, body=None, params=None):
|
||||||
|
url = GRAPH + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
token = _token(cfg)
|
||||||
|
print(json.dumps(fn(token, inputs)))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
|
||||||
|
|
||||||
|
def main(token, inputs):
|
||||||
|
mailbox = inputs.get("mailbox")
|
||||||
|
if not mailbox:
|
||||||
|
raise Exception("mailbox is required")
|
||||||
|
|
||||||
|
return graph("GET", "/users/" + q(mailbox) + "/mailFolders", token, params={"$top": 100})
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
GRAPH = "https://graph.microsoft.com/v1.0"
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _token(cfg):
|
||||||
|
tenant = str(cfg.get("tenant_id", ""))
|
||||||
|
url = "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token"
|
||||||
|
form = urllib.parse.urlencode({
|
||||||
|
"grant_type": "client_credentials",
|
||||||
|
"client_id": str(cfg.get("client_id", "")),
|
||||||
|
"client_secret": str(cfg.get("client_secret", "")),
|
||||||
|
"scope": "https://graph.microsoft.com/.default",
|
||||||
|
}).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
tok = json.loads(r.read())
|
||||||
|
if not tok.get("access_token"):
|
||||||
|
raise Exception("Token request failed: " + json.dumps(tok))
|
||||||
|
return tok["access_token"]
|
||||||
|
|
||||||
|
|
||||||
|
def graph(method, path, token, body=None, params=None):
|
||||||
|
url = GRAPH + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
token = _token(cfg)
|
||||||
|
print(json.dumps(fn(token, inputs)))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
|
||||||
|
|
||||||
|
def main(token, inputs):
|
||||||
|
mailbox = inputs.get("mailbox")
|
||||||
|
if not mailbox:
|
||||||
|
raise Exception("mailbox is required")
|
||||||
|
message_id = inputs.get("message_id")
|
||||||
|
if not message_id:
|
||||||
|
raise Exception("message_id is required")
|
||||||
|
destination_id = inputs.get("destination_id")
|
||||||
|
if not destination_id:
|
||||||
|
raise Exception("destination_id is required")
|
||||||
|
|
||||||
|
return graph(
|
||||||
|
"POST",
|
||||||
|
"/users/" + q(mailbox) + "/messages/" + q(message_id) + "/move",
|
||||||
|
token,
|
||||||
|
body={"destinationId": destination_id},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,93 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
GRAPH = "https://graph.microsoft.com/v1.0"
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _token(cfg):
|
||||||
|
tenant = str(cfg.get("tenant_id", ""))
|
||||||
|
url = "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token"
|
||||||
|
form = urllib.parse.urlencode({
|
||||||
|
"grant_type": "client_credentials",
|
||||||
|
"client_id": str(cfg.get("client_id", "")),
|
||||||
|
"client_secret": str(cfg.get("client_secret", "")),
|
||||||
|
"scope": "https://graph.microsoft.com/.default",
|
||||||
|
}).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
tok = json.loads(r.read())
|
||||||
|
if not tok.get("access_token"):
|
||||||
|
raise Exception("Token request failed: " + json.dumps(tok))
|
||||||
|
return tok["access_token"]
|
||||||
|
|
||||||
|
|
||||||
|
def graph(method, path, token, body=None, params=None):
|
||||||
|
url = GRAPH + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
token = _token(cfg)
|
||||||
|
print(json.dumps(fn(token, inputs)))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
q = lambda v: urllib.parse.quote(str(v), safe="")
|
||||||
|
|
||||||
|
|
||||||
|
def _odata_escape(v):
|
||||||
|
return str(v).replace("'", "''")
|
||||||
|
|
||||||
|
|
||||||
|
def main(token, inputs):
|
||||||
|
mailbox = inputs.get("mailbox")
|
||||||
|
if not mailbox:
|
||||||
|
raise Exception("mailbox is required")
|
||||||
|
|
||||||
|
subject = inputs.get("subject")
|
||||||
|
from_address = inputs.get("from_address")
|
||||||
|
search = inputs.get("search")
|
||||||
|
limit = int(inputs.get("limit") or 25)
|
||||||
|
|
||||||
|
if search:
|
||||||
|
params = {"$search": '"' + search + '"', "$top": limit}
|
||||||
|
elif subject or from_address:
|
||||||
|
parts = []
|
||||||
|
if subject:
|
||||||
|
parts.append("subject eq '" + _odata_escape(subject) + "'")
|
||||||
|
if from_address:
|
||||||
|
parts.append("from/emailAddress/address eq '" + _odata_escape(from_address) + "'")
|
||||||
|
params = {"$filter": " and ".join(parts), "$top": limit}
|
||||||
|
else:
|
||||||
|
params = {"$top": limit}
|
||||||
|
|
||||||
|
return graph("GET", "/users/" + q(mailbox) + "/messages", token, params=params)
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import json, os, sys, urllib.parse, urllib.request, urllib.error
|
||||||
|
|
||||||
|
GRAPH = "https://graph.microsoft.com/v1.0"
|
||||||
|
|
||||||
|
|
||||||
|
def _cfg():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_SECRETS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _inputs():
|
||||||
|
return json.loads(os.environ.get("INTEGRATION_INPUTS", "{}"))
|
||||||
|
|
||||||
|
|
||||||
|
def _token(cfg):
|
||||||
|
tenant = str(cfg.get("tenant_id", ""))
|
||||||
|
url = "https://login.microsoftonline.com/" + tenant + "/oauth2/v2.0/token"
|
||||||
|
form = urllib.parse.urlencode({
|
||||||
|
"grant_type": "client_credentials",
|
||||||
|
"client_id": str(cfg.get("client_id", "")),
|
||||||
|
"client_secret": str(cfg.get("client_secret", "")),
|
||||||
|
"scope": "https://graph.microsoft.com/.default",
|
||||||
|
}).encode("utf-8")
|
||||||
|
req = urllib.request.Request(url, data=form, headers={"Content-Type": "application/x-www-form-urlencoded"}, method="POST")
|
||||||
|
with urllib.request.urlopen(req, timeout=60) as r:
|
||||||
|
tok = json.loads(r.read())
|
||||||
|
if not tok.get("access_token"):
|
||||||
|
raise Exception("Token request failed: " + json.dumps(tok))
|
||||||
|
return tok["access_token"]
|
||||||
|
|
||||||
|
|
||||||
|
def graph(method, path, token, body=None, params=None):
|
||||||
|
url = GRAPH + path
|
||||||
|
if params:
|
||||||
|
clean = {k: v for k, v in params.items() if v not in (None, "")}
|
||||||
|
if clean:
|
||||||
|
url += "?" + urllib.parse.urlencode(clean)
|
||||||
|
data = json.dumps(body).encode("utf-8") if body is not None else None
|
||||||
|
headers = {"Authorization": "Bearer " + token, "Accept": "application/json"}
|
||||||
|
if data is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
||||||
|
with urllib.request.urlopen(req, timeout=90) as r:
|
||||||
|
raw = r.read()
|
||||||
|
return json.loads(raw) if raw else {}
|
||||||
|
|
||||||
|
|
||||||
|
def _run(fn):
|
||||||
|
try:
|
||||||
|
cfg = _cfg()
|
||||||
|
inputs = _inputs()
|
||||||
|
token = _token(cfg)
|
||||||
|
print(json.dumps(fn(token, inputs)))
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
print(json.dumps({"error": "HTTP " + str(e.code), "detail": e.read().decode("utf-8", "replace")}))
|
||||||
|
sys.exit(1)
|
||||||
|
except Exception as e:
|
||||||
|
print(json.dumps({"error": str(e)}))
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main(token, inputs):
|
||||||
|
graph("GET", "/users", token, params={"$top": 1})
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
|
_run(main)
|
||||||
Reference in New Issue
Block a user