Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
name: Publish to pub.dev

on:
push:
tags:
- 'v[0-9]+.[0-9]+.[0-9]+*' # Triggers on tags like v1.2.3 or v1.2.3+1
workflow_dispatch: # Allows you to manually trigger a retry from the Actions tab

jobs:
publish:
permissions:
id-token: write # Required for OIDC authentication
contents: read

runs-on: ubuntu-latest

environment: pub.dev

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup Dart
uses: dart-lang/setup-dart@v1

- name: Install dependencies
run: dart pub get

- name: Publish to pub.dev
# The '--force' is safe here because the tag and
# environment approval act as your confirmation.
run: dart pub publish --force
35 changes: 35 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: Dart CI

on:
push:
branches: [ '**' ]
pull_request:
branches: [ master ]

jobs:
build:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: dart-lang/setup-dart@v1
with:
sdk: stable

- name: Install dependencies
run: dart pub get

- name: Verify formatting
run: dart format --output=none --set-exit-if-changed .

- name: Analyze project source
# Adding --fatal-infos ensures the highest code quality standards
run: dart analyze --fatal-infos

- name: Run tests
run: dart test

- name: Publish Dry Run
# This ensures every PR is actually "publishable" to pub.dev
run: dart pub publish --dry-run
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ packages
*.iml
.dart_tool
/test/smtpserver.json
.DS_Store
.DS_Store
secrets.json
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,20 @@
## 7.0.0
* Major Refactoring: Simplified library structure and standardized exports.
- Renamed internal `IR` classes to `Mime`.
- Moved files into `core`, `mime`, and `smtp` directories.
- Standardized on relative imports within the package.
- Moved to community repository (github.com/dart-mailer/mailer)
* Feature: Added support for RFC 3030 (CHUNKING and BINARYMIME).
* Feature: Implemented Custom Address Validation API.
- Added `PracticalAddressValidator` (recommended for input validation).
- Added `StrictAddressValidator` (RFC 5322 compliance).
- Added `PermissiveAddressValidator` and `SimpleAddressValidator`.
* Feature: Correct IDNA encoding for domains.
- Proper NFC Unicode normalization using `unorm_dart`.
- Case folding and label validation (RFC 5890).
* Breaking: Removed `lib/src/core/entities.dart`. Import `package:mailer/mailer.dart` instead.
* Update: Bumped SDK constraint to Dart 3.

## 6.6.0
* Add Amazon simple Email Service stmp server
Thanks https://github.com/karelklic
Expand Down
248 changes: 133 additions & 115 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,158 +2,176 @@

**mailer** is an easy-to-use library for composing and sending emails in Dart.

Mailer supports file attachments and HTML emails.
> [!WARNING]
> This is an **SMTP client** library. It is designed to send emails by connecting to an **existing SMTP server** (like Gmail, SendGrid, Mailgun, or your own Postfix/Exim server).
>
> It is **not** an SMTP server and cannot receive emails or accept incoming connections from other mail clients.

## FLUTTER developers
It supports:
* **Plaintext and HTML** emails.
* **Attachments** (files, streams, etc.).
* **Inline images** (using CID).
* **Unicode** support.
* **RFC 3030** (CHUNKING) for efficient large file transfer.
* **Secure connections** (TLS/SSL) with context-aware sanitization.
* **Custom Address Validation** strategies (strict, permissive, etc.).
* **Pre-configured services** (Gmail, Yahoo, Hotmail, etc.) and generic SMTP support.

**This library does not work with flutter web.** Sending mails using the SMTP is technically not possible over HTTP.
## Usage

### Simple Example (Gmail)

Please do NOT use mailer together with your credentials. Extracting them is very easy and anybody could then send
mails using your account. If you use your gmail credentials it's even worse as an attacker could read your mails as
well.
```dart
import 'package:mailer/mailer.dart';
import 'package:mailer/smtp_server.dart';

void main() async {
String username = 'username@gmail.com';
String password = 'password'; // Use an App Password if 2FA is enabled

[Johannes Milke](https://github.com/JohannesMilke) has created an excellent tutorial on how to use `mailer` without
needing to embed your credentials in the flutter app.
final smtpServer = gmail(username, password);
// Use the SmtpServer class to configure any other SMTP server:
// final smtpServer = SmtpServer('smtp.domain.com');

[Flutter Tutorial - How To Send Email In Background \[2021\] Without Backend](https://www.youtube.com/watch?v=RDwst9icjAY)
final message = Message()
..from = Address(username, 'Your Name')
..recipients.add('destination@example.com')
..ccRecipients.addAll(['destCc1@example.com', 'destCc2@example.com'])
..bccRecipients.add(Address('bccAddress@example.com'))
..subject = 'Test Dart Mailer library :: 😀 :: ${DateTime.now()}'
..text = 'This is the plain text.\nThis is line 2 of the text part.'
..html = "<h1>Test</h1>\n<p>Hey! Here's some HTML content</p>";

The tutorial will use firebase and ask for the credentials of the android user: ![flutter-screenshot](doc/flutter_user.png)
By using the account of the android user it avoids storing your credentials in the app.
try {
final sendReport = await send(message, smtpServer);
print('Message sent: $sendReport');
} on MailerException catch (e) {
print('Message not sent.');
for (var p in e.problems) {
print('Problem: ${p.code}: ${p.msg}');
}
}
}
```

