
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 próbuje podpowiadać kolejne instrukcje, frazy czy całe bloki kodu — można je uśpić lub wyłączyć, jeśli przeszkadza. Czasami bywa to irytujące, a czasami przerażające, bo wydaje 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ą wersją mojego prompt-a
# 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
# 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
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.
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 z 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.