# Mail Modülü Yazma

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

Mail modülü e-postayı teslim eden sürücüdür: bildirim yardımcısının her mesajda çağırdığı metotları uygular.

## Genel Bakış

`MailModule` diye bir taban sınıf yoktur: tip ördek tiplemesiyle çalışır. Çekirdek, modül dizininin adını taşıyan sınıfı yükler ve üzerinde sabit bir metot kümesini çağırır.

Aynı anda tek bir Mail modülü etkindir; adı `modules/mail` içinde durur. Bu anahtarı, operatör etkinleştirdiğinde modül kendi yazar.

SampleMail ile başlayın: sözleşmenin tamamını uygular ve mesajları diske yazar.

## Ön Koşullar

- `coremio/modules/Mail/` altına yazma erişimi.
- Sunucudan erişilebilen bir taşıyıcı: SMTP ya da anahtarı olan bir HTTP API'si.
- Ayar formu: [Admin Form Oluşturucu](https://dev.wisecp.com/tr/admin-form-olusturucu).
- Arka plan: [Bildirim Şablonları Nasıl Çalışır](https://dev.wisecp.com/tr/bildirim-sablonlari-nasil-calisir).

## Yapı

Dizin adı, dosya adı ve sınıf adı aynı dizedir. Kayıt mekanizmasının tamamı budur.

```bash
coremio/modules/Mail/Acme/
├── Acme.php          sürücü sınıfı, adı Acme, ad alanı yok
├── config.php        dizi döndürür: künye + kaydedilmiş ayarlar
├── lang/en.php       düz bir key => string dizisi döndürür
├── lang/tr.php
├── logo.png          isteğe bağlı, config meta.logo ile gösterilir
└── pages/            isteğe bağlı, page_settings() yerine settings.php
```

Sınıf hiçbir ad alanı taşımaz. İlk satır doğrudan erişim koruyucusudur.

## Adım Adım

### Sürücü Sınıfını Kurma

1. `coremio/modules/Mail/Acme/Acme.php` oluşturun, erişim koruyucusuyla açın ve `class Acme` tanımlayın.
2. Yapıcıda modülün ayarlarını ve metinlerini yükleyin, isteğe bağlı geçersiz kılma dizisini birleştirin.
3. `subject`, `body`, `AddAddress` ve `addAttachment` metotlarını her biri `$this` döndürecek şekilde uygulayın.
4. Teslim kaydı için `getSubject`, `getBody`, `getAddresses` uygulayın.
5. `submit()` uygulayın: başarıda doğru değer, hatada `$this->error` dolar ve false döner.

### Ayar Sayfası

1. Formu döndüren `page_settings()` ekleyin. Çekirdek bunu `pages/settings.php` dosyasına tercih eder.
2. Üç gizli alan gönderin: operation, controller adı ve modül adı.
3. Etkinleştirme kutusunu ekleyin; etkin sürücü sizin sınıfınıza eşitse işaretli olsun.
4. `controller_save()` ekleyin: değişen alanları `config.php` içine yazın, sonra `modules/mail` anahtarını çevirin.
5. İsteğe bağlı: `controller_test_connection()` ve formu o controller ile gönderen bir buton.

### Etkinleştirme ve Doğrulama

1. `{admin}/modules/mail` açın, modülünüzü seçin, kimlik bilgilerini doldurun ve etkinleştirin.
2. Kaydetmek sınıf adını `modules/mail` anahtarına yazar; önceki sürücü kapanır.
3. Bir bildirim gönderin. Geliştirmede SampleMail kullanın ve yakalanan dosyayı okuyun.

## Referans

### Çekirdek Neyi, Nereden Çağırır

Yardımcının kullandığı metotlar yalnız bunlardır.

| Metot | Çekirdek ne zaman çağırır | Ne döndürmeli |
| --- | --- | --- |
| `__construct($external_config = [])` | Mesaj başına bir kez | hiçbir şey |
| `body($text, $template, $variables, $lang, $user)` | İlk sırada | `$this` |
| `subject($arg)` | body'den sonra, çağıran konuyu değiştiriyorsa | `$this` |
| `addAttachment($path, $name)` | Ek başına bir kez | `$this` |
| `AddAddress($address, $name)` | En son, alıcı başına bir kez | `$this` |
| `submit($isthis = false)` | Alıcı eklendikten sonra | başarıda doğru değer, hatada false |
| `getSubject()` | Başarılı gönderimden sonra (log satırı) | dize |
| `getBody()` | Başarılı gönderimden sonra (log satırı) | dize |
| `getAddresses()` | Başarılı gönderimden sonra (log satırı) | düz adres dizisi |
| `$error` (public özellik) | Yanlış değer dönen gönderimden sonra | hata metni |

İki metot isteğe bağlıdır: `set_credentials(array $data)` kayıtlı kimlik bilgilerini geçersiz kılar, `setFromEmail()` ve `setFromName()` ise göndereni.

### Sürücü Metot İmzaları

Bu imzaları birebir kopyalayın: çekirdek bazılarını kabul ettiklerinden daha az argümanla çağırır.

```php
public function __construct($external_config = []);

// $text      ham gövde, $template false olduğunda olduğu gibi kullanılır
// $template  "group/name", örn. "invoice/invoice-created"; false işlemeyi atlar
// $variables şablona verilen yer tutucu haritası
// $lang      alıcının dil kodu, operatörünki değil
// $user      alıcının kullanıcı id'si, hesabı olmayan bir adres için 0
public function body($text = '', $template = false, $variables = [], $lang = '', $user = 0);

// Konuyu ayarlamadan önce alıcı listesini sıfırlar. Bu bilinçlidir, bkz. Tuzaklar.
public function subject($arg = '');

// $arg1 ya bir adres dizesidir ya da address => name haritasıdır.
public function AddAddress($arg1 = '', $arg2 = '');

public function addAttachment($path = '', $name = '');
public function setFromEmail($email = '');
public function setFromName($name = '');
public function set_credentials($data = []);

// $isthis = true boolean yerine sürücüyü döndürür, böylece çağrı zincirlenebilir.
public function submit($isthis = false);

public function getSubject();
public function getBody();
public function getAddresses();
public function address_reset();
```

`$template` dolu geldiğinde sürücü şablonu kendisi işler. Çağrı ve dönüş biçimi sabittir:

```php
// View::notifications($type, $template_name, $content, $variables, $lang, $user): array
$look = View::notifications("mail", $template, $text, $variables, $lang, $user);

// ['subject' => '...', 'content' => '...'] döndürür, şablon eksikse false.
if ($look !== false && isset($look["subject"]) && isset($look["content"])) {
    $this->subject($look["subject"]);
    $text = $look["content"];
}
```

### Config Dosyası ve Anahtarları

`config.php` düz bir dizi döndürür. `meta` anahtarını modül listesi okur; diğer anahtarlar sizindir.

- **meta.name**: Modül listesindeki görünen ad. Dil dosyasındaki `name` anahtarı üstün gelir.
- **meta.version**: Modülün yanındaki sürüm dizesi. Serbest biçimli.
- **meta.logo**: Modül dizinindeki logo dosyasının adı. Yazılmazsa liste sırasıyla `logo.svg`, `logo.webp`, `logo.png` arar.
- **fname**: Gönderen görünen adı. Hazır sürücülerin hepsi bu anahtarı kullanır.
- **from**: Gönderen adresi. Mailjet buna `femail` der; `setFromEmail()` bu yüzden vardır.
- **Crypt::encode()**: Sırlar şifreli saklanır: `Crypt::encode($v, Config::get("crypt/user"))` ile yazın, `Crypt::decode()` ile okuyun. `config.php` dosyasına düz metin anahtar yapıştırmayın.

### Ayar Gönderimi Size Nasıl Ulaşır

Ayar formu `operation=module_controller` gönderir. Bu operation modülü yükler, sonra controller adını iki adımda çözer.

| Adım | Aranan | Sonuç |
| --- | --- | --- |
| 1 | Modül içinde 5 bayttan büyük `controllers/{controller}.php` | include edilir, dönüşü yanıt olur |
| 2 | Örnekte `controller_{controller}()`, tireler alt çizgi olarak | try/catch içinde çalışır, dönüşü yanıt olur |
| yedek | ikisi de yoksa | `['status' => 'error', 'message' => 'Module controller not found']` |

Yani `controller=test-connection` gönderimi `controller_test_connection()` metoduna ulaşır. İçinde fırlatabilirsiniz; çözücü yakalar. Şunu döndürün: `['status' => 'successful', 'message' => '...']`.

## Örnek

Eksiksiz bir sürücü, sonra onu süren çekirdek kodu.

```php
<?php
defined('CORE_FOLDER') OR exit('You can not get in here!');

class Acme
{
    public $error = null;
    public $lang = [];
    public $config = [];
    public $credentials;

    private $subject = '';
    private $body = '';
    private $addresses = [];
    private $attachments = [];

    public function __construct($external_config = [])
    {
        $config       = Modules::Config('Mail', __CLASS__);
        $this->lang   = Modules::Lang('Mail', __CLASS__);
        $this->config = array_merge($config ?: [], $external_config);
    }

    public function set_credentials($data = [])
    {
        $this->credentials = $data;
        return $this;
    }

    public function subject($arg = '')
    {
        // Dağıtıcının alıcı döngüsünün birikmesini engelleyen şey burada temizlemektir.
        $this->address_reset();
        $this->subject = (string) $arg;
        return $this;
    }

    public function body($text = '', $template = false, $variables = [], $lang = '', $user = 0)
    {
        if ($template) {
            $look = View::notifications('mail', $template, $text, $variables, $lang, $user);
            if ($look !== false && isset($look['subject']) && isset($look['content'])) {
                $this->subject($look['subject']);
                $text = $look['content'];
            }
        }
        $this->body = (string) $text;
        return $this;
    }

    public function setFromEmail($email = '')
    {
        $this->config['from'] = $email;
        return $this;
    }

    public function setFromName($name = '')
    {
        $this->config['fname'] = $name;
        return $this;
    }

    public function AddAddress($arg1 = '', $arg2 = '')
    {
        if (is_array($arg1)) foreach ($arg1 as $address => $name) $this->addresses[$address] = $name;
        else $this->addresses[$arg1] = $arg2;

        return $this;
    }

    public function addAttachment($path = '', $name = '')
    {
        $this->attachments[] = ['path' => $path, 'name' => $name];
        return $this;
    }

    public function getAddresses()
    {
        return array_keys($this->addresses);
    }

    public function getSubject()
    {
        return $this->subject;
    }

    public function getBody()
    {
        return $this->body;
    }

    public function address_reset()
    {
        $this->addresses = [];
        return true;
    }

    public function submit($isthis = false)
    {
        $config = $this->credentials ?: $this->config;
        $key    = Crypt::decode($config['api_key'] ?? '', Config::get("crypt/user"));

        $payload = [
            'from'    => ['email' => $config['from'] ?? '', 'name' => $config['fname'] ?? ''],
            'to'      => array_map(fn ($a, $n) => ['email' => $a, 'name' => $n], array_keys($this->addresses), $this->addresses),
            'subject' => $this->subject,
            'html'    => $this->body,
        ];

        $response = Utility::HttpRequest([
            'url'    => 'https://api.example.com/v1/send',
            'type'   => 'POST',
            'data'   => Utility::jencode($payload),
            'header' => ['Authorization: Bearer ' . $key, 'Content-Type: application/json'],
        ]);

        $decoded = Utility::jdecode((string) $response, true) ?: [];
        $sent    = (string) ($decoded['status'] ?? '') === 'queued';

        // Dağıtıcı yanlış değer dönüşünden sonra $this->error alanını okur; burada fırlatmaları yakalamaz.
        if (!$sent) $this->error = $decoded['message'] ?? 'Acme refused the message.';

        return $isthis ? $this : $sent;
    }

    public function controller_save(): array
    {
        $from   = (string) Filter::init("POST/from", "email");
        $fname  = (string) Filter::init("POST/fname", "hclear");
        $apiKey = (string) Filter::init("POST/api_key", "password");

        if (!$from) throw new Exception($this->lang['error-from-required'] ?? 'Sender address is required.');

        $sets = [];
        if ($from !== ($this->config['from'] ?? '')) $sets['from'] = $from;
        if ($fname !== ($this->config['fname'] ?? '')) $sets['fname'] = $fname;

        // Form kayıtlı anahtar için bir maske gösterir; maske gerçek değerin üzerine asla yazmamalıdır.
        if ($apiKey !== '*****' && $apiKey !== Crypt::decode($this->config['api_key'] ?? '', Config::get("crypt/user")))
            $sets['api_key'] = Crypt::encode($apiKey, Config::get("crypt/user"));

        if ($sets) {
            $merged = array_replace_recursive($this->config, $sets);
            $write  = FileManager::file_write(__DIR__ . DS . "config.php", Utility::array_export($merged, ['pwith' => true]));
            if (!$write) throw new Exception('Failed to save settings');
        }

        $status  = (bool) (int) Filter::init("POST/status", "numbers");
        $current = Config::get("modules/mail") == __CLASS__;
        if ($current != $status) {
            $modules         = Config::get("modules");
            $modules['mail'] = $status ? __CLASS__ : 'none';
            Config::save("modules", Config::set("modules", $modules));
        }

        return ['status' => "successful", 'message' => $this->lang['settings-save-successful'] ?? 'Saved'];
    }
}
```

```php
// Modül adı verilmeden çağrılır, Load ETKİN sürücüyü modules/mail içinden çözer.
Modules::Load("Mail");
$mailModule = Config::get("modules/mail");
$mail = $mailModule && $mailModule !== 'none' ? new $mailModule() : false;

// Alıcı başına bir geçiş. Sıraya dikkat: body, subject, attachments, address, submit.
foreach ($adminContacts['emails'] as $address => $nameStr) {
    $parse    = explode("|", (string) $nameStr);
    $aLang    = $parse[1] ?? $localLang;
    $sendMail = $mail->body($body, $templatePath, $variables, $aLang);

    if ($subject) $mail->subject($subject);
    if ($attachments) foreach ($attachments as $fn => $fname) $sendMail->addAttachment($fn, $fname);

    $sendMail = $sendMail->addAddress($address, $parse[0] ?? '')->submit();

    if ($sendMail) LogManager::Mail_Log(0, $reason, $mail->getSubject(), $mail->getBody(), implode(",", $mail->getAddresses()));
    else $errors['mail'][$address] = $mail->error;
}
```

Bu biçimi üç çağrı noktası kullanır: şablon dağıtıcısı, kuyruk işçisi ve toplu gönderici.

## Tuzaklar

> **subject() alıcı listesini temizler, bu gereklidir**
> 
> Hazır dört sürücünün hepsi `subject()` ilk satırında `address_reset()` çağırır. Dağıtıcı alıcı başına aynı örneği kullanır; bu sıfırlama olmazsa ikinci alıcı birinciye de adreslenmiş bir kopya alır.

> **submit() hatayı fırlatarak değil false döndürerek bildirir**
> 
> Dağıtım döngüsü try/catch içinde değildir ve yanlış değer dönen çağrıdan sonra `$mail->error` alanını okur. `submit()` içinden fırlatmak döngüyü keser ve kalan alıcıları düşürür. `controller_*` metotları tam tersidir: orada fırlatabilirsiniz.

> **Sırrı geçirgen filtreyle okuyun**
> 
> Diğer her filtre, API anahtarını güçlü kılan karakterleri kaldırır. Form kayıtlı anahtarı beş yıldız olarak gösterir; bu literali atlayan bir kaydetme, çalışan anahtarın üzerine maskeyi yazar.

> **Sürücünüzü etkinleştirmek öncekini devre dışı bırakır**
> 
> `modules/mail` tek bir sınıf adı tutar. SampleMail her şeyi kabul eder, mesajı `temp/sample-mail/` altına EML olarak yazar ve yerel kısmı `fail` ile başlayan alıcı için hata simüle eder.

## İlgili Makaleler

- [Bildirim Şablonları Nasıl Çalışır](https://dev.wisecp.com/tr/bildirim-sablonlari-nasil-calisir)
- [E-posta Şablonu Yazma](https://dev.wisecp.com/tr/e-posta-sablonu-yazma)
- [SMS Modülü Yazma](https://dev.wisecp.com/tr/sms-modulu-yazma)
- [Modül Yapılandırması](https://dev.wisecp.com/tr/modul-yapilandirmasi)
- [Modül Anatomisi](https://dev.wisecp.com/tr/modul-anatomisi)
- [Admin Form Oluşturucu](https://dev.wisecp.com/tr/admin-form-olusturucu)
