EWSO365

The new EWS O365 integration uses OAuth 2.0 protocol and can be used with Exchange Online and Office 365 (mail).

Email · Microsoft Exchange Online

Details

IDEWSO365
ProviderMicrosoft
CategoryEmail
From Version5.0.0
Docker Imagedemisto/py3ews:5.6.0.10133006
Supported ModulesAgentix Cloud Runtime Security Cloud Posture Security XSIAM EDR Cortex Cloud

README

Exchange Web Services (EWS) provides the functionality to enable client applications to communicate with the Exchange server. EWS provides access to much of the same data that is made available through Microsoft Office Outlook.

The EWS O365 integration implants EWS leading services. The integration allows getting information on emails and activities in a target mailbox, and some active operations on the mailbox such as deleting emails and attachments or moving emails from folder to folder.

Microsoft EWS Retirement and EwsAllowedAppIds

Microsoft has announced the final phase of Exchange Web Services (EWS) retirement in Exchange Online, with phased disablement beginning in October 2026.

As part of this transition, Microsoft is moving away from unrestricted EWS access and has introduced a new tenant-level allow list called EwsAllowedAppIds. Starting in October 2026, Microsoft will block all EWS traffic by default. Applications will only be able to access EWS if their specific Application (Client) ID is explicitly added to this allow list by the tenant administrator.

We highly recommend using the Microsoft Graph Mail integration for mailbox operations such as reading, searching, and managing emails and attachments, as Microsoft itself recommends transitioning from EWS to Graph-based APIs.

Action Required

For customers using a Self-Deployed Application

You are responsible for updating your tenant configuration before the October 2026 enforcement takes effect. To ensure this integration continues to function without disruption, you must add the Entra Application (Client) ID used by this integration to your Exchange Online tenant’s allow list.

  1. Connect to Exchange Online PowerShell and add the Application ID.

    If this is the only application you are allowing, run the following command (replace <Your-App-ID> with your actual Application Client ID):

     Set-OrganizationConfig -EwsAllowedAppIDs "<Your-App-ID>"
    

    Warning: This command replaces the entire allow list. If you already have other approved EWS applications, first read the current list and append the new ID so that you don’t overwrite existing entries.

  2. Ensure EWS is enabled for your tenant.

     Set-OrganizationConfig -EwsEnabled $true
    

    (For full details, complete timelines, and PowerShell scripts for safely appending to an existing allow list, refer to the official Microsoft documentation: Introducing EWSAllowedAppIDs: Preparing for the Final Phase of EWS Retirement)

For customers using the Cortex Application authentication method

Palo Alto Networks is making the necessary updates to align our shared Application ID with Microsoft’s new requirements, so you do not need to run the steps above. However, because this change is enforced and controlled entirely by Microsoft, we cannot validate the changes in advance or guarantee uninterrupted functionality. Palo Alto Networks is not responsible for any disruptions resulting from this rollout. We recommend verifying that your integration continues to function as expected once the October 2026 enforcement takes effect.

Retirement of RBAC Application Impersonation

As of February 2025, the Impersonation access type of the integration is deprecated by Microsoft, read about it here.
To avoid disruptions, it is imperative that administrators begin transitioning their applications immediately.
To identify accounts using the ApplicationImpersonation role use the Exchange Online PowerShell command:
Get-ManagementRoleAssignment -Role ApplicationImpersonation -GetEffectiveUsers -Delegating:$false

Use Cases

The EWS integration can be used for the following use cases.

  • Monitor a specific email account and create incidents from incoming emails to the defined folder.
    Follow the instructions in the Fetched Incidents Data section.

  • Search for an email message across mailboxes and folders.

    Use the ews-search-mailbox command to search for all emails in a specific folder within the target mailbox.
    Use the query argument to narrow the search for emails sent from a specific account and more.
    This command retrieves the ItemID field for each email item listed in the results. The ItemID value can be used in the ews-get-items command in order to get more information about the email item itself.

  • Get email attachment information.
    Use the ews-get-attachment command 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-items command 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 the ews-recover-messages command.

Architecture

This integration is based on the exchangelib python module. For more information about the module, check the documentation.

Set up the Third Party System

There are two application authentication methods available.
Follow your preferred method’s guide on how to use the admin consent flow in order to receive your authentication information:

  • Cortex XSOAR Application
    To allow access to EWS O365, an administrator has to approve the Demisto app using an admin consent flow, by clicking on the following link.
    After authorizing the Demisto app, you will get an ID, Token, and Key, which needs to be added to the integration instance configuration’s corresponding fields.

  • Self-Deployed Application - Client Credential Flow.

Authentication

For more details about the authentication used in this integration, see Microsoft Integrations - Authentication.

Permissions

In order to function as expected, the service account should have:

Impersonation rights (deprecated) - In order to perform actions on the target mailbox of other users, the service account must be part of the ApplicationImpersonation role. For more information and instructions on how to set up the permission, see Microsoft Documentation.
Most commands require this permission to function correctly. This permission is specified in each relevant command’s Permission section. For more information, see Microsoft Documentation.

eDiscovery permissions to the Exchange Server. For users to be able to use Exchange Server In-Place eDiscovery, they must be added to the Discovery Management role group. Members of the Discovery Management role group have Full Access mailbox permissions to the default discovery mailbox, which is called Discovery Search Mailbox, including access to sensitive message content. For more information, see the Microsoft documentation.
The need for this permission is specified in each relevant command’s Permission section.

full_access_as_app - The application used for authentication requires this permission to gain access to the Exchange Web Services.
To set this permission follow these steps:

  1. Navigate to Home > App registrations.
  2. Search for your app under all applications.
  3. Click API permissions > Add permission.
  4. Search for Office 365 Exchange Online API > Application Permission> full_access_as_app permission.

For more information on this permission, see the Microsoft documentation.

To limit the application’s permissions to only specific mailboxes, follow the Microsoft documentation. Note that it may take about an hour for permissions changes to take effect.

Configure Integration on Cortex

Parameter Description Required
ID / Application ID ID can be received after following the System Integration Setup (Device side steps). False
Token / Tenant ID Token can be received after following the System Integration Setup (Device side steps). False
Key / Application Secret Key can be received after following the System Integration Setup (Device side steps). False
Azure Cloud Azure Cloud environment. Options are: Worldwide (The publicly accessible Azure Cloud), US GCC (Azure cloud for the USA Government Cloud Community), US GCC-High (Azure cloud for the USA Government Cloud Community High), DoD (Azure cloud for the USA Department of Defense), Germany (Azure cloud for the German Government), China (Azure cloud for the Chinese Government ) False
Email Address Mailbox to run commands on and to fetch incidents from. To use this functionality, your account must have delegation for the account specified. For more information, see https://xsoar.pan.dev/docs/reference/integrations/ewso365/#additional-information True
UPN Address When provided, the target mailbox if it’s different from the Email Address. Otherwise, the Email Address is used. False
Name of the folder from which to fetch incidents Supports Exchange Folder ID and sub-folders, e.g., Inbox/Phishing. True
Access Type Run the commands using Delegate or Impersonation access types. False
Public Folder Whether the folder to be fetched from is public. Public folders can store and organize emails on specific topics or projects. Public folders are usually listed under the “Public Folders” section in the navigation pane in the product itself. False
Fetch incidents   False
Incident type   False
First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)   False
Maximum number of incidents per fetch (up to 200). Performance might be affected by a value higher than 50.   False
Mark fetched emails as read   False
Timeout (in seconds) for HTTP requests to Exchange Server   False
Trust any certificate (not secure)   False
Use system proxy settings   False
Run as a separate process (protects against memory depletion)   False
Use a self-deployed Azure Application Select this checkbox if you are using a self-deployed Azure application. False
Incidents Fetch Interval   False
Skip unparsable emails during fetch incidents Whether to skip unparsable emails during incident fetching. False
What time field should we filter incidents by? Default is to filter by received-time, which works well if the folder is an “Inbox”. But for a folder emails are dragged into for attention, if we filter by received-time, out-of-order processing of emails means some are ignored. Filtering by modified-time works better for such a scenario. This works best if any modifications (such as tagging) happens before moving the email into the folder, such that the move into the folder is the last modification, and triggers Cortex XSOAR to fetch it as an incident. False

Fetch Incidents

The integration imports email messages from the destination folder in the target mailbox as incidents. If the message contains any attachments, they are uploaded to the War Room as files. If the attachment is an email, Cortex XSOAR fetches information about the attached email and downloads all of its attachments (if there are any) as files.

To use Fetch incidents, configure a new instance and select the Fetches incidents option in the instance settings.

IMPORTANT:
First fetch timestamp field is used to determine how much time back to fetch incidents from. The default value is the previous 10 minutes, Meaning, if this is the first time emails are fetched from the destination folder, all emails from 10 minutes prior to the instance configuration and up to the current time will be fetched.
When set to get a long period of time, the Timeout field might need to be set to a higher value.

Pay special attention to the following fields in the instance settings:

  • Email Address – mailbox to fetch incidents from.
  • Name of the folder from which to fetch incidents – use this field to configure the destination folder from where emails should be fetched. The default is Inbox folder.

Permissions

Impersonation rights required. In order to perform actions on the target mailbox of other users, the service account must be part of the ApplicationImpersonation role.

Limitations

If Exchange is configured with an international flavor, Inbox will be named according to the configured language.

Commands

