├── .github └── PULL_REQUEST_TEMPLATE.md ├── .travis.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── NOTICE ├── README.md ├── requirements.txt ├── workmail-cap-exchange ├── README.md ├── dependencies │ └── requirements.txt ├── exchange_secrets.json ├── src │ ├── app.py │ ├── ews.py │ ├── params.py │ └── secretsmanager.py ├── template.yaml └── tst │ ├── env_vars.json │ └── lambda_query_availability.json ├── workmail-chat-bot-python ├── README.md ├── dependencies │ └── requirements.txt ├── src │ ├── app.py │ └── utils.py ├── template.yaml └── tst │ ├── env_vars.json │ └── event.json ├── workmail-hello-world-python ├── README.md ├── dependencies │ └── requirements.txt ├── src │ └── app.py ├── template.yaml └── tst │ ├── env_vars.json │ └── event.json ├── workmail-message-flow-state-machine ├── .gitignore ├── README.md ├── src │ └── app.py ├── template.yaml └── tst │ ├── env_vars.json │ └── event.json ├── workmail-restricted-mailboxes-python ├── README.md ├── src │ ├── app.py │ └── utils.py ├── template.yaml └── tst │ ├── env_vars.json │ └── event.json ├── workmail-salesforce-python ├── README.md ├── dependencies │ └── requirements.txt ├── src │ ├── app.py │ ├── email_utils.py │ └── sf_utils.py ├── template.yaml └── tst │ ├── env_vars.json │ └── event.json ├── workmail-save-and-update-email ├── README.md ├── dependencies │ └── requirements.txt ├── src │ ├── app.py │ └── utils.py ├── template.yaml ├── tst │ ├── env_vars.json │ └── event.json └── workmail-save-and-update-email.jpg ├── workmail-stop-mail-storm ├── README.md ├── src │ ├── app.js │ └── package.json ├── template.yaml └── tst │ ├── environment_variables.json │ └── event.json ├── workmail-translate-email ├── Image.png ├── README.md ├── dependencies │ └── requirements.txt ├── src │ ├── app.py │ ├── translate_helper.py │ └── utils.py ├── template.yaml └── tst │ ├── env_vars.json │ └── event.json ├── workmail-update-email ├── Image.png ├── README.md ├── dependencies │ └── requirements.txt ├── src │ ├── app.py │ └── utils.py ├── template.yaml └── tst │ ├── env_vars.json │ └── event.json ├── workmail-upstream-gateway-filter ├── README.md ├── src │ └── app.py ├── template.yaml └── tst │ ├── env_vars.json │ └── event.json ├── workmail-wordpress-python ├── README.md ├── dependencies │ └── requirements.txt ├── src │ ├── app.py │ └── utils.py ├── template.yaml └── tst │ ├── env_vars.json │ └── event.json └── workmail-ws1-integration ├── README.md ├── src ├── app.py ├── exceptions.py ├── params.py ├── secretsmanager.py ├── utils.py ├── workmail.py └── ws1.py ├── template.yaml ├── tst ├── env_vars.json ├── http_event_notification.json ├── lambda_event_notification.json └── lambda_test_connection.json ├── wm-ws1-integration-diagram.png └── ws1creds.json /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license. 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | matrix: 2 | include: 3 | - language: python 4 | python: 5 | - "3.12" 6 | install: 7 | - pip install -r requirements.txt 8 | script: 9 | - python -m pyflakes . 10 | 11 | - language: node_js 12 | node_js: 13 | - 22 14 | before_install: 15 | - cd workmail-stop-mail-storm/src 16 | install: 17 | - npm install 18 | script: 19 | - npm run jshint 20 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-samples/amazon-workmail-lambdas-templates/issues), or [recently closed](https://github.com/aws-samples/amazon-workmail-lambdas-templates/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), 45 | looking at any ['help wanted'](https://github.com/aws-samples/amazon-workmail-lambdas-templates/labels/help%20wanted) issues is a great place to start. 46 | 47 | 48 | ## Code of Conduct 49 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 50 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 51 | opensource-codeofconduct@amazon.com with any additional questions or comments. 52 | 53 | 54 | ## Security issue notifications 55 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 56 | 57 | 58 | ## Licensing 59 | 60 | See the [LICENSE](https://github.com/aws-samples/amazon-workmail-lambdas-templates/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 61 | 62 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 63 | -------------------------------------------------------------------------------- /NOTICE: -------------------------------------------------------------------------------- 1 | Amazon WorkMail Lambdas Templates 2 | Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://api.travis-ci.org/aws-samples/amazon-workmail-lambda-templates.svg?branch=master)](https://travis-ci.org/aws-samples/amazon-workmail-lambda-templates) 2 | 3 | ## Amazon WorkMail Lambda Templates 4 | 5 | Serverless applications for Amazon WorkMail. To use these applications you can deploy them via [AWS Lambda Console.](https://console.aws.amazon.com/lambda/home?region=us-east-1#/create?firstrun=true&tab=serverlessApps) 6 | 7 | For more information see [AWS WorkMail Lambda documentation](https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html) 8 | 9 | ## License 10 | 11 | This library is licensed under the Apache 2.0 License. 12 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pyflakes==3.3.2 2 | boto3==1.38.5 3 | requests==2.32.3 4 | beautifulsoup4==4.13.4 5 | simple-salesforce==1.12.6 6 | icalendar==6.1.3 -------------------------------------------------------------------------------- /workmail-cap-exchange/README.md: -------------------------------------------------------------------------------- 1 | # Amazon WorkMail / Exchange based Custom Availability Provider 2 | 3 | This application shows how to get user availability from a non-public EWS endpoint. 4 | 5 | The high level overview: 6 | - A user makes an EWS `GetUserAvailability` request to WorkMail for mailboxes that 7 | are not hosted in the WorkMail organization. 8 | - WorkMail invokes the Custom Availability Provider lambda function. 9 | - The lambda function obtains credentials for the remote EWS endpoint from secrets 10 | manager. 11 | - The lambda function calls `GetUserAvailability` on the remote EWS endpoint and 12 | relays the result back to WorkMail. 13 | 14 | ## Setup 15 | 16 | 1. Set the correct values in the `exchange_secrets.json` file. 17 | 18 | 1. `ews_url` - the URL to the remote EWS endpoint 19 | 2. `ews_username` - the username of the user used to access the remote EWS endpoint 20 | 3. `ews_password` - the password of the user used to access the remove EWS endpoint 21 | 22 | 2. Upload `exchange_secrets.json` to AWS Secrets Manager: 23 | ```shell 24 | aws secretsmanager create-secret --name production/ExchangeSecrets --secret-string file://exchange_secrets.json 25 | ``` 26 | 27 | 3. Deploy this application via [AWS Serverless Application Repository](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:489970191081:applications~workmail-cap-exchange): 28 | 29 | 1. Keep the default `production/ExchangeSecrets` value 30 | 2. Enter your WorkMail organization id 31 | 32 | 4. Create an AvailabilityConfiguration for the new lambda function 33 | ```shell 34 | aws workmail create-availability-configuration \ 35 | --organization-id \ 36 | --domain-name \ 37 | --lambda-provider 'LambdaArn=' 38 | ``` 39 | 40 | You now have a working Lambda function that will handle user availability requests. 41 | 42 | If you'd like to customize your Lambda function, open the [AWS Lambda Console](https://console.aws.amazon.com/lambda) to edit and test your Lambda 43 | function with the built-in code editor. For more information, see [Documentation](https://docs.aws.amazon.com/lambda/latest/dg/code-editor.html). 44 | 45 | For more advanced use cases, such as changing your CloudFormation template to create additional AWS resources that will support this application, follow the instructions below. 46 | 47 | ## Development 48 | 49 | Clone this repository from [Github](https://raw.githubusercontent.com/aws-samples/amazon-workmail-lambda-templates/master/workmail-cap-exchange). 50 | 51 | We recommend creating and activating a virtual environment, for more information see [Creation of virtual environments](https://docs.python.org/3/library/venv.html). 52 | 53 | If you are not familiar with CloudFormation templates, see [Learn Template Basics](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/gettingstarted.templatebasics.html). 54 | 55 | 1. Create additional resources for your application by changing [template.yaml](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-cap-exchange/template.yaml). For 56 | more information, see [documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-reference.html). 57 | 58 | 2. Modify your Lambda function by changing [app.py](https://github.com/aws-samples/amazon-workmail-lambdas-templates/blob/master/workmail-cap-exchange/src/app.py). 59 | 60 | 3. Test your lambda function locally: 61 | 62 | 1. [Set up the SAM CLI](https://aws.amazon.com/serverless/sam/). 63 | 64 | 2. Put values to `exchange_secrets.json` and create/update a secret to AWS Secrets Manager: 65 | ```shell 66 | aws secretsmanager create-secret --name production/ExchangeSecrets --secret-string file://exchange_secrets.json 67 | aws secretsmanager update-secret --secret-id production/ExchangeSecrets --secret-string file://exchange_secrets.json 68 | ``` 69 | 70 | 3. Fill out files in `tst` folder with your corresponding values 71 | 72 | ### Testing 73 | 74 | Testing Lambda locally: 75 | ```shell 76 | sam local invoke WorkMailCapExchangeFunction -e tst/lambda_query_availability.json --env-vars tst/env_vars.json 77 | ``` 78 | 79 | ### Deployment 80 | 81 | ```shell 82 | sam build 83 | ``` 84 | 85 | ```shell 86 | sam deploy --guided 87 | ``` 88 | 89 | Your Lambda function is now deployed. The output will contain `WorkMailCapExchangeFunctionName` which you can use 90 | to invoke the function using `awscli`: 91 | ```shell 92 | aws lambda invoke --function-name --payload 'file://tst/lambda_query_availability.json' /dev/stderr >/dev/null 93 | ``` 94 | 95 | ## Troubleshooting 96 | 97 | ### Where can I find logs? 98 | 99 | The logs can be found in CloudWatch. To get more detailed logging, change the loglevel in 100 | [app.py](https://github.com/aws-samples/amazon-workmail-lambdas-templates/blob/master/workmail-cap-exchange/src/app.py) 101 | to `DEBUG`. 102 | -------------------------------------------------------------------------------- /workmail-cap-exchange/dependencies/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.38.5 2 | exchangelib==5.5.1 -------------------------------------------------------------------------------- /workmail-cap-exchange/exchange_secrets.json: -------------------------------------------------------------------------------- 1 | { 2 | "ews_url": "https://hostname/ews/exchange.asmx", 3 | "ews_username": "", 4 | "ews_password": "" 5 | } -------------------------------------------------------------------------------- /workmail-cap-exchange/src/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Mapping 3 | 4 | import ews 5 | import params 6 | 7 | # setup logging 8 | logging.basicConfig() 9 | 10 | # Set to DEBUG to enable request/response logging 11 | logging.getLogger().setLevel(logging.INFO) 12 | 13 | 14 | # Create singleton session so it can be reused. 15 | SESSION = ews.Session() 16 | 17 | 18 | def lambda_handler(event: Mapping, _context) -> Mapping: 19 | """ 20 | CAP event handler. 21 | 22 | See: https://docs.aws.amazon.com/workmail/latest/adminguide/??? 23 | 24 | :param event: dict, containing information received from WorkMail. Examples: 25 | tst/lambda_query_availability.json 26 | 27 | :param context: lambda Context runtime methods and attributes. See: 28 | https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html 29 | 30 | :return: dict, containing the response payload, Example: 31 | { 32 | "mailboxes": [{ 33 | "mailbox": "user2@external.example.com", 34 | "events": [{ 35 | "startTime": "2021-05-03T23:00:00.000Z", 36 | "endTime": "2021-05-04T03:00:00.000Z", 37 | "busyType": "BUSY" 38 | }], 39 | "workingHours": { 40 | "timezone": { 41 | "name": "UTC", 42 | "bias": 0 43 | }, 44 | "workingPeriods":[{ 45 | "startMinutes": 480, 46 | "endMinutes": 1040, 47 | "days": ["MON", "TUE", "WED", "THU", "FRI"] 48 | }] 49 | } 50 | },{ 51 | "mailbox": "unknown@internal.example.com", 52 | "error": "MailboxNotFound" 53 | }] 54 | } 55 | """ 56 | try: 57 | if params.event_logging_enabled: 58 | logging.debug(event) 59 | return SESSION.execute(event) 60 | 61 | except Exception as e: 62 | if params.event_logging_enabled: 63 | logging.exception(e) 64 | raise 65 | -------------------------------------------------------------------------------- /workmail-cap-exchange/src/params.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from exchangelib import Credentials 4 | 5 | import secretsmanager 6 | 7 | # Exchange secrets id (default value is "production/ExchangeSecrets") 8 | # it is a json document with a format "exchange_secrets.json", 9 | # and is stored in AWS Secrets Manager 10 | ex_secrets_id = os.getenv("EXCHANGE_SECRETS_ID") 11 | 12 | # Exchange secrets loaded from AWS Secrets Manager 13 | ex_secrets = secretsmanager.get_secret(ex_secrets_id) 14 | 15 | # Exchange EWS endpoint and credentials 16 | ews_url = ex_secrets["ews_url"] 17 | ews_username = ex_secrets["ews_username"] 18 | ews_password = ex_secrets["ews_password"] 19 | ews_credentials = Credentials(ews_username, ews_password) 20 | 21 | # Indicates whether event data and exception messages will be logged 22 | event_logging_enabled = os.getenv("EVENT_LOGGING_ENABLED") == "True" 23 | -------------------------------------------------------------------------------- /workmail-cap-exchange/src/secretsmanager.py: -------------------------------------------------------------------------------- 1 | import json 2 | from typing import Mapping 3 | 4 | import boto3 5 | 6 | sm = boto3.client("secretsmanager") 7 | 8 | 9 | def get_secret(secret_id: str) -> Mapping: 10 | """ 11 | Load secrets from AWS Secrets Manager. 12 | 13 | See: https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html 14 | 15 | :param secret_id: secret id 16 | :return: secret content 17 | """ 18 | secret_value = sm.get_secret_value(SecretId=secret_id) 19 | secret_string = secret_value["SecretString"] 20 | return json.loads(secret_string) 21 | -------------------------------------------------------------------------------- /workmail-cap-exchange/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | WorkMail CAP Exchange 5 | 6 | Parameters: 7 | WorkMailOrganizationID: 8 | Type: String 9 | AllowedPattern: "^m-[0-9a-f]{32}$" 10 | Description: "You can find your organization id using workmail/list-organizations AWS CLI" 11 | ExchangeSecretsID: 12 | Type: String 13 | Default: "production/ExchangeSecrets" 14 | Description: "Exchange secrets (see https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-cap-exchange/README.md for more details)" 15 | EventLoggingEnabled: 16 | Type: String 17 | Default: "True" 18 | AllowedValues: ["True", "False"] 19 | Description: "Whether event logging is enabled. When set to False events and exception messages are never logged." 20 | 21 | Resources: 22 | WorkMailCapExchangeDependencyLayer: 23 | Type: AWS::Serverless::LayerVersion 24 | Properties: 25 | ContentUri: dependencies/ 26 | CompatibleRuntimes: 27 | - python3.12 28 | Metadata: 29 | BuildMethod: python3.12 30 | 31 | WorkMailCapExchangeFunction: 32 | Type: AWS::Serverless::Function 33 | Properties: 34 | CodeUri: src/ 35 | Handler: app.lambda_handler 36 | Runtime: python3.12 37 | Timeout: 20 38 | Role: !GetAtt WorkMailCapExchangeFunctionRole.Arn 39 | Layers: 40 | - !Ref WorkMailCapExchangeDependencyLayer 41 | Environment: 42 | Variables: 43 | EXCHANGE_SECRETS_ID: 44 | Ref: ExchangeSecretsID 45 | EVENT_LOGGING_ENABLED: 46 | Ref: EventLoggingEnabled 47 | 48 | WorkMailCapExchangeFunctionPermission: 49 | Type: AWS::Lambda::Permission 50 | Properties: 51 | FunctionName: !GetAtt WorkMailCapExchangeFunction.Arn 52 | Action: lambda:InvokeFunction 53 | Principal: !Sub availability.workmail.${AWS::Region}.amazonaws.com 54 | SourceArn: 55 | Fn::Sub: 56 | - "arn:aws:workmail:${AWS::Region}:${AWS::AccountId}:organization/${OrganizationId}" 57 | - { OrganizationId: !Ref WorkMailOrganizationID } 58 | 59 | WorkMailCapExchangeFunctionRole: 60 | Type: AWS::IAM::Role 61 | Properties: 62 | AssumeRolePolicyDocument: 63 | Statement: 64 | - Action: 65 | - sts:AssumeRole 66 | Effect: Allow 67 | Principal: 68 | Service: 69 | - "lambda.amazonaws.com" 70 | Version: "2012-10-17" 71 | Path: "/" 72 | Policies: 73 | - PolicyName: "AllowSecretsManagerGetValue" 74 | PolicyDocument: 75 | Version: "2012-10-17" 76 | Statement: 77 | - Effect: Allow 78 | Action: 79 | - "secretsmanager:GetSecretValue" 80 | Resource: 81 | Fn::Sub: 82 | - "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${SecretName}*" 83 | - { SecretName: !Ref ExchangeSecretsID } 84 | ManagedPolicyArns: 85 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 86 | 87 | Outputs: 88 | WorkMailCapExchangeFunctionName: 89 | Value: !Ref WorkMailCapExchangeFunction 90 | -------------------------------------------------------------------------------- /workmail-cap-exchange/tst/env_vars.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkMailCapExchangeFunction": { 3 | "EXCHANGE_SECRETS_ID": "production/ExchangeSecrets", 4 | "EVENT_LOGGING_ENABLED": "True" 5 | } 6 | } -------------------------------------------------------------------------------- /workmail-cap-exchange/tst/lambda_query_availability.json: -------------------------------------------------------------------------------- 1 | { 2 | "requester": { 3 | "email": "user1@internal.example.com", 4 | "userName": "user1", 5 | "organization": "m-0123456789abcdef0123456789abcdef", 6 | "userId": "S-1-5-18", 7 | "origin": "127.0.0.1" 8 | }, 9 | "mailboxes": [ 10 | "user2@external.example.com", 11 | "unknown@internal.example.com" 12 | ], 13 | "window": { 14 | "startDate": "2021-05-04T00:00:00+00:00", 15 | "endDate": "2021-05-06T00:00:00+00:00" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /workmail-chat-bot-python/README.md: -------------------------------------------------------------------------------- 1 | # Amazon WorkMail Chat Bot 2 | This application integrates your email with a basic chat bot using webhooks. Specifically, the application automatically posts to a configured chat channel whenever an email is sent or received which contains a configurable string in the email subject line. The code provided here already supports [Amazon Chime](https://aws.amazon.com/chime/) and [Slack](https://slack.com/), and it can easily be modified to support any other chat application that can be accessed via webhooks. 3 | 4 | ## Setup 5 | 1. Deploy this application via [AWS Serverless Application Repository](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:489970191081:applications~workmail-chat-bot-python). 6 | 1. Enter the name of your chat client. Supported: Chime, Slack. 7 | 2. Enter the WebHook URL for your chat room. For instructions on how to create a WebHook URL, see the documentation for your chat client: [Amazon Chime](https://docs.aws.amazon.com/chime/latest/ug/webhooks.html), [Slack](https://api.slack.com/incoming-webhooks#create_a_webhook). 8 | 3. [Optional] Configure your chat room to receive messages only from emails that contain certain words in subject. Leave it blank for receiving messages from all emails. 9 | For example: Enter **URGENT, Action Required** to receive messages only from emails that contains **URGENT** or **ACTION REQUIRED** in subject. 10 | Note: Case Insensitive comparison is done. For example: Setting **Urgent** will send messages for emails with subject UrGent, urgent, ...etc. 11 | 2. Open the [WorkMail Console](https://console.aws.amazon.com/workmail/) and create a **RunLambda** [Email Flow Rule](https://docs.aws.amazon.com/workmail/latest/adminguide/create-email-rules.html) that uses this Lambda function. 12 | 13 | You now have a working Lambda function that will be triggered by WorkMail based on the rule you created. 14 | 15 | If you'd like to add support for a new chat application or further customize your Lambda function, open the [AWS Lambda Console](https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions) to edit and test your Lambda function with the built-in code editor. For more information, see [Documentation](https://docs.aws.amazon.com/lambda/latest/dg/code-editor.html). 16 | 17 | For more advanced use cases, such as changing your CloudFormation template to create additional AWS resources that will support this application, follow the instructions below. 18 | 19 | ## Access Control 20 | By default, this serverless application and the resources that it creates can integrate with any [WorkMail Organization](https://docs.aws.amazon.com/workmail/latest/adminguide/organizations_overview.html) in your account, but the application and organization must be in the same region. To restrict that behavior you can either update the SourceArn attribute in [template.yaml](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-chat-bot-python/template.yaml) 21 | and then deploy the application by following the steps below **or** update the SourceArn attribute directly in the resource policy of each resource via their AWS Console after the deploying this application, [see example](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html). 22 | 23 | For more information about the SourceArn attribute, [see this documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-sourcearn). 24 | 25 | ## Development 26 | Clone this repository from [GitHub](https://github.com/aws-samples/amazon-workmail-lambda-templates). 27 | 28 | We recommend creating and activating a virtual environment, for more information see [Creation of virtual environments](https://docs.python.org/3/library/venv.html). 29 | 30 | If you are not familiar with CloudFormation templates, see [Learn Template Basics](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/gettingstarted.templatebasics.html). 31 | 32 | 1. Create additional resources for your application by changing [template.yaml](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-chat-bot-python/template.yaml). For more information, see this [documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-reference.html). 33 | 2. Modify your Lambda function by changing [app.py](https://github.com/aws-samples/amazon-workmail-lambdas-templates/blob/master/workmail-chat-bot-python/src/app.py). 34 | 3. Test your Lambda function locally: 35 | 1. [Set up the SAM CLI](https://aws.amazon.com/serverless/sam/). 36 | 2. Configure environment variables at `tst/env_vars.json`. 37 | 3. Configure test event at `tst/event.json`. 38 | 4. Invoke your Lambda function locally using: 39 | 40 | `sam local invoke WorkMailChatBotFunction -e tst/event.json --env-vars tst/env_vars.json` 41 | 42 | ### Test Message Ids 43 | This application uses a `messageId` passed to the Lambda function to retrieve the message content from WorkMail. When testing, the `tst/event.json` file uses a mock messageId which does not exist. If you want to test with a real messageId, you can configure a WorkMail Email Flow Rule with the Lambda action that uses the Lambda function created in **Setup**, and send some emails that will trigger the email flow rule. The Lambda function will emit the messageId it receives from WorkMail in the CloudWatch logs, which you can 44 | then use in your test event data. For more information see [Accessing Amazon CloudWatch logs for AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html). Note that you can access messages in transit for a maximum of one day. 45 | 46 | Once you have validated that your Lambda function behaves as expected, you are ready to deploy this Lambda function. 47 | 48 | ### Deployment 49 | If you develop using the AWS Lambda Console, then this section can be skipped. 50 | 51 | Please create an S3 bucket if you do not have one yet, see [How do I create an S3 Bucket?](https://docs.aws.amazon.com/AmazonS3/latest/user-guide/create-bucket.html). 52 | and check how to create a [Bucket Policy](https://docs.aws.amazon.com/serverlessrepo/latest/devguide/serverlessrepo-how-to-publish.html#publishing-application-through-cli). 53 | We refer to this bucket as ``. 54 | 55 | This step bundles all your code and configuration to the given S3 bucket. 56 | 57 | ```bash 58 | sam build 59 | ``` 60 | 61 | ```bash 62 | sam package \ 63 | --template-file template.yaml \ 64 | --output-template-file packaged.yaml \ 65 | --s3-bucket 66 | ``` 67 | 68 | This step updates your Cloud Formation stack to reflect the changes you made, which will in turn update changes made in the Lambda function. 69 | ```bash 70 | sam deploy \ 71 | --stack-name workmail-chat-bot \ 72 | --template-file packaged.yaml \ 73 | --parameter-overrides ChatClient=$YOUR_CHAT_CLIENT WebhookURL=$YOUR_WEBHOOK_URL ActiveWords=$OPTIONAL_ACTIVE_WORDS \ 74 | --capabilities CAPABILITY_IAM 75 | ``` 76 | 77 | Tip: surround $YOUR_WEBHOOK_URL with quotes. 78 | 79 | Your Lambda function is now deployed. You can now configure WorkMail to trigger this function. 80 | -------------------------------------------------------------------------------- /workmail-chat-bot-python/dependencies/requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.32.3 -------------------------------------------------------------------------------- /workmail-chat-bot-python/src/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import requests 4 | import utils 5 | 6 | logger = logging.getLogger() 7 | logger.setLevel(logging.INFO) 8 | 9 | MAX_CHAT_MESSAGE_LEN = 1024 10 | 11 | def construct_chat_message(message_id, from_address, subject): 12 | """ 13 | Constructs a chat message by downloading the full email message, parsing the email body, and truncating contents if required. 14 | Parameters 15 | ---------- 16 | message_id: string, required 17 | message_id of the email to download 18 | Returns 19 | ------- 20 | string 21 | chat message 22 | """ 23 | parsed_email = utils.download_email(message_id) 24 | email_body = utils.extract_email_body(parsed_email) 25 | if len(email_body) > MAX_CHAT_MESSAGE_LEN: 26 | email_body = email_body[:MAX_CHAT_MESSAGE_LEN] 27 | email_body = f"{email_body}\n\n....Content was truncated." 28 | return f"Alert: email from {from_address} with subject {subject}\n\n{email_body}" 29 | 30 | def chat_handler(event, context): 31 | """ 32 | Chat Bot for Amazon WorkMail 33 | 34 | Parameters 35 | ---------- 36 | event: dict, required 37 | AWS WorkMail Message Summary Input Format 38 | For more information, see https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html 39 | 40 | { 41 | "summaryVersion": "2018-10-10", # AWS WorkMail Message Summary Version 42 | "envelope": { 43 | "mailFrom" : { 44 | "address" : "from@domain.test" # String containing from email address 45 | }, 46 | "recipients" : [ # List of all recipient email addresses 47 | { "address" : "recipient1@domain.test" }, 48 | { "address" : "recipient2@domain.test" } 49 | ] 50 | }, 51 | "sender" : { 52 | "address" : "sender@domain.test" # String containing sender email address 53 | }, 54 | "subject" : "Hello From Amazon WorkMail!", # String containing email subject (Truncated to first 256 chars) 55 | "truncated": false, # boolean indicating if any field in message was truncated due to size limitations 56 | "messageId": "00000000-0000-0000-0000-000000000000" # String containing the id of the message in the WorkMail system 57 | } 58 | 59 | context: object, required 60 | Lambda Context runtime methods and attributes 61 | 62 | Attributes 63 | ---------- 64 | 65 | context.aws_request_id: str 66 | Lambda request ID 67 | context.client_context: object 68 | Additional context when invoked through AWS Mobile SDK 69 | context.function_name: str 70 | Lambda function name 71 | context.function_version: str 72 | Function version identifier 73 | context.get_remaining_time_in_millis: function 74 | Time in milliseconds before function times out 75 | context.identity: 76 | Cognito identity provider context when invoked through AWS Mobile SDK 77 | context.invoked_function_arn: str 78 | Function ARN 79 | context.log_group_name: str 80 | Cloudwatch Log group name 81 | context.log_stream_name: str 82 | Cloudwatch Log stream name 83 | context.memory_limit_in_mb: int 84 | Function memory 85 | 86 | https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html 87 | 88 | Returns 89 | ------ 90 | Nothing 91 | """ 92 | logger.info(event) 93 | webhook_url = os.getenv('WEBHOOK_URL') 94 | if not webhook_url: 95 | error_msg = 'WEBHOOK_URL not set in environment. Please follow https://docs.aws.amazon.com/lambda/latest/dg/env_variables.html to set it.' 96 | logger.error(error_msg) 97 | raise ValueError(error_msg) 98 | 99 | chat_client = os.getenv('CHAT_CLIENT') 100 | if not chat_client: 101 | error_msg = 'CHAT_CLIENT not set in environment. Please follow https://docs.aws.amazon.com/lambda/latest/dg/env_variables.html to set it.' 102 | logger.error(error_msg) 103 | raise ValueError(error_msg) 104 | 105 | active_words = os.getenv('ACTIVE_WORDS') 106 | subject = event['subject'] 107 | from_address = event['envelope']['mailFrom']['address'] 108 | 109 | if utils.search_active_words(subject, active_words): 110 | headers = {'Content-Type': 'application/json'} 111 | message_text = construct_chat_message(event['messageId'], from_address, subject) 112 | payload = None 113 | if chat_client == 'Chime': 114 | payload = {'Content': message_text} 115 | elif chat_client == 'Slack': 116 | payload = {'text': message_text} 117 | # To add a new custom chat client, implement here: 118 | # elif chat_client == 'CHAT_CLIENT_NAME' 119 | # payload = 'FOLLOW_CHAT_CLIENT_PAYLOAD_SYNTAX' 120 | else: 121 | error_msg = f"Unsupported chat client: {chat_client}. Expected: Chime, Slack" 122 | logger.error(error_msg) 123 | raise NotImplementedError(error_msg) 124 | try: 125 | req = requests.post(webhook_url, headers=headers, json=payload) 126 | req.raise_for_status() 127 | except Exception: 128 | error_msg = f"Error while posting message to WebHook: {webhook_url}" 129 | logger.exception(error_msg) 130 | raise ConnectionError(error_msg) 131 | else: 132 | logger.info(f"Skipping sending chat message from {from_address} as it did not match Active Words.") 133 | 134 | return 135 | -------------------------------------------------------------------------------- /workmail-chat-bot-python/src/utils.py: -------------------------------------------------------------------------------- 1 | import email 2 | import boto3 3 | import logging 4 | from botocore.exceptions import ClientError 5 | 6 | logger = logging.getLogger() 7 | 8 | def download_email(message_id): 9 | """ 10 | This method downloads full email MIME content from WorkMailMessageFlow and uses email.parser class 11 | for parsing it into Python email.message.EmailMessage class. 12 | Reference: 13 | https://docs.python.org/3.6/library/email.message.html#email.message.EmailMessage 14 | https://docs.python.org/3/library/email.parser.html 15 | Parameters 16 | ---------- 17 | message_id: string, required 18 | message_id of the email to download 19 | Returns 20 | ------- 21 | email.message.Message 22 | EmailMessage representation the downloaded email. 23 | Raises 24 | ------ 25 | botocore.exceptions.ClientError: 26 | When email message cannot be downloaded. 27 | email.errors.MessageParseError 28 | When email message cannot be parsed. 29 | """ 30 | workmail_message_flow = boto3.client('workmailmessageflow') 31 | response = None 32 | try: 33 | response = workmail_message_flow.get_raw_message_content(messageId=message_id) 34 | except ClientError as e: 35 | if e.response['Error']['Code'] == 'ResourceNotFoundException': 36 | logger.error(f"Message {message_id} does not exist. Messages in transit are no longer accessible after 1 day. \ 37 | See: https://docs.aws.amazon.com/workmail/latest/adminguide/lambda-content.html for more details.") 38 | raise(e) 39 | 40 | email_content = response['messageContent'].read() 41 | return email.message_from_bytes(email_content) 42 | 43 | def extract_email_body(parsed_email): 44 | """ 45 | Extract email message content of type "text/plain" from a parsed email 46 | Parameters 47 | ---------- 48 | parsed_email: email.message.Message, required 49 | The parsed email as returned by download_email 50 | Returns 51 | ------- 52 | string 53 | string containing text/plain email body decoded with according to the Content-Transfer-Encoding header 54 | and then according to content charset. 55 | None 56 | No content of type "text/plain" is found. 57 | """ 58 | text_content = None 59 | text_charset = None 60 | if parsed_email.is_multipart(): 61 | # Walk over message parts of this multipart email. 62 | for part in parsed_email.walk(): 63 | content_type = part.get_content_type() 64 | content_disposition = str(part.get_content_disposition()) 65 | # Look for 'text/plain' content but ignore inline attachments. 66 | if content_type == 'text/plain' and 'attachment' not in content_disposition: 67 | text_content = part.get_payload(decode=True) 68 | text_charset = part.get_content_charset() 69 | break 70 | else: 71 | text_content = parsed_email.get_payload(decode=True) 72 | text_charset = parsed_email.get_content_charset() 73 | 74 | if text_content and text_charset: 75 | return text_content.decode(text_charset) 76 | return 77 | 78 | def search_active_words(subject, active_words): 79 | """ 80 | This method looks for active_words in subject in a case-insensitive fashion 81 | 82 | Parameters 83 | ---------- 84 | subject: string, required 85 | email subject 86 | active_words: string, required 87 | active words represented in a comma delimited fashion 88 | 89 | Returns 90 | ------- 91 | True 92 | If any active words were found in subject or, 93 | No active words are configured 94 | False 95 | If no active words were found in subject 96 | """ 97 | if not active_words: 98 | return True 99 | else: 100 | # Convert to lower case words by splitting active_words. For example: 'Hello , World,' is generated as ('hello','world'). 101 | lower_words = [word.strip().lower() for word in filter(None, active_words.split(','))] 102 | # Convert subject to lower case in order to do a case insensitive lookup. 103 | subject_lower = subject.lower() 104 | for word in lower_words: 105 | if word in subject_lower: 106 | return True 107 | return False 108 | -------------------------------------------------------------------------------- /workmail-chat-bot-python/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | WorkMail Chat Bot Lambda SAM 5 | 6 | Parameters: 7 | ChatClient: 8 | Type: String 9 | AllowedValues: 10 | - Chime 11 | - Slack 12 | Description: "Name of Chat Client (Supported: Chime, Slack)" 13 | WebhookURL: 14 | Type: String 15 | NoEcho: true 16 | Description: "Chat Webhook URL (To create one, refer to the README)" 17 | ActiveWords: 18 | Type: CommaDelimitedList 19 | Default: '' 20 | Description: "Comma-separated list of words which will trigger a message to the chat channel if found in an email subject line. Leave blank to receive messages for all emails" 21 | 22 | Resources: 23 | WorkMailChatBotDependencyLayer: 24 | Type: AWS::Serverless::LayerVersion 25 | Properties: 26 | ContentUri: dependencies/ 27 | CompatibleRuntimes: 28 | - python3.12 29 | Metadata: 30 | BuildMethod: python3.12 31 | 32 | WorkMailChatBotFunction: 33 | Type: AWS::Serverless::Function 34 | Properties: 35 | CodeUri: src/ 36 | Handler: app.chat_handler 37 | Runtime: python3.12 38 | Timeout: 10 39 | Role: !GetAtt WorkMailChatBotFunctionRole.Arn 40 | Layers: 41 | - !Ref WorkMailChatBotDependencyLayer 42 | Environment: 43 | Variables: 44 | CHAT_CLIENT: 45 | Ref: ChatClient 46 | WEBHOOK_URL: 47 | Ref: WebhookURL 48 | ACTIVE_WORDS: !Join [ ",", !Ref ActiveWords ] 49 | 50 | PermissionToCallLambdaAbove: 51 | Type: AWS::Lambda::Permission 52 | DependsOn: WorkMailChatBotFunction 53 | Properties: 54 | Action: lambda:InvokeFunction 55 | FunctionName: !Ref WorkMailChatBotFunction 56 | Principal: !Sub 'workmail.${AWS::Region}.amazonaws.com' 57 | SourceArn: !Sub 'arn:aws:workmail:${AWS::Region}:${AWS::AccountId}:organization/*' 58 | 59 | WorkMailChatBotFunctionRole: 60 | Type: AWS::IAM::Role 61 | Properties: 62 | AssumeRolePolicyDocument: 63 | Version: '2012-10-17' 64 | Statement: 65 | - Effect: Allow 66 | Principal: 67 | Service: 68 | - lambda.amazonaws.com 69 | Action: 70 | - sts:AssumeRole 71 | Path: "/" 72 | ManagedPolicyArns: 73 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 74 | - "arn:aws:iam::aws:policy/AmazonWorkMailMessageFlowReadOnlyAccess" 75 | 76 | Outputs: 77 | ChatBotArn: 78 | Value: !GetAtt WorkMailChatBotFunction.Arn 79 | -------------------------------------------------------------------------------- /workmail-chat-bot-python/tst/env_vars.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkMailChatBotFunction": { 3 | "WEBHOOK_URL" : "YOUR_WEBHOOK_URL", 4 | "CHAT_CLIENT" : "YOUR_CHAT_CLIENT", 5 | "ACTIVE_WORDS": "YOUR_ACTIVE_WORDS" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /workmail-chat-bot-python/tst/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "summaryVersion": "2018-10-10", 3 | "envelope": { 4 | "mailFrom" : { 5 | "address" : "from@domain.test" 6 | }, 7 | "recipients" : [ 8 | { "address" : "recipient1@domain.test" }, 9 | { "address" : "recipient2@domain.test" } 10 | ] 11 | }, 12 | "sender" : { 13 | "address" : "sender@domain.test" 14 | }, 15 | "subject" : "Hello From Amazon WorkMail!", 16 | "messageId": "00000000-0000-0000-0000-000000000000", 17 | "truncated": false 18 | } 19 | -------------------------------------------------------------------------------- /workmail-hello-world-python/README.md: -------------------------------------------------------------------------------- 1 | # Amazon WorkMail Hello World 2 | 3 | This is a Hello World function that shows Amazon WorkMail integration 4 | with AWS Lambda. When you deploy it to your account, the system creates a 5 | Lambda function with all the necessary resources and permissions to access and modify incoming events from WorkMail. 6 | 7 | Add your own business logic to the Lambda function based on your use-case. 8 | For more information see [AWS WorkMail Lambda documentation](https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html) 9 | 10 | 11 | ## Setup 12 | 1. Deploy this application via [AWS Serverless Application Repository](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:489970191081:applications~workmail-hello-world-python) 13 | 2. Open the [WorkMail Console](https://console.aws.amazon.com/workmail/) and create a **RunLambda** [Email Flow Rule](https://docs.aws.amazon.com/workmail/latest/adminguide/create-email-rules.html) that uses this Lambda function. 14 | 15 | You now have a working Lambda function that will be triggered by WorkMail based on the rule you created. To add business logic to your Lambda function, open the [AWS Lambda Console](https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions) to edit and test your Lambda function with the built-in code editor. 16 | 17 | For more advanced use cases, such as changing your CloudFormation template to create additional AWS resources that will support this application, follow the instructions below. 18 | 19 | ## Access Control 20 | By default, this serverless application and the resources that it creates can integrate with any [WorkMail Organization](https://docs.aws.amazon.com/workmail/latest/adminguide/organizations_overview.html) in your account, but the application and organization must be in the same region. To restrict that behavior you can either update the SourceArn attribute in [template.yaml](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-hello-world-python/template.yaml) 21 | and then deploy the application by following the steps below **or** update the SourceArn attribute directly in the resource policy of each resource via their AWS Console after the deploying this application, [see example](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html). 22 | 23 | For more information about the SourceArn attribute, [see this documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-sourcearn). 24 | 25 | ## Development 26 | Clone this repository from [GitHub](https://github.com/aws-samples/amazon-workmail-lambda-templates). 27 | 28 | We recommend creating and activating a virtual environment, for more information see [Creation of virtual environments](https://docs.python.org/3/library/venv.html). 29 | 30 | If you are not familiar with CloudFormation templates, see [Learn Template Basics](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/gettingstarted.templatebasics.html). 31 | 32 | 1. Create additional resources for your application by changing [template.yaml](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-hello-world-python/template.yaml). See this [documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-reference.html) for more details. 33 | 2. Modify your Lambda function by changing [app.py](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-hello-world-python/src/app.py). 34 | 3. Test your Lambda function locally: 35 | 1. [Set up the SAM CLI](https://aws.amazon.com/serverless/sam/). 36 | 2. Configure environment variables at `tst/env_vars.json`. 37 | 3. Configure test event at `tst/event.json`. 38 | 4. Invoke your Lambda function locally using: 39 | 40 | `sam local invoke WorkMailHelloWorldFunction -e tst/event.json --env-vars tst/env_vars.json` 41 | 42 | ### Test Message Ids 43 | This application uses a `messageId` passed to the Lambda function to retrieve the message content from WorkMail. When testing, the `tst/event.json` file uses a mock messageId which does not exist. If you want to test with a real messageId, you can configure a WorkMail Email Flow Rule with the Lambda action that uses the Lambda function created in **Setup**, and send some emails that will trigger the email flow rule. The Lambda function will emit the messageId it receives from WorkMail in the CloudWatch logs, which you can 44 | then use in your test event data. For more information see [Accessing Amazon CloudWatch logs for AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html). Note that you can access messages in transit for a maximum of one day. 45 | 46 | Once you have validated that your Lambda function behaves as expected, you are ready to deploy this Lambda function. 47 | 48 | ### Deployment 49 | If you develop using the AWS Lambda Console, then this section can be skipped. 50 | 51 | Please create an S3 bucket if you do not have one yet, see [How do I create an S3 Bucket?](https://docs.aws.amazon.com/AmazonS3/latest/user-guide/create-bucket.html). 52 | and check how to create a [Bucket Policy](https://docs.aws.amazon.com/serverlessrepo/latest/devguide/serverlessrepo-how-to-publish.html#publishing-application-through-cli). 53 | We refer to this bucket as ``. 54 | 55 | This step bundles all your code and configuration to the given S3 bucket. 56 | 57 | ```bash 58 | sam build 59 | ``` 60 | 61 | ```bash 62 | sam package \ 63 | --template-file template.yaml \ 64 | --output-template-file packaged.yaml \ 65 | --s3-bucket 66 | ``` 67 | 68 | This step updates your Cloud Formation stack to reflect the changes you made, which will in turn update changes made in the Lambda function. 69 | ```bash 70 | sam deploy \ 71 | --template-file packaged.yaml \ 72 | --stack-name workmail-hello-world \ 73 | --capabilities CAPABILITY_IAM 74 | ``` 75 | Your Lambda function is now deployed. You can now configure WorkMail to trigger this function. 76 | -------------------------------------------------------------------------------- /workmail-hello-world-python/dependencies/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.38.5 -------------------------------------------------------------------------------- /workmail-hello-world-python/src/app.py: -------------------------------------------------------------------------------- 1 | from email.message import Message 2 | from botocore.exceptions import ClientError 3 | import boto3 4 | import email 5 | import os 6 | import uuid 7 | 8 | workmail_message_flow = boto3.client('workmailmessageflow') 9 | s3 = boto3.client('s3') 10 | 11 | 12 | def lambda_handler(event, context): 13 | """ 14 | 15 | Hello world example for AWS WorkMail 16 | 17 | Parameters 18 | ---------- 19 | event: dict, required 20 | AWS WorkMail Message Summary Input Format 21 | For more information, see https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html 22 | 23 | { 24 | "summaryVersion": "2019-07-28", # AWS WorkMail Message Summary Version 25 | "envelope": { 26 | "mailFrom" : { 27 | "address" : "from@domain.test" # String containing from email address 28 | }, 29 | "recipients" : [ # List of all recipient email addresses 30 | { "address" : "recipient1@domain.test" }, 31 | { "address" : "recipient2@domain.test" } 32 | ] 33 | }, 34 | "sender" : { 35 | "address" : "sender@domain.test" # String containing sender email address 36 | }, 37 | "subject" : "Hello From Amazon WorkMail!", # String containing email subject (Truncated to first 256 chars)" 38 | "messageId": "00000000-0000-0000-0000-000000000000", # String containing message id for retrieval using workmail flow API 39 | "invocationId": "00000000000000000000000000000000", # String containing the id of this lambda invocation. Useful for detecting retries and avoiding duplication 40 | "flowDirection": "INBOUND", # String indicating direction of email flow. Value is either "INBOUND" or "OUTBOUND" 41 | "truncated": false # boolean indicating if any field in message was truncated due to size limitations 42 | } 43 | 44 | context: object, required 45 | Lambda Context runtime methods and attributes 46 | https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html 47 | 48 | Returns 49 | ------- 50 | Amazon WorkMail Sync Lambda Response Format. For more information, see https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html#synchronous-schema 51 | return { 52 | 'actions': [ # Required, should contain at least 1 list element 53 | { 54 | 'action' : { # Required 55 | 'type': 'string', # Required. For example: "BOUNCE", "DEFAULT". For full list of valid values, see https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html#synchronous-schema 56 | 'parameters': { } # Optional. For bounce, can be {"bounceMessage": "message that goes in bounce mail"} 57 | }, 58 | 'recipients': list of strings, # Optional if allRecipients is present. Indicates list of recipients for which this action applies. 59 | 'allRecipients': boolean # Optional if recipients is present. Indicates whether this action applies to all recipients 60 | } 61 | ]} 62 | 63 | """ 64 | from_address = event['envelope']['mailFrom']['address'] 65 | subject = event['subject'] 66 | flow_direction = event['flowDirection'] 67 | message_id = event['messageId'] 68 | print(f"Received email with message ID {message_id}, flowDirection {flow_direction}, from {from_address} with Subject {subject}") 69 | 70 | try: 71 | raw_msg = workmail_message_flow.get_raw_message_content(messageId=message_id) 72 | parsed_msg: Message = email.message_from_bytes(raw_msg['messageContent'].read()) 73 | 74 | # Updating subject. For more examples, see https://github.com/aws-samples/amazon-workmail-lambda-templates. 75 | parsed_msg.replace_header('Subject', f"[Hello World!] {subject}") 76 | 77 | # Try to get the email bucket. 78 | updated_email_bucket_name = os.getenv('UPDATED_EMAIL_S3_BUCKET') 79 | if not updated_email_bucket_name: 80 | print('UPDATED_EMAIL_S3_BUCKET not set in environment. ' 81 | 'Please follow https://docs.aws.amazon.com/lambda/latest/dg/env_variables.html to set it.') 82 | return 83 | 84 | key = str(uuid.uuid4()) 85 | 86 | # Put the message in S3, so WorkMail can access it. 87 | s3.put_object(Body=parsed_msg.as_bytes(), Bucket=updated_email_bucket_name, Key=key) 88 | 89 | # Update the email in WorkMail. 90 | s3_reference = { 91 | 'bucket': updated_email_bucket_name, 92 | 'key': key 93 | } 94 | content = { 95 | 's3Reference': s3_reference 96 | } 97 | 98 | assert content # Silence pyflakes for unused variable 99 | 100 | # If you'd like to finalise modifying email subjects, then uncomment the line below. 101 | # workmail_message_flow.put_raw_message_content(messageId=message_id, content=content) 102 | 103 | except ClientError as e: 104 | if e.response['Error']['Code'] == 'MessageFrozen': 105 | # Redirect emails are not eligible for update, handle it gracefully. 106 | print(f"Message {message_id} is not eligible for update. This is usually the case for a redirected email") 107 | else: 108 | # Send some context about this error to Lambda Logs 109 | print(e) 110 | if e.response['Error']['Code'] == 'ResourceNotFoundException': 111 | print(f"Message {message_id} does not exist. Messages in transit are no longer accessible after 1 day") 112 | elif e.response['Error']['Code'] == 'InvalidContentLocation': 113 | print('WorkMail could not access the updated email content. See https://docs.aws.amazon.com/workmail/latest/adminguide/update-with-lambda.html') 114 | raise(e) 115 | 116 | # Return value is ignored when Lambda is configured asynchronously at Amazon WorkMail 117 | # For more information, see https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html 118 | return { 119 | 'actions': [ 120 | { 121 | 'allRecipients': True, # For all recipients 122 | 'action': {'type': 'DEFAULT'} # let the email be sent normally 123 | } 124 | ] 125 | } 126 | -------------------------------------------------------------------------------- /workmail-hello-world-python/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | WorkMail Hello World Lambda SAM 5 | 6 | Resources: 7 | WorkMailHelloWorldDependencyLayer: 8 | Type: AWS::Serverless::LayerVersion 9 | Properties: 10 | ContentUri: dependencies/ 11 | CompatibleRuntimes: 12 | - python3.12 13 | Metadata: 14 | BuildMethod: python3.12 15 | 16 | WorkMailHelloWorldFunction: 17 | Type: AWS::Serverless::Function 18 | Properties: 19 | CodeUri: src/ 20 | Handler: app.lambda_handler 21 | Runtime: python3.12 22 | Timeout: 10 23 | Role: !GetAtt WorkMailHelloWorldFunctionRole.Arn 24 | Layers: 25 | - !Ref WorkMailHelloWorldDependencyLayer 26 | Environment: 27 | Variables: 28 | UPDATED_EMAIL_S3_BUCKET: 29 | Ref: UpdatedEmailS3Bucket 30 | 31 | PermissionToCallLambdaAbove: 32 | Type: AWS::Lambda::Permission 33 | DependsOn: WorkMailHelloWorldFunction 34 | Properties: 35 | Action: lambda:InvokeFunction 36 | FunctionName: !Ref WorkMailHelloWorldFunction 37 | Principal: !Sub 'workmail.${AWS::Region}.amazonaws.com' 38 | SourceArn: !Sub 'arn:aws:workmail:${AWS::Region}:${AWS::AccountId}:organization/*' 39 | 40 | WorkMailHelloWorldFunctionRole: 41 | Type: AWS::IAM::Role 42 | Properties: 43 | AssumeRolePolicyDocument: 44 | Statement: 45 | - Action: 46 | - sts:AssumeRole 47 | Effect: Allow 48 | Principal: 49 | Service: 50 | - "lambda.amazonaws.com" 51 | Version: "2012-10-17" 52 | Path: "/" 53 | Policies: 54 | - PolicyName: "WorkMailMessageFlowAccessToS3Bucket" 55 | PolicyDocument: 56 | Version: "2012-10-17" 57 | Statement: 58 | - Effect: Allow 59 | Action: 60 | - "s3:PutObject" 61 | Resource: !Sub "${UpdatedEmailS3Bucket.Arn}/*" 62 | Condition: 63 | Bool: 64 | aws:SecureTransport: true 65 | ManagedPolicyArns: 66 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 67 | - "arn:aws:iam::aws:policy/AmazonWorkMailMessageFlowFullAccess" 68 | 69 | # WorkMail configures a S3 bucket with all the required policy for reading updated messages. 70 | # By default all the object expire after 1 day. 71 | # To know more, see https://docs.aws.amazon.com/workmail/latest/adminguide/update-with-lambda.html 72 | UpdatedEmailS3Bucket: 73 | Type: AWS::S3::Bucket 74 | DeletionPolicy: Retain 75 | Properties: 76 | BucketEncryption: 77 | ServerSideEncryptionConfiguration: 78 | - ServerSideEncryptionByDefault: 79 | SSEAlgorithm: AES256 80 | PublicAccessBlockConfiguration: 81 | BlockPublicAcls: true 82 | BlockPublicPolicy: true 83 | IgnorePublicAcls: true 84 | RestrictPublicBuckets: true 85 | VersioningConfiguration: 86 | Status: Enabled 87 | LifecycleConfiguration: 88 | Rules: 89 | - Status: Enabled 90 | ExpirationInDays: 1 # Delete after 1 day 91 | - Status: Enabled 92 | NoncurrentVersionExpirationInDays: 1 # Delete non current versions after 1 day 93 | 94 | UpdatedEmailS3BucketPolicy: 95 | Type: AWS::S3::BucketPolicy 96 | Properties: 97 | Bucket: 98 | Ref: UpdatedEmailS3Bucket 99 | PolicyDocument: 100 | Statement: 101 | - Effect: Allow 102 | Action: 103 | - "s3:GetObject" 104 | - "s3:GetObjectVersion" 105 | Resource: !Sub "${UpdatedEmailS3Bucket.Arn}/*" 106 | Condition: 107 | Bool: 108 | aws:SecureTransport: true 109 | ArnLike: 110 | aws:SourceArn: !Sub 'arn:aws:workmailmessageflow:${AWS::Region}:${AWS::AccountId}:message/*' 111 | Principal: 112 | Service: !Sub 'workmail.${AWS::Region}.amazonaws.com' # This policy enables WorkMail to read objects from your bucket 113 | 114 | Outputs: 115 | HelloWorldArn: 116 | Value: !GetAtt WorkMailHelloWorldFunction.Arn 117 | -------------------------------------------------------------------------------- /workmail-hello-world-python/tst/env_vars.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkMailHelloWorldFunction": { 3 | "UPDATED_EMAIL_S3_BUCKET": "UPDATED_EMAIL_S3_BUCKET" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /workmail-hello-world-python/tst/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "summaryVersion": "2019-07-28", 3 | "envelope": { 4 | "mailFrom" : { 5 | "address" : "from@domain.test" 6 | }, 7 | "recipients" : [ 8 | { "address" : "recipient1@domain.test" }, 9 | { "address" : "recipient2@domain.test" } 10 | ] 11 | }, 12 | "sender" : { 13 | "address" : "sender@domain.test" 14 | }, 15 | "subject" : "Hello From Amazon WorkMail!", 16 | "messageId": "00000000-0000-0000-0000-000000000000", 17 | "invocationId": "0000000000000000000000000000000000000000", 18 | "flowDirection": "OUTBOUND", 19 | "truncated": false 20 | } 21 | -------------------------------------------------------------------------------- /workmail-message-flow-state-machine/.gitignore: -------------------------------------------------------------------------------- 1 | tst/my* 2 | packaged.yaml 3 | -------------------------------------------------------------------------------- /workmail-message-flow-state-machine/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: "Message Flow State Machine - MFSM" 4 | 5 | Parameters: 6 | MachineStateForOutput: 7 | Type: String 8 | Default: '' 9 | Description: "[Optional] The name of the state within the Step Function state machine to return as output to the orchestrator Lambda function caller" 10 | WaitTimeForExecution: 11 | Type: Number 12 | Default: 1 13 | Description: "[Optional] Number of seconds to wait after the state machine starts execution to look for a result, useful when the step function finishes within few seconds" 14 | 15 | Resources: 16 | MfsmFunction: 17 | Type: AWS::Serverless::Function 18 | DependsOn: 19 | - MfsmStateMachine 20 | - MfsmExecutionTable 21 | Properties: 22 | CodeUri: src/ 23 | Handler: app.orchestrator_handler 24 | Runtime: python3.12 25 | Role: 26 | Fn::GetAtt: MfsmFunctionRole.Arn 27 | Timeout: 60 28 | Environment: 29 | Variables: 30 | STATE_MACHINE_ARN: 31 | Fn::GetAtt: MfsmStateMachine.Arn 32 | MACHINE_STATE_FOR_OUTPUT: 33 | Ref: MachineStateForOutput 34 | EXECUTION_TABLE: 35 | Ref: MfsmExecutionTable 36 | WAIT_TIME_FOR_EXECUTION: 37 | Ref: WaitTimeForExecution 38 | 39 | MfsmFunctionRole: 40 | Type: AWS::IAM::Role 41 | Properties: 42 | AssumeRolePolicyDocument: 43 | Statement: 44 | - Action: 45 | - sts:AssumeRole 46 | Effect: Allow 47 | Principal: 48 | Service: 49 | - "lambda.amazonaws.com" 50 | Version: "2012-10-17" 51 | Path: "/" 52 | ManagedPolicyArns: 53 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 54 | - "arn:aws:iam::aws:policy/AmazonWorkMailMessageFlowReadOnlyAccess" 55 | 56 | MfsmFunctionDynamoDBPolicy: 57 | Type: 'AWS::IAM::Policy' 58 | Properties: 59 | PolicyName: MfsmFunctionDynamoDBPolicy 60 | PolicyDocument: 61 | Version: "2012-10-17" 62 | Statement: 63 | - Effect: Allow 64 | Action: 65 | - dynamodb:PutItem 66 | - dynamodb:GetItem 67 | Resource: 68 | Fn::GetAtt: MfsmExecutionTable.Arn 69 | Roles: 70 | - !Ref MfsmFunctionRole 71 | 72 | MfsmFunctionStateMachinePolicy: 73 | Type: 'AWS::IAM::Policy' 74 | Properties: 75 | PolicyName: MfsmFunctionStateMachinePolicy 76 | PolicyDocument: 77 | Version: "2012-10-17" 78 | Statement: 79 | - Effect: Allow 80 | Action: 81 | - states:* 82 | Resource: 83 | - Fn::GetAtt: MfsmStateMachine.Arn 84 | - !Sub 'arn:aws:states:${AWS::Region}:${AWS::AccountId}:execution:${MfsmStateMachine.Name}:*' 85 | Roles: 86 | - !Ref MfsmFunctionRole 87 | 88 | MfsmFunctionPermission: 89 | Type: AWS::Lambda::Permission 90 | DependsOn: MfsmFunction 91 | Properties: 92 | Action: lambda:InvokeFunction 93 | FunctionName: !Ref MfsmFunction 94 | Principal: !Sub 'workmail.${AWS::Region}.amazonaws.com' 95 | SourceArn: !Sub 'arn:aws:workmail:${AWS::Region}:${AWS::AccountId}:organization/*' 96 | 97 | MfsmStateMachine: 98 | Type: AWS::StepFunctions::StateMachine 99 | Properties: 100 | DefinitionString: |- 101 | { 102 | "Comment": "WorkMail Message Flow State Machine", 103 | "StartAt": "DEFAULT Action", 104 | "States": { 105 | "DEFAULT Action": { 106 | "Comment": "A Pass state passes its input to its output, without performing work. Pass states are useful when constructing and debugging state machines.", 107 | "Type": "Pass", 108 | "Result": { 109 | "actions": [ 110 | { 111 | "allRecipients": "True", 112 | "action": { 113 | "type": "DEFAULT" 114 | } 115 | } 116 | ] 117 | }, 118 | "End": true 119 | } 120 | } 121 | } 122 | RoleArn: 123 | Fn::GetAtt: MfsmStateMachineRole.Arn 124 | 125 | MfsmStateMachineRole: 126 | Type: AWS::IAM::Role 127 | Properties: 128 | AssumeRolePolicyDocument: 129 | Statement: 130 | - Action: 131 | - sts:AssumeRole 132 | Effect: Allow 133 | Principal: 134 | Service: 135 | - "states.amazonaws.com" 136 | Version: "2012-10-17" 137 | Path: "/" 138 | ManagedPolicyArns: 139 | - "arn:aws:iam::aws:policy/CloudWatchLogsFullAccess" 140 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaRole" 141 | 142 | MfsmExecutionTable: 143 | Type: AWS::DynamoDB::Table 144 | Properties: 145 | BillingMode: PAY_PER_REQUEST 146 | AttributeDefinitions: 147 | - 148 | AttributeName: InvocationId 149 | AttributeType: S 150 | KeySchema: 151 | - 152 | AttributeName: InvocationId 153 | KeyType: HASH 154 | TimeToLiveSpecification: 155 | AttributeName: TimeToLive 156 | Enabled: true 157 | 158 | Outputs: 159 | MfsmFunctionArn: 160 | Value: !GetAtt MfsmFunction.Arn 161 | MfsmStateMachineArn: 162 | Value: !GetAtt MfsmStateMachine.Arn 163 | MfsmExecutionTableArn: 164 | Value: !GetAtt MfsmExecutionTable.Arn 165 | -------------------------------------------------------------------------------- /workmail-message-flow-state-machine/tst/env_vars.json: -------------------------------------------------------------------------------- 1 | { 2 | "MfsmFunction": { 3 | "STATE_MACHINE_ARN": "", 4 | "MACHINE_STATE_FOR_OUTPUT": "", 5 | "WAIT_TIME_FOR_EXECUTION": 1, 6 | "EXECUTION_TABLE": "" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /workmail-message-flow-state-machine/tst/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "summaryVersion": "2018-10-10", 3 | "envelope": { 4 | "mailFrom" : { 5 | "address" : "from@domain.test" 6 | }, 7 | "recipients" : [ 8 | { "address" : "recipient1@domain.test" }, 9 | { "address" : "recipient2@domain.test" } 10 | ] 11 | }, 12 | "sender" : { 13 | "address" : "sender@domain.test" 14 | }, 15 | "subject" : "Hello from Amazon WorkMail!", 16 | "messageId": "00000000-0000-0000-0000-000000000000", 17 | "invocationId": "0000000000000000000000000000000000000000", 18 | "flowDirection": "INBOUND", 19 | "truncated": false 20 | } 21 | -------------------------------------------------------------------------------- /workmail-restricted-mailboxes-python/README.md: -------------------------------------------------------------------------------- 1 | # Amazon WorkMail Restricted Mailboxes 2 | This application enables you to restrict certain mailboxes from within your Amazon WorkMail organization to send and receive internal emails only. 3 | 4 | Specifically, email messages sent from an external email address to your organization are bounced for restricted mailboxes and are allowed for other mailboxes in your organization. Email messages sent from an restricted mailbox are bounced for external recipients and are allowed for internal recipients. Optionally, this application enables you to receive a copy of such rejected email to a specific mailbox in your organization thereby enabling you to investigate it later. 5 | 6 | By default, the code provided here categorizes an email address that uses [default domain](https://docs.aws.amazon.com/workmail/latest/adminguide/default_domain.html) as internal. Email addresses that do not use default domain are categorized as external. You can easily configure emails from additional domains to be categorized as internal by [customizing your Lambda function](https://github.com/aws-samples/amazon-workmail-lambda-templates/tree/master/workmail-restricted-mailboxes-python#customizing-your-lambda-function). 7 | 8 | This application identifies a mailbox as restricted if it is member of a specified [WorkMail Group](https://docs.aws.amazon.com/workmail/latest/adminguide/groups_overview.html). As a result, you can control which mailboxes can communicate only internally. 9 | 10 | ## Setup 11 | 1. Deploy this application via [AWS Serverless Application Repository](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:489970191081:applications~workmail-restricted-mailboxes-python). 12 | 1. Enter your WorkMail Organization ID. You can find it in the Organization settings tab in the [WorkMail Console](https://console.aws.amazon.com/workmail/) 13 | 2. Enter the name of the WorkMail Group that has restricted mailboxes as members. To create a new group, follow this [instruction](https://docs.aws.amazon.com/workmail/latest/adminguide/add_new_group.html). 14 | 3. [Optional] Enter the email address of the mailbox where you would like to receive a copy of a rejected email. To create a new dedicated mailbox for this use case, follow this [instruction](https://docs.aws.amazon.com/workmail/latest/adminguide/manage-users.html#add_new_user). 15 | 2. Open the [WorkMail Console](https://console.aws.amazon.com/workmail/) and create a **RunLambda** [Email Flow Rule](https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html#synchronous-rules) that uses this Lambda function. 16 | 17 | To further customize your Lambda function, open the [AWS Lambda Console](https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions) to edit and test your Lambda function with the built-in code editor. 18 | 19 | ### Customizing Your Lambda Function 20 | If you would like to categorize emails from additional domains as internal for your organization add these domains to the list [here](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-restricted-mailboxes-python/src/utils.py#L55). 21 | 22 | For more advanced use cases, such as changing your CloudFormation template to create additional AWS resources that will support this application, follow the instructions below. 23 | 24 | ## Development 25 | Clone this repository from [GitHub](https://github.com/aws-samples/amazon-workmail-lambda-templates). 26 | 27 | We recommend creating and activating a virtual environment, for more information see [Creation of virtual environments](https://docs.python.org/3/library/venv.html). 28 | 29 | If you are not familiar with CloudFormation templates, see [Learn Template Basics](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/gettingstarted.templatebasics.html). 30 | 31 | 1. Create additional resources for your application by changing [template.yaml](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-restricted-mailboxes-python/template.yaml). For more information, see this [documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-reference.html). 32 | 2. Modify your Lambda function by changing [app.py](https://github.com/aws-samples/amazon-workmail-lambdas-templates/blob/master/workmail-restricted-mailboxes-python/src/app.py). 33 | 3. Test your Lambda function locally: 34 | 1. [Set up the SAM CLI](https://aws.amazon.com/serverless/sam/). 35 | 2. Configure environment variables at `tst/env_vars.json`. 36 | 3. Configure test event at `tst/event.json`. 37 | 4. Invoke your Lambda function locally using: 38 | 39 | `sam local invoke WorkMailRestrictedMailboxesFunction -e tst/event.json --env-vars tst/env_vars.json` 40 | 41 | ### Test Message Ids 42 | This application uses a `messageId` passed to the Lambda function to retrieve the message content from WorkMail. When testing, the `tst/event.json` file uses a mock messageId which does not exist. If you want to test with a real messageId, you can configure a WorkMail Email Flow Rule with the Lambda action that uses the Lambda function created in **Setup**, and send some emails that will trigger the email flow rule. The Lambda function will emit the messageId it receives from WorkMail in the CloudWatch logs, which you can 43 | then use in your test event data. For more information see [Accessing Amazon CloudWatch logs for AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html). Note that you can access messages in transit for a maximum of one day. 44 | 45 | Once you have validated that your Lambda function behaves as expected, you are ready to deploy this Lambda function. 46 | 47 | ### Deployment 48 | If you develop using the AWS Lambda Console, then this section can be skipped. 49 | 50 | Please create an S3 bucket if you do not have one yet, see [How do I create an S3 Bucket?](https://docs.aws.amazon.com/AmazonS3/latest/user-guide/create-bucket.html). 51 | and check how to create a [Bucket Policy](https://docs.aws.amazon.com/serverlessrepo/latest/devguide/serverlessrepo-how-to-publish.html#publishing-application-through-cli). 52 | We refer to this bucket as ``. 53 | 54 | This step bundles all your code and configuration to the given S3 bucket. 55 | 56 | ```bash 57 | sam build 58 | ``` 59 | 60 | ```bash 61 | sam package \ 62 | --template-file template.yaml \ 63 | --output-template-file packaged.yaml \ 64 | --s3-bucket 65 | ``` 66 | 67 | This step updates your Cloud Formation stack to reflect the changes you made, which will in turn update changes made in the Lambda function. 68 | ```bash 69 | sam deploy \ 70 | --template-file packaged.yaml \ 71 | --stack-name workmail-restricted-mailboxes \ 72 | --parameter-overrides WorkMailOrganizationID=$YOUR_ORGANIZATION_ID RestrictedGroupName=$YOUR_RESTRICTED_GROUP_NAME ReportMailboxAddress=$YOUR_REPORT_MAILBOX\ 73 | --capabilities CAPABILITY_IAM 74 | ``` 75 | Your Lambda function is now deployed. You can now configure WorkMail to trigger this function. 76 | -------------------------------------------------------------------------------- /workmail-restricted-mailboxes-python/src/utils.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import os 3 | import logging 4 | 5 | logger = logging.getLogger() 6 | workmail_client = boto3.client('workmail') 7 | 8 | def get_members_of_group(organization_id, group_name): 9 | """ 10 | Returns group member names for a group with given group_name 11 | Parameters 12 | ---------- 13 | organization_id: string, required 14 | Amazon WorkMail organization id 15 | group_name: string, requred 16 | Amazon Workmail group name 17 | Returns 18 | ------- 19 | list 20 | A list of string containing group member names 21 | Raises 22 | ------ 23 | Exception: 24 | When workmail group with given group_name was not found 25 | """ 26 | group_id = None 27 | for group in workmail_client.list_groups(OrganizationId=organization_id)['Groups']: 28 | if group['Name'] == group_name: 29 | group_id = group['Id'] 30 | 31 | if group_id is None: 32 | raise Exception(f"WorkMail group:{group_name} not found") 33 | 34 | all_members = workmail_client.list_group_members(OrganizationId=organization_id, GroupId=group_id)['Members'] 35 | member_names = [member['Name'].lower() for member in all_members] 36 | return member_names 37 | 38 | def filter_external(email_addresses, organization_id): 39 | """ 40 | Returns filtered list of email addresses that are external for a given organization 41 | Parameters 42 | ---------- 43 | email_addresses: list, required 44 | A list of email addresses 45 | organization_id: string, required 46 | Amazon WorkMail organization id 47 | Returns 48 | ------- 49 | list 50 | A list of email addresses that were external for the given organization 51 | """ 52 | external_email_addresses = [] 53 | default_domain = workmail_client.describe_organization(OrganizationId=organization_id)['DefaultMailDomain'] 54 | allowed_domains = [ default_domain, 55 | # "mydomain.test", Tip: You can add additional domains into this list to enable sending/receiving emails from them 56 | ] 57 | 58 | for email_address in email_addresses: 59 | # Extract domain part of email address 60 | domain = email_address['address'].lower().split('@')[1] 61 | if domain not in allowed_domains: 62 | external_email_addresses.append(email_address['address']) 63 | return external_email_addresses 64 | 65 | def filter_restricted(email_addresses, organization_id, group_name): 66 | """ 67 | Returns filtered list of email addresses that are restricted to send/recieve external emails 68 | Parameters 69 | ---------- 70 | email_addresses: list, required 71 | A list of email addresses 72 | organization_id: string, required 73 | Amazon WorkMail organization id 74 | group_name: string, requred 75 | Amazon Workmail group name 76 | Returns 77 | ------- 78 | list 79 | A list of email addresses that are restricted to send/receive external emails 80 | """ 81 | restricted_email_addresses = [] 82 | group_members = get_members_of_group(organization_id, group_name) 83 | for email_address in email_addresses: 84 | # Extract username a.k.a local-part of email address 85 | username = email_address['address'].lower().split('@')[0] 86 | if username in group_members: 87 | restricted_email_addresses.append(email_address['address']) 88 | 89 | return restricted_email_addresses 90 | 91 | def get_env_var(name): 92 | """ 93 | Helper that returns value of the environment variable key if it exists, else logs and throws ValueError 94 | Parameters 95 | ---------- 96 | name: string, required 97 | Environment variable key 98 | Returns 99 | ------- 100 | string 101 | A string containing value of the environment variable 102 | Raises 103 | ------ 104 | ValueError: 105 | When environment variable was not set 106 | """ 107 | var = os.getenv(name) 108 | if not var: 109 | error_msg = f'{name} not set in environment. Please follow https://docs.aws.amazon.com/lambda/latest/dg/env_variables.html to set it.' 110 | logger.error(error_msg) 111 | raise ValueError(error_msg) 112 | 113 | return var 114 | -------------------------------------------------------------------------------- /workmail-restricted-mailboxes-python/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | WorkMail Restricted Mailboxes Lambda SAM 5 | 6 | Parameters: 7 | WorkMailOrganizationID: 8 | Type: String 9 | AllowedPattern: "^m-[0-9a-f]{32}$" 10 | Description: "You can find your organization id in the Organization settings tab in the WorkMail console" 11 | 12 | RestrictedGroupName: 13 | Type: String 14 | Description: "WorkMail group name in your organization that has internal only mailboxes as members" 15 | 16 | ReportMailboxAddress: 17 | Type: String 18 | Default: '' 19 | Description: "[Optional] Email address of the mailbox in your organization where you would like to receive copy of a restricted email" 20 | 21 | Resources: 22 | WorkMailRestrictedMailboxesFunction: 23 | Type: AWS::Serverless::Function 24 | Properties: 25 | CodeUri: src/ 26 | Handler: app.restricted_mailboxes_handler 27 | Runtime: python3.12 28 | Role: 29 | Fn::GetAtt: WorkMailRestrictedMailboxesFunctionRole.Arn 30 | Timeout: 10 31 | Environment: 32 | Variables: 33 | WORKMAIL_ORGANIZATION_ID: 34 | Ref: WorkMailOrganizationID 35 | RESTRICTED_GROUP_NAME: 36 | Ref: RestrictedGroupName 37 | REPORT_MAILBOX_ADDRESS: 38 | Ref: ReportMailboxAddress 39 | 40 | WorkMailRestrictedMailboxesFunctionRole: 41 | Type: AWS::IAM::Role 42 | Properties: 43 | AssumeRolePolicyDocument: 44 | Statement: 45 | - Action: 46 | - sts:AssumeRole 47 | Effect: Allow 48 | Principal: 49 | Service: 50 | - "lambda.amazonaws.com" 51 | Version: "2012-10-17" 52 | Path: "/" 53 | ManagedPolicyArns: 54 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 55 | - "arn:aws:iam::aws:policy/AmazonWorkMailReadOnlyAccess" 56 | 57 | PermissionToCallLambdaAbove: 58 | Type: AWS::Lambda::Permission 59 | DependsOn: WorkMailRestrictedMailboxesFunction 60 | Properties: 61 | Action: lambda:InvokeFunction 62 | FunctionName: !Ref WorkMailRestrictedMailboxesFunction 63 | Principal: !Sub 'workmail.${AWS::Region}.amazonaws.com' 64 | SourceArn: !Sub 'arn:aws:workmail:${AWS::Region}:${AWS::AccountId}:organization/${WorkMailOrganizationID}' 65 | 66 | Outputs: 67 | WorkMailRestrictedMailboxesFunctionArn: 68 | Value: !GetAtt WorkMailRestrictedMailboxesFunction.Arn 69 | -------------------------------------------------------------------------------- /workmail-restricted-mailboxes-python/tst/env_vars.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkMailRestrictedMailboxesFunction": { 3 | "WORKMAIL_ORGANIZATION_ID" : "YOUR_ORGANIZATION_ID", 4 | "QUARANTINE_S3_BUCKET": "S3_BUCKET", 5 | "RESTRICTED_GROUP_NAME": "YOUR_RESTRICTED_GROUP_NAME" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /workmail-restricted-mailboxes-python/tst/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "summaryVersion": "2018-10-10", 3 | "envelope": { 4 | "mailFrom" : { 5 | "address" : "from@domain.test" 6 | }, 7 | "recipients" : [ 8 | { "address" : "recipient1@domain.test" }, 9 | { "address" : "recipient2@domain.test" } 10 | ] 11 | }, 12 | "sender" : { 13 | "address" : "sender@domain.test" 14 | }, 15 | "subject" : "Hello from Amazon WorkMail!", 16 | "messageId": "00000000-0000-0000-0000-000000000000", 17 | "invocationId": "0000000000000000000000000000000000000000", 18 | "flowDirection": "OUTBOUND", 19 | "truncated": false 20 | } 21 | -------------------------------------------------------------------------------- /workmail-salesforce-python/README.md: -------------------------------------------------------------------------------- 1 | # Amazon WorkMail Salesforce Integration 2 | 3 | This application replicates your email and calendar communication into Salesforce. 4 | 5 | Specifically, the application records your email and calendar communication into a Salesforce Opportunity using a random identifier referred as CaseId. A new CaseId is generated and prefixed in the subject of a new email or calendar item and a new Opportunity is created in Salesforce. All subsequent email or calendar items containing the string "[CaseId:XXX]" in the subject are recorded in the corresponding Opportunity. Additionally, the application also creates Salesforce Contacts and Accounts based on the sender of an incoming email, or recipient of an outgoing email. 6 | 7 | This application uses a dedicated Salesforce user for integration that you provide during the setup and uses [AWS Secrets Manager](https://aws.amazon.com/secrets-manager/) for persisting your user credentials securely. 8 | 9 | This application uses the [simple-salesforce](https://github.com/simple-salesforce/simple-salesforce) REST client to interact with Salesforce. 10 | 11 | 12 | ## Limitations 13 | 1. Recurring meeting requests are not yet handled gracefully. 14 | 2. While initiating a new meeting request for an existing CaseId, make sure "[CaseId:XXX]" is present in the subject of the meeting message so that application can record the meeting in the corresponding Opportunity. 15 | 16 | ## Setup 17 | 1. Deploy this application via [AWS Serverless Application Repository](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:489970191081:applications~workmail-salesforce-python) 18 | 1. Enter the username and password of your Salesforce account. 19 | 2. Enter the security token of your Salesforce account. To create an security token, see [instructions](https://help.salesforce.com/articleView?id=sf.user_security_token.htm&type=5) 20 | 2. Configure a synchronous Run Lambda rule for the Lambda function created in step 1, see [instructions](https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html#synchronous-rules). Ensure that the value of Rule timeout in your synchronous Run Lambda rule is at least 1 minute. 21 | 22 | It is possible to configure both inbound and outbound email flow rules over the same Lambda function. 23 | 24 | You now have a working Lambda function that will be triggered by WorkMail based on the rule you created. 25 | 26 | To further customize your Lambda function, open the [AWS Lambda Console](https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions) to edit and test your Lambda function with the built-in code editor. 27 | 28 | For more information, see [Documentation](https://docs.aws.amazon.com/lambda/latest/dg/code-editor.html). 29 | 30 | For more advanced use cases, such as changing your CloudFormation template to create additional AWS resources that will support this application, follow the instructions below. 31 | 32 | ## Access Control 33 | By default, this serverless application and the resources that it creates can integrate with any [WorkMail Organization](https://docs.aws.amazon.com/workmail/latest/adminguide/organizations_overview.html) in your account, but the application and organization must be in the same region. To restrict that behavior you can either update the SourceArn attribute in [template.yaml](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-salesforce-python/template.yaml) 34 | and then deploy the application by following the steps below **or** update the SourceArn attribute directly in the resource policy of each resource via their AWS Console after the deploying this application, [see example](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html). 35 | 36 | For more information about the SourceArn attribute, [see this documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-sourcearn). 37 | 38 | ## Development 39 | Clone this repository from [GitHub](https://github.com/aws-samples/amazon-workmail-lambda-templates). 40 | 41 | We recommend creating and activating a virtual environment, for more information see [Creation of virtual environments](https://docs.python.org/3/library/venv.html). 42 | 43 | If you are not familiar with CloudFormation templates, see [Learn Template Basics](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/gettingstarted.templatebasics.html). 44 | 45 | 1. Create additional resources for your application by changing [template.yaml](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-salesforce-python/template.yaml). See this [documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-reference.html) for more details. 46 | 2. Modify your Lambda function by changing [app.py](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-salesforce-python/src/app.py). 47 | 3. Test your Lambda function locally: 48 | 1. [Set up the SAM CLI](https://aws.amazon.com/serverless/sam/). 49 | 2. Configure environment variables at `tst/env_vars.json`. 50 | 3. Configure test event at `tst/event.json`. 51 | 4. Invoke your Lambda function locally using: 52 | 53 | `sam local invoke WorkMailSalesforceFunction -e tst/event.json --env-vars tst/env_vars.json` 54 | 55 | ### Test Message Ids 56 | This application uses a `messageId` passed to the Lambda function to retrieve the message content from WorkMail. When testing, the `tst/event.json` file uses a mock messageId which does not exist. If you want to test with a real messageId, you can configure a WorkMail Email Flow Rule with the Lambda action that uses the Lambda function created in **Setup**, and send some emails that will trigger the email flow rule. The Lambda function will emit the messageId it receives from WorkMail in the CloudWatch logs, which you can 57 | then use in your test event data. For more information see [Accessing Amazon CloudWatch logs for AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html). Note that you can access messages in transit for a maximum of one day. 58 | 59 | Once you have validated that your Lambda function behaves as expected, you are ready to deploy this Lambda function. 60 | 61 | ### Deployment 62 | If you develop using the AWS Lambda Console, then this section can be skipped. 63 | 64 | Please create an S3 bucket if you do not have one yet, see [How do I create an S3 Bucket?](https://docs.aws.amazon.com/AmazonS3/latest/user-guide/create-bucket.html). 65 | and check how to create a [Bucket Policy](https://docs.aws.amazon.com/serverlessrepo/latest/devguide/serverlessrepo-how-to-publish.html#publishing-application-through-cli). 66 | We refer to this bucket as ``. 67 | 68 | This step bundles all your code and configuration to the given S3 bucket. 69 | 70 | ```bash 71 | sam build 72 | ``` 73 | 74 | ```bash 75 | sam package \ 76 | --template-file template.yaml \ 77 | --output-template-file packaged.yaml \ 78 | --s3-bucket 79 | ``` 80 | 81 | This step updates your Cloud Formation stack to reflect the changes you made, which will in turn update changes made in the Lambda function. 82 | ```bash 83 | sam deploy \ 84 | --template-file packaged.yaml \ 85 | --stack-name workmail-salesforce \ 86 | --capabilities CAPABILITY_IAM 87 | ``` 88 | Your Lambda function is now deployed. You can now configure WorkMail to trigger this function. 89 | -------------------------------------------------------------------------------- /workmail-salesforce-python/dependencies/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.38.5 2 | simple-salesforce==1.12.6 3 | icalendar==6.1.3 -------------------------------------------------------------------------------- /workmail-salesforce-python/src/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | import email_utils 4 | import sf_utils 5 | from email.message import Message 6 | from botocore.exceptions import ClientError 7 | 8 | logger = logging.getLogger() 9 | logger.setLevel(logging.INFO) 10 | 11 | def salesforce_handler(event, context): 12 | """ 13 | Basic Integration With Salesforce Using WorkMail Lambda Integration 14 | Parameters 15 | ---------- 16 | email_summary: dict, required 17 | Amazon WorkMail Message Summary Input Format 18 | For more information, see https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html 19 | context: object, required 20 | Lambda Context runtime methods and attributes. See https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html 21 | Returns 22 | ------- 23 | Amazon WorkMail Sync Lambda Response Format 24 | For more information, see https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html#synchronous-schema 25 | """ 26 | from_address = event['envelope']['mailFrom']['address'] 27 | flow_direction = event['flowDirection'] 28 | message_id = event['messageId'] 29 | subject = event['subject'] if event['subject'] is not None else '' 30 | 31 | logger.info(f"Received email with message ID {message_id}, flowDirection {flow_direction}, from {from_address}") 32 | 33 | try: 34 | sf_client = sf_utils.create_sf_client() 35 | # 1. Download and parse the email using messageId 36 | parsed_email: Message = email_utils.download_email(message_id) 37 | # 2. Process the parsed email message in Salesforce 38 | sf_case: sf_utils.SalesforceCase = sf_utils.process_email(sf_client, parsed_email, event) 39 | # 3. Process the calendar invite in Salesforce if exists 40 | calendar_item = email_utils.extract_element(parsed_email, 'text/calendar') 41 | if calendar_item is not None: 42 | sf_utils.process_meeting_request(sf_client, calendar_item.get_content(), sf_case) 43 | 44 | # 4. Save updated email in WorkMail if required 45 | if sf_case.is_new_case: 46 | parsed_email.replace_header('Subject', f"[CaseId:{sf_case.case_id}] {subject}") 47 | 48 | if calendar_item is not None: 49 | sf_utils.update_icalendar_in_email(calendar_item, sf_case.case_id) 50 | 51 | email_utils.update_workmail(message_id, parsed_email) 52 | 53 | except ClientError as e: 54 | if e.response['Error']['Code'] == 'MessageFrozen': 55 | # Redirect emails are not eligible for update, handle it gracefully. 56 | logger.error(f"Message {message_id} is not eligible for update. This is usually the case for a redirected email") 57 | else: 58 | # Send some context about this error to Lambda Logs 59 | logger.error(e) 60 | if e.response['Error']['Code'] == 'ResourceNotFoundException': 61 | logger.error(f"Message {message_id} does not exist. Messages in transit are no longer accessible after 1 day") 62 | elif e.response['Error']['Code'] == 'InvalidContentLocation': 63 | logger.error('WorkMail could not access the updated email content. See https://docs.aws.amazon.com/workmail/latest/adminguide/update-with-lambda.html') 64 | raise(e) 65 | 66 | return { 67 | 'actions': [ 68 | { 69 | 'allRecipients': True, # For all recipients 70 | 'action': {'type': 'DEFAULT'} # let the email be sent normally 71 | } 72 | ] 73 | } 74 | -------------------------------------------------------------------------------- /workmail-salesforce-python/src/email_utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import boto3 3 | import email 4 | import uuid 5 | import re 6 | from email import policy 7 | import os 8 | 9 | workmail_message_flow = boto3.client('workmailmessageflow') 10 | s3 = boto3.client('s3') 11 | logger = logging.getLogger() 12 | logger.setLevel(logging.INFO) 13 | 14 | def extract_element(parsed_email, element): 15 | if parsed_email.is_multipart(): 16 | # Walk over message parts of this multipart email. 17 | for part in parsed_email.walk(): 18 | content_type = part.get_content_type() 19 | content_disposition = str(part.get_content_disposition()) 20 | if content_type == element and 'attachment' not in content_disposition: 21 | return part 22 | return None 23 | 24 | def extract_text_body(parsed_email): 25 | text_content = None 26 | text_charset = None 27 | if parsed_email.is_multipart(): 28 | # Walk over message parts of this multipart email. 29 | for part in parsed_email.walk(): 30 | content_type = part.get_content_type() 31 | content_disposition = str(part.get_content_disposition()) 32 | if content_type == 'text/plain' and 'attachment' not in content_disposition: 33 | text_content = part.get_payload(decode=True) 34 | text_charset = part.get_content_charset() 35 | break 36 | else: 37 | text_content = parsed_email.get_payload(decode=True) 38 | text_charset = parsed_email.get_content_charset() 39 | 40 | if text_content and text_charset: 41 | return text_content.decode(text_charset) 42 | return None 43 | 44 | def extract_username(user_address): 45 | first_name = 'None' 46 | last_name = 'None' 47 | if user_address is not None: 48 | display_name = user_address.split('<') 49 | if len(display_name) > 1: 50 | name = display_name[0].split(' ', 1) 51 | if len(name) > 1 and name[1] != '': 52 | first_name = name[0] 53 | last_name = name[1] 54 | else: 55 | first_name = name[0] 56 | return first_name, last_name 57 | 58 | def extract_case_id(subject): 59 | if subject: 60 | case_id = dict(re.findall(r'\[(CaseId):(\w+)\]', subject)) 61 | if 'CaseId' in case_id: 62 | case_id = case_id['CaseId'] 63 | logger.info(f"Processing CaseId: {case_id}") 64 | return case_id 65 | return None 66 | 67 | def download_email(message_id): 68 | raw_msg = workmail_message_flow.get_raw_message_content(messageId=message_id) 69 | email_generation_policy = policy.SMTP.clone(refold_source='none') 70 | return email.message_from_bytes(raw_msg['messageContent'].read(), policy=email_generation_policy) 71 | 72 | def update_workmail(message_id, content): 73 | bucket = os.getenv('UPDATED_EMAIL_BUCKET') 74 | if not bucket: 75 | raise ValueError("UPDATED_EMAIL_BUCKET not set in environment. Please follow https://docs.aws.amazon.com/lambda/latest/dg/env_variables.html to set it") 76 | 77 | key = str(uuid.uuid4()); 78 | s3.put_object(Body=content.as_bytes(), Bucket=bucket, Key=key) 79 | s3_reference = { 80 | 'bucket': bucket, 81 | 'key': key 82 | } 83 | content = { 84 | 's3Reference': s3_reference 85 | } 86 | workmail_message_flow.put_raw_message_content(messageId=message_id, content=content) 87 | logger.info("Updated email sent to WorkMail successfully") 88 | -------------------------------------------------------------------------------- /workmail-salesforce-python/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | WorkMail Salesforce Lambda SAM 5 | 6 | Parameters: 7 | Username: 8 | Type: String 9 | Description: "Enter the username of your Salesforce account" 10 | Password: 11 | Type: String 12 | NoEcho: True 13 | Description: "Enter the password of your Salesforce account" 14 | SecurityToken: 15 | Type: String 16 | NoEcho: True 17 | Description: "Enter the security token of your Salesforce account" 18 | 19 | Resources: 20 | WorkMailSalesforceDependencyLayer: 21 | Type: AWS::Serverless::LayerVersion 22 | Properties: 23 | ContentUri: dependencies/ 24 | CompatibleRuntimes: 25 | - python3.12 26 | Metadata: 27 | BuildMethod: python3.12 28 | 29 | WorkMailSalesforceFunction: 30 | Type: AWS::Serverless::Function 31 | Properties: 32 | CodeUri: src/ 33 | Handler: app.salesforce_handler 34 | Runtime: python3.12 35 | Timeout: 15 36 | Role: !GetAtt WorkMailSalesforceFunctionRole.Arn 37 | Layers: 38 | - !Ref WorkMailSalesforceDependencyLayer 39 | Environment: 40 | Variables: 41 | UPDATED_EMAIL_BUCKET: 42 | Ref: UpdatedEmailS3Bucket 43 | SF_SECRET_NAME: 44 | Ref: SFCredentials 45 | 46 | PermissionToCallLambdaAbove: 47 | Type: AWS::Lambda::Permission 48 | DependsOn: WorkMailSalesforceFunction 49 | Properties: 50 | Action: lambda:InvokeFunction 51 | FunctionName: !Ref WorkMailSalesforceFunction 52 | Principal: !Sub 'workmail.${AWS::Region}.amazonaws.com' 53 | SourceArn: !Sub 'arn:aws:workmail:${AWS::Region}:${AWS::AccountId}:organization/*' 54 | 55 | WorkMailSalesforceFunctionRole: 56 | Type: AWS::IAM::Role 57 | Properties: 58 | AssumeRolePolicyDocument: 59 | Statement: 60 | - Action: 61 | - sts:AssumeRole 62 | Effect: Allow 63 | Principal: 64 | Service: 65 | - "lambda.amazonaws.com" 66 | Version: "2012-10-17" 67 | Path: "/" 68 | Policies: 69 | - PolicyName: "WorkMailMessageFlowAccessToS3Bucket" 70 | PolicyDocument: 71 | Version: "2012-10-17" 72 | Statement: 73 | - Effect: Allow 74 | Action: 75 | - "s3:PutObject" 76 | Resource: !Sub "${UpdatedEmailS3Bucket.Arn}/*" 77 | Condition: 78 | Bool: 79 | aws:SecureTransport: 80 | true 81 | - PolicyName: "AllowSecretsManagerGetValue" 82 | PolicyDocument: 83 | Version: "2012-10-17" 84 | Statement: 85 | - Effect: Allow 86 | Action: 87 | - "secretsmanager:GetSecretValue" 88 | Resource: !Ref SFCredentials 89 | ManagedPolicyArns: 90 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 91 | - "arn:aws:iam::aws:policy/AmazonWorkMailMessageFlowFullAccess" 92 | 93 | # WorkMail configures a S3 bucket with all the required policy for reading updated messages. 94 | # By default all the object expire after 1 day. 95 | # To know more, see https://docs.aws.amazon.com/workmail/latest/adminguide/update-with-lambda.html 96 | UpdatedEmailS3Bucket: 97 | Type: AWS::S3::Bucket 98 | DeletionPolicy: Retain 99 | Properties: 100 | BucketEncryption: 101 | ServerSideEncryptionConfiguration: 102 | - ServerSideEncryptionByDefault: 103 | SSEAlgorithm: AES256 104 | PublicAccessBlockConfiguration: 105 | BlockPublicAcls: true 106 | BlockPublicPolicy: true 107 | IgnorePublicAcls: true 108 | RestrictPublicBuckets: true 109 | VersioningConfiguration: 110 | Status: Enabled 111 | LifecycleConfiguration: 112 | Rules: 113 | - Status: Enabled 114 | ExpirationInDays: 1 # Delete after 1 day 115 | - Status: Enabled 116 | NoncurrentVersionExpirationInDays: 1 # Delete non current versions after 1 day 117 | 118 | UpdatedEmailS3BucketPolicy: 119 | Type: AWS::S3::BucketPolicy 120 | Properties: 121 | Bucket: 122 | Ref: UpdatedEmailS3Bucket 123 | PolicyDocument: 124 | Statement: 125 | - Effect: Allow 126 | Action: 127 | - "s3:GetObject" 128 | - "s3:GetObjectVersion" 129 | Resource: !Sub "${UpdatedEmailS3Bucket.Arn}/*" 130 | Condition: 131 | Bool: 132 | aws:SecureTransport: true 133 | ArnLike: 134 | aws:SourceArn: !Sub 'arn:aws:workmailmessageflow:${AWS::Region}:${AWS::AccountId}:message/*' 135 | Principal: 136 | Service: !Sub 'workmail.${AWS::Region}.amazonaws.com' # This policy enables WorkMail to read objects from your bucket 137 | 138 | SFCredentials: 139 | Type: 'AWS::SecretsManager::Secret' 140 | Properties: 141 | Description: Salesforce credentials in JSON format 142 | SecretString: !Sub '{"username":"${Username}","password":"${Password}", "token":"${SecurityToken}"}' 143 | 144 | Outputs: 145 | SalesforceArn: 146 | Value: !GetAtt WorkMailSalesforceFunction.Arn 147 | -------------------------------------------------------------------------------- /workmail-salesforce-python/tst/env_vars.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkMailSalesforceFunction": { 3 | "SF_SECRET_NAME": "YOUR_SECRET_MANAGER_ARN", 4 | "UPDATED_EMAIL_BUCKET": "YOUR_UPDATED_EMAIL_BUCKET" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /workmail-salesforce-python/tst/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "summaryVersion": "2019-07-28", 3 | "envelope": { 4 | "mailFrom" : { 5 | "address" : "from@domain.test" 6 | }, 7 | "recipients" : [ 8 | { "address" : "recipient1@domain.test" }, 9 | { "address" : "recipient2@domain.test" } 10 | ] 11 | }, 12 | "sender" : { 13 | "address" : "sender@domain.test" 14 | }, 15 | "subject" : "Hello from Amazon WorkMail!", 16 | "messageId": "00000000-0000-0000-0000-000000000000", 17 | "invocationId": "0000000000000000000000000000000000000000", 18 | "flowDirection": "OUTBOUND", 19 | "truncated": false 20 | } 21 | -------------------------------------------------------------------------------- /workmail-save-and-update-email/dependencies/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.38.5 2 | beautifulsoup4==4.13.4 -------------------------------------------------------------------------------- /workmail-save-and-update-email/src/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import utils 3 | import uuid 4 | import os 5 | import json 6 | from botocore.exceptions import ClientError 7 | 8 | logger = logging.getLogger() 9 | logger.setLevel(logging.INFO) 10 | 11 | def update_handler(event, context): 12 | """ 13 | Save Original Email and Update Email Content Using WorkMail Lambda Integration 14 | 15 | Parameters 16 | ---------- 17 | email_summary: dict, required 18 | Amazon WorkMail Message Summary Input Format 19 | For more information, see https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html 20 | 21 | { 22 | "summaryVersion": "2019-07-28", # AWS WorkMail Message Summary Version 23 | "envelope": { 24 | "mailFrom" : { 25 | "address" : "from@domain.test" # String containing from email address 26 | }, 27 | "recipients" : [ # List of all recipient email addresses 28 | { "address" : "recipient1@domain.test" }, 29 | { "address" : "recipient2@domain.test" } 30 | ] 31 | }, 32 | "sender" : { 33 | "address" : "sender@domain.test" # String containing sender email address 34 | }, 35 | "subject" : "Hello From Amazon WorkMail!", # String containing email subject (Truncated to first 256 chars)" 36 | "messageId": "00000000-0000-0000-0000-000000000000", # String containing message id for retrieval using workmail flow API 37 | "invocationId": "00000000000000000000000000000000", # String containing the id of this lambda invocation. Useful for detecting retries and avoiding duplication 38 | "flowDirection": "INBOUND", # String indicating direction of email flow. Value is either "INBOUND" or "OUTBOUND" 39 | "truncated": false # boolean indicating if any field in message was truncated due to size limitations 40 | } 41 | 42 | context: object, required 43 | Lambda Context runtime methods and attributes. See https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html 44 | 45 | Returns 46 | ------- 47 | Amazon WorkMail Sync Lambda Response Format 48 | For more information, see https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html#synchronous-schema 49 | return { 50 | 'actions': [ # Required, should contain at least 1 list element 51 | { 52 | 'action' : { # Required 53 | 'type': 'string', # Required. Can be "BOUNCE", "DROP" or "DEFAULT" 54 | 'parameters': { } # Optional. For bounce, can be {"bounceMessage": "message that goes in bounce mail"} 55 | }, 56 | 'recipients': list of strings, # Optional. Indicates list of recipients for which this action applies 57 | 'default': boolean # Optional. Indicates whether this action applies to all recipients 58 | } 59 | ]} 60 | 61 | """ 62 | 63 | logger.info(f"Received event: {event}") 64 | email_from = event['envelope']['mailFrom'] 65 | recipients = event['envelope']['recipients'] 66 | message_id = event['messageId'] 67 | key = str(uuid.uuid4()) 68 | 69 | # Determine if the message is internal 70 | if utils.extract_domains([email_from]) == utils.extract_domains(recipients): 71 | internal_message = True 72 | else: 73 | internal_message = False 74 | 75 | update_internal_msg = (utils.get_env_var('UPDATE_INTERNAL_MESSAGES') == 'True') 76 | update_external_msg = (utils.get_env_var('UPDATE_EXTERNAL_MESSAGES') == 'True') 77 | save_and_update_msg = False 78 | 79 | if internal_message: 80 | if update_internal_msg: 81 | save_and_update_msg = True 82 | else: 83 | if update_external_msg: 84 | save_and_update_msg = True 85 | 86 | try: 87 | # 1. Download email 88 | downloaded_email = utils.download_email(message_id) 89 | # 2. Save and update original email 90 | if save_and_update_msg: 91 | saved_bucket = utils.get_env_var('SAVED_EMAIL_BUCKET') 92 | updated_bucket = utils.get_env_var('UPDATED_EMAIL_BUCKET') 93 | # 3. Save the orginal, unmodified, email message source 94 | utils.save_email(saved_bucket, downloaded_email.as_bytes(), key + ".eml") 95 | # 4. Save the event data (metadata) about the message so we know the envelope details that aren't in the message source 96 | utils.save_email(saved_bucket, json.dumps(event), key + ".json") 97 | # 5. Update the email with the desired modifications 98 | updated_email = utils.update_email(downloaded_email, event['subject'], event['flowDirection'], key) 99 | logger.info("Providing modified message for WorkMail") 100 | utils.update_workmail(message_id, updated_bucket, updated_email, key) 101 | else: 102 | logger.info("Preserving original message for WorkMail") 103 | 104 | except ClientError as e: 105 | if e.response['Error']['Code'] == 'MessageFrozen': 106 | # Redirect emails are not eligible for update, handle it gracefully. 107 | logger.info(f"Message {message_id} is not eligible for update. This is usually the case for a redirected email") 108 | else: 109 | logger.error(e.response['Error']['Message']) 110 | if e.response['Error']['Code'] == 'ResourceNotFoundException': 111 | logger.error(f"Message {message_id} does not exist. Messages in transit are no longer accessible after 1 day") 112 | elif e.response['Error']['Code'] == 'InvalidContentLocation': 113 | logger.error('WorkMail could not access the updated email content. See https://docs.aws.amazon.com/workmail/latest/adminguide/update-with-lambda.html') 114 | raise(e) 115 | 116 | # Resume normal email flow 117 | return { 118 | 'actions' : [{ 119 | 'action' : { 120 | 'type' : 'DEFAULT' 121 | }, 122 | 'allRecipients': 'true' 123 | }] 124 | } 125 | -------------------------------------------------------------------------------- /workmail-save-and-update-email/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: 4 | "WorkMail Save And Update Email" 5 | 6 | Parameters: 7 | Disclaimer: 8 | Type: String 9 | Default: '' 10 | Description: "[Optional] Text that you'd like to prepend to the email body." 11 | Footer: 12 | Type: String 13 | Default: '' 14 | Description: "[Optional] Text that you'd like to append to the email body. Use {key} to template the S3 object key for the saved message." 15 | SubjectTag: 16 | Type: String 17 | Default: '' 18 | Description: "[Optional] Text that you'd like to prepend to the email subject. Use {key} to template the S3 object key for the saved message." 19 | SavedBucketExpiration: 20 | Type: Number 21 | Default: '1' 22 | Description: "[Optional] Number of days to keep the saved messages in the S3 bucket. Defaults to 1." 23 | UpdateInternalMessages: 24 | Type: String 25 | Default: 'False' 26 | AllowedValues: 27 | - 'True' 28 | - 'False' 29 | Description: "[Optional] Determines if internal messages should be updated." 30 | UpdateExternalMessages: 31 | Type: String 32 | Default: 'True' 33 | AllowedValues: 34 | - 'True' 35 | - 'False' 36 | Description: "[Optional] Determines if external messages should be updated." 37 | 38 | Resources: 39 | WorkMailSaveAndUpdateEmailDependencyLayer: 40 | Type: AWS::Serverless::LayerVersion 41 | Properties: 42 | ContentUri: dependencies/ 43 | CompatibleRuntimes: 44 | - python3.12 45 | Metadata: 46 | BuildMethod: python3.12 47 | 48 | WorkMailSaveAndUpdateEmailFunction: 49 | Type: AWS::Serverless::Function 50 | DependsOn: 51 | - WorkMailUpdatedMsgBucket 52 | - WorkMailSavedMsgBucket 53 | Properties: 54 | CodeUri: src/ 55 | Handler: app.update_handler 56 | Runtime: python3.12 57 | Timeout: 10 58 | Role: 59 | Fn::GetAtt: WorkMailSaveAndUpdateEmailFunctionRole.Arn 60 | Layers: 61 | - !Ref WorkMailSaveAndUpdateEmailDependencyLayer 62 | Environment: 63 | Variables: 64 | DISCLAIMER: 65 | Ref: Disclaimer 66 | FOOTER: 67 | Ref: Footer 68 | UPDATED_EMAIL_BUCKET: 69 | Ref: WorkMailUpdatedMsgBucket 70 | SAVED_EMAIL_BUCKET: 71 | Ref: WorkMailSavedMsgBucket 72 | SUBJECT_TAG: 73 | Ref: SubjectTag 74 | UPDATE_INTERNAL_MESSAGES: 75 | Ref: UpdateInternalMessages 76 | UPDATE_EXTERNAL_MESSAGES: 77 | Ref: UpdateExternalMessages 78 | 79 | WorkMailSaveAndUpdateEmailFunctionRole: 80 | Type: AWS::IAM::Role 81 | Properties: 82 | AssumeRolePolicyDocument: 83 | Statement: 84 | - Action: 85 | - sts:AssumeRole 86 | Effect: Allow 87 | Principal: 88 | Service: 89 | - "lambda.amazonaws.com" 90 | Version: "2012-10-17" 91 | Path: "/" 92 | ManagedPolicyArns: 93 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 94 | - "arn:aws:iam::aws:policy/AmazonWorkMailMessageFlowFullAccess" 95 | Policies: 96 | - 97 | PolicyName: "allow-s3-write" 98 | PolicyDocument: 99 | Version: "2012-10-17" 100 | Statement: 101 | - 102 | Effect: "Allow" 103 | Action: 104 | - "s3:PutObject" 105 | Resource: 106 | - Fn::Sub: "${WorkMailUpdatedMsgBucket.Arn}/*" 107 | - Fn::Sub: "${WorkMailSavedMsgBucket.Arn}/*" 108 | 109 | WorkMailPermissionToInvokeLambda: 110 | Type: AWS::Lambda::Permission 111 | DependsOn: WorkMailSaveAndUpdateEmailFunction 112 | Properties: 113 | Action: lambda:InvokeFunction 114 | FunctionName: !Ref WorkMailSaveAndUpdateEmailFunction 115 | Principal: !Sub 'workmail.${AWS::Region}.amazonaws.com' 116 | SourceArn: !Sub 'arn:aws:workmail:${AWS::Region}:${AWS::AccountId}:organization/*' 117 | 118 | WorkMailUpdatedMsgBucket: 119 | Type: AWS::S3::Bucket 120 | DeletionPolicy: Retain 121 | Properties: 122 | BucketEncryption: 123 | ServerSideEncryptionConfiguration: 124 | - ServerSideEncryptionByDefault: 125 | SSEAlgorithm: AES256 126 | PublicAccessBlockConfiguration: 127 | BlockPublicAcls : true 128 | BlockPublicPolicy : true 129 | IgnorePublicAcls : true 130 | RestrictPublicBuckets : true 131 | VersioningConfiguration: 132 | Status: Enabled 133 | LifecycleConfiguration: 134 | Rules: 135 | - 136 | Status: Enabled 137 | ExpirationInDays: 1 # Delete after 1 day 138 | - 139 | Status: Enabled 140 | NoncurrentVersionExpirationInDays : 1 # Delete non current versions after 1 day 141 | 142 | WorkMailSavedMsgBucket: 143 | Type: AWS::S3::Bucket 144 | DeletionPolicy: Retain 145 | Properties: 146 | BucketEncryption: 147 | ServerSideEncryptionConfiguration: 148 | - ServerSideEncryptionByDefault: 149 | SSEAlgorithm: AES256 150 | PublicAccessBlockConfiguration: 151 | BlockPublicAcls : true 152 | BlockPublicPolicy : true 153 | IgnorePublicAcls : true 154 | RestrictPublicBuckets : true 155 | VersioningConfiguration: 156 | Status: Enabled 157 | LifecycleConfiguration: 158 | Rules: 159 | - 160 | Status: Enabled 161 | ExpirationInDays: 162 | Ref: SavedBucketExpiration 163 | - 164 | Status: Enabled 165 | NoncurrentVersionExpirationInDays: 166 | Ref: SavedBucketExpiration 167 | 168 | WorkMailUpdatedMsgBucketPolicy: 169 | Type: AWS::S3::BucketPolicy 170 | Properties: 171 | Bucket: 172 | Ref: WorkMailUpdatedMsgBucket 173 | PolicyDocument: 174 | Statement: 175 | - Action: 176 | - "s3:GetObject" 177 | - "s3:GetObjectVersion" 178 | Effect: Allow 179 | Resource: 180 | - Fn::Sub: "${WorkMailUpdatedMsgBucket.Arn}/*" 181 | Condition: 182 | Bool: 183 | aws:SecureTransport: 184 | true 185 | ArnLike: 186 | aws:SourceArn: !Sub 'arn:aws:workmailmessageflow:${AWS::Region}:${AWS::AccountId}:message/*' 187 | Principal: 188 | Service: !Sub 'workmail.${AWS::Region}.amazonaws.com' 189 | 190 | Outputs: 191 | UpdateEmailArn: 192 | Value: !GetAtt WorkMailSaveAndUpdateEmailFunction.Arn 193 | -------------------------------------------------------------------------------- /workmail-save-and-update-email/tst/env_vars.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkMailSaveAndUpdateEmailFunction": { 3 | "SAVED_EMAIL_BUCKET": "SAVED_EMAIL_BUCKET", 4 | "UPDATED_EMAIL_BUCKET": "UPDATED_EMAIL_BUCKET", 5 | "DISCLAIMER": "YOUR_CUSTOM_INTERNAL_DISCLAIMER_TEXT {key}", 6 | "FOOTER": "YOUR_CUSTOM_INTERNAL_FOOTER_TEXT {key}", 7 | "SUBJECT_TAG": "YOUR_CUSTOM_INTERNAL_SUBJECT_TAG", 8 | "UPDATE_INTERNAL_MESSAGES": "False", 9 | "UPDATE_EXTERNAL_MESSAGES": "True" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /workmail-save-and-update-email/tst/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "summaryVersion": "2018-10-10", 3 | "envelope": { 4 | "mailFrom" : { 5 | "address" : "from@domainA.test" 6 | }, 7 | "recipients" : [ 8 | { "address" : "recipient1@domainB.test" }, 9 | { "address" : "recipient2@domainB.test" } 10 | ] 11 | }, 12 | "sender" : { 13 | "address" : "sender@domain.test" 14 | }, 15 | "subject" : "Hello from Amazon WorkMail!", 16 | "messageId": "00000000-0000-0000-0000-000000000000", 17 | "invocationId": "0000000000000000000000000000000000000000", 18 | "flowDirection": "INBOUND", 19 | "truncated": false 20 | } 21 | -------------------------------------------------------------------------------- /workmail-save-and-update-email/workmail-save-and-update-email.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-workmail-lambda-templates/b570142cba833429f085a27497d1de9ce0326dbc/workmail-save-and-update-email/workmail-save-and-update-email.jpg -------------------------------------------------------------------------------- /workmail-stop-mail-storm/src/app.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true, esversion: 9 */ 2 | 'use strict'; 3 | 4 | // this is automatically installed in your lambda environment 5 | const { CloudWatch } = require('@aws-sdk/client-cloudwatch'); 6 | const cloudwatch = new CloudWatch(); // uses your lambda credentials to call Cloudwatch 7 | 8 | const ALARM_PREFIX = 'EmailsReceived-'; 9 | const DEFAULT_THRESHOLD = 20; // in case environment variable THRESHOLD is missing. 10 | 11 | async function emitNewMetric(protectedRecipients) { 12 | // see https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricData.html for detailed 13 | // explanation of the parameters 14 | const params = { 15 | Namespace: 'WorkMail', 16 | MetricData: protectedRecipients.map(protectedRecipient => 17 | ({ 18 | MetricName: 'EmailsReceived', 19 | Dimensions: [ 20 | { 21 | Name: 'EmailAddress', 22 | Value: protectedRecipient 23 | } 24 | ], 25 | Unit: 'Count', 26 | Value: 1 27 | })) 28 | }; 29 | 30 | // it is important that we emit the metric even if we block this sending later 31 | // otherwise, the alarm would clear after 5 mins, people would be able to continue the storm for a while again 32 | // that would lead to a on-off-on-off pattern, which still lets too much junk through 33 | 34 | await cloudwatch.putMetricData(params); 35 | console.log('Finished putMetricData'); 36 | } 37 | 38 | async function createMissingAlarms(alarms, protectedRecipients) { 39 | // find the protected addresses for which there is already an alarm 40 | const protectedAddressesWithAlarm = alarms.MetricAlarms 41 | .map(metricAlarm => metricAlarm.Dimensions[0].Value); 42 | 43 | // remove those from all alarms. the remaining addresses are missing alarms 44 | const protectedAddressesMissingAlarm = protectedRecipients 45 | .filter(protectedAddress => !protectedAddressesWithAlarm.includes(protectedAddress)); 46 | 47 | // create the alarm for those 48 | for (let protectedAddress of protectedAddressesMissingAlarm) { 49 | // this will create an alarm that will fire if we receive in 3 different minutes more than THRESHOLD emails 50 | // per minute, in a window of the last 5 minutes. 51 | 52 | // see https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_PutMetricAlarm.html for detailed 53 | // explanation of the parameters 54 | const params = { 55 | AlarmName: ALARM_PREFIX + protectedAddress, 56 | ComparisonOperator: 'GreaterThanThreshold', 57 | AlarmDescription: 'Mail storm in progress for group ' + protectedAddress, 58 | Dimensions: [ 59 | { 60 | Name: 'EmailAddress', 61 | Value: protectedAddress 62 | }, 63 | ], 64 | MetricName: 'EmailsReceived', 65 | Namespace: 'WorkMail', 66 | TreatMissingData: 'notBreaching', 67 | // The parameters below control how sensitive the detection for mailstorm is. 68 | DatapointsToAlarm: 3, // alarm if 3 of the last 5 datapoints are above threshold 69 | EvaluationPeriods: 5, 70 | Period: 60, // each data point for evaluation is a sum of emails received in the last 60 s. 71 | Statistic: 'Sum', 72 | // configure using the THRESHOLD lambda environment variable 73 | Threshold: parseInt(process.env.THRESHOLD || DEFAULT_THRESHOLD), 74 | 75 | }; 76 | console.log('Creating alarm for address ' + protectedAddress); 77 | const alarm = await cloudwatch.putMetricAlarm(params); 78 | console.log(alarm); 79 | } 80 | } 81 | 82 | async function verifyAlarms(protectedRecipients) { 83 | // see https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_DescribeAlarms.html for detailed 84 | // explanation of the parameters 85 | const params = { 86 | AlarmNames: protectedRecipients.map(protectedAddress => ALARM_PREFIX + protectedAddress), 87 | AlarmTypes: ['MetricAlarm'], 88 | }; 89 | 90 | const allAlarms = await cloudwatch.describeAlarms(params); 91 | console.log('Finished describeAlarms - Results:'); 92 | console.log(allAlarms); 93 | 94 | const protectedAddressesInAlarm = allAlarms.MetricAlarms 95 | .filter(metricAlarm => metricAlarm.StateValue === 'ALARM') 96 | .map(metricAlarm => metricAlarm.Dimensions[0].Value); 97 | 98 | return {allAlarms, protectedAddressesInAlarm}; 99 | } 100 | 101 | exports.lambdaHandler = async (event) => { 102 | console.log('Event received by lambda function:'); 103 | console.log(JSON.stringify(event)); // see event documentation in https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html 104 | console.log('Environment variables:'); 105 | console.log(JSON.stringify(process.env)); 106 | 107 | // split on comma, and cleanup whitespace around 108 | const allProtectedAddresses = (process.env.PROTECTED_ADDRESSES || '') 109 | .split(',').map(address => address.trim()); 110 | 111 | console.log('Email addresses to protect against mail storms: ' + allProtectedAddresses); 112 | 113 | // take only the email address of each recipient 114 | const recipients = event.envelope.recipients.map(recipient => recipient.address); 115 | 116 | // find the protected recipients among all recipients 117 | const protectedRecipients = recipients.filter(recipient => allProtectedAddresses.includes(recipient)); 118 | 119 | // in this case, we can simply let the email pass, and avoid looking at all alarms 120 | if (protectedRecipients.length === 0) { 121 | console.log('No recipient in this email is protected against mail storm. Letting the email pass.'); 122 | return { 123 | actions: [ 124 | { 125 | allRecipients: true, // for all recipients 126 | action: {type: 'DEFAULT'} // let the email be sent normally 127 | } 128 | ] 129 | }; 130 | } 131 | 132 | await emitNewMetric(protectedRecipients); 133 | const {allAlarms, protectedAddressesInAlarm} = await verifyAlarms(protectedRecipients); 134 | 135 | // an alarm can be missing for a protected address if it is the first time we receive an email for it 136 | await createMissingAlarms(allAlarms, protectedRecipients); 137 | 138 | if (protectedAddressesInAlarm.length > 0) { 139 | console.log('Bouncing email from ' + event.envelope.mailFrom.address + ' to ' + protectedAddressesInAlarm); 140 | } 141 | 142 | return { 143 | actions: [ 144 | { 145 | recipients: protectedAddressesInAlarm, // for the recipients in alarm 146 | action: {type: 'BOUNCE'}, // make it bounce 147 | }, 148 | { 149 | allRecipients: true, // for all the other recipients 150 | action: {type: 'DEFAULT'}, // let the email pass normally 151 | } 152 | ] 153 | }; 154 | }; 155 | -------------------------------------------------------------------------------- /workmail-stop-mail-storm/src/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "workmail-stop-mail-storm", 3 | "version": "1.0.0", 4 | "description": "A sample lambda email flow rule for Amazon WorkMail, intended to stop mail storms to large groups. For more information, visit https://github.com/aws-samples/amazon-workmail-lambda-templates", 5 | "main": "app.js", 6 | "scripts": { 7 | "jshint": "jshint *.js", 8 | "test": "sam build -t ../template.yaml && sam local invoke -e ../tst/event.json -n ../tst/environment_variables.json" 9 | }, 10 | "author": "Amazon WorkMail", 11 | "license": "Apache-2.0", 12 | "dependencies": { 13 | "jshint": "^2.11.0" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /workmail-stop-mail-storm/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: Lambda function to stop mail storms using WorkMail rules 4 | 5 | Parameters: 6 | ProtectedAddresses: 7 | Type: CommaDelimitedList 8 | Default: '' 9 | Description: "List of email addresses to stop mail storms, comma-separated. Example: big_group1@example.com, big_group2@example.com" 10 | MailStormThreshold: 11 | Type: Number 12 | MinValue: 1 13 | MaxValue: 10000 14 | Description: "Threshold of number of emails per minute that will trigger the mail storm protection" 15 | 16 | Resources: 17 | StopMailStormFunction: 18 | Type: AWS::Serverless::Function 19 | Properties: 20 | CodeUri: src/ 21 | Handler: app.lambdaHandler 22 | Runtime: nodejs22.x 23 | Timeout: 10 24 | Role: !GetAtt StopMailStormFunctionRole.Arn 25 | Environment: 26 | Variables: 27 | PROTECTED_ADDRESSES: !Join [ ",", !Ref ProtectedAddresses ] 28 | THRESHOLD: !Ref MailStormThreshold 29 | 30 | StopMailStormFunctionRole: 31 | Type: AWS::IAM::Role 32 | Properties: 33 | AssumeRolePolicyDocument: 34 | Statement: 35 | - Action: 36 | - sts:AssumeRole 37 | Effect: Allow 38 | Principal: 39 | Service: 40 | - "lambda.amazonaws.com" 41 | Version: "2012-10-17" 42 | Path: "/" 43 | ManagedPolicyArns: 44 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 45 | Policies: 46 | - PolicyName: "allow-cloudwatch-actions" 47 | PolicyDocument: 48 | Version: "2012-10-17" 49 | Statement: 50 | - Effect: "Allow" 51 | Action: 52 | - "cloudwatch:PutMetricData" 53 | - "cloudwatch:PutMetricAlarm" 54 | - "cloudwatch:DescribeAlarms" 55 | Resource: "*" 56 | 57 | 58 | PermissionToCallLambda: 59 | Type: AWS::Lambda::Permission 60 | DependsOn: StopMailStormFunction 61 | Properties: 62 | Action: lambda:InvokeFunction 63 | FunctionName: !Ref StopMailStormFunction 64 | Principal: !Sub 'workmail.${AWS::Region}.amazonaws.com' 65 | SourceArn: !Sub 'arn:aws:workmail:${AWS::Region}:${AWS::AccountId}:organization/*' 66 | 67 | Outputs: 68 | StopMailStormFunctionArn: 69 | Value: !GetAtt StopMailStormFunction.Arn 70 | -------------------------------------------------------------------------------- /workmail-stop-mail-storm/tst/environment_variables.json: -------------------------------------------------------------------------------- 1 | { 2 | "StopMailStormFunction": { 3 | "PROTECTED_ADDRESSES": "big_group1@domain.test, big_group2@domain.test", 4 | "THRESHOLD": 20 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /workmail-stop-mail-storm/tst/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "summaryVersion": "2018-10-10", 3 | "envelope": { 4 | "mailFrom" : { 5 | "address" : "from@domain.test" 6 | }, 7 | "recipients" : [ 8 | { "address" : "recipient1@domain.test" }, 9 | { "address" : "big_group1@domain.test" } 10 | ] 11 | }, 12 | "sender" : { 13 | "address" : "sender@domain.test" 14 | }, 15 | "subject" : "Hello From Amazon WorkMail!", 16 | "messageId": "00000000-0000-0000-0000-000000000000", 17 | "invocationId": "0000000000000000000000000000000000000000", 18 | "flowDirection": "INBOUND", 19 | "truncated": false 20 | } 21 | -------------------------------------------------------------------------------- /workmail-translate-email/Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-workmail-lambda-templates/b570142cba833429f085a27497d1de9ce0326dbc/workmail-translate-email/Image.png -------------------------------------------------------------------------------- /workmail-translate-email/README.md: -------------------------------------------------------------------------------- 1 | # Amazon WorkMail Translate Email 2 | This application translates an incoming or outgoing email into a language of your choice. 3 | 4 | Specifically, the application automatically detects the language of an email and then translates subject and body into the destination language that you provide during setup. The translated body and subject is then appended to the original body and original subject of the email respectively. 5 | 6 | ![Screenshot](Image.png) 7 | 8 | ## Setup 9 | 1. Deploy this application via [AWS Serverless Application Repository](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:489970191081:applications~workmail-translate-email). 10 | 2. Enter [language code](https://docs.aws.amazon.com/translate/latest/dg/what-is.html#what-is-languages) of the language you want your email to be translated into. 11 | 3. Configure a synchronous Run Lambda rule over the Lambda function created in step 1. See [instructions.](https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html#synchronous-rules) 12 | 13 | It is possible to configure both inbound and outbound email flow rules over the same Lambda function. 14 | 15 | You now have a working Lambda function that will be triggered by WorkMail based on the rule you created. 16 | 17 | To further customize your Lambda function, open the [AWS Lambda Console](https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions) to edit and test your Lambda function with the built-in code editor. 18 | 19 | For more information, see [documentation](https://docs.aws.amazon.com/lambda/latest/dg/code-editor.html). 20 | 21 | For more advanced use cases, such as changing your CloudFormation template to create additional AWS resources that will support this application, follow the instructions below. 22 | 23 | ## Access Control 24 | By default, this serverless application and the resources that it creates can integrate with any [WorkMail Organization](https://docs.aws.amazon.com/workmail/latest/adminguide/organizations_overview.html) in your account, but the application and organization must be in the same region. To restrict that behavior you can either update the SourceArn attribute in [template.yaml](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-translate-email/template.yaml) 25 | and then deploy the application by following the steps below **or** update the SourceArn attribute directly in the resource policy of each resource via their AWS Console after the deploying this application, [see example](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html). 26 | 27 | For more information about the SourceArn attribute, [see this documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-sourcearn). 28 | 29 | ## Development 30 | Clone this repository from [GitHub](https://github.com/aws-samples/amazon-workmail-lambda-templates). 31 | 32 | We recommend creating and activating a virtual environment, for more information see [Creation of virtual environments](https://docs.python.org/3/library/venv.html). 33 | 34 | If you are not familiar with CloudFormation templates, see [Learn Template Basics](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/gettingstarted.templatebasics.html). 35 | 36 | 1. Create additional resources for your application by changing [template.yaml](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-translate-email/template.yaml). For more information, see [documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-reference.html). 37 | 2. Modify your Lambda function by changing [app.py](https://github.com/aws-samples/amazon-workmail-lambdas-templates/blob/master/workmail-translate-email/src/app.py). 38 | 3. Test your Lambda function locally: 39 | 1. [Set up the SAM CLI](https://aws.amazon.com/serverless/sam/). 40 | 2. Configure environment variables at `tst/env_vars.json`. 41 | 3. Configure test event at `tst/event.json`. 42 | 4. Invoke your Lambda function locally using: 43 | 44 | `sam local invoke WorkMailTranslateEmailFunction -e tst/event.json --env-vars tst/env_vars.json` 45 | 46 | ### Test Message Ids 47 | This application uses a `messageId` passed to the Lambda function to retrieve the message content from WorkMail. When testing, the `tst/event.json` file uses a mock messageId which does not exist. If you want to test with a real messageId, you can configure a WorkMail Email Flow Rule with the Lambda action that uses the Lambda function created in **Setup**, and send some emails that will trigger the email flow rule. The Lambda function will emit the messageId it receives from WorkMail in the CloudWatch logs, which you can 48 | then use in your test event data. For more information see [Accessing Amazon CloudWatch logs for AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html). Note that you can access messages in transit for a maximum of one day. 49 | 50 | Once you have validated that your Lambda function behaves as expected, you are ready to deploy this Lambda function. 51 | 52 | ### Deployment 53 | If you develop using the AWS Lambda Console, then this section can be skipped. 54 | 55 | Please create an S3 bucket if you do not have one yet, see [How do I create an S3 Bucket?](https://docs.aws.amazon.com/AmazonS3/latest/user-guide/create-bucket.html). 56 | and check how to create a [Bucket Policy](https://docs.aws.amazon.com/serverlessrepo/latest/devguide/serverlessrepo-how-to-publish.html#publishing-application-through-cli). 57 | We refer to this bucket as ``. 58 | 59 | This step bundles all your code and configuration to the given S3 bucket. 60 | 61 | ```bash 62 | sam build 63 | ``` 64 | 65 | ```bash 66 | $ sam package \ 67 | --template-file template.yaml \ 68 | --output-template-file packaged.yaml \ 69 | --s3-bucket 70 | ``` 71 | 72 | This step updates your CloudFormation stack to reflect the changes you made, which will in turn update changes made in the Lambda function. 73 | ```bash 74 | $ sam deploy \ 75 | --template-file packaged.yaml \ 76 | --stack-name workmail-translate-email \ 77 | --parameter-overrides DestinationLanguage= \ 78 | --capabilities CAPABILITY_IAM 79 | ``` 80 | Your Lambda function is now deployed. You can now configure WorkMail to trigger this function. 81 | -------------------------------------------------------------------------------- /workmail-translate-email/dependencies/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.38.5 2 | beautifulsoup4==4.13.4 -------------------------------------------------------------------------------- /workmail-translate-email/src/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import utils 3 | import translate_helper 4 | from botocore.exceptions import ClientError 5 | 6 | logger = logging.getLogger() 7 | logger.setLevel(logging.INFO) 8 | 9 | def translate_handler(event, context): 10 | """ 11 | Translate Email Content Using WorkMail Lambda Integration 12 | 13 | Parameters 14 | ---------- 15 | email_summary: dict, required 16 | Amazon WorkMail Message Summary Input Format 17 | For more information, see https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html 18 | 19 | { 20 | "summaryVersion": "2019-07-28", # AWS WorkMail Message Summary Version 21 | "envelope": { 22 | "mailFrom" : { 23 | "address" : "from@domain.test" # String containing from email address 24 | }, 25 | "recipients" : [ # List of all recipient email addresses 26 | { "address" : "recipient1@domain.test" }, 27 | { "address" : "recipient2@domain.test" } 28 | ] 29 | }, 30 | "sender" : { 31 | "address" : "sender@domain.test" # String containing sender email address 32 | }, 33 | "subject" : "Hello From Amazon WorkMail!", # String containing email subject (Truncated to first 256 chars)" 34 | "messageId": "00000000-0000-0000-0000-000000000000", # String containing message id for retrieval using workmail flow API 35 | "invocationId": "00000000000000000000000000000000", # String containing the id of this lambda invocation. Useful for detecting retries and avoiding duplication 36 | "flowDirection": "INBOUND", # String indicating direction of email flow. Value is either "INBOUND" or "OUTBOUND" 37 | "truncated": false # boolean indicating if any field in message was truncated due to size limitations 38 | } 39 | 40 | context: object, required 41 | Lambda Context runtime methods and attributes. See https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html 42 | 43 | Returns 44 | ------- 45 | Amazon WorkMail Sync Lambda Response Format 46 | For more information, see https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html#synchronous-schema 47 | return { 48 | 'actions': [ # Required, should contain at least 1 list element 49 | { 50 | 'action' : { # Required 51 | 'type': 'string', # Required. Can be "BOUNCE", "DROP" or "DEFAULT" 52 | 'parameters': { } # Optional. For bounce, can be {"bounceMessage": "message that goes in bounce mail"} 53 | }, 54 | 'recipients': list of strings, # Optional. Indicates list of recipients for which this action applies 55 | 'default': boolean # Optional. Indicates whether this action applies to all recipients 56 | } 57 | ]} 58 | 59 | """ 60 | 61 | logger.info(f"Received event: {event}") 62 | message_id = event['messageId'] 63 | try: 64 | # 1. Download email 65 | downloaded_email = utils.download_email(message_id) 66 | # 2. Detect email language 67 | text_body = utils.extract_text_body(downloaded_email) 68 | # Use first 100 characters of email body and email subject to detect email source language 69 | email_language = translate_helper.detect_language(f"{event['subject']} {text_body[:100]}") 70 | if email_language != utils.get_env_var('DESTINATION_LANGUAGE'): 71 | # 3. Translate email 72 | translated_email = utils.translate_email(downloaded_email, event['subject'], email_language, text_body) 73 | # 4. Send translated email back to WorkMail 74 | utils.update_workmail(message_id, translated_email) 75 | else: 76 | logger.info('Email is already in destination language') 77 | except ClientError as e: 78 | if e.response['Error']['Code'] == 'MessageFrozen': 79 | # Redirect emails are not eligible for update, handle it gracefully. 80 | logger.info(f"Message {message_id} is not eligible for update. This is usually the case for a redirected email") 81 | else: 82 | logger.error(e.response['Error']['Message']) 83 | if e.response['Error']['Code'] == 'ResourceNotFoundException': 84 | logger.error(f"Message {message_id} does not exist. Messages in transit are no longer accessible after 1 day") 85 | elif e.response['Error']['Code'] == 'InvalidContentLocation': 86 | logger.error('WorkMail could not access the updated email content. See https://docs.aws.amazon.com/workmail/latest/adminguide/update-with-lambda.html') 87 | raise(e) 88 | 89 | # Resume normal email flow 90 | return { 91 | 'actions' : [{ 92 | 'action' : { 93 | 'type' : 'DEFAULT' 94 | }, 95 | 'allRecipients': 'true' 96 | }] 97 | } 98 | -------------------------------------------------------------------------------- /workmail-translate-email/src/translate_helper.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | 3 | comprehend = boto3.client(service_name='comprehend') 4 | translate = boto3.client(service_name='translate') 5 | 6 | def detect_language(text): 7 | """ 8 | Detects the dominant language in a text 9 | Parameters 10 | ---------- 11 | text: string, required 12 | Input text 13 | Returns 14 | ------- 15 | string 16 | Representing language code of the dominant language 17 | """ 18 | # Sending call to get language 19 | result = comprehend.detect_dominant_language(Text = text)['Languages'] 20 | # Since the result can contain more than one language find the one with the highest score. 21 | high_score = 0 22 | best_guess = '' 23 | for lang in range(len(result)): 24 | if result[lang]['Score'] > high_score: 25 | high_score = result[lang]['Score'] 26 | best_guess = result[lang]['LanguageCode'] 27 | 28 | return best_guess 29 | 30 | def translate_text(text, source_lang, destination_lang): 31 | """ 32 | Translates given text from source language into destination language 33 | Parameters 34 | ---------- 35 | text: string, required 36 | Input text in source language 37 | Returns 38 | ------- 39 | string 40 | Translated text in destination language 41 | """ 42 | result = translate.translate_text(Text=text, 43 | SourceLanguageCode=source_lang, TargetLanguageCode=destination_lang) 44 | return result.get('TranslatedText') 45 | -------------------------------------------------------------------------------- /workmail-translate-email/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: 4 | "WorkMail Translate Email" 5 | 6 | Parameters: 7 | DestinationLanguage: 8 | Type: String 9 | MinLength: 1 10 | MaxLength: 2 11 | Description: "Code of the language to translate into. Refer: https://docs.aws.amazon.com/translate/latest/dg/what-is.html#what-is-languages" 12 | 13 | Resources: 14 | WorkMailTranslateEmailDependencyLayer: 15 | Type: AWS::Serverless::LayerVersion 16 | Properties: 17 | ContentUri: dependencies/ 18 | CompatibleRuntimes: 19 | - python3.12 20 | Metadata: 21 | BuildMethod: python3.12 22 | 23 | WorkMailTranslateEmailFunction: 24 | Type: AWS::Serverless::Function 25 | DependsOn: WorkMailTranslatedMsgBucket 26 | Properties: 27 | CodeUri: src/ 28 | Handler: app.translate_handler 29 | Runtime: python3.12 30 | Timeout: 10 31 | Role: 32 | Fn::GetAtt: WorkMailTranslateEmailFunctionRole.Arn 33 | Layers: 34 | - !Ref WorkMailTranslateEmailDependencyLayer 35 | Environment: 36 | Variables: 37 | DESTINATION_LANGUAGE: 38 | Ref: DestinationLanguage 39 | TRANSLATED_EMAIL_BUCKET: 40 | Ref: WorkMailTranslatedMsgBucket 41 | 42 | WorkMailTranslateEmailFunctionRole: 43 | Type: AWS::IAM::Role 44 | Properties: 45 | AssumeRolePolicyDocument: 46 | Statement: 47 | - Action: 48 | - sts:AssumeRole 49 | Effect: Allow 50 | Principal: 51 | Service: 52 | - "lambda.amazonaws.com" 53 | Version: "2012-10-17" 54 | Path: "/" 55 | ManagedPolicyArns: 56 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 57 | - "arn:aws:iam::aws:policy/ComprehendReadOnly" 58 | - "arn:aws:iam::aws:policy/TranslateReadOnly" 59 | - "arn:aws:iam::aws:policy/AmazonWorkMailMessageFlowFullAccess" 60 | Policies: 61 | - 62 | PolicyName: "allow-s3-write" 63 | PolicyDocument: 64 | Version: "2012-10-17" 65 | Statement: 66 | - 67 | Effect: "Allow" 68 | Action: 69 | - "s3:PutObject" 70 | Resource: 71 | - Fn::Sub: "${WorkMailTranslatedMsgBucket.Arn}/*" 72 | 73 | WorkMailPermissionToInvokeLambda: 74 | Type: AWS::Lambda::Permission 75 | DependsOn: WorkMailTranslateEmailFunction 76 | Properties: 77 | Action: lambda:InvokeFunction 78 | FunctionName: !Ref WorkMailTranslateEmailFunction 79 | Principal: !Sub 'workmail.${AWS::Region}.amazonaws.com' 80 | SourceArn: !Sub 'arn:aws:workmail:${AWS::Region}:${AWS::AccountId}:organization/*' 81 | 82 | WorkMailTranslatedMsgBucket: 83 | Type: AWS::S3::Bucket 84 | DeletionPolicy: Retain 85 | Properties: 86 | BucketEncryption: 87 | ServerSideEncryptionConfiguration: 88 | - ServerSideEncryptionByDefault: 89 | SSEAlgorithm: AES256 90 | PublicAccessBlockConfiguration: 91 | BlockPublicAcls : true 92 | BlockPublicPolicy : true 93 | IgnorePublicAcls : true 94 | RestrictPublicBuckets : true 95 | VersioningConfiguration: 96 | Status: Enabled 97 | LifecycleConfiguration: 98 | Rules: 99 | - 100 | Status: Enabled 101 | ExpirationInDays: 1 # Delete after 1 day 102 | - 103 | Status: Enabled 104 | NoncurrentVersionExpirationInDays : 1 # Delete non current versions after 1 day 105 | 106 | WorkMailTranslatedMsgBucketPolicy: 107 | Type: AWS::S3::BucketPolicy 108 | Properties: 109 | Bucket: 110 | Ref: WorkMailTranslatedMsgBucket 111 | PolicyDocument: 112 | Statement: 113 | - Action: 114 | - "s3:GetObject" 115 | - "s3:GetObjectVersion" 116 | Effect: Allow 117 | Resource: 118 | - Fn::Sub: "${WorkMailTranslatedMsgBucket.Arn}/*" 119 | Condition: 120 | Bool: 121 | aws:SecureTransport: true 122 | ArnLike: 123 | aws:SourceArn: !Sub 'arn:aws:workmailmessageflow:${AWS::Region}:${AWS::AccountId}:message/*' 124 | Principal: 125 | Service: !Sub 'workmail.${AWS::Region}.amazonaws.com' 126 | 127 | Outputs: 128 | TranslateEmailArn: 129 | Value: !GetAtt WorkMailTranslateEmailFunction.Arn 130 | -------------------------------------------------------------------------------- /workmail-translate-email/tst/env_vars.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkMailTranslateEmailFunction": { 3 | "TRANSLATED_EMAIL_BUCKET": "TRANSLATED_EMAIL_S3_BUCKET", 4 | "DESTINATION_LANGUAGE": "DESTINATION_LANGUAGE_CODE" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /workmail-translate-email/tst/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "summaryVersion": "2018-10-10", 3 | "envelope": { 4 | "mailFrom" : { 5 | "address" : "from@domain.test" 6 | }, 7 | "recipients" : [ 8 | { "address" : "recipient1@domain.test" }, 9 | { "address" : "recipient2@domain.test" } 10 | ] 11 | }, 12 | "sender" : { 13 | "address" : "sender@domain.test" 14 | }, 15 | "subject" : "Hallo van Amazon WorkMail!", 16 | "messageId": "00000000-0000-0000-0000-000000000000", 17 | "invocationId": "0000000000000000000000000000000000000000", 18 | "flowDirection": "INBOUND", 19 | "truncated": false 20 | } 21 | -------------------------------------------------------------------------------- /workmail-update-email/Image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-workmail-lambda-templates/b570142cba833429f085a27497d1de9ce0326dbc/workmail-update-email/Image.png -------------------------------------------------------------------------------- /workmail-update-email/README.md: -------------------------------------------------------------------------------- 1 | # Amazon WorkMail Update Email 2 | This application enables you to add a customized disclaimer and footer in the body of emails as they are being sent or received. 3 | 4 | Specifically, email messages sent from an external email address to your organization are updated with a disclaimer and footer. The subject of the email can also be prefixed with custom text, such as **"External Email"**. These set of features ensure that users in your organization are aware of emails originating from outside your organization. 5 | 6 | ![Screenshot](Image.png) 7 | 8 | Both a disclaimer and footer are optional and are only added if a value is provided during setup. 9 | 10 | ## Setup 11 | 1. Deploy this application via [AWS Serverless Application Repository](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:489970191081:applications~workmail-update-email). 12 | 1. [Optional] Enter a disclaimer message you'd like to prepend in the email body. 13 | 2. [Optional] Enter a footer message you'd like to append in the email body. 14 | 3. [Optional] Enter a subject tag you'd like to prepend in the email subject, such as 'External'. 15 | 2. Configure a synchronous Run Lambda rule over the Lambda function created in step 1. See [instructions.](https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html#synchronous-rules) 16 | 17 | It is possible to configure both inbound and outbound email flow rules over the same Lambda function. 18 | 19 | You now have a working Lambda function that will be triggered by WorkMail based on the rule you created. 20 | 21 | To further customize your Lambda function, open the [AWS Lambda Console](https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions) to edit and test your Lambda function with the built-in code editor. 22 | 23 | If you would like to customize the way your disclaimer and footer are formatted. You can make a change [here](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-update-email/src/utils.py#L15). 24 | 25 | For more information, see [documentation](https://docs.aws.amazon.com/lambda/latest/dg/code-editor.html). 26 | 27 | For more advanced use cases, such as changing your CloudFormation template to create additional AWS resources that will support this application, follow the instructions below. 28 | 29 | ## Access Control 30 | By default, this serverless application and the resources that it creates can integrate with any [WorkMail Organization](https://docs.aws.amazon.com/workmail/latest/adminguide/organizations_overview.html) in your account, but the application and organization must be in the same region. To restrict that behavior you can either update the SourceArn attribute in [template.yaml](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-update-email/template.yaml) 31 | and then deploy the application by following the steps below **or** update the SourceArn attribute directly in the resource policy of each resource via their AWS Console after the deploying this application, [see example](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html). 32 | 33 | For more information about the SourceArn attribute, [see this documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-sourcearn). 34 | 35 | ## Development 36 | Clone this repository from [GitHub](https://github.com/aws-samples/amazon-workmail-lambda-templates). 37 | 38 | We recommend creating and activating a virtual environment, for more information see [Creation of virtual environments](https://docs.python.org/3/library/venv.html). 39 | 40 | If you are not familiar with CloudFormation templates, see [Learn Template Basics](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/gettingstarted.templatebasics.html). 41 | 42 | 1. Create additional resources for your application by changing [template.yaml](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-update-email/template.yaml). For more information, see [documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-reference.html). 43 | 2. Modify your Lambda function by changing [app.py](https://github.com/aws-samples/amazon-workmail-lambdas-templates/blob/master/workmail-update-email/src/app.py). 44 | 3. Test your Lambda function locally: 45 | 1. [Set up the SAM CLI](https://aws.amazon.com/serverless/sam/). 46 | 2. Configure environment variables at `tst/env_vars.json`. 47 | 3. Configure test event at `tst/event.json`. 48 | 4. Invoke your Lambda function locally using: 49 | 50 | `sam local invoke WorkMailUpdateEmailFunction -e tst/event.json --env-vars tst/env_vars.json` 51 | 52 | ### Test Message Ids 53 | This application uses a `messageId` passed to the Lambda function to retrieve the message content from WorkMail. When testing, the `tst/event.json` file uses a mock messageId which does not exist. If you want to test with a real messageId, you can configure a WorkMail Email Flow Rule with the Lambda action that uses the Lambda function created in **Setup**, and send some emails that will trigger the email flow rule. The Lambda function will emit the messageId it receives from WorkMail in the CloudWatch logs, which you can 54 | then use in your test event data. For more information see [Accessing Amazon CloudWatch logs for AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html). Note that you can access messages in transit for a maximum of one day. 55 | 56 | Once you have validated that your Lambda function behaves as expected, you are ready to deploy this Lambda function. 57 | 58 | ### Deployment 59 | If you develop using the AWS Lambda Console, then this section can be skipped. 60 | 61 | Please create an S3 bucket if you do not have one yet, see [How do I create an S3 Bucket?](https://docs.aws.amazon.com/AmazonS3/latest/user-guide/create-bucket.html). 62 | and check how to create a [Bucket Policy](https://docs.aws.amazon.com/serverlessrepo/latest/devguide/serverlessrepo-how-to-publish.html#publishing-application-through-cli). 63 | We refer to this bucket as ``. 64 | 65 | This step bundles all your code and configuration to the given S3 bucket. 66 | 67 | ```bash 68 | sam build 69 | ``` 70 | 71 | ```bash 72 | sam package \ 73 | --template-file template.yaml \ 74 | --output-template-file packaged.yaml \ 75 | --s3-bucket 76 | ``` 77 | 78 | This step updates your CloudFormation stack to reflect the changes you made, which will in turn update changes made in the Lambda function. 79 | ```bash 80 | sam deploy \ 81 | --template-file packaged.yaml \ 82 | --stack-name workmail-update-email \ 83 | --parameter-overrides Disclaimer=$YOUR_DISCLAIMER Footer=$YOUR_FOOTER SubjectTag=$YOUR_SUBJECT_TAG \ 84 | --capabilities CAPABILITY_IAM 85 | ``` 86 | Your Lambda function is now deployed. You can now configure WorkMail to trigger this function. 87 | -------------------------------------------------------------------------------- /workmail-update-email/dependencies/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.38.5 2 | beautifulsoup4==4.13.4 -------------------------------------------------------------------------------- /workmail-update-email/src/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import utils 3 | from botocore.exceptions import ClientError 4 | 5 | logger = logging.getLogger() 6 | logger.setLevel(logging.INFO) 7 | 8 | def update_handler(event, context): 9 | """ 10 | Update Email Content Using WorkMail Lambda Integration 11 | 12 | Parameters 13 | ---------- 14 | email_summary: dict, required 15 | Amazon WorkMail Message Summary Input Format 16 | For more information, see https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html 17 | 18 | { 19 | "summaryVersion": "2019-07-28", # AWS WorkMail Message Summary Version 20 | "envelope": { 21 | "mailFrom" : { 22 | "address" : "from@domain.test" # String containing from email address 23 | }, 24 | "recipients" : [ # List of all recipient email addresses 25 | { "address" : "recipient1@domain.test" }, 26 | { "address" : "recipient2@domain.test" } 27 | ] 28 | }, 29 | "sender" : { 30 | "address" : "sender@domain.test" # String containing sender email address 31 | }, 32 | "subject" : "Hello From Amazon WorkMail!", # String containing email subject (Truncated to first 256 chars)" 33 | "messageId": "00000000-0000-0000-0000-000000000000", # String containing message id for retrieval using workmail flow API 34 | "invocationId": "00000000000000000000000000000000", # String containing the id of this lambda invocation. Useful for detecting retries and avoiding duplication 35 | "flowDirection": "INBOUND", # String indicating direction of email flow. Value is either "INBOUND" or "OUTBOUND" 36 | "truncated": false # boolean indicating if any field in message was truncated due to size limitations 37 | } 38 | 39 | context: object, required 40 | Lambda Context runtime methods and attributes. See https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html 41 | 42 | Returns 43 | ------- 44 | Amazon WorkMail Sync Lambda Response Format 45 | For more information, see https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html#synchronous-schema 46 | return { 47 | 'actions': [ # Required, should contain at least 1 list element 48 | { 49 | 'action' : { # Required 50 | 'type': 'string', # Required. Can be "BOUNCE", "DROP" or "DEFAULT" 51 | 'parameters': { } # Optional. For bounce, can be {"bounceMessage": "message that goes in bounce mail"} 52 | }, 53 | 'recipients': list of strings, # Optional. Indicates list of recipients for which this action applies 54 | 'default': boolean # Optional. Indicates whether this action applies to all recipients 55 | } 56 | ]} 57 | 58 | """ 59 | 60 | logger.info(f"Received event: {event}") 61 | email_from = event['envelope']['mailFrom'] 62 | recipients = event['envelope']['recipients'] 63 | message_id = event['messageId'] 64 | logger.info(f"Received email with message ID {message_id}") 65 | # Do nothing for emails that are sent or received with in WorkMail organization 66 | if utils.extract_domains([email_from]) != utils.extract_domains(recipients): 67 | try: 68 | # 1. Download email 69 | downloaded_email = utils.download_email(message_id) 70 | # 2. Update email 71 | updated_email = utils.update_email(downloaded_email, event['subject'], event['flowDirection']) 72 | # 3. Send updated email back to WorkMail 73 | utils.update_workmail(message_id, updated_email) 74 | except ClientError as e: 75 | if e.response['Error']['Code'] == 'MessageFrozen': 76 | # Redirect emails are not eligible for update, handle it gracefully. 77 | logger.info(f"Message {message_id} is not eligible for update. This is usually the case for a redirected email") 78 | else: 79 | logger.error(e.response['Error']['Message']) 80 | if e.response['Error']['Code'] == 'ResourceNotFoundException': 81 | logger.error(f"Message {message_id} does not exist. Messages in transit are no longer accessible after 1 day") 82 | elif e.response['Error']['Code'] == 'InvalidContentLocation': 83 | logger.error('WorkMail could not access the updated email content. See https://docs.aws.amazon.com/workmail/latest/adminguide/update-with-lambda.html') 84 | raise(e) 85 | 86 | # Resume normal email flow 87 | return { 88 | 'actions' : [{ 89 | 'action' : { 90 | 'type' : 'DEFAULT' 91 | }, 92 | 'allRecipients': 'true' 93 | }] 94 | } 95 | -------------------------------------------------------------------------------- /workmail-update-email/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: 4 | "WorkMail Update Email" 5 | 6 | Parameters: 7 | Disclaimer: 8 | Type: String 9 | Default: '' 10 | Description: "[Optional] Text that you'd like to prepend to the email body." 11 | Footer: 12 | Type: String 13 | Default: '' 14 | Description: "[Optional] Text that you'd like to append to the email body." 15 | SubjectTag: 16 | Type: String 17 | Default: '' 18 | Description: "[Optional] Text that you'd like to prepend to the email subject." 19 | 20 | Resources: 21 | WorkMailUpdateEmailDependencyLayer: 22 | Type: AWS::Serverless::LayerVersion 23 | Properties: 24 | ContentUri: dependencies/ 25 | CompatibleRuntimes: 26 | - python3.12 27 | Metadata: 28 | BuildMethod: python3.12 29 | 30 | WorkMailUpdateEmailFunction: 31 | Type: AWS::Serverless::Function 32 | DependsOn: WorkMailUpdatedMsgBucket 33 | Properties: 34 | CodeUri: src/ 35 | Handler: app.update_handler 36 | Runtime: python3.12 37 | Timeout: 10 38 | Role: 39 | Fn::GetAtt: WorkMailUpdateEmailFunctionRole.Arn 40 | Layers: 41 | - !Ref WorkMailUpdateEmailDependencyLayer 42 | Environment: 43 | Variables: 44 | DISCLAIMER: 45 | Ref: Disclaimer 46 | FOOTER: 47 | Ref: Footer 48 | UPDATED_EMAIL_BUCKET: 49 | Ref: WorkMailUpdatedMsgBucket 50 | SUBJECT_TAG: 51 | Ref: SubjectTag 52 | 53 | WorkMailUpdateEmailFunctionRole: 54 | Type: AWS::IAM::Role 55 | Properties: 56 | AssumeRolePolicyDocument: 57 | Statement: 58 | - Action: 59 | - sts:AssumeRole 60 | Effect: Allow 61 | Principal: 62 | Service: 63 | - "lambda.amazonaws.com" 64 | Version: "2012-10-17" 65 | Path: "/" 66 | ManagedPolicyArns: 67 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 68 | - "arn:aws:iam::aws:policy/AmazonWorkMailMessageFlowFullAccess" 69 | Policies: 70 | - 71 | PolicyName: "allow-s3-write" 72 | PolicyDocument: 73 | Version: "2012-10-17" 74 | Statement: 75 | - 76 | Effect: "Allow" 77 | Action: 78 | - "s3:PutObject" 79 | Resource: 80 | - Fn::Sub: "${WorkMailUpdatedMsgBucket.Arn}/*" 81 | 82 | WorkMailPermissionToInvokeLambda: 83 | Type: AWS::Lambda::Permission 84 | DependsOn: WorkMailUpdateEmailFunction 85 | Properties: 86 | Action: lambda:InvokeFunction 87 | FunctionName: !Ref WorkMailUpdateEmailFunction 88 | Principal: !Sub 'workmail.${AWS::Region}.amazonaws.com' 89 | SourceArn: !Sub 'arn:aws:workmail:${AWS::Region}:${AWS::AccountId}:organization/*' 90 | 91 | WorkMailUpdatedMsgBucket: 92 | Type: AWS::S3::Bucket 93 | DeletionPolicy: Retain 94 | Properties: 95 | BucketEncryption: 96 | ServerSideEncryptionConfiguration: 97 | - ServerSideEncryptionByDefault: 98 | SSEAlgorithm: AES256 99 | PublicAccessBlockConfiguration: 100 | BlockPublicAcls : true 101 | BlockPublicPolicy : true 102 | IgnorePublicAcls : true 103 | RestrictPublicBuckets : true 104 | VersioningConfiguration: 105 | Status: Enabled 106 | LifecycleConfiguration: 107 | Rules: 108 | - 109 | Status: Enabled 110 | ExpirationInDays: 1 # Delete after 1 day 111 | - 112 | Status: Enabled 113 | NoncurrentVersionExpirationInDays : 1 # Delete non current versions after 1 day 114 | 115 | WorkMailUpdatedMsgBucketPolicy: 116 | Type: AWS::S3::BucketPolicy 117 | Properties: 118 | Bucket: 119 | Ref: WorkMailUpdatedMsgBucket 120 | PolicyDocument: 121 | Statement: 122 | - Action: 123 | - "s3:GetObject" 124 | - "s3:GetObjectVersion" 125 | Effect: Allow 126 | Resource: 127 | - Fn::Sub: "${WorkMailUpdatedMsgBucket.Arn}/*" 128 | Condition: 129 | Bool: 130 | aws:SecureTransport: true 131 | ArnLike: 132 | aws:SourceArn: !Sub 'arn:aws:workmailmessageflow:${AWS::Region}:${AWS::AccountId}:message/*' 133 | Principal: 134 | Service: !Sub 'workmail.${AWS::Region}.amazonaws.com' 135 | 136 | Outputs: 137 | UpdateEmailArn: 138 | Value: !GetAtt WorkMailUpdateEmailFunction.Arn 139 | -------------------------------------------------------------------------------- /workmail-update-email/tst/env_vars.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkMailUpdateEmailFunction": { 3 | "UPDATED_EMAIL_BUCKET": "YOUR_UPDATED_EMAIL_BUCKET", 4 | "DISCLAIMER": "YOUR_CUSTOM_DISCLAIMER_TEXT", 5 | "FOOTER": "YOUR_CUSTOM_FOOTER_TEXT", 6 | "SUBJECT_TAG": "YOUR_CUSTOM_SUBJECT_TAG" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /workmail-update-email/tst/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "summaryVersion": "2018-10-10", 3 | "envelope": { 4 | "mailFrom" : { 5 | "address" : "from@domainA.test" 6 | }, 7 | "recipients" : [ 8 | { "address" : "recipient1@domainB.test" }, 9 | { "address" : "recipient2@domainB.test" } 10 | ] 11 | }, 12 | "sender" : { 13 | "address" : "sender@domain.test" 14 | }, 15 | "subject" : "Hello from Amazon WorkMail!", 16 | "messageId": "00000000-0000-0000-0000-000000000000", 17 | "invocationId": "0000000000000000000000000000000000000000", 18 | "flowDirection": "INBOUND", 19 | "truncated": false 20 | } 21 | -------------------------------------------------------------------------------- /workmail-upstream-gateway-filter/README.md: -------------------------------------------------------------------------------- 1 | # Amazon WorkMail Upstream Gateway Filter 2 | 3 | This application enables you to filter messages to Junk E-Mail for multiple recipients based on the value of an email header added by an upstream email security gateway. 4 | 5 | Define the FILTER_HEADER_NAME and FILTER_HEADER_REGEX environment variables to control which messages are filtered. If the value of the header matches the regular expression then the message will be filtered into the mailbox's Junk E-Mail folder. 6 | 7 | ## Setup 8 | 1. Deploy this application via [AWS Serverless Application Repository](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:489970191081:applications~workmail-upstream-gateway-filter). 9 | 1. Enter the name of the email header that which the upstream email security gateway adds to incoming messages. 10 | 2. Enter the regular expression to match against the value of the header. 11 | 2. Open the [WorkMail Console](https://console.aws.amazon.com/workmail/) and create a synchronous **RunLambda** [Email Flow Rule](https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html#synchronous-rules) that uses this Lambda function. 12 | 13 | To further customize your Lambda function, open the [AWS Lambda Console](https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions) to edit and test your Lambda function with the built-in code editor. 14 | 15 | ### Customizing Your Lambda Function 16 | 17 | For more advanced use cases, such as changing your CloudFormation template to create additional AWS resources that will support this application, follow the instructions below. 18 | 19 | ## Development 20 | Clone this repository from [GitHub](https://github.com/aws-samples/amazon-workmail-lambda-templates). 21 | 22 | We recommend creating and activating a virtual environment, for more information see [Creation of virtual environments](https://docs.python.org/3/library/venv.html). 23 | 24 | If you are not familiar with CloudFormation templates, see [Learn Template Basics](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/gettingstarted.templatebasics.html). 25 | 26 | 1. Create additional resources for your application by changing [template.yaml](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-upstream-gateway-filter/template.yaml). For more information, see this [documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-reference.html). 27 | 2. Modify your Lambda function by changing [app.py](https://github.com/aws-samples/amazon-workmail-lambdas-templates/blob/master/workmail-upstream-gateway-filter/src/app.py). 28 | 3. Test your Lambda function locally: 29 | 1. [Set up the SAM CLI](https://aws.amazon.com/serverless/sam/). 30 | 2. Configure environment variables at `tst/env_vars.json`. 31 | 3. Configure test event at `tst/event.json`. 32 | 4. Invoke your Lambda function locally using: 33 | 34 | `sam local invoke WorkMailUpstreamGatewayFilterFunction -e tst/event.json --env-vars tst/env_vars.json` 35 | 36 | ### Test Message Ids 37 | This application uses a `messageId` passed to the Lambda function to retrieve the message content from WorkMail. When testing, the `tst/event.json` file uses a mock messageId which does not exist. If you want to test with a real messageId, you can configure a WorkMail Email Flow Rule with the Lambda action that uses the Lambda function created in **Setup**, and send some emails that will trigger the email flow rule. The Lambda function will emit the messageId it receives from WorkMail in the CloudWatch logs, which you can 38 | then use in your test event data. For more information see [Accessing Amazon CloudWatch logs for AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html). Note that you can access messages in transit for a maximum of one day. 39 | 40 | Once you have validated that your Lambda function behaves as expected, you are ready to deploy this Lambda function. 41 | 42 | ## Access Control 43 | By default, this serverless application and the resources that it creates can integrate with any [WorkMail Organization](https://docs.aws.amazon.com/workmail/latest/adminguide/organizations_overview.html) in your account, but the application and organization must be in the same region. To restrict that behavior you can either update the SourceArn attribute in [template.yaml](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-upstream-gateway-filter/template.yaml) 44 | and then deploy the application by following the steps below **or** update the SourceArn attribute directly in the resource policy of each resource via their AWS Console after the deploying this application, [see example](https://docs.aws.amazon.com/lambda/latest/dg/access-control-resource-based.html). 45 | 46 | For more information about the SourceArn attribute, [see this documentation](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_condition-keys.html#condition-keys-sourcearn). 47 | 48 | ### Deployment 49 | If you develop using the AWS Lambda Console, then this section can be skipped. 50 | 51 | Please create an S3 bucket if you do not have one yet, see [How do I create an S3 Bucket?](https://docs.aws.amazon.com/AmazonS3/latest/user-guide/create-bucket.html). 52 | and check how to create a [Bucket Policy](https://docs.aws.amazon.com/serverlessrepo/latest/devguide/serverlessrepo-how-to-publish.html#publishing-application-through-cli). 53 | We refer to this bucket as ``. 54 | 55 | This step bundles all your code and configuration to the given S3 bucket. 56 | 57 | ```bash 58 | sam package \ 59 | --template-file template.yaml \ 60 | --output-template-file packaged.yaml \ 61 | --s3-bucket 62 | ``` 63 | 64 | This step updates your Cloud Formation stack to reflect the changes you made, which will in turn update changes made in the Lambda function. 65 | ```bash 66 | sam deploy \ 67 | --template-file packaged.yaml \ 68 | --stack-name workmail-upstream-gateway-filter \ 69 | --parameter-overrides FilterHeaderName=$YOUR_FILTER_HEADER_NAME FilterHeaderRegex=$YOUR_FILTER_HEADER_REGEX\ 70 | --capabilities CAPABILITY_IAM 71 | ``` 72 | Your Lambda function is now deployed. You can now configure WorkMail to trigger this function. 73 | -------------------------------------------------------------------------------- /workmail-upstream-gateway-filter/src/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import boto3 4 | import email 5 | import re 6 | 7 | logger = logging.getLogger() 8 | logger.setLevel(logging.INFO) 9 | 10 | def upstream_gateway_handler(email_summary, context): 11 | """ 12 | Upstream Gateway Filtering for Amazon WorkMail 13 | 14 | Parameters 15 | ---------- 16 | email_summary: dict, required 17 | Amazon WorkMail Message Summary Input Format 18 | For more information, see https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html 19 | 20 | { 21 | "summaryVersion": "2019-07-28", # AWS WorkMail Message Summary Version 22 | "envelope": { 23 | "mailFrom" : { 24 | "address" : "from@domain.test" # String containing from email address 25 | }, 26 | "recipients" : [ # List of all recipient email addresses 27 | { "address" : "recipient1@domain.test" }, 28 | { "address" : "recipient2@domain.test" } 29 | ] 30 | }, 31 | "sender" : { 32 | "address" : "sender@domain.test" # String containing sender email address 33 | }, 34 | "subject" : "Hello From Amazon WorkMail!", # String containing email subject (Truncated to first 256 chars)" 35 | "messageId": "00000000-0000-0000-0000-000000000000", # String containing message id for retrieval using workmail flow API 36 | "invocationId": "00000000000000000000000000000000", # String containing the id of this lambda invocation. Useful for detecting retries and avoiding duplication 37 | "flowDirection": "INBOUND", # String indicating direction of email flow. Value is either "INBOUND" or "OUTBOUND" 38 | "truncated": false # boolean indicating if any field in message was truncated due to size limitations 39 | } 40 | 41 | context: object, required 42 | Lambda Context runtime methods and attributes 43 | 44 | Attributes 45 | ---------- 46 | 47 | context.aws_request_id: str 48 | Lambda request ID 49 | context.client_context: object 50 | Additional context when invoked through AWS Mobile SDK 51 | context.function_name: str 52 | Lambda function name 53 | context.function_version: str 54 | Function version identifier 55 | context.get_remaining_time_in_millis: function 56 | Time in milliseconds before function times out 57 | context.identity: 58 | Cognito identity provider context when invoked through AWS Mobile SDK 59 | context.invoked_function_arn: str 60 | Function ARN 61 | context.log_group_name: str 62 | Cloudwatch Log group name 63 | context.log_stream_name: str 64 | Cloudwatch Log stream name 65 | context.memory_limit_in_mb: int 66 | Function memory 67 | 68 | https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html 69 | 70 | Returns 71 | ------- 72 | Amazon WorkMail Sync Lambda Response Format 73 | For more information, see https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html#synchronous-schema 74 | return { 75 | 'actions': [ # Required, should contain at least 1 list element 76 | { 77 | 'action' : { # Required 78 | 'type': 'string', # Required. Can be "BOUNCE", "DROP", "DEFAULT", BYPASS_SPAM_CHECK, or MOVE_TO_JUNK 79 | 'parameters': { } # Optional. For bounce, can be {"bounceMessage": "message that goes in bounce mail"} 80 | }, 81 | 'recipients': list of strings, # Optional. Indicates list of recipients for which this action applies 82 | 'default': boolean # Optional. Indicates whether this action applies to all recipients 83 | } 84 | ]} 85 | 86 | """ 87 | logger.info(email_summary) 88 | 89 | # get the environment variables containing the header name and regex to match 90 | filter_header_name = os.getenv('FILTER_HEADER_NAME') 91 | if not filter_header_name: 92 | error_msg = 'FILTER_HEADER_NAME not set in environment. Please follow https://docs.aws.amazon.com/lambda/latest/dg/env_variables.html to set it.' 93 | logger.error(error_msg) 94 | raise ValueError(error_msg) 95 | filter_header_regex = os.getenv('FILTER_HEADER_REGEX') 96 | if not filter_header_regex: 97 | error_msg = 'FILTER_HEADER_REGEX not set in environment. Please follow https://docs.aws.amazon.com/lambda/latest/dg/env_variables.html to set it.' 98 | logger.error(error_msg) 99 | raise ValueError(error_msg) 100 | regexp = re.compile(filter_header_regex) 101 | 102 | # get the value of the message header 103 | workmail = boto3.client('workmailmessageflow') 104 | msg_id = email_summary['messageId'] 105 | raw_msg = workmail.get_raw_message_content(messageId=msg_id) 106 | parsed_msg = email.message_from_bytes(raw_msg['messageContent'].read()) 107 | filter_header_value = parsed_msg.get(filter_header_name) 108 | 109 | flow_direction = email_summary['flowDirection'] 110 | 111 | if flow_direction == 'INBOUND' and filter_header_value: 112 | logger.info(filter_header_name + ": " + filter_header_value) 113 | if regexp.search(filter_header_value): 114 | return { 115 | 'actions': [ 116 | { 117 | 'allRecipients': True, # For all recipients 118 | 'action' : { 'type': 'MOVE_TO_JUNK' } 119 | } 120 | ] 121 | } 122 | else: 123 | return { 124 | 'actions': [ 125 | { 126 | 'allRecipients': True, # For all recipients 127 | 'action' : { 'type': 'BYPASS_SPAM_CHECK' } 128 | } 129 | ] 130 | } 131 | 132 | logger.info("Default action. Nothing to do.") 133 | return { 134 | 'actions': [ 135 | { 136 | 'allRecipients': True, # For all recipients 137 | 'action' : { 'type' : 'DEFAULT' } # let the email be sent normally 138 | } 139 | ] 140 | } 141 | -------------------------------------------------------------------------------- /workmail-upstream-gateway-filter/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | WorkMail Upstream Gateway Filter Lambda SAM 5 | 6 | Parameters: 7 | FilterHeaderName: 8 | Type: String 9 | Default: '' 10 | Description: "Name of the header which contains the pattern to match for spam filtering." 11 | 12 | FilterHeaderRegex: 13 | Type: String 14 | Default: '' 15 | Description: "Regular expression to match against the header's value. The message will be filtered to Junk E-Mail if it matches." 16 | 17 | Resources: 18 | WorkMailUpstreamGatewayFilterFunction: 19 | Type: AWS::Serverless::Function 20 | Properties: 21 | CodeUri: src/ 22 | Handler: app.upstream_gateway_handler 23 | Runtime: python3.12 24 | Role: 25 | Fn::GetAtt: WorkMailUpstreamGatewayFilterFunctionRole.Arn 26 | Timeout: 10 27 | Environment: 28 | Variables: 29 | FILTER_HEADER_NAME: 30 | Ref: FilterHeaderName 31 | FILTER_HEADER_REGEX: 32 | Ref: FilterHeaderRegex 33 | 34 | WorkMailUpstreamGatewayFilterFunctionRole: 35 | Type: AWS::IAM::Role 36 | Properties: 37 | AssumeRolePolicyDocument: 38 | Statement: 39 | - Action: 40 | - sts:AssumeRole 41 | Effect: Allow 42 | Principal: 43 | Service: 44 | - "lambda.amazonaws.com" 45 | Version: "2012-10-17" 46 | Path: "/" 47 | ManagedPolicyArns: 48 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 49 | - "arn:aws:iam::aws:policy/AmazonWorkMailMessageFlowReadOnlyAccess" 50 | 51 | PermissionToCallLambdaAbove: 52 | Type: AWS::Lambda::Permission 53 | DependsOn: WorkMailUpstreamGatewayFilterFunction 54 | Properties: 55 | Action: lambda:InvokeFunction 56 | FunctionName: !Ref WorkMailUpstreamGatewayFilterFunction 57 | Principal: !Sub 'workmail.${AWS::Region}.amazonaws.com' 58 | SourceArn: !Sub 'arn:aws:workmail:${AWS::Region}:${AWS::AccountId}:organization/*' 59 | 60 | Outputs: 61 | WorkMailUpstreamGatewayFilterFunctionArn: 62 | Value: !GetAtt WorkMailUpstreamGatewayFilterFunction.Arn 63 | -------------------------------------------------------------------------------- /workmail-upstream-gateway-filter/tst/env_vars.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkMailUpstreamGatewayFilterFunction": { 3 | "FILTER_HEADER_NAME": "MyOrg-Spam-Verdict", 4 | "FILTER_HEADER_REGEX": "yes" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /workmail-upstream-gateway-filter/tst/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "summaryVersion": "2018-10-10", 3 | "envelope": { 4 | "mailFrom" : { 5 | "address" : "from@domain.test" 6 | }, 7 | "recipients" : [ 8 | { "address" : "recipient1@domain.test" }, 9 | { "address" : "recipient2@domain.test" } 10 | ] 11 | }, 12 | "sender" : { 13 | "address" : "sender@domain.test" 14 | }, 15 | "subject" : "Hello from Amazon WorkMail!", 16 | "messageId": "00000000-0000-0000-0000-000000000000", 17 | "invocationId": "0000000000000000000000000000000000000000", 18 | "flowDirection": "INBOUND", 19 | "truncated": false 20 | } 21 | -------------------------------------------------------------------------------- /workmail-wordpress-python/dependencies/requirements.txt: -------------------------------------------------------------------------------- 1 | boto3==1.38.5 2 | requests==2.32.3 -------------------------------------------------------------------------------- /workmail-wordpress-python/src/app.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import urllib.parse 4 | import requests 5 | import utils 6 | 7 | logger = logging.getLogger() 8 | logger.setLevel(logging.INFO) 9 | 10 | API_BASE_ENDPOINT = "https://public-api.wordpress.com/rest/v1.2/sites/" 11 | CREATE_POST_SUFFIX = "/posts/new" 12 | 13 | # A string which triggers posting to the blog. The email subject must start with this string in order 14 | # for the email to converted to a blog submission 15 | TRIGGER = "[Blog Submission]" 16 | 17 | def post_handler(event, context): 18 | """ 19 | Automated Blog Poster for Amazon WorkMail 20 | 21 | Parameters 22 | ---------- 23 | event: dict, required 24 | AWS WorkMail Message Summary Input Format 25 | For more information, see https://docs.aws.amazon.com/workmail/latest/adminguide/lambda.html 26 | 27 | { 28 | "summaryVersion": "2018-10-10", # AWS WorkMail Message Summary Version 29 | "envelope": { 30 | "mailFrom" : { 31 | "address" : "from@domain.test" # String containing from email address 32 | }, 33 | "recipients" : [ # List of all recipient email addresses 34 | { "address" : "recipient1@domain.test" }, 35 | { "address" : "recipient2@domain.test" } 36 | ] 37 | }, 38 | "sender" : { 39 | "address" : "sender@domain.test" # String containing sender email address 40 | }, 41 | "subject" : "Hello From Amazon WorkMail!", # String containing email subject (truncated to first 256 chars) 42 | "truncated": false, # boolean indicating if any field in message was truncated due to size limitations 43 | "messageId": "00000000-0000-0000-0000-000000000000" # String containing the id of the message in the WorkMail system 44 | } 45 | 46 | context: object, required 47 | Lambda Context runtime methods and attributes 48 | 49 | Attributes 50 | ---------- 51 | 52 | context.aws_request_id: str 53 | Lambda request ID 54 | context.client_context: object 55 | Additional context when invoked through AWS Mobile SDK 56 | context.function_name: str 57 | Lambda function name 58 | context.function_version: str 59 | Function version identifier 60 | context.get_remaining_time_in_millis: function 61 | Time in milliseconds before function times out 62 | context.identity: 63 | Cognito identity provider context when invoked through AWS Mobile SDK 64 | context.invoked_function_arn: str 65 | Function ARN 66 | context.log_group_name: str 67 | Cloudwatch Log group name 68 | context.log_stream_name: str 69 | Cloudwatch Log stream name 70 | context.memory_limit_in_mb: int 71 | Function memory 72 | 73 | https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html 74 | 75 | Returns 76 | ------ 77 | Nothing 78 | """ 79 | logger.info("Received event: " + str(event)) 80 | 81 | if not event['subject'].startswith(TRIGGER): 82 | return 83 | 84 | site = os.getenv('BLOG_DOMAIN') 85 | 86 | if not (site): 87 | error_msg = 'BLOG_DOMAIN not set in environment. Please follow https://docs.aws.amazon.com/lambda/latest/dg/env_variables.html to set it.' 88 | logger.error(error_msg) 89 | raise ValueError(error_msg) 90 | 91 | create_post_endpoint = API_BASE_ENDPOINT + site + CREATE_POST_SUFFIX 92 | 93 | secret_id = os.getenv('SECRET_ID') 94 | if not secret_id: 95 | error_msg = 'SECRET_ID not set in environment. Please follow https://docs.aws.amazon.com/lambda/latest/dg/env_variables.html to set it.' 96 | logger.error(error_msg) 97 | raise ValueError(error_msg) 98 | 99 | api_token = utils.get_secret_token(secret_id) 100 | 101 | # Strip off the trigger word to create the post title 102 | post_title = event['subject'][len(TRIGGER):].strip() 103 | post_author = event['envelope']['mailFrom']['address'] 104 | 105 | downloaded_email = utils.download_email(event['messageId']) 106 | email_body = utils.extract_email_body(downloaded_email) 107 | 108 | post_body = f"Author: {post_author}\n\n{email_body}" 109 | 110 | headers = { 111 | 'Content-Type': 'application/x-www-form-urlencoded', 112 | 'Authorization': 'Bearer ' + api_token 113 | } 114 | post_params = { 115 | 'title': post_title, 116 | 'status': 'draft', 117 | 'content': post_body 118 | } 119 | encoded_params = urllib.parse.urlencode(post_params) 120 | 121 | try: 122 | logger.info(f"Creating post from author '{post_author}' with title '{post_title}'") 123 | response = requests.post(create_post_endpoint, headers=headers, data=encoded_params) 124 | response.raise_for_status() 125 | logger.info("Succesfully submitted draft post") 126 | except requests.exceptions.HTTPError as error: 127 | error_msg = f"Error while creating post at endpoint: {create_post_endpoint}" 128 | logger.exception(error_msg) 129 | raise error 130 | 131 | return 132 | -------------------------------------------------------------------------------- /workmail-wordpress-python/src/utils.py: -------------------------------------------------------------------------------- 1 | import email 2 | import boto3 3 | import logging 4 | from botocore.exceptions import ClientError 5 | 6 | logger = logging.getLogger() 7 | 8 | def get_secret_token(secret_id): 9 | """ 10 | This method makes a call to AWS SecretsManager to get the secret API token which has been authorized to post to 11 | your blog. 12 | 13 | Parameters 14 | ---------- 15 | secret_id: string, required 16 | The name of the secret in Secrets Manager 17 | 18 | Returns 19 | -------- 20 | The value of the SecretString 21 | """ 22 | secrets_manager_client = boto3.client('secretsmanager') 23 | api_token = secrets_manager_client.get_secret_value(SecretId=secret_id) 24 | return api_token['SecretString'] 25 | 26 | def extract_email_body(parsed_email): 27 | """ 28 | Extract email message content of type "text/html" from a parsed email 29 | Parameters 30 | ---------- 31 | parsed_email: email.message.Message, required 32 | The parsed email as returned by download_email 33 | Returns 34 | ------- 35 | string 36 | string containing text/html email body decoded with according to the Content-Transfer-Encoding header 37 | and then according to content charset. 38 | None 39 | No content of type "text/html" is found. 40 | """ 41 | text_content = None 42 | text_charset = None 43 | if parsed_email.is_multipart(): 44 | # Walk over message parts of this multipart email. 45 | for part in parsed_email.walk(): 46 | content_type = part.get_content_type() 47 | content_disposition = str(part.get_content_disposition()) 48 | # Look for 'text/html' content but ignore inline attachments. 49 | if content_type == 'text/html' and 'attachment' not in content_disposition: 50 | text_content = part.get_payload(decode=True) 51 | text_charset = part.get_content_charset() 52 | break 53 | else: 54 | text_content = parsed_email.get_payload(decode=True) 55 | text_charset = parsed_email.get_content_charset() 56 | 57 | if text_content and text_charset: 58 | return text_content.decode(text_charset) 59 | return 60 | 61 | def download_email(message_id): 62 | """ 63 | This method downloads full email MIME content from WorkMailMessageFlow and uses email.parser class 64 | for parsing it into Python email.message.EmailMessage class. 65 | Reference: 66 | https://docs.python.org/3.6/library/email.message.html#email.message.EmailMessage 67 | https://docs.python.org/3/library/email.parser.html 68 | Parameters 69 | ---------- 70 | message_id: string, required 71 | message_id of the email to download 72 | Returns 73 | ------- 74 | email.message.Message 75 | EmailMessage representation the downloaded email. 76 | Raises 77 | ------ 78 | botocore.exceptions.ClientError: 79 | When email message cannot be downloaded. 80 | email.errors.MessageParseError 81 | When email message cannot be parsed. 82 | """ 83 | workmail_message_flow = boto3.client('workmailmessageflow') 84 | response = None 85 | try: 86 | response = workmail_message_flow.get_raw_message_content(messageId=message_id) 87 | except ClientError as e: 88 | if e.response['Error']['Code'] == 'ResourceNotFoundException': 89 | logger.error(f"Message {message_id} does not exist. Messages in transit are no longer accessible after 1 day. \ 90 | See: https://docs.aws.amazon.com/workmail/latest/adminguide/lambda-content.html for more details.") 91 | raise(e) 92 | 93 | email_content = response['messageContent'].read() 94 | return email.message_from_bytes(email_content) 95 | -------------------------------------------------------------------------------- /workmail-wordpress-python/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: 4 | "WorkMail Blog Poster Lambda SAM" 5 | 6 | Parameters: 7 | BlogDomain: 8 | Type: String 9 | Description: "The domain of the blog to which the Lambda function will post articles, e.g. 'myblog.home.blog'." 10 | SecretId: 11 | Type: String 12 | Description: "The ID of the SecretsManager secret in which the WordPress API token was saved." 13 | 14 | Resources: 15 | WorkMailBlogPosterDependencyLayer: 16 | Type: AWS::Serverless::LayerVersion 17 | Properties: 18 | ContentUri: dependencies/ 19 | CompatibleRuntimes: 20 | - python3.12 21 | Metadata: 22 | BuildMethod: python3.12 23 | 24 | WorkMailBlogPosterFunction: 25 | Type: AWS::Serverless::Function 26 | Properties: 27 | CodeUri: src/ 28 | Handler: app.post_handler 29 | Runtime: python3.12 30 | Timeout: 10 31 | Role: 32 | Fn::GetAtt: WorkMailBlogPosterFunctionRole.Arn 33 | Layers: 34 | - !Ref WorkMailBlogPosterDependencyLayer 35 | Environment: 36 | Variables: 37 | BLOG_DOMAIN: 38 | Ref: BlogDomain 39 | SECRET_ID: 40 | Ref: SecretId 41 | 42 | WorkMailBlogPosterFunctionRole: 43 | Type: AWS::IAM::Role 44 | Properties: 45 | AssumeRolePolicyDocument: 46 | Statement: 47 | - Action: 48 | - sts:AssumeRole 49 | Effect: Allow 50 | Principal: 51 | Service: 52 | - "lambda.amazonaws.com" 53 | Version: "2012-10-17" 54 | Path: "/" 55 | ManagedPolicyArns: 56 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 57 | - "arn:aws:iam::aws:policy/AmazonWorkMailMessageFlowReadOnlyAccess" 58 | Policies: 59 | - 60 | PolicyName: "allow-secret-manager-get" 61 | PolicyDocument: 62 | Version: "2012-10-17" 63 | Statement: 64 | - 65 | Effect: "Allow" 66 | Action: 67 | - "secretsmanager:GetSecretValue" 68 | Resource: 69 | Fn::Sub: 70 | - "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${SecretName}*" 71 | - { SecretName: !Ref SecretId } 72 | 73 | WorkMailPermissionToInvokeLambda: 74 | Type: AWS::Lambda::Permission 75 | DependsOn: WorkMailBlogPosterFunction 76 | Properties: 77 | Action: lambda:InvokeFunction 78 | FunctionName: !Ref WorkMailBlogPosterFunction 79 | Principal: !Sub 'workmail.${AWS::Region}.amazonaws.com' 80 | SourceArn: !Sub 'arn:aws:workmail:${AWS::Region}:${AWS::AccountId}:organization/*' 81 | 82 | Outputs: 83 | BlogPosterArn: 84 | Value: !GetAtt WorkMailBlogPosterFunction.Arn 85 | -------------------------------------------------------------------------------- /workmail-wordpress-python/tst/env_vars.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkMailBlogPosterFunction": { 3 | "BLOG_DOMAIN" : "BLOG_DOMAIN", 4 | "SECRET_ID" : "SECRET_ID" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /workmail-wordpress-python/tst/event.json: -------------------------------------------------------------------------------- 1 | { 2 | "summaryVersion": "2018-10-10", 3 | "envelope": { 4 | "mailFrom" : { 5 | "address" : "from@domain.test" 6 | }, 7 | "recipients" : [ 8 | { "address" : "recipient1@domain.test" }, 9 | { "address" : "recipient2@domain.test" } 10 | ] 11 | }, 12 | "sender" : { 13 | "address" : "sender@domain.test" 14 | }, 15 | "subject" : "[Blog Submission] My Blog Post For Amazon WorkMail", 16 | "messageId" : "00000000-0000-0000-0000-000000000000", 17 | "truncated": false 18 | } 19 | -------------------------------------------------------------------------------- /workmail-ws1-integration/README.md: -------------------------------------------------------------------------------- 1 | # Amazon WorkMail / VMware Workspace ONE UEM Integration 2 | 3 | This application shows how to integrate WorkMail with VMware Workspace ONE UEM (WS1). 4 | 5 | ![WorkMail WS1 integration diagram](wm-ws1-integration-diagram.png) 6 | 7 | The high level overview: 8 | - WorkMail user configures WS1 app on a mobile device 9 | - device posture is communicated to WS1 10 | - WS1 sends device posture change notification to API Gateway / Lambda 11 | - Lambda loads device data from WS1, containing Exchange Device ID (EasId) and Compliance Status 12 | - if device is Compliant, Mobile Device Access Override for user/device is added to WorkMail organization, 13 | allowing mobile device access 14 | - WorkMail user configures WorkMail email account using Exchange ActiveSync (EAS) on the mobile device 15 | and gets email/calendar data 16 | - if device is not Compliant, Mobile Device Access Overrides for the device are removed from WorkMail organization, 17 | blocking mobile device access 18 | - because WokMail organization has a default Mobile Device Access Rule to DENY access to all mobile devices, 19 | user can't access email/calendar data 20 | 21 | ## Setup 22 | 23 | 1. Put the values in to `ws1creds.json` file: 24 | 25 | 1. `rest_api_url` & `rest_api_key` - can be found in "WS1 > GROUPS & SETTINGS > All Settings > System > Advanced > API > REST API" 26 | 2. `rest_api_username` & `rest_api_password` - required credentials to call WS1 REST API 27 | 3. `event_notification_username` & `event_notification_password` - you can choose any values here; we will use 28 | them later, when configuring Event Notifications in WS1 29 | 30 | 2. Upload `ws1creds.json` to AWS Secrets Manager: 31 | ```shell 32 | aws secretsmanager create-secret --name production/WS1Creds --secret-string file://ws1creds.json 33 | ``` 34 | 35 | 3. Deploy this application via [AWS Serverless Application Repository](https://serverlessrepo.aws.amazon.com/applications/arn:aws:serverlessrepo:us-east-1:489970191081:applications~workmail-ws1-integration): 36 | 37 | 1. enter your WorkMail organization id 38 | 2. keep the default `production/WS1Creds` value 39 | 3. after the deployment is complete, you should get `WorkMailWS1IntegrationEndpoint`; we will use it in the next 40 | step 41 | 42 | 4. Configure Event Notifications in WS1: 43 | 44 | 1. go to "WS1 > GROUPS & SETTINGS > All Settings > System > Advanced > API > Event Notifications" 45 | 2. click "ADD RULE", enter: 46 | 1. "Target Name" - WorkMail integration 47 | 2. "Target URL" - the `WorkMailWS1IntegrationEndpoint` from the previous step 48 | 3. "Username" & "Password" - use `event_notification_username` & `event_notification_password` from the `ws1creds.json` 49 | 4. "Events" - enable all events 50 | 3. click "TEST CONNECTION" - you should get Test is successful 51 | 4. click "SAVE" 52 | 53 | You now have a working Lambda function that will handle WS1 notifications and automatically update WorkMail 54 | organization to ALLOW access for compliant devices and DENY for non-compliant ones. 55 | 56 | If you'd like to customize your Lambda function, open the [AWS Lambda Console](https://console.aws.amazon.com/lambda) to edit and test your Lambda 57 | function with the built-in code editor. For more information, see [Documentation](https://docs.aws.amazon.com/lambda/latest/dg/code-editor.html). 58 | 59 | For more advanced use cases, such as changing your CloudFormation template to create additional AWS resources that will support this application, follow the instructions below. 60 | 61 | ## Development 62 | 63 | Clone this repository from [Github](https://raw.githubusercontent.com/aws-samples/amazon-workmail-lambda-templates/master/workmail-ws1-integration). 64 | 65 | We recommend creating and activating a virtual environment, for more information see [Creation of virtual environments](https://docs.python.org/3/library/venv.html). 66 | 67 | If you are not familiar with CloudFormation templates, see [Learn Template Basics](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/gettingstarted.templatebasics.html). 68 | 69 | 1. Create additional resources for your application by changing [template.yaml](https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-ws1-integration/template.yaml). For 70 | more information, see [documentation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/template-reference.html). 71 | 72 | 2. Modify your Lambda function by changing [app.py](https://github.com/aws-samples/amazon-workmail-lambdas-templates/blob/master/workmail-ws1-integration/src/app.py). 73 | 74 | 3. Test your lambda function locally: 75 | 76 | 1. [Set up the SAM CLI](https://aws.amazon.com/serverless/sam/). 77 | 78 | 2. Put values to `ws1creds.json` and create/update a secret to AWS Secrets Manager: 79 | ```shell 80 | aws secretsmanager create-secret --name production/WS1Creds --secret-string file://ws1creds.json 81 | aws secretsmanager update-secret --secret-id production/WS1Creds --secret-string file://ws1creds.json 82 | ``` 83 | 84 | 3. Fill out files in `tst` folder with your corresponding values 85 | 86 | ### Testing 87 | 88 | Testing Lambda locally: 89 | 90 | - Test connection: 91 | ```shell 92 | sam local invoke WorkMailWS1IntegrationFunction -e tst/lambda_test_conection.json --env-vars tst/env_vars.json 93 | ``` 94 | 95 | - Test event notification: 96 | ```shell 97 | sam local invoke WorkMailWS1IntegrationFunction -e tst/lambda_event_notification.json --env-vars tst/env_vars.json 98 | ``` 99 | 100 | Testing API Gateway locally: 101 | 102 | - Run: 103 | ```shell 104 | sam local start-api --env-vars tst/env_vars.json 105 | ``` 106 | 107 | - Test connection: 108 | ```shell 109 | curl -v http://127.0.0.1:3000/ 110 | ``` 111 | 112 | - Test event notification: 113 | ```shell 114 | curl -XPOST -d @./tst/http_event_notification.json -v http://127.0.0.1:3000/ -H "Authorization: Basic XYZ" 115 | ``` 116 | 117 | ### Deployment 118 | 119 | ```shell 120 | sam deploy --guided 121 | ``` 122 | 123 | Your Lambda function is now deployed. The output will contain `WorkMailWS1IntegrationEndpoint` URL which you can use 124 | when configuring "WS1 > GROUPS & SETTINGS > All Settings > System > Advanced > API > Event Notifications". 125 | 126 | ## Troubleshooting 127 | 128 | ### Where can I find logs? 129 | 130 | The logs can be found in CloudWatch. To get more detailed logging, change the loglevel in 131 | [app.py](https://github.com/aws-samples/amazon-workmail-lambdas-templates/blob/master/workmail-ws1-integration/src/app.py) 132 | to `DEBUG`. 133 | 134 | ### WS1IntegrationException: EasId is missing - ignoring the event 135 | 136 | This error happens when Email Profile is not (yet) deployed to the mobile device. 137 | Make sure that Email Profiles are configured and are deployed to mobile devices. Check WS1 documentation for more 138 | details. 139 | 140 | -------------------------------------------------------------------------------- /workmail-ws1-integration/src/app.py: -------------------------------------------------------------------------------- 1 | import json 2 | import params 3 | import ws1 4 | import workmail 5 | import logging 6 | import utils 7 | 8 | from exceptions import WS1IntegrationException 9 | 10 | # setup logging 11 | logging.basicConfig() 12 | # change to DEBUG to log WS1 event and WS1 device data 13 | logging.getLogger().setLevel(logging.INFO) 14 | 15 | 16 | def check_authorization(event: dict) -> None: 17 | """ 18 | Check Authorization header. 19 | 20 | Username/password should be matching "event_notification_username"/"event_notification_password" values 21 | stored in "production/WS1Creds" secret, and configured in: 22 | WS1 > GROUPS & SETTINGS > All Settings > System > Advanced > API > Event Notifications 23 | 24 | :param event: dict, containing information received from WS1 25 | :return: None 26 | """ 27 | event_headers = event["headers"] 28 | if "Authorization" not in event_headers: 29 | raise WS1IntegrationException("Authorization header is missing") 30 | if event_headers["Authorization"] != params.event_notification_auth: 31 | raise WS1IntegrationException("Username/password don't match") 32 | 33 | 34 | def load_ws1_device_data(event: dict) -> dict: 35 | """ 36 | Load all device data from WS1. 37 | 38 | To access the data, the following values, are used: "rest_api_url", "rest_api_key", "rest_api_username", 39 | "rest_api_password". They are stored in "production/WS1Creds" secret, and configured in: 40 | WS1 > GROUPS & SETTINGS > All Settings > System > Advanced > API > REST API 41 | 42 | :param event: dict, containing information received from WS1 43 | :return: device data loaded from WS1 44 | """ 45 | body = utils.get_required_value(event, "body", False) 46 | event_body = json.loads(body) 47 | 48 | ws1_device_id = utils.get_required_value(event_body, "DeviceId") 49 | return ws1.load_device_data(ws1_device_id) 50 | 51 | 52 | def update_workmail_device_access(ws1_device_data: dict) -> None: 53 | """ 54 | Update device access configuration in WorkMail. 55 | 56 | - For Compliant devices, create Mobile Device Access Override in WorkMail organization with ORGANIZATION_ID. 57 | - For non-Compliant devices, remove all Mobile Device Access Overrides. 58 | 59 | See: https://docs.aws.amazon.com/workmail/latest/adminguide/mobile-overrides.html 60 | 61 | :param ws1_device_data: device data loaded from WS1 62 | :return: None 63 | """ 64 | eas_device_id = utils.get_required_value(ws1_device_data, "EasId") 65 | compliance_status = utils.get_required_value(ws1_device_data, "ComplianceStatus") 66 | 67 | if compliance_status == "Compliant": 68 | email_address = utils.get_required_value(ws1_device_data, "UserEmailAddress") 69 | description = ws1.get_device_data_url(str(ws1_device_data["Id"]["Value"])) 70 | 71 | logging.info(f"Device {eas_device_id} is Compliant, adding access override for the device/user") 72 | workmail.put_access_override( 73 | eas_device_id, 74 | email_address, 75 | description 76 | ) 77 | else: 78 | logging.info(f"Device {eas_device_id} is Not Compliant, removing all access overrides for the device") 79 | workmail.delete_access_overrides_for_device( 80 | eas_device_id 81 | ) 82 | 83 | 84 | def lambda_handler(event: dict, context) -> dict: 85 | """ 86 | WS1 event handler. 87 | 88 | See: https://docs.aws.amazon.com/workmail/latest/adminguide/mdm-integration.html 89 | 90 | :param event: dict, containing information received from WS1. Examples: 91 | tst/lambda_test_connection.json 92 | tst/lambda_event_notification.json 93 | 94 | :param context: lambda Context runtime methods and attributes. See: 95 | https://docs.aws.amazon.com/lambda/latest/dg/python-context-object.html 96 | 97 | :return: dict, containing HTTP response status code. Example: 98 | {"statusCode": 200} 99 | """ 100 | try: 101 | logging.debug(event) 102 | 103 | if event["httpMethod"] == "GET": 104 | logging.info("WS1 Test Connection") 105 | 106 | elif event["httpMethod"] == "POST": 107 | logging.info("WS1 Event Notification") 108 | check_authorization(event) 109 | ws1_device_data = load_ws1_device_data(event) 110 | update_workmail_device_access(ws1_device_data) 111 | 112 | else: 113 | raise WS1IntegrationException("Unknown request") 114 | 115 | except WS1IntegrationException as e: 116 | logging.warning(f"WS1IntegrationException: {e} - ignoring the event") 117 | logging.debug(e, exc_info=True) # change log level to DEBUG to see stack traces 118 | return {"statusCode": e.status_code} 119 | 120 | except Exception as e: 121 | logging.exception(e) 122 | return {"statusCode": 500} 123 | 124 | return {"statusCode": 200} 125 | -------------------------------------------------------------------------------- /workmail-ws1-integration/src/exceptions.py: -------------------------------------------------------------------------------- 1 | class WS1IntegrationException(Exception): 2 | """ 3 | WorkMail/WS1 integration exception. 4 | 5 | It is raised when WS1 event can't be handled. 6 | """ 7 | def __init__(self, message, status_code=400): 8 | super().__init__(message) 9 | self.status_code = status_code 10 | -------------------------------------------------------------------------------- /workmail-ws1-integration/src/params.py: -------------------------------------------------------------------------------- 1 | import os 2 | import secretsmanager 3 | import utils 4 | 5 | # WorkMail organization id 6 | organization_id = os.getenv("ORGANIZATION_ID") 7 | 8 | # WS1 secrets id (default value is "production/WS1Creds"); 9 | # it is a json document with a format "ws1creds.json", 10 | # and is stored in AWS Secrets Manager 11 | ws1creds_id = os.getenv("WS1CREDS_ID") 12 | 13 | # WS1 secrets loaded from AWS Secrets Manager 14 | ws1creds = secretsmanager.get_secret(ws1creds_id) 15 | 16 | # WS1 rest API url and key; 17 | # they can be found in: 18 | # WS1 > GROUPS & SETTINGS > All Settings > System > Advanced > API > REST API 19 | rest_api_url = ws1creds["rest_api_url"] 20 | rest_api_key = ws1creds["rest_api_key"] 21 | 22 | # WS1 user name and password, to call WS1 rest API 23 | rest_api_username = ws1creds["rest_api_username"] 24 | rest_api_password = ws1creds["rest_api_password"] 25 | 26 | # WS1 event notification username/password; 27 | # they can be configured in: 28 | # WS1 > GROUPS & SETTINGS > All Settings > System > Advanced > API > Event Notifications 29 | event_notification_username = ws1creds["event_notification_username"] 30 | event_notification_password = ws1creds["event_notification_password"] 31 | 32 | # authorization headers, 33 | # generated from provided usernames and passwords 34 | rest_api_auth = utils.create_authorization_header_value( 35 | rest_api_username, rest_api_password 36 | ) 37 | event_notification_auth = utils.create_authorization_header_value( 38 | event_notification_username, event_notification_password 39 | ) 40 | -------------------------------------------------------------------------------- /workmail-ws1-integration/src/secretsmanager.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import json 3 | 4 | sm = boto3.client("secretsmanager") 5 | 6 | 7 | def get_secret(secret_id: str) -> dict: 8 | """ 9 | Load secrets from AWS Secrets Manager. 10 | 11 | See: https://docs.aws.amazon.com/secretsmanager/latest/apireference/API_GetSecretValue.html 12 | 13 | :param secret_id: secret id 14 | :return: secret content 15 | """ 16 | secret_value = sm.get_secret_value(SecretId=secret_id) 17 | secret_string = secret_value["SecretString"] 18 | return json.loads(secret_string) 19 | -------------------------------------------------------------------------------- /workmail-ws1-integration/src/utils.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from base64 import b64encode 4 | from six import b 5 | from exceptions import WS1IntegrationException 6 | 7 | 8 | def create_authorization_header_value(username: str, password: str) -> str: 9 | """ 10 | Create an authorization header for provided username and password. 11 | 12 | :param username: username 13 | :param password: password 14 | :return: authorization header 15 | """ 16 | return "Basic " + b64encode(b(f"{username}:{password}")).decode("utf-8") 17 | 18 | 19 | def get_required_value(data: dict, key: str, log_key_value: bool = True) -> str: 20 | """ 21 | Get required value from dictionary. If value is missing, raise an exception. 22 | 23 | :param data: dict 24 | :param key: key 25 | :param log_key_value: if True, then the key/value will be logged 26 | :return: value 27 | """ 28 | value = data.get(key) 29 | 30 | if log_key_value: 31 | logging.info(f"{key}: {value}") 32 | 33 | if not value: 34 | raise WS1IntegrationException(f"{key} is missing") 35 | 36 | return str(value) 37 | -------------------------------------------------------------------------------- /workmail-ws1-integration/src/workmail.py: -------------------------------------------------------------------------------- 1 | import boto3 2 | import params 3 | import logging 4 | 5 | wm = boto3.client("workmail") 6 | 7 | 8 | def put_access_override(eas_device_id: str, email_address: str, description: str) -> None: 9 | """ 10 | Create a mobile device access override which allows access for provided device/email combination. 11 | 12 | :param eas_device_id: exchange device id 13 | :param email_address: WorkMail user email address 14 | :param description: mobile device access override description 15 | :return: None 16 | """ 17 | logging.info(f"Creating access override for user: {email_address}, with description: {description}") 18 | wm.put_mobile_device_access_override( 19 | OrganizationId=params.organization_id, 20 | UserId=email_address, 21 | DeviceId=eas_device_id, 22 | Effect="ALLOW", 23 | Description=description, 24 | ) 25 | 26 | 27 | def delete_access_overrides(overrides: list) -> None: 28 | """ 29 | Delete mobile device access overrides from the overrides list. 30 | 31 | :param overrides: list of mobile device access overrides 32 | :return: None 33 | """ 34 | for override in overrides: 35 | user_id = override["UserId"] 36 | device_id = override["DeviceId"] 37 | description = override.get("Description", "") 38 | 39 | logging.info(f"Deleting access override for user: {user_id}, with description: {description}") 40 | wm.delete_mobile_device_access_override( 41 | OrganizationId=params.organization_id, 42 | UserId=user_id, 43 | DeviceId=device_id, 44 | ) 45 | 46 | 47 | def delete_access_overrides_for_device(eas_device_id: str) -> None: 48 | """ 49 | Delete mobile device access overrides for the given device. 50 | 51 | :param eas_device_id: exchange device id 52 | :return: None 53 | """ 54 | result = wm.list_mobile_device_access_overrides( 55 | OrganizationId=params.organization_id, 56 | DeviceId=eas_device_id, 57 | ) 58 | delete_access_overrides(result["Overrides"]) 59 | 60 | while "NextToken" in result: 61 | result = wm.list_mobile_device_access_overrides( 62 | OrganizationId=params.organization_id, 63 | DeviceId=eas_device_id, 64 | NextToken=result["NextToken"], 65 | ) 66 | delete_access_overrides(result["Overrides"]) 67 | -------------------------------------------------------------------------------- /workmail-ws1-integration/src/ws1.py: -------------------------------------------------------------------------------- 1 | import urllib3 2 | import params 3 | import json 4 | import logging 5 | 6 | from exceptions import WS1IntegrationException 7 | 8 | http = urllib3.PoolManager() 9 | 10 | 11 | def get_device_data_url(ws1_device_id: str) -> str: 12 | """ 13 | Get WS1 REST API URL, pointing to device data. 14 | 15 | :param ws1_device_id: WS1 device id 16 | :return: URL 17 | """ 18 | return f"{params.rest_api_url}/mdm/devices/{ws1_device_id}" 19 | 20 | 21 | def load_device_data(ws1_device_id: str) -> dict: 22 | """ 23 | Load WS1 device data. 24 | 25 | :param ws1_device_id: WS1 device id 26 | :return: loaded WS1 device data 27 | """ 28 | ws1_device_data_url = get_device_data_url(ws1_device_id) 29 | logging.info(f"Loading {ws1_device_data_url}") 30 | 31 | response = http.request( 32 | "GET", 33 | ws1_device_data_url, 34 | headers={ 35 | "Content-Type": "application/json", 36 | "aw-tenant-code": params.rest_api_key, 37 | "Authorization": params.rest_api_auth, 38 | }, 39 | ) 40 | 41 | ws1_device_data = json.loads(response.data) 42 | logging.debug(ws1_device_data) 43 | 44 | if response.status == 200: 45 | return ws1_device_data 46 | else: 47 | raise WS1IntegrationException(ws1_device_data["message"]) 48 | -------------------------------------------------------------------------------- /workmail-ws1-integration/template.yaml: -------------------------------------------------------------------------------- 1 | AWSTemplateFormatVersion: '2010-09-09' 2 | Transform: AWS::Serverless-2016-10-31 3 | Description: > 4 | WorkMail WS1 Integration 5 | 6 | Parameters: 7 | WorkMailOrganizationID: 8 | Type: String 9 | AllowedPattern: "^m-[0-9a-f]{32}$" 10 | Description: "You can find your organization id using workmail/list-organizations AWS CLI" 11 | WS1CredsID: 12 | Type: String 13 | Default: "production/WS1Creds" 14 | Description: "WS1 secrets id (see https://github.com/aws-samples/amazon-workmail-lambda-templates/blob/master/workmail-ws1-integration/README.md for more details)" 15 | 16 | Resources: 17 | WorkMailWS1IntegrationFunction: 18 | Type: AWS::Serverless::Function 19 | Properties: 20 | CodeUri: src/ 21 | Handler: app.lambda_handler 22 | Runtime: python3.12 23 | Timeout: 10 24 | Role: !GetAtt WorkMailWS1IntegrationFunctionRole.Arn 25 | Environment: 26 | Variables: 27 | ORGANIZATION_ID: 28 | Ref: WorkMailOrganizationID 29 | WS1CREDS_ID: 30 | Ref: WS1CredsID 31 | Events: 32 | Api: 33 | Type: Api 34 | Properties: 35 | Path: / 36 | Method: ANY 37 | 38 | WorkMailWS1IntegrationFunctionRole: 39 | Type: AWS::IAM::Role 40 | Properties: 41 | AssumeRolePolicyDocument: 42 | Statement: 43 | - Action: 44 | - sts:AssumeRole 45 | Effect: Allow 46 | Principal: 47 | Service: 48 | - "lambda.amazonaws.com" 49 | Version: "2012-10-17" 50 | Path: "/" 51 | Policies: 52 | - PolicyName: "AllowSecretsManagerGetValue" 53 | PolicyDocument: 54 | Version: "2012-10-17" 55 | Statement: 56 | - Effect: Allow 57 | Action: 58 | - "secretsmanager:GetSecretValue" 59 | Resource: 60 | Fn::Sub: 61 | - "arn:aws:secretsmanager:${AWS::Region}:${AWS::AccountId}:secret:${SecretName}*" 62 | - { SecretName: !Ref WS1CredsID } 63 | - PolicyName: "AllowWorkMailDeviceAccessOverrides" 64 | PolicyDocument: 65 | Version: "2012-10-17" 66 | Statement: 67 | - Effect: Allow 68 | Action: 69 | - "workmail:PutMobileDeviceAccessOverride" 70 | - "workmail:ListMobileDeviceAccessOverrides" 71 | - "workmail:DeleteMobileDeviceAccessOverride" 72 | Resource: 73 | Fn::Sub: 74 | - "arn:aws:workmail:${AWS::Region}:${AWS::AccountId}:organization/${OrganizationId}*" 75 | - { OrganizationId: !Ref WorkMailOrganizationID } 76 | ManagedPolicyArns: 77 | - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" 78 | 79 | Outputs: 80 | WorkMailWS1IntegrationEndpoint: 81 | Value: 82 | Fn::Sub: 83 | - "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/" 84 | - { ServerlessRestApi: !Ref ServerlessRestApi } 85 | -------------------------------------------------------------------------------- /workmail-ws1-integration/tst/env_vars.json: -------------------------------------------------------------------------------- 1 | { 2 | "WorkMailWS1IntegrationFunction": { 3 | "ORGANIZATION_ID": "YOUR_ORGANIZATION_ID", 4 | "WS1CREDS_ID": "production/WS1Creds" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /workmail-ws1-integration/tst/http_event_notification.json: -------------------------------------------------------------------------------- 1 | { 2 | "DeviceId":WS1_TEST_DEVICE_ID 3 | } 4 | -------------------------------------------------------------------------------- /workmail-ws1-integration/tst/lambda_event_notification.json: -------------------------------------------------------------------------------- 1 | { 2 | "httpMethod": "POST", 3 | "headers": { 4 | "Authorization": "WS1_AUTHORIZATION_HEADER" 5 | }, 6 | "body" : "{\"DeviceId\":WS1_TEST_DEVICE_ID}" 7 | } 8 | -------------------------------------------------------------------------------- /workmail-ws1-integration/tst/lambda_test_connection.json: -------------------------------------------------------------------------------- 1 | { 2 | "httpMethod": "GET" 3 | } 4 | -------------------------------------------------------------------------------- /workmail-ws1-integration/wm-ws1-integration-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aws-samples/amazon-workmail-lambda-templates/b570142cba833429f085a27497d1de9ce0326dbc/workmail-ws1-integration/wm-ws1-integration-diagram.png -------------------------------------------------------------------------------- /workmail-ws1-integration/ws1creds.json: -------------------------------------------------------------------------------- 1 | { 2 | "rest_api_url": "", 3 | "rest_api_key": "", 4 | 5 | "rest_api_username": "", 6 | "rest_api_password": "", 7 | 8 | "event_notification_username": "", 9 | "event_notification_password": "" 10 | } 11 | --------------------------------------------------------------------------------