Appearance
Email Dispatch System (Adv_mailer)
Flow ID: SY-24 | Module(s): eshop, core, forms, blog, checkout, gift_cards, job | Complexity: High Last Updated: 2026-04-06
Business Overview
The email dispatch system is the platform's centralized outbound email infrastructure. All transactional, notification, and marketing emails flow through Adv_mailer -- a CI model that orchestrates template resolution, data binding, SMTP configuration from the registry, sending via PHPMailer, and customer email history tracking. The system serves 7+ distinct business flows across the platform.
Key capabilities:
- Customer-facing emails: order confirmations, status updates, password resets, waiting list alerts, review prompts, birthday wishes, loyalty reminders, gift cards, blog/review moderation notifications
- Admin-facing emails: new order alerts, low stock warnings, new product notifications, certificate expiration alerts
- Contact form emails: config-driven dynamic forms with attachments
- Localized content via
ExternalLangtranslation layer with per-customer language resolution - Customer email history tracking in
shop_customer_message_history - Invalid email filtering to prevent sending to sanitized/synthetic addresses
- Two SMTP profiles (
EMAIL_SHOP_MAILER,EMAIL_CONTACT_FORM) with full credential management via registry - Client-overridable at every layer: mailer model, email templates, job classes
Architecture
Callers (controllers, jobs, models)
|
v
Adv_mailer (application/models/Adv_mailer.php)
|
+-- Template resolution: Template::layoutView($key) --> emailViews.json --> view path
+-- Data binding: CI view loader renders PHP template with data + ExternalLang
+-- Subject resolution: Registry::value('EMAIL_SUBJECTS', $key, $lang)
+-- SMTP config: getDefaultSmtpCredentials($purpose) --> registry group
+-- History: addEmailToCustomerHistory() --> shop_customer_message_history
+-- Invalid filter: strpos($mail, config 'order.invalid.email')
|
v
send() / sendAdmin() / sendContactFormEmail()
|
v
doSend() -- assembles and dispatches
|
v
Mailer (ecommercen/core/Mailer.php) -- PHPMailer wrapper
|
v
PHPMailer::send() -- SMTP deliveryUnderlying library: The email transport layer uses PHPMailer (phpmailer/phpmailer Composer package). The Mailer class at ecommercen/core/Mailer.php extends PHPMailer\PHPMailer\PHPMailer directly and configures SMTP credentials from registry values at send time. All platform email ultimately dispatches through this wrapper.
Key Components
| Component | Path | Role |
|---|---|---|
Adv_mailer | application/models/Adv_mailer.php | Central email orchestration model |
Mailer | ecommercen/core/Mailer.php | PHPMailer wrapper (SMTP send) |
Template | src/Template/Template.php | Email view path resolver from JSON config |
Config | src/Template/Config.php | JSON config loader for template definitions |
ExternalLang | ecommercen/libraries/ExternalLang.php | Localized string resolver for email content |
emailViews.json | application/config/emailViews.json | Template-to-view mapping registry |
registry_helper | ecommercen/helpers/registry_helper.php | getDefaultSmtpCredentials() function |
AdvEmailViewer | ecommercen/settings/controllers/AdvEmailViewer.php | Admin: email template previewer and subject editor |
| Email views | application/views/main/mail/*.php | HTML email templates (23 templates — see AD-53) |
Internal API: Adv_mailer Methods
Customer-Facing Methods
| Method | Trigger | Template Key | SMTP Profile | Subject Key | Attachments |
|---|---|---|---|---|---|
order_complete($data) | Checkout success (all payment gateways) | orderCreated | EMAIL_SHOP_MAILER | ORDER_COMPLETE (%s = serial) | No |
order_has_been_updated($orderId, $data) | Order status change jobs | orderUpdate | EMAIL_SHOP_MAILER | ORDER_UPDATE (%s = serial) | Invoice PDF (for INVOICED/SENT/PAID_SENT) |
order_is_on_store($orderId) | Store-pickup ready job | orderOnStore | EMAIL_SHOP_MAILER | ORDER_ON_STORE (%s = serial) | Invoice PDF (conditional) |
recover_password_mail($email) | Customer password reset | resetPassword | EMAIL_SHOP_MAILER | PASSWORD_RESET | No |
sendEmailForWaitingList($email, $data, $lang) | Waiting list job | waitingListSuccess | EMAIL_SHOP_MAILER | WAITING_LIST | No |
sendCustomerPromptReview($customer, $key) | Review reminder job | reviewForFacebook / reviewForSkroutz / reviewForGoogle | EMAIL_SHOP_MAILER | REVIEW_FOR_FACEBOOK / REVIEW_FOR_SKROUTZ / REVIEW_FOR_GOOGLE | No |
sendCustomerInformDelay($order) | Delivery delay job | orderInformDelay | EMAIL_SHOP_MAILER | CUSTOMER_INFORM_DELAY (%s = serial) | No |
sentRemainingPoints($customerId) | Remaining points job | remainingPoints | EMAIL_SHOP_MAILER | REMAINING_POINTS | No |
sentBirthdayWishes($customerId, $points) | Birthday job | birthdayWishes | EMAIL_SHOP_MAILER | BIRTHDAY_WISHES (%s = name) | No |
sendGiftCardToEmail($order) | Gift card job | giftCard | EMAIL_SHOP_MAILER | GIFT_CARD | No |
sendGiftCardToInformCustomer($order) | Gift card job | giftCardInformCustomer | EMAIL_SHOP_MAILER | GIFT_CARD_INFORM_CUSTOMER | No |
sendUserReviewStatusUpdateEmail($customer, $status) | Admin review moderation | productReviewAccept / productReviewReject | EMAIL_CONTACT_FORM | USER_REVIEW_ACCEPT / USER_REVIEW_REJECT | No |
sendBlogCommentStatusUpdateEmail($blog, $status) | Admin blog comment moderation | blogCommentAccept / blogCommentReject | EMAIL_CONTACT_FORM | BLOG_COMMENT_ACCEPT / BLOG_COMMENT_REJECT | No |
Admin-Facing Methods
| Method | Trigger | Template Key | SMTP Profile | Notes |
|---|---|---|---|---|
sendAdmin() (via order_complete) | Order placed | notifyAdmin | EMAIL_SHOP_MAILER | Sent to admin_email from SMTP config |
order_with_low_stock($products) | Checkout low stock check | notifyAdminLowStock | EMAIL_SHOP_MAILER | Gated by EMAIL.NOTIFICATION_LOW_STOCK registry |
inform_new_product($productId) | Product creation in admin | notifyAdminNewProduct | EMAIL_SHOP_MAILER | Gated by EMAIL.NOTIFICATION_NEW_PRODUCT registry |
sendExpiringCertificate($data) | CDN certificate check job | N/A (raw ExternalLang) | EMAIL_SHOP_MAILER | Bypasses template system; uses doSend() directly |
Contact Form Method
| Method | Trigger | Template | SMTP Profile | Notes |
|---|---|---|---|---|
sendContactFormEmail($msg, $subject, $attachments) | Form submission controller | Pre-rendered by caller | EMAIL_CONTACT_FORM | Supports file attachments; sent to admin_email |
Stub Method
| Method | Notes |
|---|---|
register_welcome($data) | Empty no-op; exists as a hook point for client implementations |
Code Flow
Core Send Pipeline: send() (Private)
The send() method is the primary internal dispatch path for customer-facing emails:
Invalid email guard: Checks if
$customerData->mailcontains the configured invalid email domain (_invalid_mail@adveshop.local). Returnsfalseimmediately if matched. This prevents emails to sanitized/synthetic addresses (e.g., Skroutz marketplace orders, GDPR-sanitized customers).Template resolution:
Template::layoutView($key)readsemailViews.jsonand resolves the view path. Example:layoutView('orderCreated')returnsmain/mail/order_created.ExternalLang initialization: Creates
new ExternalLang($customerData->lang, $this->languageAbbr)using the customer's preferred language with the site default as fallback. Loads translations fromadv_external(ecommercen) andexternal(application) language files.View rendering:
$this->load->view($pageToLoad, $data, true)renders the PHP email template with all data merged (including$externalLangfor translation calls and$emailViewsfor component includes).Customer history: If
$customerData->idis non-empty, inserts a record intoshop_customer_message_historywith the email type, subject, and full rendered HTML body.SMTP credential resolution:
getDefaultSmtpCredentials($emailPurpose)reads the registry group (EMAIL_SHOP_MAILERorEMAIL_CONTACT_FORM) and returns all SMTP connection parameters.Dispatch: Calls
doSend()which instantiatesMailer, configures SMTP, sets from/to/cc addresses, attaches files, and sends.
Core Send Pipeline: sendAdmin() (Private)
Simplified variant for admin notifications:
- Renders view with data + ExternalLang (using site default language only).
- Resolves subject from
$data['subject']or falls back to site title. - Sends to
$emailConfig['admin_email']from the SMTP profile. - No customer history tracking.
Core Send Pipeline: doSend() (Private)
The actual PHPMailer dispatch method:
- Instantiates
Mailer(PHPMailer wrapper). - Sets from address and display name from SMTP config.
- Adds recipient(s) as To addresses.
- Adds
$this->extraRecipientsas CC (if any are set by the caller). - Adds file attachments (if any).
- Configures SMTP: host, port, user, password, auto SSL/TLS.
- Sets subject and HTML message body.
- Calls
$mailer->send()which delegates to PHPMailer.
PHPMailer Wrapper: Mailer (ecommercen/core/Mailer.php)
- Wraps PHPMailer with a clean API for the platform.
- Always uses SMTP auth, UTF-8 charset, base64 encoding, HTML mode, Greek language for error messages.
- Validates all required properties before sending (host, port, user, password, from, to, subject, message).
- 5-second SMTP timeout.
- Reinitializes all state after each send (from, to, cc, bcc, message, subject) for safe sequential use.
- Logs errors via
log_message('error', ...)on failure. fromNameFailOverdefaults to the site title from registry if no from name is provided.
Template System
Resolution Flow
emailViews.json View Files
{ application/views/main/mail/
"templateFolder": "main/mail", ├── order_created.php
"layouts": { ├── order_update.php
"orderCreated": { ├── order_on_store.php
"view": "order_created" ├── ... (23 templates total)
}, └── email_products_summary.php (component)
...
},
"components": {
"emailProductsSummary": {
"view": "email_products_summary"
}
}
}Template::layoutView($key) concatenates templateFolder + / + the view value from the matching layout entry. The resulting path is passed to CI_Loader::view().
For the full template inventory (23 files in main/mail/), the default/mail/ vs main/mail/ divergence bug, the missing apologize_shipping_delay.php file, the orphan ask_us_email.php, and the complete emailViews.json layout key list, see AD-53 Email Template Viewer.
Template::componentView($key) works identically for reusable email components (e.g., emailProductsSummary used inside order emails).
Template Data Model
All email templates receive:
| Variable | Type | Source | Description |
|---|---|---|---|
$externalLang | ExternalLang | send() | Translation helper; $externalLang->line('key') for localized strings |
$emailViews | Template | addEmailViewsToRender() | For including sub-components via $emailViews->componentView() |
$company_phone | string | Registry COMPANY_PHONE.ESHOP | Company phone number (most templates) |
Additional data varies per template (order data, customer data, product lists, tokens, etc.).
ExternalLang Translation
- Loads language files from two sources (merged):
ecommercen/language/{lang}/adv_external_lang.phpandapplication/language/{lang}/external_lang.php. - Falls back to the site default language if the customer's language has no translations.
- Supports
vsprintfparameter substitution:$externalLang->line('key', [$param1, $param2]).
Data Model
Table: shop_customer_message_history
Stores rendered email content for customer-facing emails (not admin emails).
| Column | Type | Description |
|---|---|---|
id | int (PK) | Auto-increment |
user_id | int | FK to shop_customer.id |
datetime | datetime | Send timestamp |
type | varchar | Email purpose code (e.g., ORDER_COMPLETE, PASSWORD_RESET, WAITING_LIST) |
message_type | varchar | Subject text (or 'EMAIL' fallback) |
subject | varchar | Email subject line |
email_body | text | Full rendered HTML body |
Note: Only recorded when $customerData->id is non-empty. Admin emails, contact form emails, and emails to non-registered recipients (e.g., waiting list guests) skip history.
Registry Groups
EMAIL_SHOP_MAILER -- Primary SMTP Profile
| Key | Description |
|---|---|
SMTP_HOST | SMTP server hostname |
SMTP_PORT | SMTP port |
SMTP_USER | SMTP username |
SMTP_PASS | SMTP password (encrypted in registry) |
SMTP_AUTO_SSL_TLS | Auto SSL/TLS negotiation flag |
SENDER_EMAIL | From email address |
FROM_NAME | From display name |
ADMIN_EMAIL | Admin recipient for admin-facing emails |
EMAIL_CONTACT_FORM -- Contact Form SMTP Profile
Same keys as above. Allows a separate SMTP account for contact form submissions and moderation notifications.
EMAIL_SUBJECTS -- Localized Subject Lines
All subject lines are stored per-language in the registry under the EMAIL_SUBJECTS group. Some support vsprintf placeholders (%s). The full catalog of 19 keys (with default values, vsprintf signatures, and consumer line numbers) is maintained in AD-53 Email Template Viewer — Subject Registry Keys.
Key consumer mapping (for quick reference):
| Key | %s placeholder | Adv_mailer method |
|---|---|---|
ORDER_COMPLETE | order serial | order_complete() |
ORDER_UPDATE | order serial | order_has_been_updated() |
ORDER_ON_STORE | order serial | order_is_on_store() |
PASSWORD_RESET | — | recover_password_mail() |
USER_REVIEW_ACCEPT / USER_REVIEW_REJECT | — | sendUserReviewStatusUpdateEmail() |
{contact_form_key} | — | sendContactFormEmail() (dynamic per form) |
Other Registry Keys Used
| Group | Key | Purpose |
|---|---|---|
EMAIL_FORM | SITE | Reply-to email shown in email templates |
COMPANY_PHONE | ESHOP | Company phone included in most email bodies |
TITLE | SITE | Site name used as fallback from-name and in subjects |
GLOBAL | LOGO | Logo image URL used in email headers |
GLOBAL | ADMIN_EMAIL | Admin email for certificate alerts |
EMAIL | NOTIFICATION_LOW_STOCK | Feature gate for low stock admin emails |
EMAIL | NOTIFICATION_NEW_PRODUCT | Feature gate for new product admin emails |
EMAIL_FOR_BIRTHDAY | IS_ENABLED | Feature gate for birthday emails |
POINT_SYSTEM | IS_ENABLED | Feature gate for loyalty points emails |
EMAIL_FOR_TOTAL_POINTS | IS_ENABLED | Feature gate for remaining points emails |
Caller Map
Synchronous Callers (Inline During Request)
| Caller | Method Called | Trigger |
|---|---|---|
Adv_checkout (all payment success paths) | order_complete() | Order placed and payment confirmed |
Adv_checkout::informLowStock() | order_with_low_stock() | After order, if low stock detected |
Adv_customer::forgot_password() | recover_password_mail() | Customer requests password reset |
Adv_forms::contactForm() | sendContactFormEmail() | Contact form submitted |
Adv_products_admin::add() / clone() | inform_new_product() | Admin creates/clones a product |
Adv_product_reviews_admin::changeStatus() | sendUserReviewStatusUpdateEmail() | Admin approves/rejects product reviews |
Adv_blog_comments_admin::changeStatus() | sendBlogCommentStatusUpdateEmail() | Admin approves/rejects blog comments |
Asynchronous Callers (Cron Jobs)
| Job Class | Method Called | Schedule |
|---|---|---|
AdvSendEmailBasedOnSentStatus | order_has_been_updated() | Cron (INVOICED orders) |
AdvSendEmailAndSMSBasedOnSentStatus | order_has_been_updated() | Cron (SENT courier orders) |
AdvSendEmailAndSMSBasedOnFromStoreStatus | order_is_on_store() | Cron (SENT store-pickup orders) |
AdvSendMailToCustomersForReview | sendCustomerPromptReview() | Cron (review reminders) |
AdvSendEmailForDeliveryDelay | sendCustomerInformDelay() | Cron (delivery delay alerts) |
AdvSendEmailForWaitingList | sendEmailForWaitingList() | Cron (back-in-stock alerts) |
AdvSentMailToCustomerBirthday | sentBirthdayWishes() | Cron (birthday wishes) |
AdvSentMailToCustomerRemainingPoints | sentRemainingPoints() | Cron (loyalty reminders) |
AdvSendGiftCardsToEmails | sendGiftCardToEmail() / sendGiftCardToInformCustomer() | Cron (gift card delivery) |
AdvResendGiftCardEmail | sendGiftCardToEmail() | Manual job (admin re-send) |
AdvCheckCDNCertificate | sendExpiringCertificate() | Cron (SSL cert monitoring) |
AdvSendOrderReminderMail | (delegates to order_model) | Cron (client-defined reminders) |
Legacy Cronjob Controller Callers
The Cronjob controller in application/controllers/Cronjob.php contains legacy cron methods that directly load adv_mailer and call the same flows. These are being superseded by the job framework (JobCommand classes) but remain functional:
sendEmailBasedOnSentStatus()sendEmailAndSMSBasedOnSentStatus()sendEmailAndSMSBasedOnFromStoreStatus()sendMailToCustomersForReview()sendOrderReminderMail()sendEmailForDeliveryDelay()sendEmailForDeliveryDelayWithWarning()
Admin Email Management
Admin tooling for this system lives in AdvEmailViewer (ecommercen/settings/controllers/AdvEmailViewer.php):
- Template preview (
settings/email_views) — renders all templates with synthetic Greek-market test data so admins can verify layout before deployment. - Subject editor (
settings/email_views/editEmailSubjects) — per-language editor for allEMAIL_SUBJECTSregistry keys, including anrtrimmask bug in the POST handler.
For the full viewer code flow, known issues (divergent view directories, missing apologize_shipping_delay.php, orphan ask_us_email.php), and the complete subject key catalog, see AD-53 Email Template Viewer.
Client Extension Points
1. Override the Mailer Model
Create or extend Adv_mailer in application/models/Adv_mailer.php (client repos already have this file). Override any public method to customize email content, add recipients, change templates, or add entirely new email types.
php
// In client's application/models/Adv_mailer.php
class Adv_mailer extends Adv_base_model
{
// Override to add custom CC recipients for order emails
public function order_complete($data)
{
$this->extraRecipients = ['warehouse@client.com'];
parent::order_complete($data);
$this->extraRecipients = [];
}
}2. Override Email Templates
Place custom view files in application/views/main/mail/ to override the default templates. CI's view loader checks the application/views/ path first, so client views take precedence over the defaults.
3. Override emailViews.json
Replace application/config/emailViews.json to add new template mappings or change view paths.
4. Override Job Classes
Create subclasses in application/modules/job/libraries/ (e.g., SendEmailBasedOnSentStatus extends AdvSendEmailBasedOnSentStatus) to modify query logic, add custom conditions, or inject additional processing.
5. Override ExternalLang Translations
Add or modify translations in application/language/{lang}/external_lang.php to customize email text per language.
6. Add Extra Recipients
Set $this->adv_mailer->extraRecipients before calling any send method to add CC addresses.
7. Implement register_welcome()
The register_welcome() method is a no-op stub. Client repos can implement it to send a welcome email on customer registration.
Business Rules
| Rule | Description |
|---|---|
| Invalid email filtering | Emails containing _invalid_mail@adveshop.local are silently dropped (prevents sending to sanitized/Skroutz-generated addresses) |
| Customer history tracking | Only recorded for customer-facing emails where $customerData->id is set |
| Language resolution | Customer's lang field is primary; site default language is fallback |
| SMTP profile separation | Two distinct SMTP profiles allow separate credentials for transactional vs. contact form emails |
| Subject localization | All subjects are per-language in the registry; some support vsprintf placeholders |
| Feature gating | Low stock, new product, birthday, and loyalty emails are gated by registry flags |
| One-shot delivery flags | Order status emails use is_email_sent flag; delivery delay uses delivery_warning_msg_status |
| Attachment support | Invoice PDFs attached to order update emails for INVOICED/SENT/PAID_SENT statuses |
| Admin notification dual send | order_complete() sends both a customer email and an admin notification email |
| Gift card dual send | Gift cards send two emails: one to the gift recipient and one to the purchasing customer |
| PHPMailer state reset | Mailer::initialize() is called after each send, preventing state leakage between sequential sends |
| SMTP timeout | 5-second timeout prevents request blocking on SMTP failures |
Error Handling
- PHPMailer exceptions: Caught in
Mailer::send()and logged vialog_message('error', ...). The method returnsfalseon failure. - Validation failures:
Mailer::validateProperties()checks for required fields (host, port, user, password, from, to, subject, message). Missing fields are logged toerrorInfoand the send returnsfalse. - Missing SMTP credentials:
getDefaultSmtpCredentials()throws anExceptionif any required registry key is missing. - Invalid email purpose:
getDefaultSmtpCredentials()throws anExceptionif the purpose is notEMAIL_CONTACT_FORMorEMAIL_SHOP_MAILER. - No retry mechanism: Failed sends are not retried. For cron jobs, the
is_email_sentflag remainsfalse, so the next cron run will attempt the email again.
Related Flows
- SY-02 Order Status Emails -- Order INVOICED/SENT/store-pickup notifications via this system
- SY-11 Review Reminders -- Review prompt emails via
sendCustomerPromptReview() - SY-12 Birthday & Points Emails -- Birthday wishes and loyalty reminders
- SY-13 Waiting List Notifications -- Back-in-stock alerts
- SY-14 Order Reminders -- Client-defined order follow-up emails
- SY-15 Delivery Delay -- Shipping delay apology emails
- CF-08 Payment Processing -- Triggers
order_complete()on successful payment - CF-29 Contact Forms -- Contact form email dispatch
- AD-02 Product Management -- New product admin notification
- AD-12 Blog Admin -- Blog comment moderation emails
- AD-52 Review Moderation -- triggers
sendUserReviewStatusUpdateEmail()on review approve/reject; documents the status=0 → "rejected" email quirk and loyalty side-effects - SY-19 Config Registry -- Registry groups for SMTP config and email subjects
- SY-01 Cron Framework -- Job scheduling for async email jobs
- AD-53 Email Template Viewer -- admin preview of email templates sent by this system