66from msal import ConfidentialClientApplication
77from datetime import datetime , timezone , timedelta
88
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) ---
9+ # --- Determine mode ---
1810TEST_MODE = os .getenv ("TEST_MODE" , "true" ).lower () == "true"
1911
12+ # Import Flask only if not in TEST_MODE
2013if not TEST_MODE :
2114 from flask import Flask , request
2215 app = Flask (__name__ )
2316
24-
2517# --- Microsoft Graph Email Sending Function ---
2618def send_email_via_graph (subject , body ):
27- """Send email using Microsoft Graph API via client credentials flow."""
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+
2825 if not all ([TENANT_ID , CLIENT_ID , CLIENT_SECRET , FROM_EMAIL , TO_EMAIL ]):
29- print ("❌ Missing required environment variables for email sending " )
26+ print ("❌ Missing required environment variables" )
3027 return
3128
3229 try :
@@ -37,7 +34,6 @@ def send_email_via_graph(subject, body):
3734 )
3835 token = app_msal .acquire_token_for_client (scopes = ["https://graph.microsoft.com/.default" ])
3936 access_token = token .get ("access_token" )
40-
4137 if not access_token :
4238 print (f"❌ Failed to get access token: { token } " )
4339 return
@@ -64,10 +60,8 @@ def send_email_via_graph(subject, body):
6460 except Exception as e :
6561 print (f"❌ Exception occurred while sending email: { e } " )
6662
67-
68- # --- GitHub Webhook Signature Verification ---
63+ # --- Verify GitHub webhook signature ---
6964def verify_github_signature (payload_body , signature , secret ):
70- """Verify X-Hub-Signature-256 against webhook secret."""
7165 if not secret :
7266 print ("⚠️ No webhook secret set, skipping verification" )
7367 return True
@@ -79,10 +73,8 @@ def verify_github_signature(payload_body, signature, secret):
7973 expected_signature = "sha256=" + mac .hexdigest ()
8074 return hmac .compare_digest (expected_signature , signature )
8175
82-
8376# --- Convert UTC timestamp to UTC+4 ---
8477def convert_to_utc4 (timestamp ):
85- """Convert ISO UTC timestamp string to UTC+4 formatted datetime string."""
8678 if not timestamp :
8779 return "N/A"
8880 try :
@@ -93,10 +85,8 @@ def convert_to_utc4(timestamp):
9385 print (f"⚠️ Failed to convert timestamp { timestamp } : { e } " )
9486 return timestamp
9587
96-
9788# --- Format GitHub repository event for email ---
9889def format_repo_event (repo , action ):
99- """Generate subject + body text for repository created/deleted events."""
10090 repo_name = repo ["full_name" ]
10191 visibility_icon = "🔒 Private" if repo .get ("private" ) else "🌐 Public"
10292 owner = repo ["owner" ]["login" ]
@@ -118,20 +108,20 @@ def format_repo_event(repo, action):
118108 )
119109 return subject , body
120110
121-
122- # --- Flask Handlers (only if not in TEST_MODE) ---
111+ # --- GitHub Webhook + Health Handlers ---
123112if not TEST_MODE :
124113 @app .route ("/webhook" , methods = ["POST" ])
125114 def github_webhook ():
126115 payload_body = request .data
127116 signature = request .headers .get ("X-Hub-Signature-256" )
117+ secret = os .getenv ("GITHUB_WEBHOOK_SECRET" )
128118
129119 print ("📥 Incoming GitHub webhook" )
130120 print (f"📥 Event: { request .headers .get ('X-GitHub-Event' )} " )
131121 print (f"📥 Signature header: { signature } " )
132122
133- if not verify_github_signature (payload_body , signature , GITHUB_WEBHOOK_SECRET ):
134- print ("❌ Invalid signature! Webhook rejected." )
123+ if not verify_github_signature (payload_body , signature , secret ):
124+ print (f "❌ Invalid signature! Webhook rejected." )
135125 return "❌ Invalid signature" , 401
136126
137127 try :
@@ -143,7 +133,18 @@ def github_webhook():
143133 event = request .headers .get ("X-GitHub-Event" , "" )
144134 action = data .get ("action" , "" )
145135
146- if event == "repository" and action in ["created" , "deleted" ]:
136+ repo_actions = [
137+ "created" ,
138+ "deleted" ,
139+ "publicized" , # private → public
140+ "privatized" , # public → private
141+ "archived" ,
142+ "unarchived" ,
143+ "renamed" ,
144+ "transferred"
145+ ]
146+
147+ if event == "repository" and action in repo_actions :
147148 subject , body = format_repo_event (data ["repository" ], action )
148149 print (f"📩 Sending email alert: { subject } " )
149150 send_email_via_graph (subject , body )
@@ -156,11 +157,10 @@ def github_webhook():
156157 def health_check ():
157158 return {"status" : "running" }, 200
158159
159-
160160# --- Main Entry Point ---
161161if __name__ == "__main__" :
162162 if TEST_MODE :
163- print ("🔹 TEST_MODE enabled : sending test email... " )
163+ print ("🔹 TEST_MODE: sending test email" )
164164 test_repo = {
165165 "full_name" : "quantori/sadsrepo" ,
166166 "private" : True ,
@@ -170,7 +170,7 @@ def health_check():
170170 "updated_at" : "2025-09-09T13:09:20Z" ,
171171 "html_url" : "https://github.com/quantori/sadsrepo"
172172 }
173- subject , body = format_repo_event (test_repo , "created " )
173+ subject , body = format_repo_event (test_repo , "publicized " )
174174 send_email_via_graph (subject , body )
175175 else :
176176 print ("✅ Flask is up and listening on /webhook and /health" )
0 commit comments