Skip to content

Mail

Send a mailable from the PostPublished listener:

await Mail.to(author.email).send(PostPublishedMail(post));

Amarna Mail (amarna) builds messages as mailables and sends them through SMTP, the application log, or an in-memory array transport. New Maat projects register MailServiceProvider and include the configuration below.

Configuration

config/mail.dart selects the default named mailer and sender:

final Map<String, dynamic> mail = {
'default': env('MAIL_MAILER', 'log'),
'mailers': {
'smtp': {
'transport': 'smtp',
'host': env('MAIL_HOST', '127.0.0.1'),
'port': envInt('MAIL_PORT', 587),
'username': env('MAIL_USERNAME'),
'password': env('MAIL_PASSWORD'),
'encryption': env('MAIL_ENCRYPTION', 'tls'),
},
'log': {'transport': 'log'},
'array': {'transport': 'array'},
},
'from': {
'address': env('MAIL_FROM_ADDRESS', 'hello@example.com'),
'name': env('MAIL_FROM_NAME', 'Example'),
},
};

Use log locally to inspect messages without delivering them. Use array in tests that need the assembled Email, and smtp for delivery. tls upgrades the SMTP connection with STARTTLS; ssl opens an SSL connection immediately; an empty encryption value allows an insecure connection.

Mailables

Generate a mailable:

Terminal window
maat make:mail PostPublishedMail

Define its envelope, content, and attachments:

import 'package:amarna/amarna.dart';
class PostPublishedMail extends Mailable {
PostPublishedMail(this.post);
final Post post;
@override
Envelope envelope() => const Envelope(
from: Address('hello@example.com', 'Maat Blog'),
subject: 'Your post is live',
replyTo: [Address('support@example.com')],
);
@override
Content content() => Content(
view: 'mail.post_published',
text: 'Your post "${post.title}" is now live.',
data: {'post': post},
);
@override
List<Attachment> attachments() => const [];
}

view renders through Khnum and becomes the HTML body. text is the plain text alternative. Without an explicit subject, the class name becomes a headline: PostPublishedMail becomes Post Published Mail.

Sending

await Mail.to(author.email).send(PostPublishedMail(post));
// Uses recipients declared by Envelope.to/cc/bcc.
await Mail.send(WeeklyDigestMail());
await Mail.raw('Service restored', (email) {
email
..to.add(const Address('ops@example.com'))
..subject = 'Status update';
});
// Select a configured mailer instead of the default.
await Mail.mailer('smtp').to('ada@example.com').send(WelcomeMail());

Recipients may be strings, Address values, or application objects exposing a String email field and optional String name field.

Transports

The built-in transport names are smtp, log, and array. Add a custom one before its mailer is first resolved:

MailManager.extend('archive', (settings) => ArchiveTransport(settings));

The custom class implements Transport.send(Email email). Named mailers are created lazily and cached by MailManager.

Delivery Events

MessageSending runs before transport I/O. Return false to cancel delivery:

Event.listen<MessageSending>((event) {
if (event.email.hasTo('blocked@example.com')) return false;
});
Event.listen<MessageSent>((event) {
Log.info('Sent ${event.email.subject}');
});

MessageSent fires only after the selected transport succeeds.

Testing

final mail = Mail.fake();
await Mail.to(author.email).send(PostPublishedMail(post));
mail.assertSent<PostPublishedMail>();
mail.assertSent<PostPublishedMail>((message) => message.post.id == post.id);
mail.assertNotSent<WeeklyDigestMail>();
mail.assertSentCount(1);

mail.sent<T>() returns matching mailables. mail.emails contains raw emails and emails sent directly, including notification-channel messages.

Mail is sent inline and send() completes only after the transport finishes. Queued mail will be added with the queue package; Amarna does not pretend an inline send is queued.