<h3 style={{display: 'inline'}}>ews-get-attachment</h3> ### ews-get-attachment Retrieves the actual attachments from an email message. To get all attachments for a message, only specify the item-id argument. #### Permissions Impersonation rights required. In order to perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |item-id |The ID of the email message for which to get the attachments.|Required| |target-mailbox |The mailbox in which this attachment was found. If empty, the default mailbox is used. Otherwise, the user might require impersonation rights to this mailbox.|Optional| |attachment-ids |The attachments IDs to get. If none, all attachments will be retrieved from the message. Support multiple attachments with comma-separated values or an array. |Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.FileAttachments.attachmentId|string|The attachment ID. Used for file attachments only.| |EWS.Items.FileAttachments.attachmentName|string|The attachment name. Used for file attachments only.| |EWS.Items.FileAttachments.attachmentSHA256|string|The SHA256 hash of the attached file.| |EWS.Items.FileAttachments.attachmentLastModifiedTime|date|The attachment last modified time. Used for file attachments only.| |EWS.Items.ItemAttachments.datetimeCreated|date|The created time of the attached email.| |EWS.Items.ItemAttachments.datetimeReceived|date|The received time of the attached email.| |EWS.Items.ItemAttachments.datetimeSent|date|The sent time of the attached email.| |EWS.Items.ItemAttachments.receivedBy|string|The received by address of the attached email.| |EWS.Items.ItemAttachments.subject|string|The subject of the attached email.| |EWS.Items.ItemAttachments.textBody|string|The body of the attached email (as text).| |EWS.Items.ItemAttachments.headers|Unknown|The headers of the attached email.| |EWS.Items.ItemAttachments.hasAttachments|boolean|Whether the attached email has attachments.| |EWS.Items.ItemAttachments.itemId|string|The attached email item ID.| |EWS.Items.ItemAttachments.toRecipients|Unknown|A list of recipient email addresses for the attached email.| |EWS.Items.ItemAttachments.body|string|The body of the attached email (as HTML).| |EWS.Items.ItemAttachments.attachmentSHA256|string|SHA256 hash of the attached email (as EML file).| |EWS.Items.ItemAttachments.FileAttachments.attachmentSHA256|string|SHA256 hash of the attached files inside of the attached email.| |EWS.Items.ItemAttachments.ItemAttachments.attachmentSHA256|string|SHA256 hash of the attached emails inside of the attached email.| |EWS.Items.ItemAttachments.isRead|String|The read status of the attachment.| #### Examples ``` !ews-get-attachment item-id=BBFDShfdafFSDF3FADR3434DFASDFADAFDADFADFCJebinpkUAAAfxuiVAAA= target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Context Example ``` { "EWS": { "Items": { "ItemAttachments": { "originalItemId": "BBFDShfdafFSDF3FADR3434DFASDFADAFDADFADFCJebinpkUAAAfxuiVAAA=", "attachmentSize": 2956, "receivedBy": "test@demistodev.onmicrosoft.com", "size": 28852, "author": "test2@demistodev.onmicrosoft.com", "attachmentLastModifiedTime": "2019-08-11T15:01:30+00:00", "subject": "Moving Email between mailboxes", "body": "Some text inside", "datetimeCreated": "2019-08-11T15:01:47Z", "importance": "Normal", "attachmentType": "ItemAttachment", "toRecipients": [ "test@demistodev.onmicrosoft.com" ], "mailbox": "test@demistodev.onmicrosoft.com", "isRead": false, "attachmentIsInline": false, "datetimeSent": "2019-08-07T12:50:19Z", "lastModifiedTime": "2019-08-11T15:01:30Z", "sender": "test2@demistodev.onmicrosoft.com", "attachmentName": "Moving Email between mailboxes", "datetimeReceived": "2019-08-07T12:50:20Z", "attachmentSHA256": "119e27b28dc81bdfd4f498d44bd7a6d553a74ee03bdc83e6255a53", "hasAttachments": false, "headers": [ { "name": "Subject", "value": "Moving Email between mailboxes" } ... ], "attachmentId": "BBFDShfdafFSDF3FADR3434DFASDFADAFDADFADFCJebinpkUAAAfxuiVAAABEgAQAOpEfpzDB4dFkZ+/K4XSj44=", "messageId": "message_id" } } } ``` <h3 style={{display: 'inline'}}>ews-delete-attachment</h3> ### ews-delete-attachment Deletes the attachments of an item (email message). #### Permissions Impersonation rights required. In order to perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |item-id|The ID of the email message for which to delete attachments.|Required| |target-mailbox|The mailbox in which this attachment was found. If empty, the default mailbox is used. Otherwise, the user might require impersonation rights to this mailbox.|Optional| |attachment-ids|A comma-separated list (or array) of attachment IDs to delete. If empty, all attachments will be deleted from the message.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.FileAttachments.attachmentId|string|The ID of the deleted attachment, in case of file attachment.| |EWS.Items.ItemAttachments.attachmentId|string|The ID of the deleted attachment, in case of other attachment (for example, "email").| |EWS.Items.FileAttachments.action|string|The deletion action in case of file attachment. This is a constant value: 'deleted'.| |EWS.Items.ItemAttachments.action|string|The deletion action in case of other attachment (for example, "email"). This is a constant value: 'deleted'.| #### Examples ``` !ews-delete-attachment item-id=AAMkADQ0NmwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUQZfBJjfaljfAFDVSDinpkUAAAfxxd9AAA= target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output >|action|attachmentId| >|--- |--- | >|deleted|AAMkADQ0NmwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUQZfBJjfaljfAFDVSDinpkUAAAfxxd9AAABEgAQAIUht2vrOdErec33=| ##### Context Example ``` { "EWS": { "Items": { "FileAttachments": { "action": "deleted", "attachmentId": "AAMkADQ0NmwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUQZfBJjfaljfAFDVSDinpkUAAAfxxd9AAABEgAQAIUht2vrOdErec33=" } } } } ``` <h3 style={{display: 'inline'}}>ews-get-searchable-mailboxes</h3> ### ews-get-searchable-mailboxes Get a list of searchable mailboxes. #### Permissions Requires eDiscovery permissions to the Exchange Server. For more information see the [Microsoft documentation](https://technet.microsoft.com/en-us/library/dd298059(v=exchg.160).aspx). #### Limitations No known limitations. #### Inputs There are no input arguments for this command. #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Mailboxes.mailbox|string|Addresses of the searchable mailboxes.| |EWS.Mailboxes.mailboxId|string|IDs of the searchable mailboxes.| |EWS.Mailboxes.displayName|string|The email display name.| |EWS.Mailboxes.isExternal|boolean|Whether the mailbox is external.| |EWS.Mailboxes.externalEmailAddress|string|The external email address.| #### Examples ``` !ews-get-searchable-mailboxes ``` ##### Human Readable Output >|displayName|isExternal|mailbox|mailboxId| >|--- |--- |--- |--- | >|test|false|test@demistodev.onmicrosoft.com|/o=Exchange\*\*\*/ou=Exchange Administrative Group ()/cn=\*\*/cn=\*\\_-_\*| ##### Context Example ``` { "EWS": { "Mailboxes": [ { "mailbox": "test@demistodev.onmicrosoft.com", "displayName": "test", "mailboxId": "/o=Exchange***/ou=Exchange Administrative Group ()/cn=**/cn=**-**", "isExternal": "false" } ... ] } } ``` <h3 style={{display: 'inline'}}>ews-move-item</h3> ### ews-move-item Move an item to a different folder in the mailbox. #### Permissions Impersonation rights required. In order to perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |item-id|The ID of the item to move.|Required| |target-folder-path|The path to the folder to which to move the item. Complex paths are supported, for example, "Inbox\Phishing".|Required| |target-mailbox|The mailbox on which to run the command.|Optional| |is-public|Whether the target folder is a public folder.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.newItemID|string|The item ID after the move.| |EWS.Items.messageID|string|The item message ID.| |EWS.Items.itemId|string|The original item ID.| |EWS.Items.action|string|The action taken. The value will be "moved".| #### Examples ``` !ews-move-item item-id=VDAFNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU34cSCSSSfBJebinpkUAAAAAAEMAACyyVyFtlsUQZfBJebinpkUAAAfxuiRAAA= target-folder-path=Moving target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output >|action|itemId|messageId|newItemId| >|--- |--- |--- |--- | >|moved|VDAFNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU34cSCSSSfBJebinpkUAAAAAAEMAACyyVyFtlsUQZfBJebinpkUAAAfxuiRAAA||AAVAAAVN2NkLThmZjdmNTZjNTMxFFFFJTJPMPXU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAAa2bUBAACyyVfafainpkUAAAfxxd+AAA=| ##### Context Example ``` { "EWS": { "Items": { "action": "moved", "itemId": "VDAFNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU34cSCSSSfBJebinpkUAAAAAAEMAACyyVyFtlsUQZfBJebinpkUAAAfxuiRAAA", "newItemId": "AAVAAAVN2NkLThmZjdmNTZjNTMxFFFFJTJPMPXU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAAa2bUBAACyyVfafainpkUAAAfxxd+AAA=", "messageId": "" } } } ``` </details> <h3 style={{display: 'inline'}}>ews-delete-items</h3> ### ews-delete-items Delete an item from a mailbox #### Permissions Impersonation rights required. In order to perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |item-ids|A comma-separated list (or array) of IDs to delete.|Required| |delete-type|Deletion type. Can be "trash", "soft", or "hard".|Required| |target-mailbox|The mailbox on which to run the command.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.itemId|string|The deleted item ID.| |EWS.Items.messageId|string|The deleted message ID.| |EWS.Items.action|string|The deletion action. Can be 'trash-deleted', 'soft-deleted', or 'hard-deleted'.| #### Examples ``` !ews-delete-items item-ids=VWAFA3hmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAABjKMGAACyw+kAAA= delete-type=soft target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output >|action|itemId|messageId| >|--- |--- |--- | >|soft-deleted|VWAFA3hmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAABjKMGAACyw+kAAA=|| ##### Context Example ``` { "EWS": { "Items": { "action": "soft-deleted", "itemId": "VWAFA3hmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAABjKMGAACyw+kAAA=", "messageId": "messaage_id" } } } ``` <h3 style={{display: 'inline'}}>ews-search-mailbox</h3> ### ews-search-mailbox Searches for items in the specified mailbox. Specific permissions are needed for this operation to search in a target mailbox other than the default. #### Permissions Impersonation rights required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |query|The search query string. For more information about the query syntax, see the [Microsoft documentation](https://msdn.microsoft.com/en-us/library/ee693615.aspx).|Optional| |folder-path|The folder path in which to search. If empty, searches all the folders in the mailbox.|Optional| |limit|Maximum number of results to return.|Optional| |target-mailbox|The mailbox on which to apply the search.|Optional| |is-public|Whether the folder is a public folder?|Optional| |message-id|The message ID of the email. This will be ignored if a query argument is provided.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.itemId|string|The email item ID.| |EWS.Items.hasAttachments|boolean|Whether the email has attachments.| |EWS.Items.datetimeReceived|date|Received time of the email.| |EWS.Items.datetimeSent|date|Sent time of the email.| |EWS.Items.headers|Unknown|Email headers (list).| |EWS.Items.sender|string|Sender email address of the email.| |EWS.Items.subject|string|Subject of the email.| |EWS.Items.textBody|string|Body of the email (as text).| |EWS.Items.size|number|Email size.| |EWS.Items.toRecipients|Unknown|List of email recipients addresses.| |EWS.Items.receivedBy|Unknown|Email received by address.| |EWS.Items.messageId|string|Email message ID.| |EWS.Items.body|string|Body of the email (as HTML).| |EWS.Items.FileAttachments.attachmentId|unknown|Attachment ID of the file attachment.| |EWS.Items.ItemAttachments.attachmentId|unknown|Attachment ID of the item attachment.| |EWS.Items.FileAttachments.attachmentName|unknown|Attachment name of the file attachment.| |EWS.Items.ItemAttachments.attachmentName|unknown|Attachment name of the item attachment.| |EWS.Items.isRead|String|The read status of the email.| #### Examples ``` !ews-search-mailbox query="subject:"Get Attachment Email" target-mailbox=test@demistodev.onmicrosoft.com limit=1 ``` ##### Human Readable Output >|sender|subject|hasAttachments|datetimeReceived|receivedBy|author|toRecipients| >|--- |--- |--- |--- |--- |--- |--- | >|test2@demistodev.onmicrosoft.com|Get Attachment Email|true|2019-08-11T10:57:37Z|test@demistodev.onmicrosoft.com|test2@demistodev.onmicrosoft.com|test@demistodev.onmicrosoft.com| ##### Context Example ``` { "EWS": { "Items": { "body": "\r\n\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n<style type=\"text/css\" style=\"display:none;\"></style>\r\n\r\n<body dir=\"ltr\">\r\n<div id=\"divtagrapper\" style=\"font-size:12pt;color:#000000;font-family:Calibri,Helvetica,sans-serif;\" dir=\"ltr\">\r\n<p style=\"margin-top:0;margin-bottom:0\">Some text inside email</p>\r\n</div>\r\n</body>\r\n\r\n", "itemId": "AAMkADQ0NmFFijer3FFmNTZjNTMxNwBGAAAAAAFSAAfxw+jAAA=", "toRecipients": [ "test@demistodev.onmicrosoft.com" ], "datetimeCreated": "2019-08-11T10:57:37Z", "datetimeReceived": "2019-08-11T10:57:37Z", "author": "test2@demistodev.onmicrosoft.com", "hasAttachments": true, "size": 30455, "subject": "Get Attachment Email", "FileAttachments": [ { "attachmentName": "atta1.rtf", "attachmentSHA256": "csfd81097bc049fbcff6e637ade0407a00308bfdfa339e31a44a1c4e98f28ce36e4f", "attachmentType": "FileAttachment", "attachmentSize": 555, "attachmentId": "AAMkADQ0NmFkODFkLWQ4MDEtNDE4Mi1hN2NkLThmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAABjKMGAACyyVyFtlsUQZfBJebinpkUAAAfxw+jAAABEgAQAEyq1TB2nKBLpKUiFUJ5Geg=", "attachmentIsInline": false, "attachmentLastModifiedTime": "2019-08-11T11:06:02+00:00", "attachmentContentLocation": null, "attachmentContentType": "text/rtf", "originalItemId": "AAMkADQ0NmFFijer3FFmNTZjNTMxNwBGAAAAAAFSAAfxw+jAAA=", "attachmentContentId": null } ], "headers": [ { "name": "Subject", "value": "Get Attachment Email" }, ... ], "isRead": true, "messageId": "", "receivedBy": "test@demistodev.onmicrosoft.com", "datetimeSent": "2019-08-11T10:57:36Z", "lastModifiedTime": "2019-08-11T11:13:59Z", "mailbox": "test@demistodev.onmicrosoft.com", "importance": "Normal", "textBody": "Some text inside email\r\n", "sender": "test2@demistodev.onmicrosoft.com" } } } ``` </details> <h3 style={{display: 'inline'}}>ews-get-contacts</h3> ### ews-get-contacts Retrieves contacts for a specified mailbox. #### Permissions Impersonation rights required. In order to perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |target-mailbox|The mailbox for which to retrieve the contacts.|Optional| |limit|Maximum number of results to return.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |Account.Email.EwsContacts.displayName|Unknown|The contact name.| |Account.Email.EwsContacts.lastModifiedTime|Unknown|The time that the contact was last modified.| |Account.Email.EwsContacts.emailAddresses|Unknown|Phone numbers of the contact.| |Account.Email.EwsContacts.physicalAddresses|Unknown|Physical addresses of the contact.| |Account.Email.EwsContacts.phoneNumbers.phoneNumber|Unknown|Email addresses of the contact.| #### Examples ``` !ews-get-contacts limit="1" ``` ##### Human Readable Output >|changekey|culture|datetimeCreated|datetimeReceived|datetimeSent|displayName|emailAddresses|fileAs|fileAsMapping|givenName|id|importance|itemClass|lastModifiedName|lastModifiedTime|postalAddressIndex|sensitivity|subject|uniqueBody|webClientReadFormQueryString| >|--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- |--- | >|EABYACAADcsxRwRjq/zTrN6vWSzKAK1Dl3N|en-US|2019-08-05T12:35:36Z|2019-08-05T12:35:36Z|2019-08-05T12:35:36Z|Contact Name|some@dev.microsoft.com|Contact Name|LastCommaFirst|Contact Name|AHSNNK3NQNcasnc3SAS/zTrN6vWSzK4OWAAAAAAEOAADrxRwRjq/zTrNFSsfsfVWAAK1KsF3AAA=|Normal|IPM.Contact|John Smith|2019-08-05T12:35:36Z|None|Normal|Contact Name||<https://outlook.office365.com/owa/?ItemID>=***| ##### Context Example ``` { "Account.Email": [ { "itemClass": "IPM.Contact", "lastModifiedName": "John Smith", "displayName": "Contact Name", "datetimeCreated": "2019-08-05T12:35:36Z", "datetimeReceived": "2019-08-05T12:35:36Z", "fileAsMapping": "LastCommaFirst", "importance": "Normal", "sensitivity": "Normal", "postalAddressIndex": "None", "webClientReadFormQueryString": "https://outlook.office365.com/owa/?ItemID=***", "uniqueBody": "", "fileAs": "Contact Name", "culture": "en-US", "changekey": "EABYACAADcsxRwRjq/zTrN6vWSzKAK1Dl3N", "lastModifiedTime": "2019-08-05T12:35:36Z", "datetimeSent": "2019-08-05T12:35:36Z", "emailAddresses": [ "some@dev.microsoft.com" ], "givenName": "Contact Name", "id": "AHSNNK3NQNcasnc3SAS/zTrN6vWSzK4OWAAAAAAEOAADrxRwRjq/zTrNFSsfsfVWAAK1KsF3AAA=", "subject": "Contact Name" } ] } ``` <h3 style={{display: 'inline'}}>ews-get-out-of-office</h3> ### ews-get-out-of-office Retrieves the out-of-office status for a specified mailbox. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |target-mailbox|The mailbox for which to get the out-of-office status.|Required| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |Account.Email.OutOfOffice.state|Unknown|Out-of-office state. The result can be: "Enabled", "Scheduled", or "Disabled".| |Account.Email.OutOfOffice.externalAudience|Unknown|Out-of-office external audience. Can be "None", "Known", or "All".| |Account.Email.OutOfOffice.start|Unknown|Out-of-office start date.| |Account.Email.OutOfOffice.end|Unknown|Out-of-office end date.| |Account.Email.OutOfOffice.internalReply|Unknown|Out-of-office internal reply.| |Account.Email.OutOfOffice.externalReply|Unknown|Out-of-office external reply.| |Account.Email.OutOfOffice.mailbox|Unknown|Out-of-office mailbox.| #### Examples ``` !ews-get-out-of-office target-mailbox=test@demistodev.onmicrosoft.com ``` ###### Human Readable Output >|end|externalAudience|mailbox|start|state| >|--- |--- |--- |--- |--- | >|2019-08-12T13:00:00Z|All|test@demistodev.onmicrosoft.com|2019-08-11T13:00:00Z|Disabled| ###### Context Example ``` { "Account": { "Email": { "OutOfOffice": { "start": "2019-08-11T13:00:00Z", "state": "Disabled", "mailbox": "test@demistodev.onmicrosoft.com", "end": "2019-08-12T13:00:00Z", "externalAudience": "All" } } } } ``` <h3 style={{display: 'inline'}}>ews-recover-messages</h3> ### ews-recover-messages Recovers messages that were soft-deleted. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |message-ids|A CSV list of message IDs. Run the py-ews-delete-items command to retrieve the message IDs|Required| |target-folder-path|The folder path to recover the messages to.|Required| |target-mailbox|The mailbox in which the messages found. If empty, will use the default mailbox. If you specify a different mailbox, you might need impersonation rights to the mailbox.|Optional| |is-public|Whether the target folder is a public folder.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.itemId|Unknown|The item ID of the recovered item.| |EWS.Items.messageId|Unknown|The message ID of the recovered item.| |EWS.Items.action|Unknown|The action taken on the item. The value will be 'recovered'.| #### Examples ``` !ews-recover-messages message-ids= target-folder-path=Moving target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output >|action|itemId|messageId| >|--- |--- |--- | >|recovered|AAVCSVS1hN2NkLThmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed33wX3aBwCyyVyFtlsUQZfBJebinpkUAAAa2bUBAACyyVyFtlscfxxd/AAA=|| ##### Context Example ``` { "EWS": { "Items": { "action": "recovered", "itemId": "AAVCSVS1hN2NkLThmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed33wX3aBwCyyVyFtlsUQZfBJebinpkUAAAa2bUBAACyyVyFtlscfxxd/AAA=", "messageId": "" } } } ``` </details> <h3 style={{display: 'inline'}}>ews-create-folder</h3> ### ews-create-folder Creates a new folder in a specified mailbox. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |new-folder-name|The name of the new folder.|Required| |folder-path|Path to locate the new folder. Exchange folder ID is also supported.|Required| |target-mailbox|The mailbox in which to create the folder.|Optional| #### Outputs There is no context output for this command. #### Examples ``` !ews-create-folder folder-path=Inbox new-folder-name="Created Folder" target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output > > Folder Inbox\Created Folder created successfully <h3 style={{display: 'inline'}}>ews-mark-item-as-junk</h3> ### ews-mark-item-as-junk Marks an item as junk. This is used to block an email address (meaning all future emails from this sender will be sent to the junk folder). For more information, see the [Microsoft documentation](https://msdn.microsoft.com/en-us/library/office/dn481311(v=exchg.150).aspx). #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |item-id|The item ID to mark as junk.|Required| |move-items|Whether to move the item from the original folder to the junk folder.|Optional| |target-mailbox|If empty, will use the default mailbox. If you specify a different mailbox, you might need impersonation rights to the mailbox.|Optional| #### Outputs There is no context output for this command. #### Examples ``` !ews-mark-item-as-junk item-id=AAMkcSQ0NmFkOhmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUcsBJebinpkUAAAAAAEMASFDkUAAAfxuiSAAA= move-items=yes target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output |action|itemId| |--- |--- | |marked-as-junk|AAMkcSQ0NmFkOhmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUcsBJebinpkUAAAAAAEMASFDkUAAAfxuiSAAA=| ##### Context Example ``` { "EWS": { "Items": { "action": "marked-as-junk", "itemId": "AAMkcSQ0NmFkOhmZjdmNTZjNTMxNwBGAAAAAAA4kxh+ed3JTJPMPXU3wX3aBwCyyVyFtlsUcsBJebinpkUAAAAAAEMASFDkUAAAfxuiSAAA=" } } } ``` <h3 style={{display: 'inline'}}>ews-find-folders</h3> ### ews-find-folders Retrieves information for the folders of the specified mailbox. Only folders with read permissions will be returned. Your visual folders on the mailbox, such as "Inbox", are under the folder "Top of Information Store". #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |target-mailbox|The mailbox on which to apply the command.|Optional| |is-public|Whether to find public folders.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Folders.name|string|Folder name.| |EWS.Folders.id|string|Folder ID.| |EWS.Folders.totalCount|Unknown|Number of items in the folder.| |EWS.Folders.unreadCount|number|Number of unread items in the folder.| |EWS.Folders.changeKey|number|Folder change key.| |EWS.Folders.childrenFolderCount|number|Number of sub-folders.| #### Examples ``` !ews-find-folders target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output ``` root ├── AllContacts ├── AllItems ├── Common Views ├── Deferred Action ├── ExchangeSyncData ├── Favorites ├── Freebusy Data ├── Location ├── MailboxAssociations ├── My Contacts ├── MyContactsExtended ├── People I Know ├── PeopleConnect ├── Recoverable Items │ ├── Calendar Logging │ ├── Deletions │ ── Purges │ └── Versions ├── Reminders ├── Schedule ├── Sharing ├── Shortcuts ├── Spooler Queue ├── System ├── To-Do Search ├── Top of Information Store │ ├── Calendar │ ├── Contacts │ │ ├── GAL Contacts │ │ ├── Recipient Cache │ ├── Conversation Action Settings │ ├── Deleted Items │ │ └── Create1 │ ├── Drafts │ ├── Inbox ... ``` ##### Context Example ``` { "EWS": { "Folders": [ { "unreadCount": 1, "name": "Inbox", "childrenFolderCount": 1, "totalCount": 44, "changeKey": "**********fefsduQi0", "id": "*******VyFtlFDSAFDSFDAAA=" } ... ] } } ``` <h3 style={{display: 'inline'}}>ews-get-items-from-folder</h3> ### ews-get-items-from-folder Retrieves items from a specified folder in a mailbox. The items are ordered by the item created time. Most recent is first. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |folder-path|The folder path from which to get the items.|Required| |limit|Maximum number of items to return.|Optional| |target-mailbox|The mailbox on which to apply the command.|Optional| |is-public|Whether the folder is a public folder. Default is 'False'.|Optional| |get-internal-items|If the email item contains another email as an attachment (EML or MSG file), whether to retrieve the EML/MSG file attachment. Can be "yes" or "no". Default is "no".|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.itemId|string|The item ID of the email.| |EWS.Items.hasAttachments|boolean|Whether the email has attachments.| |EWS.Items.datetimeReceived|date|Received time of the email.| |EWS.Items.datetimeSent|date|Sent time of the email.| |EWS.Items.headers|Unknown|Email headers (list).| |EWS.Items.sender|string|Sender mail address of the email.| |EWS.Items.subject|string|Subject of the email.| |EWS.Items.textBody|string|Body of the email (as text).| |EWS.Items.size|number|Email size.| |EWS.Items.toRecipients|Unknown|Email recipients addresses (list).| |EWS.Items.receivedBy|Unknown|Received by address of the email.| |EWS.Items.messageId|string|Email message ID.| |EWS.Items.body|string|Body of the email (as HTML).| |EWS.Items.FileAttachments.attachmentId|unknown|Attachment ID of file attachment.| |EWS.Items.ItemAttachments.attachmentId|unknown|Attachment ID of the item attachment.| |EWS.Items.FileAttachments.attachmentName|unknown|Attachment name of the file attachment.| |EWS.Items.ItemAttachments.attachmentName|unknown|Attachment name of the item attachment.| |EWS.Items.isRead|String|The read status of the email.| |EWS.Items.categories|String|Categories of the email.| #### Examples ``` !ews-get-items-from-folder folder-path=Test target-mailbox=test@demistodev.onmicrosoft.com limit=1 ``` ##### Human Readable Output >|sender|subject|hasAttachments|datetimeReceived|receivedBy|author|toRecipients|itemId| >|--- |--- |--- |--- |--- |--- |--- |--- | >|test2@demistodev.onmicrosoft.com|Get Attachment Email|true|2019-08-11T10:57:37Z|test@demistodev.onmicrosoft.com|test2@demistodev.onmicrosoft.com|test@demistodev.onmicrosoft.com|AAFSFSFFtlsUQZfBJebinpkUAAABjKMGAACyyVyFtlsUQZfBJebinpkUAAAsfw+jAAA=| ##### Context Example ``` { "EWS": { "Items": { "body": "\r\n\r\n<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">\r\n<style type=\"text/css\" style=\"display:none;\"></style>\r\n\r\n<body dir=\"ltr\">\r\n<div id=\"divtagdefaultwrapper\" style=\"font-size:12pt;color:#000000;font-family:Calibri,Helvetica,sans-serif;\" dir=\"ltr\">\r\n<p style=\"margin-top:0;margin-bottom:0\">Some text inside email</p>\r\n</div>\r\n</body>\r\n\r\n", "itemId": "AAFSFSFFtlsUQZfBJebinpkUAAABjKMGAACyyVyFtlsUQZfBJebinpkUAAAsfw+jAAA=", "toRecipients": [ "test@demistodev.onmicrosoft.com" ], "datetimeCreated": "2019-08-11T10:57:37Z", "datetimeReceived": "2019-08-11T10:57:37Z", "author": "test2@demistodev.onmicrosoft.com", "hasAttachments": true, "size": 21435, "subject": "Get Attachment Email", "FileAttachments": [ { "attachmentName": "atta1.rtf", "attachmentSHA256": "cd81097bcvdiojf3407a00308b48039e31a44a1c4fdnfkdknce36e4f", "attachmentType": "FileAttachment", "attachmentSize": 535, "attachmentId": "AAFSFSFFtlsUQZfBJebinpkUAAABjKMGAACyyVyFtlsUQZfBJebinpkUAAAsfw+jAAABEgAQAEyq1TB2nKBLpKUiFUJ5Geg=", "attachmentIsInline": false, "attachmentLastModifiedTime": "2019-08-11T11:06:02+00:00", "attachmentContentLocation": null, "attachmentContentType": "text/rtf", "originalItemId": "AAFSFSFFtlsUQZfBJebinpkUAAABjKMGAACyyVyFtlsUQZfBJebinpkUAAAsfw+jAAA=", "attachmentContentId": null } ], "headers": [ { "name": "Subject", "value": "Get Attachment Email" }, ... ], "isRead": true, "messageId": "", "receivedBy": "test@demistodev.onmicrosoft.com", "datetimeSent": "2019-08-11T10:57:36Z", "lastModifiedTime": "2019-08-11T11:13:59Z", "mailbox": "test@demistodev.onmicrosoft.com", "importance": "Normal", "textBody": "Some text inside email\r\n", "sender": "test2@demistodev.onmicrosoft.com" } } } ``` </details> <h3 style={{display: 'inline'}}>ews-get-items</h3> ### ews-get-items Retrieves items by item ID. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |item-ids|A CSV list of item IDs.|Required| |target-mailbox|The mailbox on which to run the command on.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.itemId|string|The email item ID.| |EWS.Items.hasAttachments|boolean|Whether the email has attachments.| |EWS.Items.datetimeReceived|date|Received time of the email.| |EWS.Items.datetimeSent|date|Sent time of the email.| |EWS.Items.headers|Unknown|Email headers (list).| |EWS.Items.sender|string|Sender mail address of the email.| |EWS.Items.subject|string|Subject of the email.| |EWS.Items.textBody|string|Body of the email (as text).| |EWS.Items.size|number|Email size.| |EWS.Items.toRecipients|Unknown|Email recipients addresses (list).| |EWS.Items.receivedBy|Unknown|Received by address of the email.| |EWS.Items.messageId|string|Email message ID.| |EWS.Items.body|string|Body of the email (as HTML).| |EWS.Items.FileAttachments.attachmentId|unknown|Attachment ID of the file attachment.| |EWS.Items.ItemAttachments.attachmentId|unknown|Attachment ID of the item attachment.| |EWS.Items.FileAttachments.attachmentName|unknown|Attachment name of the file attachment.| |EWS.Items.ItemAttachments.attachmentName|unknown|Attachment name of the item attachment.| |EWS.Items.isRead|String|The read status of the email.| |EWS.Items.categories|String|Categories of the email.| |Email.CC|String|Email addresses CC'ed to the email.| |Email.BCC|String|Email addresses BCC'ed to the email.| |Email.To|String|The recipient of the email.| |Email.From|String|The sender of the email.| |Email.Subject|String|The subject of the email.| |Email.Text|String|The plain-text version of the email.| |Email.HTML|String|The HTML version of the email.| |Email.HeadersMap|String|The headers of the email.| #### Examples ``` !ews-get-items item-ids=AAMkADQ0NmFkODFkLWQ4MDEtNDFDFZjNTMxNwBGAAAAAAA4kxhFFAfxw+jAAA= target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output ``` Identical outputs to `ews-get-items-from-folder` command. ``` <h3 style={{display: 'inline'}}>ews-move-item-between-mailboxes</h3> ### ews-move-item-between-mailboxes Moves an item from one mailbox to a different mailbox. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |item-id|The item ID to move.|Required| |destination-folder-path|The folder in the destination mailbox to which to move the item. You can specify a complex path, for example, "Inbox\Phishing".|Required| |destination-mailbox|The mailbox to which to move the item.|Required| |source-mailbox|The mailbox from which to move the item (conventionally called the "target-mailbox", the target mailbox on which to run the command).|Optional| |is-public|Whether the destination folder is a public folder. Default is "False".|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.movedToMailbox|string|The mailbox to which the item was moved.| |EWS.Items.movedToFolder|string|The folder to which the item was moved.| |EWS.Items.action|string|The action taken on the item. The value will be "moved".| #### Examples ``` !ews-move-item-between-mailboxes item-id=AAMkAGY3OTQyMzMzLWYxNjktNDE0My05NFSFSyNzBkNABGAAAAAACYCKjWAjq/zTrN6vWSzK4OWAAK2ISFSA= destination-folder-path=Moving destination-mailbox=test@demistodev.onmicrosoft.com source-mailbox=test2@demistodev.onmicrosoft.com ``` ##### Human Readable Output >Item was moved successfully. ##### Context Example ``` { "EWS": { "Items": { "movedToMailbox": "test@demistodev.onmicrosoft.com", "movedToFolder": "Moving" } } } ``` <h3 style={{display: 'inline'}}>ews-get-folder</h3> ### ews-get-folder Retrieves a single folder. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations If Exchange is configured with an international flavor, `Inbox` will be named according to the configured language. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |target-mailbox|The mailbox on which to apply the search.|Optional| |folder-path|The path of the folder to retrieve. If empty, will retrieve the folder "AllItems".|Optional| |is-public|Whether the folder is a public folder. Default is "False".|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Folders.id|string|Folder ID.| |EWS.Folders.name|string|Folder name.| |EWS.Folders.changeKey|string|Folder change key.| |EWS.Folders.totalCount|number|Total number of emails in the folder.| |EWS.Folders.childrenFolderCount|number|Number of sub-folders.| |EWS.Folders.unreadCount|number|Number of unread emails in the folder.| #### Examples ``` !ews-get-folder folder-path=demistoEmail target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output >|changeKey|childrenFolderCount|id|name|totalCount|unreadCount| >|--- |--- |--- |--- |--- |--- | >|***yFtCdJSH|0|AAMkADQ0NmFkODFkLWQ4MDEtNDE4Mi1hN2NlsjflsjfSF=|demistoEmail|1|0| ##### Context Example ``` { "EWS": { "Folders": { "unreadCount": 0, "name": "demistoEmail", "childrenFolderCount": 0, "totalCount": 1, "changeKey": "***yFtCdJSH", "id": "AAMkADQ0NmFkODFkLWQ4MDEtNDE4Mi1hN2NlsjflsjfSF=" } } } ``` <h3 style={{display: 'inline'}}>ews-expand-group</h3> ### ews-expand-group Expands a distribution list to display all members. By default, expands only the first layer of the distribution list. If recursive-expansion is "True", the command expands nested distribution lists and returns all members. #### Permissions Impersonation rights required. In order to perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |email-address|Email address of the group to expand.|Required| |recursive-expansion|Whether to enable recursive expansion. Default is "False".|Optional| #### Outputs There is no context output for this command. #### Examples ``` !ews-expand-group email-address="TestPublic" recursive-expansion="False" ``` ##### Human Readable Output >|displayName|mailbox|mailboxType| >|--- |--- |--- | >|John Wick|john@wick.com|Mailbox| ##### Context Example ``` { "EWS.ExpandGroup": { "name": "TestPublic", "members": [ { "mailboxType": "Mailbox", "displayName": "John Wick", "mailbox": "john@wick.com" } ] } } ``` <h3 style={{display: 'inline'}}>ews-mark-items-as-read</h3> ### ews-mark-items-as-read Marks items as read or unread. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs |**Argument Name**|**Description**|**Required**| |--- |--- |--- | |item-ids|A CSV list of item IDs.|Required| |operation|How to mark the item. Can be "read" or "unread". Default is "read".|Optional| |target-mailbox|The mailbox on which to run the command. If empty, the command will be applied on the default mailbox.|Optional| #### Outputs |**Path**|**Type**|**Description**| |--- |--- |--- | |EWS.Items.action|String|The action that was performed on the item.| |EWS.Items.itemId|String|The ID of the item.| |EWS.Items.messageId|String|The message ID of the item.| #### Examples ``` !ews-mark-items-as-read item-ids=AAMkADQ0NFSffU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAABjKMnpkUAAAfxw+jAAA= operation=read target-mailbox=test@demistodev.onmicrosoft.com ``` ##### Human Readable Output >|action|itemId|messageId| >|--- |--- |--- | >|marked-as-read|AAMkADQ0NFSffU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAABjKMnpkUAAAfxw+jAAA=|| ##### Context Example ``` { "EWS": { "Items": { "action": "marked-as-read", "itemId": "AAMkADQ0NFSffU3wX3aBwCyyVyFtlsUQZfBJebinpkUAAABjKMnpkUAAAfxw+jAAA= ", "messageId": "message_id" } } } ``` <h3 style={{display: 'inline'}}>send-mail</h3> ### send-mail *** Sends an email. #### Base Command `send-mail` #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations When sending the email to an Outlook account, Outlook UI fails to display custom headers. This does not happen when sending to a Gmail account. #### Inputs | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | to | Email addresses for the 'To' field. Supports comma-separated values. | Optional | | cc | Email addresses for the 'Cc' field. Supports comma-separated values. | Optional | | bcc | Email addresses for the 'Bcc' field. Supports comma-separated values. | Optional | | subject | Subject for the email to be sent. | Optional | | body | The contents (body) of the email to be sent in plain text. | Optional | | htmlBody | The contents (body) of the email to be sent in HTML format. | Optional | | attachIDs | A comma-separated list of War Room entry IDs that contain the files to attach to the email. | Optional | | attachNames | A comma-separated list to rename file names of corresponding attachment IDs. For example, rename the first two files - attachNames=file_name1,file_name2. rename first and third file - attachNames=file_name1,,file_name3. | Optional | | attachCIDs | A comma-separated list of CIDs to embed attachments inside the email itself. | Optional | | transientFile | A name for the attached file. You can pass multiple files in a comma-separated list, e.g., transientFile="t1.txt,temp.txt,t3.txt" transientFileContent="test 2,temporary file content,third file content" transientFileCID="t1.txt@xxx.yyy,t2.txt@xxx.zzz". | Optional | | transientFileContent | Content for the attached file. You can pass multiple files in a comma-separated list, e.g., transientFile="t1.txt,temp.txt,t3.txt" transientFileContent="test 2,temporary file content,third file content" transientFileCID="t1.txt@xxx.yyy,t2.txt@xxx.zzz". | Optional | | transientFileCID | CID for the attached file if it's inline. You can pass multiple files in a comma-separated list, e.g., transientFile="t1.txt,temp.txt,t3.txt" transientFileContent="test 2,temporary file content,third file content" transientFileCID="t1.txt@xxx.yyy,t2.txt@xxx.zzz". | Optional | | templateParams | Replace {varname} variables with values from this argument. Expected values are in the form of a JSON document, such ase {"varname": {"value": "some value", "key": "context key"}}. Each var name can either be provided with the value or a context key from which to retrieve the value. Note that only context data is accessible for this argument, while incident fields are not. | Optional | | additionalHeader | A comma-separated list of additional headers in the format: headerName=headerValue. For example: "headerName1=headerValue1,headerName2=headerValue2". | Optional | | raw_message | Raw email message. If provided, all other arguments will be ignored except "to", "cc", and "bcc". | Optional | | from | The email address from which to reply. | Optional | | replyTo | Email addresses that need to be used to reply to the message. Supports comma-separated values. | Optional | | importance | Sets the importance/Priority of the email. Default value is Normal. Possible values are: High, Normal, Low. Default is Normal. | Optional | | handle_inline_image | Whether to handle inline images in the HTML body. When set to 'True', inline images will be extracted from the HTML and attached to the email as an inline attachment object. Note that in some cases, attaching the image as an object may cause the image to disappear when replying to the email. Additionally, sending the image in the html body as base64 data (inline image) may cause the image to disappear if the image is too large or recognized as malicious and subsequently deleted. Possible values are: True, False. Default is True. | Optional | #### Context Output There is no context output for this command. #### Examples ``` !send-mail to=demisto@demisto.onmicrosoft.com subject=some_subject body=some_text attachIDs=110@457,116@457 htmlBody="Hello World" additionalHeader="some_header_name=some_header_value" transientFile=some_file.txt transientFileContent="Some file content" ``` ##### Human Readable Output >Mail sent successfully <h3 style={{display: 'inline'}}>ews-get-items-as-eml</h3> ### ews-get-items-as-eml Retrieves items by item ID and uploads its content as an EML file. #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | item-id | The item ID of item to upload as and EML file. | Required | | target-mailbox | The mailbox in which this email was found. If empty, the default mailbox is used. Otherwise the user might require impersonation rights to this mailbox. | Optional | #### Outputs | **Path** | **Type** | **Description** | | --- | --- | --- | | File.Size | String | The size of the file. | | File.SHA1 | String | The SHA1 hash of the file. | | File.SHA256 | String | The SHA256 hash of the file. | | File.SHA512 | String | The SHA512 hash of the file. | | File.Name | String | The name of the file. | | File.SSDeep | String | The SSDeep hash of the file. | | File.EntryID | String | EntryID of the file | | File.Info | String | Information about the file. | | File.Type | String | The file type. | | File.MD5 | String | The MD5 hash of the file. | | File.Extension | String | The extension of the file. | #### Examples > > `` <h3 style={{display: 'inline'}}>reply-mail</h3> ### reply-mail Reply to an email #### Permissions Impersonation rights are required. To perform actions on the target mailbox of other users, the service account must be part of the `ApplicationImpersonation` role. #### Limitations No known limitations. #### Inputs | **Argument Name** | **Description** | **Required** | | --- | --- | --- | | inReplyTo | ID of the item to reply to. | Required | | to | A comma-separated list of email addresses for the 'to' field. | Required | | cc | A comma-separated list of email addresses for the 'cc' field. | Optional | | bcc | A comma-separated list of email addresses for the 'bcc' field. | Optional | | subject | Subject for the email to be sent. | Optional | | body | The contents (body) of the email to send. | Optional | | htmlBody | HTML formatted content (body) of the email to be sent. This argument overrides the "body" argument. | Optional | | attachIDs | A comma-separated list of War Room entry IDs that contain files, and are used to attach files to the outgoing email. For example: attachIDs=15@8,19@8. | Optional | | attachNames | A comma-separated list of names of attachments to send. Should be the same number of elements as attachIDs. | Optional | | attachCIDs | A comma-separated list of CIDs to embed attachments within the email itself. | Optional | | handle_inline_image | Whether to handle inline images in the HTML body. When set to 'True', inline images are extracted from the HTML and attached to the email as inline attachment objects. Possible values are: True, False. Default is True. NOTE: Sometimes inline images sent in emails may not appear for recipients, either because their email system blocks the image (for example, due to image size or the email is flagged as malicious) or for other reasons. | Optional | #### Outputs There is no context output for this command. #### Examples ```!reply-mail item_id=AAMkAGY3OTQyMzMzLWYxNjktNDE0My05NmZhLWQ5MGY1YjIyNzBkNABGAAAAAACYCKjWAnXBTrnhgWJCcLX7BwDrxRwRjq/zTrN6vWSzK4OWAAAAAAEMAADrxRwRjq/zTrN6vWSzK4OWAAPYQGFeAAA= body=hello subject=hi to="avishai@demistodev.onmicrosoft.com"``` ##### Human Readable Output ##### Sent email >|attachments|from|subject|to| >|---|---|---|---| >| | avishai@demistodev.onmicrosoft.com | hi | avishai@demistodev.onmicrosoft.com | <h3 style={{display: 'inline'}}>ews-auth-reset</h3> ### ews-auth-reset Run this command if for some reason you need to rerun the authentication process. #### Permissions No additional permissions are needed. #### Limitations No known limitations. #### Inputs There is no input for this command. #### Outputs There is no context output for this command. ## Troubleshooting <h3 style={{display: 'inline'}}>Instance Configuration </h3> * If you are encountering the error 'Error in Microsoft authorization. Status: 401, body: invalid_client. Invalid client secret provided. Ensure the secret being sent in the request is the client secret value, not the client secret ID, for a secret added to app', create a new Client Secret for your application in the Azure Portal and use this value instead. <h3 style={{display: 'inline'}}>Instance Configuration </h3> No troubleshooting found. <h3 style={{display: 'inline'}}> Fetch command </h3> * If incidents are not being fetched, verify that no `pre-process` rule is configured that might filter some incidents out. * "address parts cannot contain CR or LF" error message in the logs means a corrupted email might have failed the process. In order to resolve this, you might need to remove the email from the folder being fetched. Contact Support Team if you believe the email is not corrupted. <h3 style={{display: 'inline'}}> Fetching Incidents crash due to unparsable emails </h3> If you find that your fetch incidents command is unable to parse a specific invalid email due to various parsing issues, you can follow these steps: 1. In the instance configuration, navigate to the _Collect_ section and click on _Advanced Settings_. 2. Check the box labeled _Skip unparsable emails during fetch incidents_. By enabling this option, the integration can catch and skip unparsable emails without causing the fetch incidents command to crash. When this parameter is active, a message will appear in the "Fetch History" panel of the instance whenever an unparsable email is recognized and skipped. This allows customers to be informed that a specific email was skipped and gives them the opportunity to open a support ticket if necessary. <h3 style={{display: 'inline'}}> General </h3> * ews-get-searchable-mailboxes: When using the UPN parameter, the command ews-get-searchable-mailboxes runs correctly after assigning RBAC roles requested in the management role header as explained in the [Microsoft Documentation](https://learn.microsoft.com/en-us/Exchange/policy-and-compliance/ediscovery/assign-permissions?redirectedfrom=MSDN&view=exchserver-2019).

