Skip to content

Commit

Permalink
Add sending ability based on API
Browse files Browse the repository at this point in the history
  • Loading branch information
cloud-bot committed Mar 9, 2021
1 parent bbf8495 commit 90bb27c
Show file tree
Hide file tree
Showing 12 changed files with 5,625 additions and 624 deletions.
25 changes: 24 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<a href="https://www.cloudmailin.com">
<img src="https://assets.cloudmailin.com/assets/favicon.png" alt="CloudMailin Logo" height="60" align="left" style="margin-right: 20px;" title="CloudMailin">
<img src="https://assets.cloudmailin.com/assets/favicon.png" alt="CloudMailin Logo" height="60" align="right" title="CloudMailin">
</a>

# CloudMailin Node.js Library
Expand Down Expand Up @@ -37,3 +37,26 @@ app.post("/incoming_mails/", (req, res) => {
res.status(201).json(mail);
}
```
### Sending Email
```typescript
import { MessageClient } from "cloudmailin"

const client = new MessageClient({ username: USERNAME, apiKey: API_KEY});
const response = await client.sendMessage({
to: '[email protected]',
from: '[email protected]',
plain: 'test message',
html: '<h1>Test Message</h1>',
subject: "hello world"
});
```
## Development
Generating the OpenAPI reference:
```sh
npx openapi-typescript ./path_to/api.yaml --output ./src/models/cloudmailin-api.ts
```
6 changes: 6 additions & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
module.exports = {
transform: { '^.+\\.ts?$': 'ts-jest' },
testEnvironment: 'node',
testRegex: '/tests/.*\\.(test|spec)?\\.(ts|tsx)$',
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node']
};
5,849 changes: 5,240 additions & 609 deletions package-lock.json

Large diffs are not rendered by default.

16 changes: 12 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
{
"name": "cloudmailin",
"version": "0.0.2",
"version": "0.0.3",
"description": "Official Node.js for the CloudMailin Email API - https://www.cloudmailin.com",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "rm -rf ./dist && tsc",
"lint": "eslint ./src",
"test": "echo \"Error: no test specified\" && exit 1",
"test": "jest",
"prepare": "rm -rf ./dist && tsc",
"docs": "rm -rf ./docs && typedoc ./src"
},
Expand All @@ -32,12 +32,20 @@
"bugs": {
"url": "https://github.com/cloudmailin/cloudmailin-js/issues"
},
"homepage": "https://github.com/cloudmailin/cloudmailin-js#readme",
"dependencies": {},
"homepage": "https://www.cloudmailin.com",
"readme": "https://github.com/cloudmailin/cloudmailin-js#readme",
"dependencies": {
"axios": "^0.21.1"
},
"devDependencies": {
"@types/jest": "^26.0.20",
"@types/node": "^14.14.31",
"@typescript-eslint/eslint-plugin": "^4.15.1",
"@typescript-eslint/parser": "^4.15.1",
"eslint": "^7.20.0",
"jest": "^26.6.3",
"openapi-typescript": "^3.0.3",
"ts-jest": "^26.5.3",
"typedoc": "^0.20.27",
"typescript": "^4.1.5"
}
Expand Down
15 changes: 13 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
import IncomingMail from "./interfaces/IncomingMail";
import MessageClient, { MessageClientOptions } from "./messageClient";

export { IncomingMail };
import { Errors } from "./models"
import { IncomingMail } from "./models";
import { Message, MessageRaw } from "./models"

export * as Models from "./models"

export {
MessageClient, MessageClientOptions,
Errors,
IncomingMail,
Message, MessageRaw
};
83 changes: 83 additions & 0 deletions src/messageClient.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { Message, MessageRaw, MessageResponse } from "./models/message";
import * as errors from "./models/errors";
import axios, { AxiosError, AxiosRequestConfig } from "axios";

// Allow us to easily fetch the current version from the package without hacks
// eslint-disable-next-line @typescript-eslint/no-var-requires
const { version } = require("../package.json");

export interface MessageClientOptions {
username: string,
apiKey: string;
host?: string;
baseURL?: string;
}

type requestMethod = AxiosRequestConfig['method'];

export default class MessageClient {
private options: MessageClientOptions;
private version: string;

constructor(options: MessageClientOptions) {
this.version = version;
this.options = options;

this.options.host = this.options.host || "api.cloudmailin.com";
this.options.baseURL = this.options.baseURL || `https://${this.options.host}/api/v0.1`;
}

public sendMessage(message: Message): Promise<MessageResponse> {
return this.makeRequest("POST", "/messages", message);
}

public sendRawMessage(message: MessageRaw): Promise<MessageResponse> {
return this.makeRequest("POST", "/messages", message);
}

// Allow body to be anything
// eslint-disable-next-line @typescript-eslint/ban-types
private makeRequest<T>(method: requestMethod, path: string, body?: object): Promise<T> {
const client = this.makeClient();

return client.request<T>({
method: method,
url: path,
data: body
})
.then((response) => response.data)
.catch((error) => {
const newError = this.handleError(error);
throw newError;
});
}

