Imported from tekartik/mail.dart (
packages/mail/skills/tekartik-mail-api/SKILL.md). Install upstream withnpx skills add tekartik/mail.dart --skill tekartik-mail-api. Copyright stays with the author.
Abstract mail API (tekartik_mail)
tekartik_mail is a tiny provider-agnostic email API: a MailService
interface, the message model it takes, and an email validation helper. It has
no dependency and no implementation of its own; concrete senders live in
separate packages (tekartik_mail_aws_ses, tekartik_mail_aws_ses_node,
tekartik_mail_mailjet, tekartik_mail_tk).
Guidelines
- Dependency (git, not on pub.dev):
dependencies: tekartik_mail: git: url: https://github.com/tekartik/mail.dart path: packages/mail - Imports:
package:tekartik_mail/mail.dartexports the whole model:MailService,MailMessage,MailRecipient,MailAttachment,SendMailResult. That is the only import application code needs.package:tekartik_mail/mail_mixin.dartre-exportsmail.dartand addsMailServiceMixin; import it (instead ofmail.dart) when writing aMailServiceimplementation.package:tekartik_mail/utils/mail_utils.dartadds the standalonevalidateEmail(String value)function. It is not re-exported bymail.dart; import it explicitly.
- Write shared code against
MailServiceand inject the concrete service at the edge of the app.MailServicehas exactly two members:bool get supportAttachmentsandFuture<SendMailResult> sendMail(MailMessage message). MailMessageis immutable and built entirely in its constructor; there are no setters and nocopyWith.fromandsubjectare the only required named parameters —fromis required but nullable, so passfrom: nullexplicitly when the backend defines the sender itself (some services do, e.g. a server-side template sender).to,cc,bcc,replyToareList<MailRecipient>?: null and[]are both "none", but prefer null for "not set". Provide at least one oftextandhtml; passing both lets the client pick.MailRecipient(email: ..., name: ...)has value equality on(email, name)and atoString()of'Name <email>', or justemailwhennameis null or empty. UsetoString()when building a raw header, do not concatenate by hand.MailAttachment(mimeType:, filename:, content:)takes raw bytes as aUint8List(import 'dart:typed_data'), never a base64 string: each backend encodes for its own wire format. ChecksupportAttachmentsbefore attaching anything — a service that returns false may silently drop them or throw.SendMailResultis abstract with a single nullablemessageId; each implementation returns its own class. Never construct oras-cast it to a backend type in shared code, and treat a nullmessageIdas "sent, id unknown" rather than a failure. Failures are thrown, not returned.- Implementations:
class MyMailService with MailServiceMixingetssupportAttachments => falsefor free and only has to implementsendMail; overridesupportAttachmentswhen the backend does support them. The mixin has noonclause, so it applies to any class. validateEmailis a regexhasMatchand is NOT anchored: it accepts any string that contains an address, and it requires a dotted domain ('a@b'is false,'a@b.c'is true). Trim and reject whitespace yourself before trusting it, and use it for input hints only — never as proof that an address is deliverable.- Testing: the package ships no fake. Write a small recording service with
MailServiceMixin(see the last example) and assert on the capturedMailMessages instead of hitting a real provider. Rundart test.
Examples
A reusable in-memory service
import 'package:tekartik_mail/mail_mixin.dart';
/// Result of an in-memory send.
class MemorySendMailResult implements SendMailResult {
@override
final String? messageId;
MemorySendMailResult(this.messageId);
}
/// Collects messages instead of sending them.
class MemoryMailService with MailServiceMixin {
final sentMessages = <MailMessage>[];
@override
bool get supportAttachments => true;
@override
Future<SendMailResult> sendMail(MailMessage message) async {
sentMessages.add(message);
return MemorySendMailResult('memory-${sentMessages.length}');
}
}
Building and sending a message
import 'dart:convert';
import 'dart:typed_data';
import 'package:tekartik_mail/mail.dart';
Future<String?> sendInvoice(
MailService service,
String customerEmail,
String csv,
) async {
var message = MailMessage(
from: MailRecipient(email: 'no-reply@example.com', name: 'Example Shop'),
to: [MailRecipient(email: customerEmail)],
bcc: [MailRecipient(email: 'archive@example.com')],
replyTo: [MailRecipient(email: 'support@example.com')],
subject: 'Your invoice',
text: 'Your invoice is attached.',
html: '<p>Your invoice is attached.</p>',
attachments: service.supportAttachments
? [
MailAttachment(
mimeType: 'text/csv',
filename: 'invoice.csv',
content: Uint8List.fromList(utf8.encode(csv)),
),
]
: null,
);
var result = await service.sendMail(message);
return result.messageId;
}
Filtering user input before building recipients
import 'package:tekartik_mail/mail.dart';
import 'package:tekartik_mail/utils/mail_utils.dart';
/// Keeps only the entries that look like a single email address.
List<MailRecipient> parseRecipients(Iterable<String> input) {
var recipients = <MailRecipient>[];
for (var raw in input) {
var email = raw.trim();
if (email.isEmpty || email.contains(RegExp(r'\s'))) {
continue; // validateEmail is not anchored, reject spaces first.
}
if (validateEmail(email)) {
recipients.add(MailRecipient(email: email));
}
}
return recipients;
}
Testing code that sends mail
import 'package:tekartik_mail/mail_mixin.dart';
import 'package:test/test.dart';
class _RecordingMailService with MailServiceMixin {
final sent = <MailMessage>[];
@override
Future<SendMailResult> sendMail(MailMessage message) async {
sent.add(message);
return _RecordingSendMailResult('test-${sent.length}');
}
}
class _RecordingSendMailResult implements SendMailResult {
@override
final String? messageId;
_RecordingSendMailResult(this.messageId);
}
void main() {
test('sendMail', () async {
var service = _RecordingMailService();
var result = await service.sendMail(
MailMessage(
from: MailRecipient(email: 'from@example.com', name: 'From'),
to: [MailRecipient(email: 'to@example.com')],
subject: 'Hi',
text: 'Hello',
),
);
expect(result.messageId, 'test-1');
expect(service.supportAttachments, isFalse);
var message = service.sent.single;
expect(message.from.toString(), 'From <from@example.com>');
expect(message.to!.single, MailRecipient(email: 'to@example.com'));
});
}