Configuration parameters

  • azure_cloud — Azure Cloud
  • _client_id — ID / Application ID
  • _tenant_id — Token / Tenant ID
  • credentials
  • default_target_mailbox — Email Address (required)
  • upn_mailbox — UPN Address
  • folder — Name of the folder from which to fetch incidents
  • access_type — Access Type (Impersonation is deprecated as of February 2025)
  • is_public_folder — Public Folder
  • isFetch — Fetch incidents
  • incidentType — Incident type
  • fetch_time — First fetch timestamp (<number> <time unit>, e.g., 12 hours, 7 days)
  • max_fetch — Maximum number of incidents per fetch (up to 200). Performance might be affected by a value higher than 50.
  • mark_as_read — Mark fetched emails as read
  • request_timeout — Timeout (in seconds) for HTTP requests to Exchange Server
  • insecure — Trust any certificate (not secure)
  • proxy — Use system proxy settings
  • separate_process — Run as a separate process (protects against memory depletion)
  • self_deployed — Use a self deployed Azure Application
  • client_id — ID / Application ID (Deprecated)
  • tenant_id — Token / Tenant ID (Deprecated)
  • client_secret — Key / Application Secret (Deprecated)
  • incidentFetchInterval — Incidents Fetch Interval
  • incidentFilter — What time field should we filter incidents by?
  • legacy_name — Use legacy attachment name
  • skip_unparsable_emails — Skip unparsable emails during fetch incidents

