Blog
2025-10-28 - Artur Wilczek
AI for coding? Nie ma ucieczki!

AI for coding? Nie ma ucieczki!

GitHub Copilot pisze za mnie klasę do obsługi skrzynki pocztowej MS Exchange

Wstęp

Od pewnego czasu używam GitHub Copilot Pro i modeli takich jak GPT-5, Gemini 2.5 Pro oraz Claude Sonnet 4.5 do testowego tworzenia kodu.

Używam rozszerzenia do VS Code, które daje możliwość pracy w trzech trybach:

  • Ask, jeśli chcesz o coś zapytać bez automatycznej ingerencji w istniejący kod
  • Edit, jeśli chcesz wykonać konkretną, najczęściej niewielką, zmianę w kodzie
  • Agent, jeśli chcesz, żeby Copilot był bardziej samodzielny, analizował istniejący kod i wykonywał większe modyfikacje zgodnie z Twoimi wytycznymi

Domyślnie włączone jest też automatyczne uzupełnianie kodu (Code Completions), które cały czas (jest możliwość uśpienia lub wyłączenia takiego wsparcia) próbuje podpowiadać kolejne instrukcje, frazy czy całe bloki kodu, co czasami bywa irytujące, a czasami przerażające, bo wydaję się, że agent czyta w myślach i proponuje dokładnie to (i w takiej formie), o czym akurat pomyślałem.

Zadanie dla agenta

W jednym z ostatnio realizowanych projektów pojawiła się potrzeba szybkiego napisania klasy obsługującej skrzynkę pocztową MS Exchange. W nowym, pustym folderze, w trybie Agent piszę taki prompt:

Zainicjuj nowy projekt **Node.js**, a następnie przygotuj klasę **ExchangeMailbox**:
- nowa klasa w dedykowanym pliku **ExchangeMailbox.js**
- jedna metoda **sendMail**, która daje możliwość wysyłania wiadomości pocztowych
- użyj **MS Graph API**
- zainstaluj wszystkie potrzebne pakiety **NPM**
- wszystkie potrzebne dane dostępowe do API, np. **Tenant ID**, pobieraj ze zmiennych środowiskowych definiowanych w pliku **.env**
- utwórz plik **main.js** i zaimplementuj testy nowej klasy

Prompt redaguję z użyciem składni Markdown, ale nie mam pewności, czy dla agenta ma znaczenie, że słowa kluczowe są w ten sposób wyróżnione. Tak czy owak, prompt w oknie pracy agenta jest czytelniejszy i prezentuje się tak:

Zainicjuj nowy projekt Node.js, a następnie przygotuj klasę ExchangeMailbox:

  • nowa klasa w dedykowanym pliku ExchangeMailbox.js
  • jedna metoda sendMail, która daje możliwość wysyłania wiadomości pocztowych
  • użyj MS Graph API
  • zainstaluj wszystkie potrzebne pakiety NPM
  • wszystkie potrzebne dane dostępowe do API, np. Tenant ID, pobieraj ze zmiennych środowiskowych definiowanych w pliku .env
  • utwórz plik main.js i zaimplementuj testy nowej klasy

Wyniki pracy agenta

Agent pracował około 3 minut i przygotował:

  • plik copilot-instructions.md, który jest lepszą wersja mojego prompt-a
copilot-instructions.md
# Node.js ExchangeMailbox Project

This project implements a Node.js application with ExchangeMailbox class for sending emails via MS Graph API.

## Features

- ExchangeMailbox class with sendMail method
- MS Graph API integration for email sending
- Environment variable configuration
- Sample test implementation

## Setup

1. Install dependencies: `npm install`
2. Configure .env file with MS Graph API credentials
3. Run test: `node main.js`
  • plik .env z komentarzami
.env
# MS Graph API Configuration
# Uzyskaj te wartości z Azure Portal -> App registrations

# ID aplikacji (Client ID) z Azure AD
AZURE_CLIENT_ID=your_client_id_here

# Klucz tajny aplikacji (Client Secret) z Azure AD
AZURE_CLIENT_SECRET=your_client_secret_here

