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
2 changes: 1 addition & 1 deletion .github/workflows/dapp-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ jobs:
MJ_APIKEY_PUBLIC: ${{ secrets.MAILJET_APIKEY_PUBLIC }}
MJ_APIKEY_PRIVATE: ${{ secrets.MAILJET_APIKEY_PRIVATE }}
MJ_SENDER: ${{ secrets.MAILJET_SENDER }}
MAILGUN_APIKEY: ${{ secrets.MAILGUN_APIKEY }}
BOUNCER_APIKEY: ${{ secrets.BOUNCER_APIKEY }}
WEB3MAIL_WHITELISTED_APPS: ${{ vars.WEB3MAIL_WHITELISTED_APPS }}
POCO_SUBGRAPH_URL: ${{ vars.POCO_SUBGRAPH_URL }}
steps:
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/dapp-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ jobs:
MJ_APIKEY_PUBLIC: ${{ secrets.MAILJET_APIKEY_PUBLIC }}
MJ_APIKEY_PRIVATE: ${{ secrets.MAILJET_APIKEY_PRIVATE }}
MJ_SENDER: ${{ secrets.MAILJET_SENDER }}
MAILGUN_APIKEY: ${{ secrets.MAILGUN_APIKEY }}
BOUNCER_APIKEY: ${{ secrets.BOUNCER_APIKEY }}
WEB3MAIL_WHITELISTED_APPS: ${{ vars.WEB3MAIL_WHITELISTED_APPS }}
POCO_SUBGRAPH_URL: ${{ vars.POCO_SUBGRAPH_URL }}
RPC_URL: ${{ secrets.RPC_URL }}
Expand Down
2 changes: 1 addition & 1 deletion dapp/.env.override
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
IEXEC_IN=/path/to/input/directory
IEXEC_OUT=/path/to/output/directory
IEXEC_DATASET_FILENAME="protectedData.zip"
IEXEC_APP_DEVELOPER_SECRET='{"MJ_APIKEY_PUBLIC":"dev_mailjet_public_key","MJ_APIKEY_PRIVATE":"dev_mailjet_private_key","MJ_SENDER":"dev_sender_email","MAILGUN_APIKEY":"dev_mailgun_api_key"}'
IEXEC_APP_DEVELOPER_SECRET='{"MJ_APIKEY_PUBLIC":"dev_mailjet_public_key","MJ_APIKEY_PRIVATE":"dev_mailjet_private_key","MJ_SENDER":"dev_sender_email","BOUNCER_APIKEY":"dev_bouncer_api_key"}'
IEXEC_REQUESTER_SECRET_1='{"emailSubject":"mail_object_to_send","emailContentMultiAddr":"encrypted_email_content_multiAddr","senderName":"sender_name","emailContentEncryptionKey":"email_content_encryption_key","contentType":"text/plain"}'
6 changes: 3 additions & 3 deletions dapp/local-test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ DATA_FILENAME="your_file_name.zip"
MJ_APIKEY_PUBLIC="your_mail_jet_api_key_public"
MJ_APIKEY_PRIVATE="your_mail_jet_api_private"
MJ_SENDER="your_mail_jet_sender"
# Replace the value of the variable MAILGUN_APIKEY with your own Mailgun API key
MAILGUN_APIKEY="your_mailgun_api_key"
# Replace the value of the variable BOUNCER_APIKEY with your own Bouncer API key
BOUNCER_APIKEY="your_bouncer_api_key"


# Replace the following variables with your own values:
Expand All @@ -34,7 +34,7 @@ SENDER_NAME="the_sender_name"
CONTENT_TYPE="text/plain"

IEXEC_REQUESTER_SECRET_1='{"emailSubject":"'${EMAIL_SUBJECT}'","emailContentEncryptionKey":"'${EMAIL_CONTENT_ENCRYPTION_KEY}'","emailContentMultiAddr":"'${EMAIL_CONTENT_MULTIADDR}'","senderName":"'${SENDER_NAME}'","contentType":"'${CONTENT_TYPE}'"}'
IEXEC_APP_DEVELOPER_SECRET='{"MJ_APIKEY_PUBLIC":"'$MJ_APIKEY_PUBLIC'","MJ_APIKEY_PRIVATE":"'$MJ_APIKEY_PRIVATE'","MJ_SENDER":"'$MJ_SENDER'","MAILGUN_APIKEY":"'$MAILGUN_APIKEY'"}'
IEXEC_APP_DEVELOPER_SECRET='{"MJ_APIKEY_PUBLIC":"'$MJ_APIKEY_PUBLIC'","MJ_APIKEY_PRIVATE":"'$MJ_APIKEY_PRIVATE'","MJ_SENDER":"'$MJ_SENDER'","BOUNCER_APIKEY":"'$BOUNCER_APIKEY'"}'

