@beesolve/email-service 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/cdk.d.ts +60 -0
- package/dist/cdk.js +139 -0
- package/dist/sdk.d.ts +46 -0
- package/dist/sdk.js +142 -0
- package/dist/templating.d.ts +80 -0
- package/dist/templating.js +166 -0
- package/package.json +66 -0
package/README.md
ADDED
package/dist/cdk.d.ts
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { RemovalPolicy } from "aws-cdk-lib";
|
|
2
|
+
import { Function, FunctionOptions } from "aws-cdk-lib/aws-lambda";
|
|
3
|
+
import { ConfigurationSet, EmailSendingEvent } from "aws-cdk-lib/aws-ses";
|
|
4
|
+
import { Construct } from "constructs";
|
|
5
|
+
declare class Emails extends Construct {
|
|
6
|
+
private table;
|
|
7
|
+
private queue;
|
|
8
|
+
private bucket;
|
|
9
|
+
constructor(scope: Construct, id: string, props: {
|
|
10
|
+
readonly defaultSender: {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly emailAddress: string;
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Set this if you want to set specific SES identity which will be used to send emails.
|
|
16
|
+
*/
|
|
17
|
+
readonly fromArn?: string;
|
|
18
|
+
readonly defaultConfigurationSet?: ConfigurationSet;
|
|
19
|
+
/**
|
|
20
|
+
* Set this if you want to track other than default events.
|
|
21
|
+
*
|
|
22
|
+
* @default new Set([EmailSendingEvent.SEND, EmailSendingEvent.BOUNCE, EmailSendingEvent.COMPLAINT, EmailSendingEvent.DELIVERY, EmailSendingEvent.REJECT])
|
|
23
|
+
*/
|
|
24
|
+
readonly eventsToTrack?: Set<EmailSendingEvent>;
|
|
25
|
+
readonly isProd?: boolean;
|
|
26
|
+
readonly removalPolicy?: RemovalPolicy;
|
|
27
|
+
readonly deletionProtection?: boolean;
|
|
28
|
+
/**
|
|
29
|
+
* How long should the attachments be stored in S3 bucket before they are deleted.
|
|
30
|
+
*
|
|
31
|
+
* @default 180
|
|
32
|
+
*/
|
|
33
|
+
readonly attachmentsRetentionDays?: number;
|
|
34
|
+
/**
|
|
35
|
+
* How long should the messages be stored in DyanmoDB before they are deleted.
|
|
36
|
+
*
|
|
37
|
+
* If set to 0, messages are not being persisted to DynamoDB.
|
|
38
|
+
*
|
|
39
|
+
* @default 7
|
|
40
|
+
*/
|
|
41
|
+
readonly messagesRetentionDays?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Event bus which notifications about sent emails are emitted to.
|
|
44
|
+
*
|
|
45
|
+
* @default "default"
|
|
46
|
+
*/
|
|
47
|
+
readonly eventBusName?: string;
|
|
48
|
+
/**
|
|
49
|
+
* @default
|
|
50
|
+
* {
|
|
51
|
+
* memorySize: 256,
|
|
52
|
+
* timeout: Duration.seconds(30),
|
|
53
|
+
* reservedConcurrentExecutions: 2
|
|
54
|
+
* }
|
|
55
|
+
*/
|
|
56
|
+
readonly handler?: Pick<FunctionOptions, "memorySize" | "timeout" | "reservedConcurrentExecutions">;
|
|
57
|
+
});
|
|
58
|
+
readonly grantAccess: (grantee: Function) => void;
|
|
59
|
+
}
|
|
60
|
+
export { Emails };
|
package/dist/cdk.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// packages/service-email/cdk.ts
|
|
2
|
+
import { SqsWithDlq } from "@beesolve/cdk-constructs";
|
|
3
|
+
import { Duration, RemovalPolicy } from "aws-cdk-lib";
|
|
4
|
+
import {
|
|
5
|
+
AttributeType,
|
|
6
|
+
Billing,
|
|
7
|
+
TableEncryptionV2,
|
|
8
|
+
TableV2
|
|
9
|
+
} from "aws-cdk-lib/aws-dynamodb";
|
|
10
|
+
import { EventBus } from "aws-cdk-lib/aws-events";
|
|
11
|
+
import { Effect, PolicyStatement } from "aws-cdk-lib/aws-iam";
|
|
12
|
+
import {
|
|
13
|
+
Architecture,
|
|
14
|
+
Runtime
|
|
15
|
+
} from "aws-cdk-lib/aws-lambda";
|
|
16
|
+
import { NodejsFunction } from "aws-cdk-lib/aws-lambda-nodejs";
|
|
17
|
+
import {
|
|
18
|
+
BlockPublicAccess,
|
|
19
|
+
Bucket,
|
|
20
|
+
BucketAccessControl,
|
|
21
|
+
BucketEncryption
|
|
22
|
+
} from "aws-cdk-lib/aws-s3";
|
|
23
|
+
import {
|
|
24
|
+
ConfigurationSet,
|
|
25
|
+
ConfigurationSetEventDestination,
|
|
26
|
+
EmailSendingEvent,
|
|
27
|
+
EventDestination
|
|
28
|
+
} from "aws-cdk-lib/aws-ses";
|
|
29
|
+
import { Construct } from "constructs";
|
|
30
|
+
import { resolve } from "node:path";
|
|
31
|
+
var __dirname = "/Users/ivan/data/work/github.com/beesolve/p/packages/packages/service-email";
|
|
32
|
+
|
|
33
|
+
class Emails extends Construct {
|
|
34
|
+
table;
|
|
35
|
+
queue;
|
|
36
|
+
bucket;
|
|
37
|
+
constructor(scope, id, props) {
|
|
38
|
+
super(scope, id);
|
|
39
|
+
const {
|
|
40
|
+
isProd = false,
|
|
41
|
+
attachmentsRetentionDays = 180,
|
|
42
|
+
messagesRetentionDays = 7,
|
|
43
|
+
eventBusName = "default",
|
|
44
|
+
defaultConfigurationSet = new ConfigurationSet(this, "DefaultConfigurationSet"),
|
|
45
|
+
eventsToTrack = new Set([
|
|
46
|
+
EmailSendingEvent.SEND,
|
|
47
|
+
EmailSendingEvent.BOUNCE,
|
|
48
|
+
EmailSendingEvent.COMPLAINT,
|
|
49
|
+
EmailSendingEvent.DELIVERY,
|
|
50
|
+
EmailSendingEvent.REJECT
|
|
51
|
+
])
|
|
52
|
+
} = props;
|
|
53
|
+
const eventBus = EventBus.fromEventBusName(this, "EventBus", eventBusName);
|
|
54
|
+
new ConfigurationSetEventDestination(this, "SesEventDestination", {
|
|
55
|
+
events: Array.from(eventsToTrack),
|
|
56
|
+
configurationSet: defaultConfigurationSet,
|
|
57
|
+
destination: EventDestination.eventBus(eventBus),
|
|
58
|
+
enabled: true
|
|
59
|
+
});
|
|
60
|
+
this.table = new TableV2(this, "EmailLog", {
|
|
61
|
+
partitionKey: {
|
|
62
|
+
name: "pk",
|
|
63
|
+
type: AttributeType.STRING
|
|
64
|
+
},
|
|
65
|
+
billing: Billing.onDemand(),
|
|
66
|
+
deletionProtection: props.deletionProtection ?? false,
|
|
67
|
+
encryption: TableEncryptionV2.awsManagedKey(),
|
|
68
|
+
removalPolicy: props.removalPolicy ?? RemovalPolicy.RETAIN,
|
|
69
|
+
timeToLiveAttribute: "ttl",
|
|
70
|
+
pointInTimeRecoverySpecification: {
|
|
71
|
+
pointInTimeRecoveryEnabled: isProd
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
this.bucket = new Bucket(this, "EmailAttachments", {
|
|
75
|
+
accessControl: BucketAccessControl.PRIVATE,
|
|
76
|
+
blockPublicAccess: BlockPublicAccess.BLOCK_ALL,
|
|
77
|
+
encryption: BucketEncryption.S3_MANAGED,
|
|
78
|
+
enforceSSL: true,
|
|
79
|
+
lifecycleRules: [
|
|
80
|
+
{
|
|
81
|
+
expiration: Duration.days(attachmentsRetentionDays),
|
|
82
|
+
abortIncompleteMultipartUploadAfter: Duration.days(1)
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
});
|
|
86
|
+
const handler = new NodejsFunction(this, "SqsHandler", {
|
|
87
|
+
description: "Email queue handler",
|
|
88
|
+
entry: resolve(__dirname, "./src/handler.ts"),
|
|
89
|
+
handler: "handler",
|
|
90
|
+
bundling: {
|
|
91
|
+
minify: isProd,
|
|
92
|
+
sourceMap: isProd,
|
|
93
|
+
sourcesContent: false,
|
|
94
|
+
target: "es2022"
|
|
95
|
+
},
|
|
96
|
+
memorySize: props.handler?.memorySize ?? 256,
|
|
97
|
+
timeout: props.handler?.timeout ?? Duration.seconds(30),
|
|
98
|
+
reservedConcurrentExecutions: props.handler?.reservedConcurrentExecutions ?? 2,
|
|
99
|
+
runtime: Runtime.NODEJS_24_X,
|
|
100
|
+
architecture: Architecture.ARM_64,
|
|
101
|
+
environment: {
|
|
102
|
+
BUCKET_NAME: this.bucket.bucketName,
|
|
103
|
+
TABLE_NAME: this.table.tableName,
|
|
104
|
+
DEFAULT_SENDER_NAME: props.defaultSender.name,
|
|
105
|
+
DEFAULT_SENDER_EMAIL_ADDRESS: props.defaultSender.emailAddress,
|
|
106
|
+
MESSAGES_RETENTION_DAYS: String(messagesRetentionDays),
|
|
107
|
+
EVENT_BUS_ARN: eventBus.eventBusArn,
|
|
108
|
+
DEFAULT_CONFIGURATION_SET_NAME: defaultConfigurationSet.configurationSetName
|
|
109
|
+
},
|
|
110
|
+
depsLockFilePath: resolve(`${__dirname}/../../bun.lock`)
|
|
111
|
+
});
|
|
112
|
+
this.table.grantReadWriteData(handler);
|
|
113
|
+
this.bucket.grantRead(handler);
|
|
114
|
+
eventBus.grantPutEventsTo(handler);
|
|
115
|
+
if (props.fromArn) {
|
|
116
|
+
handler.addEnvironment("FROM_ARN", props.fromArn);
|
|
117
|
+
}
|
|
118
|
+
handler.addToRolePolicy(new PolicyStatement({
|
|
119
|
+
actions: ["ses:SendEmail", "ses:SendRawEmail"],
|
|
120
|
+
resources: ["*"],
|
|
121
|
+
effect: Effect.ALLOW
|
|
122
|
+
}));
|
|
123
|
+
const { queue } = SqsWithDlq.asLambdaInput({
|
|
124
|
+
lambda: handler
|
|
125
|
+
});
|
|
126
|
+
this.queue = queue;
|
|
127
|
+
}
|
|
128
|
+
grantAccess = (grantee) => {
|
|
129
|
+
this.table.grantReadData(grantee);
|
|
130
|
+
this.queue.grantSendMessages(grantee);
|
|
131
|
+
this.bucket.grantWrite(grantee);
|
|
132
|
+
grantee.addEnvironment("BEESOLVE_EMAILS_QUEUE_URL", this.queue.queueUrl);
|
|
133
|
+
grantee.addEnvironment("BEESOLVE_EMAILS_TABLE_NAME", this.table.tableName);
|
|
134
|
+
grantee.addEnvironment("BEESOLVE_EMAILS_ATTACHMENTS_BUCKET", this.bucket.bucketName);
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
export {
|
|
138
|
+
Emails
|
|
139
|
+
};
|
package/dist/sdk.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
2
|
+
import { SQSClient } from "@aws-sdk/client-sqs";
|
|
3
|
+
import * as v from "valibot";
|
|
4
|
+
declare const requestSchema: unknown;
|
|
5
|
+
type Attachment = {
|
|
6
|
+
readonly type: "public";
|
|
7
|
+
readonly mimeType: string;
|
|
8
|
+
readonly publicUrl: string;
|
|
9
|
+
readonly customName: string;
|
|
10
|
+
} | {
|
|
11
|
+
readonly type: "s3";
|
|
12
|
+
readonly mimeType: string;
|
|
13
|
+
readonly body: Buffer;
|
|
14
|
+
readonly customName: string;
|
|
15
|
+
};
|
|
16
|
+
interface Sender {
|
|
17
|
+
readonly name: string;
|
|
18
|
+
readonly emailAddress: string;
|
|
19
|
+
}
|
|
20
|
+
declare class Email {
|
|
21
|
+
private readonly props;
|
|
22
|
+
private readonly s3Client;
|
|
23
|
+
private readonly sqsClient;
|
|
24
|
+
constructor(props?: {
|
|
25
|
+
readonly s3Client?: S3Client;
|
|
26
|
+
readonly sqsClient?: SQSClient;
|
|
27
|
+
});
|
|
28
|
+
readonly sendEmail: (request: {
|
|
29
|
+
readonly recipients: string[];
|
|
30
|
+
readonly subject: string;
|
|
31
|
+
readonly html: string;
|
|
32
|
+
readonly text?: string;
|
|
33
|
+
readonly sender?: Sender;
|
|
34
|
+
readonly attachments?: Attachment[];
|
|
35
|
+
readonly configurationSetName?: string;
|
|
36
|
+
}) => Promise<{
|
|
37
|
+
requestId: string;
|
|
38
|
+
}>;
|
|
39
|
+
readonly getMessage: (requestId: string) => Promise<{
|
|
40
|
+
readonly requestId: string;
|
|
41
|
+
readonly messageId: string;
|
|
42
|
+
readonly request: v.InferOutput<typeof requestSchema>;
|
|
43
|
+
readonly expiresAt: Date;
|
|
44
|
+
}>;
|
|
45
|
+
}
|
|
46
|
+
export { Email };
|
package/dist/sdk.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// packages/service-email/sdk.ts
|
|
2
|
+
import { assertUnreachable, call } from "@beesolve/helpers";
|
|
3
|
+
import { PutObjectCommand } from "@aws-sdk/client-s3";
|
|
4
|
+
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
|
|
5
|
+
import { GetCommand } from "@aws-sdk/lib-dynamodb";
|
|
6
|
+
import { randomBytes } from "node:crypto";
|
|
7
|
+
import * as v2 from "valibot";
|
|
8
|
+
|
|
9
|
+
// packages/service-email/src/aws.ts
|
|
10
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
11
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
12
|
+
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
|
|
13
|
+
var dynamoClient = DynamoDBDocumentClient.from(new DynamoDBClient, {
|
|
14
|
+
marshallOptions: {
|
|
15
|
+
removeUndefinedValues: true,
|
|
16
|
+
convertEmptyValues: false
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
var s3Client = new S3Client;
|
|
20
|
+
|
|
21
|
+
// packages/service-email/src/validation.ts
|
|
22
|
+
import * as v from "valibot";
|
|
23
|
+
var requestSchema = v.object({
|
|
24
|
+
id: v.string(),
|
|
25
|
+
recipients: v.array(v.pipe(v.string(), v.email())),
|
|
26
|
+
subject: v.string(),
|
|
27
|
+
html: v.string(),
|
|
28
|
+
text: v.optional(v.string()),
|
|
29
|
+
sender: v.optional(v.object({
|
|
30
|
+
name: v.string(),
|
|
31
|
+
emailAddress: v.string()
|
|
32
|
+
})),
|
|
33
|
+
attachments: v.optional(v.array(v.variant("type", [
|
|
34
|
+
v.object({
|
|
35
|
+
type: v.literal("public"),
|
|
36
|
+
mimeType: v.string(),
|
|
37
|
+
publicUrl: v.string(),
|
|
38
|
+
customName: v.string()
|
|
39
|
+
}),
|
|
40
|
+
v.object({
|
|
41
|
+
type: v.literal("s3"),
|
|
42
|
+
mimeType: v.string(),
|
|
43
|
+
fileId: v.string(),
|
|
44
|
+
customName: v.string()
|
|
45
|
+
})
|
|
46
|
+
]))),
|
|
47
|
+
configurationSetName: v.optional(v.string())
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// packages/service-email/sdk.ts
|
|
51
|
+
var message = `It seems that Emails service has not been set up correctly. Please make sure you've used official CDK construct and that you've granted access to your labmda function.`;
|
|
52
|
+
var envSchema = v2.object({
|
|
53
|
+
BEESOLVE_EMAILS_QUEUE_URL: v2.config(v2.string(), { message }),
|
|
54
|
+
BEESOLVE_EMAILS_TABLE_NAME: v2.config(v2.string(), { message }),
|
|
55
|
+
BEESOLVE_EMAILS_ATTACHMENTS_BUCKET: v2.config(v2.string(), { message })
|
|
56
|
+
});
|
|
57
|
+
var env = v2.parse(envSchema, process.env);
|
|
58
|
+
var sqsClient = new SQSClient({});
|
|
59
|
+
|
|
60
|
+
class Email {
|
|
61
|
+
props;
|
|
62
|
+
s3Client;
|
|
63
|
+
sqsClient;
|
|
64
|
+
constructor(props = {}) {
|
|
65
|
+
this.props = props;
|
|
66
|
+
this.s3Client = props.s3Client ?? s3Client;
|
|
67
|
+
this.sqsClient = props.sqsClient ?? sqsClient;
|
|
68
|
+
}
|
|
69
|
+
sendEmail = async (request) => {
|
|
70
|
+
const id = randomBytes(32).toString("base64url");
|
|
71
|
+
const { attachments, ...rest } = request;
|
|
72
|
+
const resolvedAttachments = attachments ? await call(async () => {
|
|
73
|
+
return Promise.all(attachments.map(async (item) => {
|
|
74
|
+
if (item.type === "public")
|
|
75
|
+
return item;
|
|
76
|
+
if (item.type === "s3") {
|
|
77
|
+
const fileId = randomBytes(32).toString("base64url");
|
|
78
|
+
const { body, ...rest2 } = item;
|
|
79
|
+
await this.s3Client.send(new PutObjectCommand({
|
|
80
|
+
Bucket: env.BEESOLVE_EMAILS_ATTACHMENTS_BUCKET,
|
|
81
|
+
Key: fileId,
|
|
82
|
+
Body: body
|
|
83
|
+
}));
|
|
84
|
+
return {
|
|
85
|
+
...rest2,
|
|
86
|
+
fileId
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
assertUnreachable(item);
|
|
90
|
+
}));
|
|
91
|
+
}) : undefined;
|
|
92
|
+
await this.sqsClient.send(new SendMessageCommand({
|
|
93
|
+
QueueUrl: env.BEESOLVE_EMAILS_QUEUE_URL,
|
|
94
|
+
MessageBody: JSON.stringify({
|
|
95
|
+
id,
|
|
96
|
+
...rest,
|
|
97
|
+
attachments: resolvedAttachments
|
|
98
|
+
})
|
|
99
|
+
}));
|
|
100
|
+
return { requestId: id };
|
|
101
|
+
};
|
|
102
|
+
getMessage = async (requestId) => {
|
|
103
|
+
const { Item } = await dynamoClient.send(new GetCommand({
|
|
104
|
+
TableName: env.BEESOLVE_EMAILS_TABLE_NAME,
|
|
105
|
+
Key: {
|
|
106
|
+
pk: requestId
|
|
107
|
+
}
|
|
108
|
+
}));
|
|
109
|
+
if (Item == null) {
|
|
110
|
+
throw new MessageNotFoundError(`Message for ${requestId} has not been found. Make sure you have set up messagesRetentionDays properly.`);
|
|
111
|
+
}
|
|
112
|
+
const result = v2.safeParse(requestSchema, Item.request);
|
|
113
|
+
if (!result.success) {
|
|
114
|
+
throw new MalformedRequestError(`Persisted request is malformed.`);
|
|
115
|
+
}
|
|
116
|
+
return {
|
|
117
|
+
requestId,
|
|
118
|
+
messageId: Item.messageId,
|
|
119
|
+
request: result.output,
|
|
120
|
+
expiresAt: new Date(Item.ttl * 1000)
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
class MessageNotFoundError extends Error {
|
|
126
|
+
stringified;
|
|
127
|
+
constructor(message2) {
|
|
128
|
+
super(typeof message2 === "string" ? message2 : JSON.stringify(message2));
|
|
129
|
+
this.stringified = typeof message2 !== "string";
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
class MalformedRequestError extends Error {
|
|
134
|
+
stringified;
|
|
135
|
+
constructor(message2) {
|
|
136
|
+
super(typeof message2 === "string" ? message2 : JSON.stringify(message2));
|
|
137
|
+
this.stringified = typeof message2 !== "string";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
export {
|
|
141
|
+
Email
|
|
142
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { JSX, ReactNode } from "react";
|
|
2
|
+
declare function BaseLayout<TStyles extends EmailBaseStyles>(props: {
|
|
3
|
+
readonly children: (styles: TStyles) => ReactNode;
|
|
4
|
+
readonly previewText: string;
|
|
5
|
+
readonly enhanceStyles?: (styles: EmailBaseStyles) => TStyles;
|
|
6
|
+
readonly project: {
|
|
7
|
+
readonly name: ReactNode;
|
|
8
|
+
readonly logo?: ReactNode;
|
|
9
|
+
readonly baseUri: string;
|
|
10
|
+
};
|
|
11
|
+
readonly notice?: (styles: TStyles) => ReactNode;
|
|
12
|
+
readonly notificationSettings?: (styles: TStyles) => ReactNode;
|
|
13
|
+
}): JSX.Element;
|
|
14
|
+
declare const baseStyles: {
|
|
15
|
+
readonly h1: {
|
|
16
|
+
readonly color: "#333";
|
|
17
|
+
readonly fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif";
|
|
18
|
+
readonly fontSize: "20px";
|
|
19
|
+
readonly fontWeight: "bold";
|
|
20
|
+
readonly marginBottom: "15px";
|
|
21
|
+
};
|
|
22
|
+
readonly link: {
|
|
23
|
+
readonly color: "#2754C5";
|
|
24
|
+
readonly fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif";
|
|
25
|
+
readonly fontSize: "14px";
|
|
26
|
+
readonly textDecoration: "underline";
|
|
27
|
+
};
|
|
28
|
+
readonly mainText: {
|
|
29
|
+
readonly marginBottom: "14px";
|
|
30
|
+
};
|
|
31
|
+
readonly main: {
|
|
32
|
+
readonly backgroundColor: "#fff";
|
|
33
|
+
readonly color: "#212121";
|
|
34
|
+
};
|
|
35
|
+
readonly container: {
|
|
36
|
+
readonly padding: "20px";
|
|
37
|
+
readonly margin: "0 auto";
|
|
38
|
+
readonly backgroundColor: "#eee";
|
|
39
|
+
};
|
|
40
|
+
readonly imageSection: {
|
|
41
|
+
readonly backgroundColor: "#252f3d";
|
|
42
|
+
readonly padding: "20px 0";
|
|
43
|
+
};
|
|
44
|
+
readonly coverSection: {
|
|
45
|
+
readonly backgroundColor: "#fff";
|
|
46
|
+
};
|
|
47
|
+
readonly upperSection: {
|
|
48
|
+
readonly padding: "25px 35px";
|
|
49
|
+
};
|
|
50
|
+
readonly lowerSection: {
|
|
51
|
+
readonly padding: "25px 35px";
|
|
52
|
+
};
|
|
53
|
+
readonly footerText: {
|
|
54
|
+
readonly fontSize: "12px";
|
|
55
|
+
readonly padding: "0 20px";
|
|
56
|
+
};
|
|
57
|
+
readonly cautionText: {
|
|
58
|
+
readonly margin: "0px";
|
|
59
|
+
};
|
|
60
|
+
readonly button: {
|
|
61
|
+
readonly fontSize: "14px";
|
|
62
|
+
readonly backgroundColor: "#1976d2";
|
|
63
|
+
readonly color: "#fff";
|
|
64
|
+
readonly lineHeight: 1.5;
|
|
65
|
+
readonly borderRadius: "0.5em";
|
|
66
|
+
readonly padding: "12px 24px";
|
|
67
|
+
readonly fontFamily: "Roboto,Arial,sans-serif";
|
|
68
|
+
readonly margin: "30px auto";
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
type EmailBaseStyles = typeof baseStyles;
|
|
72
|
+
import { Attributes, FunctionComponent } from "react";
|
|
73
|
+
declare function renderEmail<P extends {}>(props: {
|
|
74
|
+
readonly template: FunctionComponent<P>;
|
|
75
|
+
readonly props?: (Attributes & P) | null;
|
|
76
|
+
}): Promise<{
|
|
77
|
+
readonly html: string;
|
|
78
|
+
readonly text: string;
|
|
79
|
+
}>;
|
|
80
|
+
export { renderEmail, EmailBaseStyles, BaseLayout };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// packages/service-email/src/template/layout.tsx
|
|
2
|
+
import {
|
|
3
|
+
Body,
|
|
4
|
+
Column,
|
|
5
|
+
Container,
|
|
6
|
+
Head,
|
|
7
|
+
Hr,
|
|
8
|
+
Html,
|
|
9
|
+
Link,
|
|
10
|
+
Preview,
|
|
11
|
+
Row,
|
|
12
|
+
Section,
|
|
13
|
+
Text
|
|
14
|
+
} from "@react-email/components";
|
|
15
|
+
import { jsxDEV } from "react/jsx-dev-runtime";
|
|
16
|
+
function BaseLayout(props) {
|
|
17
|
+
const styles = props.enhanceStyles?.(baseStyles) ?? baseStyles;
|
|
18
|
+
return /* @__PURE__ */ jsxDEV(Html, {
|
|
19
|
+
children: [
|
|
20
|
+
/* @__PURE__ */ jsxDEV(Head, {}, undefined, false, undefined, this),
|
|
21
|
+
/* @__PURE__ */ jsxDEV(Body, {
|
|
22
|
+
style: styles.main,
|
|
23
|
+
children: [
|
|
24
|
+
/* @__PURE__ */ jsxDEV(Preview, {
|
|
25
|
+
children: props.previewText
|
|
26
|
+
}, undefined, false, undefined, this),
|
|
27
|
+
/* @__PURE__ */ jsxDEV(Container, {
|
|
28
|
+
style: styles.container,
|
|
29
|
+
children: [
|
|
30
|
+
/* @__PURE__ */ jsxDEV(Section, {
|
|
31
|
+
style: styles.coverSection,
|
|
32
|
+
children: [
|
|
33
|
+
/* @__PURE__ */ jsxDEV(Section, {
|
|
34
|
+
style: styles.imageSection,
|
|
35
|
+
children: /* @__PURE__ */ jsxDEV(Row, {
|
|
36
|
+
children: /* @__PURE__ */ jsxDEV(Column, {
|
|
37
|
+
children: /* @__PURE__ */ jsxDEV(Text, {
|
|
38
|
+
style: {
|
|
39
|
+
color: "#fff",
|
|
40
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
|
41
|
+
fontSize: "28px",
|
|
42
|
+
margin: "24px 0",
|
|
43
|
+
fontWeight: "bold",
|
|
44
|
+
textAlign: "center",
|
|
45
|
+
lineHeight: "1.3em"
|
|
46
|
+
},
|
|
47
|
+
children: props.project.logo ?? props.project.name
|
|
48
|
+
}, undefined, false, undefined, this)
|
|
49
|
+
}, undefined, false, undefined, this)
|
|
50
|
+
}, undefined, false, undefined, this)
|
|
51
|
+
}, undefined, false, undefined, this),
|
|
52
|
+
/* @__PURE__ */ jsxDEV(Section, {
|
|
53
|
+
style: styles.upperSection,
|
|
54
|
+
children: props.children(styles)
|
|
55
|
+
}, undefined, false, undefined, this),
|
|
56
|
+
/* @__PURE__ */ jsxDEV(Hr, {}, undefined, false, undefined, this),
|
|
57
|
+
props.notice?.(styles) ?? /* @__PURE__ */ jsxDEV(Section, {
|
|
58
|
+
style: styles.lowerSection,
|
|
59
|
+
children: /* @__PURE__ */ jsxDEV(Text, {
|
|
60
|
+
style: styles.cautionText,
|
|
61
|
+
children: [
|
|
62
|
+
/* @__PURE__ */ jsxDEV("strong", {
|
|
63
|
+
children: props.project.name
|
|
64
|
+
}, undefined, false, undefined, this),
|
|
65
|
+
" service will never email you and ask you to disclose or verify your password, credit card, or banking account number."
|
|
66
|
+
]
|
|
67
|
+
}, undefined, true, undefined, this)
|
|
68
|
+
}, undefined, false, undefined, this)
|
|
69
|
+
]
|
|
70
|
+
}, undefined, true, undefined, this),
|
|
71
|
+
props.notificationSettings?.(styles) ?? /* @__PURE__ */ jsxDEV(Text, {
|
|
72
|
+
style: styles.footerText,
|
|
73
|
+
children: [
|
|
74
|
+
"You have received this message because you are using",
|
|
75
|
+
" ",
|
|
76
|
+
/* @__PURE__ */ jsxDEV("strong", {
|
|
77
|
+
children: props.project.name
|
|
78
|
+
}, undefined, false, undefined, this),
|
|
79
|
+
" service. If you don't want to receive these emails please",
|
|
80
|
+
" ",
|
|
81
|
+
/* @__PURE__ */ jsxDEV(Link, {
|
|
82
|
+
href: `${props.project.baseUri}/app/settings`,
|
|
83
|
+
target: "_blank",
|
|
84
|
+
style: styles.link,
|
|
85
|
+
children: "manage your notification setting"
|
|
86
|
+
}, undefined, false, undefined, this),
|
|
87
|
+
"."
|
|
88
|
+
]
|
|
89
|
+
}, undefined, true, undefined, this)
|
|
90
|
+
]
|
|
91
|
+
}, undefined, true, undefined, this)
|
|
92
|
+
]
|
|
93
|
+
}, undefined, true, undefined, this)
|
|
94
|
+
]
|
|
95
|
+
}, undefined, true, undefined, this);
|
|
96
|
+
}
|
|
97
|
+
var text = {
|
|
98
|
+
color: "#333",
|
|
99
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
|
100
|
+
fontSize: "14px",
|
|
101
|
+
margin: "24px 0"
|
|
102
|
+
};
|
|
103
|
+
var baseStyles = {
|
|
104
|
+
h1: {
|
|
105
|
+
color: "#333",
|
|
106
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
|
107
|
+
fontSize: "20px",
|
|
108
|
+
fontWeight: "bold",
|
|
109
|
+
marginBottom: "15px"
|
|
110
|
+
},
|
|
111
|
+
link: {
|
|
112
|
+
color: "#2754C5",
|
|
113
|
+
fontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
|
114
|
+
fontSize: "14px",
|
|
115
|
+
textDecoration: "underline"
|
|
116
|
+
},
|
|
117
|
+
text,
|
|
118
|
+
mainText: { ...text, marginBottom: "14px" },
|
|
119
|
+
main: {
|
|
120
|
+
backgroundColor: "#fff",
|
|
121
|
+
color: "#212121"
|
|
122
|
+
},
|
|
123
|
+
container: {
|
|
124
|
+
padding: "20px",
|
|
125
|
+
margin: "0 auto",
|
|
126
|
+
backgroundColor: "#eee"
|
|
127
|
+
},
|
|
128
|
+
imageSection: {
|
|
129
|
+
backgroundColor: "#252f3d",
|
|
130
|
+
padding: "20px 0"
|
|
131
|
+
},
|
|
132
|
+
coverSection: { backgroundColor: "#fff" },
|
|
133
|
+
upperSection: { padding: "25px 35px" },
|
|
134
|
+
lowerSection: { padding: "25px 35px" },
|
|
135
|
+
footerText: {
|
|
136
|
+
...text,
|
|
137
|
+
fontSize: "12px",
|
|
138
|
+
padding: "0 20px"
|
|
139
|
+
},
|
|
140
|
+
cautionText: { ...text, margin: "0px" },
|
|
141
|
+
button: {
|
|
142
|
+
fontSize: "14px",
|
|
143
|
+
backgroundColor: "#1976d2",
|
|
144
|
+
color: "#fff",
|
|
145
|
+
lineHeight: 1.5,
|
|
146
|
+
borderRadius: "0.5em",
|
|
147
|
+
padding: "12px 24px",
|
|
148
|
+
fontFamily: "Roboto,Arial,sans-serif",
|
|
149
|
+
margin: "30px auto"
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
// packages/service-email/src/template/render.ts
|
|
153
|
+
import { render, toPlainText } from "@react-email/components";
|
|
154
|
+
import { createElement } from "react";
|
|
155
|
+
async function renderEmail(props) {
|
|
156
|
+
const html = await render(createElement(props.template, props.props));
|
|
157
|
+
const text2 = toPlainText(html);
|
|
158
|
+
return {
|
|
159
|
+
html,
|
|
160
|
+
text: text2
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
export {
|
|
164
|
+
renderEmail,
|
|
165
|
+
BaseLayout
|
|
166
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@beesolve/email-service",
|
|
3
|
+
"description": "",
|
|
4
|
+
"version": "0.1.1",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"files": [
|
|
7
|
+
"dist"
|
|
8
|
+
],
|
|
9
|
+
"exports": {
|
|
10
|
+
"./cdk": {
|
|
11
|
+
"import": {
|
|
12
|
+
"types": "./dist/cdk.d.ts",
|
|
13
|
+
"default": "./dist/cdk.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"./sdk": {
|
|
17
|
+
"import": {
|
|
18
|
+
"types": "./dist/sdk.d.ts",
|
|
19
|
+
"default": "./dist/sdk.js"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"./templating": {
|
|
23
|
+
"import": {
|
|
24
|
+
"types": "./dist/templating.d.ts",
|
|
25
|
+
"default": "./dist/templating.js"
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
"./package.json": "./package.json"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/beesolve/packages/tree/main/packages/email-service#readme",
|
|
31
|
+
"license": "MIT",
|
|
32
|
+
"repository": {
|
|
33
|
+
"type": "git",
|
|
34
|
+
"url": "git+https://github.com/beesolve/packages.git"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"type-check": "tsc --noEmit"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"typescript": "^5.9.3"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"typescript": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"@aws-sdk/client-eventbridge": "^3.978.0",
|
|
49
|
+
"@aws-sdk/client-dynamodb": "^3.978.0",
|
|
50
|
+
"@aws-sdk/client-s3": "^3.978.0",
|
|
51
|
+
"@aws-sdk/client-ses": "^3.978.0",
|
|
52
|
+
"@aws-sdk/client-sqs": "^3.978.0",
|
|
53
|
+
"@aws-sdk/lib-dynamodb": "^3.978.0",
|
|
54
|
+
"@react-email/components": "1.0.6",
|
|
55
|
+
"aws-cdk-lib": "^2.236.0",
|
|
56
|
+
"constructs": "^10.4.5",
|
|
57
|
+
"mimetext": "^3.0.27",
|
|
58
|
+
"valibot": "^1.2.0",
|
|
59
|
+
"react": "^19.2.4",
|
|
60
|
+
"@beesolve/helpers": "0.1.1",
|
|
61
|
+
"@beesolve/cdk-constructs": "0.1.1"
|
|
62
|
+
},
|
|
63
|
+
"devDependencies": {
|
|
64
|
+
"@types/react": "^19.2.10"
|
|
65
|
+
}
|
|
66
|
+
}
|