Skip to content
This repository was archived by the owner on Nov 22, 2025. It is now read-only.
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
68 changes: 67 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,75 @@ jobs:
yarn test

- name: Release
id: semantic_release
if: github.ref == 'refs/heads/develop'
run: |
git config --global user.email "support@dev.me"
git config --global user.name "DEV.ME Team"
npm i -g semantic-release @semantic-release/git @semantic-release/github conventional-changelog-conventionalcommits
npx semantic-release --no-ci --debug
npx semantic-release --no-ci --debug 2>&1 | tee release-output.txt

# Extract version and tag info from release output
if grep -q "Published release" release-output.txt; then
echo "release_published=true" >> $GITHUB_OUTPUT
VERSION=$(grep -oP 'Published release \K[0-9]+\.[0-9]+\.[0-9]+' release-output.txt | head -1)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=v$VERSION" >> $GITHUB_OUTPUT
else
echo "release_published=false" >> $GITHUB_OUTPUT
fi

- name: Add CI Summary
if: always()
run: |
echo "## 🔬 CI Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY

# Check if release step was run (only on develop branch)
if [[ "${{ github.ref }}" == "refs/heads/develop" ]]; then
if [[ "${{ steps.semantic_release.outputs.release_published }}" == "true" ]]; then
echo "### ✅ Pre-release Published Successfully!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Version:** \`${{ steps.semantic_release.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Tag:** \`${{ steps.semantic_release.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🔗 Links" >> $GITHUB_STEP_SUMMARY
echo "- [NPM Package](https://www.npmjs.com/package/@devmehq/email-validator-js/v/${{ steps.semantic_release.outputs.version }})" >> $GITHUB_STEP_SUMMARY
echo "- [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/${{ steps.semantic_release.outputs.tag }})" >> $GITHUB_STEP_SUMMARY
else
echo "### ℹ️ No Pre-release Published" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "No pre-release was created. This could be because:" >> $GITHUB_STEP_SUMMARY
echo "- No relevant commits found for release" >> $GITHUB_STEP_SUMMARY
echo "- Commits don't follow conventional commit format" >> $GITHUB_STEP_SUMMARY
echo "- Release conditions not met" >> $GITHUB_STEP_SUMMARY
fi
else
echo "### ✅ CI Tests Passed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "All tests completed successfully on branch \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "ℹ️ **Note:** Releases are only created from the \`develop\` branch" >> $GITHUB_STEP_SUMMARY
fi

echo "" >> $GITHUB_STEP_SUMMARY
echo "### 📊 Build Information" >> $GITHUB_STEP_SUMMARY
echo "- **Workflow:** \`${{ github.workflow }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Run ID:** \`${{ github.run_id }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Run Number:** \`${{ github.run_number }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Actor:** \`${{ github.actor }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Event:** \`${{ github.event_name }}\`" >> $GITHUB_STEP_SUMMARY

# Add PR information if available
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🔀 Pull Request Information" >> $GITHUB_STEP_SUMMARY
echo "- **PR Number:** #${{ github.event.pull_request.number }}" >> $GITHUB_STEP_SUMMARY
echo "- **PR Title:** ${{ github.event.pull_request.title }}" >> $GITHUB_STEP_SUMMARY
echo "- **Base Branch:** \`${{ github.event.pull_request.base.ref }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Head Branch:** \`${{ github.event.pull_request.head.ref }}\`" >> $GITHUB_STEP_SUMMARY
fi
47 changes: 46 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,53 @@ jobs:
yarn test

- name: Release
id: semantic_release
run: |
git config --global user.email "support@dev.me"
git config --global user.name "DEV.ME Team"
npm i -g semantic-release @semantic-release/git @semantic-release/github conventional-changelog-conventionalcommits
npx semantic-release --no-ci --debug
npx semantic-release --no-ci --debug 2>&1 | tee release-output.txt

# Extract version and tag info from release output
if grep -q "Published release" release-output.txt; then
echo "release_published=true" >> $GITHUB_OUTPUT
VERSION=$(grep -oP 'Published release \K[0-9]+\.[0-9]+\.[0-9]+' release-output.txt | head -1)
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "tag=v$VERSION" >> $GITHUB_OUTPUT
else
echo "release_published=false" >> $GITHUB_OUTPUT
fi

- name: Add Release Summary
if: always()
run: |
echo "## 📦 Release Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY

