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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v3
with:
node-version: 18.12.1
node-version: 20.20.2
cache: 'yarn'
- run: yarn install --frozen-lockfile
- run: yarn lint
Expand Down
2 changes: 1 addition & 1 deletion .tool-versions
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
nodejs 18.12.1
nodejs 20.20.2
yarn 1.22.17
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ mailtrap

## Nodemailer Transport

> NOTE: [Nodemailer](https://www.npmjs.com/package/nodemailer) is needed as a dependency.
> NOTE: [Nodemailer](https://www.npmjs.com/package/nodemailer) is needed as a dependency. Versions 9 and 10 are supported (Nodemailer 10 requires Node.js 20 or newer).

```sh
npm install nodemailer
Expand All @@ -207,7 +207,7 @@ npm install nodemailer
yarn add nodemailer
```

If you're using TypeScript, install `@types/nodemailer` as a `devDependency`:
If you're using TypeScript with Nodemailer 9, install `@types/nodemailer` as a `devDependency` (Nodemailer 10 ships its own type definitions, so this is not needed there):

```sh
npm install -D @types/nodemailer
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"eslint-plugin-import": "^2.29.1",
"eslint-plugin-prettier": "^4.0.0",
"jest": "^29.3.1",
"nodemailer": "^9.0.1",
"nodemailer": "^10.0.0",
"prettier": "^2.6.2",
"rimraf": "^5.0.5",
"ts-node": "^10.2.1",
Expand All @@ -48,7 +48,7 @@
},
"peerDependencies": {
"@types/nodemailer": "^6.4.9",
"nodemailer": "^9.0.1"
"nodemailer": "^9.0.1 || ^10.0.0"
},
"peerDependenciesMeta": {
"nodemailer": {
Expand Down
34 changes: 34 additions & 0 deletions src/__tests__/adapters/attachment.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Readable } from "stream";
import { readFileSync } from "node:fs";

import adaptAttachment from "../../adapters/attachement";

Expand Down Expand Up @@ -27,6 +28,21 @@ describe("adapters/attachment: ", () => {
);
});

it("throws `content required` error if adapted content is empty.", () => {
const emptyStream = new Readable({
read() {
this.push(null);
},
});

expect(() =>
adaptAttachment({ filename: "mock-filename", content: "" })
).toThrowError(new Error(CONTENT_REQUIRED));
expect(() =>
adaptAttachment({ filename: "mock-filename", content: emptyStream })
).toThrowError(new Error(CONTENT_REQUIRED));
});

it("returns adapted attachment object in case if content is buffer.", () => {
const attachment = {
filename: "mock-filename",
Expand Down Expand Up @@ -91,5 +107,23 @@ describe("adapters/attachment: ", () => {

expect(result).toEqual(expectedAttachment);
});

it("returns adapted attachment object in case if content is a content object.", () => {
const attachment = {
filename: "mock-filename",
content: { path: __filename },
};

const expectedAttachment = {
filename: attachment.filename,
content: readFileSync(__filename),
disposition: undefined,
content_id: undefined,
type: undefined,
};
const result = adaptAttachment(attachment);

expect(result).toEqual(expectedAttachment);
});
});
});
56 changes: 56 additions & 0 deletions src/__tests__/adapters/headers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,61 @@ describe("adapters/headers: ", () => {

expect(result).toEqual(expectedResult);
});

it("returns object if headers is a single `{ key, value }` pair.", () => {
const headers = {
key: "mock-key",
value: "mock-value",
};

const expectedResult = {
[headers.key]: headers.value,
};
const result = adaptHeaders(headers);

expect(result).toEqual(expectedResult);
});

it("converts non-string header values to strings.", () => {
const date = new Date("2026-01-02T03:04:05Z");
const headers = {
mockNumber: 42,
mockBoolean: true,
mockDate: date,
mockAddress: { name: "mock-name", address: "mock-email" },
mockAddressWithoutName: { address: "mock-email" },
mockNested: [[{ prepared: true, value: 7 }]],
};

const expectedResult = {
mockNumber: "42",
mockBoolean: "true",
mockDate: date.toUTCString(),
mockAddress: "mock-name <mock-email>",
mockAddressWithoutName: "mock-email",
mockNested: "7",
};
const result = adaptHeaders(headers);

expect(result).toEqual(expectedResult);
});

it("skips headers with empty values.", () => {
const headers = {
mockNull: null,
mockUndefined: undefined,
mockEmptyArray: [],
mockAddressWithoutAddress: { name: "mock-name" },
mockAddressWithBlankAddress: { name: "mock-name", address: " " },
mockKey: "mock-value",
};

const expectedResult = {
mockKey: "mock-value",
};
const result = adaptHeaders(headers);

expect(result).toEqual(expectedResult);
});
});
});
19 changes: 15 additions & 4 deletions src/__tests__/adapters/mail.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import adaptMail from "../../adapters/mail";
import config from "../../config";
import {
adaptSingleRecipient,
adaptReplyToRecipient,
adaptFirstRecipient,
} from "../../adapters/recipients";

const { ERRORS } = config;
Expand All @@ -20,6 +20,17 @@ describe("adapters/mail: ", () => {
expect(result).toEqual(expectedResult);
});

it("returns object with error `from is required` if from has no address.", () => {
const expectedResult = { success: false, errors: [FROM_REQUIRED] };

expect(adaptMail({ from: "" })).toEqual(expectedResult);
expect(adaptMail({ from: [] })).toEqual(expectedResult);
expect(adaptMail({ from: { address: "" } })).toEqual(expectedResult);
expect(adaptMail({ from: { name: "mock-name" } })).toEqual(
expectedResult
);
});

it("returns `mail` object with basic info + headers.", () => {
const data = {
from: "mock-from",
Expand All @@ -37,7 +48,7 @@ describe("adapters/mail: ", () => {
bcc: [],
headers: data.headers,
subject: data.subject,
reply_to: adaptReplyToRecipient(data.replyTo),
reply_to: adaptFirstRecipient(data.replyTo),
};
const result = adaptMail(data);

Expand All @@ -63,7 +74,7 @@ describe("adapters/mail: ", () => {
bcc: [],
headers: data.headers,
attachments: data.attachments,
reply_to: adaptReplyToRecipient(data.replyTo),
reply_to: adaptFirstRecipient(data.replyTo),
};
const result = adaptMail(data);

Expand Down Expand Up @@ -93,7 +104,7 @@ describe("adapters/mail: ", () => {
headers: data.headers,
attachments: data.attachments,
custom_variables: data.customVariables,
reply_to: adaptReplyToRecipient(data.replyTo),
reply_to: adaptFirstRecipient(data.replyTo),
};
const result = adaptMail(data);

Expand Down
68 changes: 62 additions & 6 deletions src/__tests__/adapters/recipients.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import adaptRecipients, {
adaptSingleRecipient,
adaptReplyToRecipient,
adaptFirstRecipient,
} from "../../adapters/recipients";

describe("adapters/recipients: ", () => {
Expand Down Expand Up @@ -30,6 +30,19 @@ describe("adapters/recipients: ", () => {

expect(result).toEqual(expectedResult);
});

it("omits name if Nodemailer address has no name.", () => {
const recipient = {
address: "mock-email",
};

const expectedResult = {
email: recipient.address,
};
const result = adaptSingleRecipient(recipient);

expect(result).toEqual(expectedResult);
});
});

describe("adaptRecipients(): ", () => {
Expand Down Expand Up @@ -85,14 +98,48 @@ describe("adapters/recipients: ", () => {

expect(result).toEqual(expectedResult);
});

it("flattens nested recipients arrays.", () => {
const recipients = [
"mock-email-1",
[{ name: "mock-name-2", address: "mock-email-2" }, ["mock-email-3"]],
];

const expectedResult = [
{ email: "mock-email-1" },
{ name: "mock-name-2", email: "mock-email-2" },
{ email: "mock-email-3" },
];
const result = adaptRecipients(recipients);

expect(result).toEqual(expectedResult);
});

it("expands address groups into their members.", () => {
const recipients = {
name: "mock-group",
group: [
{ name: "mock-name-1", address: "mock-email-1" },
{ address: "mock-email-2" },
],
};

const expectedResult = [
{ name: "mock-name-1", email: "mock-email-1" },
{ email: "mock-email-2" },
];
const result = adaptRecipients(recipients);

expect(result).toEqual(expectedResult);
});
});

describe("adaptReplyToRecipient(): ", () => {
describe("adaptFirstRecipient(): ", () => {
it("returns undefined if recipients is invalid.", () => {
const recipients = undefined;

const expectedResult = undefined;
const result = adaptReplyToRecipient(recipients);
const result = adaptFirstRecipient(recipients);

expect(result).toEqual(expectedResult);
});
Expand All @@ -101,7 +148,7 @@ describe("adapters/recipients: ", () => {
const recipients: any = [];

const expectedResult = undefined;
const result = adaptReplyToRecipient(recipients);
const result = adaptFirstRecipient(recipients);

expect(result).toEqual(expectedResult);
});
Expand All @@ -116,7 +163,7 @@ describe("adapters/recipients: ", () => {
name: recipients.name,
email: recipients.address,
};
const result = adaptReplyToRecipient(recipients);
const result = adaptFirstRecipient(recipients);

expect(result).toEqual(expectedResult);
});
Expand All @@ -137,7 +184,16 @@ describe("adapters/recipients: ", () => {
name: recipients[0].name,
email: recipients[0].address,
};
const result = adaptReplyToRecipient(recipients);
const result = adaptFirstRecipient(recipients);

expect(result).toEqual(expectedResult);
});

it("returns first adapted recipient if it's a nested array.", () => {
const recipients = [[], ["mock-email-1", "mock-email-2"]];

const expectedResult = { email: "mock-email-1" };
const result = adaptFirstRecipient(recipients);

expect(result).toEqual(expectedResult);
});
Expand Down
18 changes: 11 additions & 7 deletions src/adapters/attachement.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
import { Attachment as NodemailerAttachment } from "nodemailer/lib/mailer";
import Mail from "nodemailer/lib/mailer";

import adaptContent from "./content";

import CONFIG from "../config";

import { Attachment } from "../types/mailtrap";

type NodemailerAttachment = Mail.Attachment;

const { ERRORS } = CONFIG;
const { FILENAME_REQUIRED, CONTENT_REQUIRED } = ERRORS;

/**
* Adopts Nodemailer attachment to Mailtrap.
* Checks if filename or content are missing, then rejects with error.
* Otherwise specifies type of content, then builds attachment object for Mailtrap.
* Otherwise adapts the content, then builds attachment object for Mailtrap.
* @todo throw error when only filename is provided
*/
export default function adaptAttachment(
Expand All @@ -24,11 +28,11 @@ export default function adaptAttachment(
throw new Error(CONTENT_REQUIRED);
}

const content =
typeof nodemailerAttachment.content === "string" ||
nodemailerAttachment.content instanceof Buffer
? nodemailerAttachment.content
: nodemailerAttachment.content.read();
const content = adaptContent(nodemailerAttachment.content);

if (!content) {
throw new Error(CONTENT_REQUIRED);
}

return {
filename: nodemailerAttachment.filename,
Expand Down
5 changes: 3 additions & 2 deletions src/adapters/content.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { readFileSync } from "node:fs";
import { Readable } from "node:stream";
import { AttachmentLike } from "nodemailer/lib/mailer";

import { NodemailerContent } from "../types/transport";

/**
* Checks if content type is rather string or buffer, returns content.
Expand All @@ -9,7 +10,7 @@ import { AttachmentLike } from "nodemailer/lib/mailer";
* Otherwise reads file.
*/
export default function adaptContent(
content: string | Buffer | Readable | AttachmentLike
content: NodemailerContent
Comment thread
coderabbitai[bot] marked this conversation as resolved.
): string | Buffer {
Comment thread
thoda-dev marked this conversation as resolved.
if (typeof content === "string" || content instanceof Buffer) {
return content;
Expand Down
Loading