Check emails
Services email people: a label to a shop, a delivery notice, a password reset. Your service sends its mail as it does in production, by SMTP, to the mail server of your test environment, and the mail pack reads the mailbox that catches it.
Scenario: A shop gets its parcel's label by email Given a seeds/mail-registered.yaml db seed And a POST request to /api/parcels And a request payload using an application/json content example And the request payload property reference is 'PX-MAIL-9701' And the request payload property sender is 'juniper-and-jay' When the request is executed Then the response status code is 201 And within 10s the shops mailbox has an email where: | to | orders@juniper-and-jay.example | | subject | Parcel PX-MAIL-9701 registered | | attachment | PX-MAIL-9701-label.zpl |Add the pack to the project with axx pack add mail (Choose packs).
Which mailbox
Section titled “Which mailbox”How your service sends its mail decides where a test finds it:
| Your service sends | In the tests | The check |
|---|---|---|
| by SMTP, to a relay or a provider’s SMTP endpoint | a mail server that catches it: smtp4dev, which can also stub refusals, or Mailpit, GreenMail, Inbucket | a mailbox with an imap:// or pop3:// url, or Mailpit’s, read through its API |
| by SMTP, and it must really arrive | a real inbox, in a staging environment | a mailbox with an imaps:// url |
| through a provider’s HTTP API (SendGrid, SES, Mailgun, Postmark, Resend) | WireMock, against the provider’s contract | the mock pack: the request’s payload properties |
Point your service’s SMTP settings at the mail server of the test environment, as the parcels example does in its Compose file:
services: mail: image: rnwood/smtp4dev:3.15.0 ports: ['1025:25', '1143:143', '8025:80'] # SMTP, IMAP, its web interface app: environment: PARCELS_SMTP_ADDR: mail:25Register the mailbox
Section titled “Register the mailbox”Background: Given the shops mailbox with the following properties: | url | imap://${sys:local.host}:1143 | | username | axx | | password | axx |url: Mailpit’s address (http://orhttps://), orpop3://,pop3s://,imap://orimaps://and the mail server’s host.usernameandpassword: sign in over POP3 and IMAP, or to a Mailpit behind a password. The password is masked.folder: the IMAP folder,INBOXunless it says otherwise.
smtp4dev takes any username and password over IMAP, and its INBOX has every email it received. Mailpit answers POP3 once it is given a password file (--pop3-auth-file, or MP_POP3_AUTH).
Check an email
Section titled “Check an email”Then within 10s the shops mailbox has an email where: | to | orders@larkspur-lane.example | | subject | Parcel PX-MAIL-9702 delivered | | html | <b>PX-MAIL-9702</b> was delivered in Leipzig || Row | What it checks |
|---|---|
to, cc |
an address the email went to, or was copied to |
from |
the address it came from |
subject |
its subject, all of it |
text |
text its plain-text body contains |
html |
text its HTML body contains, tags and all |
attachment |
the file name of an attachment |
header <name> |
a header’s value |
Addresses compare without regard to case, and text and html take runs of spaces and line breaks as one space. Subjects and names in other charsets are decoded first. The check waits for an email that meets every row: 10 seconds, or the time within gives. When none does, it lists the emails that arrived.
Refuse mail
Section titled “Refuse mail”What does your service do when a mail server refuses its email? Stub the refusal in the mail server, as you stub an HTTP dependency’s failures in WireMock’s mappings: by the data a scenario uses. smtp4dev runs a JavaScript expression for each recipient, and error(code, message) refuses it with that SMTP code:
services: mail: image: rnwood/smtp4dev:3.15.0 environment: # quince-and-quill's mail server is busy: it asks to be tried again later. ServerOptions__RecipientValidationExpression: >- recipient.toLowerCase().endsWith("@quince-and-quill.example") ? error(451, "Mailbox busy, try again later") : trueA scenario whose shop is quince-and-quill then sees how your service copes:
Scenario: A registration email the shop's mail server refuses does not stop the registration Given a seeds/mail-refused.yaml db seed And a POST request to /api/parcels And a request payload using an application/json content example And the request payload property sender is 'quince-and-quill' When the request is executed Then the response status code is 201- A 4xx code, like 451, is a failure the sender should retry; a 5xx, like 550, one it should not.
- The stub refuses only the recipients it names, so scenarios running beside it are not affected: give each refusal a recipient of its own.
CommandValidationExpressionrefuses at other stages, such as the sender’smail from:(smtp4dev passes the command’s verb in lowercase), andMessageValidationExpressionrefuses whole messages.
Keep scenarios apart
Section titled “Keep scenarios apart”A check only looks at the emails that arrived since its scenario started, and axx never deletes or marks what it reads. Scenarios running in parallel share the mailbox, so each checks its own mail by a recipient or a subject unique to it, such as a shop or a parcel reference of its own.
For a run, axx reads each mailbox the checks name from when the services are up:
- Mailpit and IMAP say when each email arrived.
- POP3 doesn’t, so axx takes what the mailbox held when it started reading as old.
See the mail pack’s reference for every step.