Skip to content

Email Template Viewer ​

Flow ID: AD-53 | Module(s): settings | Complexity: Medium Last Updated: 2026-09-29

Business Context ​

The email template viewer provides administrators with a browser for previewing all system email templates. Located at /settings/email_views.htm, it renders each hard-coded template with synthetic Greek-market test data so admins can visually verify email layouts, translations, and content before they reach customers.

The viewer also exposes a subject line editor at /settings/email_views/editEmailSubjects.htm that allows per-language customization of email subject lines via the EMAIL_SUBJECTS registry group. This is the primary way admins change email subjects without touching code.

The viewer hard-codes 20 named template views (ecommercen/settings/controllers/AdvEmailViewer.php:166-187). The real runtime dispatcher Adv_mailer can render a slightly different set — the two lists are out of sync, though both resolve against the same template directory (see Two Mail Directories and Known Issues).


API Reference ​

REST Endpoints ​

No REST API. Email template viewing is an admin-only tool.

Legacy Admin Routes ​

RouteControllerMethodHTTPDescription
settings/email_viewsAdvEmailViewerindex()GETBrowse and preview all email templates
settings/email_views/editEmailSubjectsAdvEmailViewereditEmailSubjects()GET/POSTEdit email subject lines per language

Routes are HMVC auto-routed through the settings module — no entries in application/config/*routes*.php. The HMVC entry point is a thin subclass: application/modules/settings/controllers/Email_views.php (class Email_views extends AdvEmailViewer {}, 5 lines).

RBAC gate: constructor AdvEmailViewer.php:5-15; the check at AdvEmailViewer.php:9 — allowRole([AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN], $this->sessionRoles) → error_401() on mismatch. One gate covers both viewing and editing.


Code Flow ​

Template Preview (index) ​

AdvEmailViewer::index()                 (ecommercen/settings/controllers/AdvEmailViewer.php:160-197)
  |
  +--> initTestEmailViews()               (private, AdvEmailViewer.php:17-24)
  |    +--> Loads eshop/order_model, eshop/stores_model, encryption library
  |    +--> loadSampleDataDependencies()  (protected no-op hook, AdvEmailViewer.php:39-41 —
  |         override point for forks to load extra helpers/models; called at the
  |         end of initTestEmailViews())
  |
  +--> $data = extendSampleEmailData($this->sampleEmailData())   (AdvEmailViewer.php:164)
  |    |
  |    +--> sampleEmailData()  (protected, AdvEmailViewer.php:47-149) builds the base
  |    |    sample-data payload (inline, no external fixtures):
  |    |    - Customer scalars (name, email, phone, address)
  |    |    - orderData array with 25+ keys incl. getCurrencyData(sampleCurrencyId())
  |    |      (sampleCurrencyId() default 1, AdvEmailViewer.php:30-33; used at
  |    |      AdvEmailViewer.php:144-145)
  |    |    - store_data, transporter_name, products, loyalty_reward
  |    |    - store (top-level, mirrors orderData['store'] — consumed by
  |    |      order_on_store to preview the customer-selected pickup store's
  |    |      address/phone/opening_hours, #449)
  |    |    - customerData, token, encryption, giftCard,
  |    |      couponRules, giftCardCoupon
  |    |    - $data['emailViews'] = new Template(
  |    |          new Config(APPPATH . '/config/emailViews.json')
  |    |      )                            (AdvEmailViewer.php:131-133; only for
  |    |      views' own component resolution — viewer does NOT use
  |    |      layouts/components to choose which templates to render)
  |    |    - externalLang = new ExternalLang($this->language_abbr)  (AdvEmailViewer.php:146)
  |    |
  |    +--> extendSampleEmailData(array $data): array  (protected no-op hook,
  |         AdvEmailViewer.php:155-158 — override point for forks to add/override
  |         sample-data keys)
  |
  +--> Hard-coded $email_views list (20 entries, AdvEmailViewer.php:166-187):
  |    contact_email, notify_admin, notify_admin_low_stock,
  |    notify_admin_new_product, order_created, order_on_store, order_update,
  |    product_review_accept, product_review_reject, reset_password,
  |    review_for_facebook, review_for_skroutz, review_for_google,
  |    waiting_list_success, remaining_points, birthday_wishes,
  |    gift_card, gift_card_inform_customer, blog_comment_accept,
  |    blog_comment_reject
  |
  +--> view_content = "{$classView}/email_views"
  +--> $this->template_view_admin(...)

The admin view (application/views/admin/settings/email_views.php:24) iterates $email_views and renders each via:

php
$this->load->view($this->client_views . '/mail/' . $email_view, $data, true)

$this->client_views resolves to "main", not to the deprecated app.php:19 value. Adv_base_controller's constructor overwrites the property unconditionally (Adv_base_controller.php:104) from Template::templateFolder(), which reads mainTemplate.json:3 ("templateFolder": "main") — mainTemplate.json is the file named by $config['clientTemplate'] at app.php:14. AdvEmailViewer inherits this via Admin_c → Adv_admin_controller:29 (parent::__construct()) → Base_c → Adv_base_controller:104, and the view reads the property through MX_Loader::__get() (application/third_party/MX/Loader.php:333-336). So the viewer loads from application/views/main/mail/ — the same directory Adv_mailer uses at runtime. The deprecated app.php:19 value is not consulted on this path. See Two Mail Directories.

Subject Editor (editEmailSubjects) ​

AdvEmailViewer::editEmailSubjects()     (ecommercen/settings/controllers/AdvEmailViewer.php:220-253)
  |
  +--> Seed empty defaults for contact form keys:
  |    $contactFormEmptyData = array_map(
  |        fn ($item) => '',
  |        array_flip((new FormsConfig())->getContactFormKeys())
  |    )
  |
  +--> On POST ($this->input->post('submit')):
  |    +--> For each admin language ($this->adminLanguages):
  |    |    +--> For each posted field whose name contains $langAbbr:
  |    |         +--> $registryKey = $this->stripLanguageSuffix($postKey, $langAbbr);  # AdvEmailViewer.php:228
  |    |         +--> if ($registryKey === null) { continue; }  # AdvEmailViewer.php:229-231
  |    |         +--> $this->registry->setValue('EMAIL_SUBJECTS', $registryKey, $postValue, $langAbbr)  # AdvEmailViewer.php:233
  |    +--> Flash success (t('eshop.admin.success.entryedit')), redirect
  |
  +--> On GET:
  |    +--> For each admin language:
  |    |    +--> $this->registry->getGroupAsArray('EMAIL_SUBJECTS', $langAbbr)
  |    |    +--> Merge with $contactFormEmptyData as defaults
  |    +--> Render admin/settings/email_subjects.php with
  |         inputs named "{key}_{langAbbr}"

Domain Layer ​

No modern domain layer. The email viewer is a pure legacy admin tool.


Templates Directory ​

Canonical templates live in application/views/main/mail/ (24 files, emailViews.json:3 → templateFolder = "main/mail"). These are what Adv_mailer actually renders at email-send time.

FilePurpose
apologize_shipping_delay.phpDelivery-delay apology notice; layout orderInformDelay (emailViews.json:53-55); consumed by sendCustomerInformDelay() (Adv_mailer.php:451)
birthday_wishes.phpBirthday greetings + optional loyalty points
blog_comment_accept.phpBlog comment moderation — approved notification
blog_comment_reject.phpBlog comment moderation — rejected notification
contact_email.phpDefault contact-form email
contact_email_generated.phpGeneric contact-form email via generated form config (fullSampleForm)
email_products_summary.phpComponent included by other templates via componentView('emailProductsSummary') (emailViews.json:75-79)
gift_card.phpGift card delivery to recipient
gift_card_inform_customer.phpGift card purchase confirmation to buyer
notify_admin.phpAdmin order notification
notify_admin_low_stock.phpAdmin low-stock alert
notify_admin_new_product.phpAdmin new-product notification (inform_new_product() flagged @todo ... BROKEN ATM at Adv_mailer.php:273, method body Adv_mailer.php:274)
order_created.phpCustomer order confirmation
order_on_store.php"Your order arrived at the store"
order_update.phpOrder status update (shipping / paid / invoiced)
product_review_accept.phpReview moderation — approved notification
product_review_reject.phpReview moderation — rejected notification
remaining_points.phpLoyalty "you have remaining points" reminder
reset_password.phpPassword reset with encrypted token
return_form.phpReturn-form submission notice; layout returnEmail (emailViews.json:8-10, NEW); consumed by sendReturnFormEmail() (Adv_mailer.php:380); dispatched from Adv_forms.php:136 (shop copy) and :144 (customer copy)
review_for_facebook.phpPost-purchase review prompt for Facebook
review_for_google.phpPost-purchase review prompt for Google
review_for_skroutz.phpPost-purchase review prompt for Skroutz
waiting_list_success.phpBack-in-stock notification

Two Mail Directories ​

There are two mail view directories on disk. Only one has a live reader — both the viewer and the runtime mailer resolve to it, via two independent config files that happen to agree:

DirectoryFile CountReader
application/views/main/mail/25Both Adv_mailer (runtime sends, driven by emailViews.json::templateFolder = "main/mail") and AdvEmailViewer (admin preview, via $this->client_views resolved from mainTemplate.json::templateFolder = "main" — see Code Flow)
application/views/default/mail/16No live code path reads this directory. Adv_mailer assigns the deprecated app.php:19 $config['client_views'] = 'default' value to a property at application/models/Adv_mailer.php:29 (as does the parent Adv_base_model.php:36, for every model on every request) but never reads that property back — it resolves views solely through $this->emailViews (Adv_mailer.php:30). The viewer never touches the config item at all. So the directory itself remains stale/unreferenced regardless.

This agreement between the two live paths is a coincidence of arithmetic, not a guarantee: the viewer composes {mainTemplate.json::templateFolder} + a hardcoded /mail/ literal (application/views/admin/settings/email_views.php:24, config key clientTemplate at app.php:14), while Adv_mailer uses the whole emailViews.json::templateFolder string ("main/mail", config key emailTemplate at app.php:15). They agree only because "main" + "/mail" == "main/mail". A fork that changes mainTemplate.json:3, repoints clientTemplate, or moves emailViews.json:3 would silently make preview and send resolve to different directories, with no error raised — latent fragility for forks, not a current bug.

Inventory difference between main/mail/ and the stale default/mail/ ​

Present in main/mail/ but not in default/mail/ (8): apologize_shipping_delay, birthday_wishes, blog_comment_accept, blog_comment_reject, contact_email_generated, gift_card_inform_customer, remaining_points, return_form.

This is a stale-directory inventory gap, not a failure mode: since neither the viewer nor Adv_mailer reads default/mail/, none of these files being absent from it has any runtime effect. All 20 templates in the viewer's $email_views list resolve successfully from main/mail/.

Orphan template removed (#97) ​

The viewer previously listed ask_us_email.php for preview even though it was never dispatched at runtime — no layout entry in application/config/emailViews.json, no caller in Adv_mailer, and no EMAIL_SUBJECTS.ask_us_email registry key. It survived only because AdvEmailViewer::$email_views listed it. Both mail-directory copies and the list entry were removed together in #97 (see Advisable-com/ecommercen#97); its content was a strict subset of contact_email.php, so no replacement was needed.

Templates missing from the viewer's list ​

The viewer's hard-coded $email_views array (AdvEmailViewer.php:166-187) is missing three layouts that emailViews.json knows about:

  • contact_email_generated (layout contactEmailGenerated)
  • apologize_shipping_delay (layout orderInformDelay) — file exists in main/mail/ and resolves fine at runtime; simply not offered for preview
  • return_form (layout returnEmail, NEW) — same: exists in main/mail/, not offered for preview

The partial email_products_summary is also absent from the list, correctly — it is a component (emailViews.json:75-79), not a standalone preview target.


Subject Registry Keys ​

Subject strings live in the registry table under the EMAIL_SUBJECTS group, keyed per admin language. Defaults are seeded from application/config/db_default_values.php array email_subject_data (:42, keys L43-151) by database/seeders/InitialSeed.php::emailSubjects() (InitialSeed.php:8569-8580, reggroup='EMAIL_SUBJECTS' at L8576). Edited via AdvEmailViewer::editEmailSubjects() (setValue('EMAIL_SUBJECTS', ...) at AdvEmailViewer.php:209).

Registry KeyConsumer in application/models/Adv_mailer.php
contactForm / fullSampleFormNot consumed by Adv_mailer — read by the contact-forms flow (see CF-29 Contact Forms); call site ecommercen/forms/controllers/Adv_forms.php:62
PASSWORD_RESETrecover_password_mail() L75
ORDER_COMPLETEorder_complete() L171
ORDER_UPDATEorder_has_been_updated() L210
WAITING_LISTsendEmailForWaitingList() L268
ORDER_ON_STOREorder_is_on_store() L348
USER_REVIEW_ACCEPTsendUserReviewStatusUpdateEmail() L408
USER_REVIEW_REJECTsendUserReviewStatusUpdateEmail() L408
REVIEW_FOR_FACEBOOKsendCustomerPromptReview() L422
REVIEW_FOR_SKROUTZsendCustomerPromptReview() L425
REVIEW_FOR_GOOGLEsendCustomerPromptReview() L428
CUSTOMER_INFORM_DELAYsendCustomerInformDelay() L462
REMAINING_POINTSsentRemainingPoints() L510
BIRTHDAY_WISHESsentBirthdayWishes() L543
GIFT_CARDsendGiftCardToEmailRenderer() L589 (view) / L599 (doSend)
GIFT_CARD_INFORM_CUSTOMERsendGiftCardToEmailRenderer() L588 (view) / L598 (doSend)
BLOG_COMMENT_ACCEPTsendBlogCommentStatusUpdateEmail() L641
BLOG_COMMENT_REJECTsendBlogCommentStatusUpdateEmail() L641

The same group is also read in application/views/admin/contact_emails/list.php:89.

Templates with no matching registry key ​

  • notify_admin, notify_admin_low_stock, notify_admin_new_product — Adv_mailer::sendAdmin() falls back to getRegistryValue('TITLE', 'SITE', ...) at Adv_mailer.php:743, or uses $data['subject'] (set from the site.emails.inform_new_product.title lang line at Adv_mailer.php:286).

Email Templates Engine ​

The viewer instantiates a Template object (AdvEmailViewer.php:131-133) to enable email templates to resolve sub-components via $emailViews->componentView(). For detailed information on template resolution via emailViews.json, the Template / Config system, the complete Adv_mailer method catalog, Adv_mailer internals, SMTP credential management, and the PHPMailer transport pipeline, see SY-24 Email Dispatch System.


Email Transport & SMTP Configuration ​

For complete information on the Mailer wrapper, SMTP credentials from the registry (EMAIL_CONTACT_FORM, EMAIL_SHOP_MAILER), credential management, and PHPMailer transport internals, see SY-24 Email Dispatch System.

The admin UI for configuring SMTP credentials lives in application/views/admin/settings/email_block.php and ecommercen/settings/controllers/Adv_settings.php:63-183 (SMTP writes :69-76, reads :148-155, validation from :181). There is no MailHog, Mailgun, or SendGrid integration — the platform sends pure SMTP.


Localization ​

The viewer renders templates using new ExternalLang($this->language_abbr) (the admin's UI language). For detailed information on ExternalLang translation resolution, customer locale precedence, and how the runtime dispatch system handles multi-language email content, see SY-24 Email Dispatch System.

No MUI tables exist for email content — translations live in CI language files via ExternalLang and in registry subject entries.


Dispatch Points ​

For a complete map of where each template is dispatched from (synchronous callers, asynchronous cron jobs, legacy cronjob controller methods, and the full line numbers), see SY-24 Email Dispatch System.


Architecture ​

ComponentPathPurpose
AdvEmailViewerecommercen/settings/controllers/AdvEmailViewer.phpAdmin controller (253 lines)
Email_viewsapplication/modules/settings/controllers/Email_views.phpHMVC entry point, thin subclass of AdvEmailViewer
Adv_mailerapplication/models/Adv_mailer.phpRuntime mailer (828 lines, Adv_base_model)
Mailerecommercen/core/Mailer.phpPHPMailer wrapper (SMTP only)
Advisable\Template\Templatesrc/Template/Template.phpResolves layouts / components via emailViews.json
Advisable\Template\Configsrc/Template/Config.phpLoads template JSON config
Email views configapplication/config/emailViews.jsonLayout and component mappings (80 lines, 23 layouts)
Default subject seedsapplication/config/db_default_values.php:42 (keys L43-151)Baseline EMAIL_SUBJECTS values
Subject seederdatabase/seeders/InitialSeed.php:8569-8580Inserts defaults into registry
FormsConfigapplication/modules/forms/libraries/FormsConfig.phpEmpty subclass of AdvFormsConfig
AdvFormsConfigecommercen/forms/libraries/AdvFormsConfig.phpLoads application/config/contact_forms.php, exposes getContactFormKeys(), getContactFormKeysDropDown(), getEmailTemplateView(), etc. — not a list of email templates
ExternalLangecommercen/libraries/ExternalLang.phpPer-locale string resolver passed to every template
Admin menuapplication/config/admin_menu.php:955-968Two entries (email_views L955-961, editEmailSubjects L962-968), gated on [AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN]
Viewer templateapplication/views/admin/settings/email_views.phpIterates $email_views and loads each (:24)
Subject editor viewapplication/views/admin/settings/email_subjects.phpRenders per-language subject inputs

Role of FormsConfig ​

FormsConfig is purely a contact-form configuration accessor. AdvEmailViewer::editEmailSubjects() uses it only to seed empty defaults:

php
$contactFormEmptyData = array_map(
    fn ($item) => '',
    array_flip((new FormsConfig())->getContactFormKeys())
);

It does not enumerate email templates.


Subject Editor Flow ​

AdvEmailViewer::editEmailSubjects() (ecommercen/settings/controllers/AdvEmailViewer.php:199-229):

  1. Seeds empty defaults for contact-form keys via FormsConfig::getContactFormKeys().
  2. On POST ($this->input->post('submit')):
    • For each admin language in $this->adminLanguages:
      • For each posted field whose name contains $langAbbr:
        • Strips the language suffix via rtrim($postKey, "_$langAbbr") at AdvEmailViewer.php:208 — see Known Issues #5.
        • Calls $this->registry->setValue('EMAIL_SUBJECTS', $postKey, $postValue, $langAbbr) at AdvEmailViewer.php:209.
    • Flashes t('eshop.admin.success.entryedit') and redirects.
  3. On GET:
    • For each admin language, loads $this->registry->getGroupAsArray('EMAIL_SUBJECTS', $langAbbr).
    • Merges with the empty contact-form defaults.
    • Renders admin/settings/email_subjects.php with inputs named {key}_{langAbbr}.

Data Model ​

No dedicated tables. Email subjects are stored in the registry table under the EMAIL_SUBJECTS group with per-language entries. See Subject Registry Keys for the full list.

Registry GroupKey PatternDescription
EMAIL_SUBJECTS{template_name_or_const}Subject line for each email template, per language
EMAIL_CONTACT_FORMSMTP credentials (8 keys)Transport for contact-form and return-form emails
EMAIL_SHOP_MAILERSMTP credentials (8 keys)Transport for all other emails

Configuration ​

  • Admin menu: application/config/admin_menu.php:955-968 — two entries (settings/email_views L955-961, settings/email_views/editEmailSubjects L962-968) gated on [AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN].
  • Template config: application/config/emailViews.json.
  • Email template file name: application/config/app.php:15 (emailTemplate = 'emailViews.json').
  • Client views setting: application/config/app.php:19 ($config['client_views'] = 'default') — marked @deprecated in-source (app.php:18). The value is read into properties: ecommercen/core/models/Adv_base_model.php:36 (parent of every model, runs on every request) and application/models/Adv_mailer.php:29 both assign it, and the separately-@deprecated, zero-caller previewOrder_cart() helper (ecommercen/helpers/cart_helper.php:103) reads it too. None of these reads ever resolves a rendered view path in practice — previewOrder_cart() has no callers, and neither Adv_base_model nor Adv_mailer reads its own $client_views property back. The only client_views property that reaches a view is the controller property, and Adv_base_controller.php:104 overwrites that one unconditionally from mainTemplate.json before any view runs.
  • Role constants: application/config/constants.php:99, 101 (AUTH_ROLE_ADVISABLE = 1, AUTH_ROLE_ADMIN = 3).

Required roles: AUTH_ROLE_ADVISABLE and AUTH_ROLE_ADMIN. The constructor at AdvEmailViewer.php:5-15 calls allowRole([AUTH_ROLE_ADVISABLE, AUTH_ROLE_ADMIN], $this->sessionRoles) → error_401() on mismatch (gate itself at L9). The admin menu enforces the same set. There is no separation between viewing and editing — both operations share one role gate.


Client Extension Points ​

  • Custom email templates: Client repos can add templates under application/views/main/mail/ and wire them into emailViews.json + Adv_mailer. Client repos should use custom/ for new modern code, but the legacy mail pipeline still lives under application/.
  • Override controller: Extend AdvEmailViewer via application/modules/settings/controllers/Email_views.php to add client-specific preview entries.
  • Additional subjects: Register client-specific subjects under the EMAIL_SUBJECTS registry group per language.
  • Sample currency override (#422 / 4.115.0): Override sampleCurrencyId() in a client subclass to preview emails with a different currency. Base returns 1 (AdvEmailViewer.php:30-33); used at AdvEmailViewer.php:144-145 via getCurrencyData($this->sampleCurrencyId()). Regression-guarded by tests/Legacy/Settings/AdvEmailViewerHooksTest.php (see Tests).
  • Sample data override (#426): Override sampleEmailData(): array (AdvEmailViewer.php:47-149) to replace the entire base sample-data payload previewed in the email viewer, or override extendSampleEmailData(array $data): array (protected no-op hook, AdvEmailViewer.php:155-158) to add or override individual sample-data keys without duplicating the base payload. index() composes them as $data = $this->extendSampleEmailData($this->sampleEmailData()) (AdvEmailViewer.php:164).
  • Extra sample-data dependencies (#426): Override loadSampleDataDependencies(): void (protected no-op hook, AdvEmailViewer.php:39-41, called at the end of the private initTestEmailViews() at AdvEmailViewer.php:23) to load extra helpers/models needed by a customized sampleEmailData()/extendSampleEmailData().

Business Rules ​

  1. Preview-only: The viewer renders emails with synthetic test data — it does not send actual emails.
  2. Per-language subjects: Subjects are stored per language in the registry, allowing full multi-language customization.
  3. Sample data: Test data is Greek-market (names, addresses); it is returned by the overridable sampleEmailData() seam (AdvEmailViewer.php:47-149), not inline in index() or initTestEmailViews().
  4. No in-browser template editing: The viewer shows rendered output only. Template HTML is edited in code, not through the admin UI.
  5. Customer locale wins: At dispatch, the customer's language is preferred over the admin's language for both subject and body.
  6. SMTP only: Mail is sent via PHPMailer over SMTP using one of two credential sets (EMAIL_CONTACT_FORM or EMAIL_SHOP_MAILER).

Known Issues & Security Gaps ​

  1. getDefaultSmtpCredentials() docblock says "3 options" but only 2 are accepted. ecommercen/helpers/registry_helper.php:35 — the docblock states "There are 3 options: EMAIL_CONTACT_FORM, EMAIL_SHOP_MAILER" but lists only 2; the guard at :41 throws unless the value is exactly EMAIL_CONTACT_FORM or EMAIL_SHOP_MAILER. This is a stale doc-comment defect only — there is no functional gap, and only two purposes actually exist.
  2. sentBirthdayWishes() logs the wrong email type. Adv_mailer.php:542 passes emailType='REMAINING_POINTS' (apparent copy/paste from sentRemainingPoints()) while the subject used is BIRTHDAY_WISHES (L543). Birthday emails are recorded in customer_message_history under type REMAINING_POINTS instead of BIRTHDAY_WISHES.
  3. inform_new_product() flagged BROKEN yet still wired. Adv_mailer.php:273 carries a /* @todo refactor this is client custom and should not be called BROKEN ATM */ comment directly above the method (body at L274), yet it is still invoked from Adv_products_admin.php:280, 736.
  4. Viewer $email_views list incomplete. AdvEmailViewer.php:166-187 omits 3 real standalone templates known to emailViews.json: contact_email_generated, apologize_shipping_delay, return_form. Admins cannot preview any of the three.
  5. Blog-comment emails logged as USER_REVIEW. sendBlogCommentStatusUpdateEmail() passes emailType='USER_REVIEW' (Adv_mailer.php:640), identical to the review flow (L407), so blog-comment moderation emails are misclassified in customer history (minor).
  6. No CSRF token on subject-editor POST. AdvEmailViewer::editEmailSubjects() POST at AdvEmailViewer.php:225 carries no synchronizer token, and $config['csrf_protection'] = false (application/config/config.php:143), so an attacker-hosted auto-submitting cross-origin POST can rewrite every EMAIL_SUBJECTS registry value for a visiting ADVISABLE/ADMIN — those subject lines then ship on real customer email. Mitigated only by the RBAC gate at constructor (line 9).

