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
5 changes: 4 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [18.x, 20.x]
node-version: [18.x, 20.x, 22.x, 23.x, 24.x]
fail-fast: false
steps:
- uses: actions/checkout@v4
Expand All @@ -29,6 +29,9 @@ jobs:
- name: Run tests
run: yarn test

- name: Run type definition tests
run: yarn test:types

linters:
name: Linters
runs-on: ubuntu-latest
Expand Down
24 changes: 18 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,31 @@ yarn add emailable

## Usage

The library needs to be configured with your account's API key which is
available in your [Emailable Dashboard](https://app.emailable.com/api). Require
it with your API key:
### Authentication

### Setup
The Emailable API requires either an API key or an access token for
authentication. API keys can be created and managed in the
[Emailable Dashboard](https://app.emailable.com/api).

An API key can be set globally for the Emailable client:

```javascript
// require with API key
var emailable = require('emailable')('live_...')
var emailable = require('emailable')('your_api_key')

// ES6 import
import Emailable from 'emailable';
const emailable = Emailable('live_...');
const emailable = Emailable('your_api_key');
```

Or, you can specify an `apiKey` or an `accessToken` with each request:

```javascript
// set api_key at request time
emailable.verify({ apiKey: 'your_api_key' })

// set access_token at request time
emailable.verify({ accessToken: 'your_api_key' })
```

### Verification
Expand Down
18 changes: 12 additions & 6 deletions lib/client.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ const axios = require('axios')

class Client {

constructor(key) {
constructor(apiKey = null) {
this.instance = axios.create({ baseURL: 'https://api.emailable.com/v1/' })
if (key) {
this.instance.defaults.headers.common['Authorization'] = `Bearer ${key}`
}
this.apiKey = apiKey
}

makeGetRequest(endpoint, params = {}) {
const { apiKey, accessToken, ...filteredParams } = params
const key = apiKey || accessToken || this.apiKey
const headers = key ? { Authorization: `Bearer ${key}` } : {}

return new Promise((resolve, reject) => {
this.instance.get(endpoint, { params: params })
this.instance.get(endpoint, { params: filteredParams, headers: headers })
.then(response => resolve(response.data))
.catch(error => {
if (error.response) {
Expand All @@ -29,8 +31,12 @@ class Client {
}

makePostRequest(endpoint, data = {}) {
const { apiKey, accessToken, ...filteredData } = data
const key = apiKey || accessToken || this.apiKey
const headers = key ? { Authorization: `Bearer ${key}` } : {}

return new Promise((resolve, reject) => {
this.instance.post(endpoint, data)
this.instance.post(endpoint, filteredData, { headers: headers })
.then(response => resolve(response.data))
.catch(error => {
reject({
Expand Down
4 changes: 2 additions & 2 deletions lib/emailable.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@ declare class Emailable {

verify(email: string, options?: {}): Promise<any>

account(): Promise<any>
account(options?: {}): Promise<any>

readonly client: Client
readonly batches: Batches
}

declare function _exports(apiKey: any): Emailable
declare function _exports(apiKey?: any): Emailable
export = _exports
6 changes: 3 additions & 3 deletions lib/emailable.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const Batches = require('./batches')

class Emailable {

constructor(apiKey) {
constructor(apiKey = null) {
this.client = new Client(apiKey)
this.batches = new Batches(this.client)
}
Expand All @@ -14,8 +14,8 @@ class Emailable {
return this.client.makePostRequest('verify', { email: email, ...options })
}

account() {
return this.client.makeGetRequest('account')
account(options = {}) {
return this.client.makeGetRequest('account', options)
}

}
Expand Down
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"main": "lib/emailable.js",
"types": "lib/emailable.d.ts",
"scripts": {
"test": "mocha --timeout 10000 && tsd",
"test": "mocha --timeout 10000",
"test:types": "tsd",
"lint": "eslint"
},
"tsd": {
Expand Down Expand Up @@ -40,5 +41,6 @@
},
"dependencies": {
"axios": "^1.6.0"
}
},
"packageManager": "yarn@1.22.21+sha1.1959a18351b811cdeedbd484a8f86c3cc3bbaf72"
}
2 changes: 1 addition & 1 deletion test/account.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe('emailable.account()', () => {
expect(response.owner_email).to.be.a('string')
expect(response.available_credits).to.be.a('number')
done()
})
}).catch(done)
})

it('should return a 401 status code when no API key', done => {
Expand Down
102 changes: 102 additions & 0 deletions test/authentication.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
'use strict'

const expect = require('chai').expect
const apiKey = 'test_7aff7fc0142c65f86a00'
const email = 'jarrett@emailable.com'
const emails = ['jarrett@emailable.com', 'support@emailable.com']

describe('authentication', () => {

it('should authenticate with a global api key configured', done => {
const emailable = require('../lib/emailable')(apiKey)

Promise.all([
emailable.verify(email).then(response => {
expect(response.domain).to.be.a('string')
}),

emailable.account().then(response => {
expect(response.owner_email).to.be.a('string')
}),

emailable.batches.verify(emails)
.then(response => {
expect(response.id).to.have.lengthOf(24)
return emailable.batches.status(response.id)
})
.then(statusResponse => {
expect(statusResponse.id).to.be.a('string')
})
])
.then(() => done())
.catch(done)
})

it('should authenticate with an api key passed in at request time', done => {
const emailable = require('../lib/emailable')()

Promise.all([
emailable.verify(email, { apiKey: apiKey }).then(response => {
expect(response.domain).to.be.a('string')
}),

emailable.account({ apiKey: apiKey }).then(response => {
expect(response.owner_email).to.be.a('string')
}),

emailable.batches.verify(emails, { apiKey: apiKey })
.then(response => {
expect(response.id).to.have.lengthOf(24)
return emailable.batches.status(response.id, { apiKey: apiKey })
})
.then(statusResponse => {
expect(statusResponse.id).to.be.a('string')
})
])
.then(() => done())
.catch(done)
})

it('should prioritize request time authentication over global', done => {
const emailable = require('../lib/emailable')('invalid_api_key')

Promise.all([
emailable.verify(email, { apiKey: apiKey }).then(response => {
expect(response.domain).to.be.a('string')
}),

emailable.account({ apiKey: apiKey }).then(response => {
expect(response.owner_email).to.be.a('string')
}),

emailable.batches.verify(emails, { apiKey: apiKey })
.then(response => {
expect(response.id).to.have.lengthOf(24)
return emailable.batches.status(response.id, { apiKey: apiKey })
})
.then(statusResponse => {
expect(statusResponse.id).to.be.a('string')
})
])
.then(() => done())
.catch(done)
})

it('should not modify the original params object passed in', done => {
const emailable = require('../lib/emailable')()
const params = { apiKey: apiKey }

Promise.all([
emailable.verify(email, params).then(() => {
expect(params.apiKey).to.equal(apiKey)
}),

emailable.account(params).then(() => {
expect(params.apiKey).to.equal(apiKey)
})
])
.then(() => done())
.catch(done)
})

})
10 changes: 5 additions & 5 deletions test/batches.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ describe('emailable.batches.verify()', () => {
emailable.batches.verify(emails).then(response => {
expect(response.id).to.have.lengthOf(24)
done()
})
}).catch(done)
})

it('should return a payment error when passed { simulate: "payment_error" }', done => {
Expand All @@ -32,8 +32,8 @@ describe('emailable.batches.status()', () => {
expect(response.reason_counts).to.be.a('object')
expect(response.message).to.be.a('string')
done()
})
})
}).catch(done)
}).catch(done)
})

it('should return verifying response when passed { simulate: "verifying" }', done => {
Expand All @@ -43,8 +43,8 @@ describe('emailable.batches.status()', () => {
expect(response.total).to.be.a('number')
expect(response.message).to.be.a('string')
done()
})
})
}).catch(done)
}).catch(done)
})

})
1 change: 1 addition & 0 deletions test/emailable.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ const emailable = Emailable('test_xxxxxxxxxx')
expectType<Promise<any>>(emailable.verify('deliverable@example.com'))
expectType<Promise<any>>(emailable.verify('deliverable@example.com', { accept_all: true }))
expectType<Promise<any>>(emailable.account())
expectType<Promise<any>>(emailable.account({ apiKey: 'test_xxxxxxxxxx' }))
expectType<Promise<any>>(emailable.batches.verify(['deliverable@example.com']))
expectType<Promise<any>>(emailable.batches.verify(['deliverable@example.com'], { simulate: 'verifying' }))
expectType<Promise<any>>(emailable.batches.status('xxxxxxxxxx'))
Expand Down
5 changes: 3 additions & 2 deletions test/verify.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ describe('emailable.verify()', () => {
expect(response.user).to.be.a('string')
expect(response.duration).to.be.a('number')
done()
})
}).catch(done)
})

it('should return a valid state', done => {
const states = ['deliverable', 'undeliverable', 'risky', 'unknown']
emailable.verify('deliverable@example.com').then(response => {
expect(states.includes(response.state)).to.be.equal(true)
done()
})
}).catch(done)
})

it('should verify an email with accept-all enabled', done => {
Expand All @@ -32,6 +32,7 @@ describe('emailable.verify()', () => {
expect(response.accept_all).to.be.equal(true)
done()
})
.catch(done)
})

})