yasserstudio/perfex-crm-skills · Archived

perfex-email

Use whenever the user is sending, rendering, or debugging transactional email in a Perfex CRM module — `$this->emails_model->send_simple_email`, `send_mail_template`, email template files under `views/emails/`, admin-recipient fallback chains (`my_module_admin_email` → `contact_form_notification_email` → `smtp_email`), retry queues with exponential backoff stored in `tbl<module>_email_retries`, or cron-driven retry processing via `after_cron_run`. Also trigger when the user says "my Perfex emai…

First seen Apr 22, 2026

Installation

$ npx skills add yasserstudio/perfex-crm-skills --skill perfex-email

Summary

  • Use whenever the user is sending, rendering, or debugging transactional email in a Perfex CRM module — `$this->emails_model->send_simple_email`, `send_mail_template`, email template files under `views/emails/`, admin-recipient fallback chains (`my_module_admin_email` → `contact_form_notification_email` → `smtp_email`), retry queues with exponential backoff stored in `tbl<module>_email_retries`, or cron-driven retry processing via `after_cron_run`.
  • Also trigger when the user says "my Perfex email isn't sending", "send_simple_email returns false", "email failed but the user saw a success page", "SMTP error in my module", "email retry queue", "why didn't my notification email go out", or "email template merge fields".
  • Reinforces the rule that email failure must never break the user flow — always try/catch and enqueue on failure.

Stronger alternatives

This repository is archived — consider an actively maintained alternative.

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from yasserstudio/perfex-crm-skills.

npx skills add yasserstudio/perfex-crm-skills

Browse all from yasserstudio/perfex-crm-skills

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Repository health

Stars 3
License LICENSE
Default branch main
Open issues 0
Status Archived

Skill metadata

Parsed from SKILL.md frontmatter.

Version1.4.0
LicenseMIT
More metadata
author
yasserstudio
version
1.4.0

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 11,989 B
  • docs SUMMARY.md 865 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 7 installs

SKILL.md

Perfex Email System

You are a Perfex CRM email engineer. Your job is to send transactional email reliably from inside modules — using sendsimpleemail correctly, rendering email-client-safe templates, falling back gracefully when admin recipients aren't configured, and queueing retries for transient SMTP failures so the user flow never breaks.

Perfex has an email templates system (Setup → Email Templates) and a simple-send helper for ad-hoc messages. For module-owned emails that don't need user-editable templates, sendsimpleemail is the right primitive.

The 3 send paths

Primitive When to use
$this->emailsmodel->sendsimple_email($to, $subject, $body) Module-owned emails, admin notifications
sendmailtemplate('slug', ...) User-editable templates registered via registermergefields
Raw $this->email (CI library) Don't. Use one of the above.

sendsimpleemail pattern

public function notify_admin($contact_id, $event) {
    $this->load->model('emails_model');

    $to = $this->admin_email();
    if (!$to) return;

    $data = [
        'contact_name' => get_contact_full_name($contact_id),
        'event'        => $event,
    ];

    $body = $this->_render_email_template('admin_notification', $data);

    try {
        $sent = $this->emails_model->send_simple_email(
            $to,
            _l('my_module_admin_notification_subject'),
            $body
        );
        if (!$sent) {
            $this->enqueue_email_retry($to, $subject, $body, $data);
        }
    } catch (Throwable $e) {
        log_message('error', 'my_module: send failed: ' . $e->getMessage());
        $this->enqueue_email_retry($to, $subject, $body, $data);
    }
}

Email failures MUST NOT fail the user flow. Wrap in try/catch, log the error, continue. The user already completed their action — don't punish them because SMTP blipped.

Admin-recipient fallback chain

When sending "notify the admin", fall back through configured options:

private function admin_email() {
    return get_option('my_module_admin_email')
        ?: get_option('contact_form_notification_email')
        ?: get_option('smtp_email')
        ?: null;
}

Remember: get_option('key') ?: fallback — never pass a default as a second arg.

Render email template with merge

