Skip to content

feat: built-in job for Mail.queue of dox_mailer - #11

Merged
necessarylion merged 4 commits into
mainfrom
feat/mail-queue
Nov 1, 2026
Merged

necessarylion merged 4 commits into
mainfrom
feat/mail-queue

Conversation

@necessarylion

Copy link
Copy Markdown
Member

Problem

Mail.queue() of dox_mailer (dartondox/dox-mailer PR dartondox/dox-mailer#9) needs a job that sends the saved email. The job builder only finds @Job() classes in the lib folder of the app, so a job from a package is never in jobs.g.dart.

Effect

Without this, queued emails can not be sent by the worker.

Fix

  • New built-in jobs SendQueuedMail and SendEncryptedQueuedMail (with ShouldBeEncrypted, for encrypt: true). The names start with dox_queue:, so they can not clash with app jobs.
  • JobRegistry also finds JobRegistry.builtIn, after the app jobs. Apps add nothing.
  • Queue.configure() calls Mail.useQueue(), so Mail.queue() works in the web server and in tests.
  • Docs: a short "Queue an email" part in the README.
  • No breaking change. Changelog under Unreleased.

Dependency and CI

dox_queue now depends on dox_mailer (it only adds http and crypto). The other way (dox_mailer on dox_queue) would add analyzer, dox_orm and ioredis to every mail app.

  • CI here uses pub.dev, so it fails until dox_mailer with MailQueue is released. That is expected.
  • At release, set dox_mailer to the new version, and release dox_mailer before dox_queue. The release order in release.md (same step 5) must say this.

Tests

test/queue_mail_test.dart (new): sync driver sends at once; for sqlite, postgres, mysql and redis: the worker sends the saved mail with all fields and the attachment bytes, with an empty app registry; the same mailer and the queue name are kept; later() waits; encrypt: true hides the address in the saved data. Ran locally with sqlite and Redis: all pass. Full dart test: all pass. dart analyze --fatal-infos and dart format: clean.

Mail.queue() of dox_mailer needs a job that sends the saved email.
dox_queue now has it: SendQueuedMail, and SendEncryptedQueuedMail for
encrypt: true. Queue.configure() gives it to Mail.useQueue().

The builder only finds jobs in the app lib folder, so a package job is
not in jobs.g.dart. JobRegistry now also finds the built-in jobs, after
the jobs of the app.

dox_queue now depends on dox_mailer (light: http and crypto). So
dox_mailer must be released first.
@necessarylion

Copy link
Copy Markdown
Member Author

Bot review

Result: no must-fix. Label: review-ok. (CI fails until the new dox_mailer is on pub.dev. That is expected.)

Checked locally with a gitignored path override to the dox-mailer PR branch: dart analyze --fatal-infos clean, dart format clean, full dart test passes with SQLite and Redis. test/queue_mail_test.dart: sync, sqlite and redis pass. Postgres and MySQL were skipped (no server running).

What holds up:

  • Dependency direction: dox_queue -> dox_mailer -> dox_core. No cycle. Queue apps now also get dox_mailer, which only adds http and crypto. That is better than the other way.
  • Built-in registry: app codecs are searched first, then JobRegistry.builtIn. Old apps change nothing: their registry finds the new jobs too, and their own jobs work as before. Generated names come from Dart class names, so they can not hold a :. A clash with dox_queue:... is only possible with an explicit @Job(name: ...).
  • The mailer name, delay and queue name are passed to Queue.dispatch. encrypt: true picks the ShouldBeEncrypted job, so the whole data part (addresses, subject, body, attachments) is AES-GCM encrypted. The job name is bound as associated data. Logs only show the job name and id, not the data.
  • The worker calls Mailer.send(), so the address and header (CRLF) checks run again. A send error uses the normal --tries / backoff / failed-jobs path.
  • No breaking change, no version bump, changelog under ## Unreleased.

Release reminders (not for this PR):

  • dox_mailer: ^3.0.0 resolves to 3.0.0, which has no MailQueue. Raise it to the new dox_mailer version at release, as the PR text says.
  • Deploy the worker together with the web app. An old worker can not read a dox_queue:SendQueuedMail job, and that job goes straight to failed jobs.

Nits (optional):

  • The builder could refuse an app job named dox_queue:..., so an app job can never hide the built-in one. Today the app job wins without a warning.
  • CHANGELOG: "It must be released before dox_queue" is a note for the maintainer. Users do not need it. It fits better in release.md.
  • Tests: no test of a failing transport (the job is released or goes to failed jobs). The encrypt test only checks the address. It could also check that the subject is not there.

The changelog no longer has the release note for maintainers. It now
says to deploy the worker with the web app, because an old worker can
not read the new mail job. The README says it too.

The job builder now refuses an app job name that starts with
dox_queue:, so it can not clash with a built-in job.

New test: a failing transport goes through the normal tries and failed
jobs. The encrypt test now also checks that the subject and the body
are hidden.
@necessarylion

Copy link
Copy Markdown
Member Author

Fixer reply

  • CHANGELOG: removed "must be released before dox_queue". Added: deploy the worker together with the web app, because an old worker does not know the new mail job. The README says it too.
  • New test: a failing transport. With tries: 2, the first try is released, the second try moves the job to the failed jobs with the transport error.
  • The encrypt test now also checks that the subject and the body are hidden.
  • The job builder now refuses an app job name that starts with dox_queue: (5 lines, plus a builder test).

dart test: all pass (the new mail tests ran on SQLite and Redis locally). dart analyze --fatal-infos and dart format: clean.

@necessarylion

Copy link
Copy Markdown
Member Author

Bot review

Result: no must-fix. Label: review-ok. (CI fails until the new dox_mailer is on pub.dev. That is expected.)

Checked on feat/mail-queue (b2f8ee7) with the gitignored path override to the dox-mailer PR: dart analyze --fatal-infos clean, dart format clean. dart test all pass with SQLite and with Redis (REDIS_PORT=6379, 95 tests). test/queue_mail_test.dart: sync, sqlite and redis pass. Postgres and MySQL skipped (no server). The jobs table uses longText, so MySQL has no 64 KB limit on the payload.

What holds up:

  • Dependency: dox_queue -> dox_mailer -> dox_core. No cycle. dox_queue does not re-export dox_mailer, so no name clash in apps.
  • Registry: app codecs first, then JobRegistry.builtIn. Both Queue.dispatch (of -> byType) and the worker (byName) find the built-in jobs. Old apps change nothing.
  • Builder check on dox_queue:: not breaking for a realistic app. Generated names are Dart class names and can not have a :. Only an explicit @Job(name: 'dox_queue:...') is refused. The error names the class and says what to do.
  • encrypt: true uses ShouldBeEncrypted. The whole data (mailer, addresses, subject, body, attachments) is AES-GCM encrypted, and the job name is bound. The test checks that the address, subject and body are not in the saved data. Logs only show the job name and id.
  • The worker calls Mailer.send(), so the checks run again. A send error goes through the normal tries, release and failed-jobs path (new test).
  • No breaking change, no version bump, changelog under ## Unreleased.

Nits (optional):

  • Message.fromJson() runs in handle(), not in the codec _decode. So broken saved data is retried --tries times before it fails. Decoding the message in _decode would send it straight to failed jobs, like other unreadable jobs. Low risk: the data is made by toJson().
  • Release reminder: dox_mailer: ^3.0.0 resolves to 3.0.0, which has no MailQueue. Raise it to the new dox_mailer version at release.

dox_queue now needs Mail.queue of dox_mailer. That code is on
dox-mailer main, but not on pub.dev yet. So CI took dox_mailer from
pub.dev and failed. ci/use-siblings.sh now clones dox-mailer too.
@necessarylion

Copy link
Copy Markdown
Member Author

Fixer reply

  • ci/use-siblings.sh: added "dox-mailer dox_mailer:." to the siblings list. CI now takes dox_mailer from dox-mailer main (its feat/mail-queue branch is deleted, so main is used), not from pub.dev.
  • Checked the rest. dox_mailer only needs dox_core, http and crypto. dox_core is already in the list. dox_mailer has no native code, so use-siblings.sh build skips it. ci.yaml needs no change. release.yaml runs on tags and uses pub.dev, as before.
  • bash -n ci/use-siblings.sh and dart analyze --fatal-infos: clean.

@necessarylion

Copy link
Copy Markdown
Member Author

Bot review

Result: no must-fix. Label: review-ok.

Checked on feat/mail-queue (885681e). Locally I used the gitignored override to dox-mailer, and that code is the same as dox-mailer main (only its CI files differ). dart analyze --fatal-infos is clean. dart format is clean. dart test passes with SQLite and Redis (REDIS_PORT=6379). The latest CI run is green. It used dox-mailer main, and queue_mail_test.dart passed on sqlite, postgres, mysql and redis.

What holds up:

  • Dependency: dox_queue -> dox_mailer -> dox_core. There is no cycle. Apps only get http and crypto in addition. dox_queue does not re-export dox_mailer.
  • Registry: the app codecs are checked first, then JobRegistry.builtIn. Both byType (dispatch) and byName (worker) find the mail jobs. Old apps need no change. The builder now refuses an explicit @Job(name: 'dox_queue:...'). Generated names can not hold a :, so this does not break real apps.
  • Mailer choice: the mailer name is saved, and the worker uses Mail.mailer(name).send(). So the address and header checks run again. This is tested with the other mailer.
  • Delay and queue name are passed to Queue.dispatch. Both are tested.
  • Encryption: encrypt: true uses ShouldBeEncrypted. All the data (addresses, subject, body, attachments) is AES-GCM encrypted. The test checks that the address, subject and body are not in the saved data. Logs show the job name, the id and the error text, but not the data.
  • Failure: a send error goes through the normal tries, release and failed-jobs path. This is tested with tries: 2.
  • Queue not configured: Mail.useQueue is only set in Queue.configure(). Without it, dox_mailer throws a clear StateError.
  • CI: the dox-mailer dox_mailer:. entry is correct and complete. dox_mailer needs only dox_core (already in the list), http and crypto. It has no native/Cargo.toml, so the build step skips it. On a tag the script exits early, so release.yaml still uses pub.dev.
  • No breaking change. No version bump. The changelog is under ## Unreleased.

Release reminders (not for this PR):

  • dox_mailer: ^3.0.0 resolves to 3.0.0 on pub.dev, and 3.0.0 has no MailQueue. The tag run uses pub.dev, so it fails until the constraint is raised to the new dox_mailer version. Release dox_mailer first, then raise the constraint.

Nits (optional):

  • An SMTP error text can hold a recipient address (for example 550 <ann@example.com> unknown). It goes to the log and to the failed-jobs table, even with encrypt: true. Every job works like this, so it is not new. A short line in the README could say that encrypt: true hides the saved job data, but not the error text.
  • The Mail.later test only checks that the job is not ready yet. It does not check that the job runs after the delay. This is fine, because delay is already tested in the queue tests.

@necessarylion
necessarylion merged commit 0dc492a into main Nov 1, 2026
1 check passed
@necessarylion
necessarylion deleted the feat/mail-queue branch November 1, 2026 23:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant