Details
| ID | EWS v2 |
|---|---|
| Provider | Microsoft |
| Category | |
| From Version | 5.0.0 |
| Docker Image | demisto/py-ews:5.6.0.12072219 |
| 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 v2 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.
Note: Starting from pack version 2.0.0 the EWS v2 integration requires the Exchange server to support TLS v1.2 and up in order to connect.
Multi-Factor Authentication (MFA)
EWS v2 does not support Multi-Factor Authentication (MFA).
If using MFA, use EWS O365 (see https://xsoar.pan.dev/docs/reference/integrations/ewso365)
or if you have Graph Outlook use O365 Outlook Mail (Using Graph API) (see https://xsoar.pan.dev/docs/reference/integrations/microsoft-graph-mail)
or O365 Outlook Mail Single User (Using Graph API) (see https://xsoar.pan.dev/docs/reference/integrations/microsoft-graph-mail-single-user).
EWS v2 Playbooks
- Office 365 Search and Delete
- Search And Delete Emails - EWS
- Get Original Email - EWS
- Process Email - EWS
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.
This can be achieved in the following ways:- Use the
ews-search-mailboxescommand to search for all emails in a specific scope of mailboxes. Use the filter argument to narrow the search for emails sent from a specific account and more. - 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.
Both of these commands retrieve the ItemID field for each email item listed in the results. The
ItemIDcan be used in theews-get-itemscommand in order to get more information about the email item itself.
For instance, use theews-search-mailboxescommand to hunt for emails that were marked as malicious in prior investigations, across organization mailboxes. Focus your hunt on emails sent from a specific mail account, emails with a specific subject and more. - Use the
- Get email attachment information. Use the
ews-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 the
ews-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. - Send notifications to external users.
- Send an email asking for a response to be returned as part of a playbook. See Receiving an email reply
Configure EWS v2 in Cortex
| Parameter | Required |
|---|---|
| Email address | True |
| Password | True |
| Email address from which to fetch incidents | True |
| Name of the folder from which to fetch incidents (supports Exchange Folder ID and sub-folders e.g. Inbox/Phishing) | True |
| Public Folder | False |
| Has impersonation rights | False |
| Use system proxy settings | False |
| Fetch incidents | False |
| First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days) | False |
| Mark fetched emails as read | False |
| Incident type | False |
| ┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉ Manual Mode Exchange Server Hostname or IP address |
False |
| DOMAIN\USERNAME (e.g. DEMISTO.INT\admin) | False |
| Exchange Server Version (On-Premise only. Supported versions: 2007, 2010, 2010_SP2, 2013, 2016, and 2019) | False |
| Trust any certificate (not secure) | False |
| ┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉ Advanced Mode Override Authentication Type (NTLM, Basic, or Digest). |
False |
| Timeout (in seconds) for HTTP requests to Exchange Server | False |
| Max incidents per fetch | False |
| Run as a separate process (protects against memory depletion) | False |
| Skip unparsable emails during fetch incidents | False |
Fetched Incidents Data
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: The initial fetch interval is the previous 10 minutes. If no emails were fetched before from the destination folder, all emails from 10 minutes prior to the instance configuration and up to the current time will be fetched. Additionally, moving messages manually to the destination folder will not trigger a fetch incident. Define rules on phishing/target mailbox instead of moving messages manually.
You can configure the First fetch timestamp field to determine how much time back you want to fetch incidents.
Notice that it might require you to set the Timeout field to a higher value.
Pay special attention to the following fields in the instance settings:
-
Email address from which to fetch incidents– 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. Please note, if Exchange is configured with an international flavor,Inboxwill be named according to the configured language. -
Has impersonation rights– mark this option if you set the target mailbox to an account different than your personal account. Otherwise Delegation access will be used instead of Impersonation.
Find more information on impersonation or delegation rights in the Additional Information section.
Commands
You can execute these commands from the CLI, as part of an automation, or in a playbook.
After you successfully execute a command, a DBot message appears in the War Room with the command details.
ews-get-attachment
Retrieves the actual attachments from an item (email message). To get all attachments for a message, only specify the item-id argument.
Base Command
ews-get-attachment
Input
| 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 retrieve from the message. Support multiple attachments with comma-separated value or array. | Optional |
Context Output
| 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 | The 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. |
Command Example
!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>"
}
}
}
}
ews-delete-attachment
Deletes the attachments of an item (email message).
Base Command
ews-delete-attachment
Input
| 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 CSV list (or array) of attachment IDs to delete. If empty, all attachments will be deleted from the message. | Optional |
Context Output
| 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’. |
Command Example
!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="
}
}
}
}
ews-get-searchable-mailboxes
Returns a list of searchable mailboxes. This command requires eDiscovery permissions to the Exchange Server. For more information, see the EWSv2 integration documentation.
Base Command
ews-get-searchable-mailboxes
Input
There are no input arguments for this command.
Context Output
| 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. |
Command Example
#### Human Readable Output
>|displayName|isExternal|mailbox|mailboxId|
>|---|---|---|--- |
>| test | false |<test@demistodev.onmicrosoft.com> | /o=Exchange***/ou=Exchange Administrative Group ()/cn=**/cn=**-**|
#### Context Example
```json
{
"EWS": {
"Mailboxes": [
{
"mailbox": "test@demistodev.onmicrosoft.com",
"displayName": "test",
"mailboxId": "/o=Exchange***/ou=Exchange Administrative Group ()/cn=**/cn=**-**",
"isExternal": "false"
}
]
}
}
ews-search-mailboxes
Searches over multiple mailboxes or all Exchange mailboxes. Use either the mailbox-search-scope command or the email-addresses command to search specific mailboxes. This command requires eDiscovery permissions to the Exchange Server. For more information, see the EWS v2 integration documentation.
The number of mailboxes to search in may be limited by Microsoft Exchange. See here for more information.
Base Command
ews-search-mailboxes
Input
| Argument Name | Description | Required |
|---|---|---|
| filter | The filter query to search. | Required |
| mailbox-search-scope | The mailbox IDs to search. If empty, all mailboxes are searched. | Optional |
| limit | Maximum number of results to return. Default is 250. | Optional |
| email_addresses | CSV list or array of email addresses. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| EWS.Items.itemId | string | The item ID. |
| EWS.Items.mailbox | string | The mailbox address where the item was found. |
| EWS.Items.subject | string | The subject of the email. |
| EWS.Items.toRecipients | Unknown | List of recipient email addresses. |
| EWS.Items.sender | string | Sender email address. |
| EWS.Items.hasAttachments | boolean | Whether the email has attachments? |
| EWS.Items.datetimeSent | date | Sent time of the email. |
| EWS.Items.datetimeReceived | date | Received time of the email. |
Command Example
!ews-search-mailboxes filter="subject:Test" limit=1
Human Readable Output
datetimeReceived datetimeSent hasAttachments itemId mailbox sender subject toRecipients 2019-08-11T11:00:28Z 2019-08-11T11:00:28Z false AAMkAGY3OTQyMzMzLWYxNjktNDE0My05NmZhLWQ5MGY1YjIyNzBkNABGACASFAACYCKjWAnXDFrfsdhdnfkanpAAA= test2@demistodev.onmicrosoft.com John Smith test report dem@demistodev.onmicrosoft.com
Context Example
{
"EWS": {
"Items": {
"itemId": "AAMkAGY3OTQyMzMzLWYxNjktNDE0My05NmZhLWQ5MGY1YjIyNzBkNABGACASFAACYCKjWAnXDFrfsdhdnfkanpAAA=",
"sender": "John Smith",
"datetimeReceived": "2019-08-11T11:00:28Z",
"hasAttachments": "false",
"toRecipients": [
"dem@demistodev.onmicrosoft.com"
],
"mailbox": "test2@demistodev.onmicrosoft.com",
"datetimeSent": "2019-08-11T11:00:28Z",
"subject": "test report "
}
}
}
ews-move-item
Move an item to different folder in the mailbox.
Base Command
ews-move-item
Input
| 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. Possible values are: True, False. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| EWS.Items.newItemID | string | The item ID after 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”. |
ews-delete-items
Delete items from mailbox. This command requires eDiscovery permissions to the Exchange Server. For more information, see the EWSv2 integration documentation.
Base Command
ews-delete-items
Input
| Argument Name | Description | Required |
|---|---|---|
| item-ids | The item IDs to delete. | Required |
| delete-type | Deletion type. Can be “trash”, “soft”, or “hard”. Default is soft. | Required |
| target-mailbox | The mailbox on which to run the command. | Optional |
Context Output
| 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’. |
Command Example
!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>"
}
}
}
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.
Base Command
ews-search-mailbox
Input
| 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. Default is 100. | Optional |
| target-mailbox | The mailbox on which to apply the search. | Optional |
| is-public | Whether the folder is a Public Folder?. Possible values are: True, False. | Optional |
| message-id | The message ID of the email. This will be ignored if a query argument is provided. | Optional |
| selected-fields | A CSV list of fields to retrieve. Possible values are: . Default is all. | Optional |
| surround_id_with_angle_brackets | Whether to surround the message ID with angle brackets (<>) if it does not exist. Default is ‘True’. | Optional |
Context Output
| 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. |
Command Example
!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": "<html>\r\n<head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n<style type=\"text/css\" style=\"display:none;\"><!-- P {margin-top:0;margin-bottom:0;} --></style>\r\n</head>\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</html>\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": "<mesage_id>",
"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"
}
}
}
ews-get-contacts
Retrieves contacts for a specified mailbox.
Base Command
ews-get-contacts
Input
| Argument Name | Description | Required |
|---|---|---|
| target-mailbox | The mailbox for which to retrieve the contacts. | Optional |
| limit | Maximum number of results to return. Default is 100. | Optional |
Context Output
| 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. |
Command Example
!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": "<html><body></body></html>",
"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"
}
]
}
ews-resolve-name
This operation verifies aliases and matches display names to the correct mailbox user. It handles one ambiguous name at a time. If there are multiple potential matches, all will be returned, but limited to a maximum of 100 candidates.
Base Command
ews-resolve-name
Input
| Argument Name | Description | Required |
|---|---|---|
| identifier | The text value of this argument is used to resolve names against the following fields: First name, Last name, Display name, Full name, Office, Alias, SMTP address. Eg. John Doe or sip:johndoe@example.com. |
Required |
| full-contact-data | Describes whether the full contact details for public contacts for a resolved name are returned. Possible values are: True, False. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| EWS.ResolvedNames.FullContactInfo.contactSource | String | Whether the contact is located in the Exchange store or Active Directory Domain Services (AD DS). |
| EWS.ResolvedNames.FullContactInfo.culture | String | Represents the culture for a given item in a mailbox. |
| EWS.ResolvedNames.FullContactInfo.displayName | String | The display name of a contact. |
| EWS.ResolvedNames.FullContactInfo.ItemId | String | Contains the unique identifier and change key of an item in the Exchange store. |
| EWS.ResolvedNames.FullContactInfo.emailAddresses | String | Represents a collection of email addresses for a contact. |
| EWS.ResolvedNames.FullContactInfo.givenName | String | Contains a contact’s given name. |
| EWS.ResolvedNames.FullContactInfo.importance | String | Describes the importance of an item. |
| EWS.ResolvedNames.FullContactInfo.initials | String | Represents the initials of a contact. |
| EWS.ResolvedNames.FullContactInfo.phoneNumbers.label | String | The following are the possible values for this attribute: AssistantPhone, BusinessFax, BusinessPhone, BusinessPhone2, Callback, CarPhone, CompanyMainPhone, HomeFax, HomePhone, HomePhone2, Isdn, MobilePhone, OtherFax, OtherTelephone, Pager, PrimaryPhone, RadioPhone, Telex, TtyTddPhone |
| EWS.ResolvedNames.FullContactInfo.phoneNumbers.phoneNumber | String | The phone number of the contact |
| EWS.ResolvedNames.FullContactInfo.physicalAddresses.city | String | The physical addresses city associated with the contact. |
| EWS.ResolvedNames.FullContactInfo.physicalAddresses.country | String | The physical addresses country associated with the contact. |
| EWS.ResolvedNames.FullContactInfo.physicalAddresses.label | String | The physical addresses label associated with the contact. |
| EWS.ResolvedNames.FullContactInfo.physicalAddresses.state | String | The physical addresses state associated with the contact. |
| EWS.ResolvedNames.FullContactInfo.physicalAddresses.street | String | The physical addresses street associated with the contact. |
| EWS.ResolvedNames.FullContactInfo.physicalAddresses.zipcode | String | The physical addresses zipcode associated with the contact. |
| EWS.ResolvedNames.FullContactInfo.postalAddressIndex | String | Represents the display types for physical addresses. |
| EWS.ResolvedNames.FullContactInfo.sensitivity | String | Indicates the sensitivity level of an item. |
| EWS.ResolvedNames.email_address | String | The primary SMTP address of a mailbox user. |
| EWS.ResolvedNames.mailbox_type | String | The type of mailbox that is represented by the email address. |
| EWS.ResolvedNames.name | String | The name of a mailbox user. |
| EWS.ResolvedNames.routing_type | String | The address type for the mailbox |
Command example
!ews-resolve-name identifier=`example@example.com` full-contact-data=True
Context Example
{
"EWS": {
"ResolvedNames": {
"FullContactInfo": {
"contactSource": "ActiveDirectory",
"culture": "en-US",
"displayName": "ews-2016-test EW2016.",
"emailAddresses": [
"example-sec@example.com",
"example@example.com"
],
"givenName": "ews-2016-test",
"importance": "Normal",
"initials": "EW2016",
"phoneNumbers": [
{
"label": "AssistantPhone",
"phoneNumber": null
},
{
"label": "BusinessFax",
"phoneNumber": null
},
{
"label": "BusinessPhone",
"phoneNumber": null
},
{
"label": "HomePhone",
"phoneNumber": null
},
{
"label": "MobilePhone",
"phoneNumber": null
},
{
"label": "Pager",
"phoneNumber": null
}
],
"physicalAddresses": [
{
"city": null,
"country": null,
"label": "Business",
"state": null,
"street": null,
"zipcode": null
}
],
"postalAddressIndex": "None",
"sensitivity": "Normal"
},
"email_address": "ews-2016-test@lab-demisto.com",
"mailbox_type": "Mailbox",
"name": "ews-2016-test EW2016.",
"routing_type": "SMTP"
}
}
}
Human Readable Output
Resolved Names
primary_email_address name mailbox_type routing_type ews-2016-test@lab-demisto.com ews-2016-test EW2016. Mailbox SMTP
ews-get-out-of-office
Retrieves the out-of-office status for a specified mailbox.
Base Command
ews-get-out-of-office
Input
| Argument Name | Description | Required |
|---|---|---|
| target-mailbox | The mailbox for which to get the out-of-office status. | Required |
Context Output
| Path | Type | Description |
|---|---|---|
| Account.Email.OutOfOffice.state | Unknown | Out-of-office state. Result can be: Enabled, Scheduled, 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. |
Command Example
!ews-get-out-of-office target-mailbox=test@demistodev.onmicrosoft.com
Human Readable Output
end externalAudienc 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"
}
}
}
}
ews-recover-messages
Recovers messages that were soft-deleted.
Base Command
ews-recover-messages
Input
| 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. Default is Inbox. | 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. Possible values are: True, False. | Optional |
Context Output
| 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’. |
Command Example
!ews-recover-messages message-ids=<DFVDFmvsCSCS.com> target-folder-path=Moving target-mailbox=test@demistodev.onmicrosoft.com
Human Readable Output
action itemId messageId recovered AAVCSVS1hN2NkLThmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed33wX3aBwCyyVyFtlsUQZfBJebinpkUAAAa2bUBAACyyVyFtlscfxxd/AAA= DFVDFmvsCSCS.com
Context Example
{
"EWS": {
"Items": {
"action": "recovered",
"itemId": "AAVCSVS1hN2NkLThmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed33wX3aBwCyyVyFtlsUQZfBJebinpkUAAAa2bUBAACyyVyFtlscfxxd/AAA=",
"messageId": "<DFVDFmvsCSCS.com>"
}
}
}
ews-create-folder
Creates a new folder in a specified mailbox.
Base Command
ews-create-folder
Input
| 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. Default is Inbox. | Required |
| target-mailbox | The mailbox in which to create the folder. | Optional |
Context Output
There is no context output for this command.
Command Example
!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
ews-mark-item-as-junk
Marks an item as junk. This is commonly used to block an email address. For more information, see the Microsoft documentation: https://msdn.microsoft.com/en-us/library/office/dn481311(v=exchg.150).aspx
Base Command
ews-mark-item-as-junk
Input
| 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. Possible values are: yes, no. Default is yes. | 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 |
Context Output
There is no context output for this command.
Command Example
!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="
}
}
}
ews-find-folders
Retrieves 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”.
Base Command
ews-find-folders
Input
| Argument Name | Description | Required |
|---|---|---|
| target-mailbox | The mailbox on which to apply the command. | Optional |
| is-public | Whether to find Public Folders. Possible values are: True, False. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| EWS.Folders.name | string | Folder name. |
| EWS.Folders.id | string | Folder ID. |
| EWS.Folders.totalCount | Unknown | Number of items in folder. |
| EWS.Folders.unreadCount | number | Number of unread items in folder |
| EWS.Folders.changeKey | number | Folder change key. |
| EWS.Folders.childrenFolderCount | number | Number of sub-folders. |
Command Example
!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="
}
]
}
}
ews-get-items-from-folder
Retrieves items from a specified folder in a mailbox. The items are order by the item created time, most recent is first.
Base Command
ews-get-items-from-folder
Input
| Argument Name | Description | Required |
|---|---|---|
| folder-path | The folder path from which to get the items. | Required |
| limit | Maximum number of items to return. Default is 100. | Optional |
| target-mailbox | The mailbox to on which to apply the command. | Optional |
| is-public | Whether the folder is a Public Folder. Default is ‘False’. Possible values are: True, False. | Optional |
| get-internal-item | 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”. Possible values are: yes, no. Default is no. | Optional |
Context Output
| 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. |
Command Example
!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": "<html>\r\n<head>\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n<style type=\"text/css\" style=\"display:none;\"><!-- P {margin-top:0;margin-bottom:0;} --></style>\r\n</head>\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</html>\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": "<message_id>",
"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"
}
}
}
ews-get-items
Retrieves items by item ID.
Base Command
ews-get-items
Input
| Argument Name | Description | Required |
|---|---|---|
| item-ids | A CSV list if item IDs. | Required |
| target-mailbox | The mailbox on which to run the command on. | Optional |
Context Output
| 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. |
Command Example
!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.
ews-move-item-between-mailboxes
Moves an item from one mailbox to different mailbox.
Base Command
ews-move-item-between-mailboxes
Input
| 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”. Possible values are: True, False. | Optional |
Context Output
| Path | Type | Description |
|---|---|---|
| EWS.Items.movedToMailbox | string | The mailbox wo 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”. |
Command Example
!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"
}
}
}
ews-get-folder
Retrieves a single folder.
Base Command
ews-get-folder
Input
| 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”. Default is AllItems. | Optional |
| is-public | Whether the folder is a Public Folder. Default is “False”. Possible values are: True, False. | Optional |
Context Output
| 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. |
Command Example
!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="
}
}
}
ews-get-autodiscovery-config
Returns the auto-discovery information. Can be used to manually configure the Exchange Server.
Base Command
ews-get-autodiscovery-config
Input
There are no input arguments for this command.
Context Output
There is no context output for this command.
Command Example
#### Human Readable Output
>|api_version|auth_type|build|service_endpoint|
>|---|---|---|---|
>| Exchange2016 |###|--|<https://outlook.office365.com/EWS/Exchange.asmx>|
### ews-expand-group
***
Expands 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.
#### Base Command
`ews-expand-group`
#### Input
| **Argument Name** | **Description** | **Required** |
| --- | --- | --- |
| email-address | Email address of the group to expand. | Required |
| recursive-expansion | Whether to enable recursive expansion. Default is "False". Possible values are: True, False. Default is False. | Optional |
#### Context Output
There is no context output for this command.
#### Command Example
```!ews-expand-group email-address="TestPublic" recursive-expansion="False"```
#### Human Readable Output
>|displayNam|mailbox|mailboxtype|
>|---|---|---|
>| John Wick|<john@wick.com>|MailBox|
#### Context Example
```json
{
"EWS.ExpandGroup": {
"name": "TestPublic",
"members": [
{
"mailboxType": "Mailbox",
"displayName": "John Wick",
"mailbox": "john@wick.com"
}
]
}
}
ews-mark-items-as-read
Marks items as read or unread.
Base Command
ews-mark-items-as-read
Input
| 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”. Possible values are: read, 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 |
Context Output
| Path | Type | Description |
|---|---|---|
| EWS.Items.action | String | The action that was performed on item. |
| EWS.Items.itemId | String | The ID of the item. |
| EWS.Items.messageId | String | The message ID of the item. |
Command Example
!ews-mark-items-as-read item-ids=AAMkADQ0NFSffU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAABjKMnpkUAAAfxw+jAAA= operation=read target-mailbox=test@demistodev.onmicrosoft.com
Human Readable Output
action itemId messageId mark-as-read AAMkADQ0NFSffU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAABjKMnpkUAAAfxw+jAAA= id
Context Example
{
"EWS": {
"Items": {
"action": "marked-as-read",
"itemId": "AAMkADQ0NFSffU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAABjKMnpkUAAAfxw+jAAA= ",
"messageId": "<message_id>"
}
}
}
ews-get-items-as-eml
Retrieves items by item ID and uploads it’s content as eml file.
Base Command
ews-get-items-as-eml
Input
| 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 |
Context Output
| 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. |
send-mail
Sends an email using EWS.
Base Command
send-mail
Input
| Argument Name | Description | Required |
|---|---|---|
| to | A CSV list of email addresses for the ‘to’ field. | Required |
| cc | A CSV list of email addresses for the ‘cc’ field. | Optional |
| bcc | A CSV list of email addresses for the ‘bcc’ field. | Optional |
| subject | Subject for the email to be sent. | Required |
| replyTo | The email address specified in the ‘reply to’ field. | Optional |
| body | The contents (body) of the email to send. This argument overrides the “htmlBody” argument if the “bodyType” argument is Text. | Optional |
| htmlBody | HTML formatted content (body) of the email to be sent. This argument overrides the “body” argument if the “bodyType” argument is HTML. | Optional |
| bodyType | The message response body type. Possible values are: Text, HTML. Default is Text. | Optional |
| attachIDs | A CSV 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 CSV list of names of attachments to send. Should be the same number of elements as attachIDs. | Optional |
| attachCIDs | A CSV list of CIDs to embed attachments within the email itself. | Optional |
| raw_message | Raw email message from MimeContent type. | Optional |
| from | The email address from which to send mail. | 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.
reply-mail
Replies to an email using EWS.
Command Example
!send-mail body="hello this is a test" 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
Base Command
reply-mail
Input
| Argument Name | Description | Required |
|---|---|---|
| inReplyTo | ID of the item to reply to. | Required |
| to | A CSV list of email addresses for the ‘to’ field. | Required |
| cc | A CSV list of email addresses for the ‘cc’ field. | Optional |
| bcc | A CSV 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 be sent. | Optional |
| htmlBody | HTML formatted content (body) of the email to be sent. This argument overrides the “body” argument. | Optional |
| attachIDs | A CSV 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 CSV list of names of attachments to send. Should be the same number of elements as attachIDs. | Optional |
| attachCIDs | A CSV 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 |
Context Output
There is no context output for this command.
Command Example
!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
Additional Information
EWS Permissions
To perform actions on mailboxes of other users, and to execute searches on the Exchange server, you need specific permissions. For a comparison between Delegate and Impersonation permissions, see the Microsoft documentation
| Permission | Use Case | How to Configure |
|---|---|---|
| Delegated | One-to-one relationship between users. | Read more here. |
| Impersonation | A single account needs to access multiple mailboxes. | Read more here. |
| eDiscovery | A single account needs to access multiple mailboxes. | Read more here. |
| Compliance Search | Perform searches across mailboxes and get an estimate of the results. | Read more here. |
New-Compliance Search
The EWS v2 integration uses remote ps-session to run commands of compliance search as part of Office 365. To check if your account can connect to Office 365 Security & Compliance Center via powershell, check the following steps. New-Compliance search is a long-running task which has no limitation of searched mailboxes and therefore the suggestion is to useOffice 365 Search and Deleteplaybook. New-Compliance search returns statistics of matched content search query and doesn’t return preview of found emails in contrast toews-search-mailboxescommand.
Troubleshooting
For troubleshooting information, see the EWS V2 Troubleshooting.
Configuration parameters
credentials— Email address (required)defaultTargetMailbox— Email address from which to fetch incidents (required)folder— Name of the folder from which to fetch incidents (supports Exchange Folder ID and sub-folders e.g. Inbox/Phishing)isPublicFolder— Public Folderimpersonation— Has impersonation rightsproxy— Use system proxy settingsisFetch— Fetch incidentsfetch_time— First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)markAsRead— Mark fetched emails as readincidentType— Incident typeincidentFetchInterval— Incidents Fetch IntervalewsServer— ┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉ Manual Mode Exchange Server Hostname or IP addressdomainAndUserman— DOMAIN\USERNAME (e.g. DEMISTO.INT\admin)defaultServerVersion— Exchange Server Version (On-Premise only. Supported versions: 2007, 2010, 2010_SP2, 2013, 2013_SP1, 2016, and 2019)insecure— Trust any certificate (not secure)authType— ┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉┉ Advanced Mode Override Authentication Type (NTLM, Basic, or Digest).requestTimeout— Timeout (in seconds) for HTTP requests to Exchange ServermaxFetch— Max incidents per fetchseparate_process— Run as a separate process (protects against memory depletion)legacy_name— Use legacy attachment nameskip_unparsable_emails— Skip unparsable emails during fetch incidents
Commands (24)
-
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. This command requires eDiscovery permissions to the Exchange Server. For more information, see the EWSv2 integration documentation.
-
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-autodiscovery-configReturns the auto-discovery information. Can be used to manually configure the Exchange Server.
-
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 it's content as 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. 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-resolve-nameThis operation verifies aliases and matches display names to the correct mailbox user. It handles one ambiguous name at a time. If there are multiple potential matches, all will be returned, but limited to a maximum of 100 candidates.
-
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.
-
ews-search-mailboxesSearches over multiple mailboxes or all Exchange mailboxes. Use either the mailbox-search-scope command or the email-addresses command to search specific mailboxes. This command requires eDiscovery permissions to the Exchange Server. For more information, see the EWS v2 integration documentation.
-
reply-mailReplies to an email using EWS.
-
send-mailSends an email using EWS.
import email import hashlib from email.policy import SMTP, SMTPUTF8 from io import StringIO from multiprocessing import Process import dateparser # type: ignore import exchangelib from CommonServerPython import * from EWSApiModule import * from exchangelib import ( BASIC, DELEGATE, DIGEST, IMPERSONATION, NTLM, Body, EWSDateTime, EWSTimeZone, FileAttachment, FolderCollection, HTMLBody, ItemAttachment, Version, ) from exchangelib.errors import ( ErrorCannotOpenFileAttachment, ErrorFolderNotFound, ErrorInvalidPropertyRequest, ErrorIrresolvableConflict, ErrorMailboxMoveInProgress, ErrorMailboxStoreUnavailable, ErrorMimeContentConversionFailed, ErrorNameResolutionNoResults, RateLimitError, TransportError, ) from exchangelib.items import Contact, Item, Message from exchangelib.services import EWSService from exchangelib.util import add_xml_child, create_element from exchangelib.version import ( EXCHANGE_2007, EXCHANGE_2010, EXCHANGE_2010_SP2, EXCHANGE_2013, EXCHANGE_2013_SP1, EXCHANGE_2016, EXCHANGE_2019, ) from exchangelib.version import VERSIONS as EXC_VERSIONS from future import utils as future_utils from requests.exceptions import ConnectionError # Exchange2 2019 patch - server dosen't connect with 2019 but with other versions creating an error mismatch (see CIAC-3086), # overriding this function to remove minor version test and remove error throw. # opened bug for exchanglib here https://github.com/ecederstrand/exchangelib/issues/1210 def our_fullname(self): # pragma: no cover for build, api_version, full_name in EXC_VERSIONS: # removed 'or self.build.minor_version != build.minor_version' if self.build and self.build.major_version != build.major_version: continue if self.api_version == api_version: return full_name return None Version.fullname = our_fullname # Ignore warnings print to stdout warnings.filterwarnings("ignore") MNS, TNS = exchangelib.util.MNS, exchangelib.util.TNS # consts VERSIONS = { "2007": EXCHANGE_2007, "2010": EXCHANGE_2010, "2010_SP2": EXCHANGE_2010_SP2, "2013": EXCHANGE_2013, "2013_SP1": EXCHANGE_2013_SP1, "2016": EXCHANGE_2016, "2019": EXCHANGE_2019, } APP_NAME = "EWSv2" ATTACHMENT_ORIGINAL_ITEM_ID = "originalItemId" NEW_ITEM_ID = "newItemId" MESSAGE_ID = "messageId" ITEM_ID = "itemId" MAILBOX = "mailbox" MAILBOX_ID = "mailboxId" FOLDER_ID = "id" MOVED_TO_MAILBOX = "movedToMailbox" MOVED_TO_FOLDER = "movedToFolder" FILE_ATTACHMENT_TYPE = "FileAttachment" ITEM_ATTACHMENT_TYPE = "ItemAttachment" ATTACHMENT_TYPE = "attachmentType" TOIS_PATH = "/root/Top of Information Store/" ENTRY_CONTEXT = "EntryContext" 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}))" ) 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})" LAST_RUN_TIME = "lastRunTime" LAST_RUN_IDS = "ids" LAST_RUN_FOLDER = "folderName" ERROR_COUNTER = "errorCounter" ITEMS_RESULTS_HEADERS = [ "sender", "subject", "hasAttachments", "datetimeReceived", "receivedBy", "author", "toRecipients", "textBody", ] LAST_RUN_IDS_QUEUE_SIZE = 500 # NOTE: Same method used in EWSMailSender # If you are modifying this probably also need to modify in the other file def exchangelib_cleanup(): # pragma: no cover try: exchangelib.close_connections() except Exception as ex: demisto.error(f"Error was found in exchangelib cleanup, ignoring: {ex}") # Prep Functions def parse_auth_type(auth_type: str): # pragma: no cover auth_type = auth_type.lower() if auth_type == "ntlm": return NTLM elif auth_type == "basic": return BASIC elif auth_type == "digest": return DIGEST raise Exception("{} auth method is not supported. Choose one of {}".format(auth_type, "ntlm\\basic\\digest")) # LOGGING log_stream = None log_handler = None # Library loggers whose verbose HTTP transaction dumps must never reach stdout. # Their output (Request/Response headers, Response XML, etc.) corrupts the entry # JSON that the docker runner reads from stdout. We route it only to the # in-memory buffer used for the error-path "Full debug log" feature. EXCHANGELIB_LOGGERS = ("exchangelib", "exchangelib.util") def start_logging(): global log_stream global log_handler logging.raiseExceptions = False # Only emit DEBUG-level output when the instance Log Level is set to debug/verbose. # Otherwise keep a non-verbose level so exchangelib's HTTP transaction dumps are not produced. log_level = logging.DEBUG if is_debug_mode() else logging.INFO 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(log_level) # Ensure exchangelib's logging-based output can NEVER reach stdout: attach only the # in-memory buffer handler and stop propagation to the root/stdout handlers. for logger_name in EXCHANGELIB_LOGGERS: lib_logger = logging.getLogger(logger_name) lib_logger.addHandler(log_handler) lib_logger.setLevel(log_level) lib_logger.propagate = False else: logging.getLogger().setLevel(log_level) for logger_name in EXCHANGELIB_LOGGERS: logging.getLogger(logger_name).setLevel(log_level) # Exchange 2010 Fixes def fix_2010(client: EWSClient): # pragma: no cover version = client.server_build if client.server_build else get_on_prem_build(client.version) if version <= EXCHANGE_2010_SP2: for m in ( Item, Message, exchangelib.items.CalendarItem, exchangelib.items.Contact, exchangelib.items.DistributionList, exchangelib.items.PostItem, exchangelib.items.Task, exchangelib.items.MeetingRequest, exchangelib.items.MeetingResponse, exchangelib.items.MeetingCancellation, ): for i, f in enumerate(m.FIELDS): if f.name == "text_body": m.FIELDS.pop(i) break for m in (exchangelib.Folder, exchangelib.folders.Inbox): for i, f in enumerate(m.FIELDS): if f.name == "unread_count": m.FIELDS.pop(i) break def repr1(self): return self.__class__.__name__ + repr( (self.root, self.name, self.total_count, self.child_folder_count, self.folder_class, self.id, self.changekey) ) def repr2(self): return self.__class__.__name__ + repr( (self.root, self.name, self.total_count, self.child_folder_count, self.folder_class, self.changekey) ) def repr3(self): return self.__class__.__name__ + repr( (self.account, "[self]", self.name, self.total_count, self.child_folder_count, self.folder_class, self.changekey) ) exchangelib.Folder.__repr__ = repr1 exchangelib.folders.Inbox.__repr__ = exchangelib.folders.JunkEmail.__repr__ = repr2 exchangelib.folders.Root.__repr__ = repr3 start_logging() def str_to_unicode(obj): # pragma: no cover if isinstance(obj, dict): obj = {k: str_to_unicode(v) for k, v in list(obj.items())} elif isinstance(obj, list): obj = [str_to_unicode(k) for k in obj] elif isinstance(obj, str): obj = obj.encode("utf-8") return obj def get_time_zone() -> EWSTimeZone | None: """get the XSOAR user time zone :return: returns an ``EWSTimeZone`` if TZ available or ``None`` if not :rtype: ``Optional[EWSTimeZone]`` """ time_zone = demisto.callingContext.get("context", {}).get("User", {}).get("timeZone", None) if time_zone: time_zone = EWSTimeZone(time_zone) return time_zone def get_attachment_name(attachment_name, content_id="", is_inline=False, attachment_subject=""): # pragma: no cover demisto.debug( f"get_attachment_name called with attachment_name='{attachment_name}', content_id='{content_id}', " f"is_inline={is_inline}, attachment_subject='{attachment_subject}'" ) legacy_name = argToBoolean(demisto.params().get("legacy_name", False)) 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" return f"{content_id}-attachmentName-{attachment_name}" if not attachment_name and attachment_subject: return attachment_subject if not attachment_name and not attachment_subject: return "demisto_untitled_attachment" return attachment_name def send_email_to_mailbox( # pragma: no cover account, to, subject, body, body_type, bcc, cc, reply_to, handle_inline_image: bool = True, html_body=None, attachments=None, raw_message=None, from_address=None, ): """ Send an email to a mailbox. Args: body_type: type of the body. Can be 'html' or 'text' or None. account (Account): account from which to send an email. to (list[str]): a list of emails to send an email. subject (str): subject of the mail. body (str): body of the email. reply_to (list[str]): list of emails of which to reply to from the sent email. bcc (list[str]): list of email addresses for the 'bcc' field. cc (list[str]): list of email addresses for the 'cc' field. html_body (str): HTML formatted content (body) of the email to be sent. This argument overrides the "body" argument. attachments (list[str]): list of names of attachments to send. raw_message (str): Raw email message from MimeContent type. from_address (str): the email address from which to reply. """ if not attachments: attachments = [] message_body, inline_attachments = get_message_for_body_type(body, body_type, html_body, handle_inline_image) attachments += inline_attachments m = Message( account=account, mime_content=raw_message.encode("UTF-8") if raw_message else None, folder=account.sent, cc_recipients=cc, bcc_recipients=bcc, subject=subject, body=message_body, to_recipients=to, reply_to=reply_to, author=from_address, ) if account.protocol.version.build <= EXCHANGE_2010_SP2: m.save() for attachment in attachments: m.attach(attachment) m.send() else: for attachment in attachments: m.attach(attachment) m.send_and_save() return m def get_message_for_body_type(body, body_type, html_body, handle_inline_image: bool): """ Compatibility with Data Collection - where body_type is not provided, we will use the html_body if it exists. Compatibility with 'send-mail' command - where body_type should be provided, we will use the body_type to decide. Args: body_type: type of the body. Can be 'html' or 'text' or None. body: plain text body. html_body: HTML formatted content (body) of the email to be sent. Returns: Body: the body of the message. """ demisto.debug(f"get_message_for_body_type: Received {body_type=}, {handle_inline_image=}") attachments: list = [] if html_body and handle_inline_image: html_body, attachments = handle_html(html_body) demisto.debug(f"get_message_for_body_type: Processed HTML body with {len(attachments)} attachments") if body_type is None: # When called from a data collection task. return (HTMLBody(html_body) if html_body else Body(body)), attachments if body_type.lower() == "html" and html_body: # When called from 'send-mail' command. return HTMLBody(html_body), attachments return Body(body) if (body or not html_body) else HTMLBody(html_body), attachments class SearchMailboxes(EWSService): def __init__(self, protocol, limit): self.limit = limit super().__init__(protocol) SERVICE_NAME = "SearchMailboxes" element_container_name = f"{{{MNS}}}SearchMailboxesResult/{{{TNS}}}Items" @staticmethod def parse_element(element): # pragma: no cover to_recipients = element.find(f"{{{TNS}}}ToRecipients") if to_recipients: to_recipients = [x.text if x is not None else None for x in to_recipients] result = { ITEM_ID: element.find(f"{{{TNS}}}Id").attrib["Id"] if element.find(f"{{{TNS}}}Id") is not None else None, MAILBOX: element.find(f"{{{TNS}}}Mailbox/{{{TNS}}}PrimarySmtpAddress").text if element.find(f"{{{TNS}}}Mailbox/{{{TNS}}}PrimarySmtpAddress") is not None else None, "subject": element.find(f"{{{TNS}}}Subject").text if element.find(f"{{{TNS}}}Subject") is not None else None, "toRecipients": to_recipients, "sender": element.find(f"{{{TNS}}}Sender").text if element.find(f"{{{TNS}}}Sender") is not None else None, "hasAttachments": element.find(f"{{{TNS}}}HasAttachment").text if element.find(f"{{{TNS}}}HasAttachment") is not None else None, "datetimeSent": element.find(f"{{{TNS}}}SentTime").text if element.find(f"{{{TNS}}}SentTime") is not None else None, "datetimeReceived": element.find(f"{{{TNS}}}ReceivedTime").text if element.find(f"{{{TNS}}}ReceivedTime") is not None else None, } return result def call(self, query, mailboxes): # pragma: no cover if self.protocol.version.build < EXCHANGE_2013: raise NotImplementedError(f"{self.SERVICE_NAME} is only supported for Exchange 2013 servers and later") elements = list(self._get_elements(payload=self.get_payload(query, mailboxes))) return [self.parse_element(x) for x in elements] def get_payload(self, query, mailboxes): # pragma: no cover def get_mailbox_search_scope(mailbox_id): mailbox_search_scope = create_element("t:MailboxSearchScope") add_xml_child(mailbox_search_scope, "t:Mailbox", mailbox_id) add_xml_child(mailbox_search_scope, "t:SearchScope", "All") return mailbox_search_scope mailbox_query_element = create_element("t:MailboxQuery") add_xml_child(mailbox_query_element, "t:Query", query) mailboxes_scopes = [] for mailbox in mailboxes: mailboxes_scopes.append(get_mailbox_search_scope(mailbox)) add_xml_child(mailbox_query_element, "t:MailboxSearchScopes", mailboxes_scopes) element = create_element(f"m:{self.SERVICE_NAME}") add_xml_child(element, "m:SearchQueries", mailbox_query_element) add_xml_child(element, "m:ResultType", "PreviewOnly") add_xml_child(element, "m:PageSize", str(self.limit)) return element def search_mailboxes(client: EWSClient, args: dict): # pragma: no cover """ Search mailboxes for items matching the given filter. Args: client (EWSClient): The EWS client object. args (dict): The command arguments. Returns: dict: A dictionary containing the search results. Raises: Exception: If both mailbox_search_scope and email_addresses are provided, or if no searchable mailboxes are found. """ filter = args.get("filter", "") limit = args.get("limit", 100) mailbox_search_scope = args.get("mailbox_search_scope") email_addresses = args.get("email_addresses") mailbox_ids = [] limit_argument = arg_to_number(limit) if not limit_argument: raise DemistoException(f"Invalid limit value: {limit}. Please provide a valid integer.") if mailbox_search_scope is not None and email_addresses is not None: raise Exception("Use one of the arguments - mailbox-search-scope or email-addresses, not both") if email_addresses: email_addresses = email_addresses.split(",") all_mailboxes = GetSearchableMailboxes(protocol=client.get_protocol()).call() for email_address in email_addresses: for mailbox in all_mailboxes: addr = mailbox.get(MAILBOX, None) if addr and email_address.lower() == addr.lower(): mailbox_ids.append(mailbox[MAILBOX_ID]) if len(mailbox_ids) == 0: raise Exception("No searchable mailboxes were found for the provided email addresses.") elif mailbox_search_scope: mailbox_ids = mailbox_search_scope if type(mailbox_search_scope) is list else [mailbox_search_scope] else: all_mailboxes = GetSearchableMailboxes(protocol=client.get_protocol()).call() mailbox_ids = [x[MAILBOX_ID] for x in all_mailboxes if x.get(MAILBOX_ID, None)] try: search_results = SearchMailboxes(protocol=client.get_protocol(), limit=limit_argument).call(filter, mailbox_ids) except TransportError as e: if "ItemCount>0<" in str(e): return "No results for search query: " + filter else: raise e return get_entry_for_object("Search mailboxes results", CONTEXT_UPDATE_EWS_ITEM, search_results) def get_last_run(client: EWSClient): last_run = demisto.getLastRun() 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: []} if LAST_RUN_TIME in last_run and last_run[LAST_RUN_TIME] is not None: last_run[LAST_RUN_TIME] = EWSDateTime.from_string(last_run[LAST_RUN_TIME]) # In case we have existing last_run data if last_run.get(LAST_RUN_IDS) is None: last_run[LAST_RUN_IDS] = [] return last_run def fetch_last_emails( client: EWSClient, folder_name="Inbox", since_datetime=None, exclude_ids=None, fetch_all_history=False, fetch_time="10 minutes", ): account = client.get_account(client.account_email) qs = client.get_folder_by_path(folder_name, account, is_public=client.is_public_folder) demisto.debug(f"since_datetime: {since_datetime}") if since_datetime: qs = qs.filter(datetime_received__gte=since_datetime) else: if not fetch_all_history: tz = EWSTimeZone("UTC") first_fetch_datetime = dateparser.parse(fetch_time) if not first_fetch_datetime: raise DemistoException("Failed to parse first last run time") first_fetch_ews_datetime = first_fetch_datetime.astimezone(tz) qs = qs.filter(datetime_received__gte=first_fetch_ews_datetime) qs = qs.filter().only(*[x.name for x in Message.FIELDS]) qs = qs.filter().order_by("datetime_received") result = [] exclude_ids = exclude_ids if exclude_ids else set() demisto.debug(f"Exclude ID list: {exclude_ids}") for item in qs: try: demisto.debug( f"Looking on subject={item.subject}, " f"message_id={item.message_id}, " f"created={item.datetime_created}, " f"received={item.datetime_received}" ) if isinstance(item, Message) and item.message_id not in exclude_ids: result.append(item) demisto.debug(f"Appending {item.subject}, {item.message_id}.") if len(result) >= client.max_fetch: break except ValueError as exc: future_utils.raise_from( ValueError("Got an error when pulling incidents. You might be using the wrong exchange version."), exc ) raise exc except ErrorMimeContentConversionFailed as exc: demisto.debug(f"Encountered an ErrorMimeContentConversionFailed error object while iterating: {exc}.\ Continuing to next item.") continue except AttributeError as exc: demisto.debug(f"Encountered an Attribute error object while iterating: {exc}.\ Continuing to next item.") demisto.debug(f"EWS V2 - Got total of {len(result)} from ews query. ") return result def keys_to_camel_case(value): def str_to_camel_case(snake_str): # Add condition as Email object arrived in list and raised error if not isinstance(snake_str, str): return 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 [keys_to_camel_case(v) for v in 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 email_ec(item): # pragma: no cover 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": {} if not item.headers else {header.name: header.value for header in item.headers}, } def parse_object_as_dict_with_serialized_items(object): raw_dict = {} if object is not None: for field in object.FIELDS: try: v = getattr(object, field.name, None) if v is not None: json.dumps(v) raw_dict[field.name] = v except (TypeError, OverflowError): demisto.debug(f"Data in field {field.name} is not serilizable, skipped field value is \n{v}\n") continue return raw_dict def parse_item_as_dict(item, email_address=None, camel_case=False, compact_fields=False): # pragma: no cover def parse_object_as_dict(object): raw_dict = {} if object is not None: for field in object.FIELDS: field_val = getattr(object, field.name, None) try: json.dumps(field_val) except TypeError: field_val = parse_object_as_dict(field_val) raw_dict[field.name] = field_val 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 = parse_object_as_dict_with_serialized_items(item) 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 type(value) is list: raw_dict[dict_field] = [parse_object_as_dict(x) for x in value] 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 raw_dict["item_id"] = getattr(item, "id", None) raw_dict["id"] = getattr(item, "id", None) if compact_fields: new_dict = {} 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", ] fields_list.append("item_id") 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 cast_mime_item_to_message(item): mime_content = item.mime_content email_policy = SMTP if mime_content.isascii() else SMTPUTF8 if isinstance(mime_content, bytes): demisto.debug("Returning message as bytes") return email.message_from_bytes(mime_content, policy=email_policy) # type: ignore[arg-type] else: demisto.debug("Returning message as string") return email.message_from_string(mime_content, policy=email_policy) # type: ignore[arg-type] def parse_incident_from_item(item, is_fetch, mark_as_read): # pragma: no cover incident = {} labels = [] try: 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_created.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}) # 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"] = [] for attachment in item.attachments: if attachment is not None: attachment.parent_item = item 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, ) 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 ErrorCannotOpenFileAttachment as e: if str(e) != "The attachment could not be opened.": raise demisto.error(f"Skipped attachment: {attachment.name} - {e}") continue else: # other item attachment label_attachment_type = "attachmentItems" label_attachment_id_type = "attachmentItemsId" formatted_message: str | bytes # save the attachment if hasattr(attachment, "item") and attachment.item.mime_content: # Some items arrive with bytes attachemnt attached_email = cast_mime_item_to_message(attachment.item) if attachment.item.headers: attached_email_headers = [] for h, v in list(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)) for header in attachment.item.headers: if ( header.name.lower(), header.value, ) not in attached_email_headers and header.name.lower() != "content-type": try: attached_email.add_header(header.name, header.value) except ValueError as err: if "There may be at most" not in str(err): raise err formatted_message = get_formatted_message(attached_email) file_result = fileResult( get_attachment_name( attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline, attachment_subject=attachment.item.subject, ) + ".eml", formatted_message, ) 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, content_id=attachment.content_id, is_inline=attachment.is_inline, attachment_subject=attachment.item.subject, ) + ".eml", } ) else: incident["attachment"].append( { "name": get_attachment_name( attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline, attachment_subject=attachment.item.subject, ) + ".eml", } ) labels.append( { "type": label_attachment_type, "value": get_attachment_name( attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline, attachment_subject="" if isinstance(attachment, FileAttachment) else attachment.item.subject, ), } ) labels.append({"type": label_attachment_id_type, "value": attachment.attachment_id.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)}) # fetch history incident["dbotMirrorId"] = 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}) if mark_as_read and is_fetch: item.is_read = True try: item.save() except ErrorIrresolvableConflict: time.sleep(0.5) item.save() except ValueError as e: if item.subject and len(item.subject) > 255: demisto.debug( "Length of message subject is greater than 255, item.save could not handle it, cutting the subject." ) sub_subject = f"Length of subject greater than 255 characters. Partial subject: {item.subject[:180]}" item.subject = sub_subject item.save() else: raise e incident["labels"] = labels incident["rawJSON"] = json.dumps(parse_item_as_dict(item, None), ensure_ascii=False) except Exception as e: if "Message is not decoded yet" in str(e): demisto.debug("EWS v2 - Skipped a protected message") return None else: raise e return incident def get_formatted_message(attached_email) -> str | bytes: try: demisto.debug("Formatting attached mail as string") return attached_email.as_string() except Exception as e: demisto.info(f"Could not parse attached mail as string, trying as bytes.\n{e}") try: return attached_email.as_bytes() except Exception as e: # Python 3.12 hardened the email library: as_string()/as_bytes() raise # email.errors.HeaderWriteError for headers containing embedded newlines # (policy.verify_generated_headers defaults to True). Retry with a policy # that does not verify generated headers to preserve the previous lenient # behavior of returning the message as bytes even when a header is malformed. demisto.info(f"Could not parse attached mail as bytes, retrying with a non-verifying policy.\n{e}") try: current_policy = getattr(attached_email, "policy", SMTP) lenient_policy = current_policy.clone(verify_generated_headers=False) return attached_email.as_bytes(policy=lenient_policy) except Exception as e: demisto.error(f"Could not parse attached mail as bytes, {e}") raise Exception(f"Could not format message, {e}") def fetch_emails_as_incidents( client: EWSClient, skip_unparsable_emails: bool = False, fetch_all_history=False, fetch_time="10 minutes" ) -> list[dict]: last_run = get_last_run(client) excluded_ids = set(last_run.get(LAST_RUN_IDS, [])) try: last_emails = fetch_last_emails( client, client.folder_name, last_run.get(LAST_RUN_TIME), excluded_ids, fetch_all_history, fetch_time ) incidents = [] incident = {} # type: Dict[Any, Any] current_fetch_ids = set() last_incident_run_time = None for item in last_emails: try: if item.message_id: current_fetch_ids.add(item.message_id) incident = parse_incident_from_item(item, True, client.mark_as_read) demisto.debug(f"Parsed incident: {item.message_id}") if incident: incidents.append(incident) last_incident_run_time = item.datetime_received demisto.debug(f"Appended incident: {item.message_id}") if len(incidents) >= client.max_fetch: break 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"EWS V2 - ending fetch - got {len(incidents)} incidents.") last_fetch_time = last_run.get(LAST_RUN_TIME) last_incident_run_time = last_incident_run_time if last_incident_run_time else last_fetch_time # making sure both last fetch time and the time of last 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() debug_msg = "#### last_incident_time: {}({}). last_fetch_time: {}({}) ####" demisto.debug( debug_msg.format(last_incident_run_time, type(last_incident_run_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 = current_fetch_ids | excluded_ids new_last_run = { LAST_RUN_TIME: last_incident_run_time, LAST_RUN_FOLDER: client.folder_name, LAST_RUN_IDS: list(ids), ERROR_COUNTER: 0, } demisto.setLastRun(new_last_run) 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 get_entry_for_file_attachment(item_id, attachment): # pragma: no cover entry = fileResult( get_attachment_name(attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline), attachment.content, ) ec = {CONTEXT_UPDATE_EWS_ITEM_FOR_ATTACHMENT + CONTEXT_UPDATE_FILE_ATTACHMENT: parse_attachment_as_dict(item_id, attachment)} entry[ENTRY_CONTEXT] = filter_dict_null(ec) return entry def parse_attachment_as_dict(item_id, attachment): # pragma: no cover try: # if this is a file attachment or a non-empty email attachment if isinstance(attachment, FileAttachment) or hasattr(attachment, "item"): attachment_content = attachment.content if isinstance(attachment, FileAttachment) else attachment.item.mime_content 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, attachment_subject="" if isinstance(attachment, FileAttachment) else attachment.item.subject, ), "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, } # If this is an empty email attachment else: 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, attachment_subject="" if isinstance(attachment, FileAttachment) else attachment.item.subject, ), "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, ErrorCannotOpenFileAttachment) as e: if str(e) not in ("must be string or buffer, not None", "The attachment could not be opened."): raise demisto.debug(f"Add attachment info to context, without the content. Error: {e!s}") 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, attachment_subject="" if isinstance(attachment, FileAttachment) else attachment.item.subject, ), "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 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}", "{get_attachment_name(attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline, attachment_subject=attachment.item.subject)}"' # noqa: E501 return get_entry_for_object(title, CONTEXT_UPDATE_EWS_ITEM_FOR_ATTACHMENT + CONTEXT_UPDATE_ITEM_ATTACHMENT, dict_result) def fetch_attachments_for_message(client: EWSClient, args: dict) -> list: # pragma: no cover 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) account = client.get_account(target_mailbox or client.account_email) attachment_ids = argToList(attachment_ids) 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, ErrorCannotOpenFileAttachment) as e: if str(e) not in ("must be string or buffer, not None", "The attachment could not be opened."): raise demisto.debug(f"Skipping attachment '{attachment.name}', Error: {e}") else: entries.append(get_entry_for_item_attachment(item_id, attachment, account.primary_smtp_address)) if attachment.item.mime_content: attached_email = cast_mime_item_to_message(attachment.item) entries.append( fileResult( get_attachment_name( attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline, attachment_subject=attachment.item.subject, ) + ".eml", get_formatted_message(attached_email), ) ) return entries def prepare_args(d): # pragma: no cover d = {k.replace("-", "_"): v for k, v in list(d.items())} if "is_public" in d: d["is_public"] = d["is_public"] == "True" return d def get_limited_number_of_messages_from_qs(qs, limit): # pragma: no cover count = 0 results = [] for item in qs: if count == limit: break if isinstance(item, Message): count += 1 results.append(item) return results def search_items_in_mailbox(client: EWSClient, args: dict): # pragma: no cover 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") surround_id_with_angle_brackets = argToBoolean(args.get("surround_id_with_angle_brackets", True)) if not query and not message_id: return_error("Missing required argument. Provide query or message-id") if argToBoolean(surround_id_with_angle_brackets) and message_id and message_id[0] != "<" and message_id[-1] != ">": message_id = f"<{message_id}>" account = client.get_account(target_mailbox or client.account_email) 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 = FolderCollection(account=account, folders=[account.root.tois]).find_folders() # 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} # type: ignore 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: # we show id as 'itemId' for BC restricted_fields.remove("id") restricted_fields.add("itemId") 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 ] return get_entry_for_object( "Searched items", CONTEXT_UPDATE_EWS_ITEM, searched_items_result, headers=ITEMS_RESULTS_HEADERS if selected_all_fields else None, ) 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 = {attr: getattr(phone_number, attr, None) for attr in ["label", "phone_number"]} return result if result.get("phone_number") else {} def is_jsonable(x): try: json.dumps(x) return True except Exception: return False def parse_contact(contact): contact_dict = parse_object_as_dict_with_serialized_items(contact) for k in contact_dict: v = contact_dict[k] if isinstance(v, EWSDateTime): contact_dict[k] = v.ewsformat() # pylint: disable=E4702 contact_dict["id"] = contact.id 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"] = [elt for elt in map(parse_phone_number, contact.phone_numbers) if elt] 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 contact_dict.items() if (v and is_jsonable(v))} return contact_dict def get_contacts(client: EWSClient, args: dict): # pragma: no cover limit = args.get("limit", 100) target_mailbox = args.get("target_mailbox") account = client.get_account(target_mailbox or client.account_email) contacts = [] for contact in account.contacts.all()[: int(limit)]: # pylint: disable=E1101 contact = parse_contact(contact) contact["originMailbox"] = target_mailbox contacts.append(contact) return get_entry_for_object( f"Email contacts for {target_mailbox or client.account_email}", "Account.Email(val.Address == obj.originMailbox).EwsContacts", contacts, ) def find_folders(client: EWSClient, args: dict): # pragma: no cover target_mailbox = args.get("target_mailbox") is_public = args.get("is_public") account = client.get_account(target_mailbox or client.account_email) root = account.public_folders_root if is_public else account.root.tois # pylint: disable=E1101 root_collection = FolderCollection(account=account, folders=[root]) folders = [] for f in root_collection.find_folders(): # pylint: disable=E1101 folder = folder_to_context_entry(f) folders.append(folder) readable_output = root.tree() # pylint: disable=E1101 return { "Type": entryTypes["note"], "Contents": folders, "ContentsFormat": formats["json"], "ReadableContentsFormat": formats["markdown"], "HumanReadable": readable_output, ENTRY_CONTEXT: {"EWS.Folders(val.id == obj.id)": folders}, } def get_items_from_folder(client: EWSClient, args: dict): # pragma: no cover 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 or client.account_email) 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 attachment is not None: attachment.parent_item = item 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"] return get_entry_for_object("Items in folder " + folder_path, CONTEXT_UPDATE_EWS_ITEM, items_result, headers=hm_headers) def get_items(client: EWSClient, args: dict): # pragma: no cover item_ids = args.get("item_ids", "") target_mailbox = args.get("target_mailbox") account = client.get_account(target_mailbox or client.account_email) item_ids = argToList(item_ids) 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, False, client.mark_as_read) for x in items] items_to_context = [parse_item_as_dict(x, account.primary_smtp_address, True, True) for x in items] return { "Type": entryTypes["note"], "Contents": items_as_incidents, "ContentsFormat": formats["json"], "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown("Get items", items_to_context, ITEMS_RESULTS_HEADERS), ENTRY_CONTEXT: { CONTEXT_UPDATE_EWS_ITEM: items_to_context, "Email": [email_ec(item) for item in items], }, } def get_autodiscovery_config(): # pragma: no cover config_dict = demisto.getIntegrationContext() return { "Type": entryTypes["note"], "Contents": config_dict, "ContentsFormat": formats["json"], "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown("Auto-Discovery Exchange Configuration", config_dict), } def format_identifier(identifier): """ Exchangelib has a default smtp routing type. If there's no given routingtype, add explicitly so that exchangelib can be searched by secondary email addresses without making cusomter add it manually """ return f"smtp:{identifier}" if "@" in identifier and ":" not in identifier else identifier def resolve_name_command(client: EWSClient, args): # pragma: no cover protocol = client.get_protocol() unresolved_entry = format_identifier(args["identifier"]) full_contact_data = argToBoolean(args.get("full_contact_data", True)) resolved_names = protocol.resolve_names([unresolved_entry], return_full_contact_data=full_contact_data, search_scope="") demisto.debug(f"{len(resolved_names)=}") output = [] for rn in resolved_names: if isinstance(rn, ErrorNameResolutionNoResults): demisto.info(f"got ErrorNameResolutionNoResults error, {rn!s}") return "No results were found." elif isinstance(rn, tuple): mail, contact = rn else: mail, contact = rn, None mail_context = parse_item_as_dict(mail) if contact: mail_context["FullContactInfo"] = parse_contact(contact) output.append(mail_context) return get_entry_for_object( "Resolved Names", "EWS.ResolvedNames(val.email_address && val.email_address == obj.email_address)", remove_empty_elements(output), # noqa: F405 headers=["primary_email_address", "name", "mailbox_type", "routing_type"], hr_header_changes={"email_address": "primary_email_address"}, ) def get_item_as_eml(client: EWSClient, args: dict): # pragma: no cover item_id = args.get("item_id", "") target_mailbox = args.get("target_mailbox") account = client.get_account(target_mailbox or client.account_email) item = client.get_item_from_mailbox(account, item_id) if item.mime_content: # came across an item with bytes attachemnt which failed in the source code, added this to keep functionality email_content = cast_mime_item_to_message(item) if item.headers: attached_email_headers = [] for h, v in list(email_content.items()): if not isinstance(v, str): try: v = str(v) except: # noqa: E722 demisto.debug(f'cannot parse the header "{h}"') v = " ".join(map(str.strip, v.split("\r\n"))) attached_email_headers.append((h.lower(), v)) 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" file_result = fileResult(eml_name + ".eml", get_formatted_message(email_content)) file_result = file_result if file_result else "Failed uploading eml file to war room" return file_result return None def collect_manual_attachments(manual_attach_obj): # pragma: no cover attachments = [] for attachment in manual_attach_obj: res = demisto.getFilePath(os.path.basename(attachment["RealFileName"])) file_path = res["path"] with open(file_path, "rb") as f: attachments.append(FileAttachment(content=f.read(), name=attachment["FileName"])) return attachments def process_attachments(attach_cids="", attach_ids="", attach_names="", manual_attach_obj=None): # pragma: no cover if manual_attach_obj is None: manual_attach_obj = [] file_entries_for_attachments = [] # type: list attachments_names = [] # type: list if attach_ids: file_entries_for_attachments = attach_ids if isinstance(attach_ids, list) else attach_ids.split(",") if attach_names: attachments_names = attach_names if isinstance(attach_names, list) else attach_names.split(",") else: for att_id in file_entries_for_attachments: att_name = demisto.getFilePath(att_id)["name"] if isinstance(att_name, list): att_name = att_name[0] attachments_names.append(att_name) if len(file_entries_for_attachments) != len(attachments_names): raise Exception("attachIDs and attachNames lists should be the same length") attachments = collect_manual_attachments(manual_attach_obj) if attach_cids: file_entries_for_attachments_inline = attach_cids if isinstance(attach_cids, list) else attach_cids.split(",") for att_id_inline in file_entries_for_attachments_inline: try: file_info = demisto.getFilePath(att_id_inline) except Exception as ex: demisto.info(f"EWS error from getFilePath: {ex}") raise Exception(f"entry {att_id_inline} does not contain a file") att_name_inline = file_info["name"] with open(file_info["path"], "rb") as f: attachments.append( FileAttachment(content=f.read(), name=att_name_inline, is_inline=True, content_id=att_name_inline) ) for i in range(len(file_entries_for_attachments)): entry_id = file_entries_for_attachments[i] attachment_name = attachments_names[i] try: res = demisto.getFilePath(entry_id) except Exception as ex: raise Exception(f"entry {entry_id} does not contain a file: {ex!s}") file_path = res["path"] with open(file_path, "rb") as f: attachments.append(FileAttachment(content=f.read(), name=attachment_name)) return attachments, attachments_names def get_none_empty_addresses(addresses_ls): return [adress for adress in addresses_ls if adress] def send_email(client: EWSClient, args): time_zone = get_time_zone() account = client.get_account(target_mailbox=client.account_email, time_zone=time_zone) bcc = get_none_empty_addresses(argToList(args.get("bcc"))) cc = get_none_empty_addresses(argToList(args.get("cc"))) to = get_none_empty_addresses(argToList(args.get("to"))) replyTo = get_none_empty_addresses(argToList(args.get("replyTo"))) render_body = argToBoolean(args.get("renderBody") or False) handle_inline_image: bool = argToBoolean(args.get("handle_inline_image", True)) subject = args.get("subject") subject = subject[:252] + "..." if len(subject) > 255 else subject attachments, attachments_names = process_attachments( args.get("attachCIDs", ""), args.get("attachIDs", ""), args.get("attachNames", ""), args.get("manualAttachObj") or [] ) body_type = args.get("bodyType", args.get("body_type")) send_email_to_mailbox( account=account, to=to, subject=subject, body=args.get("body"), body_type=body_type, bcc=bcc, cc=cc, reply_to=replyTo, handle_inline_image=handle_inline_image, html_body=args.get("htmlBody"), attachments=attachments, raw_message=args.get("raw_message"), from_address=args.get("from"), ) result_object = { "from": args.get("from") or account.primary_smtp_address, "to": to, "subject": subject, "attachments": attachments_names, } results = [ { "Type": entryTypes["note"], "Contents": result_object, "ContentsFormat": formats["json"], "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown("Sent email", result_object), } ] if render_body: results.append({"Type": entryTypes["note"], "ContentsFormat": formats["html"], "Contents": args.get("htmlBody")}) return results def reply_email(client: EWSClient, args: dict): # pragma: no cover time_zone = get_time_zone() account = client.get_account(target_mailbox=client.account_email, time_zone=time_zone) bcc = argToList(args.get("bcc")) cc = argToList(args.get("cc")) to = argToList(args.get("to")) handle_inline_image: bool = argToBoolean(args.get("handle_inline_image", True)) from_mailbox = args.get("from") subject = args.get("subject", "") subject = subject[:252] + "..." if subject and len(subject) > 255 else subject attachments, attachments_names = process_attachments( args.get("attachCIDs", ""), args.get("attachIDs", ""), args.get("attachNames", ""), args.get("manualAttachObj") or [] ) client.reply_email( in_reply_to=args.get("inReplyTo", ""), to=to, body=args.get("body", ""), subject=subject, bcc=bcc, cc=cc, html_body=args.get("htmlBody"), attachments=attachments, from_mailbox=from_mailbox, account=account, handle_inline_image=handle_inline_image, ) result_object = { "from": from_mailbox or account.primary_smtp_address, "to": to, "subject": subject, "attachments": attachments_names, } return { "Type": entryTypes["note"], "Contents": result_object, "ContentsFormat": formats["json"], "ReadableContentsFormat": formats["markdown"], "HumanReadable": tableToMarkdown("Sent email", result_object), } def test_module(client: EWSClient): # pragma: no cover try: account = client.get_account(client.account_email) folder = client.get_folder_by_path(client.folder_name, account, client.is_public_folder) if not folder.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" ) 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)) demisto.results("ok") def get_client_from_params(params: dict): """ Constructs an EWSClient instance from the provided integration parameters. :param params: The dictionary received from demisto.params() :return: An EWSClient instance for interacting with the Exchange server. """ username = params.get("credentials", {}).get("identifier") password = params.get("credentials", {}).get("password") access_type = IMPERSONATION if params.get("impersonation", False) else DELEGATE account_email = params.get("defaultTargetMailbox", "") max_fetch = min(50, int(params.get("maxFetch", 50))) ews_server = params.get("ewsServer", "") auth_type = params.get("authType", "") if auth_type: auth_type = parse_auth_type(auth_type) version = params.get("defaultServerVersion", "") folder = params.get("folder", "Inbox") is_public_folder = params.get("isPublicFolder", False) request_timeout = int(params.get("requestTimeout", 120)) mark_as_read = params.get("markAsRead", False) manual_username = params.get("domainAndUserman", "") insecure = params.get("insecure", True) if ews_server and manual_username: username = manual_username return EWSClient( client_id=username, client_secret=password, access_type=access_type, default_target_mailbox=account_email, max_fetch=max_fetch, ews_server=ews_server, auth_type=auth_type, version=version, folder=folder, is_public_folder=is_public_folder, request_timeout=request_timeout, mark_as_read=mark_as_read, incident_filter=IncidentFilter.RECEIVED_FILTER, app_name=APP_NAME, insecure=insecure, ) def sub_main(): # pragma: no cover params = demisto.params() args = prepare_args(demisto.args()) client = get_client_from_params(params) skip_unparsable_emails: bool = argToBoolean(params.get("skip_unparsable_emails", False)) fetch_all_history = argToBoolean(demisto.params().get("fetchAllHistory", False)) fetch_time = demisto.params().get("fetch_time") or "10 minutes" is_test_module = False fix_2010(client) try: if demisto.command() == "test-module": is_test_module = True test_module(client) elif demisto.command() == "fetch-incidents": incidents = fetch_emails_as_incidents(client, skip_unparsable_emails, fetch_all_history, fetch_time) demisto.incidents(incidents) elif demisto.command() == "ews-get-attachment": return_results(fetch_attachments_for_message(client, args)) elif demisto.command() == "ews-delete-attachment": return_results(delete_attachments_for_message(client, args)) elif demisto.command() == "ews-get-searchable-mailboxes": return_results(get_searchable_mailboxes(client, args)) elif demisto.command() == "ews-search-mailboxes": return_results(search_mailboxes(client, args)) elif demisto.command() == "ews-move-item-between-mailboxes": return_results(move_item_between_mailboxes(client, args)) elif demisto.command() == "ews-move-item": return_results(move_item(client, args)) elif demisto.command() == "ews-delete-items": return_results(delete_items(client, args)) elif demisto.command() == "ews-search-mailbox": return_results(search_items_in_mailbox(client, args)) elif demisto.command() == "ews-get-contacts": return_results(get_contacts(client, args)) elif demisto.command() == "ews-get-out-of-office": return_results(get_out_of_office_state(client, args)) elif demisto.command() == "ews-recover-messages": return_results(recover_soft_delete_item(client, args)) elif demisto.command() == "ews-create-folder": return_results(create_folder(client, args)) elif demisto.command() == "ews-mark-item-as-junk": return_results(mark_item_as_junk(client, args)) elif demisto.command() == "ews-find-folders": return_results(find_folders(client, args)) elif demisto.command() == "ews-get-items-from-folder": return_results(get_items_from_folder(client, args)) elif demisto.command() == "ews-get-items": return_results(get_items(client, args)) elif demisto.command() == "ews-get-folder": return_results(get_folder(client, args)) elif demisto.command() == "ews-get-autodiscovery-config": return_results(get_autodiscovery_config()) elif demisto.command() == "ews-expand-group": return_results(get_expanded_group(client, args)) elif demisto.command() == "ews-mark-items-as-read": return_results(mark_item_as_read(client, args)) elif demisto.command() == "ews-resolve-name": return_results(resolve_name_command(client, args)) elif demisto.command() == "ews-get-items-as-eml": return_results(get_item_as_eml(client, args)) elif demisto.command() == "send-mail": return_results(send_email(client, args)) elif demisto.command() == "reply-mail": return_results(reply_email(client, args)) else: return_error(f'Command: "{demisto.command()}" was not recognized by this integration') except Exception as e: import time time.sleep(2) start_logging() debug_log = log_stream.getvalue() # type: ignore error_message_simple = "" error_message = "" # Office365 regular maintenance case if ( isinstance(e, ErrorMailboxMoveInProgress | ErrorMailboxStoreUnavailable) and urlparse(client.ews_server.lower()).hostname == "outlook.office365.com" ): 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 = ( "Could not connect to the server.\n" "Verify that the Hostname or IP address is correct.\n\n" f"Additional information: {e!s}" ) if isinstance(e, ErrorInvalidPropertyRequest): error_message_simple = "Verify that the Exchange version is correct." else: from exchangelib.errors import MalformedResponseError if is_test_module and isinstance(e, MalformedResponseError): error_message_simple = ( "Got invalid response from the server.\nVerify that the Hostname or IP address is is correct." ) # Legacy error handling if "Status code: 401" in debug_log: error_message_simple = ( "Got unauthorized from the server. Check credentials are correct and authentication method are supported. " ) error_message_simple += ( "You can try using 'domain\\username' as username for authentication. " if client.auth_type.lower() == "ntlm" else "" ) if "SSL: CERTIFICATE_VERIFY_FAILED" in debug_log: # same status code (503) but different error. error_message_simple = ( "Certificate verification failed - This error may happen if the server " "certificate cannot be validated or as a result of a proxy that is doing SSL/TLS " "termination. It is possible to bypass certificate validation by checking " "'Trust any certificate' in the instance settings." ) elif "Status code: 503" in debug_log: error_message_simple = ( "Got timeout from the server. " "Probably the server is not reachable with the current settings. " "Check proxy parameter. If you are using server URL - change to server IP address. " ) 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 Exception(str(e) + traceback.format_exc()) 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(): # pragma: no cover """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 try: handle_proxy() # When running big queries, like 'ews-search-mailbox' the memory might not 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() except Exception as ex: demisto.error(f"Failed starting Process: {ex}") else: sub_main() except Exception as exc: return_error(f"Found error in EWSv2: {exc}", error=f"Error: {exc}\nTraceback: {traceback.format_exc()}") # python2 uses __builtin__ python3 uses builtins if __name__ in ("__builtin__", "builtins", "__main__"): # pragma: no cover main()