Tests ​

  • tests/Legacy/Settings/AdvEmailViewerHooksTest.php (245 lines, 11 #[Test] methods) — exercises three client-override seams: sampleCurrencyId() (#422, L45-54, L57-65, L68-89), sampleEmailData() (#426), extendSampleEmailData() (#426), and loadSampleDataDependencies() (#426). Asserts each method exists, is protected, and is overridable. Uses newInstanceWithoutConstructor() on an anonymous subclass to avoid the Admin_c bootstrap.
  • tests/Legacy/Settings/AdvEmailViewerSubjectSuffixTest.php (#61 regression suite) — exercises the stripLanguageSuffix() protected seam: asserts the method exists, is protected, returns ?string (L103-113), and is fork-overridable (L288-308).
  • tests/Unit/Domains/Customer/Customer/PasswordResetServiceTest.php and tests/Integration/Domains/Customer/Customer/PasswordResetServiceIntegrationTest.php — cover the PasswordResetService caller contract (mocks recover_password_mail, asserts unknown addresses do not call the mailer). These verify the AD-53 caller contract, not the viewer itself.

Coverage gaps: no dedicated tests exercise AdvEmailViewer::index() rendering, the $email_views list, emailViews.json template resolution, getDefaultSmtpCredentials(), the Adv_mailer send pipeline (send()/sendAdmin()/doSend()), or editEmailSubjects()'s subject-writing behavior.


  • AD-13 Settings — general settings admin where SMTP credentials (EMAIL_CONTACT_FORM, EMAIL_SHOP_MAILER) are configured
  • AD-52 Review Moderation — dispatches product_review_accept / product_review_reject via sendUserReviewStatusUpdateEmail()
  • SY-11 Review Reminders — sends review_for_facebook / _skroutz / _google via sendCustomerPromptReview()
  • SY-15 Delivery Delay — sends apologize_shipping_delay (orderInformDelay layout) apology emails via sendCustomerInformDelay()
  • SY-24 Email Dispatch — the runtime pipeline (Adv_mailer + Mailer + PHPMailer) that delivers the templates previewed here
  • AD-03 Order Management Admin — order emails are the highest-volume dispatchers
  • CF-10 Customer Auth — PasswordResetService is the live caller of recover_password_mail()
  • CF-29 Contact Forms — dispatches contact_email / contact_email_generated / return_form via sendContactFormEmail() / sendReturnFormEmail()