This file provides guidance to AI coding assistants working with code in this repository.
composer i
npm ciSee package.json scripts for all available commands (build, dev, watch, lint, stylelint, test:unit, test:e2e, etc.).
Available composer commands:
composer cs:check # Check code style
composer cs:fix # Fix code style
composer psalm # Run static analysis
composer test:unit # Run unit tests
composer test:integration # Run integration tests
composer openapi # Generate OpenAPI specSee composer.json for all available commands.
- Backend: PHP (see
appinfo/info.xmlfor version requirements), Nextcloud app framework, Horde IMAP/MIME/SMTP libraries. Namespace:OCA\Mail\. - Frontend: Vue 2, Pinia, Vue Router 3, CKEditor 5, bundled with webpack.
Layered: Controllers β Services β DB Mappers.
Controller/β Thin HTTP handlers; business logic lives in services.Service/β Core logic. Key areas: account management, IMAP sync, mail sending (SMTP), drafts/outbox, S/MIME encryption, ML-based importance classification, AI integrations (thread summaries, follow-up detection).Db/β NextcloudQBMapper-based mappers and entity models.IMAP/β Low-level IMAP via Horde.IMAPClientFactorycreates authenticated clients;MessageMapperfetches raw messages.BackgroundJob/β Nextcloud background jobs for IMAP sync, ML training, outbox sending, etc.Listener/β Event listeners hooked to domain events fromlib/Events/.Contracts/β Interfaces defining main service boundaries (IMailManager,IMailTransmission, etc.).Migration/β Database migrations.
Single-page Vue 2 app. All routes render through views/Home.vue.
store/mainStore.jsβ Central Pinia store (accounts, mailboxes, messages, preferences), split intoactions.jsandgetters.js. Separate stores for outbox and mail filters.service/β JS services that call the PHP REST API.components/β Vue components (composer, envelope list, thread view, settings, etc.).router.jsβ Routes for mailbox, thread, outbox, and setup views.
- Registration:
appinfo/info.xmlregisters background jobs, CLI commands, settings pages, navigation entries, and repair steps.AppInfo/Application.phpregisters event listeners and other services via the Nextcloud bootstrap API. - Events: Domain events in
lib/Events/are dispatched after state changes;lib/Listener/reacts to them. - Mozart: Some vendor packages are namespaced into
lib/Vendor/to avoid conflicts. - REUSE & SPDX: Every file requires an SPDX license header. New files must use
AGPL-3.0-or-later, neverAGPL-3.0-only. Header format:/* * SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later */
- OpenAPI:
ResponseDefinitions.phpdocuments API types; runcomposer openapito regenerate the spec.
The diff shows what changed and how; a comment exists only to capture why β intent the code and names can't convey on their own.
- Default to none. Most changes need zero comments; express intent through clear names and small functions. This codebase uses essentially none.
- Comment the why, never the how. Legitimate reasons: a non-obvious workaround (link the issue/ticket), a subtle business rule, a deliberate deviation from the expected. Never narrate what the next lines do.
- No multi-line explanatory blocks, AI-style walkthroughs, or section banners.
- Remove a stale comment only when you're already editing that code for the task at hand β no drive-by cleanups.
These are the points maintainers raise again and again in review. Following them up front keeps review focused on design instead of the same recurring notes. CodeRabbit reads this file automatically and reviews against these conventions, so the bot flags the same things β but the author should not need the bot to learn them.
- One concern per PR. Keep unrelated edits, drive-by refactors, code-style churn, dependency/lock-file bumps and new runtime-version support out of a feature PR β put them in a separate PR so they stay backportable to stable branches. Small, single-purpose PRs sail through; large diffs draw change requests.
- The commit type must match the change (see
.github/CONTRIBUTING.md): moving code with no behavior change isrefactor:, notfeat:. Scopes stay broad (imap,ui). - Reuse before reinventing. Grep for an existing helper, mapper, constant or pattern and mirror it instead of writing a parallel implementation.
- Types over PHPDoc. Prefer native param/return/property types; drop PHPDoc that only
restates them; keep
@throwsand anything that adds information. Use precisepsalm-typearray shapes (reuse existing shape definitions) instead of barearray. Handle Psalm's possible-null andstring|false(e.g.file_get_contents) results β throw aServiceException, don't cast the failure away. Use constructor property promotion and strict comparison (===,in_array(..., true)). - Exceptions. Catch narrowly β never a blanket
\Exception/\Throwablefor a specific failure. When wrapping, pass the original as$previous(or rethrow) so the stack trace survives. Throw domain exceptions; don't leak abstraction-layer exceptions (DoesNotExistException, storage exceptions) out of a service. Keep@throwsin sync with the interface contract. - Logging. Inject
LoggerInterface; log with a meaningful message and['exception' => $e]context at the right level. Log non-critical conditions at info/debug and return rather than throwing or spamming warnings. Never swallow errors. - Dependency injection & layering. Inject collaborators (incl. OCP services) via the
constructor, not the service locator. Replace
time()withITimeFactoryfor testability. Don't inject request data such asuserIdinto services (controllers only) β pass it as an argument so the service stays usable from background jobs. Keep controllers thin (request/response only); business logic and DB access belong in services; keep constants in their owning class. - Nextcloud API boundaries. Never use another app's private
\OCA\OtherApp\*API β go through a stable OCP interface. Respect the minimum server version inappinfo/info.xml; guard newer OCP APIs withmethod_exists+ a fallback. Store new config viaIAppConfig(no dots in new keys). Controllers use#[NoAdminRequired]for non-admin routes (omit it on admin-only ones), HTTP 422 for validation errors,HTTP::STATUS_*constants, andTrapErrorover hand-written try/catch. Keep 400 (client error) vs 404 (not found) intentional. Prefer kebab-case URLs with the id in the path and a singleresourceroute. - Controller access control. Check that the current user owns the resource before
acting on an incoming id β guard against IDOR, don't act on a guessed id. Take a nullable
?string $userIdfrom the predefined core services and return 401 when it is null.
- Let mappers propagate
DoesNotExistException/OCP\DB\Exceptionto the caller β don't catch not-found inside the mapper. Reuse theQBMapperinsert/update/findhelpers instead of hand-writing queries. - Name accessors
findX/getX(notstore); keep entity@methodannotations accurate, includingint|nullfor nullable columns; register column types withaddTypein the constructor.
- Re-runnable: check column/table existence before changing schema so a partially failed migration can retry.
- Version naming: name the
Versionclass after the next unreleased minor and bumpversioninappinfo/info.xml, or the migration won't run. (See the DB index dual pattern for adding indices without a blockingchangeSchema.) - Foreign keys on referencing columns, with the delete action chosen from the
relationship: cascade delete for rows the parent owns (so account/mailbox deletion leaves
no orphans),
SET NULLfor an optional reference,RESTRICTto prevent deletion. Clean up dangling rows pre-schema. - Indexes: composite column order matters (
[a,b]β[b,a]) β match the query's WHERE/ORDER BY; add covering indexes on exactly the filtered/joined columns; make the index unique when the column is. - Portability & size: sensible column lengths (avoid MariaDB off-row storage), truncate
before writing fixed-width columns, Oracle needs nullable booleans and treats empty
strings as
NULL, Postgres/Oracle are strict about VARCHAR-vs-INT. Batch huge UPDATE/DELETE and emit progress. Inline entity constants in migrations (a loaded class can hold stale values mid-upgrade).
- Push filters/limits/cursors into the DB query; scope by
user_id; useWHERE ... INto avoid N+1; don'tarray_mergein a loop. Reuse one Horde IMAP client across a bulk operation instead of reconnecting per message. Stream large result sets via generators and guard against OOM β users can have 200+ mailboxes. Prefer a local cache over a distributed one for recomputable values and scope cache keys per user.
- Async:
async/awaitwithtry/catch, never mixed with.then; neverawaitinsideforEach(usefor...of); a missingawaitis a real bug. Await sequential per-item dispatches instead of flooding the backend, and don't fire one request per list item β push it to the backend or preload via initial state. - Structure: HTTP handling in the service layer, mutations inside store actions, business logic out of components. Add a loading/disabled state to any control that triggers an async action so a double click can't fire it twice. Don't make an element look clickable when its action is unavailable.
- Style of code: early returns over nested conditions, named constants over magic
numbers,
const/letnevervar, pure helpers free of side effects and store access. Sanitize user-controlled values before they reach the DOM. UseisDarkThemefrom@nextcloud/vue, notwindow.matchMedia('(prefers-color-scheme: dark)'). Follow the dev-manual naming the linter can't check: multi-word PascalCase component names (SettingsView, notSettings), prefixed sub-components, and acronyms with only the first letter capitalised (callHttpApi). - CSS: see Styling; keep styles scoped, follow BEM, prefer a modifier class
over manipulating inline style, use grid/spacing/breakpoint CSS variables (no hard-coded
breakpoints), and remove now-unused styles. Avoid
::v-deep/!importantinto upstream component internals; where a deep selector is genuinely needed, comment why so it isn't dropped by mistake.
- Wrap every user-facing string, including aria-labels, in
t('mail', β¦). Use one string with placeholders β never concatenate translated fragments (translators reorder words). Usen('mail', β¦)with a%nplaceholder for counts. Never compare against a hard-coded English string or use a translated string as a key. No HTML inside a translation string β translate the plain parts and HTML-encode the inserts.
- Real anchor (
<a href β¦ target="_blank">) for links, not a JS click handler, so screen readers and native middle-click work. Use semantically correct elements and letNcButtoninject the required a11y attributes rather than hand-rolling clickable markup.
- IMAP UIDs are only unique within one mailbox β never treat them as global identifiers; key on the database primary key.
- The app already has building blocks (trusted senders, RFC-2822 address parsing,
IMAPClientFactory,Horde_Mail_Rfc822_Identification) β reuse them.
Located in tests/Unit/ with structure mirroring lib/.
- Use arrange-act-assert structure with blank lines separating each phase (no literal comments)
- Mock dependencies via
$this->createMock(Interface::class) - Setup mocks in
setUp()for common fixtures - Cover the error and edge paths, not just the happy path (empty input, the throwing branch, both sort orders); new classes and changed logic need tests
- Declare typed fixture properties (
private Foo&MockObject $foo;) to avoid dynamic-property deprecation warnings; test methods returnvoid - Only mock external collaborators, never the class under test; assert arguments with
->with(...)and, instead of the removedwithConsecutive, branch on the argument withmatch/ifso behavior depends on input, not call order - Use the same constants in tests as in production code (don't hard-code their values)
- Hand-written stubs of another app's API give false safety β Psalm keeps passing when the upstream API changes, so keep them in sync or avoid them
composer test:unit # Run all unit tests
composer test:unit -- tests/Unit/Service/HtmlTest.php # Run specific test file
composer test:unit -- --filter="TestClassName" # Run tests matching filterLocated in tests/Integration/.
composer test:integration # Run all integration tests
composer test:integration -- tests/Integration/IMAP/MessageMapperTest.php # Run specific test file
composer test:integration -- --filter="TestClassName" # Run tests matching filter
composer test:integration:dev # Run and stop on first failureDo NOT commit changes unless explicitly asked to do so.
After completing code changes:
- Verify your work is complete and tests pass
- Never push directly to
mainβ always create a feature branch with a descriptive name (e.g.perf/imap-selective-headers,fix/sync-token,chore/update-agents). - Worktree branches must use descriptive feature-branch names, not generated names like
agent-xxxx. - Make sure there is no trailing whitespace
- Leave changes in working directory or staged (do not commit)
- Provide a summary of what was changed and why
- Suggest a commit message using Conventional Commits format
- There is a contributing doc with suggestions
- The user will review and commit when ready
Once a branch is pushed and under review, do not force-push. Reviewers track changes incrementally β a force-push destroys that history and forces them to re-read the full diff from scratch.
Instead, address feedback with fixup commits:
git commit --fixup=<sha> # targets the specific commit being correctedThe branch will be rebased and squashed into a clean history before merge (CI enforces this). The failing "clean history" CI check is intentional and expected during review β ignore it until the PR has a positive review, then rebase to clean up.
Commit messages are the project's durable record β read years later through git log and git blame. The diff already shows what changed and how; the message exists to capture intent: the why.
- Subject: conventional-commit, imperative, concise β aim for under 60 characters, or GitHub truncates it.
- Body: state the facts plainly β the problem and the reason for this approach. Keep it matter-of-fact; no storytelling, filler, or marketing tone. Omit it only when the subject already conveys the intent.
- Record what was intentionally left out. Note deliberate omissions and deferrals (and why) so a later reader can tell a conscious decision from an oversight.
- Don't restate the diff. Avoid mechanical bullet lists that echo the changed lines.
All commits must include two trailers at the end:
- Agent/model attribution:
Assisted-by: <AgentName>:<model-id> - DCO sign-off: Use
git commit -sto add automatically
When committing, use: git commit -m "message" -s
This ensures the sign-off includes your configured Git user email.
Example:
fix(imap): tolerate servers that omit UIDVALIDITY on SELECT
Some proxies drop UIDVALIDITY, which made us discard the local
cache and force a full resync on every run. Treat a missing
value as unchanged instead.
Assisted-by: Devstral:devstral-small-2507
Assisted-by: ClaudeCode:claude-sonnet-4-6
Assisted-by: Qwen:qwen3-coder-32b
Assisted-by: Copilot:gpt-4o
Signed-off-by: Name <email>
For all CSS colors, spacing, and dimensions, you must use the standard Nextcloud CSS variables.
Do not leave any magic numbers. If you need more specific control over dimensions use calc(x*var) when necessary.
You can find the CSS variables already in use in this repository, and the full documentation available at this link: https://docs.nextcloud.com/server/latest/developer_manual/html_css_design/css.html.