הליבה שולחת דוא"ל דרך ספריית mail_tpl, שנשענת על PHPMailer 6.9.1 (api/core/addons/PHPMailer). התבניות נערכות בפאנל הניהול, וכל שליחה עוברת באחד משלושה מסלולים: שירות POOLSE, תור במסד הנתונים או שליחה ישירה. בעמוד הזה תראו איך שולחים הודעה, איך נבחר המסלול, איך מפעילים תור ואילו מלכודות יש.
system/ בעמוד הזה הוא תיקיית הליבה הפרוסה (api/core בריפו), ו-{admin} הוא CONFIG::$admin_url.כל קריאה ל-send() שולחת דוא"ל אמיתי אם האתר מוגדר לכך. אל תריצו קוד שליחה בסקריפטים, בבדיקות או בדרכי קוד של production בלי כוונה. בפיתוח השתמשו בכתובת שלכם בלבד.
תבניות: CRM_mail_tpl#
תבניות ההודעות נשמרות בטבלת CRM_mail_tpl ונערכות בפאנל {admin}/MailTpl:
| עמודה | משמעות |
|---|---|
id, name | מזהה ושם תצוגה |
sysName | המפתח שבו הקוד פונה לתבנית |
from, fromName | כתובת ושם השולח. כש-from ריק, ברירת המחדל היא noreply@<CONFIG::$base_domain> |
cc, bcc | רשימות כתובות מופרדות בפסיק או בנקודה-פסיק |
replay | כתובת Reply-To (כך נקראת העמודה בסכמה) |
subject | נושא |
msg | גוף ההודעה ב-HTML |
הגוף, הנושא ו-fromName עוברים עיבוד Smarty (FILES::string_template) עם המשתנים שהגדרתם ב-set_var. בתבנית כותבים {$name}, {if} ו-{foreach} כמו בכל תבנית אתר (ראו תבניות Smarty).
אם הפרמטר mail_tpl ב-CRM_params מכיל נתיב של תבנית HTML, הגוף הסופי עובר דרכה כעטיפה. בעטיפה זמינים {$content} (הגוף שעובד), {$to} וכל המשתנים שהוגדרו ב-set_var. ראו פרמטרים ושפות לעריכת פרמטרים.
מכיוון שהתבנית עוברת Smarty מלא, מי שמורשה לערוך תבניות דוא"ל יכול להגיע לכוח שקרוב להרצת קוד. הגבילו את ההרשאה לפאנל MailTpl למשתמשים שסומכים עליהם.
שליחת הודעה#
$this->load->library("mail_tpl");
$m = new mail_tpl("order_confirm"); // sysName של שורה ב-CRM_mail_tpl
$m->set_to("client@example.co.il"); // אפשר לקרוא כמה פעמים; clear_to() מאפס
$m->set_var("name", $order["client_name"]); // {$name} בתבנית
$m->set_var("items", $order["items"]); // {foreach $items as $it} ... {/foreach}
$m->priority = 1; // עדיפות בתור (נמוך = קודם)
$m->attach_url = [["url" => $pdf_path, "name" => "invoice.pdf"]];
$m->reply = "sales@example.co.il";
$m->replyName = "מכירות";
$ok = $m->send(); // true אם נשלח או נכנס לתור
הערך שחוזר מ-send() הוא true כשההודעה נמסרה או נכנסה לתור. ה-constructor לא בודק שהתבנית קיימת: עבור sysName לא מוכר תקבלו אזהרות PHP והודעה ריקה. אם השם מגיע מקלט דינמי, ודאו קודם עם mail_tpl::get_tpl_data($sysName), שמחזיר false כשאין שורה.
מאפיינים ציבוריים#
| מאפיין | תיאור |
|---|---|
$to | מערך נמענים, ממולא עם set_to |
$cc, $bcc | מערכים. מתמלאים מהתבנית, אפשר להוסיף ידנית |
$from, $fromName | השולח. מתמלאים מהתבנית |
$reply, $replyName | Reply-To |
$subject, $msg | נושא וגוף (גולמיים, מעובדים בשליחה) |
$arrVars | משתני התבנית (דרך set_var) |
$tpl | תבנית העטיפה, מתוך הפרמטר mail_tpl |
$priority | עדיפות בתור, ברירת מחדל 1 |
$skip_queue | true עוקף את התור גם כש-CONFIG::$mail_queue פעיל |
$attach_url | נתיב קובץ מקומי, מערך נתיבים, או מערך של ["url" => ..., "name" => ...] |
$ical | טקסט iCalendar (BEGIN:VCALENDAR...). נשלח כחלק text/calendar ולא כקובץ מצורף, כך שתוכנות דוא"ל מציגות כרטיס אישור הגעה |
$headers | מערך כותרות מותאמות (ראו אזהרה למטה) |
מתודות#
| מתודה | תיאור |
|---|---|
__construct($sysName) | טוען את התבנית ואת cc, bcc, from ו-replay ממנה |
set_to($email) | מוסיף נמען |
clear_to() | מרוקן את הנמענים |
set_var($key, $val) | מגדיר משתנה לתבנית |
send() | עיבוד, בחירת מסלול ושליחה. מחזיר bool |
static get_tpl_data($sysName) | שורת התבנית מהטבלה, או false |
set_header שבור
set_header($key, $val) קורא ל-$this->headers(...) כאילו הייתה פונקציה, ולכן יוצר שגיאה. הגדירו כותרות ישירות במאפיין: $m->headers["X-Source"] = "orders";. ב-send() כל זוג מועבר ל-addCustomHeader.הודעה חופשית: mail_string#
כשאין שורה בטבלה, mail_string מקבלת את הגוף ישירות:
$s = new mail_string("<p>שלום {\$name}</p>");
$s->subject = "עדכון";
$s->set_var("name", $user["name"]);
$s->set_to($user["email"]);
$s->send();
mail_string יורשת את כל המתודות של mail_tpl, והגוף שלה עובר גם הוא דרך Smarty. לכן לעולם אל תשרשרו קלט משתמש לתוך המחרוזת: העבירו אותו דרך set_var, כך שהוא ערך ולא קוד תבנית.
מסלולי השליחה#
send() בוחרת מסלול לפי הסדר הבא:
- POOLSE. אם יש נמען אחד, אין
ccו-bcc, הפרמטרpoolse_mailאינו"0", התבנית לא מופיעה ברשימתpoolse_mail_exclude, ו-poolse_client::active()נכון: ההודעה נשלחת ל-POOLSE דרךpoolse_client::transactional. אם הצליח, נרשמת שורת לוג בתור (is_sent = 1) ונגמר. - תור. אם
CONFIG::$mail_queueפעיל ו-$skip_queueכבוי: ההודעה נכנסת ל-CRM_emails_queueונשלחת מאוחר יותר על ידי משימת cron. - ישיר. אחרת:
PHPMailer::send()מיידי, ואחריו שורת לוג בתור.
אם POOLSE נכשל או לא חל (קובץ שלא מתאים, שירות שלא זמין), המערכת ממשיכה למסלול 2 או 3 ולא מאבדת את ההודעה.
פרטים על מסלול POOLSE:
- הכתובת שולחת מהדומיין המאומת של חשבון POOLSE. הערך
$fromשל ההודעה הופך ל-Reply-To (או$replyאם הוגדר). - קבצים מצורפים עוברים רק מנתיב מקומי, עד 5 קבצים, עד 2MB לקובץ ו-3MB בסך הכל, ובסיומות מרשימה מותרת (pdf, ics, csv, txt, תמונות, מסמכי Office ועוד). קובץ שלא עומד בכך, או כתובת URL מרוחקת, מעביר את ההודעה כולה למסלול הרגיל עם הקובץ.
$icalעובר כחלקcalendarשל ה-API.- לכבות POOLSE לאתר:
poolse_mail = 0ב-CRM_params. להחזיק תבנית אחת מחוץ ל-POOLSE:poolse_mail_excludeעם רשימתsysNameמופרדת בפסיק.
הגדרות SMTP#
ללא הגדרה, PHPMailer שולח עם mail() של PHP. להגדרת SMTP מוסיפים ל-CONFIG_USER בקובץ ההגדרות של האתר (ראו הגדרות):
static $mail_is_smtp = true; // חייב להיות true בדיוק (===)
static $mail_smtp_host = "smtp.example.com";
static $mail_smtp_port = 587;
static $mail_smtp_auth = true;
static $mail_smtp_username = "mailer@example.com";
static $mail_smtp_password = ""; // מהכספת או ממשתנה סביבה, לא בקוד
static $mail_smtp_secure = "tls"; // "tls" או "ssl"
static $mail_queue = false; // true = שליחה דרך התור
הליבה לא מצהירה על המפתחות האלה. אתר שמשתמש בדוא"ל חייב להצהיר עליהם, גם כשהם false, כי קריאה למאפיין סטטי לא מוצהר היא שגיאה קטלנית ב-PHP 8.
התור: CRM_emails_queue#
| עמודה | משמעות |
|---|---|
id | מזהה |
data | serialize של אובייקט PHPMailer המוכן לשליחה |
mail_tpl | ה-sysName של התבנית, ללוג |
priority | עדיפות, מיון עולה |
pid | מזהה תהליך, אופציונלי |
is_sent | 0 ממתין, 1 נשלח או נוסה |
log | תוצאה: 1, תוצאת השליחה, או error |
send_date | מתי לשלוח. ברירת המחדל מגיעה מהעמודה בסכמה (CURRENT_TIMESTAMP) |
MISC::add_mail_to_queue($mail, $priority = 1, $mail_tpl = "", $is_sent = false, $pid = false) כותב לשם. עם $is_sent = true השורה היא רשומת לוג בלבד (log = 1, send_date עכשיו).
הפעלת התור#
התור נשלח על ידי Tools::cron_emails_queue, ואינו נרשם אוטומטית. כשמפעילים CONFIG::$mail_queue, יוצרים משימה ב-{admin}/cron_manager עם url = system/Tools/cron_emails_queue ותדירות של כל דקה או שתיים (ראו משימות מתוזמנות).
בכל הרצה:
- בתוך טרנזקציה: בוחרת עד 250 שורות עם
is_sent = 0ו-send_date <= NOW(), בסדרpriorityואזsend_date, עםFOR UPDATE. - מסמנת אותן כנשלחות.
- לכל שורה:
unserializeוקריאה ל-send(). הצלחה נרשמת ב-log, כישלון מסומןerrorולא ינוסה שוב. - מוחקת שורות עם
is_sent = 1שגילן מעל 24 שעות.
הודעה שנכשלה נרשמת כ-error ולא נשלחת שוב אוטומטית. בנוסף, Tools::cron_emails_queue זמין גם כ-URL רגיל בלי טוקן, כך שכל מבקר יכול להפעיל אותו. חסמו אותו ברמת השרת. ראו משימות מתוזמנות והקשחה.
האובייקט שנשמר ב-data הוא PHPMailer שכבר הוגדר, כולל Username ו-Password של ה-SMTP וגוף ההודעה המלא. זה נכון לכל מסלול, לא רק לתור: גם שליחה ישירה ושליחה דרך POOLSE כותבות שורת לוג עם אותו אובייקט. סיסמת ה-SMTP והתוכן נשמרים אפוא בטקסט גלוי ב-CRM_emails_queue. הגבילו גישה לטבלה, אל תשתפו גיבויים שלה, ותנו לסיסמה הרשאות מינימליות אצל ספק ה-SMTP.
מחיקת השורות הישנות (מעל 24 שעות) נעשית רק בתוך Tools::cron_emails_queue. אתר שלא מריץ אותה, גם כש-$mail_queue כבוי, צובר שורות לוג בלי סוף. מומלץ להריץ אותה כמשימה גם באתר בלי תור.
poolse_client#
libraries/poolse_client.php הוא לקוח ל-POOLSE, שירות דוא"ל, קמפיינים ואוטומציות של המרקט. כל המתודות סטטיות, לא זורקות חריגות, ומחזירות false או ["error" => ...] בכישלון, כדי שתקלה בשירות לא תפיל את האתר.
$this->load->library("poolse_client");
if (poolse_client::active()) {
poolse_client::subscribe(["email" => $email, "first_name" => $name, "list_ids" => [1]]);
poolse_client::event($email, "purchase_completed", ["order_total" => 250]);
}
| מתודה | תיאור |
|---|---|
active() | האם POOLSE פעיל: הפרמטר poolse_api_key מלא, או שהשירות wizzopoolse מופעל בחשבון המרקט |
api_key(), base_url() | מפתח וכתובת. פרמטר מפורש גובר על הפעלת המרקט. הכתובת נקבעת ב-poolse_api_url |
subscribe($data) | הוספה או עדכון של איש קשר (email חובה; first_name, last_name, phone, attributes, list_ids, tag_ids, consent) |
event($email, $event_name, $data = []) | אירוע לאוטומציה |
contact($email) | קריאת איש קשר |
unsubscribe($email, $source = "site") | הסרה מהרשימה |
list_id($name) | מזהה רשימה לפי שם |
transactional($to, $subject, $html, $opts = []) | הודעה בודדת. מחזיר ["status" => "sent", "message_id" => ...] או ["error" => ...] |
campaign($subject, $html, $list_ids, $opts = []) | יצירת קמפיין. הגוף חייב להכיל {{unsubscribe_url}} או {{footer_html}} |
post($path, $body), req($method, $path, $body = null, $timeout = 6) | קריאות ברמה נמוכה ל-API |
הקמפיין הוא פעולה שיווקית בלתי הפיכה לרשימה שלמה. הפעילו campaign רק על רשימות שיש להן הסכמה, ולעולם לא כחלק מבדיקה.
MISC::email: הפונקציה הישנה#
MISC::email($to, $from, $subj, $msg, $bcc = "") שולחת עם mail() של PHP ובונה את הכותרות בשרשור מחרוזות. היא לא מנקה שורות חדשות מהערכים, ולכן ערך כמו $from שמגיע מקלט משתמש מאפשר הזרקת כותרות. העדיפו תמיד mail_tpl, ואם אתם קוראים לפונקציה, ודאו שאף ערך בה לא מכיל \r או \n.
צ'קליסט לאתר חדש#
- שורת תבנית ב-
CRM_mail_tplלכל הודעה, עםsysNameיציב. fromמדומיין שיש לו SPF, DKIM ו-DMARC, או POOLSE שמטפל בזה.- החלטה: POOLSE, תור או ישיר. אם תור: משימת cron לכל דקה והגנה על ה-URL שלה.
- סיסמת SMTP מחוץ לקוד.
- בדיקה אחת עם כתובת שלכם לפני כל שימוש חדש.