Commands (22)

  • ews-auth-reset

    Run this command if for some reason you need to rerun the authentication process.

  • ews-create-folder

    Creates a new folder in a specified mailbox.

  • ews-delete-attachment

    Deletes the attachments of an item (email message).

  • ews-delete-items

    Delete items from mailbox.

  • 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.

  • 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".

  • 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.

  • ews-get-contacts

    Retrieves contacts for a specified mailbox.

  • ews-get-folder

    Retrieves a single folder.

  • ews-get-items

    Retrieves items by item ID.

  • ews-get-items-as-eml

    Retrieves items by item ID and uploads its content as an EML file.

  • 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.

  • ews-get-out-of-office

    Retrieves the out-of-office status for a specified mailbox.

  • 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.

  • ews-mark-item-as-junk

    Marks an item as junk. This is commonly used to block an email address (meaning all future emails from this sender will be sent to the junk folder). For more information, see the Microsoft documentation: https://msdn.microsoft.com/en-us/library/office/dn481311(v=exchg.150).aspx.

  • ews-mark-items-as-read

    Marks items as read or unread.

  • ews-move-item

    Move an item to different folder in the mailbox.

  • ews-move-item-between-mailboxes

    Moves an item from one mailbox to different mailbox.

  • ews-recover-messages

    Recovers messages that were soft-deleted.

  • 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.

  • reply-mail

    Replies to an email using EWS.

  • send-mail

    Sends an email.

import email
import hashlib
from email import _header_value_parser as parser
from email.policy import SMTP, SMTPUTF8
from io import StringIO
from multiprocessing import Process
from xml.sax import SAXParseException
import chardet
import demistomock as demisto  # noqa: F401
import exchangelib
from CommonServerPython import *  # noqa: F401
from EWSApiModule import *
from exchangelib import (
    DELEGATE,
    OAUTH2,
    Body,
    EWSDateTime,
    EWSTimeZone,
    ExtendedProperty,
    FileAttachment,
    HTMLBody,
    ItemAttachment,
)
from exchangelib.errors import (
    ErrorFolderNotFound,
    ErrorMailboxMoveInProgress,
    ErrorMailboxStoreUnavailable,
    MalformedResponseError,
    RateLimitError,
)
from exchangelib.items import Contact, Message
from requests.exceptions import ConnectionError

# Ignore warnings print to stdout
warnings.filterwarnings("ignore")

""" Constants """
INTEGRATION_NAME = get_integration_name()
APP_NAME = "ms-ews-o365"
FOLDER_ID_LEN = 120
MAX_INCIDENTS_PER_FETCH = 200
FETCH_TIME = demisto.params().get("fetch_time") or "10 minutes"

# move results
MOVED_TO_MAILBOX = "movedToMailbox"
MOVED_TO_FOLDER = "movedToFolder"

# item types
FILE_ATTACHMENT_TYPE = "FileAttachment"
ITEM_ATTACHMENT_TYPE = "ItemAttachment"
ATTACHMENT_TYPE = "attachmentType"

TOIS_PATH = "/root/Top of Information Store/"

# context keys
ATTACHMENT_ORIGINAL_ITEM_ID = "originalItemId"
NEW_ITEM_ID = "newItemId"
MESSAGE_ID = "messageId"
ITEM_ID = "itemId"
MAILBOX = "mailbox"
MAILBOX_ID = "mailboxId"
FOLDER_ID = "id"
TARGET_MAILBOX = "receivedBy"

# context paths
CONTEXT_UPDATE_EWS_ITEM = (
    f"EWS.Items((val.{ITEM_ID} === obj.{ITEM_ID} || "
    f"(val.{MESSAGE_ID} && obj.{MESSAGE_ID} && val.{MESSAGE_ID} === obj.{MESSAGE_ID}))"
    f" && val.{TARGET_MAILBOX} === obj.{TARGET_MAILBOX})"
)

CONTEXT_UPDATE_EWS_ITEM_FOR_ATTACHMENT = f"EWS.Items(val.{ITEM_ID} == obj.{ATTACHMENT_ORIGINAL_ITEM_ID})"
CONTEXT_UPDATE_FOLDER = f"EWS.Folders(val.{FOLDER_ID} == obj.{FOLDER_ID})"

# fetch params
LAST_RUN_TIME = "lastRunTime"
LAST_RUN_IDS = "ids"
LAST_RUN_IDS_DICT_REPRESENTATION = "ids_dict"
LAST_RUN_FOLDER = "folderName"
ERROR_COUNTER = "errorCounter"

# Types of filter
MODIFIED_FILTER = "modified-time"
RECEIVED_FILTER = "received-time"

# headers
ITEMS_RESULTS_HEADERS = [
    "sender",
    "subject",
    "hasAttachments",
    "datetimeReceived",
    "receivedBy",
    "author",
    "toRecipients",
    "textBody",
]

# attachment name param
LEGACY_NAME = argToBoolean(demisto.params().get("legacy_name", False))
UTF_8 = "utf-8"

# If you are modifying this probably also need to modify in other files


def exchangelib_cleanup():  # pragma: no cover
    key_protocols = list(exchangelib.protocol.CachingProtocol._protocol_cache.items())
    try:
        exchangelib.close_connections()
    except Exception as ex:
        demisto.error(f"Error was found in exchangelib cleanup, ignoring: {ex}")
    for key, (protocol, _) in key_protocols:
        try:
            if "thread_pool" in protocol.__dict__:
                demisto.debug(f"terminating thread pool key{key} id: {id(protocol.thread_pool)}")
                protocol.thread_pool.terminate()
                del protocol.__dict__["thread_pool"]
            else:
                demisto.info(f"Thread pool not found (ignoring terminate) in protocol dict: {dir(protocol.__dict__)}")
        except Exception as ex:
            demisto.error(f"Error with thread_pool.terminate, ignoring: {ex}")


""" LOGGING """

log_stream = None
log_handler = None


def start_logging():
    global log_stream
    global log_handler
    logging.raiseExceptions = False
    if log_stream is None:
        log_stream = StringIO()
        log_handler = logging.StreamHandler(stream=log_stream)
        log_handler.setFormatter(logging.Formatter(logging.BASIC_FORMAT))
        logger = logging.getLogger()
        logger.addHandler(log_handler)
        logger.setLevel(logging.DEBUG)


""" Helper Functions """