docker run -it --rm \
-v /tmp/iexec_in:/iexec_in \
Expand Down
9 changes: 4 additions & 5 deletions dapp/src/executeTask.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,15 @@
pocoSubgraphUrl: appDeveloperSecret.POCO_SUBGRAPH_URL,
});

// Step 2: If not, try Mailgun
// Step 2: If not, try Bouncer
if (result.isEmailValid === undefined) {
console.log('No prior verification found. Trying Mailgun...');
console.log('No prior verification found. Trying Bouncer...');

Check warning on line 64 in dapp/src/executeTask.js

View workflow job for this annotation

GitHub Actions / check-code

Unexpected console statement
result.isEmailValid = await validateEmailAddress({
emailAddress: protectedData.email,
mailgunApiKey: appDeveloperSecret.MAILGUN_APIKEY,
bouncerApiKey: appDeveloperSecret.BOUNCER_APIKEY,
});
} else {
console.log('Email already verified, skipping Mailgun check.');
console.log('Email already verified, skipping Bouncer check.');

Check warning on line 70 in dapp/src/executeTask.js

View workflow job for this annotation

GitHub Actions / check-code

Unexpected console statement
}

if (result.isEmailValid === false) {
Expand All @@ -89,7 +89,6 @@
mailJetApiKeyPublic: appDeveloperSecret.MJ_APIKEY_PUBLIC,
mailJetApiKeyPrivate: appDeveloperSecret.MJ_APIKEY_PRIVATE,
mailJetSender: appDeveloperSecret.MJ_SENDER,
mailgunApiKey: appDeveloperSecret.MAILGUN_APIKEY,
emailContent: requesterEmailContent,
emailSubject: requesterSecret.emailSubject,
contentType: requesterSecret.contentType,
Expand All @@ -100,11 +99,11 @@
result.success = false;
result.error = e.message;
}
console.log(`Protected data ${index} processed:`, result);

Check warning on line 102 in dapp/src/executeTask.js

View workflow job for this annotation

GitHub Actions / check-code

Unexpected console statement
return result;
}

async function start() {

Check warning on line 106 in dapp/src/executeTask.js

View workflow job for this annotation

GitHub Actions / check-code

Refactor this function to reduce its Cognitive Complexity from 17 to the 15 allowed
const {
IEXEC_IN,
IEXEC_OUT,
Expand Down Expand Up @@ -144,7 +143,7 @@

// Process multiple protected data
if (bulkSize > 0) {
console.log(`Processing ${bulkSize} protected data...`);

Check warning on line 146 in dapp/src/executeTask.js

View workflow job for this annotation

GitHub Actions / check-code

Unexpected console statement
const results = [];
// Process each protected data one by one to avoid rate limiting issues
for (let index = 1; index <= bulkSize; index += 1) {
Expand Down Expand Up @@ -178,7 +177,7 @@
})),
};
} else {
console.log('Processing single protected data...');

Check warning on line 180 in dapp/src/executeTask.js

View workflow job for this annotation

GitHub Actions / check-code

Unexpected console statement
const { protectedData, success, error, isEmailValid } =
await processProtectedData({
index: 0,
Expand All @@ -205,11 +204,11 @@
}
}
} catch (e) {
console.error('Something went wrong:', e.message);

Check warning on line 207 in dapp/src/executeTask.js

View workflow job for this annotation

GitHub Actions / check-code

Unexpected console statement
result = { success: false, error: e.message };
}

console.log('Writing results:', JSON.stringify(result));

Check warning on line 211 in dapp/src/executeTask.js

View workflow job for this annotation

GitHub Actions / check-code

