# Pipe Modülü Yazma

https://dev.wisecp.com/tr/pipe-modulu-yazma

Pipe modülü bir destek posta kutusunu okur ve normalleştirilmiş bir mesaj dizisi döndürür; cron zinciri onu talebe ve yanıta çevirir.

## Genel Bakış

Pipe, destek talebi e-posta alım tipidir. Üç modül gelir: Google, Microsoft ve Pop3. Taban sınıf yoktur.

Yapılandırma birimi modül değil departmandır; bağlantı durumuna dokunan her metot bir departman kimliği alır.

İstek döngüsünde modülünüze yalnız ayar ekranı ve OAuth geri çağrısı dokunur. Okumayı cron yapar: keşif getirme işini başlatır, getirme `inbox()` çağırır, üçüncü iş talebi oluşturur.

## Ön Koşullar

- `coremio/modules/Pipe/` altına yazma erişimi.
- Erişilebilir bir posta kutusu ve `imap` eklentisi ya da OAuth uygulaması.
- Önceden oluşturulmuş destek talebi departmanları.
- [Zamanlanmış Görev Ekleme](https://dev.wisecp.com/tr/zamanlanmis-gorev-ekleme).

## Yapı

Pipe sınıfları ad alanlıdır: `WISECP\Modules\Pipe`. Cron bu adı sağlayıcı dizesinden kurar.

```bash
coremio/modules/Pipe/Acme/
├── Acme.php                    namespace WISECP\Modules\Pipe; class Acme
├── config.php                  genel sağlayıcı bloğu + departman id başına bir blok
├── lang/en.php                 alan etiketleri, description, setup-guide
├── lang/tr.php
├── logo.svg
└── views/
    ├── credentialsForm.php     departman başına alanlar, ayarlar sekmesine render edilir
    └── providerForm.php        yalnız OAuth uygulamaları: genel client id ve secret
```

OAuth yönlendiricisi paylaşılandır ve her parçayı bir modüle eşler.

## Adım Adım

### Modül Sınıfını Kurma

1. `coremio/modules/Pipe/Acme/Acme.php` dosyasını `namespace WISECP\Modules\Pipe;` ve `class Acme` ile oluşturun.
2. Sınıfa dizinle eşleşen public bir `$name` verin.
3. `save_config(array $data)` özyinelemeli birleştirme olmalı.
4. `is_connected(int $did)` uygulayın; false diyen departman atlanır.
5. `inbox(int $did)` uygulayın.
6. `test_connection(int $did)` uygulayın.

### Departman Formu

1. `views/credentialsForm.php` oluşturun; `$mv`, `$mk` ve `$did` alır.
2. Alanları `module[{modülAnahtarı}][{did}][{alan}]` olarak adlandırın.
3. Mevcut değerleri `$mv["init"]->config[$did]`, etiketleri `$mv["lang"]` içinden okuyun.
4. Uzun anlatım `setup-guide` anahtarına konur.

### OAuth Ekleme

1. `is_configured()` bildirin; varlığı sağlayıcı kartının anahtarıdır.
2. `get_global_config()`, `save_provider_config()` ve `views/providerForm.php` ekleyin.
3. Yönlendirme adresini paylaşılan rota anahtarıyla üretin: `LinkGenerator::client('pipe-oauth-callback', [strtolower($this->name)])`.
4. Yetkilendirme adresini döndüren `oAuth2(int $did)` ve `callback_handle()` ekleyin.
5. Parçanızı yönlendiriciye kaydedin.
6. `clear_tokens(int $did)` ekleyin.

## Referans

### Metot Sözleşmesi

Her biri çağrılmadan önce `method_exists` ile yoklanır.

| Metot | Çağıran | Yoksa ne olur |
| --- | --- | --- |
| `inbox(int $did): array` | getirme cron'u | iş `inbox-missing` ile düşer |
| `is_connected(int $did): bool` | keşif cron'u | departman yine başlatılır |
| `save_config(array $data)` | ayar kaydetme | alanlar saklanmaz |
| `test_connection($did): array` | ayar ekranı | test butonu çalışmaz |
| `is_configured(): bool` | ayar ekranı | sağlayıcı kartı çıkmaz |
| `get_global_config(): array` | ayar ekranı | kart boş görünür |
| `save_provider_config($id, $secret): bool` | sağlayıcı kaydetme | kaydetme istisna fırlatır |
| `oAuth2($did): array` | ayar ekranı | bağlan butonu çalışmaz |
| `callback_handle(): array` | paylaşılan OAuth rotası | işleyici bulunamaz |
| `clear_tokens($did)` | ayar ekranı | bağlantı kesilemez |
| `get_connected_email($did): string` | kimlik bilgisi formu | posta kutusu gösterilmez |

Panelden yalnız `oAuth2`, `test_connection` ve `clear_tokens` çağrılabilir.

### İmzalar

```php
public function __construct();

// Özyinelemeli birleştirme, sonra config.php yeniden yazılır. Düz bir atama diğer departmanları siler.
public function save_config($data = []);

// TEK bir departmanın hazırlığı. Token'lar var, ya da hostname+username+password dolu.
public function is_connected(int $did): bool;

// Hattın olmadan yapamayacağı tek metot. Dönüş biçimi aşağıda.
public function inbox(int $did): array;

// ['status' => 'successful'] ya da ['status' => 'error', 'message' => '...'].
public function test_connection($did = 0): array;

// Yalnız OAuth modülleri.
public function is_configured(): bool;
public function get_global_config(): array;                                    // client_id, client_secret, redirect_uri
public function save_provider_config(string $client_id, string $client_secret_plain): bool;
public function oAuth2($did = 0): array;                                       // ['status' => 'successful', 'redirect' => $url]
public function callback_handle(): array;                                      // ['status' => 'connected', 'did' => 4, 'email' => '...']
public function clear_tokens($did = 0);
public function get_connected_email(int $did): string;
```

Asıl sözleşme mesaj dizisidir; eksik anahtar sessizce eksilir.

```php
return [
    'status' => 'successful',            // başka her şey yumuşak hata sayılır
    'data'   => [
        [
            'ip'          => '203.0.113.9',              // başlıklar taşıyorsa gönderen IP'si, yoksa ''
            'date'        => '2026-08-03 09:41:00',      // Y-m-d H:i:s, yinelenen özetinde kullanılır
            'subject'     => 'Cannot reach my panel',    // talebin konusu olur
            'spam'        => false,                      // true işleyicinin mesajı düşürmesini sağlar
            'msgid'       => '',                         // isteğe bağlı; boşken kararlı bir özet üretilir
            'from'        => ['name' => 'Ada L.', 'address' => 'ada@example.com'],
            'to'          => ['name' => 'Support', 'address' => 'support@example.com'],
            'message'     => '<p>The panel times out.</p>',
            'attachments' => [
                [
                    'file_name' => 'screenshot.png',     // gönderenin kullandığı ad
                    'name'      => 'a1b2c3d4e5f6.png',   // rastgeleleştirilmiş saklanan ad
                    'file_ext'  => 'png',
                    'size'      => 20481,
                    'content'   => 'iVBORw0KGgoAAAANS',  // ham baytların base64'ü
                ],
            ],
        ],
    ],
];

// Hatada ya fırlatın ya da yumuşak hata biçimini döndürün:
return ['status' => 'error', 'message' => 'Cannot connect to server', 'data' => []];
```

Fırlatmak devre kesiciyi kurar ve işi başarısız kaydeder; dönen `error` durumu işi iptal işaretler. Yapılandırma için döndürün, taşıma için fırlatın.

### Config Düzeni

`config.php` modül geneli için dize, departman durumu için tam sayı anahtar taşır. `save_config()` bu yüzden birleştirmelidir.

```php
<?php
return [
    'lookback_days' => 3,                        // modül geneli
    'provider'      => [                         // modül geneli, yalnız OAuth uygulamaları
        'client_id'     => '...',
        'client_secret' => '...',                // crypt/system ile Crypt::encode
    ],
    4 => [                                       // departman 4
        'tokens' => '...',                       // token JSON'unun Crypt::encode'u
        'email'  => 'support@example.com',
    ],
    7 => [                                       // departman 7, kimlik bilgisi tabanlı
        'protocol' => 'imap',
        'hostname' => 'mail.example.com',
        'port'     => 993,
        'username' => 'support@example.com',
        'password' => '...',
        'ssl'      => true,
    ],
];
```

Posta kutusu ile departman eşlemesi paylaşılan seçenekler dosyasındadır:

- **options/ticket-pipe/status**: Ana anahtar; kapalıyken keşif `pipe-disabled` ile iptal döner.
- **options/ticket-pipe/mail**: Departman kimliğini `provider`, `from`, `fname` satırına eşler.
- **options/ticket-pipe/prefix**: Giden konulara yazılıp dönüşte eşleştirilen referans etiketi. Varsayılanı `REF`.
- **options/ticket-pipe/existing-client**: Gönderen bir hesapla eşleşmediğinde reddetmek mi, oluşturmak mı.
- **options/ticket-pipe/spam-control**: Açıkken spam denetimleri konu ve gönderen üzerinde önce çalışır.
- **Config::save()**: İç içedir: alt ağacın tamamını yazın, `Config::save("options", Config::set("options", ['ticket-pipe' => $ticketPipe]))`.

### Cron Zinciri

Hattı üç kuyruk işleyicisi kurar:

| İşleyici | Ne yapar | Modülünüze dokunduğu yer |
| --- | --- | --- |
| discover | Departmanları gezer, her biri için bir getirme işi başlatır | `is_connected()` ve sınıf kontrolü |
| fetch | Mesajları toplar, her biri için bir mesaj işi başlatır | `inbox()` |
| message | Mesajı talebe ya da yanıta çevirir | hiçbir şey; dizinizi okur |

Dört koşul keşfin bir departmanı hatasız atlamasına yol açar: sınıf yok, `is_connected()` false, getirme havada, ya da bekleme penceresi. Pencere üç saattir.

## Örnek

Kimlik bilgisi tabanlı bir modül, sonra onu çağıran cron kodu.

```php
<?php
namespace WISECP\Modules\Pipe;

class Acme
{
    public $name = "Acme";
    public $config = [];
    public $lang = [];
    public $test = false;

    public function __construct()
    {
        $this->config = \Modules::Config("Pipe", $this->name);
        $this->lang   = \Modules::Lang("Pipe", $this->name);
    }

    // Özyinelemeli birleştirme: ayar ekranı her seferinde tek departman gönderir.
    public function save_config($data = [])
    {
        $merged       = array_replace_recursive($this->config ?: [], $data);
        $this->config = $merged;

        return \FileManager::file_write(__DIR__ . DS . "config.php", \Utility::array_export($merged, ['pwith' => true]));
    }

    public function is_connected(int $did): bool
    {
        $cfg = $this->config[$did] ?? null;
        if (!is_array($cfg)) return false;

        return !empty($cfg['hostname']) && !empty($cfg['username']) && !empty($cfg['password']);
    }

    public function test_connection($did = 0): array
    {
        try {
            // Form module[Acme][{did}][field] olarak gönderir, okunacak yol budur.
            $host = trim((string) \Filter::init("POST/module/" . $this->name . "/" . $did . "/hostname"));
            $user = trim((string) \Filter::init("POST/module/" . $this->name . "/" . $did . "/username"));
            $pass = (string) \Filter::init("POST/module/" . $this->name . "/" . $did . "/password", "password");

            if ($host === '' || $user === '' || $pass === '')
                throw new \Exception($this->lang["credentials-required"] ?? 'Hostname, username and password are required.');

            $this->open($host, $user, $pass);
        }
        catch (\Exception $e) {
            return ['status' => "error", 'message' => $e->getMessage()];
        }

        return ['status' => "successful"];
    }

    public function inbox(int $did): array
    {
        $cfg = $this->config[$did] ?? null;

        // Yapılandırma sorunu yumuşak hatadır: iş başarısız değil iptal edilir.
        if (!$cfg) return ['status' => 'error', 'message' => 'Department config not found', 'data' => []];

        // Taşıma sorunu fırlatır: kesici açılır ve iş başarısız olarak kaydedilir.
        $session = $this->open($cfg['hostname'] ?? '', $cfg['username'] ?? '', $cfg['password'] ?? '');

        $lookback = max(1, (int) ($this->config['lookback_days'] ?? 3));
        $since    = date('Y-m-d', strtotime('-' . $lookback . ' days'));

        $messages = [];
        foreach ($this->unread($session, $since) as $raw) {
            $messages[] = [
                'ip'          => (string) ($raw['sender_ip'] ?? ''),
                'date'        => \DateManager::format("Y-m-d H:i:s", $raw['date'] ?? ''),
                'subject'     => (string) ($raw['subject'] ?? ''),
                'spam'        => false,
                'from'        => ['name' => (string) ($raw['from_name'] ?? ''), 'address' => (string) ($raw['from'] ?? '')],
                'to'          => ['name' => (string) ($raw['to_name'] ?? ''),   'address' => (string) ($raw['to'] ?? '')],
                'message'     => (string) ($raw['html'] ?? ($raw['text'] ?? '')),
                'attachments' => $this->attachments($raw['parts'] ?? []),
            ];
        }

        return ['status' => "successful", 'data' => $messages];
    }
}
```

```php
// new değil getInstance: önce Modules::Load koşar ve modül config önbelleğini doldurur.
// Çıplak bir new ile yapıcı Modules::Config içinden null okur ve inbox() metodu
// "Department config not found" diyerek çıkar.
$module = \Modules::getInstance("Pipe", $provider);

if (!$module) return ['success' => false, 'result' => ['reason' => 'module-missing']];
if (!method_exists($module, 'inbox')) return ['success' => false, 'result' => ['reason' => 'inbox-missing']];

try {
    $response = $module->inbox($did);
}
catch (\Throwable $e) {
    self::set_cooldown($did, $e->getMessage());
    Admin::notify('ticket-pipe-failure', self::failure_payload($did, $provider, $depName, $depFrom, $e->getMessage()), 'error', [
        'dedupe_keys' => ['did', 'provider'],
        'recurring'   => true,
    ]);
    throw $e;
}

$status = (string) ($response['status'] ?? '');
$data   = (array)  ($response['data']   ?? []);

if ($status !== 'successful') {
    self::set_cooldown($did, (string) ($response['message'] ?? 'Unknown module error'));
    // ... bildir, sonra iptal edilmiş bir iş döndür
}

// Temiz koşu: bekleyen hata bildirimini düşür ve kesiciyi serbest bırak.
Admin::notify_resolve('ticket-pipe-failure', ['did' => $did, 'provider' => $provider]);
self::clear_cooldown($did);
```

Modüller mesaj kimliği döndürmüyor; getirme işleyicisi departman, gönderen, tarih ve konudan özet üretir.

## Tuzaklar

> **Düz bir anahtar cron'a ulaşmaz**
> 
> Her şey `options/ticket-pipe` altındadır. Benzer adlı üst seviye anahtar şikâyetsiz kaydedilir ve geri okunur, cron ise eski değeri görür.

> **save_config birleştirmeli, üzerine yazmamalı**
> 
> Ayar gönderimi yalnız düzenlenen departmanı taşır. Atama yapan bir `save_config()` diğer departmanların kimlik bilgilerini siler; tek belirti o kutuların yoklanmamasıdır.

> **Örneği fabrikayla kurun**
> 
> `Modules::getInstance()` önce yükleyiciyi çalıştırıp config önbelleğini doldurur. Çıplak bir `new` config'i null bırakır; hata `inbox()` içinde çıkar.

> **Hata departmanı üç saat uykuya yatırır**
> 
> Devre kesici, bozuk bir kutunun dakika başı bildirim çıkarmasını engeller. Test sırasında düzeltmeniz işe yaramamış görünür: departman `cooldown` ile atlanır. Getirme işini doğrudan yeniden başlatın.

> **Token'lar sistem anahtarından geçer**
> 
> OAuth token'ları ve client secret'lar `Crypt::encode($value, Config::get("crypt/system"))` ile saklanır: kullanıcı anahtarı değil, sistem anahtarı. Token JSON'unu kodlanmış saklayın.

## İlgili Makaleler

- [Zamanlanmış Görev Ekleme](https://dev.wisecp.com/tr/zamanlanmis-gorev-ekleme)
- [Mail Modülü Yazma](https://dev.wisecp.com/tr/mail-modulu-yazma)
- [Sosyal Giriş Sağlayıcısı Yazma](https://dev.wisecp.com/tr/sosyal-giris-saglayicisi-yazma)
- [Modül Yapılandırması](https://dev.wisecp.com/tr/modul-yapilandirmasi)
- [Yapılandırma Okuma ve Yazma](https://dev.wisecp.com/tr/yapilandirma-okuma-ve-yazma)
- [Admin Form Oluşturucu](https://dev.wisecp.com/tr/admin-form-olusturucu)