private handleError(error: AxiosError) {
const response = error.response;
const status = response?.status;

switch (status) {
case 422:
return new errors.CloudMailinError(error.message, error);

default:
return new errors.CloudMailinError(error.message, error);
}
}

private makeClient() {
const baseURL = `${this.options.baseURL}/${this.options.username}`;
const headers = {
Authorization: `Bearer ${this.options.apiKey}`,
"User-Agent": `CloudMailin-js ${this.version}`
};

return axios.create({
baseURL: baseURL,
responseType: "json",
maxContentLength: Infinity,
maxBodyLength: Infinity,
headers: headers
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* An interface to help with receiving CloudMailin inbound HTTP POSTs
* This relies on the JSON Normalized format
*/
export default interface IncomingMail {
export interface IncomingMail {
headers: IncomingHeaderObject;

/**
Expand Down
151 changes: 151 additions & 0 deletions src/models/cloudmailin-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/

export interface paths {
"/messages": {
post: operations["sendMessage"];
};
}

export interface components {
schemas: {
MessageCommon: {
id?: string;
/**
* The from addrress of the email message.
* This is the address to be used in the SMTP transaction itself.
* Although it will be replaced with an address used for bounce handling.
* This must match a `from:` header in the email headers.
*/
from: string;
/**
* The To addrress of the email message.
* This is the address to be used in the SMTP transaction itself.
* This must match a `To:` header in the email headers.
*/
to: string[] | string;
/**
* Whether to send this message in test mode.
* This will validate the messge but no actually send it if true.
* If the server is in test mode then it will always be in test mode
* regardless of this value.
*/
test_mode?: boolean;
/** The subject of the email. This will override any subject set in headers or raw messages. */
subject?: string;
/** Tags to */
tags?: string[] | string;
};
Message: components["schemas"]["MessageCommon"] & {
/**
* The plain text part of the email message.
* Either the plain text or the html parts are required.
*/
plain?: string;
/**
* The HTML part of the email message.
* Either the plain text or the html parts are required.
*/
html?: string;
headers?: { [key: string]: string };
attachments?: components["schemas"]["MessageAttachment"][];
};
MessageAttachment: {
file_name: string;
content: string;
content_type: string;
content_id?: string;
};
RawMessage: components["schemas"]["MessageCommon"] & {
/**
* A full raw email.
* This should consist of both headers and a message body.
* `To` and `From` headers must be present and match those in the request.
* Multiple parts, text and html or other mixed content are
* acceptable but the message must be valid and RFC822 compliant.
*
* Any attachments intended to be sent in the Raw format must also be
* encoded and included here.
*/
raw?: string;
};
Error: {
status?: number;
error?: string;
};
UnauthorizedError: {
status?: 401;
error?: string;
};
ForbiddenError: {
status?: 403;
error?: "Forbidden";
};
NotFoundError: {
status?: 404;
error?: string;
};
UnprocessableEntityError: {
status?: 422;
/** The description of the failed validation */
error?: string;
};
/** Identifier, please be aware that the format may change */
accountID: string;
/** Identifier, please be aware that the format may change */
id: string;
};
responses: {
/** The user is not Authorized */
401: {
content: {
"application/json": components["schemas"]["UnauthorizedError"];
};
};
/** The user is not Authorized */
403: {
content: {
"application/json": components["schemas"]["ForbiddenError"];
};
};
/** Resource be found or does not belong to this account */
404: {
content: {
"application/json": components["schemas"]["NotFoundError"];
};
};
/** Unprocessable Entity, most likely your input does not pass validation */
422: {
content: {
"application/json": components["schemas"]["UnprocessableEntityError"];
};
};
};
parameters: {
accountID: components["schemas"]["accountID"];
};
}

export interface operations {
sendMessage: {
responses: {
/** The message has been accepted */
202: {
content: {
"application/json": components["schemas"]["MessageCommon"];
};
};
401: components["responses"]["401"];
422: components["responses"]["422"];
};
requestBody: {
content: {
"application/json":
| components["schemas"]["Message"]
| components["schemas"]["RawMessage"];
};
};
};
}
22 changes: 22 additions & 0 deletions src/models/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { AxiosError } from "axios";

export class CloudMailinError extends Error {
public baseError: Error;
public status?: number;
public details: string;

constructor(message: string, baseError: AxiosError) {
const trueMessage = baseError.response?.data?.error || message;
super(trueMessage);

this.details = trueMessage;
this.status = baseError.response?.status;
this.baseError = baseError;

// https://github.com/Microsoft/TypeScript/wiki/Breaking-Changes#extending-built-ins-like-error-array-and-map-may-no-longer-work
Object.setPrototypeOf(this, CloudMailinError.prototype);

this.name = this.constructor.name;
Error.captureStackTrace(this, this.constructor);
}
}
5 changes: 5 additions & 0 deletions src/models/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { components } from "./cloudmailin-api";

export type Message = components['schemas']['Message'];
export type MessageRaw = components['schemas']['RawMessage'];
export type MessageResponse = components['schemas']['MessageCommon'];
Loading

0 comments on commit 90bb27c

Please sign in to comment.