def get_client_from_params(params: dict) -> EWSClient:
    """
    Parse the integration params and create an EWS client object
    Args:
        params (dict): dict received from demisto.params()

    Returns:
        EWSClient: EWS client object to interact with the exchange API
    """
    client_id = params.get("_client_id") or params.get("client_id", "")
    client_secret = (params.get("credentials") or {}).get("password") or params.get("client_secret", "")
    tenant_id = params.get("_tenant_id") or params.get("tenant_id", "")
    if not client_secret:
        raise Exception("Key / Application Secret must be provided.")
    elif not client_id:
        raise Exception("ID / Application ID must be provided.")
    elif not tenant_id:
        raise Exception("Token / Tenant ID must be provided.")

    access_type = params.get("access_type", DELEGATE) or DELEGATE
    access_type = (access_type[0] if isinstance(access_type, list) else access_type).lower()
    default_target_mailbox = params.get("default_target_mailbox", "")
    max_fetch = min(int(params.get("max_fetch", MAX_INCIDENTS_PER_FETCH)), MAX_INCIDENTS_PER_FETCH)
    azure_cloud = get_azure_cloud(params, INTEGRATION_NAME)
    ews_server = f"{azure_cloud.endpoints.exchange_online}/EWS/Exchange.asmx/"
    folder = params.get("folder", "Inbox")
    is_public_folder = argToBoolean(params.get("is_public_folder", False))
    request_timeout = int(params.get("request_timeout", 120))
    mark_as_read = params.get("mark_as_read", False)
    incident_filter = IncidentFilter(
        params.get("incidentFilter", IncidentFilter.RECEIVED_FILTER) or IncidentFilter.RECEIVED_FILTER
    )
    self_deployed = argToBoolean(params.get("self_deployed", False))
    insecure = argToBoolean(params.get("insecure", False))
    proxy = params.get("proxy", False)

    return EWSClient(
        client_id=client_id,
        client_secret=client_secret,
        access_type=access_type,
        default_target_mailbox=default_target_mailbox,
        max_fetch=max_fetch,
        ews_server=ews_server,
        auth_type=OAUTH2,
        version="O365",
        folder=folder,
        is_public_folder=is_public_folder,
        request_timeout=request_timeout,
        mark_as_read=mark_as_read,
        incident_filter=incident_filter,
        azure_cloud=azure_cloud,
        tenant_id=tenant_id,
        self_deployed=self_deployed,
        app_name=APP_NAME,
        insecure=insecure,
        proxy=proxy,
    )


def get_attachment_name(attachment_name, eml_extension=False, content_id="", is_inline=False):
    """
    Retrieve attachment name or error string if none is provided
    :param attachment_name: attachment name to retrieve
    :param eml_extension: Indicates whether the eml extension should be added
    :return: string
    """
    if is_inline and content_id and content_id != "None" and not LEGACY_NAME:
        if attachment_name is None or attachment_name == "":
            return (
                f"{content_id}-attachmentName-demisto_untitled_attachment.eml"
                if eml_extension
                else f"{content_id}-attachmentName-demisto_untitled_attachment"
            )
        elif eml_extension and not attachment_name.endswith(".eml"):
            return f"{content_id}-attachmentName-{attachment_name}.eml"
        return f"{content_id}-attachmentName-{attachment_name}"
    if attachment_name is None or attachment_name == "":
        return "demisto_untitled_attachment.eml" if eml_extension else "demisto_untitled_attachment"
    elif eml_extension and not attachment_name.endswith(".eml"):
        return f"{attachment_name}.eml"
    return attachment_name


def prepare_args(args: dict[str, str]) -> dict:
    """
    Prepare arguments to be used as the API expects it
    :param args: demisto args
    :return: transformed args
    """
    args = {k.replace("-", "_"): v for k, v in list(args.items())}
    if "is_public" in args:
        args["is_public"] = args["is_public"] == "True"  # type: ignore[assignment]
    if "from" in args:
        args["from_address"] = args.pop("from")
    return args


def get_limited_number_of_messages_from_qs(qs, limit):  # pragma: no cover
    """
    Retrieve a limited number of messages from query search
    :param qs: query search to execute
    :param limit: limit on number of items to retrieve from search
    :return: list of exchangelib.Message
    """
    count = 0
    results = []
    for item in qs:
        if count == limit:
            break
        if isinstance(item, Message):
            count += 1
            results.append(item)
    return results


def keys_to_camel_case(value):  # pragma: no cover
    """
    Transform keys from snake to camel case (does nothing if no snakes are found)
    :param value: value to transform
    :return: transformed value
    """

    def str_to_camel_case(snake_str):
        components = snake_str.split("_")
        return components[0] + "".join(x.title() for x in components[1:])

    if value is None:
        return None
    if isinstance(value, list | set):
        return list(map(keys_to_camel_case, value))
    if isinstance(value, dict):
        return {
            keys_to_camel_case(k): keys_to_camel_case(v) if isinstance(v, list | dict) else v for (k, v) in list(value.items())
        }

    return str_to_camel_case(value)


def get_last_run(client: EWSClient, last_run=None):
    """
    Retrieve the last run time
    :param client: EWS Client
    :param last_run: (Optional) last run object
    :return: last run dict
    """
    if not last_run or last_run.get(LAST_RUN_FOLDER) != client.folder_name:
        last_run = {
            LAST_RUN_TIME: None,
            LAST_RUN_FOLDER: client.folder_name,
            LAST_RUN_IDS_DICT_REPRESENTATION: {},
        }
    if last_run.get(LAST_RUN_TIME):
        last_run[LAST_RUN_TIME] = EWSDateTime.from_string(last_run[LAST_RUN_TIME])

    # In case we have existing last_run data
    if not last_run.get(LAST_RUN_IDS_DICT_REPRESENTATION):
        if not last_run.get(LAST_RUN_IDS):
            last_run[LAST_RUN_IDS_DICT_REPRESENTATION] = {}
        else:
            last_run[LAST_RUN_IDS_DICT_REPRESENTATION] = {item: "" for item in last_run.get(LAST_RUN_IDS)}
            last_run[LAST_RUN_IDS].clear()

    return last_run


def email_ec(item):
    """
    Create entry context for an email
    :param item: exchangelib.Item
    :return: entry context dict
    """
    return {
        "CC": None if not item.cc_recipients else [mailbox.email_address for mailbox in item.cc_recipients],
        "BCC": None if not item.bcc_recipients else [mailbox.email_address for mailbox in item.bcc_recipients],
        "To": None if not item.to_recipients else [mailbox.email_address for mailbox in item.to_recipients],
        "From": item.author.email_address,
        "Subject": item.subject,
        "Text": item.text_body,
        "HTML": item.body,
        "HeadersMap": None if not item.headers else {header.name: header.value for header in item.headers},
    }


def is_item_duplicate(item, exclude_ids, incident_filter):
    """
    Checks if an item is a duplicate based on ID and Timestamp.

    RFC 5322 defines Message-ID values as ``<id@domain>``, but in practice the same
    message may appear in different forms across fetches — such as ``id@domain``,
    ``<id@domain>``, ``id@domain>``, or ``<id@domain`` — and they might change between fetches.
    To avoid duplicate incidents, we verify all possible forms in exclude_ids.

    Features:
    1. Smart ID Lookup: Checks both Clean ID (abc) and Bracketed ID (<abc>, <abc, abc>).
    2. Legacy Handling: Handles cases where stored value is "" (if last run is list not dict).
    3. Timestamp Logic: Compares stored time vs item time.

    Returns:
        tuple[bool, str | None]: A tuple containing:
            - is_duplicate (bool): True if item is a duplicate (skip it), False if it should be processed.
            - stored_time (str | None): The stored fetch time for the item, or None if not found/not a duplicate.
    """
    if not item.message_id or not exclude_ids:
        return False, None

    clean_id = item.message_id.strip().strip("<>")

    found_key = None
    for candidate in (clean_id, f"<{clean_id}>", f"{clean_id}>", f"<{clean_id}"):
        if candidate in exclude_ids:
            found_key = candidate
            break

    if found_key is None:
        return False, None
    demisto.debug(f"Dedup match: {item.message_id=}, {found_key=}, {clean_id=}")
    stored_time = exclude_ids[found_key]

    # If stored_time is "" or None, it means it was from an old fetch (List format).
    # We treat it as an existing duplicate and SKIP it.
    if not stored_time:
        return True, stored_time

    current_item_time = (
        item.datetime_created.ewsformat() if incident_filter == RECEIVED_FILTER else item.last_modified_time.ewsformat()
    )
    return stored_time >= current_item_time, stored_time


def parse_item_as_dict(item, email_address=None, camel_case=False, compact_fields=False):  # pragma: no cover
    """
    Parses an exchangelib item as a dict
    :param item: exchangelib.Item to parse
    :param (Optional) email_address: string mailbox
    :param (Optional) camel_case: Is camel case
    :param (Optional) compact_fields: Is compact fields
    :return: Item as a dict
    """

    def parse_object_as_dict(obj):
        raw_dict = {}
        if obj is not None:
            for field in obj.FIELDS:
                raw_dict[field.name] = getattr(obj, field.name, None)
        return raw_dict

    def parse_folder_as_json(folder):  # pragma: no cover
        raw_dict = parse_object_as_dict(folder)
        if "parent_folder_id" in raw_dict:
            raw_dict["parent_folder_id"] = parse_folder_as_json(raw_dict["parent_folder_id"])
        if "effective_rights" in raw_dict:
            raw_dict["effective_rights"] = parse_object_as_dict(raw_dict["effective_rights"])
        return raw_dict

    raw_dict = {}
    for field, value in item._field_vals():
        if type(value) in [str, str, int, float, bool, Body, HTMLBody, None]:
            raw_dict[field] = value
    raw_dict["id"] = item.id
    demisto.debug(f"checking for attachments in email with id {item.id}")
    if getattr(item, "attachments", None):
        raw_dict["attachments"] = [parse_attachment_as_dict(item.id, x) for x in item.attachments]

    for time_field in [
        "datetime_sent",
        "datetime_created",
        "datetime_received",
        "last_modified_time",
        "reminder_due_by",
    ]:
        value = getattr(item, time_field, None)
        if value:
            raw_dict[time_field] = value.ewsformat()

    for dict_field in [
        "effective_rights",
        "parent_folder_id",
        "conversation_id",
        "author",
        "extern_id",
        "received_by",
        "received_representing",
        "reply_to",
        "sender",
        "folder",
    ]:
        value = getattr(item, dict_field, None)
        if value:
            if isinstance(value, list):
                raw_dict[dict_field] = []
                for single_val in value:
                    raw_dict[dict_field].append(parse_object_as_dict(single_val))
            else:
                raw_dict[dict_field] = parse_object_as_dict(value)

    for list_dict_field in ["headers", "cc_recipients", "to_recipients"]:
        value = getattr(item, list_dict_field, None)
        if value:
            raw_dict[list_dict_field] = [parse_object_as_dict(x) for x in value]

    for list_str_field in ["categories"]:
        value = getattr(item, list_str_field, None)
        if value:
            raw_dict[list_str_field] = value

    if getattr(item, "folder", None):
        raw_dict["folder"] = parse_folder_as_json(item.folder)
        folder_path = (
            item.folder.absolute[len(TOIS_PATH) :] if item.folder.absolute.startswith(TOIS_PATH) else item.folder.absolute
        )
        raw_dict["folder_path"] = folder_path

    if compact_fields:
        new_dict = {}
        # noinspection PyListCreation
        fields_list = [
            "datetime_created",
            "datetime_received",
            "datetime_sent",
            "sender",
            "has_attachments",
            "importance",
            "message_id",
            "last_modified_time",
            "size",
            "subject",
            "text_body",
            "headers",
            "body",
            "folder_path",
            "is_read",
            "categories",
        ]

        if "id" in raw_dict:
            new_dict["itemId"] = raw_dict["id"]
            fields_list.append("itemId")

        for field in fields_list:
            if field in raw_dict:
                new_dict[field] = raw_dict.get(field)
        for field in ["received_by", "author", "sender"]:
            if field in raw_dict:
                new_dict[field] = raw_dict.get(field, {}).get("email_address")
        for field in ["to_recipients"]:
            if field in raw_dict:
                new_dict[field] = [x.get("email_address") for x in raw_dict[field]]
        attachments = raw_dict.get("attachments")
        if attachments and len(attachments) > 0:
            file_attachments = [x for x in attachments if x[ATTACHMENT_TYPE] == FILE_ATTACHMENT_TYPE]
            if len(file_attachments) > 0:
                new_dict["FileAttachments"] = file_attachments
            item_attachments = [x for x in attachments if x[ATTACHMENT_TYPE] == ITEM_ATTACHMENT_TYPE]
            if len(item_attachments) > 0:
                new_dict["ItemAttachments"] = item_attachments

        raw_dict = new_dict

    if camel_case:
        raw_dict = keys_to_camel_case(raw_dict)

    if email_address:
        raw_dict[MAILBOX] = email_address
    return raw_dict


def get_entry_for_file_attachment(item_id, attachment):
    """
    Creates a file entry for an attachment
    :param item_id: item_id of the attachment
    :param attachment: attachment dict
    :return: file entry dict for attachment
    """
    entry = fileResult(
        get_attachment_name(attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline),
        attachment.content,
    )
    entry["EntryContext"] = {
        CONTEXT_UPDATE_EWS_ITEM_FOR_ATTACHMENT + CONTEXT_UPDATE_FILE_ATTACHMENT: parse_attachment_as_dict(item_id, attachment)
    }
    return entry


def parse_attachment_as_dict(item_id, attachment):
    """
    Creates a note entry for an attachment
    :param item_id: item_id of the attachment
    :param attachment: attachment dict
    :return: note entry dict for attachment
    """
    try:
        attachment_content = attachment.content if isinstance(attachment, FileAttachment) else attachment.item.mime_content
        if isinstance(attachment_content, str):  # Strings must be encoded before hashing
            attachment_content = attachment_content.encode()
        return {
            ATTACHMENT_ORIGINAL_ITEM_ID: item_id,
            ATTACHMENT_ID: attachment.attachment_id.id,
            "attachmentName": get_attachment_name(
                attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline
            ),
            "attachmentSHA256": hashlib.sha256(attachment_content).hexdigest() if attachment_content else None,
            "attachmentContentType": attachment.content_type,
            "attachmentContentId": attachment.content_id,
            "attachmentContentLocation": attachment.content_location,
            "attachmentSize": attachment.size,
            "attachmentLastModifiedTime": attachment.last_modified_time.ewsformat(),
            "attachmentIsInline": attachment.is_inline,
            ATTACHMENT_TYPE: FILE_ATTACHMENT_TYPE if isinstance(attachment, FileAttachment) else ITEM_ATTACHMENT_TYPE,
        }
    except TypeError as e:
        if str(e) != "must be string or buffer, not None":
            raise
        return {
            ATTACHMENT_ORIGINAL_ITEM_ID: item_id,
            ATTACHMENT_ID: attachment.attachment_id.id,
            "attachmentName": get_attachment_name(
                attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline
            ),
            "attachmentSHA256": None,
            "attachmentContentType": attachment.content_type,
            "attachmentContentId": attachment.content_id,
            "attachmentContentLocation": attachment.content_location,
            "attachmentSize": attachment.size,
            "attachmentLastModifiedTime": attachment.last_modified_time.ewsformat(),
            "attachmentIsInline": attachment.is_inline,
            ATTACHMENT_TYPE: FILE_ATTACHMENT_TYPE if isinstance(attachment, FileAttachment) else ITEM_ATTACHMENT_TYPE,
        }


def get_entry_for_item_attachment(item_id, attachment, target_email):  # pragma: no cover
    """
    Creates a note entry for an item attachment
    :param item_id: Item id
    :param attachment: exchangelib attachment
    :param target_email: target email
    :return: note entry dict for item attachment
    """
    item = attachment.item
    dict_result = parse_attachment_as_dict(item_id, attachment)
    dict_result.update(parse_item_as_dict(item, target_email, camel_case=True, compact_fields=True))
    title = (
        f'EWS get attachment got item for "{target_email}", '
        f'"{get_attachment_name(attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline)}"'  # noqa: E501
    )

    return get_entry_for_object(
        title,
        CONTEXT_UPDATE_EWS_ITEM_FOR_ATTACHMENT + CONTEXT_UPDATE_ITEM_ATTACHMENT,
        dict_result,
    )


""" Command Functions """


def fetch_attachments_for_message(client: EWSClient, args: dict):  # pragma: no cover
    """
    Fetches attachments for a message
    :param client: EWS Client
    :param args: dict of command arguments
    :return: list of parsed entries
    """
    item_id = args.get("item_id", "")
    target_mailbox = args.get("target_mailbox")
    attachment_ids = args.get("attachment_ids")
    identifiers_filter = args.get("identifiers_filter", "")
    identifiers_filter = argToList(identifiers_filter)
    attachment_ids = argToList(attachment_ids)
    account = client.get_account(target_mailbox)
    attachments = client.get_attachments_for_item(item_id, account, attachment_ids)
    entries = []
    for attachment in attachments:
        if isinstance(attachment, FileAttachment):
            try:
                if attachment.content:
                    entries.append(get_entry_for_file_attachment(item_id, attachment))
            except TypeError as e:
                if str(e) != "must be string or buffer, not None":
                    raise
        else:
            entries.append(get_entry_for_item_attachment(item_id, attachment, account.primary_smtp_address))
            if attachment.item.mime_content:
                entries.append(
                    fileResult(
                        get_attachment_name(
                            attachment_name=attachment.name,
                            eml_extension=True,
                            content_id=attachment.content_id,
                            is_inline=attachment.is_inline,
                        ),
                        attachment.item.mime_content,
                    )
                )

    return entries