if [[ "${{ steps.semantic_release.outputs.release_published }}" == "true" ]]; then
echo "### ✅ Release Published Successfully!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- **Version:** \`${{ steps.semantic_release.outputs.version }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Tag:** \`${{ steps.semantic_release.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### 🔗 Links" >> $GITHUB_STEP_SUMMARY
echo "- [NPM Package](https://www.npmjs.com/package/@devmehq/email-validator-js/v/${{ steps.semantic_release.outputs.version }})" >> $GITHUB_STEP_SUMMARY
echo "- [GitHub Release](https://github.com/${{ github.repository }}/releases/tag/${{ steps.semantic_release.outputs.tag }})" >> $GITHUB_STEP_SUMMARY
else
echo "### ℹ️ No Release Published" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "No release was created. This could be because:" >> $GITHUB_STEP_SUMMARY
echo "- No relevant commits found for release" >> $GITHUB_STEP_SUMMARY
echo "- Commits don't follow conventional commit format" >> $GITHUB_STEP_SUMMARY
echo "- Release conditions not met" >> $GITHUB_STEP_SUMMARY
fi

echo "" >> $GITHUB_STEP_SUMMARY
echo "### 📊 Build Information" >> $GITHUB_STEP_SUMMARY
echo "- **Workflow:** \`${{ github.workflow }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Run ID:** \`${{ github.run_id }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Run Number:** \`${{ github.run_number }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Actor:** \`${{ github.actor }}\`" >> $GITHUB_STEP_SUMMARY
echo "- **Event:** \`${{ github.event_name }}\`" >> $GITHUB_STEP_SUMMARY
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@

✅ **NEW:** Get domain registration status via WHOIS lookup

✅ **NEW:** Serverless support for AWS Lambda, Vercel Edge, Cloudflare Workers, and more

## Use Cases

- Increase delivery rate of email campaigns by removing spam emails
Expand Down Expand Up @@ -788,6 +790,55 @@ clearAllCaches();

**Note:** Yahoo, Hotmail, and some providers always return `validSmtp: true` as they don't allow mailbox verification.

## 🌐 Serverless Deployment

The package includes serverless adapters for major cloud platforms. The serverless implementation provides email validation without Node.js dependencies, making it suitable for edge computing environments.

### AWS Lambda

```javascript
import { apiGatewayHandler } from '@devmehq/email-validator-js/serverless/aws';

export const handler = apiGatewayHandler;
```

### Vercel Edge Functions

```javascript
import { edgeHandler } from '@devmehq/email-validator-js/serverless/vercel';

export const config = {
runtime: 'edge',
};

export default edgeHandler;
```

### Cloudflare Workers

```javascript
import { workerHandler } from '@devmehq/email-validator-js/serverless/cloudflare';

export default {
async fetch(request, env, ctx) {
return workerHandler(request, env, ctx);
},
};
```

### Features in Serverless Mode

- ✅ Syntax validation
- ✅ Typo detection and domain suggestions
- ✅ Disposable email detection (full database)
- ✅ Free email provider detection (full database)
- ✅ Batch processing
- ✅ Built-in caching
- ❌ MX record validation (requires DNS)
- ❌ SMTP verification (requires TCP sockets)

For detailed serverless documentation and more platform examples, see [docs/SERVERLESS.md](docs/SERVERLESS.md).

## 📊 Performance & Caching

The library includes intelligent caching to improve performance:
Expand Down
184 changes: 184 additions & 0 deletions __tests__/serverless-core.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/**
* Tests for serverless core functionality
*/

import { clearCache, EdgeCache, suggestDomain, validateEmailBatch, validateEmailCore } from '../src/serverless/core';

