Repository: aws-samples/amazon-textract-idp-cdk-constructs (language: python)

Issue:
feat: construct that connects Email (SES) -> s3 
### Description

- Create SES construct
- Constructs S3 bucket and outputs references
- Create SES rule to call lambda to parse email attachment and save it to S3



### References
- https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_ses-readme.html
- https://docs.aws.amazon.com/solutions/latest/constructs/aws-s3-lambda.html


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 10834bd..4f0e361 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,6 +37,7 @@ __pycache__
 .nvmrc
 .vscode
 update_dependencies_local.sh
+.idea
 !/.projenrc.js
 /test-reports/
 junit.xml
diff --git a/.projenrc.js b/.projenrc.js
index 09b76bb..ba866a5 100644
--- a/.projenrc.js
+++ b/.projenrc.js
@@ -6,7 +6,7 @@ const project = new awscdk.AwsCdkConstructLibrary({
   defaultReleaseBranch: 'main',
   name: 'amazon-textract-idp-cdk-constructs',
   repositoryUrl: 'https://github.com/aws-samples/amazon-textract-idp-cdk-constructs.git',
-  gitignore: ['test/__snapshots__/', '__pycache__', '.python-version', '.aws-sam', '.nvmrc', '.vscode', 'update_dependencies_local.sh'],
+  gitignore: ['test/__snapshots__/', '__pycache__', '.python-version', '.aws-sam', '.nvmrc', '.vscode', 'update_dependencies_local.sh', '.idea'],
   devDeps: ['aws-cdk-lib@^2.1.0'],
   peerDeps: ['aws-cdk-lib@^2.1.0'],
   keywords: ['aws-cdk', 'schadem', 'textract', 'amazon-textract', 'idp'],
diff --git a/API.md b/API.md
index 404e98c..828ec5b 100644
--- a/API.md
+++ b/API.md
@@ -5262,6 +5262,116 @@ public readonly deciderFunction: IFunction;
 ---
 
 
+### WorkmailS3IngestionPoint <a name="WorkmailS3IngestionPoint" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPoint"></a>
+
+#### Initializers <a name="Initializers" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPoint.Initializer"></a>
+
+```typescript
+import { WorkmailS3IngestionPoint } from 'amazon-textract-idp-cdk-constructs'
+
+new WorkmailS3IngestionPoint(scope: Construct, id: string, props: WorkmailS3IngestionPointProps)
+```
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| <code><a href="#amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPoint.Initializer.parameter.scope">scope</a></code> | <code>constructs.Construct</code> | *No description.* |
+| <code><a href="#amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPoint.Initializer.parameter.id">id</a></code> | <code>string</code> | *No description.* |
+| <code><a href="#amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPoint.Initializer.parameter.props">props</a></code> | <code><a href="#amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps">WorkmailS3IngestionPointProps</a></code> | *No description.* |
+
+---
+
+##### `scope`<sup>Required</sup> <a name="scope" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPoint.Initializer.parameter.scope"></a>
+
+- *Type:* constructs.Construct
+
+---
+
+##### `id`<sup>Required</sup> <a name="id" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPoint.Initializer.parameter.id"></a>
+
+- *Type:* string
+
+---
+
+##### `props`<sup>Required</sup> <a name="props" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPoint.Initializer.parameter.props"></a>
+
+- *Type:* <a href="#amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps">WorkmailS3IngestionPointProps</a>
+
+---
+
+#### Methods <a name="Methods" id="Methods"></a>
+
+| **Name** | **Description** |
+| --- | --- |
+| <code><a href="#amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPoint.toString">toString</a></code> | Returns a string representation of this construct. |
+
+---
+
+##### `toString` <a name="toString" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPoint.toString"></a>
+
+```typescript
+public toString(): string
+```
+
+Returns a string representation of this construct.
+
+#### Static Functions <a name="Static Functions" id="Static Functions"></a>
+
+| **Name** | **Description** |
+| --- | --- |
+| <code><a href="#amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPoint.isConstruct">isConstruct</a></code> | Checks if `x` is a construct. |
+
+---
+
+##### ~~`isConstruct`~~ <a name="isConstruct" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPoint.isConstruct"></a>
+
+```typescript
+import { WorkmailS3IngestionPoint } from 'amazon-textract-idp-cdk-constructs'
+
+WorkmailS3IngestionPoint.isConstruct(x: any)
+```
+
+Checks if `x` is a construct.
+
+###### `x`<sup>Required</sup> <a name="x" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPoint.isConstruct.parameter.x"></a>
+
+- *Type:* any
+
+Any object.
+
+---
+
+#### Properties <a name="Properties" id="Properties"></a>
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| <code><a href="#amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPoint.property.node">node</a></code> | <code>constructs.Node</code> | The tree node. |
+| <code><a href="#amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPoint.property.props">props</a></code> | <code><a href="#amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps">WorkmailS3IngestionPointProps</a></code> | *No description.* |
+
+---
+
+##### `node`<sup>Required</sup> <a name="node" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPoint.property.node"></a>
+
+```typescript
+public readonly node: Node;
+```
+
+- *Type:* constructs.Node
+
+The tree node.
+
+---
+
+##### `props`<sup>Required</sup> <a name="props" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPoint.property.props"></a>
+
+```typescript
+public readonly props: WorkmailS3IngestionPointProps;
+```
+
+- *Type:* <a href="#amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps">WorkmailS3IngestionPointProps</a>
+
+---
+
+
 ## Structs <a name="Structs" id="Structs"></a>
 
 ### ComprehendGenericSyncSfnTaskProps <a name="ComprehendGenericSyncSfnTaskProps" id="amazon-textract-idp-cdk-constructs.ComprehendGenericSyncSfnTaskProps"></a>
@@ -7999,5 +8109,100 @@ public readonly workflowTracingEnabled: boolean;
 
 ---
 
+### WorkmailS3IngestionPointProps <a name="WorkmailS3IngestionPointProps" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps"></a>
+
+#### Initializer <a name="Initializer" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps.Initializer"></a>
+
+```typescript
+import { WorkmailS3IngestionPointProps } from 'amazon-textract-idp-cdk-constructs'
+
+const workmailS3IngestionPointProps: WorkmailS3IngestionPointProps = { ... }
+```
+
+#### Properties <a name="Properties" id="Properties"></a>
+
+| **Name** | **Type** | **Description** |
+| --- | --- | --- |
+| <code><a href="#amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps.property.s3OutputBucket">s3OutputBucket</a></code> | <code>string</code> | Bucket name to output data to. |
+| <code><a href="#amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps.property.s3OutputPrefix">s3OutputPrefix</a></code> | <code>string</code> | The prefix to use to output files to. |
+| <code><a href="#amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps.property.workmailAccountNumber">workmailAccountNumber</a></code> | <code>string</code> | Account number for WorkMail instance. |
+| <code><a href="#amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps.property.workmailRegion">workmailRegion</a></code> | <code>string</code> | Region where WorkMailail instance exists. |
+| <code><a href="#amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps.property.lambdaMemoryMB">lambdaMemoryMB</a></code> | <code>number</code> | Lambda function memory configuration (may need to increase for larger documents). |
+| <code><a href="#amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps.property.lambdaTimeout">lambdaTimeout</a></code> | <code>number</code> | Lambda function timeout (may need to increase for larger documents). |
+
+---
+
+##### `s3OutputBucket`<sup>Required</sup> <a name="s3OutputBucket" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps.property.s3OutputBucket"></a>
+
+```typescript
+public readonly s3OutputBucket: string;
+```
+
+- *Type:* string
+
+Bucket name to output data to.
+
+---
+
+##### `s3OutputPrefix`<sup>Required</sup> <a name="s3OutputPrefix" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps.property.s3OutputPrefix"></a>
+
+```typescript
+public readonly s3OutputPrefix: string;
+```
+
+- *Type:* string
+
+The prefix to use to output files to.
+
+---
+
+##### `workmailAccountNumber`<sup>Required</sup> <a name="workmailAccountNumber" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps.property.workmailAccountNumber"></a>
+
+```typescript
+public readonly workmailAccountNumber: string;
+```
+
+- *Type:* string
+
+Account number for WorkMail instance.
+
+---
+
+##### `workmailRegion`<sup>Required</sup> <a name="workmailRegion" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps.property.workmailRegion"></a>
+
+```typescript
+public readonly workmailRegion: string;
+```
+
+- *Type:* string
+
+Region where WorkMailail instance exists.
+
+---
+
+##### `lambdaMemoryMB`<sup>Optional</sup> <a name="lambdaMemoryMB" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps.property.lambdaMemoryMB"></a>
+
+```typescript
+public readonly lambdaMemoryMB: number;
+```
+
+- *Type:* number
+
+Lambda function memory configuration (may need to increase for larger documents).
+
+---
+
+##### `lambdaTimeout`<sup>Optional</sup> <a name="lambdaTimeout" id="amazon-textract-idp-cdk-constructs.WorkmailS3IngestionPointProps.property.lambdaTimeout"></a>
+
+```typescript
+public readonly lambdaTimeout: number;
+```
+
+- *Type:* number
+
+Lambda function timeout (may need to increase for larger documents).
+
+---
+
 
 
diff --git a/README.md b/README.md
index 774805d..165f13c 100644
--- a/README.md
+++ b/README.md
@@ -66,3 +66,33 @@ Expect a Spacy textcat model at the root of the directory. Call the script <TO_I
 
 aws s3 cp s3://amazon-textract-public-content/constructs/en_textcat_demo-0.0.0.tar.gz .
 
+
+### How to use Workmail Integration
+
+In order to demonstrate this functionality, I have used below architecture where once the inbound email is delivered to your Amazon workmail inbox and if the pattern/s matches, it will invoke the rule action which is inovocation of a lambda function in this case. You can use my sample code to fetch the inbound email message body and parse it properly as text.
+
+![architecture](./images/Workmail_Lambda.png)
+
+
+### Prerequisites
+
+1. As I have used Python 3.6 as my Lambda function runtime hence some knowledge of python 3 version is required.
+
+### Steps
+
+1. First setup an Amazon workmail site, setup an organization and create a user access by following steps mentioned in 'Getting Started' document [here](https://docs.aws.amazon.com/workmail/latest/adminguide/howto-start.html). Once above setup process is done, you will have access to https://*your Organization*.awsapps.com/mail webmail url and you can login using your created user's username / password to access your emails.
+
+2. Now we will create a lambda function which will be invoked once inbound email reaches the inbox and email flow rule pattern is matched (more on this in below steps). You can use the sample lambda python(3.6) code ( lambda_function.py) provided in the 'code' folder for the same. It will fetch the inbound email message body and then parse it properly to get the message body as text. Once you get it as text you can perform various operations on it.
+
+
+3. Inbound email flow rules, also called rule actions, automatically apply to all email messages sent to anyone inside of the Amazon WorkMail organization. This differs from email rules for individual mailboxes. Now we will set up email flow rules to handle email flows based on email addresses or domains. Email flow rules are based on both the sender's and recipient's email addresses or domains.
+
+To create an email flow rule, we need to specify a rule action to apply to an email when a specified pattern is matched. Follow the documenttion link [here](https://docs.aws.amazon.com/workmail/latest/adminguide/email-flows.html#email-flows-rule-actions) to create email flow rule for your organization which you created in step #1 above. you have to select Action=Run Lambda for your rule. Below is the email flow rule created by me:
+
+![Email Flow Rule](./images/email_rule_1.png)
+
+you can now follow documentation link [here](https://docs.aws.amazon.com/workmail/latest/adminguide/email-flows.html#email-flows-patterns) to create pattern/s which need to be satisfied first in order to invoke the rule action (in this case it will invoke our lambda function). For this sample code functionality I have used my email address as pattern in 'origns' and my domain as pattern in 'destinations'. so in this case the lambda function will only be invoke if inbound email sender is my email address and destination is my domain only but you can set patterns as per your requirements. Below screen shots depicts my patterns:
+
+![Origin pattern](./images/email_rule_2.png)
+
+![Destnation pattern](./images/email_rule_3.png)
diff --git a/images/Workmail_Lambda.png b/images/Workmail_Lambda.png
new file mode 100644
index 0000000..e28f218
Binary files /dev/null and b/images/Workmail_Lambda.png differ
diff --git a/images/email_rule_1.png b/images/email_rule_1.png
new file mode 100644
index 0000000..657e1de
Binary files /dev/null and b/images/email_rule_1.png differ
diff --git a/images/email_rule_2.png b/images/email_rule_2.png
new file mode 100644
index 0000000..e010955
Binary files /dev/null and b/images/email_rule_2.png differ
diff --git a/images/email_rule_3.png b/images/email_rule_3.png
new file mode 100644
index 0000000..f431397
Binary files /dev/null and b/images/email_rule_3.png differ
diff --git a/lambda/textract_sync/env.json b/lambda/textract_sync/env.json
index d87a7c4..d0b5750 100644
--- a/lambda/textract_sync/env.json
+++ b/lambda/textract_sync/env.json
@@ -1,4 +1,5 @@
-{
+
+
     "HelloWorldFunction": {
         "S3_OUTPUT_PREFIX": "textract-output",
         "S3_OUTPUT_BUCKET": "schademcdkstackpaystubst-schademcdkidpstackpython-1fvi0dqoz24lj",
diff --git a/lambda/workmail_s3/Dockerfile b/lambda/workmail_s3/Dockerfile
new file mode 100644
index 0000000..9ae59f4
--- /dev/null
+++ b/lambda/workmail_s3/Dockerfile
@@ -0,0 +1,9 @@
+FROM public.ecr.aws/lambda/python:3.9-x86_64
+
+RUN /var/lang/bin/python -m pip install --upgrade pip
+
+# Copy function code
+COPY app/* ${LAMBDA_TASK_ROOT}/
+
+# Set the CMD to your handler (could also be done as a parameter override outside of the Dockerfile)
+CMD [ "main.lambda_handler" ]
diff --git a/lambda/workmail_s3/app/main.py b/lambda/workmail_s3/app/main.py
new file mode 100644
index 0000000..e315dfb
--- /dev/null
+++ b/lambda/workmail_s3/app/main.py
@@ -0,0 +1,94 @@
+from email.message import Message
+from botocore.exceptions import ClientError
+import boto3
+import email
+import os
+import uuid
+from email import policy
+from base64 import b64decode
+
+workmail_message_flow = boto3.client('workmailmessageflow')
+s3 = boto3.client('s3')
+
+def download_email(message_id):
+    """
+    This method downloads full email MIME content using GetRawMessageContent API and uses email.parser class
+    for parsing it into Python email.message.EmailMessage class.
+    Reference:
+        https://docs.python.org/3.7/library/email.message.html#email.message.EmailMessage
+        https://docs.python.org/3/library/email.parser.html
+    Parameters
+    ----------
+    message_id: string, required
+        message_id of the email to download
+    Returns
+    -------
+    email.message.Message
+        EmailMessage representation the downloaded email
+    """
+    response = workmail_message_flow.get_raw_message_content(messageId=message_id)
+    email_content = response['messageContent'].read()
+    email_generation_policy = policy.SMTP.clone(refold_source='none')
+    print("Downloaded email from WorkMail successfully")
+    return email.message_from_bytes(email_content, policy=email_generation_policy)
+
+
+def lambda_handler(event, context):
+    from_address = event['envelope']['mailFrom']['address']
+    subject = event['subject']
+    flow_direction = event['flowDirection']
+    message_id = event['messageId']
+    print(f"Received email with message ID {message_id}, flowDirection {flow_direction}, from {from_address} with Subject {subject}")
+
+    try:
+        raw_msg = workmail_message_flow.get_raw_message_content(messageId=message_id)
+        parsed_msg: Message = email.message_from_bytes(raw_msg['messageContent'].read())
+
+        parsed_email = download_email(message_id)
+        key = str(uuid.uuid4())
+        s3_output_bucket = os.environ.get('S3_OUTPUT_BUCKET')
+        s3_output_prefix = os.environ.get('S3_OUTPUT_PREFIX')
+
+        if not s3_output_bucket or not s3_output_prefix:
+            raise ValueError(
+                f"no s3_output_bucket: {s3_output_bucket} or s3_output_prefix: {s3_output_prefix} defined."
+            )
+        logger.debug(f"LOG_LEVEL: {log_level} \n \
+                    S3_OUTPUT_BUCKET: {s3_output_bucket} \n \
+                    S3_OUTPUT_PREFIX: {s3_output_prefix} \n \
+                    TEXTRACT_API: {textract_api} \n  ")
+
+
+        # Can take in multiple pdf attachments
+        if parsed_email.is_multipart():
+            # Walk over message parts of this multipart email.
+            for part in parsed_email.walk():
+                content_type = part.get_content_type()
+                content_disposition = str(part.get_content_disposition())
+                file_name = part.get_filename()
+                if 'attachment' in content_disposition and part.is_attachment() and "application/pdf" in part.get_content_type():
+                    s3.put_object(Body=bytes(json.dumps(full_json, indent=4).encode('UTF-8'), Bucket=s3_output_bucket, Key=file_name))
+
+                    print('Finished upload to S3 ' , fp)
+
+    except ClientError as e:
+        if e.response['Error']['Code'] == 'MessageFrozen':
+            # Redirect emails are not eligible for update, handle it gracefully.
+            print(f"Message {message_id} is not eligible for update. This is usually the case for a redirected email")
+        else:
+            # Send some context about this error to Lambda Logs
+            print(e)
+            if e.response['Error']['Code'] == 'ResourceNotFoundException':
+                print(f"Message {message_id} does not exist. Messages in transit are no longer accessible after 1 day")
+            elif e.response['Error']['Code'] == 'InvalidContentLocation':
+                print('WorkMail could not access the updated email content. See https://docs.aws.amazon.com/workmail/latest/adminguide/update-with-lambda.html')
+            raise(e)
+
+    return {
+        'actions': [
+            {
+                'allRecipients': True,  # For all recipients
+                'action': {'type': 'DEFAULT'}  # let the email be sent normally
+            }
+        ]
+    }
diff --git a/lambda/workmail_s3/app/requirements.txt b/lambda/workmail_s3/app/requirements.txt
new file mode 100644
index 0000000..e69de29
diff --git a/src/index.ts b/src/index.ts
index 1af7b95..87f2d49 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -22,7 +22,7 @@ export { ComprehendGenericSyncSfnTask, ComprehendGenericSyncSfnTaskProps } from
 export { SpacySfnTask, SpacySfnTaskProps } from './spacyClassification';
 export { DocumentSplitter, DocumentSplitterProps } from './documentSplitter';
 export { RDSAuroraServerless, RDSAuroraServerlessProps } from './rdsAuroraServerless';
-
+export { WorkmailS3IngestionPoint, WorkmailS3IngestionPointProps } from './workmailS3IngestionPoint';
 
 export function validatePatternSupported(integrationPattern: sfn.IntegrationPattern, supportedPatterns: sfn.IntegrationPattern[]) {
   if (! supportedPatterns.includes(integrationPattern)) {
diff --git a/src/workmailS3IngestionPoint.ts b/src/workmailS3IngestionPoint.ts
new file mode 100644
index 0000000..357818e
--- /dev/null
+++ b/src/workmailS3IngestionPoint.ts
@@ -0,0 +1,100 @@
+import * as path from 'path';
+import { Duration } from 'aws-cdk-lib';
+import { IRole, ManagedPolicy, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam';
+import { CfnPermission, DockerImageFunction, Function } from 'aws-cdk-lib/aws-lambda';
+import * as lambda from 'aws-cdk-lib/aws-lambda';
+import { Construct } from 'constructs';
+
+export interface WorkmailS3IngestionPointProps {
+  /** Bucket name to output data to */
+  readonly s3OutputBucket:string;
+  /** The prefix to use to output files to */
+  readonly s3OutputPrefix:string;
+  /** Region where WorkMailail instance exists */
+  readonly workmailRegion: string;
+  /** Account number for WorkMail instance */
+  readonly workmailAccountNumber: string;
+  /** Lambda function memory configuration (may need to increase for larger documents) */
+  readonly lambdaMemoryMB?: number;
+  /** Lambda function timeout (may need to increase for larger documents) */
+  readonly lambdaTimeout?: number;
+}
+
+export class WorkmailS3IngestionPoint extends Construct {
+  private workmailAccountNumber: string;
+  private workmailRegion: string;
+  private s3OutputPrefix: string;
+  private s3OutputBucket: string;
+  constructor(scope: Construct, id: string, readonly props: WorkmailS3IngestionPointProps) {
+    super(scope, id);
+    const { workmailRegion, workmailAccountNumber, s3OutputBucket, s3OutputPrefix } = props;
+    if (!workmailAccountNumber || !workmailRegion || !s3OutputBucket || !s3OutputPrefix) {
+      throw new Error('workmailAccountNumber and workmailRegion are required');
+    }
+    this.workmailAccountNumber = workmailAccountNumber;
+    this.workmailRegion = workmailRegion;
+    this.s3OutputBucket = props.s3OutputBucket;
+    this.s3OutputPrefix = props.s3OutputPrefix;
+
+    const WorkMailARN = `arn:aws:workmailmessageflow:${this.workmailRegion}:${this.workmailAccountNumber}:message/*/*/*`;
+
+    const lambdaMemoryMB = props?.lambdaMemoryMB === undefined ? 10240 : props.lambdaMemoryMB;
+    const lambdaTimeout = props?.lambdaTimeout === undefined ? 900 : props.lambdaTimeout;
+
+    const workMailGetMsgPolicy = new PolicyDocument({
+      statements: [
+        new PolicyStatement({
+          actions: ['workmailmessageflow:GetRawMessageContent'],
+          resources: [WorkMailARN],
+        }),
+        new PolicyStatement({
+          resources: [
+            path.join(`arn:aws:s3:::${this.s3OutputBucket}`, '/*'),
+            path.join(`arn:aws:s3:::${this.s3OutputBucket}`, this.s3OutputPrefix, '/*'),
+          ],
+          actions: [
+            's3:Object',
+            's3:PutObject',
+            's3:PutObjectAcl',
+          ],
+        }),
+      ],
+    });
+
+    const workmailServicesRole: IRole = new Role(this, 'WorkmailServicesRole', {
+      assumedBy: new ServicePrincipal('amazonaws.com'),
+      inlinePolicies: {
+        WorkMailGetMsgPolicy: workMailGetMsgPolicy,
+      },
+      managedPolicies: [
+        ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'),
+      ],
+    });
+
+    /**
+     * Lambda to download files from workMail and insert them into S3
+     */
+    const downloadPdfToS3Lambda: Function = new DockerImageFunction(
+      this,
+      'downloadPdfToS3Lambda',
+      {
+        code: lambda.DockerImageCode.fromImageAsset(path.join(__dirname, '../lambda/workmail_s3/')),
+        memorySize: lambdaMemoryMB,
+        architecture: lambda.Architecture.X86_64,
+        timeout: Duration.seconds(lambdaTimeout),
+        environment: {
+          S3_OUTPUT_BUCKET: this.s3OutputBucket,
+          S3_OUTPUT_PREFIX: this.s3OutputPrefix,
+        },
+        role: workmailServicesRole,
+      },
+    );
+
+    new CfnPermission(this, 'workmailPermission', {
+      action: 'lambda:InvokeFunction',
+      functionName: downloadPdfToS3Lambda.functionName,
+      principal: `workmail.${this.node.tryGetContext('region')}.amazonaws.com`,
+    });
+
+  }
+}


Test patch:
diff --git a/lambda/textract_sync/test_sam_local.sh b/lambda/textract_sync/test_sam_local.sh
old mode 100755
new mode 100644
index d3b6ae9..18d27b6
--- a/lambda/textract_sync/test_sam_local.sh
+++ b/lambda/textract_sync/test_sam_local.sh
@@ -1,2 +1,1 @@
-sam build
-sam local invoke -e events/event.json -n env.json
+sam build
\ No newline at end of file
diff --git a/test/workmailS3IngestionPoint.test.ts b/test/workmailS3IngestionPoint.test.ts
new file mode 100644
index 0000000..7d3cb56
--- /dev/null
+++ b/test/workmailS3IngestionPoint.test.ts
@@ -0,0 +1,22 @@
+import { Stack } from 'aws-cdk-lib';
+import { Template } from 'aws-cdk-lib/assertions';
+import { WorkmailS3IngestionPoint } from '../src';
+
+let stack: Stack;
+beforeEach(() => {
+  stack = new Stack();
+});
+
+describe('IntegrationWorkmailTest', () => {
+  test('Workmail Test', () => {
+    new WorkmailS3IngestionPoint(stack, 'idp-classification', {
+      s3OutputPrefix: 'sometempoutputprefix',
+      workmailAccountNumber: 'someWorkmailAccountNumber',
+      workmailRegion: 'us-east-1',
+      s3OutputBucket: 'somebucket',
+    });
+    expect(Template.fromStack(stack).toJSON()).toMatchSnapshot();
+    const template = Template.fromStack(stack);
+    template.resourceCountIs('AWS::Lambda::Function', 1);
+  });
+});