🏠 Ana Sayfa 📖 Sözlük 💬 Doküman asistanı
Ana sayfaAI ile GeliştirmeMicrosoft Graph + Outlook Ajan

📧Microsoft Graph + Outlook — Ajan Geliştirme Rehberi

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çTercihSebep
Tek kullanıcı, manuel tetikleyici, M365 UI içindeNative Outlook CopilotSıfır kod, yerleşik güvenlik, Purview kapsamı içinde
Kuruluş genelinde standart aksiyonlar (örn. tüm RFP'ler)Copilot Studio agentLow-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

  1. Entra ID → App registrations → New registration
  2. Ad: komtas-outlook-agent-{env} (dev/prod ayrı)
  3. Supported account types: Single tenant (Komtaş)
  4. API permissions → Microsoft Graph → ihtiyaca göre seç:
YetenekDelegated permissionApplication permission
E-posta okumaMail.ReadMail.Read (admin onayı)
Taslak oluşturmaMail.ReadWriteMail.ReadWrite
E-posta göndermeMail.SendMail.Send (yüksek risk)
Takvim okumaCalendars.ReadCalendars.Read
Toplantı oluşturmaCalendars.ReadWriteCalendars.ReadWrite
Webhook subscriptionSubscription.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 PolicyYalnızca onaylı mailbox'lar için Mail.Read App izni
Conditional AccessServis 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 maskelemePresidio: TC kimlik, IBAN, kart no Claude'a gitmeden maskele
Audit logHer Graph çağrısı + her Claude çağrısı Langfuse trace'ine bağlanır
Webhook clientState doğrulamaHer webhook event'inde clientState eşleşmeli
HITL gateMail.Send aksiyonu otomatik tetiklenmez; insan onayı arkasına alınır
Token rotasyonuClient 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