def search_items_in_mailbox(client: EWSClient, args: dict):  # pragma: no cover
    """
    Search items in mailbox
    :param client: EWS Client
    :param args: dict of command arguments
    :return: Output tuple
    """
    query = args.get("query")
    message_id = args.get("message_id")
    folder_path = args.get("folder_path", "")
    limit = args.get("limit", 100)
    target_mailbox = args.get("target_mailbox")
    is_public = args.get("is_public")
    selected_fields = args.get("selected_fields", "all")
    if not query and not message_id:
        return_error("Missing required argument. Provide query or message-id")

    if message_id and message_id[0] != "<" and message_id[-1] != ">":
        message_id = f"<{message_id}>"

    account = client.get_account(target_mailbox)
    limit = int(limit)
    if folder_path.lower() == "inbox":
        folders = [account.inbox]
    elif folder_path:
        is_public = client.is_default_folder(folder_path, is_public)
        folders = [client.get_folder_by_path(folder_path, account, is_public)]
    else:
        folders = account.inbox.parent.walk()  # pylint: disable=E1101

    items = []  # type: ignore
    selected_all_fields = selected_fields == "all"

    if selected_all_fields:
        restricted_fields = [x.name for x in Message.FIELDS]
    else:
        restricted_fields = set(argToList(selected_fields))  # type: ignore
        restricted_fields.update(["id", "message_id"])  # type: ignore

    for folder in folders:
        if Message not in folder.supported_item_models:
            continue
        if query:
            items_qs = folder.filter(query).only(*restricted_fields)
        else:
            items_qs = folder.filter(message_id=message_id).only(*restricted_fields)
        items += get_limited_number_of_messages_from_qs(items_qs, limit)
        if len(items) >= limit:
            break

    items = items[:limit]
    searched_items_result = [
        parse_item_as_dict(
            item,
            account.primary_smtp_address,
            camel_case=True,
            compact_fields=selected_all_fields,
        )
        for item in items
    ]

    if not selected_all_fields:
        searched_items_result = [
            {k: v for (k, v) in i.items() if k in keys_to_camel_case(restricted_fields)} for i in searched_items_result
        ]

        for item in searched_items_result:
            item["itemId"] = item.pop("id", "")

    hr_items = escape_hr_item_ids([{**item} for item in searched_items_result])
    readable_output = tableToMarkdown(
        "Searched items",
        hr_items,
        headers=ITEMS_RESULTS_HEADERS if selected_all_fields else None,
    )
    output = {CONTEXT_UPDATE_EWS_ITEM: searched_items_result}
    return readable_output, output, searched_items_result


def get_contacts(client: EWSClient, args: dict):  # pragma: no cover
    """
    Retrieve contacts of the target mailbox or client mailbox
    :param client: EWS Client
    :param args: dict of command arguments
    :return:
    """
    limit = args.get("limit", 100)
    target_mailbox = args.get("target_mailbox")

    def parse_physical_address(address):
        result = {}
        for attr in ["city", "country", "label", "state", "street", "zipcode"]:
            result[attr] = getattr(address, attr, None)
        return result

    def parse_phone_number(phone_number):
        result = {}
        for attr in ["label", "phone_number"]:
            result[attr] = getattr(phone_number, attr, None)
        return result

    def parse_contact(contact):
        contact_dict = {
            k: v if not isinstance(v, EWSDateTime) else v.ewsformat()
            for k, v in list(contact._field_vals())
            if isinstance(v, str | EWSDateTime)
        }
        if isinstance(contact, Contact) and contact.physical_addresses:
            contact_dict["physical_addresses"] = list(map(parse_physical_address, contact.physical_addresses))
        if isinstance(contact, Contact) and contact.phone_numbers:
            contact_dict["phone_numbers"] = list(map(parse_phone_number, contact.phone_numbers))
        if isinstance(contact, Contact) and contact.email_addresses and len(contact.email_addresses) > 0:
            contact_dict["emailAddresses"] = [x.email for x in contact.email_addresses]
        contact_dict = keys_to_camel_case(contact_dict)
        contact_dict = {k: v for k, v in list(contact_dict.items()) if v}
        contact_dict.pop("mimeContent", None)
        contact_dict["originMailbox"] = target_mailbox
        return contact_dict

    account = client.get_account(target_mailbox)
    contacts = []

    for contact in account.contacts.all()[: int(limit)]:  # pylint: disable=E1101
        contacts.append(parse_contact(contact))
    readable_output = tableToMarkdown(f"Email contacts for {target_mailbox}", contacts)
    output = {"Account.Email(val.Address == obj.originMailbox).EwsContacts": contacts}
    return readable_output, output, contacts


def find_folders(client: EWSClient, args: dict):
    """
    Finds folders in the mailbox
    :param client: EWS Client
    :param args: dict of command arguments
    :return: Output tuple
    """
    target_mailbox = args.get("target_mailbox")
    account = client.get_account(target_mailbox)
    root = account.root

    if client.is_public_folder:
        root = account.public_folders_root
    folders = []
    for f in root.walk():  # pylint: disable=E1101
        folder = folder_to_context_entry(f)
        folders.append(folder)
    folders_tree = root.tree()  # pylint: disable=E1101
    readable_output = folders_tree
    output = {"EWS.Folders(val.id == obj.id)": folders}
    return readable_output, output, folders


def get_items_from_folder(client: EWSClient, args: dict):  # pragma: no cover
    """
    Retrieve items from folder path
    :param client: EWS Client
    :param args: dict of command arguments
    :return: Output tuple
    """
    folder_path = args.get("folder_path", "")
    limit = args.get("limit", 100)
    target_mailbox = args.get("target_mailbox")
    is_public = args.get("is_public")
    get_internal_item = args.get("get_internal_item", "no")
    account = client.get_account(target_mailbox)
    limit = int(limit)
    get_internal_item = get_internal_item == "yes"
    is_public = client.is_default_folder(folder_path, is_public)
    folder = client.get_folder_by_path(folder_path, account, is_public)
    qs = folder.filter().order_by("-datetime_created")[:limit]
    items = get_limited_number_of_messages_from_qs(qs, limit)
    items_result = []

    for item in items:
        item_attachment = parse_item_as_dict(item, account.primary_smtp_address, camel_case=True, compact_fields=True)
        for attachment in item.attachments:
            if get_internal_item and isinstance(attachment, ItemAttachment) and isinstance(attachment.item, Message):
                # if found item attachment - switch item to the attchment
                item_attachment = parse_item_as_dict(
                    attachment.item,
                    account.primary_smtp_address,
                    camel_case=True,
                    compact_fields=True,
                )
                break
        items_result.append(item_attachment)

    hm_headers = [
        "sender",
        "subject",
        "hasAttachments",
        "datetimeReceived",
        "receivedBy",
        "author",
        "toRecipients",
        "itemId",
    ]
    hr_items = escape_hr_item_ids([{**item} for item in items_result])
    readable_output = tableToMarkdown("Items in folder " + folder_path, hr_items, headers=hm_headers)
    output = {CONTEXT_UPDATE_EWS_ITEM: items_result}
    return readable_output, output, items_result


def get_items(client: EWSClient, args: dict):  # pragma: no cover
    """
    Get items from target mailbox or client mailbox
    :param client: EWS Client
    :param args: dict of command arguments
    :return:
    """
    item_ids = args.get("item_ids", "")
    target_mailbox = args.get("target_mailbox")
    item_ids = argToList(item_ids)
    account = client.get_account(target_mailbox)
    items = client.get_items_from_mailbox(account, item_ids)
    items = [x for x in items if isinstance(x, Message)]
    items_as_incidents = [parse_incident_from_item(x) for x in items]
    items_to_context = [parse_item_as_dict(x, account.primary_smtp_address, True, True) for x in items]
    hr_items = escape_hr_item_ids([{**item} for item in items_to_context])
    readable_output = tableToMarkdown("Get items", hr_items, ITEMS_RESULTS_HEADERS)
    output = {
        CONTEXT_UPDATE_EWS_ITEM: items_to_context,
        "Email": [email_ec(item) for item in items],
    }
    return readable_output, output, items_as_incidents


def collect_manual_attachments(manualAttachObj):  # pragma: no cover
    """Collect all manual attachments' data

    Args:
        manualAttachObj (str): String representation of the manually attached files list.

    Returns:
        List[Dict]. List of the files data.
    """
    manually_attached_objects = argToList(manualAttachObj)

    attachments = []
    for attachment in manually_attached_objects:
        file_res = demisto.getFilePath(os.path.basename(attachment["RealFileName"]))

        path = file_res["path"]

        with open(path, "rb") as fp:
            data = fp.read()

        attachments.append({"name": attachment["FileName"], "data": data, "cid": ""})

    return attachments


def collect_attachments(attachments_ids, attachments_cids, attachments_names):  # pragma: no cover
    """Collect all attachments' data

    Args:
        attachments_ids (str): String representation of the files ids list.
        attachments_cids (str): String representation of the files content ids list.
        attachments_names (str): String representation of the files names list.

    Returns:
        List[Dict]. List of the files data.
    """
    attachments = []

    files_ids = argToList(attachments_ids)
    files_cids = argToList(attachments_cids)
    files_names = argToList(attachments_names)

    for index, file_id in enumerate(files_ids):
        try:
            file_res = demisto.getFilePath(file_id)
            path = file_res["path"]

            filename = files_names[index] if len(files_names) > index and files_names[index] else file_res["name"]

            cid = files_cids[index] if len(files_cids) > index and files_cids[index] else ""

            with open(path, "rb") as fp:
                data = fp.read()

            attachments.append({"name": filename, "data": data, "cid": cid})

        except Exception as e:
            demisto.error(f"Invalid entry {file_id} with exception: {e}")
            return_error(f"Entry {file_id} is not valid or is not a file entry")

    return attachments


def handle_transient_files(transient_files, transient_files_contents, transient_files_cids):
    """Creates the transient attachments data

    Args:
        transient_files (str): String representation of the transient files names list.
        transient_files_contents (str): String representation of the transient files content list.
        transient_files_cids (str): String representation of the transient files content ids list.

    Returns:
        List[Dict]. List of the transient files data.
    """
    transient_attachments = []

    files_names = argToList(transient_files)
    files_contents = argToList(transient_files_contents)
    files_cids = argToList(transient_files_cids)

    for index in range(len(files_names)):
        file_name = files_names[index]

        if index >= len(files_contents):
            break

        file_content = bytes(files_contents[index], UTF_8)

        file_cid = "" if index >= len(files_cids) else files_cids[index]

        transient_attachments.append({"name": file_name, "data": file_content, "cid": file_cid})

    return transient_attachments


def handle_template_params(template_params):  # pragma: no cover
    """Translates the template params if they exist from the context

    Args:
        template_params (str): JSON string that represent the variables names to be replaced and the desired value.
                                Value can be either real value or context key to fetch the value from.

    Returns:
        Dict. `variable_name: value_to_use` of the templated parameters.
    """
    actual_params = {}

    if template_params:
        try:
            params = json.loads(template_params)

            for p in params:
                if params[p].get("value"):
                    actual_params[p] = params[p]["value"]
                elif params[p].get("key"):
                    actual_params[p] = demisto.dt(demisto.context(), params[p]["key"])
        except ValueError as e:
            return_error(f"Unable to parse template_params: {e!s}")

    return actual_params


def create_message_object(to, cc, bcc, subject, body, additional_headers, from_address, reply_to, importance):
    """Creates the message object according to the existence of additional custom headers."""
    if additional_headers:
        return Message(
            to_recipients=to,
            author=from_address,
            cc_recipients=cc,
            bcc_recipients=bcc,
            subject=subject,
            reply_to=reply_to,
            body=body,
            importance=importance,
            **additional_headers,
        )

    return Message(
        to_recipients=to,
        author=from_address,
        cc_recipients=cc,
        bcc_recipients=bcc,
        subject=subject,
        reply_to=reply_to,
        body=body,
        importance=importance,
    )


def create_message(
    to,
    handle_inline_image: bool = True,
    subject="",
    body="",
    bcc=None,
    cc=None,
    html_body=None,
    attachments=[],
    additional_headers=None,
    from_address=None,
    reply_to=None,
    importance=None,
):  # pragma: no cover
    """Creates the Message object that will be sent.

    Args:
        to (list): Main recipients.
        cc (list): CC recipients.
        bcc (list): BCC recipients.
        subject (str): Email's subject.
        body (str): Email's simple text body.
        html_body (str): Email's html body.
        attachments (list): Files to be attached to the mail, both inline and as files.
        additional_headers (Dict): Custom headers to be added to the message.
        from_address (str): The email address from which to reply.
        reply_to (list): Email addresses that need to be used to reply to the message.
        handle_inline_image (bool): Whether to handle inline images in the HTML body.
    Returns:
        Message. Message object ready to be sent.
    """
    demisto.debug(f"create_message: Received {len(attachments)} attachments, {handle_inline_image=}")
    if not html_body:
        # This is a simple text message - we cannot have CIDs here
        message = create_message_object(to, cc, bcc, subject, body, additional_headers, from_address, reply_to, importance)

        for attachment in attachments:
            if not attachment.get("cid"):
                new_attachment = FileAttachment(name=attachment.get("name"), content=attachment.get("data"))
                message.attach(new_attachment)

    else:
        html_attachments: list = []
        if handle_inline_image:
            html_body, html_attachments = handle_html(html_body)
            attachments += html_attachments
            demisto.debug(f"create_message: Processed HTML body with {len(attachments)} attachments")
        message = create_message_object(
            to, cc, bcc, subject, HTMLBody(html_body), additional_headers, from_address, reply_to, importance
        )

        for attachment in attachments:
            if not isinstance(attachment, FileAttachment):
                if not attachment.get("cid"):
                    attachment = FileAttachment(name=attachment.get("name"), content=attachment.get("data"))
                else:
                    attachment = FileAttachment(
                        name=attachment.get("name"),
                        content=attachment.get("data"),
                        is_inline=True,
                        content_id=attachment.get("cid"),
                    )

            message.attach(attachment)

    return message


def add_additional_headers(additional_headers):
    """Adds custom headers to the Message object

    Args:
        additional_headers (str): Headers list as string. Example: headerName1=headerValue1,headerName2=headerValue2

    Returns:
        Dict. Headers dictionary in the form of: `header_name: header value`
    """
    headers = {}

    for header in argToList(additional_headers):
        header_name, header_value = header.split("=", 1)

        class TempClass(ExtendedProperty):
            distinguished_property_set_id = "InternetHeaders"
            property_name = header_name
            property_type = "String"

        try:
            Message.register(header_name, TempClass)
            headers[header_name] = header_value
        except ValueError:
            Message.deregister(header_name)
            try:
                Message.register(header_name, TempClass)
                headers[header_name] = header_value
            except ValueError as e:
                demisto.debug("EWSO365 - Header " + header_name + " could not be registered. " + str(e))

    return headers


