diff --git a/.env b/.env
deleted file mode 100644
index e611e02..0000000
--- a/.env
+++ /dev/null
@@ -1,6 +0,0 @@
-HOSTNAME=smtp.163.com
-MAIL_USER=deno_smtp@163.com
-MAIL_TO_USER=manyuanrong@qq.com
-MAIL_PASS=HDZXIALWAWGFPYCO
-PORT=465
-TLS=true
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 9e605e5..49b6e67 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -8,27 +8,37 @@ assignees: mathe42
---
-**Describe the bug**
+
+## Describe the bug
+
A clear and concise description of what the bug is.
-**To Reproduce**
-Provide a code example (without your actual password etc.) make it as minimal as you can.
+## To Reproduce
+
+Provide a code example (without your actual password etc.) make it as minimal as
+you can.
+
+## Expected behavior
-**Expected behavior**
A clear and concise description of what you expected to happen.
-**Logs**
+## Logs
+
Provide the output of `deno --version`
+
```
Put output here
```
-Provide the output of your code snippet (with console_debug set to true see https://github.com/EC-Nordbund/denomailer#configuring-your-client )
+
+Provide the output of your code snippet (with console_debug set to true see
+https://github.com/EC-Nordbund/denomailer#configuring-your-client )
```
Put log here
```
-If and only if you have problems with TLS or STARTTLS please provide the output of the following commands:
+If and only if you have problems with TLS or STARTTLS please provide the output
+of the following commands:
```
# STARTTLS
@@ -38,5 +48,7 @@ openssl s_client -debug -starttls smtp -crlf -connect your-host.de:25
openssl s_client -debug -crlf -connect your-host.de:25
```
-**Additional context**
-Add any other context about the problem here. Is there a older version you know where this was working?
+## Additional context
+
+Add any other context about the problem here. Is there a older version you know
+where this was working?
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
index 7cf3867..d497c70 100644
--- a/.github/ISSUE_TEMPLATE/feature_request.md
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -7,14 +7,20 @@ assignees: mathe42
---
-**Is your feature request related to a problem? Please describe.**
-A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+## Is your feature request related to a problem? Please describe.
+
+A clear and concise description of what the problem is. Ex. I'm always
+frustrated when [...]
+
+## Describe the solution you'd like
-**Describe the solution you'd like**
A clear and concise description of what you want to happen.
-**Describe alternatives you've considered**
-A clear and concise description of any alternative solutions or features you've considered.
+## Describe alternatives you've considered
+
+A clear and concise description of any alternative solutions or features you've
+considered.
+
+## Additional context
-**Additional context**
Add any other context or screenshots about the feature request here.
diff --git a/.github/workflows/ci2.yml b/.github/workflows/ci2.yml
new file mode 100644
index 0000000..7d9c5c4
--- /dev/null
+++ b/.github/workflows/ci2.yml
@@ -0,0 +1,19 @@
+name: test
+
+on:
+ push:
+ branches:
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v2
+ - uses: denoland/setup-deno@v1
+ with:
+ deno-version: v1.x
+ - run: docker run -d -p 1080:1080 -p 1025:1025 reachfive/fake-smtp-server
+ - run: deno test --unstable -A ./test/e2e/basic.test.ts
+ - run: deno fmt --check
+ - run: deno lint
diff --git a/LICENSE b/LICENSE
index 7099986..6fc6dc8 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,7 @@
MIT License
Copyright (c) 2019 EnokMan
+Copyright (c) 2022 Sebastian Krüger (@mathe42)
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
diff --git a/README.md b/README.md
index 61d168a..f4c2628 100644
--- a/README.md
+++ b/README.md
@@ -1,182 +1,418 @@
-## Deno SMTP mail client
+# Denomailer a SMTP-client for Deno
-### IMPORTANT SECURITY INFORMATION
+> This was forked from https://github.com/manyuanrong/deno-smtp but now is much
+> more advanced!
-PLEASE update to a version >= 0.8! 0.8 has a problem where malformed mails could
-potentialy allow attackers to create a mail (with linebreaks) to send unwanted
-SMTP commands. This could result in authentic phishing attacks! With no way for
-the user to identify that this is a phishing mail! Or that this mail contains a
-dangerous attachment!
+## Quickstart with a simple example
-Also make sure that Mails are sent one after the other as they can corrupt each
-others data!
+```ts
+// please use this line and change the version to the latest version!
+// import { SMTPClient } from 'https://deno.land/x/denomailer@x.x.x/mod.ts'
+import { SMTPClient } from "https://deno.land/x/denomailer/mod.ts";
+
+const client = new SMTPClient({
+ connection: {
+ hostname: "smtp.example.com",
+ port: 465,
+ tls: true,
+ auth: {
+ username: "example",
+ password: "password",
+ },
+ },
+});
-### Allowed Mail Formats
+await client.send({
+ from: "me@example.com",
+ to: "you@example.com",
+ subject: "example",
+ content: "...",
+ html: "
...
",
+});
-A single Mail `mail@example.de` with a name `NAME` can be encoded in the
-following ways:
+await client.close();
+```
-1. `"name@example.de"`
-2. `""`
-3. `"NAME "`
-4. `{mail: "name@example.de"}`
-5. `{mail: "name@example.de", name: "NAME"}`
+## Client
-Where 1-3 is called a "MailString".
+You can create a new client with
+`const client = new SMTPClient(/* client options */)`.
-Multiple Mails can be an Array of the above OR a object that maps names to mails
-for example:
+### Options
-`{"P1": "p1@example.de", "P2": "p2@example.de"}` we call this a MailObject.
+The only required option is `connection.hostname` but in most cases you want to
+set `connection.,auth`.
-For the fields
+Here are the full options available:
-1. `from`, `replyTo` we only allow a "MailString".
-2. `to`, `cc`, `bcc` we allow a MailObject a Array of single Mails or a single
- Mail.
+```ts
+export interface ClientOptions {
+ debug?: {
+ /**
+ * USE WITH COUTION AS THIS WILL LOG YOUR USERDATA AND ALL MAIL CONTENT TO STDOUT!
+ * @default false
+ */
+ log?: boolean;
+ /**
+ * USE WITH COUTION AS THIS WILL POSIBLY EXPOSE YOUR USERDATA AND ALL MAIL CONTENT TO ATTACKERS!
+ * @default false
+ */
+ allowUnsecure?: boolean;
+ /**
+ * USE WITH COUTION AS THIS COULD INTODRUCE BUGS
+ *
+ * This option is mainly to allow debuging to exclude some possible problem surfaces at encoding
+ * @default false
+ */
+ encodeLB?: boolean;
+ /**
+ * Disable starttls
+ */
+ noStartTLS?: boolean;
+ };
+ connection: {
+ hostname: string;
+ /**
+ * For TLS the default port is 465 else 25.
+ * @default 25 or 465
+ */
+ port?: number;
+ /**
+ * authentication data
+ */
+ auth?: {
+ username: string;
+ password: string;
+ };
+ /**
+ * Set this to `true` to connect via SSL / TLS if set to `false` STARTTLS is used.
+ * Only if `allowUnsecure` is used userdata or mail content could be exposed!
+ *
+ * @default false
+ */
+ tls?: boolean;
+ };
+ /**
+ * Create multiple connections so you can send emails faster!
+ */
+ pool?: {
+ /**
+ * Number of Workers
+ * @default 2
+ */
+ size?: number;
+ /**
+ * Time the connection has to be idle to be closed. (in ms)
+ * If a value > 1h is set it will be set to 1h
+ * @default 60000
+ */
+ timeout?: number;
+ } | boolean;
+ client?: {
+ /**
+ * There are some cases where warnings are created. These are loged by default but you can 'ignore' them or all warnings should be considered 'error'.
+ *
+ * @default log
+ */
+ warning?: "ignore" | "log" | "error";
+ /**
+ * List of preproccessors to
+ *
+ * - Filter mail
+ * - BCC all mails to someone
+ * - ...
+ */
+ preprocessors?: Preprocessor[];
+ };
+}
+```
-### Sending multiple mails
+#### connection
-Note that for race-condition reasons we can't send multiple mails at once.
-Because of that if send is already called and still processing a mail
-`client.send` will queue that sending.
+You have to set the hostname and in most cases you need the auth object as
+(near) all SMTP-Server will require a login.
-### Example
+> The only usecase where you might not need it is if you connect to a server
+> with IP protection so for example only local IP are allowed so a server
+> application can send mails without login.
-```ts
-import { SmtpClient, quotedPrintableEncode } from "https://deno.land/x/denomailer/mod.ts";
+Denomailer supports 3 security modes:
-const client = new SmtpClient();
+1. TLS
+2. STARTTLS
+3. unsecure
-await client.connect({
- hostname: "smtp.163.com",
- port: 25,
- username: "username",
- password: "password",
-});
+You have to specify wich to use. As unsecure you have to set extra config
+options in `debug` as it is not rcomended!
-await client.send({
- from: "mailaddress@163.com",
- to: "Me ",
- cc: [
- "name@example.de",
- "",
- "NAME ",
- {mail: "name@example.de"},
- {mail: "name@example.de", name: "NAME"}
- ],
- bcc: {
- "Me": "to-address@xx.com"
- },
- subject: "Mail Title",
- content: "Mail Content",
- html: "Github",
- date: "12 Mar 2022 10:38:05 GMT",
- priority: "high",
- replyTo: 'mailaddress@163.com',
- attachments: [
- { encoding: "text"; content: 'Hi', contentType: 'text/plain', filename: 'text.txt' },
- { encoding: "base64"; content: '45dasjZ==', contentType: 'image/png', filename: 'img.png' },
- {
- content: new Uint8Array([0,244,123]),
- encoding: "binary",
- contentType: 'image/jpeg',
- filename: 'bin.png'
- }
- ],
- mimeContent: [
- {
- mimeType: 'application/markdown',
- content: quotedPrintableEncode('# Title\n\nHello World!'),
- transferEncoding: 'quoted-printable'
- }
- ]
-
-});
+For TLS set `tls: true` for startTLS set `tls: false` (or don't set it).
-await client.close();
-```
+#### client
-#### TLS connection
+There are some "problems" that can be warnings or errors. With the `warning`
+option you censpecify if they should `'error'` or you can `'ignore'` them or
+`'log'` them. Default is `'log'`. This includes filtering invalid emails and
+custom preprocessors
+
+With the `preprocessors` option you can add handlers that modify each
+mail-config. For example you can add a filter so you don't send E-Mails to
+burner mails etc.
+
+A preprocessor is of the type:
```ts
-await client.connectTLS({
- hostname: "smtp.163.com",
- port: 465,
- username: "username",
- password: "password",
-});
+type Preprocessor = (
+ mail: ResolvedSendConfig,
+ client: ResolvedClientOptions,
+) => ResolvedSendConfig;
```
-#### Use in Gmail
+It gets a preproccesed E-Mail config (ResolvedSendConfig) and the preprocessed
+client options (ResolvedClientOptions) and has to return a (maybe modified)
+E-Mail config.
+
+#### pool
+
+With a normal SMTP-Client the E-Mails are send one after the other so if you
+have a heavy load you might want to use more Clients at once for that we have a
+pool option.
+
+You can set the amount of clients used (`size`) and a timeout (in ms) after that
+the connection is closed (`timeout`).
+
+Note that for each connection we create a new Worker and the used worker syntax
+requires (as of deno 1.21) to use the `--unstable` flag. Because of that this
+API has to be considered unstable but we don't expect a change! (At least for
+the public denomailer api internaly there might be some small changes that
+require specific deno versions)
+
+When you close the connection all worker are just killed.
+
+#### debug
+
+Sometimes you have specific needs for example we require encrypted connections
+to send E-Mails and authentication. To enable unsecure connections set the
+`allowUnsecure` option to `true` depending on your needs you have to disable
+startTLS to get an unsecure connection use `noStartTLS` for this.
+
+Note that we only use this in tests where the SMTP-Server is local and doesn't
+support TLS.
+
+If you want to get a full connection log use the `log` option. If you create an
+issue for a bug please add the full log (but remove your authentication data
+wich is encoded in base64).
+
+In some cases you might get problems with Linebreaks in emails before creating
+an issue please try `encodeLB: true` that changes the encoding a little and this
+might solve your problem. Please create an issue if you need this option!
+
+### Examples
```ts
-await client.connectTLS({
- hostname: "smtp.gmail.com",
- port: 465,
- username: "your username",
- password: "your password",
+const client = new SMTPClient({
+ connection: {
+ hostname: "smtp.example.com",
+ port: 465,
+ tls: true,
+ auth: {
+ username: "example",
+ password: "password",
+ },
+ },
+ pool: {
+ size: 2,
+ timeout: 60000,
+ },
+ client: {
+ warning: "log",
+ preprocessors: [filterBurnerMails],
+ },
+ debug: {
+ log: false,
+ allowUnsecure: false,
+ encodeLB: false,
+ noStartTLS: false,
+ },
});
+```
-await client.send({
- from: "someone@163.com", // Your Email address
- to: "someone@xx.com", // Email address of the destination
- subject: "Mail Title",
- content: "Mail Content,maybe HTML",
-});
+## Sending Mails
-await client.close();
+Just call `client.send(/* mail config */)`
+
+### Config
+
+The config you can set:
+
+```ts
+export interface SendConfig {
+ to: mailList;
+ cc?: mailList;
+ bcc?: mailList;
+ from: string;
+ date?: string;
+ subject: string;
+ content?: string;
+ mimeContent?: Content[];
+ html?: string;
+ inReplyTo?: string;
+ replyTo?: string;
+ references?: string;
+ priority?: "high" | "normal" | "low";
+ attachments?: Attachment[];
+ /**
+ * type of mail for example `registration` or `newsletter` etc.
+ * allowes preprocessors to hande different email types
+ */
+ internalTag?: string | symbol;
+}
```
-### Filter E-Mails
-If you want a custom E-Mail validator and filter some E-Mails (because they are burner mails or the domain is on a blacklist or only allow specific domains etc.) you can add the `mailFilter` option to the smtp-client constructor options. `mailFilter` takes a function that gets 3 Arguments the "mailbox" (all that is before @ in the mail), the "domain" (what is after the @) and `internalTag` that is a new option that can be set in the mailConfig so you can set a type for that mail for example type `newsletter` etc. `internalTag` can be a `string` or a `symbol`.
+All of it should be clear by name except:
-The filter function returns a boolean or a Promise that resolves to a boolean. There are 3 things you can do when this function is called:
+#### mimeContent
-1. return `true` the E-Mail is keept in the list
-2. return `false` the E-Mail is removed from the list
-3. throw an Error the E-Mail is aborted and never send
+There are use cases where you want to do encoding etc. on your own. This option
+allowes you to specify the content of the mail.
-So you can decide if a single mail error results in a complete mail abort or it only get removed from the list.
+#### content & html
-You can for example validate against this list: https://github.com/wesbos/burner-email-providers.
+The content should be a plain-text version of the html content you can set
+`content` to `'auto'` to generate that by denomailer but in most cases you
+should do it on your own!
+#### attachments
-### Configuring your client
+A array of attachments you can encode it as base64, text, binary. Note that
+base64 is converted to binary and only there for a better API. So don't encode
+your binary files as base64 so denomailer can convert it back to binary.
-You can pass options to your client through the `SmtpClient` constructor.
+#### internalTag
-```ts
-import { SmtpClient } from "https://deno.land/x/denomailer/mod.ts";
+This can be used with preprocessors so you can give a mail a type for example
+`'registration'`, `'newsletter'` etc. supports symbols and strings.
+
+### Allowed Mail Formats
+
+A single Mail `mail@example.de` with a name `NAME` can be encoded in the
+following ways:
+
+1. `"name@example.de"`
+2. `""`
+3. `"NAME "`
+4. `{mail: "name@example.de"}`
+5. `{mail: "name@example.de", name: "NAME"}`
+
+Multiple Mails can be an Array of the above OR a object that maps names to mails
+for example:
+
+`{"P1": "p1@example.de", "P2": "p2@example.de"}` we call this a MailObject.
+
+For the fields
-//Defaults
-const client = new SmtpClient({
- console_debug: true, // enable debugging this is good while developing should be false in production as Authentication IS LOGGED TO CONSOLE!
- unsecure: true, // allow unsecure connection to send authentication IN PLAIN TEXT and also mail content!
+1. `from`, `replyTo` we only allow a single mail string.
+2. `to`, `cc`, `bcc` we allow a MailObject a Array of single Mails (you can mix
+ formats) or a single Mail.
+
+### Examples
+
+Example with near all options:
+
+```ts
+client.send({
+ to: "abc@example.com",
+ cc: [
+ "abc@example.com",
+ "abc ",
+ {
+ name: "abc",
+ mail: "abc@example.com",
+ },
+ ],
+ bcc: {
+ abc: "abc@example.com",
+ other: "abc@example.com",
+ },
+ from: "me ",
+ replyTo: "",
+ subject: "example",
+ content: "auto",
+ html: "Hello World
",
+ internalTag: "newsletter",
+ priority: "low",
});
```
-## Pool, Worker
-> This is unstable API may change! This requires deno to run in unstable mode.
+## Other exports
-Adds 2 new classes `SMTPWorker` and `SMTPWorkerPool` (for constructor options see code for now). This creates a SMTP client (or multiple) that get automaticly killed if the connection is not used for around 60s.
+We export our implementation of an quotedPrintable encoder. There might be some
+use cases where you need it. The api of the function is considered stable!
-## TLS issues
-When getting TLS errors make shure:
-1. you use the correct port (mostly 25, 587, 465)
-2. the server supports STARTTLS when using `client.connect`
-3. the server supports TLS when using `client.connectTLS`
-4. Use the command `openssl s_client -debug -starttls smtp -crlf -connect your-host.de:587` or `openssl s_client -debug -crlf -connect your-host.de:587` and get the used cipher this should be a cipher with "forward secrecy". Check the status of the cipher on https://ciphersuite.info/cs/ . If the cipher is not STRONG this is an issue with your mail provider so you have to contact them to fix it.
-5. Feel free to create issues if you are ok with that share the port and host so a proper debug can be done.
-6. We can only support TLS where Deno supports it and Deno uses rustls wich explicitly not implemented some "weak" ciphers.
+## Stable api
+All api exported by `/mod.ts` is considered stable. But as the `pool` need the
+`--unstable` flag by Deno this has to be considered unstable. But we don't
+expect any breaking changes there - but if deno changes sytax etc. a new deno
+release can break it!
-### Non SpecCompliant SMTP-Server
-There are some SMTP-Server that don't follow the spec to 100%. This can result in unexpected errors in denomailer. If this happens (for example in https://github.com/EC-Nordbund/denomailer/blob/03a66a6f9a4b5f349ea35856f5903fb45fd0cc5f/smtp.ts#L376 the server sends a 250) please create an issue. We will try and do the following:
+Changes to them will only be made if a new major is released.
-1. Check if it is not an error in denomailer
-2. Try to fix it at the SMTP-Server side (create an issue if the server is an opensource project etc.)
-3. We will add a ***temporary*** workaround by changing denomailer. This will include log messages telling the developer (if the workaround is used) that denomailer used the workaround wich can be removed at any time.
+## Contribute
+Feel free to contribute by:
+1. creating issues for bugs and feature requests (note that you have to use the
+ bug template to get support)
+2. contribute code but keep in mind
+ - for small changes you can just create a PR
+ - for bigger changes please create an issue before! This will help to reduce
+ time creating PR that are not merged.
+ - if you fix a bug please add a test that fails before your fix
+3. contribute tests, fix typos, ...
+## TLS issues
+
+When getting TLS errors make sure:
+
+1. you use the correct port (mostly 25, 587, 465)
+2. the server supports STARTTLS when using `connection.tls = false`
+3. the server supports TLS when using `connection.tls = true`
+4. Use the command
+ `openssl s_client -debug -starttls smtp -crlf -connect your-host.de:587` or
+ `openssl s_client -debug -crlf -connect your-host.de:587` and get the used
+ cipher this should be a cipher with "forward secrecy". Check the status of
+ the cipher on https://ciphersuite.info/cs/ . If the cipher is not STRONG this
+ is an issue with your mail provider so you have to contact them to fix it.
+5. Feel free to create issues if you are ok with that share the port and host so
+ a proper debug can be done.
+6. We can only support TLS where Deno supports it and Deno uses rustls wich
+ explicitly not implemented some "weak" ciphers.
+
+## Non SpecCompliant SMTP-Server
+
+There are some SMTP-Server that don't follow the spec to 100%. This can result
+in unexpected errors in denomailer. If this happens (for example in
+https://github.com/EC-Nordbund/denomailer/blob/03a66a6f9a4b5f349ea35856f5903fb45fd0cc5f/smtp.ts#L376
+the server sends a 250) please create an issue. We will try and do the
+following:
+
+1. Check if it is not an error in denomailer
+2. Try to fix it at the SMTP-Server side (create an issue if the server is an
+ opensource project etc.)
+3. We will add a _**temporary**_ workaround by changing denomailer. This will
+ include log messages telling the developer (if the workaround is used) that
+ denomailer used the workaround wich can be removed at any time.
+
+## Breaking changes
+
+### v0.x -> v1.0
+
+1. Import `SMTPClient` not `SmtpClient`
+2. Change the constructor options (include the options used with
+ `client.connect` or `client.connectTLS` (add `tls = true` in the second
+ case))
+3. Remove `client.connect` and `client.connectTLS` calls
+4. filterMail option was removed in favor of new preprocessor option
+5. The `client.send` method did not have any breaking changes.
+6. Some internal fields where removed from `SMTPClient` only use `send` and
+ `close`! But in 99% of project these where not used
diff --git a/SECURITY.md b/SECURITY.md
index 60bcebc..1a96881 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -6,12 +6,17 @@ We provide security updates currently for these version.
| Version | Supported |
| ------- | ------------------ |
-| > 0.10.x | :white_check_mark: |
+| 1.x | :white_check_mark: |
+| 0.x | :white_check_mark: |
-We will provide security updates after 1.0 only for the latest version of that major.
-We will deprecate major versions so you should always upgrade! Note that major versions will contains breaking changes so we will add security updates for at least 1 older major so you have time to upgrade. But note that we will deprecate these versions too!
+We will provide security updates after 1.0 only for the latest version of that
+major. We will deprecate major versions so you should always upgrade! Note that
+major versions will contains breaking changes so we will add security updates
+for at least 1 older major so you have time to upgrade. But note that we will
+deprecate these versions too!
## Reporting a Vulnerability
-If you find a Vulnerability or a potential Vulnerability please send a mail to `app@ec-nordbund.de` and explain the problem.
-This don't has to be a full exploit!
+If you find a Vulnerability or a potential Vulnerability please send a mail to
+`app@ec-nordbund.de` and explain the problem. This don't has to be a full
+exploit!
diff --git a/client/basic/client.ts b/client/basic/client.ts
new file mode 100644
index 0000000..fada1d0
--- /dev/null
+++ b/client/basic/client.ts
@@ -0,0 +1,379 @@
+import type { ResolvedSendConfig } from "../../config/mail/mod.ts";
+import { ResolvedClientOptions } from "../../config/client.ts";
+import { SMTPConnection } from "./connection.ts";
+
+const CommandCode = {
+ READY: 220,
+ AUTHO_SUCCESS: 235,
+ OK: 250,
+ BEGIN_DATA: 354,
+ FAIL: 554,
+};
+
+class QUE {
+ running = false;
+ #que: (() => void)[] = [];
+ idle: Promise = Promise.resolve();
+ #idbleCB?: () => void;
+
+ que(): Promise {
+ if (!this.running) {
+ this.running = true;
+ this.idle = new Promise((res) => {
+ this.#idbleCB = res;
+ });
+ return Promise.resolve();
+ }
+
+ return new Promise((res) => {
+ this.#que.push(res);
+ });
+ }
+
+ next() {
+ if (this.#que.length === 0) {
+ this.running = false;
+ if (this.#idbleCB) this.#idbleCB();
+ return;
+ }
+
+ this.#que[0]();
+ this.#que.splice(0, 1);
+ }
+}
+
+export class SMTPClient {
+ #connection: SMTPConnection;
+ #que = new QUE();
+
+ constructor(private config: ResolvedClientOptions) {
+ const c = new SMTPConnection(config);
+ this.#connection = c;
+
+ this.#ready = (async () => {
+ await c.ready;
+ await this.#prepareConnection();
+ })();
+ }
+
+ #ready: Promise;
+
+ close() {
+ return this.#connection.close();
+ }
+
+ get isSending() {
+ return this.#que.running;
+ }
+
+ get idle() {
+ return this.#que.idle;
+ }
+
+ async send(config: ResolvedSendConfig) {
+ await this.#ready;
+ try {
+ await this.#que.que();
+
+ await this.#connection.writeCmd("MAIL", "FROM:", `<${config.from.mail}>`);
+ this.#connection.assertCode(
+ await this.#connection.readCmd(),
+ CommandCode.OK,
+ );
+
+ for (let i = 0; i < config.to.length; i++) {
+ await this.#connection.writeCmd(
+ "RCPT",
+ "TO:",
+ `<${config.to[i].mail}>`,
+ );
+ this.#connection.assertCode(
+ await this.#connection.readCmd(),
+ CommandCode.OK,
+ );
+ }
+
+ for (let i = 0; i < config.cc.length; i++) {
+ await this.#connection.writeCmd(
+ "RCPT",
+ "TO:",
+ `<${config.cc[i].mail}>`,
+ );
+ this.#connection.assertCode(
+ await this.#connection.readCmd(),
+ CommandCode.OK,
+ );
+ }
+
+ for (let i = 0; i < config.bcc.length; i++) {
+ await this.#connection.writeCmd(
+ "RCPT",
+ "TO:",
+ `<${config.bcc[i].mail}>`,
+ );
+ this.#connection.assertCode(
+ await this.#connection.readCmd(),
+ CommandCode.OK,
+ );
+ }
+
+ await this.#connection.writeCmd("DATA");
+ this.#connection.assertCode(
+ await this.#connection.readCmd(),
+ CommandCode.BEGIN_DATA,
+ );
+
+ await this.#connection.writeCmd("Subject: ", config.subject);
+ await this.#connection.writeCmd(
+ "From: ",
+ `${config.from.name} <${config.from.mail}>`,
+ );
+ if (config.to.length > 0) {
+ await this.#connection.writeCmd(
+ "To: ",
+ config.to.map((m) => `${m.name} <${m.mail}>`).join(";"),
+ );
+ }
+ if (config.cc.length > 0) {
+ await this.#connection.writeCmd(
+ "Cc: ",
+ config.cc.map((m) => `${m.name} <${m.mail}>`).join(";"),
+ );
+ }
+
+ await this.#connection.writeCmd("Date: ", config.date);
+
+ if (config.inReplyTo) {
+ await this.#connection.writeCmd("InReplyTo: ", config.inReplyTo);
+ }
+
+ if (config.references) {
+ await this.#connection.writeCmd("Refrences: ", config.references);
+ }
+
+ if (config.replyTo) {
+ await this.#connection.writeCmd(
+ "ReplyTo: ",
+ `${config.replyTo.name} <${config.replyTo.name}>`,
+ );
+ }
+
+ if (config.priority) {
+ await this.#connection.writeCmd("Priority:", config.priority);
+ }
+
+ await this.#connection.writeCmd("MIME-Version: 1.0");
+
+ let boundaryAdditionAtt = 100;
+ // calc msg boundary
+ // TODO: replace this with a match or so.
+ config.mimeContent.map((v) => v.content).join("\n").replace(
+ new RegExp("--attachment([0-9]+)", "g"),
+ (_, numb) => {
+ boundaryAdditionAtt += parseInt(numb, 10);
+
+ return "";
+ },
+ );
+
+ const dec = new TextDecoder();
+
+ config.attachments.map((v) => {
+ if (v.encoding === "text") return v.content;
+
+ const arr = new Uint8Array(v.content);
+
+ return dec.decode(arr);
+ }).join("\n").replace(
+ new RegExp("--attachment([0-9]+)", "g"),
+ (_, numb) => {
+ boundaryAdditionAtt += parseInt(numb, 10);
+
+ return "";
+ },
+ );
+
+ const attachmentBoundary = `attachment${boundaryAdditionAtt}`;
+
+ await this.#connection.writeCmd(
+ `Content-Type: multipart/mixed; boundary=${attachmentBoundary}`,
+ "\r\n",
+ );
+ await this.#connection.writeCmd(`--${attachmentBoundary}`);
+
+ let boundaryAddition = 100;
+ // calc msg boundary
+ // TODO: replace this with a match or so.
+ config.mimeContent.map((v) => v.content).join("\n").replace(
+ new RegExp("--message([0-9]+)", "g"),
+ (_, numb) => {
+ boundaryAddition += parseInt(numb, 10);
+
+ return "";
+ },
+ );
+
+ const messageBoundary = `message${boundaryAddition}`;
+
+ await this.#connection.writeCmd(
+ `Content-Type: multipart/alternative; boundary=${messageBoundary}`,
+ "\r\n",
+ );
+
+ for (let i = 0; i < config.mimeContent.length; i++) {
+ await this.#connection.writeCmd(`--${messageBoundary}`);
+ await this.#connection.writeCmd(
+ "Content-Type: " + config.mimeContent[i].mimeType,
+ );
+ if (config.mimeContent[i].transferEncoding) {
+ await this.#connection.writeCmd(
+ `Content-Transfer-Encoding: ${
+ config.mimeContent[i].transferEncoding
+ }` + "\r\n",
+ );
+ } else {
+ // Send new line
+ await this.#connection.writeCmd("");
+ }
+
+ await this.#connection.writeCmd(config.mimeContent[i].content, "\r\n");
+ }
+
+ await this.#connection.writeCmd(`--${messageBoundary}--\r\n`);
+
+ for (let i = 0; i < config.attachments.length; i++) {
+ const attachment = config.attachments[i];
+
+ await this.#connection.writeCmd(`--${attachmentBoundary}`);
+ await this.#connection.writeCmd(
+ "Content-Type:",
+ attachment.contentType + ";",
+ "name=" + attachment.filename,
+ );
+
+ await this.#connection.writeCmd(
+ "Content-Disposition: attachment; filename=" + attachment.filename,
+ "\r\n",
+ );
+
+ if (attachment.encoding === "binary") {
+ await this.#connection.writeCmd("Content-Transfer-Encoding: binary");
+
+ if (
+ attachment.content instanceof ArrayBuffer ||
+ attachment.content instanceof SharedArrayBuffer
+ ) {
+ await this.#connection.writeCmdBinary(
+ new Uint8Array(attachment.content),
+ );
+ } else {
+ await this.#connection.writeCmdBinary(attachment.content);
+ }
+
+ await this.#connection.writeCmd("\r\n");
+ } else if (attachment.encoding === "text") {
+ await this.#connection.writeCmd(
+ "Content-Transfer-Encoding: quoted-printable",
+ );
+
+ await this.#connection.writeCmd(attachment.content, "\r\n");
+ }
+ }
+
+ await this.#connection.writeCmd(`--${attachmentBoundary}--\r\n`);
+
+ await this.#connection.writeCmd(".\r\n");
+
+ this.#connection.assertCode(
+ await this.#connection.readCmd(),
+ CommandCode.OK,
+ );
+ await this.#cleanup();
+ this.#que.next();
+ } catch (ex) {
+ await this.#cleanup();
+ this.#que.next();
+ throw ex;
+ }
+ }
+
+ async #prepareConnection() {
+ this.#connection.assertCode(
+ await this.#connection.readCmd(),
+ CommandCode.READY,
+ );
+
+ await this.#connection.writeCmd("EHLO", this.config.connection.hostname);
+
+ const cmd = await this.#connection.readCmd();
+
+ if (!cmd) throw new Error("Unexpected empty response");
+
+ if (typeof cmd.args === "string") {
+ this.#supportedFeatures.add(cmd.args);
+ } else {
+ cmd.args.forEach((cmd) => {
+ this.#supportedFeatures.add(cmd);
+ });
+ }
+
+ if (
+ this.#supportedFeatures.has("STARTTLS") && !this.config.debug.noStartTLS
+ ) {
+ await this.#connection.writeCmd("STARTTLS");
+ this.#connection.assertCode(
+ await this.#connection.readCmd(),
+ CommandCode.READY,
+ );
+
+ const conn = await Deno.startTls(this.#connection.conn!, {
+ hostname: this.config.connection.hostname,
+ });
+ this.#connection.setupConnection(conn);
+ this.#connection.secure = true;
+
+ await this.#connection.writeCmd("EHLO", this.config.connection.hostname);
+
+ await this.#connection.readCmd();
+ }
+
+ if (!this.config.debug.allowUnsecure && !this.#connection.secure) {
+ this.#connection.close();
+ this.#connection = null as unknown as SMTPConnection;
+ throw new Error(
+ "Connection is not secure! Don't send authentication over non secure connection!",
+ );
+ }
+
+ if (this.config.connection.auth) {
+ await this.#connection.writeCmd("AUTH", "LOGIN");
+ this.#connection.assertCode(await this.#connection.readCmd(), 334);
+
+ await this.#connection.writeCmd(
+ btoa(this.config.connection.auth.username),
+ );
+ this.#connection.assertCode(await this.#connection.readCmd(), 334);
+
+ await this.#connection.writeCmd(
+ btoa(this.config.connection.auth.password),
+ );
+ this.#connection.assertCode(
+ await this.#connection.readCmd(),
+ CommandCode.AUTHO_SUCCESS,
+ );
+ }
+
+ await this.#cleanup();
+ }
+
+ #supportedFeatures = new Set();
+
+ async #cleanup() {
+ this.#connection.writeCmd("NOOP");
+
+ while (true) {
+ const cmd = await this.#connection.readCmd();
+ if (cmd && cmd.code === 250) return;
+ }
+ }
+}
diff --git a/client/basic/connection.ts b/client/basic/connection.ts
new file mode 100644
index 0000000..12306f6
--- /dev/null
+++ b/client/basic/connection.ts
@@ -0,0 +1,125 @@
+import { BufReader, BufWriter, TextProtoReader } from "../../deps.ts";
+import { ResolvedClientOptions } from "../../config/client.ts";
+
+const encoder = new TextEncoder();
+
+interface Command {
+ code: number;
+ args: string | (string[]);
+}
+
+export class SMTPConnection {
+ secure = false;
+
+ conn: Deno.Conn | null = null;
+ #reader: TextProtoReader | null = null;
+ #writer: BufWriter | null = null;
+
+ constructor(private config: ResolvedClientOptions) {
+ this.ready = this.#connect();
+ }
+
+ ready: Promise;
+
+ async close() {
+ if (!this.conn) {
+ return;
+ }
+ await this.conn.close();
+ }
+
+ setupConnection(conn: Deno.Conn) {
+ this.conn = conn;
+
+ const reader = new BufReader(this.conn);
+ this.#writer = new BufWriter(this.conn);
+ this.#reader = new TextProtoReader(reader);
+ }
+
+ async #connect() {
+ if (this.config.connection.tls) {
+ this.conn = await Deno.connectTls({
+ hostname: this.config.connection.hostname,
+ port: this.config.connection.port,
+ });
+ this.secure = true;
+ } else {
+ this.conn = await Deno.connect({
+ hostname: this.config.connection.hostname,
+ port: this.config.connection.port,
+ });
+ }
+
+ this.setupConnection(this.conn);
+ }
+
+ public assertCode(cmd: Command | null, code: number, msg?: string) {
+ if (!cmd) {
+ throw new Error(`invalid cmd`);
+ }
+ if (cmd.code !== code) {
+ throw new Error(msg || cmd.code + ": " + cmd.args);
+ }
+ }
+
+ public async readCmd(): Promise {
+ if (!this.#reader) {
+ return null;
+ }
+
+ const result: (string | null)[] = [];
+
+ while (
+ result.length === 0 || (result.at(-1) && result.at(-1)!.at(3) === "-")
+ ) {
+ result.push(await this.#reader.readLine());
+ }
+
+ const nonNullResult: string[] =
+ (result.at(-1) === null ? result.slice(0, result.length - 1) : // deno-lint-ignore no-explicit-any
+ result) as any;
+
+ if (nonNullResult.length === 0) return null;
+
+ const code = parseInt(nonNullResult[0].slice(0, 3));
+ const data = nonNullResult.map((v) => v.slice(4).trim());
+
+ if (this.config.debug.log) {
+ nonNullResult.forEach((v) => console.log(v));
+ }
+
+ return {
+ code,
+ args: data,
+ };
+ }
+
+ public async writeCmd(...args: string[]) {
+ if (!this.#writer) {
+ return null;
+ }
+
+ if (this.config.debug.log) {
+ console.table(args);
+ }
+
+ const data = encoder.encode([...args].join(" ") + "\r\n");
+ await this.#writer.write(data);
+ await this.#writer.flush();
+ }
+
+ public async writeCmdBinary(...args: Uint8Array[]) {
+ if (!this.#writer) {
+ return null;
+ }
+
+ if (this.config.debug.log) {
+ console.table(args.map(() => "Uint8Attay"));
+ }
+
+ for (let i = 0; i < args.length; i++) {
+ await this.#writer.write(args[i]);
+ }
+ await this.#writer.flush();
+ }
+}
diff --git a/client/mod.ts b/client/mod.ts
new file mode 100644
index 0000000..add86ed
--- /dev/null
+++ b/client/mod.ts
@@ -0,0 +1,155 @@
+import { SMTPWorkerPool } from "./pool.ts";
+import { SMTPWorker } from "./worker/worker.ts";
+import { SMTPClient } from "./basic/client.ts";
+import {
+ ClientOptions,
+ resolveClientOptions,
+ ResolvedClientOptions,
+} from "../config/client.ts";
+import {
+ resolveSendConfig,
+ SendConfig,
+ validateConfig,
+} from "../config/mail/mod.ts";
+
+/**
+ * SMTP-Client with support for pool, etc.
+ * ```ts
+ * const client = new SMTPClient({
+ * connection: {
+ * hostname: "smtp.example.com",
+ * port: 465,
+ * tls: true,
+ * auth: {
+ * username: "example",
+ * password: "password",
+ * },
+ * },
+ * });
+ *
+ * await client.send({
+ * from: "me@example.com",
+ * to: "you@example.com",
+ * subject: "example",
+ * content: "...",
+ * html: "...
",
+ * });
+ *
+ * await client.close();
+ * ```
+ */
+export class SMTPHandler {
+ #internalClient: SMTPWorker | SMTPWorkerPool | SMTPClient;
+ #clientConfig: ResolvedClientOptions;
+
+ /**
+ * create a new SMTPClient
+ *
+ * ```ts
+ * const client = new SMTPClient({
+ * connection: {
+ * hostname: "smtp.example.com",
+ * port: 465,
+ * tls: true,
+ * auth: {
+ * username: "example",
+ * password: "password",
+ * },
+ * },
+ * pool: {
+ * size: 2,
+ * timeout: 60000
+ * },
+ * client: {
+ * warning: 'log',
+ * preprocessors: [filterBurnerMails]
+ * },
+ * debug: {
+ * log: false,
+ * allowUnsecure: false,
+ * encodeLB: false,
+ * noStartTLS: false
+ * }
+ * });
+ *
+ * ```
+ *
+ * @param config client config
+ */
+ constructor(config: ClientOptions) {
+ const resolvedConfig = resolveClientOptions(config);
+
+ resolvedConfig.client.preprocessors.push(validateConfig);
+
+ this.#clientConfig = resolvedConfig;
+
+ if (resolvedConfig.debug.log) {
+ console.log("used resolved config");
+ console.log(".debug");
+ console.table(resolvedConfig.debug);
+ console.log(".connection");
+ console.table({
+ ...resolvedConfig.connection,
+ ...resolvedConfig.connection.auth
+ ? { auth: JSON.stringify(resolvedConfig.connection.auth) }
+ : {},
+ });
+ console.log(".pool");
+ console.table(resolvedConfig.pool);
+ }
+
+ const Client = resolvedConfig.pool
+ ? (resolvedConfig.pool.size > 1 ? SMTPWorkerPool : SMTPWorker)
+ : SMTPClient;
+
+ this.#internalClient = new Client(resolvedConfig);
+ }
+
+ /**
+ * Sends a E-Mail with the correspondig config.
+ * ```ts
+ * client.send({
+ * to: 'abc@example.com',
+ * cc: [
+ * 'abc@example.com',
+ * 'abc ',
+ * {
+ * name: 'abc',
+ * mail: 'abc@example.com'
+ * }
+ * ],
+ * bcc: {
+ * abc: 'abc@example.com',
+ * other: 'abc@example.com'
+ * },
+ * from: 'me ',
+ * replyTo: '',
+ * subject: 'example',
+ * content: 'auto',
+ * html: 'Hello World
',
+ * internalTag: 'newsletter',
+ * priority: 'low'
+ * })
+ * ```
+ * @param config Email config
+ * @returns nothing (for now as this might change in the future!)
+ */
+ send(config: SendConfig): Promise {
+ let resolvedConfig = resolveSendConfig(config);
+
+ for (let i = 0; i < this.#clientConfig.client.preprocessors.length; i++) {
+ const cb = this.#clientConfig.client.preprocessors[i];
+
+ resolvedConfig = cb(resolvedConfig, this.#clientConfig);
+ }
+
+ return this.#internalClient.send(resolvedConfig);
+ }
+
+ /**
+ * Closes the connection (kills all Worker / closes connection)
+ */
+ close(): void | Promise {
+ return this.#internalClient.close();
+ }
+}
diff --git a/client/pool.ts b/client/pool.ts
new file mode 100644
index 0000000..b64aa5a
--- /dev/null
+++ b/client/pool.ts
@@ -0,0 +1,27 @@
+import { ResolvedSendConfig } from "../config/mail/mod.ts";
+import { ResolvedClientOptions } from "../config/client.ts";
+import { SMTPWorker } from "./worker/worker.ts";
+
+export class SMTPWorkerPool {
+ pool: SMTPWorker[] = [];
+
+ constructor(
+ config: ResolvedClientOptions,
+ ) {
+ for (let i = 0; i < config.pool!.size; i++) {
+ this.pool.push(new SMTPWorker(config));
+ }
+ }
+
+ #lastUsed = -1;
+
+ send(mail: ResolvedSendConfig) {
+ this.#lastUsed = (this.#lastUsed + 1) % this.pool.length;
+
+ return this.pool[this.#lastUsed].send(mail);
+ }
+
+ close() {
+ this.pool.forEach((v) => v.close());
+ }
+}
diff --git a/worker.ts b/client/worker/worker-file.ts
similarity index 52%
rename from worker.ts
rename to client/worker/worker-file.ts
index 00abb78..1ec749e 100644
--- a/worker.ts
+++ b/client/worker/worker-file.ts
@@ -2,10 +2,10 @@
///
///
-import { SmtpClient } from "./smtp.ts";
-import { SendConfig } from "./config.ts";
+import { SMTPClient } from "../basic/client.ts";
+import { ResolvedSendConfig } from "../../config/mail/mod.ts";
-const client = new SmtpClient({ console_debug: true });
+let client: SMTPClient;
let cb: () => void;
const readyPromise = new Promise((res) => {
@@ -14,7 +14,7 @@ const readyPromise = new Promise((res) => {
let hasIdlePromise = false;
-async function send(config: SendConfig) {
+async function send(config: ResolvedSendConfig) {
client.send(config);
if (!hasIdlePromise) {
@@ -27,7 +27,7 @@ async function send(config: SendConfig) {
addEventListener("message", async (ev: MessageEvent) => {
if (ev.data.__setup) {
- await client.connectTLS(ev.data.__setup);
+ client = new SMTPClient(ev.data.__setup);
cb();
return;
}
@@ -35,6 +35,20 @@ addEventListener("message", async (ev: MessageEvent) => {
postMessage(client.isSending);
return;
}
- await readyPromise;
- send(ev.data);
+
+ if (ev.data.__mail) {
+ await readyPromise;
+ try {
+ const data = await send(ev.data.mail);
+ postMessage({
+ __ret: ev.data.__mail,
+ res: data,
+ });
+ } catch (ex) {
+ postMessage({
+ __ret: ev.data.__mail,
+ err: ex,
+ });
+ }
+ }
});
diff --git a/client/worker/worker.ts b/client/worker/worker.ts
new file mode 100644
index 0000000..73cd544
--- /dev/null
+++ b/client/worker/worker.ts
@@ -0,0 +1,134 @@
+import { ResolvedSendConfig } from "../../config/mail/mod.ts";
+import { ResolvedClientOptions } from "../../config/client.ts";
+
+export class SMTPWorker {
+ id = 1;
+ #timeout: number;
+
+ constructor(
+ config: ResolvedClientOptions,
+ ) {
+ this.#config = config;
+ this.#timeout = config.pool!.timeout;
+ }
+ #w!: Worker;
+ #idleTO: number | null = null;
+ #idleMode2 = false;
+ #noCon = true;
+ #config: ResolvedClientOptions;
+
+ #resolver = new Map<
+ number,
+ // deno-lint-ignore no-explicit-any
+ { res: (res: any) => void; rej: (err: Error) => void }
+ >();
+
+ #startup() {
+ this.#w = new Worker(new URL("./worker-file.ts", import.meta.url), {
+ type: "module",
+ deno: {
+ permissions: {
+ net: "inherit",
+ // ts files
+ read: true,
+ },
+ namespace: true,
+ },
+ // This allowes the deno option so only for pool and worker we need --unstable
+ // deno-lint-ignore no-explicit-any
+ } as any);
+
+ this.#w.addEventListener(
+ "message",
+ // deno-lint-ignore no-explicit-any
+ (ev: MessageEvent) => {
+ if (typeof ev.data === "object") {
+ if ("err" in ev.data) {
+ this.#resolver.get(ev.data.__ret)?.rej(ev.data.err);
+ }
+
+ if ("res" in ev.data) {
+ this.#resolver.get(ev.data.__ret)?.res(ev.data.res);
+ }
+
+ this.#resolver.delete(ev.data.__ret);
+ return;
+ }
+
+ if (ev.data) {
+ this.#stopIdle();
+ } else {
+ if (this.#idleMode2) {
+ this.#cleanup();
+ } else {
+ this.#startIdle();
+ }
+ }
+ },
+ );
+
+ this.#w.postMessage({
+ __setup: {
+ ...this.#config,
+ client: {
+ ...this.#config.client,
+ preprocessors: [],
+ },
+ },
+ });
+
+ this.#noCon = false;
+ }
+
+ #startIdle() {
+ console.log("started idle");
+ if (this.#idleTO) {
+ return;
+ }
+
+ this.#idleTO = setTimeout(() => {
+ console.log("idle mod 2");
+ this.#idleMode2 = true;
+ this.#w.postMessage({ __check_idle: true });
+ }, this.#timeout);
+ }
+
+ #stopIdle() {
+ if (this.#idleTO) {
+ clearTimeout(this.#idleTO);
+ }
+
+ this.#idleMode2 = false;
+ this.#idleTO = null;
+ }
+
+ #cleanup() {
+ console.log("killed");
+ this.#w.terminate();
+ this.#stopIdle();
+ }
+
+ public send(mail: ResolvedSendConfig) {
+ const myID = this.id;
+ this.id++;
+ this.#stopIdle();
+ if (this.#noCon) {
+ this.#startup();
+ }
+ this.#w.postMessage({
+ __mail: myID,
+ mail,
+ });
+
+ return new Promise((res, rej) => {
+ this.#resolver.set(myID, { res, rej });
+ });
+ }
+
+ close() {
+ if (this.#w) this.#w.terminate();
+ if (this.#idleTO) {
+ clearTimeout(this.#idleTO);
+ }
+ }
+}
diff --git a/code.ts b/code.ts
deleted file mode 100644
index 0407b5f..0000000
--- a/code.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-export const CommandCode = {
- READY: 220,
- AUTHO_SUCCESS: 235,
- OK: 250,
- BEGIN_DATA: 354,
- FAIL: 554,
-};
diff --git a/config.ts b/config.ts
deleted file mode 100644
index b34dff0..0000000
--- a/config.ts
+++ /dev/null
@@ -1,161 +0,0 @@
-interface ConnectConfig {
- hostname: string;
- port?: number;
-}
-
-interface ConnectConfigWithAuthentication extends ConnectConfig {
- username: string;
- password: string;
-}
-
-interface SendConfig {
- to: mailList;
- cc?: mailList;
- bcc?: mailList;
- from: string;
- date?: string;
- subject: string;
- content?: string;
- mimeContent?: Content[];
- html?: string;
- inReplyTo?: string;
- replyTo?: string;
- references?: string;
- priority?: "high" | "normal" | "low";
- attachments?: attachment[];
- internalTag?: string | symbol
-}
-
-interface baseAttachment {
- contentType: string;
- filename: string;
-}
-
-type attachment =
- & (
- | textAttachment
- | base64Attachment
- | arrayBufferLikeAttachment
- )
- & baseAttachment;
-
-type textAttachment = { encoding: "text"; content: string };
-type base64Attachment = { encoding: "base64"; content: string };
-type arrayBufferLikeAttachment = {
- content: ArrayBufferLike | Uint8Array;
- encoding: "binary";
-};
-
-interface Content {
- mimeType: string;
- content: string;
- transferEncoding?: string;
-}
-
-export type email = string;
-export type emailWithName = string;
-export type wrapedMail = string;
-
-export interface mailObject {
- mail: string;
- name?: string;
-}
-
-export type mail = string | mailObject;
-export type mailListObject = Omit, "name" | "mail">;
-export type mailList = mailListObject | mail[] | mail;
-
-export type { ConnectConfig, ConnectConfigWithAuthentication, SendConfig };
-
-function isMail(mail: string) {
- return /[^<>()\[\]\\,;:\s@"]+@[a-zA-Z0-9]+\.([a-zA-Z0-9\-]+\.)*[a-zA-Z]{2,}$/
- .test(mail);
-}
-
-function isSingleMail(mail: string) {
- return /^(([^<>()\[\]\\,;:\s@"]+@[a-zA-Z0-9\-]+\.([a-zA-Z0-9\-]+\.)*[a-zA-Z]{2,})|(<[^<>()\[\]\\,;:\s@"]+@[a-zA-Z0-9]+\.([a-zA-Z0-9\-]+\.)*[a-zA-Z]{2,}>)|([^<>]+ <[^<>()\[\]\\,;:\s@"]+@[a-zA-Z0-9]+\.([a-zA-Z0-9\-]+\.)*[a-zA-Z]{2,}>))$/
- .test(mail);
-}
-
-export function validateConfig(config: SendConfig) {
- if (config.from) {
- if (!isSingleMail(config.from)) {
- throw new Error("Mail From is not a valid E-Mail.");
- }
- }
-
- if (!validateMailList(config.to)) {
- throw new Error("Mail TO is not a valid E-Mail.");
- }
-
- if (config.cc && !validateMailList(config.cc)) {
- throw new Error("Mail CC is not a valid E-Mail.");
- }
- if (config.bcc && !validateMailList(config.bcc)) {
- throw new Error("Mail BCC is not a valid E-Mail.");
- }
-
- if (config.replyTo && !isSingleMail(config.replyTo)) {
- throw new Error("Mail ReplyTo is not a valid E-Mail.");
- }
-
- return true;
-}
-
-function validateMailList(mailList: mailList) {
- if (typeof mailList === "string") return isSingleMail(mailList);
-
- if (Array.isArray(mailList)) {
- return !mailList.some((m) => {
- if (typeof m === "string") return !isSingleMail(m);
- return !isMail(m.mail);
- });
- }
-
- if (
- (Object.keys(mailList).length === 1 && mailList.mail) ||
- (Object.keys(mailList).length === 2 && mailList.mail && mailList.name)
- ) {
- return isMail(mailList.mail);
- }
-
- return !Object.values(mailList).some((m) => !isSingleMail(m));
-}
-
-export function normaliceMailString(mail: string) {
- if (mail.includes("<")) {
- return mail;
- } else {
- return `<${mail}>`;
- }
-}
-
-export function normaliceMailList(
- mails?: mailList | null,
-): string[] {
- if (!mails) return [];
-
- if (typeof mails === "string") {
- return [normaliceMailString(mails)];
- } else if (Array.isArray(mails)) {
- return mails.map((m) => {
- if (typeof m === "string") {
- if (m.includes("<")) {
- return m;
- } else {
- return `<${m}>`;
- }
- } else {
- return m.name ? (`${m.name} <${m.mail}>`) : (`<${m.mail}>`);
- }
- });
- } else if (mails.mail) {
- return [
- mails.name ? (`${mails.name} <${mails.mail}>`) : (`<${mails.mail}>`),
- ];
- } else {
- return Object.entries(mails as mailListObject).map(
- ([name, mail]: [string, string]) => `${name} <${mail}>`,
- );
- }
-}
diff --git a/config/client.ts b/config/client.ts
new file mode 100644
index 0000000..6d5143d
--- /dev/null
+++ b/config/client.ts
@@ -0,0 +1,149 @@
+import { ResolvedSendConfig } from "./mail/mod.ts";
+
+export interface ResolvedClientOptions {
+ debug: {
+ log: boolean;
+ allowUnsecure: boolean;
+ encodeLB: boolean;
+ noStartTLS: boolean;
+ };
+ connection: {
+ hostname: string;
+ port: number;
+ auth?: {
+ username: string;
+ password: string;
+ };
+ tls: boolean;
+ };
+ pool?: {
+ size: number;
+ timeout: number;
+ };
+ client: {
+ warning: "ignore" | "log" | "error";
+ preprocessors: Preprocessor[];
+ };
+}
+
+/**
+ * Options to create a new SMTP CLient
+ */
+export interface ClientOptions {
+ debug?: {
+ /**
+ * USE WITH COUTION AS THIS WILL LOG YOUR USERDATA AND ALL MAIL CONTENT TO STDOUT!
+ * @default false
+ */
+ log?: boolean;
+ /**
+ * USE WITH COUTION AS THIS WILL POSIBLY EXPOSE YOUR USERDATA AND ALL MAIL CONTENT TO ATTACKERS!
+ * @default false
+ */
+ allowUnsecure?: boolean;
+ /**
+ * USE WITH COUTION AS THIS COULD INTODRUCE BUGS
+ *
+ * This option is mainly to allow debuging to exclude some possible problem surfaces at encoding
+ * @default false
+ */
+ encodeLB?: boolean;
+ /**
+ * Disable starttls
+ */
+ noStartTLS?: boolean;
+ };
+ connection: {
+ hostname: string;
+ /**
+ * For TLS the default port is 465 else 25.
+ * @default 25 or 465
+ */
+ port?: number;
+ /**
+ * authentication data
+ */
+ auth?: {
+ username: string;
+ password: string;
+ };
+ /**
+ * Set this to `true` to connect via SSL / TLS if set to `false` STARTTLS is used.
+ * Only if `allowUnsecure` is used userdata or mail content could be exposed!
+ *
+ * @default false
+ */
+ tls?: boolean;
+ };
+ /**
+ * Create multiple connections so you can send emails faster!
+ */
+ pool?: {
+ /**
+ * Number of Workers
+ * @default 2
+ */
+ size?: number;
+ /**
+ * Time the connection has to be idle to be closed. (in ms)
+ * If a value > 1h is set it will be set to 1h
+ * @default 60000
+ */
+ timeout?: number;
+ } | boolean;
+ client?: {
+ /**
+ * There are some cases where warnings are created. These are loged by default but you can 'ignore' them or all warnings should be considered 'error'.
+ *
+ * @default log
+ */
+ warning?: "ignore" | "log" | "error";
+ /**
+ * List of preproccessors to
+ *
+ * - Filter mail
+ * - BCC all mails to someone
+ * - ...
+ */
+ preprocessors?: Preprocessor[];
+ };
+}
+
+export type Preprocessor = (
+ mail: ResolvedSendConfig,
+ client: ResolvedClientOptions,
+) => ResolvedSendConfig;
+
+export function resolveClientOptions(
+ config: ClientOptions,
+): ResolvedClientOptions {
+ return {
+ debug: {
+ log: config.debug?.log ?? false,
+ allowUnsecure: config.debug?.allowUnsecure ?? false,
+ encodeLB: config.debug?.encodeLB ?? false,
+ noStartTLS: config.debug?.noStartTLS ?? false,
+ },
+ connection: {
+ hostname: config.connection.hostname,
+ port: config.connection.port ?? (config.connection.tls ? 465 : 25),
+ tls: config.connection.tls ?? false,
+ auth: config.connection.auth,
+ },
+ pool: (config.pool
+ ? (config.pool === true
+ ? {
+ size: 2,
+ timeout: 60000,
+ }
+ : {
+ size: config.pool.size ?? 2,
+ timeout: config.pool.timeout ?? 60000,
+ })
+ : undefined),
+ client: {
+ warning: config.client?.warning ?? "log",
+ preprocessors: config.client?.preprocessors ?? [],
+ },
+ };
+}
diff --git a/config/mail/attachments.ts b/config/mail/attachments.ts
new file mode 100644
index 0000000..3fd43fa
--- /dev/null
+++ b/config/mail/attachments.ts
@@ -0,0 +1,41 @@
+import { base64Decode } from "./encoding.ts";
+
+interface baseAttachment {
+ contentType: string;
+ filename: string;
+}
+
+export type Attachment =
+ & (
+ | textAttachment
+ | base64Attachment
+ | arrayBufferLikeAttachment
+ )
+ & baseAttachment;
+
+export type ResolvedAttachment =
+ & (
+ | textAttachment
+ | arrayBufferLikeAttachment
+ )
+ & baseAttachment;
+
+type textAttachment = { encoding: "text"; content: string };
+type base64Attachment = { encoding: "base64"; content: string };
+type arrayBufferLikeAttachment = {
+ content: ArrayBufferLike | Uint8Array;
+ encoding: "binary";
+};
+
+export function resolveAttachment(attachment: Attachment): ResolvedAttachment {
+ if (attachment.encoding === "base64") {
+ return {
+ filename: attachment.filename,
+ contentType: attachment.contentType,
+ encoding: "binary",
+ content: base64Decode(attachment.content),
+ };
+ } else {
+ return attachment;
+ }
+}
diff --git a/config/mail/content.ts b/config/mail/content.ts
new file mode 100644
index 0000000..a83cf47
--- /dev/null
+++ b/config/mail/content.ts
@@ -0,0 +1,44 @@
+import { quotedPrintableEncode } from "./encoding.ts";
+
+export interface Content {
+ mimeType: string;
+ content: string;
+ transferEncoding?: string;
+}
+
+export function resolveContent({
+ text,
+ html,
+ mimeContent,
+}: {
+ text?: string;
+ html?: string;
+ mimeContent?: Content[];
+}): Content[] {
+ const newContent = [...mimeContent ?? []];
+
+ if (text === "auto" && html) {
+ text = html
+ .replace(//g, "")
+ .replace(/