# ID dzierżawy (Tenant ID) z Azure AD
AZURE_TENANT_ID=your_tenant_id_here

# Email nadawcy (użytkownik, którego konto będzie używane do wysyłania)
SENDER_EMAIL=your_sender_email@yourdomain.com

# Opcjonalne: Email odbiorcy dla testów
TEST_RECIPIENT_EMAIL=test_recipient@example.com
  • główny plik implementujący klasę - ExchangeMailbox.js
ExchangeMailbox.js
import { ConfidentialClientApplication } from '@azure/msal-node'
import { Client } from '@microsoft/microsoft-graph-client'
import dotenv from 'dotenv'

// Ładowanie zmiennych środowiskowych
dotenv.config()

/**
 * Klasa do uwierzytelniania z MS Graph API
 */
class MSALAuthProvider {
  constructor() {
    this.clientApp = new ConfidentialClientApplication({
      auth: {
        clientId: process.env.AZURE_CLIENT_ID,
        clientSecret: process.env.AZURE_CLIENT_SECRET,
        authority: `https://login.microsoftonline.com/${process.env.AZURE_TENANT_ID}`
      }
    })
  }

  async getAccessToken() {
    const request = {
      scopes: ['https://graph.microsoft.com/.default']
    }

    try {
      const response = await this.clientApp.acquireTokenByClientCredential(request)
      return response.accessToken
    } catch (error) {
      console.error('Error acquiring token:', error)
      throw error
    }
  }
}

/**
 * Klasa ExchangeMailbox do wysyłania wiadomości email przez MS Graph API
 */
export class ExchangeMailbox {
  constructor() {
    this.authProvider = new MSALAuthProvider()
    this.client = null
    this.userEmail = process.env.SENDER_EMAIL
  }

  /**
   * Inicjalizacja klienta Graph API
   */
  async initialize() {
    try {
      this.client = Client.init({
        authProvider: async done => {
          try {
            const accessToken = await this.authProvider.getAccessToken()
            done(null, accessToken)
          } catch (error) {
            done(error, null)
          }
        }
      })
    } catch (error) {
      console.error('Error initializing Graph client:', error)
      throw error
    }
  }

  /**
   * Wysyła wiadomość email
   * @param {Object} mailOptions - Opcje wiadomości
   * @param {string} mailOptions.to - Adres odbiorcy
   * @param {string} mailOptions.subject - Temat wiadomości
   * @param {string} mailOptions.body - Treść wiadomości (HTML lub tekst)
   * @param {string} [mailOptions.bodyType='HTML'] - Typ treści: 'HTML' lub 'Text'
   * @param {Array} [mailOptions.attachments] - Załączniki (opcjonalne)
   * @returns {Promise<Object>} Wynik wysyłania
   */
  async sendMail(mailOptions) {
    if (!this.client) {
      await this.initialize()
    }

    const { to, subject, body, bodyType = 'HTML', attachments = [] } = mailOptions

    if (!to || !subject || !body) {
      throw new Error('Parametry to, subject i body są wymagane')
    }

    const message = {
      subject: subject,
      body: {
        contentType: bodyType,
        content: body
      },
      toRecipients: [
        {
          emailAddress: {
            address: to
          }
        }
      ]
    }

    // Dodanie załączników jeśli są dostępne
    if (attachments && attachments.length > 0) {
      message.attachments = attachments.map(attachment => ({
        '@odata.type': '#microsoft.graph.fileAttachment',
        name: attachment.name,
        contentBytes: attachment.contentBytes, // Base64 encoded
        contentType: attachment.contentType || 'application/octet-stream'
      }))
    }

    try {
      const response = await this.client.api(`/users/${this.userEmail}/sendMail`).post({
        message: message
      })

      console.log(`Email wysłany pomyślnie do: ${to}`)
      return {
        success: true,
        message: 'Email wysłany pomyślnie',
        response: response
      }
    } catch (error) {
      console.error('Błąd podczas wysyłania emaila:', error)
      return {
        success: false,
        message: 'Błąd podczas wysyłania emaila',
        error: error.message
      }
    }
  }

