diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index df4fdd6c..c67b9ab6 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -3,6 +3,9 @@ name: Build and push Docker image on: push: branches: [main] + # Releases. `latest` moves only when one of these is pushed — see the + # tag rules in the merge job. + tags: ['v*'] pull_request: branches: [main] @@ -122,10 +125,19 @@ jobs: uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + # `latest` used to follow the default branch, so every push + # to main went straight to the tag the README tells people to + # pull — including work in progress, and migrations that run + # automatically on boot. It now moves only on a release tag. + # + # `main` still publishes, under its own name, for anyone who + # wants the tip. tags: | type=ref,event=branch type=sha,prefix=sha- - type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable=${{ startsWith(github.ref, 'refs/tags/v') }} - name: Create manifest list and push working-directory: ${{ runner.temp }}/digests diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..5f082009 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,55 @@ +# Changelog + +Notable changes per release. plMail runs its migrations automatically on boot, +so anything that changes the schema irreversibly is called out explicitly. + +The published image tags: `latest` follows the most recent release below, +`main` follows the tip of the default branch, and `sha-…` pins one commit. + +## Unreleased + +### Added + +- **Snooze.** Put a conversation away and have it come back later — from the + message row, the list toolbar, or a JMAP client via the `Thread/set` + extension. Snoozed conversations get their own sidebar entry and view. + `app:mail:wake-snoozed` runs every minute to bring them back. +- **JMAP draft attachments.** `Email/set` now accepts uploaded blobs in + `attachments`, per RFC 8621; drafts created over JMAP previously dropped them + silently. +- **Login throttling.** Five attempts per account per fifteen minutes on the + password form, and a separate limit on the two-factor code form — which the + firewall's throttling does not cover, and which is the form an attacker + reaches holding a stolen password. +- **PHPStan** at level 5, with a baseline, in CI. **Dependabot** for composer, + npm and GitHub Actions. +- **LICENSE.** The project has always said AGPL-3.0 in its README; the licence + text is now actually distributed with it, and `composer.json` no longer + contradicts it by claiming "proprietary". + +### Fixed + +- **Snoozing from the web UI did nothing but set a timer.** No labels moved and + nothing propagated, so the conversation stayed in the inbox — locally and at + the provider — while its row vanished from the list. The sweep would then + "wake" a thread that had never left. +- **The snooze button on a message row cleared a snooze instead of setting + one**, and the toolbar's snoozed everything to a hardcoded 8am tomorrow. +- **"Test connection" in account settings crashed** with an `ArgumentCountError` + whenever the form was incomplete or the password blank — the two cases it + exists to report. +- **The admin Graph category sync caught an exception class that does not + exist**, so Graph failures propagated instead of degrading quietly. +- **JMAP `Email/set` rejected valid mailbox patches.** Current `mailboxIds` were + emitted as label ids where the protocol expects per-account binding ids; since + both are autoincrement ints, a patch removing one mailbox failed with "No such + Mailbox". + +### Changed + +- `latest` is published from release tags only. It previously followed every + push to `main`, which meant the tag the README tells people to pull could + carry unreviewed work and automatic schema migrations. +- The login form no longer shows a "forgot password?" link. There is no reset + flow — recovery is a console command — and the link was an `href="#"` that did + nothing. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d2e2d0f3..4b75fc8a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -317,11 +317,18 @@ each. ## Roadmap -- [ ] Complete the label-based architecture refactor (Label as the user-facing concept; Mailbox demoted to IMAP sync infrastructure) +- [x] Complete the label-based architecture refactor (Label as the user-facing concept; Mailbox demoted to IMAP sync infrastructure) - [x] Sanitize rendered HTML bodies - [x] Full-text search - [x] Microsoft OAuth2 / Graph send support -- [ ] Gmail-native `threadId` threading (currently RFC Message-ID based) -- [ ] Incoming IMAP flag sync over the IDLE stream -- [ ] Avatar fetching (once OAuth avatar scopes are wired) -- [ ] Nested label UI +- [x] Nested label UI — `Label::$parent`, `LabelRepository::findVisibleTreeForUser()`, and the recursive tree in `_partials/_sidebar.html.twig` +- [x] Snooze — `ThreadSnoozeService`, `app:mail:wake-snoozed`, and the `Thread/set` JMAP extension +- [ ] Gmail-native `threadId` threading (the id is carried on `Message`, but threading is still RFC Message-ID based) +- [ ] Incoming IMAP flag sync over the IDLE stream — flags currently travel outward only, so marking a message read in another client does not reflect back +- [ ] Avatar fetching (partially done: `Service/User/AvatarFromIntegration.php`; still needs OAuth avatar scopes) +- [ ] Per-identity signatures — no `signature` field exists on `User`, `Account` or `EmailAlias` +- [ ] Password reset (recovery is console-only today, which is why the login form no longer offers a link) + +> Keep this honest. Three items above were finished long before anyone ticked +> them, and a roadmap that under-reports is worse than none — it is the document +> a contributor reads to decide what to pick up. diff --git a/config/reference.php b/config/reference.php index 925f7079..7deacc25 100644 --- a/config/reference.php +++ b/config/reference.php @@ -629,7 +629,7 @@ use Symfony\Component\Config\Loader\ParamConfigurator as Param; * }>, * }, * rate_limiter?: bool|array{ // Rate limiter configuration - * enabled?: bool|Param, // Default: false + * enabled?: bool|Param, // Default: true * limiters?: array