describe('Serverless Core', () => {
beforeEach(() => {
clearCache();
});

describe('EdgeCache', () => {
it('should store and retrieve values', () => {
const cache = new EdgeCache<string>(10, 1000);
cache.set('test', 'value');
expect(cache.get('test')).toBe('value');
});

it('should respect TTL', (done) => {
const cache = new EdgeCache<string>(10, 100); // 100ms TTL
cache.set('test', 'value');
expect(cache.get('test')).toBe('value');

setTimeout(() => {
expect(cache.get('test')).toBeUndefined();
done();
}, 150);
});

it('should respect max size', () => {
const cache = new EdgeCache<number>(3, 10000);
cache.set('1', 1);
cache.set('2', 2);
cache.set('3', 3);
cache.set('4', 4); // This should evict oldest entries

expect(cache.size()).toBeLessThanOrEqual(3);
});

it('should clear cache', () => {
const cache = new EdgeCache<string>(10, 1000);
cache.set('test', 'value');
expect(cache.get('test')).toBe('value');
cache.clear();
expect(cache.get('test')).toBeUndefined();
});
});

describe('validateEmailCore', () => {
it('should validate valid email syntax', async () => {
const result = await validateEmailCore('test@valid-domain.org');
expect(result.valid).toBe(true);
expect(result.email).toBe('test@valid-domain.org');
expect(result.local).toBe('test');
expect(result.domain).toBe('valid-domain.org');
expect(result.validators.syntax?.valid).toBe(true);
});

it('should invalidate invalid email syntax', async () => {
const result = await validateEmailCore('invalid-email');
expect(result.valid).toBe(false);
expect(result.validators.syntax?.valid).toBe(false);
});

it('should detect typos in domains', async () => {
const result = await validateEmailCore('user@gmial.com');
expect(result.validators.typo?.valid).toBe(false);
expect(result.validators.typo?.suggestion).toBe('gmail.com');
});

it('should detect disposable emails', async () => {
const result = await validateEmailCore('test@mailinator.com');
expect(result.validators.disposable?.valid).toBe(false);
});

it('should detect free email providers', async () => {
const result = await validateEmailCore('test@gmail.com');
expect(result.validators.free?.valid).toBe(false);
});

it('should cache results', async () => {
const email = 'cached@valid-domain.org';

// First call
const result1 = await validateEmailCore(email);

// Second call should return cached result
const result2 = await validateEmailCore(email);

expect(result1).toEqual(result2);
});

it('should skip cache when requested', async () => {
const email = 'nocache@valid-domain.org';

await validateEmailCore(email);

const result = await validateEmailCore(email, { skipCache: true });
expect(result.email).toBe(email);
});

it('should allow disabling specific validators', async () => {
const result = await validateEmailCore('test@gmail.com', {
validateTypo: false,
validateDisposable: false,
validateFree: false,
});

expect(result.validators.typo).toBeUndefined();
expect(result.validators.disposable).toBeUndefined();
expect(result.validators.free).toBeUndefined();
});
});

describe('validateEmailBatch', () => {
it('should validate multiple emails', async () => {
const emails = ['valid@valid-domain.org', 'invalid-email', 'typo@gmial.com'];

const results = await validateEmailBatch(emails);

expect(results).toHaveLength(3);
expect(results[0].valid).toBe(true);
expect(results[1].valid).toBe(false);
expect(results[2].validators.typo?.suggestion).toBe('gmail.com');
});

it('should respect batch size option', async () => {
const emails = Array(10).fill('test@valid-domain.org');

const results = await validateEmailBatch(emails, { batchSize: 3 });

expect(results).toHaveLength(10);
results.forEach((result) => {
expect(result.email).toBe('test@valid-domain.org');
});
});
});

describe('suggestDomain', () => {
it('should suggest correct domain for common typos', () => {
expect(suggestDomain('gmial.com')).toBe('gmail.com');
expect(suggestDomain('yahooo.com')).toBe('yahoo.com');
expect(suggestDomain('hotmial.com')).toBe('hotmail.com');
expect(suggestDomain('outlok.com')).toBe('outlook.com');
});

it('should return null for correct domains', () => {
expect(suggestDomain('gmail.com')).toBeNull();
expect(suggestDomain('yahoo.com')).toBeNull();
});

it('should use custom domains when provided', () => {
const suggestion = suggestDomain('compny.com', {
customDomains: ['company.com', 'business.org'],
threshold: 2,
});
expect(suggestion).toBe('company.com');
});

it('should respect threshold option', () => {
// With low threshold (strict) - use a domain with distance > 1
expect(suggestDomain('ggggmail.com', { threshold: 1 })).toBeNull();

// With higher threshold (more lenient) - same domain should match with higher threshold
expect(suggestDomain('ggggmail.com', { threshold: 3 })).toBe('gmail.com');
});
});

describe('Cache control', () => {
it('should clear all caches', async () => {
// Add some data to cache
await validateEmailCore('test1@valid-domain.org');
await validateEmailCore('test2@valid-domain.org');

// Clear cache
clearCache();

// Cache should be empty (we can't directly test this, but we can verify behavior)
// If cache was cleared, the same validation would happen again
const result = await validateEmailCore('test1@valid-domain.org');
expect(result.email).toBe('test1@valid-domain.org');
});
});
});
3 changes: 2 additions & 1 deletion biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
"noExplicitAny": "warn",
"noDebugger": "error",
"noConsole": "off",
"noVar": "error"
"noVar": "error",
"useIterableCallbackReturn": "off"
},
"complexity": {
"noBannedTypes": "warn",
Expand Down
Loading