Ana sayfa ›
AI ile Geliştirme ›
Microsoft Graph + Outlook Ajan
📧Microsoft Graph + Outlook — Ajan Geliştirme Rehberi
📑 Bu sayfada
- G.1 Native Copilot vs. Custom Graph Ajan — Karar
- G.2 Azure App Registration & İzin Modeli
- G.3 OAuth Akışları (Delegated vs. Application)
- G.4 Outlook E-posta — Okuma & Taslak Oluşturma
- G.5 Calendar — Toplantı Bulma & Davet Gönderme
- G.6 Webhook Subscriptions (Yeni e-posta tetikleyici)
- G.7 Claude Tool-Use ile Outlook Ajanı (Tam Örnek)
- G.8 HITL: Gönderim Öncesi İnsan Onayı
- G.9 Güvenlik & Uyumluluk Kontrolleri
Microsoft 365 Copilot (s18) kullanıcı arayüzünden çalışır. Bu sayfa ise arka planda otonom çalışan, Microsoft Graph API ile e-posta okuyan, taslak hazırlayan, takvim yöneten ajanlar için rehber. Tipik kullanım: müşteri sorgularını sınıflandırma, önemli e-postaları öncelikleme, toplantı önerme, takip e-postası taslağı. Tüm gönderim aksiyonları HITL arkasında kalır.
💡 Bu sayfa neyi tamamlıyor?
s18 (M365 Copilot) "kullanıcının kendi Outlook'unda ne yapabileceğini" anlatır. Bu sayfa ise "Outlook verisini kendi ajanından nasıl programlı kullanırım" sorusunu cevaplar.
G.1 Native Copilot vs. Custom Graph Ajan — Karar Ağacı
| İhtiyaç | Tercih | Sebep |
|---|---|---|
| Tek kullanıcı, manuel tetikleyici, M365 UI içinde | Native Outlook Copilot | Sıfır kod, yerleşik güvenlik, Purview kapsamı içinde |
| Kuruluş genelinde standart aksiyonlar (örn. tüm RFP'ler) | Copilot Studio agent | Low-code, M365 + Power Platform entegrasyonu |
| Karmaşık çok adımlı tool-use, kendi LLM'in (Claude/GPT) | Graph API + Custom (bu sayfa) | Tam kontrol, çoklu sağlayıcı, n8n veya kendi runtime |
| Sadece bilgi sorgusu (Outlook'ta arama) | Microsoft 365 Chat (BizChat) | Hazır arama, sıfır geliştirme |
G.2 Azure App Registration & İzin Modeli
- Entra ID → App registrations → New registration
- Ad:
komtas-outlook-agent-{env}(dev/prod ayrı) - Supported account types: Single tenant (Komtaş)
- API permissions → Microsoft Graph → ihtiyaca göre seç:
| Yetenek | Delegated permission | Application permission |
|---|---|---|
| E-posta okuma | Mail.Read | Mail.Read (admin onayı) |
| Taslak oluşturma | Mail.ReadWrite | Mail.ReadWrite |
| E-posta gönderme | Mail.Send | Mail.Send (yüksek risk) |
| Takvim okuma | Calendars.Read | Calendars.Read |
| Toplantı oluşturma | Calendars.ReadWrite | Calendars.ReadWrite |
| Webhook subscription | — | Subscription.ReadWrite.All |
⚠️ En az ayrıcalık ilkesi
Application permission kullanıyorsanız ajan TÜM kullanıcıların kutusuna erişebilir. Bunu sınırlamak için RBAC for Applications (ApplicationAccessPolicy) ile yalnızca belirli mailbox'lara izin verin. Bu adım atlanırsa "Mail.Read App" izni kuruluş çapında sızıntı riskidir.
G.3 OAuth Akışları (Delegated vs. Application)
A) Application (Daemon) — Arka plan ajanı için
import os, msal, httpx
TENANT = os.environ["AZURE_TENANT_ID"]
CLIENT_ID = os.environ["AZURE_CLIENT_ID"]
CLIENT_SECRET = os.environ["AZURE_CLIENT_SECRET"]
app = msal.ConfidentialClientApplication(
client_id=CLIENT_ID,
client_credential=CLIENT_SECRET,
authority=f"https://login.microsoftonline.com/{TENANT}",
)
def graph_token() -> str:
result = app.acquire_token_for_client(
scopes=["https://graph.microsoft.com/.default"]
)
if "access_token" not in result:
raise RuntimeError(result.get("error_description"))
return result["access_token"]
async def graph_get(path: str):
async with httpx.AsyncClient() as h:
r = await h.get(
f"https://graph.microsoft.com/v1.0{path}",
headers={"Authorization": f"Bearer {graph_token()}"},
)
r.raise_for_status()
return r.json()
B) Delegated (Kullanıcı adına) — Auth Code Flow + Refresh Token
Kullanıcı her seferinde tarayıcıdan giriş yapmak zorunda kalmasın diye refresh token'ı şifreli olarak (Azure Key Vault) saklayın. Token süresi dolduğunda otomatik yenileyin.
G.4 Outlook E-posta — Okuma & Taslak Oluşturma
# Belirli kullanıcının son 24 saatlik gelen kutusu
async def list_recent_emails(user_email: str, hours: int = 24):
since = (datetime.utcnow() - timedelta(hours=hours)).isoformat() + "Z"
return await graph_get(
f"/users/{user_email}/messages?"
f"$filter=receivedDateTime ge {since}&"
f"$select=id,subject,from,receivedDateTime,bodyPreview,importance&"
f"$orderby=receivedDateTime desc&$top=50"
)
# E-posta gövdesini aç (HTML)
async def get_email_body(user_email: str, msg_id: str):
return await graph_get(f"/users/{user_email}/messages/{msg_id}")
# Taslak oluştur (HENÜZ GÖNDERME)
async def create_draft(user_email: str, to: str, subject: str, body_html: str):
async with httpx.AsyncClient() as h:
r = await h.post(
f"https://graph.microsoft.com/v1.0/users/{user_email}/messages",
headers={"Authorization": f"Bearer {graph_token()}",
"Content-Type": "application/json"},
json={
"subject": subject,
"body": {"contentType": "HTML", "content": body_html},
"toRecipients": [{"emailAddress": {"address": to}}],
},
)
r.raise_for_status()
return r.json() # Drafts klasöründe oluşur — kullanıcı görür ve onaylar
G.5 Calendar — Toplantı Bulma & Davet Gönderme
# Birden fazla kişi için ortak müsait zaman
async def find_meeting_times(organizer: str, attendees: list[str],
duration_min: int = 30):
body = {
"attendees": [{"emailAddress": {"address": a}} for a in attendees],
"timeConstraint": {
"timeslots": [{
"start": {"dateTime": (datetime.utcnow()).isoformat(),
"timeZone": "Europe/Istanbul"},
"end": {"dateTime": (datetime.utcnow() + timedelta(days=7)).isoformat(),
"timeZone": "Europe/Istanbul"},
}],
},
"meetingDuration": f"PT{duration_min}M",
"maxCandidates": 5,
}
async with httpx.AsyncClient() as h:
r = await h.post(
f"https://graph.microsoft.com/v1.0/users/{organizer}/findMeetingTimes",
headers={"Authorization": f"Bearer {graph_token()}",
"Content-Type": "application/json"},
json=body,
)
return r.json()
G.6 Webhook Subscriptions (Yeni e-posta tetikleyici)
Ajanı reaktif kılmak için: yeni e-posta geldiğinde Microsoft Graph webhook'unuzu çağırır.
# Subscription oluştur (her 70 saatte bir yenilenmeli)
async def create_subscription(user_email: str, notification_url: str):
expiration = (datetime.utcnow() + timedelta(hours=70)).isoformat() + "Z"
body = {
"changeType": "created",
"notificationUrl": notification_url,
"resource": f"/users/{user_email}/mailFolders('Inbox')/messages",
"expirationDateTime": expiration,
"clientState": os.environ["GRAPH_CLIENT_STATE"], # imza doğrulama için
"lifecycleNotificationUrl": notification_url + "/lifecycle",
}
async with httpx.AsyncClient() as h:
r = await h.post(
"https://graph.microsoft.com/v1.0/subscriptions",
headers={"Authorization": f"Bearer {graph_token()}"},
json=body,
)
return r.json()
⚠️ Subscription bakımı
Mailbox subscription'ları en fazla 4230 dakika (≈ 70 saat) yaşar. Cron/Celery ile 50. saatte yenileme kuralı koyun. Yenilenmezse ajan sessizce reaktif olmayı bırakır — alarm kurun.
G.7 Claude Tool-Use ile Outlook Ajanı (Tam Örnek)
from anthropic import Anthropic
claude = Anthropic()
OUTLOOK_TOOLS = [
{"name": "list_recent_emails",
"description": "Son N saatteki gelen e-postaları döner.",
"input_schema": {"type": "object", "properties": {
"hours": {"type": "integer", "minimum": 1, "maximum": 168}},
"required": ["hours"]}},
{"name": "get_email_body",
"description": "Tek bir e-postanın tam gövdesini açar.",
"input_schema": {"type": "object", "properties": {
"message_id": {"type": "string"}}, "required": ["message_id"]}},
{"name": "create_draft",
"description": "Outlook Drafts klasörüne taslak yazar. Gönderim yapmaz; gönderim insan onayına bırakılır.",
"input_schema": {"type": "object", "properties": {
"to": {"type": "string"},
"subject": {"type": "string"},
"body_html": {"type": "string"}},
"required": ["to", "subject", "body_html"]}},
{"name": "find_meeting_times",
"description": "Katılımcılar için 7 gün içinde 5 müsait zaman önerir.",
"input_schema": {"type": "object", "properties": {
"attendees": {"type": "array", "items": {"type": "string"}},
"duration_min": {"type": "integer"}},
"required": ["attendees"]}},
]
SYSTEM = """Sen Komtaş İş Geliştirme ekibi için Outlook asistanısın.
Kuralların:
1. E-posta gönderme adımını atlama — yalnızca taslak oluştur, gönderimi kullanıcı onayına bırak.
2. KVKK kapsamı: müşteri kişisel bilgilerini cevap içinde tekrarlama.
3. Türkçe profesyonel ton, maks. 5 paragraf.
4. Toplantı önerirken 09:30-17:30 arası, mola sürelerine dikkat et."""
def outlook_agent(user_query: str, max_iters: int = 6):
messages = [{"role": "user", "content": user_query}]
for _ in range(max_iters):
resp = claude.messages.create(
model="claude-sonnet-4-6",
max_tokens=2048,
system=SYSTEM,
tools=OUTLOOK_TOOLS,
messages=messages,
)
if resp.stop_reason != "tool_use":
return resp.content[0].text
# Tool'u çalıştır + sonucu mesaj geçmişine ekle
messages.append({"role": "assistant", "content": resp.content})
results = []
for block in resp.content:
if block.type == "tool_use":
output = run_outlook_tool(block.name, block.input)
results.append({"type": "tool_result",
"tool_use_id": block.id,
"content": json.dumps(output)})
messages.append({"role": "user", "content": results})
return "Maks. iterasyon aşıldı"
G.8 HITL: Gönderim Öncesi İnsan Onayı
✅ Önerilen Desen — Gönderim insan onayında
Outlook ajanları için tercih edilen yaklaşım: ajan yalnızca taslak (draft) oluşturur, gönderim yetkisi kullanıcının "Gönder" butonunda kalır. Otomatik gönderim ihtiyacı varsa Slack/Teams onay mesajı + buton akışı önerilir.
# Slack üzerinden onay-akışı (Block Kit interaktif buton)
async def request_approval(draft_id: str, summary: str, slack_user_id: str):
blocks = [
{"type": "section", "text": {"type": "mrkdwn",
"text": f"*Onay bekleyen taslak*\n{summary}"}},
{"type": "actions", "elements": [
{"type": "button", "text": {"type": "plain_text", "text": "✅ Gönder"},
"style": "primary", "value": draft_id, "action_id": "approve_draft"},
{"type": "button", "text": {"type": "plain_text", "text": "✏️ Düzenle"},
"value": draft_id, "action_id": "edit_draft"},
{"type": "button", "text": {"type": "plain_text", "text": "❌ İptal"},
"style": "danger", "value": draft_id, "action_id": "reject_draft"},
]},
]
await slack.chat_postMessage(channel=slack_user_id, blocks=blocks)
G.9 Güvenlik & Uyumluluk Kontrolleri
| Kontrol | Önlem |
|---|---|
| Application Access Policy | Yalnızca onaylı mailbox'lar için Mail.Read App izni |
| Conditional Access | Servis principal için IP allowlist + token yaşı kısa |
| Purview etiket farkındalığı | "Confidential" etiketli e-posta gövdesini LLM'e iletmemek; özet ya da metadata kullanmak önerilir |
| PII maskeleme | Presidio: TC kimlik, IBAN, kart no Claude'a gitmeden maskele |
| Audit log | Her Graph çağrısı + her Claude çağrısı Langfuse trace'ine bağlanır |
| Webhook clientState doğrulama | Her webhook event'inde clientState eşleşmeli |
| HITL gate | Mail.Send aksiyonu otomatik tetiklenmez; insan onayı arkasına alınır |
| Token rotasyonu | Client secret yerine certificate auth; 6 ayda bir döndür |
📌 İlgili
M365 Copilot kullanıcı tarafı: Microsoft Copilot (s18) · Veri sınıflandırma: AI Araçları Veri Güvenliği · KVKK: Uyumluluk