Skip to content

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 ExternalLang translation 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 delivery

Underlying 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

ComponentPathRole
Adv_mailerapplication/models/Adv_mailer.phpCentral email orchestration model
Mailerecommercen/core/Mailer.phpPHPMailer wrapper (SMTP send)
Templatesrc/Template/Template.phpEmail view path resolver from JSON config
Configsrc/Template/Config.phpJSON config loader for template definitions
ExternalLangecommercen/libraries/ExternalLang.phpLocalized string resolver for email content
emailViews.jsonapplication/config/emailViews.jsonTemplate-to-view mapping registry
registry_helperecommercen/helpers/registry_helper.phpgetDefaultSmtpCredentials() function
AdvEmailViewerecommercen/settings/controllers/AdvEmailViewer.phpAdmin: email template previewer and subject editor
Email viewsapplication/views/main/mail/*.phpHTML email templates (23 templates — see AD-53)

Internal API: Adv_mailer Methods

Customer-Facing Methods

MethodTriggerTemplate KeySMTP ProfileSubject KeyAttachments
order_complete($data)Checkout success (all payment gateways)orderCreatedEMAIL_SHOP_MAILERORDER_COMPLETE (%s = serial)No
order_has_been_updated($orderId, $data)Order status change jobsorderUpdateEMAIL_SHOP_MAILERORDER_UPDATE (%s = serial)Invoice PDF (for INVOICED/SENT/PAID_SENT)
order_is_on_store($orderId)Store-pickup ready joborderOnStoreEMAIL_SHOP_MAILERORDER_ON_STORE (%s = serial)Invoice PDF (conditional)
recover_password_mail($email)Customer password resetresetPasswordEMAIL_SHOP_MAILERPASSWORD_RESETNo
sendEmailForWaitingList($email, $data, $lang)Waiting list jobwaitingListSuccessEMAIL_SHOP_MAILERWAITING_LISTNo
sendCustomerPromptReview($customer, $key)Review reminder jobreviewForFacebook / reviewForSkroutz / reviewForGoogleEMAIL_SHOP_MAILERREVIEW_FOR_FACEBOOK / REVIEW_FOR_SKROUTZ / REVIEW_FOR_GOOGLENo
sendCustomerInformDelay($order)Delivery delay joborderInformDelayEMAIL_SHOP_MAILERCUSTOMER_INFORM_DELAY (%s = serial)No
sentRemainingPoints($customerId)Remaining points jobremainingPointsEMAIL_SHOP_MAILERREMAINING_POINTSNo
sentBirthdayWishes($customerId, $points)Birthday jobbirthdayWishesEMAIL_SHOP_MAILERBIRTHDAY_WISHES (%s = name)No
sendGiftCardToEmail($order)Gift card jobgiftCardEMAIL_SHOP_MAILERGIFT_CARDNo
sendGiftCardToInformCustomer($order)Gift card jobgiftCardInformCustomerEMAIL_SHOP_MAILERGIFT_CARD_INFORM_CUSTOMERNo
sendUserReviewStatusUpdateEmail($customer, $status)Admin review moderationproductReviewAccept / productReviewRejectEMAIL_CONTACT_FORMUSER_REVIEW_ACCEPT / USER_REVIEW_REJECTNo
sendBlogCommentStatusUpdateEmail($blog, $status)Admin blog comment moderationblogCommentAccept / blogCommentRejectEMAIL_CONTACT_FORMBLOG_COMMENT_ACCEPT / BLOG_COMMENT_REJECTNo

Admin-Facing Methods

MethodTriggerTemplate KeySMTP ProfileNotes
sendAdmin() (via order_complete)Order placednotifyAdminEMAIL_SHOP_MAILERSent to admin_email from SMTP config
order_with_low_stock($products)Checkout low stock checknotifyAdminLowStockEMAIL_SHOP_MAILERGated by EMAIL.NOTIFICATION_LOW_STOCK registry
inform_new_product($productId)Product creation in adminnotifyAdminNewProductEMAIL_SHOP_MAILERGated by EMAIL.NOTIFICATION_NEW_PRODUCT registry
sendExpiringCertificate($data)CDN certificate check jobN/A (raw ExternalLang)EMAIL_SHOP_MAILERBypasses template system; uses doSend() directly

Contact Form Method

MethodTriggerTemplateSMTP ProfileNotes
sendContactFormEmail($msg, $subject, $attachments)Form submission controllerPre-rendered by callerEMAIL_CONTACT_FORMSupports file attachments; sent to admin_email

Stub Method

MethodNotes
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:

  1. Invalid email guard: Checks if $customerData->mail contains the configured invalid email domain (_invalid_mail@adveshop.local). Returns false immediately if matched. This prevents emails to sanitized/synthetic addresses (e.g., Skroutz marketplace orders, GDPR-sanitized customers).

  2. Template resolution: Template::layoutView($key) reads emailViews.json and resolves the view path. Example: layoutView('orderCreated') returns main/mail/order_created.

  3. ExternalLang initialization: Creates new ExternalLang($customerData->lang, $this->languageAbbr) using the customer's preferred language with the site default as fallback. Loads translations from adv_external (ecommercen) and external (application) language files.

  4. View rendering: $this->load->view($pageToLoad, $data, true) renders the PHP email template with all data merged (including $externalLang for translation calls and $emailViews for component includes).

  5. Customer history: If $customerData->id is non-empty, inserts a record into shop_customer_message_history with the email type, subject, and full rendered HTML body.

  6. SMTP credential resolution: getDefaultSmtpCredentials($emailPurpose) reads the registry group (EMAIL_SHOP_MAILER or EMAIL_CONTACT_FORM) and returns all SMTP connection parameters.

  7. Dispatch: Calls doSend() which instantiates Mailer, configures SMTP, sets from/to/cc addresses, attaches files, and sends.

Core Send Pipeline: sendAdmin() (Private)

Simplified variant for admin notifications:

  1. Renders view with data + ExternalLang (using site default language only).
  2. Resolves subject from $data['subject'] or falls back to site title.
  3. Sends to $emailConfig['admin_email'] from the SMTP profile.
  4. No customer history tracking.

Core Send Pipeline: doSend() (Private)

The actual PHPMailer dispatch method:

  1. Instantiates Mailer (PHPMailer wrapper).
  2. Sets from address and display name from SMTP config.
  3. Adds recipient(s) as To addresses.
  4. Adds $this->extraRecipients as CC (if any are set by the caller).
  5. Adds file attachments (if any).
  6. Configures SMTP: host, port, user, password, auto SSL/TLS.
  7. Sets subject and HTML message body.
  8. 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.
  • fromNameFailOver defaults 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:

VariableTypeSourceDescription
$externalLangExternalLangsend()Translation helper; $externalLang->line('key') for localized strings
$emailViewsTemplateaddEmailViewsToRender()For including sub-components via $emailViews->componentView()
$company_phonestringRegistry COMPANY_PHONE.ESHOPCompany 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.php and application/language/{lang}/external_lang.php.
  • Falls back to the site default language if the customer's language has no translations.
  • Supports vsprintf parameter substitution: $externalLang->line('key', [$param1, $param2]).

Data Model

Table: shop_customer_message_history

Stores rendered email content for customer-facing emails (not admin emails).

ColumnTypeDescription
idint (PK)Auto-increment
user_idintFK to shop_customer.id
datetimedatetimeSend timestamp
typevarcharEmail purpose code (e.g., ORDER_COMPLETE, PASSWORD_RESET, WAITING_LIST)
message_typevarcharSubject text (or 'EMAIL' fallback)
subjectvarcharEmail subject line
email_bodytextFull 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

KeyDescription
SMTP_HOSTSMTP server hostname
SMTP_PORTSMTP port
SMTP_USERSMTP username
SMTP_PASSSMTP password (encrypted in registry)
SMTP_AUTO_SSL_TLSAuto SSL/TLS negotiation flag
SENDER_EMAILFrom email address
FROM_NAMEFrom display name
ADMIN_EMAILAdmin 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 placeholderAdv_mailer method
ORDER_COMPLETEorder serialorder_complete()
ORDER_UPDATEorder serialorder_has_been_updated()
ORDER_ON_STOREorder serialorder_is_on_store()
PASSWORD_RESETrecover_password_mail()
USER_REVIEW_ACCEPT / USER_REVIEW_REJECTsendUserReviewStatusUpdateEmail()
{contact_form_key}sendContactFormEmail() (dynamic per form)

Other Registry Keys Used

GroupKeyPurpose
EMAIL_FORMSITEReply-to email shown in email templates
COMPANY_PHONEESHOPCompany phone included in most email bodies
TITLESITESite name used as fallback from-name and in subjects
GLOBALLOGOLogo image URL used in email headers
GLOBALADMIN_EMAILAdmin email for certificate alerts
EMAILNOTIFICATION_LOW_STOCKFeature gate for low stock admin emails
EMAILNOTIFICATION_NEW_PRODUCTFeature gate for new product admin emails
EMAIL_FOR_BIRTHDAYIS_ENABLEDFeature gate for birthday emails
POINT_SYSTEMIS_ENABLEDFeature gate for loyalty points emails
EMAIL_FOR_TOTAL_POINTSIS_ENABLEDFeature gate for remaining points emails

Caller Map

Synchronous Callers (Inline During Request)

CallerMethod CalledTrigger
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 ClassMethod CalledSchedule
AdvSendEmailBasedOnSentStatusorder_has_been_updated()Cron (INVOICED orders)
AdvSendEmailAndSMSBasedOnSentStatusorder_has_been_updated()Cron (SENT courier orders)
AdvSendEmailAndSMSBasedOnFromStoreStatusorder_is_on_store()Cron (SENT store-pickup orders)
AdvSendMailToCustomersForReviewsendCustomerPromptReview()Cron (review reminders)
AdvSendEmailForDeliveryDelaysendCustomerInformDelay()Cron (delivery delay alerts)
AdvSendEmailForWaitingListsendEmailForWaitingList()Cron (back-in-stock alerts)
AdvSentMailToCustomerBirthdaysentBirthdayWishes()Cron (birthday wishes)
AdvSentMailToCustomerRemainingPointssentRemainingPoints()Cron (loyalty reminders)
AdvSendGiftCardsToEmailssendGiftCardToEmail() / sendGiftCardToInformCustomer()Cron (gift card delivery)
AdvResendGiftCardEmailsendGiftCardToEmail()Manual job (admin re-send)
AdvCheckCDNCertificatesendExpiringCertificate()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 all EMAIL_SUBJECTS registry keys, including an rtrim mask 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

RuleDescription
Invalid email filteringEmails containing _invalid_mail@adveshop.local are silently dropped (prevents sending to sanitized/Skroutz-generated addresses)
Customer history trackingOnly recorded for customer-facing emails where $customerData->id is set
Language resolutionCustomer's lang field is primary; site default language is fallback
SMTP profile separationTwo distinct SMTP profiles allow separate credentials for transactional vs. contact form emails
Subject localizationAll subjects are per-language in the registry; some support vsprintf placeholders
Feature gatingLow stock, new product, birthday, and loyalty emails are gated by registry flags
One-shot delivery flagsOrder status emails use is_email_sent flag; delivery delay uses delivery_warning_msg_status
Attachment supportInvoice PDFs attached to order update emails for INVOICED/SENT/PAID_SENT statuses
Admin notification dual sendorder_complete() sends both a customer email and an admin notification email
Gift card dual sendGift cards send two emails: one to the gift recipient and one to the purchasing customer
PHPMailer state resetMailer::initialize() is called after each send, preventing state leakage between sequential sends
SMTP timeout5-second timeout prevents request blocking on SMTP failures

Error Handling

  • PHPMailer exceptions: Caught in Mailer::send() and logged via log_message('error', ...). The method returns false on failure.
  • Validation failures: Mailer::validateProperties() checks for required fields (host, port, user, password, from, to, subject, message). Missing fields are logged to errorInfo and the send returns false.
  • Missing SMTP credentials: getDefaultSmtpCredentials() throws an Exception if any required registry key is missing.
  • Invalid email purpose: getDefaultSmtpCredentials() throws an Exception if the purpose is not EMAIL_CONTACT_FORM or EMAIL_SHOP_MAILER.
  • No retry mechanism: Failed sends are not retried. For cron jobs, the is_email_sent flag remains false, so the next cron run will attempt the email again.