  /**
   * Sprawdza połączenie z MS Graph API
   * @returns {Promise<boolean>} True jeśli połączenie działa
   */
  async testConnection() {
    try {
      if (!this.client) {
        await this.initialize()
      }

      // Testujemy połączenie sprawdzając token dostępu
      const accessToken = await this.authProvider.getAccessToken()
      if (accessToken && accessToken.length > 0) {
        console.log('Połączenie z MS Graph API działa. Token otrzymany pomyślnie.')
        return true
      } else {
        console.log('Nie udało się uzyskać tokenu dostępu.')
        return false
      }
    } catch (error) {
      console.error('Błąd połączenia z MS Graph API:', error)
      return false
    }
  }
}

Zgodnie z moimi wytycznymi, powstał też plik main.js, z testami użycia nowej klasy, ale nie wklejam tutaj zawartości, bo jest sztucznie rozdmuchany, nieczytelny i sprawia wrażenie jakby został przygotowany dla kogoś, kto dopiero uczy się programowania i potrzebuje maksymalnej liczby komunikatów objaśniający co się dzieje w każdej linijce programu.

Kod nie od razu działał bez błędów, ale agent sam zaproponował testowe uruchomienie i po wykryciu błędów (analizując to co dzieje się na konsoli tekstowej), próbował go naprawić. W tym przypadku iterował 3 razy robiąc poprawki i ponownie uruchamiając test.

Uruchomienie kodu, musi być zaaprobowane przez nadzorcę, czyli mnie. Bez takiego zabezpieczenia, agent mógłby wykonać dowolny kod i to w kontekście moich uprawnień. To samo tyczy się aktualizacji wrażliwych plików, takich jak package.json, czy .env. Agent zawsze czeka na pozwolenie ingerencji w takie pliki, chyba że zezwolimy mu na takie działanie w kontekście identycznej operacji.

Tym czym mnie zaskoczył, to automatycznie wygenerowany plik README.md z przydatną dokumentacją, w której są wytyczne jak zdefiniować aplikację w Microsoft Azure, wygenerować dla niej klucz API i nadać odpowiednie uprawnienia. Podaje też proste przykłady użycia nowej klasy.

# 📧 ExchangeMailbox - Node.js Email Client

Projekt Node.js implementujący klasę `ExchangeMailbox` do wysyłania wiadomości email przez Microsoft Graph API.

## 🚀 Funkcjonalności

-**Klasa ExchangeMailbox** z metodą `sendMail`
-**Integracja z MS Graph API** do wysyłania emaili
-**Uwierzytelnianie** przez Azure AD (MSAL)
-**Konfiguracja przez zmienne środowiskowe** (.env)
-**Obsługa HTML i tekstowych emaili**
-**Wsparcie dla załączników**
-**Test połączenia** z MS Graph API
-**Przykłady użycia** i testy

## 📋 Wymagania

- Node.js 16+
- Konto Microsoft 365 / Azure AD
- Zarejestrowana aplikacja w Azure Portal
- **Uprawnienia administratora Azure AD** (do zatwierdzenia uprawnień aplikacji)

## 🛠️ Instalacja

1. **Sklonuj/pobierz projekt**
2. **Zainstaluj zależności:**

   ```bash
   npm install
   ```

3. **Skonfiguruj zmienne środowiskowe** (zobacz sekcję konfiguracja)

4. **Uruchom test:**
   ```bash
   npm start
   ```

## ⚙️ Konfiguracja Azure AD

### 1. Rejestracja aplikacji w Azure Portal