def send_email(client: EWSClient, args: dict):  # pragma: no cover
    to = argToList(args.get("to"))
    subject = args.get("subject", "")
    body = args.get("body", "")
    bcc = argToList(args.get("bcc"))
    cc = argToList(args.get("cc"))
    htmlBody = args.get("htmlBody")
    attachIDs = args.get("attachIDs", "")
    attachCIDs = args.get("attachCIDs", "")
    attachNames = args.get("attachNames", "")
    manualAttachObj = args.get("manualAttachObj")
    transientFile = args.get("transientFile")
    transientFileContent = args.get("transientFileContent")
    transientFileCID = args.get("transientFileCID")
    templateParams = args.get("templateParams")
    additionalHeader = args.get("additionalHeader")
    raw_message = args.get("raw_message")
    from_address = args.get("from_address")
    reply_to = argToList(args.get("replyTo"))
    importance = args.get("importance")
    render_body = argToBoolean(args.get("renderBody", False))
    handle_inline_image = argToBoolean(args.get("handle_inline_image", True))

    # Basic validation - we allow pretty much everything but you have to have at least a recipient
    # We allow messages without subject and also without body
    if not to and not cc and not bcc:
        return_error("You must have at least one recipient")

    if raw_message:
        message = Message(
            to_recipients=to,
            cc_recipients=cc,
            bcc_recipients=bcc,
            body=raw_message,
            author=from_address,
            reply_to=reply_to,
            importance=importance,
        )

    else:
        if additionalHeader:
            additionalHeader = add_additional_headers(additionalHeader)

        # collect all types of attachments
        attachments = collect_attachments(attachIDs, attachCIDs, attachNames)
        attachments.extend(collect_manual_attachments(manualAttachObj))
        attachments.extend(handle_transient_files(transientFile, transientFileContent, transientFileCID))

        # update body and html_body with the templated params, if exists
        template_params = handle_template_params(templateParams)
        if template_params:
            body = body.format(**template_params)
            if htmlBody:
                htmlBody = htmlBody.format(**template_params)

        message = create_message(
            to,
            handle_inline_image,
            subject,
            body,
            bcc,
            cc,
            htmlBody,
            attachments,
            additionalHeader,
            from_address,
            reply_to,
            importance,
        )

    client.send_email(message)

    results = [CommandResults(entry_type=EntryType.NOTE, raw_response="Mail sent successfully")]
    if render_body:
        results.append(
            CommandResults(
                entry_type=EntryType.NOTE,
                content_format=EntryFormat.HTML,
                raw_response=htmlBody,
            )
        )

    return results


def reply_mail(client: EWSClient, args: dict):  # pragma: no cover
    to = argToList(args.get("to", ""))
    inReplyTo = args.get("inReplyTo", "")
    subject = args.get("subject", "")
    body = args.get("body", "")
    bcc = argToList(args.get("bcc"))
    cc = argToList(args.get("cc"))
    htmlBody = args.get("htmlBody")
    attachIDs = args.get("attachIDs", "")
    attachCIDs = args.get("attachCIDs", "")
    attachNames = args.get("attachNames", "")
    manualAttachObj = args.get("manualAttachObj")
    handle_inline_image: bool = argToBoolean(args.get("handle_inline_image", True))
    # collect all types of attachments
    attachments = collect_attachments(attachIDs, attachCIDs, attachNames)
    attachments.extend(collect_manual_attachments(manualAttachObj))
    client.reply_email(
        in_reply_to=inReplyTo,
        to=to,
        body=body,
        subject=subject,
        bcc=bcc,
        cc=cc,
        html_body=htmlBody,
        attachments=attachments,
        handle_inline_image=handle_inline_image,
    )


def get_item_as_eml(client: EWSClient, args: dict):  # pragma: no cover
    """
    Retrieve item as an eml
    :param client: EWS Client
    :param args: dict of command arguments
    :return: Output tuple
    """
    item_id = args.get("item_id", "")
    target_mailbox = args.get("target_mailbox")
    account = client.get_account(target_mailbox)
    item = client.get_item_from_mailbox(account, item_id)

    if item.mime_content:
        email_content = cast_mime_item_to_message(item)
        if item.headers:
            # compare header keys case-insensitive
            attached_email_headers = [
                (h.lower(), " ".join(map(str.strip, v.split("\r\n")))) for (h, v) in list(email_content.items())
            ]
            for header in item.headers:
                if (
                    header.name.lower(),
                    header.value,
                ) not in attached_email_headers and header.name.lower() != "content-type":
                    try:
                        email_content.add_header(header.name, header.value)
                    except ValueError as err:
                        if "There may be at most" not in str(err):
                            raise err

        eml_name = item.subject if item.subject else "demisto_untitled_eml"
        email_data = decode_email_data(email_content)
        file_result = fileResult(eml_name + ".eml", email_data)
        file_result = file_result if file_result else "Failed uploading eml file to war room"

        return file_result
    return None


def handle_attached_email_with_incorrect_message_id(attached_email: Message):
    """This function handles a malformed Message-ID value which can be returned in the header of certain email objects.
    This issue happens due to a current bug in "email" library and further explained in XSUP-32074.
    Public issue link: https://github.com/python/cpython/issues/105802

    The function will run on every attached email if exists, check its Message-ID header value and fix it if possible.
    Args:
        attached_email (Message): attached email object.

    Returns:
        Message: attached email object.
    """
    message_id_value = ""
    for i in range(len(attached_email._headers)):
        if attached_email._headers[i][0].lower() == "message-id":
            message_id = attached_email._headers[i][1]
            message_header = attached_email._headers[i][0]
            demisto.debug(f"Handling Message-ID header, {message_id=}.")
            try:
                message_id_value = handle_incorrect_message_id(message_id)
                if message_id_value != message_id:
                    # If the Message-ID header was fixed in the context of this function
                    # the header will be replaced in _headers list
                    attached_email._headers.pop(i)
                    attached_email._headers.append((message_header, message_id_value))

            except Exception as e:
                # The function is designed to handle a specific format error for the Message-ID header
                # as explained in the docstring.
                # That being said, we do expect the header to be in a known format.
                # If this function encounters a header format which is not in the known format and can't be fixed,
                # the header will be ignored completely to prevent crashing the fetch command.
                demisto.debug(f"Invalid {message_id=}, Error: {e}")
                break
            break
    return attached_email


def handle_attached_email_with_incorrect_from_header(attached_email: Message):
    """This function handles a malformed From value which can be returned in the header of certain email objects.
    This issue happens due to a current bug in "email" library.
    Public issue link: https://github.com/python/cpython/issues/114906

    The function will run on every attached email if exists, check its From header value and fix it if possible.
    Args:
        attached_email (Message): attached email object.

    Returns:
        Message: attached email object.
    """
    for i, (header_name, header_value) in enumerate(attached_email._headers):
        if header_name.lower() == "from":
            demisto.debug(f"Handling From header, value={header_value}.")
            try:
                new_value = parser.get_address_list(header_value)[0].value
                new_value = new_value.replace("\n", " ").replace("\r", " ").strip()
                if header_value != new_value:
                    # Update the 'From' header with the corrected value
                    attached_email._headers[i] = (header_name, new_value)
                    demisto.debug(f"From header fixed, new value: {new_value}")

            except Exception as e:
                demisto.debug(f"Error processing From header: {e}")
            break
    return attached_email


def handle_incorrect_message_id(message_id: str) -> str:
    """
    Use regex to identify and correct one of the following invalid message_id formats:
    1. '<[message_id]>' --> '<message_id>'
    2. '\r\n\t<[message_id]>' --> '\r\n\t<message_id>'
    If no necessary changes identified the original 'message_id' argument value is returned.
    """
    if re.search(r"\<\[.*\]\>", message_id):
        # find and replace "<[" with "<" and "]>" with ">"
        fixed_message_id = re.sub(r"<\[(.*?)\]>", r"<\1>", message_id)
        demisto.debug("Fixed message id {message_id} to {fixed_message_id}")
        return fixed_message_id
    return message_id


def decode_email_data(email_obj: Message):
    attached_email_bytes = email_obj.as_bytes()
    chardet_detection = chardet.detect(attached_email_bytes)
    encoding = chardet_detection.get("encoding", "utf-8") or "utf-8"
    try:
        # Trying to decode using the detected encoding
        data = attached_email_bytes.decode(encoding)
    except UnicodeDecodeError:
        # In case the detected encoding fails apply the default encoding
        demisto.info(
            f'Could not decode attached email using detected encoding: {encoding}, retrying '
            f'using utf-8.\nAttached email details: '
            f'\nMessage-ID = {email_obj.get("Message-ID")}'
            f'\nDate = {email_obj.get("Date")}'
            f'\nSubject = {email_obj.get("Subject")}'
            f'\nFrom = {email_obj.get("From")}'
            f'\nTo = {email_obj.get("To")}'
        )
        try:
            data = attached_email_bytes.decode("utf-8")
        except UnicodeDecodeError:
            demisto.info("Could not decode attached email using utf-8. returned the content without decoding")
            data = attached_email_bytes  # type: ignore

    return data


def cast_mime_item_to_message(item):
    mime_content = item.mime_content
    email_policy = SMTP if mime_content.isascii() else SMTPUTF8

    if isinstance(mime_content, str) and not mime_content.isascii():
        mime_content = mime_content.encode()

    if isinstance(mime_content, bytes):
        message = email.message_from_bytes(mime_content, policy=email_policy)  # type: ignore[arg-type]
    else:
        message = email.message_from_string(mime_content, policy=email_policy)  # type: ignore[arg-type]

    return message


def parse_incident_from_item(item):  # pragma: no cover
    """
    Parses an incident from an item
    :param item: item to parse
    :return: Parsed item
    """
    incident = {}
    labels = []
    demisto.debug(f"starting to parse the email with id {item.id} into an incident")
    try:
        incident["details"] = item.text_body or item.body
    except AttributeError:
        incident["details"] = item.body
    incident["name"] = item.subject
    labels.append({"type": "Email/subject", "value": item.subject})
    incident["occurred"] = item.datetime_received.ewsformat()

    # handle recipients
    if item.to_recipients:
        for recipient in item.to_recipients:
            labels.append({"type": "Email", "value": recipient.email_address})

    # handle cc
    if item.cc_recipients:
        for recipient in item.cc_recipients:
            labels.append({"type": "Email/cc", "value": recipient.email_address})
    # handle email from
    if item.sender:
        labels.append({"type": "Email/from", "value": item.sender.email_address})
        labels.append({"type": "Email/from/name", "value": item.sender.name})

    # email format
    email_format = ""
    try:
        if item.text_body:
            labels.append({"type": "Email/text", "value": item.text_body})
            email_format = "text"
    except AttributeError:
        pass
    if item.body:
        labels.append({"type": "Email/html", "value": item.body})
        email_format = "HTML"
    labels.append({"type": "Email/format", "value": email_format})

    # handle attachments
    if item.attachments:
        incident["attachment"] = []
        demisto.debug(f"parsing {len(item.attachments)} attachments for item with id {item.id}")
        attachment_counter = 0
        for attachment in item.attachments:
            attachment_counter += 1
            demisto.debug(f"retrieving attachment number {attachment_counter} of email with id {item.id}")
            file_result = None
            label_attachment_type = None
            label_attachment_id_type = None
            if isinstance(attachment, FileAttachment):
                try:
                    if attachment.content:
                        # file attachment
                        label_attachment_type = "attachments"
                        label_attachment_id_type = "attachmentId"

                        # save the attachment
                        file_name = get_attachment_name(
                            attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline
                        )
                        demisto.debug(
                            f"saving content number {attachment_counter}, "
                            f"of size {sys.getsizeof(attachment.content)}, of email with id {item.id}"
                        )
                        file_result = fileResult(file_name, attachment.content)

                        # check for error
                        if file_result["Type"] == entryTypes["error"]:
                            demisto.error(file_result["Contents"])
                            raise Exception(file_result["Contents"])

                        # save attachment to incident
                        incident["attachment"].append(
                            {
                                "path": file_result["FileID"],
                                "name": get_attachment_name(
                                    attachment_name=attachment.name,
                                    content_id=attachment.content_id,
                                    is_inline=attachment.is_inline,
                                ),
                                "description": FileAttachmentType.ATTACHED if not attachment.is_inline else "",
                            }
                        )
                except TypeError as e:
                    if str(e) != "must be string or buffer, not None":
                        raise
                    continue
                except SAXParseException as e:
                    # TODO: When a fix is released, we will need to bump the library version.
                    #  https://github.com/ecederstrand/exchangelib/issues/1200
                    demisto.debug(
                        f"An XML error occurred while loading an attachments content."
                        f"\nMessage ID is {item.id}"
                        f"\nError: {e.getMessage()}"
                    )
                    continue
            else:
                # other item attachment
                label_attachment_type = "attachmentItems"
                label_attachment_id_type = "attachmentItemsId"

                # save the attachment
                if attachment.item.mime_content:
                    attached_email = cast_mime_item_to_message(attachment.item)
                    if attachment.item.headers:
                        # compare header keys case-insensitive
                        attached_email_headers = []
                        attached_email = handle_attached_email_with_incorrect_message_id(attached_email)
                        attached_email = handle_attached_email_with_incorrect_from_header(attached_email)
                        for h, v in attached_email.items():
                            if not isinstance(v, str):
                                try:
                                    v = str(v)
                                except:  # noqa: E722
                                    demisto.debug(f'cannot parse the header "{h}"')
                                    continue

                            v = " ".join(map(str.strip, v.split("\r\n")))
                            attached_email_headers.append((h.lower(), v))
                        demisto.debug(f"{attached_email_headers=}")
                        for header in attachment.item.headers:
                            if (
                                header.name.lower(),
                                header.value,
                            ) not in attached_email_headers and header.name.lower() != "content-type":
                                try:
                                    if header.name.lower() == "message-id":
                                        """ Handle a case where a Message-ID header was NOT already in attached_email,
                                        and instead is coming from attachment.item.headers.
                                        Meaning it wasn't handled in handle_attached_email_with_incorrect_message_id function
                                        and instead it is handled here using handle_incorrect_message_id function."""
                                        correct_message_id = handle_incorrect_message_id(header.value)
                                        if (header.name.lower(), correct_message_id) not in attached_email_headers:
                                            attached_email.add_header(header.name, correct_message_id)
                                    else:
                                        attached_email.add_header(header.name, header.value)
                                except ValueError as err:
                                    if "There may be at most" not in str(err):
                                        raise err

                    data = decode_email_data(attached_email)
                    file_result = fileResult(
                        get_attachment_name(
                            attachment_name=attachment.name,
                            eml_extension=True,
                            content_id=attachment.content_id,
                            is_inline=attachment.is_inline,
                        ),
                        data,
                    )

                if file_result:
                    # check for error
                    if file_result["Type"] == entryTypes["error"]:
                        demisto.error(file_result["Contents"])
                        raise Exception(file_result["Contents"])

                    # save attachment to incident
                    incident["attachment"].append(
                        {
                            "path": file_result["FileID"],
                            "name": get_attachment_name(
                                attachment_name=attachment.name,
                                eml_extension=True,
                                content_id=attachment.content_id,
                                is_inline=attachment.is_inline,
                            ),
                        }
                    )

            labels.append(
                {
                    "type": label_attachment_type,
                    "value": get_attachment_name(
                        attachment_name=attachment.name, content_id=attachment.content_id, is_inline=attachment.is_inline
                    ),
                }
            )
            labels.append({"type": label_attachment_id_type, "value": attachment.attachment_id.id})
        demisto.debug(f"finished parsing attachment {attachment_counter} of email with id {item.id}")

    # handle headers
    if item.headers:
        headers = []
        for header in item.headers:
            labels.append(
                {
                    "type": f"Email/Header/{header.name}",
                    "value": str(header.value),
                }
            )
            headers.append(f"{header.name}: {header.value}")
        labels.append({"type": "Email/headers", "value": "\r\n".join(headers)})

    # handle item id
    if item.message_id:
        labels.append({"type": "Email/MessageId", "value": str(item.message_id)})

    if item.id:
        labels.append({"type": "Email/ID", "value": item.id})
        labels.append({"type": "Email/itemId", "value": item.id})

    # handle conversion id
    if item.conversation_id:
        labels.append({"type": "Email/ConversionID", "value": item.conversation_id.id})

    incident["labels"] = labels
    demisto.debug(f"Starting to generate rawJSON for incident, from email with id {item.id}")
    incident["rawJSON"] = json.dumps(parse_item_as_dict(item, None), ensure_ascii=False)
    demisto.debug(f"Finished generating rawJSON from email with id {item.id}")

    return incident


