From 96600966a7e1fd849b6caafaf0ea27d680a0b3d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= <2pi_r2@gmx.de> Date: Wed, 30 Mar 2022 12:13:46 +0200 Subject: [PATCH] add filter --- README.md | 14 +++++++++++ config.ts | 1 + smtp.ts | 70 +++++++++++++++++++++++++++++++++++++++++-------------- 3 files changed, 67 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 52cfbfc..ca9ff81 100644 --- a/README.md +++ b/README.md @@ -128,6 +128,20 @@ await client.send({ await client.close(); ``` +### 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`. + +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: + +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 + +So you can decide if a single mail error results in a complete mail abort or it only get removed from the list. + +You can for example validate against this list: https://github.com/wesbos/burner-email-providers. + + ### Configuring your client You can pass options to your client through the `SmtpClient` constructor. diff --git a/config.ts b/config.ts index 1d3834b..b34dff0 100644 --- a/config.ts +++ b/config.ts @@ -23,6 +23,7 @@ interface SendConfig { references?: string; priority?: "high" | "normal" | "low"; attachments?: attachment[]; + internalTag?: string | symbol } interface baseAttachment { diff --git a/smtp.ts b/smtp.ts index ed4bcc2..f826cd2 100644 --- a/smtp.ts +++ b/smtp.ts @@ -22,6 +22,7 @@ interface Command { interface SmtpClientOptions { console_debug?: boolean; unsecure?: boolean; + mailFilter?: (box: string, domain: string, internalTag?: string | symbol | undefined) => (Promise | boolean) } export class SmtpClient { @@ -33,13 +34,16 @@ export class SmtpClient { #console_debug = false; #allowUnsecure = false; + #mailFilter: SmtpClientOptions['mailFilter'] constructor({ console_debug = false, unsecure = false, + mailFilter }: SmtpClientOptions = {}) { this.#console_debug = console_debug; this.#allowUnsecure = unsecure; + this.#mailFilter = mailFilter } async connect(config: ConnectConfig | ConnectConfigWithAuthentication) { @@ -104,7 +108,25 @@ export class SmtpClient { const [from, fromData] = this.parseAddress(config.from); - const to = normaliceMailList(config.to).map((m) => this.parseAddress(m)); + const to = config.to ? await this.filterMails(normaliceMailList(config.to).map((m) => this.parseAddress(m)), config.internalTag) : false; + + const cc = config.cc + ? await this.filterMails(normaliceMailList(config.cc).map((v) => this.parseAddress(v)), config.internalTag) + : false; + + const bcc =config.bcc ? await this.filterMails(normaliceMailList(config.bcc).map((v) => + this.parseAddress(v) + ), config.internalTag) : false; + + if (config.replyTo) { + config.replyTo = normaliceMailString(config.replyTo); + + const res = await this.filterMail([config.replyTo, config.replyTo], config.internalTag) + + if(!res) { + throw new Error("ReplyTo Email is not vaild as the filter returned false") + } + } const date = config.date ?? new Date().toUTCString().split(",")[1].slice(1); @@ -156,28 +178,22 @@ export class SmtpClient { await this.writeCmd("MAIL", "FROM:", from); this.assertCode(await this.readCmd(), CommandCode.OK); - for (let i = 0; i < to.length; i++) { - await this.writeCmd("RCPT", "TO:", to[i][0]); - this.assertCode(await this.readCmd(), CommandCode.OK); + if(to) { + for (let i = 0; i < to.length; i++) { + await this.writeCmd("RCPT", "TO:", to[i][0]); + this.assertCode(await this.readCmd(), CommandCode.OK); + } } - const cc = config.cc - ? normaliceMailList(config.cc).map((v) => this.parseAddress(v)) - : false; - if (cc) { - console.log("cc"); for (let i = 0; i < cc.length; i++) { await this.writeCmd("RCPT", "TO:", cc[i][0]); this.assertCode(await this.readCmd(), CommandCode.OK); } } + - if (config.bcc) { - const bcc = normaliceMailList(config.bcc).map((v) => - this.parseAddress(v) - ); - + if (bcc) { for (let i = 0; i < bcc.length; i++) { await this.writeCmd("RCPT", "TO:", bcc[i][0]); this.assertCode(await this.readCmd(), CommandCode.OK); @@ -189,7 +205,9 @@ export class SmtpClient { await this.writeCmd("Subject: ", config.subject); await this.writeCmd("From: ", fromData); - await this.writeCmd("To: ", to.map((v) => v[1]).join(";")); + if(to) { + await this.writeCmd("To: ", to.map((v) => v[1]).join(";")); + } if (cc) { await this.writeCmd("Cc: ", cc.map((v) => v[1]).join(";")); } @@ -204,8 +222,6 @@ export class SmtpClient { } if (config.replyTo) { - config.replyTo = normaliceMailString(config.replyTo); - await this.writeCmd("ReplyTo: ", config.replyTo); } @@ -438,6 +454,24 @@ export class SmtpClient { return [`<${m}>`, email]; } else { return [`<${email}>`, `<${email}>`]; - } + } + } + + private async filterMail([raw, _withName]: [string, string], internalTag: string | symbol | undefined): Promise { + if(!this.#mailFilter) return true + + const [box, domain] = raw.slice(1, raw.length - 1).split('@') + + const res = await this.#mailFilter!(box, domain, internalTag) + + return res + } + + private async filterMails(mails: [string, string][], internalTag: string | symbol | undefined): Promise<[string, string][]> { + if(!this.#mailFilter) return mails + + const keep = await Promise.all(mails.map(m => this.filterMail(m, internalTag))) + + return mails.filter((_, i) => keep[i]) } }