דוא"ל ותור שליחה

שליחת דוא"ל מהליבה: תבניות mail_tpl, mail_string, מסלולי השליחה (POOLSE, תור, ישיר), תור CRM_emails_queue, הגדרות SMTP, poolse_client ומלכודות אבטחה.

⏱ 9 דק' קריאה 1653 מילים ערוך דף זה ב-GitHub

הליבה שולחת דוא"ל דרך ספריית 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, $replyNameReply-To
$subject, $msgנושא וגוף (גולמיים, מעובדים בשליחה)
$arrVarsמשתני התבנית (דרך set_var)
$tplתבנית העטיפה, מתוך הפרמטר mail_tpl
$priorityעדיפות בתור, ברירת מחדל 1
$skip_queuetrue עוקף את התור גם כש-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() בוחרת מסלול לפי הסדר הבא:

  1. POOLSE. אם יש נמען אחד, אין cc ו-bcc, הפרמטר poolse_mail אינו "0", התבנית לא מופיעה ברשימת poolse_mail_exclude, ו-poolse_client::active() נכון: ההודעה נשלחת ל-POOLSE דרך poolse_client::transactional. אם הצליח, נרשמת שורת לוג בתור (is_sent = 1) ונגמר.
  2. תור. אם CONFIG::$mail_queue פעיל ו-$skip_queue כבוי: ההודעה נכנסת ל-CRM_emails_queue ונשלחת מאוחר יותר על ידי משימת cron.
  3. ישיר. אחרת: 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מזהה
dataserialize של אובייקט PHPMailer המוכן לשליחה
mail_tplה-sysName של התבנית, ללוג
priorityעדיפות, מיון עולה
pidמזהה תהליך, אופציונלי
is_sent0 ממתין, 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 ותדירות של כל דקה או שתיים (ראו משימות מתוזמנות).

בכל הרצה:

  1. בתוך טרנזקציה: בוחרת עד 250 שורות עם is_sent = 0 ו-send_date <= NOW(), בסדר priority ואז send_date, עם FOR UPDATE.
  2. מסמנת אותן כנשלחות.
  3. לכל שורה: unserialize וקריאה ל-send(). הצלחה נרשמת ב-log, כישלון מסומן error ולא ינוסה שוב.
  4. מוחקת שורות עם is_sent = 1 שגילן מעל 24 שעות.
לא מנסה שוב, ואין אימות

הודעה שנכשלה נרשמת כ-error ולא נשלחת שוב אוטומטית. בנוסף, Tools::cron_emails_queue זמין גם כ-URL רגיל בלי טוקן, כך שכל מבקר יכול להפעיל אותו. חסמו אותו ברמת השרת. ראו משימות מתוזמנות והקשחה.

סיסמת SMTP ותוכן ההודעות בטבלה

האובייקט שנשמר ב-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 מחוץ לקוד.
  • בדיקה אחת עם כתובת שלכם לפני כל שימוש חדש.

ראו גם#

מצאתם טעות או חוסר? תקנו את הדף או פתחו Issue בריפו. התיעוד נכתב מתוך הקוד של ליבה 5.0.115.