frontend/src/: React frontend code. Entry point:main.tsx.backend/: PHP backend code.hooks/: Route definitions.ajax.php: AJAX routes.api.php: REST API routes.
app/HTTP/Controllers/: PHP controllers for handling requests.app/Providers/*ServiceProvider.php: wp-kitContainer-backed DI.Plugin::__construct()registers every provider at load time;Plugin::loaded()hooksregisterProviders()toinit:11, which calls$app->boot()on all of them (kept atinit:11for backward compatibility). Providers whoseregister()has WP-hook side effects (installer/activation, abilities) must run at load time, notboot()— activation fires beforeinit:11ever runs.MailServiceProvider::boot()force-resolvesWpMailBridgeso its constructor'spre_wp_mail/wp_mail_succeededhooks actually register (a lazy singleton would silently never wire up if nothing else resolves it first). Access the container viaPlugin::instance()->app(). There is no global on/off switch:WpMailBridgetakes overwp_mailwhenever an enabled, sendable connection exists (per-connectionenabledis the only gate; a stored top-levelenabledkey inbit_smtp_optionsis a retired 1.x/2.0-dev leftover, ignored on read).app/Settings/PluginSettings.php: schema + repository for the plugin's global preferences, backed by wp-kitSettings. Stored as a single option,bit_smtp_preferences(never re-prefix withConfig::withPrefix()— read/write it with plainget_option()/delete_option()or throughPluginSettings/SettingsRepository, notConfig::getOption()). Distinct from the legacy, vestigialbit_smtp_settingsoption.PluginSettings::getWithLegacyFallback()reads the legacy option only while the preferences blob is entirely absent/empty; once seeded, the blob is the sole source of truth for that key, even if the legacy option is later changed. Seeding runs once, gated by aConfig::DB_VERSIONbump, via theBitSmtpSettingsSeedmigration (InstallerProvider::migration()).- Cron: wp-kit
Scheduler, wired inCoreServiceProvider(retention_gcdaily job,Config::RETENTION_GC_HOOK).deactivate()clears it by explicit hook name viawp_clear_scheduled_hook(), not through aSchedulerinstance — deactivation may fire before any provider'sboot()runs this request. - Cache: wp-kit
CacheManager(transientstore,bit_smtp_prefix), consumed byMailAnalyticsService::overview()(5-minute TTL, success-only — a transient DB error is never cached).TransientStore::flush()is a documented no-op (WordPress can't enumerate/bulk-delete transients); a test or job that must invalidate a specific entry needs the exact cache key, not a flush.
- Planning
- Always start with a detailed todo list for the task.
- Break down complex tasks into actionable steps.
- Implementation
- Implement one todo at a time.
- After each step, run lint, syntax check, and tests before proceeding.
- Feedback Loop
- Use a feedback loop: after each step, review results and clarify requirements if needed.
- Ask for user feedback early to avoid unnecessary work and overuse of tokens.
- Adjust plan based on feedback before continuing.
- PHP suites are split (see
phpunit.xml.distvsphpunit.integration.xml):composer test:unitruns onlytests/Unit+tests/Golden(no WordPress/DB, Brain\Monkey). Tests that touch$wpdb, REST routes, orget_plugins()belong intests/Integration(composer test:integration, needs the dockerdb/mailpitservices), not in the unit suite. IntegrationTestCase::setUp()deletes thebit_smtp_preferencesoption before every test (mirrors itsoptions/failure_notification_activeresets), so any test seeding a legacy option (e.g.Config::updateOption('log_retention', ...)) actually reachesPluginSettings::getWithLegacyFallback()'s fallback path. A test that needs to exercise the seeded-preferences path must writePluginSettingsexplicitly.wp-kit'sResponsestatic-singleton state (used by REST/Ability controllers) resets via the publicResponse::reset(), not reflection into private properties — the property name is an implementation detail that has already changed once ($_instance→$_current) across a wp-kit bump.- Frontend tests are vitest, colocated as
*.test.tsx. Gates:pnpm test,pnpm lint,pnpm build.
bitapps/wp-kitis required by stable tag (^2.5) and imposter-scoped intoBitApps\SMTP\Deps\BitApps\WPKit\. To pick up wp-kit changes:composer update bitapps/wp-kit(re-runs Imposter). Unit-test spies subclass wp-kit classes (e.g.StaticRouter), so a wp-kit signature change surfaces as a unit-suite fatal. wp-kit's own floor is PHP 8.0; this plugin's is 8.1 — don't rely on 8.1+ syntax inside wp-kit-authored code.composer.lockis in.prettierignore— thelefthook.ymlprettypre-commit hook runspretty-quick --stagedwith no glob filter, and prettier's JSON formatting fights composer's own.php-cs-fixer(composer lint) reformatsbackend/app/Mail/MailMessage.phpwhen run under PHP 8.5 (newer than the project's 8.1 floor); lint only files you actually changed, don't run it repo-wide on a newer PHP.
- Keep frontend and backend logic separated.
- Use the provided entry points and route files for new features.
- Follow existing code patterns for controllers and routes.
- Reference key files and folders for examples of project structure and conventions.
- For a new API endpoint:
- Add route in
backend/hooks/api.php. - Implement logic in a controller under
backend/app/HTTP/Controllers/. - Backend Configuration in
backend/app/Config.php - Request validator in
backend/app/HTTP/Requests - Request Middleware in
backend/app/HTTP/Middleware - Services in
backend/app/HTTP/Services - Update frontend in
frontend/src/as needed. - Frontend uses antd, typescript
- Add route in
Always plan, implement, lint, check, and test in order. Use feedback to clarify and avoid wasted effort.
Keep this file for knowledge useful to almost every future agent session in this project. Do not repeat what the codebase already shows; point to the authoritative file or command instead. Prefer rewriting or pruning existing entries over appending new ones. When updating this file, preserve this bar for all agents and keep entries concise.