66from msal import ConfidentialClientApplication
77from datetime import datetime, timezone, timedelta
88
9- # --- Determine mode ---
9+ # --- Environment variables (loaded once) ---
10+ TENANT_ID = os.getenv("TENANT_ID")
11+ CLIENT_ID = os.getenv("CLIENT_ID")
12+ CLIENT_SECRET = os.getenv("CLIENT_SECRET")
13+ FROM_EMAIL = os.getenv("FROM_EMAIL")
14+ TO_EMAIL = os.getenv("TO_EMAIL")
15+ GITHUB_WEBHOOK_SECRET = os.getenv("GITHUB_WEBHOOK_SECRET")
16+
17+ # --- Mode toggle (Test vs Webhook server) ---
1018TEST_MODE = os.getenv("TEST_MODE", "true").lower() == "true"
1119
12- # Import Flask only if not in TEST_MODE
1320if not TEST_MODE:
1421 from flask import Flask, request
1522 app = Flask(__name__)
1623
24+
1725# --- Microsoft Graph Email Sending Function ---
1826def send_email_via_graph(subject, body):
19- TENANT_ID = os.getenv("TENANT_ID")
20- CLIENT_ID = os.getenv("CLIENT_ID")
21- CLIENT_SECRET = os.getenv("CLIENT_SECRET")
22- FROM_EMAIL = os.getenv("FROM_EMAIL")
23- TO_EMAIL = os.getenv("TO_EMAIL")
24-
27+ """Send email using Microsoft Graph API via client credentials flow."""
2528 if not all([TENANT_ID, CLIENT_ID, CLIENT_SECRET, FROM_EMAIL, TO_EMAIL]):
26- print("❌ Missing required environment variables")
29+ print("❌ Missing required environment variables for email sending ")
2730 return
2831
2932 try:
@@ -34,6 +37,7 @@ def send_email_via_graph(subject, body):
3437 )
3538 token = app_msal.acquire_token_for_client(scopes=["https://graph.microsoft.com/.default"])
3639 access_token = token.get("access_token")
40+
3741 if not access_token:
3842 print(f"❌ Failed to get access token: {token}")
3943 return
@@ -60,8 +64,10 @@ def send_email_via_graph(subject, body):
6064 except Exception as e:
6165 print(f"❌ Exception occurred while sending email: {e}")
6266
63- # --- Verify GitHub webhook signature ---
67+
68+ # --- GitHub Webhook Signature Verification ---
6469def verify_github_signature(payload_body, signature, secret):
70+ """Verify X-Hub-Signature-256 against webhook secret."""
6571 if not secret:
6672 print("⚠️ No webhook secret set, skipping verification")
6773 return True
@@ -73,8 +79,10 @@ def verify_github_signature(payload_body, signature, secret):
7379 expected_signature = "sha256=" + mac.hexdigest()
7480 return hmac.compare_digest(expected_signature, signature)
7581
82+
7683# --- Convert UTC timestamp to UTC+4 ---
7784def convert_to_utc4(timestamp):
85+ """Convert ISO UTC timestamp string to UTC+4 formatted datetime string."""
7886 if not timestamp:
7987 return "N/A"
8088 try:
@@ -85,8 +93,10 @@ def convert_to_utc4(timestamp):
8593 print(f"⚠️ Failed to convert timestamp {timestamp}: {e}")
8694 return timestamp
8795
96+
8897# --- Format GitHub repository event for email ---
8998def format_repo_event(repo, action):
99+ """Generate subject + body text for repository created/deleted events."""
90100 repo_name = repo["full_name"]
91101 visibility_icon = "🔒 Private" if repo.get("private") else "🌐 Public"
92102 owner = repo["owner"]["login"]
@@ -108,20 +118,20 @@ def format_repo_event(repo, action):
108118 )
109119 return subject, body
110120
111- # --- GitHub Webhook + Health Handlers ---
121+
122+ # --- Flask Handlers (only if not in TEST_MODE) ---
112123if not TEST_MODE:
113124 @app.route("/webhook", methods=["POST"])
114125 def github_webhook():
115126 payload_body = request.data
116127 signature = request.headers.get("X-Hub-Signature-256")
117- secret = os.getenv("GITHUB_WEBHOOK_SECRET")
118128
119129 print("📥 Incoming GitHub webhook")
120130 print(f"📥 Event: {request.headers.get('X-GitHub-Event')}")
121131 print(f"📥 Signature header: {signature}")
122132
123- if not verify_github_signature(payload_body, signature, secret ):
124- print(f "❌ Invalid signature! Webhook rejected.")
133+ if not verify_github_signature(payload_body, signature, GITHUB_WEBHOOK_SECRET ):
134+ print("❌ Invalid signature! Webhook rejected.")
125135 return "❌ Invalid signature", 401
126136
127137 try:
@@ -131,24 +141,26 @@ def github_webhook():
131141 return "❌ Bad payload", 400
132142
133143 event = request.headers.get("X-GitHub-Event", "")
144+ action = data.get("action", "")
134145
135- if event == "repository" and data.get(" action") in ["created", "deleted"]:
136- subject, body = format_repo_event(data["repository"], data[" action"] )
146+ if event == "repository" and action in ["created", "deleted"]:
147+ subject, body = format_repo_event(data["repository"], action)
137148 print(f"📩 Sending email alert: {subject}")
138149 send_email_via_graph(subject, body)
139150 else:
140- print(f"ℹ️ Ignored event: {event}, action: {data.get(' action') }")
151+ print(f"ℹ️ Ignored event: {event}, action: {action}")
141152
142153 return "OK", 200
143154
144155 @app.route("/health", methods=["GET"])
145156 def health_check():
146157 return {"status": "running"}, 200
147158
159+
148160# --- Main Entry Point ---
149161if __name__ == "__main__":
150162 if TEST_MODE:
151- print("🔹 TEST_MODE: sending test email")
163+ print("🔹 TEST_MODE enabled : sending test email... ")
152164 test_repo = {
153165 "full_name": "quantori/sadsrepo",
154166 "private": True,
0 commit comments