UNPKG

@stacksjs/cloud

Version:

The Stacks cloud/serverless integration & implementation.

336 lines (294 loc) 11.1 kB
import type { Construct } from 'constructs'; import type { NestedCloudProps } from '../types'; export declare interface EmailStackProps extends NestedCloudProps { zone: route53.IHostedZone } export declare class EmailStack { emailBucket: s3.Bucket constructor(scope: Construct, props: EmailStackProps) { const bucketPrefix = `${props.slug}-${props.appEnv}` this.emailBucket = new s3.Bucket(scope, 'EmailBucket', { bucketName: `${bucketPrefix}-email-${props.timestamp}`, versioned: true, removalPolicy: RemovalPolicy.DESTROY, autoDeleteObjects: true, encryption: s3.BucketEncryption.S3_MANAGED, lifecycleRules: [ { id: '24h', enabled: true, expiration: Duration.days(1), noncurrentVersionExpiration: Duration.days(1), prefix: 'today/', }, { id: 'Intelligent transition for Inbox', enabled: true, prefix: 'inbox/', transitions: [ { storageClass: s3.StorageClass.INTELLIGENT_TIERING, transitionAfter: Duration.days(0), }, ], }, { id: 'Intelligent transition for Sent', enabled: true, prefix: 'sent/', transitions: [ { storageClass: s3.StorageClass.INTELLIGENT_TIERING, transitionAfter: Duration.days(0), }, ], }, ], }) Tags.of(this.emailBucket).add('daily-backup', 'true') const sesPrincipal = new iam.ServicePrincipal('ses.amazonaws.com') const ruleSetName = `${props.slug}-${props.appEnv}-email-receipt-rule-set` const receiptRuleName = `${props.slug}-${props.appEnv}-email-receipt-rule` const ruleSet = new ses.CfnReceiptRuleSet(scope, 'SESReceiptRuleSet', { ruleSetName, }) this.emailBucket.addToResourcePolicy( new iam.PolicyStatement({ sid: 'AllowSESPuts', effect: iam.Effect.ALLOW, principals: [sesPrincipal], actions: ['s3:PutObject'], resources: [`${this.emailBucket.bucketArn}/*`], conditions: { StringEquals: { 'aws:SourceAccount': Stack.of(scope).account, }, ArnLike: { 'aws:SourceArn': `arn:aws:ses:${Stack.of(scope).region}:${ Stack.of(scope).account }:receipt-rule-set/${ruleSetName}:receipt-rule/${receiptRuleName}`, }, }, }), ) const receiptRule = new ses.CfnReceiptRule(scope, 'SESReceiptRule', { ruleSetName: ruleSet.ref, rule: { name: receiptRuleName, enabled: true, actions: [ { s3Action: { bucketName: this.emailBucket.bucketName, objectKeyPrefix: 'tmp/email_in/', }, }, ], recipients: config.email.mailboxes || [], scanEnabled: config.email.server?.scan || true, tlsPolicy: 'Require', }, }) receiptRule.node.addDependency(this.emailBucket) const iamGroup = new iam.Group(scope, 'IAMGroup', { groupName: `${props.slug}-${props.appEnv}-email-management-s3-group`, }) const listBucketsPolicyStatement = new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['s3:ListAllMyBuckets'], resources: ['*'], }) const policyStatement = new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: [ 's3:ListBucket', 's3:GetObject', 's3:PutObject', 's3:DeleteObject', 's3:GetObjectAcl', 's3:GetObjectVersionAcl', 's3:PutObjectAcl', 's3:PutObjectVersionAcl', ], resources: [this.emailBucket.bucketArn, `${this.emailBucket.bucketArn}/*`], }) const policy = new iam.Policy(scope, 'EmailAccessPolicy', { policyName: `${props.slug}-${props.appEnv}-email-management-s3-policy`, statements: [policyStatement, listBucketsPolicyStatement], }) iamGroup.attachInlinePolicy(policy) const sesIdentity = new ses.CfnEmailIdentity(scope, 'DomainIdentity', { emailIdentity: props.domain, dkimSigningAttributes: { nextSigningKeyLength: 'RSA_2048_BIT', }, dkimAttributes: { signingEnabled: true, }, mailFromAttributes: { behaviorOnMxFailure: 'USE_DEFAULT_VALUE', mailFromDomain: `mail.${props.domain}`, }, feedbackAttributes: { emailForwardingEnabled: true, }, }) new route53.CfnRecordSet(scope, 'DkimRecord1', { hostedZoneName: `${props.zone.zoneName}.`, name: sesIdentity.attrDkimDnsTokenName1, type: 'CNAME', resourceRecords: [sesIdentity.attrDkimDnsTokenValue1], ttl: '1800', }) new route53.CfnRecordSet(scope, 'DkimRecord2', { hostedZoneName: `${props.zone.zoneName}.`, name: sesIdentity.attrDkimDnsTokenName2, type: 'CNAME', resourceRecords: [sesIdentity.attrDkimDnsTokenValue2], ttl: '1800', }) new route53.CfnRecordSet(scope, 'DkimRecord3', { hostedZoneName: `${props.zone.zoneName}.`, name: sesIdentity.attrDkimDnsTokenName3, type: 'CNAME', resourceRecords: [sesIdentity.attrDkimDnsTokenValue3], ttl: '1800', }) new route53.MxRecord(scope, 'MxRecord', { zone: props.zone, recordName: 'mail', values: [ { priority: 10, hostName: 'feedback-smtp.us-east-1.amazonses.com', }, ], }) new route53.TxtRecord(scope, 'TxtSpfRecord', { zone: props.zone, recordName: 'mail', values: ['v=spf1 include:amazonses.com ~all'], }) new route53.TxtRecord(scope, 'TxtDmarcRecord', { zone: props.zone, recordName: '_dmarc', values: [`v=DMARC1;p=quarantine;pct=25;rua=mailto:dmarcreports@${props.domain}`], }) const lambdaEmailOutboundRole = new iam.Role(scope, 'LambdaEmailOutboundRole', { roleName: `${props.slug}-${props.appEnv}-email-outbound`, assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')], }) const lambdaEmailOutbound = new lambda.Function(scope, 'LambdaEmailOutbound', { functionName: `${props.slug}-${props.appEnv}-email-outbound`, description: 'Take the JSON and convert it in to an raw email.', code: lambda.Code.fromInline('exports.handler = async (event) => {return true;};'), handler: 'index.handler', memorySize: 256, runtime: lambda.Runtime.NODEJS_18_X, timeout: Duration.seconds(60), environment: { BUCKET: this.emailBucket.bucketName, }, role: lambdaEmailOutboundRole, }) lambdaEmailOutboundRole.addToPolicy(policyStatement) const sesPolicyStatement = new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['ses:SendRawEmail'], resources: ['*'], }) lambdaEmailOutboundRole.addToPolicy(sesPolicyStatement) const lambdaEmailInboundRole = new iam.Role(scope, 'LambdaEmailInboundRole', { roleName: `${props.slug}-${props.appEnv}-email-inbound`, assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')], }) const lambdaEmailInbound = new lambda.Function(scope, 'LambdaEmailInbound', { functionName: `${props.slug}-${props.appEnv}-email-inbound`, description: 'This Lambda organizes all the incoming emails based on the From and To field.', code: lambda.Code.fromInline('exports.handler = async (event) => {return true;};'), handler: 'index.handler', memorySize: 256, role: lambdaEmailInboundRole, runtime: lambda.Runtime.NODEJS_18_X, timeout: Duration.seconds(60), environment: { BUCKET: this.emailBucket.bucketName, }, }) new lambda.CfnPermission(scope, 'S3InboundPermission', { action: 'lambda:InvokeFunction', functionName: lambdaEmailInbound.functionName, principal: 's3.amazonaws.com', }) const inboundS3PolicyStatement = new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['s3:*'], resources: [this.emailBucket.bucketArn, `${this.emailBucket.bucketArn}/*`], }) lambdaEmailInboundRole.addToPolicy(inboundS3PolicyStatement) const sesInboundPolicyStatement = new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['ses:ListIdentities'], resources: ['*'], }) lambdaEmailInboundRole.addToPolicy(sesInboundPolicyStatement) const lambdaEmailConverterRole = new iam.Role(scope, 'LambdaEmailConverterRole', { roleName: `${props.slug}-${props.appEnv}-email-converter`, assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), managedPolicies: [iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole')], }) const lambdaEmailConverter = new lambda.Function(scope, 'LambdaEmailConverter', { functionName: `${props.slug}-${props.appEnv}-email-converter`, description: 'This Lambda converts raw emails files in to HTML and text.', code: lambda.Code.fromInline( 'exports.handler = async (event) => {console.log("hello world email converter");return true;};', ), handler: 'index.handler', memorySize: 256, role: lambdaEmailConverterRole, runtime: lambda.Runtime.NODEJS_18_X, timeout: Duration.seconds(60), environment: { BUCKET: this.emailBucket.bucketName, }, }) const converterS3PolicyStatement = new iam.PolicyStatement({ effect: iam.Effect.ALLOW, actions: ['s3:*'], resources: [this.emailBucket.bucketArn, `${this.emailBucket.bucketArn}/*`], }) new lambda.CfnPermission(scope, 'S3ConverterPermission', { action: 'lambda:InvokeFunction', functionName: lambdaEmailConverter.functionName, principal: 's3.amazonaws.com', }) lambdaEmailConverterRole.addToPolicy(converterS3PolicyStatement) this.emailBucket.addEventNotification( s3.EventType.OBJECT_CREATED_PUT, new s3n.LambdaDestination(lambdaEmailInbound), { prefix: 'tmp/email_in/' }, ) this.emailBucket.addEventNotification( s3.EventType.OBJECT_CREATED_PUT, new s3n.LambdaDestination(lambdaEmailOutbound), { prefix: 'tmp/email_out/json/' }, ) this.emailBucket.addEventNotification( s3.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: 'sent/' }, ) this.emailBucket.addEventNotification( s3.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: 'inbox/' }, ) this.emailBucket.addEventNotification( s3.EventType.OBJECT_CREATED_COPY, new s3n.LambdaDestination(lambdaEmailConverter), { prefix: 'today/' }, ) } }