## Server developers
### Advanced Usage

[Suragch](https://suragch.medium.com/) has written an excellent tutorial on
[How to send yourself email notifications from a Dart server](https://suragch.medium.com/how-to-send-yourself-email-notifications-from-a-dart-server-a7c16a1900d6)
#### Attachments and Inline Images

Thanks to Suragch for bringing those tutorials to my attention.
If you have created a tutorial yourself (or know of one) please open an issue, so that I can add it to this "list".
```dart
final message = Message()
..from = Address(username, 'Your Name')
..recipients.add('destination@example.com')
..subject = 'Inline Image Test'
..html = '<h1>Test</h1><p>Here is an image:</p><img src="cid:myimg@3.141"/>'
..attachments = [
FileAttachment(File('image.png'))
..location = Location.inline
..cid = '<myimg@3.141>'
];
```

Note that those tutorial don't need to be in English!
#### Custom Address Validation

## SMTP definitions
The library validates addresses before sending. By default, it uses a simple check (contains `@` with non-empty parts). You can customize this behavior:

Mailer provides configurations for a few common SMTP servers.
**Available validators:**
| Validator | Use Case |
|:----------|:---------|
| `PracticalAddressValidator` | **Recommended for input validation.** Rejects IP literals, quoted strings, and domains without dots. |
| `StrictAddressValidator` | Full RFC 5322 compliance. Accepts technically valid but uncommon formats. |
| `PermissiveAddressValidator` | Accepts any non-empty string. |

Please create merge requests for missing configurations.
**Validating user input (recommended):**
```dart
import 'package:mailer/src/core/address_validator.dart';

* Copy `lib/smtp_server/gmail.dart` to `lib/smtp_server/xxx.dart`
* Adapt the code. (See `lib/smtp_server.dart` for possible arguments)
* Export the newly created SMTP server in `lib/smtp_server.dart`
* Create a pull request.
final validator = PracticalAddressValidator();
final address = Address('test@example.com');

In a lot of cases you will find a configuration
in [legacy.dart](https://github.com/kaisellgren/mailer/blob/v2/lib/legacy.dart)
if (validator.validate(address)) {
print('Address is valid');
} else {
print('Address is invalid - may not be deliverable');
}
```

## Features
**Using a validator for sending:**
```dart
final message = Message()
..validator = StrictAddressValidator() // or PracticalAddressValidator()
..from = 'valid@example.com';
```

* Plaintext and HTML emails
* Unicode support
* Attachments
* Secure (filters and sanitizes all fields context-wise)
* Use any SMTP server like Gmail, Live, SendGrid, Amazon SES
* SSL/TLS support
* Pre-configured services (Gmail, Yahoo, Hotmail, etc.). Just fill in your username and password.
#### Persistent Connection

## TODO *HELP WANTED*
For sending multiple messages efficiently, use `PersistentConnection`.

* Correct encoding of non ASCII mail addresses.
* Reintegrate address validation from version 1.*
* Improve Header types. (see [ir_header.dart](lib/src/smtp/internal_representation/ir_header.dart))
We should choose the correct header based on the header name.
Known headers (`list-unsubscribe`,...) should have their own subclass.
* Improve documentation.
```dart
var connection = PersistentConnection(smtpServer);

## Examples
await connection.send(message1);
await connection.send(message2);

### Sending an email with SMTP
await connection.close();
```

See [gmail example](example/send_gmail.dart).

We also have an example which uses [gmail oauth](example/gmail_xoauth2/).
## Documentation

```dart
import 'package:mailer/mailer.dart';
import 'package:mailer/smtp_server.dart';
* [Using Inline Images (doc/inline_image_guide.md)](doc/inline_image_guide.md)
* [Gmail XOAUTH2 Guide (doc/gmail_xoauth2/README.md)](doc/gmail_xoauth2/README.md)

main() async {
// Note that using a username and password for gmail only works if
// you have two-factor authentication enabled and created an App password.
// Search for "gmail app password 2fa"
// The alternative is to use oauth.
String username = 'username@gmail.com';
String password = 'password';
## Tutorials

final smtpServer = gmail(username, password);
// Use the SmtpServer class to configure an SMTP server:
// final smtpServer = SmtpServer('smtp.domain.com');
// See the named arguments of SmtpServer for further configuration
// options.
### Flutter Developers

// Create our message.
final message = Message()
..from = Address(username, 'Your name')
..recipients.add('destination@example.com')
..ccRecipients.addAll(['destCc1@example.com', 'destCc2@example.com'])
..bccRecipients.add(Address('bccAddress@example.com'))
..subject = 'Test Dart Mailer library :: 😀 :: ${DateTime.now()}'
..text = 'This is the plain text.\nThis is line 2 of the text part.'
..html = "<h1>Test</h1>\n<p>Hey! Here's some HTML content</p>";
**Flutter Web is NOT supported.** Sending emails via SMTP directly from a browser is technically impossible due to security restrictions (CORS, lack of raw socket access). You must use a backend server or a proxy.

try {
final sendReport = await send(message, smtpServer);
print('Message sent: ' + sendReport.toString());
} on MailerException catch (e) {
print('Message not sent.');
for (var p in e.problems) {
print('Problem: ${p.code}: ${p.msg}');
}
}
// DONE


// Let's send another message using a slightly different syntax:
//
// Addresses without a name part can be set directly.
// For instance `..recipients.add('destination@example.com')`
// If you want to display a name part you have to create an
// Address object: `new Address('destination@example.com', 'Display name part')`
// Creating and adding an Address object without a name part
// `new Address('destination@example.com')` is equivalent to
// adding the mail address as `String`.
final equivalentMessage = Message()
..from = Address(username, 'Your name 😀')
..recipients.add(Address('destination@example.com'))
..ccRecipients.addAll([Address('destCc1@example.com'), 'destCc2@example.com'])
..bccRecipients.add('bccAddress@example.com')
..subject = 'Test Dart Mailer library :: 😀 :: ${DateTime.now()}'
..text = 'This is the plain text.\nThis is line 2 of the text part.'
..html = '<h1>Test</h1>\n<p>Hey! Here is some HTML content</p><img src="cid:myimg@3.141"/>'
..attachments = [
FileAttachment(File('exploits_of_a_mom.png'))
..location = Location.inline
..cid = '<myimg@3.141>'
];
**Security Warning:** Do NOT embed your SMTP credentials (username/password) directly in your client-side Flutter code. If you do, anyone can extract them and use your account to send spam.

[Johannes Milke](https://github.com/JohannesMilke) has created an excellent tutorial on how to use `mailer` without needing to embed your credentials in the Flutter app:

* [Flutter Tutorial - How To Send Email In Background [2021] Without Backend](https://www.youtube.com/watch?v=RDwst9icjAY)

The tutorial uses Firebase and requests the credentials of the Android user, avoiding storing your credentials in the app.

### Server Developers

final sendReport2 = await send(equivalentMessage, smtpServer);
[Suragch](https://suragch.medium.com/) has written an excellent tutorial on [How to send yourself email notifications from a Dart server](https://suragch.medium.com/how-to-send-yourself-email-notifications-from-a-dart-server-a7c16a1900d6).

// Sending multiple messages with the same connection
//
// Create a smtp client that will persist the connection
var connection = PersistentConnection(smtpServer);
---

// Send the first message
await connection.send(message);
If you have created a tutorial yourself (or know of one) please open an issue, so it can be added to this list. Note that tutorials don't need to be in English!

// send the equivalent message
await connection.send(equivalentMessage);
## Logging

// close the connection
await connection.close();
The library uses the `logging` package. By default, no logs are output.
To see the SMTP communication (which is helpful for debugging), configure a logger listener:

```dart
import 'package:logging/logging.dart';

void main() {
Logger.root.level = Level.ALL; // defaults to Level.INFO
Logger.root.onRecord.listen((LogRecord rec) {
print('${rec.level.name}: ${rec.time}: ${rec.message}');
});

// ... rest of your code
}
```

**Security Note:** Credentials (passwords, tokens) in `AUTH` commands are masked (`*******`) in the logs to prevent accidental leakage.

## FAQ

**Q: How do I handle large attachments?**
A: `mailer` supports RFC 3030 (CHUNKING). If the server supports it, `mailer` will automatically stream the data using `BDAT` commands, which is more efficient than the standard `DATA` command.

**Q: How do I use Gmail with 2FA?**
A: You must generate an **App Password** in your Google Account settings and use that instead of your standard password.

## License

This library is licensed under MIT.
MIT
Loading