mailosaur
Version:
The Mailosaur Node library lets you integrate email and SMS testing into your continuous integration process.
533 lines (461 loc) • 19.4 kB
JavaScript
const fs = require('fs');
const path = require('path');
const { assert } = require('chai');
const MailosaurClient = require('../lib/mailosaur');
const MailosaurError = require('../lib/models/mailosaurError');
const mailer = require('./mailer');
const isoDateString = new Date().toISOString().slice(0, 10);
const validateHtml = (email) => {
// Body
assert.match(email.html.body, /^<div dir="ltr">/, 'HTML body should match');
// Links
assert.equal(email.html.links.length, 3, 'Should have HTML links');
assert.equal(email.html.links[0].href, 'https://mailosaur.com/', 'First link should have href');
assert.equal(email.html.links[0].text, 'mailosaur', 'First link should have text');
assert.equal(email.html.links[1].href, 'https://mailosaur.com/', 'Second link should have href');
assert.isNull(email.html.links[1].text, 'Second link should have no text');
assert.equal(email.html.links[2].href, 'http://invalid/', 'Third link should have href');
assert.equal(email.html.links[2].text, 'invalid', 'Third link should have text');
// Codes
assert.equal(email.html.codes.length, 2, 'Should have verification codes');
assert.equal(email.html.codes[0].value, '123456');
assert.equal(email.html.codes[1].value, 'G3H1Y2');
// Images
assert.match(email.html.images[1].src, /cid:/);
assert.equal(email.html.images[1].alt, 'Inline image 1', 'Second image should have alt text');
};
const validateText = (email) => {
// Body
assert.match(email.text.body, /^this is a test/);
// Links
assert.equal(email.text.links.length, 2, 'Should have Text links');
assert.equal(email.text.links[0].href, 'https://mailosaur.com/', 'First link should have href');
assert.equal(email.text.links[0].text, email.text.links[0].href, 'First text link href & text should match');
assert.equal(email.text.links[1].href, 'https://mailosaur.com/', 'Second link should have href');
assert.equal(email.text.links[1].text, email.text.links[1].href, 'Second text link href & text should match');
// Codes
assert.equal(email.text.codes.length, 2, 'Should have verification codes');
assert.equal(email.text.codes[0].value, '654321');
assert.equal(email.text.codes[1].value, '5H0Y2');
};
const validateHeaders = (email) => {
const expectedFromHeader = `${email.from[0].name} <${email.from[0].email}>`;
const expectedToHeader = `${email.to[0].name} <${email.to[0].email}>`;
const { headers } = email.metadata;
assert.equal(headers.find(h => h.field.toLowerCase() === 'from').value, expectedFromHeader, 'From header should be accurate');
assert.equal(headers.find(h => h.field.toLowerCase() === 'to').value, expectedToHeader, 'To header should be accurate');
assert.equal(headers.find(h => h.field.toLowerCase() === 'subject').value, email.subject, 'Subject header should be accurate');
};
const validateMetadata = (email) => {
assert.equal(email.type, 'Email');
assert.equal(email.from.length, 1);
assert.equal(email.to.length, 1);
assert.isNotEmpty(email.from[0].email);
assert.isNotEmpty(email.from[0].name);
assert.isNotEmpty(email.to[0].email);
assert.isNotEmpty(email.to[0].name);
assert.isNotEmpty(email.subject);
assert.isNotEmpty(email.server);
assert.equal(email.received.toISOString().slice(0, 10), isoDateString);
};
const validateAttachments = (email) => {
assert.equal(email.attachments.length, 2, 'Should have attachments');
const file1 = email.attachments[0];
assert.isOk(file1.id, 'First attachment should have file id');
assert.isOk(file1.url);
assert.equal(file1.length, 82138, 'First attachment should be correct size');
assert.equal(file1.fileName, 'cat.png', 'First attachment should have filename');
assert.equal(file1.contentType, 'image/png', 'First attachment should have correct MIME type');
const file2 = email.attachments[1];
assert.isOk(file2.id, 'Second attachment should have file id');
assert.isOk(file2.url);
assert.equal(file2.length, 212080, 'Second attachment should be correct size');
assert.equal(file2.fileName, 'dog.png', 'Second attachment should have filename');
assert.equal(file2.contentType, 'image/png', 'Second attachment should have correct MIME type');
};
const validateEmail = (email) => {
validateMetadata(email);
validateAttachments(email);
validateHtml(email);
validateText(email);
assert.isOk(email.metadata.ehlo, 'ehlo is empty');
assert.isOk(email.metadata.mailFrom, 'mailFrom is empty');
assert.equal(email.metadata.rcptTo.length, 1);
};
const validateEmailSummary = (email) => {
validateMetadata(email);
assert.isNotEmpty(email.summary);
assert.equal(email.attachments, 2);
};
describe('emails', () => {
const apiKey = process.env.MAILOSAUR_API_KEY;
const server = process.env.MAILOSAUR_SERVER;
const baseUrl = process.env.MAILOSAUR_BASE_URL || 'https://mailosaur.com/';
const verifiedDomain = process.env.MAILOSAUR_VERIFIED_DOMAIN;
let client;
let emails;
before(async () => {
if (!apiKey || !server) {
throw new Error('Missing necessary environment variables - refer to README.md');
}
client = new MailosaurClient(apiKey, baseUrl);
await client.messages.deleteAll(server);
await mailer.sendEmails(mailer, client, server, 5);
const result = await client.messages.list(server);
emails = result.items;
emails.forEach(validateEmailSummary);
});
describe('list', () => {
it('should filter on older received after date', async () => {
const pastDate = new Date();
pastDate.setMinutes(pastDate.getMinutes() - 10);
const result = await client.messages.list(server, { receivedAfter: pastDate });
assert.isTrue(result.items.length > 0);
});
it('should filter on received after date', async () => {
const d = new Date();
d.setSeconds(d.getSeconds() + 60);
const result = await client.messages.list(server, { receivedAfter: d });
assert.equal(result.items.length, 0);
});
});
describe('get', () => {
it('should return a match once found', async () => {
const host = process.env.MAILOSAUR_SMTP_HOST || 'mailosaur.net';
const testEmailAddress = `wait_for_test {server}.${host}`;
await mailer.sendEmail(client, server, testEmailAddress);
const email = await client.messages.get(server, {
sentTo: testEmailAddress
});
validateEmail(email);
});
});
describe('getById', async () => {
it('should return a single email', async () => {
const email = await client.messages.getById(emails[0].id);
validateEmail(email);
validateHeaders(email);
});
it('should throw an error if email not found', async () => {
try {
await client.messages.getById('efe907e9-74ed-4113-a3e0-a3d41d914765');
} catch (err) {
assert.instanceOf(err, MailosaurError);
}
});
});
describe('search', () => {
it('should throw an error if no criteria', async () => {
try {
await client.messages.search(server, {});
} catch (err) {
assert.instanceOf(err, MailosaurError);
}
});
it('should throw a meaningful error if the timeout is reached', async () => {
const testFromEmail = 'zzyy@test.com';
try {
await client.messages.search(server, {
sentFrom: testFromEmail
}, {
timeout: 1,
});
} catch (err) {
assert.instanceOf(err, MailosaurError);
assert.equal(err.message, `No matching messages found in time. By default, only messages received in the last hour are checked (use receivedAfter to override this). The search criteria used for this query was [{"sentFrom":"${testFromEmail}"}] which timed out after 1ms`);
}
});
it('should return empty array if errors suppressed', async () => {
const result = await client.messages.search(server, {
sentTo: 'neverfound@example.com'
}, {
timeout: 1,
errorOnTimeout: false
});
assert.equal(result.items.length, 0);
});
describe('by sentFrom', () => {
it('should return matching results', async () => {
const targetEmail = emails[1];
const result = await client.messages.search(server, {
sentFrom: targetEmail.from[0].email
});
assert.equal(result.items.length, 1);
assert.equal(result.items[0].from[0].email, targetEmail.from[0].email);
assert.equal(result.items[0].subject, targetEmail.subject);
});
});
describe('by sentTo', () => {
it('should return matching results', async () => {
const targetEmail = emails[1];
const result = await client.messages.search(server, {
sentTo: targetEmail.to[0].email
});
assert.equal(result.items.length, 1);
assert.equal(result.items[0].to[0].email, targetEmail.to[0].email);
assert.equal(result.items[0].subject, targetEmail.subject);
});
});
describe('by body', () => {
it('should return matching results', async () => {
const targetEmail = emails[1];
const uniqueString = targetEmail.subject.substr(0, targetEmail.subject.indexOf(' subject'));
const result = await client.messages.search(server, {
body: `${uniqueString} html`
});
assert.equal(result.items.length, 1);
assert.equal(result.items[0].to[0].email, targetEmail.to[0].email);
assert.equal(result.items[0].subject, targetEmail.subject);
});
});
describe('by subject', () => {
it('should return matching results', async () => {
const targetEmail = emails[1];
const uniqueString = targetEmail.subject.substr(0, targetEmail.subject.indexOf(' subject'));
const result = await client.messages.search(server, {
subject: uniqueString
});
assert.equal(result.items.length, 1);
assert.equal(result.items[0].to[0].email, targetEmail.to[0].email);
assert.equal(result.items[0].subject, targetEmail.subject);
});
});
describe('with match all', () => {
it('should return matching results', async () => {
const targetEmail = emails[1];
const uniqueString = targetEmail.subject.substr(0, targetEmail.subject.indexOf(' subject'));
const result = await client.messages.search(server, {
subject: uniqueString,
body: 'this is a link',
match: 'ALL'
});
assert.equal(result.items.length, 1);
});
});
describe('with match any', () => {
it('should return matching results', async () => {
const targetEmail = emails[1];
const uniqueString = targetEmail.subject.substr(0, targetEmail.subject.indexOf(' subject'));
const result = await client.messages.search(server, {
subject: uniqueString,
body: 'this is a link',
match: 'ANY'
});
assert.equal(result.items.length, 6);
});
});
describe('with special characters', () => {
it('should support special characters', async () => {
const result = await client.messages.search(server, {
subject: 'Search with ellipsis … and emoji 👨🏿🚒'
});
assert.equal(result.items.length, 0);
});
});
});
describe('spamAnalysis', () => {
it('should perform a spam analysis on an email', async () => {
const targetId = emails[0].id;
const result = await client.analysis.spam(targetId);
result.spamFilterResults.spamAssassin.forEach((rule) => {
assert.isNumber(rule.score);
assert.isOk(rule.rule);
assert.isOk(rule.description);
});
});
});
describe('deliverabilityReport', () => {
it('should perform a deliverability report on an email', async () => {
const targetId = emails[0].id;
const result = await client.analysis.deliverability(targetId);
assert.isOk(result.spf);
assert.isOk(result.dkim);
result.dkim.forEach((dkim) => {
assert.isOk(dkim);
});
assert.isOk(result.dmarc);
assert.isOk(result.blockLists);
result.blockLists.forEach((blockList) => {
assert.isOk(blockList);
assert.isOk(blockList.id);
assert.isOk(blockList.name);
});
assert.isOk(result.content);
assert.isOk(result.dnsRecords);
assert.isOk(result.dnsRecords.a);
assert.isOk(result.dnsRecords.mx);
assert.isOk(result.dnsRecords.ptr);
assert.isOk(result.spamAssassin);
result.spamAssassin.rules.forEach((rule) => {
assert.isNumber(rule.score);
assert.isOk(rule.rule);
assert.isOk(rule.description);
});
});
});
describe('del', () => {
it('should delete an email', async () => {
const targetEmailId = emails[4].id;
await client.messages.del(targetEmailId);
});
it('should fail if attempting to delete again', async () => {
const targetEmailId = emails[4].id;
try {
await client.messages.del(targetEmailId);
} catch (err) {
assert.instanceOf(err, MailosaurError);
}
});
});
(verifiedDomain ? describe : describe.skip)('create and send', () => {
it('send with text content', async () => {
const subject = 'New message';
const message = await client.messages.create(server, {
to: `anything {verifiedDomain}`,
send: true,
subject,
text: 'This is a new email'
});
assert.isNotEmpty(message.id);
assert.equal(message.subject, subject);
});
it('send with HTML content', async () => {
const subject = 'New HTML message';
const message = await client.messages.create(server, {
to: `anything {verifiedDomain}`,
send: true,
subject,
html: '<p>This is a new email.</p>'
});
assert.isNotEmpty(message.id);
assert.equal(message.subject, subject);
});
it('send with HTML content to CC recipient', async () => {
const subject = 'CC Message';
const ccRecipient = `someoneelse {verifiedDomain}`;
const message = await client.messages.create(server, {
to: `anything {verifiedDomain}`,
send: true,
subject,
cc: ccRecipient,
html: '<p>This is a new email.</p>'
});
assert.isNotEmpty(message.id);
assert.equal(message.subject, subject);
assert.equal(message.cc.length, 1);
assert.equal(message.cc[0].email, ccRecipient);
});
it('send with attachment', async () => {
const subject = 'New message with attachment';
const buffer = fs.readFileSync(path.join(__dirname, '/resources/cat.png'));
const attachment = {
fileName: 'cat.png',
content: buffer.toString('base64'),
contentType: 'image/png'
};
const message = await client.messages.create(server, {
to: `anything {verifiedDomain}`,
send: true,
subject,
html: '<p>This is a new email.</p>',
attachments: [attachment]
});
assert.equal(message.attachments.length, 1, 'Should have attachment');
const file1 = message.attachments[0];
assert.isOk(file1.id, 'First attachment should have file id');
assert.isOk(file1.url);
assert.equal(file1.length, 82138, 'First attachment should be correct size');
assert.equal(file1.fileName, 'cat.png', 'First attachment should have filename');
assert.equal(file1.contentType, 'image/png', 'First attachment should have correct MIME type');
});
});
(verifiedDomain ? describe : describe.skip)('forward', () => {
it('forward with text content', async () => {
const targetEmailId = emails[0].id;
const body = 'Forwarded message';
const message = await client.messages.forward(targetEmailId, {
to: `anything {verifiedDomain}`,
text: body
});
assert.isNotEmpty(message.id);
assert.isTrue(message.text.body.indexOf(body) >= 0);
});
it('forward with HTML content', async () => {
const targetEmailId = emails[0].id;
const body = '<p>Forwarded <strong>HTML</strong> message.</p>';
const message = await client.messages.forward(targetEmailId, {
to: `anything {verifiedDomain}`,
html: body
});
assert.isNotEmpty(message.id);
assert.isTrue(message.html.body.indexOf(body) >= 0);
});
it('forward with HTML content to CC recipient', async () => {
const targetEmailId = emails[0].id;
const body = '<p>Forwarded <strong>HTML</strong> message.</p>';
const ccRecipient = `someoneelse {verifiedDomain}`;
const message = await client.messages.forward(targetEmailId, {
to: `anything {verifiedDomain}`,
html: body,
cc: ccRecipient
});
assert.isNotEmpty(message.id);
assert.isTrue(message.html.body.indexOf(body) >= 0);
assert.equal(message.cc.length, 1);
assert.equal(message.cc[0].email, ccRecipient);
});
});
(verifiedDomain ? describe : describe.skip)('reply', () => {
it('reply with text content', async () => {
const targetEmailId = emails[0].id;
const body = 'Reply message';
const message = await client.messages.reply(targetEmailId, {
text: body
});
assert.isNotEmpty(message.id);
assert.isTrue(message.text.body.indexOf(body) >= 0);
});
it('reply with HTML content', async () => {
const targetEmailId = emails[0].id;
const body = '<p>Reply <strong>HTML</strong> message.</p>';
const message = await client.messages.reply(targetEmailId, {
html: body
});
assert.isNotEmpty(message.id);
assert.isTrue(message.html.body.indexOf(body) >= 0);
});
it('reply with HTML content to CC recipient', async () => {
const targetEmailId = emails[0].id;
const body = '<p>Reply <strong>HTML</strong> message.</p>';
const ccRecipient = `someoneelse {verifiedDomain}`;
const message = await client.messages.reply(targetEmailId, {
html: body,
cc: ccRecipient
});
assert.isNotEmpty(message.id);
assert.isTrue(message.html.body.indexOf(body) >= 0);
assert.equal(message.cc.length, 1);
assert.equal(message.cc[0].email, ccRecipient);
});
it('reply with attachment', async () => {
const targetEmailId = emails[0].id;
const buffer = fs.readFileSync(path.join(__dirname, '/resources/cat.png'));
const attachment = {
fileName: 'cat.png',
content: buffer.toString('base64'),
contentType: 'image/png'
};
const message = await client.messages.reply(targetEmailId, {
html: '<p>This is a reply with attachment.</p>',
attachments: [attachment]
});
assert.equal(message.attachments.length, 1, 'Should have attachment');
const file1 = message.attachments[0];
assert.isOk(file1.id, 'First attachment should have file id');
assert.isOk(file1.url);
assert.equal(file1.length, 82138, 'First attachment should be correct size');
assert.equal(file1.fileName, 'cat.png', 'First attachment should have filename');
assert.equal(file1.contentType, 'image/png', 'First attachment should have correct MIME type');
});
});
});