Skip to content
Open
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
8 changes: 2 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ AdonisJS v5 package for email validation and a DB-backed suppression list.
## Install

```bash
npm i adonis-email-guard
node ace configure adonis-email-guard
npm i @ezycourse/adonis-email-guard
node ace configure @ezycourse/adonis-email-guard
```

The configure step copies `config/email-guard.ts` into your app and registers the provider.
Expand Down Expand Up @@ -55,8 +55,6 @@ if (!c.valid || c.suppressed) return; // do not send
await EmailGuard.suppress('bounce@x.com', 'bounce', 'webhook', {provider: 'postmark'});
await EmailGuard.isSuppressed('bounce@x.com'); // true
await EmailGuard.find('bounce@x.com');
await EmailGuard.list({reason: 'bounce', limit: 50});
await EmailGuard.count({source: 'webhook'});
await EmailGuard.unsuppress('bounce@x.com');
```

Expand Down Expand Up @@ -84,8 +82,6 @@ Returns `{ valid, email, reason?, checks, suggestion?, mxHost? }`.
| `unsuppress(email)` | Delete. Returns `true` if a row was removed. |
| `isSuppressed(email)` | Boolean lookup. |
| `find(email)` | Row or `null`. |
| `list(filters?)` | Filter by `reason`/`source`/`search`, with paging. |
| `count(filters?)` | Same filters, returns a count. |
| `ensureTable()` | Idempotent `CREATE TABLE IF NOT EXISTS`. |

## Suppression table schema
Expand Down
10 changes: 0 additions & 10 deletions adonis-typings/email-guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,6 @@ declare module '@ioc:Adonis/Addons/EmailGuard' {
updated_at: Date;
};

export type SuppressionListFilters = {
reason?: SuppressionReason;
source?: SuppressionSource;
search?: string;
limit?: number;
offset?: number;
};

export type EmailGuardConfig = {
tableName?: string;
/** Run `CREATE TABLE IF NOT EXISTS` for the suppression table on app boot. Default true. */
Expand Down Expand Up @@ -82,8 +74,6 @@ declare module '@ioc:Adonis/Addons/EmailGuard' {
): Promise<SuppressionRow>;
unsuppress(email: string): Promise<boolean>;
find(email: string): Promise<SuppressionRow | null>;
list(filters?: SuppressionListFilters): Promise<SuppressionRow[]>;
count(filters?: Omit<SuppressionListFilters, 'limit' | 'offset'>): Promise<number>;
/** Validates and, on failure, optionally records a suppression. Combines validate + isSuppressed. */
check(
email: string,
Expand Down
6 changes: 3 additions & 3 deletions instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

Setup steps after install:

1. **Run the configure command** (already done if you used `node ace configure adonis-email-guard`). This copies `config/email-guard.ts` into your app.
1. **Run the configure command** (already done if you used `node ace configure @ezycourse/adonis-email-guard`). This copies `config/email-guard.ts` into your app.

2. **Verify the provider was registered** in `.adonisrc.json`:

```json
{
"providers": ["./providers/AppProvider", "adonis-email-guard"]
"providers": ["./providers/AppProvider", "@ezycourse/adonis-email-guard"]
}
```

Expand All @@ -17,7 +17,7 @@ Setup steps after install:
```json
{
"compilerOptions": {
"types": ["@adonisjs/core", "@adonisjs/lucid", "adonis-email-guard"]
"types": ["@adonisjs/core", "@adonisjs/lucid", "@ezycourse/adonis-email-guard"]
}
}
```
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "adonis-email-guard",
"name": "@ezycourse/adonis-email-guard",
"version": "0.1.0",
"description": "AdonisJS v5 package: email validation (syntax, MX, SMTP, disposable, spam-trap) + DB-backed suppression list with configurable table and auto-create.",
"main": "build/providers/EmailGuardProvider.js",
Expand Down Expand Up @@ -62,9 +62,9 @@
},
"adonisjs": {
"instructionsMd": "./build/instructions.md",
"types": "adonis-email-guard",
"types": "@ezycourse/adonis-email-guard",
"providers": [
"adonis-email-guard"
"@ezycourse/adonis-email-guard"
],
"templates": {
"config": [
Expand All @@ -76,7 +76,7 @@
}
},
"publishConfig": {
"access": "public",
"access": "restricted",
"tag": "latest"
}
}
11 changes: 0 additions & 11 deletions src/EmailGuard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import type {
EmailGuardConfig,
EmailGuardContract,
EmailValidationResult,
SuppressionListFilters,
SuppressionReason,
SuppressionRow,
SuppressionSource,
Expand Down Expand Up @@ -65,16 +64,6 @@ export class EmailGuard implements EmailGuardContract {
return this.suppression.unsuppress(email);
}

public list(filters?: SuppressionListFilters): Promise<SuppressionRow[]> {
return this.suppression.list(filters);
}

public count(
filters?: Omit<SuppressionListFilters, 'limit' | 'offset'>
): Promise<number> {
return this.suppression.count(filters);
}

public ensureTable(): Promise<void> {
return this.suppression.ensureTable();
}
Expand Down
24 changes: 0 additions & 24 deletions src/Suppression/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type {DatabaseContract, QueryClientContract} from '@ioc:Adonis/Lucid/Database';
import type {
SuppressionListFilters,
SuppressionReason,
SuppressionRow,
SuppressionSource,
Expand Down Expand Up @@ -104,29 +103,6 @@ export class SuppressionService {
return Number(count) > 0;
}

public async list(filters: SuppressionListFilters = {}): Promise<SuppressionRow[]> {
const q = this.client().from(this.tableName).select('*');
if (filters.reason) q.where('reason', filters.reason);
if (filters.source) q.where('source', filters.source);
if (filters.search) q.where('email', 'like', `%${filters.search.toLowerCase()}%`);
if (filters.limit) q.limit(filters.limit);
if (filters.offset) q.offset(filters.offset);
q.orderBy('id', 'desc');
const rows = await q;
return rows.map((r: any) => this.hydrate(r));
}

public async count(
filters: Omit<SuppressionListFilters, 'limit' | 'offset'> = {}
): Promise<number> {
const q = this.client().from(this.tableName);
if (filters.reason) q.where('reason', filters.reason);
if (filters.source) q.where('source', filters.source);
if (filters.search) q.where('email', 'like', `%${filters.search.toLowerCase()}%`);
const row = (await q.count('* as total').first()) as {total: number | string} | undefined;
return row ? Number(row.total) : 0;
}

private hydrate(row: any): SuppressionRow {
let metadata: Record<string, unknown> | null = null;
if (row.metadata !== null && row.metadata !== undefined) {
Expand Down
49 changes: 2 additions & 47 deletions test/suppression.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,8 @@ describe('SuppressionService', () => {
assert.equal(updated.source, 'manual');
assert.deepEqual(updated.metadata, {note: 'updated'});

const all = await service.list();
assert.equal(all.length, 1);
const rows = await k(TABLE).select('id');
assert.equal(rows.length, 1);
});

it('handles null metadata', async () => {
Expand Down Expand Up @@ -82,49 +82,4 @@ describe('SuppressionService', () => {
assert.equal(await service.isSuppressed('gone@x.com'), false);
});
});

describe('list / count', () => {
beforeEach(async () => {
await service.suppress('one@x.com', 'bounce', 'webhook');
await service.suppress('two@x.com', 'bounce', 'manual');
await service.suppress('three@x.com', 'complaint', 'webhook');
await service.suppress('four@x.com', 'manual', 'manual');
});

it('filters by reason', async () => {
const rows = await service.list({reason: 'bounce'});
assert.equal(rows.length, 2);
assert.equal(await service.count({reason: 'bounce'}), 2);
});

it('filters by source', async () => {
const rows = await service.list({source: 'webhook'});
assert.equal(rows.length, 2);
assert.equal(await service.count({source: 'webhook'}), 2);
});

it('filters by search (LIKE on email)', async () => {
const rows = await service.list({search: 'three'});
assert.equal(rows.length, 1);
assert.equal(rows[0].email, 'three@x.com');
});

it('paginates', async () => {
const page1 = await service.list({limit: 2, offset: 0});
const page2 = await service.list({limit: 2, offset: 2});
assert.equal(page1.length, 2);
assert.equal(page2.length, 2);
assert.notEqual(page1[0].email, page2[0].email);
});

it('orders newest first', async () => {
const all = await service.list();
assert.equal(all[0].email, 'four@x.com');
assert.equal(all[all.length - 1].email, 'one@x.com');
});

it('counts all when no filter', async () => {
assert.equal(await service.count(), 4);
});
});
});