def fetch_emails_as_incidents(client: EWSClient, last_run, incident_filter, skip_unparsable_emails: bool = False):
    """
    Fetch incidents
    :param client: EWS Client
    :param last_run: last run dict
    :return:
    """
    last_run = get_last_run(client, last_run)
    demisto.debug(f"get_last_run: {last_run=}")
    last_fetch_time = last_run.get(LAST_RUN_TIME)
    excluded_ids = last_run.get(LAST_RUN_IDS_DICT_REPRESENTATION)
    try:
        last_emails = fetch_last_emails(
            client,
            client.folder_name,
            last_run.get(LAST_RUN_TIME),
            excluded_ids,
            incident_filter,
        )

        incidents = []
        incident: dict[str, str] = {}
        emails_ids = []  # Used for mark emails as read
        demisto.debug(f"{APP_NAME} - Started fetch with {len(last_emails)} at {last_fetch_time}")
        current_fetch_ids = {}
        last_modification_time = last_fetch_time

        if isinstance(last_modification_time, EWSDateTime):
            last_modification_time = last_modification_time.ewsformat()

        for item in last_emails:
            try:
                if item.message_id:
                    current_fetch_ids[item.message_id] = (
                        item.datetime_created.ewsformat()
                        if incident_filter == RECEIVED_FILTER
                        else item.last_modified_time.ewsformat()
                    )
                    incident = parse_incident_from_item(item)
                    incidents.append(incident)

                    if incident_filter == MODIFIED_FILTER:
                        item_modified_time = item.last_modified_time.ewsformat()
                        if last_modification_time is None or last_modification_time < item_modified_time:
                            last_modification_time = item_modified_time

                    if item.id and item.is_read is False:
                        emails_ids.append(item.id)

                    if len(incidents) >= client.max_fetch:
                        break
                else:
                    demisto.debug(f"Skipped item: item with no message_id {item=}")
            except Exception as e:
                if not skip_unparsable_emails:  # default is to raise and exception and fail the command
                    raise

                # when the skip param is `True`, we log the exceptions and move on instead of failing the whole fetch
                error_msg = (
                    "Encountered email parsing issue while fetching. "
                    f"Skipping item with message id: {item.message_id or '<error parsing message_id>'}"
                )
                demisto.debug(f"{error_msg}, Error: {e!s} {traceback.format_exc()}")
                demisto.updateModuleHealth(error_msg, is_error=False)

        demisto.debug(f"{APP_NAME} - ending fetch - got {len(incidents)} incidents.")

        if incident_filter == MODIFIED_FILTER:
            last_incident_run_time = last_modification_time
        else:  # default case - using 'received' time
            last_incident_run_time = incident.get("occurred", last_fetch_time)

        # making sure both last fetch time and the time of most recent incident are the same type for comparing.
        if isinstance(last_incident_run_time, EWSDateTime):
            last_incident_run_time = last_incident_run_time.ewsformat()

        if isinstance(last_fetch_time, EWSDateTime):
            last_fetch_time = last_fetch_time.ewsformat()

        demisto.debug(
            f"#### last_incident_time: {last_incident_run_time}({type(last_incident_run_time)})."
            f"last_fetch_time: {last_fetch_time}({type(last_fetch_time)}) ####"
        )

        # If the fetch query is not fully fetched (we didn't have any time progress) - then we keep the
        # id's from current fetch until progress is made. This is for when max_fetch < incidents_from_query.
        if not last_incident_run_time or not last_fetch_time or last_incident_run_time > last_fetch_time:
            ids = current_fetch_ids
        else:
            ids = excluded_ids | current_fetch_ids

        new_last_run = {
            LAST_RUN_TIME: last_incident_run_time,
            LAST_RUN_FOLDER: client.folder_name,
            LAST_RUN_IDS_DICT_REPRESENTATION: ids,
            ERROR_COUNTER: 0,
        }

        demisto.debug(f"Set last run to: {new_last_run=}")
        demisto.setLastRun(new_last_run)

        if client.mark_as_read:
            mark_item_as_read(client, {"item_ids": emails_ids})

        return incidents

    except RateLimitError:
        if LAST_RUN_TIME in last_run:
            last_run[LAST_RUN_TIME] = last_run[LAST_RUN_TIME].ewsformat()
        if ERROR_COUNTER not in last_run:
            last_run[ERROR_COUNTER] = 0
        last_run[ERROR_COUNTER] += 1
        demisto.setLastRun(last_run)
        if last_run[ERROR_COUNTER] > 2:
            raise
        return []


def fetch_last_emails(
    client: EWSClient, folder_name="Inbox", since_datetime=None, exclude_ids=None, incident_filter=RECEIVED_FILTER
):
    """
    Fetches last emails
    :param client: EWS client
    :param (Optional) folder_name: folder name to pull from
    :param (Optional) since_datetime: items will be searched after this datetime
    :param (Optional) exclude_ids: exclude ids from fetch
    :return: list of exchangelib.Items
    """
    qs = client.get_folder_by_path(folder_name, is_public=client.is_public_folder)
    demisto.debug(f"Finished getting the folder named {folder_name} by path")
    if since_datetime:
        if incident_filter == RECEIVED_FILTER:
            qs = qs.filter(datetime_received__gte=since_datetime)
        else:
            qs = qs.filter(last_modified_time__gte=since_datetime)
    else:
        tz = EWSTimeZone("UTC")
        first_fetch_datetime = dateparser.parse(FETCH_TIME)
        assert first_fetch_datetime is not None
        first_fetch_ews_datetime = EWSDateTime.from_datetime(first_fetch_datetime.replace(tzinfo=tz))
        demisto.debug(f"{first_fetch_ews_datetime=}")
        if incident_filter == RECEIVED_FILTER:
            qs = qs.filter(datetime_received__gte=first_fetch_ews_datetime)
        else:
            qs = qs.filter(last_modified_time__gte=first_fetch_ews_datetime)

    qs = qs.filter().only(*[x.name for x in Message.FIELDS if x.name.lower() != "mime_content"])
    if incident_filter == RECEIVED_FILTER:
        qs = qs.filter().order_by("datetime_received")
    else:
        qs = qs.filter().order_by("last_modified_time")

    result = []
    exclude_ids = exclude_ids if exclude_ids else {}
    demisto.debug(f"{APP_NAME} - Exclude ID list: {exclude_ids}")
    qs.chunk_size = min(client.max_fetch, 100)
    qs.page_size = min(client.max_fetch, 100)
    demisto.debug("Before iterating on queryset")
    demisto.debug(f"Size of the queryset object in fetch-incidents: {sys.getsizeof(qs)}")
    for item in qs:
        demisto.debug("next iteration of the queryset in fetch-incidents")
        if isinstance(item, Message):
            is_duplicate, previous_fetch_time = is_item_duplicate(item, exclude_ids, incident_filter)
            if is_duplicate:
                received_time = item.datetime_created.ewsformat()
                modified_time = item.last_modified_time.ewsformat()
                demisto.debug(
                    f"{item.subject=} with {item.message_id=} was excluded. previous fetch time: "
                    f"{previous_fetch_time}, (if no time - because of the transition from list to dict). "
                    f"current fetch time: {received_time if incident_filter == RECEIVED_FILTER else modified_time}"
                )
                continue
            demisto.debug(f"Appending {item.subject=} with {item.message_id=}")
            result.append(item)
            if len(result) >= client.max_fetch:
                break
    demisto.debug(f"{APP_NAME} - Got total of {len(result)} from ews query.")
    return result


def test_module(client: EWSClient, max_fetch):  # pragma: no cover
    """
    test-module
    * Max incidents per fetch <= MAX_INCIDENTS_PER_FETCH
    * Account can be retrieved
    * Account has read rights
    * Test access to fetch folder
    :param client: EWS Client
    :param max_fetch: Max fetches per incident
    :return: "ok"
    """
    try:
        if int(max_fetch) > MAX_INCIDENTS_PER_FETCH:
            return_error(
                f"Error - Max incidents per fetch cannot be greater than {MAX_INCIDENTS_PER_FETCH}. You provided: {max_fetch}"
            )
        account = client.get_account()
        if not account.root.effective_rights.read:  # pylint: disable=E1101
            raise Exception(
                "Success to authenticate, but user has no permissions to read from the mailbox. "
                "Need to delegate the user permissions to the mailbox - "
                "please read integration documentation and follow the instructions"
            )
        client.get_folder_by_path(client.folder_name, account, client.is_public_folder).test_access()
    except ErrorFolderNotFound as e:
        if "Top of Information Store" in str(e):
            raise Exception(
                "Success to authenticate, but user probably has no permissions to read from the specific folder."
                "Check user permissions. You can try !ews-find-folders command to "
                "get all the folders structure that the user has permissions to"
            )
    except Exception as e:
        if "403" in str(e):
            error_message_simple = (
                "Got invalid response with status code: 403."
                " Please make sure you have the right permissions to your application.\n"
            )
            raise DemistoException(error_message_simple)
        raise DemistoException(str(e))
    return "ok"


def sub_main():  # pragma: no cover
    is_test_module = False
    params = demisto.params()
    args = prepare_args(demisto.args())
    # client's default_target_mailbox is the authorization source for the instance
    params["default_target_mailbox"] = args.get(
        "target_mailbox", args.get("source_mailbox", params.get("default_target_mailbox", ""))
    )
    if params.get("upn_mailbox") and not (args.get("target_mailbox")):
        params["default_target_mailbox"] = params.get("upn_mailbox", "")

    if params.get("access_type") == "Impersonation":
        demisto.info(
            "Note: The access type Impersonation you are using is deprecated. For more information, "
            "please refer to the integration description."
        )
    try:
        client = get_client_from_params(params)
        start_logging()
        # replace sensitive access_token value in logs
        if not isinstance(client.credentials, CustomDomainOAuth2Credentials):  # Should not fail
            raise DemistoException("Failed to initialize EWS Client properly, check credentials")

        add_sensitive_log_strs(client.credentials.access_token.get("access_token", ""))
        command = demisto.command()
        # commands that return a single note result
        normal_commands = {
            "ews-get-searchable-mailboxes": get_searchable_mailboxes,
            "ews-move-item": move_item,
            "ews-delete-items": delete_items,
            "ews-search-mailbox": search_items_in_mailbox,
            "ews-get-contacts": get_contacts,
            "ews-get-out-of-office": get_out_of_office_state,
            "ews-recover-messages": recover_soft_delete_item,
            "ews-create-folder": create_folder,
            "ews-mark-item-as-junk": mark_item_as_junk,
            "ews-find-folders": find_folders,
            "ews-get-items-from-folder": get_items_from_folder,
            "ews-get-items": get_items,
            "ews-get-folder": get_folder,
            "ews-expand-group": get_expanded_group,
            "ews-mark-items-as-read": mark_item_as_read,
            "ews-delete-attachment": delete_attachments_for_message,
            "ews-get-attachment": fetch_attachments_for_message,
        }

        # commands that may return multiple results or non-note result
        special_output_commands = {
            "ews-get-items-as-eml": get_item_as_eml,
            "reply-mail": reply_mail,
        }
        # system commands:
        if command == "test-module":
            is_test_module = True
            demisto.results(test_module(client, params.get("max_fetch")))
        elif command == "fetch-incidents":
            last_run = demisto.getLastRun()
            incident_filter = params.get("incidentFilter", RECEIVED_FILTER)
            if incident_filter not in [RECEIVED_FILTER, MODIFIED_FILTER]:  # Ensure it's one of the allowed filter values
                incident_filter = RECEIVED_FILTER  # or if not, force it to the default, RECEIVED_FILTER
            skip_unparsable_emails: bool = argToBoolean(params.get("skip_unparsable_emails", False))
            demisto.debug(f"{incident_filter=}, {skip_unparsable_emails=}")
            incidents = fetch_emails_as_incidents(client, last_run, incident_filter, skip_unparsable_emails)
            demisto.debug(f"Saving incidents with size {sys.getsizeof(incidents)}, len:{len(incidents)}")
            demisto.incidents(incidents)

        elif command == "send-mail":
            commands_res = send_email(client, args)
            return_results(commands_res)

        elif command == "ews-move-item-between-mailboxes":
            # Needs a client configured for each one of the mailboxes
            dest_params = params.copy()
            dest_params["default_target_mailbox"] = args.get("destination_mailbox")
            dest_client = get_client_from_params(dest_params)
            return_results(
                move_item_between_mailboxes(
                    src_client=client,
                    args=args,
                    dest_client=dest_client,
                )
            )

        # special outputs commands
        elif command in special_output_commands:
            demisto.results(special_output_commands[command](client, args))  # type: ignore[operator]

        elif command == "ews-auth-reset":
            return_results(reset_auth())

        # normal commands
        else:
            output = normal_commands[command](client, args)  # type: ignore[operator]
            if isinstance(output, tuple):  # Legacy, some commands return a tuple for return outputs
                return_outputs(*output)
            else:
                return_results(output)

    except Exception as e:
        demisto.error(f"got exception {e}")
        start_logging()
        debug_log = log_stream.getvalue()  # type: ignore[union-attr]
        error_message_simple = ""

        # Office365 regular maintenance case
        if isinstance(e, ErrorMailboxStoreUnavailable | ErrorMailboxMoveInProgress):
            log_message = (
                "Office365 is undergoing load balancing operations. As a result, the service is temporarily unavailable."
            )
            if demisto.command() == "fetch-incidents":
                demisto.info(log_message)
                demisto.incidents([])
                sys.exit(0)
            if is_test_module:
                demisto.results(log_message + " Please retry the instance configuration test.")
                sys.exit(0)
            error_message_simple = log_message + " Please retry your request."

        if isinstance(e, ConnectionError):
            error_message_simple = f"Could not connect to the server.\nAdditional information: {e!s}"
        else:
            if is_test_module and isinstance(e, MalformedResponseError):
                error_message_simple = "Got invalid response from the server.\n"

        # Legacy error handling
        if "Status code: 401" in debug_log:
            error_message_simple = "Got unauthorized from the server. "

        if "Status code: 503" in debug_log:
            error_message_simple = "Got timeout from the server. Probably the server is not reachable with the current settings. "

        if not error_message_simple:
            error_message = error_message_simple = str(e)
        else:
            error_message = error_message_simple + "\n" + str(e)

        stacktrace = traceback.format_exc()
        if stacktrace:
            error_message += "\nFull stacktrace:\n" + stacktrace

        if debug_log:
            error_message += "\nFull debug log:\n" + debug_log

        if demisto.command() == "fetch-incidents":
            raise
        if demisto.command() == "ews-search-mailbox" and isinstance(e, ValueError):
            return_error(
                message="Selected invalid field, please specify valid field name.",
                error=e,
            )
        if is_test_module:
            demisto.results(error_message_simple)
        else:
            demisto.results(
                {
                    "Type": entryTypes["error"],
                    "ContentsFormat": formats["text"],
                    "Contents": error_message_simple,
                }
            )
        demisto.error(f"{e.__class__.__name__}: {error_message}")
    finally:
        exchangelib_cleanup()
        if log_stream:
            try:
                logging.getLogger().removeHandler(log_handler)  # type: ignore
                log_stream.close()
            except Exception as ex:
                demisto.error(f"EWS: unexpected exception when trying to remove log handler: {ex}")


def process_main():
    """setup stdin to fd=0 so we can read from the server"""
    sys.stdin = os.fdopen(0, "r")
    sub_main()


def main():  # pragma: no cover
    # When running big queries, like 'ews-search-mailbox' the memory might not be freed by the garbage
    # collector. `separate_process` flag will run the integration on a separate process that will prevent
    # memory leakage.
    separate_process = demisto.params().get("separate_process", False)
    demisto.debug(f"Running as separate_process: {separate_process}")
    if separate_process:
        try:
            p = Process(target=process_main)
            p.start()
            p.join()
            demisto.debug("subprocess finished")
        except Exception as ex:
            demisto.error(f"Failed starting Process: {ex}")
    else:
        sub_main()


if __name__ in ("__main__", "__builtin__", "builtins"):
    main()