Put templates in modules/my_module/views/emails/. Use inline styles and table layout — Gmail strips <style> blocks, <div> grids break in Outlook.

private function _render_email_template($template, array $data = []) {
    extract($data);
    ob_start();
    include(__DIR__ . '/../views/emails/' . $template . '.php');
    return ob_get_clean();
}

Template example (views/emails/admin_notification.php):

<table width="100%" cellpadding="0" cellspacing="0" style="background:#f4f4f4;padding:24px;">
    <tr><td align="center">
        <table width="600" cellpadding="0" cellspacing="0" style="background:#fff;border-radius:8px;padding:32px;font-family:Arial,sans-serif;">
            <tr><td>
                <h1 style="margin:0 0 16px 0;font-size:20px;color:#111;">
                    New event from <?= htmlspecialchars($contact_name) ?>
                </h1>
                <p style="margin:0;color:#333;line-height:1.5;">
                    <?= htmlspecialchars($event) ?>
                </p>
            </td></tr>
        </table>
    </td></tr>
</table>

Always htmlspecialchars() user data. A newline in a name isn't a security issue but a <script> tag in one is.

Retry queue pattern

SMTP can transiently fail. Don't drop the email — enqueue it:

Schema

CREATE TABLE `tblmymodule_email_retries` (
    `id`            INT NOT NULL AUTO_INCREMENT,
    `to_email`      VARCHAR(191) NOT NULL,
    `subject`       VARCHAR(255) NOT NULL,
    `body`          LONGTEXT NOT NULL,
    `context_json`  TEXT NULL,
    `attempts`      TINYINT NOT NULL DEFAULT 0,
    `last_error`    TEXT NULL,
    `next_try_at`   DATETIME NOT NULL,
    `created_at`    DATETIME NOT NULL,
    PRIMARY KEY (`id`),
    KEY `idx_next_try` (`next_try_at`, `attempts`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

Enqueue

private function enqueue_email_retry($to, $subject, $body, $context = []) {
    $this->db->insert(db_prefix() . 'mymodule_email_retries', [
        'to_email'     => $to,
        'subject'      => $subject,
        'body'         => $body,
        'context_json' => json_encode($context),
        'attempts'     => 0,
        'next_try_at'  => date('Y-m-d H:i:s', strtotime('+5 minutes')),
        'created_at'   => date('Y-m-d H:i:s'),
    ]);
}

Process via cron

Register a Perfex cron hook:

// module_name.php
hooks()->add_action('after_cron_run', 'my_module_process_email_retries');

function my_module_process_email_retries() {
    $CI =& get_instance();
    $CI->load->model('my_module/my_module_model');
    $CI->my_module_model->process_email_retries();
}

In the model:

public function process_email_retries() {
    $this->load->model('emails_model');

    // Prune > 7 days old
    $this->db->where('created_at <', date('Y-m-d H:i:s', strtotime('-7 days')));
    $this->db->delete(db_prefix() . 'mymodule_email_retries');

    // Grab up to 50 due, attempts < 5
    $this->db->where('next_try_at <=', date('Y-m-d H:i:s'));
    $this->db->where('attempts <', 5);
    $this->db->order_by('next_try_at', 'ASC');
    $this->db->limit(50);
    $queue = $this->db->get(db_prefix() . 'mymodule_email_retries')->result();

    foreach ($queue as $row) {
        try {
            $sent = $this->emails_model->send_simple_email($row->to_email, $row->subject, $row->body);
            if ($sent) {
                $this->db->where('id', $row->id)->delete(db_prefix() . 'mymodule_email_retries');
            } else {
                $this->_bump_retry($row, 'send_simple_email returned false');
            }
        } catch (Throwable $e) {
            $this->_bump_retry($row, $e->getMessage());
        }
    }
}

private function _bump_retry($row, $error) {
    $next_attempt = $row->attempts + 1;
    // Exponential backoff: 5min, 15min, 1h, 6h, 24h
    $backoffs = [5, 15, 60, 360, 1440];
    $minutes = $backoffs[min($next_attempt - 1, count($backoffs) - 1)];
    $this->db->where('id', $row->id)->update(db_prefix() . 'mymodule_email_retries', [
        'attempts'    => $next_attempt,
        'last_error'  => $error,
        'next_try_at' => date('Y-m-d H:i:s', strtotime("+{$minutes} minutes")),
    ]);
}

Cap at 5 attempts. After that, alert the admin (another email!) or leave for manual review via the prune-at-7-days rule.

Email language fallback

Perfex resolves which language to use for email templates in this order:

  1. Customer language — if the recipient is a customer/contact, uses their profile language
  2. Staff language — if the recipient is staff, uses their configured language
  3. Lead language — leads use the system default language
  4. System default — fallback when no recipient-specific language is set

If a template is empty for the resolved language, Perfex falls back to the system default language. If that's also empty, the email is skipped (not sent with a blank body).

Templates can be disabled per-language in Setup → Email Templates — a disabled template for a specific language means that notification won't fire for users configured to that language.

SMTP configuration check

If a dev environment has no SMTP configured, sendsimpleemail silently returns false. In a module's activation hook, warn:

if (!get_option('smtp_host')) {
    set_alert('warning', 'My Module: SMTP is not configured. Emails will queue but never send.');
}

Common SMTP pitfalls

The most common reason sendsimpleemail returns false or throws has nothing to do with your code — it's SMTP configuration. Know these before you debug application logic:

1. smtphost / smtpport mismatch gives misleading errors

The Setup → Settings → Email form accepts any value. If smtp_host is right but port is wrong, CI's email library throws generic "SMTP connection failed" errors that look like network problems. Always verify host/port against the provider's docs (Gmail: 587 TLS, Office365: 587 TLS, AWS SES: 587 TLS on regional endpoint). Port 25 almost never works on shared hosting — blocked outbound.

2. Enable mail_debug during initial setup

In application/config/email.php:

$config['mail_debug'] = TRUE;

This surfaces the actual SMTP conversation. Disable again before going live — debug output leaks into error responses if send fails mid-flow.

3. Gmail / Workspace: DMARC rejects From: spoofing

If you set smtp_email = [email protected] but From: on the message is [email protected], Gmail's DMARC policy rejects the send entirely. Two fixes:

  • Set From: to match the authenticated SMTP account, OR
  • Use a provider (SendGrid, AWS SES, Postmark) with domain-authenticated DKIM for [email protected]

Perfex's admin notifications use smtp_email as From: by default — which is why Gmail-based installs sometimes silently stop sending admin emails after a domain config change.

4. $this->email->print_debugger() is your friend

Inside a try/catch around sendsimpleemail:

try {
    $sent = $this->emails_model->send_simple_email($to, $subject, $body);
    if (!$sent) {
        log_message('error', 'email failed: ' . $this->email->print_debugger(['headers']));
    }
} catch (Throwable $e) { /* ... */ }

print_debugger() returns headers + SMTP response codes — the actual reason, not CI's sanitized message.

5. CI's $config['smtp_timeout'] defaults to 5 seconds

Slow providers (AWS SES cold region, mail.com, any TLS handshake over high-latency link) exceed this. Bump in application/config/email.php:

$config['smtp_timeout'] = 30;

Timeout errors look identical to auth errors in Perfex's logs — a too-short timeout is a silent root cause of "email works locally, fails in production."

6. sendsimpleemail returns false, no exception

CI's email library catches most failure modes and returns false without throwing. Always check the return value AND wrap in try/catch — some errors (bad To: syntax, PHP mail() backend misconfigured) throw, others just silently return false. Your retry-queue code should handle both.

Related skills

  • perfex-module-dev — registering the aftercronrun hook that processes the retry queue.
  • perfex-database — DDL for tbl<module>emailretries with the right column types.
  • perfex-security — never log recipient email addresses or message bodies on failure (PII + token leak risk).

Upstream docs