Unexpected console statement
await fs.writeFile(
`${workerEnv.IEXEC_OUT}/result.json`,
JSON.stringify(result, null, 2)
Expand Down
28 changes: 20 additions & 8 deletions dapp/src/validateEmailAddress.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,42 @@
const fetch = require('node-fetch');

async function validateEmailAddress({ emailAddress, mailgunApiKey }) {
const basicAuth = Buffer.from(`api:${mailgunApiKey}`).toString('base64');

async function validateEmailAddress({ emailAddress, bouncerApiKey }) {
try {
const response = await fetch(
`https://api.mailgun.net/v4/address/validate?address=${encodeURIComponent(
`https://api.usebouncer.com/v1.1/email/verify?email=${encodeURIComponent(
emailAddress
)}`,
{
method: 'GET',
headers: {
Authorization: `Basic ${basicAuth}`,
'x-api-key': bouncerApiKey,
},
}
);

if (!response.ok) {
console.warn('Mailgun API did not respond properly (non-2xx). Skipping.');
console.warn(
`Bouncer API did not respond properly (status ${response.status}). Skipping.`
);
return undefined;
}

const json = await response.json();
return json.result === 'deliverable';
switch (json.status) {
case 'deliverable':
return true;
case 'undeliverable':
case 'risky':
return false;
default:
// "unknown" (timeout, dns_error, unavailable_smtp...) is inconclusive
console.warn(
`Bouncer API returned an inconclusive status (${json.status}, reason: ${json.reason}). Skipping.`
);
return undefined;
}
} catch (e) {
console.warn('Mailgun API request failed:', e.message);
console.warn('Bouncer API request failed:', e.message);
return undefined;
}
}
Expand Down
2 changes: 1 addition & 1 deletion dapp/src/validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const appSecretSchema = Joi.object({
MJ_APIKEY_PUBLIC: Joi.string().required(),
MJ_APIKEY_PRIVATE: Joi.string().required(),
MJ_SENDER: Joi.string().email().required(),
MAILGUN_APIKEY: Joi.string().required(),
BOUNCER_APIKEY: Joi.string().required(),
WEB3MAIL_WHITELISTED_APPS: Joi.array().items(Joi.string()).required(),
POCO_SUBGRAPH_URL: Joi.string().uri().required(),
});
Expand Down
42 changes: 21 additions & 21 deletions dapp/tests/e2e/app.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ describe('sendEmail', () => {
MJ_APIKEY_PUBLIC: 'xxx',
MJ_APIKEY_PRIVATE: 'xxx',
MJ_SENDER: 'foo@bar.com',
MAILGUN_APIKEY: 'xxx',
BOUNCER_APIKEY: 'xxx',
WEB3MAIL_WHITELISTED_APPS:
'["0xa638bf4665ce7bd7021a4a12416ea7a0a3272b6f"]',
POCO_SUBGRAPH_URL: 'https://fake-poco.subgraph.iex.ec',
Expand Down Expand Up @@ -84,7 +84,7 @@ describe('sendEmail', () => {
process.env.IEXEC_APP_DEVELOPER_SECRET = JSON.stringify({
MJ_APIKEY_PRIVATE: 'xxx',
MJ_SENDER: 'foo@bar.com',
MAILGUN_APIKEY: 'xxx',
BOUNCER_APIKEY: 'xxx',
WEB3MAIL_WHITELISTED_APPS:
'["0xa638bf4665ce7bd7021a4a12416ea7a0a3272b6f"]',
POCO_SUBGRAPH_URL: 'https://fake-poco.subgraph.iex.ec',
Expand All @@ -108,7 +108,7 @@ describe('sendEmail', () => {
process.env.IEXEC_APP_DEVELOPER_SECRET = JSON.stringify({
MJ_APIKEY_PUBLIC: 'xxx',
MJ_SENDER: 'foo@bar.com',
MAILGUN_APIKEY: 'xxx',
BOUNCER_APIKEY: 'xxx',
WEB3MAIL_WHITELISTED_APPS:
'["0xa638bf4665ce7bd7021a4a12416ea7a0a3272b6f"]',
POCO_SUBGRAPH_URL: 'https://fake-poco.subgraph.iex.ec',
Expand All @@ -132,7 +132,7 @@ describe('sendEmail', () => {
process.env.IEXEC_APP_DEVELOPER_SECRET = JSON.stringify({
MJ_APIKEY_PUBLIC: 'xxx',
MJ_APIKEY_PRIVATE: 'xxx',
MAILGUN_APIKEY: 'xxx',
BOUNCER_APIKEY: 'xxx',
WEB3MAIL_WHITELISTED_APPS:
'["0xa638bf4665ce7bd7021a4a12416ea7a0a3272b6f"]',
POCO_SUBGRAPH_URL: 'https://fake-poco.subgraph.iex.ec',
Expand All @@ -152,7 +152,7 @@ describe('sendEmail', () => {
});
expect(files.length).toBe(2);
});
it('should fail if MAILGUN_APIKEY in developer secret is missing', async () => {
it('should fail if BOUNCER_APIKEY in developer secret is missing', async () => {
process.env.IEXEC_APP_DEVELOPER_SECRET = JSON.stringify({
MJ_APIKEY_PUBLIC: 'xxx',
MJ_APIKEY_PRIVATE: 'xxx',
Expand All @@ -169,7 +169,7 @@ describe('sendEmail', () => {
);
expect(result).toStrictEqual({
success: false,
error: 'App secret error: "MAILGUN_APIKEY" is required',
error: 'App secret error: "BOUNCER_APIKEY" is required',
});
expect(computed).toStrictEqual({
'deterministic-output-path': `${process.env.IEXEC_OUT}/result.json`,
Expand All @@ -180,7 +180,7 @@ describe('sendEmail', () => {
process.env.IEXEC_APP_DEVELOPER_SECRET = JSON.stringify({
MJ_APIKEY_PUBLIC: 'xxx',
MJ_APIKEY_PRIVATE: 'xxx',
MAILGUN_APIKEY: 'xxx',
BOUNCER_APIKEY: 'xxx',
MJ_SENDER: 'foo@bar.com',
POCO_SUBGRAPH_URL: 'https://fake-poco.subgraph.iex.ec',
});
Expand All @@ -203,7 +203,7 @@ describe('sendEmail', () => {
process.env.IEXEC_APP_DEVELOPER_SECRET = JSON.stringify({
MJ_APIKEY_PUBLIC: 'xxx',
MJ_APIKEY_PRIVATE: 'xxx',
MAILGUN_APIKEY: 'xxx',
BOUNCER_APIKEY: 'xxx',
MJ_SENDER: 'foo@bar.com',
WEB3MAIL_WHITELISTED_APPS:
'["0xa638bf4665ce7bd7021a4a12416ea7a0a3272b6f"]',
Expand Down Expand Up @@ -460,21 +460,21 @@ describe('sendEmail', () => {
});
});

describe('when mailgun is unreachable', () => {
it('should skip mailgun address validation', async () => {
describe('when bouncer is unreachable', () => {
it('should skip bouncer address validation', async () => {
// this data does not pass validation
process.env.IEXEC_DATASET_FILENAME = 'dataEmailUserDoesNotExist.zip';
// mailgun api key is fake
// bouncer api key is fake
process.env.IEXEC_APP_DEVELOPER_SECRET = JSON.stringify({
MJ_APIKEY_PUBLIC: 'xxx',
MJ_APIKEY_PRIVATE: 'xxx',
MJ_SENDER: 'foo@bar.com',
MAILGUN_APIKEY: 'fake',
BOUNCER_APIKEY: 'fake',
WEB3MAIL_WHITELISTED_APPS:
'["0xa638bf4665ce7bd7021a4a12416ea7a0a3272b6f"]',
POCO_SUBGRAPH_URL: 'https://fake-poco.subgraph.iex.ec',
});
// check the error is not an email validation error (mailgun validation is skipped, mailjet rejects)
// check the error is not an email validation error (bouncer validation is skipped, mailjet rejects)
await expect(start()).resolves.toBeUndefined();

const { result, computed, files } = await readOutputs(
Expand All @@ -498,7 +498,7 @@ describe('sendEmail', () => {
MJ_APIKEY_PUBLIC: 'xxx',
MJ_APIKEY_PRIVATE: 'xxx',
MJ_SENDER: 'foo@bar.com',
MAILGUN_APIKEY: 'xxx',
BOUNCER_APIKEY: 'xxx',
WEB3MAIL_WHITELISTED_APPS:
'["0xa638bf4665ce7bd7021a4a12416ea7a0a3272b6f"]',
POCO_SUBGRAPH_URL: 'https://fake-poco.subgraph.iex.ec',
Expand Down Expand Up @@ -529,7 +529,7 @@ describe('sendEmail', () => {
(process.env.MJ_APIKEY_PUBLIC &&
process.env.MJ_APIKEY_PRIVATE &&
process.env.MJ_SENDER &&
process.env.MAILGUN_APIKEY &&
process.env.BOUNCER_APIKEY &&
process.env.POCO_SUBGRAPH_URL)
) {
describe('with credentials', () => {
Expand All @@ -539,15 +539,15 @@ describe('sendEmail', () => {
MJ_APIKEY_PUBLIC,
MJ_APIKEY_PRIVATE,
MJ_SENDER,
MAILGUN_APIKEY,
BOUNCER_APIKEY,
WEB3MAIL_WHITELISTED_APPS,
POCO_SUBGRAPH_URL,
} = process.env;
process.env.IEXEC_APP_DEVELOPER_SECRET = JSON.stringify({
MJ_APIKEY_PUBLIC,
MJ_APIKEY_PRIVATE,
MJ_SENDER,
MAILGUN_APIKEY,
BOUNCER_APIKEY,
WEB3MAIL_WHITELISTED_APPS,
POCO_SUBGRAPH_URL,
});
Expand Down Expand Up @@ -695,7 +695,7 @@ describe('sendEmail', () => {
MJ_APIKEY_PUBLIC: 'xxx',
MJ_APIKEY_PRIVATE: 'xxx',
MJ_SENDER: 'foo@bar.com',
MAILGUN_APIKEY: 'xxx',
BOUNCER_APIKEY: 'xxx',
WEB3MAIL_WHITELISTED_APPS:
'["0xa638bf4665ce7bd7021a4a12416ea7a0a3272b6f"]',
POCO_SUBGRAPH_URL: 'https://fake-poco.subgraph.iex.ec',
Expand Down Expand Up @@ -832,7 +832,7 @@ describe('sendEmail', () => {
(process.env.MJ_APIKEY_PUBLIC &&
process.env.MJ_APIKEY_PRIVATE &&
process.env.MJ_SENDER &&
process.env.MAILGUN_APIKEY &&
process.env.BOUNCER_APIKEY &&
process.env.POCO_SUBGRAPH_URL)
) {
describe('with credentials', () => {
Expand All @@ -842,15 +842,15 @@ describe('sendEmail', () => {
MJ_APIKEY_PUBLIC,
MJ_APIKEY_PRIVATE,
MJ_SENDER,
MAILGUN_APIKEY,
BOUNCER_APIKEY,
WEB3MAIL_WHITELISTED_APPS,
POCO_SUBGRAPH_URL,
} = process.env;
process.env.IEXEC_APP_DEVELOPER_SECRET = JSON.stringify({
MJ_APIKEY_PUBLIC,
MJ_APIKEY_PRIVATE,
MJ_SENDER,
MAILGUN_APIKEY,
BOUNCER_APIKEY,
WEB3MAIL_WHITELISTED_APPS,
POCO_SUBGRAPH_URL,
});
Expand Down
83 changes: 83 additions & 0 deletions dapp/tests/unit/validateEmailAddress.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
jest.mock('node-fetch');
const fetch = require('node-fetch');
const { validateEmailAddress } = require('../../src/validateEmailAddress');

function mockBouncerResponse({ ok = true, status = 200, json = {} }) {
fetch.mockResolvedValue({
ok,
status,
json: () => Promise.resolve(json),
});
}

describe('validateEmailAddress', () => {
beforeEach(() => {
fetch.mockReset();
jest.spyOn(console, 'warn').mockImplementation(() => {});
});

it('calls Bouncer verify endpoint with the api key', async () => {
mockBouncerResponse({ json: { status: 'deliverable' } });

await validateEmailAddress({
emailAddress: 'john+test@example.com',
bouncerApiKey: 'my-api-key',
});

expect(fetch).toHaveBeenCalledWith(
'https://api.usebouncer.com/v1.1/email/verify?email=john%2Btest%40example.com',
{ method: 'GET', headers: { 'x-api-key': 'my-api-key' } }
);
});

it('returns true when status is "deliverable"', async () => {
mockBouncerResponse({
json: { status: 'deliverable', reason: 'accepted_email' },
});
await expect(
validateEmailAddress({ emailAddress: 'a@b.com', bouncerApiKey: 'k' })
).resolves.toBe(true);
});

it('returns false when status is "undeliverable"', async () => {
mockBouncerResponse({
json: { status: 'undeliverable', reason: 'rejected_email' },
});
await expect(
validateEmailAddress({ emailAddress: 'a@b.com', bouncerApiKey: 'k' })
).resolves.toBe(false);
});

it('returns false when status is "risky"', async () => {
mockBouncerResponse({
json: { status: 'risky', reason: 'low_quality' },
});
await expect(
validateEmailAddress({ emailAddress: 'a@b.com', bouncerApiKey: 'k' })
).resolves.toBe(false);
});

it('returns undefined when status is "unknown"', async () => {
mockBouncerResponse({ json: { status: 'unknown', reason: 'timeout' } });
await expect(
validateEmailAddress({ emailAddress: 'a@b.com', bouncerApiKey: 'k' })
).resolves.toBeUndefined();
});

it.each([401, 402, 429, 500])(
'returns undefined when Bouncer answers with HTTP %i',
async (status) => {
mockBouncerResponse({ ok: false, status });
await expect(
validateEmailAddress({ emailAddress: 'a@b.com', bouncerApiKey: 'k' })
).resolves.toBeUndefined();
}
);

it('returns undefined when the request fails', async () => {
fetch.mockRejectedValue(new Error('network error'));
await expect(
validateEmailAddress({ emailAddress: 'a@b.com', bouncerApiKey: 'k' })
).resolves.toBeUndefined();
});
});
Loading
Loading