Skip to content

Instantly share code, notes, and snippets.

@nsdevaraj
Created June 29, 2025 08:14
Show Gist options
  • Save nsdevaraj/b58b2b9f93fd602b9d655268af4492df to your computer and use it in GitHub Desktop.
Save nsdevaraj/b58b2b9f93fd602b9d655268af4492df to your computer and use it in GitHub Desktop.
Teams addon
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel, EmailStr
from typing import List, Optional
import httpx
import os
from dotenv import load_dotenv
import sqlite3
from datetime import datetime
import json
load_dotenv()
app = FastAPI(title="Teams Email Bridge API", version="1.0.0")
app.add_middleware(
CORSMiddleware,
allow_origins=["https://*.teams.microsoft.com", "https://teams.microsoft.com"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
security = HTTPBearer()
def init_db():
conn = sqlite3.connect('teams_email.db')
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
email TEXT NOT NULL,
access_token TEXT,
refresh_token TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS email_templates (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT,
name TEXT NOT NULL,
subject TEXT,
body TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
cursor.execute('''
CREATE TABLE IF NOT EXISTS sent_emails (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT,
recipient TEXT,
subject TEXT,
sent_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
teams_context TEXT,
FOREIGN KEY (user_id) REFERENCES users (id)
)
''')
conn.commit()
conn.close()
init_db()
# Pydantic models
class EmailRequest(BaseModel):
to: List[EmailStr]
subject: str
body: str
teams_context: Optional[str] = None
template_id: Optional[int] = None
class Template(BaseModel):
name: str
subject: str
body: str
class TeamsContext(BaseModel):
chat_snippet: Optional[str] = None
meeting_participants: Optional[List[str]] = None
meeting_id: Optional[str] = None
# Microsoft Graph API integration
class GraphAPIClient:
def __init__(self, access_token: str):
self.access_token = access_token
self.base_url = "https://graph.microsoft.com/v1.0"
self.headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json"
}
async def send_email(self, email_data: EmailRequest):
"""Send email via Microsoft Graph API"""
message = {
"message": {
"subject": email_data.subject,
"body": {
"contentType": "HTML",
"content": email_data.body
},
"toRecipients": [
{"emailAddress": {"address": recipient}}
for recipient in email_data.to
]
}
}
async with httpx.AsyncClient() as client:
response = await client.post(
f"{self.base_url}/me/sendMail",
headers=self.headers,
json=message
)
if response.status_code != 202:
raise HTTPException(
status_code=response.status_code,
detail=f"Failed to send email: {response.text}"
)
return {"status": "sent", "message_id": response.headers.get("request-id")}
async def get_contacts(self, limit: int = 50):
"""Get user's Outlook contacts"""
async with httpx.AsyncClient() as client:
response = await client.get(
f"{self.base_url}/me/contacts?$top={limit}&$select=displayName,emailAddresses",
headers=self.headers
)
if response.status_code == 200:
return response.json()["value"]
else:
raise HTTPException(
status_code=response.status_code,
detail="Failed to fetch contacts"
)
# Authentication helpers
async def verify_teams_token(credentials: HTTPAuthorizationCredentials = Depends(security)):
"""Verify Microsoft Teams/Graph API token"""
token = credentials.credentials
async with httpx.AsyncClient() as client:
response = await client.get(
"https://graph.microsoft.com/v1.0/me",
headers={"Authorization": f"Bearer {token}"}
)
if response.status_code != 200:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid or expired token"
)
user_info = response.json()
return {
"user_id": user_info["id"],
"email": user_info["userPrincipalName"],
"token": token
}
def get_db_connection():
return sqlite3.connect('teams_email.db')
# API Routes
@app.get("/")
async def root():
return {"message": "Teams Email Bridge API", "version": "1.0.0"}
@app.get("/health")
async def health_check():
return {"status": "healthy", "timestamp": datetime.utcnow().isoformat()}
@app.post("/api/v1/send-email")
async def send_email(
email_request: EmailRequest,
current_user = Depends(verify_teams_token)
):
"""Send email with optional Teams context"""
try:
graph_client = GraphAPIClient(current_user["token"])
result = await graph_client.send_email(email_request)
# Log sent email
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(
"INSERT INTO sent_emails (user_id, recipient, subject, teams_context) VALUES (?, ?, ?, ?)",
(
current_user["user_id"],
", ".join(email_request.to),
email_request.subject,
email_request.teams_context
)
)
conn.commit()
conn.close()
return result
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
@app.get("/api/v1/contacts")
async def get_contacts(
limit: int = 50,
current_user = Depends(verify_teams_token)
):
"""Get user's Outlook contacts"""
graph_client = GraphAPIClient(current_user["token"])
return await graph_client.get_contacts(limit)
@app.get("/api/v1/templates")
async def get_templates(current_user = Depends(verify_teams_token)):
"""Get user's email templates"""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(
"SELECT id, name, subject, body FROM email_templates WHERE user_id = ?",
(current_user["user_id"],)
)
templates = []
for row in cursor.fetchall():
templates.append({
"id": row[0],
"name": row[1],
"subject": row[2],
"body": row[3]
})
conn.close()
return {"templates": templates}
@app.post("/api/v1/templates")
async def create_template(
template: Template,
current_user = Depends(verify_teams_token)
):
"""Create new email template"""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(
"INSERT INTO email_templates (user_id, name, subject, body) VALUES (?, ?, ?, ?)",
(current_user["user_id"], template.name, template.subject, template.body)
)
template_id = cursor.lastrowid
conn.commit()
conn.close()
return {"id": template_id, "message": "Template created successfully"}
@app.get("/api/v1/sent-emails")
async def get_sent_emails(
limit: int = 20,
current_user = Depends(verify_teams_token)
):
"""Get user's sent email history"""
conn = get_db_connection()
cursor = conn.cursor()
cursor.execute(
"SELECT recipient, subject, sent_at, teams_context FROM sent_emails WHERE user_id = ? ORDER BY sent_at DESC LIMIT ?",
(current_user["user_id"], limit)
)
emails = []
for row in cursor.fetchall():
emails.append({
"recipient": row[0],
"subject": row[1],
"sent_at": row[2],
"teams_context": row[3]
})
conn.close()
return {"sent_emails": emails}
@app.post("/api/v1/teams-context")
async def process_teams_context(
context: TeamsContext,
current_user = Depends(verify_teams_token)
):
"""Process and format Teams context for email"""
formatted_context = ""
if context.chat_snippet:
formatted_context += f"From our Teams discussion:\n{context.chat_snippet}\n\n"
if context.meeting_participants:
participants = ", ".join(context.meeting_participants)
formatted_context += f"Meeting participants: {participants}\n\n"
return {
"formatted_context": formatted_context,
"suggested_subject": "Follow-up from our Teams discussion"
}
# Default email templates
DEFAULT_TEMPLATES = [
{
"name": "Meeting Follow-up",
"subject": "Follow-up from our Teams meeting",
"body": "Hi,<br><br>Thanks for the productive discussion in our Teams meeting. Here are the key points and next steps:<br><br>[ACTION_ITEMS]<br><br>Please let me know if I missed anything or if you have questions.<br><br>Best regards"
},
{
"name": "Quick Introduction",
"subject": "Introduction from Teams",
"body": "Hi,<br><br>Following up from our Teams discussion, I wanted to introduce you to [NAME] who can help with [TOPIC].<br><br>[CONTEXT]<br><br>Looking forward to connecting you both.<br><br>Best regards"
},
{
"name": "Action Items",
"subject": "Action items from our Teams discussion",
"body": "Hi,<br><br>Here are the action items we discussed:<br><br>• [ITEM_1]<br>• [ITEM_2]<br>• [ITEM_3]<br><br>Timeline: [DEADLINE]<br><br>Let me know if you need any clarification.<br><br>Best regards"
}
]
@app.post("/api/v1/setup-defaults")
async def setup_default_templates(current_user = Depends(verify_teams_token)):
"""Set up default templates for new users"""
conn = get_db_connection()
cursor = conn.cursor()
for template in DEFAULT_TEMPLATES:
cursor.execute(
"INSERT OR IGNORE INTO email_templates (user_id, name, subject, body) VALUES (?, ?, ?, ?)",
(current_user["user_id"], template["name"], template["subject"], template["body"])
)
conn.commit()
conn.close()
return {"message": "Default templates created successfully"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)
fastapi==0.104.1
uvicorn==0.24.0
python-multipart==0.0.6
pydantic==2.5.0
sqlalchemy==2.0.23
databases==0.8.0
aiosqlite==0.19.0
httpx==0.25.2
python-jose[cryptography]==3.3.0
python-dotenv==1.0.0
┌────────────────────────────────────┐
│ Teams Email Bridge [?] [Settings]│
├─────────────────────────────────────┤
│ 📧 Quick Compose │
│ 📋 Templates │
│ 👥 Recent Contacts │
│ 📝 Sent History │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ 📧 Send Email [X]│
├─────────────────────────────────────┤
│ To: [Contact Search/Auto-complete] │
│ Subject: [Smart suggestions] │
├─────────────────────────────────────┤
│ ✨ Include Teams Context │
│ ☐ Chat snippet ☐ Participants │
├─────────────────────────────────────┤
│ [Rich Text Editor] │
│ │
│ [Template ▼] [Send Later ▼] │
│ │
│ [Cancel] [Send Email] │
└─────────────────────────────────────┘
┌─────────────────────────────────────┐
│ Choose Template [X]│
├─────────────────────────────────────┤
│ 🔄 Meeting Follow-up │
│ 👋 Quick Introduction │
│ ✅ Action Items │
│ ➕ Create Custom Template │
├─────────────────────────────────────┤
│ Preview: │
│ [Template preview with variables] │
│ │
│ [Cancel] [Use Template] │
└─────────────────────────────────────┘
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment