EWSO365
The new EWS O365 integration uses OAuth 2.0 protocol and can be used with Exchange Online and Office 365 (mail).
Email · Microsoft Exchange Online
Details
| ID | EWSO365 |
|---|---|
| Provider | Microsoft |
| Category | |
| From Version | 5.0.0 |
| Docker Image | demisto/py3ews:5.6.0.10133006 |
| Supported Modules | Agentix Cloud Runtime Security Cloud Posture Security XSIAM EDR Cortex Cloud |
README
Exchange Web Services (EWS) provides the functionality to enable client applications to communicate with the Exchange server. EWS provides access to much of the same data that is made available through Microsoft Office Outlook.
The EWS O365 integration implants EWS leading services. The integration allows getting information on emails and activities in a target mailbox, and some active operations on the mailbox such as deleting emails and attachments or moving emails from folder to folder.
Microsoft EWS Retirement and EwsAllowedAppIds
Microsoft has announced the final phase of Exchange Web Services (EWS) retirement in Exchange Online, with phased disablement beginning in October 2026.
As part of this transition, Microsoft is moving away from unrestricted EWS access and has introduced a new tenant-level allow list called EwsAllowedAppIds. Starting in October 2026, Microsoft will block all EWS traffic by default. Applications will only be able to access EWS if their specific Application (Client) ID is explicitly added to this allow list by the tenant administrator.
We highly recommend using the Microsoft Graph Mail integration for mailbox operations such as reading, searching, and managing emails and attachments, as Microsoft itself recommends transitioning from EWS to Graph-based APIs.
Action Required
For customers using a Self-Deployed Application
You are responsible for updating your tenant configuration before the October 2026 enforcement takes effect. To ensure this integration continues to function without disruption, you must add the Entra Application (Client) ID used by this integration to your Exchange Online tenant’s allow list.
-
Connect to Exchange Online PowerShell and add the Application ID.
If this is the only application you are allowing, run the following command (replace
<Your-App-ID>with your actual Application Client ID):Set-OrganizationConfig -EwsAllowedAppIDs "<Your-App-ID>"Warning: This command replaces the entire allow list. If you already have other approved EWS applications, first read the current list and append the new ID so that you don’t overwrite existing entries.
-
Ensure EWS is enabled for your tenant.
Set-OrganizationConfig -EwsEnabled $true(For full details, complete timelines, and PowerShell scripts for safely appending to an existing allow list, refer to the official Microsoft documentation: Introducing EWSAllowedAppIDs: Preparing for the Final Phase of EWS Retirement)
For customers using the Cortex Application authentication method
Palo Alto Networks is making the necessary updates to align our shared Application ID with Microsoft’s new requirements, so you do not need to run the steps above. However, because this change is enforced and controlled entirely by Microsoft, we cannot validate the changes in advance or guarantee uninterrupted functionality. Palo Alto Networks is not responsible for any disruptions resulting from this rollout. We recommend verifying that your integration continues to function as expected once the October 2026 enforcement takes effect.
Retirement of RBAC Application Impersonation
As of February 2025, the Impersonation access type of the integration is deprecated by Microsoft, read about it here.
To avoid disruptions, it is imperative that administrators begin transitioning their applications immediately.
To identify accounts using the ApplicationImpersonation role use the Exchange Online PowerShell command:
Get-ManagementRoleAssignment -Role ApplicationImpersonation -GetEffectiveUsers -Delegating:$false
Use Cases
The EWS integration can be used for the following use cases.
-
Monitor a specific email account and create incidents from incoming emails to the defined folder.
Follow the instructions in the Fetched Incidents Data section. -
Search for an email message across mailboxes and folders.
Use the
ews-search-mailboxcommand to search for all emails in a specific folder within the target mailbox.
Use the query argument to narrow the search for emails sent from a specific account and more.
This command retrieves the ItemID field for each email item listed in the results. TheItemIDvalue can be used in theews-get-itemscommand in order to get more information about the email item itself. -
Get email attachment information.
Use theews-get-attachmentcommand to retrieve information on one attachment or all attachments of a message at once. It supports both file attachments and item attachments (e.g., email messages). -
Delete email items from a mailbox.
First, make sure you obtain the email item ID. The item ID can be obtained with one of the integration’s search commands.
Use theews-delete-itemscommand to delete one or more items from the target mailbox in a single action.
A less common use case is to remove emails that were marked as malicious from a user’s mailbox.
You can delete the items permanently (hard delete) or delete the items (soft delete), so they can be recovered by running theews-recover-messagescommand.
Architecture
This integration is based on the exchangelib python module. For more information about the module, check the documentation.
Set up the Third Party System
There are two application authentication methods available.
Follow your preferred method’s guide on how to use the admin consent flow in order to receive your authentication information:
-
Cortex XSOAR Application
To allow access to EWS O365, an administrator has to approve the Demisto app using an admin consent flow, by clicking on the following link.
After authorizing the Demisto app, you will get an ID, Token, and Key, which needs to be added to the integration instance configuration’s corresponding fields. -
Self-Deployed Application - Client Credential Flow.
Authentication
For more details about the authentication used in this integration, see Microsoft Integrations - Authentication.
Permissions
In order to function as expected, the service account should have:
Impersonation rights (deprecated) - In order to perform actions on the target mailbox of other users, the service account must be part of the ApplicationImpersonation role. For more information and instructions on how to set up the permission, see Microsoft Documentation.
Most commands require this permission to function correctly. This permission is specified in each relevant command’s Permission section. For more information, see Microsoft Documentation.
eDiscovery permissions to the Exchange Server. For users to be able to use Exchange Server In-Place eDiscovery, they must be added to the Discovery Management role group. Members of the Discovery Management role group have Full Access mailbox permissions to the default discovery mailbox, which is called Discovery Search Mailbox, including access to sensitive message content. For more information, see the Microsoft documentation.
The need for this permission is specified in each relevant command’s Permission section.
full_access_as_app - The application used for authentication requires this permission to gain access to the Exchange Web Services.
To set this permission follow these steps:
- Navigate to Home > App registrations.
- Search for your app under all applications.
- Click API permissions > Add permission.
- Search for
Office 365 Exchange OnlineAPI >Application Permission>full_access_as_apppermission.
For more information on this permission, see the Microsoft documentation.
To limit the application’s permissions to only specific mailboxes, follow the Microsoft documentation. Note that it may take about an hour for permissions changes to take effect.
Configure Integration on Cortex
| Parameter | Description | Required |
|---|---|---|
| ID / Application ID | ID can be received after following the System Integration Setup (Device side steps). | False |
| Token / Tenant ID | Token can be received after following the System Integration Setup (Device side steps). | False |
| Key / Application Secret | Key can be received after following the System Integration Setup (Device side steps). | False |
| Azure Cloud | Azure Cloud environment. Options are: Worldwide (The publicly accessible Azure Cloud), US GCC (Azure cloud for the USA Government Cloud Community), US GCC-High (Azure cloud for the USA Government Cloud Community High), DoD (Azure cloud for the USA Department of Defense), Germany (Azure cloud for the German Government), China (Azure cloud for the Chinese Government ) | False |
| Email Address | Mailbox to run commands on and to fetch incidents from. To use this functionality, your account must have delegation for the account specified. For more information, see https://xsoar.pan.dev/docs/reference/integrations/ewso365/#additional-information | True |
| UPN Address | When provided, the target mailbox if it’s different from the Email Address. Otherwise, the Email Address is used. | False |
| Name of the folder from which to fetch incidents | Supports Exchange Folder ID and sub-folders, e.g., Inbox/Phishing. | True |
| Access Type | Run the commands using Delegate or Impersonation access types. |
False |
| Public Folder | Whether the folder to be fetched from is public. Public folders can store and organize emails on specific topics or projects. Public folders are usually listed under the “Public Folders” section in the navigation pane in the product itself. | False |
| Fetch incidents | False | |
| Incident type | False | |
| First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days) | False | |
| Maximum number of incidents per fetch (up to 200). Performance might be affected by a value higher than 50. | False | |
| Mark fetched emails as read | False | |
| Timeout (in seconds) for HTTP requests to Exchange Server | False | |
| Trust any certificate (not secure) | False | |
| Use system proxy settings | False | |
| Run as a separate process (protects against memory depletion) | False | |
| Use a self-deployed Azure Application | Select this checkbox if you are using a self-deployed Azure application. | False |
| Incidents Fetch Interval | False | |
| Skip unparsable emails during fetch incidents | Whether to skip unparsable emails during incident fetching. | False |
| What time field should we filter incidents by? | Default is to filter by received-time, which works well if the folder is an “Inbox”. But for a folder emails are dragged into for attention, if we filter by received-time, out-of-order processing of emails means some are ignored. Filtering by modified-time works better for such a scenario. This works best if any modifications (such as tagging) happens before moving the email into the folder, such that the move into the folder is the last modification, and triggers Cortex XSOAR to fetch it as an incident. | False |
Fetch Incidents
The integration imports email messages from the destination folder in the target mailbox as incidents. If the message contains any attachments, they are uploaded to the War Room as files. If the attachment is an email, Cortex XSOAR fetches information about the attached email and downloads all of its attachments (if there are any) as files.
To use Fetch incidents, configure a new instance and select the Fetches incidents option in the instance settings.
IMPORTANT:
First fetch timestamp field is used to determine how much time back to fetch incidents from. The default value is the previous 10 minutes, Meaning, if this is the first time emails are fetched from the destination folder, all emails from 10 minutes prior to the instance configuration and up to the current time will be fetched.
When set to get a long period of time, the Timeout field might need to be set to a higher value.
Pay special attention to the following fields in the instance settings:
Email Address– mailbox to fetch incidents from.Name of the folder from which to fetch incidents– use this field to configure the destination folder from where emails should be fetched. The default is Inbox folder.
Permissions
Impersonation rights required. In order to perform actions on the target mailbox of other users, the service account must be part of the ApplicationImpersonation role.
Limitations
If Exchange is configured with an international flavor, Inbox will be named according to the configured language.
Commands
<h3 style={{display: 'inline'}}>ews-get-attachment</h3> ### ews-get-attachment Retrieves the actual attachments from an email message. To get all attachments for a message, only specify the item-id argument. #### Permissions Impersonation rights required. In order to perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |item-id |The ID of the email message for which to get the attachments.|Required| |target-mailbox |The mailbox in which this attachment was found. If empty, the default mailbox is used. Otherwise, the user might require impersonation rights to this mailbox.|Optional| |attachment-ids |The attachments IDs to get. If none, all attachments will be retrieved from the message. Support multiple attachments with comma-separated values or an array. |Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.FileAttachments.attachmentId|string|The attachment ID. Used for file attachments only.| |EWS.Items.FileAttachments.attachmentName|string|The attachment name. Used for file attachments only.| |EWS.Items.FileAttachments.attachmentSHA256|string|The SHA256 hash of the attached file.| |EWS.Items.FileAttachments.attachmentLastModifiedTime|date|The attachment last modified time. Used for file attachments only.| |EWS.Items.ItemAttachments.datetimeCreated|date|The created time of the attached email.| |EWS.Items.ItemAttachments.datetimeReceived|date|The received time of the attached email.| |EWS.Items.ItemAttachments.datetimeSent|date|The sent time of the attached email.| |EWS.Items.ItemAttachments.receivedBy|string|The received by address of the attached email.| |EWS.Items.ItemAttachments.subject|string|The subject of the attached email.| |EWS.Items.ItemAttachments.textBody|string|The body of the attached email (as text).| |EWS.Items.ItemAttachments.headers|Unknown|The headers of the attached email.| |EWS.Items.ItemAttachments.hasAttachments|boolean|Whether the attached email has attachments.| |EWS.Items.ItemAttachments.itemId|string|The attached email item ID.| |EWS.Items.ItemAttachments.toRecipients|Unknown|A list of recipient email addresses for the attached email.| |EWS.Items.ItemAttachments.body|string|The body of the attached email (as HTML).| |EWS.Items.ItemAttachments.attachmentSHA256|string|SHA256 hash of the attached email (as EML file).| |EWS.Items.ItemAttachments.FileAttachments.attachmentSHA256|string|SHA256 hash of the attached files inside of the attached email.| |EWS.Items.ItemAttachments.ItemAttachments.attachmentSHA256|string|SHA256 hash of the attached emails inside of the attached email.| |EWS.Items.ItemAttachments.isRead|String|The read status of the attachment.| #### Examples ``` !ews-get-attachment item-id=BBFDShfdafFSDF3FADR3434DFASDFADAFDADFADFCJebinpkUAAAfxuiVAAA= target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Context Example ``` { "EWS": { "Items": { "ItemAttachments": { "originalItemId": "BBFDShfdafFSDF3FADR3434DFASDFADAFDADFADFCJebinpkUAAAfxuiVAAA=", "attachmentSize": 2956, "receivedBy": "test@demistodev.onmicrosoft.com", "size": 28852, "author": "test2@demistodev.onmicrosoft.com", "attachmentLastModifiedTime": "2019-08-11T15:01:30+00:00", "subject": "Moving Email between mailboxes", "body": "Some text inside", "datetimeCreated": "2019-08-11T15:01:47Z", "importance": "Normal", "attachmentType": "ItemAttachment", "toRecipients": [ "test@demistodev.onmicrosoft.com" ], "mailbox": "test@demistodev.onmicrosoft.com", "isRead": false, "attachmentIsInline": false, "datetimeSent": "2019-08-07T12:50:19Z", "lastModifiedTime": "2019-08-11T15:01:30Z", "sender": "test2@demistodev.onmicrosoft.com", "attachmentName": "Moving Email between mailboxes", "datetimeReceived": "2019-08-07T12:50:20Z", "attachmentSHA256": "119e27b28dc81bdfd4f498d44bd7a6d553a74ee03bdc83e6255a53", "hasAttachments": false, "headers": [ { "name": "Subject", "value": "Moving Email between mailboxes" } ... ], "attachmentId": "BBFDShfdafFSDF3FADR3434DFASDFADAFDADFADFCJebinpkUAAAfxuiVAAABEgAQAOpEfpzDB4dFkZ+/K4XSj44=", "messageId": "message_id" } } } ``` <h3 style={{display: 'inline'}}>ews-delete-attachment</h3> ### ews-delete-attachment Deletes the attachments of an item (email message). #### Permissions Impersonation rights required. In order to perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |item-id|The ID of the email message for which to delete attachments.|Required| |target-mailbox|The mailbox in which this attachment was found. If empty, the default mailbox is used. Otherwise, the user might require impersonation rights to this mailbox.|Optional| |attachment-ids|A comma-separated list (or array) of attachment IDs to delete. If empty, all attachments will be deleted from the message.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.FileAttachments.attachmentId|string|The ID of the deleted attachment, in case of file attachment.| |EWS.Items.ItemAttachments.attachmentId|string|The ID of the deleted attachment, in case of other attachment (for example, "email").| |EWS.Items.FileAttachments.action|string|The deletion action in case of file attachment. This is a constant value: 'deleted'.| |EWS.Items.ItemAttachments.action|string|The deletion action in case of other attachment (for example, "email"). This is a constant value: 'deleted'.| #### Examples ``` !ews-delete-attachment item-id=AAMkADQ0NmwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUQZfBJjfaljfAFDVSDinpkUAAAfxxd9AAA= target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output >|action|attachmentId| >|--- |--- | >|deleted|AAMkADQ0NmwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUQZfBJjfaljfAFDVSDinpkUAAAfxxd9AAABEgAQAIUht2vrOdErec33=| ##### Context Example ``` { "EWS": { "Items": { "FileAttachments": { "action": "deleted", "attachmentId": "AAMkADQ0NmwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUQZfBJjfaljfAFDVSDinpkUAAAfxxd9AAABEgAQAIUht2vrOdErec33=" } } } } ``` <h3 style={{display: 'inline'}}>ews-get-searchable-mailboxes</h3> ### ews-get-searchable-mailboxes Get a list of searchable mailboxes. #### Permissions Requires eDiscovery permissions to the Exchange Server. For more information see the [Microsoft documentation](https://technet.microsoft.com/en-us/library/dd298059(v=exchg.160).aspx). #### Limitations No known limitations. #### Inputs There are no input arguments for this command. #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Mailboxes.mailbox|string|Addresses of the searchable mailboxes.| |EWS.Mailboxes.mailboxId|string|IDs of the searchable mailboxes.| |EWS.Mailboxes.displayName|string|The email display name.| |EWS.Mailboxes.isExternal|boolean|Whether the mailbox is external.| |EWS.Mailboxes.externalEmailAddress|string|The external email address.| #### Examples ``` !ews-get-searchable-mailboxes ``` ##### Human Readable Output >|displayName|isExternal|mailbox|mailboxId| >|--- |--- |--- |--- | >|test|false|test@demistodev.onmicrosoft.com|/o=Exchange\*\*\*/ou=Exchange Administrative Group ()/cn=\*\*/cn=\*\\_-_\*| ##### Context Example ``` { "EWS": { "Mailboxes": [ { "mailbox": "test@demistodev.onmicrosoft.com", "displayName": "test", "mailboxId": "/o=Exchange***/ou=Exchange Administrative Group ()/cn=**/cn=**-**", "isExternal": "false" } ... ] } } ``` <h3 style={{display: 'inline'}}>ews-move-item</h3> ### ews-move-item Move an item to a different folder in the mailbox. #### Permissions Impersonation rights required. In order to perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |item-id|The ID of the item to move.|Required| |target-folder-path|The path to the folder to which to move the item. Complex paths are supported, for example, "Inbox\Phishing".|Required| |target-mailbox|The mailbox on which to run the command.|Optional| |is-public|Whether the target folder is a public folder.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.newItemID|string|The item ID after the move.| |EWS.Items.messageID|string|The item message ID.| |EWS.Items.itemId|string|The original item ID.| |EWS.Items.action|string|The action taken. The value will be "moved".| #### Examples ``` !ews-move-item item-id=VDAFNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU34cSCSSSfBJebinpkUAAAAAAEMAACyyVyFtlsUQZfBJebinpkUAAAfxuiRAAA= target-folder-path=Moving target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output >|action|itemId|messageId|newItemId| >|--- |--- |--- |--- | >|moved|VDAFNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU34cSCSSSfBJebinpkUAAAAAAEMAACyyVyFtlsUQZfBJebinpkUAAAfxuiRAAA||AAVAAAVN2NkLThmZjdmNTZjNTMxFFFFJTJPMPXU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAAa2bUBAACyyVfafainpkUAAAfxxd+AAA=| ##### Context Example ``` { "EWS": { "Items": { "action": "moved", "itemId": "VDAFNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU34cSCSSSfBJebinpkUAAAAAAEMAACyyVyFtlsUQZfBJebinpkUAAAfxuiRAAA", "newItemId": "AAVAAAVN2NkLThmZjdmNTZjNTMxFFFFJTJPMPXU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAAa2bUBAACyyVfafainpkUAAAfxxd+AAA=", "messageId": "" } } } ``` </details> <h3 style={{display: 'inline'}}>ews-delete-items</h3> ### ews-delete-items Delete an item from a mailbox #### Permissions Impersonation rights required. In order to perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |item-ids|A comma-separated list (or array) of IDs to delete.|Required| |delete-type|Deletion type. Can be "trash", "soft", or "hard".|Required| |target-mailbox|The mailbox on which to run the command.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.itemId|string|The deleted item ID.| |EWS.Items.messageId|string|The deleted message ID.| |EWS.Items.action|string|The deletion action. Can be 'trash-deleted', 'soft-deleted', or 'hard-deleted'.| #### Examples ``` !ews-delete-items item-ids=VWAFA3hmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAABjKMGAACyw+kAAA= delete-type=soft target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output >|action|itemId|messageId| >|--- |--- |--- | >|soft-deleted|VWAFA3hmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAABjKMGAACyw+kAAA=|| ##### Context Example ``` { "EWS": { "Items": { "action": "soft-deleted", "itemId": "VWAFA3hmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAABjKMGAACyw+kAAA=", "messageId": "messaage_id" } } } ``` <h3 style={{display: 'inline'}}>ews-search-mailbox</h3> ### ews-search-mailbox Searches for items in the specified mailbox. Specific permissions are needed for this operation to search in a target mailbox other than the default. #### Permissions Impersonation rights required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |query|The search query string. For more information about the query syntax, see the [Microsoft documentation](https://msdn.microsoft.com/en-us/library/ee693615.aspx).|Optional| |folder-path|The folder path in which to search. If empty, searches all the folders in the mailbox.|Optional| |limit|Maximum number of results to return.|Optional| |target-mailbox|The mailbox on which to apply the search.|Optional| |is-public|Whether the folder is a public folder?|Optional| |message-id|The message ID of the email. This will be ignored if a query argument is provided.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.itemId|string|The email item ID.| |EWS.Items.hasAttachments|boolean|Whether the email has attachments.| |EWS.Items.datetimeReceived|date|Received time of the email.| |EWS.Items.datetimeSent|date|Sent time of the email.| |EWS.Items.headers|Unknown|Email headers (list).| |EWS.Items.sender|string|Sender email address of the email.| |EWS.Items.subject|string|Subject of the email.| |EWS.Items.textBody|string|Body of the email (as text).| |EWS.Items.size|number|Email size.| |EWS.Items.toRecipients|Unknown|List of email recipients addresses.| |EWS.Items.receivedBy|Unknown|Email received by address.| |EWS.Items.messageId|string|Email message ID.| |EWS.Items.body|string|Body of the email (as HTML).| |EWS.Items.FileAttachments.attachmentId|unknown|Attachment ID of the file attachment.| |EWS.Items.ItemAttachments.attachmentId|unknown|Attachment ID of the item attachment.| |EWS.Items.FileAttachments.attachmentName|unknown|Attachment name of the file attachment.| |EWS.Items.ItemAttachments.attachmentName|unknown|Attachment name of the item attachment.| |EWS.Items.isRead|String|The read status of the email.| #### Examples ``` !ews-search-mailbox query="subject:"Get Attachment Email" target-mailbox=test@demistodev.onmicrosoft.com limit=1 ``` ##### Human Readable Output >|sender|subject|hasAttachments|datetimeReceived|receivedBy|author|toRecipients| >|--- |--- |--- |--- |--- |--- |--- | >|test2@demistodev.onmicrosoft.com|Get Attachment Email|true|2019-08-11T10:57:37Z|test@demistodev.onmicrosoft.com|test2@demistodev.onmicrosoft.com|test@demistodev.onmicrosoft.com| ##### Context Example ``` { "EWS": { "Items": { "body": "\r\n\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n<style type=\"text/css\" style=\"display:none;\"></style>\r\n\r\n<body dir=\"ltr\">\r\n<div id=\"divtagrapper\" style=\"font-size:12pt;color:#000000;font-family:Calibri,Helvetica,sans-serif;\" dir=\"ltr\">\r\n<p style=\"margin-top:0;margin-bottom:0\">Some text inside email</p>\r\n</div>\r\n</body>\r\n\r\n", "itemId": "AAMkADQ0NmFFijer3FFmNTZjNTMxNwBGAAAAAAFSAAfxw+jAAA=", "toRecipients": [ "test@demistodev.onmicrosoft.com" ], "datetimeCreated": "2019-08-11T10:57:37Z", "datetimeReceived": "2019-08-11T10:57:37Z", "author": "test2@demistodev.onmicrosoft.com", "hasAttachments": true, "size": 30455, "subject": "Get Attachment Email", "FileAttachments": [ { "attachmentName": "atta1.rtf", "attachmentSHA256": "csfd81097bc049fbcff6e637ade0407a00308bfdfa339e31a44a1c4e98f28ce36e4f", "attachmentType": "FileAttachment", "attachmentSize": 555, "attachmentId": "AAMkADQ0NmFkODFkLWQ4MDEtNDE4Mi1hN2NkLThmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAABjKMGAACyyVyFtlsUQZfBJebinpkUAAAfxw+jAAABEgAQAEyq1TB2nKBLpKUiFUJ5Geg=", "attachmentIsInline": false, "attachmentLastModifiedTime": "2019-08-11T11:06:02+00:00", "attachmentContentLocation": null, "attachmentContentType": "text/rtf", "originalItemId": "AAMkADQ0NmFFijer3FFmNTZjNTMxNwBGAAAAAAFSAAfxw+jAAA=", "attachmentContentId": null } ], "headers": [ { "name": "Subject", "value": "Get Attachment Email" }, ... ], "isRead": true, "messageId": "", "receivedBy": "test@demistodev.onmicrosoft.com", "datetimeSent": "2019-08-11T10:57:36Z", "lastModifiedTime": "2019-08-11T11:13:59Z", "mailbox": "test@demistodev.onmicrosoft.com", "importance": "Normal", "textBody": "Some text inside email\r\n", "sender": "test2@demistodev.onmicrosoft.com" } } } ``` </details> <h3 style={{display: 'inline'}}>ews-get-contacts</h3> ### ews-get-contacts Retrieves contacts for a specified mailbox. #### Permissions Impersonation rights required. In order to perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |target-mailbox|The mailbox for which to retrieve the contacts.|Optional| |limit|Maximum number of results to return.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |Account.Email.EwsContacts.displayName|Unknown|The contact name.| |Account.Email.EwsContacts.lastModifiedTime|Unknown|The time that the contact was last modified.| |Account.Email.EwsContacts.emailAddresses|Unknown|Phone numbers of the contact.| |Account.Email.EwsContacts.physicalAddresses|Unknown|Physical addresses of the contact.| |Account.Email.EwsContacts.phoneNumbers.phoneNumber|Unknown|Email addresses of the contact.| #### Examples ``` !ews-get-contacts limit="1" ``` ##### Human Readable Output >|changekey|culture|datetimeCreated|datetimeReceived|datetimeSent|displayName|emailAddresses|fileAs|fileAsMapping|givenName|id|importance|itemClass|lastModifiedName|lastModifiedTime|postalAddressIndex|sensitivity|subject|uniqueBody|webClientReadFormQueryString| >|--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- | >|EABYACAADcsxRwRjq/zTrN6vWSzKAK1Dl3N|en-US|2019-08-05T12:35:36Z|2019-08-05T12:35:36Z|2019-08-05T12:35:36Z|Contact Name|some@dev.microsoft.com|Contact Name|LastCommaFirst|Contact Name|AHSNNK3NQNcasnc3SAS/zTrN6vWSzK4OWAAAAAAEOAADrxRwRjq/zTrNFSsfsfVWAAK1KsF3AAA=|Normal|IPM.Contact|John Smith|2019-08-05T12:35:36Z|None|Normal|Contact Name||<https://outlook.office365.com/owa/?ItemID>=***| ##### Context Example ``` { "Account.Email": [ { "itemClass": "IPM.Contact", "lastModifiedName": "John Smith", "displayName": "Contact Name", "datetimeCreated": "2019-08-05T12:35:36Z", "datetimeReceived": "2019-08-05T12:35:36Z", "fileAsMapping": "LastCommaFirst", "importance": "Normal", "sensitivity": "Normal", "postalAddressIndex": "None", "webClientReadFormQueryString": "https://outlook.office365.com/owa/?ItemID=***", "uniqueBody": "", "fileAs": "Contact Name", "culture": "en-US", "changekey": "EABYACAADcsxRwRjq/zTrN6vWSzKAK1Dl3N", "lastModifiedTime": "2019-08-05T12:35:36Z", "datetimeSent": "2019-08-05T12:35:36Z", "emailAddresses": [ "some@dev.microsoft.com" ], "givenName": "Contact Name", "id": "AHSNNK3NQNcasnc3SAS/zTrN6vWSzK4OWAAAAAAEOAADrxRwRjq/zTrNFSsfsfVWAAK1KsF3AAA=", "subject": "Contact Name" } ] } ``` <h3 style={{display: 'inline'}}>ews-get-out-of-office</h3> ### ews-get-out-of-office Retrieves the out-of-office status for a specified mailbox. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |target-mailbox|The mailbox for which to get the out-of-office status.|Required| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |Account.Email.OutOfOffice.state|Unknown|Out-of-office state. The result can be: "Enabled", "Scheduled", or "Disabled".| |Account.Email.OutOfOffice.externalAudience|Unknown|Out-of-office external audience. Can be "None", "Known", or "All".| |Account.Email.OutOfOffice.start|Unknown|Out-of-office start date.| |Account.Email.OutOfOffice.end|Unknown|Out-of-office end date.| |Account.Email.OutOfOffice.internalReply|Unknown|Out-of-office internal reply.| |Account.Email.OutOfOffice.externalReply|Unknown|Out-of-office external reply.| |Account.Email.OutOfOffice.mailbox|Unknown|Out-of-office mailbox.| #### Examples ``` !ews-get-out-of-office target-mailbox=test@demistodev.onmicrosoft.com ``` ###### Human Readable Output >|end|externalAudience|mailbox|start|state| >|--- |--- |--- |--- |--- | >|2019-08-12T13:00:00Z|All|test@demistodev.onmicrosoft.com|2019-08-11T13:00:00Z|Disabled| ###### Context Example ``` { "Account": { "Email": { "OutOfOffice": { "start": "2019-08-11T13:00:00Z", "state": "Disabled", "mailbox": "test@demistodev.onmicrosoft.com", "end": "2019-08-12T13:00:00Z", "externalAudience": "All" } } } } ``` <h3 style={{display: 'inline'}}>ews-recover-messages</h3> ### ews-recover-messages Recovers messages that were soft-deleted. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |message-ids|A CSV list of message IDs. Run the py-ews-delete-items command to retrieve the message IDs|Required| |target-folder-path|The folder path to recover the messages to.|Required| |target-mailbox|The mailbox in which the messages found. If empty, will use the default mailbox. If you specify a different mailbox, you might need impersonation rights to the mailbox.|Optional| |is-public|Whether the target folder is a public folder.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.itemId|Unknown|The item ID of the recovered item.| |EWS.Items.messageId|Unknown|The message ID of the recovered item.| |EWS.Items.action|Unknown|The action taken on the item. The value will be 'recovered'.| #### Examples ``` !ews-recover-messages message-ids= target-folder-path=Moving target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output >|action|itemId|messageId| >|--- |--- |--- | >|recovered|AAVCSVS1hN2NkLThmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed33wX3aBwCyyVyFtlsUQZfBJebinpkUAAAa2bUBAACyyVyFtlscfxxd/AAA=|| ##### Context Example ``` { "EWS": { "Items": { "action": "recovered", "itemId": "AAVCSVS1hN2NkLThmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed33wX3aBwCyyVyFtlsUQZfBJebinpkUAAAa2bUBAACyyVyFtlscfxxd/AAA=", "messageId": "" } } } ``` </details> <h3 style={{display: 'inline'}}>ews-create-folder</h3> ### ews-create-folder Creates a new folder in a specified mailbox. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |new-folder-name|The name of the new folder.|Required| |folder-path|Path to locate the new folder. Exchange folder ID is also supported.|Required| |target-mailbox|The mailbox in which to create the folder.|Optional| #### Outputs There is no context output for this command. #### Examples ``` !ews-create-folder folder-path=Inbox new-folder-name="Created Folder" target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output > > Folder Inbox\Created Folder created successfully <h3 style={{display: 'inline'}}>ews-mark-item-as-junk</h3> ### ews-mark-item-as-junk Marks an item as junk. This is used to block an email address (meaning all future emails from this sender will be sent to the junk folder). For more information, see the [Microsoft documentation](https://msdn.microsoft.com/en-us/library/office/dn481311(v=exchg.150).aspx). #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |item-id|The item ID to mark as junk.|Required| |move-items|Whether to move the item from the original folder to the junk folder.|Optional| |target-mailbox|If empty, will use the default mailbox. If you specify a different mailbox, you might need impersonation rights to the mailbox.|Optional| #### Outputs There is no context output for this command. #### Examples ``` !ews-mark-item-as-junk item-id=AAMkcSQ0NmFkOhmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUcsBJebinpkUAAAAAAEMASFDkUAAAfxuiSAAA= move-items=yes target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output |action|itemId| |--- |--- | |marked-as-junk|AAMkcSQ0NmFkOhmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUcsBJebinpkUAAAAAAEMASFDkUAAAfxuiSAAA=| ##### Context Example ``` { "EWS": { "Items": { "action": "marked-as-junk", "itemId": "AAMkcSQ0NmFkOhmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUcsBJebinpkUAAAAAAEMASFDkUAAAfxuiSAAA=" } } } ``` <h3 style={{display: 'inline'}}>ews-find-folders</h3> ### ews-find-folders Retrieves information for the folders of the specified mailbox. Only folders with read permissions will be returned. Your visual folders on the mailbox, such as "Inbox", are under the folder "Top of Information Store". #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |target-mailbox|The mailbox on which to apply the command.|Optional| |is-public|Whether to find public folders.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Folders.name|string|Folder name.| |EWS.Folders.id|string|Folder ID.| |EWS.Folders.totalCount|Unknown|Number of items in the folder.| |EWS.Folders.unreadCount|number|Number of unread items in the folder.| |EWS.Folders.changeKey|number|Folder change key.| |EWS.Folders.childrenFolderCount|number|Number of sub-folders.| #### Examples ``` !ews-find-folders target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output ``` root ├── AllContacts ├── AllItems ├── Common Views ├── Deferred Action ├── ExchangeSyncData ├── Favorites ├── Freebusy Data ├── Location ├── MailboxAssociations ├── My Contacts ├── MyContactsExtended ├── People I Know ├── PeopleConnect ├── Recoverable Items │ ├── Calendar Logging │ ├── Deletions │ ── Purges │ └── Versions ├── Reminders ├── Schedule ├── Sharing ├── Shortcuts ├── Spooler Queue ├── System ├── To-Do Search ├── Top of Information Store │ ├── Calendar │ ├── Contacts │ │ ├── GAL Contacts │ │ ├── Recipient Cache │ ├── Conversation Action Settings │ ├── Deleted Items │ │ └── Create1 │ ├── Drafts │ ├── Inbox ... ``` ##### Context Example ``` { "EWS": { "Folders": [ { "unreadCount": 1, "name": "Inbox", "childrenFolderCount": 1, "totalCount": 44, "changeKey": "**********fefsduQi0", "id": "*******VyFtlFDSAFDSFDAAA=" } ... ] } } ``` <h3 style={{display: 'inline'}}>ews-get-items-from-folder</h3> ### ews-get-items-from-folder Retrieves items from a specified folder in a mailbox. The items are ordered by the item created time. Most recent is first. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |folder-path|The folder path from which to get the items.|Required| |limit|Maximum number of items to return.|Optional| |target-mailbox|The mailbox on which to apply the command.|Optional| |is-public|Whether the folder is a public folder. Default is 'False'.|Optional| |get-internal-items|If the email item contains another email as an attachment (EML or MSG file), whether to retrieve the EML/MSG file attachment. Can be "yes" or "no". Default is "no".|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.itemId|string|The item ID of the email.| |EWS.Items.hasAttachments|boolean|Whether the email has attachments.| |EWS.Items.datetimeReceived|date|Received time of the email.| |EWS.Items.datetimeSent|date|Sent time of the email.| |EWS.Items.headers|Unknown|Email headers (list).| |EWS.Items.sender|string|Sender mail address of the email.| |EWS.Items.subject|string|Subject of the email.| |EWS.Items.textBody|string|Body of the email (as text).| |EWS.Items.size|number|Email size.| |EWS.Items.toRecipients|Unknown|Email recipients addresses (list).| |EWS.Items.receivedBy|Unknown|Received by address of the email.| |EWS.Items.messageId|string|Email message ID.| |EWS.Items.body|string|Body of the email (as HTML).| |EWS.Items.FileAttachments.attachmentId|unknown|Attachment ID of file attachment.| |EWS.Items.ItemAttachments.attachmentId|unknown|Attachment ID of the item attachment.| |EWS.Items.FileAttachments.attachmentName|unknown|Attachment name of the file attachment.| |EWS.Items.ItemAttachments.attachmentName|unknown|Attachment name of the item attachment.| |EWS.Items.isRead|String|The read status of the email.| |EWS.Items.categories|String|Categories of the email.| #### Examples ``` !ews-get-items-from-folder folder-path=Test target-mailbox=test@demistodev.onmicrosoft.com limit=1 ``` ##### Human Readable Output >|sender|subject|hasAttachments|datetimeReceived|receivedBy|author|toRecipients|itemId| >|--- |--- |--- |--- |--- |--- |--- |--- | >|test2@demistodev.onmicrosoft.com|Get Attachment Email|true|2019-08-11T10:57:37Z|test@demistodev.onmicrosoft.com|test2@demistodev.onmicrosoft.com|test@demistodev.onmicrosoft.com|AAFSFSFFtlsUQZfBJebinpkUAAABjKMGAACyyVyFtlsUQZfBJebinpkUAAAsfw+jAAA=| ##### Context Example ``` { "EWS": { "Items": { "body": "\r\n\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n<style type=\"text/css\" style=\"display:none;\"></style>\r\n\r\n<body dir=\"ltr\">\r\n<div id=\"divtagdefaultwrapper\" style=\"font-size:12pt;color:#000000;font-family:Calibri,Helvetica,sans-serif;\" dir=\"ltr\">\r\n<p style=\"margin-top:0;margin-bottom:0\">Some text inside email</p>\r\n</div>\r\n</body>\r\n\r\n", "itemId": "AAFSFSFFtlsUQZfBJebinpkUAAABjKMGAACyyVyFtlsUQZfBJebinpkUAAAsfw+jAAA=", "toRecipients": [ "test@demistodev.onmicrosoft.com" ], "datetimeCreated": "2019-08-11T10:57:37Z", "datetimeReceived": "2019-08-11T10:57:37Z", "author": "test2@demistodev.onmicrosoft.com", "hasAttachments": true, "size": 21435, "subject": "Get Attachment Email", "FileAttachments": [ { "attachmentName": "atta1.rtf", "attachmentSHA256": "cd81097bcvdiojf3407a00308b48039e31a44a1c4fdnfkdknce36e4f", "attachmentType": "FileAttachment", "attachmentSize": 535, "attachmentId": "AAFSFSFFtlsUQZfBJebinpkUAAABjKMGAACyyVyFtlsUQZfBJebinpkUAAAsfw+jAAABEgAQAEyq1TB2nKBLpKUiFUJ5Geg=", "attachmentIsInline": false, "attachmentLastModifiedTime": "2019-08-11T11:06:02+00:00", "attachmentContentLocation": null, "attachmentContentType": "text/rtf", "originalItemId": "AAFSFSFFtlsUQZfBJebinpkUAAABjKMGAACyyVyFtlsUQZfBJebinpkUAAAsfw+jAAA=", "attachmentContentId": null } ], "headers": [ { "name": "Subject", "value": "Get Attachment Email" }, ... ], "isRead": true, "messageId": "", "receivedBy": "test@demistodev.onmicrosoft.com", "datetimeSent": "2019-08-11T10:57:36Z", "lastModifiedTime": "2019-08-11T11:13:59Z", "mailbox": "test@demistodev.onmicrosoft.com", "importance": "Normal", "textBody": "Some text inside email\r\n", "sender": "test2@demistodev.onmicrosoft.com" } } } ``` </details> <h3 style={{display: 'inline'}}>ews-get-items</h3> ### ews-get-items Retrieves items by item ID. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |item-ids|A CSV list of item IDs.|Required| |target-mailbox|The mailbox on which to run the command on.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.itemId|string|The email item ID.| |EWS.Items.hasAttachments|boolean|Whether the email has attachments.| |EWS.Items.datetimeReceived|date|Received time of the email.| |EWS.Items.datetimeSent|date|Sent time of the email.| |EWS.Items.headers|Unknown|Email headers (list).| |EWS.Items.sender|string|Sender mail address of the email.| |EWS.Items.subject|string|Subject of the email.| |EWS.Items.textBody|string|Body of the email (as text).| |EWS.Items.size|number|Email size.| |EWS.Items.toRecipients|Unknown|Email recipients addresses (list).| |EWS.Items.receivedBy|Unknown|Received by address of the email.| |EWS.Items.messageId|string|Email message ID.| |EWS.Items.body|string|Body of the email (as HTML).| |EWS.Items.FileAttachments.attachmentId|unknown|Attachment ID of the file attachment.| |EWS.Items.ItemAttachments.attachmentId|unknown|Attachment ID of the item attachment.| |EWS.Items.FileAttachments.attachmentName|unknown|Attachment name of the file attachment.| |EWS.Items.ItemAttachments.attachmentName|unknown|Attachment name of the item attachment.| |EWS.Items.isRead|String|The read status of the email.| |EWS.Items.categories|String|Categories of the email.| |Email.CC|String|Email addresses CC'ed to the email.| |Email.BCC|String|Email addresses BCC'ed to the email.| |Email.To|String|The recipient of the email.| |Email.From|String|The sender of the email.| |Email.Subject|String|The subject of the email.| |Email.Text|String|The plain-text version of the email.| |Email.HTML|String|The HTML version of the email.| |Email.HeadersMap|String|The headers of the email.| #### Examples ``` !ews-get-items item-ids=AAMkADQ0NmFkODFkLWQ4MDEtNDFDFZjNTMxNwBGAAAAAAA4kxhFFAfxw+jAAA= target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output ``` Identical outputs to `ews-get-items-from-folder` command. ``` <h3 style={{display: 'inline'}}>ews-move-item-between-mailboxes</h3> ### ews-move-item-between-mailboxes Moves an item from one mailbox to a different mailbox. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |item-id|The item ID to move.|Required| |destination-folder-path|The folder in the destination mailbox to which to move the item. You can specify a complex path, for example, "Inbox\Phishing".|Required| |destination-mailbox|The mailbox to which to move the item.|Required| |source-mailbox|The mailbox from which to move the item (conventionally called the "target-mailbox", the target mailbox on which to run the command).|Optional| |is-public|Whether the destination folder is a public folder. Default is "False".|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.movedToMailbox|string|The mailbox to which the item was moved.| |EWS.Items.movedToFolder|string|The folder to which the item was moved.| |EWS.Items.action|string|The action taken on the item. The value will be "moved".| #### Examples ``` !ews-move-item-between-mailboxes item-id=AAMkAGY3OTQyMzMzLWYxNjktNDE0My05NFSFSyNzBkNABGAAAAAACYCKjWAjq/zTrN6vWSzK4OWAAK2ISFSA= destination-folder-path=Moving destination-mailbox=test@demistodev.onmicrosoft.com source-mailbox=test2@demistodev.onmicrosoft.com ``` ##### Human Readable Output >Item was moved successfully. ##### Context Example ``` { "EWS": { "Items": { "movedToMailbox": "test@demistodev.onmicrosoft.com", "movedToFolder": "Moving" } } } ``` <h3 style={{display: 'inline'}}>ews-get-folder</h3> ### ews-get-folder Retrieves a single folder. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations If Exchange is configured with an international flavor, `Inbox` will be named according to the configured language. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |target-mailbox|The mailbox on which to apply the search.|Optional| |folder-path|The path of the folder to retrieve. If empty, will retrieve the folder "AllItems".|Optional| |is-public|Whether the folder is a public folder. Default is "False".|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Folders.id|string|Folder ID.| |EWS.Folders.name|string|Folder name.| |EWS.Folders.changeKey|string|Folder change key.| |EWS.Folders.totalCount|number|Total number of emails in the folder.| |EWS.Folders.childrenFolderCount|number|Number of sub-folders.| |EWS.Folders.unreadCount|number|Number of unread emails in the folder.| #### Examples ``` !ews-get-folder folder-path=demistoEmail target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output >|changeKey|childrenFolderCount|id|name|totalCount|unreadCount| >|--- |--- |--- |--- |--- |--- | >|***yFtCdJSH|0|AAMkADQ0NmFkODFkLWQ4MDEtNDE4Mi1hN2NlsjflsjfSF=|demistoEmail|1|0| ##### Context Example ``` { "EWS": { "Folders": { "unreadCount": 0, "name": "demistoEmail", "childrenFolderCount": 0, "totalCount": 1, "changeKey": "***yFtCdJSH", "id": "AAMkADQ0NmFkODFkLWQ4MDEtNDE4Mi1hN2NlsjflsjfSF=" } } } ``` <h3 style={{display: 'inline'}}>ews-expand-group</h3> ### ews-expand-group Expands a distribution list to display all members. By default, expands only the first layer of the distribution list. If recursive-expansion is "True", the command expands nested distribution lists and returns all members. #### Permissions Impersonation rights required. In order to perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |email-address|Email address of the group to expand.|Required| |recursive-expansion|Whether to enable recursive expansion. Default is "False".|Optional| #### Outputs There is no context output for this command. #### Examples ``` !ews-expand-group email-address="TestPublic" recursive-expansion="False" ``` ##### Human Readable Output >|displayName|mailbox|mailboxType| >|--- |--- |--- | >|John Wick|john@wick.com|Mailbox| ##### Context Example ``` { "EWS.ExpandGroup": { "name": "TestPublic", "members": [ { "mailboxType": "Mailbox", "displayName": "John Wick", "mailbox": "john@wick.com" } ] } } ``` <h3 style={{display: 'inline'}}>ews-mark-items-as-read</h3> ### ews-mark-items-as-read Marks items as read or unread. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |item-ids|A CSV list of item IDs.|Required| |operation|How to mark the item. Can be "read" or "unread". Default is "read".|Optional| |target-mailbox|The mailbox on which to run the command. If empty, the command will be applied on the default mailbox.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.action|String|The action that was performed on the item.| |EWS.Items.itemId|String|The ID of the item.| |EWS.Items.messageId|String|The message ID of the item.| #### Examples ``` !ews-mark-items-as-read item-ids=AAMkADQ0NFSffU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAABjKMnpkUAAAfxw+jAAA= operation=read target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output >|action|itemId|messageId| >|--- |--- |--- | >|marked-as-read|AAMkADQ0NFSffU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAABjKMnpkUAAAfxw+jAAA=|| ##### Context Example ``` { "EWS": { "Items": { "action": "marked-as-read", "itemId": "AAMkADQ0NFSffU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAABjKMnpkUAAAfxw+jAAA= ", "messageId": "message_id" } } } ``` <h3 style={{display: 'inline'}}>send-mail</h3> ### send-mail *** Sends an email. #### Base Command `send-mail` #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations When sending the email to an Outlook account, Outlook UI fails to display custom headers. This does not happen when sending to a Gmail account. #### Inputs | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | to | Email addresses for the 'To' field. Supports comma-separated values. | Optional | | cc | Email addresses for the 'Cc' field. Supports comma-separated values. | Optional | | bcc | Email addresses for the 'Bcc' field. Supports comma-separated values. | Optional | | subject | Subject for the email to be sent. | Optional | | body | The contents (body) of the email to be sent in plain text. | Optional | | htmlBody | The contents (body) of the email to be sent in HTML format. | Optional | | attachIDs | A comma-separated list of War Room entry IDs that contain the files to attach to the email. | Optional | | attachNames | A comma-separated list to rename file names of corresponding attachment IDs. For example, rename the first two files - attachNames=file_name1,file_name2. rename first and third file - attachNames=file_name1,,file_name3. | Optional | | attachCIDs | A comma-separated list of CIDs to embed attachments inside the email itself. | Optional | | transientFile | A name for the attached file. You can pass multiple files in a comma-separated list, e.g., transientFile="t1.txt,temp.txt,t3.txt" transientFileContent="test 2,temporary file content,third file content" transientFileCID="t1.txt@xxx.yyy,t2.txt@xxx.zzz". | Optional | | transientFileContent | Content for the attached file. You can pass multiple files in a comma-separated list, e.g., transientFile="t1.txt,temp.txt,t3.txt" transientFileContent="test 2,temporary file content,third file content" transientFileCID="t1.txt@xxx.yyy,t2.txt@xxx.zzz". | Optional | | transientFileCID | CID for the attached file if it's inline. You can pass multiple files in a comma-separated list, e.g., transientFile="t1.txt,temp.txt,t3.txt" transientFileContent="test 2,temporary file content,third file content" transientFileCID="t1.txt@xxx.yyy,t2.txt@xxx.zzz". | Optional | | templateParams | Replace {varname} variables with values from this argument. Expected values are in the form of a JSON document, such ase {"varname": {"value": "some value", "key": "context key"}}. Each var name can either be provided with the value or a context key from which to retrieve the value. Note that only context data is accessible for this argument, while incident fields are not. | Optional | | additionalHeader | A comma-separated list of additional headers in the format: headerName=headerValue. For example: "headerName1=headerValue1,headerName2=headerValue2". | Optional | | raw_message | Raw email message. If provided, all other arguments will be ignored except "to", "cc", and "bcc". | Optional | | from | The email address from which to reply. | Optional | | replyTo | Email addresses that need to be used to reply to the message. Supports comma-separated values. | Optional | | importance | Sets the importance/Priority of the email. Default value is Normal. Possible values are: High, Normal, Low. Default is Normal. | Optional | | handle_inline_image | Whether to handle inline images in the HTML body. When set to 'True', inline images will be extracted from the HTML and attached to the email as an inline attachment object. Note that in some cases, attaching the image as an object may cause the image to disappear when replying to the email. Additionally, sending the image in the html body as base64 data (inline image) may cause the image to disappear if the image is too large or recognized as malicious and subsequently deleted. Possible values are: True, False. Default is True. | Optional | #### Context Output There is no context output for this command. #### Examples ``` !send-mail to=demisto@demisto.onmicrosoft.com subject=some_subject body=some_text attachIDs=110@457,116@457 htmlBody="Hello World" additionalHeader="some_header_name=some_header_value" transientFile=some_file.txt transientFileContent="Some file content" ``` ##### Human Readable Output >Mail sent successfully <h3 style={{display: 'inline'}}>ews-get-items-as-eml</h3> ### ews-get-items-as-eml Retrieves items by item ID and uploads its content as an EML file. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | item-id | The item ID of item to upload as and EML file. | Required | | target-mailbox | The mailbox in which this email was found. If empty, the default mailbox is used. Otherwise the user might require impersonation rights to this mailbox. | Optional | #### Outputs | **Path** | **Type** | **Description** | | --- | --- | --- | | File.Size | String | The size of the file. | | File.SHA1 | String | The SHA1 hash of the file. | | File.SHA256 | String | The SHA256 hash of the file. | | File.SHA512 | String | The SHA512 hash of the file. | | File.Name | String | The name of the file. | | File.SSDeep | String | The SSDeep hash of the file. | | File.EntryID | String | EntryID of the file | | File.Info | String | Information about the file. | | File.Type | String | The file type. | | File.MD5 | String | The MD5 hash of the file. | | File.Extension | String | The extension of the file. | #### Examples > > `` <h3 style={{display: 'inline'}}>reply-mail</h3> ### reply-mail Reply to an email #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | inReplyTo | ID of the item to reply to. | Required | | to | A comma-separated list of email addresses for the 'to' field. | Required | | cc | A comma-separated list of email addresses for the 'cc' field. | Optional | | bcc | A comma-separated list of email addresses for the 'bcc' field. | Optional | | subject | Subject for the email to be sent. | Optional | | body | The contents (body) of the email to send. | Optional | | htmlBody | HTML formatted content (body) of the email to be sent. This argument overrides the "body" argument. | Optional | | attachIDs | A comma-separated list of War Room entry IDs that contain files, and are used to attach files to the outgoing email. For example: attachIDs=15@8,19@8. | Optional | | attachNames | A comma-separated list of names of attachments to send. Should be the same number of elements as attachIDs. | Optional | | attachCIDs | A comma-separated list of CIDs to embed attachments within the email itself. | Optional | | handle_inline_image | Whether to handle inline images in the HTML body. When set to 'True', inline images are extracted from the HTML and attached to the email as inline attachment objects. Possible values are: True, False. Default is True. NOTE: Sometimes inline images sent in emails may not appear for recipients, either because their email system blocks the image (for example, due to image size or the email is flagged as malicious) or for other reasons. | Optional | #### Outputs There is no context output for this command. #### Examples ```!reply-mail item_id=AAMkAGY3OTQyMzMzLWYxNjktNDE0My05NmZhLWQ5MGY1YjIyNzBkNABGAAAAAACYCKjWAnXBTrnhgWJCcLX7BwDrxRwRjq/zTrN6vWSzK4OWAAAAAAEMAADrxRwRjq/zTrN6vWSzK4OWAAPYQGFeAAA= body=hello subject=hi to="avishai@demistodev.onmicrosoft.com"``` ##### Human Readable Output ##### Sent email >|attachments|from|subject|to| >|---|---|---|---| >| | avishai@demistodev.onmicrosoft.com | hi | avishai@demistodev.onmicrosoft.com | <h3 style={{display: 'inline'}}>ews-auth-reset</h3> ### ews-auth-reset Run this command if for some reason you need to rerun the authentication process. #### Permissions No additional permissions are needed. #### Limitations No known limitations. #### Inputs There is no input for this command. #### Outputs There is no context output for this command. ## Troubleshooting <h3 style={{display: 'inline'}}>Instance Configuration </h3> * If you are encountering the error 'Error in Microsoft authorization. Status: 401, body: invalid_client. Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app', create a new Client Secret for your application in the Azure Portal and use this value instead. <h3 style={{display: 'inline'}}>Instance Configuration </h3> No troubleshooting found. <h3 style={{display: 'inline'}}> Fetch command </h3> * If incidents are not being fetched, verify that no `pre-process` rule is configured that might filter some incidents out. * "address parts cannot contain CR or LF" error message in the logs means a corrupted email might have failed the process. In order to resolve this, you might need to remove the email from the folder being fetched. Contact Support Team if you believe the email is not corrupted. <h3 style={{display: 'inline'}}> Fetching Incidents crash due to unparsable emails </h3> If you find that your fetch incidents command is unable to parse a specific invalid email due to various parsing issues, you can follow these steps: 1. In the instance configuration, navigate to the _Collect_ section and click on _Advanced Settings_. 2. Check the box labeled _Skip unparsable emails during fetch incidents_. By enabling this option, the integration can catch and skip unparsable emails without causing the fetch incidents command to crash. When this parameter is active, a message will appear in the "Fetch History" panel of the instance whenever an unparsable email is recognized and skipped. This allows customers to be informed that a specific email was skipped and gives them the opportunity to open a support ticket if necessary. <h3 style={{display: 'inline'}}> General </h3> * ews-get-searchable-mailboxes: When using the UPN parameter, the command ews-get-searchable-mailboxes runs correctly after assigning RBAC roles requested in the management role header as explained in the [Microsoft Documentation](https://learn.microsoft.com/en-us/Exchange/policy-and-compliance/ediscovery/assign-permissions?redirectedfrom=MSDN&view=exchserver-2019).Configuration parameters
azure_cloud— Azure Cloud_client_id— ID / Application ID_tenant_id— Token / Tenant IDcredentials—default_target_mailbox— Email Address (required)upn_mailbox— UPN Addressfolder— Name of the folder from which to fetch incidentsaccess_type— Access Type (Impersonation is deprecated as of February 2025)is_public_folder— Public FolderisFetch— Fetch incidentsincidentType— Incident typefetch_time— First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)max_fetch— Maximum number of incidents per fetch (up to 200). Performance might be affected by a value higher than 50.mark_as_read— Mark fetched emails as readrequest_timeout— Timeout (in seconds) for HTTP requests to Exchange Serverinsecure— Trust any certificate (not secure)proxy— Use system proxy settingsseparate_process— Run as a separate process (protects against memory depletion)self_deployed— Use a self deployed Azure Applicationclient_id— ID / Application ID (Deprecated)tenant_id— Token / Tenant ID (Deprecated)client_secret— Key / Application Secret (Deprecated)incidentFetchInterval— Incidents Fetch IntervalincidentFilter— What time field should we filter incidents by?legacy_name— Use legacy attachment nameskip_unparsable_emails— Skip unparsable emails during fetch incidents
Commands (22)
-
ews-auth-resetRun this command if for some reason you need to rerun the authentication process.
-
ews-create-folderCreates a new folder in a specified mailbox.
-
ews-delete-attachmentDeletes the attachments of an item (email message).
-
ews-delete-itemsDelete items from mailbox.
-
ews-expand-groupExpands a distribution list to display all members. By default, expands only first layer of the distribution list. If recursive-expansion is "True", the command expands nested distribution lists and returns all members.
-
ews-find-foldersRetrieves information for folders for a specified mailbox. Only folders with read permissions will be returned. Your visual folders on the mailbox, such as "Inbox", are under the folder "Top of Information Store".
-
ews-get-attachmentRetrieves the actual attachments from an item (email message). To get all attachments for a message, only specify the item-id argument.
-
ews-get-contactsRetrieves contacts for a specified mailbox.
-
ews-get-folderRetrieves a single folder.
-
ews-get-itemsRetrieves items by item ID.
-
ews-get-items-as-emlRetrieves items by item ID and uploads its content as an EML file.
-
ews-get-items-from-folderRetrieves items from a specified folder in a mailbox. The items are order by the item created time, most recent is first.
-
ews-get-out-of-officeRetrieves the out-of-office status for a specified mailbox.
-
ews-get-searchable-mailboxesReturns a list of searchable mailboxes. This command requires eDiscovery permissions to the Exchange Server. For more information, see the EWSv2 integration documentation.
-
ews-mark-item-as-junkMarks an item as junk. This is commonly used to block an email address (meaning all future emails from this sender will be sent to the junk folder). For more information, see the Microsoft documentation: https://msdn.microsoft.com/en-us/library/office/dn481311(v=exchg.150).aspx.
-
ews-mark-items-as-readMarks items as read or unread.
-
ews-move-itemMove an item to different folder in the mailbox.
-
ews-move-item-between-mailboxesMoves an item from one mailbox to different mailbox.
-
ews-recover-messagesRecovers messages that were soft-deleted.
-
ews-search-mailboxSearches for items in the specified mailbox. Specific permissions are needed for this operation to search in a target mailbox other than the default.
-
reply-mailReplies to an email using EWS.
-
send-mailSends an email.
import email import hashlib from email import _header_value_parser as parser from email.policy import SMTP, SMTPUTF8 from io import StringIO from multiprocessing import Process from xml.sax import SAXParseException import chardet import demistomock as demisto # noqa: F401 import exchangelib from CommonServerPython import * # noqa: F401 from EWSApiModule import * from exchangelib import ( DELEGATE, OAUTH2, Body, EWSDateTime, EWSTimeZone, ExtendedProperty, FileAttachment, HTMLBody, ItemAttachment, ) from exchangelib.errors import ( ErrorFolderNotFound, ErrorMailboxMoveInProgress, ErrorMailboxStoreUnavailable, MalformedResponseError, RateLimitError, ) from exchangelib.items import Contact, Message from requests.exceptions import ConnectionError # Ignore warnings print to stdout warnings.filterwarnings("ignore") """ Constants """ INTEGRATION_NAME = get_integration_name() APP_NAME = "ms-ews-o365" FOLDER_ID_LEN = 120 MAX_INCIDENTS_PER_FETCH = 200 FETCH_TIME = demisto.params().get("fetch_time") or "10 minutes" # move results MOVED_TO_MAILBOX = "movedToMailbox" MOVED_TO_FOLDER = "movedToFolder" # item types FILE_ATTACHMENT_TYPE = "FileAttachment" ITEM_ATTACHMENT_TYPE = "ItemAttachment" ATTACHMENT_TYPE = "attachmentType" TOIS_PATH = "/root/Top of Information Store/" # context keys ATTACHMENT_ORIGINAL_ITEM_ID = "originalItemId" NEW_ITEM_ID = "newItemId" MESSAGE_ID = "messageId" ITEM_ID = "itemId" MAILBOX = "mailbox" MAILBOX_ID = "mailboxId" FOLDER_ID = "id" TARGET_MAILBOX = "receivedBy" # context paths CONTEXT_UPDATE_EWS_ITEM = ( f"EWS.Items((val.{ITEM_ID} === obj.{ITEM_ID} || " f"(val.{MESSAGE_ID} && obj.{MESSAGE_ID} && val.{MESSAGE_ID} === obj.{MESSAGE_ID}))" f" && val.{TARGET_MAILBOX} === obj.{TARGET_MAILBOX})" ) CONTEXT_UPDATE_EWS_ITEM_FOR_ATTACHMENT = f"EWS.Items(val.{ITEM_ID} == obj.{ATTACHMENT_ORIGINAL_ITEM_ID})" CONTEXT_UPDATE_FOLDER = f"EWS.Folders(val.{FOLDER_ID} == obj.{FOLDER_ID})" # fetch params LAST_RUN_TIME = "lastRunTime" LAST_RUN_IDS = "ids" LAST_RUN_IDS_DICT_REPRESENTATION = "ids_dict" LAST_RUN_FOLDER = "folderName" ERROR_COUNTER = "errorCounter" # Types of filter MODIFIED_FILTER = "modified-time" RECEIVED_FILTER = "received-time" # headers ITEMS_RESULTS_HEADERS = [ "sender", "subject", "hasAttachments", "datetimeReceived", "receivedBy", "author", "toRecipients", "textBody", ] # attachment name param LEGACY_NAME = argToBoolean(demisto.params().get("legacy_name", False)) UTF_8 = "utf-8" # If you are modifying this probably also need to modify in other files def exchangelib_cleanup(): # pragma: no cover key_protocols = list(exchangelib.protocol.CachingProtocol._protocol_cache.items()) try: exchangelib.close_connections() except Exception as ex: demisto.error(f"Error was found in exchangelib cleanup, ignoring: {ex}") for key, (protocol, _) in key_protocols: try: if "thread_pool" in protocol.__dict__: demisto.debug(f"terminating thread pool key{key} id: {id(protocol.thread_pool)}") protocol.thread_pool.terminate() del protocol.__dict__["thread_pool"] else: demisto.info(f"Thread pool not found (ignoring terminate) in protocol dict: {dir(protocol.__dict__)}") except Exception as ex: demisto.error(f"Error with thread_pool.terminate, ignoring: {ex}") """ LOGGING """ log_stream = None log_handler = None def start_logging(): global log_stream global log_handler logging.raiseExceptions = False if log_stream is None: log_stream = StringIO() log_handler = logging.StreamHandler(stream=log_stream) log_handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT)) logger = logging.getLogger() logger.addHandler(log_handler) logger.setLevel(logging.DEBUG) """ Helper Functions """ def get_client_from_params(params: dict) -> EWSClient: """ Parse the integration params and create an EWS client object Args: params (dict): dict received from demisto.params() Returns: EWSClient: EWS client object to interact with the exchange API """ client_id = params.get("_client_id") or params.get("client_id", "") client_secret = (params.get("credentials") or {}).get("password") or params.get("client_secret", "") tenant_id = params.get("_tenant_id") or params.get("tenant_id", "") if not client_secret: raise Exception("Key / Application Secret must be provided.") elif not client_id: raise Exception("ID / Application ID must be provided.") elif not tenant_id: raise Exception("Token / Tenant ID must be provided.") access_type = params.get("access_type", DELEGATE) or DELEGATE access_type = (access_type[0] if isinstance(access_type, list) else access_type).lower() default_target_mailbox = params.get("default_target_mailbox", "") max_fetch = min(int(params.get("max_fetch", MAX_INCIDENTS_PER_FETCH)), MAX_INCIDENTS_PER_FETCH) azure_cloud = get_azure_cloud(params, INTEGRATION_NAME) ews_server = f"{azure_cloud.endpoints.exchange_online}/EWS/Exchange.asmx/" folder = params.get("folder", "Inbox") is_public_folder = argToBoolean(params.get("is_public_folder", False)) request_timeout = int(params.get("request_timeout", 120)) mark_as_read = params.get("mark_as_read", False) incident_filter = IncidentFilter( params.get("incidentFilter", IncidentFilter.RECEIVED_FILTER) or IncidentFilter.RECEIVED_FILTER ) self_deployed = argToBoolean(params.get("self_deployed", False)) insecure = argToBoolean(params.get("insecure", False)) proxy = params.get("proxy", False) return EWSClient( client_id=client_id, client_secret=client_secret, access_type=access_type, default_target_mailbox=default_target_mailbox, max_fetch=max_fetch, ews_server=ews_server, auth_type=OAUTH2, version="O365", folder=folder, is_public_folder=is_public_folder, request_timeout=request_timeout, mark_as_read=mark_as_read, incident_filter=incident_filter, azure_cloud=azure_cloud, tenant_id=tenant_id, self_deployed=self_deployed, app_name=APP_NAME, insecure=insecure, proxy=proxy, ) def get_attachment_name(attachment_name, eml_extension=False, content_id="", is_inline=False): """ Retrieve attachment name or error string if none is provided :param attachment_name: attachment name to retrieve :param eml_extension: Indicates whether the eml extension should be added :return: string """ if is_inline and content_id and content_id != "None" and not LEGACY_NAME: if attachment_name is None or attachment_name == "": return ( f"{content_id}-attachmentName-demisto_untitled_attachment.eml" if eml_extension else f"{content_id}-attachmentName-demisto_untitled_attachment" ) elif eml_extension and not attachment_name.endswith(".eml"): return f"{content_id}-attachmentName-{attachment_name}.eml" return f"{content_id}-attachmentName-{attachment_name}" if attachment_name is None or attachment_name == "": return "demisto_untitled_attachment.eml" if eml_extension else "demisto_untitled_attachment" elif eml_extension and not attachment_name.endswith(".eml"): return f"{attachment_name}.eml" return attachment_name def prepare_args(args: dict[str, str]) -> dict: """ Prepare arguments to be used as the API expects it :param args: demisto args :return: transformed args """ args = {k.replace("-", "_"): v for k, v in list(args.items())} if "is_public" in args: args["is_public"] = args["is_public"] == "True" # type: ignore[assignment] if "from" in args: args["from_address"] = args.pop("from") return args def get_limited_number_of_messages_from_qs(qs, limit): # pragma: no cover """ Retrieve a limited number of messages from query search :param qs: query search to execute :param limit: limit on number of items to retrieve from search :return: list of exchangelib.Message """ count = 0 results = [] for item in qs: if count == limit: break if isinstance(item, Message): count += 1 results.append(item) return results def keys_to_camel_case(value): # pragma: no cover """ Transform keys from snake to camel case (does nothing if no snakes are found) :param value: value to transform :return: transformed value """ def str_to_camel_case(snake_str): components = snake_str.split("_") return components[0] + "".join(x.title() for x in components[1:]) if value is None: return None if isinstance(value, list | set): return list(map(keys_to_camel_case, value)) if isinstance(value, dict): return { keys_to_camel_case(k): keys_to_camel_case(v) if isinstance(v, list | dict) else v for (k, v) in list(value.items()) } return str_to_camel_case(value) def get_last_run(client: EWSClient, last_run=None): """ Retrieve the last run time :param client: EWS Client :param last_run: (Optional) last run object :return: last run dict """ if not last_run or last_run.get(LAST_RUN_FOLDER) != client.folder_name: last_run = { LAST_RUN_TIME: None, LAST_RUN_FOLDER: client.folder_name, LAST_RUN_IDS_DICT_REPRESENTATION: {}, } if last_run.get(LAST_RUN_TIME): last_run[LAST_RUN_TIME] = EWSDateTime.from_string(last_run[LAST_RUN_TIME]) # In case we have existing last_run data if not last_run.get(LAST_RUN_IDS_DICT_REPRESENTATION): if not last_run.get(LAST_RUN_IDS): last_run[LAST_RUN_IDS_DICT_REPRESENTATION] = {} else: last_run[LAST_RUN_IDS_DICT_REPRESENTATION] = {item: "" for item in last_run.get(LAST_RUN_IDS)} last_run[LAST_RUN_IDS].clear() return last_run def email_ec(item): """ Create entry context for an email :param item: exchangelib.Item :return: entry context dict """ return { "CC": None if not item.cc_recipients else [mailbox.email_address for mailbox in item.cc_recipients], "BCC": None if not item.bcc_recipients else [mailbox.email_address for mailbox in item.bcc_recipients], "To": None if not item.to_recipients else [mailbox.email_address for mailbox in item.to_recipients], "From": item.author.email_address, "Subject": item.subject, "Text": item.text_body, "HTML": item.body, "HeadersMap": None if not item.headers else {header.name: header.value for header in item.headers}, } def is_item_duplicate(item, exclude_ids, incident_filter): """ Checks if an item is a duplicate based on ID and Timestamp. RFC 5322 defines Message-ID values as ``<id@domain>``, but in practice the same message may appear in different forms across fetches — such as ``id@domain``, ``<id@domain>``, ``id@domain>``, or ``<id@domain`` — and they might change between fetches. To avoid duplicate incidents, we verify all possible forms in exclude_ids. Features: 1. Smart ID Lookup: Checks both Clean ID (abc) and Bracketed ID (<abc>, <abc, abc>). 2. Legacy Handling: Handles cases where stored value is "" (if last run is list not dict). 3. Timestamp Logic: Compares stored time vs item time. Returns: tuple[bool, str | None]: A tuple containing: - is_duplicate (bool): True if item is a duplicate (skip it), False if it should be processed. - stored_time (str | None): The stored fetch time for the item, or None if not found/not a duplicate. """ if not item.message_id or not exclude_ids: return False, None clean_id = item.message_id.strip().strip("<>") found_key = None for candidate in (clean_id, f"<{clean_id}>", f"{clean_id}>", f"<{clean_id}"): if candidate in exclude_ids: found_key = candidate break if found_key is None: return False, None demisto.debug(f"Dedup match: {item.message_id=}, {found_key=}, {clean_id=}") stored_time = exclude_ids[found_key] # If stored_time is "" or None, it means it was from an old fetch (List format). # We treat it as an existing duplicate and SKIP it. if not stored_time: return True, stored_time current_item_time = ( item.datetime_created.ewsformat() if incident_filter == RECEIVED_FILTER else item.last_modified_time.ewsformat() ) return stored_time >= current_item_time, stored_time def parse_item_as_dict(item, email_address=None, camel_case=False, compact_fields=False): # pragma: no cover """ Parses an exchangelib item as a dict :param item: exchangelib.Item to parse :param (Optional) email_address: string mailbox :param (Optional) camel_case: Is camel case :param (Optional) compact_fields: Is compact fields :return: Item as a dict """ def parse_object_as_dict(obj): raw_dict = {} if obj is not None: for field in obj.FIELDS: raw_dict[field.name] = getattr(obj, field.name, None) return raw_dict def parse_folder_as_json(folder): # pragma: no cover raw_dict = parse_object_as_dict(folder) if "parent_folder_id" in raw_dict: raw_dict["parent_folder_id"] = parse_folder_as_json(raw_dict["parent_folder_id"]) if "effective_rights" in raw_dict: raw_dict["effective_rights"] = parse_object_as_dict(raw_dict["effective_rights"]) return raw_dict raw_dict = {} for field, value in item._field_vals(): if type(value) in [str, str, int, float, bool, Body, HTMLBody, None]: raw_dict[field] = value raw_dict["id"] = item.id demisto.debug(f"checking for attachments in email with id {item.id}") if getattr(item, "attachments", None): raw_dict["attachments"] = [parse_attachment_as_dict(item.id, x) for x in item.attachments] for time_field in [ "datetime_sent", "datetime_created", "datetime_received", "last_modified_time", "reminder_due_by", ]: value = getattr(item, time_field, None) if value: raw_dict[time_field] = value.ewsformat() for dict_field in [ "effective_rights", "parent_folder_id", "conversation_id", "author", "extern_id", "received_by", "received_representing", "reply_to", "sender", "folder", ]: value = getattr(item, dict_field, None) if value: if isinstance(value, list): raw_dict[dict_field] = [] for single_val in value: raw_dict[dict_field].append(parse_object_as_dict(single_val)) else: raw_dict[dict_field] = parse_object_as_dict(value) for list_dict_field in ["headers", "cc_recipients", "to_recipients"]: value = getattr(item, list_dict_field, None) if value: raw_dict[list_dict_field] = [parse_object_as_dict(x) for x in value] for list_str_field in ["categories"]: value = getattr(item, list_str_field, None) if value: raw_dict[list_str_field] = value if getattr(item, "folder", None): raw_dict["folder"] = parse_folder_as_json(item.folder) folder_path = ( item.folder.absolute[len(TOIS_PATH) :] if item.folder.absolute.startswith(TOIS_PATH) else item.folder.absolute ) raw_dict["folder_path"] = folder_path if compact_fields: new_dict = {} # noinspection PyListCreation fields_list = [ "datetime_created", "datetime_received", "datetime_sent", "sender", "has_attachments", "importance", "message_id", "last_modified_time", "size", "subject", "text_body", "headers", "body", "folder_path", "is_read", "categories", ] if "id" in raw_dict: new_dict["itemId"] = raw_dict["id"] fields_list.append("itemId") for field in fields_list: if field in raw_dict: new_dict[field] = raw_dict.get(field) for field in ["received_by", "author", "sender"]: if field in raw_dict: new_dict[field] = raw_dict.get(field, {}).get("email_address") for field in ["to_recipients"]: if field in raw_dict: new_dict[field] = [x.get("email_address") for x in raw_dict[field]] attachments = raw_dict.get("attachments") if attachments and len(attachments) > 0: file_attachments = [x for x in attachments if x[ATTACHMENT_TYPE] == FILE_ATTACHMENT_TYPE] if len(file_attachments) > 0: new_dict["FileAttachments"] = file_attachments item_attachments = [x for x in attachments if x[ATTACHMENT_TYPE] == ITEM_ATTACHMENT_TYPE] if len(item_attachments) > 0: new_dict["ItemAttachments"] = item_attachments raw_dict = new_dict if camel_case: raw_dict = keys_to_camel_case(raw_dict) if email_address: raw_dict[MAILBOX] = email_address return raw_dict def get_entry_for_file_attachment(item_id, attachment): """ Creates a file entry for an attachment :param item_id: item_id of the attachment :param attachment: attachment dict :return: file entry dict for attachment """ entry = fileResult( get_attachment_name(attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline), attachment.content, ) entry["EntryContext"] = { CONTEXT_UPDATE_EWS_ITEM_FOR_ATTACHMENT + CONTEXT_UPDATE_FILE_ATTACHMENT: parse_attachment_as_dict(item_id, attachment) } return entry def parse_attachment_as_dict(item_id, attachment): """ Creates a note entry for an attachment :param item_id: item_id of the attachment :param attachment: attachment dict :return: note entry dict for attachment """ try: attachment_content = attachment.content if isinstance(attachment, FileAttachment) else attachment.item.mime_content if isinstance(attachment_content, str): # Strings must be encoded before hashing attachment_content = attachment_content.encode() return { ATTACHMENT_ORIGINAL_ITEM_ID: item_id, ATTACHMENT_ID: attachment.attachment_id.id, "attachmentName": get_attachment_name( attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline ), "attachmentSHA256": hashlib.sha256(attachment_content).hexdigest() if attachment_content else None, "attachmentContentType": attachment.content_type, "attachmentContentId": attachment.content_id, "attachmentContentLocation": attachment.content_location, "attachmentSize": attachment.size, "attachmentLastModifiedTime": attachment.last_modified_time.ewsformat(), "attachmentIsInline": attachment.is_inline, ATTACHMENT_TYPE: FILE_ATTACHMENT_TYPE if isinstance(attachment, FileAttachment) else ITEM_ATTACHMENT_TYPE, } except TypeError as e: if str(e) != "must be string or buffer, not None": raise return { ATTACHMENT_ORIGINAL_ITEM_ID: item_id, ATTACHMENT_ID: attachment.attachment_id.id, "attachmentName": get_attachment_name( attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline ), "attachmentSHA256": None, "attachmentContentType": attachment.content_type, "attachmentContentId": attachment.content_id, "attachmentContentLocation": attachment.content_location, "attachmentSize": attachment.size, "attachmentLastModifiedTime": attachment.last_modified_time.ewsformat(), "attachmentIsInline": attachment.is_inline, ATTACHMENT_TYPE: FILE_ATTACHMENT_TYPE if isinstance(attachment, FileAttachment) else ITEM_ATTACHMENT_TYPE, } def get_entry_for_item_attachment(item_id, attachment, target_email): # pragma: no cover """ Creates a note entry for an item attachment :param item_id: Item id :param attachment: exchangelib attachment :param target_email: target email :return: note entry dict for item attachment """ item = attachment.item dict_result = parse_attachment_as_dict(item_id, attachment) dict_result.update(parse_item_as_dict(item, target_email, camel_case=True, compact_fields=True)) title = ( f'EWS get attachment got item for "{target_email}", ' f'"{get_attachment_name(attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline)}"' # noqa: E501 ) return get_entry_for_object( title, CONTEXT_UPDATE_EWS_ITEM_FOR_ATTACHMENT + CONTEXT_UPDATE_ITEM_ATTACHMENT, dict_result, ) """ Command Functions """ def fetch_attachments_for_message(client: EWSClient, args: dict): # pragma: no cover """ Fetches attachments for a message :param client: EWS Client :param args: dict of command arguments :return: list of parsed entries """ item_id = args.get("item_id", "") target_mailbox = args.get("target_mailbox") attachment_ids = args.get("attachment_ids") identifiers_filter = args.get("identifiers_filter", "") identifiers_filter = argToList(identifiers_filter) attachment_ids = argToList(attachment_ids) account = client.get_account(target_mailbox) attachments = client.get_attachments_for_item(item_id, account, attachment_ids) entries = [] for attachment in attachments: if isinstance(attachment, FileAttachment): try: if attachment.content: entries.append(get_entry_for_file_attachment(item_id, attachment)) except TypeError as e: if str(e) != "must be string or buffer, not None": raise else: entries.append(get_entry_for_item_attachment(item_id, attachment, account.primary_smtp_address)) if attachment.item.mime_content: entries.append( fileResult( get_attachment_name( attachment_name=attachment.name, eml_extension=True, content_id=attachment.content_id, is_inline=attachment.is_inline, ), attachment.item.mime_content, ) ) return entries def search_items_in_mailbox(client: EWSClient, args: dict): # pragma: no cover """ Search items in mailbox :param client: EWS Client :param args: dict of command arguments :return: Output tuple """ query = args.get("query") message_id = args.get("message_id") folder_path = args.get("folder_path", "") limit = args.get("limit", 100) target_mailbox = args.get("target_mailbox") is_public = args.get("is_public") selected_fields = args.get("selected_fields", "all") if not query and not message_id: return_error("Missing required argument. Provide query or message-id") if message_id and message_id[0] != "<" and message_id[-1] != ">": message_id = f"<{message_id}>" account = client.get_account(target_mailbox) limit = int(limit) if folder_path.lower() == "inbox": folders = [account.inbox] elif folder_path: is_public = client.is_default_folder(folder_path, is_public) folders = [client.get_folder_by_path(folder_path, account, is_public)] else: folders = account.inbox.parent.walk() # pylint: disable=E1101 items = [] # type: ignore selected_all_fields = selected_fields == "all" if selected_all_fields: restricted_fields = [x.name for x in Message.FIELDS] else: restricted_fields = set(argToList(selected_fields)) # type: ignore restricted_fields.update(["id", "message_id"]) # type: ignore for folder in folders: if Message not in folder.supported_item_models: continue if query: items_qs = folder.filter(query).only(*restricted_fields) else: items_qs = folder.filter(message_id=message_id).only(*restricted_fields) items += get_limited_number_of_messages_from_qs(items_qs, limit) if len(items) >= limit: break items = items[:limit] searched_items_result = [ parse_item_as_dict( item, account.primary_smtp_address, camel_case=True, compact_fields=selected_all_fields, ) for item in items ] if not selected_all_fields: searched_items_result = [ {k: v for (k, v) in i.items() if k in keys_to_camel_case(restricted_fields)} for i in searched_items_result ] for item in searched_items_result: item["itemId"] = item.pop("id", "") hr_items = escape_hr_item_ids([{**item} for item in searched_items_result]) readable_output = tableToMarkdown( "Searched items", hr_items, headers=ITEMS_RESULTS_HEADERS if selected_all_fields else None, ) output = {CONTEXT_UPDATE_EWS_ITEM: searched_items_result} return readable_output, output, searched_items_result def get_contacts(client: EWSClient, args: dict): # pragma: no cover """ Retrieve contacts of the target mailbox or client mailbox :param client: EWS Client :param args: dict of command arguments :return: """ limit = args.get("limit", 100) target_mailbox = args.get("target_mailbox") def parse_physical_address(address): result = {} for attr in ["city", "country", "label", "state", "street", "zipcode"]: result[attr] = getattr(address, attr, None) return result def parse_phone_number(phone_number): result = {} for attr in ["label", "phone_number"]: result[attr] = getattr(phone_number, attr, None) return result def parse_contact(contact): contact_dict = { k: v if not isinstance(v, EWSDateTime) else v.ewsformat() for k, v in list(contact._field_vals()) if isinstance(v, str | EWSDateTime) } if isinstance(contact, Contact) and contact.physical_addresses: contact_dict["physical_addresses"] = list(map(parse_physical_address, contact.physical_addresses)) if isinstance(contact, Contact) and contact.phone_numbers: contact_dict["phone_numbers"] = list(map(parse_phone_number, contact.phone_numbers)) if isinstance(contact, Contact) and contact.email_addresses and len(contact.email_addresses) > 0: contact_dict["emailAddresses"] = [x.email for x in contact.email_addresses] contact_dict = keys_to_camel_case(contact_dict) contact_dict = {k: v for k, v in list(contact_dict.items()) if v} contact_dict.pop("mimeContent", None) contact_dict["originMailbox"] = target_mailbox return contact_dict account = client.get_account(target_mailbox) contacts = [] for contact in account.contacts.all()[: int(limit)]: # pylint: disable=E1101 contacts.append(parse_contact(contact)) readable_output = tableToMarkdown(f"Email contacts for {target_mailbox}", contacts) output = {"Account.Email(val.Address == obj.originMailbox).EwsContacts": contacts} return readable_output, output, contacts def find_folders(client: EWSClient, args: dict): """ Finds folders in the mailbox :param client: EWS Client :param args: dict of command arguments :return: Output tuple """ target_mailbox = args.get("target_mailbox") account = client.get_account(target_mailbox) root = account.root if client.is_public_folder: root = account.public_folders_root folders = [] for f in root.walk(): # pylint: disable=E1101 folder = folder_to_context_entry(f) folders.append(folder) folders_tree = root.tree() # pylint: disable=E1101 readable_output = folders_tree output = {"EWS.Folders(val.id == obj.id)": folders} return readable_output, output, folders def get_items_from_folder(client: EWSClient, args: dict): # pragma: no cover """ Retrieve items from folder path :param client: EWS Client :param args: dict of command arguments :return: Output tuple """ folder_path = args.get("folder_path", "") limit = args.get("limit", 100) target_mailbox = args.get("target_mailbox") is_public = args.get("is_public") get_internal_item = args.get("get_internal_item", "no") account = client.get_account(target_mailbox) limit = int(limit) get_internal_item = get_internal_item == "yes" is_public = client.is_default_folder(folder_path, is_public) folder = client.get_folder_by_path(folder_path, account, is_public) qs = folder.filter().order_by("-datetime_created")[:limit] items = get_limited_number_of_messages_from_qs(qs, limit) items_result = [] for item in items: item_attachment = parse_item_as_dict(item, account.primary_smtp_address, camel_case=True, compact_fields=True) for attachment in item.attachments: if get_internal_item and isinstance(attachment, ItemAttachment) and isinstance(attachment.item, Message): # if found item attachment - switch item to the attchment item_attachment = parse_item_as_dict( attachment.item, account.primary_smtp_address, camel_case=True, compact_fields=True, ) break items_result.append(item_attachment) hm_headers = [ "sender", "subject", "hasAttachments", "datetimeReceived", "receivedBy", "author", "toRecipients", "itemId", ] hr_items = escape_hr_item_ids([{**item} for item in items_result]) readable_output = tableToMarkdown("Items in folder " + folder_path, hr_items, headers=hm_headers) output = {CONTEXT_UPDATE_EWS_ITEM: items_result} return readable_output, output, items_result def get_items(client: EWSClient, args: dict): # pragma: no cover """ Get items from target mailbox or client mailbox :param client: EWS Client :param args: dict of command arguments :return: """ item_ids = args.get("item_ids", "") target_mailbox = args.get("target_mailbox") item_ids = argToList(item_ids) account = client.get_account(target_mailbox) items = client.get_items_from_mailbox(account, item_ids) items = [x for x in items if isinstance(x, Message)] items_as_incidents = [parse_incident_from_item(x) for x in items] items_to_context = [parse_item_as_dict(x, account.primary_smtp_address, True, True) for x in items] hr_items = escape_hr_item_ids([{**item} for item in items_to_context]) readable_output = tableToMarkdown("Get items", hr_items, ITEMS_RESULTS_HEADERS) output = { CONTEXT_UPDATE_EWS_ITEM: items_to_context, "Email": [email_ec(item) for item in items], } return readable_output, output, items_as_incidents def collect_manual_attachments(manualAttachObj): # pragma: no cover """Collect all manual attachments' data Args: manualAttachObj (str): String representation of the manually attached files list. Returns: List[Dict]. List of the files data. """ manually_attached_objects = argToList(manualAttachObj) attachments = [] for attachment in manually_attached_objects: file_res = demisto.getFilePath(os.path.basename(attachment["RealFileName"])) path = file_res["path"] with open(path, "rb") as fp: data = fp.read() attachments.append({"name": attachment["FileName"], "data": data, "cid": ""}) return attachments def collect_attachments(attachments_ids, attachments_cids, attachments_names): # pragma: no cover """Collect all attachments' data Args: attachments_ids (str): String representation of the files ids list. attachments_cids (str): String representation of the files content ids list. attachments_names (str): String representation of the files names list. Returns: List[Dict]. List of the files data. """ attachments = [] files_ids = argToList(attachments_ids) files_cids = argToList(attachments_cids) files_names = argToList(attachments_names) for index, file_id in enumerate(files_ids): try: file_res = demisto.getFilePath(file_id) path = file_res["path"] filename = files_names[index] if len(files_names) > index and files_names[index] else file_res["name"] cid = files_cids[index] if len(files_cids) > index and files_cids[index] else "" with open(path, "rb") as fp: data = fp.read() attachments.append({"name": filename, "data": data, "cid": cid}) except Exception as e: demisto.error(f"Invalid entry {file_id} with exception: {e}") return_error(f"Entry {file_id} is not valid or is not a file entry") return attachments def handle_transient_files(transient_files, transient_files_contents, transient_files_cids): """Creates the transient attachments data Args: transient_files (str): String representation of the transient files names list. transient_files_contents (str): String representation of the transient files content list. transient_files_cids (str): String representation of the transient files content ids list. Returns: List[Dict]. List of the transient files data. """ transient_attachments = [] files_names = argToList(transient_files) files_contents = argToList(transient_files_contents) files_cids = argToList(transient_files_cids) for index in range(len(files_names)): file_name = files_names[index] if index >= len(files_contents): break file_content = bytes(files_contents[index], UTF_8) file_cid = "" if index >= len(files_cids) else files_cids[index] transient_attachments.append({"name": file_name, "data": file_content, "cid": file_cid}) return transient_attachments def handle_template_params(template_params): # pragma: no cover """Translates the template params if they exist from the context Args: template_params (str): JSON string that represent the variables names to be replaced and the desired value. Value can be either real value or context key to fetch the value from. Returns: Dict. `variable_name: value_to_use` of the templated parameters. """ actual_params = {} if template_params: try: params = json.loads(template_params) for p in params: if params[p].get("value"): actual_params[p] = params[p]["value"] elif params[p].get("key"): actual_params[p] = demisto.dt(demisto.context(), params[p]["key"]) except ValueError as e: return_error(f"Unable to parse template_params: {e!s}") return actual_params def create_message_object(to, cc, bcc, subject, body, additional_headers, from_address, reply_to, importance): """Creates the message object according to the existence of additional custom headers.""" if additional_headers: return Message( to_recipients=to, author=from_address, cc_recipients=cc, bcc_recipients=bcc, subject=subject, reply_to=reply_to, body=body, importance=importance, **additional_headers, ) return Message( to_recipients=to, author=from_address, cc_recipients=cc, bcc_recipients=bcc, subject=subject, reply_to=reply_to, body=body, importance=importance, ) def create_message( to, handle_inline_image: bool = True, subject="", body="", bcc=None, cc=None, html_body=None, attachments=[], additional_headers=None, from_address=None, reply_to=None, importance=None, ): # pragma: no cover """Creates the Message object that will be sent. Args: to (list): Main recipients. cc (list): CC recipients. bcc (list): BCC recipients. subject (str): Email's subject. body (str): Email's simple text body. html_body (str): Email's html body. attachments (list): Files to be attached to the mail, both inline and as files. additional_headers (Dict): Custom headers to be added to the message. from_address (str): The email address from which to reply. reply_to (list): Email addresses that need to be used to reply to the message. handle_inline_image (bool): Whether to handle inline images in the HTML body. Returns: Message. Message object ready to be sent. """ demisto.debug(f"create_message: Received {len(attachments)} attachments, {handle_inline_image=}") if not html_body: # This is a simple text message - we cannot have CIDs here message = create_message_object(to, cc, bcc, subject, body, additional_headers, from_address, reply_to, importance) for attachment in attachments: if not attachment.get("cid"): new_attachment = FileAttachment(name=attachment.get("name"), content=attachment.get("data")) message.attach(new_attachment) else: html_attachments: list = [] if handle_inline_image: html_body, html_attachments = handle_html(html_body) attachments += html_attachments demisto.debug(f"create_message: Processed HTML body with {len(attachments)} attachments") message = create_message_object( to, cc, bcc, subject, HTMLBody(html_body), additional_headers, from_address, reply_to, importance ) for attachment in attachments: if not isinstance(attachment, FileAttachment): if not attachment.get("cid"): attachment = FileAttachment(name=attachment.get("name"), content=attachment.get("data")) else: attachment = FileAttachment( name=attachment.get("name"), content=attachment.get("data"), is_inline=True, content_id=attachment.get("cid"), ) message.attach(attachment) return message def add_additional_headers(additional_headers): """Adds custom headers to the Message object Args: additional_headers (str): Headers list as string. Example: headerName1=headerValue1,headerName2=headerValue2 Returns: Dict. Headers dictionary in the form of: `header_name: header value` """ headers = {} for header in argToList(additional_headers): header_name, header_value = header.split("=", 1) class TempClass(ExtendedProperty): distinguished_property_set_id = "InternetHeaders" property_name = header_name property_type = "String" try: Message.register(header_name, TempClass) headers[header_name] = header_value except ValueError: Message.deregister(header_name) try: Message.register(header_name, TempClass) headers[header_name] = header_value except ValueError as e: demisto.debug("EWSO365 - Header " + header_name + " could not be registered. " + str(e)) return headers def send_email(client: EWSClient, args: dict): # pragma: no cover to = argToList(args.get("to")) subject = args.get("subject", "") body = args.get("body", "") bcc = argToList(args.get("bcc")) cc = argToList(args.get("cc")) htmlBody = args.get("htmlBody") attachIDs = args.get("attachIDs", "") attachCIDs = args.get("attachCIDs", "") attachNames = args.get("attachNames", "") manualAttachObj = args.get("manualAttachObj") transientFile = args.get("transientFile") transientFileContent = args.get("transientFileContent") transientFileCID = args.get("transientFileCID") templateParams = args.get("templateParams") additionalHeader = args.get("additionalHeader") raw_message = args.get("raw_message") from_address = args.get("from_address") reply_to = argToList(args.get("replyTo")) importance = args.get("importance") render_body = argToBoolean(args.get("renderBody", False)) handle_inline_image = argToBoolean(args.get("handle_inline_image", True)) # Basic validation - we allow pretty much everything but you have to have at least a recipient # We allow messages without subject and also without body if not to and not cc and not bcc: return_error("You must have at least one recipient") if raw_message: message = Message( to_recipients=to, cc_recipients=cc, bcc_recipients=bcc, body=raw_message, author=from_address, reply_to=reply_to, importance=importance, ) else: if additionalHeader: additionalHeader = add_additional_headers(additionalHeader) # collect all types of attachments attachments = collect_attachments(attachIDs, attachCIDs, attachNames) attachments.extend(collect_manual_attachments(manualAttachObj)) attachments.extend(handle_transient_files(transientFile, transientFileContent, transientFileCID)) # update body and html_body with the templated params, if exists template_params = handle_template_params(templateParams) if template_params: body = body.format(**template_params) if htmlBody: htmlBody = htmlBody.format(**template_params) message = create_message( to, handle_inline_image, subject, body, bcc, cc, htmlBody, attachments, additionalHeader, from_address, reply_to, importance, ) client.send_email(message) results = [CommandResults(entry_type=EntryType.NOTE, raw_response="Mail sent successfully")] if render_body: results.append( CommandResults( entry_type=EntryType.NOTE, content_format=EntryFormat.HTML, raw_response=htmlBody, ) ) return results def reply_mail(client: EWSClient, args: dict): # pragma: no cover to = argToList(args.get("to", "")) inReplyTo = args.get("inReplyTo", "") subject = args.get("subject", "") body = args.get("body", "") bcc = argToList(args.get("bcc")) cc = argToList(args.get("cc")) htmlBody = args.get("htmlBody") attachIDs = args.get("attachIDs", "") attachCIDs = args.get("attachCIDs", "") attachNames = args.get("attachNames", "") manualAttachObj = args.get("manualAttachObj") handle_inline_image: bool = argToBoolean(args.get("handle_inline_image", True)) # collect all types of attachments attachments = collect_attachments(attachIDs, attachCIDs, attachNames) attachments.extend(collect_manual_attachments(manualAttachObj)) client.reply_email( in_reply_to=inReplyTo, to=to, body=body, subject=subject, bcc=bcc, cc=cc, html_body=htmlBody, attachments=attachments, handle_inline_image=handle_inline_image, ) def get_item_as_eml(client: EWSClient, args: dict): # pragma: no cover """ Retrieve item as an eml :param client: EWS Client :param args: dict of command arguments :return: Output tuple """ item_id = args.get("item_id", "") target_mailbox = args.get("target_mailbox") account = client.get_account(target_mailbox) item = client.get_item_from_mailbox(account, item_id) if item.mime_content: email_content = cast_mime_item_to_message(item) if item.headers: # compare header keys case-insensitive attached_email_headers = [ (h.lower(), " ".join(map(str.strip, v.split("\r\n")))) for (h, v) in list(email_content.items()) ] for header in item.headers: if ( header.name.lower(), header.value, ) not in attached_email_headers and header.name.lower() != "content-type": try: email_content.add_header(header.name, header.value) except ValueError as err: if "There may be at most" not in str(err): raise err eml_name = item.subject if item.subject else "demisto_untitled_eml" email_data = decode_email_data(email_content) file_result = fileResult(eml_name + ".eml", email_data) file_result = file_result if file_result else "Failed uploading eml file to war room" return file_result return None def handle_attached_email_with_incorrect_message_id(attached_email: Message): """This function handles a malformed Message-ID value which can be returned in the header of certain email objects. This issue happens due to a current bug in "email" library and further explained in XSUP-32074. Public issue link: https://github.com/python/cpython/issues/105802 The function will run on every attached email if exists, check its Message-ID header value and fix it if possible. Args: attached_email (Message): attached email object. Returns: Message: attached email object. """ message_id_value = "" for i in range(len(attached_email._headers)): if attached_email._headers[i][0].lower() == "message-id": message_id = attached_email._headers[i][1] message_header = attached_email._headers[i][0] demisto.debug(f"Handling Message-ID header, {message_id=}.") try: message_id_value = handle_incorrect_message_id(message_id) if message_id_value != message_id: # If the Message-ID header was fixed in the context of this function # the header will be replaced in _headers list attached_email._headers.pop(i) attached_email._headers.append((message_header, message_id_value)) except Exception as e: # The function is designed to handle a specific format error for the Message-ID header # as explained in the docstring. # That being said, we do expect the header to be in a known format. # If this function encounters a header format which is not in the known format and can't be fixed, # the header will be ignored completely to prevent crashing the fetch command. demisto.debug(f"Invalid {message_id=}, Error: {e}") break break return attached_email def handle_attached_email_with_incorrect_from_header(attached_email: Message): """This function handles a malformed From value which can be returned in the header of certain email objects. This issue happens due to a current bug in "email" library. Public issue link: https://github.com/python/cpython/issues/114906 The function will run on every attached email if exists, check its From header value and fix it if possible. Args: attached_email (Message): attached email object. Returns: Message: attached email object. """ for i, (header_name, header_value) in enumerate(attached_email._headers): if header_name.lower() == "from": demisto.debug(f"Handling From header, value={header_value}.") try: new_value = parser.get_address_list(header_value)[0].value new_value = new_value.replace("\n", " ").replace("\r", " ").strip() if header_value != new_value: # Update the 'From' header with the corrected value attached_email._headers[i] = (header_name, new_value) demisto.debug(f"From header fixed, new value: {new_value}") except Exception as e: demisto.debug(f"Error processing From header: {e}") break return attached_email def handle_incorrect_message_id(message_id: str) -> str: """ Use regex to identify and correct one of the following invalid message_id formats: 1. '<[message_id]>' --> '<message_id>' 2. '\r\n\t<[message_id]>' --> '\r\n\t<message_id>' If no necessary changes identified the original 'message_id' argument value is returned. """ if re.search(r"\<\[.*\]\>", message_id): # find and replace "<[" with "<" and "]>" with ">" fixed_message_id = re.sub(r"<\[(.*?)\]>", r"<\1>", message_id) demisto.debug("Fixed message id {message_id} to {fixed_message_id}") return fixed_message_id return message_id def decode_email_data(email_obj: Message): attached_email_bytes = email_obj.as_bytes() chardet_detection = chardet.detect(attached_email_bytes) encoding = chardet_detection.get("encoding", "utf-8") or "utf-8" try: # Trying to decode using the detected encoding data = attached_email_bytes.decode(encoding) except UnicodeDecodeError: # In case the detected encoding fails apply the default encoding demisto.info( f'Could not decode attached email using detected encoding: {encoding}, retrying ' f'using utf-8.\nAttached email details: ' f'\nMessage-ID = {email_obj.get("Message-ID")}' f'\nDate = {email_obj.get("Date")}' f'\nSubject = {email_obj.get("Subject")}' f'\nFrom = {email_obj.get("From")}' f'\nTo = {email_obj.get("To")}' ) try: data = attached_email_bytes.decode("utf-8") except UnicodeDecodeError: demisto.info("Could not decode attached email using utf-8. returned the content without decoding") data = attached_email_bytes # type: ignore return data def cast_mime_item_to_message(item): mime_content = item.mime_content email_policy = SMTP if mime_content.isascii() else SMTPUTF8 if isinstance(mime_content, str) and not mime_content.isascii(): mime_content = mime_content.encode() if isinstance(mime_content, bytes): message = email.message_from_bytes(mime_content, policy=email_policy) # type: ignore[arg-type] else: message = email.message_from_string(mime_content, policy=email_policy) # type: ignore[arg-type] return message def parse_incident_from_item(item): # pragma: no cover """ Parses an incident from an item :param item: item to parse :return: Parsed item """ incident = {} labels = [] demisto.debug(f"starting to parse the email with id {item.id} into an incident") try: incident["details"] = item.text_body or item.body except AttributeError: incident["details"] = item.body incident["name"] = item.subject labels.append({"type": "Email/subject", "value": item.subject}) incident["occurred"] = item.datetime_received.ewsformat() # handle recipients if item.to_recipients: for recipient in item.to_recipients: labels.append({"type": "Email", "value": recipient.email_address}) # handle cc if item.cc_recipients: for recipient in item.cc_recipients: labels.append({"type": "Email/cc", "value": recipient.email_address}) # handle email from if item.sender: labels.append({"type": "Email/from", "value": item.sender.email_address}) labels.append({"type": "Email/from/name", "value": item.sender.name}) # email format email_format = "" try: if item.text_body: labels.append({"type": "Email/text", "value": item.text_body}) email_format = "text" except AttributeError: pass if item.body: labels.append({"type": "Email/html", "value": item.body}) email_format = "HTML" labels.append({"type": "Email/format", "value": email_format}) # handle attachments if item.attachments: incident["attachment"] = [] demisto.debug(f"parsing {len(item.attachments)} attachments for item with id {item.id}") attachment_counter = 0 for attachment in item.attachments: attachment_counter += 1 demisto.debug(f"retrieving attachment number {attachment_counter} of email with id {item.id}") file_result = None label_attachment_type = None label_attachment_id_type = None if isinstance(attachment, FileAttachment): try: if attachment.content: # file attachment label_attachment_type = "attachments" label_attachment_id_type = "attachmentId" # save the attachment file_name = get_attachment_name( attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline ) demisto.debug( f"saving content number {attachment_counter}, " f"of size {sys.getsizeof(attachment.content)}, of email with id {item.id}" ) file_result = fileResult(file_name, attachment.content) # check for error if file_result["Type"] == entryTypes["error"]: demisto.error(file_result["Contents"]) raise Exception(file_result["Contents"]) # save attachment to incident incident["attachment"].append( { "path": file_result["FileID"], "name": get_attachment_name( attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline, ), "description": FileAttachmentType.ATTACHED if not attachment.is_inline else "", } ) except TypeError as e: if str(e) != "must be string or buffer, not None": raise continue except SAXParseException as e: # TODO: When a fix is released, we will need to bump the library version. # https://github.com/ecederstrand/exchangelib/issues/1200 demisto.debug( f"An XML error occurred while loading an attachments content." f"\nMessage ID is {item.id}" f"\nError: {e.getMessage()}" ) continue else: # other item attachment label_attachment_type = "attachmentItems" label_attachment_id_type = "attachmentItemsId" # save the attachment if attachment.item.mime_content: attached_email = cast_mime_item_to_message(attachment.item) if attachment.item.headers: # compare header keys case-insensitive attached_email_headers = [] attached_email = handle_attached_email_with_incorrect_message_id(attached_email) attached_email = handle_attached_email_with_incorrect_from_header(attached_email) for h, v in attached_email.items(): if not isinstance(v, str): try: v = str(v) except: # noqa: E722 demisto.debug(f'cannot parse the header "{h}"') continue v = " ".join(map(str.strip, v.split("\r\n"))) attached_email_headers.append((h.lower(), v)) demisto.debug(f"{attached_email_headers=}") for header in attachment.item.headers: if ( header.name.lower(), header.value, ) not in attached_email_headers and header.name.lower() != "content-type": try: if header.name.lower() == "message-id": """ Handle a case where a Message-ID header was NOT already in attached_email, and instead is coming from attachment.item.headers. Meaning it wasn't handled in handle_attached_email_with_incorrect_message_id function and instead it is handled here using handle_incorrect_message_id function.""" correct_message_id = handle_incorrect_message_id(header.value) if (header.name.lower(), correct_message_id) not in attached_email_headers: attached_email.add_header(header.name, correct_message_id) else: attached_email.add_header(header.name, header.value) except ValueError as err: if "There may be at most" not in str(err): raise err data = decode_email_data(attached_email) file_result = fileResult( get_attachment_name( attachment_name=attachment.name, eml_extension=True, content_id=attachment.content_id, is_inline=attachment.is_inline, ), data, ) if file_result: # check for error if file_result["Type"] == entryTypes["error"]: demisto.error(file_result["Contents"]) raise Exception(file_result["Contents"]) # save attachment to incident incident["attachment"].append( { "path": file_result["FileID"], "name": get_attachment_name( attachment_name=attachment.name, eml_extension=True, content_id=attachment.content_id, is_inline=attachment.is_inline, ), } ) labels.append( { "type": label_attachment_type, "value": get_attachment_name( attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline ), } ) labels.append({"type": label_attachment_id_type, "value": attachment.attachment_id.id}) demisto.debug(f"finished parsing attachment {attachment_counter} of email with id {item.id}") # handle headers if item.headers: headers = [] for header in item.headers: labels.append( { "type": f"Email/Header/{header.name}", "value": str(header.value), } ) headers.append(f"{header.name}: {header.value}") labels.append({"type": "Email/headers", "value": "\r\n".join(headers)}) # handle item id if item.message_id: labels.append({"type": "Email/MessageId", "value": str(item.message_id)}) if item.id: labels.append({"type": "Email/ID", "value": item.id}) labels.append({"type": "Email/itemId", "value": item.id}) # handle conversion id if item.conversation_id: labels.append({"type": "Email/ConversionID", "value": item.conversation_id.id}) incident["labels"] = labels demisto.debug(f"Starting to generate rawJSON for incident, from email with id {item.id}") incident["rawJSON"] = json.dumps(parse_item_as_dict(item, None), ensure_ascii=False) demisto.debug(f"Finished generating rawJSON from email with id {item.id}") return incident def fetch_emails_as_incidents(client: EWSClient, last_run, incident_filter, skip_unparsable_emails: bool = False): """ Fetch incidents :param client: EWS Client :param last_run: last run dict :return: """ last_run = get_last_run(client, last_run) demisto.debug(f"get_last_run: {last_run=}") last_fetch_time = last_run.get(LAST_RUN_TIME) excluded_ids = last_run.get(LAST_RUN_IDS_DICT_REPRESENTATION) try: last_emails = fetch_last_emails( client, client.folder_name, last_run.get(LAST_RUN_TIME), excluded_ids, incident_filter, ) incidents = [] incident: dict[str, str] = {} emails_ids = [] # Used for mark emails as read demisto.debug(f"{APP_NAME} - Started fetch with {len(last_emails)} at {last_fetch_time}") current_fetch_ids = {} last_modification_time = last_fetch_time if isinstance(last_modification_time, EWSDateTime): last_modification_time = last_modification_time.ewsformat() for item in last_emails: try: if item.message_id: current_fetch_ids[item.message_id] = ( item.datetime_created.ewsformat() if incident_filter == RECEIVED_FILTER else item.last_modified_time.ewsformat() ) incident = parse_incident_from_item(item) incidents.append(incident) if incident_filter == MODIFIED_FILTER: item_modified_time = item.last_modified_time.ewsformat() if last_modification_time is None or last_modification_time < item_modified_time: last_modification_time = item_modified_time if item.id and item.is_read is False: emails_ids.append(item.id) if len(incidents) >= client.max_fetch: break else: demisto.debug(f"Skipped item: item with no message_id {item=}") except Exception as e: if not skip_unparsable_emails: # default is to raise and exception and fail the command raise # when the skip param is `True`, we log the exceptions and move on instead of failing the whole fetch error_msg = ( "Encountered email parsing issue while fetching. " f"Skipping item with message id: {item.message_id or '<error parsing message_id>'}" ) demisto.debug(f"{error_msg}, Error: {e!s} {traceback.format_exc()}") demisto.updateModuleHealth(error_msg, is_error=False) demisto.debug(f"{APP_NAME} - ending fetch - got {len(incidents)} incidents.") if incident_filter == MODIFIED_FILTER: last_incident_run_time = last_modification_time else: # default case - using 'received' time last_incident_run_time = incident.get("occurred", last_fetch_time) # making sure both last fetch time and the time of most recent incident are the same type for comparing. if isinstance(last_incident_run_time, EWSDateTime): last_incident_run_time = last_incident_run_time.ewsformat() if isinstance(last_fetch_time, EWSDateTime): last_fetch_time = last_fetch_time.ewsformat() demisto.debug( f"#### last_incident_time: {last_incident_run_time}({type(last_incident_run_time)})." f"last_fetch_time: {last_fetch_time}({type(last_fetch_time)}) ####" ) # If the fetch query is not fully fetched (we didn't have any time progress) - then we keep the # id's from current fetch until progress is made. This is for when max_fetch < incidents_from_query. if not last_incident_run_time or not last_fetch_time or last_incident_run_time > last_fetch_time: ids = current_fetch_ids else: ids = excluded_ids | current_fetch_ids new_last_run = { LAST_RUN_TIME: last_incident_run_time, LAST_RUN_FOLDER: client.folder_name, LAST_RUN_IDS_DICT_REPRESENTATION: ids, ERROR_COUNTER: 0, } demisto.debug(f"Set last run to: {new_last_run=}") demisto.setLastRun(new_last_run) if client.mark_as_read: mark_item_as_read(client, {"item_ids": emails_ids}) return incidents except RateLimitError: if LAST_RUN_TIME in last_run: last_run[LAST_RUN_TIME] = last_run[LAST_RUN_TIME].ewsformat() if ERROR_COUNTER not in last_run: last_run[ERROR_COUNTER] = 0 last_run[ERROR_COUNTER] += 1 demisto.setLastRun(last_run) if last_run[ERROR_COUNTER] > 2: raise return [] def fetch_last_emails( client: EWSClient, folder_name="Inbox", since_datetime=None, exclude_ids=None, incident_filter=RECEIVED_FILTER ): """ Fetches last emails :param client: EWS client :param (Optional) folder_name: folder name to pull from :param (Optional) since_datetime: items will be searched after this datetime :param (Optional) exclude_ids: exclude ids from fetch :return: list of exchangelib.Items """ qs = client.get_folder_by_path(folder_name, is_public=client.is_public_folder) demisto.debug(f"Finished getting the folder named {folder_name} by path") if since_datetime: if incident_filter == RECEIVED_FILTER: qs = qs.filter(datetime_received__gte=since_datetime) else: qs = qs.filter(last_modified_time__gte=since_datetime) else: tz = EWSTimeZone("UTC") first_fetch_datetime = dateparser.parse(FETCH_TIME) assert first_fetch_datetime is not None first_fetch_ews_datetime = EWSDateTime.from_datetime(first_fetch_datetime.replace(tzinfo=tz)) demisto.debug(f"{first_fetch_ews_datetime=}") if incident_filter == RECEIVED_FILTER: qs = qs.filter(datetime_received__gte=first_fetch_ews_datetime) else: qs = qs.filter(last_modified_time__gte=first_fetch_ews_datetime) qs = qs.filter().only(*[x.name for x in Message.FIELDS if x.name.lower() != "mime_content"]) if incident_filter == RECEIVED_FILTER: qs = qs.filter().order_by("datetime_received") else: qs = qs.filter().order_by("last_modified_time") result = [] exclude_ids = exclude_ids if exclude_ids else {} demisto.debug(f"{APP_NAME} - Exclude ID list: {exclude_ids}") qs.chunk_size = min(client.max_fetch, 100) qs.page_size = min(client.max_fetch, 100) demisto.debug("Before iterating on queryset") demisto.debug(f"Size of the queryset object in fetch-incidents: {sys.getsizeof(qs)}") for item in qs: demisto.debug("next iteration of the queryset in fetch-incidents") if isinstance(item, Message): is_duplicate, previous_fetch_time = is_item_duplicate(item, exclude_ids, incident_filter) if is_duplicate: received_time = item.datetime_created.ewsformat() modified_time = item.last_modified_time.ewsformat() demisto.debug( f"{item.subject=} with {item.message_id=} was excluded. previous fetch time: " f"{previous_fetch_time}, (if no time - because of the transition from list to dict). " f"current fetch time: {received_time if incident_filter == RECEIVED_FILTER else modified_time}" ) continue demisto.debug(f"Appending {item.subject=} with {item.message_id=}") result.append(item) if len(result) >= client.max_fetch: break demisto.debug(f"{APP_NAME} - Got total of {len(result)} from ews query.") return result def test_module(client: EWSClient, max_fetch): # pragma: no cover """ test-module * Max incidents per fetch <= MAX_INCIDENTS_PER_FETCH * Account can be retrieved * Account has read rights * Test access to fetch folder :param client: EWS Client :param max_fetch: Max fetches per incident :return: "ok" """ try: if int(max_fetch) > MAX_INCIDENTS_PER_FETCH: return_error( f"Error - Max incidents per fetch cannot be greater than {MAX_INCIDENTS_PER_FETCH}. You provided: {max_fetch}" ) account = client.get_account() if not account.root.effective_rights.read: # pylint: disable=E1101 raise Exception( "Success to authenticate, but user has no permissions to read from the mailbox. " "Need to delegate the user permissions to the mailbox - " "please read integration documentation and follow the instructions" ) client.get_folder_by_path(client.folder_name, account, client.is_public_folder).test_access() except ErrorFolderNotFound as e: if "Top of Information Store" in str(e): raise Exception( "Success to authenticate, but user probably has no permissions to read from the specific folder." "Check user permissions. You can try !ews-find-folders command to " "get all the folders structure that the user has permissions to" ) except Exception as e: if "403" in str(e): error_message_simple = ( "Got invalid response with status code: 403." " Please make sure you have the right permissions to your application.\n" ) raise DemistoException(error_message_simple) raise DemistoException(str(e)) return "ok" def sub_main(): # pragma: no cover is_test_module = False params = demisto.params() args = prepare_args(demisto.args()) # client's default_target_mailbox is the authorization source for the instance params["default_target_mailbox"] = args.get( "target_mailbox", args.get("source_mailbox", params.get("default_target_mailbox", "")) ) if params.get("upn_mailbox") and not (args.get("target_mailbox")): params["default_target_mailbox"] = params.get("upn_mailbox", "") if params.get("access_type") == "Impersonation": demisto.info( "Note: The access type Impersonation you are using is deprecated. For more information, " "please refer to the integration description." ) try: client = get_client_from_params(params) start_logging() # replace sensitive access_token value in logs if not isinstance(client.credentials, CustomDomainOAuth2Credentials): # Should not fail raise DemistoException("Failed to initialize EWS Client properly, check credentials") add_sensitive_log_strs(client.credentials.access_token.get("access_token", "")) command = demisto.command() # commands that return a single note result normal_commands = { "ews-get-searchable-mailboxes": get_searchable_mailboxes, "ews-move-item": move_item, "ews-delete-items": delete_items, "ews-search-mailbox": search_items_in_mailbox, "ews-get-contacts": get_contacts, "ews-get-out-of-office": get_out_of_office_state, "ews-recover-messages": recover_soft_delete_item, "ews-create-folder": create_folder, "ews-mark-item-as-junk": mark_item_as_junk, "ews-find-folders": find_folders, "ews-get-items-from-folder": get_items_from_folder, "ews-get-items": get_items, "ews-get-folder": get_folder, "ews-expand-group": get_expanded_group, "ews-mark-items-as-read": mark_item_as_read, "ews-delete-attachment": delete_attachments_for_message, "ews-get-attachment": fetch_attachments_for_message, } # commands that may return multiple results or non-note result special_output_commands = { "ews-get-items-as-eml": get_item_as_eml, "reply-mail": reply_mail, } # system commands: if command == "test-module": is_test_module = True demisto.results(test_module(client, params.get("max_fetch"))) elif command == "fetch-incidents": last_run = demisto.getLastRun() incident_filter = params.get("incidentFilter", RECEIVED_FILTER) if incident_filter not in [RECEIVED_FILTER, MODIFIED_FILTER]: # Ensure it's one of the allowed filter values incident_filter = RECEIVED_FILTER # or if not, force it to the default, RECEIVED_FILTER skip_unparsable_emails: bool = argToBoolean(params.get("skip_unparsable_emails", False)) demisto.debug(f"{incident_filter=}, {skip_unparsable_emails=}") incidents = fetch_emails_as_incidents(client, last_run, incident_filter, skip_unparsable_emails) demisto.debug(f"Saving incidents with size {sys.getsizeof(incidents)}, len:{len(incidents)}") demisto.incidents(incidents) elif command == "send-mail": commands_res = send_email(client, args) return_results(commands_res) elif command == "ews-move-item-between-mailboxes": # Needs a client configured for each one of the mailboxes dest_params = params.copy() dest_params["default_target_mailbox"] = args.get("destination_mailbox") dest_client = get_client_from_params(dest_params) return_results( move_item_between_mailboxes( src_client=client, args=args, dest_client=dest_client, ) ) # special outputs commands elif command in special_output_commands: demisto.results(special_output_commands[command](client, args)) # type: ignore[operator] elif command == "ews-auth-reset": return_results(reset_auth()) # normal commands else: output = normal_commands[command](client, args) # type: ignore[operator] if isinstance(output, tuple): # Legacy, some commands return a tuple for return outputs return_outputs(*output) else: return_results(output) except Exception as e: demisto.error(f"got exception {e}") start_logging() debug_log = log_stream.getvalue() # type: ignore[union-attr] error_message_simple = "" # Office365 regular maintenance case if isinstance(e, ErrorMailboxStoreUnavailable | ErrorMailboxMoveInProgress): log_message = ( "Office365 is undergoing load balancing operations. As a result, the service is temporarily unavailable." ) if demisto.command() == "fetch-incidents": demisto.info(log_message) demisto.incidents([]) sys.exit(0) if is_test_module: demisto.results(log_message + " Please retry the instance configuration test.") sys.exit(0) error_message_simple = log_message + " Please retry your request." if isinstance(e, ConnectionError): error_message_simple = f"Could not connect to the server.\nAdditional information: {e!s}" else: if is_test_module and isinstance(e, MalformedResponseError): error_message_simple = "Got invalid response from the server.\n" # Legacy error handling if "Status code: 401" in debug_log: error_message_simple = "Got unauthorized from the server. " if "Status code: 503" in debug_log: error_message_simple = "Got timeout from the server. Probably the server is not reachable with the current settings. " if not error_message_simple: error_message = error_message_simple = str(e) else: error_message = error_message_simple + "\n" + str(e) stacktrace = traceback.format_exc() if stacktrace: error_message += "\nFull stacktrace:\n" + stacktrace if debug_log: error_message += "\nFull debug log:\n" + debug_log if demisto.command() == "fetch-incidents": raise if demisto.command() == "ews-search-mailbox" and isinstance(e, ValueError): return_error( message="Selected invalid field, please specify valid field name.", error=e, ) if is_test_module: demisto.results(error_message_simple) else: demisto.results( { "Type": entryTypes["error"], "ContentsFormat": formats["text"], "Contents": error_message_simple, } ) demisto.error(f"{e.__class__.__name__}: {error_message}") finally: exchangelib_cleanup() if log_stream: try: logging.getLogger().removeHandler(log_handler) # type: ignore log_stream.close() except Exception as ex: demisto.error(f"EWS: unexpected exception when trying to remove log handler: {ex}") def process_main(): """setup stdin to fd=0 so we can read from the server""" sys.stdin = os.fdopen(0, "r") sub_main() def main(): # pragma: no cover # When running big queries, like 'ews-search-mailbox' the memory might not be freed by the garbage # collector. `separate_process` flag will run the integration on a separate process that will prevent # memory leakage. separate_process = demisto.params().get("separate_process", False) demisto.debug(f"Running as separate_process: {separate_process}") if separate_process: try: p = Process(target=process_main) p.start() p.join() demisto.debug("subprocess finished") except Exception as ex: demisto.error(f"Failed starting Process: {ex}") else: sub_main() if __name__ in ("__main__", "__builtin__", "builtins"): main()