1. Przejdź do [Azure Portal](https://portal.azure.com)
2. Wybierz **Azure Active Directory** > **App registrations** > **New registration**
3. Podaj nazwę aplikacji (np. "ExchangeMailbox App")
4. Wybierz **Accounts in this organizational directory only**
5. Kliknij **Register**

### 2. Konfiguracja uprawnień API

1. W zarejestrowanej aplikacji przejdź do **API permissions**
2. Kliknij **Add a permission** > **Microsoft Graph** > **Application permissions**
3. Dodaj uprawnienie:
   - `Mail.Send` - do wysyłania emaili w imieniu aplikacji
4. Kliknij **Grant admin consent** (wymagane uprawnienia administratora)

> **Uwaga:** Używamy uwierzytelniania aplikacji (Client Credentials flow), które pozwala aplikacji działać bez logowania użytkownika, ale wymaga uprawnień administratora.

### 3. Utworzenie Client Secret

1. Przejdź do **Certificates & secrets**
2. Kliknij **New client secret**
3. Podaj opis i wybierz okres ważności
4. **Skopiuj wartość** - będzie potrzebna w pliku .env

### 4. Skopiuj identyfikatory

Z sekcji **Overview** skopiuj:

- **Application (client) ID**
- **Directory (tenant) ID**

## 🔧 Konfiguracja zmiennych środowiskowych

Skopiuj plik `.env` i wypełnij następujące wartości:

```env
# MS Graph API Configuration
AZURE_CLIENT_ID=your_client_id_here
AZURE_CLIENT_SECRET=your_client_secret_here
AZURE_TENANT_ID=your_tenant_id_here
SENDER_EMAIL=your_sender_email@yourdomain.com
TEST_RECIPIENT_EMAIL=test_recipient@example.com
```

### Gdzie znaleźć te wartości:

- **AZURE_CLIENT_ID**: Application ID z Azure Portal
- **AZURE_CLIENT_SECRET**: Client Secret utworzony w Azure Portal
- **AZURE_TENANT_ID**: Directory ID z Azure Portal
- **SENDER_EMAIL**: Email użytkownika, który będzie nadawcą (musi istnieć w organizacji)
- **TEST_RECIPIENT_EMAIL**: Email do testów (opcjonalne)

## 📖 Użycie

### Podstawowy przykład

```javascript
import { ExchangeMailbox } from './ExchangeMailbox.js'

const mailbox = new ExchangeMailbox()

// Wysłanie prostego emaila
const result = await mailbox.sendMail({
  to: 'odbiorca@example.com',
  subject: 'Temat wiadomości',
  body: 'Treść wiadomości'
})

if (result.success) {
  console.log('Email wysłany pomyślnie!')
} else {
  console.error('Błąd:', result.message)
}
```

### Email z HTML

```javascript
await mailbox.sendMail({
  to: 'odbiorca@example.com',
  subject: 'Email z HTML',
  body: '<h1>Witaj!</h1><p>To jest <strong>HTML</strong> email.</p>',
  bodyType: 'HTML'
})
```

### Email z załącznikami

```javascript
await mailbox.sendMail({
  to: 'odbiorca@example.com',
  subject: 'Email z załącznikiem',
  body: 'Zobacz załącznik w załączniku',
  attachments: [
    {
      name: 'dokument.pdf',
      contentBytes: 'base64_encoded_file_content',
      contentType: 'application/pdf'
    }
  ]
})
```

### Test połączenia

```javascript
const isConnected = await mailbox.testConnection()
if (isConnected) {
  console.log('Połączenie z MS Graph API działa!')
}
```

> **Uwaga:** Test połączenia sprawdza czy można uzyskać token dostępu z Azure AD. Nie wymaga dodatkowych uprawnień API.

## 🏃 Uruchomienie testów

```bash
# Uruchomienie testów
npm start

# lub bezpośrednio
node main.js
```

## 📁 Struktura projektu

```
exchange-mailbox-nodejs/
├── 📄 ExchangeMailbox.js    # Główna klasa do wysyłania emaili
├── 📄 main.js              # Testy i przykłady użycia
├── 📄 package.json         # Konfiguracja NPM i zależności
├── 📄 .env                 # Zmienne środowiskowe (konfiguracja)
├── 📄 .gitignore          # Wykluczenia Git (chroni .env)
├── 📄 README.md           # Dokumentacja (ten plik)
└── 📁 .github/
    └── 📄 copilot-instructions.md  # Instrukcje dla Copilot
```

## 🔍 Rozwiązywanie problemów

### Błędy uwierzytelniania

1. **"Invalid client credentials"** - sprawdź Client ID i Client Secret w pliku .env
2. **"Invalid tenant"** - sprawdź Tenant ID w pliku .env
3. **"Insufficient privileges"** - upewnij się że aplikacja ma uprawnienie `Mail.Send` i zostało zatwierdzone przez administratora

### Błędy wysyłania emaili

1. **"User not found"** - sprawdź czy SENDER_EMAIL to prawdziwy użytkownik w organizacji
2. **"Access denied"** - sprawdź czy admin zatwierdził uprawnienia aplikacji
3. **"Mailbox not found"** - upewnij się że użytkownik ma licencję Exchange Online

### Częste problemy podczas testów

1. **"/me request is only valid with delegated authentication flow"** - Ten błąd jest normalny, używamy uwierzytelniania aplikacji (client credentials)
2. **"Insufficient privileges to complete the operation"** - Nie jest potrzebne uprawnienie User.Read dla podstawowej funkcjonalności wysyłania emaili
3. **"TypeError: \_this.provider is not a function"** - Problem z konfiguracją klienta Graph, rozwiązany w aktualnej wersji

### Debugging

- Test połączenia sprawdza czy można uzyskać token dostępu (nie wymaga dodatkowych uprawnień)
- Włącz szczegółowe logi błędów w konsoli aby zobaczyć dokładne komunikaty z MS Graph API
- Sprawdź czy wszystkie zmienne środowiskowe w .env są wypełnione

## 📦 Zależności

- `@azure/msal-node` - Uwierzytelnianie Microsoft Authentication Library
- `@microsoft/microsoft-graph-client` - Klient MS Graph API
- `dotenv` - Ładowanie zmiennych środowiskowych

## 📄 Licencja

MIT License - możesz swobodnie używać, modyfikować i dystrybuować ten kod.

## 🆘 Pomoc

Jeśli napotkasz problemy:

1. Sprawdź konfigurację w Azure Portal
2. Zweryfikuj zmienne środowiskowe w `.env`
3. Uruchom test połączenia: `await mailbox.testConnection()`
4. Sprawdź logi błędów w konsoli

---

**📧 Miłego wysyłania emaili z ExchangeMailbox! 🚀**

Wnioski

Wygenerowany kod nie jest idealny. Klasa ExchangeMailbox może mieć zastosowanie w różnych projektach i nie powinna używać console.log() i console.error(), tylko generować wyjątki w przypadku błędów krytycznych. Wyjątki można "złapać" w obszarach brzegowych kodu, na przykład na styku z użytkownikiem końcowym i tam wyświetlić czytelny komunikat.

Takie rzeczy da się oczywiście poprawić i warto to robić z użyciem agenta, który działając w już zainicjowanym kontekście, będzie trzymał się swojego stylu pracy. Na przykład: jeśli stworzył plik README.md, będzie go na bieżąco aktualizował.

Po około 15 minutach pracy a agentem miałem już działającą metodę sendMail().
Działam dalej...

W pliku copilot-instructions.md dodałem sekcję ## Rules instruując agenta, żeby w metodach klasy ExchangeMailbox nie używał console.log() i console.error(). Napisałem kilka prompt-ów i po kilku minutach miałem gotowe metody do:

  • pobierania listy nieprzeczytanych wiadomości
  • odczytywania treści wybranej wiadomości
  • pobierania i zapisywania na dysku załączników z wybranej wiadomości
  • oznaczania i odznaczania wiadomości jako przeczytanej

Nie ma wątpliwości, że przyspieszenie pracy jest kilkukrotne, może nawet kilkunastokrotne.

Na ten moment nie przekonuje mnie vibe coding w stylu generowania dużej ilości kodu na raz, na przykład całej aplikacji lub wielu funkcjonalności w jednym podejściu. Praca z agentem, który w danym momencie wykonuje konkretną, dobrze opisaną pracę, daje nadzorcy możliwość szybkiej weryfikacji zarówno jakości, jak i poprawności kodu.